Added nono profiles & orca integration - Added some extensions to start working on
This commit is contained in:
@@ -0,0 +1,746 @@
|
||||
/**
|
||||
* Subagents — spawn background subagents on one of three backends
|
||||
* (pi, Claude Code, Codex) unified behind a single Effect service interface.
|
||||
*
|
||||
* Tools (for the parent LLM):
|
||||
* - subagent_spawn: fire-and-forget spawn (prompt, title, agent, working_dir,
|
||||
* model, reasoning_effort). Max 4 running at once across all backends.
|
||||
* - subagent_wait: block until the listed subagents settle, return results.
|
||||
* - subagent_cancel: stop one or more running subagents.
|
||||
* - subagent_check: peek at a subagent's status and recent activity.
|
||||
* - subagent_list: list all subagents.
|
||||
*
|
||||
* Unawaited subagents queue their result as a follow-up message when they
|
||||
* settle. `/subagents` opens a picker + full interactive takeover view.
|
||||
*
|
||||
* Architecture: Effect v4 generators throughout (backends -> manager ->
|
||||
* runtime); this file is the async boundary where tool handlers run effects
|
||||
* against one shared ManagedRuntime. All three backends are real: pi runs
|
||||
* in-process SDK sessions, claude drives the Claude Agent SDK, codex speaks
|
||||
* JSON-RPC to a scoped `codex app-server` process.
|
||||
*/
|
||||
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { StringEnum } from "@earendil-works/pi-ai";
|
||||
import type {
|
||||
ExtensionAPI,
|
||||
ExtensionCommandContext,
|
||||
ExtensionContext,
|
||||
ExtensionUIContext,
|
||||
} from "@earendil-works/pi-coding-agent";
|
||||
import {
|
||||
DEFAULT_MAX_BYTES,
|
||||
DEFAULT_MAX_LINES,
|
||||
formatSize,
|
||||
getAgentDir,
|
||||
getMarkdownTheme,
|
||||
ProjectTrustStore,
|
||||
truncateHead,
|
||||
} from "@earendil-works/pi-coding-agent";
|
||||
import { Markdown, Text } from "@earendil-works/pi-tui";
|
||||
import { Type } from "typebox";
|
||||
import { deriveBtwTitle, isModelVisible } from "./src/by-the-way.ts";
|
||||
import {
|
||||
BACKEND_NAMES,
|
||||
formatElapsed,
|
||||
latestText,
|
||||
REASONING_EFFORTS,
|
||||
type SubagentSnapshot,
|
||||
} from "./src/domain.ts";
|
||||
import {
|
||||
formatActivityStatus,
|
||||
formatContextUtilization,
|
||||
} from "./src/format.ts";
|
||||
import { SubagentManager, type SubagentManagerShape } from "./src/manager.ts";
|
||||
import {
|
||||
buildSubagentResultMessage,
|
||||
buildSubagentSpawnResult,
|
||||
SUBAGENT_CANCEL_PARAMETER_DESCRIPTIONS,
|
||||
SUBAGENT_CANCEL_TOOL_DESCRIPTION,
|
||||
SUBAGENT_CHECK_PARAMETER_DESCRIPTIONS,
|
||||
SUBAGENT_CHECK_TOOL_DESCRIPTION,
|
||||
SUBAGENT_LIST_TOOL_DESCRIPTION,
|
||||
SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS,
|
||||
SUBAGENT_SPAWN_PROMPT_GUIDELINES,
|
||||
SUBAGENT_SPAWN_PROMPT_SNIPPET,
|
||||
SUBAGENT_SPAWN_TOOL_DESCRIPTION,
|
||||
SUBAGENT_WAIT_PARAMETER_DESCRIPTIONS,
|
||||
SUBAGENT_WAIT_TOOL_DESCRIPTION,
|
||||
} from "./src/prompt.ts";
|
||||
import { createDeferredResultDelivery } from "./src/result-delivery.ts";
|
||||
import {
|
||||
createSubagentRuntime,
|
||||
runTool,
|
||||
type SubagentRuntime,
|
||||
} from "./src/runtime.ts";
|
||||
import { openSubagentPicker, openSubagentTakeover } from "./src/ui/takeover.ts";
|
||||
|
||||
const SUBAGENT_OUTPUT_MAX_BYTES = 24 * 1024;
|
||||
const WAIT_OUTPUT_MAX_BYTES = 48 * 1024;
|
||||
const WAIT_PER_AGENT_MAX_BYTES = 16 * 1024;
|
||||
|
||||
interface BtwResultData {
|
||||
readonly id: string;
|
||||
readonly title: string;
|
||||
readonly status: SubagentSnapshot["status"];
|
||||
readonly errorText?: string;
|
||||
readonly prompt: string;
|
||||
readonly answer: string;
|
||||
readonly sessionFilePath?: string;
|
||||
}
|
||||
|
||||
function describeSubagent(snap: SubagentSnapshot) {
|
||||
const details = [
|
||||
`${snap.backend}: ${snap.meta.modelLabel ?? "?"}`,
|
||||
formatContextUtilization(snap.usage),
|
||||
formatElapsed(snap),
|
||||
snap.cwd,
|
||||
].filter(Boolean);
|
||||
return `${snap.id} [${snap.status}] "${snap.title}" (${details.join(", ")})`;
|
||||
}
|
||||
|
||||
function truncatedOutput(
|
||||
snap: SubagentSnapshot,
|
||||
maxBytes = SUBAGENT_OUTPUT_MAX_BYTES,
|
||||
): string {
|
||||
const output = snap.finalText || "(no output)";
|
||||
const truncation = truncateHead(output, {
|
||||
maxBytes: Math.min(maxBytes, DEFAULT_MAX_BYTES),
|
||||
maxLines: Math.min(600, DEFAULT_MAX_LINES),
|
||||
});
|
||||
let text = truncation.content;
|
||||
if (truncation.truncated) {
|
||||
text += `\n\n[Output truncated: ${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)} shown. Full transcript in session file: ${snap.meta.sessionFilePath ?? "?"}]`;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Same-directory children inherit the live parent decision. An alternate cwd
|
||||
* is trusted only when pi's persisted trust store explicitly trusts it (or a
|
||||
* containing directory); unreadable/invalid trust data fails closed.
|
||||
*/
|
||||
function resolveChildProjectTrust(options: {
|
||||
parentCwd: string;
|
||||
childCwd: string;
|
||||
parentTrusted: boolean;
|
||||
}) {
|
||||
if (path.resolve(options.childCwd) === path.resolve(options.parentCwd)) {
|
||||
return options.parentTrusted;
|
||||
}
|
||||
try {
|
||||
const trustStore = new ProjectTrustStore(getAgentDir());
|
||||
return trustStore.get(options.childCwd) === true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export default function (pi: ExtensionAPI) {
|
||||
let runtime: SubagentRuntime | undefined;
|
||||
let managerPromise: Promise<SubagentManagerShape> | undefined;
|
||||
let sessionContext: ExtensionContext | undefined;
|
||||
let ui: ExtensionUIContext | undefined;
|
||||
let unsubStatus: (() => void) | undefined;
|
||||
const resultDelivery = createDeferredResultDelivery<SubagentSnapshot>();
|
||||
|
||||
const getRuntime = () => (runtime ??= createSubagentRuntime());
|
||||
|
||||
/** Resolve the manager service once per runtime and wire the extension hooks. */
|
||||
const getManager = () => {
|
||||
managerPromise ??= getRuntime()
|
||||
.runPromise(SubagentManager)
|
||||
.then((manager) => {
|
||||
manager.view.setOnSettled(onSettled);
|
||||
unsubStatus?.();
|
||||
unsubStatus = manager.view.subscribe(() => updateStatus(manager));
|
||||
updateStatus(manager);
|
||||
return manager;
|
||||
});
|
||||
return managerPromise;
|
||||
};
|
||||
|
||||
const updateStatus = (manager: SubagentManagerShape) => {
|
||||
if (!ui) return;
|
||||
const subs = manager.view.list();
|
||||
if (subs.length === 0) {
|
||||
ui.setStatus("subagents", undefined);
|
||||
return;
|
||||
}
|
||||
const running = subs.filter((snap) => snap.status === "running").length;
|
||||
const failed = subs.filter((snap) => snap.status === "error").length;
|
||||
const done = subs.length - running - failed;
|
||||
ui.setStatus(
|
||||
"subagents",
|
||||
formatActivityStatus(ui.theme, { running, done, failed }),
|
||||
);
|
||||
};
|
||||
|
||||
const deliverResult = (snap: SubagentSnapshot) => {
|
||||
pi.sendMessage(
|
||||
{
|
||||
customType: "subagent-result",
|
||||
content: buildSubagentResultMessage({
|
||||
id: snap.id,
|
||||
title: snap.title,
|
||||
status: snap.status,
|
||||
errorText: snap.errorText,
|
||||
output: truncatedOutput(snap),
|
||||
}),
|
||||
display: true,
|
||||
details: { id: snap.id, title: snap.title, status: snap.status },
|
||||
},
|
||||
{ deliverAs: "followUp", triggerTurn: true },
|
||||
);
|
||||
};
|
||||
|
||||
const flushResults = () => {
|
||||
for (const snap of resultDelivery.drain()) deliverResult(snap);
|
||||
};
|
||||
|
||||
const deliverBtwResult = (snap: SubagentSnapshot) => {
|
||||
// appendEntry is a synchronous SessionManager operation and emits an
|
||||
// entry_appended event, so it is safe while the parent is streaming and
|
||||
// never enters the model's context or follow-up queue.
|
||||
pi.appendEntry<BtwResultData>("btw-result", {
|
||||
id: snap.id,
|
||||
title: snap.title,
|
||||
status: snap.status,
|
||||
errorText: snap.errorText,
|
||||
prompt: snap.prompt,
|
||||
answer: truncatedOutput(snap),
|
||||
sessionFilePath: snap.meta.sessionFilePath,
|
||||
});
|
||||
ui?.notify(
|
||||
snap.status === "error"
|
||||
? `by the way “${snap.title}” failed — reopen it with /subagents`
|
||||
: `by the way “${snap.title}” answered — reopen it with /subagents`,
|
||||
snap.status === "error" ? "error" : "info",
|
||||
);
|
||||
};
|
||||
|
||||
const onSettled = (snap: SubagentSnapshot, consumed: boolean) => {
|
||||
// A shutdown can settle children while disposing their scopes. Never
|
||||
// append into a session whose extension runtime is already closing.
|
||||
if (!sessionContext) return;
|
||||
if (snap.origin === "btw") {
|
||||
deliverBtwResult({ ...snap, meta: { ...snap.meta } });
|
||||
return;
|
||||
}
|
||||
if (consumed) {
|
||||
resultDelivery.consume([snap.id]);
|
||||
return;
|
||||
}
|
||||
// Keep the result retractable while the parent is working. A later
|
||||
// subagent_wait can consume it before agent_settled flushes follow-ups.
|
||||
// Defer a copy: the live snapshot keeps mutating if the subagent is
|
||||
// restarted before the deferred result flushes.
|
||||
resultDelivery.defer({ ...snap, meta: { ...snap.meta } });
|
||||
if (sessionContext?.isIdle()) flushResults();
|
||||
};
|
||||
|
||||
pi.on("session_start", (_event, ctx) => {
|
||||
sessionContext = ctx;
|
||||
if (ctx.hasUI) ui = ctx.ui;
|
||||
});
|
||||
|
||||
pi.on("agent_settled", flushResults);
|
||||
|
||||
pi.on("session_shutdown", async () => {
|
||||
sessionContext = undefined;
|
||||
resultDelivery.clear();
|
||||
unsubStatus?.();
|
||||
unsubStatus = undefined;
|
||||
ui?.setStatus("subagents", undefined);
|
||||
ui = undefined;
|
||||
const closing = runtime;
|
||||
runtime = undefined;
|
||||
managerPromise = undefined;
|
||||
// Disposing the runtime runs the manager finalizer, which tears down all
|
||||
// subagent scopes (and, later, their real child processes).
|
||||
await closing?.dispose();
|
||||
});
|
||||
|
||||
// --- Tools -------------------------------------------------------------
|
||||
|
||||
pi.registerTool({
|
||||
name: "subagent_spawn",
|
||||
label: "Spawn Subagent",
|
||||
description: SUBAGENT_SPAWN_TOOL_DESCRIPTION,
|
||||
promptSnippet: SUBAGENT_SPAWN_PROMPT_SNIPPET,
|
||||
promptGuidelines: SUBAGENT_SPAWN_PROMPT_GUIDELINES,
|
||||
parameters: Type.Object({
|
||||
prompt: Type.String({
|
||||
description: SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.prompt,
|
||||
}),
|
||||
name: Type.String({
|
||||
description: SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.name,
|
||||
}),
|
||||
harness: StringEnum(BACKEND_NAMES, {
|
||||
description: SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.harness,
|
||||
}),
|
||||
working_dir: Type.Optional(
|
||||
Type.String({
|
||||
description: SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.workingDir,
|
||||
}),
|
||||
),
|
||||
model: Type.Optional(
|
||||
Type.String({
|
||||
description: SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.model,
|
||||
}),
|
||||
),
|
||||
reasoning_effort: Type.Optional(
|
||||
StringEnum(REASONING_EFFORTS, {
|
||||
description: SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.reasoningEffort,
|
||||
}),
|
||||
),
|
||||
}),
|
||||
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
||||
const manager = await getManager();
|
||||
const harness = params.harness;
|
||||
|
||||
const cwd = path.resolve(ctx.cwd, params.working_dir ?? ".");
|
||||
if (!fs.existsSync(cwd) || !fs.statSync(cwd).isDirectory()) {
|
||||
throw new Error(`working_dir is not a directory: ${cwd}`);
|
||||
}
|
||||
|
||||
const title = params.name.trim().slice(0, 160) || "subagent";
|
||||
const snap = await runTool(
|
||||
getRuntime(),
|
||||
manager.spawn(harness, {
|
||||
prompt: params.prompt,
|
||||
title,
|
||||
cwd,
|
||||
model: params.model,
|
||||
reasoningEffort: params.reasoning_effort,
|
||||
parent: {
|
||||
parentCwd: ctx.cwd,
|
||||
projectTrusted: resolveChildProjectTrust({
|
||||
parentCwd: ctx.cwd,
|
||||
childCwd: cwd,
|
||||
parentTrusted: ctx.isProjectTrusted(),
|
||||
}),
|
||||
inheritedModel: ctx.model
|
||||
? { provider: ctx.model.provider, id: ctx.model.id }
|
||||
: undefined,
|
||||
inheritedThinkingLevel: pi.getThinkingLevel(),
|
||||
modelRegistry: ctx.modelRegistry,
|
||||
},
|
||||
}),
|
||||
{ signal, interruptMessage: "Subagent spawn aborted." },
|
||||
);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: buildSubagentSpawnResult({
|
||||
id: snap.id,
|
||||
title: snap.title,
|
||||
harness,
|
||||
modelLabel: snap.meta.modelLabel ?? "?",
|
||||
cwd,
|
||||
}),
|
||||
},
|
||||
],
|
||||
details: {
|
||||
id: snap.id,
|
||||
title: snap.title,
|
||||
cwd,
|
||||
harness,
|
||||
model: snap.meta.modelLabel,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
pi.registerTool({
|
||||
name: "subagent_wait",
|
||||
label: "Wait for Subagents",
|
||||
description: SUBAGENT_WAIT_TOOL_DESCRIPTION,
|
||||
parameters: Type.Object({
|
||||
ids: Type.Array(Type.String(), {
|
||||
maxItems: 64,
|
||||
description: SUBAGENT_WAIT_PARAMETER_DESCRIPTIONS.ids,
|
||||
}),
|
||||
}),
|
||||
async execute(_toolCallId, params, signal, onUpdate) {
|
||||
const manager = await getManager();
|
||||
const ids = [...new Set(params.ids)];
|
||||
if (ids.length === 0)
|
||||
throw new Error("Provide at least one subagent id.");
|
||||
const known = manager.view
|
||||
.list()
|
||||
.filter(isModelVisible)
|
||||
.map((snap) => snap.id);
|
||||
const unknown = ids.filter((id) => {
|
||||
const snap = manager.view.get(id);
|
||||
return !snap || !isModelVisible(snap);
|
||||
});
|
||||
if (unknown.length > 0) {
|
||||
throw new Error(
|
||||
`Unknown subagent id(s): ${unknown.join(", ")}. Known: ${known.join(", ") || "none"}.`,
|
||||
);
|
||||
}
|
||||
|
||||
await runTool(
|
||||
getRuntime(),
|
||||
manager.waitFor(ids, (pending) => {
|
||||
onUpdate?.({
|
||||
content: [
|
||||
{ type: "text", text: `Waiting for ${pending.join(", ")}...` },
|
||||
],
|
||||
details: { pending },
|
||||
});
|
||||
}),
|
||||
{ signal, interruptMessage: "Wait aborted. Subagents keep running." },
|
||||
);
|
||||
|
||||
// Settlement may have happened before this wait began. Remove any
|
||||
// deferred automatic delivery now that the tool is returning the result.
|
||||
resultDelivery.consume(ids);
|
||||
|
||||
const sections: string[] = [];
|
||||
let remainingBytes = WAIT_OUTPUT_MAX_BYTES;
|
||||
for (const id of ids) {
|
||||
const snap = manager.view.get(id);
|
||||
if (!snap) {
|
||||
sections.push(`## ${id}\n\n(no longer tracked)`);
|
||||
continue;
|
||||
}
|
||||
const verb = snap.status === "error" ? "failed" : "finished";
|
||||
let section = `## ${snap.id} "${snap.title}" ${verb}`;
|
||||
if (snap.errorText) section += `\nError: ${snap.errorText}`;
|
||||
const headerBytes = Buffer.byteLength(section, "utf8") + 2;
|
||||
const outputBudget = Math.max(
|
||||
512,
|
||||
Math.min(WAIT_PER_AGENT_MAX_BYTES, remainingBytes - headerBytes),
|
||||
);
|
||||
section += `\n\n${truncatedOutput(snap, outputBudget)}`;
|
||||
const sectionBytes = Buffer.byteLength(section, "utf8");
|
||||
if (sectionBytes > remainingBytes) {
|
||||
sections.push(
|
||||
`## ${snap.id} "${snap.title}"\n\n[omitted: total wait output limit reached]`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
sections.push(section);
|
||||
remainingBytes -= sectionBytes;
|
||||
}
|
||||
|
||||
const combined = sections.join("\n\n---\n\n");
|
||||
const bounded = truncateHead(combined, {
|
||||
maxBytes: WAIT_OUTPUT_MAX_BYTES - 128,
|
||||
maxLines: DEFAULT_MAX_LINES,
|
||||
});
|
||||
const text = bounded.truncated
|
||||
? `${bounded.content}\n\n[wait output truncated at the total output limit]`
|
||||
: bounded.content;
|
||||
return {
|
||||
content: [{ type: "text", text }],
|
||||
details: {
|
||||
results: ids.map((id) => {
|
||||
const snap = manager.view.get(id);
|
||||
return { id, title: snap?.title, status: snap?.status };
|
||||
}),
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
pi.registerTool({
|
||||
name: "subagent_cancel",
|
||||
label: "Cancel Subagents",
|
||||
description: SUBAGENT_CANCEL_TOOL_DESCRIPTION,
|
||||
parameters: Type.Object({
|
||||
ids: Type.Array(Type.String(), {
|
||||
description: SUBAGENT_CANCEL_PARAMETER_DESCRIPTIONS.ids,
|
||||
}),
|
||||
}),
|
||||
async execute(_toolCallId, params, signal) {
|
||||
const manager = await getManager();
|
||||
const ids = [...new Set(params.ids)];
|
||||
if (ids.length === 0)
|
||||
throw new Error("Provide at least one subagent id.");
|
||||
|
||||
const known = manager.view
|
||||
.list()
|
||||
.filter(isModelVisible)
|
||||
.map((snap) => snap.id);
|
||||
const unknown = ids.filter((id) => {
|
||||
const snap = manager.view.get(id);
|
||||
return !snap || !isModelVisible(snap);
|
||||
});
|
||||
if (unknown.length > 0) {
|
||||
throw new Error(
|
||||
`Unknown subagent id(s): ${unknown.join(", ")}. Known: ${known.join(", ") || "none"}.`,
|
||||
);
|
||||
}
|
||||
|
||||
const report = await runTool(getRuntime(), manager.cancel(ids), {
|
||||
signal,
|
||||
interruptMessage: "Subagent cancellation aborted.",
|
||||
});
|
||||
|
||||
const lines = report.map((entry) =>
|
||||
entry.cancelled
|
||||
? `Cancelled ${entry.id} "${entry.title}".`
|
||||
: `${entry.id} "${entry.title}" was already ${entry.status}.`,
|
||||
);
|
||||
|
||||
return {
|
||||
content: [{ type: "text", text: lines.join("\n") }],
|
||||
details: {
|
||||
results: report.map((entry) => ({
|
||||
id: entry.id,
|
||||
title: entry.title,
|
||||
status: entry.status,
|
||||
})),
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
pi.registerTool({
|
||||
name: "subagent_check",
|
||||
label: "Check Subagent",
|
||||
description: SUBAGENT_CHECK_TOOL_DESCRIPTION,
|
||||
parameters: Type.Object({
|
||||
id: Type.String({
|
||||
description: SUBAGENT_CHECK_PARAMETER_DESCRIPTIONS.id,
|
||||
}),
|
||||
}),
|
||||
async execute(_toolCallId, params) {
|
||||
const manager = await getManager();
|
||||
const snap = manager.view.get(params.id);
|
||||
if (!snap || !isModelVisible(snap)) {
|
||||
const known = manager.view
|
||||
.list()
|
||||
.filter(isModelVisible)
|
||||
.map((s) => s.id);
|
||||
throw new Error(
|
||||
`Unknown subagent id "${params.id}". Known: ${known.join(", ") || "none"}.`,
|
||||
);
|
||||
}
|
||||
|
||||
let text = `${describeSubagent(snap)}\nTurns: ${snap.turns}`;
|
||||
if (snap.errorText) text += `\nError: ${snap.errorText}`;
|
||||
|
||||
const output = latestText(snap);
|
||||
if (output) {
|
||||
const preview = truncateHead(output, { maxBytes: 2048, maxLines: 20 });
|
||||
text += `\n\nLatest output:\n${preview.content}`;
|
||||
if (preview.truncated) text += "\n[...]";
|
||||
} else if (snap.status === "running") {
|
||||
text += "\n\n(no text output yet)";
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{ type: "text", text }],
|
||||
details: { id: snap.id, status: snap.status, turns: snap.turns },
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
pi.registerTool({
|
||||
name: "subagent_list",
|
||||
label: "List Subagents",
|
||||
description: SUBAGENT_LIST_TOOL_DESCRIPTION,
|
||||
parameters: Type.Object({}),
|
||||
async execute() {
|
||||
const manager = await getManager();
|
||||
const subs = manager.view.list().filter(isModelVisible);
|
||||
const text =
|
||||
subs.length === 0
|
||||
? "No subagents."
|
||||
: subs.map((snap) => describeSubagent(snap)).join("\n");
|
||||
return {
|
||||
content: [{ type: "text", text }],
|
||||
details: {
|
||||
subagents: subs.map((snap) => ({
|
||||
id: snap.id,
|
||||
title: snap.title,
|
||||
harness: snap.backend,
|
||||
status: snap.status,
|
||||
})),
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// --- Result message rendering ------------------------------------------
|
||||
|
||||
pi.registerMessageRenderer(
|
||||
"subagent-result",
|
||||
(message, { expanded }, theme) => {
|
||||
const details = (message.details ?? {}) as {
|
||||
id?: string;
|
||||
title?: string;
|
||||
status?: string;
|
||||
};
|
||||
const failed = details.status === "error";
|
||||
const icon = failed ? theme.fg("error", "x") : theme.fg("success", "■");
|
||||
const header =
|
||||
`${icon} ` +
|
||||
theme.fg("accent", theme.bold(`subagent ${details.id ?? "?"}`)) +
|
||||
theme.fg(
|
||||
"muted",
|
||||
` · ${details.title ?? ""} · ${failed ? "failed" : "finished"}`,
|
||||
);
|
||||
|
||||
const content =
|
||||
typeof message.content === "string" ? message.content : "";
|
||||
// Remove only the summary line. The following Error line (when present)
|
||||
// is part of the actual result and must remain visible.
|
||||
const body = content.split("\n").slice(1).join("\n").trim();
|
||||
|
||||
if (expanded) {
|
||||
const md = new Markdown(`${body}`, 0, 0, getMarkdownTheme());
|
||||
const container = new Text(header, 0, 0);
|
||||
return {
|
||||
render: (width: number) => [
|
||||
...container.render(width),
|
||||
...md.render(width),
|
||||
],
|
||||
invalidate: () => {
|
||||
container.invalidate();
|
||||
md.invalidate();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const previewLines = body.split("\n").slice(0, 8);
|
||||
let text = header;
|
||||
for (const line of previewLines)
|
||||
text += `\n${theme.fg("toolOutput", line)}`;
|
||||
if (body.split("\n").length > 8)
|
||||
text += `\n${theme.fg("dim", "... (ctrl+o to expand)")}`;
|
||||
return new Text(text, 0, 0);
|
||||
},
|
||||
);
|
||||
|
||||
pi.registerEntryRenderer<BtwResultData>(
|
||||
"btw-result",
|
||||
(entry, { expanded }, theme) => {
|
||||
const data = entry.data;
|
||||
const failed = data?.status === "error";
|
||||
const icon = failed ? theme.fg("error", "x") : theme.fg("success", "■");
|
||||
const header =
|
||||
`${icon} ` +
|
||||
theme.fg("accent", theme.bold(`by the way · ${data?.title ?? "?"}`)) +
|
||||
theme.fg(
|
||||
"muted",
|
||||
` · ${failed ? "failed" : "answered"} · ${data?.id ?? "?"}`,
|
||||
);
|
||||
const body = [
|
||||
data?.errorText ? `Error: ${data.errorText}` : "",
|
||||
data?.answer ?? "(no answer)",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n\n");
|
||||
|
||||
if (expanded) {
|
||||
const md = new Markdown(body, 0, 0, getMarkdownTheme());
|
||||
const container = new Text(header, 0, 0);
|
||||
return {
|
||||
render: (width: number) => [
|
||||
...container.render(width),
|
||||
...md.render(width),
|
||||
],
|
||||
invalidate: () => {
|
||||
container.invalidate();
|
||||
md.invalidate();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const lines = body.split("\n");
|
||||
let text = header;
|
||||
for (const line of lines.slice(0, 8))
|
||||
text += `\n${theme.fg("toolOutput", line)}`;
|
||||
if (lines.length > 8)
|
||||
text += `\n${theme.fg("dim", "... (ctrl+o to expand)")}`;
|
||||
return new Text(text, 0, 0);
|
||||
},
|
||||
);
|
||||
|
||||
// --- Commands -----------------------------------------------------------
|
||||
|
||||
const runByTheWay = async (rawArgs: string, ctx: ExtensionCommandContext) => {
|
||||
if (ctx.mode !== "tui") {
|
||||
if (ctx.hasUI)
|
||||
ctx.ui.notify("by the way is only available in the TUI", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
let prompt = rawArgs.trim();
|
||||
if (!prompt) {
|
||||
const input = await ctx.ui.input("by the way", "Ask a one-off question…");
|
||||
prompt = input?.trim() ?? "";
|
||||
if (!prompt) return;
|
||||
}
|
||||
|
||||
const manager = await getManager();
|
||||
let snap: SubagentSnapshot;
|
||||
try {
|
||||
snap = await runTool(
|
||||
getRuntime(),
|
||||
manager.spawn("pi", {
|
||||
origin: "btw",
|
||||
prompt,
|
||||
title: deriveBtwTitle(prompt),
|
||||
cwd: ctx.cwd,
|
||||
parent: {
|
||||
parentCwd: ctx.cwd,
|
||||
projectTrusted: ctx.isProjectTrusted(),
|
||||
inheritedModel: ctx.model
|
||||
? { provider: ctx.model.provider, id: ctx.model.id }
|
||||
: undefined,
|
||||
inheritedThinkingLevel: pi.getThinkingLevel(),
|
||||
modelRegistry: ctx.modelRegistry,
|
||||
},
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
ctx.ui.notify(
|
||||
error instanceof Error ? error.message : String(error),
|
||||
"error",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await openSubagentTakeover(ctx, manager.view, snap.id, {
|
||||
badge: "by the way",
|
||||
});
|
||||
};
|
||||
|
||||
pi.registerCommand("btw", {
|
||||
description:
|
||||
"Ask a one-off side question while the main agent keeps working",
|
||||
handler: runByTheWay,
|
||||
});
|
||||
|
||||
pi.registerCommand("subagents", {
|
||||
description: "List, inspect, and take over subagents",
|
||||
handler: async (_args, ctx) => {
|
||||
if (ctx.mode !== "tui") {
|
||||
if (ctx.hasUI)
|
||||
ctx.ui.notify(
|
||||
"Subagent takeover is only available in the TUI",
|
||||
"error",
|
||||
);
|
||||
return;
|
||||
}
|
||||
const manager = await getManager();
|
||||
if (manager.view.size() === 0) {
|
||||
ctx.ui.notify(
|
||||
"No subagents yet. The agent spawns them with subagent_spawn.",
|
||||
"info",
|
||||
);
|
||||
return;
|
||||
}
|
||||
await openSubagentPicker(ctx, manager.view);
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user