@astralyn/pi 0.82.4 → 0.82.6

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 (144) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/dist/cli/project-trust.d.ts.map +1 -1
  3. package/dist/cli/project-trust.js +4 -1
  4. package/dist/cli/project-trust.js.map +1 -1
  5. package/dist/core/agent-session.d.ts.map +1 -1
  6. package/dist/core/agent-session.js +2 -2
  7. package/dist/core/agent-session.js.map +1 -1
  8. package/dist/core/bash-executor.d.ts.map +1 -1
  9. package/dist/core/bash-executor.js +4 -22
  10. package/dist/core/bash-executor.js.map +1 -1
  11. package/dist/core/extensions/index.d.ts +2 -2
  12. package/dist/core/extensions/index.d.ts.map +1 -1
  13. package/dist/core/extensions/index.js +1 -1
  14. package/dist/core/extensions/index.js.map +1 -1
  15. package/dist/core/extensions/loader.d.ts +1 -1
  16. package/dist/core/extensions/loader.d.ts.map +1 -1
  17. package/dist/core/extensions/loader.js +2 -3
  18. package/dist/core/extensions/loader.js.map +1 -1
  19. package/dist/core/extensions/types.d.ts +37 -5
  20. package/dist/core/extensions/types.d.ts.map +1 -1
  21. package/dist/core/extensions/types.js +11 -0
  22. package/dist/core/extensions/types.js.map +1 -1
  23. package/dist/core/tools/output-accumulator.d.ts +3 -13
  24. package/dist/core/tools/output-accumulator.d.ts.map +1 -1
  25. package/dist/core/tools/output-accumulator.js +6 -36
  26. package/dist/core/tools/output-accumulator.js.map +1 -1
  27. package/dist/extensions/plan/constants.d.ts +11 -2
  28. package/dist/extensions/plan/constants.d.ts.map +1 -1
  29. package/dist/extensions/plan/constants.js +14 -2
  30. package/dist/extensions/plan/constants.js.map +1 -1
  31. package/dist/extensions/plan/index.d.ts +0 -18
  32. package/dist/extensions/plan/index.d.ts.map +1 -1
  33. package/dist/extensions/plan/index.js +35 -3
  34. package/dist/extensions/plan/index.js.map +1 -1
  35. package/dist/extensions/plan/view.d.ts +36 -0
  36. package/dist/extensions/plan/view.d.ts.map +1 -0
  37. package/dist/extensions/plan/view.js +74 -0
  38. package/dist/extensions/plan/view.js.map +1 -0
  39. package/dist/extensions/question/dialog.d.ts +3 -1
  40. package/dist/extensions/question/dialog.d.ts.map +1 -1
  41. package/dist/extensions/question/dialog.js +106 -52
  42. package/dist/extensions/question/dialog.js.map +1 -1
  43. package/dist/extensions/question/index.d.ts.map +1 -1
  44. package/dist/extensions/question/index.js +2 -2
  45. package/dist/extensions/question/index.js.map +1 -1
  46. package/dist/extensions/question/render-cache.d.ts +9 -0
  47. package/dist/extensions/question/render-cache.d.ts.map +1 -1
  48. package/dist/extensions/question/render-cache.js +22 -0
  49. package/dist/extensions/question/render-cache.js.map +1 -1
  50. package/dist/extensions/question/results.d.ts.map +1 -1
  51. package/dist/extensions/question/results.js +6 -1
  52. package/dist/extensions/question/results.js.map +1 -1
  53. package/dist/extensions/question/schema.d.ts +4 -0
  54. package/dist/extensions/question/schema.d.ts.map +1 -1
  55. package/dist/extensions/question/schema.js +19 -28
  56. package/dist/extensions/question/schema.js.map +1 -1
  57. package/dist/extensions/question/types.d.ts +1 -1
  58. package/dist/extensions/question/types.d.ts.map +1 -1
  59. package/dist/extensions/question/types.js.map +1 -1
  60. package/dist/extensions/router/stream.d.ts.map +1 -1
  61. package/dist/extensions/router/stream.js +44 -0
  62. package/dist/extensions/router/stream.js.map +1 -1
  63. package/dist/extensions/subagent/activity.d.ts.map +1 -1
  64. package/dist/extensions/subagent/activity.js +25 -4
  65. package/dist/extensions/subagent/activity.js.map +1 -1
  66. package/dist/extensions/subagent/agents.d.ts.map +1 -1
  67. package/dist/extensions/subagent/agents.js +12 -6
  68. package/dist/extensions/subagent/agents.js.map +1 -1
  69. package/dist/extensions/subagent/constants.d.ts +1 -1
  70. package/dist/extensions/subagent/constants.d.ts.map +1 -1
  71. package/dist/extensions/subagent/constants.js +3 -2
  72. package/dist/extensions/subagent/constants.js.map +1 -1
  73. package/dist/extensions/subagent/index.d.ts.map +1 -1
  74. package/dist/extensions/subagent/index.js +14 -0
  75. package/dist/extensions/subagent/index.js.map +1 -1
  76. package/dist/extensions/subagent/render.d.ts.map +1 -1
  77. package/dist/extensions/subagent/render.js +243 -116
  78. package/dist/extensions/subagent/render.js.map +1 -1
  79. package/dist/extensions/subagent/resolve.d.ts.map +1 -1
  80. package/dist/extensions/subagent/resolve.js +13 -9
  81. package/dist/extensions/subagent/resolve.js.map +1 -1
  82. package/dist/extensions/subagent/runner.d.ts +1 -1
  83. package/dist/extensions/subagent/runner.d.ts.map +1 -1
  84. package/dist/extensions/subagent/runner.js +9 -3
  85. package/dist/extensions/subagent/runner.js.map +1 -1
  86. package/dist/extensions/subagent/schema.d.ts.map +1 -1
  87. package/dist/extensions/subagent/schema.js +4 -4
  88. package/dist/extensions/subagent/schema.js.map +1 -1
  89. package/dist/extensions/subagent/sdk-runner.d.ts.map +1 -1
  90. package/dist/extensions/subagent/sdk-runner.js +5 -0
  91. package/dist/extensions/subagent/sdk-runner.js.map +1 -1
  92. package/dist/extensions/subagent/types.d.ts +7 -0
  93. package/dist/extensions/subagent/types.d.ts.map +1 -1
  94. package/dist/extensions/subagent/types.js.map +1 -1
  95. package/dist/extensions/todo/constants.d.ts +17 -6
  96. package/dist/extensions/todo/constants.d.ts.map +1 -1
  97. package/dist/extensions/todo/constants.js +47 -9
  98. package/dist/extensions/todo/constants.js.map +1 -1
  99. package/dist/extensions/todo/index.d.ts +1 -15
  100. package/dist/extensions/todo/index.d.ts.map +1 -1
  101. package/dist/extensions/todo/index.js +35 -4
  102. package/dist/extensions/todo/index.js.map +1 -1
  103. package/dist/extensions/todo/schema.d.ts +59 -7
  104. package/dist/extensions/todo/schema.d.ts.map +1 -1
  105. package/dist/extensions/todo/schema.js +72 -12
  106. package/dist/extensions/todo/schema.js.map +1 -1
  107. package/dist/extensions/todo/state.d.ts +20 -5
  108. package/dist/extensions/todo/state.d.ts.map +1 -1
  109. package/dist/extensions/todo/state.js +1016 -134
  110. package/dist/extensions/todo/state.js.map +1 -1
  111. package/dist/extensions/todo/view.d.ts +16 -1
  112. package/dist/extensions/todo/view.d.ts.map +1 -1
  113. package/dist/extensions/todo/view.js +293 -7
  114. package/dist/extensions/todo/view.js.map +1 -1
  115. package/dist/modes/interactive/components/extension-selector.d.ts +6 -1
  116. package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
  117. package/dist/modes/interactive/components/extension-selector.js +14 -6
  118. package/dist/modes/interactive/components/extension-selector.js.map +1 -1
  119. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  120. package/dist/modes/interactive/components/tool-execution.js +6 -0
  121. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  122. package/dist/modes/interactive/interactive-mode.d.ts +2 -0
  123. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  124. package/dist/modes/interactive/interactive-mode.js +41 -2
  125. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  126. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  127. package/dist/modes/rpc/rpc-mode.js +8 -1
  128. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  129. package/dist/utils/coalescing-terminal.d.ts +71 -17
  130. package/dist/utils/coalescing-terminal.d.ts.map +1 -1
  131. package/dist/utils/coalescing-terminal.js +136 -31
  132. package/dist/utils/coalescing-terminal.js.map +1 -1
  133. package/docs/bundled/extensions/plan.md +6 -4
  134. package/docs/bundled/extensions/question.md +9 -7
  135. package/docs/bundled/extensions/subagent.md +4 -4
  136. package/docs/bundled/extensions/todo.md +9 -5
  137. package/docs/bundled/tool-presentation.md +1 -1
  138. package/docs/extensions.md +14 -0
  139. package/npm-shrinkwrap.json +2 -2
  140. package/package.json +1 -1
  141. package/dist/utils/output-decoder.d.ts +0 -52
  142. package/dist/utils/output-decoder.d.ts.map +0 -1
  143. package/dist/utils/output-decoder.js +0 -134
  144. package/dist/utils/output-decoder.js.map +0 -1
@@ -6,15 +6,14 @@ const SINGLE_EXCERPT_LIMIT = 200;
6
6
  const RUN_LINE_EXCERPT_LIMIT = 64;
7
7
  const LIVE_TAIL_LIMIT = 100;
8
8
  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
- ];
9
+ const PROMPT_PREVIEW_LINES = 2;
10
+ const PROMPT_PREVIEW_LINE_LIMIT = 120;
11
+ const ACTIVITY_SUMMARY_LIMIT = 96;
12
+ const ACTIVITY_RESULT_LIMIT = 64;
13
+ // Sub-10s tool calls are routine; a duration only earns ink when it
14
+ // explains where the time went.
15
+ const ACTIVITY_DURATION_MIN_MS = 10_000;
16
+ const EXPANDED_LIVE_TAIL_LINES = 3;
18
17
  // Display-only: profile names stay lowercase everywhere the model sees
19
18
  // them; the transcript shows "general" as "General", "code-reviewer" as
20
19
  // "Code Reviewer".
@@ -55,18 +54,54 @@ function stripBlockMarkdown(text) {
55
54
  })
56
55
  .join("\n");
57
56
  }
57
+ // finalOutput/liveText carry model-facing truncation notices from
58
+ // boundText/tailText. Only recognize their exact boundary lines so literal
59
+ // discussion of the marker remains visible in the transcript.
60
+ const OUTPUT_TRUNCATION_NOTICE_PATTERN = /^\[Output truncated(?:: \d+ bytes omitted)?\.\]$/u;
61
+ const EARLIER_OUTPUT_NOTICE = "[Earlier output omitted.]";
62
+ function terminalOutputNotice(text) {
63
+ const lines = text.split("\n");
64
+ let index = lines.length - 1;
65
+ while (index >= 0 && !lines[index]?.trim())
66
+ index--;
67
+ if (index < 0 || !OUTPUT_TRUNCATION_NOTICE_PATTERN.test(lines[index]?.trim() ?? "")) {
68
+ return { text, truncated: false };
69
+ }
70
+ lines.splice(index, 1);
71
+ return { text: lines.join("\n").trimEnd(), truncated: true };
72
+ }
73
+ function replaceBoundaryNotices(text) {
74
+ const lines = text.split("\n");
75
+ const first = lines.findIndex((line) => Boolean(line.trim()));
76
+ if (first >= 0 && lines[first]?.trim() === EARLIER_OUTPUT_NOTICE)
77
+ lines[first] = "…";
78
+ let last = lines.length - 1;
79
+ while (last >= 0 && !lines[last]?.trim())
80
+ last--;
81
+ if (last >= 0 && OUTPUT_TRUNCATION_NOTICE_PATTERN.test(lines[last]?.trim() ?? ""))
82
+ lines[last] = "…";
83
+ return lines.join("\n");
84
+ }
85
+ function plainExcerpt(text, limit) {
86
+ return truncate(stripInlineMarkdown(stripBlockMarkdown(text)).replace(/\s+/gu, " ").trim(), limit);
87
+ }
58
88
  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);
89
+ return plainExcerpt(replaceBoundaryNotices(text), limit);
63
90
  }
64
- function liveTail(run) {
65
- const lines = run.liveText
91
+ // Generated notice-only boundary lines must never become the live line the
92
+ // user watches; identical text in the body remains ordinary model output.
93
+ function liveTailLines(run) {
94
+ const { text } = terminalOutputNotice(run.liveText);
95
+ const lines = text
66
96
  .split("\n")
67
97
  .map((line) => line.trim())
68
98
  .filter(Boolean);
69
- const last = lines.at(-1);
99
+ if (lines[0] === EARLIER_OUTPUT_NOTICE)
100
+ lines.shift();
101
+ return lines;
102
+ }
103
+ function liveTail(run) {
104
+ const last = liveTailLines(run).at(-1);
70
105
  return last ? excerpt(last, LIVE_TAIL_LIMIT) : undefined;
71
106
  }
72
107
  // Completed-run excerpts read better cut at a sentence boundary than
@@ -136,31 +171,60 @@ function liveElapsed(run) {
136
171
  const end = run.endedAt ?? Date.now();
137
172
  return formatDuration(Math.max(0, (end - run.startedAt) / 1000));
138
173
  }
174
+ function statusColor(status) {
175
+ switch (status) {
176
+ case "completed":
177
+ return "success";
178
+ case "failed":
179
+ return "error";
180
+ case "aborted":
181
+ return "warning";
182
+ case "running":
183
+ return "accent";
184
+ default:
185
+ return "muted";
186
+ }
187
+ }
139
188
  // The tool shell already paints a call-level status dot (● in
140
189
  // warning/success/error), so `●` is off-limits here: `›` marks where the
141
190
  // work currently is, and the remaining glyphs are terminal states.
142
191
  function statusMarker(status, theme) {
192
+ const glyph = status === "completed"
193
+ ? "✓"
194
+ : status === "failed"
195
+ ? "×"
196
+ : status === "aborted"
197
+ ? "■"
198
+ : status === "running"
199
+ ? "›"
200
+ : "○";
201
+ return theme.fg(statusColor(status), glyph);
202
+ }
203
+ function statusWord(status) {
143
204
  switch (status) {
144
205
  case "completed":
145
- return theme.fg("success", "✓");
206
+ return "Completed";
146
207
  case "failed":
147
- return theme.fg("error", "×");
208
+ return "Failed";
148
209
  case "aborted":
149
- return theme.fg("warning", "■");
210
+ return "Aborted";
150
211
  case "running":
151
- return theme.fg("accent", "›");
212
+ return "Running";
152
213
  default:
153
- return theme.fg("muted", "○");
214
+ return "Queued";
154
215
  }
155
216
  }
156
217
  function runTitle(run, theme, index) {
157
218
  const number = index === undefined ? "" : theme.fg("dim", `${index} · `);
158
219
  return `${statusMarker(run.status, theme)} ${number}${theme.fg("accent", displayAgentName(run.agent))}${theme.fg("dim", ` · ${truncate(run.description, 48)}`)}`;
159
220
  }
221
+ // Mid-run token totals are cache-inflated (totalTokens sums cache reads
222
+ // across turns), so live lines quote tool uses and the context watermark
223
+ // instead; settled lines get the real total.
160
224
  function runProgressText(run) {
161
225
  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`);
226
+ if (run.usage.contextTokens)
227
+ items.push(`ctx:${formatTokens(run.usage.contextTokens)}`);
164
228
  return items.join(" · ");
165
229
  }
166
230
  function runLine(run, theme, index) {
@@ -211,6 +275,8 @@ function singleCollapsedLines(details, theme) {
211
275
  let status = theme.fg("dim", intent);
212
276
  if (run.status === "running" && !isInitializing) {
213
277
  status += theme.fg("dim", ` · ${runProgressText(run)}`);
278
+ if (run.usage.cost)
279
+ status += theme.fg("dim", ` · $${run.usage.cost.toFixed(3)}`);
214
280
  const elapsed = liveElapsed(run);
215
281
  if (elapsed)
216
282
  status += theme.fg("dim", ` · ${elapsed}`);
@@ -230,53 +296,43 @@ function singleCollapsedLines(details, theme) {
230
296
  return [theme.fg("toolOutput", leadingSentences(run.finalOutput, SINGLE_EXCERPT_LIMIT))];
231
297
  return [theme.fg("muted", run.status)];
232
298
  }
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
- }
299
+ // Deep-trimmed settled footer: turns and per-run breakdowns belong to the
300
+ // expanded view. Parallel batches keep only whole-call numbers — the run
301
+ // rows above already localize the work.
302
+ function collapsedUsageText(details, extended) {
303
+ const usage = details.usage;
242
304
  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}`);
305
+ if (usage.totalTokens)
306
+ items.push(`${formatTokens(usage.totalTokens)} tok`);
307
+ if (extended && usage.toolUses)
308
+ items.push(`${usage.toolUses} tool use${usage.toolUses === 1 ? "" : "s"}`);
309
+ if (extended && usage.contextTokens)
310
+ items.push(`ctx:${formatTokens(usage.contextTokens)}`);
311
+ if (usage.cost)
312
+ items.push(`$${usage.cost.toFixed(3)}`);
313
+ if (details.endedAt !== undefined) {
314
+ items.push(formatDuration(Math.max(0, (details.endedAt - details.startedAt) / 1000)));
250
315
  }
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) {
316
+ return items.join(" · ");
317
+ }
318
+ function batchUsageText(details, isPartial) {
265
319
  const usage = details.usage;
266
320
  const items = [];
267
- if (includeToolUses && usage.toolUses)
321
+ // The aggregate mixes settled and running runs while partial, and the
322
+ // running share is cache-inflated — quote the total only once settled.
323
+ if (!isPartial && usage.totalTokens)
324
+ items.push(`${formatTokens(usage.totalTokens)} tok`);
325
+ if (usage.toolUses)
268
326
  items.push(`${usage.toolUses} tool use${usage.toolUses === 1 ? "" : "s"}`);
269
327
  if (usage.turns)
270
328
  items.push(`${usage.turns} turn${usage.turns === 1 ? "" : "s"}`);
271
- if (usage.output)
272
- items.push(`↓${formatTokens(usage.output)}`);
273
329
  if (usage.contextTokens)
274
330
  items.push(`ctx:${formatTokens(usage.contextTokens)}`);
275
331
  if (usage.cost)
276
332
  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));
333
+ if (details.endedAt !== undefined) {
334
+ items.push(formatDuration(Math.max(0, (details.endedAt - details.startedAt) / 1000)));
335
+ }
280
336
  return items.join(" · ");
281
337
  }
282
338
  function formatTokens(value) {
@@ -291,51 +347,131 @@ function formatDuration(seconds) {
291
347
  return `${Math.floor(seconds / 60)}m ${Math.round(seconds % 60)}s`;
292
348
  return `${seconds.toFixed(seconds < 10 ? 1 : 0)}s`;
293
349
  }
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.
350
+ function runDurationText(run) {
351
+ if (run.startedAt === undefined || run.endedAt === undefined)
352
+ return undefined;
353
+ return formatDuration(Math.max(0, (run.endedAt - run.startedAt) / 1000));
354
+ }
355
+ // One dim line per run: what it cost. Settled runs quote the real token
356
+ // total; running runs skip it (cache-inflated mid-run) and tick elapsed
357
+ // time instead. Batch sections fold model/thinking/cwd in here because
358
+ // their `──` header already carries verdict and duration.
359
+ function runMetricsText(run, includeIdentity) {
360
+ const usage = run.usage;
361
+ const settled = run.status !== "running" && run.status !== "queued";
362
+ const items = [];
363
+ if (includeIdentity) {
364
+ items.push(run.model);
365
+ if (run.thinking !== "off")
366
+ items.push(run.thinking);
367
+ if (run.cwd)
368
+ items.push(`cwd: ${run.cwd}`);
369
+ }
370
+ if (settled && usage.totalTokens)
371
+ items.push(`${formatTokens(usage.totalTokens)} tok`);
372
+ if (usage.toolUses)
373
+ items.push(`${usage.toolUses} tool use${usage.toolUses === 1 ? "" : "s"}`);
374
+ if (usage.turns)
375
+ items.push(`${usage.turns} turn${usage.turns === 1 ? "" : "s"}`);
376
+ if (usage.contextTokens)
377
+ items.push(`ctx:${formatTokens(usage.contextTokens)}`);
378
+ if (usage.cost)
379
+ items.push(`$${usage.cost.toFixed(3)}`);
380
+ const duration = settled ? (includeIdentity ? undefined : runDurationText(run)) : liveElapsed(run);
381
+ if (duration)
382
+ items.push(duration);
383
+ return items.join(" · ");
384
+ }
385
+ // The details prompt is a 1KB bounded copy of the briefing; the preview
386
+ // shows the first content lines (blank lines carry nothing at two lines
387
+ // of budget) and never pretends to more fidelity than that.
388
+ function promptSection(run, theme) {
389
+ const boundedPrompt = terminalOutputNotice(run.prompt);
390
+ const lines = boundedPrompt.text
391
+ .split("\n")
392
+ .map((line) => plainExcerpt(line.trim(), Number.MAX_SAFE_INTEGER))
393
+ .filter(Boolean);
394
+ const shown = lines.slice(0, PROMPT_PREVIEW_LINES);
395
+ const preview = shown.map((line) => theme.fg("dim", truncate(line, PROMPT_PREVIEW_LINE_LIMIT)));
396
+ // A one-line briefing clipped mid-sentence must still say it continues.
397
+ const clipped = shown.some((line) => line.length > PROMPT_PREVIEW_LINE_LIMIT);
398
+ const remaining = lines.length - shown.length;
399
+ const capped = boundedPrompt.truncated;
400
+ const section = [theme.fg("muted", "Prompt"), ...preview];
401
+ if (remaining > 0 || capped || clipped) {
402
+ let note = "… continues";
403
+ if (remaining > 0)
404
+ note += `, ${remaining} more line${remaining === 1 ? "" : "s"}`;
405
+ if (capped)
406
+ note += " · capped at 1KB";
407
+ section.push(theme.fg("muted", note));
408
+ }
409
+ return section;
410
+ }
411
+ // Each run expands into a report cover sheet: verdict and cost up top, a
412
+ // bounded Prompt/Activity digest in the middle, then the full Report —
413
+ // the one unbounded element, placed last so the card ends where reading
414
+ // begins. Single runs skip the `──` header (the call header already names
415
+ // them); batch sections carry a numbered header matching the contents
416
+ // list above.
298
417
  function renderRunDetails(run, theme, single, index) {
299
418
  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) {
304
- 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));
419
+ const settled = run.status !== "running" && run.status !== "queued";
420
+ if (single) {
421
+ const identity = [run.model];
422
+ if (run.thinking !== "off")
423
+ identity.push(`${run.thinking} thinking`);
424
+ if (run.cwd)
425
+ identity.push(`cwd: ${run.cwd}`);
426
+ container.addChild(new Text(`${statusMarker(run.status, theme)} ${theme.fg(statusColor(run.status), statusWord(run.status))}${theme.fg("dim", ` · ${identity.join(" · ")}`)}`, 0, 0));
427
+ }
428
+ else {
429
+ const label = index === undefined ? "" : `${index} `;
430
+ const duration = settled ? runDurationText(run) : undefined;
431
+ 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));
432
+ }
433
+ if (run.status !== "queued") {
434
+ const metrics = runMetricsText(run, !single);
435
+ if (metrics)
436
+ container.addChild(new Text(theme.fg("dim", metrics), 0, 0));
306
437
  }
307
- container.addChild(new Text(theme.fg("muted", "Task"), 0, 0));
308
- container.addChild(new Text(theme.fg("dim", run.prompt), 0, 0));
438
+ container.addChild(new Spacer(1));
439
+ for (const line of promptSection(run, theme))
440
+ container.addChild(new Text(line, 0, 0));
309
441
  if (run.activities.length > 0) {
310
442
  container.addChild(new Spacer(1));
311
- container.addChild(new Text(theme.fg("muted", "Activity"), 0, 0));
443
+ const total = run.usage.toolUses;
444
+ const label = total > run.activities.length ? `Activity · last ${run.activities.length} of ${total}` : "Activity";
445
+ container.addChild(new Text(theme.fg("muted", label), 0, 0));
312
446
  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)}`)}`;
447
+ // Deviation earns ink: × and › are marked, success rows are quiet
448
+ // one-liners without result echoes.
449
+ let text;
450
+ if (activity.status === "failed") {
451
+ text = `${theme.fg("error", "×")} ${theme.fg("toolOutput", excerpt(activity.summary, ACTIVITY_SUMMARY_LIMIT))}`;
452
+ if (activity.resultSummary) {
453
+ text += ` ${theme.fg("error", `· ${excerpt(activity.resultSummary, ACTIVITY_RESULT_LIMIT)}`)}`;
454
+ }
322
455
  }
323
- if (activity.endedAt !== undefined && activity.endedAt - activity.startedAt >= 1_000) {
456
+ else if (activity.status === "running") {
457
+ text = `${theme.fg("accent", "›")} ${theme.fg("toolOutput", excerpt(activity.summary, ACTIVITY_SUMMARY_LIMIT))}`;
458
+ }
459
+ else {
460
+ text = theme.fg("toolOutput", excerpt(activity.summary, ACTIVITY_SUMMARY_LIMIT));
461
+ }
462
+ if (activity.endedAt !== undefined && activity.endedAt - activity.startedAt >= ACTIVITY_DURATION_MIN_MS) {
324
463
  text += ` ${theme.fg("dim", `· ${formatDuration((activity.endedAt - activity.startedAt) / 1000)}`)}`;
325
464
  }
326
465
  container.addChild(new Text(text, 0, 0));
327
466
  }
328
467
  }
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.
468
+ // The live tail is the "now" end of the timeline; once the run settles,
469
+ // Report takes this slot without moving anything above it.
331
470
  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);
471
+ const tail = liveTailLines(run).slice(-EXPANDED_LIVE_TAIL_LINES);
337
472
  if (tail.length > 0) {
338
473
  container.addChild(new Spacer(1));
474
+ container.addChild(new Text(theme.fg("muted", "Working"), 0, 0));
339
475
  for (const line of tail) {
340
476
  container.addChild(new Text(theme.fg("dim", excerpt(line, LIVE_TAIL_LIMIT)), 0, 0));
341
477
  }
@@ -343,29 +479,15 @@ function renderRunDetails(run, theme, single, index) {
343
479
  }
344
480
  if (run.error) {
345
481
  container.addChild(new Spacer(1));
346
- container.addChild(new Text(theme.fg("error", `Error: ${run.error}`), 0, 0));
482
+ container.addChild(new Text(theme.fg("muted", "Error"), 0, 0));
483
+ container.addChild(new Text(theme.fg("error", run.error), 0, 0));
347
484
  }
348
485
  if (run.finalOutput) {
349
486
  container.addChild(new Spacer(1));
350
- container.addChild(new Text(theme.fg("muted", "Response"), 0, 0));
487
+ const label = settled && run.status !== "completed" ? "Report · partial" : "Report";
488
+ container.addChild(new Text(theme.fg("muted", label), 0, 0));
351
489
  container.addChild(new Markdown(run.finalOutput, 0, 0, getMarkdownTheme()));
352
490
  }
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));
368
- }
369
491
  return container;
370
492
  }
371
493
  // The call header stays a single line: run rows in the result area take
@@ -391,6 +513,10 @@ export function renderSubagentResult(result, options, theme, isError) {
391
513
  const text = result.content.find((part) => part.type === "text");
392
514
  return new Text(text?.type === "text" ? text.text : "(no output)", 0, 0);
393
515
  }
516
+ // The pre-resolution first paint carries no runs yet, in either mode.
517
+ if (details.runs.length === 0) {
518
+ return new Text(theme.fg("muted", "Initializing…"), 0, 0);
519
+ }
394
520
  if (!options.expanded) {
395
521
  const lines = [];
396
522
  if (details.mode === "single") {
@@ -403,20 +529,21 @@ export function renderSubagentResult(result, options, theme, isError) {
403
529
  }
404
530
  lines.push(theme.fg(isError ? "error" : options.isPartial ? "accent" : "muted", header));
405
531
  const shown = selectCollapsedRuns(details.runs, Boolean(options.isPartial));
532
+ // Stable ordinals (the run's task position, not its display slot)
533
+ // keep identity readable while active-first ordering reshuffles
534
+ // rows, and match the expanded section numbers.
406
535
  for (const run of shown)
407
- lines.push(runLine(run, theme));
536
+ lines.push(runLine(run, theme, details.runs.indexOf(run) + 1));
408
537
  const hidden = details.runs.length - shown.length;
409
538
  if (hidden > 0)
410
539
  lines.push(theme.fg("muted", `+${hidden} more`));
411
540
  }
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)
541
+ if (!options.isPartial) {
542
+ const usage = collapsedUsageText(details, details.mode === "single");
543
+ if (usage)
544
+ lines.push(theme.fg("dim", usage));
419
545
  lines.push(theme.fg("muted", `(${keyHint("app.tools.expand", "to expand")})`));
546
+ }
420
547
  return new Text(lines.join("\n"), 0, 0);
421
548
  }
422
549
  const container = new Container();
@@ -436,7 +563,7 @@ export function renderSubagentResult(result, options, theme, isError) {
436
563
  container.addChild(renderRunDetails(run, theme, single, single ? undefined : index + 1));
437
564
  });
438
565
  if (!single) {
439
- const usage = usageText(details);
566
+ const usage = batchUsageText(details, Boolean(options.isPartial));
440
567
  if (usage) {
441
568
  container.addChild(new Spacer(1));
442
569
  container.addChild(new Text(theme.fg("dim", usage), 0, 0));