@bastani/atomic 0.8.4 → 0.8.5

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 (245) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +24 -23
  3. package/dist/builtin/intercom/README.md +5 -5
  4. package/dist/builtin/intercom/index.ts +1 -1
  5. package/dist/builtin/intercom/package.json +1 -1
  6. package/dist/builtin/intercom/ui/compose.ts +19 -1
  7. package/dist/builtin/intercom/ui/session-list.ts +19 -1
  8. package/dist/builtin/mcp/README.md +3 -3
  9. package/dist/builtin/mcp/commands.ts +1 -1
  10. package/dist/builtin/mcp/host-html-template.ts +1 -1
  11. package/dist/builtin/mcp/mcp-panel.ts +14 -14
  12. package/dist/builtin/mcp/mcp-setup-panel.ts +4 -4
  13. package/dist/builtin/mcp/package.json +1 -1
  14. package/dist/builtin/mcp/tool-result-renderer.ts +1 -1
  15. package/dist/builtin/subagents/README.md +3 -3
  16. package/dist/builtin/subagents/package.json +1 -1
  17. package/dist/builtin/subagents/src/tui/render.ts +1844 -1062
  18. package/dist/builtin/web-access/README.md +1 -1
  19. package/dist/builtin/web-access/curator-page.ts +2 -2
  20. package/dist/builtin/web-access/index.ts +1 -1
  21. package/dist/builtin/web-access/package.json +1 -1
  22. package/dist/builtin/workflows/README.md +34 -7
  23. package/dist/builtin/workflows/builtin/deep-research-codebase.ts +23 -4
  24. package/dist/builtin/workflows/builtin/ralph.ts +1 -1
  25. package/dist/builtin/workflows/package.json +1 -1
  26. package/dist/builtin/workflows/skills/workflow/SKILL.md +75 -16
  27. package/dist/builtin/workflows/skills/workflow/references/running-workflows.md +34 -11
  28. package/dist/builtin/workflows/skills/workflow/references/sdk-authoring.md +111 -20
  29. package/dist/builtin/workflows/src/extension/discovery.ts +32 -4
  30. package/dist/builtin/workflows/src/extension/index.ts +347 -63
  31. package/dist/builtin/workflows/src/extension/render-call.ts +3 -1
  32. package/dist/builtin/workflows/src/extension/render-result.ts +7 -0
  33. package/dist/builtin/workflows/src/extension/runtime.ts +4 -2
  34. package/dist/builtin/workflows/src/extension/wiring.ts +32 -8
  35. package/dist/builtin/workflows/src/extension/workflow-schema.ts +36 -14
  36. package/dist/builtin/workflows/src/runs/background/runner.ts +2 -2
  37. package/dist/builtin/workflows/src/runs/background/status.ts +89 -0
  38. package/dist/builtin/workflows/src/runs/foreground/executor.ts +338 -78
  39. package/dist/builtin/workflows/src/runs/foreground/stage-control-registry.ts +2 -0
  40. package/dist/builtin/workflows/src/runs/foreground/stage-runner.ts +55 -7
  41. package/dist/builtin/workflows/src/runs/shared/workflow-runner.ts +146 -10
  42. package/dist/builtin/workflows/src/shared/store.ts +29 -0
  43. package/dist/builtin/workflows/src/shared/types.ts +25 -4
  44. package/dist/builtin/workflows/src/tui/graph-canvas.ts +69 -2
  45. package/dist/builtin/workflows/src/tui/graph-view.ts +97 -182
  46. package/dist/builtin/workflows/src/tui/header.ts +36 -20
  47. package/dist/builtin/workflows/src/tui/inline-form-card.ts +129 -46
  48. package/dist/builtin/workflows/src/tui/inline-form-editor.ts +111 -36
  49. package/dist/builtin/workflows/src/tui/inputs-picker.ts +311 -91
  50. package/dist/builtin/workflows/src/tui/layout.ts +1 -1
  51. package/dist/builtin/workflows/src/tui/node-card.ts +66 -37
  52. package/dist/builtin/workflows/src/tui/overlay-adapter.ts +20 -6
  53. package/dist/builtin/workflows/src/tui/prompt-card.ts +262 -85
  54. package/dist/builtin/workflows/src/tui/run-detail.ts +50 -31
  55. package/dist/builtin/workflows/src/tui/session-confirm.ts +21 -14
  56. package/dist/builtin/workflows/src/tui/session-picker.ts +35 -26
  57. package/dist/builtin/workflows/src/tui/stage-chat-view.ts +531 -960
  58. package/dist/builtin/workflows/src/tui/status-helpers.ts +6 -0
  59. package/dist/builtin/workflows/src/tui/status-list.ts +8 -4
  60. package/dist/builtin/workflows/src/tui/store-widget-installer.ts +7 -2
  61. package/dist/builtin/workflows/src/tui/switcher.ts +55 -25
  62. package/dist/builtin/workflows/src/tui/workflow-attach-pane.ts +33 -1
  63. package/dist/builtin/workflows/src/tui/workflow-list.ts +10 -6
  64. package/dist/cli/args.d.ts.map +1 -1
  65. package/dist/cli/args.js +1 -1
  66. package/dist/cli/args.js.map +1 -1
  67. package/dist/config.d.ts.map +1 -1
  68. package/dist/config.js +20 -6
  69. package/dist/config.js.map +1 -1
  70. package/dist/core/agent-session-services.d.ts +3 -3
  71. package/dist/core/agent-session-services.d.ts.map +1 -1
  72. package/dist/core/agent-session-services.js.map +1 -1
  73. package/dist/core/agent-session.d.ts +7 -7
  74. package/dist/core/agent-session.d.ts.map +1 -1
  75. package/dist/core/agent-session.js.map +1 -1
  76. package/dist/core/compaction/branch-summarization.d.ts +2 -2
  77. package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  78. package/dist/core/compaction/branch-summarization.js.map +1 -1
  79. package/dist/core/compaction/compaction.d.ts +3 -3
  80. package/dist/core/compaction/compaction.d.ts.map +1 -1
  81. package/dist/core/compaction/compaction.js.map +1 -1
  82. package/dist/core/export-html/tool-renderer.d.ts.map +1 -1
  83. package/dist/core/export-html/tool-renderer.js.map +1 -1
  84. package/dist/core/extensions/loader.d.ts +3 -2
  85. package/dist/core/extensions/loader.d.ts.map +1 -1
  86. package/dist/core/extensions/loader.js +24 -12
  87. package/dist/core/extensions/loader.js.map +1 -1
  88. package/dist/core/extensions/runner.d.ts.map +1 -1
  89. package/dist/core/extensions/runner.js +6 -0
  90. package/dist/core/extensions/runner.js.map +1 -1
  91. package/dist/core/extensions/types.d.ts +28 -17
  92. package/dist/core/extensions/types.d.ts.map +1 -1
  93. package/dist/core/extensions/types.js.map +1 -1
  94. package/dist/core/package-manager.d.ts +1 -0
  95. package/dist/core/package-manager.d.ts.map +1 -1
  96. package/dist/core/package-manager.js +65 -28
  97. package/dist/core/package-manager.js.map +1 -1
  98. package/dist/core/resource-loader.d.ts.map +1 -1
  99. package/dist/core/resource-loader.js +13 -5
  100. package/dist/core/resource-loader.js.map +1 -1
  101. package/dist/core/sdk.d.ts +3 -3
  102. package/dist/core/sdk.d.ts.map +1 -1
  103. package/dist/core/sdk.js.map +1 -1
  104. package/dist/core/session-manager.d.ts.map +1 -1
  105. package/dist/core/session-manager.js +1 -1
  106. package/dist/core/session-manager.js.map +1 -1
  107. package/dist/core/settings-manager.d.ts +2 -0
  108. package/dist/core/settings-manager.d.ts.map +1 -1
  109. package/dist/core/settings-manager.js.map +1 -1
  110. package/dist/core/slash-commands.d.ts.map +1 -1
  111. package/dist/core/slash-commands.js +1 -1
  112. package/dist/core/slash-commands.js.map +1 -1
  113. package/dist/core/system-prompt.d.ts.map +1 -1
  114. package/dist/core/system-prompt.js +5 -3
  115. package/dist/core/system-prompt.js.map +1 -1
  116. package/dist/core/tools/ask-user-question/view/components/preview/preview-block-renderer.d.ts +1 -1
  117. package/dist/core/tools/ask-user-question/view/components/preview/preview-block-renderer.d.ts.map +1 -1
  118. package/dist/core/tools/ask-user-question/view/components/preview/preview-block-renderer.js +1 -1
  119. package/dist/core/tools/ask-user-question/view/components/preview/preview-block-renderer.js.map +1 -1
  120. package/dist/core/tools/ask-user-question/view/dialog-builder.d.ts +8 -8
  121. package/dist/core/tools/ask-user-question/view/dialog-builder.d.ts.map +1 -1
  122. package/dist/core/tools/ask-user-question/view/dialog-builder.js +6 -6
  123. package/dist/core/tools/ask-user-question/view/dialog-builder.js.map +1 -1
  124. package/dist/core/tools/bash.d.ts.map +1 -1
  125. package/dist/core/tools/bash.js +1 -1
  126. package/dist/core/tools/bash.js.map +1 -1
  127. package/dist/core/tools/find.d.ts.map +1 -1
  128. package/dist/core/tools/find.js +1 -1
  129. package/dist/core/tools/find.js.map +1 -1
  130. package/dist/core/tools/grep.d.ts.map +1 -1
  131. package/dist/core/tools/grep.js +7 -4
  132. package/dist/core/tools/grep.js.map +1 -1
  133. package/dist/core/tools/index.d.ts +3 -2
  134. package/dist/core/tools/index.d.ts.map +1 -1
  135. package/dist/core/tools/index.js.map +1 -1
  136. package/dist/core/tools/ls.d.ts.map +1 -1
  137. package/dist/core/tools/ls.js +3 -2
  138. package/dist/core/tools/ls.js.map +1 -1
  139. package/dist/core/tools/read.d.ts.map +1 -1
  140. package/dist/core/tools/read.js +2 -2
  141. package/dist/core/tools/read.js.map +1 -1
  142. package/dist/core/tools/render-utils.d.ts +2 -1
  143. package/dist/core/tools/render-utils.d.ts.map +1 -1
  144. package/dist/core/tools/render-utils.js.map +1 -1
  145. package/dist/core/tools/todos.d.ts.map +1 -1
  146. package/dist/core/tools/todos.js +1 -1
  147. package/dist/core/tools/todos.js.map +1 -1
  148. package/dist/core/tools/tool-definition-wrapper.d.ts +4 -3
  149. package/dist/core/tools/tool-definition-wrapper.d.ts.map +1 -1
  150. package/dist/core/tools/tool-definition-wrapper.js.map +1 -1
  151. package/dist/core/tools/write.d.ts.map +1 -1
  152. package/dist/core/tools/write.js +1 -1
  153. package/dist/core/tools/write.js.map +1 -1
  154. package/dist/index.d.ts +2 -1
  155. package/dist/index.d.ts.map +1 -1
  156. package/dist/index.js +2 -1
  157. package/dist/index.js.map +1 -1
  158. package/dist/main.d.ts.map +1 -1
  159. package/dist/main.js +2 -2
  160. package/dist/main.js.map +1 -1
  161. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  162. package/dist/modes/interactive/components/assistant-message.js +3 -3
  163. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  164. package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
  165. package/dist/modes/interactive/components/bash-execution.js +3 -3
  166. package/dist/modes/interactive/components/bash-execution.js.map +1 -1
  167. package/dist/modes/interactive/components/branch-summary-message.d.ts.map +1 -1
  168. package/dist/modes/interactive/components/branch-summary-message.js +1 -1
  169. package/dist/modes/interactive/components/branch-summary-message.js.map +1 -1
  170. package/dist/modes/interactive/components/chat-message-renderer.d.ts +2 -1
  171. package/dist/modes/interactive/components/chat-message-renderer.d.ts.map +1 -1
  172. package/dist/modes/interactive/components/chat-message-renderer.js.map +1 -1
  173. package/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -1
  174. package/dist/modes/interactive/components/compaction-summary-message.js +1 -1
  175. package/dist/modes/interactive/components/compaction-summary-message.js.map +1 -1
  176. package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
  177. package/dist/modes/interactive/components/config-selector.js +1 -1
  178. package/dist/modes/interactive/components/config-selector.js.map +1 -1
  179. package/dist/modes/interactive/components/custom-editor.d.ts +3 -0
  180. package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
  181. package/dist/modes/interactive/components/custom-editor.js +13 -3
  182. package/dist/modes/interactive/components/custom-editor.js.map +1 -1
  183. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  184. package/dist/modes/interactive/components/footer.js +1 -1
  185. package/dist/modes/interactive/components/footer.js.map +1 -1
  186. package/dist/modes/interactive/components/index.d.ts +2 -1
  187. package/dist/modes/interactive/components/index.d.ts.map +1 -1
  188. package/dist/modes/interactive/components/index.js +2 -1
  189. package/dist/modes/interactive/components/index.js.map +1 -1
  190. package/dist/modes/interactive/components/keybinding-hints.d.ts +1 -0
  191. package/dist/modes/interactive/components/keybinding-hints.d.ts.map +1 -1
  192. package/dist/modes/interactive/components/keybinding-hints.js +47 -5
  193. package/dist/modes/interactive/components/keybinding-hints.js.map +1 -1
  194. package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  195. package/dist/modes/interactive/components/login-dialog.js +5 -5
  196. package/dist/modes/interactive/components/login-dialog.js.map +1 -1
  197. package/dist/modes/interactive/components/model-selector.d.ts +3 -3
  198. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  199. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  200. package/dist/modes/interactive/components/scoped-models-selector.d.ts +2 -2
  201. package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
  202. package/dist/modes/interactive/components/scoped-models-selector.js +7 -7
  203. package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
  204. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  205. package/dist/modes/interactive/components/session-selector.js +8 -8
  206. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  207. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  208. package/dist/modes/interactive/components/settings-selector.js +3 -3
  209. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  210. package/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
  211. package/dist/modes/interactive/components/skill-invocation-message.js +2 -2
  212. package/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
  213. package/dist/modes/interactive/components/tool-execution.d.ts +10 -12
  214. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  215. package/dist/modes/interactive/components/tool-execution.js +3 -3
  216. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  217. package/dist/modes/interactive/components/working-status.d.ts +25 -0
  218. package/dist/modes/interactive/components/working-status.d.ts.map +1 -0
  219. package/dist/modes/interactive/components/working-status.js +28 -0
  220. package/dist/modes/interactive/components/working-status.js.map +1 -0
  221. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  222. package/dist/modes/interactive/interactive-mode.js +8 -7
  223. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  224. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  225. package/dist/modes/rpc/rpc-mode.js +8 -0
  226. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  227. package/dist/modes/rpc/rpc-types.d.ts +5 -5
  228. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  229. package/dist/modes/rpc/rpc-types.js.map +1 -1
  230. package/dist/utils/tools-manager.d.ts.map +1 -1
  231. package/dist/utils/tools-manager.js.map +1 -1
  232. package/docs/development.md +2 -2
  233. package/docs/extensions.md +7 -7
  234. package/docs/packages.md +11 -8
  235. package/docs/quickstart.md +2 -2
  236. package/docs/rpc.md +1 -1
  237. package/docs/sdk.md +14 -11
  238. package/docs/session-format.md +1 -1
  239. package/docs/sessions.md +10 -10
  240. package/docs/settings.md +1 -1
  241. package/docs/terminal-setup.md +9 -9
  242. package/docs/tmux.md +10 -10
  243. package/docs/tui.md +2 -2
  244. package/docs/usage.md +9 -9
  245. package/package.json +6 -1
@@ -5,83 +5,106 @@
5
5
  import * as path from "node:path";
6
6
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
7
7
  import { getMarkdownTheme, type ExtensionContext } from "@bastani/atomic";
8
- import { Container, Markdown, Spacer, Text, visibleWidth, type Component } from "@earendil-works/pi-tui";
9
8
  import {
10
- type AgentProgress,
11
- type AsyncJobState,
12
- type AsyncJobStep,
13
- type AsyncParallelGroupStatus,
14
- type Details,
15
- MAX_WIDGET_JOBS,
16
- WIDGET_KEY,
9
+ Container,
10
+ Markdown,
11
+ Spacer,
12
+ Text,
13
+ visibleWidth,
14
+ type Component,
15
+ } from "@earendil-works/pi-tui";
16
+ import {
17
+ type AgentProgress,
18
+ type AsyncJobState,
19
+ type AsyncJobStep,
20
+ type AsyncParallelGroupStatus,
21
+ type Details,
22
+ MAX_WIDGET_JOBS,
23
+ WIDGET_KEY,
17
24
  } from "../shared/types.ts";
18
- import { formatTokens, formatUsage, formatDuration, formatModelThinking, formatToolCall, shortenPath } from "../shared/formatters.ts";
19
- import { getDisplayItems, getLastActivity, getSingleResultOutput } from "../shared/utils.ts";
25
+ import {
26
+ formatTokens,
27
+ formatUsage,
28
+ formatDuration,
29
+ formatModelThinking,
30
+ formatToolCall,
31
+ shortenPath,
32
+ } from "../shared/formatters.ts";
33
+ import {
34
+ getDisplayItems,
35
+ getLastActivity,
36
+ getSingleResultOutput,
37
+ } from "../shared/utils.ts";
20
38
  import { flatToLogicalStepIndex } from "../runs/background/parallel-groups.ts";
21
- import { aggregateStepStatus, formatActivityLabel, formatAgentRunningLabel, formatParallelOutcome } from "../shared/status-format.ts";
39
+ import {
40
+ aggregateStepStatus,
41
+ formatActivityLabel,
42
+ formatAgentRunningLabel,
43
+ formatParallelOutcome,
44
+ } from "../shared/status-format.ts";
22
45
 
23
46
  type Theme = ExtensionContext["ui"]["theme"];
24
47
 
25
48
  function getTermWidth(): number {
26
- return process.stdout.columns || 120;
49
+ return process.stdout.columns || 120;
27
50
  }
28
51
 
29
52
  const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
30
53
 
31
54
  /**
32
55
  * Truncate a line to maxWidth, preserving ANSI styling through the ellipsis.
33
- *
56
+ *
34
57
  * pi-tui's truncateToWidth adds \x1b[0m before ellipsis which resets all styling,
35
58
  * causing background color bleed in the TUI. This implementation tracks active
36
59
  * ANSI styles and re-applies them before the ellipsis.
37
- *
60
+ *
38
61
  * Uses Intl.Segmenter for proper Unicode/emoji handling (not char-by-char).
39
62
  */
40
63
  function truncLine(text: string, maxWidth: number): string {
41
- if (visibleWidth(text) <= maxWidth) return text;
42
-
43
- const targetWidth = maxWidth - 1;
44
- let result = "";
45
- let currentWidth = 0;
46
- let activeStyles: string[] = [];
47
- let i = 0;
48
-
49
- while (i < text.length) {
50
- const ansiMatch = text.slice(i).match(/^\x1b\[[0-9;]*m/);
51
- if (ansiMatch) {
52
- const code = ansiMatch[0];
53
- result += code;
54
-
55
- if (code === "\x1b[0m" || code === "\x1b[m") {
56
- activeStyles = [];
57
- } else {
58
- activeStyles.push(code);
59
- }
60
- i += code.length;
61
- continue;
62
- }
63
-
64
- let end = i;
65
- while (end < text.length && !text.slice(end).match(/^\x1b\[[0-9;]*m/)) {
66
- end++;
67
- }
68
-
69
- const textPortion = text.slice(i, end);
70
- for (const seg of segmenter.segment(textPortion)) {
71
- const grapheme = seg.segment;
72
- const graphemeWidth = visibleWidth(grapheme);
73
-
74
- if (currentWidth + graphemeWidth > targetWidth) {
75
- return result + activeStyles.join("") + "…";
76
- }
77
-
78
- result += grapheme;
79
- currentWidth += graphemeWidth;
80
- }
81
- i = end;
82
- }
83
-
84
- return result + activeStyles.join("") + "…";
64
+ if (visibleWidth(text) <= maxWidth) return text;
65
+
66
+ const targetWidth = maxWidth - 1;
67
+ let result = "";
68
+ let currentWidth = 0;
69
+ let activeStyles: string[] = [];
70
+ let i = 0;
71
+
72
+ while (i < text.length) {
73
+ const ansiMatch = text.slice(i).match(/^\x1b\[[0-9;]*m/);
74
+ if (ansiMatch) {
75
+ const code = ansiMatch[0];
76
+ result += code;
77
+
78
+ if (code === "\x1b[0m" || code === "\x1b[m") {
79
+ activeStyles = [];
80
+ } else {
81
+ activeStyles.push(code);
82
+ }
83
+ i += code.length;
84
+ continue;
85
+ }
86
+
87
+ let end = i;
88
+ while (end < text.length && !text.slice(end).match(/^\x1b\[[0-9;]*m/)) {
89
+ end++;
90
+ }
91
+
92
+ const textPortion = text.slice(i, end);
93
+ for (const seg of segmenter.segment(textPortion)) {
94
+ const grapheme = seg.segment;
95
+ const graphemeWidth = visibleWidth(grapheme);
96
+
97
+ if (currentWidth + graphemeWidth > targetWidth) {
98
+ return result + activeStyles.join("") + "…";
99
+ }
100
+
101
+ result += grapheme;
102
+ currentWidth += graphemeWidth;
103
+ }
104
+ i = end;
105
+ }
106
+
107
+ return result + activeStyles.join("") + "…";
85
108
  }
86
109
 
87
110
  const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
@@ -91,1167 +114,1926 @@ let widgetTimer: ReturnType<typeof setInterval> | undefined;
91
114
  let latestWidgetCtx: ExtensionContext | undefined;
92
115
  let latestWidgetJobs: AsyncJobState[] = [];
93
116
 
94
- const resultAnimationTimers = new Map<ReturnType<typeof setInterval>, ResultAnimationContext["state"]>();
95
- const outputActivityCache = new Map<string, { checkedAt: number; text: string }>();
96
- const STALE_EXTENSION_CONTEXT_MESSAGE = "This extension ctx is stale after session replacement or reload";
117
+ const resultAnimationTimers = new Map<
118
+ ReturnType<typeof setInterval>,
119
+ ResultAnimationContext["state"]
120
+ >();
121
+ const outputActivityCache = new Map<
122
+ string,
123
+ { checkedAt: number; text: string }
124
+ >();
125
+ const STALE_EXTENSION_CONTEXT_MESSAGE =
126
+ "This extension ctx is stale after session replacement or reload";
97
127
 
98
128
  interface ResultAnimationContext {
99
- state: { subagentResultAnimationTimer?: ReturnType<typeof setInterval> };
100
- invalidate: () => void;
129
+ state: { subagentResultAnimationTimer?: ReturnType<typeof setInterval> };
130
+ invalidate: () => void;
101
131
  }
102
132
 
103
133
  function spinnerFrame(): string {
104
- return SPINNER[Math.floor(Date.now() / WIDGET_ANIMATION_MS) % SPINNER.length]!;
134
+ return SPINNER[
135
+ Math.floor(Date.now() / WIDGET_ANIMATION_MS) % SPINNER.length
136
+ ]!;
105
137
  }
106
138
 
107
139
  function isStaleExtensionContextError(error: unknown): boolean {
108
- if (!(error instanceof Error)) return false;
109
- return error.message.includes(STALE_EXTENSION_CONTEXT_MESSAGE);
140
+ if (!(error instanceof Error)) return false;
141
+ return error.message.includes(STALE_EXTENSION_CONTEXT_MESSAGE);
110
142
  }
111
143
 
112
144
  function resultIsRunning(result: AgentToolResult<Details>): boolean {
113
- return result.details?.progress?.some((entry) => entry.status === "running")
114
- || result.details?.results.some((entry) => entry.progress?.status === "running")
115
- || false;
145
+ return (
146
+ result.details?.progress?.some((entry) => entry.status === "running") ||
147
+ result.details?.results.some(
148
+ (entry) => entry.progress?.status === "running",
149
+ ) ||
150
+ false
151
+ );
116
152
  }
117
153
 
118
154
  function stopResultAnimation(context: ResultAnimationContext): void {
119
- const timer = context.state.subagentResultAnimationTimer;
120
- if (!timer) return;
121
- clearInterval(timer);
122
- resultAnimationTimers.delete(timer);
123
- context.state.subagentResultAnimationTimer = undefined;
155
+ const timer = context.state.subagentResultAnimationTimer;
156
+ if (!timer) return;
157
+ clearInterval(timer);
158
+ resultAnimationTimers.delete(timer);
159
+ context.state.subagentResultAnimationTimer = undefined;
124
160
  }
125
161
 
126
- export function syncResultAnimation(result: AgentToolResult<Details>, context: ResultAnimationContext): void {
127
- if (!resultIsRunning(result)) {
128
- stopResultAnimation(context);
129
- return;
130
- }
131
- if (context.state.subagentResultAnimationTimer) return;
132
- const timer = setInterval(() => {
133
- try {
134
- context.invalidate();
135
- } catch (error) {
136
- if (!isStaleExtensionContextError(error)) throw error;
137
- stopResultAnimation(context);
138
- }
139
- }, WIDGET_ANIMATION_MS);
140
- timer.unref?.();
141
- context.state.subagentResultAnimationTimer = timer;
142
- resultAnimationTimers.set(timer, context.state);
162
+ export function syncResultAnimation(
163
+ result: AgentToolResult<Details>,
164
+ context: ResultAnimationContext,
165
+ ): void {
166
+ if (!resultIsRunning(result)) {
167
+ stopResultAnimation(context);
168
+ return;
169
+ }
170
+ if (context.state.subagentResultAnimationTimer) return;
171
+ const timer = setInterval(() => {
172
+ try {
173
+ context.invalidate();
174
+ } catch (error) {
175
+ if (!isStaleExtensionContextError(error)) throw error;
176
+ stopResultAnimation(context);
177
+ }
178
+ }, WIDGET_ANIMATION_MS);
179
+ timer.unref?.();
180
+ context.state.subagentResultAnimationTimer = timer;
181
+ resultAnimationTimers.set(timer, context.state);
143
182
  }
144
183
 
145
184
  function extractOutputTarget(task: string): string | undefined {
146
- const writeToMatch = task.match(/\[Write to:\s*([^\]\n]+)\]/i);
147
- if (writeToMatch?.[1]?.trim()) return writeToMatch[1].trim();
148
- const findingsMatch = task.match(/Write your findings to:\s*(\S+)/i);
149
- if (findingsMatch?.[1]?.trim()) return findingsMatch[1].trim();
150
- const outputMatch = task.match(/[Oo]utput(?:\s+to)?\s*:\s*(\S+)/i);
151
- if (outputMatch?.[1]?.trim()) return outputMatch[1].trim();
152
- return undefined;
185
+ const writeToMatch = task.match(/\[Write to:\s*([^\]\n]+)\]/i);
186
+ if (writeToMatch?.[1]?.trim()) return writeToMatch[1].trim();
187
+ const findingsMatch = task.match(/Write your findings to:\s*(\S+)/i);
188
+ if (findingsMatch?.[1]?.trim()) return findingsMatch[1].trim();
189
+ const outputMatch = task.match(/[Oo]utput(?:\s+to)?\s*:\s*(\S+)/i);
190
+ if (outputMatch?.[1]?.trim()) return outputMatch[1].trim();
191
+ return undefined;
153
192
  }
154
193
 
155
- function hasEmptyTextOutputWithoutOutputTarget(task: string, output: string): boolean {
156
- if (output.trim()) return false;
157
- return !extractOutputTarget(task);
194
+ function hasEmptyTextOutputWithoutOutputTarget(
195
+ task: string,
196
+ output: string,
197
+ ): boolean {
198
+ if (output.trim()) return false;
199
+ return !extractOutputTarget(task);
158
200
  }
159
201
 
160
202
  function getToolCallLines(
161
- result: Pick<Details["results"][number], "messages" | "toolCalls">,
162
- expanded: boolean,
203
+ result: Pick<Details["results"][number], "messages" | "toolCalls">,
204
+ expanded: boolean,
163
205
  ): string[] {
164
- if (result.messages) {
165
- return getDisplayItems(result.messages)
166
- .filter((item): item is { type: "tool"; name: string; args: Record<string, unknown> } => item.type === "tool")
167
- .map((item) => formatToolCall(item.name, item.args, expanded));
168
- }
169
- return result.toolCalls?.map((toolCall) => expanded ? toolCall.expandedText : toolCall.text) ?? [];
206
+ if (result.messages) {
207
+ return getDisplayItems(result.messages)
208
+ .filter(
209
+ (
210
+ item,
211
+ ): item is {
212
+ type: "tool";
213
+ name: string;
214
+ args: Record<string, unknown>;
215
+ } => item.type === "tool",
216
+ )
217
+ .map((item) => formatToolCall(item.name, item.args, expanded));
218
+ }
219
+ return (
220
+ result.toolCalls?.map((toolCall) =>
221
+ expanded ? toolCall.expandedText : toolCall.text,
222
+ ) ?? []
223
+ );
170
224
  }
171
225
 
172
-
173
- function formatCurrentToolLine(progress: Pick<AgentProgress, "currentTool" | "currentToolArgs" | "currentToolStartedAt">, availableWidth: number, expanded: boolean): string | undefined {
174
- if (!progress.currentTool) return undefined;
175
- const maxToolArgsLen = Math.max(50, availableWidth - 20);
176
- const toolArgsPreview = progress.currentToolArgs
177
- ? (expanded || progress.currentToolArgs.length <= maxToolArgsLen
178
- ? progress.currentToolArgs
179
- : `${progress.currentToolArgs.slice(0, maxToolArgsLen)}...`)
180
- : "";
181
- const durationSuffix = progress.currentToolStartedAt !== undefined
182
- ? ` | ${formatDuration(Math.max(0, Date.now() - progress.currentToolStartedAt))}`
183
- : "";
184
- return toolArgsPreview
185
- ? `${progress.currentTool}: ${toolArgsPreview}${durationSuffix}`
186
- : `${progress.currentTool}${durationSuffix}`;
226
+ function formatCurrentToolLine(
227
+ progress: Pick<
228
+ AgentProgress,
229
+ "currentTool" | "currentToolArgs" | "currentToolStartedAt"
230
+ >,
231
+ availableWidth: number,
232
+ expanded: boolean,
233
+ ): string | undefined {
234
+ if (!progress.currentTool) return undefined;
235
+ const maxToolArgsLen = Math.max(50, availableWidth - 20);
236
+ const toolArgsPreview = progress.currentToolArgs
237
+ ? expanded || progress.currentToolArgs.length <= maxToolArgsLen
238
+ ? progress.currentToolArgs
239
+ : `${progress.currentToolArgs.slice(0, maxToolArgsLen)}...`
240
+ : "";
241
+ const durationSuffix =
242
+ progress.currentToolStartedAt !== undefined
243
+ ? ` | ${formatDuration(Math.max(0, Date.now() - progress.currentToolStartedAt))}`
244
+ : "";
245
+ return toolArgsPreview
246
+ ? `${progress.currentTool}: ${toolArgsPreview}${durationSuffix}`
247
+ : `${progress.currentTool}${durationSuffix}`;
187
248
  }
188
249
 
189
- function buildLiveStatusLine(progress: Pick<AgentProgress, "activityState" | "lastActivityAt">): string | undefined {
190
- return formatActivityLabel(progress.lastActivityAt, progress.activityState);
250
+ function buildLiveStatusLine(
251
+ progress: Pick<AgentProgress, "activityState" | "lastActivityAt">,
252
+ ): string | undefined {
253
+ return formatActivityLabel(progress.lastActivityAt, progress.activityState);
191
254
  }
192
255
 
193
256
  function themeBold(theme: Theme, text: string): string {
194
- return ((theme as { bold?: (value: string) => string }).bold?.(text)) ?? text;
257
+ return (theme as { bold?: (value: string) => string }).bold?.(text) ?? text;
195
258
  }
196
259
 
197
260
  function statJoin(theme: Theme, parts: string[]): string {
198
- return parts.filter(Boolean).map((part) => theme.fg("dim", part)).join(` ${theme.fg("dim", "·")} `);
261
+ return parts
262
+ .filter(Boolean)
263
+ .map((part) => theme.fg("dim", part))
264
+ .join(` ${theme.fg("dim", "·")} `);
199
265
  }
200
266
 
201
267
  function formatTokenStat(tokens: number): string {
202
- return `${formatTokens(tokens)} token`;
268
+ return `${formatTokens(tokens)} token`;
203
269
  }
204
270
 
205
271
  function formatToolUseStat(count: number): string {
206
- return `${count} tool use${count === 1 ? "" : "s"}`;
272
+ return `${count} tool use${count === 1 ? "" : "s"}`;
207
273
  }
208
274
 
209
- function formatProgressStats(theme: Theme, progress: Pick<AgentProgress, "toolCount" | "tokens" | "durationMs"> | undefined, includeDuration = true): string {
210
- if (!progress) return "";
211
- const parts: string[] = [];
212
- if (progress.toolCount > 0) parts.push(formatToolUseStat(progress.toolCount));
213
- if (progress.tokens > 0) parts.push(formatTokenStat(progress.tokens));
214
- if (includeDuration && progress.durationMs > 0) parts.push(formatDuration(progress.durationMs));
215
- return statJoin(theme, parts);
275
+ function formatProgressStats(
276
+ theme: Theme,
277
+ progress:
278
+ | Pick<AgentProgress, "toolCount" | "tokens" | "durationMs">
279
+ | undefined,
280
+ includeDuration = true,
281
+ ): string {
282
+ if (!progress) return "";
283
+ const parts: string[] = [];
284
+ if (progress.toolCount > 0) parts.push(formatToolUseStat(progress.toolCount));
285
+ if (progress.tokens > 0) parts.push(formatTokenStat(progress.tokens));
286
+ if (includeDuration && progress.durationMs > 0)
287
+ parts.push(formatDuration(progress.durationMs));
288
+ return statJoin(theme, parts);
216
289
  }
217
290
 
218
291
  function firstOutputLine(text: string): string {
219
- return text.split("\n").find((line) => line.trim())?.trim() ?? "";
292
+ return (
293
+ text
294
+ .split("\n")
295
+ .find((line) => line.trim())
296
+ ?.trim() ?? ""
297
+ );
220
298
  }
221
299
 
222
- function resultStatusLine(result: Details["results"][number], output: string): string {
223
- if (result.detached) return result.detachedReason ? `Detached: ${result.detachedReason}` : "Detached";
224
- if (result.interrupted) return "Paused";
225
- if (result.exitCode !== 0) return `Error: ${result.error ?? (firstOutputLine(output) || `exit ${result.exitCode}`)}`;
226
- if (hasEmptyTextOutputWithoutOutputTarget(result.task, output)) return "Done (no text output)";
227
- return "Done";
300
+ function resultStatusLine(
301
+ result: Details["results"][number],
302
+ output: string,
303
+ ): string {
304
+ if (result.detached)
305
+ return result.detachedReason
306
+ ? `Detached: ${result.detachedReason}`
307
+ : "Detached";
308
+ if (result.interrupted) return "Paused";
309
+ if (result.exitCode !== 0)
310
+ return `Error: ${result.error ?? (firstOutputLine(output) || `exit ${result.exitCode}`)}`;
311
+ if (hasEmptyTextOutputWithoutOutputTarget(result.task, output))
312
+ return "Done (no text output)";
313
+ return "Done";
228
314
  }
229
315
 
230
- function resultGlyph(result: Details["results"][number], output: string, theme: Theme, running = result.progress?.status === "running"): string {
231
- if (running) return theme.fg("accent", spinnerFrame());
232
- if (result.detached) return theme.fg("warning", "■");
233
- if (result.interrupted) return theme.fg("warning", "■");
234
- if (result.exitCode !== 0) return theme.fg("error", "✗");
235
- if (hasEmptyTextOutputWithoutOutputTarget(result.task, output)) return theme.fg("warning", "✓");
236
- return theme.fg("success", "✓");
316
+ function resultGlyph(
317
+ result: Details["results"][number],
318
+ output: string,
319
+ theme: Theme,
320
+ running = result.progress?.status === "running",
321
+ ): string {
322
+ if (running) return theme.fg("accent", spinnerFrame());
323
+ if (result.detached) return theme.fg("warning", "■");
324
+ if (result.interrupted) return theme.fg("warning", "■");
325
+ if (result.exitCode !== 0) return theme.fg("error", "✗");
326
+ if (hasEmptyTextOutputWithoutOutputTarget(result.task, output))
327
+ return theme.fg("warning", "✓");
328
+ return theme.fg("success", "✓");
237
329
  }
238
330
 
239
331
  function compactCurrentActivity(progress: AgentProgress): string {
240
- return formatCurrentToolLine(progress, getTermWidth() - 4, false) ?? buildLiveStatusLine(progress) ?? "thinking…";
332
+ return (
333
+ formatCurrentToolLine(progress, getTermWidth() - 4, false) ??
334
+ buildLiveStatusLine(progress) ??
335
+ "thinking…"
336
+ );
241
337
  }
242
338
 
243
339
  function hasAnimatedWidgetJobs(jobs: AsyncJobState[]): boolean {
244
- return jobs.some((job) => job.status === "running");
340
+ return jobs.some((job) => job.status === "running");
245
341
  }
246
342
 
247
343
  function formatWidgetAgents(agents: string[]): string {
248
- const distinct = [...new Set(agents)];
249
- if (distinct.length === 1 && agents.length > 1) return `${distinct[0]} ×${agents.length}`;
250
- if (agents.length > 3) return `${agents.slice(0, 2).join(", ")} +${agents.length - 2} more`;
251
- return agents.join(", ");
344
+ const distinct = [...new Set(agents)];
345
+ if (distinct.length === 1 && agents.length > 1)
346
+ return `${distinct[0]} ×${agents.length}`;
347
+ if (agents.length > 3)
348
+ return `${agents.slice(0, 2).join(", ")} +${agents.length - 2} more`;
349
+ return agents.join(", ");
252
350
  }
253
351
 
254
352
  function widgetJobName(job: AsyncJobState): string {
255
- if (job.mode === "parallel") return "parallel";
256
- if (job.mode === "chain") return "chain";
257
- if (job.mode === "single" && job.agents?.length === 1) return job.agents[0]!;
258
- if (job.agents?.length) return formatWidgetAgents(job.agents);
259
- return job.mode ?? "subagent";
353
+ if (job.mode === "parallel") return "parallel";
354
+ if (job.mode === "chain") return "chain";
355
+ if (job.mode === "single" && job.agents?.length === 1) return job.agents[0]!;
356
+ if (job.agents?.length) return formatWidgetAgents(job.agents);
357
+ return job.mode ?? "subagent";
260
358
  }
261
359
 
262
360
  function getCachedLastActivity(outputFile: string | undefined): string {
263
- if (!outputFile) return "";
264
- const now = Date.now();
265
- const cached = outputActivityCache.get(outputFile);
266
- if (cached && now - cached.checkedAt < 1000) return cached.text;
267
- const text = getLastActivity(outputFile);
268
- outputActivityCache.set(outputFile, { checkedAt: now, text });
269
- return text;
361
+ if (!outputFile) return "";
362
+ const now = Date.now();
363
+ const cached = outputActivityCache.get(outputFile);
364
+ if (cached && now - cached.checkedAt < 1000) return cached.text;
365
+ const text = getLastActivity(outputFile);
366
+ outputActivityCache.set(outputFile, { checkedAt: now, text });
367
+ return text;
270
368
  }
271
369
 
272
370
  function widgetActivity(job: AsyncJobState): string {
273
- const facts: string[] = [];
274
- if (job.currentTool && job.currentToolStartedAt !== undefined) facts.push(`${job.currentTool} ${formatDuration(Math.max(0, Date.now() - job.currentToolStartedAt))}`);
275
- else if (job.currentTool) facts.push(job.currentTool);
276
- if (job.currentPath) facts.push(shortenPath(job.currentPath));
277
- if (job.turnCount !== undefined) facts.push(`${job.turnCount} turns`);
278
- if (job.toolCount !== undefined) facts.push(`${job.toolCount} tools`);
279
- const activity = formatActivityLabel(job.lastActivityAt, job.activityState)
280
- ?? (job.status === "running" ? getCachedLastActivity(job.outputFile) : "");
281
- if (activity && facts.length) return `${activity} · ${facts.join(" · ")}`;
282
- if (activity) return activity;
283
- if (facts.length) return facts.join(" · ");
284
- if (job.status === "running") return "thinking…";
285
- if (job.status === "queued") return "queued…";
286
- if (job.status === "paused") return "Paused";
287
- if (job.status === "failed") return "Failed";
288
- return "Done";
371
+ const facts: string[] = [];
372
+ if (job.currentTool && job.currentToolStartedAt !== undefined)
373
+ facts.push(
374
+ `${job.currentTool} ${formatDuration(Math.max(0, Date.now() - job.currentToolStartedAt))}`,
375
+ );
376
+ else if (job.currentTool) facts.push(job.currentTool);
377
+ if (job.currentPath) facts.push(shortenPath(job.currentPath));
378
+ if (job.turnCount !== undefined) facts.push(`${job.turnCount} turns`);
379
+ if (job.toolCount !== undefined) facts.push(`${job.toolCount} tools`);
380
+ const activity =
381
+ formatActivityLabel(job.lastActivityAt, job.activityState) ??
382
+ (job.status === "running" ? getCachedLastActivity(job.outputFile) : "");
383
+ if (activity && facts.length) return `${activity} · ${facts.join(" · ")}`;
384
+ if (activity) return activity;
385
+ if (facts.length) return facts.join(" · ");
386
+ if (job.status === "running") return "thinking…";
387
+ if (job.status === "queued") return "queued…";
388
+ if (job.status === "paused") return "Paused";
389
+ if (job.status === "failed") return "Failed";
390
+ return "Done";
289
391
  }
290
392
 
291
393
  function widgetStatusGlyph(job: AsyncJobState, theme: Theme): string {
292
- if (job.status === "running") return theme.fg("accent", spinnerFrame());
293
- if (job.status === "queued") return theme.fg("muted", "◦");
294
- if (job.status === "complete") return theme.fg("success", "✓");
295
- if (job.status === "paused") return theme.fg("warning", "■");
296
- return theme.fg("error", "✗");
394
+ if (job.status === "running") return theme.fg("accent", spinnerFrame());
395
+ if (job.status === "queued") return theme.fg("muted", "◦");
396
+ if (job.status === "complete") return theme.fg("success", "✓");
397
+ if (job.status === "paused") return theme.fg("warning", "■");
398
+ return theme.fg("error", "✗");
297
399
  }
298
400
 
299
401
  function widgetStepGlyph(status: AsyncJobStep["status"], theme: Theme): string {
300
- if (status === "running") return theme.fg("accent", spinnerFrame());
301
- if (status === "complete" || status === "completed") return theme.fg("success", "✓");
302
- if (status === "failed") return theme.fg("error", "");
303
- if (status === "paused") return theme.fg("warning", "");
304
- return theme.fg("muted", "");
402
+ if (status === "running") return theme.fg("accent", spinnerFrame());
403
+ if (status === "complete" || status === "completed")
404
+ return theme.fg("success", "");
405
+ if (status === "failed") return theme.fg("error", "");
406
+ if (status === "paused") return theme.fg("warning", "");
407
+ return theme.fg("muted", "◦");
305
408
  }
306
409
 
307
- function widgetStepStatus(status: AsyncJobStep["status"], theme: Theme): string {
308
- if (status === "running") return theme.fg("accent", "running");
309
- if (status === "complete" || status === "completed") return theme.fg("success", "complete");
310
- if (status === "failed") return theme.fg("error", "failed");
311
- if (status === "paused") return theme.fg("warning", "paused");
312
- return theme.fg("dim", status);
410
+ function widgetStepStatus(
411
+ status: AsyncJobStep["status"],
412
+ theme: Theme,
413
+ ): string {
414
+ if (status === "running") return theme.fg("accent", "running");
415
+ if (status === "complete" || status === "completed")
416
+ return theme.fg("success", "complete");
417
+ if (status === "failed") return theme.fg("error", "failed");
418
+ if (status === "paused") return theme.fg("warning", "paused");
419
+ return theme.fg("dim", status);
313
420
  }
314
421
 
315
- function widgetStepActivity(step: NonNullable<AsyncJobState["steps"]>[number]): string {
316
- const facts: string[] = [];
317
- if (step.currentTool && step.currentToolStartedAt !== undefined) facts.push(`${step.currentTool} ${formatDuration(Math.max(0, Date.now() - step.currentToolStartedAt))}`);
318
- else if (step.currentTool) facts.push(step.currentTool);
319
- if (step.currentPath) facts.push(shortenPath(step.currentPath));
320
- if (step.turnCount !== undefined) facts.push(`${step.turnCount} turns`);
321
- if (step.toolCount !== undefined) facts.push(`${step.toolCount} tools`);
322
- if (step.tokens?.total) facts.push(formatTokenStat(step.tokens.total));
323
- const activity = formatActivityLabel(step.lastActivityAt, step.activityState);
324
- if (activity && facts.length) return `${activity} · ${facts.join(" · ")}`;
325
- if (activity) return activity;
326
- return facts.join(" · ");
422
+ function widgetStepActivity(
423
+ step: NonNullable<AsyncJobState["steps"]>[number],
424
+ ): string {
425
+ const facts: string[] = [];
426
+ if (step.currentTool && step.currentToolStartedAt !== undefined)
427
+ facts.push(
428
+ `${step.currentTool} ${formatDuration(Math.max(0, Date.now() - step.currentToolStartedAt))}`,
429
+ );
430
+ else if (step.currentTool) facts.push(step.currentTool);
431
+ if (step.currentPath) facts.push(shortenPath(step.currentPath));
432
+ if (step.turnCount !== undefined) facts.push(`${step.turnCount} turns`);
433
+ if (step.toolCount !== undefined) facts.push(`${step.toolCount} tools`);
434
+ if (step.tokens?.total) facts.push(formatTokenStat(step.tokens.total));
435
+ const activity = formatActivityLabel(step.lastActivityAt, step.activityState);
436
+ if (activity && facts.length) return `${activity} · ${facts.join(" · ")}`;
437
+ if (activity) return activity;
438
+ return facts.join(" · ");
327
439
  }
328
440
 
329
-
330
- function widgetChainDetails(job: AsyncJobState, theme: Theme, expanded = false, width = getTermWidth()): string[] {
331
- if (!job.steps?.length) return [];
332
- const total = job.chainStepCount ?? job.steps.length;
333
- const lines: string[] = [];
334
- for (const span of buildAsyncChainStepSpans(total, job.steps.length, job.parallelGroups)) {
335
- const steps = job.steps.slice(span.start, span.start + span.count);
336
- if (span.isParallel) {
337
- const status = aggregateStepStatus(steps);
338
- lines.push(` ${widgetStepGlyph(status, theme)} Step ${span.stepIndex + 1}/${total}: ${themeBold(theme, "parallel group")} ${theme.fg("dim", "·")} ${theme.fg("dim", formatParallelOutcome(steps, span.count))}`);
339
- continue;
340
- }
341
- const step = steps[0];
342
- if (!step) {
343
- lines.push(` ${theme.fg("dim", `◦ Step ${span.stepIndex + 1}/${total}: pending`)}`);
344
- continue;
345
- }
346
- lines.push(...foregroundStyleWidgetStepLines(job, theme, step, "Step", span.stepIndex + 1, total, expanded, width));
347
- }
348
- return lines;
441
+ function widgetChainDetails(
442
+ job: AsyncJobState,
443
+ theme: Theme,
444
+ expanded = false,
445
+ width = getTermWidth(),
446
+ ): string[] {
447
+ if (!job.steps?.length) return [];
448
+ const total = job.chainStepCount ?? job.steps.length;
449
+ const lines: string[] = [];
450
+ for (const span of buildAsyncChainStepSpans(
451
+ total,
452
+ job.steps.length,
453
+ job.parallelGroups,
454
+ )) {
455
+ const steps = job.steps.slice(span.start, span.start + span.count);
456
+ if (span.isParallel) {
457
+ const status = aggregateStepStatus(steps);
458
+ lines.push(
459
+ ` ${widgetStepGlyph(status, theme)} Step ${span.stepIndex + 1}/${total}: ${themeBold(theme, "parallel group")} ${theme.fg("dim", "·")} ${theme.fg("dim", formatParallelOutcome(steps, span.count))}`,
460
+ );
461
+ continue;
462
+ }
463
+ const step = steps[0];
464
+ if (!step) {
465
+ lines.push(
466
+ ` ${theme.fg("dim", `◦ Step ${span.stepIndex + 1}/${total}: pending`)}`,
467
+ );
468
+ continue;
469
+ }
470
+ lines.push(
471
+ ...foregroundStyleWidgetStepLines(
472
+ job,
473
+ theme,
474
+ step,
475
+ "Step",
476
+ span.stepIndex + 1,
477
+ total,
478
+ expanded,
479
+ width,
480
+ ),
481
+ );
482
+ }
483
+ return lines;
349
484
  }
350
485
 
351
- function widgetParallelAgentDetails(job: AsyncJobState, theme: Theme): string[] {
352
- if (!job.steps?.length) return [];
353
- if (job.mode !== "parallel" && job.mode !== "chain") return [];
354
- if (job.mode === "chain" && !job.activeParallelGroup && job.parallelGroups?.length) return widgetChainDetails(job, theme);
355
- const total = job.stepsTotal ?? job.steps.length;
356
- return job.steps.map((step, index) => {
357
- const marker = index === job.steps!.length - 1 ? "└" : "├";
358
- const activity = widgetStepActivity(step);
359
- const itemTitle = job.mode === "parallel" || job.activeParallelGroup ? "Agent" : "Step";
360
- const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking);
361
- return ` ${theme.fg("dim", `${marker} ${widgetStepGlyph(step.status, theme)} ${itemTitle} ${index + 1}/${total}: ${step.agent} · ${widgetStepStatus(step.status, theme)}${modelDisplay}${activity ? ` · ${activity}` : ""}`)}`;
362
- });
486
+ function widgetParallelAgentDetails(
487
+ job: AsyncJobState,
488
+ theme: Theme,
489
+ ): string[] {
490
+ if (!job.steps?.length) return [];
491
+ if (job.mode !== "parallel" && job.mode !== "chain") return [];
492
+ if (
493
+ job.mode === "chain" &&
494
+ !job.activeParallelGroup &&
495
+ job.parallelGroups?.length
496
+ )
497
+ return widgetChainDetails(job, theme);
498
+ const total = job.stepsTotal ?? job.steps.length;
499
+ return job.steps.map((step, index) => {
500
+ const marker = index === job.steps!.length - 1 ? "└" : "├";
501
+ const activity = widgetStepActivity(step);
502
+ const itemTitle =
503
+ job.mode === "parallel" || job.activeParallelGroup ? "Agent" : "Step";
504
+ const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking);
505
+ return ` ${theme.fg("dim", `${marker} ${widgetStepGlyph(step.status, theme)} ${itemTitle} ${index + 1}/${total}: ${step.agent} · ${widgetStepStatus(step.status, theme)}${modelDisplay}${activity ? ` · ${activity}` : ""}`)}`;
506
+ });
363
507
  }
364
508
 
365
- function parseParallelGroupAgentCount(label: string | undefined): number | undefined {
366
- if (!label || !label.startsWith("[") || !label.endsWith("]")) return undefined;
367
- const inner = label.slice(1, -1).trim();
368
- if (!inner) return 0;
369
- return inner.split("+").map((part) => part.trim()).filter(Boolean).length;
509
+ function parseParallelGroupAgentCount(
510
+ label: string | undefined,
511
+ ): number | undefined {
512
+ if (!label || !label.startsWith("[") || !label.endsWith("]"))
513
+ return undefined;
514
+ const inner = label.slice(1, -1).trim();
515
+ if (!inner) return 0;
516
+ return inner
517
+ .split("+")
518
+ .map((part) => part.trim())
519
+ .filter(Boolean).length;
370
520
  }
371
521
 
372
- function isChainParallelGroupActive(details: Pick<Details, "mode" | "chainAgents" | "currentStepIndex">): boolean {
373
- if (details.mode !== "chain") return false;
374
- if (details.currentStepIndex === undefined) return false;
375
- const currentLabel = details.chainAgents?.[details.currentStepIndex];
376
- return parseParallelGroupAgentCount(currentLabel) !== undefined;
522
+ function isChainParallelGroupActive(
523
+ details: Pick<Details, "mode" | "chainAgents" | "currentStepIndex">,
524
+ ): boolean {
525
+ if (details.mode !== "chain") return false;
526
+ if (details.currentStepIndex === undefined) return false;
527
+ const currentLabel = details.chainAgents?.[details.currentStepIndex];
528
+ return parseParallelGroupAgentCount(currentLabel) !== undefined;
377
529
  }
378
530
 
379
531
  interface ChainStepSpan {
380
- stepIndex: number;
381
- start: number;
382
- count: number;
383
- isParallel: boolean;
532
+ stepIndex: number;
533
+ start: number;
534
+ count: number;
535
+ isParallel: boolean;
384
536
  }
385
537
 
386
- function buildChainStepSpans(chainAgents: string[] | undefined): ChainStepSpan[] {
387
- if (!chainAgents?.length) return [];
388
- const spans: ChainStepSpan[] = [];
389
- let start = 0;
390
- for (let stepIndex = 0; stepIndex < chainAgents.length; stepIndex++) {
391
- const label = chainAgents[stepIndex]!;
392
- const parsedCount = parseParallelGroupAgentCount(label);
393
- const count = parsedCount ?? 1;
394
- spans.push({ stepIndex, start, count, isParallel: parsedCount !== undefined });
395
- start += count;
396
- }
397
- return spans;
538
+ function buildChainStepSpans(
539
+ chainAgents: string[] | undefined,
540
+ ): ChainStepSpan[] {
541
+ if (!chainAgents?.length) return [];
542
+ const spans: ChainStepSpan[] = [];
543
+ let start = 0;
544
+ for (let stepIndex = 0; stepIndex < chainAgents.length; stepIndex++) {
545
+ const label = chainAgents[stepIndex]!;
546
+ const parsedCount = parseParallelGroupAgentCount(label);
547
+ const count = parsedCount ?? 1;
548
+ spans.push({
549
+ stepIndex,
550
+ start,
551
+ count,
552
+ isParallel: parsedCount !== undefined,
553
+ });
554
+ start += count;
555
+ }
556
+ return spans;
398
557
  }
399
558
 
400
- function buildAsyncChainStepSpans(total: number, stepCount: number, parallelGroups: AsyncParallelGroupStatus[] = []): ChainStepSpan[] {
401
- const spans: ChainStepSpan[] = [];
402
- let flatIndex = 0;
403
- for (let stepIndex = 0; stepIndex < total; stepIndex++) {
404
- const group = parallelGroups.find((candidate) => candidate.stepIndex === stepIndex);
405
- if (group) {
406
- spans.push({ stepIndex, start: group.start, count: group.count, isParallel: true });
407
- flatIndex = Math.max(flatIndex, group.start + group.count);
408
- continue;
409
- }
410
- spans.push({ stepIndex, start: flatIndex, count: flatIndex < stepCount ? 1 : 0, isParallel: false });
411
- flatIndex++;
412
- }
413
- return spans;
559
+ function buildAsyncChainStepSpans(
560
+ total: number,
561
+ stepCount: number,
562
+ parallelGroups: AsyncParallelGroupStatus[] = [],
563
+ ): ChainStepSpan[] {
564
+ const spans: ChainStepSpan[] = [];
565
+ let flatIndex = 0;
566
+ for (let stepIndex = 0; stepIndex < total; stepIndex++) {
567
+ const group = parallelGroups.find(
568
+ (candidate) => candidate.stepIndex === stepIndex,
569
+ );
570
+ if (group) {
571
+ spans.push({
572
+ stepIndex,
573
+ start: group.start,
574
+ count: group.count,
575
+ isParallel: true,
576
+ });
577
+ flatIndex = Math.max(flatIndex, group.start + group.count);
578
+ continue;
579
+ }
580
+ spans.push({
581
+ stepIndex,
582
+ start: flatIndex,
583
+ count: flatIndex < stepCount ? 1 : 0,
584
+ isParallel: false,
585
+ });
586
+ flatIndex++;
587
+ }
588
+ return spans;
414
589
  }
415
590
 
416
591
  function isDoneResult(result: Details["results"][number]): boolean {
417
- const status = result.progress?.status;
418
- if (status === "completed") return true;
419
- if (status === "running" || status === "pending") return false;
420
- if (result.interrupted || result.detached) return false;
421
- return result.exitCode === 0;
592
+ const status = result.progress?.status;
593
+ if (status === "completed") return true;
594
+ if (status === "running" || status === "pending") return false;
595
+ if (result.interrupted || result.detached) return false;
596
+ return result.exitCode === 0;
422
597
  }
423
598
 
424
599
  interface MultiProgressLabel {
425
- headerLabel: string;
426
- itemTitle: "Step" | "Agent";
427
- totalCount: number;
428
- hasParallelInChain: boolean;
429
- activeParallelGroup: boolean;
430
- groupStartIndex: number;
431
- groupEndIndex: number;
432
- showActiveGroupOnly: boolean;
600
+ headerLabel: string;
601
+ itemTitle: "Step" | "Agent";
602
+ totalCount: number;
603
+ hasParallelInChain: boolean;
604
+ activeParallelGroup: boolean;
605
+ groupStartIndex: number;
606
+ groupEndIndex: number;
607
+ showActiveGroupOnly: boolean;
433
608
  }
434
609
 
435
- function buildMultiProgressLabel(details: Pick<Details, "mode" | "results" | "progress" | "totalSteps" | "currentStepIndex" | "chainAgents">, hasRunning: boolean): MultiProgressLabel {
436
- const stepSpans = buildChainStepSpans(details.chainAgents);
437
- const hasParallelInChain = details.mode === "chain" && stepSpans.some((span) => span.isParallel);
438
- const activeParallelGroup = isChainParallelGroupActive(details);
439
- const itemTitle: "Step" | "Agent" = details.mode === "parallel" || activeParallelGroup ? "Agent" : "Step";
440
-
441
- if (details.mode === "parallel") {
442
- const totalCount = details.totalSteps ?? details.results.length;
443
- const statuses = new Array(totalCount).fill("pending") as Array<"pending" | "running" | "completed" | "failed" | "detached">;
444
- for (const progress of details.progress ?? []) {
445
- if (progress.index >= 0 && progress.index < totalCount) statuses[progress.index] = progress.status;
446
- }
447
- for (let i = 0; i < details.results.length; i++) {
448
- const result = details.results[i]!;
449
- const progressFromArray = details.progress?.find((progress) => progress.index === i)
450
- || details.progress?.find((progress) => progress.agent === result.agent && progress.status === "running");
451
- const index = result.progress?.index ?? progressFromArray?.index ?? i;
452
- if (index < 0 || index >= totalCount) continue;
453
- const status = result.progress?.status
454
- ?? (result.interrupted || result.detached
455
- ? "detached"
456
- : result.exitCode === 0
457
- ? "completed"
458
- : "failed");
459
- statuses[index] = status;
460
- }
461
- const running = statuses.filter((status) => status === "running").length;
462
- const done = statuses.filter((status) => status === "completed").length;
463
- const headerLabel = hasRunning
464
- ? `${formatAgentRunningLabel(running)} · ${done}/${totalCount} done`
465
- : `${done}/${totalCount} done`;
466
- return { headerLabel, itemTitle, totalCount, hasParallelInChain, activeParallelGroup, groupStartIndex: 0, groupEndIndex: totalCount, showActiveGroupOnly: false };
467
- }
468
-
469
- if (activeParallelGroup) {
470
- const currentStepIndex = details.currentStepIndex!;
471
- const span = stepSpans[currentStepIndex];
472
- const groupSize = span?.count ?? 1;
473
- const groupStart = span?.start ?? 0;
474
- const groupEnd = groupStart + groupSize;
475
- let running = 0;
476
- let done = 0;
477
- for (let index = groupStart; index < groupEnd; index++) {
478
- const progressEntry = details.progress?.find((progress) => progress.index === index);
479
- const resultEntry = details.results.find((result) => result.progress?.index === index);
480
- if (progressEntry?.status === "running") {
481
- running++;
482
- continue;
483
- }
484
- if (progressEntry?.status === "completed") {
485
- done++;
486
- continue;
487
- }
488
- if (resultEntry && isDoneResult(resultEntry)) done++;
489
- }
490
- const totalSteps = details.totalSteps ?? details.chainAgents?.length ?? 1;
491
- const headerLabel = hasRunning
492
- ? `step ${currentStepIndex + 1}/${totalSteps} · parallel group: ${formatAgentRunningLabel(running)} · ${done}/${groupSize} done`
493
- : `step ${currentStepIndex + 1}/${totalSteps} · parallel group: ${done}/${groupSize} done`;
494
- return { headerLabel, itemTitle, totalCount: groupSize, hasParallelInChain, activeParallelGroup, groupStartIndex: groupStart, groupEndIndex: groupEnd, showActiveGroupOnly: true };
495
- }
496
-
497
- if (details.mode === "chain" && details.chainAgents?.length) {
498
- const totalCount = details.totalSteps ?? details.chainAgents.length;
499
- const doneLogical = stepSpans.filter((span) => {
500
- for (let index = span.start; index < span.start + span.count; index++) {
501
- const progressEntry = details.progress?.find((progress) => progress.index === index);
502
- const resultEntry = details.results.find((result) => result.progress?.index === index) ?? details.results[index];
503
- if (progressEntry?.status === "running" || progressEntry?.status === "pending") return false;
504
- if (resultEntry && !isDoneResult(resultEntry)) return false;
505
- }
506
- return true;
507
- }).length;
508
- const currentStep = details.currentStepIndex !== undefined ? details.currentStepIndex + 1 : Math.min(totalCount, doneLogical + (hasRunning ? 1 : 0));
509
- const headerLabel = hasRunning ? `step ${currentStep}/${totalCount}` : `step ${doneLogical}/${totalCount}`;
510
- return { headerLabel, itemTitle, totalCount, hasParallelInChain, activeParallelGroup, groupStartIndex: 0, groupEndIndex: details.results.length, showActiveGroupOnly: false };
511
- }
512
-
513
- const totalCount = details.totalSteps ?? details.results.length;
514
- const currentStep = details.currentStepIndex !== undefined ? details.currentStepIndex + 1 : Math.min(totalCount, details.results.filter(isDoneResult).length + (hasRunning ? 1 : 0));
515
- const done = details.results.filter(isDoneResult).length;
516
- const headerLabel = hasRunning ? `step ${currentStep}/${totalCount}` : `step ${done}/${totalCount}`;
517
- return { headerLabel, itemTitle, totalCount, hasParallelInChain, activeParallelGroup, groupStartIndex: 0, groupEndIndex: details.results.length, showActiveGroupOnly: false };
610
+ function buildMultiProgressLabel(
611
+ details: Pick<
612
+ Details,
613
+ | "mode"
614
+ | "results"
615
+ | "progress"
616
+ | "totalSteps"
617
+ | "currentStepIndex"
618
+ | "chainAgents"
619
+ >,
620
+ hasRunning: boolean,
621
+ ): MultiProgressLabel {
622
+ const stepSpans = buildChainStepSpans(details.chainAgents);
623
+ const hasParallelInChain =
624
+ details.mode === "chain" && stepSpans.some((span) => span.isParallel);
625
+ const activeParallelGroup = isChainParallelGroupActive(details);
626
+ const itemTitle: "Step" | "Agent" =
627
+ details.mode === "parallel" || activeParallelGroup ? "Agent" : "Step";
628
+
629
+ if (details.mode === "parallel") {
630
+ const totalCount = details.totalSteps ?? details.results.length;
631
+ const statuses = new Array(totalCount).fill("pending") as Array<
632
+ "pending" | "running" | "completed" | "failed" | "detached"
633
+ >;
634
+ for (const progress of details.progress ?? []) {
635
+ if (progress.index >= 0 && progress.index < totalCount)
636
+ statuses[progress.index] = progress.status;
637
+ }
638
+ for (let i = 0; i < details.results.length; i++) {
639
+ const result = details.results[i]!;
640
+ const progressFromArray =
641
+ details.progress?.find((progress) => progress.index === i) ||
642
+ details.progress?.find(
643
+ (progress) =>
644
+ progress.agent === result.agent && progress.status === "running",
645
+ );
646
+ const index = result.progress?.index ?? progressFromArray?.index ?? i;
647
+ if (index < 0 || index >= totalCount) continue;
648
+ const status =
649
+ result.progress?.status ??
650
+ (result.interrupted || result.detached
651
+ ? "detached"
652
+ : result.exitCode === 0
653
+ ? "completed"
654
+ : "failed");
655
+ statuses[index] = status;
656
+ }
657
+ const running = statuses.filter((status) => status === "running").length;
658
+ const done = statuses.filter((status) => status === "completed").length;
659
+ const headerLabel = hasRunning
660
+ ? `${formatAgentRunningLabel(running)} · ${done}/${totalCount} done`
661
+ : `${done}/${totalCount} done`;
662
+ return {
663
+ headerLabel,
664
+ itemTitle,
665
+ totalCount,
666
+ hasParallelInChain,
667
+ activeParallelGroup,
668
+ groupStartIndex: 0,
669
+ groupEndIndex: totalCount,
670
+ showActiveGroupOnly: false,
671
+ };
672
+ }
673
+
674
+ if (activeParallelGroup) {
675
+ const currentStepIndex = details.currentStepIndex!;
676
+ const span = stepSpans[currentStepIndex];
677
+ const groupSize = span?.count ?? 1;
678
+ const groupStart = span?.start ?? 0;
679
+ const groupEnd = groupStart + groupSize;
680
+ let running = 0;
681
+ let done = 0;
682
+ for (let index = groupStart; index < groupEnd; index++) {
683
+ const progressEntry = details.progress?.find(
684
+ (progress) => progress.index === index,
685
+ );
686
+ const resultEntry = details.results.find(
687
+ (result) => result.progress?.index === index,
688
+ );
689
+ if (progressEntry?.status === "running") {
690
+ running++;
691
+ continue;
692
+ }
693
+ if (progressEntry?.status === "completed") {
694
+ done++;
695
+ continue;
696
+ }
697
+ if (resultEntry && isDoneResult(resultEntry)) done++;
698
+ }
699
+ const totalSteps = details.totalSteps ?? details.chainAgents?.length ?? 1;
700
+ const headerLabel = hasRunning
701
+ ? `step ${currentStepIndex + 1}/${totalSteps} · parallel group: ${formatAgentRunningLabel(running)} · ${done}/${groupSize} done`
702
+ : `step ${currentStepIndex + 1}/${totalSteps} · parallel group: ${done}/${groupSize} done`;
703
+ return {
704
+ headerLabel,
705
+ itemTitle,
706
+ totalCount: groupSize,
707
+ hasParallelInChain,
708
+ activeParallelGroup,
709
+ groupStartIndex: groupStart,
710
+ groupEndIndex: groupEnd,
711
+ showActiveGroupOnly: true,
712
+ };
713
+ }
714
+
715
+ if (details.mode === "chain" && details.chainAgents?.length) {
716
+ const totalCount = details.totalSteps ?? details.chainAgents.length;
717
+ const doneLogical = stepSpans.filter((span) => {
718
+ for (let index = span.start; index < span.start + span.count; index++) {
719
+ const progressEntry = details.progress?.find(
720
+ (progress) => progress.index === index,
721
+ );
722
+ const resultEntry =
723
+ details.results.find((result) => result.progress?.index === index) ??
724
+ details.results[index];
725
+ if (
726
+ progressEntry?.status === "running" ||
727
+ progressEntry?.status === "pending"
728
+ )
729
+ return false;
730
+ if (resultEntry && !isDoneResult(resultEntry)) return false;
731
+ }
732
+ return true;
733
+ }).length;
734
+ const currentStep =
735
+ details.currentStepIndex !== undefined
736
+ ? details.currentStepIndex + 1
737
+ : Math.min(totalCount, doneLogical + (hasRunning ? 1 : 0));
738
+ const headerLabel = hasRunning
739
+ ? `step ${currentStep}/${totalCount}`
740
+ : `step ${doneLogical}/${totalCount}`;
741
+ return {
742
+ headerLabel,
743
+ itemTitle,
744
+ totalCount,
745
+ hasParallelInChain,
746
+ activeParallelGroup,
747
+ groupStartIndex: 0,
748
+ groupEndIndex: details.results.length,
749
+ showActiveGroupOnly: false,
750
+ };
751
+ }
752
+
753
+ const totalCount = details.totalSteps ?? details.results.length;
754
+ const currentStep =
755
+ details.currentStepIndex !== undefined
756
+ ? details.currentStepIndex + 1
757
+ : Math.min(
758
+ totalCount,
759
+ details.results.filter(isDoneResult).length + (hasRunning ? 1 : 0),
760
+ );
761
+ const done = details.results.filter(isDoneResult).length;
762
+ const headerLabel = hasRunning
763
+ ? `step ${currentStep}/${totalCount}`
764
+ : `step ${done}/${totalCount}`;
765
+ return {
766
+ headerLabel,
767
+ itemTitle,
768
+ totalCount,
769
+ hasParallelInChain,
770
+ activeParallelGroup,
771
+ groupStartIndex: 0,
772
+ groupEndIndex: details.results.length,
773
+ showActiveGroupOnly: false,
774
+ };
518
775
  }
519
776
 
520
- function resultRowLabel(details: Pick<Details, "mode" | "chainAgents">, label: MultiProgressLabel, resultIndex: number, stepNumber: number): string {
521
- if (details.mode === "chain" && label.hasParallelInChain) {
522
- const span = buildChainStepSpans(details.chainAgents).find((candidate) => resultIndex >= candidate.start && resultIndex < candidate.start + candidate.count);
523
- if (span?.isParallel) return `Agent ${resultIndex - span.start + 1}/${span.count}`;
524
- if (span) return `Step ${span.stepIndex + 1}`;
525
- }
526
- if (label.itemTitle === "Agent") {
527
- const localStepNumber = label.activeParallelGroup
528
- ? Math.max(1, stepNumber - label.groupStartIndex)
529
- : stepNumber;
530
- return `Agent ${localStepNumber}/${label.totalCount}`;
531
- }
532
- return `Step ${stepNumber}`;
777
+ function resultRowLabel(
778
+ details: Pick<Details, "mode" | "chainAgents">,
779
+ label: MultiProgressLabel,
780
+ resultIndex: number,
781
+ stepNumber: number,
782
+ ): string {
783
+ if (details.mode === "chain" && label.hasParallelInChain) {
784
+ const span = buildChainStepSpans(details.chainAgents).find(
785
+ (candidate) =>
786
+ resultIndex >= candidate.start &&
787
+ resultIndex < candidate.start + candidate.count,
788
+ );
789
+ if (span?.isParallel)
790
+ return `Agent ${resultIndex - span.start + 1}/${span.count}`;
791
+ if (span) return `Step ${span.stepIndex + 1}`;
792
+ }
793
+ if (label.itemTitle === "Agent") {
794
+ const localStepNumber = label.activeParallelGroup
795
+ ? Math.max(1, stepNumber - label.groupStartIndex)
796
+ : stepNumber;
797
+ return `Agent ${localStepNumber}/${label.totalCount}`;
798
+ }
799
+ return `Step ${stepNumber}`;
533
800
  }
534
801
 
535
802
  function widgetStats(job: AsyncJobState, theme: Theme): string {
536
- const parts: string[] = [];
537
- const stepsTotal = job.stepsTotal ?? (job.agents?.length ?? 1);
538
- if (job.activeParallelGroup) {
539
- const running = job.runningSteps ?? (job.status === "running" ? 1 : 0);
540
- const done = job.completedSteps ?? (job.status === "complete" ? stepsTotal : 0);
541
- if (job.mode === "parallel") {
542
- if (job.status === "running" && running > 0) parts.push(formatAgentRunningLabel(running));
543
- if (stepsTotal > 0) parts.push(`${done}/${stepsTotal} done`);
544
- } else {
545
- const activeGroup = job.currentStep !== undefined
546
- ? job.parallelGroups?.find((group) => job.currentStep! >= group.start && job.currentStep! < group.start + group.count)
547
- : job.parallelGroups?.find((group) => group.start === 0);
548
- const logicalStep = activeGroup?.stepIndex ?? job.currentStep ?? 0;
549
- const total = job.chainStepCount ?? stepsTotal;
550
- const groupParts = [`${done}/${stepsTotal} done`];
551
- if (job.status === "running" && running > 0) groupParts.unshift(formatAgentRunningLabel(running));
552
- parts.push(`step ${logicalStep + 1}/${total} · parallel group: ${groupParts.join(" · ")}`);
553
- }
554
- } else if (job.currentStep !== undefined) {
555
- if (job.mode === "chain" && job.parallelGroups?.length) {
556
- const total = job.chainStepCount ?? stepsTotal;
557
- parts.push(`step ${flatToLogicalStepIndex(job.currentStep, total, job.parallelGroups) + 1}/${total}`);
558
- } else {
559
- parts.push(`step ${job.currentStep + 1}/${stepsTotal}`);
560
- }
561
- } else if (stepsTotal > 1) {
562
- parts.push(`steps ${stepsTotal}`);
563
- }
564
- if (job.toolCount !== undefined) parts.push(formatToolUseStat(job.toolCount));
565
- if (job.totalTokens?.total) parts.push(formatTokenStat(job.totalTokens.total));
566
- const endTime = job.status === "complete" || job.status === "failed" || job.status === "paused" ? (job.updatedAt ?? Date.now()) : Date.now();
567
- if (job.startedAt) parts.push(formatDuration(Math.max(0, endTime - job.startedAt)));
568
- return statJoin(theme, parts);
803
+ const parts: string[] = [];
804
+ const stepsTotal = job.stepsTotal ?? job.agents?.length ?? 1;
805
+ if (job.activeParallelGroup) {
806
+ const running = job.runningSteps ?? (job.status === "running" ? 1 : 0);
807
+ const done =
808
+ job.completedSteps ?? (job.status === "complete" ? stepsTotal : 0);
809
+ if (job.mode === "parallel") {
810
+ if (job.status === "running" && running > 0)
811
+ parts.push(formatAgentRunningLabel(running));
812
+ if (stepsTotal > 0) parts.push(`${done}/${stepsTotal} done`);
813
+ } else {
814
+ const activeGroup =
815
+ job.currentStep !== undefined
816
+ ? job.parallelGroups?.find(
817
+ (group) =>
818
+ job.currentStep! >= group.start &&
819
+ job.currentStep! < group.start + group.count,
820
+ )
821
+ : job.parallelGroups?.find((group) => group.start === 0);
822
+ const logicalStep = activeGroup?.stepIndex ?? job.currentStep ?? 0;
823
+ const total = job.chainStepCount ?? stepsTotal;
824
+ const groupParts = [`${done}/${stepsTotal} done`];
825
+ if (job.status === "running" && running > 0)
826
+ groupParts.unshift(formatAgentRunningLabel(running));
827
+ parts.push(
828
+ `step ${logicalStep + 1}/${total} · parallel group: ${groupParts.join(" · ")}`,
829
+ );
830
+ }
831
+ } else if (job.currentStep !== undefined) {
832
+ if (job.mode === "chain" && job.parallelGroups?.length) {
833
+ const total = job.chainStepCount ?? stepsTotal;
834
+ parts.push(
835
+ `step ${flatToLogicalStepIndex(job.currentStep, total, job.parallelGroups) + 1}/${total}`,
836
+ );
837
+ } else {
838
+ parts.push(`step ${job.currentStep + 1}/${stepsTotal}`);
839
+ }
840
+ } else if (stepsTotal > 1) {
841
+ parts.push(`steps ${stepsTotal}`);
842
+ }
843
+ if (job.toolCount !== undefined) parts.push(formatToolUseStat(job.toolCount));
844
+ if (job.totalTokens?.total)
845
+ parts.push(formatTokenStat(job.totalTokens.total));
846
+ const endTime =
847
+ job.status === "complete" ||
848
+ job.status === "failed" ||
849
+ job.status === "paused"
850
+ ? (job.updatedAt ?? Date.now())
851
+ : Date.now();
852
+ if (job.startedAt)
853
+ parts.push(formatDuration(Math.max(0, endTime - job.startedAt)));
854
+ return statJoin(theme, parts);
569
855
  }
570
856
 
571
- function widgetStepStats(theme: Theme, step: NonNullable<AsyncJobState["steps"]>[number]): string {
572
- return statJoin(theme, [
573
- step.turnCount !== undefined ? `${step.turnCount} turns` : "",
574
- step.toolCount !== undefined ? formatToolUseStat(step.toolCount) : "",
575
- step.tokens?.total ? formatTokenStat(step.tokens.total) : "",
576
- step.durationMs !== undefined ? formatDuration(step.durationMs) : "",
577
- ]);
857
+ function widgetStepStats(
858
+ theme: Theme,
859
+ step: NonNullable<AsyncJobState["steps"]>[number],
860
+ ): string {
861
+ return statJoin(theme, [
862
+ step.turnCount !== undefined ? `${step.turnCount} turns` : "",
863
+ step.toolCount !== undefined ? formatToolUseStat(step.toolCount) : "",
864
+ step.tokens?.total ? formatTokenStat(step.tokens.total) : "",
865
+ step.durationMs !== undefined ? formatDuration(step.durationMs) : "",
866
+ ]);
578
867
  }
579
868
 
580
- function modelThinkingBadge(theme: Theme, model?: string, thinking?: string): string {
581
- const label = formatModelThinking(model, thinking);
582
- return label ? theme.fg("dim", ` (${label})`) : "";
869
+ function modelThinkingBadge(
870
+ theme: Theme,
871
+ model?: string,
872
+ thinking?: string,
873
+ ): string {
874
+ const label = formatModelThinking(model, thinking);
875
+ return label ? theme.fg("dim", ` (${label})`) : "";
583
876
  }
584
877
 
585
- function widgetStepActivityLine(step: NonNullable<AsyncJobState["steps"]>[number], width: number, expanded: boolean): string {
586
- const toolLine = formatCurrentToolLine(step, width, expanded);
587
- if (toolLine) return toolLine;
588
- const activity = formatActivityLabel(step.lastActivityAt, step.activityState);
589
- if (activity) return activity;
590
- if (step.status === "running") return "thinking…";
591
- return "";
878
+ function widgetStepActivityLine(
879
+ step: NonNullable<AsyncJobState["steps"]>[number],
880
+ width: number,
881
+ expanded: boolean,
882
+ ): string {
883
+ const toolLine = formatCurrentToolLine(step, width, expanded);
884
+ if (toolLine) return toolLine;
885
+ const activity = formatActivityLabel(step.lastActivityAt, step.activityState);
886
+ if (activity) return activity;
887
+ if (step.status === "running") return "thinking…";
888
+ return "";
592
889
  }
593
890
 
594
- function widgetOutputPath(job: AsyncJobState, step: NonNullable<AsyncJobState["steps"]>[number]): string | undefined {
595
- if (typeof step.index !== "number") return undefined;
596
- return path.join(job.asyncDir, `output-${step.index}.log`);
891
+ function widgetOutputPath(
892
+ job: AsyncJobState,
893
+ step: NonNullable<AsyncJobState["steps"]>[number],
894
+ ): string | undefined {
895
+ if (typeof step.index !== "number") return undefined;
896
+ return path.join(job.asyncDir, `output-${step.index}.log`);
597
897
  }
598
898
 
599
899
  function foregroundStyleWidgetStepLines(
600
- job: AsyncJobState,
601
- theme: Theme,
602
- step: NonNullable<AsyncJobState["steps"]>[number],
603
- itemTitle: "Agent" | "Step",
604
- index: number,
605
- total: number,
606
- expanded: boolean,
607
- width: number,
900
+ job: AsyncJobState,
901
+ theme: Theme,
902
+ step: NonNullable<AsyncJobState["steps"]>[number],
903
+ itemTitle: "Agent" | "Step",
904
+ index: number,
905
+ total: number,
906
+ expanded: boolean,
907
+ width: number,
608
908
  ): string[] {
609
- const status = widgetStepStatus(step.status, theme);
610
- const stats = widgetStepStats(theme, step);
611
- const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking);
612
- const lines = [` ${widgetStepGlyph(step.status, theme)} ${itemTitle} ${index}/${total}: ${themeBold(theme, step.agent)} ${theme.fg("dim", "·")} ${status}${modelDisplay}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`];
613
- const activity = widgetStepActivityLine(step, width, expanded);
614
- if (activity) lines.push(` ${theme.fg("dim", `⎿ ${activity}`)}`);
615
- if (step.status === "running") {
616
- if (!expanded) lines.push(` ${theme.fg("accent", "Press Ctrl+O for live detail")}`);
617
- const output = widgetOutputPath(job, step);
618
- if (output) lines.push(` ${theme.fg("dim", `output: ${shortenPath(output)}`)}`);
619
- if (expanded) {
620
- const liveStatus = buildLiveStatusLine(step);
621
- if (liveStatus && liveStatus !== activity) lines.push(` ${theme.fg("accent", liveStatus)}`);
622
- for (const tool of step.recentTools?.slice(-3) ?? []) {
623
- const maxArgsLen = Math.max(40, width - 30);
624
- const argsPreview = tool.args.length <= maxArgsLen ? tool.args : `${tool.args.slice(0, maxArgsLen)}...`;
625
- lines.push(` ${theme.fg("dim", `${tool.tool}${argsPreview ? `: ${argsPreview}` : ""}`)}`);
626
- }
627
- for (const line of step.recentOutput?.slice(-5) ?? []) {
628
- lines.push(` ${theme.fg("dim", line)}`);
629
- }
630
- }
631
- }
632
- return lines;
909
+ const status = widgetStepStatus(step.status, theme);
910
+ const stats = widgetStepStats(theme, step);
911
+ const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking);
912
+ const lines = [
913
+ ` ${widgetStepGlyph(step.status, theme)} ${itemTitle} ${index}/${total}: ${themeBold(theme, step.agent)} ${theme.fg("dim", "·")} ${status}${modelDisplay}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
914
+ ];
915
+ const activity = widgetStepActivityLine(step, width, expanded);
916
+ if (activity) lines.push(` ${theme.fg("dim", `⎿ ${activity}`)}`);
917
+ if (step.status === "running") {
918
+ if (!expanded)
919
+ lines.push(` ${theme.fg("accent", "Press ctrl+o for live detail")}`);
920
+ const output = widgetOutputPath(job, step);
921
+ if (output)
922
+ lines.push(` ${theme.fg("dim", `output: ${shortenPath(output)}`)}`);
923
+ if (expanded) {
924
+ const liveStatus = buildLiveStatusLine(step);
925
+ if (liveStatus && liveStatus !== activity)
926
+ lines.push(` ${theme.fg("accent", liveStatus)}`);
927
+ for (const tool of step.recentTools?.slice(-3) ?? []) {
928
+ const maxArgsLen = Math.max(40, width - 30);
929
+ const argsPreview =
930
+ tool.args.length <= maxArgsLen
931
+ ? tool.args
932
+ : `${tool.args.slice(0, maxArgsLen)}...`;
933
+ lines.push(
934
+ ` ${theme.fg("dim", `${tool.tool}${argsPreview ? `: ${argsPreview}` : ""}`)}`,
935
+ );
936
+ }
937
+ for (const line of step.recentOutput?.slice(-5) ?? []) {
938
+ lines.push(` ${theme.fg("dim", line)}`);
939
+ }
940
+ }
941
+ }
942
+ return lines;
633
943
  }
634
944
 
635
- function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded: boolean, width: number): string[] {
636
- if (!job.steps?.length) return [` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`];
637
- if (job.mode === "chain" && !job.activeParallelGroup && job.parallelGroups?.length) return widgetChainDetails(job, theme, expanded, width);
638
- const total = job.stepsTotal ?? job.steps.length;
639
- const itemTitle = job.mode === "parallel" || job.activeParallelGroup ? "Agent" : "Step";
640
- const lines: string[] = [];
641
- for (const [index, step] of job.steps.entries()) {
642
- lines.push(...foregroundStyleWidgetStepLines(job, theme, step, itemTitle, index + 1, total, expanded, width));
643
- }
644
- return lines;
945
+ function foregroundStyleWidgetDetails(
946
+ job: AsyncJobState,
947
+ theme: Theme,
948
+ expanded: boolean,
949
+ width: number,
950
+ ): string[] {
951
+ if (!job.steps?.length)
952
+ return [` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`];
953
+ if (
954
+ job.mode === "chain" &&
955
+ !job.activeParallelGroup &&
956
+ job.parallelGroups?.length
957
+ )
958
+ return widgetChainDetails(job, theme, expanded, width);
959
+ const total = job.stepsTotal ?? job.steps.length;
960
+ const itemTitle =
961
+ job.mode === "parallel" || job.activeParallelGroup ? "Agent" : "Step";
962
+ const lines: string[] = [];
963
+ for (const [index, step] of job.steps.entries()) {
964
+ lines.push(
965
+ ...foregroundStyleWidgetStepLines(
966
+ job,
967
+ theme,
968
+ step,
969
+ itemTitle,
970
+ index + 1,
971
+ total,
972
+ expanded,
973
+ width,
974
+ ),
975
+ );
976
+ }
977
+ return lines;
645
978
  }
646
979
 
647
- function buildSingleWidgetLines(job: AsyncJobState, theme: Theme, width: number, expanded: boolean): string[] {
648
- const stats = widgetStats(job, theme);
649
- const count = job.mode === "chain" ? job.chainStepCount : job.stepsTotal ?? job.agents?.length ?? job.steps?.length;
650
- const mode = widgetJobName(job);
651
- const title = `async subagent ${mode}${count && count > 1 ? ` (${count})` : ""}`;
652
- return [
653
- `${theme.fg("toolTitle", themeBold(theme, title))} ${theme.fg("dim", "· background")}`,
654
- `${widgetStatusGlyph(job, theme)} ${themeBold(theme, mode)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
655
- ...foregroundStyleWidgetDetails(job, theme, expanded, width),
656
- ].map((line) => truncLine(line, width));
980
+ function buildSingleWidgetLines(
981
+ job: AsyncJobState,
982
+ theme: Theme,
983
+ width: number,
984
+ expanded: boolean,
985
+ ): string[] {
986
+ const stats = widgetStats(job, theme);
987
+ const count =
988
+ job.mode === "chain"
989
+ ? job.chainStepCount
990
+ : (job.stepsTotal ?? job.agents?.length ?? job.steps?.length);
991
+ const mode = widgetJobName(job);
992
+ const title = `async subagent ${mode}${count && count > 1 ? ` (${count})` : ""}`;
993
+ return [
994
+ `${theme.fg("toolTitle", themeBold(theme, title))} ${theme.fg("dim", "· background")}`,
995
+ `${widgetStatusGlyph(job, theme)} ${themeBold(theme, mode)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
996
+ ...foregroundStyleWidgetDetails(job, theme, expanded, width),
997
+ ].map((line) => truncLine(line, width));
657
998
  }
658
999
 
659
- function compactSingleWidgetLines(job: AsyncJobState, theme: Theme, width: number): string[] {
660
- const fullLines = buildSingleWidgetLines(job, theme, width, false);
661
- if (fullLines.length <= 10 || !job.steps?.length || (job.mode !== "parallel" && !job.activeParallelGroup)) return fullLines;
662
-
663
- const total = job.stepsTotal ?? job.steps.length;
664
- const itemTitle = job.mode === "parallel" || job.activeParallelGroup ? "Agent" : "Step";
665
- const lines = fullLines.slice(0, 2);
666
- for (const [index, step] of job.steps.entries()) {
667
- const status = widgetStepStatus(step.status, theme);
668
- const activity = widgetStepActivityLine(step, width, false);
669
- const stepStats = widgetStepStats(theme, step);
670
- const activitySuffix = activity ? ` ${theme.fg("dim", "·")} ${theme.fg("dim", activity)}` : "";
671
- const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking);
672
- lines.push(` ${widgetStepGlyph(step.status, theme)} ${itemTitle} ${index + 1}/${total}: ${themeBold(theme, step.agent)} ${theme.fg("dim", "·")} ${status}${modelDisplay}${activitySuffix}${stepStats ? ` ${theme.fg("dim", "·")} ${stepStats}` : ""}`);
673
- }
674
- if (job.steps.some((step) => step.status === "running")) lines.push(theme.fg("accent", " Press Ctrl+O for live detail"));
675
- return lines.map((line) => truncLine(line, width));
1000
+ function compactSingleWidgetLines(
1001
+ job: AsyncJobState,
1002
+ theme: Theme,
1003
+ width: number,
1004
+ ): string[] {
1005
+ const fullLines = buildSingleWidgetLines(job, theme, width, false);
1006
+ if (
1007
+ fullLines.length <= 10 ||
1008
+ !job.steps?.length ||
1009
+ (job.mode !== "parallel" && !job.activeParallelGroup)
1010
+ )
1011
+ return fullLines;
1012
+
1013
+ const total = job.stepsTotal ?? job.steps.length;
1014
+ const itemTitle =
1015
+ job.mode === "parallel" || job.activeParallelGroup ? "Agent" : "Step";
1016
+ const lines = fullLines.slice(0, 2);
1017
+ for (const [index, step] of job.steps.entries()) {
1018
+ const status = widgetStepStatus(step.status, theme);
1019
+ const activity = widgetStepActivityLine(step, width, false);
1020
+ const stepStats = widgetStepStats(theme, step);
1021
+ const activitySuffix = activity
1022
+ ? ` ${theme.fg("dim", "·")} ${theme.fg("dim", activity)}`
1023
+ : "";
1024
+ const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking);
1025
+ lines.push(
1026
+ ` ${widgetStepGlyph(step.status, theme)} ${itemTitle} ${index + 1}/${total}: ${themeBold(theme, step.agent)} ${theme.fg("dim", "·")} ${status}${modelDisplay}${activitySuffix}${stepStats ? ` ${theme.fg("dim", "·")} ${stepStats}` : ""}`,
1027
+ );
1028
+ }
1029
+ if (job.steps.some((step) => step.status === "running"))
1030
+ lines.push(theme.fg("accent", " Press ctrl+o for live detail"));
1031
+ return lines.map((line) => truncLine(line, width));
676
1032
  }
677
1033
 
678
- function fitWidgetLineBudget(lines: string[], theme: Theme, width: number, expanded: boolean): string[] {
679
- const rows = process.stdout.rows || 30;
680
- const budget = expanded
681
- ? Math.max(12, Math.min(24, Math.floor(rows * 0.55)))
682
- : Math.max(10, Math.min(14, Math.floor(rows * 0.35)));
683
- if (lines.length <= budget) return lines;
684
- const visibleLines = Math.max(1, budget - 1);
685
- const hiddenCount = lines.length - visibleLines;
686
- const hint = expanded
687
- ? `… ${hiddenCount} live-detail lines hidden`
688
- : `… ${hiddenCount} lines hidden · Ctrl+O expands`;
689
- return [...lines.slice(0, visibleLines), truncLine(theme.fg("dim", hint), width)];
1034
+ function fitWidgetLineBudget(
1035
+ lines: string[],
1036
+ theme: Theme,
1037
+ width: number,
1038
+ expanded: boolean,
1039
+ ): string[] {
1040
+ const rows = process.stdout.rows || 30;
1041
+ const budget = expanded
1042
+ ? Math.max(12, Math.min(24, Math.floor(rows * 0.55)))
1043
+ : Math.max(10, Math.min(14, Math.floor(rows * 0.35)));
1044
+ if (lines.length <= budget) return lines;
1045
+ const visibleLines = Math.max(1, budget - 1);
1046
+ const hiddenCount = lines.length - visibleLines;
1047
+ const hint = expanded
1048
+ ? `… ${hiddenCount} live-detail lines hidden`
1049
+ : `… ${hiddenCount} lines hidden · Ctrl+O expands`;
1050
+ return [
1051
+ ...lines.slice(0, visibleLines),
1052
+ truncLine(theme.fg("dim", hint), width),
1053
+ ];
690
1054
  }
691
1055
 
692
- function buildWidgetComponent(jobs: AsyncJobState[], expanded: boolean): (_tui: unknown, theme: Theme) => Component {
693
- return (_tui, theme) => {
694
- const width = getTermWidth();
695
- const lines = expanded
696
- ? buildWidgetLines(jobs, theme, width, true)
697
- : jobs.length === 1
698
- ? compactSingleWidgetLines(jobs[0]!, theme, width)
699
- : buildWidgetLines(jobs, theme, width, false);
700
- const container = new Container();
701
- for (const line of fitWidgetLineBudget(lines, theme, width, expanded)) container.addChild(new Text(line, 1, 0));
702
- return container;
703
- };
1056
+ function buildWidgetComponent(
1057
+ jobs: AsyncJobState[],
1058
+ expanded: boolean,
1059
+ ): (_tui: unknown, theme: Theme) => Component {
1060
+ return (_tui, theme) => {
1061
+ const width = getTermWidth();
1062
+ const lines = expanded
1063
+ ? buildWidgetLines(jobs, theme, width, true)
1064
+ : jobs.length === 1
1065
+ ? compactSingleWidgetLines(jobs[0]!, theme, width)
1066
+ : buildWidgetLines(jobs, theme, width, false);
1067
+ const container = new Container();
1068
+ for (const line of fitWidgetLineBudget(lines, theme, width, expanded))
1069
+ container.addChild(new Text(line, 1, 0));
1070
+ return container;
1071
+ };
704
1072
  }
705
1073
 
706
- export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = getTermWidth(), expanded = false): string[] {
707
- if (jobs.length === 0) return [];
708
- if (jobs.length === 1) return buildSingleWidgetLines(jobs[0]!, theme, width, expanded);
709
- const running = jobs.filter((job) => job.status === "running");
710
- const queued = jobs.filter((job) => job.status === "queued");
711
- const finished = jobs.filter((job) => job.status !== "running" && job.status !== "queued");
712
-
713
- const lines: string[] = [];
714
- const hasActive = running.length > 0 || queued.length > 0;
715
- lines.push(truncLine(`${theme.fg(hasActive ? "accent" : "dim", hasActive ? "●" : "○")} ${theme.fg(hasActive ? "accent" : "dim", "Async agents")} ${theme.fg("dim", "· background")}`, width));
716
-
717
- const items: string[][] = [];
718
- let hiddenRunning = 0;
719
- let hiddenFinished = 0;
720
- let queuedSummaryShown = false;
721
- let slots = MAX_WIDGET_JOBS;
722
-
723
- for (const job of running) {
724
- if (slots <= 0) { hiddenRunning++; continue; }
725
- const stats = widgetStats(job, theme);
726
- items.push([
727
- `${widgetStatusGlyph(job, theme)} ${themeBold(theme, widgetJobName(job))}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
728
- ` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
729
- ...widgetParallelAgentDetails(job, theme),
730
- ]);
731
- slots--;
732
- }
733
-
734
- if (queued.length > 0 && slots > 0) {
735
- items.push([`${theme.fg("muted", "◦")} ${theme.fg("dim", `${queued.length} queued`)}`]);
736
- queuedSummaryShown = true;
737
- slots--;
738
- }
739
-
740
- for (const job of finished) {
741
- if (slots <= 0) { hiddenFinished++; continue; }
742
- const stats = widgetStats(job, theme);
743
- items.push([
744
- `${widgetStatusGlyph(job, theme)} ${themeBold(theme, widgetJobName(job))}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
745
- ` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
746
- ...widgetParallelAgentDetails(job, theme),
747
- ]);
748
- slots--;
749
- }
750
-
751
- const hiddenQueued = queued.length > 0 && !queuedSummaryShown ? queued.length : 0;
752
- const hiddenTotal = hiddenRunning + hiddenFinished + hiddenQueued;
753
- if (hiddenTotal > 0) {
754
- const parts: string[] = [];
755
- if (hiddenRunning > 0) parts.push(`${hiddenRunning} running`);
756
- if (hiddenQueued > 0) parts.push(`${hiddenQueued} queued`);
757
- if (hiddenFinished > 0) parts.push(`${hiddenFinished} finished`);
758
- items.push([theme.fg("dim", `+${hiddenTotal} more (${parts.join(", ")})`)]);
759
- }
760
-
761
- for (let i = 0; i < items.length; i++) {
762
- const item = items[i]!;
763
- const last = i === items.length - 1;
764
- const branch = last ? "└─" : "├─";
765
- const continuation = last ? " " : "";
766
- lines.push(truncLine(`${theme.fg("dim", branch)} ${item[0]}`, width));
767
- for (const detail of item.slice(1)) {
768
- lines.push(truncLine(`${theme.fg("dim", continuation)} ${detail}`, width));
769
- }
770
- }
771
-
772
- return lines;
1074
+ export function buildWidgetLines(
1075
+ jobs: AsyncJobState[],
1076
+ theme: Theme,
1077
+ width = getTermWidth(),
1078
+ expanded = false,
1079
+ ): string[] {
1080
+ if (jobs.length === 0) return [];
1081
+ if (jobs.length === 1)
1082
+ return buildSingleWidgetLines(jobs[0]!, theme, width, expanded);
1083
+ const running = jobs.filter((job) => job.status === "running");
1084
+ const queued = jobs.filter((job) => job.status === "queued");
1085
+ const finished = jobs.filter(
1086
+ (job) => job.status !== "running" && job.status !== "queued",
1087
+ );
1088
+
1089
+ const lines: string[] = [];
1090
+ const hasActive = running.length > 0 || queued.length > 0;
1091
+ lines.push(
1092
+ truncLine(
1093
+ `${theme.fg(hasActive ? "accent" : "dim", hasActive ? "●" : "○")} ${theme.fg(hasActive ? "accent" : "dim", "Async agents")} ${theme.fg("dim", "· background")}`,
1094
+ width,
1095
+ ),
1096
+ );
1097
+
1098
+ const items: string[][] = [];
1099
+ let hiddenRunning = 0;
1100
+ let hiddenFinished = 0;
1101
+ let queuedSummaryShown = false;
1102
+ let slots = MAX_WIDGET_JOBS;
1103
+
1104
+ for (const job of running) {
1105
+ if (slots <= 0) {
1106
+ hiddenRunning++;
1107
+ continue;
1108
+ }
1109
+ const stats = widgetStats(job, theme);
1110
+ items.push([
1111
+ `${widgetStatusGlyph(job, theme)} ${themeBold(theme, widgetJobName(job))}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
1112
+ ` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
1113
+ ...widgetParallelAgentDetails(job, theme),
1114
+ ]);
1115
+ slots--;
1116
+ }
1117
+
1118
+ if (queued.length > 0 && slots > 0) {
1119
+ items.push([
1120
+ `${theme.fg("muted", "◦")} ${theme.fg("dim", `${queued.length} queued`)}`,
1121
+ ]);
1122
+ queuedSummaryShown = true;
1123
+ slots--;
1124
+ }
1125
+
1126
+ for (const job of finished) {
1127
+ if (slots <= 0) {
1128
+ hiddenFinished++;
1129
+ continue;
1130
+ }
1131
+ const stats = widgetStats(job, theme);
1132
+ items.push([
1133
+ `${widgetStatusGlyph(job, theme)} ${themeBold(theme, widgetJobName(job))}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
1134
+ ` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
1135
+ ...widgetParallelAgentDetails(job, theme),
1136
+ ]);
1137
+ slots--;
1138
+ }
1139
+
1140
+ const hiddenQueued =
1141
+ queued.length > 0 && !queuedSummaryShown ? queued.length : 0;
1142
+ const hiddenTotal = hiddenRunning + hiddenFinished + hiddenQueued;
1143
+ if (hiddenTotal > 0) {
1144
+ const parts: string[] = [];
1145
+ if (hiddenRunning > 0) parts.push(`${hiddenRunning} running`);
1146
+ if (hiddenQueued > 0) parts.push(`${hiddenQueued} queued`);
1147
+ if (hiddenFinished > 0) parts.push(`${hiddenFinished} finished`);
1148
+ items.push([theme.fg("dim", `+${hiddenTotal} more (${parts.join(", ")})`)]);
1149
+ }
1150
+
1151
+ for (let i = 0; i < items.length; i++) {
1152
+ const item = items[i]!;
1153
+ const last = i === items.length - 1;
1154
+ const branch = last ? "└─" : "├─";
1155
+ const continuation = last ? " " : "│ ";
1156
+ lines.push(truncLine(`${theme.fg("dim", branch)} ${item[0]}`, width));
1157
+ for (const detail of item.slice(1)) {
1158
+ lines.push(
1159
+ truncLine(`${theme.fg("dim", continuation)} ${detail}`, width),
1160
+ );
1161
+ }
1162
+ }
1163
+
1164
+ return lines;
773
1165
  }
774
1166
 
775
1167
  function refreshAnimatedWidget(): void {
776
- try {
777
- if (!latestWidgetCtx?.hasUI || latestWidgetJobs.length === 0) return;
778
- latestWidgetCtx.ui.setWidget(WIDGET_KEY, buildWidgetComponent(latestWidgetJobs, latestWidgetCtx.ui.getToolsExpanded?.() ?? false));
779
- latestWidgetCtx.ui.requestRender?.();
780
- } catch (error) {
781
- if (!isStaleExtensionContextError(error)) throw error;
782
- stopWidgetAnimation();
783
- }
1168
+ try {
1169
+ if (!latestWidgetCtx?.hasUI || latestWidgetJobs.length === 0) return;
1170
+ latestWidgetCtx.ui.setWidget(
1171
+ WIDGET_KEY,
1172
+ buildWidgetComponent(
1173
+ latestWidgetJobs,
1174
+ latestWidgetCtx.ui.getToolsExpanded?.() ?? false,
1175
+ ),
1176
+ );
1177
+ latestWidgetCtx.ui.requestRender?.();
1178
+ } catch (error) {
1179
+ if (!isStaleExtensionContextError(error)) throw error;
1180
+ stopWidgetAnimation();
1181
+ }
784
1182
  }
785
1183
 
786
1184
  function ensureWidgetAnimation(): void {
787
- if (widgetTimer) return;
788
- widgetTimer = setInterval(() => {
789
- if (!hasAnimatedWidgetJobs(latestWidgetJobs)) {
790
- stopWidgetAnimation();
791
- return;
792
- }
793
- refreshAnimatedWidget();
794
- }, WIDGET_ANIMATION_MS);
795
- widgetTimer.unref?.();
1185
+ if (widgetTimer) return;
1186
+ widgetTimer = setInterval(() => {
1187
+ if (!hasAnimatedWidgetJobs(latestWidgetJobs)) {
1188
+ stopWidgetAnimation();
1189
+ return;
1190
+ }
1191
+ refreshAnimatedWidget();
1192
+ }, WIDGET_ANIMATION_MS);
1193
+ widgetTimer.unref?.();
796
1194
  }
797
1195
 
798
1196
  export function stopWidgetAnimation(): void {
799
- if (widgetTimer) {
800
- clearInterval(widgetTimer);
801
- widgetTimer = undefined;
802
- }
803
- latestWidgetCtx = undefined;
804
- latestWidgetJobs = [];
805
- outputActivityCache.clear();
1197
+ if (widgetTimer) {
1198
+ clearInterval(widgetTimer);
1199
+ widgetTimer = undefined;
1200
+ }
1201
+ latestWidgetCtx = undefined;
1202
+ latestWidgetJobs = [];
1203
+ outputActivityCache.clear();
806
1204
  }
807
1205
 
808
1206
  export function stopResultAnimations(): void {
809
- for (const [timer, state] of resultAnimationTimers) {
810
- clearInterval(timer);
811
- state.subagentResultAnimationTimer = undefined;
812
- }
813
- resultAnimationTimers.clear();
1207
+ for (const [timer, state] of resultAnimationTimers) {
1208
+ clearInterval(timer);
1209
+ state.subagentResultAnimationTimer = undefined;
1210
+ }
1211
+ resultAnimationTimers.clear();
814
1212
  }
815
1213
 
816
1214
  /**
817
1215
  * Render the async jobs widget
818
1216
  */
819
- export function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void {
820
- if (jobs.length === 0) {
821
- stopWidgetAnimation();
822
- if (ctx.hasUI) ctx.ui.setWidget(WIDGET_KEY, undefined);
823
- return;
824
- }
825
- if (!ctx.hasUI) {
826
- stopWidgetAnimation();
827
- return;
828
- }
829
- latestWidgetCtx = ctx;
830
- latestWidgetJobs = [...jobs];
831
-
832
- ctx.ui.setWidget(WIDGET_KEY, buildWidgetComponent(jobs, ctx.ui.getToolsExpanded?.() ?? false));
833
- if (hasAnimatedWidgetJobs(jobs)) ensureWidgetAnimation();
834
- else stopWidgetAnimation();
1217
+ export function renderWidget(
1218
+ ctx: ExtensionContext,
1219
+ jobs: AsyncJobState[],
1220
+ ): void {
1221
+ if (jobs.length === 0) {
1222
+ stopWidgetAnimation();
1223
+ if (ctx.hasUI) ctx.ui.setWidget(WIDGET_KEY, undefined);
1224
+ return;
1225
+ }
1226
+ if (!ctx.hasUI) {
1227
+ stopWidgetAnimation();
1228
+ return;
1229
+ }
1230
+ latestWidgetCtx = ctx;
1231
+ latestWidgetJobs = [...jobs];
1232
+
1233
+ ctx.ui.setWidget(
1234
+ WIDGET_KEY,
1235
+ buildWidgetComponent(jobs, ctx.ui.getToolsExpanded?.() ?? false),
1236
+ );
1237
+ if (hasAnimatedWidgetJobs(jobs)) ensureWidgetAnimation();
1238
+ else stopWidgetAnimation();
835
1239
  }
836
1240
 
837
- function renderSingleCompact(d: Details, r: Details["results"][number], theme: Theme): Component {
838
- const output = r.truncation?.text || getSingleResultOutput(r);
839
- const progress = r.progress || r.progressSummary;
840
- const isRunning = r.progress?.status === "running";
841
- const contextBadge = d.context === "fork" ? theme.fg("warning", " [fork]") : "";
842
- const stats = statJoin(theme, [
843
- r.usage?.turns ? `⟳${r.usage.turns}` : "",
844
- formatProgressStats(theme, progress),
845
- ]);
846
- const c = new Container();
847
- const width = getTermWidth() - 4;
848
- const modelDisplay = modelThinkingBadge(theme, r.model);
849
- c.addChild(new Text(truncLine(`${resultGlyph(r, output, theme, isRunning)} ${theme.fg("toolTitle", theme.bold(r.agent))}${modelDisplay}${contextBadge}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`, width), 0, 0));
850
-
851
- if (isRunning && r.progress) {
852
- const activity = compactCurrentActivity(r.progress);
853
- c.addChild(new Text(truncLine(theme.fg("dim", ` ⎿ ${activity}`), width), 0, 0));
854
- const liveStatus = buildLiveStatusLine(r.progress);
855
- if (liveStatus && liveStatus !== activity) c.addChild(new Text(truncLine(theme.fg("dim", ` ${liveStatus}`), width), 0, 0));
856
- c.addChild(new Text(truncLine(theme.fg("accent", " Press Ctrl+O for live detail"), width), 0, 0));
857
- if (r.artifactPaths) c.addChild(new Text(truncLine(theme.fg("dim", ` output: ${shortenPath(r.artifactPaths.outputPath)}`), width), 0, 0));
858
- return c;
859
- }
860
-
861
- c.addChild(new Text(truncLine(theme.fg("dim", ` ⎿ ${resultStatusLine(r, output)}`), width), 0, 0));
862
- const preview = firstOutputLine(output);
863
- if (preview && r.exitCode === 0 && !hasEmptyTextOutputWithoutOutputTarget(r.task, output)) {
864
- c.addChild(new Text(truncLine(theme.fg("dim", ` ${preview}`), width), 0, 0));
865
- }
866
- if (r.sessionFile) c.addChild(new Text(truncLine(theme.fg("dim", ` session: ${shortenPath(r.sessionFile)}`), width), 0, 0));
867
- if (r.artifactPaths) c.addChild(new Text(truncLine(theme.fg("dim", ` output: ${shortenPath(r.artifactPaths.outputPath)}`), width), 0, 0));
868
- if (r.truncation?.artifactPath) c.addChild(new Text(truncLine(theme.fg("dim", ` full output: ${shortenPath(r.truncation.artifactPath)}`), width), 0, 0));
869
- return c;
1241
+ function renderSingleCompact(
1242
+ d: Details,
1243
+ r: Details["results"][number],
1244
+ theme: Theme,
1245
+ ): Component {
1246
+ const output = r.truncation?.text || getSingleResultOutput(r);
1247
+ const progress = r.progress || r.progressSummary;
1248
+ const isRunning = r.progress?.status === "running";
1249
+ const contextBadge =
1250
+ d.context === "fork" ? theme.fg("warning", " [fork]") : "";
1251
+ const stats = statJoin(theme, [
1252
+ r.usage?.turns ? `⟳${r.usage.turns}` : "",
1253
+ formatProgressStats(theme, progress),
1254
+ ]);
1255
+ const c = new Container();
1256
+ const width = getTermWidth() - 4;
1257
+ const modelDisplay = modelThinkingBadge(theme, r.model);
1258
+ c.addChild(
1259
+ new Text(
1260
+ truncLine(
1261
+ `${resultGlyph(r, output, theme, isRunning)} ${theme.fg("toolTitle", theme.bold(r.agent))}${modelDisplay}${contextBadge}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
1262
+ width,
1263
+ ),
1264
+ 0,
1265
+ 0,
1266
+ ),
1267
+ );
1268
+
1269
+ if (isRunning && r.progress) {
1270
+ const activity = compactCurrentActivity(r.progress);
1271
+ c.addChild(
1272
+ new Text(truncLine(theme.fg("dim", ` ${activity}`), width), 0, 0),
1273
+ );
1274
+ const liveStatus = buildLiveStatusLine(r.progress);
1275
+ if (liveStatus && liveStatus !== activity)
1276
+ c.addChild(
1277
+ new Text(truncLine(theme.fg("dim", ` ${liveStatus}`), width), 0, 0),
1278
+ );
1279
+ c.addChild(
1280
+ new Text(
1281
+ truncLine(theme.fg("accent", " Press ctrl+o for live detail"), width),
1282
+ 0,
1283
+ 0,
1284
+ ),
1285
+ );
1286
+ if (r.artifactPaths)
1287
+ c.addChild(
1288
+ new Text(
1289
+ truncLine(
1290
+ theme.fg(
1291
+ "dim",
1292
+ ` output: ${shortenPath(r.artifactPaths.outputPath)}`,
1293
+ ),
1294
+ width,
1295
+ ),
1296
+ 0,
1297
+ 0,
1298
+ ),
1299
+ );
1300
+ return c;
1301
+ }
1302
+
1303
+ c.addChild(
1304
+ new Text(
1305
+ truncLine(theme.fg("dim", ` ⎿ ${resultStatusLine(r, output)}`), width),
1306
+ 0,
1307
+ 0,
1308
+ ),
1309
+ );
1310
+ const preview = firstOutputLine(output);
1311
+ if (
1312
+ preview &&
1313
+ r.exitCode === 0 &&
1314
+ !hasEmptyTextOutputWithoutOutputTarget(r.task, output)
1315
+ ) {
1316
+ c.addChild(
1317
+ new Text(truncLine(theme.fg("dim", ` ${preview}`), width), 0, 0),
1318
+ );
1319
+ }
1320
+ if (r.sessionFile)
1321
+ c.addChild(
1322
+ new Text(
1323
+ truncLine(
1324
+ theme.fg("dim", ` session: ${shortenPath(r.sessionFile)}`),
1325
+ width,
1326
+ ),
1327
+ 0,
1328
+ 0,
1329
+ ),
1330
+ );
1331
+ if (r.artifactPaths)
1332
+ c.addChild(
1333
+ new Text(
1334
+ truncLine(
1335
+ theme.fg(
1336
+ "dim",
1337
+ ` output: ${shortenPath(r.artifactPaths.outputPath)}`,
1338
+ ),
1339
+ width,
1340
+ ),
1341
+ 0,
1342
+ 0,
1343
+ ),
1344
+ );
1345
+ if (r.truncation?.artifactPath)
1346
+ c.addChild(
1347
+ new Text(
1348
+ truncLine(
1349
+ theme.fg(
1350
+ "dim",
1351
+ ` full output: ${shortenPath(r.truncation.artifactPath)}`,
1352
+ ),
1353
+ width,
1354
+ ),
1355
+ 0,
1356
+ 0,
1357
+ ),
1358
+ );
1359
+ return c;
870
1360
  }
871
1361
 
872
1362
  function renderMultiCompact(d: Details, theme: Theme): Component {
873
- const hasRunning = d.progress?.some((p) => p.status === "running")
874
- || d.results.some((r) => r.progress?.status === "running");
875
- const failed = d.results.some((r) => r.exitCode !== 0 && r.progress?.status !== "running");
876
- const paused = d.results.some((r) => (r.interrupted || r.detached) && r.progress?.status !== "running");
877
- let totalSummary = d.progressSummary;
878
- if (!totalSummary) {
879
- let sawProgress = false;
880
- const summary = { toolCount: 0, tokens: 0, durationMs: 0 };
881
- for (const r of d.results) {
882
- const prog = r.progress || r.progressSummary;
883
- if (!prog) continue;
884
- sawProgress = true;
885
- summary.toolCount += prog.toolCount;
886
- summary.tokens += prog.tokens;
887
- summary.durationMs = d.mode === "chain" ? summary.durationMs + prog.durationMs : Math.max(summary.durationMs, prog.durationMs);
888
- }
889
- if (sawProgress) totalSummary = summary;
890
- }
891
- const multiLabel = buildMultiProgressLabel(d, hasRunning);
892
- const itemTitle = multiLabel.itemTitle;
893
- const stats = statJoin(theme, [multiLabel.headerLabel, formatProgressStats(theme, totalSummary)]);
894
- const glyph = hasRunning
895
- ? theme.fg("accent", spinnerFrame())
896
- : failed
897
- ? theme.fg("error", "✗")
898
- : paused
899
- ? theme.fg("warning", "■")
900
- : theme.fg("success", "✓");
901
- const contextBadge = d.context === "fork" ? theme.fg("warning", " [fork]") : "";
902
- const c = new Container();
903
- const width = getTermWidth() - 4;
904
- c.addChild(new Text(truncLine(`${glyph} ${theme.fg("toolTitle", theme.bold(d.mode))}${contextBadge}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`, width), 0, 0));
905
-
906
- const useResultsDirectly = multiLabel.hasParallelInChain || !d.chainAgents?.length;
907
- const displayStart = multiLabel.showActiveGroupOnly ? multiLabel.groupStartIndex : 0;
908
- const displayEnd = multiLabel.showActiveGroupOnly ? multiLabel.groupEndIndex : (useResultsDirectly ? d.results.length : d.chainAgents!.length);
909
- for (let i = displayStart; i < displayEnd; i++) {
910
- const r = d.results[i];
911
- const fallbackLabel = itemTitle.toLowerCase();
912
- const rowNumber = multiLabel.showActiveGroupOnly ? (i - multiLabel.groupStartIndex + 1) : (i + 1);
913
- const agentName = useResultsDirectly ? (r?.agent || `${fallbackLabel}-${rowNumber}`) : (d.chainAgents![i] || r?.agent || `${fallbackLabel}-${rowNumber}`);
914
- if (!r) {
915
- c.addChild(new Text(truncLine(theme.fg("dim", ` ${itemTitle} ${rowNumber}: ${agentName} · pending`), width), 0, 0));
916
- continue;
917
- }
918
- const output = getSingleResultOutput(r);
919
- const progressFromArray = d.progress?.find((p) => p.index === i) || d.progress?.find((p) => p.agent === r.agent && p.status === "running");
920
- const rProg = r.progress || progressFromArray || r.progressSummary;
921
- const rRunning = rProg && "status" in rProg && rProg.status === "running";
922
- const rPending = rProg && "status" in rProg && rProg.status === "pending";
923
- const stepNumber = r.progress?.index !== undefined ? r.progress.index + 1 : progressFromArray?.index !== undefined ? progressFromArray.index + 1 : i + 1;
924
- const stepStats = formatProgressStats(theme, rProg);
925
- const glyph = rPending ? theme.fg("dim", "◦") : resultGlyph(r, output, theme, rRunning);
926
- const pendingLabel = rPending ? ` ${theme.fg("dim", "· pending")}` : "";
927
- const stepLabel = resultRowLabel(d, multiLabel, i, stepNumber);
928
- const line = `${glyph} ${stepLabel}: ${themeBold(theme, agentName)}${stepStats ? ` ${theme.fg("dim", "·")} ${stepStats}` : ""}${pendingLabel}`;
929
- c.addChild(new Text(truncLine(` ${line}`, width), 0, 0));
930
- if (rRunning && rProg && "status" in rProg) {
931
- const activity = compactCurrentActivity(rProg);
932
- c.addChild(new Text(truncLine(theme.fg("dim", ` ⎿ ${activity}`), width), 0, 0));
933
- c.addChild(new Text(truncLine(theme.fg("accent", " Press Ctrl+O for live detail"), width), 0, 0));
934
- } else if (!rPending && (r.exitCode !== 0 || r.interrupted || r.detached || hasEmptyTextOutputWithoutOutputTarget(r.task, output))) {
935
- c.addChild(new Text(truncLine(theme.fg(r.exitCode !== 0 ? "error" : "dim", ` ⎿ ${resultStatusLine(r, output)}`), width), 0, 0));
936
- }
937
- const outputTarget = extractOutputTarget(r.task);
938
- if (outputTarget) c.addChild(new Text(truncLine(theme.fg("dim", ` output: ${outputTarget}`), width), 0, 0));
939
- if (r.artifactPaths) c.addChild(new Text(truncLine(theme.fg("dim", ` output: ${shortenPath(r.artifactPaths.outputPath)}`), width), 0, 0));
940
- }
941
- if (d.artifacts) c.addChild(new Text(truncLine(theme.fg("dim", ` artifacts: ${shortenPath(d.artifacts.dir)}`), width), 0, 0));
942
- return c;
1363
+ const hasRunning =
1364
+ d.progress?.some((p) => p.status === "running") ||
1365
+ d.results.some((r) => r.progress?.status === "running");
1366
+ const failed = d.results.some(
1367
+ (r) => r.exitCode !== 0 && r.progress?.status !== "running",
1368
+ );
1369
+ const paused = d.results.some(
1370
+ (r) => (r.interrupted || r.detached) && r.progress?.status !== "running",
1371
+ );
1372
+ let totalSummary = d.progressSummary;
1373
+ if (!totalSummary) {
1374
+ let sawProgress = false;
1375
+ const summary = { toolCount: 0, tokens: 0, durationMs: 0 };
1376
+ for (const r of d.results) {
1377
+ const prog = r.progress || r.progressSummary;
1378
+ if (!prog) continue;
1379
+ sawProgress = true;
1380
+ summary.toolCount += prog.toolCount;
1381
+ summary.tokens += prog.tokens;
1382
+ summary.durationMs =
1383
+ d.mode === "chain"
1384
+ ? summary.durationMs + prog.durationMs
1385
+ : Math.max(summary.durationMs, prog.durationMs);
1386
+ }
1387
+ if (sawProgress) totalSummary = summary;
1388
+ }
1389
+ const multiLabel = buildMultiProgressLabel(d, hasRunning);
1390
+ const itemTitle = multiLabel.itemTitle;
1391
+ const stats = statJoin(theme, [
1392
+ multiLabel.headerLabel,
1393
+ formatProgressStats(theme, totalSummary),
1394
+ ]);
1395
+ const glyph = hasRunning
1396
+ ? theme.fg("accent", spinnerFrame())
1397
+ : failed
1398
+ ? theme.fg("error", "✗")
1399
+ : paused
1400
+ ? theme.fg("warning", "■")
1401
+ : theme.fg("success", "✓");
1402
+ const contextBadge =
1403
+ d.context === "fork" ? theme.fg("warning", " [fork]") : "";
1404
+ const c = new Container();
1405
+ const width = getTermWidth() - 4;
1406
+ c.addChild(
1407
+ new Text(
1408
+ truncLine(
1409
+ `${glyph} ${theme.fg("toolTitle", theme.bold(d.mode))}${contextBadge}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
1410
+ width,
1411
+ ),
1412
+ 0,
1413
+ 0,
1414
+ ),
1415
+ );
1416
+
1417
+ const useResultsDirectly =
1418
+ multiLabel.hasParallelInChain || !d.chainAgents?.length;
1419
+ const displayStart = multiLabel.showActiveGroupOnly
1420
+ ? multiLabel.groupStartIndex
1421
+ : 0;
1422
+ const displayEnd = multiLabel.showActiveGroupOnly
1423
+ ? multiLabel.groupEndIndex
1424
+ : useResultsDirectly
1425
+ ? d.results.length
1426
+ : d.chainAgents!.length;
1427
+ for (let i = displayStart; i < displayEnd; i++) {
1428
+ const r = d.results[i];
1429
+ const fallbackLabel = itemTitle.toLowerCase();
1430
+ const rowNumber = multiLabel.showActiveGroupOnly
1431
+ ? i - multiLabel.groupStartIndex + 1
1432
+ : i + 1;
1433
+ const agentName = useResultsDirectly
1434
+ ? r?.agent || `${fallbackLabel}-${rowNumber}`
1435
+ : d.chainAgents![i] || r?.agent || `${fallbackLabel}-${rowNumber}`;
1436
+ if (!r) {
1437
+ c.addChild(
1438
+ new Text(
1439
+ truncLine(
1440
+ theme.fg(
1441
+ "dim",
1442
+ ` ◦ ${itemTitle} ${rowNumber}: ${agentName} · pending`,
1443
+ ),
1444
+ width,
1445
+ ),
1446
+ 0,
1447
+ 0,
1448
+ ),
1449
+ );
1450
+ continue;
1451
+ }
1452
+ const output = getSingleResultOutput(r);
1453
+ const progressFromArray =
1454
+ d.progress?.find((p) => p.index === i) ||
1455
+ d.progress?.find((p) => p.agent === r.agent && p.status === "running");
1456
+ const rProg = r.progress || progressFromArray || r.progressSummary;
1457
+ const rRunning = rProg && "status" in rProg && rProg.status === "running";
1458
+ const rPending = rProg && "status" in rProg && rProg.status === "pending";
1459
+ const stepNumber =
1460
+ r.progress?.index !== undefined
1461
+ ? r.progress.index + 1
1462
+ : progressFromArray?.index !== undefined
1463
+ ? progressFromArray.index + 1
1464
+ : i + 1;
1465
+ const stepStats = formatProgressStats(theme, rProg);
1466
+ const glyph = rPending
1467
+ ? theme.fg("dim", "◦")
1468
+ : resultGlyph(r, output, theme, rRunning);
1469
+ const pendingLabel = rPending ? ` ${theme.fg("dim", "· pending")}` : "";
1470
+ const stepLabel = resultRowLabel(d, multiLabel, i, stepNumber);
1471
+ const line = `${glyph} ${stepLabel}: ${themeBold(theme, agentName)}${stepStats ? ` ${theme.fg("dim", "·")} ${stepStats}` : ""}${pendingLabel}`;
1472
+ c.addChild(new Text(truncLine(` ${line}`, width), 0, 0));
1473
+ if (rRunning && rProg && "status" in rProg) {
1474
+ const activity = compactCurrentActivity(rProg);
1475
+ c.addChild(
1476
+ new Text(truncLine(theme.fg("dim", ` ⎿ ${activity}`), width), 0, 0),
1477
+ );
1478
+ c.addChild(
1479
+ new Text(
1480
+ truncLine(
1481
+ theme.fg("accent", " Press ctrl+o for live detail"),
1482
+ width,
1483
+ ),
1484
+ 0,
1485
+ 0,
1486
+ ),
1487
+ );
1488
+ } else if (
1489
+ !rPending &&
1490
+ (r.exitCode !== 0 ||
1491
+ r.interrupted ||
1492
+ r.detached ||
1493
+ hasEmptyTextOutputWithoutOutputTarget(r.task, output))
1494
+ ) {
1495
+ c.addChild(
1496
+ new Text(
1497
+ truncLine(
1498
+ theme.fg(
1499
+ r.exitCode !== 0 ? "error" : "dim",
1500
+ ` ⎿ ${resultStatusLine(r, output)}`,
1501
+ ),
1502
+ width,
1503
+ ),
1504
+ 0,
1505
+ 0,
1506
+ ),
1507
+ );
1508
+ }
1509
+ const outputTarget = extractOutputTarget(r.task);
1510
+ if (outputTarget)
1511
+ c.addChild(
1512
+ new Text(
1513
+ truncLine(theme.fg("dim", ` output: ${outputTarget}`), width),
1514
+ 0,
1515
+ 0,
1516
+ ),
1517
+ );
1518
+ if (r.artifactPaths)
1519
+ c.addChild(
1520
+ new Text(
1521
+ truncLine(
1522
+ theme.fg(
1523
+ "dim",
1524
+ ` output: ${shortenPath(r.artifactPaths.outputPath)}`,
1525
+ ),
1526
+ width,
1527
+ ),
1528
+ 0,
1529
+ 0,
1530
+ ),
1531
+ );
1532
+ }
1533
+ if (d.artifacts)
1534
+ c.addChild(
1535
+ new Text(
1536
+ truncLine(
1537
+ theme.fg("dim", ` artifacts: ${shortenPath(d.artifacts.dir)}`),
1538
+ width,
1539
+ ),
1540
+ 0,
1541
+ 0,
1542
+ ),
1543
+ );
1544
+ return c;
943
1545
  }
944
1546
 
945
1547
  /**
946
1548
  * Render a subagent result
947
1549
  */
948
1550
  export function renderSubagentResult(
949
- result: AgentToolResult<Details>,
950
- options: { expanded: boolean },
951
- theme: Theme,
1551
+ result: AgentToolResult<Details>,
1552
+ options: { expanded: boolean },
1553
+ theme: Theme,
952
1554
  ): Component {
953
- const d = result.details;
954
- if (!d || !d.results.length) {
955
- const t = result.content[0];
956
- const text = t?.type === "text" ? t.text : "(no output)";
957
- const contextPrefix = d?.context === "fork" ? `${theme.fg("warning", "[fork]")} ` : "";
958
- return new Text(truncLine(`${contextPrefix}${text}`, getTermWidth() - 4), 0, 0);
959
- }
960
-
961
- const expanded = options.expanded;
962
- const mdTheme = getMarkdownTheme();
963
-
964
- if (d.mode === "single" && d.results.length === 1) {
965
- const r = d.results[0];
966
- if (!expanded) return renderSingleCompact(d, r, theme);
967
- const isRunning = r.progress?.status === "running";
968
- const icon = isRunning
969
- ? theme.fg("warning", "running")
970
- : r.detached
971
- ? theme.fg("warning", "detached")
972
- : r.exitCode === 0
973
- ? theme.fg("success", "ok")
974
- : theme.fg("error", "failed");
975
- const contextBadge = d.context === "fork" ? theme.fg("warning", " [fork]") : "";
976
- const output = r.truncation?.text || getSingleResultOutput(r);
977
-
978
- const progressInfo = isRunning && r.progress
979
- ? ` | ${r.progress.toolCount} tools, ${formatTokens(r.progress.tokens)} tok, ${formatDuration(r.progress.durationMs)}`
980
- : r.progressSummary
981
- ? ` | ${r.progressSummary.toolCount} tools, ${formatTokens(r.progressSummary.tokens)} tok, ${formatDuration(r.progressSummary.durationMs)}`
982
- : "";
983
-
984
- const w = getTermWidth() - 4;
985
- const fit = (text: string) => expanded ? text : truncLine(text, w);
986
- const toolCallLines = getToolCallLines(r, expanded);
987
- const c = new Container();
988
- c.addChild(new Text(fit(`${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${contextBadge}${progressInfo}`), 0, 0));
989
- c.addChild(new Spacer(1));
990
- const taskMaxLen = Math.max(20, w - 8);
991
- const taskPreview = expanded || r.task.length <= taskMaxLen
992
- ? r.task
993
- : `${r.task.slice(0, taskMaxLen)}...`;
994
- c.addChild(
995
- new Text(fit(theme.fg("dim", `Task: ${taskPreview}`)), 0, 0),
996
- );
997
- c.addChild(new Spacer(1));
998
-
999
- if (isRunning && r.progress) {
1000
- const toolLine = formatCurrentToolLine(r.progress, w, expanded);
1001
- if (toolLine) {
1002
- c.addChild(new Text(fit(theme.fg("warning", `> ${toolLine}`)), 0, 0));
1003
- }
1004
- const liveStatusLine = buildLiveStatusLine(r.progress);
1005
- if (liveStatusLine) {
1006
- c.addChild(new Text(fit(theme.fg("accent", liveStatusLine)), 0, 0));
1007
- }
1008
- c.addChild(new Text(fit(theme.fg("accent", "Press Ctrl+O for live detail")), 0, 0));
1009
- if (r.artifactPaths) {
1010
- c.addChild(new Text(fit(theme.fg("dim", `Artifacts: ${shortenPath(r.artifactPaths.outputPath)}`)), 0, 0));
1011
- }
1012
- if (r.progress.recentTools?.length) {
1013
- for (const t of r.progress.recentTools.slice(-3)) {
1014
- const maxArgsLen = Math.max(40, w - 24);
1015
- const argsPreview = expanded || t.args.length <= maxArgsLen
1016
- ? t.args
1017
- : `${t.args.slice(0, maxArgsLen)}...`;
1018
- c.addChild(new Text(fit(theme.fg("dim", `${t.tool}: ${argsPreview}`)), 0, 0));
1019
- }
1020
- }
1021
- for (const line of (r.progress.recentOutput ?? []).slice(-5)) {
1022
- c.addChild(new Text(fit(theme.fg("dim", ` ${line}`)), 0, 0));
1023
- }
1024
- if (toolLine || liveStatusLine || r.progress.recentTools?.length || r.progress.recentOutput?.length || r.artifactPaths) {
1025
- c.addChild(new Spacer(1));
1026
- }
1027
- }
1028
-
1029
- if (expanded) {
1030
- for (const line of toolCallLines) {
1031
- c.addChild(new Text(fit(theme.fg("muted", line)), 0, 0));
1032
- }
1033
- if (toolCallLines.length) c.addChild(new Spacer(1));
1034
- }
1035
-
1036
- if (output) c.addChild(new Markdown(output, 0, 0, mdTheme));
1037
- c.addChild(new Spacer(1));
1038
- if (r.skills?.length) {
1039
- c.addChild(new Text(fit(theme.fg("dim", `Skills: ${r.skills.join(", ")}`)), 0, 0));
1040
- }
1041
- if (r.skillsWarning) {
1042
- c.addChild(new Text(fit(theme.fg("warning", `Warning: ${r.skillsWarning}`)), 0, 0));
1043
- }
1044
- if (r.attemptedModels && r.attemptedModels.length > 1) {
1045
- c.addChild(new Text(fit(theme.fg("dim", `Fallbacks: ${r.attemptedModels.join(" → ")}`)), 0, 0));
1046
- }
1047
- c.addChild(new Text(fit(theme.fg("dim", formatUsage(r.usage, r.model))), 0, 0));
1048
- if (r.sessionFile) {
1049
- c.addChild(new Text(fit(theme.fg("dim", `Session: ${shortenPath(r.sessionFile)}`)), 0, 0));
1050
- }
1051
-
1052
- if (!isRunning && r.artifactPaths) {
1053
- c.addChild(new Spacer(1));
1054
- c.addChild(new Text(fit(theme.fg("dim", `Artifacts: ${shortenPath(r.artifactPaths.outputPath)}`)), 0, 0));
1055
- }
1056
- return c;
1057
- }
1058
-
1059
- if (!expanded) return renderMultiCompact(d, theme);
1060
-
1061
- const hasRunning = d.progress?.some((p) => p.status === "running")
1062
- || d.results.some((r) => r.progress?.status === "running");
1063
- const ok = d.results.filter((r) => r.progress?.status === "completed" || (r.exitCode === 0 && r.progress?.status !== "running")).length;
1064
- const hasEmptyWithoutTarget = d.results.some((r) =>
1065
- r.exitCode === 0
1066
- && r.progress?.status !== "running"
1067
- && hasEmptyTextOutputWithoutOutputTarget(r.task, getSingleResultOutput(r)),
1068
- );
1069
- const icon = hasRunning
1070
- ? theme.fg("warning", "running")
1071
- : hasEmptyWithoutTarget
1072
- ? theme.fg("warning", "warning")
1073
- : ok === d.results.length
1074
- ? theme.fg("success", "ok")
1075
- : theme.fg("error", "failed");
1076
-
1077
- const totalSummary =
1078
- d.progressSummary ||
1079
- d.results.reduce(
1080
- (acc, r) => {
1081
- const prog = r.progress || r.progressSummary;
1082
- if (prog) {
1083
- acc.toolCount += prog.toolCount;
1084
- acc.tokens += prog.tokens;
1085
- acc.durationMs =
1086
- d.mode === "chain"
1087
- ? acc.durationMs + prog.durationMs
1088
- : Math.max(acc.durationMs, prog.durationMs);
1089
- }
1090
- return acc;
1091
- },
1092
- { toolCount: 0, tokens: 0, durationMs: 0 },
1093
- );
1094
-
1095
- const summaryStr =
1096
- totalSummary.toolCount || totalSummary.tokens
1097
- ? ` | ${totalSummary.toolCount} tools, ${formatTokens(totalSummary.tokens)} tok, ${formatDuration(totalSummary.durationMs)}`
1098
- : "";
1099
-
1100
- const modeLabel = d.mode;
1101
- const contextBadge = d.context === "fork" ? theme.fg("warning", " [fork]") : "";
1102
- const multiLabel = buildMultiProgressLabel(d, hasRunning);
1103
- const itemTitle = multiLabel.itemTitle;
1104
-
1105
- const chainVis = d.chainAgents?.length && !multiLabel.hasParallelInChain
1106
- ? d.chainAgents
1107
- .map((agent, i) => {
1108
- const result = d.results[i];
1109
- const isFailed = result && result.exitCode !== 0 && result.progress?.status !== "running";
1110
- const isComplete = result && result.exitCode === 0 && result.progress?.status !== "running";
1111
- const isEmptyWithoutTarget = Boolean(result)
1112
- && Boolean(isComplete)
1113
- && hasEmptyTextOutputWithoutOutputTarget(result.task, getSingleResultOutput(result));
1114
- const isCurrent = i === (d.currentStepIndex ?? d.results.length);
1115
- const stepIcon = isFailed
1116
- ? theme.fg("error", "failed")
1117
- : isEmptyWithoutTarget
1118
- ? theme.fg("warning", "warning")
1119
- : isComplete
1120
- ? theme.fg("success", "done")
1121
- : isCurrent && hasRunning
1122
- ? theme.fg("warning", "running")
1123
- : theme.fg("dim", "pending");
1124
- return `${stepIcon} ${agent}`;
1125
- })
1126
- .join(theme.fg("dim", " → "))
1127
- : null;
1128
-
1129
- const w = getTermWidth() - 4;
1130
- const fit = (text: string) => expanded ? text : truncLine(text, w);
1131
- const c = new Container();
1132
- c.addChild(
1133
- new Text(
1134
- fit(`${icon} ${theme.fg("toolTitle", theme.bold(modeLabel))}${contextBadge} · ${multiLabel.headerLabel}${summaryStr}`),
1135
- 0,
1136
- 0,
1137
- ),
1138
- );
1139
- if (chainVis) {
1140
- c.addChild(new Text(fit(` ${chainVis}`), 0, 0));
1141
- }
1142
-
1143
- const useResultsDirectly = multiLabel.hasParallelInChain || !d.chainAgents?.length;
1144
- const displayStart = multiLabel.showActiveGroupOnly ? multiLabel.groupStartIndex : 0;
1145
- const displayEnd = multiLabel.showActiveGroupOnly ? multiLabel.groupEndIndex : (useResultsDirectly ? d.results.length : d.chainAgents!.length);
1146
-
1147
- c.addChild(new Spacer(1));
1148
-
1149
- for (let i = displayStart; i < displayEnd; i++) {
1150
- const r = d.results[i];
1151
- const rowNumber = multiLabel.showActiveGroupOnly ? (i - multiLabel.groupStartIndex + 1) : (i + 1);
1152
- const agentName = useResultsDirectly
1153
- ? (r?.agent || `step-${rowNumber}`)
1154
- : (d.chainAgents![i] || r?.agent || `step-${rowNumber}`);
1155
-
1156
- if (!r) {
1157
- c.addChild(new Text(fit(theme.fg("dim", ` ${itemTitle} ${rowNumber}: ${agentName}`)), 0, 0));
1158
- c.addChild(new Text(theme.fg("dim", ` status: pending`), 0, 0));
1159
- c.addChild(new Spacer(1));
1160
- continue;
1161
- }
1162
-
1163
- const progressFromArray = d.progress?.find((p) => p.index === i)
1164
- || d.progress?.find((p) => p.agent === r.agent && p.status === "running");
1165
- const rProg = r.progress || progressFromArray || r.progressSummary;
1166
- const rRunning = rProg?.status === "running";
1167
- const stepNumber = typeof rProg?.index === "number" ? rProg.index + 1 : i + 1;
1168
-
1169
- const resultOutput = getSingleResultOutput(r);
1170
- const statusIcon = rRunning
1171
- ? theme.fg("warning", "running")
1172
- : r.exitCode !== 0
1173
- ? theme.fg("error", "failed")
1174
- : hasEmptyTextOutputWithoutOutputTarget(r.task, resultOutput)
1175
- ? theme.fg("warning", "warning")
1176
- : theme.fg("success", "done");
1177
- const stats = rProg ? ` | ${rProg.toolCount} tools, ${formatDuration(rProg.durationMs)}` : "";
1178
- const modelDisplay = modelThinkingBadge(theme, r.model);
1179
- const stepLabel = resultRowLabel(d, multiLabel, i, stepNumber);
1180
- const stepHeader = rRunning
1181
- ? `${statusIcon} ${stepLabel}: ${theme.bold(theme.fg("warning", r.agent))}${modelDisplay}${stats}`
1182
- : `${statusIcon} ${stepLabel}: ${theme.bold(r.agent)}${modelDisplay}${stats}`;
1183
- const toolCallLines = getToolCallLines(r, expanded);
1184
- c.addChild(new Text(fit(stepHeader), 0, 0));
1185
-
1186
- const taskMaxLen = Math.max(20, w - 12);
1187
- const taskPreview = expanded || r.task.length <= taskMaxLen
1188
- ? r.task
1189
- : `${r.task.slice(0, taskMaxLen)}...`;
1190
- c.addChild(new Text(fit(theme.fg("dim", ` task: ${taskPreview}`)), 0, 0));
1191
-
1192
- const outputTarget = extractOutputTarget(r.task);
1193
- if (outputTarget) {
1194
- c.addChild(new Text(fit(theme.fg("dim", ` output: ${outputTarget}`)), 0, 0));
1195
- }
1196
-
1197
- if (r.skills?.length) {
1198
- c.addChild(new Text(fit(theme.fg("dim", ` skills: ${r.skills.join(", ")}`)), 0, 0));
1199
- }
1200
- if (r.skillsWarning) {
1201
- c.addChild(new Text(fit(theme.fg("warning", ` Warning: ${r.skillsWarning}`)), 0, 0));
1202
- }
1203
- if (r.attemptedModels && r.attemptedModels.length > 1) {
1204
- c.addChild(new Text(fit(theme.fg("dim", ` fallbacks: ${r.attemptedModels.join(" ")}`)), 0, 0));
1205
- }
1206
-
1207
- if (rRunning && rProg) {
1208
- if (rProg.skills?.length) {
1209
- c.addChild(new Text(fit(theme.fg("accent", ` skills: ${rProg.skills.join(", ")}`)), 0, 0));
1210
- }
1211
- const toolLine = formatCurrentToolLine(rProg, w, expanded);
1212
- if (toolLine) {
1213
- c.addChild(new Text(fit(theme.fg("warning", ` > ${toolLine}`)), 0, 0));
1214
- }
1215
- const liveStatusLine = buildLiveStatusLine(rProg);
1216
- if (liveStatusLine) {
1217
- c.addChild(new Text(fit(theme.fg("accent", ` ${liveStatusLine}`)), 0, 0));
1218
- }
1219
- c.addChild(new Text(fit(theme.fg("accent", " Press Ctrl+O for live detail")), 0, 0));
1220
- if (r.artifactPaths) {
1221
- c.addChild(new Text(fit(theme.fg("dim", ` artifacts: ${shortenPath(r.artifactPaths.outputPath)}`)), 0, 0));
1222
- }
1223
- if (rProg.recentTools?.length) {
1224
- for (const t of rProg.recentTools.slice(-3)) {
1225
- const maxArgsLen = Math.max(40, w - 30);
1226
- const argsPreview = expanded || t.args.length <= maxArgsLen
1227
- ? t.args
1228
- : `${t.args.slice(0, maxArgsLen)}...`;
1229
- c.addChild(new Text(fit(theme.fg("dim", ` ${t.tool}: ${argsPreview}`)), 0, 0));
1230
- }
1231
- }
1232
- const recentLines = (rProg.recentOutput ?? []).slice(-5);
1233
- for (const line of recentLines) {
1234
- c.addChild(new Text(fit(theme.fg("dim", ` ${line}`)), 0, 0));
1235
- }
1236
- }
1237
-
1238
- if (!rRunning && r.artifactPaths) {
1239
- c.addChild(new Text(fit(theme.fg("dim", ` artifacts: ${shortenPath(r.artifactPaths.outputPath)}`)), 0, 0));
1240
- }
1241
-
1242
- if (expanded && !rRunning) {
1243
- for (const line of toolCallLines) {
1244
- c.addChild(new Text(fit(theme.fg("muted", ` ${line}`)), 0, 0));
1245
- }
1246
- if (toolCallLines.length) c.addChild(new Spacer(1));
1247
- }
1248
-
1249
- c.addChild(new Spacer(1));
1250
- }
1251
-
1252
- if (d.artifacts) {
1253
- c.addChild(new Spacer(1));
1254
- c.addChild(new Text(fit(theme.fg("dim", `Artifacts dir: ${shortenPath(d.artifacts.dir)}`)), 0, 0));
1255
- }
1256
- return c;
1555
+ const d = result.details;
1556
+ if (!d || !d.results.length) {
1557
+ const t = result.content[0];
1558
+ const text = t?.type === "text" ? t.text : "(no output)";
1559
+ const contextPrefix =
1560
+ d?.context === "fork" ? `${theme.fg("warning", "[fork]")} ` : "";
1561
+ return new Text(
1562
+ truncLine(`${contextPrefix}${text}`, getTermWidth() - 4),
1563
+ 0,
1564
+ 0,
1565
+ );
1566
+ }
1567
+
1568
+ const expanded = options.expanded;
1569
+ const mdTheme = getMarkdownTheme();
1570
+
1571
+ if (d.mode === "single" && d.results.length === 1) {
1572
+ const r = d.results[0];
1573
+ if (!expanded) return renderSingleCompact(d, r, theme);
1574
+ const isRunning = r.progress?.status === "running";
1575
+ const icon = isRunning
1576
+ ? theme.fg("warning", "running")
1577
+ : r.detached
1578
+ ? theme.fg("warning", "detached")
1579
+ : r.exitCode === 0
1580
+ ? theme.fg("success", "ok")
1581
+ : theme.fg("error", "failed");
1582
+ const contextBadge =
1583
+ d.context === "fork" ? theme.fg("warning", " [fork]") : "";
1584
+ const output = r.truncation?.text || getSingleResultOutput(r);
1585
+
1586
+ const progressInfo =
1587
+ isRunning && r.progress
1588
+ ? ` | ${r.progress.toolCount} tools, ${formatTokens(r.progress.tokens)} tok, ${formatDuration(r.progress.durationMs)}`
1589
+ : r.progressSummary
1590
+ ? ` | ${r.progressSummary.toolCount} tools, ${formatTokens(r.progressSummary.tokens)} tok, ${formatDuration(r.progressSummary.durationMs)}`
1591
+ : "";
1592
+
1593
+ const w = getTermWidth() - 4;
1594
+ const fit = (text: string) => (expanded ? text : truncLine(text, w));
1595
+ const toolCallLines = getToolCallLines(r, expanded);
1596
+ const c = new Container();
1597
+ c.addChild(
1598
+ new Text(
1599
+ fit(
1600
+ `${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${contextBadge}${progressInfo}`,
1601
+ ),
1602
+ 0,
1603
+ 0,
1604
+ ),
1605
+ );
1606
+ c.addChild(new Spacer(1));
1607
+ const taskMaxLen = Math.max(20, w - 8);
1608
+ const taskPreview =
1609
+ expanded || r.task.length <= taskMaxLen
1610
+ ? r.task
1611
+ : `${r.task.slice(0, taskMaxLen)}...`;
1612
+ c.addChild(new Text(fit(theme.fg("dim", `Task: ${taskPreview}`)), 0, 0));
1613
+ c.addChild(new Spacer(1));
1614
+
1615
+ if (isRunning && r.progress) {
1616
+ const toolLine = formatCurrentToolLine(r.progress, w, expanded);
1617
+ if (toolLine) {
1618
+ c.addChild(new Text(fit(theme.fg("warning", `> ${toolLine}`)), 0, 0));
1619
+ }
1620
+ const liveStatusLine = buildLiveStatusLine(r.progress);
1621
+ if (liveStatusLine) {
1622
+ c.addChild(new Text(fit(theme.fg("accent", liveStatusLine)), 0, 0));
1623
+ }
1624
+ c.addChild(
1625
+ new Text(fit(theme.fg("accent", "Press ctrl+o for live detail")), 0, 0),
1626
+ );
1627
+ if (r.artifactPaths) {
1628
+ c.addChild(
1629
+ new Text(
1630
+ fit(
1631
+ theme.fg(
1632
+ "dim",
1633
+ `Artifacts: ${shortenPath(r.artifactPaths.outputPath)}`,
1634
+ ),
1635
+ ),
1636
+ 0,
1637
+ 0,
1638
+ ),
1639
+ );
1640
+ }
1641
+ if (r.progress.recentTools?.length) {
1642
+ for (const t of r.progress.recentTools.slice(-3)) {
1643
+ const maxArgsLen = Math.max(40, w - 24);
1644
+ const argsPreview =
1645
+ expanded || t.args.length <= maxArgsLen
1646
+ ? t.args
1647
+ : `${t.args.slice(0, maxArgsLen)}...`;
1648
+ c.addChild(
1649
+ new Text(fit(theme.fg("dim", `${t.tool}: ${argsPreview}`)), 0, 0),
1650
+ );
1651
+ }
1652
+ }
1653
+ for (const line of (r.progress.recentOutput ?? []).slice(-5)) {
1654
+ c.addChild(new Text(fit(theme.fg("dim", ` ${line}`)), 0, 0));
1655
+ }
1656
+ if (
1657
+ toolLine ||
1658
+ liveStatusLine ||
1659
+ r.progress.recentTools?.length ||
1660
+ r.progress.recentOutput?.length ||
1661
+ r.artifactPaths
1662
+ ) {
1663
+ c.addChild(new Spacer(1));
1664
+ }
1665
+ }
1666
+
1667
+ if (expanded) {
1668
+ for (const line of toolCallLines) {
1669
+ c.addChild(new Text(fit(theme.fg("muted", line)), 0, 0));
1670
+ }
1671
+ if (toolCallLines.length) c.addChild(new Spacer(1));
1672
+ }
1673
+
1674
+ if (output) c.addChild(new Markdown(output, 0, 0, mdTheme));
1675
+ c.addChild(new Spacer(1));
1676
+ if (r.skills?.length) {
1677
+ c.addChild(
1678
+ new Text(fit(theme.fg("dim", `Skills: ${r.skills.join(", ")}`)), 0, 0),
1679
+ );
1680
+ }
1681
+ if (r.skillsWarning) {
1682
+ c.addChild(
1683
+ new Text(fit(theme.fg("warning", `Warning: ${r.skillsWarning}`)), 0, 0),
1684
+ );
1685
+ }
1686
+ if (r.attemptedModels && r.attemptedModels.length > 1) {
1687
+ c.addChild(
1688
+ new Text(
1689
+ fit(theme.fg("dim", `Fallbacks: ${r.attemptedModels.join(" ")}`)),
1690
+ 0,
1691
+ 0,
1692
+ ),
1693
+ );
1694
+ }
1695
+ c.addChild(
1696
+ new Text(fit(theme.fg("dim", formatUsage(r.usage, r.model))), 0, 0),
1697
+ );
1698
+ if (r.sessionFile) {
1699
+ c.addChild(
1700
+ new Text(
1701
+ fit(theme.fg("dim", `Session: ${shortenPath(r.sessionFile)}`)),
1702
+ 0,
1703
+ 0,
1704
+ ),
1705
+ );
1706
+ }
1707
+
1708
+ if (!isRunning && r.artifactPaths) {
1709
+ c.addChild(new Spacer(1));
1710
+ c.addChild(
1711
+ new Text(
1712
+ fit(
1713
+ theme.fg(
1714
+ "dim",
1715
+ `Artifacts: ${shortenPath(r.artifactPaths.outputPath)}`,
1716
+ ),
1717
+ ),
1718
+ 0,
1719
+ 0,
1720
+ ),
1721
+ );
1722
+ }
1723
+ return c;
1724
+ }
1725
+
1726
+ if (!expanded) return renderMultiCompact(d, theme);
1727
+
1728
+ const hasRunning =
1729
+ d.progress?.some((p) => p.status === "running") ||
1730
+ d.results.some((r) => r.progress?.status === "running");
1731
+ const ok = d.results.filter(
1732
+ (r) =>
1733
+ r.progress?.status === "completed" ||
1734
+ (r.exitCode === 0 && r.progress?.status !== "running"),
1735
+ ).length;
1736
+ const hasEmptyWithoutTarget = d.results.some(
1737
+ (r) =>
1738
+ r.exitCode === 0 &&
1739
+ r.progress?.status !== "running" &&
1740
+ hasEmptyTextOutputWithoutOutputTarget(r.task, getSingleResultOutput(r)),
1741
+ );
1742
+ const icon = hasRunning
1743
+ ? theme.fg("warning", "running")
1744
+ : hasEmptyWithoutTarget
1745
+ ? theme.fg("warning", "warning")
1746
+ : ok === d.results.length
1747
+ ? theme.fg("success", "ok")
1748
+ : theme.fg("error", "failed");
1749
+
1750
+ const totalSummary =
1751
+ d.progressSummary ||
1752
+ d.results.reduce(
1753
+ (acc, r) => {
1754
+ const prog = r.progress || r.progressSummary;
1755
+ if (prog) {
1756
+ acc.toolCount += prog.toolCount;
1757
+ acc.tokens += prog.tokens;
1758
+ acc.durationMs =
1759
+ d.mode === "chain"
1760
+ ? acc.durationMs + prog.durationMs
1761
+ : Math.max(acc.durationMs, prog.durationMs);
1762
+ }
1763
+ return acc;
1764
+ },
1765
+ { toolCount: 0, tokens: 0, durationMs: 0 },
1766
+ );
1767
+
1768
+ const summaryStr =
1769
+ totalSummary.toolCount || totalSummary.tokens
1770
+ ? ` | ${totalSummary.toolCount} tools, ${formatTokens(totalSummary.tokens)} tok, ${formatDuration(totalSummary.durationMs)}`
1771
+ : "";
1772
+
1773
+ const modeLabel = d.mode;
1774
+ const contextBadge =
1775
+ d.context === "fork" ? theme.fg("warning", " [fork]") : "";
1776
+ const multiLabel = buildMultiProgressLabel(d, hasRunning);
1777
+ const itemTitle = multiLabel.itemTitle;
1778
+
1779
+ const chainVis =
1780
+ d.chainAgents?.length && !multiLabel.hasParallelInChain
1781
+ ? d.chainAgents
1782
+ .map((agent, i) => {
1783
+ const result = d.results[i];
1784
+ const isFailed =
1785
+ result &&
1786
+ result.exitCode !== 0 &&
1787
+ result.progress?.status !== "running";
1788
+ const isComplete =
1789
+ result &&
1790
+ result.exitCode === 0 &&
1791
+ result.progress?.status !== "running";
1792
+ const isEmptyWithoutTarget =
1793
+ Boolean(result) &&
1794
+ Boolean(isComplete) &&
1795
+ hasEmptyTextOutputWithoutOutputTarget(
1796
+ result.task,
1797
+ getSingleResultOutput(result),
1798
+ );
1799
+ const isCurrent = i === (d.currentStepIndex ?? d.results.length);
1800
+ const stepIcon = isFailed
1801
+ ? theme.fg("error", "failed")
1802
+ : isEmptyWithoutTarget
1803
+ ? theme.fg("warning", "warning")
1804
+ : isComplete
1805
+ ? theme.fg("success", "done")
1806
+ : isCurrent && hasRunning
1807
+ ? theme.fg("warning", "running")
1808
+ : theme.fg("dim", "pending");
1809
+ return `${stepIcon} ${agent}`;
1810
+ })
1811
+ .join(theme.fg("dim", " "))
1812
+ : null;
1813
+
1814
+ const w = getTermWidth() - 4;
1815
+ const fit = (text: string) => (expanded ? text : truncLine(text, w));
1816
+ const c = new Container();
1817
+ c.addChild(
1818
+ new Text(
1819
+ fit(
1820
+ `${icon} ${theme.fg("toolTitle", theme.bold(modeLabel))}${contextBadge} · ${multiLabel.headerLabel}${summaryStr}`,
1821
+ ),
1822
+ 0,
1823
+ 0,
1824
+ ),
1825
+ );
1826
+ if (chainVis) {
1827
+ c.addChild(new Text(fit(` ${chainVis}`), 0, 0));
1828
+ }
1829
+
1830
+ const useResultsDirectly =
1831
+ multiLabel.hasParallelInChain || !d.chainAgents?.length;
1832
+ const displayStart = multiLabel.showActiveGroupOnly
1833
+ ? multiLabel.groupStartIndex
1834
+ : 0;
1835
+ const displayEnd = multiLabel.showActiveGroupOnly
1836
+ ? multiLabel.groupEndIndex
1837
+ : useResultsDirectly
1838
+ ? d.results.length
1839
+ : d.chainAgents!.length;
1840
+
1841
+ c.addChild(new Spacer(1));
1842
+
1843
+ for (let i = displayStart; i < displayEnd; i++) {
1844
+ const r = d.results[i];
1845
+ const rowNumber = multiLabel.showActiveGroupOnly
1846
+ ? i - multiLabel.groupStartIndex + 1
1847
+ : i + 1;
1848
+ const agentName = useResultsDirectly
1849
+ ? r?.agent || `step-${rowNumber}`
1850
+ : d.chainAgents![i] || r?.agent || `step-${rowNumber}`;
1851
+
1852
+ if (!r) {
1853
+ c.addChild(
1854
+ new Text(
1855
+ fit(theme.fg("dim", ` ${itemTitle} ${rowNumber}: ${agentName}`)),
1856
+ 0,
1857
+ 0,
1858
+ ),
1859
+ );
1860
+ c.addChild(new Text(theme.fg("dim", ` status: pending`), 0, 0));
1861
+ c.addChild(new Spacer(1));
1862
+ continue;
1863
+ }
1864
+
1865
+ const progressFromArray =
1866
+ d.progress?.find((p) => p.index === i) ||
1867
+ d.progress?.find((p) => p.agent === r.agent && p.status === "running");
1868
+ const rProg = r.progress || progressFromArray || r.progressSummary;
1869
+ const rRunning = rProg?.status === "running";
1870
+ const stepNumber =
1871
+ typeof rProg?.index === "number" ? rProg.index + 1 : i + 1;
1872
+
1873
+ const resultOutput = getSingleResultOutput(r);
1874
+ const statusIcon = rRunning
1875
+ ? theme.fg("warning", "running")
1876
+ : r.exitCode !== 0
1877
+ ? theme.fg("error", "failed")
1878
+ : hasEmptyTextOutputWithoutOutputTarget(r.task, resultOutput)
1879
+ ? theme.fg("warning", "warning")
1880
+ : theme.fg("success", "done");
1881
+ const stats = rProg
1882
+ ? ` | ${rProg.toolCount} tools, ${formatDuration(rProg.durationMs)}`
1883
+ : "";
1884
+ const modelDisplay = modelThinkingBadge(theme, r.model);
1885
+ const stepLabel = resultRowLabel(d, multiLabel, i, stepNumber);
1886
+ const stepHeader = rRunning
1887
+ ? `${statusIcon} ${stepLabel}: ${theme.bold(theme.fg("warning", r.agent))}${modelDisplay}${stats}`
1888
+ : `${statusIcon} ${stepLabel}: ${theme.bold(r.agent)}${modelDisplay}${stats}`;
1889
+ const toolCallLines = getToolCallLines(r, expanded);
1890
+ c.addChild(new Text(fit(stepHeader), 0, 0));
1891
+
1892
+ const taskMaxLen = Math.max(20, w - 12);
1893
+ const taskPreview =
1894
+ expanded || r.task.length <= taskMaxLen
1895
+ ? r.task
1896
+ : `${r.task.slice(0, taskMaxLen)}...`;
1897
+ c.addChild(
1898
+ new Text(fit(theme.fg("dim", ` task: ${taskPreview}`)), 0, 0),
1899
+ );
1900
+
1901
+ const outputTarget = extractOutputTarget(r.task);
1902
+ if (outputTarget) {
1903
+ c.addChild(
1904
+ new Text(fit(theme.fg("dim", ` output: ${outputTarget}`)), 0, 0),
1905
+ );
1906
+ }
1907
+
1908
+ if (r.skills?.length) {
1909
+ c.addChild(
1910
+ new Text(
1911
+ fit(theme.fg("dim", ` skills: ${r.skills.join(", ")}`)),
1912
+ 0,
1913
+ 0,
1914
+ ),
1915
+ );
1916
+ }
1917
+ if (r.skillsWarning) {
1918
+ c.addChild(
1919
+ new Text(
1920
+ fit(theme.fg("warning", ` Warning: ${r.skillsWarning}`)),
1921
+ 0,
1922
+ 0,
1923
+ ),
1924
+ );
1925
+ }
1926
+ if (r.attemptedModels && r.attemptedModels.length > 1) {
1927
+ c.addChild(
1928
+ new Text(
1929
+ fit(
1930
+ theme.fg("dim", ` fallbacks: ${r.attemptedModels.join(" → ")}`),
1931
+ ),
1932
+ 0,
1933
+ 0,
1934
+ ),
1935
+ );
1936
+ }
1937
+
1938
+ if (rRunning && rProg) {
1939
+ if (rProg.skills?.length) {
1940
+ c.addChild(
1941
+ new Text(
1942
+ fit(theme.fg("accent", ` skills: ${rProg.skills.join(", ")}`)),
1943
+ 0,
1944
+ 0,
1945
+ ),
1946
+ );
1947
+ }
1948
+ const toolLine = formatCurrentToolLine(rProg, w, expanded);
1949
+ if (toolLine) {
1950
+ c.addChild(
1951
+ new Text(fit(theme.fg("warning", ` > ${toolLine}`)), 0, 0),
1952
+ );
1953
+ }
1954
+ const liveStatusLine = buildLiveStatusLine(rProg);
1955
+ if (liveStatusLine) {
1956
+ c.addChild(
1957
+ new Text(fit(theme.fg("accent", ` ${liveStatusLine}`)), 0, 0),
1958
+ );
1959
+ }
1960
+ c.addChild(
1961
+ new Text(
1962
+ fit(theme.fg("accent", " Press ctrl+o for live detail")),
1963
+ 0,
1964
+ 0,
1965
+ ),
1966
+ );
1967
+ if (r.artifactPaths) {
1968
+ c.addChild(
1969
+ new Text(
1970
+ fit(
1971
+ theme.fg(
1972
+ "dim",
1973
+ ` artifacts: ${shortenPath(r.artifactPaths.outputPath)}`,
1974
+ ),
1975
+ ),
1976
+ 0,
1977
+ 0,
1978
+ ),
1979
+ );
1980
+ }
1981
+ if (rProg.recentTools?.length) {
1982
+ for (const t of rProg.recentTools.slice(-3)) {
1983
+ const maxArgsLen = Math.max(40, w - 30);
1984
+ const argsPreview =
1985
+ expanded || t.args.length <= maxArgsLen
1986
+ ? t.args
1987
+ : `${t.args.slice(0, maxArgsLen)}...`;
1988
+ c.addChild(
1989
+ new Text(
1990
+ fit(theme.fg("dim", ` ${t.tool}: ${argsPreview}`)),
1991
+ 0,
1992
+ 0,
1993
+ ),
1994
+ );
1995
+ }
1996
+ }
1997
+ const recentLines = (rProg.recentOutput ?? []).slice(-5);
1998
+ for (const line of recentLines) {
1999
+ c.addChild(new Text(fit(theme.fg("dim", ` ${line}`)), 0, 0));
2000
+ }
2001
+ }
2002
+
2003
+ if (!rRunning && r.artifactPaths) {
2004
+ c.addChild(
2005
+ new Text(
2006
+ fit(
2007
+ theme.fg(
2008
+ "dim",
2009
+ ` artifacts: ${shortenPath(r.artifactPaths.outputPath)}`,
2010
+ ),
2011
+ ),
2012
+ 0,
2013
+ 0,
2014
+ ),
2015
+ );
2016
+ }
2017
+
2018
+ if (expanded && !rRunning) {
2019
+ for (const line of toolCallLines) {
2020
+ c.addChild(new Text(fit(theme.fg("muted", ` ${line}`)), 0, 0));
2021
+ }
2022
+ if (toolCallLines.length) c.addChild(new Spacer(1));
2023
+ }
2024
+
2025
+ c.addChild(new Spacer(1));
2026
+ }
2027
+
2028
+ if (d.artifacts) {
2029
+ c.addChild(new Spacer(1));
2030
+ c.addChild(
2031
+ new Text(
2032
+ fit(theme.fg("dim", `Artifacts dir: ${shortenPath(d.artifacts.dir)}`)),
2033
+ 0,
2034
+ 0,
2035
+ ),
2036
+ );
2037
+ }
2038
+ return c;
1257
2039
  }