pi-agent-flow 2.0.2 → 2.0.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 (192) hide show
  1. package/README.md +1 -1
  2. package/agents/audit.md +24 -17
  3. package/agents/build.md +1 -1
  4. package/agents/craft.md +1 -1
  5. package/agents/debug.md +1 -1
  6. package/agents/ideas.md +1 -1
  7. package/agents/scout.md +8 -8
  8. package/agents/trace.md +23 -0
  9. package/dist/batch/batch-bash.d.ts +10 -0
  10. package/dist/batch/batch-bash.d.ts.map +1 -1
  11. package/dist/batch/batch-bash.js +35 -2
  12. package/dist/batch/batch-bash.js.map +1 -1
  13. package/dist/batch/constants.d.ts +6 -3
  14. package/dist/batch/constants.d.ts.map +1 -1
  15. package/dist/batch/execute.js +2 -2
  16. package/dist/batch/execute.js.map +1 -1
  17. package/dist/batch/index.d.ts +9 -11
  18. package/dist/batch/index.d.ts.map +1 -1
  19. package/dist/batch/index.js +88 -58
  20. package/dist/batch/index.js.map +1 -1
  21. package/dist/batch/render.d.ts +22 -5
  22. package/dist/batch/render.d.ts.map +1 -1
  23. package/dist/batch/render.js +332 -17
  24. package/dist/batch/render.js.map +1 -1
  25. package/dist/batch/summary.d.ts.map +1 -1
  26. package/dist/batch/summary.js +22 -4
  27. package/dist/batch/summary.js.map +1 -1
  28. package/dist/config/config.d.ts +5 -4
  29. package/dist/config/config.d.ts.map +1 -1
  30. package/dist/config/config.js +15 -5
  31. package/dist/config/config.js.map +1 -1
  32. package/dist/config/models.d.ts.map +1 -1
  33. package/dist/config/models.js +7 -1
  34. package/dist/config/models.js.map +1 -1
  35. package/dist/config/settings-resolver.d.ts +5 -4
  36. package/dist/config/settings-resolver.d.ts.map +1 -1
  37. package/dist/config/settings-resolver.js +50 -21
  38. package/dist/config/settings-resolver.js.map +1 -1
  39. package/dist/core2/snapshot.d.ts +21 -0
  40. package/dist/core2/snapshot.d.ts.map +1 -0
  41. package/dist/core2/snapshot.js +214 -0
  42. package/dist/core2/snapshot.js.map +1 -0
  43. package/dist/{core → flow}/agents.d.ts.map +1 -1
  44. package/dist/{core → flow}/agents.js +5 -2
  45. package/dist/{core → flow}/agents.js.map +1 -1
  46. package/dist/flow/command.d.ts +1 -1
  47. package/dist/flow/command.d.ts.map +1 -1
  48. package/dist/flow/complexity.d.ts +20 -0
  49. package/dist/flow/complexity.d.ts.map +1 -0
  50. package/dist/flow/complexity.js +34 -0
  51. package/dist/flow/complexity.js.map +1 -0
  52. package/dist/flow/continuation.d.ts +1 -1
  53. package/dist/flow/continuation.d.ts.map +1 -1
  54. package/dist/flow/continuation.js +2 -1
  55. package/dist/flow/continuation.js.map +1 -1
  56. package/dist/{core → flow}/depth.d.ts +1 -1
  57. package/dist/{core → flow}/depth.d.ts.map +1 -1
  58. package/dist/{core → flow}/depth.js.map +1 -1
  59. package/dist/{core → flow}/executor.d.ts +39 -19
  60. package/dist/flow/executor.d.ts.map +1 -0
  61. package/dist/flow/executor.js +727 -0
  62. package/dist/flow/executor.js.map +1 -0
  63. package/dist/flow/index.d.ts +2 -2
  64. package/dist/flow/index.d.ts.map +1 -1
  65. package/dist/flow/index.js +1 -1
  66. package/dist/flow/index.js.map +1 -1
  67. package/dist/flow/loop-command.d.ts +1 -1
  68. package/dist/flow/loop-command.d.ts.map +1 -1
  69. package/dist/{core/flow.d.ts → flow/runner.d.ts} +10 -13
  70. package/dist/flow/runner.d.ts.map +1 -0
  71. package/dist/{core/flow.js → flow/runner.js} +35 -34
  72. package/dist/flow/runner.js.map +1 -0
  73. package/dist/{core → flow}/session-registry.d.ts.map +1 -1
  74. package/dist/{core → flow}/session-registry.js.map +1 -1
  75. package/dist/flow/settings-command.d.ts +3 -3
  76. package/dist/flow/settings-command.d.ts.map +1 -1
  77. package/dist/flow/settings-command.js +39 -21
  78. package/dist/flow/settings-command.js.map +1 -1
  79. package/dist/{core → flow}/transition.d.ts.map +1 -1
  80. package/dist/{core → flow}/transition.js.map +1 -1
  81. package/dist/flow/types.d.ts +4 -0
  82. package/dist/flow/types.d.ts.map +1 -1
  83. package/dist/flow/warp.d.ts +1 -1
  84. package/dist/flow/warp.d.ts.map +1 -1
  85. package/dist/index.d.ts +1 -2
  86. package/dist/index.d.ts.map +1 -1
  87. package/dist/index.js +190 -189
  88. package/dist/index.js.map +1 -1
  89. package/dist/notify/notify.d.ts +1 -1
  90. package/dist/notify/notify.d.ts.map +1 -1
  91. package/dist/notify/notify.js +1 -1
  92. package/dist/snapshot/cli-args.d.ts +2 -2
  93. package/dist/snapshot/cli-args.d.ts.map +1 -1
  94. package/dist/snapshot/cli-args.js +21 -5
  95. package/dist/snapshot/cli-args.js.map +1 -1
  96. package/dist/snapshot/runner-events.d.ts +2 -2
  97. package/dist/snapshot/runner-events.d.ts.map +1 -1
  98. package/dist/snapshot/runner-events.js +48 -4
  99. package/dist/snapshot/runner-events.js.map +1 -1
  100. package/dist/snapshot/structured-output.d.ts +1 -1
  101. package/dist/snapshot/structured-output.d.ts.map +1 -1
  102. package/dist/snapshot/structured-output.js +20 -2
  103. package/dist/snapshot/structured-output.js.map +1 -1
  104. package/dist/steering/flow-prompt.d.ts +1 -3
  105. package/dist/steering/flow-prompt.d.ts.map +1 -1
  106. package/dist/steering/flow-prompt.js +7 -63
  107. package/dist/steering/flow-prompt.js.map +1 -1
  108. package/dist/steering/sliding-prompt.js +10 -10
  109. package/dist/steering/sliding-prompt.js.map +1 -1
  110. package/dist/tools/ask-user.d.ts +2 -2
  111. package/dist/tools/ask-user.d.ts.map +1 -1
  112. package/dist/tools/ask-user.js +1 -1
  113. package/dist/tools/ask-user.js.map +1 -1
  114. package/dist/tools/timed-bash.js +1 -1
  115. package/dist/tools/timed-bash.js.map +1 -1
  116. package/dist/tools/trace.d.ts +34 -0
  117. package/dist/tools/trace.d.ts.map +1 -0
  118. package/dist/tools/trace.js +180 -0
  119. package/dist/tools/trace.js.map +1 -0
  120. package/dist/tools/web-ops.d.ts +85 -0
  121. package/dist/tools/web-ops.d.ts.map +1 -0
  122. package/dist/tools/{web-tool.js → web-ops.js} +51 -125
  123. package/dist/tools/web-ops.js.map +1 -0
  124. package/dist/tui/flow-colors.d.ts +1 -0
  125. package/dist/tui/flow-colors.d.ts.map +1 -1
  126. package/dist/tui/flow-colors.js +2 -2
  127. package/dist/tui/flow-colors.js.map +1 -1
  128. package/dist/tui/render-utils.js +1 -1
  129. package/dist/tui/render-utils.js.map +1 -1
  130. package/dist/tui/render.d.ts +32 -1
  131. package/dist/tui/render.d.ts.map +1 -1
  132. package/dist/tui/render.js +666 -170
  133. package/dist/tui/render.js.map +1 -1
  134. package/dist/tui/scramble/algorithm.d.ts +4 -2
  135. package/dist/tui/scramble/algorithm.d.ts.map +1 -1
  136. package/dist/tui/scramble/algorithm.js +44 -12
  137. package/dist/tui/scramble/algorithm.js.map +1 -1
  138. package/dist/tui/scramble/constants.d.ts +3 -0
  139. package/dist/tui/scramble/constants.d.ts.map +1 -1
  140. package/dist/tui/scramble/constants.js +4 -1
  141. package/dist/tui/scramble/constants.js.map +1 -1
  142. package/dist/tui/scramble/index.d.ts +2 -1
  143. package/dist/tui/scramble/index.d.ts.map +1 -1
  144. package/dist/tui/scramble/index.js +1 -1
  145. package/dist/tui/scramble/index.js.map +1 -1
  146. package/dist/tui/scramble/manager.d.ts +1 -1
  147. package/dist/tui/scramble/manager.d.ts.map +1 -1
  148. package/dist/tui/scramble/manager.js +24 -19
  149. package/dist/tui/scramble/manager.js.map +1 -1
  150. package/dist/types/flow.d.ts +17 -1
  151. package/dist/types/flow.d.ts.map +1 -1
  152. package/dist/types/flow.js.map +1 -1
  153. package/dist/types/output.d.ts +11 -36
  154. package/dist/types/output.d.ts.map +1 -1
  155. package/dist/types/output.js +1 -1
  156. package/dist/types/ui.d.ts +1 -1
  157. package/dist/types/ui.d.ts.map +1 -1
  158. package/package.json +9 -9
  159. package/dist/core/executor.d.ts.map +0 -1
  160. package/dist/core/executor.js +0 -378
  161. package/dist/core/executor.js.map +0 -1
  162. package/dist/core/flow.d.ts.map +0 -1
  163. package/dist/core/flow.js.map +0 -1
  164. package/dist/core/session-mode.d.ts +0 -11
  165. package/dist/core/session-mode.d.ts.map +0 -1
  166. package/dist/core/session-mode.js +0 -26
  167. package/dist/core/session-mode.js.map +0 -1
  168. package/dist/core/transitions.d.ts +0 -39
  169. package/dist/core/transitions.d.ts.map +0 -1
  170. package/dist/core/transitions.js +0 -59
  171. package/dist/core/transitions.js.map +0 -1
  172. package/dist/snapshot/index.d.ts +0 -2
  173. package/dist/snapshot/index.d.ts.map +0 -1
  174. package/dist/snapshot/index.js +0 -2
  175. package/dist/snapshot/index.js.map +0 -1
  176. package/dist/snapshot/reasoning-strip.d.ts +0 -22
  177. package/dist/snapshot/reasoning-strip.d.ts.map +0 -1
  178. package/dist/snapshot/reasoning-strip.js +0 -58
  179. package/dist/snapshot/reasoning-strip.js.map +0 -1
  180. package/dist/snapshot/snapshot.d.ts +0 -77
  181. package/dist/snapshot/snapshot.d.ts.map +0 -1
  182. package/dist/snapshot/snapshot.js +0 -1824
  183. package/dist/snapshot/snapshot.js.map +0 -1
  184. package/dist/tools/web-tool.d.ts +0 -46
  185. package/dist/tools/web-tool.d.ts.map +0 -1
  186. package/dist/tools/web-tool.js.map +0 -1
  187. /package/dist/{core → flow}/agents.d.ts +0 -0
  188. /package/dist/{core → flow}/depth.js +0 -0
  189. /package/dist/{core → flow}/session-registry.d.ts +0 -0
  190. /package/dist/{core → flow}/session-registry.js +0 -0
  191. /package/dist/{core → flow}/transition.d.ts +0 -0
  192. /package/dist/{core → flow}/transition.js +0 -0
@@ -5,13 +5,13 @@
5
5
  * Expanded view adds raw tool call traces.
6
6
  */
7
7
  import * as os from "node:os";
8
- import { getMarkdownTheme } from "@mariozechner/pi-coding-agent";
9
- import { Container, Markdown, Spacer, Text, TruncatedText } from "@mariozechner/pi-tui";
8
+ import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
9
+ import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
10
10
  import { getFlowSummaryText } from "../snapshot/runner-events.js";
11
11
  import { aggregateFlowUsage, getFlowOutput, isFlowError, isFlowSuccess, } from "../types/flow.js";
12
- import { getFlowDisplayItems, getLastToolCall, getLastAssistantText, } from "../types/ui.js";
12
+ import { getFlowDisplayItems, getLastToolCall, } from "../types/ui.js";
13
13
  import { formatBatchOpsSummary } from "../batch/summary.js";
14
- import { scrambleManager, runScrambleTimer, DynamicScrambleText, getLiveText } from "./scramble/index.js";
14
+ import { scrambleManager, runScrambleTimer, DynamicScrambleText, getLiveText, hashNoise, THIN_BRAILLE_SPARK } from "./scramble/index.js";
15
15
  // ---------------------------------------------------------------------------
16
16
  // Anonymous flow-id counter — prevents scramble-state collisions when multiple
17
17
  // flow widgets share the screen and toolCallId is absent from result/args.
@@ -31,7 +31,7 @@ function getLiveTextWithFallback(id) {
31
31
  const fallbackId = id.includes("#") ? "collapsed" + id.slice(id.indexOf("#")) : "collapsed";
32
32
  return getLiveText(fallbackId);
33
33
  }
34
- import { formatCompactStats, formatFlowTypeName, lowerFirstWord, truncateChars, tailText, getTruncationBudget, visibleLength, stripAnsi, formatModelLabel, formatContextLabel, formatTps } from "./render-utils.js";
34
+ import { formatCompactStats, formatFlowTypeName, lowerFirstWord, truncateChars, tailText, getTruncationBudget, visibleLength, stripAnsi, formatModelLabel, formatContextLabel, formatTps, italic } from "./render-utils.js";
35
35
  function shortenPath(p) {
36
36
  const home = os.homedir();
37
37
  return p.startsWith(home) ? `~${p.slice(home.length)}` : p;
@@ -104,10 +104,208 @@ function renderFlowReport(output, theme, config) {
104
104
  const lines = splitOutputLines(output);
105
105
  return lines.map((line) => applyRole("actContent", line, theme, config)).join("\n");
106
106
  }
107
+ function getFlowStatus(r) {
108
+ return r.status ?? (r.exitCode === -1 ? "running" : r.exitCode === 0 ? "done" : "error");
109
+ }
110
+ function isFlowStatusComplete(r) {
111
+ const status = getFlowStatus(r);
112
+ return status === "done" || status === "error" || status === "skipped";
113
+ }
114
+ function isFlowRunning(r) {
115
+ const status = getFlowStatus(r);
116
+ return status === "running" || status === "pending";
117
+ }
118
+ function isFlowAwaiting(r) {
119
+ return getFlowStatus(r) === "awaiting";
120
+ }
121
+ /**
122
+ * Detect audit-loop groups.
123
+ *
124
+ * When the executor stamps `auditLoopGroupId` on results, grouping is
125
+ * explicit and works regardless of array layout (no contiguity required).
126
+ *
127
+ * When no `auditLoopGroupId` is present (legacy / hand-crafted results),
128
+ * we fall back to contiguity-based detection: N contiguous builds with
129
+ * `pingPongMeta` followed immediately by an audit with
130
+ * `auditParentType === "build"`.
131
+ */
132
+ export function detectGroups(results) {
133
+ const groups = [];
134
+ const rootIndices = [];
135
+ // Phase 1: explicit grouping by auditLoopGroupId
136
+ const groupMap = new Map();
137
+ const ungroupedIndices = [];
138
+ for (let i = 0; i < results.length; i++) {
139
+ const r = results[i];
140
+ if (r.auditLoopGroupId !== undefined) {
141
+ let g = groupMap.get(r.auditLoopGroupId);
142
+ if (!g) {
143
+ g = { buildIndices: [], auditIndex: -1 };
144
+ groupMap.set(r.auditLoopGroupId, g);
145
+ }
146
+ if (r.pingPongMeta) {
147
+ g.buildIndices.push(i);
148
+ }
149
+ else if (r.auditParentType === "build") {
150
+ g.auditIndex = i;
151
+ }
152
+ }
153
+ else {
154
+ ungroupedIndices.push(i);
155
+ }
156
+ }
157
+ for (const g of groupMap.values()) {
158
+ if (g.auditIndex !== -1) {
159
+ groups.push({ buildIndices: g.buildIndices, auditIndex: g.auditIndex });
160
+ }
161
+ else {
162
+ // Orphaned builds with groupId but no audit capstone
163
+ rootIndices.push(...g.buildIndices);
164
+ }
165
+ }
166
+ // Phase 2: legacy fallback on ungrouped results (contiguity-based)
167
+ let i = 0;
168
+ while (i < ungroupedIndices.length) {
169
+ const idx = ungroupedIndices[i];
170
+ const r = results[idx];
171
+ if (r.pingPongMeta) {
172
+ const buildIndices = [];
173
+ while (i < ungroupedIndices.length && results[ungroupedIndices[i]].pingPongMeta) {
174
+ buildIndices.push(ungroupedIndices[i]);
175
+ i++;
176
+ }
177
+ if (i < ungroupedIndices.length && results[ungroupedIndices[i]].auditParentType === "build") {
178
+ groups.push({ buildIndices, auditIndex: ungroupedIndices[i] });
179
+ i++;
180
+ }
181
+ else {
182
+ rootIndices.push(...buildIndices);
183
+ }
184
+ }
185
+ else if (r.auditParentType === "build" && i > 0 && results[ungroupedIndices[i - 1]].pingPongMeta) {
186
+ i++; // orphan audit already consumed
187
+ }
188
+ else {
189
+ rootIndices.push(idx);
190
+ i++;
191
+ }
192
+ }
193
+ return { groups, rootIndices };
194
+ }
195
+ /**
196
+ * Get the status icon dot for a result (● ○ ✗ ⊘).
197
+ */
107
198
  function flowStatusIcon(r, theme) {
108
- if (r.exitCode === -1)
109
- return theme.fg("warning", "(pending)");
110
- return isFlowError(r) ? theme.fg("error", "(error)") : theme.fg("success", "(done)");
199
+ const status = getFlowStatus(r);
200
+ switch (status) {
201
+ case "running":
202
+ case "pending":
203
+ return theme.fg("warning", "●");
204
+ case "awaiting":
205
+ return theme.fg("muted", "○");
206
+ case "done":
207
+ return theme.fg("success", "●");
208
+ case "error":
209
+ return theme.fg("error", "✗");
210
+ case "skipped":
211
+ return theme.fg("muted", "⊘");
212
+ default:
213
+ return theme.fg("muted", "?");
214
+ }
215
+ }
216
+ function hashStrToSeed(s) {
217
+ let h = 2166136261;
218
+ for (let i = 0; i < s.length; i++) {
219
+ h ^= s.charCodeAt(i);
220
+ h = Math.imul(h, 16777619);
221
+ }
222
+ return h >>> 0;
223
+ }
224
+ function getScintillatingStatusDot(r, theme, now, flowId) {
225
+ const status = getFlowStatus(r);
226
+ switch (status) {
227
+ case "running":
228
+ case "pending": {
229
+ const isPending = status === "pending";
230
+ const seed = hashStrToSeed(flowId || r.type);
231
+ const bucketSize = isPending ? 7000 : 5000;
232
+ const bucket = Math.floor(now / bucketSize);
233
+ const t = now % bucketSize;
234
+ const burstCount = isPending
235
+ ? 1 + Math.floor(hashNoise(seed, bucket, 0, 0x5a4f) * 2) // 1-2
236
+ : 2 + Math.floor(hashNoise(seed, bucket, 0, 0x5a4f) * 2); // 2-3
237
+ let cursor = 50;
238
+ for (let b = 0; b < burstCount; b++) {
239
+ const gap = isPending
240
+ ? 800 + Math.floor(hashNoise(seed, bucket, b * 4, 0xb8a0) * 1400) // 800-2200ms
241
+ : 500 + Math.floor(hashNoise(seed, bucket, b * 4, 0xb8a0) * 1300); // 500-1800ms
242
+ cursor += gap;
243
+ const duration = isPending
244
+ ? 80 + Math.floor(hashNoise(seed, bucket, b * 4 + 1, 0xc0de) * 170) // 80-250ms
245
+ : 100 + Math.floor(hashNoise(seed, bucket, b * 4 + 1, 0xc0de) * 250); // 100-350ms
246
+ const burstStart = cursor;
247
+ const burstEnd = cursor + duration;
248
+ cursor = burstEnd;
249
+ if (t >= burstStart && t < burstEnd) {
250
+ const tInBurst = t - burstStart;
251
+ const tick = 12 + Math.floor(hashNoise(seed, bucket, b * 4 + 3, 0xd1a0) * 10); // 12-22ms per stutter step
252
+ // Vary stutter depth: 3-tick ○●○ or 5-tick ○●○●○ per burst
253
+ const rawStutterTicks = hashNoise(seed, bucket, b * 4 + 2, 0xe7a1) > 0.5 ? 5 : 3;
254
+ const stutterLen5 = tick * 5;
255
+ const onRunMax5 = duration - stutterLen5 - 5;
256
+ const stutterTicks = (rawStutterTicks === 5 && onRunMax5 >= tick) ? 5 : 3;
257
+ const stutterLen = tick * stutterTicks;
258
+ const onRunMax = duration - stutterLen - 5;
259
+ const onRun = Math.max(tick, Math.min(Math.floor(duration * (0.35 + hashNoise(seed, bucket, b * 4 + 2, 0xf1c0) * 0.3)), onRunMax));
260
+ const cycleLen = onRun + stutterLen;
261
+ const phaseInCycle = tInBurst % cycleLen;
262
+ const cycleIdx = Math.floor(tInBurst / cycleLen);
263
+ // Helper: dip ○ with occasional sparkle
264
+ const dipDot = (dipIndex) => {
265
+ if (hashNoise(seed, bucket, cycleIdx + dipIndex * 100, 0x5ab0) < 0.05) {
266
+ const sparkIdx = Math.floor(hashNoise(seed, bucket, cycleIdx + dipIndex * 100, 0x5b1) * THIN_BRAILLE_SPARK.length);
267
+ return theme.fg("muted", THIN_BRAILLE_SPARK[sparkIdx]);
268
+ }
269
+ return theme.fg("muted", "○");
270
+ };
271
+ if (phaseInCycle < onRun) {
272
+ // Sustained bright ●
273
+ return theme.fg("warning", "●");
274
+ }
275
+ else if (phaseInCycle < onRun + tick) {
276
+ return dipDot(0); // ○ dip 1
277
+ }
278
+ else if (phaseInCycle < onRun + tick * 2) {
279
+ return theme.fg("warning", "●"); // ● flash 1
280
+ }
281
+ else if (phaseInCycle < onRun + tick * 3) {
282
+ return dipDot(1); // ○ dip 2
283
+ }
284
+ else if (stutterTicks >= 5 && phaseInCycle < onRun + tick * 4) {
285
+ return theme.fg("warning", "●"); // ● flash 2 (5-tick only)
286
+ }
287
+ else if (stutterTicks >= 5 && phaseInCycle < onRun + tick * 5) {
288
+ return dipDot(2); // ○ dip 3 (5-tick only)
289
+ }
290
+ else {
291
+ // Fallback: shouldn't reach if scheduling is correct
292
+ return theme.fg("warning", "●");
293
+ }
294
+ }
295
+ }
296
+ return theme.fg("warning", "●");
297
+ }
298
+ case "awaiting":
299
+ return theme.fg("muted", "○");
300
+ case "done":
301
+ return theme.fg("success", "●");
302
+ case "error":
303
+ return theme.fg("error", "✗");
304
+ case "skipped":
305
+ return theme.fg("muted", "⊘");
306
+ default:
307
+ return theme.fg("muted", "?");
308
+ }
111
309
  }
112
310
  /** Center a label in a fixed-width header using em-dashes. Total width = 20. */
113
311
  function sectionHeader(label) {
@@ -176,13 +374,13 @@ export function renderFlowResult(result, expanded, theme, args, config) {
176
374
  let resolvedToolCallId;
177
375
  if (args?.state) {
178
376
  const s = args.state;
179
- resolvedToolCallId = s.__flowId;
377
+ resolvedToolCallId = s.__widgetId;
180
378
  if (!resolvedToolCallId) {
181
379
  resolvedToolCallId = result._toolCallId || args?.toolCallId || args?.id;
182
380
  if (!resolvedToolCallId) {
183
381
  resolvedToolCallId = getAnonymousFlowId();
184
382
  }
185
- s.__flowId = resolvedToolCallId;
383
+ s.__widgetId = resolvedToolCallId;
186
384
  }
187
385
  }
188
386
  else {
@@ -275,6 +473,140 @@ export function renderFlowResult(result, expanded, theme, args, config) {
275
473
  return container;
276
474
  }
277
475
  // ---------------------------------------------------------------------------
476
+ // Trace rendering — simplified, no model, inline stats
477
+ // ---------------------------------------------------------------------------
478
+ export function renderTraceCall(_args, _theme, _config) {
479
+ // Trace call frame is invisible — the result frame shows 'trace <aim>'.
480
+ // Returning an empty Container avoids a duplicate 'trace' line.
481
+ return new Container();
482
+ }
483
+ export function renderTraceResult(result, expanded, theme, args, config) {
484
+ const details = result.details;
485
+ const streamingText = result.content?.[0]?.type === "text" ? result.content[0].text : undefined;
486
+ // Resolve id (same pattern as renderFlowResult)
487
+ let resolvedToolCallId;
488
+ if (args?.state) {
489
+ const s = args.state;
490
+ resolvedToolCallId = s.__widgetId;
491
+ if (!resolvedToolCallId) {
492
+ resolvedToolCallId = result._toolCallId || args?.toolCallId || args?.id;
493
+ if (!resolvedToolCallId) {
494
+ resolvedToolCallId = getAnonymousFlowId();
495
+ }
496
+ s.__widgetId = resolvedToolCallId;
497
+ }
498
+ }
499
+ else {
500
+ resolvedToolCallId = result._toolCallId || args?.toolCallId || args?.id;
501
+ }
502
+ const id = resolvedToolCallId || "trace";
503
+ const now = Date.now();
504
+ let container = new Container();
505
+ // Get the SingleResult
506
+ let r;
507
+ if (details?.results && details.results.length > 0) {
508
+ r = details.results[0];
509
+ }
510
+ const isComplete = r ? isFlowStatusComplete(r) : false;
511
+ // Header line: ● trace · <aim> · <stats>
512
+ const typeName = formatFlowTypeName("trace");
513
+ const aimText = r?.aim || r?.intent || streamingText || "trace";
514
+ const initialDot = r ? flowStatusIcon(r, theme) : theme.fg("success", "●");
515
+ const dotPlaceholder = stripAnsi(initialDot) + " ";
516
+ const statsParts = [];
517
+ if (r) {
518
+ if (r.maxContextTokens !== undefined || r.usage.contextTokens > 0) {
519
+ statsParts.push(formatContextLabel(r.usage.contextTokens, r.maxContextTokens));
520
+ }
521
+ statsParts.push(formatTps(r.usage.smoothedTps));
522
+ }
523
+ const displayStats = statsParts.length > 0 ? " · " + statsParts.join(" · ") : "";
524
+ const statsPlain = stripAnsi(displayStats);
525
+ const headerPlain = `${dotPlaceholder}${typeName}${statsPlain}`;
526
+ const headerSegments = [
527
+ { text: dotPlaceholder, style: (_s) => (r ? getScintillatingStatusDot(r, theme, Date.now(), id) : initialDot) + " " },
528
+ { text: typeName, style: (s) => applyRole("flowName", s, theme, config) },
529
+ ];
530
+ if (statsPlain) {
531
+ headerSegments.push({ text: displayStats, style: (s) => applyRole("stats", s, theme, config) });
532
+ }
533
+ container.addChild(new DynamicScrambleText(`${initialDot} ${applyRole("flowName", typeName, theme, config)}${applyRole("stats", displayStats, theme, config)}`, () => {
534
+ const now2 = Date.now();
535
+ const result2 = scrambleManager.updateText(id, "header", headerPlain, now2, isComplete, true);
536
+ return reconstructHeader(result2.content, headerSegments);
537
+ }, true));
538
+ // Cmd line: └─ cmd ▸ <last tool call>
539
+ const actTree = "└─";
540
+ const actLabel = ` cmd ▸ `;
541
+ if (r?.messages && r.messages.length > 0) {
542
+ const lastTool = getLastToolCall(r.messages);
543
+ const actStr = lastTool ? formatFlowToolCall(lastTool.name, lastTool.args, theme.fg.bind(theme)) : "[n/a]";
544
+ const actFullText = stripAnsi(lowerFirstWord(actStr));
545
+ const actInitial = `${applyRole("treeChars", actTree, theme, config)}${applyRole("prefixLabel", actLabel, theme, config)}${applyRole("actContent", italic(actFullText), theme, config)}`;
546
+ container.addChild(new DynamicScrambleText(actInitial, () => {
547
+ const now2 = Date.now();
548
+ const freshAct = lastTool ? formatFlowToolCall(lastTool.name, lastTool.args, theme.fg.bind(theme)) : "[n/a]";
549
+ const freshPlain = stripAnsi(lowerFirstWord(freshAct));
550
+ const result2 = scrambleManager.updateAct(id, freshPlain, now2, isComplete, true);
551
+ const content = result2.content;
552
+ return `${applyRole("treeChars", actTree, theme, config)}${applyRole("prefixLabel", actLabel, theme, config)}${applyRole("actContent", italic(content), theme, config)}`;
553
+ }, true));
554
+ }
555
+ else {
556
+ // No messages yet — show awaiting
557
+ const actInitial = `${applyRole("treeChars", actTree, theme, config)}${applyRole("prefixLabel", actLabel, theme, config)}${applyRole("prefixLabel", "[awaiting...]", theme, config)}`;
558
+ container.addChild(new DynamicScrambleText(actInitial, () => {
559
+ const now2 = Date.now();
560
+ const plain = "[awaiting...]";
561
+ const result2 = scrambleManager.updateAct(id, plain, now2, isComplete, true);
562
+ const content = result2.content;
563
+ return `${applyRole("treeChars", actTree, theme, config)}${applyRole("prefixLabel", actLabel, theme, config)}${applyRole((r && isFlowAwaiting(r)) ? "prefixLabel" : "actContent", italic(content), theme, config)}`;
564
+ }, true));
565
+ }
566
+ // Expanded view: add full output
567
+ if (expanded) {
568
+ const flowOutput = streamingText;
569
+ if (flowOutput) {
570
+ container.addChild(new Spacer(1));
571
+ container.addChild(new Markdown(flowOutput, 0, 0, getMarkdownTheme()));
572
+ }
573
+ }
574
+ // In-place mutation pattern (same as renderFlowResult)
575
+ if (args?.state) {
576
+ const s = args.state;
577
+ if (!s.__rootContainer) {
578
+ if (container instanceof Container) {
579
+ s.__rootContainer = container;
580
+ }
581
+ else {
582
+ const root = new Container();
583
+ root.addChild(container);
584
+ s.__rootContainer = root;
585
+ }
586
+ }
587
+ else if (container !== s.__rootContainer) {
588
+ const root = s.__rootContainer;
589
+ root.clear();
590
+ if (container instanceof Container) {
591
+ const children = [...container.children];
592
+ for (const child of children) {
593
+ root.addChild(child);
594
+ }
595
+ }
596
+ else {
597
+ root.addChild(container);
598
+ }
599
+ root.invalidate();
600
+ container = root;
601
+ }
602
+ }
603
+ if (isComplete) {
604
+ scrambleManager.completeFlow(id);
605
+ }
606
+ runScrambleTimer(args, id);
607
+ return container;
608
+ }
609
+ // ---------------------------------------------------------------------------
278
610
  // Single flow result
279
611
  // ---------------------------------------------------------------------------
280
612
  export function renderSingleFlowResult(r, expanded, theme, streamingText, toolCallId, config) {
@@ -284,7 +616,7 @@ export function renderSingleFlowResult(r, expanded, theme, streamingText, toolCa
284
616
  const displayItems = getFlowDisplayItems(r.messages);
285
617
  const flowOutput = getFlowOutput(r.messages);
286
618
  const now = Date.now();
287
- const isComplete = r.exitCode !== -1;
619
+ const isComplete = isFlowStatusComplete(r);
288
620
  if (expanded) {
289
621
  return renderFlowExpanded(r, icon, error, displayItems, flowOutput, theme, id, now, isComplete, streamingText, config);
290
622
  }
@@ -294,20 +626,24 @@ function renderFlowExpanded(r, icon, error, displayItems, flowOutput, theme, id,
294
626
  const mdTheme = getMarkdownTheme();
295
627
  const container = new Container();
296
628
  const typeName = formatFlowTypeName(r.type);
297
- let header = applyRole("flowName", typeName, theme, config);
629
+ const initialDot = flowStatusIcon(r, theme);
630
+ let header = `${initialDot} ${applyRole("flowName", typeName, theme, config)}`;
298
631
  const errorSegment = error && r.stopReason ? ` [${r.stopReason}]` : "";
299
632
  if (errorSegment)
300
633
  header += ` ${theme.fg("error", errorSegment)}`;
301
- const plainHeader = typeName + errorSegment;
634
+ const dotPlaceholder = stripAnsi(initialDot) + ' ';
635
+ const plainHeader = dotPlaceholder + typeName + errorSegment;
302
636
  const headerSegments = [
637
+ { text: dotPlaceholder, style: (_s) => getScintillatingStatusDot(r, theme, Date.now(), id) + " " },
303
638
  { text: typeName, style: (s) => applyRole("flowName", s, theme, config) },
304
639
  ];
305
640
  if (errorSegment) {
306
641
  headerSegments.push({ text: errorSegment, style: (s) => theme.fg("error", s) });
307
642
  }
308
643
  container.addChild(new DynamicScrambleText(header, () => {
309
- const result = scrambleManager.updateText(id, 'header', plainHeader, Date.now(), isComplete);
310
- return result.isAnimating ? reconstructHeader(result.content, headerSegments) : header;
644
+ const now = Date.now();
645
+ const result = scrambleManager.updateText(id, 'header', plainHeader, now, isComplete);
646
+ return reconstructHeader(result.content, headerSegments);
311
647
  }));
312
648
  if (error && r.errorMessage) {
313
649
  container.addChild(new Text(scrambleManager.renderStatic(theme.fg("error", `Error: ${r.errorMessage}`)), 0, 0));
@@ -398,7 +734,10 @@ function renderFlowExpanded(r, icon, error, displayItems, flowOutput, theme, id,
398
734
  container.addChild(new Spacer(1));
399
735
  }
400
736
  // Output: animate streaming text; show clean markdown when complete
401
- if (!isComplete && streamingText != null) {
737
+ if (isFlowAwaiting(r)) {
738
+ container.addChild(new Text(applyRole("prefixLabel", "[awaiting...]", theme, config), 0, 0));
739
+ }
740
+ else if (!isComplete && streamingText != null) {
402
741
  const msgBudget = getTruncationBudget(0);
403
742
  const displayMsg = tailText(stripAnsi(streamingText), msgBudget);
404
743
  container.addChild(new DynamicScrambleText(displayMsg, () => {
@@ -451,8 +790,8 @@ function renderFlowCollapsed(r, icon, error, flowOutput, theme, streamingText, t
451
790
  const maxWidth = process.stdout.columns ?? 80;
452
791
  const typeName = formatCollapsedFlowHeaderTypeName(r.type);
453
792
  const modelLabel = formatModelLabel(r.model);
454
- const headerPrefixLen = visibleLength(typeName) + visibleLength(modelLabel ? ` ${modelLabel} · ` : " ");
455
- const isComplete = r.exitCode !== -1;
793
+ const headerPrefixLen = visibleLength(typeName) + visibleLength(modelLabel ? ` ${modelLabel} · ` : " ");
794
+ const isComplete = isFlowStatusComplete(r);
456
795
  // Build header stats: ctxLabel · t/s
457
796
  const statsParts = [];
458
797
  if (r.maxContextTokens !== undefined || r.usage.contextTokens > 0) {
@@ -470,14 +809,17 @@ function renderFlowCollapsed(r, icon, error, flowOutput, theme, streamingText, t
470
809
  displayStats = displayStats.replace(`${tpsNum} t/s`, `${scrambledTps} t/s`);
471
810
  }
472
811
  }
473
- const modelSegment = modelLabel ? ` ${modelLabel} · ` : " ";
812
+ const modelSegment = modelLabel ? ` ${modelLabel} · ` : " ";
474
813
  const statsSegment = stripAnsi(displayStats);
475
814
  const errorSegment = error && r.stopReason ? ` [${r.stopReason}]` : "";
476
- let header = `${applyRole("flowName", typeName, theme, config)}${applyRole("modelName", modelSegment, theme, config)}${applyRole("stats", displayStats, theme, config)}`;
815
+ const initialDot = flowStatusIcon(r, theme);
816
+ let header = `${initialDot} ${applyRole("flowName", typeName, theme, config)}${applyRole("modelName", modelSegment, theme, config)}${applyRole("stats", displayStats, theme, config)}`;
477
817
  if (errorSegment)
478
818
  header += ` ${theme.fg("error", errorSegment)}`;
479
- const plainHeader = typeName + modelSegment + statsSegment + errorSegment;
819
+ const dotPlaceholder = stripAnsi(initialDot) + ' ';
820
+ const plainHeader = dotPlaceholder + typeName + modelSegment + statsSegment + errorSegment;
480
821
  const headerSegments = [
822
+ { text: dotPlaceholder, style: (_s) => getScintillatingStatusDot(r, theme, Date.now(), id) + " " },
481
823
  { text: typeName, style: (s) => applyRole("flowName", s, theme, config) },
482
824
  { text: modelSegment, style: (s) => applyRole("modelName", s, theme, config) },
483
825
  { text: statsSegment, style: (s) => applyRole("stats", s, theme, config) },
@@ -486,8 +828,9 @@ function renderFlowCollapsed(r, icon, error, flowOutput, theme, streamingText, t
486
828
  headerSegments.push({ text: errorSegment, style: (s) => theme.fg("error", s) });
487
829
  }
488
830
  container.addChild(new DynamicScrambleText(header, () => {
489
- const result = scrambleManager.updateText(id, 'header', plainHeader, Date.now(), isComplete, true);
490
- return result.isAnimating ? reconstructHeader(result.content, headerSegments) : header;
831
+ const now = Date.now();
832
+ const result = scrambleManager.updateText(id, 'header', plainHeader, now, isComplete, true);
833
+ return reconstructHeader(result.content, headerSegments);
491
834
  }, true));
492
835
  // aim: line — glitch on text change
493
836
  if (r.aim) {
@@ -495,77 +838,102 @@ function renderFlowCollapsed(r, icon, error, flowOutput, theme, streamingText, t
495
838
  const aimLabel = ` aim ▸ `;
496
839
  const aimPrefix = `${aimTree}${aimLabel}`;
497
840
  const budget = getTruncationBudget(visibleLength(aimPrefix));
498
- const displayAim = truncateChars(lowerFirstWord(r.aim), budget);
499
- container.addChild(new DynamicScrambleText(`${applyRole("treeChars", aimTree, theme, config)}${applyRole("prefixLabel", aimLabel, theme, config)}${applyRole("aimContent", displayAim, theme, config)}`, () => {
841
+ const displayAim = isFlowAwaiting(r) ? "[awaiting...]" : truncateChars(lowerFirstWord(r.aim), budget);
842
+ container.addChild(new DynamicScrambleText(`${applyRole("treeChars", aimTree, theme, config)}${applyRole("prefixLabel", aimLabel, theme, config)}${applyRole(isFlowAwaiting(r) ? "prefixLabel" : "aimContent", italic(displayAim), theme, config)}`, () => {
500
843
  const now = Date.now();
501
844
  const freshAimLabel = ` aim ▸ `;
502
845
  const freshAimPrefix = `${aimTree}${freshAimLabel}`;
503
846
  const freshBudget = getTruncationBudget(visibleLength(freshAimPrefix));
504
- const freshText = truncateChars(lowerFirstWord(r.aim), freshBudget);
847
+ const freshText = isFlowAwaiting(r) ? "[awaiting...]" : truncateChars(lowerFirstWord(r.aim), freshBudget);
505
848
  const result = scrambleManager.updateAim(id, freshText, now, isComplete, true);
506
- return `${applyRole("treeChars", aimTree, theme, config)}${applyRole("prefixLabel", freshAimLabel, theme, config)}${applyRole("aimContent", result.content, theme, config)}`;
849
+ return `${applyRole("treeChars", aimTree, theme, config)}${applyRole("prefixLabel", freshAimLabel, theme, config)}${applyRole(isFlowAwaiting(r) ? "prefixLabel" : "aimContent", italic(result.content), theme, config)}`;
507
850
  }, true));
508
851
  }
509
852
  // act: line (last tool call with count)
510
853
  const lastTool = getLastToolCall(r.messages);
511
854
  const actStr = lastTool ? formatFlowToolCall(lastTool.name, lastTool.args, theme.fg.bind(theme)) : "[n/a]";
512
- const actTree = "├─";
855
+ const isLite = config?.bodyVerbosity !== "full";
856
+ const actTree = isLite ? "└─" : "├─";
513
857
  const actLabel = ` cmd ▸ `;
514
858
  const prefixStub = `${actTree}${actLabel}`;
515
859
  const budget = getTruncationBudget(visibleLength(prefixStub));
516
860
  const actFullText = stripAnsi(lowerFirstWord(actStr));
517
- const initialActContent = actFullText.length > budget ? tailText(actFullText, budget) : actFullText;
518
- container.addChild(new DynamicScrambleText(`${applyRole("treeChars", actTree, theme, config)}${applyRole("prefixLabel", actLabel, theme, config)}${applyRole("actContent", initialActContent, theme, config)}`, () => {
861
+ const initialActContent = isFlowAwaiting(r) ? "[n/a]" : (actFullText.length > budget ? tailText(actFullText, budget) : actFullText);
862
+ container.addChild(new DynamicScrambleText(`${applyRole("treeChars", actTree, theme, config)}${applyRole("prefixLabel", actLabel, theme, config)}${applyRole(isFlowAwaiting(r) ? "prefixLabel" : "actContent", italic(initialActContent), theme, config)}`, () => {
519
863
  const now = Date.now();
520
- const displayAct = tailText(actFullText, budget);
521
- const actContent = scrambleManager.updateAct(id, displayAct, now, isComplete, true).content;
522
864
  const actLabel = ` cmd ▸ `;
523
865
  const actPrefix = `${actTree}${actLabel}`;
524
- return `${applyRole("treeChars", actTree, theme, config)}${applyRole("prefixLabel", actLabel, theme, config)}${applyRole("actContent", actContent, theme, config)}`;
866
+ const freshBudget = getTruncationBudget(visibleLength(actPrefix));
867
+ const displayAct = isFlowAwaiting(r) ? "[n/a]" : tailText(actFullText, freshBudget);
868
+ const actContent = scrambleManager.updateAct(id, displayAct, now, isComplete, true).content;
869
+ return `${applyRole("treeChars", actTree, theme, config)}${applyRole("prefixLabel", actLabel, theme, config)}${applyRole(isFlowAwaiting(r) ? "prefixLabel" : "actContent", italic(actContent), theme, config)}`;
525
870
  }, true));
526
- // msg: line (last assistant text or streaming)
527
- const msgPrefixStub = `└─ msg ▸ `;
528
- const msgBudget = getTruncationBudget(visibleLength(msgPrefixStub));
529
- let rawMsg;
530
- let useError = false;
531
- const liveMsgText = r.exitCode === -1 ? getLiveTextWithFallback(id) : undefined;
532
- if (liveMsgText != null) {
533
- rawMsg = stripAnsi(liveMsgText);
534
- }
535
- else if (r.exitCode === -1 && streamingText != null) {
536
- rawMsg = stripAnsi(streamingText);
537
- }
538
- else if (r.structuredOutput?.summary) {
539
- rawMsg = stripAnsi(r.structuredOutput.summary);
540
- }
541
- else if (flowOutput) {
542
- rawMsg = stripAnsi(flowOutput);
543
- }
544
- else if (error && r.errorMessage) {
545
- rawMsg = stripAnsi(r.errorMessage);
546
- useError = true;
547
- }
548
- else {
549
- const summary = getFlowSummaryText(r);
550
- rawMsg = stripAnsi(summary) || "[n/a]";
551
- }
552
- const initialNeedsTail = r.exitCode === -1 || streamingText != null || liveMsgText != null;
553
- const initialMsgContent = initialNeedsTail
554
- ? tailText(rawMsg, msgBudget)
555
- : truncateChars(rawMsg, msgBudget);
556
- const msgTree = "└─";
557
- const msgLabel = ` msg ▸ `;
558
- const initialMsgPrefix = `${msgTree}${msgLabel}`;
559
- container.addChild(new DynamicScrambleText(`${applyRole("treeChars", msgTree, theme, config)}${applyRole("prefixLabel", msgLabel, theme, config)}${applyRole(useError ? "msgError" : "msgContent", initialMsgContent, theme, config)}`, () => {
560
- const now = Date.now();
871
+ // msg: line (last assistant text or streaming) — full mode only
872
+ if (!isLite) {
873
+ const msgPrefixStub = `└─ msg ▸ `;
874
+ const msgBudget = getTruncationBudget(visibleLength(msgPrefixStub));
875
+ let rawMsg;
876
+ let useError = false;
877
+ if (isFlowAwaiting(r)) {
878
+ rawMsg = "[awaiting...]";
879
+ }
880
+ else if (r.status === "skipped") {
881
+ rawMsg = "[skipped]";
882
+ }
883
+ else {
884
+ const liveMsgText = isFlowRunning(r) ? getLiveTextWithFallback(id) : undefined;
885
+ if (liveMsgText != null) {
886
+ rawMsg = stripAnsi(liveMsgText);
887
+ }
888
+ else if (isFlowRunning(r) && streamingText != null) {
889
+ rawMsg = stripAnsi(streamingText);
890
+ }
891
+ else if (r.structuredOutput?.summary) {
892
+ rawMsg = stripAnsi(r.structuredOutput.summary);
893
+ }
894
+ else if (flowOutput) {
895
+ rawMsg = stripAnsi(flowOutput);
896
+ }
897
+ else if (error && r.errorMessage) {
898
+ rawMsg = stripAnsi(r.errorMessage);
899
+ useError = true;
900
+ }
901
+ else {
902
+ const summary = getFlowSummaryText(r);
903
+ rawMsg = stripAnsi(summary) || "[n/a]";
904
+ }
905
+ }
906
+ const initialNeedsTail = !isFlowAwaiting(r) && isFlowRunning(r) && (streamingText != null || getLiveTextWithFallback(id) != null);
907
+ const initialMsgContent = initialNeedsTail
908
+ ? tailText(rawMsg, msgBudget)
909
+ : truncateChars(rawMsg, msgBudget);
910
+ const msgTree = "└─";
561
911
  const msgLabel = ` msg ▸ `;
562
- const msgPrefix = `${msgTree}${msgLabel}`;
563
- const freshRawMsg = (r.exitCode === -1 ? getLiveTextWithFallback(id) : undefined) ?? rawMsg;
564
- const needsTail = r.exitCode === -1 || streamingText != null;
565
- const displayMsg = needsTail ? tailText(freshRawMsg, msgBudget) : truncateChars(freshRawMsg, msgBudget);
566
- const result = scrambleManager.updateMsg(id, displayMsg, now, isComplete, undefined, true);
567
- return `${applyRole("treeChars", msgTree, theme, config)}${applyRole("prefixLabel", msgLabel, theme, config)}${applyRole(useError ? "msgError" : "msgContent", result.content, theme, config)}`;
568
- }, true));
912
+ const initialMsgPrefix = `${msgTree}${msgLabel}`;
913
+ container.addChild(new DynamicScrambleText(`${applyRole("treeChars", msgTree, theme, config)}${applyRole("prefixLabel", msgLabel, theme, config)}${applyRole(useError ? "msgError" : "msgContent", italic(initialMsgContent), theme, config)}`, () => {
914
+ const now = Date.now();
915
+ const msgLabel = ` msg `;
916
+ const msgPrefix = `${msgTree}${msgLabel}`;
917
+ let freshRawMsg;
918
+ let needsTail;
919
+ if (isFlowAwaiting(r)) {
920
+ freshRawMsg = "[awaiting...]";
921
+ needsTail = false;
922
+ }
923
+ else if (r.status === "skipped") {
924
+ freshRawMsg = "[skipped]";
925
+ needsTail = false;
926
+ }
927
+ else {
928
+ const isRunningNow = isFlowRunning(r);
929
+ freshRawMsg = (isRunningNow ? getLiveTextWithFallback(id) : undefined) ?? rawMsg;
930
+ needsTail = isRunningNow && (streamingText != null || getLiveTextWithFallback(id) != null);
931
+ }
932
+ const displayMsg = needsTail ? tailText(freshRawMsg, msgBudget) : truncateChars(freshRawMsg, msgBudget);
933
+ const result = scrambleManager.updateMsg(id, displayMsg, now, isComplete, undefined, true);
934
+ return `${applyRole("treeChars", msgTree, theme, config)}${applyRole("prefixLabel", msgLabel, theme, config)}${applyRole(useError ? "msgError" : "msgContent", italic(result.content), theme, config)}`;
935
+ }, true));
936
+ }
569
937
  if (isComplete) {
570
938
  scrambleManager.completeFlow(id);
571
939
  }
@@ -594,7 +962,7 @@ function renderMultiFlowExpanded(results, successCount, icon, theme, baseId, now
594
962
  for (let flowIdx = 0; flowIdx < results.length; flowIdx++) {
595
963
  const r = results[flowIdx];
596
964
  const flowId = `${baseId}#${flowIdx}`;
597
- const isComplete = r.exitCode !== -1;
965
+ const isComplete = isFlowStatusComplete(r);
598
966
  const displayItems = getFlowDisplayItems(r.messages);
599
967
  const flowOutput = getFlowOutput(r.messages);
600
968
  const typeName = formatFlowTypeName(r.type);
@@ -634,7 +1002,10 @@ function renderMultiFlowExpanded(results, successCount, icon, theme, baseId, now
634
1002
  }));
635
1003
  }
636
1004
  // Output: animate streaming text; show clean markdown when complete
637
- if (!isComplete && r.streamingText != null) {
1005
+ if (isFlowAwaiting(r)) {
1006
+ container.addChild(new Text(applyRole("prefixLabel", "[awaiting...]", theme, config), 0, 0));
1007
+ }
1008
+ else if (!isComplete && r.streamingText != null) {
638
1009
  const streamingRaw = r.streamingText;
639
1010
  const msgBudget = getTruncationBudget(0);
640
1011
  const displayMsg = tailText(stripAnsi(streamingRaw), msgBudget);
@@ -685,17 +1056,113 @@ function renderMultiFlowExpanded(results, successCount, icon, theme, baseId, now
685
1056
  function renderActivityPanel(results, theme, baseId, config) {
686
1057
  const idPrefix = baseId || "panel";
687
1058
  const container = new Container();
688
- const maxWidth = process.stdout.columns ?? 80;
689
1059
  const now = Date.now();
1060
+ const { groups, rootIndices } = detectGroups(results);
1061
+ // Build ordered list of "root items" — each is either a standalone flow index
1062
+ // or a group index (rendered in original order).
1063
+ let groupCursor = 0;
1064
+ let rootCursor = 0;
1065
+ const orderedItems = [];
690
1066
  for (let i = 0; i < results.length; i++) {
691
- const r = results[i];
692
- const isLast = i === results.length - 1;
693
- const flowId = `${idPrefix}#${i}`;
694
- const typeName = formatCollapsedFlowHeaderTypeName(r.type);
695
- const modelLabel = formatModelLabel(r.model);
696
- const headerPrefix = isLast ? "└─" : "├─";
697
- const headerPrefixLen = visibleLength(headerPrefix) + 1 + visibleLength(typeName) + visibleLength(modelLabel ? ` ${modelLabel} · ` : " ");
698
- // Build header stats: ctxLabel · t/s
1067
+ // Is this index the start of a group?
1068
+ if (groupCursor < groups.length) {
1069
+ const g = groups[groupCursor];
1070
+ if (g.buildIndices[0] === i || g.auditIndex === i) {
1071
+ orderedItems.push({ kind: "group", groupIndex: groupCursor });
1072
+ groupCursor++;
1073
+ continue;
1074
+ }
1075
+ }
1076
+ // Is this a standalone flow?
1077
+ if (rootCursor < rootIndices.length && rootIndices[rootCursor] === i) {
1078
+ orderedItems.push({ kind: "flow", index: i });
1079
+ rootCursor++;
1080
+ }
1081
+ }
1082
+ for (let itemIdx = 0; itemIdx < orderedItems.length; itemIdx++) {
1083
+ const item = orderedItems[itemIdx];
1084
+ const isLastRoot = itemIdx === orderedItems.length - 1;
1085
+ if (item.kind === "flow") {
1086
+ renderStandaloneFlow(container, results[item.index], item.index, idPrefix, theme, now, config, isLastRoot);
1087
+ }
1088
+ else {
1089
+ renderGroup(container, groups[item.groupIndex], results, idPrefix, theme, now, config, isLastRoot);
1090
+ }
1091
+ // No blank line separator between root items — compact tree
1092
+ }
1093
+ return container;
1094
+ }
1095
+ // ---------------------------------------------------------------------------
1096
+ // Standalone flow (rendered at depth 0)
1097
+ // ---------------------------------------------------------------------------
1098
+ function renderStandaloneFlow(container, r, index, idPrefix, theme, now, config, isLastRoot = false) {
1099
+ const flowId = `${idPrefix}#${index}`;
1100
+ const headerPrefix = isLastRoot ? "└─" : "├─";
1101
+ const childPrefix = isLastRoot ? " " : "│ ";
1102
+ renderFlowHeader(container, r, flowId, headerPrefix, theme, now, config);
1103
+ renderFlowBody(container, r, flowId, childPrefix, theme, now, config);
1104
+ if (isFlowStatusComplete(r)) {
1105
+ scrambleManager.completeFlow(flowId);
1106
+ }
1107
+ }
1108
+ // ---------------------------------------------------------------------------
1109
+ // Group rendering (rendered at depth 1)
1110
+ // ---------------------------------------------------------------------------
1111
+ function renderGroup(container, group, results, idPrefix, theme, now, config, isLastRoot = false) {
1112
+ // ─── Group header line ───
1113
+ const headerPrefix = isLastRoot ? "" : "├─";
1114
+ const headerText = `${headerPrefix}${headerPrefix ? ' ' : ''}audit-loop`;
1115
+ container.addChild(new Text(applyRole("treeChars", headerText, theme, config), 0, 0));
1116
+ // ─── Build children ───
1117
+ for (let b = 0; b < group.buildIndices.length; b++) {
1118
+ const buildIdx = group.buildIndices[b];
1119
+ const r = results[buildIdx];
1120
+ const flowId = `${idPrefix}#${buildIdx}`;
1121
+ const isLastBuild = b === group.buildIndices.length - 1;
1122
+ // Audit always follows the last build, so every build uses ├─; only audit gets └─
1123
+ const buildHeaderPrefix = isLastRoot ? "├─" : "│ ├─";
1124
+ const buildChildPrefix = isLastRoot ? "│ " : "│ │ "; // All builds: audit follows, tree line continues
1125
+ renderFlowHeader(container, r, flowId, buildHeaderPrefix, theme, now, config);
1126
+ renderFlowBody(container, r, flowId, buildChildPrefix, theme, now, config);
1127
+ if (isFlowStatusComplete(r)) {
1128
+ scrambleManager.completeFlow(flowId);
1129
+ }
1130
+ // No blank line between builds or before audit capstone — compact tree
1131
+ }
1132
+ // ─── Audit capstone ───
1133
+ const auditIdx = group.auditIndex;
1134
+ const auditResult = results[auditIdx];
1135
+ const auditFlowId = `${idPrefix}#${auditIdx}`;
1136
+ const auditHeaderPrefix = isLastRoot ? "└─" : "│ └─";
1137
+ const auditChildPrefix = isLastRoot ? " " : "│ ";
1138
+ renderFlowHeader(container, auditResult, auditFlowId, auditHeaderPrefix, theme, now, config);
1139
+ renderFlowBody(container, auditResult, auditFlowId, auditChildPrefix, theme, now, config);
1140
+ if (isFlowStatusComplete(auditResult)) {
1141
+ scrambleManager.completeFlow(auditFlowId);
1142
+ }
1143
+ // No extra spacer — renderActivityPanel handles uniform inter-item spacing
1144
+ }
1145
+ // ---------------------------------------------------------------------------
1146
+ // Shared flow rendering helpers
1147
+ // ---------------------------------------------------------------------------
1148
+ function renderFlowHeader(container, r, flowId, headerPrefix, theme, now, config) {
1149
+ const typeName = formatCollapsedFlowHeaderTypeName(r.type);
1150
+ const modelLabel = formatModelLabel(r.model);
1151
+ const isComplete = isFlowStatusComplete(r);
1152
+ const flowComplete = isComplete;
1153
+ const error = isFlowError(r);
1154
+ const errorSegment = error && r.stopReason ? ` [${r.stopReason}]` : "";
1155
+ const initialDot = flowStatusIcon(r, theme);
1156
+ const dotPlaceholder = stripAnsi(initialDot) + ' ';
1157
+ let headerLine;
1158
+ let plainHeader;
1159
+ const headerSegments = [
1160
+ { text: headerPrefix + " ", style: (s) => applyRole("treeChars", s, theme, config) },
1161
+ { text: dotPlaceholder, style: (_s) => getScintillatingStatusDot(r, theme, Date.now(), flowId) + " " },
1162
+ { text: typeName, style: (s) => applyRole("flowName", s, theme, config) },
1163
+ ];
1164
+ {
1165
+ // Standard flow: model + stats
699
1166
  const statsParts = [];
700
1167
  if (r.maxContextTokens !== undefined || r.usage.contextTokens > 0) {
701
1168
  const ctxLabel = formatContextLabel(r.usage.contextTokens, r.maxContextTokens);
@@ -704,8 +1171,6 @@ function renderActivityPanel(results, theme, baseId, config) {
704
1171
  const tpsFormatted = formatTps(r.usage.smoothedTps);
705
1172
  statsParts.push(tpsFormatted);
706
1173
  let displayStats = statsParts.join(" · ");
707
- const flowComplete = r.exitCode !== -1;
708
- // Flash TPS value when it changes
709
1174
  const tpsNum = tpsFormatted.slice(0, -4); // remove " t/s" suffix
710
1175
  if (r.usage.smoothedTps && r.usage.smoothedTps > 0) {
711
1176
  const scrambledTps = scrambleManager.updateTps(flowId, tpsNum, now, flowComplete, true);
@@ -713,111 +1178,142 @@ function renderActivityPanel(results, theme, baseId, config) {
713
1178
  displayStats = displayStats.replace(`${tpsNum} t/s`, `${scrambledTps} t/s`);
714
1179
  }
715
1180
  }
716
- const error = isFlowError(r);
717
- const modelSegment = modelLabel ? ` ${modelLabel} · ` : " ";
718
- const statsSegment = stripAnsi(displayStats);
719
- const errorSegment = error && r.stopReason ? ` [${r.stopReason}]` : "";
720
- let headerLine = `${applyRole("treeChars", headerPrefix, theme, config)} ${applyRole("flowName", typeName, theme, config)}${applyRole("modelName", modelSegment, theme, config)}${applyRole("stats", displayStats, theme, config)}`;
1181
+ const modelSegment = modelLabel ? ` · ${modelLabel}` : "";
1182
+ const statsSegment = ` · ${displayStats}`;
1183
+ const statsPlain = stripAnsi(statsSegment);
1184
+ headerLine = `${applyRole("treeChars", headerPrefix, theme, config)} ${initialDot} ${applyRole("flowName", typeName, theme, config)}${applyRole("modelName", modelSegment, theme, config)}${applyRole("stats", statsSegment, theme, config)}`;
721
1185
  if (errorSegment) {
722
1186
  headerLine += ` ${theme.fg("error", errorSegment)}`;
723
1187
  }
724
- const plainHeader = headerPrefix + " " + typeName + modelSegment + statsSegment + errorSegment;
725
- const headerSegments = [
726
- { text: headerPrefix + " ", style: (s) => applyRole("treeChars", s, theme, config) },
727
- { text: typeName, style: (s) => applyRole("flowName", s, theme, config) },
728
- { text: modelSegment, style: (s) => applyRole("modelName", s, theme, config) },
729
- { text: statsSegment, style: (s) => applyRole("stats", s, theme, config) },
730
- ];
731
- if (errorSegment) {
732
- headerSegments.push({ text: errorSegment, style: (s) => theme.fg("error", s) });
733
- }
734
- container.addChild(new DynamicScrambleText(headerLine, () => {
735
- const result = scrambleManager.updateText(flowId, 'header', plainHeader, Date.now(), flowComplete, true);
736
- return result.isAnimating ? reconstructHeader(result.content, headerSegments) : headerLine;
737
- }, true));
738
- // Continuation indent for sub-lines
739
- const indent = isLast ? " " : "│ ";
740
- // aim: line — glitch on text change
741
- if (r.aim) {
742
- const aimTree = indent + "├─";
743
- const aimLabel = ` aim ▸ `;
744
- const aimPrefix = `${aimTree}${aimLabel}`;
745
- const budget = getTruncationBudget(visibleLength(aimPrefix));
746
- const displayAim = truncateChars(lowerFirstWord(r.aim), budget);
747
- container.addChild(new DynamicScrambleText(`${applyRole("treeChars", aimTree, theme, config)}${applyRole("prefixLabel", aimLabel, theme, config)}${applyRole("aimContent", displayAim, theme, config)}`, () => {
748
- const now = Date.now();
749
- const freshAimLabel = ` aim ▸ `;
750
- const freshAimPrefix = `${aimTree}${freshAimLabel}`;
751
- const freshBudget = getTruncationBudget(visibleLength(freshAimPrefix));
752
- const freshText = truncateChars(lowerFirstWord(r.aim), freshBudget);
753
- const result = scrambleManager.updateAim(flowId, freshText, now, flowComplete, true);
754
- return `${applyRole("treeChars", aimTree, theme, config)}${applyRole("prefixLabel", freshAimLabel, theme, config)}${applyRole("aimContent", result.content, theme, config)}`;
755
- }, true));
756
- }
757
- // act: line (last tool call with count)
758
- const lastTool = getLastToolCall(r.messages);
759
- const actStr = lastTool ? formatFlowToolCall(lastTool.name, lastTool.args, theme.fg.bind(theme)) : "[n/a]";
760
- const actTree = `${indent}├─`;
761
- const actLabel = ` cmd ▸ `;
762
- const prefixStub = `${actTree}${actLabel}`;
763
- const budget = getTruncationBudget(visibleLength(prefixStub));
764
- const actFullText = stripAnsi(lowerFirstWord(actStr));
765
- const initialActContent = actFullText.length > budget ? tailText(actFullText, budget) : actFullText;
766
- container.addChild(new DynamicScrambleText(`${applyRole("treeChars", actTree, theme, config)}${applyRole("prefixLabel", actLabel, theme, config)}${applyRole("actContent", initialActContent, theme, config)}`, () => {
1188
+ plainHeader = headerPrefix + " " + dotPlaceholder + typeName + modelSegment + statsPlain + errorSegment;
1189
+ headerSegments.push({ text: modelSegment, style: (s) => applyRole("modelName", s, theme, config) }, { text: statsPlain, style: (s) => applyRole("stats", s, theme, config) });
1190
+ }
1191
+ if (errorSegment) {
1192
+ headerSegments.push({ text: errorSegment, style: (s) => theme.fg("error", s) });
1193
+ }
1194
+ container.addChild(new DynamicScrambleText(headerLine, () => {
1195
+ const now = Date.now();
1196
+ const result = scrambleManager.updateText(flowId, 'header', plainHeader, now, flowComplete, true);
1197
+ return reconstructHeader(result.content, headerSegments);
1198
+ }, true));
1199
+ }
1200
+ function renderFlowBody(container, r, flowId, indent, theme, now, config) {
1201
+ const isComplete = isFlowStatusComplete(r);
1202
+ const flowComplete = isComplete;
1203
+ // aim: line glitch on text change
1204
+ if (r.aim) {
1205
+ const aimTree = indent + "├─";
1206
+ const aimLabel = ` aim ▸ `;
1207
+ const aimPrefix = `${aimTree}${aimLabel}`;
1208
+ const budget = getTruncationBudget(visibleLength(aimPrefix));
1209
+ const displayAim = isFlowAwaiting(r) ? "[awaiting...]" : truncateChars(lowerFirstWord(r.aim), budget);
1210
+ container.addChild(new DynamicScrambleText(`${applyRole("treeChars", aimTree, theme, config)}${applyRole("prefixLabel", aimLabel, theme, config)}${applyRole(isFlowAwaiting(r) ? "prefixLabel" : "aimContent", italic(displayAim), theme, config)}`, () => {
767
1211
  const now = Date.now();
768
- const actLabel = ` cmd ▸ `;
769
- const actPrefix = `${actTree}${actLabel}`;
770
- const freshBudget = getTruncationBudget(visibleLength(actPrefix));
771
- const displayAct = tailText(actFullText, freshBudget);
772
- const actContent = scrambleManager.updateAct(flowId, displayAct, now, flowComplete, true).content;
773
- return `${applyRole("treeChars", actTree, theme, config)}${applyRole("prefixLabel", actLabel, theme, config)}${applyRole("actContent", actContent, theme, config)}`;
1212
+ const freshAimLabel = ` aim ▸ `;
1213
+ const freshAimPrefix = `${aimTree}${freshAimLabel}`;
1214
+ const freshBudget = getTruncationBudget(visibleLength(freshAimPrefix));
1215
+ const freshText = isFlowAwaiting(r) ? "[awaiting...]" : truncateChars(lowerFirstWord(r.aim), freshBudget);
1216
+ const result = scrambleManager.updateAim(flowId, freshText, now, flowComplete, true);
1217
+ return `${applyRole("treeChars", aimTree, theme, config)}${applyRole("prefixLabel", freshAimLabel, theme, config)}${applyRole(isFlowAwaiting(r) ? "prefixLabel" : "aimContent", italic(result.content), theme, config)}`;
774
1218
  }, true));
775
- // msg: line (live streaming text or last assistant text)
1219
+ }
1220
+ // act: line (last tool call with count)
1221
+ const lastTool = getLastToolCall(r.messages);
1222
+ const actStr = lastTool ? formatFlowToolCall(lastTool.name, lastTool.args, theme.fg.bind(theme)) : "[n/a]";
1223
+ const isLite = config?.bodyVerbosity !== "full";
1224
+ const actTree = isLite ? `${indent}└─` : `${indent}├─`;
1225
+ const actLabel = ` cmd ▸ `;
1226
+ const prefixStub = `${actTree}${actLabel}`;
1227
+ const budget = getTruncationBudget(visibleLength(prefixStub));
1228
+ const actFullText = stripAnsi(lowerFirstWord(actStr));
1229
+ const initialActContent = isFlowAwaiting(r) ? "[n/a]" : (actFullText.length > budget ? tailText(actFullText, budget) : actFullText);
1230
+ container.addChild(new DynamicScrambleText(`${applyRole("treeChars", actTree, theme, config)}${applyRole("prefixLabel", actLabel, theme, config)}${applyRole(isFlowAwaiting(r) ? "prefixLabel" : "actContent", italic(initialActContent), theme, config)}`, () => {
1231
+ const now = Date.now();
1232
+ const actLabel = ` cmd ▸ `;
1233
+ const actPrefix = `${actTree}${actLabel}`;
1234
+ const freshBudget = getTruncationBudget(visibleLength(actPrefix));
1235
+ const displayAct = isFlowAwaiting(r) ? "[n/a]" : tailText(actFullText, freshBudget);
1236
+ const actContent = scrambleManager.updateAct(flowId, displayAct, now, flowComplete, true).content;
1237
+ return `${applyRole("treeChars", actTree, theme, config)}${applyRole("prefixLabel", actLabel, theme, config)}${applyRole(isFlowAwaiting(r) ? "prefixLabel" : "actContent", italic(actContent), theme, config)}`;
1238
+ }, true));
1239
+ // msg: line (live streaming text or last assistant text) — full mode only
1240
+ if (!isLite) {
776
1241
  const msgTree = `${indent}└─`;
777
1242
  const msgLabel = ` msg ▸ `;
778
1243
  const msgPrefixStub = `${msgTree}${msgLabel}`;
779
1244
  const msgBudget = getTruncationBudget(visibleLength(msgPrefixStub));
780
- const liveText = r.exitCode === -1 ? r.streamingText : undefined;
781
- const lastText = liveText || getLastAssistantText(r.messages);
782
1245
  let rawMsg;
783
1246
  let useError = false;
784
- const liveText_ = flowComplete ? undefined : getLiveTextWithFallback(flowId);
785
- if (liveText_ != null) {
786
- rawMsg = stripAnsi(liveText_);
1247
+ if (isFlowAwaiting(r)) {
1248
+ rawMsg = "[awaiting...]";
787
1249
  }
788
- else if (lastText) {
789
- rawMsg = stripAnsi(lastText);
1250
+ else if (r.status === "skipped") {
1251
+ rawMsg = "[skipped]";
790
1252
  }
791
- else if (error && r.errorMessage) {
792
- rawMsg = stripAnsi(r.errorMessage);
793
- useError = true;
1253
+ else if (isFlowStatusComplete(r) && !isFlowRunning(r)) {
1254
+ if (isFlowError(r) && r.errorMessage) {
1255
+ rawMsg = stripAnsi(r.errorMessage);
1256
+ useError = true;
1257
+ }
1258
+ else if (r.pingPongMeta && r.pingPongMeta.finalVerdict === "pass") {
1259
+ rawMsg = "[approved]";
1260
+ }
1261
+ else {
1262
+ rawMsg = "[finished]";
1263
+ }
794
1264
  }
795
1265
  else {
796
- rawMsg = "[n/a]";
1266
+ const liveMsgText = isFlowRunning(r) ? getLiveTextWithFallback(flowId) : undefined;
1267
+ if (liveMsgText != null) {
1268
+ rawMsg = stripAnsi(liveMsgText);
1269
+ }
1270
+ else if (isFlowRunning(r) && r.streamingText != null) {
1271
+ rawMsg = stripAnsi(r.streamingText);
1272
+ }
1273
+ else if (r.structuredOutput?.summary) {
1274
+ rawMsg = stripAnsi(r.structuredOutput.summary);
1275
+ }
1276
+ else {
1277
+ const flowOutput = getFlowOutput(r.messages);
1278
+ if (flowOutput) {
1279
+ rawMsg = stripAnsi(flowOutput);
1280
+ }
1281
+ else if (isFlowError(r) && r.errorMessage) {
1282
+ rawMsg = stripAnsi(r.errorMessage);
1283
+ useError = true;
1284
+ }
1285
+ else {
1286
+ const summary = getFlowSummaryText(r);
1287
+ rawMsg = stripAnsi(summary) || "[n/a]";
1288
+ }
1289
+ }
797
1290
  }
798
- const initialNeedsTail = Boolean(liveText_ || liveText || lastText);
1291
+ const initialNeedsTail = !isFlowAwaiting(r) && isFlowRunning(r) && (r.streamingText != null || getLiveTextWithFallback(flowId) != null);
799
1292
  const initialDisplayMsg = initialNeedsTail ? tailText(rawMsg, msgBudget) : truncateChars(rawMsg, msgBudget);
800
- container.addChild(new DynamicScrambleText(`${applyRole("treeChars", msgTree, theme, config)}${applyRole("prefixLabel", msgLabel, theme, config)}${applyRole(useError ? "msgError" : "msgContent", initialDisplayMsg, theme, config)}`, () => {
1293
+ container.addChild(new DynamicScrambleText(`${applyRole("treeChars", msgTree, theme, config)}${applyRole("prefixLabel", msgLabel, theme, config)}${applyRole(useError ? "msgError" : "msgContent", italic(initialDisplayMsg), theme, config)}`, () => {
801
1294
  const now = Date.now();
802
1295
  const msgLabel = ` msg ▸ `;
803
1296
  const msgPrefix = `${msgTree}${msgLabel}`;
804
- const freshBudget = getTruncationBudget(visibleLength(msgPrefix));
805
- const freshRawMsg = flowComplete ? rawMsg : (getLiveTextWithFallback(flowId) ?? rawMsg);
806
- const needsTail = Boolean(getLiveTextWithFallback(flowId) || liveText || lastText);
807
- const displayMsg = needsTail ? tailText(freshRawMsg, freshBudget) : truncateChars(freshRawMsg, freshBudget);
1297
+ let freshRawMsg;
1298
+ let needsTail;
1299
+ if (isFlowAwaiting(r)) {
1300
+ freshRawMsg = "[awaiting...]";
1301
+ needsTail = false;
1302
+ }
1303
+ else if (r.status === "skipped") {
1304
+ freshRawMsg = "[skipped]";
1305
+ needsTail = false;
1306
+ }
1307
+ else {
1308
+ const isRunningNow = isFlowRunning(r);
1309
+ freshRawMsg = (isRunningNow ? getLiveTextWithFallback(flowId) : undefined) ?? rawMsg;
1310
+ needsTail = isRunningNow && (r.streamingText != null || getLiveTextWithFallback(flowId) != null);
1311
+ }
1312
+ const displayMsg = needsTail ? tailText(freshRawMsg, msgBudget) : truncateChars(freshRawMsg, msgBudget);
808
1313
  const result = scrambleManager.updateMsg(flowId, displayMsg, now, flowComplete, undefined, true);
809
- return `${applyRole("treeChars", msgTree, theme, config)}${applyRole("prefixLabel", msgLabel, theme, config)}${applyRole(useError ? "msgError" : "msgContent", result.content, theme, config)}`;
1314
+ return `${applyRole("treeChars", msgTree, theme, config)}${applyRole("prefixLabel", msgLabel, theme, config)}${applyRole(useError ? "msgError" : "msgContent", italic(result.content), theme, config)}`;
810
1315
  }, true));
811
- if (flowComplete) {
812
- scrambleManager.completeFlow(flowId);
813
- }
814
- // Add blank line separator between flows (with continuation pipe)
815
- if (!isLast) {
816
- container.addChild(new TruncatedText(applyRole("treeChars", "│", theme, config), 0, 0));
817
- }
818
1316
  }
819
- container.addChild(new TruncatedText(applyRole("prefixLabel", "(Ctrl+O to expand tool traces)", theme, config), 0, 0));
820
- return container;
821
1317
  }
822
1318
  function renderMultiFlowCollapsed(results, theme, baseId, config) {
823
1319
  return renderActivityPanel(results, theme, baseId, config);