@oh-my-pi/pi-coding-agent 15.11.6 → 15.11.8
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 +57 -1
- package/dist/cli.js +431 -381
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/bench-cli.d.ts +78 -0
- package/dist/types/collab/crypto.d.ts +12 -0
- package/dist/types/collab/guest.d.ts +21 -0
- package/dist/types/collab/host.d.ts +13 -0
- package/dist/types/collab/protocol.d.ts +100 -0
- package/dist/types/collab/relay-client.d.ts +22 -0
- package/dist/types/commands/bench.d.ts +29 -0
- package/dist/types/commands/join.d.ts +12 -0
- package/dist/types/config/model-resolver.d.ts +3 -2
- package/dist/types/config/settings-schema.d.ts +93 -1
- package/dist/types/edit/renderer.d.ts +1 -0
- package/dist/types/extensibility/slash-commands.d.ts +1 -11
- package/dist/types/modes/components/agent-hub.d.ts +13 -0
- package/dist/types/modes/components/collab-prompt-message.d.ts +10 -0
- package/dist/types/modes/components/hook-selector.d.ts +4 -6
- package/dist/types/modes/components/oauth-selector.d.ts +10 -1
- package/dist/types/modes/components/segment-track.d.ts +11 -6
- package/dist/types/modes/components/settings-selector.d.ts +8 -1
- package/dist/types/modes/components/snapcompact-shape-preview.d.ts +31 -0
- package/dist/types/modes/components/status-line/component.d.ts +4 -1
- package/dist/types/modes/components/status-line/types.d.ts +9 -0
- package/dist/types/modes/components/tool-execution.d.ts +13 -9
- package/dist/types/modes/interactive-mode.d.ts +7 -0
- package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +3 -0
- package/dist/types/modes/setup-wizard/scenes/types.d.ts +10 -1
- package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +3 -0
- package/dist/types/modes/types.d.ts +8 -0
- package/dist/types/session/agent-session.d.ts +11 -0
- package/dist/types/session/session-manager.d.ts +21 -0
- package/dist/types/session/snapcompact-inline.d.ts +8 -3
- package/dist/types/slash-commands/builtin-registry.d.ts +9 -0
- package/dist/types/tools/bash.d.ts +2 -0
- package/dist/types/tools/eval-render.d.ts +1 -0
- package/dist/types/tools/renderers.d.ts +13 -0
- package/dist/types/tools/ssh.d.ts +1 -0
- package/package.json +14 -12
- package/scripts/bench-guard.ts +71 -0
- package/src/cli/args.ts +2 -0
- package/src/cli/bench-cli.ts +437 -0
- package/src/cli-commands.ts +2 -0
- package/src/collab/crypto.ts +57 -0
- package/src/collab/guest.ts +421 -0
- package/src/collab/host.ts +494 -0
- package/src/collab/protocol.ts +191 -0
- package/src/collab/relay-client.ts +216 -0
- package/src/commands/bench.ts +42 -0
- package/src/commands/join.ts +39 -0
- package/src/config/model-registry.ts +74 -19
- package/src/config/model-resolver.ts +36 -5
- package/src/config/settings-schema.ts +119 -1
- package/src/edit/renderer.ts +5 -0
- package/src/extensibility/slash-commands.ts +1 -97
- package/src/hindsight/client.ts +26 -1
- package/src/hindsight/state.ts +6 -2
- package/src/internal-urls/docs-index.generated.ts +4 -3
- package/src/main.ts +11 -2
- package/src/mcp/transports/stdio.ts +81 -7
- package/src/modes/components/agent-hub.ts +119 -22
- package/src/modes/components/assistant-message.ts +126 -6
- package/src/modes/components/collab-prompt-message.ts +30 -0
- package/src/modes/components/hook-selector.ts +4 -5
- package/src/modes/components/oauth-selector.ts +67 -7
- package/src/modes/components/segment-track.ts +44 -7
- package/src/modes/components/settings-selector.ts +27 -0
- package/src/modes/components/snapcompact-shape-preview-doc.md +11 -0
- package/src/modes/components/snapcompact-shape-preview.ts +192 -0
- package/src/modes/components/status-line/component.ts +21 -1
- package/src/modes/components/status-line/presets.ts +1 -1
- package/src/modes/components/status-line/segments.ts +13 -0
- package/src/modes/components/status-line/types.ts +10 -0
- package/src/modes/components/tips.txt +2 -1
- package/src/modes/components/tool-execution.ts +18 -10
- package/src/modes/controllers/input-controller.ts +80 -12
- package/src/modes/controllers/selector-controller.ts +6 -2
- package/src/modes/controllers/streaming-reveal.ts +7 -0
- package/src/modes/interactive-mode.ts +36 -4
- package/src/modes/setup-wizard/index.ts +1 -0
- package/src/modes/setup-wizard/scenes/glyph.ts +24 -6
- package/src/modes/setup-wizard/scenes/providers.ts +36 -2
- package/src/modes/setup-wizard/scenes/sign-in.ts +10 -1
- package/src/modes/setup-wizard/scenes/theme.ts +28 -1
- package/src/modes/setup-wizard/scenes/types.ts +10 -1
- package/src/modes/setup-wizard/scenes/web-search.ts +22 -6
- package/src/modes/setup-wizard/wizard-overlay.ts +38 -1
- package/src/modes/types.ts +8 -0
- package/src/modes/utils/context-usage.ts +1 -1
- package/src/modes/utils/ui-helpers.ts +7 -0
- package/src/prompts/bench.md +7 -0
- package/src/sdk.ts +240 -36
- package/src/session/agent-session.ts +22 -0
- package/src/session/session-manager.ts +44 -0
- package/src/session/snapcompact-inline.ts +20 -22
- package/src/slash-commands/builtin-registry.ts +210 -0
- package/src/tools/bash.ts +3 -0
- package/src/tools/eval-render.ts +4 -0
- package/src/tools/read.ts +38 -5
- package/src/tools/renderers.ts +13 -0
- package/src/tools/ssh.ts +3 -0
- package/src/tools/write.ts +13 -42
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guest side of a collab live session.
|
|
3
|
+
*
|
|
4
|
+
* `/join <link>` writes the host's snapshot to a replica session file and
|
|
5
|
+
* drives it through the normal `/resume` machinery, then applies live frames:
|
|
6
|
+
* entries → SessionManager + agent.replaceMessages, events →
|
|
7
|
+
* EventController.handleEvent, state → status-line overrides plus real
|
|
8
|
+
* model/thinking state applied to the replica agent. The host's subagent
|
|
9
|
+
* ecosystem is mirrored too: agent snapshots populate a local AgentRegistry
|
|
10
|
+
* (Agent Hub), EventBus traffic (observer HUD) is republished, and hub
|
|
11
|
+
* actions (chat/kill/revive/transcript reads) round-trip over the wire.
|
|
12
|
+
* Everything renders through the same components, so ctrl+o, theming, and
|
|
13
|
+
* transcript behavior are native by construction.
|
|
14
|
+
*/
|
|
15
|
+
import * as path from "node:path";
|
|
16
|
+
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
17
|
+
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
|
18
|
+
import { getConfigRootDir, logger } from "@oh-my-pi/pi-utils";
|
|
19
|
+
import type { AgentHubRemote } from "../modes/components/agent-hub";
|
|
20
|
+
import type { InteractiveModeContext } from "../modes/types";
|
|
21
|
+
import { AgentRegistry } from "../registry/agent-registry";
|
|
22
|
+
import type { AgentSessionEvent } from "../session/agent-session";
|
|
23
|
+
import { shouldDisableReasoning, toReasoningEffort } from "../thinking";
|
|
24
|
+
import { setSessionTerminalTitle } from "../utils/title-generator";
|
|
25
|
+
import { importRoomKey } from "./crypto";
|
|
26
|
+
import { collabDisplayName } from "./host";
|
|
27
|
+
import {
|
|
28
|
+
type AgentSnapshot,
|
|
29
|
+
COLLAB_PROTO,
|
|
30
|
+
type CollabFrame,
|
|
31
|
+
type CollabSessionState,
|
|
32
|
+
parseCollabLink,
|
|
33
|
+
} from "./protocol";
|
|
34
|
+
import { CollabSocket } from "./relay-client";
|
|
35
|
+
|
|
36
|
+
/** Commands a guest may run locally; everything else is host-only. */
|
|
37
|
+
export const COLLAB_GUEST_ALLOWED_COMMANDS: Record<string, true> = {
|
|
38
|
+
dump: true,
|
|
39
|
+
export: true,
|
|
40
|
+
copy: true,
|
|
41
|
+
help: true,
|
|
42
|
+
hotkeys: true,
|
|
43
|
+
theme: true,
|
|
44
|
+
settings: true,
|
|
45
|
+
leave: true,
|
|
46
|
+
collab: true,
|
|
47
|
+
exit: true,
|
|
48
|
+
quit: true,
|
|
49
|
+
};
|
|
50
|
+
const WELCOME_TIMEOUT_MS = 30_000;
|
|
51
|
+
const TRANSCRIPT_TIMEOUT_MS = 20_000;
|
|
52
|
+
|
|
53
|
+
type WelcomeFrame = Extract<CollabFrame, { t: "welcome" }>;
|
|
54
|
+
|
|
55
|
+
export class CollabGuestLink {
|
|
56
|
+
#ctx: InteractiveModeContext;
|
|
57
|
+
#socket: CollabSocket | null = null;
|
|
58
|
+
#roomId = "";
|
|
59
|
+
/** Previous session file to restore on leave; null = previous session was unsaved. */
|
|
60
|
+
#returnSessionFile: string | null = null;
|
|
61
|
+
/** Frames apply strictly in arrival order through this chain. */
|
|
62
|
+
#applyChain: Promise<void> = Promise.resolve();
|
|
63
|
+
#welcomed = false;
|
|
64
|
+
#left = false;
|
|
65
|
+
/** False until the first assistant message_start (real or synthesized) since (re)sync. */
|
|
66
|
+
#assistantStreamSynced = false;
|
|
67
|
+
state: CollabSessionState | null = null;
|
|
68
|
+
/** Local mirror of the host's agent ecosystem (refs carry `session: null`). */
|
|
69
|
+
readonly agentRegistry = new AgentRegistry();
|
|
70
|
+
/** Per-agent `hasSessionFile` from the last snapshot; gates remote transcript fetches. */
|
|
71
|
+
#agentHasTranscript = new Map<string, boolean>();
|
|
72
|
+
#pendingTranscripts = new Map<number, (r: { text: string; newSize: number } | null) => void>();
|
|
73
|
+
#nextReqId = 1;
|
|
74
|
+
readonly #hubRemote: AgentHubRemote = {
|
|
75
|
+
chat: (id, text) => {
|
|
76
|
+
this.#socket?.send({ t: "agent-cmd", cmd: "chat", agentId: id, text });
|
|
77
|
+
},
|
|
78
|
+
kill: id => {
|
|
79
|
+
this.#socket?.send({ t: "agent-cmd", cmd: "kill", agentId: id });
|
|
80
|
+
},
|
|
81
|
+
revive: id => {
|
|
82
|
+
this.#socket?.send({ t: "agent-cmd", cmd: "revive", agentId: id });
|
|
83
|
+
},
|
|
84
|
+
readTranscript: (id, fromByte) => {
|
|
85
|
+
const socket = this.#socket;
|
|
86
|
+
if (!socket || this.#agentHasTranscript.get(id) === false) {
|
|
87
|
+
return Promise.resolve(null);
|
|
88
|
+
}
|
|
89
|
+
const reqId = this.#nextReqId++;
|
|
90
|
+
const { promise, resolve } = Promise.withResolvers<{ text: string; newSize: number } | null>();
|
|
91
|
+
const timer = setTimeout(() => {
|
|
92
|
+
this.#pendingTranscripts.delete(reqId);
|
|
93
|
+
resolve(null);
|
|
94
|
+
}, TRANSCRIPT_TIMEOUT_MS);
|
|
95
|
+
this.#pendingTranscripts.set(reqId, result => {
|
|
96
|
+
clearTimeout(timer);
|
|
97
|
+
resolve(result);
|
|
98
|
+
});
|
|
99
|
+
socket.send({ t: "fetch-transcript", reqId, agentId: id, fromByte });
|
|
100
|
+
return promise;
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/** Agent Hub actions routed to the host over the wire. */
|
|
105
|
+
get hubRemote(): AgentHubRemote {
|
|
106
|
+
return this.#hubRemote;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
constructor(ctx: InteractiveModeContext) {
|
|
110
|
+
this.#ctx = ctx;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async join(link: string): Promise<void> {
|
|
114
|
+
const parsed = parseCollabLink(link);
|
|
115
|
+
if ("error" in parsed) throw new Error(parsed.error);
|
|
116
|
+
this.#roomId = parsed.roomId;
|
|
117
|
+
const key = await importRoomKey(parsed.key);
|
|
118
|
+
|
|
119
|
+
this.#returnSessionFile = this.#ctx.sessionManager.getSessionFile() ?? null;
|
|
120
|
+
|
|
121
|
+
const socket = new CollabSocket({ wsUrl: parsed.wsUrl, role: "guest", key });
|
|
122
|
+
this.#socket = socket;
|
|
123
|
+
|
|
124
|
+
const firstWelcome = Promise.withResolvers<void>();
|
|
125
|
+
let joined = false;
|
|
126
|
+
|
|
127
|
+
socket.onOpen = () => {
|
|
128
|
+
// (Re)connect: re-introduce ourselves; the host answers with a fresh
|
|
129
|
+
// welcome which (re)syncs the replica.
|
|
130
|
+
this.#welcomed = false;
|
|
131
|
+
socket.send({ t: "hello", proto: COLLAB_PROTO, name: collabDisplayName(this.#ctx) });
|
|
132
|
+
};
|
|
133
|
+
socket.onFrame = frame => {
|
|
134
|
+
this.#applyChain = this.#applyChain
|
|
135
|
+
.then(async () => {
|
|
136
|
+
if (frame.t === "welcome") {
|
|
137
|
+
await this.#applyWelcome(frame, joined);
|
|
138
|
+
if (!joined) {
|
|
139
|
+
joined = true;
|
|
140
|
+
firstWelcome.resolve();
|
|
141
|
+
}
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (!this.#welcomed || this.#left) return;
|
|
145
|
+
this.#applyFrame(frame);
|
|
146
|
+
})
|
|
147
|
+
.catch(err => logger.warn("collab guest frame apply failed", { type: frame.t, error: String(err) }));
|
|
148
|
+
};
|
|
149
|
+
socket.onClose = (reason, willReconnect) => {
|
|
150
|
+
this.#flushPendingTranscripts();
|
|
151
|
+
if (this.#left) return;
|
|
152
|
+
if (!joined) {
|
|
153
|
+
firstWelcome.reject(new Error(reason));
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (willReconnect) {
|
|
157
|
+
this.#ctx.showStatus(`Collab connection lost (${reason}), reconnecting…`, { dim: true });
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
this.#ctx.showStatus(`Collab session ended (${reason})`);
|
|
161
|
+
void this.#restoreLocalSession();
|
|
162
|
+
};
|
|
163
|
+
socket.connect();
|
|
164
|
+
|
|
165
|
+
const timeout = setTimeout(
|
|
166
|
+
() => firstWelcome.reject(new Error("timed out waiting for the host's welcome")),
|
|
167
|
+
WELCOME_TIMEOUT_MS,
|
|
168
|
+
);
|
|
169
|
+
try {
|
|
170
|
+
await firstWelcome.promise;
|
|
171
|
+
} catch (err) {
|
|
172
|
+
this.#left = true;
|
|
173
|
+
socket.close();
|
|
174
|
+
this.#socket = null;
|
|
175
|
+
throw err;
|
|
176
|
+
} finally {
|
|
177
|
+
clearTimeout(timeout);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
this.#ctx.collabGuest = this;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** User-initiated leave (or post-disconnect cleanup): restore the previous session. */
|
|
184
|
+
async leave(_reason: string): Promise<void> {
|
|
185
|
+
if (this.#left) return;
|
|
186
|
+
this.#socket?.close();
|
|
187
|
+
await this.#restoreLocalSession();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
sendPrompt(text: string, images?: ImageContent[]): void {
|
|
191
|
+
this.#socket?.send({ t: "prompt", text, images: images && images.length > 0 ? images : undefined });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
sendAbort(): void {
|
|
195
|
+
this.#socket?.send({ t: "abort" });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Write the welcome snapshot to the replica file and (re)load it through the resume machinery. */
|
|
199
|
+
async #applyWelcome(frame: WelcomeFrame, isResync: boolean): Promise<void> {
|
|
200
|
+
if (this.#left) return;
|
|
201
|
+
const replicaPath = path.join(getConfigRootDir(), "collab", `${this.#roomId}.jsonl`);
|
|
202
|
+
const lines = [frame.header, ...frame.entries].map(entry => JSON.stringify(entry)).join("\n");
|
|
203
|
+
await Bun.write(replicaPath, `${lines}\n`);
|
|
204
|
+
|
|
205
|
+
// Resume sequence (selector-controller.handleResumeSession) minus
|
|
206
|
+
// applyCwdChange: the guest process never chdirs to a host path. The
|
|
207
|
+
// SessionManager still adopts the header cwd for display/relativization.
|
|
208
|
+
this.#clearTransientUi();
|
|
209
|
+
this.#clearAgentMirror();
|
|
210
|
+
await this.#ctx.session.switchSession(replicaPath);
|
|
211
|
+
this.state = frame.state;
|
|
212
|
+
this.#applyHostState(frame.state);
|
|
213
|
+
this.#ctx.resetObserverRegistry();
|
|
214
|
+
this.#applyAgentSnapshots(frame.agents);
|
|
215
|
+
this.#assistantStreamSynced = false;
|
|
216
|
+
setSessionTerminalTitle(frame.state.sessionName ?? frame.header.title, frame.state.cwd);
|
|
217
|
+
this.#ctx.chatContainer.clear();
|
|
218
|
+
this.#ctx.renderInitialMessages({ clearTerminalHistory: true });
|
|
219
|
+
await this.#ctx.reloadTodos();
|
|
220
|
+
this.#updateStatusSegment();
|
|
221
|
+
this.#welcomed = true;
|
|
222
|
+
this.#ctx.showStatus(isResync ? "Reconnected to collab session" : "Joined collab session");
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
#applyFrame(frame: CollabFrame): void {
|
|
226
|
+
switch (frame.t) {
|
|
227
|
+
case "entry": {
|
|
228
|
+
// Entries are never rendered directly — rendering is events-only
|
|
229
|
+
// (prevents double-render). They keep the replica file, the agent's
|
|
230
|
+
// message array (/dump, context estimates), and todos current.
|
|
231
|
+
this.#ctx.sessionManager.ingestReplicatedEntry(frame.entry);
|
|
232
|
+
if (frame.entry.type === "message") {
|
|
233
|
+
this.#ctx.session.agent.replaceMessages([...this.#ctx.session.messages, frame.entry.message]);
|
|
234
|
+
}
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
case "event":
|
|
238
|
+
this.#applyEvent(frame.event);
|
|
239
|
+
break;
|
|
240
|
+
case "state": {
|
|
241
|
+
this.state = frame.state;
|
|
242
|
+
this.#applyHostState(frame.state);
|
|
243
|
+
setSessionTerminalTitle(frame.state.sessionName, frame.state.cwd);
|
|
244
|
+
this.#updateStatusSegment();
|
|
245
|
+
// Reconciler: events normally drive the loader; clear a stale one if
|
|
246
|
+
// the host reports idle (e.g. events lost across a reconnect).
|
|
247
|
+
if (!frame.state.isStreaming && this.#ctx.loadingAnimation) {
|
|
248
|
+
this.#ctx.loadingAnimation.stop();
|
|
249
|
+
this.#ctx.loadingAnimation = undefined;
|
|
250
|
+
}
|
|
251
|
+
this.#ctx.statusLine.invalidate();
|
|
252
|
+
this.#ctx.ui.requestRender();
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
case "bus":
|
|
256
|
+
// Mirrored host EventBus traffic (task subagent lifecycle/progress)
|
|
257
|
+
// feeding the observer HUD and Agent Hub progress columns.
|
|
258
|
+
this.#ctx.eventBus?.emit(frame.channel, frame.data);
|
|
259
|
+
break;
|
|
260
|
+
case "agents":
|
|
261
|
+
this.#applyAgentSnapshots(frame.agents);
|
|
262
|
+
break;
|
|
263
|
+
case "transcript": {
|
|
264
|
+
const resolve = this.#pendingTranscripts.get(frame.reqId);
|
|
265
|
+
if (resolve) {
|
|
266
|
+
this.#pendingTranscripts.delete(frame.reqId);
|
|
267
|
+
resolve(frame.error ? null : { text: frame.text, newSize: frame.newSize });
|
|
268
|
+
}
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
case "bye": {
|
|
272
|
+
this.#ctx.showStatus(`Collab session ended (${frame.reason})`);
|
|
273
|
+
this.#socket?.close();
|
|
274
|
+
void this.#restoreLocalSession();
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
case "error":
|
|
278
|
+
this.#ctx.showError(`Collab host: ${frame.message}`);
|
|
279
|
+
break;
|
|
280
|
+
default:
|
|
281
|
+
logger.debug("collab guest ignoring unexpected frame", { type: frame.t });
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
#applyEvent(event: AgentSessionEvent): void {
|
|
286
|
+
// Orphan-delta guard: when joining mid-turn the message_start for the
|
|
287
|
+
// in-flight assistant message predates the snapshot. message_update
|
|
288
|
+
// carries the full accumulating message, so synthesize the missing start
|
|
289
|
+
// before the first orphaned update; every other handler is tolerant of
|
|
290
|
+
// unknown anchors (guarded by streamingComponent/pendingTools lookups).
|
|
291
|
+
if (event.type === "message_start" && event.message.role === "assistant") {
|
|
292
|
+
this.#assistantStreamSynced = true;
|
|
293
|
+
} else if (
|
|
294
|
+
event.type === "message_update" &&
|
|
295
|
+
event.message.role === "assistant" &&
|
|
296
|
+
!this.#assistantStreamSynced
|
|
297
|
+
) {
|
|
298
|
+
this.#assistantStreamSynced = true;
|
|
299
|
+
void this.#ctx.eventController.handleEvent({ type: "message_start", message: event.message });
|
|
300
|
+
}
|
|
301
|
+
void this.#ctx.eventController.handleEvent(event);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Apply the host's real model/thinking state to the replica agent so model
|
|
306
|
+
* display and context-window math are native (no display-string overrides).
|
|
307
|
+
* Pure agent-state mutation: session.setModel/setThinkingLevel would
|
|
308
|
+
* persist entries and clamp to local credentials.
|
|
309
|
+
*/
|
|
310
|
+
#applyHostState(state: CollabSessionState): void {
|
|
311
|
+
const session = this.#ctx.session;
|
|
312
|
+
if (
|
|
313
|
+
state.model &&
|
|
314
|
+
(session.agent.state.model?.id !== state.model.id ||
|
|
315
|
+
session.agent.state.model?.provider !== state.model.provider)
|
|
316
|
+
) {
|
|
317
|
+
session.agent.setModel(state.model);
|
|
318
|
+
}
|
|
319
|
+
const level = state.thinkingLevel as ThinkingLevel | undefined;
|
|
320
|
+
session.agent.setThinkingLevel(toReasoningEffort(level));
|
|
321
|
+
session.agent.setDisableReasoning(shouldDisableReasoning(level));
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** Diff a host agent snapshot into the local registry (refs keep `session: null`). */
|
|
325
|
+
#applyAgentSnapshots(agents: AgentSnapshot[]): void {
|
|
326
|
+
const seen = new Set<string>();
|
|
327
|
+
for (const snap of agents) seen.add(snap.id);
|
|
328
|
+
for (const ref of this.agentRegistry.list()) {
|
|
329
|
+
if (!seen.has(ref.id)) {
|
|
330
|
+
this.agentRegistry.unregister(ref.id);
|
|
331
|
+
this.#agentHasTranscript.delete(ref.id);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
for (const snap of agents) {
|
|
335
|
+
if (this.agentRegistry.get(snap.id)) {
|
|
336
|
+
this.agentRegistry.setStatus(snap.id, snap.status);
|
|
337
|
+
} else {
|
|
338
|
+
this.agentRegistry.register({
|
|
339
|
+
id: snap.id,
|
|
340
|
+
displayName: snap.displayName,
|
|
341
|
+
kind: snap.kind,
|
|
342
|
+
parentId: snap.parentId,
|
|
343
|
+
session: null,
|
|
344
|
+
status: snap.status,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
// Refs are returned by reference: patch host timestamps directly so
|
|
348
|
+
// hub age/activity columns reflect the host, not local registration.
|
|
349
|
+
const ref = this.agentRegistry.get(snap.id);
|
|
350
|
+
if (ref) {
|
|
351
|
+
ref.createdAt = snap.createdAt;
|
|
352
|
+
ref.lastActivity = snap.lastActivity;
|
|
353
|
+
ref.displayName = snap.displayName;
|
|
354
|
+
}
|
|
355
|
+
this.#agentHasTranscript.set(snap.id, snap.hasSessionFile);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
#clearAgentMirror(): void {
|
|
360
|
+
for (const ref of this.agentRegistry.list()) {
|
|
361
|
+
this.agentRegistry.unregister(ref.id);
|
|
362
|
+
}
|
|
363
|
+
this.#agentHasTranscript.clear();
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** Resolve every in-flight transcript request with null (resolvers clear their own timers). */
|
|
367
|
+
#flushPendingTranscripts(): void {
|
|
368
|
+
for (const resolve of this.#pendingTranscripts.values()) {
|
|
369
|
+
resolve(null);
|
|
370
|
+
}
|
|
371
|
+
this.#pendingTranscripts.clear();
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
#clearTransientUi(): void {
|
|
375
|
+
this.#ctx.statusContainer.clear();
|
|
376
|
+
this.#ctx.pendingMessagesContainer.clear();
|
|
377
|
+
this.#ctx.compactionQueuedMessages = [];
|
|
378
|
+
this.#ctx.streamingComponent = undefined;
|
|
379
|
+
this.#ctx.streamingMessage = undefined;
|
|
380
|
+
this.#ctx.pendingTools.clear();
|
|
381
|
+
if (this.#ctx.loadingAnimation) {
|
|
382
|
+
this.#ctx.loadingAnimation.stop();
|
|
383
|
+
this.#ctx.loadingAnimation = undefined;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async #restoreLocalSession(): Promise<void> {
|
|
388
|
+
if (this.#left) return;
|
|
389
|
+
this.#left = true;
|
|
390
|
+
this.#socket = null;
|
|
391
|
+
this.#ctx.collabGuest = undefined;
|
|
392
|
+
this.#ctx.statusLine.setCollabStatus(null);
|
|
393
|
+
this.#flushPendingTranscripts();
|
|
394
|
+
this.#clearAgentMirror();
|
|
395
|
+
this.#ctx.resetObserverRegistry();
|
|
396
|
+
this.#clearTransientUi();
|
|
397
|
+
// Replica file stays on disk: it is a valid session file outside the
|
|
398
|
+
// sessions dir, so it never shows up in /resume but remains readable.
|
|
399
|
+
if (this.#returnSessionFile) {
|
|
400
|
+
await this.#ctx.handleResumeSession(this.#returnSessionFile);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
await this.#ctx.session.newSession();
|
|
404
|
+
setSessionTerminalTitle(this.#ctx.sessionManager.getSessionName(), this.#ctx.sessionManager.getCwd());
|
|
405
|
+
this.#ctx.statusLine.invalidate();
|
|
406
|
+
this.#ctx.statusLine.setSessionStartTime(Date.now());
|
|
407
|
+
this.#ctx.updateEditorTopBorder();
|
|
408
|
+
this.#ctx.updateEditorBorderColor();
|
|
409
|
+
this.#ctx.renderInitialMessages({ clearTerminalHistory: true });
|
|
410
|
+
await this.#ctx.reloadTodos();
|
|
411
|
+
this.#ctx.ui.requestRender(true, { clearScrollback: true });
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
#updateStatusSegment(): void {
|
|
415
|
+
this.#ctx.statusLine.setCollabStatus({
|
|
416
|
+
role: "guest",
|
|
417
|
+
participantCount: this.state?.participants.length ?? 1,
|
|
418
|
+
stateOverride: this.state,
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
}
|