@oh-my-pi/pi-coding-agent 17.1.8 → 17.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/dist/{CHANGELOG-vt8ene9g.md → CHANGELOG-be1f2t8h.md} +92 -0
  3. package/dist/cli.js +9986 -9776
  4. package/dist/{template-dys3vk5b.js → template-8vdv6xb4.js} +1 -1
  5. package/dist/types/advisor/runtime.d.ts +8 -2
  6. package/dist/types/advisor/transcript-recorder.d.ts +15 -0
  7. package/dist/types/auto-thinking/classifier.d.ts +9 -2
  8. package/dist/types/cli/args.d.ts +2 -0
  9. package/dist/types/cli/session-picker.d.ts +14 -7
  10. package/dist/types/collab/guest.d.ts +21 -2
  11. package/dist/types/commands/launch.d.ts +6 -0
  12. package/dist/types/config/settings-schema.d.ts +63 -8
  13. package/dist/types/cursor-bridge-tools.d.ts +54 -0
  14. package/dist/types/cursor.d.ts +193 -8
  15. package/dist/types/edit/edit-clipboard.d.ts +19 -0
  16. package/dist/types/edit/hashline/diff.d.ts +9 -1
  17. package/dist/types/edit/index.d.ts +8 -1
  18. package/dist/types/edit/streaming.d.ts +8 -1
  19. package/dist/types/extensibility/extensions/runner.d.ts +19 -2
  20. package/dist/types/extensibility/extensions/types.d.ts +25 -1
  21. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +2 -0
  22. package/dist/types/internal-urls/local-protocol.d.ts +3 -2
  23. package/dist/types/launch/terminal-output-worker-client.d.ts +5 -0
  24. package/dist/types/launch/terminal-output-worker-protocol.d.ts +14 -0
  25. package/dist/types/launch/terminal-output-worker.d.ts +1 -0
  26. package/dist/types/lsp/client.d.ts +2 -0
  27. package/dist/types/main.d.ts +2 -0
  28. package/dist/types/mcp/manager.d.ts +33 -4
  29. package/dist/types/modes/components/codex-reset-fireworks.d.ts +51 -0
  30. package/dist/types/modes/components/read-tool-group.d.ts +12 -0
  31. package/dist/types/modes/components/session-selector.d.ts +6 -0
  32. package/dist/types/modes/components/status-line/component.d.ts +23 -0
  33. package/dist/types/modes/components/tool-execution.d.ts +16 -7
  34. package/dist/types/modes/components/usage-row.d.ts +2 -0
  35. package/dist/types/modes/controllers/mcp-command-controller.d.ts +26 -1
  36. package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
  37. package/dist/types/modes/interactive-mode.d.ts +4 -2
  38. package/dist/types/modes/rpc/rpc-client.d.ts +7 -0
  39. package/dist/types/modes/rpc/rpc-types.d.ts +16 -0
  40. package/dist/types/modes/theme/theme.d.ts +12 -1
  41. package/dist/types/modes/types.d.ts +2 -1
  42. package/dist/types/sdk.d.ts +1 -0
  43. package/dist/types/session/agent-session-events.d.ts +2 -0
  44. package/dist/types/session/agent-session-types.d.ts +36 -1
  45. package/dist/types/session/agent-session.d.ts +4 -2
  46. package/dist/types/session/claude-session-store.d.ts +13 -0
  47. package/dist/types/session/codex-session-store.d.ts +14 -0
  48. package/dist/types/session/credential-pin.d.ts +58 -0
  49. package/dist/types/session/foreign-session-import.d.ts +15 -0
  50. package/dist/types/session/foreign-session-jsonl.d.ts +9 -0
  51. package/dist/types/session/foreign-session-store.d.ts +23 -0
  52. package/dist/types/session/session-advisors.d.ts +47 -2
  53. package/dist/types/session/session-entries.d.ts +19 -1
  54. package/dist/types/session/session-handoff.d.ts +4 -1
  55. package/dist/types/session/session-loader.d.ts +2 -0
  56. package/dist/types/session/session-maintenance.d.ts +1 -1
  57. package/dist/types/session/session-manager.d.ts +21 -0
  58. package/dist/types/session/session-tools.d.ts +25 -1
  59. package/dist/types/session/turn-recovery.d.ts +10 -4
  60. package/dist/types/system-prompt.d.ts +2 -1
  61. package/dist/types/task/executor.d.ts +11 -2
  62. package/dist/types/task/render.d.ts +7 -0
  63. package/dist/types/thinking.d.ts +18 -6
  64. package/dist/types/tools/acp-bridge.d.ts +28 -5
  65. package/dist/types/tools/grep.d.ts +17 -1
  66. package/dist/types/tools/hub/launch.d.ts +5 -1
  67. package/dist/types/tools/index.d.ts +4 -1
  68. package/dist/types/tools/path-utils.d.ts +19 -0
  69. package/dist/types/tools/read.d.ts +2 -0
  70. package/dist/types/tools/xdev.d.ts +2 -1
  71. package/dist/types/utils/changelog.d.ts +19 -0
  72. package/dist/types/utils/git.d.ts +10 -0
  73. package/dist/types/utils/jj.d.ts +11 -4
  74. package/dist/types/web/search/types.d.ts +2 -2
  75. package/package.json +12 -12
  76. package/src/advisor/runtime.ts +138 -12
  77. package/src/advisor/transcript-recorder.ts +56 -0
  78. package/src/auto-thinking/classifier.ts +48 -10
  79. package/src/cli/args.ts +6 -0
  80. package/src/cli/flag-tables.ts +2 -0
  81. package/src/cli/gallery-fixtures/fs.ts +25 -0
  82. package/src/cli/session-picker.ts +34 -17
  83. package/src/cli/ttsr-cli.ts +19 -1
  84. package/src/cli.ts +9 -0
  85. package/src/collab/guest.ts +37 -4
  86. package/src/collab/host.ts +1 -0
  87. package/src/commands/launch.ts +6 -0
  88. package/src/config/model-registry.ts +103 -25
  89. package/src/config/settings-schema.ts +65 -6
  90. package/src/config/settings.ts +25 -0
  91. package/src/cursor-bridge-tools.ts +81 -0
  92. package/src/cursor.ts +463 -12
  93. package/src/edit/edit-clipboard.ts +23 -0
  94. package/src/edit/hashline/diff.ts +49 -10
  95. package/src/edit/hashline/execute.ts +38 -6
  96. package/src/edit/hashline/filesystem.ts +27 -3
  97. package/src/edit/index.ts +12 -2
  98. package/src/edit/renderer.ts +4 -4
  99. package/src/edit/streaming.ts +17 -2
  100. package/src/export/html/template.js +1 -1
  101. package/src/extensibility/extensions/runner.ts +70 -0
  102. package/src/extensibility/extensions/types.ts +30 -0
  103. package/src/extensibility/legacy-pi-coding-agent-shim.ts +12 -13
  104. package/src/internal-urls/local-protocol.ts +3 -2
  105. package/src/internal-urls/mcp-protocol.ts +4 -1
  106. package/src/launch/terminal-output-worker-client.ts +53 -0
  107. package/src/launch/terminal-output-worker-protocol.ts +11 -0
  108. package/src/launch/terminal-output-worker.ts +23 -0
  109. package/src/lsp/client.ts +18 -3
  110. package/src/lsp/index.ts +4 -0
  111. package/src/lsp/render.ts +1 -1
  112. package/src/main.ts +127 -56
  113. package/src/mcp/manager.ts +120 -18
  114. package/src/modes/acp/acp-agent.ts +15 -9
  115. package/src/modes/components/chat-transcript-builder.ts +30 -11
  116. package/src/modes/components/codex-reset-fireworks.ts +369 -0
  117. package/src/modes/components/read-tool-group.ts +159 -17
  118. package/src/modes/components/session-selector.ts +19 -3
  119. package/src/modes/components/status-line/component.ts +381 -54
  120. package/src/modes/components/tool-execution.ts +78 -31
  121. package/src/modes/components/usage-row.ts +10 -5
  122. package/src/modes/controllers/event-controller.ts +26 -4
  123. package/src/modes/controllers/mcp-command-controller.ts +68 -3
  124. package/src/modes/controllers/selector-controller.ts +94 -36
  125. package/src/modes/controllers/tan-command-controller.ts +14 -0
  126. package/src/modes/interactive-mode.ts +31 -17
  127. package/src/modes/rpc/rpc-client.ts +18 -0
  128. package/src/modes/rpc/rpc-mode.ts +16 -1
  129. package/src/modes/rpc/rpc-types.ts +11 -0
  130. package/src/modes/theme/theme.ts +151 -6
  131. package/src/modes/types.ts +2 -1
  132. package/src/modes/utils/ui-helpers.ts +38 -21
  133. package/src/prompts/system/auto-thinking-difficulty.md +4 -2
  134. package/src/prompts/system/plan-mode-active.md +2 -2
  135. package/src/prompts/system/system-prompt.md +3 -0
  136. package/src/prompts/system/xdev-mount-notice.md +1 -1
  137. package/src/prompts/tools/bash.md +15 -17
  138. package/src/prompts/tools/checkpoint.md +1 -1
  139. package/src/prompts/tools/glob.md +7 -6
  140. package/src/prompts/tools/grep.md +7 -6
  141. package/src/sdk.ts +178 -15
  142. package/src/session/agent-session-events.ts +1 -0
  143. package/src/session/agent-session-types.ts +40 -1
  144. package/src/session/agent-session.ts +302 -175
  145. package/src/session/claude-session-store.ts +426 -0
  146. package/src/session/codex-session-store.ts +673 -0
  147. package/src/session/credential-pin.ts +93 -0
  148. package/src/session/foreign-session-import.ts +52 -0
  149. package/src/session/foreign-session-jsonl.ts +29 -0
  150. package/src/session/foreign-session-store.ts +26 -0
  151. package/src/session/model-controls.ts +11 -3
  152. package/src/session/session-advisors.ts +140 -16
  153. package/src/session/session-entries.ts +21 -1
  154. package/src/session/session-handoff.ts +20 -3
  155. package/src/session/session-loader.ts +25 -9
  156. package/src/session/session-maintenance.ts +59 -11
  157. package/src/session/session-manager.ts +61 -0
  158. package/src/session/session-tools.ts +107 -5
  159. package/src/session/settings-stream-fn.ts +1 -0
  160. package/src/session/turn-recovery.ts +97 -51
  161. package/src/slash-commands/builtin-registry.ts +131 -3
  162. package/src/system-prompt.ts +3 -2
  163. package/src/task/executor.ts +17 -4
  164. package/src/task/render.ts +20 -4
  165. package/src/thinking.ts +29 -10
  166. package/src/tools/acp-bridge.ts +52 -8
  167. package/src/tools/browser.ts +5 -0
  168. package/src/tools/checkpoint.ts +0 -13
  169. package/src/tools/grep.ts +64 -8
  170. package/src/tools/hub/launch.ts +10 -15
  171. package/src/tools/index.ts +28 -4
  172. package/src/tools/path-utils.ts +88 -0
  173. package/src/tools/read.ts +3 -0
  174. package/src/tools/write.ts +9 -3
  175. package/src/tools/xdev.ts +39 -14
  176. package/src/utils/changelog.ts +144 -4
  177. package/src/utils/git.ts +61 -14
  178. package/src/utils/jj.ts +19 -13
  179. package/src/web/search/providers/codex.ts +34 -0
  180. package/src/web/search/types.ts +1 -1
@@ -1,4 +1,4 @@
1
- import type { SnapshotStore } from "@oh-my-pi/hashline";
1
+ import type { Clipboard, SnapshotStore } from "@oh-my-pi/hashline";
2
2
  import type { AgentTool } from "@oh-my-pi/pi-agent-core";
3
3
  import {
4
4
  Box,
@@ -203,6 +203,13 @@ class SafeToolRendererComponent implements Component {
203
203
  */
204
204
  export interface TranscriptLiveRegionProbe {
205
205
  isBlockInLiveRegion(component: Component): boolean;
206
+ /**
207
+ * Whether none of the block's rows have entered native scrollback (see
208
+ * `TranscriptContainer.isBlockUncommitted`). Optional: standalone hosts
209
+ * without commit tracking omit it, and blocks treat their rows as
210
+ * uncommitted.
211
+ */
212
+ isBlockUncommitted?(component: Component): boolean;
206
213
  }
207
214
 
208
215
  /** Minimal TUI surface ToolExecutionComponent uses to schedule repaints and share image budget. */
@@ -215,6 +222,8 @@ export interface ToolExecutionUi {
215
222
 
216
223
  export interface ToolExecutionOptions {
217
224
  snapshots?: SnapshotStore;
225
+ /** Session-persistent edit clipboard register, forked per preview frame. */
226
+ clipboard?: Clipboard;
218
227
  showImages?: boolean; // default: true (only used if terminal supports images)
219
228
  editFuzzyThreshold?: number;
220
229
  editAllowFuzzy?: boolean;
@@ -278,6 +287,7 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
278
287
  #editFuzzyThreshold: number | undefined;
279
288
  #editAllowFuzzy: boolean | undefined;
280
289
  #snapshots?: SnapshotStore;
290
+ #clipboard?: Clipboard;
281
291
  #isPartial = true;
282
292
  #resultVersion = 0;
283
293
  #lastDisplayKey: string | undefined;
@@ -337,10 +347,21 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
337
347
  // transcript, e.g. in tests): whether this block is still repaintable.
338
348
  #liveRegion?: TranscriptLiveRegionProbe;
339
349
  // One-way latch for a detached (`async.state === "running"`) task block
340
- // that left the transcript live region: its rows are commit-eligible
341
- // history, so progress renders static gray and further partial snapshots are
342
- // dropped (see #maybeFreezeBackgroundTask).
350
+ // whose rows became native-scrollback history it left the transcript
351
+ // live region, or its head rows were committed while it was still the
352
+ // live tail. Further partial snapshots are dropped so committed rows are
353
+ // never mutated (see #maybeFreezeBackgroundTask).
343
354
  #backgroundTaskFrozen = false;
355
+ // Whether the freeze may restyle the progress rows static gray. Set only
356
+ // when the latch fired while no row was committed: a recolor of rows
357
+ // already on the tape would itself diverge immutable history and force an
358
+ // erase-replay (or, with scrollback rebuild off, a duplicate slab).
359
+ #backgroundTaskFrozenStyled = false;
360
+ // Wall clock captured at each repaintable rebuild of a task card and
361
+ // reused verbatim once the card freezes or any of its rows commit, so
362
+ // time-derived rows (current-tool elapsed, retry countdown) cannot drift
363
+ // a committed byte on later rebuilds (theme epoch, image toggles).
364
+ #taskRenderNowMs = Date.now();
344
365
  // Set on each `render()` when the last painted pending shape must be
345
366
  // replayed wholesale when the first result arrives. Reset gates key off
346
367
  // these so a topology-changing update that lands before the shape reaches
@@ -375,6 +396,7 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
375
396
  this.#editFuzzyThreshold = options.editFuzzyThreshold;
376
397
  this.#editAllowFuzzy = options.editAllowFuzzy;
377
398
  this.#snapshots = options.snapshots;
399
+ this.#clipboard = options.clipboard;
378
400
  this.#liveRegion = options.liveRegion;
379
401
  this.#tool = tool;
380
402
  this.#ui = ui;
@@ -532,6 +554,7 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
532
554
  cwd: this.#cwd,
533
555
  signal: controller.signal,
534
556
  snapshots: this.#snapshots!,
557
+ clipboard: this.#clipboard,
535
558
  fuzzyThreshold: this.#editFuzzyThreshold,
536
559
  allowFuzzy: this.#editAllowFuzzy,
537
560
  isStreaming,
@@ -559,13 +582,17 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
559
582
  _toolCallId?: string,
560
583
  ): void {
561
584
  // A detached task spawn keeps streaming progress snapshots after the
562
- // block froze (left the transcript live region). Drop them: the rows are
563
- // static gray history now, and repainting would rewrite rows the engine
564
- // may already have committed to native scrollback. The terminal snapshot
565
- // (async completed/failed → isPartial=false) still applies so a block
566
- // that is still on screen settles on real results.
567
- if (isPartial && this.#toolName === "task" && this.#maybeFreezeBackgroundTask()) {
568
- return;
585
+ // block froze (left the transcript live region, or its rows entered
586
+ // native scrollback). Drop them: repainting would rewrite rows the
587
+ // engine may already have committed. The terminal snapshot (async
588
+ // completed/failed → isPartial=false) still settles a card that is
589
+ // wholly uncommitted (still on screen); once any row is on the tape
590
+ // the card is immutable history replacing it would re-commit the
591
+ // whole slab below the stale copy — so the settlement is dropped and
592
+ // the job's result surfaces through its own delivery message.
593
+ if (this.#toolName === "task" && this.#maybeFreezeBackgroundTask()) {
594
+ if (isPartial) return;
595
+ if (!(this.#liveRegion?.isBlockUncommitted?.(this) ?? true)) return;
569
596
  }
570
597
  const hadNoResult = this.#result === undefined;
571
598
  const wasPartialResult = this.#result !== undefined && this.#isPartial;
@@ -713,22 +740,30 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
713
740
  }
714
741
 
715
742
  /**
716
- * Freeze a detached (`async.state === "running"`) task block once it leaves
717
- * the transcript's live region. Past that seam its rows are commit-eligible
718
- * native-scrollback history: repaint the progress rows static gray and drop
719
- * further partial snapshots. One-way blocks never re-enter the live
720
- * region. Returns whether the block is frozen.
743
+ * Freeze a detached (`async.state === "running"`) task block once its rows
744
+ * become native-scrollback history: the block left the transcript's live
745
+ * region (a later block streams below it), or while it is still the
746
+ * live tail its head rows were committed because the frame outgrew the
747
+ * viewport. Committed rows are immutable, so from that point every further
748
+ * partial snapshot is dropped. Rows restyle static gray only when nothing
749
+ * is committed yet; otherwise the bytes stay exactly as painted. One-way —
750
+ * blocks never re-enter the live region. Returns whether the block is
751
+ * frozen.
721
752
  */
722
753
  #maybeFreezeBackgroundTask(): boolean {
723
754
  if (this.#backgroundTaskFrozen) return true;
724
755
  if (this.#toolName !== "task" || this.#liveRegion === undefined) return false;
725
756
  const asyncState = (this.#result?.details as { async?: { state?: string } } | undefined)?.async?.state;
726
757
  if (asyncState !== "running") return false;
727
- if (this.#liveRegion.isBlockInLiveRegion(this)) return false;
758
+ const uncommitted = this.#liveRegion.isBlockUncommitted?.(this) ?? true;
759
+ if (uncommitted && this.#liveRegion.isBlockInLiveRegion(this)) return false;
728
760
  this.#backgroundTaskFrozen = true;
729
761
  this.#updateSpinnerAnimation();
730
- this.#updateDisplay();
731
- this.#ui.requestRender();
762
+ if (uncommitted) {
763
+ this.#backgroundTaskFrozenStyled = true;
764
+ this.#updateDisplay();
765
+ this.#ui.requestRender();
766
+ }
732
767
  return true;
733
768
  }
734
769
 
@@ -784,16 +819,16 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
784
819
 
785
820
  /**
786
821
  * Keeps in-flight TV-wall frames out of immutable native scrollback: the
787
- * `vibe_wait` wall and displaceable snapshots (`hub` waiting polls, `todo`
788
- * lists). Their frames replace each other rather than append, and their
789
- * rows mutate every spinner tick an unpinned commit records a per-tick
790
- * frozen snapshot AND force-seals the block (see TranscriptContainer's
791
- * committed-snapshot seal), so the next poll stacks a new frame instead of
792
- * displacing this one.
822
+ * `vibe_wait` wall, displaceable snapshots (`hub` waiting polls, `todo`
823
+ * lists), and live `task` calls. Their frames replace each other rather
824
+ * than append task progress rows rewrite in place on every snapshot —
825
+ * so an unpinned commit records a per-tick frozen snapshot (and for
826
+ * displaceable blocks force-seals them, stacking the next poll below).
827
+ * The finalized frame commits exactly once when the pin lifts.
793
828
  */
794
829
  isNativeScrollbackLiveRegionPinned(): boolean {
795
830
  if (this.isTranscriptBlockFinalized()) return false;
796
- return this.#toolName === "vibe_wait" || this.#displaceableByToolName !== undefined;
831
+ return this.#toolName === "vibe_wait" || this.#toolName === "task" || this.#displaceableByToolName !== undefined;
797
832
  }
798
833
 
799
834
  /**
@@ -827,8 +862,12 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
827
862
  this.#sealed = true;
828
863
  this.#displaceableByToolName = undefined;
829
864
  // A sealed detached task is abandoned history: settle its progress rows
830
- // on static gray.
865
+ // on static gray — but only while none of them are committed; a recolor
866
+ // on the tape would diverge immutable history.
831
867
  this.#backgroundTaskFrozen = true;
868
+ if (this.#liveRegion?.isBlockUncommitted?.(this) ?? true) {
869
+ this.#backgroundTaskFrozenStyled = true;
870
+ }
832
871
  this.stopAnimation();
833
872
  this.#updateDisplay();
834
873
  this.#ui.requestRender();
@@ -888,7 +927,7 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
888
927
  // TUI startup, so a result rendered before it lands must re-shape once it
889
928
  // does (it gates Image children vs text fallback in #rebuildDisplay); keyed
890
929
  // here for the same reason markdown.ts keys its render cache on it.
891
- const key = `${this.#resultVersion}|${this.#expanded}|${this.#isPartial}|${this.#spinnerFrame ?? "-"}|${this.#showImages}|${getThemeEpoch()}|${this.#displayInputVersion}|${this.#backgroundTaskFrozen}|${TERMINAL.imageProtocol ?? "-"}|${this.#imageSizeKey()}`;
930
+ const key = `${this.#resultVersion}|${this.#expanded}|${this.#isPartial}|${this.#spinnerFrame ?? "-"}|${this.#showImages}|${getThemeEpoch()}|${this.#displayInputVersion}|${this.#backgroundTaskFrozenStyled}|${TERMINAL.imageProtocol ?? "-"}|${this.#imageSizeKey()}`;
892
931
  if (key === this.#lastDisplayKey && this.#displayBuilt) return;
893
932
  this.#lastDisplayKey = key;
894
933
 
@@ -1292,9 +1331,17 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
1292
1331
  // draws every dispatched agent as a progress/result line, so tell
1293
1332
  // `renderCall` to drop its duplicate streaming preview list.
1294
1333
  context.hasResult = Boolean(this.#result);
1295
- // Out of the transcript live region: progress rows render static gray
1296
- // (see task/render.ts).
1297
- context.frozen = this.#backgroundTaskFrozen;
1334
+ // Settled as history (out of the live region, before any row entered
1335
+ // the tape): progress rows render static gray (see task/render.ts).
1336
+ context.frozen = this.#backgroundTaskFrozenStyled;
1337
+ // Freeze the render clock alongside the latch — and independently the
1338
+ // moment any row commits, closing the window between a commit paint
1339
+ // and the next snapshot where a settings-triggered rebuild could
1340
+ // re-derive elapsed/countdown bytes under committed rows.
1341
+ if (!this.#backgroundTaskFrozen && (this.#liveRegion?.isBlockUncommitted?.(this) ?? true)) {
1342
+ this.#taskRenderNowMs = Date.now();
1343
+ }
1344
+ context.nowMs = this.#taskRenderNowMs;
1298
1345
  } else if (isEditLikeToolName(this.#toolName)) {
1299
1346
  context.editMode = this.#editMode;
1300
1347
  const previews = this.#editDiffPreview;
@@ -15,10 +15,8 @@ function formatUsageTimestamp(ms: number): string {
15
15
  return `${date} ${time}`;
16
16
  }
17
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 {
18
+ /** Format the metrics shared by standalone usage blocks and compact tool groups. */
19
+ export function formatUsageRow(usage: Usage, durationMs?: number, ttftMs?: number, timestamp?: number): string {
22
20
  const totalInput = usage.input + usage.cacheWrite;
23
21
  const parts: string[] = [];
24
22
  // Lead with the turn's local wall-clock time (down to the second), log-line style.
@@ -40,8 +38,15 @@ export function createUsageRowBlock(usage: Usage, durationMs?: number, ttftMs?:
40
38
  const tokPerSec = (usage.output / durationMs) * 1000;
41
39
  parts.push(`${theme.icon.throughput} ${tokPerSec.toFixed(1)}/s`);
42
40
  }
41
+ return parts.join(" ");
42
+ }
43
+
44
+ // `timestamp` is optional and trails the throughput args to preserve the existing
45
+ // (usage, durationMs, ttftMs) call contract — this function is part of the package's
46
+ // public export surface (./modes/components/*).
47
+ export function createUsageRowBlock(usage: Usage, durationMs?: number, ttftMs?: number, timestamp?: number): Container {
43
48
  const block = new Container();
44
49
  block.addChild(new Spacer(1));
45
- block.addChild(new Text(theme.fg("dim", parts.join(" ")), 1, 0));
50
+ block.addChild(new Text(theme.fg("dim", formatUsageRow(usage, durationMs, ttftMs, timestamp)), 1, 0));
46
51
  return block;
47
52
  }
@@ -6,10 +6,12 @@ import { logger, prompt, sanitizeText } from "@oh-my-pi/pi-utils";
6
6
  import { INTENT_FIELD } from "@oh-my-pi/pi-wire";
7
7
  import { extractTextContent } from "../../commit/utils";
8
8
  import { settings } from "../../config/settings";
9
+ import { getEditClipboard } from "../../edit/edit-clipboard";
9
10
  import { getFileSnapshotStore } from "../../edit/file-snapshot-store";
10
11
  import { AssistantMessageComponent } from "../../modes/components/assistant-message";
11
12
  import { detectCacheInvalidation } from "../../modes/components/cache-invalidation-marker";
12
13
  import {
14
+ groupedReadUsageCallIds,
13
15
  ReadToolGroupComponent,
14
16
  readArgsCollapseIntoGroup,
15
17
  readArgsHaveTarget,
@@ -193,6 +195,10 @@ export class EventController {
193
195
  todo_auto_clear: e => this.#handleTodoAutoClear(e),
194
196
  irc_message: e => this.#handleIrcMessage(e),
195
197
  notice: e => this.#handleNotice(e),
198
+ model_changed: async () => {
199
+ this.ctx.statusLine.invalidate();
200
+ this.ctx.ui.requestRender();
201
+ },
196
202
  thinking_level_changed: async () => {
197
203
  this.ctx.statusLine.invalidate();
198
204
  this.ctx.updateEditorBorderColor();
@@ -805,6 +811,7 @@ export class EventController {
805
811
  renderArgs,
806
812
  {
807
813
  snapshots: getFileSnapshotStore(this.ctx.viewSession),
814
+ clipboard: getEditClipboard(this.ctx.viewSession),
808
815
  showImages: settings.get("terminal.showImages"),
809
816
  editFuzzyThreshold: settings.get("edit.fuzzyThreshold"),
810
817
  editAllowFuzzy: settings.get("edit.fuzzyMatch"),
@@ -954,14 +961,28 @@ export class EventController {
954
961
  }
955
962
  this.#lastAssistantComponent = lastPostToolAssistantComponent ?? this.ctx.streamingComponent;
956
963
  if (settings.get("display.showTokenUsage") && assistantUsageIsBilled(event.message.usage)) {
957
- this.ctx.chatContainer.addChild(
958
- createUsageRowBlock(
964
+ const readCallIds = groupedReadUsageCallIds(event.message);
965
+ const usageAttached =
966
+ readCallIds !== undefined &&
967
+ (this.#lastReadGroup?.attachUsage(
968
+ readCallIds,
959
969
  event.message.usage,
960
970
  event.message.duration,
961
971
  event.message.ttft,
962
972
  event.message.timestamp,
963
- ),
964
- );
973
+ ) ??
974
+ false);
975
+ if (!usageAttached) {
976
+ this.#resetReadGroup();
977
+ this.ctx.chatContainer.addChild(
978
+ createUsageRowBlock(
979
+ event.message.usage,
980
+ event.message.duration,
981
+ event.message.ttft,
982
+ event.message.timestamp,
983
+ ),
984
+ );
985
+ }
965
986
  }
966
987
  if (displayMessage === event.message) {
967
988
  this.ctx.transcriptMessageComponents.set(event.message, this.ctx.streamingComponent);
@@ -1014,6 +1035,7 @@ export class EventController {
1014
1035
  event.args,
1015
1036
  {
1016
1037
  snapshots: getFileSnapshotStore(this.ctx.viewSession),
1038
+ clipboard: getEditClipboard(this.ctx.viewSession),
1017
1039
  showImages: settings.get("terminal.showImages"),
1018
1040
  editFuzzyThreshold: settings.get("edit.fuzzyThreshold"),
1019
1041
  editAllowFuzzy: settings.get("edit.fuzzyMatch"),
@@ -48,7 +48,13 @@ import {
48
48
  searchSmitheryRegistry,
49
49
  toConfigName,
50
50
  } from "../../mcp/smithery-registry";
51
- import type { MCPAuthChallenge, MCPAuthConfig, MCPServerConfig, MCPServerConnection } from "../../mcp/types";
51
+ import type {
52
+ MCPAuthChallenge,
53
+ MCPAuthConfig,
54
+ MCPConfigFile,
55
+ MCPServerConfig,
56
+ MCPServerConnection,
57
+ } from "../../mcp/types";
52
58
  import { shortenPath } from "../../tools/render-utils";
53
59
  import { urlHyperlinkAlways } from "../../tui";
54
60
  import { copyToClipboard } from "../../utils/clipboard";
@@ -246,6 +252,64 @@ type MCPSearchParsed = {
246
252
  error?: string;
247
253
  };
248
254
 
255
+ /**
256
+ * Collect the de-duplicated union of every MCP server name we know about:
257
+ * user config, project config, and any runtime-discovered servers not
258
+ * already present in either config (`ctx.mcpManager.getAllServerNames()`
259
+ * covers connections, pending connections, and discovered-but-not-yet-
260
+ * connected sources).
261
+ *
262
+ * `includeDisabledOnly` controls names found only in
263
+ * `userConfig.disabledServers`, while `includeDisabledConfigured` controls
264
+ * config entries whose `enabled` flag is false. Both default to true because
265
+ * callers such as `/mcp list` need the complete union. Autocomplete callers
266
+ * must disable the categories their target operation cannot accept.
267
+ *
268
+ * This is the single source of truth for "every known server name": both
269
+ * `MCPCommandController#handleList()` and the `/mcp` slash-command argument
270
+ * completer (server-name autocomplete for `enable`/`disable`/`test`/etc.)
271
+ * call this instead of re-deriving the union themselves.
272
+ *
273
+ * `preloaded` lets a caller that already read both config files (e.g.
274
+ * `#handleList()`) pass them in and skip the redundant re-read.
275
+ */
276
+ export async function collectMcpServerNames(
277
+ ctx: InteractiveModeContext,
278
+ preloaded?: { userConfig: MCPConfigFile; projectConfig: MCPConfigFile },
279
+ includeDisabledOnly = true,
280
+ includeDisabledConfigured = true,
281
+ ): Promise<string[]> {
282
+ let userConfig: MCPConfigFile;
283
+ let projectConfig: MCPConfigFile;
284
+ if (preloaded) {
285
+ ({ userConfig, projectConfig } = preloaded);
286
+ } else {
287
+ const cwd = getProjectDir();
288
+ [userConfig, projectConfig] = await Promise.all([
289
+ readMCPConfigFile(getMCPConfigPath("user", cwd)),
290
+ readMCPConfigFile(getMCPConfigPath("project", cwd)),
291
+ ]);
292
+ }
293
+
294
+ const names = new Set<string>(includeDisabledOnly ? (userConfig.disabledServers ?? []) : []);
295
+ const addConfiguredNames = (config: MCPConfigFile): void => {
296
+ const servers = config.mcpServers;
297
+ if (!servers) return;
298
+ for (const name in servers) {
299
+ const server = servers[name];
300
+ if (server && (includeDisabledConfigured || server.enabled !== false)) names.add(name);
301
+ }
302
+ };
303
+ addConfiguredNames(userConfig);
304
+ addConfiguredNames(projectConfig);
305
+ if (ctx.mcpManager) {
306
+ for (const name of ctx.mcpManager.getAllServerNames()) {
307
+ names.add(name);
308
+ }
309
+ }
310
+ return [...names].sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" }));
311
+ }
312
+
249
313
  export class MCPCommandController {
250
314
  constructor(private ctx: InteractiveModeContext) {}
251
315
 
@@ -1271,10 +1335,11 @@ export class MCPCommandController {
1271
1335
 
1272
1336
  // Collect runtime-discovered servers not in config files
1273
1337
  const configServerNames = new Set([...userServers, ...projectServers]);
1274
- const disabledServerNames = new Set(await readDisabledServers(userPath));
1338
+ const disabledServerNames = new Set(userConfig.disabledServers ?? []);
1275
1339
  const discoveredServers: { name: string; source: SourceMeta }[] = [];
1276
1340
  if (this.ctx.mcpManager) {
1277
- for (const name of this.ctx.mcpManager.getAllServerNames()) {
1341
+ const allServerNames = await collectMcpServerNames(this.ctx, { userConfig, projectConfig });
1342
+ for (const name of allServerNames) {
1278
1343
  if (configServerNames.has(name)) continue;
1279
1344
  if (disabledServerNames.has(name)) continue;
1280
1345
  const source = this.ctx.mcpManager.getSource(name);
@@ -42,6 +42,13 @@ import {
42
42
  import type { InteractiveModeContext } from "../../modes/types";
43
43
  import type { SessionOAuthAccountList } from "../../session/agent-session-types";
44
44
  import type { ResetCreditAccountStatus, ResetCreditRedeemOutcome } from "../../session/auth-storage";
45
+ import {
46
+ createForeignSessionStore,
47
+ foreignSessionInfoToSessionInfo,
48
+ foreignSessionSourceName,
49
+ persistForeignSession,
50
+ } from "../../session/foreign-session-import";
51
+ import type { ForeignSessionInfo, ForeignSessionSource } from "../../session/foreign-session-store";
45
52
  import type { SessionInfo } from "../../session/session-listing";
46
53
  import { SessionManager } from "../../session/session-manager";
47
54
  import { FileSessionStorage } from "../../session/session-storage";
@@ -87,7 +94,7 @@ import { PluginSelectorComponent } from "../components/plugin-selector";
87
94
  import { ResetUsageSelectorComponent } from "../components/reset-usage-selector";
88
95
  import { renderSegmentTrack } from "../components/segment-track";
89
96
  import { SessionAccountSelectorComponent } from "../components/session-account-selector";
90
- import { SessionSelectorComponent } from "../components/session-selector";
97
+ import { SessionSelectorComponent, type SessionSelectorOptions } from "../components/session-selector";
91
98
  import { SettingsSelectorComponent } from "../components/settings-selector";
92
99
  import { ToolExecutionComponent } from "../components/tool-execution";
93
100
  import { TranscriptBlock } from "../components/transcript-container";
@@ -1358,20 +1365,87 @@ export class SelectorController {
1358
1365
  return result;
1359
1366
  }
1360
1367
 
1361
- async showSessionSelector(): Promise<void> {
1362
- const sessions = await SessionManager.list(
1363
- this.ctx.sessionManager.getCwd(),
1364
- this.ctx.sessionManager.getSessionDir(),
1365
- );
1366
- // Always open in current-folder scope; the empty-state hint in SessionList
1367
- // invites the user to Tab into all-projects rather than silently surfacing
1368
- // every project's history when the cwd has nothing to resume. See #3099.
1369
- const historyStorage = this.ctx.historyStorage;
1370
- const historyMatcher = historyStorage ? (query: string) => historyStorage.matchingSessionIds(query) : undefined;
1368
+ async showSessionSelector(source?: ForeignSessionSource): Promise<void> {
1369
+ let sessions: SessionInfo[];
1370
+ let onSelectSession: (session: SessionInfo) => Promise<boolean>;
1371
+ let selectorOptions: SessionSelectorOptions;
1372
+
1373
+ if (source) {
1374
+ const sourceName = foreignSessionSourceName(source);
1375
+ const store = createForeignSessionStore(source);
1376
+ let foreignSessions: ForeignSessionInfo[];
1377
+ try {
1378
+ foreignSessions = await store.list();
1379
+ } catch (error) {
1380
+ this.ctx.showError(
1381
+ `Failed to list ${sourceName} sessions: ${error instanceof Error ? error.message : String(error)}`,
1382
+ );
1383
+ return;
1384
+ }
1385
+ if (foreignSessions.length === 0) {
1386
+ this.ctx.showWarning(`No ${sourceName} sessions found`);
1387
+ return;
1388
+ }
1389
+ const foreignByPath = new Map(foreignSessions.map(session => [session.path, session]));
1390
+ sessions = foreignSessions.map(foreignSessionInfoToSessionInfo);
1391
+ onSelectSession = async session => {
1392
+ try {
1393
+ await this.ctx.settings.flush();
1394
+ } catch (error) {
1395
+ this.ctx.showError(
1396
+ `Failed to save pending settings: ${error instanceof Error ? error.message : String(error)}`,
1397
+ );
1398
+ return false;
1399
+ }
1400
+ const foreignSession = foreignByPath.get(session.path);
1401
+ if (!foreignSession) throw new Error(`Selected ${sourceName} session is no longer available`);
1402
+ const imported = await persistForeignSession(store, foreignSession, {
1403
+ fallbackCwd: this.ctx.sessionManager.getCwd(),
1404
+ suppressBreadcrumb: true,
1405
+ });
1406
+ const sessionFile = imported.getSessionFile();
1407
+ if (!sessionFile) throw new Error(`Failed to persist ${sourceName} session`);
1408
+ await imported.close();
1409
+ return await this.handleResumeSession(sessionFile, { settingsFlushed: true });
1410
+ };
1411
+ selectorOptions = {
1412
+ title: `Import ${sourceName} Session`,
1413
+ scopeLabel: false,
1414
+ showCwd: true,
1415
+ };
1416
+ } else {
1417
+ sessions = await SessionManager.list(
1418
+ this.ctx.sessionManager.getCwd(),
1419
+ this.ctx.sessionManager.getSessionDir(),
1420
+ );
1421
+ const historyStorage = this.ctx.historyStorage;
1422
+ const historyMatcher = historyStorage
1423
+ ? (query: string) => historyStorage.matchingSessionIds(query)
1424
+ : undefined;
1425
+ onSelectSession = session => this.handleResumeSession(session.path);
1426
+ selectorOptions = {
1427
+ onDelete: async (session: SessionInfo) => {
1428
+ if (!(await this.#detachActiveSessionBeforeDeletion(session.path))) {
1429
+ return false;
1430
+ }
1431
+ const storage = new FileSessionStorage();
1432
+ try {
1433
+ await storage.deleteSessionWithArtifacts(session.path);
1434
+ return true;
1435
+ } catch (error) {
1436
+ throw new Error(
1437
+ `Failed to delete session: ${error instanceof Error ? error.message : String(error)}`,
1438
+ { cause: error },
1439
+ );
1440
+ }
1441
+ },
1442
+ historyMatcher,
1443
+ loadAllSessions: () => SessionManager.listAll(),
1444
+ };
1445
+ }
1446
+
1371
1447
  // Keep the fullscreen picker on the alternate buffer while a selected
1372
- // session is loaded and its transcript is rebuilt. Closing it first exposes
1373
- // the stale normal buffer for the entire async switch on terminals without
1374
- // effective synchronized output.
1448
+ // session is loaded and its transcript is rebuilt.
1375
1449
  let overlayHandle: OverlayHandle | undefined;
1376
1450
  const done = () => {
1377
1451
  overlayHandle?.hide();
@@ -1384,43 +1458,27 @@ export class SelectorController {
1384
1458
  selector.lockInput();
1385
1459
  let keepOpen = false;
1386
1460
  try {
1387
- const success = await this.handleResumeSession(session.path);
1461
+ const success = await onSelectSession(session);
1388
1462
  if (!success) {
1389
1463
  keepOpen = true;
1390
1464
  selector.unlockInput();
1391
1465
  this.ctx.ui.requestRender();
1392
1466
  }
1467
+ } catch (error) {
1468
+ this.ctx.showError(error instanceof Error ? error.message : String(error));
1393
1469
  } finally {
1394
1470
  if (!keepOpen) done();
1395
1471
  }
1396
1472
  },
1397
- () => {
1398
- done();
1399
- },
1473
+ done,
1400
1474
  () => {
1401
1475
  // Release the alt buffer before teardown: shutdown() awaits flush/save/
1402
- // dispose/drain before stop() leaves the alt screen, so without this the
1403
- // fullscreen picker would freeze on screen for that window on Ctrl+C.
1476
+ // dispose/drain before stop() leaves the alt screen.
1404
1477
  done();
1405
1478
  void this.ctx.shutdown();
1406
1479
  },
1407
1480
  {
1408
- onDelete: async (session: SessionInfo) => {
1409
- if (!(await this.#detachActiveSessionBeforeDeletion(session.path))) {
1410
- return false;
1411
- }
1412
- const storage = new FileSessionStorage();
1413
- try {
1414
- await storage.deleteSessionWithArtifacts(session.path);
1415
- return true;
1416
- } catch (err) {
1417
- throw new Error(`Failed to delete session: ${err instanceof Error ? err.message : String(err)}`, {
1418
- cause: err,
1419
- });
1420
- }
1421
- },
1422
- historyMatcher,
1423
- loadAllSessions: () => SessionManager.listAll(),
1481
+ ...selectorOptions,
1424
1482
  getTerminalRows: () => this.ctx.ui.terminal.rows,
1425
1483
  fillHeight: true,
1426
1484
  },
@@ -80,6 +80,19 @@ export class TanCommandController {
80
80
  const ownerId = session.getAgentId() ?? MAIN_AGENT_ID;
81
81
  const mcpManager = this.ctx.mcpManager;
82
82
  const cwd = this.ctx.sessionManager.getCwd();
83
+ const parentArtifactsDir = this.ctx.sessionManager.getArtifactsDir();
84
+ // Snapshot the parent session's local:// mapping when dispatching. The
85
+ // interactive SessionManager is mutable and may switch transcripts while
86
+ // this background tan is still running. Use the session-manager id (not
87
+ // `session.sessionId`, which can diverge after `/fresh` or a provider
88
+ // session override) so the tan resolves the same local root the parent's
89
+ // large-paste writes and `local://` reads use — notably the Windows
90
+ // short-root fallback keys `%TEMP%/omp-local/<id>` off this id.
91
+ const parentLocalSessionId = this.ctx.sessionManager.getSessionId();
92
+ const localProtocolOptions = {
93
+ getArtifactsDir: () => parentArtifactsDir,
94
+ getSessionId: () => parentLocalSessionId,
95
+ };
83
96
  // Nest the clone inside the parent's artifact directory (like a subagent
84
97
  // session) rather than as a top-level sibling, so it shares the parent's
85
98
  // artifacts in place — no copy needed.
@@ -132,6 +145,7 @@ export class TanCommandController {
132
145
  parentAgentId: ownerId,
133
146
  agentRegistry,
134
147
  disableExtensionDiscovery: true,
148
+ localProtocolOptions,
135
149
  });
136
150
  clone = created.session;
137
151
  clone.sessionManager?.appendSessionInit?.({