pi-subagents 0.45.1 → 0.46.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 +33 -0
- package/README.md +2 -0
- package/docs/agents.md +342 -0
- package/docs/configuration.md +320 -0
- package/docs/extension-api.md +308 -0
- package/docs/missions.md +117 -0
- package/docs/models.md +190 -0
- package/docs/observability.md +174 -0
- package/docs/tool-reference.md +343 -0
- package/docs/watchdog.md +176 -0
- package/docs/workflows.md +163 -0
- package/package.json +4 -2
- package/skills/pi-subagents/references/execution-controls.md +2 -2
- package/src/agents/agents.ts +17 -8
- package/src/agents/frontmatter.ts +7 -3
- package/src/agents/skills.ts +2 -9
- package/src/api/project-panes.ts +30 -0
- package/src/extension/config.ts +15 -1
- package/src/extension/index.ts +36 -16
- package/src/extension/schemas.ts +3 -2
- package/src/extension/subagent-guide.ts +39 -0
- package/src/extension/tool-description.ts +4 -4
- package/src/inspectors/herdr/project-panes.ts +457 -62
- package/src/missions/actions.ts +25 -2
- package/src/missions/lifecycle.ts +21 -2
- package/src/missions/store.ts +77 -1
- package/src/missions/types.ts +33 -0
- package/src/runs/background/async-execution.ts +7 -1
- package/src/runs/background/completion-replay.ts +267 -0
- package/src/runs/background/result-watcher.ts +12 -4
- package/src/runs/background/resume-guidance.ts +33 -0
- package/src/runs/background/subagent-wait.ts +5 -2
- package/src/runs/background/wait-completions.ts +39 -5
- package/src/runs/background/wait-subscriptions.ts +21 -5
- package/src/runs/foreground/execution.ts +4 -0
- package/src/runs/foreground/foreground-history.ts +137 -0
- package/src/runs/foreground/subagent-executor.ts +326 -38
- package/src/runs/shared/subagent-prompt-runtime.ts +71 -5
- package/src/shared/fork-context.ts +13 -0
- package/src/shared/prompt-resources.ts +51 -0
- package/src/shared/types.ts +30 -1
- package/src/shared/utf8.ts +11 -0
- package/src/slash/prompt-workflows.ts +2 -15
- package/src/slash/slash-commands.ts +19 -1
- package/src/tui/fleet-status.ts +8 -2
- package/src/tui/fleet.ts +135 -25
- package/src/tui/render.ts +120 -7
- package/src/workflows/scripted-workflow.ts +187 -11
|
@@ -30,6 +30,7 @@ import { drainOutstandingWork } from "../background/auto-drain.ts";
|
|
|
30
30
|
const SUBAGENT_INHERIT_PROJECT_CONTEXT_ENV = "PI_SUBAGENT_INHERIT_PROJECT_CONTEXT";
|
|
31
31
|
const SUBAGENT_INHERIT_SKILLS_ENV = "PI_SUBAGENT_INHERIT_SKILLS";
|
|
32
32
|
export const SUBAGENT_INTERCOM_SESSION_NAME_ENV = "PI_SUBAGENT_INTERCOM_SESSION_NAME";
|
|
33
|
+
const STEERING_LEGACY_SETTLE_FALLBACK_MS = 1000;
|
|
33
34
|
|
|
34
35
|
const STRUCTURED_OUTPUT_INSTRUCTIONS = [
|
|
35
36
|
"This subagent step has a strict structured output contract.",
|
|
@@ -330,7 +331,7 @@ function registerToolBudget(pi: ExtensionAPI, budget: ResolvedToolBudget | undef
|
|
|
330
331
|
|
|
331
332
|
export function registerSteeringInbox(
|
|
332
333
|
pi: ExtensionAPI,
|
|
333
|
-
deps: { watch?: typeof fs.watch; nativeRealpath?: (filePath: string) => string } = {},
|
|
334
|
+
deps: { watch?: typeof fs.watch; nativeRealpath?: (filePath: string) => string; legacySettleFallbackMs?: number } = {},
|
|
334
335
|
): void {
|
|
335
336
|
const steerInbox = process.env[SUBAGENT_STEER_INBOX_ENV]?.trim();
|
|
336
337
|
if (!steerInbox) return;
|
|
@@ -343,11 +344,14 @@ export function registerSteeringInbox(
|
|
|
343
344
|
let disposed = false;
|
|
344
345
|
let agentRunning = false;
|
|
345
346
|
let inTurn = false;
|
|
347
|
+
let awaitingSettlement = false;
|
|
346
348
|
let flushing = false;
|
|
347
349
|
let started = false;
|
|
348
350
|
let canSteer = typeof sendUserMessage === "function";
|
|
349
351
|
let watcher: fs.FSWatcher | undefined;
|
|
350
352
|
let interval: NodeJS.Timeout | undefined;
|
|
353
|
+
let settleFallback: NodeJS.Timeout | undefined;
|
|
354
|
+
const legacySettleFallbackMs = deps.legacySettleFallbackMs ?? STEERING_LEGACY_SETTLE_FALLBACK_MS;
|
|
351
355
|
const acknowledge = (request: SteerRequest, state: "delivered" | "queued" | "failed", message: string, deliveryStatus?: SteerDeliveryStatus): void => {
|
|
352
356
|
if (!ackDir || !Number.isInteger(childIndex) || childIndex < 0) return;
|
|
353
357
|
writeSteerAckAt(steerAckPathFromDir(ackDir, request.id), {
|
|
@@ -375,7 +379,8 @@ export function registerSteeringInbox(
|
|
|
375
379
|
continue;
|
|
376
380
|
}
|
|
377
381
|
const requestedMode = request.mode ?? "steer";
|
|
378
|
-
const
|
|
382
|
+
const autoCanUseIdle = requestedMode === "auto" && !agentRunning && !awaitingSettlement;
|
|
383
|
+
const delivery = requestedMode === "follow_up" || (requestedMode === "auto" && (inTurn || awaitingSettlement)) ? "followUp" as const : "steer" as const;
|
|
379
384
|
const pendingFollowUps = [...pending.values()].reduce((count, entries) => count + entries.filter((entry) => entry.deliveryStatus === "queued").length, 0);
|
|
380
385
|
if (delivery === "followUp" && queued.length + pendingFollowUps >= MAX_STEER_QUEUE_SIZE) {
|
|
381
386
|
acknowledge(request, "failed", `Follow-up queue is full (${MAX_STEER_QUEUE_SIZE} messages).`);
|
|
@@ -386,7 +391,7 @@ export function registerSteeringInbox(
|
|
|
386
391
|
entries.push({ request, deliveryStatus: delivery === "followUp" ? "queued" : "delivered" });
|
|
387
392
|
pending.set(formatted, entries);
|
|
388
393
|
try {
|
|
389
|
-
sendUserMessage(formatted,
|
|
394
|
+
sendUserMessage(formatted, autoCanUseIdle ? undefined : { deliverAs: delivery });
|
|
390
395
|
} catch (error) {
|
|
391
396
|
entries.pop();
|
|
392
397
|
if (entries.length === 0) pending.delete(formatted);
|
|
@@ -440,14 +445,71 @@ export function registerSteeringInbox(
|
|
|
440
445
|
flush();
|
|
441
446
|
return undefined;
|
|
442
447
|
};
|
|
448
|
+
const clearSettleFallback = (): void => {
|
|
449
|
+
if (!settleFallback) return;
|
|
450
|
+
clearTimeout(settleFallback);
|
|
451
|
+
settleFallback = undefined;
|
|
452
|
+
};
|
|
453
|
+
const markSettled = (): undefined => {
|
|
454
|
+
clearSettleFallback();
|
|
455
|
+
agentRunning = false;
|
|
456
|
+
inTurn = false;
|
|
457
|
+
awaitingSettlement = false;
|
|
458
|
+
return activate();
|
|
459
|
+
};
|
|
460
|
+
const armLegacySettleFallback = (): void => {
|
|
461
|
+
clearSettleFallback();
|
|
462
|
+
settleFallback = setTimeout(() => {
|
|
463
|
+
settleFallback = undefined;
|
|
464
|
+
if (disposed || !awaitingSettlement) return;
|
|
465
|
+
agentRunning = false;
|
|
466
|
+
inTurn = false;
|
|
467
|
+
awaitingSettlement = false;
|
|
468
|
+
activate();
|
|
469
|
+
}, legacySettleFallbackMs);
|
|
470
|
+
settleFallback.unref?.();
|
|
471
|
+
};
|
|
443
472
|
|
|
444
473
|
const onRuntimeEvent = pi.on as unknown as (event: string, handler: (event: unknown, ctx?: unknown) => unknown) => void;
|
|
445
474
|
// Register input before the watcher so an accepted extension input cannot race request dispatch.
|
|
446
475
|
onRuntimeEvent("input", onInput);
|
|
447
476
|
onRuntimeEvent("session_start", () => start());
|
|
448
|
-
onRuntimeEvent("agent_start", () => {
|
|
449
|
-
|
|
477
|
+
onRuntimeEvent("agent_start", () => {
|
|
478
|
+
clearSettleFallback();
|
|
479
|
+
agentRunning = true;
|
|
480
|
+
awaitingSettlement = false;
|
|
481
|
+
return activate();
|
|
482
|
+
});
|
|
483
|
+
onRuntimeEvent("agent_end", (event) => {
|
|
484
|
+
inTurn = false;
|
|
485
|
+
if ((event as { willRetry?: unknown } | undefined)?.willRetry === true) {
|
|
486
|
+
clearSettleFallback();
|
|
487
|
+
agentRunning = true;
|
|
488
|
+
awaitingSettlement = true;
|
|
489
|
+
return activate();
|
|
490
|
+
}
|
|
491
|
+
agentRunning = true;
|
|
492
|
+
awaitingSettlement = true;
|
|
493
|
+
armLegacySettleFallback();
|
|
494
|
+
return activate();
|
|
495
|
+
});
|
|
496
|
+
onRuntimeEvent("agent_settled", markSettled);
|
|
497
|
+
onRuntimeEvent("session_compact", () => {
|
|
498
|
+
const unresolved = [...pending.values()].flat();
|
|
499
|
+
pending.clear();
|
|
500
|
+
for (const entry of unresolved) {
|
|
501
|
+
try {
|
|
502
|
+
writeSteerRequestToDir(steerInbox, { ...entry.request, mode: "follow_up" });
|
|
503
|
+
} catch (error) {
|
|
504
|
+
acknowledge(entry.request, "failed", `Could not retry steering after compaction: ${error instanceof Error ? error.message : String(error)}`);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return activate();
|
|
508
|
+
});
|
|
450
509
|
onRuntimeEvent("turn_start", () => {
|
|
510
|
+
clearSettleFallback();
|
|
511
|
+
agentRunning = true;
|
|
512
|
+
awaitingSettlement = false;
|
|
451
513
|
inTurn = true;
|
|
452
514
|
const next = queued.findIndex((entry) => entry.ready);
|
|
453
515
|
if (next >= 0) {
|
|
@@ -466,7 +528,11 @@ export function registerSteeringInbox(
|
|
|
466
528
|
}
|
|
467
529
|
onRuntimeEvent("session_shutdown", () => {
|
|
468
530
|
for (const entry of queued) acknowledge(entry.request, "failed", "Run ended before queued follow-up delivery.", "queued");
|
|
531
|
+
for (const entries of pending.values()) {
|
|
532
|
+
for (const entry of entries) acknowledge(entry.request, "failed", "Run ended before Pi confirmed steering input delivery.");
|
|
533
|
+
}
|
|
469
534
|
disposed = true;
|
|
535
|
+
clearSettleFallback();
|
|
470
536
|
try { watcher?.close(); } catch {}
|
|
471
537
|
if (interval) clearInterval(interval);
|
|
472
538
|
});
|
|
@@ -9,6 +9,7 @@ type SubagentExecutionContext = "fresh" | "fork";
|
|
|
9
9
|
interface BranchSessionEntry {
|
|
10
10
|
type: string;
|
|
11
11
|
id?: string;
|
|
12
|
+
cwd?: string;
|
|
12
13
|
parentId?: string | null;
|
|
13
14
|
timestamp?: string;
|
|
14
15
|
message?: {
|
|
@@ -128,6 +129,18 @@ function readSessionEntries(sessionFile: string): BranchSessionEntry[] {
|
|
|
128
129
|
});
|
|
129
130
|
}
|
|
130
131
|
|
|
132
|
+
/** Keep Pi from restoring a forked session into the parent's cwd instead of the child launch cwd. */
|
|
133
|
+
export function alignForkedSessionCwd(sessionFile: string, cwd: string): void {
|
|
134
|
+
const entries = readSessionEntries(sessionFile);
|
|
135
|
+
const header = entries[0];
|
|
136
|
+
if (header?.type !== "session") throw new Error(`Forked session ${sessionFile} does not start with a session header.`);
|
|
137
|
+
const resolvedCwd = path.resolve(cwd);
|
|
138
|
+
const effectiveCwd = fs.realpathSync.native(resolvedCwd);
|
|
139
|
+
if (header.cwd === effectiveCwd) return;
|
|
140
|
+
header.cwd = effectiveCwd;
|
|
141
|
+
fs.writeFileSync(sessionFile, `${entries.map((entry) => JSON.stringify(entry)).join("\n")}\n`, "utf-8");
|
|
142
|
+
}
|
|
143
|
+
|
|
131
144
|
export function createForkContextResolver(
|
|
132
145
|
sessionManager: ForkableSessionManager,
|
|
133
146
|
requestedContext: unknown,
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { parseFrontmatter } from "../agents/frontmatter.ts";
|
|
5
|
+
import { getAgentDir, getProjectConfigDir } from "./utils.ts";
|
|
6
|
+
|
|
7
|
+
const PROMPT_REF_PATTERN = /^(package|user|project):([A-Za-z0-9][A-Za-z0-9._-]{0,127})$/;
|
|
8
|
+
const PROMPT_VARIABLE_PATTERN = /\{\{(\w+)\}\}/g;
|
|
9
|
+
|
|
10
|
+
type PromptVariable = string | number | boolean;
|
|
11
|
+
|
|
12
|
+
export function getPromptDirectories(cwd: string) {
|
|
13
|
+
return {
|
|
14
|
+
package: path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "prompts"),
|
|
15
|
+
user: path.join(getAgentDir(), "prompts"),
|
|
16
|
+
project: path.join(getProjectConfigDir(cwd), "prompts"),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function promptVariables(vars: unknown): Record<string, PromptVariable> {
|
|
21
|
+
if (vars === undefined) return {};
|
|
22
|
+
if (!vars || typeof vars !== "object" || Array.isArray(vars)) throw new Error("prompts.render vars must be a plain object.");
|
|
23
|
+
const prototype = Object.getPrototypeOf(vars);
|
|
24
|
+
if (prototype !== null && prototype !== Object.prototype) throw new Error("prompts.render vars must be a plain object.");
|
|
25
|
+
for (const [name, value] of Object.entries(vars)) {
|
|
26
|
+
if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") {
|
|
27
|
+
throw new Error(`prompts.render variable '${name}' must be a string, number, or boolean.`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return vars as Record<string, PromptVariable>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function renderWorkflowPrompt(ref: string, vars: unknown, cwd: string): string {
|
|
34
|
+
const match = ref.match(PROMPT_REF_PATTERN);
|
|
35
|
+
if (!match) throw new Error("prompts.render ref must use package:<name>, user:<name>, or project:<name>.");
|
|
36
|
+
const scope = match[1] as keyof ReturnType<typeof getPromptDirectories>;
|
|
37
|
+
const name = match[2]!;
|
|
38
|
+
const filePath = path.join(getPromptDirectories(cwd)[scope], `${name}.md`);
|
|
39
|
+
let content: string;
|
|
40
|
+
try {
|
|
41
|
+
if (!fs.lstatSync(filePath).isFile()) throw new Error("not a regular file");
|
|
42
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
43
|
+
} catch (error) {
|
|
44
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
45
|
+
throw new Error(`Could not read prompt fragment '${ref}': ${detail}`);
|
|
46
|
+
}
|
|
47
|
+
const variables = promptVariables(vars);
|
|
48
|
+
return parseFrontmatter(content).body.trim().replace(PROMPT_VARIABLE_PATTERN, (placeholder, variable: string) => {
|
|
49
|
+
return Object.hasOwn(variables, variable) ? String(variables[variable]) : placeholder;
|
|
50
|
+
});
|
|
51
|
+
}
|
package/src/shared/types.ts
CHANGED
|
@@ -969,6 +969,8 @@ export interface WaitCompletion {
|
|
|
969
969
|
mode?: string;
|
|
970
970
|
state?: string;
|
|
971
971
|
success?: boolean;
|
|
972
|
+
/** Versioned bounded output archive retained with the durable completion replay. */
|
|
973
|
+
archivePath?: string;
|
|
972
974
|
results?: WaitCompletionChild[];
|
|
973
975
|
}
|
|
974
976
|
|
|
@@ -1045,6 +1047,7 @@ export interface Details {
|
|
|
1045
1047
|
operation: "run" | "status";
|
|
1046
1048
|
key: string;
|
|
1047
1049
|
state: "started" | "completed" | "failed" | "reused";
|
|
1050
|
+
agent?: string;
|
|
1048
1051
|
runId?: string;
|
|
1049
1052
|
phase?: string;
|
|
1050
1053
|
label?: string;
|
|
@@ -1538,6 +1541,10 @@ export interface ForegroundChildControl {
|
|
|
1538
1541
|
|
|
1539
1542
|
export interface ForegroundRunControl {
|
|
1540
1543
|
runId: string;
|
|
1544
|
+
/** Workflow shell that owns this live foreground child, when applicable. */
|
|
1545
|
+
parentWorkflowRunId?: string;
|
|
1546
|
+
/** Stable workflow lane key for this live foreground child. */
|
|
1547
|
+
workflowKey?: string;
|
|
1541
1548
|
/** Originating parent session; required for public fleet projection. */
|
|
1542
1549
|
sessionId?: string;
|
|
1543
1550
|
mode: SubagentRunMode;
|
|
@@ -1780,12 +1787,34 @@ export interface ScheduledRunsConfig {
|
|
|
1780
1787
|
|
|
1781
1788
|
export type FleetViewPlacement = "aboveEditor" | "belowEditor";
|
|
1782
1789
|
|
|
1790
|
+
export const FLEET_KEYBINDING_ACTIONS = [
|
|
1791
|
+
"close",
|
|
1792
|
+
"scrollUp",
|
|
1793
|
+
"scrollDown",
|
|
1794
|
+
"selectUp",
|
|
1795
|
+
"selectDown",
|
|
1796
|
+
"selectFirst",
|
|
1797
|
+
"selectLast",
|
|
1798
|
+
"pageUp",
|
|
1799
|
+
"pageDown",
|
|
1800
|
+
"refresh",
|
|
1801
|
+
"steer",
|
|
1802
|
+
"inspect",
|
|
1803
|
+
"stop",
|
|
1804
|
+
"toggleTools",
|
|
1805
|
+
] as const;
|
|
1806
|
+
|
|
1807
|
+
export type FleetKeybindingAction = typeof FLEET_KEYBINDING_ACTIONS[number];
|
|
1808
|
+
export type FleetKeybindingsConfig = Partial<Record<FleetKeybindingAction, string[]>>;
|
|
1809
|
+
|
|
1783
1810
|
export interface ExtensionConfig {
|
|
1784
1811
|
asyncByDefault?: boolean;
|
|
1785
1812
|
/** Show the Claude Code-style navigable fleet. Defaults to true. */
|
|
1786
1813
|
fleetView?: boolean;
|
|
1787
1814
|
/** Place the persistent FleetView above or below the editor. Defaults to belowEditor. */
|
|
1788
1815
|
fleetViewPlacement?: FleetViewPlacement;
|
|
1816
|
+
/** Local keybindings for the full Fleet inspector. */
|
|
1817
|
+
fleetKeybindings?: FleetKeybindingsConfig;
|
|
1789
1818
|
/** Show the under-editor async runs widget. Defaults to true, including when FleetView is enabled. */
|
|
1790
1819
|
asyncWidget?: boolean;
|
|
1791
1820
|
/** Tool description variant registered for the parent-facing subagent tool. Defaults to full. */
|
|
@@ -1922,7 +1951,7 @@ export const SLASH_SUBAGENT_CANCEL_EVENT = "subagent:slash:cancel";
|
|
|
1922
1951
|
export const POLL_INTERVAL_MS = 250;
|
|
1923
1952
|
export const MAX_WIDGET_JOBS = 4;
|
|
1924
1953
|
export const DEFAULT_SUBAGENT_MAX_DEPTH = 2;
|
|
1925
|
-
export const SUBAGENT_ACTIONS = ["list", "get", "models", "children.list", "create", "update", "delete", "eject", "disable", "enable", "reset", "mission.create", "mission.list", "mission.show", "mission.update", "mission.attach-run", "mission.close", "worktree.discard", "refine", "refine.show", "refine.rollback", "inspector.open", "inspector.status", "inspector.close", "project.open", "project.status", "project.close", "status", "grant-spawn-budget", "interrupt", "resume", "steer", "stop", "append-step", "approve-checkpoint", "reject-checkpoint", "doctor", "watchdog.status", "watchdog.check", "watchdog.configure", "watchdog.recommend-model", "schedule.create", "schedule.list", "schedule.show", "schedule.history", "schedule.pause", "schedule.resume", "schedule.run", "schedule.run-due", "schedule.delete"] as const;
|
|
1954
|
+
export const SUBAGENT_ACTIONS = ["list", "get", "models", "children.list", "guide", "create", "update", "delete", "eject", "disable", "enable", "reset", "mission.create", "mission.list", "mission.show", "mission.update", "mission.resolve-decision", "mission.attach-run", "mission.close", "worktree.discard", "refine", "refine.show", "refine.rollback", "inspector.open", "inspector.status", "inspector.close", "project.open", "project.status", "project.close", "status", "grant-spawn-budget", "interrupt", "resume", "steer", "stop", "append-step", "approve-checkpoint", "reject-checkpoint", "doctor", "watchdog.status", "watchdog.check", "watchdog.configure", "watchdog.recommend-model", "schedule.create", "schedule.list", "schedule.show", "schedule.history", "schedule.pause", "schedule.resume", "schedule.run", "schedule.run-due", "schedule.delete"] as const;
|
|
1926
1955
|
|
|
1927
1956
|
export const DEFAULT_FORK_PREAMBLE =
|
|
1928
1957
|
"You are a delegated subagent running from a fork of the parent session. " +
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function decodeUtf8Tail(bytes: Buffer): string {
|
|
2
|
+
let start = 0;
|
|
3
|
+
while (start < bytes.length && (bytes[start]! & 0xc0) === 0x80) start += 1;
|
|
4
|
+
return bytes.subarray(start).toString("utf-8");
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function utf8Tail(value: string, maxBytes: number): { text: string; truncated: boolean } {
|
|
8
|
+
const bytes = Buffer.from(value, "utf-8");
|
|
9
|
+
if (bytes.length <= maxBytes) return { text: value, truncated: false };
|
|
10
|
+
return { text: decodeUtf8Tail(bytes.subarray(bytes.length - maxBytes)), truncated: true };
|
|
11
|
+
}
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
3
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
4
|
import { parseFrontmatter } from "../agents/frontmatter.ts";
|
|
6
5
|
import type { SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
|
|
7
|
-
import {
|
|
6
|
+
import { getPromptDirectories } from "../shared/prompt-resources.ts";
|
|
8
7
|
|
|
9
8
|
interface PromptWorkflow {
|
|
10
9
|
name: string;
|
|
@@ -32,21 +31,9 @@ const RESERVED_COMMAND_NAMES = new Set([
|
|
|
32
31
|
"subagents-models",
|
|
33
32
|
]);
|
|
34
33
|
|
|
35
|
-
function packagePromptsDir(): string {
|
|
36
|
-
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "prompts");
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function promptDirs(cwd: string): string[] {
|
|
40
|
-
return [
|
|
41
|
-
packagePromptsDir(),
|
|
42
|
-
path.join(getAgentDir(), "prompts"),
|
|
43
|
-
path.join(getProjectConfigDir(cwd), "prompts"),
|
|
44
|
-
];
|
|
45
|
-
}
|
|
46
|
-
|
|
47
34
|
function readPromptFiles(cwd: string): string[] {
|
|
48
35
|
const files: string[] = [];
|
|
49
|
-
for (const dir of
|
|
36
|
+
for (const dir of Object.values(getPromptDirectories(cwd))) {
|
|
50
37
|
let entries: fs.Dirent[];
|
|
51
38
|
try {
|
|
52
39
|
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
@@ -22,6 +22,7 @@ import { SUBAGENT_FANOUT_CHILD_ENV } from "../runs/shared/pi-args.ts";
|
|
|
22
22
|
import type { SlashSubagentResponse, SlashSubagentUpdate } from "./slash-bridge.ts";
|
|
23
23
|
import { registerPromptWorkflowCommands } from "./prompt-workflows.ts";
|
|
24
24
|
import { openSubagentsAdmin } from "./subagents-admin.ts";
|
|
25
|
+
import { SUBAGENT_GUIDE_TOPICS } from "../extension/subagent-guide.ts";
|
|
25
26
|
import { openSubagentFleet } from "../tui/fleet.ts";
|
|
26
27
|
import {
|
|
27
28
|
applySlashUpdate,
|
|
@@ -40,6 +41,7 @@ import {
|
|
|
40
41
|
SLASH_SUBAGENT_UPDATE_EVENT,
|
|
41
42
|
DIRS,
|
|
42
43
|
type Details,
|
|
44
|
+
type FleetKeybindingsConfig,
|
|
43
45
|
type JsonSchemaObject,
|
|
44
46
|
type SingleResult,
|
|
45
47
|
type SubagentState,
|
|
@@ -628,6 +630,7 @@ function slashRunWorkflowScript(key: string, child: Record<string, unknown>): st
|
|
|
628
630
|
export function registerSlashCommands(
|
|
629
631
|
pi: ExtensionAPI,
|
|
630
632
|
state: SubagentState,
|
|
633
|
+
options: { fleetKeybindings?: FleetKeybindingsConfig } = {},
|
|
631
634
|
): void {
|
|
632
635
|
let fleetOpen = false;
|
|
633
636
|
const showFleet = async (ctx: ExtensionContext) => {
|
|
@@ -642,7 +645,7 @@ export function registerSlashCommands(
|
|
|
642
645
|
}
|
|
643
646
|
fleetOpen = true;
|
|
644
647
|
try {
|
|
645
|
-
await openSubagentFleet(ctx, state, { asyncDirRoot: DIRS.async, resultsDir: DIRS.results });
|
|
648
|
+
await openSubagentFleet(ctx, state, { asyncDirRoot: DIRS.async, resultsDir: DIRS.results, fleetKeybindings: options.fleetKeybindings });
|
|
646
649
|
} finally {
|
|
647
650
|
fleetOpen = false;
|
|
648
651
|
}
|
|
@@ -698,6 +701,21 @@ export function registerSlashCommands(
|
|
|
698
701
|
},
|
|
699
702
|
});
|
|
700
703
|
|
|
704
|
+
pi.registerCommand("subagents-guide", {
|
|
705
|
+
description: "Show a packaged subagents guide topic",
|
|
706
|
+
getArgumentCompletions: (prefix) => prefix.includes(" ") ? null : SUBAGENT_GUIDE_TOPICS
|
|
707
|
+
.filter((topic) => topic.startsWith(prefix))
|
|
708
|
+
.map((topic) => ({ value: topic, label: topic })),
|
|
709
|
+
handler: async (args, ctx) => {
|
|
710
|
+
const topic = args.trim();
|
|
711
|
+
if (topic.includes(" ")) {
|
|
712
|
+
ctx.ui.notify("Usage: /subagents-guide [topic]", "error");
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
await runSlashSubagent(pi, ctx, { action: "guide", ...(topic ? { topic } : {}) });
|
|
716
|
+
},
|
|
717
|
+
});
|
|
718
|
+
|
|
701
719
|
pi.registerCommand("subagents-refine", {
|
|
702
720
|
description: "Generate a bounded project-local refinement overlay for one subagent",
|
|
703
721
|
getArgumentCompletions: makeAgentCompletions(state),
|
package/src/tui/fleet-status.ts
CHANGED
|
@@ -144,6 +144,12 @@ function isStaleExtensionContextError(error: unknown): boolean {
|
|
|
144
144
|
|| error.message.includes("Extension context no longer active"));
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
function foregroundDescription(control: { parentWorkflowRunId?: string; workflowKey?: string }, description: string | undefined): string | undefined {
|
|
148
|
+
if (!control.parentWorkflowRunId) return description;
|
|
149
|
+
const workflow = `workflow child: ${control.parentWorkflowRunId}${control.workflowKey ? ` (${control.workflowKey})` : ""}`;
|
|
150
|
+
return description ? `${workflow} · ${description}` : workflow;
|
|
151
|
+
}
|
|
152
|
+
|
|
147
153
|
export function collectFleetStatusEntries(state: SubagentState): FleetStatusEntry[] {
|
|
148
154
|
const entries: FleetStatusEntry[] = [];
|
|
149
155
|
for (const control of state.foregroundControls.values()) {
|
|
@@ -154,7 +160,7 @@ export function collectFleetStatusEntries(state: SubagentState): FleetStatusEntr
|
|
|
154
160
|
key: `foreground-active:${control.runId}:${child.index}`,
|
|
155
161
|
agent: child.agent,
|
|
156
162
|
...(modelThinking ? { modelThinking } : {}),
|
|
157
|
-
description: child.description,
|
|
163
|
+
description: foregroundDescription(control, child.description),
|
|
158
164
|
startedAt: child.startedAt,
|
|
159
165
|
tokens: child.tokens ?? 0,
|
|
160
166
|
state: "running",
|
|
@@ -170,7 +176,7 @@ export function collectFleetStatusEntries(state: SubagentState): FleetStatusEntr
|
|
|
170
176
|
key: `foreground-active:${control.runId}:${control.currentIndex ?? 0}`,
|
|
171
177
|
agent: control.currentAgent ?? control.mode,
|
|
172
178
|
...(modelThinking ? { modelThinking } : {}),
|
|
173
|
-
description: control.description,
|
|
179
|
+
description: foregroundDescription(control, control.description),
|
|
174
180
|
startedAt: control.startedAt,
|
|
175
181
|
tokens: control.tokens ?? 0,
|
|
176
182
|
state: "running",
|