linkshell-cli 0.5.1 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/cli/src/index.js +1 -1
  2. package/dist/cli/src/index.js.map +1 -1
  3. package/dist/cli/src/runtime/acp/acp-client.d.ts +16 -0
  4. package/dist/cli/src/runtime/acp/acp-client.js +40 -2
  5. package/dist/cli/src/runtime/acp/acp-client.js.map +1 -1
  6. package/dist/cli/src/runtime/acp/agent-workspace-git.d.ts +63 -0
  7. package/dist/cli/src/runtime/acp/agent-workspace-git.js +297 -0
  8. package/dist/cli/src/runtime/acp/agent-workspace-git.js.map +1 -0
  9. package/dist/cli/src/runtime/acp/agent-workspace.d.ts +6 -0
  10. package/dist/cli/src/runtime/acp/agent-workspace.js +690 -81
  11. package/dist/cli/src/runtime/acp/agent-workspace.js.map +1 -1
  12. package/dist/cli/src/runtime/acp/claude-sdk-client.d.ts +17 -1
  13. package/dist/cli/src/runtime/acp/claude-sdk-client.js +331 -19
  14. package/dist/cli/src/runtime/acp/claude-sdk-client.js.map +1 -1
  15. package/dist/cli/src/runtime/acp/claude-stream-json-client.js +69 -14
  16. package/dist/cli/src/runtime/acp/claude-stream-json-client.js.map +1 -1
  17. package/dist/cli/src/runtime/acp/json-rpc.d.ts +1 -0
  18. package/dist/cli/src/runtime/acp/json-rpc.js +6 -0
  19. package/dist/cli/src/runtime/acp/json-rpc.js.map +1 -1
  20. package/dist/cli/src/runtime/acp-relay.d.ts +23 -0
  21. package/dist/cli/src/runtime/acp-relay.js +73 -0
  22. package/dist/cli/src/runtime/acp-relay.js.map +1 -0
  23. package/dist/cli/src/runtime/bridge-session.d.ts +5 -0
  24. package/dist/cli/src/runtime/bridge-session.js +104 -57
  25. package/dist/cli/src/runtime/bridge-session.js.map +1 -1
  26. package/dist/cli/tsconfig.tsbuildinfo +1 -1
  27. package/dist/shared-protocol/src/index.d.ts +2505 -2479
  28. package/dist/shared-protocol/src/index.js +46 -0
  29. package/dist/shared-protocol/src/index.js.map +1 -1
  30. package/package.json +3 -3
  31. package/src/index.ts +1 -1
  32. package/src/runtime/acp/acp-client.ts +53 -2
  33. package/src/runtime/acp/agent-workspace.ts +752 -76
  34. package/src/runtime/acp/claude-sdk-client.ts +352 -31
  35. package/src/runtime/acp/claude-stream-json-client.ts +66 -16
  36. package/src/runtime/acp/json-rpc.ts +5 -0
  37. package/src/runtime/bridge-session.ts +114 -61
  38. package/dist/cli/src/runtime/acp/claude-sessions.d.ts +0 -57
  39. package/dist/cli/src/runtime/acp/claude-sessions.js +0 -506
  40. package/dist/cli/src/runtime/acp/claude-sessions.js.map +0 -1
  41. package/dist/cli/src/runtime/acp/codex-rpc-bridge.d.ts +0 -34
  42. package/dist/cli/src/runtime/acp/codex-rpc-bridge.js +0 -204
  43. package/dist/cli/src/runtime/acp/codex-rpc-bridge.js.map +0 -1
  44. package/dist/cli/src/runtime/acp/codex-sessions.d.ts +0 -60
  45. package/dist/cli/src/runtime/acp/codex-sessions.js +0 -731
  46. package/dist/cli/src/runtime/acp/codex-sessions.js.map +0 -1
@@ -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
  }
@@ -1004,10 +1041,11 @@ function parseTimestamp(value) {
1004
1041
  function parseRemoteSessions(value) {
1005
1042
  const raw = asRecord(value);
1006
1043
  const sessionsValue = Array.isArray(value) ? value :
1007
- Array.isArray(raw?.threads) ? raw.threads :
1008
- Array.isArray(raw?.sessions) ? raw.sessions :
1009
- Array.isArray(raw?.items) ? raw.items :
1010
- [];
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
+ [];
1011
1049
  const result = [];
1012
1050
  for (const entry of sessionsValue) {
1013
1051
  const session = asRecord(entry);
@@ -1024,7 +1062,7 @@ function parseRemoteSessions(value) {
1024
1062
  result.push({
1025
1063
  id,
1026
1064
  cwd: firstString(source, ["cwd", "workingDirectory", "workspacePath"]),
1027
- title: firstString(source, ["title", "name", "summary"]),
1065
+ title: firstString(source, ["title", "name", "summary", "preview"]),
1028
1066
  model: firstString(source, ["model", "modelId"]),
1029
1067
  createdAt: parseTimestamp(source.createdAt ?? source.created_at),
1030
1068
  lastActivityAt: parseTimestamp(source.lastActivityAt ?? source.updatedAt ?? source.modifiedAt ?? source.lastModified ?? source.updated_at),
@@ -1032,11 +1070,265 @@ function parseRemoteSessions(value) {
1032
1070
  }
1033
1071
  return result;
1034
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
+ }
1035
1326
  export class AgentWorkspaceProxy {
1036
1327
  input;
1037
1328
  clients = new Map();
1038
1329
  agentProtocols = new Map();
1039
1330
  providerCapabilities = new Map();
1331
+ providerErrors = new Map();
1040
1332
  initialized = false;
1041
1333
  status = "unavailable";
1042
1334
  error;
@@ -1098,11 +1390,18 @@ export class AgentWorkspaceProxy {
1098
1390
  case "agent.v2.cancel": {
1099
1391
  const payload = parseTypedPayload("agent.v2.cancel", envelope.payload);
1100
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
+ }
1101
1400
  this.cancelPendingPermissions(payload.conversationId);
1102
- const cancelClient = conversation ? this.clientForProvider(conversation.provider) : undefined;
1401
+ const cancelClient = this.clientForProvider(conversation.provider);
1103
1402
  cancelClient?.cancel({
1104
- sessionId: conversation?.agentSessionId,
1105
- turnId: this.currentTurnIds.get(payload.conversationId),
1403
+ sessionId: conversation.agentSessionId,
1404
+ turnId,
1106
1405
  });
1107
1406
  this.forgetCurrentTurn(payload.conversationId);
1108
1407
  this.updateConversationStatus(payload.conversationId, "idle");
@@ -1133,6 +1432,13 @@ export class AgentWorkspaceProxy {
1133
1432
  protocolForProvider(provider) {
1134
1433
  return this.agentProtocols.get(provider);
1135
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
+ }
1136
1442
  async initialize() {
1137
1443
  if (this.initialized)
1138
1444
  return;
@@ -1197,6 +1503,7 @@ export class AgentWorkspaceProxy {
1197
1503
  try {
1198
1504
  const client = await tryCreateClient(resolved);
1199
1505
  this.clients.set(provider, client);
1506
+ this.providerErrors.delete(provider);
1200
1507
  await this.refreshProviderCapabilities(provider, client, resolved.protocol);
1201
1508
  this.status = "idle";
1202
1509
  this.error = undefined;
@@ -1217,6 +1524,7 @@ export class AgentWorkspaceProxy {
1217
1524
  };
1218
1525
  const client = await tryCreateClient(fallback);
1219
1526
  this.clients.set(provider, client);
1527
+ this.providerErrors.delete(provider);
1220
1528
  await this.refreshProviderCapabilities(provider, client, fallback.protocol);
1221
1529
  this.status = "idle";
1222
1530
  this.error = undefined;
@@ -1232,6 +1540,8 @@ export class AgentWorkspaceProxy {
1232
1540
  if (this.input.verbose) {
1233
1541
  process.stderr.write(`[agent:v2] failed to start ${provider}: ${error instanceof Error ? error.message : String(error)}\n`);
1234
1542
  }
1543
+ this.providerErrors.set(provider, error instanceof Error ? error.message : String(error));
1544
+ this.sendCapabilities();
1235
1545
  return undefined;
1236
1546
  }
1237
1547
  }
@@ -1281,7 +1591,7 @@ export class AgentWorkspaceProxy {
1281
1591
  const agentSessionId = remote.id;
1282
1592
  const existingId = this.conversationByAgentSessionId.get(agentSessionId);
1283
1593
  const now = Date.now();
1284
- const conversationId = existingId ?? `agent:${agentSessionId}`;
1594
+ const conversationId = existingId ?? makeAgentV2RemoteConversationId(provider, agentSessionId);
1285
1595
  const existing = this.conversations.get(conversationId);
1286
1596
  const cwd = remote.cwd ?? existing?.cwd ?? this.input.cwd;
1287
1597
  const conversation = {
@@ -1312,6 +1622,78 @@ export class AgentWorkspaceProxy {
1312
1622
  }
1313
1623
  }
1314
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
+ }
1315
1697
  sendCapabilities() {
1316
1698
  const providers = this.input.availableProviders.map((provider) => {
1317
1699
  const client = this.clients.get(provider);
@@ -1328,7 +1710,7 @@ export class AgentWorkspaceProxy {
1328
1710
  id: provider,
1329
1711
  label: providerLabel(provider),
1330
1712
  enabled,
1331
- reason: enabled ? undefined : `${providerLabel(provider)} 未安装或启动失败`,
1713
+ reason: enabled ? undefined : this.providerErrors.get(provider) ?? `${providerLabel(provider)} 未安装或启动失败`,
1332
1714
  supportsImages,
1333
1715
  supportsPermission,
1334
1716
  supportsPlan: enabled,
@@ -1385,18 +1767,60 @@ export class AgentWorkspaceProxy {
1385
1767
  if (!this.input.availableProviders.includes(provider)) {
1386
1768
  return this.openFailure(payload, `${providerLabel(provider)} 未安装或不可用。`);
1387
1769
  }
1388
- const client = await this.ensureProviderClient(provider);
1389
- if (!client) {
1390
- return this.openFailure(payload, `${providerLabel(provider)} 启动失败。请确认 CLI 已安装并可用。`);
1391
- }
1392
- const cwd = payload.cwd ?? this.input.cwd;
1770
+ let cwd = payload.cwd ?? this.input.cwd;
1393
1771
  let agentSessionId = payload.agentSessionId;
1394
1772
  let existingConversation = (payload.conversationId ? this.conversations.get(payload.conversationId) : undefined) ??
1395
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
+ }
1396
1799
  if (existingConversation && existingConversation.status !== "error" && existingConversation.agentSessionId) {
1397
- 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) {
1398
1804
  existingConversation = this.adoptConversationId(existingConversation.id, payload.conversationId);
1399
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
+ }
1400
1824
  this.activeConversationId = existingConversation.id;
1401
1825
  this.input.send(createEnvelope({
1402
1826
  type: "agent.v2.conversation.opened",
@@ -1404,6 +1828,7 @@ export class AgentWorkspaceProxy {
1404
1828
  payload: {
1405
1829
  conversation: existingConversation,
1406
1830
  snapshot: this.timelines.get(existingConversation.id) ?? [],
1831
+ requestedConversationId: payload.conversationId,
1407
1832
  },
1408
1833
  }));
1409
1834
  return existingConversation;
@@ -1414,7 +1839,7 @@ export class AgentWorkspaceProxy {
1414
1839
  : await client.newSession({ cwd });
1415
1840
  agentSessionId = this.extractSessionId(result) ?? agentSessionId ?? id("agent-session");
1416
1841
  const now = Date.now();
1417
- const conversationId = payload.conversationId ?? `agent:${agentSessionId}`;
1842
+ const conversationId = makeAgentV2RemoteConversationId(provider, agentSessionId);
1418
1843
  const conversation = {
1419
1844
  ...existingConversation,
1420
1845
  id: conversationId,
@@ -1436,10 +1861,15 @@ export class AgentWorkspaceProxy {
1436
1861
  this.conversationByAgentSessionId.set(agentSessionId, conversation.id);
1437
1862
  this.activeConversationId = conversation.id;
1438
1863
  this.timelines.set(conversation.id, this.timelines.get(conversation.id) ?? []);
1864
+ await this.hydrateConversationFromProvider(conversation, result);
1439
1865
  this.input.send(createEnvelope({
1440
1866
  type: "agent.v2.conversation.opened",
1441
1867
  sessionId: this.input.sessionId,
1442
- payload: { conversation, snapshot: this.timelines.get(conversation.id) ?? [] },
1868
+ payload: {
1869
+ conversation,
1870
+ snapshot: this.timelines.get(conversation.id) ?? [],
1871
+ requestedConversationId: payload.conversationId,
1872
+ },
1443
1873
  }));
1444
1874
  return conversation;
1445
1875
  }
@@ -1478,70 +1908,111 @@ export class AgentWorkspaceProxy {
1478
1908
  this.input.send(createEnvelope({
1479
1909
  type: "agent.v2.conversation.opened",
1480
1910
  sessionId: this.input.sessionId,
1481
- payload: { conversation, snapshot: this.timelines.get(conversation.id) ?? [] },
1911
+ payload: {
1912
+ conversation,
1913
+ snapshot: this.timelines.get(conversation.id) ?? [],
1914
+ requestedConversationId: payload.conversationId,
1915
+ },
1482
1916
  }));
1483
1917
  return conversation;
1484
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
+ }
1485
1954
  async sendPrompt(payload) {
1486
1955
  const conversation = this.conversations.get(payload.conversationId) ??
1487
1956
  await this.openConversation({ conversationId: payload.conversationId });
1488
1957
  if (!conversation)
1489
1958
  return;
1490
1959
  if (!conversation.agentSessionId) {
1491
- this.addItem(payload.conversationId, {
1492
- id: id("error"),
1493
- conversationId: payload.conversationId,
1494
- type: "error",
1495
- error: "Agent session 尚未就绪,消息没有发送。请重新打开对话后再试。",
1496
- createdAt: Date.now(),
1497
- });
1960
+ this.rejectAgentAction(conversation, "Agent session 尚未就绪,消息没有发送。请重新打开对话后再试。");
1498
1961
  return;
1499
1962
  }
1500
- const client = this.clientForProvider(conversation.provider);
1963
+ const client = this.clientForProvider(conversation.provider) ?? await this.ensureProviderClient(conversation.provider);
1501
1964
  if (!client) {
1502
- this.addItem(conversation.id, {
1503
- id: id("error"),
1504
- conversationId: conversation.id,
1505
- type: "error",
1506
- error: `${providerLabel(conversation.provider)} 未连接,消息没有发送。`,
1507
- createdAt: Date.now(),
1508
- });
1965
+ this.rejectAgentAction(conversation, this.providerErrors.get(conversation.provider) ?? `${providerLabel(conversation.provider)} 未连接,消息没有发送。`, "error");
1509
1966
  return;
1510
1967
  }
1511
1968
  const protocol = this.protocolForProvider(conversation.provider);
1512
1969
  if (payload.contentBlocks.some((block) => block.type === "image") && !protocolSupportsImages(protocol)) {
1513
- conversation.status = "idle";
1514
- conversation.lastActivityAt = Date.now();
1515
- 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") {
1516
1988
  this.addItem(conversation.id, {
1517
1989
  id: id("error"),
1518
1990
  conversationId: conversation.id,
1519
1991
  type: "error",
1520
- error: "当前 Agent provider 暂不支持图片输入,请升级 CLI 或切换到 Codex。",
1992
+ error: `${providerLabel(conversation.provider)} 当前不支持运行中追加输入。请等待本轮结束,或先停止后再发送。`,
1521
1993
  createdAt: Date.now(),
1522
1994
  });
1523
1995
  return;
1524
1996
  }
1525
- const priorSettings = {
1526
- model: conversation.model,
1527
- reasoningEffort: conversation.reasoningEffort,
1528
- permissionMode: conversation.permissionMode,
1529
- };
1530
- 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;
1531
2002
  conversation.reasoningEffort = payload.reasoningEffort ?? conversation.reasoningEffort;
1532
2003
  conversation.permissionMode = payload.permissionMode ?? conversation.permissionMode;
1533
2004
  conversation.collaborationMode = payload.collaborationMode ?? conversation.collaborationMode;
1534
2005
  conversation.status = "running";
1535
2006
  conversation.lastActivityAt = Date.now();
1536
2007
  this.activeConversationId = conversation.id;
1537
- if (payload.model && payload.model !== priorSettings.model) {
2008
+ if (effectiveModel && effectiveModel !== priorSettings.model) {
1538
2009
  const runtimeCapabilities = this.providerCapabilities.get(conversation.provider);
1539
- 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;
1540
2011
  this.emitNotice({
1541
2012
  conversationId: conversation.id,
1542
2013
  kind: "model_changed",
1543
2014
  title: `已切换模型 · ${label}`,
1544
- detail: `${providerLabel(conversation.provider)} 下次回复将使用 ${payload.model}`,
2015
+ detail: `${providerLabel(conversation.provider)} 下次回复将使用 ${effectiveModel}`,
1545
2016
  });
1546
2017
  }
1547
2018
  if (payload.reasoningEffort && payload.reasoningEffort !== priorSettings.reasoningEffort) {
@@ -1566,20 +2037,61 @@ export class AgentWorkspaceProxy {
1566
2037
  role: "user",
1567
2038
  content: payload.contentBlocks,
1568
2039
  text: userText,
2040
+ metadata: shouldSteer ? { delivery: "steer", targetTurnId: activeTurnId } : undefined,
1569
2041
  createdAt: Date.now(),
1570
2042
  });
1571
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
+ });
1572
2054
  try {
1573
- const result = await client.prompt({
1574
- sessionId: conversation.agentSessionId,
1575
- content: payload.contentBlocks,
1576
- clientMessageId: payload.clientMessageId,
1577
- model: payload.model,
1578
- reasoningEffort: payload.reasoningEffort,
1579
- permissionMode: payload.permissionMode,
1580
- collaborationMode: payload.collaborationMode ?? conversation.collaborationMode,
1581
- cwd: conversation.cwd,
1582
- });
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
+ }
1583
2095
  const nextAgentSessionId = this.extractSessionId(result);
1584
2096
  if (nextAgentSessionId && nextAgentSessionId !== conversation.agentSessionId) {
1585
2097
  if (conversation.agentSessionId)
@@ -1784,6 +2296,9 @@ export class AgentWorkspaceProxy {
1784
2296
  }
1785
2297
  }
1786
2298
  handleRequest(method, params) {
2299
+ if (method === "claude/askUserQuestion") {
2300
+ return this.handleStructuredInput({ ...(asRecord(params) ?? {}), source: method }, true);
2301
+ }
1787
2302
  if (method === "item/tool/requestUserInput" || method === "tool/requestUserInput") {
1788
2303
  return this.handleStructuredInput(params, true);
1789
2304
  }
@@ -1829,13 +2344,30 @@ export class AgentWorkspaceProxy {
1829
2344
  }
1830
2345
  if (method.startsWith("account/") ||
1831
2346
  method.startsWith("mcpServer/startupStatus/") ||
1832
- method === "thread/status/changed" ||
1833
2347
  method === "thread/tokenUsage/updated" ||
1834
2348
  method === "serverRequest/resolved" ||
1835
2349
  method === "mcpServer/oauthLogin/completed") {
1836
2350
  return;
1837
2351
  }
1838
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
+ }
1839
2371
  if (method === "item/tool/requestUserInput" || method === "tool/requestUserInput") {
1840
2372
  this.handleStructuredInput(params);
1841
2373
  return;
@@ -2345,9 +2877,7 @@ export class AgentWorkspaceProxy {
2345
2877
  const raw = asRecord(params) ?? {};
2346
2878
  const conversationId = this.conversationIdFromParams(raw) ?? this.fallbackConversationId();
2347
2879
  const source = firstString(raw, ["method", "source", "requestMethod"]);
2348
- const formatResponse = source === "mcpServer/elicitation/request"
2349
- ? formatMcpElicitationResponse
2350
- : formatStructuredInputResponse;
2880
+ const formatResponse = (answers, input) => formatStructuredInputResponseForSource(source, answers, input);
2351
2881
  if (!conversationId)
2352
2882
  return waitForResponse ? Promise.resolve(formatResponse({})) : undefined;
2353
2883
  const structuredInput = decodeStructuredInput(raw);
@@ -2374,13 +2904,13 @@ export class AgentWorkspaceProxy {
2374
2904
  const timer = setTimeout(() => {
2375
2905
  this.pendingStructuredInputs.delete(structuredInput.requestId);
2376
2906
  this.structuredInputWaiters.delete(structuredInput.requestId);
2377
- resolve(formatResponse({}));
2907
+ resolve(formatResponse({}, structuredInput));
2378
2908
  this.markStructuredInput(conversationId, structuredInput.requestId, {
2379
2909
  inputPending: false,
2380
2910
  inputError: "等待用户输入超时",
2381
2911
  });
2382
2912
  }, PERMISSION_TIMEOUT_MS);
2383
- this.structuredInputWaiters.set(structuredInput.requestId, { resolve, timer, source });
2913
+ this.structuredInputWaiters.set(structuredInput.requestId, { resolve, timer, source, input: structuredInput });
2384
2914
  });
2385
2915
  }
2386
2916
  toolCallFromItem(item, fallbackStatus) {
@@ -2430,6 +2960,12 @@ export class AgentWorkspaceProxy {
2430
2960
  conversationId,
2431
2961
  type: "permission",
2432
2962
  permission,
2963
+ metadata: {
2964
+ protocol: "v2",
2965
+ permissionLive: true,
2966
+ permissionExpired: false,
2967
+ permissionPending: false,
2968
+ },
2433
2969
  createdAt: Date.now(),
2434
2970
  updatedAt: Date.now(),
2435
2971
  };
@@ -2454,9 +2990,13 @@ export class AgentWorkspaceProxy {
2454
2990
  }
2455
2991
  respondPermission(payload) {
2456
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;
2457
2998
  this.pendingPermissions.delete(payload.requestId);
2458
2999
  const selectedOptionId = payload.optionId ?? selectPermissionOption(permission, payload.outcome);
2459
- const waiter = this.permissionWaiters.get(payload.requestId);
2460
3000
  if (waiter) {
2461
3001
  clearTimeout(waiter.timer);
2462
3002
  this.permissionWaiters.delete(payload.requestId);
@@ -2479,19 +3019,22 @@ export class AgentWorkspaceProxy {
2479
3019
  permissionError: undefined,
2480
3020
  permissionPending: false,
2481
3021
  });
2482
- this.updateConversationStatus(payload.conversationId, "running");
3022
+ if (permission || waiter || existingItem) {
3023
+ this.updateConversationStatus(payload.conversationId, "running");
3024
+ }
2483
3025
  }
2484
3026
  respondStructuredInput(payload) {
2485
3027
  const pending = this.pendingStructuredInputs.get(payload.requestId);
2486
- this.pendingStructuredInputs.delete(payload.requestId);
2487
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);
2488
3034
  if (waiter) {
2489
3035
  clearTimeout(waiter.timer);
2490
3036
  this.structuredInputWaiters.delete(payload.requestId);
2491
- const formatResponse = waiter.source === "mcpServer/elicitation/request"
2492
- ? formatMcpElicitationResponse
2493
- : formatStructuredInputResponse;
2494
- waiter.resolve(formatResponse(payload.answers));
3037
+ waiter.resolve(formatStructuredInputResponseForSource(waiter.source, payload.answers, waiter.input));
2495
3038
  }
2496
3039
  this.markStructuredInput(payload.conversationId, payload.requestId, {
2497
3040
  inputPending: false,
@@ -2500,7 +3043,9 @@ export class AgentWorkspaceProxy {
2500
3043
  inputError: undefined,
2501
3044
  answers: payload.answers,
2502
3045
  });
2503
- this.updateConversationStatus(pending?.conversationId ?? payload.conversationId, "running");
3046
+ if (pending || waiter || existingItem) {
3047
+ this.updateConversationStatus(pending?.conversationId ?? payload.conversationId, "running");
3048
+ }
2504
3049
  }
2505
3050
  markPermission(conversationId, requestId, metadata) {
2506
3051
  const item = this.findItem(conversationId, `permission:${requestId}`);
@@ -2675,14 +3220,14 @@ export class AgentWorkspaceProxy {
2675
3220
  this.input.send(createEnvelope({
2676
3221
  type: "agent.v2.event",
2677
3222
  sessionId: this.input.sessionId,
2678
- payload: { conversationId, conversation, item },
3223
+ payload: { conversationId, conversation: conversation ? { ...conversation } : undefined, item },
2679
3224
  }));
2680
3225
  }
2681
3226
  emitConversation(conversation) {
2682
3227
  this.input.send(createEnvelope({
2683
3228
  type: "agent.v2.event",
2684
3229
  sessionId: this.input.sessionId,
2685
- payload: { conversationId: conversation.id, conversation },
3230
+ payload: { conversationId: conversation.id, conversation: { ...conversation } },
2686
3231
  }));
2687
3232
  }
2688
3233
  emitStatus(conversationId, status, text) {
@@ -2702,6 +3247,16 @@ export class AgentWorkspaceProxy {
2702
3247
  payload: input,
2703
3248
  }));
2704
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
+ }
2705
3260
  updateConversationPreview(conversationId, text, status) {
2706
3261
  const conversation = this.conversations.get(conversationId);
2707
3262
  if (!conversation)
@@ -2823,13 +3378,23 @@ export class AgentWorkspaceProxy {
2823
3378
  handleProviderExit(provider, message) {
2824
3379
  this.clients.delete(provider);
2825
3380
  this.agentProtocols.delete(provider);
2826
- 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
+ }
2827
3387
  for (const conversation of this.conversations.values()) {
2828
3388
  if (conversation.provider !== provider)
2829
3389
  continue;
3390
+ if (conversation.status !== "running" && conversation.status !== "waiting_permission") {
3391
+ this.emitConversation(conversation);
3392
+ continue;
3393
+ }
2830
3394
  conversation.status = "error";
2831
3395
  conversation.lastMessagePreview = message;
2832
3396
  conversation.lastActivityAt = Date.now();
3397
+ this.forgetCurrentTurn(conversation.id);
2833
3398
  this.emitConversation(conversation);
2834
3399
  this.addItem(conversation.id, {
2835
3400
  id: id("error"),
@@ -2841,18 +3406,23 @@ export class AgentWorkspaceProxy {
2841
3406
  }
2842
3407
  this.sendCapabilities();
2843
3408
  }
2844
- cancelPendingPermissions(conversationId) {
3409
+ cancelPendingPermissions(conversationId, updateStatus = true) {
2845
3410
  for (const [requestId, waiter] of this.permissionWaiters) {
3411
+ if (conversationId && this.itemConversationIds.get(requestId) !== conversationId)
3412
+ continue;
2846
3413
  clearTimeout(waiter.timer);
2847
3414
  waiter.resolve(formatPermissionResponse(this.permissionSources.get(requestId), "cancelled", "cancelled"));
2848
3415
  this.pendingPermissions.delete(requestId);
2849
3416
  this.permissionSources.delete(requestId);
2850
3417
  }
2851
- this.permissionWaiters.clear();
3418
+ if (!conversationId)
3419
+ this.permissionWaiters.clear();
2852
3420
  for (const [requestId, waiter] of this.structuredInputWaiters) {
2853
- clearTimeout(waiter.timer);
2854
- waiter.resolve(formatStructuredInputResponse({}));
2855
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));
2856
3426
  if (pending) {
2857
3427
  this.markStructuredInput(pending.conversationId, requestId, {
2858
3428
  inputPending: false,
@@ -2860,9 +3430,11 @@ export class AgentWorkspaceProxy {
2860
3430
  });
2861
3431
  }
2862
3432
  this.pendingStructuredInputs.delete(requestId);
3433
+ this.structuredInputWaiters.delete(requestId);
2863
3434
  }
2864
- this.structuredInputWaiters.clear();
2865
- if (conversationId)
3435
+ if (!conversationId)
3436
+ this.structuredInputWaiters.clear();
3437
+ if (conversationId && updateStatus)
2866
3438
  this.updateConversationStatus(conversationId, "idle");
2867
3439
  }
2868
3440
  extractSessionId(value) {
@@ -2940,6 +3512,34 @@ function formatStructuredInputResponse(answers) {
2940
3512
  ])),
2941
3513
  };
2942
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
+ }
2943
3543
  function formatMcpElicitationResponse(answers) {
2944
3544
  const content = Object.fromEntries(Object.entries(answers).map(([questionId, values]) => [
2945
3545
  questionId,
@@ -2951,6 +3551,15 @@ function formatMcpElicitationResponse(answers) {
2951
3551
  _meta: { source: "linkshell" },
2952
3552
  };
2953
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
+ }
2954
3563
  function formatPermissionResponse(source, outcome, optionId) {
2955
3564
  if (source === "item/commandExecution/requestApproval" || source === "item/fileChange/requestApproval") {
2956
3565
  return { decision: outcome === "allow" ? "accept" : outcome === "deny" ? "decline" : "cancel" };