@signalridge/pi-subagents 1.7.0 → 1.8.0
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/CHANGELOG.md +5 -0
- package/README.md +5 -5
- package/examples/agent-tool-description.md +1 -1
- package/package.json +1 -1
- package/src/agent-manager.ts +41 -8
- package/src/index.ts +47 -30
- package/src/nested-tools.ts +11 -9
- package/src/ui/conversation-viewer.ts +30 -19
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.8.0
|
|
4
|
+
### Minor Changes
|
|
5
|
+
|
|
6
|
+
- 3c6a02e: Improve the subagent conversation viewer with a full Pi-style box border, upstream-aligned layout, and height-safe chat controls. Running agents can chat in the viewer, while `steer_subagent` and `@handle message` queue messages safely for queued runs; `/agents` can stop a queued run before it has a session, and cancelled queued resumes no longer leak messages or transcript subscriptions into later resumes.
|
|
7
|
+
|
|
3
8
|
## 1.7.0
|
|
4
9
|
### Minor Changes
|
|
5
10
|
|
package/README.md
CHANGED
|
@@ -11,10 +11,10 @@ A [pi](https://pi.dev) extension that brings **Claude Code-style autonomous sub-
|
|
|
11
11
|
- **Parallel background agents** — spawn multiple agents that run concurrently with automatic queuing (configurable concurrency limit, default 4) and smart group join (consolidated notifications)
|
|
12
12
|
- **Live FleetView UI** — one navigable list below the editor with a status mark per row, live tool activity, token counts, and text status labels. Enter opens the selected agent's conversation. Toggle via `/agents → Settings → Fleet view`
|
|
13
13
|
- **FleetView** — Claude Code-style navigable list of `main` + every running subagent rendered below the editor (earliest-launched first). Press `↓` (or `←`) at an empty prompt to jump in, `↑`/`↓` to move the selection, `Enter` to open the selected agent's live, auto-updating conversation, `Esc` to return. Finished agents linger briefly before dropping out, and a viewer stays open through completion so you can read the final output. Toggle via `/agents → Settings → Fleet view`
|
|
14
|
-
- **Conversation viewer** — select any agent in `/agents` to open a live-scrolling overlay of its full conversation (auto-follows new content, scroll up to pause).
|
|
14
|
+
- **Conversation viewer** — select any retained agent with a session in `/agents` to open a live-scrolling, four-sided bordered overlay of its full conversation (auto-follows new content, scroll up to pause). Press `Enter` to chat: type a message, then `Enter` to send (`Esc` or an empty submit returns); it appears as a user message and redirects the agent after its current tool. Press `x` (then `x` again to confirm) to stop a running agent — this works for background agents too
|
|
15
15
|
- **Custom agent types** — define agents in `.pi/agents/<name>.md` or `.agents/agents/<name>.md` (project) or globally, with YAML frontmatter: custom system prompts, model selection, thinking levels, tool restrictions
|
|
16
16
|
- **Nested subagents** — opt-in, default-off delegation: a custom agent that sets `allowed_subagents` gets its own ownership-scoped `Agent`, `get_subagent_result`, and `steer_subagent` tools, depth-capped from the main session (default 2). It can control only its own children, they are stopped when it finishes, and their transcripts and token spend roll up to it. The allowlist is a privilege boundary — a child runs with its own tools, so pick it as carefully as `tools:` itself
|
|
17
|
-
- **Mid-run
|
|
17
|
+
- **Mid-run chat** — inject messages into running or queued agents to redirect their work without restarting. From the main prompt, use `@handle message` (with `@` autocomplete); configure this under `/agents → Settings → Agent mentions`.
|
|
18
18
|
- **Session resume** — pick up where an agent left off, preserving full conversation context
|
|
19
19
|
- **Graceful turn limits** — agents get a "wrap up" warning before hard abort, producing clean partial results instead of cut-off output
|
|
20
20
|
- **Case-insensitive agent types** — `"explore"`, `"Explore"`, `"EXPLORE"` all work. Unknown types fall back to general-purpose with a note
|
|
@@ -135,7 +135,7 @@ The mark is a geometric shape rather than an emoji on purpose: an emoji invites
|
|
|
135
135
|
font fallback, and a fallback glyph is usually double-width, which would push
|
|
136
136
|
the rest of the row out of alignment on some terminals and not others.
|
|
137
137
|
|
|
138
|
-
The list is ordered earliest-launched first, and only shows agents you can actually open (pending/queued agents with no session yet appear once they start). At an **empty prompt**, press down or left to move focus from the prompt into the list. Up and down move the selection, Enter opens the selected agent's live conversation overlay (it auto-updates as the agent works), and Esc (or up above `main`) returns to the prompt. Selecting `main` returns to the normal view.
|
|
138
|
+
The list is ordered earliest-launched first, and only shows agents you can actually open (pending/queued agents with no session yet appear once they start). At an **empty prompt**, press down or left to move focus from the prompt into the list. Up and down move the selection, Enter opens the selected agent's live conversation overlay (it auto-updates as the agent works), and Esc (or up above `main`) returns to the prompt. Selecting `main` returns to the normal view. The conversation overlay uses a complete `╭─╮` / `│` / `╰─╯` border. Press Enter to chat with a running agent — type a message and Enter to send it (Esc or an empty submit returns). For a queued agent that has no session yet, type `@handle message` in the main prompt; the message is delivered when it starts. Press `x` twice to stop a running agent. A viewer stays open when its agent finishes so you can read the final output, and finished agents linger in the list for a few seconds before dropping out. Typing anything at a non-empty prompt behaves normally — the list only captures navigation keys when the prompt is empty. Disable it entirely via `/agents → Settings → Fleet view`.
|
|
139
139
|
|
|
140
140
|
Individual agent results use restrained text status labels:
|
|
141
141
|
|
|
@@ -341,7 +341,7 @@ Cancelling a `wait: true` call (for example, with `Esc`) stops only the wait. Th
|
|
|
341
341
|
|
|
342
342
|
### `steer_subagent`
|
|
343
343
|
|
|
344
|
-
Send a
|
|
344
|
+
Send a chat message to a running or queued agent. A running agent receives it after the current tool execution; a queued agent receives it when its session starts.
|
|
345
345
|
|
|
346
346
|
| Parameter | Type | Required | Description |
|
|
347
347
|
|-----------|------|----------|-------------|
|
|
@@ -364,7 +364,7 @@ Create new agent
|
|
|
364
364
|
Settings
|
|
365
365
|
```
|
|
366
366
|
|
|
367
|
-
- **Agent runs** — select any retained run to open its conversation viewer. The summary reports running, queued, completed, wrapped up, stopped, aborted, and failed buckets separately. While a run is still active, press Enter to open the
|
|
367
|
+
- **Agent runs** — select any retained run with a session to open its conversation viewer. Queued runs without a session remain listed but are not openable until they start; selecting one offers a stop confirmation, and `@handle message` queues a chat message. The summary reports running, queued, completed, wrapped up, stopped, aborted, and failed buckets separately. While a run is still active, press Enter to open the chat composer, then Enter again to send a message that redirects the agent (same mechanism as the `steer_subagent` tool; Esc or an empty submit returns), or press `x` (then `x` again to confirm) to stop/abort it — including **background** agents, which a global Esc can't unambiguously target (Esc still stops a blocking foreground `Agent` call). A stopped agent reports its partial output flagged as incomplete, not as a completion.
|
|
368
368
|
- **Agent types** — unified list with textual source labels (`project`, `global`, and `disabled`). Each row shows the agent's model, and the highlighted agent's full description appears below the list. The model column flags `(unavailable, fallback: inherit)` when a configured model can't be resolved (it would silently inherit the parent model), and shows `(resolved: provider/id)` when it resolves to a different provider or version than configured. Select an agent to manage it:
|
|
369
369
|
- **Default agents** (no override): Eject (export as `.md`), Disable
|
|
370
370
|
- **Default agents** (ejected/overridden): Edit, Disable, Reset to default, Delete
|
|
@@ -20,7 +20,7 @@ If the target is already known, use a direct tool — `read` for a known path, `
|
|
|
20
20
|
- Use run_in_background for work you don't need immediately. You will be notified when it completes — do NOT poll or sleep waiting for it. Continue with other work or respond to the user instead.
|
|
21
21
|
- Foreground vs background: use foreground (default) when you need the agent's results before you can proceed. Use background when you have genuinely independent work to do in parallel.
|
|
22
22
|
- Use resume with an agent ID to continue a previous agent's work. A new (non-resume) Agent call starts a fresh agent with no memory of prior runs, so the prompt must be self-contained.
|
|
23
|
-
- Use steer_subagent to send mid-run messages to a running background agent.
|
|
23
|
+
- Use steer_subagent to send mid-run messages to a running or queued background agent.
|
|
24
24
|
- Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, etc.), since it is not aware of the user's intent.
|
|
25
25
|
- If an agent's description says it should be used proactively, try to use it without the user having to ask for it first.
|
|
26
26
|
- Use tier to pick the model profile for this spawn, by name. A tier overrides the agent's own default tier. Model and thinking are not callable parameters — they are what a tier resolves to.
|
package/package.json
CHANGED
package/src/agent-manager.ts
CHANGED
|
@@ -2193,6 +2193,8 @@ export class AgentManager {
|
|
|
2193
2193
|
onToolActivity?: (activity: { type: "start" | "end"; toolName: string }) => void;
|
|
2194
2194
|
onAssistantUsage?: (usage: { input: number; output: number; cacheWrite: number }) => void;
|
|
2195
2195
|
onCompaction?: (info: unknown) => void;
|
|
2196
|
+
/** Called once a queued background resume has acquired its run slot. */
|
|
2197
|
+
onStarted?: () => void;
|
|
2196
2198
|
},
|
|
2197
2199
|
): Promise<AgentRecord | undefined> {
|
|
2198
2200
|
if (this.disposed) return undefined;
|
|
@@ -2311,6 +2313,8 @@ export class AgentManager {
|
|
|
2311
2313
|
onToolActivity?: (activity: { type: "start" | "end"; toolName: string }) => void;
|
|
2312
2314
|
onAssistantUsage?: (usage: { input: number; output: number; cacheWrite: number }) => void;
|
|
2313
2315
|
onCompaction?: (info: unknown) => void;
|
|
2316
|
+
/** Called once a queued background resume has acquired its run slot. */
|
|
2317
|
+
onStarted?: () => void;
|
|
2314
2318
|
},
|
|
2315
2319
|
): Promise<string> {
|
|
2316
2320
|
const snapshot = control.snapshot;
|
|
@@ -2340,6 +2344,28 @@ export class AgentManager {
|
|
|
2340
2344
|
record.result = undefined;
|
|
2341
2345
|
record.error = undefined;
|
|
2342
2346
|
this.syncManagedRecord(record);
|
|
2347
|
+
try {
|
|
2348
|
+
options?.onStarted?.();
|
|
2349
|
+
} catch {
|
|
2350
|
+
// Observability hooks must not prevent the resumed agent from running.
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2353
|
+
// A queued background resume reuses an existing session, so messages
|
|
2354
|
+
// sent while it waits must be flushed here rather than through the
|
|
2355
|
+
// session's idle steering queue. Otherwise cancelling this queued run
|
|
2356
|
+
// would leave those messages behind for a later resume.
|
|
2357
|
+
const pendingSteers = record.pendingSteers;
|
|
2358
|
+
record.pendingSteers = undefined;
|
|
2359
|
+
if (pendingSteers?.length) {
|
|
2360
|
+
for (const message of pendingSteers) {
|
|
2361
|
+
if (control.controller.signal.aborted) break;
|
|
2362
|
+
try {
|
|
2363
|
+
await record.session!.steer(message);
|
|
2364
|
+
} catch {
|
|
2365
|
+
// A malformed or stale queued message must not prevent the resume.
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2343
2369
|
|
|
2344
2370
|
try {
|
|
2345
2371
|
const { text, failure } = await resumeAgent(record.session!, prompt, {
|
|
@@ -2397,21 +2423,25 @@ export class AgentManager {
|
|
|
2397
2423
|
|
|
2398
2424
|
/**
|
|
2399
2425
|
* Send a steering message to an agent from the UI (mirrors the steer_subagent
|
|
2400
|
-
* tool). A
|
|
2401
|
-
* current tool execution and appears as a user message.
|
|
2402
|
-
*
|
|
2403
|
-
* session
|
|
2404
|
-
* (unknown id, or no longer running/queued).
|
|
2426
|
+
* tool). A running session delivers it now — it interrupts the agent after
|
|
2427
|
+
* its current tool execution and appears as a user message. Queued runs keep
|
|
2428
|
+
* it on `pendingSteers` until their run actually starts, including background
|
|
2429
|
+
* resumes that already have an old session attached. Returns false if the
|
|
2430
|
+
* agent can't accept steering (unknown id, or no longer running/queued).
|
|
2405
2431
|
*/
|
|
2406
2432
|
steer(id: string, message: string): boolean {
|
|
2407
2433
|
const record = this.agents.get(id);
|
|
2408
2434
|
if (!record) return false;
|
|
2409
2435
|
if (record.status !== "running" && record.status !== "queued") return false;
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2436
|
+
// A queued background resume already has its old session attached. Keep
|
|
2437
|
+
// messages in the manager until that resume actually starts; putting them
|
|
2438
|
+
// into AgentSession's idle queue would survive cancellation into a future
|
|
2439
|
+
// resume of the same conversation.
|
|
2440
|
+
if (record.status === "queued" || !record.session) {
|
|
2413
2441
|
if (!record.pendingSteers) record.pendingSteers = [];
|
|
2414
2442
|
record.pendingSteers.push(message);
|
|
2443
|
+
} else {
|
|
2444
|
+
record.session.steer(message).catch(() => {});
|
|
2415
2445
|
}
|
|
2416
2446
|
return true;
|
|
2417
2447
|
}
|
|
@@ -2461,6 +2491,9 @@ export class AgentManager {
|
|
|
2461
2491
|
resumeControl?.controller.abort();
|
|
2462
2492
|
resumeControl?.cleanup();
|
|
2463
2493
|
resumeControl?.deferred?.resolve("");
|
|
2494
|
+
// Do not let chat messages queued for a cancelled queued run leak into
|
|
2495
|
+
// the session when the same agent is resumed later.
|
|
2496
|
+
record.pendingSteers = undefined;
|
|
2464
2497
|
this.syncManagedRecord(record);
|
|
2465
2498
|
// Queued agents have no run promise yet. Still use the normal terminal
|
|
2466
2499
|
// callback so lifecycle consumers (including workflow waits) cannot hang.
|
package/src/index.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Tools:
|
|
5
5
|
* Agent — LLM-callable: spawn a sub-agent
|
|
6
6
|
* get_subagent_result — LLM-callable: check background agent status/result
|
|
7
|
-
* steer_subagent — LLM-callable:
|
|
7
|
+
* steer_subagent — LLM-callable: chat with a running or queued agent
|
|
8
8
|
*
|
|
9
9
|
* Commands:
|
|
10
10
|
* /agents — Interactive agent management menu
|
|
@@ -2168,7 +2168,7 @@ Notes:
|
|
|
2168
2168
|
- description: 3-5 words (shown in UI). Prompts must be self-contained — the agent has not seen this conversation.
|
|
2169
2169
|
- Parallel work: one message, multiple Agent calls, run_in_background: true on each. You are notified when background agents finish — never poll or sleep.
|
|
2170
2170
|
- The result is not shown to the user — summarize it for them. Verify an agent's claimed code changes before reporting work done.
|
|
2171
|
-
- resume continues a previous agent by ID; steer_subagent messages a running one.
|
|
2171
|
+
- resume continues a previous agent by ID; steer_subagent messages a running or queued one.
|
|
2172
2172
|
- isolation: "worktree" runs the agent in an isolated git worktree; changes land on a branch.`;
|
|
2173
2173
|
|
|
2174
2174
|
const fullAgentToolDescription = `Launch a new agent to handle complex, multi-step tasks autonomously. Each agent type has specific capabilities and tools available to it.
|
|
@@ -2193,7 +2193,7 @@ If the target is already known, use a direct tool — \`read\` for a known path,
|
|
|
2193
2193
|
- Use run_in_background for work you don't need immediately. You will be notified when it completes — do NOT poll or sleep waiting for it. Continue with other work or respond to the user instead.
|
|
2194
2194
|
- Foreground vs background: use foreground (default) when you need the agent's results before you can proceed. Use background when you have genuinely independent work to do in parallel.
|
|
2195
2195
|
- Use resume with an agent ID to continue a previous agent's work. A new (non-resume) Agent call starts a fresh agent with no memory of prior runs, so the prompt must be self-contained.
|
|
2196
|
-
- Use steer_subagent to send mid-run messages to a running background agent.
|
|
2196
|
+
- Use steer_subagent to send mid-run messages to a running or queued background agent.
|
|
2197
2197
|
- Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, etc.), since it is not aware of the user's intent.
|
|
2198
2198
|
- If an agent's description says it should be used proactively, try to use it without the user having to ask for it first.
|
|
2199
2199
|
- Use tier to pick the model profile for this spawn, by name. A tier overrides the agent's own default tier. Model and thinking are not callable parameters — they are what a tier resolves to.
|
|
@@ -2758,6 +2758,16 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2758
2758
|
// run starts. The resumed prompt lands as an ordinary user message at
|
|
2759
2759
|
// this index, so it is written exactly once.
|
|
2760
2760
|
const transcriptAnchor = existing.session.messages.length ?? 0;
|
|
2761
|
+
const attachTranscript = (): void => {
|
|
2762
|
+
if (!existing.outputFile || existing.outputCleanup) return;
|
|
2763
|
+
existing.outputCleanup = streamToOutputFile(
|
|
2764
|
+
existing.session!,
|
|
2765
|
+
existing.outputFile,
|
|
2766
|
+
params.resume!,
|
|
2767
|
+
ctx.cwd,
|
|
2768
|
+
transcriptAnchor,
|
|
2769
|
+
);
|
|
2770
|
+
};
|
|
2761
2771
|
const record = await manager.resume(
|
|
2762
2772
|
params.resume,
|
|
2763
2773
|
params.prompt,
|
|
@@ -2766,6 +2776,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2766
2776
|
isBackground: true,
|
|
2767
2777
|
onToolActivity: bgCallbacks.onToolActivity,
|
|
2768
2778
|
onAssistantUsage: bgCallbacks.onAssistantUsage,
|
|
2779
|
+
onStarted: attachTranscript,
|
|
2769
2780
|
},
|
|
2770
2781
|
);
|
|
2771
2782
|
if (!record) {
|
|
@@ -2774,17 +2785,6 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2774
2785
|
"Wait for it to settle, or steer it with steer_subagent.",
|
|
2775
2786
|
);
|
|
2776
2787
|
}
|
|
2777
|
-
// Wire streaming once the run actually starts (immediately, or on
|
|
2778
|
-
// queue drain).
|
|
2779
|
-
if (existing.outputFile) {
|
|
2780
|
-
existing.outputCleanup = streamToOutputFile(
|
|
2781
|
-
existing.session,
|
|
2782
|
-
existing.outputFile,
|
|
2783
|
-
params.resume,
|
|
2784
|
-
ctx.cwd,
|
|
2785
|
-
transcriptAnchor,
|
|
2786
|
-
);
|
|
2787
|
-
}
|
|
2788
2788
|
agentActivity.set(params.resume, bgState);
|
|
2789
2789
|
void bgCallbacks;
|
|
2790
2790
|
return textResult(
|
|
@@ -3128,6 +3128,9 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
3128
3128
|
if (record.status === "running") {
|
|
3129
3129
|
output +=
|
|
3130
3130
|
"Agent is still running. Use wait: true or check back later.";
|
|
3131
|
+
} else if (record.status === "queued") {
|
|
3132
|
+
output +=
|
|
3133
|
+
"Agent is queued and has not started yet. Use wait: true or check back later.";
|
|
3131
3134
|
} else if (record.status === "error") {
|
|
3132
3135
|
output += `Error: ${record.error}${partialOutputSuffix(record)}`;
|
|
3133
3136
|
} else {
|
|
@@ -3158,19 +3161,19 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
3158
3161
|
pi.registerTool(
|
|
3159
3162
|
defineTool({
|
|
3160
3163
|
name: SUBAGENT_TOOL_NAMES.STEER,
|
|
3161
|
-
label: "
|
|
3164
|
+
label: "Chat with Agent",
|
|
3162
3165
|
description:
|
|
3163
|
-
"Send a
|
|
3164
|
-
"
|
|
3166
|
+
"Send a chat message to a running or queued agent. A running agent receives it after its current tool execution; " +
|
|
3167
|
+
"a queued agent receives it when its session starts. The message is injected into the agent's conversation.",
|
|
3165
3168
|
promptSnippet:
|
|
3166
|
-
"
|
|
3169
|
+
"Chat with or redirect a running or queued background agent",
|
|
3167
3170
|
parameters: Type.Object({
|
|
3168
3171
|
agent_id: Type.String({
|
|
3169
|
-
description: "The agent ID to
|
|
3172
|
+
description: "The agent ID to message (must be currently running or queued).",
|
|
3170
3173
|
}),
|
|
3171
3174
|
message: Type.String({
|
|
3172
3175
|
description:
|
|
3173
|
-
"The
|
|
3176
|
+
"The chat message to send. This will appear as a user message in the agent's conversation.",
|
|
3174
3177
|
}),
|
|
3175
3178
|
}),
|
|
3176
3179
|
execute: async (_toolCallId, params, _signal, _onUpdate, _ctx) => {
|
|
@@ -3180,13 +3183,16 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
3180
3183
|
`Agent not found: "${params.agent_id}". It may have been cleaned up.`,
|
|
3181
3184
|
);
|
|
3182
3185
|
}
|
|
3183
|
-
if (record.status !== "running") {
|
|
3186
|
+
if (record.status !== "running" && record.status !== "queued") {
|
|
3184
3187
|
return textResult(
|
|
3185
|
-
`Agent "${params.agent_id}" is not running (status: ${record.status}). Cannot
|
|
3188
|
+
`Agent "${params.agent_id}" is not running or queued (status: ${record.status}). Cannot send a chat message.`,
|
|
3186
3189
|
);
|
|
3187
3190
|
}
|
|
3188
|
-
if (!record.session) {
|
|
3189
|
-
//
|
|
3191
|
+
if (record.status === "queued" || !record.session) {
|
|
3192
|
+
// Keep queued messages in the manager record until the run starts.
|
|
3193
|
+
// This also covers a queued background resume whose old session is
|
|
3194
|
+
// already attached; sending directly to that session would survive
|
|
3195
|
+
// cancellation into a later resume.
|
|
3190
3196
|
if (!record.pendingSteers) record.pendingSteers = [];
|
|
3191
3197
|
record.pendingSteers.push(params.message);
|
|
3192
3198
|
if (!record.detached)
|
|
@@ -3194,8 +3200,9 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
3194
3200
|
id: record.id,
|
|
3195
3201
|
message: params.message,
|
|
3196
3202
|
});
|
|
3203
|
+
const delivery = record.status === "queued" ? "when the agent starts" : "once the session initializes";
|
|
3197
3204
|
return textResult(
|
|
3198
|
-
`
|
|
3205
|
+
`Chat message queued for agent ${record.id}. It will be delivered ${delivery}.`,
|
|
3199
3206
|
);
|
|
3200
3207
|
}
|
|
3201
3208
|
|
|
@@ -3220,7 +3227,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
3220
3227
|
`${record.compactionCount} compaction${record.compactionCount === 1 ? "" : "s"}`,
|
|
3221
3228
|
);
|
|
3222
3229
|
return textResult(
|
|
3223
|
-
`
|
|
3230
|
+
`Chat message sent to agent ${record.id}. The agent will process it after its current tool execution.\n` +
|
|
3224
3231
|
`Current state: ${stateParts.join(" · ")}`,
|
|
3225
3232
|
);
|
|
3226
3233
|
} catch (err) {
|
|
@@ -3492,10 +3499,20 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
3492
3499
|
record: AgentRecord,
|
|
3493
3500
|
) {
|
|
3494
3501
|
if (!record.session) {
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3502
|
+
if (record.status === "queued") {
|
|
3503
|
+
const handleHint = record.handle
|
|
3504
|
+
? ` Use @${record.handle} <message> to chat while it waits.`
|
|
3505
|
+
: " Use steer_subagent or wait until it starts to chat.";
|
|
3506
|
+
const stop = await ctx.ui.confirm(
|
|
3507
|
+
"Queued agent",
|
|
3508
|
+
`"${sanitizeDisplayText(record.description)}" has not started yet.${handleHint}\n\nStop this queued run?`,
|
|
3509
|
+
);
|
|
3510
|
+
if (stop && manager.abort(record.id)) {
|
|
3511
|
+
ctx.ui.notify(`Stopped queued agent "${sanitizeDisplayText(record.description)}".`, "info");
|
|
3512
|
+
}
|
|
3513
|
+
return;
|
|
3514
|
+
}
|
|
3515
|
+
ctx.ui.notify("Agent is expired; no session available.", "info");
|
|
3499
3516
|
return;
|
|
3500
3517
|
}
|
|
3501
3518
|
|
package/src/nested-tools.ts
CHANGED
|
@@ -389,30 +389,32 @@ export function createNestedSubagentTools(context: NestedToolContext): ToolDefin
|
|
|
389
389
|
|
|
390
390
|
const steerTool = defineTool({
|
|
391
391
|
name: NESTED_TOOL_NAMES[2],
|
|
392
|
-
label: "
|
|
393
|
-
description: "Send guidance to a running nested agent owned by this parent.",
|
|
392
|
+
label: "Chat with Nested Agent",
|
|
393
|
+
description: "Send guidance to a running or queued nested agent owned by this parent.",
|
|
394
394
|
parameters: Type.Object({
|
|
395
395
|
agent_id: Type.String(),
|
|
396
396
|
message: Type.String(),
|
|
397
397
|
}),
|
|
398
398
|
execute: async (_toolCallId, params) => {
|
|
399
399
|
const record = lookupRecord(context, params.agent_id);
|
|
400
|
-
if (!ownsRecord(record, context.parentAgentId) ||
|
|
401
|
-
|
|
400
|
+
if (!ownsRecord(record, context.parentAgentId) ||
|
|
401
|
+
(record.status !== "running" && record.status !== "queued")) {
|
|
402
|
+
return textResult(`Running or queued nested agent not found or not owned by this parent: "${params.agent_id}".`, true);
|
|
402
403
|
}
|
|
403
|
-
//
|
|
404
|
-
//
|
|
405
|
-
|
|
404
|
+
// Queue chat for any queued run, even when a background resume already
|
|
405
|
+
// has its old session attached. The manager flushes it only when the run
|
|
406
|
+
// actually starts, so cancellation cannot leak it into a later resume.
|
|
407
|
+
if (record.status === "queued" || !record.session) {
|
|
406
408
|
if (!record.pendingSteers) record.pendingSteers = [];
|
|
407
409
|
record.pendingSteers.push(params.message);
|
|
408
|
-
return textResult(`
|
|
410
|
+
return textResult(`Chat message queued for nested agent ${params.agent_id}.`);
|
|
409
411
|
}
|
|
410
412
|
try {
|
|
411
413
|
await record.session.steer(params.message);
|
|
412
414
|
} catch (err) {
|
|
413
415
|
return textResult(`Failed to steer nested agent: ${err instanceof Error ? err.message : String(err)}`, true);
|
|
414
416
|
}
|
|
415
|
-
return textResult(`
|
|
417
|
+
return textResult(`Chat message sent to nested agent ${params.agent_id}.`);
|
|
416
418
|
},
|
|
417
419
|
});
|
|
418
420
|
|
|
@@ -17,9 +17,9 @@ import { PREVIEW_SCAN_LIMIT, safeTerminalText, sanitizeDisplayText, truncateCode
|
|
|
17
17
|
import { getAgentStatusColor, getAgentStatusLabel, getAgentStatusMark } from "./status-label.js";
|
|
18
18
|
import { createViewerKeys, type ViewerKeybindings, type ViewerKeys } from "./viewer-keys.js";
|
|
19
19
|
|
|
20
|
-
/** Base lines consumed by chrome: top border + header +
|
|
20
|
+
/** Base lines consumed by chrome: top border + header + two separators + footer + bottom border. */
|
|
21
21
|
const CHROME_LINES_BASE = 6;
|
|
22
|
-
const MIN_VIEWPORT =
|
|
22
|
+
const MIN_VIEWPORT = 1;
|
|
23
23
|
/** Height ceiling shared by the overlay's `maxHeight` and the viewer's internal viewport cap. */
|
|
24
24
|
export const VIEWPORT_HEIGHT_PCT = 70;
|
|
25
25
|
|
|
@@ -165,8 +165,14 @@ export class ConversationViewer implements Component {
|
|
|
165
165
|
render(width: number): string[] {
|
|
166
166
|
if (width < 6) return [];
|
|
167
167
|
this.refreshRecord();
|
|
168
|
+
// A custom overlay is clipped by pi after rendering. At very low heights
|
|
169
|
+
// there is no room for the complete header/footer chrome, so render nothing
|
|
170
|
+
// rather than showing a misleading half-box with a missing bottom border.
|
|
171
|
+
if (this.maxOverlayRows() < this.chromeLines() + MIN_VIEWPORT) return [];
|
|
168
172
|
const th = this.theme;
|
|
169
|
-
|
|
173
|
+
// Match pi's overlay style: a complete box with two columns reserved for
|
|
174
|
+
// the side borders and one padding column on each side of the content.
|
|
175
|
+
const innerW = width - 4;
|
|
170
176
|
this.lastInnerW = innerW;
|
|
171
177
|
const lines: string[] = [];
|
|
172
178
|
|
|
@@ -176,18 +182,20 @@ export class ConversationViewer implements Component {
|
|
|
176
182
|
};
|
|
177
183
|
const row = (content: string) => {
|
|
178
184
|
const fitted = truncateToWidth(pad(content, innerW), innerW, "...", true);
|
|
179
|
-
return
|
|
185
|
+
return `${th.fg("borderAccent", "│")} ${fitted} ${th.fg("borderAccent", "│")}`;
|
|
180
186
|
};
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
const
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
187
|
+
// Keep the border in the viewer rather than relying on the host overlay's
|
|
188
|
+
// transparent background. This makes the dialog boundary obvious and
|
|
189
|
+
// matches the reference pi-subagents viewer (╭─╮ / │ / ╰─╯).
|
|
190
|
+
const rule = (left: string, right: string) =>
|
|
191
|
+
th.fg("borderAccent", `${left}${"─".repeat(Math.max(0, width - 2))}${right}`);
|
|
192
|
+
const topRule = rule("╭", "╮");
|
|
193
|
+
const bottomRule = rule("╰", "╯");
|
|
194
|
+
// Match the reference viewer's internal separator: it keeps the header,
|
|
195
|
+
// transcript, and controls visually distinct without adding another row.
|
|
196
|
+
const hrMid = row(th.fg("dim", "─".repeat(innerW)));
|
|
197
|
+
|
|
198
|
+
lines.push(topRule);
|
|
191
199
|
const name = renderAgentName(this.record.type, th, { bold: true });
|
|
192
200
|
const modeLabel = getPromptModeLabel(this.record.type);
|
|
193
201
|
const modeTag = modeLabel ? th.fg("dim", `mode ${modeLabel}`) : undefined;
|
|
@@ -238,13 +246,13 @@ export class ConversationViewer implements Component {
|
|
|
238
246
|
const composerLine = renderedComposer.startsWith("> ") ? renderedComposer.slice(2) : renderedComposer;
|
|
239
247
|
lines.push(row(composerLine));
|
|
240
248
|
const composeHint = th.fg("dim", "Enter send · Esc cancel");
|
|
241
|
-
const composeLeft = th.fg("accent", "
|
|
249
|
+
const composeLeft = th.fg("accent", "chat");
|
|
242
250
|
const composeGap = Math.max(1, innerW - visibleWidth(composeLeft) - visibleWidth(composeHint));
|
|
243
251
|
lines.push(row(composeLeft + " ".repeat(composeGap) + composeHint));
|
|
244
252
|
} else {
|
|
245
253
|
const sep = th.fg("dim", " · ");
|
|
246
254
|
const actions: string[] = [];
|
|
247
|
-
if (this.canSteer()) actions.push(th.fg("dim", "Enter
|
|
255
|
+
if (this.canSteer()) actions.push(th.fg("dim", "Enter chat"));
|
|
248
256
|
if (this.isStoppable()) {
|
|
249
257
|
actions.push(this.stopArmed ? th.fg("error", "x again to STOP") : th.fg("dim", "x stop"));
|
|
250
258
|
}
|
|
@@ -262,7 +270,7 @@ export class ConversationViewer implements Component {
|
|
|
262
270
|
const footerGap = Math.max(1, innerW - visibleWidth(footerLeft) - visibleWidth(footerRight));
|
|
263
271
|
lines.push(row(footerLeft + " ".repeat(footerGap) + footerRight));
|
|
264
272
|
}
|
|
265
|
-
lines.push(
|
|
273
|
+
lines.push(bottomRule);
|
|
266
274
|
|
|
267
275
|
return lines;
|
|
268
276
|
}
|
|
@@ -309,11 +317,14 @@ export class ConversationViewer implements Component {
|
|
|
309
317
|
|
|
310
318
|
// ---- Private ----
|
|
311
319
|
|
|
320
|
+
private maxOverlayRows(): number {
|
|
321
|
+
return Math.floor((this.tui.terminal.rows * VIEWPORT_HEIGHT_PCT) / 100);
|
|
322
|
+
}
|
|
323
|
+
|
|
312
324
|
private viewportHeight(): number {
|
|
313
325
|
// Cap mirrors the overlay's maxHeight — otherwise the viewer would render
|
|
314
326
|
// more lines than the overlay shows and clip the footer.
|
|
315
|
-
|
|
316
|
-
return Math.max(MIN_VIEWPORT, maxRows - this.chromeLines());
|
|
327
|
+
return Math.max(MIN_VIEWPORT, this.maxOverlayRows() - this.chromeLines());
|
|
317
328
|
}
|
|
318
329
|
|
|
319
330
|
private chromeLines(): number {
|