pi-agent-flow 2.0.1 → 2.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (233) hide show
  1. package/README.md +126 -489
  2. package/agents/audit.md +24 -15
  3. package/agents/build.md +4 -3
  4. package/agents/craft.md +4 -4
  5. package/agents/debug.md +5 -4
  6. package/agents/ideas.md +6 -5
  7. package/agents/scout.md +10 -8
  8. package/agents/trace.md +23 -0
  9. package/dist/batch/apply-patch.d.ts +60 -0
  10. package/dist/batch/apply-patch.d.ts.map +1 -0
  11. package/dist/batch/apply-patch.js +477 -0
  12. package/dist/batch/apply-patch.js.map +1 -0
  13. package/dist/batch/batch-bash.d.ts +10 -6
  14. package/dist/batch/batch-bash.d.ts.map +1 -1
  15. package/dist/batch/batch-bash.js +59 -11
  16. package/dist/batch/batch-bash.js.map +1 -1
  17. package/dist/batch/constants.d.ts +38 -6
  18. package/dist/batch/constants.d.ts.map +1 -1
  19. package/dist/batch/constants.js +26 -4
  20. package/dist/batch/constants.js.map +1 -1
  21. package/dist/batch/execute.d.ts +8 -2
  22. package/dist/batch/execute.d.ts.map +1 -1
  23. package/dist/batch/execute.js +222 -67
  24. package/dist/batch/execute.js.map +1 -1
  25. package/dist/batch/fuzzy-edit.d.ts +4 -1
  26. package/dist/batch/fuzzy-edit.d.ts.map +1 -1
  27. package/dist/batch/fuzzy-edit.js +7 -2
  28. package/dist/batch/fuzzy-edit.js.map +1 -1
  29. package/dist/batch/index.d.ts +10 -24
  30. package/dist/batch/index.d.ts.map +1 -1
  31. package/dist/batch/index.js +120 -120
  32. package/dist/batch/index.js.map +1 -1
  33. package/dist/batch/render.d.ts +22 -5
  34. package/dist/batch/render.d.ts.map +1 -1
  35. package/dist/batch/render.js +353 -15
  36. package/dist/batch/render.js.map +1 -1
  37. package/dist/batch/summary.d.ts.map +1 -1
  38. package/dist/batch/summary.js +26 -4
  39. package/dist/batch/summary.js.map +1 -1
  40. package/dist/config/config.d.ts +5 -4
  41. package/dist/config/config.d.ts.map +1 -1
  42. package/dist/config/config.js +15 -5
  43. package/dist/config/config.js.map +1 -1
  44. package/dist/config/models.d.ts.map +1 -1
  45. package/dist/config/models.js +7 -1
  46. package/dist/config/models.js.map +1 -1
  47. package/dist/config/settings-resolver.d.ts +5 -4
  48. package/dist/config/settings-resolver.d.ts.map +1 -1
  49. package/dist/config/settings-resolver.js +50 -21
  50. package/dist/config/settings-resolver.js.map +1 -1
  51. package/dist/core2/snapshot.d.ts +21 -0
  52. package/dist/core2/snapshot.d.ts.map +1 -0
  53. package/dist/core2/snapshot.js +214 -0
  54. package/dist/core2/snapshot.js.map +1 -0
  55. package/dist/{core → flow}/agents.d.ts.map +1 -1
  56. package/dist/{core → flow}/agents.js +5 -2
  57. package/dist/{core → flow}/agents.js.map +1 -1
  58. package/dist/flow/auto-warp.d.ts +1 -1
  59. package/dist/flow/auto-warp.js +1 -1
  60. package/dist/flow/command.d.ts +1 -1
  61. package/dist/flow/command.d.ts.map +1 -1
  62. package/dist/flow/complexity.d.ts +20 -0
  63. package/dist/flow/complexity.d.ts.map +1 -0
  64. package/dist/flow/complexity.js +34 -0
  65. package/dist/flow/complexity.js.map +1 -0
  66. package/dist/flow/continuation.d.ts +1 -1
  67. package/dist/flow/continuation.d.ts.map +1 -1
  68. package/dist/flow/continuation.js +4 -3
  69. package/dist/flow/continuation.js.map +1 -1
  70. package/dist/{core → flow}/depth.d.ts +4 -4
  71. package/dist/{core → flow}/depth.d.ts.map +1 -1
  72. package/dist/{core → flow}/depth.js +5 -5
  73. package/dist/{core → flow}/depth.js.map +1 -1
  74. package/dist/{core → flow}/executor.d.ts +42 -22
  75. package/dist/flow/executor.d.ts.map +1 -0
  76. package/dist/flow/executor.js +727 -0
  77. package/dist/flow/executor.js.map +1 -0
  78. package/dist/flow/index.d.ts +4 -4
  79. package/dist/flow/index.d.ts.map +1 -1
  80. package/dist/flow/index.js +4 -4
  81. package/dist/flow/index.js.map +1 -1
  82. package/dist/flow/loop-command.d.ts +1 -1
  83. package/dist/flow/loop-command.d.ts.map +1 -1
  84. package/dist/flow/loop-command.js +3 -0
  85. package/dist/flow/loop-command.js.map +1 -1
  86. package/dist/{core/flow.d.ts → flow/runner.d.ts} +20 -16
  87. package/dist/flow/runner.d.ts.map +1 -0
  88. package/dist/{core/flow.js → flow/runner.js} +105 -61
  89. package/dist/flow/runner.js.map +1 -0
  90. package/dist/{core → flow}/session-registry.d.ts.map +1 -1
  91. package/dist/{core → flow}/session-registry.js.map +1 -1
  92. package/dist/flow/settings-command.d.ts +3 -3
  93. package/dist/flow/settings-command.d.ts.map +1 -1
  94. package/dist/flow/settings-command.js +43 -22
  95. package/dist/flow/settings-command.js.map +1 -1
  96. package/dist/{core/delegation.d.ts → flow/transition.d.ts} +8 -8
  97. package/dist/{core/delegation.d.ts.map → flow/transition.d.ts.map} +1 -1
  98. package/dist/{core/delegation.js → flow/transition.js} +12 -12
  99. package/dist/{core/delegation.js.map → flow/transition.js.map} +1 -1
  100. package/dist/flow/types.d.ts +4 -0
  101. package/dist/flow/types.d.ts.map +1 -1
  102. package/dist/flow/warp.d.ts +15 -0
  103. package/dist/flow/warp.d.ts.map +1 -0
  104. package/dist/flow/warp.js +207 -0
  105. package/dist/flow/warp.js.map +1 -0
  106. package/dist/index.d.ts +1 -2
  107. package/dist/index.d.ts.map +1 -1
  108. package/dist/index.js +237 -89
  109. package/dist/index.js.map +1 -1
  110. package/dist/notify/notify.d.ts +1 -1
  111. package/dist/notify/notify.d.ts.map +1 -1
  112. package/dist/notify/notify.js +1 -1
  113. package/dist/snapshot/cli-args.d.ts +2 -2
  114. package/dist/snapshot/cli-args.d.ts.map +1 -1
  115. package/dist/snapshot/cli-args.js +21 -5
  116. package/dist/snapshot/cli-args.js.map +1 -1
  117. package/dist/snapshot/runner-events.d.ts +7 -2
  118. package/dist/snapshot/runner-events.d.ts.map +1 -1
  119. package/dist/snapshot/runner-events.js +137 -19
  120. package/dist/snapshot/runner-events.js.map +1 -1
  121. package/dist/snapshot/structured-output.d.ts +1 -1
  122. package/dist/snapshot/structured-output.d.ts.map +1 -1
  123. package/dist/snapshot/structured-output.js +20 -2
  124. package/dist/snapshot/structured-output.js.map +1 -1
  125. package/dist/steering/flow-prompt.d.ts +4 -4
  126. package/dist/steering/flow-prompt.d.ts.map +1 -1
  127. package/dist/steering/flow-prompt.js +18 -37
  128. package/dist/steering/flow-prompt.js.map +1 -1
  129. package/dist/steering/sliding-prompt.d.ts.map +1 -1
  130. package/dist/steering/sliding-prompt.js +8 -7
  131. package/dist/steering/sliding-prompt.js.map +1 -1
  132. package/dist/steering/tool-utils.d.ts +31 -8
  133. package/dist/steering/tool-utils.d.ts.map +1 -1
  134. package/dist/steering/tool-utils.js +63 -30
  135. package/dist/steering/tool-utils.js.map +1 -1
  136. package/dist/tools/ask-user.d.ts +2 -19
  137. package/dist/tools/ask-user.d.ts.map +1 -1
  138. package/dist/tools/ask-user.js +14 -38
  139. package/dist/tools/ask-user.js.map +1 -1
  140. package/dist/tools/timed-bash.d.ts +1 -1
  141. package/dist/tools/timed-bash.d.ts.map +1 -1
  142. package/dist/tools/timed-bash.js +11 -9
  143. package/dist/tools/timed-bash.js.map +1 -1
  144. package/dist/tools/trace.d.ts +34 -0
  145. package/dist/tools/trace.d.ts.map +1 -0
  146. package/dist/tools/trace.js +180 -0
  147. package/dist/tools/trace.js.map +1 -0
  148. package/dist/tools/web-ops.d.ts +85 -0
  149. package/dist/tools/web-ops.d.ts.map +1 -0
  150. package/dist/tools/{web-tool.js → web-ops.js} +51 -127
  151. package/dist/tools/web-ops.js.map +1 -0
  152. package/dist/tui/flow-colors.d.ts +1 -0
  153. package/dist/tui/flow-colors.d.ts.map +1 -1
  154. package/dist/tui/flow-colors.js +2 -2
  155. package/dist/tui/flow-colors.js.map +1 -1
  156. package/dist/tui/render-utils.d.ts.map +1 -1
  157. package/dist/tui/render-utils.js +2 -4
  158. package/dist/tui/render-utils.js.map +1 -1
  159. package/dist/tui/render.d.ts +41 -1
  160. package/dist/tui/render.d.ts.map +1 -1
  161. package/dist/tui/render.js +724 -189
  162. package/dist/tui/render.js.map +1 -1
  163. package/dist/tui/scramble/algorithm.d.ts +4 -2
  164. package/dist/tui/scramble/algorithm.d.ts.map +1 -1
  165. package/dist/tui/scramble/algorithm.js +44 -12
  166. package/dist/tui/scramble/algorithm.js.map +1 -1
  167. package/dist/tui/scramble/constants.d.ts +3 -0
  168. package/dist/tui/scramble/constants.d.ts.map +1 -1
  169. package/dist/tui/scramble/constants.js +4 -1
  170. package/dist/tui/scramble/constants.js.map +1 -1
  171. package/dist/tui/scramble/index.d.ts +3 -2
  172. package/dist/tui/scramble/index.d.ts.map +1 -1
  173. package/dist/tui/scramble/index.js +2 -2
  174. package/dist/tui/scramble/index.js.map +1 -1
  175. package/dist/tui/scramble/manager.d.ts +2 -2
  176. package/dist/tui/scramble/manager.d.ts.map +1 -1
  177. package/dist/tui/scramble/manager.js +37 -20
  178. package/dist/tui/scramble/manager.js.map +1 -1
  179. package/dist/tui/scramble/utils.js +1 -1
  180. package/dist/tui/scramble/utils.js.map +1 -1
  181. package/dist/types/flow.d.ts +17 -1
  182. package/dist/types/flow.d.ts.map +1 -1
  183. package/dist/types/flow.js +11 -2
  184. package/dist/types/flow.js.map +1 -1
  185. package/dist/types/output.d.ts +11 -36
  186. package/dist/types/output.d.ts.map +1 -1
  187. package/dist/types/output.js +1 -1
  188. package/dist/types/ui.d.ts +1 -1
  189. package/dist/types/ui.d.ts.map +1 -1
  190. package/package.json +10 -10
  191. package/dist/core/executor.d.ts.map +0 -1
  192. package/dist/core/executor.js +0 -378
  193. package/dist/core/executor.js.map +0 -1
  194. package/dist/core/flow.d.ts.map +0 -1
  195. package/dist/core/flow.js.map +0 -1
  196. package/dist/core/session-mode.d.ts +0 -11
  197. package/dist/core/session-mode.d.ts.map +0 -1
  198. package/dist/core/session-mode.js +0 -26
  199. package/dist/core/session-mode.js.map +0 -1
  200. package/dist/core/transitions.d.ts +0 -39
  201. package/dist/core/transitions.d.ts.map +0 -1
  202. package/dist/core/transitions.js +0 -59
  203. package/dist/core/transitions.js.map +0 -1
  204. package/dist/flow/perform-warp.d.ts +0 -28
  205. package/dist/flow/perform-warp.d.ts.map +0 -1
  206. package/dist/flow/perform-warp.js +0 -127
  207. package/dist/flow/perform-warp.js.map +0 -1
  208. package/dist/flow/warp-command.d.ts +0 -8
  209. package/dist/flow/warp-command.d.ts.map +0 -1
  210. package/dist/flow/warp-command.js +0 -144
  211. package/dist/flow/warp-command.js.map +0 -1
  212. package/dist/flow/warp-utils.d.ts +0 -11
  213. package/dist/flow/warp-utils.d.ts.map +0 -1
  214. package/dist/flow/warp-utils.js +0 -187
  215. package/dist/flow/warp-utils.js.map +0 -1
  216. package/dist/snapshot/index.d.ts +0 -2
  217. package/dist/snapshot/index.d.ts.map +0 -1
  218. package/dist/snapshot/index.js +0 -2
  219. package/dist/snapshot/index.js.map +0 -1
  220. package/dist/snapshot/reasoning-strip.d.ts +0 -22
  221. package/dist/snapshot/reasoning-strip.d.ts.map +0 -1
  222. package/dist/snapshot/reasoning-strip.js +0 -58
  223. package/dist/snapshot/reasoning-strip.js.map +0 -1
  224. package/dist/snapshot/snapshot.d.ts +0 -77
  225. package/dist/snapshot/snapshot.d.ts.map +0 -1
  226. package/dist/snapshot/snapshot.js +0 -1791
  227. package/dist/snapshot/snapshot.js.map +0 -1
  228. package/dist/tools/web-tool.d.ts +0 -46
  229. package/dist/tools/web-tool.d.ts.map +0 -1
  230. package/dist/tools/web-tool.js.map +0 -1
  231. /package/dist/{core → flow}/agents.d.ts +0 -0
  232. /package/dist/{core → flow}/session-registry.d.ts +0 -0
  233. /package/dist/{core → flow}/session-registry.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 } 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) {
@@ -118,6 +316,24 @@ function sectionHeader(label) {
118
316
  const right = "─".repeat(Math.ceil(side));
119
317
  return `${left} ${label} ${right}`;
120
318
  }
319
+ /** Reconstruct multi-segment ANSI styles on a flat string by splitting at
320
+ * original segment boundaries and re-applying each segment's style function.
321
+ */
322
+ export function reconstructHeader(content, segments) {
323
+ let offset = 0;
324
+ const parts = [];
325
+ for (const seg of segments) {
326
+ const len = seg.text.length;
327
+ if (offset >= content.length)
328
+ break;
329
+ parts.push(seg.style(content.slice(offset, offset + len)));
330
+ offset += len;
331
+ }
332
+ if (offset < content.length) {
333
+ parts.push(content.slice(offset));
334
+ }
335
+ return parts.join("");
336
+ }
121
337
  // ---------------------------------------------------------------------------
122
338
  // renderFlowCall — shown while the flow is being invoked
123
339
  // ---------------------------------------------------------------------------
@@ -158,13 +374,13 @@ export function renderFlowResult(result, expanded, theme, args, config) {
158
374
  let resolvedToolCallId;
159
375
  if (args?.state) {
160
376
  const s = args.state;
161
- resolvedToolCallId = s.__flowId;
377
+ resolvedToolCallId = s.__widgetId;
162
378
  if (!resolvedToolCallId) {
163
379
  resolvedToolCallId = result._toolCallId || args?.toolCallId || args?.id;
164
380
  if (!resolvedToolCallId) {
165
381
  resolvedToolCallId = getAnonymousFlowId();
166
382
  }
167
- s.__flowId = resolvedToolCallId;
383
+ s.__widgetId = resolvedToolCallId;
168
384
  }
169
385
  }
170
386
  else {
@@ -257,6 +473,140 @@ export function renderFlowResult(result, expanded, theme, args, config) {
257
473
  return container;
258
474
  }
259
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
+ // ---------------------------------------------------------------------------
260
610
  // Single flow result
261
611
  // ---------------------------------------------------------------------------
262
612
  export function renderSingleFlowResult(r, expanded, theme, streamingText, toolCallId, config) {
@@ -266,7 +616,7 @@ export function renderSingleFlowResult(r, expanded, theme, streamingText, toolCa
266
616
  const displayItems = getFlowDisplayItems(r.messages);
267
617
  const flowOutput = getFlowOutput(r.messages);
268
618
  const now = Date.now();
269
- const isComplete = r.exitCode !== -1;
619
+ const isComplete = isFlowStatusComplete(r);
270
620
  if (expanded) {
271
621
  return renderFlowExpanded(r, icon, error, displayItems, flowOutput, theme, id, now, isComplete, streamingText, config);
272
622
  }
@@ -275,15 +625,25 @@ export function renderSingleFlowResult(r, expanded, theme, streamingText, toolCa
275
625
  function renderFlowExpanded(r, icon, error, displayItems, flowOutput, theme, id, now, isComplete, streamingText, config) {
276
626
  const mdTheme = getMarkdownTheme();
277
627
  const container = new Container();
278
- // Header: uppercase type name with dots, no icon, no source
279
628
  const typeName = formatFlowTypeName(r.type);
280
- let header = applyRole("flowName", typeName, theme, config);
281
- if (error && r.stopReason)
282
- header += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
283
- const plainHeader = typeName + (error && r.stopReason ? ` [${r.stopReason}]` : "");
629
+ const initialDot = flowStatusIcon(r, theme);
630
+ let header = `${initialDot} ${applyRole("flowName", typeName, theme, config)}`;
631
+ const errorSegment = error && r.stopReason ? ` [${r.stopReason}]` : "";
632
+ if (errorSegment)
633
+ header += ` ${theme.fg("error", errorSegment)}`;
634
+ const dotPlaceholder = stripAnsi(initialDot) + ' ';
635
+ const plainHeader = dotPlaceholder + typeName + errorSegment;
636
+ const headerSegments = [
637
+ { text: dotPlaceholder, style: (_s) => getScintillatingStatusDot(r, theme, Date.now(), id) + " " },
638
+ { text: typeName, style: (s) => applyRole("flowName", s, theme, config) },
639
+ ];
640
+ if (errorSegment) {
641
+ headerSegments.push({ text: errorSegment, style: (s) => theme.fg("error", s) });
642
+ }
284
643
  container.addChild(new DynamicScrambleText(header, () => {
285
- const result = scrambleManager.updateText(id, 'header', plainHeader, Date.now(), isComplete);
286
- return result.isAnimating ? applyRole("flowName", result.content, theme, config) : header;
644
+ const now = Date.now();
645
+ const result = scrambleManager.updateText(id, 'header', plainHeader, now, isComplete);
646
+ return reconstructHeader(result.content, headerSegments);
287
647
  }));
288
648
  if (error && r.errorMessage) {
289
649
  container.addChild(new Text(scrambleManager.renderStatic(theme.fg("error", `Error: ${r.errorMessage}`)), 0, 0));
@@ -374,7 +734,10 @@ function renderFlowExpanded(r, icon, error, displayItems, flowOutput, theme, id,
374
734
  container.addChild(new Spacer(1));
375
735
  }
376
736
  // Output: animate streaming text; show clean markdown when complete
377
- 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) {
378
741
  const msgBudget = getTruncationBudget(0);
379
742
  const displayMsg = tailText(stripAnsi(streamingText), msgBudget);
380
743
  container.addChild(new DynamicScrambleText(displayMsg, () => {
@@ -427,36 +790,47 @@ function renderFlowCollapsed(r, icon, error, flowOutput, theme, streamingText, t
427
790
  const maxWidth = process.stdout.columns ?? 80;
428
791
  const typeName = formatCollapsedFlowHeaderTypeName(r.type);
429
792
  const modelLabel = formatModelLabel(r.model);
430
- const headerPrefixLen = visibleLength(typeName) + visibleLength(modelLabel ? ` ${modelLabel} · ` : " ");
431
- const isComplete = r.exitCode !== -1;
793
+ const headerPrefixLen = visibleLength(typeName) + visibleLength(modelLabel ? ` ${modelLabel} · ` : " ");
794
+ const isComplete = isFlowStatusComplete(r);
432
795
  // Build header stats: ctxLabel · t/s
433
796
  const statsParts = [];
434
797
  if (r.maxContextTokens !== undefined || r.usage.contextTokens > 0) {
435
798
  const ctxLabel = formatContextLabel(r.usage.contextTokens, r.maxContextTokens);
436
799
  statsParts.push(ctxLabel);
437
800
  }
438
- const tpsValue = r.usage.smoothedTps;
439
- const tpsDisplay = tpsValue && tpsValue >= 100 ? `${Math.round(tpsValue)}` : (tpsValue && tpsValue > 0 ? tpsValue.toFixed(1) : undefined);
440
- if (tpsDisplay)
441
- statsParts.push(`${tpsDisplay} t/s`);
442
- else
443
- statsParts.push("---- t/s");
801
+ const tpsFormatted = formatTps(r.usage.smoothedTps);
802
+ statsParts.push(tpsFormatted);
444
803
  let displayStats = statsParts.join(" · ");
445
804
  // Flash TPS value when it changes
446
- if (tpsDisplay) {
447
- const scrambledTps = scrambleManager.updateTps(id, tpsDisplay, now, isComplete, true);
448
- if (scrambledTps !== tpsDisplay) {
449
- displayStats = displayStats.replace(`${tpsDisplay} t/s`, `${scrambledTps} t/s`);
805
+ const tpsNum = tpsFormatted.slice(0, -4); // remove " t/s" suffix
806
+ if (r.usage.smoothedTps && r.usage.smoothedTps > 0) {
807
+ const scrambledTps = scrambleManager.updateTps(id, tpsNum, now, isComplete, true);
808
+ if (scrambledTps !== tpsNum) {
809
+ displayStats = displayStats.replace(`${tpsNum} t/s`, `${scrambledTps} t/s`);
450
810
  }
451
811
  }
452
- let header = `${applyRole("flowName", typeName, theme, config)}${applyRole("modelName", modelLabel ? ` ${modelLabel} · ` : " ", theme, config)}${applyRole("stats", displayStats, theme, config)}`;
453
- if (error && r.stopReason)
454
- header += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
455
- // Scramble header on first render; show full styled header when complete
456
- const plainHeader = typeName + (modelLabel ? ` ${modelLabel} · ` : " ") + stripAnsi(displayStats) + (error && r.stopReason ? ` [${r.stopReason}]` : "");
812
+ const modelSegment = modelLabel ? ` ${modelLabel} · ` : " ";
813
+ const statsSegment = stripAnsi(displayStats);
814
+ const errorSegment = error && r.stopReason ? ` [${r.stopReason}]` : "";
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)}`;
817
+ if (errorSegment)
818
+ header += ` ${theme.fg("error", errorSegment)}`;
819
+ const dotPlaceholder = stripAnsi(initialDot) + ' ';
820
+ const plainHeader = dotPlaceholder + typeName + modelSegment + statsSegment + errorSegment;
821
+ const headerSegments = [
822
+ { text: dotPlaceholder, style: (_s) => getScintillatingStatusDot(r, theme, Date.now(), id) + " " },
823
+ { text: typeName, style: (s) => applyRole("flowName", s, theme, config) },
824
+ { text: modelSegment, style: (s) => applyRole("modelName", s, theme, config) },
825
+ { text: statsSegment, style: (s) => applyRole("stats", s, theme, config) },
826
+ ];
827
+ if (errorSegment) {
828
+ headerSegments.push({ text: errorSegment, style: (s) => theme.fg("error", s) });
829
+ }
457
830
  container.addChild(new DynamicScrambleText(header, () => {
458
- const result = scrambleManager.updateText(id, 'header', plainHeader, Date.now(), isComplete, true);
459
- return result.isAnimating ? applyRole("flowName", result.content, theme, config) : header;
831
+ const now = Date.now();
832
+ const result = scrambleManager.updateText(id, 'header', plainHeader, now, isComplete, true);
833
+ return reconstructHeader(result.content, headerSegments);
460
834
  }, true));
461
835
  // aim: line — glitch on text change
462
836
  if (r.aim) {
@@ -464,77 +838,102 @@ function renderFlowCollapsed(r, icon, error, flowOutput, theme, streamingText, t
464
838
  const aimLabel = ` aim ▸ `;
465
839
  const aimPrefix = `${aimTree}${aimLabel}`;
466
840
  const budget = getTruncationBudget(visibleLength(aimPrefix));
467
- const displayAim = truncateChars(lowerFirstWord(r.aim), budget);
468
- 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)}`, () => {
469
843
  const now = Date.now();
470
844
  const freshAimLabel = ` aim ▸ `;
471
845
  const freshAimPrefix = `${aimTree}${freshAimLabel}`;
472
846
  const freshBudget = getTruncationBudget(visibleLength(freshAimPrefix));
473
- const freshText = truncateChars(lowerFirstWord(r.aim), freshBudget);
847
+ const freshText = isFlowAwaiting(r) ? "[awaiting...]" : truncateChars(lowerFirstWord(r.aim), freshBudget);
474
848
  const result = scrambleManager.updateAim(id, freshText, now, isComplete, true);
475
- 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)}`;
476
850
  }, true));
477
851
  }
478
852
  // act: line (last tool call with count)
479
853
  const lastTool = getLastToolCall(r.messages);
480
854
  const actStr = lastTool ? formatFlowToolCall(lastTool.name, lastTool.args, theme.fg.bind(theme)) : "[n/a]";
481
- const actTree = "├─";
855
+ const isLite = config?.bodyVerbosity !== "full";
856
+ const actTree = isLite ? "└─" : "├─";
482
857
  const actLabel = ` cmd ▸ `;
483
858
  const prefixStub = `${actTree}${actLabel}`;
484
859
  const budget = getTruncationBudget(visibleLength(prefixStub));
485
860
  const actFullText = stripAnsi(lowerFirstWord(actStr));
486
- const initialActContent = actFullText.length > budget ? tailText(actFullText, budget) : actFullText;
487
- 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)}`, () => {
488
863
  const now = Date.now();
489
- const displayAct = tailText(actFullText, budget);
490
- const actContent = scrambleManager.updateAct(id, displayAct, now, isComplete, true).content;
491
864
  const actLabel = ` cmd ▸ `;
492
865
  const actPrefix = `${actTree}${actLabel}`;
493
- 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)}`;
494
870
  }, true));
495
- // msg: line (last assistant text or streaming)
496
- const msgPrefixStub = `└─ msg ▸ `;
497
- const msgBudget = getTruncationBudget(visibleLength(msgPrefixStub));
498
- let rawMsg;
499
- let useError = false;
500
- const liveMsgText = r.exitCode === -1 ? getLiveTextWithFallback(id) : undefined;
501
- if (liveMsgText != null) {
502
- rawMsg = stripAnsi(liveMsgText);
503
- }
504
- else if (r.exitCode === -1 && streamingText != null) {
505
- rawMsg = stripAnsi(streamingText);
506
- }
507
- else if (r.structuredOutput?.summary) {
508
- rawMsg = stripAnsi(r.structuredOutput.summary);
509
- }
510
- else if (flowOutput) {
511
- rawMsg = stripAnsi(flowOutput);
512
- }
513
- else if (error && r.errorMessage) {
514
- rawMsg = stripAnsi(r.errorMessage);
515
- useError = true;
516
- }
517
- else {
518
- const summary = getFlowSummaryText(r);
519
- rawMsg = stripAnsi(summary) || "[n/a]";
520
- }
521
- const initialNeedsTail = r.exitCode === -1 || streamingText != null || liveMsgText != null;
522
- const initialMsgContent = initialNeedsTail
523
- ? tailText(rawMsg, msgBudget)
524
- : truncateChars(rawMsg, msgBudget);
525
- const msgTree = "└─";
526
- const msgLabel = ` msg ▸ `;
527
- const initialMsgPrefix = `${msgTree}${msgLabel}`;
528
- container.addChild(new DynamicScrambleText(`${applyRole("treeChars", msgTree, theme, config)}${applyRole("prefixLabel", msgLabel, theme, config)}${applyRole(useError ? "msgError" : "msgContent", initialMsgContent, theme, config)}`, () => {
529
- 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 = "└─";
530
911
  const msgLabel = ` msg ▸ `;
531
- const msgPrefix = `${msgTree}${msgLabel}`;
532
- const freshRawMsg = (r.exitCode === -1 ? getLiveTextWithFallback(id) : undefined) ?? rawMsg;
533
- const needsTail = r.exitCode === -1 || streamingText != null;
534
- const displayMsg = needsTail ? tailText(freshRawMsg, msgBudget) : truncateChars(freshRawMsg, msgBudget);
535
- const result = scrambleManager.updateMsg(id, displayMsg, now, isComplete, undefined, true);
536
- return `${applyRole("treeChars", msgTree, theme, config)}${applyRole("prefixLabel", msgLabel, theme, config)}${applyRole(useError ? "msgError" : "msgContent", result.content, theme, config)}`;
537
- }, 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
+ }
538
937
  if (isComplete) {
539
938
  scrambleManager.completeFlow(id);
540
939
  }
@@ -563,7 +962,7 @@ function renderMultiFlowExpanded(results, successCount, icon, theme, baseId, now
563
962
  for (let flowIdx = 0; flowIdx < results.length; flowIdx++) {
564
963
  const r = results[flowIdx];
565
964
  const flowId = `${baseId}#${flowIdx}`;
566
- const isComplete = r.exitCode !== -1;
965
+ const isComplete = isFlowStatusComplete(r);
567
966
  const displayItems = getFlowDisplayItems(r.messages);
568
967
  const flowOutput = getFlowOutput(r.messages);
569
968
  const typeName = formatFlowTypeName(r.type);
@@ -603,7 +1002,10 @@ function renderMultiFlowExpanded(results, successCount, icon, theme, baseId, now
603
1002
  }));
604
1003
  }
605
1004
  // Output: animate streaming text; show clean markdown when complete
606
- 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) {
607
1009
  const streamingRaw = r.streamingText;
608
1010
  const msgBudget = getTruncationBudget(0);
609
1011
  const displayMsg = tailText(stripAnsi(streamingRaw), msgBudget);
@@ -654,131 +1056,264 @@ function renderMultiFlowExpanded(results, successCount, icon, theme, baseId, now
654
1056
  function renderActivityPanel(results, theme, baseId, config) {
655
1057
  const idPrefix = baseId || "panel";
656
1058
  const container = new Container();
657
- const maxWidth = process.stdout.columns ?? 80;
658
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 = [];
659
1066
  for (let i = 0; i < results.length; i++) {
660
- const r = results[i];
661
- const isLast = i === results.length - 1;
662
- const flowId = `${idPrefix}#${i}`;
663
- const typeName = formatCollapsedFlowHeaderTypeName(r.type);
664
- const modelLabel = formatModelLabel(r.model);
665
- const headerPrefix = isLast ? "└─" : "├─";
666
- const headerPrefixLen = visibleLength(headerPrefix) + 1 + visibleLength(typeName) + visibleLength(modelLabel ? ` ${modelLabel} · ` : " ");
667
- // 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
668
1166
  const statsParts = [];
669
1167
  if (r.maxContextTokens !== undefined || r.usage.contextTokens > 0) {
670
1168
  const ctxLabel = formatContextLabel(r.usage.contextTokens, r.maxContextTokens);
671
1169
  statsParts.push(ctxLabel);
672
1170
  }
673
- const tpsValue = r.usage.smoothedTps;
674
- const tpsDisplay = tpsValue && tpsValue >= 100 ? `${Math.round(tpsValue)}` : (tpsValue && tpsValue > 0 ? tpsValue.toFixed(1) : undefined);
675
- if (tpsDisplay)
676
- statsParts.push(`${tpsDisplay} t/s`);
677
- else
678
- statsParts.push("---- t/s");
1171
+ const tpsFormatted = formatTps(r.usage.smoothedTps);
1172
+ statsParts.push(tpsFormatted);
679
1173
  let displayStats = statsParts.join(" · ");
680
- const flowComplete = r.exitCode !== -1;
681
- // Flash TPS value when it changes
682
- if (tpsDisplay) {
683
- const scrambledTps = scrambleManager.updateTps(flowId, tpsDisplay, now, flowComplete, true);
684
- if (scrambledTps !== tpsDisplay) {
685
- displayStats = displayStats.replace(`${tpsDisplay} t/s`, `${scrambledTps} t/s`);
686
- }
687
- }
688
- const error = isFlowError(r);
689
- // Header line
690
- let headerLine = `${applyRole("treeChars", headerPrefix, theme, config)} ${applyRole("flowName", typeName, theme, config)}${applyRole("modelName", modelLabel ? ` ${modelLabel} · ` : " ", theme, config)}${applyRole("stats", displayStats, theme, config)}`;
691
- if (error && r.stopReason) {
692
- headerLine += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
693
- }
694
- const plainHeader = headerPrefix + " " + typeName + (modelLabel ? ` ${modelLabel} · ` : " ") + stripAnsi(displayStats) + (error && r.stopReason ? ` [${r.stopReason}]` : "");
695
- container.addChild(new DynamicScrambleText(headerLine, () => {
696
- const result = scrambleManager.updateText(flowId, 'header', plainHeader, Date.now(), flowComplete, true);
697
- return result.isAnimating ? applyRole("flowName", result.content, theme, config) : headerLine;
698
- }, true));
699
- // Continuation indent for sub-lines
700
- const indent = isLast ? " " : "│ ";
701
- // aim: line — glitch on text change
702
- if (r.aim) {
703
- const aimTree = indent + "├─";
704
- const aimLabel = ` aim ▸ `;
705
- const aimPrefix = `${aimTree}${aimLabel}`;
706
- const budget = getTruncationBudget(visibleLength(aimPrefix));
707
- const displayAim = truncateChars(lowerFirstWord(r.aim), budget);
708
- container.addChild(new DynamicScrambleText(`${applyRole("treeChars", aimTree, theme, config)}${applyRole("prefixLabel", aimLabel, theme, config)}${applyRole("aimContent", displayAim, theme, config)}`, () => {
709
- const now = Date.now();
710
- const freshAimLabel = ` aim ▸ `;
711
- const freshAimPrefix = `${aimTree}${freshAimLabel}`;
712
- const freshBudget = getTruncationBudget(visibleLength(freshAimPrefix));
713
- const freshText = truncateChars(lowerFirstWord(r.aim), freshBudget);
714
- const result = scrambleManager.updateAim(flowId, freshText, now, flowComplete, true);
715
- return `${applyRole("treeChars", aimTree, theme, config)}${applyRole("prefixLabel", freshAimLabel, theme, config)}${applyRole("aimContent", result.content, theme, config)}`;
716
- }, true));
717
- }
718
- // act: line (last tool call with count)
719
- const lastTool = getLastToolCall(r.messages);
720
- const actStr = lastTool ? formatFlowToolCall(lastTool.name, lastTool.args, theme.fg.bind(theme)) : "[n/a]";
721
- const actTree = `${indent}├─`;
722
- const actLabel = ` cmd ▸ `;
723
- const prefixStub = `${actTree}${actLabel}`;
724
- const budget = getTruncationBudget(visibleLength(prefixStub));
725
- const actFullText = stripAnsi(lowerFirstWord(actStr));
726
- const initialActContent = actFullText.length > budget ? tailText(actFullText, budget) : actFullText;
727
- container.addChild(new DynamicScrambleText(`${applyRole("treeChars", actTree, theme, config)}${applyRole("prefixLabel", actLabel, theme, config)}${applyRole("actContent", initialActContent, theme, config)}`, () => {
1174
+ const tpsNum = tpsFormatted.slice(0, -4); // remove " t/s" suffix
1175
+ if (r.usage.smoothedTps && r.usage.smoothedTps > 0) {
1176
+ const scrambledTps = scrambleManager.updateTps(flowId, tpsNum, now, flowComplete, true);
1177
+ if (scrambledTps !== tpsNum) {
1178
+ displayStats = displayStats.replace(`${tpsNum} t/s`, `${scrambledTps} t/s`);
1179
+ }
1180
+ }
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)}`;
1185
+ if (errorSegment) {
1186
+ headerLine += ` ${theme.fg("error", errorSegment)}`;
1187
+ }
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)}`, () => {
728
1211
  const now = Date.now();
729
- const actLabel = ` cmd ▸ `;
730
- const actPrefix = `${actTree}${actLabel}`;
731
- const freshBudget = getTruncationBudget(visibleLength(actPrefix));
732
- const displayAct = tailText(actFullText, freshBudget);
733
- const actContent = scrambleManager.updateAct(flowId, displayAct, now, flowComplete, true).content;
734
- 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)}`;
735
1218
  }, true));
736
- // 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) {
737
1241
  const msgTree = `${indent}└─`;
738
1242
  const msgLabel = ` msg ▸ `;
739
1243
  const msgPrefixStub = `${msgTree}${msgLabel}`;
740
1244
  const msgBudget = getTruncationBudget(visibleLength(msgPrefixStub));
741
- const liveText = r.exitCode === -1 ? r.streamingText : undefined;
742
- const lastText = liveText || getLastAssistantText(r.messages);
743
1245
  let rawMsg;
744
1246
  let useError = false;
745
- const liveText_ = flowComplete ? undefined : getLiveTextWithFallback(flowId);
746
- if (liveText_ != null) {
747
- rawMsg = stripAnsi(liveText_);
1247
+ if (isFlowAwaiting(r)) {
1248
+ rawMsg = "[awaiting...]";
748
1249
  }
749
- else if (lastText) {
750
- rawMsg = stripAnsi(lastText);
1250
+ else if (r.status === "skipped") {
1251
+ rawMsg = "[skipped]";
751
1252
  }
752
- else if (error && r.errorMessage) {
753
- rawMsg = stripAnsi(r.errorMessage);
754
- 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
+ }
755
1264
  }
756
1265
  else {
757
- 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
+ }
758
1290
  }
759
- const initialNeedsTail = Boolean(liveText_ || liveText || lastText);
1291
+ const initialNeedsTail = !isFlowAwaiting(r) && isFlowRunning(r) && (r.streamingText != null || getLiveTextWithFallback(flowId) != null);
760
1292
  const initialDisplayMsg = initialNeedsTail ? tailText(rawMsg, msgBudget) : truncateChars(rawMsg, msgBudget);
761
- 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)}`, () => {
762
1294
  const now = Date.now();
763
1295
  const msgLabel = ` msg ▸ `;
764
1296
  const msgPrefix = `${msgTree}${msgLabel}`;
765
- const freshBudget = getTruncationBudget(visibleLength(msgPrefix));
766
- const freshRawMsg = flowComplete ? rawMsg : (getLiveTextWithFallback(flowId) ?? rawMsg);
767
- const needsTail = Boolean(getLiveTextWithFallback(flowId) || liveText || lastText);
768
- 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);
769
1313
  const result = scrambleManager.updateMsg(flowId, displayMsg, now, flowComplete, undefined, true);
770
- 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)}`;
771
1315
  }, true));
772
- if (flowComplete) {
773
- scrambleManager.completeFlow(flowId);
774
- }
775
- // Add blank line separator between flows (with continuation pipe)
776
- if (!isLast) {
777
- container.addChild(new TruncatedText(applyRole("treeChars", "│", theme, config), 0, 0));
778
- }
779
1316
  }
780
- container.addChild(new TruncatedText(applyRole("prefixLabel", "(Ctrl+O to expand tool traces)", theme, config), 0, 0));
781
- return container;
782
1317
  }
783
1318
  function renderMultiFlowCollapsed(results, theme, baseId, config) {
784
1319
  return renderActivityPanel(results, theme, baseId, config);