pi-fast-subagent 0.4.0 → 0.5.1

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.
Files changed (3) hide show
  1. package/README.md +140 -29
  2. package/index.ts +301 -15
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -9,6 +9,7 @@ Runs subagents with `createAgentSession()` in same process instead of spawning `
9
9
  - Single mode: `{ agent, task }`
10
10
  - Parallel mode: `{ tasks: [...] }`
11
11
  - Background mode: `{ agent, task, background: true }` — fire-and-forget with poll/cancel
12
+ - Slash commands for background job status + cancellation via selector UI
12
13
  - Per-call model override
13
14
  - User + project agent discovery
14
15
  - Project agents override user agents
@@ -60,58 +61,151 @@ model: anthropic/claude-haiku-4-5
60
61
  You are code exploration specialist. Read relevant files, trace data flow, summarize findings clearly.
61
62
  ```
62
63
 
64
+ ## Background Agents
65
+
66
+ Every foreground subagent can be moved to background at any time. Background jobs run concurrently while you continue chatting. When a job finishes, pi automatically posts the result as a follow-up message.
67
+
68
+ ### Status bar
69
+
70
+ While a foreground subagent is running, the pi status bar shows:
71
+ ```
72
+ {agent-name} running · Ctrl+Shift+B to move to background
73
+ ```
74
+
75
+ While background jobs are running:
76
+ ```
77
+ ⧗ N bg agents
78
+ ```
79
+
80
+ ### Moving to background
81
+
82
+ **Keyboard shortcut (while subagent is running):**
83
+ ```
84
+ Ctrl+Shift+B
85
+ ```
86
+
87
+ **Slash command:**
88
+ ```
89
+ /fast-subagent:bg fg_ab12cd34
90
+ ```
91
+
92
+ **Via tool call:**
93
+ ```js
94
+ subagent({ action: "detach", jobId: "fg_ab12cd34" })
95
+ ```
96
+
97
+ ### Auto-completion announcement
98
+
99
+ When a background job finishes, pi injects a follow-up message automatically:
100
+ ```
101
+ Background subagent ✓: sa_ab12cd34 (scout, 4.2s)
102
+ > Explore src and summarize architecture
103
+
104
+ <result output>
105
+ ```
106
+
107
+ Failed jobs are announced the same way with ✗ and the error message.
108
+
63
109
  ## Slash Commands
64
110
 
65
- ### `/agent`
111
+ ### `/fast-subagent:agent`
66
112
 
67
113
  List all available agents:
68
114
 
69
115
  ```
70
- /agent
116
+ /fast-subagent:agent
71
117
  ```
72
118
 
73
119
  Show details for a specific agent (description, file path, model, tools, system prompt):
74
120
 
75
121
  ```
76
- /agent scout
122
+ /fast-subagent:agent scout
77
123
  ```
78
124
 
79
125
  Tab-completion is supported for agent names.
80
126
 
81
- ## Usage
127
+ ### `/fast-subagent:bg`
82
128
 
83
- ### List agents
129
+ Detach a running foreground subagent to background. Each foreground job has a `fg_` prefixed ID shown in the status bar.
84
130
 
85
- ```js
86
- subagent({ action: "list" })
131
+ ```
132
+ /fast-subagent:bg fg_ab12cd34
87
133
  ```
88
134
 
89
- ### Single
135
+ Omit ID to list all active foreground jobs:
90
136
 
91
- ```js
92
- subagent({
93
- agent: "scout",
94
- task: "Explore src and summarize architecture"
95
- })
137
+ ```
138
+ /fast-subagent:bg
139
+ ```
140
+
141
+ ### `/fast-subagent:bg-status`
142
+
143
+ Open selector UI showing all active background jobs. Arrow keys to navigate, Enter to view full details.
144
+
145
+ ```
146
+ /fast-subagent:bg-status
147
+ ```
148
+
149
+ Skip the selector — show details for a specific job directly:
150
+
151
+ ```
152
+ /fast-subagent:bg-status sa_ab12cd34
96
153
  ```
97
154
 
98
- ### General-purpose built-in agent
155
+ ### `/fast-subagent:bg-cancel`
156
+
157
+ Open selector UI to choose a running job to cancel:
158
+
159
+ ```
160
+ /fast-subagent:bg-cancel
161
+ ```
162
+
163
+ Cancel a specific job directly:
164
+
165
+ ```
166
+ /fast-subagent:bg-cancel sa_ab12cd34
167
+ ```
168
+
169
+ ## Keyboard Shortcuts
170
+
171
+ | Shortcut | Action |
172
+ |---|---|
173
+ | `Ctrl+Shift+B` | Move active foreground subagent to background |
174
+
175
+ ## Roadmap
176
+
177
+ Goal: keep this extension **small and focused** — aligned with pi's philosophy of minimal, composable tooling. No feature creep. Every addition must earn its place.
178
+
179
+ - **UI/UX polish** — improve visibility of running subagents: clearer status lines, better progress feedback, agent name + task always visible during execution
180
+ - ~~**Background subagents**~~ ✔ shipped in v0.4.0 — fire-and-forget with `background: true`, poll/cancel/detach support
181
+
182
+ ## Notes
183
+
184
+ - Async/background isolation not supported in-process
185
+ - Git worktree isolation not supported
186
+ - Nested subagent depth limited to 2 by default
187
+
188
+ ## Tool Reference
189
+
190
+ > These are `subagent` tool call examples used by the LLM internally. Not typically invoked directly by users.
191
+
192
+ ### List / discover agents
99
193
 
100
194
  ```js
101
- subagent({
102
- agent: "general",
103
- task: "Summarize open TODOs and propose next step"
104
- })
195
+ // List all agents
196
+ subagent({ action: "list" })
197
+
198
+ // Get details for a specific agent
199
+ subagent({ action: "get", agent: "scout" })
200
+
201
+ // Scope filter: "user" | "project" | "both" (default)
202
+ subagent({ action: "list", agentScope: "project" })
105
203
  ```
106
204
 
107
- ### Override model
205
+ ### Single
108
206
 
109
207
  ```js
110
- subagent({
111
- agent: "scout",
112
- task: "Explore src and summarize architecture",
113
- model: "anthropic/claude-haiku-4-5"
114
- })
208
+ subagent({ agent: "scout", task: "Explore src and summarize architecture" })
115
209
  ```
116
210
 
117
211
  ### Parallel
@@ -122,15 +216,32 @@ subagent({
122
216
  { agent: "scout", task: "Map auth flow" },
123
217
  { agent: "scout", task: "Map navigation" }
124
218
  ],
125
- concurrency: 2
219
+ concurrency: 2 // default: 4
126
220
  })
221
+
222
+ // Repeat one task N times
223
+ subagent({ tasks: [{ agent: "scout", task: "Explore src", count: 3 }] })
127
224
  ```
128
225
 
129
- ## Notes
226
+ ### Background
130
227
 
131
- - Async/background isolation not supported in-process
132
- - Git worktree isolation not supported
133
- - Nested subagent depth limited to 2 by default
228
+ ```js
229
+ // Fire-and-forget returns job ID immediately
230
+ subagent({ agent: "scout", task: "Explore src", background: true })
231
+ // → { jobId: "sa_ab12cd34", status: "running" }
232
+
233
+ subagent({ action: "poll", jobId: "sa_ab12cd34" }) // check progress
234
+ subagent({ action: "cancel", jobId: "sa_ab12cd34" }) // abort
235
+ subagent({ action: "status" }) // list all bg jobs
236
+ subagent({ action: "detach", jobId: "fg_ab12cd34" }) // move fg → bg
237
+ ```
238
+
239
+ ### Options
240
+
241
+ ```js
242
+ subagent({ agent: "scout", task: "...", model: "anthropic/claude-haiku-4-5" })
243
+ subagent({ agent: "scout", task: "...", cwd: "/path/to/project" })
244
+ ```
134
245
 
135
246
  ## Publish
136
247
 
package/index.ts CHANGED
@@ -8,6 +8,7 @@
8
8
  * Agent .md files are compatible with pi-subagents frontmatter format.
9
9
  */
10
10
 
11
+ import { randomUUID } from "node:crypto";
11
12
  import type {
12
13
  AgentToolResult,
13
14
  AgentToolUpdateCallback,
@@ -16,9 +17,9 @@ import type {
16
17
  ToolRenderResultOptions,
17
18
  } from "@mariozechner/pi-coding-agent";
18
19
  import { BackgroundJobManager } from "./background-job-manager.js";
19
- import type { BackgroundHandleLike, BackgroundJobResult } from "./background-types.js";
20
+ import type { BackgroundHandleLike, BackgroundJobResult, BackgroundSubagentJob } from "./background-types.js";
20
21
  import { Theme } from "@mariozechner/pi-coding-agent";
21
- import { truncateToWidth, wrapTextWithAnsi } from "@mariozechner/pi-tui";
22
+ import { Key, truncateToWidth, wrapTextWithAnsi } from "@mariozechner/pi-tui";
22
23
  import { truncateToVisualLines, keyHint } from "@mariozechner/pi-coding-agent";
23
24
  import {
24
25
  AuthStorage,
@@ -97,6 +98,8 @@ function summarizeToolArgs(toolName: unknown, toolInput: unknown): string {
97
98
  let _authStorage: ReturnType<typeof AuthStorage.create> | null = null;
98
99
  let _modelRegistry: ReturnType<typeof ModelRegistry.create> | null = null;
99
100
  let _bgManager: BackgroundJobManager | null = null;
101
+ let _onBgJobComplete: ((job: BackgroundSubagentJob) => void) | null = null;
102
+ let _setBgStatus: ((text: string | undefined) => void) | null = null;
100
103
 
101
104
  function getAuth() {
102
105
  if (!_authStorage) _authStorage = AuthStorage.create();
@@ -105,10 +108,26 @@ function getAuth() {
105
108
  }
106
109
 
107
110
  function getBgManager(): BackgroundJobManager {
108
- if (!_bgManager) _bgManager = new BackgroundJobManager();
111
+ if (!_bgManager) _bgManager = new BackgroundJobManager({
112
+ onJobComplete: (job) => _onBgJobComplete?.(job),
113
+ });
109
114
  return _bgManager;
110
115
  }
111
116
 
117
+ function refreshBgStatus(): void {
118
+ const running = getBgManager().getRunningJobs();
119
+ _setBgStatus?.(running.length > 0 ? `⧗ ${running.length} bg agent${running.length > 1 ? "s" : ""}` : undefined);
120
+ }
121
+
122
+ // ─── Foreground detach registry ───────────────────────────────────────────────
123
+
124
+ interface ForegroundDetachEntry {
125
+ agentName: string;
126
+ task: string;
127
+ detach: () => string; // returns bg job id
128
+ }
129
+ const _fgJobs = new Map<string, ForegroundDetachEntry>();
130
+
112
131
  // ─── In-process runner ───────────────────────────────────────────────────────
113
132
 
114
133
  const MAX_DEPTH = 2;
@@ -150,6 +169,7 @@ interface SubagentDetails {
150
169
  running: boolean;
151
170
  elapsedMs?: number;
152
171
  model?: string;
172
+ backgroundJobId?: string;
153
173
  toolCalls: ToolCallEntry[];
154
174
  }
155
175
 
@@ -162,6 +182,26 @@ function formatDuration(ms: number): string {
162
182
  return m > 0 ? `${m}m ${rem}s` : `${rem}s`;
163
183
  }
164
184
 
185
+ function summarizeTask(task: string, max = 60): string {
186
+ return task.length > max ? task.slice(0, max - 3) + "..." : task;
187
+ }
188
+
189
+ function formatBgJobSummary(job: BackgroundSubagentJob, now = Date.now()): string {
190
+ const dur = job.completedAt ? formatDuration(job.completedAt - job.startedAt) : formatDuration(now - job.startedAt);
191
+ return `${job.id} [${job.status}] ${job.agentName} · ${dur} · ${summarizeTask(job.task)}`;
192
+ }
193
+
194
+ function formatBgJobDetails(job: BackgroundSubagentJob, now = Date.now()): string {
195
+ const dur = job.completedAt ? formatDuration(job.completedAt - job.startedAt) : formatDuration(now - job.startedAt);
196
+ const lines = [`${job.id} [${job.status}] ${job.agentName} · ${dur}`, `Task: ${job.task}`];
197
+ if (job.model) lines.push(`Model: ${job.model}`);
198
+ if (job.status === "completed") lines.push(`\nResult:\n${job.resultSummary ?? "(no output)"}`);
199
+ if (job.status === "failed") lines.push(`\nError: ${job.error ?? "(unknown)"}`);
200
+ if (job.status === "cancelled") lines.push("\nCancelled.");
201
+ if (job.status === "running") lines.push("\nStill running.");
202
+ return lines.join("\n");
203
+ }
204
+
165
205
  // Module-level depth counter — avoids process.env race conditions in parallel mode
166
206
  let _currentDepth = 0;
167
207
 
@@ -461,8 +501,9 @@ const SubagentParams = Type.Object({
461
501
  Type.Literal("status"),
462
502
  Type.Literal("poll"),
463
503
  Type.Literal("cancel"),
504
+ Type.Literal("detach"),
464
505
  ],
465
- { description: "'list'/'get' for agents, 'status' for bg jobs, 'poll'/'cancel' for a specific job" },
506
+ { description: "'list'/'get' for agents, 'status' for bg jobs, 'poll'/'cancel' for a specific job, 'detach' to move a foreground job to background" },
466
507
  ),
467
508
  ),
468
509
  agentScope: Type.Optional(
@@ -476,9 +517,65 @@ const SubagentParams = Type.Object({
476
517
  // ─── Extension entry point ────────────────────────────────────────────────────
477
518
 
478
519
  export default function (pi: ExtensionAPI) {
520
+ // ─── Status keys ────────────────────────────────────────────────────────────────────
521
+ const BG_STATUS_KEY = "fast-subagent-bg";
522
+ const FG_STATUS_KEY = "fast-subagent-fg";
523
+
524
+ // ─── Background job lifecycle ─────────────────────────────────────────────────────
525
+ _onBgJobComplete = (job) => {
526
+ refreshBgStatus();
527
+ const elapsed = job.completedAt ? ((job.completedAt - job.startedAt) / 1000).toFixed(1) : "?";
528
+ const statusEmoji = job.status === "completed" ? "✓" : "✗";
529
+ const taskPreview = job.task.length > 80 ? `${job.task.slice(0, 80)}…` : job.task;
530
+ const output = job.status === "completed"
531
+ ? (job.resultSummary ?? "(no output)")
532
+ : `Error: ${job.error ?? "unknown"}`;
533
+ const modelInfo = job.model ? ` · ${job.model}` : "";
534
+ pi.sendUserMessage(
535
+ [
536
+ `**Background subagent ${statusEmoji}: ${job.id}** (${job.agentName}, ${elapsed}s${modelInfo})`,
537
+ `> ${taskPreview}`,
538
+ ``,
539
+ output,
540
+ ].join("\n"),
541
+ { deliverAs: "followUp" },
542
+ );
543
+ };
544
+
545
+ pi.on("session_start", async (_event, ctx) => {
546
+ _setBgStatus = (text) => ctx.ui.setStatus(BG_STATUS_KEY, text);
547
+ });
548
+
549
+ pi.on("session_shutdown", async () => {
550
+ getBgManager().shutdown();
551
+ _bgManager = null;
552
+ _setBgStatus = null;
553
+ });
554
+
555
+ // ─── Ctrl+Shift+B — move foreground subagent to background ─────────────────────────
556
+ pi.registerShortcut(Key.ctrlShift("b"), {
557
+ description: "Move foreground subagent to background",
558
+ handler: async (ctx) => {
559
+ const entry = [..._fgJobs.values()][0];
560
+ if (!entry) {
561
+ ctx.ui.notify("No foreground subagent running.", "info");
562
+ return;
563
+ }
564
+ try {
565
+ const bgJobId = entry.detach();
566
+ ctx.ui.notify(
567
+ `Moved ${entry.agentName} to background as ${bgJobId}. Completion will be announced automatically.`,
568
+ "info",
569
+ );
570
+ } catch (e) {
571
+ ctx.ui.notify(e instanceof Error ? e.message : String(e), "error");
572
+ }
573
+ },
574
+ });
575
+
479
576
  // ─── /agent slash command ─────────────────────────────────────────────────
480
- pi.registerCommand("agent", {
481
- description: "List available subagents. Usage: /agent [name] — show details for a specific agent.",
577
+ pi.registerCommand("fast-subagent:agent", {
578
+ description: "List available subagents. Usage: /fast-subagent:agent [name] — show details for a specific agent.",
482
579
  getArgumentCompletions(prefix: string) {
483
580
  const agents = discoverAgents(process.cwd());
484
581
  return agents
@@ -537,11 +634,132 @@ export default function (pi: ExtensionAPI) {
537
634
  }
538
635
  }
539
636
  lines.push("");
540
- lines.push("Tip: /agent <name> for details · Add .md files to .pi/agents/ to create new agents");
637
+ lines.push("Tip: /fast-subagent:agent <name> for details · Add .md files to .pi/agents/ to create new agents");
541
638
  ctx.ui.notify(lines.join("\n"), "info");
542
639
  },
543
640
  });
544
641
 
642
+ // ─── /bg slash command ────────────────────────────────────────────────────
643
+ pi.registerCommand("fast-subagent:bg", {
644
+ description: "Move a running foreground subagent to background. Shortcut: Ctrl+Shift+B. Usage: /fast-subagent:bg [fg-job-id] — omit ID to list active foreground jobs.",
645
+ getArgumentCompletions(_prefix: string) {
646
+ return [..._fgJobs.keys()].map((id) => ({ value: id, label: id }));
647
+ },
648
+ async handler(args: string, ctx) {
649
+ const id = args.trim();
650
+ if (!id) {
651
+ if (_fgJobs.size === 0) {
652
+ ctx.ui.notify("No active foreground subagent jobs.", "info");
653
+ return;
654
+ }
655
+ const lines = ["Active foreground jobs (use /fast-subagent:bg <id> to detach):"];
656
+ for (const [fgId, entry] of _fgJobs) {
657
+ lines.push(` ${fgId} ${entry.agentName}: ${summarizeTask(entry.task)}`);
658
+ }
659
+ ctx.ui.notify(lines.join("\n"), "info");
660
+ return;
661
+ }
662
+ const entry = _fgJobs.get(id);
663
+ if (!entry) {
664
+ ctx.ui.notify(`Foreground job "${id}" not found (already done or invalid).`, "warning");
665
+ return;
666
+ }
667
+ const bgJobId = entry.detach();
668
+ ctx.ui.notify(
669
+ `Moved to background: ${bgJobId}\nTo check status, ask me to poll job ${bgJobId}.`,
670
+ "info",
671
+ );
672
+ },
673
+ });
674
+
675
+ // ─── /bg-status slash command ─────────────────────────────────────────────
676
+ pi.registerCommand("fast-subagent:bg-status", {
677
+ description: "Show active background subagents. Usage: /fast-subagent:bg-status [sa-job-id] — omit ID to open selector.",
678
+ getArgumentCompletions(prefix: string) {
679
+ return getBgManager().getAllJobs()
680
+ .filter((job) => job.id.startsWith(prefix))
681
+ .map((job) => ({ value: job.id, label: formatBgJobSummary(job) }));
682
+ },
683
+ async handler(args: string, ctx) {
684
+ const id = args.trim();
685
+ if (id) {
686
+ const job = getBgManager().getJob(id);
687
+ if (!job) {
688
+ ctx.ui.notify(`Background job "${id}" not found.`, "warning");
689
+ return;
690
+ }
691
+ ctx.ui.notify(formatBgJobDetails(job), "info");
692
+ return;
693
+ }
694
+
695
+ const jobs = getBgManager().getRunningJobs().sort((a, b) => b.startedAt - a.startedAt);
696
+ if (jobs.length === 0) {
697
+ ctx.ui.notify("No active background subagent jobs.", "info");
698
+ return;
699
+ }
700
+
701
+ const options = jobs.map((job) => formatBgJobSummary(job));
702
+ const selected = await ctx.ui.select("Active background subagents", options);
703
+ if (!selected) return;
704
+
705
+ const jobId = selected.split(" ")[0] ?? "";
706
+ const job = getBgManager().getJob(jobId);
707
+ if (!job) {
708
+ ctx.ui.notify(`Background job "${jobId}" not found.`, "warning");
709
+ return;
710
+ }
711
+ ctx.ui.notify(formatBgJobDetails(job), "info");
712
+ },
713
+ });
714
+
715
+ // ─── /bg-cancel slash command ─────────────────────────────────────────────
716
+ pi.registerCommand("fast-subagent:bg-cancel", {
717
+ description: "Cancel running background subagent. Usage: /fast-subagent:bg-cancel [sa-job-id] — omit ID to choose with arrow keys.",
718
+ getArgumentCompletions(prefix: string) {
719
+ return getBgManager().getRunningJobs()
720
+ .filter((job) => job.id.startsWith(prefix))
721
+ .map((job) => ({ value: job.id, label: formatBgJobSummary(job) }));
722
+ },
723
+ async handler(args: string, ctx) {
724
+ let jobId = args.trim();
725
+
726
+ if (!jobId) {
727
+ const jobs = getBgManager().getRunningJobs().sort((a, b) => b.startedAt - a.startedAt);
728
+ if (jobs.length === 0) {
729
+ ctx.ui.notify("No running background subagent jobs to cancel.", "info");
730
+ return;
731
+ }
732
+
733
+ const options = jobs.map((job) => formatBgJobSummary(job));
734
+ const selected = await ctx.ui.select("Cancel background subagent", options);
735
+ if (!selected) return;
736
+ jobId = selected.split(" ")[0] ?? "";
737
+ }
738
+
739
+ const job = getBgManager().getJob(jobId);
740
+ if (!job) {
741
+ ctx.ui.notify(`Background job "${jobId}" not found.`, "warning");
742
+ return;
743
+ }
744
+ if (job.status !== "running") {
745
+ ctx.ui.notify(`Background job "${jobId}" already ${job.status}.`, "info");
746
+ return;
747
+ }
748
+
749
+ const confirmed = await ctx.ui.confirm(
750
+ "Cancel background subagent?",
751
+ `${formatBgJobSummary(job)}\n\nTask:\n${job.task}`,
752
+ );
753
+ if (!confirmed) return;
754
+
755
+ const result = getBgManager().cancel(jobId);
756
+ const msg = result === "cancelled" ? `Background job "${jobId}" cancelled.`
757
+ : result === "already_done" ? `Background job "${jobId}" already completed.`
758
+ : `Background job "${jobId}" not found.`;
759
+ ctx.ui.notify(msg, result === "cancelled" ? "info" : "warning");
760
+ },
761
+ });
762
+
545
763
  pi.registerTool({
546
764
  name: "subagent",
547
765
  label: "Subagent",
@@ -633,6 +851,7 @@ export default function (pi: ExtensionAPI) {
633
851
  }
634
852
 
635
853
  function statusLine(): string {
854
+ if (details.backgroundJobId) return `moved to background · ${details.backgroundJobId}`;
636
855
  if (details.running) {
637
856
  const parts: string[] = ["running"];
638
857
  if (details.usage?.turns) parts.push(`${details.usage.turns} turn${details.usage.turns > 1 ? "s" : ""}`);
@@ -720,6 +939,8 @@ export default function (pi: ExtensionAPI) {
720
939
  : "";
721
940
  const statusWithHint = [status, expandHint].filter(Boolean).join(" ");
722
941
  if (statusWithHint) out.push(truncateToWidth(statusWithHint, width, "..."));
942
+ if (details.running && !details.backgroundJobId)
943
+ out.push(truncateToWidth(theme.fg("dim", "Ctrl+Shift+B: move to background"), width, "..."));
723
944
 
724
945
  return out;
725
946
  },
@@ -803,6 +1024,15 @@ export default function (pi: ExtensionAPI) {
803
1024
  return { content: [{ type: "text", text: msg }] };
804
1025
  }
805
1026
 
1027
+ // ── Foreground → background detach ────────────────────────────────────────
1028
+ if (params.action === "detach") {
1029
+ if (!params.jobId) return { content: [{ type: "text", text: "Provide jobId (fg_xxxxx) to detach." }] };
1030
+ const fgEntry = _fgJobs.get(params.jobId);
1031
+ if (!fgEntry) return { content: [{ type: "text", text: `Foreground job "${params.jobId}" not found (already completed or invalid).` }] };
1032
+ const bgJobId = fgEntry.detach();
1033
+ return { content: [{ type: "text", text: `Moved to background: ${bgJobId}\nTo check status, ask me to poll job ${bgJobId}.` }] };
1034
+ }
1035
+
806
1036
  // ── Single mode ───────────────────────────────────────────────────────────
807
1037
  if (params.agent && params.task) {
808
1038
  const { agent, error } = findAgent(params.agent);
@@ -816,18 +1046,74 @@ export default function (pi: ExtensionAPI) {
816
1046
  agent, params.task, cwd, params.model, bgAbort.signal, undefined
817
1047
  ).then((r) => ({ summary: r.output, exitCode: r.exitCode, error: r.error, model: r.model }));
818
1048
  const jobId = getBgManager().adoptHandle(agent.name, params.task, cwd, handle, resultPromise);
819
- return { content: [{ type: "text", text: `Background job started: ${jobId}\nCheck progress: subagent({ action: "poll", jobId: "${jobId}" })` }] };
1049
+ return { content: [{ type: "text", text: `Background job started: ${jobId}\nTo check status, ask me to poll job ${jobId}.` }] };
820
1050
  }
821
1051
 
822
- const result = await runAgent(
823
- agent,
824
- params.task,
825
- cwd,
826
- params.model,
827
- signal,
828
- onUpdate,
1052
+ // Foreground run with detach support
1053
+ const fgId = `fg_${randomUUID().slice(0, 8)}`;
1054
+ const agentAbort = new AbortController();
1055
+ const forwardAbort = () => agentAbort.abort();
1056
+ signal?.addEventListener("abort", forwardAbort, { once: true });
1057
+
1058
+ let detachResolveFn: ((bgJobId: string) => void) | null = null;
1059
+ const detachPromise = new Promise<string>((resolve) => { detachResolveFn = resolve; });
1060
+
1061
+ // Wrap onUpdate so detach can stop forwarding updates to the parent
1062
+ // agent's listener (which becomes invalid once execute() returns).
1063
+ let forwardUpdates = true;
1064
+ const wrappedOnUpdate: OnUpdate | undefined = onUpdate
1065
+ ? (partial) => { if (forwardUpdates) onUpdate(partial); }
1066
+ : undefined;
1067
+
1068
+ const agentRunPromise: Promise<RunResult> = runAgent(
1069
+ agent, params.task, cwd, params.model, agentAbort.signal, wrappedOnUpdate,
829
1070
  );
830
1071
 
1072
+ // Derived promise for the bg manager (used only if we detach)
1073
+ const bgResultPromise: Promise<BackgroundJobResult> = agentRunPromise
1074
+ .then((r) => ({ summary: r.output, exitCode: r.exitCode, error: r.error, model: r.model }));
1075
+
1076
+ _fgJobs.set(fgId, {
1077
+ agentName: agent.name,
1078
+ task: params.task,
1079
+ detach: () => {
1080
+ forwardUpdates = false;
1081
+ signal?.removeEventListener("abort", forwardAbort);
1082
+ const bgHandle: BackgroundHandleLike = { abort: () => agentAbort.abort() };
1083
+ const bgJobId = getBgManager().adoptHandle(agent.name, params.task, cwd, bgHandle, bgResultPromise);
1084
+ refreshBgStatus();
1085
+ detachResolveFn?.(bgJobId);
1086
+ return bgJobId;
1087
+ },
1088
+ });
1089
+
1090
+ ctx.ui.setStatus(FG_STATUS_KEY, `${agent.name} running · Ctrl+Shift+B to move to background`);
1091
+
1092
+ let runResult: RunResult | null = null;
1093
+ const outcome = await Promise.race([
1094
+ agentRunPromise.then((r) => { runResult = r; return "done" as const; }),
1095
+ detachPromise.then(() => "detached" as const),
1096
+ ]).finally(() => {
1097
+ _fgJobs.delete(fgId);
1098
+ signal?.removeEventListener("abort", forwardAbort);
1099
+ ctx.ui.setStatus(FG_STATUS_KEY, undefined);
1100
+ });
1101
+
1102
+ if (outcome === "detached") {
1103
+ const bgJobId = await detachPromise; // already resolved — instant
1104
+ return {
1105
+ content: [{ type: "text", text: `Moved to background: ${bgJobId}. Completion will be announced automatically.` }],
1106
+ details: {
1107
+ task: params.task,
1108
+ usage: { input: 0, output: 0, cost: 0, turns: 0 },
1109
+ running: false,
1110
+ backgroundJobId: bgJobId,
1111
+ toolCalls: [],
1112
+ } satisfies SubagentDetails,
1113
+ };
1114
+ }
1115
+
1116
+ const result = runResult!;
831
1117
  return {
832
1118
  content: [{ type: "text", text: getFinalText(result) }],
833
1119
  details: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-fast-subagent",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "In-process subagent delegation for pi with single, parallel, and background modes",
5
5
  "type": "module",
6
6
  "keywords": [