pi-squire 0.12.0 → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -1
- package/README.md +57 -1
- package/package.json +3 -1
- package/src/index.ts +82 -22
- 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/resume-checks.ts +159 -0
- package/src/resume-picker.ts +50 -0
- package/src/resume.ts +39 -98
- package/src/tmux.ts +128 -1
- package/src/tracking.ts +57 -127
- package/src/windows.ts +24 -0
- package/src/workspace.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,35 @@ follows [Semantic Versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.13.1] - 2026-10-27
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- A delegate's popup title names the key that closes it, with your tmux prefix,
|
|
14
|
+
for example `scout-first · C-b d to close`.
|
|
15
|
+
|
|
16
|
+
## [0.13.0] - 2026-10-26
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- `/delegates`, also on `alt+shift+d`, opens a panel of the delegations whose
|
|
21
|
+
window is open (newest first): `enter` opens one in a tmux popup, or switches
|
|
22
|
+
to it when it is in your own session; `p` peeks at its pane in an overlay (`r` refreshes,
|
|
23
|
+
`enter` opens); `s` switches your client to it; `x` closes it, asking only
|
|
24
|
+
while it runs; `n` opens the new-delegate form. `/delegate-open <id> [--popup|--switch|--peek]` does one of
|
|
25
|
+
these directly. Popup and switch act on the most recently active client of the
|
|
26
|
+
parent's session; the parent closes a popup once its delegate's window is gone.
|
|
27
|
+
Commands and a shortcut only: the model gets no new tool.
|
|
28
|
+
- `/delegate` with no arguments opens a new-delegate form: pick an agent, write
|
|
29
|
+
the task, name it, then a settings screen for model, thinking level (only the
|
|
30
|
+
levels the model supports, clamped, visibly, when the agent's default or a new model does not support it), auto-exit,
|
|
31
|
+
working directory and worktree. It launches through the same path as the
|
|
32
|
+
typed command. `/delegate-resume` with no arguments opens a picker of the
|
|
33
|
+
delegations that can be resumed, then an editor for the follow-up. Typed
|
|
34
|
+
commands and the tools are unchanged; the two commands no longer print
|
|
35
|
+
their usage when typed bare.
|
|
36
|
+
- `@earendil-works/pi-ai` is now a peer dependency (`*`), supplied by Pi.
|
|
37
|
+
|
|
9
38
|
## [0.12.0] - 2026-10-26
|
|
10
39
|
|
|
11
40
|
### Changed
|
|
@@ -491,7 +520,9 @@ author's dotfiles; the changes below are against that copy.
|
|
|
491
520
|
|
|
492
521
|
- The published file list includes `child.ts`, which `index.ts` imports.
|
|
493
522
|
|
|
494
|
-
[Unreleased]: https://github.com/gvanderclay/pi-squire/compare/v0.
|
|
523
|
+
[Unreleased]: https://github.com/gvanderclay/pi-squire/compare/v0.13.1...HEAD
|
|
524
|
+
[0.13.1]: https://github.com/gvanderclay/pi-squire/compare/v0.13.0...v0.13.1
|
|
525
|
+
[0.13.0]: https://github.com/gvanderclay/pi-squire/compare/v0.12.0...v0.13.0
|
|
495
526
|
[0.12.0]: https://github.com/gvanderclay/pi-squire/compare/v0.11.0...v0.12.0
|
|
496
527
|
[0.11.0]: https://github.com/gvanderclay/pi-squire/compare/v0.10.0...v0.11.0
|
|
497
528
|
[0.10.0]: https://github.com/gvanderclay/pi-squire/compare/v0.9.5...v0.10.0
|
package/README.md
CHANGED
|
@@ -75,6 +75,20 @@ When the delegate finishes, its answer reaches the parent as one `[delegate]`
|
|
|
75
75
|
message that quotes the task (see [Results](#results)), and the delegate then
|
|
76
76
|
closes its own window ([auto-exit](#auto-exit)).
|
|
77
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
|
+
|
|
78
92
|
## How a delegation works
|
|
79
93
|
|
|
80
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…>`
|
|
@@ -165,6 +179,35 @@ The values are `companion` (the default when the setting is absent) and
|
|
|
165
179
|
`current`. The file is read at each launch. Any other value refuses the launch
|
|
166
180
|
with an error naming the file, before the task is sent or a window opens.
|
|
167
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; its title names that key with your
|
|
201
|
+
prefix. When the delegate's window goes, the parent
|
|
202
|
+
closes the popup within one poll (up to 5 s). No tmux key bindings or options are
|
|
203
|
+
added.
|
|
204
|
+
|
|
205
|
+
`/delegate-open <id> [--popup|--switch|--peek]` does one of these on one
|
|
206
|
+
delegation without the panel: with no flag or with `--popup`, what `enter` does (a popup, or a
|
|
207
|
+
switch for a window in the parent's own session, since a popup of one's own session would show it
|
|
208
|
+
inside itself); `--switch` always switches; `--peek` opens the peek overlay. Ids complete from the
|
|
209
|
+
open delegations.
|
|
210
|
+
|
|
168
211
|
## Working directory
|
|
169
212
|
|
|
170
213
|
`--cwd <dir>` (the `delegate` tool's `cwd`) runs the delegate in an existing
|
|
@@ -826,6 +869,15 @@ The model does the same with `delegation_resume` (`id` and `task`, optionally
|
|
|
826
869
|
`model`, `thinking` and `auto_exit`), which returns the `delegate` tool's result
|
|
827
870
|
plus `resumedFrom`. The command completes the ids that can still be resumed.
|
|
828
871
|
|
|
872
|
+
`/delegate-resume` with no arguments opens a picker instead: the delegations
|
|
873
|
+
that pass every check below (newest first, from one look at tmux), then the
|
|
874
|
+
multi-line editor for the follow-up. The checks change nothing: it refuses as the
|
|
875
|
+
command does when tmux is older than 3.2 or `windowPlacement` is invalid, and leaves out a
|
|
876
|
+
delegation whose recorded thinking level is invalid or whose model and fallbacks cannot be used
|
|
877
|
+
(credentials, usage limits). An exited window is left for the resume to close, and trust is
|
|
878
|
+
asked only when you resume. The model, thinking level and auto-exit are the delegation's own,
|
|
879
|
+
and it resumes as the command does.
|
|
880
|
+
|
|
829
881
|
- The delegate's own session file is reopened with `pi --session <absolute path>`
|
|
830
882
|
and the recorded `--session-dir`, in a new window named like the first. The
|
|
831
883
|
delegate keeps everything it read and decided; Pi appends to the same file.
|
|
@@ -993,7 +1045,11 @@ Other extensions may rely on:
|
|
|
993
1045
|
- the `delegate`, `delegation_status`, `delegation_close` and `delegation_resume` tool names,
|
|
994
1046
|
parameters, the recorded `workspace` shape and the `workspaceSnapshot`
|
|
995
1047
|
shape in results and status;
|
|
996
|
-
- the `/delegate` and `/delegate-resume` syntax and flags
|
|
1048
|
+
- the `/delegate` and `/delegate-resume` syntax and flags, and their
|
|
1049
|
+
no-argument forms: `/delegate` opens the new-delegate form and
|
|
1050
|
+
`/delegate-resume` the resume picker;
|
|
1051
|
+
- the `/delegates` command and its `alt+shift+d` shortcut, and `/delegate-open <id>` with its
|
|
1052
|
+
`--popup`, `--switch` and `--peek` flags;
|
|
997
1053
|
- the `[delegate]` result text and the closed-without-a-result text;
|
|
998
1054
|
- `AGENT.md`'s frontmatter keys and its location,
|
|
999
1055
|
`<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.1",
|
|
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,9 +53,12 @@ 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";
|
|
61
|
+
import { pickResume } from "./resume-picker.ts";
|
|
59
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";
|
|
@@ -587,19 +590,19 @@ export default function delegate(
|
|
|
587
590
|
return roster;
|
|
588
591
|
}
|
|
589
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
|
+
|
|
590
603
|
/** The request as a start, or an error naming what to fix. */
|
|
591
604
|
async function validate(request: Parsed, roster: Roster, models: ModelRegistry): Promise<Start> {
|
|
592
|
-
if (roster.agents.length === 0)
|
|
593
|
-
throw new Error(
|
|
594
|
-
`no agents are defined; add ${join(
|
|
595
|
-
getAgentDir(),
|
|
596
|
-
"squire",
|
|
597
|
-
"agents",
|
|
598
|
-
"<name>",
|
|
599
|
-
"AGENT.md",
|
|
600
|
-
)} with frontmatter description, model and thinking (optionally auto-exit, exclude-tools, tools, extensions and fallback), and the prompt as its body`,
|
|
601
|
-
);
|
|
602
|
-
}
|
|
605
|
+
if (roster.agents.length === 0) throw new Error(noAgents());
|
|
603
606
|
const agent = roster.agents.find((candidate) => candidate.name === request.agent);
|
|
604
607
|
if (agent === undefined) {
|
|
605
608
|
throw new Error(
|
|
@@ -1009,22 +1012,18 @@ export default function delegate(
|
|
|
1009
1012
|
const started = (launched: Launched, start: Start): string =>
|
|
1010
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("")}`;
|
|
1011
1014
|
|
|
1012
|
-
|
|
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> {
|
|
1013
1017
|
const fail = (message: string) => ctx.ui.notify(`delegate: ${message}`, "error");
|
|
1014
|
-
const outcome = parse(input);
|
|
1015
|
-
if ("error" in outcome) {
|
|
1016
|
-
fail(outcome.error);
|
|
1017
|
-
return;
|
|
1018
|
-
}
|
|
1019
1018
|
try {
|
|
1020
|
-
const cwd = normalizeWorkspaceValue(
|
|
1021
|
-
const worktree_source = normalizeWorkspaceValue(
|
|
1022
|
-
const worktree_base = normalizeWorkspaceValue(
|
|
1023
|
-
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");
|
|
1024
1023
|
if (worktree_source !== undefined && worktree === undefined)
|
|
1025
1024
|
throw new Error("--worktree-source requires --worktree");
|
|
1026
1025
|
const start = await validate(
|
|
1027
|
-
{ ...
|
|
1026
|
+
{ ...parsed, cwd, worktree, worktree_base, worktree_source },
|
|
1028
1027
|
rosterFor(ctx),
|
|
1029
1028
|
ctx.modelRegistry,
|
|
1030
1029
|
);
|
|
@@ -1035,7 +1034,44 @@ export default function delegate(
|
|
|
1035
1034
|
}
|
|
1036
1035
|
}
|
|
1037
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
|
+
|
|
1038
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
|
+
}
|
|
1039
1075
|
const outcome = parseResume(input);
|
|
1040
1076
|
if ("error" in outcome) {
|
|
1041
1077
|
ctx.ui.notify(`delegate: ${outcome.error}`, "error");
|
|
@@ -1243,6 +1279,30 @@ export default function delegate(
|
|
|
1243
1279
|
handler: resumeCommand,
|
|
1244
1280
|
});
|
|
1245
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
|
+
|
|
1246
1306
|
pi.registerCommand("delegate-clear", {
|
|
1247
1307
|
description: "Clear usage-limit marks: all, a provider, or a provider/model",
|
|
1248
1308
|
getArgumentCompletions: (prefix: string) => {
|