pi-subagents 0.24.3 → 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 +18 -5
- package/README.md +3 -1
- package/package.json +4 -8
- 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 +7 -23
- package/src/intercom/intercom-bridge.ts +2 -1
- package/src/runs/background/async-execution.ts +6 -3
- package/src/runs/background/async-job-tracker.ts +16 -8
- package/src/runs/background/subagent-runner.ts +18 -7
- package/src/runs/foreground/execution.ts +17 -5
- package/src/runs/foreground/subagent-executor.ts +4 -4
- 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 +1 -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/utils.ts +11 -1
- package/src/tui/render.ts +148 -144
package/src/tui/render.ts
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
WIDGET_KEY,
|
|
17
17
|
} from "../shared/types.ts";
|
|
18
18
|
import { formatTokens, formatUsage, formatDuration, formatModelThinking, formatToolCall, shortenPath } from "../shared/formatters.ts";
|
|
19
|
-
import { getDisplayItems,
|
|
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,10 +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);
|
|
361
|
-
return ` ${theme.fg("dim", `${marker} ${widgetStepGlyph(step.status, theme)} ${itemTitle} ${index + 1}/${total}: ${step.agent} · ${widgetStepStatus(step.status, theme)}${modelDisplay}${activity ? ` · ${activity}` : ""}`)}`;
|
|
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}` : ""}`)}`;
|
|
362
413
|
});
|
|
363
414
|
}
|
|
364
415
|
|
|
@@ -563,8 +614,7 @@ function widgetStats(job: AsyncJobState, theme: Theme): string {
|
|
|
563
614
|
}
|
|
564
615
|
if (job.toolCount !== undefined) parts.push(formatToolUseStat(job.toolCount));
|
|
565
616
|
if (job.totalTokens?.total) parts.push(formatTokenStat(job.totalTokens.total));
|
|
566
|
-
|
|
567
|
-
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)));
|
|
568
618
|
return statJoin(theme, parts);
|
|
569
619
|
}
|
|
570
620
|
|
|
@@ -582,10 +632,10 @@ function modelThinkingBadge(theme: Theme, model?: string, thinking?: string): st
|
|
|
582
632
|
return label ? theme.fg("dim", ` (${label})`) : "";
|
|
583
633
|
}
|
|
584
634
|
|
|
585
|
-
function widgetStepActivityLine(step: NonNullable<AsyncJobState["steps"]>[number], width: number, expanded: boolean): string {
|
|
586
|
-
const toolLine = formatCurrentToolLine(step, width, expanded);
|
|
635
|
+
function widgetStepActivityLine(step: NonNullable<AsyncJobState["steps"]>[number], width: number, expanded: boolean, snapshotNow?: number): string {
|
|
636
|
+
const toolLine = formatCurrentToolLine(step, width, expanded, snapshotNow);
|
|
587
637
|
if (toolLine) return toolLine;
|
|
588
|
-
const activity =
|
|
638
|
+
const activity = buildLiveStatusLine(step, snapshotNow);
|
|
589
639
|
if (activity) return activity;
|
|
590
640
|
if (step.status === "running") return "thinking…";
|
|
591
641
|
return "";
|
|
@@ -609,15 +659,15 @@ function foregroundStyleWidgetStepLines(
|
|
|
609
659
|
const status = widgetStepStatus(step.status, theme);
|
|
610
660
|
const stats = widgetStepStats(theme, step);
|
|
611
661
|
const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking);
|
|
612
|
-
const lines = [` ${widgetStepGlyph(step.status, theme)} ${itemTitle} ${index}/${total}: ${themeBold(theme, step.agent)} ${theme.fg("dim", "·")} ${status}${modelDisplay}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`];
|
|
613
|
-
const activity = widgetStepActivityLine(step, width, expanded);
|
|
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);
|
|
614
664
|
if (activity) lines.push(` ${theme.fg("dim", `⎿ ${activity}`)}`);
|
|
615
665
|
if (step.status === "running") {
|
|
616
666
|
if (!expanded) lines.push(` ${theme.fg("accent", "Press Ctrl+O for live detail")}`);
|
|
617
667
|
const output = widgetOutputPath(job, step);
|
|
618
668
|
if (output) lines.push(` ${theme.fg("dim", `output: ${shortenPath(output)}`)}`);
|
|
619
669
|
if (expanded) {
|
|
620
|
-
const liveStatus = buildLiveStatusLine(step);
|
|
670
|
+
const liveStatus = buildLiveStatusLine(step, job.updatedAt);
|
|
621
671
|
if (liveStatus && liveStatus !== activity) lines.push(` ${theme.fg("accent", liveStatus)}`);
|
|
622
672
|
for (const tool of step.recentTools?.slice(-3) ?? []) {
|
|
623
673
|
const maxArgsLen = Math.max(40, width - 30);
|
|
@@ -665,11 +715,11 @@ function compactSingleWidgetLines(job: AsyncJobState, theme: Theme, width: numbe
|
|
|
665
715
|
const lines = fullLines.slice(0, 2);
|
|
666
716
|
for (const [index, step] of job.steps.entries()) {
|
|
667
717
|
const status = widgetStepStatus(step.status, theme);
|
|
668
|
-
const activity = widgetStepActivityLine(step, width, false);
|
|
718
|
+
const activity = widgetStepActivityLine(step, width, false, job.updatedAt);
|
|
669
719
|
const stepStats = widgetStepStats(theme, step);
|
|
670
720
|
const activitySuffix = activity ? ` ${theme.fg("dim", "·")} ${theme.fg("dim", activity)}` : "";
|
|
671
721
|
const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking);
|
|
672
|
-
lines.push(` ${widgetStepGlyph(step.status, theme)} ${itemTitle} ${index + 1}/${total}: ${themeBold(theme, step.agent)} ${theme.fg("dim", "·")} ${status}${modelDisplay}${activitySuffix}${stepStats ? ` ${theme.fg("dim", "·")} ${stepStats}` : ""}`);
|
|
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}` : ""}`);
|
|
673
723
|
}
|
|
674
724
|
if (job.steps.some((step) => step.status === "running")) lines.push(theme.fg("accent", " Press Ctrl+O for live detail"));
|
|
675
725
|
return lines.map((line) => truncLine(line, width));
|
|
@@ -712,7 +762,8 @@ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = ge
|
|
|
712
762
|
|
|
713
763
|
const lines: string[] = [];
|
|
714
764
|
const hasActive = running.length > 0 || queued.length > 0;
|
|
715
|
-
|
|
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));
|
|
716
767
|
|
|
717
768
|
const items: string[][] = [];
|
|
718
769
|
let hiddenRunning = 0;
|
|
@@ -772,66 +823,16 @@ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = ge
|
|
|
772
823
|
return lines;
|
|
773
824
|
}
|
|
774
825
|
|
|
775
|
-
function refreshAnimatedWidget(): void {
|
|
776
|
-
try {
|
|
777
|
-
if (!latestWidgetCtx?.hasUI || latestWidgetJobs.length === 0) return;
|
|
778
|
-
latestWidgetCtx.ui.setWidget(WIDGET_KEY, buildWidgetComponent(latestWidgetJobs, latestWidgetCtx.ui.getToolsExpanded?.() ?? false));
|
|
779
|
-
latestWidgetCtx.ui.requestRender?.();
|
|
780
|
-
} catch (error) {
|
|
781
|
-
if (!isStaleExtensionContextError(error)) throw error;
|
|
782
|
-
stopWidgetAnimation();
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
function ensureWidgetAnimation(): void {
|
|
787
|
-
if (widgetTimer) return;
|
|
788
|
-
widgetTimer = setInterval(() => {
|
|
789
|
-
if (!hasAnimatedWidgetJobs(latestWidgetJobs)) {
|
|
790
|
-
stopWidgetAnimation();
|
|
791
|
-
return;
|
|
792
|
-
}
|
|
793
|
-
refreshAnimatedWidget();
|
|
794
|
-
}, WIDGET_ANIMATION_MS);
|
|
795
|
-
widgetTimer.unref?.();
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
export function stopWidgetAnimation(): void {
|
|
799
|
-
if (widgetTimer) {
|
|
800
|
-
clearInterval(widgetTimer);
|
|
801
|
-
widgetTimer = undefined;
|
|
802
|
-
}
|
|
803
|
-
latestWidgetCtx = undefined;
|
|
804
|
-
latestWidgetJobs = [];
|
|
805
|
-
outputActivityCache.clear();
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
export function stopResultAnimations(): void {
|
|
809
|
-
for (const [timer, state] of resultAnimationTimers) {
|
|
810
|
-
clearInterval(timer);
|
|
811
|
-
state.subagentResultAnimationTimer = undefined;
|
|
812
|
-
}
|
|
813
|
-
resultAnimationTimers.clear();
|
|
814
|
-
}
|
|
815
|
-
|
|
816
826
|
/**
|
|
817
827
|
* Render the async jobs widget
|
|
818
828
|
*/
|
|
819
829
|
export function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void {
|
|
820
830
|
if (jobs.length === 0) {
|
|
821
|
-
stopWidgetAnimation();
|
|
822
831
|
if (ctx.hasUI) ctx.ui.setWidget(WIDGET_KEY, undefined);
|
|
823
832
|
return;
|
|
824
833
|
}
|
|
825
|
-
if (!ctx.hasUI)
|
|
826
|
-
stopWidgetAnimation();
|
|
827
|
-
return;
|
|
828
|
-
}
|
|
829
|
-
latestWidgetCtx = ctx;
|
|
830
|
-
latestWidgetJobs = [...jobs];
|
|
831
|
-
|
|
834
|
+
if (!ctx.hasUI) return;
|
|
832
835
|
ctx.ui.setWidget(WIDGET_KEY, buildWidgetComponent(jobs, ctx.ui.getToolsExpanded?.() ?? false));
|
|
833
|
-
if (hasAnimatedWidgetJobs(jobs)) ensureWidgetAnimation();
|
|
834
|
-
else stopWidgetAnimation();
|
|
835
836
|
}
|
|
836
837
|
|
|
837
838
|
function renderSingleCompact(d: Details, r: Details["results"][number], theme: Theme): Component {
|
|
@@ -849,9 +850,10 @@ function renderSingleCompact(d: Details, r: Details["results"][number], theme: T
|
|
|
849
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));
|
|
850
851
|
|
|
851
852
|
if (isRunning && r.progress) {
|
|
853
|
+
const progressSnapshotNow = snapshotNowForProgress(r.progress);
|
|
852
854
|
const activity = compactCurrentActivity(r.progress);
|
|
853
855
|
c.addChild(new Text(truncLine(theme.fg("dim", ` ⎿ ${activity}`), width), 0, 0));
|
|
854
|
-
const liveStatus = buildLiveStatusLine(r.progress);
|
|
856
|
+
const liveStatus = buildLiveStatusLine(r.progress, progressSnapshotNow);
|
|
855
857
|
if (liveStatus && liveStatus !== activity) c.addChild(new Text(truncLine(theme.fg("dim", ` ${liveStatus}`), width), 0, 0));
|
|
856
858
|
c.addChild(new Text(truncLine(theme.fg("accent", " Press Ctrl+O for live detail"), width), 0, 0));
|
|
857
859
|
if (r.artifactPaths) c.addChild(new Text(truncLine(theme.fg("dim", ` output: ${shortenPath(r.artifactPaths.outputPath)}`), width), 0, 0));
|
|
@@ -892,7 +894,7 @@ function renderMultiCompact(d: Details, theme: Theme): Component {
|
|
|
892
894
|
const itemTitle = multiLabel.itemTitle;
|
|
893
895
|
const stats = statJoin(theme, [multiLabel.headerLabel, formatProgressStats(theme, totalSummary)]);
|
|
894
896
|
const glyph = hasRunning
|
|
895
|
-
? theme.fg("accent",
|
|
897
|
+
? theme.fg("accent", runningGlyph(runningSeed(progressRunningSeed(totalSummary), d.currentStepIndex)))
|
|
896
898
|
: failed
|
|
897
899
|
? theme.fg("error", "✗")
|
|
898
900
|
: paused
|
|
@@ -922,7 +924,7 @@ function renderMultiCompact(d: Details, theme: Theme): Component {
|
|
|
922
924
|
const rPending = rProg && "status" in rProg && rProg.status === "pending";
|
|
923
925
|
const stepNumber = r.progress?.index !== undefined ? r.progress.index + 1 : progressFromArray?.index !== undefined ? progressFromArray.index + 1 : i + 1;
|
|
924
926
|
const stepStats = formatProgressStats(theme, rProg);
|
|
925
|
-
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));
|
|
926
928
|
const pendingLabel = rPending ? ` ${theme.fg("dim", "· pending")}` : "";
|
|
927
929
|
const stepLabel = resultRowLabel(d, multiLabel, i, stepNumber);
|
|
928
930
|
const line = `${glyph} ${stepLabel}: ${themeBold(theme, agentName)}${stepStats ? ` ${theme.fg("dim", "·")} ${stepStats}` : ""}${pendingLabel}`;
|
|
@@ -997,11 +999,12 @@ export function renderSubagentResult(
|
|
|
997
999
|
c.addChild(new Spacer(1));
|
|
998
1000
|
|
|
999
1001
|
if (isRunning && r.progress) {
|
|
1000
|
-
const
|
|
1002
|
+
const progressSnapshotNow = snapshotNowForProgress(r.progress);
|
|
1003
|
+
const toolLine = formatCurrentToolLine(r.progress, w, expanded, progressSnapshotNow);
|
|
1001
1004
|
if (toolLine) {
|
|
1002
1005
|
c.addChild(new Text(fit(theme.fg("warning", `> ${toolLine}`)), 0, 0));
|
|
1003
1006
|
}
|
|
1004
|
-
const liveStatusLine = buildLiveStatusLine(r.progress);
|
|
1007
|
+
const liveStatusLine = buildLiveStatusLine(r.progress, progressSnapshotNow);
|
|
1005
1008
|
if (liveStatusLine) {
|
|
1006
1009
|
c.addChild(new Text(fit(theme.fg("accent", liveStatusLine)), 0, 0));
|
|
1007
1010
|
}
|
|
@@ -1208,11 +1211,12 @@ export function renderSubagentResult(
|
|
|
1208
1211
|
if (rProg.skills?.length) {
|
|
1209
1212
|
c.addChild(new Text(fit(theme.fg("accent", ` skills: ${rProg.skills.join(", ")}`)), 0, 0));
|
|
1210
1213
|
}
|
|
1211
|
-
const
|
|
1214
|
+
const progressSnapshotNow = snapshotNowForProgress(rProg);
|
|
1215
|
+
const toolLine = formatCurrentToolLine(rProg, w, expanded, progressSnapshotNow);
|
|
1212
1216
|
if (toolLine) {
|
|
1213
1217
|
c.addChild(new Text(fit(theme.fg("warning", ` > ${toolLine}`)), 0, 0));
|
|
1214
1218
|
}
|
|
1215
|
-
const liveStatusLine = buildLiveStatusLine(rProg);
|
|
1219
|
+
const liveStatusLine = buildLiveStatusLine(rProg, progressSnapshotNow);
|
|
1216
1220
|
if (liveStatusLine) {
|
|
1217
1221
|
c.addChild(new Text(fit(theme.fg("accent", ` ${liveStatusLine}`)), 0, 0));
|
|
1218
1222
|
}
|