pi-subagents 0.60.0 → 0.61.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/CHANGELOG.md +32 -0
- package/docs/agents.md +2 -2
- package/docs/configuration.md +9 -5
- package/docs/extension-api.md +14 -7
- package/docs/models.md +1 -1
- package/docs/observability.md +1 -1
- package/docs/tool-reference.md +12 -3
- package/docs/workflows.md +14 -13
- package/install.mjs +2 -1
- package/package.json +1 -1
- package/skills/pi-subagents/SKILL.md +6 -4
- package/skills/pi-subagents/references/constraints-and-recipes.md +1 -1
- package/skills/pi-subagents/references/execution-controls.md +42 -11
- package/skills/pi-subagents/references/multi-lane-orchestration.md +1 -1
- package/skills/pi-subagents/references/prompting-and-roles.md +4 -4
- package/skills/pi-subagents/references/review-and-validation.md +1 -1
- package/src/agents/agent-management.ts +102 -63
- package/src/agents/agents.ts +527 -221
- package/src/api/background-work.ts +7 -2
- package/src/api/external-runs.ts +67 -4
- package/src/api/preflight.ts +13 -8
- package/src/api/shared-types.ts +1 -0
- package/src/extension/index.ts +7 -4
- package/src/extension/public-execution.ts +47 -4
- package/src/extension/rpc.ts +62 -4
- package/src/extension/schemas.ts +11 -7
- package/src/extension/tool-description.ts +14 -18
- package/src/runs/background/async-execution.ts +51 -35
- package/src/runs/background/async-job-tracker.ts +62 -3
- package/src/runs/background/async-resume.ts +3 -1
- package/src/runs/background/async-status.ts +59 -9
- package/src/runs/background/auto-drain.ts +1 -1
- package/src/runs/background/fleet-view.ts +1 -1
- package/src/runs/background/result-watcher.ts +1 -1
- package/src/runs/background/resume-guidance.ts +1 -1
- package/src/runs/background/run-status.ts +2 -2
- package/src/runs/background/subagent-runner.ts +1 -2
- package/src/runs/background/subagent-wait.ts +20 -21
- package/src/runs/background/wait-completions.ts +1 -1
- package/src/runs/background/wait-tool.ts +24 -18
- package/src/runs/foreground/execution.ts +61 -2
- package/src/runs/foreground/subagent-executor.ts +178 -58
- package/src/runs/shared/acceptance.ts +43 -18
- package/src/runs/shared/host-step-status.ts +1 -0
- package/src/runs/shared/model-fallback.ts +61 -17
- package/src/runs/shared/permissions.ts +1 -1
- package/src/runs/shared/tool-timeout.ts +1 -1
- package/src/runs/shared/workflow-graph.ts +3 -2
- package/src/shared/types.ts +40 -3
- package/src/shared/workflow-child-permit.ts +91 -0
- package/src/slash/prompt-template-bridge.ts +37 -1
- package/src/slash/slash-commands.ts +18 -26
- package/src/tui/render.ts +95 -52
- package/src/workflows/scripted-workflow.ts +153 -4
- package/src/workflows/workflow-child-summary.ts +1 -1
- package/src/workflows/workflow-receipt.ts +41 -4
- package/src/workflows/workflow-resources.ts +150 -0
|
@@ -3,7 +3,7 @@ import * as fs from "node:fs";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { keyText, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { Key, matchesKey, truncateToWidth, type Component, type KeyId, type TUI } from "@earendil-works/pi-tui";
|
|
6
|
-
import {
|
|
6
|
+
import { discoverAgentSnapshot, discoverAgents, findBlockingAgentDiagnostic, formatUnknownAgentError, resolveAgentName, unknownAgentDiagnosticContext, type AgentConfig, type AgentDiscoveryDiagnostic, type AgentScope, type UnknownAgentDiagnosticContext } from "../agents/agents.ts";
|
|
7
7
|
import { listRuntimeAgentConfigs, mergeRuntimeAgents } from "../agents/runtime-agent-registry.ts";
|
|
8
8
|
import { resolveExistingReadPaths } from "../shared/settings.ts";
|
|
9
9
|
import {
|
|
@@ -113,9 +113,13 @@ const extractExecutionFlags = (rawArgs: string): { args: string; bg: boolean; fo
|
|
|
113
113
|
};
|
|
114
114
|
|
|
115
115
|
function discoverSlashAgents(pi: ExtensionAPI, cwd: string, scope: AgentScope): { agents: AgentConfig[]; agentDiagnostics?: AgentDiscoveryDiagnostic[]; unknownAgentDiagnosticContext: UnknownAgentDiagnosticContext } {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
116
|
+
if (listRuntimeAgentConfigs(pi).length === 0) {
|
|
117
|
+
const discovered = discoverAgents(cwd, scope);
|
|
118
|
+
return { ...discovered, unknownAgentDiagnosticContext: unknownAgentDiagnosticContext(discovered) };
|
|
119
|
+
}
|
|
120
|
+
const snapshot = discoverAgentSnapshot(cwd, scope, undefined, { includeChains: false });
|
|
121
|
+
const discovered = snapshot.effective;
|
|
122
|
+
const all = snapshot.all;
|
|
119
123
|
const configuredAgents: AgentConfig[] = [
|
|
120
124
|
...all.builtin,
|
|
121
125
|
...all.package,
|
|
@@ -133,13 +137,6 @@ const makeAgentCompletions = (pi: ExtensionAPI, state: SubagentState) => (prefix
|
|
|
133
137
|
.map((agent) => ({ value: agent.name, label: agent.name }));
|
|
134
138
|
};
|
|
135
139
|
|
|
136
|
-
const makeBuiltinAgentNameCompletions = () => (prefix: string) => {
|
|
137
|
-
if (prefix.includes(" ")) return null;
|
|
138
|
-
return BUILTIN_AGENT_NAMES
|
|
139
|
-
.filter((name) => name.startsWith(prefix))
|
|
140
|
-
.map((name) => ({ value: name, label: name }));
|
|
141
|
-
};
|
|
142
|
-
|
|
143
140
|
const makeProviderCompletions = (state: SubagentState) => (prefix: string) => {
|
|
144
141
|
if (prefix.includes(" ")) return null;
|
|
145
142
|
const available = state.lastUiContext?.modelRegistry?.getAvailable?.();
|
|
@@ -420,7 +417,7 @@ function detailsFromSessionEntry(entry: unknown): Details | undefined {
|
|
|
420
417
|
}
|
|
421
418
|
if (record.type !== "message" || !record.message || typeof record.message !== "object") return undefined;
|
|
422
419
|
const message = record.message as { role?: unknown; toolName?: unknown; details?: unknown };
|
|
423
|
-
if (message.role !== "toolResult" || (message.toolName !== "subagent" && message.toolName !== "subagent_wait")) return undefined;
|
|
420
|
+
if (message.role !== "toolResult" || (message.toolName !== "subagent" && message.toolName !== "bg_wait" && message.toolName !== "subagent_wait")) return undefined;
|
|
424
421
|
return isSubagentDetails(message.details) ? message.details : undefined;
|
|
425
422
|
}
|
|
426
423
|
|
|
@@ -1005,7 +1002,7 @@ export function registerSlashCommands(
|
|
|
1005
1002
|
ctx.ui.notify(`Foreground run ${control.runId} is not currently detachable.`, "info");
|
|
1006
1003
|
return;
|
|
1007
1004
|
}
|
|
1008
|
-
sendSlashText(pi, `Detached foreground run ${control.runId} without terminating its child. Use subagent({ action: "status", id: ${JSON.stringify(control.runId)} }) or
|
|
1005
|
+
sendSlashText(pi, `Detached foreground run ${control.runId} without terminating its child. Use subagent({ action: "status", id: ${JSON.stringify(control.runId)} }) or bg_wait({ id: ${JSON.stringify(control.runId)} }) to recover the eventual result. This does not daemonize the process or guarantee survival across Pi reload/restart.`);
|
|
1009
1006
|
};
|
|
1010
1007
|
|
|
1011
1008
|
pi.registerCommand("subagents-detach", {
|
|
@@ -1140,25 +1137,20 @@ export function registerSlashCommands(
|
|
|
1140
1137
|
});
|
|
1141
1138
|
|
|
1142
1139
|
pi.registerCommand("subagents-models", {
|
|
1143
|
-
description: "Show
|
|
1144
|
-
getArgumentCompletions:
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1140
|
+
description: "Show model mappings for discovered subagents",
|
|
1141
|
+
getArgumentCompletions: makeAgentCompletions(pi, state),
|
|
1142
|
+
handler: async (args, ctx) => {
|
|
1143
|
+
const trimmed = args.trim();
|
|
1144
|
+
if (!trimmed) {
|
|
1145
|
+
await runCommand(ctx, { action: "models" });
|
|
1149
1146
|
return;
|
|
1150
1147
|
}
|
|
1151
1148
|
const parts = trimmed.split(/\s+/).filter(Boolean);
|
|
1152
1149
|
if (parts.length !== 1) {
|
|
1153
|
-
ctx.ui.notify("Usage: /subagents-models [
|
|
1154
|
-
return;
|
|
1155
|
-
}
|
|
1156
|
-
const agent = parts[0]!;
|
|
1157
|
-
if (!(BUILTIN_AGENT_NAMES as readonly string[]).includes(agent)) {
|
|
1158
|
-
ctx.ui.notify(`Unknown builtin agent: ${agent}`, "error");
|
|
1150
|
+
ctx.ui.notify("Usage: /subagents-models [agent-name]", "error");
|
|
1159
1151
|
return;
|
|
1160
1152
|
}
|
|
1161
|
-
await runCommand(ctx, { action: "models", agent });
|
|
1153
|
+
await runCommand(ctx, { action: "models", agent: parts[0]! });
|
|
1162
1154
|
},
|
|
1163
1155
|
});
|
|
1164
1156
|
|
package/src/tui/render.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
type HostStepVerdict,
|
|
18
18
|
type NestedRunSummary,
|
|
19
19
|
type NestedStepSummary,
|
|
20
|
+
type WorkflowGraphNode,
|
|
20
21
|
type WorkflowNodeStatus,
|
|
21
22
|
type MainWindowRendererConfig,
|
|
22
23
|
MAX_WIDGET_JOBS,
|
|
@@ -41,6 +42,15 @@ import { workflowGraphStageNodes } from "../runs/shared/workflow-graph.ts";
|
|
|
41
42
|
|
|
42
43
|
type Theme = ExtensionContext["ui"]["theme"];
|
|
43
44
|
|
|
45
|
+
interface WorkflowWidgetProjection {
|
|
46
|
+
stages: WorkflowGraphNode[];
|
|
47
|
+
steps: AsyncJobStep[];
|
|
48
|
+
stageProgress?: { total: number; current?: number };
|
|
49
|
+
plannedKeys?: Set<string>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
type WorkflowWidgetProjectionLookup = (job: AsyncJobState) => WorkflowWidgetProjection;
|
|
53
|
+
|
|
44
54
|
interface MainWindowRenderLayout {
|
|
45
55
|
horizontalSpacing: number;
|
|
46
56
|
compactResultMaxLines?: number;
|
|
@@ -194,10 +204,7 @@ function runningSeed(...values: Array<number | undefined>): number | undefined {
|
|
|
194
204
|
|
|
195
205
|
function runningGlyph(seed?: number): string {
|
|
196
206
|
if (seed === undefined) return STATIC_RUNNING_GLYPH;
|
|
197
|
-
|
|
198
|
-
// activity; keep the seed so parallel rows stay phase-offset.
|
|
199
|
-
const clock = Math.floor(Date.now() / 125);
|
|
200
|
-
return RUNNING_FRAMES[Math.abs(seed + clock) % RUNNING_FRAMES.length]!;
|
|
207
|
+
return RUNNING_FRAMES[Math.abs(seed) % RUNNING_FRAMES.length]!;
|
|
201
208
|
}
|
|
202
209
|
|
|
203
210
|
function animatedSeed(seed: number | undefined, frame: number | undefined): number | undefined {
|
|
@@ -358,8 +365,7 @@ function workflowStepPriority(step: AsyncJobStep, currentNodeId?: string): numbe
|
|
|
358
365
|
}
|
|
359
366
|
|
|
360
367
|
/** Merge materialized child rows with the planned workflow stages for rendering. */
|
|
361
|
-
function workflowWidgetSteps(job: AsyncJobState): AsyncJobStep[] {
|
|
362
|
-
const planned = job.mode === "workflow" ? workflowGraphStageNodes(job.workflowGraph) : [];
|
|
368
|
+
function workflowWidgetSteps(job: AsyncJobState, planned = job.mode === "workflow" ? workflowGraphStageNodes(job.workflowGraph) : []): AsyncJobStep[] {
|
|
363
369
|
const loaded = job.steps ?? [];
|
|
364
370
|
if (planned.length === 0) return loaded;
|
|
365
371
|
|
|
@@ -415,9 +421,8 @@ function workflowWidgetSteps(job: AsyncJobState): AsyncJobStep[] {
|
|
|
415
421
|
return entries.map(({ step }) => step);
|
|
416
422
|
}
|
|
417
423
|
|
|
418
|
-
function workflowStageProgress(job: AsyncJobState): { total: number; current?: number } | undefined {
|
|
424
|
+
function workflowStageProgress(job: AsyncJobState, stages = job.mode === "workflow" ? workflowGraphStageNodes(job.workflowGraph) : []): { total: number; current?: number } | undefined {
|
|
419
425
|
if (job.mode !== "workflow") return undefined;
|
|
420
|
-
const stages = workflowGraphStageNodes(job.workflowGraph);
|
|
421
426
|
if (stages.length === 0) return undefined;
|
|
422
427
|
const currentId = job.workflowGraph?.currentNodeId;
|
|
423
428
|
const currentIndex = currentId ? stages.findIndex((stage) => stage.id === currentId) : -1;
|
|
@@ -430,8 +435,28 @@ function workflowStageProgress(job: AsyncJobState): { total: number; current?: n
|
|
|
430
435
|
return { total: stages.length };
|
|
431
436
|
}
|
|
432
437
|
|
|
433
|
-
function
|
|
434
|
-
const
|
|
438
|
+
function buildWorkflowWidgetProjection(job: AsyncJobState): WorkflowWidgetProjection {
|
|
439
|
+
const stages = job.mode === "workflow" ? workflowGraphStageNodes(job.workflowGraph) : [];
|
|
440
|
+
const stageProgress = workflowStageProgress(job, stages);
|
|
441
|
+
const steps = workflowWidgetSteps(job, stages);
|
|
442
|
+
const projection: WorkflowWidgetProjection = { stages, steps };
|
|
443
|
+
if (stageProgress) projection.stageProgress = stageProgress;
|
|
444
|
+
if (stages.length) projection.plannedKeys = new Set(stages.map((node) => node.id));
|
|
445
|
+
return projection;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function workflowWidgetProjectionLookup(): WorkflowWidgetProjectionLookup {
|
|
449
|
+
const projections = new WeakMap<AsyncJobState, WorkflowWidgetProjection>();
|
|
450
|
+
return (job: AsyncJobState) => {
|
|
451
|
+
const cached = projections.get(job);
|
|
452
|
+
if (cached) return cached;
|
|
453
|
+
const projection = buildWorkflowWidgetProjection(job);
|
|
454
|
+
projections.set(job, projection);
|
|
455
|
+
return projection;
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function laneStepForJob(job: AsyncJobState, steps = workflowWidgetSteps(job)): AsyncJobStep | undefined {
|
|
435
460
|
if (steps.length === 0) return undefined;
|
|
436
461
|
const graphCurrent = job.workflowGraph?.currentNodeId;
|
|
437
462
|
if (graphCurrent) {
|
|
@@ -487,7 +512,8 @@ function isTerminalLaneState(state: AsyncJobState["status"]): boolean {
|
|
|
487
512
|
}
|
|
488
513
|
|
|
489
514
|
/** Project already-loaded async status facts into one bounded, render-only lane row. */
|
|
490
|
-
export function projectAsyncLane(job: AsyncJobState, selectedStep
|
|
515
|
+
export function projectAsyncLane(job: AsyncJobState, ...args: [selectedStep?: AsyncJobStep]): AsyncLaneProjection | undefined {
|
|
516
|
+
const selectedStep = args.length === 0 ? laneStepForJob(job) : args[0];
|
|
491
517
|
const trace = laneTraceForJob(job);
|
|
492
518
|
const workspace = job.cwd ? boundedLaneValue(shortenPath(job.cwd)) : undefined;
|
|
493
519
|
const label = compactTaskText(selectedStep?.description, selectedStep?.label)
|
|
@@ -553,14 +579,16 @@ function formatLaneProjectionLines(lane: AsyncLaneProjection, theme: Theme, inde
|
|
|
553
579
|
];
|
|
554
580
|
}
|
|
555
581
|
|
|
556
|
-
function laneRenderKey(job: AsyncJobState): unknown {
|
|
557
|
-
const
|
|
582
|
+
function laneRenderKey(job: AsyncJobState, projection?: WorkflowWidgetProjection): unknown {
|
|
583
|
+
const selectedStep = projection ? laneStepForJob(job, projection.steps) : laneStepForJob(job);
|
|
584
|
+
const lane = projectAsyncLane(job, selectedStep);
|
|
558
585
|
return lane ? [lane.label, lane.role, lane.phase, lane.state, lane.gate, lane.next, lane.output, lane.workspace, lane.ref, lane.chips] : undefined;
|
|
559
586
|
}
|
|
560
587
|
|
|
561
|
-
function widgetLaneDetailLines(job: AsyncJobState, theme: Theme): string[] {
|
|
588
|
+
function widgetLaneDetailLines(job: AsyncJobState, theme: Theme, projection?: WorkflowWidgetProjection): string[] {
|
|
562
589
|
if (job.steps?.length && (job.mode === "parallel" || job.mode === "chain")) return [];
|
|
563
|
-
const
|
|
590
|
+
const selectedStep = projection ? laneStepForJob(job, projection.steps) : laneStepForJob(job);
|
|
591
|
+
const lane = projectAsyncLane(job, selectedStep);
|
|
564
592
|
return lane ? formatLaneProjectionLines(lane, theme, " ") : [];
|
|
565
593
|
}
|
|
566
594
|
|
|
@@ -1008,6 +1036,7 @@ function hostStepRenderKey(row: AsyncStatusWorkflowRow): unknown[] {
|
|
|
1008
1036
|
}
|
|
1009
1037
|
|
|
1010
1038
|
export function widgetRenderKey(job: AsyncJobState, expanded = false): string {
|
|
1039
|
+
const projection = buildWorkflowWidgetProjection(job);
|
|
1011
1040
|
return JSON.stringify({
|
|
1012
1041
|
asyncDir: job.asyncDir,
|
|
1013
1042
|
status: job.status,
|
|
@@ -1026,13 +1055,13 @@ export function widgetRenderKey(job: AsyncJobState, expanded = false): string {
|
|
|
1026
1055
|
workflowHostSteps: projectAsyncWorkflowRows([], job.hostSteps).map(hostStepRenderKey),
|
|
1027
1056
|
workflowGraph: job.mode === "workflow" && job.workflowGraph ? {
|
|
1028
1057
|
currentNodeId: job.workflowGraph.currentNodeId,
|
|
1029
|
-
stages:
|
|
1058
|
+
stages: projection.stages.map((node) => [node.id, node.status, node.agent, node.phase, node.label, node.flatIndex, node.outputName, node.structured, node.error]),
|
|
1030
1059
|
} : undefined,
|
|
1031
1060
|
preflight: expanded ? job.preflight : job.preflight ? formatWorkflowPreflightPlanSummary(job.preflight) : undefined,
|
|
1032
1061
|
preflightWarnings: expanded ? job.workflow?.preflightWarnings : job.workflow?.preflightWarnings?.length || undefined,
|
|
1033
1062
|
steps: job.steps?.map((step, index) => widgetStepRenderKey(step, index, expanded)),
|
|
1034
1063
|
nestedChildren: nestedRenderKey(job.nestedChildren, expanded),
|
|
1035
|
-
lane: laneRenderKey(job),
|
|
1064
|
+
lane: laneRenderKey(job, projection),
|
|
1036
1065
|
stepsTotal: job.stepsTotal,
|
|
1037
1066
|
runningSteps: job.runningSteps,
|
|
1038
1067
|
completedSteps: job.completedSteps,
|
|
@@ -1696,14 +1725,13 @@ function buildForegroundResultEntries(
|
|
|
1696
1725
|
return label.itemTitle === "Agent" ? withDuplicateForegroundLabels(entries, label.totalCount) : entries;
|
|
1697
1726
|
}
|
|
1698
1727
|
|
|
1699
|
-
function widgetStats(job: AsyncJobState, theme: Theme): string {
|
|
1728
|
+
function widgetStats(job: AsyncJobState, theme: Theme, projection = buildWorkflowWidgetProjection(job)): string {
|
|
1700
1729
|
const parts: string[] = [];
|
|
1701
|
-
const stageProgress =
|
|
1730
|
+
const { stageProgress } = projection;
|
|
1702
1731
|
const stepsTotal = stageProgress?.total ?? job.stepsTotal ?? (job.agents?.length ?? 1);
|
|
1703
1732
|
const isSingleChild = isSingleChildAsyncJob(job);
|
|
1704
1733
|
if (stageProgress) {
|
|
1705
|
-
const
|
|
1706
|
-
const currentStage = stageProgress.current !== undefined ? stages[stageProgress.current] : undefined;
|
|
1734
|
+
const currentStage = stageProgress.current !== undefined ? projection.stages[stageProgress.current] : undefined;
|
|
1707
1735
|
const focus = currentStage
|
|
1708
1736
|
? [compactTaskText(undefined, currentStage.label) ?? boundedLaneValue(currentStage.id), currentStage.agent ? boundedLaneValue(currentStage.agent) : "", workflowNodeStatusLabel(currentStage.status)].filter(Boolean)
|
|
1709
1737
|
: [];
|
|
@@ -2014,10 +2042,10 @@ function hostStepWidgetLines(job: AsyncJobState, theme: Theme, indent: string):
|
|
|
2014
2042
|
return lines;
|
|
2015
2043
|
}
|
|
2016
2044
|
|
|
2017
|
-
function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded: boolean, width: number, frame?: number): string[] {
|
|
2018
|
-
const steps =
|
|
2045
|
+
function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded: boolean, width: number, frame?: number, projection = buildWorkflowWidgetProjection(job)): string[] {
|
|
2046
|
+
const { steps } = projection;
|
|
2019
2047
|
if (!steps.length) {
|
|
2020
|
-
const lane = projectAsyncLane(job);
|
|
2048
|
+
const lane = projectAsyncLane(job, laneStepForJob(job, steps));
|
|
2021
2049
|
return [
|
|
2022
2050
|
...workflowPreflightLines(job, expanded),
|
|
2023
2051
|
...(lane ? formatLaneProjectionLines(lane, theme, " ") : []),
|
|
@@ -2032,12 +2060,11 @@ function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded
|
|
|
2032
2060
|
if (group) {
|
|
2033
2061
|
lines.push(...parallelWidgetGroupDetails(job, theme, group, expanded, width, frame, Boolean(job.activeParallelGroup)));
|
|
2034
2062
|
} else {
|
|
2035
|
-
const stageProgress =
|
|
2063
|
+
const { stageProgress, plannedKeys } = projection;
|
|
2036
2064
|
const total = job.mode === "chain"
|
|
2037
2065
|
? job.chainStepCount ?? job.stepsTotal ?? steps.length
|
|
2038
2066
|
: stageProgress?.total ?? job.stepsTotal ?? steps.length;
|
|
2039
2067
|
const stageTotal = stageProgress?.total ?? total;
|
|
2040
|
-
const plannedKeys = stageProgress ? new Set(workflowGraphStageNodes(job.workflowGraph).map((node) => node.id)) : undefined;
|
|
2041
2068
|
const extraStepCount = plannedKeys
|
|
2042
2069
|
? steps.filter((step) => step.workflowKey === undefined || !plannedKeys.has(step.workflowKey)).length
|
|
2043
2070
|
: total;
|
|
@@ -2059,9 +2086,9 @@ function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded
|
|
|
2059
2086
|
return lines;
|
|
2060
2087
|
}
|
|
2061
2088
|
|
|
2062
|
-
function buildSingleWidgetLines(job: AsyncJobState, theme: Theme, width: number, expanded: boolean, frame?: number): string[] {
|
|
2063
|
-
const stats = widgetStats(job, theme);
|
|
2064
|
-
const count = job.mode === "chain" ? job.chainStepCount :
|
|
2089
|
+
function buildSingleWidgetLines(job: AsyncJobState, theme: Theme, width: number, expanded: boolean, frame?: number, projection = buildWorkflowWidgetProjection(job)): string[] {
|
|
2090
|
+
const stats = widgetStats(job, theme, projection);
|
|
2091
|
+
const count = job.mode === "chain" ? job.chainStepCount : projection.stageProgress?.total ?? job.stepsTotal ?? job.agents?.length ?? job.steps?.length;
|
|
2065
2092
|
const mode = widgetJobName(job);
|
|
2066
2093
|
const title = isSingleChildAsyncJob(job)
|
|
2067
2094
|
? "async subagent"
|
|
@@ -2071,12 +2098,12 @@ function buildSingleWidgetLines(job: AsyncJobState, theme: Theme, width: number,
|
|
|
2071
2098
|
return [
|
|
2072
2099
|
`${theme.fg("toolTitle", themeBold(theme, title))} ${theme.fg("dim", "· background")}`,
|
|
2073
2100
|
...(collapseDetails ? [] : [summary]),
|
|
2074
|
-
...foregroundStyleWidgetDetails(job, theme, expanded, width, frame),
|
|
2101
|
+
...foregroundStyleWidgetDetails(job, theme, expanded, width, frame, projection),
|
|
2075
2102
|
].map((line) => truncLine(line, width));
|
|
2076
2103
|
}
|
|
2077
2104
|
|
|
2078
|
-
function compactSingleWidgetLines(job: AsyncJobState, theme: Theme, width: number, frame?: number): string[] {
|
|
2079
|
-
const fullLines = buildSingleWidgetLines(job, theme, width, false, frame);
|
|
2105
|
+
function compactSingleWidgetLines(job: AsyncJobState, theme: Theme, width: number, frame?: number, projection = buildWorkflowWidgetProjection(job)): string[] {
|
|
2106
|
+
const fullLines = buildSingleWidgetLines(job, theme, width, false, frame, projection);
|
|
2080
2107
|
if (fullLines.length <= 10 || !job.steps?.length || (job.mode !== "parallel" && !job.activeParallelGroup)) return fullLines;
|
|
2081
2108
|
|
|
2082
2109
|
const group = activeParallelWidgetGroup(job);
|
|
@@ -2232,11 +2259,11 @@ function progressiveHeaderLine(jobs: AsyncJobState[], theme: Theme, width: numbe
|
|
|
2232
2259
|
return truncLine(`${theme.fg(hasActive ? "accent" : "dim", glyph)} ${theme.fg(hasActive ? "accent" : "dim", "Async agents")} ${theme.fg("dim", "·")} ${theme.fg("dim", parts.join(", ") || `${jobs.length} total`)}`, width);
|
|
2233
2260
|
}
|
|
2234
2261
|
|
|
2235
|
-
function progressiveJobLine(job: AsyncJobState, theme: Theme, width: number, frame?: number): string {
|
|
2236
|
-
const stats = widgetStats(job, theme);
|
|
2262
|
+
function progressiveJobLine(job: AsyncJobState, theme: Theme, width: number, frame?: number, projection = buildWorkflowWidgetProjection(job)): string {
|
|
2263
|
+
const stats = widgetStats(job, theme, projection);
|
|
2237
2264
|
const activity = widgetActivity(job);
|
|
2238
2265
|
const status = job.status === "complete" ? "done" : job.status;
|
|
2239
|
-
const lane = projectAsyncLane(job);
|
|
2266
|
+
const lane = projectAsyncLane(job, laneStepForJob(job, projection.steps));
|
|
2240
2267
|
const laneSummary = lane
|
|
2241
2268
|
? [lane.label ?? lane.role, lane.phase ? `phase:${lane.phase}` : undefined, lane.output ? `out:${lane.output}` : undefined, lane.workspace ? `workspace:${lane.workspace}` : `ref:${lane.ref}`].filter(Boolean).join(" · ")
|
|
2242
2269
|
: "";
|
|
@@ -2264,7 +2291,7 @@ function progressiveHiddenLine(hiddenJobs: AsyncJobState[], theme: Theme, width:
|
|
|
2264
2291
|
return truncLine(theme.fg("dim", ` +${hiddenJobs.length} more${parts.length ? ` (${parts.join(", ")})` : ""}`), width);
|
|
2265
2292
|
}
|
|
2266
2293
|
|
|
2267
|
-
function buildProgressiveWidgetLines(jobs: AsyncJobState[], theme: Theme, width: number, lockedRows: number, previousKeys: string[], frame?: number): { lines: string[]; visibleJobKeys: string[] } {
|
|
2294
|
+
function buildProgressiveWidgetLines(jobs: AsyncJobState[], theme: Theme, width: number, lockedRows: number, previousKeys: string[], frame?: number, projectionFor: WorkflowWidgetProjectionLookup = workflowWidgetProjectionLookup()): { lines: string[]; visibleJobKeys: string[] } {
|
|
2268
2295
|
const rowCount = Math.max(1, lockedRows);
|
|
2269
2296
|
if (rowCount === 1) return { lines: buildSingleLineWidgetLines(jobs, theme, width, frame), visibleJobKeys: [] };
|
|
2270
2297
|
|
|
@@ -2283,7 +2310,7 @@ function buildProgressiveWidgetLines(jobs: AsyncJobState[], theme: Theme, width:
|
|
|
2283
2310
|
|
|
2284
2311
|
const lines = [
|
|
2285
2312
|
progressiveHeaderLine(jobs, theme, width, frame),
|
|
2286
|
-
...visibleJobs.map((job) => progressiveJobLine(job, theme, width, frame)),
|
|
2313
|
+
...visibleJobs.map((job) => progressiveJobLine(job, theme, width, frame, projectionFor(job))),
|
|
2287
2314
|
];
|
|
2288
2315
|
if (hiddenJobs.length > 0 && lines.length < rowCount) lines.push(progressiveHiddenLine(hiddenJobs, theme, width));
|
|
2289
2316
|
while (lines.length < rowCount) lines.push(" ");
|
|
@@ -2314,7 +2341,7 @@ function fitWidgetLineBudget(lines: string[], theme: Theme, width: number, expan
|
|
|
2314
2341
|
return [...lines.slice(0, visibleLines), truncLine(theme.fg("dim", hint), width)];
|
|
2315
2342
|
}
|
|
2316
2343
|
|
|
2317
|
-
function fitAdaptiveWidgetLines(jobs: AsyncJobState[], buildLines: () => string[], theme: Theme, width: number, expanded: boolean, frame?: number): string[] {
|
|
2344
|
+
function fitAdaptiveWidgetLines(jobs: AsyncJobState[], buildLines: () => string[], theme: Theme, width: number, expanded: boolean, frame?: number, projectionFor?: WorkflowWidgetProjectionLookup): string[] {
|
|
2318
2345
|
if (expanded) {
|
|
2319
2346
|
resetWidgetLayoutSession();
|
|
2320
2347
|
return fitWidgetLineBudget(buildLines(), theme, width, true);
|
|
@@ -2330,7 +2357,7 @@ function fitAdaptiveWidgetLines(jobs: AsyncJobState[], buildLines: () => string[
|
|
|
2330
2357
|
}
|
|
2331
2358
|
|
|
2332
2359
|
if (hasMatchingSession && widgetLayoutSession?.tier === "progressive" && widgetLayoutSession.lockedRows !== undefined) {
|
|
2333
|
-
const rendered = buildProgressiveWidgetLines(jobs, theme, width, widgetLayoutSession.lockedRows, widgetLayoutSession.visibleJobKeys, frame);
|
|
2360
|
+
const rendered = buildProgressiveWidgetLines(jobs, theme, width, widgetLayoutSession.lockedRows, widgetLayoutSession.visibleJobKeys, frame, projectionFor);
|
|
2334
2361
|
widgetLayoutSession.visibleJobKeys = rendered.visibleJobKeys;
|
|
2335
2362
|
return rendered.lines;
|
|
2336
2363
|
}
|
|
@@ -2340,7 +2367,7 @@ function fitAdaptiveWidgetLines(jobs: AsyncJobState[], buildLines: () => string[
|
|
|
2340
2367
|
widgetLayoutSession = { expanded, rows, columns, tier: "full", visibleJobKeys: [] };
|
|
2341
2368
|
return fitWidgetLineBudget(lines, theme, width, false);
|
|
2342
2369
|
}
|
|
2343
|
-
if (availableRows > 2 && jobs.length === 1 &&
|
|
2370
|
+
if (availableRows > 2 && jobs.length === 1 && projectionFor?.(jobs[0]!).stageProgress) {
|
|
2344
2371
|
widgetLayoutSession = { expanded, rows, columns, tier: "full", visibleJobKeys: [] };
|
|
2345
2372
|
return fitWidgetLineBudget(lines, theme, width, false);
|
|
2346
2373
|
}
|
|
@@ -2351,31 +2378,42 @@ function fitAdaptiveWidgetLines(jobs: AsyncJobState[], buildLines: () => string[
|
|
|
2351
2378
|
}
|
|
2352
2379
|
|
|
2353
2380
|
const lockedRows = Math.min(availableRows, collapsedWidgetLineBudget(rows));
|
|
2354
|
-
const rendered = buildProgressiveWidgetLines(jobs, theme, width, lockedRows, [], frame);
|
|
2381
|
+
const rendered = buildProgressiveWidgetLines(jobs, theme, width, lockedRows, [], frame, projectionFor);
|
|
2355
2382
|
widgetLayoutSession = { expanded, rows, columns, tier: "progressive", lockedRows, visibleJobKeys: rendered.visibleJobKeys };
|
|
2356
2383
|
return rendered.lines;
|
|
2357
2384
|
}
|
|
2358
2385
|
|
|
2359
|
-
function buildWidgetComponent(jobs: AsyncJobState[],
|
|
2386
|
+
function buildWidgetComponent(jobs: AsyncJobState[], isExpanded: () => boolean): (_tui: unknown, theme: Theme) => Component {
|
|
2360
2387
|
return (_tui, theme) => {
|
|
2361
2388
|
const container = new Container();
|
|
2389
|
+
let cachedRenderWidth: number | undefined;
|
|
2390
|
+
let cachedFrame: number | undefined;
|
|
2391
|
+
let cachedExpanded: boolean | undefined;
|
|
2392
|
+
let cachedLines: string[] | undefined;
|
|
2362
2393
|
container.render = (renderWidth: number): string[] => {
|
|
2363
|
-
const width = Math.max(0, renderWidth - 2);
|
|
2364
2394
|
const frame = Math.floor(Date.now() / WIDGET_ANIMATION_INTERVAL_MS);
|
|
2395
|
+
const expanded = isExpanded();
|
|
2396
|
+
if (cachedLines && cachedRenderWidth === renderWidth && cachedFrame === frame && cachedExpanded === expanded) return cachedLines;
|
|
2397
|
+
const width = Math.max(0, renderWidth - 2);
|
|
2398
|
+
const projectionFor = workflowWidgetProjectionLookup();
|
|
2365
2399
|
const buildLines = (): string[] => expanded
|
|
2366
|
-
?
|
|
2400
|
+
? buildWidgetLinesWithProjection(jobs, theme, width, true, frame, projectionFor)
|
|
2367
2401
|
: jobs.length === 1
|
|
2368
|
-
? compactSingleWidgetLines(jobs[0]!, theme, width, frame)
|
|
2369
|
-
:
|
|
2370
|
-
|
|
2402
|
+
? compactSingleWidgetLines(jobs[0]!, theme, width, frame, projectionFor(jobs[0]!))
|
|
2403
|
+
: buildWidgetLinesWithProjection(jobs, theme, width, false, frame, projectionFor);
|
|
2404
|
+
cachedRenderWidth = renderWidth;
|
|
2405
|
+
cachedFrame = frame;
|
|
2406
|
+
cachedExpanded = expanded;
|
|
2407
|
+
cachedLines = fitAdaptiveWidgetLines(jobs, buildLines, theme, width, expanded, frame, projectionFor).map((line) => paddedWidgetLine(line, renderWidth));
|
|
2408
|
+
return cachedLines;
|
|
2371
2409
|
};
|
|
2372
2410
|
return container;
|
|
2373
2411
|
};
|
|
2374
2412
|
}
|
|
2375
2413
|
|
|
2376
|
-
|
|
2414
|
+
function buildWidgetLinesWithProjection(jobs: AsyncJobState[], theme: Theme, width = getTermWidth(), expanded = false, frame?: number, projectionFor: WorkflowWidgetProjectionLookup = workflowWidgetProjectionLookup()): string[] {
|
|
2377
2415
|
if (jobs.length === 0) return [];
|
|
2378
|
-
if (jobs.length === 1) return buildSingleWidgetLines(jobs[0]!, theme, width, expanded, frame);
|
|
2416
|
+
if (jobs.length === 1) return buildSingleWidgetLines(jobs[0]!, theme, width, expanded, frame, projectionFor(jobs[0]!));
|
|
2379
2417
|
const running = jobs.filter((job) => job.status === "running");
|
|
2380
2418
|
const queued = jobs.filter((job) => job.status === "queued");
|
|
2381
2419
|
const finished = jobs.filter((job) => job.status !== "running" && job.status !== "queued");
|
|
@@ -2391,11 +2429,12 @@ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = ge
|
|
|
2391
2429
|
let queuedSummaryShown = false;
|
|
2392
2430
|
let slots = MAX_WIDGET_JOBS;
|
|
2393
2431
|
const appendJob = (job: AsyncJobState): void => {
|
|
2394
|
-
const
|
|
2432
|
+
const projection = projectionFor(job);
|
|
2433
|
+
const stats = widgetStats(job, theme, projection);
|
|
2395
2434
|
items.push([
|
|
2396
2435
|
`${widgetStatusGlyph(job, theme, frame)} ${themeBold(theme, widgetJobName(job))}${contextModeBadge(theme, job.context)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
|
|
2397
2436
|
` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
|
|
2398
|
-
...widgetLaneDetailLines(job, theme),
|
|
2437
|
+
...widgetLaneDetailLines(job, theme, projection),
|
|
2399
2438
|
...widgetParallelAgentDetails(job, theme, expanded, width, frame),
|
|
2400
2439
|
]);
|
|
2401
2440
|
};
|
|
@@ -2442,6 +2481,10 @@ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = ge
|
|
|
2442
2481
|
return lines;
|
|
2443
2482
|
}
|
|
2444
2483
|
|
|
2484
|
+
export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = getTermWidth(), expanded = false, frame?: number): string[] {
|
|
2485
|
+
return buildWidgetLinesWithProjection(jobs, theme, width, expanded, frame);
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2445
2488
|
/**
|
|
2446
2489
|
* Render the async jobs widget
|
|
2447
2490
|
*/
|
|
@@ -2456,7 +2499,7 @@ export function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void
|
|
|
2456
2499
|
ctx.ui.setWidget(WIDGET_KEY, encodeAsyncStatusSnapshotWidget(jobs));
|
|
2457
2500
|
return;
|
|
2458
2501
|
}
|
|
2459
|
-
ctx.ui.setWidget(WIDGET_KEY, buildWidgetComponent(jobs, ctx.ui.getToolsExpanded?.() ?? false));
|
|
2502
|
+
ctx.ui.setWidget(WIDGET_KEY, buildWidgetComponent(jobs, () => ctx.ui.getToolsExpanded?.() ?? false));
|
|
2460
2503
|
}
|
|
2461
2504
|
|
|
2462
2505
|
function renderSingleCompact(
|