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 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.12.0...HEAD
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.12.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
- async function command(input: string, ctx: ExtensionContext): Promise<void> {
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(outcome.parsed.cwd, "--cwd");
1021
- const worktree_source = normalizeWorkspaceValue(outcome.parsed.worktree_source, "--worktree-source");
1022
- const worktree_base = normalizeWorkspaceValue(outcome.parsed.worktree_base, "--worktree-base");
1023
- const worktree = normalizeWorkspaceValue(outcome.parsed.worktree, "--worktree");
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
- { ...outcome.parsed, cwd, worktree, worktree_base, worktree_source },
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) => {