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
@@ -11,6 +11,12 @@ const MAX_TIMELINE_ITEMS = 200;
11
11
  function id(prefix) {
12
12
  return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
13
13
  }
14
+ function normalizeAgentSessionIdSegment(value) {
15
+ return value.replace(/[^a-zA-Z0-9_-]+/g, "-");
16
+ }
17
+ export function makeAgentV2RemoteConversationId(provider, agentSessionId) {
18
+ return `agent-remote-${provider}-${normalizeAgentSessionIdSegment(agentSessionId)}`;
19
+ }
14
20
  function stringify(value) {
15
21
  if (typeof value === "string")
16
22
  return value;
@@ -66,6 +72,12 @@ function extractItem(value) {
66
72
  return undefined;
67
73
  return asRecord(raw.item) ?? raw;
68
74
  }
75
+ function extractTurn(value) {
76
+ const raw = asRecord(value);
77
+ if (!raw)
78
+ return undefined;
79
+ return asRecord(raw.turn) ?? raw;
80
+ }
69
81
  function stringifyDefined(value) {
70
82
  if (value === undefined || value === null || value === "")
71
83
  return undefined;
@@ -107,6 +119,29 @@ function normalizePlanStatus(value) {
107
119
  return "in_progress";
108
120
  return "pending";
109
121
  }
122
+ function agentStatusFromThreadStatus(value) {
123
+ const raw = asRecord(value);
124
+ const status = typeof value === "string"
125
+ ? value
126
+ : firstString(raw, ["status", "state", "phase", "type"]) ??
127
+ firstString(asRecord(raw?.status), ["status", "state", "phase", "type"]);
128
+ const normalized = normalizedIdentifier(status);
129
+ if (!normalized)
130
+ return undefined;
131
+ if (["active", "running", "inprogress", "busy", "working", "streaming"].includes(normalized)) {
132
+ return "running";
133
+ }
134
+ if (["idle", "ready", "completed", "complete", "done", "finished"].includes(normalized)) {
135
+ return "idle";
136
+ }
137
+ if (["waitingpermission", "waitingforpermission", "requirespermission", "needspermission"].includes(normalized)) {
138
+ return "waiting_permission";
139
+ }
140
+ if (["error", "failed", "systemerror", "fatal"].includes(normalized)) {
141
+ return "error";
142
+ }
143
+ return undefined;
144
+ }
110
145
  function nameFromToolMethod(method) {
111
146
  if (method.includes("commandExecution"))
112
147
  return "命令";
@@ -473,7 +508,9 @@ function parseStructuredInputQuestion(value, index) {
473
508
  question,
474
509
  isOther: raw.isOther === true,
475
510
  isSecret: raw.isSecret === true || raw.secret === true,
476
- selectionLimit: firstNumber(raw, ["selectionLimit", "maxSelections"]),
511
+ selectionLimit: raw.multiSelect === true
512
+ ? options.length || undefined
513
+ : firstNumber(raw, ["selectionLimit", "maxSelections"]),
477
514
  options: options.length > 0 ? options : undefined,
478
515
  };
479
516
  }
@@ -555,16 +592,12 @@ function providerLabel(provider) {
555
592
  }
556
593
  const ALL_REASONING_EFFORTS = ["none", "minimal", "low", "medium", "high", "xhigh"];
557
594
  const ALL_PERMISSION_MODES = ["read_only", "workspace_write", "full_access"];
595
+ // Codex `model/list` is the source of truth — these are only used when the RPC
596
+ // returns nothing so mobile still sees a usable picker.
558
597
  const CODEX_FALLBACK_MODELS = [
559
598
  { id: "default", label: "默认模型" },
560
- { id: "gpt-5", label: "GPT-5" },
561
- { id: "gpt-5-codex", label: "GPT-5 Codex" },
562
- { id: "gpt-5-pro", label: "GPT-5 Pro" },
563
- { id: "gpt-5-high", label: "GPT-5 High" },
564
- { id: "o3", label: "o3" },
565
- { id: "o3-mini", label: "o3-mini" },
566
599
  ];
567
- const CODEX_FALLBACK_DEFAULT_MODEL = "gpt-5";
600
+ const CODEX_FALLBACK_DEFAULT_MODEL = "default";
568
601
  const CODEX_COMMAND_NAMES = ["plan", "exit-plan", "compact", "clear", "status", "review", "subagents"];
569
602
  const CLAUDE_REMOTE_HIDDEN_COMMANDS = new Set([
570
603
  "add-dir",
@@ -937,10 +970,13 @@ function runtimeCommands(provider, value) {
937
970
  function parseModelListCapabilities(value) {
938
971
  const raw = asRecord(value);
939
972
  const modelsValue = Array.isArray(value) ? value :
940
- Array.isArray(raw?.models) ? raw.models :
941
- Array.isArray(raw?.items) ? raw.items :
942
- Array.isArray(raw?.modelOptions) ? raw.modelOptions :
943
- [];
973
+ Array.isArray(raw?.data) ? raw.data :
974
+ Array.isArray(raw?.models) ? raw.models :
975
+ Array.isArray(raw?.items) ? raw.items :
976
+ Array.isArray(raw?.modelOptions) ? raw.modelOptions :
977
+ [];
978
+ let inferredDefault;
979
+ const efforts = new Set();
944
980
  const models = modelsValue
945
981
  .map((entry, index) => {
946
982
  const model = asRecord(entry);
@@ -949,23 +985,45 @@ function parseModelListCapabilities(value) {
949
985
  ? { id: entry, label: entry }
950
986
  : undefined;
951
987
  }
988
+ if (model.hidden === true)
989
+ return undefined;
952
990
  const modelId = firstString(model, ["id", "model", "name", "value"]) ?? `model-${index + 1}`;
953
- const label = firstString(model, ["label", "title", "displayName", "name"]) ?? modelId;
991
+ const label = firstString(model, ["displayName", "label", "title", "name"]) ?? modelId;
992
+ if (model.isDefault === true && !inferredDefault)
993
+ inferredDefault = modelId;
994
+ const supported = Array.isArray(model.supportedReasoningEfforts) ? model.supportedReasoningEfforts : [];
995
+ for (const item of supported) {
996
+ const effort = typeof item === "string"
997
+ ? item
998
+ : firstString(asRecord(item), ["reasoningEffort", "id", "value"]);
999
+ if (effort && ALL_REASONING_EFFORTS.includes(effort)) {
1000
+ efforts.add(effort);
1001
+ }
1002
+ }
954
1003
  return { id: modelId, label };
955
1004
  })
956
1005
  .filter((entry) => Boolean(entry));
957
1006
  const defaultModel = firstString(raw, ["defaultModel", "default_model", "currentModel"]) ??
958
- firstString(asRecord(raw?.defaults), ["model"]);
959
- const effortsValue = Array.isArray(raw?.reasoningEfforts) ? raw.reasoningEfforts :
1007
+ firstString(asRecord(raw?.defaults), ["model"]) ??
1008
+ inferredDefault;
1009
+ const explicitEffortsValue = Array.isArray(raw?.reasoningEfforts) ? raw.reasoningEfforts :
960
1010
  Array.isArray(raw?.reasoning_efforts) ? raw.reasoning_efforts :
961
1011
  Array.isArray(raw?.efforts) ? raw.efforts :
962
1012
  undefined;
963
- const reasoningEfforts = effortsValue
964
- ?.filter((entry) => typeof entry === "string" && ALL_REASONING_EFFORTS.includes(entry));
1013
+ if (explicitEffortsValue) {
1014
+ for (const item of explicitEffortsValue) {
1015
+ if (typeof item === "string" && ALL_REASONING_EFFORTS.includes(item)) {
1016
+ efforts.add(item);
1017
+ }
1018
+ }
1019
+ }
1020
+ const reasoningEfforts = efforts.size
1021
+ ? ALL_REASONING_EFFORTS.filter((effort) => efforts.has(effort))
1022
+ : undefined;
965
1023
  if (models.length === 0 && !defaultModel && !reasoningEfforts?.length)
966
1024
  return undefined;
967
1025
  return {
968
- ...(models.length > 0 ? { models: [{ id: "default", label: "默认模型" }, ...models] } : {}),
1026
+ ...(models.length > 0 ? { models } : {}),
969
1027
  ...(defaultModel ? { defaultModel } : {}),
970
1028
  ...(reasoningEfforts?.length ? { reasoningEfforts } : {}),
971
1029
  };
@@ -983,10 +1041,11 @@ function parseTimestamp(value) {
983
1041
  function parseRemoteSessions(value) {
984
1042
  const raw = asRecord(value);
985
1043
  const sessionsValue = Array.isArray(value) ? value :
986
- Array.isArray(raw?.threads) ? raw.threads :
987
- Array.isArray(raw?.sessions) ? raw.sessions :
988
- Array.isArray(raw?.items) ? raw.items :
989
- [];
1044
+ Array.isArray(raw?.data) ? raw.data :
1045
+ Array.isArray(raw?.threads) ? raw.threads :
1046
+ Array.isArray(raw?.sessions) ? raw.sessions :
1047
+ Array.isArray(raw?.items) ? raw.items :
1048
+ [];
990
1049
  const result = [];
991
1050
  for (const entry of sessionsValue) {
992
1051
  const session = asRecord(entry);
@@ -1003,7 +1062,7 @@ function parseRemoteSessions(value) {
1003
1062
  result.push({
1004
1063
  id,
1005
1064
  cwd: firstString(source, ["cwd", "workingDirectory", "workspacePath"]),
1006
- title: firstString(source, ["title", "name", "summary"]),
1065
+ title: firstString(source, ["title", "name", "summary", "preview"]),
1007
1066
  model: firstString(source, ["model", "modelId"]),
1008
1067
  createdAt: parseTimestamp(source.createdAt ?? source.created_at),
1009
1068
  lastActivityAt: parseTimestamp(source.lastActivityAt ?? source.updatedAt ?? source.modifiedAt ?? source.lastModified ?? source.updated_at),
@@ -1011,11 +1070,265 @@ function parseRemoteSessions(value) {
1011
1070
  }
1012
1071
  return result;
1013
1072
  }
1073
+ function threadFromProviderResult(value) {
1074
+ const raw = asRecord(value);
1075
+ return asRecord(raw?.thread) ?? raw;
1076
+ }
1077
+ function threadFromTurnsListResult(sessionId, value) {
1078
+ const raw = asRecord(value);
1079
+ const order = firstString(raw, ["sortDirection", "order", "sortOrder", "sort"]);
1080
+ const turns = Array.isArray(raw?.turns) ? raw.turns :
1081
+ Array.isArray(raw?.data) ? raw.data :
1082
+ Array.isArray(raw?.items) ? raw.items :
1083
+ Array.isArray(value) ? value :
1084
+ undefined;
1085
+ if (!turns)
1086
+ return undefined;
1087
+ const extracted = turns
1088
+ .map(extractTurn)
1089
+ .filter((turn) => Boolean(turn));
1090
+ const chronological = order && /asc/i.test(order)
1091
+ ? extracted
1092
+ : [...extracted].reverse();
1093
+ return { id: sessionId, turns: chronological };
1094
+ }
1095
+ function turnItemsFromThread(thread) {
1096
+ if (!thread)
1097
+ return [];
1098
+ const turnsValue = Array.isArray(thread.turns)
1099
+ ? thread.turns
1100
+ : Array.isArray(thread.items)
1101
+ ? [{ id: firstString(thread, ["turnId", "id"]), items: thread.items }]
1102
+ : [];
1103
+ const result = [];
1104
+ for (const turnEntry of turnsValue) {
1105
+ const turn = extractTurn(turnEntry);
1106
+ if (!turn)
1107
+ continue;
1108
+ const turnId = firstString(turn, ["id", "turnId"]);
1109
+ const createdAt = parseTimestamp(turn.createdAt ??
1110
+ turn.created_at ??
1111
+ turn.startedAt ??
1112
+ turn.started_at ??
1113
+ turn.completedAt ??
1114
+ turn.completed_at ??
1115
+ turn.updatedAt ??
1116
+ turn.updated_at);
1117
+ const itemsRecord = asRecord(turn.items);
1118
+ const items = Array.isArray(turn.items)
1119
+ ? turn.items
1120
+ : Array.isArray(itemsRecord?.data)
1121
+ ? itemsRecord.data
1122
+ : Array.isArray(itemsRecord?.items)
1123
+ ? itemsRecord.items
1124
+ : Array.isArray(turn.item)
1125
+ ? turn.item
1126
+ : Array.isArray(turn.events)
1127
+ ? turn.events
1128
+ : [];
1129
+ for (const itemEntry of items) {
1130
+ const item = extractItem(itemEntry);
1131
+ if (!item)
1132
+ continue;
1133
+ result.push({
1134
+ ...item,
1135
+ ...(turnId && !firstString(item, ["turnId"]) ? { turnId } : {}),
1136
+ __turnCreatedAt: createdAt,
1137
+ });
1138
+ }
1139
+ }
1140
+ return result;
1141
+ }
1142
+ function activeTurnIdFromThread(thread) {
1143
+ if (!thread)
1144
+ return undefined;
1145
+ const turnsValue = Array.isArray(thread.turns)
1146
+ ? thread.turns
1147
+ : Array.isArray(thread.items)
1148
+ ? [{ id: firstString(thread, ["turnId", "id"]), items: thread.items, status: thread.status }]
1149
+ : [];
1150
+ let activeTurnId;
1151
+ for (const turnEntry of turnsValue) {
1152
+ const turn = extractTurn(turnEntry);
1153
+ if (!turn)
1154
+ continue;
1155
+ const turnId = firstString(turn, ["id", "turnId"]);
1156
+ if (!turnId)
1157
+ continue;
1158
+ const status = firstString(turn, ["status", "state", "phase"]) ??
1159
+ firstString(asRecord(turn.status), ["status", "state", "phase", "type"]);
1160
+ const normalized = normalizedIdentifier(status);
1161
+ if ([
1162
+ "active",
1163
+ "running",
1164
+ "inprogress",
1165
+ "busy",
1166
+ "working",
1167
+ "streaming",
1168
+ "waitingpermission",
1169
+ "waitingforpermission",
1170
+ "requirespermission",
1171
+ "needspermission",
1172
+ ].includes(normalized)) {
1173
+ activeTurnId = turnId;
1174
+ }
1175
+ }
1176
+ return activeTurnId;
1177
+ }
1178
+ function timelineItemFromProviderItem(item, conversationId, index) {
1179
+ const itemType = firstString(item, ["type"]);
1180
+ const normalized = normalizedIdentifier(itemType);
1181
+ const itemId = firstString(item, ["id", "itemId", "messageId", "toolCallId"]) ?? `history-${index + 1}`;
1182
+ const createdAt = parseTimestamp(item.createdAt ?? item.created_at ?? item.timestamp ?? item.__turnCreatedAt) ??
1183
+ Date.now() + index;
1184
+ const base = {
1185
+ id: itemId,
1186
+ conversationId,
1187
+ turnId: firstString(item, ["turnId"]),
1188
+ itemId,
1189
+ createdAt,
1190
+ updatedAt: parseTimestamp(item.updatedAt ?? item.updated_at) ?? createdAt,
1191
+ isStreaming: false,
1192
+ };
1193
+ if (normalized === "usermessage" ||
1194
+ normalized === "userinput" ||
1195
+ (normalized === "message" && item.role === "user")) {
1196
+ const content = contentBlocksFromItem(item);
1197
+ const text = textFromBlocks(content);
1198
+ if (!content.length && !text)
1199
+ return undefined;
1200
+ return {
1201
+ ...base,
1202
+ type: "message",
1203
+ kind: "chat",
1204
+ role: "user",
1205
+ content,
1206
+ text,
1207
+ };
1208
+ }
1209
+ if (normalized === "agentmessage" ||
1210
+ normalized === "assistantmessage" ||
1211
+ (normalized === "message" && item.role !== "user")) {
1212
+ const content = contentBlocksFromItem(item);
1213
+ const text = textFromBlocks(content);
1214
+ if (!content.length && !text)
1215
+ return undefined;
1216
+ return {
1217
+ ...base,
1218
+ type: "message",
1219
+ kind: "chat",
1220
+ role: item.role === "system" ? "system" : "assistant",
1221
+ content,
1222
+ text,
1223
+ };
1224
+ }
1225
+ if (normalized === "reasoning" || normalized === "thinking") {
1226
+ const text = firstString(item, ["text", "content", "summary", "message"]) ??
1227
+ stringifyDefined(item.contentItems ?? item.summary);
1228
+ if (!text)
1229
+ return undefined;
1230
+ return {
1231
+ ...base,
1232
+ type: "status",
1233
+ kind: "thinking",
1234
+ role: "system",
1235
+ text,
1236
+ };
1237
+ }
1238
+ if (normalized === "enteredreviewmode" || normalized === "exitedreviewmode") {
1239
+ const text = firstString(item, ["review", "text", "content", "summary", "message"]);
1240
+ return {
1241
+ ...base,
1242
+ type: "status",
1243
+ kind: "review",
1244
+ role: "system",
1245
+ text: text ?? (normalized === "enteredreviewmode" ? "正在审查" : "审查已完成"),
1246
+ };
1247
+ }
1248
+ if (normalized === "contextcompaction") {
1249
+ return {
1250
+ ...base,
1251
+ type: "status",
1252
+ kind: "context_compaction",
1253
+ role: "system",
1254
+ text: firstString(item, ["text", "summary", "message"]) ?? "上下文已压缩",
1255
+ };
1256
+ }
1257
+ if (isSubagentItemType(itemType)) {
1258
+ const subagent = decodeSubagentAction(item, normalizeToolStatus(item.status, true));
1259
+ if (!subagent)
1260
+ return undefined;
1261
+ return {
1262
+ ...base,
1263
+ type: "status",
1264
+ kind: "subagent_action",
1265
+ role: "system",
1266
+ text: summarizeSubagentAction(subagent),
1267
+ subagent,
1268
+ };
1269
+ }
1270
+ if (isToolItemType(itemType) || toolNameFromItem(item)) {
1271
+ const toolId = firstString(item, ["id", "itemId", "toolCallId"]) ?? itemId;
1272
+ const toolCall = {
1273
+ id: toolId,
1274
+ name: toolNameFromItem(item) ?? "工具",
1275
+ input: toolInputFromItem(item),
1276
+ output: normalized === "filechange" || normalized === "diff"
1277
+ ? extractDiffText(item) ?? firstString(item, ["output", "summary"])
1278
+ : firstString(item, ["aggregatedOutput", "output", "stdout", "stderr"]) ??
1279
+ stringifyDefined(item.result ?? item.error ?? item.contentItems),
1280
+ createdAt,
1281
+ status: normalizeToolStatus(item.status, true),
1282
+ };
1283
+ const kind = toolCall.name.includes("文件") || normalized === "filechange" || normalized === "diff"
1284
+ ? "file_change"
1285
+ : toolCall.name.includes("命令") || normalized === "commandexecution"
1286
+ ? "command_execution"
1287
+ : "tool_activity";
1288
+ return {
1289
+ ...base,
1290
+ id: `tool:${toolId}`,
1291
+ type: "tool_call",
1292
+ kind,
1293
+ itemId: toolId,
1294
+ toolCall,
1295
+ commandExecution: kind === "command_execution" ? commandExecutionFromTool(toolCall) : undefined,
1296
+ fileChange: kind === "file_change" ? fileChangeFromTool(toolCall) : undefined,
1297
+ };
1298
+ }
1299
+ return undefined;
1300
+ }
1301
+ function timelineItemsFromProviderThread(value, conversationId) {
1302
+ const items = turnItemsFromThread(threadFromProviderResult(value))
1303
+ .map((item, index) => timelineItemFromProviderItem(item, conversationId, index))
1304
+ .filter((item) => Boolean(item));
1305
+ const byId = new Map();
1306
+ for (const item of items) {
1307
+ byId.set(item.id, item);
1308
+ }
1309
+ return [...byId.values()].sort((a, b) => a.createdAt - b.createdAt).slice(-MAX_TIMELINE_ITEMS);
1310
+ }
1311
+ function previewFromTimelineItem(item) {
1312
+ if (item.error)
1313
+ return item.error;
1314
+ if (item.text)
1315
+ return item.text;
1316
+ if (item.content?.length)
1317
+ return textFromBlocks(item.content);
1318
+ if (item.toolCall)
1319
+ return `${item.toolCall.name} · ${item.toolCall.status}`;
1320
+ if (item.permission)
1321
+ return `需要授权 ${item.permission.toolName ?? ""}`.trim();
1322
+ if (item.subagent)
1323
+ return summarizeSubagentAction(item.subagent);
1324
+ return undefined;
1325
+ }
1014
1326
  export class AgentWorkspaceProxy {
1015
1327
  input;
1016
1328
  clients = new Map();
1017
1329
  agentProtocols = new Map();
1018
1330
  providerCapabilities = new Map();
1331
+ providerErrors = new Map();
1019
1332
  initialized = false;
1020
1333
  status = "unavailable";
1021
1334
  error;
@@ -1077,11 +1390,18 @@ export class AgentWorkspaceProxy {
1077
1390
  case "agent.v2.cancel": {
1078
1391
  const payload = parseTypedPayload("agent.v2.cancel", envelope.payload);
1079
1392
  const conversation = this.conversations.get(payload.conversationId);
1393
+ if (!conversation)
1394
+ break;
1395
+ const turnId = this.currentTurnIds.get(payload.conversationId);
1396
+ if (this.protocolForProvider(conversation.provider) === "codex-app-server" && !turnId) {
1397
+ this.rejectAgentAction(conversation, "无法停止 Codex:当前运行 turn 尚未同步,请重新打开对话后再试。", conversation.status);
1398
+ break;
1399
+ }
1080
1400
  this.cancelPendingPermissions(payload.conversationId);
1081
- const cancelClient = conversation ? this.clientForProvider(conversation.provider) : undefined;
1401
+ const cancelClient = this.clientForProvider(conversation.provider);
1082
1402
  cancelClient?.cancel({
1083
- sessionId: conversation?.agentSessionId,
1084
- turnId: this.currentTurnIds.get(payload.conversationId),
1403
+ sessionId: conversation.agentSessionId,
1404
+ turnId,
1085
1405
  });
1086
1406
  this.forgetCurrentTurn(payload.conversationId);
1087
1407
  this.updateConversationStatus(payload.conversationId, "idle");
@@ -1112,6 +1432,13 @@ export class AgentWorkspaceProxy {
1112
1432
  protocolForProvider(provider) {
1113
1433
  return this.agentProtocols.get(provider);
1114
1434
  }
1435
+ defaultModelForProvider(provider) {
1436
+ const capabilities = this.providerCapabilities.get(provider);
1437
+ const defaultModel = capabilities?.defaultModel?.trim();
1438
+ if (defaultModel)
1439
+ return defaultModel;
1440
+ return capabilities?.models?.find((model) => model.id && model.id !== "default")?.id;
1441
+ }
1115
1442
  async initialize() {
1116
1443
  if (this.initialized)
1117
1444
  return;
@@ -1176,6 +1503,7 @@ export class AgentWorkspaceProxy {
1176
1503
  try {
1177
1504
  const client = await tryCreateClient(resolved);
1178
1505
  this.clients.set(provider, client);
1506
+ this.providerErrors.delete(provider);
1179
1507
  await this.refreshProviderCapabilities(provider, client, resolved.protocol);
1180
1508
  this.status = "idle";
1181
1509
  this.error = undefined;
@@ -1196,6 +1524,7 @@ export class AgentWorkspaceProxy {
1196
1524
  };
1197
1525
  const client = await tryCreateClient(fallback);
1198
1526
  this.clients.set(provider, client);
1527
+ this.providerErrors.delete(provider);
1199
1528
  await this.refreshProviderCapabilities(provider, client, fallback.protocol);
1200
1529
  this.status = "idle";
1201
1530
  this.error = undefined;
@@ -1211,6 +1540,8 @@ export class AgentWorkspaceProxy {
1211
1540
  if (this.input.verbose) {
1212
1541
  process.stderr.write(`[agent:v2] failed to start ${provider}: ${error instanceof Error ? error.message : String(error)}\n`);
1213
1542
  }
1543
+ this.providerErrors.set(provider, error instanceof Error ? error.message : String(error));
1544
+ this.sendCapabilities();
1214
1545
  return undefined;
1215
1546
  }
1216
1547
  }
@@ -1260,7 +1591,7 @@ export class AgentWorkspaceProxy {
1260
1591
  const agentSessionId = remote.id;
1261
1592
  const existingId = this.conversationByAgentSessionId.get(agentSessionId);
1262
1593
  const now = Date.now();
1263
- const conversationId = existingId ?? `agent:${agentSessionId}`;
1594
+ const conversationId = existingId ?? makeAgentV2RemoteConversationId(provider, agentSessionId);
1264
1595
  const existing = this.conversations.get(conversationId);
1265
1596
  const cwd = remote.cwd ?? existing?.cwd ?? this.input.cwd;
1266
1597
  const conversation = {
@@ -1291,6 +1622,78 @@ export class AgentWorkspaceProxy {
1291
1622
  }
1292
1623
  }
1293
1624
  }
1625
+ async hydrateConversationFromProvider(conversation, seedResult) {
1626
+ const client = this.clientForProvider(conversation.provider);
1627
+ if (!client || !conversation.agentSessionId)
1628
+ return;
1629
+ let source = seedResult;
1630
+ const hasSeedTurns = turnItemsFromThread(threadFromProviderResult(seedResult)).length > 0;
1631
+ if (!hasSeedTurns) {
1632
+ const readSession = client.readSession;
1633
+ if (typeof readSession === "function") {
1634
+ try {
1635
+ source = await readSession.call(client, {
1636
+ sessionId: conversation.agentSessionId,
1637
+ includeTurns: true,
1638
+ });
1639
+ }
1640
+ catch (error) {
1641
+ if (this.input.verbose) {
1642
+ process.stderr.write(`[agent:v2] thread/read hydration failed for ${conversation.provider}: ${error instanceof Error ? error.message : String(error)}\n`);
1643
+ }
1644
+ }
1645
+ }
1646
+ if (turnItemsFromThread(threadFromProviderResult(source)).length === 0) {
1647
+ const listTurns = client.listTurns;
1648
+ if (typeof listTurns === "function") {
1649
+ try {
1650
+ const turnsResult = await listTurns.call(client, {
1651
+ sessionId: conversation.agentSessionId,
1652
+ limit: MAX_TIMELINE_ITEMS,
1653
+ sortDirection: "desc",
1654
+ itemsView: "full",
1655
+ });
1656
+ const turnsThread = threadFromTurnsListResult(conversation.agentSessionId, turnsResult);
1657
+ if (turnsThread)
1658
+ source = { thread: turnsThread };
1659
+ }
1660
+ catch (error) {
1661
+ if (this.input.verbose) {
1662
+ process.stderr.write(`[agent:v2] thread/turns/list hydration failed for ${conversation.provider}: ${error instanceof Error ? error.message : String(error)}\n`);
1663
+ }
1664
+ }
1665
+ }
1666
+ }
1667
+ }
1668
+ const thread = threadFromProviderResult(source);
1669
+ const activeTurnId = activeTurnIdFromThread(thread);
1670
+ if (activeTurnId)
1671
+ this.rememberTurnConversationId(conversation.id, activeTurnId);
1672
+ const model = firstString(thread, ["model", "modelId", "currentModel"]);
1673
+ if (model && !conversation.model)
1674
+ conversation.model = model;
1675
+ const hydratedItems = timelineItemsFromProviderThread(source, conversation.id);
1676
+ if (hydratedItems.length === 0)
1677
+ return;
1678
+ const existing = this.timelines.get(conversation.id) ?? [];
1679
+ const merged = new Map();
1680
+ for (const item of [...existing, ...hydratedItems]) {
1681
+ merged.set(item.id, item);
1682
+ }
1683
+ const nextItems = [...merged.values()]
1684
+ .sort((a, b) => a.createdAt - b.createdAt)
1685
+ .slice(-MAX_TIMELINE_ITEMS);
1686
+ this.timelines.set(conversation.id, nextItems);
1687
+ for (const item of nextItems) {
1688
+ this.rememberItemConversationId(conversation.id, item);
1689
+ }
1690
+ const lastPreview = [...nextItems].reverse()
1691
+ .map((item) => previewText(previewFromTimelineItem(item) ?? ""))
1692
+ .find(Boolean);
1693
+ if (lastPreview && !conversation.lastMessagePreview) {
1694
+ conversation.lastMessagePreview = lastPreview;
1695
+ }
1696
+ }
1294
1697
  sendCapabilities() {
1295
1698
  const providers = this.input.availableProviders.map((provider) => {
1296
1699
  const client = this.clients.get(provider);
@@ -1307,7 +1710,7 @@ export class AgentWorkspaceProxy {
1307
1710
  id: provider,
1308
1711
  label: providerLabel(provider),
1309
1712
  enabled,
1310
- reason: enabled ? undefined : `${providerLabel(provider)} 未安装或启动失败`,
1713
+ reason: enabled ? undefined : this.providerErrors.get(provider) ?? `${providerLabel(provider)} 未安装或启动失败`,
1311
1714
  supportsImages,
1312
1715
  supportsPermission,
1313
1716
  supportsPlan: enabled,
@@ -1364,18 +1767,60 @@ export class AgentWorkspaceProxy {
1364
1767
  if (!this.input.availableProviders.includes(provider)) {
1365
1768
  return this.openFailure(payload, `${providerLabel(provider)} 未安装或不可用。`);
1366
1769
  }
1367
- const client = await this.ensureProviderClient(provider);
1368
- if (!client) {
1369
- return this.openFailure(payload, `${providerLabel(provider)} 启动失败。请确认 CLI 已安装并可用。`);
1370
- }
1371
- const cwd = payload.cwd ?? this.input.cwd;
1770
+ let cwd = payload.cwd ?? this.input.cwd;
1372
1771
  let agentSessionId = payload.agentSessionId;
1373
1772
  let existingConversation = (payload.conversationId ? this.conversations.get(payload.conversationId) : undefined) ??
1374
1773
  (agentSessionId ? this.conversations.get(this.conversationByAgentSessionId.get(agentSessionId) ?? "") : undefined);
1774
+ if (!payload.cwd && existingConversation?.cwd)
1775
+ cwd = existingConversation.cwd;
1776
+ const client = await this.ensureProviderClient(provider);
1777
+ if (!client) {
1778
+ const message = this.providerErrors.get(provider) ?? `${providerLabel(provider)} 启动失败。请确认 CLI 已安装并可用。`;
1779
+ if (existingConversation) {
1780
+ this.activeConversationId = existingConversation.id;
1781
+ this.input.send(createEnvelope({
1782
+ type: "agent.v2.conversation.opened",
1783
+ sessionId: this.input.sessionId,
1784
+ payload: {
1785
+ conversation: existingConversation,
1786
+ snapshot: this.timelines.get(existingConversation.id) ?? [],
1787
+ requestedConversationId: payload.conversationId,
1788
+ providerError: message,
1789
+ },
1790
+ }));
1791
+ this.sendCapabilities();
1792
+ return existingConversation;
1793
+ }
1794
+ if (payload.conversationId) {
1795
+ return this.openOfflineHistory(payload, message, cwd);
1796
+ }
1797
+ return this.openFailure(payload, message, cwd);
1798
+ }
1375
1799
  if (existingConversation && existingConversation.status !== "error" && existingConversation.agentSessionId) {
1376
- if (payload.conversationId && existingConversation.id !== payload.conversationId) {
1800
+ const requestedCanonicalId = makeAgentV2RemoteConversationId(provider, existingConversation.agentSessionId);
1801
+ if (payload.conversationId &&
1802
+ payload.conversationId === requestedCanonicalId &&
1803
+ existingConversation.id !== payload.conversationId) {
1377
1804
  existingConversation = this.adoptConversationId(existingConversation.id, payload.conversationId);
1378
1805
  }
1806
+ if ((this.timelines.get(existingConversation.id) ?? []).length === 0 && existingConversation.status !== "running") {
1807
+ try {
1808
+ const existingAgentSessionId = existingConversation.agentSessionId;
1809
+ const result = await client.loadSession({ sessionId: existingAgentSessionId, cwd });
1810
+ const nextAgentSessionId = this.extractSessionId(result);
1811
+ if (nextAgentSessionId && nextAgentSessionId !== existingAgentSessionId) {
1812
+ this.conversationByAgentSessionId.delete(existingAgentSessionId);
1813
+ existingConversation.agentSessionId = nextAgentSessionId;
1814
+ this.conversationByAgentSessionId.set(nextAgentSessionId, existingConversation.id);
1815
+ }
1816
+ await this.hydrateConversationFromProvider(existingConversation, result);
1817
+ }
1818
+ catch (error) {
1819
+ if (this.input.verbose) {
1820
+ process.stderr.write(`[agent:v2] resume failed for ${provider}: ${error instanceof Error ? error.message : String(error)}\n`);
1821
+ }
1822
+ }
1823
+ }
1379
1824
  this.activeConversationId = existingConversation.id;
1380
1825
  this.input.send(createEnvelope({
1381
1826
  type: "agent.v2.conversation.opened",
@@ -1383,6 +1828,7 @@ export class AgentWorkspaceProxy {
1383
1828
  payload: {
1384
1829
  conversation: existingConversation,
1385
1830
  snapshot: this.timelines.get(existingConversation.id) ?? [],
1831
+ requestedConversationId: payload.conversationId,
1386
1832
  },
1387
1833
  }));
1388
1834
  return existingConversation;
@@ -1393,7 +1839,7 @@ export class AgentWorkspaceProxy {
1393
1839
  : await client.newSession({ cwd });
1394
1840
  agentSessionId = this.extractSessionId(result) ?? agentSessionId ?? id("agent-session");
1395
1841
  const now = Date.now();
1396
- const conversationId = payload.conversationId ?? `agent:${agentSessionId}`;
1842
+ const conversationId = makeAgentV2RemoteConversationId(provider, agentSessionId);
1397
1843
  const conversation = {
1398
1844
  ...existingConversation,
1399
1845
  id: conversationId,
@@ -1415,10 +1861,15 @@ export class AgentWorkspaceProxy {
1415
1861
  this.conversationByAgentSessionId.set(agentSessionId, conversation.id);
1416
1862
  this.activeConversationId = conversation.id;
1417
1863
  this.timelines.set(conversation.id, this.timelines.get(conversation.id) ?? []);
1864
+ await this.hydrateConversationFromProvider(conversation, result);
1418
1865
  this.input.send(createEnvelope({
1419
1866
  type: "agent.v2.conversation.opened",
1420
1867
  sessionId: this.input.sessionId,
1421
- payload: { conversation, snapshot: this.timelines.get(conversation.id) ?? [] },
1868
+ payload: {
1869
+ conversation,
1870
+ snapshot: this.timelines.get(conversation.id) ?? [],
1871
+ requestedConversationId: payload.conversationId,
1872
+ },
1422
1873
  }));
1423
1874
  return conversation;
1424
1875
  }
@@ -1457,70 +1908,111 @@ export class AgentWorkspaceProxy {
1457
1908
  this.input.send(createEnvelope({
1458
1909
  type: "agent.v2.conversation.opened",
1459
1910
  sessionId: this.input.sessionId,
1460
- payload: { conversation, snapshot: this.timelines.get(conversation.id) ?? [] },
1911
+ payload: {
1912
+ conversation,
1913
+ snapshot: this.timelines.get(conversation.id) ?? [],
1914
+ requestedConversationId: payload.conversationId,
1915
+ },
1461
1916
  }));
1462
1917
  return conversation;
1463
1918
  }
1919
+ openOfflineHistory(payload, message, cwd = payload.cwd ?? this.input.cwd) {
1920
+ const now = Date.now();
1921
+ const conversation = {
1922
+ id: payload.conversationId,
1923
+ agentSessionId: payload.agentSessionId,
1924
+ provider: payload.provider ?? this.input.availableProviders[0] ?? "codex",
1925
+ cwd,
1926
+ title: payload.title ?? titleFromCwd(cwd),
1927
+ model: payload.model,
1928
+ reasoningEffort: payload.reasoningEffort,
1929
+ permissionMode: payload.permissionMode,
1930
+ collaborationMode: payload.collaborationMode,
1931
+ status: "idle",
1932
+ archived: false,
1933
+ lastActivityAt: now,
1934
+ createdAt: now,
1935
+ };
1936
+ this.conversations.set(conversation.id, conversation);
1937
+ if (conversation.agentSessionId) {
1938
+ this.conversationByAgentSessionId.set(conversation.agentSessionId, conversation.id);
1939
+ }
1940
+ this.activeConversationId = conversation.id;
1941
+ this.input.send(createEnvelope({
1942
+ type: "agent.v2.conversation.opened",
1943
+ sessionId: this.input.sessionId,
1944
+ payload: {
1945
+ conversation,
1946
+ snapshot: this.timelines.get(conversation.id) ?? [],
1947
+ requestedConversationId: payload.conversationId,
1948
+ providerError: message,
1949
+ },
1950
+ }));
1951
+ this.sendCapabilities();
1952
+ return conversation;
1953
+ }
1464
1954
  async sendPrompt(payload) {
1465
1955
  const conversation = this.conversations.get(payload.conversationId) ??
1466
1956
  await this.openConversation({ conversationId: payload.conversationId });
1467
1957
  if (!conversation)
1468
1958
  return;
1469
1959
  if (!conversation.agentSessionId) {
1470
- this.addItem(payload.conversationId, {
1471
- id: id("error"),
1472
- conversationId: payload.conversationId,
1473
- type: "error",
1474
- error: "Agent session 尚未就绪,消息没有发送。请重新打开对话后再试。",
1475
- createdAt: Date.now(),
1476
- });
1960
+ this.rejectAgentAction(conversation, "Agent session 尚未就绪,消息没有发送。请重新打开对话后再试。");
1477
1961
  return;
1478
1962
  }
1479
- const client = this.clientForProvider(conversation.provider);
1963
+ const client = this.clientForProvider(conversation.provider) ?? await this.ensureProviderClient(conversation.provider);
1480
1964
  if (!client) {
1481
- this.addItem(conversation.id, {
1482
- id: id("error"),
1483
- conversationId: conversation.id,
1484
- type: "error",
1485
- error: `${providerLabel(conversation.provider)} 未连接,消息没有发送。`,
1486
- createdAt: Date.now(),
1487
- });
1965
+ this.rejectAgentAction(conversation, this.providerErrors.get(conversation.provider) ?? `${providerLabel(conversation.provider)} 未连接,消息没有发送。`, "error");
1488
1966
  return;
1489
1967
  }
1490
1968
  const protocol = this.protocolForProvider(conversation.provider);
1491
1969
  if (payload.contentBlocks.some((block) => block.type === "image") && !protocolSupportsImages(protocol)) {
1492
- conversation.status = "idle";
1493
- conversation.lastActivityAt = Date.now();
1494
- this.emitConversation(conversation);
1970
+ this.rejectAgentAction(conversation, "当前 Agent provider 暂不支持图片输入,请升级 CLI 或切换到 Codex。");
1971
+ return;
1972
+ }
1973
+ const priorSettings = {
1974
+ model: conversation.model,
1975
+ reasoningEffort: conversation.reasoningEffort,
1976
+ permissionMode: conversation.permissionMode,
1977
+ };
1978
+ const effectiveModel = payload.model ??
1979
+ conversation.model ??
1980
+ (protocol === "codex-app-server" ? this.defaultModelForProvider(conversation.provider) : undefined);
1981
+ const wasRunning = conversation.status === "running";
1982
+ const activeTurnId = payload.targetTurnId ?? this.currentTurnIds.get(conversation.id);
1983
+ const shouldSteer = Boolean(payload.delivery !== "new_turn" &&
1984
+ wasRunning &&
1985
+ protocol === "codex-app-server" &&
1986
+ activeTurnId);
1987
+ if (wasRunning && payload.delivery !== "new_turn" && protocol !== "codex-app-server") {
1495
1988
  this.addItem(conversation.id, {
1496
1989
  id: id("error"),
1497
1990
  conversationId: conversation.id,
1498
1991
  type: "error",
1499
- error: "当前 Agent provider 暂不支持图片输入,请升级 CLI 或切换到 Codex。",
1992
+ error: `${providerLabel(conversation.provider)} 当前不支持运行中追加输入。请等待本轮结束,或先停止后再发送。`,
1500
1993
  createdAt: Date.now(),
1501
1994
  });
1502
1995
  return;
1503
1996
  }
1504
- const priorSettings = {
1505
- model: conversation.model,
1506
- reasoningEffort: conversation.reasoningEffort,
1507
- permissionMode: conversation.permissionMode,
1508
- };
1509
- conversation.model = payload.model ?? conversation.model;
1997
+ if (payload.delivery === "steer" && protocol === "codex-app-server" && !activeTurnId) {
1998
+ this.rejectAgentAction(conversation, "当前没有可追加输入的 Codex turn。请等待本轮开始后重试,或作为新消息发送。");
1999
+ return;
2000
+ }
2001
+ conversation.model = effectiveModel ?? conversation.model;
1510
2002
  conversation.reasoningEffort = payload.reasoningEffort ?? conversation.reasoningEffort;
1511
2003
  conversation.permissionMode = payload.permissionMode ?? conversation.permissionMode;
1512
2004
  conversation.collaborationMode = payload.collaborationMode ?? conversation.collaborationMode;
1513
2005
  conversation.status = "running";
1514
2006
  conversation.lastActivityAt = Date.now();
1515
2007
  this.activeConversationId = conversation.id;
1516
- if (payload.model && payload.model !== priorSettings.model) {
2008
+ if (effectiveModel && effectiveModel !== priorSettings.model) {
1517
2009
  const runtimeCapabilities = this.providerCapabilities.get(conversation.provider);
1518
- const label = runtimeCapabilities?.models?.find((m) => m.id === payload.model)?.label ?? payload.model;
2010
+ const label = runtimeCapabilities?.models?.find((m) => m.id === effectiveModel)?.label ?? effectiveModel;
1519
2011
  this.emitNotice({
1520
2012
  conversationId: conversation.id,
1521
2013
  kind: "model_changed",
1522
2014
  title: `已切换模型 · ${label}`,
1523
- detail: `${providerLabel(conversation.provider)} 下次回复将使用 ${payload.model}`,
2015
+ detail: `${providerLabel(conversation.provider)} 下次回复将使用 ${effectiveModel}`,
1524
2016
  });
1525
2017
  }
1526
2018
  if (payload.reasoningEffort && payload.reasoningEffort !== priorSettings.reasoningEffort) {
@@ -1545,20 +2037,61 @@ export class AgentWorkspaceProxy {
1545
2037
  role: "user",
1546
2038
  content: payload.contentBlocks,
1547
2039
  text: userText,
2040
+ metadata: shouldSteer ? { delivery: "steer", targetTurnId: activeTurnId } : undefined,
1548
2041
  createdAt: Date.now(),
1549
2042
  });
1550
2043
  this.emitConversation(conversation);
2044
+ const promptAsNewTurn = () => client.prompt({
2045
+ sessionId: conversation.agentSessionId,
2046
+ content: payload.contentBlocks,
2047
+ clientMessageId: payload.clientMessageId,
2048
+ model: effectiveModel,
2049
+ reasoningEffort: payload.reasoningEffort,
2050
+ permissionMode: payload.permissionMode,
2051
+ collaborationMode: payload.collaborationMode ?? conversation.collaborationMode,
2052
+ cwd: conversation.cwd,
2053
+ });
1551
2054
  try {
1552
- const result = await client.prompt({
1553
- sessionId: conversation.agentSessionId,
1554
- content: payload.contentBlocks,
1555
- clientMessageId: payload.clientMessageId,
1556
- model: payload.model,
1557
- reasoningEffort: payload.reasoningEffort,
1558
- permissionMode: payload.permissionMode,
1559
- collaborationMode: payload.collaborationMode ?? conversation.collaborationMode,
1560
- cwd: conversation.cwd,
1561
- });
2055
+ const steer = client.steer;
2056
+ let result;
2057
+ if (shouldSteer && typeof steer === "function") {
2058
+ try {
2059
+ result = await steer.call(client, {
2060
+ sessionId: conversation.agentSessionId,
2061
+ turnId: activeTurnId,
2062
+ content: payload.contentBlocks,
2063
+ });
2064
+ }
2065
+ catch (steerError) {
2066
+ this.forgetCurrentTurn(conversation.id, activeTurnId);
2067
+ const userItem = this.findItem(conversation.id, payload.clientMessageId);
2068
+ if (userItem) {
2069
+ this.upsertItem(conversation.id, {
2070
+ ...userItem,
2071
+ metadata: {
2072
+ ...(userItem.metadata ?? {}),
2073
+ delivery: "new_turn",
2074
+ fallbackFrom: "steer",
2075
+ failedTargetTurnId: activeTurnId,
2076
+ },
2077
+ updatedAt: Date.now(),
2078
+ });
2079
+ }
2080
+ this.addItem(conversation.id, {
2081
+ id: id("status"),
2082
+ conversationId: conversation.id,
2083
+ type: "status",
2084
+ role: "system",
2085
+ text: "Codex 未接收运行中追加输入,已改为发送新消息。",
2086
+ status: "running",
2087
+ createdAt: Date.now(),
2088
+ });
2089
+ result = await promptAsNewTurn();
2090
+ }
2091
+ }
2092
+ else {
2093
+ result = await promptAsNewTurn();
2094
+ }
1562
2095
  const nextAgentSessionId = this.extractSessionId(result);
1563
2096
  if (nextAgentSessionId && nextAgentSessionId !== conversation.agentSessionId) {
1564
2097
  if (conversation.agentSessionId)
@@ -1763,6 +2296,9 @@ export class AgentWorkspaceProxy {
1763
2296
  }
1764
2297
  }
1765
2298
  handleRequest(method, params) {
2299
+ if (method === "claude/askUserQuestion") {
2300
+ return this.handleStructuredInput({ ...(asRecord(params) ?? {}), source: method }, true);
2301
+ }
1766
2302
  if (method === "item/tool/requestUserInput" || method === "tool/requestUserInput") {
1767
2303
  return this.handleStructuredInput(params, true);
1768
2304
  }
@@ -1808,13 +2344,30 @@ export class AgentWorkspaceProxy {
1808
2344
  }
1809
2345
  if (method.startsWith("account/") ||
1810
2346
  method.startsWith("mcpServer/startupStatus/") ||
1811
- method === "thread/status/changed" ||
1812
2347
  method === "thread/tokenUsage/updated" ||
1813
2348
  method === "serverRequest/resolved" ||
1814
2349
  method === "mcpServer/oauthLogin/completed") {
1815
2350
  return;
1816
2351
  }
1817
2352
  const conversationId = this.conversationIdFromParams(params) ?? this.fallbackConversationId();
2353
+ if (method === "thread/status/changed") {
2354
+ const status = agentStatusFromThreadStatus(params);
2355
+ if (conversationId && status) {
2356
+ const raw = asRecord(params);
2357
+ const turnId = this.extractTurnId(raw);
2358
+ if (turnId && (status === "running" || status === "waiting_permission")) {
2359
+ this.rememberTurnConversationId(conversationId, turnId);
2360
+ }
2361
+ if (status === "idle" || status === "error") {
2362
+ this.forgetCurrentTurn(conversationId, turnId);
2363
+ }
2364
+ const message = status === "error"
2365
+ ? firstString(raw, ["message", "error", "reason"])
2366
+ : undefined;
2367
+ this.updateConversationStatus(conversationId, status, message);
2368
+ }
2369
+ return;
2370
+ }
1818
2371
  if (method === "item/tool/requestUserInput" || method === "tool/requestUserInput") {
1819
2372
  this.handleStructuredInput(params);
1820
2373
  return;
@@ -2324,9 +2877,7 @@ export class AgentWorkspaceProxy {
2324
2877
  const raw = asRecord(params) ?? {};
2325
2878
  const conversationId = this.conversationIdFromParams(raw) ?? this.fallbackConversationId();
2326
2879
  const source = firstString(raw, ["method", "source", "requestMethod"]);
2327
- const formatResponse = source === "mcpServer/elicitation/request"
2328
- ? formatMcpElicitationResponse
2329
- : formatStructuredInputResponse;
2880
+ const formatResponse = (answers, input) => formatStructuredInputResponseForSource(source, answers, input);
2330
2881
  if (!conversationId)
2331
2882
  return waitForResponse ? Promise.resolve(formatResponse({})) : undefined;
2332
2883
  const structuredInput = decodeStructuredInput(raw);
@@ -2353,13 +2904,13 @@ export class AgentWorkspaceProxy {
2353
2904
  const timer = setTimeout(() => {
2354
2905
  this.pendingStructuredInputs.delete(structuredInput.requestId);
2355
2906
  this.structuredInputWaiters.delete(structuredInput.requestId);
2356
- resolve(formatResponse({}));
2907
+ resolve(formatResponse({}, structuredInput));
2357
2908
  this.markStructuredInput(conversationId, structuredInput.requestId, {
2358
2909
  inputPending: false,
2359
2910
  inputError: "等待用户输入超时",
2360
2911
  });
2361
2912
  }, PERMISSION_TIMEOUT_MS);
2362
- this.structuredInputWaiters.set(structuredInput.requestId, { resolve, timer, source });
2913
+ this.structuredInputWaiters.set(structuredInput.requestId, { resolve, timer, source, input: structuredInput });
2363
2914
  });
2364
2915
  }
2365
2916
  toolCallFromItem(item, fallbackStatus) {
@@ -2409,6 +2960,12 @@ export class AgentWorkspaceProxy {
2409
2960
  conversationId,
2410
2961
  type: "permission",
2411
2962
  permission,
2963
+ metadata: {
2964
+ protocol: "v2",
2965
+ permissionLive: true,
2966
+ permissionExpired: false,
2967
+ permissionPending: false,
2968
+ },
2412
2969
  createdAt: Date.now(),
2413
2970
  updatedAt: Date.now(),
2414
2971
  };
@@ -2433,9 +2990,13 @@ export class AgentWorkspaceProxy {
2433
2990
  }
2434
2991
  respondPermission(payload) {
2435
2992
  const permission = this.pendingPermissions.get(payload.requestId);
2993
+ const waiter = this.permissionWaiters.get(payload.requestId);
2994
+ const existingItem = this.findItem(payload.conversationId, `permission:${payload.requestId}`);
2995
+ const alreadyResolved = Boolean(existingItem?.metadata?.permissionOutcome);
2996
+ if (!permission && !waiter && alreadyResolved)
2997
+ return;
2436
2998
  this.pendingPermissions.delete(payload.requestId);
2437
2999
  const selectedOptionId = payload.optionId ?? selectPermissionOption(permission, payload.outcome);
2438
- const waiter = this.permissionWaiters.get(payload.requestId);
2439
3000
  if (waiter) {
2440
3001
  clearTimeout(waiter.timer);
2441
3002
  this.permissionWaiters.delete(payload.requestId);
@@ -2458,19 +3019,22 @@ export class AgentWorkspaceProxy {
2458
3019
  permissionError: undefined,
2459
3020
  permissionPending: false,
2460
3021
  });
2461
- this.updateConversationStatus(payload.conversationId, "running");
3022
+ if (permission || waiter || existingItem) {
3023
+ this.updateConversationStatus(payload.conversationId, "running");
3024
+ }
2462
3025
  }
2463
3026
  respondStructuredInput(payload) {
2464
3027
  const pending = this.pendingStructuredInputs.get(payload.requestId);
2465
- this.pendingStructuredInputs.delete(payload.requestId);
2466
3028
  const waiter = this.structuredInputWaiters.get(payload.requestId);
3029
+ const existingItem = this.findItem(payload.conversationId, `input:${payload.requestId}`);
3030
+ const alreadySubmitted = existingItem?.metadata?.inputSubmitted === true;
3031
+ if (!pending && !waiter && alreadySubmitted)
3032
+ return;
3033
+ this.pendingStructuredInputs.delete(payload.requestId);
2467
3034
  if (waiter) {
2468
3035
  clearTimeout(waiter.timer);
2469
3036
  this.structuredInputWaiters.delete(payload.requestId);
2470
- const formatResponse = waiter.source === "mcpServer/elicitation/request"
2471
- ? formatMcpElicitationResponse
2472
- : formatStructuredInputResponse;
2473
- waiter.resolve(formatResponse(payload.answers));
3037
+ waiter.resolve(formatStructuredInputResponseForSource(waiter.source, payload.answers, waiter.input));
2474
3038
  }
2475
3039
  this.markStructuredInput(payload.conversationId, payload.requestId, {
2476
3040
  inputPending: false,
@@ -2479,7 +3043,9 @@ export class AgentWorkspaceProxy {
2479
3043
  inputError: undefined,
2480
3044
  answers: payload.answers,
2481
3045
  });
2482
- this.updateConversationStatus(pending?.conversationId ?? payload.conversationId, "running");
3046
+ if (pending || waiter || existingItem) {
3047
+ this.updateConversationStatus(pending?.conversationId ?? payload.conversationId, "running");
3048
+ }
2483
3049
  }
2484
3050
  markPermission(conversationId, requestId, metadata) {
2485
3051
  const item = this.findItem(conversationId, `permission:${requestId}`);
@@ -2654,14 +3220,14 @@ export class AgentWorkspaceProxy {
2654
3220
  this.input.send(createEnvelope({
2655
3221
  type: "agent.v2.event",
2656
3222
  sessionId: this.input.sessionId,
2657
- payload: { conversationId, conversation, item },
3223
+ payload: { conversationId, conversation: conversation ? { ...conversation } : undefined, item },
2658
3224
  }));
2659
3225
  }
2660
3226
  emitConversation(conversation) {
2661
3227
  this.input.send(createEnvelope({
2662
3228
  type: "agent.v2.event",
2663
3229
  sessionId: this.input.sessionId,
2664
- payload: { conversationId: conversation.id, conversation },
3230
+ payload: { conversationId: conversation.id, conversation: { ...conversation } },
2665
3231
  }));
2666
3232
  }
2667
3233
  emitStatus(conversationId, status, text) {
@@ -2681,6 +3247,16 @@ export class AgentWorkspaceProxy {
2681
3247
  payload: input,
2682
3248
  }));
2683
3249
  }
3250
+ rejectAgentAction(conversation, message, status = "idle") {
3251
+ this.updateConversationStatus(conversation.id, status, message);
3252
+ this.addItem(conversation.id, {
3253
+ id: id("error"),
3254
+ conversationId: conversation.id,
3255
+ type: "error",
3256
+ error: message,
3257
+ createdAt: Date.now(),
3258
+ });
3259
+ }
2684
3260
  updateConversationPreview(conversationId, text, status) {
2685
3261
  const conversation = this.conversations.get(conversationId);
2686
3262
  if (!conversation)
@@ -2802,13 +3378,23 @@ export class AgentWorkspaceProxy {
2802
3378
  handleProviderExit(provider, message) {
2803
3379
  this.clients.delete(provider);
2804
3380
  this.agentProtocols.delete(provider);
2805
- this.cancelPendingPermissions();
3381
+ this.providerErrors.set(provider, message);
3382
+ for (const conversation of this.conversations.values()) {
3383
+ if (conversation.provider === provider && conversation.status === "waiting_permission") {
3384
+ this.cancelPendingPermissions(conversation.id, false);
3385
+ }
3386
+ }
2806
3387
  for (const conversation of this.conversations.values()) {
2807
3388
  if (conversation.provider !== provider)
2808
3389
  continue;
3390
+ if (conversation.status !== "running" && conversation.status !== "waiting_permission") {
3391
+ this.emitConversation(conversation);
3392
+ continue;
3393
+ }
2809
3394
  conversation.status = "error";
2810
3395
  conversation.lastMessagePreview = message;
2811
3396
  conversation.lastActivityAt = Date.now();
3397
+ this.forgetCurrentTurn(conversation.id);
2812
3398
  this.emitConversation(conversation);
2813
3399
  this.addItem(conversation.id, {
2814
3400
  id: id("error"),
@@ -2820,18 +3406,23 @@ export class AgentWorkspaceProxy {
2820
3406
  }
2821
3407
  this.sendCapabilities();
2822
3408
  }
2823
- cancelPendingPermissions(conversationId) {
3409
+ cancelPendingPermissions(conversationId, updateStatus = true) {
2824
3410
  for (const [requestId, waiter] of this.permissionWaiters) {
3411
+ if (conversationId && this.itemConversationIds.get(requestId) !== conversationId)
3412
+ continue;
2825
3413
  clearTimeout(waiter.timer);
2826
3414
  waiter.resolve(formatPermissionResponse(this.permissionSources.get(requestId), "cancelled", "cancelled"));
2827
3415
  this.pendingPermissions.delete(requestId);
2828
3416
  this.permissionSources.delete(requestId);
2829
3417
  }
2830
- this.permissionWaiters.clear();
3418
+ if (!conversationId)
3419
+ this.permissionWaiters.clear();
2831
3420
  for (const [requestId, waiter] of this.structuredInputWaiters) {
2832
- clearTimeout(waiter.timer);
2833
- waiter.resolve(formatStructuredInputResponse({}));
2834
3421
  const pending = this.pendingStructuredInputs.get(requestId);
3422
+ if (conversationId && pending?.conversationId !== conversationId)
3423
+ continue;
3424
+ clearTimeout(waiter.timer);
3425
+ waiter.resolve(formatStructuredInputResponseForSource(waiter.source, {}, waiter.input));
2835
3426
  if (pending) {
2836
3427
  this.markStructuredInput(pending.conversationId, requestId, {
2837
3428
  inputPending: false,
@@ -2839,9 +3430,11 @@ export class AgentWorkspaceProxy {
2839
3430
  });
2840
3431
  }
2841
3432
  this.pendingStructuredInputs.delete(requestId);
3433
+ this.structuredInputWaiters.delete(requestId);
2842
3434
  }
2843
- this.structuredInputWaiters.clear();
2844
- if (conversationId)
3435
+ if (!conversationId)
3436
+ this.structuredInputWaiters.clear();
3437
+ if (conversationId && updateStatus)
2845
3438
  this.updateConversationStatus(conversationId, "idle");
2846
3439
  }
2847
3440
  extractSessionId(value) {
@@ -2919,6 +3512,34 @@ function formatStructuredInputResponse(answers) {
2919
3512
  ])),
2920
3513
  };
2921
3514
  }
3515
+ function formatClaudeAskUserQuestionResponse(answers, input) {
3516
+ const questions = input?.questions ?? [];
3517
+ const byQuestionText = {};
3518
+ for (const question of questions) {
3519
+ const values = answers[question.id] ?? answers[question.question] ?? [];
3520
+ const cleaned = values.map((value) => value.trim()).filter(Boolean);
3521
+ if (cleaned.length === 0)
3522
+ continue;
3523
+ byQuestionText[question.question] = question.selectionLimit && question.selectionLimit > 1
3524
+ ? cleaned
3525
+ : cleaned[0] ?? "";
3526
+ }
3527
+ return {
3528
+ behavior: "allow",
3529
+ updatedInput: {
3530
+ questions: questions.map((question) => ({
3531
+ question: question.question,
3532
+ header: question.header,
3533
+ options: question.options?.map((option) => ({
3534
+ label: option.label,
3535
+ description: option.description,
3536
+ })) ?? [],
3537
+ multiSelect: Boolean(question.selectionLimit && question.selectionLimit > 1),
3538
+ })),
3539
+ answers: byQuestionText,
3540
+ },
3541
+ };
3542
+ }
2922
3543
  function formatMcpElicitationResponse(answers) {
2923
3544
  const content = Object.fromEntries(Object.entries(answers).map(([questionId, values]) => [
2924
3545
  questionId,
@@ -2930,6 +3551,15 @@ function formatMcpElicitationResponse(answers) {
2930
3551
  _meta: { source: "linkshell" },
2931
3552
  };
2932
3553
  }
3554
+ function formatStructuredInputResponseForSource(source, answers, input) {
3555
+ if (source === "mcpServer/elicitation/request") {
3556
+ return formatMcpElicitationResponse(answers);
3557
+ }
3558
+ if (source === "claude/askUserQuestion") {
3559
+ return formatClaudeAskUserQuestionResponse(answers, input);
3560
+ }
3561
+ return formatStructuredInputResponse(answers);
3562
+ }
2933
3563
  function formatPermissionResponse(source, outcome, optionId) {
2934
3564
  if (source === "item/commandExecution/requestApproval" || source === "item/fileChange/requestApproval") {
2935
3565
  return { decision: outcome === "allow" ? "accept" : outcome === "deny" ? "decline" : "cancel" };