@oh-my-pi/pi-coding-agent 17.0.4 → 17.0.6

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.
Files changed (143) hide show
  1. package/CHANGELOG.md +209 -0
  2. package/dist/cli.js +3616 -3676
  3. package/dist/types/advisor/advise-tool.d.ts +3 -0
  4. package/dist/types/advisor/runtime.d.ts +7 -1
  5. package/dist/types/async/job-manager.d.ts +2 -0
  6. package/dist/types/config/model-registry.d.ts +7 -0
  7. package/dist/types/config/model-resolver.d.ts +8 -0
  8. package/dist/types/config/models-config-schema.d.ts +10 -0
  9. package/dist/types/config/models-config.d.ts +6 -0
  10. package/dist/types/dap/client.d.ts +10 -0
  11. package/dist/types/dap/types.d.ts +6 -5
  12. package/dist/types/exec/bash-executor.d.ts +1 -0
  13. package/dist/types/extensibility/extensions/load-errors.d.ts +3 -0
  14. package/dist/types/extensibility/extensions/runner.d.ts +4 -2
  15. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +16 -0
  16. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +8 -0
  17. package/dist/types/launch/spawn-options.d.ts +10 -0
  18. package/dist/types/launch/spawn-options.test.d.ts +1 -0
  19. package/dist/types/modes/components/status-line/component.d.ts +10 -3
  20. package/dist/types/modes/components/status-line/types.d.ts +3 -1
  21. package/dist/types/modes/components/tool-execution.d.ts +2 -0
  22. package/dist/types/modes/components/transcript-container.d.ts +4 -3
  23. package/dist/types/modes/components/tree-selector.d.ts +6 -2
  24. package/dist/types/modes/components/usage-row.d.ts +1 -1
  25. package/dist/types/modes/interactive-mode.d.ts +2 -0
  26. package/dist/types/modes/print-mode.d.ts +4 -0
  27. package/dist/types/modes/rpc/rpc-client.d.ts +1 -1
  28. package/dist/types/modes/rpc/rpc-frame.d.ts +9 -0
  29. package/dist/types/modes/rpc/rpc-input.d.ts +5 -0
  30. package/dist/types/modes/rpc/rpc-mode.d.ts +1 -1
  31. package/dist/types/modes/setup-wizard/index.d.ts +1 -1
  32. package/dist/types/modes/setup-wizard/scenes/model.d.ts +3 -0
  33. package/dist/types/modes/types.d.ts +2 -0
  34. package/dist/types/sdk.d.ts +2 -0
  35. package/dist/types/session/agent-session.d.ts +42 -6
  36. package/dist/types/session/messages.d.ts +6 -0
  37. package/dist/types/session/session-paths.d.ts +21 -4
  38. package/dist/types/tools/browser/aria/aria-snapshot.d.ts +17 -7
  39. package/dist/types/tools/browser/cmux/socket-client.d.ts +2 -0
  40. package/dist/types/tools/browser/tab-protocol.d.ts +2 -0
  41. package/dist/types/tools/browser/tab-worker.d.ts +2 -0
  42. package/dist/types/tools/gh.d.ts +5 -3
  43. package/dist/types/tools/hub/index.d.ts +1 -1
  44. package/dist/types/tools/hub/jobs.d.ts +1 -0
  45. package/dist/types/tools/hub/types.d.ts +2 -0
  46. package/dist/types/tools/tool-timeouts.d.ts +1 -1
  47. package/dist/types/tools/xdev.d.ts +5 -3
  48. package/dist/types/utils/git.d.ts +33 -0
  49. package/dist/types/vibe/__tests__/token-rate.test.d.ts +1 -0
  50. package/dist/types/vibe/runtime.d.ts +23 -0
  51. package/dist/types/web/search/providers/codex.d.ts +1 -1
  52. package/package.json +12 -12
  53. package/src/advisor/__tests__/advisor.test.ts +150 -0
  54. package/src/advisor/advise-tool.ts +4 -0
  55. package/src/advisor/runtime.ts +38 -14
  56. package/src/async/job-manager.ts +3 -0
  57. package/src/cli/bench-cli.ts +8 -2
  58. package/src/cli/dry-balance-cli.ts +1 -0
  59. package/src/config/model-registry.ts +89 -8
  60. package/src/config/model-resolver.ts +78 -14
  61. package/src/config/models-config-schema.ts +1 -0
  62. package/src/config/settings.ts +3 -1
  63. package/src/dap/client.ts +168 -1
  64. package/src/dap/config.ts +51 -1
  65. package/src/dap/session.ts +575 -234
  66. package/src/dap/types.ts +6 -5
  67. package/src/discovery/agents.ts +2 -2
  68. package/src/exec/bash-executor.ts +68 -8
  69. package/src/extensibility/extensions/load-errors.ts +13 -0
  70. package/src/extensibility/extensions/runner.ts +6 -4
  71. package/src/extensibility/legacy-pi-coding-agent-shim.ts +26 -0
  72. package/src/extensibility/plugins/legacy-pi-compat.ts +41 -6
  73. package/src/launch/broker.ts +34 -31
  74. package/src/launch/client.ts +7 -1
  75. package/src/launch/spawn-options.test.ts +31 -0
  76. package/src/launch/spawn-options.ts +17 -0
  77. package/src/lsp/types.ts +5 -1
  78. package/src/main.ts +25 -4
  79. package/src/mcp/transports/stdio.test.ts +9 -1
  80. package/src/modes/components/ask-dialog.ts +137 -73
  81. package/src/modes/components/chat-transcript-builder.ts +10 -1
  82. package/src/modes/components/status-line/component.ts +57 -3
  83. package/src/modes/components/status-line/segments.ts +20 -3
  84. package/src/modes/components/status-line/types.ts +3 -1
  85. package/src/modes/components/tool-execution.ts +5 -0
  86. package/src/modes/components/transcript-container.ts +23 -122
  87. package/src/modes/components/tree-selector.ts +10 -4
  88. package/src/modes/components/usage-row.ts +17 -1
  89. package/src/modes/controllers/command-controller.ts +41 -1
  90. package/src/modes/controllers/event-controller.ts +7 -3
  91. package/src/modes/controllers/input-controller.ts +1 -1
  92. package/src/modes/controllers/selector-controller.ts +29 -8
  93. package/src/modes/interactive-mode.ts +102 -60
  94. package/src/modes/noninteractive-dispose.test.ts +14 -1
  95. package/src/modes/print-mode.ts +81 -9
  96. package/src/modes/rpc/rpc-client.ts +76 -35
  97. package/src/modes/rpc/rpc-frame.ts +156 -0
  98. package/src/modes/rpc/rpc-input.ts +38 -0
  99. package/src/modes/rpc/rpc-mode.ts +11 -4
  100. package/src/modes/setup-wizard/index.ts +2 -0
  101. package/src/modes/setup-wizard/scenes/model.ts +134 -0
  102. package/src/modes/setup-wizard/scenes/sign-in.ts +3 -1
  103. package/src/modes/types.ts +2 -0
  104. package/src/modes/utils/ui-helpers.ts +9 -3
  105. package/src/prompts/system/workflow-notice.md +89 -74
  106. package/src/prompts/tools/browser.md +3 -2
  107. package/src/prompts/tools/debug.md +2 -7
  108. package/src/prompts/tools/github.md +6 -1
  109. package/src/prompts/tools/hub.md +4 -2
  110. package/src/prompts/tools/task-async-contract.md +1 -0
  111. package/src/prompts/tools/task.md +9 -2
  112. package/src/sdk.ts +38 -6
  113. package/src/session/agent-session.ts +484 -188
  114. package/src/session/messages.test.ts +91 -0
  115. package/src/session/messages.ts +248 -110
  116. package/src/session/session-manager.ts +34 -3
  117. package/src/session/session-paths.ts +38 -9
  118. package/src/slash-commands/builtin-registry.ts +1 -0
  119. package/src/task/executor.ts +104 -33
  120. package/src/task/index.ts +46 -9
  121. package/src/task/worktree.ts +10 -0
  122. package/src/tools/browser/aria/aria-snapshot.ts +36 -8
  123. package/src/tools/browser/cmux/cmux-tab.ts +2 -1
  124. package/src/tools/browser/cmux/socket-client.ts +139 -3
  125. package/src/tools/browser/run-cancellation.ts +4 -0
  126. package/src/tools/browser/tab-protocol.ts +2 -0
  127. package/src/tools/browser/tab-supervisor.ts +21 -11
  128. package/src/tools/browser/tab-worker.ts +199 -33
  129. package/src/tools/debug.ts +3 -0
  130. package/src/tools/gh.ts +40 -2
  131. package/src/tools/hub/index.ts +4 -1
  132. package/src/tools/hub/jobs.ts +42 -1
  133. package/src/tools/hub/types.ts +2 -0
  134. package/src/tools/tool-timeouts.ts +1 -1
  135. package/src/tools/xdev.ts +11 -4
  136. package/src/tools/yield.ts +4 -1
  137. package/src/utils/git.ts +237 -0
  138. package/src/{modes/components/status-line → utils}/token-rate.ts +6 -0
  139. package/src/vibe/__tests__/token-rate.test.ts +96 -0
  140. package/src/vibe/runtime.ts +50 -0
  141. package/src/web/search/index.ts +9 -5
  142. package/src/web/search/providers/codex.ts +195 -99
  143. /package/dist/types/{modes/components/status-line → utils}/token-rate.d.ts +0 -0
@@ -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";
@@ -19,17 +18,11 @@ interface FinalizableBlock {
19
18
  /**
20
19
  * Monotonic content version for blocks that can still mutate *after*
21
20
  * reporting finalized (e.g. `AssistantMessageComponent`: the inline error
22
- * restored at the next turn's `agent_start`, late tool-result images). While
23
- * the block's rows are still on screen (not yet fully committed), the
24
- * committed-scrollback render bypass replays its previous rows only when the
25
- * version is unchanged; a bump forces a real render so the TUI's
26
- * committed-prefix audit can observe and re-anchor the change. Once the rows
27
- * fully commit to native scrollback they are dropped from the local frame
28
- * (compacted) — a later mutation no longer recommits on an ordinary frame
29
- * (immutable history the terminal owns; recommitting would duplicate it) and
30
- * instead rehydrates on the next destructive full replay
31
- * ({@link "@oh-my-pi/pi-tui".NativeScrollbackReplay}). Blocks that never
32
- * mutate post-finalize simply omit the method.
21
+ * restored at the next turn's `agent_start`, late tool-result images). The
22
+ * committed-scrollback render bypass only replays a block's previous rows
23
+ * when the version is unchanged; without this signal a post-finalize
24
+ * mutation would stay invisible until a global invalidation. Blocks that
25
+ * never mutate post-finalize simply omit the method.
33
26
  */
34
27
  getTranscriptBlockVersion?(): number;
35
28
  /**
@@ -130,8 +123,6 @@ interface BlockSegment {
130
123
  sep: number;
131
124
  /** Whether the block reported finalized when this segment was rendered. */
132
125
  finalized: boolean;
133
- /** Safe to drop from the local frame once its rows fully commit to native scrollback: produced while finalized. */
134
- compactable: boolean;
135
126
  /** Block version observed when this segment was rendered (see {@link FinalizableBlock}). */
136
127
  version: number | undefined;
137
128
  }
@@ -167,12 +158,7 @@ const EMPTY_TAIL: readonly string[] = [];
167
158
  */
168
159
  export class TranscriptContainer
169
160
  extends Container
170
- implements
171
- NativeScrollbackLiveRegion,
172
- NativeScrollbackCommittedRows,
173
- NativeScrollbackReplay,
174
- RenderStablePrefix,
175
- ViewportTailProvider
161
+ implements NativeScrollbackLiveRegion, NativeScrollbackCommittedRows, RenderStablePrefix, ViewportTailProvider
176
162
  {
177
163
  // Bumped to retire every block segment at once (theme change / clear); a
178
164
  // segment is only reused when its stored generation matches.
@@ -181,6 +167,7 @@ export class TranscriptContainer
181
167
  // final: the leading finalized blocks plus the first live block's declared
182
168
  // settled rows. TUI commits rows to native scrollback only above it.
183
169
  #nativeScrollbackLiveRegionStart: number | undefined;
170
+ #nativeScrollbackLiveRegionPinned = false;
184
171
  // Persistent assembled transcript rows. Rows before the stable floor are
185
172
  // byte-identical to the previous render; rows at/after it were re-pushed.
186
173
  #lines: string[] = [];
@@ -190,14 +177,6 @@ export class TranscriptContainer
190
177
  // Finalized blocks wholly before this boundary are immutable on-screen history;
191
178
  // their previous contribution can be replayed without calling render().
192
179
  #committedRows = 0;
193
- // Leading children whose rows were handed to native scrollback and dropped
194
- // from the local frame. Children remain owned by the session and can be
195
- // re-rendered when the TUI prepares a destructive full replay.
196
- #compactedChildStart = 0;
197
- // Suppresses re-compaction for the rehydrating render. The TUI feeds its old
198
- // committed-row count immediately before render, so resetting that count
199
- // alone cannot distinguish a replay from an ordinary update.
200
- #replayPending = false;
201
180
  // Stable-prefix floor accumulated across renders since the last
202
181
  // getRenderStablePrefixRows() read (see RenderStablePrefix: reading
203
182
  // consumes the report and re-bases the baseline). Out-of-band renders
@@ -213,14 +192,12 @@ export class TranscriptContainer
213
192
  override clear(): void {
214
193
  this.#generation++;
215
194
  super.clear();
216
- this.#compactedChildStart = 0;
217
195
  this.#committedRows = 0;
218
- this.#replayPending = false;
219
196
  }
220
197
 
221
198
  override setNativeScrollbackCommittedRows(rows: number): void {
222
199
  this.#committedRows = Number.isFinite(rows) ? Math.max(0, Math.trunc(rows)) : 0;
223
- for (let i = this.#compactedChildStart; i < this.children.length; i++) {
200
+ for (let i = 0; i < this.children.length; i++) {
224
201
  const child = this.children[i]!;
225
202
  const segment = this.#segments[i];
226
203
  if (segment === undefined || segment.component !== child) continue;
@@ -243,18 +220,6 @@ export class TranscriptContainer
243
220
  }
244
221
  }
245
222
 
246
- override prepareNativeScrollbackReplay(): void {
247
- // Replay retires the old terminal tape, so descendants may discard layout
248
- // locks whose only purpose was keeping that immutable history byte-stable.
249
- super.prepareNativeScrollbackReplay();
250
- if (this.#compactedChildStart === 0) return;
251
- this.#compactedChildStart = 0;
252
- this.#replayPending = true;
253
- this.#generation++;
254
- this.#lines.length = 0;
255
- this.#stableRowsFloor = 0;
256
- }
257
-
258
223
  getRenderStablePrefixRows(): number {
259
224
  const value = Math.min(this.#stableRowsFloor, this.#lines.length);
260
225
  this.#stableRowsFloor = this.#lines.length;
@@ -265,6 +230,11 @@ export class TranscriptContainer
265
230
  return this.#nativeScrollbackLiveRegionStart;
266
231
  }
267
232
 
233
+ /** Propagates viewport pinning from the first still-mutating transcript block. */
234
+ isNativeScrollbackLiveRegionPinned(): boolean {
235
+ return this.#nativeScrollbackLiveRegionPinned;
236
+ }
237
+
268
238
  /**
269
239
  * Whether none of `component`'s rows (per the most recent render) have
270
240
  * entered native scrollback. Callers that retract ephemeral blocks (IRC
@@ -275,14 +245,8 @@ export class TranscriptContainer
275
245
  * committed rows and is safely removable.
276
246
  */
277
247
  isBlockUncommitted(component: Component): boolean {
278
- const index = this.children.indexOf(component);
279
- // Compacted prefix is already committed native history and must not be
280
- // retracted. Compacted slots may be sparse holes after a later re-render
281
- // (render only fills from #compactedChildStart), so the loop below must
282
- // skip undefined entries.
283
- if (index >= 0 && index < this.#compactedChildStart) return false;
284
248
  for (const segment of this.#segments) {
285
- if (segment === undefined || segment.component !== component) continue;
249
+ if (segment.component !== component) continue;
286
250
  return segment.rowCount === 0 || segment.startRow >= this.#committedRows;
287
251
  }
288
252
  return true;
@@ -336,7 +300,7 @@ export class TranscriptContainer
336
300
  if (maxRows <= 0) return EMPTY_TAIL;
337
301
  const collected: (readonly string[])[] = [];
338
302
  let total = 0;
339
- for (let i = this.children.length - 1; i >= this.#compactedChildStart && total < maxRows; i--) {
303
+ for (let i = this.children.length - 1; i >= 0 && total < maxRows; i--) {
340
304
  const contribution = stripPlainBlankEdges(this.children[i]!.render(width));
341
305
  if (contribution.length === 0) continue;
342
306
  // One blank separator sits between this block and the (already
@@ -358,9 +322,9 @@ export class TranscriptContainer
358
322
  override render(width: number): readonly string[] {
359
323
  width = Math.max(1, width);
360
324
  this.#nativeScrollbackLiveRegionStart = undefined;
325
+ this.#nativeScrollbackLiveRegionPinned = false;
361
326
 
362
327
  const count = this.children.length;
363
- if (this.#compactedChildStart > count) this.#compactedChildStart = count;
364
328
 
365
329
  // Seal displaceable snapshots whose rows are already on the tape (per the
366
330
  // previous frame's segments — the geometry the committed count was
@@ -370,7 +334,7 @@ export class TranscriptContainer
370
334
  // frame, and every frame so a block that BECAME displaceable after its
371
335
  // pending-preview rows committed (late result on a scrolled-off call) is
372
336
  // caught too.
373
- for (let i = this.#compactedChildStart; i < count && i < this.#segments.length; i++) {
337
+ for (let i = 0; i < count && i < this.#segments.length; i++) {
374
338
  const previous = this.#segments[i];
375
339
  if (previous === undefined) continue;
376
340
  if (previous.startRow >= this.#committedRows) break;
@@ -386,10 +350,14 @@ export class TranscriptContainer
386
350
  // reaches.
387
351
  let liveStartIndex = -1;
388
352
  let hasLiveBlock = false;
389
- for (let i = this.#compactedChildStart; i < count; i++) {
353
+ for (let i = 0; i < count; i++) {
390
354
  if (!isBlockFinalized(this.children[i]!)) {
391
355
  liveStartIndex = i;
392
356
  hasLiveBlock = true;
357
+ this.#nativeScrollbackLiveRegionPinned =
358
+ (
359
+ this.children[i] as Component & Partial<NativeScrollbackLiveRegion>
360
+ ).isNativeScrollbackLiveRegionPinned?.() === true;
393
361
  break;
394
362
  }
395
363
  }
@@ -418,7 +386,7 @@ export class TranscriptContainer
418
386
  // Frame row cursor: rows emitted (reused or pushed) so far.
419
387
  let row = 0;
420
388
  let stableRows = 0;
421
- for (let i = this.#compactedChildStart; i < count; i++) {
389
+ for (let i = 0; i < count; i++) {
422
390
  const child = this.children[i]!;
423
391
 
424
392
  // This child's contribution: its current render with plain-blank
@@ -459,7 +427,6 @@ export class TranscriptContainer
459
427
  previous.width === width &&
460
428
  previous.generation === this.#generation);
461
429
  const contribution = reusable ? previous.contribution : stripPlainBlankEdges(raw);
462
- const compactable = finalized && previous?.finalized !== false;
463
430
 
464
431
  // Empty (or stripped-to-nothing) children contribute nothing and never
465
432
  // affect spacing. An empty still-live child still gates the commit
@@ -484,7 +451,6 @@ export class TranscriptContainer
484
451
  rowCount: 0,
485
452
  sep: 0,
486
453
  finalized,
487
- compactable,
488
454
  version,
489
455
  };
490
456
  continue;
@@ -536,7 +502,6 @@ export class TranscriptContainer
536
502
  rowCount,
537
503
  sep,
538
504
  finalized,
539
- compactable,
540
505
  version,
541
506
  };
542
507
  row += rowCount;
@@ -546,72 +511,8 @@ export class TranscriptContainer
546
511
  if (lines.length !== row) lines.length = row;
547
512
  this.#segments = segments;
548
513
  this.#stableRowsFloor = Math.min(stableFloorBefore, stableRows, row);
549
- if (this.#replayPending) {
550
- this.#replayPending = false;
551
- } else {
552
- this.#compactCommittedPrefix();
553
- }
554
514
  return lines;
555
515
  }
556
-
557
- #compactCommittedPrefix(): void {
558
- if (this.#committedRows <= 0 || this.#compactedChildStart >= this.children.length) return;
559
- const lines = this.#lines;
560
- const segments = this.#segments;
561
- let dropRows = 0;
562
- let dropUntil = this.#compactedChildStart;
563
- for (let i = this.#compactedChildStart; i < segments.length; i++) {
564
- const segment = segments[i];
565
- if (segment === undefined || !segment.compactable) break;
566
- const segmentEnd = segment.startRow + segment.rowCount;
567
- if (segmentEnd > this.#committedRows) break;
568
- dropRows = segmentEnd;
569
- dropUntil = i + 1;
570
- }
571
- const retained = segments[dropUntil];
572
- if (retained !== undefined && retained.sep > 0) {
573
- const committedSeparatorRows = this.#committedRows - retained.startRow;
574
- if (committedSeparatorRows <= 0) {
575
- dropRows = 0;
576
- dropUntil = this.#compactedChildStart;
577
- } else {
578
- const trim = Math.min(retained.sep, committedSeparatorRows);
579
- dropRows += trim;
580
- retained.sep -= trim;
581
- retained.rowCount -= trim;
582
- }
583
- }
584
- if (dropRows === 0) return;
585
-
586
- lines.splice(0, dropRows);
587
- for (let i = this.#compactedChildStart; i < dropUntil; i++) {
588
- const segment = segments[i];
589
- if (segment === undefined) continue;
590
- segments[i] = {
591
- component: segment.component,
592
- rawRef: EMPTY_TAIL,
593
- contribution: EMPTY_TAIL,
594
- width: segment.width,
595
- generation: segment.generation,
596
- startRow: 0,
597
- rowCount: 0,
598
- sep: 0,
599
- finalized: true,
600
- compactable: false,
601
- version: segment.version,
602
- };
603
- }
604
- for (let i = dropUntil; i < segments.length; i++) {
605
- const segment = segments[i];
606
- if (segment !== undefined) segment.startRow -= dropRows;
607
- }
608
- this.#compactedChildStart = dropUntil;
609
- this.#committedRows = Math.max(0, this.#committedRows - dropRows);
610
- this.#stableRowsFloor = 0;
611
- if (this.#nativeScrollbackLiveRegionStart !== undefined) {
612
- this.#nativeScrollbackLiveRegionStart = Math.max(0, this.#nativeScrollbackLiveRegionStart - dropRows);
613
- }
614
- }
615
516
  }
616
517
 
617
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
- "Up/Down: move. Left/Right: page. Shift+L: label. Ctrl+O/Shift+Ctrl+O: filter. Alt+D/T/U/L/A: filter. Type to search",
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,
@@ -6,9 +6,25 @@ import { theme } from "../../modes/theme/theme";
6
6
  /** Below this the rate is nonsense (cached/instant responses yield absurd tok/s). */
7
7
  const MIN_DURATION_MS = 100;
8
8
 
9
- export function createUsageRowBlock(usage: Usage, durationMs?: number, ttftMs?: number): Container {
9
+ /** Local `YYYY-MM-DD HH:mm:ss` stamp for the per-turn usage row. */
10
+ function formatUsageTimestamp(ms: number): string {
11
+ const d = new Date(ms);
12
+ const pad = (n: number): string => String(n).padStart(2, "0");
13
+ const date = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
14
+ const time = `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
15
+ return `${date} ${time}`;
16
+ }
17
+
18
+ // `timestamp` is optional and trails the throughput args to preserve the existing
19
+ // (usage, durationMs, ttftMs) call contract — this function is part of the package's
20
+ // public export surface (./modes/components/*).
21
+ export function createUsageRowBlock(usage: Usage, durationMs?: number, ttftMs?: number, timestamp?: number): Container {
10
22
  const totalInput = usage.input + usage.cacheWrite;
11
23
  const parts: string[] = [];
24
+ // Lead with the turn's local wall-clock time (down to the second), log-line style.
25
+ if (timestamp !== undefined && Number.isFinite(timestamp) && timestamp > 0) {
26
+ parts.push(formatUsageTimestamp(timestamp));
27
+ }
12
28
  parts.push(`${theme.icon.input} ${formatNumber(totalInput)}`);
13
29
  parts.push(`${theme.icon.output} ${formatNumber(usage.output)}`);
14
30
  if (usage.cacheRead > 0) {
@@ -13,6 +13,7 @@ import {
13
13
  import { Loader, Markdown, padding, Spacer, Text, visibleWidth } from "@oh-my-pi/pi-tui";
14
14
  import { formatDuration, Snowflake, sanitizeText } from "@oh-my-pi/pi-utils";
15
15
  import { shouldEnableAppendOnlyContext } from "../../config/append-only-context-mode";
16
+ import { type BashResult, isPersistentShellCdCommand } from "../../exec/bash-executor";
16
17
  import { type LoadedCustomShare, loadCustomShare } from "../../export/custom-share";
17
18
  import { shareSession } from "../../export/share";
18
19
  import type { CompactOptions } from "../../extensibility/extensions/types";
@@ -1100,6 +1101,12 @@ export class CommandController {
1100
1101
 
1101
1102
  async handleBashCommand(command: string, excludeFromContext = false): Promise<void> {
1102
1103
  const isDeferred = this.ctx.session.isStreaming;
1104
+ const shouldPersistCwd = isPersistentShellCdCommand(command);
1105
+ if (isDeferred && shouldPersistCwd) {
1106
+ this.ctx.showWarning("Wait for the current response to finish or abort it before changing directories.");
1107
+ return;
1108
+ }
1109
+
1103
1110
  this.ctx.bashComponent = new BashExecutionComponent(command, this.ctx.ui, excludeFromContext);
1104
1111
 
1105
1112
  if (isDeferred) {
@@ -1120,7 +1127,6 @@ export class CommandController {
1120
1127
  },
1121
1128
  { excludeFromContext, useUserShell: true },
1122
1129
  );
1123
-
1124
1130
  if (this.ctx.bashComponent) {
1125
1131
  const meta = outputMeta().truncationFromSummary(result, { direction: "tail" }).get();
1126
1132
  this.ctx.bashComponent.setComplete(result.exitCode, result.cancelled, {
@@ -1128,6 +1134,15 @@ export class CommandController {
1128
1134
  truncation: meta?.truncation,
1129
1135
  });
1130
1136
  }
1137
+ try {
1138
+ if (shouldPersistCwd) await this.#applyBashResultCwd(result);
1139
+ } catch (error) {
1140
+ this.ctx.showError(
1141
+ `Bash command completed, but OMP failed to update its working directory: ${
1142
+ error instanceof Error ? error.message : "Unknown error"
1143
+ }`,
1144
+ );
1145
+ }
1131
1146
  } catch (error) {
1132
1147
  if (this.ctx.bashComponent) {
1133
1148
  this.ctx.bashComponent.setComplete(undefined, false);
@@ -1139,6 +1154,31 @@ export class CommandController {
1139
1154
  this.ctx.ui.requestRender();
1140
1155
  }
1141
1156
 
1157
+ async #moveInteractiveCwd(resolvedPath: string): Promise<void> {
1158
+ await this.ctx.sessionManager.moveTo(resolvedPath);
1159
+ await this.ctx.applyCwdChange(resolvedPath);
1160
+ this.ctx.updateEditorBorderColor();
1161
+ await this.ctx.reloadTodos();
1162
+ }
1163
+
1164
+ async #applyBashResultCwd(result: BashResult): Promise<void> {
1165
+ if (result.cancelled || result.exitCode !== 0 || !result.workingDir) return;
1166
+ if (!path.isAbsolute(result.workingDir)) return;
1167
+
1168
+ const resolvedPath = path.resolve(result.workingDir);
1169
+ if (resolvedPath === path.resolve(this.ctx.sessionManager.getCwd())) return;
1170
+
1171
+ let isDirectory = false;
1172
+ try {
1173
+ isDirectory = (await fs.stat(resolvedPath)).isDirectory();
1174
+ } catch {
1175
+ isDirectory = false;
1176
+ }
1177
+ if (!isDirectory) return;
1178
+
1179
+ await this.#moveInteractiveCwd(resolvedPath);
1180
+ }
1181
+
1142
1182
  async handlePythonCommand(code: string, excludeFromContext = false): Promise<void> {
1143
1183
  const isDeferred = this.ctx.session.isStreaming;
1144
1184
  this.ctx.pythonComponent = new EvalExecutionComponent(code, this.ctx.ui, excludeFromContext);
@@ -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 true;
260
+ return settings.get("terminal.showImages");
262
261
  }
263
262
 
264
263
  #insertAfterTranscriptComponent(anchor: Component | undefined, component: Component): boolean {
@@ -913,7 +912,12 @@ export class EventController {
913
912
  this.#lastAssistantComponent = lastPostToolAssistantComponent ?? this.ctx.streamingComponent;
914
913
  if (settings.get("display.showTokenUsage") && assistantUsageIsBilled(event.message.usage)) {
915
914
  this.ctx.chatContainer.addChild(
916
- createUsageRowBlock(event.message.usage, event.message.duration, event.message.ttft),
915
+ createUsageRowBlock(
916
+ event.message.usage,
917
+ event.message.duration,
918
+ event.message.ttft,
919
+ event.message.timestamp,
920
+ ),
917
921
  );
918
922
  }
919
923
  this.ctx.streamingComponent = undefined;
@@ -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.loopPrompt = text;
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(value as boolean);
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
- let wantsSummary = false;
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
- this.ctx.session.modelRegistry.refreshInBackground();
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
- await this.ctx.session.modelRegistry.refresh();
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(