@oh-my-pi/pi-coding-agent 15.10.1 → 15.10.3
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 +113 -1
- package/dist/types/cli/gallery-fixtures/types.d.ts +7 -1
- package/dist/types/cli/startup-cwd.d.ts +2 -0
- package/dist/types/commands/launch.d.ts +3 -0
- package/dist/types/config/keybindings.d.ts +2 -2
- package/dist/types/config/model-provider-priority.d.ts +1 -0
- package/dist/types/config/model-resolver.d.ts +4 -1
- package/dist/types/config/settings.d.ts +7 -2
- package/dist/types/debug/report-bundle.d.ts +3 -0
- package/dist/types/edit/file-snapshot-store.d.ts +18 -10
- package/dist/types/edit/index.d.ts +0 -1
- package/dist/types/eval/py/__tests__/prelude.test.d.ts +1 -0
- package/dist/types/extensibility/extensions/types.d.ts +4 -1
- package/dist/types/lsp/client.d.ts +10 -0
- package/dist/types/lsp/index.d.ts +0 -5
- package/dist/types/main.d.ts +14 -9
- package/dist/types/mcp/tool-bridge.d.ts +2 -0
- package/dist/types/modes/components/assistant-message.d.ts +0 -9
- package/dist/types/modes/components/custom-editor.d.ts +1 -1
- package/dist/types/modes/components/late-diagnostics-message.d.ts +20 -0
- package/dist/types/modes/components/read-tool-group.d.ts +6 -0
- package/dist/types/modes/components/session-selector.d.ts +16 -7
- package/dist/types/modes/components/status-line.d.ts +2 -0
- package/dist/types/modes/components/tool-execution.d.ts +0 -18
- package/dist/types/modes/controllers/event-controller.d.ts +17 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/magic-keywords.d.ts +1 -1
- package/dist/types/modes/markdown-prose.d.ts +1 -1
- package/dist/types/modes/types.d.ts +7 -0
- package/dist/types/modes/workflow.d.ts +3 -3
- package/dist/types/session/auth-storage.d.ts +1 -1
- package/dist/types/session/messages.d.ts +11 -8
- package/dist/types/session/session-manager.d.ts +5 -2
- package/dist/types/session/yield-queue.d.ts +10 -1
- package/dist/types/task/executor.d.ts +10 -0
- package/dist/types/tools/eval-render.d.ts +0 -1
- package/dist/types/tools/eval.d.ts +8 -0
- package/dist/types/tools/gh-cache-invalidation.d.ts +6 -0
- package/dist/types/tools/github-cache.d.ts +12 -0
- package/dist/types/tools/index.d.ts +31 -0
- package/dist/types/tools/path-utils.d.ts +13 -1
- package/dist/types/tools/read.d.ts +2 -1
- package/dist/types/tools/render-utils.d.ts +3 -1
- package/dist/types/tools/renderers.d.ts +0 -15
- package/dist/types/tools/search.d.ts +2 -2
- package/dist/types/tools/write.d.ts +0 -2
- package/dist/types/tools/yield.d.ts +8 -0
- package/dist/types/tui/code-cell.d.ts +0 -2
- package/dist/types/tui/hyperlink.d.ts +5 -7
- package/dist/types/tui/output-block.d.ts +0 -18
- package/package.json +9 -9
- package/src/cli/args.ts +3 -1
- package/src/cli/dry-balance-cli.ts +2 -4
- package/src/cli/gallery-cli.ts +4 -0
- package/src/cli/gallery-fixtures/codeintel.ts +0 -1
- package/src/cli/gallery-fixtures/fs.ts +68 -1
- package/src/cli/gallery-fixtures/types.ts +8 -1
- package/src/cli/startup-cwd.ts +68 -0
- package/src/commands/launch.ts +3 -0
- package/src/commit/agentic/agent.ts +1 -0
- package/src/commit/model-selection.ts +3 -2
- package/src/config/model-provider-priority.ts +55 -0
- package/src/config/model-registry.ts +4 -22
- package/src/config/model-resolver.ts +39 -7
- package/src/config/settings.ts +86 -41
- package/src/debug/index.ts +8 -0
- package/src/debug/raw-sse-buffer.ts +7 -4
- package/src/debug/report-bundle.ts +9 -0
- package/src/edit/file-snapshot-store.ts +33 -1
- package/src/edit/hashline/diff.ts +86 -0
- package/src/edit/hashline/execute.ts +14 -1
- package/src/edit/hashline/filesystem.ts +2 -1
- package/src/edit/index.ts +31 -17
- package/src/edit/renderer.ts +116 -31
- package/src/eval/__tests__/llm-bridge.test.ts +20 -0
- package/src/eval/js/context-manager.ts +32 -15
- package/src/eval/js/shared/prelude.txt +26 -10
- package/src/eval/llm-bridge.ts +14 -3
- package/src/eval/py/__tests__/prelude.test.ts +19 -0
- package/src/eval/py/executor.ts +23 -11
- package/src/eval/py/prelude.py +1 -1
- package/src/extensibility/extensions/types.ts +10 -1
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/lsp/client.ts +23 -11
- package/src/lsp/config.ts +11 -1
- package/src/lsp/index.ts +189 -61
- package/src/main.ts +144 -78
- package/src/mcp/tool-bridge.ts +2 -0
- package/src/memories/index.ts +2 -2
- package/src/modes/components/assistant-message.ts +3 -15
- package/src/modes/components/custom-editor.ts +143 -111
- package/src/modes/components/late-diagnostics-message.ts +60 -0
- package/src/modes/components/model-selector.ts +59 -13
- package/src/modes/components/oauth-selector.ts +33 -7
- package/src/modes/components/plan-review-overlay.ts +26 -5
- package/src/modes/components/read-tool-group.ts +415 -35
- package/src/modes/components/session-selector.ts +89 -35
- package/src/modes/components/status-line.ts +19 -4
- package/src/modes/components/tips.txt +1 -1
- package/src/modes/components/tool-execution.ts +7 -49
- package/src/modes/components/transcript-container.ts +108 -32
- package/src/modes/components/user-message.ts +1 -1
- package/src/modes/controllers/event-controller.ts +32 -1
- package/src/modes/controllers/input-controller.ts +56 -9
- package/src/modes/interactive-mode.ts +107 -20
- package/src/modes/magic-keywords.ts +1 -1
- package/src/modes/markdown-prose.ts +1 -1
- package/src/modes/theme/shimmer.ts +20 -9
- package/src/modes/types.ts +7 -0
- package/src/modes/utils/ui-helpers.ts +26 -5
- package/src/modes/workflow.ts +10 -10
- package/src/prompts/system/manual-continue.md +7 -0
- package/src/prompts/system/plan-mode-active.md +56 -72
- package/src/prompts/system/workflow-notice.md +1 -1
- package/src/prompts/tools/bash.md +9 -0
- package/src/prompts/tools/browser.md +1 -1
- package/src/prompts/tools/eval.md +5 -2
- package/src/prompts/tools/lsp-late-diagnostic.md +8 -0
- package/src/prompts/tools/read.md +2 -2
- package/src/sdk.ts +85 -10
- package/src/session/agent-session.ts +42 -15
- package/src/session/auth-storage.ts +2 -0
- package/src/session/messages.ts +21 -14
- package/src/session/session-manager.ts +98 -25
- package/src/session/yield-queue.ts +20 -2
- package/src/task/executor.ts +72 -36
- package/src/task/render.ts +3 -4
- package/src/tiny/title-client.ts +6 -1
- package/src/tools/bash.ts +7 -7
- package/src/tools/browser/tab-supervisor.ts +13 -1
- package/src/tools/browser/tab-worker.ts +33 -4
- package/src/tools/eval-render.ts +4 -23
- package/src/tools/eval.ts +13 -2
- package/src/tools/find.ts +148 -99
- package/src/tools/gh-cache-invalidation.ts +200 -0
- package/src/tools/github-cache.ts +25 -0
- package/src/tools/index.ts +32 -0
- package/src/tools/inspect-image.ts +2 -2
- package/src/tools/path-utils.ts +47 -24
- package/src/tools/plan-mode-guard.ts +52 -7
- package/src/tools/read.ts +41 -20
- package/src/tools/render-utils.ts +3 -1
- package/src/tools/renderers.ts +0 -15
- package/src/tools/search.ts +38 -3
- package/src/tools/ssh.ts +0 -1
- package/src/tools/todo.ts +1 -0
- package/src/tools/write.ts +5 -14
- package/src/tools/yield.ts +10 -1
- package/src/tui/code-cell.ts +1 -6
- package/src/tui/hyperlink.ts +13 -23
- package/src/tui/output-block.ts +2 -97
- package/src/utils/commit-message-generator.ts +2 -2
- package/src/utils/enhanced-paste.ts +30 -2
- package/src/web/search/providers/codex.ts +37 -8
package/src/lsp/client.ts
CHANGED
|
@@ -946,18 +946,28 @@ export async function shutdownClient(key: string): Promise<void> {
|
|
|
946
946
|
// LSP Protocol Methods
|
|
947
947
|
// =============================================================================
|
|
948
948
|
|
|
949
|
-
/** Default timeout for LSP requests (30 seconds) */
|
|
949
|
+
/** Default timeout for LSP requests when no abort signal is provided (30 seconds) */
|
|
950
950
|
const DEFAULT_REQUEST_TIMEOUT_MS = 30000;
|
|
951
951
|
|
|
952
952
|
/**
|
|
953
953
|
* Send an LSP request and wait for response.
|
|
954
|
+
*
|
|
955
|
+
* Timeout policy:
|
|
956
|
+
* - If `timeoutMs` is explicitly provided, that value is used.
|
|
957
|
+
* - Else, if `signal` is provided, no internal timer is installed (the caller
|
|
958
|
+
* owns the deadline via the signal — typically a wall-clock `AbortSignal.timeout`
|
|
959
|
+
* from the LSP tool). Installing a second hard-coded 30s timer here used to
|
|
960
|
+
* cause "timed out after 30000ms" errors even when the caller had requested
|
|
961
|
+
* `timeout: 60`.
|
|
962
|
+
* - Else (no signal, no explicit timeout), fall back to `DEFAULT_REQUEST_TIMEOUT_MS`
|
|
963
|
+
* to avoid leaking pending requests forever.
|
|
954
964
|
*/
|
|
955
965
|
export async function sendRequest(
|
|
956
966
|
client: LspClient,
|
|
957
967
|
method: string,
|
|
958
968
|
params: unknown,
|
|
959
969
|
signal?: AbortSignal,
|
|
960
|
-
timeoutMs
|
|
970
|
+
timeoutMs?: number,
|
|
961
971
|
): Promise<unknown> {
|
|
962
972
|
// Atomically increment and capture request ID
|
|
963
973
|
const id = ++client.requestId;
|
|
@@ -993,15 +1003,17 @@ export async function sendRequest(
|
|
|
993
1003
|
reject(reason);
|
|
994
1004
|
};
|
|
995
1005
|
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
client.pendingRequests.
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1006
|
+
const effectiveTimeoutMs = timeoutMs ?? (signal ? undefined : DEFAULT_REQUEST_TIMEOUT_MS);
|
|
1007
|
+
if (effectiveTimeoutMs !== undefined) {
|
|
1008
|
+
timeout = setTimeout(() => {
|
|
1009
|
+
if (client.pendingRequests.has(id)) {
|
|
1010
|
+
client.pendingRequests.delete(id);
|
|
1011
|
+
const err = new Error(`LSP request ${method} timed out after ${effectiveTimeoutMs}ms`);
|
|
1012
|
+
cleanup();
|
|
1013
|
+
reject(err);
|
|
1014
|
+
}
|
|
1015
|
+
}, effectiveTimeoutMs);
|
|
1016
|
+
}
|
|
1005
1017
|
if (signal) {
|
|
1006
1018
|
signal.addEventListener("abort", abortHandler, { once: true });
|
|
1007
1019
|
if (signal.aborted) {
|
package/src/lsp/config.ts
CHANGED
|
@@ -450,13 +450,23 @@ export function loadConfig(cwd: string): LspConfig {
|
|
|
450
450
|
*/
|
|
451
451
|
export function getServersForFile(config: LspConfig, filePath: string): Array<[string, ServerConfig]> {
|
|
452
452
|
const ext = path.extname(filePath).toLowerCase();
|
|
453
|
+
const extNoDot = ext.startsWith(".") ? ext.slice(1) : ext;
|
|
453
454
|
const fileName = path.basename(filePath).toLowerCase();
|
|
454
455
|
const matches: Array<[string, ServerConfig]> = [];
|
|
455
456
|
|
|
456
457
|
for (const [name, serverConfig] of Object.entries(config.servers)) {
|
|
457
458
|
const supportsFile = serverConfig.fileTypes.some(fileType => {
|
|
459
|
+
// Accept both `.ts` and `ts` forms in user config / fixtures so a
|
|
460
|
+
// missing dot in `fileTypes` doesn't silently exclude the server
|
|
461
|
+
// from extension-based routing (e.g. rename_file's relevance filter).
|
|
458
462
|
const normalized = fileType.toLowerCase();
|
|
459
|
-
|
|
463
|
+
const normalizedNoDot = normalized.startsWith(".") ? normalized.slice(1) : normalized;
|
|
464
|
+
return (
|
|
465
|
+
normalized === ext ||
|
|
466
|
+
normalized === fileName ||
|
|
467
|
+
normalizedNoDot === extNoDot ||
|
|
468
|
+
normalizedNoDot === fileName
|
|
469
|
+
);
|
|
460
470
|
});
|
|
461
471
|
|
|
462
472
|
if (supportsFile) {
|
package/src/lsp/index.ts
CHANGED
|
@@ -40,7 +40,6 @@ import {
|
|
|
40
40
|
rangesOverlap,
|
|
41
41
|
} from "./edits";
|
|
42
42
|
import { detectLspmux } from "./lspmux";
|
|
43
|
-
import { renderCall, renderResult } from "./render";
|
|
44
43
|
import {
|
|
45
44
|
type CodeAction,
|
|
46
45
|
type CodeActionContext,
|
|
@@ -302,6 +301,22 @@ function isProjectAwareLspServer(serverConfig: ServerConfig): boolean {
|
|
|
302
301
|
const DIAGNOSTIC_MESSAGE_LIMIT = 50;
|
|
303
302
|
const SINGLE_DIAGNOSTICS_WAIT_TIMEOUT_MS = 3000;
|
|
304
303
|
const BATCH_DIAGNOSTICS_WAIT_TIMEOUT_MS = 400;
|
|
304
|
+
const DIAGNOSTICS_POLL_MS = 100;
|
|
305
|
+
const DIAGNOSTICS_SETTLE_MS = 250;
|
|
306
|
+
/**
|
|
307
|
+
* How long the edit/write writethrough blocks inline waiting for fresh
|
|
308
|
+
* diagnostics before handing slow servers off to the deferred late-injection
|
|
309
|
+
* channel. Keeps the common fast-server case inline while letting an edit
|
|
310
|
+
* return promptly when a server (e.g. a large-monorepo tsserver) is slow to
|
|
311
|
+
* publish fresh diagnostics.
|
|
312
|
+
*/
|
|
313
|
+
const INLINE_DIAGNOSTICS_WAIT_TIMEOUT_MS = 500;
|
|
314
|
+
/**
|
|
315
|
+
* Inner per-server diagnostics wait budget for the background/deferred fetch.
|
|
316
|
+
* Longer than the inline cap (and the old 3s default) so a slow server still
|
|
317
|
+
* delivers late instead of giving up before it ever publishes.
|
|
318
|
+
*/
|
|
319
|
+
const DEFERRED_DIAGNOSTICS_WAIT_TIMEOUT_MS = 12_000;
|
|
305
320
|
const MAX_GLOB_DIAGNOSTIC_TARGETS = 20;
|
|
306
321
|
const WORKSPACE_SYMBOL_LIMIT = 200;
|
|
307
322
|
const PROJECT_INDEXED_ACTIONS: ReadonlySet<string> = new Set([
|
|
@@ -461,27 +476,15 @@ interface WaitForDiagnosticsOptions {
|
|
|
461
476
|
signal?: AbortSignal;
|
|
462
477
|
minVersion?: number;
|
|
463
478
|
expectedDocumentVersion?: number;
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
return undefined;
|
|
474
|
-
}
|
|
475
|
-
if (expectedDocumentVersion === undefined) {
|
|
476
|
-
return publishedDiagnostics.diagnostics;
|
|
477
|
-
}
|
|
478
|
-
if (publishedDiagnostics.version === expectedDocumentVersion) {
|
|
479
|
-
return publishedDiagnostics.diagnostics;
|
|
480
|
-
}
|
|
481
|
-
if (allowUnversioned && publishedDiagnostics.version == null) {
|
|
482
|
-
return publishedDiagnostics.diagnostics;
|
|
483
|
-
}
|
|
484
|
-
return undefined;
|
|
479
|
+
/**
|
|
480
|
+
* Quiescence window (ms). typescript-language-server never echoes the document
|
|
481
|
+
* version (issue #983) and emits diagnostics from several sources at different
|
|
482
|
+
* times, so there is no single "complete, version-matched" publish to gate on.
|
|
483
|
+
* When the server does not exact-version-match, accept the latest publish only
|
|
484
|
+
* after no newer one has arrived for this long, letting an in-flight pre-edit
|
|
485
|
+
* publish be superseded by the fresh one.
|
|
486
|
+
*/
|
|
487
|
+
settleMs?: number;
|
|
485
488
|
}
|
|
486
489
|
|
|
487
490
|
async function waitForDiagnostics(
|
|
@@ -489,26 +492,35 @@ async function waitForDiagnostics(
|
|
|
489
492
|
uri: string,
|
|
490
493
|
options: WaitForDiagnosticsOptions = {},
|
|
491
494
|
): Promise<Diagnostic[]> {
|
|
492
|
-
const { timeoutMs = 3000, signal, minVersion, expectedDocumentVersion,
|
|
495
|
+
const { timeoutMs = 3000, signal, minVersion, expectedDocumentVersion, settleMs = DIAGNOSTICS_SETTLE_MS } = options;
|
|
493
496
|
const start = Date.now();
|
|
497
|
+
let settledRef: PublishedDiagnostics | undefined;
|
|
498
|
+
let settledAt = 0;
|
|
494
499
|
while (Date.now() - start < timeoutMs) {
|
|
495
500
|
throwIfAborted(signal);
|
|
496
501
|
const versionOk = minVersion === undefined || client.diagnosticsVersion > minVersion;
|
|
497
|
-
const
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
502
|
+
const published = client.diagnostics.get(uri);
|
|
503
|
+
if (published && versionOk) {
|
|
504
|
+
// Server honored our exact document version → authoritative, accept now.
|
|
505
|
+
if (expectedDocumentVersion !== undefined && published.version === expectedDocumentVersion) {
|
|
506
|
+
return published.diagnostics;
|
|
507
|
+
}
|
|
508
|
+
// Unversioned/mismatched publish: wait for the stream to go quiet so an
|
|
509
|
+
// in-flight publish for the pre-edit content is superseded by the fresh one.
|
|
510
|
+
if (published !== settledRef) {
|
|
511
|
+
settledRef = published;
|
|
512
|
+
settledAt = Date.now();
|
|
513
|
+
} else if (Date.now() - settledAt >= settleMs) {
|
|
514
|
+
return published.diagnostics;
|
|
515
|
+
}
|
|
504
516
|
}
|
|
505
|
-
await Bun.sleep(
|
|
517
|
+
await Bun.sleep(DIAGNOSTICS_POLL_MS);
|
|
506
518
|
}
|
|
507
519
|
const versionOk = minVersion === undefined || client.diagnosticsVersion > minVersion;
|
|
508
520
|
if (!versionOk) {
|
|
509
521
|
return [];
|
|
510
522
|
}
|
|
511
|
-
return
|
|
523
|
+
return client.diagnostics.get(uri)?.diagnostics ?? [];
|
|
512
524
|
}
|
|
513
525
|
|
|
514
526
|
/** Project type detection result */
|
|
@@ -613,7 +625,8 @@ interface GetDiagnosticsForFileOptions {
|
|
|
613
625
|
signal?: AbortSignal;
|
|
614
626
|
minVersions?: ServerVersionMap;
|
|
615
627
|
expectedDocumentVersions?: ServerVersionMap;
|
|
616
|
-
|
|
628
|
+
/** Per-server wait budget (ms). Defaults to {@link SINGLE_DIAGNOSTICS_WAIT_TIMEOUT_MS}. */
|
|
629
|
+
timeoutMs?: number;
|
|
617
630
|
}
|
|
618
631
|
|
|
619
632
|
/**
|
|
@@ -669,7 +682,7 @@ async function getDiagnosticsForFile(
|
|
|
669
682
|
servers: Array<[string, ServerConfig]>,
|
|
670
683
|
options: GetDiagnosticsForFileOptions = {},
|
|
671
684
|
): Promise<FileDiagnosticsResult | undefined> {
|
|
672
|
-
const { signal, minVersions, expectedDocumentVersions,
|
|
685
|
+
const { signal, minVersions, expectedDocumentVersions, timeoutMs } = options;
|
|
673
686
|
if (servers.length === 0) {
|
|
674
687
|
return undefined;
|
|
675
688
|
}
|
|
@@ -701,11 +714,10 @@ async function getDiagnosticsForFile(
|
|
|
701
714
|
const minVersion = minVersions?.get(serverName);
|
|
702
715
|
const expectedDocumentVersion = expectedDocumentVersions?.get(serverName);
|
|
703
716
|
const diagnostics = await waitForDiagnostics(client, uri, {
|
|
704
|
-
timeoutMs:
|
|
717
|
+
timeoutMs: timeoutMs ?? SINGLE_DIAGNOSTICS_WAIT_TIMEOUT_MS,
|
|
705
718
|
signal,
|
|
706
719
|
minVersion,
|
|
707
720
|
expectedDocumentVersion,
|
|
708
|
-
allowUnversioned: allowUnversionedLspDiagnostics,
|
|
709
721
|
});
|
|
710
722
|
return { serverName, diagnostics };
|
|
711
723
|
}),
|
|
@@ -1007,6 +1019,7 @@ async function scheduleDeferredDiagnosticsFetch(args: {
|
|
|
1007
1019
|
signal: combined,
|
|
1008
1020
|
minVersions: args.minVersions,
|
|
1009
1021
|
expectedDocumentVersions: args.expectedDocumentVersions,
|
|
1022
|
+
timeoutMs: DEFERRED_DIAGNOSTICS_WAIT_TIMEOUT_MS,
|
|
1010
1023
|
});
|
|
1011
1024
|
if (args.signal.aborted || diagnostics === undefined) return;
|
|
1012
1025
|
args.callback(diagnostics);
|
|
@@ -1015,6 +1028,70 @@ async function scheduleDeferredDiagnosticsFetch(args: {
|
|
|
1015
1028
|
}
|
|
1016
1029
|
}
|
|
1017
1030
|
|
|
1031
|
+
/**
|
|
1032
|
+
* Fetch post-write diagnostics without making the edit/write block on a slow
|
|
1033
|
+
* language server.
|
|
1034
|
+
*
|
|
1035
|
+
* Blocks inline only briefly ({@link INLINE_DIAGNOSTICS_WAIT_TIMEOUT_MS}) for a
|
|
1036
|
+
* fresh result. Freshness is enforced by the pre-edit `minVersions` baseline:
|
|
1037
|
+
* exact document-version matches return immediately, and unversioned/mismatched
|
|
1038
|
+
* publishes must settle with no newer publish before inline acceptance. If
|
|
1039
|
+
* nothing fresh arrives in the inline window and a deferred
|
|
1040
|
+
* channel is available, the in-flight fetch is handed off to deliver late via
|
|
1041
|
+
* `onDeferredDiagnostics`, and this returns `undefined` so the tool result
|
|
1042
|
+
* lands immediately. Without a deferred channel (direct/CI callers) it blocks
|
|
1043
|
+
* for the standard budget so the result is still returned inline.
|
|
1044
|
+
*/
|
|
1045
|
+
async function fetchDiagnosticsWithDeferral(args: {
|
|
1046
|
+
dst: string;
|
|
1047
|
+
cwd: string;
|
|
1048
|
+
servers: Array<[string, ServerConfig]>;
|
|
1049
|
+
minVersions: ServerVersionMap | undefined;
|
|
1050
|
+
expectedDocumentVersions: ServerVersionMap | undefined;
|
|
1051
|
+
transformDiagnostics?: ResolvedWritethroughOptions["transformDiagnostics"];
|
|
1052
|
+
deferred?: { onDeferredDiagnostics: (diagnostics: FileDiagnosticsResult) => void; signal: AbortSignal };
|
|
1053
|
+
signal?: AbortSignal;
|
|
1054
|
+
}): Promise<FileDiagnosticsResult | undefined> {
|
|
1055
|
+
const { dst, cwd, servers, minVersions, expectedDocumentVersions, transformDiagnostics, deferred, signal } = args;
|
|
1056
|
+
const apply = (d: FileDiagnosticsResult | undefined) =>
|
|
1057
|
+
d && transformDiagnostics ? transformDiagnostics(dst, d) : d;
|
|
1058
|
+
|
|
1059
|
+
if (!deferred) {
|
|
1060
|
+
// No late-injection channel: block for the standard budget and return inline.
|
|
1061
|
+
return apply(
|
|
1062
|
+
await getDiagnosticsForFile(dst, cwd, servers, {
|
|
1063
|
+
signal,
|
|
1064
|
+
minVersions,
|
|
1065
|
+
expectedDocumentVersions,
|
|
1066
|
+
}),
|
|
1067
|
+
);
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// One background fetch with a generous inner budget; await it only briefly inline.
|
|
1071
|
+
const fetchPromise = getDiagnosticsForFile(dst, cwd, servers, {
|
|
1072
|
+
signal: deferred.signal,
|
|
1073
|
+
minVersions,
|
|
1074
|
+
expectedDocumentVersions,
|
|
1075
|
+
timeoutMs: DEFERRED_DIAGNOSTICS_WAIT_TIMEOUT_MS,
|
|
1076
|
+
});
|
|
1077
|
+
const INLINE_TIMEOUT = Symbol("inline-diagnostics-timeout");
|
|
1078
|
+
const raced = await Promise.race([
|
|
1079
|
+
fetchPromise,
|
|
1080
|
+
Bun.sleep(INLINE_DIAGNOSTICS_WAIT_TIMEOUT_MS).then(() => INLINE_TIMEOUT),
|
|
1081
|
+
]);
|
|
1082
|
+
if (raced !== INLINE_TIMEOUT) {
|
|
1083
|
+
return apply(raced as FileDiagnosticsResult | undefined);
|
|
1084
|
+
}
|
|
1085
|
+
// Slow server: deliver late via the deferred channel; nothing inline. The
|
|
1086
|
+
// deferred sink (edit tool) applies its own dedup, so pass the raw result.
|
|
1087
|
+
void fetchPromise
|
|
1088
|
+
.then(diagnostics => {
|
|
1089
|
+
if (diagnostics && !deferred.signal.aborted) deferred.onDeferredDiagnostics(diagnostics);
|
|
1090
|
+
})
|
|
1091
|
+
.catch(() => {});
|
|
1092
|
+
return undefined;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1018
1095
|
async function runLspWritethrough(
|
|
1019
1096
|
dst: string,
|
|
1020
1097
|
content: string,
|
|
@@ -1047,6 +1124,7 @@ async function runLspWritethrough(
|
|
|
1047
1124
|
let formatter: FileFormatResult | undefined;
|
|
1048
1125
|
let diagnostics: FileDiagnosticsResult | undefined;
|
|
1049
1126
|
let timedOut = false;
|
|
1127
|
+
let synced = false;
|
|
1050
1128
|
try {
|
|
1051
1129
|
const timeoutSignal = AbortSignal.timeout(5_000);
|
|
1052
1130
|
timeoutSignal.addEventListener(
|
|
@@ -1090,19 +1168,8 @@ async function runLspWritethrough(
|
|
|
1090
1168
|
|
|
1091
1169
|
// 5. Notify saved to LSP servers
|
|
1092
1170
|
await notifyFileSaved(dst, cwd, lspServers, operationSignal);
|
|
1093
|
-
|
|
1094
|
-
// 6. Get diagnostics from all servers (wait for fresh results)
|
|
1095
|
-
if (enableDiagnostics) {
|
|
1096
|
-
const fetched = await getDiagnosticsForFile(dst, cwd, servers, {
|
|
1097
|
-
signal: operationSignal,
|
|
1098
|
-
minVersions,
|
|
1099
|
-
expectedDocumentVersions,
|
|
1100
|
-
allowUnversionedLspDiagnostics: false,
|
|
1101
|
-
});
|
|
1102
|
-
diagnostics =
|
|
1103
|
-
fetched && options.transformDiagnostics ? options.transformDiagnostics(dst, fetched) : fetched;
|
|
1104
|
-
}
|
|
1105
1171
|
});
|
|
1172
|
+
synced = true;
|
|
1106
1173
|
} catch {
|
|
1107
1174
|
if (timedOut) {
|
|
1108
1175
|
formatter = undefined;
|
|
@@ -1123,6 +1190,19 @@ async function runLspWritethrough(
|
|
|
1123
1190
|
await getWritePromise();
|
|
1124
1191
|
}
|
|
1125
1192
|
|
|
1193
|
+
if (synced && enableDiagnostics) {
|
|
1194
|
+
diagnostics = await fetchDiagnosticsWithDeferral({
|
|
1195
|
+
dst,
|
|
1196
|
+
cwd,
|
|
1197
|
+
servers,
|
|
1198
|
+
minVersions,
|
|
1199
|
+
expectedDocumentVersions,
|
|
1200
|
+
transformDiagnostics: options.transformDiagnostics,
|
|
1201
|
+
deferred,
|
|
1202
|
+
signal,
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1126
1206
|
if (formatter !== undefined) {
|
|
1127
1207
|
diagnostics ??= {
|
|
1128
1208
|
server: servers.map(([name]) => name).join(", "),
|
|
@@ -1229,10 +1309,6 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
|
|
|
1229
1309
|
readonly summary = "Query LSP (language server) for diagnostics, hover info, and references";
|
|
1230
1310
|
readonly description: string;
|
|
1231
1311
|
readonly parameters = lspSchema;
|
|
1232
|
-
readonly renderCall = renderCall;
|
|
1233
|
-
readonly renderResult = renderResult;
|
|
1234
|
-
readonly mergeCallAndResult = true;
|
|
1235
|
-
readonly inline = true;
|
|
1236
1312
|
readonly strict = true;
|
|
1237
1313
|
|
|
1238
1314
|
constructor(private readonly session: ToolSession) {
|
|
@@ -1261,7 +1337,7 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
|
|
|
1261
1337
|
|
|
1262
1338
|
// Status action doesn't need a file
|
|
1263
1339
|
if (action === "status") {
|
|
1264
|
-
const
|
|
1340
|
+
const configuredNames = Object.keys(config.servers);
|
|
1265
1341
|
const lspmuxState = await detectLspmux();
|
|
1266
1342
|
const lspmuxStatus = lspmuxState.available
|
|
1267
1343
|
? lspmuxState.running
|
|
@@ -1269,14 +1345,40 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
|
|
|
1269
1345
|
: "lspmux: installed but server not running"
|
|
1270
1346
|
: "";
|
|
1271
1347
|
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1348
|
+
// `Object.keys(config.servers)` reflects what is *configured & resolvable
|
|
1349
|
+
// on PATH* — it does NOT prove the server actually starts. A wrapper
|
|
1350
|
+
// binary that exits immediately (e.g. rustup without the rust-analyzer
|
|
1351
|
+
// component) still appears here. Distinguish "configured" from
|
|
1352
|
+
// "started" (have a live in-process client) so callers cannot mistake
|
|
1353
|
+
// presence-on-PATH for a working server.
|
|
1354
|
+
const startedClients = getActiveClients();
|
|
1355
|
+
const startedByConfigName = new Map<string, LspServerStatus>();
|
|
1356
|
+
// getActiveClients() reports `name = client.config.command` (the
|
|
1357
|
+
// unresolved binary name from defaults.json), so match against
|
|
1358
|
+
// `serverConfig.command`, not the resolved path.
|
|
1359
|
+
for (const [name, serverConfig] of Object.entries(config.servers)) {
|
|
1360
|
+
const matched = startedClients.find(c => c.name === serverConfig.command);
|
|
1361
|
+
if (matched) startedByConfigName.set(name, matched);
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
const lines: string[] = [];
|
|
1365
|
+
if (configuredNames.length === 0) {
|
|
1366
|
+
lines.push("No language servers configured for this project");
|
|
1367
|
+
} else {
|
|
1368
|
+
const labelled = configuredNames.map(name => {
|
|
1369
|
+
const started = startedByConfigName.get(name);
|
|
1370
|
+
if (!started) return `${name} (configured, not started)`;
|
|
1371
|
+
return `${name} (${started.status})`;
|
|
1372
|
+
});
|
|
1373
|
+
lines.push(`Language servers: ${labelled.join(", ")}`);
|
|
1374
|
+
lines.push(
|
|
1375
|
+
" note: 'configured, not started' means the binary resolves on PATH but no request has spawned it yet; 'ready' means a client process is live for this cwd.",
|
|
1376
|
+
);
|
|
1377
|
+
}
|
|
1378
|
+
if (lspmuxStatus) lines.push(lspmuxStatus);
|
|
1276
1379
|
|
|
1277
|
-
const output = lspmuxStatus ? `${serverStatus}\n${lspmuxStatus}` : serverStatus;
|
|
1278
1380
|
return {
|
|
1279
|
-
content: [{ type: "text", text:
|
|
1381
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
1280
1382
|
details: { action, success: true, request: params },
|
|
1281
1383
|
};
|
|
1282
1384
|
}
|
|
@@ -1505,7 +1607,26 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
|
|
|
1505
1607
|
}
|
|
1506
1608
|
|
|
1507
1609
|
const lspParams = { files: pairs };
|
|
1508
|
-
|
|
1610
|
+
// Filter to servers whose fileTypes match either the source or any
|
|
1611
|
+
// destination path. Asking every configured server about a .md/.sql/.txt
|
|
1612
|
+
// rename used to stack up willRenameFiles requests against irrelevant
|
|
1613
|
+
// language servers and hit the wall-clock timeout. A server only has
|
|
1614
|
+
// something useful to say about a rename if it understands one of the
|
|
1615
|
+
// affected file extensions.
|
|
1616
|
+
const allLspServers = getLspServers(config);
|
|
1617
|
+
const relevantNames = new Set<string>();
|
|
1618
|
+
const collectRelevant = (filePath: string) => {
|
|
1619
|
+
for (const [name] of getLspServersForFile(config, filePath)) {
|
|
1620
|
+
relevantNames.add(name);
|
|
1621
|
+
}
|
|
1622
|
+
};
|
|
1623
|
+
collectRelevant(source);
|
|
1624
|
+
collectRelevant(dest);
|
|
1625
|
+
for (const pair of pairs) {
|
|
1626
|
+
collectRelevant(uriToFile(pair.oldUri));
|
|
1627
|
+
collectRelevant(uriToFile(pair.newUri));
|
|
1628
|
+
}
|
|
1629
|
+
const servers = allLspServers.filter(([name]) => relevantNames.has(name));
|
|
1509
1630
|
const respondingServers = new Set<string>();
|
|
1510
1631
|
const perServerEdits: Array<{ serverName: string; edit: WorkspaceEdit }> = [];
|
|
1511
1632
|
const serverNotes: string[] = [];
|
|
@@ -1829,8 +1950,15 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
|
|
|
1829
1950
|
throw new ToolAbortError();
|
|
1830
1951
|
}
|
|
1831
1952
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1953
|
+
// Echo a (truncated) preview of the params we sent so the caller can
|
|
1954
|
+
// tell parse / shape errors (e.g. nested args dropped, missing field)
|
|
1955
|
+
// apart from genuine server errors without spinning up another debug call.
|
|
1956
|
+
const previewRaw = JSON.stringify(requestParams ?? null);
|
|
1957
|
+
const preview = previewRaw.length > 400 ? `${previewRaw.slice(0, 397)}...` : previewRaw;
|
|
1832
1958
|
return {
|
|
1833
|
-
content: [
|
|
1959
|
+
content: [
|
|
1960
|
+
{ type: "text", text: `LSP error from ${chosenName} on ${method}: ${msg}\n params: ${preview}` },
|
|
1961
|
+
],
|
|
1834
1962
|
details: { action, serverName: chosenName, success: false, request: params },
|
|
1835
1963
|
};
|
|
1836
1964
|
}
|