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