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.
@@ -41,6 +41,87 @@ interface AgentPermission {
41
41
  options: { id: string; label: string; kind: "allow" | "deny" | "other" }[];
42
42
  }
43
43
 
44
+ type AgentTimelineKind =
45
+ | "chat"
46
+ | "thinking"
47
+ | "tool_activity"
48
+ | "command_execution"
49
+ | "file_change"
50
+ | "subagent_action"
51
+ | "plan"
52
+ | "user_input_prompt"
53
+ | "review"
54
+ | "context_compaction";
55
+
56
+ interface AgentFileChangeEntry {
57
+ path: string;
58
+ kind?: string;
59
+ added?: number;
60
+ removed?: number;
61
+ }
62
+
63
+ interface AgentFileChange {
64
+ entries: AgentFileChangeEntry[];
65
+ diff?: string;
66
+ summary?: string;
67
+ changeSetId?: string;
68
+ status?: AgentToolCall["status"];
69
+ }
70
+
71
+ interface AgentCommandExecution {
72
+ command?: string;
73
+ cwd?: string;
74
+ output?: string;
75
+ exitCode?: number | null;
76
+ status?: AgentToolCall["status"];
77
+ }
78
+
79
+ interface AgentStructuredInputOption {
80
+ id: string;
81
+ label: string;
82
+ description?: string;
83
+ }
84
+
85
+ interface AgentStructuredInputQuestion {
86
+ id: string;
87
+ header?: string;
88
+ question: string;
89
+ isOther?: boolean;
90
+ isSecret?: boolean;
91
+ selectionLimit?: number;
92
+ options?: AgentStructuredInputOption[];
93
+ }
94
+
95
+ interface AgentStructuredInput {
96
+ requestId: string;
97
+ questions: AgentStructuredInputQuestion[];
98
+ }
99
+
100
+ interface AgentSubagentRef {
101
+ threadId: string;
102
+ agentId?: string;
103
+ nickname?: string;
104
+ role?: string;
105
+ model?: string;
106
+ prompt?: string;
107
+ }
108
+
109
+ interface AgentSubagentState {
110
+ threadId: string;
111
+ status: string;
112
+ message?: string;
113
+ }
114
+
115
+ interface AgentSubagentAction {
116
+ tool: string;
117
+ status: string;
118
+ prompt?: string;
119
+ model?: string;
120
+ receiverThreadIds: string[];
121
+ receiverAgents: AgentSubagentRef[];
122
+ agentStates: Record<string, AgentSubagentState>;
123
+ }
124
+
44
125
  interface AgentConversation {
45
126
  id: string;
46
127
  agentSessionId?: string;
@@ -61,10 +142,17 @@ interface AgentTimelineItem {
61
142
  id: string;
62
143
  conversationId: string;
63
144
  type: "message" | "tool_call" | "plan" | "permission" | "status" | "error";
145
+ kind?: AgentTimelineKind;
146
+ turnId?: string;
147
+ itemId?: string;
64
148
  role?: "user" | "assistant" | "system";
65
149
  content?: AgentContentBlock[];
66
150
  text?: string;
67
151
  toolCall?: AgentToolCall;
152
+ commandExecution?: AgentCommandExecution;
153
+ fileChange?: AgentFileChange;
154
+ subagent?: AgentSubagentAction;
155
+ structuredInput?: AgentStructuredInput;
68
156
  plan?: AgentPlanStep[];
69
157
  permission?: AgentPermission;
70
158
  status?: AgentStatus;
@@ -80,6 +168,11 @@ interface PendingPermissionWaiter {
80
168
  timer: ReturnType<typeof setTimeout>;
81
169
  }
82
170
 
171
+ interface PendingStructuredInputWaiter {
172
+ resolve: (value: unknown) => void;
173
+ timer: ReturnType<typeof setTimeout>;
174
+ }
175
+
83
176
  const PERMISSION_TIMEOUT_MS = 5 * 60_000;
84
177
  const MAX_TIMELINE_ITEMS = 200;
85
178
 
@@ -109,6 +202,32 @@ function firstString(value: Record<string, unknown> | undefined, keys: string[])
109
202
  return undefined;
110
203
  }
111
204
 
205
+ function normalizedIdentifier(value: string | undefined): string {
206
+ return (value ?? "").toLowerCase().replace(/[_\-\s/]+/g, "");
207
+ }
208
+
209
+ function firstNumber(value: Record<string, unknown> | undefined, keys: string[]): number | undefined {
210
+ if (!value) return undefined;
211
+ for (const key of keys) {
212
+ const next = value[key];
213
+ if (typeof next === "number" && Number.isFinite(next)) return next;
214
+ }
215
+ return undefined;
216
+ }
217
+
218
+ function stringArray(value: unknown): string[] {
219
+ if (!Array.isArray(value)) return [];
220
+ return value.filter((entry): entry is string => typeof entry === "string" && entry.length > 0);
221
+ }
222
+
223
+ function arrayFromKeys(value: Record<string, unknown>, keys: string[]): unknown[] {
224
+ for (const key of keys) {
225
+ const next = value[key];
226
+ if (Array.isArray(next)) return next;
227
+ }
228
+ return [];
229
+ }
230
+
112
231
  function extractItem(value: unknown): Record<string, unknown> | undefined {
113
232
  const raw = asRecord(value);
114
233
  if (!raw) return undefined;
@@ -161,24 +280,29 @@ function nameFromToolMethod(method: string): string {
161
280
  }
162
281
 
163
282
  function isToolItemType(itemType: string | undefined): boolean {
283
+ const normalized = normalizedIdentifier(itemType);
164
284
  return (
165
- itemType === "commandExecution" ||
166
- itemType === "fileChange" ||
167
- itemType === "mcpToolCall" ||
168
- itemType === "dynamicToolCall"
285
+ normalized === "commandexecution" ||
286
+ normalized === "filechange" ||
287
+ normalized === "diff" ||
288
+ normalized === "toolcall" ||
289
+ normalized === "mcptoolcall" ||
290
+ normalized === "dynamictoolcall"
169
291
  );
170
292
  }
171
293
 
172
294
  function toolNameFromItem(item: Record<string, unknown>): string | undefined {
173
295
  const itemType = firstString(item, ["type"]);
174
- if (itemType === "commandExecution") return "命令";
175
- if (itemType === "fileChange") return "文件修改";
176
- if (itemType === "mcpToolCall") {
296
+ const normalized = normalizedIdentifier(itemType);
297
+ if (normalized === "commandexecution") return "命令";
298
+ if (normalized === "filechange" || normalized === "diff") return "文件修改";
299
+ if (normalized === "toolcall") return firstString(item, ["toolName", "tool", "name", "title"]) ?? "工具";
300
+ if (normalized === "mcptoolcall") {
177
301
  const server = firstString(item, ["server"]);
178
302
  const tool = firstString(item, ["tool", "toolName", "name"]);
179
303
  return [server, tool].filter(Boolean).join(" · ") || "MCP 工具";
180
304
  }
181
- if (itemType === "dynamicToolCall") {
305
+ if (normalized === "dynamictoolcall") {
182
306
  const namespace = firstString(item, ["namespace"]);
183
307
  const tool = firstString(item, ["tool", "toolName", "name"]);
184
308
  return [namespace, tool].filter(Boolean).join(" · ") || "工具";
@@ -201,6 +325,95 @@ function summarizeFileChanges(changes: unknown[]): string | undefined {
201
325
  return lines.length > 0 ? lines.slice(0, 8).join("\n") : undefined;
202
326
  }
203
327
 
328
+ function fileChangeEntriesFromItem(item: Record<string, unknown>): AgentFileChangeEntry[] {
329
+ const changes = Array.isArray(item.changes) ? item.changes : [];
330
+ const entries: AgentFileChangeEntry[] = [];
331
+ for (const change of changes) {
332
+ const raw = asRecord(change);
333
+ if (!raw) continue;
334
+ const path =
335
+ firstString(raw, ["path", "file", "filePath", "absolutePath", "relativePath"]) ??
336
+ firstString(asRecord(raw.update), ["path", "file", "filePath"]);
337
+ if (!path) continue;
338
+ const totals = asRecord(raw.totals) ?? asRecord(raw.diffStats) ?? asRecord(raw.stats);
339
+ const entry: AgentFileChangeEntry = { path };
340
+ const kind = firstString(raw, ["kind", "type", "operation", "action"]);
341
+ const added = firstNumber(raw, ["added", "additions"]) ?? firstNumber(totals, ["added", "additions"]);
342
+ const removed = firstNumber(raw, ["removed", "deletions"]) ?? firstNumber(totals, ["removed", "deletions"]);
343
+ if (kind) entry.kind = kind;
344
+ if (added !== undefined) entry.added = added;
345
+ if (removed !== undefined) entry.removed = removed;
346
+ entries.push(entry);
347
+ }
348
+ const directPath = firstString(item, ["path", "file", "filePath", "absolutePath", "relativePath"]);
349
+ if (entries.length === 0 && directPath) {
350
+ const entry: AgentFileChangeEntry = { path: directPath };
351
+ const kind = firstString(item, ["kind", "type", "operation", "action"]);
352
+ if (kind) entry.kind = kind;
353
+ return [entry];
354
+ }
355
+ return entries;
356
+ }
357
+
358
+ function commandExecutionFromItem(
359
+ item: Record<string, unknown>,
360
+ status: AgentToolCall["status"],
361
+ output?: string,
362
+ ): AgentCommandExecution | undefined {
363
+ const command = firstString(item, ["command"]);
364
+ const cwd = firstString(item, ["cwd"]);
365
+ const exitCode = firstNumber(item, ["exitCode", "code"]);
366
+ if (!command && !cwd && !output && exitCode === undefined) return undefined;
367
+ return { command, cwd, output, exitCode: exitCode ?? undefined, status };
368
+ }
369
+
370
+ function fileChangeFromItem(
371
+ item: Record<string, unknown>,
372
+ status: AgentToolCall["status"],
373
+ diff?: string,
374
+ ): AgentFileChange | undefined {
375
+ const entries = fileChangeEntriesFromItem(item);
376
+ const summary = summarizeFileChanges(Array.isArray(item.changes) ? item.changes : []);
377
+ const changeSetId = firstString(item, ["changeSetId", "changesetId", "patchId"]);
378
+ if (entries.length === 0 && !diff && !summary && !changeSetId) return undefined;
379
+ return { entries, diff, summary, changeSetId, status };
380
+ }
381
+
382
+ function commandExecutionFromTool(toolCall: AgentToolCall): AgentCommandExecution | undefined {
383
+ const input = toolCall.input?.trim();
384
+ if (!input && !toolCall.output) return undefined;
385
+ const [commandPart, cwdPart] = input?.split(/\n\ncwd:\s*/i) ?? [];
386
+ return {
387
+ command: commandPart || input,
388
+ cwd: cwdPart,
389
+ output: toolCall.output,
390
+ status: toolCall.status,
391
+ };
392
+ }
393
+
394
+ function fileChangeFromTool(toolCall: AgentToolCall): AgentFileChange | undefined {
395
+ const diff = toolCall.output && looksLikeDiff(toolCall.output) ? toolCall.output : undefined;
396
+ const entries: AgentFileChangeEntry[] = (toolCall.input ?? "")
397
+ .split("\n")
398
+ .map((line) => line.trim())
399
+ .filter(Boolean)
400
+ .map((line) => {
401
+ const [kind, ...rest] = line.split(/\s+/);
402
+ const path = rest.length > 0 ? rest.join(" ") : kind;
403
+ const entry: AgentFileChangeEntry = { path: path ?? line };
404
+ if (rest.length > 0 && kind) entry.kind = kind;
405
+ return entry;
406
+ })
407
+ .filter((entry) => entry.path.length > 0);
408
+ if (entries.length === 0 && !diff && !toolCall.output) return undefined;
409
+ return {
410
+ entries,
411
+ diff,
412
+ summary: diff ? undefined : toolCall.output,
413
+ status: toolCall.status,
414
+ };
415
+ }
416
+
204
417
  function looksLikeDiff(text: string): boolean {
205
418
  const value = text.trim();
206
419
  return (
@@ -247,13 +460,14 @@ function extractDiffText(value: unknown): string | undefined {
247
460
 
248
461
  function toolInputFromItem(item: Record<string, unknown>): string | undefined {
249
462
  const itemType = firstString(item, ["type"]);
250
- if (itemType === "commandExecution") {
463
+ const normalized = normalizedIdentifier(itemType);
464
+ if (normalized === "commandexecution") {
251
465
  const command = firstString(item, ["command"]);
252
466
  const cwd = firstString(item, ["cwd"]);
253
467
  if (command && cwd) return `${command}\n\ncwd: ${cwd}`;
254
468
  return command ?? cwd;
255
469
  }
256
- if (itemType === "fileChange") {
470
+ if (normalized === "filechange" || normalized === "diff") {
257
471
  const changes = Array.isArray(item.changes) ? item.changes : [];
258
472
  return summarizeFileChanges(changes) ?? firstString(item, ["path", "file", "filePath", "absolutePath", "relativePath"]);
259
473
  }
@@ -271,6 +485,167 @@ function textFromBlocks(blocks: AgentContentBlock[]): string {
271
485
  .join("\n");
272
486
  }
273
487
 
488
+ function isSubagentItemType(itemType: string | undefined): boolean {
489
+ const normalized = normalizedIdentifier(itemType);
490
+ return (
491
+ normalized === "collabagenttoolcall" ||
492
+ normalized === "collabtoolcall" ||
493
+ normalized.startsWith("collabagentspawn") ||
494
+ normalized.startsWith("collabwaiting") ||
495
+ normalized.startsWith("collabclose") ||
496
+ normalized.startsWith("collabresume") ||
497
+ normalized.startsWith("collabagentinteraction")
498
+ );
499
+ }
500
+
501
+ function parseSubagentRef(value: unknown): AgentSubagentRef | undefined {
502
+ const raw = asRecord(value);
503
+ if (!raw) return undefined;
504
+ const threadId = firstString(raw, ["threadId", "threadID", "id", "sessionId"]);
505
+ if (!threadId) return undefined;
506
+ return {
507
+ threadId,
508
+ agentId: firstString(raw, ["agentId", "agentID"]),
509
+ nickname: firstString(raw, ["nickname", "name", "label"]),
510
+ role: firstString(raw, ["role", "kind"]),
511
+ model: firstString(raw, ["model", "modelName"]),
512
+ prompt: firstString(raw, ["prompt", "instructions", "message"]),
513
+ };
514
+ }
515
+
516
+ function parseSubagentStates(value: unknown): Record<string, AgentSubagentState> {
517
+ const result: Record<string, AgentSubagentState> = {};
518
+ if (Array.isArray(value)) {
519
+ for (const entry of value) {
520
+ const raw = asRecord(entry);
521
+ const threadId = firstString(raw, ["threadId", "threadID", "id", "sessionId"]);
522
+ const status = firstString(raw, ["status", "state", "phase"]);
523
+ if (!threadId || !status) continue;
524
+ result[threadId] = {
525
+ threadId,
526
+ status,
527
+ message: firstString(raw, ["message", "summary", "text"]),
528
+ };
529
+ }
530
+ return result;
531
+ }
532
+ const raw = asRecord(value);
533
+ if (!raw) return result;
534
+ for (const [threadId, entry] of Object.entries(raw)) {
535
+ const state = asRecord(entry);
536
+ if (state) {
537
+ result[threadId] = {
538
+ threadId,
539
+ status: firstString(state, ["status", "state", "phase"]) ?? "running",
540
+ message: firstString(state, ["message", "summary", "text"]),
541
+ };
542
+ } else if (typeof entry === "string") {
543
+ result[threadId] = { threadId, status: entry };
544
+ }
545
+ }
546
+ return result;
547
+ }
548
+
549
+ function parseStructuredInputOption(value: unknown, index: number): AgentStructuredInputOption | undefined {
550
+ const raw = asRecord(value);
551
+ if (!raw) {
552
+ if (typeof value === "string" && value.trim()) {
553
+ return { id: `option-${index + 1}`, label: value.trim() };
554
+ }
555
+ return undefined;
556
+ }
557
+ const label = firstString(raw, ["label", "title", "text", "value"]);
558
+ if (!label) return undefined;
559
+ return {
560
+ id: firstString(raw, ["id", "optionId", "value"]) ?? `option-${index + 1}`,
561
+ label,
562
+ description: firstString(raw, ["description", "detail", "subtitle"]),
563
+ };
564
+ }
565
+
566
+ function parseStructuredInputQuestion(value: unknown, index: number): AgentStructuredInputQuestion | undefined {
567
+ const raw = asRecord(value);
568
+ if (!raw) return undefined;
569
+ const question = firstString(raw, ["question", "prompt", "text", "message", "label"]);
570
+ if (!question) return undefined;
571
+ const options = arrayFromKeys(raw, ["options", "choices", "items"])
572
+ .map(parseStructuredInputOption)
573
+ .filter((option): option is AgentStructuredInputOption => Boolean(option));
574
+ return {
575
+ id: firstString(raw, ["id", "questionId", "key"]) ?? `question-${index + 1}`,
576
+ header: firstString(raw, ["header", "title"]),
577
+ question,
578
+ isOther: raw.isOther === true,
579
+ isSecret: raw.isSecret === true || raw.secret === true,
580
+ selectionLimit: firstNumber(raw, ["selectionLimit", "maxSelections"]),
581
+ options: options.length > 0 ? options : undefined,
582
+ };
583
+ }
584
+
585
+ function decodeStructuredInput(value: unknown): AgentStructuredInput | undefined {
586
+ const raw = asRecord(value) ?? {};
587
+ const questions = arrayFromKeys(raw, ["questions", "items", "prompts"])
588
+ .map(parseStructuredInputQuestion)
589
+ .filter((question): question is AgentStructuredInputQuestion => Boolean(question));
590
+ if (questions.length === 0) {
591
+ const single = parseStructuredInputQuestion(raw, 0);
592
+ if (single) questions.push(single);
593
+ }
594
+ if (questions.length === 0) return undefined;
595
+ return {
596
+ requestId: firstString(raw, ["requestId", "id", "inputId"]) ?? id("input"),
597
+ questions,
598
+ };
599
+ }
600
+
601
+ function decodeSubagentAction(
602
+ item: Record<string, unknown>,
603
+ status: "running" | "completed" | "failed" | "pending",
604
+ ): AgentSubagentAction | undefined {
605
+ const nested = asRecord(item.action) ?? asRecord(item.toolCall) ?? asRecord(item.call) ?? {};
606
+ const receiverAgents = [
607
+ ...arrayFromKeys(item, ["receiverAgents", "agents", "subagents", "receivers"]).map(parseSubagentRef),
608
+ ...arrayFromKeys(nested, ["receiverAgents", "agents", "subagents", "receivers"]).map(parseSubagentRef),
609
+ ].filter((entry): entry is AgentSubagentRef => Boolean(entry));
610
+ const receiverThreadIds = [
611
+ ...stringArray(item.receiverThreadIds),
612
+ ...stringArray(item.threadIds),
613
+ ...stringArray(item.childThreadIds),
614
+ ...stringArray(item.agentThreadIds),
615
+ ...stringArray(nested.receiverThreadIds),
616
+ ...stringArray(nested.threadIds),
617
+ ...receiverAgents.map((agent) => agent.threadId),
618
+ ].filter((threadId, index, array) => array.indexOf(threadId) === index);
619
+ const agentStates = {
620
+ ...parseSubagentStates(item.agentStates ?? item.states ?? item.statusByThread),
621
+ ...parseSubagentStates(nested.agentStates ?? nested.states ?? nested.statusByThread),
622
+ };
623
+ if (receiverThreadIds.length === 0 && Object.keys(agentStates).length === 0) return undefined;
624
+ return {
625
+ tool: firstString(item, ["tool", "toolName", "name", "type"]) ??
626
+ firstString(nested, ["tool", "toolName", "name", "type"]) ??
627
+ "subagent",
628
+ status,
629
+ prompt: firstString(item, ["prompt", "instructions", "message"]) ??
630
+ firstString(nested, ["prompt", "instructions", "message"]),
631
+ model: firstString(item, ["model", "modelName"]) ?? firstString(nested, ["model", "modelName"]),
632
+ receiverThreadIds,
633
+ receiverAgents,
634
+ agentStates,
635
+ };
636
+ }
637
+
638
+ function summarizeSubagentAction(action: AgentSubagentAction): string {
639
+ const count = Math.max(1, action.receiverThreadIds.length, action.receiverAgents.length);
640
+ const normalized = normalizedIdentifier(action.tool);
641
+ if (normalized.includes("spawn")) return `启动 ${count} 个子 Agent`;
642
+ if (normalized.includes("wait")) return `等待 ${count} 个子 Agent`;
643
+ if (normalized.includes("resume")) return `恢复 ${count} 个子 Agent`;
644
+ if (normalized.includes("close")) return `关闭 ${count} 个子 Agent`;
645
+ if (normalized.includes("sendinput")) return `更新 ${count} 个子 Agent`;
646
+ return count === 1 ? "子 Agent 活动" : `${count} 个子 Agent 活动`;
647
+ }
648
+
274
649
  function previewText(text: string): string {
275
650
  return text.replace(/\s+/g, " ").trim().slice(0, 160);
276
651
  }
@@ -281,21 +656,9 @@ function providerLabel(provider: AgentProvider): string {
281
656
  return "Custom";
282
657
  }
283
658
 
284
- function providerSetupReason(provider: AgentProvider, activeProvider: AgentProvider, error?: string): string {
285
- if (provider === activeProvider) {
286
- return error ?? `${providerLabel(provider)} Agent 正在初始化或不可用。`;
287
- }
288
- if (provider === "codex") {
289
- return `当前 CLI 启用的是 ${providerLabel(activeProvider)} Agent。`;
290
- }
291
- if (provider === "claude") {
292
- return "Claude ACP adapter 尚未启用,请用 --agent-provider claude --agent-command 配置。";
293
- }
294
- return "Custom Agent 需要用 --agent-provider custom --agent-command 配置后才能使用。";
295
- }
296
-
297
659
  export class AgentWorkspaceProxy {
298
- private client: AcpClient | undefined;
660
+ private clients = new Map<AgentProvider, AcpClient>();
661
+ private agentProtocols = new Map<AgentProvider, AgentProtocol>();
299
662
  private initialized = false;
300
663
  private status: AgentStatus = "unavailable";
301
664
  private error: string | undefined;
@@ -308,14 +671,15 @@ export class AgentWorkspaceProxy {
308
671
  private pendingPermissions = new Map<string, AgentPermission>();
309
672
  private permissionWaiters = new Map<string, PendingPermissionWaiter>();
310
673
  private permissionSources = new Map<string, string>();
674
+ private pendingStructuredInputs = new Map<string, { conversationId: string; input: AgentStructuredInput }>();
675
+ private structuredInputWaiters = new Map<string, PendingStructuredInputWaiter>();
311
676
  private toolConversationIds = new Map<string, string>();
312
- private agentProtocol: AgentProtocol | undefined;
313
677
 
314
678
  constructor(
315
679
  private readonly input: {
316
680
  sessionId: string;
317
681
  cwd: string;
318
- provider: AgentProvider;
682
+ availableProviders: AgentProvider[];
319
683
  command?: string;
320
684
  send: (envelope: Envelope) => void;
321
685
  verbose?: boolean;
@@ -359,7 +723,8 @@ export class AgentWorkspaceProxy {
359
723
  const payload = parseTypedPayload("agent.v2.cancel", envelope.payload);
360
724
  const conversation = this.conversations.get(payload.conversationId);
361
725
  this.cancelPendingPermissions(payload.conversationId);
362
- this.client?.cancel({
726
+ const cancelClient = conversation ? this.clientForProvider(conversation.provider) : undefined;
727
+ cancelClient?.cancel({
363
728
  sessionId: conversation?.agentSessionId,
364
729
  turnId: this.currentTurnId,
365
730
  });
@@ -373,93 +738,113 @@ export class AgentWorkspaceProxy {
373
738
  this.respondPermission(payload);
374
739
  break;
375
740
  }
741
+ case "agent.v2.structured_input.respond": {
742
+ const payload = parseTypedPayload("agent.v2.structured_input.respond", envelope.payload);
743
+ this.respondStructuredInput(payload);
744
+ break;
745
+ }
376
746
  }
377
747
  }
378
748
 
379
749
  stop(): void {
380
- this.client?.stop();
381
- this.client = undefined;
750
+ for (const client of this.clients.values()) {
751
+ client.stop();
752
+ }
753
+ this.clients.clear();
754
+ }
755
+
756
+ private clientForProvider(provider: AgentProvider): AcpClient | undefined {
757
+ return this.clients.get(provider);
758
+ }
759
+
760
+ private protocolForProvider(provider: AgentProvider): AgentProtocol | undefined {
761
+ return this.agentProtocols.get(provider);
382
762
  }
383
763
 
384
764
  private async initialize(): Promise<void> {
385
765
  if (this.initialized) return;
386
- await this.ensureClient();
766
+ // trigger capability report immediately, lazy-start providers on first use
767
+ this.initialized = true;
768
+ this.status = "idle";
769
+ this.error = undefined;
770
+ this.sendCapabilities();
387
771
  }
388
772
 
389
- private async ensureClient(): Promise<void> {
390
- if (this.client) return;
773
+ private async ensureProviderClient(provider: AgentProvider): Promise<AcpClient | undefined> {
774
+ const existing = this.clients.get(provider);
775
+ if (existing) return existing;
391
776
 
392
777
  const resolved = resolveAgentCommand({
393
- provider: this.input.provider,
778
+ provider,
394
779
  command: this.input.command,
395
780
  });
396
781
  if (!resolved) {
397
- this.status = "unavailable";
398
- this.error = `Agent Workspace requires --agent-command for ${this.input.provider}`;
399
- return;
782
+ if (this.input.verbose) {
783
+ process.stderr.write(`[agent:v2] no command for provider ${provider}\n`);
784
+ }
785
+ return undefined;
400
786
  }
401
787
 
402
788
  try {
403
- this.agentProtocol = resolved.protocol;
404
- this.client = new AcpClient({
789
+ this.agentProtocols.set(provider, resolved.protocol);
790
+ const client = new AcpClient({
405
791
  command: resolved.command,
406
792
  protocol: resolved.protocol,
407
793
  framing: resolved.framing,
408
794
  cwd: this.input.cwd,
409
795
  onNotification: (method, params) => this.handleNotification(method, params),
410
796
  onRequest: (method, params) => this.handleRequest(method, params),
411
- onExit: (message) => this.handleExit(message),
797
+ onExit: (message) => this.handleProviderExit(provider, message),
412
798
  });
413
- await this.client.initialize();
414
- this.initialized = true;
799
+ await client.initialize();
800
+ this.clients.set(provider, client);
415
801
  this.status = "idle";
416
802
  this.error = undefined;
803
+ this.sendCapabilities();
804
+ return client;
417
805
  } catch (error) {
418
- this.client?.stop();
419
- this.client = undefined;
420
- this.status = "error";
421
- this.error = error instanceof Error ? error.message : String(error);
806
+ if (this.input.verbose) {
807
+ process.stderr.write(`[agent:v2] failed to start ${provider}: ${error instanceof Error ? error.message : String(error)}\n`);
808
+ }
809
+ return undefined;
422
810
  }
423
811
  }
424
812
 
425
813
  private sendCapabilities(): void {
426
- const enabled = Boolean(this.client && this.initialized && !this.error);
427
- const supportsImages = enabled && this.agentProtocol === "codex-app-server";
428
- const activeProvider = this.input.provider;
429
- const providerIds: AgentProvider[] = ["codex", "claude"];
430
- if (activeProvider === "custom") providerIds.push("custom");
814
+ const providers = this.input.availableProviders.map((provider) => {
815
+ const client = this.clients.get(provider);
816
+ const protocol = this.agentProtocols.get(provider);
817
+ const enabled = Boolean(client);
818
+ const supportsImages = enabled && protocol === "codex-app-server";
819
+ return {
820
+ id: provider,
821
+ label: providerLabel(provider),
822
+ enabled,
823
+ reason: enabled ? undefined : `${providerLabel(provider)} 未安装或启动失败`,
824
+ supportsImages,
825
+ supportsPermission: enabled,
826
+ supportsPlan: enabled,
827
+ supportsCancel: enabled,
828
+ };
829
+ });
830
+ const anyEnabled = providers.some((p) => p.enabled);
431
831
  this.input.send(createEnvelope({
432
832
  type: "agent.v2.capabilities",
433
833
  sessionId: this.input.sessionId,
434
834
  payload: {
435
- enabled,
436
- provider: activeProvider,
437
- providers: providerIds.map((provider) => {
438
- const isActive = provider === activeProvider;
439
- const canUse = isActive && enabled;
440
- return {
441
- id: provider,
442
- label: providerLabel(provider),
443
- enabled: canUse,
444
- reason: canUse
445
- ? undefined
446
- : providerSetupReason(provider, activeProvider, isActive ? this.error : undefined),
447
- supportsImages: canUse && supportsImages,
448
- supportsPermission: canUse,
449
- supportsPlan: canUse,
450
- supportsCancel: canUse,
451
- };
452
- }),
835
+ enabled: anyEnabled,
836
+ provider: this.input.availableProviders[0] ?? "codex",
837
+ providers,
453
838
  protocolVersion: 1,
454
839
  workspaceProtocolVersion: 2,
455
- error: enabled ? undefined : this.error,
456
- supportsSessionList: enabled,
457
- supportsSessionLoad: enabled,
458
- supportsImages,
840
+ error: anyEnabled ? undefined : "没有可用的 Agent provider。请安装 Claude Code 或 Codex CLI。",
841
+ supportsSessionList: anyEnabled,
842
+ supportsSessionLoad: anyEnabled,
843
+ supportsImages: providers.some((p) => p.supportsImages),
459
844
  supportsAudio: false,
460
- supportsPermission: enabled,
461
- supportsPlan: enabled,
462
- supportsCancel: enabled,
845
+ supportsPermission: anyEnabled,
846
+ supportsPlan: anyEnabled,
847
+ supportsCancel: anyEnabled,
463
848
  },
464
849
  }));
465
850
  }
@@ -474,18 +859,22 @@ export class AgentWorkspaceProxy {
474
859
  permissionMode?: AgentPermissionMode;
475
860
  title?: string;
476
861
  }): Promise<AgentConversation | undefined> {
477
- await this.ensureClient();
478
- this.sendCapabilities();
479
- if (payload.provider && payload.provider !== this.input.provider) {
862
+ const provider = payload.provider ?? this.input.availableProviders[0];
863
+ if (!provider) {
864
+ return this.openFailure(payload, "没有可用的 Agent provider。");
865
+ }
866
+ if (!this.input.availableProviders.includes(provider)) {
480
867
  return this.openFailure(
481
868
  payload,
482
- `当前 CLI 只启用了 ${providerLabel(this.input.provider)} Agent,不能在这个会话里启动 ${providerLabel(payload.provider)}。`,
869
+ `${providerLabel(provider)} 未安装或不可用。`,
483
870
  );
484
871
  }
485
- if (!this.client) {
872
+
873
+ const client = await this.ensureProviderClient(provider);
874
+ if (!client) {
486
875
  return this.openFailure(
487
876
  payload,
488
- this.error ?? "Agent Workspace 不可用,请确认 CLI 已使用 --agent-ui 启动。",
877
+ `${providerLabel(provider)} 启动失败。请确认 CLI 已安装并可用。`,
489
878
  );
490
879
  }
491
880
 
@@ -513,8 +902,8 @@ export class AgentWorkspaceProxy {
513
902
 
514
903
  try {
515
904
  const result = agentSessionId
516
- ? await this.client.loadSession({ sessionId: agentSessionId, cwd })
517
- : await this.client.newSession({ cwd });
905
+ ? await client.loadSession({ sessionId: agentSessionId, cwd })
906
+ : await client.newSession({ cwd });
518
907
  agentSessionId = this.extractSessionId(result) ?? agentSessionId ?? id("agent-session");
519
908
  const now = Date.now();
520
909
  const conversationId = payload.conversationId ?? `agent:${agentSessionId}`;
@@ -522,7 +911,7 @@ export class AgentWorkspaceProxy {
522
911
  ...existingConversation,
523
912
  id: conversationId,
524
913
  agentSessionId,
525
- provider: payload.provider ?? this.input.provider,
914
+ provider,
526
915
  cwd,
527
916
  title: payload.title ?? existingConversation?.title ?? titleFromCwd(cwd),
528
917
  model: payload.model ?? existingConversation?.model,
@@ -567,7 +956,7 @@ export class AgentWorkspaceProxy {
567
956
  const now = Date.now();
568
957
  const conversation: AgentConversation = {
569
958
  id: fallbackId,
570
- provider: payload.provider ?? this.input.provider,
959
+ provider: payload.provider ?? this.input.availableProviders[0] ?? "codex",
571
960
  cwd,
572
961
  title: payload.title ?? titleFromCwd(cwd),
573
962
  model: payload.model,
@@ -607,9 +996,12 @@ export class AgentWorkspaceProxy {
607
996
  const conversation =
608
997
  this.conversations.get(payload.conversationId) ??
609
998
  await this.openConversation({ conversationId: payload.conversationId });
610
- if (!conversation || !this.client || !conversation.agentSessionId) return;
999
+ if (!conversation || !conversation.agentSessionId) return;
1000
+ const client = this.clientForProvider(conversation.provider);
1001
+ if (!client) return;
611
1002
 
612
- if (payload.contentBlocks.some((block) => block.type === "image") && this.agentProtocol !== "codex-app-server") {
1003
+ const protocol = this.protocolForProvider(conversation.provider);
1004
+ if (payload.contentBlocks.some((block) => block.type === "image") && protocol !== "codex-app-server") {
613
1005
  conversation.status = "idle";
614
1006
  conversation.lastActivityAt = Date.now();
615
1007
  this.emitConversation(conversation);
@@ -643,7 +1035,7 @@ export class AgentWorkspaceProxy {
643
1035
  this.emitConversation(conversation);
644
1036
 
645
1037
  try {
646
- const result = await this.client.prompt({
1038
+ const result = await client.prompt({
647
1039
  sessionId: conversation.agentSessionId,
648
1040
  content: payload.contentBlocks,
649
1041
  clientMessageId: payload.clientMessageId,
@@ -670,6 +1062,9 @@ export class AgentWorkspaceProxy {
670
1062
  }
671
1063
 
672
1064
  private handleRequest(method: string, params: unknown): Promise<unknown> | unknown {
1065
+ if (method === "item/tool/requestUserInput" || method === "tool/requestUserInput") {
1066
+ return this.handleStructuredInput(params, true);
1067
+ }
673
1068
  if (isPermissionRequestMethod(method)) {
674
1069
  return this.handlePermission(params, true, method);
675
1070
  }
@@ -696,6 +1091,10 @@ export class AgentWorkspaceProxy {
696
1091
  }
697
1092
 
698
1093
  const conversationId = this.conversationIdFromParams(params) ?? this.activeConversationId;
1094
+ if (method === "item/tool/requestUserInput" || method === "tool/requestUserInput") {
1095
+ this.handleStructuredInput(params);
1096
+ return;
1097
+ }
699
1098
  if (isPermissionRequestMethod(method)) {
700
1099
  this.handlePermission(params, false, method);
701
1100
  return;
@@ -845,14 +1244,20 @@ export class AgentWorkspaceProxy {
845
1244
  const item = extractItem(params);
846
1245
  if (!item) return;
847
1246
  const itemType = firstString(item, ["type"]);
848
- if (itemType === "agentMessage" || itemType === "assistantMessage") {
1247
+ const normalizedItemType = normalizedIdentifier(itemType);
1248
+ if (normalizedItemType === "agentmessage" || normalizedItemType === "assistantmessage") {
849
1249
  this.handleCompletedMessageItem(item, true);
850
1250
  return;
851
1251
  }
852
- if (itemType === "plan") {
1252
+ if (normalizedItemType === "plan") {
853
1253
  this.handlePlanUpdated({ plan: [item] });
854
1254
  return;
855
1255
  }
1256
+ if (isSubagentItemType(itemType)) {
1257
+ this.handleSubagentItem(item, "running", true);
1258
+ return;
1259
+ }
1260
+ if (this.handleSemanticSystemItem(item, "running", true)) return;
856
1261
  const conversationId = this.conversationIdFromParams(item) ?? this.activeConversationId;
857
1262
  const toolCall = this.toolCallFromItem(item, "running");
858
1263
  if (!conversationId || !toolCall) return;
@@ -864,10 +1269,20 @@ export class AgentWorkspaceProxy {
864
1269
  const item = extractItem(params);
865
1270
  if (!item) return;
866
1271
  const itemType = firstString(item, ["type"]);
867
- if (itemType === "agentMessage" || itemType === "assistantMessage") {
1272
+ const normalizedItemType = normalizedIdentifier(itemType);
1273
+ if (normalizedItemType === "agentmessage" || normalizedItemType === "assistantmessage") {
868
1274
  this.handleCompletedMessageItem(item, false);
869
1275
  return;
870
1276
  }
1277
+ if (normalizedItemType === "plan") {
1278
+ this.handlePlanDelta({ ...item, delta: firstString(item, ["text", "content", "message"]) });
1279
+ return;
1280
+ }
1281
+ if (isSubagentItemType(itemType)) {
1282
+ this.handleSubagentItem(item, normalizeToolStatus(item.status, true), false);
1283
+ return;
1284
+ }
1285
+ if (this.handleSemanticSystemItem(item, normalizeToolStatus(item.status, true), false)) return;
871
1286
  const conversationId = this.conversationIdFromParams(item) ?? this.activeConversationId;
872
1287
  const toolCall = this.toolCallFromItem(item, normalizeToolStatus(item.status, true));
873
1288
  if (!conversationId || !toolCall) return;
@@ -1029,6 +1444,127 @@ export class AgentWorkspaceProxy {
1029
1444
  this.updateConversationPreview(conversationId, text, raw.done === true ? "idle" : "running");
1030
1445
  }
1031
1446
 
1447
+ private handleSemanticSystemItem(
1448
+ item: Record<string, unknown>,
1449
+ status: AgentToolCall["status"],
1450
+ streaming: boolean,
1451
+ ): boolean {
1452
+ const itemType = firstString(item, ["type"]);
1453
+ const normalized = normalizedIdentifier(itemType);
1454
+ const conversationId = this.conversationIdFromParams(item) ?? this.activeConversationId;
1455
+ if (!conversationId) return false;
1456
+ const itemId = firstString(item, ["id", "itemId"]) ?? id("item");
1457
+ const existing = this.findItem(conversationId, itemId);
1458
+ const base = {
1459
+ id: itemId,
1460
+ conversationId,
1461
+ type: "status" as const,
1462
+ role: "system" as const,
1463
+ turnId: this.extractTurnId(item) ?? this.currentTurnId,
1464
+ itemId,
1465
+ createdAt: existing?.createdAt ?? Date.now(),
1466
+ updatedAt: Date.now(),
1467
+ isStreaming: streaming,
1468
+ };
1469
+
1470
+ if (normalized === "reasoning" || normalized === "thinking") {
1471
+ const text = firstString(item, ["text", "content", "summary", "message"]) ??
1472
+ stringifyDefined(item.contentItems ?? item.summary);
1473
+ this.upsertItem(conversationId, {
1474
+ ...base,
1475
+ kind: "thinking",
1476
+ text: text ?? (streaming ? "正在思考" : "完成思考"),
1477
+ });
1478
+ return true;
1479
+ }
1480
+
1481
+ if (normalized === "enteredreviewmode") {
1482
+ const target = firstString(item, ["review", "target", "label"]) ?? "changes";
1483
+ this.upsertItem(conversationId, {
1484
+ ...base,
1485
+ kind: "review",
1486
+ text: status === "completed" ? `已完成审查 ${target}` : `正在审查 ${target}`,
1487
+ });
1488
+ return true;
1489
+ }
1490
+
1491
+ if (normalized === "contextcompaction") {
1492
+ this.upsertItem(conversationId, {
1493
+ ...base,
1494
+ kind: "context_compaction",
1495
+ text: status === "completed" ? "上下文已压缩" : "正在压缩上下文",
1496
+ });
1497
+ return true;
1498
+ }
1499
+
1500
+ return false;
1501
+ }
1502
+
1503
+ private handleSubagentItem(
1504
+ item: Record<string, unknown>,
1505
+ status: AgentToolCall["status"],
1506
+ streaming: boolean,
1507
+ ): void {
1508
+ const conversationId = this.conversationIdFromParams(item) ?? this.activeConversationId;
1509
+ if (!conversationId) return;
1510
+ const subagent = decodeSubagentAction(item, status);
1511
+ if (!subagent) return;
1512
+ const itemId = firstString(item, ["id", "itemId"]) ?? id("subagent");
1513
+ const text = summarizeSubagentAction(subagent);
1514
+ const existing = this.findItem(conversationId, itemId);
1515
+ this.upsertItem(conversationId, {
1516
+ id: itemId,
1517
+ conversationId,
1518
+ type: "status",
1519
+ kind: "subagent_action",
1520
+ role: "system",
1521
+ turnId: this.extractTurnId(item) ?? this.currentTurnId,
1522
+ itemId,
1523
+ text,
1524
+ subagent,
1525
+ createdAt: existing?.createdAt ?? Date.now(),
1526
+ updatedAt: Date.now(),
1527
+ isStreaming: streaming,
1528
+ });
1529
+ this.updateConversationPreview(conversationId, text, streaming ? "running" : "idle");
1530
+ }
1531
+
1532
+ private handleStructuredInput(params: unknown, waitForResponse = false): Promise<unknown> | void {
1533
+ const raw = asRecord(params) ?? {};
1534
+ const conversationId = this.conversationIdFromParams(raw) ?? this.activeConversationId;
1535
+ if (!conversationId) return waitForResponse ? Promise.resolve(formatStructuredInputResponse({})) : undefined;
1536
+ const structuredInput = decodeStructuredInput(raw);
1537
+ if (!structuredInput) return waitForResponse ? Promise.resolve(formatStructuredInputResponse({})) : undefined;
1538
+ const text = structuredInput.questions.map((question) => question.question).join("\n");
1539
+ this.pendingStructuredInputs.set(structuredInput.requestId, { conversationId, input: structuredInput });
1540
+ this.upsertItem(conversationId, {
1541
+ id: `input:${structuredInput.requestId}`,
1542
+ conversationId,
1543
+ type: "status",
1544
+ kind: "user_input_prompt",
1545
+ role: "system",
1546
+ text,
1547
+ structuredInput,
1548
+ metadata: { inputPending: true },
1549
+ createdAt: this.findItem(conversationId, `input:${structuredInput.requestId}`)?.createdAt ?? Date.now(),
1550
+ updatedAt: Date.now(),
1551
+ });
1552
+ this.updateConversationPreview(conversationId, "需要用户输入", "running");
1553
+ if (!waitForResponse) return;
1554
+ return new Promise((resolve) => {
1555
+ const timer = setTimeout(() => {
1556
+ this.pendingStructuredInputs.delete(structuredInput.requestId);
1557
+ this.structuredInputWaiters.delete(structuredInput.requestId);
1558
+ resolve(formatStructuredInputResponse({}));
1559
+ this.markStructuredInput(conversationId, structuredInput.requestId, {
1560
+ inputPending: false,
1561
+ inputError: "等待用户输入超时",
1562
+ });
1563
+ }, PERMISSION_TIMEOUT_MS);
1564
+ this.structuredInputWaiters.set(structuredInput.requestId, { resolve, timer });
1565
+ });
1566
+ }
1567
+
1032
1568
  private toolCallFromItem(
1033
1569
  item: Record<string, unknown>,
1034
1570
  fallbackStatus: AgentToolCall["status"],
@@ -1036,13 +1572,14 @@ export class AgentWorkspaceProxy {
1036
1572
  const itemId = firstString(item, ["id", "itemId", "toolCallId"]);
1037
1573
  if (!itemId) return undefined;
1038
1574
  const itemType = firstString(item, ["type"]);
1575
+ const normalizedItemType = normalizedIdentifier(itemType);
1039
1576
  const name = toolNameFromItem(item);
1040
1577
  if (!name && !isToolItemType(itemType)) return undefined;
1041
1578
  const bufferedOutput = this.toolOutputBuffers.get(itemId);
1042
1579
  const rawOutput =
1043
1580
  firstString(item, ["aggregatedOutput", "output", "stdout", "stderr"]) ??
1044
1581
  stringifyDefined(item.result ?? item.error ?? item.contentItems);
1045
- const output = itemType === "fileChange"
1582
+ const output = normalizedItemType === "filechange" || normalizedItemType === "diff"
1046
1583
  ? extractDiffText(item) ?? bufferedOutput ?? rawOutput
1047
1584
  : rawOutput ?? bufferedOutput;
1048
1585
  return {
@@ -1124,8 +1661,10 @@ export class AgentWorkspaceProxy {
1124
1661
  ));
1125
1662
  this.permissionSources.delete(payload.requestId);
1126
1663
  } else {
1127
- this.client?.respondPermission({
1128
- sessionId: this.conversations.get(payload.conversationId)?.agentSessionId,
1664
+ const conversation = this.conversations.get(payload.conversationId);
1665
+ const respondClient = conversation ? this.clientForProvider(conversation.provider) : undefined;
1666
+ respondClient?.respondPermission({
1667
+ sessionId: conversation?.agentSessionId,
1129
1668
  requestId: payload.requestId,
1130
1669
  outcome: payload.outcome === "cancelled" ? "deny" : payload.outcome,
1131
1670
  optionId: selectedOptionId,
@@ -1134,6 +1673,41 @@ export class AgentWorkspaceProxy {
1134
1673
  this.updateConversationStatus(payload.conversationId, "running");
1135
1674
  }
1136
1675
 
1676
+ private respondStructuredInput(payload: {
1677
+ conversationId: string;
1678
+ requestId: string;
1679
+ answers: Record<string, string[]>;
1680
+ }): void {
1681
+ const pending = this.pendingStructuredInputs.get(payload.requestId);
1682
+ this.pendingStructuredInputs.delete(payload.requestId);
1683
+ const waiter = this.structuredInputWaiters.get(payload.requestId);
1684
+ if (waiter) {
1685
+ clearTimeout(waiter.timer);
1686
+ this.structuredInputWaiters.delete(payload.requestId);
1687
+ waiter.resolve(formatStructuredInputResponse(payload.answers));
1688
+ }
1689
+ this.markStructuredInput(payload.conversationId, payload.requestId, {
1690
+ inputPending: false,
1691
+ inputSubmitted: true,
1692
+ answers: payload.answers,
1693
+ });
1694
+ this.updateConversationStatus(pending?.conversationId ?? payload.conversationId, "running");
1695
+ }
1696
+
1697
+ private markStructuredInput(
1698
+ conversationId: string,
1699
+ requestId: string,
1700
+ metadata: Record<string, unknown>,
1701
+ ): void {
1702
+ const item = this.findItem(conversationId, `input:${requestId}`);
1703
+ if (!item) return;
1704
+ this.upsertItem(conversationId, {
1705
+ ...item,
1706
+ metadata: { ...(item.metadata ?? {}), ...metadata },
1707
+ updatedAt: Date.now(),
1708
+ });
1709
+ }
1710
+
1137
1711
  private addItem(conversationId: string, item: AgentTimelineItem): void {
1138
1712
  const timeline = this.timelines.get(conversationId) ?? [];
1139
1713
  timeline.push(item);
@@ -1173,11 +1747,20 @@ export class AgentWorkspaceProxy {
1173
1747
  };
1174
1748
  this.toolConversationIds.set(toolCall.id, conversationId);
1175
1749
  this.toolConversationIds.set(nextToolCall.id, conversationId);
1750
+ const kind: AgentTimelineKind = nextToolCall.name.includes("文件")
1751
+ ? "file_change"
1752
+ : nextToolCall.name.includes("命令")
1753
+ ? "command_execution"
1754
+ : "tool_activity";
1176
1755
  this.upsertItem(conversationId, {
1177
1756
  id: `tool:${nextToolCall.id}`,
1178
1757
  conversationId,
1179
1758
  type: "tool_call",
1759
+ kind,
1760
+ itemId: nextToolCall.id,
1180
1761
  toolCall: nextToolCall,
1762
+ commandExecution: kind === "command_execution" ? commandExecutionFromTool(nextToolCall) : undefined,
1763
+ fileChange: kind === "file_change" ? fileChangeFromTool(nextToolCall) : undefined,
1181
1764
  createdAt: nextToolCall.createdAt ?? Date.now(),
1182
1765
  updatedAt: Date.now(),
1183
1766
  });
@@ -1348,12 +1931,12 @@ export class AgentWorkspaceProxy {
1348
1931
  return undefined;
1349
1932
  }
1350
1933
 
1351
- private handleExit(message: string): void {
1934
+ private handleProviderExit(provider: AgentProvider, message: string): void {
1935
+ this.clients.delete(provider);
1936
+ this.agentProtocols.delete(provider);
1352
1937
  this.cancelPendingPermissions();
1353
- this.status = "error";
1354
- this.error = message;
1355
- this.client = undefined;
1356
1938
  for (const conversation of this.conversations.values()) {
1939
+ if (conversation.provider !== provider) continue;
1357
1940
  conversation.status = "error";
1358
1941
  conversation.lastMessagePreview = message;
1359
1942
  conversation.lastActivityAt = Date.now();
@@ -1366,6 +1949,7 @@ export class AgentWorkspaceProxy {
1366
1949
  createdAt: Date.now(),
1367
1950
  });
1368
1951
  }
1952
+ this.sendCapabilities();
1369
1953
  }
1370
1954
 
1371
1955
  private cancelPendingPermissions(conversationId?: string): void {
@@ -1380,6 +1964,19 @@ export class AgentWorkspaceProxy {
1380
1964
  this.permissionSources.delete(requestId);
1381
1965
  }
1382
1966
  this.permissionWaiters.clear();
1967
+ for (const [requestId, waiter] of this.structuredInputWaiters) {
1968
+ clearTimeout(waiter.timer);
1969
+ waiter.resolve(formatStructuredInputResponse({}));
1970
+ const pending = this.pendingStructuredInputs.get(requestId);
1971
+ if (pending) {
1972
+ this.markStructuredInput(pending.conversationId, requestId, {
1973
+ inputPending: false,
1974
+ inputError: "已停止",
1975
+ });
1976
+ }
1977
+ this.pendingStructuredInputs.delete(requestId);
1978
+ }
1979
+ this.structuredInputWaiters.clear();
1383
1980
  if (conversationId) this.updateConversationStatus(conversationId, "idle");
1384
1981
  }
1385
1982
 
@@ -1452,11 +2049,21 @@ function isPermissionRequestMethod(method: string): boolean {
1452
2049
  return (
1453
2050
  method === "session/request_permission" ||
1454
2051
  method.endsWith("/requestApproval") ||
1455
- method === "mcpServer/elicitation/request" ||
1456
- method === "item/tool/requestUserInput"
2052
+ method === "mcpServer/elicitation/request"
1457
2053
  );
1458
2054
  }
1459
2055
 
2056
+ function formatStructuredInputResponse(answers: Record<string, string[]>): unknown {
2057
+ return {
2058
+ answers: Object.fromEntries(
2059
+ Object.entries(answers).map(([questionId, values]) => [
2060
+ questionId,
2061
+ { answers: values.map((value) => value.trim()).filter(Boolean) },
2062
+ ]),
2063
+ ),
2064
+ };
2065
+ }
2066
+
1460
2067
  function formatPermissionResponse(
1461
2068
  source: string | undefined,
1462
2069
  outcome: "allow" | "deny" | "cancelled",