linkshell-cli 0.2.88 → 0.2.90

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.
@@ -30,6 +30,32 @@ function firstString(value, keys) {
30
30
  }
31
31
  return undefined;
32
32
  }
33
+ function normalizedIdentifier(value) {
34
+ return (value ?? "").toLowerCase().replace(/[_\-\s/]+/g, "");
35
+ }
36
+ function firstNumber(value, keys) {
37
+ if (!value)
38
+ return undefined;
39
+ for (const key of keys) {
40
+ const next = value[key];
41
+ if (typeof next === "number" && Number.isFinite(next))
42
+ return next;
43
+ }
44
+ return undefined;
45
+ }
46
+ function stringArray(value) {
47
+ if (!Array.isArray(value))
48
+ return [];
49
+ return value.filter((entry) => typeof entry === "string" && entry.length > 0);
50
+ }
51
+ function arrayFromKeys(value, keys) {
52
+ for (const key of keys) {
53
+ const next = value[key];
54
+ if (Array.isArray(next))
55
+ return next;
56
+ }
57
+ return [];
58
+ }
33
59
  function extractItem(value) {
34
60
  const raw = asRecord(value);
35
61
  if (!raw)
@@ -87,23 +113,29 @@ function nameFromToolMethod(method) {
87
113
  return "工具";
88
114
  }
89
115
  function isToolItemType(itemType) {
90
- return (itemType === "commandExecution" ||
91
- itemType === "fileChange" ||
92
- itemType === "mcpToolCall" ||
93
- itemType === "dynamicToolCall");
116
+ const normalized = normalizedIdentifier(itemType);
117
+ return (normalized === "commandexecution" ||
118
+ normalized === "filechange" ||
119
+ normalized === "diff" ||
120
+ normalized === "toolcall" ||
121
+ normalized === "mcptoolcall" ||
122
+ normalized === "dynamictoolcall");
94
123
  }
95
124
  function toolNameFromItem(item) {
96
125
  const itemType = firstString(item, ["type"]);
97
- if (itemType === "commandExecution")
126
+ const normalized = normalizedIdentifier(itemType);
127
+ if (normalized === "commandexecution")
98
128
  return "命令";
99
- if (itemType === "fileChange")
129
+ if (normalized === "filechange" || normalized === "diff")
100
130
  return "文件修改";
101
- if (itemType === "mcpToolCall") {
131
+ if (normalized === "toolcall")
132
+ return firstString(item, ["toolName", "tool", "name", "title"]) ?? "工具";
133
+ if (normalized === "mcptoolcall") {
102
134
  const server = firstString(item, ["server"]);
103
135
  const tool = firstString(item, ["tool", "toolName", "name"]);
104
136
  return [server, tool].filter(Boolean).join(" · ") || "MCP 工具";
105
137
  }
106
- if (itemType === "dynamicToolCall") {
138
+ if (normalized === "dynamictoolcall") {
107
139
  const namespace = firstString(item, ["namespace"]);
108
140
  const tool = firstString(item, ["tool", "toolName", "name"]);
109
141
  return [namespace, tool].filter(Boolean).join(" · ") || "工具";
@@ -124,6 +156,92 @@ function summarizeFileChanges(changes) {
124
156
  .filter((line) => Boolean(line));
125
157
  return lines.length > 0 ? lines.slice(0, 8).join("\n") : undefined;
126
158
  }
159
+ function fileChangeEntriesFromItem(item) {
160
+ const changes = Array.isArray(item.changes) ? item.changes : [];
161
+ const entries = [];
162
+ for (const change of changes) {
163
+ const raw = asRecord(change);
164
+ if (!raw)
165
+ continue;
166
+ const path = firstString(raw, ["path", "file", "filePath", "absolutePath", "relativePath"]) ??
167
+ firstString(asRecord(raw.update), ["path", "file", "filePath"]);
168
+ if (!path)
169
+ continue;
170
+ const totals = asRecord(raw.totals) ?? asRecord(raw.diffStats) ?? asRecord(raw.stats);
171
+ const entry = { path };
172
+ const kind = firstString(raw, ["kind", "type", "operation", "action"]);
173
+ const added = firstNumber(raw, ["added", "additions"]) ?? firstNumber(totals, ["added", "additions"]);
174
+ const removed = firstNumber(raw, ["removed", "deletions"]) ?? firstNumber(totals, ["removed", "deletions"]);
175
+ if (kind)
176
+ entry.kind = kind;
177
+ if (added !== undefined)
178
+ entry.added = added;
179
+ if (removed !== undefined)
180
+ entry.removed = removed;
181
+ entries.push(entry);
182
+ }
183
+ const directPath = firstString(item, ["path", "file", "filePath", "absolutePath", "relativePath"]);
184
+ if (entries.length === 0 && directPath) {
185
+ const entry = { path: directPath };
186
+ const kind = firstString(item, ["kind", "type", "operation", "action"]);
187
+ if (kind)
188
+ entry.kind = kind;
189
+ return [entry];
190
+ }
191
+ return entries;
192
+ }
193
+ function commandExecutionFromItem(item, status, output) {
194
+ const command = firstString(item, ["command"]);
195
+ const cwd = firstString(item, ["cwd"]);
196
+ const exitCode = firstNumber(item, ["exitCode", "code"]);
197
+ if (!command && !cwd && !output && exitCode === undefined)
198
+ return undefined;
199
+ return { command, cwd, output, exitCode: exitCode ?? undefined, status };
200
+ }
201
+ function fileChangeFromItem(item, status, diff) {
202
+ const entries = fileChangeEntriesFromItem(item);
203
+ const summary = summarizeFileChanges(Array.isArray(item.changes) ? item.changes : []);
204
+ const changeSetId = firstString(item, ["changeSetId", "changesetId", "patchId"]);
205
+ if (entries.length === 0 && !diff && !summary && !changeSetId)
206
+ return undefined;
207
+ return { entries, diff, summary, changeSetId, status };
208
+ }
209
+ function commandExecutionFromTool(toolCall) {
210
+ const input = toolCall.input?.trim();
211
+ if (!input && !toolCall.output)
212
+ return undefined;
213
+ const [commandPart, cwdPart] = input?.split(/\n\ncwd:\s*/i) ?? [];
214
+ return {
215
+ command: commandPart || input,
216
+ cwd: cwdPart,
217
+ output: toolCall.output,
218
+ status: toolCall.status,
219
+ };
220
+ }
221
+ function fileChangeFromTool(toolCall) {
222
+ const diff = toolCall.output && looksLikeDiff(toolCall.output) ? toolCall.output : undefined;
223
+ const entries = (toolCall.input ?? "")
224
+ .split("\n")
225
+ .map((line) => line.trim())
226
+ .filter(Boolean)
227
+ .map((line) => {
228
+ const [kind, ...rest] = line.split(/\s+/);
229
+ const path = rest.length > 0 ? rest.join(" ") : kind;
230
+ const entry = { path: path ?? line };
231
+ if (rest.length > 0 && kind)
232
+ entry.kind = kind;
233
+ return entry;
234
+ })
235
+ .filter((entry) => entry.path.length > 0);
236
+ if (entries.length === 0 && !diff && !toolCall.output)
237
+ return undefined;
238
+ return {
239
+ entries,
240
+ diff,
241
+ summary: diff ? undefined : toolCall.output,
242
+ status: toolCall.status,
243
+ };
244
+ }
127
245
  function looksLikeDiff(text) {
128
246
  const value = text.trim();
129
247
  return (value.startsWith("diff --git ") ||
@@ -170,14 +288,15 @@ function extractDiffText(value) {
170
288
  }
171
289
  function toolInputFromItem(item) {
172
290
  const itemType = firstString(item, ["type"]);
173
- if (itemType === "commandExecution") {
291
+ const normalized = normalizedIdentifier(itemType);
292
+ if (normalized === "commandexecution") {
174
293
  const command = firstString(item, ["command"]);
175
294
  const cwd = firstString(item, ["cwd"]);
176
295
  if (command && cwd)
177
296
  return `${command}\n\ncwd: ${cwd}`;
178
297
  return command ?? cwd;
179
298
  }
180
- if (itemType === "fileChange") {
299
+ if (normalized === "filechange" || normalized === "diff") {
181
300
  const changes = Array.isArray(item.changes) ? item.changes : [];
182
301
  return summarizeFileChanges(changes) ?? firstString(item, ["path", "file", "filePath", "absolutePath", "relativePath"]);
183
302
  }
@@ -192,6 +311,170 @@ function textFromBlocks(blocks) {
192
311
  .filter(Boolean)
193
312
  .join("\n");
194
313
  }
314
+ function isSubagentItemType(itemType) {
315
+ const normalized = normalizedIdentifier(itemType);
316
+ return (normalized === "collabagenttoolcall" ||
317
+ normalized === "collabtoolcall" ||
318
+ normalized.startsWith("collabagentspawn") ||
319
+ normalized.startsWith("collabwaiting") ||
320
+ normalized.startsWith("collabclose") ||
321
+ normalized.startsWith("collabresume") ||
322
+ normalized.startsWith("collabagentinteraction"));
323
+ }
324
+ function parseSubagentRef(value) {
325
+ const raw = asRecord(value);
326
+ if (!raw)
327
+ return undefined;
328
+ const threadId = firstString(raw, ["threadId", "threadID", "id", "sessionId"]);
329
+ if (!threadId)
330
+ return undefined;
331
+ return {
332
+ threadId,
333
+ agentId: firstString(raw, ["agentId", "agentID"]),
334
+ nickname: firstString(raw, ["nickname", "name", "label"]),
335
+ role: firstString(raw, ["role", "kind"]),
336
+ model: firstString(raw, ["model", "modelName"]),
337
+ prompt: firstString(raw, ["prompt", "instructions", "message"]),
338
+ };
339
+ }
340
+ function parseSubagentStates(value) {
341
+ const result = {};
342
+ if (Array.isArray(value)) {
343
+ for (const entry of value) {
344
+ const raw = asRecord(entry);
345
+ const threadId = firstString(raw, ["threadId", "threadID", "id", "sessionId"]);
346
+ const status = firstString(raw, ["status", "state", "phase"]);
347
+ if (!threadId || !status)
348
+ continue;
349
+ result[threadId] = {
350
+ threadId,
351
+ status,
352
+ message: firstString(raw, ["message", "summary", "text"]),
353
+ };
354
+ }
355
+ return result;
356
+ }
357
+ const raw = asRecord(value);
358
+ if (!raw)
359
+ return result;
360
+ for (const [threadId, entry] of Object.entries(raw)) {
361
+ const state = asRecord(entry);
362
+ if (state) {
363
+ result[threadId] = {
364
+ threadId,
365
+ status: firstString(state, ["status", "state", "phase"]) ?? "running",
366
+ message: firstString(state, ["message", "summary", "text"]),
367
+ };
368
+ }
369
+ else if (typeof entry === "string") {
370
+ result[threadId] = { threadId, status: entry };
371
+ }
372
+ }
373
+ return result;
374
+ }
375
+ function parseStructuredInputOption(value, index) {
376
+ const raw = asRecord(value);
377
+ if (!raw) {
378
+ if (typeof value === "string" && value.trim()) {
379
+ return { id: `option-${index + 1}`, label: value.trim() };
380
+ }
381
+ return undefined;
382
+ }
383
+ const label = firstString(raw, ["label", "title", "text", "value"]);
384
+ if (!label)
385
+ return undefined;
386
+ return {
387
+ id: firstString(raw, ["id", "optionId", "value"]) ?? `option-${index + 1}`,
388
+ label,
389
+ description: firstString(raw, ["description", "detail", "subtitle"]),
390
+ };
391
+ }
392
+ function parseStructuredInputQuestion(value, index) {
393
+ const raw = asRecord(value);
394
+ if (!raw)
395
+ return undefined;
396
+ const question = firstString(raw, ["question", "prompt", "text", "message", "label"]);
397
+ if (!question)
398
+ return undefined;
399
+ const options = arrayFromKeys(raw, ["options", "choices", "items"])
400
+ .map(parseStructuredInputOption)
401
+ .filter((option) => Boolean(option));
402
+ return {
403
+ id: firstString(raw, ["id", "questionId", "key"]) ?? `question-${index + 1}`,
404
+ header: firstString(raw, ["header", "title"]),
405
+ question,
406
+ isOther: raw.isOther === true,
407
+ isSecret: raw.isSecret === true || raw.secret === true,
408
+ selectionLimit: firstNumber(raw, ["selectionLimit", "maxSelections"]),
409
+ options: options.length > 0 ? options : undefined,
410
+ };
411
+ }
412
+ function decodeStructuredInput(value) {
413
+ const raw = asRecord(value) ?? {};
414
+ const questions = arrayFromKeys(raw, ["questions", "items", "prompts"])
415
+ .map(parseStructuredInputQuestion)
416
+ .filter((question) => Boolean(question));
417
+ if (questions.length === 0) {
418
+ const single = parseStructuredInputQuestion(raw, 0);
419
+ if (single)
420
+ questions.push(single);
421
+ }
422
+ if (questions.length === 0)
423
+ return undefined;
424
+ return {
425
+ requestId: firstString(raw, ["requestId", "id", "inputId"]) ?? id("input"),
426
+ questions,
427
+ };
428
+ }
429
+ function decodeSubagentAction(item, status) {
430
+ const nested = asRecord(item.action) ?? asRecord(item.toolCall) ?? asRecord(item.call) ?? {};
431
+ const receiverAgents = [
432
+ ...arrayFromKeys(item, ["receiverAgents", "agents", "subagents", "receivers"]).map(parseSubagentRef),
433
+ ...arrayFromKeys(nested, ["receiverAgents", "agents", "subagents", "receivers"]).map(parseSubagentRef),
434
+ ].filter((entry) => Boolean(entry));
435
+ const receiverThreadIds = [
436
+ ...stringArray(item.receiverThreadIds),
437
+ ...stringArray(item.threadIds),
438
+ ...stringArray(item.childThreadIds),
439
+ ...stringArray(item.agentThreadIds),
440
+ ...stringArray(nested.receiverThreadIds),
441
+ ...stringArray(nested.threadIds),
442
+ ...receiverAgents.map((agent) => agent.threadId),
443
+ ].filter((threadId, index, array) => array.indexOf(threadId) === index);
444
+ const agentStates = {
445
+ ...parseSubagentStates(item.agentStates ?? item.states ?? item.statusByThread),
446
+ ...parseSubagentStates(nested.agentStates ?? nested.states ?? nested.statusByThread),
447
+ };
448
+ if (receiverThreadIds.length === 0 && Object.keys(agentStates).length === 0)
449
+ return undefined;
450
+ return {
451
+ tool: firstString(item, ["tool", "toolName", "name", "type"]) ??
452
+ firstString(nested, ["tool", "toolName", "name", "type"]) ??
453
+ "subagent",
454
+ status,
455
+ prompt: firstString(item, ["prompt", "instructions", "message"]) ??
456
+ firstString(nested, ["prompt", "instructions", "message"]),
457
+ model: firstString(item, ["model", "modelName"]) ?? firstString(nested, ["model", "modelName"]),
458
+ receiverThreadIds,
459
+ receiverAgents,
460
+ agentStates,
461
+ };
462
+ }
463
+ function summarizeSubagentAction(action) {
464
+ const count = Math.max(1, action.receiverThreadIds.length, action.receiverAgents.length);
465
+ const normalized = normalizedIdentifier(action.tool);
466
+ if (normalized.includes("spawn"))
467
+ return `启动 ${count} 个子 Agent`;
468
+ if (normalized.includes("wait"))
469
+ return `等待 ${count} 个子 Agent`;
470
+ if (normalized.includes("resume"))
471
+ return `恢复 ${count} 个子 Agent`;
472
+ if (normalized.includes("close"))
473
+ return `关闭 ${count} 个子 Agent`;
474
+ if (normalized.includes("sendinput"))
475
+ return `更新 ${count} 个子 Agent`;
476
+ return count === 1 ? "子 Agent 活动" : `${count} 个子 Agent 活动`;
477
+ }
195
478
  function previewText(text) {
196
479
  return text.replace(/\s+/g, " ").trim().slice(0, 160);
197
480
  }
@@ -202,21 +485,10 @@ function providerLabel(provider) {
202
485
  return "Claude";
203
486
  return "Custom";
204
487
  }
205
- function providerSetupReason(provider, activeProvider, error) {
206
- if (provider === activeProvider) {
207
- return error ?? `${providerLabel(provider)} Agent 正在初始化或不可用。`;
208
- }
209
- if (provider === "codex") {
210
- return `当前 CLI 启用的是 ${providerLabel(activeProvider)} Agent。`;
211
- }
212
- if (provider === "claude") {
213
- return "Claude ACP adapter 尚未启用,请用 --agent-provider claude --agent-command 配置。";
214
- }
215
- return "Custom Agent 需要用 --agent-provider custom --agent-command 配置后才能使用。";
216
- }
217
488
  export class AgentWorkspaceProxy {
218
489
  input;
219
- client;
490
+ clients = new Map();
491
+ agentProtocols = new Map();
220
492
  initialized = false;
221
493
  status = "unavailable";
222
494
  error;
@@ -229,8 +501,9 @@ export class AgentWorkspaceProxy {
229
501
  pendingPermissions = new Map();
230
502
  permissionWaiters = new Map();
231
503
  permissionSources = new Map();
504
+ pendingStructuredInputs = new Map();
505
+ structuredInputWaiters = new Map();
232
506
  toolConversationIds = new Map();
233
- agentProtocol;
234
507
  constructor(input) {
235
508
  this.input = input;
236
509
  }
@@ -269,7 +542,8 @@ export class AgentWorkspaceProxy {
269
542
  const payload = parseTypedPayload("agent.v2.cancel", envelope.payload);
270
543
  const conversation = this.conversations.get(payload.conversationId);
271
544
  this.cancelPendingPermissions(payload.conversationId);
272
- this.client?.cancel({
545
+ const cancelClient = conversation ? this.clientForProvider(conversation.provider) : undefined;
546
+ cancelClient?.cancel({
273
547
  sessionId: conversation?.agentSessionId,
274
548
  turnId: this.currentTurnId,
275
549
  });
@@ -283,102 +557,122 @@ export class AgentWorkspaceProxy {
283
557
  this.respondPermission(payload);
284
558
  break;
285
559
  }
560
+ case "agent.v2.structured_input.respond": {
561
+ const payload = parseTypedPayload("agent.v2.structured_input.respond", envelope.payload);
562
+ this.respondStructuredInput(payload);
563
+ break;
564
+ }
286
565
  }
287
566
  }
288
567
  stop() {
289
- this.client?.stop();
290
- this.client = undefined;
568
+ for (const client of this.clients.values()) {
569
+ client.stop();
570
+ }
571
+ this.clients.clear();
572
+ }
573
+ clientForProvider(provider) {
574
+ return this.clients.get(provider);
575
+ }
576
+ protocolForProvider(provider) {
577
+ return this.agentProtocols.get(provider);
291
578
  }
292
579
  async initialize() {
293
580
  if (this.initialized)
294
581
  return;
295
- await this.ensureClient();
582
+ // trigger capability report immediately, lazy-start providers on first use
583
+ this.initialized = true;
584
+ this.status = "idle";
585
+ this.error = undefined;
586
+ this.sendCapabilities();
296
587
  }
297
- async ensureClient() {
298
- if (this.client)
299
- return;
588
+ async ensureProviderClient(provider) {
589
+ const existing = this.clients.get(provider);
590
+ if (existing)
591
+ return existing;
300
592
  const resolved = resolveAgentCommand({
301
- provider: this.input.provider,
593
+ provider,
302
594
  command: this.input.command,
303
595
  });
304
596
  if (!resolved) {
305
- this.status = "unavailable";
306
- this.error = `Agent Workspace requires --agent-command for ${this.input.provider}`;
307
- return;
597
+ if (this.input.verbose) {
598
+ process.stderr.write(`[agent:v2] no command for provider ${provider}\n`);
599
+ }
600
+ return undefined;
308
601
  }
309
602
  try {
310
- this.agentProtocol = resolved.protocol;
311
- this.client = new AcpClient({
603
+ this.agentProtocols.set(provider, resolved.protocol);
604
+ const client = new AcpClient({
312
605
  command: resolved.command,
313
606
  protocol: resolved.protocol,
314
607
  framing: resolved.framing,
315
608
  cwd: this.input.cwd,
316
609
  onNotification: (method, params) => this.handleNotification(method, params),
317
610
  onRequest: (method, params) => this.handleRequest(method, params),
318
- onExit: (message) => this.handleExit(message),
611
+ onExit: (message) => this.handleProviderExit(provider, message),
319
612
  });
320
- await this.client.initialize();
321
- this.initialized = true;
613
+ await client.initialize();
614
+ this.clients.set(provider, client);
322
615
  this.status = "idle";
323
616
  this.error = undefined;
617
+ this.sendCapabilities();
618
+ return client;
324
619
  }
325
620
  catch (error) {
326
- this.client?.stop();
327
- this.client = undefined;
328
- this.status = "error";
329
- this.error = error instanceof Error ? error.message : String(error);
621
+ if (this.input.verbose) {
622
+ process.stderr.write(`[agent:v2] failed to start ${provider}: ${error instanceof Error ? error.message : String(error)}\n`);
623
+ }
624
+ return undefined;
330
625
  }
331
626
  }
332
627
  sendCapabilities() {
333
- const enabled = Boolean(this.client && this.initialized && !this.error);
334
- const supportsImages = enabled && this.agentProtocol === "codex-app-server";
335
- const activeProvider = this.input.provider;
336
- const providerIds = ["codex", "claude"];
337
- if (activeProvider === "custom")
338
- providerIds.push("custom");
628
+ const providers = this.input.availableProviders.map((provider) => {
629
+ const client = this.clients.get(provider);
630
+ const protocol = this.agentProtocols.get(provider);
631
+ const enabled = Boolean(client);
632
+ const supportsImages = enabled && protocol === "codex-app-server";
633
+ return {
634
+ id: provider,
635
+ label: providerLabel(provider),
636
+ enabled,
637
+ reason: enabled ? undefined : `${providerLabel(provider)} 未安装或启动失败`,
638
+ supportsImages,
639
+ supportsPermission: enabled,
640
+ supportsPlan: enabled,
641
+ supportsCancel: enabled,
642
+ };
643
+ });
644
+ const anyEnabled = providers.some((p) => p.enabled);
339
645
  this.input.send(createEnvelope({
340
646
  type: "agent.v2.capabilities",
341
647
  sessionId: this.input.sessionId,
342
648
  payload: {
343
- enabled,
344
- provider: activeProvider,
345
- providers: providerIds.map((provider) => {
346
- const isActive = provider === activeProvider;
347
- const canUse = isActive && enabled;
348
- return {
349
- id: provider,
350
- label: providerLabel(provider),
351
- enabled: canUse,
352
- reason: canUse
353
- ? undefined
354
- : providerSetupReason(provider, activeProvider, isActive ? this.error : undefined),
355
- supportsImages: canUse && supportsImages,
356
- supportsPermission: canUse,
357
- supportsPlan: canUse,
358
- supportsCancel: canUse,
359
- };
360
- }),
649
+ enabled: anyEnabled,
650
+ provider: this.input.availableProviders[0] ?? "codex",
651
+ providers,
361
652
  protocolVersion: 1,
362
653
  workspaceProtocolVersion: 2,
363
- error: enabled ? undefined : this.error,
364
- supportsSessionList: enabled,
365
- supportsSessionLoad: enabled,
366
- supportsImages,
654
+ error: anyEnabled ? undefined : "没有可用的 Agent provider。请安装 Claude Code 或 Codex CLI。",
655
+ supportsSessionList: anyEnabled,
656
+ supportsSessionLoad: anyEnabled,
657
+ supportsImages: providers.some((p) => p.supportsImages),
367
658
  supportsAudio: false,
368
- supportsPermission: enabled,
369
- supportsPlan: enabled,
370
- supportsCancel: enabled,
659
+ supportsPermission: anyEnabled,
660
+ supportsPlan: anyEnabled,
661
+ supportsCancel: anyEnabled,
371
662
  },
372
663
  }));
373
664
  }
374
665
  async openConversation(payload) {
375
- await this.ensureClient();
376
- this.sendCapabilities();
377
- if (payload.provider && payload.provider !== this.input.provider) {
378
- return this.openFailure(payload, `当前 CLI 只启用了 ${providerLabel(this.input.provider)} Agent,不能在这个会话里启动 ${providerLabel(payload.provider)}。`);
666
+ const provider = payload.provider ?? this.input.availableProviders[0];
667
+ if (!provider) {
668
+ return this.openFailure(payload, "没有可用的 Agent provider。");
669
+ }
670
+ if (!this.input.availableProviders.includes(provider)) {
671
+ return this.openFailure(payload, `${providerLabel(provider)} 未安装或不可用。`);
379
672
  }
380
- if (!this.client) {
381
- return this.openFailure(payload, this.error ?? "Agent Workspace 不可用,请确认 CLI 已使用 --agent-ui 启动。");
673
+ const client = await this.ensureProviderClient(provider);
674
+ if (!client) {
675
+ return this.openFailure(payload, `${providerLabel(provider)} 启动失败。请确认 CLI 已安装并可用。`);
382
676
  }
383
677
  const cwd = payload.cwd ?? this.input.cwd;
384
678
  let agentSessionId = payload.agentSessionId;
@@ -401,8 +695,8 @@ export class AgentWorkspaceProxy {
401
695
  }
402
696
  try {
403
697
  const result = agentSessionId
404
- ? await this.client.loadSession({ sessionId: agentSessionId, cwd })
405
- : await this.client.newSession({ cwd });
698
+ ? await client.loadSession({ sessionId: agentSessionId, cwd })
699
+ : await client.newSession({ cwd });
406
700
  agentSessionId = this.extractSessionId(result) ?? agentSessionId ?? id("agent-session");
407
701
  const now = Date.now();
408
702
  const conversationId = payload.conversationId ?? `agent:${agentSessionId}`;
@@ -410,7 +704,7 @@ export class AgentWorkspaceProxy {
410
704
  ...existingConversation,
411
705
  id: conversationId,
412
706
  agentSessionId,
413
- provider: payload.provider ?? this.input.provider,
707
+ provider,
414
708
  cwd,
415
709
  title: payload.title ?? existingConversation?.title ?? titleFromCwd(cwd),
416
710
  model: payload.model ?? existingConversation?.model,
@@ -443,7 +737,7 @@ export class AgentWorkspaceProxy {
443
737
  const now = Date.now();
444
738
  const conversation = {
445
739
  id: fallbackId,
446
- provider: payload.provider ?? this.input.provider,
740
+ provider: payload.provider ?? this.input.availableProviders[0] ?? "codex",
447
741
  cwd,
448
742
  title: payload.title ?? titleFromCwd(cwd),
449
743
  model: payload.model,
@@ -474,9 +768,13 @@ export class AgentWorkspaceProxy {
474
768
  async sendPrompt(payload) {
475
769
  const conversation = this.conversations.get(payload.conversationId) ??
476
770
  await this.openConversation({ conversationId: payload.conversationId });
477
- if (!conversation || !this.client || !conversation.agentSessionId)
771
+ if (!conversation || !conversation.agentSessionId)
478
772
  return;
479
- if (payload.contentBlocks.some((block) => block.type === "image") && this.agentProtocol !== "codex-app-server") {
773
+ const client = this.clientForProvider(conversation.provider);
774
+ if (!client)
775
+ return;
776
+ const protocol = this.protocolForProvider(conversation.provider);
777
+ if (payload.contentBlocks.some((block) => block.type === "image") && protocol !== "codex-app-server") {
480
778
  conversation.status = "idle";
481
779
  conversation.lastActivityAt = Date.now();
482
780
  this.emitConversation(conversation);
@@ -507,7 +805,7 @@ export class AgentWorkspaceProxy {
507
805
  });
508
806
  this.emitConversation(conversation);
509
807
  try {
510
- const result = await this.client.prompt({
808
+ const result = await client.prompt({
511
809
  sessionId: conversation.agentSessionId,
512
810
  content: payload.contentBlocks,
513
811
  clientMessageId: payload.clientMessageId,
@@ -534,6 +832,9 @@ export class AgentWorkspaceProxy {
534
832
  }
535
833
  }
536
834
  handleRequest(method, params) {
835
+ if (method === "item/tool/requestUserInput" || method === "tool/requestUserInput") {
836
+ return this.handleStructuredInput(params, true);
837
+ }
537
838
  if (isPermissionRequestMethod(method)) {
538
839
  return this.handlePermission(params, true, method);
539
840
  }
@@ -556,6 +857,10 @@ export class AgentWorkspaceProxy {
556
857
  return;
557
858
  }
558
859
  const conversationId = this.conversationIdFromParams(params) ?? this.activeConversationId;
860
+ if (method === "item/tool/requestUserInput" || method === "tool/requestUserInput") {
861
+ this.handleStructuredInput(params);
862
+ return;
863
+ }
559
864
  if (isPermissionRequestMethod(method)) {
560
865
  this.handlePermission(params, false, method);
561
866
  return;
@@ -712,14 +1017,21 @@ export class AgentWorkspaceProxy {
712
1017
  if (!item)
713
1018
  return;
714
1019
  const itemType = firstString(item, ["type"]);
715
- if (itemType === "agentMessage" || itemType === "assistantMessage") {
1020
+ const normalizedItemType = normalizedIdentifier(itemType);
1021
+ if (normalizedItemType === "agentmessage" || normalizedItemType === "assistantmessage") {
716
1022
  this.handleCompletedMessageItem(item, true);
717
1023
  return;
718
1024
  }
719
- if (itemType === "plan") {
1025
+ if (normalizedItemType === "plan") {
720
1026
  this.handlePlanUpdated({ plan: [item] });
721
1027
  return;
722
1028
  }
1029
+ if (isSubagentItemType(itemType)) {
1030
+ this.handleSubagentItem(item, "running", true);
1031
+ return;
1032
+ }
1033
+ if (this.handleSemanticSystemItem(item, "running", true))
1034
+ return;
723
1035
  const conversationId = this.conversationIdFromParams(item) ?? this.activeConversationId;
724
1036
  const toolCall = this.toolCallFromItem(item, "running");
725
1037
  if (!conversationId || !toolCall)
@@ -732,10 +1044,21 @@ export class AgentWorkspaceProxy {
732
1044
  if (!item)
733
1045
  return;
734
1046
  const itemType = firstString(item, ["type"]);
735
- if (itemType === "agentMessage" || itemType === "assistantMessage") {
1047
+ const normalizedItemType = normalizedIdentifier(itemType);
1048
+ if (normalizedItemType === "agentmessage" || normalizedItemType === "assistantmessage") {
736
1049
  this.handleCompletedMessageItem(item, false);
737
1050
  return;
738
1051
  }
1052
+ if (normalizedItemType === "plan") {
1053
+ this.handlePlanDelta({ ...item, delta: firstString(item, ["text", "content", "message"]) });
1054
+ return;
1055
+ }
1056
+ if (isSubagentItemType(itemType)) {
1057
+ this.handleSubagentItem(item, normalizeToolStatus(item.status, true), false);
1058
+ return;
1059
+ }
1060
+ if (this.handleSemanticSystemItem(item, normalizeToolStatus(item.status, true), false))
1061
+ return;
739
1062
  const conversationId = this.conversationIdFromParams(item) ?? this.activeConversationId;
740
1063
  const toolCall = this.toolCallFromItem(item, normalizeToolStatus(item.status, true));
741
1064
  if (!conversationId || !toolCall)
@@ -900,18 +1223,131 @@ export class AgentWorkspaceProxy {
900
1223
  });
901
1224
  this.updateConversationPreview(conversationId, text, raw.done === true ? "idle" : "running");
902
1225
  }
1226
+ handleSemanticSystemItem(item, status, streaming) {
1227
+ const itemType = firstString(item, ["type"]);
1228
+ const normalized = normalizedIdentifier(itemType);
1229
+ const conversationId = this.conversationIdFromParams(item) ?? this.activeConversationId;
1230
+ if (!conversationId)
1231
+ return false;
1232
+ const itemId = firstString(item, ["id", "itemId"]) ?? id("item");
1233
+ const existing = this.findItem(conversationId, itemId);
1234
+ const base = {
1235
+ id: itemId,
1236
+ conversationId,
1237
+ type: "status",
1238
+ role: "system",
1239
+ turnId: this.extractTurnId(item) ?? this.currentTurnId,
1240
+ itemId,
1241
+ createdAt: existing?.createdAt ?? Date.now(),
1242
+ updatedAt: Date.now(),
1243
+ isStreaming: streaming,
1244
+ };
1245
+ if (normalized === "reasoning" || normalized === "thinking") {
1246
+ const text = firstString(item, ["text", "content", "summary", "message"]) ??
1247
+ stringifyDefined(item.contentItems ?? item.summary);
1248
+ this.upsertItem(conversationId, {
1249
+ ...base,
1250
+ kind: "thinking",
1251
+ text: text ?? (streaming ? "正在思考" : "完成思考"),
1252
+ });
1253
+ return true;
1254
+ }
1255
+ if (normalized === "enteredreviewmode") {
1256
+ const target = firstString(item, ["review", "target", "label"]) ?? "changes";
1257
+ this.upsertItem(conversationId, {
1258
+ ...base,
1259
+ kind: "review",
1260
+ text: status === "completed" ? `已完成审查 ${target}` : `正在审查 ${target}`,
1261
+ });
1262
+ return true;
1263
+ }
1264
+ if (normalized === "contextcompaction") {
1265
+ this.upsertItem(conversationId, {
1266
+ ...base,
1267
+ kind: "context_compaction",
1268
+ text: status === "completed" ? "上下文已压缩" : "正在压缩上下文",
1269
+ });
1270
+ return true;
1271
+ }
1272
+ return false;
1273
+ }
1274
+ handleSubagentItem(item, status, streaming) {
1275
+ const conversationId = this.conversationIdFromParams(item) ?? this.activeConversationId;
1276
+ if (!conversationId)
1277
+ return;
1278
+ const subagent = decodeSubagentAction(item, status);
1279
+ if (!subagent)
1280
+ return;
1281
+ const itemId = firstString(item, ["id", "itemId"]) ?? id("subagent");
1282
+ const text = summarizeSubagentAction(subagent);
1283
+ const existing = this.findItem(conversationId, itemId);
1284
+ this.upsertItem(conversationId, {
1285
+ id: itemId,
1286
+ conversationId,
1287
+ type: "status",
1288
+ kind: "subagent_action",
1289
+ role: "system",
1290
+ turnId: this.extractTurnId(item) ?? this.currentTurnId,
1291
+ itemId,
1292
+ text,
1293
+ subagent,
1294
+ createdAt: existing?.createdAt ?? Date.now(),
1295
+ updatedAt: Date.now(),
1296
+ isStreaming: streaming,
1297
+ });
1298
+ this.updateConversationPreview(conversationId, text, streaming ? "running" : "idle");
1299
+ }
1300
+ handleStructuredInput(params, waitForResponse = false) {
1301
+ const raw = asRecord(params) ?? {};
1302
+ const conversationId = this.conversationIdFromParams(raw) ?? this.activeConversationId;
1303
+ if (!conversationId)
1304
+ return waitForResponse ? Promise.resolve(formatStructuredInputResponse({})) : undefined;
1305
+ const structuredInput = decodeStructuredInput(raw);
1306
+ if (!structuredInput)
1307
+ return waitForResponse ? Promise.resolve(formatStructuredInputResponse({})) : undefined;
1308
+ const text = structuredInput.questions.map((question) => question.question).join("\n");
1309
+ this.pendingStructuredInputs.set(structuredInput.requestId, { conversationId, input: structuredInput });
1310
+ this.upsertItem(conversationId, {
1311
+ id: `input:${structuredInput.requestId}`,
1312
+ conversationId,
1313
+ type: "status",
1314
+ kind: "user_input_prompt",
1315
+ role: "system",
1316
+ text,
1317
+ structuredInput,
1318
+ metadata: { inputPending: true },
1319
+ createdAt: this.findItem(conversationId, `input:${structuredInput.requestId}`)?.createdAt ?? Date.now(),
1320
+ updatedAt: Date.now(),
1321
+ });
1322
+ this.updateConversationPreview(conversationId, "需要用户输入", "running");
1323
+ if (!waitForResponse)
1324
+ return;
1325
+ return new Promise((resolve) => {
1326
+ const timer = setTimeout(() => {
1327
+ this.pendingStructuredInputs.delete(structuredInput.requestId);
1328
+ this.structuredInputWaiters.delete(structuredInput.requestId);
1329
+ resolve(formatStructuredInputResponse({}));
1330
+ this.markStructuredInput(conversationId, structuredInput.requestId, {
1331
+ inputPending: false,
1332
+ inputError: "等待用户输入超时",
1333
+ });
1334
+ }, PERMISSION_TIMEOUT_MS);
1335
+ this.structuredInputWaiters.set(structuredInput.requestId, { resolve, timer });
1336
+ });
1337
+ }
903
1338
  toolCallFromItem(item, fallbackStatus) {
904
1339
  const itemId = firstString(item, ["id", "itemId", "toolCallId"]);
905
1340
  if (!itemId)
906
1341
  return undefined;
907
1342
  const itemType = firstString(item, ["type"]);
1343
+ const normalizedItemType = normalizedIdentifier(itemType);
908
1344
  const name = toolNameFromItem(item);
909
1345
  if (!name && !isToolItemType(itemType))
910
1346
  return undefined;
911
1347
  const bufferedOutput = this.toolOutputBuffers.get(itemId);
912
1348
  const rawOutput = firstString(item, ["aggregatedOutput", "output", "stdout", "stderr"]) ??
913
1349
  stringifyDefined(item.result ?? item.error ?? item.contentItems);
914
- const output = itemType === "fileChange"
1350
+ const output = normalizedItemType === "filechange" || normalizedItemType === "diff"
915
1351
  ? extractDiffText(item) ?? bufferedOutput ?? rawOutput
916
1352
  : rawOutput ?? bufferedOutput;
917
1353
  return {
@@ -980,8 +1416,10 @@ export class AgentWorkspaceProxy {
980
1416
  this.permissionSources.delete(payload.requestId);
981
1417
  }
982
1418
  else {
983
- this.client?.respondPermission({
984
- sessionId: this.conversations.get(payload.conversationId)?.agentSessionId,
1419
+ const conversation = this.conversations.get(payload.conversationId);
1420
+ const respondClient = conversation ? this.clientForProvider(conversation.provider) : undefined;
1421
+ respondClient?.respondPermission({
1422
+ sessionId: conversation?.agentSessionId,
985
1423
  requestId: payload.requestId,
986
1424
  outcome: payload.outcome === "cancelled" ? "deny" : payload.outcome,
987
1425
  optionId: selectedOptionId,
@@ -989,6 +1427,32 @@ export class AgentWorkspaceProxy {
989
1427
  }
990
1428
  this.updateConversationStatus(payload.conversationId, "running");
991
1429
  }
1430
+ respondStructuredInput(payload) {
1431
+ const pending = this.pendingStructuredInputs.get(payload.requestId);
1432
+ this.pendingStructuredInputs.delete(payload.requestId);
1433
+ const waiter = this.structuredInputWaiters.get(payload.requestId);
1434
+ if (waiter) {
1435
+ clearTimeout(waiter.timer);
1436
+ this.structuredInputWaiters.delete(payload.requestId);
1437
+ waiter.resolve(formatStructuredInputResponse(payload.answers));
1438
+ }
1439
+ this.markStructuredInput(payload.conversationId, payload.requestId, {
1440
+ inputPending: false,
1441
+ inputSubmitted: true,
1442
+ answers: payload.answers,
1443
+ });
1444
+ this.updateConversationStatus(pending?.conversationId ?? payload.conversationId, "running");
1445
+ }
1446
+ markStructuredInput(conversationId, requestId, metadata) {
1447
+ const item = this.findItem(conversationId, `input:${requestId}`);
1448
+ if (!item)
1449
+ return;
1450
+ this.upsertItem(conversationId, {
1451
+ ...item,
1452
+ metadata: { ...(item.metadata ?? {}), ...metadata },
1453
+ updatedAt: Date.now(),
1454
+ });
1455
+ }
992
1456
  addItem(conversationId, item) {
993
1457
  const timeline = this.timelines.get(conversationId) ?? [];
994
1458
  timeline.push(item);
@@ -1028,11 +1492,20 @@ export class AgentWorkspaceProxy {
1028
1492
  };
1029
1493
  this.toolConversationIds.set(toolCall.id, conversationId);
1030
1494
  this.toolConversationIds.set(nextToolCall.id, conversationId);
1495
+ const kind = nextToolCall.name.includes("文件")
1496
+ ? "file_change"
1497
+ : nextToolCall.name.includes("命令")
1498
+ ? "command_execution"
1499
+ : "tool_activity";
1031
1500
  this.upsertItem(conversationId, {
1032
1501
  id: `tool:${nextToolCall.id}`,
1033
1502
  conversationId,
1034
1503
  type: "tool_call",
1504
+ kind,
1505
+ itemId: nextToolCall.id,
1035
1506
  toolCall: nextToolCall,
1507
+ commandExecution: kind === "command_execution" ? commandExecutionFromTool(nextToolCall) : undefined,
1508
+ fileChange: kind === "file_change" ? fileChangeFromTool(nextToolCall) : undefined,
1036
1509
  createdAt: nextToolCall.createdAt ?? Date.now(),
1037
1510
  updatedAt: Date.now(),
1038
1511
  });
@@ -1178,12 +1651,13 @@ export class AgentWorkspaceProxy {
1178
1651
  return this.conversationByAgentSessionId.get(threadId);
1179
1652
  return undefined;
1180
1653
  }
1181
- handleExit(message) {
1654
+ handleProviderExit(provider, message) {
1655
+ this.clients.delete(provider);
1656
+ this.agentProtocols.delete(provider);
1182
1657
  this.cancelPendingPermissions();
1183
- this.status = "error";
1184
- this.error = message;
1185
- this.client = undefined;
1186
1658
  for (const conversation of this.conversations.values()) {
1659
+ if (conversation.provider !== provider)
1660
+ continue;
1187
1661
  conversation.status = "error";
1188
1662
  conversation.lastMessagePreview = message;
1189
1663
  conversation.lastActivityAt = Date.now();
@@ -1196,6 +1670,7 @@ export class AgentWorkspaceProxy {
1196
1670
  createdAt: Date.now(),
1197
1671
  });
1198
1672
  }
1673
+ this.sendCapabilities();
1199
1674
  }
1200
1675
  cancelPendingPermissions(conversationId) {
1201
1676
  for (const [requestId, waiter] of this.permissionWaiters) {
@@ -1205,6 +1680,19 @@ export class AgentWorkspaceProxy {
1205
1680
  this.permissionSources.delete(requestId);
1206
1681
  }
1207
1682
  this.permissionWaiters.clear();
1683
+ for (const [requestId, waiter] of this.structuredInputWaiters) {
1684
+ clearTimeout(waiter.timer);
1685
+ waiter.resolve(formatStructuredInputResponse({}));
1686
+ const pending = this.pendingStructuredInputs.get(requestId);
1687
+ if (pending) {
1688
+ this.markStructuredInput(pending.conversationId, requestId, {
1689
+ inputPending: false,
1690
+ inputError: "已停止",
1691
+ });
1692
+ }
1693
+ this.pendingStructuredInputs.delete(requestId);
1694
+ }
1695
+ this.structuredInputWaiters.clear();
1208
1696
  if (conversationId)
1209
1697
  this.updateConversationStatus(conversationId, "idle");
1210
1698
  }
@@ -1272,8 +1760,15 @@ function selectPermissionOption(permission, outcome) {
1272
1760
  function isPermissionRequestMethod(method) {
1273
1761
  return (method === "session/request_permission" ||
1274
1762
  method.endsWith("/requestApproval") ||
1275
- method === "mcpServer/elicitation/request" ||
1276
- method === "item/tool/requestUserInput");
1763
+ method === "mcpServer/elicitation/request");
1764
+ }
1765
+ function formatStructuredInputResponse(answers) {
1766
+ return {
1767
+ answers: Object.fromEntries(Object.entries(answers).map(([questionId, values]) => [
1768
+ questionId,
1769
+ { answers: values.map((value) => value.trim()).filter(Boolean) },
1770
+ ])),
1771
+ };
1277
1772
  }
1278
1773
  function formatPermissionResponse(source, outcome, optionId) {
1279
1774
  if (source === "item/commandExecution/requestApproval" || source === "item/fileChange/requestApproval") {