pi-subagents 0.52.0 → 0.53.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 +58 -0
- package/README.md +4 -0
- package/docs/configuration.md +11 -1
- package/docs/extension-api.md +3 -1
- package/docs/workflows.md +2 -0
- package/package.json +2 -1
- package/prompts/council.md +48 -0
- package/skills/council-mode/SKILL.md +230 -0
- package/skills/pi-subagents/SKILL.md +2 -0
- package/skills/pi-subagents/references/constraints-and-recipes.md +1 -0
- package/skills/pi-subagents/references/execution-controls.md +11 -0
- package/skills/pi-subagents/references/multi-lane-orchestration.md +39 -0
- package/src/agents/agent-management.ts +22 -3
- package/src/agents/agent-serializer.ts +2 -0
- package/src/agents/agents.ts +29 -13
- package/src/agents/builtin-names.ts +9 -0
- package/src/agents/runtime-agent-registry.ts +418 -0
- package/src/api/agents.ts +7 -0
- package/src/api/external-job-provider.ts +3 -2
- package/src/api/preflight.ts +1 -1
- package/src/extension/config.ts +3 -0
- package/src/extension/doctor.ts +1 -0
- package/src/extension/index.ts +17 -2
- package/src/extension/rpc.ts +41 -1
- package/src/extension/schemas.ts +7 -4
- package/src/extension/tool-description.ts +2 -2
- package/src/runs/background/async-execution.ts +2 -1
- package/src/runs/background/async-job-tracker.ts +4 -3
- package/src/runs/background/async-resume.ts +2 -1
- package/src/runs/background/async-status-snapshot.ts +14 -5
- package/src/runs/background/auto-drain.ts +1 -0
- package/src/runs/background/result-watcher.ts +8 -0
- package/src/runs/background/subagent-runner.ts +7 -4
- package/src/runs/background/subagent-wait.ts +9 -5
- package/src/runs/background/terminal-run-index.ts +15 -6
- package/src/runs/background/wait-tool.ts +1 -0
- package/src/runs/foreground/execution.ts +5 -1
- package/src/runs/foreground/subagent-executor.ts +182 -46
- package/src/runs/foreground/workflow-detach-reconcile.ts +83 -15
- package/src/runs/shared/acceptance.ts +44 -1
- package/src/runs/shared/model-exclusions.ts +242 -0
- package/src/runs/shared/model-fallback.ts +55 -2
- package/src/runs/shared/subagent-control.ts +25 -3
- package/src/shared/fork-context.ts +17 -1
- package/src/shared/model-info.ts +20 -0
- package/src/shared/settings.ts +2 -2
- package/src/shared/types.ts +35 -0
- package/src/slash/slash-commands.ts +20 -6
- package/src/slash/slash-live-state.ts +3 -3
- package/src/tui/fleet-status.ts +86 -1
- package/src/tui/fleet.ts +55 -2
- package/src/tui/render.ts +73 -3
- package/src/workflows/scripted-workflow.ts +100 -12
- package/src/workflows/workflow-receipt.ts +140 -0
package/src/shared/settings.ts
CHANGED
|
@@ -282,7 +282,7 @@ export function resolveStepBehavior(
|
|
|
282
282
|
}
|
|
283
283
|
}
|
|
284
284
|
|
|
285
|
-
const outputMode = stepOverrides.outputMode ?? "inline";
|
|
285
|
+
const outputMode = stepOverrides.outputMode ?? agentConfig.outputMode ?? "inline";
|
|
286
286
|
const model = stepOverrides.model ?? agentConfig.model;
|
|
287
287
|
return { output, outputMode, reads, progress, skills, model };
|
|
288
288
|
}
|
|
@@ -465,7 +465,7 @@ export function resolveParallelBehaviors(
|
|
|
465
465
|
}
|
|
466
466
|
}
|
|
467
467
|
|
|
468
|
-
const outputMode = task.outputMode ?? "inline";
|
|
468
|
+
const outputMode = task.outputMode ?? config.outputMode ?? "inline";
|
|
469
469
|
const model = task.model ?? config.model;
|
|
470
470
|
return { output, outputMode, reads, progress, skills, model };
|
|
471
471
|
});
|
package/src/shared/types.ts
CHANGED
|
@@ -71,6 +71,29 @@ export interface WorkflowGraphSnapshot {
|
|
|
71
71
|
currentNodeId?: string;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
export type WorkflowReceiptState = "complete" | "failed" | "paused" | "stopped";
|
|
75
|
+
|
|
76
|
+
type WorkflowReceiptEntryResumability =
|
|
77
|
+
| { latestRunId: string; resumability: { state: "resumable" } }
|
|
78
|
+
| { latestRunId?: string; resumability: { state: "not-resumable"; reason: string } };
|
|
79
|
+
|
|
80
|
+
export type WorkflowReceiptEntry = WorkflowReceiptEntryResumability & {
|
|
81
|
+
key: string;
|
|
82
|
+
agent?: string;
|
|
83
|
+
requestedContext?: "fresh" | "fork";
|
|
84
|
+
resolvedContext?: "fresh" | "fork" | "mixed";
|
|
85
|
+
outputReference?: string;
|
|
86
|
+
continuation: { runIds: string[] };
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export interface WorkflowReceipt {
|
|
90
|
+
version: 1;
|
|
91
|
+
workflowRunId: string;
|
|
92
|
+
state: WorkflowReceiptState;
|
|
93
|
+
createdAt: number;
|
|
94
|
+
entries: Record<string, WorkflowReceiptEntry>;
|
|
95
|
+
}
|
|
96
|
+
|
|
74
97
|
export interface SavedOutputReference {
|
|
75
98
|
path: string;
|
|
76
99
|
bytes: number;
|
|
@@ -161,6 +184,7 @@ export interface ControlConfig {
|
|
|
161
184
|
export interface ResolvedControlConfig {
|
|
162
185
|
enabled: boolean;
|
|
163
186
|
needsAttentionAfterMs: number;
|
|
187
|
+
needsAttentionAfterMsIsExplicit?: boolean;
|
|
164
188
|
activeNoticeAfterMs: number;
|
|
165
189
|
activeNoticeAfterTurns?: number;
|
|
166
190
|
activeNoticeAfterTokens?: number;
|
|
@@ -529,6 +553,8 @@ export interface SteeringRecoveryDescriptor {
|
|
|
529
553
|
structuredOutputSchema?: JsonSchemaObject;
|
|
530
554
|
acceptance?: AcceptanceInput;
|
|
531
555
|
controlConfig?: ResolvedControlConfig;
|
|
556
|
+
/** Resolved launch context for this async child. */
|
|
557
|
+
context?: "fresh" | "fork";
|
|
532
558
|
/** Raw per-run bridge override. Omitted descriptors continue to use global config. */
|
|
533
559
|
intercomBridge?: IntercomBridgeConfig;
|
|
534
560
|
absoluteDeadlineAt?: number;
|
|
@@ -1099,6 +1125,8 @@ export interface Details {
|
|
|
1099
1125
|
}>;
|
|
1100
1126
|
emits: unknown[];
|
|
1101
1127
|
console: Array<{ level: "log" | "info" | "warn" | "error"; text: string }>;
|
|
1128
|
+
/** Terminal keyed child receipt. Async workflows also persist it beside status.json. */
|
|
1129
|
+
receipt?: WorkflowReceipt;
|
|
1102
1130
|
};
|
|
1103
1131
|
chatProgress?: {
|
|
1104
1132
|
mode: "off" | "live-card";
|
|
@@ -2026,6 +2054,13 @@ export interface ExtensionConfig {
|
|
|
2026
2054
|
/** Artifact cleanup retention. Set cleanupDays to 0 to disable cleanup. */
|
|
2027
2055
|
artifactConfig?: Pick<ArtifactConfig, "cleanupDays">;
|
|
2028
2056
|
intercomBridge?: IntercomBridgeConfig;
|
|
2057
|
+
/** Control how slow result-index scans are logged. Defaults to \"all\".
|
|
2058
|
+
* - \"all\": log every slow scan, including scans that find nothing.
|
|
2059
|
+
* - \"activity\": log only slow scans that found or scheduled work. Silences
|
|
2060
|
+
* the periodic healthy rescan that inspects zero files while no async runs
|
|
2061
|
+
* are pending, which otherwise spams the session transcript.
|
|
2062
|
+
* - \"off\": never log slow result-index scans. */
|
|
2063
|
+
resultScanLogging?: "all" | "activity" | "off";
|
|
2029
2064
|
proactiveSkillSubagents?: ProactiveSkillSubagentsConfig | false;
|
|
2030
2065
|
scheduledRuns?: ScheduledRunsConfig;
|
|
2031
2066
|
/** Durable mission behavior. Missions are automatic by default; set enabled:false to disable auto-create. Explicit mission actions/fields still work. */
|
|
@@ -3,7 +3,8 @@ 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 { BUILTIN_AGENT_NAMES, discoverAgents, findBlockingAgentDiagnostic, resolveAgentName } from "../agents/agents.ts";
|
|
6
|
+
import { BUILTIN_AGENT_NAMES, discoverAgents, discoverAgentsAll, findBlockingAgentDiagnostic, resolveAgentName, type AgentConfig, type AgentDiscoveryDiagnostic, type AgentScope } from "../agents/agents.ts";
|
|
7
|
+
import { listRuntimeAgentConfigs, mergeRuntimeAgents } from "../agents/runtime-agent-registry.ts";
|
|
7
8
|
import { resolveExistingReadPaths } from "../shared/settings.ts";
|
|
8
9
|
import {
|
|
9
10
|
DEFAULT_PROVIDER_MODELS_MAX_AGE_DAYS,
|
|
@@ -107,9 +108,22 @@ const extractExecutionFlags = (rawArgs: string): { args: string; bg: boolean; fo
|
|
|
107
108
|
return { args, bg, fork };
|
|
108
109
|
};
|
|
109
110
|
|
|
110
|
-
|
|
111
|
+
function discoverSlashAgents(pi: ExtensionAPI, cwd: string, scope: AgentScope): { agents: AgentConfig[]; agentDiagnostics?: AgentDiscoveryDiagnostic[] } {
|
|
112
|
+
const discovered = discoverAgents(cwd, scope);
|
|
113
|
+
if (listRuntimeAgentConfigs(pi).length === 0) return discovered;
|
|
114
|
+
const all = discoverAgentsAll(cwd);
|
|
115
|
+
const configuredAgents: AgentConfig[] = [
|
|
116
|
+
...all.builtin,
|
|
117
|
+
...all.package,
|
|
118
|
+
...(scope !== "project" ? all.user : []),
|
|
119
|
+
...(scope !== "user" ? all.project : []),
|
|
120
|
+
];
|
|
121
|
+
return mergeRuntimeAgents(pi, discovered, configuredAgents);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const makeAgentCompletions = (pi: ExtensionAPI, state: SubagentState) => (prefix: string) => {
|
|
111
125
|
if (!state.baseCwd || prefix.includes(" ")) return null;
|
|
112
|
-
return
|
|
126
|
+
return discoverSlashAgents(pi, state.baseCwd, "both").agents
|
|
113
127
|
.filter((agent) => agent.name.startsWith(prefix))
|
|
114
128
|
.map((agent) => ({ value: agent.name, label: agent.name }));
|
|
115
129
|
};
|
|
@@ -662,7 +676,7 @@ export function registerSlashCommands(
|
|
|
662
676
|
|
|
663
677
|
pi.registerCommand("run", {
|
|
664
678
|
description: "Run one subagent through workflowScript: /run agent[output=file] [task] [--bg] [--fork]",
|
|
665
|
-
getArgumentCompletions: makeAgentCompletions(state),
|
|
679
|
+
getArgumentCompletions: makeAgentCompletions(pi, state),
|
|
666
680
|
handler: async (args, ctx) => {
|
|
667
681
|
const { args: cleanedArgs, bg, fork } = extractExecutionFlags(args);
|
|
668
682
|
const input = cleanedArgs.trim();
|
|
@@ -672,7 +686,7 @@ export function registerSlashCommands(
|
|
|
672
686
|
const task = firstSpace === -1 ? "" : input.slice(firstSpace + 1).trim();
|
|
673
687
|
|
|
674
688
|
if (!state.baseCwd) { ctx.ui.notify("Subagent session cwd is not initialized yet", "error"); return; }
|
|
675
|
-
const discovered =
|
|
689
|
+
const discovered = discoverSlashAgents(pi, state.baseCwd, "both");
|
|
676
690
|
const resolvedAgent = resolveAgentName(agentName, discovered.agents);
|
|
677
691
|
const candidates = resolvedAgent.error
|
|
678
692
|
? discovered.agents.filter((agent) => resolveAgentName(agentName, [agent]).agent)
|
|
@@ -746,7 +760,7 @@ export function registerSlashCommands(
|
|
|
746
760
|
|
|
747
761
|
pi.registerCommand("subagents-refine", {
|
|
748
762
|
description: "Generate a bounded project-local refinement overlay for one subagent",
|
|
749
|
-
getArgumentCompletions: makeAgentCompletions(state),
|
|
763
|
+
getArgumentCompletions: makeAgentCompletions(pi, state),
|
|
750
764
|
handler: async (args, ctx) => {
|
|
751
765
|
const parts = args.trim().split(/\s+/).filter(Boolean);
|
|
752
766
|
if (parts.length !== 1) {
|
|
@@ -82,7 +82,7 @@ function buildParallelInitialResult(params: SubagentParamsLike): AgentToolResult
|
|
|
82
82
|
details: {
|
|
83
83
|
mode: "parallel",
|
|
84
84
|
...(params.async ? { background: true } : {}),
|
|
85
|
-
...(params.context ? { context: params.context } : {}),
|
|
85
|
+
...(params.context === "fresh" || params.context === "fork" ? { context: params.context } : {}),
|
|
86
86
|
results: tasks.map((task, index) => createPlaceholderResult(task.agent, task.task, "running", index)),
|
|
87
87
|
progress: tasks.map((task, index) => ({
|
|
88
88
|
index,
|
|
@@ -138,7 +138,7 @@ function buildChainInitialResult(params: SubagentParamsLike): AgentToolResult<De
|
|
|
138
138
|
details: {
|
|
139
139
|
mode: "chain",
|
|
140
140
|
...(params.async ? { background: true } : {}),
|
|
141
|
-
...(params.context ? { context: params.context } : {}),
|
|
141
|
+
...(params.context === "fresh" || params.context === "fork" ? { context: params.context } : {}),
|
|
142
142
|
results,
|
|
143
143
|
progress: results.map((result, index) => ({
|
|
144
144
|
index,
|
|
@@ -167,7 +167,7 @@ function buildSingleInitialResult(params: SubagentParamsLike): AgentToolResult<D
|
|
|
167
167
|
details: {
|
|
168
168
|
mode: "single",
|
|
169
169
|
...(params.async ? { background: true } : {}),
|
|
170
|
-
...(params.context ? { context: params.context } : {}),
|
|
170
|
+
...(params.context === "fresh" || params.context === "fork" ? { context: params.context } : {}),
|
|
171
171
|
results: [createPlaceholderResult(agent, task, "running", 0)],
|
|
172
172
|
progress: [{
|
|
173
173
|
index: 0,
|
package/src/tui/fleet-status.ts
CHANGED
|
@@ -27,6 +27,17 @@ type FleetStatusEntry = {
|
|
|
27
27
|
state: string;
|
|
28
28
|
external?: true;
|
|
29
29
|
nestedChildren?: NestedRunSummary[];
|
|
30
|
+
workflowRows?: FleetWorkflowRow[];
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
type FleetWorkflowRow = {
|
|
34
|
+
name: string;
|
|
35
|
+
state: AsyncJobStep["status"];
|
|
36
|
+
modelThinking?: string;
|
|
37
|
+
activity?: string;
|
|
38
|
+
startedAt?: number;
|
|
39
|
+
tokens?: number;
|
|
40
|
+
overflow?: number;
|
|
30
41
|
};
|
|
31
42
|
|
|
32
43
|
type FleetNestedRow = {
|
|
@@ -42,6 +53,7 @@ type FleetNestedRow = {
|
|
|
42
53
|
type FleetTreeRow =
|
|
43
54
|
| { kind: "owner"; entry: FleetStatusEntry }
|
|
44
55
|
| { kind: "child"; entry: FleetStatusEntry; last: boolean }
|
|
56
|
+
| { kind: "workflow"; ownerKey: string; row: FleetWorkflowRow; last: boolean }
|
|
45
57
|
| { kind: "nested"; ownerKey: string; row: FleetNestedRow; last: boolean };
|
|
46
58
|
|
|
47
59
|
export interface FleetStatusOptions {
|
|
@@ -92,6 +104,51 @@ function nestedActivity(node: NestedRunSummary | NestedStepSummary): string | un
|
|
|
92
104
|
return undefined;
|
|
93
105
|
}
|
|
94
106
|
|
|
107
|
+
function workflowStepActivity(step: AsyncJobStep): string | undefined {
|
|
108
|
+
if (step.currentTool) return `tool ${step.currentTool}`;
|
|
109
|
+
if (step.currentPath) return step.currentPath.split(/[\\/]/).at(-1);
|
|
110
|
+
if (step.activityState === "needs_attention") return "needs attention";
|
|
111
|
+
if (step.activityState === "active_long_running") return "long-running";
|
|
112
|
+
if (step.turnCount !== undefined) return `${step.turnCount} turns`;
|
|
113
|
+
if (step.toolCount !== undefined) return `${step.toolCount} tools`;
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function workflowStepName(step: AsyncJobStep, index: number): string {
|
|
118
|
+
const key = step.workflowKey ?? `step ${index + 1}`;
|
|
119
|
+
const label = step.label && step.label !== key ? ` · ${step.label}` : "";
|
|
120
|
+
const phase = step.phase ? `${step.phase}: ` : "";
|
|
121
|
+
return `${phase}${key}${label} (${step.agent})`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function workflowFleetRows(steps: AsyncJobStep[] | undefined): FleetWorkflowRow[] {
|
|
125
|
+
return (steps ?? []).map((step, index) => {
|
|
126
|
+
const modelThinking = formatModelThinking(step.model, step.thinking) || undefined;
|
|
127
|
+
const activity = workflowStepActivity(step);
|
|
128
|
+
return {
|
|
129
|
+
name: workflowStepName(step, index),
|
|
130
|
+
state: step.status,
|
|
131
|
+
...(modelThinking ? { modelThinking } : {}),
|
|
132
|
+
...(activity ? { activity } : {}),
|
|
133
|
+
...(step.startedAt !== undefined ? { startedAt: step.startedAt } : {}),
|
|
134
|
+
...(step.tokens?.total !== undefined ? { tokens: step.tokens.total } : {}),
|
|
135
|
+
};
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function visibleWorkflowRows(rows: FleetWorkflowRow[] | undefined, visibleLimit: number): FleetWorkflowRow[] {
|
|
140
|
+
if (!rows?.length) return [];
|
|
141
|
+
if (rows.length <= visibleLimit) return rows;
|
|
142
|
+
const selected = new Set<number>();
|
|
143
|
+
for (const [index, row] of rows.entries()) {
|
|
144
|
+
if (row.state !== "complete" && row.state !== "completed") selected.add(index);
|
|
145
|
+
if (selected.size >= visibleLimit) break;
|
|
146
|
+
}
|
|
147
|
+
for (let index = rows.length - 1; index >= 0 && selected.size < visibleLimit; index--) selected.add(index);
|
|
148
|
+
const visible = [...selected].sort((left, right) => left - right).map((index) => rows[index]!);
|
|
149
|
+
return [{ name: `… +${rows.length - visible.length} hidden workflow steps`, state: "complete", overflow: rows.length - visible.length }, ...visible];
|
|
150
|
+
}
|
|
151
|
+
|
|
95
152
|
function nestedStatusGlyph(state: FleetNestedRow["state"], theme: Theme): string {
|
|
96
153
|
if (state === "running") return theme.fg("accent", "●");
|
|
97
154
|
if (state === "queued" || state === "pending") return theme.fg("muted", "◦");
|
|
@@ -194,9 +251,10 @@ function fleetTreeRows(entries: FleetStatusEntry[]): FleetTreeRow[] {
|
|
|
194
251
|
if (entry.parentKey && entryKeys.has(entry.parentKey)) continue;
|
|
195
252
|
rows.push({ kind: "owner", entry });
|
|
196
253
|
const attached = childrenByParent.get(entry.key) ?? [];
|
|
254
|
+
const workflowRows = visibleWorkflowRows(entry.workflowRows, attached.length > 0 ? 2 : 4);
|
|
197
255
|
for (const [index, child] of attached.entries()) {
|
|
198
256
|
const nested = nestedFleetRows(child.nestedChildren, 3);
|
|
199
|
-
const laterRows = index < attached.length - 1 || Boolean(entry.nestedChildren?.length);
|
|
257
|
+
const laterRows = index < attached.length - 1 || workflowRows.length > 0 || Boolean(entry.nestedChildren?.length);
|
|
200
258
|
rows.push({ kind: "child", entry: child, last: !laterRows && nested.length === 0 });
|
|
201
259
|
for (const [nestedIndex, row] of nested.entries()) rows.push({
|
|
202
260
|
kind: "nested",
|
|
@@ -205,6 +263,7 @@ function fleetTreeRows(entries: FleetStatusEntry[]): FleetTreeRow[] {
|
|
|
205
263
|
last: nestedIndex === nested.length - 1 && !laterRows,
|
|
206
264
|
});
|
|
207
265
|
}
|
|
266
|
+
for (const [index, row] of workflowRows.entries()) rows.push({ kind: "workflow", ownerKey: entry.key, row, last: index === workflowRows.length - 1 && !entry.nestedChildren?.length });
|
|
208
267
|
const nested = nestedFleetRows(entry.nestedChildren, attached.length > 0 ? 3 : 4);
|
|
209
268
|
for (const [index, row] of nested.entries()) rows.push({ kind: "nested", ownerKey: entry.key, row, last: index === nested.length - 1 });
|
|
210
269
|
}
|
|
@@ -282,6 +341,7 @@ export function collectFleetStatusEntries(state: SubagentState): FleetStatusEntr
|
|
|
282
341
|
startedAt,
|
|
283
342
|
tokens: job.totalTokens?.total ?? 0,
|
|
284
343
|
state: job.status,
|
|
344
|
+
...(job.steps?.length ? { workflowRows: workflowFleetRows(job.steps) } : {}),
|
|
285
345
|
...(job.nestedChildren?.length ? { nestedChildren: job.nestedChildren } : {}),
|
|
286
346
|
});
|
|
287
347
|
continue;
|
|
@@ -548,6 +608,8 @@ export class SubagentFleetStatus {
|
|
|
548
608
|
if (row.kind === "owner" || row.kind === "child") {
|
|
549
609
|
const ownerIndex = this.entries.findIndex((entry) => entry.key === row.entry.key);
|
|
550
610
|
lines.push(this.renderEntry(ownerIndex + 1, selectedIndex, row.entry, width, theme, row.kind === "child" ? (row.last ? "└─" : "├─") : undefined));
|
|
611
|
+
} else if (row.kind === "workflow") {
|
|
612
|
+
lines.push(this.renderWorkflowRow(row.row, row.last, width, theme));
|
|
551
613
|
} else {
|
|
552
614
|
lines.push(this.renderNestedRow(row.row, row.last, width, theme));
|
|
553
615
|
}
|
|
@@ -576,6 +638,20 @@ export class SubagentFleetStatus {
|
|
|
576
638
|
return truncateToWidth(`${left}${theme.fg("dim", elapsed)}`, width);
|
|
577
639
|
}
|
|
578
640
|
|
|
641
|
+
private renderWorkflowRow(row: FleetWorkflowRow, last: boolean, width: number, theme: Theme): string {
|
|
642
|
+
const marker = last ? "└─" : "├─";
|
|
643
|
+
const indent = " ";
|
|
644
|
+
if (row.overflow !== undefined) return truncateToWidth(`${indent}${marker} ${theme.fg("dim", `+${row.overflow} hidden workflow steps`)}`, width);
|
|
645
|
+
const modelThinking = row.modelThinking ? ` (${row.modelThinking})` : "";
|
|
646
|
+
const activity = row.activity ? ` · ${row.activity}` : "";
|
|
647
|
+
const left = `${indent}${marker} ${nestedStatusGlyph(row.state, theme)} ${theme.fg("muted", `${row.name}${modelThinking}`)} · ${row.state}${activity}`;
|
|
648
|
+
const details = [
|
|
649
|
+
row.startedAt !== undefined ? formatFleetElapsed(Date.now() - row.startedAt) : undefined,
|
|
650
|
+
row.tokens !== undefined ? formatFleetTokens(row.tokens) : undefined,
|
|
651
|
+
].filter(Boolean).join(" · ");
|
|
652
|
+
return truncateToWidth(`${left}${details ? theme.fg("dim", ` · ${details}`) : ""}`, width);
|
|
653
|
+
}
|
|
654
|
+
|
|
579
655
|
private bullet(rosterIndex: number, selectedIndex: number, theme: Theme): string {
|
|
580
656
|
return rosterIndex === selectedIndex ? theme.fg("accent", ">") : " ";
|
|
581
657
|
}
|
|
@@ -624,6 +700,15 @@ export class SubagentFleetStatus {
|
|
|
624
700
|
entry.external,
|
|
625
701
|
Math.round((now - entry.startedAt) / 1000),
|
|
626
702
|
entry.tokens,
|
|
703
|
+
visibleWorkflowRows(entry.workflowRows, entry.parentKey ? 2 : 4).map((row) => [
|
|
704
|
+
row.name,
|
|
705
|
+
row.state,
|
|
706
|
+
row.modelThinking,
|
|
707
|
+
row.activity,
|
|
708
|
+
row.startedAt,
|
|
709
|
+
row.tokens,
|
|
710
|
+
row.overflow,
|
|
711
|
+
]),
|
|
627
712
|
nestedFleetRows(entry.nestedChildren, entry.parentKey ? 3 : 4).map((row) => [
|
|
628
713
|
row.name,
|
|
629
714
|
row.state,
|
package/src/tui/fleet.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi, type Compo
|
|
|
6
6
|
import { snapshotExternalRuns, type ExternalRun } from "../api/external-runs.ts";
|
|
7
7
|
import { getArtifactPaths, getArtifactsDir } from "../shared/artifacts.ts";
|
|
8
8
|
import { formatDuration, formatModelThinking, formatTokens, shortenPath } from "../shared/formatters.ts";
|
|
9
|
-
import { DIRS, type AsyncJobState, type Details, type FleetKeybindingAction, type FleetKeybindingsConfig, type ForegroundChildControl, type ForegroundResumeChild, type ForegroundResumeRun, type ForegroundRunControl, type SubagentState } from "../shared/types.ts";
|
|
9
|
+
import { DIRS, type AsyncJobState, type AsyncJobStep, type Details, type FleetKeybindingAction, type FleetKeybindingsConfig, type ForegroundChildControl, type ForegroundResumeChild, type ForegroundResumeRun, type ForegroundRunControl, type SubagentState } from "../shared/types.ts";
|
|
10
10
|
import { decodeUtf8Tail } from "../shared/utf8.ts";
|
|
11
11
|
import { readStatus } from "../shared/utils.ts";
|
|
12
12
|
import { formatAsyncRunTranscript } from "../runs/background/fleet-view.ts";
|
|
@@ -494,15 +494,68 @@ function externalElapsedEnd(run: ExternalRun): number {
|
|
|
494
494
|
return terminal ? (run.endedAt ?? run.updatedAt ?? Date.now()) : Date.now();
|
|
495
495
|
}
|
|
496
496
|
|
|
497
|
+
function workflowStepLabel(step: AsyncJobStep, index: number): string {
|
|
498
|
+
const key = step.workflowKey ?? `step ${index + 1}`;
|
|
499
|
+
const label = step.label && step.label !== key ? ` · ${step.label}` : "";
|
|
500
|
+
const phase = step.phase ? `${step.phase}: ` : "";
|
|
501
|
+
return `${phase}${key}${label} (${step.agent})`;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function workflowStepActivity(step: AsyncJobStep): string | undefined {
|
|
505
|
+
if (step.currentTool) return `tool ${step.currentTool}`;
|
|
506
|
+
if (step.currentPath) return shortenPath(step.currentPath);
|
|
507
|
+
if (step.activityState === "needs_attention") return "needs attention";
|
|
508
|
+
if (step.activityState === "active_long_running") return "long-running";
|
|
509
|
+
if (step.turnCount !== undefined) return `${step.turnCount} turns`;
|
|
510
|
+
if (step.toolCount !== undefined) return `${step.toolCount} tools`;
|
|
511
|
+
return undefined;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function visibleWorkflowProgressSteps(steps: AsyncJobStep[], visibleLimit: number): Array<{ step: AsyncJobStep; index: number } | { hidden: number }> {
|
|
515
|
+
if (steps.length <= visibleLimit) return steps.map((step, index) => ({ step, index }));
|
|
516
|
+
const selected = new Set<number>();
|
|
517
|
+
for (const [index, step] of steps.entries()) {
|
|
518
|
+
if (step.status !== "complete" && step.status !== "completed") selected.add(index);
|
|
519
|
+
if (selected.size >= visibleLimit) break;
|
|
520
|
+
}
|
|
521
|
+
for (let index = steps.length - 1; index >= 0 && selected.size < visibleLimit; index--) selected.add(index);
|
|
522
|
+
const visible = [...selected].sort((left, right) => left - right).map((index) => ({ step: steps[index]!, index }));
|
|
523
|
+
return [{ hidden: steps.length - visible.length }, ...visible];
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function workflowProgressLines(steps: AsyncJobStep[] | undefined): string[] {
|
|
527
|
+
if (!steps?.length) return [];
|
|
528
|
+
const lines = ["Workflow progress:"];
|
|
529
|
+
for (const row of visibleWorkflowProgressSteps(steps, 8)) {
|
|
530
|
+
if ("hidden" in row) {
|
|
531
|
+
lines.push(` +${row.hidden} hidden workflow steps`);
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
const activity = workflowStepActivity(row.step);
|
|
535
|
+
const context = contextModeLabel(row.step.context);
|
|
536
|
+
const details = [row.step.status, activity, context, row.step.tokens?.total !== undefined ? formatTokens(row.step.tokens.total) : undefined].filter(Boolean).join(" · ");
|
|
537
|
+
lines.push(` ${row.index + 1}. ${workflowStepLabel(row.step, row.index)}${details ? ` — ${details}` : ""}`);
|
|
538
|
+
}
|
|
539
|
+
return lines;
|
|
540
|
+
}
|
|
541
|
+
|
|
497
542
|
function asyncDetail(item: Extract<FleetItem, { kind: "async" }>, state: SubagentState): string[] {
|
|
498
543
|
const status = readStatus(item.run.asyncDir);
|
|
499
544
|
if (status) {
|
|
500
545
|
const trackedJob = state.fleetJobs?.get(item.runId) ?? state.asyncJobs.get(item.runId);
|
|
501
|
-
|
|
546
|
+
const lines = formatAsyncRunTranscript(status, item.run.asyncDir, {
|
|
502
547
|
index: item.index,
|
|
503
548
|
lines: TRANSCRIPT_LINES,
|
|
504
549
|
sessionRoots: uniquePaths([...(state.trustedSessionRoots ?? []), trackedJob?.sessionRoot]),
|
|
505
550
|
}).split("\n");
|
|
551
|
+
if (status.mode === "workflow" && item.index === undefined) {
|
|
552
|
+
const progress = workflowProgressLines((status.steps ?? item.run.steps) as AsyncJobStep[]);
|
|
553
|
+
if (progress.length) {
|
|
554
|
+
const modeIndex = lines.findIndex((line) => line.startsWith("Mode:"));
|
|
555
|
+
lines.splice(modeIndex >= 0 ? modeIndex + 1 : 0, 0, "", ...progress);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
return lines;
|
|
506
559
|
}
|
|
507
560
|
const outputPath = item.index !== undefined ? path.join(item.run.asyncDir, `output-${item.index}.log`) : undefined;
|
|
508
561
|
return [
|
package/src/tui/render.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import * as path from "node:path";
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
6
7
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
7
8
|
import { getMarkdownTheme, keyText, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
8
9
|
import { Container, Markdown, Spacer, Text, visibleWidth, type Component } from "@earendil-works/pi-tui";
|
|
@@ -535,7 +536,76 @@ function compactCurrentActivity(progress: AgentProgress): string {
|
|
|
535
536
|
return formatCurrentToolLine(progress, getTermWidth() - 4, false, snapshotNow) ?? buildLiveStatusLine(progress, snapshotNow) ?? "thinking…";
|
|
536
537
|
}
|
|
537
538
|
|
|
538
|
-
|
|
539
|
+
function textDigest(text: string): string {
|
|
540
|
+
return createHash("sha256").update(text).digest("hex").slice(0, 16);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function displayTextRenderKey(text: string): unknown[] {
|
|
544
|
+
const normalized = sanitizeDisplayText(text);
|
|
545
|
+
return [normalized.length, textDigest(normalized)];
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function expandedStepActivityRenderKey(step: AsyncJobStep): unknown[] {
|
|
549
|
+
return [
|
|
550
|
+
step.recentTools?.slice(-3).map((tool) => [tool.tool, displayTextRenderKey(tool.args), tool.endMs]),
|
|
551
|
+
compactRecentOutputLines(step.recentOutput).map(displayTextRenderKey),
|
|
552
|
+
];
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function widgetStepRenderKey(step: AsyncJobStep, index: number, expanded = false): unknown[] {
|
|
556
|
+
return [
|
|
557
|
+
step.index ?? index,
|
|
558
|
+
step.agent,
|
|
559
|
+
step.workflowKey,
|
|
560
|
+
step.phase,
|
|
561
|
+
step.label,
|
|
562
|
+
step.status,
|
|
563
|
+
step.activityState,
|
|
564
|
+
step.lastActivityAt,
|
|
565
|
+
step.currentTool,
|
|
566
|
+
step.currentToolArgs,
|
|
567
|
+
step.currentToolStartedAt,
|
|
568
|
+
step.currentPath,
|
|
569
|
+
step.turnCount,
|
|
570
|
+
step.toolCount,
|
|
571
|
+
step.startedAt,
|
|
572
|
+
step.endedAt,
|
|
573
|
+
step.durationMs,
|
|
574
|
+
step.tokens?.total,
|
|
575
|
+
step.model,
|
|
576
|
+
step.thinking,
|
|
577
|
+
step.context,
|
|
578
|
+
step.error,
|
|
579
|
+
expanded ? expandedStepActivityRenderKey(step) : undefined,
|
|
580
|
+
nestedRenderKey(step.children, expanded),
|
|
581
|
+
];
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function nestedRenderKey(children: NestedRunSummary[] | undefined, expanded = false): unknown[] {
|
|
585
|
+
return (children ?? []).map((child) => [
|
|
586
|
+
child.id,
|
|
587
|
+
child.state,
|
|
588
|
+
child.agent,
|
|
589
|
+
child.model,
|
|
590
|
+
child.thinking,
|
|
591
|
+
child.activityState,
|
|
592
|
+
child.lastActivityAt,
|
|
593
|
+
child.currentTool,
|
|
594
|
+
child.currentToolStartedAt,
|
|
595
|
+
child.currentPath,
|
|
596
|
+
child.turnCount,
|
|
597
|
+
child.toolCount,
|
|
598
|
+
child.startedAt,
|
|
599
|
+
child.endedAt,
|
|
600
|
+
child.lastUpdate,
|
|
601
|
+
child.error,
|
|
602
|
+
child.totalTokens?.total,
|
|
603
|
+
child.steps?.map((step, index) => widgetStepRenderKey({ ...step, agent: step.agent, status: step.status }, index, expanded)),
|
|
604
|
+
nestedRenderKey(child.children, expanded),
|
|
605
|
+
]);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
export function widgetRenderKey(job: AsyncJobState, expanded = false): string {
|
|
539
609
|
return JSON.stringify({
|
|
540
610
|
asyncDir: job.asyncDir,
|
|
541
611
|
status: job.status,
|
|
@@ -551,8 +621,8 @@ export function widgetRenderKey(job: AsyncJobState): string {
|
|
|
551
621
|
currentStep: job.currentStep,
|
|
552
622
|
chainStepCount: job.chainStepCount,
|
|
553
623
|
parallelGroups: job.parallelGroups,
|
|
554
|
-
steps: job.steps,
|
|
555
|
-
nestedChildren: job.nestedChildren,
|
|
624
|
+
steps: job.steps?.map((step, index) => widgetStepRenderKey(step, index, expanded)),
|
|
625
|
+
nestedChildren: nestedRenderKey(job.nestedChildren, expanded),
|
|
556
626
|
stepsTotal: job.stepsTotal,
|
|
557
627
|
runningSteps: job.runningSteps,
|
|
558
628
|
completedSteps: job.completedSteps,
|