@oh-my-pi/pi-coding-agent 15.10.12 → 15.11.1

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 (158) hide show
  1. package/CHANGELOG.md +90 -4
  2. package/dist/cli.js +869 -825
  3. package/dist/types/async/index.d.ts +0 -1
  4. package/dist/types/capability/mcp.d.ts +1 -0
  5. package/dist/types/cli/gallery-fixtures/types.d.ts +5 -0
  6. package/dist/types/config/keybindings.d.ts +6 -1
  7. package/dist/types/config/settings-schema.d.ts +66 -34
  8. package/dist/types/export/html/template.generated.d.ts +1 -1
  9. package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
  10. package/dist/types/extensibility/shared-events.d.ts +2 -2
  11. package/dist/types/internal-urls/history-protocol.d.ts +14 -0
  12. package/dist/types/internal-urls/index.d.ts +1 -0
  13. package/dist/types/internal-urls/types.d.ts +1 -1
  14. package/dist/types/irc/bus.d.ts +66 -0
  15. package/dist/types/mcp/oauth-discovery.d.ts +2 -0
  16. package/dist/types/mcp/oauth-flow.d.ts +6 -1
  17. package/dist/types/mcp/transports/stdio.d.ts +1 -0
  18. package/dist/types/mcp/types.d.ts +2 -0
  19. package/dist/types/modes/components/agent-hub.d.ts +30 -0
  20. package/dist/types/modes/components/assistant-message.d.ts +1 -0
  21. package/dist/types/modes/components/compaction-summary-message.d.ts +10 -4
  22. package/dist/types/modes/components/custom-editor.d.ts +2 -0
  23. package/dist/types/modes/components/mcp-add-wizard.d.ts +2 -1
  24. package/dist/types/modes/components/settings-selector.d.ts +1 -0
  25. package/dist/types/modes/components/status-line/types.d.ts +3 -0
  26. package/dist/types/modes/components/tool-execution.d.ts +8 -0
  27. package/dist/types/modes/components/transcript-container.d.ts +3 -2
  28. package/dist/types/modes/components/ttsr-notification.d.ts +5 -1
  29. package/dist/types/modes/components/welcome.d.ts +3 -9
  30. package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
  31. package/dist/types/modes/controllers/tool-args-reveal.d.ts +43 -0
  32. package/dist/types/modes/interactive-mode.d.ts +3 -2
  33. package/dist/types/modes/theme/theme.d.ts +3 -1
  34. package/dist/types/modes/types.d.ts +3 -2
  35. package/dist/types/modes/utils/ui-helpers.d.ts +1 -1
  36. package/dist/types/registry/agent-lifecycle.d.ts +51 -0
  37. package/dist/types/registry/agent-registry.d.ts +16 -5
  38. package/dist/types/session/agent-session.d.ts +35 -30
  39. package/dist/types/session/messages.d.ts +2 -4
  40. package/dist/types/session/session-history-format.d.ts +12 -0
  41. package/dist/types/session/session-manager.d.ts +21 -3
  42. package/dist/types/session/streaming-output.d.ts +23 -0
  43. package/dist/types/task/executor.d.ts +11 -2
  44. package/dist/types/task/index.d.ts +11 -4
  45. package/dist/types/task/output-manager.d.ts +0 -7
  46. package/dist/types/task/repair-args.d.ts +8 -7
  47. package/dist/types/task/types.d.ts +55 -51
  48. package/dist/types/tools/browser/tab-worker.d.ts +3 -1
  49. package/dist/types/tools/find.d.ts +0 -11
  50. package/dist/types/tools/grouped-file-output.d.ts +0 -49
  51. package/dist/types/tools/index.d.ts +1 -3
  52. package/dist/types/tools/irc.d.ts +76 -38
  53. package/dist/types/tools/job.d.ts +7 -1
  54. package/dist/types/tools/render-utils.d.ts +22 -0
  55. package/examples/extensions/with-deps/package.json +1 -0
  56. package/package.json +11 -10
  57. package/scripts/bundle-dist.ts +28 -19
  58. package/src/async/index.ts +0 -1
  59. package/src/capability/mcp.ts +1 -0
  60. package/src/cli/gallery-cli.ts +6 -5
  61. package/src/cli/gallery-fixtures/agentic.ts +230 -115
  62. package/src/cli/gallery-fixtures/types.ts +5 -0
  63. package/src/cli.ts +20 -6
  64. package/src/commit/agentic/tools/analyze-file.ts +38 -19
  65. package/src/config/keybindings.ts +6 -1
  66. package/src/config/mcp-schema.json +4 -0
  67. package/src/config/settings-schema.ts +68 -41
  68. package/src/config/settings.ts +7 -0
  69. package/src/edit/renderer.ts +96 -46
  70. package/src/eval/__tests__/agent-bridge.test.ts +5 -3
  71. package/src/eval/agent-bridge.ts +3 -16
  72. package/src/eval/js/shared/prelude.txt +1 -1
  73. package/src/eval/py/prelude.py +5 -6
  74. package/src/export/html/template.generated.ts +1 -1
  75. package/src/export/html/template.js +44 -14
  76. package/src/extensibility/custom-tools/types.ts +2 -2
  77. package/src/extensibility/shared-events.ts +2 -2
  78. package/src/internal-urls/docs-index.generated.ts +9 -9
  79. package/src/internal-urls/history-protocol.ts +113 -0
  80. package/src/internal-urls/index.ts +1 -0
  81. package/src/internal-urls/router.ts +3 -1
  82. package/src/internal-urls/types.ts +1 -1
  83. package/src/irc/bus.ts +292 -0
  84. package/src/main.ts +8 -60
  85. package/src/mcp/manager.ts +3 -0
  86. package/src/mcp/oauth-discovery.ts +27 -2
  87. package/src/mcp/oauth-flow.ts +47 -1
  88. package/src/mcp/transports/stdio.ts +3 -0
  89. package/src/mcp/types.ts +2 -0
  90. package/src/modes/components/{session-observer-overlay.ts → agent-hub.ts} +586 -367
  91. package/src/modes/components/assistant-message.ts +15 -0
  92. package/src/modes/components/btw-panel.ts +5 -1
  93. package/src/modes/components/compaction-summary-message.ts +68 -32
  94. package/src/modes/components/custom-editor.ts +10 -0
  95. package/src/modes/components/mcp-add-wizard.ts +13 -0
  96. package/src/modes/components/settings-selector.ts +2 -0
  97. package/src/modes/components/status-line/component.ts +22 -12
  98. package/src/modes/components/status-line/types.ts +3 -0
  99. package/src/modes/components/tool-execution.ts +31 -1
  100. package/src/modes/components/transcript-container.ts +99 -18
  101. package/src/modes/components/tree-selector.ts +6 -1
  102. package/src/modes/components/ttsr-notification.ts +72 -30
  103. package/src/modes/components/welcome.ts +9 -33
  104. package/src/modes/controllers/event-controller.ts +93 -4
  105. package/src/modes/controllers/extension-ui-controller.ts +8 -8
  106. package/src/modes/controllers/input-controller.ts +18 -2
  107. package/src/modes/controllers/mcp-command-controller.ts +34 -2
  108. package/src/modes/controllers/selector-controller.ts +25 -17
  109. package/src/modes/controllers/tool-args-reveal.ts +174 -0
  110. package/src/modes/interactive-mode.ts +17 -15
  111. package/src/modes/theme/theme.ts +24 -5
  112. package/src/modes/types.ts +3 -5
  113. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  114. package/src/modes/utils/ui-helpers.ts +51 -49
  115. package/src/prompts/system/irc-incoming.md +3 -4
  116. package/src/prompts/system/orchestrate-notice.md +2 -2
  117. package/src/prompts/system/subagent-system-prompt.md +0 -5
  118. package/src/prompts/system/system-prompt.md +1 -0
  119. package/src/prompts/system/workflow-notice.md +2 -2
  120. package/src/prompts/tools/eval.md +3 -3
  121. package/src/prompts/tools/irc.md +29 -19
  122. package/src/prompts/tools/read.md +2 -2
  123. package/src/prompts/tools/task-summary.md +5 -16
  124. package/src/prompts/tools/task.md +43 -29
  125. package/src/registry/agent-lifecycle.ts +218 -0
  126. package/src/registry/agent-registry.ts +16 -5
  127. package/src/sdk.ts +29 -9
  128. package/src/session/agent-session.ts +268 -241
  129. package/src/session/messages.ts +11 -78
  130. package/src/session/session-history-format.ts +246 -0
  131. package/src/session/session-manager.ts +59 -5
  132. package/src/session/streaming-output.ts +60 -0
  133. package/src/task/executor.ts +855 -466
  134. package/src/task/index.ts +723 -794
  135. package/src/task/output-manager.ts +0 -11
  136. package/src/task/render.ts +142 -66
  137. package/src/task/repair-args.ts +21 -9
  138. package/src/task/types.ts +73 -66
  139. package/src/tools/ask.ts +4 -2
  140. package/src/tools/bash.ts +15 -5
  141. package/src/tools/browser/tab-worker.ts +26 -7
  142. package/src/tools/browser.ts +28 -1
  143. package/src/tools/find.ts +2 -27
  144. package/src/tools/grouped-file-output.ts +1 -118
  145. package/src/tools/index.ts +4 -12
  146. package/src/tools/irc.ts +596 -171
  147. package/src/tools/job.ts +41 -7
  148. package/src/tools/read.ts +57 -1
  149. package/src/tools/render-utils.ts +56 -0
  150. package/src/tools/renderers.ts +2 -0
  151. package/src/tools/resolve.ts +4 -1
  152. package/src/tools/write.ts +65 -47
  153. package/src/web/search/providers/anthropic.ts +29 -4
  154. package/dist/types/async/support.d.ts +0 -2
  155. package/dist/types/modes/components/session-observer-overlay.d.ts +0 -11
  156. package/dist/types/task/simple-mode.d.ts +0 -8
  157. package/src/async/support.ts +0 -5
  158. package/src/task/simple-mode.ts +0 -27
@@ -21,6 +21,7 @@ import { isSilentAbort, readPendingDisplayTag, resolveAbortLabel } from "../../s
21
21
  import type { ResolveToolDetails } from "../../tools/resolve";
22
22
  import { interruptHint } from "../shared";
23
23
  import { StreamingRevealController } from "./streaming-reveal";
24
+ import { ToolArgsRevealController } from "./tool-args-reveal";
24
25
 
25
26
  type AgentSessionEventKind = AgentSessionEvent["type"];
26
27
 
@@ -77,7 +78,16 @@ export class EventController {
77
78
  // Insertion-ordered IRC cards not yet retired; values are the transcript
78
79
  // components each card contributed (see #retireIrcCard for the guard).
79
80
  #liveIrcCards = new Map<string, Component[]>();
81
+ // Most recent `job` tool block whose result still had every watched job
82
+ // running. Kept un-finalized (live) so the next `job` call displaces it —
83
+ // one persistent poll instead of a stack of "waiting on N jobs" frames —
84
+ // and sealed in place the moment anything else lands below it.
85
+ #displaceablePollComponent: ToolExecutionComponent | undefined = undefined;
86
+ // Most recent TTSR notification block. A new ttsr_triggered event merges its
87
+ // rules into this block while it is still the (live-region) transcript tail.
88
+ #lastTtsrNotification: TtsrNotificationComponent | undefined = undefined;
80
89
  #streamingReveal: StreamingRevealController;
90
+ #toolArgsReveal: ToolArgsRevealController;
81
91
  #handlers: AgentSessionEventHandlers;
82
92
 
83
93
  constructor(private ctx: InteractiveModeContext) {
@@ -86,6 +96,10 @@ export class EventController {
86
96
  getHideThinkingBlock: () => this.ctx.hideThinkingBlock,
87
97
  requestRender: () => this.ctx.ui.requestRender(),
88
98
  });
99
+ this.#toolArgsReveal = new ToolArgsRevealController({
100
+ getSmoothStreaming: () => this.ctx.settings.get("display.smoothStreaming"),
101
+ requestRender: () => this.ctx.ui.requestRender(),
102
+ });
89
103
  this.#handlers = {
90
104
  agent_start: e => this.#handleAgentStart(e),
91
105
  agent_end: e => this.#handleAgentEnd(e),
@@ -119,6 +133,7 @@ export class EventController {
119
133
 
120
134
  dispose(): void {
121
135
  this.#streamingReveal.stop();
136
+ this.#toolArgsReveal.stop();
122
137
  this.#cancelIdleCompaction();
123
138
  for (const timer of this.#ircExpiryTimers.values()) {
124
139
  clearTimeout(timer);
@@ -282,6 +297,7 @@ export class EventController {
282
297
  const signature = `${textContent}\u0000${imageCount}`;
283
298
 
284
299
  this.#resetReadGroup();
300
+ this.#resolveDisplaceablePoll();
285
301
  const wasOptimistic = this.ctx.optimisticUserMessageSignature === signature;
286
302
  const wasLocallySubmitted = this.ctx.locallySubmittedUserSignatures.delete(signature) || wasOptimistic;
287
303
  if (!wasOptimistic) {
@@ -389,6 +405,28 @@ export class EventController {
389
405
  }
390
406
  }
391
407
 
408
+ /**
409
+ * Resolve the pending displaceable poll block before the next block lands.
410
+ * A follow-up `job` call displaces it — the stale "waiting on N jobs" frame
411
+ * is removed so repeated polls read as one persistent poll — while anything
412
+ * else seals it in place as final history. Removal is safe only because a
413
+ * displaceable block never finalizes: commits stop at the first live block,
414
+ * so none of its rows have entered native scrollback (see
415
+ * ToolExecutionComponent.isDisplaceableBlock).
416
+ */
417
+ #resolveDisplaceablePoll(nextToolName?: string): void {
418
+ const previous = this.#displaceablePollComponent;
419
+ if (!previous) return;
420
+ this.#displaceablePollComponent = undefined;
421
+ if (nextToolName === "job" && previous.isDisplaceableBlock()) {
422
+ this.ctx.chatContainer.removeChild(previous);
423
+ }
424
+ // Sealing stops the waiting-poll spinner and freezes the block (for a
425
+ // just-removed component it only clears the animation timer).
426
+ previous.seal();
427
+ this.ctx.ui.requestRender();
428
+ }
429
+
392
430
  async #handleNotice(event: Extract<AgentSessionEvent, { type: "notice" }>): Promise<void> {
393
431
  const message = event.source ? `${event.source}: ${event.message}` : event.message;
394
432
  if (event.level === "error") {
@@ -444,6 +482,7 @@ export class EventController {
444
482
  continue;
445
483
  }
446
484
  if (!readArgsTargetInternalUrl(content.arguments)) {
485
+ if (!this.ctx.pendingTools.has(content.id)) this.#resolveDisplaceablePoll(content.name);
447
486
  this.#trackReadToolCall(content.id, content.arguments);
448
487
  const component = this.ctx.pendingTools.get(content.id);
449
488
  if (component) {
@@ -460,11 +499,25 @@ export class EventController {
460
499
 
461
500
  // Preserve the raw partial JSON for renderers that need to surface fields before the JSON object closes.
462
501
  // Bash uses this to show inline env assignments during streaming instead of popping them in at completion.
463
- const renderArgs =
464
- "partialJson" in content
465
- ? { ...content.arguments, __partialJson: content.partialJson }
466
- : content.arguments;
502
+ // While the JSON is still open, ToolArgsRevealController paces the
503
+ // reveal (write/edit/bash previews grow smoothly when a slow provider
504
+ // delivers large batches); once it closes, the final args render
505
+ // as-is — mirroring how assistant text snaps at message_end.
506
+ const partialJson = "partialJson" in content ? content.partialJson : undefined;
507
+ let renderArgs: Record<string, unknown>;
508
+ if (typeof partialJson === "string") {
509
+ renderArgs = this.#toolArgsReveal.setTarget(
510
+ content.id,
511
+ partialJson,
512
+ content.customWireName !== undefined,
513
+ content.arguments,
514
+ );
515
+ } else {
516
+ this.#toolArgsReveal.finish(content.id);
517
+ renderArgs = content.arguments;
518
+ }
467
519
  if (!this.ctx.pendingTools.has(content.id)) {
520
+ this.#resolveDisplaceablePoll(content.name);
468
521
  this.#resetReadGroup();
469
522
  const tool = this.ctx.session.getToolByName(content.name);
470
523
  const component = new ToolExecutionComponent(
@@ -484,10 +537,12 @@ export class EventController {
484
537
  component.setExpanded(this.ctx.toolOutputExpanded);
485
538
  this.ctx.chatContainer.addChild(component);
486
539
  this.ctx.pendingTools.set(content.id, component);
540
+ this.#toolArgsReveal.bind(content.id, component);
487
541
  } else {
488
542
  const component = this.ctx.pendingTools.get(content.id);
489
543
  if (component) {
490
544
  component.updateArgs(renderArgs, content.id);
545
+ this.#toolArgsReveal.bind(content.id, component);
491
546
  }
492
547
  }
493
548
  }
@@ -522,6 +577,7 @@ export class EventController {
522
577
  if (this.ctx.streamingComponent && event.message.role === "assistant") {
523
578
  this.ctx.streamingMessage = event.message;
524
579
  this.#streamingReveal.stop();
580
+ this.#toolArgsReveal.flushAll();
525
581
  let errorMessage: string | undefined;
526
582
  const aborted = this.ctx.streamingMessage.stopReason === "aborted";
527
583
  const silentlyAborted = aborted && isSilentAbort(this.ctx.streamingMessage.errorMessage);
@@ -561,6 +617,9 @@ export class EventController {
561
617
  component.seal();
562
618
  }
563
619
  }
620
+ // These calls will never produce a result either, so the tracked
621
+ // waiting poll cannot be displaced anymore — freeze it in place.
622
+ this.#resolveDisplaceablePoll();
564
623
  }
565
624
  this.#lastAssistantComponent = this.ctx.streamingComponent;
566
625
  this.#lastAssistantComponent.setUsageInfo(event.message.usage);
@@ -589,6 +648,7 @@ export class EventController {
589
648
  async #handleToolExecutionStart(event: Extract<AgentSessionEvent, { type: "tool_execution_start" }>): Promise<void> {
590
649
  this.#updateWorkingMessageFromIntent(event.intent);
591
650
  if (!this.ctx.pendingTools.has(event.toolCallId)) {
651
+ this.#resolveDisplaceablePoll(event.toolName);
592
652
  if (event.toolName === "read" && readArgsHaveTarget(event.args) && !readArgsTargetInternalUrl(event.args)) {
593
653
  this.#trackReadToolCall(event.toolCallId, event.args);
594
654
  const component = this.ctx.pendingTools.get(event.toolCallId);
@@ -697,6 +757,14 @@ export class EventController {
697
757
  this.ctx.pendingTools.delete(event.toolCallId);
698
758
  this.#backgroundToolCallIds.delete(event.toolCallId);
699
759
  }
760
+ if (
761
+ event.toolName === "job" &&
762
+ component instanceof ToolExecutionComponent &&
763
+ component.isDisplaceableBlock()
764
+ ) {
765
+ // Remember the waiting poll so the next `job` call can displace it.
766
+ this.#displaceablePollComponent = component;
767
+ }
700
768
  this.ctx.ui.requestRender();
701
769
  }
702
770
  }
@@ -727,6 +795,7 @@ export class EventController {
727
795
  async #handleAgentEnd(_event: Extract<AgentSessionEvent, { type: "agent_end" }>): Promise<void> {
728
796
  this.#agentTurnActive = false;
729
797
  this.#streamingReveal.stop();
798
+ this.#toolArgsReveal.flushAll();
730
799
  if (this.ctx.loadingAnimation) {
731
800
  this.ctx.loadingAnimation.stop();
732
801
  this.ctx.loadingAnimation = undefined;
@@ -759,6 +828,9 @@ export class EventController {
759
828
  this.#readToolCallArgs.clear();
760
829
  this.#readToolCallAssistantComponents.clear();
761
830
  this.#resetReadGroup();
831
+ // The turn is over: nothing else lands this turn, so the waiting poll is
832
+ // final history — seal it instead of letting its spinner tick while idle.
833
+ this.#resolveDisplaceablePoll();
762
834
  this.#lastAssistantComponent = undefined;
763
835
  this.ctx.ui.requestRender();
764
836
  this.#scheduleIdleCompaction();
@@ -908,9 +980,26 @@ export class EventController {
908
980
  }
909
981
 
910
982
  async #handleTtsrTriggered(event: Extract<AgentSessionEvent, { type: "ttsr_triggered" }>): Promise<void> {
983
+ // Consecutive notifications (e.g. per-tool matches from one assistant
984
+ // message) merge into the previous block instead of stacking. Mutating an
985
+ // existing block is only safe while it sits inside the live region — a
986
+ // still-mutating block above it means none of its rows have been committed
987
+ // to native scrollback yet (commits are prefix-only and stop at the first
988
+ // live block), so the grown block still repaints.
989
+ const previous = this.#lastTtsrNotification;
990
+ if (
991
+ previous &&
992
+ this.ctx.chatContainer.children.at(-1) === previous &&
993
+ this.ctx.chatContainer.isWithinLiveRegion(previous)
994
+ ) {
995
+ previous.addRules(event.rules);
996
+ this.ctx.ui.requestRender();
997
+ return;
998
+ }
911
999
  const component = new TtsrNotificationComponent(event.rules);
912
1000
  component.setExpanded(this.ctx.toolOutputExpanded);
913
1001
  this.ctx.present(component);
1002
+ this.#lastTtsrNotification = component;
914
1003
  }
915
1004
 
916
1005
  async #handleTodoReminder(event: Extract<AgentSessionEvent, { type: "todo_reminder" }>): Promise<void> {
@@ -140,7 +140,7 @@ export class ExtensionUiController {
140
140
  reload: async () => {
141
141
  await this.ctx.session.reload();
142
142
  this.ctx.chatContainer.clear();
143
- this.ctx.renderInitialMessages(undefined, { clearTerminalHistory: true });
143
+ this.ctx.renderInitialMessages({ clearTerminalHistory: true });
144
144
  await this.ctx.reloadTodos();
145
145
  this.ctx.showStatus("Reloaded session");
146
146
  },
@@ -197,7 +197,7 @@ export class ExtensionUiController {
197
197
 
198
198
  // Update UI
199
199
  this.ctx.chatContainer.clear();
200
- this.ctx.renderInitialMessages(undefined, { clearTerminalHistory: true });
200
+ this.ctx.renderInitialMessages({ clearTerminalHistory: true });
201
201
  await this.ctx.reloadTodos();
202
202
  this.ctx.editor.setText(result.selectedText);
203
203
  this.ctx.showStatus("Branched to new session");
@@ -212,7 +212,7 @@ export class ExtensionUiController {
212
212
 
213
213
  // Update UI
214
214
  this.ctx.chatContainer.clear();
215
- this.ctx.renderInitialMessages(undefined, { clearTerminalHistory: true });
215
+ this.ctx.renderInitialMessages({ clearTerminalHistory: true });
216
216
  await this.ctx.reloadTodos();
217
217
  if (result.editorText && !this.ctx.editor.getText().trim()) {
218
218
  this.ctx.editor.setText(result.editorText);
@@ -230,7 +230,7 @@ export class ExtensionUiController {
230
230
  }
231
231
  setSessionTerminalTitle(this.ctx.sessionManager.getSessionName(), this.ctx.sessionManager.getCwd());
232
232
  this.ctx.chatContainer.clear();
233
- this.ctx.renderInitialMessages(undefined, { clearTerminalHistory: true });
233
+ this.ctx.renderInitialMessages({ clearTerminalHistory: true });
234
234
  await this.ctx.reloadTodos();
235
235
  return { cancelled: false };
236
236
  },
@@ -376,7 +376,7 @@ export class ExtensionUiController {
376
376
  reload: async () => {
377
377
  await this.ctx.session.reload();
378
378
  this.ctx.chatContainer.clear();
379
- this.ctx.renderInitialMessages(undefined, { clearTerminalHistory: true });
379
+ this.ctx.renderInitialMessages({ clearTerminalHistory: true });
380
380
  await this.ctx.reloadTodos();
381
381
  this.ctx.showStatus("Reloaded session");
382
382
  },
@@ -426,7 +426,7 @@ export class ExtensionUiController {
426
426
 
427
427
  // Update UI
428
428
  this.ctx.chatContainer.clear();
429
- this.ctx.renderInitialMessages(undefined, { clearTerminalHistory: true });
429
+ this.ctx.renderInitialMessages({ clearTerminalHistory: true });
430
430
  await this.ctx.reloadTodos();
431
431
  this.ctx.editor.setText(result.selectedText);
432
432
  this.ctx.showStatus("Branched to new session");
@@ -441,7 +441,7 @@ export class ExtensionUiController {
441
441
 
442
442
  // Update UI
443
443
  this.ctx.chatContainer.clear();
444
- this.ctx.renderInitialMessages(undefined, { clearTerminalHistory: true });
444
+ this.ctx.renderInitialMessages({ clearTerminalHistory: true });
445
445
  await this.ctx.reloadTodos();
446
446
  if (result.editorText && !this.ctx.editor.getText().trim()) {
447
447
  this.ctx.editor.setText(result.editorText);
@@ -458,7 +458,7 @@ export class ExtensionUiController {
458
458
  return { cancelled: true };
459
459
  }
460
460
  this.ctx.chatContainer.clear();
461
- this.ctx.renderInitialMessages(undefined, { clearTerminalHistory: true });
461
+ this.ctx.renderInitialMessages({ clearTerminalHistory: true });
462
462
  await this.ctx.reloadTodos();
463
463
  return { cancelled: false };
464
464
  },
@@ -235,10 +235,26 @@ export class InputController {
235
235
  for (const key of this.ctx.keybindings.getKeys("app.clipboard.copyLine")) {
236
236
  this.ctx.editor.setCustomKeyHandler(key, () => this.handleCopyCurrentLine());
237
237
  }
238
- for (const key of this.ctx.keybindings.getKeys("app.session.observe")) {
239
- this.ctx.editor.setCustomKeyHandler(key, () => this.ctx.showSessionObserver());
238
+ const hubKeys = new Set([
239
+ ...this.ctx.keybindings.getKeys("app.agents.hub"),
240
+ ...this.ctx.keybindings.getKeys("app.session.observe"),
241
+ ]);
242
+ for (const key of hubKeys) {
243
+ this.ctx.editor.setCustomKeyHandler(key, () => this.ctx.showAgentHub());
240
244
  }
241
245
 
246
+ // Double-tap left arrow on an empty editor opens the agent hub — same
247
+ // 500ms window as the double-escape state machine above.
248
+ this.ctx.editor.onLeftAtStart = () => {
249
+ const now = Date.now();
250
+ if (now - this.ctx.lastLeftTapTime < 500) {
251
+ this.ctx.lastLeftTapTime = 0;
252
+ this.ctx.showAgentHub();
253
+ } else {
254
+ this.ctx.lastLeftTapTime = now;
255
+ }
256
+ };
257
+
242
258
  this.#setupEnhancedPaste();
243
259
 
244
260
  this.ctx.editor.onChange = (text: string) => {
@@ -127,6 +127,7 @@ interface OAuthFlowResult {
127
127
  credentialId: string;
128
128
  clientId?: string;
129
129
  clientSecret?: string;
130
+ resource?: string;
130
131
  }
131
132
 
132
133
  type MCPAddScope = "user" | "project";
@@ -490,6 +491,7 @@ export class MCPCommandController {
490
491
 
491
492
  try {
492
493
  const oauthClientSecret = finalConfig.oauth?.clientSecret ?? "";
494
+ const oauthResource = oauth.resource ?? finalConfig.url;
493
495
  const oauthResult = await this.#handleOAuthFlow(
494
496
  oauth.authorizationUrl,
495
497
  oauth.tokenUrl,
@@ -499,15 +501,18 @@ export class MCPCommandController {
499
501
  finalConfig.oauth?.callbackPort,
500
502
  finalConfig.oauth?.callbackPath,
501
503
  finalConfig.oauth?.redirectUri,
504
+ oauthResource,
502
505
  );
503
506
  const persistedClientId = oauthResult.clientId ?? oauth.clientId ?? finalConfig.oauth?.clientId;
504
507
  const persistedClientSecret = oauthResult.clientSecret ?? finalConfig.oauth?.clientSecret;
508
+ const persistedResource = oauthResult.resource ?? oauthResource;
505
509
  finalConfig = {
506
510
  ...finalConfig,
507
511
  auth: {
508
512
  type: "oauth",
509
513
  credentialId: oauthResult.credentialId,
510
514
  tokenUrl: oauth.tokenUrl,
515
+ resource: persistedResource,
511
516
  clientId: persistedClientId,
512
517
  clientSecret: persistedClientSecret,
513
518
  },
@@ -548,8 +553,25 @@ export class MCPCommandController {
548
553
  done();
549
554
  this.#handleWizardCancel();
550
555
  },
551
- async (authUrl: string, tokenUrl: string, clientId: string, clientSecret: string, scopes: string) => {
552
- return await this.#handleOAuthFlow(authUrl, tokenUrl, clientId, clientSecret, scopes);
556
+ async (
557
+ authUrl: string,
558
+ tokenUrl: string,
559
+ clientId: string,
560
+ clientSecret: string,
561
+ scopes: string,
562
+ resource?: string,
563
+ ) => {
564
+ return await this.#handleOAuthFlow(
565
+ authUrl,
566
+ tokenUrl,
567
+ clientId,
568
+ clientSecret,
569
+ scopes,
570
+ undefined,
571
+ undefined,
572
+ undefined,
573
+ resource,
574
+ );
553
575
  },
554
576
  async (config: MCPServerConfig) => {
555
577
  return await this.#handleTestConnection(config);
@@ -579,6 +601,7 @@ export class MCPCommandController {
579
601
  callbackPort?: number,
580
602
  callbackPath?: string,
581
603
  redirectUri?: string,
604
+ resource?: string,
582
605
  ): Promise<OAuthFlowResult> {
583
606
  const authStorage = this.ctx.session.modelRegistry.authStorage;
584
607
  let parsedAuthUrl: URL;
@@ -617,6 +640,7 @@ export class MCPCommandController {
617
640
  redirectUri,
618
641
  callbackPort,
619
642
  callbackPath,
643
+ resource,
620
644
  },
621
645
  {
622
646
  onAuth: (info: { url: string; instructions?: string }) => {
@@ -704,6 +728,7 @@ export class MCPCommandController {
704
728
  credentialId,
705
729
  clientId: flow.resolvedClientId,
706
730
  clientSecret: flow.registeredClientSecret,
731
+ resource: flow.resource,
707
732
  };
708
733
  } catch (error) {
709
734
  const errorMsg = error instanceof Error ? error.message : String(error);
@@ -804,6 +829,7 @@ export class MCPCommandController {
804
829
  tokenUrl: string;
805
830
  clientId?: string;
806
831
  scopes?: string;
832
+ resource?: string;
807
833
  }> {
808
834
  // First test if server actually needs auth by connecting without OAuth
809
835
  let connectionSucceeded = false;
@@ -1415,6 +1441,9 @@ export class MCPCommandController {
1415
1441
 
1416
1442
  this.#showMessage(["", theme.fg("muted", `Reauthorizing "${name}"...`), ""].join("\n"));
1417
1443
 
1444
+ const oauthResource =
1445
+ oauth.resource ?? currentAuth?.resource ?? ("url" in baseConfig ? baseConfig.url : undefined);
1446
+
1418
1447
  const oauthResult = await this.#handleOAuthFlow(
1419
1448
  oauth.authorizationUrl,
1420
1449
  oauth.tokenUrl,
@@ -1424,10 +1453,12 @@ export class MCPCommandController {
1424
1453
  found.config.oauth?.callbackPort,
1425
1454
  found.config.oauth?.callbackPath,
1426
1455
  found.config.oauth?.redirectUri,
1456
+ oauthResource,
1427
1457
  );
1428
1458
 
1429
1459
  const persistedClientId = oauthResult.clientId ?? oauth.clientId ?? found.config.oauth?.clientId;
1430
1460
  const persistedClientSecret = oauthResult.clientSecret ?? (oauthClientSecret || undefined);
1461
+ const persistedResource = oauthResult.resource ?? oauthResource;
1431
1462
 
1432
1463
  const updated: MCPServerConfig = {
1433
1464
  ...baseConfig,
@@ -1435,6 +1466,7 @@ export class MCPCommandController {
1435
1466
  type: "oauth",
1436
1467
  credentialId: oauthResult.credentialId,
1437
1468
  tokenUrl: oauth.tokenUrl,
1469
+ resource: persistedResource,
1438
1470
  clientId: persistedClientId,
1439
1471
  clientSecret: persistedClientSecret,
1440
1472
  },
@@ -40,6 +40,7 @@ import { shortenPath } from "../../tools/render-utils";
40
40
  import { copyToClipboard } from "../../utils/clipboard";
41
41
  import { setSessionTerminalTitle } from "../../utils/title-generator";
42
42
  import { AgentDashboard } from "../components/agent-dashboard";
43
+ import { AgentHubOverlayComponent } from "../components/agent-hub";
43
44
  import { AssistantMessageComponent } from "../components/assistant-message";
44
45
  import { CopySelectorComponent } from "../components/copy-selector";
45
46
  import { ExtensionDashboard } from "../components/extensions";
@@ -47,7 +48,6 @@ import { HistorySearchComponent } from "../components/history-search";
47
48
  import { ModelSelectorComponent } from "../components/model-selector";
48
49
  import { OAuthSelectorComponent } from "../components/oauth-selector";
49
50
  import { PluginSelectorComponent } from "../components/plugin-selector";
50
- import { SessionObserverOverlayComponent } from "../components/session-observer-overlay";
51
51
  import { SessionSelectorComponent } from "../components/session-selector";
52
52
  import { SettingsSelectorComponent } from "../components/settings-selector";
53
53
  import { ToolExecutionComponent } from "../components/tool-execution";
@@ -120,6 +120,7 @@ export class SelectorController {
120
120
  separator: settings.get("statusLine.separator"),
121
121
  showHookStatus: settings.get("statusLine.showHookStatus"),
122
122
  sessionAccent: settings.get("statusLine.sessionAccent"),
123
+ transparent: settings.get("statusLine.transparent"),
123
124
  ...previewSettings,
124
125
  });
125
126
  this.ctx.updateEditorTopBorder();
@@ -147,6 +148,7 @@ export class SelectorController {
147
148
  separator: settings.get("statusLine.separator"),
148
149
  showHookStatus: settings.get("statusLine.showHookStatus"),
149
150
  sessionAccent: settings.get("statusLine.sessionAccent"),
151
+ transparent: settings.get("statusLine.transparent"),
150
152
  });
151
153
  this.ctx.updateEditorTopBorder();
152
154
  this.ctx.ui.requestRender();
@@ -351,6 +353,7 @@ export class SelectorController {
351
353
  case "statusLineShowHooks":
352
354
  case "statusLine.showHookStatus":
353
355
  case "statusLine.sessionAccent":
356
+ case "statusLine.transparent":
354
357
  case "statusLineSegments":
355
358
  case "statusLineModelThinking":
356
359
  case "statusLinePathAbbreviate":
@@ -369,6 +372,7 @@ export class SelectorController {
369
372
  separator: settings.get("statusLine.separator"),
370
373
  showHookStatus: settings.get("statusLine.showHookStatus"),
371
374
  sessionAccent: settings.get("statusLine.sessionAccent"),
375
+ transparent: settings.get("statusLine.transparent"),
372
376
  segmentOptions: settings.get("statusLine.segmentOptions"),
373
377
  };
374
378
  this.ctx.statusLine.updateSettings(statusLineSettings);
@@ -578,7 +582,7 @@ export class SelectorController {
578
582
  }
579
583
 
580
584
  this.ctx.chatContainer.clear();
581
- this.ctx.renderInitialMessages(undefined, { clearTerminalHistory: true });
585
+ this.ctx.renderInitialMessages({ clearTerminalHistory: true });
582
586
  this.ctx.editor.setText(result.selectedText);
583
587
  done();
584
588
  this.ctx.showStatus("Branched to new session");
@@ -719,9 +723,10 @@ export class SelectorController {
719
723
  return;
720
724
  }
721
725
 
722
- // Update UI — pass the context built by navigateTree to skip a second O(N) walk.
726
+ // Update UI — rebuild the display transcript for the new leaf (the
727
+ // context from navigateTree is the LLM context, not the transcript).
723
728
  this.ctx.chatContainer.clear();
724
- this.ctx.renderInitialMessages(result.sessionContext, { clearTerminalHistory: true });
729
+ this.ctx.renderInitialMessages({ clearTerminalHistory: true });
725
730
  await this.ctx.reloadTodos();
726
731
  if (result.editorText && !this.ctx.editor.getText().trim()) {
727
732
  this.ctx.editor.setText(result.editorText);
@@ -846,7 +851,7 @@ export class SelectorController {
846
851
  this.ctx.statusLine.setSessionStartTime(Date.now());
847
852
  this.ctx.updateEditorTopBorder();
848
853
  this.ctx.updateEditorBorderColor();
849
- this.ctx.renderInitialMessages(undefined, { clearTerminalHistory: true });
854
+ this.ctx.renderInitialMessages({ clearTerminalHistory: true });
850
855
  await this.ctx.reloadTodos();
851
856
  this.ctx.ui.requestRender(true, { clearScrollback: true });
852
857
  return true;
@@ -871,7 +876,7 @@ export class SelectorController {
871
876
 
872
877
  // Clear and re-render the chat
873
878
  this.ctx.chatContainer.clear();
874
- this.ctx.renderInitialMessages(undefined, { clearTerminalHistory: true });
879
+ this.ctx.renderInitialMessages({ clearTerminalHistory: true });
875
880
  await this.ctx.reloadTodos();
876
881
  this.ctx.showStatus(movedProject ? `Resumed session in ${shortenPath(newCwd)}` : "Resumed session");
877
882
  }
@@ -1074,31 +1079,34 @@ export class SelectorController {
1074
1079
  });
1075
1080
  }
1076
1081
 
1077
- showSessionObserver(registry: SessionObserverRegistry): void {
1078
- const observeKeys = this.ctx.keybindings.getKeys("app.session.observe");
1079
- let cleanup: (() => void) | undefined;
1082
+ showAgentHub(observers: SessionObserverRegistry): void {
1083
+ const hubKeys = [
1084
+ ...this.ctx.keybindings.getKeys("app.agents.hub"),
1085
+ ...this.ctx.keybindings.getKeys("app.session.observe"),
1086
+ ];
1087
+ let hub: AgentHubOverlayComponent | undefined;
1080
1088
  let overlayHandle: OverlayHandle | undefined;
1081
1089
 
1082
1090
  const done = () => {
1083
- cleanup?.();
1091
+ hub?.dispose();
1084
1092
  overlayHandle?.hide();
1085
1093
  this.ctx.ui.requestRender();
1086
1094
  };
1087
1095
 
1088
- const selector = new SessionObserverOverlayComponent(registry, done, observeKeys);
1089
-
1090
- cleanup = registry.onChange(() => {
1091
- selector.refreshFromRegistry();
1092
- this.ctx.ui.requestRender();
1096
+ hub = new AgentHubOverlayComponent({
1097
+ observers,
1098
+ hubKeys,
1099
+ onDone: done,
1100
+ requestRender: () => this.ctx.ui.requestRender(),
1093
1101
  });
1094
1102
 
1095
- overlayHandle = this.ctx.ui.showOverlay(selector, {
1103
+ overlayHandle = this.ctx.ui.showOverlay(hub, {
1096
1104
  anchor: "bottom-center",
1097
1105
  width: "100%",
1098
1106
  maxHeight: "100%",
1099
1107
  margin: 0,
1100
1108
  });
1101
- this.ctx.ui.setFocus(selector);
1109
+ this.ctx.ui.setFocus(hub);
1102
1110
  this.ctx.ui.requestRender();
1103
1111
  }
1104
1112
  }