linkshell-cli 0.5.0 → 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 +728 -98
  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 +784 -89
  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
  }
@@ -766,16 +809,12 @@ interface ProviderRuntimeCapabilities {
766
809
  const ALL_REASONING_EFFORTS = ["none", "minimal", "low", "medium", "high", "xhigh"] as const;
767
810
  const ALL_PERMISSION_MODES = ["read_only", "workspace_write", "full_access"] as const;
768
811
 
812
+ // Codex `model/list` is the source of truth — these are only used when the RPC
813
+ // returns nothing so mobile still sees a usable picker.
769
814
  const CODEX_FALLBACK_MODELS: AgentModelOption[] = [
770
815
  { id: "default", label: "默认模型" },
771
- { id: "gpt-5", label: "GPT-5" },
772
- { id: "gpt-5-codex", label: "GPT-5 Codex" },
773
- { id: "gpt-5-pro", label: "GPT-5 Pro" },
774
- { id: "gpt-5-high", label: "GPT-5 High" },
775
- { id: "o3", label: "o3" },
776
- { id: "o3-mini", label: "o3-mini" },
777
816
  ];
778
- const CODEX_FALLBACK_DEFAULT_MODEL = "gpt-5";
817
+ const CODEX_FALLBACK_DEFAULT_MODEL = "default";
779
818
  const CODEX_COMMAND_NAMES = ["plan", "exit-plan", "compact", "clear", "status", "review", "subagents"] as const;
780
819
  const CLAUDE_REMOTE_HIDDEN_COMMANDS = new Set([
781
820
  "add-dir",
@@ -1161,10 +1200,13 @@ function parseModelListCapabilities(value: unknown): ProviderRuntimeCapabilities
1161
1200
  const raw = asRecord(value);
1162
1201
  const modelsValue =
1163
1202
  Array.isArray(value) ? value :
1203
+ Array.isArray(raw?.data) ? raw.data :
1164
1204
  Array.isArray(raw?.models) ? raw.models :
1165
1205
  Array.isArray(raw?.items) ? raw.items :
1166
1206
  Array.isArray(raw?.modelOptions) ? raw.modelOptions :
1167
1207
  [];
1208
+ let inferredDefault: string | undefined;
1209
+ const efforts = new Set<string>();
1168
1210
  const models = modelsValue
1169
1211
  .map((entry, index) => {
1170
1212
  const model = asRecord(entry);
@@ -1173,24 +1215,44 @@ function parseModelListCapabilities(value: unknown): ProviderRuntimeCapabilities
1173
1215
  ? { id: entry, label: entry }
1174
1216
  : undefined;
1175
1217
  }
1218
+ if (model.hidden === true) return undefined;
1176
1219
  const modelId = firstString(model, ["id", "model", "name", "value"]) ?? `model-${index + 1}`;
1177
- const label = firstString(model, ["label", "title", "displayName", "name"]) ?? modelId;
1220
+ const label = firstString(model, ["displayName", "label", "title", "name"]) ?? modelId;
1221
+ if (model.isDefault === true && !inferredDefault) inferredDefault = modelId;
1222
+ const supported = Array.isArray(model.supportedReasoningEfforts) ? model.supportedReasoningEfforts : [];
1223
+ for (const item of supported) {
1224
+ const effort = typeof item === "string"
1225
+ ? item
1226
+ : firstString(asRecord(item), ["reasoningEffort", "id", "value"]);
1227
+ if (effort && ALL_REASONING_EFFORTS.includes(effort as typeof ALL_REASONING_EFFORTS[number])) {
1228
+ efforts.add(effort);
1229
+ }
1230
+ }
1178
1231
  return { id: modelId, label };
1179
1232
  })
1180
1233
  .filter((entry): entry is AgentModelOption => Boolean(entry));
1181
1234
  const defaultModel =
1182
1235
  firstString(raw, ["defaultModel", "default_model", "currentModel"]) ??
1183
- firstString(asRecord(raw?.defaults), ["model"]);
1184
- const effortsValue =
1236
+ firstString(asRecord(raw?.defaults), ["model"]) ??
1237
+ inferredDefault;
1238
+ const explicitEffortsValue =
1185
1239
  Array.isArray(raw?.reasoningEfforts) ? raw.reasoningEfforts :
1186
1240
  Array.isArray(raw?.reasoning_efforts) ? raw.reasoning_efforts :
1187
1241
  Array.isArray(raw?.efforts) ? raw.efforts :
1188
1242
  undefined;
1189
- const reasoningEfforts = effortsValue
1190
- ?.filter((entry): entry is string => typeof entry === "string" && ALL_REASONING_EFFORTS.includes(entry as typeof ALL_REASONING_EFFORTS[number]));
1243
+ if (explicitEffortsValue) {
1244
+ for (const item of explicitEffortsValue) {
1245
+ if (typeof item === "string" && ALL_REASONING_EFFORTS.includes(item as typeof ALL_REASONING_EFFORTS[number])) {
1246
+ efforts.add(item);
1247
+ }
1248
+ }
1249
+ }
1250
+ const reasoningEfforts = efforts.size
1251
+ ? ALL_REASONING_EFFORTS.filter((effort) => efforts.has(effort))
1252
+ : undefined;
1191
1253
  if (models.length === 0 && !defaultModel && !reasoningEfforts?.length) return undefined;
1192
1254
  return {
1193
- ...(models.length > 0 ? { models: [{ id: "default", label: "默认模型" }, ...models] } : {}),
1255
+ ...(models.length > 0 ? { models } : {}),
1194
1256
  ...(defaultModel ? { defaultModel } : {}),
1195
1257
  ...(reasoningEfforts?.length ? { reasoningEfforts } : {}),
1196
1258
  };
@@ -1218,6 +1280,7 @@ function parseRemoteSessions(value: unknown): Array<{
1218
1280
  const raw = asRecord(value);
1219
1281
  const sessionsValue =
1220
1282
  Array.isArray(value) ? value :
1283
+ Array.isArray(raw?.data) ? raw.data :
1221
1284
  Array.isArray(raw?.threads) ? raw.threads :
1222
1285
  Array.isArray(raw?.sessions) ? raw.sessions :
1223
1286
  Array.isArray(raw?.items) ? raw.items :
@@ -1243,7 +1306,7 @@ function parseRemoteSessions(value: unknown): Array<{
1243
1306
  result.push({
1244
1307
  id,
1245
1308
  cwd: firstString(source, ["cwd", "workingDirectory", "workspacePath"]),
1246
- title: firstString(source, ["title", "name", "summary"]),
1309
+ title: firstString(source, ["title", "name", "summary", "preview"]),
1247
1310
  model: firstString(source, ["model", "modelId"]),
1248
1311
  createdAt: parseTimestamp(source.createdAt ?? source.created_at),
1249
1312
  lastActivityAt: parseTimestamp(source.lastActivityAt ?? source.updatedAt ?? source.modifiedAt ?? source.lastModified ?? source.updated_at),
@@ -1252,10 +1315,279 @@ function parseRemoteSessions(value: unknown): Array<{
1252
1315
  return result;
1253
1316
  }
1254
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
+
1255
1586
  export class AgentWorkspaceProxy {
1256
1587
  private clients = new Map<AgentProvider, AcpClient | ClaudeSdkClient | ClaudeStreamJsonClient>();
1257
1588
  private agentProtocols = new Map<AgentProvider, AgentProtocol>();
1258
1589
  private providerCapabilities = new Map<AgentProvider, ProviderRuntimeCapabilities>();
1590
+ private providerErrors = new Map<AgentProvider, string>();
1259
1591
  private initialized = false;
1260
1592
  private status: AgentStatus = "unavailable";
1261
1593
  private error: string | undefined;
@@ -1328,11 +1660,21 @@ export class AgentWorkspaceProxy {
1328
1660
  case "agent.v2.cancel": {
1329
1661
  const payload = parseTypedPayload("agent.v2.cancel", envelope.payload);
1330
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
+ }
1331
1673
  this.cancelPendingPermissions(payload.conversationId);
1332
- const cancelClient = conversation ? this.clientForProvider(conversation.provider) : undefined;
1674
+ const cancelClient = this.clientForProvider(conversation.provider);
1333
1675
  cancelClient?.cancel({
1334
- sessionId: conversation?.agentSessionId,
1335
- turnId: this.currentTurnIds.get(payload.conversationId),
1676
+ sessionId: conversation.agentSessionId,
1677
+ turnId,
1336
1678
  });
1337
1679
  this.forgetCurrentTurn(payload.conversationId);
1338
1680
  this.updateConversationStatus(payload.conversationId, "idle");
@@ -1367,6 +1709,13 @@ export class AgentWorkspaceProxy {
1367
1709
  return this.agentProtocols.get(provider);
1368
1710
  }
1369
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
+
1370
1719
  private async initialize(): Promise<void> {
1371
1720
  if (this.initialized) return;
1372
1721
  this.initialized = true;
@@ -1433,6 +1782,7 @@ export class AgentWorkspaceProxy {
1433
1782
  try {
1434
1783
  const client = await tryCreateClient(resolved);
1435
1784
  this.clients.set(provider, client);
1785
+ this.providerErrors.delete(provider);
1436
1786
  await this.refreshProviderCapabilities(provider, client, resolved.protocol);
1437
1787
  this.status = "idle";
1438
1788
  this.error = undefined;
@@ -1452,6 +1802,7 @@ export class AgentWorkspaceProxy {
1452
1802
  };
1453
1803
  const client = await tryCreateClient(fallback);
1454
1804
  this.clients.set(provider, client);
1805
+ this.providerErrors.delete(provider);
1455
1806
  await this.refreshProviderCapabilities(provider, client, fallback.protocol);
1456
1807
  this.status = "idle";
1457
1808
  this.error = undefined;
@@ -1466,6 +1817,8 @@ export class AgentWorkspaceProxy {
1466
1817
  if (this.input.verbose) {
1467
1818
  process.stderr.write(`[agent:v2] failed to start ${provider}: ${error instanceof Error ? error.message : String(error)}\n`);
1468
1819
  }
1820
+ this.providerErrors.set(provider, error instanceof Error ? error.message : String(error));
1821
+ this.sendCapabilities();
1469
1822
  return undefined;
1470
1823
  }
1471
1824
  }
@@ -1518,7 +1871,7 @@ export class AgentWorkspaceProxy {
1518
1871
  const agentSessionId = remote.id;
1519
1872
  const existingId = this.conversationByAgentSessionId.get(agentSessionId);
1520
1873
  const now = Date.now();
1521
- const conversationId = existingId ?? `agent:${agentSessionId}`;
1874
+ const conversationId = existingId ?? makeAgentV2RemoteConversationId(provider, agentSessionId);
1522
1875
  const existing = this.conversations.get(conversationId);
1523
1876
  const cwd = remote.cwd ?? existing?.cwd ?? this.input.cwd;
1524
1877
  const conversation: AgentConversation = {
@@ -1549,6 +1902,83 @@ export class AgentWorkspaceProxy {
1549
1902
  }
1550
1903
  }
1551
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
+
1552
1982
  private sendCapabilities(): void {
1553
1983
  const providers = this.input.availableProviders.map((provider) => {
1554
1984
  const client = this.clients.get(provider);
@@ -1568,7 +1998,7 @@ export class AgentWorkspaceProxy {
1568
1998
  id: provider,
1569
1999
  label: providerLabel(provider),
1570
2000
  enabled,
1571
- reason: enabled ? undefined : `${providerLabel(provider)} 未安装或启动失败`,
2001
+ reason: enabled ? undefined : this.providerErrors.get(provider) ?? `${providerLabel(provider)} 未安装或启动失败`,
1572
2002
  supportsImages,
1573
2003
  supportsPermission,
1574
2004
  supportsPlan: enabled,
@@ -1640,24 +2070,63 @@ export class AgentWorkspaceProxy {
1640
2070
  );
1641
2071
  }
1642
2072
 
1643
- const client = await this.ensureProviderClient(provider);
1644
- if (!client) {
1645
- return this.openFailure(
1646
- payload,
1647
- `${providerLabel(provider)} 启动失败。请确认 CLI 已安装并可用。`,
1648
- );
1649
- }
1650
-
1651
- const cwd = payload.cwd ?? this.input.cwd;
2073
+ let cwd = payload.cwd ?? this.input.cwd;
1652
2074
  let agentSessionId = payload.agentSessionId;
1653
2075
  let existingConversation =
1654
2076
  (payload.conversationId ? this.conversations.get(payload.conversationId) : undefined) ??
1655
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
+ }
1656
2103
 
1657
2104
  if (existingConversation && existingConversation.status !== "error" && existingConversation.agentSessionId) {
1658
- 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
+ ) {
1659
2111
  existingConversation = this.adoptConversationId(existingConversation.id, payload.conversationId);
1660
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
+ }
1661
2130
  this.activeConversationId = existingConversation.id;
1662
2131
  this.input.send(createEnvelope({
1663
2132
  type: "agent.v2.conversation.opened",
@@ -1665,6 +2134,7 @@ export class AgentWorkspaceProxy {
1665
2134
  payload: {
1666
2135
  conversation: existingConversation,
1667
2136
  snapshot: this.timelines.get(existingConversation.id) ?? [],
2137
+ requestedConversationId: payload.conversationId,
1668
2138
  },
1669
2139
  }));
1670
2140
  return existingConversation;
@@ -1676,7 +2146,7 @@ export class AgentWorkspaceProxy {
1676
2146
  : await client.newSession({ cwd });
1677
2147
  agentSessionId = this.extractSessionId(result) ?? agentSessionId ?? id("agent-session");
1678
2148
  const now = Date.now();
1679
- const conversationId = payload.conversationId ?? `agent:${agentSessionId}`;
2149
+ const conversationId = makeAgentV2RemoteConversationId(provider, agentSessionId);
1680
2150
  const conversation: AgentConversation = {
1681
2151
  ...existingConversation,
1682
2152
  id: conversationId,
@@ -1698,10 +2168,15 @@ export class AgentWorkspaceProxy {
1698
2168
  this.conversationByAgentSessionId.set(agentSessionId, conversation.id);
1699
2169
  this.activeConversationId = conversation.id;
1700
2170
  this.timelines.set(conversation.id, this.timelines.get(conversation.id) ?? []);
2171
+ await this.hydrateConversationFromProvider(conversation, result);
1701
2172
  this.input.send(createEnvelope({
1702
2173
  type: "agent.v2.conversation.opened",
1703
2174
  sessionId: this.input.sessionId,
1704
- payload: { conversation, snapshot: this.timelines.get(conversation.id) ?? [] },
2175
+ payload: {
2176
+ conversation,
2177
+ snapshot: this.timelines.get(conversation.id) ?? [],
2178
+ requestedConversationId: payload.conversationId,
2179
+ },
1705
2180
  }));
1706
2181
  return conversation;
1707
2182
  } catch (error) {
@@ -1753,15 +2228,71 @@ export class AgentWorkspaceProxy {
1753
2228
  this.input.send(createEnvelope({
1754
2229
  type: "agent.v2.conversation.opened",
1755
2230
  sessionId: this.input.sessionId,
1756
- payload: { conversation, snapshot: this.timelines.get(conversation.id) ?? [] },
2231
+ payload: {
2232
+ conversation,
2233
+ snapshot: this.timelines.get(conversation.id) ?? [],
2234
+ requestedConversationId: payload.conversationId,
2235
+ },
1757
2236
  }));
1758
2237
  return conversation;
1759
2238
  }
1760
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
+ },
2285
+ }));
2286
+ this.sendCapabilities();
2287
+ return conversation;
2288
+ }
2289
+
1761
2290
  private async sendPrompt(payload: {
1762
2291
  conversationId: string;
1763
2292
  clientMessageId: string;
1764
2293
  contentBlocks: AgentContentBlock[];
2294
+ delivery?: "auto" | "new_turn" | "steer";
2295
+ targetTurnId?: string;
1765
2296
  model?: string;
1766
2297
  reasoningEffort?: string;
1767
2298
  permissionMode?: AgentPermissionMode;
@@ -1772,39 +2303,28 @@ export class AgentWorkspaceProxy {
1772
2303
  await this.openConversation({ conversationId: payload.conversationId });
1773
2304
  if (!conversation) return;
1774
2305
  if (!conversation.agentSessionId) {
1775
- this.addItem(payload.conversationId, {
1776
- id: id("error"),
1777
- conversationId: payload.conversationId,
1778
- type: "error",
1779
- error: "Agent session 尚未就绪,消息没有发送。请重新打开对话后再试。",
1780
- createdAt: Date.now(),
1781
- });
2306
+ this.rejectAgentAction(
2307
+ conversation,
2308
+ "Agent session 尚未就绪,消息没有发送。请重新打开对话后再试。",
2309
+ );
1782
2310
  return;
1783
2311
  }
1784
- const client = this.clientForProvider(conversation.provider);
2312
+ const client = this.clientForProvider(conversation.provider) ?? await this.ensureProviderClient(conversation.provider);
1785
2313
  if (!client) {
1786
- this.addItem(conversation.id, {
1787
- id: id("error"),
1788
- conversationId: conversation.id,
1789
- type: "error",
1790
- error: `${providerLabel(conversation.provider)} 未连接,消息没有发送。`,
1791
- createdAt: Date.now(),
1792
- });
2314
+ this.rejectAgentAction(
2315
+ conversation,
2316
+ this.providerErrors.get(conversation.provider) ?? `${providerLabel(conversation.provider)} 未连接,消息没有发送。`,
2317
+ "error",
2318
+ );
1793
2319
  return;
1794
2320
  }
1795
2321
 
1796
2322
  const protocol = this.protocolForProvider(conversation.provider);
1797
2323
  if (payload.contentBlocks.some((block) => block.type === "image") && !protocolSupportsImages(protocol)) {
1798
- conversation.status = "idle";
1799
- conversation.lastActivityAt = Date.now();
1800
- this.emitConversation(conversation);
1801
- this.addItem(conversation.id, {
1802
- id: id("error"),
1803
- conversationId: conversation.id,
1804
- type: "error",
1805
- error: "当前 Agent provider 暂不支持图片输入,请升级 CLI 或切换到 Codex。",
1806
- createdAt: Date.now(),
1807
- });
2324
+ this.rejectAgentAction(
2325
+ conversation,
2326
+ "当前 Agent provider 暂不支持图片输入,请升级 CLI 或切换到 Codex。",
2327
+ );
1808
2328
  return;
1809
2329
  }
1810
2330
 
@@ -1813,7 +2333,35 @@ export class AgentWorkspaceProxy {
1813
2333
  reasoningEffort: conversation.reasoningEffort,
1814
2334
  permissionMode: conversation.permissionMode,
1815
2335
  };
1816
- 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;
1817
2365
  conversation.reasoningEffort = payload.reasoningEffort ?? conversation.reasoningEffort;
1818
2366
  conversation.permissionMode = payload.permissionMode ?? conversation.permissionMode;
1819
2367
  conversation.collaborationMode = payload.collaborationMode ?? conversation.collaborationMode;
@@ -1821,14 +2369,14 @@ export class AgentWorkspaceProxy {
1821
2369
  conversation.lastActivityAt = Date.now();
1822
2370
  this.activeConversationId = conversation.id;
1823
2371
 
1824
- if (payload.model && payload.model !== priorSettings.model) {
2372
+ if (effectiveModel && effectiveModel !== priorSettings.model) {
1825
2373
  const runtimeCapabilities = this.providerCapabilities.get(conversation.provider);
1826
- 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;
1827
2375
  this.emitNotice({
1828
2376
  conversationId: conversation.id,
1829
2377
  kind: "model_changed",
1830
2378
  title: `已切换模型 · ${label}`,
1831
- detail: `${providerLabel(conversation.provider)} 下次回复将使用 ${payload.model}`,
2379
+ detail: `${providerLabel(conversation.provider)} 下次回复将使用 ${effectiveModel}`,
1832
2380
  });
1833
2381
  }
1834
2382
  if (payload.reasoningEffort && payload.reasoningEffort !== priorSettings.reasoningEffort) {
@@ -1854,21 +2402,62 @@ export class AgentWorkspaceProxy {
1854
2402
  role: "user",
1855
2403
  content: payload.contentBlocks,
1856
2404
  text: userText,
2405
+ metadata: shouldSteer ? { delivery: "steer", targetTurnId: activeTurnId } : undefined,
1857
2406
  createdAt: Date.now(),
1858
2407
  });
1859
2408
  this.emitConversation(conversation);
1860
2409
 
1861
- try {
1862
- const result = await client.prompt({
1863
- sessionId: conversation.agentSessionId,
2410
+ const promptAsNewTurn = () =>
2411
+ client.prompt({
2412
+ sessionId: conversation.agentSessionId!,
1864
2413
  content: payload.contentBlocks,
1865
2414
  clientMessageId: payload.clientMessageId,
1866
- model: payload.model,
2415
+ model: effectiveModel,
1867
2416
  reasoningEffort: payload.reasoningEffort,
1868
2417
  permissionMode: payload.permissionMode,
1869
2418
  collaborationMode: payload.collaborationMode ?? conversation.collaborationMode,
1870
2419
  cwd: conversation.cwd,
1871
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
+ }
1872
2461
  const nextAgentSessionId = this.extractSessionId(result);
1873
2462
  if (nextAgentSessionId && nextAgentSessionId !== conversation.agentSessionId) {
1874
2463
  if (conversation.agentSessionId) this.conversationByAgentSessionId.delete(conversation.agentSessionId);
@@ -2101,6 +2690,9 @@ export class AgentWorkspaceProxy {
2101
2690
  }
2102
2691
 
2103
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
+ }
2104
2696
  if (method === "item/tool/requestUserInput" || method === "tool/requestUserInput") {
2105
2697
  return this.handleStructuredInput(params, true);
2106
2698
  }
@@ -2148,7 +2740,6 @@ export class AgentWorkspaceProxy {
2148
2740
  if (
2149
2741
  method.startsWith("account/") ||
2150
2742
  method.startsWith("mcpServer/startupStatus/") ||
2151
- method === "thread/status/changed" ||
2152
2743
  method === "thread/tokenUsage/updated" ||
2153
2744
  method === "serverRequest/resolved" ||
2154
2745
  method === "mcpServer/oauthLogin/completed"
@@ -2157,6 +2748,25 @@ export class AgentWorkspaceProxy {
2157
2748
  }
2158
2749
 
2159
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
+ }
2160
2770
  if (method === "item/tool/requestUserInput" || method === "tool/requestUserInput") {
2161
2771
  this.handleStructuredInput(params);
2162
2772
  return;
@@ -2666,9 +3276,8 @@ export class AgentWorkspaceProxy {
2666
3276
  const raw = asRecord(params) ?? {};
2667
3277
  const conversationId = this.conversationIdFromParams(raw) ?? this.fallbackConversationId();
2668
3278
  const source = firstString(raw, ["method", "source", "requestMethod"]);
2669
- const formatResponse = source === "mcpServer/elicitation/request"
2670
- ? formatMcpElicitationResponse
2671
- : formatStructuredInputResponse;
3279
+ const formatResponse = (answers: Record<string, string[]>, input?: AgentStructuredInput) =>
3280
+ formatStructuredInputResponseForSource(source, answers, input);
2672
3281
  if (!conversationId) return waitForResponse ? Promise.resolve(formatResponse({})) : undefined;
2673
3282
  const structuredInput = decodeStructuredInput(raw);
2674
3283
  if (!structuredInput) return waitForResponse ? Promise.resolve(formatResponse({})) : undefined;
@@ -2692,13 +3301,13 @@ export class AgentWorkspaceProxy {
2692
3301
  const timer = setTimeout(() => {
2693
3302
  this.pendingStructuredInputs.delete(structuredInput.requestId);
2694
3303
  this.structuredInputWaiters.delete(structuredInput.requestId);
2695
- resolve(formatResponse({}));
3304
+ resolve(formatResponse({}, structuredInput));
2696
3305
  this.markStructuredInput(conversationId, structuredInput.requestId, {
2697
3306
  inputPending: false,
2698
3307
  inputError: "等待用户输入超时",
2699
3308
  });
2700
3309
  }, PERMISSION_TIMEOUT_MS);
2701
- this.structuredInputWaiters.set(structuredInput.requestId, { resolve, timer, source });
3310
+ this.structuredInputWaiters.set(structuredInput.requestId, { resolve, timer, source, input: structuredInput });
2702
3311
  });
2703
3312
  }
2704
3313
 
@@ -2754,6 +3363,12 @@ export class AgentWorkspaceProxy {
2754
3363
  conversationId,
2755
3364
  type: "permission",
2756
3365
  permission,
3366
+ metadata: {
3367
+ protocol: "v2",
3368
+ permissionLive: true,
3369
+ permissionExpired: false,
3370
+ permissionPending: false,
3371
+ },
2757
3372
  createdAt: Date.now(),
2758
3373
  updatedAt: Date.now(),
2759
3374
  };
@@ -2784,10 +3399,13 @@ export class AgentWorkspaceProxy {
2784
3399
  optionId?: string;
2785
3400
  }): void {
2786
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;
2787
3406
  this.pendingPermissions.delete(payload.requestId);
2788
3407
  const selectedOptionId =
2789
3408
  payload.optionId ?? selectPermissionOption(permission, payload.outcome);
2790
- const waiter = this.permissionWaiters.get(payload.requestId);
2791
3409
  if (waiter) {
2792
3410
  clearTimeout(waiter.timer);
2793
3411
  this.permissionWaiters.delete(payload.requestId);
@@ -2813,7 +3431,9 @@ export class AgentWorkspaceProxy {
2813
3431
  permissionError: undefined,
2814
3432
  permissionPending: false,
2815
3433
  });
2816
- this.updateConversationStatus(payload.conversationId, "running");
3434
+ if (permission || waiter || existingItem) {
3435
+ this.updateConversationStatus(payload.conversationId, "running");
3436
+ }
2817
3437
  }
2818
3438
 
2819
3439
  private respondStructuredInput(payload: {
@@ -2822,15 +3442,15 @@ export class AgentWorkspaceProxy {
2822
3442
  answers: Record<string, string[]>;
2823
3443
  }): void {
2824
3444
  const pending = this.pendingStructuredInputs.get(payload.requestId);
2825
- this.pendingStructuredInputs.delete(payload.requestId);
2826
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);
2827
3450
  if (waiter) {
2828
3451
  clearTimeout(waiter.timer);
2829
3452
  this.structuredInputWaiters.delete(payload.requestId);
2830
- const formatResponse = waiter.source === "mcpServer/elicitation/request"
2831
- ? formatMcpElicitationResponse
2832
- : formatStructuredInputResponse;
2833
- waiter.resolve(formatResponse(payload.answers));
3453
+ waiter.resolve(formatStructuredInputResponseForSource(waiter.source, payload.answers, waiter.input));
2834
3454
  }
2835
3455
  this.markStructuredInput(payload.conversationId, payload.requestId, {
2836
3456
  inputPending: false,
@@ -2839,7 +3459,9 @@ export class AgentWorkspaceProxy {
2839
3459
  inputError: undefined,
2840
3460
  answers: payload.answers,
2841
3461
  });
2842
- this.updateConversationStatus(pending?.conversationId ?? payload.conversationId, "running");
3462
+ if (pending || waiter || existingItem) {
3463
+ this.updateConversationStatus(pending?.conversationId ?? payload.conversationId, "running");
3464
+ }
2843
3465
  }
2844
3466
 
2845
3467
  private markPermission(
@@ -3039,7 +3661,7 @@ export class AgentWorkspaceProxy {
3039
3661
  this.input.send(createEnvelope({
3040
3662
  type: "agent.v2.event",
3041
3663
  sessionId: this.input.sessionId,
3042
- payload: { conversationId, conversation, item },
3664
+ payload: { conversationId, conversation: conversation ? { ...conversation } : undefined, item },
3043
3665
  }));
3044
3666
  }
3045
3667
 
@@ -3047,7 +3669,7 @@ export class AgentWorkspaceProxy {
3047
3669
  this.input.send(createEnvelope({
3048
3670
  type: "agent.v2.event",
3049
3671
  sessionId: this.input.sessionId,
3050
- payload: { conversationId: conversation.id, conversation },
3672
+ payload: { conversationId: conversation.id, conversation: { ...conversation } },
3051
3673
  }));
3052
3674
  }
3053
3675
 
@@ -3082,6 +3704,21 @@ export class AgentWorkspaceProxy {
3082
3704
  }));
3083
3705
  }
3084
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
+
3085
3722
  private updateConversationPreview(
3086
3723
  conversationId: string,
3087
3724
  text: string,
@@ -3209,12 +3846,22 @@ export class AgentWorkspaceProxy {
3209
3846
  private handleProviderExit(provider: AgentProvider, message: string): void {
3210
3847
  this.clients.delete(provider);
3211
3848
  this.agentProtocols.delete(provider);
3212
- 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
+ }
3213
3855
  for (const conversation of this.conversations.values()) {
3214
3856
  if (conversation.provider !== provider) continue;
3857
+ if (conversation.status !== "running" && conversation.status !== "waiting_permission") {
3858
+ this.emitConversation(conversation);
3859
+ continue;
3860
+ }
3215
3861
  conversation.status = "error";
3216
3862
  conversation.lastMessagePreview = message;
3217
3863
  conversation.lastActivityAt = Date.now();
3864
+ this.forgetCurrentTurn(conversation.id);
3218
3865
  this.emitConversation(conversation);
3219
3866
  this.addItem(conversation.id, {
3220
3867
  id: id("error"),
@@ -3227,8 +3874,9 @@ export class AgentWorkspaceProxy {
3227
3874
  this.sendCapabilities();
3228
3875
  }
3229
3876
 
3230
- private cancelPendingPermissions(conversationId?: string): void {
3877
+ private cancelPendingPermissions(conversationId?: string, updateStatus = true): void {
3231
3878
  for (const [requestId, waiter] of this.permissionWaiters) {
3879
+ if (conversationId && this.itemConversationIds.get(requestId) !== conversationId) continue;
3232
3880
  clearTimeout(waiter.timer);
3233
3881
  waiter.resolve(formatPermissionResponse(
3234
3882
  this.permissionSources.get(requestId),
@@ -3238,11 +3886,12 @@ export class AgentWorkspaceProxy {
3238
3886
  this.pendingPermissions.delete(requestId);
3239
3887
  this.permissionSources.delete(requestId);
3240
3888
  }
3241
- this.permissionWaiters.clear();
3889
+ if (!conversationId) this.permissionWaiters.clear();
3242
3890
  for (const [requestId, waiter] of this.structuredInputWaiters) {
3243
- clearTimeout(waiter.timer);
3244
- waiter.resolve(formatStructuredInputResponse({}));
3245
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));
3246
3895
  if (pending) {
3247
3896
  this.markStructuredInput(pending.conversationId, requestId, {
3248
3897
  inputPending: false,
@@ -3250,9 +3899,10 @@ export class AgentWorkspaceProxy {
3250
3899
  });
3251
3900
  }
3252
3901
  this.pendingStructuredInputs.delete(requestId);
3902
+ this.structuredInputWaiters.delete(requestId);
3253
3903
  }
3254
- this.structuredInputWaiters.clear();
3255
- if (conversationId) this.updateConversationStatus(conversationId, "idle");
3904
+ if (!conversationId) this.structuredInputWaiters.clear();
3905
+ if (conversationId && updateStatus) this.updateConversationStatus(conversationId, "idle");
3256
3906
  }
3257
3907
 
3258
3908
  private extractSessionId(value: unknown): string | undefined {
@@ -3340,6 +3990,37 @@ function formatStructuredInputResponse(answers: Record<string, string[]>): unkno
3340
3990
  };
3341
3991
  }
3342
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
+
3343
4024
  function formatMcpElicitationResponse(answers: Record<string, string[]>): unknown {
3344
4025
  const content = Object.fromEntries(
3345
4026
  Object.entries(answers).map(([questionId, values]) => [
@@ -3354,6 +4035,20 @@ function formatMcpElicitationResponse(answers: Record<string, string[]>): unknow
3354
4035
  };
3355
4036
  }
3356
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
+
3357
4052
  function formatPermissionResponse(
3358
4053
  source: string | undefined,
3359
4054
  outcome: "allow" | "deny" | "cancelled",