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.
- package/dist/cli/src/index.js +1 -1
- package/dist/cli/src/index.js.map +1 -1
- package/dist/cli/src/runtime/acp/acp-client.d.ts +16 -0
- package/dist/cli/src/runtime/acp/acp-client.js +40 -2
- package/dist/cli/src/runtime/acp/acp-client.js.map +1 -1
- package/dist/cli/src/runtime/acp/agent-workspace-git.d.ts +63 -0
- package/dist/cli/src/runtime/acp/agent-workspace-git.js +297 -0
- package/dist/cli/src/runtime/acp/agent-workspace-git.js.map +1 -0
- package/dist/cli/src/runtime/acp/agent-workspace.d.ts +6 -0
- package/dist/cli/src/runtime/acp/agent-workspace.js +728 -98
- package/dist/cli/src/runtime/acp/agent-workspace.js.map +1 -1
- package/dist/cli/src/runtime/acp/claude-sdk-client.d.ts +17 -1
- package/dist/cli/src/runtime/acp/claude-sdk-client.js +331 -19
- package/dist/cli/src/runtime/acp/claude-sdk-client.js.map +1 -1
- package/dist/cli/src/runtime/acp/claude-stream-json-client.js +69 -14
- package/dist/cli/src/runtime/acp/claude-stream-json-client.js.map +1 -1
- package/dist/cli/src/runtime/acp/json-rpc.d.ts +1 -0
- package/dist/cli/src/runtime/acp/json-rpc.js +6 -0
- package/dist/cli/src/runtime/acp/json-rpc.js.map +1 -1
- package/dist/cli/src/runtime/acp-relay.d.ts +23 -0
- package/dist/cli/src/runtime/acp-relay.js +73 -0
- package/dist/cli/src/runtime/acp-relay.js.map +1 -0
- package/dist/cli/src/runtime/bridge-session.d.ts +5 -0
- package/dist/cli/src/runtime/bridge-session.js +104 -57
- package/dist/cli/src/runtime/bridge-session.js.map +1 -1
- package/dist/cli/tsconfig.tsbuildinfo +1 -1
- package/dist/shared-protocol/src/index.d.ts +2505 -2479
- package/dist/shared-protocol/src/index.js +46 -0
- package/dist/shared-protocol/src/index.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +1 -1
- package/src/runtime/acp/acp-client.ts +53 -2
- package/src/runtime/acp/agent-workspace.ts +784 -89
- package/src/runtime/acp/claude-sdk-client.ts +352 -31
- package/src/runtime/acp/claude-stream-json-client.ts +66 -16
- package/src/runtime/acp/json-rpc.ts +5 -0
- package/src/runtime/bridge-session.ts +114 -61
- package/dist/cli/src/runtime/acp/claude-sessions.d.ts +0 -57
- package/dist/cli/src/runtime/acp/claude-sessions.js +0 -506
- package/dist/cli/src/runtime/acp/claude-sessions.js.map +0 -1
- package/dist/cli/src/runtime/acp/codex-rpc-bridge.d.ts +0 -34
- package/dist/cli/src/runtime/acp/codex-rpc-bridge.js +0 -204
- package/dist/cli/src/runtime/acp/codex-rpc-bridge.js.map +0 -1
- package/dist/cli/src/runtime/acp/codex-sessions.d.ts +0 -60
- package/dist/cli/src/runtime/acp/codex-sessions.js +0 -731
- 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:
|
|
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 = "
|
|
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?.
|
|
941
|
-
Array.isArray(raw?.
|
|
942
|
-
Array.isArray(raw?.
|
|
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, ["
|
|
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
|
-
|
|
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
|
-
|
|
964
|
-
|
|
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
|
|
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?.
|
|
987
|
-
Array.isArray(raw?.
|
|
988
|
-
Array.isArray(raw?.
|
|
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 =
|
|
1401
|
+
const cancelClient = this.clientForProvider(conversation.provider);
|
|
1082
1402
|
cancelClient?.cancel({
|
|
1083
|
-
sessionId: conversation
|
|
1084
|
-
turnId
|
|
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 ??
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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: {
|
|
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: {
|
|
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.
|
|
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.
|
|
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
|
|
1493
|
-
|
|
1494
|
-
|
|
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:
|
|
1992
|
+
error: `${providerLabel(conversation.provider)} 当前不支持运行中追加输入。请等待本轮结束,或先停止后再发送。`,
|
|
1500
1993
|
createdAt: Date.now(),
|
|
1501
1994
|
});
|
|
1502
1995
|
return;
|
|
1503
1996
|
}
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
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 (
|
|
2008
|
+
if (effectiveModel && effectiveModel !== priorSettings.model) {
|
|
1517
2009
|
const runtimeCapabilities = this.providerCapabilities.get(conversation.provider);
|
|
1518
|
-
const label = runtimeCapabilities?.models?.find((m) => m.id ===
|
|
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)} 下次回复将使用 ${
|
|
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
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
2844
|
-
|
|
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" };
|