@iowarp/clio-coder 0.4.5 → 0.4.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 (188) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/CONTRIBUTING.md +14 -9
  3. package/NOTICE +16 -0
  4. package/README.md +312 -685
  5. package/dist/{acp-H3CU2HBQ.js → acp-UVEVWSDL.js} +6 -6
  6. package/dist/{agents-IXQQZ7BD.js → agents-VWY37DTK.js} +31 -30
  7. package/dist/assets/codewiki.json +1 -1
  8. package/dist/{auth-R7KYF7N3.js → auth-PCV2XQDV.js} +6 -6
  9. package/dist/{builtins-CRFDQUVP.js → builtins-PD652MTK.js} +3 -3
  10. package/dist/{chunk-SDWNDICN.js → chunk-24T2YZ7A.js} +58 -56
  11. package/dist/{chunk-FSTIBAGM.js → chunk-2AKYFREM.js} +7 -7
  12. package/dist/{chunk-SE4ZKILO.js → chunk-3BJGMT2D.js} +30 -9
  13. package/dist/{chunk-GVYWZNRK.js → chunk-3HQVBIKD.js} +2 -2
  14. package/dist/{chunk-EWD5P3GK.js → chunk-3JLFK5MW.js} +4 -4
  15. package/dist/{chunk-O2RQ7LCK.js → chunk-3O2ZAUGQ.js} +2 -2
  16. package/dist/{chunk-GXCCHKN6.js → chunk-3QKXHCGS.js} +4 -4
  17. package/dist/{chunk-YEJIG7LN.js → chunk-3WBALRQH.js} +7 -7
  18. package/dist/{chunk-I7VLWRK2.js → chunk-4DU2TVI4.js} +5 -5
  19. package/dist/{chunk-TEG3RYVZ.js → chunk-4U4TKZFR.js} +3 -3
  20. package/dist/{chunk-EHCPSZJF.js → chunk-4VMV6OS6.js} +2 -2
  21. package/dist/{chunk-T7FDXNRU.js → chunk-536HXRFE.js} +4 -4
  22. package/dist/{chunk-ZMZQEBKI.js → chunk-5QRX7SLS.js} +8 -8
  23. package/dist/{chunk-RXIOCVBZ.js → chunk-6HAO6X5B.js} +4 -4
  24. package/dist/{chunk-HGSLRG33.js → chunk-6OII3LOK.js} +2 -2
  25. package/dist/{chunk-U2JOLB7O.js → chunk-6RYDAVQT.js} +32 -32
  26. package/dist/{chunk-T53XI6I7.js → chunk-6VRRJT7L.js} +2 -2
  27. package/dist/{chunk-RLTT5GVY.js → chunk-7JSSKXG2.js} +4 -4
  28. package/dist/{chunk-633EXGZA.js → chunk-ACCKVSHU.js} +3 -3
  29. package/dist/{chunk-6SYEGZI3.js → chunk-B3MOMLJ4.js} +2 -2
  30. package/dist/{chunk-UXAC2FX4.js → chunk-BLTEAUCB.js} +3 -3
  31. package/dist/{chunk-DRD2A54W.js → chunk-BT7HGDOX.js} +15 -15
  32. package/dist/{chunk-F34AR6O3.js → chunk-DXLUVUD4.js} +5 -5
  33. package/dist/{chunk-HR3HPQ76.js → chunk-E7RF4KBO.js} +2 -2
  34. package/dist/chunk-EM2WNQS2.js +93 -0
  35. package/dist/{chunk-KYKNBD4Y.js → chunk-F5Z7EWMN.js} +953 -233
  36. package/dist/{chunk-VLPTKCKT.js → chunk-GLYBJ4RQ.js} +3 -3
  37. package/dist/{chunk-KDWLXVMN.js → chunk-HPTJF6DG.js} +16 -39
  38. package/dist/{chunk-6CDNRHWH.js → chunk-HZ2T2YED.js} +2 -2
  39. package/dist/{chunk-UTUNT5QL.js → chunk-HZFBVU5X.js} +7 -7
  40. package/dist/{chunk-KF6GFW6O.js → chunk-I2K2NCZQ.js} +7 -7
  41. package/dist/{chunk-5DDOTTG4.js → chunk-IGLVVNEG.js} +2 -2
  42. package/dist/{chunk-4SVIVOMC.js → chunk-IKRWLEDA.js} +5 -5
  43. package/dist/{chunk-R6MAYTMS.js → chunk-JMPL7NUB.js} +3 -3
  44. package/dist/{chunk-5ZMXGW7P.js → chunk-KAFMW7WM.js} +4 -85
  45. package/dist/{chunk-R6MICQYU.js → chunk-LIN2K3EL.js} +4 -4
  46. package/dist/{chunk-QEQISF75.js → chunk-LK2FN5JY.js} +1 -1
  47. package/dist/{chunk-YYB3MC65.js → chunk-N2OKBREN.js} +2 -2
  48. package/dist/{chunk-XXBQKW32.js → chunk-NNWJYUNO.js} +4 -4
  49. package/dist/{chunk-TZ7MFRG4.js → chunk-P5JMBPDJ.js} +22 -20
  50. package/dist/{chunk-WFKDPU7U.js → chunk-P6M7RKMI.js} +2 -2
  51. package/dist/{chunk-MDU3C27C.js → chunk-PZN2LQY6.js} +5 -5
  52. package/dist/{chunk-S32UQ5GL.js → chunk-Q7NNUHQV.js} +15 -15
  53. package/dist/{chunk-U7VQ4NI4.js → chunk-QCOV5DFY.js} +5 -5
  54. package/dist/{chunk-MC3JSVR6.js → chunk-QJMRYTCX.js} +7 -7
  55. package/dist/{chunk-DIMRLNIC.js → chunk-QNWRHOMF.js} +3 -3
  56. package/dist/{chunk-ILJ7DWGE.js → chunk-QXFBABFJ.js} +4 -4
  57. package/dist/chunk-R2CT5YDO.js +66 -0
  58. package/dist/{chunk-E3BQTQUP.js → chunk-RGZNQ5CT.js} +3 -3
  59. package/dist/{chunk-SBZX6ARC.js → chunk-SXQKNKRA.js} +7 -7
  60. package/dist/{chunk-WLMSOT73.js → chunk-TJFDL26W.js} +3 -3
  61. package/dist/{chunk-EQEJJ53P.js → chunk-VAHLPXPY.js} +2 -2
  62. package/dist/{chunk-7TKWWPD7.js → chunk-VANN3GDP.js} +3 -23
  63. package/dist/{chunk-GNQRWTMY.js → chunk-VHMX7SXF.js} +2 -2
  64. package/dist/{chunk-UL36QJX6.js → chunk-WKUYQBCG.js} +6 -6
  65. package/dist/{chunk-FKIOX6DS.js → chunk-WNGF5AJX.js} +9 -4
  66. package/dist/{chunk-ICO4TQ2F.js → chunk-WV6TDAPN.js} +36 -20
  67. package/dist/{chunk-K6RDB7IA.js → chunk-X3BI7HTV.js} +9 -9
  68. package/dist/{chunk-NY6IWDVU.js → chunk-XWMSPJYN.js} +2 -2
  69. package/dist/{chunk-7HBHHVCK.js → chunk-YPRSNUTC.js} +2 -2
  70. package/dist/{chunk-ONRTXOZY.js → chunk-ZS43Y2FT.js} +2 -2
  71. package/dist/cli/index.js +27 -27
  72. package/dist/{clio-5I2IBFYL.js → clio-QQCMTUZE.js} +6 -6
  73. package/dist/{code-nav-YXIAEZVS.js → code-nav-GUPM6766.js} +8 -8
  74. package/dist/{config-Y2JDJ3JJ.js → config-IIM7YOD3.js} +42 -41
  75. package/dist/{configure-SML6UBZ5.js → configure-4ZM5HWJX.js} +28 -10
  76. package/dist/{context-S2EEC3EJ.js → context-44BZLJ3J.js} +12 -12
  77. package/dist/{context-P5YSEO55.js → context-AIDVQKMW.js} +34 -33
  78. package/dist/{context-QQ53HSGJ.js → context-OL5P3HEP.js} +25 -24
  79. package/dist/{context-clear-TMEQCOTH.js → context-clear-OQV76MYP.js} +34 -33
  80. package/dist/{context-working-set-CNLSQN5K.js → context-working-set-ZUW3EGLS.js} +10 -9
  81. package/dist/{detail-5572VLBO.js → detail-UXPWRANG.js} +35 -34
  82. package/dist/{dispatch-runner-FRHD6P2O.js → dispatch-runner-P3ZW6G77.js} +38 -37
  83. package/dist/{doctor-N3LCM2LG.js → doctor-JJVGPZWT.js} +18 -17
  84. package/dist/{eval-BUU3ZOUZ.js → eval-V2RTN3UH.js} +26 -25
  85. package/dist/{evidence-DTGEN456.js → evidence-35PYWGSO.js} +34 -33
  86. package/dist/{evidence-SQ5DNFEN.js → evidence-CIGGKI6J.js} +36 -35
  87. package/dist/{evolve-IJNUFZDS.js → evolve-33SM3NJP.js} +34 -33
  88. package/dist/{fleet-ZNNVSYFO.js → fleet-LFO7SIPC.js} +62 -61
  89. package/dist/{fleet-commands-5YE7NPJL.js → fleet-commands-RLK72GU3.js} +9 -9
  90. package/dist/fleet-decisions-3SBSCTCW.js +1 -1
  91. package/dist/{fleet-graph-6HHPWF5T.js → fleet-graph-JZIBCGMW.js} +11 -11
  92. package/dist/{fleet-inspect-JEYMGCGC.js → fleet-inspect-2L5YGGZJ.js} +35 -34
  93. package/dist/{fleet-validate-4PM4GDDL.js → fleet-validate-LIAVKDUU.js} +12 -12
  94. package/dist/{fleet-verify-NJDTHTO3.js → fleet-verify-JIK3OQZR.js} +34 -33
  95. package/dist/{fleet-view-UP2BWQQY.js → fleet-view-IWHYGQKK.js} +35 -34
  96. package/dist/{init-S3GZQ5ZF.js → init-NJEDOTQW.js} +46 -45
  97. package/dist/{interop-YOTGXFJM.js → interop-RVCAYECG.js} +4 -4
  98. package/dist/{inventory-K2OZ36YJ.js → inventory-74UL7UHU.js} +35 -34
  99. package/dist/{library-GJI6W6OK.js → library-CTKZA7K3.js} +10 -10
  100. package/dist/{memory-OQOXQGYD.js → memory-I76I5VUH.js} +34 -33
  101. package/dist/{models-3IK2Y5WE.js → models-D4YDIQZQ.js} +19 -18
  102. package/dist/{monitor-FTJLTKCQ.js → monitor-NKVD55HX.js} +40 -39
  103. package/dist/{orchestrator-N47O7SFD.js → orchestrator-I6OATVPK.js} +658 -1092
  104. package/dist/{preload-65U34NG6.js → preload-EJAWJEIO.js} +34 -33
  105. package/dist/{reset-MUVQDLNL.js → reset-4XMQKDNZ.js} +5 -3
  106. package/dist/{resources-LM76COAO.js → resources-TVYVXK3Q.js} +10 -10
  107. package/dist/{run-3VFZQ33N.js → run-OWQNNSJU.js} +61 -60
  108. package/dist/{share-4JUB4BL7.js → share-NPZZMUXG.js} +11 -11
  109. package/dist/{skills-S36AAJHU.js → skills-OXFEAWMT.js} +12 -12
  110. package/dist/{skills-eval-G6T57L7M.js → skills-eval-J5TCKSJA.js} +38 -37
  111. package/dist/{skills-inventory-WLM46PJH.js → skills-inventory-7II4DM57.js} +10 -10
  112. package/dist/{slash-commands-GBARG3Y7.js → slash-commands-MDSDSWYF.js} +26 -25
  113. package/dist/{targets-N5REUETU.js → targets-KILZEOCE.js} +36 -20
  114. package/dist/{tasks-6BVY5YZF.js → tasks-R6P6OV22.js} +7 -7
  115. package/dist/{terminal-lease-57UEOJKS.js → terminal-lease-LZCG2KN3.js} +2 -2
  116. package/dist/{upgrade-4HD5HAIV.js → upgrade-D2OZH6IQ.js} +9 -9
  117. package/dist/{usage-4GWM2WWP.js → usage-PIASNUS4.js} +40 -39
  118. package/dist/{verifiers-GSKWKWVG.js → verifiers-LTHUY2PX.js} +9 -9
  119. package/dist/{verify-3AS3ULCY.js → verify-HYF7KBVE.js} +7 -7
  120. package/dist/{wiki-generate-7BV7XWIY.js → wiki-generate-RK7JQ5NP.js} +51 -50
  121. package/dist/worker/entry.js +41 -39
  122. package/docs/architecture/context-engine.md +1 -1
  123. package/docs/architecture/tui-design.md +45 -30
  124. package/docs/guide/commands-and-modes.md +26 -25
  125. package/docs/guide/configuration-and-targets.md +122 -27
  126. package/docs/guide/configuration-reference.md +9 -4
  127. package/docs/guide/fleet-dispatch.md +1 -6
  128. package/docs/guide/glossary.md +1 -1
  129. package/docs/guide/installation-and-lifecycle.md +44 -11
  130. package/docs/process/development-pipeline.md +1 -1
  131. package/docs/process/documentation-coverage.md +1 -1
  132. package/docs/process/release-cut-checklist.md +18 -6
  133. package/package.json +1 -1
  134. package/src/cli/ask.ts +10 -10
  135. package/src/cli/configure-editor.ts +51 -0
  136. package/src/cli/configure-interop.ts +1 -1
  137. package/src/cli/configure-oauth.ts +1 -1
  138. package/src/cli/configure-onboarding.ts +232 -96
  139. package/src/cli/configure-prompts.ts +76 -0
  140. package/src/cli/configure-quick.ts +274 -0
  141. package/src/cli/configure-target.ts +14 -2
  142. package/src/cli/configure.ts +413 -174
  143. package/src/cli/oauth-manual-input.ts +1 -1
  144. package/src/cli/reset.ts +2 -0
  145. package/src/cli/select.ts +35 -4
  146. package/src/cli/upgrade.ts +2 -2
  147. package/src/core/config.ts +6 -1
  148. package/src/core/defaults.ts +33 -11
  149. package/src/{interactive → core}/external-editor.ts +4 -4
  150. package/src/domains/config/classify.ts +1 -1
  151. package/src/domains/config/keybindings.ts +3 -31
  152. package/src/domains/providers/probe/fingerprint.ts +13 -6
  153. package/src/domains/providers/probe/reasoning.ts +2 -0
  154. package/src/domains/providers/runtimes/common/probe-helpers.ts +23 -7
  155. package/src/domains/providers/runtimes/protocol/anthropic-compat.ts +7 -2
  156. package/src/domains/providers/runtimes/protocol/openai-compat.ts +10 -4
  157. package/src/interactive/application-controller.ts +1 -31
  158. package/src/interactive/chat-panel.ts +163 -637
  159. package/src/interactive/chat-renderer.ts +13 -30
  160. package/src/interactive/dispatch-board.ts +1 -3
  161. package/src/interactive/editor-submit.ts +8 -31
  162. package/src/interactive/footer/dashboard.ts +8 -3
  163. package/src/interactive/footer/widgets.ts +50 -91
  164. package/src/interactive/interactive-application.ts +16 -42
  165. package/src/interactive/interactive-event-projection.ts +2 -44
  166. package/src/interactive/interactive-input-runtime.ts +3 -28
  167. package/src/interactive/interactive-presentation.ts +30 -9
  168. package/src/interactive/interactive-slash-runtime.ts +3 -40
  169. package/src/interactive/layout.ts +29 -4
  170. package/src/interactive/overlay-general-openers.ts +2 -0
  171. package/src/interactive/overlay-lifecycle.ts +1 -0
  172. package/src/interactive/overlay-session-lifecycle.ts +1 -1
  173. package/src/interactive/overlays/settings.ts +9 -8
  174. package/src/interactive/renderers/preview.ts +16 -0
  175. package/src/interactive/renderers/tool-execution.ts +67 -160
  176. package/src/interactive/renderers/worker-entry.ts +57 -41
  177. package/src/interactive/slash-autocomplete.ts +1 -6
  178. package/src/interactive/slash-commands.ts +7 -39
  179. package/src/interactive/status/controller.ts +0 -5
  180. package/src/interactive/status/state-machine.ts +44 -26
  181. package/src/interactive/status/types.ts +3 -0
  182. package/src/interactive/status/verbs.ts +13 -12
  183. package/src/interactive/transcript-detail.ts +50 -112
  184. package/src/interactive/view/artifacts.ts +4 -0
  185. package/src/interactive/view/view-overlay.ts +44 -34
  186. package/src/tools/presentation.ts +1 -1
  187. package/src/tools/registry.ts +1 -1
  188. package/src/tools/verify/numeric.ts +4 -4
@@ -1,3 +1,6 @@
1
+ import type { TranscriptDetailPolicy } from "../transcript-detail.js";
2
+ import { transcriptDetail } from "../transcript-detail.js";
3
+ import { previewBudget, previewRows } from "./preview.js";
1
4
  /**
2
5
  * Structured renderer for tool-execution chat segments (Slice A of the
3
6
  * pi-coding-agent parity work). pi-coding-agent renders every tool call as
@@ -48,10 +51,7 @@ const ARG_PREVIEW_LIMIT = 60;
48
51
  const WEB_FETCH_ARG_PREVIEW_LIMIT = 140;
49
52
  const FULL_RESULT_PREVIEW_LIMIT = 60_000;
50
53
  const FULL_RESULT_ROW_LIMIT = 120;
51
- const STREAMING_RESULT_ROW_LIMIT = 20;
52
54
  const ARGS_BODY_LINE_LIMIT = 24;
53
- /** Rows of mutation diff a folded edit/write row keeps visible before it defers to the body. */
54
- const FOLDED_DIFF_ROW_LIMIT = 24;
55
55
  const STATUS_OK_GLYPH = GLYPH.ok;
56
56
  const STATUS_ERROR_GLYPH = GLYPH.error;
57
57
 
@@ -111,6 +111,8 @@ export interface ToolBodyRenderOptions {
111
111
  unbounded?: boolean;
112
112
  /** Live rows color mutation diffs; replay and export deliberately use plain text. */
113
113
  diffStyle?: "color" | "plain";
114
+ detail?: TranscriptDetailPolicy;
115
+ terminalRows?: number;
114
116
  }
115
117
 
116
118
  /** Row cap for a tool body: unbounded lifts both the row and char limits. */
@@ -1110,30 +1112,6 @@ function renderResultBlock(
1110
1112
  return renderOutputRows(resultText(unwrapped, resultCharLimit(opts)), width, isError, resultRowLimit(opts));
1111
1113
  }
1112
1114
 
1113
- /**
1114
- * Header-only render for a tool call that has not yet finished. Used by the
1115
- * live chat panel from streamed argument formation through execution. The
1116
- * lifecycle tail distinguishes a forming call, a ready call, and a running
1117
- * call without inventing execution time before Pi starts the tool.
1118
- */
1119
- export function renderToolCallHeader(call: ToolExecutionStart, width: number): string[] {
1120
- return wrap(
1121
- headerLine(call.toolName, call.args, call.phase ?? "running", {
1122
- elapsedMs: call.elapsedMs,
1123
- }),
1124
- width,
1125
- );
1126
- }
1127
-
1128
- /** Running-only footer used when an operator pauses live output; execution continues. */
1129
- export function renderToolRunningStatus(call: ToolExecutionStart, width: number): string[] {
1130
- const elapsed = optionalCompactMs(call.elapsedMs);
1131
- return [
1132
- ...renderToolCallHeader(call, width),
1133
- ...indentAndWrap(dim(elapsed ? `live output paused · ${elapsed}` : "live output paused"), width, false),
1134
- ];
1135
- }
1136
-
1137
1115
  function sublineStatus(call: ToolExecutionStart | ToolExecutionFinished): HeaderStatus {
1138
1116
  // Discriminate on `result` rather than `isError`: only `ToolExecutionFinished`
1139
1117
  // carries a `result` field, so a `ToolExecutionStart` with a stray
@@ -1143,59 +1121,8 @@ function sublineStatus(call: ToolExecutionStart | ToolExecutionFinished): Header
1143
1121
  return call.isError ? "error" : "ok";
1144
1122
  }
1145
1123
 
1146
- /**
1147
- * One-line subline form of a tool call. Format:
1148
- * ▸ <body><status>
1149
- * `status` is "" for in-flight, " ✓" green for success, " ✗" red for error.
1150
- * Output is width-wrapped via wrapTextWithAnsi.
1151
- *
1152
- * When `expandKey` is supplied AND the call has finished, a dim ` (<key>)`
1153
- * discoverability hint rides at the very end of the atomic status tail so users
1154
- * see how to expand the collapsed block. Composing the full line (facts, status
1155
- * glyph, duration, offload, and hint) before wrapping keeps the status glyph
1156
- * and duration together on one row. The hint is suppressed for in-flight calls
1157
- * (still running, no useful body to expand yet) and when `expandKey` is
1158
- * empty/undefined (no key bound, hint would be misleading). The renderer never
1159
- * imports the keybindings manager directly; the caller resolves the key string
1160
- * and passes it in to keep this module pure.
1161
- */
1162
- export interface ToolSublineRenderOptions {
1163
- /** Live rows color the folded mutation diff; replay and export request plain rows. */
1164
- diffStyle?: "color" | "plain";
1165
- /**
1166
- * Whether the folded row carries the extras its tool's presentation asks
1167
- * for (today the bounded mutation diff). The bare subline level drops them;
1168
- * the failure excerpt is not one of them and always rides the row.
1169
- */
1170
- foldedExtras?: "per-tool" | "none";
1171
- }
1172
-
1173
- /**
1174
- * The bounded diff a folded mutation row keeps under itself, when the tool's
1175
- * presentation asks for one and the result carries one. A folded `edit` that
1176
- * hid its change told the operator nothing they could act on.
1177
- */
1178
- function foldedDiffRows(finished: ToolExecutionFinished, width: number, opts: ToolSublineRenderOptions): string[] {
1179
- if (opts.foldedExtras === "none") return [];
1180
- if (finished.isError || isNonExecutedOutcome(finished.outcome)) return [];
1181
- const presentation =
1182
- toolResultPresentationPolicy(finished.result) ?? toolPresentationPolicy(finished.toolName, finished.args);
1183
- if (!presentation.showDiffWhenFolded) return [];
1184
- const diff = resultDiff(finished.result);
1185
- if (diff === null) return [];
1186
- return truncateRowsMiddle(
1187
- renderMutationDiffBlock(diff, width, opts.diffStyle !== "plain"),
1188
- FOLDED_DIFF_ROW_LIMIT,
1189
- false,
1190
- );
1191
- }
1192
-
1193
- export function renderToolSubline(
1194
- call: ToolExecutionStart | ToolExecutionFinished,
1195
- width: number,
1196
- expandKey?: string,
1197
- opts: ToolSublineRenderOptions = {},
1198
- ): string[] {
1124
+ /** Stable action identity, outcome, and captured-result metadata. */
1125
+ export function renderToolSubline(call: ToolExecutionStart | ToolExecutionFinished, width: number): string[] {
1199
1126
  const status = sublineStatus(call);
1200
1127
  const meta: StatusMeta =
1201
1128
  "result" in call
@@ -1208,11 +1135,7 @@ export function renderToolSubline(
1208
1135
  : { elapsedMs: call.elapsedMs };
1209
1136
  const parts = sublineParts(call, status, meta);
1210
1137
  const excerpt = "result" in call ? failureExcerpt(call, width) : "";
1211
- const showHint = "result" in call && expandKey !== undefined && expandKey.length > 0;
1212
- const tail = showHint ? `${parts.tail}${dim(` (${expandKey})`)}` : parts.tail;
1213
- const lines = wrapSublineWithTail(`${parts.lead}${excerpt}`, tail, width);
1214
- if ("result" in call) lines.push(...foldedDiffRows(call, width, opts));
1215
- return lines;
1138
+ return wrapSublineWithTail(`${parts.lead}${excerpt}`, parts.tail, width);
1216
1139
  }
1217
1140
 
1218
1141
  /**
@@ -1300,6 +1223,7 @@ export function renderToolResultOnly(
1300
1223
  width: number,
1301
1224
  opts: ToolBodyRenderOptions = {},
1302
1225
  ): string[] {
1226
+ if (!opts.unbounded && opts.detail) return renderToolPreview(finished, width, opts.detail, opts);
1303
1227
  const status: HeaderStatus = finished.isError ? "error" : "ok";
1304
1228
  const statusMeta: StatusMeta = {
1305
1229
  durationMs: finished.durationMs,
@@ -1315,50 +1239,6 @@ export function renderToolResultOnly(
1315
1239
  return out;
1316
1240
  }
1317
1241
 
1318
- /**
1319
- * Streaming render for an in-flight tool call whose expanded block should
1320
- * surface the latest partial output. Used by the chat panel between
1321
- * `tool_execution_start` and `tool_execution_end` when the user has expanded
1322
- * the tool segment. The header carries the running lifecycle tail, secondary
1323
- * arguments remain visible, and the latest cumulative Pi result renders under
1324
- * a labeled live-output rail capped at the streaming row limit. An empty result renders
1325
- * `(no output yet)` so the user can distinguish a slow start from a stalled
1326
- * call.
1327
- */
1328
- export function renderToolStreamingExecution(
1329
- call: ToolExecutionStart,
1330
- width: number,
1331
- partialResult: unknown,
1332
- ): string[] {
1333
- const out: string[] = [];
1334
- out.push(...renderToolCallHeader({ ...call, phase: "running" }, width));
1335
- out.push(...renderArgsBody(call.toolName, call.args, width, false));
1336
- const partial: ToolExecutionFinished = {
1337
- toolCallId: call.toolCallId,
1338
- toolName: call.toolName,
1339
- args: call.args,
1340
- result: partialResult,
1341
- isError: false,
1342
- };
1343
- out.push(...renderOutputMeta(partial, width, false, "live output"));
1344
- const partialOutput = unwrapResultEnvelope(partialResult);
1345
- if (isEmptyResult(partialOutput)) {
1346
- out.push(...indentAndWrap(dim("(no output yet)"), width, false));
1347
- } else {
1348
- if (call.toolName === "bash") {
1349
- const bashArgs = asBashArgs(redactToolArgs(call.args));
1350
- if (bashArgs !== null) {
1351
- const commandLine = `${cyanBold("$")} ${highlightBashCommand(stripShellWrapperForDisplay(bashArgs.command))}`;
1352
- out.push(...indentAndWrap(commandLine, width, false));
1353
- }
1354
- }
1355
- out.push(
1356
- ...renderOutputRows(resultText(partialOutput, FULL_RESULT_PREVIEW_LIMIT), width, false, STREAMING_RESULT_ROW_LIMIT),
1357
- );
1358
- }
1359
- return out;
1360
- }
1361
-
1362
1242
  /** Display-only lifecycle for operator-issued `!` and `!!` bash commands. */
1363
1243
  export interface BashTranscriptExecution {
1364
1244
  command: string;
@@ -1372,13 +1252,6 @@ export interface BashTranscriptExecution {
1372
1252
  fullOutputPath?: string | undefined;
1373
1253
  excludeFromContext?: boolean | undefined;
1374
1254
  error?: string | undefined;
1375
- /**
1376
- * Whether the block draws its one-line row instead of the full body. The
1377
- * caller resolves this from the transcript detail policy and the operator's
1378
- * override, the same way the panel resolves a model bash call. Omitted means
1379
- * folded.
1380
- */
1381
- folded?: boolean | undefined;
1382
1255
  }
1383
1256
 
1384
1257
  /**
@@ -1389,7 +1262,7 @@ export interface BashTranscriptExecution {
1389
1262
  export function renderBashTranscriptExecution(
1390
1263
  execution: BashTranscriptExecution,
1391
1264
  width: number,
1392
- expandKey?: string,
1265
+ _expandKey?: string,
1393
1266
  bodyOptions: ToolBodyRenderOptions = {},
1394
1267
  ): string[] {
1395
1268
  const shownBytes = Buffer.byteLength(execution.output, "utf8");
@@ -1409,30 +1282,12 @@ export function renderBashTranscriptExecution(
1409
1282
  content: [{ type: "text", text: execution.output }],
1410
1283
  details,
1411
1284
  };
1412
- const folded = execution.folded !== false;
1413
1285
  if (execution.running) {
1414
- if (folded) {
1415
- return renderToolSubline(
1416
- {
1417
- toolCallId: "local-bash",
1418
- toolName: "bash",
1419
- args,
1420
- elapsedMs: execution.elapsedMs,
1421
- phase: "running",
1422
- },
1423
- width,
1424
- );
1425
- }
1426
- return renderToolStreamingExecution(
1427
- {
1428
- toolCallId: "local-bash",
1429
- toolName: "bash",
1430
- args,
1431
- elapsedMs: execution.elapsedMs,
1432
- phase: "running",
1433
- },
1286
+ return renderToolPreview(
1287
+ { toolCallId: "local-bash", toolName: "bash", args, phase: "running", elapsedMs: execution.elapsedMs },
1434
1288
  width,
1435
- result,
1289
+ bodyOptions.detail ?? transcriptDetail(),
1290
+ { ...bodyOptions, operator: true, partialResult: result },
1436
1291
  );
1437
1292
  }
1438
1293
  const message = execution.error?.trim();
@@ -1454,5 +1309,57 @@ export function renderBashTranscriptExecution(
1454
1309
  ...(execution.fullOutputPath !== undefined ? { offloadPath: execution.fullOutputPath } : {}),
1455
1310
  },
1456
1311
  };
1457
- return folded ? renderToolSubline(finished, width, expandKey) : renderToolExecution(finished, width, bodyOptions);
1312
+ return bodyOptions.unbounded
1313
+ ? renderToolExecution(finished, width, bodyOptions)
1314
+ : renderToolPreview(finished, width, bodyOptions.detail ?? transcriptDetail(), { ...bodyOptions, operator: true });
1315
+ }
1316
+
1317
+ /** One action identity plus a bounded result; arguments and full output belong in /view. */
1318
+ export function renderToolPreview(
1319
+ call: ToolExecutionStart | ToolExecutionFinished,
1320
+ width: number,
1321
+ detail: TranscriptDetailPolicy,
1322
+ options: ToolBodyRenderOptions & { terminalRows?: number; partialResult?: unknown; operator?: boolean } = {},
1323
+ ): string[] {
1324
+ const finished = "result" in call ? call : undefined;
1325
+ const failure = finished?.isError === true || finished?.outcome !== undefined;
1326
+ const limit = previewBudget(
1327
+ failure
1328
+ ? detail.errorRows
1329
+ : options.operator
1330
+ ? detail.operatorBashRows
1331
+ : call.toolName === "bash"
1332
+ ? detail.bashRows
1333
+ : detail.resultRows,
1334
+ options.terminalRows,
1335
+ );
1336
+ const rows = renderToolSubline(call, width);
1337
+ const result = finished?.result ?? options.partialResult;
1338
+ const diff = finished && !failure ? resultDiff(result) : null;
1339
+ if (diff !== null && detail.diffRows > 0) {
1340
+ rows.push(
1341
+ ...previewRows(
1342
+ renderMutationDiffBlock(redactSecretString(diff), width, options.diffStyle !== "plain"),
1343
+ previewBudget(detail.diffRows, options.terminalRows),
1344
+ width,
1345
+ ),
1346
+ );
1347
+ } else if (limit > 0 && result !== undefined) {
1348
+ const text = resultText(unwrapResultEnvelope(result), Number.POSITIVE_INFINITY);
1349
+ const body = indentAndWrap(redactSecretString(text), width, failure);
1350
+ rows.push(...previewRows(body, limit, width, call.toolName === "bash" || !finished));
1351
+ }
1352
+ return rows;
1353
+ }
1354
+
1355
+ /** Only ordinary successful observations can lose their individual metadata rows. */
1356
+ export function canGroupObservation(call: ToolExecutionFinished): boolean {
1357
+ return (
1358
+ ["read", "grep", "find", "ls"].includes(call.toolName) &&
1359
+ !call.isError &&
1360
+ !call.outcome &&
1361
+ !call.evictedReason &&
1362
+ !isTruncatedResult(call) &&
1363
+ offloadPathOf(call) === null
1364
+ );
1458
1365
  }
@@ -1,27 +1,8 @@
1
- /**
2
- * Renderer for a worker's transcript block: the attributed stream a `/run`,
3
- * `/delegate`, or model-driven dispatch produces.
4
- *
5
- * One shape serves every runtime. A local Clio worker, a Claude subprocess, and
6
- * a delegated ACP peer differ only in what the header can name about their
7
- * route, so the body, the tool line, and the receipt footer are identical and
8
- * an operator learns one grammar. The origin glyph is the only thing that says
9
- * who asked, which is the same rule the board and the footer chip follow.
10
- *
11
- * ◇ coder · node-a/example-coder-model · run 2mkas6s
12
- * │ Hello! I'm the coder worker.
13
- * │ ⚙ read · artifact
14
- * └ ✓ execution ok · 4.8k tok · 9.6s · contract pass
15
- * │ quality: grounded
16
- *
17
- * Folded (the default for a run the model asked for) keeps identity and
18
- * execution on a tool-style subline, followed by the settled quality fact:
19
- *
20
- * ◆ scout · node-b/example-scout-model · run 3nc18jo ✓ execution ok · 41s (Ctrl+O)
21
- * │ quality: validation failed
22
- *
23
- * Pure: no I/O, no module-level mutable state beyond the shared theme handle.
24
- */
1
+ import { redactSecretString } from "../../domains/safety/redaction.js";
2
+ import type { TranscriptDetailPolicy } from "../transcript-detail.js";
3
+ import { transcriptDetail } from "../transcript-detail.js";
4
+ import { previewBudget, previewRows } from "./preview.js";
5
+ /** Bounded worker summaries share the main transcript's output style. */
25
6
 
26
7
  import { parseJsonObjectPayload } from "../../core/json-payload.js";
27
8
  import { trustStateWord } from "../../domains/evidence/trust-projection.js";
@@ -48,10 +29,8 @@ const SEPARATOR = " · ";
48
29
  const BODY_LINE_LIMIT = 80;
49
30
 
50
31
  export interface WorkerEntryRenderOptions {
51
- /** Collapsed to the header line plus outcome. Default for a run the model asked for. */
52
- folded: boolean;
53
- /** Key hint appended to a folded header, e.g. "Ctrl+O". Omitted when unknown. */
54
- expandKey?: string | undefined;
32
+ detail?: TranscriptDetailPolicy;
33
+ terminalRows?: number;
55
34
  /** Render the full body without the line cap; `/export` sets this. */
56
35
  unbounded?: boolean;
57
36
  }
@@ -392,15 +371,14 @@ function footerLine(entry: WorkerEntryState, width: number): string {
392
371
  * renders only when the caller resolved a key binding for it, so a rebound or
393
372
  * unbound key never advertises a wrong chord.
394
373
  */
395
- function foldedLine(entry: WorkerEntryState, width: number, expandKey: string | undefined): string {
374
+ function actionLine(entry: WorkerEntryState, width: number): string {
396
375
  const identity = `${originGlyph(entry)} ${identityUnits(entry).join(dim(SEPARATOR))}`;
397
376
  const status =
398
377
  isPending(entry) || entry.receipt === undefined ? pendingUnit(entry) : outcomeUnit(entry.receipt, false);
399
378
  const elapsed =
400
379
  entry.receipt?.durationMs === undefined ? "" : dim(`${SEPARATOR}${formatCompactMs(entry.receipt.durationMs)}`);
401
- const hint = expandKey === undefined || expandKey.length === 0 ? "" : dim(` (${expandKey})`);
402
- const full = ` ${status}${elapsed}${hint}`;
403
- let tail = visibleWidth(identity) + visibleWidth(full) <= width ? full : ` ${status}${hint}`;
380
+ const full = ` ${status}${elapsed}`;
381
+ let tail = visibleWidth(identity) + visibleWidth(full) <= width ? full : ` ${status}`;
404
382
  // Reserve one identity cell. The optional hint yields before execution
405
383
  // status when the suffix alone would exhaust the header's width.
406
384
  if (visibleWidth(tail) >= width) tail = ` ${status}`;
@@ -421,15 +399,53 @@ export function renderWorkerEntryLines(
421
399
  "muted",
422
400
  safeWidth,
423
401
  );
424
- if (options.folded) return [foldedLine(entry, safeWidth, options.expandKey), ...quality];
425
- const tools = toolLine(entry, safeWidth);
402
+ const detail = options.detail ?? transcriptDetail();
403
+ if (options.unbounded) {
404
+ const tools = toolLine(entry, safeWidth);
405
+ return [
406
+ headerLine(entry, safeWidth),
407
+ ...bodyLines(entry, safeWidth, true),
408
+ ...attemptLines(entry, safeWidth),
409
+ ...(tools ? [tools] : []),
410
+ ...failureLines(entry, safeWidth),
411
+ footerLine(entry, safeWidth),
412
+ ...quality,
413
+ ].map(redactSecretString);
414
+ }
415
+ const budget = (limit: number) => previewBudget(limit, options.terminalRows);
416
+ const summary = bodySourceLines(entry).flatMap((line) => railLines(redactSecretString(line), "muted", safeWidth));
417
+ const actions = entry.progress
418
+ ? [...entry.progress.recentActions]
419
+ .reverse()
420
+ .concat(entry.progress.currentAction ? [entry.progress.currentAction] : [])
421
+ : [];
422
+ const trail = actions.flatMap((action) =>
423
+ railLines(
424
+ `${GLYPH.phaseTool} ${action.descriptor ? `${action.descriptor.verb} ${action.descriptor.object}` : action.tool}`,
425
+ "muted",
426
+ safeWidth,
427
+ ),
428
+ );
429
+ const tools = detail.workerActivity && trail.length === 0 ? toolLine(entry, safeWidth) : null;
430
+ const failure = previewRows(failureLines(entry, safeWidth), budget(detail.errorRows), safeWidth);
431
+ const presented = presentedContractAnswer(entry)?.footer;
426
432
  return [
427
- headerLine(entry, safeWidth),
428
- ...bodyLines(entry, safeWidth, options.unbounded === true),
429
- ...attemptLines(entry, safeWidth),
430
- ...(tools === null ? [] : [tools]),
431
- ...failureLines(entry, safeWidth),
432
- footerLine(entry, safeWidth),
433
+ actionLine(entry, safeWidth),
434
+ ...(detail.workerRows > 0 ? previewRows(summary, budget(detail.workerRows), safeWidth, entry.pending) : []),
435
+ ...previewRows(attemptLines(entry, safeWidth), budget(detail.errorRows), safeWidth, true),
436
+ ...(tools ? [tools] : []),
437
+ ...(detail.workerActivity ? previewRows(trail, budget(4), safeWidth, true) : []),
438
+ ...failure,
439
+ ...(presented ? railLines(presented, "muted", safeWidth) : []),
440
+ ...(entry.receipt?.abandonedDetail ? railLines(entry.receipt.abandonedDetail, "warning", safeWidth) : []),
441
+ ...(entry.receipt?.receiptUnavailable ? railLines("receipt unavailable", "warning", safeWidth) : []),
442
+ ...(entry.droppedLines
443
+ ? railLines(
444
+ `… ${entry.droppedLines} earlier lines unavailable here · /view dispatch:${entry.runId}`,
445
+ "muted",
446
+ safeWidth,
447
+ )
448
+ : []),
433
449
  ...quality,
434
- ];
450
+ ].map(redactSecretString);
435
451
  }
@@ -154,12 +154,7 @@ function completionGrammar(command: string, args: CommandArgsSpec): CommandArgsS
154
154
  ? {
155
155
  positionals: spec.positionals.map((pos) => {
156
156
  const completionSlot = COMPLETION_SLOT_MANIFEST[`${path}:pos:${pos.name}`];
157
- const values =
158
- command === "output" && pos.name === "verbosity"
159
- ? (["minimal", "default", "verbose"] as const)
160
- : command === "settings" && pos.name === "area"
161
- ? SETTINGS_AREA_IDS
162
- : undefined;
157
+ const values = command === "settings" && pos.name === "area" ? SETTINGS_AREA_IDS : undefined;
163
158
  return { ...pos, ...(completionSlot ? { completionSlot } : {}), ...(values ? { values } : {}) };
164
159
  }),
165
160
  }
@@ -174,8 +174,6 @@ type SlashCommandVariant =
174
174
  | { kind: "files-usage"; reason: string }
175
175
  | { kind: "thinking-set"; level: string }
176
176
  | { kind: "thinking-picker" }
177
- | { kind: "output-set"; verbosity: string }
178
- | { kind: "output-picker" }
179
177
  | { kind: "model" }
180
178
  | { kind: "model-set"; pattern: string }
181
179
  | { kind: "settings"; area?: SettingsAreaId; group?: string }
@@ -207,11 +205,6 @@ export type SetThinkingLevelResult =
207
205
  | { status: "unsupported"; level: string; supported: ReadonlyArray<string> }
208
206
  | { status: "unavailable" };
209
207
 
210
- export type SetOutputVerbosityResult =
211
- | { status: "applied"; verbosity: string }
212
- | { status: "unsupported"; verbosity: string; supported: ReadonlyArray<string> }
213
- | { status: "unavailable" };
214
-
215
208
  /**
216
209
  * What one admitted dispatch-tool call reports back to `/council`. The council
217
210
  * itself is watched on the board and in the transcript, so success is silent;
@@ -728,8 +721,6 @@ export interface SlashCommandContext {
728
721
  thinkingLevelChoices?: () => ReadonlyArray<{ value: string; label: string }>;
729
722
  openThinkingPicker?: () => void;
730
723
  /** Apply a transcript verbosity named on the command line; refused values are reported by the caller. */
731
- setOutputVerbosity?: (verbosity: string) => SetOutputVerbosityResult;
732
- openOutputPicker?: () => void;
733
724
  openModel: () => void;
734
725
  /** Live providers contract used by `/model <pattern>` to resolve directly. */
735
726
  providers: ProvidersContract;
@@ -1917,35 +1908,6 @@ export const BUILTIN_SLASH_COMMANDS: ReadonlyArray<BuiltinSlashCommand> = [
1917
1908
  ctx.render();
1918
1909
  },
1919
1910
  },
1920
- {
1921
- name: "output",
1922
- description: "Set transcript detail to minimal, default, or verbose",
1923
- group: "Configure",
1924
- kinds: ["output-set", "output-picker"],
1925
- args: { positionals: [{ name: "verbosity", required: false }] },
1926
- fromArgs(parsed) {
1927
- if (parsed.error) return { kind: "usage-error", command: "output", reason: parsed.error };
1928
- const verbosity = parsed.positionals[0];
1929
- return verbosity ? { kind: "output-set", verbosity } : { kind: "output-picker" };
1930
- },
1931
- handle(command, ctx) {
1932
- if (command.kind === "output-picker") {
1933
- if (ctx.openOutputPicker) ctx.openOutputPicker();
1934
- else ctx.notice("error", "output picker is not wired in this session");
1935
- return;
1936
- }
1937
- if (command.kind !== "output-set") return;
1938
- const result = ctx.setOutputVerbosity?.(command.verbosity) ?? { status: "unavailable" as const };
1939
- if (result.status === "applied") {
1940
- ctx.notice("success", `output detail: ${result.verbosity}`);
1941
- } else if (result.status === "unsupported") {
1942
- ctx.notice("error", `output detail "${result.verbosity}" is not one of ${result.supported.join(", ")}`);
1943
- } else {
1944
- ctx.notice("error", "output detail cannot be set right now");
1945
- }
1946
- ctx.render();
1947
- },
1948
- },
1949
1911
  {
1950
1912
  name: "model",
1951
1913
  description: "Open model selector or set a model",
@@ -2199,6 +2161,13 @@ function reloadExtensionsCommand(ctx: SlashCommandContext): void {
2199
2161
  export function parseSlashCommand(input: string): SlashCommand {
2200
2162
  const trimmed = input.trim();
2201
2163
  if (trimmed.length === 0) return { kind: "empty" };
2164
+ if (/^\/output(?:\s|$)/u.test(trimmed)) {
2165
+ return {
2166
+ kind: "usage-error",
2167
+ command: "output",
2168
+ reason: "Use Alt+O to cycle Output style, or /settings interface to save a default. Use /view for full details",
2169
+ };
2170
+ }
2202
2171
  if (trimmed.startsWith(COMMAND_ESCAPE)) return { kind: "unknown", text: trimmed.slice(1) };
2203
2172
  for (const entry of BUILTIN_SLASH_COMMANDS) {
2204
2173
  const match = entry.match ? entry.match(trimmed) : matchFromSpec(entry, trimmed);
@@ -2286,7 +2255,6 @@ const COMMAND_ORDER = [
2286
2255
  "help",
2287
2256
  "model",
2288
2257
  "thinking",
2289
- "output",
2290
2258
  "settings",
2291
2259
  "new",
2292
2260
  "resume",
@@ -206,9 +206,6 @@ export function createStatusController(deps: StatusControllerDeps): StatusContro
206
206
  deps.bus.on(BusChannels.PermissionResolved, () => apply({ type: "overlay_pop", overlay: "tool_blocked" }, true)),
207
207
  deps.bus.on(BusChannels.CompactionBegin, () => apply({ type: "overlay_push", overlay: "compacting" }, true)),
208
208
  deps.bus.on(BusChannels.CompactionEnd, () => apply({ type: "overlay_pop", overlay: "compacting" }, true)),
209
- deps.bus.on(BusChannels.DispatchStarted, (payload) =>
210
- apply({ type: "overlay_push", overlay: "dispatching", data: payload }, true),
211
- ),
212
209
  deps.bus.on(BusChannels.DispatchProgress, (payload) => {
213
210
  if (!isWorkerStreamEvidence(payload)) return;
214
211
  const at = now();
@@ -216,8 +213,6 @@ export function createStatusController(deps: StatusControllerDeps): StatusContro
216
213
  lastDispatchProgressAt = at;
217
214
  apply({ type: "dispatch_progress" });
218
215
  }),
219
- deps.bus.on(BusChannels.DispatchCompleted, () => apply({ type: "overlay_pop", overlay: "dispatching" }, true)),
220
- deps.bus.on(BusChannels.DispatchFailed, () => apply({ type: "overlay_pop", overlay: "dispatching" }, true)),
221
216
  deps.bus.on(BusChannels.RunAborted, (payload) => {
222
217
  scheduleAbortCeiling();
223
218
  // Carry abort provenance into the summary; a dispatch drain and a
@@ -245,6 +245,7 @@ function activePhaseAfterStuck(prev: AgentStatus): StatusPhase {
245
245
  }
246
246
 
247
247
  function isAwaitingDispatch(prev: AgentStatus): boolean {
248
+ if (activePhaseAfterStuck(prev) === "tool_running" && prev.tool?.toolName === ToolNames.Dispatch) return true;
248
249
  if (prev.phase === "dispatching") return true;
249
250
  if (prev.activePhases?.has("dispatching")) return true;
250
251
  return (prev.overlayStack ?? []).some((frame) => frame.phase === "dispatching");
@@ -360,26 +361,9 @@ export function reduceStatus(prev: AgentStatus, event: StatusInputEvent, ctx: Re
360
361
  if (base === "preparing") return { ...next, phase: "waiting_model", resumePhase: undefined };
361
362
  return next;
362
363
  }
363
- case "message_start": {
364
- const next = refreshMeaningful(prev, ctx);
365
- const role = (event.message as { role?: unknown }).role;
366
- if (role === "assistant" && prev.phase === "tool_running") return { ...next, phase: "writing" };
367
- if (
368
- role === "assistant" &&
369
- (prev.phase === "preparing" || prev.phase === "waiting_model" || prev.phase === "thinking")
370
- )
371
- return { ...next, phase: "writing", resumePhase: undefined };
372
- return next;
373
- }
374
- case "message_update": {
375
- const base = activePhaseAfterStuck(prev);
376
- const next = refreshMeaningful({ ...prev, phase: base }, ctx);
377
- if (base === "preparing" || base === "waiting_model" || base === "thinking") {
378
- return { ...next, phase: "writing", resumePhase: undefined };
379
- }
380
- if (CORE_ACTIVE_PHASES.has(base)) return { ...next, resumePhase: undefined };
381
- return next;
382
- }
364
+ case "message_start":
365
+ case "message_update":
366
+ return refreshMeaningful(prev, ctx);
383
367
  case "tool_execution_update":
384
368
  return refreshMeaningful(prev, ctx);
385
369
  case "message_end":
@@ -390,10 +374,22 @@ export function reduceStatus(prev: AgentStatus, event: StatusInputEvent, ctx: Re
390
374
  runTally: foldMessageIntoRunTally(prev.runTally ?? emptyRunTally(), event.message),
391
375
  };
392
376
  case "tool_execution_end": {
393
- const next = refreshMeaningful(prev, ctx);
394
- if (prev.phase === "tool_running") return { ...next, phase: "preparing", tool: undefined, toolStartedAt: undefined };
377
+ const activeTools = (prev.activeTools ?? []).filter((tool) => tool.toolCallId !== event.toolCallId);
378
+ const next = { ...refreshMeaningful(prev, ctx), activeTools };
379
+ const remaining = activeTools.at(-1);
380
+ if (remaining && prev.phase === "tool_running")
381
+ return { ...next, tool: remaining, toolStartedAt: remaining.startedAt };
382
+ if (prev.phase === "tool_running")
383
+ return { ...next, phase: "preparing", tool: undefined, toolStartedAt: undefined, activeTools: [] };
395
384
  if (prev.phase === "stuck" && prev.resumePhase === "tool_running") {
396
- return { ...next, phase: "preparing", resumePhase: undefined, tool: undefined, toolStartedAt: undefined };
385
+ return {
386
+ ...next,
387
+ phase: "preparing",
388
+ resumePhase: undefined,
389
+ tool: undefined,
390
+ toolStartedAt: undefined,
391
+ activeTools: [],
392
+ };
397
393
  }
398
394
  // A blocked call's end can land while an overlay is still visible
399
395
  // (permission prompt not yet resolved, retry or dispatch in front).
@@ -422,7 +418,14 @@ export function reduceStatus(prev: AgentStatus, event: StatusInputEvent, ctx: Re
422
418
  // tool is done (its end never landed or was an admission block): the
423
419
  // model is generating, nothing is executing, so drop the tool display.
424
420
  if (base === "preparing" || base === "waiting_model" || base === "writing" || base === "tool_running") {
425
- return { ...next, phase: "thinking", resumePhase: undefined, tool: undefined, toolStartedAt: undefined };
421
+ return {
422
+ ...next,
423
+ phase: "thinking",
424
+ resumePhase: undefined,
425
+ tool: undefined,
426
+ toolStartedAt: undefined,
427
+ activeTools: [],
428
+ };
426
429
  }
427
430
  if (CORE_ACTIVE_PHASES.has(base)) return { ...next, resumePhase: undefined };
428
431
  return next;
@@ -433,19 +436,34 @@ export function reduceStatus(prev: AgentStatus, event: StatusInputEvent, ctx: Re
433
436
  // Streamed answer text while a tool still shows as running: same as
434
437
  // above, the model is writing, so the running-tool spinner must clear.
435
438
  if (base === "preparing" || base === "waiting_model" || base === "thinking" || base === "tool_running") {
436
- return { ...next, phase: "writing", resumePhase: undefined, tool: undefined, toolStartedAt: undefined };
439
+ return {
440
+ ...next,
441
+ phase: "writing",
442
+ resumePhase: undefined,
443
+ tool: undefined,
444
+ toolStartedAt: undefined,
445
+ activeTools: [],
446
+ };
437
447
  }
438
448
  if (CORE_ACTIVE_PHASES.has(base)) return { ...next, resumePhase: undefined };
439
449
  return next;
440
450
  }
441
451
  case "tool_execution_start": {
442
452
  const tool: ToolOverlay = {
453
+ toolCallId: event.toolCallId,
454
+ startedAt: ctx.now,
443
455
  toolName: event.toolName,
444
456
  toolPreview: compactPreview(event.args),
445
457
  };
446
458
  // Stamp this call's own start so the footer's running-tool timer counts
447
459
  // from here, not from turn start.
448
- return { ...refreshMeaningful(prev, ctx), phase: "tool_running", tool, toolStartedAt: ctx.now };
460
+ return {
461
+ ...refreshMeaningful(prev, ctx),
462
+ phase: "tool_running",
463
+ tool,
464
+ toolStartedAt: ctx.now,
465
+ activeTools: [...(prev.activeTools ?? []).filter((active) => active.toolCallId !== event.toolCallId), tool],
466
+ };
449
467
  }
450
468
  case "retry_status": {
451
469
  const phase = event.status.phase;