pi-agent-flow 2.2.14 → 2.2.16

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 (254) hide show
  1. package/README.md +12 -0
  2. package/dist/batch/apply-patch.d.ts +2 -1
  3. package/dist/batch/apply-patch.d.ts.map +1 -1
  4. package/dist/batch/batch-bash.d.ts.map +1 -1
  5. package/dist/batch/batch-bash.js +9 -43
  6. package/dist/batch/batch-bash.js.map +1 -1
  7. package/dist/batch/constants.d.ts +9 -1
  8. package/dist/batch/constants.d.ts.map +1 -1
  9. package/dist/batch/constants.js +7 -0
  10. package/dist/batch/constants.js.map +1 -1
  11. package/dist/batch/execute.d.ts +1 -13
  12. package/dist/batch/execute.d.ts.map +1 -1
  13. package/dist/batch/execute.js +87 -28
  14. package/dist/batch/execute.js.map +1 -1
  15. package/dist/batch/index.d.ts.map +1 -1
  16. package/dist/batch/index.js +16 -14
  17. package/dist/batch/index.js.map +1 -1
  18. package/dist/batch/render.d.ts.map +1 -1
  19. package/dist/batch/render.js.map +1 -1
  20. package/dist/batch/summary.d.ts +0 -6
  21. package/dist/batch/summary.d.ts.map +1 -1
  22. package/dist/batch/summary.js +3 -3
  23. package/dist/batch/summary.js.map +1 -1
  24. package/dist/batch/symbols.d.ts.map +1 -1
  25. package/dist/batch/symbols.js +3 -1
  26. package/dist/batch/symbols.js.map +1 -1
  27. package/dist/batch/truncate-output.d.ts +31 -0
  28. package/dist/batch/truncate-output.d.ts.map +1 -0
  29. package/dist/batch/truncate-output.js +120 -0
  30. package/dist/batch/truncate-output.js.map +1 -0
  31. package/dist/config/config.d.ts +2 -1
  32. package/dist/config/config.d.ts.map +1 -1
  33. package/dist/config/config.js +10 -24
  34. package/dist/config/config.js.map +1 -1
  35. package/dist/config/models.d.ts.map +1 -1
  36. package/dist/config/models.js +7 -5
  37. package/dist/config/models.js.map +1 -1
  38. package/dist/config/paths.d.ts +3 -0
  39. package/dist/config/paths.d.ts.map +1 -0
  40. package/dist/config/paths.js +9 -0
  41. package/dist/config/paths.js.map +1 -0
  42. package/dist/config/resolver-helpers.d.ts +44 -0
  43. package/dist/config/resolver-helpers.d.ts.map +1 -0
  44. package/dist/config/resolver-helpers.js +135 -0
  45. package/dist/config/resolver-helpers.js.map +1 -0
  46. package/dist/config/settings-resolver.d.ts +0 -6
  47. package/dist/config/settings-resolver.d.ts.map +1 -1
  48. package/dist/config/settings-resolver.js +27 -256
  49. package/dist/config/settings-resolver.js.map +1 -1
  50. package/dist/core2/snapshot.d.ts +0 -2
  51. package/dist/core2/snapshot.d.ts.map +1 -1
  52. package/dist/core2/snapshot.js +91 -24
  53. package/dist/core2/snapshot.js.map +1 -1
  54. package/dist/flow/agents.d.ts +1 -1
  55. package/dist/flow/agents.d.ts.map +1 -1
  56. package/dist/flow/agents.js +18 -9
  57. package/dist/flow/agents.js.map +1 -1
  58. package/dist/flow/audit-formatters.d.ts +20 -0
  59. package/dist/flow/audit-formatters.d.ts.map +1 -0
  60. package/dist/flow/audit-formatters.js +123 -0
  61. package/dist/flow/audit-formatters.js.map +1 -0
  62. package/dist/flow/complexity.d.ts +3 -2
  63. package/dist/flow/complexity.d.ts.map +1 -1
  64. package/dist/flow/complexity.js +1 -1
  65. package/dist/flow/complexity.js.map +1 -1
  66. package/dist/flow/continuation.d.ts.map +1 -1
  67. package/dist/flow/continuation.js +6 -4
  68. package/dist/flow/continuation.js.map +1 -1
  69. package/dist/flow/cycle-guard.d.ts +20 -0
  70. package/dist/flow/cycle-guard.d.ts.map +1 -0
  71. package/dist/flow/cycle-guard.js +56 -0
  72. package/dist/flow/cycle-guard.js.map +1 -0
  73. package/dist/flow/depth.d.ts +0 -1
  74. package/dist/flow/depth.d.ts.map +1 -1
  75. package/dist/flow/depth.js +3 -2
  76. package/dist/flow/depth.js.map +1 -1
  77. package/dist/flow/dump-io.d.ts +15 -0
  78. package/dist/flow/dump-io.d.ts.map +1 -0
  79. package/dist/flow/dump-io.js +169 -0
  80. package/dist/flow/dump-io.js.map +1 -0
  81. package/dist/flow/execute-single.d.ts +8 -0
  82. package/dist/flow/execute-single.d.ts.map +1 -0
  83. package/dist/flow/execute-single.js +132 -0
  84. package/dist/flow/execute-single.js.map +1 -0
  85. package/dist/flow/executor.d.ts +3 -62
  86. package/dist/flow/executor.d.ts.map +1 -1
  87. package/dist/flow/executor.js +36 -342
  88. package/dist/flow/executor.js.map +1 -1
  89. package/dist/flow/flow-args.d.ts +14 -0
  90. package/dist/flow/flow-args.d.ts.map +1 -0
  91. package/dist/flow/flow-args.js +119 -0
  92. package/dist/flow/flow-args.js.map +1 -0
  93. package/dist/flow/flow-live.d.ts +18 -0
  94. package/dist/flow/flow-live.d.ts.map +1 -0
  95. package/dist/flow/flow-live.js +46 -0
  96. package/dist/flow/flow-live.js.map +1 -0
  97. package/dist/flow/process-lifecycle.d.ts +28 -0
  98. package/dist/flow/process-lifecycle.d.ts.map +1 -0
  99. package/dist/flow/process-lifecycle.js +114 -0
  100. package/dist/flow/process-lifecycle.js.map +1 -0
  101. package/dist/flow/runner.d.ts +4 -54
  102. package/dist/flow/runner.d.ts.map +1 -1
  103. package/dist/flow/runner.js +55 -517
  104. package/dist/flow/runner.js.map +1 -1
  105. package/dist/flow/settings-command.d.ts +2 -24
  106. package/dist/flow/settings-command.d.ts.map +1 -1
  107. package/dist/flow/settings-command.js +14 -716
  108. package/dist/flow/settings-command.js.map +1 -1
  109. package/dist/flow/settings-handler.d.ts +9 -0
  110. package/dist/flow/settings-handler.d.ts.map +1 -0
  111. package/dist/flow/settings-handler.js +377 -0
  112. package/dist/flow/settings-handler.js.map +1 -0
  113. package/dist/flow/settings-items.d.ts +42 -0
  114. package/dist/flow/settings-items.d.ts.map +1 -0
  115. package/dist/flow/settings-items.js +380 -0
  116. package/dist/flow/settings-items.js.map +1 -0
  117. package/dist/flow/spawn-utils.d.ts +17 -0
  118. package/dist/flow/spawn-utils.d.ts.map +1 -0
  119. package/dist/flow/spawn-utils.js +40 -0
  120. package/dist/flow/spawn-utils.js.map +1 -0
  121. package/dist/flow/store.d.ts.map +1 -1
  122. package/dist/flow/store.js +2 -8
  123. package/dist/flow/store.js.map +1 -1
  124. package/dist/flow/transition.d.ts +0 -1
  125. package/dist/flow/transition.d.ts.map +1 -1
  126. package/dist/flow/transition.js +0 -3
  127. package/dist/flow/transition.js.map +1 -1
  128. package/dist/index.d.ts +3 -0
  129. package/dist/index.d.ts.map +1 -1
  130. package/dist/index.js +94 -57
  131. package/dist/index.js.map +1 -1
  132. package/dist/io/atomic-write.d.ts +10 -0
  133. package/dist/io/atomic-write.d.ts.map +1 -0
  134. package/dist/io/atomic-write.js +30 -0
  135. package/dist/io/atomic-write.js.map +1 -0
  136. package/dist/notify/notify.d.ts.map +1 -1
  137. package/dist/notify/notify.js +1 -4
  138. package/dist/notify/notify.js.map +1 -1
  139. package/dist/snapshot/cli-args.d.ts +1 -2
  140. package/dist/snapshot/cli-args.d.ts.map +1 -1
  141. package/dist/snapshot/runner-events.d.ts +10 -0
  142. package/dist/snapshot/runner-events.d.ts.map +1 -1
  143. package/dist/snapshot/runner-events.js +112 -30
  144. package/dist/snapshot/runner-events.js.map +1 -1
  145. package/dist/snapshot/structured-output.d.ts.map +1 -1
  146. package/dist/snapshot/structured-output.js +3 -1
  147. package/dist/snapshot/structured-output.js.map +1 -1
  148. package/dist/snapshot/trace-output.d.ts.map +1 -1
  149. package/dist/snapshot/trace-output.js +8 -4
  150. package/dist/snapshot/trace-output.js.map +1 -1
  151. package/dist/steering/sliding-prompt.d.ts.map +1 -1
  152. package/dist/steering/sliding-prompt.js +12 -15
  153. package/dist/steering/sliding-prompt.js.map +1 -1
  154. package/dist/steering/tool-utils.d.ts +3 -3
  155. package/dist/steering/tool-utils.d.ts.map +1 -1
  156. package/dist/steering/tool-utils.js +3 -3
  157. package/dist/steering/tool-utils.js.map +1 -1
  158. package/dist/tools/ask-user.js.map +1 -1
  159. package/dist/tools/timed-bash.d.ts.map +1 -1
  160. package/dist/tools/timed-bash.js +8 -3
  161. package/dist/tools/timed-bash.js.map +1 -1
  162. package/dist/tools/trace.d.ts +0 -1
  163. package/dist/tools/trace.d.ts.map +1 -1
  164. package/dist/tools/trace.js +79 -62
  165. package/dist/tools/trace.js.map +1 -1
  166. package/dist/tools/web-ops.d.ts +0 -49
  167. package/dist/tools/web-ops.d.ts.map +1 -1
  168. package/dist/tools/web-ops.js +9 -9
  169. package/dist/tools/web-ops.js.map +1 -1
  170. package/dist/tui/body-render.d.ts +31 -0
  171. package/dist/tui/body-render.d.ts.map +1 -0
  172. package/dist/tui/body-render.js +589 -0
  173. package/dist/tui/body-render.js.map +1 -0
  174. package/dist/tui/context-display.d.ts +18 -0
  175. package/dist/tui/context-display.d.ts.map +1 -0
  176. package/dist/tui/context-display.js +29 -0
  177. package/dist/tui/context-display.js.map +1 -0
  178. package/dist/tui/flow-colors.d.ts +6 -1
  179. package/dist/tui/flow-colors.d.ts.map +1 -1
  180. package/dist/tui/flow-colors.js +1 -0
  181. package/dist/tui/flow-colors.js.map +1 -1
  182. package/dist/tui/flow-live-state.d.ts +36 -0
  183. package/dist/tui/flow-live-state.d.ts.map +1 -0
  184. package/dist/tui/flow-live-state.js +53 -0
  185. package/dist/tui/flow-live-state.js.map +1 -0
  186. package/dist/tui/grouping.d.ts +27 -0
  187. package/dist/tui/grouping.d.ts.map +1 -0
  188. package/dist/tui/grouping.js +191 -0
  189. package/dist/tui/grouping.js.map +1 -0
  190. package/dist/tui/header.d.ts +25 -0
  191. package/dist/tui/header.d.ts.map +1 -0
  192. package/dist/tui/header.js +95 -0
  193. package/dist/tui/header.js.map +1 -0
  194. package/dist/tui/render-utils.d.ts +1 -0
  195. package/dist/tui/render-utils.d.ts.map +1 -1
  196. package/dist/tui/render-utils.js +4 -5
  197. package/dist/tui/render-utils.js.map +1 -1
  198. package/dist/tui/render.d.ts +19 -33
  199. package/dist/tui/render.d.ts.map +1 -1
  200. package/dist/tui/render.js +112 -1014
  201. package/dist/tui/render.js.map +1 -1
  202. package/dist/tui/scramble/animation-status.d.ts +5 -0
  203. package/dist/tui/scramble/animation-status.d.ts.map +1 -0
  204. package/dist/tui/scramble/animation-status.js +93 -0
  205. package/dist/tui/scramble/animation-status.js.map +1 -0
  206. package/dist/tui/scramble/constants.d.ts +0 -4
  207. package/dist/tui/scramble/constants.d.ts.map +1 -1
  208. package/dist/tui/scramble/constants.js +3 -3
  209. package/dist/tui/scramble/constants.js.map +1 -1
  210. package/dist/tui/scramble/dynamic-text.d.ts +10 -0
  211. package/dist/tui/scramble/dynamic-text.d.ts.map +1 -0
  212. package/dist/tui/scramble/dynamic-text.js +19 -0
  213. package/dist/tui/scramble/dynamic-text.js.map +1 -0
  214. package/dist/tui/scramble/index.d.ts +8 -2
  215. package/dist/tui/scramble/index.d.ts.map +1 -1
  216. package/dist/tui/scramble/index.js +8 -1
  217. package/dist/tui/scramble/index.js.map +1 -1
  218. package/dist/tui/scramble/lifecycle.d.ts +4 -0
  219. package/dist/tui/scramble/lifecycle.d.ts.map +1 -0
  220. package/dist/tui/scramble/lifecycle.js +75 -0
  221. package/dist/tui/scramble/lifecycle.js.map +1 -0
  222. package/dist/tui/scramble/line-state.d.ts +5 -0
  223. package/dist/tui/scramble/line-state.d.ts.map +1 -0
  224. package/dist/tui/scramble/line-state.js +99 -0
  225. package/dist/tui/scramble/line-state.js.map +1 -0
  226. package/dist/tui/scramble/manager.d.ts +3 -14
  227. package/dist/tui/scramble/manager.d.ts.map +1 -1
  228. package/dist/tui/scramble/manager.js +22 -405
  229. package/dist/tui/scramble/manager.js.map +1 -1
  230. package/dist/tui/scramble/singleton.d.ts +3 -0
  231. package/dist/tui/scramble/singleton.d.ts.map +1 -0
  232. package/dist/tui/scramble/singleton.js +3 -0
  233. package/dist/tui/scramble/singleton.js.map +1 -0
  234. package/dist/tui/scramble/timer.d.ts +6 -0
  235. package/dist/tui/scramble/timer.d.ts.map +1 -0
  236. package/dist/tui/scramble/timer.js +28 -0
  237. package/dist/tui/scramble/timer.js.map +1 -0
  238. package/dist/tui/scramble/utils.d.ts +0 -2
  239. package/dist/tui/scramble/utils.d.ts.map +1 -1
  240. package/dist/tui/scramble/utils.js +0 -9
  241. package/dist/tui/scramble/utils.js.map +1 -1
  242. package/dist/tui/scramble/value-flash.d.ts +6 -0
  243. package/dist/tui/scramble/value-flash.d.ts.map +1 -0
  244. package/dist/tui/scramble/value-flash.js +141 -0
  245. package/dist/tui/scramble/value-flash.js.map +1 -0
  246. package/dist/tui/traces.d.ts +13 -0
  247. package/dist/tui/traces.d.ts.map +1 -0
  248. package/dist/tui/traces.js +97 -0
  249. package/dist/tui/traces.js.map +1 -0
  250. package/dist/types/ui.d.ts +0 -4
  251. package/dist/types/ui.d.ts.map +1 -1
  252. package/dist/types/ui.js +1 -30
  253. package/dist/types/ui.js.map +1 -1
  254. package/package.json +5 -1
@@ -4,14 +4,21 @@
4
4
  * Option B: collapsed view shows structured report (Summary/Done/Not Done/Next Steps).
5
5
  * Expanded view adds raw tool call traces.
6
6
  */
7
- import * as os from "node:os";
8
- import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
9
- import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
10
- import { getFlowSummaryText } from "../snapshot/runner-events.js";
11
- import { aggregateFlowUsage, getFlowOutput, isFlowError, isFlowSuccess, } from "../types/flow.js";
12
- import { getFlowDisplayItems, getLastToolCall, } from "../types/ui.js";
13
- import { formatBatchOpsSummary } from "../batch/summary.js";
14
- import { scrambleManager, runScrambleTimer, DynamicScrambleText, getLiveText, hashNoise, THIN_BRAILLE_SPARK } from "./scramble/index.js";
7
+ import { Container, Text } from "@earendil-works/pi-tui";
8
+ import { isFlowError, isFlowSuccess } from "../types/flow.js";
9
+ import { getFlowDisplayItems } from "../types/ui.js";
10
+ import { getFlowOutput } from "../types/flow.js";
11
+ import { scrambleManager, runScrambleTimer } from "./scramble/index.js";
12
+ import { buildBootPhaseSingleResult, getFlowLiveState } from "./flow-live-state.js";
13
+ import { applyRole } from "./flow-colors.js";
14
+ import { detectGroups, flowStatusIcon, isFlowStatusComplete, isFlowRunning, isFlowAwaiting, } from "./grouping.js";
15
+ import { renderFlowHeader, } from "./header.js";
16
+ import { renderFlowExpanded, renderFlowCollapsed, renderFlowBody, renderMultiFlowExpanded, } from "./body-render.js";
17
+ // Re-export all extracted symbols for backward compatibility
18
+ export { detectGroups, flowStatusIcon, getFlowStatus, isFlowStatusComplete, isFlowRunning, isFlowAwaiting, hashStrToSeed, getScintillatingStatusDot, } from "./grouping.js";
19
+ export { reconstructHeader, sectionHeader, renderFlowHeader, formatCollapsedFlowHeaderTypeName, } from "./header.js";
20
+ export { renderFlowExpanded, renderFlowCollapsed, renderFlowBody, renderMultiFlowExpanded, } from "./body-render.js";
21
+ export { getContentRole, applyScrambledContextLabel, getLiveTextWithFallback, shortenPath, formatFlowToolCall, splitOutputLines, renderToolTraces, renderFlowReport, } from "./traces.js";
15
22
  // ---------------------------------------------------------------------------
16
23
  // Anonymous flow-id counter — prevents scramble-state collisions when multiple
17
24
  // flow widgets share the screen and toolCallId is absent from result/args.
@@ -24,327 +31,6 @@ function getAnonymousFlowId() {
24
31
  export function resetAnonymousFlowIdCounter() {
25
32
  anonFlowIdCounter = 0;
26
33
  }
27
- function getContentRole(baseRole, text, useError) {
28
- if (useError)
29
- return "msgError";
30
- if (["[awaiting...]", "[skipped]", "[approved]", "[finished]"].includes(text)) {
31
- return "placeholder";
32
- }
33
- return baseRole;
34
- }
35
- function getLiveTextWithFallback(id) {
36
- const value = getLiveText(id);
37
- if (value !== undefined)
38
- return value;
39
- const fallbackId = id.includes("#") ? "collapsed" + id.slice(id.indexOf("#")) : "collapsed";
40
- return getLiveText(fallbackId);
41
- }
42
- import { formatCompactStats, formatFlowTypeName, lowerFirstWord, truncateChars, tailText, getTruncationBudget, visibleLength, stripAnsi, formatModelLabel, formatContextLabel, formatTps } from "./render-utils.js";
43
- function shortenPath(p) {
44
- const home = os.homedir();
45
- return p.startsWith(home) ? `~${p.slice(home.length)}` : p;
46
- }
47
- import { applyRole, } from "./flow-colors.js";
48
- function formatCollapsedFlowHeaderTypeName(type) {
49
- return type.toLowerCase();
50
- }
51
- function formatFlowToolCall(toolName, args, fg) {
52
- const pathArg = (args.file_path || args.path || "...");
53
- switch (toolName) {
54
- case "bash": {
55
- const cmd = (args.command || "...").replace(/[\n\r\t]+/g, " ").replace(/ +/g, " ").trim();
56
- return fg("muted", "$ ") + fg("toolOutput", cmd);
57
- }
58
- case "read": {
59
- let text = fg("accent", shortenPath(pathArg));
60
- const offset = args.offset;
61
- const limit = args.limit;
62
- if (offset !== undefined || limit !== undefined) {
63
- const start = offset ?? 1;
64
- const end = limit !== undefined ? start + limit - 1 : "";
65
- text += fg("warning", `:${start}${end ? `-${end}` : ""}`);
66
- }
67
- return fg("muted", "read ") + text;
68
- }
69
- case "write": {
70
- const lines = (args.content || "").split("\n").length;
71
- let text = fg("muted", "write ") + fg("accent", shortenPath(pathArg));
72
- if (lines > 1)
73
- text += fg("dim", ` (${lines} lines)`);
74
- return text;
75
- }
76
- case "edit":
77
- return fg("muted", "edit ") + fg("accent", shortenPath(pathArg));
78
- case "ls":
79
- return fg("muted", "ls ") + fg("accent", shortenPath((args.path || ".")));
80
- case "find":
81
- return fg("muted", "find ") + fg("accent", (args.pattern || "*")) + fg("dim", ` in ${shortenPath((args.path || "."))}`);
82
- case "grep":
83
- return fg("muted", "grep ") + fg("accent", `/${(args.pattern || "")}/`) + fg("dim", ` in ${shortenPath((args.path || "."))}`);
84
- case "batch":
85
- case "batch_read": {
86
- const summary = formatBatchOpsSummary(args);
87
- return fg("muted", `${toolName} `) + fg("accent", summary);
88
- }
89
- default:
90
- return fg("accent", toolName) + fg("dim", ` ${JSON.stringify(args)}`);
91
- }
92
- }
93
- // ---------------------------------------------------------------------------
94
- // Shared rendering building blocks
95
- // ---------------------------------------------------------------------------
96
- function splitOutputLines(text) {
97
- const lines = text.replace(/\r\n?/g, "\n").split("\n");
98
- if (lines.length > 1 && lines[lines.length - 1] === "")
99
- lines.pop();
100
- return lines;
101
- }
102
- function renderToolTraces(items, theme, config) {
103
- const lines = [];
104
- for (const item of items) {
105
- if (item.type === "toolCall") {
106
- lines.push(applyRole("prefixLabel", "→ ", theme, config) + formatFlowToolCall(item.name, item.args, theme.fg.bind(theme)));
107
- }
108
- }
109
- return lines.join("\n");
110
- }
111
- function renderFlowReport(output, theme, config) {
112
- const lines = splitOutputLines(output);
113
- return lines.map((line) => applyRole("actContent", line, theme, config)).join("\n");
114
- }
115
- function getFlowStatus(r) {
116
- return r.status ?? (r.exitCode === -1 ? "running" : r.exitCode === 0 ? "done" : "error");
117
- }
118
- function isFlowStatusComplete(r) {
119
- const status = getFlowStatus(r);
120
- return status === "done" || status === "error" || status === "skipped";
121
- }
122
- function isFlowRunning(r) {
123
- const status = getFlowStatus(r);
124
- return status === "running" || status === "pending";
125
- }
126
- function isFlowAwaiting(r) {
127
- return getFlowStatus(r) === "awaiting";
128
- }
129
- /**
130
- * Detect audit-loop groups.
131
- *
132
- * When the executor stamps `auditLoopGroupId` on results, grouping is
133
- * explicit and works regardless of array layout (no contiguity required).
134
- *
135
- * When no `auditLoopGroupId` is present (legacy / hand-crafted results),
136
- * we fall back to contiguity-based detection: N contiguous builds with
137
- * `pingPongMeta` followed immediately by an audit with
138
- * `auditParentType === "build"`.
139
- */
140
- export function detectGroups(results) {
141
- const groups = [];
142
- const rootIndices = [];
143
- // Phase 1: explicit grouping by auditLoopGroupId
144
- const groupMap = new Map();
145
- const ungroupedIndices = [];
146
- for (let i = 0; i < results.length; i++) {
147
- const r = results[i];
148
- if (r.auditLoopGroupId !== undefined) {
149
- let g = groupMap.get(r.auditLoopGroupId);
150
- if (!g) {
151
- g = { buildIndices: [], auditIndex: -1 };
152
- groupMap.set(r.auditLoopGroupId, g);
153
- }
154
- if (r.pingPongMeta) {
155
- g.buildIndices.push(i);
156
- }
157
- else if (r.auditParentType === "build") {
158
- g.auditIndex = i;
159
- }
160
- }
161
- else {
162
- ungroupedIndices.push(i);
163
- }
164
- }
165
- for (const g of groupMap.values()) {
166
- if (g.auditIndex !== -1) {
167
- groups.push({ buildIndices: g.buildIndices, auditIndex: g.auditIndex });
168
- }
169
- else {
170
- // Orphaned builds with groupId but no audit capstone
171
- rootIndices.push(...g.buildIndices);
172
- }
173
- }
174
- // Phase 2: legacy fallback on ungrouped results (contiguity-based)
175
- let i = 0;
176
- while (i < ungroupedIndices.length) {
177
- const idx = ungroupedIndices[i];
178
- const r = results[idx];
179
- if (r.pingPongMeta) {
180
- const buildIndices = [];
181
- while (i < ungroupedIndices.length && results[ungroupedIndices[i]].pingPongMeta) {
182
- buildIndices.push(ungroupedIndices[i]);
183
- i++;
184
- }
185
- if (i < ungroupedIndices.length && results[ungroupedIndices[i]].auditParentType === "build") {
186
- groups.push({ buildIndices, auditIndex: ungroupedIndices[i] });
187
- i++;
188
- }
189
- else {
190
- rootIndices.push(...buildIndices);
191
- }
192
- }
193
- else if (r.auditParentType === "build" && i > 0 && results[ungroupedIndices[i - 1]].pingPongMeta) {
194
- i++; // orphan audit already consumed
195
- }
196
- else {
197
- rootIndices.push(idx);
198
- i++;
199
- }
200
- }
201
- return { groups, rootIndices };
202
- }
203
- /**
204
- * Get the status icon dot for a result (● ○ ✗ ⊘).
205
- */
206
- function flowStatusIcon(r, theme) {
207
- const status = getFlowStatus(r);
208
- switch (status) {
209
- case "running":
210
- case "pending":
211
- return theme.fg("warning", "●");
212
- case "awaiting":
213
- return theme.fg("muted", "○");
214
- case "done":
215
- return theme.fg("success", "●");
216
- case "error":
217
- return theme.fg("error", "✗");
218
- case "skipped":
219
- return theme.fg("muted", "⊘");
220
- default:
221
- return theme.fg("muted", "?");
222
- }
223
- }
224
- function hashStrToSeed(s) {
225
- let h = 2166136261;
226
- for (let i = 0; i < s.length; i++) {
227
- h ^= s.charCodeAt(i);
228
- h = Math.imul(h, 16777619);
229
- }
230
- return h >>> 0;
231
- }
232
- function getScintillatingStatusDot(r, theme, now, flowId) {
233
- const status = getFlowStatus(r);
234
- switch (status) {
235
- case "running":
236
- case "pending": {
237
- const isPending = status === "pending";
238
- const seed = hashStrToSeed(flowId || r.type);
239
- const bucketSize = isPending ? 7000 : 5000;
240
- const bucket = Math.floor(now / bucketSize);
241
- const t = now % bucketSize;
242
- const burstCount = isPending
243
- ? 1 + Math.floor(hashNoise(seed, bucket, 0, 0x5a4f) * 2) // 1-2
244
- : 2 + Math.floor(hashNoise(seed, bucket, 0, 0x5a4f) * 2); // 2-3
245
- let cursor = 50;
246
- for (let b = 0; b < burstCount; b++) {
247
- const gap = isPending
248
- ? 800 + Math.floor(hashNoise(seed, bucket, b * 4, 0xb8a0) * 1400) // 800-2200ms
249
- : 500 + Math.floor(hashNoise(seed, bucket, b * 4, 0xb8a0) * 1300); // 500-1800ms
250
- cursor += gap;
251
- const duration = isPending
252
- ? 80 + Math.floor(hashNoise(seed, bucket, b * 4 + 1, 0xc0de) * 170) // 80-250ms
253
- : 100 + Math.floor(hashNoise(seed, bucket, b * 4 + 1, 0xc0de) * 250); // 100-350ms
254
- const burstStart = cursor;
255
- const burstEnd = cursor + duration;
256
- cursor = burstEnd;
257
- if (t >= burstStart && t < burstEnd) {
258
- const tInBurst = t - burstStart;
259
- const tick = 12 + Math.floor(hashNoise(seed, bucket, b * 4 + 3, 0xd1a0) * 10); // 12-22ms per stutter step
260
- // Vary stutter depth: 3-tick ○●○ or 5-tick ○●○●○ per burst
261
- const rawStutterTicks = hashNoise(seed, bucket, b * 4 + 2, 0xe7a1) > 0.5 ? 5 : 3;
262
- const stutterLen5 = tick * 5;
263
- const onRunMax5 = duration - stutterLen5 - 5;
264
- const stutterTicks = (rawStutterTicks === 5 && onRunMax5 >= tick) ? 5 : 3;
265
- const stutterLen = tick * stutterTicks;
266
- const onRunMax = duration - stutterLen - 5;
267
- const onRun = Math.max(tick, Math.min(Math.floor(duration * (0.35 + hashNoise(seed, bucket, b * 4 + 2, 0xf1c0) * 0.3)), onRunMax));
268
- const cycleLen = onRun + stutterLen;
269
- const phaseInCycle = tInBurst % cycleLen;
270
- const cycleIdx = Math.floor(tInBurst / cycleLen);
271
- // Helper: dip ○ with occasional sparkle
272
- const dipDot = (dipIndex) => {
273
- if (hashNoise(seed, bucket, cycleIdx + dipIndex * 100, 0x5ab0) < 0.05) {
274
- const sparkIdx = Math.floor(hashNoise(seed, bucket, cycleIdx + dipIndex * 100, 0x5b1) * THIN_BRAILLE_SPARK.length);
275
- return theme.fg("muted", THIN_BRAILLE_SPARK[sparkIdx]);
276
- }
277
- return theme.fg("muted", "○");
278
- };
279
- if (phaseInCycle < onRun) {
280
- // Sustained bright ●
281
- return theme.fg("warning", "●");
282
- }
283
- else if (phaseInCycle < onRun + tick) {
284
- return dipDot(0); // ○ dip 1
285
- }
286
- else if (phaseInCycle < onRun + tick * 2) {
287
- return theme.fg("warning", "●"); // ● flash 1
288
- }
289
- else if (phaseInCycle < onRun + tick * 3) {
290
- return dipDot(1); // ○ dip 2
291
- }
292
- else if (stutterTicks >= 5 && phaseInCycle < onRun + tick * 4) {
293
- return theme.fg("warning", "●"); // ● flash 2 (5-tick only)
294
- }
295
- else if (stutterTicks >= 5 && phaseInCycle < onRun + tick * 5) {
296
- return dipDot(2); // ○ dip 3 (5-tick only)
297
- }
298
- else {
299
- // Fallback: shouldn't reach if scheduling is correct
300
- return theme.fg("warning", "●");
301
- }
302
- }
303
- }
304
- return theme.fg("warning", "●");
305
- }
306
- case "awaiting":
307
- return theme.fg("muted", "○");
308
- case "done":
309
- return theme.fg("success", "●");
310
- case "error":
311
- return theme.fg("error", "✗");
312
- case "skipped":
313
- return theme.fg("muted", "⊘");
314
- default:
315
- return theme.fg("muted", "?");
316
- }
317
- }
318
- /** Center a label in a fixed-width header using em-dashes. Total width = 20. */
319
- function sectionHeader(label) {
320
- const total = 20;
321
- const innerLen = label.length + 2; // account for spaces around label
322
- const side = (total - innerLen) / 2;
323
- const left = "─".repeat(Math.floor(side));
324
- const right = "─".repeat(Math.ceil(side));
325
- return `${left} ${label} ${right}`;
326
- }
327
- /** Reconstruct multi-segment ANSI styles on a flat string by splitting at
328
- * original segment boundaries and re-applying each segment's style function.
329
- */
330
- export function reconstructHeader(content, segments) {
331
- let offset = 0;
332
- const parts = [];
333
- for (const seg of segments) {
334
- const len = seg.text.length;
335
- if (offset >= content.length)
336
- break;
337
- parts.push(seg.style(content.slice(offset, offset + len)));
338
- offset += len;
339
- }
340
- if (offset < content.length) {
341
- parts.push(content.slice(offset));
342
- }
343
- return parts.join("");
344
- }
345
- // ---------------------------------------------------------------------------
346
- // renderFlowCall — shown while the flow is being invoked
347
- // ---------------------------------------------------------------------------
348
34
  export function renderFlowCall(args, theme, config) {
349
35
  let container = new Text("", 0, 0);
350
36
  // In-place mutation pattern: reuse the stored root container
@@ -357,7 +43,7 @@ export function renderFlowCall(args, theme, config) {
357
43
  s.__rootContainer = root;
358
44
  container = root;
359
45
  }
360
- else if (container !== s.__rootContainer) {
46
+ else {
361
47
  const root = s.__rootContainer;
362
48
  root.clear();
363
49
  root.addChild(container);
@@ -373,13 +59,13 @@ export function renderFlowCall(args, theme, config) {
373
59
  export function renderFlowResult(result, expanded, theme, args, config) {
374
60
  const details = result.details;
375
61
  const streamingText = result.content?.[0]?.type === "text" ? result.content[0].text : undefined;
376
- const sharedContext = details?.sharedContext;
377
- // Resolve a stable id for this flow widget. Once an id is stored in
378
- // state we keep reusing it to prevent mid-render id switches that would
379
- // reset scramble animation state. On first render we prefer result._toolCallId,
380
- // then args inputs, then a per-state anonymous counter.
381
- // This prevents scramble-state collisions when multiple flow widgets are
382
- // visible simultaneously (e.g. sequential flows) and toolCallId is absent.
62
+ const resolvedIdForLive = result._toolCallId
63
+ || args?.toolCallId
64
+ || args?.id;
65
+ const live = getFlowLiveState(resolvedIdForLive);
66
+ const sharedContext = details?.sharedContext
67
+ ?? args?.sharedContext
68
+ ?? live?.sharedContext;
383
69
  let resolvedToolCallId;
384
70
  if (args?.state) {
385
71
  const s = args.state;
@@ -396,41 +82,30 @@ export function renderFlowResult(result, expanded, theme, args, config) {
396
82
  resolvedToolCallId = result._toolCallId || args?.toolCallId || args?.id;
397
83
  }
398
84
  let container;
399
- if (!details || details.results.length === 0) {
400
- // Ghost Dashboard: render a placeholder status line during the zero state
401
- const flowRequest = args?.flow?.[0];
402
- if (flowRequest) {
403
- const ghostResult = {
404
- type: flowRequest.type || "unknown",
405
- agentSource: "user",
406
- intent: flowRequest.intent || "Processing...",
407
- aim: flowRequest.aim ?? flowRequest.intent ?? "Processing...",
408
- acceptance: flowRequest.acceptance,
409
- exitCode: -1, // In progress
410
- messages: [],
411
- stderr: "",
412
- usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0, toolCalls: 0 },
413
- ...(flowRequest.model ? { model: flowRequest.model } : {}),
414
- ...(flowRequest.maxContextTokens !== undefined ? { maxContextTokens: flowRequest.maxContextTokens } : {}),
415
- };
416
- const ghostId = resolvedToolCallId || 'ghost';
417
- if (expanded) {
418
- const now = Date.now();
419
- container = renderFlowExpanded(ghostResult, flowStatusIcon(ghostResult, theme), false, getFlowDisplayItems([]), getFlowOutput([]), theme, ghostId, now, false, streamingText || "", config, undefined);
420
- }
421
- else {
422
- container = renderFlowCollapsed(ghostResult, flowStatusIcon(ghostResult, theme), false, streamingText || "", theme, undefined, ghostId, config);
423
- }
424
- }
425
- else {
426
- container = new Text(scrambleManager.renderStatic(streamingText || ""), 0, 0);
427
- }
428
- }
429
- else if (details.results.length === 1) {
430
- container = renderSingleFlowResult(details.results[0], expanded, theme, streamingText, resolvedToolCallId, config, sharedContext);
85
+ const flowRequest = args?.flow?.[0];
86
+ const bootResult = buildBootPhaseSingleResult(live, flowRequest);
87
+ const results = details?.results?.length
88
+ ? details.results
89
+ : bootResult
90
+ ? [bootResult]
91
+ : [];
92
+ if (results.length === 0) {
93
+ container = new Text(scrambleManager.renderStatic(streamingText || ""), 0, 0);
94
+ }
95
+ else if (results.length === 1) {
96
+ container = renderSingleFlowResult(results[0], expanded, theme, streamingText, resolvedToolCallId, config, sharedContext);
97
+ }
98
+ else if (details && details.results.length > 1) {
99
+ container = renderMultiFlowResult(details, expanded, theme, resolvedToolCallId, config, sharedContext);
431
100
  }
432
101
  else {
433
- container = renderMultiFlowResult(details, expanded, theme, resolvedToolCallId, config, sharedContext);
102
+ container = renderMultiFlowResult({
103
+ mode: "flow",
104
+ flowStyle: "fork",
105
+ projectAgentsDir: details?.projectAgentsDir ?? null,
106
+ results,
107
+ sharedContext,
108
+ }, expanded, theme, resolvedToolCallId, config, sharedContext);
434
109
  }
435
110
  // In-place mutation pattern: reuse the stored root container
436
111
  // so the TUI host's cached reference stays valid.
@@ -447,7 +122,7 @@ export function renderFlowResult(result, expanded, theme, args, config) {
447
122
  s.__rootContainer = root;
448
123
  }
449
124
  }
450
- else if (container !== s.__rootContainer) {
125
+ else {
451
126
  // Subsequent renders: transfer children to the stored container.
452
127
  // Use a snapshot of the children array so the loop remains safe even if
453
128
  // addChild() mutates the source array (removes from old parent).
@@ -469,17 +144,8 @@ export function renderFlowResult(result, expanded, theme, args, config) {
469
144
  }
470
145
  // Scramble animation timer — shared helper so any renderer can animate.
471
146
  // Use resolvedToolCallId so the timer scope matches the state scope.
472
- let timerId;
473
- if (!details || details.results.length === 0) {
474
- const flowRequest = args?.flow?.[0];
475
- timerId = resolvedToolCallId || (flowRequest ? 'ghost' : 'single');
476
- }
477
- else if (details.results.length === 1) {
478
- timerId = resolvedToolCallId || 'single';
479
- }
480
- else {
481
- timerId = resolvedToolCallId || 'multi';
482
- }
147
+ const timerId = resolvedToolCallId
148
+ || (results.length > 1 ? "multi" : results.length === 1 ? "single" : "empty");
483
149
  runScrambleTimer(args, timerId);
484
150
  return container;
485
151
  }
@@ -499,342 +165,6 @@ export function renderSingleFlowResult(r, expanded, theme, streamingText, toolCa
499
165
  }
500
166
  return renderFlowCollapsed(r, icon, error, flowOutput, theme, streamingText, id, config, sharedContext);
501
167
  }
502
- function renderFlowExpanded(r, icon, error, displayItems, flowOutput, theme, id, now, isComplete, streamingText, config, sharedContext) {
503
- const mdTheme = getMarkdownTheme();
504
- const container = new Container();
505
- const typeName = formatFlowTypeName(r.type);
506
- if (sharedContext) {
507
- container.addChild(new Text(applyRole("prefixLabel", "── shared context ──", theme, config), 0, 0));
508
- container.addChild(new Text(applyRole("aimContent", sharedContext.preview, theme, config), 0, 0));
509
- const statsParts = [];
510
- statsParts.push(`${sharedContext.userMessageCount} user`);
511
- statsParts.push(`${sharedContext.assistantMessageCount} assistant`);
512
- statsParts.push(`${sharedContext.totalTokens} tokens`);
513
- for (const [name, count] of Object.entries(sharedContext.toolCalls)) {
514
- statsParts.push(`${count}× ${name}`);
515
- }
516
- container.addChild(new Text(applyRole("stats", statsParts.join(" · "), theme, config), 0, 0));
517
- container.addChild(new Spacer(1));
518
- container.addChild(new Text(applyRole("prefixLabel", sectionHeader(typeName), theme, config), 0, 0));
519
- container.addChild(new Spacer(1));
520
- }
521
- const initialDot = flowStatusIcon(r, theme);
522
- let header = `${initialDot} ${applyRole("flowName", typeName, theme, config)}`;
523
- const errorSegment = error && r.stopReason ? ` [${r.stopReason}]` : "";
524
- if (errorSegment)
525
- header += ` ${theme.fg("error", errorSegment)}`;
526
- const dotPlaceholder = stripAnsi(initialDot) + ' ';
527
- const plainHeader = dotPlaceholder + typeName + errorSegment;
528
- const headerSegments = [
529
- { text: dotPlaceholder, style: (_s) => getScintillatingStatusDot(r, theme, Date.now(), id) + " " },
530
- { text: typeName, style: (s) => applyRole("flowName", s, theme, config) },
531
- ];
532
- if (errorSegment) {
533
- headerSegments.push({ text: errorSegment, style: (s) => theme.fg("error", s) });
534
- }
535
- container.addChild(new DynamicScrambleText(header, () => {
536
- const now = Date.now();
537
- const result = scrambleManager.updateText(id, 'header', plainHeader, now, isComplete);
538
- return reconstructHeader(result.content, headerSegments);
539
- }));
540
- if (error && r.errorMessage) {
541
- container.addChild(new Text(scrambleManager.renderStatic(theme.fg("error", `Error: ${r.errorMessage}`)), 0, 0));
542
- }
543
- // Stats: dashboard format
544
- const displayUsage = sharedContext ? { ...r.usage, input: sharedContext.totalTokens } : r.usage;
545
- const inlineStats = formatCompactStats(displayUsage, r.model);
546
- container.addChild(new DynamicScrambleText(applyRole("stats", inlineStats, theme, config), () => {
547
- const result = scrambleManager.updateText(id, 'stats', stripAnsi(inlineStats), Date.now(), isComplete);
548
- return result.isAnimating ? applyRole("stats", result.content, theme, config) : applyRole("stats", inlineStats, theme, config);
549
- }));
550
- // Intent — column-aware truncation (budget recalculated inside closure for resize handling)
551
- const intentBudget = getTruncationBudget(0);
552
- const displayIntent = truncateChars(r.intent, intentBudget);
553
- container.addChild(new Spacer(1));
554
- container.addChild(new Text(applyRole("prefixLabel", sectionHeader("intent"), theme, config), 0, 0));
555
- container.addChild(new DynamicScrambleText(applyRole("aimContent", displayIntent, theme, config), () => {
556
- const budget = getTruncationBudget(0);
557
- const text = truncateChars(r.intent, budget);
558
- const result = scrambleManager.updateText(id, 'intent', text, Date.now(), isComplete);
559
- return result.isAnimating ? applyRole("aimContent", result.content, theme, config) : applyRole("aimContent", text, theme, config);
560
- }));
561
- // Acceptance
562
- if (r.acceptance) {
563
- const acceptanceRaw = r.acceptance;
564
- const acceptanceBudget = getTruncationBudget(0);
565
- const acceptanceText = truncateChars(acceptanceRaw, acceptanceBudget);
566
- container.addChild(new Spacer(1));
567
- container.addChild(new Text(applyRole("prefixLabel", sectionHeader("acceptance"), theme, config), 0, 0));
568
- container.addChild(new DynamicScrambleText(applyRole("aimContent", acceptanceText, theme, config), () => {
569
- const budget = getTruncationBudget(0);
570
- const text = truncateChars(acceptanceRaw, budget);
571
- const result = scrambleManager.updateText(id, 'acceptance', text, Date.now(), isComplete);
572
- return result.isAnimating ? applyRole("aimContent", result.content, theme, config) : applyRole("aimContent", text, theme, config);
573
- }));
574
- }
575
- // Flow report (structured output)
576
- container.addChild(new Spacer(1));
577
- container.addChild(new Text(applyRole("prefixLabel", sectionHeader("report"), theme, config), 0, 0));
578
- // Structured output summary (compact badge when available)
579
- if (r.structuredOutput) {
580
- const so = r.structuredOutput;
581
- const statusColor = so.status === "complete" ? "success" : so.status === "partial" ? "warning" : "error";
582
- const statusText = `[${so.status}] ${so.summary}`;
583
- const statusStatic = `${theme.fg(statusColor, `[${so.status}]`)} ${applyRole("aimContent", so.summary, theme, config)}`;
584
- container.addChild(new DynamicScrambleText(statusStatic, () => {
585
- const result = scrambleManager.updateText(id, 'report-status', statusText, Date.now(), isComplete, false);
586
- return result.isAnimating ? `${theme.fg(statusColor, result.content.split(' ')[0])} ${applyRole("aimContent", result.content.slice(result.content.indexOf(' ') + 1), theme, config)}` : statusStatic;
587
- }));
588
- if (so.files.length > 0) {
589
- const filesText = `Files: ${so.files.map((f) => f.path).join(", ")}`;
590
- container.addChild(new DynamicScrambleText(applyRole("aimContent", filesText, theme, config), () => {
591
- const result = scrambleManager.updateText(id, 'report-files', filesText, Date.now(), isComplete, false);
592
- return result.isAnimating ? applyRole("aimContent", result.content, theme, config) : applyRole("aimContent", filesText, theme, config);
593
- }));
594
- }
595
- if (so.commands?.length > 0) {
596
- const cmdLabels = so.commands.map((c) => {
597
- const short = c.command.length > 30 ? c.command.slice(0, 30) + "..." : c.command;
598
- return `${c.tool ?? "cmd"}: ${short}`;
599
- });
600
- const commandsText = `Commands: ${cmdLabels.join(", ")}`;
601
- container.addChild(new DynamicScrambleText(applyRole("aimContent", commandsText, theme, config), () => {
602
- const result = scrambleManager.updateText(id, 'report-commands', commandsText, Date.now(), isComplete, false);
603
- return result.isAnimating ? applyRole("aimContent", result.content, theme, config) : applyRole("aimContent", commandsText, theme, config);
604
- }));
605
- }
606
- if (so.notDone.length > 0) {
607
- const notDoneText = `Not Done: ${so.notDone.map((item) => {
608
- const details = [
609
- item.reason ? `reason: ${item.reason}` : undefined,
610
- item.blocker ? `blocker: ${item.blocker}` : undefined,
611
- item.nextStep ? `next: ${item.nextStep}` : undefined,
612
- ].filter(Boolean).join("; ");
613
- return details ? `${item.item} (${details})` : item.item;
614
- }).join("; ")}`;
615
- container.addChild(new DynamicScrambleText(applyRole("aimContent", notDoneText, theme, config), () => {
616
- const result = scrambleManager.updateText(id, 'report-notDone', notDoneText, Date.now(), isComplete, false);
617
- return result.isAnimating ? applyRole("aimContent", result.content, theme, config) : applyRole("aimContent", notDoneText, theme, config);
618
- }));
619
- }
620
- if (so.nextSteps.length > 0) {
621
- const nextStepsText = `Next: ${so.nextSteps.join("; ")}`;
622
- container.addChild(new DynamicScrambleText(applyRole("aimContent", nextStepsText, theme, config), () => {
623
- const result = scrambleManager.updateText(id, 'report-nextSteps', nextStepsText, Date.now(), isComplete, false);
624
- return result.isAnimating ? applyRole("aimContent", result.content, theme, config) : applyRole("aimContent", nextStepsText, theme, config);
625
- }));
626
- }
627
- container.addChild(new Spacer(1));
628
- }
629
- // Output: animate streaming text; show clean markdown when complete
630
- if (isFlowAwaiting(r)) {
631
- container.addChild(new Text(applyRole("placeholder", "[awaiting...]", theme, config), 0, 0));
632
- }
633
- else if (!isComplete && streamingText != null) {
634
- const msgBudget = getTruncationBudget(0);
635
- const displayMsg = tailText(stripAnsi(streamingText), msgBudget);
636
- container.addChild(new DynamicScrambleText(applyRole("msgContent", displayMsg, theme, config), () => {
637
- const budget = getTruncationBudget(0);
638
- const freshStreamingText = getLiveTextWithFallback(id) ?? streamingText;
639
- const text = tailText(stripAnsi(freshStreamingText), budget);
640
- const result = scrambleManager.updateMsg(id, text, Date.now(), isComplete, undefined, true);
641
- return result.isAnimating ? applyRole("msgContent", result.content, theme, config) : applyRole("msgContent", text, theme, config);
642
- }));
643
- }
644
- else if (flowOutput) {
645
- container.addChild(new Markdown(flowOutput.trim(), 0, 0, mdTheme));
646
- }
647
- else {
648
- const summary = getFlowSummaryText(r);
649
- container.addChild(new DynamicScrambleText(applyRole("msgContent", summary, theme, config), () => {
650
- const result = scrambleManager.updateText(id, 'output-summary', summary, Date.now(), isComplete, false);
651
- return result.isAnimating ? applyRole("msgContent", result.content, theme, config) : applyRole("msgContent", summary, theme, config);
652
- }));
653
- }
654
- // Tool traces (expanded only) — per-line scramble
655
- const toolCallItems = displayItems.filter((item) => item.type === "toolCall");
656
- if (toolCallItems.length > 0) {
657
- container.addChild(new Spacer(1));
658
- container.addChild(new Text(applyRole("prefixLabel", sectionHeader("tool calls"), theme, config), 0, 0));
659
- const toolPrefixLen = visibleLength("→ ");
660
- const toolBudget = getTruncationBudget(toolPrefixLen);
661
- for (let i = 0; i < toolCallItems.length; i++) {
662
- const item = toolCallItems[i];
663
- const lineText = applyRole("prefixLabel", "→ ", theme, config) + formatFlowToolCall(item.name, item.args, theme.fg.bind(theme));
664
- const plainText = stripAnsi(lineText);
665
- const displayTool = truncateChars(plainText, toolBudget);
666
- const initialScrambled = scrambleManager.updateText(id, `tool#${i}`, displayTool, now, isComplete).content;
667
- container.addChild(new DynamicScrambleText(initialScrambled, () => {
668
- const budget = getTruncationBudget(toolPrefixLen);
669
- const freshPlain = stripAnsi(lineText);
670
- const text = truncateChars(freshPlain, budget);
671
- return scrambleManager.updateText(id, `tool#${i}`, text, Date.now(), isComplete).content;
672
- }));
673
- }
674
- }
675
- if (isComplete) {
676
- scrambleManager.completeFlow(id);
677
- }
678
- return container;
679
- }
680
- function renderFlowCollapsed(r, icon, error, flowOutput, theme, streamingText, toolCallId, config, sharedContext) {
681
- const id = toolCallId || "collapsed";
682
- const now = Date.now();
683
- const container = new Container();
684
- const maxWidth = process.stdout.columns ?? 80;
685
- const typeName = formatCollapsedFlowHeaderTypeName(r.type);
686
- const modelLabel = formatModelLabel(r.model);
687
- const headerPrefixLen = visibleLength(typeName) + visibleLength(modelLabel ? ` ${modelLabel} · ` : " ");
688
- const isComplete = isFlowStatusComplete(r);
689
- // Build header stats: ctxLabel · t/s
690
- const statsParts = [];
691
- const ctxTokens = sharedContext?.totalTokens ?? r.usage.contextTokens;
692
- if (r.maxContextTokens !== undefined || ctxTokens > 0) {
693
- const ctxLabel = formatContextLabel(ctxTokens, r.maxContextTokens);
694
- statsParts.push(ctxLabel);
695
- }
696
- let displayStats = statsParts.join(" · ");
697
- if (r.usage.smoothedTps && r.usage.smoothedTps > 0) {
698
- const tpsFormatted = formatTps(r.usage.smoothedTps);
699
- statsParts.push(tpsFormatted);
700
- displayStats = statsParts.join(" · ");
701
- // Flash TPS value when it changes
702
- const tpsNum = tpsFormatted.slice(0, -4); // remove " t/s" suffix
703
- const scrambledTps = scrambleManager.updateTps(id, tpsNum, now, isComplete, true);
704
- if (scrambledTps !== tpsNum) {
705
- displayStats = displayStats.replace(`${tpsNum} t/s`, `${scrambledTps} t/s`);
706
- }
707
- }
708
- const modelSegment = modelLabel ? ` ${modelLabel} · ` : " ";
709
- const statsSegment = stripAnsi(displayStats);
710
- const errorSegment = error && r.stopReason ? ` [${r.stopReason}]` : "";
711
- const initialDot = flowStatusIcon(r, theme);
712
- let header = `${initialDot} ${applyRole("flowName", typeName, theme, config)}${applyRole("modelName", modelSegment, theme, config)}${applyRole("stats", displayStats, theme, config)}`;
713
- if (errorSegment)
714
- header += ` ${theme.fg("error", errorSegment)}`;
715
- const dotPlaceholder = stripAnsi(initialDot) + ' ';
716
- const plainHeader = dotPlaceholder + typeName + modelSegment + statsSegment + errorSegment;
717
- const headerSegments = [
718
- { text: dotPlaceholder, style: (_s) => getScintillatingStatusDot(r, theme, Date.now(), id) + " " },
719
- { text: typeName, style: (s) => applyRole("flowName", s, theme, config) },
720
- { text: modelSegment, style: (s) => applyRole("modelName", s, theme, config) },
721
- { text: statsSegment, style: (s) => applyRole("stats", s, theme, config) },
722
- ];
723
- if (errorSegment) {
724
- headerSegments.push({ text: errorSegment, style: (s) => theme.fg("error", s) });
725
- }
726
- container.addChild(new DynamicScrambleText(header, () => {
727
- const now = Date.now();
728
- const result = scrambleManager.updateText(id, 'header', plainHeader, now, isComplete, true);
729
- return reconstructHeader(result.content, headerSegments);
730
- }, true));
731
- // aim: line — glitch on text change
732
- if (r.aim && r.type !== "trace") {
733
- const aimTree = "├─";
734
- const aimLabel = ` aim ▸ `;
735
- const aimPrefix = `${aimTree}${aimLabel}`;
736
- const budget = getTruncationBudget(visibleLength(aimPrefix));
737
- const displayAim = isFlowAwaiting(r) ? "[awaiting...]" : truncateChars(lowerFirstWord(r.aim), budget);
738
- container.addChild(new DynamicScrambleText(`${applyRole("treeChars", aimTree, theme, config)}${applyRole("prefixLabel", aimLabel, theme, config)}${applyRole(getContentRole("aimContent", displayAim), displayAim, theme, config)}`, () => {
739
- const now = Date.now();
740
- const freshAimLabel = ` aim ▸ `;
741
- const freshAimPrefix = `${aimTree}${freshAimLabel}`;
742
- const freshBudget = getTruncationBudget(visibleLength(freshAimPrefix));
743
- const freshText = isFlowAwaiting(r) ? "[awaiting...]" : truncateChars(lowerFirstWord(r.aim), freshBudget);
744
- const result = scrambleManager.updateAim(id, freshText, now, isComplete, true);
745
- return `${applyRole("treeChars", aimTree, theme, config)}${applyRole("prefixLabel", freshAimLabel, theme, config)}${applyRole(getContentRole("aimContent", freshText), result.content, theme, config)}`;
746
- }, true));
747
- }
748
- // act: line (last tool call with count)
749
- const lastTool = getLastToolCall(r.messages);
750
- const actStr = lastTool ? formatFlowToolCall(lastTool.name, lastTool.args, theme.fg.bind(theme)) : "[n/a]";
751
- const isLite = config?.bodyVerbosity !== "full";
752
- const actTree = isLite ? "└─" : "├─";
753
- const actLabel = ` cmd ▸ `;
754
- const prefixStub = `${actTree}${actLabel}`;
755
- const budget = getTruncationBudget(visibleLength(prefixStub));
756
- const actFullText = stripAnsi(lowerFirstWord(actStr));
757
- const initialActContent = isFlowAwaiting(r) ? "[n/a]" : (actFullText.length > budget ? tailText(actFullText, budget) : actFullText);
758
- container.addChild(new DynamicScrambleText(`${applyRole("treeChars", actTree, theme, config)}${applyRole("prefixLabel", actLabel, theme, config)}${applyRole(getContentRole("actContent", initialActContent), initialActContent, theme, config)}`, () => {
759
- const now = Date.now();
760
- const actLabel = ` cmd ▸ `;
761
- const actPrefix = `${actTree}${actLabel}`;
762
- const freshBudget = getTruncationBudget(visibleLength(actPrefix));
763
- const displayAct = isFlowAwaiting(r) ? "[n/a]" : tailText(actFullText, freshBudget);
764
- const actContent = scrambleManager.updateAct(id, displayAct, now, isComplete, true).content;
765
- return `${applyRole("treeChars", actTree, theme, config)}${applyRole("prefixLabel", actLabel, theme, config)}${applyRole(getContentRole("actContent", displayAct), actContent, theme, config)}`;
766
- }, true));
767
- // msg: line (last assistant text or streaming) — full mode only
768
- if (!isLite) {
769
- const msgPrefixStub = `└─ msg ▸ `;
770
- const msgBudget = getTruncationBudget(visibleLength(msgPrefixStub));
771
- let rawMsg;
772
- let useError = false;
773
- if (isFlowAwaiting(r)) {
774
- rawMsg = "[awaiting...]";
775
- }
776
- else if (r.status === "skipped") {
777
- rawMsg = "[skipped]";
778
- }
779
- else {
780
- const liveMsgText = isFlowRunning(r) ? getLiveTextWithFallback(id) : undefined;
781
- if (liveMsgText != null) {
782
- rawMsg = stripAnsi(liveMsgText);
783
- }
784
- else if (isFlowRunning(r) && streamingText != null) {
785
- rawMsg = stripAnsi(streamingText);
786
- }
787
- else if (r.structuredOutput?.summary) {
788
- rawMsg = stripAnsi(r.structuredOutput.summary);
789
- }
790
- else if (flowOutput) {
791
- rawMsg = stripAnsi(flowOutput);
792
- }
793
- else if (error && r.errorMessage) {
794
- rawMsg = stripAnsi(r.errorMessage);
795
- useError = true;
796
- }
797
- else {
798
- const summary = getFlowSummaryText(r);
799
- rawMsg = stripAnsi(summary) || "[n/a]";
800
- }
801
- }
802
- const initialNeedsTail = !isFlowAwaiting(r) && isFlowRunning(r) && (streamingText != null || getLiveTextWithFallback(id) != null);
803
- const initialMsgContent = initialNeedsTail
804
- ? tailText(rawMsg, msgBudget)
805
- : truncateChars(rawMsg, msgBudget);
806
- const msgTree = "└─";
807
- const msgLabel = ` msg ▸ `;
808
- const initialMsgPrefix = `${msgTree}${msgLabel}`;
809
- container.addChild(new DynamicScrambleText(`${applyRole("treeChars", msgTree, theme, config)}${applyRole("prefixLabel", msgLabel, theme, config)}${applyRole(getContentRole("msgContent", initialMsgContent, useError), initialMsgContent, theme, config)}`, () => {
810
- const now = Date.now();
811
- const msgLabel = ` msg ▸ `;
812
- const msgPrefix = `${msgTree}${msgLabel}`;
813
- let freshRawMsg;
814
- let needsTail;
815
- if (isFlowAwaiting(r)) {
816
- freshRawMsg = "[awaiting...]";
817
- needsTail = false;
818
- }
819
- else if (r.status === "skipped") {
820
- freshRawMsg = "[skipped]";
821
- needsTail = false;
822
- }
823
- else {
824
- const isRunningNow = isFlowRunning(r);
825
- freshRawMsg = (isRunningNow ? getLiveTextWithFallback(id) : undefined) ?? rawMsg;
826
- needsTail = isRunningNow && (streamingText != null || getLiveTextWithFallback(id) != null);
827
- }
828
- const displayMsg = needsTail ? tailText(freshRawMsg, msgBudget) : truncateChars(freshRawMsg, msgBudget);
829
- const result = scrambleManager.updateMsg(id, displayMsg, now, isComplete, undefined, true);
830
- return `${applyRole("treeChars", msgTree, theme, config)}${applyRole("prefixLabel", msgLabel, theme, config)}${applyRole(getContentRole("msgContent", freshRawMsg, useError), result.content, theme, config)}`;
831
- }, true));
832
- }
833
- if (isComplete) {
834
- scrambleManager.completeFlow(id);
835
- }
836
- return container;
837
- }
838
168
  // ---------------------------------------------------------------------------
839
169
  // Multi-flow result
840
170
  // ---------------------------------------------------------------------------
@@ -850,119 +180,12 @@ function renderMultiFlowResult(details, expanded, theme, toolCallId, config, sha
850
180
  }
851
181
  return renderMultiFlowCollapsed(results, theme, baseId, config, sharedContext);
852
182
  }
853
- function renderMultiFlowExpanded(results, successCount, icon, theme, baseId, now, config, sharedContext) {
854
- const mdTheme = getMarkdownTheme();
855
- const container = new Container();
856
- if (sharedContext) {
857
- container.addChild(new Text(applyRole("prefixLabel", "── shared context ──", theme, config), 0, 0));
858
- container.addChild(new Text(applyRole("aimContent", sharedContext.preview, theme, config), 0, 0));
859
- const statsParts = [];
860
- statsParts.push(`${sharedContext.userMessageCount} user`);
861
- statsParts.push(`${sharedContext.assistantMessageCount} assistant`);
862
- statsParts.push(`${sharedContext.totalTokens} tokens`);
863
- for (const [name, count] of Object.entries(sharedContext.toolCalls)) {
864
- statsParts.push(`${count}× ${name}`);
865
- }
866
- container.addChild(new Text(applyRole("stats", statsParts.join(" · "), theme, config), 0, 0));
867
- container.addChild(new Spacer(1));
868
- }
869
- // Summary: just show count, no icon
870
- container.addChild(new Text(applyRole("flowName", `${results.length} flows`, theme, config), 0, 0));
871
- for (let flowIdx = 0; flowIdx < results.length; flowIdx++) {
872
- const r = results[flowIdx];
873
- const flowId = `${baseId}#${flowIdx}`;
874
- const isComplete = isFlowStatusComplete(r);
875
- const displayItems = getFlowDisplayItems(r.messages);
876
- const flowOutput = getFlowOutput(r.messages);
877
- const typeName = formatFlowTypeName(r.type);
878
- container.addChild(new Spacer(1));
879
- // Per-flow header: ─── EXPLORER (no icon)
880
- const headerStatic = applyRole("prefixLabel", sectionHeader(typeName), theme, config);
881
- container.addChild(new DynamicScrambleText(headerStatic, () => {
882
- const result = scrambleManager.updateText(flowId, 'header', typeName, Date.now(), isComplete, true);
883
- return result.isAnimating ? applyRole("prefixLabel", result.content, theme, config) : headerStatic;
884
- }));
885
- // Stats: dashboard format
886
- const flowStats = formatCompactStats(r.usage, r.model);
887
- container.addChild(new DynamicScrambleText(applyRole("stats", flowStats, theme, config), () => {
888
- const result = scrambleManager.updateText(flowId, 'stats', stripAnsi(flowStats), Date.now(), isComplete, true);
889
- return result.isAnimating ? applyRole("stats", result.content, theme, config) : applyRole("stats", flowStats, theme, config);
890
- }));
891
- // Intent: just show text, no prefix (budget computed dynamically for resize recalculation)
892
- const intentBudget = getTruncationBudget(0);
893
- const displayIntent = truncateChars(r.intent, intentBudget);
894
- container.addChild(new DynamicScrambleText(applyRole("aimContent", displayIntent, theme, config), () => {
895
- const budget = getTruncationBudget(0);
896
- const text = truncateChars(r.intent, budget);
897
- const result = scrambleManager.updateText(flowId, 'intent', text, Date.now(), isComplete, true);
898
- return result.isAnimating ? applyRole("aimContent", result.content, theme, config) : applyRole("aimContent", text, theme, config);
899
- }));
900
- if (r.acceptance) {
901
- const acceptanceRaw = r.acceptance;
902
- const acceptancePrefix = "Acceptance: ";
903
- const acceptanceBudget = getTruncationBudget(visibleLength(acceptancePrefix));
904
- const acceptanceText = truncateChars(acceptanceRaw, acceptanceBudget);
905
- const acceptanceStatic = applyRole("aimContent", `${acceptancePrefix}${acceptanceText}`, theme, config);
906
- container.addChild(new DynamicScrambleText(acceptanceStatic, () => {
907
- const budget = getTruncationBudget(visibleLength(acceptancePrefix));
908
- const text = truncateChars(acceptanceRaw, budget);
909
- const result = scrambleManager.updateText(flowId, 'acceptance', text, Date.now(), isComplete, true);
910
- return result.isAnimating ? applyRole("aimContent", `${acceptancePrefix}${result.content}`, theme, config) : applyRole("aimContent", `${acceptancePrefix}${text}`, theme, config);
911
- }));
912
- }
913
- // Output: animate streaming text; show clean markdown when complete
914
- if (isFlowAwaiting(r)) {
915
- container.addChild(new Text(applyRole("placeholder", "[awaiting...]", theme, config), 0, 0));
916
- }
917
- else if (!isComplete && r.streamingText != null) {
918
- const streamingRaw = r.streamingText;
919
- const msgBudget = getTruncationBudget(0);
920
- const displayMsg = tailText(stripAnsi(streamingRaw), msgBudget);
921
- container.addChild(new DynamicScrambleText(applyRole("msgContent", displayMsg, theme, config), () => {
922
- const budget = getTruncationBudget(0);
923
- const freshStreamingText = getLiveTextWithFallback(flowId) ?? streamingRaw;
924
- const text = tailText(stripAnsi(freshStreamingText), budget);
925
- const result = scrambleManager.updateMsg(flowId, text, Date.now(), isComplete, undefined, true);
926
- return result.isAnimating ? applyRole("msgContent", result.content, theme, config) : applyRole("msgContent", text, theme, config);
927
- }));
928
- }
929
- else if (flowOutput) {
930
- container.addChild(new Spacer(1));
931
- container.addChild(new Markdown(flowOutput.trim(), 0, 0, mdTheme));
932
- }
933
- // Tool traces in expanded view — per-line scramble
934
- const toolCallItems = displayItems.filter((item) => item.type === "toolCall");
935
- if (toolCallItems.length > 0) {
936
- container.addChild(new Spacer(1));
937
- container.addChild(new Text(applyRole("prefixLabel", sectionHeader("tool calls"), theme, config), 0, 0));
938
- const toolPrefixLen = visibleLength("→ ");
939
- const toolBudget = getTruncationBudget(toolPrefixLen);
940
- for (let i = 0; i < toolCallItems.length; i++) {
941
- const item = toolCallItems[i];
942
- const lineText = applyRole("prefixLabel", "→ ", theme, config) + formatFlowToolCall(item.name, item.args, theme.fg.bind(theme));
943
- const plainText = stripAnsi(lineText);
944
- const displayTool = truncateChars(plainText, toolBudget);
945
- const initialScrambled = scrambleManager.updateText(flowId, `tool#${i}`, displayTool, now, isComplete).content;
946
- container.addChild(new DynamicScrambleText(initialScrambled, () => {
947
- const budget = getTruncationBudget(toolPrefixLen);
948
- const freshPlain = stripAnsi(lineText);
949
- const text = truncateChars(freshPlain, budget);
950
- return scrambleManager.updateText(flowId, `tool#${i}`, text, Date.now(), isComplete).content;
951
- }));
952
- }
953
- }
954
- if (isComplete) {
955
- scrambleManager.completeFlow(flowId);
956
- }
957
- }
958
- // Total stats: dashboard format
959
- const totalUsage = aggregateFlowUsage(results);
960
- const totalModel = results[0]?.model;
961
- const totalStats = formatCompactStats(totalUsage, totalModel);
962
- container.addChild(new Spacer(1));
963
- container.addChild(new Text(applyRole("stats", totalStats, theme, config), 0, 0));
964
- return container;
183
+ function renderMultiFlowCollapsed(results, theme, baseId, config, sharedContext) {
184
+ return renderActivityPanel(results, theme, baseId, config, sharedContext);
965
185
  }
186
+ // ---------------------------------------------------------------------------
187
+ // Activity panel
188
+ // ---------------------------------------------------------------------------
966
189
  function renderActivityPanel(results, theme, baseId, config, sharedContext) {
967
190
  const idPrefix = baseId || "panel";
968
191
  const container = new Container();
@@ -1020,9 +243,65 @@ function renderStandaloneFlow(container, r, index, idPrefix, theme, now, config,
1020
243
  // ---------------------------------------------------------------------------
1021
244
  function renderGroup(container, group, results, idPrefix, theme, now, config, isLastRoot = false, sharedContext) {
1022
245
  // ─── Group header line ───
1023
- const headerPrefix = isLastRoot ? "" : "├─";
1024
- const headerText = `${headerPrefix}${headerPrefix ? ' ' : ''}audit-loop`;
1025
- container.addChild(new Text(applyRole("treeChars", headerText, theme, config), 0, 0));
246
+ // Collect all children for aggregate status computation
247
+ const childIndices = [...group.buildIndices, group.auditIndex];
248
+ const children = childIndices.map((idx) => results[idx]);
249
+ // Compute aggregate status icon
250
+ const anyRunning = children.some((r) => isFlowRunning(r));
251
+ const anyAwaiting = children.some((r) => isFlowAwaiting(r));
252
+ const anyError = children.some((r) => isFlowError(r));
253
+ const allComplete = children.every((r) => isFlowStatusComplete(r));
254
+ const buildChildren = group.buildIndices.map((idx) => results[idx]);
255
+ const anyPass = buildChildren.some((r) => r.pingPongMeta?.finalVerdict === "pass");
256
+ let aggregateIcon;
257
+ if (anyRunning) {
258
+ aggregateIcon = theme.fg("warning", "↻");
259
+ }
260
+ else if (anyAwaiting) {
261
+ aggregateIcon = theme.fg("muted", "○");
262
+ }
263
+ else if (allComplete && anyPass) {
264
+ aggregateIcon = theme.fg("success", "✓");
265
+ }
266
+ else if (anyError) {
267
+ aggregateIcon = theme.fg("error", "✗");
268
+ }
269
+ else if (allComplete) {
270
+ aggregateIcon = theme.fg("muted", "⊘");
271
+ }
272
+ else {
273
+ aggregateIcon = theme.fg("muted", "?");
274
+ }
275
+ // Compute cycle counter badge from build metadata
276
+ const maxCycles = Math.max(...buildChildren.map((r) => r.pingPongMeta?.cycles ?? 0));
277
+ const hasPingPong = buildChildren.some((r) => r.pingPongMeta !== undefined);
278
+ let badgeText = "";
279
+ if (hasPingPong && maxCycles > 0) {
280
+ const anyIncomplete = children.some((r) => !isFlowStatusComplete(r));
281
+ if (anyIncomplete) {
282
+ badgeText = `cycle ${maxCycles}`;
283
+ }
284
+ else {
285
+ const finalVerdict = buildChildren.find((r) => r.pingPongMeta?.finalVerdict)?.pingPongMeta?.finalVerdict ?? "";
286
+ const cyclesLabel = maxCycles === 1 ? "1 cycle" : `${maxCycles} cycles`;
287
+ badgeText = finalVerdict ? `${cyclesLabel} · ${finalVerdict}` : cyclesLabel;
288
+ }
289
+ }
290
+ // Build header parts
291
+ const treePart = isLastRoot ? "" : "├─ ";
292
+ const label = " audit-loop";
293
+ const badge = badgeText ? ` · ${badgeText}` : "";
294
+ // Compute trailing rule to pad to ~50 visible chars
295
+ const targetWidth = 50;
296
+ const visibleWidth = treePart.length + 1 + label.length + badge.length;
297
+ const ruleWidth = Math.max(0, targetWidth - visibleWidth);
298
+ const rule = "─".repeat(ruleWidth);
299
+ const headerLine = applyRole("treeChars", treePart, theme, config) +
300
+ aggregateIcon +
301
+ applyRole("groupHeader", label, theme, config) +
302
+ applyRole("groupHeader", badge, theme, config) +
303
+ applyRole("treeChars", rule, theme, config);
304
+ container.addChild(new Text(headerLine, 0, 0));
1026
305
  // ─── Build children ───
1027
306
  for (let b = 0; b < group.buildIndices.length; b++) {
1028
307
  const buildIdx = group.buildIndices[b];
@@ -1052,185 +331,4 @@ function renderGroup(container, group, results, idPrefix, theme, now, config, is
1052
331
  }
1053
332
  // No extra spacer — renderActivityPanel handles uniform inter-item spacing
1054
333
  }
1055
- // ---------------------------------------------------------------------------
1056
- // Shared flow rendering helpers
1057
- // ---------------------------------------------------------------------------
1058
- function renderFlowHeader(container, r, flowId, headerPrefix, theme, now, config, sharedContext) {
1059
- const typeName = formatCollapsedFlowHeaderTypeName(r.type);
1060
- const modelLabel = formatModelLabel(r.model);
1061
- const isComplete = isFlowStatusComplete(r);
1062
- const flowComplete = isComplete;
1063
- const error = isFlowError(r);
1064
- const errorSegment = error && r.stopReason ? ` [${r.stopReason}]` : "";
1065
- const initialDot = flowStatusIcon(r, theme);
1066
- const dotPlaceholder = stripAnsi(initialDot) + ' ';
1067
- let headerLine;
1068
- let plainHeader;
1069
- const headerSegments = [
1070
- { text: headerPrefix + " ", style: (s) => applyRole("treeChars", s, theme, config) },
1071
- { text: dotPlaceholder, style: (_s) => getScintillatingStatusDot(r, theme, Date.now(), flowId) + " " },
1072
- { text: typeName, style: (s) => applyRole("flowName", s, theme, config) },
1073
- ];
1074
- {
1075
- // Standard flow: model + stats
1076
- const statsParts = [];
1077
- if (r.maxContextTokens !== undefined || r.usage.contextTokens > 0) {
1078
- const ctxTokens = sharedContext?.totalTokens ?? r.usage.contextTokens;
1079
- const ctxLabel = formatContextLabel(ctxTokens, r.maxContextTokens);
1080
- statsParts.push(ctxLabel);
1081
- }
1082
- let displayStats = statsParts.join(" · ");
1083
- if (r.usage.smoothedTps && r.usage.smoothedTps > 0) {
1084
- const tpsFormatted = formatTps(r.usage.smoothedTps);
1085
- statsParts.push(tpsFormatted);
1086
- displayStats = statsParts.join(" · ");
1087
- const tpsNum = tpsFormatted.slice(0, -4); // remove " t/s" suffix
1088
- const scrambledTps = scrambleManager.updateTps(flowId, tpsNum, now, flowComplete, true);
1089
- if (scrambledTps !== tpsNum) {
1090
- displayStats = displayStats.replace(`${tpsNum} t/s`, `${scrambledTps} t/s`);
1091
- }
1092
- }
1093
- const modelSegment = modelLabel ? ` ${modelLabel}` : "";
1094
- const statsSegment = ` · ${displayStats}`;
1095
- const statsPlain = stripAnsi(statsSegment);
1096
- headerLine = `${applyRole("treeChars", headerPrefix, theme, config)} ${initialDot} ${applyRole("flowName", typeName, theme, config)}${applyRole("modelName", modelSegment, theme, config)}${applyRole("stats", statsSegment, theme, config)}`;
1097
- if (errorSegment) {
1098
- headerLine += ` ${theme.fg("error", errorSegment)}`;
1099
- }
1100
- plainHeader = headerPrefix + " " + dotPlaceholder + typeName + modelSegment + statsPlain + errorSegment;
1101
- headerSegments.push({ text: modelSegment, style: (s) => applyRole("modelName", s, theme, config) }, { text: statsPlain, style: (s) => applyRole("stats", s, theme, config) });
1102
- }
1103
- if (errorSegment) {
1104
- headerSegments.push({ text: errorSegment, style: (s) => theme.fg("error", s) });
1105
- }
1106
- container.addChild(new DynamicScrambleText(headerLine, () => {
1107
- const now = Date.now();
1108
- const result = scrambleManager.updateText(flowId, 'header', plainHeader, now, flowComplete, true);
1109
- return reconstructHeader(result.content, headerSegments);
1110
- }, true));
1111
- }
1112
- function renderFlowBody(container, r, flowId, indent, theme, now, config) {
1113
- const isComplete = isFlowStatusComplete(r);
1114
- const flowComplete = isComplete;
1115
- // aim: line — glitch on text change
1116
- if (r.aim && r.type !== "trace") {
1117
- const aimTree = indent + "├─";
1118
- const aimLabel = ` aim ▸ `;
1119
- const aimPrefix = `${aimTree}${aimLabel}`;
1120
- const budget = getTruncationBudget(visibleLength(aimPrefix));
1121
- const displayAim = isFlowAwaiting(r) ? "[awaiting...]" : truncateChars(lowerFirstWord(r.aim), budget);
1122
- container.addChild(new DynamicScrambleText(`${applyRole("treeChars", aimTree, theme, config)}${applyRole("prefixLabel", aimLabel, theme, config)}${applyRole(getContentRole("aimContent", displayAim), displayAim, theme, config)}`, () => {
1123
- const now = Date.now();
1124
- const freshAimLabel = ` aim ▸ `;
1125
- const freshAimPrefix = `${aimTree}${freshAimLabel}`;
1126
- const freshBudget = getTruncationBudget(visibleLength(freshAimPrefix));
1127
- const freshText = isFlowAwaiting(r) ? "[awaiting...]" : truncateChars(lowerFirstWord(r.aim), freshBudget);
1128
- const result = scrambleManager.updateAim(flowId, freshText, now, flowComplete, true);
1129
- return `${applyRole("treeChars", aimTree, theme, config)}${applyRole("prefixLabel", freshAimLabel, theme, config)}${applyRole(getContentRole("aimContent", freshText), result.content, theme, config)}`;
1130
- }, true));
1131
- }
1132
- // act: line (last tool call with count)
1133
- const lastTool = getLastToolCall(r.messages);
1134
- const actStr = lastTool ? formatFlowToolCall(lastTool.name, lastTool.args, theme.fg.bind(theme)) : "[n/a]";
1135
- const isLite = config?.bodyVerbosity !== "full";
1136
- const actTree = isLite ? `${indent}└─` : `${indent}├─`;
1137
- const actLabel = ` cmd ▸ `;
1138
- const prefixStub = `${actTree}${actLabel}`;
1139
- const budget = getTruncationBudget(visibleLength(prefixStub));
1140
- const actFullText = stripAnsi(lowerFirstWord(actStr));
1141
- const initialActContent = isFlowAwaiting(r) ? "[n/a]" : (actFullText.length > budget ? tailText(actFullText, budget) : actFullText);
1142
- container.addChild(new DynamicScrambleText(`${applyRole("treeChars", actTree, theme, config)}${applyRole("prefixLabel", actLabel, theme, config)}${applyRole(getContentRole("actContent", initialActContent), initialActContent, theme, config)}`, () => {
1143
- const now = Date.now();
1144
- const actLabel = ` cmd ▸ `;
1145
- const actPrefix = `${actTree}${actLabel}`;
1146
- const freshBudget = getTruncationBudget(visibleLength(actPrefix));
1147
- const displayAct = isFlowAwaiting(r) ? "[n/a]" : tailText(actFullText, freshBudget);
1148
- const actContent = scrambleManager.updateAct(flowId, displayAct, now, flowComplete, true).content;
1149
- return `${applyRole("treeChars", actTree, theme, config)}${applyRole("prefixLabel", actLabel, theme, config)}${applyRole(getContentRole("actContent", displayAct), actContent, theme, config)}`;
1150
- }, true));
1151
- // msg: line (live streaming text or last assistant text) — full mode only
1152
- if (!isLite) {
1153
- const msgTree = `${indent}└─`;
1154
- const msgLabel = ` msg ▸ `;
1155
- const msgPrefixStub = `${msgTree}${msgLabel}`;
1156
- const msgBudget = getTruncationBudget(visibleLength(msgPrefixStub));
1157
- let rawMsg;
1158
- let useError = false;
1159
- if (isFlowAwaiting(r)) {
1160
- rawMsg = "[awaiting...]";
1161
- }
1162
- else if (r.status === "skipped") {
1163
- rawMsg = "[skipped]";
1164
- }
1165
- else if (isFlowStatusComplete(r) && !isFlowRunning(r)) {
1166
- if (isFlowError(r) && r.errorMessage) {
1167
- rawMsg = stripAnsi(r.errorMessage);
1168
- useError = true;
1169
- }
1170
- else if ((r.pingPongMeta?.finalVerdict === "pass" || r.structuredOutput?.verdict === "pass") && r.type === "audit") {
1171
- rawMsg = "[approved]";
1172
- }
1173
- else if (r.type === "audit" && !r.structuredOutput?.summary && !getFlowOutput(r.messages)) {
1174
- rawMsg = "[finished]";
1175
- }
1176
- else {
1177
- rawMsg = stripAnsi(r.structuredOutput?.summary ?? getFlowOutput(r.messages) ?? getFlowSummaryText(r)) || "[n/a]";
1178
- }
1179
- }
1180
- else {
1181
- const liveMsgText = isFlowRunning(r) ? getLiveTextWithFallback(flowId) : undefined;
1182
- if (liveMsgText != null) {
1183
- rawMsg = stripAnsi(liveMsgText);
1184
- }
1185
- else if (isFlowRunning(r) && r.streamingText != null) {
1186
- rawMsg = stripAnsi(r.streamingText);
1187
- }
1188
- else if (r.structuredOutput?.summary) {
1189
- rawMsg = stripAnsi(r.structuredOutput.summary);
1190
- }
1191
- else {
1192
- const flowOutput = getFlowOutput(r.messages);
1193
- if (flowOutput) {
1194
- rawMsg = stripAnsi(flowOutput);
1195
- }
1196
- else if (isFlowError(r) && r.errorMessage) {
1197
- rawMsg = stripAnsi(r.errorMessage);
1198
- useError = true;
1199
- }
1200
- else {
1201
- const summary = getFlowSummaryText(r);
1202
- rawMsg = stripAnsi(summary) || "[n/a]";
1203
- }
1204
- }
1205
- }
1206
- const initialNeedsTail = !isFlowAwaiting(r) && isFlowRunning(r) && (r.streamingText != null || getLiveTextWithFallback(flowId) != null);
1207
- const initialDisplayMsg = initialNeedsTail ? tailText(rawMsg, msgBudget) : truncateChars(rawMsg, msgBudget);
1208
- container.addChild(new DynamicScrambleText(`${applyRole("treeChars", msgTree, theme, config)}${applyRole("prefixLabel", msgLabel, theme, config)}${applyRole(getContentRole("msgContent", initialDisplayMsg, useError), initialDisplayMsg, theme, config)}`, () => {
1209
- const now = Date.now();
1210
- const msgLabel = ` msg ▸ `;
1211
- const msgPrefix = `${msgTree}${msgLabel}`;
1212
- let freshRawMsg;
1213
- let needsTail;
1214
- if (isFlowAwaiting(r)) {
1215
- freshRawMsg = "[awaiting...]";
1216
- needsTail = false;
1217
- }
1218
- else if (r.status === "skipped") {
1219
- freshRawMsg = "[skipped]";
1220
- needsTail = false;
1221
- }
1222
- else {
1223
- const isRunningNow = isFlowRunning(r);
1224
- freshRawMsg = (isRunningNow ? getLiveTextWithFallback(flowId) : undefined) ?? rawMsg;
1225
- needsTail = isRunningNow && (r.streamingText != null || getLiveTextWithFallback(flowId) != null);
1226
- }
1227
- const displayMsg = needsTail ? tailText(freshRawMsg, msgBudget) : truncateChars(freshRawMsg, msgBudget);
1228
- const result = scrambleManager.updateMsg(flowId, displayMsg, now, flowComplete, undefined, true);
1229
- return `${applyRole("treeChars", msgTree, theme, config)}${applyRole("prefixLabel", msgLabel, theme, config)}${applyRole(getContentRole("msgContent", freshRawMsg, useError), result.content, theme, config)}`;
1230
- }, true));
1231
- }
1232
- }
1233
- function renderMultiFlowCollapsed(results, theme, baseId, config, sharedContext) {
1234
- return renderActivityPanel(results, theme, baseId, config, sharedContext);
1235
- }
1236
334
  //# sourceMappingURL=render.js.map