pi-squire 0.11.0 → 0.13.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 +42 -1
- package/README.md +94 -4
- package/package.json +3 -1
- package/src/index.ts +129 -35
- package/src/navigator.ts +382 -0
- package/src/new-delegate.ts +438 -0
- package/src/poll.ts +111 -0
- package/src/popups.ts +49 -0
- package/src/render.ts +2 -2
- package/src/resume-checks.ts +159 -0
- package/src/resume-picker.ts +50 -0
- package/src/resume.ts +69 -104
- package/src/tmux.ts +278 -38
- package/src/tracking.ts +76 -131
- package/src/window-placement.ts +31 -0
- package/src/windows.ts +24 -0
- package/src/workspace.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,45 @@ follows [Semantic Versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.13.0] - 2026-10-26
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `/delegates`, also on `alt+shift+d`, opens a panel of the delegations whose
|
|
14
|
+
window is open (newest first): `enter` opens one in a tmux popup, or switches
|
|
15
|
+
to it when it is in your own session; `p` peeks at its pane in an overlay (`r` refreshes,
|
|
16
|
+
`enter` opens); `s` switches your client to it; `x` closes it, asking only
|
|
17
|
+
while it runs; `n` opens the new-delegate form. `/delegate-open <id> [--popup|--switch|--peek]` does one of
|
|
18
|
+
these directly. Popup and switch act on the most recently active client of the
|
|
19
|
+
parent's session; the parent closes a popup once its delegate's window is gone.
|
|
20
|
+
Commands and a shortcut only: the model gets no new tool.
|
|
21
|
+
- `/delegate` with no arguments opens a new-delegate form: pick an agent, write
|
|
22
|
+
the task, name it, then a settings screen for model, thinking level (only the
|
|
23
|
+
levels the model supports, clamped, visibly, when the agent's default or a new model does not support it), auto-exit,
|
|
24
|
+
working directory and worktree. It launches through the same path as the
|
|
25
|
+
typed command. `/delegate-resume` with no arguments opens a picker of the
|
|
26
|
+
delegations that can be resumed, then an editor for the follow-up. Typed
|
|
27
|
+
commands and the tools are unchanged; the two commands no longer print
|
|
28
|
+
their usage when typed bare.
|
|
29
|
+
- `@earendil-works/pi-ai` is now a peer dependency (`*`), supplied by Pi.
|
|
30
|
+
|
|
31
|
+
## [0.12.0] - 2026-10-26
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- Delegates open in a companion tmux session, `<your session>-squire`, instead
|
|
36
|
+
of your own session, created on demand and removed by tmux with its last
|
|
37
|
+
window. pi-squire marks the session it creates (tmux session option
|
|
38
|
+
`@squire-companion`), so only a marked session counts as a companion: your
|
|
39
|
+
own session called `pi-squire` gets `pi-squire-squire`. Set
|
|
40
|
+
`"windowPlacement": "current"` in `<agent dir>/squire/settings.json` to open
|
|
41
|
+
them in your session as before; any other value refuses the launch.
|
|
42
|
+
The start message, `delegation_status` and the delegate card name the session,
|
|
43
|
+
and the `delegate` and `delegation_resume` results and the status details gain
|
|
44
|
+
an optional `windowSession`. `session:launch` is unchanged.
|
|
45
|
+
- tmux 3.2 or later is now required (it was 3.0); `/delegate`, `/delegate-resume`
|
|
46
|
+
and the tools refuse an older tmux with its version.
|
|
47
|
+
|
|
9
48
|
## [0.11.0] - 2026-10-26
|
|
10
49
|
|
|
11
50
|
### Added
|
|
@@ -474,7 +513,9 @@ author's dotfiles; the changes below are against that copy.
|
|
|
474
513
|
|
|
475
514
|
- The published file list includes `child.ts`, which `index.ts` imports.
|
|
476
515
|
|
|
477
|
-
[Unreleased]: https://github.com/gvanderclay/pi-squire/compare/v0.
|
|
516
|
+
[Unreleased]: https://github.com/gvanderclay/pi-squire/compare/v0.13.0...HEAD
|
|
517
|
+
[0.13.0]: https://github.com/gvanderclay/pi-squire/compare/v0.12.0...v0.13.0
|
|
518
|
+
[0.12.0]: https://github.com/gvanderclay/pi-squire/compare/v0.11.0...v0.12.0
|
|
478
519
|
[0.11.0]: https://github.com/gvanderclay/pi-squire/compare/v0.10.0...v0.11.0
|
|
479
520
|
[0.10.0]: https://github.com/gvanderclay/pi-squire/compare/v0.9.5...v0.10.0
|
|
480
521
|
[0.9.5]: https://github.com/gvanderclay/pi-squire/compare/v0.9.4...v0.9.5
|
package/README.md
CHANGED
|
@@ -16,9 +16,11 @@ You need:
|
|
|
16
16
|
options the parent guidance uses, and the extension APIs the package relies
|
|
17
17
|
on. Pi supplies the peer dependencies `@earendil-works/pi-coding-agent` and
|
|
18
18
|
`typebox`.
|
|
19
|
-
- tmux 3.
|
|
20
|
-
first version with `new-
|
|
21
|
-
|
|
19
|
+
- tmux 3.2 or later, with the Pi session running in a tmux pane. That is the
|
|
20
|
+
first version with `new-session -e` and `display-popup`. Windows are placed
|
|
21
|
+
relative to the pane's session (see [Where delegate windows
|
|
22
|
+
open](#where-delegate-windows-open)), so a client attached elsewhere does
|
|
23
|
+
not matter.
|
|
22
24
|
- A provider of `message:*`;
|
|
23
25
|
[pi-session-mail](https://github.com/gvanderclay/pi-session-mail) is one
|
|
24
26
|
example. Without a provider there is no way to send the task or get an
|
|
@@ -73,6 +75,20 @@ When the delegate finishes, its answer reaches the parent as one `[delegate]`
|
|
|
73
75
|
message that quotes the task (see [Results](#results)), and the delegate then
|
|
74
76
|
closes its own window ([auto-exit](#auto-exit)).
|
|
75
77
|
|
|
78
|
+
`/delegate` with no arguments opens a form instead: pick an agent, write the
|
|
79
|
+
task in the multi-line editor, give it an optional label (empty for the
|
|
80
|
+
default name), then a settings screen. Its first row, Start, launches; the rows
|
|
81
|
+
below it all begin at the agent's defaults: Model (a filterable list of the
|
|
82
|
+
models your Pi has credentials for), Thinking (only the levels the selected
|
|
83
|
+
model supports; a level the model does not support, whether from the agent's
|
|
84
|
+
default or after changing the model, moves to the nearest supported one and says so), Auto-exit, Working directory (your own, to change)
|
|
85
|
+
and Worktree (a new branch name, empty for none). A worktree and a changed
|
|
86
|
+
working directory exclude each other; the base, source and fallbacks stay
|
|
87
|
+
command-only. The form starts the delegate through the same path as the
|
|
88
|
+
command, in the background, with the same start message. The label and
|
|
89
|
+
directory are checked as `--label` and `--cwd` are, before anything is sent
|
|
90
|
+
or opened.
|
|
91
|
+
|
|
76
92
|
## How a delegation works
|
|
77
93
|
|
|
78
94
|
`/delegate <agent> [--model <provider/id>] [--thinking <level>] [--label <name>] [--cwd <dir> | --worktree <branch> [--worktree-source <dir>] [--worktree-base <commit-ish>]] [--auto-exit | --no-auto-exit] <task…>`
|
|
@@ -130,6 +146,67 @@ temporary folder, not as text on the command line.
|
|
|
130
146
|
missing. In print mode (`pi -p`) Pi does not show the refusal notices, so
|
|
131
147
|
the refusal is silent there.
|
|
132
148
|
|
|
149
|
+
## Where delegate windows open
|
|
150
|
+
|
|
151
|
+
By default a delegate's window opens in a **companion session** named after
|
|
152
|
+
yours with `-squire` added: from the session `work`, delegates go to
|
|
153
|
+
`work-squire`. Your own session stays as it is, even when its name already
|
|
154
|
+
ends in `-squire`: a session called `pi-squire` gets `pi-squire-squire`.
|
|
155
|
+
pi-squire marks each companion session it creates with the tmux session
|
|
156
|
+
option `@squire-companion`; a parent that runs in a marked session uses it, so
|
|
157
|
+
a delegate that delegates does not make `work-squire-squire`.
|
|
158
|
+
|
|
159
|
+
- The session is created on demand, by the launch that needs it, and tmux
|
|
160
|
+
removes it with its last window, so it does not outlive its delegates.
|
|
161
|
+
- `detach-on-destroy` is set to `off` (and the mark set) on that session only: if you are
|
|
162
|
+
looking at it when its last delegate exits, tmux moves you to your most
|
|
163
|
+
recent other session instead of detaching you. Your sessions and tmux's
|
|
164
|
+
global options are never changed.
|
|
165
|
+
- The start message, the delegate card and the `delegate` and
|
|
166
|
+
`delegation_resume` results show the session the window opened in, as the
|
|
167
|
+
optional `windowSession`; they keep that name. `delegation_status` looks it
|
|
168
|
+
up each time, so it follows a window you move to another session, which is
|
|
169
|
+
still found, closed and resumed.
|
|
170
|
+
|
|
171
|
+
To open windows in your own session, as before, set `windowPlacement` in
|
|
172
|
+
`<agent dir>/squire/settings.json`, beside `worktreeRoot`:
|
|
173
|
+
|
|
174
|
+
```json
|
|
175
|
+
{ "windowPlacement": "current" }
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
The values are `companion` (the default when the setting is absent) and
|
|
179
|
+
`current`. The file is read at each launch. Any other value refuses the launch
|
|
180
|
+
with an error naming the file, before the task is sent or a window opens.
|
|
181
|
+
|
|
182
|
+
## Reaching delegates
|
|
183
|
+
|
|
184
|
+
Delegate windows sit out of your way, so reach them from the parent. `/delegates`
|
|
185
|
+
(or `alt+shift+d`) opens a panel of the delegations whose window is open: running,
|
|
186
|
+
or done with auto-exit off, newest first. The model has no tool for any of this.
|
|
187
|
+
|
|
188
|
+
| Key | Does |
|
|
189
|
+
|---|---|
|
|
190
|
+
| `↑` / `↓` | select a delegation; `esc` leaves the panel |
|
|
191
|
+
| `enter` | open: a popup over your terminal, or a switch when the window is in the parent's own session |
|
|
192
|
+
| `p` | peek: a snapshot of the window's pane (`capture-pane`) in an overlay; `r` refreshes, `enter` opens, `esc` returns to the list |
|
|
193
|
+
| `s` | switch your tmux client to the window; `prefix L` returns you (tmux's last-session key) |
|
|
194
|
+
| `x` | close the delegation as `delegation_close` does; asks first only while it is running |
|
|
195
|
+
| `n` | new delegate: the same form `/delegate` opens with no arguments (see [First use](#first-use)) |
|
|
196
|
+
|
|
197
|
+
Popup and switch act on the most recently active tmux client attached to the
|
|
198
|
+
parent's session. With none attached they refuse with a notice; peek and close
|
|
199
|
+
still work. The popup attaches to the window's session at that window, and
|
|
200
|
+
detaching in it (`prefix d`) closes it. When the delegate's window goes, the parent
|
|
201
|
+
closes the popup within one poll (up to 5 s). No tmux key bindings or options are
|
|
202
|
+
added.
|
|
203
|
+
|
|
204
|
+
`/delegate-open <id> [--popup|--switch|--peek]` does one of these on one
|
|
205
|
+
delegation without the panel: with no flag or with `--popup`, what `enter` does (a popup, or a
|
|
206
|
+
switch for a window in the parent's own session, since a popup of one's own session would show it
|
|
207
|
+
inside itself); `--switch` always switches; `--peek` opens the peek overlay. Ids complete from the
|
|
208
|
+
open delegations.
|
|
209
|
+
|
|
133
210
|
## Working directory
|
|
134
211
|
|
|
135
212
|
`--cwd <dir>` (the `delegate` tool's `cwd`) runs the delegate in an existing
|
|
@@ -791,6 +868,15 @@ The model does the same with `delegation_resume` (`id` and `task`, optionally
|
|
|
791
868
|
`model`, `thinking` and `auto_exit`), which returns the `delegate` tool's result
|
|
792
869
|
plus `resumedFrom`. The command completes the ids that can still be resumed.
|
|
793
870
|
|
|
871
|
+
`/delegate-resume` with no arguments opens a picker instead: the delegations
|
|
872
|
+
that pass every check below (newest first, from one look at tmux), then the
|
|
873
|
+
multi-line editor for the follow-up. The checks change nothing: it refuses as the
|
|
874
|
+
command does when tmux is older than 3.2 or `windowPlacement` is invalid, and leaves out a
|
|
875
|
+
delegation whose recorded thinking level is invalid or whose model and fallbacks cannot be used
|
|
876
|
+
(credentials, usage limits). An exited window is left for the resume to close, and trust is
|
|
877
|
+
asked only when you resume. The model, thinking level and auto-exit are the delegation's own,
|
|
878
|
+
and it resumes as the command does.
|
|
879
|
+
|
|
794
880
|
- The delegate's own session file is reopened with `pi --session <absolute path>`
|
|
795
881
|
and the recorded `--session-dir`, in a new window named like the first. The
|
|
796
882
|
delegate keeps everything it read and decided; Pi appends to the same file.
|
|
@@ -958,7 +1044,11 @@ Other extensions may rely on:
|
|
|
958
1044
|
- the `delegate`, `delegation_status`, `delegation_close` and `delegation_resume` tool names,
|
|
959
1045
|
parameters, the recorded `workspace` shape and the `workspaceSnapshot`
|
|
960
1046
|
shape in results and status;
|
|
961
|
-
- the `/delegate` and `/delegate-resume` syntax and flags
|
|
1047
|
+
- the `/delegate` and `/delegate-resume` syntax and flags, and their
|
|
1048
|
+
no-argument forms: `/delegate` opens the new-delegate form and
|
|
1049
|
+
`/delegate-resume` the resume picker;
|
|
1050
|
+
- the `/delegates` command and its `alt+shift+d` shortcut, and `/delegate-open <id>` with its
|
|
1051
|
+
`--popup`, `--switch` and `--peek` flags;
|
|
962
1052
|
- the `[delegate]` result text and the closed-without-a-result text;
|
|
963
1053
|
- `AGENT.md`'s frontmatter keys and its location,
|
|
964
1054
|
`<agent dir>/squire/agents/<name>/AGENT.md`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-squire",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Starts a delegate Pi session in a background tmux window, sends it the task over message:*, and shows the parent its result",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -44,12 +44,14 @@
|
|
|
44
44
|
"access": "public"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
|
+
"@earendil-works/pi-ai": "*",
|
|
47
48
|
"@earendil-works/pi-coding-agent": ">=1.3.0",
|
|
48
49
|
"@earendil-works/pi-tui": "*",
|
|
49
50
|
"typebox": "*"
|
|
50
51
|
},
|
|
51
52
|
"devDependencies": {
|
|
52
53
|
"@biomejs/biome": "2.5.14",
|
|
54
|
+
"@earendil-works/pi-ai": "1.3.0",
|
|
53
55
|
"@earendil-works/pi-coding-agent": "1.3.0",
|
|
54
56
|
"@earendil-works/pi-tui": "1.3.0",
|
|
55
57
|
"@types/node": "26.6.3",
|
package/src/index.ts
CHANGED
|
@@ -53,12 +53,16 @@ import { Type } from "typebox";
|
|
|
53
53
|
import { type Agent, isThinking, type Roster, readRoster, THINKING_LEVELS } from "./agents.ts";
|
|
54
54
|
import { AGENT_ENV, AUTO_EXIT_ENV, FALLBACK_ENV, registerChild } from "./child.ts";
|
|
55
55
|
import { activeMark, clearMarks, isFree, readMarks } from "./limits.ts";
|
|
56
|
+
import { createNavigator, OPEN_USAGE, openCompletions, parseOpen } from "./navigator.ts";
|
|
57
|
+
import { type NewDelegateDeps, type NewRequest, startNewDelegate } from "./new-delegate.ts";
|
|
56
58
|
import { dirtyNotice, renderDelegateCall, renderDelegateResult, resourceLine } from "./render.ts";
|
|
57
59
|
import { createResults, delegateName, type WorkspaceAttempt } from "./results.ts";
|
|
58
60
|
import { createResume, type ResumeRequest, resumableIds, startedLine } from "./resume.ts";
|
|
59
|
-
import {
|
|
61
|
+
import { pickResume } from "./resume-picker.ts";
|
|
62
|
+
import { createTmuxClient, type OpenedWindow, requireTmuxVersion, type TmuxClient, WindowOpenError } from "./tmux.ts";
|
|
60
63
|
import { createTracking, toolResult } from "./tracking.ts";
|
|
61
64
|
import { createProactive, createUsageClient, type Proactive, type UsageClient } from "./usage.ts";
|
|
65
|
+
import { type WindowPlacement, windowPlacement } from "./window-placement.ts";
|
|
62
66
|
import {
|
|
63
67
|
normalizeWorkspaceValue,
|
|
64
68
|
prepareDirectory,
|
|
@@ -149,6 +153,9 @@ export const DelegateSchema = Type.Object({
|
|
|
149
153
|
autoExit: Type.Boolean(),
|
|
150
154
|
windowId: Type.String(),
|
|
151
155
|
windowName: Type.String(),
|
|
156
|
+
windowSession: Type.Optional(
|
|
157
|
+
Type.String({ description: "The tmux session the window opened in, as tmux named it then." }),
|
|
158
|
+
),
|
|
152
159
|
cwd: Type.String({ description: "The directory the delegate's window opened in." }),
|
|
153
160
|
sessionDir: Type.String({ description: "The session-storage directory the delegate was told to use." }),
|
|
154
161
|
workspace: WorkspaceSchema,
|
|
@@ -216,6 +223,8 @@ type Launched = {
|
|
|
216
223
|
name: string;
|
|
217
224
|
windowId: string;
|
|
218
225
|
windowName: string;
|
|
226
|
+
/** The session the window opened in; undefined when tmux could not say. */
|
|
227
|
+
windowSession: string | undefined;
|
|
219
228
|
requestId: string;
|
|
220
229
|
cwd: string;
|
|
221
230
|
sessionDir: string;
|
|
@@ -581,19 +590,19 @@ export default function delegate(
|
|
|
581
590
|
return roster;
|
|
582
591
|
}
|
|
583
592
|
|
|
593
|
+
/** The refusal for an empty roster. */
|
|
594
|
+
const noAgents = (): string =>
|
|
595
|
+
`no agents are defined; add ${join(
|
|
596
|
+
getAgentDir(),
|
|
597
|
+
"squire",
|
|
598
|
+
"agents",
|
|
599
|
+
"<name>",
|
|
600
|
+
"AGENT.md",
|
|
601
|
+
)} with frontmatter description, model and thinking (optionally auto-exit, exclude-tools, tools, extensions and fallback), and the prompt as its body`;
|
|
602
|
+
|
|
584
603
|
/** The request as a start, or an error naming what to fix. */
|
|
585
604
|
async function validate(request: Parsed, roster: Roster, models: ModelRegistry): Promise<Start> {
|
|
586
|
-
if (roster.agents.length === 0)
|
|
587
|
-
throw new Error(
|
|
588
|
-
`no agents are defined; add ${join(
|
|
589
|
-
getAgentDir(),
|
|
590
|
-
"squire",
|
|
591
|
-
"agents",
|
|
592
|
-
"<name>",
|
|
593
|
-
"AGENT.md",
|
|
594
|
-
)} with frontmatter description, model and thinking (optionally auto-exit, exclude-tools, tools, extensions and fallback), and the prompt as its body`,
|
|
595
|
-
);
|
|
596
|
-
}
|
|
605
|
+
if (roster.agents.length === 0) throw new Error(noAgents());
|
|
597
606
|
const agent = roster.agents.find((candidate) => candidate.name === request.agent);
|
|
598
607
|
if (agent === undefined) {
|
|
599
608
|
throw new Error(
|
|
@@ -640,7 +649,7 @@ export default function delegate(
|
|
|
640
649
|
|
|
641
650
|
/**
|
|
642
651
|
* Build the delegate's command line and environment, emit `session:launch`
|
|
643
|
-
* and open its window; returns the window id. A launch and a resume share
|
|
652
|
+
* and open its window; returns the window id and session. A launch and a resume share
|
|
644
653
|
* it and differ only in `session`, the arguments that choose the session:
|
|
645
654
|
* `--session-id <id> --name <name>` for a new one, `--session <path>` to reopen.
|
|
646
655
|
*/
|
|
@@ -656,9 +665,24 @@ export default function delegate(
|
|
|
656
665
|
/** The tool call that started this launch or resume; undefined from a command. */
|
|
657
666
|
toolCallId: string | undefined;
|
|
658
667
|
ctx: ExtensionContext;
|
|
668
|
+
/** Chosen once, when the launch or resume began, so no later settings change can refuse it after mail. */
|
|
669
|
+
placement: WindowPlacement;
|
|
659
670
|
onWindowOpened?: () => void;
|
|
660
|
-
}): Promise<
|
|
661
|
-
const {
|
|
671
|
+
}): Promise<OpenedWindow> {
|
|
672
|
+
const {
|
|
673
|
+
id,
|
|
674
|
+
name,
|
|
675
|
+
session,
|
|
676
|
+
workspace,
|
|
677
|
+
trusted,
|
|
678
|
+
cwd,
|
|
679
|
+
sessionDir,
|
|
680
|
+
start,
|
|
681
|
+
toolCallId,
|
|
682
|
+
ctx,
|
|
683
|
+
placement,
|
|
684
|
+
onWindowOpened,
|
|
685
|
+
} = input;
|
|
662
686
|
const parent = ctx.sessionManager.getSessionId();
|
|
663
687
|
const managedContext =
|
|
664
688
|
workspace.mode === "managed"
|
|
@@ -699,9 +723,9 @@ export default function delegate(
|
|
|
699
723
|
// Listeners may only append to `args` and add to `env`; there is no veto.
|
|
700
724
|
const payload: LaunchPayload = { args: argv, env, agent: start.agent.name, cwd, toolCallId };
|
|
701
725
|
pi.events.emit(LAUNCH, payload);
|
|
702
|
-
let
|
|
726
|
+
let opened: OpenedWindow;
|
|
703
727
|
try {
|
|
704
|
-
|
|
728
|
+
opened = await tmux.openWindow({ name, cwd, argv: payload.args, env: payload.env }, placement);
|
|
705
729
|
onWindowOpened?.();
|
|
706
730
|
} catch (err) {
|
|
707
731
|
if (err instanceof WindowOpenError) {
|
|
@@ -709,7 +733,7 @@ export default function delegate(
|
|
|
709
733
|
}
|
|
710
734
|
throw new WindowOpenError(`could not open window ${name} for ${id}: ${(err as Error).message}`, "unknown");
|
|
711
735
|
}
|
|
712
|
-
return
|
|
736
|
+
return opened;
|
|
713
737
|
}
|
|
714
738
|
|
|
715
739
|
async function launchWindow(input: {
|
|
@@ -722,11 +746,13 @@ export default function delegate(
|
|
|
722
746
|
start: Start;
|
|
723
747
|
toolCallId: string | undefined;
|
|
724
748
|
ctx: ExtensionContext;
|
|
749
|
+
placement: WindowPlacement;
|
|
725
750
|
onWindowOpened: () => void;
|
|
726
751
|
}): Promise<Launched> {
|
|
727
|
-
const { id, requestId, workspace, trusted, cwd, sessionDir, start, toolCallId, ctx, onWindowOpened } =
|
|
752
|
+
const { id, requestId, workspace, trusted, cwd, sessionDir, start, toolCallId, ctx, placement, onWindowOpened } =
|
|
753
|
+
input;
|
|
728
754
|
const name = delegateName(start.agent.name, id, start.label);
|
|
729
|
-
const windowId = await openChild({
|
|
755
|
+
const { id: windowId, session: windowSession } = await openChild({
|
|
730
756
|
id,
|
|
731
757
|
name,
|
|
732
758
|
session: ["--session-id", id, "--name", name],
|
|
@@ -737,6 +763,7 @@ export default function delegate(
|
|
|
737
763
|
start,
|
|
738
764
|
toolCallId,
|
|
739
765
|
ctx,
|
|
766
|
+
placement,
|
|
740
767
|
onWindowOpened,
|
|
741
768
|
});
|
|
742
769
|
// No task text in the record: the request copy in `sent/` has it.
|
|
@@ -758,7 +785,7 @@ export default function delegate(
|
|
|
758
785
|
ctx,
|
|
759
786
|
);
|
|
760
787
|
tracking.startPolling();
|
|
761
|
-
return { id, name, windowId, windowName: name, requestId, cwd, sessionDir, workspace };
|
|
788
|
+
return { id, name, windowId, windowName: name, windowSession, requestId, cwd, sessionDir, workspace };
|
|
762
789
|
}
|
|
763
790
|
|
|
764
791
|
async function launch(
|
|
@@ -768,6 +795,9 @@ export default function delegate(
|
|
|
768
795
|
toolCallId?: string,
|
|
769
796
|
): Promise<Launched> {
|
|
770
797
|
if (!tmux.insideTmux()) throw new Error(NOT_IN_TMUX);
|
|
798
|
+
await requireTmuxVersion(tmux);
|
|
799
|
+
// Read once, before anything is created or mailed; the launch keeps this value.
|
|
800
|
+
const placement = windowPlacement(getAgentDir());
|
|
771
801
|
const sessionDir = ctx.sessionManager.getSessionDir();
|
|
772
802
|
if (!isAbsolute(sessionDir))
|
|
773
803
|
throw new Error(
|
|
@@ -878,6 +908,7 @@ export default function delegate(
|
|
|
878
908
|
start,
|
|
879
909
|
toolCallId,
|
|
880
910
|
ctx,
|
|
911
|
+
placement,
|
|
881
912
|
onWindowOpened: () => {
|
|
882
913
|
windowOpened = true;
|
|
883
914
|
},
|
|
@@ -949,12 +980,13 @@ export default function delegate(
|
|
|
949
980
|
delegateSchema: DelegateSchema,
|
|
950
981
|
notInTmux: NOT_IN_TMUX,
|
|
951
982
|
startPolling: tracking.startPolling,
|
|
983
|
+
placement: () => windowPlacement(getAgentDir()),
|
|
952
984
|
roster: rosterFor,
|
|
953
985
|
chooseModel: (explicit, agent, models, preferred) => chooseModel(explicit, agent, models, proactive, preferred),
|
|
954
986
|
thinkingProblem,
|
|
955
987
|
prepareDirectory: (cwd, ctx, signal) => prepareDirectory({ cwd, signal }, ctx, getAgentDir()),
|
|
956
988
|
sendTask,
|
|
957
|
-
open: ({ id, name, sessionPath, workspace, trusted, cwd, sessionDir, start, toolCallId, ctx }) =>
|
|
989
|
+
open: ({ id, name, sessionPath, workspace, trusted, cwd, sessionDir, start, toolCallId, ctx, placement }) =>
|
|
958
990
|
openChild({
|
|
959
991
|
id,
|
|
960
992
|
name,
|
|
@@ -966,32 +998,32 @@ export default function delegate(
|
|
|
966
998
|
start,
|
|
967
999
|
toolCallId,
|
|
968
1000
|
ctx,
|
|
1001
|
+
placement,
|
|
969
1002
|
}),
|
|
970
1003
|
});
|
|
971
1004
|
|
|
1005
|
+
/** The session a window opened in, for the start text. */
|
|
1006
|
+
const inSession = (session: string | undefined): string => (session === undefined ? "" : ` in session ${session}`);
|
|
1007
|
+
|
|
972
1008
|
/** Where a selected directory put the delegate, for the start text; inherited starts stay compact. */
|
|
973
1009
|
const placedIn = (launched: Launched): string =>
|
|
974
1010
|
launched.workspace.mode !== "inherited" ? ` in ${launched.cwd}` : "";
|
|
975
1011
|
|
|
976
1012
|
const started = (launched: Launched, start: Start): string =>
|
|
977
|
-
`delegate: ${start.agent.name} ${launched.id} started in window ${launched.windowName}${placedIn(launched)}${launched.workspace.mode === "managed" ? `. Branch ${launched.workspace.createdBranch} starts at ${launched.workspace.baseCommit}. ${dirtyNotice(launched.workspace.sourceDirty)}` : ""}${start.skipped.length > 0 ? `. ${fallbackNote(start)}` : ""}${start.notices.map((line) => `. ${line}`).join("")}`;
|
|
1013
|
+
`delegate: ${start.agent.name} ${launched.id} started in window ${launched.windowName}${inSession(launched.windowSession)}${placedIn(launched)}${launched.workspace.mode === "managed" ? `. Branch ${launched.workspace.createdBranch} starts at ${launched.workspace.baseCommit}. ${dirtyNotice(launched.workspace.sourceDirty)}` : ""}${start.skipped.length > 0 ? `. ${fallbackNote(start)}` : ""}${start.notices.map((line) => `. ${line}`).join("")}`;
|
|
978
1014
|
|
|
979
|
-
|
|
1015
|
+
/** Start a delegate from a parsed `/delegate` or the form, telling the user how it went. */
|
|
1016
|
+
async function runDelegate(parsed: Parsed, ctx: ExtensionContext): Promise<void> {
|
|
980
1017
|
const fail = (message: string) => ctx.ui.notify(`delegate: ${message}`, "error");
|
|
981
|
-
const outcome = parse(input);
|
|
982
|
-
if ("error" in outcome) {
|
|
983
|
-
fail(outcome.error);
|
|
984
|
-
return;
|
|
985
|
-
}
|
|
986
1018
|
try {
|
|
987
|
-
const cwd = normalizeWorkspaceValue(
|
|
988
|
-
const worktree_source = normalizeWorkspaceValue(
|
|
989
|
-
const worktree_base = normalizeWorkspaceValue(
|
|
990
|
-
const worktree = normalizeWorkspaceValue(
|
|
1019
|
+
const cwd = normalizeWorkspaceValue(parsed.cwd, "--cwd");
|
|
1020
|
+
const worktree_source = normalizeWorkspaceValue(parsed.worktree_source, "--worktree-source");
|
|
1021
|
+
const worktree_base = normalizeWorkspaceValue(parsed.worktree_base, "--worktree-base");
|
|
1022
|
+
const worktree = normalizeWorkspaceValue(parsed.worktree, "--worktree");
|
|
991
1023
|
if (worktree_source !== undefined && worktree === undefined)
|
|
992
1024
|
throw new Error("--worktree-source requires --worktree");
|
|
993
1025
|
const start = await validate(
|
|
994
|
-
{ ...
|
|
1026
|
+
{ ...parsed, cwd, worktree, worktree_base, worktree_source },
|
|
995
1027
|
rosterFor(ctx),
|
|
996
1028
|
ctx.modelRegistry,
|
|
997
1029
|
);
|
|
@@ -1002,7 +1034,44 @@ export default function delegate(
|
|
|
1002
1034
|
}
|
|
1003
1035
|
}
|
|
1004
1036
|
|
|
1037
|
+
/** What the new-delegate form needs, shared by `/delegate` and the panel's `n`. */
|
|
1038
|
+
const newDelegateDeps: NewDelegateDeps = {
|
|
1039
|
+
unavailable: () => (tmux.insideTmux() ? undefined : NOT_IN_TMUX),
|
|
1040
|
+
roster: rosterFor,
|
|
1041
|
+
noAgents,
|
|
1042
|
+
labelProblem,
|
|
1043
|
+
start: (request: NewRequest, context) => runDelegate(request, context),
|
|
1044
|
+
};
|
|
1045
|
+
|
|
1046
|
+
/** `/delegate`: with no arguments the new-delegate form, otherwise the typed request. */
|
|
1047
|
+
async function command(input: string, ctx: ExtensionContext): Promise<void> {
|
|
1048
|
+
if (input.trim() === "") {
|
|
1049
|
+
await startNewDelegate(ctx, newDelegateDeps);
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
const outcome = parse(input);
|
|
1053
|
+
if ("error" in outcome) {
|
|
1054
|
+
ctx.ui.notify(`delegate: ${outcome.error}`, "error");
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
await runDelegate(outcome.parsed, ctx);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1005
1060
|
async function resumeCommand(input: string, ctx: ExtensionContext): Promise<void> {
|
|
1061
|
+
if (input.trim() === "") {
|
|
1062
|
+
await pickResume(ctx, {
|
|
1063
|
+
unavailable: () => (tmux.insideTmux() ? undefined : NOT_IN_TMUX),
|
|
1064
|
+
eligible: resumer.eligible,
|
|
1065
|
+
resume: async (request, context) => {
|
|
1066
|
+
try {
|
|
1067
|
+
context.ui.notify(startedLine(await resumer.resume(request, context)), "info");
|
|
1068
|
+
} catch (err) {
|
|
1069
|
+
context.ui.notify(`delegate: ${(err as Error).message}`, "error");
|
|
1070
|
+
}
|
|
1071
|
+
},
|
|
1072
|
+
});
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1006
1075
|
const outcome = parseResume(input);
|
|
1007
1076
|
if ("error" in outcome) {
|
|
1008
1077
|
ctx.ui.notify(`delegate: ${outcome.error}`, "error");
|
|
@@ -1115,7 +1184,7 @@ export default function delegate(
|
|
|
1115
1184
|
const launched = await launch(start, ctx, signal, toolCallId);
|
|
1116
1185
|
ctx.ui.notify(started(launched, start), "info");
|
|
1117
1186
|
return toolResult(
|
|
1118
|
-
`Started delegation ${launched.id}: ${start.agent.name} (${start.model}, thinking ${start.thinking}) in window ${launched.windowName}${placedIn(launched)}.${launched.workspace.mode === "managed" ? ` Branch ${launched.workspace.createdBranch} starts at ${launched.workspace.baseCommit}. ${dirtyNotice(launched.workspace.sourceDirty)}` : ""} It runs in the background, and its result arrives as a message that starts your next turn on its own; delegation_close ends it.${start.skipped.length > 0 ? ` ${fallbackNote(start)}` : ""}${start.notices.map((line) => ` ${line}`).join("")}`,
|
|
1187
|
+
`Started delegation ${launched.id}: ${start.agent.name} (${start.model}, thinking ${start.thinking}) in window ${launched.windowName}${inSession(launched.windowSession)}${placedIn(launched)}.${launched.workspace.mode === "managed" ? ` Branch ${launched.workspace.createdBranch} starts at ${launched.workspace.baseCommit}. ${dirtyNotice(launched.workspace.sourceDirty)}` : ""} It runs in the background, and its result arrives as a message that starts your next turn on its own; delegation_close ends it.${start.skipped.length > 0 ? ` ${fallbackNote(start)}` : ""}${start.notices.map((line) => ` ${line}`).join("")}`,
|
|
1119
1188
|
{
|
|
1120
1189
|
outcome: "started",
|
|
1121
1190
|
id: launched.id,
|
|
@@ -1127,6 +1196,7 @@ export default function delegate(
|
|
|
1127
1196
|
autoExit: start.autoExit,
|
|
1128
1197
|
windowId: launched.windowId,
|
|
1129
1198
|
windowName: launched.windowName,
|
|
1199
|
+
...(launched.windowSession === undefined ? {} : { windowSession: launched.windowSession }),
|
|
1130
1200
|
cwd: launched.cwd,
|
|
1131
1201
|
sessionDir: launched.sessionDir,
|
|
1132
1202
|
workspace: launched.workspace,
|
|
@@ -1209,6 +1279,30 @@ export default function delegate(
|
|
|
1209
1279
|
handler: resumeCommand,
|
|
1210
1280
|
});
|
|
1211
1281
|
|
|
1282
|
+
const navigator = createNavigator(results, tmux, tracking, (ctx: ExtensionContext) =>
|
|
1283
|
+
startNewDelegate(ctx, newDelegateDeps),
|
|
1284
|
+
);
|
|
1285
|
+
|
|
1286
|
+
pi.registerCommand("delegates", {
|
|
1287
|
+
description: "Open the panel of delegates whose window is open: popup, peek, switch, close",
|
|
1288
|
+
handler: async (_args: string, ctx: ExtensionContext) => navigator.panel(ctx),
|
|
1289
|
+
});
|
|
1290
|
+
|
|
1291
|
+
pi.registerShortcut("alt+shift+d", {
|
|
1292
|
+
description: "Open the delegates panel",
|
|
1293
|
+
handler: (ctx: ExtensionContext) => navigator.panel(ctx),
|
|
1294
|
+
});
|
|
1295
|
+
|
|
1296
|
+
pi.registerCommand("delegate-open", {
|
|
1297
|
+
description: `Reach one open delegate: ${OPEN_USAGE}`,
|
|
1298
|
+
getArgumentCompletions: async (prefix: string) => openCompletions(prefix, await navigator.openIds()),
|
|
1299
|
+
handler: async (args: string, ctx: ExtensionContext) => {
|
|
1300
|
+
const parsed = parseOpen(args);
|
|
1301
|
+
if ("error" in parsed) return ctx.ui.notify(`delegate: ${parsed.error}`, "error");
|
|
1302
|
+
await navigator.openDelegate(ctx, parsed.id, parsed.mode);
|
|
1303
|
+
},
|
|
1304
|
+
});
|
|
1305
|
+
|
|
1212
1306
|
pi.registerCommand("delegate-clear", {
|
|
1213
1307
|
description: "Clear usage-limit marks: all, a provider, or a provider/model",
|
|
1214
1308
|
getArgumentCompletions: (prefix: string) => {
|