@oh-my-pi/pi-coding-agent 16.5.0 → 16.5.2

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 (198) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/dist/cli.js +3556 -3504
  3. package/dist/types/advisor/advise-tool.d.ts +12 -1
  4. package/dist/types/advisor/runtime.d.ts +41 -1
  5. package/dist/types/cli/args.d.ts +2 -0
  6. package/dist/types/cli/update-cli.d.ts +4 -1
  7. package/dist/types/cli/usage-cli.d.ts +3 -0
  8. package/dist/types/cli/usage-error.d.ts +4 -0
  9. package/dist/types/config/api-key-resolver.d.ts +2 -2
  10. package/dist/types/config/model-registry.d.ts +3 -3
  11. package/dist/types/config/model-resolver.d.ts +8 -1
  12. package/dist/types/config/models-config.d.ts +1 -1
  13. package/dist/types/config/settings-schema.d.ts +10 -0
  14. package/dist/types/discovery/substitute-plugin-root.d.ts +22 -0
  15. package/dist/types/eval/__tests__/process-entry-import.test.d.ts +1 -0
  16. package/dist/types/eval/backend.d.ts +3 -3
  17. package/dist/types/eval/bridge-timeout.d.ts +9 -1
  18. package/dist/types/eval/js/context-manager.d.ts +5 -3
  19. package/dist/types/eval/js/process-entry.d.ts +6 -0
  20. package/dist/types/eval/js/worker-core.d.ts +15 -1
  21. package/dist/types/eval/py/spawn-options.d.ts +10 -0
  22. package/dist/types/eval/py/tool-bridge.d.ts +1 -0
  23. package/dist/types/extensibility/custom-tools/types.d.ts +3 -0
  24. package/dist/types/extensibility/extensions/runner.d.ts +3 -1
  25. package/dist/types/extensibility/extensions/types.d.ts +3 -0
  26. package/dist/types/extensibility/extensions/wrapper.d.ts +3 -6
  27. package/dist/types/extensibility/plugins/bun-git-cache.d.ts +3 -0
  28. package/dist/types/goals/guided-setup.d.ts +12 -0
  29. package/dist/types/internal-urls/history-protocol.d.ts +3 -2
  30. package/dist/types/internal-urls/memory-protocol.d.ts +6 -7
  31. package/dist/types/internal-urls/registry-helpers.d.ts +19 -0
  32. package/dist/types/main.d.ts +1 -0
  33. package/dist/types/mcp/oauth-discovery.d.ts +2 -0
  34. package/dist/types/mcp/oauth-flow.d.ts +2 -0
  35. package/dist/types/modes/components/__tests__/dynamic-border.test.d.ts +1 -0
  36. package/dist/types/modes/components/agent-hub.d.ts +10 -0
  37. package/dist/types/modes/components/dynamic-border.d.ts +5 -3
  38. package/dist/types/modes/components/login-dialog.d.ts +2 -0
  39. package/dist/types/modes/components/mcp-add-wizard.d.ts +1 -0
  40. package/dist/types/modes/components/read-tool-group.d.ts +0 -2
  41. package/dist/types/modes/components/transcript-container.d.ts +3 -2
  42. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  43. package/dist/types/modes/interactive-mode.d.ts +1 -0
  44. package/dist/types/modes/magic-keyword-boundary.d.ts +9 -0
  45. package/dist/types/modes/orchestrate.d.ts +1 -1
  46. package/dist/types/modes/rpc/host-tools.d.ts +2 -0
  47. package/dist/types/modes/rpc/rpc-mode.d.ts +26 -6
  48. package/dist/types/modes/types.d.ts +1 -0
  49. package/dist/types/modes/ultrathink.d.ts +1 -1
  50. package/dist/types/modes/utils/transcript-render-helpers.d.ts +12 -0
  51. package/dist/types/modes/workflow.d.ts +1 -1
  52. package/dist/types/session/agent-session.d.ts +6 -0
  53. package/dist/types/session/exit-diagnostics.d.ts +11 -0
  54. package/dist/types/session/messages.d.ts +15 -0
  55. package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +11 -0
  56. package/dist/types/subprocess/worker-client.d.ts +6 -0
  57. package/dist/types/tools/bash-skill-urls.d.ts +1 -0
  58. package/dist/types/tools/grep.d.ts +0 -2
  59. package/dist/types/tools/read.d.ts +0 -4
  60. package/dist/types/web/search/provider.d.ts +10 -3
  61. package/dist/types/web/search/providers/codex.d.ts +5 -4
  62. package/package.json +12 -12
  63. package/src/advisor/__tests__/advisor.test.ts +917 -42
  64. package/src/advisor/advise-tool.ts +17 -1
  65. package/src/advisor/runtime.ts +270 -67
  66. package/src/autolearn/controller.ts +15 -3
  67. package/src/autoresearch/dashboard.ts +2 -2
  68. package/src/cli/args.ts +12 -0
  69. package/src/cli/auth-broker-cli.ts +30 -11
  70. package/src/cli/auth-gateway-cli.ts +5 -1
  71. package/src/cli/config-cli.ts +15 -3
  72. package/src/cli/dry-balance-cli.ts +14 -4
  73. package/src/cli/flag-tables.ts +21 -7
  74. package/src/cli/update-cli.ts +62 -11
  75. package/src/cli/usage-cli.ts +58 -5
  76. package/src/cli/usage-error.ts +7 -0
  77. package/src/cli.ts +23 -1
  78. package/src/commands/acp.ts +11 -2
  79. package/src/commands/launch.ts +12 -3
  80. package/src/commands/token.ts +3 -1
  81. package/src/config/api-key-resolver.ts +12 -3
  82. package/src/config/config-file.ts +30 -12
  83. package/src/config/model-registry.ts +7 -7
  84. package/src/config/model-resolver.ts +21 -7
  85. package/src/config/models-config.ts +1 -1
  86. package/src/config/settings-schema.ts +10 -0
  87. package/src/cursor.ts +2 -0
  88. package/src/discovery/claude-plugins.ts +9 -3
  89. package/src/discovery/omp-plugins.ts +6 -2
  90. package/src/discovery/substitute-plugin-root.ts +32 -0
  91. package/src/eval/__tests__/agent-bridge.test.ts +19 -14
  92. package/src/eval/__tests__/bridge-timeout.test.ts +106 -0
  93. package/src/eval/__tests__/js-context-manager.test.ts +158 -1
  94. package/src/eval/__tests__/kernel-spawn.test.ts +12 -0
  95. package/src/eval/__tests__/prelude-agent.test.ts +20 -0
  96. package/src/eval/__tests__/process-entry-import.test.ts +27 -0
  97. package/src/eval/agent-bridge.ts +121 -116
  98. package/src/eval/backend.ts +3 -3
  99. package/src/eval/bridge-timeout.ts +20 -2
  100. package/src/eval/executor-base.ts +85 -7
  101. package/src/eval/jl/kernel.ts +2 -1
  102. package/src/eval/js/context-manager.ts +109 -32
  103. package/src/eval/js/process-entry.ts +27 -0
  104. package/src/eval/js/shared/runtime.ts +1 -1
  105. package/src/eval/js/worker-core.ts +70 -9
  106. package/src/eval/js/worker-entry.ts +1 -1
  107. package/src/eval/py/__tests__/prelude.test.ts +72 -0
  108. package/src/eval/py/kernel.ts +2 -1
  109. package/src/eval/py/prelude.py +28 -1
  110. package/src/eval/py/spawn-options.ts +13 -0
  111. package/src/eval/py/tool-bridge.ts +13 -14
  112. package/src/eval/rb/kernel.ts +2 -1
  113. package/src/exec/bash-executor.ts +30 -43
  114. package/src/extensibility/custom-tools/types.ts +3 -0
  115. package/src/extensibility/extensions/runner.ts +3 -0
  116. package/src/extensibility/extensions/types.ts +3 -0
  117. package/src/extensibility/extensions/wrapper.ts +18 -18
  118. package/src/extensibility/plugins/bun-git-cache.ts +91 -0
  119. package/src/extensibility/plugins/legacy-pi-compat.ts +32 -16
  120. package/src/extensibility/plugins/manager.ts +28 -7
  121. package/src/goals/guided-setup.ts +29 -1
  122. package/src/internal-urls/history-protocol.ts +95 -15
  123. package/src/internal-urls/memory-protocol.ts +13 -9
  124. package/src/internal-urls/registry-helpers.ts +50 -1
  125. package/src/launch/broker.ts +38 -25
  126. package/src/lsp/client.ts +7 -1
  127. package/src/main.ts +29 -0
  128. package/src/mcp/oauth-discovery.ts +20 -1
  129. package/src/mcp/oauth-flow.ts +3 -1
  130. package/src/mcp/tool-bridge.ts +57 -6
  131. package/src/modes/components/__tests__/dynamic-border.test.ts +55 -0
  132. package/src/modes/components/agent-dashboard.ts +2 -2
  133. package/src/modes/components/agent-hub.ts +15 -2
  134. package/src/modes/components/agent-transcript-viewer.ts +2 -2
  135. package/src/modes/components/chat-transcript-builder.ts +26 -4
  136. package/src/modes/components/dynamic-border.ts +9 -6
  137. package/src/modes/components/extensions/extension-list.ts +2 -2
  138. package/src/modes/components/hook-selector.ts +10 -4
  139. package/src/modes/components/login-dialog.ts +5 -0
  140. package/src/modes/components/mcp-add-wizard.ts +5 -0
  141. package/src/modes/components/plan-review-overlay.ts +11 -11
  142. package/src/modes/components/read-tool-group.ts +1 -8
  143. package/src/modes/components/status-line/component.ts +10 -1
  144. package/src/modes/components/transcript-container.ts +110 -7
  145. package/src/modes/controllers/command-controller.ts +12 -4
  146. package/src/modes/controllers/event-controller.ts +80 -15
  147. package/src/modes/controllers/input-controller.ts +4 -2
  148. package/src/modes/controllers/mcp-command-controller.ts +6 -7
  149. package/src/modes/controllers/selector-controller.ts +24 -5
  150. package/src/modes/controllers/todo-command-controller.ts +18 -14
  151. package/src/modes/interactive-mode.ts +7 -3
  152. package/src/modes/magic-keyword-boundary.ts +23 -0
  153. package/src/modes/orchestrate.ts +6 -5
  154. package/src/modes/print-mode.ts +9 -0
  155. package/src/modes/prompt-action-autocomplete.ts +6 -1
  156. package/src/modes/rpc/host-tools.ts +15 -0
  157. package/src/modes/rpc/rpc-mode.ts +123 -48
  158. package/src/modes/types.ts +1 -1
  159. package/src/modes/ultrathink.ts +6 -5
  160. package/src/modes/utils/transcript-render-helpers.ts +54 -0
  161. package/src/modes/utils/ui-helpers.ts +27 -1
  162. package/src/modes/workflow.ts +6 -5
  163. package/src/prompts/advisor/system.md +1 -0
  164. package/src/prompts/system/system-prompt.md +1 -0
  165. package/src/prompts/tools/eval.md +2 -2
  166. package/src/prompts/tools/grep.md +1 -2
  167. package/src/prompts/tools/read.md +2 -4
  168. package/src/sdk.ts +61 -34
  169. package/src/session/agent-session.ts +283 -39
  170. package/src/session/exit-diagnostics.ts +108 -0
  171. package/src/session/messages.test.ts +66 -0
  172. package/src/session/messages.ts +37 -0
  173. package/src/session/streaming-output.ts +40 -12
  174. package/src/slash-commands/helpers/active-oauth-account.ts +22 -2
  175. package/src/slash-commands/helpers/logout.ts +23 -3
  176. package/src/slash-commands/helpers/usage-report.ts +14 -2
  177. package/src/subprocess/worker-client.ts +9 -2
  178. package/src/system-prompt.test.ts +36 -0
  179. package/src/system-prompt.ts +1 -1
  180. package/src/task/executor.ts +8 -0
  181. package/src/task/render.test.ts +36 -0
  182. package/src/task/render.ts +55 -43
  183. package/src/tools/bash-skill-urls.ts +4 -1
  184. package/src/tools/bash.ts +1 -0
  185. package/src/tools/browser/registry.ts +17 -3
  186. package/src/tools/eval.ts +14 -9
  187. package/src/tools/gh.ts +3 -1
  188. package/src/tools/grep.ts +5 -45
  189. package/src/tools/path-utils.ts +7 -1
  190. package/src/tools/read.ts +23 -74
  191. package/src/tools/write.ts +82 -9
  192. package/src/tools/yield.ts +29 -1
  193. package/src/utils/title-generator.ts +10 -6
  194. package/src/web/search/index.ts +39 -22
  195. package/src/web/search/provider.ts +33 -16
  196. package/src/web/search/providers/codex.ts +68 -21
  197. package/src/web/search/providers/perplexity-auth.ts +20 -11
  198. package/src/web/search/providers/perplexity.ts +14 -2
@@ -3,6 +3,7 @@ import {
3
3
  Container,
4
4
  type NativeScrollbackCommittedRows,
5
5
  type NativeScrollbackLiveRegion,
6
+ type NativeScrollbackReplay,
6
7
  type RenderStablePrefix,
7
8
  type ViewportTailProvider,
8
9
  } from "@oh-my-pi/pi-tui";
@@ -119,6 +120,8 @@ interface BlockSegment {
119
120
  sep: number;
120
121
  /** Whether the block reported finalized when this segment was rendered. */
121
122
  finalized: boolean;
123
+ /** Safe to drop after commit: produced while finalized, without post-finalize version tracking. */
124
+ compactable: boolean;
122
125
  /** Block version observed when this segment was rendered (see {@link FinalizableBlock}). */
123
126
  version: number | undefined;
124
127
  }
@@ -154,7 +157,12 @@ const EMPTY_TAIL: readonly string[] = [];
154
157
  */
155
158
  export class TranscriptContainer
156
159
  extends Container
157
- implements NativeScrollbackLiveRegion, NativeScrollbackCommittedRows, RenderStablePrefix, ViewportTailProvider
160
+ implements
161
+ NativeScrollbackLiveRegion,
162
+ NativeScrollbackCommittedRows,
163
+ NativeScrollbackReplay,
164
+ RenderStablePrefix,
165
+ ViewportTailProvider
158
166
  {
159
167
  // Bumped to retire every block segment at once (theme change / clear); a
160
168
  // segment is only reused when its stored generation matches.
@@ -172,6 +180,14 @@ export class TranscriptContainer
172
180
  // Finalized blocks wholly before this boundary are immutable on-screen history;
173
181
  // their previous contribution can be replayed without calling render().
174
182
  #committedRows = 0;
183
+ // Leading children whose rows were handed to native scrollback and dropped
184
+ // from the local frame. Children remain owned by the session and can be
185
+ // re-rendered when the TUI prepares a destructive full replay.
186
+ #compactedChildStart = 0;
187
+ // Suppresses re-compaction for the rehydrating render. The TUI feeds its old
188
+ // committed-row count immediately before render, so resetting that count
189
+ // alone cannot distinguish a replay from an ordinary update.
190
+ #replayPending = false;
175
191
  // Stable-prefix floor accumulated across renders since the last
176
192
  // getRenderStablePrefixRows() read (see RenderStablePrefix: reading
177
193
  // consumes the report and re-bases the baseline). Out-of-band renders
@@ -187,12 +203,24 @@ export class TranscriptContainer
187
203
  override clear(): void {
188
204
  this.#generation++;
189
205
  super.clear();
206
+ this.#compactedChildStart = 0;
207
+ this.#committedRows = 0;
208
+ this.#replayPending = false;
190
209
  }
191
210
 
192
211
  setNativeScrollbackCommittedRows(rows: number): void {
193
212
  this.#committedRows = Number.isFinite(rows) ? Math.max(0, Math.trunc(rows)) : 0;
194
213
  }
195
214
 
215
+ prepareNativeScrollbackReplay(): void {
216
+ if (this.#compactedChildStart === 0) return;
217
+ this.#compactedChildStart = 0;
218
+ this.#replayPending = true;
219
+ this.#generation++;
220
+ this.#lines.length = 0;
221
+ this.#stableRowsFloor = 0;
222
+ }
223
+
196
224
  getRenderStablePrefixRows(): number {
197
225
  const value = Math.min(this.#stableRowsFloor, this.#lines.length);
198
226
  this.#stableRowsFloor = this.#lines.length;
@@ -213,8 +241,14 @@ export class TranscriptContainer
213
241
  * committed rows and is safely removable.
214
242
  */
215
243
  isBlockUncommitted(component: Component): boolean {
244
+ const index = this.children.indexOf(component);
245
+ // Compacted prefix is already committed native history and must not be
246
+ // retracted. Compacted slots may be sparse holes after a later re-render
247
+ // (render only fills from #compactedChildStart), so the loop below must
248
+ // skip undefined entries.
249
+ if (index >= 0 && index < this.#compactedChildStart) return false;
216
250
  for (const segment of this.#segments) {
217
- if (segment.component !== component) continue;
251
+ if (segment === undefined || segment.component !== component) continue;
218
252
  return segment.rowCount === 0 || segment.startRow >= this.#committedRows;
219
253
  }
220
254
  return true;
@@ -268,7 +302,7 @@ export class TranscriptContainer
268
302
  if (maxRows <= 0) return EMPTY_TAIL;
269
303
  const collected: (readonly string[])[] = [];
270
304
  let total = 0;
271
- for (let i = this.children.length - 1; i >= 0 && total < maxRows; i--) {
305
+ for (let i = this.children.length - 1; i >= this.#compactedChildStart && total < maxRows; i--) {
272
306
  const contribution = stripPlainBlankEdges(this.children[i]!.render(width));
273
307
  if (contribution.length === 0) continue;
274
308
  // One blank separator sits between this block and the (already
@@ -292,6 +326,7 @@ export class TranscriptContainer
292
326
  this.#nativeScrollbackLiveRegionStart = undefined;
293
327
 
294
328
  const count = this.children.length;
329
+ if (this.#compactedChildStart > count) this.#compactedChildStart = count;
295
330
 
296
331
  // Seal displaceable snapshots whose rows are already on the tape (per the
297
332
  // previous frame's segments — the geometry the committed count was
@@ -301,8 +336,9 @@ export class TranscriptContainer
301
336
  // frame, and every frame so a block that BECAME displaceable after its
302
337
  // pending-preview rows committed (late result on a scrolled-off call) is
303
338
  // caught too.
304
- for (let i = 0; i < count && i < this.#segments.length; i++) {
305
- const previous = this.#segments[i]!;
339
+ for (let i = this.#compactedChildStart; i < count && i < this.#segments.length; i++) {
340
+ const previous = this.#segments[i];
341
+ if (previous === undefined) continue;
306
342
  if (previous.startRow >= this.#committedRows) break;
307
343
  if (previous.rowCount === 0 || previous.component !== this.children[i]) continue;
308
344
  sealCommittedSnapshot(previous.component);
@@ -316,7 +352,7 @@ export class TranscriptContainer
316
352
  // reaches.
317
353
  let liveStartIndex = -1;
318
354
  let hasLiveBlock = false;
319
- for (let i = 0; i < count; i++) {
355
+ for (let i = this.#compactedChildStart; i < count; i++) {
320
356
  if (!isBlockFinalized(this.children[i]!)) {
321
357
  liveStartIndex = i;
322
358
  hasLiveBlock = true;
@@ -348,7 +384,7 @@ export class TranscriptContainer
348
384
  // Frame row cursor: rows emitted (reused or pushed) so far.
349
385
  let row = 0;
350
386
  let stableRows = 0;
351
- for (let i = 0; i < count; i++) {
387
+ for (let i = this.#compactedChildStart; i < count; i++) {
352
388
  const child = this.children[i]!;
353
389
 
354
390
  // This child's contribution: its current render with plain-blank
@@ -389,6 +425,7 @@ export class TranscriptContainer
389
425
  previous.width === width &&
390
426
  previous.generation === this.#generation);
391
427
  const contribution = reusable ? previous.contribution : stripPlainBlankEdges(raw);
428
+ const compactable = finalized && version === undefined && previous?.finalized !== false;
392
429
 
393
430
  // Empty (or stripped-to-nothing) children contribute nothing and never
394
431
  // affect spacing. An empty still-live child still gates the commit
@@ -413,6 +450,7 @@ export class TranscriptContainer
413
450
  rowCount: 0,
414
451
  sep: 0,
415
452
  finalized,
453
+ compactable,
416
454
  version,
417
455
  };
418
456
  continue;
@@ -464,6 +502,7 @@ export class TranscriptContainer
464
502
  rowCount,
465
503
  sep,
466
504
  finalized,
505
+ compactable,
467
506
  version,
468
507
  };
469
508
  row += rowCount;
@@ -473,8 +512,72 @@ export class TranscriptContainer
473
512
  if (lines.length !== row) lines.length = row;
474
513
  this.#segments = segments;
475
514
  this.#stableRowsFloor = Math.min(stableFloorBefore, stableRows, row);
515
+ if (this.#replayPending) {
516
+ this.#replayPending = false;
517
+ } else {
518
+ this.#compactCommittedPrefix();
519
+ }
476
520
  return lines;
477
521
  }
522
+
523
+ #compactCommittedPrefix(): void {
524
+ if (this.#committedRows <= 0 || this.#compactedChildStart >= this.children.length) return;
525
+ const lines = this.#lines;
526
+ const segments = this.#segments;
527
+ let dropRows = 0;
528
+ let dropUntil = this.#compactedChildStart;
529
+ for (let i = this.#compactedChildStart; i < segments.length; i++) {
530
+ const segment = segments[i];
531
+ if (segment === undefined || !segment.compactable) break;
532
+ const segmentEnd = segment.startRow + segment.rowCount;
533
+ if (segmentEnd > this.#committedRows) break;
534
+ dropRows = segmentEnd;
535
+ dropUntil = i + 1;
536
+ }
537
+ const retained = segments[dropUntil];
538
+ if (retained !== undefined && retained.sep > 0) {
539
+ const committedSeparatorRows = this.#committedRows - retained.startRow;
540
+ if (committedSeparatorRows <= 0) {
541
+ dropRows = 0;
542
+ dropUntil = this.#compactedChildStart;
543
+ } else {
544
+ const trim = Math.min(retained.sep, committedSeparatorRows);
545
+ dropRows += trim;
546
+ retained.sep -= trim;
547
+ retained.rowCount -= trim;
548
+ }
549
+ }
550
+ if (dropRows === 0) return;
551
+
552
+ lines.splice(0, dropRows);
553
+ for (let i = this.#compactedChildStart; i < dropUntil; i++) {
554
+ const segment = segments[i];
555
+ if (segment === undefined) continue;
556
+ segments[i] = {
557
+ component: segment.component,
558
+ rawRef: EMPTY_TAIL,
559
+ contribution: EMPTY_TAIL,
560
+ width: segment.width,
561
+ generation: segment.generation,
562
+ startRow: 0,
563
+ rowCount: 0,
564
+ sep: 0,
565
+ finalized: true,
566
+ compactable: false,
567
+ version: segment.version,
568
+ };
569
+ }
570
+ for (let i = dropUntil; i < segments.length; i++) {
571
+ const segment = segments[i];
572
+ if (segment !== undefined) segment.startRow -= dropRows;
573
+ }
574
+ this.#compactedChildStart = dropUntil;
575
+ this.#committedRows = Math.max(0, this.#committedRows - dropRows);
576
+ this.#stableRowsFloor = 0;
577
+ if (this.#nativeScrollbackLiveRegionStart !== undefined) {
578
+ this.#nativeScrollbackLiveRegionStart = Math.max(0, this.#nativeScrollbackLiveRegionStart - dropRows);
579
+ }
580
+ }
478
581
  }
479
582
 
480
583
  /**
@@ -1388,14 +1388,22 @@ function formatWindowSuffix(label: string, windowLabel: string, uiTheme: typeof
1388
1388
  return uiTheme.fg("dim", `(${windowLabel})`);
1389
1389
  }
1390
1390
 
1391
+ /** ` (org)` suffix when the report is org-attributed — two subscriptions can share one email. */
1392
+ function orgSuffix(report: UsageReport): string {
1393
+ const orgName = report.metadata?.orgName;
1394
+ const orgId = report.metadata?.orgId;
1395
+ const org = typeof orgName === "string" && orgName ? orgName : typeof orgId === "string" ? orgId : undefined;
1396
+ return org ? ` (${org})` : "";
1397
+ }
1398
+
1391
1399
  function formatAccountLabel(limit: UsageLimit, report: UsageReport, index: number): string {
1392
1400
  const email = report.metadata?.email;
1393
- if (typeof email === "string" && email) return email;
1401
+ if (typeof email === "string" && email) return `${email}${orgSuffix(report)}`;
1394
1402
  const accountId =
1395
1403
  typeof report.metadata?.accountId === "string" && report.metadata.accountId
1396
1404
  ? report.metadata.accountId
1397
1405
  : limit.scope.accountId || undefined;
1398
- if (accountId) return accountId;
1406
+ if (accountId) return `${accountId}${orgSuffix(report)}`;
1399
1407
  const projectId =
1400
1408
  typeof report.metadata?.projectId === "string" && report.metadata.projectId
1401
1409
  ? report.metadata.projectId
@@ -1406,9 +1414,9 @@ function formatAccountLabel(limit: UsageLimit, report: UsageReport, index: numbe
1406
1414
 
1407
1415
  function formatUnlimitedReportLabel(report: UsageReport, index: number): string {
1408
1416
  const email = report.metadata?.email;
1409
- if (typeof email === "string" && email) return email;
1417
+ if (typeof email === "string" && email) return `${email}${orgSuffix(report)}`;
1410
1418
  const accountId = report.metadata?.accountId;
1411
- if (typeof accountId === "string" && accountId) return accountId;
1419
+ if (typeof accountId === "string" && accountId) return `${accountId}${orgSuffix(report)}`;
1412
1420
  const projectId = report.metadata?.projectId;
1413
1421
  if (typeof projectId === "string" && projectId) return projectId;
1414
1422
  return `account ${index + 1}`;
@@ -1,4 +1,4 @@
1
- import type { ImageContent } from "@oh-my-pi/pi-ai";
1
+ import type { AssistantMessage, ImageContent } from "@oh-my-pi/pi-ai";
2
2
  import * as AIError from "@oh-my-pi/pi-ai/error";
3
3
  import { getStreamingPartialJson } from "@oh-my-pi/pi-ai/utils/block-symbols";
4
4
  import { type Component, Loader, TERMINAL } from "@oh-my-pi/pi-tui";
@@ -32,7 +32,11 @@ import { vocalizer } from "../../tts/vocalizer";
32
32
  import { canonicalizeMessage } from "../../utils/thinking-display";
33
33
  import { interruptHint } from "../shared";
34
34
  import { createAssistantMessageComponent } from "../utils/interactive-context-helpers";
35
- import { assistantUsageIsBilled } from "../utils/transcript-render-helpers";
35
+ import {
36
+ assistantHasVisibleContent,
37
+ assistantUsageIsBilled,
38
+ splitAssistantMessageToolTimeline,
39
+ } from "../utils/transcript-render-helpers";
36
40
  import { StreamingRevealController } from "./streaming-reveal";
37
41
  import { streamingStringKeysForTool, ToolArgsRevealController } from "./tool-args-reveal";
38
42
 
@@ -79,6 +83,8 @@ export class EventController {
79
83
  #backgroundTaskCallIds = new Set<string>();
80
84
  #readToolCallArgs = new Map<string, Record<string, unknown>>();
81
85
  #readToolCallAssistantComponents = new Map<string, AssistantMessageComponent>();
86
+ #toolTimelineComponents = new Map<string, Component>();
87
+ #postToolAssistantComponents = new Map<string, AssistantMessageComponent>();
82
88
  #lastAssistantComponent: AssistantMessageComponent | undefined = undefined;
83
89
  // Assistant component whose turn-ending error is currently mirrored in the
84
90
  // pinned banner. Its inline `Error: …` line is suppressed while pinned and
@@ -254,6 +260,39 @@ export class EventController {
254
260
  assistantComponent.setToolResultImages(toolCallId, images);
255
261
  return true;
256
262
  }
263
+
264
+ #insertAfterTranscriptComponent(anchor: Component | undefined, component: Component): boolean {
265
+ const children = this.ctx.chatContainer.children;
266
+ const anchorIndex = anchor ? children.indexOf(anchor) : -1;
267
+ if (anchorIndex < 0) return false;
268
+ if (children.slice(anchorIndex + 1).some(child => !this.ctx.chatContainer.isBlockUncommitted(child))) {
269
+ return false;
270
+ }
271
+ this.ctx.chatContainer.addChild(component);
272
+ children.splice(children.length - 1, 1);
273
+ children.splice(anchorIndex + 1, 0, component);
274
+ return true;
275
+ }
276
+
277
+ #upsertPostToolAssistantSegment(
278
+ toolCallId: string,
279
+ segment: AssistantMessage | undefined,
280
+ ): AssistantMessageComponent | undefined {
281
+ if (!segment || !assistantHasVisibleContent(segment)) return undefined;
282
+ const existing = this.#postToolAssistantComponents.get(toolCallId);
283
+ if (existing) {
284
+ existing.updateContent(segment);
285
+ return existing;
286
+ }
287
+ const component = createAssistantMessageComponent(this.ctx);
288
+ component.updateContent(segment);
289
+ this.#postToolAssistantComponents.set(toolCallId, component);
290
+ if (!this.#insertAfterTranscriptComponent(this.#toolTimelineComponents.get(toolCallId), component)) {
291
+ this.ctx.chatContainer.addChild(component);
292
+ }
293
+ return component;
294
+ }
295
+
257
296
  #updateWorkingMessageFromIntent(intent: unknown): void {
258
297
  if (this.ctx.session.isAborting) return;
259
298
  // Streamed JSON can deliver non-string `i` (object, number, boolean) before
@@ -281,6 +320,8 @@ export class EventController {
281
320
  this.#lastVisibleBlockCount = 0;
282
321
  this.#renderedCustomMessages.clear();
283
322
  this.#lastIntent = undefined;
323
+ this.#toolTimelineComponents.clear();
324
+ this.#postToolAssistantComponents.clear();
284
325
  this.#backgroundTaskCallIds.clear();
285
326
  this.#readToolCallArgs.clear();
286
327
  this.#readToolCallAssistantComponents.clear();
@@ -365,6 +406,8 @@ export class EventController {
365
406
  }
366
407
 
367
408
  async #handleAgentStart(_event: Extract<AgentSessionEvent, { type: "agent_start" }>): Promise<void> {
409
+ this.#toolTimelineComponents.clear();
410
+ this.#postToolAssistantComponents.clear();
368
411
  this.#lastIntent = undefined;
369
412
  this.#readToolCallArgs.clear();
370
413
  this.#readToolCallAssistantComponents.clear();
@@ -463,7 +506,10 @@ export class EventController {
463
506
  this.ctx.streamingComponent = createAssistantMessageComponent(this.ctx);
464
507
  this.ctx.streamingMessage = event.message;
465
508
  this.ctx.chatContainer.addChild(this.ctx.streamingComponent);
466
- this.#streamingReveal.begin(this.ctx.streamingComponent, this.ctx.streamingMessage);
509
+ this.#streamingReveal.begin(
510
+ this.ctx.streamingComponent,
511
+ splitAssistantMessageToolTimeline(this.ctx.streamingMessage).beforeTools,
512
+ );
467
513
  this.ctx.ui.requestRender();
468
514
  }
469
515
  }
@@ -648,7 +694,8 @@ export class EventController {
648
694
  this.#streamingReveal.resyncVisibility();
649
695
  }
650
696
  this.ctx.streamingMessage = event.message;
651
- this.#streamingReveal.setTarget(this.ctx.streamingMessage);
697
+ const timeline = splitAssistantMessageToolTimeline(this.ctx.streamingMessage);
698
+ this.#streamingReveal.setTarget(timeline.beforeTools);
652
699
 
653
700
  const visibleBlockCount = this.ctx.streamingMessage.content.filter(
654
701
  content =>
@@ -691,6 +738,7 @@ export class EventController {
691
738
  const group = this.#getReadGroup();
692
739
  group.updateArgs(content.arguments, content.id);
693
740
  this.ctx.pendingTools.set(content.id, group);
741
+ this.#toolTimelineComponents.set(content.id, group);
694
742
  }
695
743
  continue;
696
744
  }
@@ -737,6 +785,7 @@ export class EventController {
737
785
  component.setExpanded(this.ctx.toolOutputExpanded);
738
786
  this.ctx.chatContainer.addChild(component);
739
787
  this.ctx.pendingTools.set(content.id, component);
788
+ this.#toolTimelineComponents.set(content.id, component);
740
789
  this.#toolArgsReveal.bind(content.id, component);
741
790
  } else {
742
791
  const component = this.ctx.pendingTools.get(content.id);
@@ -746,6 +795,9 @@ export class EventController {
746
795
  }
747
796
  }
748
797
  }
798
+ for (const [toolCallId, segment] of timeline.afterToolCalls) {
799
+ this.#upsertPostToolAssistantSegment(toolCallId, segment);
800
+ }
749
801
 
750
802
  // Update working message with intent from streamed tool arguments
751
803
  for (const content of this.ctx.streamingMessage.content) {
@@ -810,15 +862,18 @@ export class EventController {
810
862
  errorMessage = resolveAbortLabel(this.ctx.streamingMessage, this.ctx.viewSession.retryAttempt);
811
863
  this.ctx.streamingMessage.errorMessage = errorMessage;
812
864
  }
813
- if (silentlyAborted || ttsrSilenced) {
814
- // Silence the streaming render by downgrading stopReason to "stop" for
815
- // display only — does NOT mutate the persisted message's stopReason
816
- // (the marker on errorMessage drives replay-side suppression).
817
- const msgWithoutAbort = { ...this.ctx.streamingMessage, stopReason: "stop" as const };
818
- this.ctx.streamingComponent.updateContent(msgWithoutAbort);
819
- } else {
820
- this.ctx.streamingComponent.updateContent(this.ctx.streamingMessage);
821
- }
865
+ const displayMessage: AssistantMessage =
866
+ silentlyAborted || ttsrSilenced
867
+ ? {
868
+ // Silence the streaming render by downgrading stopReason to "stop" for
869
+ // display only does NOT mutate the persisted message's stopReason
870
+ // (the marker on errorMessage drives replay-side suppression).
871
+ ...this.ctx.streamingMessage,
872
+ stopReason: "stop",
873
+ }
874
+ : this.ctx.streamingMessage;
875
+ const displayTimeline = splitAssistantMessageToolTimeline(displayMessage);
876
+ this.ctx.streamingComponent.updateContent(displayTimeline.beforeTools);
822
877
 
823
878
  if (this.ctx.streamingMessage.stopReason !== "aborted" && this.ctx.streamingMessage.stopReason !== "error") {
824
879
  for (const [toolCallId, component] of this.ctx.pendingTools.entries()) {
@@ -848,8 +903,14 @@ export class EventController {
848
903
  }
849
904
  this.ctx.lastAssistantUsage = usage;
850
905
  }
851
- this.#lastAssistantComponent = this.ctx.streamingComponent;
852
- this.#lastAssistantComponent.markTranscriptBlockFinalized();
906
+ this.ctx.streamingComponent.markTranscriptBlockFinalized();
907
+ let lastPostToolAssistantComponent: AssistantMessageComponent | undefined;
908
+ for (const [toolCallId, segment] of displayTimeline.afterToolCalls) {
909
+ const component = this.#upsertPostToolAssistantSegment(toolCallId, segment);
910
+ component?.markTranscriptBlockFinalized();
911
+ if (component) lastPostToolAssistantComponent = component;
912
+ }
913
+ this.#lastAssistantComponent = lastPostToolAssistantComponent ?? this.ctx.streamingComponent;
853
914
  if (settings.get("display.showTokenUsage") && assistantUsageIsBilled(event.message.usage)) {
854
915
  this.ctx.chatContainer.addChild(
855
916
  createUsageRowBlock(event.message.usage, event.message.duration, event.message.ttft),
@@ -886,6 +947,7 @@ export class EventController {
886
947
  const group = this.#getReadGroup();
887
948
  group.updateArgs(event.args, event.toolCallId);
888
949
  this.ctx.pendingTools.set(event.toolCallId, group);
950
+ this.#toolTimelineComponents.set(event.toolCallId, group);
889
951
  }
890
952
  this.ctx.ui.requestRender();
891
953
  return;
@@ -911,6 +973,7 @@ export class EventController {
911
973
  component.setExpanded(this.ctx.toolOutputExpanded);
912
974
  this.ctx.chatContainer.addChild(component);
913
975
  this.ctx.pendingTools.set(event.toolCallId, component);
976
+ this.#toolTimelineComponents.set(event.toolCallId, component);
914
977
  this.ctx.ui.requestRender();
915
978
  } else {
916
979
  // The tool is about to run, so its arguments are final and validated.
@@ -1104,6 +1167,8 @@ export class EventController {
1104
1167
  );
1105
1168
  this.#readToolCallArgs.clear();
1106
1169
  this.#readToolCallAssistantComponents.clear();
1170
+ this.#toolTimelineComponents.clear();
1171
+ this.#postToolAssistantComponents.clear();
1107
1172
  this.#resetReadGroup();
1108
1173
  // The turn is over: nothing else lands this turn, so the waiting poll is
1109
1174
  // final history — seal it instead of letting its spinner tick while idle.
@@ -488,14 +488,16 @@ export class InputController {
488
488
  // main session, or returns the focused subagent view to the main session.
489
489
  // Focused ←← intentionally matches Esc. From the main session the gesture
490
490
  // stays inert when there are no subagents (requireContent); the explicit
491
- // hub key still opens the empty roster.
491
+ // hub key still opens the empty roster. `armCloseTap` hands this gesture's
492
+ // tap state to the hub so the same ←← that opened it also arms its close —
493
+ // otherwise the hub's fresh detector demands a second ←← (issue #4780).
492
494
  this.ctx.editor.onLeftAtStart = () => {
493
495
  if (this.ctx.focusedAgentId) {
494
496
  this.#handleFocusedLeftTap();
495
497
  return;
496
498
  }
497
499
  if (this.#detectLeftDoubleTap()) {
498
- this.ctx.showAgentHub({ requireContent: true });
500
+ this.ctx.showAgentHub({ requireContent: true, armCloseTap: true });
499
501
  }
500
502
  };
501
503
 
@@ -14,6 +14,7 @@ import {
14
14
  fetchResourceMetadataScopes,
15
15
  loadAllMCPConfigs,
16
16
  MCPManager,
17
+ type OAuthEndpoints,
17
18
  } from "../../mcp";
18
19
  import { connectToServer, disconnectServer, listTools } from "../../mcp/client";
19
20
  import {
@@ -603,6 +604,7 @@ export class MCPCommandController {
603
604
  callbackPath: finalConfig.oauth?.callbackPath,
604
605
  redirectUri: finalConfig.oauth?.redirectUri,
605
606
  prompt: finalConfig.oauth?.prompt,
607
+ registrationUrl: oauth.registrationUrl,
606
608
  serverUrl: finalConfig.url,
607
609
  resource: oauthResource,
608
610
  stripSameOriginResource: oauthResourceIsFallback,
@@ -684,6 +686,7 @@ export class MCPCommandController {
684
686
  redirectUri?: string;
685
687
  prompt?: string;
686
688
  serverUrl?: string;
689
+ registrationUrl?: string;
687
690
  resource?: string;
688
691
  stripSameOriginResource?: boolean;
689
692
  /**
@@ -747,6 +750,7 @@ export class MCPCommandController {
747
750
  {
748
751
  authorizationUrl: authUrl,
749
752
  tokenUrl: tokenUrl,
753
+ registrationUrl: opts?.registrationUrl,
750
754
  clientId: resolvedClientId,
751
755
  clientSecret: resolvedClientSecret,
752
756
  scopes: scopes || undefined,
@@ -1038,13 +1042,7 @@ export class MCPCommandController {
1038
1042
  return next;
1039
1043
  }
1040
1044
 
1041
- async #resolveOAuthEndpointsFromServer(config: MCPServerConfig): Promise<{
1042
- authorizationUrl: string;
1043
- tokenUrl: string;
1044
- clientId?: string;
1045
- scopes?: string;
1046
- resource?: string;
1047
- }> {
1045
+ async #resolveOAuthEndpointsFromServer(config: MCPServerConfig): Promise<OAuthEndpoints> {
1048
1046
  // Stdio servers manage credentials inside the child process; OMP's OAuth
1049
1047
  // flow only applies to http/sse transports. Without this guard the
1050
1048
  // unauthenticated preflight below spawns the child, which happily reuses
@@ -1739,6 +1737,7 @@ export class MCPCommandController {
1739
1737
  callbackPath: found.config.oauth?.callbackPath,
1740
1738
  redirectUri: found.config.oauth?.redirectUri,
1741
1739
  prompt: found.config.oauth?.prompt,
1740
+ registrationUrl: oauth.registrationUrl,
1742
1741
  serverUrl,
1743
1742
  resource: oauthResource,
1744
1743
  stripSameOriginResource: oauthResourceIsFallback,
@@ -634,7 +634,8 @@ export class SelectorController {
634
634
  onPick: async (model, selector) => {
635
635
  try {
636
636
  // Session-only: update agent state but don't persist the model to settings.
637
- await this.ctx.session.setModelTemporary(model);
637
+ const roleThinkingLevel = this.ctx.session.resolveTemporaryModelThinkingLevel(model);
638
+ await this.ctx.session.setModelTemporary(model, roleThinkingLevel);
638
639
  this.ctx.statusLine.invalidate();
639
640
  this.ctx.updateEditorBorderColor();
640
641
  const roleSelectorHint = this.ctx.keybindings.getKeys("app.model.select")[0] ?? "Alt+M";
@@ -718,7 +719,7 @@ export class SelectorController {
718
719
  if (role === "default") {
719
720
  const { switched } = await this.ctx.session.setModel(model, role, {
720
721
  selector,
721
- thinkingLevel: concreteThinking,
722
+ thinkingLevel: isAuto ? ThinkingLevel.Inherit : concreteThinking,
722
723
  persist: true,
723
724
  currentContextTokens,
724
725
  });
@@ -777,6 +778,7 @@ export class SelectorController {
777
778
  this.ctx.showError(error instanceof Error ? error.message : String(error));
778
779
  }
779
780
  },
781
+
780
782
  onLoginRequest: providerId => {
781
783
  done();
782
784
  void this.#loginThenReopenModelHub(providerId);
@@ -1291,7 +1293,7 @@ export class SelectorController {
1291
1293
  this.ctx.ui.setFocus(dialog);
1292
1294
  this.ctx.ui.requestRender();
1293
1295
  try {
1294
- await this.ctx.session.modelRegistry.authStorage.login(providerId as OAuthProvider, {
1296
+ const identity = await this.ctx.session.modelRegistry.authStorage.login(providerId as OAuthProvider, {
1295
1297
  signal: dialog.signal,
1296
1298
  onAuth: (info: { url: string; launchUrl?: string; instructions?: string }) => {
1297
1299
  // The dialog renders the full URL (SSH-safe copy target) and
@@ -1310,8 +1312,18 @@ export class SelectorController {
1310
1312
  });
1311
1313
  this.ctx.session.modelRegistry.refreshInBackground();
1312
1314
  const block = new TranscriptBlock();
1315
+ // Name the account (and Anthropic organization) that was stored so a
1316
+ // login that lands on an unintended account/subscription is visible
1317
+ // immediately instead of silently replacing an existing registration.
1318
+ const whoBase = identity?.type === "oauth" ? (identity.email ?? identity.accountId) : undefined;
1319
+ const whoOrg = identity?.type === "oauth" ? (identity.orgName ?? identity.orgId) : undefined;
1320
+ const who = whoBase ? ` as ${whoBase}${whoOrg ? ` (${whoOrg})` : ""}` : whoOrg ? ` as ${whoOrg}` : "";
1313
1321
  block.addChild(
1314
- new Text(theme.fg("success", `${theme.status.success} Successfully logged in to ${providerId}`), 1, 0),
1322
+ new Text(
1323
+ theme.fg("success", `${theme.status.success} Successfully logged in to ${providerId}${who}`),
1324
+ 1,
1325
+ 0,
1326
+ ),
1315
1327
  );
1316
1328
  block.addChild(new Text(theme.fg("dim", `Credentials saved to ${getAgentDbPath()}`), 1, 0));
1317
1329
  this.ctx.present(block);
@@ -1532,7 +1544,10 @@ export class SelectorController {
1532
1544
  });
1533
1545
  }
1534
1546
 
1535
- showAgentHub(observers: SessionObserverRegistry, options?: { requireContent?: boolean }): void {
1547
+ showAgentHub(
1548
+ observers: SessionObserverRegistry,
1549
+ options?: { requireContent?: boolean; armCloseTap?: boolean },
1550
+ ): void {
1536
1551
  const hubKeys = [
1537
1552
  ...this.ctx.keybindings.getKeys("app.agents.hub"),
1538
1553
  ...this.ctx.keybindings.getKeys("app.session.observe"),
@@ -1587,6 +1602,10 @@ export class SelectorController {
1587
1602
  this.ctx.editorContainer.clear();
1588
1603
  this.ctx.editorContainer.addChild(hub);
1589
1604
  this.ctx.ui.setFocus(hub);
1605
+ // When the hub was raised by the editor's double-← gesture, prime its own
1606
+ // close detector so the *next* single ← dismisses it — the two taps that
1607
+ // opened it were consumed by the editor's detector (issue #4780).
1608
+ if (options?.armCloseTap) hub.armCloseTap();
1590
1609
  this.ctx.ui.requestRender();
1591
1610
  };
1592
1611