pi-subagents 0.60.0 → 0.62.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 +55 -0
- package/docs/agents.md +7 -3
- 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 +13 -4
- package/docs/workflows.md +14 -13
- package/install.mjs +1 -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 +41 -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 +121 -63
- package/src/agents/agent-serializer.ts +3 -0
- package/src/agents/agents.ts +543 -223
- package/src/agents/runtime-agent-registry.ts +5 -1
- package/src/api/background-work.ts +7 -2
- package/src/api/external-runs.ts +67 -4
- package/src/api/preflight.ts +17 -8
- package/src/api/shared-types.ts +1 -0
- package/src/extension/index.ts +7 -4
- package/src/extension/public-execution.ts +48 -4
- package/src/extension/rpc.ts +62 -4
- package/src/extension/schemas.ts +17 -9
- package/src/extension/tool-description.ts +15 -19
- package/src/runs/background/active-async-capacity.ts +26 -7
- package/src/runs/background/async-execution.ts +98 -51
- package/src/runs/background/async-job-tracker.ts +62 -3
- package/src/runs/background/async-resume.ts +6 -3
- 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/process-terminal.ts +16 -0
- 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/scheduled-runs.ts +63 -6
- package/src/runs/background/steering.ts +4 -1
- package/src/runs/background/subagent-runner.ts +15 -9
- 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 +18 -18
- package/src/runs/foreground/execution.ts +75 -9
- package/src/runs/foreground/subagent-executor.ts +181 -60
- package/src/runs/shared/acceptance.ts +113 -27
- package/src/runs/shared/capability-ceiling.ts +1 -0
- package/src/runs/shared/dynamic-fanout.ts +1 -1
- package/src/runs/shared/host-step-status.ts +1 -0
- package/src/runs/shared/model-fallback.ts +61 -17
- package/src/runs/shared/parallel-utils.ts +2 -6
- package/src/runs/shared/permissions.ts +1 -1
- package/src/runs/shared/pi-args.ts +24 -12
- package/src/runs/shared/pi-spawn.ts +69 -35
- package/src/runs/shared/structured-output.ts +33 -6
- package/src/runs/shared/subagent-prompt-runtime.ts +20 -3
- package/src/runs/shared/task-intent.ts +17 -6
- package/src/runs/shared/tool-timeout.ts +1 -1
- package/src/runs/shared/workflow-graph.ts +3 -2
- package/src/shared/atomic-json.ts +3 -1
- package/src/shared/fork-context.ts +0 -12
- package/src/shared/fork-session-cwd.ts +27 -0
- package/src/shared/launch-contract.ts +3 -0
- package/src/shared/types.ts +43 -4
- 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/slash/subagents-admin.ts +2 -0
- 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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { stableJsonDigest } from "./launch-contract.ts";
|
|
2
|
+
import type { WorkflowResourceProvenanceV1 } from "./types.ts";
|
|
2
3
|
|
|
3
4
|
export interface WorkflowChildPermitInput {
|
|
4
5
|
issuerPackage: string;
|
|
@@ -114,3 +115,93 @@ export function workflowChildPermitConsumed(permit: WorkflowChildPermit): boolea
|
|
|
114
115
|
const state = records.get(permit as object)?.state;
|
|
115
116
|
return state === "claimed" || state === "consumed";
|
|
116
117
|
}
|
|
118
|
+
|
|
119
|
+
export interface WorkflowResourceHostAuthority {
|
|
120
|
+
keys: readonly string[];
|
|
121
|
+
commands: readonly string[];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface WorkflowResourceAuthority {
|
|
125
|
+
host?: WorkflowResourceHostAuthority;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface WorkflowResourcePermitInput {
|
|
129
|
+
resourceName: string;
|
|
130
|
+
resourceVersion: number;
|
|
131
|
+
resourceId: string;
|
|
132
|
+
scriptDigest: string;
|
|
133
|
+
authority: WorkflowResourceAuthority;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface WorkflowResourcePermit {
|
|
137
|
+
readonly __workflowResourcePermit: unique symbol;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
interface WorkflowResourcePermitRecord {
|
|
141
|
+
resourceName: string;
|
|
142
|
+
resourceVersion: number;
|
|
143
|
+
resourceId: string;
|
|
144
|
+
scriptDigest: string;
|
|
145
|
+
authority: WorkflowResourceAuthority;
|
|
146
|
+
provenance: WorkflowResourceProvenanceV1;
|
|
147
|
+
state: "available" | "consumed";
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const resourceRecords = new WeakMap<object, WorkflowResourcePermitRecord>();
|
|
151
|
+
|
|
152
|
+
function cloneWorkflowResourceAuthority(authority: WorkflowResourceAuthority): WorkflowResourceAuthority {
|
|
153
|
+
if (!authority || typeof authority !== "object" || Array.isArray(authority)) throw new Error("Workflow resource authority must be an object.");
|
|
154
|
+
if (authority.host === undefined) return Object.freeze({});
|
|
155
|
+
if (!authority.host || typeof authority.host !== "object" || Array.isArray(authority.host)) throw new Error("Workflow resource host authority must be an object.");
|
|
156
|
+
const { keys, commands } = authority.host;
|
|
157
|
+
if (!Array.isArray(keys) || keys.some((key) => typeof key !== "string" || !key.trim())) throw new Error("Workflow resource host authority keys must be non-empty strings.");
|
|
158
|
+
if (!Array.isArray(commands) || commands.some((command) => typeof command !== "string" || !command.trim())) throw new Error("Workflow resource host authority commands must be non-empty strings.");
|
|
159
|
+
return Object.freeze({ host: Object.freeze({ keys: Object.freeze([...keys]), commands: Object.freeze([...commands]) }) });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Package-internal permit for a workflow resource resolved by the extension. */
|
|
163
|
+
export function createWorkflowResourcePermit(input: WorkflowResourcePermitInput): WorkflowResourcePermit {
|
|
164
|
+
const resourceName = required(input.resourceName, "resourceName");
|
|
165
|
+
const resourceId = required(input.resourceId, "resourceId");
|
|
166
|
+
const scriptDigest = required(input.scriptDigest, "scriptDigest");
|
|
167
|
+
if (!Number.isInteger(input.resourceVersion) || input.resourceVersion < 1) throw new Error("resourceVersion must be a positive integer.");
|
|
168
|
+
const authority = cloneWorkflowResourceAuthority(input.authority);
|
|
169
|
+
const permit = Object.freeze(Object.create(null)) as WorkflowResourcePermit;
|
|
170
|
+
resourceRecords.set(permit as object, {
|
|
171
|
+
resourceName,
|
|
172
|
+
resourceVersion: input.resourceVersion,
|
|
173
|
+
resourceId,
|
|
174
|
+
scriptDigest,
|
|
175
|
+
authority,
|
|
176
|
+
provenance: Object.freeze({
|
|
177
|
+
kind: "workflow",
|
|
178
|
+
name: resourceName,
|
|
179
|
+
version: input.resourceVersion,
|
|
180
|
+
invocation: "named",
|
|
181
|
+
expansion: "resolved",
|
|
182
|
+
id: resourceId,
|
|
183
|
+
}),
|
|
184
|
+
state: "available",
|
|
185
|
+
});
|
|
186
|
+
return permit;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function consumeWorkflowResourcePermit(permit: WorkflowResourcePermit, script: string): { provenance: WorkflowResourceProvenanceV1; authority: WorkflowResourceAuthority } | string {
|
|
190
|
+
const record = resourceRecords.get(permit as object);
|
|
191
|
+
if (!record) return "Workflow resource permit is invalid.";
|
|
192
|
+
if (record.state !== "available") return "Workflow resource permit is already consumed.";
|
|
193
|
+
if (stableJsonDigest(script) !== record.scriptDigest) return "Workflow resource permit does not match the resolved workflow script.";
|
|
194
|
+
record.state = "consumed";
|
|
195
|
+
return { provenance: record.provenance, authority: record.authority };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Validate a host call against the authority attached to a consumed resource. */
|
|
199
|
+
export function authorizeWorkflowResourceHost(permit: WorkflowResourcePermit, key: string, command: string): string | undefined {
|
|
200
|
+
const record = resourceRecords.get(permit as object);
|
|
201
|
+
if (!record || record.state !== "consumed") return "Workflow resource authority is unavailable.";
|
|
202
|
+
const host = record.authority.host;
|
|
203
|
+
if (!host) return "runs.host is not allowed for this workflow resource.";
|
|
204
|
+
if (!host.keys.includes(key)) return `runs.host('${key}') is not allowed for workflow resource '${record.resourceName}'.`;
|
|
205
|
+
if (!host.commands.includes(command.trim())) return `The command for runs.host('${key}') is not allowed for workflow resource '${record.resourceName}'.`;
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
type SubagentDelegationInvalidResponse,
|
|
8
8
|
type SubagentDelegationRequest,
|
|
9
9
|
type SubagentDelegationResponse,
|
|
10
|
+
type SubagentDelegationUpdate,
|
|
10
11
|
} from "../api/delegation.ts";
|
|
11
12
|
import { parseSubagentDelegationRequest } from "./delegation-request.ts";
|
|
12
13
|
import {
|
|
@@ -66,6 +67,37 @@ function validId(value: unknown): value is string {
|
|
|
66
67
|
return typeof value === "string" && value.trim().length > 0 && value.length <= 256 && !/[\r\n]/.test(value);
|
|
67
68
|
}
|
|
68
69
|
|
|
70
|
+
function sameStringArray(left: string[] | undefined, right: string[] | undefined): boolean {
|
|
71
|
+
if (left === right) return true;
|
|
72
|
+
if (!left || !right || left.length !== right.length) return false;
|
|
73
|
+
return left.every((value, index) => value === right[index]);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function sameRecentTools(
|
|
77
|
+
left: Array<{ tool: string; args: string }> | undefined,
|
|
78
|
+
right: Array<{ tool: string; args: string }> | undefined,
|
|
79
|
+
): boolean {
|
|
80
|
+
if (left === right) return true;
|
|
81
|
+
if (!left || !right || left.length !== right.length) return false;
|
|
82
|
+
return left.every((tool, index) => tool.tool === right[index]?.tool && tool.args === right[index]?.args);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Duration is a heartbeat clock, not delegation-visible progress; terminal usage remains authoritative. */
|
|
86
|
+
function sameStructuredDelegationUpdateProgress(left: SubagentDelegationUpdate, right: SubagentDelegationUpdate): boolean {
|
|
87
|
+
return left.requestId === right.requestId
|
|
88
|
+
&& left.ownerRunId === right.ownerRunId
|
|
89
|
+
&& left.nodeId === right.nodeId
|
|
90
|
+
&& left.runId === right.runId
|
|
91
|
+
&& left.currentTool === right.currentTool
|
|
92
|
+
&& left.currentToolArgs === right.currentToolArgs
|
|
93
|
+
&& left.recentOutput === right.recentOutput
|
|
94
|
+
&& sameStringArray(left.recentOutputLines, right.recentOutputLines)
|
|
95
|
+
&& sameRecentTools(left.recentTools, right.recentTools)
|
|
96
|
+
&& left.model === right.model
|
|
97
|
+
&& left.toolCount === right.toolCount
|
|
98
|
+
&& left.tokens === right.tokens;
|
|
99
|
+
}
|
|
100
|
+
|
|
69
101
|
export function registerPromptTemplateDelegationBridge<Ctx extends { cwd?: string }>(
|
|
70
102
|
options: PromptTemplateBridgeOptions<Ctx>,
|
|
71
103
|
): {
|
|
@@ -298,6 +330,7 @@ export function registerPromptTemplateDelegationBridge<Ctx extends { cwd?: strin
|
|
|
298
330
|
const executeRequest = structuredRequest && options.executeStructured
|
|
299
331
|
? options.executeStructured
|
|
300
332
|
: options.execute;
|
|
333
|
+
let lastStructuredUpdate: SubagentDelegationUpdate | undefined;
|
|
301
334
|
const result = await executeRequest(
|
|
302
335
|
requestId,
|
|
303
336
|
params,
|
|
@@ -307,7 +340,10 @@ export function registerPromptTemplateDelegationBridge<Ctx extends { cwd?: strin
|
|
|
307
340
|
if (key ? !ownsAttempt(key, controller) : !ownsLegacyRequest(requestId, controller)) return;
|
|
308
341
|
if (structuredRequest) {
|
|
309
342
|
const payload = toSubagentDelegationUpdate(structuredRequest, update);
|
|
310
|
-
if (payload
|
|
343
|
+
if (payload && (!lastStructuredUpdate || !sameStructuredDelegationUpdateProgress(lastStructuredUpdate, payload))) {
|
|
344
|
+
lastStructuredUpdate = payload;
|
|
345
|
+
options.events.emit(SUBAGENT_DELEGATION_UPDATE_EVENT, payload);
|
|
346
|
+
}
|
|
311
347
|
return;
|
|
312
348
|
}
|
|
313
349
|
const payload = toDelegationUpdate(requestId, update);
|
|
@@ -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 !== "
|
|
420
|
+
if (message.role !== "toolResult" || (message.toolName !== "subagent" && message.toolName !== "bg_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
|
|
|
@@ -97,6 +97,7 @@ function buildBuiltinBase(agent: AgentConfig): BuiltinAgentOverrideBase {
|
|
|
97
97
|
systemPrompt: agent.systemPrompt,
|
|
98
98
|
...(agent.skills !== undefined ? { skills: [...agent.skills] } : {}),
|
|
99
99
|
...(agent.tools !== undefined ? { tools: [...agent.tools] } : {}),
|
|
100
|
+
...(agent.excludeTools !== undefined ? { excludeTools: [...agent.excludeTools] } : {}),
|
|
100
101
|
...(agent.mcpDirectTools !== undefined ? { mcpDirectTools: [...agent.mcpDirectTools] } : {}),
|
|
101
102
|
...(agent.subagentOnlyExtensions !== undefined ? { subagentOnlyExtensions: [...agent.subagentOnlyExtensions] } : {}),
|
|
102
103
|
...(agent.mutationTools !== undefined ? { mutationTools: [...agent.mutationTools] } : {}),
|
|
@@ -188,6 +189,7 @@ function metadataFor(agent: AgentConfig): string {
|
|
|
188
189
|
if (agent.fallbackModels?.length) lines.push(`Fallback models: ${agent.fallbackModels.join(", ")}`);
|
|
189
190
|
if (agent.thinking !== undefined) lines.push(`Thinking: ${agent.thinking === false ? "off" : agent.thinking}`);
|
|
190
191
|
if (tools.length) lines.push(`Tools: ${tools.join(", ")}`);
|
|
192
|
+
if (agent.excludeTools?.length) lines.push(`Excluded tools: ${agent.excludeTools.join(", ")}`);
|
|
191
193
|
if (agent.skills?.length) lines.push(`Skills: ${agent.skills.join(", ")}`);
|
|
192
194
|
lines.push(`System prompt mode: ${agent.systemPromptMode}`);
|
|
193
195
|
lines.push(`Inherit project context: ${agent.inheritProjectContext ? "true" : "false"}`);
|
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(
|