@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
@@ -0,0 +1,174 @@
1
+ import { parseStreamingJson } from "@oh-my-pi/pi-ai/utils/json-parse";
2
+ import { nextStep, STREAMING_REVEAL_FRAME_MS } from "./streaming-reveal";
3
+
4
+ /** Minimal component surface the reveal pushes frames into. */
5
+ type ToolArgsRevealComponent = {
6
+ updateArgs(args: unknown, toolCallId?: string): void;
7
+ };
8
+
9
+ type ToolArgsRevealControllerOptions = {
10
+ getSmoothStreaming(): boolean;
11
+ requestRender(): void;
12
+ };
13
+
14
+ type RevealEntry = {
15
+ component: ToolArgsRevealComponent | undefined;
16
+ /** Latest raw streamed argument text (JSON for function tools, raw text for custom tools). */
17
+ target: string;
18
+ /** Revealed UTF-16 code units of `target`. */
19
+ revealed: number;
20
+ /** Custom-tool raw input: display args are `{ input: prefix }`, never parsed as JSON. */
21
+ rawInput: boolean;
22
+ };
23
+
24
+ /** Clamp a slice end into `text`, never splitting a surrogate pair: a prefix
25
+ * ending on a high surrogate would feed a lone surrogate into the parsed
26
+ * preview args (providers decode UTF-8 incrementally, so the raw stream
27
+ * itself never contains one). */
28
+ function clampSliceEnd(text: string, end: number): number {
29
+ if (end <= 0) return 0;
30
+ if (end >= text.length) return text.length;
31
+ const code = text.charCodeAt(end - 1);
32
+ return code >= 0xd800 && code <= 0xdbff ? end + 1 : end;
33
+ }
34
+
35
+ /** Display args for a revealed raw-stream prefix. Function-tool prefixes are
36
+ * re-parsed with the same streaming-tolerant parser providers use, so every
37
+ * frame is a state the provider itself could have produced; custom tools
38
+ * mirror the provider's `{ input }` shape. `__partialJson` carries the
39
+ * matching raw prefix for renderers that read it directly (bash env preview,
40
+ * edit strategies). */
41
+ function buildDisplayArgs(prefix: string, rawInput: boolean): Record<string, unknown> {
42
+ const base: Record<string, unknown> = rawInput ? { input: prefix } : parseStreamingJson(prefix);
43
+ return { ...base, __partialJson: prefix };
44
+ }
45
+
46
+ /**
47
+ * Paces streamed tool-call arguments the same way StreamingRevealController
48
+ * paces assistant text: providers that deliver `partialJson` in large batches
49
+ * (or throttle their partial parses) would otherwise make write/edit/bash
50
+ * streaming previews jump in chunks. Each pending tool call reveals its raw
51
+ * argument stream at the shared 30fps cadence with the same adaptive
52
+ * catch-up step, re-parsing the revealed prefix per frame.
53
+ *
54
+ * Reveal units are UTF-16 code units of the raw stream, not graphemes —
55
+ * the prefix goes through a JSON parser rather than straight to the screen,
56
+ * so only surrogate-pair integrity matters (see {@link clampSliceEnd}).
57
+ */
58
+ export class ToolArgsRevealController {
59
+ readonly #getSmoothStreaming: () => boolean;
60
+ readonly #requestRender: () => void;
61
+ readonly #entries = new Map<string, RevealEntry>();
62
+ #timer: NodeJS.Timeout | undefined;
63
+
64
+ constructor(options: ToolArgsRevealControllerOptions) {
65
+ this.#getSmoothStreaming = options.getSmoothStreaming;
66
+ this.#requestRender = options.requestRender;
67
+ }
68
+
69
+ /**
70
+ * Record the latest streamed argument text for a tool call and return the
71
+ * args to render right now. With smoothing disabled the full target passes
72
+ * through in the caller's legacy shape (`{ ...args, __partialJson }`).
73
+ */
74
+ setTarget(
75
+ id: string,
76
+ partialJson: string,
77
+ rawInput: boolean,
78
+ fullArgs: Record<string, unknown>,
79
+ ): Record<string, unknown> {
80
+ if (!this.#getSmoothStreaming()) {
81
+ // Toggle may flip mid-call: drop any live entry so ticks stop.
82
+ this.#entries.delete(id);
83
+ return { ...fullArgs, __partialJson: partialJson };
84
+ }
85
+ let entry = this.#entries.get(id);
86
+ if (!entry) {
87
+ entry = { component: undefined, target: partialJson, revealed: 0, rawInput };
88
+ this.#entries.set(id, entry);
89
+ } else {
90
+ // Streams only append; a non-prefix target means a rewind — snap into range.
91
+ if (!partialJson.startsWith(entry.target)) {
92
+ entry.revealed = Math.min(entry.revealed, partialJson.length);
93
+ }
94
+ entry.target = partialJson;
95
+ }
96
+ entry.revealed = clampSliceEnd(entry.target, entry.revealed);
97
+ this.#syncTimer();
98
+ return buildDisplayArgs(entry.target.slice(0, entry.revealed), entry.rawInput);
99
+ }
100
+
101
+ /** Attach the component future ticks push frames into. */
102
+ bind(id: string, component: ToolArgsRevealComponent): void {
103
+ const entry = this.#entries.get(id);
104
+ if (entry) entry.component = component;
105
+ }
106
+
107
+ /** Final arguments arrived (the JSON closed): drop the reveal so the
108
+ * caller's final-args render wins immediately, mirroring how assistant
109
+ * text snaps to the full message at message_end. */
110
+ finish(id: string): void {
111
+ this.#entries.delete(id);
112
+ if (this.#entries.size === 0) this.#stopTimer();
113
+ }
114
+
115
+ /** Snap every live entry to its full received stream and clear. Used at
116
+ * message_end (abort/error mid-stream) so sealed components freeze showing
117
+ * everything that arrived rather than a mid-reveal prefix. */
118
+ flushAll(): void {
119
+ for (const [id, entry] of this.#entries) {
120
+ if (entry.component && entry.revealed < entry.target.length) {
121
+ entry.component.updateArgs(buildDisplayArgs(entry.target, entry.rawInput), id);
122
+ }
123
+ }
124
+ this.#entries.clear();
125
+ this.#stopTimer();
126
+ }
127
+
128
+ /** Clear without pushing (teardown). */
129
+ stop(): void {
130
+ this.#entries.clear();
131
+ this.#stopTimer();
132
+ }
133
+
134
+ #syncTimer(): void {
135
+ for (const entry of this.#entries.values()) {
136
+ if (entry.revealed < entry.target.length) {
137
+ this.#startTimer();
138
+ return;
139
+ }
140
+ }
141
+ this.#stopTimer();
142
+ }
143
+
144
+ #startTimer(): void {
145
+ if (this.#timer) return;
146
+ this.#timer = setInterval(() => {
147
+ this.#tick();
148
+ }, STREAMING_REVEAL_FRAME_MS);
149
+ this.#timer.unref?.();
150
+ }
151
+
152
+ #stopTimer(): void {
153
+ if (!this.#timer) return;
154
+ clearInterval(this.#timer);
155
+ this.#timer = undefined;
156
+ }
157
+
158
+ #tick(): void {
159
+ let advanced = false;
160
+ for (const [id, entry] of this.#entries) {
161
+ const backlog = entry.target.length - entry.revealed;
162
+ if (backlog <= 0 || !entry.component) continue;
163
+ entry.revealed = clampSliceEnd(entry.target, entry.revealed + nextStep(backlog));
164
+ entry.component.updateArgs(buildDisplayArgs(entry.target.slice(0, entry.revealed), entry.rawInput), id);
165
+ advanced = true;
166
+ }
167
+ if (advanced) {
168
+ this.#requestRender();
169
+ } else {
170
+ // Every entry caught up (or unbound); setTarget restarts on growth.
171
+ this.#stopTimer();
172
+ }
173
+ }
174
+ }
@@ -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;
@@ -495,8 +496,12 @@ export class InteractiveMode implements InteractiveModeContext {
495
496
  }
496
497
 
497
498
  playWelcomeIntro(): void {
498
- this.#welcomeComponent?.playIntro(() => this.ui.requestRender());
499
+ const welcome = this.#welcomeComponent;
500
+ // Component-scoped: the intro only mutates the welcome box's own rows,
501
+ // so a resumed long transcript is not re-walked per animation frame.
502
+ welcome?.playIntro(() => this.ui.requestComponentRender(welcome));
499
503
  }
504
+
500
505
  async init(options: InteractiveModeInitOptions = {}): Promise<void> {
501
506
  if (this.isInitialized) return;
502
507
 
@@ -1049,6 +1054,7 @@ export class InteractiveMode implements InteractiveModeContext {
1049
1054
  separator: settings.get("statusLine.separator"),
1050
1055
  showHookStatus: settings.get("statusLine.showHookStatus"),
1051
1056
  sessionAccent: settings.get("statusLine.sessionAccent"),
1057
+ transparent: settings.get("statusLine.transparent"),
1052
1058
  segmentOptions: settings.get("statusLine.segmentOptions"),
1053
1059
  });
1054
1060
  }
@@ -1088,7 +1094,9 @@ export class InteractiveMode implements InteractiveModeContext {
1088
1094
 
1089
1095
  rebuildChatFromMessages(): void {
1090
1096
  this.chatContainer.clear();
1091
- const context = this.session.buildDisplaySessionContext();
1097
+ // Full-history transcript: compactions render as inline dividers instead
1098
+ // of restarting the visible conversation (the LLM context still resets).
1099
+ const context = this.session.buildTranscriptSessionContext();
1092
1100
  this.renderSessionContext(context);
1093
1101
  }
1094
1102
 
@@ -2880,11 +2888,8 @@ export class InteractiveMode implements InteractiveModeContext {
2880
2888
  this.#uiHelpers.renderSessionContext(sessionContext, options);
2881
2889
  }
2882
2890
 
2883
- renderInitialMessages(
2884
- prebuiltContext?: SessionContext,
2885
- options?: { preserveExistingChat?: boolean; clearTerminalHistory?: boolean },
2886
- ): void {
2887
- this.#uiHelpers.renderInitialMessages(prebuiltContext, options);
2891
+ renderInitialMessages(options?: { preserveExistingChat?: boolean; clearTerminalHistory?: boolean }): void {
2892
+ this.#uiHelpers.renderInitialMessages(options);
2888
2893
  }
2889
2894
 
2890
2895
  getUserMessageText(message: Message): string {
@@ -3036,7 +3041,9 @@ export class InteractiveMode implements InteractiveModeContext {
3036
3041
  this.#voiceAnimationInterval = setInterval(() => {
3037
3042
  this.#voiceHue = (this.#voiceHue + 8) % 360;
3038
3043
  this.#updateMicIcon();
3039
- this.ui.requestRender();
3044
+ // Component-scoped: the hue sweep only recolors the editor's cursor
3045
+ // glyph, so the transcript subtree is reused per animation frame.
3046
+ this.ui.requestComponentRender(this.editor);
3040
3047
  }, 60);
3041
3048
  }
3042
3049
 
@@ -3068,13 +3075,8 @@ export class InteractiveMode implements InteractiveModeContext {
3068
3075
  await this.#selectorController.showDebugSelector();
3069
3076
  }
3070
3077
 
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);
3078
+ showAgentHub(): void {
3079
+ this.#selectorController.showAgentHub(this.#observerRegistry);
3078
3080
  }
3079
3081
 
3080
3082
  resetObserverRegistry(): void {
@@ -108,6 +108,7 @@ export type SymbolKey =
108
108
  | "icon.time"
109
109
  | "icon.pi"
110
110
  | "icon.agents"
111
+ | "icon.job"
111
112
  | "icon.cache"
112
113
  | "icon.input"
113
114
  | "icon.output"
@@ -129,6 +130,8 @@ export type SymbolKey =
129
130
  | "icon.extensionInstruction"
130
131
  // STT
131
132
  | "icon.mic"
133
+ // Compaction divider
134
+ | "icon.camera"
132
135
  // Thinking Levels
133
136
  | "thinking.minimal"
134
137
  | "thinking.low"
@@ -220,7 +223,8 @@ export type SymbolKey =
220
223
  | "tool.resolve"
221
224
  | "tool.review"
222
225
  | "tool.inspectImage"
223
- | "tool.goal";
226
+ | "tool.goal"
227
+ | "tool.irc";
224
228
 
225
229
  type SymbolMap = Record<SymbolKey, string>;
226
230
 
@@ -301,6 +305,7 @@ const UNICODE_SYMBOLS: SymbolMap = {
301
305
  "icon.time": "⏱",
302
306
  "icon.pi": "π",
303
307
  "icon.agents": "👥",
308
+ "icon.job": "⚙",
304
309
  "icon.cache": "💾",
305
310
  "icon.input": "⤵",
306
311
  "icon.output": "⤴",
@@ -322,13 +327,15 @@ const UNICODE_SYMBOLS: SymbolMap = {
322
327
  "icon.extensionInstruction": "📘",
323
328
  // STT
324
329
  "icon.mic": "🎤",
330
+ // Compaction divider
331
+ "icon.camera": "📷",
325
332
  // Thinking levels
326
333
  "thinking.minimal": "◔ min",
327
334
  "thinking.low": "◑ low",
328
335
  "thinking.medium": "◒ med",
329
336
  "thinking.high": "◕ high",
330
337
  "thinking.xhigh": "◉ xhigh",
331
- "thinking.autoPending": "▣?",
338
+ "thinking.autoPending": "",
332
339
  // Checkboxes
333
340
  "checkbox.checked": "☑",
334
341
  "checkbox.unchecked": "☐",
@@ -414,6 +421,7 @@ const UNICODE_SYMBOLS: SymbolMap = {
414
421
  "tool.review": "◉",
415
422
  "tool.inspectImage": "🖼",
416
423
  "tool.goal": "◎",
424
+ "tool.irc": "✉",
417
425
  };
418
426
 
419
427
  const NERD_SYMBOLS: SymbolMap = {
@@ -561,6 +569,8 @@ const NERD_SYMBOLS: SymbolMap = {
561
569
  "icon.pi": "\ue22c",
562
570
  // pick:  | alt: 
563
571
  "icon.agents": "\uf0c0",
572
+ // pick: (nf-fa-gear) | alt: ⚙
573
+ "icon.job": "\uf013",
564
574
  // pick:  | alt:  
565
575
  "icon.cache": "\uf1c0",
566
576
  // pick:  | alt:  →
@@ -599,6 +609,8 @@ const NERD_SYMBOLS: SymbolMap = {
599
609
  "icon.extensionInstruction": "\uf02d",
600
610
  // STT - fa-microphone
601
611
  "icon.mic": "\uf130",
612
+ // Compaction divider - fa-camera-retro
613
+ "icon.camera": "\uf083",
602
614
  // Thinking Levels - emoji labels
603
615
  // pick: 🤨 min | alt:  min  min
604
616
  "thinking.minimal": "\u{F0E7} min",
@@ -610,8 +622,8 @@ const NERD_SYMBOLS: SymbolMap = {
610
622
  "thinking.high": "\u{F111} high",
611
623
  // pick: 🧠 xhi | alt:  xhi  xhi
612
624
  "thinking.xhigh": "\u{F06D} xhi",
613
- // pick: 󰞋 (nf-md-help_box) | alt: [?]
614
- "thinking.autoPending": "\u{f078b}",
625
+ // pick: (fa-circle-o-notch) | alt: 󰂼 (nf-md-cached) ⟳
626
+ "thinking.autoPending": "\uf1ce",
615
627
  // Checkboxes
616
628
  // pick:  | alt:  
617
629
  "checkbox.checked": "\uf14a",
@@ -708,6 +720,7 @@ const NERD_SYMBOLS: SymbolMap = {
708
720
  "tool.review": "\uEA70",
709
721
  "tool.inspectImage": "\uEAEA",
710
722
  "tool.goal": "\uEBF8",
723
+ "tool.irc": "\uF086",
711
724
  };
712
725
 
713
726
  const ASCII_SYMBOLS: SymbolMap = {
@@ -787,6 +800,7 @@ const ASCII_SYMBOLS: SymbolMap = {
787
800
  "icon.time": "t:",
788
801
  "icon.pi": "pi",
789
802
  "icon.agents": "AG",
803
+ "icon.job": "bg",
790
804
  "icon.cache": "cache",
791
805
  "icon.input": "in:",
792
806
  "icon.output": "out:",
@@ -808,13 +822,15 @@ const ASCII_SYMBOLS: SymbolMap = {
808
822
  "icon.extensionInstruction": "IN",
809
823
  // STT
810
824
  "icon.mic": "MIC",
825
+ // Compaction divider
826
+ "icon.camera": "[o]",
811
827
  // Thinking Levels
812
828
  "thinking.minimal": "[min]",
813
829
  "thinking.low": "[low]",
814
830
  "thinking.medium": "[med]",
815
831
  "thinking.high": "[high]",
816
832
  "thinking.xhigh": "[xhi]",
817
- "thinking.autoPending": "[?]",
833
+ "thinking.autoPending": "[~]",
818
834
  // Checkboxes
819
835
  "checkbox.checked": "[x]",
820
836
  "checkbox.unchecked": "[ ]",
@@ -898,6 +914,7 @@ const ASCII_SYMBOLS: SymbolMap = {
898
914
  "tool.review": "rev",
899
915
  "tool.inspectImage": "[i]",
900
916
  "tool.goal": "(o)",
917
+ "tool.irc": "irc",
901
918
  };
902
919
 
903
920
  const SYMBOL_PRESETS: Record<SymbolPreset, SymbolMap> = {
@@ -1666,6 +1683,7 @@ export class Theme {
1666
1683
  time: this.#symbols["icon.time"],
1667
1684
  pi: this.#symbols["icon.pi"],
1668
1685
  agents: this.#symbols["icon.agents"],
1686
+ job: this.#symbols["icon.job"],
1669
1687
  cache: this.#symbols["icon.cache"],
1670
1688
  input: this.#symbols["icon.input"],
1671
1689
  output: this.#symbols["icon.output"],
@@ -1686,6 +1704,7 @@ export class Theme {
1686
1704
  extensionContextFile: this.#symbols["icon.extensionContextFile"],
1687
1705
  extensionInstruction: this.#symbols["icon.extensionInstruction"],
1688
1706
  mic: this.#symbols["icon.mic"],
1707
+ camera: this.#symbols["icon.camera"],
1689
1708
  };
1690
1709
  }
1691
1710
 
@@ -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|…`.