@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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Dirent } from "node:fs";
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
2
|
+
import { existsSync, statSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
5
5
|
import type { BunFile } from "bun";
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
sendRequest,
|
|
20
20
|
setIdleTimeout,
|
|
21
21
|
syncContent,
|
|
22
|
+
WARMUP_TIMEOUT_MS,
|
|
22
23
|
} from "./client";
|
|
23
24
|
import { getLinterClient } from "./clients";
|
|
24
25
|
import { getServersForFile, hasCapability, type LspConfig, loadConfig } from "./config";
|
|
@@ -72,23 +73,36 @@ export interface LspWarmupResult {
|
|
|
72
73
|
}>;
|
|
73
74
|
}
|
|
74
75
|
|
|
76
|
+
/** Options for warming up LSP servers */
|
|
77
|
+
export interface LspWarmupOptions {
|
|
78
|
+
/** Called when starting to connect to servers */
|
|
79
|
+
onConnecting?: (serverNames: string[]) => void;
|
|
80
|
+
}
|
|
81
|
+
|
|
75
82
|
/**
|
|
76
83
|
* Warm up LSP servers for a directory by connecting to all detected servers.
|
|
77
84
|
* This should be called at startup to avoid cold-start delays.
|
|
78
85
|
*
|
|
79
86
|
* @param cwd - Working directory to detect and start servers for
|
|
87
|
+
* @param options - Optional callbacks for progress reporting
|
|
80
88
|
* @returns Status of each server that was started
|
|
81
89
|
*/
|
|
82
|
-
export async function warmupLspServers(cwd: string): Promise<LspWarmupResult> {
|
|
90
|
+
export async function warmupLspServers(cwd: string, options?: LspWarmupOptions): Promise<LspWarmupResult> {
|
|
83
91
|
const config = await loadConfig(cwd);
|
|
84
92
|
setIdleTimeout(config.idleTimeoutMs);
|
|
85
93
|
const servers: LspWarmupResult["servers"] = [];
|
|
86
94
|
const lspServers = getLspServers(config);
|
|
87
95
|
|
|
88
|
-
//
|
|
96
|
+
// Notify caller which servers we're connecting to
|
|
97
|
+
if (lspServers.length > 0 && options?.onConnecting) {
|
|
98
|
+
options.onConnecting(lspServers.map(([name]) => name));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Start all detected servers in parallel with a short timeout
|
|
102
|
+
// Servers that don't respond quickly will be initialized lazily on first use
|
|
89
103
|
const results = await Promise.allSettled(
|
|
90
104
|
lspServers.map(async ([name, serverConfig]) => {
|
|
91
|
-
const client = await getOrCreateClient(serverConfig, cwd);
|
|
105
|
+
const client = await getOrCreateClient(serverConfig, cwd, WARMUP_TIMEOUT_MS);
|
|
92
106
|
return { name, client, fileTypes: serverConfig.fileTypes };
|
|
93
107
|
}),
|
|
94
108
|
);
|
|
@@ -136,14 +150,18 @@ async function syncFileContent(
|
|
|
136
150
|
content: string,
|
|
137
151
|
cwd: string,
|
|
138
152
|
servers: Array<[string, ServerConfig]>,
|
|
153
|
+
signal?: AbortSignal,
|
|
139
154
|
): Promise<void> {
|
|
155
|
+
signal?.throwIfAborted();
|
|
140
156
|
await Promise.allSettled(
|
|
141
157
|
servers.map(async ([_serverName, serverConfig]) => {
|
|
158
|
+
signal?.throwIfAborted();
|
|
142
159
|
if (serverConfig.createClient) {
|
|
143
160
|
return;
|
|
144
161
|
}
|
|
145
162
|
const client = await getOrCreateClient(serverConfig, cwd);
|
|
146
|
-
|
|
163
|
+
signal?.throwIfAborted();
|
|
164
|
+
await syncContent(client, absolutePath, content, signal);
|
|
147
165
|
}),
|
|
148
166
|
);
|
|
149
167
|
}
|
|
@@ -160,14 +178,17 @@ async function notifyFileSaved(
|
|
|
160
178
|
absolutePath: string,
|
|
161
179
|
cwd: string,
|
|
162
180
|
servers: Array<[string, ServerConfig]>,
|
|
181
|
+
signal?: AbortSignal,
|
|
163
182
|
): Promise<void> {
|
|
183
|
+
signal?.throwIfAborted();
|
|
164
184
|
await Promise.allSettled(
|
|
165
185
|
servers.map(async ([_serverName, serverConfig]) => {
|
|
186
|
+
signal?.throwIfAborted();
|
|
166
187
|
if (serverConfig.createClient) {
|
|
167
188
|
return;
|
|
168
189
|
}
|
|
169
190
|
const client = await getOrCreateClient(serverConfig, cwd);
|
|
170
|
-
await notifySaved(client, absolutePath);
|
|
191
|
+
await notifySaved(client, absolutePath, signal);
|
|
171
192
|
}),
|
|
172
193
|
);
|
|
173
194
|
}
|
|
@@ -227,13 +248,19 @@ function findFileByExtensions(baseDir: string, extensions: string[], maxDepth: n
|
|
|
227
248
|
const normalized = extensions.map((ext) => ext.toLowerCase());
|
|
228
249
|
const search = (dir: string, depth: number): string | null => {
|
|
229
250
|
if (depth > maxDepth) return null;
|
|
230
|
-
|
|
251
|
+
const entries: Dirent[] = [];
|
|
231
252
|
try {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
253
|
+
const names = Array.from(new Bun.Glob("*").scanSync({ cwd: dir, onlyFiles: false }));
|
|
254
|
+
for (const name of names) {
|
|
255
|
+
const fullPath = path.join(dir, name);
|
|
256
|
+
let isDir = false;
|
|
257
|
+
try {
|
|
258
|
+
isDir = statSync(fullPath).isDirectory();
|
|
259
|
+
} catch {
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
entries.push({ name, isFile: () => !isDir, isDirectory: () => isDir } as Dirent);
|
|
263
|
+
}
|
|
237
264
|
} catch {
|
|
238
265
|
return null;
|
|
239
266
|
}
|
|
@@ -298,9 +325,15 @@ function getServerForWorkspaceAction(config: LspConfig, action: string): [string
|
|
|
298
325
|
return null;
|
|
299
326
|
}
|
|
300
327
|
|
|
301
|
-
async function waitForDiagnostics(
|
|
328
|
+
async function waitForDiagnostics(
|
|
329
|
+
client: LspClient,
|
|
330
|
+
uri: string,
|
|
331
|
+
timeoutMs = 3000,
|
|
332
|
+
signal?: AbortSignal,
|
|
333
|
+
): Promise<Diagnostic[]> {
|
|
302
334
|
const start = Date.now();
|
|
303
335
|
while (Date.now() - start < timeoutMs) {
|
|
336
|
+
signal?.throwIfAborted();
|
|
304
337
|
const diagnostics = client.diagnostics.get(uri);
|
|
305
338
|
if (diagnostics !== undefined) return diagnostics;
|
|
306
339
|
await sleep(100);
|
|
@@ -440,6 +473,7 @@ async function getDiagnosticsForFile(
|
|
|
440
473
|
absolutePath: string,
|
|
441
474
|
cwd: string,
|
|
442
475
|
servers: Array<[string, ServerConfig]>,
|
|
476
|
+
signal?: AbortSignal,
|
|
443
477
|
): Promise<FileDiagnosticsResult | undefined> {
|
|
444
478
|
if (servers.length === 0) {
|
|
445
479
|
return undefined;
|
|
@@ -453,6 +487,7 @@ async function getDiagnosticsForFile(
|
|
|
453
487
|
// Wait for diagnostics from all servers in parallel
|
|
454
488
|
const results = await Promise.allSettled(
|
|
455
489
|
servers.map(async ([serverName, serverConfig]) => {
|
|
490
|
+
signal?.throwIfAborted();
|
|
456
491
|
// Use custom linter client if configured
|
|
457
492
|
if (serverConfig.createClient) {
|
|
458
493
|
const linterClient = getLinterClient(serverName, serverConfig, cwd);
|
|
@@ -462,8 +497,9 @@ async function getDiagnosticsForFile(
|
|
|
462
497
|
|
|
463
498
|
// Default: use LSP
|
|
464
499
|
const client = await getOrCreateClient(serverConfig, cwd);
|
|
500
|
+
signal?.throwIfAborted();
|
|
465
501
|
// Content already synced + didSave sent, just wait for diagnostics
|
|
466
|
-
const diagnostics = await waitForDiagnostics(client, uri);
|
|
502
|
+
const diagnostics = await waitForDiagnostics(client, uri, 3000, signal);
|
|
467
503
|
return { serverName, diagnostics };
|
|
468
504
|
}),
|
|
469
505
|
);
|
|
@@ -539,6 +575,7 @@ async function formatContent(
|
|
|
539
575
|
content: string,
|
|
540
576
|
cwd: string,
|
|
541
577
|
servers: Array<[string, ServerConfig]>,
|
|
578
|
+
signal?: AbortSignal,
|
|
542
579
|
): Promise<string> {
|
|
543
580
|
if (servers.length === 0) {
|
|
544
581
|
return content;
|
|
@@ -548,6 +585,7 @@ async function formatContent(
|
|
|
548
585
|
|
|
549
586
|
for (const [serverName, serverConfig] of servers) {
|
|
550
587
|
try {
|
|
588
|
+
signal?.throwIfAborted();
|
|
551
589
|
// Use custom linter client if configured
|
|
552
590
|
if (serverConfig.createClient) {
|
|
553
591
|
const linterClient = getLinterClient(serverName, serverConfig, cwd);
|
|
@@ -556,6 +594,7 @@ async function formatContent(
|
|
|
556
594
|
|
|
557
595
|
// Default: use LSP
|
|
558
596
|
const client = await getOrCreateClient(serverConfig, cwd);
|
|
597
|
+
signal?.throwIfAborted();
|
|
559
598
|
|
|
560
599
|
const caps = client.serverCapabilities;
|
|
561
600
|
if (!caps?.documentFormattingProvider) {
|
|
@@ -563,10 +602,15 @@ async function formatContent(
|
|
|
563
602
|
}
|
|
564
603
|
|
|
565
604
|
// Request formatting (content already synced)
|
|
566
|
-
const edits = (await sendRequest(
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
605
|
+
const edits = (await sendRequest(
|
|
606
|
+
client,
|
|
607
|
+
"textDocument/formatting",
|
|
608
|
+
{
|
|
609
|
+
textDocument: { uri },
|
|
610
|
+
options: DEFAULT_FORMAT_OPTIONS,
|
|
611
|
+
},
|
|
612
|
+
signal,
|
|
613
|
+
)) as TextEdit[] | null;
|
|
570
614
|
|
|
571
615
|
if (!edits || edits.length === 0) {
|
|
572
616
|
return content;
|
|
@@ -633,28 +677,29 @@ export function createLspWritethrough(cwd: string, options?: WritethroughOptions
|
|
|
633
677
|
let formatter: FileFormatResult | undefined;
|
|
634
678
|
let diagnostics: FileDiagnosticsResult | undefined;
|
|
635
679
|
try {
|
|
636
|
-
|
|
637
|
-
|
|
680
|
+
const timeoutSignal = AbortSignal.timeout(10_000);
|
|
681
|
+
const operationSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
682
|
+
await untilAborted(operationSignal, async () => {
|
|
638
683
|
if (useCustomFormatter) {
|
|
639
684
|
// Custom linters (e.g. Biome CLI) require on-disk input.
|
|
640
685
|
await writeContent(content);
|
|
641
|
-
finalContent = await formatContent(dst, content, cwd, customLinterServers);
|
|
686
|
+
finalContent = await formatContent(dst, content, cwd, customLinterServers, operationSignal);
|
|
642
687
|
formatter = finalContent !== content ? FileFormatResult.FORMATTED : FileFormatResult.UNCHANGED;
|
|
643
688
|
await writeContent(finalContent);
|
|
644
|
-
await syncFileContent(dst, finalContent, cwd, lspServers);
|
|
689
|
+
await syncFileContent(dst, finalContent, cwd, lspServers, operationSignal);
|
|
645
690
|
} else {
|
|
646
691
|
// 1. Sync original content to LSP servers
|
|
647
|
-
await syncFileContent(dst, content, cwd, lspServers);
|
|
692
|
+
await syncFileContent(dst, content, cwd, lspServers, operationSignal);
|
|
648
693
|
|
|
649
694
|
// 2. Format in-memory via LSP
|
|
650
695
|
if (enableFormat) {
|
|
651
|
-
finalContent = await formatContent(dst, content, cwd, lspServers);
|
|
696
|
+
finalContent = await formatContent(dst, content, cwd, lspServers, operationSignal);
|
|
652
697
|
formatter = finalContent !== content ? FileFormatResult.FORMATTED : FileFormatResult.UNCHANGED;
|
|
653
698
|
}
|
|
654
699
|
|
|
655
700
|
// 3. If formatted, sync formatted content to LSP servers
|
|
656
701
|
if (finalContent !== content) {
|
|
657
|
-
await syncFileContent(dst, finalContent, cwd, lspServers);
|
|
702
|
+
await syncFileContent(dst, finalContent, cwd, lspServers, operationSignal);
|
|
658
703
|
}
|
|
659
704
|
|
|
660
705
|
// 4. Write to disk
|
|
@@ -662,11 +707,11 @@ export function createLspWritethrough(cwd: string, options?: WritethroughOptions
|
|
|
662
707
|
}
|
|
663
708
|
|
|
664
709
|
// 5. Notify saved to LSP servers
|
|
665
|
-
await notifyFileSaved(dst, cwd, lspServers);
|
|
710
|
+
await notifyFileSaved(dst, cwd, lspServers, operationSignal);
|
|
666
711
|
|
|
667
712
|
// 6. Get diagnostics from all servers
|
|
668
713
|
if (enableDiagnostics) {
|
|
669
|
-
diagnostics = await getDiagnosticsForFile(dst, cwd, servers);
|
|
714
|
+
diagnostics = await getDiagnosticsForFile(dst, cwd, servers, operationSignal);
|
|
670
715
|
}
|
|
671
716
|
});
|
|
672
717
|
} catch {
|
|
@@ -1362,11 +1407,3 @@ export function createLspTool(session: ToolSession): AgentTool<typeof lspSchema,
|
|
|
1362
1407
|
},
|
|
1363
1408
|
};
|
|
1364
1409
|
}
|
|
1365
|
-
|
|
1366
|
-
export const lspTool = createLspTool({
|
|
1367
|
-
cwd: process.cwd(),
|
|
1368
|
-
hasUI: false,
|
|
1369
|
-
rulebookRules: [],
|
|
1370
|
-
getSessionFile: () => null,
|
|
1371
|
-
getSessionSpawns: () => null,
|
|
1372
|
-
});
|
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
import { sendNotification, sendRequest } from "./client";
|
|
2
2
|
import type { Diagnostic, ExpandMacroResult, LspClient, RelatedTest, Runnable, WorkspaceEdit } from "./types";
|
|
3
|
-
import { fileToUri } from "./utils";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Wait for specified milliseconds.
|
|
7
|
-
*/
|
|
8
|
-
async function sleep(ms: number): Promise<void> {
|
|
9
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
10
|
-
}
|
|
3
|
+
import { fileToUri, sleep } from "./utils";
|
|
11
4
|
|
|
12
5
|
/**
|
|
13
6
|
* Run flycheck (cargo check) and collect diagnostics.
|
|
@@ -19,10 +12,56 @@ async function sleep(ms: number): Promise<void> {
|
|
|
19
12
|
*/
|
|
20
13
|
export async function flycheck(client: LspClient, file?: string): Promise<Diagnostic[]> {
|
|
21
14
|
const textDocument = file ? { uri: fileToUri(file) } : null;
|
|
15
|
+
|
|
16
|
+
const countDiagnostics = (diagnostics: Map<string, Diagnostic[]>): number => {
|
|
17
|
+
let count = 0;
|
|
18
|
+
for (const diags of diagnostics.values()) {
|
|
19
|
+
count += diags.length;
|
|
20
|
+
}
|
|
21
|
+
return count;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Capture current diagnostic version before triggering flycheck
|
|
25
|
+
const initialDiagnosticsVersion = client.diagnosticsVersion;
|
|
26
|
+
const initialDiagnosticsCount = countDiagnostics(client.diagnostics);
|
|
27
|
+
|
|
22
28
|
await sendNotification(client, "rust-analyzer/runFlycheck", { textDocument });
|
|
23
29
|
|
|
24
|
-
//
|
|
25
|
-
|
|
30
|
+
// Bounded polling: wait for diagnostics to stabilize or timeout
|
|
31
|
+
// Poll every 100ms for up to 8 seconds (80 iterations)
|
|
32
|
+
const pollIntervalMs = 100;
|
|
33
|
+
const maxPollIterations = 80;
|
|
34
|
+
const stabilityThreshold = 3; // Consider stable after 3 iterations without change
|
|
35
|
+
const minStableDurationMs = 2000; // Avoid early exit when diagnostics are re-published unchanged.
|
|
36
|
+
const startTime = Date.now();
|
|
37
|
+
let lastDiagnosticsVersion = initialDiagnosticsVersion;
|
|
38
|
+
let lastDiagnosticsCount = initialDiagnosticsCount;
|
|
39
|
+
let stableIterations = 0;
|
|
40
|
+
|
|
41
|
+
for (let i = 0; i < maxPollIterations; i++) {
|
|
42
|
+
await sleep(pollIntervalMs);
|
|
43
|
+
|
|
44
|
+
const currentDiagnosticsVersion = client.diagnosticsVersion;
|
|
45
|
+
const currentDiagnosticsCount = countDiagnostics(client.diagnostics);
|
|
46
|
+
|
|
47
|
+
// Check if diagnostics have stabilized
|
|
48
|
+
if (currentDiagnosticsVersion === lastDiagnosticsVersion && currentDiagnosticsCount === lastDiagnosticsCount) {
|
|
49
|
+
stableIterations++;
|
|
50
|
+
const elapsedMs = Date.now() - startTime;
|
|
51
|
+
const countChangedFromStart = currentDiagnosticsCount !== initialDiagnosticsCount;
|
|
52
|
+
if (
|
|
53
|
+
currentDiagnosticsVersion !== initialDiagnosticsVersion &&
|
|
54
|
+
stableIterations >= stabilityThreshold &&
|
|
55
|
+
(countChangedFromStart || elapsedMs >= minStableDurationMs)
|
|
56
|
+
) {
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
stableIterations = 0;
|
|
61
|
+
lastDiagnosticsVersion = currentDiagnosticsVersion;
|
|
62
|
+
lastDiagnosticsCount = currentDiagnosticsCount;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
26
65
|
|
|
27
66
|
// Collect all diagnostics from client
|
|
28
67
|
const allDiags: Diagnostic[] = [];
|
|
@@ -403,6 +403,7 @@ export interface LspClient {
|
|
|
403
403
|
process: Subprocess;
|
|
404
404
|
requestId: number;
|
|
405
405
|
diagnostics: Map<string, Diagnostic[]>;
|
|
406
|
+
diagnosticsVersion: number;
|
|
406
407
|
openFiles: Map<string, OpenFile>;
|
|
407
408
|
pendingRequests: Map<number, PendingRequest>;
|
|
408
409
|
messageBuffer: Uint8Array;
|
|
@@ -492,7 +492,7 @@ export function extractHoverText(
|
|
|
492
492
|
* Sleep for the specified number of milliseconds.
|
|
493
493
|
*/
|
|
494
494
|
export function sleep(ms: number): Promise<void> {
|
|
495
|
-
return
|
|
495
|
+
return Bun.sleep(ms);
|
|
496
496
|
}
|
|
497
497
|
|
|
498
498
|
/**
|