@oh-my-pi/pi-coding-agent 17.0.3 → 17.0.5
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 +88 -35
- package/dist/cli.js +3540 -3521
- package/dist/types/advisor/advise-tool.d.ts +3 -0
- package/dist/types/advisor/runtime.d.ts +7 -1
- package/dist/types/async/job-manager.d.ts +2 -0
- package/dist/types/config/model-registry.d.ts +7 -0
- package/dist/types/config/model-resolver.d.ts +8 -0
- package/dist/types/config/models-config-schema.d.ts +10 -0
- package/dist/types/config/models-config.d.ts +6 -0
- package/dist/types/dap/client.d.ts +10 -0
- package/dist/types/dap/types.d.ts +6 -5
- package/dist/types/extensibility/extensions/runner.d.ts +4 -2
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +16 -0
- package/dist/types/launch/spawn-options.d.ts +10 -0
- package/dist/types/launch/spawn-options.test.d.ts +1 -0
- package/dist/types/mnemopi/state.d.ts +32 -9
- package/dist/types/modes/components/status-line/component.d.ts +2 -3
- package/dist/types/modes/components/status-line/types.d.ts +3 -1
- package/dist/types/modes/components/tool-execution.d.ts +2 -0
- package/dist/types/modes/components/transcript-container.d.ts +4 -3
- package/dist/types/modes/components/tree-selector.d.ts +6 -2
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/print-mode.d.ts +4 -0
- package/dist/types/modes/rpc/rpc-input.d.ts +5 -0
- package/dist/types/modes/rpc/rpc-mode.d.ts +1 -1
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +19 -1
- package/dist/types/session/messages.d.ts +6 -0
- package/dist/types/task/types.d.ts +6 -6
- package/dist/types/tools/browser/aria/aria-snapshot.d.ts +17 -7
- package/dist/types/tools/browser/cmux/socket-client.d.ts +2 -0
- package/dist/types/tools/browser/tab-protocol.d.ts +2 -0
- package/dist/types/tools/browser/tab-worker.d.ts +2 -0
- package/dist/types/tools/gh.d.ts +5 -3
- package/dist/types/tools/hub/index.d.ts +1 -1
- package/dist/types/tools/hub/jobs.d.ts +1 -0
- package/dist/types/tools/hub/types.d.ts +2 -0
- package/dist/types/tools/tool-timeouts.d.ts +1 -1
- package/dist/types/utils/git.d.ts +33 -0
- package/dist/types/web/search/providers/codex.d.ts +1 -1
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +150 -0
- package/src/advisor/advise-tool.ts +4 -0
- package/src/advisor/runtime.ts +38 -14
- package/src/async/job-manager.ts +3 -0
- package/src/cli/bench-cli.ts +8 -2
- package/src/cli/dry-balance-cli.ts +1 -0
- package/src/config/model-registry.ts +89 -8
- package/src/config/model-resolver.ts +78 -14
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings.ts +3 -1
- package/src/dap/client.ts +168 -1
- package/src/dap/config.ts +51 -1
- package/src/dap/session.ts +575 -234
- package/src/dap/types.ts +6 -5
- package/src/discovery/agents.ts +2 -2
- package/src/extensibility/extensions/runner.ts +6 -4
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +26 -0
- package/src/launch/broker.ts +34 -31
- package/src/launch/client.ts +7 -1
- package/src/launch/spawn-options.test.ts +31 -0
- package/src/launch/spawn-options.ts +17 -0
- package/src/lsp/types.ts +5 -1
- package/src/main.ts +17 -4
- package/src/mcp/transports/stdio.test.ts +9 -1
- package/src/mnemopi/backend.ts +1 -1
- package/src/mnemopi/embed-worker.ts +8 -7
- package/src/mnemopi/state.ts +45 -18
- package/src/modes/components/ask-dialog.ts +137 -73
- package/src/modes/components/status-line/component.ts +2 -2
- package/src/modes/components/status-line/segments.ts +20 -3
- package/src/modes/components/status-line/types.ts +3 -1
- package/src/modes/components/tool-execution.ts +5 -0
- package/src/modes/components/transcript-container.ts +18 -111
- package/src/modes/components/tree-selector.ts +10 -4
- package/src/modes/controllers/event-controller.ts +1 -2
- package/src/modes/controllers/input-controller.ts +1 -1
- package/src/modes/controllers/selector-controller.ts +29 -8
- package/src/modes/interactive-mode.ts +40 -8
- package/src/modes/noninteractive-dispose.test.ts +12 -1
- package/src/modes/print-mode.ts +21 -9
- package/src/modes/rpc/rpc-input.ts +38 -0
- package/src/modes/rpc/rpc-mode.ts +7 -2
- package/src/modes/setup-wizard/scenes/sign-in.ts +3 -1
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/ui-helpers.ts +3 -2
- package/src/prompts/tools/browser.md +3 -2
- package/src/prompts/tools/debug.md +2 -7
- package/src/prompts/tools/github.md +6 -1
- package/src/prompts/tools/hub.md +4 -2
- package/src/prompts/tools/task-async-contract.md +1 -0
- package/src/prompts/tools/task.md +9 -2
- package/src/sdk.ts +38 -6
- package/src/session/agent-session.ts +395 -162
- package/src/session/messages.test.ts +91 -0
- package/src/session/messages.ts +248 -110
- package/src/session/session-loader.ts +31 -3
- package/src/slash-commands/builtin-registry.ts +1 -0
- package/src/stt/recorder.ts +68 -55
- package/src/task/executor.ts +59 -33
- package/src/task/index.ts +46 -9
- package/src/task/types.ts +11 -8
- package/src/task/worktree.ts +10 -0
- package/src/tools/browser/aria/aria-snapshot.ts +36 -8
- package/src/tools/browser/cmux/cmux-tab.ts +2 -1
- package/src/tools/browser/cmux/socket-client.ts +139 -3
- package/src/tools/browser/run-cancellation.ts +4 -0
- package/src/tools/browser/tab-protocol.ts +2 -0
- package/src/tools/browser/tab-supervisor.ts +21 -11
- package/src/tools/browser/tab-worker.ts +199 -33
- package/src/tools/debug.ts +3 -0
- package/src/tools/gh.ts +40 -2
- package/src/tools/hub/index.ts +4 -1
- package/src/tools/hub/jobs.ts +42 -1
- package/src/tools/hub/types.ts +2 -0
- package/src/tools/tool-timeouts.ts +1 -1
- package/src/utils/git.ts +237 -0
- package/src/web/search/index.ts +9 -5
- package/src/web/search/providers/codex.ts +195 -99
|
@@ -3,7 +3,6 @@ import {
|
|
|
3
3
|
Container,
|
|
4
4
|
type NativeScrollbackCommittedRows,
|
|
5
5
|
type NativeScrollbackLiveRegion,
|
|
6
|
-
type NativeScrollbackReplay,
|
|
7
6
|
type RenderStablePrefix,
|
|
8
7
|
type ViewportTailProvider,
|
|
9
8
|
} from "@oh-my-pi/pi-tui";
|
|
@@ -124,8 +123,6 @@ interface BlockSegment {
|
|
|
124
123
|
sep: number;
|
|
125
124
|
/** Whether the block reported finalized when this segment was rendered. */
|
|
126
125
|
finalized: boolean;
|
|
127
|
-
/** Safe to drop after commit: produced while finalized, without post-finalize version tracking. */
|
|
128
|
-
compactable: boolean;
|
|
129
126
|
/** Block version observed when this segment was rendered (see {@link FinalizableBlock}). */
|
|
130
127
|
version: number | undefined;
|
|
131
128
|
}
|
|
@@ -161,12 +158,7 @@ const EMPTY_TAIL: readonly string[] = [];
|
|
|
161
158
|
*/
|
|
162
159
|
export class TranscriptContainer
|
|
163
160
|
extends Container
|
|
164
|
-
implements
|
|
165
|
-
NativeScrollbackLiveRegion,
|
|
166
|
-
NativeScrollbackCommittedRows,
|
|
167
|
-
NativeScrollbackReplay,
|
|
168
|
-
RenderStablePrefix,
|
|
169
|
-
ViewportTailProvider
|
|
161
|
+
implements NativeScrollbackLiveRegion, NativeScrollbackCommittedRows, RenderStablePrefix, ViewportTailProvider
|
|
170
162
|
{
|
|
171
163
|
// Bumped to retire every block segment at once (theme change / clear); a
|
|
172
164
|
// segment is only reused when its stored generation matches.
|
|
@@ -175,6 +167,7 @@ export class TranscriptContainer
|
|
|
175
167
|
// final: the leading finalized blocks plus the first live block's declared
|
|
176
168
|
// settled rows. TUI commits rows to native scrollback only above it.
|
|
177
169
|
#nativeScrollbackLiveRegionStart: number | undefined;
|
|
170
|
+
#nativeScrollbackLiveRegionPinned = false;
|
|
178
171
|
// Persistent assembled transcript rows. Rows before the stable floor are
|
|
179
172
|
// byte-identical to the previous render; rows at/after it were re-pushed.
|
|
180
173
|
#lines: string[] = [];
|
|
@@ -184,14 +177,6 @@ export class TranscriptContainer
|
|
|
184
177
|
// Finalized blocks wholly before this boundary are immutable on-screen history;
|
|
185
178
|
// their previous contribution can be replayed without calling render().
|
|
186
179
|
#committedRows = 0;
|
|
187
|
-
// Leading children whose rows were handed to native scrollback and dropped
|
|
188
|
-
// from the local frame. Children remain owned by the session and can be
|
|
189
|
-
// re-rendered when the TUI prepares a destructive full replay.
|
|
190
|
-
#compactedChildStart = 0;
|
|
191
|
-
// Suppresses re-compaction for the rehydrating render. The TUI feeds its old
|
|
192
|
-
// committed-row count immediately before render, so resetting that count
|
|
193
|
-
// alone cannot distinguish a replay from an ordinary update.
|
|
194
|
-
#replayPending = false;
|
|
195
180
|
// Stable-prefix floor accumulated across renders since the last
|
|
196
181
|
// getRenderStablePrefixRows() read (see RenderStablePrefix: reading
|
|
197
182
|
// consumes the report and re-bases the baseline). Out-of-band renders
|
|
@@ -207,14 +192,12 @@ export class TranscriptContainer
|
|
|
207
192
|
override clear(): void {
|
|
208
193
|
this.#generation++;
|
|
209
194
|
super.clear();
|
|
210
|
-
this.#compactedChildStart = 0;
|
|
211
195
|
this.#committedRows = 0;
|
|
212
|
-
this.#replayPending = false;
|
|
213
196
|
}
|
|
214
197
|
|
|
215
198
|
override setNativeScrollbackCommittedRows(rows: number): void {
|
|
216
199
|
this.#committedRows = Number.isFinite(rows) ? Math.max(0, Math.trunc(rows)) : 0;
|
|
217
|
-
for (let i =
|
|
200
|
+
for (let i = 0; i < this.children.length; i++) {
|
|
218
201
|
const child = this.children[i]!;
|
|
219
202
|
const segment = this.#segments[i];
|
|
220
203
|
if (segment === undefined || segment.component !== child) continue;
|
|
@@ -237,18 +220,6 @@ export class TranscriptContainer
|
|
|
237
220
|
}
|
|
238
221
|
}
|
|
239
222
|
|
|
240
|
-
override prepareNativeScrollbackReplay(): void {
|
|
241
|
-
// Replay retires the old terminal tape, so descendants may discard layout
|
|
242
|
-
// locks whose only purpose was keeping that immutable history byte-stable.
|
|
243
|
-
super.prepareNativeScrollbackReplay();
|
|
244
|
-
if (this.#compactedChildStart === 0) return;
|
|
245
|
-
this.#compactedChildStart = 0;
|
|
246
|
-
this.#replayPending = true;
|
|
247
|
-
this.#generation++;
|
|
248
|
-
this.#lines.length = 0;
|
|
249
|
-
this.#stableRowsFloor = 0;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
223
|
getRenderStablePrefixRows(): number {
|
|
253
224
|
const value = Math.min(this.#stableRowsFloor, this.#lines.length);
|
|
254
225
|
this.#stableRowsFloor = this.#lines.length;
|
|
@@ -259,6 +230,11 @@ export class TranscriptContainer
|
|
|
259
230
|
return this.#nativeScrollbackLiveRegionStart;
|
|
260
231
|
}
|
|
261
232
|
|
|
233
|
+
/** Propagates viewport pinning from the first still-mutating transcript block. */
|
|
234
|
+
isNativeScrollbackLiveRegionPinned(): boolean {
|
|
235
|
+
return this.#nativeScrollbackLiveRegionPinned;
|
|
236
|
+
}
|
|
237
|
+
|
|
262
238
|
/**
|
|
263
239
|
* Whether none of `component`'s rows (per the most recent render) have
|
|
264
240
|
* entered native scrollback. Callers that retract ephemeral blocks (IRC
|
|
@@ -269,14 +245,8 @@ export class TranscriptContainer
|
|
|
269
245
|
* committed rows and is safely removable.
|
|
270
246
|
*/
|
|
271
247
|
isBlockUncommitted(component: Component): boolean {
|
|
272
|
-
const index = this.children.indexOf(component);
|
|
273
|
-
// Compacted prefix is already committed native history and must not be
|
|
274
|
-
// retracted. Compacted slots may be sparse holes after a later re-render
|
|
275
|
-
// (render only fills from #compactedChildStart), so the loop below must
|
|
276
|
-
// skip undefined entries.
|
|
277
|
-
if (index >= 0 && index < this.#compactedChildStart) return false;
|
|
278
248
|
for (const segment of this.#segments) {
|
|
279
|
-
if (segment
|
|
249
|
+
if (segment.component !== component) continue;
|
|
280
250
|
return segment.rowCount === 0 || segment.startRow >= this.#committedRows;
|
|
281
251
|
}
|
|
282
252
|
return true;
|
|
@@ -330,7 +300,7 @@ export class TranscriptContainer
|
|
|
330
300
|
if (maxRows <= 0) return EMPTY_TAIL;
|
|
331
301
|
const collected: (readonly string[])[] = [];
|
|
332
302
|
let total = 0;
|
|
333
|
-
for (let i = this.children.length - 1; i >=
|
|
303
|
+
for (let i = this.children.length - 1; i >= 0 && total < maxRows; i--) {
|
|
334
304
|
const contribution = stripPlainBlankEdges(this.children[i]!.render(width));
|
|
335
305
|
if (contribution.length === 0) continue;
|
|
336
306
|
// One blank separator sits between this block and the (already
|
|
@@ -352,9 +322,9 @@ export class TranscriptContainer
|
|
|
352
322
|
override render(width: number): readonly string[] {
|
|
353
323
|
width = Math.max(1, width);
|
|
354
324
|
this.#nativeScrollbackLiveRegionStart = undefined;
|
|
325
|
+
this.#nativeScrollbackLiveRegionPinned = false;
|
|
355
326
|
|
|
356
327
|
const count = this.children.length;
|
|
357
|
-
if (this.#compactedChildStart > count) this.#compactedChildStart = count;
|
|
358
328
|
|
|
359
329
|
// Seal displaceable snapshots whose rows are already on the tape (per the
|
|
360
330
|
// previous frame's segments — the geometry the committed count was
|
|
@@ -364,7 +334,7 @@ export class TranscriptContainer
|
|
|
364
334
|
// frame, and every frame so a block that BECAME displaceable after its
|
|
365
335
|
// pending-preview rows committed (late result on a scrolled-off call) is
|
|
366
336
|
// caught too.
|
|
367
|
-
for (let i =
|
|
337
|
+
for (let i = 0; i < count && i < this.#segments.length; i++) {
|
|
368
338
|
const previous = this.#segments[i];
|
|
369
339
|
if (previous === undefined) continue;
|
|
370
340
|
if (previous.startRow >= this.#committedRows) break;
|
|
@@ -380,10 +350,14 @@ export class TranscriptContainer
|
|
|
380
350
|
// reaches.
|
|
381
351
|
let liveStartIndex = -1;
|
|
382
352
|
let hasLiveBlock = false;
|
|
383
|
-
for (let i =
|
|
353
|
+
for (let i = 0; i < count; i++) {
|
|
384
354
|
if (!isBlockFinalized(this.children[i]!)) {
|
|
385
355
|
liveStartIndex = i;
|
|
386
356
|
hasLiveBlock = true;
|
|
357
|
+
this.#nativeScrollbackLiveRegionPinned =
|
|
358
|
+
(
|
|
359
|
+
this.children[i] as Component & Partial<NativeScrollbackLiveRegion>
|
|
360
|
+
).isNativeScrollbackLiveRegionPinned?.() === true;
|
|
387
361
|
break;
|
|
388
362
|
}
|
|
389
363
|
}
|
|
@@ -412,7 +386,7 @@ export class TranscriptContainer
|
|
|
412
386
|
// Frame row cursor: rows emitted (reused or pushed) so far.
|
|
413
387
|
let row = 0;
|
|
414
388
|
let stableRows = 0;
|
|
415
|
-
for (let i =
|
|
389
|
+
for (let i = 0; i < count; i++) {
|
|
416
390
|
const child = this.children[i]!;
|
|
417
391
|
|
|
418
392
|
// This child's contribution: its current render with plain-blank
|
|
@@ -453,7 +427,6 @@ export class TranscriptContainer
|
|
|
453
427
|
previous.width === width &&
|
|
454
428
|
previous.generation === this.#generation);
|
|
455
429
|
const contribution = reusable ? previous.contribution : stripPlainBlankEdges(raw);
|
|
456
|
-
const compactable = finalized && version === undefined && previous?.finalized !== false;
|
|
457
430
|
|
|
458
431
|
// Empty (or stripped-to-nothing) children contribute nothing and never
|
|
459
432
|
// affect spacing. An empty still-live child still gates the commit
|
|
@@ -478,7 +451,6 @@ export class TranscriptContainer
|
|
|
478
451
|
rowCount: 0,
|
|
479
452
|
sep: 0,
|
|
480
453
|
finalized,
|
|
481
|
-
compactable,
|
|
482
454
|
version,
|
|
483
455
|
};
|
|
484
456
|
continue;
|
|
@@ -530,7 +502,6 @@ export class TranscriptContainer
|
|
|
530
502
|
rowCount,
|
|
531
503
|
sep,
|
|
532
504
|
finalized,
|
|
533
|
-
compactable,
|
|
534
505
|
version,
|
|
535
506
|
};
|
|
536
507
|
row += rowCount;
|
|
@@ -540,72 +511,8 @@ export class TranscriptContainer
|
|
|
540
511
|
if (lines.length !== row) lines.length = row;
|
|
541
512
|
this.#segments = segments;
|
|
542
513
|
this.#stableRowsFloor = Math.min(stableFloorBefore, stableRows, row);
|
|
543
|
-
if (this.#replayPending) {
|
|
544
|
-
this.#replayPending = false;
|
|
545
|
-
} else {
|
|
546
|
-
this.#compactCommittedPrefix();
|
|
547
|
-
}
|
|
548
514
|
return lines;
|
|
549
515
|
}
|
|
550
|
-
|
|
551
|
-
#compactCommittedPrefix(): void {
|
|
552
|
-
if (this.#committedRows <= 0 || this.#compactedChildStart >= this.children.length) return;
|
|
553
|
-
const lines = this.#lines;
|
|
554
|
-
const segments = this.#segments;
|
|
555
|
-
let dropRows = 0;
|
|
556
|
-
let dropUntil = this.#compactedChildStart;
|
|
557
|
-
for (let i = this.#compactedChildStart; i < segments.length; i++) {
|
|
558
|
-
const segment = segments[i];
|
|
559
|
-
if (segment === undefined || !segment.compactable) break;
|
|
560
|
-
const segmentEnd = segment.startRow + segment.rowCount;
|
|
561
|
-
if (segmentEnd > this.#committedRows) break;
|
|
562
|
-
dropRows = segmentEnd;
|
|
563
|
-
dropUntil = i + 1;
|
|
564
|
-
}
|
|
565
|
-
const retained = segments[dropUntil];
|
|
566
|
-
if (retained !== undefined && retained.sep > 0) {
|
|
567
|
-
const committedSeparatorRows = this.#committedRows - retained.startRow;
|
|
568
|
-
if (committedSeparatorRows <= 0) {
|
|
569
|
-
dropRows = 0;
|
|
570
|
-
dropUntil = this.#compactedChildStart;
|
|
571
|
-
} else {
|
|
572
|
-
const trim = Math.min(retained.sep, committedSeparatorRows);
|
|
573
|
-
dropRows += trim;
|
|
574
|
-
retained.sep -= trim;
|
|
575
|
-
retained.rowCount -= trim;
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
if (dropRows === 0) return;
|
|
579
|
-
|
|
580
|
-
lines.splice(0, dropRows);
|
|
581
|
-
for (let i = this.#compactedChildStart; i < dropUntil; i++) {
|
|
582
|
-
const segment = segments[i];
|
|
583
|
-
if (segment === undefined) continue;
|
|
584
|
-
segments[i] = {
|
|
585
|
-
component: segment.component,
|
|
586
|
-
rawRef: EMPTY_TAIL,
|
|
587
|
-
contribution: EMPTY_TAIL,
|
|
588
|
-
width: segment.width,
|
|
589
|
-
generation: segment.generation,
|
|
590
|
-
startRow: 0,
|
|
591
|
-
rowCount: 0,
|
|
592
|
-
sep: 0,
|
|
593
|
-
finalized: true,
|
|
594
|
-
compactable: false,
|
|
595
|
-
version: segment.version,
|
|
596
|
-
};
|
|
597
|
-
}
|
|
598
|
-
for (let i = dropUntil; i < segments.length; i++) {
|
|
599
|
-
const segment = segments[i];
|
|
600
|
-
if (segment !== undefined) segment.startRow -= dropRows;
|
|
601
|
-
}
|
|
602
|
-
this.#compactedChildStart = dropUntil;
|
|
603
|
-
this.#committedRows = Math.max(0, this.#committedRows - dropRows);
|
|
604
|
-
this.#stableRowsFloor = 0;
|
|
605
|
-
if (this.#nativeScrollbackLiveRegionStart !== undefined) {
|
|
606
|
-
this.#nativeScrollbackLiveRegionStart = Math.max(0, this.#nativeScrollbackLiveRegionStart - dropRows);
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
516
|
}
|
|
610
517
|
|
|
611
518
|
/**
|
|
@@ -66,7 +66,7 @@ class TreeList implements Component {
|
|
|
66
66
|
#activePathIds: Set<string> = new Set();
|
|
67
67
|
#lastSelectedId: string | null = null;
|
|
68
68
|
|
|
69
|
-
onSelect?: (entryId: string) => void;
|
|
69
|
+
onSelect?: (entryId: string, options: { summarize: boolean }) => void;
|
|
70
70
|
onCancel?: () => void;
|
|
71
71
|
onLabelEdit?: (entryId: string, currentLabel: string | undefined) => void;
|
|
72
72
|
|
|
@@ -792,10 +792,16 @@ class TreeList implements Component {
|
|
|
792
792
|
} else if (matchesKey(keyData, "right")) {
|
|
793
793
|
// Page down
|
|
794
794
|
this.#selectedIndex = Math.min(this.#filteredNodes.length - 1, this.#selectedIndex + this.maxVisibleLines);
|
|
795
|
+
} else if (matchesKey(keyData, "shift+enter") || matchesKey(keyData, "shift+return")) {
|
|
796
|
+
// Summarize-and-switch: fork with a branch summary without the extra prompt.
|
|
797
|
+
const selected = this.#filteredNodes[this.#selectedIndex];
|
|
798
|
+
if (selected && this.onSelect) {
|
|
799
|
+
this.onSelect(selected.node.entry.id, { summarize: true });
|
|
800
|
+
}
|
|
795
801
|
} else if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
|
|
796
802
|
const selected = this.#filteredNodes[this.#selectedIndex];
|
|
797
803
|
if (selected && this.onSelect) {
|
|
798
|
-
this.onSelect(selected.node.entry.id);
|
|
804
|
+
this.onSelect(selected.node.entry.id, { summarize: false });
|
|
799
805
|
}
|
|
800
806
|
} else if (matchesAppInterrupt(keyData)) {
|
|
801
807
|
if (this.#searchQuery) {
|
|
@@ -923,7 +929,7 @@ export class TreeSelectorComponent extends Container {
|
|
|
923
929
|
tree: SessionTreeNode[],
|
|
924
930
|
currentLeafId: string | null,
|
|
925
931
|
terminalHeight: number,
|
|
926
|
-
onSelect: (entryId: string) => void,
|
|
932
|
+
onSelect: (entryId: string, options: { summarize: boolean }) => void,
|
|
927
933
|
onCancel: () => void,
|
|
928
934
|
private readonly onLabelChangeCallback?: (entryId: string, label: string | undefined) => void,
|
|
929
935
|
initialFilterMode: FilterMode = "default",
|
|
@@ -948,7 +954,7 @@ export class TreeSelectorComponent extends Container {
|
|
|
948
954
|
new TruncatedText(
|
|
949
955
|
theme.fg(
|
|
950
956
|
"muted",
|
|
951
|
-
"
|
|
957
|
+
"Enter: switch. Shift+Enter: summarize & switch. Shift+L: label. Ctrl+O: filter. Alt+D/T/U/L/A: filter. Type to search",
|
|
952
958
|
),
|
|
953
959
|
0,
|
|
954
960
|
0,
|
|
@@ -247,7 +247,6 @@ export class EventController {
|
|
|
247
247
|
toolCallId: string,
|
|
248
248
|
result: { content: Array<{ type: string; data?: string; mimeType?: string }> },
|
|
249
249
|
): boolean {
|
|
250
|
-
if (!settings.get("terminal.showImages")) return false;
|
|
251
250
|
const assistantComponent = this.#readToolCallAssistantComponents.get(toolCallId);
|
|
252
251
|
if (!assistantComponent) return false;
|
|
253
252
|
const images: ImageContent[] = result.content
|
|
@@ -258,7 +257,7 @@ export class EventController {
|
|
|
258
257
|
.map(content => ({ type: "image", data: content.data, mimeType: content.mimeType }));
|
|
259
258
|
if (images.length === 0) return false;
|
|
260
259
|
assistantComponent.setToolResultImages(toolCallId, images);
|
|
261
|
-
return
|
|
260
|
+
return settings.get("terminal.showImages");
|
|
262
261
|
}
|
|
263
262
|
|
|
264
263
|
#insertAfterTranscriptComponent(anchor: Component | undefined, component: Component): boolean {
|
|
@@ -771,7 +771,7 @@ export class InputController {
|
|
|
771
771
|
// While loop mode is on, every user-typed prompt becomes the new loop
|
|
772
772
|
// prompt that auto-resubmits after each yield.
|
|
773
773
|
if (this.ctx.loopModeEnabled) {
|
|
774
|
-
this.ctx.
|
|
774
|
+
this.ctx.setLoopPrompt(text);
|
|
775
775
|
}
|
|
776
776
|
|
|
777
777
|
// Queue input during compaction
|
|
@@ -442,13 +442,20 @@ export class SelectorController {
|
|
|
442
442
|
break;
|
|
443
443
|
|
|
444
444
|
// Settings with UI side effects
|
|
445
|
-
case "showImages":
|
|
445
|
+
case "terminal.showImages":
|
|
446
|
+
case "showImages": {
|
|
447
|
+
const visible = value as boolean;
|
|
446
448
|
for (const child of this.ctx.chatContainer.children) {
|
|
447
449
|
if (child instanceof ToolExecutionComponent) {
|
|
448
|
-
child.setShowImages(
|
|
450
|
+
child.setShowImages(visible);
|
|
451
|
+
} else if (child instanceof AssistantMessageComponent) {
|
|
452
|
+
child.setImagesVisible(visible);
|
|
449
453
|
}
|
|
450
454
|
}
|
|
455
|
+
if (!visible) this.ctx.ui.clearInlineImages();
|
|
456
|
+
this.ctx.ui.resetDisplay();
|
|
451
457
|
break;
|
|
458
|
+
}
|
|
452
459
|
case "hideThinkingBlock":
|
|
453
460
|
this.ctx.hideThinkingBlock = value as boolean;
|
|
454
461
|
for (const child of this.ctx.chatContainer.children) {
|
|
@@ -1130,7 +1137,7 @@ export class SelectorController {
|
|
|
1130
1137
|
tree,
|
|
1131
1138
|
realLeafId,
|
|
1132
1139
|
this.ctx.ui.terminal.rows,
|
|
1133
|
-
async entryId => {
|
|
1140
|
+
async (entryId, options) => {
|
|
1134
1141
|
// Selecting the current leaf is a no-op (already there)
|
|
1135
1142
|
if (entryId === realLeafId) {
|
|
1136
1143
|
done();
|
|
@@ -1141,13 +1148,15 @@ export class SelectorController {
|
|
|
1141
1148
|
// Ask about summarization
|
|
1142
1149
|
done(); // Close selector first
|
|
1143
1150
|
|
|
1144
|
-
// Loop until user makes a complete choice or cancels to tree
|
|
1145
|
-
|
|
1151
|
+
// Loop until user makes a complete choice or cancels to tree.
|
|
1152
|
+
// Shift+Enter in the tree selector pre-answers "Summarize" and
|
|
1153
|
+
// skips the prompt entirely.
|
|
1154
|
+
let wantsSummary = options.summarize;
|
|
1146
1155
|
let customInstructions: string | undefined;
|
|
1147
1156
|
|
|
1148
1157
|
const branchSummariesEnabled = settings.get("branchSummary.enabled");
|
|
1149
1158
|
|
|
1150
|
-
while (branchSummariesEnabled) {
|
|
1159
|
+
while (!wantsSummary && branchSummariesEnabled) {
|
|
1151
1160
|
const summaryChoice = await this.ctx.showHookSelector("Summarize branch?", [
|
|
1152
1161
|
"No summary",
|
|
1153
1162
|
"Summarize",
|
|
@@ -1476,7 +1485,14 @@ export class SelectorController {
|
|
|
1476
1485
|
// focus (#5339).
|
|
1477
1486
|
onManualCodeInput: useManualInput ? () => dialog.showManualInput(MANUAL_LOGIN_PROMPT) : undefined,
|
|
1478
1487
|
});
|
|
1479
|
-
|
|
1488
|
+
// Scope the post-login refresh to the just-authenticated provider with an
|
|
1489
|
+
// `online` strategy: the default all-provider `online-if-uncached` reuses
|
|
1490
|
+
// a fresh authoritative cache row (e.g. an empty result fetched before
|
|
1491
|
+
// login), so newly persisted credentials would never re-run discovery and
|
|
1492
|
+
// models would stay unavailable in-session (#5780). Unrelated providers
|
|
1493
|
+
// are left untouched. `refreshProvider` swallows discovery failures, so
|
|
1494
|
+
// awaiting cannot reject the login.
|
|
1495
|
+
await this.ctx.session.modelRegistry.refreshProvider(providerId, "online");
|
|
1480
1496
|
const block = new TranscriptBlock();
|
|
1481
1497
|
// Name the account (and Anthropic organization) that was stored so a
|
|
1482
1498
|
// login that lands on an unintended account/subscription is visible
|
|
@@ -1516,7 +1532,12 @@ export class SelectorController {
|
|
|
1516
1532
|
return;
|
|
1517
1533
|
}
|
|
1518
1534
|
|
|
1519
|
-
|
|
1535
|
+
// Provider-scoped online refresh so the removed credential's stale
|
|
1536
|
+
// endpoint/deployment models are invalidated deterministically; the
|
|
1537
|
+
// default all-provider `online-if-uncached` would reuse the fresh
|
|
1538
|
+
// authoritative cache row and keep showing models the credential
|
|
1539
|
+
// unlocked (#5780). Other providers are left untouched.
|
|
1540
|
+
await this.ctx.session.modelRegistry.refreshProvider(providerId, "online");
|
|
1520
1541
|
const block = new TranscriptBlock();
|
|
1521
1542
|
block.addChild(
|
|
1522
1543
|
new Text(
|
|
@@ -208,6 +208,8 @@ import type {
|
|
|
208
208
|
} from "./types";
|
|
209
209
|
import { UiHelpers } from "./utils/ui-helpers";
|
|
210
210
|
|
|
211
|
+
const STILL_CLOSING_DELAY_MS = 3_000;
|
|
212
|
+
|
|
211
213
|
const HINT_SHIMMER_PALETTE: ShimmerPalette = {
|
|
212
214
|
low: "dim",
|
|
213
215
|
mid: "muted",
|
|
@@ -451,6 +453,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
451
453
|
vibeModeEnabled = false;
|
|
452
454
|
planModePlanFilePath: string | undefined = undefined;
|
|
453
455
|
loopModeEnabled = false;
|
|
456
|
+
loopModePaused = false;
|
|
454
457
|
loopPrompt: string | undefined = undefined;
|
|
455
458
|
loopLimit: LoopLimitRuntime | undefined = undefined;
|
|
456
459
|
#loopAutoSubmitTimer: NodeJS.Timeout | undefined;
|
|
@@ -696,6 +699,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
696
699
|
this.errorBannerContainer = new AnchoredLiveContainer();
|
|
697
700
|
this.modelCycleContainer = new AnchoredLiveContainer();
|
|
698
701
|
this.editor = new CustomEditor(getEditorTheme());
|
|
702
|
+
this.ui.enableScopedInputRender(this.editor);
|
|
699
703
|
this.editor.setUseTerminalCursor(this.ui.getShowHardwareCursor());
|
|
700
704
|
this.editor.setImeSafeCursorLayout(settings.get("tui.imeSafeCursor"));
|
|
701
705
|
this.editor.setAutocompleteMaxVisible(settings.get("autocompleteMaxVisible"));
|
|
@@ -1349,6 +1353,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1349
1353
|
this.disableLoopMode("Loop limit reached. Loop mode disabled.");
|
|
1350
1354
|
return;
|
|
1351
1355
|
}
|
|
1356
|
+
this.#syncLoopModeStatus();
|
|
1352
1357
|
|
|
1353
1358
|
if (action === "compact") {
|
|
1354
1359
|
await this.handleCompactCommand();
|
|
@@ -1358,19 +1363,36 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1358
1363
|
this.#submitLoopPromptWhenReady(prompt);
|
|
1359
1364
|
}
|
|
1360
1365
|
|
|
1366
|
+
#syncLoopModeStatus(): void {
|
|
1367
|
+
const state: "waiting" | "running" | "paused" = this.loopModePaused
|
|
1368
|
+
? "paused"
|
|
1369
|
+
: this.loopPrompt
|
|
1370
|
+
? "running"
|
|
1371
|
+
: "waiting";
|
|
1372
|
+
this.statusLine.setLoopModeStatus(this.loopModeEnabled ? { state, limit: this.loopLimit } : undefined);
|
|
1373
|
+
this.ui.requestRender();
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1361
1376
|
disableLoopMode(message = "Loop mode disabled."): void {
|
|
1362
1377
|
const wasEnabled = this.loopModeEnabled;
|
|
1363
1378
|
this.loopModeEnabled = false;
|
|
1379
|
+
this.loopModePaused = false;
|
|
1364
1380
|
this.loopPrompt = undefined;
|
|
1365
1381
|
this.loopLimit = undefined;
|
|
1366
1382
|
this.#cancelLoopAutoSubmit();
|
|
1367
|
-
this
|
|
1368
|
-
this.ui.requestRender();
|
|
1383
|
+
this.#syncLoopModeStatus();
|
|
1369
1384
|
if (wasEnabled) {
|
|
1370
1385
|
this.showStatus(message);
|
|
1371
1386
|
}
|
|
1372
1387
|
}
|
|
1373
1388
|
|
|
1389
|
+
setLoopPrompt(prompt: string): void {
|
|
1390
|
+
if (!this.loopModeEnabled) return;
|
|
1391
|
+
this.loopPrompt = prompt;
|
|
1392
|
+
this.loopModePaused = false;
|
|
1393
|
+
this.#syncLoopModeStatus();
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1374
1396
|
/**
|
|
1375
1397
|
* Pause the loop without exiting it: drops the captured prompt and any
|
|
1376
1398
|
* pending auto-resubmit. Loop mode stays enabled — the next prompt the
|
|
@@ -1378,7 +1400,9 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1378
1400
|
*/
|
|
1379
1401
|
pauseLoop(): void {
|
|
1380
1402
|
this.loopPrompt = undefined;
|
|
1403
|
+
this.loopModePaused = true;
|
|
1381
1404
|
this.#cancelLoopAutoSubmit();
|
|
1405
|
+
this.#syncLoopModeStatus();
|
|
1382
1406
|
}
|
|
1383
1407
|
|
|
1384
1408
|
async handleLoopCommand(args = ""): Promise<string | undefined> {
|
|
@@ -1392,10 +1416,10 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1392
1416
|
return undefined;
|
|
1393
1417
|
}
|
|
1394
1418
|
this.loopModeEnabled = true;
|
|
1419
|
+
this.loopModePaused = false;
|
|
1395
1420
|
this.loopPrompt = undefined;
|
|
1396
1421
|
this.loopLimit = createLoopLimitRuntime(parsed.limit);
|
|
1397
|
-
this
|
|
1398
|
-
this.ui.requestRender();
|
|
1422
|
+
this.#syncLoopModeStatus();
|
|
1399
1423
|
const limitSuffix = parsed.limit ? ` Limited to ${describeLoopLimit(parsed.limit)}.` : "";
|
|
1400
1424
|
const remainingSuffix = this.loopLimit ? ` ${describeLoopLimitRuntime(this.loopLimit)}.` : "";
|
|
1401
1425
|
const tail = parsed.prompt ? "Repeating it after each turn." : "Your next prompt will repeat after each turn.";
|
|
@@ -3746,10 +3770,17 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
3746
3770
|
// first runs the work, the other awaits the same settled promise.
|
|
3747
3771
|
// The teardown is registered lazily in `init()` — a `/exit` reached
|
|
3748
3772
|
// before `init()` completed falls back to a direct dispose.
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
}
|
|
3752
|
-
|
|
3773
|
+
const stillClosingTimer = setTimeout(() => {
|
|
3774
|
+
this.showStatus("Still closing… (flushing memory backend / network)");
|
|
3775
|
+
}, STILL_CLOSING_DELAY_MS);
|
|
3776
|
+
try {
|
|
3777
|
+
if (this.#signalTeardown) {
|
|
3778
|
+
await this.#signalTeardown();
|
|
3779
|
+
} else {
|
|
3780
|
+
await this.session.dispose({ mnemopiConsolidateTimeoutMs: SHUTDOWN_CONSOLIDATE_BUDGET_MS });
|
|
3781
|
+
}
|
|
3782
|
+
} finally {
|
|
3783
|
+
clearTimeout(stillClosingTimer);
|
|
3753
3784
|
}
|
|
3754
3785
|
|
|
3755
3786
|
// Do not force a final render during teardown: disposed session/UI state can
|
|
@@ -3794,6 +3825,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
3794
3825
|
const nextEditor = factory
|
|
3795
3826
|
? factory(this.ui, getEditorTheme(), this.keybindings)
|
|
3796
3827
|
: new CustomEditor(getEditorTheme());
|
|
3828
|
+
if (!factory) this.ui.enableScopedInputRender(nextEditor);
|
|
3797
3829
|
|
|
3798
3830
|
nextEditor.setUseTerminalCursor(this.ui.getShowHardwareCursor());
|
|
3799
3831
|
nextEditor.setImeSafeCursorLayout(this.settings.get("tui.imeSafeCursor"));
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { describe, expect, it, spyOn } from "bun:test";
|
|
9
9
|
import type { AssistantMessage } from "@oh-my-pi/pi-ai";
|
|
10
10
|
import type { AgentSession } from "../session/agent-session";
|
|
11
|
+
import * as telemetryExport from "../telemetry-export";
|
|
11
12
|
import { runPrintMode } from "./print-mode";
|
|
12
13
|
|
|
13
14
|
/** Stand-in for `process.exit`: it terminates, so nothing after it should run. */
|
|
@@ -35,11 +36,20 @@ describe("print-mode error exit disposes the session before exit", () => {
|
|
|
35
36
|
extensionRunner: undefined,
|
|
36
37
|
subscribe: () => {},
|
|
37
38
|
state: { messages: [errorMsg] },
|
|
39
|
+
getLastAssistantMessage: () => errorMsg,
|
|
40
|
+
prepareForHeadlessAdvisorDrain: () => {},
|
|
41
|
+
waitForAdvisorCatchup: async () => {
|
|
42
|
+
order.push("catchup");
|
|
43
|
+
return true;
|
|
44
|
+
},
|
|
38
45
|
dispose: async () => {
|
|
39
46
|
order.push("dispose");
|
|
40
47
|
},
|
|
41
48
|
} as unknown as AgentSession;
|
|
42
49
|
|
|
50
|
+
const flushSpy = spyOn(telemetryExport, "flushTelemetryExport").mockImplementation(async () => {
|
|
51
|
+
order.push("flush");
|
|
52
|
+
});
|
|
43
53
|
const exitSpy = spyOn(process, "exit").mockImplementation(((code: number) => {
|
|
44
54
|
order.push("exit");
|
|
45
55
|
throw new ProcessExit(code);
|
|
@@ -53,8 +63,9 @@ describe("print-mode error exit disposes the session before exit", () => {
|
|
|
53
63
|
} finally {
|
|
54
64
|
exitSpy.mockRestore();
|
|
55
65
|
stderrSpy.mockRestore();
|
|
66
|
+
flushSpy.mockRestore();
|
|
56
67
|
}
|
|
57
68
|
|
|
58
|
-
expect(order).toEqual(["dispose", "exit"]);
|
|
69
|
+
expect(order).toEqual(["catchup", "flush", "dispose", "exit"]);
|
|
59
70
|
});
|
|
60
71
|
});
|
package/src/modes/print-mode.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* - `omp --mode json "prompt"` - JSON event stream
|
|
7
7
|
*/
|
|
8
8
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
9
|
-
import type {
|
|
9
|
+
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
|
10
10
|
import { logger, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
11
11
|
import { type AgentSession, type AgentSessionEvent, SHUTDOWN_CONSOLIDATE_BUDGET_MS } from "../session/agent-session";
|
|
12
12
|
import { isSilentAbort } from "../session/messages";
|
|
@@ -29,6 +29,11 @@ export interface PrintModeOptions {
|
|
|
29
29
|
printThoughts?: boolean;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/** Matches the longest built-in provider request deadline while bounding tool-loop stalls. */
|
|
33
|
+
export const PRINT_MODE_ADVISOR_DRAIN_TIMEOUT_MS = 10 * 60_000;
|
|
34
|
+
/** Error exits cannot hold automation for the full normal drain budget. */
|
|
35
|
+
export const PRINT_MODE_ERROR_ADVISOR_DRAIN_TIMEOUT_MS = 30_000;
|
|
36
|
+
|
|
32
37
|
/** Drop the provider-opaque replay payload (e.g. encrypted reasoning items) before printing. */
|
|
33
38
|
function stripProviderPayload<T extends AgentMessage>(message: T): T {
|
|
34
39
|
if (!("providerPayload" in message) || message.providerPayload === undefined) return message;
|
|
@@ -130,14 +135,19 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
|
|
|
130
135
|
await logger.time("print:prompt:next", () => session.prompt(message));
|
|
131
136
|
}
|
|
132
137
|
|
|
138
|
+
// From this point onward a late blocker must be recorded without starting a
|
|
139
|
+
// primary turn whose response print mode would never emit.
|
|
140
|
+
session.prepareForHeadlessAdvisorDrain();
|
|
141
|
+
|
|
133
142
|
// In text mode, output final response
|
|
134
143
|
if (mode === "text") {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
144
|
+
// Read via the session accessor, not the raw state tail: a classifier
|
|
145
|
+
// refusal is pruned from active context at settle, and an aborted turn
|
|
146
|
+
// can trail synthetic tool results — both would hide the terminal
|
|
147
|
+
// assistant message (and its error) from a last-element read.
|
|
148
|
+
const assistantMsg = session.getLastAssistantMessage();
|
|
140
149
|
|
|
150
|
+
if (assistantMsg) {
|
|
141
151
|
// Check for error/aborted — skip silent-abort (plan-mode compaction transition)
|
|
142
152
|
if (
|
|
143
153
|
(assistantMsg.stopReason === "error" || assistantMsg.stopReason === "aborted") &&
|
|
@@ -151,6 +161,7 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
|
|
|
151
161
|
// `dispose()` (releaseTabsForOwner) actually runs — otherwise an
|
|
152
162
|
// OMP-owned Chromium survives this exit (issue #5643). `dispose()`
|
|
153
163
|
// is idempotent, so the unreachable call below is a harmless no-op.
|
|
164
|
+
await session.waitForAdvisorCatchup(PRINT_MODE_ERROR_ADVISOR_DRAIN_TIMEOUT_MS);
|
|
154
165
|
await flushTelemetryExport();
|
|
155
166
|
await session.dispose({ mnemopiConsolidateTimeoutMs: SHUTDOWN_CONSOLIDATE_BUDGET_MS });
|
|
156
167
|
const flushed = process.stderr.write(`${errorLine}\n`);
|
|
@@ -180,14 +191,15 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
|
|
|
180
191
|
}
|
|
181
192
|
}
|
|
182
193
|
|
|
183
|
-
|
|
184
|
-
|
|
194
|
+
await session.waitForAdvisorCatchup(PRINT_MODE_ADVISOR_DRAIN_TIMEOUT_MS);
|
|
195
|
+
|
|
196
|
+
// Ensure stdout, including late JSON advisor events, is fully flushed before returning.
|
|
197
|
+
// This prevents race conditions where the process exits before all output is written.
|
|
185
198
|
await new Promise<void>((resolve, reject) => {
|
|
186
199
|
process.stdout.write("", err => {
|
|
187
200
|
if (err) reject(err);
|
|
188
201
|
else resolve();
|
|
189
202
|
});
|
|
190
203
|
});
|
|
191
|
-
|
|
192
204
|
await session.dispose({ mnemopiConsolidateTimeoutMs: SHUTDOWN_CONSOLIDATE_BUDGET_MS });
|
|
193
205
|
}
|