@esso0428/pi-subagents 0.17.27 → 0.17.29
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 +15 -0
- package/README.md +12 -16
- package/examples/agent-tool-description.md +9 -10
- package/package.json +1 -1
- package/src/agent-manager.ts +6 -26
- package/src/index.ts +140 -270
- package/src/types.ts +2 -1
- package/src/ui/agent-widget.ts +36 -14
- package/src/ui/conversation-blocks.ts +2 -0
- package/src/ui/conversation-role.ts +11 -9
- package/src/ui/conversation-timeline.ts +1 -1
- package/src/ui/conversation-viewer.ts +92 -22
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.17.29] - 2026-10-02
|
|
11
|
+
|
|
12
|
+
> **Breaking: Agent calls are always detached.** `Agent` now returns an ID immediately for every call; retrieve results after completion notifications with `get_subagent_result`. `run_in_background` remains accepted but has no effect.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- **Detached every Agent invocation and removed inline results**: fresh calls bypass the background queue and resumed calls run asynchronously; both return immediately and use completion notifications plus `get_subagent_result` for retrieval. `wait`, `wait_group`, and `wait_group_done` no longer depend on `run_in_background`, so a notification group can be used from any call rather than only a background one.
|
|
16
|
+
- **Made the live conversation viewer show and restore queued steering messages**: pending steers now appear as dim timeline rows until delivery, `Alt+Up` recalls the full pending queue into the composer joined by newlines, and the multiline composer accepts `Ctrl+J` with one rendered row per line.
|
|
17
|
+
|
|
18
|
+
## [0.17.28] - 2026-10-02
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- **Stopped the agents widget from stealing arrow keys from `/settings` and other extensions' menus**: v0.17.24 replaced the widget's focus probe with "no modal of ours is open", which is strictly weaker than the check it removed. pi routes terminal input to extension listeners *before* the focused component, and it swaps the prompt editor out for every dialog while `getEditorText()` still reports the detached, empty editor — so the empty-prompt gate passed and the widget consumed `↑`/`↓`/`enter`/`escape` from pi's own `/settings`, from other extensions' selectors such as `qa-ask`, and from our `/agents` menus. Focus detection is restored to `focused == null || focused instanceof Editor`, kept alongside the `withModal()` tracking added in v0.17.25 so our own dialogs are additionally guaranteed priority.
|
|
22
|
+
|
|
23
|
+
Roster `↑`/`↓` navigation is consequently inert in installations where this extension resolves its own copy of `@earendil-works/pi-tui` at a different version than pi's host, because the two `Editor` classes are then unrelated objects. That is deliberate: `SettingsList` (what `/settings` focuses) is itself a pi-tui class instance, so no structural probe can tell it apart from the editor, and `setEditorComponent()` lets any extension wrap the editor (`pi-tmux-cursor-focus` subclasses it under tmux), so class identity is the only test that can. Inert is the safe failure — it disables this one feature instead of every other menu in the host.
|
|
24
|
+
|
|
10
25
|
## [0.17.27] - 2026-10-02
|
|
11
26
|
|
|
12
27
|
### Fixed
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @esso0428/pi-subagents
|
|
2
2
|
|
|
3
|
-
A [pi](https://pi.dev) extension that brings **Claude Code-style autonomous sub-agents** to pi. Spawn specialized agents that run in isolated sessions — each with its own tools, system prompt, model, and thinking level.
|
|
3
|
+
A [pi](https://pi.dev) extension that brings **Claude Code-style autonomous sub-agents** to pi. Spawn specialized agents that run in isolated sessions — each with its own tools, system prompt, model, and thinking level. Agent calls always detach, steer them mid-run, resume completed sessions, and define your own custom agent types.
|
|
4
4
|
|
|
5
5
|
> **Fork of [`@tintinweb/pi-subagents`](https://github.com/tintinweb/pi-subagents) integrating `npm:pi-subagents`-style JSON agent overrides — configure agents via `settings.json` without writing `.md` files.**
|
|
6
6
|
|
|
@@ -55,15 +55,15 @@ Agent({
|
|
|
55
55
|
subagent_type: "Explore",
|
|
56
56
|
prompt: "Find all files that handle authentication",
|
|
57
57
|
description: "Find auth files",
|
|
58
|
-
run_in_background: true,
|
|
58
|
+
run_in_background: true, // accepted for compatibility; has no effect
|
|
59
59
|
})
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
Agent calls always return an ID immediately and notify you on completion. Results arrive via the completion notification; call `get_subagent_result` with the ID to retrieve the result. `run_in_background` is retained for compatibility but has no effect.
|
|
63
63
|
|
|
64
64
|
### Wait groups
|
|
65
65
|
|
|
66
|
-
Use `wait: true`
|
|
66
|
+
Use `wait: true` to suppress an individual completion notification and receive one grouped notification instead:
|
|
67
67
|
|
|
68
68
|
```
|
|
69
69
|
const group = subagent_wait_group({
|
|
@@ -75,7 +75,6 @@ Agent({
|
|
|
75
75
|
subagent_type: "Explore",
|
|
76
76
|
prompt: "Design a group-handle API",
|
|
77
77
|
description: "Design group handle",
|
|
78
|
-
run_in_background: true,
|
|
79
78
|
wait: true,
|
|
80
79
|
wait_group: group.group_id,
|
|
81
80
|
})
|
|
@@ -84,7 +83,6 @@ Agent({
|
|
|
84
83
|
subagent_type: "Plan",
|
|
85
84
|
prompt: "Design a batch API",
|
|
86
85
|
description: "Design batch API",
|
|
87
|
-
run_in_background: true,
|
|
88
86
|
wait: true,
|
|
89
87
|
wait_group: group.group_id,
|
|
90
88
|
wait_group_done: true,
|
|
@@ -121,13 +119,13 @@ Schedules are **session-scoped**: they reset on `/new` and restore on `/resume`.
|
|
|
121
119
|
|
|
122
120
|
Restrictions:
|
|
123
121
|
- `schedule` cannot be combined with `inherit_context` (no parent conversation exists at fire time) or `resume` (schedules create fresh agents).
|
|
124
|
-
- `run_in_background` is
|
|
122
|
+
- `run_in_background` is accepted for compatibility and has no effect; scheduled agents always run detached.
|
|
125
123
|
- Scheduled fires bypass the `maxConcurrent` queue so a 5-minute interval cannot be deferred behind long-running manual agents.
|
|
126
124
|
- **Headless `pi -p` doesn't wait for scheduled subagents.**
|
|
127
125
|
|
|
128
126
|
## UI
|
|
129
127
|
|
|
130
|
-
The extension renders a persistent Agents widget above the editor. It initializes during TUI `session_start`, after the session branch restores completed-agent history, so openable agents are visible immediately when a session starts or resumes. By default it shows all agents (`widgetMode: all`), including
|
|
128
|
+
The extension renders a persistent Agents widget above the editor. It initializes during TUI `session_start`, after the session branch restores completed-agent history, so openable agents are visible immediately when a session starts or resumes. By default it shows all agents (`widgetMode: all`), including every managed run. Switch to `background` or `off` via `/agents → Settings → Widget`:
|
|
131
129
|
|
|
132
130
|
```
|
|
133
131
|
● Agents
|
|
@@ -171,7 +169,7 @@ Individual agent results render Claude Code-style in the conversation:
|
|
|
171
169
|
|
|
172
170
|
Completed results can be expanded (ctrl+o in pi) to show the full agent output inline. The viewer's tool preview is read-only; its scrollbar and `j`/`k`/wheel/page/top/bottom controls never modify the agent transcript.
|
|
173
171
|
|
|
174
|
-
By default,
|
|
172
|
+
By default, agents stream their full conversation to a per-subagent transcript — a JSON-lines file at `<os-tmpdir>/pi-subagents-<uid>/<cwd>/<session>/tasks/<agent-id>.output` (owner-only `0700`, cleared on reboot). Set `output_transcript: false` on a custom agent to write no transcript path or file for it, or set `outputTranscript: false` in `subagents.json` to make transcripts opt-in for the whole project (frontmatter overrides the project default). This governs **only** the transcript: it is independent of `persist_session` (the pi session on disk), and it does not affect `isolation: worktree` (which commits the agent's work to a git branch) or `memory:` (durable files) — set those accordingly if the goal is to keep a run off disk entirely. Background agent completion notifications render as styled boxes:
|
|
175
173
|
|
|
176
174
|
```
|
|
177
175
|
✓ Find auth files completed
|
|
@@ -257,11 +255,11 @@ All fields are optional — sensible defaults for everything.
|
|
|
257
255
|
| `session_dir` | pi default | Optional session directory when `persist_session: true`; omitted uses pi's normal session location, and relative paths resolve from the agent cwd |
|
|
258
256
|
| `prompt_mode` | `replace` | `replace`: body is the full system prompt (no AGENTS.md / CLAUDE.md inheritance). `append`: body appended to parent's prompt (agent acts as a "parent twin" — inherits parent's AGENTS.md / CLAUDE.md) |
|
|
259
257
|
| `inherit_context` | `false` | Fork parent conversation into agent |
|
|
260
|
-
| `run_in_background` | `false` |
|
|
258
|
+
| `run_in_background` | `false` | Compatibility parameter; has no effect because Agent calls always detach |
|
|
261
259
|
| `isolated` | `false` | Hermetic specialist mode: forces `extensions: false` + `skills: false` + drops `ext:` selectors. Only built-in tools. Distinct from `isolation: worktree` (filesystem) |
|
|
262
260
|
| `enabled` | `true` | Set to `false` to disable an agent (useful for hiding a default agent per-project) |
|
|
263
261
|
|
|
264
|
-
Frontmatter is authoritative. If an agent file sets `model`, `thinking`, `max_turns`, `inherit_context`, `
|
|
262
|
+
Frontmatter is authoritative. If an agent file sets `model`, `thinking`, `max_turns`, `inherit_context`, `isolated`, or `isolation`, those values are locked for that agent. `run_in_background` is accepted for compatibility but has no effect. `Agent` tool parameters only fill fields the agent config leaves unspecified.
|
|
265
263
|
|
|
266
264
|
**Forgiving `model:` resolution.** A `model:` pin is matched against pi's model registry tolerantly, so cosmetic id variations don't silently drop the agent back to the parent's model: `.` and `-` are treated as equivalent in version numbers (`claude-haiku-4.5` ≡ `claude-haiku-4-5`), a trailing `-YYYYMMDD` date stamp is optional (`anthropic/claude-haiku-4-5-20251001` matches an undated registry id and vice-versa), and a `provider/modelId` whose named provider doesn't carry that model retries the bare id against every provider. Precedence is **exact → fuzzy under the named provider → same model under any provider → unavailable**, so an exact match always wins and dated snapshots aren't conflated. If nothing resolves, the pin can't run and the agent inherits the parent model — `/agents → Agent types` flags this case as `(unavailable, fallback: inherit)` and shows the resolved target `(→ provider/id)` when resolution lands on a different provider or version than configured. (This is distinct from [Model Scope](#model-scope) enforcement, which matches the `enabledModels` allowlist by *exact* entry.)
|
|
267
265
|
|
|
@@ -419,7 +417,7 @@ Launch a sub-agent.
|
|
|
419
417
|
| `model` | string | no | Model — `provider/modelId` or fuzzy name (`"haiku"`, `"sonnet"`). Resolved tolerantly (`.`/`-` and a trailing date stamp interchangeable) with provider fallback |
|
|
420
418
|
| `thinking` | string | no | Thinking level: off, minimal, low, medium, high, xhigh, max (availability depends on pi version and model) |
|
|
421
419
|
| `max_turns` | number | no | Max agentic turns. Omit for unlimited (default) |
|
|
422
|
-
| `run_in_background` | boolean | no |
|
|
420
|
+
| `run_in_background` | boolean | no | Compatibility parameter; has no effect because Agent calls never block |
|
|
423
421
|
| `resume` | string | no | Agent ID to resume a previous session |
|
|
424
422
|
| `isolated` | boolean | no | No extension/MCP tools |
|
|
425
423
|
| `isolation` | `"worktree"` | no | Run in an isolated git worktree |
|
|
@@ -461,7 +459,7 @@ Create new agent ← manual wizard or AI-generated
|
|
|
461
459
|
Settings ← max concurrency, max turns, grace turns, join mode
|
|
462
460
|
```
|
|
463
461
|
|
|
464
|
-
- **Running agents** — select one to open its live conversation viewer. While it's still running, press `Enter` to open the steering 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
|
|
462
|
+
- **Running agents** — select one to open its live conversation viewer. While it's still running, press `Enter` to open the steering 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. A stopped agent reports its partial output flagged as incomplete, not as a completion.
|
|
465
463
|
- **Agent types** — unified list with source indicators: `•` (project), `◦` (global), `✕` (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 `(→ provider/id)` when it resolves to a different provider or version than configured. Select an agent to manage it:
|
|
466
464
|
- **Default agents** (no override): Eject (export as `.md`), Disable
|
|
467
465
|
- **Default agents** (ejected/overridden): Edit, Disable, Reset to default, Delete
|
|
@@ -489,9 +487,7 @@ Instead of hard-aborting at the turn limit, agents get a graceful shutdown:
|
|
|
489
487
|
|
|
490
488
|
## Concurrency
|
|
491
489
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
Foreground agents bypass the queue — they block the parent anyway.
|
|
490
|
+
Agent tool calls bypass the configurable background concurrency queue (default: 4), preserving immediate detached execution. Other programmatic background spawns may still be queued; the widget shows queued agents as a collapsed count.
|
|
495
491
|
|
|
496
492
|
## Join Strategies
|
|
497
493
|
|
|
@@ -14,23 +14,22 @@ If the target is already known, use a direct tool — `read` for a known path, `
|
|
|
14
14
|
## Usage notes
|
|
15
15
|
|
|
16
16
|
- Always include a short (3-5 word) description summarizing what the agent will do (shown in UI).
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
- 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.
|
|
24
|
-
- Use steer_subagent to send mid-run messages to a running background agent.
|
|
17
|
+
- Every Agent call is detached and returns an agent ID immediately. run_in_background is accepted for compatibility but has no effect. When you launch multiple agents for independent work, send them in a single message with multiple tool uses so they run concurrently.
|
|
18
|
+
- Results arrive via the completion notification; call get_subagent_result once per completed task ID with wait omitted or false to render the native expandable result. Agent results are not returned inline — never poll or sleep waiting for them.
|
|
19
|
+
- For nonblocking grouped notification, set wait: true. Omit wait_group for a one-agent implicit group, or create an explicit group with subagent_wait_group and seal it (or set wait_group_done: true on the final Agent call).
|
|
20
|
+
- Trust but verify: an agent's summary describes what it intended to do, not necessarily what it did. When an agent writes or edits code, check the actual changes before reporting work as done.
|
|
21
|
+
- Use resume with an agent ID to continue a previous agent's work; resume is detached and also returns immediately.
|
|
22
|
+
- Use steer_subagent to send mid-run messages to a running agent.
|
|
25
23
|
- 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.
|
|
26
24
|
- If an agent's description says it should be used proactively, try to use it without the user having to ask for it first.
|
|
27
25
|
- Use model to specify a different model (as "provider/modelId", or fuzzy e.g. "haiku", "sonnet").
|
|
28
26
|
- Use thinking to control extended thinking level.
|
|
29
|
-
- Use inherit_context if the agent needs the parent conversation history.
|
|
27
|
+
- Use inherit_context if the agent needs the parent conversation history.
|
|
28
|
+
- Use isolation: "worktree" to run the agent in an isolated git worktree (safe parallel file modifications). The worktree is automatically cleaned up if the agent makes no changes; otherwise the path and branch are returned in the result.{{scheduleGuideline}}
|
|
30
29
|
|
|
31
30
|
## Writing the prompt
|
|
32
31
|
|
|
33
|
-
|
|
32
|
+
Provide clear, detailed prompts so the agent can work autonomously. Brief it like a smart colleague who just walked into the room — it hasn't seen this conversation, doesn't know what you've tried, doesn't understand why this task matters.
|
|
34
33
|
- Explain what you're trying to accomplish and why.
|
|
35
34
|
- Describe what you've already learned or ruled out.
|
|
36
35
|
- Give enough context about the surrounding problem that the agent can make judgment calls rather than just following a narrow instruction.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@esso0428/pi-subagents",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.29",
|
|
4
4
|
"description": "A pi extension that brings smart Claude Code-style autonomous sub-agents to pi, with npm:pi-subagents-style JSON agent overrides.",
|
|
5
5
|
"author": "ESSO0428",
|
|
6
6
|
"repository": {
|
package/src/agent-manager.ts
CHANGED
|
@@ -638,32 +638,6 @@ export class AgentManager {
|
|
|
638
638
|
}
|
|
639
639
|
}
|
|
640
640
|
|
|
641
|
-
/**
|
|
642
|
-
* Spawn an agent and wait for completion (foreground use).
|
|
643
|
-
* Foreground agents bypass the concurrency queue.
|
|
644
|
-
* Returns { id, record } so callers can access the agent ID.
|
|
645
|
-
*
|
|
646
|
-
* @param onSpawned - Called synchronously after spawn(), before onSessionCreated fires.
|
|
647
|
-
* Use this to set record.outputFile so streamToOutputFile can pick it up.
|
|
648
|
-
*/
|
|
649
|
-
async spawnAndWait(
|
|
650
|
-
pi: ExtensionAPI,
|
|
651
|
-
ctx: ExtensionContext,
|
|
652
|
-
type: SubagentType,
|
|
653
|
-
prompt: string,
|
|
654
|
-
options: Omit<SpawnOptions, "isBackground">,
|
|
655
|
-
onSpawned?: (id: string) => void,
|
|
656
|
-
): Promise<{ id: string; record: AgentRecord }> {
|
|
657
|
-
const id = this.spawn(pi, ctx, type, prompt, {
|
|
658
|
-
...options,
|
|
659
|
-
isBackground: false,
|
|
660
|
-
onSpawned,
|
|
661
|
-
});
|
|
662
|
-
const record = this.agents.get(id)!;
|
|
663
|
-
await record.promise;
|
|
664
|
-
return { id, record };
|
|
665
|
-
}
|
|
666
|
-
|
|
667
641
|
/**
|
|
668
642
|
* Resume an existing agent session with a new prompt.
|
|
669
643
|
*/
|
|
@@ -680,11 +654,15 @@ export class AgentManager {
|
|
|
680
654
|
record.completedAt = undefined;
|
|
681
655
|
record.result = undefined;
|
|
682
656
|
record.error = undefined;
|
|
657
|
+
record.resultConsumed = false;
|
|
658
|
+
record.isBackground = true;
|
|
659
|
+
this.onStart?.(record);
|
|
683
660
|
const resumedModel = record.session.model;
|
|
684
661
|
record.invocation = {
|
|
685
662
|
...(record.invocation ?? {}),
|
|
686
663
|
...(resumedModel && { effectiveModelName: resumedModel.name ?? resumedModel.id }),
|
|
687
664
|
effectiveThinking: record.session.thinkingLevel,
|
|
665
|
+
runInBackground: true,
|
|
688
666
|
};
|
|
689
667
|
this.checkpoint(record);
|
|
690
668
|
|
|
@@ -709,11 +687,13 @@ export class AgentManager {
|
|
|
709
687
|
record.result = text;
|
|
710
688
|
record.completedAt = Date.now();
|
|
711
689
|
this.checkpoint(record);
|
|
690
|
+
try { this.onComplete?.(record); } catch { /* ignore completion side-effect errors */ }
|
|
712
691
|
} catch (err) {
|
|
713
692
|
record.status = "error";
|
|
714
693
|
record.error = err instanceof Error ? err.message : String(err);
|
|
715
694
|
record.completedAt = Date.now();
|
|
716
695
|
this.checkpoint(record);
|
|
696
|
+
try { this.onComplete?.(record); } catch { /* ignore completion side-effect errors */ }
|
|
717
697
|
}
|
|
718
698
|
|
|
719
699
|
return record;
|
package/src/index.ts
CHANGED
|
@@ -38,7 +38,6 @@ import {
|
|
|
38
38
|
type AgentDetails,
|
|
39
39
|
AgentWidget,
|
|
40
40
|
buildInvocationTags,
|
|
41
|
-
describeActivity,
|
|
42
41
|
fgPreservingNestedStyles,
|
|
43
42
|
formatDuration,
|
|
44
43
|
formatMs,
|
|
@@ -81,7 +80,7 @@ function formatLifetimeTokens(o: { lifetimeUsage: LifetimeUsage }): string {
|
|
|
81
80
|
|
|
82
81
|
/**
|
|
83
82
|
* Create an AgentActivity state and spawn callbacks for tracking tool usage.
|
|
84
|
-
* Used by
|
|
83
|
+
* Used by the detached Agent path for activity and usage tracking.
|
|
85
84
|
*/
|
|
86
85
|
function createActivityTracker(maxTurns?: number, onStreamUpdate?: () => void) {
|
|
87
86
|
const state: AgentActivity = {
|
|
@@ -189,27 +188,6 @@ function formatTaskNotification(record: AgentRecord, resultMaxLen: number): stri
|
|
|
189
188
|
].filter(Boolean).join('\n');
|
|
190
189
|
}
|
|
191
190
|
|
|
192
|
-
/** Build AgentDetails from a base + record-specific fields. */
|
|
193
|
-
function buildDetails(
|
|
194
|
-
base: Pick<AgentDetails, "displayName" | "description" | "subagentType" | "modelName" | "tags">,
|
|
195
|
-
record: { toolUses: number; startedAt: number; completedAt?: number; status: string; error?: string; id?: string; session?: any; lifetimeUsage: LifetimeUsage },
|
|
196
|
-
activity?: AgentActivity,
|
|
197
|
-
overrides?: Partial<AgentDetails>,
|
|
198
|
-
): AgentDetails {
|
|
199
|
-
return {
|
|
200
|
-
...base,
|
|
201
|
-
toolUses: record.toolUses,
|
|
202
|
-
tokens: formatLifetimeTokens(record),
|
|
203
|
-
turnCount: activity?.turnCount,
|
|
204
|
-
maxTurns: activity?.maxTurns,
|
|
205
|
-
durationMs: (record.completedAt ?? Date.now()) - record.startedAt,
|
|
206
|
-
status: record.status as AgentDetails["status"],
|
|
207
|
-
agentId: record.id,
|
|
208
|
-
error: record.error,
|
|
209
|
-
...overrides,
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
|
|
213
191
|
/** Build notification details for the custom message renderer. */
|
|
214
192
|
function buildNotificationDetails(record: AgentRecord, resultMaxLen: number, activity?: AgentActivity): NotificationDetails {
|
|
215
193
|
const totalTokens = getLifetimeTotal(record.lifetimeUsage);
|
|
@@ -871,7 +849,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
871
849
|
description:
|
|
872
850
|
'Opt-in only — fire later instead of now. Omit to run immediately (the default, almost always correct). ' +
|
|
873
851
|
'Formats: 6-field cron ("0 0 9 * * 1" = 9am Mon), interval ("5m"/"1h"), one-shot ("+10m" or ISO). ' +
|
|
874
|
-
'
|
|
852
|
+
'Scheduled agents run detached; run_in_background has no effect. Incompatible with inherit_context and resume. Returns job ID.',
|
|
875
853
|
}),
|
|
876
854
|
),
|
|
877
855
|
};
|
|
@@ -892,10 +870,10 @@ Custom agents: .pi/agents/<name>.md (project) or ${getAgentDir()}/agents/<name>.
|
|
|
892
870
|
|
|
893
871
|
Notes:
|
|
894
872
|
- description: 3-5 words (shown in UI). Prompts must be self-contained — the agent has not seen this conversation.
|
|
895
|
-
-
|
|
873
|
+
- Every Agent call is detached and returns an agent ID immediately. run_in_background is accepted for compatibility but has no effect. You are notified when agents finish — never poll or sleep.
|
|
896
874
|
- For nonblocking grouped notification, add wait: true. Use subagent_wait_group to create/update/seal explicit groups; wait_group_done seals after the final spawn.
|
|
897
|
-
-
|
|
898
|
-
- resume continues a previous agent by ID; steer_subagent messages a running one.
|
|
875
|
+
- Results arrive via the completion notification; then call get_subagent_result once per task ID with wait omitted or false to render the native expandable result. The result is not shown inline. Verify an agent's claimed code changes before reporting work done.
|
|
876
|
+
- resume continues a previous agent by ID and is also detached; steer_subagent messages a running one.
|
|
899
877
|
- isolation: "worktree" runs the agent in an isolated git worktree; changes land on a branch.`;
|
|
900
878
|
|
|
901
879
|
const fullAgentToolDescription = `Launch a new agent to handle complex, multi-step tasks autonomously. Each agent type has specific capabilities and tools available to it.
|
|
@@ -914,15 +892,12 @@ If the target is already known, use a direct tool — \`read\` for a known path,
|
|
|
914
892
|
## Usage notes
|
|
915
893
|
|
|
916
894
|
- Always include a short (3-5 word) description summarizing what the agent will do (shown in UI).
|
|
917
|
-
-
|
|
918
|
-
-
|
|
895
|
+
- Every Agent call is detached and returns an agent ID immediately. run_in_background is accepted for compatibility but has no effect. When you launch multiple agents for independent work, send them in a single message with multiple tool uses so they run concurrently.
|
|
896
|
+
- Results arrive via the completion notification; call get_subagent_result once per completed task ID with wait omitted or false to render the native expandable result. Agent results are not returned inline — never poll or sleep waiting for them.
|
|
897
|
+
- For nonblocking grouped notification, set wait: true. Omit wait_group for a one-agent implicit group, or create an explicit group with subagent_wait_group and seal it (or set wait_group_done: true on the final Agent call).
|
|
919
898
|
- Trust but verify: an agent's summary describes what it intended to do, not necessarily what it did. When an agent writes or edits code, check the actual changes before reporting work as done.
|
|
920
|
-
- Use
|
|
921
|
-
-
|
|
922
|
-
- When a background or wait-group completion notification arrives, call get_subagent_result once per completed task-id with wait omitted or false before summarizing if the user needs the outputs. This preserves native expandable Get Subagent Result UI without blocking.
|
|
923
|
-
- 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.
|
|
924
|
-
- 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.
|
|
925
|
-
- Use steer_subagent to send mid-run messages to a running background agent.
|
|
899
|
+
- Use resume with an agent ID to continue a previous agent's work; resume is detached and also returns immediately.
|
|
900
|
+
- Use steer_subagent to send mid-run messages to a running agent.
|
|
926
901
|
- 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.
|
|
927
902
|
- If an agent's description says it should be used proactively, try to use it without the user having to ask for it first.
|
|
928
903
|
- Use model to specify a different model (as "provider/modelId", or fuzzy e.g. "haiku", "sonnet").
|
|
@@ -998,9 +973,9 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
998
973
|
promptGuidelines: [
|
|
999
974
|
"Use Agent with specialized agents when the task matches an agent type's description. Subagents are valuable for parallelizing independent queries or for protecting the main context window from excessive results, but should not be used excessively when not needed. Importantly, avoid duplicating work that subagents are already doing — if you delegate research to a subagent, do not also perform the same searches yourself.",
|
|
1000
975
|
"For broad codebase exploration or research, spawn Agent with an appropriate subagent_type (e.g. Explore). Otherwise use direct tools (read, grep, find) when the target is already known.",
|
|
1001
|
-
"
|
|
1002
|
-
"
|
|
1003
|
-
"
|
|
976
|
+
"Every Agent call is detached and returns an agent ID immediately. run_in_background is accepted for compatibility but has no effect; continue with other work instead of waiting.",
|
|
977
|
+
"Use wait: true to receive one grouped completion notification; create/update/seal explicit groups with subagent_wait_group.",
|
|
978
|
+
"Results arrive via the completion notification. Then call get_subagent_result once per completed task ID with wait omitted or false before summarizing if the user needs the outputs; this preserves native expandable result UI without blocking.",
|
|
1004
979
|
"Trust but verify: an agent's summary describes intent, not outcome. When an agent writes or edits code, check the actual changes before reporting work as done.",
|
|
1005
980
|
],
|
|
1006
981
|
parameters: Type.Object({
|
|
@@ -1032,12 +1007,12 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1032
1007
|
),
|
|
1033
1008
|
run_in_background: Type.Optional(
|
|
1034
1009
|
Type.Boolean({
|
|
1035
|
-
description: "
|
|
1010
|
+
description: "Accepted for compatibility; it has no effect. Agent calls always detach, return an agent ID immediately, and notify on completion.",
|
|
1036
1011
|
}),
|
|
1037
1012
|
),
|
|
1038
1013
|
wait: Type.Optional(
|
|
1039
1014
|
Type.Boolean({
|
|
1040
|
-
description: "
|
|
1015
|
+
description: "Suppress the individual completion notification and wait for a sealed group notification. This never blocks execution.",
|
|
1041
1016
|
}),
|
|
1042
1017
|
),
|
|
1043
1018
|
wait_group: Type.Optional(
|
|
@@ -1164,7 +1139,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1164
1139
|
|
|
1165
1140
|
// ---- Execute ----
|
|
1166
1141
|
|
|
1167
|
-
execute: async (toolCallId, params,
|
|
1142
|
+
execute: async (toolCallId, params, _signal, _onUpdate, ctx) => {
|
|
1168
1143
|
// Ensure we have UI context for widget rendering
|
|
1169
1144
|
widget.setUICtx(ctx.ui as UICtx);
|
|
1170
1145
|
|
|
@@ -1174,7 +1149,6 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1174
1149
|
const rawType = params.subagent_type as SubagentType;
|
|
1175
1150
|
const resolved = resolveType(rawType);
|
|
1176
1151
|
const subagentType = resolved ?? "general-purpose";
|
|
1177
|
-
const fellBack = resolved === undefined;
|
|
1178
1152
|
|
|
1179
1153
|
const displayName = getDisplayName(subagentType);
|
|
1180
1154
|
|
|
@@ -1226,7 +1200,6 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1226
1200
|
|
|
1227
1201
|
const thinking = resolvedConfig.thinking;
|
|
1228
1202
|
const inheritContext = resolvedConfig.inheritContext;
|
|
1229
|
-
const runInBackground = resolvedConfig.runInBackground;
|
|
1230
1203
|
const wait = params.wait === true;
|
|
1231
1204
|
const waitGroup = typeof params.wait_group === "string" ? params.wait_group.trim() : undefined;
|
|
1232
1205
|
const waitGroupDone = params.wait_group_done === true;
|
|
@@ -1274,7 +1247,8 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1274
1247
|
maxTurns: normalizeMaxTurns(resolvedConfig.maxTurns),
|
|
1275
1248
|
isolated,
|
|
1276
1249
|
inheritContext,
|
|
1277
|
-
|
|
1250
|
+
// Agent calls are always detached; run_in_background is a compatibility no-op.
|
|
1251
|
+
runInBackground: true,
|
|
1278
1252
|
isolation,
|
|
1279
1253
|
};
|
|
1280
1254
|
// Tool-result render shows the mode label too; viewer's header already does.
|
|
@@ -1292,9 +1266,6 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1292
1266
|
if ((waitGroup || waitGroupDone) && !wait) {
|
|
1293
1267
|
return textResult("wait_group and wait_group_done require wait: true.");
|
|
1294
1268
|
}
|
|
1295
|
-
if (wait && !runInBackground) {
|
|
1296
|
-
return textResult("wait: true requires run_in_background: true; it controls nonblocking background notifications.");
|
|
1297
|
-
}
|
|
1298
1269
|
if (wait && params.schedule) {
|
|
1299
1270
|
return textResult("Cannot combine wait: true with schedule — scheduled jobs are separate future runs.");
|
|
1300
1271
|
}
|
|
@@ -1313,9 +1284,6 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1313
1284
|
if (params.inherit_context) {
|
|
1314
1285
|
return textResult("Cannot combine `schedule` with `inherit_context` — there is no parent conversation at fire time.");
|
|
1315
1286
|
}
|
|
1316
|
-
if (params.run_in_background === false) {
|
|
1317
|
-
return textResult("Cannot combine `schedule` with `run_in_background: false` — scheduled jobs always run in background.");
|
|
1318
|
-
}
|
|
1319
1287
|
if (!scheduler.isActive()) {
|
|
1320
1288
|
return textResult("Scheduler is not active in this session yet. Try again after the session has fully started.");
|
|
1321
1289
|
}
|
|
@@ -1343,7 +1311,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1343
1311
|
}
|
|
1344
1312
|
}
|
|
1345
1313
|
|
|
1346
|
-
// Resume existing agent
|
|
1314
|
+
// Resume existing agent without blocking the parent tool call.
|
|
1347
1315
|
if (params.resume) {
|
|
1348
1316
|
const existing = manager.getRecord(params.resume);
|
|
1349
1317
|
if (!existing) {
|
|
@@ -1352,241 +1320,133 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1352
1320
|
if (!existing.session) {
|
|
1353
1321
|
return textResult(`Agent "${params.resume}" has no active session to resume.`);
|
|
1354
1322
|
}
|
|
1355
|
-
const record = await manager.resume(params.resume, params.prompt, signal);
|
|
1356
|
-
if (!record) {
|
|
1357
|
-
return textResult(`Failed to resume agent "${params.resume}".`);
|
|
1358
|
-
}
|
|
1359
|
-
// A failed resume surfaces the error, plus any partial output THIS
|
|
1360
|
-
// resume produced (never the previous turn's answer, #144).
|
|
1361
|
-
if (record.status === "error") {
|
|
1362
|
-
return textResult(`Agent failed: ${record.error}${partialOutputSuffix(record)}`, buildDetails(detailBase, record));
|
|
1363
|
-
}
|
|
1364
|
-
return textResult(
|
|
1365
|
-
record.result?.trim() || "No output.",
|
|
1366
|
-
buildDetails(detailBase, record),
|
|
1367
|
-
);
|
|
1368
|
-
}
|
|
1369
|
-
|
|
1370
|
-
// Background execution
|
|
1371
|
-
if (runInBackground) {
|
|
1372
|
-
const { state: bgState, callbacks: bgCallbacks } = createActivityTracker(effectiveMaxTurns);
|
|
1373
|
-
|
|
1374
|
-
// Wrap onSessionCreated to wire output file streaming.
|
|
1375
|
-
// The callback reads the transcript paths installed synchronously by
|
|
1376
|
-
// onSpawned before the agent can queue or start.
|
|
1377
|
-
let id = "";
|
|
1378
|
-
let effectiveWaitGroupId: string | undefined;
|
|
1379
|
-
let implicitWaitGroupId: string | undefined;
|
|
1380
|
-
if (wait) {
|
|
1381
|
-
if (waitGroup) {
|
|
1382
|
-
if (!waitGroups.hasGroup(waitGroup)) {
|
|
1383
|
-
return textResult(`Wait group not found: "${waitGroup}". Create it with subagent_wait_group first.`);
|
|
1384
|
-
}
|
|
1385
|
-
effectiveWaitGroupId = waitGroup;
|
|
1386
|
-
} else {
|
|
1387
|
-
implicitWaitGroupId = waitGroups.create(params.description);
|
|
1388
|
-
effectiveWaitGroupId = implicitWaitGroupId;
|
|
1389
|
-
}
|
|
1390
|
-
}
|
|
1391
|
-
const joinMode = wait ? undefined : resolveJoinMode(defaultJoinMode, true);
|
|
1392
|
-
const origBgOnSession = bgCallbacks.onSessionCreated;
|
|
1393
|
-
bgCallbacks.onSessionCreated = (session: any) => {
|
|
1394
|
-
origBgOnSession(session);
|
|
1395
|
-
const rec = manager.getRecord(id);
|
|
1396
|
-
if (rec?.outputFile) {
|
|
1397
|
-
rec.outputCleanup = streamToOutputFile(session, rec.outputFile, id, ctx.cwd, rec.historyFile);
|
|
1398
|
-
}
|
|
1399
|
-
};
|
|
1400
|
-
|
|
1401
|
-
try {
|
|
1402
|
-
id = manager.spawn(pi, ctx, subagentType, params.prompt, {
|
|
1403
|
-
description: params.description,
|
|
1404
|
-
model,
|
|
1405
|
-
maxTurns: effectiveMaxTurns,
|
|
1406
|
-
isolated,
|
|
1407
|
-
inheritContext,
|
|
1408
|
-
thinkingLevel: thinking,
|
|
1409
|
-
isBackground: true,
|
|
1410
|
-
isolation,
|
|
1411
|
-
invocation: agentInvocation,
|
|
1412
|
-
waitGroupId: effectiveWaitGroupId,
|
|
1413
|
-
onSpawned: (spawnedId) => {
|
|
1414
|
-
id = spawnedId;
|
|
1415
|
-
attachTranscript(manager.getRecord(spawnedId), spawnedId);
|
|
1416
|
-
if (effectiveWaitGroupId) waitGroups.addAgent(effectiveWaitGroupId, spawnedId);
|
|
1417
|
-
},
|
|
1418
|
-
...bgCallbacks,
|
|
1419
|
-
});
|
|
1420
|
-
} catch (err) {
|
|
1421
|
-
if (effectiveWaitGroupId && id) waitGroups.removeAgent(effectiveWaitGroupId, id);
|
|
1422
|
-
if (implicitWaitGroupId) waitGroups.discard(implicitWaitGroupId);
|
|
1423
|
-
return textResult(err instanceof Error ? err.message : String(err));
|
|
1424
|
-
}
|
|
1425
|
-
|
|
1426
|
-
// Set join metadata after spawn. Transcript metadata was installed by
|
|
1427
|
-
// the manager's synchronous onSpawned callback before this point.
|
|
1428
|
-
const record = manager.getRecord(id);
|
|
1429
|
-
if (record && joinMode) {
|
|
1430
|
-
record.joinMode = joinMode;
|
|
1431
|
-
record.toolCallId = toolCallId;
|
|
1432
|
-
}
|
|
1433
1323
|
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
// smart or group — add to current batch
|
|
1440
|
-
currentBatchAgents.push({ id, joinMode });
|
|
1441
|
-
// Debounce: reset timer on each new agent so parallel tool calls
|
|
1442
|
-
// dispatched across multiple event loop ticks are captured together
|
|
1443
|
-
if (batchFinalizeTimer) clearTimeout(batchFinalizeTimer);
|
|
1444
|
-
batchFinalizeTimer = setTimeout(finalizeBatch, 100);
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
agentActivity.set(id, bgState);
|
|
1324
|
+
// A resumed turn has a new unread result and follows the same detached
|
|
1325
|
+
// completion-notification flow as a fresh Agent call.
|
|
1326
|
+
existing.resultConsumed = false;
|
|
1327
|
+
existing.toolCallId = toolCallId;
|
|
1328
|
+
void manager.resume(params.resume, params.prompt);
|
|
1448
1329
|
widget.ensureTimer();
|
|
1449
1330
|
widget.update();
|
|
1450
1331
|
|
|
1451
|
-
// Emit created event
|
|
1452
|
-
pi.events.emit("subagents:created", {
|
|
1453
|
-
id,
|
|
1454
|
-
type: subagentType,
|
|
1455
|
-
description: params.description,
|
|
1456
|
-
isBackground: true,
|
|
1457
|
-
});
|
|
1458
|
-
|
|
1459
|
-
const isQueued = record?.status === "queued";
|
|
1460
1332
|
return textResult(
|
|
1461
|
-
`Agent
|
|
1462
|
-
`Agent ID: ${
|
|
1333
|
+
`Agent resumed in background.\n` +
|
|
1334
|
+
`Agent ID: ${params.resume}\n` +
|
|
1463
1335
|
`Type: ${displayName}\n` +
|
|
1464
|
-
`Description: ${params.description}\n` +
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
(effectiveWaitGroupId
|
|
1468
|
-
? `\nWait group: ${effectiveWaitGroupId}${implicitWaitGroupId || waitGroupDone ? " (sealed)" : " (open — seal it with subagent_wait_group)"}.\n` +
|
|
1469
|
-
`You will receive one grouped notification when the sealed wait group completes.\n`
|
|
1470
|
-
: `\nYou will be notified when this agent completes.\n`) +
|
|
1471
|
-
`After the completion notification, call get_subagent_result with wait omitted or false to render native expandable results; use steer_subagent to send messages while it is running.\n` +
|
|
1336
|
+
`Description: ${params.description}\n\n` +
|
|
1337
|
+
`You will receive a completion notification when this agent finishes.\n` +
|
|
1338
|
+
`Results arrive via that notification; after it, call get_subagent_result with wait omitted or false to render native expandable results.\n` +
|
|
1472
1339
|
`Do not duplicate this agent's work.`,
|
|
1473
|
-
{ ...detailBase, toolUses: 0, tokens: "", durationMs: 0, status: "background" as const, agentId:
|
|
1340
|
+
{ ...detailBase, toolUses: 0, tokens: "", durationMs: 0, status: "background" as const, agentId: params.resume },
|
|
1474
1341
|
);
|
|
1475
1342
|
}
|
|
1476
1343
|
|
|
1477
|
-
//
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
spinnerFrame: spinnerFrame % SPINNER.length,
|
|
1493
|
-
};
|
|
1494
|
-
onUpdate?.({
|
|
1495
|
-
content: [{ type: "text", text: `${fgState.toolUses} tool uses...` }],
|
|
1496
|
-
details: details as any,
|
|
1497
|
-
});
|
|
1498
|
-
};
|
|
1499
|
-
|
|
1500
|
-
const { state: fgState, callbacks: fgCallbacks } = createActivityTracker(effectiveMaxTurns, streamUpdate);
|
|
1501
|
-
|
|
1502
|
-
// Wire session creation: register in widget + stream to output file.
|
|
1503
|
-
// The output file path is set synchronously after spawn (below),
|
|
1504
|
-
// before onSessionCreated fires — same pattern as background agents.
|
|
1505
|
-
const origOnSession = fgCallbacks.onSessionCreated;
|
|
1506
|
-
fgCallbacks.onSessionCreated = (session: any) => {
|
|
1507
|
-
origOnSession(session);
|
|
1508
|
-
for (const a of manager.listAgents()) {
|
|
1509
|
-
if (a.session === session) {
|
|
1510
|
-
fgId = a.id;
|
|
1511
|
-
agentActivity.set(a.id, fgState);
|
|
1512
|
-
widget.ensureTimer();
|
|
1513
|
-
widget.update();
|
|
1514
|
-
break;
|
|
1344
|
+
// All fresh Agent calls use the detached path. `run_in_background` is
|
|
1345
|
+
// intentionally ignored; bypassQueue preserves the historical behavior
|
|
1346
|
+
// where foreground calls started immediately despite the background cap.
|
|
1347
|
+
const { state: bgState, callbacks: bgCallbacks } = createActivityTracker(effectiveMaxTurns);
|
|
1348
|
+
|
|
1349
|
+
// Wrap onSessionCreated to wire output file streaming.
|
|
1350
|
+
// The callback reads the transcript paths installed synchronously by
|
|
1351
|
+
// onSpawned before the agent can queue or start.
|
|
1352
|
+
let id = "";
|
|
1353
|
+
let effectiveWaitGroupId: string | undefined;
|
|
1354
|
+
let implicitWaitGroupId: string | undefined;
|
|
1355
|
+
if (wait) {
|
|
1356
|
+
if (waitGroup) {
|
|
1357
|
+
if (!waitGroups.hasGroup(waitGroup)) {
|
|
1358
|
+
return textResult(`Wait group not found: "${waitGroup}". Create it with subagent_wait_group first.`);
|
|
1515
1359
|
}
|
|
1360
|
+
effectiveWaitGroupId = waitGroup;
|
|
1361
|
+
} else {
|
|
1362
|
+
implicitWaitGroupId = waitGroups.create(params.description);
|
|
1363
|
+
effectiveWaitGroupId = implicitWaitGroupId;
|
|
1516
1364
|
}
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1365
|
+
}
|
|
1366
|
+
const joinMode = wait ? undefined : resolveJoinMode(defaultJoinMode, true);
|
|
1367
|
+
const origBgOnSession = bgCallbacks.onSessionCreated;
|
|
1368
|
+
bgCallbacks.onSessionCreated = (session: any) => {
|
|
1369
|
+
origBgOnSession(session);
|
|
1370
|
+
const rec = manager.getRecord(id);
|
|
1371
|
+
if (rec?.outputFile) {
|
|
1372
|
+
rec.outputCleanup = streamToOutputFile(session, rec.outputFile, id, ctx.cwd, rec.historyFile);
|
|
1523
1373
|
}
|
|
1524
1374
|
};
|
|
1525
1375
|
|
|
1526
|
-
// Animate spinner at ~80ms (smooth rotation through 10 braille frames)
|
|
1527
|
-
const spinnerInterval = setInterval(() => {
|
|
1528
|
-
spinnerFrame++;
|
|
1529
|
-
streamUpdate();
|
|
1530
|
-
}, 80);
|
|
1531
|
-
|
|
1532
|
-
streamUpdate();
|
|
1533
|
-
|
|
1534
|
-
let record: AgentRecord;
|
|
1535
1376
|
try {
|
|
1536
|
-
|
|
1377
|
+
id = manager.spawn(pi, ctx, subagentType, params.prompt, {
|
|
1537
1378
|
description: params.description,
|
|
1538
1379
|
model,
|
|
1539
1380
|
maxTurns: effectiveMaxTurns,
|
|
1540
1381
|
isolated,
|
|
1541
1382
|
inheritContext,
|
|
1542
1383
|
thinkingLevel: thinking,
|
|
1384
|
+
isBackground: true,
|
|
1385
|
+
bypassQueue: true,
|
|
1543
1386
|
isolation,
|
|
1544
1387
|
invocation: agentInvocation,
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1388
|
+
waitGroupId: effectiveWaitGroupId,
|
|
1389
|
+
onSpawned: (spawnedId) => {
|
|
1390
|
+
id = spawnedId;
|
|
1391
|
+
attachTranscript(manager.getRecord(spawnedId), spawnedId);
|
|
1392
|
+
if (effectiveWaitGroupId) waitGroups.addAgent(effectiveWaitGroupId, spawnedId);
|
|
1393
|
+
},
|
|
1394
|
+
...bgCallbacks,
|
|
1552
1395
|
});
|
|
1553
|
-
record = fgResult.record;
|
|
1554
1396
|
} catch (err) {
|
|
1555
|
-
|
|
1397
|
+
if (effectiveWaitGroupId && id) waitGroups.removeAgent(effectiveWaitGroupId, id);
|
|
1398
|
+
if (implicitWaitGroupId) waitGroups.discard(implicitWaitGroupId);
|
|
1556
1399
|
return textResult(err instanceof Error ? err.message : String(err));
|
|
1557
1400
|
}
|
|
1558
1401
|
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
if (
|
|
1563
|
-
|
|
1564
|
-
|
|
1402
|
+
// Set join metadata after spawn. Transcript metadata was installed by
|
|
1403
|
+
// the manager's synchronous onSpawned callback before this point.
|
|
1404
|
+
const record = manager.getRecord(id);
|
|
1405
|
+
if (record && joinMode) {
|
|
1406
|
+
record.joinMode = joinMode;
|
|
1407
|
+
record.toolCallId = toolCallId;
|
|
1565
1408
|
}
|
|
1566
1409
|
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1410
|
+
if (effectiveWaitGroupId) {
|
|
1411
|
+
if (implicitWaitGroupId || waitGroupDone) waitGroups.seal(effectiveWaitGroupId);
|
|
1412
|
+
} else if (joinMode == null || joinMode === 'async') {
|
|
1413
|
+
// No join mode or explicit async — not part of any batch.
|
|
1414
|
+
} else {
|
|
1415
|
+
// Smart or group — add to current batch.
|
|
1416
|
+
currentBatchAgents.push({ id, joinMode });
|
|
1417
|
+
// Debounce: reset timer on each new agent so parallel tool calls
|
|
1418
|
+
// dispatched across multiple event loop ticks are captured together.
|
|
1419
|
+
if (batchFinalizeTimer) clearTimeout(batchFinalizeTimer);
|
|
1420
|
+
batchFinalizeTimer = setTimeout(finalizeBatch, 100);
|
|
1421
|
+
}
|
|
1571
1422
|
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
? `Note: Unknown agent type "${rawType}" — using ${resolveType("general-purpose") ? "general-purpose" : "the fallback agent config"}.\n\n`
|
|
1576
|
-
: "";
|
|
1423
|
+
agentActivity.set(id, bgState);
|
|
1424
|
+
widget.ensureTimer();
|
|
1425
|
+
widget.update();
|
|
1577
1426
|
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1427
|
+
// Emit created event.
|
|
1428
|
+
pi.events.emit("subagents:created", {
|
|
1429
|
+
id,
|
|
1430
|
+
type: subagentType,
|
|
1431
|
+
description: params.description,
|
|
1432
|
+
isBackground: true,
|
|
1433
|
+
});
|
|
1582
1434
|
|
|
1583
|
-
const
|
|
1584
|
-
const statsParts = [`${record.toolUses} tool uses`];
|
|
1585
|
-
if (tokenText) statsParts.push(tokenText);
|
|
1435
|
+
const isQueued = record?.status === "queued";
|
|
1586
1436
|
return textResult(
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1437
|
+
`Agent ${isQueued ? "queued" : "started"} in background.\n` +
|
|
1438
|
+
`Agent ID: ${id}\n` +
|
|
1439
|
+
`Type: ${displayName}\n` +
|
|
1440
|
+
`Description: ${params.description}\n` +
|
|
1441
|
+
(record?.outputFile ? `Output file: ${record.outputFile}\n` : "") +
|
|
1442
|
+
(isQueued ? `Position: queued (max ${manager.getMaxConcurrent()} concurrent)\n` : "") +
|
|
1443
|
+
(effectiveWaitGroupId
|
|
1444
|
+
? `\nWait group: ${effectiveWaitGroupId}${implicitWaitGroupId || waitGroupDone ? " (sealed)" : " (open — seal it with subagent_wait_group)"}.\n` +
|
|
1445
|
+
`You will receive one grouped completion notification when the sealed wait group completes.\n`
|
|
1446
|
+
: `\nYou will receive a completion notification when this agent completes.\n`) +
|
|
1447
|
+
`Results arrive via the completion notification; after it, call get_subagent_result with wait omitted or false to render native expandable results.\n` +
|
|
1448
|
+
`Do not duplicate this agent's work.`,
|
|
1449
|
+
{ ...detailBase, toolUses: 0, tokens: "", durationMs: 0, status: "background" as const, agentId: id },
|
|
1590
1450
|
);
|
|
1591
1451
|
},
|
|
1592
1452
|
}));
|
|
@@ -1597,7 +1457,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1597
1457
|
name: SUBAGENT_TOOL_NAMES.WAIT_GROUP,
|
|
1598
1458
|
label: "Subagent Wait Group",
|
|
1599
1459
|
description:
|
|
1600
|
-
"Create, update, or seal a nonblocking wait group for
|
|
1460
|
+
"Create, update, or seal a nonblocking wait group for detached Agent calls. " +
|
|
1601
1461
|
"A sealed group sends one completion notification after all member agents finish.",
|
|
1602
1462
|
promptSnippet: "Create, update, or seal a grouped subagent completion notification",
|
|
1603
1463
|
parameters: Type.Object({
|
|
@@ -1626,8 +1486,8 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1626
1486
|
`Created subagent wait group.\n` +
|
|
1627
1487
|
`Group ID: ${createdId}\n` +
|
|
1628
1488
|
`Summary: ${summary}\n\n` +
|
|
1629
|
-
`Use Agent with
|
|
1630
|
-
`Seal the group after adding members.`,
|
|
1489
|
+
`Use Agent with wait: true, wait_group: "${createdId}". ` +
|
|
1490
|
+
`Seal the group after adding members. Agent calls are detached automatically.`,
|
|
1631
1491
|
);
|
|
1632
1492
|
}
|
|
1633
1493
|
if (action === "update") {
|
|
@@ -1666,7 +1526,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1666
1526
|
name: SUBAGENT_TOOL_NAMES.GET_RESULT,
|
|
1667
1527
|
label: "Get Agent Result",
|
|
1668
1528
|
description:
|
|
1669
|
-
"Check status and retrieve results from a
|
|
1529
|
+
"Check status and retrieve results from a detached agent. Use the agent ID returned by Agent.",
|
|
1670
1530
|
promptSnippet: "Check status and retrieve results from a background agent",
|
|
1671
1531
|
parameters: Type.Object({
|
|
1672
1532
|
agent_id: Type.String({
|
|
@@ -2341,7 +2201,7 @@ extensions: <true (inherit all MCP/extension tools), false (none), or comma-sepa
|
|
|
2341
2201
|
skills: <true (inherit all), false (none), or comma-separated skill names to preload into prompt. Default: true>
|
|
2342
2202
|
disallowed_tools: <comma-separated tool names to block, even if otherwise available. Omit for none>
|
|
2343
2203
|
inherit_context: <true to fork parent conversation into agent so it sees chat history. Default: false>
|
|
2344
|
-
run_in_background: <
|
|
2204
|
+
run_in_background: <accepted for compatibility; has no effect>
|
|
2345
2205
|
output_transcript: <false to write no transcript file or path for this agent. Independent of persist_session. Default: true>
|
|
2346
2206
|
isolated: <true for no extension/MCP tools, only built-in tools. Default: false>
|
|
2347
2207
|
memory: <"user" (global), "project" (per-project), or "local" (gitignored per-project) for persistent memory. Omit for none>
|
|
@@ -2363,23 +2223,33 @@ Guidelines for choosing settings:
|
|
|
2363
2223
|
|
|
2364
2224
|
Write the file using the write tool. Only write the file, nothing else.`;
|
|
2365
2225
|
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2226
|
+
let id: string;
|
|
2227
|
+
try {
|
|
2228
|
+
id = manager.spawn(pi, ctx, "general-purpose", generatePrompt, {
|
|
2229
|
+
description: `Generate ${name} agent`,
|
|
2230
|
+
maxTurns: 5,
|
|
2231
|
+
isBackground: true,
|
|
2232
|
+
bypassQueue: true,
|
|
2233
|
+
});
|
|
2234
|
+
} catch (err) {
|
|
2235
|
+
ctx.ui.notify(`Generation failed: ${err instanceof Error ? err.message : String(err)}`, "warning");
|
|
2373
2236
|
return;
|
|
2374
2237
|
}
|
|
2375
2238
|
|
|
2376
|
-
|
|
2239
|
+
const record = manager.getRecord(id);
|
|
2240
|
+
void record?.promise?.then(() => {
|
|
2241
|
+
if (record.status === "error") {
|
|
2242
|
+
ctx.ui.notify(`Generation failed: ${record.error}`, "warning");
|
|
2243
|
+
return;
|
|
2244
|
+
}
|
|
2377
2245
|
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2246
|
+
reloadCustomAgents();
|
|
2247
|
+
if (existsSync(targetPath)) {
|
|
2248
|
+
ctx.ui.notify(`Created ${targetPath}`, "info");
|
|
2249
|
+
} else {
|
|
2250
|
+
ctx.ui.notify("Agent generation completed but file was not created. Check the agent output.", "warning");
|
|
2251
|
+
}
|
|
2252
|
+
});
|
|
2383
2253
|
}
|
|
2384
2254
|
|
|
2385
2255
|
async function showManualWizard(ctx: ExtensionCommandContext, targetDir: string) {
|
|
@@ -2548,7 +2418,7 @@ ${systemPrompt}
|
|
|
2548
2418
|
{
|
|
2549
2419
|
id: "widgetMode",
|
|
2550
2420
|
label: "Widget",
|
|
2551
|
-
description: "Above-editor agent widget: all = every agent; background = hide foreground
|
|
2421
|
+
description: "Above-editor agent widget: all = every agent; background = hide explicitly foreground programmatic runs; off = hide the widget.",
|
|
2552
2422
|
currentValue: getWidgetMode(),
|
|
2553
2423
|
values: ["all", "background", "off"],
|
|
2554
2424
|
},
|
package/src/types.ts
CHANGED
|
@@ -51,7 +51,7 @@ export interface AgentConfig {
|
|
|
51
51
|
promptMode: "replace" | "append";
|
|
52
52
|
/** Default for spawn: fork parent conversation. undefined = caller decides. */
|
|
53
53
|
inheritContext?: boolean;
|
|
54
|
-
/**
|
|
54
|
+
/** Compatibility field; Agent calls always run detached regardless of this value. */
|
|
55
55
|
runInBackground?: boolean;
|
|
56
56
|
/** Default for spawn: no extension tools. undefined = caller decides. */
|
|
57
57
|
isolated?: boolean;
|
|
@@ -147,6 +147,7 @@ export interface AgentInvocation {
|
|
|
147
147
|
maxTurns?: number;
|
|
148
148
|
isolated?: boolean;
|
|
149
149
|
inheritContext?: boolean;
|
|
150
|
+
/** Effective detached execution mode; retained for persisted UI compatibility. */
|
|
150
151
|
runInBackground?: boolean;
|
|
151
152
|
isolation?: IsolationMode;
|
|
152
153
|
}
|
package/src/ui/agent-widget.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Uses the callback form of setWidget for themed rendering.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { isKeyRelease, Key, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
8
|
+
import { Editor, isKeyRelease, Key, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
9
9
|
import type { AgentManager } from "../agent-manager.js";
|
|
10
10
|
import { getConfig } from "../agent-types.js";
|
|
11
11
|
import type { AgentInvocation, AgentRecord, SubagentType, WidgetMode } from "../types.js";
|
|
@@ -394,22 +394,44 @@ export class AgentWidget {
|
|
|
394
394
|
* Track an AgentWidget-owned modal (the `/agents` menus and friends).
|
|
395
395
|
*
|
|
396
396
|
* The widget's terminal listener runs before the focused component, so while
|
|
397
|
-
* a modal is open the listener must not claim ↑/↓/enter/escape.
|
|
398
|
-
* prompt editor currently owns the keyboard is deliberately not consulted:
|
|
399
|
-
* pi can replace it through `ctx.ui.setEditorComponent()` (for example
|
|
400
|
-
* pi-tmux-cursor-focus wraps it under tmux), and the extension may also
|
|
401
|
-
* resolve its own copy of `@earendil-works/pi-tui` at a different version
|
|
402
|
-
* than the host's. Neither `instanceof` nor a structural probe can identify
|
|
403
|
-
* the editor reliably in that environment, and a wrong answer silently
|
|
404
|
-
* disables roster navigation entirely.
|
|
397
|
+
* a modal is open the listener must not claim ↑/↓/enter/escape.
|
|
405
398
|
*/
|
|
406
399
|
setModalOpen(open: boolean): void {
|
|
407
400
|
this.modalDepth = open ? this.modalDepth + 1 : Math.max(0, this.modalDepth - 1);
|
|
408
401
|
}
|
|
409
402
|
|
|
410
|
-
/**
|
|
411
|
-
|
|
412
|
-
|
|
403
|
+
/**
|
|
404
|
+
* True when pi's prompt editor owns the keyboard.
|
|
405
|
+
*
|
|
406
|
+
* Both halves of this are load-bearing, and both were arrived at the hard way.
|
|
407
|
+
*
|
|
408
|
+
* A terminal-input listener runs BEFORE the focused component, and pi swaps
|
|
409
|
+
* the prompt editor out for every dialog while `getEditorText()` keeps
|
|
410
|
+
* reporting the detached — empty — editor. Without this guard the widget eats
|
|
411
|
+
* ↑/↓/enter/escape from `/settings`, from other extensions' pickers, and from
|
|
412
|
+
* pi's own menus (upstream issue #123). Only the prompt editor may claim keys.
|
|
413
|
+
*
|
|
414
|
+
* `focused == null` errs toward the editor so list activation keeps working.
|
|
415
|
+
* `focused instanceof Editor` is deliberately NOT replaced with a structural
|
|
416
|
+
* probe: pi's own `/settings` focuses `SettingsList`, a pi-tui class instance,
|
|
417
|
+
* so no shape test separates it from the editor, and another extension can
|
|
418
|
+
* wrap the editor through `setEditorComponent()` (pi-tmux-cursor-focus does
|
|
419
|
+
* exactly that whenever pi runs under tmux), so class identity is the only
|
|
420
|
+
* test that can.
|
|
421
|
+
*
|
|
422
|
+
* That leaves one known failure mode, which is why this is conservative
|
|
423
|
+
* rather than clever: if this extension resolves its own copy of
|
|
424
|
+
* `@earendil-works/pi-tui` at a different version than the host's, the two
|
|
425
|
+
* `Editor` classes are unrelated objects, the check is always false, and
|
|
426
|
+
* roster ↑/↓ stays inert. Inert is the safe failure — it breaks this one
|
|
427
|
+
* feature instead of every other menu in the host. Aligning the peer so the
|
|
428
|
+
* check works is a dependency-graph change in the host project, not something
|
|
429
|
+
* to paper over here.
|
|
430
|
+
*/
|
|
431
|
+
private editorHasFocus(): boolean {
|
|
432
|
+
if (this.modalDepth > 0) return false;
|
|
433
|
+
const focused = (this.tui as { focusedComponent?: unknown } | undefined)?.focusedComponent;
|
|
434
|
+
return focused == null || focused instanceof Editor;
|
|
413
435
|
}
|
|
414
436
|
|
|
415
437
|
private selectedIndexOf(records: readonly AgentRecord[]): number {
|
|
@@ -451,7 +473,7 @@ export class AgentWidget {
|
|
|
451
473
|
if (records.length === 0 || !ui) return false;
|
|
452
474
|
|
|
453
475
|
if (!this.navigationActive) {
|
|
454
|
-
if (direction !== 1 || !this.
|
|
476
|
+
if (direction !== 1 || !this.editorHasFocus() || (ui.getEditorText?.() ?? "") !== "") return false;
|
|
455
477
|
this.activate(records);
|
|
456
478
|
return true;
|
|
457
479
|
}
|
|
@@ -479,7 +501,7 @@ export class AgentWidget {
|
|
|
479
501
|
/** Handle terminal input before it reaches the focused prompt editor. */
|
|
480
502
|
handleKey(data: string): { consume?: boolean; data?: string } | undefined {
|
|
481
503
|
if (!this.uiCtx || isKeyRelease(data)) return undefined;
|
|
482
|
-
if (!this.
|
|
504
|
+
if (!this.editorHasFocus()) {
|
|
483
505
|
if (this.navigationActive) this.deactivate();
|
|
484
506
|
return undefined;
|
|
485
507
|
}
|
|
@@ -33,6 +33,8 @@ export interface ConversationBlock {
|
|
|
33
33
|
/** Compatibility-facing short alias used by the timeline renderer. */
|
|
34
34
|
result?: ConversationToolResultSnapshot;
|
|
35
35
|
toolStatus?: "pending" | "success" | "error";
|
|
36
|
+
/** Message is queued for delivery and has not yet entered the transcript. */
|
|
37
|
+
pending?: boolean;
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
export interface ConversationFormatOptions {
|
|
@@ -21,15 +21,17 @@ export function renderConversationRoleHeader(
|
|
|
21
21
|
theme: ConversationRoleTheme,
|
|
22
22
|
): string {
|
|
23
23
|
const label = conversationRoleLabel(block);
|
|
24
|
-
const color = block.
|
|
25
|
-
? "
|
|
26
|
-
: block.
|
|
27
|
-
? "
|
|
28
|
-
: block.role === "
|
|
29
|
-
? "
|
|
30
|
-
: block.role === "
|
|
31
|
-
? "
|
|
32
|
-
: "
|
|
24
|
+
const color = block.pending
|
|
25
|
+
? "dim"
|
|
26
|
+
: block.kind === "tool"
|
|
27
|
+
? "toolTitle"
|
|
28
|
+
: block.role === "user"
|
|
29
|
+
? "userMessageText"
|
|
30
|
+
: block.role === "assistant"
|
|
31
|
+
? "accent"
|
|
32
|
+
: block.role === "custom"
|
|
33
|
+
? "customMessageLabel"
|
|
34
|
+
: "muted";
|
|
33
35
|
const detail = block.kind === "tool" && block.toolCallId
|
|
34
36
|
? theme.fg("dim", ` · ${block.toolCallId}`)
|
|
35
37
|
: "";
|
|
@@ -326,7 +326,7 @@ export class ConversationTimeline implements Component {
|
|
|
326
326
|
try {
|
|
327
327
|
const markdown = new Markdown(block.markdown || "∅", 2, 0, getMarkdownTheme(), {
|
|
328
328
|
color: (text) => this.theme.fg(
|
|
329
|
-
block.role === "user" ? "userMessageText" : block.role === "meta" ? "muted" : "text",
|
|
329
|
+
block.pending ? "dim" : block.role === "user" ? "userMessageText" : block.role === "meta" ? "muted" : "text",
|
|
330
330
|
text,
|
|
331
331
|
),
|
|
332
332
|
});
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import type { AgentSession, ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
9
9
|
import { copyToClipboard } from "@earendil-works/pi-coding-agent";
|
|
10
|
-
import { type Component,
|
|
10
|
+
import { type Component, Editor, Key, matchesKey, type TUI, type TuiMouseEvent, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
11
11
|
import type { AgentRecord } from "../types.js";
|
|
12
12
|
import { getLifetimeTotal, getSessionContextPercent } from "../usage.js";
|
|
13
13
|
import type { Theme } from "./agent-widget.js";
|
|
@@ -68,7 +68,10 @@ function renderScrollMoreRule(
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
/** The live fields needed by the viewer; historical viewers use a static source. */
|
|
71
|
-
export type ConversationSource = Pick<AgentSession, "messages" | "subscribe"
|
|
71
|
+
export type ConversationSource = Pick<AgentSession, "messages" | "subscribe"> & {
|
|
72
|
+
getSteeringMessages?: () => readonly string[];
|
|
73
|
+
clearQueue?: () => { steering: string[]; followUp: string[] };
|
|
74
|
+
};
|
|
72
75
|
|
|
73
76
|
export function createStaticConversationSource(
|
|
74
77
|
messages: AgentSession["messages"],
|
|
@@ -80,6 +83,22 @@ type RenderLine = TimelineRenderLine;
|
|
|
80
83
|
|
|
81
84
|
const FULL_TOOL_PREVIEW_MAX_CHARS = 500_000;
|
|
82
85
|
|
|
86
|
+
function pendingSteerBlockId(text: string, occurrence: number, usedIds: Set<string>): string {
|
|
87
|
+
let hash = 0;
|
|
88
|
+
for (let index = 0; index < text.length; index++) {
|
|
89
|
+
hash = (hash * 31 + text.charCodeAt(index)) >>> 0;
|
|
90
|
+
}
|
|
91
|
+
const preferred = `pending-steer-${hash.toString(36)}-${occurrence}`;
|
|
92
|
+
let id = preferred;
|
|
93
|
+
let suffix = 2;
|
|
94
|
+
while (usedIds.has(id)) {
|
|
95
|
+
id = `${preferred}-${suffix}`;
|
|
96
|
+
suffix++;
|
|
97
|
+
}
|
|
98
|
+
usedIds.add(id);
|
|
99
|
+
return id;
|
|
100
|
+
}
|
|
101
|
+
|
|
83
102
|
class FullToolPreview implements Component {
|
|
84
103
|
private scrollOffset = 0;
|
|
85
104
|
private pageSize = 1;
|
|
@@ -216,6 +235,7 @@ export class ConversationViewer implements Component {
|
|
|
216
235
|
private scrollOffset = 0;
|
|
217
236
|
private autoScroll = true;
|
|
218
237
|
private unsubscribe: (() => void) | undefined;
|
|
238
|
+
private pendingSteers: string[] = [];
|
|
219
239
|
private lastInnerW = 0;
|
|
220
240
|
private closed = false;
|
|
221
241
|
private stopArmed = false;
|
|
@@ -223,7 +243,7 @@ export class ConversationViewer implements Component {
|
|
|
223
243
|
private hoveredPreview = false;
|
|
224
244
|
private pressedHeaderAction: "preview" | "close" | undefined;
|
|
225
245
|
private keys: ViewerKeys;
|
|
226
|
-
private composer:
|
|
246
|
+
private composer: Editor | undefined;
|
|
227
247
|
private searchMode = false;
|
|
228
248
|
private searchQuery = "";
|
|
229
249
|
private searchMatches: ConversationMatch[] = [];
|
|
@@ -252,14 +272,18 @@ export class ConversationViewer implements Component {
|
|
|
252
272
|
private operations?: ConversationViewerOperations,
|
|
253
273
|
) {
|
|
254
274
|
this.keys = createViewerKeys(keybindings);
|
|
255
|
-
this.
|
|
275
|
+
this.pendingSteers = this.readPendingSteers();
|
|
276
|
+
this.blocks = this.formatBlocks();
|
|
256
277
|
this.timeline = new ConversationTimeline(tui, theme, {
|
|
257
278
|
cwd: operations?.ctx.cwd,
|
|
258
279
|
record,
|
|
259
280
|
onChange: (change) => this.handleTimelineChange(change),
|
|
260
281
|
});
|
|
261
282
|
this.timeline.setBlocks(this.blocks);
|
|
262
|
-
this.unsubscribe = session.subscribe(() =>
|
|
283
|
+
this.unsubscribe = session.subscribe((event) => {
|
|
284
|
+
if (event.type === "queue_update") this.pendingSteers = [...event.steering];
|
|
285
|
+
this.scheduleSessionRefresh();
|
|
286
|
+
});
|
|
263
287
|
}
|
|
264
288
|
|
|
265
289
|
handleInput(data: string): void {
|
|
@@ -270,10 +294,14 @@ export class ConversationViewer implements Component {
|
|
|
270
294
|
if (this.composer) {
|
|
271
295
|
if (matchesKey(data, Key.alt("up")) || data === "a-up" || data === "alt+up") {
|
|
272
296
|
this.recallSteerDraft();
|
|
297
|
+
} else if (matchesKey(data, Key.escape)) {
|
|
298
|
+
this.composer = undefined;
|
|
299
|
+
this.steerHistoryIndex = -1;
|
|
300
|
+
this.steerHistoryDraft = "";
|
|
273
301
|
} else {
|
|
274
|
-
const before = this.composer.
|
|
302
|
+
const before = this.composer.getText();
|
|
275
303
|
this.composer.handleInput(data);
|
|
276
|
-
if (this.composer && before !== this.composer.
|
|
304
|
+
if (this.composer && before !== this.composer.getText()) {
|
|
277
305
|
this.steerHistoryIndex = -1;
|
|
278
306
|
this.steerHistoryDraft = "";
|
|
279
307
|
}
|
|
@@ -562,8 +590,8 @@ export class ConversationViewer implements Component {
|
|
|
562
590
|
|
|
563
591
|
lines.push(hrMid);
|
|
564
592
|
if (this.composer) {
|
|
565
|
-
|
|
566
|
-
const hint = th.fg("dim", "Enter send · Esc cancel · Alt+Up recall");
|
|
593
|
+
for (const composerLine of this.composer.render(innerW)) lines.push(row(composerLine));
|
|
594
|
+
const hint = th.fg("dim", "Enter send · Ctrl+J newline · Esc cancel · Alt+Up recall");
|
|
567
595
|
const label = th.fg("accent", "✎ steer");
|
|
568
596
|
lines.push(row(label + " ".repeat(Math.max(1, innerW - visibleWidth(label) - visibleWidth(hint))) + hint));
|
|
569
597
|
} else if (this.searchMode) {
|
|
@@ -615,6 +643,31 @@ export class ConversationViewer implements Component {
|
|
|
615
643
|
this.cachedVersion = -1;
|
|
616
644
|
}
|
|
617
645
|
|
|
646
|
+
private readPendingSteers(): string[] {
|
|
647
|
+
return this.session.getSteeringMessages ? [...this.session.getSteeringMessages()] : [];
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
private formatBlocks(): ConversationBlock[] {
|
|
651
|
+
const blocks = formatConversationMessages(this.session.messages);
|
|
652
|
+
const usedIds = new Set(blocks.map((block) => block.id));
|
|
653
|
+
const occurrences = new Map<string, number>();
|
|
654
|
+
for (const text of this.pendingSteers) {
|
|
655
|
+
const occurrence = occurrences.get(text) ?? 0;
|
|
656
|
+
occurrences.set(text, occurrence + 1);
|
|
657
|
+
blocks.push({
|
|
658
|
+
id: pendingSteerBlockId(text, occurrence, usedIds),
|
|
659
|
+
kind: "text",
|
|
660
|
+
role: "user",
|
|
661
|
+
header: "User",
|
|
662
|
+
markdown: text,
|
|
663
|
+
copyText: text,
|
|
664
|
+
fullText: text,
|
|
665
|
+
pending: true,
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
return blocks;
|
|
669
|
+
}
|
|
670
|
+
|
|
618
671
|
private scheduleSessionRefresh(): void {
|
|
619
672
|
if (this.closed || this.sessionRefreshQueued) return;
|
|
620
673
|
this.sessionRefreshQueued = true;
|
|
@@ -622,7 +675,8 @@ export class ConversationViewer implements Component {
|
|
|
622
675
|
this.sessionRefreshQueued = false;
|
|
623
676
|
if (this.closed) return;
|
|
624
677
|
if (this.hasFocusedBlock) this.selectedBlockId = this.currentBlock()?.id;
|
|
625
|
-
this.
|
|
678
|
+
this.pendingSteers = this.readPendingSteers();
|
|
679
|
+
this.blocks = this.formatBlocks();
|
|
626
680
|
this.timeline.setBlocks(this.blocks);
|
|
627
681
|
this.cacheVersion++;
|
|
628
682
|
this.invalidate();
|
|
@@ -665,7 +719,8 @@ export class ConversationViewer implements Component {
|
|
|
665
719
|
}
|
|
666
720
|
|
|
667
721
|
private chromeLines(): number {
|
|
668
|
-
|
|
722
|
+
const composerLines = this.composer?.render(this.lastInnerW || 80).length ?? 0;
|
|
723
|
+
return CHROME_LINES_BASE + (this.invocationLine() ? 1 : 0) + composerLines;
|
|
669
724
|
}
|
|
670
725
|
|
|
671
726
|
private invocationLine(): string | undefined {
|
|
@@ -987,7 +1042,16 @@ export class ConversationViewer implements Component {
|
|
|
987
1042
|
}
|
|
988
1043
|
|
|
989
1044
|
private openComposer(): void {
|
|
990
|
-
const input = new
|
|
1045
|
+
const input = new Editor(this.tui, {
|
|
1046
|
+
borderColor: (text) => this.theme.fg("borderMuted", text),
|
|
1047
|
+
selectList: {
|
|
1048
|
+
selectedPrefix: (text) => this.theme.fg("accent", text),
|
|
1049
|
+
selectedText: (text) => this.theme.fg("accent", text),
|
|
1050
|
+
description: (text) => this.theme.fg("muted", text),
|
|
1051
|
+
scrollInfo: (text) => this.theme.fg("muted", text),
|
|
1052
|
+
noMatch: (text) => this.theme.fg("muted", text),
|
|
1053
|
+
},
|
|
1054
|
+
});
|
|
991
1055
|
input.focused = true;
|
|
992
1056
|
this.steerHistoryIndex = -1;
|
|
993
1057
|
this.steerHistoryDraft = "";
|
|
@@ -1003,25 +1067,31 @@ export class ConversationViewer implements Component {
|
|
|
1003
1067
|
}
|
|
1004
1068
|
this.tui.requestRender();
|
|
1005
1069
|
};
|
|
1006
|
-
input.onEscape = () => {
|
|
1007
|
-
this.composer = undefined;
|
|
1008
|
-
this.steerHistoryIndex = -1;
|
|
1009
|
-
this.steerHistoryDraft = "";
|
|
1010
|
-
this.tui.requestRender();
|
|
1011
|
-
};
|
|
1012
1070
|
this.composer = input;
|
|
1013
1071
|
this.tui.requestRender();
|
|
1014
1072
|
}
|
|
1015
1073
|
|
|
1016
|
-
/** Recall
|
|
1074
|
+
/** Recall queued messages before local history so an undelivered steer is never hidden. */
|
|
1017
1075
|
private recallSteerDraft(): void {
|
|
1018
|
-
if (!this.composer
|
|
1076
|
+
if (!this.composer) return;
|
|
1077
|
+
if (this.pendingSteers.length > 0) {
|
|
1078
|
+
if (!this.session.clearQueue) return;
|
|
1079
|
+
const { steering, followUp } = this.session.clearQueue();
|
|
1080
|
+
const queuedText = [...steering, ...followUp].join("\n");
|
|
1081
|
+
const currentText = this.composer.getText();
|
|
1082
|
+
this.composer.setText([queuedText, currentText].filter((text) => text.trim()).join("\n"));
|
|
1083
|
+
this.pendingSteers = [];
|
|
1084
|
+
this.steerHistoryIndex = -1;
|
|
1085
|
+
this.steerHistoryDraft = "";
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
if (this.steerHistory.length === 0) return;
|
|
1019
1089
|
if (this.steerHistoryIndex < 0) {
|
|
1020
|
-
this.steerHistoryDraft = this.composer.
|
|
1090
|
+
this.steerHistoryDraft = this.composer.getText();
|
|
1021
1091
|
this.steerHistoryIndex = 0;
|
|
1022
1092
|
} else if (this.steerHistoryIndex < this.steerHistory.length - 1) {
|
|
1023
1093
|
this.steerHistoryIndex++;
|
|
1024
1094
|
}
|
|
1025
|
-
this.composer.
|
|
1095
|
+
this.composer.setText(this.steerHistory[this.steerHistoryIndex] ?? this.steerHistoryDraft);
|
|
1026
1096
|
}
|
|
1027
1097
|
}
|