From 97130e1e35c33f438513e03d3cd59fa94dd2a9f5 Mon Sep 17 00:00:00 2001 From: tintinweb Date: Sun, 22 Mar 2026 14:50:47 +0100 Subject: [PATCH] add biome add stop subagents --- biome.json | 26 ++++++++++++++++++++++ package.json | 7 +++++- src/index.ts | 62 +++++++++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 89 insertions(+), 6 deletions(-) create mode 100644 biome.json diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..d8476c6 --- /dev/null +++ b/biome.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.4.8/schema.json", + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "style": { + "recommended": false + }, + "suspicious": { + "noExplicitAny": "off", + "noControlCharactersInRegex": "off", + "noEmptyInterface": "off" + } + } + }, + "formatter": { + "enabled": false + }, + "files": { + "includes": [ + "src/**/*.ts", + "test/**/*.ts" + ] + } +} diff --git a/package.json b/package.json index b6bd97a..2e3d48e 100644 --- a/package.json +++ b/package.json @@ -27,13 +27,18 @@ "@sinclair/typebox": "latest" }, "scripts": { + "build": "tsc", + "prepublishOnly": "npm run lint && npm run typecheck && npm run test && npm run build", "test": "vitest run", "test:watch": "vitest", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "lint": "biome check src/ test/", + "lint:fix": "biome check --fix src/ test/" }, "devDependencies": { "@types/node": "^20.0.0", "typescript": "^5.0.0", + "@biomejs/biome": "^2.3.5", "vitest": "^4.0.18" }, "pi": { diff --git a/src/index.ts b/src/index.ts index fdbbfd7..d7fab2d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -108,6 +108,19 @@ export default function (pi: ExtensionAPI) { }); } + /** Stop a subagent via pi.events RPC (requires @tintinweb/pi-subagents extension). */ + function stopSubagent(agentId: string): Promise { + const requestId = randomUUID(); + return new Promise((resolve) => { + const timer = setTimeout(() => { unsub(); resolve(false); }, 10000); + const unsub = pi.events.on(`subagents:rpc:stop:reply:${requestId}`, (p: unknown) => { + unsub(); clearTimeout(timer); + resolve((p as any).success ?? false); + }); + pi.events.emit("subagents:rpc:stop", { requestId, agentId }); + }); + } + /** Build a prompt for a task being executed by a subagent. */ function buildTaskPrompt(task: { id: string; subject: string; description: string }, additionalContext?: string): string { let prompt = `You are executing task #${task.id}: "${task.subject}"\n\n${task.description}`; @@ -160,17 +173,22 @@ export default function (pi: ExtensionAPI) { }); // Failure → store error, revert to pending, don't cascade (branch stops) + // Intentional stop (status === "stopped") → mark completed, preserve partial result pi.events.on("subagents:failed", (data) => { - const { id, error, status } = data as { id: string; error?: string; status: string }; + const { id, error, result, status } = data as { id: string; error?: string; result?: string; status: string }; const taskId = agentTaskMap.get(id); if (!taskId) return; agentTaskMap.delete(id); const task = store.get(taskId); if (!task) return; - store.update(task.id, { - status: "pending", - metadata: { ...task.metadata, lastError: error || status }, - }); + + if (status === "stopped") { + // Intentional stop — mark completed, preserve partial result + store.update(task.id, { status: "completed", metadata: { ...task.metadata, result: result || task.metadata?.result } }); + } else { + // Actual error — revert to pending + store.update(task.id, { status: "pending", metadata: { ...task.metadata, lastError: error || status } }); + } widget.setActiveTask(task.id, false); widget.update(); }); @@ -630,6 +648,31 @@ Set up task dependencies: const processOutput = tracker.getOutput(task_id); if (!processOutput) { + // No shell process — check if this is a subagent task + const task = store.get(task_id); + if (!task) throw new Error(`No task found with ID ${task_id}`); + + if (task.metadata?.agentId) { + // Subagent task — wait for completion if blocking + if (block && task.status === "in_progress") { + await new Promise((resolve) => { + const timer = setTimeout(() => { unsubOk(); unsubFail(); resolve(); }, timeout ?? 30000); + const cleanup = () => { clearTimeout(timer); resolve(); }; + const unsubOk = pi.events.on("subagents:completed", (d: unknown) => { + if ((d as any).id === task.metadata?.agentId) { unsubOk(); unsubFail(); cleanup(); } + }); + const unsubFail = pi.events.on("subagents:failed", (d: unknown) => { + if ((d as any).id === task.metadata?.agentId) { unsubOk(); unsubFail(); cleanup(); } + }); + // Re-check in case status changed between the outer check and listener registration + const current = store.get(task_id); + if (current && current.status !== "in_progress") { unsubOk(); unsubFail(); cleanup(); } + signal?.addEventListener("abort", () => { unsubOk(); unsubFail(); cleanup(); }, { once: true }); + }); + } + const updated = store.get(task_id) ?? task; + return textResult(`Task #${task_id} [${updated.status}] — subagent ${task.metadata.agentId}`); + } throw new Error(`No background process for task ${task_id}`); } @@ -671,6 +714,15 @@ Set up task dependencies: const stopped = await tracker.stop(taskId); if (!stopped) { + // No shell process — check if this is a subagent task + const task = store.get(taskId); + if (task?.metadata?.agentId && task.status === "in_progress") { + store.update(taskId, { status: "completed" }); + await stopSubagent(task.metadata.agentId); + widget.setActiveTask(taskId, false); + widget.update(); + return textResult(`Task #${taskId} stopped successfully`); + } throw new Error(`No running background process for task ${taskId}`); }