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 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.11.0...HEAD
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.0 or later, with the Pi session running in a tmux pane. That is the
20
- first version with `new-window -e`. A window is opened in the pane's
21
- session, so a client attached elsewhere does not matter.
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.11.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 { createTmuxClient, type TmuxClient, WindowOpenError } from "./tmux.ts";
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<string> {
661
- const { id, name, session, workspace, trusted, cwd, sessionDir, start, toolCallId, ctx, onWindowOpened } = input;
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 windowId: string;
726
+ let opened: OpenedWindow;
703
727
  try {
704
- windowId = await tmux.openWindow({ name, cwd, argv: payload.args, env: payload.env });
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 windowId;
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 } = input;
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
- 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> {
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(outcome.parsed.cwd, "--cwd");
988
- const worktree_source = normalizeWorkspaceValue(outcome.parsed.worktree_source, "--worktree-source");
989
- const worktree_base = normalizeWorkspaceValue(outcome.parsed.worktree_base, "--worktree-base");
990
- 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");
991
1023
  if (worktree_source !== undefined && worktree === undefined)
992
1024
  throw new Error("--worktree-source requires --worktree");
993
1025
  const start = await validate(
994
- { ...outcome.parsed, cwd, worktree, worktree_base, worktree_source },
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) => {