pi-subagent-in-memory 0.1.6 → 0.2.2
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 +47 -10
- package/extensions/index.ts +429 -97
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,11 +23,25 @@ Spawns an in-process subagent session using pi's `createAgentSession` SDK. The s
|
|
|
23
23
|
| `provider` | string | | LLM provider (e.g. `anthropic`, `google`, `openai`) |
|
|
24
24
|
| `model` | string | | Model ID. Supports `provider/model` format (e.g. `openai/gpt-4o-mini`) |
|
|
25
25
|
| `cwd` | string | | Working directory for the subagent |
|
|
26
|
-
| `timeout` | number | | Timeout in seconds. Aborts the subagent if exceeded |
|
|
26
|
+
| `timeout` | number | | Timeout in seconds. Aborts the subagent if exceeded. Defaults to the configured default timeout (300s, see `/saim-timeout`) |
|
|
27
27
|
| `columnWidthPercent` | number | | Card width as % of terminal (33–100). Controls card grid layout |
|
|
28
28
|
|
|
29
29
|
If `provider` and `model` are omitted, the subagent inherits the main agent's model.
|
|
30
30
|
|
|
31
|
+
### ⏱️ Timeouts That Don't Destroy Child Work
|
|
32
|
+
|
|
33
|
+
Every subagent has a timeout (default **300s**, configurable via `--saim-timeout` / `/saim-timeout`, `0` = unlimited). When a parent agent is aborted or times out while a child subagent is still running, the child is **not killed**. Instead it detaches:
|
|
34
|
+
|
|
35
|
+
- The parent's tool call returns immediately with a note that the child continues in the background
|
|
36
|
+
- The child keeps running, still bounded by its **own** timeout
|
|
37
|
+
- When it finishes, its output is written to `result.md` (or `error.md` on failure) as usual, so the work can be harvested later
|
|
38
|
+
|
|
39
|
+
On any failure (timeout, error), whatever text the subagent had already produced is salvaged to `partial-result.md` alongside `error.md` — nothing is silently discarded.
|
|
40
|
+
|
|
41
|
+
### 🛑 Nesting Depth Limit
|
|
42
|
+
|
|
43
|
+
Subagents may spawn their own subagents, but only up to a configured max depth (default **2**: main agent → subagent → sub-subagent). Subagents at the max depth don't receive the `subagent_create` tool at all, so a runaway fork cascade is structurally impossible. Configure via `--saim-max-depth` / `/saim-max-depth` (1–10).
|
|
44
|
+
|
|
31
45
|
### 📊 Live TUI Card Widgets
|
|
32
46
|
|
|
33
47
|
Each running subagent is displayed as a colored card widget above the editor:
|
|
@@ -40,12 +54,20 @@ Each running subagent is displayed as a colored card widget above the editor:
|
|
|
40
54
|
|
|
41
55
|
### 🔍 Subagent Detail Overlay (`Ctrl+N`)
|
|
42
56
|
|
|
43
|
-
Press **Ctrl+1** through **Ctrl+9** to open a detail popup for the
|
|
57
|
+
Press **Ctrl+1** through **Ctrl+9** to open a detail popup for the **N-th visible** subagent card (1 = leftmost/topmost in the current window):
|
|
44
58
|
|
|
45
59
|
- **Prompt** — Full prompt text with word wrapping (up to 5 lines)
|
|
46
60
|
- **Messages** — Live-updating stream of the subagent's activity (text output, tool calls, status changes), always showing the latest 5 lines
|
|
47
61
|
- Press the same **Ctrl+N** shortcut or **Escape** to close the overlay
|
|
48
62
|
|
|
63
|
+
### 📑 Paging Through Cards (`Ctrl+Alt+←/→`)
|
|
64
|
+
|
|
65
|
+
When more subagents have been spawned than fit in the visible window (see `/saim-set-max-tui-overlays` below):
|
|
66
|
+
|
|
67
|
+
- **Ctrl+Alt+←** scrolls back to older subagent cards
|
|
68
|
+
- **Ctrl+Alt+→** scrolls forward to newer subagent cards
|
|
69
|
+
- A `subagents X–Y of N (Ctrl+Alt+←/→ to page)` hint is displayed above the cards whenever paging is active
|
|
70
|
+
|
|
49
71
|
### 📝 JSONL Session Logging
|
|
50
72
|
|
|
51
73
|
Every subagent session is logged to disk for debugging and auditing:
|
|
@@ -53,11 +75,12 @@ Every subagent session is logged to disk for debugging and auditing:
|
|
|
53
75
|
```
|
|
54
76
|
.pi/subagent-in-memory/<mainSessionId>/
|
|
55
77
|
├── subagent_1/
|
|
56
|
-
│ ├── events.jsonl
|
|
57
|
-
│ └── result.md
|
|
78
|
+
│ ├── events.jsonl # Full event stream (text, tool calls, results)
|
|
79
|
+
│ └── result.md # Final subagent output
|
|
58
80
|
├── subagent_2/
|
|
59
81
|
│ ├── events.jsonl
|
|
60
|
-
│
|
|
82
|
+
│ ├── error.md # On failure: what went wrong
|
|
83
|
+
│ └── partial-result.md # On failure: any text produced before the failure
|
|
61
84
|
└── ...
|
|
62
85
|
```
|
|
63
86
|
|
|
@@ -69,11 +92,25 @@ The JSONL log includes:
|
|
|
69
92
|
|
|
70
93
|
### 🔄 Nested Subagent Support
|
|
71
94
|
|
|
72
|
-
Subagents can spawn their own subagents. All nested cards render in the main agent's widget — they share the same module-level state regardless of nesting depth. This is achieved by passing the `subagent_create` tool directly as an `AgentTool` to child sessions.
|
|
95
|
+
Subagents can spawn their own subagents, up to the configured max depth (see above). All nested cards render in the main agent's widget — they share the same module-level state regardless of nesting depth. This is achieved by passing the `subagent_create` tool directly as an `AgentTool` to child sessions.
|
|
96
|
+
|
|
97
|
+
### 🎛️ Slash Commands
|
|
98
|
+
|
|
99
|
+
| Command | Description |
|
|
100
|
+
|---------|-------------|
|
|
101
|
+
| `/saim-max-depth [n]` | Show or set the max subagent nesting depth (1–10, default 2). Applies to newly created subagents. |
|
|
102
|
+
| `/saim-timeout [seconds]` | Show or set the default subagent timeout in seconds (default 300, `0` = unlimited). A per-call `timeout` parameter overrides it. |
|
|
103
|
+
| `/saim-toggle-overlay [on\|off\|toggle]` | Enable, disable, or toggle the subagent TUI overlay. When disabled, **no card widget is mounted even while subagents are actively running** — they continue executing silently in the background. |
|
|
104
|
+
| `/saim-set-max-tui-overlays <N>` | Set the maximum number of cards displayed at once (1–9, default 3). Older cards remain accessible via **Ctrl+Alt+←/→**. |
|
|
105
|
+
| `/saim-clear-tui-overlay` | Clear all subagent cards from the TUI and close any open detail overlay. |
|
|
73
106
|
|
|
74
|
-
###
|
|
107
|
+
### 🚩 CLI Flags
|
|
75
108
|
|
|
76
|
-
|
|
109
|
+
| Flag | Description |
|
|
110
|
+
|------|-------------|
|
|
111
|
+
| `--saim-max-depth <n>` | Max subagent nesting depth (1–10, default 2). Same as `/saim-max-depth`. |
|
|
112
|
+
| `--saim-timeout <seconds>` | Default subagent timeout in seconds (default 300, `0` = unlimited). Same as `/saim-timeout`. |
|
|
113
|
+
| `--saim-no-tui` | Start with the subagent overlay disabled (equivalent to running `/saim-toggle-overlay off` immediately on startup). Subagents still run normally — only the TUI cards are hidden. |
|
|
77
114
|
|
|
78
115
|
## Install
|
|
79
116
|
|
|
@@ -92,7 +129,7 @@ pi remove npm:pi-subagent-in-memory
|
|
|
92
129
|
After installing, start pi and check:
|
|
93
130
|
|
|
94
131
|
1. The `subagent_create` tool should appear in the tool list
|
|
95
|
-
2. The `/
|
|
132
|
+
2. The `/saim-max-depth`, `/saim-timeout`, `/saim-toggle-overlay`, `/saim-set-max-tui-overlays`, and `/saim-clear-tui-overlay` commands should be available (type `/` to see commands)
|
|
96
133
|
3. Ask the agent to "run a subagent to list files" — you should see a card widget appear
|
|
97
134
|
|
|
98
135
|
## Usage Examples
|
|
@@ -118,7 +155,7 @@ Once installed, the LLM will discover the `subagent_create` tool from its schema
|
|
|
118
155
|
|
|
119
156
|
## How It Works
|
|
120
157
|
|
|
121
|
-
1. **Tool registration** — On load, registers `subagent_create` as a tool and
|
|
158
|
+
1. **Tool registration** — On load, registers `subagent_create` as a tool plus the `/saim-*` commands and `--saim-no-tui` flag. No system prompt modifications.
|
|
122
159
|
2. **Session creation** — When the LLM calls `subagent_create`, a new `createAgentSession` is created in-process with its own model, auth, and coding tools (read, write, edit, bash, grep, find, ls).
|
|
123
160
|
3. **Event streaming** — All subagent events (text deltas, tool calls, completions) are forwarded as `tool_execution_update` events to the parent agent and logged to JSONL.
|
|
124
161
|
4. **Widget rendering** — A TUI widget renders card(s) above the editor, updated on every event.
|
package/extensions/index.ts
CHANGED
|
@@ -13,9 +13,22 @@
|
|
|
13
13
|
* - Live TUI card widgets showing subagent status and output
|
|
14
14
|
* - JSONL event logging to ~/.pi/subagent-in-memory/<sessionId>/
|
|
15
15
|
* - Nested subagent support (subagents can spawn subagents)
|
|
16
|
-
* -
|
|
16
|
+
* - Slash commands to control TUI overlay:
|
|
17
|
+
* /saim-toggle-overlay [on|off] — enable/disable rendering
|
|
18
|
+
* /saim-set-max-tui-overlays <N> — limit visible cards (1-9)
|
|
19
|
+
* /saim-clear-tui-overlay — clear all cards & close any overlay
|
|
20
|
+
* - Runtime limits (each also available as a CLI flag):
|
|
21
|
+
* /saim-max-depth [n] — max subagent nesting depth (default 2)
|
|
22
|
+
* /saim-timeout [seconds] — default subagent timeout (default 300s,
|
|
23
|
+
* 0 = unlimited)
|
|
24
|
+
* - Parent aborts/timeouts do NOT kill running children: the child detaches,
|
|
25
|
+
* keeps running under its own timeout, and still writes result.md /
|
|
26
|
+
* error.md (plus partial-result.md salvaging any text produced so far).
|
|
17
27
|
* - Multi-provider support (Anthropic, OpenAI, Google, etc.)
|
|
18
|
-
* - Ctrl
|
|
28
|
+
* - Ctrl+<N> to inspect subagent prompt & live messages
|
|
29
|
+
* - Ctrl+Alt+Left / Ctrl+Alt+Right to page through cards when there are
|
|
30
|
+
* more subagents than the visible window allows
|
|
31
|
+
* - --saim-no-tui CLI flag to start with the overlay disabled
|
|
19
32
|
*
|
|
20
33
|
* Results are written to ./.pi/subagent-in-memory/<mainSessionId>/subagent_<N>/result.md
|
|
21
34
|
* (or error.md on failure) so the calling agent gets a short pointer instead of
|
|
@@ -24,19 +37,12 @@
|
|
|
24
37
|
|
|
25
38
|
import { Type, type Static } from "@sinclair/typebox";
|
|
26
39
|
import {
|
|
27
|
-
|
|
40
|
+
createAgentSessionFromServices,
|
|
41
|
+
createAgentSessionServices,
|
|
28
42
|
SessionManager,
|
|
29
|
-
AuthStorage,
|
|
30
|
-
DefaultResourceLoader,
|
|
31
|
-
getAgentDir,
|
|
32
|
-
createCodingTools,
|
|
33
|
-
createGrepTool,
|
|
34
|
-
createFindTool,
|
|
35
|
-
createLsTool,
|
|
36
43
|
} from "@mariozechner/pi-coding-agent";
|
|
37
44
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
38
|
-
import type { AgentToolResult, AgentToolUpdateCallback } from "@mariozechner/pi-coding-agent";
|
|
39
|
-
import { resolveModel } from "./model.ts";
|
|
45
|
+
import type { AgentToolResult, AgentToolUpdateCallback, ToolDefinition } from "@mariozechner/pi-coding-agent";
|
|
40
46
|
import { renderCard, type CardTheme } from "./tui-draw.ts";
|
|
41
47
|
|
|
42
48
|
import { visibleWidth, truncateToWidth, wrapTextWithAnsi, matchesKey, Key } from "@mariozechner/pi-tui";
|
|
@@ -142,21 +148,99 @@ let widgetRenderVersion = 0;
|
|
|
142
148
|
|
|
143
149
|
// Track open detail overlay so we can trigger re-renders
|
|
144
150
|
let activeDetailTui: any = null;
|
|
151
|
+
let activeDetailDone: ((result: void) => void) | null = null;
|
|
152
|
+
|
|
153
|
+
// ── Runtime limits (commands & flags) ───────────────────────────
|
|
154
|
+
const DEFAULT_MAX_DEPTH = 2;
|
|
155
|
+
const MAX_DEPTH_HARD_LIMIT = 10;
|
|
156
|
+
const DEFAULT_TIMEOUT_SECS = 300;
|
|
157
|
+
// Max nesting depth: 1 = only the main agent may spawn subagents,
|
|
158
|
+
// 2 = subagents may spawn one further level, etc. Prevents runaway forks.
|
|
159
|
+
let maxSubagentDepth = DEFAULT_MAX_DEPTH;
|
|
160
|
+
// Default per-subagent timeout in seconds, used when a subagent_create call
|
|
161
|
+
// doesn't pass `timeout` explicitly. 0 = unlimited.
|
|
162
|
+
let defaultTimeoutSecs = DEFAULT_TIMEOUT_SECS;
|
|
163
|
+
|
|
164
|
+
/** Thrown into the tool-call promise when the PARENT aborts, so the child can
|
|
165
|
+
* detach and keep running instead of being killed mid-flight. */
|
|
166
|
+
class ParentAbortedError extends Error {
|
|
167
|
+
constructor() {
|
|
168
|
+
super("Subagent was aborted by parent");
|
|
169
|
+
}
|
|
170
|
+
}
|
|
145
171
|
|
|
146
|
-
function
|
|
172
|
+
function parseIntSetting(raw: unknown): number | undefined {
|
|
173
|
+
if (typeof raw !== "string") return undefined;
|
|
174
|
+
const trimmed = raw.trim();
|
|
175
|
+
if (trimmed === "") return undefined;
|
|
176
|
+
const n = Number(trimmed);
|
|
177
|
+
return Number.isInteger(n) ? n : Number.NaN;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ── TUI overlay controls (commands & flag) ──────────────────────
|
|
181
|
+
const DEFAULT_MAX_VISIBLE_OVERLAYS = 3;
|
|
182
|
+
const MAX_OVERLAYS_HARD_LIMIT = 9;
|
|
183
|
+
let overlayEnabled = true;
|
|
184
|
+
let maxVisibleOverlays = DEFAULT_MAX_VISIBLE_OVERLAYS;
|
|
185
|
+
// Number of cards to skip from the END of the list. 0 means "show the latest
|
|
186
|
+
// `maxVisibleOverlays`". Increasing windowOffset pages back into older cards.
|
|
187
|
+
let windowOffset = 0;
|
|
188
|
+
|
|
189
|
+
function getVisibleSubagents(): SubagentCard[] {
|
|
147
190
|
if (subagents.length === 0) return [];
|
|
191
|
+
const total = subagents.length;
|
|
192
|
+
const window = Math.min(maxVisibleOverlays, total);
|
|
193
|
+
// Clamp offset to valid range; user paging may have left it stale.
|
|
194
|
+
const maxOffset = Math.max(0, total - window);
|
|
195
|
+
if (windowOffset > maxOffset) windowOffset = maxOffset;
|
|
196
|
+
if (windowOffset < 0) windowOffset = 0;
|
|
197
|
+
const end = total - windowOffset;
|
|
198
|
+
const start = Math.max(0, end - window);
|
|
199
|
+
return subagents.slice(start, end);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function unmountWidget() {
|
|
203
|
+
if (!currentCtx) return;
|
|
204
|
+
if (widgetMounted) {
|
|
205
|
+
currentCtx.ui.setWidget("in-memory-subagent-cards", undefined);
|
|
206
|
+
}
|
|
207
|
+
widgetMounted = false;
|
|
208
|
+
widgetTui = null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function closeActiveDetail() {
|
|
212
|
+
if (activeDetailDone) {
|
|
213
|
+
try { activeDetailDone(); } catch {}
|
|
214
|
+
}
|
|
215
|
+
activeDetailTui = null;
|
|
216
|
+
activeDetailDone = null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function renderSubagentCards(theme: any, width: number): string[] {
|
|
220
|
+
if (!overlayEnabled) return [];
|
|
221
|
+
const visible = getVisibleSubagents();
|
|
222
|
+
if (visible.length === 0) return [];
|
|
148
223
|
|
|
149
224
|
// Derive cols from columnWidthPercent (all cards share the same value).
|
|
150
|
-
const pct =
|
|
225
|
+
const pct = visible[visible.length - 1].columnWidthPercent;
|
|
151
226
|
const cols = Math.min(3, Math.max(1, Math.round(100 / pct)));
|
|
152
227
|
const gap = 1;
|
|
153
228
|
const colWidth = Math.floor((width - gap * (cols - 1)) / cols);
|
|
154
229
|
const maxContentLines = 4;
|
|
155
230
|
const lines: string[] = [""];
|
|
156
231
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
232
|
+
// Page indicator if there are more cards than fit on screen
|
|
233
|
+
const total = subagents.length;
|
|
234
|
+
if (total > visible.length) {
|
|
235
|
+
const firstVisible = total - windowOffset - visible.length + 1;
|
|
236
|
+
const lastVisible = total - windowOffset;
|
|
237
|
+
const hint = `subagents ${firstVisible}–${lastVisible} of ${total} (Ctrl+Alt+←/→ to page)`;
|
|
238
|
+
lines.push(theme.fg("dim", truncateToWidth(hint, width)));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
for (let i = 0; i < visible.length; i += cols) {
|
|
242
|
+
const rowCards = visible.slice(i, i + cols).map((sa, idx) => {
|
|
243
|
+
const cardTheme = CARD_THEMES[(sa.num - 1) % CARD_THEMES.length];
|
|
160
244
|
|
|
161
245
|
const titleText = `${sa.title} [${sa.modelLabel}]`;
|
|
162
246
|
const innerW = colWidth - 4;
|
|
@@ -166,8 +250,8 @@ function renderSubagentCards(theme: any, width: number): string[] {
|
|
|
166
250
|
const trimmedLines = contentLines.map((l) =>
|
|
167
251
|
visibleWidth(l) > innerW ? truncateToWidth(l, innerW - 1) + "…" : l
|
|
168
252
|
);
|
|
169
|
-
const
|
|
170
|
-
const content =
|
|
253
|
+
const visibleContentLines = trimmedLines.slice(0, maxContentLines);
|
|
254
|
+
const content = visibleContentLines.join("\n") + (contentLines.length > maxContentLines ? "\n…" : "");
|
|
171
255
|
|
|
172
256
|
let statusRaw: string;
|
|
173
257
|
if (sa.status === "created") {
|
|
@@ -286,12 +370,8 @@ function requestSubagentRender() {
|
|
|
286
370
|
syncAnimationTimer();
|
|
287
371
|
|
|
288
372
|
if (!currentCtx) return;
|
|
289
|
-
if (subagents.length === 0) {
|
|
290
|
-
|
|
291
|
-
currentCtx.ui.setWidget("in-memory-subagent-cards", undefined);
|
|
292
|
-
widgetMounted = false;
|
|
293
|
-
widgetTui = null;
|
|
294
|
-
}
|
|
373
|
+
if (!overlayEnabled || subagents.length === 0) {
|
|
374
|
+
unmountWidget();
|
|
295
375
|
try { activeDetailTui?.requestRender(); } catch {}
|
|
296
376
|
return;
|
|
297
377
|
}
|
|
@@ -431,8 +511,9 @@ const SubagentParams = Type.Object({
|
|
|
431
511
|
timeout: Type.Optional(
|
|
432
512
|
Type.Number({
|
|
433
513
|
description:
|
|
434
|
-
"Timeout in seconds for the subagent execution. If exceeded, the subagent is aborted
|
|
435
|
-
"
|
|
514
|
+
"Timeout in seconds for the subagent execution. If exceeded, the subagent is aborted " +
|
|
515
|
+
"(its own in-flight nested subagents keep running and still write their results). " +
|
|
516
|
+
"Defaults to the configured default timeout (300s unless changed via --saim-timeout or /saim-timeout).",
|
|
436
517
|
minimum: 1,
|
|
437
518
|
})
|
|
438
519
|
),
|
|
@@ -458,7 +539,18 @@ async function executeSubagent(
|
|
|
458
539
|
fallbackProvider?: string,
|
|
459
540
|
fallbackModel?: string,
|
|
460
541
|
fallbackCwd?: string,
|
|
542
|
+
depth: number = 1,
|
|
461
543
|
): Promise<AgentToolResult<any>> {
|
|
544
|
+
if (depth > maxSubagentDepth) {
|
|
545
|
+
throw new Error(
|
|
546
|
+
`Subagent nesting depth limit reached (max depth ${maxSubagentDepth}, see /saim-max-depth). ` +
|
|
547
|
+
"Perform the task directly instead of delegating to another subagent.",
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
if (signal?.aborted) {
|
|
551
|
+
throw new Error("Subagent was aborted before it started");
|
|
552
|
+
}
|
|
553
|
+
|
|
462
554
|
subagentCount++;
|
|
463
555
|
const subagentNum = subagents.length + 1; // display number based on currently visible cards
|
|
464
556
|
const outDir = join(".pi", "subagent-in-memory", mainSessionId, `subagent_${subagentCount}`);
|
|
@@ -477,36 +569,49 @@ async function executeSubagent(
|
|
|
477
569
|
throw new Error("Could not determine model. Provide provider and model parameters.");
|
|
478
570
|
}
|
|
479
571
|
|
|
480
|
-
const { model: resolvedModel, apiKey } = await resolveModel(providerName, modelId);
|
|
481
|
-
|
|
482
572
|
const cwd = params.cwd ?? fallbackCwd ?? process.cwd();
|
|
483
573
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
//
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
574
|
+
// Create pi's normal cwd-bound services first. This loads packages from
|
|
575
|
+
// ~/.pi/agent/settings.json and <cwd>/.pi/settings.json, then applies any
|
|
576
|
+
// extension-provided registerProvider() calls to the model registry. Resolving
|
|
577
|
+
// the requested model after this step is what makes package-provided models
|
|
578
|
+
// such as openai-codex/gpt-5.5 visible to subagents.
|
|
579
|
+
const services = await createAgentSessionServices({ cwd });
|
|
580
|
+
const resolvedModel = services.modelRegistry.find(providerName, modelId);
|
|
581
|
+
if (!resolvedModel) {
|
|
582
|
+
const providerModels = services.modelRegistry
|
|
583
|
+
.getAll()
|
|
584
|
+
.filter((model) => model.provider === providerName)
|
|
585
|
+
.map((model) => model.id)
|
|
586
|
+
.sort();
|
|
587
|
+
const diagnostics = services.diagnostics
|
|
588
|
+
.filter((diagnostic) => diagnostic.type === "error")
|
|
589
|
+
.map((diagnostic) => diagnostic.message);
|
|
590
|
+
const details = [
|
|
591
|
+
providerModels.length > 0
|
|
592
|
+
? `Known models for ${providerName}: ${providerModels.join(", ")}`
|
|
593
|
+
: `Provider ${providerName} has no registered models.`,
|
|
594
|
+
diagnostics.length > 0 ? `Service diagnostics: ${diagnostics.join("; ")}` : undefined,
|
|
595
|
+
].filter(Boolean).join(" ");
|
|
596
|
+
throw new Error(`Could not find model ${providerName}/${modelId}. ${details}`.trim());
|
|
597
|
+
}
|
|
501
598
|
|
|
502
|
-
|
|
599
|
+
// Subagents at the max depth don't get the subagent_create tool at all, so
|
|
600
|
+
// the LLM can't even attempt a deeper fork.
|
|
601
|
+
const canNest = depth < maxSubagentDepth;
|
|
602
|
+
const { session } = await createAgentSessionFromServices({
|
|
603
|
+
services,
|
|
604
|
+
sessionManager: SessionManager.inMemory(),
|
|
503
605
|
model: resolvedModel,
|
|
504
|
-
authStorage,
|
|
505
|
-
tools,
|
|
506
606
|
thinkingLevel: "off",
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
607
|
+
// Keep subagent context/tool surface intentionally small: built-in coding
|
|
608
|
+
// tools plus nested subagent support. Package extensions are still loaded
|
|
609
|
+
// above so provider registrations are available, but their tools are not
|
|
610
|
+
// activated unless explicitly listed here.
|
|
611
|
+
tools: canNest
|
|
612
|
+
? ["read", "bash", "edit", "write", "grep", "find", "ls", "subagent_create"]
|
|
613
|
+
: ["read", "bash", "edit", "write", "grep", "find", "ls"],
|
|
614
|
+
customTools: canNest ? [createSubagentAgentTool(providerName, modelId, cwd, depth + 1)] : [],
|
|
510
615
|
});
|
|
511
616
|
|
|
512
617
|
// Set up JSONL event log
|
|
@@ -522,6 +627,7 @@ async function executeSubagent(
|
|
|
522
627
|
model: modelId,
|
|
523
628
|
task: params.task,
|
|
524
629
|
title: params.title,
|
|
630
|
+
depth,
|
|
525
631
|
});
|
|
526
632
|
let lastEventId = session.sessionId;
|
|
527
633
|
|
|
@@ -545,26 +651,21 @@ async function executeSubagent(
|
|
|
545
651
|
details: { sessionId: session.sessionId, status: "created" },
|
|
546
652
|
});
|
|
547
653
|
|
|
548
|
-
// Timeout handling
|
|
654
|
+
// Timeout handling — explicit params.timeout wins, otherwise the configured
|
|
655
|
+
// default applies. 0 means unlimited.
|
|
656
|
+
const timeoutSecs = params.timeout ?? defaultTimeoutSecs;
|
|
549
657
|
let timeoutTimer: ReturnType<typeof setTimeout> | undefined;
|
|
550
658
|
const timeoutController = new AbortController();
|
|
551
|
-
if (
|
|
659
|
+
if (timeoutSecs > 0) {
|
|
552
660
|
timeoutTimer = setTimeout(() => {
|
|
553
661
|
timeoutController.abort();
|
|
554
|
-
},
|
|
662
|
+
}, timeoutSecs * 1000);
|
|
555
663
|
}
|
|
556
664
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
card.status = "error";
|
|
560
|
-
card.endedAt = Date.now();
|
|
561
|
-
appendMessage(card, "[aborted]");
|
|
562
|
-
requestSubagentRender();
|
|
563
|
-
};
|
|
665
|
+
// Hoisted out of the run promise so error paths can salvage partial output.
|
|
666
|
+
let finalText = "";
|
|
564
667
|
|
|
565
|
-
|
|
566
|
-
const result = await new Promise<string>((resolve, reject) => {
|
|
567
|
-
let finalText = "";
|
|
668
|
+
const runPromise = new Promise<string>((resolve, reject) => {
|
|
568
669
|
let textDeltaBuffer = "";
|
|
569
670
|
let toolcallDeltaBuffer = "";
|
|
570
671
|
let lastPartialUpdateAt = 0;
|
|
@@ -714,15 +815,17 @@ async function executeSubagent(
|
|
|
714
815
|
}
|
|
715
816
|
});
|
|
716
817
|
|
|
717
|
-
if (signal) {
|
|
718
|
-
signal.addEventListener("abort", () => {
|
|
719
|
-
combinedAbort();
|
|
720
|
-
reject(new Error("Subagent was aborted"));
|
|
721
|
-
});
|
|
722
|
-
}
|
|
723
818
|
timeoutController.signal.addEventListener("abort", () => {
|
|
724
|
-
|
|
725
|
-
|
|
819
|
+
// Abort only this subagent's own session. Its in-flight nested
|
|
820
|
+
// subagents receive the abort as a PARENT abort and detach instead of
|
|
821
|
+
// dying (see ParentAbortedError handling below), so their work is
|
|
822
|
+
// still written to disk.
|
|
823
|
+
session.abort();
|
|
824
|
+
card.status = "error";
|
|
825
|
+
card.endedAt = Date.now();
|
|
826
|
+
appendMessage(card, `[timed out after ${timeoutSecs}s]`);
|
|
827
|
+
requestSubagentRender();
|
|
828
|
+
reject(new Error(`Subagent timed out after ${timeoutSecs}s`));
|
|
726
829
|
});
|
|
727
830
|
|
|
728
831
|
session.prompt(params.task).catch((err) => {
|
|
@@ -732,30 +835,85 @@ async function executeSubagent(
|
|
|
732
835
|
requestSubagentRender();
|
|
733
836
|
reject(err);
|
|
734
837
|
});
|
|
735
|
-
|
|
838
|
+
});
|
|
736
839
|
|
|
840
|
+
const finalizeSuccess = async (result: string): Promise<string> => {
|
|
737
841
|
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
738
842
|
session.dispose();
|
|
739
843
|
await flushJsonl(jsonlPath);
|
|
740
|
-
|
|
741
844
|
const resultPath = join(outDir, "result.md");
|
|
742
845
|
writeFileSync(resultPath, result, "utf-8");
|
|
846
|
+
return resultPath;
|
|
847
|
+
};
|
|
743
848
|
|
|
744
|
-
|
|
745
|
-
content: [{ type: "text", text: `Execution succeeded. Result is in \`${resultPath}\`` }],
|
|
746
|
-
details: { sessionId: session.sessionId, status: "completed", outputDir: outDir },
|
|
747
|
-
};
|
|
748
|
-
} catch (err: any) {
|
|
849
|
+
const finalizeError = async (err: any): Promise<{ errorPath: string; partialPath?: string }> => {
|
|
749
850
|
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
750
851
|
session.dispose();
|
|
751
852
|
await flushJsonl(jsonlPath);
|
|
752
|
-
|
|
753
853
|
const errorMsg = err?.message ?? String(err);
|
|
754
854
|
const errorPath = join(outDir, "error.md");
|
|
755
|
-
|
|
855
|
+
let body = `# Subagent Error\n\n${errorMsg}\n`;
|
|
856
|
+
let partialPath: string | undefined;
|
|
857
|
+
if (finalText.trim()) {
|
|
858
|
+
partialPath = join(outDir, "partial-result.md");
|
|
859
|
+
writeFileSync(partialPath, finalText, "utf-8");
|
|
860
|
+
body += `\nPartial output produced before the failure was saved to \`partial-result.md\`.\n`;
|
|
861
|
+
}
|
|
862
|
+
writeFileSync(errorPath, body, "utf-8");
|
|
863
|
+
return { errorPath, partialPath };
|
|
864
|
+
};
|
|
865
|
+
|
|
866
|
+
// A parent abort races against the run instead of killing the session, so
|
|
867
|
+
// a timed-out/aborted parent can no longer destroy useful child work.
|
|
868
|
+
let parentAbortPromise: Promise<never> | undefined;
|
|
869
|
+
if (signal) {
|
|
870
|
+
parentAbortPromise = new Promise<never>((_, reject) => {
|
|
871
|
+
signal.addEventListener("abort", () => reject(new ParentAbortedError()), { once: true });
|
|
872
|
+
});
|
|
873
|
+
}
|
|
756
874
|
|
|
875
|
+
try {
|
|
876
|
+
const result = await (parentAbortPromise ? Promise.race([runPromise, parentAbortPromise]) : runPromise);
|
|
877
|
+
const resultPath = await finalizeSuccess(result);
|
|
757
878
|
return {
|
|
758
|
-
content: [{ type: "text", text: `Execution
|
|
879
|
+
content: [{ type: "text", text: `Execution succeeded. Result is in \`${resultPath}\`` }],
|
|
880
|
+
details: { sessionId: session.sessionId, status: "completed", outputDir: outDir },
|
|
881
|
+
};
|
|
882
|
+
} catch (err: any) {
|
|
883
|
+
if (err instanceof ParentAbortedError) {
|
|
884
|
+
// The parent aborted (typically because it hit its own timeout) while
|
|
885
|
+
// this subagent was still working. Don't kill it — detach: let it run
|
|
886
|
+
// to completion, still bounded by its own timeout, and write result.md
|
|
887
|
+
// / error.md as usual so the work can be harvested later.
|
|
888
|
+
appendMessage(card, "[parent aborted — continuing detached]");
|
|
889
|
+
requestSubagentRender();
|
|
890
|
+
runPromise
|
|
891
|
+
.then((result) => finalizeSuccess(result))
|
|
892
|
+
.catch((e) => finalizeError(e))
|
|
893
|
+
.catch(() => {});
|
|
894
|
+
return {
|
|
895
|
+
content: [
|
|
896
|
+
{
|
|
897
|
+
type: "text",
|
|
898
|
+
text:
|
|
899
|
+
"Subagent was aborted by its parent but continues running detached (its own timeout still applies). " +
|
|
900
|
+
`Its result will be written to \`${outDir}\` when it finishes.`,
|
|
901
|
+
},
|
|
902
|
+
],
|
|
903
|
+
details: { sessionId: session.sessionId, status: "detached", outputDir: outDir },
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
const { errorPath, partialPath } = await finalizeError(err);
|
|
908
|
+
return {
|
|
909
|
+
content: [
|
|
910
|
+
{
|
|
911
|
+
type: "text",
|
|
912
|
+
text:
|
|
913
|
+
`Execution failed. Detail is in \`${errorPath}\`` +
|
|
914
|
+
(partialPath ? ` (partial output in \`${partialPath}\`)` : ""),
|
|
915
|
+
},
|
|
916
|
+
],
|
|
759
917
|
details: { sessionId: session.sessionId, status: "error", outputDir: outDir },
|
|
760
918
|
};
|
|
761
919
|
}
|
|
@@ -766,19 +924,22 @@ function createSubagentAgentTool(
|
|
|
766
924
|
parentProvider: string,
|
|
767
925
|
parentModel: string,
|
|
768
926
|
parentCwd: string,
|
|
769
|
-
|
|
927
|
+
childDepth: number,
|
|
928
|
+
): ToolDefinition<typeof SubagentParams> {
|
|
770
929
|
return {
|
|
771
930
|
name: "subagent_create",
|
|
772
931
|
label: "Subagent",
|
|
773
932
|
description:
|
|
774
933
|
"Create a subagent to perform a task. The subagent runs in-process with its own session. " +
|
|
775
|
-
"Progress is streamed back as execution updates. Returns the final result when the subagent finishes."
|
|
934
|
+
"Progress is streamed back as execution updates. Returns the final result when the subagent finishes. " +
|
|
935
|
+
"Nesting is limited to a configured max depth — if the limit is reached, do the work directly instead.",
|
|
776
936
|
parameters: SubagentParams,
|
|
777
937
|
async execute(
|
|
778
938
|
toolCallId: string,
|
|
779
939
|
params: SubagentParamsType,
|
|
780
940
|
signal?: AbortSignal,
|
|
781
941
|
onUpdate?: AgentToolUpdateCallback,
|
|
942
|
+
_ctx?: any,
|
|
782
943
|
) {
|
|
783
944
|
return executeSubagent(
|
|
784
945
|
toolCallId,
|
|
@@ -788,6 +949,7 @@ function createSubagentAgentTool(
|
|
|
788
949
|
parentProvider,
|
|
789
950
|
parentModel,
|
|
790
951
|
parentCwd,
|
|
952
|
+
childDepth,
|
|
791
953
|
);
|
|
792
954
|
},
|
|
793
955
|
};
|
|
@@ -795,46 +957,213 @@ function createSubagentAgentTool(
|
|
|
795
957
|
|
|
796
958
|
// ── Extension entry point ───────────────────────────────────────
|
|
797
959
|
export default function (pi: ExtensionAPI) {
|
|
960
|
+
pi.registerFlag("saim-no-tui", {
|
|
961
|
+
description: "Start with the subagent TUI overlay disabled (equivalent to /saim-toggle-overlay off).",
|
|
962
|
+
type: "boolean",
|
|
963
|
+
default: false,
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
pi.registerFlag("saim-max-depth", {
|
|
967
|
+
description: `Max subagent nesting depth, 1-${MAX_DEPTH_HARD_LIMIT} (default ${DEFAULT_MAX_DEPTH}). Prevents runaway subagent forks.`,
|
|
968
|
+
type: "string",
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
pi.registerFlag("saim-timeout", {
|
|
972
|
+
description: `Default subagent timeout in seconds, 0 = unlimited (default ${DEFAULT_TIMEOUT_SECS}). Per-call \`timeout\` parameter overrides it.`,
|
|
973
|
+
type: "string",
|
|
974
|
+
});
|
|
975
|
+
|
|
798
976
|
pi.on("session_start", async (_event, ctx) => {
|
|
799
977
|
currentCtx = ctx;
|
|
800
978
|
mainSessionId = ctx.sessionManager.getSessionId?.() ?? `session-${Date.now()}`;
|
|
801
979
|
subagentCount = 0;
|
|
802
980
|
subagents.length = 0;
|
|
803
981
|
activeDetailTui = null;
|
|
982
|
+
activeDetailDone = null;
|
|
804
983
|
widgetMounted = false;
|
|
805
984
|
widgetTui = null;
|
|
985
|
+
windowOffset = 0;
|
|
806
986
|
if (flashTimer) { clearInterval(flashTimer); flashTimer = null; }
|
|
807
987
|
ctx.ui.setWidget("in-memory-subagent-cards", undefined);
|
|
988
|
+
|
|
989
|
+
// Apply --saim-no-tui flag if present
|
|
990
|
+
if (pi.getFlag("saim-no-tui") === true) {
|
|
991
|
+
overlayEnabled = false;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// Apply --saim-max-depth / --saim-timeout flags if present
|
|
995
|
+
const depthFlag = parseIntSetting(pi.getFlag("saim-max-depth"));
|
|
996
|
+
if (depthFlag !== undefined) {
|
|
997
|
+
if (Number.isInteger(depthFlag) && depthFlag >= 1 && depthFlag <= MAX_DEPTH_HARD_LIMIT) {
|
|
998
|
+
maxSubagentDepth = depthFlag;
|
|
999
|
+
} else {
|
|
1000
|
+
ctx.ui.notify(
|
|
1001
|
+
`Invalid --saim-max-depth (expected 1-${MAX_DEPTH_HARD_LIMIT}); keeping ${maxSubagentDepth}`,
|
|
1002
|
+
"warning",
|
|
1003
|
+
);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
const timeoutFlag = parseIntSetting(pi.getFlag("saim-timeout"));
|
|
1007
|
+
if (timeoutFlag !== undefined) {
|
|
1008
|
+
if (Number.isInteger(timeoutFlag) && timeoutFlag >= 0) {
|
|
1009
|
+
defaultTimeoutSecs = timeoutFlag;
|
|
1010
|
+
} else {
|
|
1011
|
+
ctx.ui.notify(
|
|
1012
|
+
`Invalid --saim-timeout (expected seconds >= 0); keeping ${defaultTimeoutSecs}`,
|
|
1013
|
+
"warning",
|
|
1014
|
+
);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
|
|
808
1018
|
requestSubagentRender();
|
|
809
1019
|
});
|
|
810
1020
|
|
|
811
|
-
pi.registerCommand("
|
|
812
|
-
description:
|
|
1021
|
+
pi.registerCommand("saim-max-depth", {
|
|
1022
|
+
description: `Show or set max subagent nesting depth (1-${MAX_DEPTH_HARD_LIMIT}, default ${DEFAULT_MAX_DEPTH}). Usage: /saim-max-depth [n]`,
|
|
1023
|
+
handler: async (args, ctx) => {
|
|
1024
|
+
const arg = (args ?? "").trim();
|
|
1025
|
+
if (arg === "") {
|
|
1026
|
+
ctx.ui.notify(`Max subagent nesting depth: ${maxSubagentDepth}`, "info");
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
const n = Number(arg);
|
|
1030
|
+
if (!Number.isInteger(n) || n < 1 || n > MAX_DEPTH_HARD_LIMIT) {
|
|
1031
|
+
ctx.ui.notify(`Provide an integer between 1 and ${MAX_DEPTH_HARD_LIMIT}.`, "warning");
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
maxSubagentDepth = n;
|
|
1035
|
+
ctx.ui.notify(`Max subagent nesting depth set to ${n} (applies to newly created subagents)`, "info");
|
|
1036
|
+
},
|
|
1037
|
+
});
|
|
1038
|
+
|
|
1039
|
+
pi.registerCommand("saim-timeout", {
|
|
1040
|
+
description: `Show or set the default subagent timeout in seconds (0 = unlimited, default ${DEFAULT_TIMEOUT_SECS}). Usage: /saim-timeout [seconds]`,
|
|
1041
|
+
handler: async (args, ctx) => {
|
|
1042
|
+
const arg = (args ?? "").trim();
|
|
1043
|
+
const fmt = (secs: number) => (secs === 0 ? "unlimited" : `${secs}s`);
|
|
1044
|
+
if (arg === "") {
|
|
1045
|
+
ctx.ui.notify(`Default subagent timeout: ${fmt(defaultTimeoutSecs)}`, "info");
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
const n = Number(arg);
|
|
1049
|
+
if (!Number.isInteger(n) || n < 0) {
|
|
1050
|
+
ctx.ui.notify("Provide a non-negative integer number of seconds (0 = unlimited).", "warning");
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
defaultTimeoutSecs = n;
|
|
1054
|
+
ctx.ui.notify(`Default subagent timeout set to ${fmt(n)} (applies to newly created subagents)`, "info");
|
|
1055
|
+
},
|
|
1056
|
+
});
|
|
1057
|
+
|
|
1058
|
+
pi.registerCommand("saim-clear-tui-overlay", {
|
|
1059
|
+
description: "Clear subagent TUI cards and close any open detail overlay",
|
|
813
1060
|
handler: async (_args, ctx) => {
|
|
814
1061
|
subagents.length = 0;
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
1062
|
+
windowOffset = 0;
|
|
1063
|
+
closeActiveDetail();
|
|
1064
|
+
unmountWidget();
|
|
818
1065
|
if (flashTimer) { clearInterval(flashTimer); flashTimer = null; }
|
|
819
|
-
ctx.ui.
|
|
820
|
-
|
|
1066
|
+
ctx.ui.notify("Subagent TUI cards cleared", "info");
|
|
1067
|
+
},
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
pi.registerCommand("saim-toggle-overlay", {
|
|
1071
|
+
description: "Enable or disable subagent detail overlays. Usage: /saim-toggle-overlay [on|off|toggle]",
|
|
1072
|
+
handler: async (args, ctx) => {
|
|
1073
|
+
const arg = (args ?? "").trim().toLowerCase();
|
|
1074
|
+
let next: boolean;
|
|
1075
|
+
if (arg === "on" || arg === "true" || arg === "1") {
|
|
1076
|
+
next = true;
|
|
1077
|
+
} else if (arg === "off" || arg === "false" || arg === "0") {
|
|
1078
|
+
next = false;
|
|
1079
|
+
} else if (arg === "" || arg === "toggle") {
|
|
1080
|
+
next = !overlayEnabled;
|
|
1081
|
+
} else {
|
|
1082
|
+
ctx.ui.notify(`Unknown argument "${arg}". Use on, off, or toggle.`, "warning");
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
overlayEnabled = next;
|
|
1086
|
+
if (!overlayEnabled) {
|
|
1087
|
+
// Force-tear down even while subagents are running.
|
|
1088
|
+
closeActiveDetail();
|
|
1089
|
+
unmountWidget();
|
|
1090
|
+
}
|
|
1091
|
+
requestSubagentRender();
|
|
1092
|
+
ctx.ui.notify(`Subagent TUI overlay ${overlayEnabled ? "enabled" : "disabled"}`, "info");
|
|
1093
|
+
},
|
|
1094
|
+
});
|
|
1095
|
+
|
|
1096
|
+
pi.registerCommand("saim-set-max-tui-overlays", {
|
|
1097
|
+
description: `Set max visible subagent cards (1-${MAX_OVERLAYS_HARD_LIMIT}). Older cards remain accessible via Ctrl+Alt+←/→.`,
|
|
1098
|
+
handler: async (args, ctx) => {
|
|
1099
|
+
const n = parseInt((args ?? "").trim(), 10);
|
|
1100
|
+
if (!Number.isFinite(n) || n < 1 || n > MAX_OVERLAYS_HARD_LIMIT) {
|
|
1101
|
+
ctx.ui.notify(
|
|
1102
|
+
`Provide an integer between 1 and ${MAX_OVERLAYS_HARD_LIMIT}.`,
|
|
1103
|
+
"warning",
|
|
1104
|
+
);
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
maxVisibleOverlays = n;
|
|
1108
|
+
windowOffset = 0;
|
|
1109
|
+
requestSubagentRender();
|
|
1110
|
+
ctx.ui.notify(`Max visible subagent cards set to ${n}`, "info");
|
|
1111
|
+
},
|
|
1112
|
+
});
|
|
1113
|
+
|
|
1114
|
+
// Page through cards when there are more subagents than fit on screen.
|
|
1115
|
+
pi.registerShortcut(Key.ctrlAlt("left"), {
|
|
1116
|
+
description: "Page to older subagent cards",
|
|
1117
|
+
handler: async (ctx) => {
|
|
1118
|
+
if (!overlayEnabled) return;
|
|
1119
|
+
const total = subagents.length;
|
|
1120
|
+
const window = Math.min(maxVisibleOverlays, total);
|
|
1121
|
+
const maxOffset = Math.max(0, total - window);
|
|
1122
|
+
if (windowOffset >= maxOffset) {
|
|
1123
|
+
ctx.ui.notify("Already showing the oldest subagents", "info");
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
windowOffset = Math.min(maxOffset, windowOffset + window);
|
|
1127
|
+
requestSubagentRender();
|
|
821
1128
|
},
|
|
822
1129
|
});
|
|
823
1130
|
|
|
824
|
-
|
|
1131
|
+
pi.registerShortcut(Key.ctrlAlt("right"), {
|
|
1132
|
+
description: "Page to newer subagent cards",
|
|
1133
|
+
handler: async (ctx) => {
|
|
1134
|
+
if (!overlayEnabled) return;
|
|
1135
|
+
if (windowOffset === 0) {
|
|
1136
|
+
ctx.ui.notify("Already showing the latest subagents", "info");
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
const window = Math.min(maxVisibleOverlays, subagents.length);
|
|
1140
|
+
windowOffset = Math.max(0, windowOffset - window);
|
|
1141
|
+
requestSubagentRender();
|
|
1142
|
+
},
|
|
1143
|
+
});
|
|
1144
|
+
|
|
1145
|
+
// Register Ctrl+1 through Ctrl+9 to open subagent detail overlay.
|
|
1146
|
+
// The number maps to the position WITHIN the currently visible window
|
|
1147
|
+
// (1 = first visible card), not the absolute subagent number.
|
|
825
1148
|
for (let n = 1; n <= 9; n++) {
|
|
826
1149
|
pi.registerShortcut(Key.ctrl(`${n}` as any), {
|
|
827
|
-
description: `Inspect subagent #${n}`,
|
|
1150
|
+
description: `Inspect visible subagent #${n}`,
|
|
828
1151
|
handler: async (ctx) => {
|
|
829
|
-
|
|
1152
|
+
if (!overlayEnabled) {
|
|
1153
|
+
ctx.ui.notify("Subagent TUI overlay is disabled", "warning");
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
const visible = getVisibleSubagents();
|
|
1157
|
+
const card = visible[n - 1];
|
|
830
1158
|
if (!card) {
|
|
831
|
-
ctx.ui.notify(`No subagent #${n}`, "warning");
|
|
1159
|
+
ctx.ui.notify(`No visible subagent #${n}`, "warning");
|
|
832
1160
|
return;
|
|
833
1161
|
}
|
|
834
1162
|
|
|
835
1163
|
await ctx.ui.custom<void>(
|
|
836
1164
|
(tui: any, theme: any, _keybindings: any, done: (result: void) => void) => {
|
|
837
1165
|
activeDetailTui = tui;
|
|
1166
|
+
activeDetailDone = done;
|
|
838
1167
|
return new SubagentDetailOverlay(card, n, theme, done);
|
|
839
1168
|
},
|
|
840
1169
|
{
|
|
@@ -849,6 +1178,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
849
1178
|
);
|
|
850
1179
|
|
|
851
1180
|
activeDetailTui = null;
|
|
1181
|
+
activeDetailDone = null;
|
|
852
1182
|
syncAnimationTimer();
|
|
853
1183
|
requestSubagentRender();
|
|
854
1184
|
},
|
|
@@ -860,7 +1190,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
860
1190
|
label: "Subagent",
|
|
861
1191
|
description:
|
|
862
1192
|
"Create a subagent to perform a task. The subagent runs in-process with its own session. " +
|
|
863
|
-
"Progress is streamed back as execution updates. Returns the final result when the subagent finishes."
|
|
1193
|
+
"Progress is streamed back as execution updates. Returns the final result when the subagent finishes. " +
|
|
1194
|
+
"Nesting is limited to a configured max depth — if the limit is reached, do the work directly instead.",
|
|
864
1195
|
parameters: SubagentParams,
|
|
865
1196
|
|
|
866
1197
|
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
@@ -877,6 +1208,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
877
1208
|
providerName,
|
|
878
1209
|
modelId,
|
|
879
1210
|
cwd,
|
|
1211
|
+
1,
|
|
880
1212
|
);
|
|
881
1213
|
},
|
|
882
1214
|
});
|
package/package.json
CHANGED