From d0ae6a5fd4d7ec937a417275d57cfa99f5baac22 Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Tue, 16 Dec 2025 09:39:31 +1000 Subject: [PATCH 1/2] wip --- .../src/cli/cmd/tui/routes/session/index.tsx | 22 +++++++++++++++++-- packages/opencode/src/lsp/client.ts | 7 +++++- packages/opencode/src/tool/edit.ts | 4 +++- packages/opencode/src/tool/write.ts | 4 +++- 4 files changed, 32 insertions(+), 5 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 48f7db05426..089955a6291 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -1414,7 +1414,16 @@ ToolRegistry.register({ return props.input.content }) - const diagnostics = createMemo(() => props.metadata.diagnostics?.[props.input.filePath ?? ""] ?? []) + const diagnostics = createMemo(() => { + const filePath = props.input.filePath ?? "" + const diags = props.metadata.diagnostics ?? {} + // Case-insensitive lookup on Windows + if (process.platform === "win32") { + const key = Object.keys(diags).find((k) => k.toLowerCase() === filePath.toLowerCase()) + return key ? diags[key] : [] + } + return diags[filePath] ?? [] + }) return ( <> @@ -1587,7 +1596,16 @@ ToolRegistry.register({ const diffContent = createMemo(() => props.metadata.diff ?? props.permission["diff"]) const diagnostics = createMemo(() => { - const arr = props.metadata.diagnostics?.[props.input.filePath ?? ""] ?? [] + const filePath = props.input.filePath ?? "" + const diags = props.metadata.diagnostics ?? {} + // Case-insensitive lookup on Windows + let arr: any[] + if (process.platform === "win32") { + const key = Object.keys(diags).find((k) => k.toLowerCase() === filePath.toLowerCase()) + arr = key ? diags[key] : [] + } else { + arr = diags[filePath] ?? [] + } return arr.filter((x) => x.severity === 1).slice(0, 3) }) diff --git a/packages/opencode/src/lsp/client.ts b/packages/opencode/src/lsp/client.ts index ce426cf622d..48c5da419e1 100644 --- a/packages/opencode/src/lsp/client.ts +++ b/packages/opencode/src/lsp/client.ts @@ -50,6 +50,7 @@ export namespace LSPClient { const path = fileURLToPath(params.uri) l.info("textDocument/publishDiagnostics", { path, + count: params.diagnostics.length, }) const exists = diagnostics.has(path) diagnostics.set(path, params.diagnostics) @@ -187,7 +188,11 @@ export namespace LSPClient { return await withTimeout( new Promise((resolve) => { unsub = Bus.subscribe(Event.Diagnostics, (event) => { - if (event.properties.path === input.path && event.properties.serverID === result.serverID) { + const pathsMatch = + process.platform === "win32" + ? event.properties.path.toLowerCase() === input.path.toLowerCase() + : event.properties.path === input.path + if (pathsMatch && event.properties.serverID === result.serverID) { log.info("got diagnostics", input) unsub?.() resolve() diff --git a/packages/opencode/src/tool/edit.ts b/packages/opencode/src/tool/edit.ts index fdf115ac466..e0e1d560092 100644 --- a/packages/opencode/src/tool/edit.ts +++ b/packages/opencode/src/tool/edit.ts @@ -142,7 +142,9 @@ export const EditTool = Tool.define("edit", { const diagnostics = await LSP.diagnostics() for (const [file, issues] of Object.entries(diagnostics)) { if (issues.length === 0) continue - if (file === filePath) { + const fileMatches = + process.platform === "win32" ? file.toLowerCase() === filePath.toLowerCase() : file === filePath + if (fileMatches) { const errors = issues.filter((item) => item.severity === 1) const limited = errors.slice(0, MAX_DIAGNOSTICS_PER_FILE) const suffix = diff --git a/packages/opencode/src/tool/write.ts b/packages/opencode/src/tool/write.ts index 03f2ba891a5..304c0269fbf 100644 --- a/packages/opencode/src/tool/write.ts +++ b/packages/opencode/src/tool/write.ts @@ -87,7 +87,9 @@ export const WriteTool = Tool.define("write", { 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) { + const fileMatches = + process.platform === "win32" ? file.toLowerCase() === filepath.toLowerCase() : file === filepath + if (fileMatches) { output += `\nThis file has errors, please fix\n\n${limited.map(LSP.Diagnostic.pretty).join("\n")}${suffix}\n\n` continue } From 499eeaa02dde82f9be9d447e6396d78cadea2936 Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Tue, 16 Dec 2025 09:45:03 +1000 Subject: [PATCH 2/2] fix(win32): Normalise LSP paths on windows --- .../src/cli/cmd/tui/routes/session/index.tsx | 23 ++++------------- packages/opencode/src/lsp/client.ts | 25 +++++++++---------- packages/opencode/src/tool/edit.ts | 20 ++++++--------- packages/opencode/src/tool/write.ts | 5 ++-- packages/opencode/src/util/filesystem.ts | 14 +++++++++++ 5 files changed, 41 insertions(+), 46 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 089955a6291..b9ef2580be4 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -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) @@ -1415,14 +1416,8 @@ ToolRegistry.register({ }) const diagnostics = createMemo(() => { - const filePath = props.input.filePath ?? "" - const diags = props.metadata.diagnostics ?? {} - // Case-insensitive lookup on Windows - if (process.platform === "win32") { - const key = Object.keys(diags).find((k) => k.toLowerCase() === filePath.toLowerCase()) - return key ? diags[key] : [] - } - return diags[filePath] ?? [] + const filePath = Filesystem.normalizePath(props.input.filePath ?? "") + return props.metadata.diagnostics?.[filePath] ?? [] }) return ( @@ -1596,16 +1591,8 @@ ToolRegistry.register({ const diffContent = createMemo(() => props.metadata.diff ?? props.permission["diff"]) const diagnostics = createMemo(() => { - const filePath = props.input.filePath ?? "" - const diags = props.metadata.diagnostics ?? {} - // Case-insensitive lookup on Windows - let arr: any[] - if (process.platform === "win32") { - const key = Object.keys(diags).find((k) => k.toLowerCase() === filePath.toLowerCase()) - arr = key ? diags[key] : [] - } else { - arr = diags[filePath] ?? [] - } + const filePath = Filesystem.normalizePath(props.input.filePath ?? "") + const arr = props.metadata.diagnostics?.[filePath] ?? [] return arr.filter((x) => x.severity === 1).slice(0, 3) }) diff --git a/packages/opencode/src/lsp/client.ts b/packages/opencode/src/lsp/client.ts index 48c5da419e1..f06c2c938ea 100644 --- a/packages/opencode/src/lsp/client.ts +++ b/packages/opencode/src/lsp/client.ts @@ -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" }) @@ -47,15 +48,15 @@ export namespace LSPClient { const diagnostics = new Map() 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) @@ -182,18 +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((resolve) => { unsub = Bus.subscribe(Event.Diagnostics, (event) => { - const pathsMatch = - process.platform === "win32" - ? event.properties.path.toLowerCase() === input.path.toLowerCase() - : event.properties.path === input.path - if (pathsMatch && 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() } diff --git a/packages/opencode/src/tool/edit.ts b/packages/opencode/src/tool/edit.ts index e0e1d560092..b49bd7abe00 100644 --- a/packages/opencode/src/tool/edit.ts +++ b/packages/opencode/src/tool/edit.ts @@ -140,18 +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 - const fileMatches = - process.platform === "win32" ? file.toLowerCase() === filePath.toLowerCase() : file === filePath - if (fileMatches) { - 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\n${limited.map(LSP.Diagnostic.pretty).join("\n")}${suffix}\n\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\n${limited.map(LSP.Diagnostic.pretty).join("\n")}${suffix}\n\n` } const filediff: Snapshot.FileDiff = { diff --git a/packages/opencode/src/tool/write.ts b/packages/opencode/src/tool/write.ts index 304c0269fbf..6b8fd3dd111 100644 --- a/packages/opencode/src/tool/write.ts +++ b/packages/opencode/src/tool/write.ts @@ -80,6 +80,7 @@ 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 @@ -87,9 +88,7 @@ export const WriteTool = Tool.define("write", { 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` : "" - const fileMatches = - process.platform === "win32" ? file.toLowerCase() === filepath.toLowerCase() : file === filepath - if (fileMatches) { + if (file === normalizedFilepath) { output += `\nThis file has errors, please fix\n\n${limited.map(LSP.Diagnostic.pretty).join("\n")}${suffix}\n\n` continue } diff --git a/packages/opencode/src/util/filesystem.ts b/packages/opencode/src/util/filesystem.ts index a3dcfc70367..98fbe533de3 100644 --- a/packages/opencode/src/util/filesystem.ts +++ b/packages/opencode/src/util/filesystem.ts @@ -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 { + return p + } + } export function overlaps(a: string, b: string) { const relA = relative(a, b) const relB = relative(b, a)