pi-subagents 0.40.0 → 0.41.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 +76 -0
- package/README.md +246 -525
- package/agents/oracle.md +1 -0
- package/package.json +12 -4
- package/prompts/parallel-context-build.md +1 -1
- package/prompts/parallel-handoff-plan.md +1 -1
- package/prompts/review-loop.md +1 -1
- package/skills/pi-subagents/SKILL.md +6 -6
- package/skills/pi-subagents/references/constraints-and-recipes.md +19 -26
- package/skills/pi-subagents/references/execution-controls.md +98 -97
- package/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
- package/skills/pi-subagents/references/prompting-and-roles.md +18 -27
- package/src/agents/agent-management.ts +155 -65
- package/src/agents/agent-serializer.ts +19 -0
- package/src/agents/agents.ts +154 -71
- package/src/agents/chain-serializer.ts +10 -7
- package/src/agents/frontmatter.ts +5 -3
- package/src/agents/identity.ts +1 -1
- package/src/agents/proactive-skills.ts +13 -10
- package/src/agents/skills.ts +23 -6
- package/src/api/control-channel.ts +4 -0
- package/src/api/delegation.ts +26 -194
- package/src/api/external-runs.ts +129 -0
- package/src/api/intercom-bridge.ts +3 -0
- package/src/api/pi-args.ts +5 -0
- package/src/api/preflight.ts +3 -3
- package/src/api/shared-types.ts +19 -0
- package/src/extension/config.ts +10 -0
- package/src/extension/control-notices.ts +5 -39
- package/src/extension/doctor.ts +10 -9
- package/src/extension/fanout-child.ts +7 -4
- package/src/extension/index.ts +232 -68
- package/src/extension/rpc.ts +18 -12
- package/src/extension/schemas.ts +48 -37
- package/src/extension/tool-description.ts +36 -86
- package/src/inspectors/herdr/actions.ts +229 -0
- package/src/inspectors/herdr/client.ts +130 -0
- package/src/inspectors/herdr/inspector-runner.ts +141 -0
- package/src/inspectors/herdr/project-panes.ts +154 -0
- package/src/integrations/herdr-status.ts +330 -0
- package/src/intercom/intercom-bridge.ts +3 -2
- package/src/intercom/result-intercom.ts +5 -1
- package/src/missions/actions.ts +372 -0
- package/src/missions/lifecycle.ts +314 -0
- package/src/missions/store.ts +442 -0
- package/src/missions/types.ts +135 -0
- package/src/policy/authority.ts +46 -0
- package/src/profiles/profiles.ts +29 -3
- package/src/runs/background/async-execution.ts +98 -49
- package/src/runs/background/async-job-tracker.ts +10 -2
- package/src/runs/background/async-resume.ts +6 -6
- package/src/runs/background/async-status.ts +29 -1
- package/src/runs/background/auto-drain.ts +3 -3
- package/src/runs/background/chain-append.ts +3 -2
- package/src/runs/background/control-channel.ts +9 -7
- package/src/runs/background/fleet-view.ts +3 -4
- package/src/runs/background/notify.ts +2 -1
- package/src/runs/background/process-terminal.ts +5 -5
- package/src/runs/background/result-watcher.ts +13 -5
- package/src/runs/background/run-id-resolver.ts +3 -3
- package/src/runs/background/run-status.ts +35 -8
- package/src/runs/background/scheduled-runs.ts +602 -375
- package/src/runs/background/stale-run-reconciler.ts +3 -3
- package/src/runs/background/subagent-runner.ts +608 -445
- package/src/runs/background/subagent-wait.ts +50 -9
- package/src/runs/background/wait-subscriptions.ts +253 -0
- package/src/runs/background/wait-tool.ts +12 -4
- package/src/runs/foreground/async-steering-action.ts +3 -3
- package/src/runs/foreground/chain-clarify.ts +8 -4
- package/src/runs/foreground/chain-execution.ts +56 -30
- package/src/runs/foreground/execution.ts +15 -2
- package/src/runs/foreground/subagent-executor.ts +1023 -273
- package/src/runs/shared/acceptance.ts +28 -6
- package/src/runs/shared/child-protocol.ts +302 -22
- package/src/runs/shared/dynamic-fanout.ts +1 -1
- package/src/runs/shared/external-cli-runner.ts +130 -0
- package/src/runs/shared/long-running-guard.ts +42 -1
- package/src/runs/shared/nested-events.ts +59 -5
- package/src/runs/shared/nested-render.ts +9 -4
- package/src/runs/shared/parallel-handoff.ts +86 -2
- package/src/runs/shared/parallel-utils.ts +11 -2
- package/src/runs/shared/permissions.ts +95 -0
- package/src/runs/shared/pi-args.ts +11 -1
- package/src/runs/shared/pi-spawn.ts +11 -1
- package/src/runs/shared/run-history.ts +1 -1
- package/src/runs/shared/subagent-prompt-runtime.ts +36 -5
- package/src/runs/shared/subagent-startup-retry.ts +5 -2
- package/src/runs/shared/turn-budget.ts +6 -6
- package/src/runs/shared/worktree.ts +122 -12
- package/src/shared/accessible-dir.ts +29 -7
- package/src/shared/artifacts.ts +18 -1
- package/src/shared/fork-context.ts +3 -2
- package/src/shared/launch-contract.ts +1 -0
- package/src/shared/settings.ts +10 -0
- package/src/shared/types.ts +156 -14
- package/src/shared/utils.ts +8 -6
- package/src/slash/delegation-adapters.ts +32 -194
- package/src/slash/delegation-request.ts +43 -126
- package/src/slash/prompt-template-bridge.ts +158 -205
- package/src/slash/prompt-workflows.ts +21 -57
- package/src/slash/slash-bridge.ts +14 -0
- package/src/slash/slash-commands.ts +31 -632
- package/src/slash/subagents-admin.ts +18 -14
- package/src/tui/fleet-status.ts +156 -21
- package/src/tui/fleet-transcript.ts +110 -5
- package/src/tui/fleet.ts +56 -24
- package/src/tui/render.ts +291 -109
- package/src/types/pi-runtime-compat.d.ts +14 -0
- package/src/watchdog/lsp-diagnostics.ts +12 -7
- package/src/watchdog/model-selection.ts +2 -2
- package/src/watchdog/permission-arbiter.ts +145 -0
- package/src/watchdog/register-child.ts +1 -1
- package/src/watchdog/register-main.ts +1 -1
- package/src/watchdog/review.ts +4 -1
- package/src/watchdog/runtime.ts +3 -2
- package/src/workflows/chat-progress.ts +140 -0
- package/src/workflows/scripted-workflow.ts +415 -0
- package/agents/advisor.md +0 -73
- package/src/extension/chain-validation.ts +0 -181
|
@@ -84,22 +84,22 @@ function liveAvailableModels(ctx: ExtensionContext) {
|
|
|
84
84
|
|
|
85
85
|
function buildBuiltinBase(agent: AgentConfig): BuiltinAgentOverrideBase {
|
|
86
86
|
return {
|
|
87
|
-
model: agent.model,
|
|
88
|
-
|
|
89
|
-
thinking: agent.thinking,
|
|
87
|
+
...(agent.model !== undefined ? { model: agent.model } : {}),
|
|
88
|
+
...(agent.fallbackModels !== undefined ? { fallbackModels: [...agent.fallbackModels] } : {}),
|
|
89
|
+
...(agent.thinking !== undefined ? { thinking: agent.thinking } : {}),
|
|
90
90
|
systemPromptMode: agent.systemPromptMode,
|
|
91
91
|
inheritProjectContext: agent.inheritProjectContext,
|
|
92
92
|
inheritSkills: agent.inheritSkills,
|
|
93
|
-
defaultContext: agent.defaultContext,
|
|
94
|
-
acceptanceRole: agent.acceptanceRole,
|
|
95
|
-
disabled: agent.disabled,
|
|
93
|
+
...(agent.defaultContext !== undefined ? { defaultContext: agent.defaultContext } : {}),
|
|
94
|
+
...(agent.acceptanceRole !== undefined ? { acceptanceRole: agent.acceptanceRole } : {}),
|
|
95
|
+
...(agent.disabled !== undefined ? { disabled: agent.disabled } : {}),
|
|
96
96
|
systemPrompt: agent.systemPrompt,
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
completionGuard: agent.completionGuard,
|
|
102
|
-
toolBudget: agent.toolBudget,
|
|
97
|
+
...(agent.skills !== undefined ? { skills: [...agent.skills] } : {}),
|
|
98
|
+
...(agent.tools !== undefined ? { tools: [...agent.tools] } : {}),
|
|
99
|
+
...(agent.mcpDirectTools !== undefined ? { mcpDirectTools: [...agent.mcpDirectTools] } : {}),
|
|
100
|
+
...(agent.subagentOnlyExtensions !== undefined ? { subagentOnlyExtensions: [...agent.subagentOnlyExtensions] } : {}),
|
|
101
|
+
...(agent.completionGuard !== undefined ? { completionGuard: agent.completionGuard } : {}),
|
|
102
|
+
...(agent.toolBudget !== undefined ? { toolBudget: agent.toolBudget } : {}),
|
|
103
103
|
};
|
|
104
104
|
}
|
|
105
105
|
|
|
@@ -290,7 +290,9 @@ async function saveAgentModel(ctx: ExtensionContext, agent: AgentConfig, selecte
|
|
|
290
290
|
|
|
291
291
|
const readOnlyMessage = readOnlyAgentMessage(agent, "model");
|
|
292
292
|
if (readOnlyMessage) return readOnlyMessage;
|
|
293
|
-
const updated
|
|
293
|
+
const updated = editableAgentConfig(agent);
|
|
294
|
+
if (selectedModel === undefined) delete updated.model;
|
|
295
|
+
else updated.model = selectedModel;
|
|
294
296
|
fs.writeFileSync(updated.filePath, serializeAgent(updated, {
|
|
295
297
|
preserveFrontmatterFields: preservedAgentFrontmatterFields(agent, { model: selectedModel }),
|
|
296
298
|
}), "utf-8");
|
|
@@ -311,7 +313,9 @@ async function saveAgentThinking(ctx: ExtensionContext, agent: AgentConfig, sele
|
|
|
311
313
|
|
|
312
314
|
const readOnlyMessage = readOnlyAgentMessage(agent, "thinking");
|
|
313
315
|
if (readOnlyMessage) return readOnlyMessage;
|
|
314
|
-
const updated
|
|
316
|
+
const updated = editableAgentConfig(agent);
|
|
317
|
+
if (selectedThinking === undefined) delete updated.thinking;
|
|
318
|
+
else updated.thinking = selectedThinking;
|
|
315
319
|
fs.writeFileSync(updated.filePath, serializeAgent(updated, {
|
|
316
320
|
preserveFrontmatterFields: preservedAgentFrontmatterFields(agent, { thinking: selectedThinking }),
|
|
317
321
|
}), "utf-8");
|
package/src/tui/fleet-status.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { type EditorComponent, isKeyRelease, Key, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
3
3
|
import { formatModelThinking } from "../shared/formatters.ts";
|
|
4
|
-
import type { AsyncJobStep, FleetViewPlacement, SubagentState } from "../shared/types.ts";
|
|
4
|
+
import type { AsyncJobStep, FleetViewPlacement, NestedRunSummary, NestedStepSummary, SubagentState } from "../shared/types.ts";
|
|
5
|
+
import { formatWorkflowJsonPreview } from "../workflows/scripted-workflow.ts";
|
|
5
6
|
|
|
6
7
|
export const FLEET_STATUS_WIDGET_KEY = "subagent-fleet-status";
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
// Six rows fit the accepted collapsed hierarchy: one owner, four direct leaves, and overflow.
|
|
10
|
+
const MAX_AGENT_ROWS = 6;
|
|
9
11
|
const REFRESH_MS = 500;
|
|
10
12
|
|
|
11
13
|
type Theme = ExtensionContext["ui"]["theme"];
|
|
@@ -19,8 +21,23 @@ type FleetStatusEntry = {
|
|
|
19
21
|
description?: string;
|
|
20
22
|
startedAt: number;
|
|
21
23
|
tokens: number;
|
|
24
|
+
state: string;
|
|
25
|
+
nestedChildren?: NestedRunSummary[];
|
|
22
26
|
};
|
|
23
27
|
|
|
28
|
+
type FleetNestedRow = {
|
|
29
|
+
name: string;
|
|
30
|
+
state: NestedRunSummary["state"] | NestedStepSummary["status"];
|
|
31
|
+
modelThinking?: string;
|
|
32
|
+
activity?: string;
|
|
33
|
+
startedAt?: number;
|
|
34
|
+
overflow?: number;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
type FleetTreeRow =
|
|
38
|
+
| { kind: "owner"; entry: FleetStatusEntry }
|
|
39
|
+
| { kind: "nested"; ownerKey: string; row: FleetNestedRow; last: boolean };
|
|
40
|
+
|
|
24
41
|
export interface FleetStatusOptions {
|
|
25
42
|
refreshMs?: number;
|
|
26
43
|
maxAgentRows?: number;
|
|
@@ -55,6 +72,67 @@ function isActiveState(value: string): boolean {
|
|
|
55
72
|
return value === "running" || value === "queued" || value === "pending";
|
|
56
73
|
}
|
|
57
74
|
|
|
75
|
+
function nestedRunLabel(run: NestedRunSummary): string {
|
|
76
|
+
if (run.agent) return run.agent;
|
|
77
|
+
if (run.agents?.length) return run.agents.length === 1 ? run.agents[0]! : `${run.agents.slice(0, 2).join(", ")}${run.agents.length > 2 ? ` +${run.agents.length - 2}` : ""}`;
|
|
78
|
+
return run.id;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function nestedActivity(node: NestedRunSummary | NestedStepSummary): string | undefined {
|
|
82
|
+
if (node.currentTool) return `tool ${node.currentTool}`;
|
|
83
|
+
if (node.currentPath) return node.currentPath.split(/[\\/]/).at(-1);
|
|
84
|
+
if (node.activityState === "needs_attention") return "needs attention";
|
|
85
|
+
if (node.activityState === "active_long_running") return "long-running";
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function nestedStatusGlyph(state: FleetNestedRow["state"], theme: Theme): string {
|
|
90
|
+
if (state === "running") return theme.fg("accent", "●");
|
|
91
|
+
if (state === "queued" || state === "pending") return theme.fg("muted", "◦");
|
|
92
|
+
if (state === "complete" || state === "completed") return theme.fg("success", "✓");
|
|
93
|
+
if (state === "failed" || state === "rejected") return theme.fg("error", "✗");
|
|
94
|
+
return theme.fg("warning", "■");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function nestedFleetRows(children: NestedRunSummary[] | undefined): FleetNestedRow[] {
|
|
98
|
+
const rows: FleetNestedRow[] = [];
|
|
99
|
+
for (const child of children ?? []) {
|
|
100
|
+
const steps = (child.mode === "parallel" || child.mode === "chain") ? child.steps ?? [] : [];
|
|
101
|
+
if (steps.length > 0) {
|
|
102
|
+
for (const step of steps) {
|
|
103
|
+
rows.push({
|
|
104
|
+
name: step.agent,
|
|
105
|
+
state: step.status,
|
|
106
|
+
...(formatModelThinking(step.model, step.thinking) ? { modelThinking: formatModelThinking(step.model, step.thinking) } : {}),
|
|
107
|
+
...(nestedActivity(step) ? { activity: nestedActivity(step) } : {}),
|
|
108
|
+
...(step.startedAt !== undefined ? { startedAt: step.startedAt } : {}),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
rows.push({
|
|
114
|
+
name: nestedRunLabel(child),
|
|
115
|
+
state: child.state,
|
|
116
|
+
...(formatModelThinking(child.model, child.thinking) ? { modelThinking: formatModelThinking(child.model, child.thinking) } : {}),
|
|
117
|
+
...(nestedActivity(child) ? { activity: nestedActivity(child) } : {}),
|
|
118
|
+
...(child.startedAt !== undefined ? { startedAt: child.startedAt } : {}),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
const visible = rows.slice(0, 4);
|
|
122
|
+
if (rows.length > visible.length) visible.push({ name: `… +${rows.length - visible.length} more nested leaves`, state: "complete", overflow: rows.length - visible.length });
|
|
123
|
+
return visible;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function fleetTreeRows(entries: FleetStatusEntry[]): FleetTreeRow[] {
|
|
127
|
+
const rows: FleetTreeRow[] = [];
|
|
128
|
+
for (const entry of entries) {
|
|
129
|
+
rows.push({ kind: "owner", entry });
|
|
130
|
+
const nested = nestedFleetRows(entry.nestedChildren);
|
|
131
|
+
for (const [index, row] of nested.entries()) rows.push({ kind: "nested", ownerKey: entry.key, row, last: index === nested.length - 1 });
|
|
132
|
+
}
|
|
133
|
+
return rows;
|
|
134
|
+
}
|
|
135
|
+
|
|
58
136
|
function isStaleExtensionContextError(error: unknown): boolean {
|
|
59
137
|
// Pi currently exposes stale contexts as plain Errors without a stable code or subtype.
|
|
60
138
|
return error instanceof Error
|
|
@@ -75,6 +153,10 @@ export function collectFleetStatusEntries(state: SubagentState): FleetStatusEntr
|
|
|
75
153
|
description: child.description,
|
|
76
154
|
startedAt: child.startedAt,
|
|
77
155
|
tokens: child.tokens ?? 0,
|
|
156
|
+
state: "running",
|
|
157
|
+
...((control.nestedChildren?.filter((nested) => nested.parentStepIndex === child.index).length ?? 0) > 0
|
|
158
|
+
? { nestedChildren: control.nestedChildren!.filter((nested) => nested.parentStepIndex === child.index) }
|
|
159
|
+
: control.activeChildren.size === 1 && control.nestedChildren?.length ? { nestedChildren: control.nestedChildren } : {}),
|
|
78
160
|
});
|
|
79
161
|
}
|
|
80
162
|
continue;
|
|
@@ -87,12 +169,27 @@ export function collectFleetStatusEntries(state: SubagentState): FleetStatusEntr
|
|
|
87
169
|
description: control.description,
|
|
88
170
|
startedAt: control.startedAt,
|
|
89
171
|
tokens: control.tokens ?? 0,
|
|
172
|
+
state: "running",
|
|
173
|
+
...(control.nestedChildren?.length ? { nestedChildren: control.nestedChildren } : {}),
|
|
90
174
|
});
|
|
91
175
|
}
|
|
92
176
|
|
|
93
177
|
for (const job of state.asyncJobs.values()) {
|
|
94
178
|
if (!isActiveState(job.status)) continue;
|
|
95
179
|
const startedAt = job.startedAt ?? job.updatedAt ?? Date.now();
|
|
180
|
+
if (job.mode === "workflow") {
|
|
181
|
+
const latestEmit = job.workflow?.emits?.length ? formatWorkflowJsonPreview(job.workflow.emits.at(-1), 120) : undefined;
|
|
182
|
+
entries.push({
|
|
183
|
+
key: `async:${job.asyncId}`,
|
|
184
|
+
agent: "workflow",
|
|
185
|
+
description: latestEmit !== undefined ? `latest emit: ${latestEmit}` : job.description,
|
|
186
|
+
startedAt,
|
|
187
|
+
tokens: job.totalTokens?.total ?? 0,
|
|
188
|
+
state: job.status,
|
|
189
|
+
...(job.nestedChildren?.length ? { nestedChildren: job.nestedChildren } : {}),
|
|
190
|
+
});
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
96
193
|
const steps: AsyncJobStep[] | undefined = job.steps?.length
|
|
97
194
|
? job.steps
|
|
98
195
|
: job.agents?.map((agent, index) => {
|
|
@@ -107,6 +204,8 @@ export function collectFleetStatusEntries(state: SubagentState): FleetStatusEntr
|
|
|
107
204
|
description: job.description,
|
|
108
205
|
startedAt,
|
|
109
206
|
tokens: job.totalTokens?.total ?? 0,
|
|
207
|
+
state: job.status,
|
|
208
|
+
...(job.nestedChildren?.length ? { nestedChildren: job.nestedChildren } : {}),
|
|
110
209
|
});
|
|
111
210
|
continue;
|
|
112
211
|
}
|
|
@@ -122,6 +221,12 @@ export function collectFleetStatusEntries(state: SubagentState): FleetStatusEntr
|
|
|
122
221
|
description: step.description ?? job.description,
|
|
123
222
|
startedAt: step.startedAt ?? startedAt,
|
|
124
223
|
tokens: step.tokens?.total ?? (steps.length === 1 ? job.totalTokens?.total ?? 0 : 0),
|
|
224
|
+
state: step.status,
|
|
225
|
+
...((step.children?.length ?? 0) > 0
|
|
226
|
+
? { nestedChildren: step.children }
|
|
227
|
+
: job.nestedChildren?.filter((nested) => nested.parentStepIndex === index).length
|
|
228
|
+
? { nestedChildren: job.nestedChildren.filter((nested) => nested.parentStepIndex === index) }
|
|
229
|
+
: {}),
|
|
125
230
|
});
|
|
126
231
|
}
|
|
127
232
|
}
|
|
@@ -302,21 +407,30 @@ export class SubagentFleetStatus {
|
|
|
302
407
|
|
|
303
408
|
render(width: number, theme: Theme): string[] {
|
|
304
409
|
if (this.entries.length === 0) return [];
|
|
410
|
+
if (!this.active) {
|
|
411
|
+
const tokens = this.entries.reduce((total, entry) => total + entry.tokens, 0);
|
|
412
|
+
const label = `${this.entries.length} active ${this.entries.length === 1 ? "agent" : "agents"}`;
|
|
413
|
+
return [truncateToWidth(` ${theme.fg("muted", label)} · ${theme.fg("dim", `${formatFleetTokens(tokens)} · ↓/← to inspect`)}`, width)];
|
|
414
|
+
}
|
|
305
415
|
const roster = this.rosterKeys();
|
|
306
416
|
const selectedIndex = Math.max(0, roster.indexOf(this.selectedKey));
|
|
307
|
-
const
|
|
308
|
-
? "↑↓/jk select · enter inspect · esc back"
|
|
309
|
-
: "esc to interrupt · ← for agents · ↓ to manage";
|
|
310
|
-
const lines = [truncateToWidth(` ${theme.fg("dim", hint)}`, width), ""];
|
|
417
|
+
const lines = [truncateToWidth(` ${theme.fg("dim", "↑↓/jk select · enter inspect · esc back")}`, width), ""];
|
|
311
418
|
lines.push(truncateToWidth(` ${this.bullet(0, selectedIndex, theme)} main`, width));
|
|
312
419
|
|
|
313
|
-
const
|
|
314
|
-
const
|
|
315
|
-
const
|
|
316
|
-
const
|
|
420
|
+
const tree = fleetTreeRows(this.entries);
|
|
421
|
+
const selectedTreeIndex = Math.max(0, tree.findIndex((row) => row.kind === "owner" && row.entry.key === this.selectedKey));
|
|
422
|
+
const visibleCount = Math.min(this.maxAgentRows, tree.length);
|
|
423
|
+
const start = selectedTreeIndex < visibleCount ? 0 : selectedTreeIndex - visibleCount + 1;
|
|
424
|
+
const hiddenBelow = tree.length - (start + visibleCount);
|
|
317
425
|
if (start > 0) lines.push(rightAlign("", theme.fg("dim", `↑ ${start} more`), width));
|
|
318
426
|
for (let index = start; index < start + visibleCount; index++) {
|
|
319
|
-
|
|
427
|
+
const row = tree[index]!;
|
|
428
|
+
if (row.kind === "owner") {
|
|
429
|
+
const ownerIndex = this.entries.findIndex((entry) => entry.key === row.entry.key);
|
|
430
|
+
lines.push(this.renderEntry(ownerIndex + 1, selectedIndex, row.entry, width, theme));
|
|
431
|
+
} else {
|
|
432
|
+
lines.push(this.renderNestedRow(row.row, row.last, width, theme));
|
|
433
|
+
}
|
|
320
434
|
}
|
|
321
435
|
if (hiddenBelow > 0) lines.push(rightAlign("", theme.fg("dim", `↓ ${hiddenBelow} more`), width));
|
|
322
436
|
return lines;
|
|
@@ -325,14 +439,24 @@ export class SubagentFleetStatus {
|
|
|
325
439
|
private renderEntry(rosterIndex: number, selectedIndex: number, entry: FleetStatusEntry, width: number, theme: Theme): string {
|
|
326
440
|
const description = entry.description?.replace(/\s+/g, " ").trim();
|
|
327
441
|
const agent = entry.modelThinking ? `${entry.agent} (${entry.modelThinking})` : entry.agent;
|
|
328
|
-
const left = ` ${this.bullet(rosterIndex, selectedIndex, theme)} ${theme.fg("muted", agent)}${description ? ` ${description}` : ""}`;
|
|
442
|
+
const left = ` ${this.bullet(rosterIndex, selectedIndex, theme)} ${theme.fg("muted", agent)} · ${entry.state}${description ? ` ${description}` : ""}`;
|
|
329
443
|
const elapsed = Date.now() - entry.startedAt;
|
|
330
444
|
const right = theme.fg("dim", `${formatFleetElapsed(elapsed)} · ${formatFleetTokens(entry.tokens)}`);
|
|
331
445
|
return rightAlign(left, right, width);
|
|
332
446
|
}
|
|
333
447
|
|
|
448
|
+
private renderNestedRow(row: FleetNestedRow, last: boolean, width: number, theme: Theme): string {
|
|
449
|
+
const marker = last ? "└─" : "├─";
|
|
450
|
+
if (row.overflow !== undefined) return truncateToWidth(` ${marker} ${theme.fg("dim", `+${row.overflow} nested leaves`)}`, width);
|
|
451
|
+
const modelThinking = row.modelThinking ? ` (${row.modelThinking})` : "";
|
|
452
|
+
const activity = row.activity ? ` · ${row.activity}` : "";
|
|
453
|
+
const left = ` ${marker} ${nestedStatusGlyph(row.state, theme)} ${theme.fg("muted", `${row.name}${modelThinking}`)} · ${row.state}${activity}`;
|
|
454
|
+
const elapsed = row.startedAt !== undefined ? ` · ${formatFleetElapsed(Date.now() - row.startedAt)}` : "";
|
|
455
|
+
return truncateToWidth(`${left}${theme.fg("dim", elapsed)}`, width);
|
|
456
|
+
}
|
|
457
|
+
|
|
334
458
|
private bullet(rosterIndex: number, selectedIndex: number, theme: Theme): string {
|
|
335
|
-
return rosterIndex === selectedIndex ? theme.fg("accent", "
|
|
459
|
+
return rosterIndex === selectedIndex ? theme.fg("accent", ">") : " ";
|
|
336
460
|
}
|
|
337
461
|
|
|
338
462
|
private rosterKeys(): string[] {
|
|
@@ -368,14 +492,25 @@ export class SubagentFleetStatus {
|
|
|
368
492
|
active: this.active,
|
|
369
493
|
selected: this.selectedKey,
|
|
370
494
|
inspectorOpen: this.inspectorOpen,
|
|
371
|
-
entries: this.entries.map((entry) =>
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
495
|
+
entries: this.entries.map((entry) => this.active
|
|
496
|
+
? [
|
|
497
|
+
entry.key,
|
|
498
|
+
entry.agent,
|
|
499
|
+
entry.state,
|
|
500
|
+
entry.modelThinking,
|
|
501
|
+
entry.description,
|
|
502
|
+
Math.round((now - entry.startedAt) / 1000),
|
|
503
|
+
entry.tokens,
|
|
504
|
+
entry.nestedChildren?.map((child) => [
|
|
505
|
+
child.id,
|
|
506
|
+
child.state,
|
|
507
|
+
child.model,
|
|
508
|
+
child.thinking,
|
|
509
|
+
child.lastUpdate,
|
|
510
|
+
child.steps?.map((step) => [step.agent, step.status, step.model, step.thinking, step.lastActivityAt]),
|
|
511
|
+
]),
|
|
512
|
+
]
|
|
513
|
+
: [entry.key, entry.state, entry.tokens]),
|
|
379
514
|
});
|
|
380
515
|
}
|
|
381
516
|
|
|
@@ -7,6 +7,89 @@ const DEFAULT_MAX_RECORDS = 240;
|
|
|
7
7
|
const DEFAULT_MAX_BYTES = 2 * 1024 * 1024;
|
|
8
8
|
const MAX_MESSAGE_CHARS = 64 * 1024;
|
|
9
9
|
const TOOL_PREVIEW_LINES = 7;
|
|
10
|
+
const BINARY_CONTENT_PLACEHOLDER = "[binary content omitted for safe display]";
|
|
11
|
+
|
|
12
|
+
function isUnsafeDisplayCodePoint(codePoint: number): boolean {
|
|
13
|
+
const terminalControl = (codePoint <= 0x1f && codePoint !== 0x09 && codePoint !== 0x0a)
|
|
14
|
+
|| (codePoint >= 0x7f && codePoint <= 0x9f);
|
|
15
|
+
const bidiControl = codePoint === 0x061c
|
|
16
|
+
|| codePoint === 0x200e
|
|
17
|
+
|| codePoint === 0x200f
|
|
18
|
+
|| (codePoint >= 0x202a && codePoint <= 0x202e)
|
|
19
|
+
|| (codePoint >= 0x2066 && codePoint <= 0x2069);
|
|
20
|
+
const privateUse = (codePoint >= 0xe000 && codePoint <= 0xf8ff)
|
|
21
|
+
|| (codePoint >= 0xf0000 && codePoint <= 0xffffd)
|
|
22
|
+
|| (codePoint >= 0x100000 && codePoint <= 0x10fffd);
|
|
23
|
+
const invalidScalar = codePoint >= 0xd800 && codePoint <= 0xdfff;
|
|
24
|
+
const nonCharacter = (codePoint >= 0xfdd0 && codePoint <= 0xfdef)
|
|
25
|
+
|| (codePoint & 0xffff) === 0xfffe
|
|
26
|
+
|| (codePoint & 0xffff) === 0xffff;
|
|
27
|
+
return terminalControl || bidiControl || privateUse || invalidScalar || nonCharacter;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function looksLikeBinaryContent(text: string): boolean {
|
|
31
|
+
if (text.includes("\0")) return true;
|
|
32
|
+
let suspiciousControls = 0;
|
|
33
|
+
let replacementCharacters = 0;
|
|
34
|
+
let codePoints = 0;
|
|
35
|
+
for (const character of text) {
|
|
36
|
+
codePoints++;
|
|
37
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
38
|
+
if ((codePoint <= 0x08) || (codePoint >= 0x0e && codePoint <= 0x1f)) suspiciousControls++;
|
|
39
|
+
if (codePoint === 0xfffd) replacementCharacters++;
|
|
40
|
+
}
|
|
41
|
+
if (codePoints === 0) return false;
|
|
42
|
+
return (suspiciousControls >= 4 && suspiciousControls / codePoints >= 0.1)
|
|
43
|
+
|| (replacementCharacters >= 3 && replacementCharacters / codePoints >= 0.1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function safeDisplayText(text: string): string {
|
|
47
|
+
const normalized = text.replace(/\r\n/g, "\n");
|
|
48
|
+
if (looksLikeBinaryContent(normalized)) return BINARY_CONTENT_PLACEHOLDER;
|
|
49
|
+
let safe = "";
|
|
50
|
+
for (const character of normalized) {
|
|
51
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
52
|
+
safe += isUnsafeDisplayCodePoint(codePoint)
|
|
53
|
+
? `[U+${codePoint.toString(16).toUpperCase().padStart(4, "0")}]`
|
|
54
|
+
: character;
|
|
55
|
+
}
|
|
56
|
+
return safe;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function sanitizeJsonDisplayValue(value: unknown): { value: unknown; changed: boolean } {
|
|
60
|
+
if (typeof value === "string") {
|
|
61
|
+
const safe = safeDisplayText(value);
|
|
62
|
+
return { value: safe, changed: safe !== value };
|
|
63
|
+
}
|
|
64
|
+
if (Array.isArray(value)) {
|
|
65
|
+
const sanitized = value.map(sanitizeJsonDisplayValue);
|
|
66
|
+
return {
|
|
67
|
+
value: sanitized.map((entry) => entry.value),
|
|
68
|
+
changed: sanitized.some((entry) => entry.changed),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
if (value && typeof value === "object") {
|
|
72
|
+
let changed = false;
|
|
73
|
+
const sanitized: Record<string, unknown> = Object.create(null);
|
|
74
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
75
|
+
const safeKey = safeDisplayText(key);
|
|
76
|
+
const safeValue = sanitizeJsonDisplayValue(nested);
|
|
77
|
+
sanitized[safeKey] = safeValue.value;
|
|
78
|
+
changed ||= safeKey !== key || safeValue.changed;
|
|
79
|
+
}
|
|
80
|
+
return { value: changed ? sanitized : value, changed };
|
|
81
|
+
}
|
|
82
|
+
return { value, changed: false };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function safeToolArgsPayload(payload: string): string {
|
|
86
|
+
try {
|
|
87
|
+
const sanitized = sanitizeJsonDisplayValue(JSON.parse(payload));
|
|
88
|
+
return sanitized.changed ? JSON.stringify(sanitized.value) : safeDisplayText(payload);
|
|
89
|
+
} catch {
|
|
90
|
+
return safeDisplayText(payload);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
10
93
|
|
|
11
94
|
type Theme = ExtensionContext["ui"]["theme"];
|
|
12
95
|
|
|
@@ -142,6 +225,27 @@ function clipMessage(text: string): string {
|
|
|
142
225
|
return `${text.slice(0, MAX_MESSAGE_CHARS)}\n\n… message truncated`;
|
|
143
226
|
}
|
|
144
227
|
|
|
228
|
+
function safeTranscriptEvent(event: FleetTranscriptEvent): FleetTranscriptEvent {
|
|
229
|
+
if (event.kind === "assistant") {
|
|
230
|
+
return {
|
|
231
|
+
...event,
|
|
232
|
+
text: safeDisplayText(event.text),
|
|
233
|
+
...(event.model ? { model: safeDisplayText(event.model) } : {}),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
if (event.kind === "user" || event.kind === "notice") {
|
|
237
|
+
return { ...event, text: safeDisplayText(event.text) };
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
...event,
|
|
241
|
+
name: safeDisplayText(event.name),
|
|
242
|
+
...(event.args !== undefined ? { args: safeDisplayText(event.args) } : {}),
|
|
243
|
+
...(event.argsPayload !== undefined ? { argsPayload: safeToolArgsPayload(event.argsPayload) } : {}),
|
|
244
|
+
...(event.output !== undefined ? { output: safeDisplayText(event.output) } : {}),
|
|
245
|
+
...(event.error !== undefined ? { error: safeDisplayText(event.error) } : {}),
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
145
249
|
function findTool(
|
|
146
250
|
events: FleetTranscriptEvent[],
|
|
147
251
|
toolCallId: string | undefined,
|
|
@@ -274,13 +378,13 @@ function parseTranscriptLines(lines: string[], conversationStarted = false): { e
|
|
|
274
378
|
for (const event of events) {
|
|
275
379
|
if (event.kind === "tool") delete (event as MutableToolEvent).resultSeen;
|
|
276
380
|
}
|
|
277
|
-
return { events, malformed, explicitTruncation };
|
|
381
|
+
return { events: events.map(safeTranscriptEvent), malformed, explicitTruncation };
|
|
278
382
|
}
|
|
279
383
|
|
|
280
384
|
export function readFleetTranscript(filePath: string, options: FleetTranscriptReadOptions): FleetTranscript {
|
|
281
385
|
const validated = validateTranscriptPath(filePath, options.trustedRoots);
|
|
282
386
|
if (!validated.resolvedPath) {
|
|
283
|
-
return { path: filePath, events: [], truncated: false, ...(validated.warning ? { warning: validated.warning } : {}) };
|
|
387
|
+
return { path: filePath, events: [], truncated: false, ...(validated.warning ? { warning: safeDisplayText(validated.warning) } : {}) };
|
|
284
388
|
}
|
|
285
389
|
const maxRecords = Math.max(1, options.maxRecords ?? DEFAULT_MAX_RECORDS);
|
|
286
390
|
const tail = readTailLines(validated.resolvedPath, Math.max(1024, options.maxBytes ?? DEFAULT_MAX_BYTES));
|
|
@@ -295,7 +399,7 @@ export function readFleetTranscript(filePath: string, options: FleetTranscriptRe
|
|
|
295
399
|
path: filePath,
|
|
296
400
|
events: parsed.events,
|
|
297
401
|
truncated: tail.truncated || tail.lines.length > maxRecords || parsed.explicitTruncation,
|
|
298
|
-
...(warnings.length ? { warning: warnings.join(" ") } : {}),
|
|
402
|
+
...(warnings.length ? { warning: safeDisplayText(warnings.join(" ")) } : {}),
|
|
299
403
|
};
|
|
300
404
|
}
|
|
301
405
|
|
|
@@ -404,12 +508,13 @@ export function renderFleetTranscript(
|
|
|
404
508
|
const lines: string[] = [];
|
|
405
509
|
if (transcript.truncated) lines.push(bounded(theme.fg("dim", "↑ Earlier activity omitted"), width));
|
|
406
510
|
if (transcript.warning) {
|
|
407
|
-
for (const line of renderWrapped(transcript.warning, Math.max(1, width - 2))) {
|
|
511
|
+
for (const line of renderWrapped(safeDisplayText(transcript.warning), Math.max(1, width - 2))) {
|
|
408
512
|
lines.push(bounded(`${theme.fg("warning", "!")} ${theme.fg("warning", line)}`, width));
|
|
409
513
|
}
|
|
410
514
|
}
|
|
411
515
|
|
|
412
|
-
for (const
|
|
516
|
+
for (const rawEvent of transcript.events) {
|
|
517
|
+
const event = safeTranscriptEvent(rawEvent);
|
|
413
518
|
if (event.kind === "tool") {
|
|
414
519
|
if (options.expandedTools && (event.output || event.argsPayload || event.error)) {
|
|
415
520
|
lines.push(...renderExpandedTool(event, width, theme));
|
package/src/tui/fleet.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { getMarkdownTheme, type ExtensionContext } from "@earendil-works/pi-codi
|
|
|
5
5
|
import { Key, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi, type Component, type MarkdownTheme } from "@earendil-works/pi-tui";
|
|
6
6
|
import { getArtifactPaths, getArtifactsDir } from "../shared/artifacts.ts";
|
|
7
7
|
import { formatDuration, formatModelThinking, formatTokens, shortenPath } from "../shared/formatters.ts";
|
|
8
|
-
import {
|
|
8
|
+
import { DIRS, type AsyncJobState, type Details, type ForegroundChildControl, type ForegroundResumeChild, type ForegroundResumeRun, type ForegroundRunControl, type SubagentState } from "../shared/types.ts";
|
|
9
9
|
import { readStatus } from "../shared/utils.ts";
|
|
10
10
|
import { formatAsyncRunTranscript } from "../runs/background/fleet-view.ts";
|
|
11
11
|
import { listAsyncRuns, type AsyncRunSummary } from "../runs/background/async-status.ts";
|
|
@@ -14,9 +14,11 @@ import { stopAsyncRun } from "../runs/foreground/async-stop-action.ts";
|
|
|
14
14
|
import { contextModeBadge, contextModeLabel } from "../runs/shared/context-mode.ts";
|
|
15
15
|
import { FLEET_STATUS_WIDGET_KEY } from "./fleet-status.ts";
|
|
16
16
|
import { readFleetTranscript, renderFleetTranscript, type FleetTranscript } from "./fleet-transcript.ts";
|
|
17
|
+
import { handleHerdrInspectorAction } from "../inspectors/herdr/actions.ts";
|
|
17
18
|
|
|
18
19
|
const REFRESH_MS = 750;
|
|
19
20
|
const MAX_RECENT_ASYNC_RUNS = 20;
|
|
21
|
+
const MAX_FLEET_HISTORY_CANDIDATES = 100;
|
|
20
22
|
const TRANSCRIPT_LINES = 200;
|
|
21
23
|
|
|
22
24
|
type Theme = ExtensionContext["ui"]["theme"];
|
|
@@ -45,6 +47,7 @@ export interface FleetActionResult {
|
|
|
45
47
|
export interface FleetActionHandlers {
|
|
46
48
|
steer(input: { runId: string; asyncDir: string; index?: number; message: string }): Promise<FleetActionResult>;
|
|
47
49
|
stop(input: { runId: string; asyncDir: string; index?: number }): Promise<FleetActionResult> | FleetActionResult;
|
|
50
|
+
inspect?(input: { runId: string; asyncDir: string; index?: number }): Promise<FleetActionResult>;
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
export interface FleetViewOptions {
|
|
@@ -104,7 +107,7 @@ function trackedJobSummary(job: AsyncJobState): AsyncRunSummary {
|
|
|
104
107
|
|
|
105
108
|
function asyncItems(run: AsyncRunSummary, description?: string): FleetItem[] {
|
|
106
109
|
const updatedAt = run.lastUpdate ?? run.endedAt ?? run.startedAt;
|
|
107
|
-
if (run.steps.length === 0) {
|
|
110
|
+
if (run.steps.length === 0 || run.mode === "workflow") {
|
|
108
111
|
return [{ key: `async:${run.id}`, kind: "async", runId: run.id, agent: run.mode, state: run.state, updatedAt, run, ...(description ? { description } : {}) }];
|
|
109
112
|
}
|
|
110
113
|
return run.steps.map((step) => ({
|
|
@@ -121,6 +124,14 @@ function asyncItems(run: AsyncRunSummary, description?: string): FleetItem[] {
|
|
|
121
124
|
}));
|
|
122
125
|
}
|
|
123
126
|
|
|
127
|
+
function orderFleetAsyncRuns(runs: AsyncRunSummary[], terminalLimit: number): AsyncRunSummary[] {
|
|
128
|
+
const updatedAt = (run: AsyncRunSummary) => run.lastUpdate ?? run.endedAt ?? run.startedAt;
|
|
129
|
+
const byNewest = (left: AsyncRunSummary, right: AsyncRunSummary) => updatedAt(right) - updatedAt(left);
|
|
130
|
+
const active = runs.filter((run) => run.state === "queued" || run.state === "running").sort(byNewest);
|
|
131
|
+
const terminal = runs.filter((run) => run.state !== "queued" && run.state !== "running").sort(byNewest);
|
|
132
|
+
return [...active, ...terminal.slice(0, terminalLimit)];
|
|
133
|
+
}
|
|
134
|
+
|
|
124
135
|
export function collectFleetSnapshot(
|
|
125
136
|
state: SubagentState,
|
|
126
137
|
options: { asyncDirRoot?: string; resultsDir?: string; limit?: number } = {},
|
|
@@ -163,30 +174,35 @@ export function collectFleetSnapshot(
|
|
|
163
174
|
try {
|
|
164
175
|
let runs: AsyncRunSummary[];
|
|
165
176
|
const descriptions = new Map<string, string>();
|
|
177
|
+
const tracked = [...(state.fleetJobs ?? state.asyncJobs).values()]
|
|
178
|
+
.filter((job) => belongsToCurrentSession(job.sessionId, state.currentSessionId));
|
|
179
|
+
const byUpdate = (left: AsyncJobState, right: AsyncJobState) => (right.updatedAt ?? right.startedAt ?? 0) - (left.updatedAt ?? left.startedAt ?? 0);
|
|
180
|
+
const active = tracked.filter((job) => job.status === "queued" || job.status === "running").sort(byUpdate);
|
|
181
|
+
const recent = tracked.filter((job) => job.status !== "queued" && job.status !== "running").sort(byUpdate).slice(0, options.limit ?? MAX_RECENT_ASYNC_RUNS);
|
|
182
|
+
const trackedRuns: AsyncRunSummary[] = [];
|
|
183
|
+
for (const job of [...active, ...recent]) {
|
|
184
|
+
try {
|
|
185
|
+
trackedRuns.push(trackedJobSummary(job));
|
|
186
|
+
if (job.description) descriptions.set(job.asyncId, job.description);
|
|
187
|
+
} catch (cause) {
|
|
188
|
+
error = `Failed to inspect async run '${job.asyncId}': ${cause instanceof Error ? cause.message : String(cause)}`;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
166
191
|
if (options.asyncDirRoot !== undefined) {
|
|
167
|
-
|
|
192
|
+
const trackedIds = new Set(trackedRuns.map((run) => run.id));
|
|
193
|
+
const history = listAsyncRuns(options.asyncDirRoot, {
|
|
168
194
|
...(state.currentSessionId ? { sessionId: state.currentSessionId } : {}),
|
|
169
|
-
|
|
170
|
-
resultsDir: options.resultsDir ??
|
|
195
|
+
entryLimit: MAX_FLEET_HISTORY_CANDIDATES,
|
|
196
|
+
resultsDir: options.resultsDir ?? DIRS.results,
|
|
171
197
|
reconcile: false,
|
|
172
|
-
});
|
|
198
|
+
}).filter((run) => !trackedIds.has(run.id));
|
|
199
|
+
runs = [...trackedRuns, ...history];
|
|
173
200
|
} else {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const recent = tracked.filter((job) => job.status !== "queued" && job.status !== "running").sort(byUpdate).slice(0, options.limit ?? MAX_RECENT_ASYNC_RUNS);
|
|
179
|
-
runs = [];
|
|
180
|
-
for (const job of [...active, ...recent]) {
|
|
181
|
-
try {
|
|
182
|
-
runs.push(trackedJobSummary(job));
|
|
183
|
-
if (job.description) descriptions.set(job.asyncId, job.description);
|
|
184
|
-
} catch (cause) {
|
|
185
|
-
error = `Failed to inspect async run '${job.asyncId}': ${cause instanceof Error ? cause.message : String(cause)}`;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
201
|
+
runs = trackedRuns;
|
|
202
|
+
}
|
|
203
|
+
for (const run of orderFleetAsyncRuns(runs, options.limit ?? MAX_RECENT_ASYNC_RUNS)) {
|
|
204
|
+
items.push(...asyncItems(run, descriptions.get(run.id)));
|
|
188
205
|
}
|
|
189
|
-
for (const run of runs) items.push(...asyncItems(run, descriptions.get(run.id)));
|
|
190
206
|
} catch (cause) {
|
|
191
207
|
error = cause instanceof Error ? cause.message : String(cause);
|
|
192
208
|
}
|
|
@@ -390,7 +406,7 @@ function itemStats(item: FleetItem): string[] {
|
|
|
390
406
|
tokens = item.child.tokens;
|
|
391
407
|
tools = item.child.toolCount;
|
|
392
408
|
} else {
|
|
393
|
-
model = item.step?.model;
|
|
409
|
+
model = formatModelThinking(item.step?.model, item.step?.thinking) || undefined;
|
|
394
410
|
tokens = item.step?.tokens?.total ?? (item.index === undefined ? item.run.totalTokens?.total : undefined);
|
|
395
411
|
tools = item.step?.toolCount ?? (item.index === undefined ? item.run.toolCount : undefined);
|
|
396
412
|
const terminalRun = item.state !== "queued" && item.state !== "running" && item.state !== "pending";
|
|
@@ -662,6 +678,12 @@ export class SubagentFleetComponent implements Component {
|
|
|
662
678
|
}
|
|
663
679
|
return;
|
|
664
680
|
}
|
|
681
|
+
if (data === "H") {
|
|
682
|
+
const target = this.selectedAsyncAction();
|
|
683
|
+
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 });
|
|
684
|
+
else this.runAction(() => this.options.actions!.inspect!({ runId: target.item.runId, asyncDir: target.item.run.asyncDir, ...(target.item.index !== undefined ? { index: target.item.index } : {}) }));
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
665
687
|
if (data === "D") {
|
|
666
688
|
const target = this.selectedAsyncAction();
|
|
667
689
|
if ("reason" in target || !this.options.actions) this.setActionNotice({ text: "reason" in target ? target.reason : "Fleet controls are unavailable in this context.", isError: true });
|
|
@@ -792,7 +814,7 @@ export class SubagentFleetComponent implements Component {
|
|
|
792
814
|
}
|
|
793
815
|
lines.push(this.theme.fg("border", `├${"─".repeat(rosterWidth)}┴${"─".repeat(detailWidth)}┤`));
|
|
794
816
|
const position = this.snapshot.items.length ? `${this.selected + 1}/${this.snapshot.items.length}` : "0/0";
|
|
795
|
-
const footer = ` ↑↓/jk agent · s steer · D stop · x/Ctrl+O tools · r refresh · Esc close · ${position}`;
|
|
817
|
+
const footer = ` ↑↓/jk agent · H Herdr · s steer · D stop · x/Ctrl+O tools · r refresh · Esc close · ${position}`;
|
|
796
818
|
lines.push(this.theme.fg("border", "│") + fit(this.theme.fg("dim", footer), innerWidth) + this.theme.fg("border", "│"));
|
|
797
819
|
lines.push(this.theme.fg("border", `╰${"─".repeat(innerWidth)}╯`));
|
|
798
820
|
return lines.map((line) => truncateToWidth(line, width));
|
|
@@ -819,9 +841,19 @@ export async function openSubagentFleet(ctx: ExtensionContext, state: SubagentSt
|
|
|
819
841
|
runId: input.runId,
|
|
820
842
|
...(input.index !== undefined ? { index: input.index } : {}),
|
|
821
843
|
message: input.message,
|
|
822
|
-
location: { asyncDir: input.asyncDir, resolvedId: input.runId },
|
|
844
|
+
location: { asyncDir: input.asyncDir, resolvedId: input.runId } as Parameters<typeof steerAsyncRun>[0]["location"],
|
|
823
845
|
}), `Failed to steer async run ${input.runId}.`),
|
|
824
846
|
stop: (input: { runId: string; asyncDir: string; index?: number }) => firstToolResultText(stopAsyncRun(state, input.runId, undefined, { asyncDir: input.asyncDir, resolvedId: input.runId }), `Failed to stop async run ${input.runId}.`),
|
|
847
|
+
inspect: async (input: { runId: string; asyncDir: string; index?: number }) => firstToolResultText(await handleHerdrInspectorAction("inspector.open", {
|
|
848
|
+
id: input.runId,
|
|
849
|
+
dir: input.asyncDir,
|
|
850
|
+
...(input.index !== undefined ? { index: input.index } : {}),
|
|
851
|
+
}, {
|
|
852
|
+
state,
|
|
853
|
+
cwd: state.baseCwd,
|
|
854
|
+
...(state.authorityPolicy ? { authorityPolicy: state.authorityPolicy } : {}),
|
|
855
|
+
...(state.missionStoreConfig ? { missions: state.missionStoreConfig } : {}),
|
|
856
|
+
}), `Failed to open Herdr inspector for async run ${input.runId}.`),
|
|
825
857
|
} satisfies FleetActionHandlers;
|
|
826
858
|
try {
|
|
827
859
|
await ctx.ui.custom<undefined>(
|