gsd-pi 2.8.2 → 2.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/cli.js +5 -0
- package/dist/loader.js +1 -1
- package/dist/update-check.d.ts +24 -0
- package/dist/update-check.js +93 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/extensions/types.d.ts +4 -2
- package/node_modules/@gsd/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/client.d.ts +46 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/client.js +758 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/client.js.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/config.d.ts +23 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/config.d.ts.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/config.js +267 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/config.js.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/edits.d.ts +17 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/edits.d.ts.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/edits.js +101 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/edits.js.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/helpers.d.ts +15 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/helpers.d.ts.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/helpers.js +46 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/helpers.js.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/index.d.ts +35 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/index.d.ts.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/index.js +709 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/index.js.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lsp-integration.test.d.ts +2 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lsp-integration.test.d.ts.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lsp-integration.test.js +308 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lsp-integration.test.js.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lspmux.d.ts +34 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lspmux.d.ts.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lspmux.js +136 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lspmux.js.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/types.d.ts +262 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/types.d.ts.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/types.js +64 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/types.js.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/utils.d.ts +50 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/utils.d.ts.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/utils.js +574 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/utils.js.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/slash-commands.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/slash-commands.js +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/index.d.ts +13 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/index.js +4 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/index.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts +10 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/components/settings-selector.js +2 -2
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +2 -0
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js +80 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/rpc/rpc-mode.js +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts +5 -0
- package/node_modules/@gsd/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/rpc/rpc-types.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/src/core/extensions/types.ts +4 -2
- package/node_modules/@gsd/pi-coding-agent/src/core/lsp/client.ts +880 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/lsp/config.ts +325 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/lsp/defaults.json +456 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/lsp/edits.ts +109 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/lsp/helpers.ts +54 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/lsp/index.ts +943 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/lsp/lsp-integration.test.ts +407 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/lsp/lsp.md +33 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/lsp/lspmux.ts +199 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/lsp/types.ts +421 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/lsp/utils.ts +682 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/slash-commands.ts +1 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/tools/index.ts +10 -0
- package/node_modules/@gsd/pi-coding-agent/src/modes/interactive/components/settings-selector.ts +2 -2
- package/node_modules/@gsd/pi-coding-agent/src/modes/interactive/interactive-mode.ts +94 -2
- package/node_modules/@gsd/pi-coding-agent/src/modes/rpc/rpc-mode.ts +2 -2
- package/node_modules/@gsd/pi-coding-agent/src/modes/rpc/rpc-types.ts +2 -1
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +4 -2
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/client.d.ts +46 -0
- package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/lsp/client.js +758 -0
- package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/lsp/config.d.ts +23 -0
- package/packages/pi-coding-agent/dist/core/lsp/config.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/lsp/config.js +267 -0
- package/packages/pi-coding-agent/dist/core/lsp/config.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/lsp/edits.d.ts +17 -0
- package/packages/pi-coding-agent/dist/core/lsp/edits.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/lsp/edits.js +101 -0
- package/packages/pi-coding-agent/dist/core/lsp/edits.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/lsp/helpers.d.ts +15 -0
- package/packages/pi-coding-agent/dist/core/lsp/helpers.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/lsp/helpers.js +46 -0
- package/packages/pi-coding-agent/dist/core/lsp/helpers.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/lsp/index.d.ts +35 -0
- package/packages/pi-coding-agent/dist/core/lsp/index.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/lsp/index.js +709 -0
- package/packages/pi-coding-agent/dist/core/lsp/index.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/lsp/lsp-integration.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/lsp/lsp-integration.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/lsp/lsp-integration.test.js +308 -0
- package/packages/pi-coding-agent/dist/core/lsp/lsp-integration.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/lsp/lspmux.d.ts +34 -0
- package/packages/pi-coding-agent/dist/core/lsp/lspmux.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/lsp/lspmux.js +136 -0
- package/packages/pi-coding-agent/dist/core/lsp/lspmux.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/lsp/types.d.ts +262 -0
- package/packages/pi-coding-agent/dist/core/lsp/types.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/lsp/types.js +64 -0
- package/packages/pi-coding-agent/dist/core/lsp/types.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/lsp/utils.d.ts +50 -0
- package/packages/pi-coding-agent/dist/core/lsp/utils.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/lsp/utils.js +574 -0
- package/packages/pi-coding-agent/dist/core/lsp/utils.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/slash-commands.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/slash-commands.js +1 -0
- package/packages/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/index.d.ts +13 -0
- package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/index.js +4 -0
- package/packages/pi-coding-agent/dist/core/tools/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts +10 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js +2 -2
- package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +2 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +80 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts +5 -0
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.js.map +1 -1
- package/packages/pi-coding-agent/src/core/extensions/types.ts +4 -2
- package/packages/pi-coding-agent/src/core/lsp/client.ts +880 -0
- package/packages/pi-coding-agent/src/core/lsp/config.ts +325 -0
- package/packages/pi-coding-agent/src/core/lsp/defaults.json +456 -0
- package/packages/pi-coding-agent/src/core/lsp/edits.ts +109 -0
- package/packages/pi-coding-agent/src/core/lsp/helpers.ts +54 -0
- package/packages/pi-coding-agent/src/core/lsp/index.ts +943 -0
- package/packages/pi-coding-agent/src/core/lsp/lsp-integration.test.ts +407 -0
- package/packages/pi-coding-agent/src/core/lsp/lsp.md +33 -0
- package/packages/pi-coding-agent/src/core/lsp/lspmux.ts +199 -0
- package/packages/pi-coding-agent/src/core/lsp/types.ts +421 -0
- package/packages/pi-coding-agent/src/core/lsp/utils.ts +682 -0
- package/packages/pi-coding-agent/src/core/slash-commands.ts +1 -0
- package/packages/pi-coding-agent/src/core/tools/index.ts +10 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/settings-selector.ts +2 -2
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +94 -2
- package/packages/pi-coding-agent/src/modes/rpc/rpc-mode.ts +2 -2
- package/packages/pi-coding-agent/src/modes/rpc/rpc-types.ts +2 -1
- package/src/resources/extensions/ask-user-questions.ts +42 -2
- package/src/resources/extensions/bg-shell/index.ts +34 -37
- package/src/resources/extensions/browser-tools/core.d.ts +205 -0
- package/src/resources/extensions/browser-tools/index.ts +2 -2
- package/src/resources/extensions/browser-tools/refs.ts +1 -1
- package/src/resources/extensions/browser-tools/tools/session.ts +1 -1
- package/src/resources/extensions/context7/index.ts +2 -2
- package/src/resources/extensions/get-secrets-from-user.ts +3 -2
- package/src/resources/extensions/google-search/index.ts +1 -1
- package/src/resources/extensions/gsd/auto.ts +126 -12
- package/src/resources/extensions/gsd/commands.ts +218 -3
- package/src/resources/extensions/gsd/doctor.ts +1 -1
- package/src/resources/extensions/gsd/git-service.ts +163 -13
- package/src/resources/extensions/gsd/guided-flow.ts +19 -9
- package/src/resources/extensions/gsd/index.ts +17 -7
- package/src/resources/extensions/gsd/preferences.ts +1 -1
- package/src/resources/extensions/gsd/tests/git-service.test.ts +226 -0
- package/src/resources/extensions/gsd/tests/migrate-command.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/migrate-transformer.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +10 -10
- package/src/resources/extensions/gsd/tests/next-milestone-id.test.ts +87 -0
- package/src/resources/extensions/gsd/tests/worktree.test.ts +352 -0
- package/src/resources/extensions/gsd/types.ts +1 -0
- package/src/resources/extensions/gsd/worktree.ts +20 -1
- package/src/resources/extensions/mac-tools/index.ts +1 -1
- package/src/resources/extensions/search-the-web/command-search-provider.ts +1 -1
- package/src/resources/extensions/search-the-web/format.ts +1 -1
- package/src/resources/extensions/search-the-web/index.ts +5 -5
- package/src/resources/extensions/search-the-web/native-search.ts +5 -6
- package/src/resources/extensions/search-the-web/tool-fetch-page.ts +7 -7
- package/src/resources/extensions/search-the-web/tool-llm-context.ts +11 -11
- package/src/resources/extensions/search-the-web/tool-search.ts +10 -10
- package/src/resources/extensions/shared/interview-ui.ts +2 -2
|
@@ -0,0 +1,943 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as fsSync from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import type { AgentTool, AgentToolResult, AgentToolUpdateCallback } from "@gsd/pi-agent-core";
|
|
7
|
+
import {
|
|
8
|
+
ensureFileOpen,
|
|
9
|
+
getActiveClients,
|
|
10
|
+
getOrCreateClient,
|
|
11
|
+
type LspServerStatus,
|
|
12
|
+
refreshFile,
|
|
13
|
+
sendRequest,
|
|
14
|
+
setIdleTimeout,
|
|
15
|
+
WARMUP_TIMEOUT_MS,
|
|
16
|
+
} from "./client.js";
|
|
17
|
+
import { getServersForFile, type LspConfig, loadConfig } from "./config.js";
|
|
18
|
+
import { applyWorkspaceEdit } from "./edits.js";
|
|
19
|
+
import { ToolAbortError, clampTimeout, throwIfAborted } from "./helpers.js";
|
|
20
|
+
import { detectLspmux } from "./lspmux.js";
|
|
21
|
+
import {
|
|
22
|
+
type CodeAction,
|
|
23
|
+
type CodeActionContext,
|
|
24
|
+
type Command,
|
|
25
|
+
type Diagnostic,
|
|
26
|
+
type DocumentSymbol,
|
|
27
|
+
type Hover,
|
|
28
|
+
type Location,
|
|
29
|
+
type LocationLink,
|
|
30
|
+
type LspClient,
|
|
31
|
+
type LspParams,
|
|
32
|
+
type LspToolDetails,
|
|
33
|
+
lspSchema,
|
|
34
|
+
type ServerConfig,
|
|
35
|
+
type SymbolInformation,
|
|
36
|
+
type WorkspaceEdit,
|
|
37
|
+
} from "./types.js";
|
|
38
|
+
import {
|
|
39
|
+
applyCodeAction,
|
|
40
|
+
collectGlobMatches,
|
|
41
|
+
dedupeWorkspaceSymbols,
|
|
42
|
+
extractHoverText,
|
|
43
|
+
fileToUri,
|
|
44
|
+
filterWorkspaceSymbols,
|
|
45
|
+
formatCodeAction,
|
|
46
|
+
formatDiagnostic,
|
|
47
|
+
formatDiagnosticsSummary,
|
|
48
|
+
formatDocumentSymbol,
|
|
49
|
+
formatGroupedDiagnosticMessages,
|
|
50
|
+
formatLocation,
|
|
51
|
+
formatSymbolInformation,
|
|
52
|
+
formatWorkspaceEdit,
|
|
53
|
+
hasGlobPattern,
|
|
54
|
+
readLocationContext,
|
|
55
|
+
resolveSymbolColumn,
|
|
56
|
+
sortDiagnostics,
|
|
57
|
+
symbolKindToIcon,
|
|
58
|
+
uriToFile,
|
|
59
|
+
} from "./utils.js";
|
|
60
|
+
|
|
61
|
+
export type { LspServerStatus } from "./client.js";
|
|
62
|
+
export type { LspToolDetails } from "./types.js";
|
|
63
|
+
export { lspSchema } from "./types.js";
|
|
64
|
+
|
|
65
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
66
|
+
const lspDescription = fsSync.readFileSync(path.join(__dirname, "lsp.md"), "utf-8");
|
|
67
|
+
|
|
68
|
+
// =============================================================================
|
|
69
|
+
// Warmup API
|
|
70
|
+
// =============================================================================
|
|
71
|
+
|
|
72
|
+
export interface LspWarmupResult {
|
|
73
|
+
servers: Array<{
|
|
74
|
+
name: string;
|
|
75
|
+
status: "ready" | "error";
|
|
76
|
+
fileTypes: string[];
|
|
77
|
+
error?: string;
|
|
78
|
+
}>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function warmupLspServers(cwd: string): Promise<LspWarmupResult> {
|
|
82
|
+
const config = loadConfig(cwd);
|
|
83
|
+
setIdleTimeout(config.idleTimeoutMs);
|
|
84
|
+
const servers: LspWarmupResult["servers"] = [];
|
|
85
|
+
const lspServers = getLspServers(config);
|
|
86
|
+
|
|
87
|
+
const results = await Promise.allSettled(
|
|
88
|
+
lspServers.map(async ([name, serverConfig]) => {
|
|
89
|
+
const client = await getOrCreateClient(serverConfig, cwd, serverConfig.warmupTimeoutMs ?? WARMUP_TIMEOUT_MS);
|
|
90
|
+
return { name, client, fileTypes: serverConfig.fileTypes };
|
|
91
|
+
}),
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
for (let i = 0; i < results.length; i++) {
|
|
95
|
+
const result = results[i];
|
|
96
|
+
const [name, serverConfig] = lspServers[i];
|
|
97
|
+
if (result.status === "fulfilled") {
|
|
98
|
+
servers.push({
|
|
99
|
+
name: result.value.name,
|
|
100
|
+
status: "ready",
|
|
101
|
+
fileTypes: result.value.fileTypes,
|
|
102
|
+
});
|
|
103
|
+
} else {
|
|
104
|
+
servers.push({
|
|
105
|
+
name,
|
|
106
|
+
status: "error",
|
|
107
|
+
fileTypes: serverConfig.fileTypes,
|
|
108
|
+
error: result.reason?.message ?? String(result.reason),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return { servers };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function getLspStatus(): LspServerStatus[] {
|
|
117
|
+
return getActiveClients();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// =============================================================================
|
|
121
|
+
// Internal Helpers
|
|
122
|
+
// =============================================================================
|
|
123
|
+
|
|
124
|
+
const configCache = new Map<string, LspConfig>();
|
|
125
|
+
|
|
126
|
+
function getConfig(cwd: string): LspConfig {
|
|
127
|
+
let config = configCache.get(cwd);
|
|
128
|
+
if (!config) {
|
|
129
|
+
config = loadConfig(cwd);
|
|
130
|
+
setIdleTimeout(config.idleTimeoutMs);
|
|
131
|
+
configCache.set(cwd, config);
|
|
132
|
+
}
|
|
133
|
+
return config;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function getLspServers(config: LspConfig): Array<[string, ServerConfig]> {
|
|
137
|
+
return Object.entries(config.servers) as Array<[string, ServerConfig]>;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function getLspServersForFile(config: LspConfig, filePath: string): Array<[string, ServerConfig]> {
|
|
141
|
+
return getServersForFile(config, filePath);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function getLspServerForFile(config: LspConfig, filePath: string): [string, ServerConfig] | null {
|
|
145
|
+
const servers = getLspServersForFile(config, filePath);
|
|
146
|
+
return servers.length > 0 ? servers[0] : null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const DIAGNOSTIC_MESSAGE_LIMIT = 50;
|
|
150
|
+
const SINGLE_DIAGNOSTICS_WAIT_TIMEOUT_MS = 3000;
|
|
151
|
+
const BATCH_DIAGNOSTICS_WAIT_TIMEOUT_MS = 400;
|
|
152
|
+
const MAX_GLOB_DIAGNOSTIC_TARGETS = 20;
|
|
153
|
+
const WORKSPACE_SYMBOL_LIMIT = 200;
|
|
154
|
+
|
|
155
|
+
function limitDiagnosticMessages(messages: string[]): string[] {
|
|
156
|
+
if (messages.length <= DIAGNOSTIC_MESSAGE_LIMIT) {
|
|
157
|
+
return messages;
|
|
158
|
+
}
|
|
159
|
+
return messages.slice(0, DIAGNOSTIC_MESSAGE_LIMIT);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const LOCATION_CONTEXT_LINES = 1;
|
|
163
|
+
const REFERENCE_CONTEXT_LIMIT = 50;
|
|
164
|
+
|
|
165
|
+
function normalizeLocationResult(result: Location | Location[] | LocationLink | LocationLink[] | null): Location[] {
|
|
166
|
+
if (!result) return [];
|
|
167
|
+
const raw = Array.isArray(result) ? result : [result];
|
|
168
|
+
return raw.flatMap(loc => {
|
|
169
|
+
if ("uri" in loc) {
|
|
170
|
+
return [loc as Location];
|
|
171
|
+
}
|
|
172
|
+
if ("targetUri" in loc) {
|
|
173
|
+
const link = loc as LocationLink;
|
|
174
|
+
return [{ uri: link.targetUri, range: link.targetSelectionRange ?? link.targetRange }];
|
|
175
|
+
}
|
|
176
|
+
return [];
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function formatLocationWithContext(location: Location, cwd: string): Promise<string> {
|
|
181
|
+
const header = ` ${formatLocation(location, cwd)}`;
|
|
182
|
+
const context = await readLocationContext(
|
|
183
|
+
uriToFile(location.uri),
|
|
184
|
+
location.range.start.line + 1,
|
|
185
|
+
LOCATION_CONTEXT_LINES,
|
|
186
|
+
);
|
|
187
|
+
if (context.length === 0) {
|
|
188
|
+
return header;
|
|
189
|
+
}
|
|
190
|
+
return `${header}\n${context.map(lineText => ` ${lineText}`).join("\n")}`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function reloadServer(client: LspClient, serverName: string, signal?: AbortSignal): Promise<string> {
|
|
194
|
+
let output = `Restarted ${serverName}`;
|
|
195
|
+
const reloadMethods = ["rust-analyzer/reloadWorkspace", "workspace/didChangeConfiguration"];
|
|
196
|
+
for (const method of reloadMethods) {
|
|
197
|
+
try {
|
|
198
|
+
await sendRequest(client, method, method.includes("Configuration") ? { settings: {} } : null, signal);
|
|
199
|
+
output = `Reloaded ${serverName}`;
|
|
200
|
+
break;
|
|
201
|
+
} catch {
|
|
202
|
+
// Method not supported, try next
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (output.startsWith("Restarted")) {
|
|
206
|
+
client.proc.kill();
|
|
207
|
+
}
|
|
208
|
+
return output;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function waitForDiagnostics(
|
|
212
|
+
client: LspClient,
|
|
213
|
+
uri: string,
|
|
214
|
+
timeoutMs = 3000,
|
|
215
|
+
signal?: AbortSignal,
|
|
216
|
+
minVersion?: number,
|
|
217
|
+
): Promise<Diagnostic[]> {
|
|
218
|
+
const start = Date.now();
|
|
219
|
+
while (Date.now() - start < timeoutMs) {
|
|
220
|
+
throwIfAborted(signal);
|
|
221
|
+
const diagnostics = client.diagnostics.get(uri);
|
|
222
|
+
const versionOk = minVersion === undefined || client.diagnosticsVersion > minVersion;
|
|
223
|
+
if (diagnostics !== undefined && versionOk) return diagnostics;
|
|
224
|
+
await new Promise<void>(resolve => setTimeout(resolve, 100));
|
|
225
|
+
}
|
|
226
|
+
return client.diagnostics.get(uri) ?? [];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// =============================================================================
|
|
230
|
+
// Workspace Diagnostics
|
|
231
|
+
// =============================================================================
|
|
232
|
+
|
|
233
|
+
interface ProjectType {
|
|
234
|
+
type: "rust" | "typescript" | "go" | "python" | "unknown";
|
|
235
|
+
command?: string[];
|
|
236
|
+
description: string;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function detectProjectType(cwd: string): ProjectType {
|
|
240
|
+
if (fs.existsSync(path.join(cwd, "Cargo.toml"))) {
|
|
241
|
+
return { type: "rust", command: ["cargo", "check", "--message-format=short"], description: "Rust (cargo check)" };
|
|
242
|
+
}
|
|
243
|
+
if (fs.existsSync(path.join(cwd, "tsconfig.json"))) {
|
|
244
|
+
return { type: "typescript", command: ["npx", "tsc", "--noEmit"], description: "TypeScript (tsc --noEmit)" };
|
|
245
|
+
}
|
|
246
|
+
if (fs.existsSync(path.join(cwd, "go.mod"))) {
|
|
247
|
+
return { type: "go", command: ["go", "build", "./..."], description: "Go (go build)" };
|
|
248
|
+
}
|
|
249
|
+
if (fs.existsSync(path.join(cwd, "pyproject.toml")) || fs.existsSync(path.join(cwd, "pyrightconfig.json"))) {
|
|
250
|
+
return { type: "python", command: ["pyright"], description: "Python (pyright)" };
|
|
251
|
+
}
|
|
252
|
+
return { type: "unknown", description: "Unknown project type" };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function runWorkspaceDiagnostics(
|
|
256
|
+
cwd: string,
|
|
257
|
+
signal?: AbortSignal,
|
|
258
|
+
): Promise<{ output: string; projectType: ProjectType }> {
|
|
259
|
+
throwIfAborted(signal);
|
|
260
|
+
const projectType = detectProjectType(cwd);
|
|
261
|
+
if (!projectType.command) {
|
|
262
|
+
return {
|
|
263
|
+
output: "Cannot detect project type. Supported: Rust (Cargo.toml), TypeScript (tsconfig.json), Go (go.mod), Python (pyproject.toml)",
|
|
264
|
+
projectType,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
const [cmd, ...cmdArgs] = projectType.command;
|
|
268
|
+
const proc = spawn(cmd, cmdArgs, {
|
|
269
|
+
cwd,
|
|
270
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
271
|
+
});
|
|
272
|
+
const abortHandler = () => {
|
|
273
|
+
proc.kill();
|
|
274
|
+
};
|
|
275
|
+
if (signal) {
|
|
276
|
+
signal.addEventListener("abort", abortHandler, { once: true });
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
const stdoutChunks: Buffer[] = [];
|
|
281
|
+
const stderrChunks: Buffer[] = [];
|
|
282
|
+
|
|
283
|
+
proc.stdout?.on("data", (chunk: Buffer) => stdoutChunks.push(chunk));
|
|
284
|
+
proc.stderr?.on("data", (chunk: Buffer) => stderrChunks.push(chunk));
|
|
285
|
+
|
|
286
|
+
const exitCode = await new Promise<number>((resolve) => {
|
|
287
|
+
proc.on("exit", (code: number | null) => resolve(code ?? 1));
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
const stdout = Buffer.concat(stdoutChunks).toString("utf-8");
|
|
291
|
+
const stderr = Buffer.concat(stderrChunks).toString("utf-8");
|
|
292
|
+
|
|
293
|
+
throwIfAborted(signal);
|
|
294
|
+
const combined = (stdout + stderr).trim();
|
|
295
|
+
if (!combined) {
|
|
296
|
+
return { output: "No issues found", projectType };
|
|
297
|
+
}
|
|
298
|
+
const lines = combined.split("\n");
|
|
299
|
+
if (lines.length > 50) {
|
|
300
|
+
return { output: `${lines.slice(0, 50).join("\n")}\n... and ${lines.length - 50} more lines`, projectType };
|
|
301
|
+
}
|
|
302
|
+
return { output: combined, projectType };
|
|
303
|
+
} catch (e: unknown) {
|
|
304
|
+
if (signal?.aborted) {
|
|
305
|
+
throw new ToolAbortError();
|
|
306
|
+
}
|
|
307
|
+
return { output: `Failed to run ${projectType.command.join(" ")}: ${e}`, projectType };
|
|
308
|
+
} finally {
|
|
309
|
+
signal?.removeEventListener("abort", abortHandler);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// =============================================================================
|
|
314
|
+
// Path Resolution
|
|
315
|
+
// =============================================================================
|
|
316
|
+
|
|
317
|
+
function resolveToCwd(file: string, cwd: string): string {
|
|
318
|
+
return path.resolve(cwd, file);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// =============================================================================
|
|
322
|
+
// Tool Factory
|
|
323
|
+
// =============================================================================
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Create an LSP tool configured for a specific working directory.
|
|
327
|
+
*/
|
|
328
|
+
export function createLspTool(cwd: string): AgentTool<typeof lspSchema, LspToolDetails> {
|
|
329
|
+
return {
|
|
330
|
+
name: "lsp",
|
|
331
|
+
label: "LSP",
|
|
332
|
+
description: lspDescription,
|
|
333
|
+
parameters: lspSchema,
|
|
334
|
+
|
|
335
|
+
async execute(
|
|
336
|
+
_toolCallId: string,
|
|
337
|
+
params: LspParams,
|
|
338
|
+
signal?: AbortSignal,
|
|
339
|
+
_onUpdate?: AgentToolUpdateCallback<LspToolDetails>,
|
|
340
|
+
): Promise<AgentToolResult<LspToolDetails>> {
|
|
341
|
+
const { action, file, line, symbol, occurrence, query, new_name, apply, timeout } = params;
|
|
342
|
+
const timeoutSec = clampTimeout(timeout);
|
|
343
|
+
const timeoutSignal = AbortSignal.timeout(timeoutSec * 1000);
|
|
344
|
+
signal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
345
|
+
throwIfAborted(signal);
|
|
346
|
+
|
|
347
|
+
const config = getConfig(cwd);
|
|
348
|
+
|
|
349
|
+
// Status action doesn't need a file
|
|
350
|
+
if (action === "status") {
|
|
351
|
+
const servers = Object.keys(config.servers);
|
|
352
|
+
const lspmuxState = await detectLspmux();
|
|
353
|
+
const lspmuxStatus = lspmuxState.available
|
|
354
|
+
? lspmuxState.running
|
|
355
|
+
? "lspmux: active (multiplexing enabled)"
|
|
356
|
+
: "lspmux: installed but server not running"
|
|
357
|
+
: "";
|
|
358
|
+
|
|
359
|
+
const serverStatus =
|
|
360
|
+
servers.length > 0
|
|
361
|
+
? `Active language servers: ${servers.join(", ")}`
|
|
362
|
+
: "No language servers configured for this project";
|
|
363
|
+
|
|
364
|
+
const output = lspmuxStatus ? `${serverStatus}\n${lspmuxStatus}` : serverStatus;
|
|
365
|
+
return {
|
|
366
|
+
content: [{ type: "text", text: output }],
|
|
367
|
+
details: { action, success: true, request: params },
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Diagnostics can be batch or single-file
|
|
372
|
+
if (action === "diagnostics") {
|
|
373
|
+
if (!file) {
|
|
374
|
+
const result = await runWorkspaceDiagnostics(cwd, signal);
|
|
375
|
+
return {
|
|
376
|
+
content: [
|
|
377
|
+
{
|
|
378
|
+
type: "text",
|
|
379
|
+
text: `Workspace diagnostics (${result.projectType.description}):\n${result.output}`,
|
|
380
|
+
},
|
|
381
|
+
],
|
|
382
|
+
details: { action, success: true, request: params },
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
let targets: string[];
|
|
387
|
+
let truncatedGlobTargets = false;
|
|
388
|
+
if (hasGlobPattern(file)) {
|
|
389
|
+
const globMatches = await collectGlobMatches(file, cwd, MAX_GLOB_DIAGNOSTIC_TARGETS);
|
|
390
|
+
targets = globMatches.matches;
|
|
391
|
+
truncatedGlobTargets = globMatches.truncated;
|
|
392
|
+
} else {
|
|
393
|
+
targets = [file];
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (targets.length === 0) {
|
|
397
|
+
return {
|
|
398
|
+
content: [{ type: "text", text: `No files matched pattern: ${file}` }],
|
|
399
|
+
details: { action, success: true, request: params },
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const detailed = targets.length > 1 || truncatedGlobTargets;
|
|
404
|
+
const diagnosticsWaitTimeoutMs = detailed
|
|
405
|
+
? Math.min(BATCH_DIAGNOSTICS_WAIT_TIMEOUT_MS, timeoutSec * 1000)
|
|
406
|
+
: Math.min(SINGLE_DIAGNOSTICS_WAIT_TIMEOUT_MS, timeoutSec * 1000);
|
|
407
|
+
const results: string[] = [];
|
|
408
|
+
const allServerNames = new Set<string>();
|
|
409
|
+
if (truncatedGlobTargets) {
|
|
410
|
+
results.push(
|
|
411
|
+
`[W] Pattern matched more than ${MAX_GLOB_DIAGNOSTIC_TARGETS} files; showing first ${MAX_GLOB_DIAGNOSTIC_TARGETS}. Narrow the glob or use workspace diagnostics.`,
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
for (const target of targets) {
|
|
416
|
+
throwIfAborted(signal);
|
|
417
|
+
const resolved = resolveToCwd(target, cwd);
|
|
418
|
+
const servers = getServersForFile(config, resolved);
|
|
419
|
+
if (servers.length === 0) {
|
|
420
|
+
results.push(`[E] ${target}: No language server found`);
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const uri = fileToUri(resolved);
|
|
425
|
+
const relPath = path.relative(cwd, resolved);
|
|
426
|
+
const allDiagnostics: Diagnostic[] = [];
|
|
427
|
+
|
|
428
|
+
for (const [serverName, serverConfig] of servers) {
|
|
429
|
+
allServerNames.add(serverName);
|
|
430
|
+
try {
|
|
431
|
+
throwIfAborted(signal);
|
|
432
|
+
const client = await getOrCreateClient(serverConfig, cwd);
|
|
433
|
+
const minVersion = client.diagnosticsVersion;
|
|
434
|
+
await refreshFile(client, resolved, signal);
|
|
435
|
+
const diagnostics = await waitForDiagnostics(
|
|
436
|
+
client,
|
|
437
|
+
uri,
|
|
438
|
+
diagnosticsWaitTimeoutMs,
|
|
439
|
+
signal,
|
|
440
|
+
minVersion,
|
|
441
|
+
);
|
|
442
|
+
allDiagnostics.push(...diagnostics);
|
|
443
|
+
} catch (err: unknown) {
|
|
444
|
+
if (err instanceof ToolAbortError || signal?.aborted) {
|
|
445
|
+
throw err;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Deduplicate
|
|
451
|
+
const seen = new Set<string>();
|
|
452
|
+
const uniqueDiagnostics: Diagnostic[] = [];
|
|
453
|
+
for (const d of allDiagnostics) {
|
|
454
|
+
const key = `${d.range.start.line}:${d.range.start.character}:${d.range.end.line}:${d.range.end.character}:${d.message}`;
|
|
455
|
+
if (!seen.has(key)) {
|
|
456
|
+
seen.add(key);
|
|
457
|
+
uniqueDiagnostics.push(d);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
sortDiagnostics(uniqueDiagnostics);
|
|
462
|
+
|
|
463
|
+
if (!detailed && targets.length === 1) {
|
|
464
|
+
if (uniqueDiagnostics.length === 0) {
|
|
465
|
+
return {
|
|
466
|
+
content: [{ type: "text", text: "No diagnostics" }],
|
|
467
|
+
details: { action, serverName: Array.from(allServerNames).join(", "), success: true },
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const summary = formatDiagnosticsSummary(uniqueDiagnostics);
|
|
472
|
+
const formatted = uniqueDiagnostics.map(d => formatDiagnostic(d, relPath));
|
|
473
|
+
const output = `${summary}:\n${formatGroupedDiagnosticMessages(formatted)}`;
|
|
474
|
+
return {
|
|
475
|
+
content: [{ type: "text", text: output }],
|
|
476
|
+
details: { action, serverName: Array.from(allServerNames).join(", "), success: true },
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (uniqueDiagnostics.length === 0) {
|
|
481
|
+
results.push(`OK ${relPath}: no issues`);
|
|
482
|
+
} else {
|
|
483
|
+
const summary = formatDiagnosticsSummary(uniqueDiagnostics);
|
|
484
|
+
results.push(`[E] ${relPath}: ${summary}`);
|
|
485
|
+
const formatted = uniqueDiagnostics.map(d => formatDiagnostic(d, relPath));
|
|
486
|
+
results.push(formatGroupedDiagnosticMessages(formatted));
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return {
|
|
491
|
+
content: [{ type: "text", text: results.join("\n") }],
|
|
492
|
+
details: { action, serverName: Array.from(allServerNames).join(", "), success: true },
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const requiresFile = !file && action !== "symbols" && action !== "reload";
|
|
497
|
+
|
|
498
|
+
if (requiresFile) {
|
|
499
|
+
return {
|
|
500
|
+
content: [{ type: "text", text: "Error: file parameter required for this action" }],
|
|
501
|
+
details: { action, success: false },
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const resolvedFile = file ? resolveToCwd(file, cwd) : null;
|
|
506
|
+
|
|
507
|
+
// Workspace symbol search (no file)
|
|
508
|
+
if (action === "symbols" && !resolvedFile) {
|
|
509
|
+
const normalizedQuery = query?.trim();
|
|
510
|
+
if (!normalizedQuery) {
|
|
511
|
+
return {
|
|
512
|
+
content: [{ type: "text", text: "Error: query parameter required for workspace symbol search" }],
|
|
513
|
+
details: { action, success: false, request: params },
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
const servers = getLspServers(config);
|
|
517
|
+
if (servers.length === 0) {
|
|
518
|
+
return {
|
|
519
|
+
content: [{ type: "text", text: "No language server found for this action" }],
|
|
520
|
+
details: { action, success: false, request: params },
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
const aggregatedSymbols: SymbolInformation[] = [];
|
|
524
|
+
const respondingServers = new Set<string>();
|
|
525
|
+
for (const [workspaceServerName, workspaceServerConfig] of servers) {
|
|
526
|
+
throwIfAborted(signal);
|
|
527
|
+
try {
|
|
528
|
+
const workspaceClient = await getOrCreateClient(workspaceServerConfig, cwd);
|
|
529
|
+
const workspaceResult = (await sendRequest(
|
|
530
|
+
workspaceClient,
|
|
531
|
+
"workspace/symbol",
|
|
532
|
+
{ query: normalizedQuery },
|
|
533
|
+
signal,
|
|
534
|
+
)) as SymbolInformation[] | null;
|
|
535
|
+
if (!workspaceResult || workspaceResult.length === 0) {
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
respondingServers.add(workspaceServerName);
|
|
539
|
+
aggregatedSymbols.push(...filterWorkspaceSymbols(workspaceResult, normalizedQuery));
|
|
540
|
+
} catch (err: unknown) {
|
|
541
|
+
if (err instanceof ToolAbortError || signal?.aborted) {
|
|
542
|
+
throw err;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
const dedupedSymbols = dedupeWorkspaceSymbols(aggregatedSymbols);
|
|
547
|
+
if (dedupedSymbols.length === 0) {
|
|
548
|
+
return {
|
|
549
|
+
content: [{ type: "text", text: `No symbols matching "${normalizedQuery}"` }],
|
|
550
|
+
details: {
|
|
551
|
+
action,
|
|
552
|
+
serverName: Array.from(respondingServers).join(", "),
|
|
553
|
+
success: true,
|
|
554
|
+
request: params,
|
|
555
|
+
},
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
const limitedSymbols = dedupedSymbols.slice(0, WORKSPACE_SYMBOL_LIMIT);
|
|
559
|
+
const lines = limitedSymbols.map(s => formatSymbolInformation(s, cwd));
|
|
560
|
+
const truncationLine =
|
|
561
|
+
dedupedSymbols.length > WORKSPACE_SYMBOL_LIMIT
|
|
562
|
+
? `\n... ${dedupedSymbols.length - WORKSPACE_SYMBOL_LIMIT} additional symbol(s) omitted`
|
|
563
|
+
: "";
|
|
564
|
+
return {
|
|
565
|
+
content: [
|
|
566
|
+
{
|
|
567
|
+
type: "text",
|
|
568
|
+
text: `Found ${dedupedSymbols.length} symbol(s) matching "${normalizedQuery}":\n${lines.map(l => ` ${l}`).join("\n")}${truncationLine}`,
|
|
569
|
+
},
|
|
570
|
+
],
|
|
571
|
+
details: {
|
|
572
|
+
action,
|
|
573
|
+
serverName: Array.from(respondingServers).join(", "),
|
|
574
|
+
success: true,
|
|
575
|
+
request: params,
|
|
576
|
+
},
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Reload all servers (no file)
|
|
581
|
+
if (action === "reload" && !resolvedFile) {
|
|
582
|
+
const servers = getLspServers(config);
|
|
583
|
+
if (servers.length === 0) {
|
|
584
|
+
return {
|
|
585
|
+
content: [{ type: "text", text: "No language server found for this action" }],
|
|
586
|
+
details: { action, success: false, request: params },
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
const outputs: string[] = [];
|
|
590
|
+
for (const [workspaceServerName, workspaceServerConfig] of servers) {
|
|
591
|
+
throwIfAborted(signal);
|
|
592
|
+
try {
|
|
593
|
+
const workspaceClient = await getOrCreateClient(workspaceServerConfig, cwd);
|
|
594
|
+
outputs.push(await reloadServer(workspaceClient, workspaceServerName, signal));
|
|
595
|
+
} catch (err: unknown) {
|
|
596
|
+
if (err instanceof ToolAbortError || signal?.aborted) {
|
|
597
|
+
throw err;
|
|
598
|
+
}
|
|
599
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
600
|
+
outputs.push(`Failed to reload ${workspaceServerName}: ${errorMessage}`);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
return {
|
|
604
|
+
content: [{ type: "text", text: outputs.join("\n") }],
|
|
605
|
+
details: { action, serverName: servers.map(([name]) => name).join(", "), success: true, request: params },
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// File-specific actions
|
|
610
|
+
const serverInfo = resolvedFile ? getLspServerForFile(config, resolvedFile) : null;
|
|
611
|
+
if (!serverInfo) {
|
|
612
|
+
return {
|
|
613
|
+
content: [{ type: "text", text: "No language server found for this action" }],
|
|
614
|
+
details: { action, success: false },
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const [serverName, serverConfig] = serverInfo;
|
|
619
|
+
|
|
620
|
+
try {
|
|
621
|
+
const client = await getOrCreateClient(serverConfig, cwd);
|
|
622
|
+
const targetFile = resolvedFile;
|
|
623
|
+
|
|
624
|
+
if (targetFile) {
|
|
625
|
+
await ensureFileOpen(client, targetFile, signal);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const uri = targetFile ? fileToUri(targetFile) : "";
|
|
629
|
+
const resolvedLine = line ?? 1;
|
|
630
|
+
const resolvedCharacter = targetFile
|
|
631
|
+
? await resolveSymbolColumn(targetFile, resolvedLine, symbol, occurrence)
|
|
632
|
+
: 0;
|
|
633
|
+
const position = { line: resolvedLine - 1, character: resolvedCharacter };
|
|
634
|
+
|
|
635
|
+
let output: string;
|
|
636
|
+
|
|
637
|
+
switch (action) {
|
|
638
|
+
case "definition": {
|
|
639
|
+
const result = (await sendRequest(
|
|
640
|
+
client,
|
|
641
|
+
"textDocument/definition",
|
|
642
|
+
{
|
|
643
|
+
textDocument: { uri },
|
|
644
|
+
position,
|
|
645
|
+
},
|
|
646
|
+
signal,
|
|
647
|
+
)) as Location | Location[] | LocationLink | LocationLink[] | null;
|
|
648
|
+
|
|
649
|
+
const locations = normalizeLocationResult(result);
|
|
650
|
+
|
|
651
|
+
if (locations.length === 0) {
|
|
652
|
+
output = "No definition found";
|
|
653
|
+
} else {
|
|
654
|
+
const lines = await Promise.all(
|
|
655
|
+
locations.map(location => formatLocationWithContext(location, cwd)),
|
|
656
|
+
);
|
|
657
|
+
output = `Found ${locations.length} definition(s):\n${lines.join("\n")}`;
|
|
658
|
+
}
|
|
659
|
+
break;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
case "type_definition": {
|
|
663
|
+
const result = (await sendRequest(
|
|
664
|
+
client,
|
|
665
|
+
"textDocument/typeDefinition",
|
|
666
|
+
{
|
|
667
|
+
textDocument: { uri },
|
|
668
|
+
position,
|
|
669
|
+
},
|
|
670
|
+
signal,
|
|
671
|
+
)) as Location | Location[] | LocationLink | LocationLink[] | null;
|
|
672
|
+
|
|
673
|
+
const locations = normalizeLocationResult(result);
|
|
674
|
+
|
|
675
|
+
if (locations.length === 0) {
|
|
676
|
+
output = "No type definition found";
|
|
677
|
+
} else {
|
|
678
|
+
const lines = await Promise.all(
|
|
679
|
+
locations.map(location => formatLocationWithContext(location, cwd)),
|
|
680
|
+
);
|
|
681
|
+
output = `Found ${locations.length} type definition(s):\n${lines.join("\n")}`;
|
|
682
|
+
}
|
|
683
|
+
break;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
case "implementation": {
|
|
687
|
+
const result = (await sendRequest(
|
|
688
|
+
client,
|
|
689
|
+
"textDocument/implementation",
|
|
690
|
+
{
|
|
691
|
+
textDocument: { uri },
|
|
692
|
+
position,
|
|
693
|
+
},
|
|
694
|
+
signal,
|
|
695
|
+
)) as Location | Location[] | LocationLink | LocationLink[] | null;
|
|
696
|
+
|
|
697
|
+
const locations = normalizeLocationResult(result);
|
|
698
|
+
|
|
699
|
+
if (locations.length === 0) {
|
|
700
|
+
output = "No implementation found";
|
|
701
|
+
} else {
|
|
702
|
+
const lines = await Promise.all(
|
|
703
|
+
locations.map(location => formatLocationWithContext(location, cwd)),
|
|
704
|
+
);
|
|
705
|
+
output = `Found ${locations.length} implementation(s):\n${lines.join("\n")}`;
|
|
706
|
+
}
|
|
707
|
+
break;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
case "references": {
|
|
711
|
+
const result = (await sendRequest(
|
|
712
|
+
client,
|
|
713
|
+
"textDocument/references",
|
|
714
|
+
{
|
|
715
|
+
textDocument: { uri },
|
|
716
|
+
position,
|
|
717
|
+
context: { includeDeclaration: true },
|
|
718
|
+
},
|
|
719
|
+
signal,
|
|
720
|
+
)) as Location[] | null;
|
|
721
|
+
|
|
722
|
+
if (!result || result.length === 0) {
|
|
723
|
+
output = "No references found";
|
|
724
|
+
} else {
|
|
725
|
+
const contextualReferences = result.slice(0, REFERENCE_CONTEXT_LIMIT);
|
|
726
|
+
const plainReferences = result.slice(REFERENCE_CONTEXT_LIMIT);
|
|
727
|
+
const contextualLines = await Promise.all(
|
|
728
|
+
contextualReferences.map(location => formatLocationWithContext(location, cwd)),
|
|
729
|
+
);
|
|
730
|
+
const plainLines = plainReferences.map(location => ` ${formatLocation(location, cwd)}`);
|
|
731
|
+
const lines = plainLines.length
|
|
732
|
+
? [
|
|
733
|
+
...contextualLines,
|
|
734
|
+
` ... ${plainLines.length} additional reference(s) shown without context`,
|
|
735
|
+
...plainLines,
|
|
736
|
+
]
|
|
737
|
+
: contextualLines;
|
|
738
|
+
output = `Found ${result.length} reference(s):\n${lines.join("\n")}`;
|
|
739
|
+
}
|
|
740
|
+
break;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
case "hover": {
|
|
744
|
+
const result = (await sendRequest(
|
|
745
|
+
client,
|
|
746
|
+
"textDocument/hover",
|
|
747
|
+
{
|
|
748
|
+
textDocument: { uri },
|
|
749
|
+
position,
|
|
750
|
+
},
|
|
751
|
+
signal,
|
|
752
|
+
)) as Hover | null;
|
|
753
|
+
|
|
754
|
+
if (!result || !result.contents) {
|
|
755
|
+
output = "No hover information";
|
|
756
|
+
} else {
|
|
757
|
+
output = extractHoverText(result.contents);
|
|
758
|
+
}
|
|
759
|
+
break;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
case "code_actions": {
|
|
763
|
+
const diagnostics = client.diagnostics.get(uri) ?? [];
|
|
764
|
+
const context: CodeActionContext = {
|
|
765
|
+
diagnostics,
|
|
766
|
+
only: !apply && query ? [query] : undefined,
|
|
767
|
+
triggerKind: 1,
|
|
768
|
+
};
|
|
769
|
+
|
|
770
|
+
const result = (await sendRequest(
|
|
771
|
+
client,
|
|
772
|
+
"textDocument/codeAction",
|
|
773
|
+
{
|
|
774
|
+
textDocument: { uri },
|
|
775
|
+
range: { start: position, end: position },
|
|
776
|
+
context,
|
|
777
|
+
},
|
|
778
|
+
signal,
|
|
779
|
+
)) as (CodeAction | Command)[] | null;
|
|
780
|
+
|
|
781
|
+
if (!result || result.length === 0) {
|
|
782
|
+
output = "No code actions available";
|
|
783
|
+
break;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
if (apply === true && query) {
|
|
787
|
+
const normalizedQuery = query.trim();
|
|
788
|
+
if (normalizedQuery.length === 0) {
|
|
789
|
+
output = "Error: query parameter required when apply=true for code_actions";
|
|
790
|
+
break;
|
|
791
|
+
}
|
|
792
|
+
const parsedIndex = /^\d+$/.test(normalizedQuery) ? Number.parseInt(normalizedQuery, 10) : null;
|
|
793
|
+
const selectedAction = result.find(
|
|
794
|
+
(actionItem, index) =>
|
|
795
|
+
(parsedIndex !== null && index === parsedIndex) ||
|
|
796
|
+
actionItem.title.toLowerCase().includes(normalizedQuery.toLowerCase()),
|
|
797
|
+
);
|
|
798
|
+
|
|
799
|
+
if (!selectedAction) {
|
|
800
|
+
const actionLines = result.map((actionItem, index) => ` ${formatCodeAction(actionItem, index)}`);
|
|
801
|
+
output = `No code action matches "${normalizedQuery}". Available actions:\n${actionLines.join("\n")}`;
|
|
802
|
+
break;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
const appliedAction = await applyCodeAction(selectedAction, {
|
|
806
|
+
resolveCodeAction: async (actionItem: CodeAction) =>
|
|
807
|
+
(await sendRequest(client, "codeAction/resolve", actionItem, signal)) as CodeAction,
|
|
808
|
+
applyWorkspaceEdit: async (edit: WorkspaceEdit) => applyWorkspaceEdit(edit, cwd),
|
|
809
|
+
executeCommand: async (commandItem: Command) => {
|
|
810
|
+
await sendRequest(
|
|
811
|
+
client,
|
|
812
|
+
"workspace/executeCommand",
|
|
813
|
+
{
|
|
814
|
+
command: commandItem.command,
|
|
815
|
+
arguments: commandItem.arguments ?? [],
|
|
816
|
+
},
|
|
817
|
+
signal,
|
|
818
|
+
);
|
|
819
|
+
},
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
if (!appliedAction) {
|
|
823
|
+
output = `Action "${selectedAction.title}" has no workspace edit or command to apply`;
|
|
824
|
+
break;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
const summaryLines: string[] = [];
|
|
828
|
+
if (appliedAction.edits.length > 0) {
|
|
829
|
+
summaryLines.push(" Workspace edit:");
|
|
830
|
+
summaryLines.push(...appliedAction.edits.map(item => ` ${item}`));
|
|
831
|
+
}
|
|
832
|
+
if (appliedAction.executedCommands.length > 0) {
|
|
833
|
+
summaryLines.push(" Executed command(s):");
|
|
834
|
+
summaryLines.push(...appliedAction.executedCommands.map(commandName => ` ${commandName}`));
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
output = `Applied "${appliedAction.title}":\n${summaryLines.join("\n")}`;
|
|
838
|
+
break;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const actionLines = result.map((actionItem, index) => ` ${formatCodeAction(actionItem, index)}`);
|
|
842
|
+
output = `${result.length} code action(s):\n${actionLines.join("\n")}`;
|
|
843
|
+
break;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
case "symbols": {
|
|
847
|
+
if (!targetFile) {
|
|
848
|
+
output = "Error: file parameter required for document symbols";
|
|
849
|
+
break;
|
|
850
|
+
}
|
|
851
|
+
const result = (await sendRequest(
|
|
852
|
+
client,
|
|
853
|
+
"textDocument/documentSymbol",
|
|
854
|
+
{
|
|
855
|
+
textDocument: { uri },
|
|
856
|
+
},
|
|
857
|
+
signal,
|
|
858
|
+
)) as (DocumentSymbol | SymbolInformation)[] | null;
|
|
859
|
+
|
|
860
|
+
if (!result || result.length === 0) {
|
|
861
|
+
output = "No symbols found";
|
|
862
|
+
} else {
|
|
863
|
+
const relPath = path.relative(cwd, targetFile);
|
|
864
|
+
if ("selectionRange" in result[0]) {
|
|
865
|
+
const lines = (result as DocumentSymbol[]).flatMap(s => formatDocumentSymbol(s));
|
|
866
|
+
output = `Symbols in ${relPath}:\n${lines.join("\n")}`;
|
|
867
|
+
} else {
|
|
868
|
+
const lines = (result as SymbolInformation[]).map(s => {
|
|
869
|
+
const line = s.location.range.start.line + 1;
|
|
870
|
+
const icon = symbolKindToIcon(s.kind);
|
|
871
|
+
return `${icon} ${s.name} @ line ${line}`;
|
|
872
|
+
});
|
|
873
|
+
output = `Symbols in ${relPath}:\n${lines.join("\n")}`;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
break;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
case "rename": {
|
|
880
|
+
if (!new_name) {
|
|
881
|
+
return {
|
|
882
|
+
content: [{ type: "text", text: "Error: new_name parameter required for rename" }],
|
|
883
|
+
details: { action, serverName, success: false },
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
const result = (await sendRequest(
|
|
888
|
+
client,
|
|
889
|
+
"textDocument/rename",
|
|
890
|
+
{
|
|
891
|
+
textDocument: { uri },
|
|
892
|
+
position,
|
|
893
|
+
newName: new_name,
|
|
894
|
+
},
|
|
895
|
+
signal,
|
|
896
|
+
)) as WorkspaceEdit | null;
|
|
897
|
+
|
|
898
|
+
if (!result) {
|
|
899
|
+
output = "Rename returned no edits";
|
|
900
|
+
} else {
|
|
901
|
+
const shouldApply = apply !== false;
|
|
902
|
+
if (shouldApply) {
|
|
903
|
+
const applied = await applyWorkspaceEdit(result, cwd);
|
|
904
|
+
output = `Applied rename:\n${applied.map(a => ` ${a}`).join("\n")}`;
|
|
905
|
+
} else {
|
|
906
|
+
const preview = formatWorkspaceEdit(result, cwd);
|
|
907
|
+
output = `Rename preview:\n${preview.map(p => ` ${p}`).join("\n")}`;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
break;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
case "reload": {
|
|
914
|
+
output = await reloadServer(client, serverName, signal);
|
|
915
|
+
break;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
default:
|
|
919
|
+
output = `Unknown action: ${action}`;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
return {
|
|
923
|
+
content: [{ type: "text", text: output }],
|
|
924
|
+
details: { serverName, action, success: true, request: params },
|
|
925
|
+
};
|
|
926
|
+
} catch (err: unknown) {
|
|
927
|
+
if (err instanceof ToolAbortError || signal?.aborted) {
|
|
928
|
+
throw new ToolAbortError();
|
|
929
|
+
}
|
|
930
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
931
|
+
return {
|
|
932
|
+
content: [{ type: "text", text: `LSP error: ${errorMessage}` }],
|
|
933
|
+
details: { serverName, action, success: false, request: params },
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
},
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
/**
|
|
941
|
+
* Default LSP tool using process.cwd().
|
|
942
|
+
*/
|
|
943
|
+
export const lspTool = createLspTool(process.cwd());
|