gentle-pi 2.3.0 → 2.5.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/README.md +195 -11
- package/assets/agents/gentle-ai-worker.md +9 -0
- package/assets/agents/sdd-explore.md +1 -0
- package/assets/orchestrator-delegation.md +21 -10
- package/assets/orchestrator.md +8 -12
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +8 -7
- package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/README.md +10 -0
- package/contracts/review-provider-contract-mirror/v1.2.0/bundle/manifest.json +74 -0
- package/contracts/review-provider-contract-mirror/v1.2.0/bundle/orchestration/pi.md +53 -0
- package/contracts/review-provider-contract-mirror/v1.2.0/bundle/schemas/targeted-validator.schema.json +1 -0
- package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/generated/provider-capabilities.baseline.json +9 -2
- package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/generated/provider-roles.baseline.json +2 -2
- package/docs/delegated-verification.md +25 -0
- package/docs/review-integration.md +1 -1
- package/docs/telemetry.md +38 -0
- package/extensions/ask-user-choice.ts +26 -20
- package/extensions/codegraph-tools.ts +94 -5
- package/extensions/gentle-agents.ts +588 -0
- package/extensions/gentle-ai.ts +1421 -143
- package/extensions/gentle-shell.ts +547 -0
- package/extensions/gentle-todo.ts +199 -0
- package/extensions/quiet-tools.ts +1 -1
- package/lib/agent-home.ts +8 -0
- package/lib/agents-config.ts +318 -0
- package/lib/agents-history.ts +80 -0
- package/lib/agents-protocol.ts +429 -0
- package/lib/agents-runner.ts +490 -0
- package/lib/agents-transcript.ts +87 -0
- package/lib/agents-view.ts +557 -0
- package/lib/agents-widget.ts +222 -0
- package/lib/gentle-ai-renderer.ts +142 -26
- package/lib/native-choice-list.ts +194 -0
- package/lib/native-fullscreen-interaction.ts +47 -0
- package/lib/native-pointer-region.ts +164 -0
- package/lib/native-review-cli.ts +103 -12
- package/lib/provider-contract-bundle.ts +88 -6
- package/lib/review-candidate-view-owner.ts +177 -0
- package/lib/review-candidate-view.ts +127 -35
- package/lib/review-consent-ui.ts +65 -0
- package/lib/review-host-relay.ts +146 -60
- package/lib/review-integration-v2.ts +92 -13
- package/lib/review-last-event-controller.ts +1 -0
- package/lib/review-relay-contract.ts +11 -0
- package/lib/review-repository.ts +2 -2
- package/lib/review-risk-assessment.ts +339 -0
- package/lib/review-session-standing-permission-ipc.ts +309 -0
- package/lib/review-session-standing-permission.ts +219 -0
- package/lib/sdd-preflight.ts +2 -2
- package/lib/shell-bar.ts +138 -0
- package/lib/shell-card.ts +136 -0
- package/lib/shell-changes-view.ts +205 -0
- package/lib/shell-changes.ts +210 -0
- package/lib/shell-gauge.ts +40 -0
- package/lib/shell-prompt.ts +119 -0
- package/lib/shell-todo.ts +280 -0
- package/lib/shell-usage-view.ts +76 -0
- package/lib/shell-usage.ts +246 -0
- package/lib/telemetry-trigger.ts +151 -0
- package/package.json +4 -4
- package/runtime/native-review-cli.mjs +102 -11
- package/runtime/review-integration-v2.mjs +92 -13
- package/runtime/review-relay-contract.mjs +11 -0
- package/runtime/review-risk-assessment.mjs +340 -0
- package/runtime/telemetry-trigger.mjs +152 -0
- package/scripts/build-runtime-modules.mjs +2 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/test-packed-runner.mjs +22 -0
- package/scripts/verify-package-files.mjs +18 -13
- package/skills/_shared/review-ledger-contract.md +9 -1
- package/skills/issue-creation/SKILL.md +53 -93
- package/tests/agents-config.test.ts +143 -0
- package/tests/agents-fake-child.ts +52 -0
- package/tests/agents-history.test.ts +54 -0
- package/tests/agents-protocol.test.ts +153 -0
- package/tests/agents-runner-process.test.ts +111 -0
- package/tests/agents-runner.test.ts +402 -0
- package/tests/agents-transcript.test.ts +30 -0
- package/tests/agents-view.test.ts +274 -0
- package/tests/agents-widget.test.ts +111 -0
- package/tests/ask-user-choice.test.ts +157 -3
- package/tests/codegraph-tools.test.ts +110 -1
- package/tests/devbinary/native-review-parity.devtest.ts +108 -0
- package/tests/fixtures/agents-process-child.mjs +23 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/README.md +22 -0
- package/{contracts/review-provider-contract-mirror/v1.1.0/bundle → tests/fixtures/provider-contract-bundle/v1.2.0}/manifest.json +11 -2
- package/tests/fixtures/provider-contract-bundle/v1.2.0/orchestration/pi.md +97 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/schemas/lens.schema.json +16 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/schemas/refuter.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/lens.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/refuter.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/targeted-validator.json +1 -0
- package/tests/gentle-agents.test.ts +741 -0
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-installer.test.ts +47 -47
- package/tests/gentle-ai-renderer.test.ts +65 -0
- package/tests/gentle-ai.test.ts +31 -14
- package/tests/gentle-card-text.ts +35 -0
- package/tests/gentle-shell.test.ts +527 -0
- package/tests/gentle-todo.test.ts +182 -0
- package/tests/issue-creation-skill.test.ts +103 -0
- package/tests/native-choice-list.test.ts +202 -0
- package/tests/native-fullscreen-interaction.test.ts +125 -0
- package/tests/native-pointer-region.test.ts +245 -0
- package/tests/native-review-capability-contract.test.ts +33 -1
- package/tests/native-review-cli.test.ts +40 -0
- package/tests/native-review-consent.test.ts +91 -0
- package/tests/native-review-parity-runtime.test.ts +8 -2
- package/tests/native-review-parity.test.ts +29 -22
- package/tests/orchestrator-budget.test.ts +71 -2
- package/tests/orchestrator-rdd-ownership.test.ts +10 -1
- package/tests/package-manifest.test.ts +134 -9
- package/tests/provider-contract-bundle.test.ts +76 -0
- package/tests/provider-contract-mirror.test.ts +19 -0
- package/tests/quiet-tool-rendering.test.ts +96 -37
- package/tests/rdd-aware-verification-contract.test.ts +216 -0
- package/tests/rdd-status-line.test.ts +286 -0
- package/tests/review-agent-end-preflight.test.ts +408 -0
- package/tests/review-candidate-view.test.ts +452 -6
- package/tests/review-contract-prompt.test.ts +142 -0
- package/tests/review-controller-native-recovery.test.ts +29 -4
- package/tests/review-controller-native-routing.test.ts +321 -4
- package/tests/review-controller-workspace-root.test.ts +45 -2
- package/tests/review-controller.test.ts +26 -1
- package/tests/review-host-relay-routing.test.ts +229 -11
- package/tests/review-host-relay.test.ts +195 -7
- package/tests/review-integration-v2-forward.test.ts +47 -0
- package/tests/review-integration-v2.test.ts +112 -0
- package/tests/review-last-event-closure.test.ts +7 -2
- package/tests/review-ledger-contract.test.ts +1 -1
- package/tests/review-relay-contract.test.ts +26 -0
- package/tests/review-repository.test.ts +28 -1
- package/tests/review-risk-assessment.test.ts +626 -0
- package/tests/review-session-standing-permission-controller.test.ts +608 -0
- package/tests/review-session-standing-permission-ipc.test.ts +233 -0
- package/tests/review-session-standing-permission-runtime.test.ts +212 -0
- package/tests/review-session-standing-permission.test.ts +126 -0
- package/tests/runtime-harness.mjs +1 -0
- package/tests/shell-bar.test.ts +176 -0
- package/tests/shell-card.test.ts +118 -0
- package/tests/shell-changes-view.test.ts +146 -0
- package/tests/shell-changes.test.ts +182 -0
- package/tests/shell-prompt.test.ts +118 -0
- package/tests/shell-todo.test.ts +170 -0
- package/tests/shell-usage-view.test.ts +62 -0
- package/tests/shell-usage.test.ts +197 -0
- package/tests/telemetry-trigger.test.ts +349 -0
- package/tests/writer-edit-surface-scope.test.ts +153 -17
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/schemas/lens.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/schemas/refuter.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/lens.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/refuter.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/targeted-validator.json +0 -0
- /package/{contracts/review-provider-contract-mirror/v1.1.0/bundle → tests/fixtures/provider-contract-bundle/v1.2.0}/schemas/targeted-validator.schema.json +0 -0
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
3
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import { join, resolve } from "node:path";
|
|
6
|
+
import { keyHint, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
8
|
+
import { AGENT_MODE, discoverAgents, loadAgentsConfig, resolveAgentProfile, type AgentDefinition, type AgentMode } from "../lib/agents-config.ts";
|
|
9
|
+
import { isFinished, TASK_STATUS, TaskStore, type AskRequest, type TaskRecord } from "../lib/agents-protocol.ts";
|
|
10
|
+
import { AgentRunner, piCommand, type AskAnswer, type RunnerDeps, type TaskRequest } from "../lib/agents-runner.ts";
|
|
11
|
+
import { hasReviewSessionPermission, resolveCanonicalGitRepositoryIdentitySync, type ReviewSessionManager } from "../lib/review-session-standing-permission.ts";
|
|
12
|
+
import { historyDir, loadHistory, loadStoredTask, pruneHistory, saveTask } from "../lib/agents-history.ts";
|
|
13
|
+
import { sessionToMarkdown } from "../lib/agents-transcript.ts";
|
|
14
|
+
import { AgentsView } from "../lib/agents-view.ts";
|
|
15
|
+
import { createNativeFullscreenInteraction } from "../lib/native-fullscreen-interaction.ts";
|
|
16
|
+
import { AGENTS_GLYPH, renderAgentsCard, widgetExpiryMs, widgetRows } from "../lib/agents-widget.ts";
|
|
17
|
+
import { CARD_TONE, renderCard } from "../lib/shell-card.ts";
|
|
18
|
+
import { openInExternalEditor } from "./gentle-shell.ts";
|
|
19
|
+
import { resolveGentlePiAgentHome } from "../lib/agent-home.ts";
|
|
20
|
+
|
|
21
|
+
// Gentle Agents: subagents as isolated `pi --mode rpc` children, a task
|
|
22
|
+
// store that notifies per task, and a Gentle Shell card above the editor.
|
|
23
|
+
// The tool names match the retired pi-subagents package so prompts, skills,
|
|
24
|
+
// and gentle-ai's delegation rules keep working unchanged.
|
|
25
|
+
|
|
26
|
+
export const AGENTS_WIDGET_KEY = "gentle-agents";
|
|
27
|
+
export const AGENTS_COMMAND_NAME = "gentle:agents";
|
|
28
|
+
export const AGENTS_RESULT_TYPE = "gentle-agents.result";
|
|
29
|
+
const COLLAPSE_KEY_DEFAULT = "ctrl+shift+a";
|
|
30
|
+
const VIEW_KEY_DEFAULT = "alt+a";
|
|
31
|
+
const STOP_KEY_DEFAULT = "alt+s";
|
|
32
|
+
const OVERLAY_HEIGHT_RATIO = 0.8;
|
|
33
|
+
const OVERLAY_MIN_ROWS = 12;
|
|
34
|
+
const RENDER_COALESCE_MS = 400;
|
|
35
|
+
const CLOCK_TICK_MS = 1000;
|
|
36
|
+
const TOOL_PREFIX = "subagent_";
|
|
37
|
+
|
|
38
|
+
export interface AgentsDeps extends RunnerDeps {
|
|
39
|
+
home: string;
|
|
40
|
+
agentHome?: string;
|
|
41
|
+
env: NodeJS.ProcessEnv;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function agentRuntimePaths(home: string, agentHome = join(home, ".pi", "agent")): { sessions: string; transcripts: string } {
|
|
45
|
+
const root = join(agentHome, "gentle-agents");
|
|
46
|
+
return { sessions: join(root, "sessions"), transcripts: join(root, "transcripts") };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface ToolText {
|
|
50
|
+
content: Array<{ type: "text"; text: string }>;
|
|
51
|
+
details: Record<string, unknown>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const defaultDeps = (env: NodeJS.ProcessEnv): AgentsDeps => ({
|
|
55
|
+
spawn: (command, args, options) => spawn(command, args, { cwd: options.cwd, env: options.env, stdio: options.stdio ?? ["pipe", "pipe", "pipe"] }),
|
|
56
|
+
now: () => Date.now(),
|
|
57
|
+
schedule: (fn, ms) => {
|
|
58
|
+
const timer = setTimeout(fn, ms);
|
|
59
|
+
timer.unref?.();
|
|
60
|
+
return () => clearTimeout(timer);
|
|
61
|
+
},
|
|
62
|
+
pi: piCommand(),
|
|
63
|
+
home: os.homedir(),
|
|
64
|
+
env,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
export function agentsEnabled(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
68
|
+
if (env.GENTLE_PI_AGENTS_CHILD === "1") return false;
|
|
69
|
+
const value = env.GENTLE_PI_AGENTS?.trim().toLowerCase();
|
|
70
|
+
return !(value === "0" || value === "false" || value === "off");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// The retired pi-subagents package registers the same tool names. While it
|
|
74
|
+
// is still installed we stay out of the way and say how to switch.
|
|
75
|
+
export const LEGACY_SUBAGENTS_PACKAGE = "pi-subagents-j0k3r";
|
|
76
|
+
|
|
77
|
+
export function legacySubagentsInstalled(home: string): boolean {
|
|
78
|
+
return legacySubagentsInstalledAt(join(home, ".pi", "agent"));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function legacySubagentsInstalledAt(agentHome: string): boolean {
|
|
82
|
+
const settingsPath = join(agentHome, "settings.json");
|
|
83
|
+
if (!existsSync(settingsPath)) return false;
|
|
84
|
+
try {
|
|
85
|
+
const settings = JSON.parse(readFileSync(settingsPath, "utf8")) as { packages?: unknown };
|
|
86
|
+
return Array.isArray(settings.packages) && settings.packages.some((entry) => typeof entry === "string" && entry.includes(LEGACY_SUBAGENTS_PACKAGE));
|
|
87
|
+
} catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function agentsViewKey(env: NodeJS.ProcessEnv = process.env): string | undefined {
|
|
93
|
+
const value = env.GENTLE_PI_AGENTS_VIEW_KEY?.trim();
|
|
94
|
+
if (value === undefined) return VIEW_KEY_DEFAULT;
|
|
95
|
+
return value === "" || value.toLowerCase() === "off" ? undefined : value;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function agentsCollapseKey(env: NodeJS.ProcessEnv = process.env): string | undefined {
|
|
99
|
+
const value = env.GENTLE_PI_AGENTS_KEY?.trim();
|
|
100
|
+
if (value === undefined) return COLLAPSE_KEY_DEFAULT;
|
|
101
|
+
return value === "" || value.toLowerCase() === "off" ? undefined : value;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function agentsStopKey(env: NodeJS.ProcessEnv = process.env): string | undefined {
|
|
105
|
+
const value = env.GENTLE_PI_AGENTS_STOP_KEY?.trim();
|
|
106
|
+
if (value === undefined) return STOP_KEY_DEFAULT;
|
|
107
|
+
return value === "" || value.toLowerCase() === "off" ? undefined : value;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function text(value: string, details: Record<string, unknown> = {}): ToolText {
|
|
111
|
+
return { content: [{ type: "text", text: value }], details };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function taskDetails(task: TaskRecord): Record<string, unknown> {
|
|
115
|
+
return { gentleAgents: { taskId: task.id, agent: task.agent, status: task.status, mode: task.mode } };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function describeTask(task: TaskRecord): string {
|
|
119
|
+
const head = `${task.id} · ${task.agent} · ${task.status} · ${task.mode}`;
|
|
120
|
+
const detail = task.error ? `\n${task.error}` : "";
|
|
121
|
+
return `${head} · ${task.turns} turns · ${task.toolCalls} tool calls · last: ${task.lastStep}${detail}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function finishedText(task: TaskRecord): string {
|
|
125
|
+
if (task.status === "completed") return task.result ?? "(the subagent returned no text)";
|
|
126
|
+
return `Subagent ${task.agent} ${task.status}${task.error ? `: ${task.error}` : ""}${task.result ? `\n\nLast answer:\n${task.result}` : ""}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// pi's keybinding hint needs a live theme; outside one (tests, headless) the
|
|
130
|
+
// plain words still tell the reader what the key does.
|
|
131
|
+
function expandHint(expanded: boolean): string {
|
|
132
|
+
try {
|
|
133
|
+
return keyHint("app.tools.expand", expanded ? "collapse" : "expand");
|
|
134
|
+
} catch {
|
|
135
|
+
return expanded ? "collapse" : "expand";
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// What the model reads when a background task ends: the outcome first, then
|
|
140
|
+
// the answer itself. The card renderer shows the same text.
|
|
141
|
+
export function completionText(task: TaskRecord): string {
|
|
142
|
+
const outcome = task.status === "completed" ? "finished" : task.status.replace("_", " ");
|
|
143
|
+
return `Subagent ${task.agent} (task ${task.id}, "${task.label}") ${outcome}.\n\n${finishedText(task)}`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Host-side answer to a child's dialog: the same ctx.ui the human already
|
|
147
|
+
// uses, so a subagent's question looks like any other pi dialog.
|
|
148
|
+
export async function answerThroughUi(ui: ExtensionContext["ui"] | undefined, ask: AskRequest, raw: Record<string, unknown>): Promise<AskAnswer> {
|
|
149
|
+
if (!ui) return { cancelled: true };
|
|
150
|
+
const title = `${AGENTS_GLYPH} ${ask.title}`;
|
|
151
|
+
switch (ask.method) {
|
|
152
|
+
case "select": {
|
|
153
|
+
const options = Array.isArray(raw.options) ? raw.options.map(String) : [];
|
|
154
|
+
const value = await ui.select(title, options);
|
|
155
|
+
return value === undefined ? { cancelled: true } : { value };
|
|
156
|
+
}
|
|
157
|
+
case "confirm":
|
|
158
|
+
return { confirmed: await ui.confirm(title, typeof raw.message === "string" ? raw.message : "") };
|
|
159
|
+
case "input": {
|
|
160
|
+
const value = await ui.input(title, typeof raw.placeholder === "string" ? raw.placeholder : undefined);
|
|
161
|
+
return value === undefined ? { cancelled: true } : { value };
|
|
162
|
+
}
|
|
163
|
+
case "editor": {
|
|
164
|
+
const value = await ui.editor(title, typeof raw.prefill === "string" ? raw.prefill : undefined);
|
|
165
|
+
return value === undefined ? { cancelled: true } : { value };
|
|
166
|
+
}
|
|
167
|
+
default:
|
|
168
|
+
return { cancelled: true };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv = process.env, overrides: Partial<AgentsDeps> = {}): void {
|
|
173
|
+
if (!agentsEnabled(env)) return;
|
|
174
|
+
const deps: AgentsDeps = { ...defaultDeps(env), ...overrides };
|
|
175
|
+
const selectedHome = overrides.agentHome ?? (overrides.home === undefined ? resolveGentlePiAgentHome(deps.env) : join(deps.home, ".pi", "agent"));
|
|
176
|
+
// Expand environment tildes like Pi, but leave explicit path APIs literal.
|
|
177
|
+
const environmentHome = overrides.agentHome === undefined && overrides.home === undefined;
|
|
178
|
+
const expandedHome = environmentHome && selectedHome === "~" ? deps.home
|
|
179
|
+
: environmentHome && (selectedHome.startsWith("~/") || (process.platform === "win32" && selectedHome.startsWith("~\\"))) ? join(deps.home, selectedHome.slice(2)) : selectedHome;
|
|
180
|
+
// Freeze the host's root before a child uses a different session cwd.
|
|
181
|
+
const agentHome = resolve(expandedHome);
|
|
182
|
+
if (legacySubagentsInstalledAt(agentHome)) {
|
|
183
|
+
pi.on("session_start", (_event, ctx) => {
|
|
184
|
+
if (ctx.hasUI) ctx.ui.notify(`${AGENTS_GLYPH} Gentle Agents is waiting: remove the old package first with "pi remove npm:${LEGACY_SUBAGENTS_PACKAGE}"`, "warning");
|
|
185
|
+
});
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const collapseKey = agentsCollapseKey(env);
|
|
189
|
+
const viewKey = agentsViewKey(env);
|
|
190
|
+
const stopKey = agentsStopKey(env);
|
|
191
|
+
const store = new TaskStore();
|
|
192
|
+
const tasksDir = historyDir(deps.home, agentHome);
|
|
193
|
+
let ui: ExtensionContext["ui"] | undefined;
|
|
194
|
+
let host: { requestRender(): void } | undefined;
|
|
195
|
+
let sessions: ExtensionContext["sessionManager"] | undefined;
|
|
196
|
+
let collapsed = false;
|
|
197
|
+
let renderQueued = false;
|
|
198
|
+
let cancelClock: (() => void) | undefined;
|
|
199
|
+
const ownedTaskIds = new Set<string>();
|
|
200
|
+
const stoppingTaskIds = new Set<string>();
|
|
201
|
+
let stopAllConfirmation: Promise<void> | undefined;
|
|
202
|
+
|
|
203
|
+
// The card and its clock follow the session pi has open right now; a task
|
|
204
|
+
// started before /new or /resume stays in the store and comes back with
|
|
205
|
+
// its session. Before the first session_start there is nothing to scope by.
|
|
206
|
+
const activeSessionId = (): string | undefined => (sessions === undefined ? undefined : sessions.getSessionId() ?? "");
|
|
207
|
+
const visibleTasks = (): TaskRecord[] => store.list(activeSessionId());
|
|
208
|
+
|
|
209
|
+
const requestRender = () => {
|
|
210
|
+
if (renderQueued) return;
|
|
211
|
+
renderQueued = true;
|
|
212
|
+
deps.schedule(() => {
|
|
213
|
+
renderQueued = false;
|
|
214
|
+
host?.requestRender();
|
|
215
|
+
}, RENDER_COALESCE_MS);
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
// The elapsed column ticks once a second while something runs. Once every
|
|
219
|
+
// task is done, one frame is due when the next finished row leaves the
|
|
220
|
+
// card, so an idle terminal still sees it clear.
|
|
221
|
+
const tickClock = () => {
|
|
222
|
+
cancelClock?.();
|
|
223
|
+
cancelClock = undefined;
|
|
224
|
+
const tasks = visibleTasks();
|
|
225
|
+
if (tasks.some((task) => !isFinished(task.status))) {
|
|
226
|
+
cancelClock = deps.schedule(() => {
|
|
227
|
+
requestRender();
|
|
228
|
+
tickClock();
|
|
229
|
+
}, CLOCK_TICK_MS);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
const expiry = widgetExpiryMs(tasks, deps.now());
|
|
233
|
+
if (expiry === undefined) return;
|
|
234
|
+
cancelClock = deps.schedule(() => {
|
|
235
|
+
host?.requestRender();
|
|
236
|
+
tickClock();
|
|
237
|
+
}, expiry);
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
// A finished task goes to disk once, after its child is gone; the history
|
|
241
|
+
// is then trimmed to the configured size. Failures never reach the TUI.
|
|
242
|
+
const persist = (task: TaskRecord) => {
|
|
243
|
+
void saveTask(tasksDir, task, store.thread(task.id))
|
|
244
|
+
.then(() => pruneHistory(tasksDir, loadAgentsConfig({ cwd: task.cwd, home: deps.home, agentHome }).historyMaxTasks))
|
|
245
|
+
.catch(() => {});
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
// A background result is delivered as a message: queued behind the current
|
|
249
|
+
// turn if the model is busy, or starting a turn right away if it is idle.
|
|
250
|
+
const deliver = (task: TaskRecord) => {
|
|
251
|
+
pi.sendMessage({ customType: AGENTS_RESULT_TYPE, content: completionText(task), display: true, details: taskDetails(task) }, { deliverAs: "followUp", triggerTurn: true });
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const runner = new AgentRunner(store, loadAgentsConfig({ cwd: process.cwd(), home: deps.home, agentHome }), deps, {
|
|
255
|
+
askUser: (_taskId, ask, raw) => answerThroughUi(ui, ask, raw),
|
|
256
|
+
onFinish: (task) => {
|
|
257
|
+
ownedTaskIds.delete(task.id);
|
|
258
|
+
requestRender();
|
|
259
|
+
persist(task);
|
|
260
|
+
if (task.mode === AGENT_MODE.BACKGROUND && task.status !== TASK_STATUS.CANCELLED) deliver(task);
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
pi.registerMessageRenderer(AGENTS_RESULT_TYPE, (message, options, theme) => {
|
|
265
|
+
const details = (message.details as { gentleAgents?: { agent?: string; status?: string } } | undefined)?.gentleAgents;
|
|
266
|
+
const content = message.content as string | Array<{ type: string; text?: string }>;
|
|
267
|
+
const body = (typeof content === "string" ? content : content.map((part) => (part.type === "text" ? (part.text ?? "") : "")).join("\n")).split("\n");
|
|
268
|
+
const tone = details?.status === "completed" ? CARD_TONE.SUCCESS : CARD_TONE.ERROR;
|
|
269
|
+
const hint = expandHint(options.expanded);
|
|
270
|
+
return {
|
|
271
|
+
render(width: number) {
|
|
272
|
+
return renderCard({ title: "Agent result", subtitle: details?.agent, body, tone, glyph: AGENTS_GLYPH }, theme, width, { expanded: options.expanded, hint });
|
|
273
|
+
},
|
|
274
|
+
invalidate() {},
|
|
275
|
+
};
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
const isOwnedActive = (task: TaskRecord | undefined): task is TaskRecord => task !== undefined && ownedTaskIds.has(task.id) && !isFinished(task.status);
|
|
279
|
+
|
|
280
|
+
const stopSelected = async (task: TaskRecord, ctx: ExtensionContext): Promise<void> => {
|
|
281
|
+
const selected = store.get(task.id);
|
|
282
|
+
if (!isOwnedActive(selected)) return;
|
|
283
|
+
if (selected.status === TASK_STATUS.QUEUED) {
|
|
284
|
+
if (runner.cancel(selected.id)) ctx.ui.notify(`Stopped ${selected.agent}.`);
|
|
285
|
+
else ctx.ui.notify(`Task ${selected.agent} already finished.`, "warning");
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
if (stoppingTaskIds.has(selected.id)) return;
|
|
289
|
+
stoppingTaskIds.add(selected.id);
|
|
290
|
+
try {
|
|
291
|
+
const message = selected.status === TASK_STATUS.WAITING ? "Its pending question will be dismissed." : "Current work may be incomplete.";
|
|
292
|
+
if (!await ctx.ui.confirm(`Stop ${selected.agent}?`, message)) return;
|
|
293
|
+
const current = store.get(selected.id);
|
|
294
|
+
if (!isOwnedActive(current)) {
|
|
295
|
+
ctx.ui.notify(`Task ${selected.agent} already finished.`, "warning");
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (runner.cancel(current.id)) ctx.ui.notify(`Stopped ${current.agent}.`);
|
|
299
|
+
else ctx.ui.notify(`Task ${current.agent} already finished.`, "warning");
|
|
300
|
+
} finally {
|
|
301
|
+
stoppingTaskIds.delete(selected.id);
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const stopAll = (ctx: ExtensionContext): Promise<void> => {
|
|
306
|
+
if (stopAllConfirmation) return stopAllConfirmation;
|
|
307
|
+
const active = store.list().filter(isOwnedActive);
|
|
308
|
+
if (active.length === 0) {
|
|
309
|
+
ctx.ui.notify("No active subagents to stop.");
|
|
310
|
+
return Promise.resolve();
|
|
311
|
+
}
|
|
312
|
+
const count = active.length;
|
|
313
|
+
const noun = count === 1 ? "subagent" : "subagents";
|
|
314
|
+
const confirmation = (async () => {
|
|
315
|
+
try {
|
|
316
|
+
if (!await ctx.ui.confirm(`Stop ${count} active ${noun}?`, `Only these ${count} ${noun} will stop. Current work may be incomplete.`)) return;
|
|
317
|
+
const cancelled = active.filter((task) => runner.cancel(task.id)).length;
|
|
318
|
+
ctx.ui.notify(`Stopped ${cancelled} ${cancelled === 1 ? "subagent" : "subagents"}.`);
|
|
319
|
+
} finally {
|
|
320
|
+
stopAllConfirmation = undefined;
|
|
321
|
+
}
|
|
322
|
+
})();
|
|
323
|
+
stopAllConfirmation = confirmation;
|
|
324
|
+
return confirmation;
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
// Tasks from earlier sessions come back from disk on demand.
|
|
328
|
+
const resolveTask = async (id: string): Promise<TaskRecord | undefined> => {
|
|
329
|
+
const live = store.get(id);
|
|
330
|
+
if (live) return live;
|
|
331
|
+
const stored = await loadStoredTask(tasksDir, id);
|
|
332
|
+
if (stored) store.restore(stored.task, stored.thread);
|
|
333
|
+
return stored?.task;
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
const openOverlay = async (ctx: ExtensionContext) => {
|
|
337
|
+
if (!ctx.hasUI) return;
|
|
338
|
+
for (const stored of await loadHistory(tasksDir)) store.restore(stored.task, stored.thread);
|
|
339
|
+
let view: AgentsView | undefined;
|
|
340
|
+
let overlayHost: { requestRender(force?: boolean): void; stop(): void; start(): void } | undefined;
|
|
341
|
+
const chosen = await ctx.ui.custom<TaskRecord | null>(
|
|
342
|
+
(tui, theme, _keybindings, done) => {
|
|
343
|
+
overlayHost = tui;
|
|
344
|
+
view = new AgentsView({
|
|
345
|
+
theme,
|
|
346
|
+
rows: Math.max(OVERLAY_MIN_ROWS, Math.floor(tui.terminal.rows * OVERLAY_HEIGHT_RATIO)),
|
|
347
|
+
store,
|
|
348
|
+
sessionId: ctx.sessionManager.getSessionId() ?? "",
|
|
349
|
+
now: () => deps.now(),
|
|
350
|
+
onCancel: (task) => void stopSelected(task, ctx),
|
|
351
|
+
canCancel: isOwnedActive,
|
|
352
|
+
onOpen: (task) => done(task),
|
|
353
|
+
onClose: () => done(null),
|
|
354
|
+
requestRender: () => tui.requestRender(),
|
|
355
|
+
});
|
|
356
|
+
const interaction = createNativeFullscreenInteraction({
|
|
357
|
+
keyboardTarget: view,
|
|
358
|
+
requestRender: () => tui.requestRender(),
|
|
359
|
+
mouseObserver: view.mouseObserver(),
|
|
360
|
+
});
|
|
361
|
+
interaction.addChild(view);
|
|
362
|
+
return interaction;
|
|
363
|
+
},
|
|
364
|
+
{ overlay: true, overlayOptions: { width: "92%", anchor: "center" } },
|
|
365
|
+
);
|
|
366
|
+
view?.dispose();
|
|
367
|
+
if (!chosen || !overlayHost) return;
|
|
368
|
+
if (!chosen.sessionPath) {
|
|
369
|
+
ctx.ui.notify("This task has no session file yet.", "warning");
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
// The child's session is JSONL; the reader gets a markdown transcript.
|
|
373
|
+
let transcriptPath: string;
|
|
374
|
+
try {
|
|
375
|
+
transcriptPath = await writeTranscript(chosen);
|
|
376
|
+
} catch (error) {
|
|
377
|
+
ctx.ui.notify(`Could not read the task's session: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
if (!openInExternalEditor(overlayHost, transcriptPath)) ctx.ui.notify("No editor configured. Set $VISUAL or $EDITOR.", "warning");
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
const writeTranscript = async (task: TaskRecord): Promise<string> => {
|
|
384
|
+
const dir = agentRuntimePaths(deps.home, agentHome).transcripts;
|
|
385
|
+
await mkdir(dir, { recursive: true });
|
|
386
|
+
const markdown = sessionToMarkdown(await readFile(task.sessionPath ?? "", "utf8"), { title: `${task.agent} · ${task.label} · ${task.status}` });
|
|
387
|
+
const path = join(dir, `${task.id}.md`);
|
|
388
|
+
await writeFile(path, markdown, "utf8");
|
|
389
|
+
return path;
|
|
390
|
+
};
|
|
391
|
+
// A status change is worth a frame right away; deltas inside a task are
|
|
392
|
+
// coalesced so a chatty child cannot flood the terminal.
|
|
393
|
+
store.subscribeSummary(() => {
|
|
394
|
+
host?.requestRender();
|
|
395
|
+
tickClock();
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
const showWidget = (ctx: ExtensionContext) => {
|
|
399
|
+
ui = ctx.hasUI ? ctx.ui : undefined;
|
|
400
|
+
sessions = ctx.sessionManager;
|
|
401
|
+
tickClock();
|
|
402
|
+
ui?.setWidget(AGENTS_WIDGET_KEY, (tui, theme) => {
|
|
403
|
+
host = tui;
|
|
404
|
+
return {
|
|
405
|
+
render(width: number) {
|
|
406
|
+
const lines = renderAgentsCard(visibleTasks(), theme, width, deps.now(), { collapsed, collapseKey, maxRows: widgetRows(tui.terminal?.rows), viewKey });
|
|
407
|
+
return lines.length === 0 ? [] : [...lines, ""];
|
|
408
|
+
},
|
|
409
|
+
invalidate() {},
|
|
410
|
+
};
|
|
411
|
+
});
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
const roots = (ctx: ExtensionContext) => ({ cwd: ctx.sessionManager.getCwd(), home: deps.home, agentHome });
|
|
415
|
+
|
|
416
|
+
const buildRequest = (ctx: ExtensionContext, agent: AgentDefinition, prompt: string, label: string | undefined, context: string | undefined, mode: AgentMode, resume?: string): TaskRequest => {
|
|
417
|
+
const config = loadAgentsConfig(roots(ctx));
|
|
418
|
+
const profile = resolveAgentProfile(agent, config);
|
|
419
|
+
const sessionDir = agentRuntimePaths(deps.home, agentHome).sessions;
|
|
420
|
+
mkdirSync(sessionDir, { recursive: true });
|
|
421
|
+
const parentSessionManager = ctx.sessionManager as unknown as ReviewSessionManager;
|
|
422
|
+
const parentSessionId = ctx.sessionManager.getSessionId() ?? "";
|
|
423
|
+
const parentWorktreeRoot = ctx.sessionManager.getCwd();
|
|
424
|
+
const parentRepositoryIdentity = resolveCanonicalGitRepositoryIdentitySync(parentWorktreeRoot);
|
|
425
|
+
return {
|
|
426
|
+
agent,
|
|
427
|
+
prompt,
|
|
428
|
+
label,
|
|
429
|
+
context,
|
|
430
|
+
mode,
|
|
431
|
+
cwd: parentWorktreeRoot,
|
|
432
|
+
parentSessionId,
|
|
433
|
+
model: profile.model,
|
|
434
|
+
thinking: profile.thinking,
|
|
435
|
+
sessionDir,
|
|
436
|
+
resumeSessionPath: resume,
|
|
437
|
+
env: deps.env,
|
|
438
|
+
...(parentRepositoryIdentity === undefined ? {} : {
|
|
439
|
+
authorizeParentStandingReviewPermission: (repositoryIdentity: string) => {
|
|
440
|
+
try {
|
|
441
|
+
return repositoryIdentity === parentRepositoryIdentity &&
|
|
442
|
+
parentSessionManager.getSessionId() === parentSessionId &&
|
|
443
|
+
parentSessionId.length > 0 &&
|
|
444
|
+
hasReviewSessionPermission({
|
|
445
|
+
sessionManager: parentSessionManager,
|
|
446
|
+
sessionId: parentSessionId,
|
|
447
|
+
worktreeRoot: parentWorktreeRoot,
|
|
448
|
+
repositoryIdentity: parentRepositoryIdentity,
|
|
449
|
+
});
|
|
450
|
+
} catch {
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
453
|
+
},
|
|
454
|
+
}),
|
|
455
|
+
};
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
const launch = async (ctx: ExtensionContext, request: TaskRequest): Promise<ToolText> => {
|
|
459
|
+
const task = runner.run(request);
|
|
460
|
+
ownedTaskIds.add(task.id);
|
|
461
|
+
store.subscribe(task.id, () => requestRender());
|
|
462
|
+
if (request.mode === AGENT_MODE.BACKGROUND) return text(`Started ${task.agent} in the background as task ${task.id}. Use subagent_status or subagent_result with that id.`, taskDetails(task));
|
|
463
|
+
const finished = await runner.waitFor(task.id);
|
|
464
|
+
return text(finishedText(finished), taskDetails(finished));
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
const tool = (name: string, description: string, parameters: Record<string, unknown>, execute: (params: Record<string, unknown>, ctx: ExtensionContext) => Promise<ToolText>) => {
|
|
468
|
+
pi.registerTool({
|
|
469
|
+
name: `${TOOL_PREFIX}${name}`,
|
|
470
|
+
label: `Agent ${name.replace(/_/g, " ")}`,
|
|
471
|
+
description,
|
|
472
|
+
parameters: { type: "object", additionalProperties: false, ...parameters } as never,
|
|
473
|
+
renderCall(args, theme) {
|
|
474
|
+
const params = args as { agent?: string; task_id?: string };
|
|
475
|
+
return new Text(theme.fg("toolTitle", `${AGENTS_GLYPH} agent ${name.replace(/_/g, " ")}${params.agent ? ` · ${params.agent}` : params.task_id ? ` · ${params.task_id}` : ""}`), 0, 0);
|
|
476
|
+
},
|
|
477
|
+
renderResult(result, options, theme) {
|
|
478
|
+
const body = result.content.map((part) => (part.type === "text" ? part.text : "")).join("\n");
|
|
479
|
+
return new Text(options.expanded ? body : theme.fg("muted", body.split("\n")[0] ?? ""), 0, 0);
|
|
480
|
+
},
|
|
481
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
482
|
+
return execute(params as Record<string, unknown>, ctx);
|
|
483
|
+
},
|
|
484
|
+
});
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
tool("list_agents", "List the subagents defined for this project and user, with their descriptions.", { properties: {} }, async (_params, ctx) => {
|
|
488
|
+
const { agents, errors } = discoverAgents(roots(ctx));
|
|
489
|
+
const lines = agents.map((agent) => `- ${agent.name} (${agent.scope}): ${agent.description || "no description"}`);
|
|
490
|
+
const problems = errors.map((error) => `! ${error}`);
|
|
491
|
+
return text(lines.length === 0 ? "No subagents defined." : [...lines, ...problems].join("\n"));
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
tool(
|
|
495
|
+
"run",
|
|
496
|
+
"Delegate a task to a named subagent. Task mode waits for the answer; background mode returns a task id immediately.",
|
|
497
|
+
{
|
|
498
|
+
required: ["agent", "task"],
|
|
499
|
+
properties: {
|
|
500
|
+
agent: { type: "string", description: "Subagent name from subagent_list_agents." },
|
|
501
|
+
task: { type: "string", description: "What the subagent must do, self-contained." },
|
|
502
|
+
label: { type: "string", description: "Three to six words naming the work, shown on the agents card, e.g. 'map footer data sources'." },
|
|
503
|
+
context: { type: "string", description: "Optional extra context appended to the task." },
|
|
504
|
+
mode: { type: "string", enum: ["task", "background"], description: "task waits for the result (default); background returns immediately." },
|
|
505
|
+
},
|
|
506
|
+
},
|
|
507
|
+
async (params, ctx) => {
|
|
508
|
+
const { agents } = discoverAgents(roots(ctx));
|
|
509
|
+
const agent = agents.find((candidate) => candidate.name === params.agent);
|
|
510
|
+
if (!agent) return text(`Error: no subagent named "${String(params.agent)}". Known: ${agents.map((candidate) => candidate.name).join(", ") || "none"}`, { error: "unknown agent" });
|
|
511
|
+
const mode = (params.mode as AgentMode | undefined) ?? agent.mode ?? loadAgentsConfig(roots(ctx)).defaultMode;
|
|
512
|
+
return launch(ctx, buildRequest(ctx, agent, String(params.task ?? ""), typeof params.label === "string" ? params.label : undefined, typeof params.context === "string" ? params.context : undefined, mode));
|
|
513
|
+
},
|
|
514
|
+
);
|
|
515
|
+
|
|
516
|
+
tool("status", "Report the status of one subagent task.", { required: ["task_id"], properties: { task_id: { type: "string" } } }, async (params) => {
|
|
517
|
+
const task = await resolveTask(String(params.task_id));
|
|
518
|
+
return task ? text(describeTask(task), taskDetails(task)) : text(`Error: no task ${String(params.task_id)}`, { error: "unknown task" });
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
tool("result", "Return the final answer of a finished subagent task, or its current state if it is still running.", { required: ["task_id"], properties: { task_id: { type: "string" } } }, async (params) => {
|
|
522
|
+
const task = await resolveTask(String(params.task_id));
|
|
523
|
+
if (!task) return text(`Error: no task ${String(params.task_id)}`, { error: "unknown task" });
|
|
524
|
+
return text(isFinished(task.status) ? finishedText(task) : `Task ${task.id} is still ${task.status} (last: ${task.lastStep}).`, taskDetails(task));
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
tool("list_tasks", "List the subagent tasks of this session, newest first.", { properties: {} }, async (_params, ctx) => {
|
|
528
|
+
const tasks = store.list(ctx.sessionManager.getSessionId() ?? "");
|
|
529
|
+
return text(tasks.length === 0 ? "No subagent tasks in this session." : tasks.map(describeTask).join("\n"));
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
tool("cancel", "Cancel a queued or running subagent task.", { required: ["task_id"], properties: { task_id: { type: "string" } } }, async (params) => {
|
|
533
|
+
const id = String(params.task_id);
|
|
534
|
+
return runner.cancel(id) ? text(`Cancelled task ${id}.`) : text(`Error: task ${id} is not running.`, { error: "not running" });
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
tool("send_message", "Steer a running subagent with a message delivered before its next model call.", { required: ["task_id", "message"], properties: { task_id: { type: "string" }, message: { type: "string" } } }, async (params) => {
|
|
538
|
+
const id = String(params.task_id);
|
|
539
|
+
return runner.steer(id, String(params.message ?? "")) ? text(`Message queued for task ${id}.`) : text(`Error: task ${id} is not running.`, { error: "not running" });
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
tool(
|
|
543
|
+
"continue",
|
|
544
|
+
"Resume a finished subagent task in its own session with a follow-up prompt.",
|
|
545
|
+
{ required: ["task_id", "prompt"], properties: { task_id: { type: "string" }, prompt: { type: "string" }, label: { type: "string", description: "Three to six words naming the follow-up." }, mode: { type: "string", enum: ["task", "background"] } } },
|
|
546
|
+
async (params, ctx) => {
|
|
547
|
+
const previous = await resolveTask(String(params.task_id));
|
|
548
|
+
if (!previous) return text(`Error: no task ${String(params.task_id)}`, { error: "unknown task" });
|
|
549
|
+
if (!isFinished(previous.status) || !previous.sessionPath) return text(`Error: task ${previous.id} cannot be continued yet (${previous.status}).`, { error: "not continuable" });
|
|
550
|
+
const agent = discoverAgents(roots(ctx)).agents.find((candidate) => candidate.name === previous.agent);
|
|
551
|
+
if (!agent) return text(`Error: subagent "${previous.agent}" is no longer defined.`, { error: "unknown agent" });
|
|
552
|
+
const mode = (params.mode as AgentMode | undefined) ?? (previous.mode as AgentMode);
|
|
553
|
+
return launch(ctx, buildRequest(ctx, agent, String(params.prompt ?? ""), typeof params.label === "string" ? params.label : undefined, undefined, mode, previous.sessionPath));
|
|
554
|
+
},
|
|
555
|
+
);
|
|
556
|
+
|
|
557
|
+
if (collapseKey) {
|
|
558
|
+
pi.registerShortcut(collapseKey as Parameters<ExtensionAPI["registerShortcut"]>[0], {
|
|
559
|
+
description: "Collapse or expand the agents card",
|
|
560
|
+
handler: async () => {
|
|
561
|
+
collapsed = !collapsed;
|
|
562
|
+
host?.requestRender();
|
|
563
|
+
},
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
pi.registerCommand(AGENTS_COMMAND_NAME, {
|
|
568
|
+
description: "Show this session's subagents with their threads; a widens the list to every session. Press o to open a task's session in $EDITOR.",
|
|
569
|
+
handler: async (_args, ctx) => openOverlay(ctx),
|
|
570
|
+
});
|
|
571
|
+
if (viewKey) {
|
|
572
|
+
pi.registerShortcut(viewKey as Parameters<ExtensionAPI["registerShortcut"]>[0], {
|
|
573
|
+
description: "Show the subagents overlay",
|
|
574
|
+
handler: async (ctx) => openOverlay(ctx),
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
if (stopKey) {
|
|
578
|
+
pi.registerShortcut(stopKey as Parameters<ExtensionAPI["registerShortcut"]>[0], {
|
|
579
|
+
description: "Stop active subagent(s)",
|
|
580
|
+
handler: async (ctx) => stopAll(ctx),
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
pi.on("session_start", (_event, ctx) => showWidget(ctx));
|
|
585
|
+
pi.on("session_shutdown", () => {
|
|
586
|
+
runner.cancelAll();
|
|
587
|
+
});
|
|
588
|
+
}
|