linkshell-cli 0.10.19 → 0.10.21

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.
Files changed (108) hide show
  1. package/README.md +3 -2
  2. package/dist/cli/src/commands/screen.js +2 -2
  3. package/dist/cli/src/commands/screen.js.map +1 -1
  4. package/dist/cli/tsconfig.tsbuildinfo +1 -1
  5. package/dist/host/src/acp-settings.d.ts +12 -0
  6. package/dist/host/src/acp-settings.js +41 -0
  7. package/dist/host/src/acp-settings.js.map +1 -0
  8. package/dist/host/src/carry.js +13 -2
  9. package/dist/host/src/carry.js.map +1 -1
  10. package/dist/host/src/drivers/acp/client-services.d.ts +29 -0
  11. package/dist/host/src/drivers/acp/client-services.js +192 -0
  12. package/dist/host/src/drivers/acp/client-services.js.map +1 -0
  13. package/dist/host/src/drivers/acp/connection.d.ts +37 -8
  14. package/dist/host/src/drivers/acp/connection.js +127 -110
  15. package/dist/host/src/drivers/acp/connection.js.map +1 -1
  16. package/dist/host/src/drivers/acp/driver.d.ts +92 -2
  17. package/dist/host/src/drivers/acp/driver.js +599 -52
  18. package/dist/host/src/drivers/acp/driver.js.map +1 -1
  19. package/dist/host/src/drivers/acp/editor.d.ts +31 -0
  20. package/dist/host/src/drivers/acp/editor.js +242 -0
  21. package/dist/host/src/drivers/acp/editor.js.map +1 -0
  22. package/dist/host/src/drivers/acp/mapper.d.ts +2 -1
  23. package/dist/host/src/drivers/acp/mapper.js +88 -35
  24. package/dist/host/src/drivers/acp/mapper.js.map +1 -1
  25. package/dist/host/src/drivers/acp/mcp.d.ts +20 -0
  26. package/dist/host/src/drivers/acp/mcp.js +139 -0
  27. package/dist/host/src/drivers/acp/mcp.js.map +1 -0
  28. package/dist/host/src/drivers/acp/questions.js +14 -0
  29. package/dist/host/src/drivers/acp/questions.js.map +1 -1
  30. package/dist/host/src/drivers/acp/updates.d.ts +10 -0
  31. package/dist/host/src/drivers/acp/updates.js +137 -0
  32. package/dist/host/src/drivers/acp/updates.js.map +1 -0
  33. package/dist/host/src/drivers/claude/activity.d.ts +1 -0
  34. package/dist/host/src/drivers/claude/activity.js +17 -3
  35. package/dist/host/src/drivers/claude/activity.js.map +1 -1
  36. package/dist/host/src/drivers/claude/children.d.ts +7 -0
  37. package/dist/host/src/drivers/claude/children.js +31 -2
  38. package/dist/host/src/drivers/claude/children.js.map +1 -1
  39. package/dist/host/src/drivers/claude/driver.js +1 -0
  40. package/dist/host/src/drivers/claude/driver.js.map +1 -1
  41. package/dist/host/src/drivers/claude/tasks.d.ts +3 -3
  42. package/dist/host/src/drivers/claude/transcript.d.ts +497 -38
  43. package/dist/host/src/drivers/codex/driver.d.ts +3 -0
  44. package/dist/host/src/drivers/codex/driver.js +36 -1
  45. package/dist/host/src/drivers/codex/driver.js.map +1 -1
  46. package/dist/host/src/drivers/registry.d.ts +6 -0
  47. package/dist/host/src/drivers/registry.js +8 -0
  48. package/dist/host/src/drivers/registry.js.map +1 -1
  49. package/dist/host/src/drivers/types.d.ts +34 -1
  50. package/dist/host/src/fs.d.ts +3 -3
  51. package/dist/host/src/host.js +21 -3
  52. package/dist/host/src/host.js.map +1 -1
  53. package/dist/host/src/hub.d.ts +249 -2
  54. package/dist/host/src/hub.js +150 -9
  55. package/dist/host/src/hub.js.map +1 -1
  56. package/dist/host/src/input.d.ts +5 -3
  57. package/dist/host/src/input.js +8 -6
  58. package/dist/host/src/input.js.map +1 -1
  59. package/dist/host/src/questions.d.ts +3 -1
  60. package/dist/host/src/questions.js +33 -7
  61. package/dist/host/src/questions.js.map +1 -1
  62. package/dist/host/src/rpc/server.js +14 -0
  63. package/dist/host/src/rpc/server.js.map +1 -1
  64. package/dist/host/src/screen-metrics.d.ts +3 -0
  65. package/dist/host/src/screen-metrics.js +27 -0
  66. package/dist/host/src/screen-metrics.js.map +1 -0
  67. package/dist/host/src/screen.js +7 -6
  68. package/dist/host/src/screen.js.map +1 -1
  69. package/dist/host/src/slim.js +15 -4
  70. package/dist/host/src/slim.js.map +1 -1
  71. package/dist/host/src/store.js +7 -2
  72. package/dist/host/src/store.js.map +1 -1
  73. package/dist/host/src/terminals.d.ts +8 -3
  74. package/dist/host/src/terminals.js +3 -2
  75. package/dist/host/src/terminals.js.map +1 -1
  76. package/dist/wire/src/acp.d.ts +510 -0
  77. package/dist/wire/src/acp.js +39 -0
  78. package/dist/wire/src/acp.js.map +1 -0
  79. package/dist/wire/src/async-questions.d.ts +2 -2
  80. package/dist/wire/src/computer-preview.d.ts +2 -2
  81. package/dist/wire/src/content.d.ts +465 -0
  82. package/dist/wire/src/content.js +58 -0
  83. package/dist/wire/src/content.js.map +1 -0
  84. package/dist/wire/src/editor.d.ts +702 -0
  85. package/dist/wire/src/editor.js +23 -0
  86. package/dist/wire/src/editor.js.map +1 -0
  87. package/dist/wire/src/index.d.ts +3 -0
  88. package/dist/wire/src/index.js +3 -0
  89. package/dist/wire/src/index.js.map +1 -1
  90. package/dist/wire/src/model.d.ts +1383 -79
  91. package/dist/wire/src/model.js +20 -1
  92. package/dist/wire/src/model.js.map +1 -1
  93. package/dist/wire/src/peer.d.ts +1 -0
  94. package/dist/wire/src/peer.js +22 -6
  95. package/dist/wire/src/peer.js.map +1 -1
  96. package/dist/wire/src/questions.d.ts +3 -0
  97. package/dist/wire/src/questions.js +65 -0
  98. package/dist/wire/src/questions.js.map +1 -0
  99. package/dist/wire/src/relay.d.ts +4 -4
  100. package/dist/wire/src/rpc.d.ts +19023 -2518
  101. package/dist/wire/src/rpc.js +41 -3
  102. package/dist/wire/src/rpc.js.map +1 -1
  103. package/dist/wire/src/updates.d.ts +3848 -1114
  104. package/dist/wire/src/updates.js +42 -40
  105. package/dist/wire/src/updates.js.map +1 -1
  106. package/dist/wire/src/workflow.d.ts +22 -22
  107. package/package.json +4 -4
  108. package/src/commands/screen.ts +2 -2
@@ -1,6 +1,14 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
- import { RpcError, } from "@linkshell/wire";
3
+ import { createHash } from "node:crypto";
4
+ import { realpath, stat } from "node:fs/promises";
5
+ import { isAbsolute } from "node:path";
6
+ import { pathToFileURL } from "node:url";
7
+ import { RpcError, acpAgentSettingsSchema, acpProviderSchema, questionAnswerError, } from "@linkshell/wire";
8
+ import { AcpClientServices } from "./client-services.js";
9
+ import { AcpUpdates, usageUpdate } from "./updates.js";
10
+ import { AcpMcpBridge } from "./mcp.js";
11
+ import { AcpEditor } from "./editor.js";
4
12
  import { AcpConnection, AcpProtocolError } from "./connection.js";
5
13
  import { mapAcpQuestions, questionMethod } from "./questions.js";
6
14
  import { AcpItemTracker, mapPermissionRequest, normalizeAcpUpdate, toAcpPrompt, withContext, toConfigOptions, toHistory, toStopReason, } from "./mapper.js";
@@ -39,6 +47,7 @@ export class AcpDriver {
39
47
  label;
40
48
  tier;
41
49
  capabilities = {
50
+ acp: true,
42
51
  interrupt: true,
43
52
  steer: false,
44
53
  permissions: true,
@@ -58,12 +67,50 @@ export class AcpDriver {
58
67
  stopped = false;
59
68
  nextPermissionId = 1;
60
69
  authenticating;
70
+ credentialsChanged = false;
71
+ services;
72
+ mcp;
73
+ edits;
74
+ serviceRequests = new Map();
75
+ terminalCalls = new Map();
76
+ interactions = new Map();
77
+ children = new Map();
78
+ discoveredDirectories = new Map();
61
79
  constructor(spec, options) {
62
80
  this.spec = spec;
63
81
  this.options = options;
64
82
  this.id = spec.id;
65
83
  this.label = spec.label;
66
84
  this.tier = spec.tier;
85
+ this.services = new AcpClientServices({ env: this.env,
86
+ scope: (id) => { const state = this.sessions.get(id); return state ? { cwd: state.cwd, additionalDirectories: state.settings.additionalDirectories } : undefined; },
87
+ output: (id, terminalId, text, snapshot) => {
88
+ for (const call of this.terminalCalls.get(`${id}\n${terminalId}`) ?? []) {
89
+ // The first terminal reference carries the backlog; later output is already a delta.
90
+ if (text)
91
+ this.emit(id, { sessionUpdate: "tool_call_update", toolCallId: call, appendOutput: text });
92
+ if (snapshot.exitStatus)
93
+ this.emit(id, { sessionUpdate: "tool_call_update", toolCallId: call, rawOutput: { truncated: snapshot.truncated, ...snapshot.exitStatus } });
94
+ }
95
+ },
96
+ });
97
+ this.mcp = new AcpMcpBridge({ env: this.env,
98
+ notify: (params) => this.connection?.notify("mcp/message", params),
99
+ request: async (sessionId, method, params, id) => {
100
+ if (method === "ping")
101
+ return {};
102
+ const state = sessionId ? this.sessions.get(sessionId) : undefined;
103
+ if (method === "roots/list" && state)
104
+ return { roots: [state.cwd, ...state.settings.additionalDirectories].map((path) => ({ uri: pathToFileURL(path).href, name: path })) };
105
+ if (method === "elicitation/create" && state)
106
+ return this.onQuestions(method, { mode: "form", ...params, sessionId }, id);
107
+ throw new RpcError(-32601, `MCP 客户端不支持 ${method}`);
108
+ },
109
+ });
110
+ this.edits = new AcpEditor({ capabilities: () => this.connection?.capabilities ?? {}, settings: () => this.settings(),
111
+ request: (method, params) => this.connection?.request(method, params, method === "nes/close" ? 5000 : 30_000) ?? Promise.reject(RpcError.app("offline", "ACP 连接已关闭")),
112
+ notify: (method, params) => this.connection?.notify(method, params),
113
+ });
67
114
  }
68
115
  status() {
69
116
  return this.current;
@@ -95,8 +142,173 @@ export class AcpDriver {
95
142
  clearTimeout(this.restartTimer);
96
143
  for (const state of this.sessions.values())
97
144
  this.cancelPermissions(state);
145
+ for (const id of [...this.interactions.keys()])
146
+ this.resolveInteraction(id, "cancel");
147
+ await this.edits.close();
98
148
  await this.connection?.stop();
99
149
  this.connection = undefined;
150
+ for (const controller of this.serviceRequests.values())
151
+ controller.abort();
152
+ this.serviceRequests.clear();
153
+ this.terminalCalls.clear();
154
+ await this.services.close();
155
+ await this.mcp.close();
156
+ }
157
+ async acpInfo(nativeId) {
158
+ await this.ensureStarted();
159
+ if (!this.connection?.alive)
160
+ throw RpcError.app("agent_unavailable", this.current.problem ?? "ACP 启动失败");
161
+ const caps = this.connection.capabilities;
162
+ return { features: {
163
+ protocolVersion: this.connection.protocolVersion,
164
+ authMethods: (this.connection.initializeResult.authMethods ?? []).filter((method) => !method.type || ["terminal", "agent"].includes(method.type)).map((method) => ({ id: method.id, name: method.name ?? method.id, description: method.description ?? undefined, type: method.type === "terminal" ? "terminal" : "agent" })),
165
+ logout: this.connection.protocolVersion === 2 ? caps.auth !== undefined : Boolean(caps.auth?.logout), providers: Boolean(caps.providers) && this.settings().experimental,
166
+ nes: Boolean(caps.nes) && this.settings().experimental,
167
+ additionalDirectories: Boolean(caps.sessionCapabilities?.additionalDirectories), mcpHttp: caps.mcpCapabilities?.http === true, mcpSse: caps.mcpCapabilities?.sse === true,
168
+ }, settings: nativeId ? this.sessions.get(nativeId)?.settings ?? this.settings() : this.settings(), interactions: this.pendingInteractions() };
169
+ }
170
+ async editor(input) { await this.ensureStarted(); return this.edits.run(input); }
171
+ pendingInteractions() { return [...this.interactions.values()].map((entry) => entry.request.update); }
172
+ async configureAcp(settings, nativeId) {
173
+ settings = acpAgentSettingsSchema.parse(settings);
174
+ await this.validateDirectories(settings);
175
+ const affected = nativeId ? [this.sessions.get(nativeId)].filter((state) => !!state) : [...this.sessions.values()];
176
+ if (affected.some((state) => state.turnActive || state.inflight || state.permissions.size || state.questions.size) || this.interactions.size)
177
+ throw RpcError.app("busy", "请先结束运行和待处理的交互,再修改 ACP 配置");
178
+ if (nativeId) {
179
+ if (settings.protocolVersion !== this.settings().protocolVersion || settings.experimental !== this.settings().experimental)
180
+ throw RpcError.app("invalid_params", "协议版本和实验功能需要在 AI 全局设置中修改");
181
+ this.validateSessionSettings(settings);
182
+ await this.detach(nativeId);
183
+ this.host?.state(this.id, nativeId).set("acp-settings", JSON.stringify(settings));
184
+ const state = this.sessions.get(nativeId);
185
+ if (state)
186
+ state.settings = settings;
187
+ this.host?.detached(this.id, nativeId);
188
+ }
189
+ else {
190
+ await this.stop();
191
+ this.stopped = false;
192
+ this.children.clear();
193
+ for (const [id, state] of this.sessions) {
194
+ state.loaded = false;
195
+ if (!this.host?.state(this.id, id).get("acp-settings"))
196
+ state.settings = settings;
197
+ this.host?.detached(this.id, id);
198
+ }
199
+ }
200
+ }
201
+ async authenticate(methodId, logout = false) {
202
+ const { features } = await this.acpInfo();
203
+ if (logout) {
204
+ if (!features.logout)
205
+ throw RpcError.app("not_supported", "这个 AI 不支持退出登录");
206
+ if ([...this.sessions.values()].some((state) => state.turnActive))
207
+ throw RpcError.app("busy", "请先结束正在运行的会话");
208
+ await this.rpc(this.connection.protocolVersion === 2 ? "auth/logout" : "logout", {});
209
+ this.signedOut = { state: "missing" };
210
+ return {};
211
+ }
212
+ const method = this.connection.initializeResult.authMethods?.find((entry) => entry.id === methodId);
213
+ if (!method || (method.type && !["agent", "terminal"].includes(method.type)))
214
+ throw RpcError.app("invalid_params", "请选择可用的登录方式");
215
+ if (method.type === "terminal") {
216
+ if (!this.host?.loginTerminal || ["http", "websocket"].includes(this.spec.transport ?? ""))
217
+ throw RpcError.app("not_supported", "这个连接不支持终端登录");
218
+ const launch = await this.host.loginTerminal(this.id, { command: this.spec.command, args: [...this.spec.args, ...(method.args ?? [])], env: { ...this.spec.env, ...method.env } });
219
+ void launch.exited.then((code) => {
220
+ if (this.stopped)
221
+ return;
222
+ this.signedOut = code === 0 ? { state: "ok" } : { state: "missing", hint: "终端登录未成功,请重试" };
223
+ this.credentialsChanged = code === 0;
224
+ this.host?.authChanged?.(this.id);
225
+ });
226
+ return { terminalId: launch.terminalId };
227
+ }
228
+ await this.rpc(this.connection.protocolVersion === 2 ? "auth/login" : "authenticate", { methodId }, 0);
229
+ this.signedOut = { state: "ok" };
230
+ return {};
231
+ }
232
+ async providers(operation, config) {
233
+ const { features } = await this.acpInfo();
234
+ if (!features.providers)
235
+ throw RpcError.app("not_supported", "这个 AI 不支持供应商配置");
236
+ const list = async () => {
237
+ const response = await this.rpc("providers/list", {});
238
+ return response.providers.map((provider) => acpProviderSchema.parse(provider));
239
+ };
240
+ if (operation === "list")
241
+ return list();
242
+ const provider = (await list()).find((entry) => entry.providerId === config?.providerId);
243
+ if (!provider || !config)
244
+ throw RpcError.app("invalid_params", "供应商不存在");
245
+ if (operation === "disable" && provider.required)
246
+ throw RpcError.app("invalid_params", "不能停用必需的供应商");
247
+ if (operation === "set" && (!config.apiType || !provider.supported.includes(config.apiType) || !config.baseUrl))
248
+ throw RpcError.app("invalid_params", "供应商地址或协议无效");
249
+ await this.rpc(`providers/${operation}`, operation === "disable" ? { providerId: config.providerId } : { ...config, headers: config.headers ?? {} });
250
+ return list();
251
+ }
252
+ async respondInteraction(requestId, response) {
253
+ const pending = this.interactions.get(requestId);
254
+ if (!pending)
255
+ throw RpcError.app("not_found", "这个请求已结束");
256
+ if (response.answers) {
257
+ const error = (pending.request.update.questions ?? []).map((question) => questionAnswerError(question, response.answers.find((answer) => answer.id === question.id))).find(Boolean);
258
+ if (error)
259
+ throw RpcError.app("invalid_params", error);
260
+ const result = pending.request.answer(response.answers);
261
+ this.interactions.delete(requestId);
262
+ pending.resolve(result);
263
+ this.host?.interaction?.(this.id, { requestId, resolved: true });
264
+ }
265
+ else {
266
+ if (!pending.request.update.options.some((option) => option.optionId === response.optionId))
267
+ throw RpcError.app("invalid_params", "选项已失效");
268
+ this.resolveInteraction(requestId, response.optionId);
269
+ }
270
+ }
271
+ resolveInteraction(requestId, optionId) {
272
+ const pending = this.interactions.get(requestId);
273
+ if (!pending)
274
+ return;
275
+ this.interactions.delete(requestId);
276
+ pending.resolve(pending.request.respond(optionId));
277
+ this.host?.interaction?.(this.id, { requestId, resolved: true });
278
+ }
279
+ async validateDirectories(settings) {
280
+ for (const path of settings.additionalDirectories) {
281
+ if (!isAbsolute(path) || !await stat(await realpath(path)).then((info) => info.isDirectory()).catch(() => false))
282
+ throw RpcError.app("invalid_params", `附加目录不存在或不是绝对路径:${path}`);
283
+ }
284
+ if (new Set(settings.mcpServers.map((server) => server.name)).size !== settings.mcpServers.length)
285
+ throw RpcError.app("invalid_params", "MCP 服务名称不能重复");
286
+ }
287
+ validateSessionSettings(settings) {
288
+ const caps = this.connection?.capabilities;
289
+ if (settings.additionalDirectories.length && !caps?.sessionCapabilities?.additionalDirectories)
290
+ throw RpcError.app("not_supported", "这个 AI 未声明支持附加目录");
291
+ for (const server of settings.mcpServers) {
292
+ if ((server.type === "http" || server.type === "sse") && !caps?.mcpCapabilities?.[server.type])
293
+ throw RpcError.app("not_supported", `这个 AI 不支持 ${server.type} MCP 服务`);
294
+ if (server.type === "stdio" && this.connection?.protocolVersion === 2 && !caps?.mcpCapabilities?.stdio)
295
+ throw RpcError.app("not_supported", "这个 AI 不支持 stdio MCP 服务");
296
+ if (server.type === "acp" && !caps?.mcpCapabilities?.acp)
297
+ throw RpcError.app("not_supported", "这个 AI 不支持通过 ACP 连接 MCP 服务");
298
+ }
299
+ }
300
+ async sessionParameters(cwd, settings = this.settings()) {
301
+ await this.ensureStarted();
302
+ await this.validateDirectories(settings);
303
+ this.validateSessionSettings(settings);
304
+ const v2 = this.connection?.protocolVersion === 2;
305
+ return { cwd, mcpServers: settings.mcpServers.map((server) => {
306
+ if (server.type === "stdio")
307
+ return { ...(v2 ? { type: "stdio" } : {}), name: server.name, command: server.command, args: server.args, env: Object.entries(server.env).map(([name, value]) => ({ name, value })) };
308
+ if (server.type === "acp")
309
+ return { type: "acp", name: server.name, serverId: this.mcp.open(server, cwd) };
310
+ return { type: server.type, name: server.name, url: server.url, headers: Object.entries(server.headers).map(([name, value]) => ({ name, value })) };
311
+ }), ...(settings.additionalDirectories.length ? { additionalDirectories: settings.additionalDirectories } : {}), ...this.sessionMeta() };
100
312
  }
101
313
  async listSessions(limit) {
102
314
  if (!this.connection?.alive || !this.connection.capabilities.sessionCapabilities?.list)
@@ -106,6 +318,8 @@ export class AcpDriver {
106
318
  do {
107
319
  const page = await this.rpc("session/list", cursor ? { cursor } : {});
108
320
  for (const info of page.sessions ?? []) {
321
+ if (Array.isArray(info.additionalDirectories) && info.additionalDirectories.every((path) => typeof path === "string" && isAbsolute(path)))
322
+ this.discoveredDirectories.set(info.sessionId, info.additionalDirectories);
109
323
  const updatedAt = info.updatedAt ? Date.parse(info.updatedAt) || Date.now() : Date.now();
110
324
  found.push({
111
325
  nativeId: info.sessionId, cwd: info.cwd, title: info.title ?? undefined, createdAt: updatedAt, updatedAt,
@@ -126,7 +340,10 @@ export class AcpDriver {
126
340
  const state = this.sessions.get(nativeId);
127
341
  if (state?.permissions.size || state?.questions.size)
128
342
  return "waiting";
129
- return state?.turnActive ? "running" : "idle";
343
+ for (const [id, child] of this.sessions)
344
+ if ((child.permissions.size || child.questions.size) && this.isChildOf(id, nativeId))
345
+ return "waiting";
346
+ return state?.reportedState ?? (state?.turnActive ? "running" : "idle");
130
347
  }
131
348
  /** ACP's session/fork, for agents that offer it: the whole conversation, in `cwd`. */
132
349
  async fork(nativeId, options) {
@@ -136,7 +353,9 @@ export class AcpDriver {
136
353
  throw RpcError.app("not_supported", `${this.label} 不支持从会话分叉`);
137
354
  if (options.upTo)
138
355
  throw RpcError.app("not_supported", `${this.label} 只能分叉整个会话`);
139
- const response = await this.rpc("session/fork", { sessionId: nativeId, cwd: options.cwd, mcpServers: [], ...this.sessionMeta() });
356
+ const settings = this.sessions.get(nativeId)?.settings ?? this.settings();
357
+ const response = await this.rpc("session/fork", { sessionId: nativeId, ...await this.sessionParameters(options.cwd, settings) });
358
+ this.stateFor(response.sessionId, options.cwd).settings = settings;
140
359
  const now = Date.now();
141
360
  return { nativeId: response.sessionId, cwd: options.cwd, createdAt: now, updatedAt: now };
142
361
  }
@@ -148,12 +367,13 @@ export class AcpDriver {
148
367
  return this.id === "claude" ? { _meta: { claudeCode: { emitRawSDKMessages: [{ type: "active_goal" }] } } } : {};
149
368
  }
150
369
  async createSession(options) {
370
+ await this.ensureStarted();
371
+ if (this.connection?.capabilities.sessions === false)
372
+ throw RpcError.app("not_supported", "这个 ACP Agent 不支持会话,可在文件中使用编辑建议");
151
373
  let response;
152
374
  try {
153
375
  response = await this.rpc("session/new", {
154
- cwd: options.cwd,
155
- mcpServers: [],
156
- ...this.sessionMeta(),
376
+ ...await this.sessionParameters(options.cwd),
157
377
  });
158
378
  }
159
379
  catch (error) {
@@ -163,6 +383,7 @@ export class AcpDriver {
163
383
  const state = this.stateFor(response.sessionId, options.cwd);
164
384
  state.loaded = true;
165
385
  state.config = toConfigOptions(response);
386
+ this.sessionCommands(state, response);
166
387
  if (options.model)
167
388
  await this.setConfig(response.sessionId, "model", options.model).catch(() => { });
168
389
  const now = Date.now();
@@ -176,23 +397,30 @@ export class AcpDriver {
176
397
  const state = this.stateFor(nativeId, context.cwd);
177
398
  if (state.loaded) {
178
399
  this.emitConfig(nativeId, state);
179
- this.host?.update(this.id, nativeId, { sessionUpdate: "ls_status", state: this.localState(nativeId) });
400
+ this.host?.update(this.id, nativeId, { sessionUpdate: "ls_status", state: this.localState(nativeId), turnActive: state.turnActive });
180
401
  return [];
181
402
  }
182
403
  const capabilities = this.connection?.capabilities;
183
404
  let history = [];
184
- if (capabilities?.loadSession) {
405
+ state.config = [];
406
+ state.commands = undefined;
407
+ if (capabilities?.loadSession || this.connection.protocolVersion === 2) {
185
408
  state.replaying = [];
409
+ state.updates = new AcpUpdates();
186
410
  try {
187
- const response = await this.rpc("session/load", {
411
+ const response = await this.rpc(this.connection.protocolVersion === 2 ? "session/resume" : "session/load", {
188
412
  sessionId: nativeId,
189
- cwd: context.cwd,
190
- mcpServers: [],
191
- ...this.sessionMeta(),
413
+ ...await this.sessionParameters(context.cwd, state.settings),
414
+ ...(this.connection.protocolVersion === 2 ? { replayFrom: { type: "start" } } : {}),
192
415
  });
193
- state.config = toConfigOptions(response);
416
+ if (response.configOptions !== undefined || response.modes !== undefined || response.models !== undefined)
417
+ state.config = this.mergeConfig(state, toConfigOptions(response));
418
+ this.sessionCommands(state, response);
194
419
  state.tracker = new AcpItemTracker();
195
- history = toHistory(state.replaying, state.tracker);
420
+ const unfinished = [];
421
+ history = toHistory(state.replaying, state.tracker, undefined, unfinished);
422
+ if (unfinished.length)
423
+ history.push({ itemId: `acp:snapshot:${createHash("sha256").update(JSON.stringify(unfinished)).digest("hex")}`, updates: unfinished });
196
424
  }
197
425
  finally {
198
426
  state.replaying = undefined;
@@ -201,11 +429,10 @@ export class AcpDriver {
201
429
  else if (capabilities?.sessionCapabilities?.resume) {
202
430
  const response = await this.rpc("session/resume", {
203
431
  sessionId: nativeId,
204
- cwd: context.cwd,
205
- mcpServers: [],
206
- ...this.sessionMeta(),
432
+ ...await this.sessionParameters(context.cwd, state.settings),
207
433
  });
208
434
  state.config = toConfigOptions(response);
435
+ this.sessionCommands(state, response);
209
436
  history.push({
210
437
  itemId: "linkshell:acp-history-unavailable",
211
438
  updates: [{
@@ -219,7 +446,7 @@ export class AcpDriver {
219
446
  }
220
447
  state.loaded = true;
221
448
  this.emitConfig(nativeId, state);
222
- this.host?.update(this.id, nativeId, { sessionUpdate: "ls_status", state: this.localState(nativeId) });
449
+ this.host?.update(this.id, nativeId, { sessionUpdate: "ls_status", state: this.localState(nativeId), turnActive: state.turnActive });
223
450
  return history;
224
451
  }
225
452
  async detach(nativeId) {
@@ -228,6 +455,11 @@ export class AcpDriver {
228
455
  return;
229
456
  state.loaded = false;
230
457
  this.cancelPermissions(state);
458
+ await this.services.close(nativeId);
459
+ await this.mcp.close(nativeId);
460
+ for (const key of this.terminalCalls.keys())
461
+ if (key.startsWith(`${nativeId}\n`))
462
+ this.terminalCalls.delete(key);
231
463
  if (this.connection?.capabilities.sessionCapabilities?.close) {
232
464
  await this.rpc("session/close", { sessionId: nativeId }).catch(() => { });
233
465
  }
@@ -236,13 +468,19 @@ export class AcpDriver {
236
468
  const state = this.sessions.get(nativeId);
237
469
  if (!state?.loaded)
238
470
  throw RpcError.app("not_ready", "session is not open");
471
+ if (state.reportedState === "unknown")
472
+ throw RpcError.app("not_ready", "Agent 尚未确认当前执行状态,请刷新会话或停止后重试");
473
+ for (const block of content) {
474
+ if (block.type === "image" && !this.capabilities.images || block.type === "audio" && !this.capabilities.audio || block.type === "resource" && !this.capabilities.embeddedContext)
475
+ throw RpcError.app("not_supported", `${this.label} 未声明支持这种附件`);
476
+ }
239
477
  const prompt = toAcpPrompt(content);
240
478
  if (prompt.length === 0)
241
479
  throw RpcError.app("invalid_params", "nothing to send");
242
480
  if (state.turnActive) {
243
481
  if (this.capabilities.steer) {
244
482
  this.echoUserMessage(nativeId, content, clientMessageId);
245
- void this.sendPrompt(nativeId, state, withContext(prompt, context));
483
+ void this.sendPrompt(nativeId, state, withContext(prompt, context), clientMessageId);
246
484
  return "steered";
247
485
  }
248
486
  state.queue.push({ content, clientMessageId, context });
@@ -262,6 +500,12 @@ export class AcpDriver {
262
500
  this.cancelPermissions(state);
263
501
  this.connection?.notify("session/cancel", { sessionId: nativeId });
264
502
  }
503
+ async cancelSubagent(nativeId, childId) {
504
+ const permitted = [...this.children.get(childId)?.entries() ?? []].some(([parent, association]) => association.canCancel && (parent === nativeId || this.isChildOf(parent, nativeId)));
505
+ if (!permitted)
506
+ throw RpcError.app("not_supported", "这个子代理未允许单独停止");
507
+ this.connection?.notify("session/cancel", { sessionId: childId });
508
+ }
265
509
  /** Stops the running turn only: the first queued message starts as soon as it has ended. */
266
510
  async sendQueuedNow(nativeId) {
267
511
  const state = this.sessions.get(nativeId);
@@ -271,20 +515,21 @@ export class AcpDriver {
271
515
  this.connection?.notify("session/cancel", { sessionId: nativeId });
272
516
  }
273
517
  async respondPermission(nativeId, requestId, optionId) {
274
- const question = this.sessions.get(nativeId)?.questions.get(requestId);
518
+ const owner = this.requestOwner(nativeId, requestId);
519
+ const question = owner?.state.questions.get(requestId);
275
520
  if (question) {
276
521
  // Not answering: skip (the agent goes on without an answer) or stop.
277
- this.sessions.get(nativeId)?.questions.delete(requestId);
522
+ owner.state.questions.delete(requestId);
278
523
  question.resolve(question.request.respond(optionId));
279
- this.host?.update(this.id, nativeId, { sessionUpdate: "ls_permission_resolved", requestId, optionId });
524
+ this.publish(owner.id, { sessionUpdate: "ls_permission_resolved", requestId, optionId });
280
525
  return;
281
526
  }
282
- const resolve = this.sessions.get(nativeId)?.permissions.get(requestId);
283
- if (!resolve)
527
+ const pending = owner?.state.permissions.get(requestId);
528
+ if (!pending)
284
529
  throw RpcError.app("not_found", "this permission request is no longer pending");
285
- this.sessions.get(nativeId)?.permissions.delete(requestId);
286
- resolve({ outcome: { outcome: "selected", optionId } });
287
- this.host?.update(this.id, nativeId, { sessionUpdate: "ls_permission_resolved", requestId, optionId });
530
+ owner.state.permissions.delete(requestId);
531
+ pending.resolve({ outcome: { outcome: "selected", optionId } });
532
+ this.publish(owner.id, { sessionUpdate: "ls_permission_resolved", requestId, optionId });
288
533
  }
289
534
  async setConfig(nativeId, optionId, value) {
290
535
  const state = this.sessions.get(nativeId);
@@ -301,13 +546,24 @@ export class AcpDriver {
301
546
  await this.rpc("session/set_model", { sessionId: nativeId, modelId: value });
302
547
  }
303
548
  else {
304
- await this.rpc("session/set_config_option", { sessionId: nativeId, configId: optionId, value });
549
+ const response = await this.rpc("session/set_config_option", {
550
+ sessionId: nativeId, configId: optionId, ...(option.type === "boolean" ? { type: "boolean", value: value === "on" } : { ...(this.connection?.protocolVersion === 2 ? { type: "select" } : {}), value }),
551
+ });
552
+ if (Array.isArray(response.configOptions)) {
553
+ state.config = this.mergeConfig(state, toConfigOptions(response));
554
+ this.emitConfig(nativeId, state);
555
+ return;
556
+ }
305
557
  }
306
558
  option.current = value;
307
559
  this.emitConfig(nativeId, state);
308
560
  }
309
561
  // ── internals ──────────────────────────────────────────────────────
310
562
  async detectVersion() {
563
+ if (this.spec.transport === "http" || this.spec.transport === "websocket")
564
+ return "remote";
565
+ if (this.spec.transport === "stdio")
566
+ return "";
311
567
  const probe = this.spec.version ?? { command: this.spec.command, args: ["--version"] };
312
568
  try {
313
569
  const { stdout, stderr } = await execFileAsync(probe.command, probe.args, { env: this.env, timeout: 15_000 });
@@ -322,22 +578,63 @@ export class AcpDriver {
322
578
  booting;
323
579
  /** Starts the agent process once; concurrent callers share the attempt. */
324
580
  ensureStarted() {
325
- if (this.connection?.alive)
581
+ const refresh = this.credentialsChanged && ![...this.sessions.values()].some((state) => state.turnActive);
582
+ if (this.connection?.alive && !refresh)
326
583
  return Promise.resolve();
327
- this.booting ??= this.boot().finally(() => {
584
+ this.booting ??= (async () => {
585
+ if (refresh) {
586
+ this.credentialsChanged = false;
587
+ await this.connection?.stop();
588
+ this.connection = undefined;
589
+ for (const [id, state] of this.sessions) {
590
+ state.loaded = false;
591
+ this.host?.detached(this.id, id);
592
+ }
593
+ }
594
+ await this.boot();
595
+ })().finally(() => {
328
596
  this.booting = undefined;
329
597
  });
330
598
  return this.booting;
331
599
  }
332
600
  async boot() {
601
+ const settings = this.settings();
602
+ const remote = this.spec.transport === "http" || this.spec.transport === "websocket";
603
+ const headers = {};
604
+ for (const [key, name] of Object.entries(this.spec.headerEnv ?? {})) {
605
+ const value = this.env[name];
606
+ if (value)
607
+ headers[key] = value;
608
+ }
333
609
  const connection = new AcpConnection({
334
610
  command: this.spec.command,
335
611
  args: this.spec.args,
336
612
  env: this.env,
613
+ protocolVersion: settings.protocolVersion,
614
+ experimental: settings.experimental,
615
+ transport: remote ? { type: this.spec.transport, url: this.spec.url, headers } : undefined,
337
616
  clientVersion: this.options.hostVersion,
338
617
  onUpdate: (sessionId, update) => this.onUpdate(sessionId, update),
339
618
  onRequest: (method, params, id) => this.onRequest(method, params, id),
340
619
  onCancelRequest: (id) => this.cancelQuestionRequest(id),
620
+ onNotification: (method, params) => {
621
+ if (method !== "elicitation/complete")
622
+ return;
623
+ const id = params?.elicitationId;
624
+ if (!id)
625
+ return;
626
+ for (const [requestId, pending] of this.interactions)
627
+ if (pending.request.update.url?.elicitationId === id)
628
+ this.resolveInteraction(requestId, "cancel");
629
+ for (const [sessionId, state] of this.sessions)
630
+ for (const [requestId, pending] of state.questions) {
631
+ if (pending.request.update.url?.elicitationId !== id)
632
+ continue;
633
+ state.questions.delete(requestId);
634
+ pending.resolve(pending.request.respond("cancel"));
635
+ this.host?.update(this.id, sessionId, { sessionUpdate: "ls_permission_resolved", requestId });
636
+ }
637
+ },
341
638
  onExit: (reason) => this.onExit(reason),
342
639
  });
343
640
  this.connection = connection;
@@ -351,6 +648,8 @@ export class AcpDriver {
351
648
  ...this.capabilities,
352
649
  steer: steering || queueing,
353
650
  images: caps.promptCapabilities?.image === true,
651
+ audio: caps.promptCapabilities?.audio === true,
652
+ embeddedContext: caps.promptCapabilities?.embeddedContext === true,
354
653
  fork: Boolean(caps.sessionCapabilities?.fork),
355
654
  };
356
655
  this.current = { installed: true, version: this.current.version };
@@ -384,6 +683,15 @@ export class AcpDriver {
384
683
  if (this.stopped)
385
684
  return;
386
685
  this.host?.log(`[${this.id}] ${reason}`);
686
+ for (const controller of this.serviceRequests.values())
687
+ controller.abort();
688
+ this.serviceRequests.clear();
689
+ this.terminalCalls.clear();
690
+ void this.services.close();
691
+ void this.mcp.close();
692
+ void this.edits.close();
693
+ for (const id of [...this.interactions.keys()])
694
+ this.resolveInteraction(id, "cancel");
387
695
  for (const [nativeId, state] of this.sessions) {
388
696
  if (!state.loaded)
389
697
  continue;
@@ -402,10 +710,15 @@ export class AcpDriver {
402
710
  stateFor(nativeId, cwd) {
403
711
  let state = this.sessions.get(nativeId);
404
712
  if (!state) {
713
+ const saved = this.host?.state(this.id, nativeId).get("acp-settings");
405
714
  state = {
406
715
  cwd,
716
+ settings: saved ? acpAgentSettingsSchema.parse(JSON.parse(saved)) : { ...this.settings(), ...(this.discoveredDirectories.has(nativeId) ? { additionalDirectories: this.discoveredDirectories.get(nativeId) } : {}) },
407
717
  loaded: false,
408
718
  tracker: new AcpItemTracker(),
719
+ updates: new AcpUpdates(),
720
+ messageAliases: new Map(JSON.parse(this.host?.state(this.id, nativeId).get("acp-message-aliases") ?? "[]")),
721
+ deferredUserUpdates: [],
409
722
  config: [],
410
723
  turnActive: false,
411
724
  inflight: 0,
@@ -419,36 +732,168 @@ export class AcpDriver {
419
732
  return state;
420
733
  }
421
734
  emitConfig(nativeId, state) {
422
- if (state.config.length === 0)
423
- return;
424
735
  this.host?.update(this.id, nativeId, {
425
736
  sessionUpdate: "ls_config",
426
737
  options: state.config.map(({ source: _source, ...option }) => option),
427
738
  });
739
+ if (state.commands)
740
+ this.host?.update(this.id, nativeId, state.commands);
741
+ }
742
+ sessionCommands(state, response) {
743
+ const commands = response.availableCommands;
744
+ if (!Array.isArray(commands))
745
+ return;
746
+ const update = normalizeAcpUpdate({ sessionUpdate: "available_commands_update", availableCommands: commands });
747
+ if (update?.sessionUpdate === "available_commands_update")
748
+ state.commands = update;
749
+ }
750
+ mergeConfig(state, options) {
751
+ return [...options, ...state.config.filter((option) => option.source !== "configOptions" && !options.some((next) => next.id === option.id || next.category === option.category))];
428
752
  }
429
753
  /** Feeds an update through the item tracker and on to the host. */
430
754
  emit(nativeId, update) {
431
755
  const state = this.sessions.get(nativeId);
432
756
  const tracked = state ? state.tracker.feed(update) : [{ update }];
433
757
  for (const entry of tracked)
434
- this.host?.update(this.id, nativeId, entry.update, entry.itemId);
758
+ this.publish(nativeId, entry.update, entry.itemId);
435
759
  }
436
760
  closeMessage(nativeId) {
437
761
  const state = this.sessions.get(nativeId);
438
762
  if (!state)
439
763
  return;
440
764
  for (const entry of state.tracker.close())
441
- this.host?.update(this.id, nativeId, entry.update, entry.itemId);
765
+ this.publish(nativeId, entry.update, entry.itemId);
766
+ }
767
+ /** Child streams reuse the existing nested timeline and never overwrite the parent's settings or status. */
768
+ publish(nativeId, update, itemId) {
769
+ const parents = this.children.get(nativeId);
770
+ if (!parents?.size) {
771
+ this.host?.update(this.id, nativeId, update, itemId);
772
+ return;
773
+ }
774
+ if (["ls_status", "ls_config", "session_info_update", "usage_update", "available_commands_update", "current_mode_update", "ls_goal", "plan"].includes(update.sessionUpdate))
775
+ return;
776
+ const prefix = `acp:${encodeURIComponent(nativeId)}:`;
777
+ for (const [parent, association] of parents) {
778
+ const mapped = update.sessionUpdate === "user_message_chunk"
779
+ ? { sessionUpdate: "ls_message", messageId: update.messageId ?? "prompt", role: "user", content: [update.content], append: true }
780
+ : { ...update };
781
+ if ("messageId" in mapped && mapped.messageId)
782
+ mapped.messageId = prefix + mapped.messageId;
783
+ if ("toolCallId" in mapped && mapped.toolCallId)
784
+ mapped.toolCallId = prefix + mapped.toolCallId;
785
+ if (["agent_message_chunk", "agent_thought_chunk", "ls_message", "ls_message_done", "tool_call", "tool_call_update", "ls_turn"].includes(mapped.sessionUpdate)) {
786
+ mapped.parentToolCallId = "parentToolCallId" in update && update.parentToolCallId ? prefix + update.parentToolCallId : this.childCallId(nativeId);
787
+ }
788
+ if (mapped.sessionUpdate === "ls_permission") {
789
+ mapped.title = `${association.title ?? "子代理"} · ${mapped.title}`;
790
+ mapped.childSessionId ??= nativeId;
791
+ }
792
+ this.onMappedUpdate(parent, mapped);
793
+ }
794
+ }
795
+ childCallId(id) { return `acp:subagent:${encodeURIComponent(id)}`; }
796
+ subagentUpdate(parent, raw) {
797
+ const update = raw;
798
+ const childId = update.sessionId;
799
+ if (typeof childId !== "string" || childId === parent || this.isChildOf(parent, childId))
800
+ return;
801
+ const parentState = this.sessions.get(parent);
802
+ if (!parentState)
803
+ return;
804
+ const parents = this.children.get(childId) ?? new Map();
805
+ const created = !parents.has(parent);
806
+ const association = parents.get(parent) ?? { canCancel: false };
807
+ if (update.title !== undefined)
808
+ association.title = update.title ?? undefined;
809
+ if (update.description !== undefined)
810
+ association.description = update.description ?? undefined;
811
+ if (update.capabilities !== undefined)
812
+ association.canCancel = !!update.capabilities?.cancel;
813
+ if (update.state !== undefined)
814
+ association.state = update.state;
815
+ parents.set(parent, association);
816
+ this.children.set(childId, parents);
817
+ const child = this.stateFor(childId, parentState.cwd);
818
+ child.loaded = true;
819
+ child.settings = parentState.settings;
820
+ const status = association.state;
821
+ const state = status?.state === "running" ? "running" : status?.state === "requires_action" ? "paused" : status?.state === "idle"
822
+ ? status.stopReason === "cancelled" ? "stopped" : status.stopReason === "error" ? "failed" : status.stopReason ? "completed" : "unknown" : "unknown";
823
+ const call = {
824
+ toolCallId: this.childCallId(childId), title: association.title ?? "子代理", kind: "other",
825
+ status: state === "failed" || state === "stopped" ? "failed" : state === "completed" ? "completed" : "in_progress",
826
+ detail: { type: "subagent", action: "spawn", name: association.title, task: association.description, nativeSessionId: childId, canCancel: association.canCancel, state },
827
+ };
828
+ this.onMappedUpdate(parent, { sessionUpdate: created ? "tool_call" : "tool_call_update", ...call });
829
+ }
830
+ isChildOf(child, parent, seen = new Set()) {
831
+ if (seen.has(child))
832
+ return false;
833
+ seen.add(child);
834
+ const parents = this.children.get(child);
835
+ return !!parents && (parents.has(parent) || [...parents.keys()].some((id) => this.isChildOf(id, parent, seen)));
836
+ }
837
+ requestOwner(nativeId, requestId) {
838
+ for (const [id, state] of this.sessions)
839
+ if ((id === nativeId || this.isChildOf(id, nativeId)) && (state.permissions.has(requestId) || state.questions.has(requestId)))
840
+ return { id, state };
841
+ return undefined;
442
842
  }
443
843
  onUpdate(sessionId, raw) {
444
- const update = normalizeAcpUpdate(raw);
445
- if (!update)
844
+ const record = raw;
845
+ if (record?.sessionUpdate === "notice") {
846
+ if (!this.sessions.get(sessionId)?.replaying && typeof record.title === "string" && typeof record.severity === "string") {
847
+ const roots = (id) => this.children.has(id) ? [...this.children.get(id).keys()].flatMap(roots) : [id];
848
+ for (const root of new Set(roots(sessionId)))
849
+ this.host?.notice?.(this.id, root, { severity: record.severity, title: record.title, description: typeof record.description === "string" ? record.description : undefined });
850
+ }
446
851
  return;
852
+ }
447
853
  const state = this.sessions.get(sessionId);
448
- if (state?.replaying) {
449
- state.replaying.push(update);
854
+ if (record?.sessionUpdate === "subagent_update" && this.settings().experimental) {
855
+ this.subagentUpdate(sessionId, raw);
450
856
  return;
451
857
  }
858
+ const version = this.connection?.protocolVersion ?? 1;
859
+ if (version === 2 && state && record?.sessionUpdate === "state_update") {
860
+ const status = raw;
861
+ for (const parent of this.children.get(sessionId)?.keys() ?? [])
862
+ this.subagentUpdate(parent, { sessionId, state: status });
863
+ state.reportedState = status.state === "running" ? "running" : status.state === "requires_action" ? "waiting" : status.state === "idle" ? "idle" : "unknown";
864
+ if (status.state === "running" || status.state === "requires_action") {
865
+ if (!state.turnActive && !state.replaying)
866
+ this.emit(sessionId, { sessionUpdate: "ls_turn", state: "started" });
867
+ state.turnActive = true;
868
+ state.idleReason = undefined;
869
+ }
870
+ else if (status.state === "idle") {
871
+ if (state.replaying)
872
+ state.turnActive = false;
873
+ else if (state.turnActive) {
874
+ state.idleReason = toStopReason(status.stopReason);
875
+ if (!state.inflight)
876
+ this.finishTurn(sessionId, state, state.idleReason);
877
+ }
878
+ }
879
+ const usage = status.usage ? usageUpdate(status.usage) : undefined;
880
+ if (usage)
881
+ this.onMappedUpdate(sessionId, usage);
882
+ if (!state.replaying && !this.children.has(sessionId))
883
+ this.host?.update(this.id, sessionId, { sessionUpdate: "ls_status", state: this.localState(sessionId), turnActive: state.turnActive });
884
+ return;
885
+ }
886
+ if (version === 2 && state && record?.sessionUpdate === "user_message" && state.inflight > 0 && !state.replaying) {
887
+ state.deferredUserUpdates.push(raw);
888
+ return;
889
+ }
890
+ const remapped = version === 2 && raw && typeof raw === "object" && "messageId" in raw && typeof raw.messageId === "string"
891
+ ? { ...raw, messageId: state?.messageAliases.get(raw.messageId) ?? raw.messageId } : raw;
892
+ for (const update of state?.updates.map(remapped, version) ?? [normalizeAcpUpdate(remapped)].filter((entry) => !!entry))
893
+ this.onMappedUpdate(sessionId, update);
894
+ }
895
+ onMappedUpdate(sessionId, update) {
896
+ const state = this.sessions.get(sessionId);
452
897
  if (state?.turnActive && (update.sessionUpdate === "tool_call" || update.sessionUpdate === "tool_call_update"))
453
898
  state.toolCalls.add(update.toolCallId);
454
899
  if (update.sessionUpdate === "current_mode_update" && state) {
@@ -457,11 +902,43 @@ export class AcpDriver {
457
902
  mode.current = update.currentModeId;
458
903
  }
459
904
  if (update.sessionUpdate === "ls_config" && state) {
460
- state.config = update.options.map((option) => ({ ...option, source: "configOptions" }));
905
+ state.config = this.mergeConfig(state, update.options.map((option) => ({ ...option, source: "configOptions" })));
906
+ update = { ...update, options: state.config.map(({ source: _source, ...option }) => option) };
907
+ }
908
+ if (update.sessionUpdate === "available_commands_update" && state)
909
+ state.commands = update;
910
+ if (state?.replaying) {
911
+ state.replaying.push(update);
912
+ return;
461
913
  }
462
914
  this.emit(sessionId, update);
915
+ if (this.connection?.protocolVersion !== 2 && (update.sessionUpdate === "tool_call" || update.sessionUpdate === "tool_call_update")) {
916
+ for (const content of update.content ?? []) {
917
+ if (content.type !== "terminal")
918
+ continue;
919
+ const key = `${sessionId}\n${content.terminalId}`;
920
+ const calls = this.terminalCalls.get(key) ?? new Set();
921
+ if (!calls.has(update.toolCallId)) {
922
+ calls.add(update.toolCallId);
923
+ this.terminalCalls.set(key, calls);
924
+ const snapshot = this.services.output(sessionId, content.terminalId);
925
+ if (snapshot)
926
+ this.emit(sessionId, { sessionUpdate: "tool_call_update", toolCallId: update.toolCallId, replaceOutput: snapshot.output });
927
+ }
928
+ }
929
+ }
463
930
  }
464
931
  onRequest(method, params, id) {
932
+ if (method === "mcp/message") {
933
+ const controller = new AbortController();
934
+ this.serviceRequests.set(id, controller);
935
+ return this.mcp.request(params, controller.signal).finally(() => this.serviceRequests.delete(id));
936
+ }
937
+ if (method.startsWith("fs/") || method.startsWith("terminal/")) {
938
+ const controller = new AbortController();
939
+ this.serviceRequests.set(id, controller);
940
+ return this.services.request(method, params, controller.signal).finally(() => this.serviceRequests.delete(id));
941
+ }
465
942
  if (questionMethod(this.id, method))
466
943
  return this.onQuestions(method, params, id);
467
944
  if (method !== "session/request_permission") {
@@ -474,9 +951,9 @@ export class AcpDriver {
474
951
  if (!state || !mapped)
475
952
  return { outcome: { outcome: "cancelled" } };
476
953
  return new Promise((resolve) => {
477
- state.permissions.set(requestId, resolve);
954
+ state.permissions.set(requestId, { rpcId: id, resolve });
478
955
  this.closeMessage(sessionId);
479
- this.host?.update(this.id, sessionId, mapped.update);
956
+ this.publish(sessionId, mapped.update);
480
957
  });
481
958
  }
482
959
  /** Questions are routed by explicit session id, then Cursor's tool id, never a "last session". */
@@ -500,6 +977,17 @@ export class AcpDriver {
500
977
  throw new RpcError(-32602, "无法确定这个问题属于哪个会话,未提交任何回答");
501
978
  }
502
979
  onQuestions(method, params, id) {
980
+ const scope = params;
981
+ if (method === "elicitation/create" && scope.sessionId === undefined && (typeof scope.requestId === "string" || typeof scope.requestId === "number")) {
982
+ if (!this.connection?.hasPendingRequest(scope.requestId))
983
+ throw new RpcError(-32602, "授权请求已经结束");
984
+ const requestId = `${this.id}-q${this.nextPermissionId++}-${String(id)}`;
985
+ const request = mapAcpQuestions(this.id, method, params, requestId);
986
+ return new Promise((resolve) => {
987
+ this.interactions.set(requestId, { rpcId: id, parentId: scope.requestId, request, resolve });
988
+ this.host?.interaction?.(this.id, request.update);
989
+ });
990
+ }
503
991
  const sessionId = this.questionSession(params);
504
992
  const state = this.sessions.get(sessionId);
505
993
  const requestId = `${this.id}-q${this.nextPermissionId++}-${String(id)}`;
@@ -507,33 +995,50 @@ export class AcpDriver {
507
995
  return new Promise((resolve) => {
508
996
  state.questions.set(requestId, { rpcId: id, request, resolve });
509
997
  this.closeMessage(sessionId);
510
- this.host?.update(this.id, sessionId, request.update);
998
+ this.publish(sessionId, request.update);
511
999
  });
512
1000
  }
513
1001
  cancelQuestionRequest(id) {
1002
+ this.serviceRequests.get(id)?.abort();
1003
+ for (const [requestId, pending] of this.interactions)
1004
+ if (pending.rpcId === id)
1005
+ this.resolveInteraction(requestId, "cancel");
514
1006
  for (const [sessionId, state] of this.sessions) {
1007
+ for (const [requestId, pending] of state.permissions) {
1008
+ if (pending.rpcId !== id)
1009
+ continue;
1010
+ state.permissions.delete(requestId);
1011
+ pending.resolve({ outcome: { outcome: "cancelled" } });
1012
+ this.publish(sessionId, { sessionUpdate: "ls_permission_resolved", requestId });
1013
+ }
515
1014
  for (const [requestId, pending] of state.questions) {
516
1015
  if (pending.rpcId !== id)
517
1016
  continue;
518
1017
  state.questions.delete(requestId);
519
1018
  pending.resolve(pending.request.respond("cancel"));
520
- this.host?.update(this.id, sessionId, { sessionUpdate: "ls_permission_resolved", requestId });
1019
+ this.publish(sessionId, { sessionUpdate: "ls_permission_resolved", requestId });
521
1020
  }
522
1021
  }
523
1022
  }
524
1023
  async answerQuestion(nativeId, requestId, answers) {
525
- const state = this.sessions.get(nativeId);
1024
+ const owner = this.requestOwner(nativeId, requestId);
1025
+ const state = owner?.state;
526
1026
  const pending = state?.questions.get(requestId);
527
1027
  if (!state || !pending)
528
1028
  throw RpcError.app("not_found", "这个问题已经不在等回答了");
529
1029
  const response = pending.request.answer(answers);
530
1030
  state.questions.delete(requestId);
531
1031
  pending.resolve(response);
532
- this.host?.update(this.id, nativeId, { sessionUpdate: "ls_permission_resolved", requestId, optionId: "answered", answers });
1032
+ const recorded = answers.map((answer) => pending.request.update.questions?.find((question) => question.id === answer.id)?.secret ? { id: answer.id, values: ["••••••"] } : answer);
1033
+ this.publish(owner.id, { sessionUpdate: "ls_permission_resolved", requestId, optionId: "answered", answers: recorded });
533
1034
  }
534
1035
  cancelPermissions(state) {
535
- for (const resolve of state.permissions.values())
536
- resolve({ outcome: { outcome: "cancelled" } });
1036
+ const nativeId = [...this.sessions].find(([, value]) => value === state)?.[0];
1037
+ if (nativeId)
1038
+ for (const requestId of [...state.permissions.keys(), ...state.questions.keys()])
1039
+ this.publish(nativeId, { sessionUpdate: "ls_permission_resolved", requestId });
1040
+ for (const pending of state.permissions.values())
1041
+ pending.resolve({ outcome: { outcome: "cancelled" } });
537
1042
  state.permissions.clear();
538
1043
  for (const pending of state.questions.values())
539
1044
  pending.resolve(pending.request.respond("cancel"));
@@ -549,16 +1054,28 @@ export class AcpDriver {
549
1054
  }
550
1055
  startTurn(nativeId, state, content, clientMessageId, context) {
551
1056
  state.turnActive = true;
1057
+ state.reportedState = undefined;
1058
+ state.idleReason = undefined;
552
1059
  this.echoUserMessage(nativeId, content, clientMessageId);
553
1060
  this.emit(nativeId, { sessionUpdate: "ls_turn", state: "started" });
554
- void this.sendPrompt(nativeId, state, withContext(toAcpPrompt(content), context));
1061
+ void this.sendPrompt(nativeId, state, withContext(toAcpPrompt(content), context), clientMessageId);
555
1062
  }
556
- async sendPrompt(nativeId, state, prompt) {
1063
+ async sendPrompt(nativeId, state, prompt, clientMessageId) {
557
1064
  state.inflight += 1;
558
1065
  let stopReason;
559
1066
  try {
560
1067
  const result = await this.rpc("session/prompt", { sessionId: nativeId, prompt }, 0);
561
- stopReason = toStopReason(result.stopReason);
1068
+ if (this.connection?.protocolVersion === 2) {
1069
+ if (typeof result.messageId !== "string")
1070
+ throw new Error("ACP 2 未返回已接收的消息 ID");
1071
+ state.messageAliases.set(result.messageId, `local-${clientMessageId}`);
1072
+ this.host?.state(this.id, nativeId).set("acp-message-aliases", JSON.stringify([...state.messageAliases]));
1073
+ }
1074
+ else
1075
+ stopReason = toStopReason(result.stopReason);
1076
+ const usage = result.usage ? usageUpdate(result.usage) : undefined;
1077
+ if (usage)
1078
+ this.emit(nativeId, usage);
562
1079
  }
563
1080
  catch (error) {
564
1081
  stopReason = "error";
@@ -570,8 +1087,19 @@ export class AcpDriver {
570
1087
  // A steered prompt settles the earlier one; the turn ends with the last.
571
1088
  if (state.inflight > 0)
572
1089
  return;
1090
+ for (const raw of state.deferredUserUpdates.splice(0))
1091
+ this.onUpdate(nativeId, raw);
1092
+ const ended = stopReason ?? state.idleReason;
1093
+ if (ended)
1094
+ this.finishTurn(nativeId, state, ended);
1095
+ }
1096
+ finishTurn(nativeId, state, stopReason) {
1097
+ if (!state.turnActive)
1098
+ return;
573
1099
  this.closeMessage(nativeId);
574
1100
  state.turnActive = false;
1101
+ state.reportedState = stopReason === "error" ? "error" : "idle";
1102
+ state.idleReason = undefined;
575
1103
  state.toolCalls.clear();
576
1104
  this.cancelPermissions(state);
577
1105
  this.emit(nativeId, { sessionUpdate: "ls_turn", state: "ended", stopReason });
@@ -581,6 +1109,9 @@ export class AcpDriver {
581
1109
  if (next && state.loaded)
582
1110
  this.startTurn(nativeId, state, next.content, next.clientMessageId, next.context);
583
1111
  }
1112
+ settings() {
1113
+ return this.host?.agentSettings?.(this.id) ?? this.spec.settings ?? acpAgentSettingsSchema.parse({});
1114
+ }
584
1115
  /** The agent's own delete, when it has one (ACP session/delete). */
585
1116
  async delete(nativeId) {
586
1117
  if (!this.connection?.alive)
@@ -631,11 +1162,20 @@ export class AcpDriver {
631
1162
  }
632
1163
  const connection = this.connection;
633
1164
  const opensSession = ["session/new", "session/load", "session/resume", "session/fork"].includes(method);
1165
+ const serverIds = opensSession ? (params.mcpServers ?? []).flatMap((server) => server.type === "acp" && server.serverId ? [server.serverId] : []) : [];
1166
+ let succeeded = false;
1167
+ const bindServers = (result) => {
1168
+ succeeded = true;
1169
+ const sessionId = result?.sessionId ?? params.sessionId;
1170
+ if (sessionId)
1171
+ this.mcp.bind(serverIds, sessionId);
1172
+ return result;
1173
+ };
634
1174
  try {
635
1175
  const result = await connection.request(method, params, timeoutMs);
636
1176
  if (opensSession)
637
1177
  this.signedOut = undefined;
638
- return result;
1178
+ return bindServers(result);
639
1179
  }
640
1180
  catch (error) {
641
1181
  if (!opensSession)
@@ -659,12 +1199,19 @@ export class AcpDriver {
659
1199
  await this.authenticating.promise;
660
1200
  const result = await connection.request(method, params, timeoutMs);
661
1201
  this.signedOut = undefined;
662
- return result;
1202
+ return bindServers(result);
663
1203
  }
664
1204
  catch (retryError) {
665
1205
  throw this.signInError(retryError);
666
1206
  }
667
1207
  }
1208
+ finally {
1209
+ if (!succeeded && serverIds.length)
1210
+ await this.mcp.close(undefined, serverIds);
1211
+ for (const [requestId, pending] of this.interactions)
1212
+ if (!connection.hasPendingRequest(pending.parentId))
1213
+ this.resolveInteraction(requestId, "cancel");
1214
+ }
668
1215
  }
669
1216
  }
670
1217
  //# sourceMappingURL=driver.js.map