@oh-my-pi/pi-coding-agent 3.30.0 → 3.32.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/CHANGELOG.md +85 -0
- package/package.json +5 -5
- package/src/cli/args.ts +4 -0
- package/src/core/agent-session.ts +29 -2
- package/src/core/bash-executor.ts +2 -1
- package/src/core/custom-commands/bundled/review/index.ts +367 -14
- package/src/core/custom-commands/bundled/wt/index.ts +1 -1
- package/src/core/sdk.ts +10 -2
- package/src/core/session-manager.ts +158 -246
- package/src/core/session-storage.ts +379 -0
- package/src/core/settings-manager.ts +155 -4
- package/src/core/slash-commands.ts +39 -13
- package/src/core/system-prompt.ts +62 -64
- package/src/core/tools/ask.ts +5 -4
- package/src/core/tools/bash-interceptor.ts +26 -61
- package/src/core/tools/bash.ts +13 -8
- package/src/core/tools/edit-diff.ts +11 -4
- package/src/core/tools/edit.ts +7 -13
- package/src/core/tools/find.ts +111 -50
- package/src/core/tools/gemini-image.ts +128 -147
- package/src/core/tools/grep.ts +397 -415
- package/src/core/tools/index.test.ts +5 -1
- package/src/core/tools/index.ts +8 -4
- package/src/core/tools/ls.ts +12 -10
- package/src/core/tools/lsp/client.ts +84 -19
- package/src/core/tools/lsp/config.ts +205 -656
- package/src/core/tools/lsp/defaults.json +465 -0
- package/src/core/tools/lsp/index.ts +72 -35
- package/src/core/tools/lsp/rust-analyzer.ts +49 -10
- package/src/core/tools/lsp/types.ts +1 -0
- package/src/core/tools/lsp/utils.ts +1 -1
- package/src/core/tools/read.ts +150 -74
- package/src/core/tools/render-utils.ts +70 -10
- package/src/core/tools/review.ts +38 -126
- package/src/core/tools/task/artifacts.ts +5 -4
- package/src/core/tools/task/commands.ts +4 -0
- package/src/core/tools/task/executor.ts +94 -83
- package/src/core/tools/task/index.ts +130 -92
- package/src/core/tools/task/parallel.ts +30 -3
- package/src/core/tools/task/render.ts +85 -39
- package/src/core/tools/task/types.ts +15 -6
- package/src/core/tools/task/worker.ts +124 -89
- package/src/core/tools/web-fetch.ts +112 -377
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/artifacthub.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/arxiv.ts +8 -4
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/aur.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/biorxiv.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/bluesky.ts +10 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/brew.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/cheatsh.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/chocolatey.ts +6 -1
- package/src/core/tools/web-scrapers/choosealicense.ts +110 -0
- package/src/core/tools/web-scrapers/cisa-kev.ts +100 -0
- package/src/core/tools/web-scrapers/clojars.ts +180 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/coingecko.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/crates-io.ts +7 -2
- package/src/core/tools/web-scrapers/crossref.ts +149 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/devto.ts +8 -4
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/discogs.ts +6 -1
- package/src/core/tools/web-scrapers/discourse.ts +221 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/dockerhub.ts +7 -3
- package/src/core/tools/web-scrapers/fdroid.ts +158 -0
- package/src/core/tools/web-scrapers/firefox-addons.ts +214 -0
- package/src/core/tools/web-scrapers/flathub.ts +239 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/github-gist.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/github.ts +63 -32
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/gitlab.ts +31 -19
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/go-pkg.ts +8 -4
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/hackage.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/hackernews.ts +18 -18
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/hex.ts +3 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/huggingface.ts +10 -10
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/iacr.ts +8 -4
- package/src/core/tools/web-scrapers/index.ts +250 -0
- package/src/core/tools/web-scrapers/jetbrains-marketplace.ts +169 -0
- package/src/core/tools/web-scrapers/lemmy.ts +220 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/lobsters.ts +3 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/mastodon.ts +11 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/maven.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/mdn.ts +2 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/metacpan.ts +13 -7
- package/src/core/tools/web-scrapers/musicbrainz.ts +273 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/npm.ts +12 -5
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/nuget.ts +9 -5
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/nvd.ts +6 -1
- package/src/core/tools/web-scrapers/ollama.ts +267 -0
- package/src/core/tools/web-scrapers/open-vsx.ts +119 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/opencorporates.ts +2 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/openlibrary.ts +18 -12
- package/src/core/tools/web-scrapers/orcid.ts +299 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/osv.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/packagist.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/pub-dev.ts +3 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/pubmed.ts +8 -4
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/pypi.ts +7 -3
- package/src/core/tools/web-scrapers/rawg.ts +124 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/readthedocs.ts +7 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/reddit.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/repology.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/rfc.ts +7 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/rubygems.ts +6 -1
- package/src/core/tools/web-scrapers/searchcode.ts +217 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/sec-edgar.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/semantic-scholar.ts +2 -2
- package/src/core/tools/web-scrapers/snapcraft.ts +200 -0
- package/src/core/tools/web-scrapers/sourcegraph.ts +373 -0
- package/src/core/tools/web-scrapers/spdx.ts +121 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/spotify.ts +3 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/stackoverflow.ts +3 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/terraform.ts +11 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/tldr.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/twitter.ts +15 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/types.ts +98 -27
- package/src/core/tools/web-scrapers/utils.ts +162 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/vimeo.ts +3 -3
- package/src/core/tools/web-scrapers/vscode-marketplace.ts +195 -0
- package/src/core/tools/web-scrapers/w3c.ts +163 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/wikidata.ts +13 -5
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/wikipedia.ts +7 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/youtube.ts +72 -20
- package/src/core/tools/write.ts +21 -18
- package/src/core/voice.ts +3 -2
- package/src/lib/worktree/collapse.ts +2 -1
- package/src/lib/worktree/git.ts +2 -18
- package/src/main.ts +59 -3
- package/src/modes/interactive/components/extensions/extension-dashboard.ts +33 -19
- package/src/modes/interactive/components/extensions/extension-list.ts +15 -8
- package/src/modes/interactive/components/hook-editor.ts +2 -1
- package/src/modes/interactive/components/model-selector.ts +19 -4
- package/src/modes/interactive/interactive-mode.ts +41 -63
- package/src/modes/interactive/theme/theme.ts +58 -58
- package/src/modes/rpc/rpc-mode.ts +10 -9
- package/src/prompts/review-request.md +27 -0
- package/src/prompts/reviewer.md +64 -68
- package/src/prompts/tools/output.md +22 -3
- package/src/prompts/tools/task.md +32 -33
- package/src/utils/clipboard.ts +2 -1
- package/examples/extensions/subagent/agents/reviewer.md +0 -35
- package/src/core/tools/web-fetch-handlers/index.ts +0 -69
- package/src/core/tools/web-fetch-handlers/utils.ts +0 -91
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/academic.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/business.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/dev-platforms.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/documentation.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/finance-media.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/git-hosting.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/media.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/package-managers-2.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/package-managers.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/package-registries.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/research.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/security.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/social-extended.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/social.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/stackexchange.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/standards.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/wikipedia.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/youtube.test.ts +0 -0
|
@@ -110,6 +110,8 @@ describe("createTools", () => {
|
|
|
110
110
|
getEditFuzzyMatch: () => true,
|
|
111
111
|
getGitToolEnabled: () => false,
|
|
112
112
|
getBashInterceptorEnabled: () => true,
|
|
113
|
+
getBashInterceptorSimpleLsEnabled: () => true,
|
|
114
|
+
getBashInterceptorRules: () => [],
|
|
113
115
|
},
|
|
114
116
|
});
|
|
115
117
|
const tools = await createTools(session);
|
|
@@ -128,6 +130,8 @@ describe("createTools", () => {
|
|
|
128
130
|
getEditFuzzyMatch: () => true,
|
|
129
131
|
getGitToolEnabled: () => true,
|
|
130
132
|
getBashInterceptorEnabled: () => true,
|
|
133
|
+
getBashInterceptorSimpleLsEnabled: () => true,
|
|
134
|
+
getBashInterceptorRules: () => [],
|
|
131
135
|
},
|
|
132
136
|
});
|
|
133
137
|
const tools = await createTools(session);
|
|
@@ -183,6 +187,6 @@ describe("createTools", () => {
|
|
|
183
187
|
});
|
|
184
188
|
|
|
185
189
|
it("HIDDEN_TOOLS contains review tools", () => {
|
|
186
|
-
expect(Object.keys(HIDDEN_TOOLS).sort()).toEqual(["complete", "report_finding"
|
|
190
|
+
expect(Object.keys(HIDDEN_TOOLS).sort()).toEqual(["complete", "report_finding"]);
|
|
187
191
|
});
|
|
188
192
|
});
|
package/src/core/tools/index.ts
CHANGED
|
@@ -17,14 +17,14 @@ export {
|
|
|
17
17
|
getLspStatus,
|
|
18
18
|
type LspServerStatus,
|
|
19
19
|
type LspToolDetails,
|
|
20
|
+
type LspWarmupOptions,
|
|
20
21
|
type LspWarmupResult,
|
|
21
|
-
lspTool,
|
|
22
22
|
warmupLspServers,
|
|
23
23
|
} from "./lsp/index";
|
|
24
24
|
export { createNotebookTool, type NotebookToolDetails } from "./notebook";
|
|
25
25
|
export { createOutputTool, type OutputToolDetails } from "./output";
|
|
26
26
|
export { createReadTool, type ReadToolDetails } from "./read";
|
|
27
|
-
export { reportFindingTool,
|
|
27
|
+
export { reportFindingTool, type SubmitReviewDetails } from "./review";
|
|
28
28
|
export { filterRulebookRules, formatRulesForPrompt, type RulebookToolDetails } from "./rulebook";
|
|
29
29
|
export { BUNDLED_AGENTS, createTaskTool, taskTool } from "./task/index";
|
|
30
30
|
export type { TruncationResult } from "./truncate";
|
|
@@ -53,6 +53,7 @@ export { createWriteTool, type WriteToolDetails } from "./write";
|
|
|
53
53
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
54
54
|
import type { Rule } from "../../capability/rule";
|
|
55
55
|
import type { EventBus } from "../event-bus";
|
|
56
|
+
import type { BashInterceptorRule } from "../settings-manager";
|
|
56
57
|
import { createAskTool } from "./ask";
|
|
57
58
|
import { createBashTool } from "./bash";
|
|
58
59
|
import { createCompleteTool } from "./complete";
|
|
@@ -65,7 +66,7 @@ import { createLspTool } from "./lsp/index";
|
|
|
65
66
|
import { createNotebookTool } from "./notebook";
|
|
66
67
|
import { createOutputTool } from "./output";
|
|
67
68
|
import { createReadTool } from "./read";
|
|
68
|
-
import { reportFindingTool
|
|
69
|
+
import { reportFindingTool } from "./review";
|
|
69
70
|
import { createRulebookTool } from "./rulebook";
|
|
70
71
|
import { createTaskTool } from "./task/index";
|
|
71
72
|
import { createWebFetchTool } from "./web-fetch";
|
|
@@ -93,6 +94,8 @@ export interface ToolSession {
|
|
|
93
94
|
getSessionFile: () => string | null;
|
|
94
95
|
/** Get session spawns */
|
|
95
96
|
getSessionSpawns: () => string | null;
|
|
97
|
+
/** Get resolved model string if explicitly set for this session */
|
|
98
|
+
getModelString?: () => string | undefined;
|
|
96
99
|
/** Settings manager (optional) */
|
|
97
100
|
settings?: {
|
|
98
101
|
getImageAutoResize(): boolean;
|
|
@@ -102,6 +105,8 @@ export interface ToolSession {
|
|
|
102
105
|
getEditFuzzyMatch(): boolean;
|
|
103
106
|
getGitToolEnabled(): boolean;
|
|
104
107
|
getBashInterceptorEnabled(): boolean;
|
|
108
|
+
getBashInterceptorSimpleLsEnabled(): boolean;
|
|
109
|
+
getBashInterceptorRules(): BashInterceptorRule[];
|
|
105
110
|
};
|
|
106
111
|
}
|
|
107
112
|
|
|
@@ -129,7 +134,6 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
|
|
|
129
134
|
export const HIDDEN_TOOLS: Record<string, ToolFactory> = {
|
|
130
135
|
complete: createCompleteTool,
|
|
131
136
|
report_finding: () => reportFindingTool,
|
|
132
|
-
submit_review: () => submitReviewTool,
|
|
133
137
|
};
|
|
134
138
|
|
|
135
139
|
export type ToolName = keyof typeof BUILTIN_TOOLS;
|
package/src/core/tools/ls.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
2
1
|
import nodePath from "node:path";
|
|
3
2
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
4
3
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
@@ -53,23 +52,25 @@ export function createLsTool(session: ToolSession): AgentTool<typeof lsSchema> {
|
|
|
53
52
|
const dirPath = resolveToCwd(path || ".", session.cwd);
|
|
54
53
|
const effectiveLimit = limit ?? DEFAULT_LIMIT;
|
|
55
54
|
|
|
56
|
-
// Check if path exists
|
|
57
|
-
|
|
55
|
+
// Check if path exists and is a directory
|
|
56
|
+
let dirStat: Awaited<ReturnType<Bun.BunFile["stat"]>>;
|
|
57
|
+
try {
|
|
58
|
+
dirStat = await Bun.file(dirPath).stat();
|
|
59
|
+
} catch {
|
|
58
60
|
throw new Error(`Path not found: ${dirPath}`);
|
|
59
61
|
}
|
|
60
62
|
|
|
61
|
-
|
|
62
|
-
const stat = statSync(dirPath);
|
|
63
|
-
if (!stat.isDirectory()) {
|
|
63
|
+
if (!dirStat.isDirectory()) {
|
|
64
64
|
throw new Error(`Not a directory: ${dirPath}`);
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
// Read directory entries
|
|
68
68
|
let entries: string[];
|
|
69
69
|
try {
|
|
70
|
-
entries =
|
|
71
|
-
} catch (
|
|
72
|
-
|
|
70
|
+
entries = await Array.fromAsync(new Bun.Glob("*").scan({ cwd: dirPath, dot: true, onlyFiles: false }));
|
|
71
|
+
} catch (error) {
|
|
72
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
73
|
+
throw new Error(`Cannot read directory: ${message}`);
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
// Sort alphabetically (case-insensitive)
|
|
@@ -82,6 +83,7 @@ export function createLsTool(session: ToolSession): AgentTool<typeof lsSchema> {
|
|
|
82
83
|
let fileCount = 0;
|
|
83
84
|
|
|
84
85
|
for (const entry of entries) {
|
|
86
|
+
signal?.throwIfAborted();
|
|
85
87
|
if (results.length >= effectiveLimit) {
|
|
86
88
|
entryLimitReached = true;
|
|
87
89
|
break;
|
|
@@ -92,7 +94,7 @@ export function createLsTool(session: ToolSession): AgentTool<typeof lsSchema> {
|
|
|
92
94
|
let age = "";
|
|
93
95
|
|
|
94
96
|
try {
|
|
95
|
-
const entryStat =
|
|
97
|
+
const entryStat = await Bun.file(fullPath).stat();
|
|
96
98
|
if (entryStat.isDirectory()) {
|
|
97
99
|
suffix = "/";
|
|
98
100
|
dirCount += 1;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
+
import { logger } from "../../logger";
|
|
2
3
|
import { applyWorkspaceEdit } from "./edits";
|
|
3
4
|
import type {
|
|
4
5
|
Diagnostic,
|
|
@@ -266,6 +267,7 @@ async function startMessageReader(client: LspClient): Promise<void> {
|
|
|
266
267
|
if (message.method === "textDocument/publishDiagnostics" && message.params) {
|
|
267
268
|
const params = message.params as { uri: string; diagnostics: Diagnostic[] };
|
|
268
269
|
client.diagnostics.set(params.uri, params.diagnostics);
|
|
270
|
+
client.diagnosticsVersion += 1;
|
|
269
271
|
}
|
|
270
272
|
}
|
|
271
273
|
|
|
@@ -363,7 +365,7 @@ async function sendResponse(
|
|
|
363
365
|
try {
|
|
364
366
|
await writeMessage(client.process.stdin as import("bun").FileSink, response);
|
|
365
367
|
} catch (err) {
|
|
366
|
-
|
|
368
|
+
logger.error("LSP failed to respond.", { method, error: String(err) });
|
|
367
369
|
}
|
|
368
370
|
}
|
|
369
371
|
|
|
@@ -371,10 +373,16 @@ async function sendResponse(
|
|
|
371
373
|
// Client Management
|
|
372
374
|
// =============================================================================
|
|
373
375
|
|
|
376
|
+
/** Timeout for warmup initialize requests (5 seconds) */
|
|
377
|
+
export const WARMUP_TIMEOUT_MS = 5000;
|
|
378
|
+
|
|
374
379
|
/**
|
|
375
380
|
* Get or create an LSP client for the given server configuration and working directory.
|
|
381
|
+
* @param config - Server configuration
|
|
382
|
+
* @param cwd - Working directory
|
|
383
|
+
* @param initTimeoutMs - Optional timeout for the initialize request (defaults to 30s)
|
|
376
384
|
*/
|
|
377
|
-
export async function getOrCreateClient(config: ServerConfig, cwd: string): Promise<LspClient> {
|
|
385
|
+
export async function getOrCreateClient(config: ServerConfig, cwd: string, initTimeoutMs?: number): Promise<LspClient> {
|
|
378
386
|
const key = `${config.command}:${cwd}`;
|
|
379
387
|
|
|
380
388
|
// Check if client already exists
|
|
@@ -408,6 +416,7 @@ export async function getOrCreateClient(config: ServerConfig, cwd: string): Prom
|
|
|
408
416
|
config,
|
|
409
417
|
requestId: 0,
|
|
410
418
|
diagnostics: new Map(),
|
|
419
|
+
diagnosticsVersion: 0,
|
|
411
420
|
openFiles: new Map(),
|
|
412
421
|
pendingRequests: new Map(),
|
|
413
422
|
messageBuffer: new Uint8Array(0),
|
|
@@ -427,14 +436,20 @@ export async function getOrCreateClient(config: ServerConfig, cwd: string): Prom
|
|
|
427
436
|
|
|
428
437
|
try {
|
|
429
438
|
// Send initialize request
|
|
430
|
-
const initResult = (await sendRequest(
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
439
|
+
const initResult = (await sendRequest(
|
|
440
|
+
client,
|
|
441
|
+
"initialize",
|
|
442
|
+
{
|
|
443
|
+
processId: process.pid,
|
|
444
|
+
rootUri: fileToUri(cwd),
|
|
445
|
+
rootPath: cwd,
|
|
446
|
+
capabilities: CLIENT_CAPABILITIES,
|
|
447
|
+
initializationOptions: config.initOptions ?? {},
|
|
448
|
+
workspaceFolders: [{ uri: fileToUri(cwd), name: cwd.split("/").pop() ?? "workspace" }],
|
|
449
|
+
},
|
|
450
|
+
undefined, // signal
|
|
451
|
+
initTimeoutMs,
|
|
452
|
+
)) as { capabilities?: unknown };
|
|
438
453
|
|
|
439
454
|
if (!initResult) {
|
|
440
455
|
throw new Error("Failed to initialize LSP: no response");
|
|
@@ -516,9 +531,15 @@ export async function ensureFileOpen(client: LspClient, filePath: string): Promi
|
|
|
516
531
|
* Sync in-memory content to the LSP client without reading from disk.
|
|
517
532
|
* Use this to provide instant feedback during edits before the file is saved.
|
|
518
533
|
*/
|
|
519
|
-
export async function syncContent(
|
|
534
|
+
export async function syncContent(
|
|
535
|
+
client: LspClient,
|
|
536
|
+
filePath: string,
|
|
537
|
+
content: string,
|
|
538
|
+
signal?: AbortSignal,
|
|
539
|
+
): Promise<void> {
|
|
520
540
|
const uri = fileToUri(filePath);
|
|
521
541
|
const lockKey = `${client.name}:${uri}`;
|
|
542
|
+
signal?.throwIfAborted();
|
|
522
543
|
|
|
523
544
|
const existingLock = fileOperationLocks.get(lockKey);
|
|
524
545
|
if (existingLock) {
|
|
@@ -534,6 +555,7 @@ export async function syncContent(client: LspClient, filePath: string, content:
|
|
|
534
555
|
if (!info) {
|
|
535
556
|
// Open file with provided content instead of reading from disk
|
|
536
557
|
const languageId = detectLanguageId(filePath);
|
|
558
|
+
signal?.throwIfAborted();
|
|
537
559
|
await sendNotification(client, "textDocument/didOpen", {
|
|
538
560
|
textDocument: {
|
|
539
561
|
uri,
|
|
@@ -548,6 +570,7 @@ export async function syncContent(client: LspClient, filePath: string, content:
|
|
|
548
570
|
}
|
|
549
571
|
|
|
550
572
|
const version = ++info.version;
|
|
573
|
+
signal?.throwIfAborted();
|
|
551
574
|
await sendNotification(client, "textDocument/didChange", {
|
|
552
575
|
textDocument: { uri, version },
|
|
553
576
|
contentChanges: [{ text: content }],
|
|
@@ -567,11 +590,12 @@ export async function syncContent(client: LspClient, filePath: string, content:
|
|
|
567
590
|
* Notify LSP that a file was saved.
|
|
568
591
|
* Assumes content was already synced via syncContent - just sends didSave.
|
|
569
592
|
*/
|
|
570
|
-
export async function notifySaved(client: LspClient, filePath: string): Promise<void> {
|
|
593
|
+
export async function notifySaved(client: LspClient, filePath: string, signal?: AbortSignal): Promise<void> {
|
|
571
594
|
const uri = fileToUri(filePath);
|
|
572
595
|
const info = client.openFiles.get(uri);
|
|
573
596
|
if (!info) return; // File not open, nothing to notify
|
|
574
597
|
|
|
598
|
+
signal?.throwIfAborted();
|
|
575
599
|
await sendNotification(client, "textDocument/didSave", {
|
|
576
600
|
textDocument: { uri },
|
|
577
601
|
});
|
|
@@ -650,12 +674,25 @@ export function shutdownClient(key: string): void {
|
|
|
650
674
|
// LSP Protocol Methods
|
|
651
675
|
// =============================================================================
|
|
652
676
|
|
|
677
|
+
/** Default timeout for LSP requests (30 seconds) */
|
|
678
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 30000;
|
|
679
|
+
|
|
653
680
|
/**
|
|
654
681
|
* Send an LSP request and wait for response.
|
|
655
682
|
*/
|
|
656
|
-
export async function sendRequest(
|
|
683
|
+
export async function sendRequest(
|
|
684
|
+
client: LspClient,
|
|
685
|
+
method: string,
|
|
686
|
+
params: unknown,
|
|
687
|
+
signal?: AbortSignal,
|
|
688
|
+
timeoutMs: number = DEFAULT_REQUEST_TIMEOUT_MS,
|
|
689
|
+
): Promise<unknown> {
|
|
657
690
|
// Atomically increment and capture request ID
|
|
658
691
|
const id = ++client.requestId;
|
|
692
|
+
if (signal?.aborted) {
|
|
693
|
+
const reason = signal.reason instanceof Error ? signal.reason : new Error("Operation aborted");
|
|
694
|
+
return Promise.reject(reason);
|
|
695
|
+
}
|
|
659
696
|
|
|
660
697
|
const request: LspJsonRpcRequest = {
|
|
661
698
|
jsonrpc: "2.0",
|
|
@@ -667,22 +704,49 @@ export async function sendRequest(client: LspClient, method: string, params: unk
|
|
|
667
704
|
client.lastActivity = Date.now();
|
|
668
705
|
|
|
669
706
|
return new Promise((resolve, reject) => {
|
|
707
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
708
|
+
const cleanup = () => {
|
|
709
|
+
if (signal) {
|
|
710
|
+
signal.removeEventListener("abort", abortHandler);
|
|
711
|
+
}
|
|
712
|
+
};
|
|
713
|
+
const abortHandler = () => {
|
|
714
|
+
if (client.pendingRequests.has(id)) {
|
|
715
|
+
client.pendingRequests.delete(id);
|
|
716
|
+
}
|
|
717
|
+
if (timeout) clearTimeout(timeout);
|
|
718
|
+
cleanup();
|
|
719
|
+
const reason = signal?.reason instanceof Error ? signal.reason : new Error("Operation aborted");
|
|
720
|
+
reject(reason);
|
|
721
|
+
};
|
|
722
|
+
|
|
670
723
|
// Set timeout
|
|
671
|
-
|
|
724
|
+
timeout = setTimeout(() => {
|
|
672
725
|
if (client.pendingRequests.has(id)) {
|
|
673
726
|
client.pendingRequests.delete(id);
|
|
674
|
-
|
|
727
|
+
const err = new Error(`LSP request ${method} timed out`);
|
|
728
|
+
cleanup();
|
|
729
|
+
reject(err);
|
|
675
730
|
}
|
|
676
|
-
},
|
|
731
|
+
}, timeoutMs);
|
|
732
|
+
if (signal) {
|
|
733
|
+
signal.addEventListener("abort", abortHandler, { once: true });
|
|
734
|
+
if (signal.aborted) {
|
|
735
|
+
abortHandler();
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
677
739
|
|
|
678
740
|
// Register pending request with timeout wrapper
|
|
679
741
|
client.pendingRequests.set(id, {
|
|
680
742
|
resolve: (result) => {
|
|
681
|
-
clearTimeout(timeout);
|
|
743
|
+
if (timeout) clearTimeout(timeout);
|
|
744
|
+
cleanup();
|
|
682
745
|
resolve(result);
|
|
683
746
|
},
|
|
684
747
|
reject: (err) => {
|
|
685
|
-
clearTimeout(timeout);
|
|
748
|
+
if (timeout) clearTimeout(timeout);
|
|
749
|
+
cleanup();
|
|
686
750
|
reject(err);
|
|
687
751
|
},
|
|
688
752
|
method,
|
|
@@ -690,8 +754,9 @@ export async function sendRequest(client: LspClient, method: string, params: unk
|
|
|
690
754
|
|
|
691
755
|
// Write request
|
|
692
756
|
writeMessage(client.process.stdin as import("bun").FileSink, request).catch((err) => {
|
|
693
|
-
clearTimeout(timeout);
|
|
757
|
+
if (timeout) clearTimeout(timeout);
|
|
694
758
|
client.pendingRequests.delete(id);
|
|
759
|
+
cleanup();
|
|
695
760
|
reject(err);
|
|
696
761
|
});
|
|
697
762
|
});
|