@leing2021/super-pi 0.21.0 → 0.22.0

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 (76) hide show
  1. package/README.md +29 -16
  2. package/extensions/subagent/agent-management.ts +595 -0
  3. package/extensions/subagent/agent-manager-chain-detail.ts +162 -0
  4. package/extensions/subagent/agent-manager-detail.ts +231 -0
  5. package/extensions/subagent/agent-manager-edit.ts +390 -0
  6. package/extensions/subagent/agent-manager-list.ts +278 -0
  7. package/extensions/subagent/agent-manager-parallel.ts +304 -0
  8. package/extensions/subagent/agent-manager.ts +705 -0
  9. package/extensions/subagent/agent-scope.ts +8 -0
  10. package/extensions/subagent/agent-selection.ts +25 -0
  11. package/extensions/subagent/agent-serializer.ts +123 -0
  12. package/extensions/subagent/agent-templates.ts +62 -0
  13. package/extensions/subagent/agents/context-builder.md +37 -0
  14. package/extensions/subagent/agents/delegate.md +9 -0
  15. package/extensions/subagent/agents/oracle.md +73 -0
  16. package/extensions/subagent/agents/planner.md +52 -0
  17. package/extensions/subagent/agents/researcher.md +50 -0
  18. package/extensions/subagent/agents/reviewer.md +38 -0
  19. package/extensions/subagent/agents/scout.md +48 -0
  20. package/extensions/subagent/agents/worker.md +52 -0
  21. package/extensions/subagent/agents.ts +761 -0
  22. package/extensions/subagent/artifacts.ts +100 -0
  23. package/extensions/subagent/async-execution.ts +520 -0
  24. package/extensions/subagent/async-job-tracker.ts +216 -0
  25. package/extensions/subagent/async-status.ts +241 -0
  26. package/extensions/subagent/chain-clarify.ts +1364 -0
  27. package/extensions/subagent/chain-execution.ts +853 -0
  28. package/extensions/subagent/chain-serializer.ts +126 -0
  29. package/extensions/subagent/completion-dedupe.ts +65 -0
  30. package/extensions/subagent/doctor.ts +200 -0
  31. package/extensions/subagent/execution.ts +738 -0
  32. package/extensions/subagent/file-coalescer.ts +42 -0
  33. package/extensions/subagent/fork-context.ts +63 -0
  34. package/extensions/subagent/formatters.ts +122 -0
  35. package/extensions/subagent/frontmatter.ts +31 -0
  36. package/extensions/subagent/index.ts +585 -0
  37. package/extensions/subagent/intercom-bridge.ts +240 -0
  38. package/extensions/subagent/jsonl-writer.ts +83 -0
  39. package/extensions/subagent/model-fallback.ts +108 -0
  40. package/extensions/subagent/notify.ts +110 -0
  41. package/extensions/subagent/parallel-utils.ts +108 -0
  42. package/extensions/subagent/pi-args.ts +138 -0
  43. package/extensions/subagent/pi-spawn.ts +100 -0
  44. package/extensions/subagent/post-exit-stdio-guard.ts +87 -0
  45. package/extensions/subagent/prompt-template-bridge.ts +399 -0
  46. package/extensions/subagent/prompts/gather-context-and-clarify.md +13 -0
  47. package/extensions/subagent/prompts/parallel-cleanup.md +42 -0
  48. package/extensions/subagent/prompts/parallel-research.md +50 -0
  49. package/extensions/subagent/prompts/parallel-review.md +40 -0
  50. package/extensions/subagent/render-helpers.ts +82 -0
  51. package/extensions/subagent/render.ts +836 -0
  52. package/extensions/subagent/result-intercom.ts +237 -0
  53. package/extensions/subagent/result-watcher.ts +171 -0
  54. package/extensions/subagent/run-history.ts +57 -0
  55. package/extensions/subagent/run-status.ts +136 -0
  56. package/extensions/subagent/schemas.ts +164 -0
  57. package/extensions/subagent/session-tokens.ts +50 -0
  58. package/extensions/subagent/settings.ts +367 -0
  59. package/extensions/subagent/single-output.ts +97 -0
  60. package/extensions/subagent/skills.ts +626 -0
  61. package/extensions/subagent/slash-bridge.ts +176 -0
  62. package/extensions/subagent/slash-commands.ts +303 -0
  63. package/extensions/subagent/slash-live-state.ts +294 -0
  64. package/extensions/subagent/subagent-control.ts +150 -0
  65. package/extensions/subagent/subagent-executor.ts +1899 -0
  66. package/extensions/subagent/subagent-prompt-runtime.ts +75 -0
  67. package/extensions/subagent/subagent-runner.ts +1470 -0
  68. package/extensions/subagent/subagents-status.ts +472 -0
  69. package/extensions/subagent/text-editor.ts +272 -0
  70. package/extensions/subagent/top-level-async.ts +15 -0
  71. package/extensions/subagent/types.ts +623 -0
  72. package/extensions/subagent/utils.ts +456 -0
  73. package/extensions/subagent/worktree.ts +579 -0
  74. package/extensions/super-pi-extension/index.ts +2 -55
  75. package/package.json +12 -6
  76. package/skills/pi-subagents/SKILL.md +566 -0
@@ -0,0 +1,836 @@
1
+ // Based on pi-subagents by Nico Bailon (https://github.com/nicobailon/pi-subagents)
2
+ // MIT License
3
+ /**
4
+ * Rendering functions for subagent results
5
+ */
6
+
7
+ import type { AgentToolResult } from "@mariozechner/pi-agent-core";
8
+ import { getMarkdownTheme, type ExtensionContext } from "@mariozechner/pi-coding-agent";
9
+ import { Container, Markdown, Spacer, Text, visibleWidth, type Component } from "@mariozechner/pi-tui";
10
+ import {
11
+ type AgentProgress,
12
+ type AsyncJobState,
13
+ type Details,
14
+ MAX_WIDGET_JOBS,
15
+ WIDGET_KEY,
16
+ } from "./types.ts";
17
+ import { formatTokens, formatUsage, formatDuration, formatToolCall, shortenPath } from "./formatters.ts";
18
+ import { getDisplayItems, getLastActivity, getSingleResultOutput } from "./utils.ts";
19
+
20
+ type Theme = ExtensionContext["ui"]["theme"];
21
+
22
+ function getTermWidth(): number {
23
+ return process.stdout.columns || 120;
24
+ }
25
+
26
+ const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
27
+
28
+ /**
29
+ * Truncate a line to maxWidth, preserving ANSI styling through the ellipsis.
30
+ *
31
+ * pi-tui's truncateToWidth adds \x1b[0m before ellipsis which resets all styling,
32
+ * causing background color bleed in the TUI. This implementation tracks active
33
+ * ANSI styles and re-applies them before the ellipsis.
34
+ *
35
+ * Uses Intl.Segmenter for proper Unicode/emoji handling (not char-by-char).
36
+ */
37
+ function truncLine(text: string, maxWidth: number): string {
38
+ if (visibleWidth(text) <= maxWidth) return text;
39
+
40
+ const targetWidth = maxWidth - 1;
41
+ let result = "";
42
+ let currentWidth = 0;
43
+ let activeStyles: string[] = [];
44
+ let i = 0;
45
+
46
+ while (i < text.length) {
47
+ const ansiMatch = text.slice(i).match(/^\x1b\[[0-9;]*m/);
48
+ if (ansiMatch) {
49
+ const code = ansiMatch[0];
50
+ result += code;
51
+
52
+ if (code === "\x1b[0m" || code === "\x1b[m") {
53
+ activeStyles = [];
54
+ } else {
55
+ activeStyles.push(code);
56
+ }
57
+ i += code.length;
58
+ continue;
59
+ }
60
+
61
+ let end = i;
62
+ while (end < text.length && !text.slice(end).match(/^\x1b\[[0-9;]*m/)) {
63
+ end++;
64
+ }
65
+
66
+ const textPortion = text.slice(i, end);
67
+ for (const seg of segmenter.segment(textPortion)) {
68
+ const grapheme = seg.segment;
69
+ const graphemeWidth = visibleWidth(grapheme);
70
+
71
+ if (currentWidth + graphemeWidth > targetWidth) {
72
+ return result + activeStyles.join("") + "…";
73
+ }
74
+
75
+ result += grapheme;
76
+ currentWidth += graphemeWidth;
77
+ }
78
+ i = end;
79
+ }
80
+
81
+ return result + activeStyles.join("") + "…";
82
+ }
83
+
84
+ const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
85
+ const WIDGET_ANIMATION_MS = 80;
86
+
87
+ let widgetTimer: ReturnType<typeof setInterval> | undefined;
88
+ let latestWidgetCtx: ExtensionContext | undefined;
89
+ let latestWidgetJobs: AsyncJobState[] = [];
90
+
91
+ const resultAnimationTimers = new Map<ReturnType<typeof setInterval>, ResultAnimationContext["state"]>();
92
+ const outputActivityCache = new Map<string, { checkedAt: number; text: string }>();
93
+
94
+ export interface ResultAnimationContext {
95
+ state: { subagentResultAnimationTimer?: ReturnType<typeof setInterval> };
96
+ invalidate: () => void;
97
+ }
98
+
99
+ function spinnerFrame(): string {
100
+ return SPINNER[Math.floor(Date.now() / WIDGET_ANIMATION_MS) % SPINNER.length]!;
101
+ }
102
+
103
+ function resultIsRunning(result: AgentToolResult<Details>): boolean {
104
+ return result.details?.progress?.some((entry) => entry.status === "running")
105
+ || result.details?.results.some((entry) => entry.progress?.status === "running")
106
+ || false;
107
+ }
108
+
109
+ function stopResultAnimation(context: ResultAnimationContext): void {
110
+ const timer = context.state.subagentResultAnimationTimer;
111
+ if (!timer) return;
112
+ clearInterval(timer);
113
+ resultAnimationTimers.delete(timer);
114
+ context.state.subagentResultAnimationTimer = undefined;
115
+ }
116
+
117
+ export function syncResultAnimation(result: AgentToolResult<Details>, context: ResultAnimationContext): void {
118
+ if (!resultIsRunning(result)) {
119
+ stopResultAnimation(context);
120
+ return;
121
+ }
122
+ if (context.state.subagentResultAnimationTimer) return;
123
+ const timer = setInterval(() => context.invalidate(), WIDGET_ANIMATION_MS);
124
+ timer.unref?.();
125
+ context.state.subagentResultAnimationTimer = timer;
126
+ resultAnimationTimers.set(timer, context.state);
127
+ }
128
+
129
+ function extractOutputTarget(task: string): string | undefined {
130
+ const writeToMatch = task.match(/\[Write to:\s*([^\]\n]+)\]/i);
131
+ if (writeToMatch?.[1]?.trim()) return writeToMatch[1].trim();
132
+ const findingsMatch = task.match(/Write your findings to:\s*(\S+)/i);
133
+ if (findingsMatch?.[1]?.trim()) return findingsMatch[1].trim();
134
+ const outputMatch = task.match(/[Oo]utput(?:\s+to)?\s*:\s*(\S+)/i);
135
+ if (outputMatch?.[1]?.trim()) return outputMatch[1].trim();
136
+ return undefined;
137
+ }
138
+
139
+ function hasEmptyTextOutputWithoutOutputTarget(task: string, output: string): boolean {
140
+ if (output.trim()) return false;
141
+ return !extractOutputTarget(task);
142
+ }
143
+
144
+ function getToolCallLines(
145
+ result: Pick<Details["results"][number], "messages" | "toolCalls">,
146
+ expanded: boolean,
147
+ ): string[] {
148
+ if (result.messages) {
149
+ return getDisplayItems(result.messages)
150
+ .filter((item): item is { type: "tool"; name: string; args: Record<string, unknown> } => item.type === "tool")
151
+ .map((item) => formatToolCall(item.name, item.args, expanded));
152
+ }
153
+ return result.toolCalls?.map((toolCall) => expanded ? toolCall.expandedText : toolCall.text) ?? [];
154
+ }
155
+
156
+ function formatActivityAge(ms: number): string {
157
+ if (ms < 1000) return "now";
158
+ if (ms < 60000) return `${Math.floor(ms / 1000)}s`;
159
+ return `${Math.floor(ms / 60000)}m`;
160
+ }
161
+
162
+ function formatActivityLabel(lastActivityAt: number | undefined, needsAttention?: boolean, now = Date.now()): string | undefined {
163
+ if (lastActivityAt === undefined) return needsAttention ? "needs attention" : undefined;
164
+ const age = formatActivityAge(Math.max(0, now - lastActivityAt));
165
+ return needsAttention ? `no activity for ${age}` : age === "now" ? "active now" : `active ${age} ago`;
166
+ }
167
+
168
+ function formatCurrentToolLine(progress: Pick<AgentProgress, "currentTool" | "currentToolArgs" | "currentToolStartedAt">, availableWidth: number, expanded: boolean): string | undefined {
169
+ if (!progress.currentTool) return undefined;
170
+ const maxToolArgsLen = Math.max(50, availableWidth - 20);
171
+ const toolArgsPreview = progress.currentToolArgs
172
+ ? (expanded || progress.currentToolArgs.length <= maxToolArgsLen
173
+ ? progress.currentToolArgs
174
+ : `${progress.currentToolArgs.slice(0, maxToolArgsLen)}...`)
175
+ : "";
176
+ const durationSuffix = progress.currentToolStartedAt !== undefined
177
+ ? ` | ${formatDuration(Math.max(0, Date.now() - progress.currentToolStartedAt))}`
178
+ : "";
179
+ return toolArgsPreview
180
+ ? `${progress.currentTool}: ${toolArgsPreview}${durationSuffix}`
181
+ : `${progress.currentTool}${durationSuffix}`;
182
+ }
183
+
184
+ function buildLiveStatusLine(progress: Pick<AgentProgress, "activityState" | "lastActivityAt">): string | undefined {
185
+ return formatActivityLabel(progress.lastActivityAt, progress.activityState === "needs_attention");
186
+ }
187
+
188
+ function themeBold(theme: Theme, text: string): string {
189
+ return ((theme as { bold?: (value: string) => string }).bold?.(text)) ?? text;
190
+ }
191
+
192
+ function statJoin(theme: Theme, parts: string[]): string {
193
+ return parts.filter(Boolean).map((part) => theme.fg("dim", part)).join(` ${theme.fg("dim", "·")} `);
194
+ }
195
+
196
+ function formatTokenStat(tokens: number): string {
197
+ return `${formatTokens(tokens)} token`;
198
+ }
199
+
200
+ function formatToolUseStat(count: number): string {
201
+ return `${count} tool use${count === 1 ? "" : "s"}`;
202
+ }
203
+
204
+ function formatProgressStats(theme: Theme, progress: Pick<AgentProgress, "toolCount" | "tokens" | "durationMs"> | undefined, includeDuration = true): string {
205
+ if (!progress) return "";
206
+ const parts: string[] = [];
207
+ if (progress.toolCount > 0) parts.push(formatToolUseStat(progress.toolCount));
208
+ if (progress.tokens > 0) parts.push(formatTokenStat(progress.tokens));
209
+ if (includeDuration && progress.durationMs > 0) parts.push(formatDuration(progress.durationMs));
210
+ return statJoin(theme, parts);
211
+ }
212
+
213
+ function firstOutputLine(text: string): string {
214
+ return text.split("\n").find((line) => line.trim())?.trim() ?? "";
215
+ }
216
+
217
+ function resultStatusLine(result: Details["results"][number], output: string): string {
218
+ if (result.detached) return result.detachedReason ? `Detached: ${result.detachedReason}` : "Detached";
219
+ if (result.interrupted) return "Paused";
220
+ if (result.exitCode !== 0) return `Error: ${result.error ?? (firstOutputLine(output) || `exit ${result.exitCode}`)}`;
221
+ if (hasEmptyTextOutputWithoutOutputTarget(result.task, output)) return "Done (no text output)";
222
+ return "Done";
223
+ }
224
+
225
+ function resultGlyph(result: Details["results"][number], output: string, theme: Theme, running = result.progress?.status === "running"): string {
226
+ if (running) return theme.fg("accent", spinnerFrame());
227
+ if (result.detached) return theme.fg("warning", "■");
228
+ if (result.interrupted) return theme.fg("warning", "■");
229
+ if (result.exitCode !== 0) return theme.fg("error", "✗");
230
+ if (hasEmptyTextOutputWithoutOutputTarget(result.task, output)) return theme.fg("warning", "✓");
231
+ return theme.fg("success", "✓");
232
+ }
233
+
234
+ function compactCurrentActivity(progress: AgentProgress): string {
235
+ return formatCurrentToolLine(progress, getTermWidth() - 4, false) ?? buildLiveStatusLine(progress) ?? "thinking…";
236
+ }
237
+
238
+ function hasAnimatedWidgetJobs(jobs: AsyncJobState[]): boolean {
239
+ return jobs.some((job) => job.status === "running");
240
+ }
241
+
242
+ function widgetJobName(job: AsyncJobState): string {
243
+ if (job.agents?.length) return job.agents.join(" → ");
244
+ return job.mode ?? "subagent";
245
+ }
246
+
247
+ function getCachedLastActivity(outputFile: string | undefined): string {
248
+ if (!outputFile) return "";
249
+ const now = Date.now();
250
+ const cached = outputActivityCache.get(outputFile);
251
+ if (cached && now - cached.checkedAt < 1000) return cached.text;
252
+ const text = getLastActivity(outputFile);
253
+ outputActivityCache.set(outputFile, { checkedAt: now, text });
254
+ return text;
255
+ }
256
+
257
+ function widgetActivity(job: AsyncJobState): string {
258
+ if (job.currentTool && job.currentToolStartedAt !== undefined) {
259
+ return `${job.currentTool} ${formatDuration(Math.max(0, Date.now() - job.currentToolStartedAt))}`;
260
+ }
261
+ const activity = formatActivityLabel(job.lastActivityAt, job.activityState === "needs_attention")
262
+ ?? (job.status === "running" ? getCachedLastActivity(job.outputFile) : "");
263
+ if (activity) return activity;
264
+ if (job.status === "queued") return "queued…";
265
+ if (job.status === "paused") return "Paused";
266
+ if (job.status === "failed") return "Failed";
267
+ return "Done";
268
+ }
269
+
270
+ function widgetStatusGlyph(job: AsyncJobState, theme: Theme): string {
271
+ if (job.status === "running") return theme.fg("accent", spinnerFrame());
272
+ if (job.status === "queued") return theme.fg("muted", "◦");
273
+ if (job.status === "complete") return theme.fg("success", "✓");
274
+ if (job.status === "paused") return theme.fg("warning", "■");
275
+ return theme.fg("error", "✗");
276
+ }
277
+
278
+ function widgetStats(job: AsyncJobState, theme: Theme): string {
279
+ const parts: string[] = [];
280
+ const stepsTotal = job.stepsTotal ?? (job.agents?.length ?? 1);
281
+ if (job.currentStep !== undefined) parts.push(`step ${job.currentStep + 1}/${stepsTotal}`);
282
+ else if (stepsTotal > 1) parts.push(`steps ${stepsTotal}`);
283
+ if (job.totalTokens?.total) parts.push(formatTokenStat(job.totalTokens.total));
284
+ const endTime = job.status === "complete" || job.status === "failed" || job.status === "paused" ? (job.updatedAt ?? Date.now()) : Date.now();
285
+ if (job.startedAt) parts.push(formatDuration(Math.max(0, endTime - job.startedAt)));
286
+ return statJoin(theme, parts);
287
+ }
288
+
289
+ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = getTermWidth()): string[] {
290
+ if (jobs.length === 0) return [];
291
+ const running = jobs.filter((job) => job.status === "running");
292
+ const queued = jobs.filter((job) => job.status === "queued");
293
+ const finished = jobs.filter((job) => job.status !== "running" && job.status !== "queued");
294
+
295
+ const lines: string[] = [];
296
+ const hasActive = running.length > 0 || queued.length > 0;
297
+ lines.push(truncLine(`${theme.fg(hasActive ? "accent" : "dim", hasActive ? "●" : "○")} ${theme.fg(hasActive ? "accent" : "dim", "Agents")} ${theme.fg("dim", "· /subagents-status")}`, width));
298
+
299
+ const items: string[][] = [];
300
+ let hiddenRunning = 0;
301
+ let hiddenFinished = 0;
302
+ let queuedSummaryShown = false;
303
+ let slots = MAX_WIDGET_JOBS;
304
+
305
+ for (const job of running) {
306
+ if (slots <= 0) { hiddenRunning++; continue; }
307
+ const stats = widgetStats(job, theme);
308
+ items.push([
309
+ `${widgetStatusGlyph(job, theme)} ${themeBold(theme, widgetJobName(job))}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
310
+ ` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
311
+ ]);
312
+ slots--;
313
+ }
314
+
315
+ if (queued.length > 0 && slots > 0) {
316
+ items.push([`${theme.fg("muted", "◦")} ${theme.fg("dim", `${queued.length} queued`)}`]);
317
+ queuedSummaryShown = true;
318
+ slots--;
319
+ }
320
+
321
+ for (const job of finished) {
322
+ if (slots <= 0) { hiddenFinished++; continue; }
323
+ const stats = widgetStats(job, theme);
324
+ items.push([
325
+ `${widgetStatusGlyph(job, theme)} ${themeBold(theme, widgetJobName(job))}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
326
+ ` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
327
+ ]);
328
+ slots--;
329
+ }
330
+
331
+ const hiddenQueued = queued.length > 0 && !queuedSummaryShown ? queued.length : 0;
332
+ const hiddenTotal = hiddenRunning + hiddenFinished + hiddenQueued;
333
+ if (hiddenTotal > 0) {
334
+ const parts: string[] = [];
335
+ if (hiddenRunning > 0) parts.push(`${hiddenRunning} running`);
336
+ if (hiddenQueued > 0) parts.push(`${hiddenQueued} queued`);
337
+ if (hiddenFinished > 0) parts.push(`${hiddenFinished} finished`);
338
+ items.push([theme.fg("dim", `+${hiddenTotal} more (${parts.join(", ")})`)]);
339
+ }
340
+
341
+ for (let i = 0; i < items.length; i++) {
342
+ const item = items[i]!;
343
+ const last = i === items.length - 1;
344
+ const branch = last ? "└─" : "├─";
345
+ const continuation = last ? " " : "│ ";
346
+ lines.push(truncLine(`${theme.fg("dim", branch)} ${item[0]}`, width));
347
+ for (const detail of item.slice(1)) {
348
+ lines.push(truncLine(`${theme.fg("dim", continuation)} ${detail}`, width));
349
+ }
350
+ }
351
+
352
+ return lines;
353
+ }
354
+
355
+ function refreshAnimatedWidget(): void {
356
+ if (!latestWidgetCtx?.hasUI || latestWidgetJobs.length === 0) return;
357
+ latestWidgetCtx.ui.setWidget(WIDGET_KEY, buildWidgetLines(latestWidgetJobs, latestWidgetCtx.ui.theme));
358
+ latestWidgetCtx.ui.requestRender?.();
359
+ }
360
+
361
+ function ensureWidgetAnimation(): void {
362
+ if (widgetTimer) return;
363
+ widgetTimer = setInterval(() => {
364
+ if (!hasAnimatedWidgetJobs(latestWidgetJobs)) {
365
+ stopWidgetAnimation();
366
+ return;
367
+ }
368
+ refreshAnimatedWidget();
369
+ }, WIDGET_ANIMATION_MS);
370
+ widgetTimer.unref?.();
371
+ }
372
+
373
+ export function stopWidgetAnimation(): void {
374
+ if (widgetTimer) {
375
+ clearInterval(widgetTimer);
376
+ widgetTimer = undefined;
377
+ }
378
+ latestWidgetCtx = undefined;
379
+ latestWidgetJobs = [];
380
+ outputActivityCache.clear();
381
+ }
382
+
383
+ export function stopResultAnimations(): void {
384
+ for (const [timer, state] of resultAnimationTimers) {
385
+ clearInterval(timer);
386
+ state.subagentResultAnimationTimer = undefined;
387
+ }
388
+ resultAnimationTimers.clear();
389
+ }
390
+
391
+ /**
392
+ * Render the async jobs widget
393
+ */
394
+ export function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void {
395
+ if (jobs.length === 0) {
396
+ stopWidgetAnimation();
397
+ if (ctx.hasUI) ctx.ui.setWidget(WIDGET_KEY, undefined);
398
+ return;
399
+ }
400
+ if (!ctx.hasUI) {
401
+ stopWidgetAnimation();
402
+ return;
403
+ }
404
+ latestWidgetCtx = ctx;
405
+ latestWidgetJobs = [...jobs];
406
+
407
+ ctx.ui.setWidget(WIDGET_KEY, buildWidgetLines(jobs, ctx.ui.theme));
408
+ if (hasAnimatedWidgetJobs(jobs)) ensureWidgetAnimation();
409
+ else stopWidgetAnimation();
410
+ }
411
+
412
+ function renderSingleCompact(d: Details, r: Details["results"][number], theme: Theme): Component {
413
+ const output = r.truncation?.text || getSingleResultOutput(r);
414
+ const progress = r.progress || r.progressSummary;
415
+ const isRunning = r.progress?.status === "running";
416
+ const contextBadge = d.context === "fork" ? theme.fg("warning", " [fork]") : "";
417
+ const stats = statJoin(theme, [
418
+ r.usage?.turns ? `⟳${r.usage.turns}` : "",
419
+ formatProgressStats(theme, progress),
420
+ ]);
421
+ const c = new Container();
422
+ const width = getTermWidth() - 4;
423
+ c.addChild(new Text(truncLine(`${resultGlyph(r, output, theme, isRunning)} ${theme.fg("toolTitle", theme.bold(r.agent))}${contextBadge}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`, width), 0, 0));
424
+
425
+ if (isRunning && r.progress) {
426
+ const activity = compactCurrentActivity(r.progress);
427
+ c.addChild(new Text(truncLine(theme.fg("dim", ` ⎿ ${activity}`), width), 0, 0));
428
+ const liveStatus = buildLiveStatusLine(r.progress);
429
+ if (liveStatus && liveStatus !== activity) c.addChild(new Text(truncLine(theme.fg("dim", ` ${liveStatus}`), width), 0, 0));
430
+ c.addChild(new Text(truncLine(theme.fg("accent", " Press Ctrl+O for live detail"), width), 0, 0));
431
+ if (r.artifactPaths) c.addChild(new Text(truncLine(theme.fg("dim", ` output: ${shortenPath(r.artifactPaths.outputPath)}`), width), 0, 0));
432
+ return c;
433
+ }
434
+
435
+ c.addChild(new Text(truncLine(theme.fg("dim", ` ⎿ ${resultStatusLine(r, output)}`), width), 0, 0));
436
+ const preview = firstOutputLine(output);
437
+ if (preview && r.exitCode === 0 && !hasEmptyTextOutputWithoutOutputTarget(r.task, output)) {
438
+ c.addChild(new Text(truncLine(theme.fg("dim", ` ${preview}`), width), 0, 0));
439
+ }
440
+ if (r.sessionFile) c.addChild(new Text(truncLine(theme.fg("dim", ` session: ${shortenPath(r.sessionFile)}`), width), 0, 0));
441
+ if (r.artifactPaths) c.addChild(new Text(truncLine(theme.fg("dim", ` output: ${shortenPath(r.artifactPaths.outputPath)}`), width), 0, 0));
442
+ if (r.truncation?.artifactPath) c.addChild(new Text(truncLine(theme.fg("dim", ` full output: ${shortenPath(r.truncation.artifactPath)}`), width), 0, 0));
443
+ return c;
444
+ }
445
+
446
+ function renderMultiCompact(d: Details, theme: Theme): Component {
447
+ const hasRunning = d.progress?.some((p) => p.status === "running")
448
+ || d.results.some((r) => r.progress?.status === "running");
449
+ const ok = d.results.filter((r) =>
450
+ !r.interrupted
451
+ && !r.detached
452
+ && (r.progress?.status === "completed" || (r.exitCode === 0 && r.progress?.status !== "running" && r.progress?.status !== "pending"))
453
+ ).length;
454
+ const failed = d.results.some((r) => r.exitCode !== 0 && r.progress?.status !== "running");
455
+ const paused = d.results.some((r) => (r.interrupted || r.detached) && r.progress?.status !== "running");
456
+ let totalSummary = d.progressSummary;
457
+ if (!totalSummary) {
458
+ let sawProgress = false;
459
+ const summary = { toolCount: 0, tokens: 0, durationMs: 0 };
460
+ for (const r of d.results) {
461
+ const prog = r.progress || r.progressSummary;
462
+ if (!prog) continue;
463
+ sawProgress = true;
464
+ summary.toolCount += prog.toolCount;
465
+ summary.tokens += prog.tokens;
466
+ summary.durationMs = d.mode === "chain" ? summary.durationMs + prog.durationMs : Math.max(summary.durationMs, prog.durationMs);
467
+ }
468
+ if (sawProgress) totalSummary = summary;
469
+ }
470
+ const hasParallelInChain = d.chainAgents?.some((a) => a.startsWith("["));
471
+ const totalCount = hasParallelInChain ? d.results.length : (d.totalSteps ?? d.results.length);
472
+ const currentStep = d.currentStepIndex !== undefined ? d.currentStepIndex + 1 : Math.min(totalCount, ok + (hasRunning ? 1 : 0));
473
+ const itemLabel = d.mode === "parallel" ? "agent" : "step";
474
+ const itemTitle = d.mode === "parallel" ? "Agent" : "Step";
475
+ const stepInfo = hasRunning ? `${itemLabel} ${currentStep}/${totalCount}` : `${itemLabel} ${ok}/${totalCount}`;
476
+ const stats = statJoin(theme, [stepInfo, formatProgressStats(theme, totalSummary)]);
477
+ const glyph = hasRunning
478
+ ? theme.fg("accent", spinnerFrame())
479
+ : failed
480
+ ? theme.fg("error", "✗")
481
+ : paused
482
+ ? theme.fg("warning", "■")
483
+ : theme.fg("success", "✓");
484
+ const contextBadge = d.context === "fork" ? theme.fg("warning", " [fork]") : "";
485
+ const c = new Container();
486
+ const width = getTermWidth() - 4;
487
+ c.addChild(new Text(truncLine(`${glyph} ${theme.fg("toolTitle", theme.bold(d.mode))}${contextBadge}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`, width), 0, 0));
488
+
489
+ const useResultsDirectly = hasParallelInChain || !d.chainAgents?.length;
490
+ const stepsToShow = useResultsDirectly ? d.results.length : d.chainAgents!.length;
491
+ for (let i = 0; i < stepsToShow; i++) {
492
+ const r = d.results[i];
493
+ const agentName = useResultsDirectly ? (r?.agent || `${itemLabel}-${i + 1}`) : (d.chainAgents![i] || r?.agent || `${itemLabel}-${i + 1}`);
494
+ if (!r) {
495
+ c.addChild(new Text(truncLine(theme.fg("dim", ` ◦ ${itemTitle} ${i + 1}: ${agentName} · pending`), width), 0, 0));
496
+ continue;
497
+ }
498
+ const output = getSingleResultOutput(r);
499
+ const progressFromArray = d.progress?.find((p) => p.index === i) || d.progress?.find((p) => p.agent === r.agent && p.status === "running");
500
+ const rProg = r.progress || progressFromArray || r.progressSummary;
501
+ const rRunning = rProg && "status" in rProg && rProg.status === "running";
502
+ const rPending = rProg && "status" in rProg && rProg.status === "pending";
503
+ const stepNumber = r.progress?.index !== undefined ? r.progress.index + 1 : progressFromArray?.index !== undefined ? progressFromArray.index + 1 : i + 1;
504
+ const stepStats = formatProgressStats(theme, rProg);
505
+ const glyph = rPending ? theme.fg("dim", "◦") : resultGlyph(r, output, theme, rRunning);
506
+ const pendingLabel = rPending ? ` ${theme.fg("dim", "· pending")}` : "";
507
+ const line = `${glyph} ${itemTitle} ${stepNumber}: ${themeBold(theme, agentName)}${stepStats ? ` ${theme.fg("dim", "·")} ${stepStats}` : ""}${pendingLabel}`;
508
+ c.addChild(new Text(truncLine(` ${line}`, width), 0, 0));
509
+ if (rRunning && rProg && "status" in rProg) {
510
+ const activity = compactCurrentActivity(rProg);
511
+ c.addChild(new Text(truncLine(theme.fg("dim", ` ⎿ ${activity}`), width), 0, 0));
512
+ c.addChild(new Text(truncLine(theme.fg("accent", " Press Ctrl+O for live detail"), width), 0, 0));
513
+ } else if (!rPending && (r.exitCode !== 0 || r.interrupted || r.detached || hasEmptyTextOutputWithoutOutputTarget(r.task, output))) {
514
+ c.addChild(new Text(truncLine(theme.fg(r.exitCode !== 0 ? "error" : "dim", ` ⎿ ${resultStatusLine(r, output)}`), width), 0, 0));
515
+ }
516
+ const outputTarget = extractOutputTarget(r.task);
517
+ if (outputTarget) c.addChild(new Text(truncLine(theme.fg("dim", ` output: ${outputTarget}`), width), 0, 0));
518
+ if (r.artifactPaths) c.addChild(new Text(truncLine(theme.fg("dim", ` output: ${shortenPath(r.artifactPaths.outputPath)}`), width), 0, 0));
519
+ }
520
+ if (d.artifacts) c.addChild(new Text(truncLine(theme.fg("dim", ` artifacts: ${shortenPath(d.artifacts.dir)}`), width), 0, 0));
521
+ return c;
522
+ }
523
+
524
+ /**
525
+ * Render a subagent result
526
+ */
527
+ export function renderSubagentResult(
528
+ result: AgentToolResult<Details>,
529
+ options: { expanded: boolean },
530
+ theme: Theme,
531
+ ): Component {
532
+ const d = result.details;
533
+ if (!d || !d.results.length) {
534
+ const t = result.content[0];
535
+ const text = t?.type === "text" ? t.text : "(no output)";
536
+ const contextPrefix = d?.context === "fork" ? `${theme.fg("warning", "[fork]")} ` : "";
537
+ return new Text(truncLine(`${contextPrefix}${text}`, getTermWidth() - 4), 0, 0);
538
+ }
539
+
540
+ const expanded = options.expanded;
541
+ const mdTheme = getMarkdownTheme();
542
+
543
+ if (d.mode === "single" && d.results.length === 1) {
544
+ const r = d.results[0];
545
+ if (!expanded) return renderSingleCompact(d, r, theme);
546
+ const isRunning = r.progress?.status === "running";
547
+ const icon = isRunning
548
+ ? theme.fg("warning", "running")
549
+ : r.detached
550
+ ? theme.fg("warning", "detached")
551
+ : r.exitCode === 0
552
+ ? theme.fg("success", "ok")
553
+ : theme.fg("error", "failed");
554
+ const contextBadge = d.context === "fork" ? theme.fg("warning", " [fork]") : "";
555
+ const output = r.truncation?.text || getSingleResultOutput(r);
556
+
557
+ const progressInfo = isRunning && r.progress
558
+ ? ` | ${r.progress.toolCount} tools, ${formatTokens(r.progress.tokens)} tok, ${formatDuration(r.progress.durationMs)}`
559
+ : r.progressSummary
560
+ ? ` | ${r.progressSummary.toolCount} tools, ${formatTokens(r.progressSummary.tokens)} tok, ${formatDuration(r.progressSummary.durationMs)}`
561
+ : "";
562
+
563
+ const w = getTermWidth() - 4;
564
+ const fit = (text: string) => expanded ? text : truncLine(text, w);
565
+ const toolCallLines = getToolCallLines(r, expanded);
566
+ const c = new Container();
567
+ c.addChild(new Text(fit(`${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${contextBadge}${progressInfo}`), 0, 0));
568
+ c.addChild(new Spacer(1));
569
+ const taskMaxLen = Math.max(20, w - 8);
570
+ const taskPreview = expanded || r.task.length <= taskMaxLen
571
+ ? r.task
572
+ : `${r.task.slice(0, taskMaxLen)}...`;
573
+ c.addChild(
574
+ new Text(fit(theme.fg("dim", `Task: ${taskPreview}`)), 0, 0),
575
+ );
576
+ c.addChild(new Spacer(1));
577
+
578
+ if (isRunning && r.progress) {
579
+ const toolLine = formatCurrentToolLine(r.progress, w, expanded);
580
+ if (toolLine) {
581
+ c.addChild(new Text(fit(theme.fg("warning", `> ${toolLine}`)), 0, 0));
582
+ }
583
+ const liveStatusLine = buildLiveStatusLine(r.progress);
584
+ if (liveStatusLine) {
585
+ c.addChild(new Text(fit(theme.fg("accent", liveStatusLine)), 0, 0));
586
+ }
587
+ c.addChild(new Text(fit(theme.fg("accent", "Press Ctrl+O for live detail")), 0, 0));
588
+ if (r.artifactPaths) {
589
+ c.addChild(new Text(fit(theme.fg("dim", `Artifacts: ${shortenPath(r.artifactPaths.outputPath)}`)), 0, 0));
590
+ }
591
+ if (r.progress.recentTools?.length) {
592
+ for (const t of r.progress.recentTools.slice(-3)) {
593
+ const maxArgsLen = Math.max(40, w - 24);
594
+ const argsPreview = expanded || t.args.length <= maxArgsLen
595
+ ? t.args
596
+ : `${t.args.slice(0, maxArgsLen)}...`;
597
+ c.addChild(new Text(fit(theme.fg("dim", `${t.tool}: ${argsPreview}`)), 0, 0));
598
+ }
599
+ }
600
+ for (const line of (r.progress.recentOutput ?? []).slice(-5)) {
601
+ c.addChild(new Text(fit(theme.fg("dim", ` ${line}`)), 0, 0));
602
+ }
603
+ if (toolLine || liveStatusLine || r.progress.recentTools?.length || r.progress.recentOutput?.length || r.artifactPaths) {
604
+ c.addChild(new Spacer(1));
605
+ }
606
+ }
607
+
608
+ if (expanded) {
609
+ for (const line of toolCallLines) {
610
+ c.addChild(new Text(fit(theme.fg("muted", line)), 0, 0));
611
+ }
612
+ if (toolCallLines.length) c.addChild(new Spacer(1));
613
+ }
614
+
615
+ if (output) c.addChild(new Markdown(output, 0, 0, mdTheme));
616
+ c.addChild(new Spacer(1));
617
+ if (r.skills?.length) {
618
+ c.addChild(new Text(fit(theme.fg("dim", `Skills: ${r.skills.join(", ")}`)), 0, 0));
619
+ }
620
+ if (r.skillsWarning) {
621
+ c.addChild(new Text(fit(theme.fg("warning", `Warning: ${r.skillsWarning}`)), 0, 0));
622
+ }
623
+ if (r.attemptedModels && r.attemptedModels.length > 1) {
624
+ c.addChild(new Text(fit(theme.fg("dim", `Fallbacks: ${r.attemptedModels.join(" → ")}`)), 0, 0));
625
+ }
626
+ c.addChild(new Text(fit(theme.fg("dim", formatUsage(r.usage, r.model))), 0, 0));
627
+ if (r.sessionFile) {
628
+ c.addChild(new Text(fit(theme.fg("dim", `Session: ${shortenPath(r.sessionFile)}`)), 0, 0));
629
+ }
630
+
631
+ if (!isRunning && r.artifactPaths) {
632
+ c.addChild(new Spacer(1));
633
+ c.addChild(new Text(fit(theme.fg("dim", `Artifacts: ${shortenPath(r.artifactPaths.outputPath)}`)), 0, 0));
634
+ }
635
+ return c;
636
+ }
637
+
638
+ if (!expanded) return renderMultiCompact(d, theme);
639
+
640
+ const hasRunning = d.progress?.some((p) => p.status === "running")
641
+ || d.results.some((r) => r.progress?.status === "running");
642
+ const ok = d.results.filter((r) => r.progress?.status === "completed" || (r.exitCode === 0 && r.progress?.status !== "running")).length;
643
+ const hasEmptyWithoutTarget = d.results.some((r) =>
644
+ r.exitCode === 0
645
+ && r.progress?.status !== "running"
646
+ && hasEmptyTextOutputWithoutOutputTarget(r.task, getSingleResultOutput(r)),
647
+ );
648
+ const icon = hasRunning
649
+ ? theme.fg("warning", "running")
650
+ : hasEmptyWithoutTarget
651
+ ? theme.fg("warning", "warning")
652
+ : ok === d.results.length
653
+ ? theme.fg("success", "ok")
654
+ : theme.fg("error", "failed");
655
+
656
+ const totalSummary =
657
+ d.progressSummary ||
658
+ d.results.reduce(
659
+ (acc, r) => {
660
+ const prog = r.progress || r.progressSummary;
661
+ if (prog) {
662
+ acc.toolCount += prog.toolCount;
663
+ acc.tokens += prog.tokens;
664
+ acc.durationMs =
665
+ d.mode === "chain"
666
+ ? acc.durationMs + prog.durationMs
667
+ : Math.max(acc.durationMs, prog.durationMs);
668
+ }
669
+ return acc;
670
+ },
671
+ { toolCount: 0, tokens: 0, durationMs: 0 },
672
+ );
673
+
674
+ const summaryStr =
675
+ totalSummary.toolCount || totalSummary.tokens
676
+ ? ` | ${totalSummary.toolCount} tools, ${formatTokens(totalSummary.tokens)} tok, ${formatDuration(totalSummary.durationMs)}`
677
+ : "";
678
+
679
+ const modeLabel = d.mode;
680
+ const contextBadge = d.context === "fork" ? theme.fg("warning", " [fork]") : "";
681
+ const hasParallelInChain = d.chainAgents?.some((a) => a.startsWith("["));
682
+ const totalCount = hasParallelInChain ? d.results.length : (d.totalSteps ?? d.results.length);
683
+ const currentStep = d.currentStepIndex !== undefined ? d.currentStepIndex + 1 : ok + 1;
684
+ const stepInfo = hasRunning ? ` ${currentStep}/${totalCount}` : ` ${ok}/${totalCount}`;
685
+ const itemTitle = d.mode === "parallel" ? "Agent" : "Step";
686
+
687
+ const chainVis = d.chainAgents?.length && !hasParallelInChain
688
+ ? d.chainAgents
689
+ .map((agent, i) => {
690
+ const result = d.results[i];
691
+ const isFailed = result && result.exitCode !== 0 && result.progress?.status !== "running";
692
+ const isComplete = result && result.exitCode === 0 && result.progress?.status !== "running";
693
+ const isEmptyWithoutTarget = Boolean(result)
694
+ && Boolean(isComplete)
695
+ && hasEmptyTextOutputWithoutOutputTarget(result.task, getSingleResultOutput(result));
696
+ const isCurrent = i === (d.currentStepIndex ?? d.results.length);
697
+ const stepIcon = isFailed
698
+ ? theme.fg("error", "failed")
699
+ : isEmptyWithoutTarget
700
+ ? theme.fg("warning", "warning")
701
+ : isComplete
702
+ ? theme.fg("success", "done")
703
+ : isCurrent && hasRunning
704
+ ? theme.fg("warning", "running")
705
+ : theme.fg("dim", "pending");
706
+ return `${stepIcon} ${agent}`;
707
+ })
708
+ .join(theme.fg("dim", " → "))
709
+ : null;
710
+
711
+ const w = getTermWidth() - 4;
712
+ const fit = (text: string) => expanded ? text : truncLine(text, w);
713
+ const c = new Container();
714
+ c.addChild(
715
+ new Text(
716
+ fit(`${icon} ${theme.fg("toolTitle", theme.bold(modeLabel))}${contextBadge}${stepInfo}${summaryStr}`),
717
+ 0,
718
+ 0,
719
+ ),
720
+ );
721
+ if (chainVis) {
722
+ c.addChild(new Text(fit(` ${chainVis}`), 0, 0));
723
+ }
724
+
725
+ const useResultsDirectly = hasParallelInChain || !d.chainAgents?.length;
726
+ const stepsToShow = useResultsDirectly ? d.results.length : d.chainAgents!.length;
727
+
728
+ c.addChild(new Spacer(1));
729
+
730
+ for (let i = 0; i < stepsToShow; i++) {
731
+ const r = d.results[i];
732
+ const agentName = useResultsDirectly
733
+ ? (r?.agent || `step-${i + 1}`)
734
+ : (d.chainAgents![i] || r?.agent || `step-${i + 1}`);
735
+
736
+ if (!r) {
737
+ c.addChild(new Text(fit(theme.fg("dim", ` ${itemTitle} ${i + 1}: ${agentName}`)), 0, 0));
738
+ c.addChild(new Text(theme.fg("dim", ` status: pending`), 0, 0));
739
+ c.addChild(new Spacer(1));
740
+ continue;
741
+ }
742
+
743
+ const progressFromArray = d.progress?.find((p) => p.index === i)
744
+ || d.progress?.find((p) => p.agent === r.agent && p.status === "running");
745
+ const rProg = r.progress || progressFromArray || r.progressSummary;
746
+ const rRunning = rProg?.status === "running";
747
+ const stepNumber = typeof rProg?.index === "number" ? rProg.index + 1 : i + 1;
748
+
749
+ const resultOutput = getSingleResultOutput(r);
750
+ const statusIcon = rRunning
751
+ ? theme.fg("warning", "running")
752
+ : r.exitCode !== 0
753
+ ? theme.fg("error", "failed")
754
+ : hasEmptyTextOutputWithoutOutputTarget(r.task, resultOutput)
755
+ ? theme.fg("warning", "warning")
756
+ : theme.fg("success", "done");
757
+ const stats = rProg ? ` | ${rProg.toolCount} tools, ${formatDuration(rProg.durationMs)}` : "";
758
+ const modelDisplay = r.model ? theme.fg("dim", ` (${r.model})`) : "";
759
+ const stepHeader = rRunning
760
+ ? `${statusIcon} ${itemTitle} ${stepNumber}: ${theme.bold(theme.fg("warning", r.agent))}${modelDisplay}${stats}`
761
+ : `${statusIcon} ${itemTitle} ${stepNumber}: ${theme.bold(r.agent)}${modelDisplay}${stats}`;
762
+ const toolCallLines = getToolCallLines(r, expanded);
763
+ c.addChild(new Text(fit(stepHeader), 0, 0));
764
+
765
+ const taskMaxLen = Math.max(20, w - 12);
766
+ const taskPreview = expanded || r.task.length <= taskMaxLen
767
+ ? r.task
768
+ : `${r.task.slice(0, taskMaxLen)}...`;
769
+ c.addChild(new Text(fit(theme.fg("dim", ` task: ${taskPreview}`)), 0, 0));
770
+
771
+ const outputTarget = extractOutputTarget(r.task);
772
+ if (outputTarget) {
773
+ c.addChild(new Text(fit(theme.fg("dim", ` output: ${outputTarget}`)), 0, 0));
774
+ }
775
+
776
+ if (r.skills?.length) {
777
+ c.addChild(new Text(fit(theme.fg("dim", ` skills: ${r.skills.join(", ")}`)), 0, 0));
778
+ }
779
+ if (r.skillsWarning) {
780
+ c.addChild(new Text(fit(theme.fg("warning", ` Warning: ${r.skillsWarning}`)), 0, 0));
781
+ }
782
+ if (r.attemptedModels && r.attemptedModels.length > 1) {
783
+ c.addChild(new Text(fit(theme.fg("dim", ` fallbacks: ${r.attemptedModels.join(" → ")}`)), 0, 0));
784
+ }
785
+
786
+ if (rRunning && rProg) {
787
+ if (rProg.skills?.length) {
788
+ c.addChild(new Text(fit(theme.fg("accent", ` skills: ${rProg.skills.join(", ")}`)), 0, 0));
789
+ }
790
+ const toolLine = formatCurrentToolLine(rProg, w, expanded);
791
+ if (toolLine) {
792
+ c.addChild(new Text(fit(theme.fg("warning", ` > ${toolLine}`)), 0, 0));
793
+ }
794
+ const liveStatusLine = buildLiveStatusLine(rProg);
795
+ if (liveStatusLine) {
796
+ c.addChild(new Text(fit(theme.fg("accent", ` ${liveStatusLine}`)), 0, 0));
797
+ }
798
+ c.addChild(new Text(fit(theme.fg("accent", " Press Ctrl+O for live detail")), 0, 0));
799
+ if (r.artifactPaths) {
800
+ c.addChild(new Text(fit(theme.fg("dim", ` artifacts: ${shortenPath(r.artifactPaths.outputPath)}`)), 0, 0));
801
+ }
802
+ if (rProg.recentTools?.length) {
803
+ for (const t of rProg.recentTools.slice(-3)) {
804
+ const maxArgsLen = Math.max(40, w - 30);
805
+ const argsPreview = expanded || t.args.length <= maxArgsLen
806
+ ? t.args
807
+ : `${t.args.slice(0, maxArgsLen)}...`;
808
+ c.addChild(new Text(fit(theme.fg("dim", ` ${t.tool}: ${argsPreview}`)), 0, 0));
809
+ }
810
+ }
811
+ const recentLines = (rProg.recentOutput ?? []).slice(-5);
812
+ for (const line of recentLines) {
813
+ c.addChild(new Text(fit(theme.fg("dim", ` ${line}`)), 0, 0));
814
+ }
815
+ }
816
+
817
+ if (!rRunning && r.artifactPaths) {
818
+ c.addChild(new Text(fit(theme.fg("dim", ` artifacts: ${shortenPath(r.artifactPaths.outputPath)}`)), 0, 0));
819
+ }
820
+
821
+ if (expanded && !rRunning) {
822
+ for (const line of toolCallLines) {
823
+ c.addChild(new Text(fit(theme.fg("muted", ` ${line}`)), 0, 0));
824
+ }
825
+ if (toolCallLines.length) c.addChild(new Spacer(1));
826
+ }
827
+
828
+ c.addChild(new Spacer(1));
829
+ }
830
+
831
+ if (d.artifacts) {
832
+ c.addChild(new Spacer(1));
833
+ c.addChild(new Text(fit(theme.fg("dim", `Artifacts dir: ${shortenPath(d.artifacts.dir)}`)), 0, 0));
834
+ }
835
+ return c;
836
+ }