diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 9bad3a89d..0c0049ca5 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed LSP client returning `-32601 Method not found` for defined server→client requests (`window/showMessageRequest`, `window/showDocument`, `workspace/{semanticTokens,inlayHint,codeLens,codeAction,diagnostic}/refresh`). Servers that stall waiting for a real reply (same failure mode as #3029) now receive the spec no-op result ([#3044](https://github.com/can1357/oh-my-pi/issues/3044)). + ## [16.1.1] - 2026-06-19 ### Changed diff --git a/packages/coding-agent/src/lsp/client.ts b/packages/coding-agent/src/lsp/client.ts index 3a4ab3af0..3cd67f061 100644 --- a/packages/coding-agent/src/lsp/client.ts +++ b/packages/coding-agent/src/lsp/client.ts @@ -482,6 +482,28 @@ async function handleServerRequest(client: LspClient, message: LspJsonRpcRequest await sendResponse(client, message.id, null, message.method); return; } + if (message.method === "window/showMessageRequest") { + // Headless: no UI to surface the prompt. Spec says null = "no action selected". + await sendResponse(client, message.id, null, message.method); + return; + } + if (message.method === "window/showDocument") { + // Headless: nothing to display. Spec result is `{ success: boolean }`. + await sendResponse(client, message.id, { success: false }, message.method); + return; + } + if ( + message.method === "workspace/semanticTokens/refresh" || + message.method === "workspace/inlayHint/refresh" || + message.method === "workspace/codeLens/refresh" || + message.method === "workspace/codeAction/refresh" || + message.method === "workspace/diagnostic/refresh" + ) { + // Void acknowledgement per spec; servers that stall waiting for a reply + // (same failure mode as the dynamic-registration hang in #3029) move on. + await sendResponse(client, message.id, null, message.method); + return; + } await sendResponse(client, message.id, null, message.method, { code: -32601, message: `Method not found: ${message.method}`, diff --git a/packages/coding-agent/test/tools/lsp-regressions.test.ts b/packages/coding-agent/test/tools/lsp-regressions.test.ts index 45039443e..551c7e8dd 100644 --- a/packages/coding-agent/test/tools/lsp-regressions.test.ts +++ b/packages/coding-agent/test/tools/lsp-regressions.test.ts @@ -380,6 +380,67 @@ describe("lsp regressions", () => { } }); + it("answers defined server→client requests with spec no-op results", async () => { + // Same failure class as #3029: a defined server→client request + // (window/showMessage{Request}, window/showDocument, workspace/*/refresh) + // must receive a spec-shaped reply, not a -32601. Headless omp can't + // surface UI prompts but still owes a defined no-op. + const tempDir = TempDir.createSync("@omp-lsp-server-requests-"); + try { + const server = installFakeLsp((message, srv) => { + if (message.method === "initialize") { + srv.send({ jsonrpc: "2.0", id: message.id, result: { capabilities: {} } }); + } else if (message.method === "initialized") { + srv.send({ + jsonrpc: "2.0", + id: 9101, + method: "window/showMessageRequest", + params: { type: 1, message: "x", actions: [{ title: "Cancel" }] }, + }); + srv.send({ + jsonrpc: "2.0", + id: 9102, + method: "window/showDocument", + params: { uri: "file:///tmp/a.md" }, + }); + srv.send({ jsonrpc: "2.0", id: 9103, method: "workspace/semanticTokens/refresh" }); + srv.send({ jsonrpc: "2.0", id: 9104, method: "workspace/inlayHint/refresh" }); + srv.send({ jsonrpc: "2.0", id: 9105, method: "workspace/codeLens/refresh" }); + srv.send({ jsonrpc: "2.0", id: 9106, method: "workspace/diagnostic/refresh" }); + } else if (message.method === "shutdown") { + srv.send({ jsonrpc: "2.0", id: message.id, result: null }); + } else if (message.method === "exit") { + srv.exit(0); + } + }); + + const config: ServerConfig = { + command: "fake-lsp", + fileTypes: ["rs"], + rootMarkers: [], + }; + + await lspClient.getOrCreateClient(config, tempDir.path(), 1_000); + + const showMessage = await server.waitFor(message => message.id === 9101 && message.method === undefined); + expect(showMessage.error).toBeUndefined(); + expect(showMessage.result).toBeNull(); + + const showDocument = await server.waitFor(message => message.id === 9102 && message.method === undefined); + expect(showDocument.error).toBeUndefined(); + expect(showDocument.result).toEqual({ success: false }); + + for (const id of [9103, 9104, 9105, 9106]) { + const refresh = await server.waitFor(message => message.id === id && message.method === undefined); + expect(refresh.error).toBeUndefined(); + expect(refresh.result).toBeNull(); + } + } finally { + await lspClient.shutdownAll(); + tempDir.removeSync(); + } + }); + it("opens rust-analyzer Cargo workspace files before polling workspace readiness", async () => { const tempDir = TempDir.createSync("@omp-lsp-rust-workspace-"); try {