linkshell-cli 0.5.1 → 0.5.2

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 (46) hide show
  1. package/dist/cli/src/index.js +1 -1
  2. package/dist/cli/src/index.js.map +1 -1
  3. package/dist/cli/src/runtime/acp/acp-client.d.ts +16 -0
  4. package/dist/cli/src/runtime/acp/acp-client.js +40 -2
  5. package/dist/cli/src/runtime/acp/acp-client.js.map +1 -1
  6. package/dist/cli/src/runtime/acp/agent-workspace-git.d.ts +63 -0
  7. package/dist/cli/src/runtime/acp/agent-workspace-git.js +297 -0
  8. package/dist/cli/src/runtime/acp/agent-workspace-git.js.map +1 -0
  9. package/dist/cli/src/runtime/acp/agent-workspace.d.ts +6 -0
  10. package/dist/cli/src/runtime/acp/agent-workspace.js +690 -81
  11. package/dist/cli/src/runtime/acp/agent-workspace.js.map +1 -1
  12. package/dist/cli/src/runtime/acp/claude-sdk-client.d.ts +17 -1
  13. package/dist/cli/src/runtime/acp/claude-sdk-client.js +331 -19
  14. package/dist/cli/src/runtime/acp/claude-sdk-client.js.map +1 -1
  15. package/dist/cli/src/runtime/acp/claude-stream-json-client.js +69 -14
  16. package/dist/cli/src/runtime/acp/claude-stream-json-client.js.map +1 -1
  17. package/dist/cli/src/runtime/acp/json-rpc.d.ts +1 -0
  18. package/dist/cli/src/runtime/acp/json-rpc.js +6 -0
  19. package/dist/cli/src/runtime/acp/json-rpc.js.map +1 -1
  20. package/dist/cli/src/runtime/acp-relay.d.ts +23 -0
  21. package/dist/cli/src/runtime/acp-relay.js +73 -0
  22. package/dist/cli/src/runtime/acp-relay.js.map +1 -0
  23. package/dist/cli/src/runtime/bridge-session.d.ts +5 -0
  24. package/dist/cli/src/runtime/bridge-session.js +104 -57
  25. package/dist/cli/src/runtime/bridge-session.js.map +1 -1
  26. package/dist/cli/tsconfig.tsbuildinfo +1 -1
  27. package/dist/shared-protocol/src/index.d.ts +2505 -2479
  28. package/dist/shared-protocol/src/index.js +46 -0
  29. package/dist/shared-protocol/src/index.js.map +1 -1
  30. package/package.json +3 -3
  31. package/src/index.ts +1 -1
  32. package/src/runtime/acp/acp-client.ts +53 -2
  33. package/src/runtime/acp/agent-workspace.ts +752 -76
  34. package/src/runtime/acp/claude-sdk-client.ts +352 -31
  35. package/src/runtime/acp/claude-stream-json-client.ts +66 -16
  36. package/src/runtime/acp/json-rpc.ts +5 -0
  37. package/src/runtime/bridge-session.ts +114 -61
  38. package/dist/cli/src/runtime/acp/claude-sessions.d.ts +0 -57
  39. package/dist/cli/src/runtime/acp/claude-sessions.js +0 -506
  40. package/dist/cli/src/runtime/acp/claude-sessions.js.map +0 -1
  41. package/dist/cli/src/runtime/acp/codex-rpc-bridge.d.ts +0 -34
  42. package/dist/cli/src/runtime/acp/codex-rpc-bridge.js +0 -204
  43. package/dist/cli/src/runtime/acp/codex-rpc-bridge.js.map +0 -1
  44. package/dist/cli/src/runtime/acp/codex-sessions.d.ts +0 -60
  45. package/dist/cli/src/runtime/acp/codex-sessions.js +0 -731
  46. package/dist/cli/src/runtime/acp/codex-sessions.js.map +0 -1
@@ -201,6 +201,7 @@ interface PendingStructuredInputWaiter {
201
201
  resolve: (value: unknown) => void;
202
202
  timer: ReturnType<typeof setTimeout>;
203
203
  source?: string;
204
+ input: AgentStructuredInput;
204
205
  }
205
206
 
206
207
  const PERMISSION_TIMEOUT_MS = 5 * 60_000;
@@ -210,6 +211,17 @@ function id(prefix: string): string {
210
211
  return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
211
212
  }
212
213
 
214
+ function normalizeAgentSessionIdSegment(value: string): string {
215
+ return value.replace(/[^a-zA-Z0-9_-]+/g, "-");
216
+ }
217
+
218
+ export function makeAgentV2RemoteConversationId(
219
+ provider: AgentProvider,
220
+ agentSessionId: string,
221
+ ): string {
222
+ return `agent-remote-${provider}-${normalizeAgentSessionIdSegment(agentSessionId)}`;
223
+ }
224
+
213
225
  function stringify(value: unknown): string {
214
226
  if (typeof value === "string") return value;
215
227
  try {
@@ -264,6 +276,12 @@ function extractItem(value: unknown): Record<string, unknown> | undefined {
264
276
  return asRecord(raw.item) ?? raw;
265
277
  }
266
278
 
279
+ function extractTurn(value: unknown): Record<string, unknown> | undefined {
280
+ const raw = asRecord(value);
281
+ if (!raw) return undefined;
282
+ return asRecord(raw.turn) ?? raw;
283
+ }
284
+
267
285
  function stringifyDefined(value: unknown): string | undefined {
268
286
  if (value === undefined || value === null || value === "") return undefined;
269
287
  return stringify(value);
@@ -302,6 +320,29 @@ function normalizePlanStatus(value: unknown): AgentPlanStep["status"] {
302
320
  return "pending";
303
321
  }
304
322
 
323
+ function agentStatusFromThreadStatus(value: unknown): AgentStatus | undefined {
324
+ const raw = asRecord(value);
325
+ const status = typeof value === "string"
326
+ ? value
327
+ : firstString(raw, ["status", "state", "phase", "type"]) ??
328
+ firstString(asRecord(raw?.status), ["status", "state", "phase", "type"]);
329
+ const normalized = normalizedIdentifier(status);
330
+ if (!normalized) return undefined;
331
+ if (["active", "running", "inprogress", "busy", "working", "streaming"].includes(normalized)) {
332
+ return "running";
333
+ }
334
+ if (["idle", "ready", "completed", "complete", "done", "finished"].includes(normalized)) {
335
+ return "idle";
336
+ }
337
+ if (["waitingpermission", "waitingforpermission", "requirespermission", "needspermission"].includes(normalized)) {
338
+ return "waiting_permission";
339
+ }
340
+ if (["error", "failed", "systemerror", "fatal"].includes(normalized)) {
341
+ return "error";
342
+ }
343
+ return undefined;
344
+ }
345
+
305
346
  function nameFromToolMethod(method: string): string {
306
347
  if (method.includes("commandExecution")) return "命令";
307
348
  if (method.includes("fileChange")) return "文件修改";
@@ -670,7 +711,9 @@ function parseStructuredInputQuestion(value: unknown, index: number): AgentStruc
670
711
  question,
671
712
  isOther: raw.isOther === true,
672
713
  isSecret: raw.isSecret === true || raw.secret === true,
673
- selectionLimit: firstNumber(raw, ["selectionLimit", "maxSelections"]),
714
+ selectionLimit: raw.multiSelect === true
715
+ ? options.length || undefined
716
+ : firstNumber(raw, ["selectionLimit", "maxSelections"]),
674
717
  options: options.length > 0 ? options : undefined,
675
718
  };
676
719
  }
@@ -1237,6 +1280,7 @@ function parseRemoteSessions(value: unknown): Array<{
1237
1280
  const raw = asRecord(value);
1238
1281
  const sessionsValue =
1239
1282
  Array.isArray(value) ? value :
1283
+ Array.isArray(raw?.data) ? raw.data :
1240
1284
  Array.isArray(raw?.threads) ? raw.threads :
1241
1285
  Array.isArray(raw?.sessions) ? raw.sessions :
1242
1286
  Array.isArray(raw?.items) ? raw.items :
@@ -1262,7 +1306,7 @@ function parseRemoteSessions(value: unknown): Array<{
1262
1306
  result.push({
1263
1307
  id,
1264
1308
  cwd: firstString(source, ["cwd", "workingDirectory", "workspacePath"]),
1265
- title: firstString(source, ["title", "name", "summary"]),
1309
+ title: firstString(source, ["title", "name", "summary", "preview"]),
1266
1310
  model: firstString(source, ["model", "modelId"]),
1267
1311
  createdAt: parseTimestamp(source.createdAt ?? source.created_at),
1268
1312
  lastActivityAt: parseTimestamp(source.lastActivityAt ?? source.updatedAt ?? source.modifiedAt ?? source.lastModified ?? source.updated_at),
@@ -1271,10 +1315,279 @@ function parseRemoteSessions(value: unknown): Array<{
1271
1315
  return result;
1272
1316
  }
1273
1317
 
1318
+ function threadFromProviderResult(value: unknown): Record<string, unknown> | undefined {
1319
+ const raw = asRecord(value);
1320
+ return asRecord(raw?.thread) ?? raw;
1321
+ }
1322
+
1323
+ function threadFromTurnsListResult(sessionId: string, value: unknown): Record<string, unknown> | undefined {
1324
+ const raw = asRecord(value);
1325
+ const order = firstString(raw, ["sortDirection", "order", "sortOrder", "sort"]);
1326
+ const turns =
1327
+ Array.isArray(raw?.turns) ? raw.turns :
1328
+ Array.isArray(raw?.data) ? raw.data :
1329
+ Array.isArray(raw?.items) ? raw.items :
1330
+ Array.isArray(value) ? value :
1331
+ undefined;
1332
+ if (!turns) return undefined;
1333
+ const extracted = turns
1334
+ .map(extractTurn)
1335
+ .filter((turn): turn is Record<string, unknown> => Boolean(turn));
1336
+ const chronological = order && /asc/i.test(order)
1337
+ ? extracted
1338
+ : [...extracted].reverse();
1339
+ return { id: sessionId, turns: chronological };
1340
+ }
1341
+
1342
+ function turnItemsFromThread(thread: Record<string, unknown> | undefined): Record<string, unknown>[] {
1343
+ if (!thread) return [];
1344
+ const turnsValue = Array.isArray(thread.turns)
1345
+ ? thread.turns
1346
+ : Array.isArray(thread.items)
1347
+ ? [{ id: firstString(thread, ["turnId", "id"]), items: thread.items }]
1348
+ : [];
1349
+ const result: Record<string, unknown>[] = [];
1350
+ for (const turnEntry of turnsValue) {
1351
+ const turn = extractTurn(turnEntry);
1352
+ if (!turn) continue;
1353
+ const turnId = firstString(turn, ["id", "turnId"]);
1354
+ const createdAt = parseTimestamp(
1355
+ turn.createdAt ??
1356
+ turn.created_at ??
1357
+ turn.startedAt ??
1358
+ turn.started_at ??
1359
+ turn.completedAt ??
1360
+ turn.completed_at ??
1361
+ turn.updatedAt ??
1362
+ turn.updated_at,
1363
+ );
1364
+ const itemsRecord = asRecord(turn.items);
1365
+ const items = Array.isArray(turn.items)
1366
+ ? turn.items
1367
+ : Array.isArray(itemsRecord?.data)
1368
+ ? itemsRecord.data
1369
+ : Array.isArray(itemsRecord?.items)
1370
+ ? itemsRecord.items
1371
+ : Array.isArray(turn.item)
1372
+ ? turn.item
1373
+ : Array.isArray(turn.events)
1374
+ ? turn.events
1375
+ : [];
1376
+ for (const itemEntry of items) {
1377
+ const item = extractItem(itemEntry);
1378
+ if (!item) continue;
1379
+ result.push({
1380
+ ...item,
1381
+ ...(turnId && !firstString(item, ["turnId"]) ? { turnId } : {}),
1382
+ __turnCreatedAt: createdAt,
1383
+ });
1384
+ }
1385
+ }
1386
+ return result;
1387
+ }
1388
+
1389
+ function activeTurnIdFromThread(thread: Record<string, unknown> | undefined): string | undefined {
1390
+ if (!thread) return undefined;
1391
+ const turnsValue = Array.isArray(thread.turns)
1392
+ ? thread.turns
1393
+ : Array.isArray(thread.items)
1394
+ ? [{ id: firstString(thread, ["turnId", "id"]), items: thread.items, status: thread.status }]
1395
+ : [];
1396
+ let activeTurnId: string | undefined;
1397
+ for (const turnEntry of turnsValue) {
1398
+ const turn = extractTurn(turnEntry);
1399
+ if (!turn) continue;
1400
+ const turnId = firstString(turn, ["id", "turnId"]);
1401
+ if (!turnId) continue;
1402
+ const status =
1403
+ firstString(turn, ["status", "state", "phase"]) ??
1404
+ firstString(asRecord(turn.status), ["status", "state", "phase", "type"]);
1405
+ const normalized = normalizedIdentifier(status);
1406
+ if ([
1407
+ "active",
1408
+ "running",
1409
+ "inprogress",
1410
+ "busy",
1411
+ "working",
1412
+ "streaming",
1413
+ "waitingpermission",
1414
+ "waitingforpermission",
1415
+ "requirespermission",
1416
+ "needspermission",
1417
+ ].includes(normalized)) {
1418
+ activeTurnId = turnId;
1419
+ }
1420
+ }
1421
+ return activeTurnId;
1422
+ }
1423
+
1424
+ function timelineItemFromProviderItem(
1425
+ item: Record<string, unknown>,
1426
+ conversationId: string,
1427
+ index: number,
1428
+ ): AgentTimelineItem | undefined {
1429
+ const itemType = firstString(item, ["type"]);
1430
+ const normalized = normalizedIdentifier(itemType);
1431
+ const itemId = firstString(item, ["id", "itemId", "messageId", "toolCallId"]) ?? `history-${index + 1}`;
1432
+ const createdAt =
1433
+ parseTimestamp(item.createdAt ?? item.created_at ?? item.timestamp ?? item.__turnCreatedAt) ??
1434
+ Date.now() + index;
1435
+ const base = {
1436
+ id: itemId,
1437
+ conversationId,
1438
+ turnId: firstString(item, ["turnId"]),
1439
+ itemId,
1440
+ createdAt,
1441
+ updatedAt: parseTimestamp(item.updatedAt ?? item.updated_at) ?? createdAt,
1442
+ isStreaming: false,
1443
+ };
1444
+
1445
+ if (
1446
+ normalized === "usermessage" ||
1447
+ normalized === "userinput" ||
1448
+ (normalized === "message" && item.role === "user")
1449
+ ) {
1450
+ const content = contentBlocksFromItem(item);
1451
+ const text = textFromBlocks(content);
1452
+ if (!content.length && !text) return undefined;
1453
+ return {
1454
+ ...base,
1455
+ type: "message",
1456
+ kind: "chat",
1457
+ role: "user",
1458
+ content,
1459
+ text,
1460
+ };
1461
+ }
1462
+
1463
+ if (
1464
+ normalized === "agentmessage" ||
1465
+ normalized === "assistantmessage" ||
1466
+ (normalized === "message" && item.role !== "user")
1467
+ ) {
1468
+ const content = contentBlocksFromItem(item);
1469
+ const text = textFromBlocks(content);
1470
+ if (!content.length && !text) return undefined;
1471
+ return {
1472
+ ...base,
1473
+ type: "message",
1474
+ kind: "chat",
1475
+ role: item.role === "system" ? "system" : "assistant",
1476
+ content,
1477
+ text,
1478
+ };
1479
+ }
1480
+
1481
+ if (normalized === "reasoning" || normalized === "thinking") {
1482
+ const text = firstString(item, ["text", "content", "summary", "message"]) ??
1483
+ stringifyDefined(item.contentItems ?? item.summary);
1484
+ if (!text) return undefined;
1485
+ return {
1486
+ ...base,
1487
+ type: "status",
1488
+ kind: "thinking",
1489
+ role: "system",
1490
+ text,
1491
+ };
1492
+ }
1493
+
1494
+ if (normalized === "enteredreviewmode" || normalized === "exitedreviewmode") {
1495
+ const text = firstString(item, ["review", "text", "content", "summary", "message"]);
1496
+ return {
1497
+ ...base,
1498
+ type: "status",
1499
+ kind: "review",
1500
+ role: "system",
1501
+ text: text ?? (normalized === "enteredreviewmode" ? "正在审查" : "审查已完成"),
1502
+ };
1503
+ }
1504
+
1505
+ if (normalized === "contextcompaction") {
1506
+ return {
1507
+ ...base,
1508
+ type: "status",
1509
+ kind: "context_compaction",
1510
+ role: "system",
1511
+ text: firstString(item, ["text", "summary", "message"]) ?? "上下文已压缩",
1512
+ };
1513
+ }
1514
+
1515
+ if (isSubagentItemType(itemType)) {
1516
+ const subagent = decodeSubagentAction(item, normalizeToolStatus(item.status, true));
1517
+ if (!subagent) return undefined;
1518
+ return {
1519
+ ...base,
1520
+ type: "status",
1521
+ kind: "subagent_action",
1522
+ role: "system",
1523
+ text: summarizeSubagentAction(subagent),
1524
+ subagent,
1525
+ };
1526
+ }
1527
+
1528
+ if (isToolItemType(itemType) || toolNameFromItem(item)) {
1529
+ const toolId = firstString(item, ["id", "itemId", "toolCallId"]) ?? itemId;
1530
+ const toolCall: AgentToolCall = {
1531
+ id: toolId,
1532
+ name: toolNameFromItem(item) ?? "工具",
1533
+ input: toolInputFromItem(item),
1534
+ output:
1535
+ normalized === "filechange" || normalized === "diff"
1536
+ ? extractDiffText(item) ?? firstString(item, ["output", "summary"])
1537
+ : firstString(item, ["aggregatedOutput", "output", "stdout", "stderr"]) ??
1538
+ stringifyDefined(item.result ?? item.error ?? item.contentItems),
1539
+ createdAt,
1540
+ status: normalizeToolStatus(item.status, true),
1541
+ };
1542
+ const kind: AgentTimelineKind = toolCall.name.includes("文件") || normalized === "filechange" || normalized === "diff"
1543
+ ? "file_change"
1544
+ : toolCall.name.includes("命令") || normalized === "commandexecution"
1545
+ ? "command_execution"
1546
+ : "tool_activity";
1547
+ return {
1548
+ ...base,
1549
+ id: `tool:${toolId}`,
1550
+ type: "tool_call",
1551
+ kind,
1552
+ itemId: toolId,
1553
+ toolCall,
1554
+ commandExecution: kind === "command_execution" ? commandExecutionFromTool(toolCall) : undefined,
1555
+ fileChange: kind === "file_change" ? fileChangeFromTool(toolCall) : undefined,
1556
+ };
1557
+ }
1558
+
1559
+ return undefined;
1560
+ }
1561
+
1562
+ function timelineItemsFromProviderThread(
1563
+ value: unknown,
1564
+ conversationId: string,
1565
+ ): AgentTimelineItem[] {
1566
+ const items = turnItemsFromThread(threadFromProviderResult(value))
1567
+ .map((item, index) => timelineItemFromProviderItem(item, conversationId, index))
1568
+ .filter((item): item is AgentTimelineItem => Boolean(item));
1569
+ const byId = new Map<string, AgentTimelineItem>();
1570
+ for (const item of items) {
1571
+ byId.set(item.id, item);
1572
+ }
1573
+ return [...byId.values()].sort((a, b) => a.createdAt - b.createdAt).slice(-MAX_TIMELINE_ITEMS);
1574
+ }
1575
+
1576
+ function previewFromTimelineItem(item: AgentTimelineItem): string | undefined {
1577
+ if (item.error) return item.error;
1578
+ if (item.text) return item.text;
1579
+ if (item.content?.length) return textFromBlocks(item.content);
1580
+ if (item.toolCall) return `${item.toolCall.name} · ${item.toolCall.status}`;
1581
+ if (item.permission) return `需要授权 ${item.permission.toolName ?? ""}`.trim();
1582
+ if (item.subagent) return summarizeSubagentAction(item.subagent);
1583
+ return undefined;
1584
+ }
1585
+
1274
1586
  export class AgentWorkspaceProxy {
1275
1587
  private clients = new Map<AgentProvider, AcpClient | ClaudeSdkClient | ClaudeStreamJsonClient>();
1276
1588
  private agentProtocols = new Map<AgentProvider, AgentProtocol>();
1277
1589
  private providerCapabilities = new Map<AgentProvider, ProviderRuntimeCapabilities>();
1590
+ private providerErrors = new Map<AgentProvider, string>();
1278
1591
  private initialized = false;
1279
1592
  private status: AgentStatus = "unavailable";
1280
1593
  private error: string | undefined;
@@ -1347,11 +1660,21 @@ export class AgentWorkspaceProxy {
1347
1660
  case "agent.v2.cancel": {
1348
1661
  const payload = parseTypedPayload("agent.v2.cancel", envelope.payload);
1349
1662
  const conversation = this.conversations.get(payload.conversationId);
1663
+ if (!conversation) break;
1664
+ const turnId = this.currentTurnIds.get(payload.conversationId);
1665
+ if (this.protocolForProvider(conversation.provider) === "codex-app-server" && !turnId) {
1666
+ this.rejectAgentAction(
1667
+ conversation,
1668
+ "无法停止 Codex:当前运行 turn 尚未同步,请重新打开对话后再试。",
1669
+ conversation.status,
1670
+ );
1671
+ break;
1672
+ }
1350
1673
  this.cancelPendingPermissions(payload.conversationId);
1351
- const cancelClient = conversation ? this.clientForProvider(conversation.provider) : undefined;
1674
+ const cancelClient = this.clientForProvider(conversation.provider);
1352
1675
  cancelClient?.cancel({
1353
- sessionId: conversation?.agentSessionId,
1354
- turnId: this.currentTurnIds.get(payload.conversationId),
1676
+ sessionId: conversation.agentSessionId,
1677
+ turnId,
1355
1678
  });
1356
1679
  this.forgetCurrentTurn(payload.conversationId);
1357
1680
  this.updateConversationStatus(payload.conversationId, "idle");
@@ -1386,6 +1709,13 @@ export class AgentWorkspaceProxy {
1386
1709
  return this.agentProtocols.get(provider);
1387
1710
  }
1388
1711
 
1712
+ private defaultModelForProvider(provider: AgentProvider): string | undefined {
1713
+ const capabilities = this.providerCapabilities.get(provider);
1714
+ const defaultModel = capabilities?.defaultModel?.trim();
1715
+ if (defaultModel) return defaultModel;
1716
+ return capabilities?.models?.find((model) => model.id && model.id !== "default")?.id;
1717
+ }
1718
+
1389
1719
  private async initialize(): Promise<void> {
1390
1720
  if (this.initialized) return;
1391
1721
  this.initialized = true;
@@ -1452,6 +1782,7 @@ export class AgentWorkspaceProxy {
1452
1782
  try {
1453
1783
  const client = await tryCreateClient(resolved);
1454
1784
  this.clients.set(provider, client);
1785
+ this.providerErrors.delete(provider);
1455
1786
  await this.refreshProviderCapabilities(provider, client, resolved.protocol);
1456
1787
  this.status = "idle";
1457
1788
  this.error = undefined;
@@ -1471,6 +1802,7 @@ export class AgentWorkspaceProxy {
1471
1802
  };
1472
1803
  const client = await tryCreateClient(fallback);
1473
1804
  this.clients.set(provider, client);
1805
+ this.providerErrors.delete(provider);
1474
1806
  await this.refreshProviderCapabilities(provider, client, fallback.protocol);
1475
1807
  this.status = "idle";
1476
1808
  this.error = undefined;
@@ -1485,6 +1817,8 @@ export class AgentWorkspaceProxy {
1485
1817
  if (this.input.verbose) {
1486
1818
  process.stderr.write(`[agent:v2] failed to start ${provider}: ${error instanceof Error ? error.message : String(error)}\n`);
1487
1819
  }
1820
+ this.providerErrors.set(provider, error instanceof Error ? error.message : String(error));
1821
+ this.sendCapabilities();
1488
1822
  return undefined;
1489
1823
  }
1490
1824
  }
@@ -1537,7 +1871,7 @@ export class AgentWorkspaceProxy {
1537
1871
  const agentSessionId = remote.id;
1538
1872
  const existingId = this.conversationByAgentSessionId.get(agentSessionId);
1539
1873
  const now = Date.now();
1540
- const conversationId = existingId ?? `agent:${agentSessionId}`;
1874
+ const conversationId = existingId ?? makeAgentV2RemoteConversationId(provider, agentSessionId);
1541
1875
  const existing = this.conversations.get(conversationId);
1542
1876
  const cwd = remote.cwd ?? existing?.cwd ?? this.input.cwd;
1543
1877
  const conversation: AgentConversation = {
@@ -1568,6 +1902,83 @@ export class AgentWorkspaceProxy {
1568
1902
  }
1569
1903
  }
1570
1904
 
1905
+ private async hydrateConversationFromProvider(
1906
+ conversation: AgentConversation,
1907
+ seedResult?: unknown,
1908
+ ): Promise<void> {
1909
+ const client = this.clientForProvider(conversation.provider);
1910
+ if (!client || !conversation.agentSessionId) return;
1911
+ let source = seedResult;
1912
+ const hasSeedTurns = turnItemsFromThread(threadFromProviderResult(seedResult)).length > 0;
1913
+ if (!hasSeedTurns) {
1914
+ const readSession = (client as { readSession?: (input: { sessionId: string; includeTurns?: boolean }) => Promise<unknown> }).readSession;
1915
+ if (typeof readSession === "function") {
1916
+ try {
1917
+ source = await readSession.call(client, {
1918
+ sessionId: conversation.agentSessionId,
1919
+ includeTurns: true,
1920
+ });
1921
+ } catch (error) {
1922
+ if (this.input.verbose) {
1923
+ process.stderr.write(`[agent:v2] thread/read hydration failed for ${conversation.provider}: ${error instanceof Error ? error.message : String(error)}\n`);
1924
+ }
1925
+ }
1926
+ }
1927
+ if (turnItemsFromThread(threadFromProviderResult(source)).length === 0) {
1928
+ const listTurns = (client as { listTurns?: (input: {
1929
+ sessionId: string;
1930
+ limit?: number;
1931
+ cursor?: string;
1932
+ sortDirection?: "asc" | "desc";
1933
+ itemsView?: "summary" | "full";
1934
+ }) => Promise<unknown> }).listTurns;
1935
+ if (typeof listTurns === "function") {
1936
+ try {
1937
+ const turnsResult = await listTurns.call(client, {
1938
+ sessionId: conversation.agentSessionId,
1939
+ limit: MAX_TIMELINE_ITEMS,
1940
+ sortDirection: "desc",
1941
+ itemsView: "full",
1942
+ });
1943
+ const turnsThread = threadFromTurnsListResult(conversation.agentSessionId, turnsResult);
1944
+ if (turnsThread) source = { thread: turnsThread };
1945
+ } catch (error) {
1946
+ if (this.input.verbose) {
1947
+ process.stderr.write(`[agent:v2] thread/turns/list hydration failed for ${conversation.provider}: ${error instanceof Error ? error.message : String(error)}\n`);
1948
+ }
1949
+ }
1950
+ }
1951
+ }
1952
+ }
1953
+
1954
+ const thread = threadFromProviderResult(source);
1955
+ const activeTurnId = activeTurnIdFromThread(thread);
1956
+ if (activeTurnId) this.rememberTurnConversationId(conversation.id, activeTurnId);
1957
+ const model = firstString(thread, ["model", "modelId", "currentModel"]);
1958
+ if (model && !conversation.model) conversation.model = model;
1959
+
1960
+ const hydratedItems = timelineItemsFromProviderThread(source, conversation.id);
1961
+ if (hydratedItems.length === 0) return;
1962
+ const existing = this.timelines.get(conversation.id) ?? [];
1963
+ const merged = new Map<string, AgentTimelineItem>();
1964
+ for (const item of [...existing, ...hydratedItems]) {
1965
+ merged.set(item.id, item);
1966
+ }
1967
+ const nextItems = [...merged.values()]
1968
+ .sort((a, b) => a.createdAt - b.createdAt)
1969
+ .slice(-MAX_TIMELINE_ITEMS);
1970
+ this.timelines.set(conversation.id, nextItems);
1971
+ for (const item of nextItems) {
1972
+ this.rememberItemConversationId(conversation.id, item);
1973
+ }
1974
+ const lastPreview = [...nextItems].reverse()
1975
+ .map((item) => previewText(previewFromTimelineItem(item) ?? ""))
1976
+ .find(Boolean);
1977
+ if (lastPreview && !conversation.lastMessagePreview) {
1978
+ conversation.lastMessagePreview = lastPreview;
1979
+ }
1980
+ }
1981
+
1571
1982
  private sendCapabilities(): void {
1572
1983
  const providers = this.input.availableProviders.map((provider) => {
1573
1984
  const client = this.clients.get(provider);
@@ -1587,7 +1998,7 @@ export class AgentWorkspaceProxy {
1587
1998
  id: provider,
1588
1999
  label: providerLabel(provider),
1589
2000
  enabled,
1590
- reason: enabled ? undefined : `${providerLabel(provider)} 未安装或启动失败`,
2001
+ reason: enabled ? undefined : this.providerErrors.get(provider) ?? `${providerLabel(provider)} 未安装或启动失败`,
1591
2002
  supportsImages,
1592
2003
  supportsPermission,
1593
2004
  supportsPlan: enabled,
@@ -1659,24 +2070,63 @@ export class AgentWorkspaceProxy {
1659
2070
  );
1660
2071
  }
1661
2072
 
1662
- const client = await this.ensureProviderClient(provider);
1663
- if (!client) {
1664
- return this.openFailure(
1665
- payload,
1666
- `${providerLabel(provider)} 启动失败。请确认 CLI 已安装并可用。`,
1667
- );
1668
- }
1669
-
1670
- const cwd = payload.cwd ?? this.input.cwd;
2073
+ let cwd = payload.cwd ?? this.input.cwd;
1671
2074
  let agentSessionId = payload.agentSessionId;
1672
2075
  let existingConversation =
1673
2076
  (payload.conversationId ? this.conversations.get(payload.conversationId) : undefined) ??
1674
2077
  (agentSessionId ? this.conversations.get(this.conversationByAgentSessionId.get(agentSessionId) ?? "") : undefined);
2078
+ if (!payload.cwd && existingConversation?.cwd) cwd = existingConversation.cwd;
2079
+
2080
+ const client = await this.ensureProviderClient(provider);
2081
+ if (!client) {
2082
+ const message = this.providerErrors.get(provider) ?? `${providerLabel(provider)} 启动失败。请确认 CLI 已安装并可用。`;
2083
+ if (existingConversation) {
2084
+ this.activeConversationId = existingConversation.id;
2085
+ this.input.send(createEnvelope({
2086
+ type: "agent.v2.conversation.opened",
2087
+ sessionId: this.input.sessionId,
2088
+ payload: {
2089
+ conversation: existingConversation,
2090
+ snapshot: this.timelines.get(existingConversation.id) ?? [],
2091
+ requestedConversationId: payload.conversationId,
2092
+ providerError: message,
2093
+ },
2094
+ }));
2095
+ this.sendCapabilities();
2096
+ return existingConversation;
2097
+ }
2098
+ if (payload.conversationId) {
2099
+ return this.openOfflineHistory(payload, message, cwd);
2100
+ }
2101
+ return this.openFailure(payload, message, cwd);
2102
+ }
1675
2103
 
1676
2104
  if (existingConversation && existingConversation.status !== "error" && existingConversation.agentSessionId) {
1677
- if (payload.conversationId && existingConversation.id !== payload.conversationId) {
2105
+ const requestedCanonicalId = makeAgentV2RemoteConversationId(provider, existingConversation.agentSessionId);
2106
+ if (
2107
+ payload.conversationId &&
2108
+ payload.conversationId === requestedCanonicalId &&
2109
+ existingConversation.id !== payload.conversationId
2110
+ ) {
1678
2111
  existingConversation = this.adoptConversationId(existingConversation.id, payload.conversationId);
1679
2112
  }
2113
+ if ((this.timelines.get(existingConversation.id) ?? []).length === 0 && existingConversation.status !== "running") {
2114
+ try {
2115
+ const existingAgentSessionId = existingConversation.agentSessionId!;
2116
+ const result = await client.loadSession({ sessionId: existingAgentSessionId, cwd });
2117
+ const nextAgentSessionId = this.extractSessionId(result);
2118
+ if (nextAgentSessionId && nextAgentSessionId !== existingAgentSessionId) {
2119
+ this.conversationByAgentSessionId.delete(existingAgentSessionId);
2120
+ existingConversation.agentSessionId = nextAgentSessionId;
2121
+ this.conversationByAgentSessionId.set(nextAgentSessionId, existingConversation.id);
2122
+ }
2123
+ await this.hydrateConversationFromProvider(existingConversation, result);
2124
+ } catch (error) {
2125
+ if (this.input.verbose) {
2126
+ process.stderr.write(`[agent:v2] resume failed for ${provider}: ${error instanceof Error ? error.message : String(error)}\n`);
2127
+ }
2128
+ }
2129
+ }
1680
2130
  this.activeConversationId = existingConversation.id;
1681
2131
  this.input.send(createEnvelope({
1682
2132
  type: "agent.v2.conversation.opened",
@@ -1684,6 +2134,7 @@ export class AgentWorkspaceProxy {
1684
2134
  payload: {
1685
2135
  conversation: existingConversation,
1686
2136
  snapshot: this.timelines.get(existingConversation.id) ?? [],
2137
+ requestedConversationId: payload.conversationId,
1687
2138
  },
1688
2139
  }));
1689
2140
  return existingConversation;
@@ -1695,7 +2146,7 @@ export class AgentWorkspaceProxy {
1695
2146
  : await client.newSession({ cwd });
1696
2147
  agentSessionId = this.extractSessionId(result) ?? agentSessionId ?? id("agent-session");
1697
2148
  const now = Date.now();
1698
- const conversationId = payload.conversationId ?? `agent:${agentSessionId}`;
2149
+ const conversationId = makeAgentV2RemoteConversationId(provider, agentSessionId);
1699
2150
  const conversation: AgentConversation = {
1700
2151
  ...existingConversation,
1701
2152
  id: conversationId,
@@ -1717,10 +2168,15 @@ export class AgentWorkspaceProxy {
1717
2168
  this.conversationByAgentSessionId.set(agentSessionId, conversation.id);
1718
2169
  this.activeConversationId = conversation.id;
1719
2170
  this.timelines.set(conversation.id, this.timelines.get(conversation.id) ?? []);
2171
+ await this.hydrateConversationFromProvider(conversation, result);
1720
2172
  this.input.send(createEnvelope({
1721
2173
  type: "agent.v2.conversation.opened",
1722
2174
  sessionId: this.input.sessionId,
1723
- payload: { conversation, snapshot: this.timelines.get(conversation.id) ?? [] },
2175
+ payload: {
2176
+ conversation,
2177
+ snapshot: this.timelines.get(conversation.id) ?? [],
2178
+ requestedConversationId: payload.conversationId,
2179
+ },
1724
2180
  }));
1725
2181
  return conversation;
1726
2182
  } catch (error) {
@@ -1772,8 +2228,62 @@ export class AgentWorkspaceProxy {
1772
2228
  this.input.send(createEnvelope({
1773
2229
  type: "agent.v2.conversation.opened",
1774
2230
  sessionId: this.input.sessionId,
1775
- payload: { conversation, snapshot: this.timelines.get(conversation.id) ?? [] },
2231
+ payload: {
2232
+ conversation,
2233
+ snapshot: this.timelines.get(conversation.id) ?? [],
2234
+ requestedConversationId: payload.conversationId,
2235
+ },
2236
+ }));
2237
+ return conversation;
2238
+ }
2239
+
2240
+ private openOfflineHistory(
2241
+ payload: {
2242
+ conversationId?: string;
2243
+ agentSessionId?: string;
2244
+ cwd?: string;
2245
+ provider?: AgentProvider;
2246
+ model?: string;
2247
+ reasoningEffort?: string;
2248
+ permissionMode?: AgentPermissionMode;
2249
+ collaborationMode?: AgentCollaborationMode;
2250
+ title?: string;
2251
+ },
2252
+ message: string,
2253
+ cwd = payload.cwd ?? this.input.cwd,
2254
+ ): AgentConversation {
2255
+ const now = Date.now();
2256
+ const conversation: AgentConversation = {
2257
+ id: payload.conversationId!,
2258
+ agentSessionId: payload.agentSessionId,
2259
+ provider: payload.provider ?? this.input.availableProviders[0] ?? "codex",
2260
+ cwd,
2261
+ title: payload.title ?? titleFromCwd(cwd),
2262
+ model: payload.model,
2263
+ reasoningEffort: payload.reasoningEffort,
2264
+ permissionMode: payload.permissionMode,
2265
+ collaborationMode: payload.collaborationMode,
2266
+ status: "idle",
2267
+ archived: false,
2268
+ lastActivityAt: now,
2269
+ createdAt: now,
2270
+ };
2271
+ this.conversations.set(conversation.id, conversation);
2272
+ if (conversation.agentSessionId) {
2273
+ this.conversationByAgentSessionId.set(conversation.agentSessionId, conversation.id);
2274
+ }
2275
+ this.activeConversationId = conversation.id;
2276
+ this.input.send(createEnvelope({
2277
+ type: "agent.v2.conversation.opened",
2278
+ sessionId: this.input.sessionId,
2279
+ payload: {
2280
+ conversation,
2281
+ snapshot: this.timelines.get(conversation.id) ?? [],
2282
+ requestedConversationId: payload.conversationId,
2283
+ providerError: message,
2284
+ },
1776
2285
  }));
2286
+ this.sendCapabilities();
1777
2287
  return conversation;
1778
2288
  }
1779
2289
 
@@ -1781,6 +2291,8 @@ export class AgentWorkspaceProxy {
1781
2291
  conversationId: string;
1782
2292
  clientMessageId: string;
1783
2293
  contentBlocks: AgentContentBlock[];
2294
+ delivery?: "auto" | "new_turn" | "steer";
2295
+ targetTurnId?: string;
1784
2296
  model?: string;
1785
2297
  reasoningEffort?: string;
1786
2298
  permissionMode?: AgentPermissionMode;
@@ -1791,39 +2303,28 @@ export class AgentWorkspaceProxy {
1791
2303
  await this.openConversation({ conversationId: payload.conversationId });
1792
2304
  if (!conversation) return;
1793
2305
  if (!conversation.agentSessionId) {
1794
- this.addItem(payload.conversationId, {
1795
- id: id("error"),
1796
- conversationId: payload.conversationId,
1797
- type: "error",
1798
- error: "Agent session 尚未就绪,消息没有发送。请重新打开对话后再试。",
1799
- createdAt: Date.now(),
1800
- });
2306
+ this.rejectAgentAction(
2307
+ conversation,
2308
+ "Agent session 尚未就绪,消息没有发送。请重新打开对话后再试。",
2309
+ );
1801
2310
  return;
1802
2311
  }
1803
- const client = this.clientForProvider(conversation.provider);
2312
+ const client = this.clientForProvider(conversation.provider) ?? await this.ensureProviderClient(conversation.provider);
1804
2313
  if (!client) {
1805
- this.addItem(conversation.id, {
1806
- id: id("error"),
1807
- conversationId: conversation.id,
1808
- type: "error",
1809
- error: `${providerLabel(conversation.provider)} 未连接,消息没有发送。`,
1810
- createdAt: Date.now(),
1811
- });
2314
+ this.rejectAgentAction(
2315
+ conversation,
2316
+ this.providerErrors.get(conversation.provider) ?? `${providerLabel(conversation.provider)} 未连接,消息没有发送。`,
2317
+ "error",
2318
+ );
1812
2319
  return;
1813
2320
  }
1814
2321
 
1815
2322
  const protocol = this.protocolForProvider(conversation.provider);
1816
2323
  if (payload.contentBlocks.some((block) => block.type === "image") && !protocolSupportsImages(protocol)) {
1817
- conversation.status = "idle";
1818
- conversation.lastActivityAt = Date.now();
1819
- this.emitConversation(conversation);
1820
- this.addItem(conversation.id, {
1821
- id: id("error"),
1822
- conversationId: conversation.id,
1823
- type: "error",
1824
- error: "当前 Agent provider 暂不支持图片输入,请升级 CLI 或切换到 Codex。",
1825
- createdAt: Date.now(),
1826
- });
2324
+ this.rejectAgentAction(
2325
+ conversation,
2326
+ "当前 Agent provider 暂不支持图片输入,请升级 CLI 或切换到 Codex。",
2327
+ );
1827
2328
  return;
1828
2329
  }
1829
2330
 
@@ -1832,7 +2333,35 @@ export class AgentWorkspaceProxy {
1832
2333
  reasoningEffort: conversation.reasoningEffort,
1833
2334
  permissionMode: conversation.permissionMode,
1834
2335
  };
1835
- conversation.model = payload.model ?? conversation.model;
2336
+ const effectiveModel = payload.model ??
2337
+ conversation.model ??
2338
+ (protocol === "codex-app-server" ? this.defaultModelForProvider(conversation.provider) : undefined);
2339
+ const wasRunning = conversation.status === "running";
2340
+ const activeTurnId = payload.targetTurnId ?? this.currentTurnIds.get(conversation.id);
2341
+ const shouldSteer = Boolean(
2342
+ payload.delivery !== "new_turn" &&
2343
+ wasRunning &&
2344
+ protocol === "codex-app-server" &&
2345
+ activeTurnId,
2346
+ );
2347
+ if (wasRunning && payload.delivery !== "new_turn" && protocol !== "codex-app-server") {
2348
+ this.addItem(conversation.id, {
2349
+ id: id("error"),
2350
+ conversationId: conversation.id,
2351
+ type: "error",
2352
+ error: `${providerLabel(conversation.provider)} 当前不支持运行中追加输入。请等待本轮结束,或先停止后再发送。`,
2353
+ createdAt: Date.now(),
2354
+ });
2355
+ return;
2356
+ }
2357
+ if (payload.delivery === "steer" && protocol === "codex-app-server" && !activeTurnId) {
2358
+ this.rejectAgentAction(
2359
+ conversation,
2360
+ "当前没有可追加输入的 Codex turn。请等待本轮开始后重试,或作为新消息发送。",
2361
+ );
2362
+ return;
2363
+ }
2364
+ conversation.model = effectiveModel ?? conversation.model;
1836
2365
  conversation.reasoningEffort = payload.reasoningEffort ?? conversation.reasoningEffort;
1837
2366
  conversation.permissionMode = payload.permissionMode ?? conversation.permissionMode;
1838
2367
  conversation.collaborationMode = payload.collaborationMode ?? conversation.collaborationMode;
@@ -1840,14 +2369,14 @@ export class AgentWorkspaceProxy {
1840
2369
  conversation.lastActivityAt = Date.now();
1841
2370
  this.activeConversationId = conversation.id;
1842
2371
 
1843
- if (payload.model && payload.model !== priorSettings.model) {
2372
+ if (effectiveModel && effectiveModel !== priorSettings.model) {
1844
2373
  const runtimeCapabilities = this.providerCapabilities.get(conversation.provider);
1845
- const label = runtimeCapabilities?.models?.find((m) => m.id === payload.model)?.label ?? payload.model;
2374
+ const label = runtimeCapabilities?.models?.find((m) => m.id === effectiveModel)?.label ?? effectiveModel;
1846
2375
  this.emitNotice({
1847
2376
  conversationId: conversation.id,
1848
2377
  kind: "model_changed",
1849
2378
  title: `已切换模型 · ${label}`,
1850
- detail: `${providerLabel(conversation.provider)} 下次回复将使用 ${payload.model}`,
2379
+ detail: `${providerLabel(conversation.provider)} 下次回复将使用 ${effectiveModel}`,
1851
2380
  });
1852
2381
  }
1853
2382
  if (payload.reasoningEffort && payload.reasoningEffort !== priorSettings.reasoningEffort) {
@@ -1873,21 +2402,62 @@ export class AgentWorkspaceProxy {
1873
2402
  role: "user",
1874
2403
  content: payload.contentBlocks,
1875
2404
  text: userText,
2405
+ metadata: shouldSteer ? { delivery: "steer", targetTurnId: activeTurnId } : undefined,
1876
2406
  createdAt: Date.now(),
1877
2407
  });
1878
2408
  this.emitConversation(conversation);
1879
2409
 
1880
- try {
1881
- const result = await client.prompt({
1882
- sessionId: conversation.agentSessionId,
2410
+ const promptAsNewTurn = () =>
2411
+ client.prompt({
2412
+ sessionId: conversation.agentSessionId!,
1883
2413
  content: payload.contentBlocks,
1884
2414
  clientMessageId: payload.clientMessageId,
1885
- model: payload.model,
2415
+ model: effectiveModel,
1886
2416
  reasoningEffort: payload.reasoningEffort,
1887
2417
  permissionMode: payload.permissionMode,
1888
2418
  collaborationMode: payload.collaborationMode ?? conversation.collaborationMode,
1889
2419
  cwd: conversation.cwd,
1890
2420
  });
2421
+
2422
+ try {
2423
+ const steer = (client as { steer?: (input: { sessionId: string; turnId: string; content: AgentContentBlock[] }) => Promise<unknown> }).steer;
2424
+ let result: unknown;
2425
+ if (shouldSteer && typeof steer === "function") {
2426
+ try {
2427
+ result = await steer.call(client, {
2428
+ sessionId: conversation.agentSessionId,
2429
+ turnId: activeTurnId!,
2430
+ content: payload.contentBlocks,
2431
+ });
2432
+ } catch (steerError) {
2433
+ this.forgetCurrentTurn(conversation.id, activeTurnId);
2434
+ const userItem = this.findItem(conversation.id, payload.clientMessageId);
2435
+ if (userItem) {
2436
+ this.upsertItem(conversation.id, {
2437
+ ...userItem,
2438
+ metadata: {
2439
+ ...(userItem.metadata ?? {}),
2440
+ delivery: "new_turn",
2441
+ fallbackFrom: "steer",
2442
+ failedTargetTurnId: activeTurnId,
2443
+ },
2444
+ updatedAt: Date.now(),
2445
+ });
2446
+ }
2447
+ this.addItem(conversation.id, {
2448
+ id: id("status"),
2449
+ conversationId: conversation.id,
2450
+ type: "status",
2451
+ role: "system",
2452
+ text: "Codex 未接收运行中追加输入,已改为发送新消息。",
2453
+ status: "running",
2454
+ createdAt: Date.now(),
2455
+ });
2456
+ result = await promptAsNewTurn();
2457
+ }
2458
+ } else {
2459
+ result = await promptAsNewTurn();
2460
+ }
1891
2461
  const nextAgentSessionId = this.extractSessionId(result);
1892
2462
  if (nextAgentSessionId && nextAgentSessionId !== conversation.agentSessionId) {
1893
2463
  if (conversation.agentSessionId) this.conversationByAgentSessionId.delete(conversation.agentSessionId);
@@ -2120,6 +2690,9 @@ export class AgentWorkspaceProxy {
2120
2690
  }
2121
2691
 
2122
2692
  private handleRequest(method: string, params: unknown): Promise<unknown> | unknown {
2693
+ if (method === "claude/askUserQuestion") {
2694
+ return this.handleStructuredInput({ ...(asRecord(params) ?? {}), source: method }, true);
2695
+ }
2123
2696
  if (method === "item/tool/requestUserInput" || method === "tool/requestUserInput") {
2124
2697
  return this.handleStructuredInput(params, true);
2125
2698
  }
@@ -2167,7 +2740,6 @@ export class AgentWorkspaceProxy {
2167
2740
  if (
2168
2741
  method.startsWith("account/") ||
2169
2742
  method.startsWith("mcpServer/startupStatus/") ||
2170
- method === "thread/status/changed" ||
2171
2743
  method === "thread/tokenUsage/updated" ||
2172
2744
  method === "serverRequest/resolved" ||
2173
2745
  method === "mcpServer/oauthLogin/completed"
@@ -2176,6 +2748,25 @@ export class AgentWorkspaceProxy {
2176
2748
  }
2177
2749
 
2178
2750
  const conversationId = this.conversationIdFromParams(params) ?? this.fallbackConversationId();
2751
+ if (method === "thread/status/changed") {
2752
+ const status = agentStatusFromThreadStatus(params);
2753
+ if (conversationId && status) {
2754
+ const raw = asRecord(params);
2755
+ const turnId = this.extractTurnId(raw);
2756
+ if (turnId && (status === "running" || status === "waiting_permission")) {
2757
+ this.rememberTurnConversationId(conversationId, turnId);
2758
+ }
2759
+ if (status === "idle" || status === "error") {
2760
+ this.forgetCurrentTurn(conversationId, turnId);
2761
+ }
2762
+ const message =
2763
+ status === "error"
2764
+ ? firstString(raw, ["message", "error", "reason"])
2765
+ : undefined;
2766
+ this.updateConversationStatus(conversationId, status, message);
2767
+ }
2768
+ return;
2769
+ }
2179
2770
  if (method === "item/tool/requestUserInput" || method === "tool/requestUserInput") {
2180
2771
  this.handleStructuredInput(params);
2181
2772
  return;
@@ -2685,9 +3276,8 @@ export class AgentWorkspaceProxy {
2685
3276
  const raw = asRecord(params) ?? {};
2686
3277
  const conversationId = this.conversationIdFromParams(raw) ?? this.fallbackConversationId();
2687
3278
  const source = firstString(raw, ["method", "source", "requestMethod"]);
2688
- const formatResponse = source === "mcpServer/elicitation/request"
2689
- ? formatMcpElicitationResponse
2690
- : formatStructuredInputResponse;
3279
+ const formatResponse = (answers: Record<string, string[]>, input?: AgentStructuredInput) =>
3280
+ formatStructuredInputResponseForSource(source, answers, input);
2691
3281
  if (!conversationId) return waitForResponse ? Promise.resolve(formatResponse({})) : undefined;
2692
3282
  const structuredInput = decodeStructuredInput(raw);
2693
3283
  if (!structuredInput) return waitForResponse ? Promise.resolve(formatResponse({})) : undefined;
@@ -2711,13 +3301,13 @@ export class AgentWorkspaceProxy {
2711
3301
  const timer = setTimeout(() => {
2712
3302
  this.pendingStructuredInputs.delete(structuredInput.requestId);
2713
3303
  this.structuredInputWaiters.delete(structuredInput.requestId);
2714
- resolve(formatResponse({}));
3304
+ resolve(formatResponse({}, structuredInput));
2715
3305
  this.markStructuredInput(conversationId, structuredInput.requestId, {
2716
3306
  inputPending: false,
2717
3307
  inputError: "等待用户输入超时",
2718
3308
  });
2719
3309
  }, PERMISSION_TIMEOUT_MS);
2720
- this.structuredInputWaiters.set(structuredInput.requestId, { resolve, timer, source });
3310
+ this.structuredInputWaiters.set(structuredInput.requestId, { resolve, timer, source, input: structuredInput });
2721
3311
  });
2722
3312
  }
2723
3313
 
@@ -2773,6 +3363,12 @@ export class AgentWorkspaceProxy {
2773
3363
  conversationId,
2774
3364
  type: "permission",
2775
3365
  permission,
3366
+ metadata: {
3367
+ protocol: "v2",
3368
+ permissionLive: true,
3369
+ permissionExpired: false,
3370
+ permissionPending: false,
3371
+ },
2776
3372
  createdAt: Date.now(),
2777
3373
  updatedAt: Date.now(),
2778
3374
  };
@@ -2803,10 +3399,13 @@ export class AgentWorkspaceProxy {
2803
3399
  optionId?: string;
2804
3400
  }): void {
2805
3401
  const permission = this.pendingPermissions.get(payload.requestId);
3402
+ const waiter = this.permissionWaiters.get(payload.requestId);
3403
+ const existingItem = this.findItem(payload.conversationId, `permission:${payload.requestId}`);
3404
+ const alreadyResolved = Boolean(existingItem?.metadata?.permissionOutcome);
3405
+ if (!permission && !waiter && alreadyResolved) return;
2806
3406
  this.pendingPermissions.delete(payload.requestId);
2807
3407
  const selectedOptionId =
2808
3408
  payload.optionId ?? selectPermissionOption(permission, payload.outcome);
2809
- const waiter = this.permissionWaiters.get(payload.requestId);
2810
3409
  if (waiter) {
2811
3410
  clearTimeout(waiter.timer);
2812
3411
  this.permissionWaiters.delete(payload.requestId);
@@ -2832,7 +3431,9 @@ export class AgentWorkspaceProxy {
2832
3431
  permissionError: undefined,
2833
3432
  permissionPending: false,
2834
3433
  });
2835
- this.updateConversationStatus(payload.conversationId, "running");
3434
+ if (permission || waiter || existingItem) {
3435
+ this.updateConversationStatus(payload.conversationId, "running");
3436
+ }
2836
3437
  }
2837
3438
 
2838
3439
  private respondStructuredInput(payload: {
@@ -2841,15 +3442,15 @@ export class AgentWorkspaceProxy {
2841
3442
  answers: Record<string, string[]>;
2842
3443
  }): void {
2843
3444
  const pending = this.pendingStructuredInputs.get(payload.requestId);
2844
- this.pendingStructuredInputs.delete(payload.requestId);
2845
3445
  const waiter = this.structuredInputWaiters.get(payload.requestId);
3446
+ const existingItem = this.findItem(payload.conversationId, `input:${payload.requestId}`);
3447
+ const alreadySubmitted = existingItem?.metadata?.inputSubmitted === true;
3448
+ if (!pending && !waiter && alreadySubmitted) return;
3449
+ this.pendingStructuredInputs.delete(payload.requestId);
2846
3450
  if (waiter) {
2847
3451
  clearTimeout(waiter.timer);
2848
3452
  this.structuredInputWaiters.delete(payload.requestId);
2849
- const formatResponse = waiter.source === "mcpServer/elicitation/request"
2850
- ? formatMcpElicitationResponse
2851
- : formatStructuredInputResponse;
2852
- waiter.resolve(formatResponse(payload.answers));
3453
+ waiter.resolve(formatStructuredInputResponseForSource(waiter.source, payload.answers, waiter.input));
2853
3454
  }
2854
3455
  this.markStructuredInput(payload.conversationId, payload.requestId, {
2855
3456
  inputPending: false,
@@ -2858,7 +3459,9 @@ export class AgentWorkspaceProxy {
2858
3459
  inputError: undefined,
2859
3460
  answers: payload.answers,
2860
3461
  });
2861
- this.updateConversationStatus(pending?.conversationId ?? payload.conversationId, "running");
3462
+ if (pending || waiter || existingItem) {
3463
+ this.updateConversationStatus(pending?.conversationId ?? payload.conversationId, "running");
3464
+ }
2862
3465
  }
2863
3466
 
2864
3467
  private markPermission(
@@ -3058,7 +3661,7 @@ export class AgentWorkspaceProxy {
3058
3661
  this.input.send(createEnvelope({
3059
3662
  type: "agent.v2.event",
3060
3663
  sessionId: this.input.sessionId,
3061
- payload: { conversationId, conversation, item },
3664
+ payload: { conversationId, conversation: conversation ? { ...conversation } : undefined, item },
3062
3665
  }));
3063
3666
  }
3064
3667
 
@@ -3066,7 +3669,7 @@ export class AgentWorkspaceProxy {
3066
3669
  this.input.send(createEnvelope({
3067
3670
  type: "agent.v2.event",
3068
3671
  sessionId: this.input.sessionId,
3069
- payload: { conversationId: conversation.id, conversation },
3672
+ payload: { conversationId: conversation.id, conversation: { ...conversation } },
3070
3673
  }));
3071
3674
  }
3072
3675
 
@@ -3101,6 +3704,21 @@ export class AgentWorkspaceProxy {
3101
3704
  }));
3102
3705
  }
3103
3706
 
3707
+ private rejectAgentAction(
3708
+ conversation: AgentConversation,
3709
+ message: string,
3710
+ status: AgentStatus = "idle",
3711
+ ): void {
3712
+ this.updateConversationStatus(conversation.id, status, message);
3713
+ this.addItem(conversation.id, {
3714
+ id: id("error"),
3715
+ conversationId: conversation.id,
3716
+ type: "error",
3717
+ error: message,
3718
+ createdAt: Date.now(),
3719
+ });
3720
+ }
3721
+
3104
3722
  private updateConversationPreview(
3105
3723
  conversationId: string,
3106
3724
  text: string,
@@ -3228,12 +3846,22 @@ export class AgentWorkspaceProxy {
3228
3846
  private handleProviderExit(provider: AgentProvider, message: string): void {
3229
3847
  this.clients.delete(provider);
3230
3848
  this.agentProtocols.delete(provider);
3231
- this.cancelPendingPermissions();
3849
+ this.providerErrors.set(provider, message);
3850
+ for (const conversation of this.conversations.values()) {
3851
+ if (conversation.provider === provider && conversation.status === "waiting_permission") {
3852
+ this.cancelPendingPermissions(conversation.id, false);
3853
+ }
3854
+ }
3232
3855
  for (const conversation of this.conversations.values()) {
3233
3856
  if (conversation.provider !== provider) continue;
3857
+ if (conversation.status !== "running" && conversation.status !== "waiting_permission") {
3858
+ this.emitConversation(conversation);
3859
+ continue;
3860
+ }
3234
3861
  conversation.status = "error";
3235
3862
  conversation.lastMessagePreview = message;
3236
3863
  conversation.lastActivityAt = Date.now();
3864
+ this.forgetCurrentTurn(conversation.id);
3237
3865
  this.emitConversation(conversation);
3238
3866
  this.addItem(conversation.id, {
3239
3867
  id: id("error"),
@@ -3246,8 +3874,9 @@ export class AgentWorkspaceProxy {
3246
3874
  this.sendCapabilities();
3247
3875
  }
3248
3876
 
3249
- private cancelPendingPermissions(conversationId?: string): void {
3877
+ private cancelPendingPermissions(conversationId?: string, updateStatus = true): void {
3250
3878
  for (const [requestId, waiter] of this.permissionWaiters) {
3879
+ if (conversationId && this.itemConversationIds.get(requestId) !== conversationId) continue;
3251
3880
  clearTimeout(waiter.timer);
3252
3881
  waiter.resolve(formatPermissionResponse(
3253
3882
  this.permissionSources.get(requestId),
@@ -3257,11 +3886,12 @@ export class AgentWorkspaceProxy {
3257
3886
  this.pendingPermissions.delete(requestId);
3258
3887
  this.permissionSources.delete(requestId);
3259
3888
  }
3260
- this.permissionWaiters.clear();
3889
+ if (!conversationId) this.permissionWaiters.clear();
3261
3890
  for (const [requestId, waiter] of this.structuredInputWaiters) {
3262
- clearTimeout(waiter.timer);
3263
- waiter.resolve(formatStructuredInputResponse({}));
3264
3891
  const pending = this.pendingStructuredInputs.get(requestId);
3892
+ if (conversationId && pending?.conversationId !== conversationId) continue;
3893
+ clearTimeout(waiter.timer);
3894
+ waiter.resolve(formatStructuredInputResponseForSource(waiter.source, {}, waiter.input));
3265
3895
  if (pending) {
3266
3896
  this.markStructuredInput(pending.conversationId, requestId, {
3267
3897
  inputPending: false,
@@ -3269,9 +3899,10 @@ export class AgentWorkspaceProxy {
3269
3899
  });
3270
3900
  }
3271
3901
  this.pendingStructuredInputs.delete(requestId);
3902
+ this.structuredInputWaiters.delete(requestId);
3272
3903
  }
3273
- this.structuredInputWaiters.clear();
3274
- if (conversationId) this.updateConversationStatus(conversationId, "idle");
3904
+ if (!conversationId) this.structuredInputWaiters.clear();
3905
+ if (conversationId && updateStatus) this.updateConversationStatus(conversationId, "idle");
3275
3906
  }
3276
3907
 
3277
3908
  private extractSessionId(value: unknown): string | undefined {
@@ -3359,6 +3990,37 @@ function formatStructuredInputResponse(answers: Record<string, string[]>): unkno
3359
3990
  };
3360
3991
  }
3361
3992
 
3993
+ function formatClaudeAskUserQuestionResponse(
3994
+ answers: Record<string, string[]>,
3995
+ input?: AgentStructuredInput,
3996
+ ): unknown {
3997
+ const questions = input?.questions ?? [];
3998
+ const byQuestionText: Record<string, string | string[]> = {};
3999
+ for (const question of questions) {
4000
+ const values = answers[question.id] ?? answers[question.question] ?? [];
4001
+ const cleaned = values.map((value) => value.trim()).filter(Boolean);
4002
+ if (cleaned.length === 0) continue;
4003
+ byQuestionText[question.question] = question.selectionLimit && question.selectionLimit > 1
4004
+ ? cleaned
4005
+ : cleaned[0] ?? "";
4006
+ }
4007
+ return {
4008
+ behavior: "allow",
4009
+ updatedInput: {
4010
+ questions: questions.map((question) => ({
4011
+ question: question.question,
4012
+ header: question.header,
4013
+ options: question.options?.map((option) => ({
4014
+ label: option.label,
4015
+ description: option.description,
4016
+ })) ?? [],
4017
+ multiSelect: Boolean(question.selectionLimit && question.selectionLimit > 1),
4018
+ })),
4019
+ answers: byQuestionText,
4020
+ },
4021
+ };
4022
+ }
4023
+
3362
4024
  function formatMcpElicitationResponse(answers: Record<string, string[]>): unknown {
3363
4025
  const content = Object.fromEntries(
3364
4026
  Object.entries(answers).map(([questionId, values]) => [
@@ -3373,6 +4035,20 @@ function formatMcpElicitationResponse(answers: Record<string, string[]>): unknow
3373
4035
  };
3374
4036
  }
3375
4037
 
4038
+ function formatStructuredInputResponseForSource(
4039
+ source: string | undefined,
4040
+ answers: Record<string, string[]>,
4041
+ input?: AgentStructuredInput,
4042
+ ): unknown {
4043
+ if (source === "mcpServer/elicitation/request") {
4044
+ return formatMcpElicitationResponse(answers);
4045
+ }
4046
+ if (source === "claude/askUserQuestion") {
4047
+ return formatClaudeAskUserQuestionResponse(answers, input);
4048
+ }
4049
+ return formatStructuredInputResponse(answers);
4050
+ }
4051
+
3376
4052
  function formatPermissionResponse(
3377
4053
  source: string | undefined,
3378
4054
  outcome: "allow" | "deny" | "cancelled",