viveworker 0.1.2 → 0.1.4
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/scripts/viveworker-bridge.mjs +1277 -63
- package/scripts/viveworker.mjs +2 -2
- package/web/app.css +242 -30
- package/web/app.js +357 -122
- package/web/i18n.js +15 -7
- package/web/sw.js +1 -1
|
@@ -10,6 +10,7 @@ import os from "node:os";
|
|
|
10
10
|
import path from "node:path";
|
|
11
11
|
import process from "node:process";
|
|
12
12
|
import { createInterface } from "node:readline";
|
|
13
|
+
import { inspect } from "node:util";
|
|
13
14
|
import { fileURLToPath } from "node:url";
|
|
14
15
|
import webPush from "web-push";
|
|
15
16
|
import { DEFAULT_LOCALE, SUPPORTED_LOCALES, localeDisplayName, normalizeLocale, resolveLocalePreference, t } from "../web/i18n.js";
|
|
@@ -33,7 +34,7 @@ const PAIRING_RATE_LIMIT_WINDOW_MS = 15 * 60 * 1000;
|
|
|
33
34
|
const PAIRING_RATE_LIMIT_MAX_ATTEMPTS = 8;
|
|
34
35
|
const DEFAULT_COMPLETION_REPLY_IMAGE_MAX_BYTES = 15 * 1024 * 1024;
|
|
35
36
|
const DEFAULT_COMPLETION_REPLY_UPLOAD_TTL_MS = 24 * 60 * 60 * 1000;
|
|
36
|
-
const MAX_COMPLETION_REPLY_IMAGE_COUNT =
|
|
37
|
+
const MAX_COMPLETION_REPLY_IMAGE_COUNT = 4;
|
|
37
38
|
|
|
38
39
|
const cli = parseCliArgs(process.argv.slice(2));
|
|
39
40
|
const envFile = resolveEnvFile(cli.envFile);
|
|
@@ -58,6 +59,7 @@ const runtime = {
|
|
|
58
59
|
sourceFile: "",
|
|
59
60
|
},
|
|
60
61
|
rolloutThreadLabels: new Map(),
|
|
62
|
+
rolloutThreadCwds: new Map(),
|
|
61
63
|
threadStates: new Map(),
|
|
62
64
|
threadOwnerClientIds: new Map(),
|
|
63
65
|
nativeApprovalsByToken: new Map(),
|
|
@@ -83,6 +85,7 @@ const restoredPendingPlanStateChanged = restorePendingPlanRequests({ config, run
|
|
|
83
85
|
const restoredPendingUserInputStateChanged = restorePendingUserInputRequests({ config, runtime, state });
|
|
84
86
|
runtime.recentHistoryItems = normalizeHistoryItems(state.recentHistoryItems ?? [], config.maxHistoryItems);
|
|
85
87
|
runtime.recentTimelineEntries = normalizeTimelineEntries(state.recentTimelineEntries ?? [], config.maxTimelineEntries);
|
|
88
|
+
const restoredTimelineImagePathsStateChanged = await backfillPersistedTimelineImagePaths({ config, runtime, state });
|
|
86
89
|
runtime.historyFileState.offset = Number(state.historyFileOffset) || 0;
|
|
87
90
|
runtime.historyFileState.sourceFile = cleanText(state.historyFileSourceFile ?? "");
|
|
88
91
|
|
|
@@ -216,6 +219,29 @@ function kindTitle(locale, kind) {
|
|
|
216
219
|
}
|
|
217
220
|
}
|
|
218
221
|
|
|
222
|
+
function looksLikeGeneratedThreadTitle(value) {
|
|
223
|
+
const normalized = cleanText(value || "");
|
|
224
|
+
if (!normalized.includes("|")) {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
const prefix = cleanText(normalized.split("|", 1)[0] || "");
|
|
228
|
+
if (!prefix) {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
const titleKeys = [
|
|
232
|
+
"server.title.userMessage",
|
|
233
|
+
"server.title.assistantCommentary",
|
|
234
|
+
"server.title.assistantFinal",
|
|
235
|
+
"server.title.approval",
|
|
236
|
+
"server.title.plan",
|
|
237
|
+
"server.title.planReady",
|
|
238
|
+
"server.title.choice",
|
|
239
|
+
"server.title.choiceReadOnly",
|
|
240
|
+
"server.title.complete",
|
|
241
|
+
];
|
|
242
|
+
return SUPPORTED_LOCALES.some((locale) => titleKeys.some((key) => t(locale, key) === prefix));
|
|
243
|
+
}
|
|
244
|
+
|
|
219
245
|
function formatLocalizedTitle(locale, baseKeyOrTitle, threadLabel) {
|
|
220
246
|
const baseTitle = baseKeyOrTitle.includes(".") ? t(locale, baseKeyOrTitle) : baseKeyOrTitle;
|
|
221
247
|
return formatTitle(baseTitle, threadLabel);
|
|
@@ -277,7 +303,7 @@ function normalizeHistoryItem(raw) {
|
|
|
277
303
|
const stableId = cleanText(raw.stableId ?? raw.id ?? "");
|
|
278
304
|
const kind = cleanText(raw.kind ?? "");
|
|
279
305
|
const title = cleanText(raw.title ?? "");
|
|
280
|
-
const messageText =
|
|
306
|
+
const messageText = normalizeTimelineMessageText(raw.messageText ?? "");
|
|
281
307
|
const createdAtMs = Number(raw.createdAtMs) || Date.now();
|
|
282
308
|
if (!stableId || !historyKinds.has(kind) || !title) {
|
|
283
309
|
return null;
|
|
@@ -292,6 +318,7 @@ function normalizeHistoryItem(raw) {
|
|
|
292
318
|
threadLabel: cleanText(raw.threadLabel ?? ""),
|
|
293
319
|
summary: normalizeNotificationText(raw.summary ?? "") || formatNotificationBody(messageText, 100) || "",
|
|
294
320
|
messageText,
|
|
321
|
+
imagePaths: normalizeTimelineImagePaths(raw.imagePaths ?? raw.localImagePaths ?? []),
|
|
295
322
|
createdAtMs,
|
|
296
323
|
readOnly: raw.readOnly !== false,
|
|
297
324
|
primaryLabel: cleanText(raw.primaryLabel ?? "") || "詳細",
|
|
@@ -365,7 +392,7 @@ function normalizeTimelineEntry(raw) {
|
|
|
365
392
|
return null;
|
|
366
393
|
}
|
|
367
394
|
|
|
368
|
-
const messageText =
|
|
395
|
+
const messageText = normalizeTimelineMessageText(raw.messageText ?? "");
|
|
369
396
|
const summary =
|
|
370
397
|
normalizeNotificationText(raw.summary ?? "") ||
|
|
371
398
|
formatNotificationBody(messageText, 180) ||
|
|
@@ -383,6 +410,7 @@ function normalizeTimelineEntry(raw) {
|
|
|
383
410
|
title,
|
|
384
411
|
summary,
|
|
385
412
|
messageText,
|
|
413
|
+
imagePaths: normalizeTimelineImagePaths(raw.imagePaths ?? raw.localImagePaths ?? []),
|
|
386
414
|
createdAtMs,
|
|
387
415
|
readOnly: raw.readOnly !== false,
|
|
388
416
|
primaryLabel: cleanText(raw.primaryLabel ?? "") || "詳細",
|
|
@@ -728,6 +756,9 @@ async function scanOnce({ config, runtime, state }) {
|
|
|
728
756
|
}
|
|
729
757
|
|
|
730
758
|
if (sessionIndexChanged || knownFilesChanged) {
|
|
759
|
+
if (knownFilesChanged) {
|
|
760
|
+
runtime.rolloutThreadCwds = new Map();
|
|
761
|
+
}
|
|
731
762
|
runtime.rolloutThreadLabels = await buildRolloutThreadLabelIndex(runtime.knownFiles, runtime.sessionIndex);
|
|
732
763
|
dirty = refreshResolvedThreadLabels({ config, runtime, state }) || dirty;
|
|
733
764
|
}
|
|
@@ -769,6 +800,27 @@ async function scanOnce({ config, runtime, state }) {
|
|
|
769
800
|
now,
|
|
770
801
|
});
|
|
771
802
|
dirty = dirty || historyTimelineChanged;
|
|
803
|
+
|
|
804
|
+
const timelineImageBackfillChanged = await backfillRecentTimelineEntryImages({
|
|
805
|
+
config,
|
|
806
|
+
runtime,
|
|
807
|
+
state,
|
|
808
|
+
});
|
|
809
|
+
dirty = dirty || timelineImageBackfillChanged;
|
|
810
|
+
|
|
811
|
+
const persistedTimelineImageBackfillChanged = await backfillPersistedTimelineImagePaths({
|
|
812
|
+
config,
|
|
813
|
+
runtime,
|
|
814
|
+
state,
|
|
815
|
+
});
|
|
816
|
+
dirty = dirty || persistedTimelineImageBackfillChanged;
|
|
817
|
+
|
|
818
|
+
const interruptedTimelineBackfillChanged = backfillInterruptedTimelineEntries({
|
|
819
|
+
config,
|
|
820
|
+
runtime,
|
|
821
|
+
state,
|
|
822
|
+
});
|
|
823
|
+
dirty = dirty || interruptedTimelineBackfillChanged;
|
|
772
824
|
}
|
|
773
825
|
|
|
774
826
|
dirty = cleanupExpiredPlanRequests({ runtime, state, now }) || dirty;
|
|
@@ -1181,6 +1233,242 @@ async function processHistoryTimelineFile({ config, runtime, state, now }) {
|
|
|
1181
1233
|
return dirty;
|
|
1182
1234
|
}
|
|
1183
1235
|
|
|
1236
|
+
async function backfillRecentTimelineEntryImages({ config, runtime, state }) {
|
|
1237
|
+
const candidates = runtime.recentTimelineEntries.filter(
|
|
1238
|
+
(entry) =>
|
|
1239
|
+
cleanText(entry?.kind || "") === "user_message" &&
|
|
1240
|
+
cleanText(entry?.threadId || "") &&
|
|
1241
|
+
normalizeTimelineImagePaths(entry?.imagePaths ?? []).length === 0
|
|
1242
|
+
);
|
|
1243
|
+
if (candidates.length === 0 || !Array.isArray(runtime.knownFiles) || runtime.knownFiles.length === 0) {
|
|
1244
|
+
return false;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
const fileCache = new Map();
|
|
1248
|
+
let changed = false;
|
|
1249
|
+
const nextEntries = runtime.recentTimelineEntries.map((entry) => ({ ...entry }));
|
|
1250
|
+
|
|
1251
|
+
for (let index = 0; index < nextEntries.length; index += 1) {
|
|
1252
|
+
const entry = nextEntries[index];
|
|
1253
|
+
if (
|
|
1254
|
+
cleanText(entry?.kind || "") !== "user_message" ||
|
|
1255
|
+
!cleanText(entry?.threadId || "") ||
|
|
1256
|
+
normalizeTimelineImagePaths(entry?.imagePaths ?? []).length > 0
|
|
1257
|
+
) {
|
|
1258
|
+
continue;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
const hydrated = await hydrateTimelineEntryImagesFromRollout({
|
|
1262
|
+
config,
|
|
1263
|
+
runtime,
|
|
1264
|
+
entry,
|
|
1265
|
+
fileCache,
|
|
1266
|
+
});
|
|
1267
|
+
if (!hydrated) {
|
|
1268
|
+
continue;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
nextEntries[index] = hydrated;
|
|
1272
|
+
changed = true;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
if (!changed) {
|
|
1276
|
+
return false;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
const normalized = normalizeTimelineEntries(nextEntries, config.maxTimelineEntries);
|
|
1280
|
+
runtime.recentTimelineEntries = normalized;
|
|
1281
|
+
state.recentTimelineEntries = normalized;
|
|
1282
|
+
return true;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
async function backfillPersistedTimelineImagePaths({ config, runtime, state }) {
|
|
1286
|
+
let changed = false;
|
|
1287
|
+
const nextEntries = [];
|
|
1288
|
+
for (const entry of runtime.recentTimelineEntries) {
|
|
1289
|
+
const nextImagePaths = await normalizePersistedTimelineImagePaths({
|
|
1290
|
+
config,
|
|
1291
|
+
state,
|
|
1292
|
+
imagePaths: entry?.imagePaths ?? [],
|
|
1293
|
+
});
|
|
1294
|
+
if (JSON.stringify(nextImagePaths) !== JSON.stringify(normalizeTimelineImagePaths(entry?.imagePaths ?? []))) {
|
|
1295
|
+
changed = true;
|
|
1296
|
+
nextEntries.push({
|
|
1297
|
+
...entry,
|
|
1298
|
+
imagePaths: nextImagePaths,
|
|
1299
|
+
});
|
|
1300
|
+
continue;
|
|
1301
|
+
}
|
|
1302
|
+
nextEntries.push(entry);
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
if (!changed) {
|
|
1306
|
+
return false;
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
const normalized = normalizeTimelineEntries(nextEntries, config.maxTimelineEntries);
|
|
1310
|
+
runtime.recentTimelineEntries = normalized;
|
|
1311
|
+
state.recentTimelineEntries = normalized;
|
|
1312
|
+
return true;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
function backfillInterruptedTimelineEntries({ config, runtime, state }) {
|
|
1316
|
+
const locale = normalizeLocale(config.defaultLocale) || DEFAULT_LOCALE;
|
|
1317
|
+
let changed = false;
|
|
1318
|
+
const nextEntries = runtime.recentTimelineEntries.map((entry) => {
|
|
1319
|
+
const nextMessageText = normalizeTimelineMessageText(entry?.messageText ?? "", locale);
|
|
1320
|
+
const nextSummary = normalizeNotificationText(entry?.summary ?? "", locale) || formatNotificationBody(nextMessageText, 180);
|
|
1321
|
+
if (nextMessageText === (entry?.messageText ?? "") && nextSummary === (entry?.summary ?? "")) {
|
|
1322
|
+
return entry;
|
|
1323
|
+
}
|
|
1324
|
+
changed = true;
|
|
1325
|
+
return {
|
|
1326
|
+
...entry,
|
|
1327
|
+
messageText: nextMessageText,
|
|
1328
|
+
summary: nextSummary,
|
|
1329
|
+
};
|
|
1330
|
+
});
|
|
1331
|
+
|
|
1332
|
+
if (!changed) {
|
|
1333
|
+
return false;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
runtime.recentTimelineEntries = nextEntries;
|
|
1337
|
+
state.recentTimelineEntries = nextEntries;
|
|
1338
|
+
return true;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
async function hydrateTimelineEntryImagesFromRollout({ config, runtime, entry, fileCache }) {
|
|
1342
|
+
const threadId = cleanText(entry?.threadId || "");
|
|
1343
|
+
if (!threadId) {
|
|
1344
|
+
return null;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
const rolloutFile = findRolloutFileForThread(runtime, threadId);
|
|
1348
|
+
if (!rolloutFile) {
|
|
1349
|
+
return null;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
let recentMessages = fileCache.get(rolloutFile);
|
|
1353
|
+
if (!recentMessages) {
|
|
1354
|
+
recentMessages = await readRecentRolloutUserMessagesWithImages({
|
|
1355
|
+
filePath: rolloutFile,
|
|
1356
|
+
maxBytes: Math.max(config.maxReadBytes * 4, 1024 * 1024),
|
|
1357
|
+
});
|
|
1358
|
+
fileCache.set(rolloutFile, recentMessages);
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
if (!recentMessages.length) {
|
|
1362
|
+
return null;
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
const entryCreatedAtMs = Number(entry?.createdAtMs) || 0;
|
|
1366
|
+
const entryMessageText = normalizeTimelineMessageText(entry?.messageText ?? "");
|
|
1367
|
+
let bestMatch = null;
|
|
1368
|
+
|
|
1369
|
+
for (const candidate of recentMessages) {
|
|
1370
|
+
const candidateCreatedAtMs = Number(candidate?.createdAtMs) || 0;
|
|
1371
|
+
if (entryCreatedAtMs && candidateCreatedAtMs && Math.abs(candidateCreatedAtMs - entryCreatedAtMs) > 15_000) {
|
|
1372
|
+
continue;
|
|
1373
|
+
}
|
|
1374
|
+
if (entryMessageText && normalizeTimelineMessageText(candidate?.messageText ?? "") !== entryMessageText) {
|
|
1375
|
+
continue;
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
if (!bestMatch) {
|
|
1379
|
+
bestMatch = candidate;
|
|
1380
|
+
continue;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
const previousDiff = Math.abs((Number(bestMatch.createdAtMs) || 0) - entryCreatedAtMs);
|
|
1384
|
+
const nextDiff = Math.abs(candidateCreatedAtMs - entryCreatedAtMs);
|
|
1385
|
+
if (nextDiff < previousDiff) {
|
|
1386
|
+
bestMatch = candidate;
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
if (!bestMatch || normalizeTimelineImagePaths(bestMatch.imagePaths).length === 0) {
|
|
1391
|
+
return null;
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
const nextMessageText = normalizeTimelineMessageText(bestMatch.messageText ?? entryMessageText);
|
|
1395
|
+
return normalizeTimelineEntry({
|
|
1396
|
+
...entry,
|
|
1397
|
+
messageText: nextMessageText,
|
|
1398
|
+
summary: formatNotificationBody(nextMessageText, 180) || cleanText(entry.summary || ""),
|
|
1399
|
+
imagePaths: bestMatch.imagePaths,
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
function findRolloutFileForThread(runtime, threadId) {
|
|
1404
|
+
const normalizedThreadId = cleanText(threadId || "");
|
|
1405
|
+
if (!normalizedThreadId) {
|
|
1406
|
+
return "";
|
|
1407
|
+
}
|
|
1408
|
+
return (
|
|
1409
|
+
(Array.isArray(runtime.knownFiles) ? runtime.knownFiles : []).find(
|
|
1410
|
+
(filePath) => extractThreadIdFromRolloutPath(filePath) === normalizedThreadId
|
|
1411
|
+
) || ""
|
|
1412
|
+
);
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
async function readRecentRolloutUserMessagesWithImages({ filePath, maxBytes }) {
|
|
1416
|
+
let stat;
|
|
1417
|
+
try {
|
|
1418
|
+
stat = await fs.stat(filePath);
|
|
1419
|
+
} catch {
|
|
1420
|
+
return [];
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
const readLength = Math.max(0, Math.min(Number(maxBytes) || 0, stat.size));
|
|
1424
|
+
const startOffset = Math.max(0, stat.size - readLength);
|
|
1425
|
+
let chunk = "";
|
|
1426
|
+
try {
|
|
1427
|
+
const handle = await fs.open(filePath, "r");
|
|
1428
|
+
const buffer = Buffer.alloc(readLength);
|
|
1429
|
+
try {
|
|
1430
|
+
await handle.read(buffer, 0, readLength, startOffset);
|
|
1431
|
+
} finally {
|
|
1432
|
+
await handle.close();
|
|
1433
|
+
}
|
|
1434
|
+
chunk = buffer.toString("utf8");
|
|
1435
|
+
} catch {
|
|
1436
|
+
return [];
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
const lines = chunk.split("\n");
|
|
1440
|
+
if (startOffset > 0 && lines.length > 0) {
|
|
1441
|
+
lines.shift();
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
const matches = [];
|
|
1445
|
+
for (const rawLine of lines) {
|
|
1446
|
+
if (!rawLine.trim()) {
|
|
1447
|
+
continue;
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
let record;
|
|
1451
|
+
try {
|
|
1452
|
+
record = JSON.parse(rawLine);
|
|
1453
|
+
} catch {
|
|
1454
|
+
continue;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
const extracted = extractRolloutUserMessage(record);
|
|
1458
|
+
if (!extracted || normalizeTimelineImagePaths(extracted.imagePaths).length === 0) {
|
|
1459
|
+
continue;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
matches.push({
|
|
1463
|
+
createdAtMs: Date.parse(record.timestamp ?? "") || 0,
|
|
1464
|
+
messageText: extracted.messageText,
|
|
1465
|
+
imagePaths: extracted.imagePaths,
|
|
1466
|
+
});
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
return matches;
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1184
1472
|
async function querySqliteTimelineRows({ logsDbFile, cursorId, minTsSec = 0 }) {
|
|
1185
1473
|
const conditions = [
|
|
1186
1474
|
`id > ${Math.max(0, Number(cursorId) || 0)}`,
|
|
@@ -1272,9 +1560,31 @@ function buildSqliteTimelineEntry({ row, config, runtime }) {
|
|
|
1272
1560
|
});
|
|
1273
1561
|
}
|
|
1274
1562
|
|
|
1275
|
-
function
|
|
1563
|
+
function extractRolloutUserMessage(record) {
|
|
1276
1564
|
const payload = isPlainObject(record?.payload) ? record.payload : null;
|
|
1277
|
-
if (!payload
|
|
1565
|
+
if (!payload) {
|
|
1566
|
+
return null;
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
if (record?.type === "event_msg" && payload.type === "user_message") {
|
|
1570
|
+
const messageText = normalizeTimelineMessageText(payload.message ?? "");
|
|
1571
|
+
const imagePaths = normalizeTimelineImagePaths(payload.local_images ?? payload.localImagePaths ?? []);
|
|
1572
|
+
if (!messageText && imagePaths.length === 0) {
|
|
1573
|
+
return null;
|
|
1574
|
+
}
|
|
1575
|
+
return {
|
|
1576
|
+
itemId: cleanText(payload.turn_id || record.timestamp || ""),
|
|
1577
|
+
messageText,
|
|
1578
|
+
imagePaths,
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
if (payload.type !== "message" || payload.role !== "user") {
|
|
1583
|
+
return null;
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
if (rolloutContentHasImages(payload.content)) {
|
|
1587
|
+
// Prefer the richer event_msg.user_message entry when images are attached.
|
|
1278
1588
|
return null;
|
|
1279
1589
|
}
|
|
1280
1590
|
|
|
@@ -1283,13 +1593,139 @@ function buildRolloutUserTimelineEntry({ record, fileState, runtime }) {
|
|
|
1283
1593
|
return null;
|
|
1284
1594
|
}
|
|
1285
1595
|
|
|
1596
|
+
return {
|
|
1597
|
+
itemId: cleanText(payload.id || record.timestamp || ""),
|
|
1598
|
+
messageText,
|
|
1599
|
+
imagePaths: [],
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
function extractTimestampPrefixFromImagePath(filePath) {
|
|
1604
|
+
const match = path.basename(cleanText(filePath || "")).match(/^(\d{10,})-/u);
|
|
1605
|
+
return match ? Number(match[1]) || 0 : 0;
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
async function listReplyUploadFiles(config) {
|
|
1609
|
+
try {
|
|
1610
|
+
const entries = await fs.readdir(config.replyUploadsDir, { withFileTypes: true });
|
|
1611
|
+
return entries
|
|
1612
|
+
.filter((entry) => entry.isFile())
|
|
1613
|
+
.map((entry) => {
|
|
1614
|
+
const filePath = path.join(config.replyUploadsDir, entry.name);
|
|
1615
|
+
return {
|
|
1616
|
+
filePath,
|
|
1617
|
+
extension: path.extname(entry.name).toLowerCase(),
|
|
1618
|
+
ts: extractTimestampPrefixFromImagePath(entry.name),
|
|
1619
|
+
};
|
|
1620
|
+
})
|
|
1621
|
+
.filter((entry) => entry.ts > 0);
|
|
1622
|
+
} catch {
|
|
1623
|
+
return [];
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
async function findReplyUploadFallback(config, sourcePath, usedPaths = new Set()) {
|
|
1628
|
+
const targetTs = extractTimestampPrefixFromImagePath(sourcePath);
|
|
1629
|
+
const targetExtension = path.extname(cleanText(sourcePath || "")).toLowerCase();
|
|
1630
|
+
if (!targetTs) {
|
|
1631
|
+
return "";
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
const uploads = await listReplyUploadFiles(config);
|
|
1635
|
+
const candidates = uploads
|
|
1636
|
+
.filter((entry) => !usedPaths.has(entry.filePath))
|
|
1637
|
+
.filter((entry) => !targetExtension || entry.extension === targetExtension)
|
|
1638
|
+
.filter((entry) => Math.abs(entry.ts - targetTs) <= 60_000)
|
|
1639
|
+
.sort((left, right) => Math.abs(left.ts - targetTs) - Math.abs(right.ts - targetTs));
|
|
1640
|
+
|
|
1641
|
+
return candidates[0]?.filePath || "";
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
async function copyTimelineAttachmentToPersistentDir(config, sourcePath) {
|
|
1645
|
+
const normalizedSourcePath = resolvePath(cleanText(sourcePath || ""));
|
|
1646
|
+
if (!normalizedSourcePath) {
|
|
1647
|
+
return "";
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
await fs.mkdir(config.timelineAttachmentsDir, { recursive: true });
|
|
1651
|
+
const extension = path.extname(normalizedSourcePath) || ".img";
|
|
1652
|
+
const destinationPath = path.join(
|
|
1653
|
+
config.timelineAttachmentsDir,
|
|
1654
|
+
`${Date.now()}-${crypto.randomUUID()}${extension}`
|
|
1655
|
+
);
|
|
1656
|
+
await fs.copyFile(normalizedSourcePath, destinationPath);
|
|
1657
|
+
return destinationPath;
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
async function normalizePersistedTimelineImagePaths({ config, state, imagePaths = [] }) {
|
|
1661
|
+
const normalizedImagePaths = normalizeTimelineImagePaths(imagePaths);
|
|
1662
|
+
if (normalizedImagePaths.length === 0) {
|
|
1663
|
+
return [];
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
const aliases = isPlainObject(state.timelineImagePathAliases) ? state.timelineImagePathAliases : (state.timelineImagePathAliases = {});
|
|
1667
|
+
const usedFallbacks = new Set();
|
|
1668
|
+
const nextPaths = [];
|
|
1669
|
+
|
|
1670
|
+
for (const rawPath of normalizedImagePaths) {
|
|
1671
|
+
const normalizedPath = cleanText(rawPath || "");
|
|
1672
|
+
if (!normalizedPath) {
|
|
1673
|
+
continue;
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
const aliasedPath = cleanText(aliases[normalizedPath] || "");
|
|
1677
|
+
if (aliasedPath) {
|
|
1678
|
+
try {
|
|
1679
|
+
await fs.access(aliasedPath);
|
|
1680
|
+
nextPaths.push(aliasedPath);
|
|
1681
|
+
continue;
|
|
1682
|
+
} catch {
|
|
1683
|
+
// Fall through and repair below.
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
let existingSourcePath = normalizedPath;
|
|
1688
|
+
try {
|
|
1689
|
+
await fs.access(existingSourcePath);
|
|
1690
|
+
} catch {
|
|
1691
|
+
existingSourcePath = await findReplyUploadFallback(config, normalizedPath, usedFallbacks);
|
|
1692
|
+
if (!existingSourcePath) {
|
|
1693
|
+
continue;
|
|
1694
|
+
}
|
|
1695
|
+
usedFallbacks.add(existingSourcePath);
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
let persistentPath = existingSourcePath;
|
|
1699
|
+
if (!existingSourcePath.startsWith(`${config.timelineAttachmentsDir}${path.sep}`)) {
|
|
1700
|
+
persistentPath = await copyTimelineAttachmentToPersistentDir(config, existingSourcePath);
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
aliases[normalizedPath] = persistentPath;
|
|
1704
|
+
nextPaths.push(persistentPath);
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
return normalizeTimelineImagePaths(nextPaths);
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
function buildRolloutUserTimelineEntry({ record, fileState, runtime }) {
|
|
1711
|
+
const extracted = extractRolloutUserMessage(record);
|
|
1712
|
+
if (!extracted) {
|
|
1713
|
+
return null;
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1286
1716
|
const threadId = cleanText(fileState.threadId || "");
|
|
1287
1717
|
if (!threadId) {
|
|
1288
1718
|
return null;
|
|
1289
1719
|
}
|
|
1290
1720
|
|
|
1291
1721
|
const createdAtMs = Date.parse(record.timestamp ?? "") || Date.now();
|
|
1292
|
-
const stableId = messageTimelineStableId(
|
|
1722
|
+
const stableId = messageTimelineStableId(
|
|
1723
|
+
"user_message",
|
|
1724
|
+
threadId,
|
|
1725
|
+
extracted.itemId,
|
|
1726
|
+
extracted.messageText,
|
|
1727
|
+
createdAtMs
|
|
1728
|
+
);
|
|
1293
1729
|
const threadLabel = getNativeThreadLabel({
|
|
1294
1730
|
runtime,
|
|
1295
1731
|
conversationId: threadId,
|
|
@@ -1303,8 +1739,9 @@ function buildRolloutUserTimelineEntry({ record, fileState, runtime }) {
|
|
|
1303
1739
|
threadId,
|
|
1304
1740
|
threadLabel,
|
|
1305
1741
|
title: threadLabel || kindTitle(DEFAULT_LOCALE, "user_message"),
|
|
1306
|
-
summary: formatNotificationBody(messageText, 180) || messageText,
|
|
1307
|
-
messageText,
|
|
1742
|
+
summary: formatNotificationBody(extracted.messageText, 180) || extracted.messageText,
|
|
1743
|
+
messageText: extracted.messageText,
|
|
1744
|
+
imagePaths: extracted.imagePaths,
|
|
1308
1745
|
createdAtMs,
|
|
1309
1746
|
readOnly: true,
|
|
1310
1747
|
});
|
|
@@ -1316,7 +1753,7 @@ function buildHistoryUserTimelineEntry({ record, runtime, config }) {
|
|
|
1316
1753
|
}
|
|
1317
1754
|
|
|
1318
1755
|
const threadId = cleanText(record.session_id || record.sessionId || "");
|
|
1319
|
-
const messageText =
|
|
1756
|
+
const messageText = normalizeTimelineMessageText(record.text ?? "");
|
|
1320
1757
|
if (!threadId || !messageText) {
|
|
1321
1758
|
return null;
|
|
1322
1759
|
}
|
|
@@ -3303,19 +3740,142 @@ function runCurl(args) {
|
|
|
3303
3740
|
}
|
|
3304
3741
|
|
|
3305
3742
|
const IMPLEMENT_PLAN_PROMPT_PREFIX = "PLEASE IMPLEMENT THIS PLAN:";
|
|
3743
|
+
const COMPLETION_REPLY_WORKSPACE_STAGE_DIR = ".viveworker-attachments";
|
|
3744
|
+
const COMPLETION_REPLY_WORKSPACE_STAGE_TTL_MS = 60 * 60 * 1000;
|
|
3745
|
+
const COMPLETION_REPLY_WORKSPACE_STAGE_CLEANUP_DELAY_MS = 10 * 60 * 1000;
|
|
3306
3746
|
|
|
3307
3747
|
function buildImplementPlanPrompt(planContent) {
|
|
3308
3748
|
return `${IMPLEMENT_PLAN_PROMPT_PREFIX}\n${formatPlanDetailText(planContent)}`;
|
|
3309
3749
|
}
|
|
3310
3750
|
|
|
3311
|
-
function
|
|
3312
|
-
|
|
3313
|
-
|
|
3751
|
+
function buildTurnInput(text, options = {}) {
|
|
3752
|
+
const items = [];
|
|
3753
|
+
const normalizedText = String(text ?? "");
|
|
3754
|
+
const localImagePaths = Array.isArray(options?.localImagePaths)
|
|
3755
|
+
? options.localImagePaths
|
|
3756
|
+
.map((value) => cleanText(value || ""))
|
|
3757
|
+
.filter(Boolean)
|
|
3758
|
+
: [];
|
|
3759
|
+
|
|
3760
|
+
if (normalizedText) {
|
|
3761
|
+
items.push({
|
|
3314
3762
|
type: "text",
|
|
3315
|
-
text:
|
|
3763
|
+
text: normalizedText,
|
|
3316
3764
|
text_elements: [],
|
|
3317
|
-
}
|
|
3318
|
-
|
|
3765
|
+
});
|
|
3766
|
+
}
|
|
3767
|
+
|
|
3768
|
+
for (const localImagePath of localImagePaths) {
|
|
3769
|
+
items.push({
|
|
3770
|
+
type: "local_image",
|
|
3771
|
+
path: localImagePath,
|
|
3772
|
+
});
|
|
3773
|
+
}
|
|
3774
|
+
|
|
3775
|
+
return items;
|
|
3776
|
+
}
|
|
3777
|
+
|
|
3778
|
+
function buildComposerStyleLocalImageInput(text, localImagePaths = []) {
|
|
3779
|
+
const items = [];
|
|
3780
|
+
const normalizedText = String(text ?? "");
|
|
3781
|
+
|
|
3782
|
+
if (normalizedText) {
|
|
3783
|
+
items.push({
|
|
3784
|
+
type: "text",
|
|
3785
|
+
text: normalizedText,
|
|
3786
|
+
text_elements: [],
|
|
3787
|
+
});
|
|
3788
|
+
}
|
|
3789
|
+
|
|
3790
|
+
for (const localImagePath of localImagePaths) {
|
|
3791
|
+
const normalizedPath = cleanText(localImagePath || "");
|
|
3792
|
+
if (!normalizedPath) {
|
|
3793
|
+
continue;
|
|
3794
|
+
}
|
|
3795
|
+
items.push({
|
|
3796
|
+
type: "localImage",
|
|
3797
|
+
path: normalizedPath,
|
|
3798
|
+
});
|
|
3799
|
+
}
|
|
3800
|
+
|
|
3801
|
+
return items;
|
|
3802
|
+
}
|
|
3803
|
+
|
|
3804
|
+
function buildComposerStyleImageInput(text, imageDataUrls = []) {
|
|
3805
|
+
const items = [];
|
|
3806
|
+
const normalizedText = String(text ?? "");
|
|
3807
|
+
|
|
3808
|
+
if (normalizedText) {
|
|
3809
|
+
items.push({
|
|
3810
|
+
type: "text",
|
|
3811
|
+
text: normalizedText,
|
|
3812
|
+
text_elements: [],
|
|
3813
|
+
});
|
|
3814
|
+
}
|
|
3815
|
+
|
|
3816
|
+
for (const imageUrl of imageDataUrls) {
|
|
3817
|
+
const normalizedUrl = cleanText(imageUrl || "");
|
|
3818
|
+
if (!normalizedUrl) {
|
|
3819
|
+
continue;
|
|
3820
|
+
}
|
|
3821
|
+
items.push({
|
|
3822
|
+
type: "image",
|
|
3823
|
+
url: normalizedUrl,
|
|
3824
|
+
});
|
|
3825
|
+
}
|
|
3826
|
+
|
|
3827
|
+
return items;
|
|
3828
|
+
}
|
|
3829
|
+
|
|
3830
|
+
function buildUserInputPayload(items, finalOutputJsonSchema = null) {
|
|
3831
|
+
return {
|
|
3832
|
+
items: Array.isArray(items) ? items : [],
|
|
3833
|
+
final_output_json_schema: finalOutputJsonSchema ?? null,
|
|
3834
|
+
};
|
|
3835
|
+
}
|
|
3836
|
+
|
|
3837
|
+
function buildTurnContentItems(text, imageDataUrls = []) {
|
|
3838
|
+
const items = [];
|
|
3839
|
+
const normalizedText = String(text ?? "");
|
|
3840
|
+
if (normalizedText) {
|
|
3841
|
+
items.push({
|
|
3842
|
+
type: "input_text",
|
|
3843
|
+
text: normalizedText,
|
|
3844
|
+
});
|
|
3845
|
+
}
|
|
3846
|
+
for (const imageUrl of imageDataUrls) {
|
|
3847
|
+
if (!imageUrl) {
|
|
3848
|
+
continue;
|
|
3849
|
+
}
|
|
3850
|
+
items.push({
|
|
3851
|
+
type: "input_image",
|
|
3852
|
+
image_url: imageUrl,
|
|
3853
|
+
detail: "original",
|
|
3854
|
+
});
|
|
3855
|
+
}
|
|
3856
|
+
return items;
|
|
3857
|
+
}
|
|
3858
|
+
|
|
3859
|
+
function buildTurnImageItems(text, imageDataUrls = []) {
|
|
3860
|
+
const items = [];
|
|
3861
|
+
const normalizedText = String(text ?? "");
|
|
3862
|
+
if (normalizedText) {
|
|
3863
|
+
items.push({
|
|
3864
|
+
type: "text",
|
|
3865
|
+
text: normalizedText,
|
|
3866
|
+
text_elements: [],
|
|
3867
|
+
});
|
|
3868
|
+
}
|
|
3869
|
+
for (const imageUrl of imageDataUrls) {
|
|
3870
|
+
if (!imageUrl) {
|
|
3871
|
+
continue;
|
|
3872
|
+
}
|
|
3873
|
+
items.push({
|
|
3874
|
+
type: "image",
|
|
3875
|
+
image_url: imageUrl,
|
|
3876
|
+
});
|
|
3877
|
+
}
|
|
3878
|
+
return items;
|
|
3319
3879
|
}
|
|
3320
3880
|
|
|
3321
3881
|
function buildRequestedCollaborationMode(threadState, requestedMode = "default") {
|
|
@@ -3338,6 +3898,49 @@ function buildRequestedCollaborationMode(threadState, requestedMode = "default")
|
|
|
3338
3898
|
};
|
|
3339
3899
|
}
|
|
3340
3900
|
|
|
3901
|
+
function normalizeIpcErrorMessage(errorValue) {
|
|
3902
|
+
if (typeof errorValue === "string") {
|
|
3903
|
+
return cleanText(errorValue || "") || "ipc-request-failed";
|
|
3904
|
+
}
|
|
3905
|
+
if (errorValue instanceof Error) {
|
|
3906
|
+
return cleanText(errorValue.message || "") || errorValue.name || "ipc-request-failed";
|
|
3907
|
+
}
|
|
3908
|
+
if (Array.isArray(errorValue)) {
|
|
3909
|
+
try {
|
|
3910
|
+
return JSON.stringify(errorValue);
|
|
3911
|
+
} catch {
|
|
3912
|
+
return "ipc-request-failed";
|
|
3913
|
+
}
|
|
3914
|
+
}
|
|
3915
|
+
if (isPlainObject(errorValue)) {
|
|
3916
|
+
const candidateFields = [
|
|
3917
|
+
errorValue.message,
|
|
3918
|
+
errorValue.error,
|
|
3919
|
+
errorValue.details,
|
|
3920
|
+
errorValue.reason,
|
|
3921
|
+
];
|
|
3922
|
+
const directMessage = candidateFields
|
|
3923
|
+
.map((value) => (typeof value === "string" ? cleanText(value || "") : ""))
|
|
3924
|
+
.find(Boolean);
|
|
3925
|
+
if (directMessage) {
|
|
3926
|
+
return directMessage;
|
|
3927
|
+
}
|
|
3928
|
+
try {
|
|
3929
|
+
return JSON.stringify(errorValue);
|
|
3930
|
+
} catch {
|
|
3931
|
+
return "ipc-request-failed";
|
|
3932
|
+
}
|
|
3933
|
+
}
|
|
3934
|
+
if (errorValue && typeof errorValue === "object") {
|
|
3935
|
+
try {
|
|
3936
|
+
return JSON.stringify(errorValue);
|
|
3937
|
+
} catch {
|
|
3938
|
+
return "ipc-request-failed";
|
|
3939
|
+
}
|
|
3940
|
+
}
|
|
3941
|
+
return cleanText(String(errorValue ?? "")) || "ipc-request-failed";
|
|
3942
|
+
}
|
|
3943
|
+
|
|
3341
3944
|
function buildDefaultCollaborationMode(threadState) {
|
|
3342
3945
|
// Fallback turns must leave Plan mode unless the caller explicitly opts in.
|
|
3343
3946
|
return buildRequestedCollaborationMode(threadState, "default");
|
|
@@ -3417,6 +4020,18 @@ class NativeIpcClient {
|
|
|
3417
4020
|
);
|
|
3418
4021
|
}
|
|
3419
4022
|
|
|
4023
|
+
async startTurnDirect(conversationId, turnStartParams, ownerClientId = null) {
|
|
4024
|
+
const targetClientId =
|
|
4025
|
+
ownerClientId ??
|
|
4026
|
+
this.runtime.threadOwnerClientIds.get(conversationId) ??
|
|
4027
|
+
null;
|
|
4028
|
+
return this.sendRequest(
|
|
4029
|
+
"turn/start",
|
|
4030
|
+
buildDirectTurnStartPayload(conversationId, turnStartParams),
|
|
4031
|
+
{ targetClientId }
|
|
4032
|
+
);
|
|
4033
|
+
}
|
|
4034
|
+
|
|
3420
4035
|
async submitUserInputRequest(conversationId, requestId, answers, ownerClientId = null) {
|
|
3421
4036
|
return this.sendThreadFollowerRequest(
|
|
3422
4037
|
"thread-follower-submit-user-input-request",
|
|
@@ -3573,7 +4188,14 @@ class NativeIpcClient {
|
|
|
3573
4188
|
clearTimeout(pending.timeout);
|
|
3574
4189
|
|
|
3575
4190
|
if (message.resultType === "error") {
|
|
3576
|
-
|
|
4191
|
+
console.log(
|
|
4192
|
+
`[ipc] error method=${cleanText(message.method || "") || "unknown"} requestId=${cleanText(message.requestId || "") || "unknown"} payload=${inspect(message.error, { depth: 6, breakLength: 160 })}`
|
|
4193
|
+
);
|
|
4194
|
+
const error = new Error(normalizeIpcErrorMessage(message.error));
|
|
4195
|
+
if (message.error && typeof message.error === "object") {
|
|
4196
|
+
error.ipcError = message.error;
|
|
4197
|
+
}
|
|
4198
|
+
pending.reject(error);
|
|
3577
4199
|
return;
|
|
3578
4200
|
}
|
|
3579
4201
|
|
|
@@ -3960,6 +4582,64 @@ function getNativeThreadLabel({ runtime, conversationId, cwd }) {
|
|
|
3960
4582
|
return shortId(normalizedConversationId) || "Codex task";
|
|
3961
4583
|
}
|
|
3962
4584
|
|
|
4585
|
+
async function findRolloutThreadCwd(runtime, conversationId) {
|
|
4586
|
+
const normalizedConversationId = cleanText(conversationId || "");
|
|
4587
|
+
if (!normalizedConversationId) {
|
|
4588
|
+
return "";
|
|
4589
|
+
}
|
|
4590
|
+
|
|
4591
|
+
const cachedCwd = resolvePath(cleanText(runtime.rolloutThreadCwds?.get(normalizedConversationId) || ""));
|
|
4592
|
+
if (cachedCwd) {
|
|
4593
|
+
return cachedCwd;
|
|
4594
|
+
}
|
|
4595
|
+
|
|
4596
|
+
const knownFiles = Array.isArray(runtime.knownFiles) ? runtime.knownFiles : [];
|
|
4597
|
+
if (!knownFiles.length) {
|
|
4598
|
+
return "";
|
|
4599
|
+
}
|
|
4600
|
+
|
|
4601
|
+
const prioritizedFiles = [];
|
|
4602
|
+
const fallbackFiles = [];
|
|
4603
|
+
for (const filePath of knownFiles) {
|
|
4604
|
+
if (extractThreadIdFromRolloutPath(filePath) === normalizedConversationId) {
|
|
4605
|
+
prioritizedFiles.push(filePath);
|
|
4606
|
+
} else {
|
|
4607
|
+
fallbackFiles.push(filePath);
|
|
4608
|
+
}
|
|
4609
|
+
}
|
|
4610
|
+
|
|
4611
|
+
const filesToInspect = prioritizedFiles.length ? [...prioritizedFiles, ...fallbackFiles] : fallbackFiles;
|
|
4612
|
+
for (const filePath of filesToInspect) {
|
|
4613
|
+
const metadata = await extractRolloutThreadMetadata(filePath);
|
|
4614
|
+
if (cleanText(metadata?.threadId || "") !== normalizedConversationId) {
|
|
4615
|
+
continue;
|
|
4616
|
+
}
|
|
4617
|
+
const resolvedCwd = resolvePath(cleanText(metadata?.cwd || ""));
|
|
4618
|
+
if (resolvedCwd) {
|
|
4619
|
+
runtime.rolloutThreadCwds.set(normalizedConversationId, resolvedCwd);
|
|
4620
|
+
return resolvedCwd;
|
|
4621
|
+
}
|
|
4622
|
+
}
|
|
4623
|
+
|
|
4624
|
+
return "";
|
|
4625
|
+
}
|
|
4626
|
+
|
|
4627
|
+
async function resolveConversationCwd(runtime, conversationId) {
|
|
4628
|
+
const normalizedConversationId = cleanText(conversationId || "");
|
|
4629
|
+
if (!normalizedConversationId) {
|
|
4630
|
+
return "";
|
|
4631
|
+
}
|
|
4632
|
+
|
|
4633
|
+
const threadStateCwd = resolvePath(
|
|
4634
|
+
cleanText(runtime.threadStates.get(normalizedConversationId)?.cwd || "")
|
|
4635
|
+
);
|
|
4636
|
+
if (threadStateCwd) {
|
|
4637
|
+
return threadStateCwd;
|
|
4638
|
+
}
|
|
4639
|
+
|
|
4640
|
+
return await findRolloutThreadCwd(runtime, normalizedConversationId);
|
|
4641
|
+
}
|
|
4642
|
+
|
|
3963
4643
|
function formatNativeApprovalMessage(kind, params, locale = config?.defaultLocale || DEFAULT_LOCALE) {
|
|
3964
4644
|
if (kind === "command") {
|
|
3965
4645
|
return formatCommandApprovalMessage(params, locale);
|
|
@@ -4409,6 +5089,7 @@ function readSession(req, config, state) {
|
|
|
4409
5089
|
pairedAtMs: Number(payload.pairedAtMs) || 0,
|
|
4410
5090
|
expiresAtMs: Number(payload.expiresAtMs) || 0,
|
|
4411
5091
|
deviceId,
|
|
5092
|
+
temporaryPairing: payload?.temporaryPairing === true,
|
|
4412
5093
|
};
|
|
4413
5094
|
}
|
|
4414
5095
|
|
|
@@ -4466,6 +5147,22 @@ function setSessionCookie(res, config) {
|
|
|
4466
5147
|
}));
|
|
4467
5148
|
}
|
|
4468
5149
|
|
|
5150
|
+
function setTemporarySessionCookie(res, config) {
|
|
5151
|
+
const secure = config.nativeApprovalPublicBaseUrl.startsWith("https://");
|
|
5152
|
+
const now = Date.now();
|
|
5153
|
+
const token = signSessionPayload({
|
|
5154
|
+
sessionId: crypto.randomUUID(),
|
|
5155
|
+
pairedAtMs: now,
|
|
5156
|
+
expiresAtMs: now + config.sessionTtlMs,
|
|
5157
|
+
temporaryPairing: true,
|
|
5158
|
+
}, config.sessionSecret);
|
|
5159
|
+
res.setHeader("Set-Cookie", buildSetCookieHeader({
|
|
5160
|
+
value: token,
|
|
5161
|
+
maxAgeSecs: Math.max(1, Math.floor(config.sessionTtlMs / 1000)),
|
|
5162
|
+
secure,
|
|
5163
|
+
}));
|
|
5164
|
+
}
|
|
5165
|
+
|
|
4469
5166
|
function clearSessionCookie(res, config) {
|
|
4470
5167
|
const secure = config.nativeApprovalPublicBaseUrl.startsWith("https://");
|
|
4471
5168
|
res.setHeader("Set-Cookie", buildSetCookieHeader({ value: "", maxAgeSecs: 0, secure }));
|
|
@@ -4623,15 +5320,27 @@ function pairingCredentialConsumed(config, state) {
|
|
|
4623
5320
|
}
|
|
4624
5321
|
|
|
4625
5322
|
function isPairingAvailableForState(config, state) {
|
|
4626
|
-
return isPairingAvailable(config) && !
|
|
5323
|
+
return isPairingAvailable(config) && !pairingCodeConsumed(config, state);
|
|
5324
|
+
}
|
|
5325
|
+
|
|
5326
|
+
function pairingCodeConsumed(config, state) {
|
|
5327
|
+
const code = cleanText(config?.pairingCode ?? "").toUpperCase();
|
|
5328
|
+
if (!code) {
|
|
5329
|
+
return false;
|
|
5330
|
+
}
|
|
5331
|
+
const consumedAtMs = Number(state?.pairingConsumedAt) || 0;
|
|
5332
|
+
const consumedCredential = cleanText(state?.pairingConsumedCredential ?? "");
|
|
5333
|
+
return consumedAtMs > 0 && consumedCredential === `code:${code}`;
|
|
4627
5334
|
}
|
|
4628
5335
|
|
|
4629
|
-
function markPairingConsumed(state,
|
|
4630
|
-
const current =
|
|
5336
|
+
function markPairingConsumed(state, credential, now = Date.now()) {
|
|
5337
|
+
const current = cleanText(credential || "");
|
|
4631
5338
|
if (!current) {
|
|
4632
5339
|
return false;
|
|
4633
5340
|
}
|
|
4634
|
-
|
|
5341
|
+
const consumedAtMs = Number(state?.pairingConsumedAt) || 0;
|
|
5342
|
+
const consumedCredential = cleanText(state?.pairingConsumedCredential ?? "");
|
|
5343
|
+
if (consumedAtMs > 0 && consumedCredential === current) {
|
|
4635
5344
|
return false;
|
|
4636
5345
|
}
|
|
4637
5346
|
state.pairingConsumedAt = now;
|
|
@@ -4643,7 +5352,7 @@ function validatePairingPayload(payload, config, state) {
|
|
|
4643
5352
|
if (!config.authRequired) {
|
|
4644
5353
|
return { ok: true };
|
|
4645
5354
|
}
|
|
4646
|
-
if (!
|
|
5355
|
+
if (!isPairingAvailable(config)) {
|
|
4647
5356
|
return { ok: false, error: "pairing-unavailable" };
|
|
4648
5357
|
}
|
|
4649
5358
|
|
|
@@ -4651,10 +5360,16 @@ function validatePairingPayload(payload, config, state) {
|
|
|
4651
5360
|
const token = cleanText(payload?.token ?? "");
|
|
4652
5361
|
const matchesCode = code && cleanText(config.pairingCode).toUpperCase() === code;
|
|
4653
5362
|
const matchesToken = token && cleanText(config.pairingToken) === token;
|
|
4654
|
-
if (
|
|
4655
|
-
return { ok:
|
|
5363
|
+
if (matchesToken) {
|
|
5364
|
+
return { ok: true, credential: `token:${token}` };
|
|
5365
|
+
}
|
|
5366
|
+
if (matchesCode) {
|
|
5367
|
+
if (pairingCodeConsumed(config, state)) {
|
|
5368
|
+
return { ok: false, error: "pairing-unavailable" };
|
|
5369
|
+
}
|
|
5370
|
+
return { ok: true, credential: `code:${code}` };
|
|
4656
5371
|
}
|
|
4657
|
-
return { ok:
|
|
5372
|
+
return { ok: false, error: "invalid-pairing-credentials" };
|
|
4658
5373
|
}
|
|
4659
5374
|
|
|
4660
5375
|
function readRemoteAddress(req) {
|
|
@@ -5084,6 +5799,28 @@ function buildOperationalTimelineEntries(runtime, state, config, locale) {
|
|
|
5084
5799
|
return items.filter(Boolean);
|
|
5085
5800
|
}
|
|
5086
5801
|
|
|
5802
|
+
function sanitizeTimelineThreadFilterLabel(value, threadId = "") {
|
|
5803
|
+
const normalized = cleanText(value || "");
|
|
5804
|
+
if (!normalized) {
|
|
5805
|
+
return "";
|
|
5806
|
+
}
|
|
5807
|
+
|
|
5808
|
+
const normalizedThreadId = cleanText(threadId || "");
|
|
5809
|
+
if (normalizedThreadId && (normalized === normalizedThreadId || normalized === shortId(normalizedThreadId))) {
|
|
5810
|
+
return "";
|
|
5811
|
+
}
|
|
5812
|
+
|
|
5813
|
+
if (/^[0-9a-f]{8}(?:-[0-9a-f]{4}){0,4}$/iu.test(normalized)) {
|
|
5814
|
+
return "";
|
|
5815
|
+
}
|
|
5816
|
+
|
|
5817
|
+
if (looksLikeGeneratedThreadTitle(normalized)) {
|
|
5818
|
+
return "";
|
|
5819
|
+
}
|
|
5820
|
+
|
|
5821
|
+
return normalized;
|
|
5822
|
+
}
|
|
5823
|
+
|
|
5087
5824
|
function buildTimelineThreads(entries, config) {
|
|
5088
5825
|
const byThread = new Map();
|
|
5089
5826
|
for (const entry of entries) {
|
|
@@ -5091,11 +5828,14 @@ function buildTimelineThreads(entries, config) {
|
|
|
5091
5828
|
if (!threadId) {
|
|
5092
5829
|
continue;
|
|
5093
5830
|
}
|
|
5831
|
+
const preferredLabel =
|
|
5832
|
+
sanitizeTimelineThreadFilterLabel(entry.threadLabel || "", threadId) ||
|
|
5833
|
+
t(DEFAULT_LOCALE, "server.fallback.codexTask");
|
|
5094
5834
|
const existing = byThread.get(threadId);
|
|
5095
5835
|
if (!existing) {
|
|
5096
5836
|
byThread.set(threadId, {
|
|
5097
5837
|
id: threadId,
|
|
5098
|
-
label:
|
|
5838
|
+
label: preferredLabel,
|
|
5099
5839
|
latestAtMs: Number(entry.createdAtMs) || 0,
|
|
5100
5840
|
preview: cleanText(entry.summary || entry.title || ""),
|
|
5101
5841
|
entryCount: 1,
|
|
@@ -5105,7 +5845,7 @@ function buildTimelineThreads(entries, config) {
|
|
|
5105
5845
|
existing.entryCount += 1;
|
|
5106
5846
|
if (Number(entry.createdAtMs) > Number(existing.latestAtMs)) {
|
|
5107
5847
|
existing.latestAtMs = Number(entry.createdAtMs) || existing.latestAtMs;
|
|
5108
|
-
existing.label =
|
|
5848
|
+
existing.label = preferredLabel || existing.label;
|
|
5109
5849
|
existing.preview = cleanText(entry.summary || entry.title || "") || existing.preview;
|
|
5110
5850
|
}
|
|
5111
5851
|
}
|
|
@@ -5131,6 +5871,7 @@ function buildTimelineResponse(runtime, state, config, locale) {
|
|
|
5131
5871
|
threadId: entry.threadId,
|
|
5132
5872
|
threadLabel: entry.threadLabel,
|
|
5133
5873
|
summary: entry.summary,
|
|
5874
|
+
imageUrls: buildTimelineEntryImageUrls(entry),
|
|
5134
5875
|
createdAtMs: entry.createdAtMs,
|
|
5135
5876
|
}));
|
|
5136
5877
|
|
|
@@ -5193,6 +5934,49 @@ function buildPreviousApprovalContext(runtime, approval) {
|
|
|
5193
5934
|
};
|
|
5194
5935
|
}
|
|
5195
5936
|
|
|
5937
|
+
function buildInterruptedTimelineContext(runtime, entry, locale) {
|
|
5938
|
+
if (!runtime || !isTurnAbortedDisplayMessage(entry?.messageText)) {
|
|
5939
|
+
return null;
|
|
5940
|
+
}
|
|
5941
|
+
|
|
5942
|
+
const threadId = cleanText(entry?.threadId || "");
|
|
5943
|
+
const interruptedCreatedAtMs = Number(entry?.createdAtMs) || 0;
|
|
5944
|
+
if (!threadId || !interruptedCreatedAtMs) {
|
|
5945
|
+
return null;
|
|
5946
|
+
}
|
|
5947
|
+
|
|
5948
|
+
const previousEntry = runtime.recentTimelineEntries
|
|
5949
|
+
.filter((candidate) => {
|
|
5950
|
+
if (!timelineMessageKinds.has(cleanText(candidate?.kind || ""))) {
|
|
5951
|
+
return false;
|
|
5952
|
+
}
|
|
5953
|
+
if (cleanText(candidate?.threadId || "") !== threadId) {
|
|
5954
|
+
return false;
|
|
5955
|
+
}
|
|
5956
|
+
if (Number(candidate?.createdAtMs) <= 0 || Number(candidate?.createdAtMs) >= interruptedCreatedAtMs) {
|
|
5957
|
+
return false;
|
|
5958
|
+
}
|
|
5959
|
+
return !isTurnAbortedDisplayMessage(candidate?.messageText);
|
|
5960
|
+
})
|
|
5961
|
+
.sort((left, right) => Number(right?.createdAtMs ?? 0) - Number(left?.createdAtMs ?? 0))[0];
|
|
5962
|
+
|
|
5963
|
+
if (!previousEntry) {
|
|
5964
|
+
return null;
|
|
5965
|
+
}
|
|
5966
|
+
|
|
5967
|
+
const sourceText = normalizeLongText(previousEntry.messageText || previousEntry.summary || "");
|
|
5968
|
+
if (!sourceText) {
|
|
5969
|
+
return null;
|
|
5970
|
+
}
|
|
5971
|
+
|
|
5972
|
+
return {
|
|
5973
|
+
kind: previousEntry.kind,
|
|
5974
|
+
label: t(locale, "detail.interruptedTask"),
|
|
5975
|
+
createdAtMs: Number(previousEntry.createdAtMs) || 0,
|
|
5976
|
+
messageHtml: renderMessageHtml(sourceText, "<p></p>"),
|
|
5977
|
+
};
|
|
5978
|
+
}
|
|
5979
|
+
|
|
5196
5980
|
function buildPendingPlanDetail(planRequest, locale) {
|
|
5197
5981
|
return {
|
|
5198
5982
|
kind: "plan",
|
|
@@ -5336,19 +6120,32 @@ function buildHistoryDetail(item, locale, runtime = null) {
|
|
|
5336
6120
|
threadLabel: item.threadLabel || "",
|
|
5337
6121
|
createdAtMs: Number(item.createdAtMs) || 0,
|
|
5338
6122
|
messageHtml: renderMessageHtml(item.messageText, `<p>${escapeHtml(t(locale, "detail.detailUnavailable"))}</p>`),
|
|
6123
|
+
interruptNotice: interruptedDetailNotice(item.messageText, locale),
|
|
5339
6124
|
readOnly: true,
|
|
5340
6125
|
reply: replyEnabled
|
|
5341
6126
|
? {
|
|
5342
6127
|
enabled: true,
|
|
5343
6128
|
supportsPlanMode: true,
|
|
5344
|
-
supportsImages:
|
|
6129
|
+
supportsImages: true,
|
|
5345
6130
|
}
|
|
5346
6131
|
: null,
|
|
5347
6132
|
actions: [],
|
|
5348
6133
|
};
|
|
5349
6134
|
}
|
|
5350
6135
|
|
|
5351
|
-
function
|
|
6136
|
+
function buildTimelineEntryImageUrls(entry) {
|
|
6137
|
+
const imagePaths = normalizeTimelineImagePaths(entry?.imagePaths ?? []);
|
|
6138
|
+
if (imagePaths.length === 0) {
|
|
6139
|
+
return [];
|
|
6140
|
+
}
|
|
6141
|
+
const token = cleanText(entry?.token || "");
|
|
6142
|
+
if (!token) {
|
|
6143
|
+
return [];
|
|
6144
|
+
}
|
|
6145
|
+
return imagePaths.map((_, index) => `/api/timeline/${encodeURIComponent(token)}/images/${index}`);
|
|
6146
|
+
}
|
|
6147
|
+
|
|
6148
|
+
function buildTimelineMessageDetail(entry, locale, runtime = null) {
|
|
5352
6149
|
return {
|
|
5353
6150
|
kind: entry.kind,
|
|
5354
6151
|
token: entry.token,
|
|
@@ -5357,6 +6154,9 @@ function buildTimelineMessageDetail(entry, locale) {
|
|
|
5357
6154
|
threadLabel: entry.threadLabel || "",
|
|
5358
6155
|
createdAtMs: Number(entry.createdAtMs) || 0,
|
|
5359
6156
|
messageHtml: renderMessageHtml(entry.messageText, `<p>${escapeHtml(t(locale, "detail.detailUnavailable"))}</p>`),
|
|
6157
|
+
imageUrls: buildTimelineEntryImageUrls(entry),
|
|
6158
|
+
previousContext: buildInterruptedTimelineContext(runtime, entry, locale),
|
|
6159
|
+
interruptNotice: interruptedDetailNotice(entry.messageText, locale),
|
|
5360
6160
|
readOnly: true,
|
|
5361
6161
|
actions: [],
|
|
5362
6162
|
};
|
|
@@ -5449,8 +6249,224 @@ function normalizeCompletionReplyLocalImagePaths(paths) {
|
|
|
5449
6249
|
.filter(Boolean);
|
|
5450
6250
|
}
|
|
5451
6251
|
|
|
6252
|
+
function guessImageMimeTypeFromPath(filePath) {
|
|
6253
|
+
const extension = path.extname(cleanText(filePath || "")).toLowerCase();
|
|
6254
|
+
const mimeTypes = {
|
|
6255
|
+
".jpg": "image/jpeg",
|
|
6256
|
+
".jpeg": "image/jpeg",
|
|
6257
|
+
".png": "image/png",
|
|
6258
|
+
".webp": "image/webp",
|
|
6259
|
+
".gif": "image/gif",
|
|
6260
|
+
".heic": "image/heic",
|
|
6261
|
+
".heif": "image/heif",
|
|
6262
|
+
};
|
|
6263
|
+
return mimeTypes[extension] || "application/octet-stream";
|
|
6264
|
+
}
|
|
6265
|
+
|
|
6266
|
+
async function buildCompletionReplyImageDataUrls(localImagePaths) {
|
|
6267
|
+
const urls = [];
|
|
6268
|
+
for (const filePath of localImagePaths) {
|
|
6269
|
+
const buffer = await fs.readFile(filePath);
|
|
6270
|
+
const mimeType = guessImageMimeTypeFromPath(filePath);
|
|
6271
|
+
urls.push(`data:${mimeType};base64,${buffer.toString("base64")}`);
|
|
6272
|
+
}
|
|
6273
|
+
return urls;
|
|
6274
|
+
}
|
|
6275
|
+
|
|
6276
|
+
function scheduleBestEffortFileCleanup(paths, delayMs = COMPLETION_REPLY_WORKSPACE_STAGE_CLEANUP_DELAY_MS) {
|
|
6277
|
+
if (!Array.isArray(paths) || paths.length === 0) {
|
|
6278
|
+
return;
|
|
6279
|
+
}
|
|
6280
|
+
|
|
6281
|
+
const timer = setTimeout(async () => {
|
|
6282
|
+
await Promise.all(
|
|
6283
|
+
paths.map(async (filePath) => {
|
|
6284
|
+
try {
|
|
6285
|
+
await fs.rm(filePath, { force: true });
|
|
6286
|
+
} catch {
|
|
6287
|
+
// Ignore best-effort cleanup errors.
|
|
6288
|
+
}
|
|
6289
|
+
})
|
|
6290
|
+
);
|
|
6291
|
+
}, delayMs);
|
|
6292
|
+
timer.unref?.();
|
|
6293
|
+
}
|
|
6294
|
+
|
|
6295
|
+
function buildDirectTurnStartPayload(conversationId, turnStartParams = {}) {
|
|
6296
|
+
return {
|
|
6297
|
+
threadId: cleanText(conversationId || ""),
|
|
6298
|
+
input: Array.isArray(turnStartParams.input) ? turnStartParams.input : [],
|
|
6299
|
+
cwd: cleanText(turnStartParams.cwd || "") || null,
|
|
6300
|
+
approvalPolicy: turnStartParams.approvalPolicy ?? null,
|
|
6301
|
+
approvalsReviewer: cleanText(turnStartParams.approvalsReviewer || "") || "user",
|
|
6302
|
+
sandboxPolicy: turnStartParams.sandboxPolicy ?? null,
|
|
6303
|
+
model: turnStartParams.model ?? null,
|
|
6304
|
+
serviceTier: turnStartParams.serviceTier ?? null,
|
|
6305
|
+
effort: turnStartParams.effort ?? null,
|
|
6306
|
+
summary: cleanText(turnStartParams.summary || "") || "none",
|
|
6307
|
+
personality: turnStartParams.personality ?? null,
|
|
6308
|
+
outputSchema: turnStartParams.outputSchema ?? null,
|
|
6309
|
+
collaborationMode: isPlainObject(turnStartParams.collaborationMode)
|
|
6310
|
+
? turnStartParams.collaborationMode
|
|
6311
|
+
: null,
|
|
6312
|
+
attachments: Array.isArray(turnStartParams.attachments) ? turnStartParams.attachments : [],
|
|
6313
|
+
};
|
|
6314
|
+
}
|
|
6315
|
+
|
|
6316
|
+
async function cleanupExpiredWorkspaceReplyImages(stageDir) {
|
|
6317
|
+
try {
|
|
6318
|
+
const entries = await fs.readdir(stageDir, { withFileTypes: true });
|
|
6319
|
+
const cutoffMs = Date.now() - COMPLETION_REPLY_WORKSPACE_STAGE_TTL_MS;
|
|
6320
|
+
await Promise.all(
|
|
6321
|
+
entries.map(async (entry) => {
|
|
6322
|
+
if (!entry.isFile()) {
|
|
6323
|
+
return;
|
|
6324
|
+
}
|
|
6325
|
+
const filePath = path.join(stageDir, entry.name);
|
|
6326
|
+
try {
|
|
6327
|
+
const stat = await fs.stat(filePath);
|
|
6328
|
+
if (Number(stat.mtimeMs) < cutoffMs) {
|
|
6329
|
+
await fs.rm(filePath, { force: true });
|
|
6330
|
+
}
|
|
6331
|
+
} catch {
|
|
6332
|
+
// Ignore best-effort cleanup errors.
|
|
6333
|
+
}
|
|
6334
|
+
})
|
|
6335
|
+
);
|
|
6336
|
+
} catch {
|
|
6337
|
+
// Ignore missing stage dir.
|
|
6338
|
+
}
|
|
6339
|
+
}
|
|
6340
|
+
|
|
6341
|
+
async function stageCompletionReplyImagesForThreadCwd(localImagePaths, cwd) {
|
|
6342
|
+
const normalizedCwd = resolvePath(cleanText(cwd || ""));
|
|
6343
|
+
if (!normalizedCwd || !Array.isArray(localImagePaths) || localImagePaths.length === 0) {
|
|
6344
|
+
return [];
|
|
6345
|
+
}
|
|
6346
|
+
|
|
6347
|
+
const stageDir = path.join(normalizedCwd, COMPLETION_REPLY_WORKSPACE_STAGE_DIR);
|
|
6348
|
+
await cleanupExpiredWorkspaceReplyImages(stageDir);
|
|
6349
|
+
await fs.mkdir(stageDir, { recursive: true });
|
|
6350
|
+
|
|
6351
|
+
const stagedPaths = [];
|
|
6352
|
+
for (const sourcePath of localImagePaths) {
|
|
6353
|
+
const extension = path.extname(cleanText(sourcePath || "")) || ".img";
|
|
6354
|
+
const stagedPath = path.join(stageDir, `${Date.now()}-${crypto.randomUUID()}${extension}`);
|
|
6355
|
+
await fs.copyFile(sourcePath, stagedPath);
|
|
6356
|
+
stagedPaths.push(stagedPath);
|
|
6357
|
+
}
|
|
6358
|
+
return stagedPaths;
|
|
6359
|
+
}
|
|
6360
|
+
|
|
6361
|
+
async function buildCompletionReplyTurnCandidates(
|
|
6362
|
+
messageText,
|
|
6363
|
+
localImagePaths,
|
|
6364
|
+
collaborationMode,
|
|
6365
|
+
cwd = null,
|
|
6366
|
+
workspaceLocalImagePaths = []
|
|
6367
|
+
) {
|
|
6368
|
+
const baseCandidate = {
|
|
6369
|
+
attachments: [],
|
|
6370
|
+
cwd: cleanText(cwd || "") || null,
|
|
6371
|
+
approvalPolicy: null,
|
|
6372
|
+
sandboxPolicy: null,
|
|
6373
|
+
model: null,
|
|
6374
|
+
serviceTier: null,
|
|
6375
|
+
effort: null,
|
|
6376
|
+
summary: "none",
|
|
6377
|
+
personality: null,
|
|
6378
|
+
outputSchema: null,
|
|
6379
|
+
collaborationMode,
|
|
6380
|
+
};
|
|
6381
|
+
|
|
6382
|
+
if (!localImagePaths.length) {
|
|
6383
|
+
return [
|
|
6384
|
+
{
|
|
6385
|
+
name: "text-only",
|
|
6386
|
+
transport: "thread-follower",
|
|
6387
|
+
turnStartParams: {
|
|
6388
|
+
...baseCandidate,
|
|
6389
|
+
input: buildTurnInput(messageText),
|
|
6390
|
+
localImagePaths: [],
|
|
6391
|
+
local_image_paths: [],
|
|
6392
|
+
remoteImageUrls: [],
|
|
6393
|
+
remote_image_urls: [],
|
|
6394
|
+
},
|
|
6395
|
+
},
|
|
6396
|
+
];
|
|
6397
|
+
}
|
|
6398
|
+
|
|
6399
|
+
const imageDataUrls = await buildCompletionReplyImageDataUrls(localImagePaths);
|
|
6400
|
+
const workspaceImagePaths = normalizeCompletionReplyLocalImagePaths(workspaceLocalImagePaths);
|
|
6401
|
+
const candidates = [];
|
|
6402
|
+
|
|
6403
|
+
if (workspaceImagePaths.length) {
|
|
6404
|
+
candidates.push({
|
|
6405
|
+
// Match the Desktop composer path as closely as possible:
|
|
6406
|
+
// text + localImage(path) passed through the normal thread-follower route.
|
|
6407
|
+
name: "workspace-local-image-composer-input",
|
|
6408
|
+
transport: "thread-follower",
|
|
6409
|
+
turnStartParams: {
|
|
6410
|
+
...baseCandidate,
|
|
6411
|
+
input: buildComposerStyleLocalImageInput(messageText, workspaceImagePaths),
|
|
6412
|
+
localImagePaths: [],
|
|
6413
|
+
local_image_paths: [],
|
|
6414
|
+
remoteImageUrls: [],
|
|
6415
|
+
remote_image_urls: [],
|
|
6416
|
+
},
|
|
6417
|
+
});
|
|
6418
|
+
}
|
|
6419
|
+
|
|
6420
|
+
candidates.push(
|
|
6421
|
+
{
|
|
6422
|
+
// This mirrors the desktop composer input items before submission:
|
|
6423
|
+
// text + image(url=data:image/...).
|
|
6424
|
+
name: "image-data-url-composer-input",
|
|
6425
|
+
transport: "thread-follower",
|
|
6426
|
+
turnStartParams: {
|
|
6427
|
+
...baseCandidate,
|
|
6428
|
+
input: buildComposerStyleImageInput(messageText, imageDataUrls),
|
|
6429
|
+
localImagePaths: [],
|
|
6430
|
+
local_image_paths: [],
|
|
6431
|
+
remoteImageUrls: [],
|
|
6432
|
+
remote_image_urls: [],
|
|
6433
|
+
},
|
|
6434
|
+
},
|
|
6435
|
+
{
|
|
6436
|
+
name: "local-image-composer-input",
|
|
6437
|
+
transport: "thread-follower",
|
|
6438
|
+
turnStartParams: {
|
|
6439
|
+
...baseCandidate,
|
|
6440
|
+
input: buildComposerStyleLocalImageInput(messageText, localImagePaths),
|
|
6441
|
+
localImagePaths: [],
|
|
6442
|
+
local_image_paths: [],
|
|
6443
|
+
remoteImageUrls: [],
|
|
6444
|
+
remote_image_urls: [],
|
|
6445
|
+
},
|
|
6446
|
+
},
|
|
6447
|
+
{
|
|
6448
|
+
// This currently reaches Codex, but the image is dropped before the final
|
|
6449
|
+
// UserInput core submission. Keep it last as a diagnostic fallback.
|
|
6450
|
+
name: "remote-image-urls-data-url",
|
|
6451
|
+
transport: "thread-follower",
|
|
6452
|
+
turnStartParams: {
|
|
6453
|
+
...baseCandidate,
|
|
6454
|
+
input: buildTurnInput(messageText),
|
|
6455
|
+
localImagePaths: [],
|
|
6456
|
+
local_image_paths: [],
|
|
6457
|
+
remoteImageUrls: imageDataUrls,
|
|
6458
|
+
remote_image_urls: imageDataUrls,
|
|
6459
|
+
},
|
|
6460
|
+
}
|
|
6461
|
+
);
|
|
6462
|
+
|
|
6463
|
+
return candidates;
|
|
6464
|
+
}
|
|
6465
|
+
|
|
5452
6466
|
async function handleCompletionReply({
|
|
6467
|
+
config,
|
|
5453
6468
|
runtime,
|
|
6469
|
+
state,
|
|
5454
6470
|
completionItem,
|
|
5455
6471
|
text,
|
|
5456
6472
|
planMode = false,
|
|
@@ -5485,34 +6501,97 @@ async function handleCompletionReply({
|
|
|
5485
6501
|
}
|
|
5486
6502
|
|
|
5487
6503
|
const threadState = runtime.threadStates.get(conversationId) ?? null;
|
|
6504
|
+
const resolvedCwd = await resolveConversationCwd(runtime, conversationId);
|
|
6505
|
+
const stagedWorkspaceImagePaths = await stageCompletionReplyImagesForThreadCwd(
|
|
6506
|
+
normalizedLocalImagePaths,
|
|
6507
|
+
resolvedCwd
|
|
6508
|
+
);
|
|
6509
|
+
const timelineImageAliases = [];
|
|
6510
|
+
if (normalizedLocalImagePaths.length > 0) {
|
|
6511
|
+
const persistentTimelineImagePaths = await normalizePersistedTimelineImagePaths({
|
|
6512
|
+
config,
|
|
6513
|
+
state,
|
|
6514
|
+
imagePaths: normalizedLocalImagePaths,
|
|
6515
|
+
});
|
|
6516
|
+
for (let index = 0; index < persistentTimelineImagePaths.length; index += 1) {
|
|
6517
|
+
const persistentPath = cleanText(persistentTimelineImagePaths[index] || "");
|
|
6518
|
+
if (!persistentPath) {
|
|
6519
|
+
continue;
|
|
6520
|
+
}
|
|
6521
|
+
const uploadPath = cleanText(normalizedLocalImagePaths[index] || "");
|
|
6522
|
+
const stagedPath = cleanText(stagedWorkspaceImagePaths[index] || "");
|
|
6523
|
+
if (uploadPath) {
|
|
6524
|
+
timelineImageAliases.push([uploadPath, persistentPath]);
|
|
6525
|
+
}
|
|
6526
|
+
if (stagedPath) {
|
|
6527
|
+
timelineImageAliases.push([stagedPath, persistentPath]);
|
|
6528
|
+
}
|
|
6529
|
+
}
|
|
6530
|
+
}
|
|
5488
6531
|
const collaborationMode = buildRequestedCollaborationMode(
|
|
5489
6532
|
threadState,
|
|
5490
6533
|
planMode ? "plan" : "default"
|
|
5491
6534
|
);
|
|
5492
|
-
const
|
|
5493
|
-
|
|
5494
|
-
|
|
5495
|
-
localImagePaths: normalizedLocalImagePaths,
|
|
5496
|
-
local_image_paths: normalizedLocalImagePaths,
|
|
5497
|
-
remoteImageUrls: [],
|
|
5498
|
-
remote_image_urls: [],
|
|
5499
|
-
cwd: null,
|
|
5500
|
-
approvalPolicy: null,
|
|
5501
|
-
sandboxPolicy: null,
|
|
5502
|
-
model: null,
|
|
5503
|
-
serviceTier: null,
|
|
5504
|
-
effort: null,
|
|
5505
|
-
summary: "none",
|
|
5506
|
-
personality: null,
|
|
5507
|
-
outputSchema: null,
|
|
6535
|
+
const turnCandidates = await buildCompletionReplyTurnCandidates(
|
|
6536
|
+
messageText,
|
|
6537
|
+
normalizedLocalImagePaths,
|
|
5508
6538
|
collaborationMode,
|
|
5509
|
-
|
|
6539
|
+
resolvedCwd,
|
|
6540
|
+
stagedWorkspaceImagePaths
|
|
6541
|
+
);
|
|
6542
|
+
let lastError = null;
|
|
6543
|
+
const ownerClientId = runtime.threadOwnerClientIds.get(conversationId) ?? null;
|
|
5510
6544
|
|
|
5511
|
-
|
|
5512
|
-
|
|
5513
|
-
|
|
5514
|
-
|
|
6545
|
+
for (const candidate of turnCandidates) {
|
|
6546
|
+
try {
|
|
6547
|
+
console.log(
|
|
6548
|
+
`[completion-reply] try candidate=${candidate.name} transport=${cleanText(candidate.transport || "thread-follower")} owner=${cleanText(ownerClientId || "") || "none"} images=${normalizedLocalImagePaths.length} workspaceImages=${stagedWorkspaceImagePaths.length} cwd=${cleanText(resolvedCwd || "") || "none"}`
|
|
6549
|
+
);
|
|
6550
|
+
if (candidate.transport === "direct-turn-start" && ownerClientId) {
|
|
6551
|
+
await runtime.ipcClient.startTurnDirect(
|
|
6552
|
+
conversationId,
|
|
6553
|
+
candidate.turnStartParams,
|
|
6554
|
+
ownerClientId
|
|
6555
|
+
);
|
|
6556
|
+
} else {
|
|
6557
|
+
await runtime.ipcClient.startTurn(
|
|
6558
|
+
conversationId,
|
|
6559
|
+
candidate.turnStartParams,
|
|
6560
|
+
ownerClientId
|
|
6561
|
+
);
|
|
6562
|
+
}
|
|
6563
|
+
console.log(
|
|
6564
|
+
`[completion-reply] success candidate=${candidate.name} transport=${cleanText(candidate.transport || "thread-follower")}`
|
|
6565
|
+
);
|
|
6566
|
+
if (timelineImageAliases.length > 0) {
|
|
6567
|
+
const aliases = isPlainObject(state.timelineImagePathAliases)
|
|
6568
|
+
? state.timelineImagePathAliases
|
|
6569
|
+
: (state.timelineImagePathAliases = {});
|
|
6570
|
+
for (const [sourcePath, persistentPath] of timelineImageAliases) {
|
|
6571
|
+
aliases[sourcePath] = persistentPath;
|
|
6572
|
+
}
|
|
6573
|
+
await saveState(config.stateFile, state);
|
|
6574
|
+
}
|
|
6575
|
+
scheduleBestEffortFileCleanup(stagedWorkspaceImagePaths);
|
|
6576
|
+
return;
|
|
6577
|
+
} catch (error) {
|
|
6578
|
+
lastError = error;
|
|
6579
|
+
console.log(
|
|
6580
|
+
`[completion-reply] failed candidate=${candidate.name} transport=${cleanText(candidate.transport || "thread-follower")} error=${normalizeIpcErrorMessage(error)} raw=${inspect(error?.ipcError ?? error, { depth: 6, breakLength: 160 })}`
|
|
6581
|
+
);
|
|
6582
|
+
}
|
|
6583
|
+
}
|
|
6584
|
+
|
|
6585
|
+
await Promise.all(
|
|
6586
|
+
stagedWorkspaceImagePaths.map(async (filePath) => {
|
|
6587
|
+
try {
|
|
6588
|
+
await fs.rm(filePath, { force: true });
|
|
6589
|
+
} catch {
|
|
6590
|
+
// Ignore best-effort cleanup errors.
|
|
6591
|
+
}
|
|
6592
|
+
})
|
|
5515
6593
|
);
|
|
6594
|
+
throw lastError || new Error("completion-reply-image-send-failed");
|
|
5516
6595
|
}
|
|
5517
6596
|
|
|
5518
6597
|
async function handlePlanDecision({ config, runtime, state, planRequest, decision }) {
|
|
@@ -5530,7 +6609,7 @@ async function handlePlanDecision({ config, runtime, state, planRequest, decisio
|
|
|
5530
6609
|
planRequest.threadState
|
|
5531
6610
|
);
|
|
5532
6611
|
const turnStartParams = {
|
|
5533
|
-
input:
|
|
6612
|
+
input: buildTurnInput(buildImplementPlanPrompt(planRequest.rawPlanContent)),
|
|
5534
6613
|
attachments: [],
|
|
5535
6614
|
cwd: null,
|
|
5536
6615
|
approvalPolicy: null,
|
|
@@ -5613,7 +6692,7 @@ async function handleNativeApprovalDecision({ config, runtime, state, approval,
|
|
|
5613
6692
|
function buildApiItemDetail({ config, runtime, state, kind, token, locale }) {
|
|
5614
6693
|
if (timelineMessageKinds.has(kind)) {
|
|
5615
6694
|
const entry = timelineEntryByToken(runtime, token, kind);
|
|
5616
|
-
return entry ? buildTimelineMessageDetail(entry, locale) : null;
|
|
6695
|
+
return entry ? buildTimelineMessageDetail(entry, locale, runtime) : null;
|
|
5617
6696
|
}
|
|
5618
6697
|
if (kind === "approval") {
|
|
5619
6698
|
const approval = runtime.nativeApprovalsByToken.get(token);
|
|
@@ -5644,6 +6723,16 @@ function buildApiItemDetail({ config, runtime, state, kind, token, locale }) {
|
|
|
5644
6723
|
return historyItem ? buildHistoryDetail(historyItem, locale, runtime) : null;
|
|
5645
6724
|
}
|
|
5646
6725
|
|
|
6726
|
+
function resolveTimelineEntryImagePath(runtime, token, index) {
|
|
6727
|
+
const entry = timelineEntryByToken(runtime, token);
|
|
6728
|
+
if (!entry) {
|
|
6729
|
+
return "";
|
|
6730
|
+
}
|
|
6731
|
+
const imagePaths = normalizeTimelineImagePaths(entry.imagePaths ?? []);
|
|
6732
|
+
const resolvedIndex = Math.max(0, Number(index) || 0);
|
|
6733
|
+
return cleanText(imagePaths[resolvedIndex] || "");
|
|
6734
|
+
}
|
|
6735
|
+
|
|
5647
6736
|
function resolveWebAsset(urlPath) {
|
|
5648
6737
|
let relativePath = cleanText(urlPath || "");
|
|
5649
6738
|
if (!relativePath || relativePath === "/") {
|
|
@@ -5676,6 +6765,17 @@ function contentTypeForFile(filePath) {
|
|
|
5676
6765
|
return "image/svg+xml";
|
|
5677
6766
|
case ".png":
|
|
5678
6767
|
return "image/png";
|
|
6768
|
+
case ".jpg":
|
|
6769
|
+
case ".jpeg":
|
|
6770
|
+
return "image/jpeg";
|
|
6771
|
+
case ".webp":
|
|
6772
|
+
return "image/webp";
|
|
6773
|
+
case ".gif":
|
|
6774
|
+
return "image/gif";
|
|
6775
|
+
case ".heic":
|
|
6776
|
+
return "image/heic";
|
|
6777
|
+
case ".heif":
|
|
6778
|
+
return "image/heif";
|
|
5679
6779
|
default:
|
|
5680
6780
|
return "application/octet-stream";
|
|
5681
6781
|
}
|
|
@@ -5707,7 +6807,7 @@ function resolveManifestPairingToken({ config, state, requestedToken }) {
|
|
|
5707
6807
|
if (!token) {
|
|
5708
6808
|
return "";
|
|
5709
6809
|
}
|
|
5710
|
-
if (!
|
|
6810
|
+
if (!isPairingAvailable(config)) {
|
|
5711
6811
|
return "";
|
|
5712
6812
|
}
|
|
5713
6813
|
return cleanText(config.pairingToken) === token ? token : "";
|
|
@@ -5860,6 +6960,7 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
5860
6960
|
httpsEnabled: config.nativeApprovalPublicBaseUrl.startsWith("https://"),
|
|
5861
6961
|
appVersion: appPackageVersion,
|
|
5862
6962
|
deviceId: session.deviceId || null,
|
|
6963
|
+
temporaryPairing: session.temporaryPairing === true,
|
|
5863
6964
|
...buildSessionLocalePayload(config, state, session.deviceId),
|
|
5864
6965
|
});
|
|
5865
6966
|
}
|
|
@@ -5884,6 +6985,17 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
5884
6985
|
return writeJson(res, 400, { error: validation.error });
|
|
5885
6986
|
}
|
|
5886
6987
|
|
|
6988
|
+
if (payload?.temporary === true && cleanText(payload?.token || "")) {
|
|
6989
|
+
clearPairingFailures(runtime, remoteAddress);
|
|
6990
|
+
setTemporarySessionCookie(res, config);
|
|
6991
|
+
return writeJson(res, 200, {
|
|
6992
|
+
ok: true,
|
|
6993
|
+
authenticated: true,
|
|
6994
|
+
pairingAvailable: isPairingAvailableForState(config, state),
|
|
6995
|
+
temporaryPairing: true,
|
|
6996
|
+
});
|
|
6997
|
+
}
|
|
6998
|
+
|
|
5887
6999
|
const pairedDeviceId = readDeviceId(req, config) || crypto.randomUUID();
|
|
5888
7000
|
if ("detectedLocale" in payload) {
|
|
5889
7001
|
upsertDetectedDeviceLocale(state, pairedDeviceId, payload.detectedLocale);
|
|
@@ -5898,7 +7010,9 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
5898
7010
|
lastLocale: normalizeSupportedLocale(payload?.detectedLocale),
|
|
5899
7011
|
}
|
|
5900
7012
|
);
|
|
5901
|
-
|
|
7013
|
+
if (String(validation.credential || "").startsWith("code:")) {
|
|
7014
|
+
markPairingConsumed(state, validation.credential);
|
|
7015
|
+
}
|
|
5902
7016
|
clearPairingFailures(runtime, remoteAddress);
|
|
5903
7017
|
await saveState(config.stateFile, state);
|
|
5904
7018
|
setPairingCookies(res, config, pairedDeviceId);
|
|
@@ -6047,7 +7161,10 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
6047
7161
|
await saveState(config.stateFile, state);
|
|
6048
7162
|
return writeJson(res, 410, { error: "push-subscription-expired" });
|
|
6049
7163
|
}
|
|
6050
|
-
return writeJson(res, 500, {
|
|
7164
|
+
return writeJson(res, 500, {
|
|
7165
|
+
error: error.message,
|
|
7166
|
+
ipcError: error.ipcError ?? null,
|
|
7167
|
+
});
|
|
6051
7168
|
}
|
|
6052
7169
|
}
|
|
6053
7170
|
|
|
@@ -6106,6 +7223,34 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
6106
7223
|
return writeJson(res, 200, buildTimelineResponse(runtime, state, config, locale));
|
|
6107
7224
|
}
|
|
6108
7225
|
|
|
7226
|
+
const apiTimelineImageMatch = url.pathname.match(/^\/api\/timeline\/([^/]+)\/images\/(\d+)$/u);
|
|
7227
|
+
if (apiTimelineImageMatch && req.method === "GET") {
|
|
7228
|
+
const session = requireApiSession(req, res, config, state);
|
|
7229
|
+
if (!session) {
|
|
7230
|
+
return;
|
|
7231
|
+
}
|
|
7232
|
+
const token = decodeURIComponent(apiTimelineImageMatch[1]);
|
|
7233
|
+
const index = Number(apiTimelineImageMatch[2]) || 0;
|
|
7234
|
+
const filePath = resolveTimelineEntryImagePath(runtime, token, index);
|
|
7235
|
+
if (!filePath) {
|
|
7236
|
+
res.statusCode = 404;
|
|
7237
|
+
res.end("not-found");
|
|
7238
|
+
return;
|
|
7239
|
+
}
|
|
7240
|
+
try {
|
|
7241
|
+
const body = await fs.readFile(filePath);
|
|
7242
|
+
res.statusCode = 200;
|
|
7243
|
+
res.setHeader("Content-Type", contentTypeForFile(filePath));
|
|
7244
|
+
res.setHeader("Cache-Control", "private, max-age=300");
|
|
7245
|
+
res.end(body);
|
|
7246
|
+
return;
|
|
7247
|
+
} catch {
|
|
7248
|
+
res.statusCode = 404;
|
|
7249
|
+
res.end("not-found");
|
|
7250
|
+
return;
|
|
7251
|
+
}
|
|
7252
|
+
}
|
|
7253
|
+
|
|
6109
7254
|
const apiItemMatch = url.pathname.match(/^\/api\/items\/([^/]+)\/([^/]+)$/u);
|
|
6110
7255
|
if (apiItemMatch && req.method === "GET") {
|
|
6111
7256
|
const session = requireApiSession(req, res, config, state);
|
|
@@ -6141,7 +7286,9 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
6141
7286
|
? await stageCompletionReplyImages(config, req)
|
|
6142
7287
|
: await parseJsonBody(req);
|
|
6143
7288
|
await handleCompletionReply({
|
|
7289
|
+
config,
|
|
6144
7290
|
runtime,
|
|
7291
|
+
state,
|
|
6145
7292
|
completionItem,
|
|
6146
7293
|
text: payload?.text ?? "",
|
|
6147
7294
|
planMode: payload?.planMode === true,
|
|
@@ -6161,8 +7308,7 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
6161
7308
|
error.message === "completion-reply-image-limit" ||
|
|
6162
7309
|
error.message === "completion-reply-image-invalid-type" ||
|
|
6163
7310
|
error.message === "completion-reply-image-too-large" ||
|
|
6164
|
-
error.message === "completion-reply-image-invalid-upload"
|
|
6165
|
-
error.message === "completion-reply-image-disabled"
|
|
7311
|
+
error.message === "completion-reply-image-invalid-upload"
|
|
6166
7312
|
) {
|
|
6167
7313
|
return writeJson(res, 400, { error: error.message });
|
|
6168
7314
|
}
|
|
@@ -6886,10 +8032,6 @@ async function stageCompletionReplyImages(config, req) {
|
|
|
6886
8032
|
.getAll("image")
|
|
6887
8033
|
.filter((value) => typeof File !== "undefined" && value instanceof File);
|
|
6888
8034
|
|
|
6889
|
-
if (files.length > 0) {
|
|
6890
|
-
throw new Error("completion-reply-image-disabled");
|
|
6891
|
-
}
|
|
6892
|
-
|
|
6893
8035
|
if (files.length > MAX_COMPLETION_REPLY_IMAGE_COUNT) {
|
|
6894
8036
|
throw new Error("completion-reply-image-limit");
|
|
6895
8037
|
}
|
|
@@ -7883,6 +9025,9 @@ function buildConfig(cli) {
|
|
|
7883
9025
|
codexLogsDbFile: resolvePath(process.env.CODEX_LOGS_DB_FILE || ""),
|
|
7884
9026
|
stateFile,
|
|
7885
9027
|
replyUploadsDir: resolvePath(process.env.REPLY_UPLOADS_DIR || path.join(path.dirname(stateFile), "uploads")),
|
|
9028
|
+
timelineAttachmentsDir: resolvePath(
|
|
9029
|
+
process.env.TIMELINE_ATTACHMENTS_DIR || path.join(path.dirname(stateFile), "timeline-attachments")
|
|
9030
|
+
),
|
|
7886
9031
|
pollIntervalMs: numberEnv("POLL_INTERVAL_MS", 2500),
|
|
7887
9032
|
replaySeconds: numberEnv("REPLAY_SECONDS", 300),
|
|
7888
9033
|
sessionIndexRefreshMs: numberEnv("SESSION_INDEX_REFRESH_MS", 30000),
|
|
@@ -8122,6 +9267,7 @@ async function loadState(stateFile) {
|
|
|
8122
9267
|
pendingUserInputRequests: parsed.pendingUserInputRequests ?? {},
|
|
8123
9268
|
recentHistoryItems: parsed.recentHistoryItems ?? [],
|
|
8124
9269
|
recentTimelineEntries: parsed.recentTimelineEntries ?? [],
|
|
9270
|
+
timelineImagePathAliases: parsed.timelineImagePathAliases ?? {},
|
|
8125
9271
|
sqliteCompletionCursorId: Number(parsed.sqliteCompletionCursorId) || 0,
|
|
8126
9272
|
sqliteCompletionSourceFile: cleanText(parsed.sqliteCompletionSourceFile ?? ""),
|
|
8127
9273
|
sqliteMessageCursorId: Number(parsed.sqliteMessageCursorId) || 0,
|
|
@@ -8146,6 +9292,7 @@ async function loadState(stateFile) {
|
|
|
8146
9292
|
pendingUserInputRequests: {},
|
|
8147
9293
|
recentHistoryItems: [],
|
|
8148
9294
|
recentTimelineEntries: [],
|
|
9295
|
+
timelineImagePathAliases: {},
|
|
8149
9296
|
sqliteCompletionCursorId: 0,
|
|
8150
9297
|
sqliteCompletionSourceFile: "",
|
|
8151
9298
|
sqliteMessageCursorId: 0,
|
|
@@ -8232,6 +9379,10 @@ function sanitizeResolvedThreadLabel(value, conversationId) {
|
|
|
8232
9379
|
return "";
|
|
8233
9380
|
}
|
|
8234
9381
|
|
|
9382
|
+
if (looksLikeGeneratedThreadTitle(normalized)) {
|
|
9383
|
+
return "";
|
|
9384
|
+
}
|
|
9385
|
+
|
|
8235
9386
|
return normalized;
|
|
8236
9387
|
}
|
|
8237
9388
|
|
|
@@ -8269,7 +9420,7 @@ function extractRolloutMessageText(content) {
|
|
|
8269
9420
|
content
|
|
8270
9421
|
.map((entry) =>
|
|
8271
9422
|
isPlainObject(entry) && (entry.type === "input_text" || entry.type === "output_text")
|
|
8272
|
-
?
|
|
9423
|
+
? normalizeTimelineMessageText(entry.text ?? "")
|
|
8273
9424
|
: ""
|
|
8274
9425
|
)
|
|
8275
9426
|
.filter(Boolean)
|
|
@@ -8277,6 +9428,24 @@ function extractRolloutMessageText(content) {
|
|
|
8277
9428
|
);
|
|
8278
9429
|
}
|
|
8279
9430
|
|
|
9431
|
+
function rolloutContentHasImages(content) {
|
|
9432
|
+
if (!Array.isArray(content)) {
|
|
9433
|
+
return false;
|
|
9434
|
+
}
|
|
9435
|
+
return content.some((entry) => {
|
|
9436
|
+
if (!isPlainObject(entry)) {
|
|
9437
|
+
return false;
|
|
9438
|
+
}
|
|
9439
|
+
if (entry.type === "input_image" || entry.type === "image" || entry.type === "localImage") {
|
|
9440
|
+
return true;
|
|
9441
|
+
}
|
|
9442
|
+
if (entry.type !== "input_text" && entry.type !== "output_text") {
|
|
9443
|
+
return false;
|
|
9444
|
+
}
|
|
9445
|
+
return isInlineImagePlaceholderText(entry.text ?? "");
|
|
9446
|
+
});
|
|
9447
|
+
}
|
|
9448
|
+
|
|
8280
9449
|
function deriveRolloutThreadTitleCandidate(text) {
|
|
8281
9450
|
const normalized = cleanText(normalizeNotificationText(text));
|
|
8282
9451
|
if (!normalized) {
|
|
@@ -8657,8 +9826,50 @@ function normalizeLongText(value) {
|
|
|
8657
9826
|
.trim();
|
|
8658
9827
|
}
|
|
8659
9828
|
|
|
8660
|
-
function
|
|
8661
|
-
|
|
9829
|
+
function replaceTurnAbortedMarkup(value, locale = DEFAULT_LOCALE) {
|
|
9830
|
+
const raw = String(value || "");
|
|
9831
|
+
if (!/<turn_aborted>/iu.test(raw)) {
|
|
9832
|
+
return raw;
|
|
9833
|
+
}
|
|
9834
|
+
return raw.replace(/<turn_aborted>[\s\S]*?<\/turn_aborted>/giu, t(locale, "server.message.turnAborted"));
|
|
9835
|
+
}
|
|
9836
|
+
|
|
9837
|
+
function isTurnAbortedDisplayMessage(value) {
|
|
9838
|
+
const normalized = normalizeLongText(String(value || ""));
|
|
9839
|
+
const englishMessage = normalizeLongText(t(DEFAULT_LOCALE, "server.message.turnAborted"));
|
|
9840
|
+
const japaneseMessage = normalizeLongText(t("ja", "server.message.turnAborted"));
|
|
9841
|
+
return /<turn_aborted>/iu.test(String(value || "")) || normalized === englishMessage || normalized === japaneseMessage;
|
|
9842
|
+
}
|
|
9843
|
+
|
|
9844
|
+
function interruptedDetailNotice(value, locale = DEFAULT_LOCALE) {
|
|
9845
|
+
return isTurnAbortedDisplayMessage(value) ? t(locale, "detail.turnAbortedNotice") : "";
|
|
9846
|
+
}
|
|
9847
|
+
|
|
9848
|
+
function stripInlineImagePlaceholderMarkup(value) {
|
|
9849
|
+
return String(value || "")
|
|
9850
|
+
.replace(/<image\b[^>]*>/giu, "")
|
|
9851
|
+
.replace(/<\/image>/giu, "");
|
|
9852
|
+
}
|
|
9853
|
+
|
|
9854
|
+
function isInlineImagePlaceholderText(value) {
|
|
9855
|
+
return cleanText(stripInlineImagePlaceholderMarkup(value)) === "" && /<\/?image\b/iu.test(String(value || ""));
|
|
9856
|
+
}
|
|
9857
|
+
|
|
9858
|
+
function normalizeTimelineMessageText(value, locale = DEFAULT_LOCALE) {
|
|
9859
|
+
return normalizeLongText(replaceTurnAbortedMarkup(stripInlineImagePlaceholderMarkup(value), locale));
|
|
9860
|
+
}
|
|
9861
|
+
|
|
9862
|
+
function normalizeTimelineImagePaths(value) {
|
|
9863
|
+
if (!Array.isArray(value)) {
|
|
9864
|
+
return [];
|
|
9865
|
+
}
|
|
9866
|
+
return value
|
|
9867
|
+
.map((entry) => cleanText(entry || ""))
|
|
9868
|
+
.filter(Boolean);
|
|
9869
|
+
}
|
|
9870
|
+
|
|
9871
|
+
function normalizeNotificationText(value, locale = DEFAULT_LOCALE) {
|
|
9872
|
+
return normalizeLongText(replaceTurnAbortedMarkup(stripNotificationMarkup(value), locale))
|
|
8662
9873
|
.replace(/\n{2,}/gu, "\n")
|
|
8663
9874
|
.trim();
|
|
8664
9875
|
}
|
|
@@ -8734,6 +9945,8 @@ function stripNotificationMarkup(value) {
|
|
|
8734
9945
|
return String(value || "")
|
|
8735
9946
|
.replace(/^\s*<\/?proposed_plan>\s*$/gimu, "")
|
|
8736
9947
|
.replace(/<\/?proposed_plan>/giu, "")
|
|
9948
|
+
.replace(/<image\b[^>]*>/giu, "")
|
|
9949
|
+
.replace(/<\/image>/giu, "")
|
|
8737
9950
|
.replace(/!\[([^\]]*)\]\(([^)]+)\)/gu, "$1")
|
|
8738
9951
|
.replace(/\[([^\]]+)\]\(([^)]+)\)/gu, "$1")
|
|
8739
9952
|
.replace(/^\s{0,3}#{1,6}\s+/gmu, "")
|
|
@@ -8933,6 +10146,7 @@ async function main() {
|
|
|
8933
10146
|
if (
|
|
8934
10147
|
migratedPairedDevicesStateChanged ||
|
|
8935
10148
|
restoredPendingPlanStateChanged ||
|
|
10149
|
+
restoredTimelineImagePathsStateChanged ||
|
|
8936
10150
|
restoredPendingUserInputStateChanged ||
|
|
8937
10151
|
refreshResolvedThreadLabels({ config, runtime, state })
|
|
8938
10152
|
) {
|