From 062d01bd2f5c3ab6de7747dc5028e66b81dac6f5 Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Tue, 2 Jun 2026 17:52:14 +0900 Subject: feat(lsp): add config-driven LSP support (Roblox Luau via luau-lsp) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add Language Server Protocol integration modeled on opencode's, wired for this codebase's plain-TypeScript tool/agent architecture. Core (@dispatch/core): - lsp/client.ts: LSP/JSON-RPC client over stdio (vscode-jsonrpc) with the initialize handshake, didOpen/didChange sync, push + pull diagnostics (textDocument/diagnostic, workspace/diagnostic), and a generic request() passthrough for hover/definition/references/documentSymbol. - lsp/server.ts: resolves dispatch.toml [lsp] entries into spawn specs. Config-driven only — no builtin registry, no auto-download. - lsp/manager.ts: process-wide LspManager owning client lifecycles, keyed by root+serverID, lazy spawn + reuse + graceful shutdown. - lsp/language.ts: extension->languageId map incl. .luau -> "luau". - lsp/diagnostic.ts: error-only block formatting (1-based). - tools/lsp.ts: on-demand 'lsp' tool (1-based coords -> 0-based wire). - write-file.ts: optional onAfterWrite hook for diagnostics-on-write. - config schema: validate [lsp] block; DispatchConfig.lsp + LspServerConfig. API (@dispatch/api): - AgentManager owns one LspManager; per-working-directory server cache cleared on config reload; diagnostics appended to write_file results; 'lsp' tool gated by new perm_lsp setting; shutdownAll on destroy(). Config: - dispatch.toml: documented, commented [lsp.luau-lsp] Roblox example. Tests: fake-lsp-server fixture + client/manager/server/diagnostic/schema/ tool/write-hook suites, plus an opt-in real-binary luau-lsp smoke test (auto-skipped when luau-lsp is absent). 652 pass; biome + 3 typechecks green. --- packages/api/tests/agent-manager.test.ts | 30 ++++++++++++++++++++++++++++++ packages/api/tests/routes.test.ts | 30 ++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) (limited to 'packages/api/tests') diff --git a/packages/api/tests/agent-manager.test.ts b/packages/api/tests/agent-manager.test.ts index 3353aff..6efe15e 100644 --- a/packages/api/tests/agent-manager.test.ts +++ b/packages/api/tests/agent-manager.test.ts @@ -228,6 +228,36 @@ vi.mock("@dispatch/core", () => ({ execute: async () => ["file1.ts"], }; }, + createLspTool(_getContext: unknown): ToolDefinition { + return { + name: "lsp", + description: "query the language server", + parameters: { _type: "z.ZodObject", shape: {} } as unknown as ToolDefinition["parameters"], + execute: async () => "mock lsp", + }; + }, + LspManager: class MockLspManager { + hasServerForFile() { + return false; + } + async getClients() { + return []; + } + async touchFile() {} + getDiagnostics() { + return {}; + } + async request() { + return []; + } + async shutdownAll() {} + }, + resolveServersFromConfig(_lsp: unknown) { + return []; + }, + reportDiagnostics(_file: string, _issues: unknown) { + return ""; + }, createRunShellTool(_wd: string): ToolDefinition { return { name: "run_shell", diff --git a/packages/api/tests/routes.test.ts b/packages/api/tests/routes.test.ts index a8db5ce..6ec8ca6 100644 --- a/packages/api/tests/routes.test.ts +++ b/packages/api/tests/routes.test.ts @@ -82,6 +82,36 @@ vi.mock("@dispatch/core", () => ({ execute: async () => ["file1.ts"], }; }, + createLspTool(_getContext: unknown): ToolDefinition { + return { + name: "lsp", + description: "query the language server", + parameters: { _type: "z.ZodObject", shape: {} } as unknown as ToolDefinition["parameters"], + execute: async () => "mock lsp", + }; + }, + LspManager: class MockLspManager { + hasServerForFile() { + return false; + } + async getClients() { + return []; + } + async touchFile() {} + getDiagnostics() { + return {}; + } + async request() { + return []; + } + async shutdownAll() {} + }, + resolveServersFromConfig(_lsp: unknown) { + return []; + }, + reportDiagnostics(_file: string, _issues: unknown) { + return ""; + }, createRunShellTool(_wd: string): ToolDefinition { return { name: "run_shell", -- cgit v1.2.3