@oh-my-pi/pi-coding-agent 15.10.12 → 15.11.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 (125) hide show
  1. package/CHANGELOG.md +60 -3
  2. package/dist/cli.js +841 -803
  3. package/dist/types/async/index.d.ts +0 -1
  4. package/dist/types/cli/gallery-fixtures/types.d.ts +5 -0
  5. package/dist/types/config/keybindings.d.ts +6 -1
  6. package/dist/types/config/settings-schema.d.ts +56 -33
  7. package/dist/types/export/html/template.generated.d.ts +1 -1
  8. package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
  9. package/dist/types/extensibility/shared-events.d.ts +2 -2
  10. package/dist/types/internal-urls/history-protocol.d.ts +14 -0
  11. package/dist/types/internal-urls/index.d.ts +1 -0
  12. package/dist/types/internal-urls/types.d.ts +1 -1
  13. package/dist/types/irc/bus.d.ts +66 -0
  14. package/dist/types/modes/components/agent-hub.d.ts +30 -0
  15. package/dist/types/modes/components/compaction-summary-message.d.ts +10 -4
  16. package/dist/types/modes/components/custom-editor.d.ts +2 -0
  17. package/dist/types/modes/components/tool-execution.d.ts +8 -0
  18. package/dist/types/modes/components/ttsr-notification.d.ts +5 -1
  19. package/dist/types/modes/components/welcome.d.ts +3 -9
  20. package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
  21. package/dist/types/modes/interactive-mode.d.ts +3 -2
  22. package/dist/types/modes/theme/theme.d.ts +2 -1
  23. package/dist/types/modes/types.d.ts +3 -2
  24. package/dist/types/modes/utils/ui-helpers.d.ts +1 -1
  25. package/dist/types/registry/agent-lifecycle.d.ts +51 -0
  26. package/dist/types/registry/agent-registry.d.ts +16 -5
  27. package/dist/types/session/agent-session.d.ts +35 -30
  28. package/dist/types/session/messages.d.ts +2 -4
  29. package/dist/types/session/session-history-format.d.ts +12 -0
  30. package/dist/types/session/session-manager.d.ts +21 -3
  31. package/dist/types/session/streaming-output.d.ts +23 -0
  32. package/dist/types/task/executor.d.ts +11 -2
  33. package/dist/types/task/index.d.ts +11 -4
  34. package/dist/types/task/output-manager.d.ts +0 -7
  35. package/dist/types/task/repair-args.d.ts +8 -7
  36. package/dist/types/task/types.d.ts +55 -51
  37. package/dist/types/tools/browser/tab-worker.d.ts +3 -1
  38. package/dist/types/tools/find.d.ts +0 -11
  39. package/dist/types/tools/grouped-file-output.d.ts +0 -49
  40. package/dist/types/tools/index.d.ts +1 -3
  41. package/dist/types/tools/irc.d.ts +76 -38
  42. package/dist/types/tools/job.d.ts +7 -1
  43. package/examples/extensions/with-deps/package.json +1 -0
  44. package/package.json +11 -10
  45. package/scripts/bundle-dist.ts +28 -19
  46. package/src/async/index.ts +0 -1
  47. package/src/cli/gallery-cli.ts +1 -1
  48. package/src/cli/gallery-fixtures/agentic.ts +230 -115
  49. package/src/cli/gallery-fixtures/types.ts +5 -0
  50. package/src/cli.ts +20 -6
  51. package/src/commit/agentic/tools/analyze-file.ts +38 -19
  52. package/src/config/keybindings.ts +6 -1
  53. package/src/config/settings-schema.ts +56 -40
  54. package/src/config/settings.ts +7 -0
  55. package/src/eval/__tests__/agent-bridge.test.ts +5 -3
  56. package/src/eval/agent-bridge.ts +3 -16
  57. package/src/eval/js/shared/prelude.txt +1 -1
  58. package/src/eval/py/prelude.py +5 -6
  59. package/src/export/html/template.generated.ts +1 -1
  60. package/src/export/html/template.js +38 -13
  61. package/src/extensibility/custom-tools/types.ts +2 -2
  62. package/src/extensibility/shared-events.ts +2 -2
  63. package/src/internal-urls/docs-index.generated.ts +8 -8
  64. package/src/internal-urls/history-protocol.ts +113 -0
  65. package/src/internal-urls/index.ts +1 -0
  66. package/src/internal-urls/router.ts +3 -1
  67. package/src/internal-urls/types.ts +1 -1
  68. package/src/irc/bus.ts +292 -0
  69. package/src/main.ts +8 -60
  70. package/src/modes/components/{session-observer-overlay.ts → agent-hub.ts} +586 -367
  71. package/src/modes/components/compaction-summary-message.ts +68 -32
  72. package/src/modes/components/custom-editor.ts +10 -0
  73. package/src/modes/components/tool-execution.ts +31 -1
  74. package/src/modes/components/ttsr-notification.ts +72 -30
  75. package/src/modes/components/welcome.ts +9 -33
  76. package/src/modes/controllers/event-controller.ts +65 -0
  77. package/src/modes/controllers/extension-ui-controller.ts +8 -8
  78. package/src/modes/controllers/input-controller.ts +18 -2
  79. package/src/modes/controllers/selector-controller.ts +21 -17
  80. package/src/modes/interactive-mode.ts +8 -13
  81. package/src/modes/theme/theme.ts +18 -5
  82. package/src/modes/types.ts +3 -5
  83. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  84. package/src/modes/utils/ui-helpers.ts +51 -49
  85. package/src/prompts/system/irc-incoming.md +3 -4
  86. package/src/prompts/system/orchestrate-notice.md +2 -2
  87. package/src/prompts/system/subagent-system-prompt.md +0 -5
  88. package/src/prompts/system/system-prompt.md +1 -0
  89. package/src/prompts/system/workflow-notice.md +2 -2
  90. package/src/prompts/tools/eval.md +3 -3
  91. package/src/prompts/tools/irc.md +29 -19
  92. package/src/prompts/tools/read.md +2 -2
  93. package/src/prompts/tools/task-summary.md +5 -16
  94. package/src/prompts/tools/task.md +38 -29
  95. package/src/registry/agent-lifecycle.ts +218 -0
  96. package/src/registry/agent-registry.ts +16 -5
  97. package/src/sdk.ts +29 -9
  98. package/src/session/agent-session.ts +243 -237
  99. package/src/session/messages.ts +11 -78
  100. package/src/session/session-history-format.ts +246 -0
  101. package/src/session/session-manager.ts +59 -5
  102. package/src/session/streaming-output.ts +60 -0
  103. package/src/task/executor.ts +855 -466
  104. package/src/task/index.ts +718 -794
  105. package/src/task/output-manager.ts +0 -11
  106. package/src/task/render.ts +133 -63
  107. package/src/task/repair-args.ts +21 -9
  108. package/src/task/types.ts +73 -66
  109. package/src/tools/ask.ts +4 -2
  110. package/src/tools/bash.ts +15 -5
  111. package/src/tools/browser/tab-worker.ts +26 -7
  112. package/src/tools/browser.ts +28 -1
  113. package/src/tools/find.ts +2 -27
  114. package/src/tools/grouped-file-output.ts +1 -118
  115. package/src/tools/index.ts +4 -12
  116. package/src/tools/irc.ts +596 -171
  117. package/src/tools/job.ts +41 -7
  118. package/src/tools/read.ts +57 -1
  119. package/src/tools/renderers.ts +2 -0
  120. package/src/tools/resolve.ts +4 -1
  121. package/dist/types/async/support.d.ts +0 -2
  122. package/dist/types/modes/components/session-observer-overlay.d.ts +0 -11
  123. package/dist/types/task/simple-mode.d.ts +0 -8
  124. package/src/async/support.ts +0 -5
  125. package/src/task/simple-mode.ts +0 -27
@@ -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) => {
@@ -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";
@@ -578,7 +578,7 @@ export class SelectorController {
578
578
  }
579
579
 
580
580
  this.ctx.chatContainer.clear();
581
- this.ctx.renderInitialMessages(undefined, { clearTerminalHistory: true });
581
+ this.ctx.renderInitialMessages({ clearTerminalHistory: true });
582
582
  this.ctx.editor.setText(result.selectedText);
583
583
  done();
584
584
  this.ctx.showStatus("Branched to new session");
@@ -719,9 +719,10 @@ export class SelectorController {
719
719
  return;
720
720
  }
721
721
 
722
- // Update UI — pass the context built by navigateTree to skip a second O(N) walk.
722
+ // Update UI — rebuild the display transcript for the new leaf (the
723
+ // context from navigateTree is the LLM context, not the transcript).
723
724
  this.ctx.chatContainer.clear();
724
- this.ctx.renderInitialMessages(result.sessionContext, { clearTerminalHistory: true });
725
+ this.ctx.renderInitialMessages({ clearTerminalHistory: true });
725
726
  await this.ctx.reloadTodos();
726
727
  if (result.editorText && !this.ctx.editor.getText().trim()) {
727
728
  this.ctx.editor.setText(result.editorText);
@@ -846,7 +847,7 @@ export class SelectorController {
846
847
  this.ctx.statusLine.setSessionStartTime(Date.now());
847
848
  this.ctx.updateEditorTopBorder();
848
849
  this.ctx.updateEditorBorderColor();
849
- this.ctx.renderInitialMessages(undefined, { clearTerminalHistory: true });
850
+ this.ctx.renderInitialMessages({ clearTerminalHistory: true });
850
851
  await this.ctx.reloadTodos();
851
852
  this.ctx.ui.requestRender(true, { clearScrollback: true });
852
853
  return true;
@@ -871,7 +872,7 @@ export class SelectorController {
871
872
 
872
873
  // Clear and re-render the chat
873
874
  this.ctx.chatContainer.clear();
874
- this.ctx.renderInitialMessages(undefined, { clearTerminalHistory: true });
875
+ this.ctx.renderInitialMessages({ clearTerminalHistory: true });
875
876
  await this.ctx.reloadTodos();
876
877
  this.ctx.showStatus(movedProject ? `Resumed session in ${shortenPath(newCwd)}` : "Resumed session");
877
878
  }
@@ -1074,31 +1075,34 @@ export class SelectorController {
1074
1075
  });
1075
1076
  }
1076
1077
 
1077
- showSessionObserver(registry: SessionObserverRegistry): void {
1078
- const observeKeys = this.ctx.keybindings.getKeys("app.session.observe");
1079
- let cleanup: (() => void) | undefined;
1078
+ showAgentHub(observers: SessionObserverRegistry): void {
1079
+ const hubKeys = [
1080
+ ...this.ctx.keybindings.getKeys("app.agents.hub"),
1081
+ ...this.ctx.keybindings.getKeys("app.session.observe"),
1082
+ ];
1083
+ let hub: AgentHubOverlayComponent | undefined;
1080
1084
  let overlayHandle: OverlayHandle | undefined;
1081
1085
 
1082
1086
  const done = () => {
1083
- cleanup?.();
1087
+ hub?.dispose();
1084
1088
  overlayHandle?.hide();
1085
1089
  this.ctx.ui.requestRender();
1086
1090
  };
1087
1091
 
1088
- const selector = new SessionObserverOverlayComponent(registry, done, observeKeys);
1089
-
1090
- cleanup = registry.onChange(() => {
1091
- selector.refreshFromRegistry();
1092
- this.ctx.ui.requestRender();
1092
+ hub = new AgentHubOverlayComponent({
1093
+ observers,
1094
+ hubKeys,
1095
+ onDone: done,
1096
+ requestRender: () => this.ctx.ui.requestRender(),
1093
1097
  });
1094
1098
 
1095
- overlayHandle = this.ctx.ui.showOverlay(selector, {
1099
+ overlayHandle = this.ctx.ui.showOverlay(hub, {
1096
1100
  anchor: "bottom-center",
1097
1101
  width: "100%",
1098
1102
  maxHeight: "100%",
1099
1103
  margin: 0,
1100
1104
  });
1101
- this.ctx.ui.setFocus(selector);
1105
+ this.ctx.ui.setFocus(hub);
1102
1106
  this.ctx.ui.requestRender();
1103
1107
  }
1104
1108
  }
@@ -327,6 +327,7 @@ export class InteractiveMode implements InteractiveModeContext {
327
327
  #pendingSubmissionDispose: (() => void) | undefined;
328
328
  lastSigintTime = 0;
329
329
  lastEscapeTime = 0;
330
+ lastLeftTapTime = 0;
330
331
  shutdownRequested = false;
331
332
  #isShuttingDown = false;
332
333
  hookSelector: HookSelectorComponent | undefined = undefined;
@@ -1088,7 +1089,9 @@ export class InteractiveMode implements InteractiveModeContext {
1088
1089
 
1089
1090
  rebuildChatFromMessages(): void {
1090
1091
  this.chatContainer.clear();
1091
- const context = this.session.buildDisplaySessionContext();
1092
+ // Full-history transcript: compactions render as inline dividers instead
1093
+ // of restarting the visible conversation (the LLM context still resets).
1094
+ const context = this.session.buildTranscriptSessionContext();
1092
1095
  this.renderSessionContext(context);
1093
1096
  }
1094
1097
 
@@ -2880,11 +2883,8 @@ export class InteractiveMode implements InteractiveModeContext {
2880
2883
  this.#uiHelpers.renderSessionContext(sessionContext, options);
2881
2884
  }
2882
2885
 
2883
- renderInitialMessages(
2884
- prebuiltContext?: SessionContext,
2885
- options?: { preserveExistingChat?: boolean; clearTerminalHistory?: boolean },
2886
- ): void {
2887
- this.#uiHelpers.renderInitialMessages(prebuiltContext, options);
2886
+ renderInitialMessages(options?: { preserveExistingChat?: boolean; clearTerminalHistory?: boolean }): void {
2887
+ this.#uiHelpers.renderInitialMessages(options);
2888
2888
  }
2889
2889
 
2890
2890
  getUserMessageText(message: Message): string {
@@ -3068,13 +3068,8 @@ export class InteractiveMode implements InteractiveModeContext {
3068
3068
  await this.#selectorController.showDebugSelector();
3069
3069
  }
3070
3070
 
3071
- showSessionObserver(): void {
3072
- const sessions = this.#observerRegistry.getSessions();
3073
- if (sessions.length <= 1) {
3074
- this.showStatus("No active subagent sessions");
3075
- return;
3076
- }
3077
- this.#selectorController.showSessionObserver(this.#observerRegistry);
3071
+ showAgentHub(): void {
3072
+ this.#selectorController.showAgentHub(this.#observerRegistry);
3078
3073
  }
3079
3074
 
3080
3075
  resetObserverRegistry(): void {
@@ -129,6 +129,8 @@ export type SymbolKey =
129
129
  | "icon.extensionInstruction"
130
130
  // STT
131
131
  | "icon.mic"
132
+ // Compaction divider
133
+ | "icon.camera"
132
134
  // Thinking Levels
133
135
  | "thinking.minimal"
134
136
  | "thinking.low"
@@ -220,7 +222,8 @@ export type SymbolKey =
220
222
  | "tool.resolve"
221
223
  | "tool.review"
222
224
  | "tool.inspectImage"
223
- | "tool.goal";
225
+ | "tool.goal"
226
+ | "tool.irc";
224
227
 
225
228
  type SymbolMap = Record<SymbolKey, string>;
226
229
 
@@ -322,13 +325,15 @@ const UNICODE_SYMBOLS: SymbolMap = {
322
325
  "icon.extensionInstruction": "📘",
323
326
  // STT
324
327
  "icon.mic": "🎤",
328
+ // Compaction divider
329
+ "icon.camera": "📷",
325
330
  // Thinking levels
326
331
  "thinking.minimal": "◔ min",
327
332
  "thinking.low": "◑ low",
328
333
  "thinking.medium": "◒ med",
329
334
  "thinking.high": "◕ high",
330
335
  "thinking.xhigh": "◉ xhigh",
331
- "thinking.autoPending": "▣?",
336
+ "thinking.autoPending": "",
332
337
  // Checkboxes
333
338
  "checkbox.checked": "☑",
334
339
  "checkbox.unchecked": "☐",
@@ -414,6 +419,7 @@ const UNICODE_SYMBOLS: SymbolMap = {
414
419
  "tool.review": "◉",
415
420
  "tool.inspectImage": "🖼",
416
421
  "tool.goal": "◎",
422
+ "tool.irc": "✉",
417
423
  };
418
424
 
419
425
  const NERD_SYMBOLS: SymbolMap = {
@@ -599,6 +605,8 @@ const NERD_SYMBOLS: SymbolMap = {
599
605
  "icon.extensionInstruction": "\uf02d",
600
606
  // STT - fa-microphone
601
607
  "icon.mic": "\uf130",
608
+ // Compaction divider - fa-camera-retro
609
+ "icon.camera": "\uf083",
602
610
  // Thinking Levels - emoji labels
603
611
  // pick: 🤨 min | alt:  min  min
604
612
  "thinking.minimal": "\u{F0E7} min",
@@ -610,8 +618,8 @@ const NERD_SYMBOLS: SymbolMap = {
610
618
  "thinking.high": "\u{F111} high",
611
619
  // pick: 🧠 xhi | alt:  xhi  xhi
612
620
  "thinking.xhigh": "\u{F06D} xhi",
613
- // pick: 󰞋 (nf-md-help_box) | alt: [?]
614
- "thinking.autoPending": "\u{f078b}",
621
+ // pick: (fa-circle-o-notch) | alt: 󰂼 (nf-md-cached) ⟳
622
+ "thinking.autoPending": "\uf1ce",
615
623
  // Checkboxes
616
624
  // pick:  | alt:  
617
625
  "checkbox.checked": "\uf14a",
@@ -708,6 +716,7 @@ const NERD_SYMBOLS: SymbolMap = {
708
716
  "tool.review": "\uEA70",
709
717
  "tool.inspectImage": "\uEAEA",
710
718
  "tool.goal": "\uEBF8",
719
+ "tool.irc": "\uF086",
711
720
  };
712
721
 
713
722
  const ASCII_SYMBOLS: SymbolMap = {
@@ -808,13 +817,15 @@ const ASCII_SYMBOLS: SymbolMap = {
808
817
  "icon.extensionInstruction": "IN",
809
818
  // STT
810
819
  "icon.mic": "MIC",
820
+ // Compaction divider
821
+ "icon.camera": "[o]",
811
822
  // Thinking Levels
812
823
  "thinking.minimal": "[min]",
813
824
  "thinking.low": "[low]",
814
825
  "thinking.medium": "[med]",
815
826
  "thinking.high": "[high]",
816
827
  "thinking.xhigh": "[xhi]",
817
- "thinking.autoPending": "[?]",
828
+ "thinking.autoPending": "[~]",
818
829
  // Checkboxes
819
830
  "checkbox.checked": "[x]",
820
831
  "checkbox.unchecked": "[ ]",
@@ -898,6 +909,7 @@ const ASCII_SYMBOLS: SymbolMap = {
898
909
  "tool.review": "rev",
899
910
  "tool.inspectImage": "[i]",
900
911
  "tool.goal": "(o)",
912
+ "tool.irc": "irc",
901
913
  };
902
914
 
903
915
  const SYMBOL_PRESETS: Record<SymbolPreset, SymbolMap> = {
@@ -1686,6 +1698,7 @@ export class Theme {
1686
1698
  extensionContextFile: this.#symbols["icon.extensionContextFile"],
1687
1699
  extensionInstruction: this.#symbols["icon.extensionInstruction"],
1688
1700
  mic: this.#symbols["icon.mic"],
1701
+ camera: this.#symbols["icon.camera"],
1689
1702
  };
1690
1703
  }
1691
1704
 
@@ -136,6 +136,7 @@ export interface InteractiveModeContext {
136
136
  locallySubmittedUserSignatures: Set<string>;
137
137
  lastSigintTime: number;
138
138
  lastEscapeTime: number;
139
+ lastLeftTapTime: number;
139
140
  shutdownRequested: boolean;
140
141
  hookSelector: HookSelectorComponent | undefined;
141
142
  hookInput: HookInputComponent | undefined;
@@ -225,10 +226,7 @@ export interface InteractiveModeContext {
225
226
  sessionContext: SessionContext,
226
227
  options?: { updateFooter?: boolean; populateHistory?: boolean },
227
228
  ): void;
228
- renderInitialMessages(
229
- prebuiltContext?: SessionContext,
230
- options?: { preserveExistingChat?: boolean; clearTerminalHistory?: boolean },
231
- ): void;
229
+ renderInitialMessages(options?: { preserveExistingChat?: boolean; clearTerminalHistory?: boolean }): void;
232
230
  getUserMessageText(message: Message): string;
233
231
  findLastAssistantMessage(): AssistantMessage | undefined;
234
232
  extractAssistantText(message: AssistantMessage): string;
@@ -292,7 +290,7 @@ export interface InteractiveModeContext {
292
290
  showProviderSetup(): Promise<void>;
293
291
  showHookConfirm(title: string, message: string): Promise<boolean>;
294
292
  showDebugSelector(): Promise<void>;
295
- showSessionObserver(): void;
293
+ showAgentHub(): void;
296
294
  resetObserverRegistry(): void;
297
295
 
298
296
  // Input handling
@@ -50,6 +50,7 @@ export function buildHotkeysMarkdown(bindings: HotkeysMarkdownBindings): string
50
50
  `| \`${appKey(bindings, "app.editor.external")}\` | Edit message in external editor |`,
51
51
  `| \`${appKey(bindings, "app.clipboard.pasteImage")}\` | Paste image from clipboard |`,
52
52
  `| \`${appKey(bindings, "app.stt.toggle")}\` | Toggle speech-to-text recording |`,
53
+ `| \`${appKey(bindings, "app.agents.hub")}\` / \`${appKey(bindings, "app.session.observe")}\` / double-tap \`←\` (empty editor) | Open the agent hub |`,
53
54
  "| `#` | Open prompt actions |",
54
55
  "| `/` | Slash commands |",
55
56
  "| `!` | Run bash command |",
@@ -35,6 +35,7 @@ import {
35
35
  type SkillPromptDetails,
36
36
  } from "../../session/messages";
37
37
  import type { SessionContext } from "../../session/session-manager";
38
+ import { createIrcMessageCard } from "../../tools/irc";
38
39
  import { formatBytes, formatDuration } from "../../tools/render-utils";
39
40
 
40
41
  type TextBlock = { type: "text"; text: string };
@@ -190,49 +191,31 @@ export class UiHelpers {
190
191
  this.ctx.chatContainer.addChild(component);
191
192
  break;
192
193
  }
193
- if (
194
- message.customType === "irc:incoming" ||
195
- message.customType === "irc:autoreply" ||
196
- message.customType === "irc:relay"
197
- ) {
194
+ if (message.customType === "irc:incoming" || message.customType === "irc:relay") {
198
195
  const details = (
199
196
  message as CustomMessage<{
200
197
  from?: string;
201
198
  to?: string;
202
199
  message?: string;
203
- reply?: string;
204
200
  body?: string;
205
- kind?: "message" | "reply";
201
+ replyTo?: string;
206
202
  }>
207
203
  ).details;
208
- let arrow: string;
209
- let body: string;
210
- if (message.customType === "irc:incoming") {
211
- const peer = details?.from ?? "?";
212
- body = details?.message ?? "";
213
- arrow = `⇦ ${peer}`;
214
- } else if (message.customType === "irc:autoreply") {
215
- const peer = details?.to ?? "?";
216
- body = details?.reply ?? "";
217
- arrow = `⇨ ${peer}`;
218
- } else {
219
- const from = details?.from ?? "?";
220
- const to = details?.to ?? "?";
221
- body = details?.body ?? "";
222
- arrow = `${from} ⇨ ${to}`;
223
- }
224
- const block = new TranscriptBlock();
225
- const header = `${theme.fg("accent", `[IRC] ${arrow}`)}`;
226
- const headerComponent = new Text(header, 1, 0);
227
- block.addChild(headerComponent);
228
- if (body) {
229
- for (const line of body.split("\n")) {
230
- const lineComponent = new Text(theme.fg("muted", ` ${line}`), 0, 0);
231
- block.addChild(lineComponent);
232
- }
233
- }
234
- this.ctx.chatContainer.addChild(block);
235
- return [block];
204
+ const incoming = message.customType === "irc:incoming";
205
+ const card = createIrcMessageCard(
206
+ {
207
+ kind: incoming ? "incoming" : "relay",
208
+ from: details?.from,
209
+ to: details?.to,
210
+ body: incoming ? details?.message : details?.body,
211
+ replyTo: details?.replyTo,
212
+ timestamp: message.timestamp,
213
+ },
214
+ () => this.ctx.toolOutputExpanded,
215
+ theme,
216
+ );
217
+ this.ctx.chatContainer.addChild(card);
218
+ return [card];
236
219
  }
237
220
  const renderer = this.ctx.session.extensionRunner?.getMessageRenderer(message.customType);
238
221
  // Both HookMessage and CustomMessage have the same structure, cast for compatibility
@@ -337,13 +320,23 @@ export class UiHelpers {
337
320
  let readGroup: ReadToolGroupComponent | null = null;
338
321
  const readToolCallArgs = new Map<string, Record<string, unknown>>();
339
322
  const readToolCallAssistantComponents = new Map<string, AssistantMessageComponent>();
340
- const deferredMessages: AgentMessage[] = [];
341
- for (const message of sessionContext.messages) {
342
- // Defer compaction summaries so they render at the bottom (visible after scroll)
343
- if (message.role === "compactionSummary") {
344
- deferredMessages.push(message);
345
- continue;
323
+ // Rebuild-time mirror of the event controller's displaceable-poll
324
+ // bookkeeping: a `job` poll that found every watched job still running is
325
+ // superseded by the next `job` call, so a rebuilt transcript collapses a
326
+ // repeated-poll run to its final snapshot instead of replaying the spam.
327
+ let waitingPoll: ToolExecutionComponent | null = null;
328
+ const resolveWaitingPoll = (nextToolName?: string) => {
329
+ const previous = waitingPoll;
330
+ if (!previous) return;
331
+ waitingPoll = null;
332
+ if (nextToolName === "job" && previous.isDisplaceableBlock()) {
333
+ this.ctx.chatContainer.removeChild(previous);
346
334
  }
335
+ // Sealing freezes the block and stops the waiting-poll spinner that
336
+ // updateResult armed.
337
+ previous.seal();
338
+ };
339
+ for (const message of sessionContext.messages) {
347
340
  // Assistant messages need special handling for tool calls
348
341
  if (message.role === "assistant") {
349
342
  this.ctx.addMessageToChat(message);
@@ -379,6 +372,7 @@ export class UiHelpers {
379
372
  if (content.type !== "toolCall") {
380
373
  continue;
381
374
  }
375
+ resolveWaitingPoll(content.name);
382
376
 
383
377
  if (
384
378
  content.name === "read" &&
@@ -493,8 +487,17 @@ export class UiHelpers {
493
487
  if (component) {
494
488
  component.updateResult(message, false, message.toolCallId);
495
489
  this.ctx.pendingTools.delete(message.toolCallId);
490
+ if (
491
+ message.toolName === "job" &&
492
+ component instanceof ToolExecutionComponent &&
493
+ component.isDisplaceableBlock()
494
+ ) {
495
+ waitingPoll = component;
496
+ }
496
497
  }
497
498
  } else {
499
+ // A user prompt closes the displacement window, same as the live path.
500
+ if (message.role === "user") resolveWaitingPoll();
498
501
  // All other messages use standard rendering
499
502
  this.ctx.addMessageToChat(message, options);
500
503
  }
@@ -504,17 +507,15 @@ export class UiHelpers {
504
507
  // rebuilt group freezes (even with a never-persisted result) and commits to
505
508
  // native scrollback like every other historical block.
506
509
  readGroup?.seal();
507
-
508
- // Render deferred messages (compaction summaries) at the bottom so they're visible
509
- for (const message of deferredMessages) {
510
- this.ctx.addMessageToChat(message, options);
511
- }
510
+ // A trailing waiting poll is final history on rebuild; seal it so it
511
+ // freezes (and its spinner timer stops) like every other block.
512
+ resolveWaitingPoll();
512
513
 
513
514
  this.ctx.pendingTools.clear();
514
515
  this.ctx.ui.requestRender();
515
516
  }
516
517
 
517
- renderInitialMessages(prebuiltContext?: SessionContext, options: RenderInitialMessagesOptions = {}): void {
518
+ renderInitialMessages(options: RenderInitialMessagesOptions = {}): void {
518
519
  // This path is used to rebuild the visible chat transcript (e.g. after custom/debug UI).
519
520
  // Clear existing rendered chat first to avoid duplicating the full session in the container.
520
521
  // On a non-preserving rebuild the existing blocks are discarded for good, so
@@ -530,8 +531,9 @@ export class UiHelpers {
530
531
  this.ctx.pendingBashComponents = [];
531
532
  this.ctx.pendingPythonComponents = [];
532
533
 
533
- // Reuse a pre-built context when available (e.g. from navigateTree) to avoid a second O(N) walk.
534
- const context = prebuiltContext ?? this.ctx.sessionManager.buildSessionContext();
534
+ // Display always uses the full-history transcript: compactions show as
535
+ // inline dividers instead of restarting the visible conversation.
536
+ const context = this.ctx.session.buildTranscriptSessionContext();
535
537
  this.ctx.renderSessionContext(context, {
536
538
  updateFooter: true,
537
539
  populateHistory: true,
@@ -1,8 +1,7 @@
1
1
  <irc>
2
- You received an IRC message from agent `{{from}}`.
2
+ Incoming IRC message from agent `{{from}}`{{#if replyTo}} (replying to {{replyTo}}){{/if}}:
3
3
 
4
- Reply briefly and directly using the conversation context already available to you. NEVER call tools. The reply you write is delivered back to `{{from}}` as your answer.
5
-
6
- Message:
7
4
  {{message}}
5
+
6
+ If a response is expected, reply with the `irc` tool (`op: "send"`, `to: "{{from}}"`) — you may finish your current step first. Nobody replies on your behalf.
8
7
  </irc>
@@ -8,7 +8,7 @@ You decompose, dispatch, verify, and iterate. Substantial and parallelizable wor
8
8
  <rules>
9
9
  1. **NEVER yield until everything is closed.** A phase finishing is *not* a yield point — launch the next phase in the same turn. Stop only when every requested item is verifiably done, or you hit a concrete [blocked] state that genuinely requires the user.
10
10
  2. **Enumerate the full surface before dispatching.** If the request references audits, plans, checklists, phase lists, or file lists, expand them into a flat set of items in `todo`. "Most of them" or "the important ones" is failure. Re-read the source documents — NEVER work from memory.
11
- 3. **Parallelize maximally; NEVER launch a one-off task.** Every set of edits with disjoint file scope MUST ship as one `task` batch — fan the work as wide as it decomposes. A single-task batch for divisible work is a failure: split it. If you are about to dispatch exactly one subagent, stop — either there is more to run alongside it (find it and batch them) or the change is small enough to make inline yourself (do it). Serialize only when one subagent produces a contract (types, schema, shared module) the next consumes — and state the dependency when you do.
11
+ 3. **Parallelize maximally; NEVER launch a one-off task.** Every set of edits with disjoint file scope MUST ship as parallel `task` calls in one message — fan the work as wide as it decomposes. Dispatching divisible work one call at a time, serially, is a failure: split it and dispatch together. If you are about to dispatch exactly one subagent, stop — either there is more to run alongside it (find it and dispatch them together) or the change is small enough to make inline yourself (do it). Serialize only when one subagent produces a contract (types, schema, shared module) the next consumes — and state the dependency when you do.
12
12
  4. **Each `task` assignment is self-contained.** Subagents have no shared context. Spell out: target files (≤3–5 explicit paths, no globs), the change with APIs and patterns, edge cases, and observable acceptance criteria. NEVER assume they read the same plan you did.
13
13
  5. **Verify after every phase before launching the next.** Run the appropriate gate: `bun check` for types, package-scoped `bun test` for behavior, `lsp diagnostics` for changed files. If a phase introduced breakage, dispatch fix-up subagents *before* moving on. NEVER declare a phase done on a red tree.
14
14
  6. **Commit policy.** If the request asks for commits or the repo workflow expects them, commit after each green phase with a focused message. NEVER commit a red tree. NEVER commit work the user did not ask to commit.
@@ -21,7 +21,7 @@ You decompose, dispatch, verify, and iterate. Substantial and parallelizable wor
21
21
  <workflow>
22
22
  1. **Ingest.** Read every referenced file (audits, plans, prior agent output, current branch state). Run `git status` to see uncommitted changes.
23
23
  2. **Plan.** Materialize the full work surface in `todo` as ordered phases. Within each phase, list the parallelizable units.
24
- 3. **Dispatch phase.** Launch all parallel `task` subagents in one call. Wait for the batch.
24
+ 3. **Dispatch phase.** Launch all parallel `task` subagents in one message, then collect every result (async results / `job poll`) before moving on.
25
25
  4. **Verify phase.** Run the gates. On failure, dispatch fix-up subagents and re-verify. Do not advance with a red gate.
26
26
  5. **Commit phase** (if applicable). Focused message naming the phase.
27
27
  6. **Advance.** Mark the phase done in `todo`, immediately start the next phase. No summary message between phases — keep going.
@@ -32,11 +32,6 @@ You are working in an isolated working tree at `{{worktree}}` for this sub-task.
32
32
  You NEVER modify files outside this tree or in the original repository.
33
33
  {{/if}}
34
34
 
35
- {{#if contextFile}}
36
- # Conversation Context
37
- If you need additional information, your conversation with the user is in {{contextFile}} — `read` its tail or `search` it for relevant terms.
38
- {{/if}}
39
-
40
35
  {{#if ircPeers}}
41
36
  # IRC Peers
42
37
  You can reach other live agents via the `irc` tool. Your id is `{{ircSelfId}}`. Currently visible peers:
@@ -149,6 +149,7 @@ With most FS/bash-like tools, static references to them will automatically resol
149
149
  - `agent://<id>`: full agent output artifact
150
150
  - `/<path>`: JSON field extraction
151
151
  - `artifact://<id>`: Artifact content
152
+ - `history://<agentId>`: agent transcript as concise markdown; bare `history://` lists agents
152
153
  - `local://<name>.md`: Plan artifacts and shared content with subagents
153
154
  {{#if hasObsidian}}
154
155
  - `vault://<vault>/<path>`: Obsidian vault content (read/edit). `vault://` lists vaults; `vault://_/…` targets the active vault. File-scoped `?op=outline|backlinks|links|tags|properties|tasks|base|…`; vault-scoped `?op=search&q=…|daily|tasks|orphans|unresolved|bases|…`.
@@ -13,8 +13,8 @@ Worth it when the task benefits from decomposition + parallel coverage, or from
13
13
  <helpers>
14
14
  State persists across cells, so scout in one cell and fan out in the next. Every cell has:
15
15
 
16
- - `agent(prompt, *, agent_type="task", model=None, context=None, label=None, schema=None)` — run ONE subagent; returns its final text, or the validated object when `schema` (a JSON Schema dict) is given. With `schema` the subagent is forced to emit structured output that is validated for you — branch on the object, not on parsed prose. `agent_type` picks a discovered agent ("explore", "reviewer", "oracle", …); `context` is shared background; `label` names the artifact. Subagents are told their final text IS the return value, so they hand back raw data. `agent()` blocks until the subagent finishes; eval-spawned agents nest at most 3 deep.
17
- - `parallel(thunks)` — run zero-arg callables concurrently through a bounded pool, preserving input order; returns once all finish. The pool runs as wide as a `task` tool batch — don't hand-tune it; fan out as wide as the work divides. A thunk that raises propagates — wrap risky work in `try/except` inside the thunk to keep partial results. In a loop, bind each closure's value with a default arg (`lambda d=d: …`) or every thunk captures the last one.
16
+ - `agent(prompt, *, agent_type="task", model=None, label=None, schema=None)` — run ONE subagent; returns its final text, or the validated object when `schema` (a JSON Schema dict) is given. With `schema` the subagent is forced to emit structured output that is validated for you — branch on the object, not on parsed prose. `agent_type` picks a discovered agent ("explore", "reviewer", "oracle", …); `label` names the artifact. Shared background goes in a `local://` file referenced from each prompt, not a parameter. Subagents are told their final text IS the return value, so they hand back raw data. `agent()` blocks until the subagent finishes; eval-spawned agents nest at most 3 deep.
17
+ - `parallel(thunks)` — run zero-arg callables concurrently through a bounded pool, preserving input order; returns once all finish. The pool is bounded by the session's `task` concurrency — don't hand-tune it; fan out as wide as the work divides. A thunk that raises propagates — wrap risky work in `try/except` inside the thunk to keep partial results. In a loop, bind each closure's value with a default arg (`lambda d=d: …`) or every thunk captures the last one.
18
18
  - `pipeline(items, *stages)` — map items through `stages` left-to-right. There is a BARRIER between stages: ALL items clear stage N before stage N+1 begins. Each stage is a one-arg callable; stage 1 gets the original item, later stages get the previous result. Same pool width as `parallel()`.
19
19
  - `completion(prompt, *, model="default", system=None, schema=None)` — oneshot, stateless model call (no tools, no history). Tiers: "smol", "default", "slow". Cheap classification/scoring inside a fan-out.
20
20
  - `log(message)` — emit a progress line above the status tree. `phase(title)` — start a phase; the status lines that follow group under it.
@@ -46,9 +46,9 @@ tool.<name>(args) → unknown
46
46
  Invoke any session tool by name. `args` is the tool's parameter object.
47
47
  completion(prompt, model?="default", system?=None, schema?=None) → str | dict
48
48
  Oneshot, stateless completion (no history, no tools). `model` picks a tier: "smol" (fast), "default" (this session's model), "slow" (most capable). Pass `system` for a system prompt. Pass a JSON-Schema `schema` to force structured output and get the parsed object back; otherwise returns the completion text.
49
- {{#if spawns}}agent(prompt, agent_type?="task", model?=None, context?=None, label?=None, schema?=None) → str | dict
50
- Run a subagent and return its final output. Defaults to the bundled "task" agent; pass `agent_type`/`agentType` for another discovered agent. Pass a JSON-Schema `schema` to force structured output and get the parsed object back.
51
- {{#if js}} In JS, pass options as one trailing object — never positional: agent(prompt, { agentType, context, schema }).
49
+ {{#if spawns}}agent(prompt, agent_type?="task", model?=None, label?=None, schema?=None) → str | dict
50
+ Run a subagent and return its final output. Defaults to the bundled "task" agent; pass `agent_type`/`agentType` for another discovered agent. Pass a JSON-Schema `schema` to force structured output and get the parsed object back. Share background by writing a `local://` file and referencing it in the prompt.
51
+ {{#if js}} In JS, pass options as one trailing object — never positional: agent(prompt, { agentType, schema }).
52
52
  {{/if}}
53
53
  {{/if}}
54
54
  parallel(thunks) → list