pi-subagents 0.24.2 → 0.24.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.md +13 -5
- package/package.json +4 -8
- package/prompts/review-loop.md +41 -0
- package/skills/pi-subagents/SKILL.md +51 -21
- package/src/agents/agent-management.ts +5 -0
- package/src/agents/agent-serializer.ts +2 -0
- package/src/agents/agents.ts +30 -6
- package/src/agents/skills.ts +25 -23
- package/src/extension/config.ts +16 -0
- package/src/extension/index.ts +8 -24
- package/src/extension/schemas.ts +1 -1
- package/src/intercom/intercom-bridge.ts +2 -1
- package/src/runs/background/async-execution.ts +16 -5
- package/src/runs/background/async-job-tracker.ts +16 -8
- package/src/runs/background/async-status.ts +5 -2
- package/src/runs/background/run-status.ts +4 -1
- package/src/runs/background/subagent-runner.ts +34 -7
- package/src/runs/foreground/execution.ts +17 -5
- package/src/runs/foreground/subagent-executor.ts +6 -7
- package/src/runs/shared/completion-guard.ts +23 -1
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +365 -0
- package/src/runs/shared/parallel-utils.ts +2 -0
- package/src/runs/shared/pi-args.ts +5 -0
- package/src/runs/shared/run-history.ts +12 -7
- package/src/runs/shared/single-output.ts +12 -2
- package/src/shared/artifacts.ts +2 -2
- package/src/shared/formatters.ts +13 -0
- package/src/shared/model-info.ts +10 -0
- package/src/shared/types.ts +1 -0
- package/src/shared/utils.ts +11 -1
- package/src/tui/render.ts +160 -147
package/src/tui/render.ts
CHANGED
|
@@ -15,8 +15,8 @@ import {
|
|
|
15
15
|
MAX_WIDGET_JOBS,
|
|
16
16
|
WIDGET_KEY,
|
|
17
17
|
} from "../shared/types.ts";
|
|
18
|
-
import { formatTokens, formatUsage, formatDuration, formatToolCall, shortenPath } from "../shared/formatters.ts";
|
|
19
|
-
import { getDisplayItems,
|
|
18
|
+
import { formatTokens, formatUsage, formatDuration, formatModelThinking, formatToolCall, shortenPath } from "../shared/formatters.ts";
|
|
19
|
+
import { getDisplayItems, getSingleResultOutput } from "../shared/utils.ts";
|
|
20
20
|
import { flatToLogicalStepIndex } from "../runs/background/parallel-groups.ts";
|
|
21
21
|
import { aggregateStepStatus, formatActivityLabel, formatAgentRunningLabel, formatParallelOutcome } from "../shared/status-format.ts";
|
|
22
22
|
|
|
@@ -84,64 +84,49 @@ function truncLine(text: string, maxWidth: number): string {
|
|
|
84
84
|
return result + activeStyles.join("") + "…";
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
const
|
|
88
|
-
const
|
|
87
|
+
const RUNNING_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
88
|
+
const STATIC_RUNNING_GLYPH = "●";
|
|
89
89
|
|
|
90
|
-
|
|
91
|
-
let latestWidgetCtx: ExtensionContext | undefined;
|
|
92
|
-
let latestWidgetJobs: AsyncJobState[] = [];
|
|
90
|
+
type ProgressSeedSource = Partial<Pick<AgentProgress, "index" | "toolCount" | "tokens" | "durationMs" | "lastActivityAt" | "currentToolStartedAt" | "turnCount">>;
|
|
93
91
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
92
|
+
function runningSeed(...values: Array<number | undefined>): number | undefined {
|
|
93
|
+
let seed: number | undefined;
|
|
94
|
+
for (const value of values) {
|
|
95
|
+
if (value === undefined || !Number.isFinite(value)) continue;
|
|
96
|
+
seed = (seed ?? 0) + Math.trunc(value);
|
|
97
|
+
}
|
|
98
|
+
return seed;
|
|
101
99
|
}
|
|
102
100
|
|
|
103
|
-
function
|
|
104
|
-
|
|
101
|
+
function runningGlyph(seed?: number): string {
|
|
102
|
+
if (seed === undefined) return STATIC_RUNNING_GLYPH;
|
|
103
|
+
return RUNNING_FRAMES[Math.abs(seed) % RUNNING_FRAMES.length]!;
|
|
105
104
|
}
|
|
106
105
|
|
|
107
|
-
function
|
|
108
|
-
if (!
|
|
109
|
-
return
|
|
106
|
+
function progressRunningSeed(progress: ProgressSeedSource | undefined): number | undefined {
|
|
107
|
+
if (!progress) return undefined;
|
|
108
|
+
return runningSeed(
|
|
109
|
+
progress.index,
|
|
110
|
+
progress.toolCount,
|
|
111
|
+
progress.tokens,
|
|
112
|
+
progress.durationMs,
|
|
113
|
+
progress.lastActivityAt,
|
|
114
|
+
progress.currentToolStartedAt,
|
|
115
|
+
progress.turnCount,
|
|
116
|
+
);
|
|
110
117
|
}
|
|
111
118
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|| result.details?.results.some((entry) => entry.progress?.status === "running")
|
|
115
|
-
|| false;
|
|
119
|
+
interface LegacyResultAnimationContext {
|
|
120
|
+
state: { subagentResultAnimationTimer?: ReturnType<typeof setInterval> };
|
|
116
121
|
}
|
|
117
122
|
|
|
118
|
-
function
|
|
123
|
+
export function clearLegacyResultAnimationTimer(context: LegacyResultAnimationContext): void {
|
|
119
124
|
const timer = context.state.subagentResultAnimationTimer;
|
|
120
125
|
if (!timer) return;
|
|
121
126
|
clearInterval(timer);
|
|
122
|
-
resultAnimationTimers.delete(timer);
|
|
123
127
|
context.state.subagentResultAnimationTimer = undefined;
|
|
124
128
|
}
|
|
125
129
|
|
|
126
|
-
export function syncResultAnimation(result: AgentToolResult<Details>, context: ResultAnimationContext): void {
|
|
127
|
-
if (!resultIsRunning(result)) {
|
|
128
|
-
stopResultAnimation(context);
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
if (context.state.subagentResultAnimationTimer) return;
|
|
132
|
-
const timer = setInterval(() => {
|
|
133
|
-
try {
|
|
134
|
-
context.invalidate();
|
|
135
|
-
} catch (error) {
|
|
136
|
-
if (!isStaleExtensionContextError(error)) throw error;
|
|
137
|
-
stopResultAnimation(context);
|
|
138
|
-
}
|
|
139
|
-
}, WIDGET_ANIMATION_MS);
|
|
140
|
-
timer.unref?.();
|
|
141
|
-
context.state.subagentResultAnimationTimer = timer;
|
|
142
|
-
resultAnimationTimers.set(timer, context.state);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
130
|
function extractOutputTarget(task: string): string | undefined {
|
|
146
131
|
const writeToMatch = task.match(/\[Write to:\s*([^\]\n]+)\]/i);
|
|
147
132
|
if (writeToMatch?.[1]?.trim()) return writeToMatch[1].trim();
|
|
@@ -170,7 +155,17 @@ function getToolCallLines(
|
|
|
170
155
|
}
|
|
171
156
|
|
|
172
157
|
|
|
173
|
-
function
|
|
158
|
+
function snapshotNowForProgress(progress: Pick<AgentProgress, "currentToolStartedAt" | "durationMs" | "lastActivityAt">): number | undefined {
|
|
159
|
+
if (progress.currentToolStartedAt !== undefined && progress.durationMs !== undefined) return progress.currentToolStartedAt + progress.durationMs;
|
|
160
|
+
return progress.lastActivityAt;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function formatCurrentToolLine(
|
|
164
|
+
progress: Pick<AgentProgress, "currentTool" | "currentToolArgs" | "currentToolStartedAt">,
|
|
165
|
+
availableWidth: number,
|
|
166
|
+
expanded: boolean,
|
|
167
|
+
snapshotNow?: number,
|
|
168
|
+
): string | undefined {
|
|
174
169
|
if (!progress.currentTool) return undefined;
|
|
175
170
|
const maxToolArgsLen = Math.max(50, availableWidth - 20);
|
|
176
171
|
const toolArgsPreview = progress.currentToolArgs
|
|
@@ -178,16 +173,20 @@ function formatCurrentToolLine(progress: Pick<AgentProgress, "currentTool" | "cu
|
|
|
178
173
|
? progress.currentToolArgs
|
|
179
174
|
: `${progress.currentToolArgs.slice(0, maxToolArgsLen)}...`)
|
|
180
175
|
: "";
|
|
181
|
-
const durationSuffix = progress.currentToolStartedAt !== undefined
|
|
182
|
-
? ` | ${formatDuration(Math.max(0,
|
|
176
|
+
const durationSuffix = progress.currentToolStartedAt !== undefined && snapshotNow !== undefined
|
|
177
|
+
? ` | ${formatDuration(Math.max(0, snapshotNow - progress.currentToolStartedAt))}`
|
|
183
178
|
: "";
|
|
184
179
|
return toolArgsPreview
|
|
185
180
|
? `${progress.currentTool}: ${toolArgsPreview}${durationSuffix}`
|
|
186
181
|
: `${progress.currentTool}${durationSuffix}`;
|
|
187
182
|
}
|
|
188
183
|
|
|
189
|
-
function buildLiveStatusLine(progress: Pick<AgentProgress, "activityState" | "lastActivityAt"
|
|
190
|
-
return formatActivityLabel(progress.lastActivityAt, progress.activityState);
|
|
184
|
+
function buildLiveStatusLine(progress: Pick<AgentProgress, "activityState" | "lastActivityAt">, snapshotNow?: number): string | undefined {
|
|
185
|
+
if (progress.lastActivityAt !== undefined && snapshotNow !== undefined) return formatActivityLabel(progress.lastActivityAt, progress.activityState, snapshotNow);
|
|
186
|
+
if (progress.activityState === "needs_attention") return "needs attention";
|
|
187
|
+
if (progress.activityState === "active_long_running") return "active but long-running";
|
|
188
|
+
if (progress.lastActivityAt !== undefined) return "active";
|
|
189
|
+
return undefined;
|
|
191
190
|
}
|
|
192
191
|
|
|
193
192
|
function themeBold(theme: Theme, text: string): string {
|
|
@@ -227,8 +226,8 @@ function resultStatusLine(result: Details["results"][number], output: string): s
|
|
|
227
226
|
return "Done";
|
|
228
227
|
}
|
|
229
228
|
|
|
230
|
-
function resultGlyph(result: Details["results"][number], output: string, theme: Theme, running = result.progress?.status === "running"): string {
|
|
231
|
-
if (running) return theme.fg("accent",
|
|
229
|
+
function resultGlyph(result: Details["results"][number], output: string, theme: Theme, running = result.progress?.status === "running", seed = progressRunningSeed(result.progress ?? result.progressSummary)): string {
|
|
230
|
+
if (running) return theme.fg("accent", runningGlyph(seed));
|
|
232
231
|
if (result.detached) return theme.fg("warning", "■");
|
|
233
232
|
if (result.interrupted) return theme.fg("warning", "■");
|
|
234
233
|
if (result.exitCode !== 0) return theme.fg("error", "✗");
|
|
@@ -237,11 +236,35 @@ function resultGlyph(result: Details["results"][number], output: string, theme:
|
|
|
237
236
|
}
|
|
238
237
|
|
|
239
238
|
function compactCurrentActivity(progress: AgentProgress): string {
|
|
240
|
-
|
|
239
|
+
const snapshotNow = snapshotNowForProgress(progress);
|
|
240
|
+
return formatCurrentToolLine(progress, getTermWidth() - 4, false, snapshotNow) ?? buildLiveStatusLine(progress, snapshotNow) ?? "thinking…";
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
-
function
|
|
244
|
-
return
|
|
243
|
+
export function widgetRenderKey(job: AsyncJobState): string {
|
|
244
|
+
return JSON.stringify({
|
|
245
|
+
asyncDir: job.asyncDir,
|
|
246
|
+
status: job.status,
|
|
247
|
+
activityState: job.activityState,
|
|
248
|
+
lastActivityAt: job.lastActivityAt,
|
|
249
|
+
currentTool: job.currentTool,
|
|
250
|
+
currentToolStartedAt: job.currentToolStartedAt,
|
|
251
|
+
currentPath: job.currentPath,
|
|
252
|
+
turnCount: job.turnCount,
|
|
253
|
+
toolCount: job.toolCount,
|
|
254
|
+
mode: job.mode,
|
|
255
|
+
agents: job.agents,
|
|
256
|
+
currentStep: job.currentStep,
|
|
257
|
+
chainStepCount: job.chainStepCount,
|
|
258
|
+
parallelGroups: job.parallelGroups,
|
|
259
|
+
steps: job.steps,
|
|
260
|
+
stepsTotal: job.stepsTotal,
|
|
261
|
+
runningSteps: job.runningSteps,
|
|
262
|
+
completedSteps: job.completedSteps,
|
|
263
|
+
activeParallelGroup: job.activeParallelGroup,
|
|
264
|
+
startedAt: job.startedAt,
|
|
265
|
+
updatedAt: job.updatedAt,
|
|
266
|
+
totalTokens: job.totalTokens,
|
|
267
|
+
});
|
|
245
268
|
}
|
|
246
269
|
|
|
247
270
|
function formatWidgetAgents(agents: string[]): string {
|
|
@@ -259,25 +282,14 @@ function widgetJobName(job: AsyncJobState): string {
|
|
|
259
282
|
return job.mode ?? "subagent";
|
|
260
283
|
}
|
|
261
284
|
|
|
262
|
-
function getCachedLastActivity(outputFile: string | undefined): string {
|
|
263
|
-
if (!outputFile) return "";
|
|
264
|
-
const now = Date.now();
|
|
265
|
-
const cached = outputActivityCache.get(outputFile);
|
|
266
|
-
if (cached && now - cached.checkedAt < 1000) return cached.text;
|
|
267
|
-
const text = getLastActivity(outputFile);
|
|
268
|
-
outputActivityCache.set(outputFile, { checkedAt: now, text });
|
|
269
|
-
return text;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
285
|
function widgetActivity(job: AsyncJobState): string {
|
|
273
286
|
const facts: string[] = [];
|
|
274
|
-
if (job.currentTool && job.currentToolStartedAt !== undefined) facts.push(`${job.currentTool} ${formatDuration(Math.max(0,
|
|
287
|
+
if (job.currentTool && job.currentToolStartedAt !== undefined && job.updatedAt !== undefined) facts.push(`${job.currentTool} ${formatDuration(Math.max(0, job.updatedAt - job.currentToolStartedAt))}`);
|
|
275
288
|
else if (job.currentTool) facts.push(job.currentTool);
|
|
276
289
|
if (job.currentPath) facts.push(shortenPath(job.currentPath));
|
|
277
290
|
if (job.turnCount !== undefined) facts.push(`${job.turnCount} turns`);
|
|
278
291
|
if (job.toolCount !== undefined) facts.push(`${job.toolCount} tools`);
|
|
279
|
-
const activity =
|
|
280
|
-
?? (job.status === "running" ? getCachedLastActivity(job.outputFile) : "");
|
|
292
|
+
const activity = buildLiveStatusLine(job, job.updatedAt);
|
|
281
293
|
if (activity && facts.length) return `${activity} · ${facts.join(" · ")}`;
|
|
282
294
|
if (activity) return activity;
|
|
283
295
|
if (facts.length) return facts.join(" · ");
|
|
@@ -288,16 +300,55 @@ function widgetActivity(job: AsyncJobState): string {
|
|
|
288
300
|
return "Done";
|
|
289
301
|
}
|
|
290
302
|
|
|
303
|
+
function widgetStepRunningSeed(step: NonNullable<AsyncJobState["steps"]>[number], fallbackIndex?: number): number | undefined {
|
|
304
|
+
return runningSeed(
|
|
305
|
+
fallbackIndex,
|
|
306
|
+
step.index,
|
|
307
|
+
step.toolCount,
|
|
308
|
+
step.turnCount,
|
|
309
|
+
step.tokens?.total,
|
|
310
|
+
step.lastActivityAt,
|
|
311
|
+
step.currentToolStartedAt,
|
|
312
|
+
step.durationMs,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function widgetStepsRunningSeed(steps: Array<NonNullable<AsyncJobState["steps"]>[number]> | undefined): number | undefined {
|
|
317
|
+
let seed: number | undefined;
|
|
318
|
+
for (const [index, step] of (steps ?? []).entries()) seed = runningSeed(seed, widgetStepRunningSeed(step, index));
|
|
319
|
+
return seed;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function widgetJobRunningSeed(job: AsyncJobState): number | undefined {
|
|
323
|
+
return runningSeed(
|
|
324
|
+
job.updatedAt,
|
|
325
|
+
job.lastActivityAt,
|
|
326
|
+
job.toolCount,
|
|
327
|
+
job.turnCount,
|
|
328
|
+
job.totalTokens?.total,
|
|
329
|
+
job.currentStep,
|
|
330
|
+
job.runningSteps,
|
|
331
|
+
job.completedSteps,
|
|
332
|
+
widgetStepsRunningSeed(job.steps),
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function widgetJobsRunningSeed(jobs: AsyncJobState[]): number | undefined {
|
|
337
|
+
let seed: number | undefined;
|
|
338
|
+
for (const job of jobs) seed = runningSeed(seed, widgetJobRunningSeed(job));
|
|
339
|
+
return seed;
|
|
340
|
+
}
|
|
341
|
+
|
|
291
342
|
function widgetStatusGlyph(job: AsyncJobState, theme: Theme): string {
|
|
292
|
-
if (job.status === "running") return theme.fg("accent",
|
|
343
|
+
if (job.status === "running") return theme.fg("accent", runningGlyph(widgetJobRunningSeed(job)));
|
|
293
344
|
if (job.status === "queued") return theme.fg("muted", "◦");
|
|
294
345
|
if (job.status === "complete") return theme.fg("success", "✓");
|
|
295
346
|
if (job.status === "paused") return theme.fg("warning", "■");
|
|
296
347
|
return theme.fg("error", "✗");
|
|
297
348
|
}
|
|
298
349
|
|
|
299
|
-
function widgetStepGlyph(status: AsyncJobStep["status"], theme: Theme): string {
|
|
300
|
-
if (status === "running") return theme.fg("accent",
|
|
350
|
+
function widgetStepGlyph(status: AsyncJobStep["status"], theme: Theme, seed?: number): string {
|
|
351
|
+
if (status === "running") return theme.fg("accent", runningGlyph(seed));
|
|
301
352
|
if (status === "complete" || status === "completed") return theme.fg("success", "✓");
|
|
302
353
|
if (status === "failed") return theme.fg("error", "✗");
|
|
303
354
|
if (status === "paused") return theme.fg("warning", "■");
|
|
@@ -312,15 +363,15 @@ function widgetStepStatus(status: AsyncJobStep["status"], theme: Theme): string
|
|
|
312
363
|
return theme.fg("dim", status);
|
|
313
364
|
}
|
|
314
365
|
|
|
315
|
-
function widgetStepActivity(step: NonNullable<AsyncJobState["steps"]>[number]): string {
|
|
366
|
+
function widgetStepActivity(step: NonNullable<AsyncJobState["steps"]>[number], snapshotNow?: number): string {
|
|
316
367
|
const facts: string[] = [];
|
|
317
|
-
if (step.currentTool && step.currentToolStartedAt !== undefined) facts.push(`${step.currentTool} ${formatDuration(Math.max(0,
|
|
368
|
+
if (step.currentTool && step.currentToolStartedAt !== undefined && snapshotNow !== undefined) facts.push(`${step.currentTool} ${formatDuration(Math.max(0, snapshotNow - step.currentToolStartedAt))}`);
|
|
318
369
|
else if (step.currentTool) facts.push(step.currentTool);
|
|
319
370
|
if (step.currentPath) facts.push(shortenPath(step.currentPath));
|
|
320
371
|
if (step.turnCount !== undefined) facts.push(`${step.turnCount} turns`);
|
|
321
372
|
if (step.toolCount !== undefined) facts.push(`${step.toolCount} tools`);
|
|
322
373
|
if (step.tokens?.total) facts.push(formatTokenStat(step.tokens.total));
|
|
323
|
-
const activity =
|
|
374
|
+
const activity = buildLiveStatusLine(step, snapshotNow);
|
|
324
375
|
if (activity && facts.length) return `${activity} · ${facts.join(" · ")}`;
|
|
325
376
|
if (activity) return activity;
|
|
326
377
|
return facts.join(" · ");
|
|
@@ -335,7 +386,7 @@ function widgetChainDetails(job: AsyncJobState, theme: Theme, expanded = false,
|
|
|
335
386
|
const steps = job.steps.slice(span.start, span.start + span.count);
|
|
336
387
|
if (span.isParallel) {
|
|
337
388
|
const status = aggregateStepStatus(steps);
|
|
338
|
-
lines.push(` ${widgetStepGlyph(status, theme)} Step ${span.stepIndex + 1}/${total}: ${themeBold(theme, "parallel group")} ${theme.fg("dim", "·")} ${theme.fg("dim", formatParallelOutcome(steps, span.count))}`);
|
|
389
|
+
lines.push(` ${widgetStepGlyph(status, theme, widgetStepsRunningSeed(steps))} Step ${span.stepIndex + 1}/${total}: ${themeBold(theme, "parallel group")} ${theme.fg("dim", "·")} ${theme.fg("dim", formatParallelOutcome(steps, span.count))}`);
|
|
339
390
|
continue;
|
|
340
391
|
}
|
|
341
392
|
const step = steps[0];
|
|
@@ -355,9 +406,10 @@ function widgetParallelAgentDetails(job: AsyncJobState, theme: Theme): string[]
|
|
|
355
406
|
const total = job.stepsTotal ?? job.steps.length;
|
|
356
407
|
return job.steps.map((step, index) => {
|
|
357
408
|
const marker = index === job.steps!.length - 1 ? "└" : "├";
|
|
358
|
-
const activity = widgetStepActivity(step);
|
|
409
|
+
const activity = widgetStepActivity(step, job.updatedAt);
|
|
359
410
|
const itemTitle = job.mode === "parallel" || job.activeParallelGroup ? "Agent" : "Step";
|
|
360
|
-
|
|
411
|
+
const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking);
|
|
412
|
+
return ` ${theme.fg("dim", `${marker} ${widgetStepGlyph(step.status, theme, widgetStepRunningSeed(step, index))} ${itemTitle} ${index + 1}/${total}: ${step.agent} · ${widgetStepStatus(step.status, theme)}${modelDisplay}${activity ? ` · ${activity}` : ""}`)}`;
|
|
361
413
|
});
|
|
362
414
|
}
|
|
363
415
|
|
|
@@ -562,8 +614,7 @@ function widgetStats(job: AsyncJobState, theme: Theme): string {
|
|
|
562
614
|
}
|
|
563
615
|
if (job.toolCount !== undefined) parts.push(formatToolUseStat(job.toolCount));
|
|
564
616
|
if (job.totalTokens?.total) parts.push(formatTokenStat(job.totalTokens.total));
|
|
565
|
-
|
|
566
|
-
if (job.startedAt) parts.push(formatDuration(Math.max(0, endTime - job.startedAt)));
|
|
617
|
+
if (job.startedAt !== undefined && job.updatedAt !== undefined) parts.push(formatDuration(Math.max(0, job.updatedAt - job.startedAt)));
|
|
567
618
|
return statJoin(theme, parts);
|
|
568
619
|
}
|
|
569
620
|
|
|
@@ -576,10 +627,15 @@ function widgetStepStats(theme: Theme, step: NonNullable<AsyncJobState["steps"]>
|
|
|
576
627
|
]);
|
|
577
628
|
}
|
|
578
629
|
|
|
579
|
-
function
|
|
580
|
-
const
|
|
630
|
+
function modelThinkingBadge(theme: Theme, model?: string, thinking?: string): string {
|
|
631
|
+
const label = formatModelThinking(model, thinking);
|
|
632
|
+
return label ? theme.fg("dim", ` (${label})`) : "";
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function widgetStepActivityLine(step: NonNullable<AsyncJobState["steps"]>[number], width: number, expanded: boolean, snapshotNow?: number): string {
|
|
636
|
+
const toolLine = formatCurrentToolLine(step, width, expanded, snapshotNow);
|
|
581
637
|
if (toolLine) return toolLine;
|
|
582
|
-
const activity =
|
|
638
|
+
const activity = buildLiveStatusLine(step, snapshotNow);
|
|
583
639
|
if (activity) return activity;
|
|
584
640
|
if (step.status === "running") return "thinking…";
|
|
585
641
|
return "";
|
|
@@ -602,15 +658,16 @@ function foregroundStyleWidgetStepLines(
|
|
|
602
658
|
): string[] {
|
|
603
659
|
const status = widgetStepStatus(step.status, theme);
|
|
604
660
|
const stats = widgetStepStats(theme, step);
|
|
605
|
-
const
|
|
606
|
-
const
|
|
661
|
+
const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking);
|
|
662
|
+
const lines = [` ${widgetStepGlyph(step.status, theme, widgetStepRunningSeed(step, index - 1))} ${itemTitle} ${index}/${total}: ${themeBold(theme, step.agent)} ${theme.fg("dim", "·")} ${status}${modelDisplay}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`];
|
|
663
|
+
const activity = widgetStepActivityLine(step, width, expanded, job.updatedAt);
|
|
607
664
|
if (activity) lines.push(` ${theme.fg("dim", `⎿ ${activity}`)}`);
|
|
608
665
|
if (step.status === "running") {
|
|
609
666
|
if (!expanded) lines.push(` ${theme.fg("accent", "Press Ctrl+O for live detail")}`);
|
|
610
667
|
const output = widgetOutputPath(job, step);
|
|
611
668
|
if (output) lines.push(` ${theme.fg("dim", `output: ${shortenPath(output)}`)}`);
|
|
612
669
|
if (expanded) {
|
|
613
|
-
const liveStatus = buildLiveStatusLine(step);
|
|
670
|
+
const liveStatus = buildLiveStatusLine(step, job.updatedAt);
|
|
614
671
|
if (liveStatus && liveStatus !== activity) lines.push(` ${theme.fg("accent", liveStatus)}`);
|
|
615
672
|
for (const tool of step.recentTools?.slice(-3) ?? []) {
|
|
616
673
|
const maxArgsLen = Math.max(40, width - 30);
|
|
@@ -658,10 +715,11 @@ function compactSingleWidgetLines(job: AsyncJobState, theme: Theme, width: numbe
|
|
|
658
715
|
const lines = fullLines.slice(0, 2);
|
|
659
716
|
for (const [index, step] of job.steps.entries()) {
|
|
660
717
|
const status = widgetStepStatus(step.status, theme);
|
|
661
|
-
const activity = widgetStepActivityLine(step, width, false);
|
|
718
|
+
const activity = widgetStepActivityLine(step, width, false, job.updatedAt);
|
|
662
719
|
const stepStats = widgetStepStats(theme, step);
|
|
663
720
|
const activitySuffix = activity ? ` ${theme.fg("dim", "·")} ${theme.fg("dim", activity)}` : "";
|
|
664
|
-
|
|
721
|
+
const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking);
|
|
722
|
+
lines.push(` ${widgetStepGlyph(step.status, theme, widgetStepRunningSeed(step, index))} ${itemTitle} ${index + 1}/${total}: ${themeBold(theme, step.agent)} ${theme.fg("dim", "·")} ${status}${modelDisplay}${activitySuffix}${stepStats ? ` ${theme.fg("dim", "·")} ${stepStats}` : ""}`);
|
|
665
723
|
}
|
|
666
724
|
if (job.steps.some((step) => step.status === "running")) lines.push(theme.fg("accent", " Press Ctrl+O for live detail"));
|
|
667
725
|
return lines.map((line) => truncLine(line, width));
|
|
@@ -704,7 +762,8 @@ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = ge
|
|
|
704
762
|
|
|
705
763
|
const lines: string[] = [];
|
|
706
764
|
const hasActive = running.length > 0 || queued.length > 0;
|
|
707
|
-
|
|
765
|
+
const headerGlyph = running.length > 0 ? runningGlyph(widgetJobsRunningSeed(running)) : hasActive ? "●" : "○";
|
|
766
|
+
lines.push(truncLine(`${theme.fg(hasActive ? "accent" : "dim", headerGlyph)} ${theme.fg(hasActive ? "accent" : "dim", "Async agents")} ${theme.fg("dim", "· background")}`, width));
|
|
708
767
|
|
|
709
768
|
const items: string[][] = [];
|
|
710
769
|
let hiddenRunning = 0;
|
|
@@ -764,66 +823,16 @@ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = ge
|
|
|
764
823
|
return lines;
|
|
765
824
|
}
|
|
766
825
|
|
|
767
|
-
function refreshAnimatedWidget(): void {
|
|
768
|
-
try {
|
|
769
|
-
if (!latestWidgetCtx?.hasUI || latestWidgetJobs.length === 0) return;
|
|
770
|
-
latestWidgetCtx.ui.setWidget(WIDGET_KEY, buildWidgetComponent(latestWidgetJobs, latestWidgetCtx.ui.getToolsExpanded?.() ?? false));
|
|
771
|
-
latestWidgetCtx.ui.requestRender?.();
|
|
772
|
-
} catch (error) {
|
|
773
|
-
if (!isStaleExtensionContextError(error)) throw error;
|
|
774
|
-
stopWidgetAnimation();
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
function ensureWidgetAnimation(): void {
|
|
779
|
-
if (widgetTimer) return;
|
|
780
|
-
widgetTimer = setInterval(() => {
|
|
781
|
-
if (!hasAnimatedWidgetJobs(latestWidgetJobs)) {
|
|
782
|
-
stopWidgetAnimation();
|
|
783
|
-
return;
|
|
784
|
-
}
|
|
785
|
-
refreshAnimatedWidget();
|
|
786
|
-
}, WIDGET_ANIMATION_MS);
|
|
787
|
-
widgetTimer.unref?.();
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
export function stopWidgetAnimation(): void {
|
|
791
|
-
if (widgetTimer) {
|
|
792
|
-
clearInterval(widgetTimer);
|
|
793
|
-
widgetTimer = undefined;
|
|
794
|
-
}
|
|
795
|
-
latestWidgetCtx = undefined;
|
|
796
|
-
latestWidgetJobs = [];
|
|
797
|
-
outputActivityCache.clear();
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
export function stopResultAnimations(): void {
|
|
801
|
-
for (const [timer, state] of resultAnimationTimers) {
|
|
802
|
-
clearInterval(timer);
|
|
803
|
-
state.subagentResultAnimationTimer = undefined;
|
|
804
|
-
}
|
|
805
|
-
resultAnimationTimers.clear();
|
|
806
|
-
}
|
|
807
|
-
|
|
808
826
|
/**
|
|
809
827
|
* Render the async jobs widget
|
|
810
828
|
*/
|
|
811
829
|
export function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void {
|
|
812
830
|
if (jobs.length === 0) {
|
|
813
|
-
stopWidgetAnimation();
|
|
814
831
|
if (ctx.hasUI) ctx.ui.setWidget(WIDGET_KEY, undefined);
|
|
815
832
|
return;
|
|
816
833
|
}
|
|
817
|
-
if (!ctx.hasUI)
|
|
818
|
-
stopWidgetAnimation();
|
|
819
|
-
return;
|
|
820
|
-
}
|
|
821
|
-
latestWidgetCtx = ctx;
|
|
822
|
-
latestWidgetJobs = [...jobs];
|
|
823
|
-
|
|
834
|
+
if (!ctx.hasUI) return;
|
|
824
835
|
ctx.ui.setWidget(WIDGET_KEY, buildWidgetComponent(jobs, ctx.ui.getToolsExpanded?.() ?? false));
|
|
825
|
-
if (hasAnimatedWidgetJobs(jobs)) ensureWidgetAnimation();
|
|
826
|
-
else stopWidgetAnimation();
|
|
827
836
|
}
|
|
828
837
|
|
|
829
838
|
function renderSingleCompact(d: Details, r: Details["results"][number], theme: Theme): Component {
|
|
@@ -837,12 +846,14 @@ function renderSingleCompact(d: Details, r: Details["results"][number], theme: T
|
|
|
837
846
|
]);
|
|
838
847
|
const c = new Container();
|
|
839
848
|
const width = getTermWidth() - 4;
|
|
840
|
-
|
|
849
|
+
const modelDisplay = modelThinkingBadge(theme, r.model);
|
|
850
|
+
c.addChild(new Text(truncLine(`${resultGlyph(r, output, theme, isRunning)} ${theme.fg("toolTitle", theme.bold(r.agent))}${modelDisplay}${contextBadge}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`, width), 0, 0));
|
|
841
851
|
|
|
842
852
|
if (isRunning && r.progress) {
|
|
853
|
+
const progressSnapshotNow = snapshotNowForProgress(r.progress);
|
|
843
854
|
const activity = compactCurrentActivity(r.progress);
|
|
844
855
|
c.addChild(new Text(truncLine(theme.fg("dim", ` ⎿ ${activity}`), width), 0, 0));
|
|
845
|
-
const liveStatus = buildLiveStatusLine(r.progress);
|
|
856
|
+
const liveStatus = buildLiveStatusLine(r.progress, progressSnapshotNow);
|
|
846
857
|
if (liveStatus && liveStatus !== activity) c.addChild(new Text(truncLine(theme.fg("dim", ` ${liveStatus}`), width), 0, 0));
|
|
847
858
|
c.addChild(new Text(truncLine(theme.fg("accent", " Press Ctrl+O for live detail"), width), 0, 0));
|
|
848
859
|
if (r.artifactPaths) c.addChild(new Text(truncLine(theme.fg("dim", ` output: ${shortenPath(r.artifactPaths.outputPath)}`), width), 0, 0));
|
|
@@ -883,7 +894,7 @@ function renderMultiCompact(d: Details, theme: Theme): Component {
|
|
|
883
894
|
const itemTitle = multiLabel.itemTitle;
|
|
884
895
|
const stats = statJoin(theme, [multiLabel.headerLabel, formatProgressStats(theme, totalSummary)]);
|
|
885
896
|
const glyph = hasRunning
|
|
886
|
-
? theme.fg("accent",
|
|
897
|
+
? theme.fg("accent", runningGlyph(runningSeed(progressRunningSeed(totalSummary), d.currentStepIndex)))
|
|
887
898
|
: failed
|
|
888
899
|
? theme.fg("error", "✗")
|
|
889
900
|
: paused
|
|
@@ -913,7 +924,7 @@ function renderMultiCompact(d: Details, theme: Theme): Component {
|
|
|
913
924
|
const rPending = rProg && "status" in rProg && rProg.status === "pending";
|
|
914
925
|
const stepNumber = r.progress?.index !== undefined ? r.progress.index + 1 : progressFromArray?.index !== undefined ? progressFromArray.index + 1 : i + 1;
|
|
915
926
|
const stepStats = formatProgressStats(theme, rProg);
|
|
916
|
-
const glyph = rPending ? theme.fg("dim", "◦") : resultGlyph(r, output, theme, rRunning);
|
|
927
|
+
const glyph = rPending ? theme.fg("dim", "◦") : resultGlyph(r, output, theme, rRunning, progressRunningSeed(rProg));
|
|
917
928
|
const pendingLabel = rPending ? ` ${theme.fg("dim", "· pending")}` : "";
|
|
918
929
|
const stepLabel = resultRowLabel(d, multiLabel, i, stepNumber);
|
|
919
930
|
const line = `${glyph} ${stepLabel}: ${themeBold(theme, agentName)}${stepStats ? ` ${theme.fg("dim", "·")} ${stepStats}` : ""}${pendingLabel}`;
|
|
@@ -988,11 +999,12 @@ export function renderSubagentResult(
|
|
|
988
999
|
c.addChild(new Spacer(1));
|
|
989
1000
|
|
|
990
1001
|
if (isRunning && r.progress) {
|
|
991
|
-
const
|
|
1002
|
+
const progressSnapshotNow = snapshotNowForProgress(r.progress);
|
|
1003
|
+
const toolLine = formatCurrentToolLine(r.progress, w, expanded, progressSnapshotNow);
|
|
992
1004
|
if (toolLine) {
|
|
993
1005
|
c.addChild(new Text(fit(theme.fg("warning", `> ${toolLine}`)), 0, 0));
|
|
994
1006
|
}
|
|
995
|
-
const liveStatusLine = buildLiveStatusLine(r.progress);
|
|
1007
|
+
const liveStatusLine = buildLiveStatusLine(r.progress, progressSnapshotNow);
|
|
996
1008
|
if (liveStatusLine) {
|
|
997
1009
|
c.addChild(new Text(fit(theme.fg("accent", liveStatusLine)), 0, 0));
|
|
998
1010
|
}
|
|
@@ -1166,7 +1178,7 @@ export function renderSubagentResult(
|
|
|
1166
1178
|
? theme.fg("warning", "warning")
|
|
1167
1179
|
: theme.fg("success", "done");
|
|
1168
1180
|
const stats = rProg ? ` | ${rProg.toolCount} tools, ${formatDuration(rProg.durationMs)}` : "";
|
|
1169
|
-
const modelDisplay =
|
|
1181
|
+
const modelDisplay = modelThinkingBadge(theme, r.model);
|
|
1170
1182
|
const stepLabel = resultRowLabel(d, multiLabel, i, stepNumber);
|
|
1171
1183
|
const stepHeader = rRunning
|
|
1172
1184
|
? `${statusIcon} ${stepLabel}: ${theme.bold(theme.fg("warning", r.agent))}${modelDisplay}${stats}`
|
|
@@ -1199,11 +1211,12 @@ export function renderSubagentResult(
|
|
|
1199
1211
|
if (rProg.skills?.length) {
|
|
1200
1212
|
c.addChild(new Text(fit(theme.fg("accent", ` skills: ${rProg.skills.join(", ")}`)), 0, 0));
|
|
1201
1213
|
}
|
|
1202
|
-
const
|
|
1214
|
+
const progressSnapshotNow = snapshotNowForProgress(rProg);
|
|
1215
|
+
const toolLine = formatCurrentToolLine(rProg, w, expanded, progressSnapshotNow);
|
|
1203
1216
|
if (toolLine) {
|
|
1204
1217
|
c.addChild(new Text(fit(theme.fg("warning", ` > ${toolLine}`)), 0, 0));
|
|
1205
1218
|
}
|
|
1206
|
-
const liveStatusLine = buildLiveStatusLine(rProg);
|
|
1219
|
+
const liveStatusLine = buildLiveStatusLine(rProg, progressSnapshotNow);
|
|
1207
1220
|
if (liveStatusLine) {
|
|
1208
1221
|
c.addChild(new Text(fit(theme.fg("accent", ` ${liveStatusLine}`)), 0, 0));
|
|
1209
1222
|
}
|