tabminal 3.0.15 → 3.0.17
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/package.json +1 -1
- package/public/app.js +427 -182
- package/src/acp-manager.mjs +717 -163
- package/src/acp-test-agent.mjs +128 -0
- package/src/server.mjs +26 -10
package/src/acp-manager.mjs
CHANGED
|
@@ -746,6 +746,21 @@ export function mergeAgentMessageText(previousText, chunkText) {
|
|
|
746
746
|
const chunk = String(chunkText || '');
|
|
747
747
|
if (!previous) return chunk;
|
|
748
748
|
if (!chunk) return previous;
|
|
749
|
+
if (previous === chunk) return previous;
|
|
750
|
+
if (chunk.startsWith(previous)) {
|
|
751
|
+
return chunk;
|
|
752
|
+
}
|
|
753
|
+
if (previous.startsWith(chunk)) {
|
|
754
|
+
return previous;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
const maxOverlap = Math.min(previous.length, chunk.length, 2048);
|
|
758
|
+
for (let overlap = maxOverlap; overlap >= 2; overlap -= 1) {
|
|
759
|
+
if (previous.slice(-overlap) === chunk.slice(0, overlap)) {
|
|
760
|
+
return `${previous}${chunk.slice(overlap)}`;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
749
764
|
if (/\s$/.test(previous) || /^\s/.test(chunk)) {
|
|
750
765
|
return `${previous}${chunk}`;
|
|
751
766
|
}
|
|
@@ -1008,108 +1023,547 @@ function cloneSerializable(value, fallback) {
|
|
|
1008
1023
|
}
|
|
1009
1024
|
}
|
|
1010
1025
|
|
|
1011
|
-
function
|
|
1012
|
-
|
|
1026
|
+
function parseIsoTimestamp(value) {
|
|
1027
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
1028
|
+
return 0;
|
|
1029
|
+
}
|
|
1030
|
+
const timestamp = Date.parse(value);
|
|
1031
|
+
return Number.isFinite(timestamp) ? timestamp : 0;
|
|
1013
1032
|
}
|
|
1014
1033
|
|
|
1015
|
-
function
|
|
1034
|
+
function getSerializedTabContentScore(tab = {}) {
|
|
1035
|
+
const messages = Array.isArray(tab.messages) ? tab.messages : [];
|
|
1036
|
+
const toolCalls = Array.isArray(tab.toolCalls) ? tab.toolCalls : [];
|
|
1037
|
+
const permissions = Array.isArray(tab.permissions) ? tab.permissions : [];
|
|
1038
|
+
const plan = Array.isArray(tab.plan) ? tab.plan : [];
|
|
1039
|
+
const terminals = Array.isArray(tab.terminals) ? tab.terminals : [];
|
|
1040
|
+
return (
|
|
1041
|
+
messages.length * 10000
|
|
1042
|
+
+ toolCalls.length * 100
|
|
1043
|
+
+ permissions.length * 10
|
|
1044
|
+
+ plan.length
|
|
1045
|
+
+ terminals.length
|
|
1046
|
+
);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
function getSerializedTabActivity(tab = {}) {
|
|
1050
|
+
let maxSyntheticTurn = -1;
|
|
1051
|
+
let maxTimestamp = parseIsoTimestamp(tab.createdAt);
|
|
1052
|
+
let maxOrder = 0;
|
|
1053
|
+
const visit = (item) => {
|
|
1054
|
+
maxTimestamp = Math.max(
|
|
1055
|
+
maxTimestamp,
|
|
1056
|
+
parseIsoTimestamp(item?.createdAt),
|
|
1057
|
+
parseIsoTimestamp(item?.updatedAt)
|
|
1058
|
+
);
|
|
1059
|
+
const order = Number(item?.order || 0);
|
|
1060
|
+
if (Number.isFinite(order) && order > maxOrder) {
|
|
1061
|
+
maxOrder = order;
|
|
1062
|
+
}
|
|
1063
|
+
const syntheticTurnKey = getSyntheticTurnKey(item?.streamKey || '');
|
|
1064
|
+
const turn = syntheticTurnKey ? Number(syntheticTurnKey) : -1;
|
|
1065
|
+
if (Number.isFinite(turn) && turn > maxSyntheticTurn) {
|
|
1066
|
+
maxSyntheticTurn = turn;
|
|
1067
|
+
}
|
|
1068
|
+
};
|
|
1069
|
+
for (const message of Array.isArray(tab.messages) ? tab.messages : []) {
|
|
1070
|
+
visit(message);
|
|
1071
|
+
}
|
|
1072
|
+
for (const toolCall of Array.isArray(tab.toolCalls) ? tab.toolCalls : []) {
|
|
1073
|
+
visit(toolCall);
|
|
1074
|
+
}
|
|
1075
|
+
for (
|
|
1076
|
+
const permission of Array.isArray(tab.permissions) ? tab.permissions : []
|
|
1077
|
+
) {
|
|
1078
|
+
visit(permission);
|
|
1079
|
+
}
|
|
1080
|
+
if (tab.usage && typeof tab.usage === 'object') {
|
|
1081
|
+
visit(tab.usage);
|
|
1082
|
+
}
|
|
1016
1083
|
return {
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
? message.kind
|
|
1022
|
-
: 'message',
|
|
1023
|
-
text: typeof message.text === 'string'
|
|
1024
|
-
? message.text
|
|
1025
|
-
: ''
|
|
1084
|
+
maxSyntheticTurn,
|
|
1085
|
+
maxTimestamp,
|
|
1086
|
+
maxOrder,
|
|
1087
|
+
contentScore: getSerializedTabContentScore(tab)
|
|
1026
1088
|
};
|
|
1027
1089
|
}
|
|
1028
1090
|
|
|
1029
|
-
|
|
1030
|
-
|
|
1091
|
+
function compareSerializedTabActivity(left = {}, right = {}) {
|
|
1092
|
+
const leftActivity = getSerializedTabActivity(left);
|
|
1093
|
+
const rightActivity = getSerializedTabActivity(right);
|
|
1094
|
+
if (leftActivity.maxSyntheticTurn !== rightActivity.maxSyntheticTurn) {
|
|
1095
|
+
return rightActivity.maxSyntheticTurn - leftActivity.maxSyntheticTurn;
|
|
1096
|
+
}
|
|
1097
|
+
if (leftActivity.maxTimestamp !== rightActivity.maxTimestamp) {
|
|
1098
|
+
return rightActivity.maxTimestamp - leftActivity.maxTimestamp;
|
|
1099
|
+
}
|
|
1100
|
+
if (leftActivity.maxOrder !== rightActivity.maxOrder) {
|
|
1101
|
+
return rightActivity.maxOrder - leftActivity.maxOrder;
|
|
1102
|
+
}
|
|
1103
|
+
if (leftActivity.contentScore !== rightActivity.contentScore) {
|
|
1104
|
+
return rightActivity.contentScore - leftActivity.contentScore;
|
|
1105
|
+
}
|
|
1106
|
+
return 0;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
function compareSerializedTabBase(left = {}, right = {}) {
|
|
1110
|
+
const leftLinked = !!String(left.terminalSessionId || '').trim();
|
|
1111
|
+
const rightLinked = !!String(right.terminalSessionId || '').trim();
|
|
1112
|
+
if (leftLinked !== rightLinked) {
|
|
1113
|
+
return Number(rightLinked) - Number(leftLinked);
|
|
1114
|
+
}
|
|
1115
|
+
const leftCreatedAt = parseIsoTimestamp(left.createdAt);
|
|
1116
|
+
const rightCreatedAt = parseIsoTimestamp(right.createdAt);
|
|
1117
|
+
if (leftCreatedAt !== rightCreatedAt) {
|
|
1118
|
+
return rightCreatedAt - leftCreatedAt;
|
|
1119
|
+
}
|
|
1120
|
+
const leftTitleLength = String(left.title || '').trim().length;
|
|
1121
|
+
const rightTitleLength = String(right.title || '').trim().length;
|
|
1122
|
+
if (leftTitleLength !== rightTitleLength) {
|
|
1123
|
+
return rightTitleLength - leftTitleLength;
|
|
1124
|
+
}
|
|
1125
|
+
return compareSerializedTabActivity(left, right);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
function pickBestTitle(values = []) {
|
|
1129
|
+
let best = '';
|
|
1130
|
+
for (const value of values) {
|
|
1131
|
+
const text = String(value || '').trim();
|
|
1132
|
+
if (text.length > best.length) {
|
|
1133
|
+
best = text;
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
return best;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
function pickLongerArray(left, right) {
|
|
1140
|
+
const leftItems = Array.isArray(left) ? left : [];
|
|
1141
|
+
const rightItems = Array.isArray(right) ? right : [];
|
|
1142
|
+
return rightItems.length > leftItems.length ? rightItems : leftItems;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
function mergeSerializedTabGroup(group = []) {
|
|
1146
|
+
if (!Array.isArray(group) || group.length === 0) {
|
|
1031
1147
|
return null;
|
|
1032
1148
|
}
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1149
|
+
if (group.length === 1) {
|
|
1150
|
+
const only = cloneSerializable(group[0], null);
|
|
1151
|
+
if (only && Array.isArray(only.messages)) {
|
|
1152
|
+
only.messages = normalizeAgentTranscriptMessages(only.messages);
|
|
1153
|
+
}
|
|
1154
|
+
return only;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
const base = [...group].sort(compareSerializedTabBase)[0];
|
|
1158
|
+
const content = [...group].sort(compareSerializedTabActivity)[0];
|
|
1159
|
+
const merged = cloneSerializable(base, null);
|
|
1160
|
+
if (!merged) {
|
|
1037
1161
|
return null;
|
|
1038
1162
|
}
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1163
|
+
|
|
1164
|
+
const bestTitle = pickBestTitle(group.map((entry) => entry?.title));
|
|
1165
|
+
merged.title = String(merged.title || '').trim()
|
|
1166
|
+
|| String(content.title || '').trim()
|
|
1167
|
+
|| bestTitle;
|
|
1168
|
+
merged.cwd = String(merged.cwd || content.cwd || '');
|
|
1169
|
+
merged.terminalSessionId = String(
|
|
1170
|
+
merged.terminalSessionId || content.terminalSessionId || ''
|
|
1171
|
+
);
|
|
1172
|
+
merged.currentModeId = String(
|
|
1173
|
+
merged.currentModeId || content.currentModeId || ''
|
|
1174
|
+
);
|
|
1175
|
+
merged.availableModes = cloneSerializable(
|
|
1176
|
+
pickLongerArray(merged.availableModes, content.availableModes),
|
|
1177
|
+
[]
|
|
1178
|
+
);
|
|
1179
|
+
merged.availableCommands = cloneSerializable(
|
|
1180
|
+
pickLongerArray(merged.availableCommands, content.availableCommands),
|
|
1181
|
+
[]
|
|
1182
|
+
);
|
|
1183
|
+
merged.configOptions = cloneSerializable(
|
|
1184
|
+
pickLongerArray(merged.configOptions, content.configOptions),
|
|
1185
|
+
[]
|
|
1186
|
+
);
|
|
1187
|
+
merged.messages = normalizeAgentTranscriptMessages(
|
|
1188
|
+
cloneSerializable(content.messages, [])
|
|
1189
|
+
);
|
|
1190
|
+
merged.toolCalls = cloneSerializable(content.toolCalls, []);
|
|
1191
|
+
merged.permissions = cloneSerializable(content.permissions, []);
|
|
1192
|
+
merged.plan = cloneSerializable(content.plan, []);
|
|
1193
|
+
merged.usage = cloneSerializable(content.usage, null);
|
|
1194
|
+
merged.terminals = cloneSerializable(
|
|
1195
|
+
pickLongerArray(merged.terminals, content.terminals),
|
|
1196
|
+
[]
|
|
1197
|
+
);
|
|
1198
|
+
return merged;
|
|
1046
1199
|
}
|
|
1047
1200
|
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1201
|
+
function dedupeSerializedTabs(entries = []) {
|
|
1202
|
+
const groups = new Map();
|
|
1203
|
+
const order = [];
|
|
1204
|
+
for (const entry of Array.isArray(entries) ? entries : []) {
|
|
1205
|
+
if (!entry || typeof entry !== 'object') {
|
|
1206
|
+
continue;
|
|
1207
|
+
}
|
|
1208
|
+
const acpSessionId = String(entry.acpSessionId || '').trim();
|
|
1209
|
+
const key = acpSessionId || `id:${String(entry.id || '').trim()}`;
|
|
1210
|
+
if (!groups.has(key)) {
|
|
1211
|
+
groups.set(key, []);
|
|
1212
|
+
order.push(key);
|
|
1213
|
+
}
|
|
1214
|
+
groups.get(key).push(entry);
|
|
1051
1215
|
}
|
|
1052
|
-
const
|
|
1053
|
-
|
|
1054
|
-
|
|
1216
|
+
const deduped = [];
|
|
1217
|
+
let changed = false;
|
|
1218
|
+
for (const key of order) {
|
|
1219
|
+
const group = groups.get(key) || [];
|
|
1220
|
+
const merged = mergeSerializedTabGroup(group);
|
|
1221
|
+
if (merged) {
|
|
1222
|
+
deduped.push(merged);
|
|
1223
|
+
}
|
|
1224
|
+
if (group.length > 1) {
|
|
1225
|
+
changed = true;
|
|
1226
|
+
}
|
|
1055
1227
|
}
|
|
1228
|
+
return { tabs: deduped, changed };
|
|
1229
|
+
}
|
|
1056
1230
|
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1231
|
+
function normalizePersistedTimelineOrder(value, fallback = 0) {
|
|
1232
|
+
return Number.isFinite(value) && value > 0 ? value : fallback;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
function getSyntheticTurnKey(streamKey = '') {
|
|
1236
|
+
const match = /^synthetic:(\d+):/.exec(String(streamKey || ''));
|
|
1237
|
+
return match ? match[1] : '';
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
function buildMessageReplaySignature(message = {}) {
|
|
1241
|
+
return [
|
|
1242
|
+
String(message?.role || ''),
|
|
1243
|
+
String(message?.kind || ''),
|
|
1244
|
+
String(message?.streamKey || ''),
|
|
1245
|
+
String(message?.text || '')
|
|
1246
|
+
].join('\u0000');
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
export function normalizeAgentTranscriptMessages(messages = []) {
|
|
1250
|
+
const normalized = Array.isArray(messages)
|
|
1251
|
+
? messages.map((message, index) =>
|
|
1252
|
+
normalizePersistedMessage(message, index + 1)
|
|
1253
|
+
)
|
|
1254
|
+
: [];
|
|
1255
|
+
if (normalized.length <= 1) {
|
|
1256
|
+
return normalized;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
const blocks = [];
|
|
1260
|
+
let index = 0;
|
|
1261
|
+
while (index < normalized.length) {
|
|
1262
|
+
const first = normalized[index];
|
|
1263
|
+
const syntheticTurnKey = getSyntheticTurnKey(first.streamKey);
|
|
1264
|
+
const block = [first];
|
|
1265
|
+
index += 1;
|
|
1266
|
+
if (!syntheticTurnKey) {
|
|
1267
|
+
blocks.push(block);
|
|
1268
|
+
continue;
|
|
1269
|
+
}
|
|
1270
|
+
while (index < normalized.length) {
|
|
1271
|
+
const next = normalized[index];
|
|
1272
|
+
if (getSyntheticTurnKey(next.streamKey) !== syntheticTurnKey) {
|
|
1273
|
+
break;
|
|
1274
|
+
}
|
|
1275
|
+
block.push(next);
|
|
1276
|
+
index += 1;
|
|
1277
|
+
}
|
|
1278
|
+
const dedupedBlock = [];
|
|
1279
|
+
const dedupedIndexes = new Map();
|
|
1280
|
+
for (const message of block) {
|
|
1281
|
+
const signature = buildMessageReplaySignature(message);
|
|
1282
|
+
const existingIndex = dedupedIndexes.get(signature);
|
|
1283
|
+
if (existingIndex === undefined) {
|
|
1284
|
+
dedupedIndexes.set(signature, dedupedBlock.length);
|
|
1285
|
+
dedupedBlock.push(message);
|
|
1061
1286
|
continue;
|
|
1062
1287
|
}
|
|
1063
|
-
if (message.
|
|
1064
|
-
|
|
1065
|
-
state.offset = chunk.length;
|
|
1066
|
-
state.started = true;
|
|
1067
|
-
if (state.offset >= message.text.length) {
|
|
1068
|
-
state.index += 1;
|
|
1069
|
-
state.offset = 0;
|
|
1070
|
-
}
|
|
1071
|
-
if (state.index >= state.messages.length) {
|
|
1072
|
-
state.exhausted = true;
|
|
1073
|
-
}
|
|
1074
|
-
return true;
|
|
1288
|
+
if (String(message.role || '') === 'assistant') {
|
|
1289
|
+
dedupedBlock[existingIndex] = message;
|
|
1075
1290
|
}
|
|
1076
1291
|
}
|
|
1077
|
-
|
|
1078
|
-
|
|
1292
|
+
blocks.push(dedupedBlock);
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
const dedupedBlocks = [];
|
|
1296
|
+
let previousSignature = '';
|
|
1297
|
+
for (const block of blocks) {
|
|
1298
|
+
const signature = block
|
|
1299
|
+
.map((message) => buildMessageReplaySignature(message))
|
|
1300
|
+
.join('\u0001');
|
|
1301
|
+
if (signature && signature === previousSignature) {
|
|
1302
|
+
continue;
|
|
1303
|
+
}
|
|
1304
|
+
dedupedBlocks.push(block);
|
|
1305
|
+
previousSignature = signature;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
return dedupedBlocks.flat();
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
export function createRestoreCaptureState(messages = [], options = {}) {
|
|
1312
|
+
const toolCalls = Array.isArray(options.toolCalls)
|
|
1313
|
+
? options.toolCalls
|
|
1314
|
+
: [];
|
|
1315
|
+
return {
|
|
1316
|
+
baselineMessages: normalizeAgentTranscriptMessages(messages),
|
|
1317
|
+
baselineToolCalls: new Map(
|
|
1318
|
+
toolCalls
|
|
1319
|
+
.map((entry) => normalizePersistedTimelineEntry(entry, 0))
|
|
1320
|
+
.filter((entry) => typeof entry.toolCallId === 'string')
|
|
1321
|
+
.map((entry) => [entry.toolCallId, entry])
|
|
1322
|
+
),
|
|
1323
|
+
messages: [],
|
|
1324
|
+
syntheticStreams: new Map(),
|
|
1325
|
+
syntheticStreamTurn: getNextSyntheticStreamTurn(messages),
|
|
1326
|
+
messageCounter: 0,
|
|
1327
|
+
nextTimelineOrder: null
|
|
1079
1328
|
};
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
function getRestoreComparableAttachment(attachment = {}) {
|
|
1332
|
+
return [
|
|
1333
|
+
String(attachment?.kind || ''),
|
|
1334
|
+
String(attachment?.name || ''),
|
|
1335
|
+
String(attachment?.path || ''),
|
|
1336
|
+
String(attachment?.url || ''),
|
|
1337
|
+
Number.isFinite(attachment?.size) ? attachment.size : 0,
|
|
1338
|
+
Number.isFinite(attachment?.lastModified)
|
|
1339
|
+
? attachment.lastModified
|
|
1340
|
+
: 0
|
|
1341
|
+
];
|
|
1342
|
+
}
|
|
1080
1343
|
|
|
1081
|
-
|
|
1082
|
-
|
|
1344
|
+
function buildRestoreComparableMessage(message = {}) {
|
|
1345
|
+
return JSON.stringify({
|
|
1346
|
+
role: String(message?.role || ''),
|
|
1347
|
+
kind: String(message?.kind || ''),
|
|
1348
|
+
text: String(message?.text || ''),
|
|
1349
|
+
attachments: Array.isArray(message?.attachments)
|
|
1350
|
+
? message.attachments.map(getRestoreComparableAttachment)
|
|
1351
|
+
: []
|
|
1352
|
+
});
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
function areRestoreMessagesEquivalent(left = {}, right = {}) {
|
|
1356
|
+
return buildRestoreComparableMessage(left)
|
|
1357
|
+
=== buildRestoreComparableMessage(right);
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
function isRestoreMessageContinuation(previousText = '', chunkText = '') {
|
|
1361
|
+
const previous = String(previousText || '');
|
|
1362
|
+
const chunk = String(chunkText || '');
|
|
1363
|
+
if (!previous || !chunk || previous === chunk) {
|
|
1364
|
+
return false;
|
|
1365
|
+
}
|
|
1366
|
+
if (chunk.startsWith(previous) || previous.startsWith(chunk)) {
|
|
1367
|
+
return true;
|
|
1368
|
+
}
|
|
1369
|
+
const maxOverlap = Math.min(previous.length, chunk.length, 2048);
|
|
1370
|
+
for (let overlap = maxOverlap; overlap >= 2; overlap -= 1) {
|
|
1371
|
+
if (previous.slice(-overlap) === chunk.slice(0, overlap)) {
|
|
1372
|
+
return true;
|
|
1373
|
+
}
|
|
1083
1374
|
}
|
|
1375
|
+
return false;
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
function maybeAdvanceRestoreCaptureTurn(capture, update, role, kind, text) {
|
|
1084
1379
|
if (
|
|
1085
|
-
|
|
1086
|
-
||
|
|
1380
|
+
!capture
|
|
1381
|
+
|| update?.messageId
|
|
1382
|
+
|| role !== 'user'
|
|
1383
|
+
|| kind !== 'message'
|
|
1087
1384
|
) {
|
|
1088
|
-
|
|
1089
|
-
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1387
|
+
const last = capture.messages[capture.messages.length - 1] || null;
|
|
1388
|
+
if (!last) {
|
|
1389
|
+
return;
|
|
1090
1390
|
}
|
|
1391
|
+
if (last.role !== 'user' || last.kind !== 'message') {
|
|
1392
|
+
capture.syntheticStreamTurn += 1;
|
|
1393
|
+
capture.syntheticStreams.clear();
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1396
|
+
if (!isRestoreMessageContinuation(last.text, text)) {
|
|
1397
|
+
capture.syntheticStreamTurn += 1;
|
|
1398
|
+
capture.syntheticStreams.clear();
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1091
1401
|
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
return
|
|
1402
|
+
function getRestoreCaptureStreamKey(capture, update, role, kind, text = '') {
|
|
1403
|
+
maybeAdvanceRestoreCaptureTurn(capture, update, role, kind, text);
|
|
1404
|
+
if (update?.messageId) {
|
|
1405
|
+
return update.messageId;
|
|
1406
|
+
}
|
|
1407
|
+
const bucketKey = `${update?.sessionUpdate}:${role}:${kind}`;
|
|
1408
|
+
let streamKey = capture.syntheticStreams.get(bucketKey) || '';
|
|
1409
|
+
if (!streamKey) {
|
|
1410
|
+
streamKey = [
|
|
1411
|
+
'synthetic',
|
|
1412
|
+
capture.syntheticStreamTurn,
|
|
1413
|
+
update?.sessionUpdate || 'message_chunk',
|
|
1414
|
+
role,
|
|
1415
|
+
kind
|
|
1416
|
+
].join(':');
|
|
1417
|
+
capture.syntheticStreams.set(bucketKey, streamKey);
|
|
1418
|
+
}
|
|
1419
|
+
return streamKey;
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
export function captureRestoreReplayChunk(capture, update, role, kind, text) {
|
|
1423
|
+
if (!capture) return false;
|
|
1424
|
+
const chunk = String(text || '');
|
|
1425
|
+
if (!chunk) return true;
|
|
1426
|
+
const streamKey = getRestoreCaptureStreamKey(
|
|
1427
|
+
capture,
|
|
1428
|
+
update,
|
|
1429
|
+
role,
|
|
1430
|
+
kind,
|
|
1431
|
+
chunk
|
|
1432
|
+
);
|
|
1433
|
+
const last = capture.messages[capture.messages.length - 1] || null;
|
|
1434
|
+
if (
|
|
1435
|
+
last
|
|
1436
|
+
&& last.streamKey === streamKey
|
|
1437
|
+
&& last.role === role
|
|
1438
|
+
&& last.kind === kind
|
|
1439
|
+
) {
|
|
1440
|
+
last.text = mergeAgentMessageText(last.text, chunk);
|
|
1441
|
+
return true;
|
|
1096
1442
|
}
|
|
1443
|
+
if (!update?.messageId) {
|
|
1444
|
+
capture.messageCounter += 1;
|
|
1445
|
+
}
|
|
1446
|
+
const baselineMessage = capture.baselineMessages[capture.messages.length] || null;
|
|
1447
|
+
const canReuseBaseline = !!(
|
|
1448
|
+
baselineMessage
|
|
1449
|
+
&& baselineMessage.role === role
|
|
1450
|
+
&& baselineMessage.kind === kind
|
|
1451
|
+
);
|
|
1452
|
+
capture.messages.push({
|
|
1453
|
+
id: canReuseBaseline
|
|
1454
|
+
? (baselineMessage.id || crypto.randomUUID())
|
|
1455
|
+
: crypto.randomUUID(),
|
|
1456
|
+
streamKey: canReuseBaseline
|
|
1457
|
+
? (baselineMessage.streamKey || streamKey)
|
|
1458
|
+
: streamKey,
|
|
1459
|
+
role,
|
|
1460
|
+
kind,
|
|
1461
|
+
text: chunk,
|
|
1462
|
+
createdAt: canReuseBaseline
|
|
1463
|
+
? (baselineMessage.createdAt || '')
|
|
1464
|
+
: '',
|
|
1465
|
+
order: typeof capture.nextTimelineOrder === 'function'
|
|
1466
|
+
? capture.nextTimelineOrder()
|
|
1467
|
+
: capture.messages.length + 1,
|
|
1468
|
+
attachments: canReuseBaseline
|
|
1469
|
+
&& Array.isArray(baselineMessage.attachments)
|
|
1470
|
+
? cloneSerializable(baselineMessage.attachments, [])
|
|
1471
|
+
: []
|
|
1472
|
+
});
|
|
1473
|
+
return true;
|
|
1474
|
+
}
|
|
1097
1475
|
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1476
|
+
export function finalizeRestoreCaptureMessages(tab) {
|
|
1477
|
+
const capture = tab?.restoreCapture;
|
|
1478
|
+
if (!capture) {
|
|
1101
1479
|
return false;
|
|
1102
1480
|
}
|
|
1481
|
+
const baselineMessages = Array.isArray(capture.baselineMessages)
|
|
1482
|
+
? capture.baselineMessages
|
|
1483
|
+
: [];
|
|
1484
|
+
const replayMessages = Array.isArray(capture.messages)
|
|
1485
|
+
? capture.messages
|
|
1486
|
+
: [];
|
|
1487
|
+
const nextMessages = replayMessages.length > 0
|
|
1488
|
+
? normalizeAgentTranscriptMessages(replayMessages)
|
|
1489
|
+
: baselineMessages;
|
|
1490
|
+
const replacedMessages = !(
|
|
1491
|
+
baselineMessages.length === nextMessages.length
|
|
1492
|
+
&& baselineMessages.every((message, index) =>
|
|
1493
|
+
areRestoreMessagesEquivalent(message, nextMessages[index] || {})
|
|
1494
|
+
)
|
|
1495
|
+
);
|
|
1496
|
+
tab.messages = nextMessages;
|
|
1497
|
+
const maxMessageOrder = tab.messages.reduce(
|
|
1498
|
+
(maxOrder, message) => Math.max(
|
|
1499
|
+
maxOrder,
|
|
1500
|
+
normalizePersistedTimelineOrder(message.order, 0)
|
|
1501
|
+
),
|
|
1502
|
+
0
|
|
1503
|
+
);
|
|
1504
|
+
const maxToolCallOrder = Array.from(tab.toolCalls.values()).reduce(
|
|
1505
|
+
(maxOrder, toolCall) => Math.max(
|
|
1506
|
+
maxOrder,
|
|
1507
|
+
normalizePersistedTimelineOrder(toolCall?.order, 0)
|
|
1508
|
+
),
|
|
1509
|
+
0
|
|
1510
|
+
);
|
|
1511
|
+
const maxPermissionOrder = Array.from(tab.permissions.values()).reduce(
|
|
1512
|
+
(maxOrder, permission) => Math.max(
|
|
1513
|
+
maxOrder,
|
|
1514
|
+
normalizePersistedTimelineOrder(permission?.order, 0)
|
|
1515
|
+
),
|
|
1516
|
+
0
|
|
1517
|
+
);
|
|
1518
|
+
tab.timelineCounter = Math.max(
|
|
1519
|
+
maxMessageOrder,
|
|
1520
|
+
maxToolCallOrder,
|
|
1521
|
+
maxPermissionOrder
|
|
1522
|
+
);
|
|
1523
|
+
tab.messageCounter = Math.max(tab.messageCounter, tab.messages.length);
|
|
1524
|
+
tab.restoreCapture = null;
|
|
1525
|
+
return replacedMessages;
|
|
1526
|
+
}
|
|
1103
1527
|
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1528
|
+
export function buildRestoredToolCall(
|
|
1529
|
+
previous = null,
|
|
1530
|
+
baseline = null,
|
|
1531
|
+
update = {},
|
|
1532
|
+
nextTimelineOrder = null,
|
|
1533
|
+
options = {}
|
|
1534
|
+
) {
|
|
1535
|
+
const persisted = cloneSerializable(baseline, {}) || {};
|
|
1536
|
+
const current = cloneSerializable(previous, {}) || {};
|
|
1537
|
+
const allowEmptyCreatedAt = options.allowEmptyCreatedAt === true;
|
|
1538
|
+
const nextOrder = normalizePersistedTimelineOrder(current.order, 0)
|
|
1539
|
+
|| (
|
|
1540
|
+
typeof nextTimelineOrder === 'function'
|
|
1541
|
+
? nextTimelineOrder()
|
|
1542
|
+
: normalizePersistedTimelineOrder(persisted.order, 0)
|
|
1543
|
+
)
|
|
1544
|
+
|| 1;
|
|
1545
|
+
const inheritedCreatedAt = String(
|
|
1546
|
+
current.createdAt || persisted.createdAt || ''
|
|
1547
|
+
).trim();
|
|
1548
|
+
const createdAt = inheritedCreatedAt
|
|
1549
|
+
|| (allowEmptyCreatedAt ? '' : new Date().toISOString());
|
|
1550
|
+
const nextToolCall = {
|
|
1551
|
+
...persisted,
|
|
1552
|
+
...current,
|
|
1553
|
+
...update,
|
|
1554
|
+
createdAt,
|
|
1555
|
+
order: nextOrder
|
|
1556
|
+
};
|
|
1557
|
+
if (!nextToolCall.toolCallId) {
|
|
1558
|
+
nextToolCall.toolCallId = String(update.toolCallId || '');
|
|
1108
1559
|
}
|
|
1109
|
-
if (
|
|
1110
|
-
|
|
1560
|
+
if (typeof nextToolCall.title !== 'string') {
|
|
1561
|
+
nextToolCall.title = '';
|
|
1111
1562
|
}
|
|
1112
|
-
|
|
1563
|
+
if (typeof nextToolCall.status !== 'string') {
|
|
1564
|
+
nextToolCall.status = 'pending';
|
|
1565
|
+
}
|
|
1566
|
+
return nextToolCall;
|
|
1113
1567
|
}
|
|
1114
1568
|
|
|
1115
1569
|
function normalizePersistedMessage(message = {}, fallbackOrder = 0) {
|
|
@@ -1192,12 +1646,26 @@ function normalizePersistedTerminalSummary(summary = {}) {
|
|
|
1192
1646
|
};
|
|
1193
1647
|
}
|
|
1194
1648
|
|
|
1649
|
+
export function getNextSyntheticStreamTurn(messages = []) {
|
|
1650
|
+
if (!Array.isArray(messages) || messages.length === 0) {
|
|
1651
|
+
return 0;
|
|
1652
|
+
}
|
|
1653
|
+
return messages.reduce((maxTurn, entry) => {
|
|
1654
|
+
const streamKey = String(entry?.streamKey || '');
|
|
1655
|
+
const match = /^synthetic:(\d+):/.exec(streamKey);
|
|
1656
|
+
if (!match) {
|
|
1657
|
+
return maxTurn;
|
|
1658
|
+
}
|
|
1659
|
+
const turn = Number.parseInt(match[1], 10);
|
|
1660
|
+
if (!Number.isFinite(turn)) {
|
|
1661
|
+
return maxTurn;
|
|
1662
|
+
}
|
|
1663
|
+
return Math.max(maxTurn, turn + 1);
|
|
1664
|
+
}, 0);
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1195
1667
|
function restorePersistedTabSnapshot(tab, snapshot = {}) {
|
|
1196
|
-
const messages =
|
|
1197
|
-
? snapshot.messages.map((message, index) =>
|
|
1198
|
-
normalizePersistedMessage(message, index + 1)
|
|
1199
|
-
)
|
|
1200
|
-
: [];
|
|
1668
|
+
const messages = normalizeAgentTranscriptMessages(snapshot.messages);
|
|
1201
1669
|
const toolCalls = Array.isArray(snapshot.toolCalls)
|
|
1202
1670
|
? snapshot.toolCalls.map((entry, index) =>
|
|
1203
1671
|
normalizePersistedTimelineEntry(
|
|
@@ -1279,6 +1747,13 @@ function restorePersistedTabSnapshot(tab, snapshot = {}) {
|
|
|
1279
1747
|
maxPermissionOrder
|
|
1280
1748
|
);
|
|
1281
1749
|
tab.messageCounter = Math.max(tab.messageCounter, messages.length);
|
|
1750
|
+
const maxSyntheticTurn = getNextSyntheticStreamTurn(messages);
|
|
1751
|
+
tab.syntheticStreamTurn = Math.max(
|
|
1752
|
+
Number.isFinite(tab.syntheticStreamTurn)
|
|
1753
|
+
? tab.syntheticStreamTurn
|
|
1754
|
+
: 0,
|
|
1755
|
+
maxSyntheticTurn
|
|
1756
|
+
);
|
|
1282
1757
|
}
|
|
1283
1758
|
|
|
1284
1759
|
class LocalExecTerminal extends EventEmitter {
|
|
@@ -1748,7 +2223,7 @@ class AcpRuntime extends EventEmitter {
|
|
|
1748
2223
|
syntheticStreams: new Map(),
|
|
1749
2224
|
syntheticStreamTurn: 0,
|
|
1750
2225
|
pendingUserEcho: null,
|
|
1751
|
-
|
|
2226
|
+
restoreCapture: null,
|
|
1752
2227
|
currentModeId,
|
|
1753
2228
|
availableModes,
|
|
1754
2229
|
availableCommands,
|
|
@@ -2005,14 +2480,20 @@ class AcpRuntime extends EventEmitter {
|
|
|
2005
2480
|
availableModes: meta.availableModes || [],
|
|
2006
2481
|
availableCommands: meta.availableCommands || [],
|
|
2007
2482
|
configOptions: meta.configOptions || [],
|
|
2008
|
-
messages:
|
|
2009
|
-
toolCalls:
|
|
2010
|
-
permissions:
|
|
2011
|
-
plan:
|
|
2483
|
+
messages: [],
|
|
2484
|
+
toolCalls: [],
|
|
2485
|
+
permissions: [],
|
|
2486
|
+
plan: [],
|
|
2012
2487
|
usage: meta.usage || null,
|
|
2013
2488
|
terminals: meta.terminals || []
|
|
2014
2489
|
});
|
|
2015
|
-
|
|
2490
|
+
// For loadSession-capable runtimes, transcript ordering comes from the
|
|
2491
|
+
// authoritative replay stream, not the persisted snapshot.
|
|
2492
|
+
tab.restoreCapture = createRestoreCaptureState(meta.messages || [], {
|
|
2493
|
+
toolCalls: meta.toolCalls || []
|
|
2494
|
+
});
|
|
2495
|
+
tab.restoreCapture.nextTimelineOrder = () =>
|
|
2496
|
+
this.#nextTimelineOrder(tab);
|
|
2016
2497
|
tab.status = 'restoring';
|
|
2017
2498
|
tab.busy = true;
|
|
2018
2499
|
|
|
@@ -2020,41 +2501,14 @@ class AcpRuntime extends EventEmitter {
|
|
|
2020
2501
|
this.sessionToTabId.set(tab.acpSessionId, tab.id);
|
|
2021
2502
|
|
|
2022
2503
|
try {
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2504
|
+
await this.#loadSessionIntoTab(tab, meta);
|
|
2505
|
+
this.#broadcast(tab, {
|
|
2506
|
+
type: 'snapshot',
|
|
2507
|
+
tab: this.serializeTab(tab)
|
|
2027
2508
|
});
|
|
2028
|
-
const restoredSessionId = response?.sessionId || meta.acpSessionId;
|
|
2029
|
-
if (restoredSessionId !== tab.acpSessionId) {
|
|
2030
|
-
this.sessionToTabId.delete(tab.acpSessionId);
|
|
2031
|
-
tab.acpSessionId = restoredSessionId;
|
|
2032
|
-
this.sessionToTabId.set(tab.acpSessionId, tab.id);
|
|
2033
|
-
}
|
|
2034
|
-
if (typeof response?.title === 'string') {
|
|
2035
|
-
tab.title = response.title;
|
|
2036
|
-
}
|
|
2037
|
-
tab.currentModeId = response?.modes?.currentModeId || '';
|
|
2038
|
-
tab.availableModes = this.#resolveAvailableModes(
|
|
2039
|
-
response?.modes?.availableModes,
|
|
2040
|
-
tab.availableModes
|
|
2041
|
-
);
|
|
2042
|
-
tab.availableCommands = this.#resolveAvailableCommands(
|
|
2043
|
-
response?.availableCommands,
|
|
2044
|
-
tab.availableCommands
|
|
2045
|
-
);
|
|
2046
|
-
tab.configOptions = this.#resolveConfigOptions(
|
|
2047
|
-
response?.configOptions,
|
|
2048
|
-
tab.configOptions,
|
|
2049
|
-
response?.models
|
|
2050
|
-
);
|
|
2051
|
-
tab.restoreReplay = null;
|
|
2052
|
-
tab.status = 'ready';
|
|
2053
|
-
tab.busy = false;
|
|
2054
|
-
tab.errorMessage = '';
|
|
2055
2509
|
return this.serializeTab(tab);
|
|
2056
2510
|
} catch (error) {
|
|
2057
|
-
tab.
|
|
2511
|
+
tab.restoreCapture = null;
|
|
2058
2512
|
this.tabs.delete(tab.id);
|
|
2059
2513
|
this.sessionToTabId.delete(tab.acpSessionId);
|
|
2060
2514
|
throw error;
|
|
@@ -2131,16 +2585,31 @@ class AcpRuntime extends EventEmitter {
|
|
|
2131
2585
|
createdAt: new Date().toISOString(),
|
|
2132
2586
|
title: meta.title || ''
|
|
2133
2587
|
});
|
|
2588
|
+
// Resume rebuilds transcript ordering from the runtime replay stream.
|
|
2589
|
+
tab.restoreCapture = createRestoreCaptureState([]);
|
|
2590
|
+
tab.restoreCapture.nextTimelineOrder = () =>
|
|
2591
|
+
this.#nextTimelineOrder(tab);
|
|
2134
2592
|
tab.status = 'restoring';
|
|
2135
2593
|
tab.busy = true;
|
|
2136
2594
|
|
|
2137
2595
|
this.tabs.set(tab.id, tab);
|
|
2138
2596
|
this.sessionToTabId.set(tab.acpSessionId, tab.id);
|
|
2139
2597
|
|
|
2598
|
+
try {
|
|
2599
|
+
await this.#loadSessionIntoTab(tab, meta);
|
|
2600
|
+
return this.serializeTab(tab);
|
|
2601
|
+
} catch (error) {
|
|
2602
|
+
this.tabs.delete(tab.id);
|
|
2603
|
+
this.sessionToTabId.delete(tab.acpSessionId);
|
|
2604
|
+
throw error;
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
|
|
2608
|
+
async #loadSessionIntoTab(tab, meta) {
|
|
2140
2609
|
try {
|
|
2141
2610
|
const response = await this.connection.loadSession({
|
|
2142
2611
|
cwd: meta.cwd,
|
|
2143
|
-
sessionId:
|
|
2612
|
+
sessionId: tab.acpSessionId,
|
|
2144
2613
|
mcpServers: []
|
|
2145
2614
|
});
|
|
2146
2615
|
const restoredSessionId = response?.sessionId || meta.acpSessionId;
|
|
@@ -2166,13 +2635,16 @@ class AcpRuntime extends EventEmitter {
|
|
|
2166
2635
|
tab.configOptions,
|
|
2167
2636
|
response?.models
|
|
2168
2637
|
);
|
|
2638
|
+
const replacedMessages = finalizeRestoreCaptureMessages(tab);
|
|
2169
2639
|
tab.status = 'ready';
|
|
2170
2640
|
tab.busy = false;
|
|
2171
2641
|
tab.errorMessage = '';
|
|
2172
|
-
|
|
2642
|
+
if (replacedMessages) {
|
|
2643
|
+
this.#markTabDirty(tab);
|
|
2644
|
+
}
|
|
2645
|
+
return replacedMessages;
|
|
2173
2646
|
} catch (error) {
|
|
2174
|
-
|
|
2175
|
-
this.sessionToTabId.delete(tab.acpSessionId);
|
|
2647
|
+
tab.restoreCapture = null;
|
|
2176
2648
|
throw error;
|
|
2177
2649
|
}
|
|
2178
2650
|
}
|
|
@@ -2186,6 +2658,7 @@ class AcpRuntime extends EventEmitter {
|
|
|
2186
2658
|
}
|
|
2187
2659
|
|
|
2188
2660
|
serializeTab(tab) {
|
|
2661
|
+
tab.messages = normalizeAgentTranscriptMessages(tab.messages);
|
|
2189
2662
|
return {
|
|
2190
2663
|
id: tab.id,
|
|
2191
2664
|
runtimeId: tab.runtimeId,
|
|
@@ -2628,32 +3101,62 @@ class AcpRuntime extends EventEmitter {
|
|
|
2628
3101
|
}
|
|
2629
3102
|
|
|
2630
3103
|
async #handleSessionUpdate(params) {
|
|
3104
|
+
const update = params.update;
|
|
2631
3105
|
const tab = this.#getTabBySession(params.sessionId);
|
|
2632
3106
|
if (!tab) return;
|
|
2633
|
-
const update = params.update;
|
|
2634
3107
|
let broadcastUpdate = update;
|
|
2635
3108
|
let didChange = false;
|
|
3109
|
+
let suppressSessionUpdateBroadcast = false;
|
|
2636
3110
|
|
|
2637
3111
|
switch (update.sessionUpdate) {
|
|
2638
|
-
case 'agent_message_chunk':
|
|
2639
|
-
this.#appendContentChunk(
|
|
2640
|
-
|
|
3112
|
+
case 'agent_message_chunk': {
|
|
3113
|
+
const result = this.#appendContentChunk(
|
|
3114
|
+
tab,
|
|
3115
|
+
update,
|
|
3116
|
+
'assistant',
|
|
3117
|
+
'message'
|
|
3118
|
+
);
|
|
3119
|
+
didChange = !!result.didChange;
|
|
3120
|
+
suppressSessionUpdateBroadcast = !!result.suppressBroadcast;
|
|
2641
3121
|
break;
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
3122
|
+
}
|
|
3123
|
+
case 'agent_thought_chunk': {
|
|
3124
|
+
const result = this.#appendContentChunk(
|
|
3125
|
+
tab,
|
|
3126
|
+
update,
|
|
3127
|
+
'assistant',
|
|
3128
|
+
'thought'
|
|
3129
|
+
);
|
|
3130
|
+
didChange = !!result.didChange;
|
|
3131
|
+
suppressSessionUpdateBroadcast = !!result.suppressBroadcast;
|
|
2645
3132
|
break;
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
3133
|
+
}
|
|
3134
|
+
case 'user_message_chunk': {
|
|
3135
|
+
const result = this.#appendContentChunk(
|
|
3136
|
+
tab,
|
|
3137
|
+
update,
|
|
3138
|
+
'user',
|
|
3139
|
+
'message'
|
|
3140
|
+
);
|
|
3141
|
+
didChange = !!result.didChange;
|
|
3142
|
+
suppressSessionUpdateBroadcast = !!result.suppressBroadcast;
|
|
2649
3143
|
break;
|
|
3144
|
+
}
|
|
2650
3145
|
case 'tool_call': {
|
|
2651
3146
|
this.#advanceSyntheticStreamTurn(tab);
|
|
2652
|
-
const
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
3147
|
+
const baseline = tab.restoreCapture?.baselineToolCalls?.get(
|
|
3148
|
+
update.toolCallId
|
|
3149
|
+
) || null;
|
|
3150
|
+
const previous = tab.toolCalls.get(update.toolCallId) || null;
|
|
3151
|
+
const nextToolCall = buildRestoredToolCall(
|
|
3152
|
+
previous,
|
|
3153
|
+
baseline,
|
|
3154
|
+
update,
|
|
3155
|
+
() => this.#nextTimelineOrder(tab),
|
|
3156
|
+
{
|
|
3157
|
+
allowEmptyCreatedAt: !!tab.restoreCapture
|
|
3158
|
+
}
|
|
3159
|
+
);
|
|
2657
3160
|
tab.toolCalls.set(update.toolCallId, nextToolCall);
|
|
2658
3161
|
broadcastUpdate = nextToolCall;
|
|
2659
3162
|
didChange = true;
|
|
@@ -2661,17 +3164,19 @@ class AcpRuntime extends EventEmitter {
|
|
|
2661
3164
|
}
|
|
2662
3165
|
case 'tool_call_update': {
|
|
2663
3166
|
this.#advanceSyntheticStreamTurn(tab);
|
|
2664
|
-
const previous = tab.toolCalls.get(update.toolCallId) ||
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
3167
|
+
const previous = tab.toolCalls.get(update.toolCallId) || null;
|
|
3168
|
+
const baseline = tab.restoreCapture?.baselineToolCalls?.get(
|
|
3169
|
+
update.toolCallId
|
|
3170
|
+
) || null;
|
|
3171
|
+
const nextToolCall = buildRestoredToolCall(
|
|
3172
|
+
previous,
|
|
3173
|
+
baseline,
|
|
3174
|
+
update,
|
|
3175
|
+
() => this.#nextTimelineOrder(tab),
|
|
3176
|
+
{
|
|
3177
|
+
allowEmptyCreatedAt: !!tab.restoreCapture
|
|
3178
|
+
}
|
|
3179
|
+
);
|
|
2675
3180
|
tab.toolCalls.set(update.toolCallId, nextToolCall);
|
|
2676
3181
|
broadcastUpdate = nextToolCall;
|
|
2677
3182
|
didChange = true;
|
|
@@ -2715,17 +3220,19 @@ class AcpRuntime extends EventEmitter {
|
|
|
2715
3220
|
break;
|
|
2716
3221
|
}
|
|
2717
3222
|
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
3223
|
+
if (!suppressSessionUpdateBroadcast) {
|
|
3224
|
+
this.#broadcast(tab, {
|
|
3225
|
+
type: 'session_update',
|
|
3226
|
+
update: broadcastUpdate,
|
|
3227
|
+
tab: {
|
|
3228
|
+
title: tab.title,
|
|
3229
|
+
currentModeId: tab.currentModeId,
|
|
3230
|
+
availableModes: tab.availableModes,
|
|
3231
|
+
availableCommands: tab.availableCommands,
|
|
3232
|
+
configOptions: tab.configOptions
|
|
3233
|
+
}
|
|
3234
|
+
});
|
|
3235
|
+
}
|
|
2729
3236
|
if (didChange) {
|
|
2730
3237
|
this.#markTabDirty(tab);
|
|
2731
3238
|
}
|
|
@@ -2737,10 +3244,17 @@ class AcpRuntime extends EventEmitter {
|
|
|
2737
3244
|
? (content.text || '')
|
|
2738
3245
|
: `[${content.type}]`;
|
|
2739
3246
|
if (role === 'user' && kind === 'message' && this.#consumeUserEcho(tab, text)) {
|
|
2740
|
-
return
|
|
3247
|
+
return {
|
|
3248
|
+
didChange: false,
|
|
3249
|
+
suppressBroadcast: false
|
|
3250
|
+
};
|
|
2741
3251
|
}
|
|
2742
|
-
if (
|
|
2743
|
-
|
|
3252
|
+
if (tab.restoreCapture) {
|
|
3253
|
+
captureRestoreReplayChunk(tab.restoreCapture, update, role, kind, text);
|
|
3254
|
+
return {
|
|
3255
|
+
didChange: false,
|
|
3256
|
+
suppressBroadcast: true
|
|
3257
|
+
};
|
|
2744
3258
|
}
|
|
2745
3259
|
const streamKey = this.#getStreamKey(tab, update, role, kind);
|
|
2746
3260
|
const last = tab.messages[tab.messages.length - 1] || null;
|
|
@@ -2761,7 +3275,10 @@ class AcpRuntime extends EventEmitter {
|
|
|
2761
3275
|
kind,
|
|
2762
3276
|
text: appendedText
|
|
2763
3277
|
});
|
|
2764
|
-
return
|
|
3278
|
+
return {
|
|
3279
|
+
didChange: true,
|
|
3280
|
+
suppressBroadcast: false
|
|
3281
|
+
};
|
|
2765
3282
|
}
|
|
2766
3283
|
|
|
2767
3284
|
if (!update.messageId) {
|
|
@@ -2781,6 +3298,10 @@ class AcpRuntime extends EventEmitter {
|
|
|
2781
3298
|
type: 'message_open',
|
|
2782
3299
|
message
|
|
2783
3300
|
});
|
|
3301
|
+
return {
|
|
3302
|
+
didChange: true,
|
|
3303
|
+
suppressBroadcast: false
|
|
3304
|
+
};
|
|
2784
3305
|
}
|
|
2785
3306
|
|
|
2786
3307
|
#consumeUserEcho(tab, text) {
|
|
@@ -3538,8 +4059,8 @@ export class AcpManager {
|
|
|
3538
4059
|
}, this.transcriptPersistDelayMs);
|
|
3539
4060
|
}
|
|
3540
4061
|
|
|
3541
|
-
|
|
3542
|
-
|
|
4062
|
+
#getSerializedTabs() {
|
|
4063
|
+
const serializedTabs = Array.from(this.tabs.values()).map((entry) => {
|
|
3543
4064
|
const tab = entry.serialize();
|
|
3544
4065
|
return {
|
|
3545
4066
|
id: tab.id,
|
|
@@ -3577,6 +4098,21 @@ export class AcpManager {
|
|
|
3577
4098
|
: []
|
|
3578
4099
|
};
|
|
3579
4100
|
});
|
|
4101
|
+
return dedupeSerializedTabs(serializedTabs).tabs;
|
|
4102
|
+
}
|
|
4103
|
+
|
|
4104
|
+
#findOpenSerializedTabBySessionId(sessionId) {
|
|
4105
|
+
const targetSessionId = String(sessionId || '').trim();
|
|
4106
|
+
if (!targetSessionId) {
|
|
4107
|
+
return null;
|
|
4108
|
+
}
|
|
4109
|
+
return this.#getSerializedTabs().find(
|
|
4110
|
+
(tab) => String(tab?.acpSessionId || '').trim() === targetSessionId
|
|
4111
|
+
) || null;
|
|
4112
|
+
}
|
|
4113
|
+
|
|
4114
|
+
getPersistedTabs() {
|
|
4115
|
+
return this.#getSerializedTabs();
|
|
3580
4116
|
}
|
|
3581
4117
|
|
|
3582
4118
|
persistTabs() {
|
|
@@ -3608,15 +4144,15 @@ export class AcpManager {
|
|
|
3608
4144
|
restoring: this.restoring,
|
|
3609
4145
|
definitions: await this.listDefinitions(),
|
|
3610
4146
|
configs: await this.listAgentConfigs(),
|
|
3611
|
-
tabs:
|
|
4147
|
+
tabs: this.#getSerializedTabs()
|
|
3612
4148
|
};
|
|
3613
4149
|
}
|
|
3614
4150
|
|
|
3615
4151
|
async listInventory() {
|
|
4152
|
+
const tabs = this.#getSerializedTabs();
|
|
3616
4153
|
return {
|
|
3617
4154
|
restoring: this.restoring,
|
|
3618
|
-
tabs:
|
|
3619
|
-
const serialized = entry.serialize();
|
|
4155
|
+
tabs: tabs.map((serialized) => {
|
|
3620
4156
|
return {
|
|
3621
4157
|
id: serialized.id,
|
|
3622
4158
|
runtimeId: serialized.runtimeId,
|
|
@@ -3871,6 +4407,13 @@ export class AcpManager {
|
|
|
3871
4407
|
throw new Error(availability.reason || 'Agent unavailable');
|
|
3872
4408
|
}
|
|
3873
4409
|
|
|
4410
|
+
const existingTab = this.#findOpenSerializedTabBySessionId(
|
|
4411
|
+
options.sessionId
|
|
4412
|
+
);
|
|
4413
|
+
if (existingTab) {
|
|
4414
|
+
return existingTab;
|
|
4415
|
+
}
|
|
4416
|
+
|
|
3874
4417
|
const cwd = path.resolve(options.cwd || process.cwd());
|
|
3875
4418
|
const { runtimeEntry, createdRuntime, runtimeStoreKey } =
|
|
3876
4419
|
this.#ensureRuntimeEntry(definition, cwd);
|
|
@@ -3917,10 +4460,21 @@ export class AcpManager {
|
|
|
3917
4460
|
|
|
3918
4461
|
async restoreTabs(validTerminalSessionIds = new Set()) {
|
|
3919
4462
|
await this.ensureConfigsLoaded();
|
|
3920
|
-
const
|
|
4463
|
+
const dedupedTabs = dedupeSerializedTabs(await this.loadTabs());
|
|
4464
|
+
const entries = dedupedTabs.tabs;
|
|
3921
4465
|
let changed = false;
|
|
4466
|
+
if (dedupedTabs.changed) {
|
|
4467
|
+
changed = true;
|
|
4468
|
+
}
|
|
3922
4469
|
|
|
3923
4470
|
for (const meta of entries) {
|
|
4471
|
+
const existingTab = this.#findOpenSerializedTabBySessionId(
|
|
4472
|
+
meta.acpSessionId
|
|
4473
|
+
);
|
|
4474
|
+
if (existingTab) {
|
|
4475
|
+
changed = true;
|
|
4476
|
+
continue;
|
|
4477
|
+
}
|
|
3924
4478
|
if (
|
|
3925
4479
|
meta.terminalSessionId
|
|
3926
4480
|
&& !validTerminalSessionIds.has(meta.terminalSessionId)
|