@astralyn/pi 0.82.5 → 0.82.7

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 (223) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +1 -1
  3. package/dist/cli/project-trust.d.ts.map +1 -1
  4. package/dist/cli/project-trust.js +4 -1
  5. package/dist/cli/project-trust.js.map +1 -1
  6. package/dist/cli/session-picker.d.ts.map +1 -1
  7. package/dist/cli/session-picker.js +11 -13
  8. package/dist/cli/session-picker.js.map +1 -1
  9. package/dist/core/bash-executor.d.ts.map +1 -1
  10. package/dist/core/bash-executor.js +4 -22
  11. package/dist/core/bash-executor.js.map +1 -1
  12. package/dist/core/extensions/index.d.ts +1 -1
  13. package/dist/core/extensions/index.d.ts.map +1 -1
  14. package/dist/core/extensions/index.js.map +1 -1
  15. package/dist/core/extensions/types.d.ts +32 -5
  16. package/dist/core/extensions/types.d.ts.map +1 -1
  17. package/dist/core/extensions/types.js.map +1 -1
  18. package/dist/core/session-manager.d.ts.map +1 -1
  19. package/dist/core/session-manager.js +2 -2
  20. package/dist/core/session-manager.js.map +1 -1
  21. package/dist/core/tools/bash.d.ts +1 -5
  22. package/dist/core/tools/bash.d.ts.map +1 -1
  23. package/dist/core/tools/bash.js +16 -41
  24. package/dist/core/tools/bash.js.map +1 -1
  25. package/dist/core/tools/edit.d.ts.map +1 -1
  26. package/dist/core/tools/edit.js +42 -11
  27. package/dist/core/tools/edit.js.map +1 -1
  28. package/dist/core/tools/find.d.ts.map +1 -1
  29. package/dist/core/tools/find.js +6 -7
  30. package/dist/core/tools/find.js.map +1 -1
  31. package/dist/core/tools/grep.d.ts.map +1 -1
  32. package/dist/core/tools/grep.js +17 -8
  33. package/dist/core/tools/grep.js.map +1 -1
  34. package/dist/core/tools/ls.d.ts.map +1 -1
  35. package/dist/core/tools/ls.js +3 -3
  36. package/dist/core/tools/ls.js.map +1 -1
  37. package/dist/core/tools/output-accumulator.d.ts +3 -13
  38. package/dist/core/tools/output-accumulator.d.ts.map +1 -1
  39. package/dist/core/tools/output-accumulator.js +6 -36
  40. package/dist/core/tools/output-accumulator.js.map +1 -1
  41. package/dist/core/tools/read.d.ts.map +1 -1
  42. package/dist/core/tools/read.js +4 -4
  43. package/dist/core/tools/read.js.map +1 -1
  44. package/dist/core/tools/render-utils.d.ts +7 -0
  45. package/dist/core/tools/render-utils.d.ts.map +1 -1
  46. package/dist/core/tools/render-utils.js +11 -0
  47. package/dist/core/tools/render-utils.js.map +1 -1
  48. package/dist/core/tools/write.d.ts.map +1 -1
  49. package/dist/core/tools/write.js +2 -3
  50. package/dist/core/tools/write.js.map +1 -1
  51. package/dist/extensions/deepwiki/render.d.ts.map +1 -1
  52. package/dist/extensions/deepwiki/render.js +34 -22
  53. package/dist/extensions/deepwiki/render.js.map +1 -1
  54. package/dist/extensions/plan/constants.d.ts +43 -6
  55. package/dist/extensions/plan/constants.d.ts.map +1 -1
  56. package/dist/extensions/plan/constants.js +51 -8
  57. package/dist/extensions/plan/constants.js.map +1 -1
  58. package/dist/extensions/plan/index.d.ts +12 -3
  59. package/dist/extensions/plan/index.d.ts.map +1 -1
  60. package/dist/extensions/plan/index.js +173 -35
  61. package/dist/extensions/plan/index.js.map +1 -1
  62. package/dist/extensions/plan/schema.d.ts +5 -0
  63. package/dist/extensions/plan/schema.d.ts.map +1 -1
  64. package/dist/extensions/plan/schema.js.map +1 -1
  65. package/dist/extensions/plan/state.d.ts +11 -2
  66. package/dist/extensions/plan/state.d.ts.map +1 -1
  67. package/dist/extensions/plan/state.js +31 -9
  68. package/dist/extensions/plan/state.js.map +1 -1
  69. package/dist/extensions/plan/storage.d.ts +11 -3
  70. package/dist/extensions/plan/storage.d.ts.map +1 -1
  71. package/dist/extensions/plan/storage.js +45 -20
  72. package/dist/extensions/plan/storage.js.map +1 -1
  73. package/dist/extensions/plan/view.d.ts +44 -0
  74. package/dist/extensions/plan/view.d.ts.map +1 -0
  75. package/dist/extensions/plan/view.js +149 -0
  76. package/dist/extensions/plan/view.js.map +1 -0
  77. package/dist/extensions/question/dialog.d.ts.map +1 -1
  78. package/dist/extensions/question/dialog.js +29 -10
  79. package/dist/extensions/question/dialog.js.map +1 -1
  80. package/dist/extensions/question/index.d.ts.map +1 -1
  81. package/dist/extensions/question/index.js +7 -2
  82. package/dist/extensions/question/index.js.map +1 -1
  83. package/dist/extensions/question/render.d.ts +4 -8
  84. package/dist/extensions/question/render.d.ts.map +1 -1
  85. package/dist/extensions/question/render.js +202 -29
  86. package/dist/extensions/question/render.js.map +1 -1
  87. package/dist/extensions/question/results.d.ts.map +1 -1
  88. package/dist/extensions/question/results.js +1 -0
  89. package/dist/extensions/question/results.js.map +1 -1
  90. package/dist/extensions/question/schema.d.ts.map +1 -1
  91. package/dist/extensions/question/schema.js +2 -2
  92. package/dist/extensions/question/schema.js.map +1 -1
  93. package/dist/extensions/question/types.d.ts +1 -0
  94. package/dist/extensions/question/types.d.ts.map +1 -1
  95. package/dist/extensions/question/types.js +1 -1
  96. package/dist/extensions/question/types.js.map +1 -1
  97. package/dist/extensions/router/stream.d.ts +1 -0
  98. package/dist/extensions/router/stream.d.ts.map +1 -1
  99. package/dist/extensions/router/stream.js +75 -0
  100. package/dist/extensions/router/stream.js.map +1 -1
  101. package/dist/extensions/subagent/activity.d.ts.map +1 -1
  102. package/dist/extensions/subagent/activity.js +25 -4
  103. package/dist/extensions/subagent/activity.js.map +1 -1
  104. package/dist/extensions/subagent/constants.d.ts +1 -0
  105. package/dist/extensions/subagent/constants.d.ts.map +1 -1
  106. package/dist/extensions/subagent/constants.js +1 -0
  107. package/dist/extensions/subagent/constants.js.map +1 -1
  108. package/dist/extensions/subagent/index.d.ts.map +1 -1
  109. package/dist/extensions/subagent/index.js +16 -0
  110. package/dist/extensions/subagent/index.js.map +1 -1
  111. package/dist/extensions/subagent/render.d.ts.map +1 -1
  112. package/dist/extensions/subagent/render.js +352 -152
  113. package/dist/extensions/subagent/render.js.map +1 -1
  114. package/dist/extensions/subagent/resolve.d.ts.map +1 -1
  115. package/dist/extensions/subagent/resolve.js +13 -9
  116. package/dist/extensions/subagent/resolve.js.map +1 -1
  117. package/dist/extensions/subagent/retry.d.ts +17 -0
  118. package/dist/extensions/subagent/retry.d.ts.map +1 -0
  119. package/dist/extensions/subagent/retry.js +41 -0
  120. package/dist/extensions/subagent/retry.js.map +1 -0
  121. package/dist/extensions/subagent/runner.d.ts.map +1 -1
  122. package/dist/extensions/subagent/runner.js +37 -7
  123. package/dist/extensions/subagent/runner.js.map +1 -1
  124. package/dist/extensions/subagent/schema.d.ts.map +1 -1
  125. package/dist/extensions/subagent/schema.js +2 -2
  126. package/dist/extensions/subagent/schema.js.map +1 -1
  127. package/dist/extensions/subagent/sdk-runner.d.ts +6 -0
  128. package/dist/extensions/subagent/sdk-runner.d.ts.map +1 -1
  129. package/dist/extensions/subagent/sdk-runner.js +46 -24
  130. package/dist/extensions/subagent/sdk-runner.js.map +1 -1
  131. package/dist/extensions/subagent/types.d.ts +7 -0
  132. package/dist/extensions/subagent/types.d.ts.map +1 -1
  133. package/dist/extensions/subagent/types.js.map +1 -1
  134. package/dist/extensions/todo/constants.d.ts +17 -6
  135. package/dist/extensions/todo/constants.d.ts.map +1 -1
  136. package/dist/extensions/todo/constants.js +22 -9
  137. package/dist/extensions/todo/constants.js.map +1 -1
  138. package/dist/extensions/todo/index.d.ts +0 -14
  139. package/dist/extensions/todo/index.d.ts.map +1 -1
  140. package/dist/extensions/todo/index.js +13 -1
  141. package/dist/extensions/todo/index.js.map +1 -1
  142. package/dist/extensions/todo/schema.d.ts +53 -2
  143. package/dist/extensions/todo/schema.d.ts.map +1 -1
  144. package/dist/extensions/todo/schema.js +57 -8
  145. package/dist/extensions/todo/schema.js.map +1 -1
  146. package/dist/extensions/todo/state.d.ts +16 -5
  147. package/dist/extensions/todo/state.d.ts.map +1 -1
  148. package/dist/extensions/todo/state.js +874 -130
  149. package/dist/extensions/todo/state.js.map +1 -1
  150. package/dist/extensions/todo/view.d.ts +16 -1
  151. package/dist/extensions/todo/view.d.ts.map +1 -1
  152. package/dist/extensions/todo/view.js +428 -1
  153. package/dist/extensions/todo/view.js.map +1 -1
  154. package/dist/migrations.d.ts.map +1 -1
  155. package/dist/migrations.js +2 -1
  156. package/dist/migrations.js.map +1 -1
  157. package/dist/modes/interactive/components/armin.d.ts.map +1 -1
  158. package/dist/modes/interactive/components/armin.js +5 -0
  159. package/dist/modes/interactive/components/armin.js.map +1 -1
  160. package/dist/modes/interactive/components/bash-execution.d.ts +1 -0
  161. package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
  162. package/dist/modes/interactive/components/bash-execution.js +4 -2
  163. package/dist/modes/interactive/components/bash-execution.js.map +1 -1
  164. package/dist/modes/interactive/components/countdown-timer.d.ts +13 -6
  165. package/dist/modes/interactive/components/countdown-timer.d.ts.map +1 -1
  166. package/dist/modes/interactive/components/countdown-timer.js +54 -17
  167. package/dist/modes/interactive/components/countdown-timer.js.map +1 -1
  168. package/dist/modes/interactive/components/extension-selector.d.ts +6 -1
  169. package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
  170. package/dist/modes/interactive/components/extension-selector.js +23 -11
  171. package/dist/modes/interactive/components/extension-selector.js.map +1 -1
  172. package/dist/modes/interactive/components/keybinding-hints.d.ts +11 -1
  173. package/dist/modes/interactive/components/keybinding-hints.d.ts.map +1 -1
  174. package/dist/modes/interactive/components/keybinding-hints.js +56 -0
  175. package/dist/modes/interactive/components/keybinding-hints.js.map +1 -1
  176. package/dist/modes/interactive/components/session-selector.d.ts +3 -0
  177. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  178. package/dist/modes/interactive/components/session-selector.js +52 -2
  179. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  180. package/dist/modes/interactive/components/tool-execution.d.ts +10 -0
  181. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  182. package/dist/modes/interactive/components/tool-execution.js +139 -18
  183. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  184. package/dist/modes/interactive/components/tool-group.d.ts +1 -0
  185. package/dist/modes/interactive/components/tool-group.d.ts.map +1 -1
  186. package/dist/modes/interactive/components/tool-group.js +5 -0
  187. package/dist/modes/interactive/components/tool-group.js.map +1 -1
  188. package/dist/modes/interactive/components/tree-selector.d.ts +10 -1
  189. package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
  190. package/dist/modes/interactive/components/tree-selector.js +55 -2
  191. package/dist/modes/interactive/components/tree-selector.js.map +1 -1
  192. package/dist/modes/interactive/interactive-mode.d.ts +5 -0
  193. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  194. package/dist/modes/interactive/interactive-mode.js +79 -19
  195. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  196. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  197. package/dist/modes/interactive/theme/theme.js +3 -2
  198. package/dist/modes/interactive/theme/theme.js.map +1 -1
  199. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  200. package/dist/modes/rpc/rpc-mode.js +8 -1
  201. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  202. package/dist/utils/paths.d.ts +6 -0
  203. package/dist/utils/paths.d.ts.map +1 -1
  204. package/dist/utils/paths.js +8 -0
  205. package/dist/utils/paths.js.map +1 -1
  206. package/docs/bundled/README.md +1 -0
  207. package/docs/bundled/extensions/deepwiki.md +1 -1
  208. package/docs/bundled/extensions/plan.md +13 -10
  209. package/docs/bundled/extensions/question.md +7 -5
  210. package/docs/bundled/extensions/router.md +3 -4
  211. package/docs/bundled/extensions/statusline.md +0 -1
  212. package/docs/bundled/extensions/subagent.md +5 -4
  213. package/docs/bundled/extensions/todo.md +9 -7
  214. package/docs/bundled/tool-presentation.md +17 -16
  215. package/docs/extensions.md +34 -5
  216. package/docs/rpc.md +1 -1
  217. package/docs/usage.md +1 -1
  218. package/npm-shrinkwrap.json +6 -6
  219. package/package.json +1 -1
  220. package/dist/utils/output-decoder.d.ts +0 -52
  221. package/dist/utils/output-decoder.d.ts.map +0 -1
  222. package/dist/utils/output-decoder.js +0 -134
  223. package/dist/utils/output-decoder.js.map +0 -1
@@ -1,20 +1,38 @@
1
1
  import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
2
2
  import { keyHint } from "../../modes/interactive/components/keybinding-hints.js";
3
3
  import { getMarkdownTheme } from "../../modes/interactive/theme/theme.js";
4
+ import { getSubagentRetryView } from "./retry.js";
4
5
  import { statusSummary } from "./runner.js";
5
6
  const SINGLE_EXCERPT_LIMIT = 200;
6
- const RUN_LINE_EXCERPT_LIMIT = 64;
7
+ const RUN_LINE_EXCERPT_LIMIT = 96;
8
+ const RUN_ERROR_EXCERPT_LIMIT = 200;
9
+ const RETRY_ERROR_EXCERPT_LIMIT = 160;
7
10
  const LIVE_TAIL_LIMIT = 100;
8
11
  const COLLAPSED_RUN_LIMIT = 4;
9
- const ACTIVITY_GROUPS = [
10
- { toolName: "read", verb: "read", singular: "file", plural: "files" },
11
- { toolName: "grep", verb: "searched", singular: "pattern", plural: "patterns" },
12
- { toolName: "find", verb: "searched", singular: "path pattern", plural: "path patterns" },
13
- { toolName: "ls", verb: "listed", singular: "directory", plural: "directories" },
14
- { toolName: "bash", verb: "ran", singular: "command", plural: "commands" },
15
- { toolName: "edit", verb: "edited", singular: "file", plural: "files" },
16
- { toolName: "write", verb: "wrote", singular: "file", plural: "files" },
17
- ];
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.
18
+ const ACTIVITY_DURATION_MIN_MS = 10_000;
19
+ const EXPANDED_LIVE_TAIL_LINES = 3;
20
+ class IndentedText {
21
+ text;
22
+ indent;
23
+ constructor(text, indent) {
24
+ this.text = new Text(text, 0, 0);
25
+ this.indent = indent;
26
+ }
27
+ render(width) {
28
+ const indent = Math.min(this.indent, Math.max(0, width - 1));
29
+ const prefix = " ".repeat(indent);
30
+ return this.text.render(Math.max(1, width - indent)).map((line) => `${prefix}${line}`);
31
+ }
32
+ invalidate() {
33
+ this.text.invalidate();
34
+ }
35
+ }
18
36
  // Display-only: profile names stay lowercase everywhere the model sees
19
37
  // them; the transcript shows "general" as "General", "code-reviewer" as
20
38
  // "Code Reviewer".
@@ -55,18 +73,54 @@ function stripBlockMarkdown(text) {
55
73
  })
56
74
  .join("\n");
57
75
  }
76
+ // finalOutput/liveText carry model-facing truncation notices from
77
+ // boundText/tailText. Only recognize their exact boundary lines so literal
78
+ // discussion of the marker remains visible in the transcript.
79
+ const OUTPUT_TRUNCATION_NOTICE_PATTERN = /^\[Output truncated(?:: \d+ bytes omitted)?\.\]$/u;
80
+ const EARLIER_OUTPUT_NOTICE = "[Earlier output omitted.]";
81
+ function terminalOutputNotice(text) {
82
+ const lines = text.split("\n");
83
+ let index = lines.length - 1;
84
+ while (index >= 0 && !lines[index]?.trim())
85
+ index--;
86
+ if (index < 0 || !OUTPUT_TRUNCATION_NOTICE_PATTERN.test(lines[index]?.trim() ?? "")) {
87
+ return { text, truncated: false };
88
+ }
89
+ lines.splice(index, 1);
90
+ return { text: lines.join("\n").trimEnd(), truncated: true };
91
+ }
92
+ function replaceBoundaryNotices(text) {
93
+ const lines = text.split("\n");
94
+ const first = lines.findIndex((line) => Boolean(line.trim()));
95
+ if (first >= 0 && lines[first]?.trim() === EARLIER_OUTPUT_NOTICE)
96
+ lines[first] = "…";
97
+ let last = lines.length - 1;
98
+ while (last >= 0 && !lines[last]?.trim())
99
+ last--;
100
+ if (last >= 0 && OUTPUT_TRUNCATION_NOTICE_PATTERN.test(lines[last]?.trim() ?? ""))
101
+ lines[last] = "…";
102
+ return lines.join("\n");
103
+ }
104
+ function plainExcerpt(text, limit) {
105
+ return truncate(stripInlineMarkdown(stripBlockMarkdown(text)).replace(/\s+/gu, " ").trim(), limit);
106
+ }
58
107
  function excerpt(text, limit) {
59
- // finalOutput/liveText carry model-facing truncation notices from
60
- // boundText/tailText; in the transcript an ellipsis is enough.
61
- const cleaned = text.replace(/\[(?:Output truncated(?:: \d+ bytes omitted\.)?|Earlier output omitted\.)\]/gu, "…");
62
- return truncate(stripInlineMarkdown(stripBlockMarkdown(cleaned)).replace(/\s+/gu, " ").trim(), limit);
108
+ return plainExcerpt(replaceBoundaryNotices(text), limit);
63
109
  }
64
- function liveTail(run) {
65
- const lines = run.liveText
110
+ // Generated notice-only boundary lines must never become the live line the
111
+ // user watches; identical text in the body remains ordinary model output.
112
+ function liveTailLines(run) {
113
+ const { text } = terminalOutputNotice(run.liveText);
114
+ const lines = text
66
115
  .split("\n")
67
116
  .map((line) => line.trim())
68
117
  .filter(Boolean);
69
- const last = lines.at(-1);
118
+ if (lines[0] === EARLIER_OUTPUT_NOTICE)
119
+ lines.shift();
120
+ return lines;
121
+ }
122
+ function liveTail(run) {
123
+ const last = liveTailLines(run).at(-1);
70
124
  return last ? excerpt(last, LIVE_TAIL_LIMIT) : undefined;
71
125
  }
72
126
  // Completed-run excerpts read better cut at a sentence boundary than
@@ -83,17 +137,30 @@ function leadingSentences(text, limit) {
83
137
  }
84
138
  if (boundary >= limit / 2)
85
139
  return window.slice(0, boundary).trimEnd();
86
- return truncate(window, limit);
140
+ return truncate(cleaned, limit);
87
141
  }
88
142
  const VERIFY_COMMAND_PATTERN = /\b(test|lint|typecheck|tsc|vitest|jest|pytest|build|check|fmt|format|compile)\b/iu;
89
143
  const INSPECT_COMMAND_PATTERN = /\b(rg|grep|find|git (?:diff|status|log|show)|ls|tree|cat|head|tail)\b/iu;
90
144
  const RETRY_MARKER_PATTERN = /^Retrying\b/u;
91
145
  const STREAM_MARKERS = new Set(["Thinking…", "Writing response…"]);
146
+ function retryText(run) {
147
+ const retry = getSubagentRetryView(run);
148
+ if (!retry)
149
+ return undefined;
150
+ const status = retry.remainingSeconds > 0
151
+ ? `Retrying (${retry.attempt}/${retry.maxAttempts}) in ${retry.remainingSeconds}s`
152
+ : `Retrying now… (${retry.attempt}/${retry.maxAttempts})`;
153
+ const error = excerpt(retry.error, RETRY_ERROR_EXCERPT_LIMIT);
154
+ return error ? `${status} — ${error}` : status;
155
+ }
92
156
  // Translates raw tool traffic into what the worker is doing right now, so
93
157
  // the collapsed card reads as a phase instead of a command line. Modelled
94
158
  // on tool-type classification; the raw tool summary stays as evidence on
95
159
  // the next line.
96
160
  function runIntent(run) {
161
+ const retry = retryText(run);
162
+ if (retry)
163
+ return retry;
97
164
  const current = run.currentActivity;
98
165
  if (current && RETRY_MARKER_PATTERN.test(current))
99
166
  return current;
@@ -125,6 +192,8 @@ function runIntent(run) {
125
192
  // The raw tool summary backs up the intent line; suppress it when it would
126
193
  // just repeat a stream/retry marker.
127
194
  function runEvidence(run) {
195
+ if (getSubagentRetryView(run))
196
+ return undefined;
128
197
  const current = run.currentActivity;
129
198
  if (!current || STREAM_MARKERS.has(current) || RETRY_MARKER_PATTERN.test(current))
130
199
  return undefined;
@@ -136,44 +205,77 @@ function liveElapsed(run) {
136
205
  const end = run.endedAt ?? Date.now();
137
206
  return formatDuration(Math.max(0, (end - run.startedAt) / 1000));
138
207
  }
208
+ function statusColor(status) {
209
+ switch (status) {
210
+ case "completed":
211
+ return "success";
212
+ case "failed":
213
+ return "error";
214
+ case "aborted":
215
+ return "warning";
216
+ case "running":
217
+ return "accent";
218
+ default:
219
+ return "muted";
220
+ }
221
+ }
139
222
  // The tool shell already paints a call-level status dot (● in
140
223
  // warning/success/error), so `●` is off-limits here: `›` marks where the
141
224
  // work currently is, and the remaining glyphs are terminal states.
142
225
  function statusMarker(status, theme) {
226
+ const glyph = status === "completed"
227
+ ? "✓"
228
+ : status === "failed"
229
+ ? "×"
230
+ : status === "aborted"
231
+ ? "■"
232
+ : status === "running"
233
+ ? "›"
234
+ : "○";
235
+ return theme.fg(statusColor(status), glyph);
236
+ }
237
+ function statusWord(status) {
143
238
  switch (status) {
144
239
  case "completed":
145
- return theme.fg("success", "✓");
240
+ return "Completed";
146
241
  case "failed":
147
- return theme.fg("error", "×");
242
+ return "Failed";
148
243
  case "aborted":
149
- return theme.fg("warning", "■");
244
+ return "Aborted";
150
245
  case "running":
151
- return theme.fg("accent", "›");
246
+ return "Running";
152
247
  default:
153
- return theme.fg("muted", "○");
248
+ return "Queued";
154
249
  }
155
250
  }
156
251
  function runTitle(run, theme, index) {
157
252
  const number = index === undefined ? "" : theme.fg("dim", `${index} · `);
158
253
  return `${statusMarker(run.status, theme)} ${number}${theme.fg("accent", displayAgentName(run.agent))}${theme.fg("dim", ` · ${truncate(run.description, 48)}`)}`;
159
254
  }
255
+ // Mid-run token totals are cache-inflated (totalTokens sums cache reads
256
+ // across turns), so live lines quote tool uses and the context watermark
257
+ // instead; settled lines get the real total.
160
258
  function runProgressText(run) {
161
259
  const items = [`${run.usage.toolUses} tool use${run.usage.toolUses === 1 ? "" : "s"}`];
162
- if (run.usage.totalTokens)
163
- items.push(`${formatTokens(run.usage.totalTokens)} tokens`);
260
+ if (run.usage.contextTokens)
261
+ items.push(`ctx: ${formatTokens(run.usage.contextTokens)}`);
164
262
  return items.join(" · ");
165
263
  }
166
264
  function runLine(run, theme, index) {
167
265
  let line = runTitle(run, theme, index);
168
- const detail = run.status === "running"
169
- ? runIntent(run)
170
- : run.status === "failed" || run.status === "aborted"
171
- ? run.error && excerpt(run.error, RUN_LINE_EXCERPT_LIMIT)
172
- : run.finalOutput && excerpt(run.finalOutput, RUN_LINE_EXCERPT_LIMIT);
266
+ const retry = retryText(run);
267
+ const detail = retry ??
268
+ (run.status === "running"
269
+ ? excerpt(runIntent(run), LIVE_TAIL_LIMIT)
270
+ : run.status === "failed" || run.status === "aborted"
271
+ ? undefined
272
+ : run.finalOutput && leadingSentences(run.finalOutput, RUN_LINE_EXCERPT_LIMIT));
173
273
  if (detail)
174
- line += theme.fg(run.status === "failed" ? "error" : "dim", ` — ${detail}`);
274
+ line += theme.fg("dim", ` — ${detail}`);
175
275
  if (run.status === "running") {
176
276
  line += theme.fg("dim", ` · ${runProgressText(run)}`);
277
+ if (run.usage.cost)
278
+ line += theme.fg("dim", ` · $${run.usage.cost.toFixed(3)}`);
177
279
  const elapsed = liveElapsed(run);
178
280
  if (elapsed)
179
281
  line += theme.fg("dim", ` · ${elapsed}`);
@@ -184,6 +286,20 @@ function runLine(run, theme, index) {
184
286
  }
185
287
  return line;
186
288
  }
289
+ function runErrorExcerpt(run) {
290
+ if ((run.status !== "failed" && run.status !== "aborted") || !run.error)
291
+ return undefined;
292
+ return leadingSentences(run.error, RUN_ERROR_EXCERPT_LIMIT);
293
+ }
294
+ function runErrorIndent(index) {
295
+ return index === undefined ? 2 : 5 + String(index).length;
296
+ }
297
+ function addRunSummary(container, run, theme, index) {
298
+ container.addChild(new Text(runLine(run, theme, index), 0, 0));
299
+ const error = runErrorExcerpt(run);
300
+ if (error)
301
+ container.addChild(new IndentedText(theme.fg("error", error), runErrorIndent(index)));
302
+ }
187
303
  // Keep active work visible while a batch is in flight: completed runs
188
304
  // may otherwise crowd a still-running task out of the collapsed window.
189
305
  function selectCollapsedRuns(runs, isPartial) {
@@ -192,32 +308,44 @@ function selectCollapsedRuns(runs, isPartial) {
192
308
  if (!isPartial)
193
309
  return runs.slice(0, COLLAPSED_RUN_LIMIT);
194
310
  const active = runs.filter((run) => run.status === "running" || run.status === "queued");
311
+ const retrying = active.filter((run) => getSubagentRetryView(run));
312
+ const retryingSet = new Set(retrying);
313
+ const otherActive = active.filter((run) => !retryingSet.has(run));
195
314
  const settled = runs.filter((run) => run.status !== "running" && run.status !== "queued");
196
- return [...active, ...settled].slice(0, COLLAPSED_RUN_LIMIT);
315
+ return [...retrying, ...otherActive, ...settled].slice(0, COLLAPSED_RUN_LIMIT);
197
316
  }
198
317
  function singleCollapsedLines(details, theme) {
199
318
  const run = details.runs[0];
200
319
  if (!run)
201
320
  return [theme.fg("muted", "Initializing…")];
202
321
  if (run.status === "running" || run.status === "queued") {
203
- const isInitializing = run.status === "running" && run.usage.turns === 0 && run.usage.toolUses === 0 && run.liveText.length === 0;
204
- const intent = run.status === "queued"
205
- ? (run.currentActivity ?? "queued")
206
- : isInitializing
207
- ? "Initializing…"
208
- : runIntent(run);
322
+ const retry = retryText(run);
323
+ const isInitializing = run.status === "running" &&
324
+ !retry &&
325
+ run.usage.turns === 0 &&
326
+ run.usage.toolUses === 0 &&
327
+ run.liveText.length === 0;
328
+ const rawIntent = retry ??
329
+ (run.status === "queued"
330
+ ? (run.currentActivity ?? statusWord(run.status))
331
+ : isInitializing
332
+ ? "Initializing…"
333
+ : runIntent(run));
334
+ const intent = excerpt(rawIntent, LIVE_TAIL_LIMIT);
209
335
  // No status marker here: the shell's call-level dot already covers
210
336
  // the whole card, and a single run has nothing to disambiguate.
211
337
  let status = theme.fg("dim", intent);
212
338
  if (run.status === "running" && !isInitializing) {
213
339
  status += theme.fg("dim", ` · ${runProgressText(run)}`);
340
+ if (run.usage.cost)
341
+ status += theme.fg("dim", ` · $${run.usage.cost.toFixed(3)}`);
214
342
  const elapsed = liveElapsed(run);
215
343
  if (elapsed)
216
344
  status += theme.fg("dim", ` · ${elapsed}`);
217
345
  }
218
346
  const lines = [status];
219
347
  const evidence = runEvidence(run);
220
- if (evidence && evidence !== intent)
348
+ if (evidence && evidence !== rawIntent)
221
349
  lines.push(`${theme.fg("accent", "›")} ${theme.fg("dim", excerpt(evidence, LIVE_TAIL_LIMIT))}`);
222
350
  const tail = liveTail(run);
223
351
  if (tail)
@@ -228,55 +356,45 @@ function singleCollapsedLines(details, theme) {
228
356
  return [theme.fg("error", excerpt(run.error, SINGLE_EXCERPT_LIMIT))];
229
357
  if (run.finalOutput)
230
358
  return [theme.fg("toolOutput", leadingSentences(run.finalOutput, SINGLE_EXCERPT_LIMIT))];
231
- return [theme.fg("muted", run.status)];
359
+ return [theme.fg("muted", statusWord(run.status))];
232
360
  }
233
- function activitySummaryText(details) {
234
- const toolCounts = new Map();
235
- let activityCount = 0;
236
- for (const run of details.runs) {
237
- for (const activity of run.activities) {
238
- activityCount++;
239
- toolCounts.set(activity.toolName, (toolCounts.get(activity.toolName) ?? 0) + 1);
240
- }
241
- }
361
+ // Deep-trimmed settled footer: turns and per-run breakdowns belong to the
362
+ // expanded view. Parallel batches keep only whole-call numbers — the run
363
+ // rows above already localize the work.
364
+ function collapsedUsageText(details, extended) {
365
+ const usage = details.usage;
242
366
  const items = [];
243
- let groupedCount = 0;
244
- for (const group of ACTIVITY_GROUPS) {
245
- const count = toolCounts.get(group.toolName) ?? 0;
246
- if (count === 0)
247
- continue;
248
- groupedCount += count;
249
- items.push(`${group.verb} ${count} ${count === 1 ? group.singular : group.plural}`);
367
+ if (usage.totalTokens)
368
+ items.push(`${formatTokens(usage.totalTokens)} tok`);
369
+ if (extended && usage.toolUses)
370
+ items.push(`${usage.toolUses} tool use${usage.toolUses === 1 ? "" : "s"}`);
371
+ if (extended && usage.contextTokens)
372
+ items.push(`ctx: ${formatTokens(usage.contextTokens)}`);
373
+ if (usage.cost)
374
+ items.push(`$${usage.cost.toFixed(3)}`);
375
+ if (details.endedAt !== undefined) {
376
+ items.push(formatDuration(Math.max(0, (details.endedAt - details.startedAt) / 1000)));
250
377
  }
251
- const otherCount = activityCount - groupedCount;
252
- if (otherCount > 0)
253
- items.push(`used ${otherCount} other tool${otherCount === 1 ? "" : "s"}`);
254
- if (items.length === 0)
255
- return "";
256
- const earlierCount = Math.max(0, details.usage.toolUses - activityCount);
257
- if (earlierCount > 0)
258
- items.push(`${earlierCount} earlier tool use${earlierCount === 1 ? "" : "s"}`);
259
- return `${items[0][0].toUpperCase()}${items[0].slice(1)}${items
260
- .slice(1)
261
- .map((item) => ` · ${item}`)
262
- .join("")}`;
263
- }
264
- function usageText(details, includeToolUses = true) {
378
+ return items.join(" · ");
379
+ }
380
+ function batchUsageText(details, isPartial) {
265
381
  const usage = details.usage;
266
382
  const items = [];
267
- if (includeToolUses && usage.toolUses)
383
+ // The aggregate mixes settled and running runs while partial, and the
384
+ // running share is cache-inflated — quote the total only once settled.
385
+ if (!isPartial && usage.totalTokens)
386
+ items.push(`${formatTokens(usage.totalTokens)} tok`);
387
+ if (usage.toolUses)
268
388
  items.push(`${usage.toolUses} tool use${usage.toolUses === 1 ? "" : "s"}`);
269
389
  if (usage.turns)
270
390
  items.push(`${usage.turns} turn${usage.turns === 1 ? "" : "s"}`);
271
- if (usage.output)
272
- items.push(`↓${formatTokens(usage.output)}`);
273
391
  if (usage.contextTokens)
274
- items.push(`ctx:${formatTokens(usage.contextTokens)}`);
392
+ items.push(`ctx: ${formatTokens(usage.contextTokens)}`);
275
393
  if (usage.cost)
276
394
  items.push(`$${usage.cost.toFixed(3)}`);
277
- const duration = details.endedAt ? Math.max(0, (details.endedAt - details.startedAt) / 1000) : undefined;
278
- if (duration !== undefined)
279
- items.push(formatDuration(duration));
395
+ if (details.endedAt !== undefined) {
396
+ items.push(formatDuration(Math.max(0, (details.endedAt - details.startedAt) / 1000)));
397
+ }
280
398
  return items.join(" · ");
281
399
  }
282
400
  function formatTokens(value) {
@@ -287,55 +405,139 @@ function formatTokens(value) {
287
405
  return `${(value / 1_000_000).toFixed(1)}M`;
288
406
  }
289
407
  function formatDuration(seconds) {
290
- if (seconds >= 90)
291
- return `${Math.floor(seconds / 60)}m ${Math.round(seconds % 60)}s`;
408
+ const roundedSeconds = Math.round(seconds);
409
+ if (roundedSeconds >= 60)
410
+ return `${Math.floor(roundedSeconds / 60)}m ${roundedSeconds % 60}s`;
292
411
  return `${seconds.toFixed(seconds < 10 ? 1 : 0)}s`;
293
412
  }
294
- // A single run's title and usage would duplicate the call header and
295
- // the call-level usage line, so `single` drops both and folds the
296
- // duration into the metadata line instead. Multi-run sections carry a
297
- // numbered `──` header that matches the table of contents above them.
413
+ function runDurationText(run) {
414
+ if (run.startedAt === undefined || run.endedAt === undefined)
415
+ return undefined;
416
+ return formatDuration(Math.max(0, (run.endedAt - run.startedAt) / 1000));
417
+ }
418
+ // One dim line per run: what it cost. Settled runs quote the real token
419
+ // total; running runs skip it (cache-inflated mid-run) and tick elapsed
420
+ // time instead. Batch sections fold model/thinking/cwd in here because
421
+ // their `──` header already carries verdict and duration.
422
+ function runMetricsText(run, includeIdentity) {
423
+ const usage = run.usage;
424
+ const settled = run.status !== "running" && run.status !== "queued";
425
+ const items = [];
426
+ if (includeIdentity) {
427
+ items.push(run.model);
428
+ if (run.thinking !== "off")
429
+ items.push(run.thinking);
430
+ if (run.cwd)
431
+ items.push(`cwd: ${run.cwd}`);
432
+ }
433
+ if (settled && usage.totalTokens)
434
+ items.push(`${formatTokens(usage.totalTokens)} tok`);
435
+ if (usage.toolUses)
436
+ items.push(`${usage.toolUses} tool use${usage.toolUses === 1 ? "" : "s"}`);
437
+ if (usage.turns)
438
+ items.push(`${usage.turns} turn${usage.turns === 1 ? "" : "s"}`);
439
+ if (usage.contextTokens)
440
+ items.push(`ctx: ${formatTokens(usage.contextTokens)}`);
441
+ if (usage.cost)
442
+ items.push(`$${usage.cost.toFixed(3)}`);
443
+ const duration = settled ? (includeIdentity ? undefined : runDurationText(run)) : liveElapsed(run);
444
+ if (duration)
445
+ items.push(duration);
446
+ return items.join(" · ");
447
+ }
448
+ // The details prompt is a 1KB bounded copy of the briefing; the preview
449
+ // shows the first content lines (blank lines carry nothing at two lines
450
+ // of budget) and never pretends to more fidelity than that.
451
+ function promptSection(run, theme) {
452
+ const boundedPrompt = terminalOutputNotice(run.prompt);
453
+ const lines = boundedPrompt.text
454
+ .split("\n")
455
+ .map((line) => plainExcerpt(line.trim(), Number.MAX_SAFE_INTEGER))
456
+ .filter(Boolean);
457
+ const shown = lines.slice(0, PROMPT_PREVIEW_LINES);
458
+ const preview = shown.map((line) => theme.fg("dim", truncate(line, PROMPT_PREVIEW_LINE_LIMIT)));
459
+ // A one-line briefing clipped mid-sentence must still say it continues.
460
+ const clipped = shown.some((line) => line.length > PROMPT_PREVIEW_LINE_LIMIT);
461
+ const remaining = lines.length - shown.length;
462
+ const capped = boundedPrompt.truncated;
463
+ const section = [theme.fg("muted", "Prompt"), ...preview];
464
+ if (remaining > 0 || capped || clipped) {
465
+ let note = "… continues";
466
+ if (remaining > 0)
467
+ note += `, ${remaining} more line${remaining === 1 ? "" : "s"}`;
468
+ if (capped)
469
+ note += " · capped at 1KB";
470
+ section.push(theme.fg("muted", note));
471
+ }
472
+ return section;
473
+ }
474
+ // Each run expands into a report cover sheet: verdict and cost up top, a
475
+ // bounded Prompt/Activity digest in the middle, then the full Report —
476
+ // the one unbounded element, placed last so the card ends where reading
477
+ // begins. Single runs skip the `──` header (the call header already names
478
+ // them); batch sections carry a numbered header matching the contents
479
+ // list above.
298
480
  function renderRunDetails(run, theme, single, index) {
299
481
  const container = new Container();
300
- const duration = run.startedAt !== undefined && run.endedAt !== undefined
301
- ? formatDuration(Math.max(0, (run.endedAt - run.startedAt) / 1000))
302
- : undefined;
303
- if (!single) {
482
+ const settled = run.status !== "running" && run.status !== "queued";
483
+ if (single) {
484
+ const identity = [run.model];
485
+ if (run.thinking !== "off")
486
+ identity.push(`${run.thinking} thinking`);
487
+ if (run.cwd)
488
+ identity.push(`cwd: ${run.cwd}`);
489
+ container.addChild(new Text(`${statusMarker(run.status, theme)} ${theme.fg(statusColor(run.status), statusWord(run.status))}${theme.fg("dim", ` · ${identity.join(" · ")}`)}`, 0, 0));
490
+ }
491
+ else {
304
492
  const label = index === undefined ? "" : `${index} · `;
305
- container.addChild(new Text(`${theme.fg("dim", `── ${label}`)}${theme.fg("accent", displayAgentName(run.agent))}${theme.fg("dim", ` · ${truncate(run.description, 48)}`)}`, 0, 0));
493
+ const duration = settled ? runDurationText(run) : undefined;
494
+ 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));
495
+ }
496
+ if (run.status !== "queued") {
497
+ const metrics = runMetricsText(run, !single);
498
+ if (metrics)
499
+ container.addChild(new Text(theme.fg("dim", metrics), 0, 0));
306
500
  }
307
- container.addChild(new Text(theme.fg("muted", "Task"), 0, 0));
308
- container.addChild(new Text(theme.fg("dim", run.prompt), 0, 0));
501
+ const retry = retryText(run);
502
+ if (retry)
503
+ container.addChild(new Text(theme.fg("accent", retry), 0, 0));
504
+ container.addChild(new Spacer(1));
505
+ for (const line of promptSection(run, theme))
506
+ container.addChild(new Text(line, 0, 0));
309
507
  if (run.activities.length > 0) {
310
508
  container.addChild(new Spacer(1));
311
- container.addChild(new Text(theme.fg("muted", "Activity"), 0, 0));
509
+ const total = run.usage.toolUses;
510
+ const label = total > run.activities.length ? `Activity · last ${run.activities.length} of ${total}` : "Activity";
511
+ container.addChild(new Text(theme.fg("muted", label), 0, 0));
312
512
  for (const activity of run.activities) {
313
- const marker = activity.status === "succeeded"
314
- ? theme.fg("success", "✓")
315
- : activity.status === "failed"
316
- ? theme.fg("error", "×")
317
- : theme.fg("accent", "›");
318
- let text = `${marker} ${theme.fg("toolOutput", excerpt(activity.summary, 96))}`;
319
- if (activity.resultSummary) {
320
- const color = activity.status === "failed" ? "error" : "dim";
321
- text += ` ${theme.fg(color, `· ${excerpt(activity.resultSummary, 64)}`)}`;
513
+ // Deviation earns ink: × and › are marked, success rows are quiet
514
+ // one-liners without result echoes.
515
+ let text;
516
+ if (activity.status === "failed") {
517
+ text = `${theme.fg("error", "×")} ${theme.fg("toolOutput", excerpt(activity.summary, ACTIVITY_SUMMARY_LIMIT))}`;
518
+ if (activity.resultSummary) {
519
+ text += ` ${theme.fg("error", `· ${excerpt(activity.resultSummary, ACTIVITY_RESULT_LIMIT)}`)}`;
520
+ }
322
521
  }
323
- if (activity.endedAt !== undefined && activity.endedAt - activity.startedAt >= 1_000) {
522
+ else if (activity.status === "running") {
523
+ text = `${theme.fg("accent", "›")} ${theme.fg("toolOutput", excerpt(activity.summary, ACTIVITY_SUMMARY_LIMIT))}`;
524
+ }
525
+ else {
526
+ text = ` ${theme.fg("toolOutput", excerpt(activity.summary, ACTIVITY_SUMMARY_LIMIT))}`;
527
+ }
528
+ if (activity.endedAt !== undefined && activity.endedAt - activity.startedAt >= ACTIVITY_DURATION_MIN_MS) {
324
529
  text += ` ${theme.fg("dim", `· ${formatDuration((activity.endedAt - activity.startedAt) / 1000)}`)}`;
325
530
  }
326
531
  container.addChild(new Text(text, 0, 0));
327
532
  }
328
533
  }
329
- // The live tail is the "now" end of the timeline, so it sits after the
330
- // activity list; once the run settles, Response takes its place.
534
+ // The live tail is the "now" end of the timeline; once the run settles,
535
+ // Report takes this slot without moving anything above it.
331
536
  if (run.status === "running" && run.liveText) {
332
- const tail = run.liveText
333
- .split("\n")
334
- .map((line) => line.trim())
335
- .filter(Boolean)
336
- .slice(-4);
537
+ const tail = liveTailLines(run).slice(-EXPANDED_LIVE_TAIL_LINES);
337
538
  if (tail.length > 0) {
338
539
  container.addChild(new Spacer(1));
540
+ container.addChild(new Text(theme.fg("muted", "Working"), 0, 0));
339
541
  for (const line of tail) {
340
542
  container.addChild(new Text(theme.fg("dim", excerpt(line, LIVE_TAIL_LIMIT)), 0, 0));
341
543
  }
@@ -343,28 +545,16 @@ function renderRunDetails(run, theme, single, index) {
343
545
  }
344
546
  if (run.error) {
345
547
  container.addChild(new Spacer(1));
346
- container.addChild(new Text(theme.fg("error", `Error: ${run.error}`), 0, 0));
548
+ container.addChild(new Text(theme.fg("muted", "Error"), 0, 0));
549
+ container.addChild(new Text(theme.fg("error", run.error), 0, 0));
347
550
  }
348
551
  if (run.finalOutput) {
349
552
  container.addChild(new Spacer(1));
350
- container.addChild(new Text(theme.fg("muted", "Response"), 0, 0));
351
- container.addChild(new Markdown(run.finalOutput, 0, 0, getMarkdownTheme()));
352
- }
353
- const metadata = [
354
- `${run.model} · ${run.thinking}`,
355
- run.cwd ? `cwd: ${run.cwd}` : undefined,
356
- run.usage.toolUses ? `${run.usage.toolUses} tool use${run.usage.toolUses === 1 ? "" : "s"}` : undefined,
357
- run.usage.turns ? `${run.usage.turns} turn${run.usage.turns === 1 ? "" : "s"}` : undefined,
358
- run.usage.output ? `↓${formatTokens(run.usage.output)}` : undefined,
359
- run.usage.contextTokens ? `ctx:${formatTokens(run.usage.contextTokens)}` : undefined,
360
- run.usage.cost ? `$${run.usage.cost.toFixed(3)}` : undefined,
361
- single ? duration : undefined,
362
- ]
363
- .filter((item) => Boolean(item))
364
- .join(" · ");
365
- if (metadata) {
366
- container.addChild(new Spacer(1));
367
- container.addChild(new Text(theme.fg("dim", metadata), 0, 0));
553
+ const label = settled && run.status !== "completed" ? "Report · partial" : "Report";
554
+ container.addChild(new Text(theme.fg("muted", label), 0, 0));
555
+ container.addChild(new Markdown(run.finalOutput, 0, 0, getMarkdownTheme(), {
556
+ color: (text) => theme.fg("toolOutput", text),
557
+ }));
368
558
  }
369
559
  return container;
370
560
  }
@@ -391,33 +581,43 @@ export function renderSubagentResult(result, options, theme, isError) {
391
581
  const text = result.content.find((part) => part.type === "text");
392
582
  return new Text(text?.type === "text" ? text.text : "(no output)", 0, 0);
393
583
  }
584
+ // The pre-resolution first paint carries no runs yet, in either mode.
585
+ if (details.runs.length === 0) {
586
+ return new Text(theme.fg("muted", "Initializing…"), 0, 0);
587
+ }
394
588
  if (!options.expanded) {
395
- const lines = [];
396
589
  if (details.mode === "single") {
397
- lines.push(...singleCollapsedLines(details, theme));
398
- }
399
- else {
400
- let header = statusSummary(details);
401
- if (options.isPartial) {
402
- header += ` · ${formatDuration(Math.max(0, (Date.now() - details.startedAt) / 1000))}`;
590
+ const lines = singleCollapsedLines(details, theme);
591
+ if (!options.isPartial) {
592
+ const usage = collapsedUsageText(details, true);
593
+ if (usage)
594
+ lines.push(theme.fg("dim", usage));
595
+ lines.push(theme.fg("muted", `(${keyHint("app.tools.expand", "to expand")})`));
403
596
  }
404
- lines.push(theme.fg(isError ? "error" : options.isPartial ? "accent" : "muted", header));
405
- const shown = selectCollapsedRuns(details.runs, Boolean(options.isPartial));
406
- for (const run of shown)
407
- lines.push(runLine(run, theme));
408
- const hidden = details.runs.length - shown.length;
409
- if (hidden > 0)
410
- lines.push(theme.fg("muted", `+${hidden} more`));
597
+ return new Text(lines.join("\n"), 0, 0);
598
+ }
599
+ const container = new Container();
600
+ let header = statusSummary(details);
601
+ if (options.isPartial) {
602
+ header += ` · ${formatDuration(Math.max(0, (Date.now() - details.startedAt) / 1000))}`;
603
+ }
604
+ container.addChild(new Text(theme.fg(isError ? "error" : options.isPartial ? "accent" : "muted", header), 0, 0));
605
+ const shown = selectCollapsedRuns(details.runs, Boolean(options.isPartial));
606
+ // Stable ordinals (the run's task position, not its display slot)
607
+ // keep identity readable while active-first ordering reshuffles
608
+ // rows, and match the expanded section numbers.
609
+ for (const run of shown)
610
+ addRunSummary(container, run, theme, details.runs.indexOf(run) + 1);
611
+ const hidden = details.runs.length - shown.length;
612
+ if (hidden > 0)
613
+ container.addChild(new Text(theme.fg("muted", `+${hidden} more`), 0, 0));
614
+ if (!options.isPartial) {
615
+ const usage = collapsedUsageText(details, false);
616
+ if (usage)
617
+ container.addChild(new Text(theme.fg("dim", usage), 0, 0));
618
+ container.addChild(new Text(theme.fg("muted", `(${keyHint("app.tools.expand", "to expand")})`), 0, 0));
411
619
  }
412
- const activitySummary = activitySummaryText(details);
413
- if (activitySummary)
414
- lines.push(theme.fg("dim", activitySummary));
415
- const usage = usageText(details, !activitySummary);
416
- if (usage)
417
- lines.push(theme.fg("dim", usage));
418
- if (!options.isPartial)
419
- lines.push(theme.fg("muted", `(${keyHint("app.tools.expand", "to expand")})`));
420
- return new Text(lines.join("\n"), 0, 0);
620
+ return container;
421
621
  }
422
622
  const container = new Container();
423
623
  const single = details.mode === "single" && details.runs.length === 1;
@@ -427,7 +627,7 @@ export function renderSubagentResult(result, options, theme, isError) {
427
627
  // numbers against the `──` section headers below makes an 8-run
428
628
  // batch navigable by eye.
429
629
  details.runs.forEach((run, index) => {
430
- container.addChild(new Text(runLine(run, theme, index + 1), 0, 0));
630
+ addRunSummary(container, run, theme, index + 1);
431
631
  });
432
632
  }
433
633
  details.runs.forEach((run, index) => {
@@ -436,7 +636,7 @@ export function renderSubagentResult(result, options, theme, isError) {
436
636
  container.addChild(renderRunDetails(run, theme, single, single ? undefined : index + 1));
437
637
  });
438
638
  if (!single) {
439
- const usage = usageText(details);
639
+ const usage = batchUsageText(details, Boolean(options.isPartial));
440
640
  if (usage) {
441
641
  container.addChild(new Spacer(1));
442
642
  container.addChild(new Text(theme.fg("dim", usage), 0, 0));