Skip to content
Open
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
4 changes: 4 additions & 0 deletions packages/coding-agent/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## [Unreleased]

### Fixed

- Fixed Pyright LSP semantic requests hanging during startup by sending the initial workspace configuration notification after `initialized`. ([#5276](https://github.com/can1357/oh-my-pi/issues/5276))

## [16.4.7] - 2026-07-12

### Added
Expand Down
8 changes: 7 additions & 1 deletion packages/coding-agent/src/lsp/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -750,8 +750,14 @@ export async function getOrCreateClient(

client.serverCapabilities = initResult.capabilities as LspClient["serverCapabilities"];

// Send initialized notification
// Finish the initialize handshake before publishing the client as ready.
await sendNotification(client, "initialized", {}, signal);
await sendNotification(
client,
"workspace/didChangeConfiguration",
{ settings: config.settings ?? {} },
signal,
);

client.status = "ready";
// Publish only after init succeeds: pre-init clients are reachable
Expand Down
60 changes: 60 additions & 0 deletions packages/coding-agent/test/tools/lsp-regressions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,66 @@ describe("lsp regressions", () => {
}
});

it("sends initial workspace configuration after initialized before semantic requests (#5276)", async () => {
const tempDir = TempDir.createSync("@omp-lsp-initial-config-");
try {
const server = installFakeLsp((message, srv) => {
if (message.method === "initialize") {
srv.send({ jsonrpc: "2.0", id: message.id, result: { capabilities: { hoverProvider: true } } });
} else if (message.method === "textDocument/hover") {
srv.send({ jsonrpc: "2.0", id: message.id, result: { contents: "configured hover" } });
} else if (message.method === "shutdown") {
srv.send({ jsonrpc: "2.0", id: message.id, result: null });
} else if (message.method === "exit") {
srv.exit(0);
}
});
const settings = {
"typescript-language-server": {
preferences: {
includePackageJsonAutoImports: "on",
importModuleSpecifierPreference: "non-relative",
},
inlayHints: {
includeInlayEnumMemberValueHints: true,
includeInlayParameterNameHints: "all",
},
},
};
const config: ServerConfig = {
command: "fake-lsp",
fileTypes: ["ts"],
rootMarkers: [],
settings,
};

const client = await lspClient.getOrCreateClient(config, tempDir.path(), 1_000);
const result = await lspClient.sendRequest(
client,
"textDocument/hover",
{
textDocument: { uri: fileToUri(path.join(tempDir.path(), "src", "configured.ts")) },
position: { line: 0, character: 0 },
},
undefined,
50,
);

expect(result).toEqual({ contents: "configured hover" });
const methods = server.received.map(message => message.method);
expect(methods.slice(0, 4)).toEqual([
"initialize",
"initialized",
"workspace/didChangeConfiguration",
"textDocument/hover",
]);
expect(server.received[2].params).toEqual({ settings: config.settings });
} finally {
await lspClient.shutdownAll();
tempDir.removeSync();
}
});

it("accepts dynamic capability registration before semantic requests", async () => {
const tempDir = TempDir.createSync("@omp-lsp-dynamic-registration-");
try {
Expand Down