pi-sessions 0.18.1 → 0.19.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/README.md +43 -39
- package/extensions/hosts/contract.ts +10 -46
- package/extensions/hosts/registry.ts +4 -78
- package/extensions/pi-sessions.ts +27 -53
- package/extensions/session-handoff/board-loading.ts +4 -1
- package/extensions/session-handoff/board-view-model.ts +1 -0
- package/extensions/session-handoff/board.ts +4 -1
- package/extensions/session-handoff/install.ts +47 -33
- package/extensions/session-handoff/launch/backend.ts +16 -4
- package/extensions/session-handoff/launch-options.ts +2 -17
- package/extensions/session-handoff/launch-target.ts +4 -9
- package/extensions/session-handoff/receipt.ts +1 -1
- package/extensions/session-handoff/tool-presenter.ts +1 -1
- package/extensions/session-handoff/tool-schema.ts +1 -9
- package/extensions/session-handoff/tool-view-model.ts +6 -2
- package/extensions/session-handoff/tool.ts +2 -2
- package/extensions/session-messaging/directory-list.ts +115 -0
- package/extensions/session-messaging/directory.ts +114 -0
- package/extensions/session-messaging/message-router.ts +3 -8
- package/extensions/session-messaging/pi/directory-tools.ts +41 -0
- package/extensions/session-messaging/pi/install.ts +102 -0
- package/extensions/session-messaging/pi/message-contracts.ts +4 -6
- package/extensions/session-messaging/pi/reachable-contract.ts +3 -4
- package/extensions/session-messaging/pi/reachable-tool.ts +25 -175
- package/extensions/session-messaging/pi/send-message-presenter.ts +2 -1
- package/extensions/session-messaging/pi/tools.ts +49 -91
- package/extensions/session-search/extract.ts +12 -2
- package/extensions/shared/lineage.ts +8 -0
- package/extensions/shared/session-index/sync.ts +3 -0
- package/extensions/subagents/install.ts +0 -27
- package/extensions/subagents/launch-target.ts +0 -1
- package/extensions/subagents/reconcile.ts +10 -2
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -68,8 +68,9 @@ Open the frontend implementation task in a session to the right.
|
|
|
68
68
|
| ------------------ | ---------------------------------------------------------------------- | ------------------------------------------------------- |
|
|
69
69
|
| Session Search | `session_search` pi tool | Search through old sessions |
|
|
70
70
|
| Session Ask | `session_ask` pi tool | Ask questions about old sessions |
|
|
71
|
-
| Session Handoff | `session_handoff` pi tool, `/handoff` board | Start
|
|
72
|
-
| Session Messaging | `session_reachable`, `session_send_message`, `session_cancel` pi tools | Coordinate between live Pi sessions
|
|
71
|
+
| Session Handoff | `session_handoff` pi tool, `/handoff` board | Start user-facing child sessions |
|
|
72
|
+
| Session Messaging | `session_reachable`, `session_send_message`, `session_cancel` pi tools | Coordinate between live user-facing Pi sessions |
|
|
73
|
+
| Subagents | `subagent_list`, `subagent_send`, `subagent_cancel`, `subagent_launch`, `submit_task_report` | Delegate and manage background work |
|
|
73
74
|
| Session Picker | `Alt+O` | Reference old sessions in your prompt |
|
|
74
75
|
| Session Index | `/session-index` slash command | Shows index status and rebuilds the local session index |
|
|
75
76
|
| Session Auto Title | in background, `/title` slash command | Give sessions titles |
|
|
@@ -107,34 +108,44 @@ Use `kind: "user"` or `kind: "subagent"` to filter by session type across the wh
|
|
|
107
108
|
|
|
108
109
|
## Session Handoff
|
|
109
110
|
|
|
110
|
-
The `session_handoff` tool
|
|
111
|
+
The `session_handoff` tool creates a user-facing child session with a self-contained task. Ask for a direction when you want a visible split, or a deferred handoff when you only want the prepared session. Use `subagent_launch` to delegate suitable independent work to a background subagent.
|
|
111
112
|
|
|
112
113
|
Directional launches use tmux when the current terminal is inside tmux, or Ghostty on macOS. Deferred launches create the child without starting it and copy its resume command to the clipboard. A background subagent runs in a detached tmux window and reports back when finished; note that subagents are started with `--approve`, meaning pi will always start in the directory as trusted.
|
|
113
114
|
|
|
114
|
-
When
|
|
115
|
+
When a host claims peers, pi-sessions leaves all user-facing coordination and launches to it. Subagent tools remain available; external-host launch targets are not part of pi-sessions.
|
|
115
116
|
|
|
116
|
-
Run `/handoff` to open the **Handoffs** board. The Subagents and User sessions tabs show status, age, launch details, and the actions currently available: stop, copy an observation command, or copy a resume command.
|
|
117
|
+
Run `/handoff` to open the **Handoffs** board. The Subagents and User sessions tabs show status, age, launch details, and the actions currently available: stop, copy an observation command, or copy a resume command. With a peer-claiming host, only Subagents are shown.
|
|
118
|
+
|
|
119
|
+
Both launch tools take `goal`, `title` (at most 64 characters), and `requestResponse`, with optional `cwd`, `provider`, `model`, and `thinkingLevel` (`off`, `minimal`, `low`, `medium`, `high`, `xhigh`). `session_handoff` additionally requires `launch` (`left`, `right`, `up`, `down` when a split backend is available, or `deferred` when enabled). `subagent_launch` has no `launch` parameter. Provider and model must be supplied together.
|
|
117
120
|
|
|
118
121
|
## Session Messaging
|
|
119
122
|
|
|
120
|
-
|
|
123
|
+
Standalone agents can coordinate with live user-facing Pi sessions:
|
|
124
|
+
|
|
125
|
+
- `session_reachable({})` lists live user sessions only; there is no scope parameter
|
|
126
|
+
- `session_send_message({ session, message, requestResponse })` sends a message to a live user session
|
|
127
|
+
- `session_cancel({ session })` aborts another live user session's current turn
|
|
128
|
+
|
|
129
|
+
Subagent coordination is a separate addressable directory:
|
|
121
130
|
|
|
122
|
-
- `
|
|
123
|
-
- `
|
|
124
|
-
- `
|
|
131
|
+
- `subagent_list({ scope? })` lists the ledger/roster with `branch` (default) or `tree` scope
|
|
132
|
+
- `subagent_send({ session, message, requestResponse })` sends to a live subagent (including siblings) or a dormant owned worker, waking owned workers automatically
|
|
133
|
+
- `subagent_cancel({ session })` stops a subagent, recording owned cancellation intent before teardown
|
|
134
|
+
- `subagent_launch` creates a delegated background worker (parameters above)
|
|
135
|
+
- `submit_task_report({ status, summary, details?, references?, nextSteps? })` reports to the parent; available only in a subagent session
|
|
125
136
|
|
|
126
|
-
|
|
137
|
+
Pass bare session UUIDs, not `@session:` tokens. A subagent cannot message or cancel its parent; it reports through `submit_task_report`. Incoming messages start an idle recipient and steer a running one. Other dormant sessions cannot receive messages, but remain searchable with `session_search` and `session_ask`. Cancellation results describe delivery and, when available, the resulting worker state; they do not expose transport/managed subsystem tags.
|
|
127
138
|
|
|
128
|
-
## Host extension API
|
|
139
|
+
## Host extension API
|
|
129
140
|
|
|
130
|
-
A host
|
|
141
|
+
A host claims ownership of peer coordination, rather than supplying launch, listing, or wake callbacks. External extensions register over `pi.events`; neither extension imports the other.
|
|
131
142
|
|
|
132
143
|
Install a listener during extension initialization, **not** in your `session_start` handler:
|
|
133
144
|
|
|
134
145
|
```ts
|
|
135
|
-
pi.events.on("pi-sessions:hosts
|
|
146
|
+
pi.events.on("pi-sessions:hosts", (request) => {
|
|
136
147
|
const { register } = request as HostRegistrationRequest;
|
|
137
|
-
register({ name: "swb",
|
|
148
|
+
register({ name: "swb", peers: true });
|
|
138
149
|
});
|
|
139
150
|
```
|
|
140
151
|
|
|
@@ -147,37 +158,30 @@ interface HostRegistrationRequest {
|
|
|
147
158
|
|
|
148
159
|
interface Host {
|
|
149
160
|
name: string;
|
|
150
|
-
|
|
151
|
-
listSessions?(): Promise<HostSession[]>;
|
|
152
|
-
wake?(sessionId: string): Promise<void>;
|
|
161
|
+
peers: true;
|
|
153
162
|
}
|
|
163
|
+
```
|
|
154
164
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
cwd: string;
|
|
159
|
-
title: string;
|
|
160
|
-
model: string;
|
|
161
|
-
resumeCommand: string;
|
|
162
|
-
}
|
|
165
|
+
- **Registration:** pi-sessions emits `pi-sessions:hosts` at every `session_start`, including reloads and switches. Register synchronously before returning or awaiting anything. Registration freezes when `emit` returns; late calls throw. Names must match `[a-z][a-z0-9-]*`, be unique, and cannot be `left`, `right`, `up`, `down`, `deferred`, `subagent`, `tmux`, or `ghostty`. TypeBox validates exactly `{ name, peers: true }`; additional properties are rejected.
|
|
166
|
+
- **Ownership:** any registered claim suppresses pi-sessions' `session_reachable`, `session_send_message`, `session_cancel`, and `session_handoff`. Peer tools are never registered at extension load, so a host may register its own tools at load without conflicts. Discovery runs before late registration. Pi currently has no tool-unregistration API: definitions retained from a prior standalone session are deactivated with `setActiveTools` and execution-guarded; reload rebuilds registrations. A new standalone session reactivates pi-sessions' tools.
|
|
167
|
+
- **Subagents:** `subagent_list`, `subagent_send`, `subagent_cancel`, `subagent_launch`, and `submit_task_report` remain pi-sessions-owned under a host. Availability still follows feature settings, tmux installation, delegation depth, and whether the caller is a subagent. The host supplies no waker; owned-worker recovery retains its two 30-second broker-registration waits.
|
|
163
168
|
|
|
164
|
-
|
|
165
|
-
| { success: true; clipboardStatus?: "copied" | "failed" }
|
|
166
|
-
| { success: false; error: string };
|
|
169
|
+
## Durable lineage entry
|
|
167
170
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
171
|
+
Any extension may append this custom entry to a child session:
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
{
|
|
175
|
+
type: "custom",
|
|
176
|
+
customType: "pi-sessions.lineage",
|
|
177
|
+
data: {
|
|
178
|
+
parentSessionId: "parent-session-uuid",
|
|
179
|
+
parentSessionFile: "/absolute/path/to/parent.jsonl", // optional
|
|
180
|
+
},
|
|
172
181
|
}
|
|
173
182
|
```
|
|
174
183
|
|
|
175
|
-
|
|
176
|
-
- **Launch:** the child file, lineage, title, and automatic bootstrap are durable before `launch` runs. External-host children always use Pi's default session directory for `cwd`, even when the parent uses a custom directory. Start interactive Pi in that cwd with `--session-id`, `--approve`, and `--model` using the supplied `model` (including its thinking suffix, such as `provider/model:high`), or execute `resumeCommand`. Preserve the same Pi agent directory and load pi-sessions in the child. Do not inject a second initial prompt. Return `{ success: true }` after starting the process; return `{ success: false, error }` or throw on failure. The tool surfaces the surviving child's resume command on failure. `clipboardStatus` is used by the deferred host; external hosts normally omit it.
|
|
177
|
-
- **Discovery:** `listSessions()` returns all open user-facing sessions this host can wake, whether running or dormant. Exclude closed sessions and subagents. It is called on demand by `session_reachable` and dormant-message routing, not polled in the background. Return unique session IDs; competing host ownership is an error. Hosts without both `listSessions` and `wake` do not contribute dormant reachability. `wake` without `listSessions` is invalid.
|
|
178
|
-
- **Wake:** `wake(sessionId)` starts or resumes an owned session and resolves with no value; throw on failure. It must be idempotent across callers because different Pi processes can wake the same target concurrently. Within one sender, concurrent wakes are coalesced. After it resolves, pi-sessions waits **30 seconds** for broker registration before delivering. It retries wake/delivery once if the target disconnects before acceptance, but does not restart a host session on registration timeout. The broker alone supplies liveness; do not wait for broker readiness inside `wake`. Host callbacks have no imposed timeout: bound your own I/O. Built-in split launch commands use a 15-second timeout; subagent stale-window recovery retains two 30-second registration waits.
|
|
179
|
-
|
|
180
|
-
Subagents are not hosts. They retain parent-owned tmux windows, automatic approval, their own ledger, and their own wake/recovery policy. `/handoff` remains a receipt board; it accepts external-host launch receipts but does not provide a separate launch picker or wake action.
|
|
184
|
+
Using Pi's extension API, call `pi.appendEntry("pi-sessions.lineage", { parentSessionId, parentSessionFile? })` in the child (Pi supplies the entry id, timestamp, and tree link). The index uses the first TypeBox-valid lineage entry only when the session header has no `parentSession`; the header takes precedence. The parent id works even without an accessible parent file. This durable relation survives leaving a host; the SQLite index is only its rebuildable cache.
|
|
181
185
|
|
|
182
186
|
## Session picker
|
|
183
187
|
|
|
@@ -213,7 +217,7 @@ If you want to override the shortcut, put this in your `~/.pi/agent/settings.jso
|
|
|
213
217
|
|
|
214
218
|
`roster` limits which models a handoff may launch a child session on. Defaults to pi's own `enabledModels` scoping, then to all configured models. May optionally include a thinking level; listing a model more than once adds to the levels it allows.
|
|
215
219
|
|
|
216
|
-
`deferred.enable` (default `true`) offers the `deferred` launch. Turn it off
|
|
220
|
+
`deferred.enable` (default `true`) offers the `deferred` peer launch. Turn it off to leave only split launches. Hosts own their own launch policy.
|
|
217
221
|
|
|
218
222
|
`deferred.copyToClipboard` (default `true`) controls whether deferred handoffs copy the resume command to the clipboard. When off, the resume command is only shown in the tool call.
|
|
219
223
|
|
|
@@ -1,51 +1,15 @@
|
|
|
1
1
|
import { type Static, Type } from "typebox";
|
|
2
2
|
|
|
3
|
-
export const HOSTS_CHANNEL = "pi-sessions:hosts
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
export const HOST_LAUNCH_RESULT_SCHEMA = Type.Union([
|
|
15
|
-
Type.Object({
|
|
16
|
-
success: Type.Literal(true),
|
|
17
|
-
clipboardStatus: Type.Optional(Type.Union([Type.Literal("copied"), Type.Literal("failed")])),
|
|
18
|
-
}),
|
|
19
|
-
Type.Object({ success: Type.Literal(false), error: Type.String() }),
|
|
20
|
-
]);
|
|
21
|
-
|
|
22
|
-
export const HOST_SESSION_SCHEMA = Type.Object({
|
|
23
|
-
sessionId: Type.String({ minLength: 1 }),
|
|
24
|
-
cwd: Type.String({ minLength: 1 }),
|
|
25
|
-
title: Type.String(),
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
export const HOST_SCHEMA = Type.Object({
|
|
29
|
-
name: Type.String({ pattern: "^[a-z][a-z0-9-]*$" }),
|
|
30
|
-
launch: Type.Function([HOST_LAUNCH_INPUT_SCHEMA], Type.Unknown()),
|
|
31
|
-
listSessions: Type.Optional(Type.Function([], Type.Unknown())),
|
|
32
|
-
wake: Type.Optional(Type.Function([Type.String()], Type.Unknown())),
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
export type HostLaunchInput = Static<typeof HOST_LAUNCH_INPUT_SCHEMA>;
|
|
36
|
-
export type HostLaunchResult = Static<typeof HOST_LAUNCH_RESULT_SCHEMA>;
|
|
37
|
-
export type HostSession = Static<typeof HOST_SESSION_SCHEMA>;
|
|
38
|
-
export interface Host {
|
|
39
|
-
name: string;
|
|
40
|
-
launch(input: HostLaunchInput): Promise<HostLaunchResult>;
|
|
41
|
-
listSessions?(): Promise<HostSession[]>;
|
|
42
|
-
wake?(sessionId: string): Promise<void>;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export interface HostedSession extends HostSession {
|
|
46
|
-
host: string;
|
|
47
|
-
}
|
|
48
|
-
|
|
3
|
+
export const HOSTS_CHANNEL = "pi-sessions:hosts";
|
|
4
|
+
export const HOST_SCHEMA = Type.Object(
|
|
5
|
+
{
|
|
6
|
+
name: Type.String({ pattern: "^[a-z][a-z0-9-]*$" }),
|
|
7
|
+
peers: Type.Literal(true),
|
|
8
|
+
},
|
|
9
|
+
{ additionalProperties: false },
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
export type Host = Static<typeof HOST_SCHEMA>;
|
|
49
13
|
export interface HostRegistrationRequest {
|
|
50
14
|
register(host: Host): void;
|
|
51
15
|
}
|
|
@@ -1,15 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { Type } from "typebox";
|
|
3
2
|
import { parseTypeBoxValue } from "../shared/typebox.ts";
|
|
4
|
-
import {
|
|
5
|
-
HOST_LAUNCH_RESULT_SCHEMA,
|
|
6
|
-
HOST_SCHEMA,
|
|
7
|
-
HOST_SESSION_SCHEMA,
|
|
8
|
-
HOSTS_CHANNEL,
|
|
9
|
-
type Host,
|
|
10
|
-
type HostedSession,
|
|
11
|
-
type HostLaunchInput,
|
|
12
|
-
} from "./contract.ts";
|
|
3
|
+
import { HOST_SCHEMA, HOSTS_CHANNEL, type Host } from "./contract.ts";
|
|
13
4
|
|
|
14
5
|
const RESERVED_NAMES = new Set([
|
|
15
6
|
"left",
|
|
@@ -39,46 +30,7 @@ export class HostRegistry {
|
|
|
39
30
|
if (RESERVED_NAMES.has(host.name) || hosts.some((entry) => entry.name === host.name)) {
|
|
40
31
|
throw new Error(`Host name is reserved or already registered: ${host.name}`);
|
|
41
32
|
}
|
|
42
|
-
|
|
43
|
-
throw new Error(`Host ${host.name}: wake requires listSessions.`);
|
|
44
|
-
}
|
|
45
|
-
const name = host.name;
|
|
46
|
-
const launch = host.launch.bind(host);
|
|
47
|
-
const listSessions = host.listSessions?.bind(host);
|
|
48
|
-
const wake = host.wake?.bind(host);
|
|
49
|
-
hosts.push(
|
|
50
|
-
Object.freeze({
|
|
51
|
-
name,
|
|
52
|
-
async launch(input: HostLaunchInput) {
|
|
53
|
-
return parseTypeBoxValue(
|
|
54
|
-
HOST_LAUNCH_RESULT_SCHEMA,
|
|
55
|
-
await launch(input),
|
|
56
|
-
`Host ${name} launch result`,
|
|
57
|
-
);
|
|
58
|
-
},
|
|
59
|
-
...(listSessions
|
|
60
|
-
? {
|
|
61
|
-
listSessions: async () =>
|
|
62
|
-
parseTypeBoxValue(
|
|
63
|
-
Type.Array(HOST_SESSION_SCHEMA),
|
|
64
|
-
await listSessions(),
|
|
65
|
-
`Host ${name} sessions`,
|
|
66
|
-
),
|
|
67
|
-
}
|
|
68
|
-
: {}),
|
|
69
|
-
...(wake
|
|
70
|
-
? {
|
|
71
|
-
async wake(target: string) {
|
|
72
|
-
parseTypeBoxValue(
|
|
73
|
-
Type.Void(),
|
|
74
|
-
await wake(target),
|
|
75
|
-
`Host ${name} wake result`,
|
|
76
|
-
);
|
|
77
|
-
},
|
|
78
|
-
}
|
|
79
|
-
: {}),
|
|
80
|
-
}),
|
|
81
|
-
);
|
|
33
|
+
hosts.push(Object.freeze({ ...host }));
|
|
82
34
|
} catch (error) {
|
|
83
35
|
registrationError = error;
|
|
84
36
|
throw error;
|
|
@@ -97,33 +49,7 @@ export class HostRegistry {
|
|
|
97
49
|
return this.hosts;
|
|
98
50
|
}
|
|
99
51
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
this.hosts.map(async (host) => {
|
|
103
|
-
if (!host.wake || !host.listSessions) return [];
|
|
104
|
-
return (await host.listSessions()).map((session) => ({ ...session, host: host.name }));
|
|
105
|
-
}),
|
|
106
|
-
);
|
|
107
|
-
const result = sessions.flat();
|
|
108
|
-
const ids = new Set<string>();
|
|
109
|
-
for (const session of result) {
|
|
110
|
-
if (ids.has(session.sessionId))
|
|
111
|
-
throw new Error(`Multiple hosts claim session ${session.sessionId}.`);
|
|
112
|
-
ids.add(session.sessionId);
|
|
113
|
-
}
|
|
114
|
-
return result;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
async owns(target: string): Promise<boolean> {
|
|
118
|
-
return (await this.listSessions()).some((session) => session.sessionId === target);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
async wake(target: string): Promise<void> {
|
|
122
|
-
const hosts = this.hosts;
|
|
123
|
-
const session = (await this.listSessions()).find((session) => session.sessionId === target);
|
|
124
|
-
if (this.hosts !== hosts) throw new Error("The host registry changed while waking a session.");
|
|
125
|
-
const host = hosts.find((host) => host.name === session?.host);
|
|
126
|
-
if (!host?.wake) throw new Error(`No host can wake session ${target}.`);
|
|
127
|
-
await host.wake(target);
|
|
52
|
+
claimsPeers(): boolean {
|
|
53
|
+
return this.hosts.length > 0;
|
|
128
54
|
}
|
|
129
55
|
}
|
|
@@ -9,18 +9,15 @@ import { installAutoTitle } from "./session-auto-title/install.ts";
|
|
|
9
9
|
import { installHandoff } from "./session-handoff/install.ts";
|
|
10
10
|
import { installHooks } from "./session-hooks/install.ts";
|
|
11
11
|
import { installIndex } from "./session-index/install.ts";
|
|
12
|
+
import { createPeerDirectory, createSubagentDirectory } from "./session-messaging/directory.ts";
|
|
12
13
|
import { installMessaging } from "./session-messaging/install.ts";
|
|
13
|
-
import { MessageRouter
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
createSessionCancelTool,
|
|
17
|
-
createSessionSendMessageTool,
|
|
18
|
-
} from "./session-messaging/pi/tools.ts";
|
|
14
|
+
import { MessageRouter } from "./session-messaging/message-router.ts";
|
|
15
|
+
import { installDirectoryTools } from "./session-messaging/pi/install.ts";
|
|
19
16
|
import { installSearch } from "./session-search/install.ts";
|
|
20
17
|
import type { SessionLifecycle } from "./shared/composition.ts";
|
|
21
18
|
import { createSessionModelRuntime, type ModelRuntimeProvider } from "./shared/model-runtime.ts";
|
|
22
19
|
import { loadSettings, readCompactionSettings } from "./shared/settings.ts";
|
|
23
|
-
import { installSubagents } from "./subagents/install.ts";
|
|
20
|
+
import { installSubagents, type SubagentsHandle } from "./subagents/install.ts";
|
|
24
21
|
|
|
25
22
|
/**
|
|
26
23
|
* The single advertised entrypoint. It loads settings once, constructs each feature in
|
|
@@ -57,65 +54,42 @@ export default function piSessions(pi: ExtensionAPI): void {
|
|
|
57
54
|
if (messaging) {
|
|
58
55
|
lifecycles.push(messaging);
|
|
59
56
|
}
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
57
|
+
const messageRouter: MessageRouter | undefined = messaging
|
|
58
|
+
? new MessageRouter(
|
|
59
|
+
messaging,
|
|
60
|
+
() => subagents?.waker,
|
|
61
|
+
() => sessionEpoch,
|
|
62
|
+
)
|
|
63
63
|
: undefined;
|
|
64
|
-
const subagents =
|
|
64
|
+
const subagents: SubagentsHandle | undefined =
|
|
65
65
|
settings.features.subagents && messaging && messageRouter
|
|
66
66
|
? installSubagents(pi, {
|
|
67
67
|
settings,
|
|
68
68
|
messaging,
|
|
69
69
|
readCompactionSettings,
|
|
70
70
|
sendMessage: (request) => messageRouter.sendMessage(request),
|
|
71
|
-
hasHostedSessions: () => hosts.getHosts().some((host) => Boolean(host.wake)),
|
|
72
71
|
})
|
|
73
72
|
: undefined;
|
|
74
73
|
if (subagents) {
|
|
75
|
-
wakers.unshift(subagents.waker);
|
|
76
74
|
lifecycles.push(subagents);
|
|
77
75
|
}
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
: { kind: "plain" },
|
|
89
|
-
hostedSessions,
|
|
90
|
-
getCachedRelationTo: messaging.getCachedRelationTo,
|
|
91
|
-
...(subagents ? { getParentSessionId: () => subagents.getParentSessionId() } : {}),
|
|
92
|
-
}),
|
|
93
|
-
);
|
|
94
|
-
pi.registerTool(
|
|
95
|
-
createSessionCancelTool(subagents ?? messaging, {
|
|
96
|
-
role: isSubagent ? { kind: "subagent" } : { kind: "plain" },
|
|
97
|
-
...(subagents ? { getParentSessionId: () => subagents.getParentSessionId() } : {}),
|
|
98
|
-
}),
|
|
99
|
-
);
|
|
100
|
-
pi.registerTool(
|
|
101
|
-
createSessionReachableTool({
|
|
102
|
-
indexPath: index.path,
|
|
103
|
-
hostedSessions,
|
|
104
|
-
listSessions: () => messaging.listSessions(),
|
|
105
|
-
listHostedSessions: () => hosts.listSessions(),
|
|
106
|
-
getRelationTo: messaging.getCachedRelationTo,
|
|
107
|
-
...(subagents
|
|
108
|
-
? {
|
|
109
|
-
listSubagents: async (scope) => (await subagents.roster.resolve(scope)).entries,
|
|
110
|
-
getParentSessionId: () => subagents.getParentSessionId(),
|
|
111
|
-
}
|
|
76
|
+
const directoryTools = messaging
|
|
77
|
+
? installDirectoryTools(pi, {
|
|
78
|
+
peers: createPeerDirectory(messaging, {
|
|
79
|
+
indexPath: index.path,
|
|
80
|
+
listSessions: () => messaging.listSessions(),
|
|
81
|
+
getRelationTo: messaging.getCachedRelationTo,
|
|
82
|
+
getParentSessionId: () => subagents?.getParentSessionId(),
|
|
83
|
+
}),
|
|
84
|
+
...(subagents && messageRouter
|
|
85
|
+
? { subagents: createSubagentDirectory(subagents, messageRouter, messaging, index.path) }
|
|
112
86
|
: {}),
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
registerMessagingTools();
|
|
87
|
+
claimsPeers: () => hosts.claimsPeers(),
|
|
88
|
+
})
|
|
89
|
+
: undefined;
|
|
117
90
|
if (settings.features.handoff) {
|
|
118
91
|
const board = {
|
|
92
|
+
showUserSessions: () => !hosts.claimsPeers(),
|
|
119
93
|
roster: subagents?.roster,
|
|
120
94
|
cancelSubagent: subagents
|
|
121
95
|
? (sessionId: string) => subagents.cancelSession(sessionId)
|
|
@@ -128,7 +102,7 @@ export default function piSessions(pi: ExtensionAPI): void {
|
|
|
128
102
|
settings,
|
|
129
103
|
index,
|
|
130
104
|
getModelRuntime,
|
|
131
|
-
|
|
105
|
+
claimsPeers: () => hosts.claimsPeers(),
|
|
132
106
|
...(subagents ? { getLaunchTargets: () => subagents.getLaunchTargets() } : {}),
|
|
133
107
|
board,
|
|
134
108
|
}),
|
|
@@ -167,7 +141,7 @@ export default function piSessions(pi: ExtensionAPI): void {
|
|
|
167
141
|
for (const lifecycle of lifecycles) {
|
|
168
142
|
await lifecycle.onSessionStart?.(event, ctx);
|
|
169
143
|
}
|
|
170
|
-
|
|
144
|
+
directoryTools?.refresh();
|
|
171
145
|
for (const lifecycle of lifecycles) {
|
|
172
146
|
lifecycle.onSessionReady?.(ctx);
|
|
173
147
|
}
|
|
@@ -5,6 +5,7 @@ import { HANDOFF_BOOTSTRAP_PENDING_CUSTOM_TYPE } from "./metadata.ts";
|
|
|
5
5
|
import { parseHandoffLaunchReceiptEntry } from "./receipt.ts";
|
|
6
6
|
|
|
7
7
|
export interface HandoffBoardServices {
|
|
8
|
+
showUserSessions: () => boolean;
|
|
8
9
|
roster?: SubagentRoster | undefined;
|
|
9
10
|
cancelSubagent?: ((sessionId: string) => Promise<unknown>) | undefined;
|
|
10
11
|
listLiveSessions?: (() => Promise<string[]>) | undefined;
|
|
@@ -15,7 +16,8 @@ export async function loadHandoffBoardSnapshot(
|
|
|
15
16
|
entries: readonly SessionEntry[],
|
|
16
17
|
services: HandoffBoardServices,
|
|
17
18
|
): Promise<HandoffBoardSnapshot> {
|
|
18
|
-
const
|
|
19
|
+
const showUserSessions = services.showUserSessions();
|
|
20
|
+
const userSessions = showUserSessions ? collectUserSessions(entries) : [];
|
|
19
21
|
const [branchRoster, liveSessionIds, hydratedUserSessions] = await Promise.all([
|
|
20
22
|
services.roster?.resolve("branch") ?? Promise.resolve({ entries: [], total: 0 }),
|
|
21
23
|
services.listLiveSessions?.() ?? Promise.resolve([]),
|
|
@@ -29,6 +31,7 @@ export async function loadHandoffBoardSnapshot(
|
|
|
29
31
|
),
|
|
30
32
|
]);
|
|
31
33
|
return {
|
|
34
|
+
showUserSessions,
|
|
32
35
|
subagents: branchRoster.entries,
|
|
33
36
|
userSessions: hydratedUserSessions,
|
|
34
37
|
liveSessionIds: new Set(liveSessionIds),
|
|
@@ -278,6 +278,8 @@ export class HandoffBoard implements Focusable {
|
|
|
278
278
|
}
|
|
279
279
|
|
|
280
280
|
private renderTabs(row: number, width: number): string {
|
|
281
|
+
if (!this.snapshot.showUserSessions)
|
|
282
|
+
return this.theme.fg("accent", this.theme.bold("Subagents"));
|
|
281
283
|
this.controlHits.set(
|
|
282
284
|
row,
|
|
283
285
|
[
|
|
@@ -493,7 +495,7 @@ export class HandoffBoard implements Focusable {
|
|
|
493
495
|
|
|
494
496
|
const action = view.action;
|
|
495
497
|
const items: LegendItem[] = [
|
|
496
|
-
{ key: "<>", description: "tab" },
|
|
498
|
+
...(!this.snapshot.showUserSessions ? [] : [{ key: "<>", description: "tab" }]),
|
|
497
499
|
{ key: "↑↓", description: "select" },
|
|
498
500
|
];
|
|
499
501
|
if (action?.canStop) {
|
|
@@ -557,6 +559,7 @@ export class HandoffBoard implements Focusable {
|
|
|
557
559
|
}
|
|
558
560
|
|
|
559
561
|
private setTab(tab: HandoffBoardTab): void {
|
|
562
|
+
if (tab === "user-sessions" && !this.snapshot.showUserSessions) return;
|
|
560
563
|
this.tab = tab;
|
|
561
564
|
this.clearTransientState();
|
|
562
565
|
this.clampSelection();
|