pi-agent-flow 1.8.28 → 1.8.30

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 (87) hide show
  1. package/README.md +121 -19
  2. package/agents/build.md +0 -3
  3. package/agents/ideas.md +0 -4
  4. package/agents/scout.md +0 -3
  5. package/dist/agents.d.ts +0 -2
  6. package/dist/agents.d.ts.map +1 -1
  7. package/dist/agents.js +0 -11
  8. package/dist/agents.js.map +1 -1
  9. package/dist/ask-user.d.ts +1 -1
  10. package/dist/ask-user.d.ts.map +1 -1
  11. package/dist/ask-user.js +35 -9
  12. package/dist/ask-user.js.map +1 -1
  13. package/dist/batch/batch-bash.d.ts +5 -0
  14. package/dist/batch/batch-bash.d.ts.map +1 -1
  15. package/dist/batch/batch-bash.js +39 -5
  16. package/dist/batch/batch-bash.js.map +1 -1
  17. package/dist/batch/constants.d.ts +2 -6
  18. package/dist/batch/constants.d.ts.map +1 -1
  19. package/dist/batch/constants.js +2 -0
  20. package/dist/batch/constants.js.map +1 -1
  21. package/dist/batch/execute.js +4 -4
  22. package/dist/batch/render.d.ts +1 -1
  23. package/dist/batch/render.d.ts.map +1 -1
  24. package/dist/batch/render.js +19 -3
  25. package/dist/batch/render.js.map +1 -1
  26. package/dist/executor.d.ts.map +1 -1
  27. package/dist/executor.js +6 -0
  28. package/dist/executor.js.map +1 -1
  29. package/dist/flow-prompt.d.ts +3 -2
  30. package/dist/flow-prompt.d.ts.map +1 -1
  31. package/dist/flow-prompt.js +3 -5
  32. package/dist/flow-prompt.js.map +1 -1
  33. package/dist/flow.d.ts +0 -1
  34. package/dist/flow.d.ts.map +1 -1
  35. package/dist/flow.js +1 -5
  36. package/dist/flow.js.map +1 -1
  37. package/dist/index.d.ts +2 -2
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +41 -20
  40. package/dist/index.js.map +1 -1
  41. package/dist/notify-state.d.ts +33 -0
  42. package/dist/notify-state.d.ts.map +1 -0
  43. package/dist/notify-state.js +38 -0
  44. package/dist/notify-state.js.map +1 -0
  45. package/dist/notify.d.ts.map +1 -1
  46. package/dist/notify.js +69 -2
  47. package/dist/notify.js.map +1 -1
  48. package/dist/render.d.ts.map +1 -1
  49. package/dist/render.js +280 -92
  50. package/dist/render.js.map +1 -1
  51. package/dist/scramble.d.ts +171 -0
  52. package/dist/scramble.d.ts.map +1 -0
  53. package/dist/scramble.js +2236 -0
  54. package/dist/scramble.js.map +1 -0
  55. package/dist/settings-resolver.js +2 -2
  56. package/dist/settings-resolver.js.map +1 -1
  57. package/dist/single-select-layout.js +1 -1
  58. package/dist/snapshot.d.ts +1 -6
  59. package/dist/snapshot.d.ts.map +1 -1
  60. package/dist/snapshot.js +38 -17
  61. package/dist/snapshot.js.map +1 -1
  62. package/dist/spec-mode.d.ts +13 -0
  63. package/dist/spec-mode.d.ts.map +1 -0
  64. package/dist/spec-mode.js +90 -0
  65. package/dist/spec-mode.js.map +1 -0
  66. package/dist/steering-hint.d.ts +45 -0
  67. package/dist/steering-hint.d.ts.map +1 -0
  68. package/dist/steering-hint.js +186 -0
  69. package/dist/steering-hint.js.map +1 -0
  70. package/dist/tool-utils.d.ts +0 -1
  71. package/dist/tool-utils.d.ts.map +1 -1
  72. package/dist/tool-utils.js +0 -7
  73. package/dist/tool-utils.js.map +1 -1
  74. package/dist/transitions.js +1 -1
  75. package/dist/transitions.js.map +1 -1
  76. package/dist/types.d.ts +3 -3
  77. package/dist/types.d.ts.map +1 -1
  78. package/dist/types.js +1 -1
  79. package/dist/web-tool.d.ts +13 -2
  80. package/dist/web-tool.d.ts.map +1 -1
  81. package/dist/web-tool.js +54 -11
  82. package/dist/web-tool.js.map +1 -1
  83. package/package.json +1 -1
  84. package/dist/sliding-prompt.d.ts +0 -40
  85. package/dist/sliding-prompt.d.ts.map +0 -1
  86. package/dist/sliding-prompt.js +0 -121
  87. package/dist/sliding-prompt.js.map +0 -1
package/dist/render.js CHANGED
@@ -10,7 +10,8 @@ import { Container, Markdown, Spacer, Text, TruncatedText } from "@mariozechner/
10
10
  import { getFlowSummaryText } from "./runner-events.js";
11
11
  import { aggregateFlowUsage, getFlowDisplayItems, getFlowOutput, getLastToolCall, getLastAssistantText, isFlowError, isFlowSuccess, } from "./types.js";
12
12
  import { formatBatchOpsSummary } from "./batch/render.js";
13
- import { formatCompactStats, formatCompactTokenPair, formatCountdown, formatFlowTypeName, italic, lowerFirstWord, truncateChars, tailText, getTruncationBudget, visibleLength } from "./render-utils.js";
13
+ import { scrambleManager, runScrambleTimer } from "./scramble.js";
14
+ import { formatCompactStats, formatCompactTokenPair, formatCountdown, formatFlowTypeName, italic, lowerFirstWord, truncateChars, tailText, getTruncationBudget, visibleLength, stripAnsi } from "./render-utils.js";
14
15
  function shortenPath(p) {
15
16
  const home = os.homedir();
16
17
  return p.startsWith(home) ? `~${p.slice(home.length)}` : p;
@@ -85,7 +86,7 @@ function renderFlowReport(output, theme) {
85
86
  function flowStatusIcon(r, theme) {
86
87
  if (r.exitCode === -1)
87
88
  return theme.fg("warning", "⏳");
88
- return isFlowError(r) ? theme.fg("error", "") : theme.fg("success", "");
89
+ return isFlowError(r) ? theme.fg("error", "") : theme.fg("success", "");
89
90
  }
90
91
  /** Center a label in a fixed-width header using em-dashes. Total width = 20. */
91
92
  function sectionHeader(label) {
@@ -101,15 +102,6 @@ function getLiveCountdown(r) {
101
102
  return undefined;
102
103
  return formatCountdown(r.deadlineAtMs - Date.now());
103
104
  }
104
- function formatAimLinePrefix(treePrefix, r) {
105
- const countdown = getLiveCountdown(r);
106
- const aimLabel = "aim:";
107
- return countdown ? `${treePrefix} ${aimLabel} [${countdown}] - ` : `${treePrefix} ${aimLabel} `;
108
- }
109
- function formatMsgLinePrefix(treePrefix, r) {
110
- const msgLabel = "msg:";
111
- return `${treePrefix} ${msgLabel} [${formatCompactTokenPair(r.usage)}] - `;
112
- }
113
105
  // ---------------------------------------------------------------------------
114
106
  // renderFlowCall — shown while the flow is being invoked
115
107
  // ---------------------------------------------------------------------------
@@ -124,6 +116,7 @@ export function renderFlowCall(args, theme) {
124
116
  export function renderFlowResult(result, expanded, theme, args) {
125
117
  const details = result.details;
126
118
  const streamingText = result.content?.[0]?.type === "text" ? result.content[0].text : undefined;
119
+ let container;
127
120
  if (!details || details.results.length === 0) {
128
121
  // Ghost Dashboard: render a placeholder status line during the zero state
129
122
  const flowRequest = args?.flow?.[0];
@@ -139,29 +132,45 @@ export function renderFlowResult(result, expanded, theme, args) {
139
132
  stderr: "",
140
133
  usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0, toolCalls: 0 },
141
134
  };
142
- return renderFlowCollapsed(ghostResult, flowStatusIcon(ghostResult, theme), false, streamingText || "", theme);
135
+ if (expanded) {
136
+ const now = Date.now();
137
+ container = renderFlowExpanded(ghostResult, flowStatusIcon(ghostResult, theme), false, getFlowDisplayItems([]), getFlowOutput([]), theme, "ghost", now, false, streamingText || "");
138
+ }
139
+ else {
140
+ container = renderFlowCollapsed(ghostResult, flowStatusIcon(ghostResult, theme), false, streamingText || "", theme);
141
+ }
142
+ }
143
+ else {
144
+ container = new Text(streamingText || "", 0, 0);
143
145
  }
144
- return new Text(streamingText || "", 0, 0);
145
146
  }
146
- if (details.results.length === 1) {
147
- return renderSingleFlowResult(details.results[0], expanded, theme, streamingText);
147
+ else if (details.results.length === 1) {
148
+ container = renderSingleFlowResult(details.results[0], expanded, theme, streamingText);
148
149
  }
149
- return renderMultiFlowResult(details, expanded, theme);
150
+ else {
151
+ container = renderMultiFlowResult(details, expanded, theme);
152
+ }
153
+ // Scramble animation timer — shared helper so any renderer can animate.
154
+ runScrambleTimer(args);
155
+ return container;
150
156
  }
151
157
  // ---------------------------------------------------------------------------
152
158
  // Single flow result
153
159
  // ---------------------------------------------------------------------------
154
- function renderSingleFlowResult(r, expanded, theme, streamingText) {
160
+ function renderSingleFlowResult(r, expanded, theme, streamingText, toolCallId) {
161
+ const id = toolCallId || "single";
155
162
  const error = isFlowError(r);
156
163
  const icon = flowStatusIcon(r, theme);
157
164
  const displayItems = getFlowDisplayItems(r.messages);
158
165
  const flowOutput = getFlowOutput(r.messages);
166
+ const now = Date.now();
167
+ const isComplete = r.exitCode !== -1;
159
168
  if (expanded) {
160
- return renderFlowExpanded(r, icon, error, displayItems, flowOutput, theme);
169
+ return renderFlowExpanded(r, icon, error, displayItems, flowOutput, theme, id, now, isComplete, streamingText);
161
170
  }
162
- return renderFlowCollapsed(r, icon, error, flowOutput, theme, streamingText);
171
+ return renderFlowCollapsed(r, icon, error, flowOutput, theme, streamingText, id);
163
172
  }
164
- function renderFlowExpanded(r, icon, error, displayItems, flowOutput, theme) {
173
+ function renderFlowExpanded(r, icon, error, displayItems, flowOutput, theme, id, now, isComplete, streamingText) {
165
174
  const mdTheme = getMarkdownTheme();
166
175
  const container = new Container();
167
176
  // Header: uppercase type name with dots, no icon, no source
@@ -169,22 +178,27 @@ function renderFlowExpanded(r, icon, error, displayItems, flowOutput, theme) {
169
178
  let header = theme.fg("toolTitle", theme.bold(typeName));
170
179
  if (error && r.stopReason)
171
180
  header += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
172
- container.addChild(new Text(header, 0, 0));
181
+ const plainHeader = typeName + (error && r.stopReason ? ` [${r.stopReason}]` : "");
182
+ const headerResult = scrambleManager.updateText(id, 'header', plainHeader, now, isComplete);
183
+ container.addChild(new Text(headerResult.isAnimating ? theme.fg("toolTitle", headerResult.content) : header, 0, 0));
173
184
  if (error && r.errorMessage) {
174
185
  container.addChild(new Text(theme.fg("error", `Error: ${r.errorMessage}`), 0, 0));
175
186
  }
176
187
  // Stats: dashboard format
177
188
  const inlineStats = formatCompactStats(r.usage, r.model);
178
- container.addChild(new Text(theme.fg("dim", inlineStats), 0, 0));
189
+ const statsResult = scrambleManager.updateText(id, 'stats', stripAnsi(inlineStats), now, isComplete);
190
+ container.addChild(new Text(statsResult.isAnimating ? theme.fg("dim", statsResult.content) : theme.fg("dim", inlineStats), 0, 0));
179
191
  // Intent
180
192
  container.addChild(new Spacer(1));
181
193
  container.addChild(new Text(theme.fg("muted", sectionHeader("intent")), 0, 0));
182
- container.addChild(new Text(theme.fg("dim", r.intent), 0, 0));
194
+ const intentResult = scrambleManager.updateText(id, 'intent', r.intent, now, isComplete);
195
+ container.addChild(new Text(intentResult.isAnimating ? theme.fg("dim", intentResult.content) : theme.fg("dim", r.intent), 0, 0));
183
196
  // Acceptance
184
197
  if (r.acceptance) {
185
198
  container.addChild(new Spacer(1));
186
199
  container.addChild(new Text(theme.fg("muted", sectionHeader("acceptance")), 0, 0));
187
- container.addChild(new Text(theme.fg("dim", r.acceptance), 0, 0));
200
+ const acceptanceResult = scrambleManager.updateText(id, 'acceptance', r.acceptance, now, isComplete);
201
+ container.addChild(new Text(acceptanceResult.isAnimating ? theme.fg("dim", acceptanceResult.content) : theme.fg("dim", r.acceptance), 0, 0));
188
202
  }
189
203
  // Flow report (structured output)
190
204
  container.addChild(new Spacer(1));
@@ -193,144 +207,253 @@ function renderFlowExpanded(r, icon, error, displayItems, flowOutput, theme) {
193
207
  if (r.structuredOutput) {
194
208
  const so = r.structuredOutput;
195
209
  const statusColor = so.status === "complete" ? "success" : so.status === "partial" ? "warning" : "error";
196
- container.addChild(new Text(`${theme.fg(statusColor, `[${so.status}]`)} ${theme.fg("dim", so.summary)}`, 0, 0));
210
+ const statusText = `[${so.status}] ${so.summary}`;
211
+ const statusResult = scrambleManager.updateText(id, 'report-status', statusText, now, isComplete, false);
212
+ container.addChild(new Text(statusResult.isAnimating ? `${theme.fg(statusColor, statusResult.content.split(' ')[0])} ${theme.fg("dim", statusResult.content.slice(statusResult.content.indexOf(' ') + 1))}` : `${theme.fg(statusColor, `[${so.status}]`)} ${theme.fg("dim", so.summary)}`, 0, 0));
197
213
  if (so.files.length > 0) {
198
- container.addChild(new Text(theme.fg("dim", `Files: ${so.files.map((f) => f.path).join(", ")}`), 0, 0));
214
+ const filesText = `Files: ${so.files.map((f) => f.path).join(", ")}`;
215
+ const filesResult = scrambleManager.updateText(id, 'report-files', filesText, now, isComplete, false);
216
+ container.addChild(new Text(filesResult.isAnimating ? theme.fg("dim", filesResult.content) : theme.fg("dim", filesText), 0, 0));
199
217
  }
200
218
  if (so.commands?.length > 0) {
201
219
  const cmdLabels = so.commands.map((c) => {
202
220
  const short = c.command.length > 30 ? c.command.slice(0, 30) + "..." : c.command;
203
221
  return `${c.tool ?? "cmd"}: ${short}`;
204
222
  });
205
- container.addChild(new Text(theme.fg("dim", `Commands: ${cmdLabels.join(", ")}`), 0, 0));
223
+ const commandsText = `Commands: ${cmdLabels.join(", ")}`;
224
+ const commandsResult = scrambleManager.updateText(id, 'report-commands', commandsText, now, isComplete, false);
225
+ container.addChild(new Text(commandsResult.isAnimating ? theme.fg("dim", commandsResult.content) : theme.fg("dim", commandsText), 0, 0));
206
226
  }
207
227
  if (so.notDone.length > 0) {
208
- const notDoneText = so.notDone.map((item) => {
228
+ const notDoneText = `Not Done: ${so.notDone.map((item) => {
209
229
  const details = [
210
230
  item.reason ? `reason: ${item.reason}` : undefined,
211
231
  item.blocker ? `blocker: ${item.blocker}` : undefined,
212
232
  item.nextStep ? `next: ${item.nextStep}` : undefined,
213
233
  ].filter(Boolean).join("; ");
214
234
  return details ? `${item.item} (${details})` : item.item;
215
- }).join("; ");
216
- container.addChild(new Text(theme.fg("dim", `Not Done: ${notDoneText}`), 0, 0));
235
+ }).join("; ")}`;
236
+ const notDoneResult = scrambleManager.updateText(id, 'report-notDone', notDoneText, now, isComplete, false);
237
+ container.addChild(new Text(notDoneResult.isAnimating ? theme.fg("dim", notDoneResult.content) : theme.fg("dim", notDoneText), 0, 0));
217
238
  }
218
239
  if (so.nextSteps.length > 0) {
219
- container.addChild(new Text(theme.fg("dim", `Next: ${so.nextSteps.join("; ")}`), 0, 0));
240
+ const nextStepsText = `Next: ${so.nextSteps.join("; ")}`;
241
+ const nextStepsResult = scrambleManager.updateText(id, 'report-nextSteps', nextStepsText, now, isComplete, false);
242
+ container.addChild(new Text(nextStepsResult.isAnimating ? theme.fg("dim", nextStepsResult.content) : theme.fg("dim", nextStepsText), 0, 0));
220
243
  }
221
244
  container.addChild(new Spacer(1));
222
245
  }
223
- if (flowOutput) {
246
+ // Output: animate streaming text; show clean markdown when complete
247
+ if (!isComplete && streamingText) {
248
+ const scrambled = scrambleManager.updateMsg(id, stripAnsi(streamingText), now, isComplete).content;
249
+ container.addChild(new Text(scrambled, 0, 0));
250
+ }
251
+ else if (flowOutput) {
224
252
  container.addChild(new Markdown(flowOutput.trim(), 0, 0, mdTheme));
225
253
  }
226
254
  else {
227
255
  const summary = getFlowSummaryText(r);
228
- container.addChild(new Text(theme.fg("muted", summary), 0, 0));
256
+ const summaryResult = scrambleManager.updateText(id, 'output-summary', summary, now, isComplete, false);
257
+ container.addChild(new Text(summaryResult.isAnimating ? theme.fg("muted", summaryResult.content) : theme.fg("muted", summary), 0, 0));
229
258
  }
230
- // Tool traces (expanded only)
231
- const toolTraces = renderToolTraces(displayItems, theme);
232
- if (toolTraces) {
259
+ // Tool traces (expanded only) — per-line scramble
260
+ const toolCallItems = displayItems.filter((item) => item.type === "toolCall");
261
+ if (toolCallItems.length > 0) {
233
262
  container.addChild(new Spacer(1));
234
263
  container.addChild(new Text(theme.fg("muted", sectionHeader("tool calls")), 0, 0));
235
- container.addChild(new Text(toolTraces, 0, 0));
264
+ for (let i = 0; i < toolCallItems.length; i++) {
265
+ const item = toolCallItems[i];
266
+ const lineText = theme.fg("muted", "→ ") + formatFlowToolCall(item.name, item.args, theme.fg.bind(theme));
267
+ const plainText = stripAnsi(lineText);
268
+ const scrambled = scrambleManager.updateText(id, `tool#${i}`, plainText, now, isComplete).content;
269
+ container.addChild(new Text(scrambled, 0, 0));
270
+ }
271
+ }
272
+ if (isComplete) {
273
+ scrambleManager.completeFlow(id);
236
274
  }
237
275
  return container;
238
276
  }
239
- function renderFlowCollapsed(r, icon, error, flowOutput, theme, streamingText) {
277
+ function renderFlowCollapsed(r, icon, error, flowOutput, theme, streamingText, toolCallId) {
278
+ const id = toolCallId || "collapsed";
279
+ const now = Date.now();
240
280
  const container = new Container();
241
281
  const maxWidth = process.stdout.columns ?? 80;
242
282
  const stats = formatCompactStats(r.usage, r.model, maxWidth, { skipTokens: true, skipContext: true, hideModel: true });
283
+ const isComplete = r.exitCode !== -1;
284
+ // Flash TPS value when it changes
285
+ const tpsMatch = stats.match(/tps:\s*(\S+)/);
286
+ let displayStats = stats;
287
+ if (tpsMatch) {
288
+ const scrambledTps = scrambleManager.updateTps(id, tpsMatch[1], now, isComplete, true);
289
+ if (scrambledTps !== tpsMatch[1]) {
290
+ displayStats = stats.replace(tpsMatch[1], scrambledTps);
291
+ }
292
+ }
243
293
  const typeName = formatCollapsedFlowHeaderTypeName(r.type);
244
294
  const modelLabel = r.model ? r.model.replace(/^[^/]+\//, "").toLowerCase() : "";
245
- let header = `${theme.fg("accent", theme.bold(typeName))}${theme.fg("dim", modelLabel ? ` - ${modelLabel} - ` : " - ")}${theme.fg("dim", stats)}`;
295
+ let header = `${theme.fg("accent", theme.bold(typeName))}${theme.fg("dim", modelLabel ? ` - ${modelLabel} - ` : " - ")}${theme.fg("dim", displayStats)}`;
246
296
  if (error && r.stopReason)
247
297
  header += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
248
- container.addChild(new TruncatedText(header, 0, 0));
249
- // aim: line (short headline)
298
+ // Scramble header on first render; show full styled header when complete
299
+ const plainHeader = typeName + (modelLabel ? ` - ${modelLabel} - ` : " - ") + stripAnsi(displayStats) + (error && r.stopReason ? ` [${r.stopReason}]` : "");
300
+ const headerResult = scrambleManager.updateText(id, 'header', plainHeader, now, isComplete, true);
301
+ const headerDisplay = headerResult.isAnimating ? theme.fg("accent", headerResult.content) : header;
302
+ container.addChild(new TruncatedText(headerDisplay, 0, 0));
303
+ // aim: line — cascade/ripple/illuminate on text change
250
304
  if (r.aim) {
251
- const aimPrefix = formatAimLinePrefix("├─", r);
252
- const dirContent = truncateChars(lowerFirstWord(r.aim), getTruncationBudget(visibleLength(aimPrefix)));
253
- container.addChild(new TruncatedText(`${theme.fg("dim", aimPrefix)}${theme.fg("dim", italic(dirContent))}`, 0, 0));
305
+ const countdown = getLiveCountdown(r);
306
+ const treePrefix = "├─";
307
+ const aimPrefix = countdown
308
+ ? `${treePrefix} aim: [${countdown}] - `
309
+ : `${treePrefix} aim: `;
310
+ const budget = getTruncationBudget(visibleLength(aimPrefix));
311
+ const displayAim = truncateChars(lowerFirstWord(r.aim), budget);
312
+ const aimResult = scrambleManager.updateAim(id, displayAim, now, isComplete, true);
313
+ const aimContent = aimResult.content;
314
+ container.addChild(new TruncatedText(`${theme.fg("dim", aimPrefix)}${theme.fg("dim", italic(aimContent))}`, 0, 0));
254
315
  }
255
316
  // act: line (last tool call with count)
256
317
  const lastTool = getLastToolCall(r.messages);
257
318
  if (lastTool) {
258
319
  const actStr = formatFlowToolCall(lastTool.name, lastTool.args, theme.fg.bind(theme));
259
- const actPrefix = `├─ act: [${r.usage.toolCalls}] - `;
260
- const actContent = truncateChars(lowerFirstWord(actStr), getTruncationBudget(visibleLength(actPrefix)));
320
+ const prefixStub = `├─ act: [${r.usage.toolCalls}] - `;
321
+ const budget = getTruncationBudget(visibleLength(prefixStub));
322
+ const actFullText = stripAnsi(lowerFirstWord(actStr));
323
+ let actContent;
324
+ if (scrambleManager.getMode() === 'stream') {
325
+ actContent = scrambleManager.streamAct(id, actFullText, now, isComplete, budget);
326
+ }
327
+ else {
328
+ const displayAct = truncateChars(actFullText, budget);
329
+ actContent = scrambleManager.updateAct(id, displayAct, now, isComplete, true).content;
330
+ }
331
+ let actKpi = String(r.usage.toolCalls);
332
+ const scrambledActKpi = scrambleManager.updateActKpi(id, actKpi, now, isComplete, true);
333
+ if (scrambledActKpi !== actKpi) {
334
+ actKpi = scrambledActKpi;
335
+ }
336
+ const actPrefix = `├─ act: [${actKpi}] - `;
261
337
  container.addChild(new TruncatedText(`${theme.fg("dim", actPrefix)}${italic(actContent)}`, 0, 0));
262
338
  }
263
339
  // msg: line (last assistant text or streaming)
264
- const msgPrefix = formatMsgLinePrefix("└─", r);
265
- const msgBudget = getTruncationBudget(visibleLength(msgPrefix));
340
+ let msgKpi = formatCompactTokenPair(r.usage);
341
+ const scrambledMsgKpi = scrambleManager.updateMsgKpi(id, msgKpi, now, isComplete, false);
342
+ if (scrambledMsgKpi !== msgKpi) {
343
+ msgKpi = scrambledMsgKpi;
344
+ }
345
+ const msgPrefixStub = `└─ msg: [${msgKpi}] - `;
346
+ const msgBudget = getTruncationBudget(visibleLength(msgPrefixStub));
347
+ let rawMsg;
348
+ let useError = false;
266
349
  if (r.exitCode === -1 && streamingText) {
267
- const logContent = tailText(streamingText, msgBudget);
268
- container.addChild(new TruncatedText(`${theme.fg("dim", msgPrefix)}${theme.fg("dim", italic(logContent))}`, 0, 0));
350
+ rawMsg = stripAnsi(streamingText);
269
351
  }
270
352
  else if (r.structuredOutput?.summary) {
271
- const logContent = truncateChars(r.structuredOutput.summary, msgBudget);
272
- container.addChild(new TruncatedText(`${theme.fg("dim", msgPrefix)}${theme.fg("dim", italic(logContent))}`, 0, 0));
353
+ rawMsg = stripAnsi(r.structuredOutput.summary);
273
354
  }
274
355
  else if (flowOutput) {
275
- const logContent = tailText(flowOutput, msgBudget);
276
- container.addChild(new TruncatedText(`${theme.fg("dim", msgPrefix)}${theme.fg("dim", italic(logContent))}`, 0, 0));
356
+ rawMsg = stripAnsi(flowOutput);
277
357
  }
278
358
  else if (streamingText) {
279
- const logContent = tailText(streamingText, msgBudget);
280
- container.addChild(new TruncatedText(`${theme.fg("dim", msgPrefix)}${theme.fg("dim", italic(logContent))}`, 0, 0));
359
+ rawMsg = stripAnsi(streamingText);
281
360
  }
282
361
  else if (error && r.errorMessage) {
283
- const logContent = truncateChars(r.errorMessage, msgBudget);
284
- container.addChild(new TruncatedText(`${theme.fg("dim", msgPrefix)}${theme.fg("error", italic(logContent))}`, 0, 0));
362
+ rawMsg = stripAnsi(r.errorMessage);
363
+ useError = true;
364
+ }
365
+ else {
366
+ rawMsg = "[n/a]";
367
+ }
368
+ let msgContent;
369
+ if (scrambleManager.getMode() === 'stream') {
370
+ msgContent = scrambleManager.streamMsg(id, rawMsg, now, isComplete, msgBudget);
285
371
  }
286
372
  else {
287
- container.addChild(new TruncatedText(`${theme.fg("dim", msgPrefix)}${theme.fg("dim", italic("[n/a]"))}`, 0, 0));
373
+ // For active (incomplete) flows, pass full text to keep animation stable.
374
+ // TruncatedText handles display truncation. Completed flows truncate as before.
375
+ if (!isComplete) {
376
+ msgContent = scrambleManager.updateMsg(id, rawMsg, now, isComplete, undefined, true).content;
377
+ }
378
+ else {
379
+ const needsTail = (r.exitCode === -1 && streamingText) || streamingText;
380
+ const displayMsg = needsTail ? tailText(rawMsg, msgBudget) : truncateChars(rawMsg, msgBudget);
381
+ msgContent = scrambleManager.updateMsg(id, displayMsg, now, isComplete, undefined, true).content;
382
+ }
383
+ }
384
+ const msgPrefix = `└─ msg: [${msgKpi}] - `;
385
+ container.addChild(new TruncatedText(`${theme.fg("dim", msgPrefix)}${theme.fg(useError ? "error" : "dim", italic(msgContent))}`, 0, 0));
386
+ if (isComplete) {
387
+ scrambleManager.completeFlow(id);
288
388
  }
289
389
  return container;
290
390
  }
291
391
  // ---------------------------------------------------------------------------
292
392
  // Multi-flow result
293
393
  // ---------------------------------------------------------------------------
294
- function renderMultiFlowResult(details, expanded, theme) {
394
+ function renderMultiFlowResult(details, expanded, theme, toolCallId) {
395
+ const baseId = toolCallId || "multi";
295
396
  const results = details.results;
296
397
  const successCount = results.filter((r) => isFlowSuccess(r)).length;
297
398
  const failCount = results.filter((r) => isFlowError(r)).length;
298
- const icon = failCount > 0 ? theme.fg("warning", "◐") : theme.fg("success", "");
399
+ const icon = failCount > 0 ? theme.fg("warning", "◐") : theme.fg("success", "");
400
+ const now = Date.now();
299
401
  if (expanded) {
300
- return renderMultiFlowExpanded(results, successCount, icon, theme);
402
+ return renderMultiFlowExpanded(results, successCount, icon, theme, baseId, now);
301
403
  }
302
- return renderMultiFlowCollapsed(results, theme);
404
+ return renderMultiFlowCollapsed(results, theme, baseId);
303
405
  }
304
- function renderMultiFlowExpanded(results, successCount, icon, theme) {
406
+ function renderMultiFlowExpanded(results, successCount, icon, theme, baseId, now) {
305
407
  const mdTheme = getMarkdownTheme();
306
408
  const container = new Container();
307
409
  // Summary: just show count, no icon
308
410
  container.addChild(new Text(theme.fg("accent", `${results.length} flows`), 0, 0));
309
- for (const r of results) {
411
+ for (let flowIdx = 0; flowIdx < results.length; flowIdx++) {
412
+ const r = results[flowIdx];
413
+ const flowId = `${baseId}#${flowIdx}`;
414
+ const isComplete = r.exitCode !== -1;
310
415
  const displayItems = getFlowDisplayItems(r.messages);
311
416
  const flowOutput = getFlowOutput(r.messages);
312
417
  const typeName = formatFlowTypeName(r.type);
313
418
  container.addChild(new Spacer(1));
314
419
  // Per-flow header: ─── EXPLORER (no icon)
315
- container.addChild(new Text(theme.fg("muted", sectionHeader(typeName)), 0, 0));
420
+ const headerResult = scrambleManager.updateText(flowId, 'header', typeName, now, isComplete, true);
421
+ container.addChild(new Text(headerResult.isAnimating ? theme.fg("muted", headerResult.content) : theme.fg("muted", sectionHeader(typeName)), 0, 0));
316
422
  // Stats: dashboard format
317
423
  const flowStats = formatCompactStats(r.usage, r.model);
318
- container.addChild(new Text(theme.fg("dim", flowStats), 0, 0));
424
+ const statsResult = scrambleManager.updateText(flowId, 'stats', stripAnsi(flowStats), now, isComplete, true);
425
+ container.addChild(new Text(statsResult.isAnimating ? theme.fg("dim", statsResult.content) : theme.fg("dim", flowStats), 0, 0));
319
426
  // Intent: just show text, no prefix
320
- container.addChild(new Text(theme.fg("dim", r.intent), 0, 0));
427
+ const intentResult = scrambleManager.updateText(flowId, 'intent', r.intent, now, isComplete, true);
428
+ container.addChild(new Text(intentResult.isAnimating ? theme.fg("dim", intentResult.content) : theme.fg("dim", r.intent), 0, 0));
321
429
  if (r.acceptance) {
322
- container.addChild(new Text(theme.fg("dim", `Acceptance: ${r.acceptance}`), 0, 0));
430
+ const acceptanceResult = scrambleManager.updateText(flowId, 'acceptance', r.acceptance, now, isComplete, true);
431
+ container.addChild(new Text(acceptanceResult.isAnimating ? theme.fg("dim", acceptanceResult.content) : theme.fg("dim", `Acceptance: ${r.acceptance}`), 0, 0));
323
432
  }
324
- if (flowOutput) {
433
+ // Output: animate streaming text; show clean markdown when complete
434
+ if (!isComplete && r.streamingText) {
435
+ const scrambled = scrambleManager.updateMsg(flowId, stripAnsi(r.streamingText), now, isComplete, undefined, true).content;
436
+ container.addChild(new Text(scrambled, 0, 0));
437
+ }
438
+ else if (flowOutput) {
325
439
  container.addChild(new Spacer(1));
326
440
  container.addChild(new Markdown(flowOutput.trim(), 0, 0, mdTheme));
327
441
  }
328
- // Tool traces in expanded view
329
- const toolTraces = renderToolTraces(displayItems, theme);
330
- if (toolTraces) {
442
+ // Tool traces in expanded view — per-line scramble
443
+ const toolCallItems = displayItems.filter((item) => item.type === "toolCall");
444
+ if (toolCallItems.length > 0) {
331
445
  container.addChild(new Spacer(1));
332
446
  container.addChild(new Text(theme.fg("muted", sectionHeader("tool calls")), 0, 0));
333
- container.addChild(new Text(toolTraces, 0, 0));
447
+ for (let i = 0; i < toolCallItems.length; i++) {
448
+ const item = toolCallItems[i];
449
+ const lineText = theme.fg("muted", "→ ") + formatFlowToolCall(item.name, item.args, theme.fg.bind(theme));
450
+ const plainText = stripAnsi(lineText);
451
+ const scrambled = scrambleManager.updateText(flowId, `tool#${i}`, plainText, now, isComplete).content;
452
+ container.addChild(new Text(scrambled, 0, 0));
453
+ }
454
+ }
455
+ if (isComplete) {
456
+ scrambleManager.completeFlow(flowId);
334
457
  }
335
458
  }
336
459
  // Total stats: dashboard format
@@ -341,54 +464,119 @@ function renderMultiFlowExpanded(results, successCount, icon, theme) {
341
464
  container.addChild(new Text(theme.fg("dim", totalStats), 0, 0));
342
465
  return container;
343
466
  }
344
- function renderActivityPanel(results, theme) {
467
+ function renderActivityPanel(results, theme, baseId) {
468
+ const idPrefix = baseId || "panel";
345
469
  const container = new Container();
346
470
  const maxWidth = process.stdout.columns ?? 80;
471
+ const now = Date.now();
347
472
  for (let i = 0; i < results.length; i++) {
348
473
  const r = results[i];
349
474
  const isLast = i === results.length - 1;
475
+ const flowId = `${idPrefix}#${i}`;
350
476
  const stats = formatCompactStats(r.usage, r.model, maxWidth, { skipTokens: true, skipContext: true, hideModel: true });
477
+ // Flash TPS value when it changes
478
+ const tpsMatch = stats.match(/tps:\s*(\S+)/);
479
+ const flowComplete = r.exitCode !== -1;
480
+ let displayStats = stats;
481
+ if (tpsMatch) {
482
+ const scrambledTps = scrambleManager.updateTps(flowId, tpsMatch[1], now, flowComplete, true);
483
+ if (scrambledTps !== tpsMatch[1]) {
484
+ displayStats = stats.replace(tpsMatch[1], scrambledTps);
485
+ }
486
+ }
351
487
  const error = isFlowError(r);
352
488
  const typeName = formatCollapsedFlowHeaderTypeName(r.type);
353
489
  // Header line
354
490
  const headerPrefix = isLast ? "└─" : "├─";
355
491
  const modelLabel = r.model ? r.model.replace(/^[^/]+\//, "").toLowerCase() : "";
356
- let headerLine = `${theme.fg("dim", headerPrefix)} ${theme.fg("accent", theme.bold(typeName))}${theme.fg("dim", modelLabel ? ` - ${modelLabel} - ` : " - ")}${theme.fg("dim", stats)}`;
492
+ let headerLine = `${theme.fg("dim", headerPrefix)} ${theme.fg("accent", theme.bold(typeName))}${theme.fg("dim", modelLabel ? ` - ${modelLabel} - ` : " - ")}${theme.fg("dim", displayStats)}`;
357
493
  if (error && r.stopReason) {
358
494
  headerLine += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
359
495
  }
360
- container.addChild(new TruncatedText(headerLine, 0, 0));
496
+ const plainHeader = headerPrefix + " " + typeName + (modelLabel ? ` - ${modelLabel} - ` : " - ") + stripAnsi(displayStats) + (error && r.stopReason ? ` [${r.stopReason}]` : "");
497
+ const headerResult = scrambleManager.updateText(flowId, 'header', plainHeader, now, flowComplete, true);
498
+ const headerDisplay = headerResult.isAnimating ? theme.fg("accent", headerResult.content) : headerLine;
499
+ container.addChild(new TruncatedText(headerDisplay, 0, 0));
361
500
  // Continuation indent for sub-lines
362
501
  const indent = isLast ? " " : "│ ";
363
- // aim: line (short headline)
502
+ // aim: line cascade/ripple/illuminate on text change
364
503
  if (r.aim) {
365
- const aimPrefix = formatAimLinePrefix(indent + "├─", r);
366
- const dirContent = truncateChars(lowerFirstWord(r.aim), getTruncationBudget(visibleLength(aimPrefix)));
367
- container.addChild(new TruncatedText(`${theme.fg("dim", aimPrefix)}${theme.fg("dim", italic(dirContent))}`, 0, 0));
504
+ const countdown = getLiveCountdown(r);
505
+ const treePrefix = indent + "├─";
506
+ const aimPrefix = countdown
507
+ ? `${treePrefix} aim: [${countdown}] - `
508
+ : `${treePrefix} aim: `;
509
+ const budget = getTruncationBudget(visibleLength(aimPrefix));
510
+ const displayAim = truncateChars(lowerFirstWord(r.aim), budget);
511
+ const aimResult = scrambleManager.updateAim(flowId, displayAim, now, flowComplete, true);
512
+ const aimContent = aimResult.content;
513
+ container.addChild(new TruncatedText(`${theme.fg("dim", aimPrefix)}${theme.fg("dim", italic(aimContent))}`, 0, 0));
368
514
  }
369
515
  // act: line (last tool call with count)
370
516
  const lastTool = getLastToolCall(r.messages);
371
517
  if (lastTool) {
372
518
  const actStr = formatFlowToolCall(lastTool.name, lastTool.args, theme.fg.bind(theme));
373
- const actPrefix = `${indent}├─ act: [${r.usage.toolCalls}] - `;
374
- const actContent = truncateChars(lowerFirstWord(actStr), getTruncationBudget(visibleLength(actPrefix)));
519
+ const prefixStub = `${indent}├─ act: [${r.usage.toolCalls}] - `;
520
+ const budget = getTruncationBudget(visibleLength(prefixStub));
521
+ const actFullText = stripAnsi(lowerFirstWord(actStr));
522
+ let actContent;
523
+ if (scrambleManager.getMode() === 'stream') {
524
+ actContent = scrambleManager.streamAct(flowId, actFullText, now, flowComplete, budget);
525
+ }
526
+ else {
527
+ const displayAct = truncateChars(actFullText, budget);
528
+ actContent = scrambleManager.updateAct(flowId, displayAct, now, flowComplete, true).content;
529
+ }
530
+ let actKpi = String(r.usage.toolCalls);
531
+ const scrambledActKpi = scrambleManager.updateActKpi(flowId, actKpi, now, flowComplete, false);
532
+ if (scrambledActKpi !== actKpi) {
533
+ actKpi = scrambledActKpi;
534
+ }
535
+ const actPrefix = `${indent}├─ act: [${actKpi}] - `;
375
536
  container.addChild(new TruncatedText(`${theme.fg("dim", actPrefix)}${italic(actContent)}`, 0, 0));
376
537
  }
377
538
  // msg: line (live streaming text or last assistant text)
378
- const msgPrefix = formatMsgLinePrefix(indent + "└─", r);
379
- const msgBudget = getTruncationBudget(visibleLength(msgPrefix));
539
+ let msgKpi = formatCompactTokenPair(r.usage);
540
+ const scrambledMsgKpi = scrambleManager.updateMsgKpi(flowId, msgKpi, now, flowComplete, false);
541
+ if (scrambledMsgKpi !== msgKpi) {
542
+ msgKpi = scrambledMsgKpi;
543
+ }
544
+ const msgPrefixStub = `${indent}└─ msg: [${msgKpi}] - `;
545
+ const msgBudget = getTruncationBudget(visibleLength(msgPrefixStub));
380
546
  const liveText = r.exitCode === -1 ? r.streamingText : undefined;
381
547
  const lastText = liveText || getLastAssistantText(r.messages);
548
+ let rawMsg;
549
+ let useError = false;
382
550
  if (lastText) {
383
- const logContent = tailText(lastText, msgBudget);
384
- container.addChild(new TruncatedText(`${theme.fg("dim", msgPrefix)}${theme.fg("dim", italic(logContent))}`, 0, 0));
551
+ rawMsg = stripAnsi(lastText);
385
552
  }
386
553
  else if (error && r.errorMessage) {
387
- const logContent = truncateChars(r.errorMessage, msgBudget);
388
- container.addChild(new TruncatedText(`${theme.fg("dim", msgPrefix)}${theme.fg("error", italic(logContent))}`, 0, 0));
554
+ rawMsg = stripAnsi(r.errorMessage);
555
+ useError = true;
556
+ }
557
+ else {
558
+ rawMsg = "[n/a]";
559
+ }
560
+ let msgContent;
561
+ if (scrambleManager.getMode() === 'stream') {
562
+ msgContent = scrambleManager.streamMsg(flowId, rawMsg, now, flowComplete, msgBudget);
389
563
  }
390
564
  else {
391
- container.addChild(new TruncatedText(`${theme.fg("dim", msgPrefix)}${theme.fg("dim", italic("[n/a]"))}`, 0, 0));
565
+ // For active (incomplete) flows, pass full text to keep animation stable.
566
+ // TruncatedText handles display truncation. Completed flows truncate as before.
567
+ if (!flowComplete) {
568
+ msgContent = scrambleManager.updateMsg(flowId, rawMsg, now, flowComplete, undefined, true).content;
569
+ }
570
+ else {
571
+ const needsTail = Boolean(liveText || lastText);
572
+ const displayMsg = needsTail ? tailText(rawMsg, msgBudget) : truncateChars(rawMsg, msgBudget);
573
+ msgContent = scrambleManager.updateMsg(flowId, displayMsg, now, flowComplete).content;
574
+ }
575
+ }
576
+ const msgPrefix = `${indent}└─ msg: [${msgKpi}] - `;
577
+ container.addChild(new TruncatedText(`${theme.fg("dim", msgPrefix)}${theme.fg(useError ? "error" : "dim", italic(msgContent))}`, 0, 0));
578
+ if (flowComplete) {
579
+ scrambleManager.completeFlow(flowId);
392
580
  }
393
581
  // Add blank line separator between flows (with continuation pipe)
394
582
  if (!isLast) {
@@ -398,7 +586,7 @@ function renderActivityPanel(results, theme) {
398
586
  container.addChild(new TruncatedText(theme.fg("muted", "(Ctrl+O to expand tool traces)"), 0, 0));
399
587
  return container;
400
588
  }
401
- function renderMultiFlowCollapsed(results, theme) {
402
- return renderActivityPanel(results, theme);
589
+ function renderMultiFlowCollapsed(results, theme, baseId) {
590
+ return renderActivityPanel(results, theme, baseId);
403
591
  }
404
592
  //# sourceMappingURL=render.js.map