pi-prompt-template-model 0.6.6 → 0.6.7

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.6.7] - 2026-03-28
4
+
5
+ ### Changed
6
+ - Delegation progress widget now shows a unified tool stream where completed tools scroll chronologically with the active tool highlighted at the bottom, replacing the old design where the active tool flashed at the top and disappeared on completion.
7
+ - Delegation progress widget now renders the subagent's model name in the header (e.g., `delegate [fork] gpt-5.3-codex | 14 tools, 170k tok, 2m47s`).
8
+ - Delegation progress widget no longer caps tool history or output lines. The box grows to show the full execution trace.
9
+ - Delegation progress widget now refreshes every second during idle periods (model thinking) so the elapsed timer ticks smoothly instead of freezing between progress updates.
10
+ - Enriched the delegation bridge protocol to pass through full `recentOutputLines`, `recentTools` history, and `model` from pi-subagents progress data, replacing the old single-line `recentOutput` and missing tool/model fields.
11
+ - Removed `lastTool`/`lastToolArgs` tracking from live state (dead code after the unified tool stream redesign).
12
+
3
13
  ## [0.6.6] - 2026-03-28
4
14
 
5
15
  ### Added
package/index.ts CHANGED
@@ -132,9 +132,12 @@ export default function promptModelExtension(pi: ExtensionAPI) {
132
132
 
133
133
  for (const command of pi.getCommands()) {
134
134
  if (command.source !== "skill") continue;
135
- if (!command.sourceInfo.path) continue;
135
+ const sourceInfo = "sourceInfo" in command
136
+ ? (command as { sourceInfo?: { path?: string } }).sourceInfo
137
+ : undefined;
138
+ if (!sourceInfo?.path) continue;
136
139
  if (!candidates.has(command.name)) continue;
137
- return command.sourceInfo.path;
140
+ return sourceInfo.path;
138
141
  }
139
142
 
140
143
  return undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-prompt-template-model",
3
- "version": "0.6.6",
3
+ "version": "0.6.7",
4
4
  "type": "module",
5
5
  "description": "Prompt template model selector extension for pi coding agent",
6
6
  "author": "Nico Bailon",
@@ -52,6 +52,9 @@ export interface DelegatedSubagentUpdate {
52
52
  currentTool?: string;
53
53
  currentToolArgs?: string;
54
54
  recentOutput?: string;
55
+ recentOutputLines?: string[];
56
+ recentTools?: Array<{ tool: string; args: string }>;
57
+ model?: string;
55
58
  toolCount?: number;
56
59
  durationMs?: number;
57
60
  tokens?: number;
@@ -65,6 +68,9 @@ export interface DelegatedSubagentTaskProgress {
65
68
  currentTool?: string;
66
69
  currentToolArgs?: string;
67
70
  recentOutput?: string;
71
+ recentOutputLines?: string[];
72
+ recentTools?: Array<{ tool: string; args: string }>;
73
+ model?: string;
68
74
  toolCount?: number;
69
75
  durationMs?: number;
70
76
  tokens?: number;
@@ -74,9 +80,9 @@ export interface DelegatedSubagentLiveState {
74
80
  status?: string;
75
81
  currentTool?: string;
76
82
  currentToolArgs?: string;
77
- lastTool?: string;
78
- lastToolArgs?: string;
79
83
  recentOutput: string[];
84
+ recentTools: Array<{ tool: string; args: string }>;
85
+ model?: string;
80
86
  toolCount: number;
81
87
  durationMs: number;
82
88
  tokens: number;
@@ -151,6 +157,7 @@ export function updateDelegatedLiveState(requestId: string, update: Partial<Dele
151
157
  const now = Date.now();
152
158
  const existing = delegatedLiveState.get(requestId) ?? {
153
159
  recentOutput: [],
160
+ recentTools: [],
154
161
  toolCount: 0,
155
162
  durationMs: 0,
156
163
  tokens: 0,
@@ -158,21 +165,16 @@ export function updateDelegatedLiveState(requestId: string, update: Partial<Dele
158
165
  startedAt: now,
159
166
  updatedAt: now,
160
167
  };
161
- // When a tool finishes (currentTool goes undefined), preserve it as lastTool
162
- const toolJustCleared = update.currentTool === undefined && existing.currentTool !== undefined;
163
- const lastTool = toolJustCleared ? existing.currentTool : (update.currentTool ?? existing.lastTool);
164
- const lastToolArgs = toolJustCleared ? existing.currentToolArgs : (update.currentToolArgs ?? existing.lastToolArgs);
165
-
166
168
  const next: DelegatedSubagentLiveState = {
167
169
  ...existing,
168
170
  ...update,
169
171
  recentOutput: update.recentOutput ?? existing.recentOutput,
172
+ recentTools: update.recentTools ?? existing.recentTools,
173
+ model: update.model ?? existing.model,
170
174
  toolCount: update.toolCount ?? existing.toolCount,
171
175
  durationMs: update.durationMs ?? (now - existing.startedAt),
172
176
  tokens: update.tokens ?? existing.tokens,
173
177
  taskProgress: update.taskProgress ?? existing.taskProgress,
174
- lastTool,
175
- lastToolArgs,
176
178
  startedAt: existing.startedAt,
177
179
  updatedAt: now,
178
180
  };
@@ -184,6 +186,7 @@ export function appendDelegatedLiveOutput(requestId: string, line?: string): voi
184
186
  const fallbackNow = Date.now();
185
187
  const existing = delegatedLiveState.get(requestId) ?? {
186
188
  recentOutput: [],
189
+ recentTools: [],
187
190
  toolCount: 0,
188
191
  durationMs: 0,
189
192
  tokens: 0,
@@ -191,7 +194,7 @@ export function appendDelegatedLiveOutput(requestId: string, line?: string): voi
191
194
  startedAt: fallbackNow,
192
195
  updatedAt: fallbackNow,
193
196
  };
194
- const recentOutput = [...existing.recentOutput, line].slice(-12);
197
+ const recentOutput = [...existing.recentOutput, line];
195
198
  delegatedLiveState.set(requestId, {
196
199
  ...existing,
197
200
  recentOutput,
package/subagent-step.ts CHANGED
@@ -204,6 +204,9 @@ function formatProgressStatus(update: DelegatedSubagentUpdate): string | undefin
204
204
  if (update.currentTool) {
205
205
  return `running ${update.currentTool}${update.currentToolArgs ? ` ${update.currentToolArgs}` : ""}`;
206
206
  }
207
+ if (update.taskProgress?.some((task) => task.status === "running")) {
208
+ return "running";
209
+ }
207
210
  if (update.toolCount && update.toolCount > 0) {
208
211
  return `completed ${update.toolCount} tool${update.toolCount === 1 ? "" : "s"}`;
209
212
  }
@@ -216,6 +219,38 @@ function formatParallelProgressStatus(update: DelegatedSubagentUpdate): string |
216
219
  return `parallel ${completed}/${update.taskProgress.length} running`;
217
220
  }
218
221
 
222
+ function hasOwn<T extends object>(value: T, key: PropertyKey): boolean {
223
+ return Object.prototype.hasOwnProperty.call(value, key);
224
+ }
225
+
226
+ function sanitizeOutputLines(lines: string[] | undefined): string[] {
227
+ if (!lines || lines.length === 0) return [];
228
+ return lines.filter((line): line is string => typeof line === "string" && line.trim() && line.trim() !== "(running...)");
229
+ }
230
+
231
+ function collectNewOutputLines(previous: string[] | undefined, next: string[] | undefined): string[] {
232
+ const previousLines = sanitizeOutputLines(previous);
233
+ const nextLines = sanitizeOutputLines(next);
234
+ if (nextLines.length === 0) return [];
235
+ if (previousLines.length === 0) return nextLines;
236
+
237
+ const overlapLimit = Math.min(previousLines.length, nextLines.length);
238
+ for (let overlap = overlapLimit; overlap > 0; overlap--) {
239
+ let matches = true;
240
+ for (let index = 0; index < overlap; index++) {
241
+ if (previousLines[previousLines.length - overlap + index] !== nextLines[index]) {
242
+ matches = false;
243
+ break;
244
+ }
245
+ }
246
+ if (matches) {
247
+ return nextLines.slice(overlap);
248
+ }
249
+ }
250
+
251
+ return nextLines;
252
+ }
253
+
219
254
  function mergeTaskProgress(
220
255
  requestTasks: DelegatedSubagentTask[] | undefined,
221
256
  existingProgress: DelegatedSubagentTaskProgress[] | undefined,
@@ -235,6 +270,9 @@ function mergeTaskProgress(
235
270
  currentTool: existing?.currentTool,
236
271
  currentToolArgs: existing?.currentToolArgs,
237
272
  recentOutput: existing?.recentOutput,
273
+ recentOutputLines: existing?.recentOutputLines,
274
+ recentTools: existing?.recentTools,
275
+ model: existing?.model ?? task.model,
238
276
  toolCount: existing?.toolCount,
239
277
  durationMs: existing?.durationMs,
240
278
  tokens: existing?.tokens,
@@ -255,11 +293,20 @@ function mergeTaskProgress(
255
293
  }
256
294
  if (targetIndex < 0) continue;
257
295
  consumed.add(targetIndex);
296
+ const current = merged[targetIndex]!;
258
297
  merged[targetIndex] = {
259
- ...merged[targetIndex],
260
- ...entry,
261
298
  index: targetIndex,
262
- agent: merged[targetIndex]!.agent,
299
+ agent: current.agent,
300
+ status: entry.status ?? current.status,
301
+ currentTool: hasOwn(entry, "currentTool") ? entry.currentTool : current.currentTool,
302
+ currentToolArgs: hasOwn(entry, "currentToolArgs") ? entry.currentToolArgs : current.currentToolArgs,
303
+ recentOutput: entry.recentOutput ?? current.recentOutput,
304
+ recentOutputLines: entry.recentOutputLines ?? current.recentOutputLines,
305
+ recentTools: entry.recentTools ?? current.recentTools,
306
+ model: entry.model ?? current.model,
307
+ toolCount: entry.toolCount ?? current.toolCount,
308
+ durationMs: entry.durationMs ?? current.durationMs,
309
+ tokens: entry.tokens ?? current.tokens,
263
310
  };
264
311
  }
265
312
 
@@ -316,18 +363,29 @@ async function requestDelegatedRun(
316
363
 
317
364
  let lastProgressStatus = "";
318
365
  let widgetSet = false;
366
+ let refreshTimer: ReturnType<typeof setInterval> | null = null;
319
367
 
320
368
  const showWidget = () => {
321
369
  if (!ctx.hasUI || widgetSet) return;
322
370
  widgetSet = true;
323
371
  ctx.ui.setWidget(
324
372
  DELEGATED_WIDGET_KEY,
325
- (_tui, theme) => createDelegatedProgressWidget(request.requestId, request.agent, request.context, request.task, request.tasks, theme),
373
+ (_tui, theme) => createDelegatedProgressWidget(request.requestId, request.agent, request.context, request.task, request.tasks, theme, request.model),
326
374
  { placement: "aboveEditor" },
327
375
  );
376
+ // Force TUI repaints every second so the elapsed timer ticks during idle periods
377
+ refreshTimer = setInterval(() => {
378
+ if (done) return;
379
+ const statusLine = lastProgressStatus || "running...";
380
+ ctx.ui.setStatus("prompt-subagent", `delegating to ${requestLabel} · ${statusLine}`);
381
+ }, 1000);
328
382
  };
329
383
 
330
384
  const clearWidget = () => {
385
+ if (refreshTimer) {
386
+ clearInterval(refreshTimer);
387
+ refreshTimer = null;
388
+ }
331
389
  if (ctx.hasUI && widgetSet) {
332
390
  ctx.ui.setWidget(DELEGATED_WIDGET_KEY, undefined);
333
391
  widgetSet = false;
@@ -338,9 +396,11 @@ async function requestDelegatedRun(
338
396
  if (done || !data || typeof data !== "object") return;
339
397
  const update = data as DelegatedSubagentUpdate;
340
398
  if (update.requestId !== request.requestId) return;
399
+
400
+ const previousTaskProgress = getDelegatedLiveState(request.requestId)?.taskProgress;
341
401
  const mergedTaskProgress = mergeTaskProgress(
342
402
  request.tasks,
343
- getDelegatedLiveState(request.requestId)?.taskProgress,
403
+ previousTaskProgress,
344
404
  update.taskProgress,
345
405
  );
346
406
  const isParallel = (request.tasks?.length ?? 0) > 0;
@@ -353,18 +413,46 @@ async function requestDelegatedRun(
353
413
  if (progressStatus) {
354
414
  lastProgressStatus = progressStatus;
355
415
  }
416
+
356
417
  updateDelegatedLiveState(request.requestId, {
357
418
  status: progressStatus ?? (lastProgressStatus || "running..."),
358
419
  currentTool: update.currentTool,
359
420
  currentToolArgs: update.currentToolArgs,
421
+ recentTools: update.recentTools,
422
+ model: update.model,
360
423
  toolCount: update.toolCount,
361
424
  durationMs: update.durationMs,
362
425
  tokens: update.tokens,
363
426
  taskProgress: mergedTaskProgress,
364
427
  });
365
- appendDelegatedLiveOutput(request.requestId, update.recentOutput);
366
- if (mergedTaskProgress) {
428
+
429
+ if (!isParallel) {
430
+ if (update.recentOutputLines && update.recentOutputLines.length > 0) {
431
+ updateDelegatedLiveState(request.requestId, {
432
+ recentOutput: sanitizeOutputLines(update.recentOutputLines),
433
+ });
434
+ } else {
435
+ appendDelegatedLiveOutput(request.requestId, update.recentOutput);
436
+ }
437
+ }
438
+
439
+ if (isParallel && mergedTaskProgress) {
367
440
  for (const task of mergedTaskProgress) {
441
+ const previousTask =
442
+ previousTaskProgress?.find((entry) => entry.index === task.index) ??
443
+ previousTaskProgress?.find((entry) => entry.agent === task.agent);
444
+
445
+ const newOutputLines = collectNewOutputLines(previousTask?.recentOutputLines, task.recentOutputLines);
446
+ if (newOutputLines.length > 0) {
447
+ for (const line of newOutputLines) {
448
+ appendDelegatedLiveOutput(request.requestId, line);
449
+ }
450
+ continue;
451
+ }
452
+
453
+ if (!task.recentOutput || task.recentOutput === previousTask?.recentOutput) {
454
+ continue;
455
+ }
368
456
  appendDelegatedLiveOutput(request.requestId, task.recentOutput);
369
457
  }
370
458
  }
@@ -567,11 +655,12 @@ export async function executeSubagentPromptStep(options: DelegatedPromptOptions)
567
655
  agent: preparedTasks[0]!.agent,
568
656
  };
569
657
  } catch (error) {
570
- const responseText = error instanceof Error ? error.message : String(error);
658
+ const cause = error instanceof Error ? error : new Error(String(error));
659
+ const responseText = cause.message;
571
660
  if (isParallelRequest) {
572
- throw new Error(`Parallel delegated prompts (${promptLabel}) failed: ${responseText}`);
661
+ throw new Error(`Parallel delegated prompts (${promptLabel}) failed: ${responseText}`, { cause });
573
662
  }
574
- throw new Error(`Prompt \`${preparedTasks[0]!.promptName}\` delegated subagent \`${preparedTasks[0]!.agent}\` failed: ${responseText}`);
663
+ throw new Error(`Prompt \`${preparedTasks[0]!.promptName}\` delegated subagent \`${preparedTasks[0]!.agent}\` failed: ${responseText}`, { cause });
575
664
  } finally {
576
665
  clearDelegatedLiveState(request.requestId);
577
666
  if (ctx.hasUI) {
@@ -18,6 +18,28 @@ function formatTokens(n: number | undefined): string {
18
18
  return String(n);
19
19
  }
20
20
 
21
+ function normalizeModelLabel(model: string | undefined): string | undefined {
22
+ if (!model) return undefined;
23
+ return model.includes("/") ? model.split("/").pop() : model;
24
+ }
25
+
26
+ function formatToolCall(tool: string, args: string): string {
27
+ const safeArgs = args ?? "";
28
+ switch (tool) {
29
+ case "bash": {
30
+ const cmd = safeArgs.replace(/[\n\t]/g, " ").trim();
31
+ return `$ ${cmd.length > 80 ? cmd.slice(0, 80) + "..." : cmd}`;
32
+ }
33
+ case "read": return `[read: ${safeArgs}]`;
34
+ case "write": return `[write: ${safeArgs}]`;
35
+ case "edit": return `[edit: ${safeArgs}]`;
36
+ default: {
37
+ const short = safeArgs.length > 60 ? safeArgs.slice(0, 60) + "..." : safeArgs;
38
+ return `[${tool}: ${short}]`;
39
+ }
40
+ }
41
+ }
42
+
21
43
  export function createDelegatedProgressWidget(
22
44
  requestId: string,
23
45
  agent: string,
@@ -25,11 +47,18 @@ export function createDelegatedProgressWidget(
25
47
  task: string,
26
48
  tasks: DelegatedSubagentTask[] | undefined,
27
49
  theme: Theme,
50
+ model?: string,
28
51
  ): Container & { dispose?(): void } {
29
52
  const contextSuffix = context === "fork" ? theme.fg("warning", " [fork]") : "";
30
- const taskPreview = task.length > 120 ? `${task.slice(0, 120)}...` : task;
53
+ const taskPreview = task.length > 200 ? `${task.slice(0, 200)}...` : task;
31
54
  const parallelTasks = tasks ?? [];
32
55
  const isParallel = parallelTasks.length > 0;
56
+ const parallelModels = [...new Set(parallelTasks
57
+ .map((task) => normalizeModelLabel(task.model))
58
+ .filter((entry): entry is string => !!entry))];
59
+ const requestModel = isParallel
60
+ ? (parallelModels.length === 1 ? parallelModels[0] : undefined)
61
+ : normalizeModelLabel(model);
33
62
 
34
63
  const container = new Container();
35
64
  container.addChild(new Spacer(1));
@@ -44,7 +73,7 @@ export function createDelegatedProgressWidget(
44
73
  const key = stateKey(state, elapsed);
45
74
  if (key !== lastKey) {
46
75
  lastKey = key;
47
- rebuildBox(box, agent, contextSuffix, taskPreview, parallelTasks, isParallel, state, elapsed, theme);
76
+ rebuildBox(box, agent, contextSuffix, taskPreview, parallelTasks, isParallel, state, elapsed, theme, requestModel);
48
77
  }
49
78
  return Container.prototype.render.call(container, width);
50
79
  };
@@ -55,11 +84,16 @@ export function createDelegatedProgressWidget(
55
84
  function stateKey(state: DelegatedSubagentLiveState | undefined, elapsed: number): string {
56
85
  if (!state) return "none";
57
86
  const elapsedBucket = Math.floor(elapsed / 1000);
58
- const tool = state.currentTool ?? state.lastTool ?? "";
87
+ const tool = state.currentTool ?? "";
88
+ const outputLen = state.recentOutput.length;
89
+ const outputTail = state.recentOutput.length > 0
90
+ ? state.recentOutput[state.recentOutput.length - 1]?.slice(0, 80) ?? ""
91
+ : "";
92
+ const toolsLen = state.recentTools.length;
59
93
  const taskProgressKey = state.taskProgress
60
94
  .map((entry) => `${entry.index ?? ""}:${entry.agent}:${entry.status ?? ""}:${entry.currentTool ?? ""}:${entry.toolCount ?? 0}`)
61
95
  .join("|");
62
- return `${state.status}|${tool}|${state.toolCount}|${state.tokens}|${state.recentOutput.length}|${taskProgressKey}|${elapsedBucket}`;
96
+ return `${state.status}|${tool}|${state.toolCount}|${state.tokens}|${outputLen}:${outputTail}|${toolsLen}|${state.model ?? ""}|${taskProgressKey}|${elapsedBucket}`;
63
97
  }
64
98
 
65
99
  function rebuildBox(
@@ -72,34 +106,49 @@ function rebuildBox(
72
106
  state: DelegatedSubagentLiveState | undefined,
73
107
  elapsed: number,
74
108
  theme: Theme,
109
+ requestModel?: string,
75
110
  ): void {
76
111
  box.clear();
77
112
 
78
- const toolCount = state?.toolCount ?? 0;
79
- const tokens = formatTokens(state?.tokens);
113
+ const taskProgress = state?.taskProgress ?? [];
114
+ const baseToolCount = state?.toolCount ?? 0;
115
+ const baseTokens = state?.tokens ?? 0;
116
+ const parallelToolCount = taskProgress.reduce((sum, entry) => sum + (entry.toolCount ?? 0), 0);
117
+ const parallelTokens = taskProgress.reduce((sum, entry) => sum + (entry.tokens ?? 0), 0);
118
+ const toolCount = isParallel && parallelToolCount > 0 ? parallelToolCount : baseToolCount;
119
+ const tokens = isParallel && parallelTokens > 0 ? parallelTokens : baseTokens;
120
+ const tokensLabel = formatTokens(tokens);
80
121
  const duration = formatDuration(elapsed);
81
- const isThinking = toolCount === 0 && (state?.tokens ?? 0) === 0;
122
+ const isThinking = toolCount === 0 && tokens === 0;
82
123
  const icon = theme.fg("warning", "...");
124
+ const modelLabel = isParallel
125
+ ? requestModel
126
+ : normalizeModelLabel(state?.model ?? requestModel);
127
+ const modelSuffix = modelLabel ? ` ${theme.fg("dim", modelLabel)}` : "";
83
128
  const stats = isThinking
84
129
  ? `thinking, ${duration}`
85
- : `${toolCount} tool${toolCount === 1 ? "" : "s"}, ${tokens} tok, ${duration}`;
86
- const taskProgress = state?.taskProgress ?? [];
130
+ : `${toolCount} tool${toolCount === 1 ? "" : "s"}, ${tokensLabel} tok, ${duration}`;
87
131
 
132
+ // Header
88
133
  if (isParallel) {
89
134
  const completedCount = taskProgress.filter((entry) => entry.status === "completed").length;
90
135
  const runningLabel = `parallel ${completedCount}/${parallelTasks.length} running`;
91
- box.addChild(new Text(`${icon} ${theme.fg("toolTitle", theme.bold(runningLabel))}${contextSuffix} | ${stats}`, 0, 0));
136
+ box.addChild(new Text(`${icon} ${theme.fg("toolTitle", theme.bold(runningLabel))}${contextSuffix}${modelSuffix} | ${stats}`, 0, 0));
92
137
  } else {
93
138
  box.addChild(new Text(
94
- `${icon} ${theme.fg("toolTitle", theme.bold(agent))}${contextSuffix} | ${stats}`,
139
+ `${icon} ${theme.fg("toolTitle", theme.bold(agent))}${contextSuffix}${modelSuffix} | ${stats}`,
95
140
  0, 0,
96
141
  ));
97
142
  }
98
143
  box.addChild(new Spacer(1));
144
+
145
+ // Task preview
99
146
  if (!isParallel) {
100
147
  box.addChild(new Text(theme.fg("dim", `Task: ${taskPreview}`), 0, 0));
148
+ box.addChild(new Spacer(1));
101
149
  }
102
150
 
151
+ // Parallel task list
103
152
  if (isParallel) {
104
153
  for (let index = 0; index < parallelTasks.length; index++) {
105
154
  const task = parallelTasks[index]!;
@@ -111,35 +160,38 @@ function rebuildBox(
111
160
  if (taskStatus === "running") {
112
161
  const runningTool = progress.currentTool ? ` ${progress.currentTool}...` : "";
113
162
  box.addChild(new Text(theme.fg("dim", ` ${task.agent}: running${runningTool}`), 0, 0));
114
- continue;
115
- }
116
- if (taskStatus === "completed") {
163
+ } else if (taskStatus === "completed") {
117
164
  const toolSuffix =
118
165
  progress?.toolCount !== undefined
119
166
  ? ` (${progress.toolCount} tool${progress.toolCount === 1 ? "" : "s"})`
120
167
  : "";
121
168
  box.addChild(new Text(theme.fg("dim", ` ${task.agent}: completed${toolSuffix}`), 0, 0));
122
- continue;
123
- }
124
- if (taskStatus === "failed") {
169
+ } else if (taskStatus === "failed") {
125
170
  box.addChild(new Text(theme.fg("dim", ` ${task.agent}: failed`), 0, 0));
126
- continue;
171
+ } else {
172
+ box.addChild(new Text(theme.fg("dim", ` ${task.agent}: pending`), 0, 0));
127
173
  }
128
- box.addChild(new Text(theme.fg("dim", ` ${task.agent}: pending`), 0, 0));
129
174
  }
130
175
  return;
131
176
  }
132
177
 
133
- const activeTool = state?.currentTool;
134
- const displayTool = activeTool ?? state?.lastTool;
135
- if (displayTool) {
136
- const toolArgs = activeTool ? state?.currentToolArgs : state?.lastToolArgs;
137
- const toolLine = `${displayTool}${toolArgs ? ` ${toolArgs}` : ""}`;
138
- box.addChild(new Text(theme.fg("dim", toolLine), 0, 0));
178
+ // Unified tool stream: completed tools (dim) then active tool (warning) at bottom.
179
+ // When a tool finishes it moves from active → completed in place — no visual jump.
180
+ const recentTools = state?.recentTools ?? [];
181
+ for (const tool of recentTools) {
182
+ box.addChild(new Text(theme.fg("dim", formatToolCall(tool.tool, tool.args)), 0, 0));
183
+ }
184
+ if (state?.currentTool) {
185
+ const active = formatToolCall(state.currentTool, state.currentToolArgs ?? "");
186
+ box.addChild(new Text(theme.fg("warning", `> ${active}`), 0, 0));
139
187
  }
140
188
 
189
+ // Recent output
141
190
  if (state && state.recentOutput.length > 0) {
142
- for (const line of state.recentOutput.slice(-4)) {
191
+ if (recentTools.length > 0 || state.currentTool) {
192
+ box.addChild(new Spacer(1));
193
+ }
194
+ for (const line of state.recentOutput) {
143
195
  box.addChild(new Text(theme.fg("dim", ` ${line}`), 0, 0));
144
196
  }
145
197
  }