@astralyn/pi 0.84.3 → 0.84.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 (269) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/cli/args.d.ts +1 -0
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +11 -0
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/cli/auth-command.d.ts.map +1 -1
  7. package/dist/cli/auth-command.js +5 -4
  8. package/dist/cli/auth-command.js.map +1 -1
  9. package/dist/cli/project-trust.d.ts.map +1 -1
  10. package/dist/cli/project-trust.js +1 -4
  11. package/dist/cli/project-trust.js.map +1 -1
  12. package/dist/core/agent-session.d.ts +6 -1
  13. package/dist/core/agent-session.d.ts.map +1 -1
  14. package/dist/core/agent-session.js +7 -6
  15. package/dist/core/agent-session.js.map +1 -1
  16. package/dist/core/experimental.d.ts +4 -0
  17. package/dist/core/experimental.d.ts.map +1 -1
  18. package/dist/core/experimental.js +4 -0
  19. package/dist/core/experimental.js.map +1 -1
  20. package/dist/core/extensions/index.d.ts +1 -1
  21. package/dist/core/extensions/index.d.ts.map +1 -1
  22. package/dist/core/extensions/index.js.map +1 -1
  23. package/dist/core/extensions/runner.d.ts.map +1 -1
  24. package/dist/core/extensions/runner.js +4 -0
  25. package/dist/core/extensions/runner.js.map +1 -1
  26. package/dist/core/extensions/types.d.ts +10 -14
  27. package/dist/core/extensions/types.d.ts.map +1 -1
  28. package/dist/core/extensions/types.js.map +1 -1
  29. package/dist/core/keybindings.d.ts +29 -0
  30. package/dist/core/keybindings.d.ts.map +1 -1
  31. package/dist/core/keybindings.js +4 -0
  32. package/dist/core/keybindings.js.map +1 -1
  33. package/dist/core/model-config.d.ts +5 -0
  34. package/dist/core/model-config.d.ts.map +1 -1
  35. package/dist/core/model-config.js +1 -0
  36. package/dist/core/model-config.js.map +1 -1
  37. package/dist/core/model-registry.d.ts +2 -0
  38. package/dist/core/model-registry.d.ts.map +1 -1
  39. package/dist/core/model-registry.js +4 -0
  40. package/dist/core/model-registry.js.map +1 -1
  41. package/dist/core/project-trust.js +2 -2
  42. package/dist/core/project-trust.js.map +1 -1
  43. package/dist/core/sdk.d.ts +5 -3
  44. package/dist/core/sdk.d.ts.map +1 -1
  45. package/dist/core/sdk.js +2 -1
  46. package/dist/core/sdk.js.map +1 -1
  47. package/dist/core/session-manager.js +2 -2
  48. package/dist/core/session-manager.js.map +1 -1
  49. package/dist/core/settings-manager.d.ts +6 -0
  50. package/dist/core/settings-manager.d.ts.map +1 -1
  51. package/dist/core/settings-manager.js +12 -0
  52. package/dist/core/settings-manager.js.map +1 -1
  53. package/dist/core/system-prompt.js +1 -1
  54. package/dist/core/system-prompt.js.map +1 -1
  55. package/dist/core/tools/bash.d.ts +2 -1
  56. package/dist/core/tools/bash.d.ts.map +1 -1
  57. package/dist/core/tools/bash.js +3 -1
  58. package/dist/core/tools/bash.js.map +1 -1
  59. package/dist/core/tools/edit.d.ts.map +1 -1
  60. package/dist/core/tools/edit.js +2 -0
  61. package/dist/core/tools/edit.js.map +1 -1
  62. package/dist/core/tools/find.js +1 -1
  63. package/dist/core/tools/find.js.map +1 -1
  64. package/dist/core/tools/grep.js +1 -1
  65. package/dist/core/tools/grep.js.map +1 -1
  66. package/dist/core/tools/read.d.ts.map +1 -1
  67. package/dist/core/tools/read.js +2 -0
  68. package/dist/core/tools/read.js.map +1 -1
  69. package/dist/core/tools/write.d.ts.map +1 -1
  70. package/dist/core/tools/write.js +2 -0
  71. package/dist/core/tools/write.js.map +1 -1
  72. package/dist/extensions/background/index.d.ts +74 -0
  73. package/dist/extensions/background/index.d.ts.map +1 -0
  74. package/dist/extensions/background/index.js +332 -0
  75. package/dist/extensions/background/index.js.map +1 -0
  76. package/dist/extensions/background/manager.d.ts +81 -0
  77. package/dist/extensions/background/manager.d.ts.map +1 -0
  78. package/dist/extensions/background/manager.js +320 -0
  79. package/dist/extensions/background/manager.js.map +1 -0
  80. package/dist/extensions/background/registry.d.ts +138 -0
  81. package/dist/extensions/background/registry.d.ts.map +1 -0
  82. package/dist/extensions/background/registry.js +358 -0
  83. package/dist/extensions/background/registry.js.map +1 -0
  84. package/dist/extensions/background/render.d.ts +26 -0
  85. package/dist/extensions/background/render.d.ts.map +1 -0
  86. package/dist/extensions/background/render.js +118 -0
  87. package/dist/extensions/background/render.js.map +1 -0
  88. package/dist/extensions/index.d.ts.map +1 -1
  89. package/dist/extensions/index.js +2 -0
  90. package/dist/extensions/index.js.map +1 -1
  91. package/dist/extensions/subagent/activity.d.ts +0 -1
  92. package/dist/extensions/subagent/activity.d.ts.map +1 -1
  93. package/dist/extensions/subagent/activity.js +2 -5
  94. package/dist/extensions/subagent/activity.js.map +1 -1
  95. package/dist/extensions/subagent/agents-command.d.ts +4 -0
  96. package/dist/extensions/subagent/agents-command.d.ts.map +1 -0
  97. package/dist/extensions/subagent/agents-command.js +385 -0
  98. package/dist/extensions/subagent/agents-command.js.map +1 -0
  99. package/dist/extensions/subagent/agents.d.ts +3 -6
  100. package/dist/extensions/subagent/agents.d.ts.map +1 -1
  101. package/dist/extensions/subagent/agents.js +22 -190
  102. package/dist/extensions/subagent/agents.js.map +1 -1
  103. package/dist/extensions/subagent/constants.d.ts +4 -5
  104. package/dist/extensions/subagent/constants.d.ts.map +1 -1
  105. package/dist/extensions/subagent/constants.js +11 -7
  106. package/dist/extensions/subagent/constants.js.map +1 -1
  107. package/dist/extensions/subagent/index.d.ts +0 -3
  108. package/dist/extensions/subagent/index.d.ts.map +1 -1
  109. package/dist/extensions/subagent/index.js +58 -315
  110. package/dist/extensions/subagent/index.js.map +1 -1
  111. package/dist/extensions/subagent/render.d.ts +1 -1
  112. package/dist/extensions/subagent/render.d.ts.map +1 -1
  113. package/dist/extensions/subagent/render.js +208 -585
  114. package/dist/extensions/subagent/render.js.map +1 -1
  115. package/dist/extensions/subagent/resolve.d.ts +2 -2
  116. package/dist/extensions/subagent/resolve.d.ts.map +1 -1
  117. package/dist/extensions/subagent/resolve.js +23 -15
  118. package/dist/extensions/subagent/resolve.js.map +1 -1
  119. package/dist/extensions/subagent/runner.d.ts +1 -2
  120. package/dist/extensions/subagent/runner.d.ts.map +1 -1
  121. package/dist/extensions/subagent/runner.js +109 -131
  122. package/dist/extensions/subagent/runner.js.map +1 -1
  123. package/dist/extensions/subagent/schema.d.ts +4 -10
  124. package/dist/extensions/subagent/schema.d.ts.map +1 -1
  125. package/dist/extensions/subagent/schema.js +8 -13
  126. package/dist/extensions/subagent/schema.js.map +1 -1
  127. package/dist/extensions/subagent/sdk-runner.d.ts.map +1 -1
  128. package/dist/extensions/subagent/sdk-runner.js +9 -20
  129. package/dist/extensions/subagent/sdk-runner.js.map +1 -1
  130. package/dist/extensions/subagent/settings.d.ts +2 -3
  131. package/dist/extensions/subagent/settings.d.ts.map +1 -1
  132. package/dist/extensions/subagent/settings.js +30 -21
  133. package/dist/extensions/subagent/settings.js.map +1 -1
  134. package/dist/extensions/subagent/text.d.ts +3 -0
  135. package/dist/extensions/subagent/text.d.ts.map +1 -0
  136. package/dist/extensions/subagent/text.js +16 -0
  137. package/dist/extensions/subagent/text.js.map +1 -0
  138. package/dist/extensions/subagent/types.d.ts +13 -33
  139. package/dist/extensions/subagent/types.d.ts.map +1 -1
  140. package/dist/extensions/subagent/types.js.map +1 -1
  141. package/dist/extensions/subagent/ui/model-list.d.ts +68 -0
  142. package/dist/extensions/subagent/ui/model-list.d.ts.map +1 -0
  143. package/dist/extensions/subagent/ui/model-list.js +248 -0
  144. package/dist/extensions/subagent/ui/model-list.js.map +1 -0
  145. package/dist/extensions/todo/constants.d.ts +6 -17
  146. package/dist/extensions/todo/constants.d.ts.map +1 -1
  147. package/dist/extensions/todo/constants.js +20 -31
  148. package/dist/extensions/todo/constants.js.map +1 -1
  149. package/dist/extensions/todo/index.d.ts.map +1 -1
  150. package/dist/extensions/todo/index.js +36 -57
  151. package/dist/extensions/todo/index.js.map +1 -1
  152. package/dist/extensions/todo/schema.d.ts +20 -59
  153. package/dist/extensions/todo/schema.d.ts.map +1 -1
  154. package/dist/extensions/todo/schema.js +23 -69
  155. package/dist/extensions/todo/schema.js.map +1 -1
  156. package/dist/extensions/todo/state.d.ts +23 -65
  157. package/dist/extensions/todo/state.d.ts.map +1 -1
  158. package/dist/extensions/todo/state.js +238 -1262
  159. package/dist/extensions/todo/state.js.map +1 -1
  160. package/dist/extensions/todo/view.d.ts +19 -10
  161. package/dist/extensions/todo/view.d.ts.map +1 -1
  162. package/dist/extensions/todo/view.js +411 -463
  163. package/dist/extensions/todo/view.js.map +1 -1
  164. package/dist/extensions/todo/widget.d.ts +15 -0
  165. package/dist/extensions/todo/widget.d.ts.map +1 -0
  166. package/dist/extensions/todo/widget.js +50 -0
  167. package/dist/extensions/todo/widget.js.map +1 -0
  168. package/dist/index.d.ts +1 -1
  169. package/dist/index.d.ts.map +1 -1
  170. package/dist/index.js.map +1 -1
  171. package/dist/main.d.ts.map +1 -1
  172. package/dist/main.js +5 -1
  173. package/dist/main.js.map +1 -1
  174. package/dist/modes/interactive/components/extension-selector.d.ts +1 -2
  175. package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
  176. package/dist/modes/interactive/components/extension-selector.js +5 -11
  177. package/dist/modes/interactive/components/extension-selector.js.map +1 -1
  178. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  179. package/dist/modes/interactive/components/model-selector.js +2 -1
  180. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  181. package/dist/modes/interactive/components/settings-selector.d.ts +3 -1
  182. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  183. package/dist/modes/interactive/components/settings-selector.js +10 -0
  184. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  185. package/dist/modes/interactive/interactive-mode.d.ts +8 -2
  186. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  187. package/dist/modes/interactive/interactive-mode.js +77 -25
  188. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  189. package/dist/modes/interactive/model-catalog-refresh.d.ts +7 -0
  190. package/dist/modes/interactive/model-catalog-refresh.d.ts.map +1 -0
  191. package/dist/modes/interactive/model-catalog-refresh.js +36 -0
  192. package/dist/modes/interactive/model-catalog-refresh.js.map +1 -0
  193. package/dist/modes/interactive/theme/dark.json +2 -0
  194. package/dist/modes/interactive/theme/light.json +2 -0
  195. package/dist/modes/interactive/theme/theme-controller.d.ts +10 -2
  196. package/dist/modes/interactive/theme/theme-controller.d.ts.map +1 -1
  197. package/dist/modes/interactive/theme/theme-controller.js +23 -10
  198. package/dist/modes/interactive/theme/theme-controller.js.map +1 -1
  199. package/dist/modes/interactive/theme/theme-schema.json +9 -1
  200. package/dist/modes/interactive/theme/theme.d.ts +5 -3
  201. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  202. package/dist/modes/interactive/theme/theme.js +12 -2
  203. package/dist/modes/interactive/theme/theme.js.map +1 -1
  204. package/dist/modes/json-event.d.ts +4 -1
  205. package/dist/modes/json-event.d.ts.map +1 -1
  206. package/dist/modes/json-event.js +5 -2
  207. package/dist/modes/json-event.js.map +1 -1
  208. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  209. package/dist/modes/rpc/rpc-mode.js +1 -8
  210. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  211. package/dist/package-manager-cli.js +1 -1
  212. package/dist/package-manager-cli.js.map +1 -1
  213. package/dist/server/create-harness.d.ts.map +1 -1
  214. package/dist/server/create-harness.js +2 -0
  215. package/dist/server/create-harness.js.map +1 -1
  216. package/dist/utils/tools-manager.d.ts +10 -1
  217. package/dist/utils/tools-manager.d.ts.map +1 -1
  218. package/dist/utils/tools-manager.js +14 -19
  219. package/dist/utils/tools-manager.js.map +1 -1
  220. package/docs/bundled/README.md +2 -1
  221. package/docs/bundled/extensions/background.md +64 -0
  222. package/docs/bundled/extensions/subagent.md +84 -57
  223. package/docs/bundled/extensions/todo.md +84 -24
  224. package/docs/bundled/tool-presentation.md +1 -1
  225. package/docs/compaction.md +6 -6
  226. package/docs/custom-provider.md +1 -1
  227. package/docs/docs.json +4 -0
  228. package/docs/environment-variables.md +8 -2
  229. package/docs/extensions.md +6 -2
  230. package/docs/json.md +6 -4
  231. package/docs/keybindings.md +9 -2
  232. package/docs/rpc.md +15 -4
  233. package/docs/sdk.md +9 -0
  234. package/docs/settings.md +31 -0
  235. package/docs/terminal-setup.md +23 -2
  236. package/docs/themes.md +24 -3
  237. package/docs/tui.md +2 -2
  238. package/docs/usage.md +4 -3
  239. package/examples/README.md +1 -1
  240. package/examples/extensions/README.md +0 -1
  241. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  242. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  243. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  244. package/examples/extensions/gondolin/package-lock.json +2 -2
  245. package/examples/extensions/gondolin/package.json +1 -1
  246. package/examples/extensions/sandbox/package-lock.json +2 -2
  247. package/examples/extensions/sandbox/package.json +1 -1
  248. package/examples/extensions/with-deps/package-lock.json +2 -2
  249. package/examples/extensions/with-deps/package.json +1 -1
  250. package/npm-shrinkwrap.json +170 -221
  251. package/package.json +6 -6
  252. package/dist/extensions/subagent/picker.d.ts +0 -31
  253. package/dist/extensions/subagent/picker.d.ts.map +0 -1
  254. package/dist/extensions/subagent/picker.js +0 -109
  255. package/dist/extensions/subagent/picker.js.map +0 -1
  256. package/dist/extensions/todo/overlay.d.ts +0 -24
  257. package/dist/extensions/todo/overlay.d.ts.map +0 -1
  258. package/dist/extensions/todo/overlay.js +0 -135
  259. package/dist/extensions/todo/overlay.js.map +0 -1
  260. package/examples/extensions/subagent/README.md +0 -175
  261. package/examples/extensions/subagent/agents/planner.md +0 -37
  262. package/examples/extensions/subagent/agents/reviewer.md +0 -35
  263. package/examples/extensions/subagent/agents/scout.md +0 -50
  264. package/examples/extensions/subagent/agents/worker.md +0 -24
  265. package/examples/extensions/subagent/agents.ts +0 -126
  266. package/examples/extensions/subagent/index.ts +0 -1009
  267. package/examples/extensions/subagent/prompts/implement-and-review.md +0 -10
  268. package/examples/extensions/subagent/prompts/implement.md +0 -10
  269. package/examples/extensions/subagent/prompts/scout-and-plan.md +0 -9
@@ -1,232 +1,52 @@
1
1
  import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
2
- import { keyHint } from "../../modes/interactive/components/keybinding-hints.js";
3
2
  import { getMarkdownTheme } from "../../modes/interactive/theme/theme.js";
4
3
  import { getSubagentRetryView } from "./retry.js";
5
4
  import { statusSummary } from "./runner.js";
6
- const SINGLE_EXCERPT_LIMIT = 200;
7
- const RUN_LINE_EXCERPT_LIMIT = 96;
8
- const RUN_ERROR_EXCERPT_LIMIT = 200;
9
- const RETRY_ERROR_EXCERPT_LIMIT = 160;
10
- const LIVE_TAIL_LIMIT = 100;
11
- const COLLAPSED_RUN_LIMIT = 4;
12
- const PROMPT_PREVIEW_LINES = 2;
13
- const PROMPT_PREVIEW_LINE_LIMIT = 120;
14
- const ACTIVITY_SUMMARY_LIMIT = 96;
15
- const ACTIVITY_RESULT_LIMIT = 64;
16
- // Sub-10s tool calls are routine; a duration only earns ink when it
17
- // explains where the time went.
5
+ import { firstPlainLine, plainLine } from "./text.js";
6
+ const TASK_SUMMARY_LIMIT = 72;
7
+ const ACTIVITY_SUMMARY_LIMIT = 120;
8
+ const ACTIVITY_RESULT_LIMIT = 96;
9
+ const RETRY_ERROR_LIMIT = 160;
10
+ const FALLBACK_OUTPUT_LIMIT = 4_000;
18
11
  const ACTIVITY_DURATION_MIN_MS = 10_000;
19
- const EXPANDED_LIVE_TAIL_LINES = 3;
20
- class IndentedText {
21
- constructor(text, indent) {
22
- this.text = new Text(text, 0, 0);
23
- this.indent = indent;
24
- }
25
- render(width) {
26
- const indent = Math.min(this.indent, Math.max(0, width - 1));
27
- const prefix = " ".repeat(indent);
28
- return this.text.render(Math.max(1, width - indent)).map((line) => `${prefix}${line}`);
29
- }
30
- invalidate() {
31
- this.text.invalidate();
32
- }
33
- }
34
- // Display-only: profile names stay lowercase everywhere the model sees
35
- // them; the transcript shows "general" as "General", "code-reviewer" as
36
- // "Code Reviewer".
37
- function displayAgentName(name) {
38
- return name
39
- .split(/[-_]/u)
40
- .filter(Boolean)
41
- .map((word) => `${word[0]?.toUpperCase() ?? ""}${word.slice(1)}`)
42
- .join(" ");
43
- }
44
- function singleAgentName(args) {
45
- return displayAgentName(args.agent ?? "general");
46
- }
47
12
  function truncate(text, limit) {
48
- // Count code points, not UTF-16 code units: slicing a surrogate pair in
49
- // half would render as a lone replacement glyph in the terminal.
50
- const chars = [...text];
51
- return chars.length <= limit ? text : `${chars.slice(0, Math.max(0, limit - 1)).join("")}…`;
52
- }
53
- const DUNDER_IDENTIFIER_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/u;
54
- const CODE_OPERATOR_SUFFIX_PATTERN = /(?:={1,3}|!={1,2}|<=?|>=?|:=|[-+*/%&|^]=?)$/u;
55
- const CODE_OPERATOR_PREFIX_PATTERN = /^(?:={1,3}|!={1,2}|<=?|>=?|:=|[-+*/%&|^]=?)/u;
56
- const CODE_KEYWORD_SUFFIX_PATTERN = /\b(?:return|yield|raise|del|global|nonlocal|import|from|as)\s*$/u;
57
- // Double underscores are ambiguous: Markdown uses them for strong emphasis,
58
- // while code uses them for dunder identifiers. Preserve identifier-shaped
59
- // spans in clear code contexts and strip the remaining emphasis markers.
60
- function stripUnderscoreStrong(text) {
61
- return text.replace(/__([^_\n]+)__/gu, (match, content, offset) => {
62
- const before = text[offset - 1] ?? "";
63
- const after = text[offset + match.length] ?? "";
64
- const pairedCodeDelimiter = (before === "`" && after === "`") || (before === '"' && after === '"') || (before === "'" && after === "'");
65
- const codeAdjacent = before === "." || after === "(" || after === "." || after === "[";
66
- const trimmedBefore = text.slice(0, offset).trimEnd();
67
- const trimmedAfter = text.slice(offset + match.length).trimStart();
68
- const operatorAdjacent = CODE_OPERATOR_SUFFIX_PATTERN.test(trimmedBefore) || CODE_OPERATOR_PREFIX_PATTERN.test(trimmedAfter);
69
- const keywordAdjacent = CODE_KEYWORD_SUFFIX_PATTERN.test(trimmedBefore);
70
- return DUNDER_IDENTIFIER_PATTERN.test(content) &&
71
- (pairedCodeDelimiter || codeAdjacent || operatorAdjacent || keywordAdjacent)
72
- ? match
73
- : content;
74
- });
75
- }
76
- // Transcript excerpts render inside plain Text components, so markdown
77
- // punctuation would show up literally; strip the common inline syntax.
78
- function stripInlineMarkdown(text) {
79
- return (stripUnderscoreStrong(text.replace(/```[a-zA-Z0-9-]*/gu, ""))
80
- .replace(/`([^`]*)`/gu, "$1")
81
- // Demarcated bold only: `x**2**` is exponentiation, not emphasis.
82
- .replace(/(?<![A-Za-z0-9])\*\*([^*]+)\*\*(?![A-Za-z0-9])/gu, "$1")
83
- .replace(/\[([^\]]+)\]\([^)]*\)/gu, "$1")
84
- .replace(/^#{1,6}\s+/gmu, ""));
85
- }
86
- // Markdown tables and horizontal rules turn into symbol soup when
87
- // flattened onto one line; drop those lines before inline stripping.
88
- function stripBlockMarkdown(text) {
89
- return text
90
- .split("\n")
91
- .filter((line) => {
92
- const trimmed = line.trim();
93
- if (trimmed.startsWith("|"))
94
- return false;
95
- return !(trimmed.length >= 3 && /^[-—─═=*_|:\s]+$/u.test(trimmed));
96
- })
97
- .join("\n");
98
- }
99
- // finalOutput/liveText carry model-facing truncation notices from
100
- // boundText/tailText. Only recognize their exact boundary lines so literal
101
- // discussion of the marker remains visible in the transcript.
102
- const OUTPUT_TRUNCATION_NOTICE_PATTERN = /^\[Output truncated(?:: \d+ bytes omitted)?\.\]$/u;
103
- const EARLIER_OUTPUT_NOTICE = "[Earlier output omitted.]";
104
- function terminalOutputNotice(text) {
105
- const lines = text.split("\n");
106
- let index = lines.length - 1;
107
- while (index >= 0 && !lines[index]?.trim())
108
- index--;
109
- if (index < 0 || !OUTPUT_TRUNCATION_NOTICE_PATTERN.test(lines[index]?.trim() ?? "")) {
110
- return { text, truncated: false };
111
- }
112
- lines.splice(index, 1);
113
- return { text: lines.join("\n").trimEnd(), truncated: true };
114
- }
115
- function replaceBoundaryNotices(text) {
116
- const lines = text.split("\n");
117
- const first = lines.findIndex((line) => Boolean(line.trim()));
118
- if (first >= 0 && lines[first]?.trim() === EARLIER_OUTPUT_NOTICE)
119
- lines[first] = "…";
120
- let last = lines.length - 1;
121
- while (last >= 0 && !lines[last]?.trim())
122
- last--;
123
- if (last >= 0 && OUTPUT_TRUNCATION_NOTICE_PATTERN.test(lines[last]?.trim() ?? ""))
124
- lines[last] = "…";
125
- return lines.join("\n");
126
- }
127
- function plainExcerpt(text, limit) {
128
- return truncate(stripInlineMarkdown(stripBlockMarkdown(text)).replace(/\s+/gu, " ").trim(), limit);
129
- }
130
- function excerpt(text, limit) {
131
- return plainExcerpt(replaceBoundaryNotices(text), limit);
132
- }
133
- // Generated notice-only boundary lines must never become the live line the
134
- // user watches; identical text in the body remains ordinary model output.
135
- function liveTailLines(run) {
136
- const { text } = terminalOutputNotice(run.liveText);
137
- const lines = text
138
- .split("\n")
139
- .map((line) => line.trim())
140
- .filter(Boolean);
141
- if (lines[0] === EARLIER_OUTPUT_NOTICE)
142
- lines.shift();
143
- return lines;
144
- }
145
- function liveTail(run) {
146
- const last = liveTailLines(run).at(-1);
147
- return last ? excerpt(last, LIVE_TAIL_LIMIT) : undefined;
148
- }
149
- // Completed-run excerpts read better cut at a sentence boundary than
150
- // mid-word; fall back to a hard truncate when no boundary lands in the
151
- // second half of the window.
152
- function leadingSentences(text, limit) {
153
- const cleaned = excerpt(text, Number.MAX_SAFE_INTEGER);
154
- if (cleaned.length <= limit)
155
- return cleaned;
156
- const window = cleaned.slice(0, limit);
157
- let boundary = -1;
158
- for (const match of window.matchAll(/[.!?。!?](?=\s|$)/gu)) {
159
- boundary = (match.index ?? 0) + match[0].length;
160
- }
161
- if (boundary >= limit / 2)
162
- return window.slice(0, boundary).trimEnd();
163
- return truncate(cleaned, limit);
13
+ const characters = [...text];
14
+ return characters.length <= limit ? text : `${characters.slice(0, Math.max(0, limit - 1)).join("")}…`;
15
+ }
16
+ function taskPrompt(args, index, run) {
17
+ // Tool calls restored from before the tasks-array shape carry a legacy
18
+ // args object without `tasks`; those rows fall back to the description.
19
+ const tasks = args.tasks;
20
+ return tasks?.[index]?.prompt ?? run.description;
21
+ }
22
+ function taskSummary(args, index, run) {
23
+ const firstLine = firstPlainLine(taskPrompt(args, index, run));
24
+ return truncate(firstLine || plainLine(run.description) || `Task ${index + 1}`, TASK_SUMMARY_LIMIT);
25
+ }
26
+ function profileLabel(agent) {
27
+ if (agent === "explorer")
28
+ return "Explorer";
29
+ if (agent === "general")
30
+ return "General";
31
+ return agent;
164
32
  }
165
- const VERIFY_COMMAND_PATTERN = /\b(test|lint|typecheck|tsc|vitest|jest|pytest|build|check|fmt|format|compile)\b/iu;
166
- const INSPECT_COMMAND_PATTERN = /\b(rg|grep|find|git (?:diff|status|log|show)|ls|tree|cat|head|tail)\b/iu;
167
- const RETRY_MARKER_PATTERN = /^Retrying\b/u;
168
- const STREAM_MARKERS = new Set(["Thinking…", "Writing response…"]);
169
- function retryText(run) {
170
- const retry = getSubagentRetryView(run);
171
- if (!retry)
172
- return undefined;
173
- const status = retry.remainingSeconds > 0
174
- ? `Retrying (${retry.attempt}/${retry.maxAttempts}) in ${retry.remainingSeconds}s`
175
- : `Retrying now… (${retry.attempt}/${retry.maxAttempts})`;
176
- const error = excerpt(retry.error, RETRY_ERROR_EXCERPT_LIMIT);
177
- return error ? `${status} — ${error}` : status;
178
- }
179
- // Translates raw tool traffic into what the worker is doing right now, so
180
- // the collapsed card reads as a phase instead of a command line. Modelled
181
- // on tool-type classification; the raw tool summary stays as evidence on
182
- // the next line.
183
- function runIntent(run) {
184
- const retry = retryText(run);
185
- if (retry)
186
- return retry;
187
- const current = run.currentActivity;
188
- if (current && RETRY_MARKER_PATTERN.test(current))
189
- return current;
190
- const running = [...run.activities].reverse().find((activity) => activity.status === "running");
191
- if (running) {
192
- if (running.toolName === "edit" || running.toolName === "write")
193
- return "Applying changes";
194
- if (running.toolName === "bash") {
195
- if (VERIFY_COMMAND_PATTERN.test(running.summary))
196
- return "Verifying changes";
197
- return INSPECT_COMMAND_PATTERN.test(running.summary) ? "Exploring code" : "Running commands";
198
- }
199
- return "Exploring code";
200
- }
201
- const recent = run.activities.slice(-5);
202
- if (current && STREAM_MARKERS.has(current)) {
203
- if (recent.some((activity) => activity.status === "failed"))
204
- return "Investigating a failure";
205
- return current;
206
- }
207
- if (recent.some((activity) => activity.status === "failed"))
208
- return "Investigating a failure";
209
- if (recent.some((activity) => activity.toolName === "edit" || activity.toolName === "write"))
210
- return "Reviewing changes";
211
- if (recent.length > 0)
212
- return "Exploring code";
213
- return current ?? "Thinking…";
33
+ function formatTokens(value) {
34
+ if (value < 1_000)
35
+ return String(value);
36
+ if (value < 1_000_000)
37
+ return `${(value / 1_000).toFixed(value < 10_000 ? 1 : 0)}k`;
38
+ return `${(value / 1_000_000).toFixed(1)}M`;
214
39
  }
215
- // The raw tool summary backs up the intent line; suppress it when it would
216
- // just repeat a stream/retry marker.
217
- function runEvidence(run) {
218
- if (getSubagentRetryView(run))
219
- return undefined;
220
- const current = run.currentActivity;
221
- if (!current || STREAM_MARKERS.has(current) || RETRY_MARKER_PATTERN.test(current))
222
- return undefined;
223
- return current;
40
+ function formatDuration(seconds) {
41
+ const roundedSeconds = Math.round(seconds);
42
+ if (roundedSeconds >= 60)
43
+ return `${Math.floor(roundedSeconds / 60)}m ${roundedSeconds % 60}s`;
44
+ return `${seconds.toFixed(seconds < 10 ? 1 : 0)}s`;
224
45
  }
225
- function liveElapsed(run) {
226
- if (run.startedAt === undefined)
46
+ function elapsed(startedAt, endedAt) {
47
+ if (startedAt === undefined)
227
48
  return undefined;
228
- const end = run.endedAt ?? Date.now();
229
- return formatDuration(Math.max(0, (end - run.startedAt) / 1000));
49
+ return formatDuration(Math.max(0, ((endedAt ?? Date.now()) - startedAt) / 1_000));
230
50
  }
231
51
  function statusColor(status) {
232
52
  switch (status) {
@@ -242,9 +62,6 @@ function statusColor(status) {
242
62
  return "muted";
243
63
  }
244
64
  }
245
- // The tool shell already paints a call-level status dot (● in
246
- // warning/success/error), so `●` is off-limits here: `›` marks where the
247
- // work currently is, and the remaining glyphs are terminal states.
248
65
  function statusMarker(status, theme) {
249
66
  const glyph = status === "completed"
250
67
  ? "✓"
@@ -258,413 +75,219 @@ function statusMarker(status, theme) {
258
75
  return theme.fg(statusColor(status), glyph);
259
76
  }
260
77
  function statusWord(status) {
261
- switch (status) {
262
- case "completed":
263
- return "Completed";
264
- case "failed":
265
- return "Failed";
266
- case "aborted":
267
- return "Aborted";
268
- case "running":
269
- return "Running";
270
- default:
271
- return "Queued";
272
- }
78
+ if (status === "completed")
79
+ return "Completed";
80
+ if (status === "failed")
81
+ return "Failed";
82
+ if (status === "aborted")
83
+ return "Aborted";
84
+ if (status === "running")
85
+ return "Running";
86
+ return "Queued";
273
87
  }
274
- function runTitle(run, theme, index) {
275
- const number = index === undefined ? "" : theme.fg("dim", `${index} · `);
276
- return `${statusMarker(run.status, theme)} ${number}${theme.fg("accent", displayAgentName(run.agent))}${theme.fg("dim", ` · ${truncate(run.description, 48)}`)}`;
277
- }
278
- // Mid-run token totals are cache-inflated (totalTokens sums cache reads
279
- // across turns), so live lines quote tool uses and the context watermark
280
- // instead; settled lines get the real total.
281
- function runProgressText(run) {
282
- const items = [`${run.usage.toolUses} tool use${run.usage.toolUses === 1 ? "" : "s"}`];
283
- if (run.usage.contextTokens)
284
- items.push(`ctx: ${formatTokens(run.usage.contextTokens)}`);
285
- return items.join(" · ");
88
+ function retryText(run) {
89
+ const retry = getSubagentRetryView(run);
90
+ if (!retry)
91
+ return undefined;
92
+ const state = retry.remainingSeconds > 0
93
+ ? `Retrying (${retry.attempt}/${retry.maxAttempts}) in ${retry.remainingSeconds}s`
94
+ : `Retrying now… (${retry.attempt}/${retry.maxAttempts})`;
95
+ const error = truncate(plainLine(retry.error), RETRY_ERROR_LIMIT);
96
+ return error ? `${state} — ${error}` : state;
286
97
  }
287
- function runLine(run, theme, index) {
288
- let line = runTitle(run, theme, index);
98
+ function runningDetail(run) {
289
99
  const retry = retryText(run);
290
- const detail = retry ??
291
- (run.status === "running"
292
- ? excerpt(runIntent(run), LIVE_TAIL_LIMIT)
293
- : run.status === "failed" || run.status === "aborted"
294
- ? undefined
295
- : run.finalOutput && leadingSentences(run.finalOutput, RUN_LINE_EXCERPT_LIMIT));
296
- if (detail)
100
+ if (retry)
101
+ return retry;
102
+ if (run.currentActivity)
103
+ return truncate(plainLine(run.currentActivity), ACTIVITY_SUMMARY_LIMIT);
104
+ if (run.status === "queued")
105
+ return "Waiting for a worker slot";
106
+ if (run.status === "running" && run.usage.turns === 0 && run.usage.toolUses === 0)
107
+ return "Initializing…";
108
+ if (run.status === "running")
109
+ return "Thinking…";
110
+ if ((run.status === "failed" || run.status === "aborted") && run.error) {
111
+ return truncate(plainLine(run.error), ACTIVITY_RESULT_LIMIT);
112
+ }
113
+ return undefined;
114
+ }
115
+ function batchMarker(details, theme) {
116
+ if (details.runs.some((run) => run.status === "failed"))
117
+ return theme.fg("error", "×");
118
+ if (details.runs.some((run) => run.status === "aborted"))
119
+ return theme.fg("warning", "■");
120
+ return theme.fg("success", "✓");
121
+ }
122
+ function progressHeader(details, theme) {
123
+ const settled = details.runs.filter((run) => run.status !== "running" && run.status !== "queued").length;
124
+ return theme.fg("accent", `› ${settled}/${details.runs.length} done · ${elapsed(details.startedAt) ?? "0.0s"}`);
125
+ }
126
+ function taskRow(run, index, args, theme, live) {
127
+ let line = ` ${statusMarker(run.status, theme)} ${theme.fg("accent", profileLabel(run.agent))}${theme.fg("dim", ` · ${taskSummary(args, index, run)}`)}`;
128
+ const detail = live ? runningDetail(run) : undefined;
129
+ if (detail) {
297
130
  line += theme.fg("dim", ` — ${detail}`);
298
- if (run.status === "running") {
299
- line += theme.fg("dim", ` · ${runProgressText(run)}`);
300
- if (run.usage.cost)
301
- line += theme.fg("dim", ` · $${run.usage.cost.toFixed(3)}`);
302
- const elapsed = liveElapsed(run);
303
- if (elapsed)
304
- line += theme.fg("dim", ` · ${elapsed}`);
305
131
  }
306
- const settled = run.status !== "running" && run.status !== "queued";
307
- if (settled && run.startedAt !== undefined && run.endedAt !== undefined) {
308
- line += theme.fg("dim", ` · ${formatDuration(Math.max(0, (run.endedAt - run.startedAt) / 1000))}`);
132
+ else if (run.status !== "running" && run.status !== "queued") {
133
+ const duration = elapsed(run.startedAt, run.endedAt);
134
+ if (duration)
135
+ line += theme.fg("dim", ` · ${duration}`);
309
136
  }
310
137
  return line;
311
138
  }
312
- function runErrorExcerpt(run) {
313
- if ((run.status !== "failed" && run.status !== "aborted") || !run.error)
314
- return undefined;
315
- return leadingSentences(run.error, RUN_ERROR_EXCERPT_LIMIT);
316
- }
317
- function runErrorIndent(index) {
318
- return index === undefined ? 2 : 5 + String(index).length;
319
- }
320
- function addRunSummary(container, run, theme, index) {
321
- container.addChild(new Text(runLine(run, theme, index), 0, 0));
322
- const error = runErrorExcerpt(run);
323
- if (error)
324
- container.addChild(new IndentedText(theme.fg("error", error), runErrorIndent(index)));
325
- }
326
- // Keep active work visible while a batch is in flight: completed runs
327
- // may otherwise crowd a still-running task out of the collapsed window.
328
- function selectCollapsedRuns(runs, isPartial) {
329
- if (runs.length <= COLLAPSED_RUN_LIMIT)
330
- return runs;
331
- if (!isPartial)
332
- return runs.slice(0, COLLAPSED_RUN_LIMIT);
333
- const active = runs.filter((run) => run.status === "running" || run.status === "queued");
334
- const retrying = active.filter((run) => getSubagentRetryView(run));
335
- const retryingSet = new Set(retrying);
336
- const otherActive = active.filter((run) => !retryingSet.has(run));
337
- const settled = runs.filter((run) => run.status !== "running" && run.status !== "queued");
338
- return [...retrying, ...otherActive, ...settled].slice(0, COLLAPSED_RUN_LIMIT);
339
- }
340
- function singleCollapsedLines(details, theme) {
341
- const run = details.runs[0];
342
- if (!run)
343
- return [theme.fg("muted", "Initializing…")];
344
- if (run.status === "running" || run.status === "queued") {
345
- const retry = retryText(run);
346
- const isInitializing = run.status === "running" &&
347
- !retry &&
348
- run.usage.turns === 0 &&
349
- run.usage.toolUses === 0 &&
350
- run.liveText.length === 0;
351
- const rawIntent = retry ??
352
- (run.status === "queued"
353
- ? (run.currentActivity ?? statusWord(run.status))
354
- : isInitializing
355
- ? "Initializing…"
356
- : runIntent(run));
357
- const intent = excerpt(rawIntent, LIVE_TAIL_LIMIT);
358
- // No status marker here: the shell's call-level dot already covers
359
- // the whole card, and a single run has nothing to disambiguate.
360
- let status = theme.fg("dim", intent);
361
- if (run.status === "running" && !isInitializing) {
362
- status += theme.fg("dim", ` · ${runProgressText(run)}`);
363
- if (run.usage.cost)
364
- status += theme.fg("dim", ` · $${run.usage.cost.toFixed(3)}`);
365
- const elapsed = liveElapsed(run);
366
- if (elapsed)
367
- status += theme.fg("dim", ` · ${elapsed}`);
368
- }
369
- const lines = [status];
370
- const evidence = runEvidence(run);
371
- if (evidence && evidence !== rawIntent)
372
- lines.push(`${theme.fg("accent", "›")} ${theme.fg("dim", excerpt(evidence, LIVE_TAIL_LIMIT))}`);
373
- const tail = liveTail(run);
374
- if (tail)
375
- lines.push(theme.fg("dim", tail));
376
- return lines;
377
- }
378
- if (run.error)
379
- return [theme.fg("error", excerpt(run.error, SINGLE_EXCERPT_LIMIT))];
380
- if (run.finalOutput)
381
- return [theme.fg("toolOutput", leadingSentences(run.finalOutput, SINGLE_EXCERPT_LIMIT))];
382
- return [theme.fg("muted", statusWord(run.status))];
383
- }
384
- // Deep-trimmed settled footer: turns and per-run breakdowns belong to the
385
- // expanded view. Parallel batches keep only whole-call numbers — the run
386
- // rows above already localize the work.
387
- function collapsedUsageText(details, extended) {
388
- const usage = details.usage;
389
- const items = [];
390
- if (usage.totalTokens)
391
- items.push(`${formatTokens(usage.totalTokens)} tok`);
392
- if (extended && usage.toolUses)
393
- items.push(`${usage.toolUses} tool use${usage.toolUses === 1 ? "" : "s"}`);
394
- if (extended && usage.contextTokens)
395
- items.push(`ctx: ${formatTokens(usage.contextTokens)}`);
396
- if (usage.cost)
397
- items.push(`$${usage.cost.toFixed(3)}`);
398
- if (details.endedAt !== undefined) {
399
- items.push(formatDuration(Math.max(0, (details.endedAt - details.startedAt) / 1000)));
400
- }
401
- return items.join(" · ");
402
- }
403
- function batchUsageText(details, isPartial) {
404
- const usage = details.usage;
405
- const items = [];
406
- // The aggregate mixes settled and running runs while partial, and the
407
- // running share is cache-inflated — quote the total only once settled.
408
- if (!isPartial && usage.totalTokens)
409
- items.push(`${formatTokens(usage.totalTokens)} tok`);
410
- if (usage.toolUses)
411
- items.push(`${usage.toolUses} tool use${usage.toolUses === 1 ? "" : "s"}`);
412
- if (usage.turns)
413
- items.push(`${usage.turns} turn${usage.turns === 1 ? "" : "s"}`);
414
- if (usage.contextTokens)
415
- items.push(`ctx: ${formatTokens(usage.contextTokens)}`);
416
- if (usage.cost)
417
- items.push(`$${usage.cost.toFixed(3)}`);
418
- if (details.endedAt !== undefined) {
419
- items.push(formatDuration(Math.max(0, (details.endedAt - details.startedAt) / 1000)));
420
- }
421
- return items.join(" · ");
422
- }
423
- function formatTokens(value) {
424
- if (value < 1_000)
425
- return String(value);
426
- if (value < 1_000_000)
427
- return `${(value / 1_000).toFixed(value < 10_000 ? 1 : 0)}k`;
428
- return `${(value / 1_000_000).toFixed(1)}M`;
429
- }
430
- function formatDuration(seconds) {
431
- const roundedSeconds = Math.round(seconds);
432
- if (roundedSeconds >= 60)
433
- return `${Math.floor(roundedSeconds / 60)}m ${roundedSeconds % 60}s`;
434
- return `${seconds.toFixed(seconds < 10 ? 1 : 0)}s`;
435
- }
436
- function runDurationText(run) {
437
- if (run.startedAt === undefined || run.endedAt === undefined)
438
- return undefined;
439
- return formatDuration(Math.max(0, (run.endedAt - run.startedAt) / 1000));
440
- }
441
- // One dim line per run: what it cost. Settled runs quote the real token
442
- // total; running runs skip it (cache-inflated mid-run) and tick elapsed
443
- // time instead. Batch sections fold model/thinking/cwd in here because
444
- // their `──` header already carries verdict and duration.
445
- function runMetricsText(run, includeIdentity) {
446
- const usage = run.usage;
139
+ function outcomeSummary(details) {
140
+ const completed = details.runs.filter((run) => run.status === "completed").length;
141
+ const failed = details.runs.filter((run) => run.status === "failed").length;
142
+ const aborted = details.runs.filter((run) => run.status === "aborted").length;
143
+ const parts = [];
144
+ if (completed)
145
+ parts.push(`${completed} completed`);
146
+ if (failed)
147
+ parts.push(`${failed} failed`);
148
+ if (aborted)
149
+ parts.push(`${aborted} aborted`);
150
+ if (parts.length === 0)
151
+ parts.push(statusSummary(details));
152
+ const duration = elapsed(details.startedAt, details.endedAt);
153
+ if (duration)
154
+ parts.push(duration);
155
+ if (details.usage.cost)
156
+ parts.push(`$${details.usage.cost.toFixed(3)}`);
157
+ return parts.join(" · ");
158
+ }
159
+ function runMetrics(run) {
160
+ const items = [run.model];
161
+ if (run.thinking !== "off")
162
+ items.push(`${run.thinking} thinking`);
163
+ if (run.cwd)
164
+ items.push(`cwd: ${run.cwd}`);
447
165
  const settled = run.status !== "running" && run.status !== "queued";
448
- const items = [];
449
- if (includeIdentity) {
450
- items.push(run.model);
451
- if (run.thinking !== "off")
452
- items.push(run.thinking);
453
- if (run.cwd)
454
- items.push(`cwd: ${run.cwd}`);
455
- }
456
- if (settled && usage.totalTokens)
457
- items.push(`${formatTokens(usage.totalTokens)} tok`);
458
- if (usage.toolUses)
459
- items.push(`${usage.toolUses} tool use${usage.toolUses === 1 ? "" : "s"}`);
460
- if (usage.turns)
461
- items.push(`${usage.turns} turn${usage.turns === 1 ? "" : "s"}`);
462
- if (usage.contextTokens)
463
- items.push(`ctx: ${formatTokens(usage.contextTokens)}`);
464
- if (usage.cost)
465
- items.push(`$${usage.cost.toFixed(3)}`);
466
- const duration = settled ? (includeIdentity ? undefined : runDurationText(run)) : liveElapsed(run);
166
+ if (settled && run.usage.totalTokens)
167
+ items.push(`${formatTokens(run.usage.totalTokens)} tok`);
168
+ if (run.usage.toolUses)
169
+ items.push(`${run.usage.toolUses} tool use${run.usage.toolUses === 1 ? "" : "s"}`);
170
+ if (run.usage.turns)
171
+ items.push(`${run.usage.turns} turn${run.usage.turns === 1 ? "" : "s"}`);
172
+ if (run.usage.contextTokens)
173
+ items.push(`ctx: ${formatTokens(run.usage.contextTokens)}`);
174
+ if (run.usage.cost)
175
+ items.push(`$${run.usage.cost.toFixed(3)}`);
176
+ const duration = elapsed(run.startedAt, run.endedAt);
467
177
  if (duration)
468
178
  items.push(duration);
469
179
  return items.join(" · ");
470
180
  }
471
- // The details prompt is a 1KB bounded copy of the briefing; the preview
472
- // shows the first content lines (blank lines carry nothing at two lines
473
- // of budget) and never pretends to more fidelity than that.
474
- function promptSection(run, theme) {
475
- const boundedPrompt = terminalOutputNotice(run.prompt);
476
- const lines = boundedPrompt.text
477
- .split("\n")
478
- .map((line) => plainExcerpt(line.trim(), Number.MAX_SAFE_INTEGER))
479
- .filter(Boolean);
480
- const shown = lines.slice(0, PROMPT_PREVIEW_LINES);
481
- const preview = shown.map((line) => theme.fg("dim", truncate(line, PROMPT_PREVIEW_LINE_LIMIT)));
482
- // A one-line briefing clipped mid-sentence must still say it continues.
483
- const clipped = shown.some((line) => line.length > PROMPT_PREVIEW_LINE_LIMIT);
484
- const remaining = lines.length - shown.length;
485
- const capped = boundedPrompt.truncated;
486
- const section = [theme.fg("muted", "Prompt"), ...preview];
487
- if (remaining > 0 || capped || clipped) {
488
- let note = "… continues";
489
- if (remaining > 0)
490
- note += `, ${remaining} more line${remaining === 1 ? "" : "s"}`;
491
- if (capped)
492
- note += " · capped at 1KB";
493
- section.push(theme.fg("muted", note));
181
+ function activityLine(activity, run, theme) {
182
+ const summary = activity.status === "running" && run.currentActivity
183
+ ? truncate(plainLine(run.currentActivity), ACTIVITY_SUMMARY_LIMIT)
184
+ : truncate(plainLine(activity.summary), ACTIVITY_SUMMARY_LIMIT);
185
+ let line = activity.status === "failed"
186
+ ? `${theme.fg("error", "×")} ${theme.fg("toolOutput", summary)}`
187
+ : activity.status === "running"
188
+ ? `${theme.fg("accent", "›")} ${theme.fg("toolOutput", summary)}`
189
+ : ` ${theme.fg("toolOutput", summary)}`;
190
+ if (activity.status === "failed" && activity.resultSummary) {
191
+ line += theme.fg("error", ` · ${truncate(plainLine(activity.resultSummary), ACTIVITY_RESULT_LIMIT)}`);
192
+ }
193
+ if (activity.endedAt !== undefined && activity.endedAt - activity.startedAt >= ACTIVITY_DURATION_MIN_MS) {
194
+ line += theme.fg("dim", ` · ${formatDuration((activity.endedAt - activity.startedAt) / 1_000)}`);
494
195
  }
495
- return section;
196
+ return line;
197
+ }
198
+ function runHeader(run, index, args, theme) {
199
+ return `${theme.fg("dim", `── #${index + 1} `)}${statusMarker(run.status, theme)} ${theme.fg("accent", profileLabel(run.agent))}${theme.fg("dim", ` · ${taskSummary(args, index, run)}`)}`;
496
200
  }
497
- // Each run expands into a report cover sheet: verdict and cost up top, a
498
- // bounded Prompt/Activity digest in the middle, then the full Report —
499
- // the one unbounded element, placed last so the card ends where reading
500
- // begins. Single runs skip the `──` header (the call header already names
501
- // them); batch sections carry a numbered header matching the contents
502
- // list above.
503
- function renderRunDetails(run, theme, single, index) {
201
+ function renderRunningRun(run, index, args, theme) {
504
202
  const container = new Container();
505
- const settled = run.status !== "running" && run.status !== "queued";
506
- if (single) {
507
- const identity = [run.model];
508
- if (run.thinking !== "off")
509
- identity.push(`${run.thinking} thinking`);
510
- if (run.cwd)
511
- identity.push(`cwd: ${run.cwd}`);
512
- container.addChild(new Text(`${statusMarker(run.status, theme)} ${theme.fg(statusColor(run.status), statusWord(run.status))}${theme.fg("dim", ` · ${identity.join(" · ")}`)}`, 0, 0));
513
- }
514
- else {
515
- const label = index === undefined ? "" : `${index} · `;
516
- const duration = settled ? runDurationText(run) : undefined;
517
- container.addChild(new Text(`${theme.fg("dim", `── ${label}`)}${statusMarker(run.status, theme)} ${theme.fg("accent", displayAgentName(run.agent))}${theme.fg("dim", ` · ${truncate(run.description, 48)}`)}${duration ? theme.fg("dim", ` · ${duration}`) : ""}`, 0, 0));
518
- }
519
- if (run.status !== "queued") {
520
- const metrics = runMetricsText(run, !single);
521
- if (metrics)
522
- container.addChild(new Text(theme.fg("dim", metrics), 0, 0));
523
- }
203
+ container.addChild(new Text(runHeader(run, index, args, theme), 0, 0));
204
+ container.addChild(new Text(theme.fg("dim", runMetrics(run)), 0, 0));
524
205
  const retry = retryText(run);
525
206
  if (retry)
526
207
  container.addChild(new Text(theme.fg("accent", retry), 0, 0));
527
208
  container.addChild(new Spacer(1));
528
- for (const line of promptSection(run, theme))
529
- container.addChild(new Text(line, 0, 0));
209
+ const total = Math.max(run.usage.toolUses, run.activities.length);
210
+ const label = total > run.activities.length ? `Activity · last ${run.activities.length} of ${total}` : "Activity";
211
+ container.addChild(new Text(theme.fg("muted", label), 0, 0));
530
212
  if (run.activities.length > 0) {
531
- container.addChild(new Spacer(1));
532
- const total = run.usage.toolUses;
533
- const label = total > run.activities.length ? `Activity · last ${run.activities.length} of ${total}` : "Activity";
534
- container.addChild(new Text(theme.fg("muted", label), 0, 0));
535
- for (const activity of run.activities) {
536
- // Deviation earns ink: × and › are marked, success rows are quiet
537
- // one-liners without result echoes.
538
- let text;
539
- if (activity.status === "failed") {
540
- text = `${theme.fg("error", "×")} ${theme.fg("toolOutput", excerpt(activity.summary, ACTIVITY_SUMMARY_LIMIT))}`;
541
- if (activity.resultSummary) {
542
- text += ` ${theme.fg("error", `· ${excerpt(activity.resultSummary, ACTIVITY_RESULT_LIMIT)}`)}`;
543
- }
544
- }
545
- else if (activity.status === "running") {
546
- text = `${theme.fg("accent", "›")} ${theme.fg("toolOutput", excerpt(activity.summary, ACTIVITY_SUMMARY_LIMIT))}`;
547
- }
548
- else {
549
- text = ` ${theme.fg("toolOutput", excerpt(activity.summary, ACTIVITY_SUMMARY_LIMIT))}`;
550
- }
551
- if (activity.endedAt !== undefined && activity.endedAt - activity.startedAt >= ACTIVITY_DURATION_MIN_MS) {
552
- text += ` ${theme.fg("dim", `· ${formatDuration((activity.endedAt - activity.startedAt) / 1000)}`)}`;
553
- }
554
- container.addChild(new Text(text, 0, 0));
555
- }
213
+ for (const activity of run.activities)
214
+ container.addChild(new Text(activityLine(activity, run, theme), 0, 0));
556
215
  }
557
- // The live tail is the "now" end of the timeline; once the run settles,
558
- // Report takes this slot without moving anything above it.
559
- if (run.status === "running" && run.liveText) {
560
- const tail = liveTailLines(run).slice(-EXPANDED_LIVE_TAIL_LINES);
561
- if (tail.length > 0) {
562
- container.addChild(new Spacer(1));
563
- container.addChild(new Text(theme.fg("muted", "Working"), 0, 0));
564
- for (const line of tail) {
565
- container.addChild(new Text(theme.fg("dim", excerpt(line, LIVE_TAIL_LIMIT)), 0, 0));
566
- }
567
- }
216
+ else {
217
+ const fallback = retry
218
+ ? "Waiting to retry"
219
+ : run.status === "failed" || run.status === "aborted"
220
+ ? "No activity recorded"
221
+ : (runningDetail(run) ?? statusWord(run.status));
222
+ container.addChild(new Text(theme.fg("dim", ` ${fallback}`), 0, 0));
568
223
  }
569
224
  if (run.error) {
570
225
  container.addChild(new Spacer(1));
571
226
  container.addChild(new Text(theme.fg("muted", "Error"), 0, 0));
572
227
  container.addChild(new Text(theme.fg("error", run.error), 0, 0));
573
228
  }
574
- if (run.finalOutput) {
229
+ return container;
230
+ }
231
+ function renderSettledRun(run, index, args, theme) {
232
+ const container = new Container();
233
+ container.addChild(new Text(runHeader(run, index, args, theme), 0, 0));
234
+ container.addChild(new Text(theme.fg("dim", runMetrics(run)), 0, 0));
235
+ container.addChild(new Spacer(1));
236
+ container.addChild(new Text(theme.fg("muted", "Prompt"), 0, 0));
237
+ container.addChild(new Text(theme.fg("toolOutput", taskPrompt(args, index, run)), 0, 0));
238
+ if (run.error) {
575
239
  container.addChild(new Spacer(1));
576
- const label = settled && run.status !== "completed" ? "Report · partial" : "Report";
577
- container.addChild(new Text(theme.fg("muted", label), 0, 0));
578
- container.addChild(new Markdown(run.finalOutput, 0, 0, getMarkdownTheme(), {
240
+ container.addChild(new Text(theme.fg("muted", "Error"), 0, 0));
241
+ container.addChild(new Text(theme.fg("error", run.error), 0, 0));
242
+ }
243
+ container.addChild(new Spacer(1));
244
+ const reportLabel = run.status !== "completed" && run.report ? "Report · partial" : "Report";
245
+ container.addChild(new Text(theme.fg("muted", reportLabel), 0, 0));
246
+ if (run.report) {
247
+ container.addChild(new Markdown(run.report, 0, 0, getMarkdownTheme(), {
579
248
  color: (text) => theme.fg("toolOutput", text),
580
249
  }));
581
250
  }
251
+ else {
252
+ container.addChild(new Text(theme.fg("muted", "(No report.)"), 0, 0));
253
+ }
582
254
  return container;
583
255
  }
584
- // The call header stays a single line: run rows in the result area take
585
- // over within the first update and carry richer per-task state, so a
586
- // task list here would render everything twice. The task count alone
587
- // distinguishes a batch from a single delegation.
256
+ function fallbackResult(result, theme, isError) {
257
+ const text = result.content.find((part) => part.type === "text");
258
+ const value = text?.type === "text" ? truncate(text.text, FALLBACK_OUTPUT_LIMIT) : "(no output)";
259
+ return new Text(theme.fg(isError ? "error" : "muted", value), 0, 0);
260
+ }
588
261
  export function renderSubagentCall(args, theme) {
589
- const modes = [args.prompt != null && "prompt", args.tasks != null && "tasks"].filter((mode) => Boolean(mode));
590
- if (modes.length > 1) {
591
- return new Text(`${theme.fg("toolTitle", theme.bold("Subagent "))}${theme.fg("error", `invalid · ${modes.join(" + ")}`)}`, 0, 0);
592
- }
593
- if (args.tasks) {
594
- return new Text(`${theme.fg("toolTitle", theme.bold("Multi-Agent"))}${theme.fg("dim", ` · ${args.tasks.length} task${args.tasks.length === 1 ? "" : "s"}`)}`, 0, 0);
595
- }
596
- let text = theme.fg("toolTitle", theme.bold(`${singleAgentName(args)} Agent`));
597
- if (args.description)
598
- text += theme.fg("dim", ` · ${truncate(args.description, 72)}`);
599
- return new Text(text, 0, 0);
262
+ const count = Array.isArray(args.tasks) ? args.tasks.length : 0;
263
+ const label = `${count} task${count === 1 ? "" : "s"}`;
264
+ return new Text(`${theme.fg("toolTitle", theme.bold("Subagent"))}${theme.fg(count > 0 ? "dim" : "error", ` · ${label}`)}`, 0, 0);
600
265
  }
601
- export function renderSubagentResult(result, options, theme, isError) {
266
+ export function renderSubagentResult(result, options, theme, args, isError) {
602
267
  const details = result.details;
603
- if (!details) {
604
- const text = result.content.find((part) => part.type === "text");
605
- const label = text?.type === "text" ? text.text : "(no output)";
606
- return new Text(theme.fg(isError ? "error" : "muted", label), 0, 0);
607
- }
608
- // The pre-resolution first paint carries no runs yet, in either mode.
609
- if (details.runs.length === 0) {
268
+ if (!details || !Array.isArray(details.runs))
269
+ return fallbackResult(result, theme, isError);
270
+ if (details.runs.length === 0)
610
271
  return new Text(theme.fg("muted", "Initializing…"), 0, 0);
611
- }
612
272
  if (!options.expanded) {
613
- if (details.mode === "single") {
614
- const lines = singleCollapsedLines(details, theme);
615
- if (!options.isPartial) {
616
- const usage = collapsedUsageText(details, true);
617
- if (usage)
618
- lines.push(theme.fg("dim", usage));
619
- lines.push(theme.fg("muted", `(${keyHint("app.tools.expand", "to expand")})`));
620
- }
621
- return new Text(lines.join("\n"), 0, 0);
622
- }
623
273
  const container = new Container();
624
- let header = statusSummary(details);
625
- if (options.isPartial) {
626
- header += ` · ${formatDuration(Math.max(0, (Date.now() - details.startedAt) / 1000))}`;
627
- }
628
- container.addChild(new Text(theme.fg(isError ? "error" : options.isPartial ? "accent" : "muted", header), 0, 0));
629
- const shown = selectCollapsedRuns(details.runs, Boolean(options.isPartial));
630
- // Stable ordinals (the run's task position, not its display slot)
631
- // keep identity readable while active-first ordering reshuffles
632
- // rows, and match the expanded section numbers.
633
- for (const run of shown)
634
- addRunSummary(container, run, theme, details.runs.indexOf(run) + 1);
635
- const hidden = details.runs.length - shown.length;
636
- if (hidden > 0)
637
- container.addChild(new Text(theme.fg("muted", `+${hidden} more`), 0, 0));
638
- if (!options.isPartial) {
639
- const usage = collapsedUsageText(details, false);
640
- if (usage)
641
- container.addChild(new Text(theme.fg("dim", usage), 0, 0));
642
- container.addChild(new Text(theme.fg("muted", `(${keyHint("app.tools.expand", "to expand")})`), 0, 0));
274
+ const header = options.isPartial
275
+ ? progressHeader(details, theme)
276
+ : `${batchMarker(details, theme)} ${theme.fg(isError ? "error" : "muted", outcomeSummary(details))}`;
277
+ container.addChild(new Text(header, 0, 0));
278
+ for (const [index, run] of details.runs.entries()) {
279
+ container.addChild(new Text(taskRow(run, index, args, theme, options.isPartial), 0, 0));
643
280
  }
644
281
  return container;
645
282
  }
646
283
  const container = new Container();
647
- const single = details.mode === "single" && details.runs.length === 1;
648
- if (!single) {
649
- container.addChild(new Text(theme.fg(isError ? "error" : "toolTitle", statusSummary(details)), 0, 0));
650
- // Numbered table of contents: the TUI cannot jump, but matching the
651
- // numbers against the `──` section headers below makes an 8-run
652
- // batch navigable by eye.
653
- details.runs.forEach((run, index) => {
654
- addRunSummary(container, run, theme, index + 1);
655
- });
656
- }
657
- details.runs.forEach((run, index) => {
658
- if (!single || index > 0)
659
- container.addChild(new Spacer(1));
660
- container.addChild(renderRunDetails(run, theme, single, single ? undefined : index + 1));
661
- });
662
- if (!single) {
663
- const usage = batchUsageText(details, Boolean(options.isPartial));
664
- if (usage) {
665
- container.addChild(new Spacer(1));
666
- container.addChild(new Text(theme.fg("dim", usage), 0, 0));
667
- }
284
+ const header = options.isPartial
285
+ ? progressHeader(details, theme)
286
+ : `${batchMarker(details, theme)} ${theme.fg(isError ? "error" : "toolTitle", outcomeSummary(details))}`;
287
+ container.addChild(new Text(header, 0, 0));
288
+ for (const [index, run] of details.runs.entries()) {
289
+ container.addChild(new Spacer(1));
290
+ container.addChild(options.isPartial ? renderRunningRun(run, index, args, theme) : renderSettledRun(run, index, args, theme));
668
291
  }
669
292
  return container;
670
293
  }