pi-subagents 0.62.0 → 0.63.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 +28 -0
- package/docs/agents.md +5 -4
- package/docs/configuration.md +28 -2
- package/docs/models.md +5 -5
- package/docs/observability.md +4 -1
- package/docs/tool-reference.md +2 -2
- package/package.json +1 -1
- package/skills/pi-subagents/references/management-authoring-rpc.md +1 -1
- package/skills/pi-subagents/references/prompting-and-roles.md +3 -3
- package/src/agents/agent-management.ts +22 -4
- package/src/agents/agents.ts +107 -125
- package/src/api/shared-types.ts +3 -0
- package/src/extension/config.ts +20 -0
- package/src/inspectors/herdr/inspector-runner.ts +19 -13
- package/src/runs/background/active-async-capacity.ts +0 -1
- package/src/runs/background/async-execution.ts +53 -7
- package/src/runs/background/async-resume.ts +3 -0
- package/src/runs/background/async-status.ts +18 -2
- package/src/runs/background/notify.ts +13 -1
- package/src/runs/background/run-status.ts +22 -2
- package/src/runs/background/subagent-runner.ts +30 -2
- package/src/runs/background/wait-completions.ts +13 -0
- package/src/runs/foreground/subagent-executor.ts +35 -3
- package/src/runs/shared/acceptance.ts +15 -9
- package/src/runs/shared/lane-metadata.ts +24 -3
- package/src/runs/shared/parallel-handoff.ts +4 -0
- package/src/runs/shared/pi-args.ts +8 -2
- package/src/runs/shared/task-intent.ts +5 -2
- package/src/runs/shared/worktree.ts +467 -63
- package/src/shared/types.ts +29 -0
- package/src/shared/utils.ts +18 -7
- package/src/slash/subagents-admin.ts +24 -12
- package/src/tui/fleet-status.ts +61 -2
- package/src/tui/fleet.ts +12 -7
- package/src/tui/render.ts +222 -14
- package/src/workflows/workflow-checklist.ts +441 -0
package/src/shared/types.ts
CHANGED
|
@@ -382,6 +382,17 @@ export type SubagentResultStatus = "completed" | "failed" | "paused" | "stopped"
|
|
|
382
382
|
export type SubagentOutputState = "present" | "absent" | "unknown";
|
|
383
383
|
export type SubagentRunMode = "single" | "parallel" | "chain" | "workflow";
|
|
384
384
|
export type SubagentResultMode = SubagentRunMode;
|
|
385
|
+
export type WorktreeProvider = "auto" | "native" | "worktrunk";
|
|
386
|
+
export type ManagedWorktreeProvider = Exclude<WorktreeProvider, "auto">;
|
|
387
|
+
|
|
388
|
+
export interface WorktreeNaming {
|
|
389
|
+
requestedBranch: string;
|
|
390
|
+
branchPrefix: string;
|
|
391
|
+
label: string;
|
|
392
|
+
sanitizedPathComponent: string;
|
|
393
|
+
collision?: "branch" | "path" | "both";
|
|
394
|
+
collisionSuffix?: string;
|
|
395
|
+
}
|
|
385
396
|
|
|
386
397
|
export interface ParallelHandoffPatch {
|
|
387
398
|
path: string;
|
|
@@ -425,6 +436,10 @@ export interface ParallelHandoffCleanupTask {
|
|
|
425
436
|
index: number;
|
|
426
437
|
path: string;
|
|
427
438
|
branch: string;
|
|
439
|
+
/** Provider that allocated this worktree; omitted in old manifests. */
|
|
440
|
+
provider?: ManagedWorktreeProvider;
|
|
441
|
+
/** Branch/path naming evidence retained with the cleanup authority. */
|
|
442
|
+
naming?: WorktreeNaming;
|
|
428
443
|
worktreeRemoved: boolean;
|
|
429
444
|
branchRemoved: boolean;
|
|
430
445
|
preserved?: boolean;
|
|
@@ -1202,6 +1217,8 @@ export interface SingleResult {
|
|
|
1202
1217
|
* result row's array position.
|
|
1203
1218
|
*/
|
|
1204
1219
|
index: number;
|
|
1220
|
+
/** Workflow child key that owns this result when returned from workflow details. */
|
|
1221
|
+
workflowKey?: string;
|
|
1205
1222
|
agent: string;
|
|
1206
1223
|
task: string;
|
|
1207
1224
|
/** Human-readable display name for the child's own session (agent + task
|
|
@@ -1307,6 +1324,8 @@ export interface WaitCompletionChild {
|
|
|
1307
1324
|
sessionFile?: string;
|
|
1308
1325
|
success?: boolean;
|
|
1309
1326
|
outputState?: SubagentOutputState;
|
|
1327
|
+
structuredOutput?: unknown;
|
|
1328
|
+
structuredOutputPath?: string;
|
|
1310
1329
|
error?: string;
|
|
1311
1330
|
model?: string;
|
|
1312
1331
|
contextOverflow?: boolean;
|
|
@@ -1841,6 +1860,10 @@ export interface AsyncStatus {
|
|
|
1841
1860
|
worktreePath?: string;
|
|
1842
1861
|
/** Display-only branch copied at launch; handoff remains authoritative. */
|
|
1843
1862
|
branch?: string;
|
|
1863
|
+
/** Display-only provisioning provider copied at launch; handoff remains authoritative. */
|
|
1864
|
+
provider?: ManagedWorktreeProvider;
|
|
1865
|
+
/** Display-only naming evidence copied at launch; handoff remains authoritative. */
|
|
1866
|
+
naming?: WorktreeNaming;
|
|
1844
1867
|
/** Child run identity for workflow capacity reconciliation. */
|
|
1845
1868
|
runId?: string;
|
|
1846
1869
|
/** True only when this workflow child owns a detached async runner. */
|
|
@@ -2554,6 +2577,12 @@ export interface ExtensionConfig {
|
|
|
2554
2577
|
worktreeSetupHook?: string;
|
|
2555
2578
|
worktreeSetupHookTimeoutMs?: number;
|
|
2556
2579
|
worktreeBaseDir?: string;
|
|
2580
|
+
/** Enable managed worktrees when a launch does not provide an explicit value. */
|
|
2581
|
+
worktree?: boolean;
|
|
2582
|
+
/** Worktree allocator selection. Defaults to auto. */
|
|
2583
|
+
worktreeProvider?: WorktreeProvider;
|
|
2584
|
+
/** Namespace used by managed worktree branches. Defaults to pi-subagents/. */
|
|
2585
|
+
worktreeBranchPrefix?: string;
|
|
2557
2586
|
/** Where to store subagent artifact files. Defaults to "session" (the pi session directory, or OS temp when unavailable). Set to "project" for cwd/.pi/subagents. */
|
|
2558
2587
|
artifactDir?: ArtifactDirPreference;
|
|
2559
2588
|
/** Artifact cleanup retention. Set cleanupDays to 0 to disable cleanup. */
|
package/src/shared/utils.ts
CHANGED
|
@@ -237,6 +237,12 @@ export function findLatestSessionFile(sessionDir: string): string | null {
|
|
|
237
237
|
return latest ? latest.path : null;
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
+
const PI_TURN_TIMING_FOOTER = /(?:\r?\n)*\x1b\[38;2;136;136;136m✻ Turn took [^()\r\n]+ \(Total time [^·\r\n]+ · \d+ turns?\)\x1b\[0m[ \t]*$/u;
|
|
241
|
+
|
|
242
|
+
function stripPiTurnTimingFooter(text: string): string {
|
|
243
|
+
return text.replace(PI_TURN_TIMING_FOOTER, "");
|
|
244
|
+
}
|
|
245
|
+
|
|
240
246
|
export function getFinalOutput(messages: Message[]): string {
|
|
241
247
|
const validTextParts: string[] = [];
|
|
242
248
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
@@ -246,21 +252,26 @@ export function getFinalOutput(messages: Message[]): string {
|
|
|
246
252
|
|| ("stopReason" in msg && msg.stopReason === "error");
|
|
247
253
|
if (hasAssistantError) continue;
|
|
248
254
|
const messageText = msg.content
|
|
249
|
-
.
|
|
250
|
-
|
|
255
|
+
.flatMap((part) => {
|
|
256
|
+
if (part.type !== "text") return [];
|
|
257
|
+
const text = stripPiTurnTimingFooter(part.text);
|
|
258
|
+
return text.trim().length > 0 ? [text] : [];
|
|
259
|
+
})
|
|
251
260
|
.join("\n");
|
|
252
261
|
for (let j = msg.content.length - 1; j >= 0; j--) {
|
|
253
262
|
const part = msg.content[j];
|
|
254
|
-
if (!part || part.type !== "text"
|
|
255
|
-
|
|
256
|
-
if (
|
|
257
|
-
|
|
263
|
+
if (!part || part.type !== "text") continue;
|
|
264
|
+
const text = stripPiTurnTimingFooter(part.text);
|
|
265
|
+
if (text.trim().length === 0) continue;
|
|
266
|
+
validTextParts.push(text);
|
|
267
|
+
if (/```acceptance[-_]report\s*\n[\s\S]*?```/i.test(text)) return messageText;
|
|
268
|
+
for (const match of text.matchAll(/```(?:json|jsonc|json5)\s*\n([\s\S]*?)```/gi)) {
|
|
258
269
|
const body = match[1] ?? "";
|
|
259
270
|
if (/"(?:criteriaSatisfied|criteria_satisfied)"/.test(body) && /"(?:changedFiles|changed_files|testsAddedOrUpdated|tests_added_or_updated|commandsRun|commands_run|validationOutput|validation_output|residualRisks|residual_risks|noStagedFiles|no_staged_files|diffSummary|diff_summary|reviewFindings|review_findings|manualNotes|manual_notes)"/.test(body)) {
|
|
260
271
|
return messageText;
|
|
261
272
|
}
|
|
262
273
|
}
|
|
263
|
-
if (/ACCEPTANCE_REPORT\s*:/i.test(
|
|
274
|
+
if (/ACCEPTANCE_REPORT\s*:/i.test(text)) return messageText;
|
|
264
275
|
}
|
|
265
276
|
}
|
|
266
277
|
return validTextParts[0] ?? "";
|
|
@@ -2,7 +2,6 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import {
|
|
5
|
-
agentHasFrontmatterField,
|
|
6
5
|
discoverAgentsAll,
|
|
7
6
|
EXTRA_AGENT_DIRS_ENV,
|
|
8
7
|
frontmatterNameForConfig,
|
|
@@ -116,18 +115,15 @@ type AgentSelection =
|
|
|
116
115
|
|
|
117
116
|
function savesThroughSettings(agent: AgentConfig, field: EditableOverrideField): boolean {
|
|
118
117
|
if (agent.source === "builtin") return true;
|
|
119
|
-
if (agent.source === "package")
|
|
120
|
-
if (field === "systemPrompt") return false;
|
|
121
|
-
return !agentHasFrontmatterField(agent, field);
|
|
122
|
-
}
|
|
118
|
+
if (agent.source === "package") return true;
|
|
123
119
|
if (!agent.override) return false;
|
|
120
|
+
if (agent.override.fields?.includes(field) === true) return true;
|
|
124
121
|
// A lower-scope override can flow into a higher-scope custom agent with the
|
|
125
122
|
// same name. Persist that agent's edits in its own frontmatter instead of
|
|
126
123
|
// rewriting the shared lower-scope override used by another agent.
|
|
127
124
|
if (agent.source !== agent.override.scope) return false;
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
// not redirect edits to an unrelated frontmatter-owned field.
|
|
125
|
+
// Compare the effective value with the pre-override base so an override on one
|
|
126
|
+
// field does not redirect edits to an unrelated frontmatter-owned field.
|
|
131
127
|
return agent[field] !== agent.override.base[field];
|
|
132
128
|
}
|
|
133
129
|
|
|
@@ -256,10 +252,12 @@ async function chooseThinking(ctx: ExtensionContext, agent: AgentConfig): Promis
|
|
|
256
252
|
}
|
|
257
253
|
|
|
258
254
|
async function chooseOverrideScope(ctx: ExtensionContext, agent: AgentConfig): Promise<"user" | "project" | undefined> {
|
|
259
|
-
if (agent.override?.scope) return agent.override.scope;
|
|
260
255
|
const d = discoverAgentsAll(ctx.cwd);
|
|
256
|
+
if (agent.override?.scope) {
|
|
257
|
+
return agent.source === "project" && agent.override.scope === "user" && d.projectSettingsPath ? "project" : agent.override.scope;
|
|
258
|
+
}
|
|
261
259
|
if (!d.projectSettingsPath || !ctx.hasUI) return "user";
|
|
262
|
-
const choice = await ctx.ui.select(`Save
|
|
260
|
+
const choice = await ctx.ui.select(`Save subagent override for ${agent.name}`, ["user", "project"]);
|
|
263
261
|
return choice === "user" || choice === "project" ? choice : undefined;
|
|
264
262
|
}
|
|
265
263
|
|
|
@@ -271,6 +269,18 @@ function persistSettingsField(
|
|
|
271
269
|
value: string | undefined,
|
|
272
270
|
): { filePath: string; overridden: boolean } {
|
|
273
271
|
const base = agent.override?.base ?? buildBuiltinBase(agent);
|
|
272
|
+
const shadowsLowerScope = scope === "project" && agent.override?.fieldScopes?.[field]?.includes("user") === true;
|
|
273
|
+
if (shadowsLowerScope && (value === undefined || value === base[field])) {
|
|
274
|
+
const filePath = field === "model"
|
|
275
|
+
? mergeBuiltinAgentOverride(ctx.cwd, agent.name, scope, { model: value ?? base.model ?? false })
|
|
276
|
+
: field === "thinking"
|
|
277
|
+
? mergeBuiltinAgentOverride(ctx.cwd, agent.name, scope, { thinking: value ?? (base.thinking === false ? "off" : base.thinking) ?? false })
|
|
278
|
+
: mergeBuiltinAgentOverride(ctx.cwd, agent.name, scope, { systemPrompt: value ?? base.systemPrompt });
|
|
279
|
+
return {
|
|
280
|
+
filePath,
|
|
281
|
+
overridden: true,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
274
284
|
if (value === undefined || value === base[field]) {
|
|
275
285
|
return {
|
|
276
286
|
filePath: removeBuiltinAgentOverrideFields(ctx.cwd, agent.name, scope, [field]).path,
|
|
@@ -358,13 +368,15 @@ async function saveAgentSystemPrompt(ctx: ExtensionContext, agent: AgentConfig,
|
|
|
358
368
|
}
|
|
359
369
|
|
|
360
370
|
async function editSystemPrompt(ctx: ExtensionContext, agent: AgentConfig): Promise<string | null> {
|
|
361
|
-
|
|
371
|
+
const saveThroughSettings = savesThroughSettings(agent, "systemPrompt");
|
|
372
|
+
if (!saveThroughSettings) {
|
|
362
373
|
const readOnlyMessage = readOnlyAgentMessage(agent, "systemPrompt");
|
|
363
374
|
if (readOnlyMessage) return readOnlyMessage;
|
|
364
375
|
}
|
|
365
376
|
const edited = await ctx.ui.editor(`Edit '${agent.name}' system prompt`, agent.systemPrompt ?? "");
|
|
366
377
|
if (edited === undefined) return null;
|
|
367
|
-
|
|
378
|
+
const projectOwnsLowerScopeOverride = agent.source === "project" && agent.override?.scope === "user" && discoverAgentsAll(ctx.cwd).projectSettingsPath;
|
|
379
|
+
if (edited.replace(/\s+$/, "") === (agent.systemPrompt ?? "").replace(/\s+$/, "") && !(saveThroughSettings && projectOwnsLowerScopeOverride)) {
|
|
368
380
|
return `System prompt for '${agent.name}' left unchanged.`;
|
|
369
381
|
}
|
|
370
382
|
return saveAgentSystemPrompt(ctx, agent, edited);
|
package/src/tui/fleet-status.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { contextModeLabel } from "../runs/shared/context-mode.ts";
|
|
|
8
8
|
import { formatWorkflowJsonPreview } from "../workflows/scripted-workflow.ts";
|
|
9
9
|
import { hostStepReportName, hostStepVerdictLabel } from "../runs/shared/host-step-status.ts";
|
|
10
10
|
import { isStaleExtensionContextError } from "../shared/extension-context.ts";
|
|
11
|
+
import { formatWorkflowChecklistBottleneck, formatWorkflowChecklistPhase, formatWorkflowChecklistSummary, projectWorkflowChecklist, type WorkflowChecklistPhase, type WorkflowChecklistProjection } from "../workflows/workflow-checklist.ts";
|
|
11
12
|
|
|
12
13
|
export const FLEET_STATUS_WIDGET_KEY = "subagent-fleet-status";
|
|
13
14
|
|
|
@@ -35,6 +36,7 @@ type FleetStatusEntry = {
|
|
|
35
36
|
projectPane?: HerdrProjectPaneSnapshot;
|
|
36
37
|
nestedChildren?: NestedRunSummary[];
|
|
37
38
|
workflowRows?: AsyncStatusWorkflowRow[];
|
|
39
|
+
workflowChecklist?: WorkflowChecklistProjection;
|
|
38
40
|
};
|
|
39
41
|
|
|
40
42
|
type FleetNestedRow = {
|
|
@@ -50,6 +52,7 @@ type FleetNestedRow = {
|
|
|
50
52
|
type FleetTreeRow =
|
|
51
53
|
| { kind: "owner"; entry: FleetStatusEntry }
|
|
52
54
|
| { kind: "child"; entry: FleetStatusEntry; last: boolean }
|
|
55
|
+
| { kind: "workflow-phase"; ownerKey: string; phase: WorkflowChecklistPhase; last: boolean }
|
|
53
56
|
| { kind: "workflow"; ownerKey: string; row: AsyncStatusWorkflowRow; last: boolean }
|
|
54
57
|
| { kind: "nested"; ownerKey: string; row: FleetNestedRow; last: boolean };
|
|
55
58
|
|
|
@@ -117,6 +120,18 @@ function visibleWorkflowRows(rows: AsyncStatusWorkflowRow[] | undefined, visible
|
|
|
117
120
|
return [{ name: `… +${rows.length - visible.length} hidden workflow steps`, state: "complete", overflow: rows.length - visible.length }, ...visible];
|
|
118
121
|
}
|
|
119
122
|
|
|
123
|
+
function visibleWorkflowPhases(checklist: WorkflowChecklistProjection | undefined, visibleLimit: number): WorkflowChecklistPhase[] {
|
|
124
|
+
const phases = checklist?.phases ?? [];
|
|
125
|
+
if (phases.length <= visibleLimit) return phases;
|
|
126
|
+
const selected = new Set<number>();
|
|
127
|
+
for (const [index, phase] of phases.entries()) {
|
|
128
|
+
if (phase.state !== "complete") selected.add(index);
|
|
129
|
+
if (selected.size >= visibleLimit) break;
|
|
130
|
+
}
|
|
131
|
+
for (let index = phases.length - 1; index >= 0 && selected.size < visibleLimit; index--) selected.add(index);
|
|
132
|
+
return [...selected].sort((left, right) => left - right).map((index) => phases[index]!);
|
|
133
|
+
}
|
|
134
|
+
|
|
120
135
|
function isWorkflowRowTerminal(row: AsyncStatusWorkflowRow): boolean {
|
|
121
136
|
if (row.kind) return row.state === "done" || row.state === "cancelled" || row.state === "error";
|
|
122
137
|
return row.state === "complete" || row.state === "completed";
|
|
@@ -224,10 +239,11 @@ function fleetTreeRows(entries: FleetStatusEntry[]): FleetTreeRow[] {
|
|
|
224
239
|
if (entry.parentKey && entryKeys.has(entry.parentKey)) continue;
|
|
225
240
|
rows.push({ kind: "owner", entry });
|
|
226
241
|
const attached = childrenByParent.get(entry.key) ?? [];
|
|
242
|
+
const workflowPhases = visibleWorkflowPhases(entry.workflowChecklist, attached.length > 0 ? 2 : 4);
|
|
227
243
|
const workflowRows = visibleWorkflowRows(entry.workflowRows, attached.length > 0 ? 2 : 4);
|
|
228
244
|
for (const [index, child] of attached.entries()) {
|
|
229
245
|
const nested = nestedFleetRows(child.nestedChildren, 3);
|
|
230
|
-
const laterRows = index < attached.length - 1 || workflowRows.length > 0 || Boolean(entry.nestedChildren?.length);
|
|
246
|
+
const laterRows = index < attached.length - 1 || workflowPhases.length > 0 || workflowRows.length > 0 || Boolean(entry.nestedChildren?.length);
|
|
231
247
|
rows.push({ kind: "child", entry: child, last: !laterRows && nested.length === 0 });
|
|
232
248
|
for (const [nestedIndex, row] of nested.entries()) rows.push({
|
|
233
249
|
kind: "nested",
|
|
@@ -236,6 +252,12 @@ function fleetTreeRows(entries: FleetStatusEntry[]): FleetTreeRow[] {
|
|
|
236
252
|
last: nestedIndex === nested.length - 1 && !laterRows,
|
|
237
253
|
});
|
|
238
254
|
}
|
|
255
|
+
for (const [index, phase] of workflowPhases.entries()) rows.push({
|
|
256
|
+
kind: "workflow-phase",
|
|
257
|
+
ownerKey: entry.key,
|
|
258
|
+
phase,
|
|
259
|
+
last: index === workflowPhases.length - 1 && workflowRows.length === 0 && !entry.nestedChildren?.length,
|
|
260
|
+
});
|
|
239
261
|
for (const [index, row] of workflowRows.entries()) rows.push({ kind: "workflow", ownerKey: entry.key, row, last: index === workflowRows.length - 1 && !entry.nestedChildren?.length });
|
|
240
262
|
const nested = nestedFleetRows(entry.nestedChildren, attached.length > 0 ? 3 : 4);
|
|
241
263
|
for (const [index, row] of nested.entries()) rows.push({ kind: "nested", ownerKey: entry.key, row, last: index === nested.length - 1 });
|
|
@@ -364,6 +386,14 @@ export function collectFleetStatusEntries(state: SubagentState): FleetStatusEntr
|
|
|
364
386
|
const latestEmit = job.workflow?.emits?.length ? formatWorkflowJsonPreview(job.workflow.emits.at(-1), 120) : undefined;
|
|
365
387
|
const workflowSteps = workflowStepsWithoutMaterializedChildren(job.steps, materializedChildrenByWorkflow.get(`async:${job.asyncId}`));
|
|
366
388
|
const workflowRows = projectAsyncWorkflowRows(workflowSteps, job.workflowGraph ?? job.hostSteps, job.preflight);
|
|
389
|
+
const workflowChecklist = projectWorkflowChecklist({
|
|
390
|
+
graph: job.workflowGraph,
|
|
391
|
+
steps: job.steps,
|
|
392
|
+
hostSteps: job.hostSteps,
|
|
393
|
+
preflight: job.preflight,
|
|
394
|
+
trace: job.workflow?.trace,
|
|
395
|
+
now: job.updatedAt ?? Date.now(),
|
|
396
|
+
});
|
|
367
397
|
entries.push({
|
|
368
398
|
key: `async:${job.asyncId}`,
|
|
369
399
|
...(linkedParentKey ? { parentKey: linkedParentKey } : {}),
|
|
@@ -375,6 +405,7 @@ export function collectFleetStatusEntries(state: SubagentState): FleetStatusEntr
|
|
|
375
405
|
...(job.totalTokens?.window !== undefined ? { window: job.totalTokens.window } : {}),
|
|
376
406
|
state: job.status,
|
|
377
407
|
...(workflowRows.length ? { workflowRows } : {}),
|
|
408
|
+
...(workflowChecklist.total ? { workflowChecklist } : {}),
|
|
378
409
|
...(job.nestedChildren?.length ? { nestedChildren: job.nestedChildren } : {}),
|
|
379
410
|
});
|
|
380
411
|
continue;
|
|
@@ -668,6 +699,8 @@ export class SubagentFleetStatus {
|
|
|
668
699
|
lines.push(this.renderEntry(rosterIndex, selectedIndex, row.entry, width, theme, row.kind === "child" ? (row.last ? "└─" : "├─") : undefined));
|
|
669
700
|
} else if (row.kind === "workflow") {
|
|
670
701
|
lines.push(this.renderWorkflowRow(row.row, row.last, width, theme));
|
|
702
|
+
} else if (row.kind === "workflow-phase") {
|
|
703
|
+
lines.push(this.renderWorkflowPhaseRow(row.phase, row.last, width, theme));
|
|
671
704
|
} else {
|
|
672
705
|
lines.push(this.renderNestedRow(row.row, row.last, width, theme));
|
|
673
706
|
}
|
|
@@ -691,7 +724,10 @@ export class SubagentFleetStatus {
|
|
|
691
724
|
private renderEntry(rosterIndex: number, selectedIndex: number, entry: FleetStatusEntry, width: number, theme: Theme, branch?: string): string {
|
|
692
725
|
const agent = entry.modelThinking ? `${entry.agent} (${entry.modelThinking})` : entry.agent;
|
|
693
726
|
const prefix = branch ? ` ${branch}` : " ";
|
|
694
|
-
const
|
|
727
|
+
const checklist = entry.workflowWrapper && entry.workflowChecklist
|
|
728
|
+
? ` · checklist ${formatWorkflowChecklistSummary(entry.workflowChecklist)}${entry.workflowChecklist.bottleneck ? ` · bottleneck ${formatWorkflowChecklistBottleneck(entry.workflowChecklist.bottleneck)}` : ""}`
|
|
729
|
+
: "";
|
|
730
|
+
const left = `${prefix} ${this.bullet(rosterIndex, selectedIndex, theme)} ${theme.fg("muted", agent)} · ${entry.state}${checklist}`;
|
|
695
731
|
const elapsed = Date.now() - entry.startedAt;
|
|
696
732
|
const rightText = entry.projectPane
|
|
697
733
|
? `${entry.projectPane.summary ?? "—"} · ${formatFleetElapsed(Date.now() - entry.projectPane.refreshedAt)} ago`
|
|
@@ -730,6 +766,20 @@ export class SubagentFleetStatus {
|
|
|
730
766
|
return theme.fg("warning", state);
|
|
731
767
|
}
|
|
732
768
|
|
|
769
|
+
private renderWorkflowPhaseRow(phase: WorkflowChecklistPhase, last: boolean, width: number, theme: Theme): string {
|
|
770
|
+
const marker = last ? "└─" : "├─";
|
|
771
|
+
const glyph = phase.state === "complete"
|
|
772
|
+
? theme.fg("success", "✓")
|
|
773
|
+
: phase.state === "running"
|
|
774
|
+
? theme.fg("accent", "●")
|
|
775
|
+
: phase.state === "blocked" || phase.state === "failed"
|
|
776
|
+
? theme.fg("error", phase.state === "blocked" ? "!" : "✗")
|
|
777
|
+
: phase.state === "queued"
|
|
778
|
+
? theme.fg("muted", "◦")
|
|
779
|
+
: theme.fg("warning", "■");
|
|
780
|
+
return truncateToWidth(` ${marker} ${glyph} ${theme.fg("muted", formatWorkflowChecklistPhase(phase))}`, width);
|
|
781
|
+
}
|
|
782
|
+
|
|
733
783
|
private renderWorkflowRow(row: AsyncStatusWorkflowRow, last: boolean, width: number, theme: Theme): string {
|
|
734
784
|
const marker = last ? "└─" : "├─";
|
|
735
785
|
const indent = " ";
|
|
@@ -809,6 +859,15 @@ export class SubagentFleetStatus {
|
|
|
809
859
|
entry.external,
|
|
810
860
|
Math.round((now - entry.startedAt) / 1000),
|
|
811
861
|
entry.tokens,
|
|
862
|
+
entry.workflowChecklist ? [
|
|
863
|
+
entry.workflowChecklist.total,
|
|
864
|
+
entry.workflowChecklist.done,
|
|
865
|
+
entry.workflowChecklist.running,
|
|
866
|
+
entry.workflowChecklist.queued,
|
|
867
|
+
entry.workflowChecklist.blocked,
|
|
868
|
+
entry.workflowChecklist.failed,
|
|
869
|
+
entry.workflowChecklist.phases.map((phase) => [phase.key, phase.state, phase.done, phase.total, phase.running, phase.queued, phase.blocked, phase.failed, phase.items.map((item) => [item.key, item.state, item.currentTool, item.currentPath, item.durationMs, item.toolCount, item.error])]),
|
|
870
|
+
] : undefined,
|
|
812
871
|
visibleWorkflowRows(entry.workflowRows, entry.parentKey ? 2 : 4).map((row) => [
|
|
813
872
|
row.kind,
|
|
814
873
|
row.name,
|
package/src/tui/fleet.ts
CHANGED
|
@@ -42,7 +42,7 @@ export const DEFAULT_FLEET_KEYBINDINGS: Record<FleetKeybindingAction, string[]>
|
|
|
42
42
|
pageDown: ["pageDown"],
|
|
43
43
|
refresh: ["r", "R"],
|
|
44
44
|
steer: ["s"],
|
|
45
|
-
inspect: ["H"],
|
|
45
|
+
inspect: ["return", "H"],
|
|
46
46
|
stop: ["D"],
|
|
47
47
|
toggleTools: ["x", "X", "ctrl+o"],
|
|
48
48
|
};
|
|
@@ -952,6 +952,12 @@ export class SubagentFleetComponent implements Component {
|
|
|
952
952
|
return { runId: parent.asyncId, asyncDir: parent.asyncDir };
|
|
953
953
|
}
|
|
954
954
|
|
|
955
|
+
private inspectSelectedHerdr(): void {
|
|
956
|
+
const target = this.selectedHerdrInspectAction();
|
|
957
|
+
if ("reason" in target || !this.options.actions?.inspect) this.setActionNotice({ text: "reason" in target ? target.reason : "Herdr inspector controls are unavailable in this context.", isError: true });
|
|
958
|
+
else this.runAction(() => this.options.actions!.inspect!(target));
|
|
959
|
+
}
|
|
960
|
+
|
|
955
961
|
private actionLines(): string[] {
|
|
956
962
|
const lines: string[] = [];
|
|
957
963
|
if (this.actionBusy) lines.push(this.theme.fg("accent", "Action pending..."));
|
|
@@ -1179,12 +1185,6 @@ export class SubagentFleetComponent implements Component {
|
|
|
1179
1185
|
}
|
|
1180
1186
|
return;
|
|
1181
1187
|
}
|
|
1182
|
-
if (matchesFleetAction(data, this.keybindings, "inspect")) {
|
|
1183
|
-
const target = this.selectedHerdrInspectAction();
|
|
1184
|
-
if ("reason" in target || !this.options.actions?.inspect) this.setActionNotice({ text: "reason" in target ? target.reason : "Herdr inspector controls are unavailable in this context.", isError: true });
|
|
1185
|
-
else this.runAction(() => this.options.actions!.inspect!(target));
|
|
1186
|
-
return;
|
|
1187
|
-
}
|
|
1188
1188
|
if (matchesFleetAction(data, this.keybindings, "stop")) {
|
|
1189
1189
|
const target = this.selectedAsyncAction();
|
|
1190
1190
|
if ("reason" in target || !this.options.actions) this.setActionNotice({ text: "reason" in target ? target.reason : "Fleet controls are unavailable in this context.", isError: true });
|
|
@@ -1201,6 +1201,11 @@ export class SubagentFleetComponent implements Component {
|
|
|
1201
1201
|
this.expandedTools = !this.expandedTools;
|
|
1202
1202
|
this.transcriptCache = undefined;
|
|
1203
1203
|
this.tui.requestRender();
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
if (matchesFleetAction(data, this.keybindings, "inspect")) {
|
|
1207
|
+
this.inspectSelectedHerdr();
|
|
1208
|
+
return;
|
|
1204
1209
|
}
|
|
1205
1210
|
}
|
|
1206
1211
|
|