@oh-my-pi/pi-coding-agent 15.10.2 → 15.10.4
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 +66 -1
- package/dist/types/cli/gallery-fixtures/types.d.ts +7 -1
- package/dist/types/edit/index.d.ts +0 -1
- package/dist/types/eval/__tests__/js-context-manager.test.d.ts +1 -0
- package/dist/types/eval/bridge-timeout.d.ts +1 -1
- package/dist/types/eval/{llm-bridge.d.ts → completion-bridge.d.ts} +8 -8
- package/dist/types/eval/idle-timeout.d.ts +1 -1
- package/dist/types/lsp/index.d.ts +0 -5
- package/dist/types/main.d.ts +11 -0
- package/dist/types/modes/components/assistant-message.d.ts +0 -9
- 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/tool-execution.d.ts +0 -18
- package/dist/types/modes/types.d.ts +4 -0
- package/dist/types/session/messages.d.ts +11 -8
- package/dist/types/session/yield-queue.d.ts +10 -1
- package/dist/types/tools/eval-render.d.ts +0 -1
- package/dist/types/tools/index.d.ts +31 -0
- package/dist/types/tools/path-utils.d.ts +5 -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/write.d.ts +0 -2
- 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/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/commit/agentic/agent.ts +1 -0
- package/src/edit/hashline/diff.ts +86 -0
- package/src/edit/hashline/execute.ts +14 -1
- package/src/edit/index.ts +31 -17
- package/src/edit/renderer.ts +116 -31
- package/src/eval/__tests__/agent-bridge.test.ts +13 -0
- package/src/eval/__tests__/{llm-bridge.test.ts → completion-bridge.test.ts} +60 -54
- package/src/eval/__tests__/js-context-manager.test.ts +241 -0
- package/src/eval/agent-bridge.ts +6 -1
- package/src/eval/bridge-timeout.ts +1 -1
- package/src/eval/{llm-bridge.ts → completion-bridge.ts} +30 -27
- package/src/eval/idle-timeout.ts +1 -1
- package/src/eval/js/context-manager.ts +66 -6
- package/src/eval/js/shared/prelude.txt +28 -12
- package/src/eval/js/tool-bridge.ts +3 -3
- package/src/eval/js/worker-entry.ts +6 -0
- package/src/eval/py/prelude.py +3 -3
- package/src/internal-urls/docs-index.generated.ts +8 -7
- package/src/lsp/index.ts +128 -52
- package/src/main.ts +54 -14
- package/src/modes/components/assistant-message.ts +3 -15
- package/src/modes/components/late-diagnostics-message.ts +60 -0
- 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/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/controllers/event-controller.ts +6 -1
- package/src/modes/controllers/input-controller.ts +10 -2
- package/src/modes/types.ts +4 -0
- package/src/modes/utils/ui-helpers.ts +26 -5
- package/src/prompts/system/manual-continue.md +7 -0
- package/src/prompts/system/plan-mode-active.md +56 -72
- package/src/prompts/system/tiny-title-system.md +1 -1
- package/src/prompts/system/title-system.md +16 -3
- package/src/prompts/system/workflow-notice.md +1 -1
- package/src/prompts/tools/eval.md +6 -4
- package/src/prompts/tools/lsp-late-diagnostic.md +8 -0
- package/src/sdk.ts +59 -1
- package/src/session/agent-session.ts +5 -3
- package/src/session/messages.ts +21 -14
- package/src/session/session-manager.ts +2 -2
- package/src/session/yield-queue.ts +20 -2
- package/src/task/executor.ts +1 -0
- package/src/tiny/title-client.ts +6 -1
- package/src/tools/bash.ts +0 -7
- package/src/tools/eval-render.ts +6 -25
- package/src/tools/eval.ts +1 -1
- package/src/tools/find.ts +148 -106
- package/src/tools/index.ts +32 -0
- package/src/tools/path-utils.ts +19 -22
- package/src/tools/read.ts +16 -8
- package/src/tools/render-utils.ts +3 -1
- package/src/tools/renderers.ts +0 -15
- package/src/tools/ssh.ts +0 -1
- package/src/tools/todo.ts +1 -0
- package/src/tools/write.ts +3 -12
- 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/title-generator.ts +2 -2
- /package/dist/types/eval/__tests__/{llm-bridge.test.d.ts → completion-bridge.test.d.ts} +0 -0
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) {
|
package/src/main.ts
CHANGED
|
@@ -171,7 +171,8 @@ export async function submitInteractiveInput(
|
|
|
171
171
|
|
|
172
172
|
try {
|
|
173
173
|
using _keepalive = new EventLoopKeepalive();
|
|
174
|
-
// Continue shortcuts submit an already-started
|
|
174
|
+
// Continue shortcuts submit an already-started synthetic developer prompt with
|
|
175
|
+
// no optimistic user message.
|
|
175
176
|
if (!input.started && !mode.markPendingSubmissionStarted(input)) {
|
|
176
177
|
return;
|
|
177
178
|
}
|
|
@@ -182,6 +183,8 @@ export async function submitInteractiveInput(
|
|
|
182
183
|
display: input.display ?? false,
|
|
183
184
|
attribution: "agent",
|
|
184
185
|
});
|
|
186
|
+
} else if (input.synthetic) {
|
|
187
|
+
await session.prompt(input.text, { synthetic: true, expandPromptTemplates: false });
|
|
185
188
|
} else {
|
|
186
189
|
await session.prompt(input.text, { images: input.images });
|
|
187
190
|
}
|
|
@@ -381,6 +384,22 @@ async function promptMoveSession(session: SessionInfo): Promise<SessionPromptRes
|
|
|
381
384
|
}
|
|
382
385
|
}
|
|
383
386
|
|
|
387
|
+
/**
|
|
388
|
+
* Friendly CLI failure raised by {@link createSessionManager} when the user's
|
|
389
|
+
* session-resolution flags (`--resume`/`--fork`/cross-project prompts) cannot
|
|
390
|
+
* be satisfied. {@link runRootCommand} catches it and prints a clean stderr
|
|
391
|
+
* message instead of letting it surface as `[Uncaught Exception]`
|
|
392
|
+
* (see issue #2084).
|
|
393
|
+
*/
|
|
394
|
+
export class SessionResolutionError extends Error {
|
|
395
|
+
readonly hint?: string;
|
|
396
|
+
constructor(message: string, hint?: string) {
|
|
397
|
+
super(message);
|
|
398
|
+
this.name = "SessionResolutionError";
|
|
399
|
+
this.hint = hint;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
384
403
|
type MissingCwdMoveResult =
|
|
385
404
|
| { status: "not-needed" }
|
|
386
405
|
| { status: "declined" }
|
|
@@ -400,7 +419,7 @@ async function moveMissingCwdSessionIfNeeded(
|
|
|
400
419
|
|
|
401
420
|
const movePromptResult = await askToMoveSession(session);
|
|
402
421
|
if (movePromptResult === "unavailable") {
|
|
403
|
-
throw new
|
|
422
|
+
throw new SessionResolutionError(
|
|
404
423
|
`Session "${sessionArg}" belongs to a directory that no longer exists (${sourceCwd}); run interactively to move it into the current project.`,
|
|
405
424
|
);
|
|
406
425
|
}
|
|
@@ -463,7 +482,7 @@ export async function createSessionManager(
|
|
|
463
482
|
): Promise<SessionManager | undefined> {
|
|
464
483
|
if (parsed.fork) {
|
|
465
484
|
if (parsed.noSession) {
|
|
466
|
-
throw new
|
|
485
|
+
throw new SessionResolutionError("--fork requires session persistence");
|
|
467
486
|
}
|
|
468
487
|
const forkSource = parsed.fork;
|
|
469
488
|
if (forkSource.includes("/") || forkSource.includes("\\") || forkSource.endsWith(".jsonl")) {
|
|
@@ -471,7 +490,10 @@ export async function createSessionManager(
|
|
|
471
490
|
}
|
|
472
491
|
const match = await resolveResumableSession(forkSource, cwd, parsed.sessionDir);
|
|
473
492
|
if (!match) {
|
|
474
|
-
throw new
|
|
493
|
+
throw new SessionResolutionError(
|
|
494
|
+
`Session "${forkSource}" not found.`,
|
|
495
|
+
"Run `omp --resume` without an argument to pick from recent sessions, or `omp` to start a new one.",
|
|
496
|
+
);
|
|
475
497
|
}
|
|
476
498
|
return await SessionManager.forkFrom(match.session.path, cwd, parsed.sessionDir);
|
|
477
499
|
}
|
|
@@ -486,7 +508,10 @@ export async function createSessionManager(
|
|
|
486
508
|
}
|
|
487
509
|
const match = await resolveResumableSession(sessionArg, cwd, parsed.sessionDir);
|
|
488
510
|
if (!match) {
|
|
489
|
-
throw new
|
|
511
|
+
throw new SessionResolutionError(
|
|
512
|
+
`Session "${sessionArg}" not found.`,
|
|
513
|
+
"Run `omp --resume` without an argument to pick from recent sessions, or `omp` to start a new one.",
|
|
514
|
+
);
|
|
490
515
|
}
|
|
491
516
|
if (match.scope === "local") {
|
|
492
517
|
const moveResult = await moveMissingCwdSessionIfNeeded(
|
|
@@ -522,7 +547,7 @@ export async function createSessionManager(
|
|
|
522
547
|
}
|
|
523
548
|
const forkPromptResult = await askToForkSession(match.session);
|
|
524
549
|
if (forkPromptResult === "unavailable") {
|
|
525
|
-
throw new
|
|
550
|
+
throw new SessionResolutionError(
|
|
526
551
|
`Session "${sessionArg}" is in another project (${match.session.cwd}); run interactively to fork it into the current project.`,
|
|
527
552
|
);
|
|
528
553
|
}
|
|
@@ -919,14 +944,29 @@ export async function runRootCommand(
|
|
|
919
944
|
);
|
|
920
945
|
}
|
|
921
946
|
|
|
922
|
-
// Create session manager based on CLI flags
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
947
|
+
// Create session manager based on CLI flags. SessionResolutionError signals a
|
|
948
|
+
// user-facing failure (unknown --resume/--fork id, non-interactive fork
|
|
949
|
+
// prompt, --fork with --no-session): print + exit cleanly instead of letting
|
|
950
|
+
// it surface as `[Uncaught Exception]` (see issue #2084).
|
|
951
|
+
let sessionManager: SessionManager | undefined;
|
|
952
|
+
try {
|
|
953
|
+
sessionManager = await logger.time(
|
|
954
|
+
"createSessionManager",
|
|
955
|
+
createSessionManager,
|
|
956
|
+
parsedArgs,
|
|
957
|
+
cwd,
|
|
958
|
+
settingsInstance,
|
|
959
|
+
);
|
|
960
|
+
} catch (error: unknown) {
|
|
961
|
+
if (error instanceof SessionResolutionError) {
|
|
962
|
+
process.stderr.write(`${chalk.red(`Error: ${error.message}`)}\n`);
|
|
963
|
+
if (error.hint) {
|
|
964
|
+
process.stderr.write(`${chalk.dim(error.hint)}\n`);
|
|
965
|
+
}
|
|
966
|
+
process.exit(1);
|
|
967
|
+
}
|
|
968
|
+
throw error;
|
|
969
|
+
}
|
|
930
970
|
|
|
931
971
|
// User declined the cross-project fork prompt — exit cleanly with a friendly
|
|
932
972
|
// message rather than letting the decline bubble up as an uncaught exception
|
|
@@ -4,7 +4,7 @@ import { formatNumber } from "@oh-my-pi/pi-utils";
|
|
|
4
4
|
import { settings } from "../../config/settings";
|
|
5
5
|
import type { AssistantThinkingRenderer } from "../../extensibility/extensions/types";
|
|
6
6
|
import { getMarkdownTheme, theme } from "../../modes/theme/theme";
|
|
7
|
-
import {
|
|
7
|
+
import { resolveAbortLabel, shouldRenderAbortReason } from "../../session/messages";
|
|
8
8
|
import { resolveImageOptions } from "../../tools/render-utils";
|
|
9
9
|
|
|
10
10
|
/**
|
|
@@ -74,18 +74,6 @@ export class AssistantMessageComponent extends Container {
|
|
|
74
74
|
return this.#transcriptBlockFinalized;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
/**
|
|
78
|
-
* Assistant text/thinking streams in append-only: earlier rendered rows never
|
|
79
|
-
* re-layout, new content only grows the block at the bottom. The transcript
|
|
80
|
-
* reports this so the renderer may commit scrolled-off head rows of a long
|
|
81
|
-
* streamed reply to native scrollback instead of dropping them (see
|
|
82
|
-
* `NativeScrollbackLiveRegion#getNativeScrollbackCommitSafeEnd`). Volatile
|
|
83
|
-
* blocks (tool previews that collapse) intentionally do not implement this.
|
|
84
|
-
*/
|
|
85
|
-
isTranscriptBlockAppendOnly(): boolean {
|
|
86
|
-
return true;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
77
|
markTranscriptBlockFinalized(): void {
|
|
90
78
|
this.#transcriptBlockFinalized = true;
|
|
91
79
|
}
|
|
@@ -252,7 +240,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
252
240
|
// But only if there are no tool calls (tool execution components will show the error)
|
|
253
241
|
const hasToolCalls = message.content.some(c => c.type === "toolCall");
|
|
254
242
|
if (!hasToolCalls) {
|
|
255
|
-
if (message.stopReason === "aborted" &&
|
|
243
|
+
if (message.stopReason === "aborted" && shouldRenderAbortReason(message.errorMessage)) {
|
|
256
244
|
const abortMessage = resolveAbortLabel(message.errorMessage);
|
|
257
245
|
if (hasVisibleContent) {
|
|
258
246
|
this.#contentContainer.addChild(new Spacer(1));
|
|
@@ -268,7 +256,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
268
256
|
}
|
|
269
257
|
if (
|
|
270
258
|
message.errorMessage &&
|
|
271
|
-
|
|
259
|
+
shouldRenderAbortReason(message.errorMessage) &&
|
|
272
260
|
message.stopReason !== "aborted" &&
|
|
273
261
|
message.stopReason !== "error"
|
|
274
262
|
) {
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Container, Text } from "@oh-my-pi/pi-tui";
|
|
2
|
+
import { formatDiagnostics } from "../../tools/render-utils";
|
|
3
|
+
import { getLanguageFromPath, theme } from "../theme/theme";
|
|
4
|
+
|
|
5
|
+
/** One file's worth of late LSP diagnostics, as carried on the transcript message. */
|
|
6
|
+
export interface LateDiagnosticsFile {
|
|
7
|
+
path?: string;
|
|
8
|
+
summary?: string;
|
|
9
|
+
errored?: boolean;
|
|
10
|
+
messages?: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Renders late LSP diagnostics (arrived after edit/write returned) in the
|
|
15
|
+
* transcript, reusing the same tree renderer the edit/write tools use so the
|
|
16
|
+
* styling stays consistent. Supports the global tool-output expand toggle.
|
|
17
|
+
*/
|
|
18
|
+
export class LateDiagnosticsMessageComponent extends Container {
|
|
19
|
+
#expanded = false;
|
|
20
|
+
|
|
21
|
+
constructor(private readonly files: LateDiagnosticsFile[]) {
|
|
22
|
+
super();
|
|
23
|
+
this.#rebuild();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
setExpanded(expanded: boolean): void {
|
|
27
|
+
if (this.#expanded === expanded) return;
|
|
28
|
+
this.#expanded = expanded;
|
|
29
|
+
this.#rebuild();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
override invalidate(): void {
|
|
33
|
+
super.invalidate();
|
|
34
|
+
this.#rebuild();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
#rebuild(): void {
|
|
38
|
+
this.clear();
|
|
39
|
+
|
|
40
|
+
const messages: string[] = [];
|
|
41
|
+
const summaries: string[] = [];
|
|
42
|
+
let errored = false;
|
|
43
|
+
for (const file of this.files) {
|
|
44
|
+
if (file.messages?.length) messages.push(...file.messages);
|
|
45
|
+
if (file.summary) summaries.push(file.summary);
|
|
46
|
+
if (file.errored) errored = true;
|
|
47
|
+
}
|
|
48
|
+
if (messages.length === 0) return;
|
|
49
|
+
|
|
50
|
+
const text = formatDiagnostics(
|
|
51
|
+
{ errored, summary: summaries.join(", "), messages },
|
|
52
|
+
this.#expanded,
|
|
53
|
+
theme,
|
|
54
|
+
fp => theme.getLangIcon(getLanguageFromPath(fp)),
|
|
55
|
+
{ title: "Late diagnostics" },
|
|
56
|
+
);
|
|
57
|
+
const body = text.replace(/^\n+/, "");
|
|
58
|
+
if (body) this.addChild(new Text(body, 1, 0));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -141,6 +141,9 @@ export class PlanReviewOverlay implements Component {
|
|
|
141
141
|
#bodyClickRows = new Set<number>();
|
|
142
142
|
/** 1-based column at/under which a region-row click targets the sidebar. */
|
|
143
143
|
#sidebarClickMaxCol = 0;
|
|
144
|
+
/** Option index the pointer is currently hovering, or undefined. Updated from
|
|
145
|
+
* motion mouse reports and cleared when the pointer leaves the option rows. */
|
|
146
|
+
#hoveredOption: number | undefined;
|
|
144
147
|
|
|
145
148
|
#annotating = false;
|
|
146
149
|
#input: Input;
|
|
@@ -315,9 +318,10 @@ export class PlanReviewOverlay implements Component {
|
|
|
315
318
|
* Hit-test an SGR mouse report (`\x1b[<b;x;yM/m`) against the click maps the
|
|
316
319
|
* last render recorded. Returns true when consumed. The fullscreen overlay
|
|
317
320
|
* paints from screen row 0, so a 1-based mouse row maps directly to the
|
|
318
|
-
* rendered-line index. Wheel scrolls the body;
|
|
319
|
-
*
|
|
320
|
-
* the body column focuses
|
|
321
|
+
* rendered-line index. Wheel scrolls the body; pointer motion lights up the
|
|
322
|
+
* hovered option row; a left click on an option activates it (select +
|
|
323
|
+
* confirm), on a ToC row jumps to that section, and on the body column focuses
|
|
324
|
+
* the body.
|
|
321
325
|
*/
|
|
322
326
|
#handleMouse(data: string): boolean {
|
|
323
327
|
const match = /^\x1b\[<(\d+);(\d+);(\d+)([Mm])$/.exec(data);
|
|
@@ -331,7 +335,13 @@ export class PlanReviewOverlay implements Component {
|
|
|
331
335
|
return true;
|
|
332
336
|
}
|
|
333
337
|
if (match[4] !== "M") return true; // release
|
|
334
|
-
if (button & 32)
|
|
338
|
+
if (button & 32) {
|
|
339
|
+
// Motion (hover or drag): light up the option row under the pointer so a
|
|
340
|
+
// mouse user gets the same affordance the keyboard cursor gives. Any
|
|
341
|
+
// non-option row clears the highlight.
|
|
342
|
+
this.#setHoveredOption(this.#optionClickRows.get(row));
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
335
345
|
if ((button & 3) !== 0) return true; // not the left button
|
|
336
346
|
const optionIndex = this.#optionClickRows.get(row);
|
|
337
347
|
if (optionIndex !== undefined) {
|
|
@@ -355,6 +365,12 @@ export class PlanReviewOverlay implements Component {
|
|
|
355
365
|
return true;
|
|
356
366
|
}
|
|
357
367
|
|
|
368
|
+
/** Set the hovered option from a hit-tested row, ignoring disabled rows and
|
|
369
|
+
* non-option rows (both clear the highlight). */
|
|
370
|
+
#setHoveredOption(index: number | undefined): void {
|
|
371
|
+
this.#hoveredOption = index !== undefined && !this.#disabled.has(index) ? index : undefined;
|
|
372
|
+
}
|
|
373
|
+
|
|
358
374
|
#cycleRegion(direction: number): void {
|
|
359
375
|
// Sidebar is skipped from the cycle when it is not shown.
|
|
360
376
|
const regions: Focus[] = this.#sidebarShown ? ["toc", "body", "actions"] : ["body", "actions"];
|
|
@@ -611,14 +627,19 @@ export class PlanReviewOverlay implements Component {
|
|
|
611
627
|
return this.#options.map((label, i) => {
|
|
612
628
|
const selected = i === this.#selectedIndex;
|
|
613
629
|
const isDisabled = this.#disabled.has(i);
|
|
630
|
+
const hovered = !isDisabled && i === this.#hoveredOption;
|
|
614
631
|
// The cursor marks the selected option; it dims when actions are not the
|
|
615
632
|
// focused region so the active region's highlight stays unambiguous.
|
|
616
633
|
const cursor = selected ? theme.fg(active ? "accent" : "dim", `${theme.nav.cursor} `) : " ";
|
|
617
|
-
|
|
634
|
+
let text = isDisabled
|
|
618
635
|
? theme.fg("dim", label)
|
|
619
636
|
: selected && active
|
|
620
637
|
? theme.bold(theme.fg("accent", label))
|
|
621
638
|
: theme.fg("text", label);
|
|
639
|
+
// A pointer hovering an option paints a highlight band behind its label,
|
|
640
|
+
// distinct from the keyboard selection (cursor glyph + bold accent) which
|
|
641
|
+
// stays where it is. One space of padding gives the band a button shape.
|
|
642
|
+
if (hovered) text = theme.bg("selectedBg", ` ${text} `);
|
|
622
643
|
return cursor + text;
|
|
623
644
|
});
|
|
624
645
|
}
|