@linkshell/host 0.4.18 → 0.4.20
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 +14 -2
- package/dist/host/src/acp-settings.d.ts +12 -0
- package/dist/host/src/acp-settings.js +41 -0
- package/dist/host/src/acp-settings.js.map +1 -0
- package/dist/host/src/carry.js +13 -2
- package/dist/host/src/carry.js.map +1 -1
- package/dist/host/src/drivers/acp/client-services.d.ts +29 -0
- package/dist/host/src/drivers/acp/client-services.js +192 -0
- package/dist/host/src/drivers/acp/client-services.js.map +1 -0
- package/dist/host/src/drivers/acp/connection.d.ts +40 -7
- package/dist/host/src/drivers/acp/connection.js +132 -103
- package/dist/host/src/drivers/acp/connection.js.map +1 -1
- package/dist/host/src/drivers/acp/driver.d.ts +95 -2
- package/dist/host/src/drivers/acp/driver.js +628 -53
- package/dist/host/src/drivers/acp/driver.js.map +1 -1
- package/dist/host/src/drivers/acp/editor.d.ts +31 -0
- package/dist/host/src/drivers/acp/editor.js +242 -0
- package/dist/host/src/drivers/acp/editor.js.map +1 -0
- package/dist/host/src/drivers/acp/mapper.d.ts +2 -1
- package/dist/host/src/drivers/acp/mapper.js +88 -35
- package/dist/host/src/drivers/acp/mapper.js.map +1 -1
- package/dist/host/src/drivers/acp/mcp.d.ts +20 -0
- package/dist/host/src/drivers/acp/mcp.js +139 -0
- package/dist/host/src/drivers/acp/mcp.js.map +1 -0
- package/dist/host/src/drivers/acp/questions.js +14 -0
- package/dist/host/src/drivers/acp/questions.js.map +1 -1
- package/dist/host/src/drivers/acp/updates.d.ts +10 -0
- package/dist/host/src/drivers/acp/updates.js +137 -0
- package/dist/host/src/drivers/acp/updates.js.map +1 -0
- package/dist/host/src/drivers/claude/activity.d.ts +1 -0
- package/dist/host/src/drivers/claude/activity.js +17 -3
- package/dist/host/src/drivers/claude/activity.js.map +1 -1
- package/dist/host/src/drivers/claude/children.d.ts +7 -0
- package/dist/host/src/drivers/claude/children.js +31 -2
- package/dist/host/src/drivers/claude/children.js.map +1 -1
- package/dist/host/src/drivers/claude/driver.d.ts +4 -1
- package/dist/host/src/drivers/claude/driver.js +42 -1
- package/dist/host/src/drivers/claude/driver.js.map +1 -1
- package/dist/host/src/drivers/claude/tasks.d.ts +3 -3
- package/dist/host/src/drivers/claude/transcript.d.ts +497 -38
- package/dist/host/src/drivers/codex/archives.d.ts +2 -0
- package/dist/host/src/drivers/codex/archives.js +18 -0
- package/dist/host/src/drivers/codex/archives.js.map +1 -0
- package/dist/host/src/drivers/codex/attention.js +2 -2
- package/dist/host/src/drivers/codex/attention.js.map +1 -1
- package/dist/host/src/drivers/codex/driver.d.ts +4 -0
- package/dist/host/src/drivers/codex/driver.js +51 -2
- package/dist/host/src/drivers/codex/driver.js.map +1 -1
- package/dist/host/src/drivers/registry.d.ts +6 -0
- package/dist/host/src/drivers/registry.js +8 -0
- package/dist/host/src/drivers/registry.js.map +1 -1
- package/dist/host/src/drivers/types.d.ts +38 -1
- package/dist/host/src/fs.d.ts +3 -3
- package/dist/host/src/host.js +21 -3
- package/dist/host/src/host.js.map +1 -1
- package/dist/host/src/hub.d.ts +250 -2
- package/dist/host/src/hub.js +184 -13
- package/dist/host/src/hub.js.map +1 -1
- package/dist/host/src/input.d.ts +5 -3
- package/dist/host/src/input.js +8 -6
- package/dist/host/src/input.js.map +1 -1
- package/dist/host/src/questions.d.ts +3 -1
- package/dist/host/src/questions.js +33 -7
- package/dist/host/src/questions.js.map +1 -1
- package/dist/host/src/rpc/server.js +14 -0
- package/dist/host/src/rpc/server.js.map +1 -1
- package/dist/host/src/screen.js +5 -5
- package/dist/host/src/screen.js.map +1 -1
- package/dist/host/src/slim.js +15 -4
- package/dist/host/src/slim.js.map +1 -1
- package/dist/host/src/store.d.ts +2 -0
- package/dist/host/src/store.js +16 -2
- package/dist/host/src/store.js.map +1 -1
- package/dist/host/src/terminals.d.ts +8 -3
- package/dist/host/src/terminals.js +3 -2
- package/dist/host/src/terminals.js.map +1 -1
- package/dist/host/tsconfig.tsbuildinfo +1 -1
- package/dist/wire/src/acp.d.ts +510 -0
- package/dist/wire/src/acp.js +39 -0
- package/dist/wire/src/acp.js.map +1 -0
- package/dist/wire/src/async-questions.d.ts +2 -2
- package/dist/wire/src/computer-preview.d.ts +2 -2
- package/dist/wire/src/content.d.ts +465 -0
- package/dist/wire/src/content.js +58 -0
- package/dist/wire/src/content.js.map +1 -0
- package/dist/wire/src/editor.d.ts +702 -0
- package/dist/wire/src/editor.js +23 -0
- package/dist/wire/src/editor.js.map +1 -0
- package/dist/wire/src/index.d.ts +3 -0
- package/dist/wire/src/index.js +3 -0
- package/dist/wire/src/index.js.map +1 -1
- package/dist/wire/src/model.d.ts +1383 -79
- package/dist/wire/src/model.js +20 -1
- package/dist/wire/src/model.js.map +1 -1
- package/dist/wire/src/peer.d.ts +1 -0
- package/dist/wire/src/peer.js +22 -6
- package/dist/wire/src/peer.js.map +1 -1
- package/dist/wire/src/questions.d.ts +3 -0
- package/dist/wire/src/questions.js +65 -0
- package/dist/wire/src/questions.js.map +1 -0
- package/dist/wire/src/relay.d.ts +4 -4
- package/dist/wire/src/rpc.d.ts +19023 -2518
- package/dist/wire/src/rpc.js +41 -3
- package/dist/wire/src/rpc.js.map +1 -1
- package/dist/wire/src/updates.d.ts +3848 -1114
- package/dist/wire/src/updates.js +42 -40
- package/dist/wire/src/updates.js.map +1 -1
- package/dist/wire/src/workflow.d.ts +22 -22
- package/package.json +4 -3
package/dist/host/src/hub.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RpcError, questionReplies, updateAsyncQuestions, sessionIdFor, } from "@linkshell/wire";
|
|
1
|
+
import { RpcError, questionReplies, questionAnswerError, updateAsyncQuestions, sessionIdFor, } from "@linkshell/wire";
|
|
2
2
|
import { inOrder } from "./drivers/acp/driver.js";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
@@ -91,10 +91,13 @@ export class SessionHub {
|
|
|
91
91
|
store;
|
|
92
92
|
log;
|
|
93
93
|
home;
|
|
94
|
+
acpServices;
|
|
94
95
|
drivers = new Map();
|
|
95
96
|
live = new Map();
|
|
96
97
|
summaryListeners = new Set();
|
|
97
98
|
removedListeners = new Set();
|
|
99
|
+
interactionListeners = new Set();
|
|
100
|
+
agentListeners = new Set();
|
|
98
101
|
auth = new Map();
|
|
99
102
|
/** Terminals (`linkshell <agent>`) currently driving handoff sessions. */
|
|
100
103
|
desktops = new Map();
|
|
@@ -104,6 +107,29 @@ export class SessionHub {
|
|
|
104
107
|
authRefreshing;
|
|
105
108
|
previews;
|
|
106
109
|
driverHost = {
|
|
110
|
+
authChanged: (agent) => {
|
|
111
|
+
const driver = this.drivers.get(agent);
|
|
112
|
+
if (!driver)
|
|
113
|
+
return;
|
|
114
|
+
void this.refreshAuth(driver).then(() => { const info = this.agents().find((entry) => entry.id === agent); if (info)
|
|
115
|
+
for (const listener of this.agentListeners)
|
|
116
|
+
listener(info); }).catch(() => { });
|
|
117
|
+
},
|
|
118
|
+
agentSettings: (agent) => this.acpServices.settings.settings(agent),
|
|
119
|
+
loginTerminal: async (agent, launch) => {
|
|
120
|
+
if (!this.acpServices)
|
|
121
|
+
throw RpcError.app("not_supported", "终端登录不可用");
|
|
122
|
+
return this.acpServices.loginTerminal(agent, launch);
|
|
123
|
+
},
|
|
124
|
+
interaction: (agent, request) => { for (const listener of this.interactionListeners)
|
|
125
|
+
listener(agent, request); },
|
|
126
|
+
notice: (agent, nativeId, notice) => {
|
|
127
|
+
const live = this.live.get(sessionIdFor(agent, nativeId));
|
|
128
|
+
if (!live || live.importing)
|
|
129
|
+
return;
|
|
130
|
+
for (const subscriber of live.subscribers)
|
|
131
|
+
subscriber.notice?.(notice);
|
|
132
|
+
},
|
|
107
133
|
tasks: (agent, nativeId) => {
|
|
108
134
|
const id = sessionIdFor(agent, nativeId);
|
|
109
135
|
return this.store.getSession(id) ? this.tasks(id) : [];
|
|
@@ -151,10 +177,13 @@ export class SessionHub {
|
|
|
151
177
|
};
|
|
152
178
|
constructor(store, drivers, log = () => { },
|
|
153
179
|
/** Where the host keeps its own files: worktrees go under it. */
|
|
154
|
-
home = join(homedir(), ".linkshell")) {
|
|
180
|
+
home = join(homedir(), ".linkshell"), acpServices) {
|
|
155
181
|
this.store = store;
|
|
156
182
|
this.log = log;
|
|
157
183
|
this.home = home;
|
|
184
|
+
this.acpServices = acpServices;
|
|
185
|
+
if (!acpServices)
|
|
186
|
+
this.driverHost.agentSettings = undefined;
|
|
158
187
|
const capture = process.platform === "darwin" ? new MacPreviewCapture(log) : undefined;
|
|
159
188
|
this.previews = new ComputerPreviews({
|
|
160
189
|
load: (id) => store.getDriverState(id, "computer-preview"),
|
|
@@ -167,6 +196,8 @@ export class SessionHub {
|
|
|
167
196
|
this.drivers.set(driver.id, driver);
|
|
168
197
|
}
|
|
169
198
|
async start(options = {}) {
|
|
199
|
+
for (const id of this.store.resetLiveSessions())
|
|
200
|
+
this.pendingQuestions.delete(id);
|
|
170
201
|
await Promise.all([...this.drivers.values()].map(async (driver) => {
|
|
171
202
|
try {
|
|
172
203
|
const status = await driver.start(this.driverHost);
|
|
@@ -177,6 +208,7 @@ export class SessionHub {
|
|
|
177
208
|
return;
|
|
178
209
|
for (const session of await driver.listSessions(100))
|
|
179
210
|
this.recordDiscovered(driver.id, session);
|
|
211
|
+
await this.refreshArchives(driver);
|
|
180
212
|
}
|
|
181
213
|
catch (error) {
|
|
182
214
|
this.log(`[hub] ${driver.id} failed to start: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -198,12 +230,25 @@ export class SessionHub {
|
|
|
198
230
|
try {
|
|
199
231
|
for (const session of await driver.listSessions(limit))
|
|
200
232
|
this.recordDiscovered(driver.id, session);
|
|
233
|
+
await this.refreshArchives(driver);
|
|
201
234
|
}
|
|
202
235
|
catch (error) {
|
|
203
236
|
this.log(`[hub] ${driver.id} discovery failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
204
237
|
}
|
|
205
238
|
}));
|
|
206
239
|
}
|
|
240
|
+
async refreshArchives(driver) {
|
|
241
|
+
for (const nativeId of await driver.archivedSessions?.() ?? []) {
|
|
242
|
+
const before = this.store.getSession(sessionIdFor(driver.id, nativeId));
|
|
243
|
+
// Only reconcile sessions already known here; never import the entire native archive.
|
|
244
|
+
if (!before || (before.archived && before.state === "idle"))
|
|
245
|
+
continue;
|
|
246
|
+
this.recordDiscovered(driver.id, {
|
|
247
|
+
nativeId, cwd: before.cwd, createdAt: before.createdAt, updatedAt: before.updatedAt,
|
|
248
|
+
archived: true, state: "idle", asyncQuestions: [],
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
207
252
|
async stop() {
|
|
208
253
|
if (this.discoveryTimer)
|
|
209
254
|
clearInterval(this.discoveryTimer);
|
|
@@ -225,6 +270,85 @@ export class SessionHub {
|
|
|
225
270
|
};
|
|
226
271
|
});
|
|
227
272
|
}
|
|
273
|
+
onInteraction(listener) {
|
|
274
|
+
this.interactionListeners.add(listener);
|
|
275
|
+
return () => { this.interactionListeners.delete(listener); };
|
|
276
|
+
}
|
|
277
|
+
onAgent(listener) { this.agentListeners.add(listener); return () => { this.agentListeners.delete(listener); }; }
|
|
278
|
+
async acpInfo(agent, sessionId) {
|
|
279
|
+
const driver = this.requireDriver(agent);
|
|
280
|
+
if (!driver.acpInfo)
|
|
281
|
+
throw RpcError.app("not_supported", "这个 AI 不使用 ACP");
|
|
282
|
+
const session = sessionId ? this.getSession(sessionId) : undefined;
|
|
283
|
+
if (session && session.agent !== agent)
|
|
284
|
+
throw RpcError.app("invalid_params", "会话与 AI 不匹配");
|
|
285
|
+
return driver.acpInfo(session?.nativeId);
|
|
286
|
+
}
|
|
287
|
+
pendingInteractions() { return [...this.drivers.values()].flatMap((driver) => (driver.pendingInteractions?.() ?? []).map((request) => ({ agent: driver.id, request }))); }
|
|
288
|
+
async editor(input) {
|
|
289
|
+
const driver = this.requireDriver(input.agent);
|
|
290
|
+
if (!driver.editor)
|
|
291
|
+
throw RpcError.app("not_supported", "这个 AI 不支持编辑建议");
|
|
292
|
+
return driver.editor(input);
|
|
293
|
+
}
|
|
294
|
+
async configureAcp(agent, settings, sessionId) {
|
|
295
|
+
const driver = this.requireDriver(agent);
|
|
296
|
+
if (!driver.configureAcp || !this.acpServices)
|
|
297
|
+
throw RpcError.app("not_supported", "这个 AI 不支持 ACP 配置");
|
|
298
|
+
const session = sessionId ? this.getSession(sessionId) : undefined;
|
|
299
|
+
if (session && session.agent !== agent)
|
|
300
|
+
throw RpcError.app("invalid_params", "会话与 AI 不匹配");
|
|
301
|
+
await driver.configureAcp(settings, session?.nativeId);
|
|
302
|
+
if (!session)
|
|
303
|
+
this.acpServices.settings.setSettings(agent, settings);
|
|
304
|
+
}
|
|
305
|
+
async authenticate(agent, methodId, logout) {
|
|
306
|
+
const driver = this.requireDriver(agent);
|
|
307
|
+
if (!driver.authenticate)
|
|
308
|
+
throw RpcError.app("not_supported", "这个 AI 不支持 ACP 登录");
|
|
309
|
+
const result = await driver.authenticate(methodId, logout);
|
|
310
|
+
await this.refreshAuth(driver);
|
|
311
|
+
return result;
|
|
312
|
+
}
|
|
313
|
+
async respondInteraction(agent, requestId, response) {
|
|
314
|
+
const driver = this.requireDriver(agent);
|
|
315
|
+
if (!driver.respondInteraction)
|
|
316
|
+
throw RpcError.app("not_supported", "这个 AI 不支持此交互");
|
|
317
|
+
await driver.respondInteraction(requestId, response);
|
|
318
|
+
}
|
|
319
|
+
async providers(agent, operation, config) {
|
|
320
|
+
const driver = this.requireDriver(agent);
|
|
321
|
+
if (!driver.providers)
|
|
322
|
+
throw RpcError.app("not_supported", "这个 AI 不支持供应商配置");
|
|
323
|
+
return driver.providers(operation, config);
|
|
324
|
+
}
|
|
325
|
+
async customAgents(save, remove) {
|
|
326
|
+
const services = this.acpServices;
|
|
327
|
+
if (!services)
|
|
328
|
+
throw RpcError.app("not_supported", "自定义 ACP 配置不可用");
|
|
329
|
+
if (save && remove)
|
|
330
|
+
throw RpcError.app("invalid_params", "不能同时保存和移除 Agent");
|
|
331
|
+
const id = save?.id ?? remove;
|
|
332
|
+
if (id) {
|
|
333
|
+
const existing = this.drivers.get(id);
|
|
334
|
+
if (existing && !services.settings.agents().some((agent) => agent.id === id))
|
|
335
|
+
throw RpcError.app("invalid_params", "不能覆盖或移除内置 AI");
|
|
336
|
+
const driver = save ? services.createDriver(save) : undefined;
|
|
337
|
+
if (existing) {
|
|
338
|
+
await existing.configureAcp?.(services.settings.settings(id));
|
|
339
|
+
await existing.stop();
|
|
340
|
+
this.drivers.delete(id);
|
|
341
|
+
}
|
|
342
|
+
if (save && driver) {
|
|
343
|
+
services.settings.saveAgent(save);
|
|
344
|
+
this.drivers.set(id, driver);
|
|
345
|
+
await driver.start(this.driverHost);
|
|
346
|
+
}
|
|
347
|
+
else
|
|
348
|
+
services.settings.removeAgent(id);
|
|
349
|
+
}
|
|
350
|
+
return services.settings.agents();
|
|
351
|
+
}
|
|
228
352
|
/** Re-checks login state in the background when the cached answer is older than `maxAgeMs`. */
|
|
229
353
|
refreshAuthIfStale(maxAgeMs = 60_000) {
|
|
230
354
|
if (this.authRefreshing)
|
|
@@ -336,9 +460,12 @@ export class SessionHub {
|
|
|
336
460
|
if (first) {
|
|
337
461
|
decorated.permission = {
|
|
338
462
|
requestId: first.requestId,
|
|
463
|
+
childSessionId: first.childSessionId,
|
|
339
464
|
toolCallId: first.toolCallId,
|
|
340
465
|
title: first.title,
|
|
341
466
|
detail: first.detail,
|
|
467
|
+
tool: first.tool,
|
|
468
|
+
url: first.url,
|
|
342
469
|
options: first.options,
|
|
343
470
|
questions: first.questions,
|
|
344
471
|
};
|
|
@@ -511,9 +638,13 @@ export class SessionHub {
|
|
|
511
638
|
// Serve the log we have; not_ready sessions attach once they get a turn.
|
|
512
639
|
if (!(error instanceof RpcError && error.appCode === "not_ready")) {
|
|
513
640
|
this.log(`[hub] attach ${sessionId} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
641
|
+
this.commit(sessionId, { sessionUpdate: "ls_status", state: "error" });
|
|
642
|
+
this.commit(sessionId, {
|
|
643
|
+
sessionUpdate: "ls_error",
|
|
644
|
+
code: error instanceof RpcError ? error.appCode ?? "history_unavailable" : "history_unavailable",
|
|
645
|
+
message: `读取会话消息失败:${error instanceof Error ? error.message : String(error)}`,
|
|
646
|
+
hint: "重新打开会话可重试",
|
|
647
|
+
}, undefined, summary.updatedAt);
|
|
517
648
|
}
|
|
518
649
|
}
|
|
519
650
|
}
|
|
@@ -654,6 +785,8 @@ export class SessionHub {
|
|
|
654
785
|
task: detail?.task ?? call.title,
|
|
655
786
|
agentType: detail?.agentType,
|
|
656
787
|
name: detail?.name,
|
|
788
|
+
nativeSessionId: detail?.nativeSessionId,
|
|
789
|
+
canCancel: detail?.canCancel,
|
|
657
790
|
running,
|
|
658
791
|
failed: (workflow?.state ? workflow.state === "failed" : detail?.state ? detail.state === "failed" : state.status === "failed") || undefined,
|
|
659
792
|
startedAt: workflow?.startedAt ?? event.ts,
|
|
@@ -665,6 +798,13 @@ export class SessionHub {
|
|
|
665
798
|
}
|
|
666
799
|
return [...agents.values()].reverse();
|
|
667
800
|
}
|
|
801
|
+
async cancelSubagent(sessionId, nativeSessionId) {
|
|
802
|
+
const session = this.getSession(sessionId), driver = this.requireDriver(session.agent);
|
|
803
|
+
if (!driver.cancelSubagent)
|
|
804
|
+
throw RpcError.app("not_supported", "这个 AI 不支持单独停止子代理");
|
|
805
|
+
await this.ensureAttached(sessionId);
|
|
806
|
+
await driver.cancelSubagent(session.nativeId, nativeSessionId);
|
|
807
|
+
}
|
|
668
808
|
subagentsOf(sessionId, live) {
|
|
669
809
|
if (!live.subagents) {
|
|
670
810
|
live.subagents = new Map();
|
|
@@ -1012,9 +1152,11 @@ export class SessionHub {
|
|
|
1012
1152
|
const other = question.other ? answer.other?.trim() || undefined : undefined;
|
|
1013
1153
|
return [{ id: question.id, values, other }];
|
|
1014
1154
|
});
|
|
1015
|
-
const
|
|
1016
|
-
|
|
1017
|
-
|
|
1155
|
+
for (const question of request.questions) {
|
|
1156
|
+
const error = questionAnswerError(question, kept.find((answer) => answer.id === question.id));
|
|
1157
|
+
if (error)
|
|
1158
|
+
throw RpcError.app("invalid_params", error);
|
|
1159
|
+
}
|
|
1018
1160
|
await driver.answerQuestion(summary.nativeId, requestId, kept);
|
|
1019
1161
|
}
|
|
1020
1162
|
async desktopLaunch(agent, args, sessionId, context) {
|
|
@@ -1153,7 +1295,7 @@ export class SessionHub {
|
|
|
1153
1295
|
if (live.importChanged && stored)
|
|
1154
1296
|
this.emitSummary(stored);
|
|
1155
1297
|
}
|
|
1156
|
-
live.attached =
|
|
1298
|
+
live.attached = !this.store.getSession(sessionId)?.archived;
|
|
1157
1299
|
}
|
|
1158
1300
|
finally {
|
|
1159
1301
|
const buffered = live.buffer ?? [];
|
|
@@ -1183,7 +1325,7 @@ export class SessionHub {
|
|
|
1183
1325
|
session = { ...session, updatedAt: before.updatedAt };
|
|
1184
1326
|
}
|
|
1185
1327
|
}
|
|
1186
|
-
const
|
|
1328
|
+
const stored = this.store.upsertSession({
|
|
1187
1329
|
id: sessionIdFor(agent, session.nativeId),
|
|
1188
1330
|
agent,
|
|
1189
1331
|
nativeId: session.nativeId,
|
|
@@ -1195,9 +1337,14 @@ export class SessionHub {
|
|
|
1195
1337
|
createdAt: session.createdAt,
|
|
1196
1338
|
updatedAt: session.updatedAt,
|
|
1197
1339
|
});
|
|
1340
|
+
const { created } = stored;
|
|
1341
|
+
const summary = session.archived !== undefined && session.archived !== stored.summary.archived
|
|
1342
|
+
? this.store.patchSession(stored.summary.id, { archived: session.archived })
|
|
1343
|
+
: stored.summary;
|
|
1198
1344
|
const changed = !!before &&
|
|
1199
1345
|
(before.title !== summary.title ||
|
|
1200
1346
|
before.state !== summary.state ||
|
|
1347
|
+
before.archived !== summary.archived ||
|
|
1201
1348
|
before.updatedAt !== summary.updatedAt ||
|
|
1202
1349
|
before.cwd !== summary.cwd ||
|
|
1203
1350
|
before.model !== summary.model);
|
|
@@ -1242,6 +1389,8 @@ export class SessionHub {
|
|
|
1242
1389
|
// A finished turn can't still be waiting on approvals; clear stale cards.
|
|
1243
1390
|
if (update.sessionUpdate === "ls_turn" && update.state === "ended" && !update.parentToolCallId) {
|
|
1244
1391
|
for (const requestId of [...live.permissions.keys()]) {
|
|
1392
|
+
if (live.permissions.get(requestId)?.childSessionId)
|
|
1393
|
+
continue;
|
|
1245
1394
|
this.commit(sessionId, { sessionUpdate: "ls_permission_resolved", requestId });
|
|
1246
1395
|
}
|
|
1247
1396
|
if (!live.importing)
|
|
@@ -1278,6 +1427,18 @@ export class SessionHub {
|
|
|
1278
1427
|
activityChanged = true;
|
|
1279
1428
|
};
|
|
1280
1429
|
switch (update.sessionUpdate) {
|
|
1430
|
+
case "ls_message": {
|
|
1431
|
+
if (update.parentToolCallId || !update.content)
|
|
1432
|
+
break;
|
|
1433
|
+
const text = update.content.filter((block) => block.type === "text").map((block) => block.text).join("");
|
|
1434
|
+
if (update.role === "user" && !before.title && text.trim())
|
|
1435
|
+
patch.title = compact(text, TITLE_LENGTH);
|
|
1436
|
+
if (update.role === "agent" && text.trim())
|
|
1437
|
+
patch.preview = compact(text, PREVIEW_LENGTH);
|
|
1438
|
+
if (live.turnActive && update.role === "agent")
|
|
1439
|
+
setActivity({ kind: "responding" });
|
|
1440
|
+
break;
|
|
1441
|
+
}
|
|
1281
1442
|
case "user_message_chunk": {
|
|
1282
1443
|
const text = update.content.type === "text" ? update.content.text : "";
|
|
1283
1444
|
if (text && !before.title)
|
|
@@ -1315,10 +1476,15 @@ export class SessionHub {
|
|
|
1315
1476
|
live.turnActive = update.state === "started";
|
|
1316
1477
|
setActivity(live.turnActive ? { kind: "thinking" } : undefined);
|
|
1317
1478
|
// Leftover permissions are cleared right after a turn ends (see commit).
|
|
1318
|
-
patch.state = live.turnActive ? "running" : update.stopReason === "error" ? "error" : "idle";
|
|
1479
|
+
patch.state = live.turnActive ? "running" : [...live.permissions.values()].some((permission) => permission.childSessionId) ? "waiting" : update.stopReason === "error" ? "error" : "idle";
|
|
1319
1480
|
break;
|
|
1320
1481
|
case "ls_status":
|
|
1321
1482
|
patch.state = update.state;
|
|
1483
|
+
if (!live.importing) {
|
|
1484
|
+
live.turnActive = update.turnActive ?? (update.state === "running" || update.state === "waiting");
|
|
1485
|
+
if (!live.turnActive)
|
|
1486
|
+
setActivity(undefined);
|
|
1487
|
+
}
|
|
1322
1488
|
break;
|
|
1323
1489
|
case "ls_permission":
|
|
1324
1490
|
live.permissions.set(update.requestId, update);
|
|
@@ -1332,8 +1498,13 @@ export class SessionHub {
|
|
|
1332
1498
|
patch.state = live.turnActive ? "running" : "idle";
|
|
1333
1499
|
break;
|
|
1334
1500
|
case "session_info_update":
|
|
1335
|
-
if (update.title)
|
|
1336
|
-
patch.title = compact(update.title, TITLE_LENGTH);
|
|
1501
|
+
if (update.title !== undefined)
|
|
1502
|
+
patch.title = update.title === null ? null : compact(update.title, TITLE_LENGTH);
|
|
1503
|
+
if (update.updatedAt !== undefined) {
|
|
1504
|
+
const updatedAt = update.updatedAt === null ? before.createdAt : Date.parse(update.updatedAt);
|
|
1505
|
+
if (Number.isFinite(updatedAt))
|
|
1506
|
+
patch.updatedAt = updatedAt;
|
|
1507
|
+
}
|
|
1337
1508
|
if (update.model)
|
|
1338
1509
|
patch.model = update.model;
|
|
1339
1510
|
break;
|