Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ import { Editor } from "../../util/editor"
import stripAnsi from "strip-ansi"
import { Footer } from "./footer.tsx"
import { usePromptRef } from "../../context/prompt"
import { Filesystem } from "@/util/filesystem"

addDefaultParsers(parsers.parsers)

Expand Down Expand Up @@ -1414,7 +1415,10 @@ ToolRegistry.register<typeof WriteTool>({
return props.input.content
})

const diagnostics = createMemo(() => props.metadata.diagnostics?.[props.input.filePath ?? ""] ?? [])
const diagnostics = createMemo(() => {
const filePath = Filesystem.normalizePath(props.input.filePath ?? "")
return props.metadata.diagnostics?.[filePath] ?? []
})

return (
<>
Expand Down Expand Up @@ -1587,7 +1591,8 @@ ToolRegistry.register<typeof EditTool>({
const diffContent = createMemo(() => props.metadata.diff ?? props.permission["diff"])

const diagnostics = createMemo(() => {
const arr = props.metadata.diagnostics?.[props.input.filePath ?? ""] ?? []
const filePath = Filesystem.normalizePath(props.input.filePath ?? "")
const arr = props.metadata.diagnostics?.[filePath] ?? []
return arr.filter((x) => x.severity === 1).slice(0, 3)
})

Expand Down
22 changes: 13 additions & 9 deletions packages/opencode/src/lsp/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type { LSPServer } from "./server"
import { NamedError } from "@opencode-ai/util/error"
import { withTimeout } from "../util/timeout"
import { Instance } from "../project/instance"
import { Filesystem } from "../util/filesystem"

export namespace LSPClient {
const log = Log.create({ service: "lsp.client" })
Expand Down Expand Up @@ -47,14 +48,15 @@ export namespace LSPClient {

const diagnostics = new Map<string, Diagnostic[]>()
connection.onNotification("textDocument/publishDiagnostics", (params) => {
const path = fileURLToPath(params.uri)
const filePath = Filesystem.normalizePath(fileURLToPath(params.uri))
l.info("textDocument/publishDiagnostics", {
path,
path: filePath,
count: params.diagnostics.length,
})
const exists = diagnostics.has(path)
diagnostics.set(path, params.diagnostics)
const exists = diagnostics.has(filePath)
diagnostics.set(filePath, params.diagnostics)
if (!exists && input.serverID === "typescript") return
Bus.publish(Event.Diagnostics, { path, serverID: input.serverID })
Bus.publish(Event.Diagnostics, { path: filePath, serverID: input.serverID })
})
connection.onRequest("window/workDoneProgress/create", (params) => {
l.info("window/workDoneProgress/create", params)
Expand Down Expand Up @@ -181,14 +183,16 @@ export namespace LSPClient {
return diagnostics
},
async waitForDiagnostics(input: { path: string }) {
input.path = path.isAbsolute(input.path) ? input.path : path.resolve(Instance.directory, input.path)
log.info("waiting for diagnostics", input)
const normalizedPath = Filesystem.normalizePath(
path.isAbsolute(input.path) ? input.path : path.resolve(Instance.directory, input.path),
)
log.info("waiting for diagnostics", { path: normalizedPath })
let unsub: () => void
return await withTimeout(
new Promise<void>((resolve) => {
unsub = Bus.subscribe(Event.Diagnostics, (event) => {
if (event.properties.path === input.path && event.properties.serverID === result.serverID) {
log.info("got diagnostics", input)
if (event.properties.path === normalizedPath && event.properties.serverID === result.serverID) {
log.info("got diagnostics", { path: normalizedPath })
unsub?.()
resolve()
}
Expand Down
18 changes: 8 additions & 10 deletions packages/opencode/src/tool/edit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,16 +140,14 @@ export const EditTool = Tool.define("edit", {
let output = ""
await LSP.touchFile(filePath, true)
const diagnostics = await LSP.diagnostics()
for (const [file, issues] of Object.entries(diagnostics)) {
if (issues.length === 0) continue
if (file === filePath) {
const errors = issues.filter((item) => item.severity === 1)
const limited = errors.slice(0, MAX_DIAGNOSTICS_PER_FILE)
const suffix =
errors.length > MAX_DIAGNOSTICS_PER_FILE ? `\n... and ${errors.length - MAX_DIAGNOSTICS_PER_FILE} more` : ""
output += `\nThis file has errors, please fix\n<file_diagnostics>\n${limited.map(LSP.Diagnostic.pretty).join("\n")}${suffix}\n</file_diagnostics>\n`
continue
}
const normalizedFilePath = Filesystem.normalizePath(filePath)
const issues = diagnostics[normalizedFilePath] ?? []
if (issues.length > 0) {
const errors = issues.filter((item) => item.severity === 1)
const limited = errors.slice(0, MAX_DIAGNOSTICS_PER_FILE)
const suffix =
errors.length > MAX_DIAGNOSTICS_PER_FILE ? `\n... and ${errors.length - MAX_DIAGNOSTICS_PER_FILE} more` : ""
output += `\nThis file has errors, please fix\n<file_diagnostics>\n${limited.map(LSP.Diagnostic.pretty).join("\n")}${suffix}\n</file_diagnostics>\n`
}

const filediff: Snapshot.FileDiff = {
Expand Down
3 changes: 2 additions & 1 deletion packages/opencode/src/tool/write.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,14 +80,15 @@ export const WriteTool = Tool.define("write", {
let output = ""
await LSP.touchFile(filepath, true)
const diagnostics = await LSP.diagnostics()
const normalizedFilepath = Filesystem.normalizePath(filepath)
let projectDiagnosticsCount = 0
for (const [file, issues] of Object.entries(diagnostics)) {
if (issues.length === 0) continue
const sorted = issues.toSorted((a, b) => (a.severity ?? 4) - (b.severity ?? 4))
const limited = sorted.slice(0, MAX_DIAGNOSTICS_PER_FILE)
const suffix =
issues.length > MAX_DIAGNOSTICS_PER_FILE ? `\n... and ${issues.length - MAX_DIAGNOSTICS_PER_FILE} more` : ""
if (file === filepath) {
if (file === normalizedFilepath) {
output += `\nThis file has errors, please fix\n<file_diagnostics>\n${limited.map(LSP.Diagnostic.pretty).join("\n")}${suffix}\n</file_diagnostics>\n`
continue
}
Expand Down
14 changes: 14 additions & 0 deletions packages/opencode/src/util/filesystem.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,21 @@
import { realpathSync } from "fs"
import { exists } from "fs/promises"
import { dirname, join, relative } from "path"

export namespace Filesystem {
/**
* On Windows, normalize a path to its canonical casing using the filesystem.
* This is needed because Windows paths are case-insensitive but LSP servers
* may return paths with different casing than what we send them.
*/
export function normalizePath(p: string): string {
if (process.platform !== "win32") return p
try {
return realpathSync.native(p)
} catch {
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The normalizePath function silently returns the original path when an error occurs. This could hide important issues like permission errors or invalid paths. Consider logging the error for debugging purposes, or at least distinguishing between "path doesn't exist" (which is acceptable) and other errors that might indicate a real problem. This is especially important for a Windows-specific fix where understanding why normalization failed could be crucial for troubleshooting.

Suggested change
} catch {
} catch (err: any) {
// If the error is "path does not exist", return original path silently.
if (err && (err.code === "ENOENT" || err.code === "ENOTDIR")) {
return p
}
// Otherwise, log the error for debugging.
console.warn(`[Filesystem.normalizePath] Failed to normalize path "${p}":`, err)

Copilot uses AI. Check for mistakes.
return p
}
}
Comment on lines +11 to +18
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new normalizePath function lacks test coverage. Given that this function is critical for Windows path handling and is used throughout the codebase for LSP diagnostics matching, it should have tests covering: (1) Windows behavior when paths exist, (2) Windows behavior when paths don't exist, (3) non-Windows platforms returning unchanged paths, and (4) edge cases like paths with different casing on Windows. The project uses Bun's test framework and has existing test coverage for other utilities and LSP functionality.

Copilot uses AI. Check for mistakes.
export function overlaps(a: string, b: string) {
const relA = relative(a, b)
const relB = relative(b, a)
Expand Down
Loading