tabminal 3.0.15 → 3.0.16
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 +707 -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,543 @@ 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
|
+
}
|
|
1230
|
+
|
|
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
|
+
}
|
|
1056
1248
|
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
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
|
+
}
|
|
1343
|
+
|
|
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
|
+
}
|
|
1080
1354
|
|
|
1081
|
-
|
|
1082
|
-
|
|
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
|
-
return false;
|
|
1385
|
+
return;
|
|
1090
1386
|
}
|
|
1387
|
+
const last = capture.messages[capture.messages.length - 1] || null;
|
|
1388
|
+
if (!last) {
|
|
1389
|
+
return;
|
|
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;
|
|
1442
|
+
}
|
|
1443
|
+
if (!update?.messageId) {
|
|
1444
|
+
capture.messageCounter += 1;
|
|
1096
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
|
+
) {
|
|
1534
|
+
const persisted = cloneSerializable(baseline, {}) || {};
|
|
1535
|
+
const current = cloneSerializable(previous, {}) || {};
|
|
1536
|
+
const nextOrder = normalizePersistedTimelineOrder(current.order, 0)
|
|
1537
|
+
|| (
|
|
1538
|
+
typeof nextTimelineOrder === 'function'
|
|
1539
|
+
? nextTimelineOrder()
|
|
1540
|
+
: normalizePersistedTimelineOrder(persisted.order, 0)
|
|
1541
|
+
)
|
|
1542
|
+
|| 1;
|
|
1543
|
+
const createdAt = String(
|
|
1544
|
+
current.createdAt || persisted.createdAt || ''
|
|
1545
|
+
).trim() || new Date().toISOString();
|
|
1546
|
+
const nextToolCall = {
|
|
1547
|
+
...persisted,
|
|
1548
|
+
...current,
|
|
1549
|
+
...update,
|
|
1550
|
+
createdAt,
|
|
1551
|
+
order: nextOrder
|
|
1552
|
+
};
|
|
1553
|
+
if (!nextToolCall.toolCallId) {
|
|
1554
|
+
nextToolCall.toolCallId = String(update.toolCallId || '');
|
|
1108
1555
|
}
|
|
1109
|
-
if (
|
|
1110
|
-
|
|
1556
|
+
if (typeof nextToolCall.title !== 'string') {
|
|
1557
|
+
nextToolCall.title = '';
|
|
1111
1558
|
}
|
|
1112
|
-
|
|
1559
|
+
if (typeof nextToolCall.status !== 'string') {
|
|
1560
|
+
nextToolCall.status = 'pending';
|
|
1561
|
+
}
|
|
1562
|
+
return nextToolCall;
|
|
1113
1563
|
}
|
|
1114
1564
|
|
|
1115
1565
|
function normalizePersistedMessage(message = {}, fallbackOrder = 0) {
|
|
@@ -1192,12 +1642,26 @@ function normalizePersistedTerminalSummary(summary = {}) {
|
|
|
1192
1642
|
};
|
|
1193
1643
|
}
|
|
1194
1644
|
|
|
1645
|
+
export function getNextSyntheticStreamTurn(messages = []) {
|
|
1646
|
+
if (!Array.isArray(messages) || messages.length === 0) {
|
|
1647
|
+
return 0;
|
|
1648
|
+
}
|
|
1649
|
+
return messages.reduce((maxTurn, entry) => {
|
|
1650
|
+
const streamKey = String(entry?.streamKey || '');
|
|
1651
|
+
const match = /^synthetic:(\d+):/.exec(streamKey);
|
|
1652
|
+
if (!match) {
|
|
1653
|
+
return maxTurn;
|
|
1654
|
+
}
|
|
1655
|
+
const turn = Number.parseInt(match[1], 10);
|
|
1656
|
+
if (!Number.isFinite(turn)) {
|
|
1657
|
+
return maxTurn;
|
|
1658
|
+
}
|
|
1659
|
+
return Math.max(maxTurn, turn + 1);
|
|
1660
|
+
}, 0);
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1195
1663
|
function restorePersistedTabSnapshot(tab, snapshot = {}) {
|
|
1196
|
-
const messages =
|
|
1197
|
-
? snapshot.messages.map((message, index) =>
|
|
1198
|
-
normalizePersistedMessage(message, index + 1)
|
|
1199
|
-
)
|
|
1200
|
-
: [];
|
|
1664
|
+
const messages = normalizeAgentTranscriptMessages(snapshot.messages);
|
|
1201
1665
|
const toolCalls = Array.isArray(snapshot.toolCalls)
|
|
1202
1666
|
? snapshot.toolCalls.map((entry, index) =>
|
|
1203
1667
|
normalizePersistedTimelineEntry(
|
|
@@ -1279,6 +1743,13 @@ function restorePersistedTabSnapshot(tab, snapshot = {}) {
|
|
|
1279
1743
|
maxPermissionOrder
|
|
1280
1744
|
);
|
|
1281
1745
|
tab.messageCounter = Math.max(tab.messageCounter, messages.length);
|
|
1746
|
+
const maxSyntheticTurn = getNextSyntheticStreamTurn(messages);
|
|
1747
|
+
tab.syntheticStreamTurn = Math.max(
|
|
1748
|
+
Number.isFinite(tab.syntheticStreamTurn)
|
|
1749
|
+
? tab.syntheticStreamTurn
|
|
1750
|
+
: 0,
|
|
1751
|
+
maxSyntheticTurn
|
|
1752
|
+
);
|
|
1282
1753
|
}
|
|
1283
1754
|
|
|
1284
1755
|
class LocalExecTerminal extends EventEmitter {
|
|
@@ -1748,7 +2219,7 @@ class AcpRuntime extends EventEmitter {
|
|
|
1748
2219
|
syntheticStreams: new Map(),
|
|
1749
2220
|
syntheticStreamTurn: 0,
|
|
1750
2221
|
pendingUserEcho: null,
|
|
1751
|
-
|
|
2222
|
+
restoreCapture: null,
|
|
1752
2223
|
currentModeId,
|
|
1753
2224
|
availableModes,
|
|
1754
2225
|
availableCommands,
|
|
@@ -2005,14 +2476,20 @@ class AcpRuntime extends EventEmitter {
|
|
|
2005
2476
|
availableModes: meta.availableModes || [],
|
|
2006
2477
|
availableCommands: meta.availableCommands || [],
|
|
2007
2478
|
configOptions: meta.configOptions || [],
|
|
2008
|
-
messages:
|
|
2009
|
-
toolCalls:
|
|
2010
|
-
permissions:
|
|
2011
|
-
plan:
|
|
2479
|
+
messages: [],
|
|
2480
|
+
toolCalls: [],
|
|
2481
|
+
permissions: [],
|
|
2482
|
+
plan: [],
|
|
2012
2483
|
usage: meta.usage || null,
|
|
2013
2484
|
terminals: meta.terminals || []
|
|
2014
2485
|
});
|
|
2015
|
-
|
|
2486
|
+
// For loadSession-capable runtimes, transcript ordering comes from the
|
|
2487
|
+
// authoritative replay stream, not the persisted snapshot.
|
|
2488
|
+
tab.restoreCapture = createRestoreCaptureState(meta.messages || [], {
|
|
2489
|
+
toolCalls: meta.toolCalls || []
|
|
2490
|
+
});
|
|
2491
|
+
tab.restoreCapture.nextTimelineOrder = () =>
|
|
2492
|
+
this.#nextTimelineOrder(tab);
|
|
2016
2493
|
tab.status = 'restoring';
|
|
2017
2494
|
tab.busy = true;
|
|
2018
2495
|
|
|
@@ -2020,41 +2497,14 @@ class AcpRuntime extends EventEmitter {
|
|
|
2020
2497
|
this.sessionToTabId.set(tab.acpSessionId, tab.id);
|
|
2021
2498
|
|
|
2022
2499
|
try {
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2500
|
+
await this.#loadSessionIntoTab(tab, meta);
|
|
2501
|
+
this.#broadcast(tab, {
|
|
2502
|
+
type: 'snapshot',
|
|
2503
|
+
tab: this.serializeTab(tab)
|
|
2027
2504
|
});
|
|
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
2505
|
return this.serializeTab(tab);
|
|
2056
2506
|
} catch (error) {
|
|
2057
|
-
tab.
|
|
2507
|
+
tab.restoreCapture = null;
|
|
2058
2508
|
this.tabs.delete(tab.id);
|
|
2059
2509
|
this.sessionToTabId.delete(tab.acpSessionId);
|
|
2060
2510
|
throw error;
|
|
@@ -2131,16 +2581,31 @@ class AcpRuntime extends EventEmitter {
|
|
|
2131
2581
|
createdAt: new Date().toISOString(),
|
|
2132
2582
|
title: meta.title || ''
|
|
2133
2583
|
});
|
|
2584
|
+
// Resume rebuilds transcript ordering from the runtime replay stream.
|
|
2585
|
+
tab.restoreCapture = createRestoreCaptureState([]);
|
|
2586
|
+
tab.restoreCapture.nextTimelineOrder = () =>
|
|
2587
|
+
this.#nextTimelineOrder(tab);
|
|
2134
2588
|
tab.status = 'restoring';
|
|
2135
2589
|
tab.busy = true;
|
|
2136
2590
|
|
|
2137
2591
|
this.tabs.set(tab.id, tab);
|
|
2138
2592
|
this.sessionToTabId.set(tab.acpSessionId, tab.id);
|
|
2139
2593
|
|
|
2594
|
+
try {
|
|
2595
|
+
await this.#loadSessionIntoTab(tab, meta);
|
|
2596
|
+
return this.serializeTab(tab);
|
|
2597
|
+
} catch (error) {
|
|
2598
|
+
this.tabs.delete(tab.id);
|
|
2599
|
+
this.sessionToTabId.delete(tab.acpSessionId);
|
|
2600
|
+
throw error;
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2604
|
+
async #loadSessionIntoTab(tab, meta) {
|
|
2140
2605
|
try {
|
|
2141
2606
|
const response = await this.connection.loadSession({
|
|
2142
2607
|
cwd: meta.cwd,
|
|
2143
|
-
sessionId:
|
|
2608
|
+
sessionId: tab.acpSessionId,
|
|
2144
2609
|
mcpServers: []
|
|
2145
2610
|
});
|
|
2146
2611
|
const restoredSessionId = response?.sessionId || meta.acpSessionId;
|
|
@@ -2166,13 +2631,16 @@ class AcpRuntime extends EventEmitter {
|
|
|
2166
2631
|
tab.configOptions,
|
|
2167
2632
|
response?.models
|
|
2168
2633
|
);
|
|
2634
|
+
const replacedMessages = finalizeRestoreCaptureMessages(tab);
|
|
2169
2635
|
tab.status = 'ready';
|
|
2170
2636
|
tab.busy = false;
|
|
2171
2637
|
tab.errorMessage = '';
|
|
2172
|
-
|
|
2638
|
+
if (replacedMessages) {
|
|
2639
|
+
this.#markTabDirty(tab);
|
|
2640
|
+
}
|
|
2641
|
+
return replacedMessages;
|
|
2173
2642
|
} catch (error) {
|
|
2174
|
-
|
|
2175
|
-
this.sessionToTabId.delete(tab.acpSessionId);
|
|
2643
|
+
tab.restoreCapture = null;
|
|
2176
2644
|
throw error;
|
|
2177
2645
|
}
|
|
2178
2646
|
}
|
|
@@ -2186,6 +2654,7 @@ class AcpRuntime extends EventEmitter {
|
|
|
2186
2654
|
}
|
|
2187
2655
|
|
|
2188
2656
|
serializeTab(tab) {
|
|
2657
|
+
tab.messages = normalizeAgentTranscriptMessages(tab.messages);
|
|
2189
2658
|
return {
|
|
2190
2659
|
id: tab.id,
|
|
2191
2660
|
runtimeId: tab.runtimeId,
|
|
@@ -2628,32 +3097,59 @@ class AcpRuntime extends EventEmitter {
|
|
|
2628
3097
|
}
|
|
2629
3098
|
|
|
2630
3099
|
async #handleSessionUpdate(params) {
|
|
3100
|
+
const update = params.update;
|
|
2631
3101
|
const tab = this.#getTabBySession(params.sessionId);
|
|
2632
3102
|
if (!tab) return;
|
|
2633
|
-
const update = params.update;
|
|
2634
3103
|
let broadcastUpdate = update;
|
|
2635
3104
|
let didChange = false;
|
|
3105
|
+
let suppressSessionUpdateBroadcast = false;
|
|
2636
3106
|
|
|
2637
3107
|
switch (update.sessionUpdate) {
|
|
2638
|
-
case 'agent_message_chunk':
|
|
2639
|
-
this.#appendContentChunk(
|
|
2640
|
-
|
|
3108
|
+
case 'agent_message_chunk': {
|
|
3109
|
+
const result = this.#appendContentChunk(
|
|
3110
|
+
tab,
|
|
3111
|
+
update,
|
|
3112
|
+
'assistant',
|
|
3113
|
+
'message'
|
|
3114
|
+
);
|
|
3115
|
+
didChange = !!result.didChange;
|
|
3116
|
+
suppressSessionUpdateBroadcast = !!result.suppressBroadcast;
|
|
2641
3117
|
break;
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
3118
|
+
}
|
|
3119
|
+
case 'agent_thought_chunk': {
|
|
3120
|
+
const result = this.#appendContentChunk(
|
|
3121
|
+
tab,
|
|
3122
|
+
update,
|
|
3123
|
+
'assistant',
|
|
3124
|
+
'thought'
|
|
3125
|
+
);
|
|
3126
|
+
didChange = !!result.didChange;
|
|
3127
|
+
suppressSessionUpdateBroadcast = !!result.suppressBroadcast;
|
|
2645
3128
|
break;
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
3129
|
+
}
|
|
3130
|
+
case 'user_message_chunk': {
|
|
3131
|
+
const result = this.#appendContentChunk(
|
|
3132
|
+
tab,
|
|
3133
|
+
update,
|
|
3134
|
+
'user',
|
|
3135
|
+
'message'
|
|
3136
|
+
);
|
|
3137
|
+
didChange = !!result.didChange;
|
|
3138
|
+
suppressSessionUpdateBroadcast = !!result.suppressBroadcast;
|
|
2649
3139
|
break;
|
|
3140
|
+
}
|
|
2650
3141
|
case 'tool_call': {
|
|
2651
3142
|
this.#advanceSyntheticStreamTurn(tab);
|
|
2652
|
-
const
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
3143
|
+
const baseline = tab.restoreCapture?.baselineToolCalls?.get(
|
|
3144
|
+
update.toolCallId
|
|
3145
|
+
) || null;
|
|
3146
|
+
const previous = tab.toolCalls.get(update.toolCallId) || null;
|
|
3147
|
+
const nextToolCall = buildRestoredToolCall(
|
|
3148
|
+
previous,
|
|
3149
|
+
baseline,
|
|
3150
|
+
update,
|
|
3151
|
+
() => this.#nextTimelineOrder(tab)
|
|
3152
|
+
);
|
|
2657
3153
|
tab.toolCalls.set(update.toolCallId, nextToolCall);
|
|
2658
3154
|
broadcastUpdate = nextToolCall;
|
|
2659
3155
|
didChange = true;
|
|
@@ -2661,17 +3157,16 @@ class AcpRuntime extends EventEmitter {
|
|
|
2661
3157
|
}
|
|
2662
3158
|
case 'tool_call_update': {
|
|
2663
3159
|
this.#advanceSyntheticStreamTurn(tab);
|
|
2664
|
-
const previous = tab.toolCalls.get(update.toolCallId) ||
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
};
|
|
3160
|
+
const previous = tab.toolCalls.get(update.toolCallId) || null;
|
|
3161
|
+
const baseline = tab.restoreCapture?.baselineToolCalls?.get(
|
|
3162
|
+
update.toolCallId
|
|
3163
|
+
) || null;
|
|
3164
|
+
const nextToolCall = buildRestoredToolCall(
|
|
3165
|
+
previous,
|
|
3166
|
+
baseline,
|
|
3167
|
+
update,
|
|
3168
|
+
() => this.#nextTimelineOrder(tab)
|
|
3169
|
+
);
|
|
2675
3170
|
tab.toolCalls.set(update.toolCallId, nextToolCall);
|
|
2676
3171
|
broadcastUpdate = nextToolCall;
|
|
2677
3172
|
didChange = true;
|
|
@@ -2715,17 +3210,19 @@ class AcpRuntime extends EventEmitter {
|
|
|
2715
3210
|
break;
|
|
2716
3211
|
}
|
|
2717
3212
|
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
3213
|
+
if (!suppressSessionUpdateBroadcast) {
|
|
3214
|
+
this.#broadcast(tab, {
|
|
3215
|
+
type: 'session_update',
|
|
3216
|
+
update: broadcastUpdate,
|
|
3217
|
+
tab: {
|
|
3218
|
+
title: tab.title,
|
|
3219
|
+
currentModeId: tab.currentModeId,
|
|
3220
|
+
availableModes: tab.availableModes,
|
|
3221
|
+
availableCommands: tab.availableCommands,
|
|
3222
|
+
configOptions: tab.configOptions
|
|
3223
|
+
}
|
|
3224
|
+
});
|
|
3225
|
+
}
|
|
2729
3226
|
if (didChange) {
|
|
2730
3227
|
this.#markTabDirty(tab);
|
|
2731
3228
|
}
|
|
@@ -2737,10 +3234,17 @@ class AcpRuntime extends EventEmitter {
|
|
|
2737
3234
|
? (content.text || '')
|
|
2738
3235
|
: `[${content.type}]`;
|
|
2739
3236
|
if (role === 'user' && kind === 'message' && this.#consumeUserEcho(tab, text)) {
|
|
2740
|
-
return
|
|
3237
|
+
return {
|
|
3238
|
+
didChange: false,
|
|
3239
|
+
suppressBroadcast: false
|
|
3240
|
+
};
|
|
2741
3241
|
}
|
|
2742
|
-
if (
|
|
2743
|
-
|
|
3242
|
+
if (tab.restoreCapture) {
|
|
3243
|
+
captureRestoreReplayChunk(tab.restoreCapture, update, role, kind, text);
|
|
3244
|
+
return {
|
|
3245
|
+
didChange: false,
|
|
3246
|
+
suppressBroadcast: true
|
|
3247
|
+
};
|
|
2744
3248
|
}
|
|
2745
3249
|
const streamKey = this.#getStreamKey(tab, update, role, kind);
|
|
2746
3250
|
const last = tab.messages[tab.messages.length - 1] || null;
|
|
@@ -2761,7 +3265,10 @@ class AcpRuntime extends EventEmitter {
|
|
|
2761
3265
|
kind,
|
|
2762
3266
|
text: appendedText
|
|
2763
3267
|
});
|
|
2764
|
-
return
|
|
3268
|
+
return {
|
|
3269
|
+
didChange: true,
|
|
3270
|
+
suppressBroadcast: false
|
|
3271
|
+
};
|
|
2765
3272
|
}
|
|
2766
3273
|
|
|
2767
3274
|
if (!update.messageId) {
|
|
@@ -2781,6 +3288,10 @@ class AcpRuntime extends EventEmitter {
|
|
|
2781
3288
|
type: 'message_open',
|
|
2782
3289
|
message
|
|
2783
3290
|
});
|
|
3291
|
+
return {
|
|
3292
|
+
didChange: true,
|
|
3293
|
+
suppressBroadcast: false
|
|
3294
|
+
};
|
|
2784
3295
|
}
|
|
2785
3296
|
|
|
2786
3297
|
#consumeUserEcho(tab, text) {
|
|
@@ -3538,8 +4049,8 @@ export class AcpManager {
|
|
|
3538
4049
|
}, this.transcriptPersistDelayMs);
|
|
3539
4050
|
}
|
|
3540
4051
|
|
|
3541
|
-
|
|
3542
|
-
|
|
4052
|
+
#getSerializedTabs() {
|
|
4053
|
+
const serializedTabs = Array.from(this.tabs.values()).map((entry) => {
|
|
3543
4054
|
const tab = entry.serialize();
|
|
3544
4055
|
return {
|
|
3545
4056
|
id: tab.id,
|
|
@@ -3577,6 +4088,21 @@ export class AcpManager {
|
|
|
3577
4088
|
: []
|
|
3578
4089
|
};
|
|
3579
4090
|
});
|
|
4091
|
+
return dedupeSerializedTabs(serializedTabs).tabs;
|
|
4092
|
+
}
|
|
4093
|
+
|
|
4094
|
+
#findOpenSerializedTabBySessionId(sessionId) {
|
|
4095
|
+
const targetSessionId = String(sessionId || '').trim();
|
|
4096
|
+
if (!targetSessionId) {
|
|
4097
|
+
return null;
|
|
4098
|
+
}
|
|
4099
|
+
return this.#getSerializedTabs().find(
|
|
4100
|
+
(tab) => String(tab?.acpSessionId || '').trim() === targetSessionId
|
|
4101
|
+
) || null;
|
|
4102
|
+
}
|
|
4103
|
+
|
|
4104
|
+
getPersistedTabs() {
|
|
4105
|
+
return this.#getSerializedTabs();
|
|
3580
4106
|
}
|
|
3581
4107
|
|
|
3582
4108
|
persistTabs() {
|
|
@@ -3608,15 +4134,15 @@ export class AcpManager {
|
|
|
3608
4134
|
restoring: this.restoring,
|
|
3609
4135
|
definitions: await this.listDefinitions(),
|
|
3610
4136
|
configs: await this.listAgentConfigs(),
|
|
3611
|
-
tabs:
|
|
4137
|
+
tabs: this.#getSerializedTabs()
|
|
3612
4138
|
};
|
|
3613
4139
|
}
|
|
3614
4140
|
|
|
3615
4141
|
async listInventory() {
|
|
4142
|
+
const tabs = this.#getSerializedTabs();
|
|
3616
4143
|
return {
|
|
3617
4144
|
restoring: this.restoring,
|
|
3618
|
-
tabs:
|
|
3619
|
-
const serialized = entry.serialize();
|
|
4145
|
+
tabs: tabs.map((serialized) => {
|
|
3620
4146
|
return {
|
|
3621
4147
|
id: serialized.id,
|
|
3622
4148
|
runtimeId: serialized.runtimeId,
|
|
@@ -3871,6 +4397,13 @@ export class AcpManager {
|
|
|
3871
4397
|
throw new Error(availability.reason || 'Agent unavailable');
|
|
3872
4398
|
}
|
|
3873
4399
|
|
|
4400
|
+
const existingTab = this.#findOpenSerializedTabBySessionId(
|
|
4401
|
+
options.sessionId
|
|
4402
|
+
);
|
|
4403
|
+
if (existingTab) {
|
|
4404
|
+
return existingTab;
|
|
4405
|
+
}
|
|
4406
|
+
|
|
3874
4407
|
const cwd = path.resolve(options.cwd || process.cwd());
|
|
3875
4408
|
const { runtimeEntry, createdRuntime, runtimeStoreKey } =
|
|
3876
4409
|
this.#ensureRuntimeEntry(definition, cwd);
|
|
@@ -3917,10 +4450,21 @@ export class AcpManager {
|
|
|
3917
4450
|
|
|
3918
4451
|
async restoreTabs(validTerminalSessionIds = new Set()) {
|
|
3919
4452
|
await this.ensureConfigsLoaded();
|
|
3920
|
-
const
|
|
4453
|
+
const dedupedTabs = dedupeSerializedTabs(await this.loadTabs());
|
|
4454
|
+
const entries = dedupedTabs.tabs;
|
|
3921
4455
|
let changed = false;
|
|
4456
|
+
if (dedupedTabs.changed) {
|
|
4457
|
+
changed = true;
|
|
4458
|
+
}
|
|
3922
4459
|
|
|
3923
4460
|
for (const meta of entries) {
|
|
4461
|
+
const existingTab = this.#findOpenSerializedTabBySessionId(
|
|
4462
|
+
meta.acpSessionId
|
|
4463
|
+
);
|
|
4464
|
+
if (existingTab) {
|
|
4465
|
+
changed = true;
|
|
4466
|
+
continue;
|
|
4467
|
+
}
|
|
3924
4468
|
if (
|
|
3925
4469
|
meta.terminalSessionId
|
|
3926
4470
|
&& !validTerminalSessionIds.has(meta.terminalSessionId)
|