agentel 0.2.6 → 0.2.8
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/README.md +45 -18
- package/docs/code-reference.md +9 -5
- package/docs/history-source-handling.md +166 -65
- package/docs/release.md +1 -1
- package/package.json +5 -2
- package/src/archive.js +200 -17
- package/src/cli.js +1794 -104
- package/src/config.js +11 -0
- package/src/importers/gemini.js +2 -1
- package/src/importers.js +1675 -134
- package/src/search.js +416 -176
- package/src/web-export-instructions.js +6 -4
package/src/cli.js
CHANGED
|
@@ -9,7 +9,7 @@ const readline = require("readline");
|
|
|
9
9
|
const { spawnSync } = require("child_process");
|
|
10
10
|
const { pathToFileURL } = require("url");
|
|
11
11
|
const { disableAutostart, enableAutostart, autostartStatus, recallInvocation } = require("./autostart");
|
|
12
|
-
const { archiveRoot, countSessions, ensureConversationMarkdown, ensureSessionView, findSessionById, listSessions, readEvents, readTranscript, revealSession, sessionViewPathForSession, usageTokenSummary, VIEW_SCHEMA_VERSION, writeSession } = require("./archive");
|
|
12
|
+
const { archiveRoot, cachedListSessionsSnapshot, countSessions, ensureConversationMarkdown, ensureSessionView, findSessionById, isWebChatProvider, listSessions, readEvents, readTranscript, revealSession, sessionViewPathForSession, usageTokenSummary, VIEW_SCHEMA_VERSION, walk, writeSession } = require("./archive");
|
|
13
13
|
const { logsCommand } = require("./commands/logs");
|
|
14
14
|
const { serverCommand: runServerCommand } = require("./commands/server");
|
|
15
15
|
const { effectiveImportSources, getConfigKey, initConfig, loadConfig, normalizeRemoteEndpointInput, saveConfig, setConfigKey } = require("./config");
|
|
@@ -18,7 +18,7 @@ const { discoverCliHistory, importCliHistory, importWebChat, importWindsurfTraje
|
|
|
18
18
|
const { runMcpServer } = require("./mcp");
|
|
19
19
|
const { defaultRedactionConfig, loadRedactionConfig, redactionBuiltInNames, redactionLabel, saveRedactionConfig } = require("./redaction");
|
|
20
20
|
const { buildIndex, listHistorySessions, listRecentSessions, readIndexSummary, searchPastSessions } = require("./search");
|
|
21
|
-
const { IMPORT_SOURCE_ORDER } = require("./sources");
|
|
21
|
+
const { HISTORY_PROVIDER_OPTIONS, IMPORT_SOURCE_ORDER } = require("./sources");
|
|
22
22
|
const { ensureBaseDirs, ensureDir, paths, readJson, writeJson } = require("./paths");
|
|
23
23
|
const { runSupervisorForeground, startSupervisorDetached, stopSupervisor, supervisorStatus } = require("./supervisor");
|
|
24
24
|
const { configureRemoteFromFlags, hasRemoteTarget, listRemoteSnapshots, replaceRemoteArchive, snapshotArchive, syncArchive, wipeRemoteArchive } = require("./sync");
|
|
@@ -27,7 +27,19 @@ const { listWebAccounts, renameWebAccount } = require("./web-accounts");
|
|
|
27
27
|
const { webExportInstructions } = require("./web-export-instructions");
|
|
28
28
|
|
|
29
29
|
const HISTORY_AUTH_COOKIE = "agentlog_history";
|
|
30
|
-
const SESSION_WEB_PAYLOAD_VERSION =
|
|
30
|
+
const SESSION_WEB_PAYLOAD_VERSION = 6;
|
|
31
|
+
// Bumped when any list-derived payload shape changes (recent/tree/repo-sessions/stats).
|
|
32
|
+
// Combined with the listSessions snapshot fingerprint to form the etag, so a
|
|
33
|
+
// client receiving a 304 is guaranteed to have a payload that still matches the
|
|
34
|
+
// current renderer.
|
|
35
|
+
const LIST_PAYLOAD_VERSION = 1;
|
|
36
|
+
// Memoize statsPayload between requests. Stats are archive-wide aggregates
|
|
37
|
+
// derived purely from the listSessions snapshot, so the snapshot fingerprint
|
|
38
|
+
// + filter shape is a complete cache key. Bounded at a small number of
|
|
39
|
+
// distinct filter shapes (today: includeWebChats true/false × the empty
|
|
40
|
+
// filter set) to keep memory trivial.
|
|
41
|
+
const _statsPayloadCache = new Map();
|
|
42
|
+
const STATS_PAYLOAD_CACHE_LIMIT = 8;
|
|
31
43
|
|
|
32
44
|
async function main(argv = process.argv.slice(2), env = process.env) {
|
|
33
45
|
const { positionals, flags } = parseArgs(argv);
|
|
@@ -153,10 +165,12 @@ async function initCommand(flags, env) {
|
|
|
153
165
|
});
|
|
154
166
|
discoveryProgress.done();
|
|
155
167
|
const importSources = await chooseImportSources(flags, discovered);
|
|
168
|
+
let backfillSince = "";
|
|
156
169
|
|
|
157
170
|
if (!flags["skip-import"]) {
|
|
158
171
|
const since = await chooseImportSince(flags);
|
|
159
172
|
if (since) {
|
|
173
|
+
backfillSince = since;
|
|
160
174
|
printSection("Backfill");
|
|
161
175
|
printMuted(`Backing up existing history into ${fullPath(cfg.storage.root || paths(env).data)}`);
|
|
162
176
|
const progress = createProgressReporter();
|
|
@@ -170,6 +184,7 @@ async function initCommand(flags, env) {
|
|
|
170
184
|
}
|
|
171
185
|
|
|
172
186
|
const watcherSetup = await chooseWatcherSources(flags, discovered, importSources);
|
|
187
|
+
if (backfillSince) applyUpdateSincePreference(cfg, backfillSince);
|
|
173
188
|
cfg.imports.sources = watcherSetup.sources;
|
|
174
189
|
cfg.imports.autoDiscoverSources = watcherSetup.autoDiscoverSources;
|
|
175
190
|
saveConfig(cfg, env);
|
|
@@ -1445,6 +1460,7 @@ function printConfigSetupSummary(cfg, env, loginStatus = null, changed = false)
|
|
|
1445
1460
|
printCheck("Config", fullPath(paths(env).config));
|
|
1446
1461
|
printCheck("Watcher sources", (cfg.imports?.sources || []).join(", ") || "none");
|
|
1447
1462
|
printCheck("Import window", `${cfg.imports?.defaultSinceDays ?? 30} days`);
|
|
1463
|
+
printCheck("Update rebuild window", defaultImportSince(cfg));
|
|
1448
1464
|
printCheck("Auto-discover sources", cfg.imports?.autoDiscoverSources === false ? "disabled" : "enabled");
|
|
1449
1465
|
printCheck("Sync interval", formatConfigInterval(cfg.sync?.intervalMinutes ?? 30));
|
|
1450
1466
|
if (loginStatus) {
|
|
@@ -1796,19 +1812,35 @@ async function updateCommand(flags, env) {
|
|
|
1796
1812
|
return;
|
|
1797
1813
|
}
|
|
1798
1814
|
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1815
|
+
const preservedWebChats = preserveUpdateWebChatArchives(env);
|
|
1816
|
+
let restoredWebChats = false;
|
|
1817
|
+
try {
|
|
1818
|
+
if (running.running) {
|
|
1819
|
+
const stopped = stopSupervisor(env);
|
|
1820
|
+
if (stopped) {
|
|
1821
|
+
printCheck("Watcher", "stop signal sent");
|
|
1822
|
+
await waitForSupervisorExit(running.pid);
|
|
1823
|
+
}
|
|
1804
1824
|
}
|
|
1805
|
-
}
|
|
1806
1825
|
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1826
|
+
for (const target of targets) {
|
|
1827
|
+
fs.rmSync(target.path, { recursive: true, force: true });
|
|
1828
|
+
printCheck("Removed", fullPath(target.path));
|
|
1829
|
+
}
|
|
1830
|
+
ensureBaseDirs(p);
|
|
1831
|
+
const restored = restoreUpdateWebChatArchives(preservedWebChats, env);
|
|
1832
|
+
restoredWebChats = true;
|
|
1833
|
+
cleanupUpdatePreservedWebChats(preservedWebChats);
|
|
1834
|
+
if (restored.sessionCount) printCheck("Preserved web chats", `${restored.sessionCount} session(s)`);
|
|
1835
|
+
} catch (error) {
|
|
1836
|
+
if (!restoredWebChats) {
|
|
1837
|
+
const restored = restoreUpdateWebChatArchives(preservedWebChats, env);
|
|
1838
|
+
restoredWebChats = true;
|
|
1839
|
+
if (restored.sessionCount) printMuted(`Restored preserved web chats after update interruption: ${restored.sessionCount} session(s).`);
|
|
1840
|
+
}
|
|
1841
|
+
cleanupUpdatePreservedWebChats(preservedWebChats);
|
|
1842
|
+
throw error;
|
|
1810
1843
|
}
|
|
1811
|
-
ensureBaseDirs(p);
|
|
1812
1844
|
|
|
1813
1845
|
printSection("Import");
|
|
1814
1846
|
const progress = flags.json ? null : createProgressReporter();
|
|
@@ -1852,7 +1884,128 @@ async function updateCommand(flags, env) {
|
|
|
1852
1884
|
}
|
|
1853
1885
|
}
|
|
1854
1886
|
|
|
1855
|
-
printMuted("Config, redaction settings, web account labels, source histories, and recall integrations were not changed.");
|
|
1887
|
+
printMuted("Config, redaction settings, web account labels, web chat archives, source histories, and recall integrations were not changed.");
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
function preserveUpdateWebChatArchives(env) {
|
|
1891
|
+
const sessions = listSessions(env).filter(updatePreservedWebChatSession);
|
|
1892
|
+
if (!sessions.length) return { tempRoot: "", sessions: [], sessionCount: 0, fileCount: 0 };
|
|
1893
|
+
const archive = archiveRoot(env);
|
|
1894
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "agentlog-update-web-chats-"));
|
|
1895
|
+
const copied = new Set();
|
|
1896
|
+
let fileCount = 0;
|
|
1897
|
+
for (const session of sessions) {
|
|
1898
|
+
for (const sourcePath of updateWebChatArchivePaths(session)) {
|
|
1899
|
+
if (copyUpdateArchivePath(sourcePath, archive, tempRoot, copied)) fileCount++;
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
return { tempRoot, sessions, sessionCount: sessions.length, fileCount };
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
function updatePreservedWebChatSession(session) {
|
|
1906
|
+
if (isWebChatProvider(session?.provider, session?.sourceType)) return true;
|
|
1907
|
+
return /^\[(chatgpt|claude)\]conversations\//i.test(String(session?.scopeCanonical || ""));
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
function updateWebChatArchivePaths(session) {
|
|
1911
|
+
const values = [
|
|
1912
|
+
session?.conversationPath,
|
|
1913
|
+
session?.metadataPath,
|
|
1914
|
+
session?.transcriptPath,
|
|
1915
|
+
session?.eventPath,
|
|
1916
|
+
session?.viewPath,
|
|
1917
|
+
session?.rawPath
|
|
1918
|
+
];
|
|
1919
|
+
const rawRecords = Array.isArray(session?.rawFiles) ? session.rawFiles : [];
|
|
1920
|
+
for (const record of rawRecords) {
|
|
1921
|
+
values.push(record?.archivedPath, record?.sharedRawPath);
|
|
1922
|
+
}
|
|
1923
|
+
if (session?.rawPath) {
|
|
1924
|
+
const manifest = readJson(path.join(session.rawPath, "manifest.json"), null);
|
|
1925
|
+
for (const record of Array.isArray(manifest?.files) ? manifest.files : []) {
|
|
1926
|
+
values.push(record?.archivedPath, record?.sharedRawPath);
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
return values.filter(Boolean);
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
function copyUpdateArchivePath(sourcePath, archive, tempRoot, copied) {
|
|
1933
|
+
const archiveRootPath = path.resolve(archive);
|
|
1934
|
+
const resolved = path.resolve(String(sourcePath || ""));
|
|
1935
|
+
const relative = path.relative(archiveRootPath, resolved);
|
|
1936
|
+
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) return false;
|
|
1937
|
+
if (copied.has(relative)) return false;
|
|
1938
|
+
const stat = safeUpdateStat(resolved);
|
|
1939
|
+
if (!stat) return false;
|
|
1940
|
+
const target = path.join(tempRoot, relative);
|
|
1941
|
+
ensureDir(path.dirname(target));
|
|
1942
|
+
if (stat.isDirectory()) {
|
|
1943
|
+
fs.cpSync(resolved, target, { recursive: true, force: true, preserveTimestamps: true });
|
|
1944
|
+
} else if (stat.isFile()) {
|
|
1945
|
+
fs.copyFileSync(resolved, target);
|
|
1946
|
+
} else {
|
|
1947
|
+
return false;
|
|
1948
|
+
}
|
|
1949
|
+
copied.add(relative);
|
|
1950
|
+
return true;
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
function restoreUpdateWebChatArchives(snapshot, env) {
|
|
1954
|
+
if (!snapshot?.tempRoot) return { sessionCount: 0, fileCount: 0 };
|
|
1955
|
+
const archive = archiveRoot(env);
|
|
1956
|
+
ensureDir(archive);
|
|
1957
|
+
let fileCount = 0;
|
|
1958
|
+
walk(snapshot.tempRoot, (file) => {
|
|
1959
|
+
const relative = path.relative(snapshot.tempRoot, file);
|
|
1960
|
+
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) return;
|
|
1961
|
+
const target = path.join(archive, relative);
|
|
1962
|
+
ensureDir(path.dirname(target));
|
|
1963
|
+
fs.copyFileSync(file, target);
|
|
1964
|
+
fileCount++;
|
|
1965
|
+
});
|
|
1966
|
+
writeUpdatePreservedWebChatManifest(snapshot.sessions || [], env);
|
|
1967
|
+
return { sessionCount: snapshot.sessionCount || (snapshot.sessions || []).length, fileCount };
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
function writeUpdatePreservedWebChatManifest(sessions, env) {
|
|
1971
|
+
if (!sessions.length) return;
|
|
1972
|
+
const entries = sessions
|
|
1973
|
+
.map((session) => {
|
|
1974
|
+
const metadata = readJson(session.metadataPath, null) || session;
|
|
1975
|
+
if (!metadata?.sessionId) return null;
|
|
1976
|
+
return {
|
|
1977
|
+
sessionId: metadata.sessionId,
|
|
1978
|
+
provider: metadata.provider,
|
|
1979
|
+
repoCanonical: metadata.repoCanonical || "",
|
|
1980
|
+
scopeCanonical: metadata.scopeCanonical || "",
|
|
1981
|
+
startedAt: metadata.startedAt || "",
|
|
1982
|
+
conversationPath: metadata.conversationPath || session.conversationPath || "",
|
|
1983
|
+
metadataPath: session.metadataPath || "",
|
|
1984
|
+
transcriptPath: metadata.transcriptPath || session.transcriptPath || "",
|
|
1985
|
+
eventPath: metadata.eventPath || session.eventPath || "",
|
|
1986
|
+
viewPath: metadata.viewPath || session.viewPath || "",
|
|
1987
|
+
rawPath: metadata.rawPath || session.rawPath || ""
|
|
1988
|
+
};
|
|
1989
|
+
})
|
|
1990
|
+
.filter(Boolean)
|
|
1991
|
+
.sort((a, b) => String(b.startedAt).localeCompare(String(a.startedAt)));
|
|
1992
|
+
if (!entries.length) return;
|
|
1993
|
+
const manifestPath = path.join(archiveRoot(env), "sessions", "manifest.json");
|
|
1994
|
+
ensureDir(path.dirname(manifestPath));
|
|
1995
|
+
writeJson(manifestPath, { sessions: entries });
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
function cleanupUpdatePreservedWebChats(snapshot) {
|
|
1999
|
+
if (snapshot?.tempRoot) fs.rmSync(snapshot.tempRoot, { recursive: true, force: true });
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
function safeUpdateStat(target) {
|
|
2003
|
+
try {
|
|
2004
|
+
return fs.statSync(target);
|
|
2005
|
+
} catch (error) {
|
|
2006
|
+
if (error.code === "ENOENT") return null;
|
|
2007
|
+
throw error;
|
|
2008
|
+
}
|
|
1856
2009
|
}
|
|
1857
2010
|
|
|
1858
2011
|
async function waitForSupervisorExit(pid, timeoutMs = 5000) {
|
|
@@ -1907,11 +2060,41 @@ function updateTargets(env) {
|
|
|
1907
2060
|
}
|
|
1908
2061
|
|
|
1909
2062
|
function defaultImportSince(config) {
|
|
1910
|
-
const
|
|
1911
|
-
if (
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
2063
|
+
const updateSince = normalizeUpdateSincePreference(config?.imports?.updateSince);
|
|
2064
|
+
if (updateSince) return updateSince;
|
|
2065
|
+
return "all";
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
function shouldRememberUpdateSince(source, flags = {}) {
|
|
2069
|
+
if (!(flags.since || flags["import-since"])) return false;
|
|
2070
|
+
return String(source || "").trim().toLowerCase() === "all";
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
function rememberUpdateSincePreference(since, env) {
|
|
2074
|
+
const cfg = loadConfig(env);
|
|
2075
|
+
if (!applyUpdateSincePreference(cfg, since)) return false;
|
|
2076
|
+
cfg.updatedAt = new Date().toISOString();
|
|
2077
|
+
saveConfig(cfg, env);
|
|
2078
|
+
return true;
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
function applyUpdateSincePreference(cfg, since) {
|
|
2082
|
+
const normalized = normalizeUpdateSincePreference(since);
|
|
2083
|
+
if (!normalized) return false;
|
|
2084
|
+
cfg.imports = {
|
|
2085
|
+
...(cfg.imports || {}),
|
|
2086
|
+
updateSince: normalized
|
|
2087
|
+
};
|
|
2088
|
+
return true;
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
function normalizeUpdateSincePreference(value) {
|
|
2092
|
+
const text = String(value || "").trim().toLowerCase();
|
|
2093
|
+
if (!text) return "";
|
|
2094
|
+
if (text === "all") return "all";
|
|
2095
|
+
const match = text.match(/^(\d+)([dhm])$/);
|
|
2096
|
+
if (!match) return "";
|
|
2097
|
+
return `${Math.max(1, Math.round(Number(match[1])))}${match[2]}`;
|
|
1915
2098
|
}
|
|
1916
2099
|
|
|
1917
2100
|
function coalesceTargets(targets) {
|
|
@@ -2191,27 +2374,34 @@ async function importCommand(args, flags, env) {
|
|
|
2191
2374
|
if (flags.instructions || flags.instruction || flags.docs) {
|
|
2192
2375
|
return printWebExportInstructions(instructions, flags);
|
|
2193
2376
|
}
|
|
2194
|
-
let
|
|
2195
|
-
if (!
|
|
2377
|
+
let importFiles = webImportFiles(args, flags);
|
|
2378
|
+
if (!importFiles.length) {
|
|
2379
|
+
if (process.stdin.isTTY && !flags.json) return interactiveWebChatImport(sub, instructions, flags, env);
|
|
2380
|
+
return printWebExportInstructions(instructions, flags);
|
|
2381
|
+
}
|
|
2196
2382
|
let username = flags.username || "";
|
|
2197
2383
|
let displayName = flags["display-name"] || flags.displayName || "";
|
|
2198
2384
|
if (!username && process.stdin.isTTY) {
|
|
2199
2385
|
username = (await ask(`${sub} account username/email: `)).trim();
|
|
2200
2386
|
if (!displayName) displayName = (await ask(`Display name [${username}]: `)).trim() || username;
|
|
2201
2387
|
}
|
|
2202
|
-
|
|
2388
|
+
importFiles = importFiles.map((file) => resolveUserPath(file, { base: process.cwd() }));
|
|
2389
|
+
const progress = flags.json ? null : createProgressReporter();
|
|
2390
|
+
const result = importWebChat(instructions.source, importFiles.length === 1 ? importFiles[0] : importFiles, {
|
|
2203
2391
|
scope: flags.scope || "local",
|
|
2204
2392
|
dryRun: flags["dry-run"],
|
|
2205
2393
|
username,
|
|
2206
2394
|
displayName,
|
|
2207
|
-
accountId: flags["account-id"] || flags.accountId || ""
|
|
2395
|
+
accountId: flags["account-id"] || flags.accountId || "",
|
|
2396
|
+
onProgress: progress?.update
|
|
2208
2397
|
}, env);
|
|
2398
|
+
progress?.done();
|
|
2209
2399
|
if (flags.json) {
|
|
2210
2400
|
console.log(JSON.stringify(result, null, 2));
|
|
2211
2401
|
return;
|
|
2212
2402
|
}
|
|
2213
2403
|
printPageTitle("agentlog import", `${sub} ${flags["dry-run"] ? "dry run" : "complete"}`);
|
|
2214
|
-
printCheck("File", fullPath(result.file));
|
|
2404
|
+
printCheck(importFiles.length === 1 ? "File" : "Files", importFiles.length === 1 ? fullPath(result.file) : importFiles.map(fullPath).join(", "));
|
|
2215
2405
|
printCheck("Account", `${result.displayName || result.username} (${result.accountId})`);
|
|
2216
2406
|
printCheck("Conversations", String(result.conversations));
|
|
2217
2407
|
printCheck("Imported", String(result.imported));
|
|
@@ -2226,12 +2416,13 @@ async function importCommand(args, flags, env) {
|
|
|
2226
2416
|
: source === "all"
|
|
2227
2417
|
? loadConfig(env).imports?.sources
|
|
2228
2418
|
: undefined;
|
|
2419
|
+
const since = flags.since || flags["import-since"] || "30d";
|
|
2229
2420
|
const progress = flags.json ? null : createProgressReporter();
|
|
2230
2421
|
const results = importCliHistory(
|
|
2231
2422
|
{
|
|
2232
2423
|
source,
|
|
2233
2424
|
sources: explicitSources,
|
|
2234
|
-
since
|
|
2425
|
+
since,
|
|
2235
2426
|
repos,
|
|
2236
2427
|
dryRun: flags["dry-run"],
|
|
2237
2428
|
explainSkips: Boolean(flags["explain-skips"]),
|
|
@@ -2240,6 +2431,9 @@ async function importCommand(args, flags, env) {
|
|
|
2240
2431
|
env
|
|
2241
2432
|
);
|
|
2242
2433
|
progress?.done();
|
|
2434
|
+
if (!flags["dry-run"] && shouldRememberUpdateSince(source, flags)) {
|
|
2435
|
+
rememberUpdateSincePreference(since, env);
|
|
2436
|
+
}
|
|
2243
2437
|
if (flags.json) {
|
|
2244
2438
|
console.log(JSON.stringify(results, null, 2));
|
|
2245
2439
|
return;
|
|
@@ -2248,6 +2442,92 @@ async function importCommand(args, flags, env) {
|
|
|
2248
2442
|
printImportResults(results, { explainSkips: Boolean(flags["explain-skips"]) });
|
|
2249
2443
|
}
|
|
2250
2444
|
|
|
2445
|
+
function webImportFiles(args = [], flags = {}) {
|
|
2446
|
+
const files = [];
|
|
2447
|
+
const flagValue = flags.file || flags.files;
|
|
2448
|
+
if (Array.isArray(flagValue)) files.push(...flagValue);
|
|
2449
|
+
else if (flagValue) files.push(...String(flagValue).split(","));
|
|
2450
|
+
files.push(...args.slice(1));
|
|
2451
|
+
return files.map((item) => String(item || "").trim()).filter(Boolean);
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
async function interactiveWebChatImport(sub, instructions, flags = {}, env = process.env) {
|
|
2455
|
+
if (!instructions) throw new Error("unknown web export instruction source");
|
|
2456
|
+
printPageTitle("agentlog import", `${instructions.source} walkthrough`);
|
|
2457
|
+
printWebExportWalkthroughIntro(instructions);
|
|
2458
|
+
|
|
2459
|
+
const defaultPath = defaultWebExportPath(instructions.source);
|
|
2460
|
+
const defaultPathLabel = defaultPath ? compactPath(defaultPath) : "";
|
|
2461
|
+
const importFiles = [];
|
|
2462
|
+
while (true) {
|
|
2463
|
+
const prompt = importFiles.length
|
|
2464
|
+
? "Additional export path (blank when done): "
|
|
2465
|
+
: `Export path${defaultPathLabel ? ` [${defaultPathLabel}]` : ""}: `;
|
|
2466
|
+
let answer = (await ask(prompt)).trim();
|
|
2467
|
+
if (!answer && !importFiles.length && defaultPath) answer = defaultPath;
|
|
2468
|
+
if (!answer) break;
|
|
2469
|
+
importFiles.push(resolveUserPath(answer, { base: process.cwd() }));
|
|
2470
|
+
}
|
|
2471
|
+
if (!importFiles.length) {
|
|
2472
|
+
printMuted("No export path provided.");
|
|
2473
|
+
printWebExportInstructionBlock(instructions);
|
|
2474
|
+
return;
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
const usernameDefault = flags.username || "";
|
|
2478
|
+
const usernamePrompt = usernameDefault ? `Account username/email [${usernameDefault}]: ` : "Account username/email: ";
|
|
2479
|
+
const username = flags.username || (await ask(usernamePrompt)).trim();
|
|
2480
|
+
if (!username) throw new Error(`--username is required for a new ${instructions.label} export account`);
|
|
2481
|
+
const displayDefault = flags["display-name"] || flags.displayName || username;
|
|
2482
|
+
const displayName = flags["display-name"] || flags.displayName || ((await ask(`Display name [${displayDefault}]: `)).trim() || displayDefault);
|
|
2483
|
+
const shouldDryRun = Boolean(flags["dry-run"]);
|
|
2484
|
+
const runOptions = {
|
|
2485
|
+
scope: flags.scope || "local",
|
|
2486
|
+
dryRun: shouldDryRun,
|
|
2487
|
+
username,
|
|
2488
|
+
displayName,
|
|
2489
|
+
accountId: flags["account-id"] || flags.accountId || ""
|
|
2490
|
+
};
|
|
2491
|
+
const target = importFiles.length === 1 ? importFiles[0] : importFiles;
|
|
2492
|
+
const progress = createProgressReporter();
|
|
2493
|
+
const result = importWebChat(instructions.source, target, { ...runOptions, onProgress: progress.update }, env);
|
|
2494
|
+
progress.done();
|
|
2495
|
+
printWebChatImportResult(sub, result, importFiles, shouldDryRun);
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
function printWebExportWalkthroughIntro(instructions) {
|
|
2499
|
+
printSection(`${instructions.label} Export`);
|
|
2500
|
+
if (instructions.source === "chatgpt") {
|
|
2501
|
+
printMuted("If you have OpenAI-export.zip and it is large, unzip it first.");
|
|
2502
|
+
printMuted("Best path: the extracted OpenAI-export/User Online Activity folder.");
|
|
2503
|
+
printMuted("If your export is split, enter the parent folder or enter each Conversations__...chatgpt...part folder one at a time, then press Enter on a blank line.");
|
|
2504
|
+
} else {
|
|
2505
|
+
printMuted(`Enter the ${instructions.fileDescription} path. Press Enter on a blank line when done.`);
|
|
2506
|
+
}
|
|
2507
|
+
printCheck("Request page", instructions.requestUrl);
|
|
2508
|
+
printCheck("Help", instructions.helpUrl);
|
|
2509
|
+
}
|
|
2510
|
+
|
|
2511
|
+
function printWebChatImportResult(sub, result, importFiles, dryRun) {
|
|
2512
|
+
printPageTitle("agentlog import", `${sub} ${dryRun ? "dry run" : "complete"}`);
|
|
2513
|
+
printCheck(importFiles.length === 1 ? "File" : "Files", importFiles.length === 1 ? fullPath(result.file) : importFiles.map(fullPath).join(", "));
|
|
2514
|
+
printCheck("Account", `${result.displayName || result.username} (${result.accountId})`);
|
|
2515
|
+
printCheck("Conversations", String(result.conversations));
|
|
2516
|
+
printCheck("Imported", String(result.imported));
|
|
2517
|
+
printCheck("Skipped", String(result.skipped));
|
|
2518
|
+
for (const error of result.errors || []) console.log(` ! ${error}`);
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2521
|
+
function defaultWebExportPath(source) {
|
|
2522
|
+
if (source === "chatgpt") {
|
|
2523
|
+
const activity = path.join(os.homedir(), "Downloads", "OpenAI-export", "User Online Activity");
|
|
2524
|
+
if (fs.existsSync(activity)) return activity;
|
|
2525
|
+
const root = path.join(os.homedir(), "Downloads", "OpenAI-export");
|
|
2526
|
+
if (fs.existsSync(root)) return root;
|
|
2527
|
+
}
|
|
2528
|
+
return "";
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2251
2531
|
async function accountsCommand(args, flags, env, title = "agentlog accounts") {
|
|
2252
2532
|
const action = args[0] || "list";
|
|
2253
2533
|
if (action === "list") {
|
|
@@ -2427,7 +2707,11 @@ function readSessionView(sessionId, env) {
|
|
|
2427
2707
|
|
|
2428
2708
|
function sessionViewPayload(view, env = process.env, options = {}) {
|
|
2429
2709
|
const baked = ensureSessionView(view.session, env);
|
|
2430
|
-
const transcriptMessages = viewerTranscriptMessages(baked.transcript_messages || [],
|
|
2710
|
+
const transcriptMessages = viewerTranscriptMessages(baked.transcript_messages || [], {
|
|
2711
|
+
...options,
|
|
2712
|
+
session: view.session,
|
|
2713
|
+
env
|
|
2714
|
+
});
|
|
2431
2715
|
const canonicalEvents = viewerCanonicalEvents(baked.canonical_events || [], options);
|
|
2432
2716
|
const resumeCommand = resumeCommandForSession(view.session);
|
|
2433
2717
|
const payload = {
|
|
@@ -2444,6 +2728,7 @@ function sessionViewPayload(view, env = process.env, options = {}) {
|
|
|
2444
2728
|
chat_display_name: view.session.chatDisplayName || undefined,
|
|
2445
2729
|
chat_project_path: view.session.chatProjectPath || undefined,
|
|
2446
2730
|
conversation_kind: view.session.conversationKind || undefined,
|
|
2731
|
+
parent_composer_id: view.session.parentComposerId || undefined,
|
|
2447
2732
|
pinned: Boolean(view.session.pinned) || undefined,
|
|
2448
2733
|
cwd: view.session.cwd || undefined,
|
|
2449
2734
|
title: view.session.title || undefined,
|
|
@@ -2469,8 +2754,44 @@ function sessionViewPayload(view, env = process.env, options = {}) {
|
|
|
2469
2754
|
}
|
|
2470
2755
|
|
|
2471
2756
|
function viewerTranscriptMessages(messages, options = {}) {
|
|
2472
|
-
|
|
2473
|
-
|
|
2757
|
+
const withAttachments = options.session?.sessionId
|
|
2758
|
+
? messages.map((message, index) => viewerMessageWithAttachmentUrls(message, options.session, index, options.env || process.env))
|
|
2759
|
+
: messages;
|
|
2760
|
+
if (!options.compactTranscriptMetadata) return withAttachments;
|
|
2761
|
+
return withAttachments.map(compactViewerTranscriptMessage);
|
|
2762
|
+
}
|
|
2763
|
+
|
|
2764
|
+
function viewerMessageWithAttachmentUrls(message, session, position, env = process.env) {
|
|
2765
|
+
if (!message || typeof message !== "object") return message;
|
|
2766
|
+
const metadata = message.metadata && typeof message.metadata === "object" ? message.metadata : null;
|
|
2767
|
+
const attachments = Array.isArray(metadata?.attachments) ? metadata.attachments : [];
|
|
2768
|
+
if (!attachments.length) return message;
|
|
2769
|
+
const messageIndex = Number.isFinite(Number(message.index)) ? Number(message.index) : position;
|
|
2770
|
+
return {
|
|
2771
|
+
...message,
|
|
2772
|
+
metadata: {
|
|
2773
|
+
...metadata,
|
|
2774
|
+
attachments: attachments.map((attachment, attachmentIndex) => ({
|
|
2775
|
+
...attachment,
|
|
2776
|
+
...viewerAttachmentAvailability(session, message, attachment, attachmentIndex, env, messageIndex)
|
|
2777
|
+
}))
|
|
2778
|
+
}
|
|
2779
|
+
};
|
|
2780
|
+
}
|
|
2781
|
+
|
|
2782
|
+
function viewerAttachmentAvailability(session, message, attachment, attachmentIndex, env, messageIndex) {
|
|
2783
|
+
if (attachment?.url) return { url: attachment.url, available: true };
|
|
2784
|
+
const resolved = resolveSessionAttachmentRecord(session, message, attachment, attachmentIndex, env, { probeOnly: true });
|
|
2785
|
+
if (!resolved) return { available: false };
|
|
2786
|
+
if (resolved.cleanup) resolved.cleanup();
|
|
2787
|
+
return {
|
|
2788
|
+
url: sessionAttachmentUrl(session.sessionId, messageIndex, attachmentIndex),
|
|
2789
|
+
available: true
|
|
2790
|
+
};
|
|
2791
|
+
}
|
|
2792
|
+
|
|
2793
|
+
function sessionAttachmentUrl(sessionId, messageIndex, attachmentIndex) {
|
|
2794
|
+
return `/api/session-attachment?id=${encodeURIComponent(sessionId)}&message=${encodeURIComponent(String(messageIndex))}&attachment=${encodeURIComponent(String(attachmentIndex))}`;
|
|
2474
2795
|
}
|
|
2475
2796
|
|
|
2476
2797
|
function compactViewerTranscriptMessage(message) {
|
|
@@ -2652,19 +2973,30 @@ function historyWebCommand(flags, env) {
|
|
|
2652
2973
|
}
|
|
2653
2974
|
if (url.pathname === "/api/recent") {
|
|
2654
2975
|
const filters = historyFiltersFromSearchParams(url.searchParams, 20);
|
|
2655
|
-
|
|
2976
|
+
respondWithListSnapshot(req, res, "recent", filterShapeKey(filters), env, () =>
|
|
2977
|
+
listRecentSessions(filters.limit, filters, env)
|
|
2978
|
+
);
|
|
2656
2979
|
return;
|
|
2657
2980
|
}
|
|
2658
2981
|
if (url.pathname === "/api/tree") {
|
|
2659
2982
|
const filters = historyFiltersFromSearchParams(url.searchParams, 20);
|
|
2660
|
-
|
|
2983
|
+
respondWithListSnapshot(req, res, "tree", filterShapeKey(filters), env, () =>
|
|
2984
|
+
sessionTreePayload(filters, env)
|
|
2985
|
+
);
|
|
2661
2986
|
return;
|
|
2662
2987
|
}
|
|
2663
2988
|
if (url.pathname === "/api/repo-sessions") {
|
|
2664
2989
|
const filters = historyFiltersFromSearchParams(url.searchParams, 20);
|
|
2665
2990
|
const repoKey = url.searchParams.get("repo_key") || "";
|
|
2666
2991
|
const offset = Math.max(0, Number(url.searchParams.get("offset") || 0));
|
|
2667
|
-
|
|
2992
|
+
respondWithListSnapshot(
|
|
2993
|
+
req,
|
|
2994
|
+
res,
|
|
2995
|
+
"repo-sessions",
|
|
2996
|
+
repoSessionsFilterKey(repoKey, filters, offset),
|
|
2997
|
+
env,
|
|
2998
|
+
() => repoSessionsPayload(repoKey, filters, offset, env)
|
|
2999
|
+
);
|
|
2668
3000
|
return;
|
|
2669
3001
|
}
|
|
2670
3002
|
if (url.pathname === "/api/search" || url.pathname === "/search") {
|
|
@@ -2677,6 +3009,10 @@ function historyWebCommand(flags, env) {
|
|
|
2677
3009
|
writeJsonResponse(res, results);
|
|
2678
3010
|
return;
|
|
2679
3011
|
}
|
|
3012
|
+
if (url.pathname === "/api/session-attachment") {
|
|
3013
|
+
serveSessionAttachment(res, url.searchParams, env);
|
|
3014
|
+
return;
|
|
3015
|
+
}
|
|
2680
3016
|
if (url.pathname === "/api/session") {
|
|
2681
3017
|
try {
|
|
2682
3018
|
const includeMarkdown = url.searchParams.get("markdown") === "1";
|
|
@@ -2713,7 +3049,9 @@ function historyWebCommand(flags, env) {
|
|
|
2713
3049
|
}
|
|
2714
3050
|
if (url.pathname === "/api/stats") {
|
|
2715
3051
|
const filters = statsFiltersFromSearchParams(url.searchParams);
|
|
2716
|
-
|
|
3052
|
+
respondWithListSnapshot(req, res, "stats", filterShapeKey({ ...filters, limit: 0 }), env, (snapshot) =>
|
|
3053
|
+
memoizedStatsPayload(snapshot, filters, env)
|
|
3054
|
+
);
|
|
2717
3055
|
return;
|
|
2718
3056
|
}
|
|
2719
3057
|
if (isHome) {
|
|
@@ -2775,6 +3113,7 @@ function statsFiltersFromSearchParams(params) {
|
|
|
2775
3113
|
function sessionTreePayload(filters, env) {
|
|
2776
3114
|
const pageSize = clampLimit(filters.limit, 20);
|
|
2777
3115
|
const sessions = listHistorySessions(filters, env);
|
|
3116
|
+
const sourceOptionSessions = listHistorySessions(historySourceOptionFilters(filters), env);
|
|
2778
3117
|
const repos = new Map();
|
|
2779
3118
|
for (const session of sessions) {
|
|
2780
3119
|
const repoKey = sessionRepoKey(session);
|
|
@@ -2805,10 +3144,60 @@ function sessionTreePayload(filters, env) {
|
|
|
2805
3144
|
return {
|
|
2806
3145
|
generated_at: new Date().toISOString(),
|
|
2807
3146
|
count: sessions.length,
|
|
3147
|
+
available_source_options: availableHistorySourceOptions(sourceOptionSessions),
|
|
2808
3148
|
groups
|
|
2809
3149
|
};
|
|
2810
3150
|
}
|
|
2811
3151
|
|
|
3152
|
+
function historySourceOptionFilters(filters) {
|
|
3153
|
+
return {
|
|
3154
|
+
...(filters || {}),
|
|
3155
|
+
provider: "",
|
|
3156
|
+
source: "",
|
|
3157
|
+
sourceType: "",
|
|
3158
|
+
source_type: ""
|
|
3159
|
+
};
|
|
3160
|
+
}
|
|
3161
|
+
|
|
3162
|
+
function availableHistorySourceOptions(sessions) {
|
|
3163
|
+
const rows = Array.isArray(sessions) ? sessions : [];
|
|
3164
|
+
return HISTORY_PROVIDER_OPTIONS
|
|
3165
|
+
.map((option) => {
|
|
3166
|
+
const count = rows.filter((session) => historySourceOptionMatchesSession(option.source, session)).length;
|
|
3167
|
+
return count ? { value: option.source, label: option.label, count } : null;
|
|
3168
|
+
})
|
|
3169
|
+
.filter(Boolean);
|
|
3170
|
+
}
|
|
3171
|
+
|
|
3172
|
+
function historySourceOptionMatchesSession(source, session) {
|
|
3173
|
+
const provider = String(session?.provider || "").toLowerCase();
|
|
3174
|
+
const sourceType = String(session?.source_type || session?.sourceType || "").toLowerCase();
|
|
3175
|
+
const sourceTypes = {
|
|
3176
|
+
"codex-cli": ["codex-cli-history", "cli-history"],
|
|
3177
|
+
"codex-desktop": ["codex-desktop-history"],
|
|
3178
|
+
"codex-sdk": ["codex-sdk-history"],
|
|
3179
|
+
"claude-code-desktop": ["claude-code-desktop-metadata"],
|
|
3180
|
+
"claude-workspace": ["claude-workspace-desktop"],
|
|
3181
|
+
"claude-sdk": ["claude-sdk-history"],
|
|
3182
|
+
"devin-cli": ["devin-cli-history"],
|
|
3183
|
+
cline: ["cline-task-history"],
|
|
3184
|
+
"opencode-cli": ["opencode-cli-history", "opencode-cli-sqlite-history"],
|
|
3185
|
+
"opencode-desktop": ["opencode-desktop-history", "opencode-desktop-sqlite-history"],
|
|
3186
|
+
"opencode-web": ["opencode-web-sqlite-history"],
|
|
3187
|
+
aider: ["aider-chat-history"]
|
|
3188
|
+
}[source];
|
|
3189
|
+
if (sourceTypes) return sourceTypes.includes(sourceType);
|
|
3190
|
+
const providers = {
|
|
3191
|
+
chatgpt: ["chatgpt"],
|
|
3192
|
+
claude: ["claude_code"],
|
|
3193
|
+
"claude-web": ["claude_web"],
|
|
3194
|
+
"gemini-cli": ["gemini_cli"],
|
|
3195
|
+
antigravity: ["antigravity"],
|
|
3196
|
+
cursor: ["cursor"]
|
|
3197
|
+
}[source];
|
|
3198
|
+
return providers ? providers.includes(provider) : false;
|
|
3199
|
+
}
|
|
3200
|
+
|
|
2812
3201
|
function historySessionUpdatedAt(session) {
|
|
2813
3202
|
if (session.time_status === "recovered-time-unknown") return "";
|
|
2814
3203
|
return session.ended_at || session.started_at || session.archived_at || "";
|
|
@@ -2850,6 +3239,7 @@ function statsPayloadForSessions(sessions, options = {}) {
|
|
|
2850
3239
|
let totalInputTokens = 0;
|
|
2851
3240
|
let totalOutputTokens = 0;
|
|
2852
3241
|
let totalCacheTokens = 0;
|
|
3242
|
+
let totalReasoningTokens = 0;
|
|
2853
3243
|
let totalEstimatedTokens = 0;
|
|
2854
3244
|
let totalConversations = 0;
|
|
2855
3245
|
let totalMessages = 0;
|
|
@@ -2873,6 +3263,7 @@ function statsPayloadForSessions(sessions, options = {}) {
|
|
|
2873
3263
|
totalInputTokens += usageTokens.tokens_input;
|
|
2874
3264
|
totalOutputTokens += usageTokens.tokens_output;
|
|
2875
3265
|
totalCacheTokens += usageTokens.tokens_cache;
|
|
3266
|
+
totalReasoningTokens += usageTokens.tokens_reasoning;
|
|
2876
3267
|
totalEstimatedTokens += usageTokens.tokens_estimated;
|
|
2877
3268
|
totalConversations += 1;
|
|
2878
3269
|
totalMessages += Number.isFinite(messageCount) ? messageCount : 0;
|
|
@@ -2900,7 +3291,7 @@ function statsPayloadForSessions(sessions, options = {}) {
|
|
|
2900
3291
|
dayBucket.models[modelGroup].conversations += 1;
|
|
2901
3292
|
dayBucket.models[modelGroup].user_messages += userMessageCount;
|
|
2902
3293
|
|
|
2903
|
-
if (!dailyActivity.has(dayKey)) dailyActivity.set(dayKey, { sessions: 0, conversations: 0, user_messages: 0, tokens: 0, tokens_input: 0, tokens_output: 0, tokens_cache: 0, tokens_estimated: 0, messages: 0 });
|
|
3294
|
+
if (!dailyActivity.has(dayKey)) dailyActivity.set(dayKey, { sessions: 0, conversations: 0, user_messages: 0, tokens: 0, tokens_input: 0, tokens_output: 0, tokens_cache: 0, tokens_reasoning: 0, tokens_estimated: 0, messages: 0 });
|
|
2904
3295
|
const activity = dailyActivity.get(dayKey);
|
|
2905
3296
|
activity.sessions += 1;
|
|
2906
3297
|
activity.conversations += 1;
|
|
@@ -2912,7 +3303,7 @@ function statsPayloadForSessions(sessions, options = {}) {
|
|
|
2912
3303
|
if (!isWebChatStatsProvider(provider)) {
|
|
2913
3304
|
const repoKey = sessionRepoKey(session);
|
|
2914
3305
|
const repoLabel = sessionRepoLabel(session);
|
|
2915
|
-
if (!repoMap.has(repoKey)) repoMap.set(repoKey, { repo_key: repoKey, repo_display: repoLabel, providers: {}, companies: {}, models: {}, tokens: 0, tokens_input: 0, tokens_output: 0, tokens_cache: 0, tokens_estimated: 0, conversations: 0, user_messages: 0 });
|
|
3306
|
+
if (!repoMap.has(repoKey)) repoMap.set(repoKey, { repo_key: repoKey, repo_display: repoLabel, providers: {}, companies: {}, models: {}, tokens: 0, tokens_input: 0, tokens_output: 0, tokens_cache: 0, tokens_reasoning: 0, tokens_estimated: 0, conversations: 0, user_messages: 0 });
|
|
2916
3307
|
const repoBucket = repoMap.get(repoKey);
|
|
2917
3308
|
repoBucket.repo_display = repoLabel;
|
|
2918
3309
|
if (!repoBucket.providers[provider]) repoBucket.providers[provider] = statsBucket();
|
|
@@ -2934,7 +3325,7 @@ function statsPayloadForSessions(sessions, options = {}) {
|
|
|
2934
3325
|
if (dayKey) {
|
|
2935
3326
|
if (!dailyRepoMap.has(dayKey)) dailyRepoMap.set(dayKey, new Map());
|
|
2936
3327
|
const dailyRepos = dailyRepoMap.get(dayKey);
|
|
2937
|
-
if (!dailyRepos.has(repoKey)) dailyRepos.set(repoKey, { repo_key: repoKey, repo_display: repoLabel, providers: {}, companies: {}, models: {}, tokens: 0, tokens_input: 0, tokens_output: 0, tokens_cache: 0, tokens_estimated: 0, conversations: 0, user_messages: 0 });
|
|
3328
|
+
if (!dailyRepos.has(repoKey)) dailyRepos.set(repoKey, { repo_key: repoKey, repo_display: repoLabel, providers: {}, companies: {}, models: {}, tokens: 0, tokens_input: 0, tokens_output: 0, tokens_cache: 0, tokens_reasoning: 0, tokens_estimated: 0, conversations: 0, user_messages: 0 });
|
|
2938
3329
|
const dailyRepoBucket = dailyRepos.get(repoKey);
|
|
2939
3330
|
dailyRepoBucket.repo_display = repoLabel;
|
|
2940
3331
|
if (!dailyRepoBucket.providers[provider]) dailyRepoBucket.providers[provider] = statsBucket();
|
|
@@ -3015,6 +3406,7 @@ function statsPayloadForSessions(sessions, options = {}) {
|
|
|
3015
3406
|
tokens_input: entry.tokens_input,
|
|
3016
3407
|
tokens_output: entry.tokens_output,
|
|
3017
3408
|
tokens_cache: entry.tokens_cache,
|
|
3409
|
+
tokens_reasoning: entry.tokens_reasoning,
|
|
3018
3410
|
tokens_estimated: entry.tokens_estimated,
|
|
3019
3411
|
conversations: entry.conversations,
|
|
3020
3412
|
user_messages: entry.user_messages,
|
|
@@ -3037,6 +3429,7 @@ function statsPayloadForSessions(sessions, options = {}) {
|
|
|
3037
3429
|
tokens_input: entry.tokens_input,
|
|
3038
3430
|
tokens_output: entry.tokens_output,
|
|
3039
3431
|
tokens_cache: entry.tokens_cache,
|
|
3432
|
+
tokens_reasoning: entry.tokens_reasoning,
|
|
3040
3433
|
tokens_estimated: entry.tokens_estimated,
|
|
3041
3434
|
conversations: entry.conversations,
|
|
3042
3435
|
user_messages: entry.user_messages,
|
|
@@ -3083,7 +3476,7 @@ function statsPayloadForSessions(sessions, options = {}) {
|
|
|
3083
3476
|
const favoriteProviderEntry = byProvider[0] || null;
|
|
3084
3477
|
const dailyActivityList = activitySorted.map((day) => {
|
|
3085
3478
|
const entry = dailyActivity.get(day);
|
|
3086
|
-
return { date: day, sessions: entry.sessions, conversations: entry.conversations, user_messages: entry.user_messages, messages: entry.messages, tokens: entry.tokens, tokens_input: entry.tokens_input, tokens_output: entry.tokens_output, tokens_cache: entry.tokens_cache, tokens_estimated: entry.tokens_estimated };
|
|
3479
|
+
return { date: day, sessions: entry.sessions, conversations: entry.conversations, user_messages: entry.user_messages, messages: entry.messages, tokens: entry.tokens, tokens_input: entry.tokens_input, tokens_output: entry.tokens_output, tokens_cache: entry.tokens_cache, tokens_reasoning: entry.tokens_reasoning, tokens_estimated: entry.tokens_estimated };
|
|
3087
3480
|
});
|
|
3088
3481
|
const monthAgg = new Map();
|
|
3089
3482
|
let peakDayTokens = 0;
|
|
@@ -3135,6 +3528,7 @@ function statsPayloadForSessions(sessions, options = {}) {
|
|
|
3135
3528
|
sdk_total_input_tokens: sdkStats ? sdkStats.total_input_tokens : undefined,
|
|
3136
3529
|
sdk_total_output_tokens: sdkStats ? sdkStats.total_output_tokens : undefined,
|
|
3137
3530
|
sdk_total_cache_tokens: sdkStats ? sdkStats.total_cache_tokens : undefined,
|
|
3531
|
+
sdk_total_reasoning_tokens: sdkStats ? sdkStats.total_reasoning_tokens : undefined,
|
|
3138
3532
|
sdk_total_estimated_tokens: sdkStats ? sdkStats.total_estimated_tokens : undefined,
|
|
3139
3533
|
message_count: totalMessages,
|
|
3140
3534
|
user_message_count: totalUserMessages,
|
|
@@ -3142,6 +3536,7 @@ function statsPayloadForSessions(sessions, options = {}) {
|
|
|
3142
3536
|
total_input_tokens: totalInputTokens,
|
|
3143
3537
|
total_output_tokens: totalOutputTokens,
|
|
3144
3538
|
total_cache_tokens: totalCacheTokens,
|
|
3539
|
+
total_reasoning_tokens: totalReasoningTokens,
|
|
3145
3540
|
total_estimated_tokens: totalEstimatedTokens,
|
|
3146
3541
|
active_days: activeDays,
|
|
3147
3542
|
current_streak: currentStreak,
|
|
@@ -3261,7 +3656,7 @@ function statsUsageForSession(session) {
|
|
|
3261
3656
|
}
|
|
3262
3657
|
|
|
3263
3658
|
function statsBucket() {
|
|
3264
|
-
return { tokens: 0, tokens_input: 0, tokens_output: 0, tokens_cache: 0, tokens_estimated: 0, conversations: 0, user_messages: 0 };
|
|
3659
|
+
return { tokens: 0, tokens_input: 0, tokens_output: 0, tokens_cache: 0, tokens_reasoning: 0, tokens_estimated: 0, conversations: 0, user_messages: 0 };
|
|
3265
3660
|
}
|
|
3266
3661
|
|
|
3267
3662
|
function statsUserMessageCount(session) {
|
|
@@ -3275,6 +3670,7 @@ function addStatsTokens(bucket, usageTokens) {
|
|
|
3275
3670
|
bucket.tokens_input += usageTokens.tokens_input;
|
|
3276
3671
|
bucket.tokens_output += usageTokens.tokens_output;
|
|
3277
3672
|
bucket.tokens_cache += usageTokens.tokens_cache;
|
|
3673
|
+
bucket.tokens_reasoning += usageTokens.tokens_reasoning;
|
|
3278
3674
|
bucket.tokens_estimated += usageTokens.tokens_estimated;
|
|
3279
3675
|
}
|
|
3280
3676
|
|
|
@@ -3282,15 +3678,32 @@ function statsTokenBreakdown(usage) {
|
|
|
3282
3678
|
let input = positiveStatsNumber(usage?.inputTokens);
|
|
3283
3679
|
let output = positiveStatsNumber(usage?.outputTokens);
|
|
3284
3680
|
const cache = positiveStatsNumber(usage?.cacheInputTokens);
|
|
3285
|
-
const
|
|
3286
|
-
|
|
3287
|
-
|
|
3681
|
+
const cacheIncluded =
|
|
3682
|
+
usage?.cacheInputTokensIncludedInInput === true ||
|
|
3683
|
+
usage?.cache_input_tokens_included_in_input === true ||
|
|
3684
|
+
usage?.cacheReadTokensIncludedInInput === true ||
|
|
3685
|
+
usage?.cache_read_tokens_included_in_input === true;
|
|
3686
|
+
const countedCache = cacheIncluded ? 0 : cache;
|
|
3687
|
+
const reasoning = positiveStatsNumber(usage?.reasoningOutputTokens);
|
|
3688
|
+
const reasoningIncluded =
|
|
3689
|
+
usage?.reasoningOutputTokensIncludedInOutput === true ||
|
|
3690
|
+
usage?.reasoning_output_tokens_included_in_output === true ||
|
|
3691
|
+
usage?.reasoningTokensIncludedInOutput === true ||
|
|
3692
|
+
usage?.reasoning_tokens_included_in_output === true;
|
|
3693
|
+
const countedReasoning = reasoningIncluded ? 0 : reasoning;
|
|
3694
|
+
const splitTokens = input + output + countedCache + countedReasoning;
|
|
3695
|
+
const totalTokens = positiveStatsNumber(usage?.totalTokens);
|
|
3696
|
+
if (!splitTokens && totalTokens > 0) input = totalTokens;
|
|
3697
|
+
const categoryTokens = input + output + countedCache + countedReasoning;
|
|
3698
|
+
const authoritative = usage?.authoritativeTotalTokens === true || usage?.authoritative_total_tokens === true;
|
|
3699
|
+
const tokens = authoritative && totalTokens ? totalTokens : Math.max(totalTokens, categoryTokens);
|
|
3288
3700
|
return {
|
|
3289
|
-
tokens
|
|
3701
|
+
tokens,
|
|
3290
3702
|
tokens_input: input,
|
|
3291
3703
|
tokens_output: output,
|
|
3292
3704
|
tokens_cache: cache,
|
|
3293
|
-
|
|
3705
|
+
tokens_reasoning: reasoning,
|
|
3706
|
+
tokens_estimated: usage?.estimated ? tokens : 0
|
|
3294
3707
|
};
|
|
3295
3708
|
}
|
|
3296
3709
|
|
|
@@ -3470,6 +3883,90 @@ function writeNotModifiedSessionResponse(res, etag) {
|
|
|
3470
3883
|
res.end();
|
|
3471
3884
|
}
|
|
3472
3885
|
|
|
3886
|
+
/**
|
|
3887
|
+
* Compute an etag for a list-derived endpoint (recent/tree/repo-sessions/stats).
|
|
3888
|
+
* Combines the listSessions snapshot fingerprint with the endpoint kind,
|
|
3889
|
+
* filter shape, and renderer version. The fingerprint changes iff any
|
|
3890
|
+
* session's metadata mtime/size changed or the count changed, so the etag
|
|
3891
|
+
* automatically invalidates when the supervisor imports new history.
|
|
3892
|
+
*/
|
|
3893
|
+
function listSnapshotEtag(snapshot, kind, filterKey) {
|
|
3894
|
+
if (!snapshot || !snapshot.fingerprint) return "";
|
|
3895
|
+
const components = [
|
|
3896
|
+
snapshot.fingerprint,
|
|
3897
|
+
String(kind || ""),
|
|
3898
|
+
String(filterKey || ""),
|
|
3899
|
+
`v${LIST_PAYLOAD_VERSION}`
|
|
3900
|
+
].join("|");
|
|
3901
|
+
const digest = crypto.createHash("sha1").update(components).digest("base64url");
|
|
3902
|
+
return `"list-${digest}"`;
|
|
3903
|
+
}
|
|
3904
|
+
|
|
3905
|
+
function filterShapeKey(filters) {
|
|
3906
|
+
if (!filters || typeof filters !== "object") return "default";
|
|
3907
|
+
return [
|
|
3908
|
+
`repo=${filters.repo || ""}`,
|
|
3909
|
+
`provider=${filters.provider || ""}`,
|
|
3910
|
+
`sourceType=${filters.sourceType || ""}`,
|
|
3911
|
+
`since=${filters.since || ""}`,
|
|
3912
|
+
`limit=${filters.limit ?? ""}`,
|
|
3913
|
+
`webChats=${filters.includeWebChats === false ? "0" : "1"}`
|
|
3914
|
+
].join("&");
|
|
3915
|
+
}
|
|
3916
|
+
|
|
3917
|
+
function repoSessionsFilterKey(repoKey, filters, offset) {
|
|
3918
|
+
return `${repoKey || ""}|${offset || 0}|${filterShapeKey(filters)}`;
|
|
3919
|
+
}
|
|
3920
|
+
|
|
3921
|
+
/**
|
|
3922
|
+
* Wrap a list endpoint with snapshot-based etag handling.
|
|
3923
|
+
*
|
|
3924
|
+
* - `kind` is a stable string per endpoint (e.g. "recent", "tree").
|
|
3925
|
+
* - `filterKey` describes the shape of filters/pagination so distinct queries
|
|
3926
|
+
* get distinct etags.
|
|
3927
|
+
* - `compute(snapshot)` builds the payload. The snapshot is passed so callers
|
|
3928
|
+
* can avoid re-walking the archive.
|
|
3929
|
+
*
|
|
3930
|
+
* Returns true once the response has been written (either 304 or full body),
|
|
3931
|
+
* letting the dispatcher `return` immediately. Errors fall through to the
|
|
3932
|
+
* caller so existing handlers keep their error shape.
|
|
3933
|
+
*/
|
|
3934
|
+
function respondWithListSnapshot(req, res, kind, filterKey, env, compute) {
|
|
3935
|
+
const snapshot = cachedListSessionsSnapshot(env);
|
|
3936
|
+
const etag = listSnapshotEtag(snapshot, kind, filterKey);
|
|
3937
|
+
if (etag && requestMatchesEtag(req, etag)) {
|
|
3938
|
+
res.writeHead(304, {
|
|
3939
|
+
...securityHeaders("application/json; charset=utf-8"),
|
|
3940
|
+
...sessionCacheHeaders(etag)
|
|
3941
|
+
});
|
|
3942
|
+
res.end();
|
|
3943
|
+
return true;
|
|
3944
|
+
}
|
|
3945
|
+
const payload = compute(snapshot);
|
|
3946
|
+
writeJsonResponse(res, payload, 200, { headers: sessionCacheHeaders(etag) });
|
|
3947
|
+
return true;
|
|
3948
|
+
}
|
|
3949
|
+
|
|
3950
|
+
function memoizedStatsPayload(snapshot, filters, env) {
|
|
3951
|
+
const filterKey = filterShapeKey({ ...filters, limit: 0 });
|
|
3952
|
+
const cacheKey = `${snapshot.fingerprint}|${filterKey}`;
|
|
3953
|
+
const cached = _statsPayloadCache.get(cacheKey);
|
|
3954
|
+
if (cached) {
|
|
3955
|
+
// Refresh LRU position so frequently-used payloads don't get evicted by
|
|
3956
|
+
// less-used filter combinations.
|
|
3957
|
+
_statsPayloadCache.delete(cacheKey);
|
|
3958
|
+
_statsPayloadCache.set(cacheKey, cached);
|
|
3959
|
+
return cached;
|
|
3960
|
+
}
|
|
3961
|
+
const payload = statsPayload(filters, env);
|
|
3962
|
+
_statsPayloadCache.set(cacheKey, payload);
|
|
3963
|
+
while (_statsPayloadCache.size > STATS_PAYLOAD_CACHE_LIMIT) {
|
|
3964
|
+
const oldest = _statsPayloadCache.keys().next().value;
|
|
3965
|
+
_statsPayloadCache.delete(oldest);
|
|
3966
|
+
}
|
|
3967
|
+
return payload;
|
|
3968
|
+
}
|
|
3969
|
+
|
|
3473
3970
|
function writeJsonResponse(res, payload, status = 200, options = {}) {
|
|
3474
3971
|
res.writeHead(status, {
|
|
3475
3972
|
...securityHeaders("application/json; charset=utf-8"),
|
|
@@ -3478,6 +3975,469 @@ function writeJsonResponse(res, payload, status = 200, options = {}) {
|
|
|
3478
3975
|
res.end(options.pretty ? JSON.stringify(payload, null, 2) : JSON.stringify(payload));
|
|
3479
3976
|
}
|
|
3480
3977
|
|
|
3978
|
+
function serveSessionAttachment(res, params, env = process.env) {
|
|
3979
|
+
let resolved;
|
|
3980
|
+
try {
|
|
3981
|
+
resolved = resolveSessionAttachmentFile(params, env);
|
|
3982
|
+
} catch (error) {
|
|
3983
|
+
writeJsonResponse(res, { error: error.message }, 404);
|
|
3984
|
+
return;
|
|
3985
|
+
}
|
|
3986
|
+
let stat;
|
|
3987
|
+
try {
|
|
3988
|
+
stat = fs.statSync(resolved.file);
|
|
3989
|
+
} catch (error) {
|
|
3990
|
+
if (resolved.cleanup) resolved.cleanup();
|
|
3991
|
+
writeJsonResponse(res, { error: error.message }, 404);
|
|
3992
|
+
return;
|
|
3993
|
+
}
|
|
3994
|
+
const headers = {
|
|
3995
|
+
...securityHeaders(resolved.contentType || "application/octet-stream"),
|
|
3996
|
+
"cache-control": "private, max-age=3600",
|
|
3997
|
+
"content-length": String(stat.size),
|
|
3998
|
+
"content-disposition": contentDispositionInline(resolved.name || path.basename(resolved.file))
|
|
3999
|
+
};
|
|
4000
|
+
res.writeHead(200, headers);
|
|
4001
|
+
const stream = fs.createReadStream(resolved.file);
|
|
4002
|
+
let cleaned = false;
|
|
4003
|
+
const cleanup = () => {
|
|
4004
|
+
if (cleaned) return;
|
|
4005
|
+
cleaned = true;
|
|
4006
|
+
if (resolved.cleanup) resolved.cleanup();
|
|
4007
|
+
};
|
|
4008
|
+
stream.on("error", (error) => {
|
|
4009
|
+
cleanup();
|
|
4010
|
+
res.destroy(error);
|
|
4011
|
+
});
|
|
4012
|
+
stream.on("close", cleanup);
|
|
4013
|
+
res.on("close", cleanup);
|
|
4014
|
+
stream.pipe(res);
|
|
4015
|
+
}
|
|
4016
|
+
|
|
4017
|
+
function contentDispositionInline(name) {
|
|
4018
|
+
const safe = String(name || "attachment")
|
|
4019
|
+
.replace(/[\\/\r\n"]/g, "_")
|
|
4020
|
+
.slice(0, 180) || "attachment";
|
|
4021
|
+
return `inline; filename="${safe}"`;
|
|
4022
|
+
}
|
|
4023
|
+
|
|
4024
|
+
function resolveSessionAttachmentFile(params, env = process.env, options = {}) {
|
|
4025
|
+
const sessionId = paramValue(params, "id");
|
|
4026
|
+
const messageNumber = Number(paramValue(params, "message"));
|
|
4027
|
+
const attachmentNumber = Number(paramValue(params, "attachment"));
|
|
4028
|
+
if (!sessionId) throw new Error("missing session id");
|
|
4029
|
+
if (!Number.isInteger(messageNumber) || messageNumber < 0) throw new Error("missing message index");
|
|
4030
|
+
if (!Number.isInteger(attachmentNumber) || attachmentNumber < 0) throw new Error("missing attachment index");
|
|
4031
|
+
const session = findSessionById(sessionId, env);
|
|
4032
|
+
if (!session) throw new Error(`no archived session found for ${sessionId}`);
|
|
4033
|
+
const messages = readTranscript(session.transcriptPath);
|
|
4034
|
+
const message = messages.find((item) => Number(item?.index) === messageNumber) || messages[messageNumber];
|
|
4035
|
+
const attachments = Array.isArray(message?.metadata?.attachments) ? message.metadata.attachments : [];
|
|
4036
|
+
const attachment = attachments[attachmentNumber];
|
|
4037
|
+
if (!attachment) throw new Error("attachment not found");
|
|
4038
|
+
const record = resolveSessionAttachmentRecord(session, message, attachment, attachmentNumber, env, options);
|
|
4039
|
+
if (!record) throw new Error("attachment file was not included in the raw export");
|
|
4040
|
+
return record;
|
|
4041
|
+
}
|
|
4042
|
+
|
|
4043
|
+
function resolveSessionAttachmentRecord(session, message, attachment, attachmentNumber, env = process.env, options = {}) {
|
|
4044
|
+
const context = attachmentResolutionContext(session, message, attachment, attachmentNumber);
|
|
4045
|
+
const resolved = resolveAttachmentFromRawManifests(session, context, env, options);
|
|
4046
|
+
if (!resolved) return null;
|
|
4047
|
+
return {
|
|
4048
|
+
...resolved,
|
|
4049
|
+
name: attachment.name || resolved.name,
|
|
4050
|
+
contentType: attachmentContentType(attachment, resolved.name || resolved.file)
|
|
4051
|
+
};
|
|
4052
|
+
}
|
|
4053
|
+
|
|
4054
|
+
function paramValue(params, key) {
|
|
4055
|
+
if (params && typeof params.get === "function") return params.get(key) || "";
|
|
4056
|
+
return params?.[key] || "";
|
|
4057
|
+
}
|
|
4058
|
+
|
|
4059
|
+
function attachmentResolutionContext(session, message, attachment, attachmentIndex) {
|
|
4060
|
+
const conversationEntryPath = webRawReferenceEntryPath(session);
|
|
4061
|
+
const conversationDir = conversationEntryPath ? posixDirname(conversationEntryPath) : "";
|
|
4062
|
+
const conversationPart = conversationDir.split("/").filter(Boolean).pop() || "";
|
|
4063
|
+
const assetPointers = Array.isArray(message?.metadata?.assetPointers) ? message.metadata.assetPointers : [];
|
|
4064
|
+
const names = new Set();
|
|
4065
|
+
const ids = new Set();
|
|
4066
|
+
const pointers = new Set();
|
|
4067
|
+
addAttachmentName(names, attachment?.name);
|
|
4068
|
+
addAttachmentId(ids, attachment?.id);
|
|
4069
|
+
addAttachmentId(ids, attachment?.libraryFileId);
|
|
4070
|
+
addAttachmentPointer(pointers, attachment?.assetPointer);
|
|
4071
|
+
if (assetPointers.length === 1 || assetPointers.length === attachmentIndex + 1) {
|
|
4072
|
+
addAttachmentPointer(pointers, assetPointers[attachmentIndex]?.assetPointer);
|
|
4073
|
+
}
|
|
4074
|
+
for (const pointer of pointers) addAttachmentId(ids, pointer);
|
|
4075
|
+
return {
|
|
4076
|
+
attachment,
|
|
4077
|
+
attachmentIndex,
|
|
4078
|
+
names: [...names],
|
|
4079
|
+
ids: [...ids],
|
|
4080
|
+
pointers: [...pointers],
|
|
4081
|
+
conversationEntryPath,
|
|
4082
|
+
conversationDir,
|
|
4083
|
+
conversationPart,
|
|
4084
|
+
isImage: attachmentIsImage(attachment)
|
|
4085
|
+
};
|
|
4086
|
+
}
|
|
4087
|
+
|
|
4088
|
+
function addAttachmentName(set, value) {
|
|
4089
|
+
const base = posixBasename(String(value || "").trim());
|
|
4090
|
+
if (base) set.add(base);
|
|
4091
|
+
}
|
|
4092
|
+
|
|
4093
|
+
function addAttachmentId(set, value) {
|
|
4094
|
+
const token = attachmentToken(value);
|
|
4095
|
+
if (token) set.add(token);
|
|
4096
|
+
}
|
|
4097
|
+
|
|
4098
|
+
function addAttachmentPointer(set, value) {
|
|
4099
|
+
const text = String(value || "").trim();
|
|
4100
|
+
if (text) set.add(text);
|
|
4101
|
+
}
|
|
4102
|
+
|
|
4103
|
+
function attachmentToken(value) {
|
|
4104
|
+
return posixBasename(String(value || "")
|
|
4105
|
+
.trim()
|
|
4106
|
+
.toLowerCase()
|
|
4107
|
+
.replace(/^file-service:\/\//, "")
|
|
4108
|
+
.replace(/^sandbox:\/\//, "")
|
|
4109
|
+
.replace(/^attachment:\/\//, "")
|
|
4110
|
+
.replace(/[?#].*$/, ""));
|
|
4111
|
+
}
|
|
4112
|
+
|
|
4113
|
+
function webRawReferenceEntryPath(session) {
|
|
4114
|
+
const ref = (Array.isArray(session?.rawFiles) ? session.rawFiles : [])
|
|
4115
|
+
.find((item) => item?.filename === "web-export-reference" && item.entryPath);
|
|
4116
|
+
return ref?.entryPath || "";
|
|
4117
|
+
}
|
|
4118
|
+
|
|
4119
|
+
function resolveAttachmentFromRawManifests(session, context, env = process.env, options = {}) {
|
|
4120
|
+
const manifests = webRawExportManifests(session, env);
|
|
4121
|
+
for (const manifest of manifests) {
|
|
4122
|
+
const direct = bestDirectRawAttachment(manifest, context);
|
|
4123
|
+
if (direct) return direct;
|
|
4124
|
+
}
|
|
4125
|
+
for (const manifest of manifests) {
|
|
4126
|
+
const zipped = bestZipRawAttachment(manifest, context, options);
|
|
4127
|
+
if (zipped) return zipped;
|
|
4128
|
+
}
|
|
4129
|
+
return null;
|
|
4130
|
+
}
|
|
4131
|
+
|
|
4132
|
+
function webRawExportManifests(session, env = process.env) {
|
|
4133
|
+
const manifests = [];
|
|
4134
|
+
for (const raw of Array.isArray(session?.rawFiles) ? session.rawFiles : []) {
|
|
4135
|
+
if (raw?.filename !== "web-export-reference" && !/web export raw manifest/i.test(String(raw?.note || ""))) continue;
|
|
4136
|
+
const manifestPath = raw.archivedPath || "";
|
|
4137
|
+
const manifest = manifestPath ? readJson(manifestPath, null) : null;
|
|
4138
|
+
if (manifest && typeof manifest === "object") manifests.push({ manifest, reference: raw, manifestPath });
|
|
4139
|
+
}
|
|
4140
|
+
const sessionManifestPath = session?.rawPath ? path.join(session.rawPath, "manifest.json") : "";
|
|
4141
|
+
const sessionManifest = sessionManifestPath ? readJson(sessionManifestPath, null) : null;
|
|
4142
|
+
if (sessionManifest && typeof sessionManifest === "object") {
|
|
4143
|
+
manifests.push({ manifest: sessionManifest, reference: null, manifestPath: sessionManifestPath });
|
|
4144
|
+
}
|
|
4145
|
+
return manifests;
|
|
4146
|
+
}
|
|
4147
|
+
|
|
4148
|
+
function bestDirectRawAttachment(rawManifest, context) {
|
|
4149
|
+
let best = null;
|
|
4150
|
+
for (const file of Array.isArray(rawManifest?.manifest?.files) ? rawManifest.manifest.files : []) {
|
|
4151
|
+
if (!file?.archivedPath || !file.entryPath) continue;
|
|
4152
|
+
if (isZipPath(file.entryPath) || isZipPath(file.archivedPath)) continue;
|
|
4153
|
+
const score = attachmentPathScore(file.entryPath, context);
|
|
4154
|
+
if (score < 100) continue;
|
|
4155
|
+
if (!best || score > best.score) best = { score, file };
|
|
4156
|
+
}
|
|
4157
|
+
if (!best) return null;
|
|
4158
|
+
return {
|
|
4159
|
+
kind: "file",
|
|
4160
|
+
file: best.file.archivedPath,
|
|
4161
|
+
name: posixBasename(best.file.entryPath),
|
|
4162
|
+
entryPath: best.file.entryPath
|
|
4163
|
+
};
|
|
4164
|
+
}
|
|
4165
|
+
|
|
4166
|
+
function bestZipRawAttachment(rawManifest, context, options = {}) {
|
|
4167
|
+
let best = null;
|
|
4168
|
+
for (const container of zipContainersForManifest(rawManifest)) {
|
|
4169
|
+
const match = bestZipContainerAttachment(container, context);
|
|
4170
|
+
if (!match) continue;
|
|
4171
|
+
if (!best || match.score > best.score) best = match;
|
|
4172
|
+
}
|
|
4173
|
+
if (!best) return null;
|
|
4174
|
+
if (options.probeOnly) {
|
|
4175
|
+
return {
|
|
4176
|
+
kind: "zip-entry",
|
|
4177
|
+
name: best.name,
|
|
4178
|
+
entryPath: best.entryPath,
|
|
4179
|
+
containerPath: best.containerPath
|
|
4180
|
+
};
|
|
4181
|
+
}
|
|
4182
|
+
const extracted = extractZipChainToTemp(best.containerPath, best.chain, best.name);
|
|
4183
|
+
return {
|
|
4184
|
+
kind: "zip-entry",
|
|
4185
|
+
file: extracted.file,
|
|
4186
|
+
cleanup: extracted.cleanup,
|
|
4187
|
+
name: best.name,
|
|
4188
|
+
entryPath: best.entryPath,
|
|
4189
|
+
containerPath: best.containerPath
|
|
4190
|
+
};
|
|
4191
|
+
}
|
|
4192
|
+
|
|
4193
|
+
function zipContainersForManifest(rawManifest) {
|
|
4194
|
+
const result = [];
|
|
4195
|
+
const seen = new Set();
|
|
4196
|
+
const add = (item, entryPath = "") => {
|
|
4197
|
+
const archivedPath = item?.archivedPath || item?.containerPath || "";
|
|
4198
|
+
if (!archivedPath || !isZipPath(archivedPath) || seen.has(archivedPath)) return;
|
|
4199
|
+
seen.add(archivedPath);
|
|
4200
|
+
result.push({ path: archivedPath, entryPath: entryPath || item.entryPath || item.originalPath || path.basename(archivedPath) });
|
|
4201
|
+
};
|
|
4202
|
+
for (const container of Array.isArray(rawManifest?.manifest?.containers) ? rawManifest.manifest.containers : []) add(container);
|
|
4203
|
+
for (const file of Array.isArray(rawManifest?.manifest?.files) ? rawManifest.manifest.files : []) {
|
|
4204
|
+
if (isZipPath(file?.entryPath || "")) add(file);
|
|
4205
|
+
}
|
|
4206
|
+
return result;
|
|
4207
|
+
}
|
|
4208
|
+
|
|
4209
|
+
function bestZipContainerAttachment(container, context) {
|
|
4210
|
+
const entries = cachedZipEntries(container.path);
|
|
4211
|
+
const containerPrefix = isZipPath(container.entryPath) ? container.entryPath.replace(/\.zip$/i, "") : container.entryPath;
|
|
4212
|
+
const directZipIsConversationPart = zipEntryMatchesConversationPart(container.entryPath, context);
|
|
4213
|
+
let best = null;
|
|
4214
|
+
for (const entry of entries) {
|
|
4215
|
+
if (!safeZipMemberName(entry)) continue;
|
|
4216
|
+
if (isZipPath(entry)) {
|
|
4217
|
+
if (!openAiConversationExportPath(entry) && !zipEntryMatchesConversationPart(entry, context)) continue;
|
|
4218
|
+
if (context.conversationPart && !zipEntryMatchesConversationPart(entry, context)) continue;
|
|
4219
|
+
const nestedBest = bestNestedZipAttachment(container.path, entry, context);
|
|
4220
|
+
if (nestedBest && (!best || nestedBest.score > best.score)) best = nestedBest;
|
|
4221
|
+
continue;
|
|
4222
|
+
}
|
|
4223
|
+
if (!directZipIsConversationPart && context.conversationPart && openAiConversationExportPath(container.entryPath)) continue;
|
|
4224
|
+
const score = Math.max(
|
|
4225
|
+
attachmentPathScore(entry, context),
|
|
4226
|
+
attachmentPathScore(containerPrefix ? `${containerPrefix}/${entry}` : entry, context)
|
|
4227
|
+
);
|
|
4228
|
+
if (score < 100) continue;
|
|
4229
|
+
if (!best || score > best.score) {
|
|
4230
|
+
best = {
|
|
4231
|
+
score,
|
|
4232
|
+
containerPath: container.path,
|
|
4233
|
+
chain: [entry],
|
|
4234
|
+
entryPath: containerPrefix ? `${containerPrefix}/${entry}` : entry,
|
|
4235
|
+
name: posixBasename(entry)
|
|
4236
|
+
};
|
|
4237
|
+
}
|
|
4238
|
+
}
|
|
4239
|
+
return best;
|
|
4240
|
+
}
|
|
4241
|
+
|
|
4242
|
+
function bestNestedZipAttachment(containerPath, nestedEntry, context) {
|
|
4243
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "agentlog-zip-list-"));
|
|
4244
|
+
const tempZip = path.join(tempDir, "nested.zip");
|
|
4245
|
+
try {
|
|
4246
|
+
extractZipMemberToFile(containerPath, nestedEntry, tempZip);
|
|
4247
|
+
let best = null;
|
|
4248
|
+
for (const entry of listZipEntries(tempZip)) {
|
|
4249
|
+
if (!safeZipMemberName(entry) || isZipPath(entry)) continue;
|
|
4250
|
+
const nestedPrefix = nestedEntry.replace(/\.zip$/i, "");
|
|
4251
|
+
const score = Math.max(
|
|
4252
|
+
attachmentPathScore(entry, context),
|
|
4253
|
+
attachmentPathScore(`${nestedPrefix}/${entry}`, context)
|
|
4254
|
+
);
|
|
4255
|
+
if (score < 100) continue;
|
|
4256
|
+
if (!best || score > best.score) {
|
|
4257
|
+
best = {
|
|
4258
|
+
score,
|
|
4259
|
+
containerPath,
|
|
4260
|
+
chain: [nestedEntry, entry],
|
|
4261
|
+
entryPath: `${nestedPrefix}/${entry}`,
|
|
4262
|
+
name: posixBasename(entry)
|
|
4263
|
+
};
|
|
4264
|
+
}
|
|
4265
|
+
}
|
|
4266
|
+
return best;
|
|
4267
|
+
} finally {
|
|
4268
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
4269
|
+
}
|
|
4270
|
+
}
|
|
4271
|
+
|
|
4272
|
+
const ZIP_ENTRY_CACHE = new Map();
|
|
4273
|
+
|
|
4274
|
+
function cachedZipEntries(file) {
|
|
4275
|
+
let stat = null;
|
|
4276
|
+
try {
|
|
4277
|
+
stat = fs.statSync(file);
|
|
4278
|
+
} catch {
|
|
4279
|
+
return [];
|
|
4280
|
+
}
|
|
4281
|
+
const key = `${file}:${stat.size}:${Math.trunc(stat.mtimeMs)}`;
|
|
4282
|
+
if (ZIP_ENTRY_CACHE.has(key)) return ZIP_ENTRY_CACHE.get(key);
|
|
4283
|
+
const entries = listZipEntries(file);
|
|
4284
|
+
ZIP_ENTRY_CACHE.set(key, entries);
|
|
4285
|
+
return entries;
|
|
4286
|
+
}
|
|
4287
|
+
|
|
4288
|
+
function listZipEntries(file) {
|
|
4289
|
+
const result = spawnSync("unzip", ["-Z1", file], { encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
|
|
4290
|
+
if (result.status !== 0) return [];
|
|
4291
|
+
return result.stdout.split(/\r?\n/).filter(Boolean);
|
|
4292
|
+
}
|
|
4293
|
+
|
|
4294
|
+
function extractZipChainToTemp(containerPath, chain, name) {
|
|
4295
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "agentlog-attachment-"));
|
|
4296
|
+
let currentZip = containerPath;
|
|
4297
|
+
try {
|
|
4298
|
+
for (let index = 0; index < chain.length; index++) {
|
|
4299
|
+
const entry = chain[index];
|
|
4300
|
+
const last = index === chain.length - 1;
|
|
4301
|
+
const target = path.join(tempDir, last ? `attachment${path.extname(name || entry) || ".bin"}` : `nested-${index}.zip`);
|
|
4302
|
+
extractZipMemberToFile(currentZip, entry, target);
|
|
4303
|
+
currentZip = target;
|
|
4304
|
+
}
|
|
4305
|
+
return {
|
|
4306
|
+
file: currentZip,
|
|
4307
|
+
cleanup: () => fs.rmSync(tempDir, { recursive: true, force: true })
|
|
4308
|
+
};
|
|
4309
|
+
} catch (error) {
|
|
4310
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
4311
|
+
throw error;
|
|
4312
|
+
}
|
|
4313
|
+
}
|
|
4314
|
+
|
|
4315
|
+
function extractZipMemberToFile(zipPath, entryName, target) {
|
|
4316
|
+
if (!safeZipMemberName(entryName)) throw new Error("unsafe zip entry path");
|
|
4317
|
+
ensureDir(path.dirname(target));
|
|
4318
|
+
const fd = fs.openSync(target, "w", 0o600);
|
|
4319
|
+
try {
|
|
4320
|
+
const result = spawnSync("unzip", ["-p", zipPath, entryName], { stdio: ["ignore", fd, "pipe"] });
|
|
4321
|
+
if (result.status !== 0) {
|
|
4322
|
+
const message = result.stderr ? result.stderr.toString("utf8").trim() : "";
|
|
4323
|
+
throw new Error(`failed to extract attachment${message ? `: ${message}` : ""}`);
|
|
4324
|
+
}
|
|
4325
|
+
} finally {
|
|
4326
|
+
fs.closeSync(fd);
|
|
4327
|
+
}
|
|
4328
|
+
}
|
|
4329
|
+
|
|
4330
|
+
function attachmentPathScore(entryPath, context) {
|
|
4331
|
+
const lower = comparableAttachmentPath(entryPath);
|
|
4332
|
+
if (!lower || lower.endsWith("/")) return 0;
|
|
4333
|
+
const base = posixBasename(lower);
|
|
4334
|
+
const baseNoExt = stripExtension(base);
|
|
4335
|
+
let score = 0;
|
|
4336
|
+
if (context.conversationDir && (lower.startsWith(`${context.conversationDir.toLowerCase()}/`) || lower.includes(`/${context.conversationDir.toLowerCase()}/`))) {
|
|
4337
|
+
score += 25;
|
|
4338
|
+
}
|
|
4339
|
+
if (context.conversationPart && lower.includes(context.conversationPart.toLowerCase())) score += 20;
|
|
4340
|
+
for (const name of context.names) {
|
|
4341
|
+
const normalizedName = posixBasename(comparableAttachmentPath(name));
|
|
4342
|
+
const normalizedNameNoExt = stripExtension(normalizedName);
|
|
4343
|
+
if (!normalizedName) continue;
|
|
4344
|
+
if (base === normalizedName || lower.endsWith(`/${normalizedName}`)) score = Math.max(score, 180);
|
|
4345
|
+
else if (baseNoExt === normalizedNameNoExt) score = Math.max(score, 145);
|
|
4346
|
+
else if (normalizedNameNoExt.length >= 6 && baseNoExt.includes(normalizedNameNoExt)) score = Math.max(score, 105);
|
|
4347
|
+
}
|
|
4348
|
+
for (const id of context.ids) {
|
|
4349
|
+
const token = attachmentToken(id);
|
|
4350
|
+
if (!token) continue;
|
|
4351
|
+
if (baseNoExt === token) score = Math.max(score, 175);
|
|
4352
|
+
else if (base.startsWith(`${token}.`) || base.startsWith(`${token}-`) || base.startsWith(`${token}_`)) score = Math.max(score, 165);
|
|
4353
|
+
else if (token.length >= 6 && base.includes(token)) score = Math.max(score, 115);
|
|
4354
|
+
}
|
|
4355
|
+
if (context.isImage && imageExtension(entryPath)) score += 20;
|
|
4356
|
+
if (isArchiveMetadataName(entryPath) && score < 160) return 0;
|
|
4357
|
+
return score;
|
|
4358
|
+
}
|
|
4359
|
+
|
|
4360
|
+
function comparableAttachmentPath(value) {
|
|
4361
|
+
return String(value || "")
|
|
4362
|
+
.replace(/\\/g, "/")
|
|
4363
|
+
.replace(/[\u00a0\u202f]/g, " ")
|
|
4364
|
+
.replace(/\s+/g, " ")
|
|
4365
|
+
.toLowerCase();
|
|
4366
|
+
}
|
|
4367
|
+
|
|
4368
|
+
function zipEntryMatchesConversationPart(entryPath, context) {
|
|
4369
|
+
const part = String(context?.conversationPart || "").toLowerCase();
|
|
4370
|
+
if (!part) return false;
|
|
4371
|
+
const withoutZip = String(entryPath || "").replace(/\\/g, "/").replace(/\.zip$/i, "").toLowerCase();
|
|
4372
|
+
return withoutZip === part || withoutZip.endsWith(`/${part}`);
|
|
4373
|
+
}
|
|
4374
|
+
|
|
4375
|
+
function openAiConversationExportPath(name) {
|
|
4376
|
+
const text = String(name || "");
|
|
4377
|
+
return /(^|\/)Conversations__[^/]*chatgpt[^/]*(?:\/|$)/i.test(text) || /(^|\/)Conversations__[^/]*chatgpt[^/]*\.zip$/i.test(text);
|
|
4378
|
+
}
|
|
4379
|
+
|
|
4380
|
+
function safeZipMemberName(name) {
|
|
4381
|
+
const text = String(name || "");
|
|
4382
|
+
if (!text || text.includes("\0") || path.isAbsolute(text)) return false;
|
|
4383
|
+
return !text.split(/[\\/]+/).some((part) => part === "..");
|
|
4384
|
+
}
|
|
4385
|
+
|
|
4386
|
+
function isZipPath(value) {
|
|
4387
|
+
return /\.zip$/i.test(String(value || ""));
|
|
4388
|
+
}
|
|
4389
|
+
|
|
4390
|
+
function isArchiveMetadataName(value) {
|
|
4391
|
+
return /\.(json|jsonl|ndjson|html?|csv)$/i.test(String(value || ""));
|
|
4392
|
+
}
|
|
4393
|
+
|
|
4394
|
+
function posixBasename(value) {
|
|
4395
|
+
return String(value || "").replace(/\\/g, "/").split("/").filter(Boolean).pop() || "";
|
|
4396
|
+
}
|
|
4397
|
+
|
|
4398
|
+
function posixDirname(value) {
|
|
4399
|
+
const parts = String(value || "").replace(/\\/g, "/").split("/").filter(Boolean);
|
|
4400
|
+
parts.pop();
|
|
4401
|
+
return parts.join("/");
|
|
4402
|
+
}
|
|
4403
|
+
|
|
4404
|
+
function stripExtension(value) {
|
|
4405
|
+
return String(value || "").replace(/\.[^.]+$/, "");
|
|
4406
|
+
}
|
|
4407
|
+
|
|
4408
|
+
function attachmentIsImage(attachment) {
|
|
4409
|
+
return /^image\//i.test(String(attachment?.mimeType || attachment?.pointerMimeType || "")) ||
|
|
4410
|
+
imageExtension(attachment?.name || "");
|
|
4411
|
+
}
|
|
4412
|
+
|
|
4413
|
+
function imageExtension(value) {
|
|
4414
|
+
return /\.(png|jpe?g|gif|webp|bmp|tiff?|heic|heif|avif)$/i.test(String(value || ""));
|
|
4415
|
+
}
|
|
4416
|
+
|
|
4417
|
+
function attachmentContentType(attachment, filename = "") {
|
|
4418
|
+
const explicit = String(attachment?.mimeType || attachment?.pointerMimeType || "").trim();
|
|
4419
|
+
if (/^[A-Za-z0-9!#$&^_.+-]+\/[A-Za-z0-9!#$&^_.+-]+$/.test(explicit)) return explicit;
|
|
4420
|
+
const ext = path.extname(String(filename || attachment?.name || "")).toLowerCase();
|
|
4421
|
+
const types = {
|
|
4422
|
+
".png": "image/png",
|
|
4423
|
+
".jpg": "image/jpeg",
|
|
4424
|
+
".jpeg": "image/jpeg",
|
|
4425
|
+
".gif": "image/gif",
|
|
4426
|
+
".webp": "image/webp",
|
|
4427
|
+
".bmp": "image/bmp",
|
|
4428
|
+
".tif": "image/tiff",
|
|
4429
|
+
".tiff": "image/tiff",
|
|
4430
|
+
".heic": "image/heic",
|
|
4431
|
+
".heif": "image/heif",
|
|
4432
|
+
".avif": "image/avif",
|
|
4433
|
+
".pdf": "application/pdf",
|
|
4434
|
+
".txt": "text/plain",
|
|
4435
|
+
".csv": "text/csv",
|
|
4436
|
+
".json": "application/json"
|
|
4437
|
+
};
|
|
4438
|
+
return types[ext] || "application/octet-stream";
|
|
4439
|
+
}
|
|
4440
|
+
|
|
3481
4441
|
function writeUnauthorizedHistoryResponse(res, pathname) {
|
|
3482
4442
|
if (isHistoryApiPath(pathname)) {
|
|
3483
4443
|
writeJsonResponse(res, { error: "unauthorized", code: "unauthorized" }, 401);
|
|
@@ -4852,19 +5812,29 @@ async function chooseDataRoot(flags, env) {
|
|
|
4852
5812
|
|
|
4853
5813
|
printSection("Archive Storage");
|
|
4854
5814
|
printMuted("Redacted transcripts, metadata, and indexes live in one data directory.");
|
|
4855
|
-
printMuted(
|
|
5815
|
+
printMuted("Enter a path relative to ~/, a ~/... path, or an absolute path.");
|
|
4856
5816
|
printMuted("Press Enter to use the suggested data directory.");
|
|
4857
|
-
const answer = (await ask(` Data directory [${
|
|
5817
|
+
const answer = (await ask(` Data directory [${compactPath(defaultRoot)}]: `)).trim();
|
|
4858
5818
|
return answer ? resolveUserPath(answer) : defaultRoot;
|
|
4859
5819
|
}
|
|
4860
5820
|
|
|
4861
|
-
function resolveUserPath(value) {
|
|
4862
|
-
const input =
|
|
5821
|
+
function resolveUserPath(value, options = {}) {
|
|
5822
|
+
const input = normalizeUserPathInput(value);
|
|
4863
5823
|
if (!input) return "";
|
|
4864
5824
|
if (input === "~") return os.homedir();
|
|
4865
5825
|
if (input.startsWith("~/")) return path.resolve(os.homedir(), input.slice(2));
|
|
4866
5826
|
if (path.isAbsolute(input)) return path.resolve(input);
|
|
4867
|
-
|
|
5827
|
+
const base = options.base ? path.resolve(options.base) : os.homedir();
|
|
5828
|
+
return path.resolve(base, input);
|
|
5829
|
+
}
|
|
5830
|
+
|
|
5831
|
+
function normalizeUserPathInput(value) {
|
|
5832
|
+
let input = String(value || "").trim();
|
|
5833
|
+
while (input.length >= 2 && ((input.startsWith("'") && input.endsWith("'")) || (input.startsWith('"') && input.endsWith('"')))) {
|
|
5834
|
+
input = input.slice(1, -1).trim();
|
|
5835
|
+
}
|
|
5836
|
+
input = input.replace(/^['"]+|['"]+$/g, "");
|
|
5837
|
+
return input.replace(/\\([\\ "'()&;<>|\[\]{}?*])/g, "$1");
|
|
4868
5838
|
}
|
|
4869
5839
|
|
|
4870
5840
|
async function chooseSetupSettings(flags) {
|
|
@@ -5319,10 +6289,10 @@ function createProgressReporter(options = {}) {
|
|
|
5319
6289
|
const file = event.path ? ` ${path.basename(event.path).slice(0, 36)}` : "";
|
|
5320
6290
|
const eventMode = event.kind || mode;
|
|
5321
6291
|
const label = String(event.provider || "work").padEnd(eventMode === "discovery" ? 16 : 12);
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
6292
|
+
const detail = event.message
|
|
6293
|
+
|| (eventMode === "discovery" || eventMode === "sync"
|
|
6294
|
+
? "scanning"
|
|
6295
|
+
: `imported=${event.imported || 0} skipped=${event.skipped || 0} errors=${event.errors || 0}`);
|
|
5326
6296
|
const count = hasTotal ? `${current}/${total}` : `${current}`;
|
|
5327
6297
|
const line = `${label} [${bar}] ${percent}% ${count} ${detail}${file}`;
|
|
5328
6298
|
process.stdout.write(`\r${line.padEnd(lastLine.length)}`);
|
|
@@ -5584,6 +6554,9 @@ details[open] > summary .folder-icon-open{display:block}
|
|
|
5584
6554
|
.session-meta-right .meta-project-block{display:inline-flex;align-items:center;min-width:0;max-width:min(100%,28rem);line-height:1.25}
|
|
5585
6555
|
.session-meta-right .repo-label-github,.session-meta-right .repo-label-webchat,.session-meta-right .repo-label-plain{min-height:0;line-height:1.25}
|
|
5586
6556
|
.session-meta-right .repo-label-github .github-repo-icon{width:13px;height:13px}
|
|
6557
|
+
.parent-session-link{display:inline-flex;align-items:center;gap:4px;border:1px solid #e2e8f0;background:#fff;color:#334155;border-radius:6px;padding:2px 7px;font:12px/1.25 inherit;font-weight:600;white-space:nowrap;cursor:pointer}
|
|
6558
|
+
.parent-session-link:hover{background:#f8fafc;border-color:#cbd5e1}
|
|
6559
|
+
.parent-session-link svg{width:13px;height:13px}
|
|
5587
6560
|
.session-stat.model{max-width:min(400px,46vw);gap:5px}
|
|
5588
6561
|
.session-model-brand{display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto}
|
|
5589
6562
|
.session-stat.model .session-model-text{display:inline-block;max-width:100%;overflow:hidden;text-overflow:ellipsis;vertical-align:bottom}
|
|
@@ -5670,8 +6643,22 @@ mark.search-match.search-match-current{background:#fde047;color:#422006;box-shad
|
|
|
5670
6643
|
.bubble p{margin:0 0 8px;overflow-wrap:anywhere}
|
|
5671
6644
|
.bubble p:last-child{margin-bottom:0}
|
|
5672
6645
|
.plain-message{white-space:pre-wrap;overflow-wrap:anywhere}
|
|
6646
|
+
.attachment-list{display:grid;gap:8px;margin-top:9px}
|
|
6647
|
+
.tool-body + .attachment-list{margin-top:10px}
|
|
6648
|
+
.attachment-card{display:grid;grid-template-columns:auto minmax(0,1fr);align-items:center;gap:9px;max-width:min(420px,100%);padding:7px 9px;border:1px solid #e5e7eb;border-radius:8px;background:#fff;color:#334155;text-decoration:none}
|
|
6649
|
+
.attachment-card:hover{background:#f8fafc;border-color:#cbd5e1}
|
|
6650
|
+
.attachment-card.image{align-items:start}
|
|
6651
|
+
.attachment-thumb{display:inline-flex;align-items:center;justify-content:center;width:34px;height:34px;border-radius:7px;background:#f1f5f9;color:#475569;overflow:hidden;flex:0 0 auto}
|
|
6652
|
+
.attachment-thumb svg{width:17px;height:17px}
|
|
6653
|
+
.attachment-thumb.image{width:96px;height:72px;background:#f8fafc;border:1px solid #eef2f7}
|
|
6654
|
+
.attachment-thumb.image img{width:100%;height:100%;object-fit:cover;display:block}
|
|
6655
|
+
.attachment-copy{display:grid;gap:1px;min-width:0}
|
|
6656
|
+
.attachment-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#172033;font-size:12px;font-weight:600}
|
|
6657
|
+
.attachment-meta{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#64748b;font-size:11px;line-height:1.35}
|
|
5673
6658
|
.bubble a{color:#2457a6;text-decoration:underline;text-underline-offset:2px}
|
|
6659
|
+
.bubble a.attachment-card{color:#334155;text-decoration:none}
|
|
5674
6660
|
.bubble .inline-code{border:1px solid #e5e7eb;border-radius:4px;background:#f8fafc;padding:1px 4px;font:12px/1.35 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
|
|
6661
|
+
.citation-chip{display:inline-flex;align-items:center;height:17px;margin:0 1px;padding:0 5px;border-radius:999px;background:#eef2ff;color:#3730a3;font-size:10.5px;font-weight:650;line-height:1;vertical-align:baseline}
|
|
5675
6662
|
.unsupported-device-block{display:flex;align-items:center;gap:8px;margin:5px 0 7px;padding:7px 9px;border:1px solid #e5e7eb;border-left:3px solid #cbd5e1;border-radius:7px;background:#f8fafc;color:#64748b;font-size:12px;font-weight:500;line-height:1.35}
|
|
5676
6663
|
.unsupported-device-block:first-child{margin-top:0}
|
|
5677
6664
|
.unsupported-device-block:last-child{margin-bottom:0}
|
|
@@ -5687,6 +6674,35 @@ mark.search-match.search-match-current{background:#fde047;color:#422006;box-shad
|
|
|
5687
6674
|
.session-summary-card{border-left-color:#D97757;background:#fff}
|
|
5688
6675
|
.session-summary-body{padding:11px 13px 12px;color:#334155;font-size:13px;line-height:1.55;background:#fff}
|
|
5689
6676
|
.session-summary-body .md-heading:first-child{margin-top:0}
|
|
6677
|
+
.session-subagents-card{border-left-color:#7c3aed;background:#fff}
|
|
6678
|
+
.session-subagent-run-card{border-left-color:#7c3aed;background:#fff}
|
|
6679
|
+
.session-subagents-body{padding:0 12px 12px;background:#fff;color:#334155}
|
|
6680
|
+
.subagent-section-title{margin:10px 0 2px;color:#64748b;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.04em}
|
|
6681
|
+
.subagent-list{display:grid;gap:0}
|
|
6682
|
+
.subagent-row{display:grid;gap:5px;padding:10px 0;border-top:1px solid #f1f5f9}
|
|
6683
|
+
.subagent-row:first-child{border-top:0}
|
|
6684
|
+
.subagent-head{display:flex;align-items:center;gap:6px;min-width:0}
|
|
6685
|
+
.subagent-name{font-size:13px;font-weight:650;color:#172033;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
6686
|
+
.subagent-description{font-size:12.5px;line-height:1.45;color:#475569}
|
|
6687
|
+
.subagent-preview{font:12px/1.45 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;color:#64748b;background:#fafbfc;border-radius:6px;padding:7px 8px;white-space:pre-wrap;overflow-wrap:anywhere}
|
|
6688
|
+
.subagent-tools{display:flex;flex-wrap:wrap;gap:5px}
|
|
6689
|
+
.subagent-chip{display:inline-flex;align-items:center;min-height:17px;max-width:220px;padding:0 6px;border-radius:999px;background:#f1f5f9;color:#475569;font-size:11px;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
6690
|
+
.subagent-open{justify-self:start;margin-top:2px;border:1px solid #e2e8f0;background:#fff;color:#334155;border-radius:6px;padding:3px 7px;font:12px/1.2 inherit;cursor:pointer}
|
|
6691
|
+
.subagent-open:hover{background:#f8fafc;border-color:#cbd5e1}
|
|
6692
|
+
.session-modal{position:fixed;inset:0;z-index:80;display:flex;align-items:center;justify-content:center;padding:28px;background:rgba(15,23,42,.34);backdrop-filter:blur(2px)}
|
|
6693
|
+
.session-modal[hidden]{display:none}
|
|
6694
|
+
.session-modal-shell{display:flex;flex-direction:column;width:min(960px,calc(100vw - 48px));height:min(82vh,860px);border:1px solid #dbe3ee;border-radius:10px;background:#fff;box-shadow:0 24px 80px rgba(15,23,42,.28);overflow:hidden}
|
|
6695
|
+
.session-modal-head{display:flex;align-items:flex-start;justify-content:space-between;gap:14px;padding:13px 15px;border-bottom:1px solid #e5e7eb;background:#fff}
|
|
6696
|
+
.session-modal-title{min-width:0}
|
|
6697
|
+
.session-modal-title strong{display:block;color:#172033;font-size:15px;line-height:1.25;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
6698
|
+
.session-modal-meta{display:flex;flex-wrap:wrap;gap:5px;margin-top:5px;color:#64748b;font-size:12px}
|
|
6699
|
+
.session-modal-close{display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;width:30px;height:30px;border:1px solid #e2e8f0;border-radius:7px;background:#fff;color:#475569;cursor:pointer}
|
|
6700
|
+
.session-modal-close:hover{background:#f8fafc;border-color:#cbd5e1}
|
|
6701
|
+
.session-modal-close-glyph{font-size:16px;font-weight:650;line-height:1}
|
|
6702
|
+
.session-modal-body{flex:1 1 auto;min-height:0;overflow:auto;padding:18px 18px 28px;background:#fff}
|
|
6703
|
+
.session-modal-body .message{max-width:100%}
|
|
6704
|
+
.session-modal-body .bubble{max-width:min(100%,760px)}
|
|
6705
|
+
.session-modal-body.inline-empty{display:flex;align-items:center;justify-content:center;color:#64748b}
|
|
5690
6706
|
.context-card summary{display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:9px;min-height:38px;padding:7px 10px 7px 11px;cursor:pointer;list-style:none;transition:background .12s ease}
|
|
5691
6707
|
.context-prefix{display:inline-flex;align-items:center;gap:6px}
|
|
5692
6708
|
.context-end{display:inline-flex;align-items:center;gap:6px}
|
|
@@ -5959,6 +6975,18 @@ mark.search-match.search-match-current{background:#fde047;color:#422006;box-shad
|
|
|
5959
6975
|
<button id="jumpEnd" class="jump-end" type="button" title="Jump to end" hidden><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 5v14"/><path d="m19 12-7 7-7-7"/></svg></button>
|
|
5960
6976
|
</section>
|
|
5961
6977
|
</main>
|
|
6978
|
+
<div id="sessionModal" class="session-modal" hidden role="dialog" aria-modal="true" aria-labelledby="sessionModalTitle">
|
|
6979
|
+
<div class="session-modal-shell">
|
|
6980
|
+
<div class="session-modal-head">
|
|
6981
|
+
<div class="session-modal-title">
|
|
6982
|
+
<strong id="sessionModalTitle">Subagent transcript</strong>
|
|
6983
|
+
<div id="sessionModalMeta" class="session-modal-meta"></div>
|
|
6984
|
+
</div>
|
|
6985
|
+
<button id="sessionModalClose" class="session-modal-close" type="button" aria-label="Close subagent transcript"><span class="session-modal-close-glyph" aria-hidden="true">X</span></button>
|
|
6986
|
+
</div>
|
|
6987
|
+
<div id="sessionModalBody" class="session-modal-body"></div>
|
|
6988
|
+
</div>
|
|
6989
|
+
</div>
|
|
5962
6990
|
<section id="statsPane" class="stats-pane" aria-label="Stats">
|
|
5963
6991
|
<div class="stats-metrics" id="statsMetrics"></div>
|
|
5964
6992
|
<div class="stats-section">
|
|
@@ -6054,6 +7082,11 @@ const copyDetailsButton = document.getElementById('copyDetailsButton');
|
|
|
6054
7082
|
const copyResumeButton = document.getElementById('copyResumeButton');
|
|
6055
7083
|
const copyDetailsWrap = document.getElementById('copyDetailsWrap');
|
|
6056
7084
|
const copyResumeWrap = document.getElementById('copyResumeWrap');
|
|
7085
|
+
const sessionModal = document.getElementById('sessionModal');
|
|
7086
|
+
const sessionModalTitle = document.getElementById('sessionModalTitle');
|
|
7087
|
+
const sessionModalMeta = document.getElementById('sessionModalMeta');
|
|
7088
|
+
const sessionModalBody = document.getElementById('sessionModalBody');
|
|
7089
|
+
const sessionModalClose = document.getElementById('sessionModalClose');
|
|
6057
7090
|
const form = document.getElementById('filters');
|
|
6058
7091
|
const queryInput = document.getElementById('q');
|
|
6059
7092
|
|
|
@@ -6071,6 +7104,7 @@ const repoOffsets = new Map();
|
|
|
6071
7104
|
let currentSessionId = '';
|
|
6072
7105
|
let currentSessionPayload = null;
|
|
6073
7106
|
let viewMode = 'readable';
|
|
7107
|
+
let viewScrollRestoreSerial = 0;
|
|
6074
7108
|
let sidebarWidth = 390;
|
|
6075
7109
|
let activeSearchTerm = '';
|
|
6076
7110
|
let pendingSessionId = '';
|
|
@@ -6710,6 +7744,7 @@ function formatTokenCount(value) {
|
|
|
6710
7744
|
function usageTokenSummary(usage) {
|
|
6711
7745
|
if (!usage || typeof usage !== 'object') return { inputTokens: 0, outputTokens: 0, totalTokens: 0, extraTokens: 0, estimated: false };
|
|
6712
7746
|
const estimated = usage.estimated === true || usage.estimated === 'true';
|
|
7747
|
+
const authoritativeTotalTokens = usage.authoritativeTotalTokens === true || usage.authoritative_total_tokens === true;
|
|
6713
7748
|
const inputTokens = positiveTokenNumber(firstUsageNumber(
|
|
6714
7749
|
usage.inputTokens,
|
|
6715
7750
|
usage.input_tokens,
|
|
@@ -6749,22 +7784,36 @@ function usageTokenSummary(usage) {
|
|
|
6749
7784
|
firstUsageNumber(usage.cachedContentTokenCount, usage.cached_content_token_count),
|
|
6750
7785
|
firstUsageNumber(usage.cachedTokens, usage.cached_tokens, usage.cacheTokens, usage.cache_tokens, usage.cached)
|
|
6751
7786
|
);
|
|
7787
|
+
const cacheInputTokensIncludedInInput =
|
|
7788
|
+
usage.cacheInputTokensIncludedInInput === true ||
|
|
7789
|
+
usage.cache_input_tokens_included_in_input === true ||
|
|
7790
|
+
usage.cacheReadTokensIncludedInInput === true ||
|
|
7791
|
+
usage.cache_read_tokens_included_in_input === true;
|
|
7792
|
+
const countedCacheInputTokens = cacheInputTokensIncludedInInput ? 0 : cacheInputTokens;
|
|
6752
7793
|
const reasoningOutputTokens = sumPositiveTokenNumbers(
|
|
6753
7794
|
firstUsageNumber(usage.reasoningOutputTokens, usage.reasoning_output_tokens),
|
|
6754
7795
|
firstUsageNumber(usage.thoughtsTokens, usage.thoughts_tokens, usage.thoughtsTokenCount, usage.thoughts_token_count),
|
|
6755
7796
|
firstUsageNumber(usage.reasoningTokens, usage.reasoning_tokens, usage.reasoningTokenCount, usage.reasoning_token_count)
|
|
6756
7797
|
);
|
|
7798
|
+
const reasoningOutputTokensIncludedInOutput =
|
|
7799
|
+
usage.reasoningOutputTokensIncludedInOutput === true ||
|
|
7800
|
+
usage.reasoning_output_tokens_included_in_output === true ||
|
|
7801
|
+
usage.reasoningTokensIncludedInOutput === true ||
|
|
7802
|
+
usage.reasoning_tokens_included_in_output === true;
|
|
7803
|
+
const countedReasoningOutputTokens = reasoningOutputTokensIncludedInOutput ? 0 : reasoningOutputTokens;
|
|
6757
7804
|
const toolUsePromptTokens = sumPositiveTokenNumbers(
|
|
6758
7805
|
firstUsageNumber(usage.toolUsePromptTokens, usage.tool_use_prompt_tokens, usage.toolUsePromptTokenCount, usage.tool_use_prompt_token_count)
|
|
6759
7806
|
);
|
|
6760
|
-
const extraTokens =
|
|
7807
|
+
const extraTokens = countedCacheInputTokens + countedReasoningOutputTokens + toolUsePromptTokens;
|
|
6761
7808
|
const splitTokens = inputTokens + outputTokens;
|
|
6762
7809
|
const cumulativeTokens = totalInputTokens + totalOutputTokens;
|
|
6763
7810
|
const categoryTokens = splitTokens + extraTokens;
|
|
6764
|
-
const totalTokens =
|
|
6765
|
-
?
|
|
6766
|
-
:
|
|
6767
|
-
|
|
7811
|
+
const totalTokens = authoritativeTotalTokens && explicitTotalTokens
|
|
7812
|
+
? explicitTotalTokens
|
|
7813
|
+
: explicitTotalTokens || categoryTokens
|
|
7814
|
+
? Math.max(explicitTotalTokens, categoryTokens)
|
|
7815
|
+
: cumulativeTokens;
|
|
7816
|
+
return { inputTokens, outputTokens, cacheInputTokens, countedCacheInputTokens, cacheInputTokensIncludedInInput, reasoningOutputTokens, countedReasoningOutputTokens, reasoningOutputTokensIncludedInOutput, toolUsePromptTokens, totalTokens, extraTokens, authoritativeTotalTokens, estimated };
|
|
6768
7817
|
}
|
|
6769
7818
|
|
|
6770
7819
|
function firstUsageNumber() {
|
|
@@ -6964,11 +8013,15 @@ function sessionDisplayTitle(payload) {
|
|
|
6964
8013
|
|
|
6965
8014
|
function renderTree(payload, options) {
|
|
6966
8015
|
const opts = options || {};
|
|
8016
|
+
const sourceFilterReset = updateSourceFilterOptions(payload.available_source_options || []);
|
|
6967
8017
|
treeTitle.textContent = 'Sessions';
|
|
6968
8018
|
treeCount.textContent = String(payload.count || 0);
|
|
6969
8019
|
tree.innerHTML = '';
|
|
6970
8020
|
if (sidebarHead) sidebarHead.classList.remove('tree-scrolled');
|
|
6971
8021
|
repoOffsets.clear();
|
|
8022
|
+
if (sourceFilterReset && !opts.skipSourceReload) {
|
|
8023
|
+
window.setTimeout(() => loadTree({ skipSourceReload: true }).catch((error) => { setEmptySession(error.message); }), 0);
|
|
8024
|
+
}
|
|
6972
8025
|
if (!payload.groups || !payload.groups.length) {
|
|
6973
8026
|
tree.innerHTML = '<div class="inline-empty">No archived sessions matched these filters.</div>';
|
|
6974
8027
|
if (!opts.skipAutoLoad) setEmptySession('No archived sessions matched these filters.');
|
|
@@ -7268,6 +8321,14 @@ function renderSession(payload) {
|
|
|
7268
8321
|
if (sessionMetaRight) {
|
|
7269
8322
|
const innerParts = [];
|
|
7270
8323
|
if (payload.source_type) innerParts.push('<span class="meta-source">' + esc(sourceTypeLabel(payload.provider, payload.source_type)) + '</span>');
|
|
8324
|
+
if (payload.parent_composer_id) {
|
|
8325
|
+
if (innerParts.length) innerParts.push('<span class="meta-dot" aria-hidden="true">\u00b7</span>');
|
|
8326
|
+
innerParts.push(
|
|
8327
|
+
'<button class="parent-session-link" type="button" data-parent-session="' + esc(payload.parent_composer_id) + '" title="Open parent session">' +
|
|
8328
|
+
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m12 19-7-7 7-7"/><path d="M19 12H5"/></svg>' +
|
|
8329
|
+
'<span>Parent session</span></button>'
|
|
8330
|
+
);
|
|
8331
|
+
}
|
|
7271
8332
|
const repoRaw = payload.repo_display || payload.repo || payload.scope || '';
|
|
7272
8333
|
if (repoRaw) {
|
|
7273
8334
|
if (innerParts.length) innerParts.push('<span class="meta-dot" aria-hidden="true">\u00b7</span>');
|
|
@@ -7283,7 +8344,16 @@ function renderSession(payload) {
|
|
|
7283
8344
|
sessionMetaRight.innerHTML = innerParts.length
|
|
7284
8345
|
? '<div class="session-meta-right-inner">' + innerParts.join('') + '</div>'
|
|
7285
8346
|
: '';
|
|
8347
|
+
const parentLink = sessionMetaRight.querySelector('[data-parent-session]');
|
|
8348
|
+
if (parentLink) {
|
|
8349
|
+
parentLink.addEventListener('click', (event) => {
|
|
8350
|
+
event.preventDefault();
|
|
8351
|
+
event.stopPropagation();
|
|
8352
|
+
loadSession(parentLink.getAttribute('data-parent-session') || '', { preserveCurrent: true });
|
|
8353
|
+
});
|
|
8354
|
+
}
|
|
7286
8355
|
const titleBits = [];
|
|
8356
|
+
if (payload.parent_composer_id) titleBits.push('Parent session: ' + payload.parent_composer_id);
|
|
7287
8357
|
if (payload.time_status === 'recovered-time-unknown') titleBits.push('Recovered time unknown');
|
|
7288
8358
|
else if (payload.started_at) titleBits.push('Started ' + formatWhen(payload.started_at));
|
|
7289
8359
|
if (payload.session_id) titleBits.push('Session ID: ' + payload.session_id);
|
|
@@ -7312,7 +8382,7 @@ function renderSession(payload) {
|
|
|
7312
8382
|
);
|
|
7313
8383
|
copyResumeButton.classList.remove('copied');
|
|
7314
8384
|
jumpEnd.hidden = false;
|
|
7315
|
-
setView(viewMode);
|
|
8385
|
+
setView(viewMode, { preserveScroll: false });
|
|
7316
8386
|
for (const button of tree.querySelectorAll('.session-node')) {
|
|
7317
8387
|
button.classList.toggle('active', button.dataset.id === payload.session_id);
|
|
7318
8388
|
}
|
|
@@ -7330,17 +8400,20 @@ function renderMessages(messages, sessionSummary) {
|
|
|
7330
8400
|
readableView.className = '';
|
|
7331
8401
|
readableView.innerHTML = '';
|
|
7332
8402
|
const summaryText = sessionSummaryText(sessionSummary);
|
|
8403
|
+
const subagentsElement = sessionSubagentsElement(sessionSummary);
|
|
8404
|
+
const subagentRunItems = subagentRunTimelineItems(sessionSummary);
|
|
7333
8405
|
const visibleMessages = summaryText
|
|
7334
8406
|
? (messages || []).filter((message) => message?.metadata?.summaryKind !== 'conversation_summary')
|
|
7335
8407
|
: (messages || []);
|
|
7336
|
-
const renderItems = pairedToolRenderItems(visibleMessages);
|
|
7337
|
-
if (!renderItems.length && !summaryText) {
|
|
8408
|
+
const renderItems = mergeTimelineRenderItems(pairedToolRenderItems(visibleMessages), subagentRunItems);
|
|
8409
|
+
if (!renderItems.length && !summaryText && !subagentsElement) {
|
|
7338
8410
|
readableView.className = 'inline-empty';
|
|
7339
8411
|
readableView.textContent = 'No transcript messages are available for this session.';
|
|
7340
8412
|
return;
|
|
7341
8413
|
}
|
|
7342
8414
|
const summaryElement = sessionSummaryElement(sessionSummary);
|
|
7343
8415
|
if (summaryElement) readableView.appendChild(summaryElement);
|
|
8416
|
+
if (subagentsElement) readableView.appendChild(subagentsElement);
|
|
7344
8417
|
const scheduleNext = typeof window.requestAnimationFrame === 'function'
|
|
7345
8418
|
? (fn) => window.requestAnimationFrame(fn)
|
|
7346
8419
|
: (fn) => window.setTimeout(fn, 0);
|
|
@@ -7374,6 +8447,25 @@ function renderMessages(messages, sessionSummary) {
|
|
|
7374
8447
|
renderChunk();
|
|
7375
8448
|
}
|
|
7376
8449
|
|
|
8450
|
+
function mergeTimelineRenderItems(items, extras) {
|
|
8451
|
+
if (!Array.isArray(extras) || !extras.length) return items || [];
|
|
8452
|
+
return [
|
|
8453
|
+
...(items || []).map((item, index) => ({ ...item, _timelineOrder: index * 2 })),
|
|
8454
|
+
...extras.map((item, index) => ({ ...item, _timelineOrder: index * 2 + 1 }))
|
|
8455
|
+
]
|
|
8456
|
+
.sort((left, right) => renderItemTimestampMs(left) - renderItemTimestampMs(right) || left._timelineOrder - right._timelineOrder)
|
|
8457
|
+
.map((item) => {
|
|
8458
|
+
const { _timelineOrder, ...rest } = item;
|
|
8459
|
+
return rest;
|
|
8460
|
+
});
|
|
8461
|
+
}
|
|
8462
|
+
|
|
8463
|
+
function renderItemTimestampMs(item) {
|
|
8464
|
+
const value = item?.message?.timestamp || item?.lastTimestamp || '';
|
|
8465
|
+
const time = value ? new Date(value).getTime() : NaN;
|
|
8466
|
+
return Number.isFinite(time) ? time : Number.MAX_SAFE_INTEGER;
|
|
8467
|
+
}
|
|
8468
|
+
|
|
7377
8469
|
function pairedToolRenderItems(messages) {
|
|
7378
8470
|
const pairings = pairedToolResultIndexes(messages);
|
|
7379
8471
|
const skippedResultIndexes = new Set(pairings.duplicateResultIndexes);
|
|
@@ -7600,6 +8692,246 @@ function summarySourceLabel(source) {
|
|
|
7600
8692
|
return 'summary';
|
|
7601
8693
|
}
|
|
7602
8694
|
|
|
8695
|
+
function sessionSubagents(sessionSummary) {
|
|
8696
|
+
const definitionValue = sessionSummary && sessionSummary.claudeSubagents;
|
|
8697
|
+
const definitions = definitionValue && typeof definitionValue === 'object' && Array.isArray(definitionValue.definitions)
|
|
8698
|
+
? definitionValue.definitions.filter((item) => item && item.name)
|
|
8699
|
+
: [];
|
|
8700
|
+
if (!definitions.length) return null;
|
|
8701
|
+
return { ...(definitionValue || {}), definitions };
|
|
8702
|
+
}
|
|
8703
|
+
|
|
8704
|
+
function sessionSubagentRuns(sessionSummary) {
|
|
8705
|
+
const sources = [
|
|
8706
|
+
{ key: 'claudeSubagentRuns', provider: 'claude_code', providerLabel: 'Claude' },
|
|
8707
|
+
{ key: 'codexSubagentRuns', provider: 'codex', providerLabel: 'Codex' }
|
|
8708
|
+
];
|
|
8709
|
+
const runs = [];
|
|
8710
|
+
for (const source of sources) {
|
|
8711
|
+
const runValue = sessionSummary && sessionSummary[source.key];
|
|
8712
|
+
const sourceRuns = runValue && typeof runValue === 'object' && Array.isArray(runValue.runs)
|
|
8713
|
+
? runValue.runs.filter((item) => item && (item.agentId || item.agentNickname || item.title || item.sessionId))
|
|
8714
|
+
: [];
|
|
8715
|
+
for (const run of sourceRuns) {
|
|
8716
|
+
runs.push({ provider: source.provider, providerLabel: source.providerLabel, ...run });
|
|
8717
|
+
}
|
|
8718
|
+
}
|
|
8719
|
+
if (!runs.length) return null;
|
|
8720
|
+
return { runs };
|
|
8721
|
+
}
|
|
8722
|
+
|
|
8723
|
+
function subagentRunTimelineItems(sessionSummary) {
|
|
8724
|
+
const runSummary = sessionSubagentRuns(sessionSummary);
|
|
8725
|
+
if (!runSummary) return [];
|
|
8726
|
+
return runSummary.runs.map((run, index) => {
|
|
8727
|
+
const timestamp = run.startedAt || run.endedAt || '';
|
|
8728
|
+
return {
|
|
8729
|
+
type: 'subagent-run',
|
|
8730
|
+
run,
|
|
8731
|
+
message: {
|
|
8732
|
+
role: 'system',
|
|
8733
|
+
content: formatSubagentRunPlainText(run),
|
|
8734
|
+
timestamp,
|
|
8735
|
+
metadata: { provider: run.provider || 'unknown', generatedContext: true, contextKind: 'subagent_run' }
|
|
8736
|
+
},
|
|
8737
|
+
lastTimestamp: run.endedAt || timestamp,
|
|
8738
|
+
_subagentIndex: index
|
|
8739
|
+
};
|
|
8740
|
+
});
|
|
8741
|
+
}
|
|
8742
|
+
|
|
8743
|
+
function sessionSubagentsElement(sessionSummary) {
|
|
8744
|
+
const subagents = sessionSubagents(sessionSummary);
|
|
8745
|
+
if (!subagents) return null;
|
|
8746
|
+
const definitionCount = Number(subagents.count || subagents.definitions.length) || subagents.definitions.length;
|
|
8747
|
+
const chips = [];
|
|
8748
|
+
if (definitionCount) chips.push(definitionCount + ' definition' + (definitionCount === 1 ? '' : 's'));
|
|
8749
|
+
if (subagents.projectCount) chips.push(subagents.projectCount + ' project');
|
|
8750
|
+
if (subagents.userCount) chips.push(subagents.userCount + ' user');
|
|
8751
|
+
if (Array.isArray(subagents.shadowedNames) && subagents.shadowedNames.length) chips.push(subagents.shadowedNames.length + ' shadowed');
|
|
8752
|
+
const row = document.createElement('article');
|
|
8753
|
+
row.className = 'message context session-subagents-message';
|
|
8754
|
+
const bubble = document.createElement('div');
|
|
8755
|
+
bubble.className = 'bubble';
|
|
8756
|
+
const details = document.createElement('details');
|
|
8757
|
+
details.className = 'context-card session-subagents-card';
|
|
8758
|
+
details.open = true;
|
|
8759
|
+
details.innerHTML =
|
|
8760
|
+
'<summary>' +
|
|
8761
|
+
'<span class="context-prefix"><span class="context-caret"></span><span class="context-glyph">' + contextIconSvg('subagents') + '</span></span>' +
|
|
8762
|
+
'<span class="context-copy"><span class="context-title">Claude subagents</span>' +
|
|
8763
|
+
'<span class="context-meta">' + chips.slice(0, 4).map((chip) => '<span class="context-chip">' + esc(chip) + '</span>').join('') + '</span></span>' +
|
|
8764
|
+
'<span class="context-end"></span>' +
|
|
8765
|
+
'</summary>';
|
|
8766
|
+
const end = details.querySelector('.context-end');
|
|
8767
|
+
if (end) end.appendChild(messageCopyButton(formatSubagentsPlainText(subagents)));
|
|
8768
|
+
const body = document.createElement('div');
|
|
8769
|
+
body.className = 'session-subagents-body';
|
|
8770
|
+
body.innerHTML =
|
|
8771
|
+
'<div class="subagent-list">' +
|
|
8772
|
+
subagents.definitions.map(renderSubagentDefinition).join('') +
|
|
8773
|
+
'</div>';
|
|
8774
|
+
details.appendChild(body);
|
|
8775
|
+
bubble.appendChild(details);
|
|
8776
|
+
row.appendChild(bubble);
|
|
8777
|
+
return row;
|
|
8778
|
+
}
|
|
8779
|
+
|
|
8780
|
+
function renderSubagentDefinition(definition) {
|
|
8781
|
+
const chips = [];
|
|
8782
|
+
if (definition.scope) chips.push(definition.scope);
|
|
8783
|
+
if (definition.model) chips.push(definition.model);
|
|
8784
|
+
if (definition.relativePath) chips.push(definition.relativePath);
|
|
8785
|
+
const tools = Array.isArray(definition.tools) ? definition.tools : [];
|
|
8786
|
+
return '<div class="subagent-row">' +
|
|
8787
|
+
'<div class="subagent-head"><span class="subagent-name" title="' + esc(definition.name || '') + '">' + esc(definition.name || 'subagent') + '</span>' +
|
|
8788
|
+
chips.map((chip) => '<span class="subagent-chip">' + esc(chip) + '</span>').join('') +
|
|
8789
|
+
'</div>' +
|
|
8790
|
+
(definition.description ? '<div class="subagent-description">' + esc(definition.description) + '</div>' : '') +
|
|
8791
|
+
(tools.length ? '<div class="subagent-tools">' + tools.map((tool) => '<span class="subagent-chip">' + esc(tool) + '</span>').join('') + '</div>' : '') +
|
|
8792
|
+
(definition.instructionPreview ? '<pre class="subagent-preview">' + esc(definition.instructionPreview) + '</pre>' : '') +
|
|
8793
|
+
'</div>';
|
|
8794
|
+
}
|
|
8795
|
+
|
|
8796
|
+
function subagentRunElement(run) {
|
|
8797
|
+
const chips = [];
|
|
8798
|
+
if (run.agentNickname) chips.push(run.agentNickname);
|
|
8799
|
+
if (run.agentRole || run.agentType) chips.push(run.agentRole || run.agentType);
|
|
8800
|
+
if (run.status) chips.push(run.status);
|
|
8801
|
+
if (run.agentId) chips.push(run.agentId);
|
|
8802
|
+
if (Array.isArray(run.models) && run.models.length) chips.push(run.models.join(', '));
|
|
8803
|
+
if (run.messageCount) chips.push(formatFullNumber(run.messageCount) + ' messages');
|
|
8804
|
+
if (run.toolCallCount) chips.push(formatFullNumber(run.toolCallCount) + ' tools');
|
|
8805
|
+
const title = run.title || run.agentNickname || run.agentType || run.agentRole || run.agentId || 'subagent run';
|
|
8806
|
+
const preview = [run.promptPreview ? 'Prompt: ' + run.promptPreview : '', run.resultPreview ? 'Result: ' + run.resultPreview : ''].filter(Boolean).join('\\n\\n');
|
|
8807
|
+
const row = document.createElement('article');
|
|
8808
|
+
row.className = 'message context session-subagent-run-message';
|
|
8809
|
+
const bubble = document.createElement('div');
|
|
8810
|
+
bubble.className = 'bubble';
|
|
8811
|
+
const details = document.createElement('details');
|
|
8812
|
+
details.className = 'context-card session-subagent-run-card';
|
|
8813
|
+
details.innerHTML =
|
|
8814
|
+
'<summary>' +
|
|
8815
|
+
'<span class="context-prefix"><span class="context-caret"></span><span class="context-glyph">' + contextIconSvg('subagents') + '</span></span>' +
|
|
8816
|
+
'<span class="context-copy"><span class="context-title">Subagent: ' + esc(title) + '</span>' +
|
|
8817
|
+
'<span class="context-meta">' + chips.slice(0, 4).map((chip) => '<span class="context-chip">' + esc(chip) + '</span>').join('') + '</span></span>' +
|
|
8818
|
+
'<span class="context-end"><span class="context-time"' + timeAttr(run.startedAt || run.endedAt) + '>' + esc(relativeTime(run.startedAt || run.endedAt)) + '</span></span>' +
|
|
8819
|
+
'</summary>';
|
|
8820
|
+
const end = details.querySelector('.context-end');
|
|
8821
|
+
if (end) end.appendChild(messageCopyButton(formatSubagentRunPlainText(run)));
|
|
8822
|
+
const body = document.createElement('div');
|
|
8823
|
+
body.className = 'session-subagents-body';
|
|
8824
|
+
body.innerHTML =
|
|
8825
|
+
'<div class="subagent-list"><div class="subagent-row">' +
|
|
8826
|
+
(run.relativePath ? '<div class="subagent-description">' + esc(run.relativePath) + '</div>' : '') +
|
|
8827
|
+
(preview ? '<pre class="subagent-preview">' + esc(preview) + '</pre>' : '') +
|
|
8828
|
+
(run.sessionId ? '<button class="subagent-open" type="button" data-subagent-session="' + esc(run.sessionId) + '">Open transcript</button>' : '') +
|
|
8829
|
+
'</div></div>';
|
|
8830
|
+
const open = body.querySelector('[data-subagent-session]');
|
|
8831
|
+
if (open) {
|
|
8832
|
+
open.addEventListener('click', (event) => {
|
|
8833
|
+
event.preventDefault();
|
|
8834
|
+
event.stopPropagation();
|
|
8835
|
+
openSubagentModal(open.getAttribute('data-subagent-session') || '');
|
|
8836
|
+
});
|
|
8837
|
+
}
|
|
8838
|
+
details.appendChild(body);
|
|
8839
|
+
bubble.appendChild(details);
|
|
8840
|
+
row.appendChild(bubble);
|
|
8841
|
+
return row;
|
|
8842
|
+
}
|
|
8843
|
+
|
|
8844
|
+
function formatSubagentsPlainText(subagents) {
|
|
8845
|
+
const lines = ['Claude subagent definitions'];
|
|
8846
|
+
for (const definition of subagents.definitions || []) {
|
|
8847
|
+
lines.push('');
|
|
8848
|
+
lines.push(definition.name || 'subagent');
|
|
8849
|
+
const meta = [definition.scope, definition.model, definition.relativePath].filter(Boolean).join(' | ');
|
|
8850
|
+
if (meta) lines.push(meta);
|
|
8851
|
+
if (definition.description) lines.push(definition.description);
|
|
8852
|
+
if (Array.isArray(definition.tools) && definition.tools.length) lines.push('Tools: ' + definition.tools.join(', '));
|
|
8853
|
+
if (definition.instructionPreview) lines.push(definition.instructionPreview);
|
|
8854
|
+
}
|
|
8855
|
+
return lines.join('\\n');
|
|
8856
|
+
}
|
|
8857
|
+
|
|
8858
|
+
function formatSubagentRunPlainText(run) {
|
|
8859
|
+
const lines = [run.title || run.agentNickname || run.agentType || run.agentRole || run.agentId || 'Subagent run'];
|
|
8860
|
+
const meta = [run.providerLabel, run.agentNickname, run.agentRole || run.agentType, run.status, run.agentId, run.agentPath, run.relativePath].filter(Boolean).join(' | ');
|
|
8861
|
+
if (meta) lines.push(meta);
|
|
8862
|
+
if (run.startedAt || run.endedAt) lines.push([run.startedAt, run.endedAt].filter(Boolean).join(' - '));
|
|
8863
|
+
if (run.messageCount) lines.push(formatFullNumber(run.messageCount) + ' messages');
|
|
8864
|
+
if (run.toolCallCount) lines.push(formatFullNumber(run.toolCallCount) + ' tool calls');
|
|
8865
|
+
if (run.sessionId) lines.push('Child session: ' + run.sessionId);
|
|
8866
|
+
if (run.promptPreview) lines.push('Prompt: ' + run.promptPreview);
|
|
8867
|
+
if (run.resultPreview) lines.push('Result: ' + run.resultPreview);
|
|
8868
|
+
return lines.join('\\n');
|
|
8869
|
+
}
|
|
8870
|
+
|
|
8871
|
+
async function openSubagentModal(sessionId) {
|
|
8872
|
+
if (!sessionId || !sessionModal || !sessionModalBody) return;
|
|
8873
|
+
sessionModal.hidden = false;
|
|
8874
|
+
if (sessionModalTitle) sessionModalTitle.textContent = 'Subagent transcript';
|
|
8875
|
+
if (sessionModalMeta) sessionModalMeta.innerHTML = '<span class="context-chip">' + esc(sessionId) + '</span>';
|
|
8876
|
+
sessionModalBody.className = 'session-modal-body inline-empty';
|
|
8877
|
+
sessionModalBody.textContent = 'Loading subagent transcript...';
|
|
8878
|
+
try {
|
|
8879
|
+
const { payload } = await fetchSessionPayload(sessionId);
|
|
8880
|
+
if (!payload || payload.session_id !== sessionId || sessionModal.hidden) return;
|
|
8881
|
+
renderSubagentModalPayload(payload);
|
|
8882
|
+
} catch (error) {
|
|
8883
|
+
if (sessionModalBody) {
|
|
8884
|
+
sessionModalBody.className = 'session-modal-body inline-empty';
|
|
8885
|
+
sessionModalBody.textContent = error.message || 'Unable to load subagent transcript.';
|
|
8886
|
+
}
|
|
8887
|
+
}
|
|
8888
|
+
}
|
|
8889
|
+
|
|
8890
|
+
function renderSubagentModalPayload(payload) {
|
|
8891
|
+
if (!sessionModalBody) return;
|
|
8892
|
+
if (sessionModalTitle) sessionModalTitle.textContent = sessionDisplayTitle(payload) || 'Subagent transcript';
|
|
8893
|
+
if (sessionModalMeta) {
|
|
8894
|
+
const parts = [];
|
|
8895
|
+
if (payload.source_type) parts.push(sourceTypeLabel(payload.provider, payload.source_type));
|
|
8896
|
+
if (payload.started_at) parts.push(relativeTime(payload.started_at));
|
|
8897
|
+
if (payload.messages != null) parts.push(formatFullNumber(payload.messages) + ' messages');
|
|
8898
|
+
if (payload.parent_composer_id) parts.push('parent ' + payload.parent_composer_id);
|
|
8899
|
+
sessionModalMeta.innerHTML = parts.map((part) => '<span class="context-chip">' + esc(part) + '</span>').join('');
|
|
8900
|
+
}
|
|
8901
|
+
sessionModalBody.className = 'session-modal-body';
|
|
8902
|
+
sessionModalBody.innerHTML = '';
|
|
8903
|
+
const messages = Array.isArray(payload.transcript_messages) ? payload.transcript_messages : [];
|
|
8904
|
+
if (!messages.length) {
|
|
8905
|
+
sessionModalBody.className = 'session-modal-body inline-empty';
|
|
8906
|
+
sessionModalBody.textContent = 'No transcript messages are available for this subagent.';
|
|
8907
|
+
return;
|
|
8908
|
+
}
|
|
8909
|
+
const previousPayload = currentSessionPayload;
|
|
8910
|
+
currentSessionPayload = payload;
|
|
8911
|
+
try {
|
|
8912
|
+
let previousTimestamp = '';
|
|
8913
|
+
for (const item of pairedToolRenderItems(messages)) {
|
|
8914
|
+
const message = item.message || {};
|
|
8915
|
+
const gap = renderTimeGap(previousTimestamp, message.timestamp);
|
|
8916
|
+
if (gap) sessionModalBody.appendChild(gap);
|
|
8917
|
+
sessionModalBody.appendChild(messageElement(message, item));
|
|
8918
|
+
previousTimestamp = item.lastTimestamp || message.timestamp || previousTimestamp;
|
|
8919
|
+
}
|
|
8920
|
+
collapseLongToolChains(sessionModalBody, 10);
|
|
8921
|
+
} finally {
|
|
8922
|
+
currentSessionPayload = previousPayload;
|
|
8923
|
+
}
|
|
8924
|
+
}
|
|
8925
|
+
|
|
8926
|
+
function closeSubagentModal() {
|
|
8927
|
+
if (!sessionModal) return;
|
|
8928
|
+
sessionModal.hidden = true;
|
|
8929
|
+
if (sessionModalBody) {
|
|
8930
|
+
sessionModalBody.className = 'session-modal-body';
|
|
8931
|
+
sessionModalBody.innerHTML = '';
|
|
8932
|
+
}
|
|
8933
|
+
}
|
|
8934
|
+
|
|
7603
8935
|
function renderTimeGap(previous, current) {
|
|
7604
8936
|
const diffSec = timeGapSeconds(previous, current);
|
|
7605
8937
|
if (diffSec < 5) return null;
|
|
@@ -7693,18 +9025,20 @@ function highlightSearchMatches(root, term) {
|
|
|
7693
9025
|
|
|
7694
9026
|
function messageElement(message, options = {}) {
|
|
7695
9027
|
if (options.type === 'tool-group') return toolGroupElement(options);
|
|
9028
|
+
if (options.type === 'subagent-run') return subagentRunElement(options.run || {});
|
|
7696
9029
|
const context = generatedContextForMessage(message);
|
|
7697
9030
|
if (context) return contextMessageElement(message, context);
|
|
7698
9031
|
const role = String(message.role || 'unknown').toLowerCase();
|
|
7699
9032
|
const content = String(message.content || '');
|
|
7700
9033
|
const toolCalls = messageToolCalls(message, options.pairedToolResults || []);
|
|
7701
9034
|
const toolResult = role === 'tool' ? parseToolResult(content, message) : null;
|
|
7702
|
-
const contentWithoutTools = toolCalls.length ? stripToolInvocationLines(content) : content;
|
|
9035
|
+
const contentWithoutTools = isViewerStructuredToolCallMessage(message) ? '' : toolCalls.length ? stripToolInvocationLines(content) : content;
|
|
7703
9036
|
const toolOnly = toolCalls.length && !contentWithoutTools.trim();
|
|
7704
9037
|
const className = toolOnly ? 'tool' : ['user', 'assistant', 'system', 'tool'].includes(role) ? role : 'assistant';
|
|
7705
9038
|
const bodyContent = toolResult
|
|
7706
9039
|
? renderToolResult(toolResult)
|
|
7707
9040
|
: renderMessageBodyWithTools(className, contentWithoutTools, toolCalls);
|
|
9041
|
+
const attachmentsHtml = renderMessageAttachments(message);
|
|
7708
9042
|
const row = document.createElement('article');
|
|
7709
9043
|
row.className = 'message ' + className;
|
|
7710
9044
|
if (toolCalls.length) row.classList.add('tool-call-turn');
|
|
@@ -7729,7 +9063,7 @@ function messageElement(message, options = {}) {
|
|
|
7729
9063
|
'<div class="message-head"><span class="role">' + esc(roleLabel(role)) + '</span>' +
|
|
7730
9064
|
'<span class="message-time"' + timeAttr(message.timestamp) + '>' + esc(relativeTime(message.timestamp)) + '</span>' +
|
|
7731
9065
|
usageChipHtml + '</div>';
|
|
7732
|
-
bubble.innerHTML = headHtml + bodyContent;
|
|
9066
|
+
bubble.innerHTML = headHtml + bodyContent + attachmentsHtml;
|
|
7733
9067
|
const head = bubble.querySelector('.message-head');
|
|
7734
9068
|
if (head) head.appendChild(messageCopyButton(content));
|
|
7735
9069
|
row.appendChild(bubble);
|
|
@@ -7864,6 +9198,7 @@ function contextTitle(kind) {
|
|
|
7864
9198
|
local_command_caveat: 'Local command',
|
|
7865
9199
|
local_command_stdout: 'Local command',
|
|
7866
9200
|
system_reminder: 'System reminder',
|
|
9201
|
+
subagents: 'Claude subagents',
|
|
7867
9202
|
compaction: 'Compaction',
|
|
7868
9203
|
metadata: 'Context metadata'
|
|
7869
9204
|
};
|
|
@@ -7917,6 +9252,86 @@ function xmlTagText(text, tag) {
|
|
|
7917
9252
|
return source.slice(bodyStart, end).trim();
|
|
7918
9253
|
}
|
|
7919
9254
|
|
|
9255
|
+
function renderMessageAttachments(message) {
|
|
9256
|
+
const attachments = messageAttachments(message);
|
|
9257
|
+
if (!attachments.length) return '';
|
|
9258
|
+
return '<div class="attachment-list">' + attachments.map(renderAttachmentCard).join('') + '</div>';
|
|
9259
|
+
}
|
|
9260
|
+
|
|
9261
|
+
function messageAttachments(message) {
|
|
9262
|
+
const metadata = message && message.metadata && typeof message.metadata === 'object' ? message.metadata : {};
|
|
9263
|
+
const attachments = Array.isArray(metadata.attachments) ? metadata.attachments : [];
|
|
9264
|
+
const attachedPointers = new Set(attachments.map((attachment) => attachment && attachment.assetPointer).filter(Boolean));
|
|
9265
|
+
const result = attachments.map((attachment, index) => ({ ...attachment, index, kind: 'attachment' }));
|
|
9266
|
+
const pointers = Array.isArray(metadata.assetPointers) ? metadata.assetPointers : [];
|
|
9267
|
+
for (const pointer of pointers) {
|
|
9268
|
+
if (!pointer || !pointer.assetPointer || attachedPointers.has(pointer.assetPointer)) continue;
|
|
9269
|
+
result.push({
|
|
9270
|
+
index: result.length,
|
|
9271
|
+
kind: 'asset',
|
|
9272
|
+
name: pointer.assetPointer,
|
|
9273
|
+
assetPointer: pointer.assetPointer,
|
|
9274
|
+
mimeType: pointer.mimeType,
|
|
9275
|
+
pointerContentType: pointer.contentType,
|
|
9276
|
+
width: pointer.width,
|
|
9277
|
+
height: pointer.height,
|
|
9278
|
+
size: pointer.size
|
|
9279
|
+
});
|
|
9280
|
+
}
|
|
9281
|
+
return result;
|
|
9282
|
+
}
|
|
9283
|
+
|
|
9284
|
+
function renderAttachmentCard(attachment) {
|
|
9285
|
+
const name = attachmentDisplayName(attachment);
|
|
9286
|
+
const meta = attachmentMetaLabel(attachment);
|
|
9287
|
+
const title = [name, meta, attachment.assetPointer || ''].filter(Boolean).join('\\n');
|
|
9288
|
+
const isImage = attachmentIsImageForDisplay(attachment);
|
|
9289
|
+
const hasUrl = attachment.available !== false && typeof attachment.url === 'string' && attachment.url;
|
|
9290
|
+
const thumb = isImage && hasUrl
|
|
9291
|
+
? '<span class="attachment-thumb image"><img loading="lazy" src="' + esc(attachment.url) + '" alt="' + esc(name) + '"></span>'
|
|
9292
|
+
: '<span class="attachment-thumb">' + attachmentIconSvg(isImage ? 'image' : 'file') + '</span>';
|
|
9293
|
+
const body =
|
|
9294
|
+
'<span class="attachment-copy"><span class="attachment-name">' + esc(name) + '</span>' +
|
|
9295
|
+
(meta ? '<span class="attachment-meta">' + esc(meta) + '</span>' : '') +
|
|
9296
|
+
(!hasUrl ? '<span class="attachment-meta">not available in raw export</span>' : '') +
|
|
9297
|
+
'</span>';
|
|
9298
|
+
const html = thumb + body;
|
|
9299
|
+
if (!hasUrl) return '<div class="attachment-card" title="' + esc(title) + '">' + html + '</div>';
|
|
9300
|
+
return '<a class="attachment-card' + (isImage ? ' image' : '') + '" href="' + esc(attachment.url) + '" target="_blank" rel="noopener" title="' + esc(title) + '">' + html + '</a>';
|
|
9301
|
+
}
|
|
9302
|
+
|
|
9303
|
+
function attachmentDisplayName(attachment) {
|
|
9304
|
+
return String(attachment && (attachment.name || attachment.id || attachment.assetPointer || 'attachment')).split(/[\\\\/]+/).pop() || 'attachment';
|
|
9305
|
+
}
|
|
9306
|
+
|
|
9307
|
+
function attachmentMetaLabel(attachment) {
|
|
9308
|
+
const bits = [];
|
|
9309
|
+
if (attachment.mimeType) bits.push(attachment.mimeType);
|
|
9310
|
+
else if (attachment.pointerContentType) bits.push(String(attachment.pointerContentType).replace(/_/g, ' '));
|
|
9311
|
+
if (attachment.width && attachment.height) bits.push(String(attachment.width) + 'x' + String(attachment.height));
|
|
9312
|
+
if (attachment.size) bits.push(formatAttachmentBytes(attachment.size));
|
|
9313
|
+
return bits.join(' · ');
|
|
9314
|
+
}
|
|
9315
|
+
|
|
9316
|
+
function formatAttachmentBytes(value) {
|
|
9317
|
+
const bytes = Number(value || 0);
|
|
9318
|
+
if (!Number.isFinite(bytes) || bytes <= 0) return '';
|
|
9319
|
+
if (bytes < 1024) return Math.round(bytes) + ' B';
|
|
9320
|
+
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(bytes >= 10 * 1024 ? 0 : 1).replace(/\\.0$/, '') + ' KB';
|
|
9321
|
+
return (bytes / (1024 * 1024)).toFixed(bytes >= 10 * 1024 * 1024 ? 0 : 1).replace(/\\.0$/, '') + ' MB';
|
|
9322
|
+
}
|
|
9323
|
+
|
|
9324
|
+
function attachmentIsImageForDisplay(attachment) {
|
|
9325
|
+
return /^image\\//i.test(String(attachment && attachment.mimeType || '')) || /\\.(png|jpe?g|gif|webp|bmp|tiff?|heic|heif|avif)$/i.test(attachmentDisplayName(attachment));
|
|
9326
|
+
}
|
|
9327
|
+
|
|
9328
|
+
function attachmentIconSvg(kind) {
|
|
9329
|
+
if (kind === 'image') {
|
|
9330
|
+
return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect width="18" height="18" x="3" y="3" rx="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.1-3.1a2 2 0 0 0-2.8 0L6 21"/></svg>';
|
|
9331
|
+
}
|
|
9332
|
+
return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/></svg>';
|
|
9333
|
+
}
|
|
9334
|
+
|
|
7920
9335
|
function renderMessageBodyWithTools(className, content, toolCalls) {
|
|
7921
9336
|
const body = className === 'user' ? renderPlainText(content) : renderRichText(content);
|
|
7922
9337
|
const stack = toolCalls.length > 1
|
|
@@ -8020,7 +9435,9 @@ function messageToolCalls(message, pairedResultMessages) {
|
|
|
8020
9435
|
const eventCalls = canonicalEventsForMessage(message, 'tool.called').map(toolCallFromEvent).filter(Boolean);
|
|
8021
9436
|
if (eventCalls.length) return attachPairedToolResults(eventCalls, pairedResultMessages);
|
|
8022
9437
|
const metadataCalls = Array.isArray(message?.metadata?.toolCalls) ? message.metadata.toolCalls : [];
|
|
9438
|
+
const structuredCall = metadataCalls.length ? null : viewerStructuredToolCall(message);
|
|
8023
9439
|
const textCalls = toolInvocationsFromText(message?.content || '');
|
|
9440
|
+
if (structuredCall) return attachPairedToolResults([structuredCall], pairedResultMessages);
|
|
8024
9441
|
if (metadataCalls.length) {
|
|
8025
9442
|
const calls = metadataCalls.map((meta, index) => {
|
|
8026
9443
|
const text = textCalls[index] || {};
|
|
@@ -8069,6 +9486,62 @@ function messageToolCalls(message, pairedResultMessages) {
|
|
|
8069
9486
|
return attachPairedToolResults(calls.filter((call) => call.kind || call.title || call.argument), pairedResultMessages);
|
|
8070
9487
|
}
|
|
8071
9488
|
|
|
9489
|
+
function isViewerStructuredToolCallMessage(message) {
|
|
9490
|
+
return Boolean(viewerStructuredToolCall(message));
|
|
9491
|
+
}
|
|
9492
|
+
|
|
9493
|
+
function viewerStructuredToolCall(message) {
|
|
9494
|
+
const meta = message && message.metadata && typeof message.metadata === 'object' ? message.metadata : {};
|
|
9495
|
+
const recipient = String(meta.recipient || '').trim();
|
|
9496
|
+
if (!recipient || recipient === 'all' || recipient === 'assistant') return null;
|
|
9497
|
+
if (String(meta.source || '') !== 'chatgpt-export' && String(currentSessionPayload?.provider || '') !== 'chatgpt') return null;
|
|
9498
|
+
const content = String(message?.content || '').trim();
|
|
9499
|
+
if (!content) return null;
|
|
9500
|
+
const parsed = parseToolArgumentsForViewer(content);
|
|
9501
|
+
const kind = viewerChatGptToolName(recipient, parsed);
|
|
9502
|
+
const preview = typeof parsed === 'object' && parsed
|
|
9503
|
+
? JSON.stringify(parsed, null, 2)
|
|
9504
|
+
: content;
|
|
9505
|
+
return toolCard({
|
|
9506
|
+
kind,
|
|
9507
|
+
name: kind,
|
|
9508
|
+
title: roleLabel(kind.replace(/[._-]+/g, ' ')),
|
|
9509
|
+
category: recipient.includes('web.') || kind.includes('web.') ? 'web' : '',
|
|
9510
|
+
rawCategory: 'chatgpt_tool_call',
|
|
9511
|
+
status: 'tool_call',
|
|
9512
|
+
argument: summarizeViewerToolArguments(parsed) || content.slice(0, 240),
|
|
9513
|
+
inputPreview: preview.slice(0, 2000),
|
|
9514
|
+
arguments: parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null
|
|
9515
|
+
});
|
|
9516
|
+
}
|
|
9517
|
+
|
|
9518
|
+
function parseToolArgumentsForViewer(value) {
|
|
9519
|
+
try { return JSON.parse(String(value || '').trim()); } catch { return String(value || '').trim(); }
|
|
9520
|
+
}
|
|
9521
|
+
|
|
9522
|
+
function viewerChatGptToolName(recipient, args) {
|
|
9523
|
+
if (recipient === 'web.run' && args && typeof args === 'object' && !Array.isArray(args)) {
|
|
9524
|
+
if (args.search_query || args.searchQuery) return 'web.search';
|
|
9525
|
+
if (args.open) return 'web.open';
|
|
9526
|
+
if (args.image_query || args.imageQuery) return 'web.image_search';
|
|
9527
|
+
if (args.finance) return 'web.finance';
|
|
9528
|
+
if (args.weather) return 'web.weather';
|
|
9529
|
+
if (args.sports) return 'web.sports';
|
|
9530
|
+
if (args.time) return 'web.time';
|
|
9531
|
+
}
|
|
9532
|
+
return recipient || 'tool';
|
|
9533
|
+
}
|
|
9534
|
+
|
|
9535
|
+
function summarizeViewerToolArguments(value) {
|
|
9536
|
+
if (!value || typeof value !== 'object') return String(value || '').slice(0, 240);
|
|
9537
|
+
for (const key of ['query', 'pattern', 'command', 'cmd', 'prompt']) {
|
|
9538
|
+
if (value[key]) return String(value[key]).slice(0, 240);
|
|
9539
|
+
}
|
|
9540
|
+
if (Array.isArray(value.search_query) && value.search_query[0]?.q) return String(value.search_query[0].q).slice(0, 240);
|
|
9541
|
+
if (Array.isArray(value.open) && value.open[0]?.ref_id) return String(value.open[0].ref_id).slice(0, 240);
|
|
9542
|
+
return Object.entries(value).slice(0, 3).map(([key, item]) => key + ': ' + (typeof item === 'string' ? item : JSON.stringify(item))).join(', ').slice(0, 240);
|
|
9543
|
+
}
|
|
9544
|
+
|
|
8072
9545
|
function attachPairedToolResults(calls, pairedResultMessages) {
|
|
8073
9546
|
if (!Array.isArray(pairedResultMessages) || !pairedResultMessages.length) return calls;
|
|
8074
9547
|
const results = pairedResultMessages
|
|
@@ -8515,6 +9988,7 @@ function contextIconSvg(kind) {
|
|
|
8515
9988
|
command_message: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="4 17 10 11 4 5"/><line x1="12" x2="20" y1="19" y2="19"/></svg>',
|
|
8516
9989
|
local_command: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="4 17 10 11 4 5"/><line x1="12" x2="20" y1="19" y2="19"/></svg>',
|
|
8517
9990
|
system_reminder: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" x2="12" y1="8" y2="12"/><line x1="12" x2="12.01" y1="16" y2="16"/></svg>',
|
|
9991
|
+
subagents: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M16 18a4 4 0 0 0-8 0"/><circle cx="12" cy="10" r="3"/><path d="M4 20a3 3 0 0 1 3-3"/><path d="M20 20a3 3 0 0 0-3-3"/><path d="M6 9a2 2 0 1 0 0 4"/><path d="M18 9a2 2 0 1 1 0 4"/></svg>',
|
|
8518
9992
|
conversation_summary: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15a4 4 0 0 1-4 4H8l-5 3V7a4 4 0 0 1 4-4h10a4 4 0 0 1 4 4z"/><path d="M8 8h8"/><path d="M8 12h6"/></svg>',
|
|
8519
9993
|
metadata: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" x2="12" y1="16" y2="12"/><line x1="12" x2="12.01" y1="8" y2="8"/></svg>'
|
|
8520
9994
|
};
|
|
@@ -8553,22 +10027,23 @@ function parseToolResult(content, message) {
|
|
|
8553
10027
|
const event = canonicalEventsForMessage(message, 'tool.completed')[0];
|
|
8554
10028
|
if (event) {
|
|
8555
10029
|
const result = toolResultFromMetadata(event.body?.toolResult || {}, event.indexed || {}, event.body?.text || content);
|
|
8556
|
-
if (result) return result;
|
|
10030
|
+
if (result) return refineViewerToolResult(result, event.body?.text || content);
|
|
8557
10031
|
}
|
|
8558
10032
|
if (message?.metadata?.toolResult) {
|
|
8559
10033
|
const result = toolResultFromMetadata(message.metadata.toolResult, {}, content);
|
|
8560
|
-
if (result) return result;
|
|
10034
|
+
if (result) return refineViewerToolResult(result, content);
|
|
8561
10035
|
}
|
|
8562
10036
|
const text = String(content || '').trim();
|
|
8563
10037
|
if (!text) return null;
|
|
8564
|
-
const structured = parseFileViewResult(text) || parseCommandResult(text) || parseSkillResult(text) || parseSearchResult(text);
|
|
10038
|
+
const structured = parseChatGptFileToolResult(text) || parseFileViewResult(text) || parseCommandResult(text) || parseSkillResult(text) || parseSearchResult(text);
|
|
8565
10039
|
if (structured) return structured;
|
|
8566
10040
|
return genericToolResult(text);
|
|
8567
10041
|
}
|
|
8568
10042
|
|
|
8569
10043
|
function toolResultFromMetadata(result, indexed, fallbackText) {
|
|
8570
|
-
const output = String(result.output || result.text || result.content || fallbackText || '').trimEnd();
|
|
8571
|
-
|
|
10044
|
+
const output = formatChatGptCitationMarkersForPlainText(String(result.output || result.text || result.content || fallbackText || '').trimEnd());
|
|
10045
|
+
const summary = formatChatGptCitationMarkersForPlainText(result.summary || indexed.summary || '');
|
|
10046
|
+
if (!output && !summary) return null;
|
|
8572
10047
|
const category = normalizedToolCategory(result.category || indexed.toolCategory || result.rawCategory || '', result.kind || indexed.title || '');
|
|
8573
10048
|
return {
|
|
8574
10049
|
id: result.id || result.callId || result.call_id || result.toolCallId || result.tool_call_id || result.toolUseId || result.tool_use_id || '',
|
|
@@ -8579,14 +10054,62 @@ function toolResultFromMetadata(result, indexed, fallbackText) {
|
|
|
8579
10054
|
category,
|
|
8580
10055
|
categoryLabel: result.categoryLabel || toolCategoryLabel(category),
|
|
8581
10056
|
icon: result.icon || indexed.toolIcon || toolIcon(category, result.kind || indexed.title || ''),
|
|
8582
|
-
detail:
|
|
8583
|
-
count: result.lineCount ? result.lineCount + ' line' + (Number(result.lineCount) === 1 ? '' : 's') : lineCountLabel(output ||
|
|
8584
|
-
output: output ||
|
|
10057
|
+
detail: summary || firstLine(output),
|
|
10058
|
+
count: result.lineCount ? result.lineCount + ' line' + (Number(result.lineCount) === 1 ? '' : 's') : lineCountLabel(output || summary || ''),
|
|
10059
|
+
output: output || summary || '',
|
|
8585
10060
|
lineStart: Number(result.startLine || result.start_line || result.lineStart || result.line_start || 0) || 0,
|
|
8586
10061
|
collapsed: Boolean(result.collapsed) || String(output).split('\\n').length > 18
|
|
8587
10062
|
};
|
|
8588
10063
|
}
|
|
8589
10064
|
|
|
10065
|
+
function refineViewerToolResult(result, fallbackText) {
|
|
10066
|
+
const chatgpt = parseChatGptFileToolResult(result?.output || fallbackText || '');
|
|
10067
|
+
if (!chatgpt) return result;
|
|
10068
|
+
return {
|
|
10069
|
+
...result,
|
|
10070
|
+
...chatgpt,
|
|
10071
|
+
id: result.id || chatgpt.id || '',
|
|
10072
|
+
header: chatgpt.header || result.header || 'Tool result',
|
|
10073
|
+
output: chatgpt.output || result.output || '',
|
|
10074
|
+
count: chatgpt.count || result.count || ''
|
|
10075
|
+
};
|
|
10076
|
+
}
|
|
10077
|
+
|
|
10078
|
+
function parseChatGptFileToolResult(text) {
|
|
10079
|
+
const value = String(text || '').trim();
|
|
10080
|
+
if (!value) return null;
|
|
10081
|
+
if (/^All the files uploaded by the user have been fully loaded\\./i.test(value)) {
|
|
10082
|
+
return {
|
|
10083
|
+
header: 'Tool result · Files',
|
|
10084
|
+
kind: 'Uploaded files loaded',
|
|
10085
|
+
category: 'read',
|
|
10086
|
+
categoryLabel: 'Files',
|
|
10087
|
+
icon: 'R',
|
|
10088
|
+
detail: firstLine(formatChatGptCitationMarkersForPlainText(value)),
|
|
10089
|
+
count: lineCountLabel(value),
|
|
10090
|
+
output: formatChatGptCitationMarkersForPlainText(value),
|
|
10091
|
+
collapsed: false
|
|
10092
|
+
};
|
|
10093
|
+
}
|
|
10094
|
+
if (!/<PARSED TEXT FOR PAGE:\\s*\\d+\\s*\\/\\s*\\d+>/i.test(value)) return null;
|
|
10095
|
+
const pages = Array.from(value.matchAll(/<PARSED TEXT FOR PAGE:\\s*(\\d+)\\s*\\/\\s*(\\d+)>/gi));
|
|
10096
|
+
const pageCount = pages.reduce((max, match) => Math.max(max, Number(match[2]) || 0), 0);
|
|
10097
|
+
const citation = chatGptCitationMarkerDetail(value.match(/\uE200filecite\uE202([^\uE201]+)\uE201/i)?.[1] || '');
|
|
10098
|
+
const detail = [citation, pageCount ? pageCount + ' page' + (pageCount === 1 ? '' : 's') : ''].filter(Boolean).join(' · ');
|
|
10099
|
+
const output = formatChatGptCitationMarkersForPlainText(value);
|
|
10100
|
+
return {
|
|
10101
|
+
header: 'Tool result · Files',
|
|
10102
|
+
kind: 'Parsed uploaded file',
|
|
10103
|
+
category: 'read',
|
|
10104
|
+
categoryLabel: 'Files',
|
|
10105
|
+
icon: 'R',
|
|
10106
|
+
detail: detail || 'Uploaded file parsed text',
|
|
10107
|
+
count: lineCountLabel(output),
|
|
10108
|
+
output,
|
|
10109
|
+
collapsed: true
|
|
10110
|
+
};
|
|
10111
|
+
}
|
|
10112
|
+
|
|
8590
10113
|
function parseFileViewResult(text) {
|
|
8591
10114
|
const match = text.match(/^<file-view\\b([^>]*)>\\n?([\\s\\S]*?)(?:\\n?<\\/file-view>\\s*)?$/);
|
|
8592
10115
|
if (!match) return null;
|
|
@@ -8671,7 +10194,7 @@ function renderToolResult(result, options) {
|
|
|
8671
10194
|
}
|
|
8672
10195
|
|
|
8673
10196
|
function renderToolOutput(output, options = {}) {
|
|
8674
|
-
const text = String(output || '');
|
|
10197
|
+
const text = formatChatGptCitationMarkersForPlainText(String(output || ''));
|
|
8675
10198
|
const lines = text.split('\\n');
|
|
8676
10199
|
if (lines.length <= 1) return '<pre class="tool-output">' + esc(text) + '</pre>';
|
|
8677
10200
|
const start = Number(options.lineStart || 0) || 1;
|
|
@@ -8864,6 +10387,13 @@ function renderInline(value) {
|
|
|
8864
10387
|
function renderInlinePlain(value) {
|
|
8865
10388
|
let html = esc(value);
|
|
8866
10389
|
const redactions = [];
|
|
10390
|
+
const citations = [];
|
|
10391
|
+
html = html.replace(/\uE200([A-Za-z_]*cite)\uE202([^\uE201]+)\uE201/g, function (_, kind, ref) {
|
|
10392
|
+
const index = citations.length;
|
|
10393
|
+
const title = chatGptCitationMarkerTitle(kind, ref);
|
|
10394
|
+
citations.push('<span class="citation-chip" title="' + esc(title) + '">' + esc(citationLabel(kind, ref)) + '</span>');
|
|
10395
|
+
return '\\u0000CITATION_' + index + '\\u0000';
|
|
10396
|
+
});
|
|
8867
10397
|
html = html.replace(/\\[REDACTED(?::|\\s+)([^\\]\\n]+)\\]/g, function (_, kind) {
|
|
8868
10398
|
const index = redactions.length;
|
|
8869
10399
|
const label = redactionLabel(kind);
|
|
@@ -8877,9 +10407,49 @@ function renderInlinePlain(value) {
|
|
|
8877
10407
|
html = html.replace(/\\u0000REDACTION_(\\d+)\\u0000/g, function (_, index) {
|
|
8878
10408
|
return redactions[Number(index)] || '';
|
|
8879
10409
|
});
|
|
10410
|
+
html = html.replace(/\\u0000CITATION_(\\d+)\\u0000/g, function (_, index) {
|
|
10411
|
+
return citations[Number(index)] || '';
|
|
10412
|
+
});
|
|
8880
10413
|
return html;
|
|
8881
10414
|
}
|
|
8882
10415
|
|
|
10416
|
+
function citationLabel(kind, ref) {
|
|
10417
|
+
const type = String(kind || '').toLowerCase();
|
|
10418
|
+
const parts = chatGptCitationMarkerParts(ref);
|
|
10419
|
+
if (type.includes('file')) {
|
|
10420
|
+
const line = parts.find((part) => /^L\\d+/i.test(part));
|
|
10421
|
+
if (line) return line.length <= 14 ? line : 'file';
|
|
10422
|
+
const file = parts.find((part) => /file/i.test(part)) || '';
|
|
10423
|
+
const shortFile = file.replace(/^turn\\d+/, '').replace(/^[-_:]+/, '');
|
|
10424
|
+
return shortFile && shortFile.length <= 12 ? shortFile : 'file';
|
|
10425
|
+
}
|
|
10426
|
+
const text = parts[parts.length - 1] || String(ref || '').trim();
|
|
10427
|
+
const short = text.replace(/^turn\\d+/, '').replace(/^[-_:]+/, '');
|
|
10428
|
+
return short && short.length <= 12 ? short : 'cite';
|
|
10429
|
+
}
|
|
10430
|
+
|
|
10431
|
+
function chatGptCitationMarkerTitle(kind, ref) {
|
|
10432
|
+
const label = /file/i.test(String(kind || '')) ? 'ChatGPT file citation' : 'ChatGPT citation';
|
|
10433
|
+
const detail = chatGptCitationMarkerDetail(ref);
|
|
10434
|
+
return detail ? label + ': ' + detail : label;
|
|
10435
|
+
}
|
|
10436
|
+
|
|
10437
|
+
function chatGptCitationMarkerDetail(ref) {
|
|
10438
|
+
return chatGptCitationMarkerParts(ref).join(' ');
|
|
10439
|
+
}
|
|
10440
|
+
|
|
10441
|
+
function chatGptCitationMarkerParts(ref) {
|
|
10442
|
+
return String(ref || '').split('\uE202').map((part) => part.trim()).filter(Boolean);
|
|
10443
|
+
}
|
|
10444
|
+
|
|
10445
|
+
function formatChatGptCitationMarkersForPlainText(value) {
|
|
10446
|
+
return String(value || '').replace(/\uE200([A-Za-z_]*cite)\uE202([^\uE201]+)\uE201/g, function (_, kind, ref) {
|
|
10447
|
+
const label = /file/i.test(String(kind || '')) ? 'file citation' : 'citation';
|
|
10448
|
+
const detail = chatGptCitationMarkerDetail(ref);
|
|
10449
|
+
return detail ? '[' + label + ': ' + detail + ']' : '[' + label + ']';
|
|
10450
|
+
});
|
|
10451
|
+
}
|
|
10452
|
+
|
|
8883
10453
|
function renderMarkdownLink(label, href) {
|
|
8884
10454
|
const value = String(href || '').trim();
|
|
8885
10455
|
if (!/^(https?:|mailto:|file:|\\/|#)/i.test(value)) {
|
|
@@ -8904,7 +10474,47 @@ function compactSkillPath(value) {
|
|
|
8904
10474
|
return '.../' + parts.slice(-2).join('/');
|
|
8905
10475
|
}
|
|
8906
10476
|
|
|
8907
|
-
function
|
|
10477
|
+
function clampScrollRatio(value) {
|
|
10478
|
+
const number = Number(value);
|
|
10479
|
+
if (!Number.isFinite(number)) return 0;
|
|
10480
|
+
return Math.max(0, Math.min(1, number));
|
|
10481
|
+
}
|
|
10482
|
+
|
|
10483
|
+
function captureRelativeScrollPosition() {
|
|
10484
|
+
const scroller = document.querySelector('.detail-scroll');
|
|
10485
|
+
if (!scroller) return null;
|
|
10486
|
+
const maxScroll = Math.max(0, scroller.scrollHeight - scroller.clientHeight);
|
|
10487
|
+
return {
|
|
10488
|
+
scroller,
|
|
10489
|
+
ratio: maxScroll > 0 ? clampScrollRatio(scroller.scrollTop / maxScroll) : 0
|
|
10490
|
+
};
|
|
10491
|
+
}
|
|
10492
|
+
|
|
10493
|
+
function restoreRelativeScrollPosition(position, serial) {
|
|
10494
|
+
if (!position || !position.scroller) return;
|
|
10495
|
+
if (serial !== viewScrollRestoreSerial) return;
|
|
10496
|
+
const scroller = position.scroller;
|
|
10497
|
+
const maxScroll = Math.max(0, scroller.scrollHeight - scroller.clientHeight);
|
|
10498
|
+
scroller.scrollTop = Math.round(maxScroll * clampScrollRatio(position.ratio));
|
|
10499
|
+
if (typeof updateJumpEndVisibility === 'function') updateJumpEndVisibility();
|
|
10500
|
+
}
|
|
10501
|
+
|
|
10502
|
+
function scheduleRelativeScrollRestore(position, serial) {
|
|
10503
|
+
if (!position) return;
|
|
10504
|
+
const schedule = window.requestAnimationFrame
|
|
10505
|
+
? (fn) => window.requestAnimationFrame(fn)
|
|
10506
|
+
: (fn) => window.setTimeout(fn, 0);
|
|
10507
|
+
schedule(() => {
|
|
10508
|
+
restoreRelativeScrollPosition(position, serial);
|
|
10509
|
+
schedule(() => restoreRelativeScrollPosition(position, serial));
|
|
10510
|
+
});
|
|
10511
|
+
}
|
|
10512
|
+
|
|
10513
|
+
function setView(mode, options) {
|
|
10514
|
+
const opts = options || {};
|
|
10515
|
+
const preserveScroll = opts.preserveScroll !== false && mode !== viewMode;
|
|
10516
|
+
const scrollPosition = preserveScroll ? captureRelativeScrollPosition() : null;
|
|
10517
|
+
const restoreSerial = ++viewScrollRestoreSerial;
|
|
8908
10518
|
viewMode = mode;
|
|
8909
10519
|
const markdown = mode === 'markdown';
|
|
8910
10520
|
readableButton.classList.toggle('active', !markdown);
|
|
@@ -8913,9 +10523,15 @@ function setView(mode) {
|
|
|
8913
10523
|
markdownButton.setAttribute('aria-pressed', markdown ? 'true' : 'false');
|
|
8914
10524
|
readableView.style.display = markdown ? 'none' : 'block';
|
|
8915
10525
|
markdownView.style.display = markdown ? 'block' : 'none';
|
|
8916
|
-
|
|
8917
|
-
|
|
8918
|
-
|
|
10526
|
+
const markdownLoad = markdown
|
|
10527
|
+
? ensureMarkdownLoaded().catch((error) => {
|
|
10528
|
+
markdownView.textContent = error.message || 'Markdown could not be loaded.';
|
|
10529
|
+
})
|
|
10530
|
+
: null;
|
|
10531
|
+
if (preserveScroll) {
|
|
10532
|
+
scheduleRelativeScrollRestore(scrollPosition, restoreSerial);
|
|
10533
|
+
if (markdownLoad) markdownLoad.finally(() => scheduleRelativeScrollRestore(scrollPosition, restoreSerial));
|
|
10534
|
+
}
|
|
8919
10535
|
}
|
|
8920
10536
|
|
|
8921
10537
|
async function ensureMarkdownLoaded() {
|
|
@@ -9064,7 +10680,8 @@ function treeSignature(payload) {
|
|
|
9064
10680
|
const sessions = (group.sessions || []).map((session) => session.session_id + '=' + (session.updated_at || session.ended_at || session.started_at || '')).join('|');
|
|
9065
10681
|
return (group.repo_key || '') + '#' + (group.count || 0) + ':' + sessions;
|
|
9066
10682
|
}).join('||');
|
|
9067
|
-
|
|
10683
|
+
const sourceSig = (payload.available_source_options || []).map((option) => option.value || '').join(',');
|
|
10684
|
+
return groupSig + '::' + (payload.count || 0) + '::sources=' + sourceSig;
|
|
9068
10685
|
}
|
|
9069
10686
|
|
|
9070
10687
|
function startLiveRefresh() {
|
|
@@ -9262,14 +10879,27 @@ recentButton.onclick = () => {
|
|
|
9262
10879
|
setSelectValue('scope', 'all');
|
|
9263
10880
|
loadTree().catch((error) => { setEmptySession(error.message); });
|
|
9264
10881
|
};
|
|
9265
|
-
readableButton.onclick = () => setView('readable');
|
|
9266
|
-
markdownButton.onclick = () => setView('markdown');
|
|
10882
|
+
readableButton.onclick = () => setView('readable', { preserveScroll: true });
|
|
10883
|
+
markdownButton.onclick = () => setView('markdown', { preserveScroll: true });
|
|
9267
10884
|
copyDetailsButton.onclick = () => {
|
|
9268
10885
|
copySessionDetails();
|
|
9269
10886
|
};
|
|
9270
10887
|
copyResumeButton.onclick = () => {
|
|
9271
10888
|
copyResumeCommand();
|
|
9272
10889
|
};
|
|
10890
|
+
if (sessionModalClose) sessionModalClose.onclick = closeSubagentModal;
|
|
10891
|
+
if (sessionModal) {
|
|
10892
|
+
sessionModal.addEventListener('click', (event) => {
|
|
10893
|
+
if (event.target === sessionModal) closeSubagentModal();
|
|
10894
|
+
});
|
|
10895
|
+
}
|
|
10896
|
+
window.addEventListener('keydown', (event) => {
|
|
10897
|
+
if (event.key === 'Escape' && sessionModal && !sessionModal.hidden) {
|
|
10898
|
+
closeSubagentModal();
|
|
10899
|
+
event.preventDefault();
|
|
10900
|
+
event.stopPropagation();
|
|
10901
|
+
}
|
|
10902
|
+
});
|
|
9273
10903
|
jumpEnd.onclick = () => {
|
|
9274
10904
|
const scroller = document.querySelector('.detail-scroll');
|
|
9275
10905
|
scroller.scrollTo({ top: scroller.scrollHeight, behavior: 'smooth' });
|
|
@@ -9500,7 +11130,7 @@ function statsBreakdownGroupId(entry) {
|
|
|
9500
11130
|
}
|
|
9501
11131
|
|
|
9502
11132
|
function emptyStatsTotals() {
|
|
9503
|
-
return { tokens: 0, tokens_input: 0, tokens_output: 0, tokens_cache: 0, tokens_estimated: 0, conversations: 0, user_messages: 0 };
|
|
11133
|
+
return { tokens: 0, tokens_input: 0, tokens_output: 0, tokens_cache: 0, tokens_reasoning: 0, tokens_estimated: 0, conversations: 0, user_messages: 0 };
|
|
9504
11134
|
}
|
|
9505
11135
|
|
|
9506
11136
|
function addStatsTotals(target, entry) {
|
|
@@ -9509,6 +11139,7 @@ function addStatsTotals(target, entry) {
|
|
|
9509
11139
|
target.tokens_input += Number(entry.tokens_input || 0);
|
|
9510
11140
|
target.tokens_output += Number(entry.tokens_output || 0);
|
|
9511
11141
|
target.tokens_cache += Number(entry.tokens_cache || 0);
|
|
11142
|
+
target.tokens_reasoning += Number(entry.tokens_reasoning || 0);
|
|
9512
11143
|
target.tokens_estimated += Number(entry.tokens_estimated || 0);
|
|
9513
11144
|
target.conversations += Number(entry.conversations ?? entry.sessions ?? 0);
|
|
9514
11145
|
target.user_messages += Number(entry.user_messages || 0);
|
|
@@ -9593,7 +11224,7 @@ function statsRepoRowsForRange(payload) {
|
|
|
9593
11224
|
for (const row of Array.isArray(day.repos) ? day.repos : []) {
|
|
9594
11225
|
const key = row.repo_key || row.repo_display || 'unknown';
|
|
9595
11226
|
if (!repos.has(key)) {
|
|
9596
|
-
repos.set(key, { repo_key: row.repo_key || key, repo_display: row.repo_display || key, providers: {}, companies: {}, models: {}, tokens: 0, tokens_input: 0, tokens_output: 0, tokens_cache: 0, tokens_estimated: 0, conversations: 0, user_messages: 0 });
|
|
11227
|
+
repos.set(key, { repo_key: row.repo_key || key, repo_display: row.repo_display || key, providers: {}, companies: {}, models: {}, tokens: 0, tokens_input: 0, tokens_output: 0, tokens_cache: 0, tokens_reasoning: 0, tokens_estimated: 0, conversations: 0, user_messages: 0 });
|
|
9597
11228
|
}
|
|
9598
11229
|
const target = repos.get(key);
|
|
9599
11230
|
target.repo_display = row.repo_display || target.repo_display;
|
|
@@ -9647,7 +11278,7 @@ function renderStats(payload) {
|
|
|
9647
11278
|
}
|
|
9648
11279
|
|
|
9649
11280
|
function statsTokenMetric() {
|
|
9650
|
-
if (statsShowInputTokens && statsShowOutputTokens &&
|
|
11281
|
+
if (statsShowInputTokens && statsShowOutputTokens && statsShowCacheTokens) return 'tokens';
|
|
9651
11282
|
if (statsShowInputTokens && !statsShowOutputTokens && !statsShowCacheTokens) return 'tokens_input';
|
|
9652
11283
|
if (!statsShowInputTokens && statsShowOutputTokens && !statsShowCacheTokens) return 'tokens_output';
|
|
9653
11284
|
if (!statsShowInputTokens && !statsShowOutputTokens && statsShowCacheTokens) return 'tokens_cache';
|
|
@@ -9676,7 +11307,7 @@ function statsMetricValue(entry, metric) {
|
|
|
9676
11307
|
}
|
|
9677
11308
|
|
|
9678
11309
|
function isTokenMetric(metric) {
|
|
9679
|
-
return metric === 'tokens' || metric === 'tokens_input' || metric === 'tokens_output' || metric === 'tokens_cache' || metric === 'tokens_selected';
|
|
11310
|
+
return metric === 'tokens' || metric === 'tokens_input' || metric === 'tokens_output' || metric === 'tokens_cache' || metric === 'tokens_reasoning' || metric === 'tokens_selected';
|
|
9680
11311
|
}
|
|
9681
11312
|
|
|
9682
11313
|
function statsActivityMetric() {
|
|
@@ -10014,6 +11645,7 @@ function renderStatsMetrics(payload) {
|
|
|
10014
11645
|
const totalInTok = Number(payload.total_input_tokens || 0);
|
|
10015
11646
|
const totalOutTok = Number(payload.total_output_tokens || 0);
|
|
10016
11647
|
const totalCacheTok = Number(payload.total_cache_tokens || 0);
|
|
11648
|
+
const totalReasonTok = Number(payload.total_reasoning_tokens || 0);
|
|
10017
11649
|
const totalEstimatedTok = Number(payload.total_estimated_tokens || 0);
|
|
10018
11650
|
const sdkSessions = Number(payload.sdk_session_count || 0);
|
|
10019
11651
|
const sdkTokens = Number(payload.sdk_total_tokens || 0);
|
|
@@ -10028,6 +11660,7 @@ function renderStatsMetrics(payload) {
|
|
|
10028
11660
|
const usageParts = [];
|
|
10029
11661
|
if (totalInTok || totalOutTok) usageParts.push(formatCompactNumber(totalInTok) + ' in / ' + formatCompactNumber(totalOutTok) + ' out');
|
|
10030
11662
|
if (totalCacheTok) usageParts.push(formatCompactNumber(totalCacheTok) + ' cache');
|
|
11663
|
+
if (totalReasonTok) usageParts.push(formatCompactNumber(totalReasonTok) + ' reasoning');
|
|
10031
11664
|
if (totalEstimatedTok) usageParts.push('~' + formatCompactNumber(totalEstimatedTok) + ' estimated');
|
|
10032
11665
|
tokenSub = usageParts.join(' · ');
|
|
10033
11666
|
} else {
|
|
@@ -10184,7 +11817,7 @@ function renderHeatmapSection(payload) {
|
|
|
10184
11817
|
});
|
|
10185
11818
|
renderSecondaryHeatmap(payload.split_stats && payload.split_stats.sdk, 'statsSdkActivitySub', 'statsSdkHeatmap', range, metric, {
|
|
10186
11819
|
emptySubText: '',
|
|
10187
|
-
emptyText: 'No SDK jobs imported.'
|
|
11820
|
+
emptyText: 'No SDK jobs imported. Import once with agentlog import --sources codex-sdk,claude-sdk --since all. Keep them updated with agentlog config sources add codex-sdk,claude-sdk.'
|
|
10188
11821
|
});
|
|
10189
11822
|
}
|
|
10190
11823
|
|
|
@@ -10571,7 +12204,7 @@ function setupCustomSelects() {
|
|
|
10571
12204
|
const input = field.querySelector('input');
|
|
10572
12205
|
const trigger = field.querySelector('.select-trigger');
|
|
10573
12206
|
if (field.dataset.select === 'provider') hydrateProviderSelectOptions(field);
|
|
10574
|
-
const options =
|
|
12207
|
+
const options = bindSelectFieldOptions(field);
|
|
10575
12208
|
const selectedOption = options.find((item) => (item.dataset.value || '') === input.value) || options.find((item) => item.classList.contains('active')) || options[0];
|
|
10576
12209
|
if (selectedOption) setSelectTriggerLabel(field, selectedOption);
|
|
10577
12210
|
updateSelectFieldState(field);
|
|
@@ -10582,23 +12215,65 @@ function setupCustomSelects() {
|
|
|
10582
12215
|
}
|
|
10583
12216
|
field.classList.toggle('open');
|
|
10584
12217
|
};
|
|
10585
|
-
for (const option of options) {
|
|
10586
|
-
option.onclick = (event) => {
|
|
10587
|
-
event.stopPropagation();
|
|
10588
|
-
input.value = option.dataset.value || '';
|
|
10589
|
-
setSelectTriggerLabel(field, option);
|
|
10590
|
-
options.forEach((item) => item.classList.toggle('active', item === option));
|
|
10591
|
-
updateSelectFieldState(field);
|
|
10592
|
-
field.classList.remove('open');
|
|
10593
|
-
refreshForFilterChange();
|
|
10594
|
-
};
|
|
10595
|
-
}
|
|
10596
12218
|
}
|
|
10597
12219
|
document.addEventListener('click', () => {
|
|
10598
12220
|
for (const field of document.querySelectorAll('.select-field.open')) field.classList.remove('open');
|
|
10599
12221
|
});
|
|
10600
12222
|
}
|
|
10601
12223
|
|
|
12224
|
+
function bindSelectFieldOptions(field) {
|
|
12225
|
+
const input = field.querySelector('input');
|
|
12226
|
+
const options = Array.from(field.querySelectorAll('.select-option'));
|
|
12227
|
+
for (const option of options) {
|
|
12228
|
+
option.onclick = (event) => {
|
|
12229
|
+
event.stopPropagation();
|
|
12230
|
+
input.value = option.dataset.value || '';
|
|
12231
|
+
setSelectTriggerLabel(field, option);
|
|
12232
|
+
options.forEach((item) => item.classList.toggle('active', item === option));
|
|
12233
|
+
updateSelectFieldState(field);
|
|
12234
|
+
field.classList.remove('open');
|
|
12235
|
+
refreshForFilterChange();
|
|
12236
|
+
};
|
|
12237
|
+
}
|
|
12238
|
+
return options;
|
|
12239
|
+
}
|
|
12240
|
+
|
|
12241
|
+
function updateSourceFilterOptions(sourceOptions) {
|
|
12242
|
+
const field = document.querySelector('.select-field[data-select="provider"]');
|
|
12243
|
+
if (!field) return false;
|
|
12244
|
+
const input = field.querySelector('input');
|
|
12245
|
+
const menu = field.querySelector('.select-menu');
|
|
12246
|
+
if (!input || !menu) return false;
|
|
12247
|
+
const options = normalizeSourceFilterOptions(sourceOptions);
|
|
12248
|
+
const current = input.value || '';
|
|
12249
|
+
const values = new Set(options.map((option) => option.value));
|
|
12250
|
+
const reset = Boolean(current && !values.has(current));
|
|
12251
|
+
menu.innerHTML =
|
|
12252
|
+
'<button class="select-option" type="button" data-value="" data-label="All sources">All sources</button>' +
|
|
12253
|
+
options.map((option) =>
|
|
12254
|
+
'<button class="select-option" type="button" data-value="' + esc(option.value) + '" data-label="' + esc(option.label) + '">' +
|
|
12255
|
+
esc(option.label) +
|
|
12256
|
+
'</button>'
|
|
12257
|
+
).join('');
|
|
12258
|
+
hydrateProviderSelectOptions(field);
|
|
12259
|
+
bindSelectFieldOptions(field);
|
|
12260
|
+
setSelectValue('provider', reset ? '' : current);
|
|
12261
|
+
return reset;
|
|
12262
|
+
}
|
|
12263
|
+
|
|
12264
|
+
function normalizeSourceFilterOptions(sourceOptions) {
|
|
12265
|
+
const seen = new Set();
|
|
12266
|
+
const out = [];
|
|
12267
|
+
for (const option of Array.isArray(sourceOptions) ? sourceOptions : []) {
|
|
12268
|
+
const value = String(option?.value || option?.source || '').trim();
|
|
12269
|
+
const label = String(option?.label || value).trim();
|
|
12270
|
+
if (!value || seen.has(value)) continue;
|
|
12271
|
+
seen.add(value);
|
|
12272
|
+
out.push({ value, label: label || value });
|
|
12273
|
+
}
|
|
12274
|
+
return out;
|
|
12275
|
+
}
|
|
12276
|
+
|
|
10602
12277
|
function hydrateProviderSelectOptions(field) {
|
|
10603
12278
|
for (const option of field.querySelectorAll('.select-option')) {
|
|
10604
12279
|
if (!option.dataset.label) option.dataset.label = option.textContent.trim();
|
|
@@ -10679,6 +12354,12 @@ function setupKeyboardShortcuts() {
|
|
|
10679
12354
|
const isInput = target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable);
|
|
10680
12355
|
|
|
10681
12356
|
if (event.key === 'Escape') {
|
|
12357
|
+
if (sessionModal && !sessionModal.hidden) {
|
|
12358
|
+
closeSubagentModal();
|
|
12359
|
+
event.preventDefault();
|
|
12360
|
+
event.stopPropagation();
|
|
12361
|
+
return;
|
|
12362
|
+
}
|
|
10682
12363
|
const searchInput = document.getElementById('q');
|
|
10683
12364
|
if (searchInput && (searchInput.value || activeSearchTerm)) {
|
|
10684
12365
|
searchInput.value = '';
|
|
@@ -10908,8 +12589,8 @@ Start here:
|
|
|
10908
12589
|
Archive and import:
|
|
10909
12590
|
init interactive setup and optional first import
|
|
10910
12591
|
import import local Codex, Claude, Gemini, Devin, Cursor, Cline, OpenCode, Aider, and Antigravity history
|
|
10911
|
-
import chatgpt [path]
|
|
10912
|
-
import claude-web [path]
|
|
12592
|
+
import chatgpt [path] guided ChatGPT export import; with path, import ZIP/folder
|
|
12593
|
+
import claude-web [path] guided Claude.ai export import; with path, import ZIP/folder
|
|
10913
12594
|
import windsurf <path> import downloaded Windsurf trajectory Markdown file/folder
|
|
10914
12595
|
import accounts list or rename ChatGPT/Claude.ai export accounts
|
|
10915
12596
|
sync choose a remote, preview, confirm, then upload archive objects
|
|
@@ -10992,8 +12673,8 @@ Import sources:
|
|
|
10992
12673
|
all configured default local sources
|
|
10993
12674
|
|
|
10994
12675
|
Web export sources:
|
|
10995
|
-
chatgpt
|
|
10996
|
-
claude-web
|
|
12676
|
+
chatgpt guided import for ChatGPT/OpenAI export ZIPs or folders
|
|
12677
|
+
claude-web guided import for Claude.ai export ZIPs or folders
|
|
10997
12678
|
windsurf downloaded Cascade trajectory Markdown file/folder
|
|
10998
12679
|
|
|
10999
12680
|
Examples:
|
|
@@ -11018,10 +12699,11 @@ Examples:
|
|
|
11018
12699
|
agentlog import chatgpt
|
|
11019
12700
|
agentlog import claude-web
|
|
11020
12701
|
agentlog import chatgpt ~/Downloads/chatgpt-export.zip --username you@example.com
|
|
11021
|
-
agentlog import
|
|
12702
|
+
agentlog import chatgpt "~/Downloads/OpenAI-export/User Online Activity" --username you@example.com
|
|
12703
|
+
agentlog import claude-web ~/Downloads/claude-export --username you --display-name "Personal Claude" --scope local
|
|
11022
12704
|
agentlog import windsurf ~/Downloads/cascade-chat-conversation.md
|
|
11023
12705
|
agentlog import windsurf ~/windsurf-cascade-export
|
|
11024
|
-
agentlog import accounts rename claude-web
|
|
12706
|
+
agentlog import accounts rename claude-web you --display-name "Personal Claude"
|
|
11025
12707
|
|
|
11026
12708
|
Details:
|
|
11027
12709
|
--since accepts 30d, 12h, 60m, ISO dates, or all.
|
|
@@ -11029,7 +12711,8 @@ Details:
|
|
|
11029
12711
|
--dry-run shows what would be imported without writing archive files.
|
|
11030
12712
|
--explain-skips includes per-session skip reasons for supported sources.
|
|
11031
12713
|
--json prints machine-readable import results.
|
|
11032
|
-
ChatGPT and Claude.ai imports
|
|
12714
|
+
ChatGPT and Claude.ai imports prompt for export paths and account labels when omitted in a terminal.
|
|
12715
|
+
Use --instructions to print export instructions without starting the walkthrough.
|
|
11033
12716
|
Windsurf imports prompt for the export path when omitted in a terminal.
|
|
11034
12717
|
Windsurf local cache scanning is disabled because current Cascade transcripts are encrypted binary stores. Use the Windsurf "Download trajectory" Markdown export with \`agentlog import windsurf <file-or-folder>\`.
|
|
11035
12718
|
See docs/history-source-handling.md for source-specific storage paths.
|
|
@@ -11156,7 +12839,7 @@ Usage:
|
|
|
11156
12839
|
Examples:
|
|
11157
12840
|
agentlog accounts list
|
|
11158
12841
|
agentlog accounts rename
|
|
11159
|
-
agentlog accounts rename claude-web
|
|
12842
|
+
agentlog accounts rename claude-web you --display-name "Personal Claude"
|
|
11160
12843
|
`,
|
|
11161
12844
|
config: `
|
|
11162
12845
|
agentlog config
|
|
@@ -11390,7 +13073,7 @@ Usage:
|
|
|
11390
13073
|
agentlog update [--yes] [--dry-run] [--since 30d|all] [--sources a,b,c] [--sync] [--no-index] [--no-restart]
|
|
11391
13074
|
|
|
11392
13075
|
What it removes:
|
|
11393
|
-
archive/index data local archive objects and search indexes
|
|
13076
|
+
archive/index data local agent archive objects and search indexes
|
|
11394
13077
|
import state file/session fingerprints so sources are read again
|
|
11395
13078
|
cache/spool reveal cache and pending local spool files
|
|
11396
13079
|
sync state local upload bookkeeping; remote objects are not deleted
|
|
@@ -11399,13 +13082,14 @@ What it keeps:
|
|
|
11399
13082
|
config.json storage, source, sync, privacy, and watcher preferences
|
|
11400
13083
|
redaction.yaml redaction rules
|
|
11401
13084
|
web account labels ChatGPT/Claude.ai account display names
|
|
13085
|
+
web chat archives manually imported ChatGPT/Claude.ai archive objects
|
|
11402
13086
|
source histories Codex, Claude, Gemini, Devin, Cursor, etc. source data
|
|
11403
13087
|
recall integrations agent command/skill files
|
|
11404
13088
|
|
|
11405
13089
|
Options:
|
|
11406
13090
|
--dry-run show targets and preferences without deleting or importing
|
|
11407
13091
|
--yes skip confirmation
|
|
11408
|
-
--since 30d|90d|all import window; defaults to
|
|
13092
|
+
--since 30d|90d|all import window; defaults to the saved rebuild window, then all
|
|
11409
13093
|
--sources a,b,c override configured sources for this run
|
|
11410
13094
|
--sync upload changed objects after reimport; use sync replace to remove stale remote objects
|
|
11411
13095
|
--no-index skip rebuilding the search index
|
|
@@ -11564,13 +13248,19 @@ module.exports = {
|
|
|
11564
13248
|
_historyWebInternals: {
|
|
11565
13249
|
HISTORY_AUTH_COOKIE,
|
|
11566
13250
|
constantTimeEqual,
|
|
13251
|
+
filterShapeKey,
|
|
11567
13252
|
historyHtml,
|
|
11568
13253
|
isHistoryApiPath,
|
|
13254
|
+
listSnapshotEtag,
|
|
13255
|
+
memoizedStatsPayload,
|
|
11569
13256
|
parseCookies,
|
|
11570
13257
|
readSessionMarkdown,
|
|
11571
13258
|
readSessionView,
|
|
13259
|
+
resolveSessionAttachmentFile,
|
|
11572
13260
|
resumeCommandForSession,
|
|
13261
|
+
repoSessionsFilterKey,
|
|
11573
13262
|
securityHeaders,
|
|
13263
|
+
sessionTreePayload,
|
|
11574
13264
|
sessionViewPayload,
|
|
11575
13265
|
sessionCookie,
|
|
11576
13266
|
statsPayload,
|