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.
- package/README.md +140 -29
- package/index.ts +301 -15
- 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
|
-
|
|
127
|
+
### `/fast-subagent:bg`
|
|
82
128
|
|
|
83
|
-
|
|
129
|
+
Detach a running foreground subagent to background. Each foreground job has a `fg_` prefixed ID shown in the status bar.
|
|
84
130
|
|
|
85
|
-
```
|
|
86
|
-
subagent
|
|
131
|
+
```
|
|
132
|
+
/fast-subagent:bg fg_ab12cd34
|
|
87
133
|
```
|
|
88
134
|
|
|
89
|
-
|
|
135
|
+
Omit ID to list all active foreground jobs:
|
|
90
136
|
|
|
91
|
-
```
|
|
92
|
-
subagent
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
226
|
+
### Background
|
|
130
227
|
|
|
131
|
-
|
|
132
|
-
-
|
|
133
|
-
|
|
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}\
|
|
1049
|
+
return { content: [{ type: "text", text: `Background job started: ${jobId}\nTo check status, ask me to poll job ${jobId}.` }] };
|
|
820
1050
|
}
|
|
821
1051
|
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
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: {
|