agentel 0.2.5 → 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 +77 -37
- package/docs/code-reference.md +26 -13
- package/docs/history-source-handling.md +247 -82
- package/docs/release.md +1 -1
- package/package.json +5 -2
- package/src/archive.js +200 -17
- package/src/canonical-events.js +74 -25
- package/src/cli.js +2561 -204
- package/src/config.js +11 -0
- package/src/doctor.js +2 -0
- package/src/importers/claude.js +309 -11
- package/src/importers/gemini.js +2 -1
- package/src/importers/providers.js +22 -0
- package/src/importers.js +2142 -212
- package/src/parser-versions.js +1 -0
- package/src/search.js +417 -176
- package/src/sources.js +1 -0
- package/src/web-export-instructions.js +79 -0
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,15 +18,28 @@ 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");
|
|
25
25
|
const { version } = require("./version");
|
|
26
26
|
const { listWebAccounts, renameWebAccount } = require("./web-accounts");
|
|
27
|
+
const { webExportInstructions } = require("./web-export-instructions");
|
|
27
28
|
|
|
28
29
|
const HISTORY_AUTH_COOKIE = "agentlog_history";
|
|
29
|
-
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;
|
|
30
43
|
|
|
31
44
|
async function main(argv = process.argv.slice(2), env = process.env) {
|
|
32
45
|
const { positionals, flags } = parseArgs(argv);
|
|
@@ -152,10 +165,12 @@ async function initCommand(flags, env) {
|
|
|
152
165
|
});
|
|
153
166
|
discoveryProgress.done();
|
|
154
167
|
const importSources = await chooseImportSources(flags, discovered);
|
|
168
|
+
let backfillSince = "";
|
|
155
169
|
|
|
156
170
|
if (!flags["skip-import"]) {
|
|
157
171
|
const since = await chooseImportSince(flags);
|
|
158
172
|
if (since) {
|
|
173
|
+
backfillSince = since;
|
|
159
174
|
printSection("Backfill");
|
|
160
175
|
printMuted(`Backing up existing history into ${fullPath(cfg.storage.root || paths(env).data)}`);
|
|
161
176
|
const progress = createProgressReporter();
|
|
@@ -169,6 +184,7 @@ async function initCommand(flags, env) {
|
|
|
169
184
|
}
|
|
170
185
|
|
|
171
186
|
const watcherSetup = await chooseWatcherSources(flags, discovered, importSources);
|
|
187
|
+
if (backfillSince) applyUpdateSincePreference(cfg, backfillSince);
|
|
172
188
|
cfg.imports.sources = watcherSetup.sources;
|
|
173
189
|
cfg.imports.autoDiscoverSources = watcherSetup.autoDiscoverSources;
|
|
174
190
|
saveConfig(cfg, env);
|
|
@@ -1444,6 +1460,7 @@ function printConfigSetupSummary(cfg, env, loginStatus = null, changed = false)
|
|
|
1444
1460
|
printCheck("Config", fullPath(paths(env).config));
|
|
1445
1461
|
printCheck("Watcher sources", (cfg.imports?.sources || []).join(", ") || "none");
|
|
1446
1462
|
printCheck("Import window", `${cfg.imports?.defaultSinceDays ?? 30} days`);
|
|
1463
|
+
printCheck("Update rebuild window", defaultImportSince(cfg));
|
|
1447
1464
|
printCheck("Auto-discover sources", cfg.imports?.autoDiscoverSources === false ? "disabled" : "enabled");
|
|
1448
1465
|
printCheck("Sync interval", formatConfigInterval(cfg.sync?.intervalMinutes ?? 30));
|
|
1449
1466
|
if (loginStatus) {
|
|
@@ -1795,19 +1812,35 @@ async function updateCommand(flags, env) {
|
|
|
1795
1812
|
return;
|
|
1796
1813
|
}
|
|
1797
1814
|
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
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
|
+
}
|
|
1803
1824
|
}
|
|
1804
|
-
}
|
|
1805
1825
|
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
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;
|
|
1809
1843
|
}
|
|
1810
|
-
ensureBaseDirs(p);
|
|
1811
1844
|
|
|
1812
1845
|
printSection("Import");
|
|
1813
1846
|
const progress = flags.json ? null : createProgressReporter();
|
|
@@ -1851,7 +1884,128 @@ async function updateCommand(flags, env) {
|
|
|
1851
1884
|
}
|
|
1852
1885
|
}
|
|
1853
1886
|
|
|
1854
|
-
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
|
+
}
|
|
1855
2009
|
}
|
|
1856
2010
|
|
|
1857
2011
|
async function waitForSupervisorExit(pid, timeoutMs = 5000) {
|
|
@@ -1906,11 +2060,41 @@ function updateTargets(env) {
|
|
|
1906
2060
|
}
|
|
1907
2061
|
|
|
1908
2062
|
function defaultImportSince(config) {
|
|
1909
|
-
const
|
|
1910
|
-
if (
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
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]}`;
|
|
1914
2098
|
}
|
|
1915
2099
|
|
|
1916
2100
|
function coalesceTargets(targets) {
|
|
@@ -2185,33 +2369,39 @@ async function importCommand(args, flags, env) {
|
|
|
2185
2369
|
printImportResults([result]);
|
|
2186
2370
|
return;
|
|
2187
2371
|
}
|
|
2188
|
-
if (sub === "claude-web" || sub === "chatgpt") {
|
|
2189
|
-
|
|
2190
|
-
if (
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2372
|
+
if (sub === "claude-web" || sub === "claude_web" || sub === "chatgpt") {
|
|
2373
|
+
const instructions = webExportInstructions(sub);
|
|
2374
|
+
if (flags.instructions || flags.instruction || flags.docs) {
|
|
2375
|
+
return printWebExportInstructions(instructions, flags);
|
|
2376
|
+
}
|
|
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);
|
|
2194
2381
|
}
|
|
2195
|
-
if (!importFile) throw new Error(`usage: agentlog import ${sub} <path> --username <name> [--display-name <name>] [--account-id <id>] [--scope local|team]`);
|
|
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 || "";
|
|
@@ -2827,10 +3216,13 @@ function compareHistorySessionsForTree(a, b) {
|
|
|
2827
3216
|
|
|
2828
3217
|
function statsPayload(filters, env) {
|
|
2829
3218
|
const sessions = listHistorySessions({ ...filters, limit: 5000 }, env);
|
|
2830
|
-
return statsPayloadForSessions(sessions);
|
|
3219
|
+
return statsPayloadForSessions(sessions, { includeSdk: statsFiltersTargetSdk(filters) });
|
|
2831
3220
|
}
|
|
2832
3221
|
|
|
2833
3222
|
function statsPayloadForSessions(sessions, options = {}) {
|
|
3223
|
+
const allSessions = Array.isArray(sessions) ? sessions : [];
|
|
3224
|
+
const sdkSessions = allSessions.filter(isSdkStatsSession);
|
|
3225
|
+
const statsSessions = options.includeSdk ? allSessions : allSessions.filter((session) => !isSdkStatsSession(session));
|
|
2834
3226
|
const providerSet = new Set();
|
|
2835
3227
|
const companySet = new Set();
|
|
2836
3228
|
const modelGroupSet = new Set();
|
|
@@ -2847,13 +3239,14 @@ function statsPayloadForSessions(sessions, options = {}) {
|
|
|
2847
3239
|
let totalInputTokens = 0;
|
|
2848
3240
|
let totalOutputTokens = 0;
|
|
2849
3241
|
let totalCacheTokens = 0;
|
|
3242
|
+
let totalReasoningTokens = 0;
|
|
2850
3243
|
let totalEstimatedTokens = 0;
|
|
2851
3244
|
let totalConversations = 0;
|
|
2852
3245
|
let totalMessages = 0;
|
|
2853
3246
|
let totalUserMessages = 0;
|
|
2854
3247
|
let peakSessionTokens = 0;
|
|
2855
3248
|
let peakSessionLabel = "";
|
|
2856
|
-
for (const session of
|
|
3249
|
+
for (const session of statsSessions) {
|
|
2857
3250
|
const provider = String(session.provider || "unknown");
|
|
2858
3251
|
const modelGroup = statsSessionPrimaryModel(session);
|
|
2859
3252
|
const companyGroup = statsSessionCompany(session, provider, modelGroup);
|
|
@@ -2870,6 +3263,7 @@ function statsPayloadForSessions(sessions, options = {}) {
|
|
|
2870
3263
|
totalInputTokens += usageTokens.tokens_input;
|
|
2871
3264
|
totalOutputTokens += usageTokens.tokens_output;
|
|
2872
3265
|
totalCacheTokens += usageTokens.tokens_cache;
|
|
3266
|
+
totalReasoningTokens += usageTokens.tokens_reasoning;
|
|
2873
3267
|
totalEstimatedTokens += usageTokens.tokens_estimated;
|
|
2874
3268
|
totalConversations += 1;
|
|
2875
3269
|
totalMessages += Number.isFinite(messageCount) ? messageCount : 0;
|
|
@@ -2897,7 +3291,7 @@ function statsPayloadForSessions(sessions, options = {}) {
|
|
|
2897
3291
|
dayBucket.models[modelGroup].conversations += 1;
|
|
2898
3292
|
dayBucket.models[modelGroup].user_messages += userMessageCount;
|
|
2899
3293
|
|
|
2900
|
-
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 });
|
|
2901
3295
|
const activity = dailyActivity.get(dayKey);
|
|
2902
3296
|
activity.sessions += 1;
|
|
2903
3297
|
activity.conversations += 1;
|
|
@@ -2909,7 +3303,7 @@ function statsPayloadForSessions(sessions, options = {}) {
|
|
|
2909
3303
|
if (!isWebChatStatsProvider(provider)) {
|
|
2910
3304
|
const repoKey = sessionRepoKey(session);
|
|
2911
3305
|
const repoLabel = sessionRepoLabel(session);
|
|
2912
|
-
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 });
|
|
2913
3307
|
const repoBucket = repoMap.get(repoKey);
|
|
2914
3308
|
repoBucket.repo_display = repoLabel;
|
|
2915
3309
|
if (!repoBucket.providers[provider]) repoBucket.providers[provider] = statsBucket();
|
|
@@ -2931,7 +3325,7 @@ function statsPayloadForSessions(sessions, options = {}) {
|
|
|
2931
3325
|
if (dayKey) {
|
|
2932
3326
|
if (!dailyRepoMap.has(dayKey)) dailyRepoMap.set(dayKey, new Map());
|
|
2933
3327
|
const dailyRepos = dailyRepoMap.get(dayKey);
|
|
2934
|
-
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 });
|
|
2935
3329
|
const dailyRepoBucket = dailyRepos.get(repoKey);
|
|
2936
3330
|
dailyRepoBucket.repo_display = repoLabel;
|
|
2937
3331
|
if (!dailyRepoBucket.providers[provider]) dailyRepoBucket.providers[provider] = statsBucket();
|
|
@@ -3012,6 +3406,7 @@ function statsPayloadForSessions(sessions, options = {}) {
|
|
|
3012
3406
|
tokens_input: entry.tokens_input,
|
|
3013
3407
|
tokens_output: entry.tokens_output,
|
|
3014
3408
|
tokens_cache: entry.tokens_cache,
|
|
3409
|
+
tokens_reasoning: entry.tokens_reasoning,
|
|
3015
3410
|
tokens_estimated: entry.tokens_estimated,
|
|
3016
3411
|
conversations: entry.conversations,
|
|
3017
3412
|
user_messages: entry.user_messages,
|
|
@@ -3034,6 +3429,7 @@ function statsPayloadForSessions(sessions, options = {}) {
|
|
|
3034
3429
|
tokens_input: entry.tokens_input,
|
|
3035
3430
|
tokens_output: entry.tokens_output,
|
|
3036
3431
|
tokens_cache: entry.tokens_cache,
|
|
3432
|
+
tokens_reasoning: entry.tokens_reasoning,
|
|
3037
3433
|
tokens_estimated: entry.tokens_estimated,
|
|
3038
3434
|
conversations: entry.conversations,
|
|
3039
3435
|
user_messages: entry.user_messages,
|
|
@@ -3080,7 +3476,7 @@ function statsPayloadForSessions(sessions, options = {}) {
|
|
|
3080
3476
|
const favoriteProviderEntry = byProvider[0] || null;
|
|
3081
3477
|
const dailyActivityList = activitySorted.map((day) => {
|
|
3082
3478
|
const entry = dailyActivity.get(day);
|
|
3083
|
-
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 };
|
|
3084
3480
|
});
|
|
3085
3481
|
const monthAgg = new Map();
|
|
3086
3482
|
let peakDayTokens = 0;
|
|
@@ -3114,21 +3510,33 @@ function statsPayloadForSessions(sessions, options = {}) {
|
|
|
3114
3510
|
const avgMessagesPerConversation =
|
|
3115
3511
|
totalConversations > 0 ? totalMessages / totalConversations : 0;
|
|
3116
3512
|
const splitStats = options.includeSplit === false ? null : {
|
|
3117
|
-
agent: statsPayloadForSessions(
|
|
3118
|
-
chat: statsPayloadForSessions(
|
|
3513
|
+
agent: statsPayloadForSessions(allSessions.filter((session) => !isSdkStatsSession(session) && statsSessionCategory(session) === "agent"), { includeSplit: false, category: "agent" }),
|
|
3514
|
+
chat: statsPayloadForSessions(allSessions.filter((session) => !isSdkStatsSession(session) && statsSessionCategory(session) === "chat"), { includeSplit: false, category: "chat" }),
|
|
3515
|
+
sdk: statsPayloadForSessions(sdkSessions, { includeSplit: false, includeSdk: true, category: "sdk" })
|
|
3119
3516
|
};
|
|
3517
|
+
const sdkStats = splitStats?.sdk || null;
|
|
3120
3518
|
return {
|
|
3121
3519
|
category: options.category || "all",
|
|
3122
3520
|
generated_at: new Date().toISOString(),
|
|
3123
3521
|
session_count: totalConversations,
|
|
3124
3522
|
agent_session_count: splitStats ? splitStats.agent.session_count : undefined,
|
|
3125
3523
|
chat_session_count: splitStats ? splitStats.chat.session_count : undefined,
|
|
3524
|
+
sdk_session_count: sdkStats ? sdkStats.session_count : undefined,
|
|
3525
|
+
sdk_message_count: sdkStats ? sdkStats.message_count : undefined,
|
|
3526
|
+
sdk_user_message_count: sdkStats ? sdkStats.user_message_count : undefined,
|
|
3527
|
+
sdk_total_tokens: sdkStats ? sdkStats.total_tokens : undefined,
|
|
3528
|
+
sdk_total_input_tokens: sdkStats ? sdkStats.total_input_tokens : undefined,
|
|
3529
|
+
sdk_total_output_tokens: sdkStats ? sdkStats.total_output_tokens : undefined,
|
|
3530
|
+
sdk_total_cache_tokens: sdkStats ? sdkStats.total_cache_tokens : undefined,
|
|
3531
|
+
sdk_total_reasoning_tokens: sdkStats ? sdkStats.total_reasoning_tokens : undefined,
|
|
3532
|
+
sdk_total_estimated_tokens: sdkStats ? sdkStats.total_estimated_tokens : undefined,
|
|
3126
3533
|
message_count: totalMessages,
|
|
3127
3534
|
user_message_count: totalUserMessages,
|
|
3128
3535
|
total_tokens: totalTokens,
|
|
3129
3536
|
total_input_tokens: totalInputTokens,
|
|
3130
3537
|
total_output_tokens: totalOutputTokens,
|
|
3131
3538
|
total_cache_tokens: totalCacheTokens,
|
|
3539
|
+
total_reasoning_tokens: totalReasoningTokens,
|
|
3132
3540
|
total_estimated_tokens: totalEstimatedTokens,
|
|
3133
3541
|
active_days: activeDays,
|
|
3134
3542
|
current_streak: currentStreak,
|
|
@@ -3221,6 +3629,20 @@ function statsSessionCategory(session) {
|
|
|
3221
3629
|
return provider === "chatgpt" || provider === "claude_web" ? "chat" : "agent";
|
|
3222
3630
|
}
|
|
3223
3631
|
|
|
3632
|
+
const SDK_STATS_SOURCE_TYPES = new Set(["codex-sdk-history", "claude-sdk-history"]);
|
|
3633
|
+
|
|
3634
|
+
function isSdkStatsSession(session) {
|
|
3635
|
+
const provider = String(session?.provider || "").toLowerCase();
|
|
3636
|
+
const sourceType = String(session?.sourceType || session?.source_type || "").toLowerCase();
|
|
3637
|
+
return provider === "claude_sdk" || SDK_STATS_SOURCE_TYPES.has(sourceType);
|
|
3638
|
+
}
|
|
3639
|
+
|
|
3640
|
+
function statsFiltersTargetSdk(filters = {}) {
|
|
3641
|
+
const provider = String(filters.provider || filters.source || "").trim().toLowerCase().replace(/[-\s]+/g, "_");
|
|
3642
|
+
const sourceType = String(filters.sourceType || filters.source_type || "").trim().toLowerCase();
|
|
3643
|
+
return provider === "codex_sdk" || provider === "claude_sdk" || SDK_STATS_SOURCE_TYPES.has(sourceType);
|
|
3644
|
+
}
|
|
3645
|
+
|
|
3224
3646
|
function isWebChatStatsProvider(provider) {
|
|
3225
3647
|
return provider === "chatgpt" || provider === "claude_web";
|
|
3226
3648
|
}
|
|
@@ -3234,7 +3656,7 @@ function statsUsageForSession(session) {
|
|
|
3234
3656
|
}
|
|
3235
3657
|
|
|
3236
3658
|
function statsBucket() {
|
|
3237
|
-
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 };
|
|
3238
3660
|
}
|
|
3239
3661
|
|
|
3240
3662
|
function statsUserMessageCount(session) {
|
|
@@ -3248,6 +3670,7 @@ function addStatsTokens(bucket, usageTokens) {
|
|
|
3248
3670
|
bucket.tokens_input += usageTokens.tokens_input;
|
|
3249
3671
|
bucket.tokens_output += usageTokens.tokens_output;
|
|
3250
3672
|
bucket.tokens_cache += usageTokens.tokens_cache;
|
|
3673
|
+
bucket.tokens_reasoning += usageTokens.tokens_reasoning;
|
|
3251
3674
|
bucket.tokens_estimated += usageTokens.tokens_estimated;
|
|
3252
3675
|
}
|
|
3253
3676
|
|
|
@@ -3255,15 +3678,32 @@ function statsTokenBreakdown(usage) {
|
|
|
3255
3678
|
let input = positiveStatsNumber(usage?.inputTokens);
|
|
3256
3679
|
let output = positiveStatsNumber(usage?.outputTokens);
|
|
3257
3680
|
const cache = positiveStatsNumber(usage?.cacheInputTokens);
|
|
3258
|
-
const
|
|
3259
|
-
|
|
3260
|
-
|
|
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);
|
|
3261
3700
|
return {
|
|
3262
|
-
tokens
|
|
3701
|
+
tokens,
|
|
3263
3702
|
tokens_input: input,
|
|
3264
3703
|
tokens_output: output,
|
|
3265
3704
|
tokens_cache: cache,
|
|
3266
|
-
|
|
3705
|
+
tokens_reasoning: reasoning,
|
|
3706
|
+
tokens_estimated: usage?.estimated ? tokens : 0
|
|
3267
3707
|
};
|
|
3268
3708
|
}
|
|
3269
3709
|
|
|
@@ -3443,6 +3883,90 @@ function writeNotModifiedSessionResponse(res, etag) {
|
|
|
3443
3883
|
res.end();
|
|
3444
3884
|
}
|
|
3445
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
|
+
|
|
3446
3970
|
function writeJsonResponse(res, payload, status = 200, options = {}) {
|
|
3447
3971
|
res.writeHead(status, {
|
|
3448
3972
|
...securityHeaders("application/json; charset=utf-8"),
|
|
@@ -3451,6 +3975,469 @@ function writeJsonResponse(res, payload, status = 200, options = {}) {
|
|
|
3451
3975
|
res.end(options.pretty ? JSON.stringify(payload, null, 2) : JSON.stringify(payload));
|
|
3452
3976
|
}
|
|
3453
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
|
+
|
|
3454
4441
|
function writeUnauthorizedHistoryResponse(res, pathname) {
|
|
3455
4442
|
if (isHistoryApiPath(pathname)) {
|
|
3456
4443
|
writeJsonResponse(res, { error: "unauthorized", code: "unauthorized" }, 401);
|
|
@@ -3513,7 +4500,7 @@ function serverCommand(flags, env) {
|
|
|
3513
4500
|
}
|
|
3514
4501
|
|
|
3515
4502
|
const RECALL_TARGET_SOURCE_MAP = {
|
|
3516
|
-
codex: ["codex-cli", "codex-desktop"],
|
|
4503
|
+
codex: ["codex-cli", "codex-desktop", "codex-sdk"],
|
|
3517
4504
|
claude: ["claude", "claude-code-desktop", "claude-workspace"],
|
|
3518
4505
|
gemini: ["gemini-cli"],
|
|
3519
4506
|
antigravity: ["antigravity"],
|
|
@@ -4037,7 +5024,7 @@ function recallArchiveHints() {
|
|
|
4037
5024
|
return `- Sessions live under \`~/.agentlog/data/agentlog/sessions/repo=<repo-or-path-key>/provider=<provider>/year=YYYY/month=MM/day=DD/session=<session_id>.conversation.md\`.
|
|
4038
5025
|
- Git repositories use canonical keys like \`github.com/org/repo\`. Non-git directories may use stable \`path:<hash>\` storage keys, but history results include \`repo_display\` and \`cwd\` with the readable local path.
|
|
4039
5026
|
- When the user names a repo or folder, add \`--repo "<repo-or-path>"\`; it matches canonical repo keys, \`path:<hash>\`, web scopes, local \`cwd\`, and display labels, so local paths and path fragments work.
|
|
4040
|
-
- Useful filters include \`--provider <provider>\`, \`--since 30d\`, and \`--repo "<repo-or-path>"\`. Provider aliases are ordered as OpenAI (\`codex-cli\`, \`codex-desktop\`, \`chatgpt\`), Anthropic (\`claude\`, \`claude-code-desktop\`, \`claude-workspace\`, \`claude-web\`, \`claude-sdk\`), Google (\`gemini-cli\`, \`antigravity\`), then other local tools (\`devin-cli\`, \`cursor\`, \`cline\`, \`opencode\`, \`aider\`).
|
|
5027
|
+
- Useful filters include \`--provider <provider>\`, \`--since 30d\`, and \`--repo "<repo-or-path>"\`. Provider aliases are ordered as OpenAI (\`codex-cli\`, \`codex-desktop\`, \`codex-sdk\`, \`chatgpt\`), Anthropic (\`claude\`, \`claude-code-desktop\`, \`claude-workspace\`, \`claude-web\`, \`claude-sdk\`), Google (\`gemini-cli\`, \`antigravity\`), then other local tools (\`devin-cli\`, \`cursor\`, \`cline\`, \`opencode\`, \`aider\`).
|
|
4041
5028
|
- If the user is asking about the current repository, start without \`--repo\` unless results are noisy; current-repo matches are already weighted higher.`;
|
|
4042
5029
|
}
|
|
4043
5030
|
|
|
@@ -4305,6 +5292,10 @@ function printDiscovery(label, result) {
|
|
|
4305
5292
|
|
|
4306
5293
|
function printImportResults(results, options = {}) {
|
|
4307
5294
|
for (const result of results) {
|
|
5295
|
+
if (result.instructions) {
|
|
5296
|
+
printWebExportInstructionBlock(result.instructions);
|
|
5297
|
+
continue;
|
|
5298
|
+
}
|
|
4308
5299
|
const detailText = result.details ? formatDetails(result.details) : "";
|
|
4309
5300
|
const details = detailText ? ` ${detailText}` : "";
|
|
4310
5301
|
printCheck(
|
|
@@ -4326,6 +5317,37 @@ function printImportResults(results, options = {}) {
|
|
|
4326
5317
|
}
|
|
4327
5318
|
}
|
|
4328
5319
|
|
|
5320
|
+
function printWebExportInstructions(instructions, flags = {}) {
|
|
5321
|
+
if (!instructions) throw new Error("unknown web export instruction source");
|
|
5322
|
+
if (flags.json) {
|
|
5323
|
+
console.log(JSON.stringify({
|
|
5324
|
+
provider: instructions.provider,
|
|
5325
|
+
source: instructions.source,
|
|
5326
|
+
manual: true,
|
|
5327
|
+
instructions
|
|
5328
|
+
}, null, 2));
|
|
5329
|
+
return;
|
|
5330
|
+
}
|
|
5331
|
+
printPageTitle("agentlog import", `${instructions.source} export instructions`);
|
|
5332
|
+
printWebExportInstructionBlock(instructions);
|
|
5333
|
+
}
|
|
5334
|
+
|
|
5335
|
+
function printWebExportInstructionBlock(instructions) {
|
|
5336
|
+
printSection(`${instructions.label} Export`);
|
|
5337
|
+
printMuted(`Request and download the ${instructions.fileDescription}, then import it from disk.`);
|
|
5338
|
+
printCheck("Request page", instructions.requestUrl);
|
|
5339
|
+
printCheck("Help", instructions.helpUrl);
|
|
5340
|
+
printSection("Steps");
|
|
5341
|
+
(instructions.steps || []).forEach((step, index) => {
|
|
5342
|
+
console.log(` ${index + 1}. ${step}`);
|
|
5343
|
+
});
|
|
5344
|
+
if (instructions.notes?.length) {
|
|
5345
|
+
printSection("Notes");
|
|
5346
|
+
for (const note of instructions.notes) console.log(` - ${note}`);
|
|
5347
|
+
}
|
|
5348
|
+
printCommand("Import after download", instructions.importCommand);
|
|
5349
|
+
}
|
|
5350
|
+
|
|
4329
5351
|
function printPageTitle(title, subtitle = "") {
|
|
4330
5352
|
if (!process.stdout.isTTY) {
|
|
4331
5353
|
console.log(title);
|
|
@@ -4790,19 +5812,29 @@ async function chooseDataRoot(flags, env) {
|
|
|
4790
5812
|
|
|
4791
5813
|
printSection("Archive Storage");
|
|
4792
5814
|
printMuted("Redacted transcripts, metadata, and indexes live in one data directory.");
|
|
4793
|
-
printMuted(
|
|
5815
|
+
printMuted("Enter a path relative to ~/, a ~/... path, or an absolute path.");
|
|
4794
5816
|
printMuted("Press Enter to use the suggested data directory.");
|
|
4795
|
-
const answer = (await ask(` Data directory [${
|
|
5817
|
+
const answer = (await ask(` Data directory [${compactPath(defaultRoot)}]: `)).trim();
|
|
4796
5818
|
return answer ? resolveUserPath(answer) : defaultRoot;
|
|
4797
5819
|
}
|
|
4798
5820
|
|
|
4799
|
-
function resolveUserPath(value) {
|
|
4800
|
-
const input =
|
|
5821
|
+
function resolveUserPath(value, options = {}) {
|
|
5822
|
+
const input = normalizeUserPathInput(value);
|
|
4801
5823
|
if (!input) return "";
|
|
4802
5824
|
if (input === "~") return os.homedir();
|
|
4803
5825
|
if (input.startsWith("~/")) return path.resolve(os.homedir(), input.slice(2));
|
|
4804
5826
|
if (path.isAbsolute(input)) return path.resolve(input);
|
|
4805
|
-
|
|
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");
|
|
4806
5838
|
}
|
|
4807
5839
|
|
|
4808
5840
|
async function chooseSetupSettings(flags) {
|
|
@@ -5089,6 +6121,14 @@ function importSourceOptions(discovered) {
|
|
|
5089
6121
|
description: "Codex desktop app conversations from the local Codex state database.",
|
|
5090
6122
|
defaultSelected: Boolean(discovered.codexDesktop?.sessions)
|
|
5091
6123
|
},
|
|
6124
|
+
{
|
|
6125
|
+
source: "codex-sdk",
|
|
6126
|
+
label: "Codex SDK jobs",
|
|
6127
|
+
count: discovered.codexSdk?.sessions || 0,
|
|
6128
|
+
summary: sourceSummary(discovered.codexSdk),
|
|
6129
|
+
description: "Codex exec and SDK-style batch runs from the local Codex state database; disabled by default because volume can be high.",
|
|
6130
|
+
defaultSelected: false
|
|
6131
|
+
},
|
|
5092
6132
|
{
|
|
5093
6133
|
source: "claude",
|
|
5094
6134
|
label: "Claude Code CLI",
|
|
@@ -5249,10 +6289,10 @@ function createProgressReporter(options = {}) {
|
|
|
5249
6289
|
const file = event.path ? ` ${path.basename(event.path).slice(0, 36)}` : "";
|
|
5250
6290
|
const eventMode = event.kind || mode;
|
|
5251
6291
|
const label = String(event.provider || "work").padEnd(eventMode === "discovery" ? 16 : 12);
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
5255
|
-
|
|
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}`);
|
|
5256
6296
|
const count = hasTotal ? `${current}/${total}` : `${current}`;
|
|
5257
6297
|
const line = `${label} [${bar}] ${percent}% ${count} ${detail}${file}`;
|
|
5258
6298
|
process.stdout.write(`\r${line.padEnd(lastLine.length)}`);
|
|
@@ -5514,6 +6554,9 @@ details[open] > summary .folder-icon-open{display:block}
|
|
|
5514
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}
|
|
5515
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}
|
|
5516
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}
|
|
5517
6560
|
.session-stat.model{max-width:min(400px,46vw);gap:5px}
|
|
5518
6561
|
.session-model-brand{display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto}
|
|
5519
6562
|
.session-stat.model .session-model-text{display:inline-block;max-width:100%;overflow:hidden;text-overflow:ellipsis;vertical-align:bottom}
|
|
@@ -5600,17 +6643,66 @@ mark.search-match.search-match-current{background:#fde047;color:#422006;box-shad
|
|
|
5600
6643
|
.bubble p{margin:0 0 8px;overflow-wrap:anywhere}
|
|
5601
6644
|
.bubble p:last-child{margin-bottom:0}
|
|
5602
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}
|
|
5603
6658
|
.bubble a{color:#2457a6;text-decoration:underline;text-underline-offset:2px}
|
|
6659
|
+
.bubble a.attachment-card{color:#334155;text-decoration:none}
|
|
5604
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}
|
|
5605
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}
|
|
5606
6663
|
.unsupported-device-block:first-child{margin-top:0}
|
|
5607
6664
|
.unsupported-device-block:last-child{margin-bottom:0}
|
|
5608
6665
|
.unsupported-device-block:before{content:"";width:6px;height:6px;border-radius:999px;background:#94a3b8;flex:0 0 auto}
|
|
5609
6666
|
.context-card{margin:4px 0;border:1px solid #e5e7eb;border-left:3px solid #cbd5e1;border-radius:8px;background:#fff;color:#475569;overflow:hidden;transition:border-color .15s ease}
|
|
6667
|
+
.context-line{display:flex;align-items:center;gap:8px;min-height:28px;margin:2px 0;color:#64748b;font-size:12px;line-height:1.35}
|
|
6668
|
+
.context-line .context-glyph{width:22px;height:22px}
|
|
6669
|
+
.context-line .context-title{flex:0 0 auto}
|
|
6670
|
+
.context-line .context-meta{flex:1 1 auto}
|
|
6671
|
+
.context-line .context-end{margin-left:auto}
|
|
6672
|
+
.context-line .message-copy{opacity:1;width:22px;height:22px;color:#94a3b8}
|
|
5610
6673
|
.session-summary-message{margin-bottom:14px}
|
|
5611
6674
|
.session-summary-card{border-left-color:#D97757;background:#fff}
|
|
5612
6675
|
.session-summary-body{padding:11px 13px 12px;color:#334155;font-size:13px;line-height:1.55;background:#fff}
|
|
5613
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}
|
|
5614
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}
|
|
5615
6707
|
.context-prefix{display:inline-flex;align-items:center;gap:6px}
|
|
5616
6708
|
.context-end{display:inline-flex;align-items:center;gap:6px}
|
|
@@ -5648,13 +6740,31 @@ mark.search-match.search-match-current{background:#fde047;color:#422006;box-shad
|
|
|
5648
6740
|
.md-table{border-collapse:collapse;width:max-content;max-width:100%;font-size:13px}
|
|
5649
6741
|
.md-table th,.md-table td{border:1px solid #e5e7eb;padding:5px 7px;text-align:left;vertical-align:top}
|
|
5650
6742
|
.md-table th{background:#f8fafc;font-weight:600}
|
|
5651
|
-
.tool-
|
|
5652
|
-
.tool-
|
|
5653
|
-
.tool-
|
|
6743
|
+
.tool-group-card{margin:0;border:0;background:transparent}
|
|
6744
|
+
.tool-group-card > summary{display:flex;align-items:center;gap:8px;min-height:28px;margin:0 0 5px;color:#64748b;font-size:13px;font-weight:600;line-height:1.35;cursor:pointer;list-style:none}
|
|
6745
|
+
.tool-group-card > summary::-webkit-details-marker{display:none}
|
|
6746
|
+
.tool-group-prefix{display:inline-flex;align-items:center;gap:7px;flex:0 0 auto}
|
|
6747
|
+
.tool-group-caret{width:0;height:0;border-top:4px solid transparent;border-bottom:4px solid transparent;border-left:5px solid #94a3b8;transition:transform .15s ease}
|
|
6748
|
+
.tool-group-card[open] .tool-group-caret{transform:rotate(90deg)}
|
|
6749
|
+
.tool-group-title{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
6750
|
+
.tool-group-card[open] > .tool-stack{gap:0;border:1px solid #e5e7eb;border-radius:8px;background:#fff;overflow:hidden}
|
|
6751
|
+
.tool-group-card[open] > .tool-stack > .tool-callout{border:0;border-radius:0;border-bottom:1px solid #eef2f7}
|
|
6752
|
+
.tool-group-card[open] > .tool-stack > .tool-callout:last-child{border-bottom:0}
|
|
6753
|
+
.tool-group-card[open] > .tool-stack > .tool-callout > summary{min-height:26px;padding:2px 8px}
|
|
6754
|
+
.tool-callout{display:block;margin:0;border:1px solid #e5e7eb;border-radius:8px;background:#fff;color:#0f172a;overflow:hidden;transition:border-color .15s ease,background .15s ease}
|
|
6755
|
+
.tool-callout > summary{display:flex;align-items:center;gap:7px;min-height:28px;padding:3px 8px;cursor:pointer;list-style:none;transition:background .12s ease}
|
|
6756
|
+
.tool-callout > summary::-webkit-details-marker{display:none}
|
|
6757
|
+
.tool-callout > summary:before{content:"";flex:0 0 auto;width:0;height:0;border-top:4px solid transparent;border-bottom:4px solid transparent;border-left:5px solid #94a3b8;transition:transform .15s ease}
|
|
6758
|
+
.tool-callout[open] > summary:before{transform:rotate(90deg)}
|
|
6759
|
+
.tool-callout:hover,.tool-callout[open]{border-color:#dbe3ef}
|
|
6760
|
+
.tool-callout > summary:hover{background:#fafbfc}
|
|
6761
|
+
.tool-callout[open] > summary{border-bottom:1px solid #f1f5f9}
|
|
6762
|
+
.tool-stack{display:grid;gap:3px;margin-top:3px}
|
|
5654
6763
|
.tool-stack:first-child{margin-top:0}
|
|
5655
6764
|
.tool-body + .tool-stack{margin-top:8px}
|
|
5656
|
-
.tool-
|
|
5657
|
-
.tool-glyph
|
|
6765
|
+
.tool-stack-heading{display:flex;align-items:center;min-height:24px;margin:0 0 1px;color:#64748b;font-size:12px;font-weight:600;line-height:1.35}
|
|
6766
|
+
.tool-glyph{display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;width:22px;height:22px;border-radius:6px;background:#f1f5f9;color:#475569;border:0}
|
|
6767
|
+
.tool-glyph svg{width:13px;height:13px}
|
|
5658
6768
|
.tool-callout.shell .tool-glyph{background:#f1f5f9;color:#334155}
|
|
5659
6769
|
.tool-callout.edit .tool-glyph{background:#fef3c7;color:#92400e}
|
|
5660
6770
|
.tool-callout.read .tool-glyph{background:#e0f2fe;color:#075985}
|
|
@@ -5663,16 +6773,21 @@ mark.search-match.search-match-current{background:#fde047;color:#422006;box-shad
|
|
|
5663
6773
|
.tool-callout.task .tool-glyph{background:#dcfce7;color:#166534}
|
|
5664
6774
|
.tool-callout.mcp .tool-glyph{background:#ffedd5;color:#9a3412}
|
|
5665
6775
|
.tool-callout.skill .tool-glyph{background:#e0e7ff;color:#3730a3}
|
|
5666
|
-
.tool-
|
|
5667
|
-
.tool-
|
|
6776
|
+
.tool-call-line{display:flex;align-items:baseline;gap:5px;min-width:0;flex:1 1 auto}
|
|
6777
|
+
.tool-action{flex:0 0 auto;color:#0f172a;font-size:12.5px;font-weight:600;line-height:1.3}
|
|
6778
|
+
.tool-subject{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#475569;font-size:12.5px;font-weight:500;line-height:1.3}
|
|
6779
|
+
.tool-callout.shell .tool-subject{font:12px/1.3 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;color:#475569}
|
|
6780
|
+
.tool-callout-body{display:grid;gap:6px;min-width:0;padding:6px 8px 8px 12px;background:#fff}
|
|
6781
|
+
.tool-call-meta{display:flex;align-items:center;gap:5px;flex-wrap:wrap;min-width:0}
|
|
5668
6782
|
.tool-chip{display:inline-flex;align-items:center;height:18px;padding:0 7px;border-radius:999px;background:#f1f5f9;color:#475569;border:0;font-size:11px;font-weight:500;letter-spacing:.01em}
|
|
5669
6783
|
.tool-status{display:inline-flex;align-items:center;height:18px;border-radius:999px;padding:0 7px;background:#f1f5f9;color:#64748b;border:0;font-size:11px;font-weight:600}
|
|
5670
6784
|
.tool-status.completed{background:#dcfce7;color:#166534}
|
|
5671
6785
|
.tool-status.pending{background:#fef9c3;color:#854d0e}
|
|
5672
6786
|
.tool-status.failed,.tool-status.error{background:#fee2e2;color:#991b1b}
|
|
5673
|
-
.tool-
|
|
5674
|
-
.tool-
|
|
5675
|
-
.tool-
|
|
6787
|
+
.tool-preview{display:block;margin:0;padding:6px 7px;border:1px solid #edf2f7;border-radius:7px;background:#fafbfc;color:#1e293b;overflow-x:hidden;overflow-y:visible;font:12px/1.4 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word}
|
|
6788
|
+
.tool-paired-result{display:grid;gap:4px;min-width:0}
|
|
6789
|
+
.tool-result-meta{display:flex;align-items:center;gap:8px;min-width:0;color:#64748b;font-size:11px;font-weight:600}
|
|
6790
|
+
.tool-result-meta span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
5676
6791
|
.tool-diff{display:block;margin-top:6px;border:1px solid #e5e7eb;border-radius:6px;background:#fff;overflow:hidden;font:12px/1.55 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
|
|
5677
6792
|
.tool-diff[open] .tool-diff-summary{border-bottom:1px solid #f1f5f9}
|
|
5678
6793
|
.tool-diff-summary{display:flex;align-items:center;gap:8px;min-height:28px;padding:4px 10px;cursor:pointer;color:#475569;font-size:11px;font-weight:600;list-style:none;background:#fafbfc}
|
|
@@ -5681,24 +6796,28 @@ mark.search-match.search-match-current{background:#fde047;color:#422006;box-shad
|
|
|
5681
6796
|
.tool-diff[open] .tool-diff-summary:before{transform:rotate(90deg)}
|
|
5682
6797
|
.tool-diff-summary .add-count{color:#16a34a}
|
|
5683
6798
|
.tool-diff-summary .del-count{color:#dc2626}
|
|
5684
|
-
.tool-diff-body{display:block;
|
|
6799
|
+
.tool-diff-body{display:block;overflow-x:hidden;overflow-y:visible;background:#f8fafc}
|
|
5685
6800
|
.tool-diff-block{display:block}
|
|
5686
6801
|
.tool-diff-block + .tool-diff-block{border-top:1px solid #e5e7eb;margin-top:4px;padding-top:4px}
|
|
5687
|
-
.tool-diff-line{display:block;padding:0 10px;white-space:pre-wrap;overflow-wrap:anywhere;color:#0f172a}
|
|
6802
|
+
.tool-diff-line{display:block;padding:0 10px;white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word;color:#0f172a}
|
|
5688
6803
|
.tool-diff-line.add{background:#e6ffec;color:#1a7f37}
|
|
5689
6804
|
.tool-diff-line.del{background:#ffebe9;color:#a40e26}
|
|
5690
6805
|
.tool-diff-line.ctx{color:#6b7280}
|
|
5691
6806
|
.tool-diff-line.meta{color:#6b7280;font-weight:600;background:#f1f5f9}
|
|
5692
6807
|
.tool-diff-line.hunk{color:#7c3aed;background:#faf5ff;font-weight:600}
|
|
5693
6808
|
.tool-result{margin:0;border:1px solid #e5e7eb;border-radius:8px;background:#fff;overflow:hidden}
|
|
6809
|
+
.tool-result.inline{margin-top:1px;border-radius:7px;background:#f8fafc}
|
|
5694
6810
|
.tool-result summary{display:flex;align-items:center;gap:8px;min-height:34px;padding:6px 10px 6px 11px;cursor:pointer;color:#0f172a;font-weight:600;list-style:none;transition:background .12s ease}
|
|
6811
|
+
.tool-result.inline summary{min-height:30px;padding:5px 9px;background:#fafbfc}
|
|
5695
6812
|
.tool-result summary:hover{background:#fafbfc}
|
|
6813
|
+
.tool-result.inline summary:hover{background:#f1f5f9}
|
|
5696
6814
|
.tool-result[open] summary{border-bottom:1px solid #f1f5f9}
|
|
5697
6815
|
.tool-result summary::-webkit-details-marker{display:none}
|
|
5698
6816
|
.tool-result summary:before{content:"";flex:0 0 auto;width:0;height:0;border-top:4px solid transparent;border-bottom:4px solid transparent;border-left:5px solid #94a3b8;transition:transform .15s ease}
|
|
5699
6817
|
.tool-result[open] summary:before{transform:rotate(90deg)}
|
|
5700
6818
|
.tool-result-kind{display:inline-flex;align-items:center;gap:7px;font-size:13px;font-weight:600;color:#0f172a;letter-spacing:-0.005em;flex:0 0 auto;white-space:nowrap}
|
|
5701
|
-
.tool-result-kind .tool-glyph{width:
|
|
6819
|
+
.tool-result-kind .tool-glyph{width:20px;height:20px;border-radius:6px}
|
|
6820
|
+
.tool-result.inline .tool-result-kind .tool-glyph{width:20px;height:20px}
|
|
5702
6821
|
.tool-result-kind .tool-glyph svg{width:12px;height:12px}
|
|
5703
6822
|
.tool-result[data-category="shell"] .tool-result-kind .tool-glyph{background:#f1f5f9;color:#334155}
|
|
5704
6823
|
.tool-result[data-category="edit"] .tool-result-kind .tool-glyph{background:#fef3c7;color:#92400e}
|
|
@@ -5709,7 +6828,12 @@ mark.search-match.search-match-current{background:#fde047;color:#422006;box-shad
|
|
|
5709
6828
|
.tool-result[data-category="mcp"] .tool-result-kind .tool-glyph{background:#ffedd5;color:#9a3412}
|
|
5710
6829
|
.tool-result-detail{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#64748b;font-size:12px;font-weight:500}
|
|
5711
6830
|
.tool-result-count{margin-left:auto;flex:0 0 auto;color:#94a3b8;font-size:11px;font-weight:600;letter-spacing:.01em;white-space:nowrap}
|
|
5712
|
-
.tool-output{max-width:100%;
|
|
6831
|
+
.tool-output{max-width:100%;margin:0;padding:6px 7px;background:#f8fafc;color:#0f172a;overflow-x:hidden;overflow-y:visible;font:12px/1.45 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word}
|
|
6832
|
+
.tool-result.inline .tool-output{background:#fff}
|
|
6833
|
+
.tool-output-lines{display:grid;gap:0;padding:5px 0}
|
|
6834
|
+
.tool-output-line{display:grid;grid-template-columns:minmax(2.4em,max-content) minmax(0,1fr);gap:10px;min-width:0;padding:0 7px}
|
|
6835
|
+
.tool-line-number{color:#94a3b8;text-align:right;user-select:none}
|
|
6836
|
+
.tool-line-text{min-width:0;white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word}
|
|
5713
6837
|
.skill-link{display:inline-flex;align-items:center;gap:4px;max-width:100%;vertical-align:middle;border:1px solid #c7d2fe;border-radius:999px;background:#eef2ff;color:#1e1b4b;padding:1px 7px 2px;font-size:.93em;line-height:1.35;white-space:nowrap}
|
|
5714
6838
|
.skill-mark{font:700 11px/1 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;color:#4338ca}
|
|
5715
6839
|
.skill-name,.skill-path{min-width:0;overflow:hidden;text-overflow:ellipsis}
|
|
@@ -5750,6 +6874,7 @@ mark.search-match.search-match-current{background:#fde047;color:#422006;box-shad
|
|
|
5750
6874
|
<button class="select-option active" type="button" data-value="">All sources</button>
|
|
5751
6875
|
<button class="select-option" type="button" data-value="codex-cli">Codex CLI</button>
|
|
5752
6876
|
<button class="select-option" type="button" data-value="codex-desktop">Codex Desktop</button>
|
|
6877
|
+
<button class="select-option" type="button" data-value="codex-sdk">Codex SDK jobs</button>
|
|
5753
6878
|
<button class="select-option" type="button" data-value="chatgpt">ChatGPT</button>
|
|
5754
6879
|
<button class="select-option" type="button" data-value="claude">Claude Code CLI</button>
|
|
5755
6880
|
<button class="select-option" type="button" data-value="claude-code-desktop">Claude Code Desktop</button>
|
|
@@ -5850,6 +6975,18 @@ mark.search-match.search-match-current{background:#fde047;color:#422006;box-shad
|
|
|
5850
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>
|
|
5851
6976
|
</section>
|
|
5852
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>
|
|
5853
6990
|
<section id="statsPane" class="stats-pane" aria-label="Stats">
|
|
5854
6991
|
<div class="stats-metrics" id="statsMetrics"></div>
|
|
5855
6992
|
<div class="stats-section">
|
|
@@ -5879,6 +7016,11 @@ mark.search-match.search-match-current{background:#fde047;color:#422006;box-shad
|
|
|
5879
7016
|
<div id="statsChatActivitySub" class="stats-card-meta" hidden></div>
|
|
5880
7017
|
<div id="statsChatHeatmap" class="stats-heatmap-wrap"></div>
|
|
5881
7018
|
</div>
|
|
7019
|
+
<div class="stats-card stats-card--compact">
|
|
7020
|
+
<div class="stats-card-title">SDK jobs</div>
|
|
7021
|
+
<div id="statsSdkActivitySub" class="stats-card-meta" hidden></div>
|
|
7022
|
+
<div id="statsSdkHeatmap" class="stats-heatmap-wrap"></div>
|
|
7023
|
+
</div>
|
|
5882
7024
|
</div>
|
|
5883
7025
|
<div class="stats-section">
|
|
5884
7026
|
<div class="stats-section-head stats-section-head--breakdown">
|
|
@@ -5940,6 +7082,11 @@ const copyDetailsButton = document.getElementById('copyDetailsButton');
|
|
|
5940
7082
|
const copyResumeButton = document.getElementById('copyResumeButton');
|
|
5941
7083
|
const copyDetailsWrap = document.getElementById('copyDetailsWrap');
|
|
5942
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');
|
|
5943
7090
|
const form = document.getElementById('filters');
|
|
5944
7091
|
const queryInput = document.getElementById('q');
|
|
5945
7092
|
|
|
@@ -5957,6 +7104,7 @@ const repoOffsets = new Map();
|
|
|
5957
7104
|
let currentSessionId = '';
|
|
5958
7105
|
let currentSessionPayload = null;
|
|
5959
7106
|
let viewMode = 'readable';
|
|
7107
|
+
let viewScrollRestoreSerial = 0;
|
|
5960
7108
|
let sidebarWidth = 390;
|
|
5961
7109
|
let activeSearchTerm = '';
|
|
5962
7110
|
let pendingSessionId = '';
|
|
@@ -6033,7 +7181,7 @@ function brandIconSvg(kind, className) {
|
|
|
6033
7181
|
|
|
6034
7182
|
function brandKeyForSourceValue(value) {
|
|
6035
7183
|
const key = String(value || '').trim().toLowerCase();
|
|
6036
|
-
if (['codex-cli', 'codex-desktop', 'chatgpt'].includes(key)) return 'openai';
|
|
7184
|
+
if (['codex-cli', 'codex-desktop', 'codex-sdk', 'chatgpt'].includes(key)) return 'openai';
|
|
6037
7185
|
if (['claude', 'claude-code-desktop', 'claude-workspace', 'claude-web', 'claude-sdk'].includes(key)) return 'claude';
|
|
6038
7186
|
if (['gemini-cli', 'antigravity'].includes(key)) return 'gemini';
|
|
6039
7187
|
if (key === 'devin-cli') return 'devin';
|
|
@@ -6360,8 +7508,8 @@ function companyColor(company) {
|
|
|
6360
7508
|
}
|
|
6361
7509
|
|
|
6362
7510
|
const MODEL_COLOR_PALETTES = {
|
|
6363
|
-
openai: { family: 'openai', light: '#
|
|
6364
|
-
claude: { family: 'claude', light: '#
|
|
7511
|
+
openai: { family: 'openai', light: '#BAE6FD', base: '#06B6D4', dark: PROVIDER_COLORS.codex, darker: '#1D4ED8', top: '#172554' },
|
|
7512
|
+
claude: { family: 'claude', light: '#FED7AA', base: '#FB923C', dark: PROVIDER_COLORS.claude_code, darker: '#9A3412', top: '#831843' },
|
|
6365
7513
|
gemini: { family: 'gemini', light: '#C4B5FD', base: PROVIDER_COLORS.gemini_cli, dark: '#7E22CE', darker: '#581C87' },
|
|
6366
7514
|
devin: { family: 'devin', light: '#5EEAD4', base: PROVIDER_COLORS.devin, dark: '#0F766E', darker: '#115E59' },
|
|
6367
7515
|
cursor: { family: 'cursor', light: '#475569', base: PROVIDER_COLORS.cursor, dark: '#020617', darker: '#020617' },
|
|
@@ -6409,6 +7557,9 @@ function modelCapabilityColor(text, palette) {
|
|
|
6409
7557
|
}
|
|
6410
7558
|
|
|
6411
7559
|
function openAiModelColor(text, palette) {
|
|
7560
|
+
if (text.includes('gpt-5.5') || text.includes('gpt-5-5')) return palette.top;
|
|
7561
|
+
if (text.includes('gpt-5.4') || text.includes('gpt-5-4')) return palette.darker;
|
|
7562
|
+
if (text.includes('gpt-5.3') || text.includes('gpt-5-3')) return palette.dark;
|
|
6412
7563
|
if (text.includes('gpt-5.1') || text.includes('gpt-5-1')) return palette.top;
|
|
6413
7564
|
if (text.includes('gpt-5') || modelHasToken(text, ['o3'])) return palette.darker;
|
|
6414
7565
|
if (text.includes('gpt-4.1') || modelHasToken(text, ['o4'])) return palette.dark;
|
|
@@ -6417,10 +7568,12 @@ function openAiModelColor(text, palette) {
|
|
|
6417
7568
|
}
|
|
6418
7569
|
|
|
6419
7570
|
function claudeModelColor(text, palette) {
|
|
6420
|
-
if (text.includes('high-thinking') || text.includes('high_thinking')) return palette.top;
|
|
6421
|
-
if (modelHasToken(text, ['opus'])) return palette.darker;
|
|
6422
|
-
if (modelHasToken(text, ['sonnet'])) return palette.dark;
|
|
6423
7571
|
if (modelHasToken(text, ['haiku'])) return palette.light;
|
|
7572
|
+
if (text.includes('claude-4.6') && modelHasToken(text, ['opus'])) return palette.top;
|
|
7573
|
+
if (text.includes('claude-4.5') && modelHasToken(text, ['opus'])) return palette.darker;
|
|
7574
|
+
if (text.includes('high-thinking') || text.includes('high_thinking')) return palette.darker;
|
|
7575
|
+
if (modelHasToken(text, ['opus'])) return palette.dark;
|
|
7576
|
+
if (modelHasToken(text, ['sonnet'])) return palette.base;
|
|
6424
7577
|
return palette.base;
|
|
6425
7578
|
}
|
|
6426
7579
|
|
|
@@ -6429,6 +7582,91 @@ function modelHasToken(text, tokens) {
|
|
|
6429
7582
|
return new RegExp('(^|[^a-z0-9])(' + escaped + ')([^a-z0-9]|$)').test(text);
|
|
6430
7583
|
}
|
|
6431
7584
|
|
|
7585
|
+
const CANONICAL_COMPANY_ORDER = ['openai', 'anthropic', 'google', 'cognition', 'cursor', 'stealth', 'unknown'];
|
|
7586
|
+
const CANONICAL_PROVIDER_ORDER = ['codex', 'chatgpt', 'claude_code', 'claude_desktop', 'claude_sdk', 'claude_web', 'gemini_cli', 'antigravity', 'devin', 'windsurf', 'cursor', 'cline', 'opencode', 'aider', 'unknown'];
|
|
7587
|
+
|
|
7588
|
+
function canonicalCompanyRank(company) {
|
|
7589
|
+
const idx = CANONICAL_COMPANY_ORDER.indexOf(String(company || '').toLowerCase());
|
|
7590
|
+
return idx < 0 ? CANONICAL_COMPANY_ORDER.length : idx;
|
|
7591
|
+
}
|
|
7592
|
+
|
|
7593
|
+
function canonicalProviderRank(provider) {
|
|
7594
|
+
const idx = CANONICAL_PROVIDER_ORDER.indexOf(String(provider || '').toLowerCase());
|
|
7595
|
+
return idx < 0 ? CANONICAL_PROVIDER_ORDER.length : idx;
|
|
7596
|
+
}
|
|
7597
|
+
|
|
7598
|
+
function companyForModel(text) {
|
|
7599
|
+
const value = String(text || '').toLowerCase();
|
|
7600
|
+
if (value.startsWith('composer-') || value.startsWith('cursor-') || value === 'auto' || value === 'default') return 'cursor';
|
|
7601
|
+
const palette = modelFamilyPalette(value);
|
|
7602
|
+
if (!palette) return 'unknown';
|
|
7603
|
+
if (palette.family === 'openai') return 'openai';
|
|
7604
|
+
if (palette.family === 'claude') return 'anthropic';
|
|
7605
|
+
if (palette.family === 'gemini' || palette.family === 'antigravity') return 'google';
|
|
7606
|
+
if (palette.family === 'devin' || palette.family === 'windsurf') return 'cognition';
|
|
7607
|
+
if (palette.family === 'cursor') return 'cursor';
|
|
7608
|
+
return 'unknown';
|
|
7609
|
+
}
|
|
7610
|
+
|
|
7611
|
+
function canonicalModelRank(model) {
|
|
7612
|
+
const text = String(model || '').toLowerCase();
|
|
7613
|
+
const company = companyForModel(text);
|
|
7614
|
+
const companyRank = canonicalCompanyRank(company);
|
|
7615
|
+
let withinRank = 950;
|
|
7616
|
+
if (company === 'openai') {
|
|
7617
|
+
if (modelHasToken(text, ['nano'])) withinRank = 920;
|
|
7618
|
+
else if (modelHasToken(text, ['mini'])) withinRank = 900;
|
|
7619
|
+
else if (text.includes('gpt-4') && !text.includes('gpt-4.1')) withinRank = 700;
|
|
7620
|
+
else if (text.includes('gpt-4.1')) withinRank = 600;
|
|
7621
|
+
else if (modelHasToken(text, ['o1'])) withinRank = 590;
|
|
7622
|
+
else if (modelHasToken(text, ['o3', 'o4'])) withinRank = 580;
|
|
7623
|
+
else if (text.includes('gpt-5.5') || text.includes('gpt-5-5')) withinRank = 100;
|
|
7624
|
+
else if (text.includes('gpt-5.4') || text.includes('gpt-5-4')) withinRank = 200;
|
|
7625
|
+
else if (text.includes('gpt-5.3') || text.includes('gpt-5-3')) withinRank = 300;
|
|
7626
|
+
else if (text.includes('gpt-5.1') || text.includes('gpt-5-1')) withinRank = 400;
|
|
7627
|
+
else if (text.includes('gpt-5')) withinRank = 500;
|
|
7628
|
+
} else if (company === 'anthropic') {
|
|
7629
|
+
if (modelHasToken(text, ['haiku'])) withinRank = 920;
|
|
7630
|
+
else if (text.includes('opus-4-7') || text.includes('claude-4.7')) withinRank = 100;
|
|
7631
|
+
else if (text.includes('claude-4.6') && modelHasToken(text, ['opus'])) withinRank = 200;
|
|
7632
|
+
else if (text.includes('claude-4.5') && modelHasToken(text, ['opus'])) withinRank = 300;
|
|
7633
|
+
else if (text.includes('claude-4') && modelHasToken(text, ['opus'])) withinRank = 400;
|
|
7634
|
+
else if (modelHasToken(text, ['opus'])) withinRank = 500;
|
|
7635
|
+
else if (text.includes('claude-4.5') && modelHasToken(text, ['sonnet'])) withinRank = 600;
|
|
7636
|
+
else if (text.includes('claude-4') && modelHasToken(text, ['sonnet'])) withinRank = 700;
|
|
7637
|
+
else if (modelHasToken(text, ['sonnet'])) withinRank = 800;
|
|
7638
|
+
} else if (company === 'google') {
|
|
7639
|
+
// Major version dictates the bucket (newer first); flash variants get +50 within their generation.
|
|
7640
|
+
let base = 700;
|
|
7641
|
+
if (text.includes('gemini-3')) base = 100;
|
|
7642
|
+
else if (text.includes('gemini-2')) base = 300;
|
|
7643
|
+
else if (text.includes('gemini-1.5')) base = 500;
|
|
7644
|
+
else if (text.includes('gemini-1')) base = 600;
|
|
7645
|
+
let modifier = 0;
|
|
7646
|
+
if (text.includes('flash-lite')) modifier = 80;
|
|
7647
|
+
else if (text.includes('flash')) modifier = 50;
|
|
7648
|
+
withinRank = base + modifier;
|
|
7649
|
+
} else if (company === 'cursor') {
|
|
7650
|
+
if (text === 'composer-1.5') withinRank = 100;
|
|
7651
|
+
else if (text === 'composer-1') withinRank = 200;
|
|
7652
|
+
else if (text.startsWith('composer-')) withinRank = 300;
|
|
7653
|
+
else if (text === 'auto') withinRank = 700;
|
|
7654
|
+
else if (text === 'default') withinRank = 800;
|
|
7655
|
+
else if (text.startsWith('cursor-')) withinRank = 600;
|
|
7656
|
+
}
|
|
7657
|
+
return companyRank * 10000 + withinRank;
|
|
7658
|
+
}
|
|
7659
|
+
|
|
7660
|
+
function canonicalGroupRank(group) {
|
|
7661
|
+
if (statsBreakdownMode === 'model') return canonicalModelRank(group);
|
|
7662
|
+
if (statsBreakdownMode === 'company') return canonicalCompanyRank(group);
|
|
7663
|
+
return canonicalProviderRank(group);
|
|
7664
|
+
}
|
|
7665
|
+
|
|
7666
|
+
function statsCanonicalOrderedGroups(groups) {
|
|
7667
|
+
return [...groups].sort((a, b) => canonicalGroupRank(a) - canonicalGroupRank(b) || String(a).localeCompare(String(b)));
|
|
7668
|
+
}
|
|
7669
|
+
|
|
6432
7670
|
function statsBreakdownLabel(group) {
|
|
6433
7671
|
if (statsBreakdownMode === 'model') return modelLabel(group);
|
|
6434
7672
|
if (statsBreakdownMode === 'company') return companyLabel(group);
|
|
@@ -6506,6 +7744,7 @@ function formatTokenCount(value) {
|
|
|
6506
7744
|
function usageTokenSummary(usage) {
|
|
6507
7745
|
if (!usage || typeof usage !== 'object') return { inputTokens: 0, outputTokens: 0, totalTokens: 0, extraTokens: 0, estimated: false };
|
|
6508
7746
|
const estimated = usage.estimated === true || usage.estimated === 'true';
|
|
7747
|
+
const authoritativeTotalTokens = usage.authoritativeTotalTokens === true || usage.authoritative_total_tokens === true;
|
|
6509
7748
|
const inputTokens = positiveTokenNumber(firstUsageNumber(
|
|
6510
7749
|
usage.inputTokens,
|
|
6511
7750
|
usage.input_tokens,
|
|
@@ -6545,22 +7784,36 @@ function usageTokenSummary(usage) {
|
|
|
6545
7784
|
firstUsageNumber(usage.cachedContentTokenCount, usage.cached_content_token_count),
|
|
6546
7785
|
firstUsageNumber(usage.cachedTokens, usage.cached_tokens, usage.cacheTokens, usage.cache_tokens, usage.cached)
|
|
6547
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;
|
|
6548
7793
|
const reasoningOutputTokens = sumPositiveTokenNumbers(
|
|
6549
7794
|
firstUsageNumber(usage.reasoningOutputTokens, usage.reasoning_output_tokens),
|
|
6550
7795
|
firstUsageNumber(usage.thoughtsTokens, usage.thoughts_tokens, usage.thoughtsTokenCount, usage.thoughts_token_count),
|
|
6551
7796
|
firstUsageNumber(usage.reasoningTokens, usage.reasoning_tokens, usage.reasoningTokenCount, usage.reasoning_token_count)
|
|
6552
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;
|
|
6553
7804
|
const toolUsePromptTokens = sumPositiveTokenNumbers(
|
|
6554
7805
|
firstUsageNumber(usage.toolUsePromptTokens, usage.tool_use_prompt_tokens, usage.toolUsePromptTokenCount, usage.tool_use_prompt_token_count)
|
|
6555
7806
|
);
|
|
6556
|
-
const extraTokens =
|
|
7807
|
+
const extraTokens = countedCacheInputTokens + countedReasoningOutputTokens + toolUsePromptTokens;
|
|
6557
7808
|
const splitTokens = inputTokens + outputTokens;
|
|
6558
7809
|
const cumulativeTokens = totalInputTokens + totalOutputTokens;
|
|
6559
7810
|
const categoryTokens = splitTokens + extraTokens;
|
|
6560
|
-
const totalTokens =
|
|
6561
|
-
?
|
|
6562
|
-
:
|
|
6563
|
-
|
|
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 };
|
|
6564
7817
|
}
|
|
6565
7818
|
|
|
6566
7819
|
function firstUsageNumber() {
|
|
@@ -6760,11 +8013,15 @@ function sessionDisplayTitle(payload) {
|
|
|
6760
8013
|
|
|
6761
8014
|
function renderTree(payload, options) {
|
|
6762
8015
|
const opts = options || {};
|
|
8016
|
+
const sourceFilterReset = updateSourceFilterOptions(payload.available_source_options || []);
|
|
6763
8017
|
treeTitle.textContent = 'Sessions';
|
|
6764
8018
|
treeCount.textContent = String(payload.count || 0);
|
|
6765
8019
|
tree.innerHTML = '';
|
|
6766
8020
|
if (sidebarHead) sidebarHead.classList.remove('tree-scrolled');
|
|
6767
8021
|
repoOffsets.clear();
|
|
8022
|
+
if (sourceFilterReset && !opts.skipSourceReload) {
|
|
8023
|
+
window.setTimeout(() => loadTree({ skipSourceReload: true }).catch((error) => { setEmptySession(error.message); }), 0);
|
|
8024
|
+
}
|
|
6768
8025
|
if (!payload.groups || !payload.groups.length) {
|
|
6769
8026
|
tree.innerHTML = '<div class="inline-empty">No archived sessions matched these filters.</div>';
|
|
6770
8027
|
if (!opts.skipAutoLoad) setEmptySession('No archived sessions matched these filters.');
|
|
@@ -7064,6 +8321,14 @@ function renderSession(payload) {
|
|
|
7064
8321
|
if (sessionMetaRight) {
|
|
7065
8322
|
const innerParts = [];
|
|
7066
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
|
+
}
|
|
7067
8332
|
const repoRaw = payload.repo_display || payload.repo || payload.scope || '';
|
|
7068
8333
|
if (repoRaw) {
|
|
7069
8334
|
if (innerParts.length) innerParts.push('<span class="meta-dot" aria-hidden="true">\u00b7</span>');
|
|
@@ -7079,7 +8344,16 @@ function renderSession(payload) {
|
|
|
7079
8344
|
sessionMetaRight.innerHTML = innerParts.length
|
|
7080
8345
|
? '<div class="session-meta-right-inner">' + innerParts.join('') + '</div>'
|
|
7081
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
|
+
}
|
|
7082
8355
|
const titleBits = [];
|
|
8356
|
+
if (payload.parent_composer_id) titleBits.push('Parent session: ' + payload.parent_composer_id);
|
|
7083
8357
|
if (payload.time_status === 'recovered-time-unknown') titleBits.push('Recovered time unknown');
|
|
7084
8358
|
else if (payload.started_at) titleBits.push('Started ' + formatWhen(payload.started_at));
|
|
7085
8359
|
if (payload.session_id) titleBits.push('Session ID: ' + payload.session_id);
|
|
@@ -7108,7 +8382,7 @@ function renderSession(payload) {
|
|
|
7108
8382
|
);
|
|
7109
8383
|
copyResumeButton.classList.remove('copied');
|
|
7110
8384
|
jumpEnd.hidden = false;
|
|
7111
|
-
setView(viewMode);
|
|
8385
|
+
setView(viewMode, { preserveScroll: false });
|
|
7112
8386
|
for (const button of tree.querySelectorAll('.session-node')) {
|
|
7113
8387
|
button.classList.toggle('active', button.dataset.id === payload.session_id);
|
|
7114
8388
|
}
|
|
@@ -7126,16 +8400,20 @@ function renderMessages(messages, sessionSummary) {
|
|
|
7126
8400
|
readableView.className = '';
|
|
7127
8401
|
readableView.innerHTML = '';
|
|
7128
8402
|
const summaryText = sessionSummaryText(sessionSummary);
|
|
8403
|
+
const subagentsElement = sessionSubagentsElement(sessionSummary);
|
|
8404
|
+
const subagentRunItems = subagentRunTimelineItems(sessionSummary);
|
|
7129
8405
|
const visibleMessages = summaryText
|
|
7130
8406
|
? (messages || []).filter((message) => message?.metadata?.summaryKind !== 'conversation_summary')
|
|
7131
8407
|
: (messages || []);
|
|
7132
|
-
|
|
8408
|
+
const renderItems = mergeTimelineRenderItems(pairedToolRenderItems(visibleMessages), subagentRunItems);
|
|
8409
|
+
if (!renderItems.length && !summaryText && !subagentsElement) {
|
|
7133
8410
|
readableView.className = 'inline-empty';
|
|
7134
8411
|
readableView.textContent = 'No transcript messages are available for this session.';
|
|
7135
8412
|
return;
|
|
7136
8413
|
}
|
|
7137
8414
|
const summaryElement = sessionSummaryElement(sessionSummary);
|
|
7138
8415
|
if (summaryElement) readableView.appendChild(summaryElement);
|
|
8416
|
+
if (subagentsElement) readableView.appendChild(subagentsElement);
|
|
7139
8417
|
const scheduleNext = typeof window.requestAnimationFrame === 'function'
|
|
7140
8418
|
? (fn) => window.requestAnimationFrame(fn)
|
|
7141
8419
|
: (fn) => window.setTimeout(fn, 0);
|
|
@@ -7144,16 +8422,17 @@ function renderMessages(messages, sessionSummary) {
|
|
|
7144
8422
|
const renderChunk = () => {
|
|
7145
8423
|
if (renderSerial !== renderMessagesSerial) return;
|
|
7146
8424
|
const fragment = document.createDocumentFragment();
|
|
7147
|
-
const end = Math.min(
|
|
8425
|
+
const end = Math.min(renderItems.length, index + MESSAGE_RENDER_CHUNK_SIZE);
|
|
7148
8426
|
for (; index < end; index++) {
|
|
7149
|
-
const
|
|
8427
|
+
const item = renderItems[index];
|
|
8428
|
+
const message = item.message;
|
|
7150
8429
|
const gap = renderTimeGap(previousTimestamp, message.timestamp);
|
|
7151
8430
|
if (gap) fragment.appendChild(gap);
|
|
7152
|
-
fragment.appendChild(messageElement(message));
|
|
7153
|
-
|
|
8431
|
+
fragment.appendChild(messageElement(message, item));
|
|
8432
|
+
previousTimestamp = item.lastTimestamp || message.timestamp || previousTimestamp;
|
|
7154
8433
|
}
|
|
7155
8434
|
readableView.appendChild(fragment);
|
|
7156
|
-
if (index <
|
|
8435
|
+
if (index < renderItems.length) {
|
|
7157
8436
|
scheduleNext(renderChunk);
|
|
7158
8437
|
return;
|
|
7159
8438
|
}
|
|
@@ -7168,6 +8447,204 @@ function renderMessages(messages, sessionSummary) {
|
|
|
7168
8447
|
renderChunk();
|
|
7169
8448
|
}
|
|
7170
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
|
+
|
|
8469
|
+
function pairedToolRenderItems(messages) {
|
|
8470
|
+
const pairings = pairedToolResultIndexes(messages);
|
|
8471
|
+
const skippedResultIndexes = new Set(pairings.duplicateResultIndexes);
|
|
8472
|
+
const result = [];
|
|
8473
|
+
for (let index = 0; index < messages.length; index++) {
|
|
8474
|
+
if (skippedResultIndexes.has(index)) continue;
|
|
8475
|
+
const message = messages[index];
|
|
8476
|
+
const calls = messageToolCalls(message);
|
|
8477
|
+
if (!calls.length) {
|
|
8478
|
+
result.push({ type: 'message', message });
|
|
8479
|
+
continue;
|
|
8480
|
+
}
|
|
8481
|
+
const pairedResults = (pairings.byCallMessage.get(index) || [])
|
|
8482
|
+
.filter((resultIndex) => !skippedResultIndexes.has(resultIndex))
|
|
8483
|
+
.map((resultIndex) => {
|
|
8484
|
+
skippedResultIndexes.add(resultIndex);
|
|
8485
|
+
return messages[resultIndex];
|
|
8486
|
+
});
|
|
8487
|
+
let next = index + 1;
|
|
8488
|
+
while (pairedResults.length < calls.length && next < messages.length && isPairableToolResultMessage(messages[next])) {
|
|
8489
|
+
if (skippedResultIndexes.has(next)) {
|
|
8490
|
+
next += 1;
|
|
8491
|
+
continue;
|
|
8492
|
+
}
|
|
8493
|
+
pairedResults.push(messages[next]);
|
|
8494
|
+
skippedResultIndexes.add(next);
|
|
8495
|
+
next += 1;
|
|
8496
|
+
}
|
|
8497
|
+
if (pairedResults.length) {
|
|
8498
|
+
result.push({
|
|
8499
|
+
type: 'message',
|
|
8500
|
+
message,
|
|
8501
|
+
pairedToolResults: pairedResults,
|
|
8502
|
+
lastTimestamp: pairedResults[pairedResults.length - 1].timestamp || message.timestamp
|
|
8503
|
+
});
|
|
8504
|
+
index = next - 1;
|
|
8505
|
+
} else {
|
|
8506
|
+
result.push({ type: 'message', message });
|
|
8507
|
+
}
|
|
8508
|
+
}
|
|
8509
|
+
return groupConsecutiveToolItems(result);
|
|
8510
|
+
}
|
|
8511
|
+
|
|
8512
|
+
function groupConsecutiveToolItems(items) {
|
|
8513
|
+
const grouped = [];
|
|
8514
|
+
let run = [];
|
|
8515
|
+
const flush = () => {
|
|
8516
|
+
if (run.length > 1) {
|
|
8517
|
+
grouped.push({
|
|
8518
|
+
type: 'tool-group',
|
|
8519
|
+
message: run[0].message,
|
|
8520
|
+
items: run,
|
|
8521
|
+
lastTimestamp: run[run.length - 1].lastTimestamp || run[run.length - 1].message.timestamp || run[0].message.timestamp
|
|
8522
|
+
});
|
|
8523
|
+
} else if (run.length === 1) {
|
|
8524
|
+
grouped.push(run[0]);
|
|
8525
|
+
}
|
|
8526
|
+
run = [];
|
|
8527
|
+
};
|
|
8528
|
+
for (const item of items) {
|
|
8529
|
+
if (!isToolActivityRenderItem(item)) {
|
|
8530
|
+
flush();
|
|
8531
|
+
grouped.push(item);
|
|
8532
|
+
continue;
|
|
8533
|
+
}
|
|
8534
|
+
run.push(item);
|
|
8535
|
+
}
|
|
8536
|
+
flush();
|
|
8537
|
+
return grouped;
|
|
8538
|
+
}
|
|
8539
|
+
|
|
8540
|
+
function isToolActivityRenderItem(item) {
|
|
8541
|
+
if (!item || item.type !== 'message') return false;
|
|
8542
|
+
const message = item.message || {};
|
|
8543
|
+
const role = String(message.role || '').toLowerCase();
|
|
8544
|
+
if (role === 'tool') return Boolean(parseToolResult(message.content || '', message));
|
|
8545
|
+
if (role !== 'assistant') return false;
|
|
8546
|
+
const calls = messageToolCalls(message, item.pairedToolResults || []);
|
|
8547
|
+
if (!calls.length) return false;
|
|
8548
|
+
return !stripToolInvocationLines(message.content || '').trim();
|
|
8549
|
+
}
|
|
8550
|
+
|
|
8551
|
+
function pairedToolResultIndexes(messages) {
|
|
8552
|
+
const calls = [];
|
|
8553
|
+
const callsByEventId = new Map();
|
|
8554
|
+
const callsById = new Map();
|
|
8555
|
+
const callsByKind = new Map();
|
|
8556
|
+
const addCallKey = (map, key, callIndex) => {
|
|
8557
|
+
if (!key) return;
|
|
8558
|
+
if (!map.has(key)) map.set(key, []);
|
|
8559
|
+
map.get(key).push(callIndex);
|
|
8560
|
+
};
|
|
8561
|
+
for (let messagePosition = 0; messagePosition < messages.length; messagePosition++) {
|
|
8562
|
+
const toolCalls = messageToolCalls(messages[messagePosition]);
|
|
8563
|
+
for (const call of toolCalls) {
|
|
8564
|
+
const callIndex = calls.length;
|
|
8565
|
+
calls.push({ messagePosition, call });
|
|
8566
|
+
addCallKey(callsByEventId, call.eventId || '', callIndex);
|
|
8567
|
+
addCallKey(callsById, normalizedToolId(call.id), callIndex);
|
|
8568
|
+
addCallKey(callsByKind, normalizeToolToken(call.kind || call.name || call.title || ''), callIndex);
|
|
8569
|
+
}
|
|
8570
|
+
}
|
|
8571
|
+
|
|
8572
|
+
const resultRecords = preferredToolResultRecords(messages);
|
|
8573
|
+
const byCallMessage = new Map();
|
|
8574
|
+
const usedCalls = new Set();
|
|
8575
|
+
for (const record of resultRecords.records) {
|
|
8576
|
+
const parentEventId = record.event?.parentEventId || '';
|
|
8577
|
+
let callIndex = firstUnusedIndex(callsByEventId.get(parentEventId), usedCalls);
|
|
8578
|
+
if (callIndex < 0) callIndex = firstUnusedIndex(callsById.get(normalizedToolId(record.result.id)), usedCalls);
|
|
8579
|
+
if (callIndex < 0) callIndex = firstUnusedIndex(callsByKind.get(normalizeToolToken(record.result.name || record.result.kind || record.result.title || '')), usedCalls);
|
|
8580
|
+
if (callIndex < 0) continue;
|
|
8581
|
+
usedCalls.add(callIndex);
|
|
8582
|
+
const messagePosition = calls[callIndex].messagePosition;
|
|
8583
|
+
if (!byCallMessage.has(messagePosition)) byCallMessage.set(messagePosition, []);
|
|
8584
|
+
byCallMessage.get(messagePosition).push(record.index);
|
|
8585
|
+
}
|
|
8586
|
+
return { byCallMessage, duplicateResultIndexes: resultRecords.duplicateResultIndexes };
|
|
8587
|
+
}
|
|
8588
|
+
|
|
8589
|
+
function preferredToolResultRecords(messages) {
|
|
8590
|
+
const records = [];
|
|
8591
|
+
const byId = new Map();
|
|
8592
|
+
const duplicateResultIndexes = new Set();
|
|
8593
|
+
for (let index = 0; index < messages.length; index++) {
|
|
8594
|
+
if (!isPairableToolResultMessage(messages[index])) continue;
|
|
8595
|
+
const result = parseToolResult(messages[index]?.content || '', messages[index]);
|
|
8596
|
+
if (!result) continue;
|
|
8597
|
+
const event = canonicalEventsForMessage(messages[index], 'tool.completed')[0] || null;
|
|
8598
|
+
const record = { index, result, event };
|
|
8599
|
+
records.push(record);
|
|
8600
|
+
const id = normalizedToolId(result.id);
|
|
8601
|
+
if (!id) continue;
|
|
8602
|
+
const existing = byId.get(id);
|
|
8603
|
+
if (!existing) {
|
|
8604
|
+
byId.set(id, record);
|
|
8605
|
+
continue;
|
|
8606
|
+
}
|
|
8607
|
+
const preferred = preferredToolResultRecord(existing, record);
|
|
8608
|
+
const discarded = preferred === existing ? record : existing;
|
|
8609
|
+
duplicateResultIndexes.add(discarded.index);
|
|
8610
|
+
byId.set(id, preferred);
|
|
8611
|
+
}
|
|
8612
|
+
return {
|
|
8613
|
+
records: records.filter((record) => !duplicateResultIndexes.has(record.index)),
|
|
8614
|
+
duplicateResultIndexes
|
|
8615
|
+
};
|
|
8616
|
+
}
|
|
8617
|
+
|
|
8618
|
+
function preferredToolResultRecord(left, right) {
|
|
8619
|
+
return toolResultDisplayScore(right.result) > toolResultDisplayScore(left.result) ? right : left;
|
|
8620
|
+
}
|
|
8621
|
+
|
|
8622
|
+
function toolResultDisplayScore(result) {
|
|
8623
|
+
const rawCategory = normalizeToolToken(result?.rawCategory || '');
|
|
8624
|
+
const kind = normalizeToolToken(result?.kind || '');
|
|
8625
|
+
const category = normalizeToolToken(result?.category || '');
|
|
8626
|
+
let score = 0;
|
|
8627
|
+
if (['exec_command_end', 'patch_apply_end', 'mcp_tool_call_end', 'web_search_end', 'tool_result', 'tool_output'].includes(rawCategory)) score += 2000;
|
|
8628
|
+
if (rawCategory === 'function_call_output' || rawCategory === 'custom_tool_call_output') score -= 1000;
|
|
8629
|
+
if (category && category !== 'function') score += 250;
|
|
8630
|
+
if (kind && !kind.startsWith('call_')) score += 150;
|
|
8631
|
+
if (/^\$\s/.test(String(result?.detail || result?.output || ''))) score += 250;
|
|
8632
|
+
score += Math.min(200, String(result?.output || '').length / 200);
|
|
8633
|
+
return score;
|
|
8634
|
+
}
|
|
8635
|
+
|
|
8636
|
+
function firstUnusedIndex(indexes, used) {
|
|
8637
|
+
if (!Array.isArray(indexes)) return -1;
|
|
8638
|
+
for (const index of indexes) {
|
|
8639
|
+
if (!used.has(index)) return index;
|
|
8640
|
+
}
|
|
8641
|
+
return -1;
|
|
8642
|
+
}
|
|
8643
|
+
|
|
8644
|
+
function isPairableToolResultMessage(message) {
|
|
8645
|
+
return String(message?.role || '').toLowerCase() === 'tool' && Boolean(parseToolResult(message?.content || '', message));
|
|
8646
|
+
}
|
|
8647
|
+
|
|
7171
8648
|
function sessionSummaryText(sessionSummary) {
|
|
7172
8649
|
if (!sessionSummary || typeof sessionSummary !== 'object') return '';
|
|
7173
8650
|
return String(
|
|
@@ -7179,48 +8656,284 @@ function sessionSummaryText(sessionSummary) {
|
|
|
7179
8656
|
).trim();
|
|
7180
8657
|
}
|
|
7181
8658
|
|
|
7182
|
-
function sessionSummaryElement(sessionSummary) {
|
|
7183
|
-
const text = sessionSummaryText(sessionSummary);
|
|
7184
|
-
if (!text) return null;
|
|
8659
|
+
function sessionSummaryElement(sessionSummary) {
|
|
8660
|
+
const text = sessionSummaryText(sessionSummary);
|
|
8661
|
+
if (!text) return null;
|
|
8662
|
+
const row = document.createElement('article');
|
|
8663
|
+
row.className = 'message context session-summary-message';
|
|
8664
|
+
const bubble = document.createElement('div');
|
|
8665
|
+
bubble.className = 'bubble';
|
|
8666
|
+
const details = document.createElement('details');
|
|
8667
|
+
details.className = 'context-card session-summary-card';
|
|
8668
|
+
details.open = true;
|
|
8669
|
+
details.innerHTML =
|
|
8670
|
+
'<summary>' +
|
|
8671
|
+
'<span class="context-prefix"><span class="context-caret"></span><span class="context-glyph">' + contextIconSvg('conversation_summary') + '</span></span>' +
|
|
8672
|
+
'<span class="context-copy"><span class="context-title">Conversation summary</span>' +
|
|
8673
|
+
'<span class="context-meta"><span class="context-chip">' + esc(summarySourceLabel(sessionSummary.source)) + '</span></span></span>' +
|
|
8674
|
+
'<span class="context-end"></span>' +
|
|
8675
|
+
'</summary>';
|
|
8676
|
+
const end = details.querySelector('.context-end');
|
|
8677
|
+
if (end) end.appendChild(messageCopyButton(text));
|
|
8678
|
+
const body = document.createElement('div');
|
|
8679
|
+
body.className = 'session-summary-body';
|
|
8680
|
+
body.innerHTML = renderRichText(text);
|
|
8681
|
+
details.appendChild(body);
|
|
8682
|
+
bubble.appendChild(details);
|
|
8683
|
+
row.appendChild(bubble);
|
|
8684
|
+
return row;
|
|
8685
|
+
}
|
|
8686
|
+
|
|
8687
|
+
function summarySourceLabel(source) {
|
|
8688
|
+
const value = String(source || '').trim();
|
|
8689
|
+
if (value === 'claude-web-export') return 'Claude.ai export';
|
|
8690
|
+
if (value === 'chatgpt-export') return 'ChatGPT export';
|
|
8691
|
+
if (value) return value.replace(/[-_]+/g, ' ');
|
|
8692
|
+
return 'summary';
|
|
8693
|
+
}
|
|
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');
|
|
7185
8807
|
const row = document.createElement('article');
|
|
7186
|
-
row.className = 'message context session-
|
|
8808
|
+
row.className = 'message context session-subagent-run-message';
|
|
7187
8809
|
const bubble = document.createElement('div');
|
|
7188
8810
|
bubble.className = 'bubble';
|
|
7189
8811
|
const details = document.createElement('details');
|
|
7190
|
-
details.className = 'context-card session-
|
|
7191
|
-
details.open = true;
|
|
8812
|
+
details.className = 'context-card session-subagent-run-card';
|
|
7192
8813
|
details.innerHTML =
|
|
7193
8814
|
'<summary>' +
|
|
7194
|
-
'<span class="context-prefix"><span class="context-caret"></span><span class="context-glyph">' + contextIconSvg('
|
|
7195
|
-
'<span class="context-copy"><span class="context-title">
|
|
7196
|
-
'<span class="context-meta"
|
|
7197
|
-
'<span class="context-end"></span>' +
|
|
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>' +
|
|
7198
8819
|
'</summary>';
|
|
7199
8820
|
const end = details.querySelector('.context-end');
|
|
7200
|
-
if (end) end.appendChild(messageCopyButton(
|
|
8821
|
+
if (end) end.appendChild(messageCopyButton(formatSubagentRunPlainText(run)));
|
|
7201
8822
|
const body = document.createElement('div');
|
|
7202
|
-
body.className = 'session-
|
|
7203
|
-
body.innerHTML =
|
|
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
|
+
}
|
|
7204
8838
|
details.appendChild(body);
|
|
7205
8839
|
bubble.appendChild(details);
|
|
7206
8840
|
row.appendChild(bubble);
|
|
7207
8841
|
return row;
|
|
7208
8842
|
}
|
|
7209
8843
|
|
|
7210
|
-
function
|
|
7211
|
-
const
|
|
7212
|
-
|
|
7213
|
-
|
|
7214
|
-
|
|
7215
|
-
|
|
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
|
+
}
|
|
7216
8933
|
}
|
|
7217
8934
|
|
|
7218
8935
|
function renderTimeGap(previous, current) {
|
|
7219
|
-
|
|
7220
|
-
const prev = new Date(previous).getTime();
|
|
7221
|
-
const next = new Date(current).getTime();
|
|
7222
|
-
if (!Number.isFinite(prev) || !Number.isFinite(next)) return null;
|
|
7223
|
-
const diffSec = (next - prev) / 1000;
|
|
8936
|
+
const diffSec = timeGapSeconds(previous, current);
|
|
7224
8937
|
if (diffSec < 5) return null;
|
|
7225
8938
|
const label = formatGapLabel(diffSec);
|
|
7226
8939
|
if (!label) return null;
|
|
@@ -7230,6 +8943,14 @@ function renderTimeGap(previous, current) {
|
|
|
7230
8943
|
return node;
|
|
7231
8944
|
}
|
|
7232
8945
|
|
|
8946
|
+
function timeGapSeconds(previous, current) {
|
|
8947
|
+
if (!previous || !current) return 0;
|
|
8948
|
+
const prev = new Date(previous).getTime();
|
|
8949
|
+
const next = new Date(current).getTime();
|
|
8950
|
+
if (!Number.isFinite(prev) || !Number.isFinite(next)) return 0;
|
|
8951
|
+
return Math.max(0, (next - prev) / 1000);
|
|
8952
|
+
}
|
|
8953
|
+
|
|
7233
8954
|
function formatGapLabel(diffSec) {
|
|
7234
8955
|
if (diffSec < 60) return Math.round(diffSec) + 's';
|
|
7235
8956
|
if (diffSec < 3600) return Math.round(diffSec / 60) + 'm';
|
|
@@ -7302,19 +9023,22 @@ function highlightSearchMatches(root, term) {
|
|
|
7302
9023
|
walk(root);
|
|
7303
9024
|
}
|
|
7304
9025
|
|
|
7305
|
-
function messageElement(message) {
|
|
9026
|
+
function messageElement(message, options = {}) {
|
|
9027
|
+
if (options.type === 'tool-group') return toolGroupElement(options);
|
|
9028
|
+
if (options.type === 'subagent-run') return subagentRunElement(options.run || {});
|
|
7306
9029
|
const context = generatedContextForMessage(message);
|
|
7307
9030
|
if (context) return contextMessageElement(message, context);
|
|
7308
9031
|
const role = String(message.role || 'unknown').toLowerCase();
|
|
7309
9032
|
const content = String(message.content || '');
|
|
7310
|
-
const toolCalls = messageToolCalls(message);
|
|
9033
|
+
const toolCalls = messageToolCalls(message, options.pairedToolResults || []);
|
|
7311
9034
|
const toolResult = role === 'tool' ? parseToolResult(content, message) : null;
|
|
7312
|
-
const contentWithoutTools = toolCalls.length ? stripToolInvocationLines(content) : content;
|
|
9035
|
+
const contentWithoutTools = isViewerStructuredToolCallMessage(message) ? '' : toolCalls.length ? stripToolInvocationLines(content) : content;
|
|
7313
9036
|
const toolOnly = toolCalls.length && !contentWithoutTools.trim();
|
|
7314
9037
|
const className = toolOnly ? 'tool' : ['user', 'assistant', 'system', 'tool'].includes(role) ? role : 'assistant';
|
|
7315
9038
|
const bodyContent = toolResult
|
|
7316
9039
|
? renderToolResult(toolResult)
|
|
7317
9040
|
: renderMessageBodyWithTools(className, contentWithoutTools, toolCalls);
|
|
9041
|
+
const attachmentsHtml = renderMessageAttachments(message);
|
|
7318
9042
|
const row = document.createElement('article');
|
|
7319
9043
|
row.className = 'message ' + className;
|
|
7320
9044
|
if (toolCalls.length) row.classList.add('tool-call-turn');
|
|
@@ -7339,14 +9063,29 @@ function messageElement(message) {
|
|
|
7339
9063
|
'<div class="message-head"><span class="role">' + esc(roleLabel(role)) + '</span>' +
|
|
7340
9064
|
'<span class="message-time"' + timeAttr(message.timestamp) + '>' + esc(relativeTime(message.timestamp)) + '</span>' +
|
|
7341
9065
|
usageChipHtml + '</div>';
|
|
7342
|
-
bubble.innerHTML = headHtml + bodyContent;
|
|
9066
|
+
bubble.innerHTML = headHtml + bodyContent + attachmentsHtml;
|
|
7343
9067
|
const head = bubble.querySelector('.message-head');
|
|
7344
9068
|
if (head) head.appendChild(messageCopyButton(content));
|
|
7345
9069
|
row.appendChild(bubble);
|
|
7346
9070
|
return row;
|
|
7347
9071
|
}
|
|
7348
9072
|
|
|
9073
|
+
function toolGroupElement(group) {
|
|
9074
|
+
const calls = [];
|
|
9075
|
+
for (const item of group.items || []) {
|
|
9076
|
+
calls.push(...toolCardsForRenderItem(item));
|
|
9077
|
+
}
|
|
9078
|
+
const row = document.createElement('article');
|
|
9079
|
+
row.className = 'message tool tool-call-turn tool-group-turn';
|
|
9080
|
+
const bubble = document.createElement('div');
|
|
9081
|
+
bubble.className = 'bubble';
|
|
9082
|
+
bubble.innerHTML = renderToolGroupCard(calls);
|
|
9083
|
+
row.appendChild(bubble);
|
|
9084
|
+
return row;
|
|
9085
|
+
}
|
|
9086
|
+
|
|
7349
9087
|
function contextMessageElement(message, context) {
|
|
9088
|
+
if (context.kind === 'task_notification') return contextLineElement(message, context);
|
|
7350
9089
|
const row = document.createElement('article');
|
|
7351
9090
|
row.className = 'message context context-' + escClass(context.kind || 'metadata');
|
|
7352
9091
|
const bubble = document.createElement('div');
|
|
@@ -7371,6 +9110,25 @@ function contextMessageElement(message, context) {
|
|
|
7371
9110
|
return row;
|
|
7372
9111
|
}
|
|
7373
9112
|
|
|
9113
|
+
function contextLineElement(message, context) {
|
|
9114
|
+
const row = document.createElement('article');
|
|
9115
|
+
row.className = 'message context context-line-turn context-' + escClass(context.kind || 'metadata');
|
|
9116
|
+
const bubble = document.createElement('div');
|
|
9117
|
+
bubble.className = 'bubble';
|
|
9118
|
+
const line = document.createElement('div');
|
|
9119
|
+
line.className = 'context-line';
|
|
9120
|
+
line.innerHTML =
|
|
9121
|
+
'<span class="context-glyph">' + contextIconSvg(context.kind) + '</span>' +
|
|
9122
|
+
'<span class="context-title">' + esc(contextTitle(context.kind)) + '</span>' +
|
|
9123
|
+
'<span class="context-meta">' + context.chips.map((chip) => '<span class="context-chip">' + esc(chip) + '</span>').join('') + '</span>' +
|
|
9124
|
+
'<span class="context-end"><span class="context-time"' + timeAttr(message.timestamp) + '>' + esc(relativeTime(message.timestamp)) + '</span></span>';
|
|
9125
|
+
const end = line.querySelector('.context-end');
|
|
9126
|
+
if (end) end.appendChild(messageCopyButton(message.content || ''));
|
|
9127
|
+
bubble.appendChild(line);
|
|
9128
|
+
row.appendChild(bubble);
|
|
9129
|
+
return row;
|
|
9130
|
+
}
|
|
9131
|
+
|
|
7374
9132
|
function generatedContextForMessage(message) {
|
|
7375
9133
|
const metadata = message?.metadata || {};
|
|
7376
9134
|
const content = String(message?.content || '').trim();
|
|
@@ -7440,6 +9198,7 @@ function contextTitle(kind) {
|
|
|
7440
9198
|
local_command_caveat: 'Local command',
|
|
7441
9199
|
local_command_stdout: 'Local command',
|
|
7442
9200
|
system_reminder: 'System reminder',
|
|
9201
|
+
subagents: 'Claude subagents',
|
|
7443
9202
|
compaction: 'Compaction',
|
|
7444
9203
|
metadata: 'Context metadata'
|
|
7445
9204
|
};
|
|
@@ -7493,12 +9252,137 @@ function xmlTagText(text, tag) {
|
|
|
7493
9252
|
return source.slice(bodyStart, end).trim();
|
|
7494
9253
|
}
|
|
7495
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
|
+
|
|
7496
9335
|
function renderMessageBodyWithTools(className, content, toolCalls) {
|
|
7497
9336
|
const body = className === 'user' ? renderPlainText(content) : renderRichText(content);
|
|
7498
|
-
const stack = toolCalls.length
|
|
9337
|
+
const stack = toolCalls.length > 1
|
|
9338
|
+
? renderToolGroupCard(toolCalls)
|
|
9339
|
+
: toolCalls.length
|
|
9340
|
+
? '<div class="tool-stack">' + toolCalls.map(renderToolCallout).join('') + '</div>'
|
|
9341
|
+
: '';
|
|
7499
9342
|
return body ? '<div class="tool-body">' + body + '</div>' + stack : stack;
|
|
7500
9343
|
}
|
|
7501
9344
|
|
|
9345
|
+
function renderToolGroupCard(calls) {
|
|
9346
|
+
const tools = Array.isArray(calls) ? calls : [];
|
|
9347
|
+
if (!tools.length) return '';
|
|
9348
|
+
return '<details class="tool-group-card">' +
|
|
9349
|
+
'<summary><span class="tool-group-prefix"><span class="tool-group-caret"></span><span class="tool-glyph">' +
|
|
9350
|
+
renderToolIcon({ category: dominantToolCategory(tools), kind: 'Tool activity' }) +
|
|
9351
|
+
'</span></span><span class="tool-group-title">' +
|
|
9352
|
+
esc(toolStackSummary(tools) || 'Used ' + tools.length + ' tools') +
|
|
9353
|
+
'</span></summary>' +
|
|
9354
|
+
'<div class="tool-stack">' + tools.map(renderToolCallout).join('') + '</div></details>';
|
|
9355
|
+
}
|
|
9356
|
+
|
|
9357
|
+
function toolCardsForRenderItem(item) {
|
|
9358
|
+
const message = item?.message || {};
|
|
9359
|
+
if (String(message.role || '').toLowerCase() === 'tool') {
|
|
9360
|
+
const result = parseToolResult(message.content || '', message);
|
|
9361
|
+
return result ? [toolCardFromResult(result)] : [];
|
|
9362
|
+
}
|
|
9363
|
+
return messageToolCalls(message, item?.pairedToolResults || []);
|
|
9364
|
+
}
|
|
9365
|
+
|
|
9366
|
+
function toolCardFromResult(result) {
|
|
9367
|
+
const category = normalizedToolCategory(result.category, result.kind);
|
|
9368
|
+
const detail = result.detail || result.summary || firstLine(result.output || '') || result.kind || 'Tool output';
|
|
9369
|
+
return toolCard({
|
|
9370
|
+
kind: result.name || result.kind || 'Tool output',
|
|
9371
|
+
title: result.kind || result.title || '',
|
|
9372
|
+
category,
|
|
9373
|
+
categoryLabel: result.categoryLabel || toolCategoryLabel(category),
|
|
9374
|
+
icon: result.icon || toolIcon(category, result.kind || ''),
|
|
9375
|
+
status: result.status || 'completed',
|
|
9376
|
+
argument: detail,
|
|
9377
|
+
rawInputSummary: detail,
|
|
9378
|
+
inputPreview: '',
|
|
9379
|
+
target: result.target || '',
|
|
9380
|
+
id: result.id || '',
|
|
9381
|
+
result,
|
|
9382
|
+
resultOnly: true
|
|
9383
|
+
});
|
|
9384
|
+
}
|
|
9385
|
+
|
|
7502
9386
|
function copyIconSvg() {
|
|
7503
9387
|
return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>';
|
|
7504
9388
|
}
|
|
@@ -7547,13 +9431,15 @@ function detectToolName(text) {
|
|
|
7547
9431
|
return tool ? tool.label : '';
|
|
7548
9432
|
}
|
|
7549
9433
|
|
|
7550
|
-
function messageToolCalls(message) {
|
|
9434
|
+
function messageToolCalls(message, pairedResultMessages) {
|
|
7551
9435
|
const eventCalls = canonicalEventsForMessage(message, 'tool.called').map(toolCallFromEvent).filter(Boolean);
|
|
7552
|
-
if (eventCalls.length) return eventCalls;
|
|
9436
|
+
if (eventCalls.length) return attachPairedToolResults(eventCalls, pairedResultMessages);
|
|
7553
9437
|
const metadataCalls = Array.isArray(message?.metadata?.toolCalls) ? message.metadata.toolCalls : [];
|
|
9438
|
+
const structuredCall = metadataCalls.length ? null : viewerStructuredToolCall(message);
|
|
7554
9439
|
const textCalls = toolInvocationsFromText(message?.content || '');
|
|
9440
|
+
if (structuredCall) return attachPairedToolResults([structuredCall], pairedResultMessages);
|
|
7555
9441
|
if (metadataCalls.length) {
|
|
7556
|
-
|
|
9442
|
+
const calls = metadataCalls.map((meta, index) => {
|
|
7557
9443
|
const text = textCalls[index] || {};
|
|
7558
9444
|
const kind = meta.displayName || meta.name || text.kind || 'Tool';
|
|
7559
9445
|
const argument = meta.argument || meta.rawInputSummary || text.argument || '';
|
|
@@ -7569,10 +9455,11 @@ function messageToolCalls(message) {
|
|
|
7569
9455
|
categoryLabel: meta.categoryLabel || '',
|
|
7570
9456
|
icon: meta.icon || '',
|
|
7571
9457
|
rawCategory: meta.rawCategory || '',
|
|
7572
|
-
id: meta.id || '',
|
|
9458
|
+
id: meta.id || meta.callId || meta.call_id || meta.toolCallId || meta.tool_call_id || meta.toolUseId || meta.tool_use_id || '',
|
|
7573
9459
|
arguments: meta.arguments || null
|
|
7574
9460
|
});
|
|
7575
9461
|
}).filter((call) => call.kind || call.title || call.argument);
|
|
9462
|
+
return attachPairedToolResults(calls, pairedResultMessages);
|
|
7576
9463
|
}
|
|
7577
9464
|
const total = Math.max(metadataCalls.length, textCalls.length);
|
|
7578
9465
|
const calls = [];
|
|
@@ -7592,11 +9479,123 @@ function messageToolCalls(message) {
|
|
|
7592
9479
|
categoryLabel: meta.categoryLabel || '',
|
|
7593
9480
|
icon: meta.icon || '',
|
|
7594
9481
|
rawCategory: meta.rawCategory || '',
|
|
7595
|
-
id: meta.id || '',
|
|
9482
|
+
id: meta.id || meta.callId || meta.call_id || meta.toolCallId || meta.tool_call_id || meta.toolUseId || meta.tool_use_id || '',
|
|
7596
9483
|
arguments: meta.arguments || null
|
|
7597
9484
|
}));
|
|
7598
9485
|
}
|
|
7599
|
-
return calls.filter((call) => call.kind || call.title || call.argument);
|
|
9486
|
+
return attachPairedToolResults(calls.filter((call) => call.kind || call.title || call.argument), pairedResultMessages);
|
|
9487
|
+
}
|
|
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
|
+
|
|
9545
|
+
function attachPairedToolResults(calls, pairedResultMessages) {
|
|
9546
|
+
if (!Array.isArray(pairedResultMessages) || !pairedResultMessages.length) return calls;
|
|
9547
|
+
const results = pairedResultMessages
|
|
9548
|
+
.map((message) => parseToolResult(message?.content || '', message))
|
|
9549
|
+
.filter(Boolean);
|
|
9550
|
+
if (!results.length) return calls;
|
|
9551
|
+
const remaining = results.slice();
|
|
9552
|
+
return calls.map((call) => {
|
|
9553
|
+
const matchIndex = pairedToolResultIndex(call, remaining);
|
|
9554
|
+
if (matchIndex < 0) return call;
|
|
9555
|
+
const [result] = remaining.splice(matchIndex, 1);
|
|
9556
|
+
return { ...call, result };
|
|
9557
|
+
});
|
|
9558
|
+
}
|
|
9559
|
+
|
|
9560
|
+
function pairedToolResultIndex(call, results) {
|
|
9561
|
+
const callId = normalizedToolId(call?.id);
|
|
9562
|
+
if (callId) {
|
|
9563
|
+
const idMatch = results.findIndex((result) => normalizedToolId(result?.id) === callId);
|
|
9564
|
+
if (idMatch >= 0) return idMatch;
|
|
9565
|
+
}
|
|
9566
|
+
const callKind = normalizeToolToken(call?.kind || call?.name || call?.title || '');
|
|
9567
|
+
if (callKind) {
|
|
9568
|
+
const kindMatch = results.findIndex((result) => normalizeToolToken(result?.name || result?.kind || result?.title || '') === callKind);
|
|
9569
|
+
if (kindMatch >= 0) return kindMatch;
|
|
9570
|
+
}
|
|
9571
|
+
return results.length ? 0 : -1;
|
|
9572
|
+
}
|
|
9573
|
+
|
|
9574
|
+
function toolStackSummary(tools) {
|
|
9575
|
+
const counts = {};
|
|
9576
|
+
for (const tool of tools) {
|
|
9577
|
+
const category = normalizedToolCategory(tool.category, tool.kind);
|
|
9578
|
+
counts[category] = (counts[category] || 0) + 1;
|
|
9579
|
+
}
|
|
9580
|
+
const parts = [];
|
|
9581
|
+
if (counts.read) parts.push('explored ' + counts.read + ' ' + (counts.read === 1 ? 'file' : 'files'));
|
|
9582
|
+
if (counts.search) parts.push('searched ' + counts.search + ' ' + (counts.search === 1 ? 'time' : 'times'));
|
|
9583
|
+
if (counts.shell) parts.push('ran ' + counts.shell + ' ' + (counts.shell === 1 ? 'command' : 'commands'));
|
|
9584
|
+
if (counts.edit) parts.push('edited ' + counts.edit + ' ' + (counts.edit === 1 ? 'file' : 'files'));
|
|
9585
|
+
const known = ['read', 'search', 'shell', 'edit'];
|
|
9586
|
+
const other = Object.entries(counts).filter(([key]) => !known.includes(key)).reduce((sum, [, count]) => sum + count, 0);
|
|
9587
|
+
if (other) parts.push('used ' + other + ' ' + (other === 1 ? 'tool' : 'tools'));
|
|
9588
|
+
const text = parts.join(', ');
|
|
9589
|
+
return text ? text.charAt(0).toUpperCase() + text.slice(1) : '';
|
|
9590
|
+
}
|
|
9591
|
+
|
|
9592
|
+
function dominantToolCategory(tools) {
|
|
9593
|
+
const counts = {};
|
|
9594
|
+
for (const tool of tools || []) {
|
|
9595
|
+
const category = normalizedToolCategory(tool.category, tool.kind);
|
|
9596
|
+
counts[category] = (counts[category] || 0) + 1;
|
|
9597
|
+
}
|
|
9598
|
+
return Object.entries(counts).sort((a, b) => b[1] - a[1])[0]?.[0] || 'tool';
|
|
7600
9599
|
}
|
|
7601
9600
|
|
|
7602
9601
|
function toolCallFromEvent(event) {
|
|
@@ -7615,7 +9614,8 @@ function toolCallFromEvent(event) {
|
|
|
7615
9614
|
categoryLabel: meta.categoryLabel || '',
|
|
7616
9615
|
icon: meta.icon || event.indexed?.toolIcon || '',
|
|
7617
9616
|
rawCategory: meta.rawCategory || '',
|
|
7618
|
-
id: meta.id || '',
|
|
9617
|
+
id: meta.id || meta.callId || meta.call_id || meta.toolCallId || meta.tool_call_id || meta.toolUseId || meta.tool_use_id || '',
|
|
9618
|
+
eventId: event.eventId || '',
|
|
7619
9619
|
arguments: meta.arguments || null
|
|
7620
9620
|
});
|
|
7621
9621
|
}
|
|
@@ -7682,6 +9682,10 @@ function normalizeToolToken(value) {
|
|
|
7682
9682
|
return String(value || '').trim().toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '');
|
|
7683
9683
|
}
|
|
7684
9684
|
|
|
9685
|
+
function normalizedToolId(value) {
|
|
9686
|
+
return String(value || '').trim().toLowerCase();
|
|
9687
|
+
}
|
|
9688
|
+
|
|
7685
9689
|
function toolCategoryLabel(category) {
|
|
7686
9690
|
const labels = {
|
|
7687
9691
|
shell: 'Shell',
|
|
@@ -7710,18 +9714,92 @@ function escClass(value) {
|
|
|
7710
9714
|
function renderToolCallout(tool) {
|
|
7711
9715
|
const card = toolCard(tool);
|
|
7712
9716
|
const isSkill = card.category === 'skill' || card.kind === 'Skill';
|
|
7713
|
-
const title = card.title || (isSkill ? 'Skill loaded' : card.kind);
|
|
7714
|
-
const category = card.category ? '<span class="tool-chip">' + esc(card.categoryLabel || card.category) + '</span>' : '';
|
|
7715
|
-
const statusClass = humanToolStatusClass(card.status);
|
|
7716
|
-
const status = card.status ? '<span class="tool-status' + (statusClass ? ' ' + statusClass : '') + '">' + esc(humanToolStatus(card.status)) + '</span>' : '';
|
|
7717
|
-
const target = card.target ? '<span class="tool-target">' + esc(card.target) + '</span>' : '';
|
|
7718
9717
|
const diff = renderToolDiff(card);
|
|
7719
|
-
const
|
|
7720
|
-
|
|
7721
|
-
|
|
7722
|
-
|
|
7723
|
-
'
|
|
7724
|
-
|
|
9718
|
+
const category = normalizedToolCategory(card.category, card.kind);
|
|
9719
|
+
return '<details class="tool-callout ' + escClass(category) + (isSkill ? ' skill' : '') + (card.result ? ' has-result' : '') + '" data-category="' + esc(escClass(category)) + '">' +
|
|
9720
|
+
renderToolActivitySummary(card) +
|
|
9721
|
+
renderToolCalloutBody(card, diff) +
|
|
9722
|
+
'</details>';
|
|
9723
|
+
}
|
|
9724
|
+
|
|
9725
|
+
function renderToolActivitySummary(card) {
|
|
9726
|
+
const label = toolActivityLabel(card);
|
|
9727
|
+
const count = card.result?.count || '';
|
|
9728
|
+
const statusClass = humanToolStatusClass(card.status);
|
|
9729
|
+
const status = !card.result && card.status ? '<span class="tool-status' + (statusClass ? ' ' + statusClass : '') + '">' + esc(humanToolStatus(card.status)) + '</span>' : '';
|
|
9730
|
+
return '<summary><span class="tool-glyph">' + renderToolIcon(card) + '</span>' +
|
|
9731
|
+
'<span class="tool-call-line"><span class="tool-action">' + esc(label.action) + '</span>' +
|
|
9732
|
+
(label.subject ? '<span class="tool-subject">' + esc(label.subject) + '</span>' : '') +
|
|
9733
|
+
'</span>' + status +
|
|
9734
|
+
(count ? '<span class="tool-result-count">' + esc(count) + '</span>' : '') +
|
|
9735
|
+
'</summary>';
|
|
9736
|
+
}
|
|
9737
|
+
|
|
9738
|
+
function renderToolCalloutBody(card, diff) {
|
|
9739
|
+
const rows = [];
|
|
9740
|
+
const meta = [];
|
|
9741
|
+
if (card.categoryLabel) meta.push(card.categoryLabel);
|
|
9742
|
+
if (card.status) meta.push(humanToolStatus(card.status));
|
|
9743
|
+
if (card.target) meta.push(card.target);
|
|
9744
|
+
if (meta.length) rows.push('<div class="tool-call-meta">' + meta.map((item) => '<span class="tool-chip">' + esc(item) + '</span>').join('') + '</div>');
|
|
9745
|
+
const preview = card.resultOnly ? '' : String(card.inputPreview || card.argument || '').trim();
|
|
9746
|
+
if (preview) rows.push('<pre class="tool-preview">' + esc(preview) + '</pre>');
|
|
9747
|
+
if (diff) rows.push(diff);
|
|
9748
|
+
if (card.result) rows.push(renderPairedToolResult(card.result));
|
|
9749
|
+
return rows.length ? '<div class="tool-callout-body">' + rows.join('') + '</div>' : '';
|
|
9750
|
+
}
|
|
9751
|
+
|
|
9752
|
+
function renderPairedToolResult(result) {
|
|
9753
|
+
const category = normalizedToolCategory(result.category, result.kind);
|
|
9754
|
+
const meta = [result.kind, result.detail, result.count].filter(Boolean);
|
|
9755
|
+
return '<div class="tool-paired-result" data-category="' + esc(escClass(category)) + '">' +
|
|
9756
|
+
(meta.length ? '<div class="tool-result-meta">' + meta.map((item) => '<span>' + esc(item) + '</span>').join('') + '</div>' : '') +
|
|
9757
|
+
renderToolOutput(result.output || '', { lineStart: result.lineStart }) + '</div>';
|
|
9758
|
+
}
|
|
9759
|
+
|
|
9760
|
+
function toolActivityLabel(card) {
|
|
9761
|
+
const category = normalizedToolCategory(card.category, card.kind);
|
|
9762
|
+
const fallback = toolSubjectFallback(card);
|
|
9763
|
+
if (category === 'shell') return { action: 'Ran', subject: shellCommandText(card) || fallback };
|
|
9764
|
+
if (category === 'read') return { action: 'Read', subject: compactPathLabel(card.target || fallback) };
|
|
9765
|
+
if (category === 'search') return { action: 'Searched', subject: fallback };
|
|
9766
|
+
if (category === 'edit') return { action: 'Edited', subject: compactPathLabel(card.target || fallback) };
|
|
9767
|
+
if (category === 'web') return { action: webToolVerb(card), subject: fallback };
|
|
9768
|
+
if (category === 'task') return { action: 'Ran', subject: fallback };
|
|
9769
|
+
if (category === 'skill') return { action: 'Loaded', subject: fallback };
|
|
9770
|
+
if (category === 'mcp') return { action: 'Called', subject: fallback };
|
|
9771
|
+
return { action: card.title || card.kind || 'Used', subject: fallback && fallback !== (card.title || card.kind) ? fallback : '' };
|
|
9772
|
+
}
|
|
9773
|
+
|
|
9774
|
+
function shellCommandText(card) {
|
|
9775
|
+
const args = card.arguments && typeof card.arguments === 'object' && !Array.isArray(card.arguments) ? card.arguments : {};
|
|
9776
|
+
return firstToolString(args.cmd, args.command, args.script, card.inputPreview, card.argument);
|
|
9777
|
+
}
|
|
9778
|
+
|
|
9779
|
+
function webToolVerb(card) {
|
|
9780
|
+
const key = normalizeToolToken(card.kind || card.title || '');
|
|
9781
|
+
if (key.includes('fetch')) return 'Fetched';
|
|
9782
|
+
if (key.includes('search')) return 'Searched';
|
|
9783
|
+
if (key.includes('open')) return 'Opened';
|
|
9784
|
+
return 'Opened';
|
|
9785
|
+
}
|
|
9786
|
+
|
|
9787
|
+
function toolSubjectFallback(card) {
|
|
9788
|
+
return firstToolString(card.target, card.inputPreview, card.argument, card.title, card.kind);
|
|
9789
|
+
}
|
|
9790
|
+
|
|
9791
|
+
function compactPathLabel(value) {
|
|
9792
|
+
const text = String(value || '').trim();
|
|
9793
|
+
if (!text || /\s/.test(text) || !text.includes('/')) return text;
|
|
9794
|
+
return text.split('/').filter(Boolean).pop() || text;
|
|
9795
|
+
}
|
|
9796
|
+
|
|
9797
|
+
function firstToolString() {
|
|
9798
|
+
for (const value of arguments) {
|
|
9799
|
+
if (typeof value === 'string' && value.trim()) return value.trim();
|
|
9800
|
+
if (Array.isArray(value) && value.length) return value.map((item) => String(item || '').trim()).filter(Boolean).join(' ');
|
|
9801
|
+
}
|
|
9802
|
+
return '';
|
|
7725
9803
|
}
|
|
7726
9804
|
|
|
7727
9805
|
function hasPatchMarker(value) {
|
|
@@ -7818,7 +9896,7 @@ function renderToolDiff(card) {
|
|
|
7818
9896
|
if (adds) summaryParts.push('<span class="add-count">+' + adds + '</span>');
|
|
7819
9897
|
if (dels) summaryParts.push('<span class="del-count">-' + dels + '</span>');
|
|
7820
9898
|
const summaryLabel = editCount > 1 ? editCount + ' edits' : label;
|
|
7821
|
-
return '<details class="tool-diff"
|
|
9899
|
+
return '<details class="tool-diff">' +
|
|
7822
9900
|
'<summary class="tool-diff-summary">' + esc(summaryLabel) + summaryParts.join('') + '</summary>' +
|
|
7823
9901
|
'<div class="tool-diff-body">' + blocks.join('') + '</div>' +
|
|
7824
9902
|
'</details>';
|
|
@@ -7910,6 +9988,7 @@ function contextIconSvg(kind) {
|
|
|
7910
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>',
|
|
7911
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>',
|
|
7912
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>',
|
|
7913
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>',
|
|
7914
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>'
|
|
7915
9994
|
};
|
|
@@ -7948,36 +10027,89 @@ function parseToolResult(content, message) {
|
|
|
7948
10027
|
const event = canonicalEventsForMessage(message, 'tool.completed')[0];
|
|
7949
10028
|
if (event) {
|
|
7950
10029
|
const result = toolResultFromMetadata(event.body?.toolResult || {}, event.indexed || {}, event.body?.text || content);
|
|
7951
|
-
if (result) return result;
|
|
10030
|
+
if (result) return refineViewerToolResult(result, event.body?.text || content);
|
|
7952
10031
|
}
|
|
7953
10032
|
if (message?.metadata?.toolResult) {
|
|
7954
10033
|
const result = toolResultFromMetadata(message.metadata.toolResult, {}, content);
|
|
7955
|
-
if (result) return result;
|
|
10034
|
+
if (result) return refineViewerToolResult(result, content);
|
|
7956
10035
|
}
|
|
7957
10036
|
const text = String(content || '').trim();
|
|
7958
10037
|
if (!text) return null;
|
|
7959
|
-
const structured = parseFileViewResult(text) || parseCommandResult(text) || parseSkillResult(text) || parseSearchResult(text);
|
|
10038
|
+
const structured = parseChatGptFileToolResult(text) || parseFileViewResult(text) || parseCommandResult(text) || parseSkillResult(text) || parseSearchResult(text);
|
|
7960
10039
|
if (structured) return structured;
|
|
7961
10040
|
return genericToolResult(text);
|
|
7962
10041
|
}
|
|
7963
10042
|
|
|
7964
10043
|
function toolResultFromMetadata(result, indexed, fallbackText) {
|
|
7965
|
-
const output = String(result.output || result.text || result.content || fallbackText || '').trimEnd();
|
|
7966
|
-
|
|
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;
|
|
7967
10047
|
const category = normalizedToolCategory(result.category || indexed.toolCategory || result.rawCategory || '', result.kind || indexed.title || '');
|
|
7968
10048
|
return {
|
|
10049
|
+
id: result.id || result.callId || result.call_id || result.toolCallId || result.tool_call_id || result.toolUseId || result.tool_use_id || '',
|
|
10050
|
+
name: result.name || result.toolName || indexed.toolName || '',
|
|
10051
|
+
rawCategory: result.rawCategory || '',
|
|
7969
10052
|
header: 'Tool result' + (result.title || indexed.title ? ' · ' + (result.title || indexed.title) : ''),
|
|
7970
10053
|
kind: result.kind || indexed.title || 'Tool output',
|
|
7971
10054
|
category,
|
|
7972
10055
|
categoryLabel: result.categoryLabel || toolCategoryLabel(category),
|
|
7973
10056
|
icon: result.icon || indexed.toolIcon || toolIcon(category, result.kind || indexed.title || ''),
|
|
7974
|
-
detail:
|
|
7975
|
-
count: result.lineCount ? result.lineCount + ' line' + (Number(result.lineCount) === 1 ? '' : 's') : lineCountLabel(output ||
|
|
7976
|
-
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 || '',
|
|
10060
|
+
lineStart: Number(result.startLine || result.start_line || result.lineStart || result.line_start || 0) || 0,
|
|
7977
10061
|
collapsed: Boolean(result.collapsed) || String(output).split('\\n').length > 18
|
|
7978
10062
|
};
|
|
7979
10063
|
}
|
|
7980
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
|
+
|
|
7981
10113
|
function parseFileViewResult(text) {
|
|
7982
10114
|
const match = text.match(/^<file-view\\b([^>]*)>\\n?([\\s\\S]*?)(?:\\n?<\\/file-view>\\s*)?$/);
|
|
7983
10115
|
if (!match) return null;
|
|
@@ -7993,6 +10125,7 @@ function parseFileViewResult(text) {
|
|
|
7993
10125
|
detail: [attrs.path || basename, lineLabel].filter(Boolean).join(' · '),
|
|
7994
10126
|
count: lineCountLabel(code),
|
|
7995
10127
|
output: code,
|
|
10128
|
+
lineStart: Number(attrs.start_line || 0) || 0,
|
|
7996
10129
|
collapsed: code.split('\\n').length > 24
|
|
7997
10130
|
};
|
|
7998
10131
|
}
|
|
@@ -8050,14 +10183,25 @@ function genericToolResult(text) {
|
|
|
8050
10183
|
};
|
|
8051
10184
|
}
|
|
8052
10185
|
|
|
8053
|
-
function renderToolResult(result) {
|
|
8054
|
-
const
|
|
10186
|
+
function renderToolResult(result, options) {
|
|
10187
|
+
const inline = Boolean(options && options.inline);
|
|
8055
10188
|
const category = normalizedToolCategory(result.category, result.kind);
|
|
8056
|
-
return '<details class="tool-result" data-category="' + esc(escClass(category)) + '"
|
|
10189
|
+
return '<details class="tool-result' + (inline ? ' inline' : '') + '" data-category="' + esc(escClass(category)) + '">' +
|
|
8057
10190
|
'<summary><span class="tool-result-kind"><span class="tool-glyph">' + renderToolIcon({ icon: result.icon, category, kind: result.kind }) + '</span>' + esc(result.kind) + '</span>' +
|
|
8058
10191
|
(result.detail ? '<span class="tool-result-detail">' + esc(result.detail) + '</span>' : '') +
|
|
8059
10192
|
(result.count ? '<span class="tool-result-count">' + esc(result.count) + '</span>' : '') +
|
|
8060
|
-
'</summary
|
|
10193
|
+
'</summary>' + renderToolOutput(result.output || '', { lineStart: result.lineStart }) + '</details>';
|
|
10194
|
+
}
|
|
10195
|
+
|
|
10196
|
+
function renderToolOutput(output, options = {}) {
|
|
10197
|
+
const text = formatChatGptCitationMarkersForPlainText(String(output || ''));
|
|
10198
|
+
const lines = text.split('\\n');
|
|
10199
|
+
if (lines.length <= 1) return '<pre class="tool-output">' + esc(text) + '</pre>';
|
|
10200
|
+
const start = Number(options.lineStart || 0) || 1;
|
|
10201
|
+
return '<div class="tool-output tool-output-lines">' + lines.map((line, index) =>
|
|
10202
|
+
'<div class="tool-output-line"><span class="tool-line-number">' + esc(start + index) + '</span><span class="tool-line-text">' +
|
|
10203
|
+
(line ? esc(line) : ' ') + '</span></div>'
|
|
10204
|
+
).join('') + '</div>';
|
|
8061
10205
|
}
|
|
8062
10206
|
|
|
8063
10207
|
function parseXmlishAttrs(value) {
|
|
@@ -8243,6 +10387,13 @@ function renderInline(value) {
|
|
|
8243
10387
|
function renderInlinePlain(value) {
|
|
8244
10388
|
let html = esc(value);
|
|
8245
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
|
+
});
|
|
8246
10397
|
html = html.replace(/\\[REDACTED(?::|\\s+)([^\\]\\n]+)\\]/g, function (_, kind) {
|
|
8247
10398
|
const index = redactions.length;
|
|
8248
10399
|
const label = redactionLabel(kind);
|
|
@@ -8256,9 +10407,49 @@ function renderInlinePlain(value) {
|
|
|
8256
10407
|
html = html.replace(/\\u0000REDACTION_(\\d+)\\u0000/g, function (_, index) {
|
|
8257
10408
|
return redactions[Number(index)] || '';
|
|
8258
10409
|
});
|
|
10410
|
+
html = html.replace(/\\u0000CITATION_(\\d+)\\u0000/g, function (_, index) {
|
|
10411
|
+
return citations[Number(index)] || '';
|
|
10412
|
+
});
|
|
8259
10413
|
return html;
|
|
8260
10414
|
}
|
|
8261
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
|
+
|
|
8262
10453
|
function renderMarkdownLink(label, href) {
|
|
8263
10454
|
const value = String(href || '').trim();
|
|
8264
10455
|
if (!/^(https?:|mailto:|file:|\\/|#)/i.test(value)) {
|
|
@@ -8283,7 +10474,47 @@ function compactSkillPath(value) {
|
|
|
8283
10474
|
return '.../' + parts.slice(-2).join('/');
|
|
8284
10475
|
}
|
|
8285
10476
|
|
|
8286
|
-
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;
|
|
8287
10518
|
viewMode = mode;
|
|
8288
10519
|
const markdown = mode === 'markdown';
|
|
8289
10520
|
readableButton.classList.toggle('active', !markdown);
|
|
@@ -8292,9 +10523,15 @@ function setView(mode) {
|
|
|
8292
10523
|
markdownButton.setAttribute('aria-pressed', markdown ? 'true' : 'false');
|
|
8293
10524
|
readableView.style.display = markdown ? 'none' : 'block';
|
|
8294
10525
|
markdownView.style.display = markdown ? 'block' : 'none';
|
|
8295
|
-
|
|
8296
|
-
|
|
8297
|
-
|
|
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
|
+
}
|
|
8298
10535
|
}
|
|
8299
10536
|
|
|
8300
10537
|
async function ensureMarkdownLoaded() {
|
|
@@ -8443,7 +10680,8 @@ function treeSignature(payload) {
|
|
|
8443
10680
|
const sessions = (group.sessions || []).map((session) => session.session_id + '=' + (session.updated_at || session.ended_at || session.started_at || '')).join('|');
|
|
8444
10681
|
return (group.repo_key || '') + '#' + (group.count || 0) + ':' + sessions;
|
|
8445
10682
|
}).join('||');
|
|
8446
|
-
|
|
10683
|
+
const sourceSig = (payload.available_source_options || []).map((option) => option.value || '').join(',');
|
|
10684
|
+
return groupSig + '::' + (payload.count || 0) + '::sources=' + sourceSig;
|
|
8447
10685
|
}
|
|
8448
10686
|
|
|
8449
10687
|
function startLiveRefresh() {
|
|
@@ -8641,14 +10879,27 @@ recentButton.onclick = () => {
|
|
|
8641
10879
|
setSelectValue('scope', 'all');
|
|
8642
10880
|
loadTree().catch((error) => { setEmptySession(error.message); });
|
|
8643
10881
|
};
|
|
8644
|
-
readableButton.onclick = () => setView('readable');
|
|
8645
|
-
markdownButton.onclick = () => setView('markdown');
|
|
10882
|
+
readableButton.onclick = () => setView('readable', { preserveScroll: true });
|
|
10883
|
+
markdownButton.onclick = () => setView('markdown', { preserveScroll: true });
|
|
8646
10884
|
copyDetailsButton.onclick = () => {
|
|
8647
10885
|
copySessionDetails();
|
|
8648
10886
|
};
|
|
8649
10887
|
copyResumeButton.onclick = () => {
|
|
8650
10888
|
copyResumeCommand();
|
|
8651
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
|
+
});
|
|
8652
10903
|
jumpEnd.onclick = () => {
|
|
8653
10904
|
const scroller = document.querySelector('.detail-scroll');
|
|
8654
10905
|
scroller.scrollTo({ top: scroller.scrollHeight, behavior: 'smooth' });
|
|
@@ -8710,7 +10961,7 @@ setupStatsActivityControls();
|
|
|
8710
10961
|
setupStatsBreakdownControls();
|
|
8711
10962
|
|
|
8712
10963
|
async function loadStats() {
|
|
8713
|
-
const elements = ['chartTokensPerDay', 'chartSessionsPerDay', 'chartTokensPerRepo', 'chartSessionsPerRepo', 'statsAgentHeatmap', 'statsChatHeatmap'].map((id) => document.getElementById(id));
|
|
10964
|
+
const elements = ['chartTokensPerDay', 'chartSessionsPerDay', 'chartTokensPerRepo', 'chartSessionsPerRepo', 'statsAgentHeatmap', 'statsChatHeatmap', 'statsSdkHeatmap'].map((id) => document.getElementById(id));
|
|
8714
10965
|
for (const el of elements) {
|
|
8715
10966
|
if (el) {
|
|
8716
10967
|
el.classList.add('stats-empty');
|
|
@@ -8879,7 +11130,7 @@ function statsBreakdownGroupId(entry) {
|
|
|
8879
11130
|
}
|
|
8880
11131
|
|
|
8881
11132
|
function emptyStatsTotals() {
|
|
8882
|
-
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 };
|
|
8883
11134
|
}
|
|
8884
11135
|
|
|
8885
11136
|
function addStatsTotals(target, entry) {
|
|
@@ -8888,6 +11139,7 @@ function addStatsTotals(target, entry) {
|
|
|
8888
11139
|
target.tokens_input += Number(entry.tokens_input || 0);
|
|
8889
11140
|
target.tokens_output += Number(entry.tokens_output || 0);
|
|
8890
11141
|
target.tokens_cache += Number(entry.tokens_cache || 0);
|
|
11142
|
+
target.tokens_reasoning += Number(entry.tokens_reasoning || 0);
|
|
8891
11143
|
target.tokens_estimated += Number(entry.tokens_estimated || 0);
|
|
8892
11144
|
target.conversations += Number(entry.conversations ?? entry.sessions ?? 0);
|
|
8893
11145
|
target.user_messages += Number(entry.user_messages || 0);
|
|
@@ -8972,7 +11224,7 @@ function statsRepoRowsForRange(payload) {
|
|
|
8972
11224
|
for (const row of Array.isArray(day.repos) ? day.repos : []) {
|
|
8973
11225
|
const key = row.repo_key || row.repo_display || 'unknown';
|
|
8974
11226
|
if (!repos.has(key)) {
|
|
8975
|
-
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 });
|
|
8976
11228
|
}
|
|
8977
11229
|
const target = repos.get(key);
|
|
8978
11230
|
target.repo_display = row.repo_display || target.repo_display;
|
|
@@ -9003,8 +11255,9 @@ function renderStatsDailyCharts() {
|
|
|
9003
11255
|
if (empty2) { empty2.classList.add('stats-empty'); empty2.textContent = 'No days in this range.'; }
|
|
9004
11256
|
return;
|
|
9005
11257
|
}
|
|
9006
|
-
|
|
9007
|
-
renderDailyChart('
|
|
11258
|
+
const canonicalGroups = statsCanonicalOrderedGroups(groups);
|
|
11259
|
+
renderDailyChart('chartTokensPerDay', densified, canonicalGroups, usageMetric, 336);
|
|
11260
|
+
renderDailyChart('chartSessionsPerDay', densified, canonicalGroups, activityMetric, 336);
|
|
9008
11261
|
}
|
|
9009
11262
|
|
|
9010
11263
|
function renderStats(payload) {
|
|
@@ -9019,12 +11272,13 @@ function renderStats(payload) {
|
|
|
9019
11272
|
renderStatsLegend(groups, breakdownTotals);
|
|
9020
11273
|
renderStatsDailyCharts();
|
|
9021
11274
|
const repoRows = statsRepoRowsForRange(payload);
|
|
9022
|
-
|
|
9023
|
-
renderRepoChart('
|
|
11275
|
+
const canonicalGroupsForRepo = statsCanonicalOrderedGroups(groups);
|
|
11276
|
+
renderRepoChart('chartTokensPerRepo', repoRows, canonicalGroupsForRepo, statsTokenMetric());
|
|
11277
|
+
renderRepoChart('chartSessionsPerRepo', repoRows, canonicalGroupsForRepo, statsActivityMetric());
|
|
9024
11278
|
}
|
|
9025
11279
|
|
|
9026
11280
|
function statsTokenMetric() {
|
|
9027
|
-
if (statsShowInputTokens && statsShowOutputTokens &&
|
|
11281
|
+
if (statsShowInputTokens && statsShowOutputTokens && statsShowCacheTokens) return 'tokens';
|
|
9028
11282
|
if (statsShowInputTokens && !statsShowOutputTokens && !statsShowCacheTokens) return 'tokens_input';
|
|
9029
11283
|
if (!statsShowInputTokens && statsShowOutputTokens && !statsShowCacheTokens) return 'tokens_output';
|
|
9030
11284
|
if (!statsShowInputTokens && !statsShowOutputTokens && statsShowCacheTokens) return 'tokens_cache';
|
|
@@ -9053,7 +11307,7 @@ function statsMetricValue(entry, metric) {
|
|
|
9053
11307
|
}
|
|
9054
11308
|
|
|
9055
11309
|
function isTokenMetric(metric) {
|
|
9056
|
-
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';
|
|
9057
11311
|
}
|
|
9058
11312
|
|
|
9059
11313
|
function statsActivityMetric() {
|
|
@@ -9391,7 +11645,12 @@ function renderStatsMetrics(payload) {
|
|
|
9391
11645
|
const totalInTok = Number(payload.total_input_tokens || 0);
|
|
9392
11646
|
const totalOutTok = Number(payload.total_output_tokens || 0);
|
|
9393
11647
|
const totalCacheTok = Number(payload.total_cache_tokens || 0);
|
|
11648
|
+
const totalReasonTok = Number(payload.total_reasoning_tokens || 0);
|
|
9394
11649
|
const totalEstimatedTok = Number(payload.total_estimated_tokens || 0);
|
|
11650
|
+
const sdkSessions = Number(payload.sdk_session_count || 0);
|
|
11651
|
+
const sdkTokens = Number(payload.sdk_total_tokens || 0);
|
|
11652
|
+
const sdkInputTokens = Number(payload.sdk_total_input_tokens || 0);
|
|
11653
|
+
const sdkOutputTokens = Number(payload.sdk_total_output_tokens || 0);
|
|
9395
11654
|
const um = Number(payload.user_message_count || 0);
|
|
9396
11655
|
const tm = Number(payload.message_count || 0);
|
|
9397
11656
|
const avgMsgs = Number(payload.avg_messages_per_conversation);
|
|
@@ -9401,6 +11660,7 @@ function renderStatsMetrics(payload) {
|
|
|
9401
11660
|
const usageParts = [];
|
|
9402
11661
|
if (totalInTok || totalOutTok) usageParts.push(formatCompactNumber(totalInTok) + ' in / ' + formatCompactNumber(totalOutTok) + ' out');
|
|
9403
11662
|
if (totalCacheTok) usageParts.push(formatCompactNumber(totalCacheTok) + ' cache');
|
|
11663
|
+
if (totalReasonTok) usageParts.push(formatCompactNumber(totalReasonTok) + ' reasoning');
|
|
9404
11664
|
if (totalEstimatedTok) usageParts.push('~' + formatCompactNumber(totalEstimatedTok) + ' estimated');
|
|
9405
11665
|
tokenSub = usageParts.join(' · ');
|
|
9406
11666
|
} else {
|
|
@@ -9416,6 +11676,13 @@ function renderStatsMetrics(payload) {
|
|
|
9416
11676
|
value: formatFullNumber(payload.session_count || 0),
|
|
9417
11677
|
sub: formatFullNumber(payload.agent_session_count || 0) + ' agent · ' + formatFullNumber(payload.chat_session_count || 0) + ' chat'
|
|
9418
11678
|
},
|
|
11679
|
+
sdkSessions || sdkTokens ? {
|
|
11680
|
+
label: 'SDK jobs',
|
|
11681
|
+
value: formatFullNumber(sdkSessions),
|
|
11682
|
+
sub: (sdkTokens ? formatCompactNumber(sdkTokens) + ' tokens' : '0 tokens') +
|
|
11683
|
+
(sdkInputTokens || sdkOutputTokens ? ' · ' + formatCompactNumber(sdkInputTokens) + ' in / ' + formatCompactNumber(sdkOutputTokens) + ' out' : '') +
|
|
11684
|
+
' · kept separate'
|
|
11685
|
+
} : null,
|
|
9419
11686
|
{
|
|
9420
11687
|
label: 'Messages',
|
|
9421
11688
|
value: formatFullNumber(tm),
|
|
@@ -9472,6 +11739,7 @@ function renderStatsMetrics(payload) {
|
|
|
9472
11739
|
}
|
|
9473
11740
|
];
|
|
9474
11741
|
container.innerHTML = metrics
|
|
11742
|
+
.filter(Boolean)
|
|
9475
11743
|
.map(function (metric) {
|
|
9476
11744
|
const vt =
|
|
9477
11745
|
metric.valueHtml == null &&
|
|
@@ -9545,7 +11813,11 @@ function renderHeatmapSection(payload) {
|
|
|
9545
11813
|
renderSecondaryHeatmap(payload.split_stats && payload.split_stats.agent, 'statsAgentActivitySub', 'statsAgentHeatmap', range, metric);
|
|
9546
11814
|
renderSecondaryHeatmap(payload.split_stats && payload.split_stats.chat, 'statsChatActivitySub', 'statsChatHeatmap', range, metric, {
|
|
9547
11815
|
emptySubText: '',
|
|
9548
|
-
emptyText: 'No chat activity yet.
|
|
11816
|
+
emptyText: 'No chat activity yet. Run agentlog import chatgpt or agentlog import claude-web for official export instructions.'
|
|
11817
|
+
});
|
|
11818
|
+
renderSecondaryHeatmap(payload.split_stats && payload.split_stats.sdk, 'statsSdkActivitySub', 'statsSdkHeatmap', range, metric, {
|
|
11819
|
+
emptySubText: '',
|
|
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.'
|
|
9549
11821
|
});
|
|
9550
11822
|
}
|
|
9551
11823
|
|
|
@@ -9780,15 +12052,11 @@ function renderDailyChart(elementId, daily, providers, metric, height) {
|
|
|
9780
12052
|
return '<line class="stats-axis-line" x1="' + padding.left + '" x2="' + (padding.left + innerW) + '" y1="' + y.toFixed(2) + '" y2="' + y.toFixed(2) + '"/>'
|
|
9781
12053
|
+ '<text class="stats-axis-label" x="' + (padding.left - 6) + '" y="' + (y + 3).toFixed(2) + '" text-anchor="end">' + esc(formatCompactNumber(value)) + '</text>';
|
|
9782
12054
|
}).join('');
|
|
9783
|
-
|
|
9784
|
-
|
|
9785
|
-
|
|
9786
|
-
|
|
9787
|
-
|
|
9788
|
-
const cx = padding.left + idx * xStep + xStep / 2;
|
|
9789
|
-
const tick = formatChartMonthTick(day.date);
|
|
9790
|
-
return '<text class="stats-axis-label" x="' + cx.toFixed(2) + '" y="' + (padding.top + innerH + 18) + '" text-anchor="middle">' + esc(tick) + '</text>';
|
|
9791
|
-
}).join('');
|
|
12055
|
+
const xLabels = dailyChartMonthTicks(visibleDaily, xStep, padding.left, innerW)
|
|
12056
|
+
.map((tickEntry) =>
|
|
12057
|
+
'<text class="stats-axis-label" x="' + tickEntry.x.toFixed(2) + '" y="' + (padding.top + innerH + 18) + '" text-anchor="middle">' + esc(tickEntry.label) + '</text>'
|
|
12058
|
+
)
|
|
12059
|
+
.join('');
|
|
9792
12060
|
const hits = visibleDaily.map((day, dayIdx) => {
|
|
9793
12061
|
const xHit = padding.left + dayIdx * xStep;
|
|
9794
12062
|
return '<rect class="stats-chart-hit" pointer-events="all" x="' + xHit.toFixed(2) + '" y="' + padding.top + '" width="' + xStep.toFixed(2) + '" height="' + innerH.toFixed(2) + '" fill="transparent" data-day-index="' + dayIdx + '"/>';
|
|
@@ -9800,6 +12068,33 @@ function renderDailyChart(elementId, daily, providers, metric, height) {
|
|
|
9800
12068
|
bindDailyChartHover(el);
|
|
9801
12069
|
}
|
|
9802
12070
|
|
|
12071
|
+
function dailyChartMonthTicks(daily, xStep, left, innerW) {
|
|
12072
|
+
const monthStarts = [];
|
|
12073
|
+
let prevYm = '';
|
|
12074
|
+
for (let idx = 0; idx < daily.length; idx += 1) {
|
|
12075
|
+
const day = daily[idx];
|
|
12076
|
+
const ym = String(day?.date || '').slice(0, 7);
|
|
12077
|
+
if (!/^\\d{4}-\\d{2}$/.test(ym) || ym === prevYm) continue;
|
|
12078
|
+
prevYm = ym;
|
|
12079
|
+
monthStarts.push({ idx, date: day.date, ym });
|
|
12080
|
+
}
|
|
12081
|
+
const maxLabels = Math.max(2, Math.floor(innerW / 58));
|
|
12082
|
+
const step = Math.max(1, Math.ceil(monthStarts.length / maxLabels));
|
|
12083
|
+
const selected = [];
|
|
12084
|
+
for (let idx = 0; idx < monthStarts.length; idx += step) selected.push(monthStarts[idx]);
|
|
12085
|
+
const last = monthStarts[monthStarts.length - 1];
|
|
12086
|
+
if (last && selected[selected.length - 1] !== last) {
|
|
12087
|
+
const previous = selected[selected.length - 1];
|
|
12088
|
+
if (previous && monthStarts.indexOf(previous) >= monthStarts.length - step) selected[selected.length - 1] = last;
|
|
12089
|
+
else selected.push(last);
|
|
12090
|
+
}
|
|
12091
|
+
return selected
|
|
12092
|
+
.map((entry) => ({
|
|
12093
|
+
x: left + entry.idx * xStep + xStep / 2,
|
|
12094
|
+
label: formatChartMonthTick(entry.date)
|
|
12095
|
+
}));
|
|
12096
|
+
}
|
|
12097
|
+
|
|
9803
12098
|
function trimEmptyDailyChartEdges(daily, providers, metric) {
|
|
9804
12099
|
const rows = Array.isArray(daily) ? daily : [];
|
|
9805
12100
|
const hasValue = (day) => providers.some((provider) => statsMetricValue(day?.providers?.[provider], metric) > 0);
|
|
@@ -9909,7 +12204,7 @@ function setupCustomSelects() {
|
|
|
9909
12204
|
const input = field.querySelector('input');
|
|
9910
12205
|
const trigger = field.querySelector('.select-trigger');
|
|
9911
12206
|
if (field.dataset.select === 'provider') hydrateProviderSelectOptions(field);
|
|
9912
|
-
const options =
|
|
12207
|
+
const options = bindSelectFieldOptions(field);
|
|
9913
12208
|
const selectedOption = options.find((item) => (item.dataset.value || '') === input.value) || options.find((item) => item.classList.contains('active')) || options[0];
|
|
9914
12209
|
if (selectedOption) setSelectTriggerLabel(field, selectedOption);
|
|
9915
12210
|
updateSelectFieldState(field);
|
|
@@ -9920,23 +12215,65 @@ function setupCustomSelects() {
|
|
|
9920
12215
|
}
|
|
9921
12216
|
field.classList.toggle('open');
|
|
9922
12217
|
};
|
|
9923
|
-
for (const option of options) {
|
|
9924
|
-
option.onclick = (event) => {
|
|
9925
|
-
event.stopPropagation();
|
|
9926
|
-
input.value = option.dataset.value || '';
|
|
9927
|
-
setSelectTriggerLabel(field, option);
|
|
9928
|
-
options.forEach((item) => item.classList.toggle('active', item === option));
|
|
9929
|
-
updateSelectFieldState(field);
|
|
9930
|
-
field.classList.remove('open');
|
|
9931
|
-
refreshForFilterChange();
|
|
9932
|
-
};
|
|
9933
|
-
}
|
|
9934
12218
|
}
|
|
9935
12219
|
document.addEventListener('click', () => {
|
|
9936
12220
|
for (const field of document.querySelectorAll('.select-field.open')) field.classList.remove('open');
|
|
9937
12221
|
});
|
|
9938
12222
|
}
|
|
9939
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
|
+
|
|
9940
12277
|
function hydrateProviderSelectOptions(field) {
|
|
9941
12278
|
for (const option of field.querySelectorAll('.select-option')) {
|
|
9942
12279
|
if (!option.dataset.label) option.dataset.label = option.textContent.trim();
|
|
@@ -10017,6 +12354,12 @@ function setupKeyboardShortcuts() {
|
|
|
10017
12354
|
const isInput = target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable);
|
|
10018
12355
|
|
|
10019
12356
|
if (event.key === 'Escape') {
|
|
12357
|
+
if (sessionModal && !sessionModal.hidden) {
|
|
12358
|
+
closeSubagentModal();
|
|
12359
|
+
event.preventDefault();
|
|
12360
|
+
event.stopPropagation();
|
|
12361
|
+
return;
|
|
12362
|
+
}
|
|
10020
12363
|
const searchInput = document.getElementById('q');
|
|
10021
12364
|
if (searchInput && (searchInput.value || activeSearchTerm)) {
|
|
10022
12365
|
searchInput.value = '';
|
|
@@ -10246,8 +12589,8 @@ Start here:
|
|
|
10246
12589
|
Archive and import:
|
|
10247
12590
|
init interactive setup and optional first import
|
|
10248
12591
|
import import local Codex, Claude, Gemini, Devin, Cursor, Cline, OpenCode, Aider, and Antigravity history
|
|
10249
|
-
import chatgpt
|
|
10250
|
-
import claude-web
|
|
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
|
|
10251
12594
|
import windsurf <path> import downloaded Windsurf trajectory Markdown file/folder
|
|
10252
12595
|
import accounts list or rename ChatGPT/Claude.ai export accounts
|
|
10253
12596
|
sync choose a remote, preview, confirm, then upload archive objects
|
|
@@ -10303,8 +12646,8 @@ agentlog import
|
|
|
10303
12646
|
Usage:
|
|
10304
12647
|
agentlog import --source <source> [--since 30d|all]
|
|
10305
12648
|
agentlog import --sources <a,b,c> [--since 30d|all]
|
|
10306
|
-
agentlog import chatgpt
|
|
10307
|
-
agentlog import claude-web
|
|
12649
|
+
agentlog import chatgpt [path] [--username <name>] [--scope local|team]
|
|
12650
|
+
agentlog import claude-web [path] [--username <name>] [--scope local|team]
|
|
10308
12651
|
agentlog import windsurf <file-or-folder>
|
|
10309
12652
|
agentlog import accounts list
|
|
10310
12653
|
agentlog import accounts rename <provider> <account-id-or-username> --display-name <name>
|
|
@@ -10312,6 +12655,7 @@ Usage:
|
|
|
10312
12655
|
Import sources:
|
|
10313
12656
|
codex-cli terminal Codex sessions from Codex state and rollout files
|
|
10314
12657
|
codex-desktop Codex desktop app sessions from Codex state and rollout files
|
|
12658
|
+
codex-sdk high-volume Codex exec/SDK batch jobs; opt-in
|
|
10315
12659
|
claude interactive Claude Code CLI JSONL transcripts
|
|
10316
12660
|
claude-code-desktop Claude Code sessions launched from the Claude desktop app
|
|
10317
12661
|
claude-workspace Claude app workspace/local-agent sessions
|
|
@@ -10329,13 +12673,14 @@ Import sources:
|
|
|
10329
12673
|
all configured default local sources
|
|
10330
12674
|
|
|
10331
12675
|
Web export sources:
|
|
10332
|
-
chatgpt
|
|
10333
|
-
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
|
|
10334
12678
|
windsurf downloaded Cascade trajectory Markdown file/folder
|
|
10335
12679
|
|
|
10336
12680
|
Examples:
|
|
10337
12681
|
agentlog import --source codex-cli --since 30d
|
|
10338
12682
|
agentlog import --source codex-desktop --since all
|
|
12683
|
+
agentlog import --source codex-sdk --since all
|
|
10339
12684
|
agentlog import --source claude --since 30d
|
|
10340
12685
|
agentlog import --source claude-code-desktop --since all
|
|
10341
12686
|
agentlog import --source claude-workspace --since all
|
|
@@ -10351,11 +12696,14 @@ Examples:
|
|
|
10351
12696
|
agentlog import --source cursor --since all --explain-skips
|
|
10352
12697
|
agentlog import --source cursor --since all --explain-skips --json
|
|
10353
12698
|
agentlog import status --json
|
|
12699
|
+
agentlog import chatgpt
|
|
12700
|
+
agentlog import claude-web
|
|
10354
12701
|
agentlog import chatgpt ~/Downloads/chatgpt-export.zip --username you@example.com
|
|
10355
|
-
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
|
|
10356
12704
|
agentlog import windsurf ~/Downloads/cascade-chat-conversation.md
|
|
10357
12705
|
agentlog import windsurf ~/windsurf-cascade-export
|
|
10358
|
-
agentlog import accounts rename claude-web
|
|
12706
|
+
agentlog import accounts rename claude-web you --display-name "Personal Claude"
|
|
10359
12707
|
|
|
10360
12708
|
Details:
|
|
10361
12709
|
--since accepts 30d, 12h, 60m, ISO dates, or all.
|
|
@@ -10363,7 +12711,9 @@ Details:
|
|
|
10363
12711
|
--dry-run shows what would be imported without writing archive files.
|
|
10364
12712
|
--explain-skips includes per-session skip reasons for supported sources.
|
|
10365
12713
|
--json prints machine-readable import results.
|
|
10366
|
-
|
|
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.
|
|
12716
|
+
Windsurf imports prompt for the export path when omitted in a terminal.
|
|
10367
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>\`.
|
|
10368
12718
|
See docs/history-source-handling.md for source-specific storage paths.
|
|
10369
12719
|
`;
|
|
@@ -10489,7 +12839,7 @@ Usage:
|
|
|
10489
12839
|
Examples:
|
|
10490
12840
|
agentlog accounts list
|
|
10491
12841
|
agentlog accounts rename
|
|
10492
|
-
agentlog accounts rename claude-web
|
|
12842
|
+
agentlog accounts rename claude-web you --display-name "Personal Claude"
|
|
10493
12843
|
`,
|
|
10494
12844
|
config: `
|
|
10495
12845
|
agentlog config
|
|
@@ -10723,7 +13073,7 @@ Usage:
|
|
|
10723
13073
|
agentlog update [--yes] [--dry-run] [--since 30d|all] [--sources a,b,c] [--sync] [--no-index] [--no-restart]
|
|
10724
13074
|
|
|
10725
13075
|
What it removes:
|
|
10726
|
-
archive/index data local archive objects and search indexes
|
|
13076
|
+
archive/index data local agent archive objects and search indexes
|
|
10727
13077
|
import state file/session fingerprints so sources are read again
|
|
10728
13078
|
cache/spool reveal cache and pending local spool files
|
|
10729
13079
|
sync state local upload bookkeeping; remote objects are not deleted
|
|
@@ -10732,13 +13082,14 @@ What it keeps:
|
|
|
10732
13082
|
config.json storage, source, sync, privacy, and watcher preferences
|
|
10733
13083
|
redaction.yaml redaction rules
|
|
10734
13084
|
web account labels ChatGPT/Claude.ai account display names
|
|
13085
|
+
web chat archives manually imported ChatGPT/Claude.ai archive objects
|
|
10735
13086
|
source histories Codex, Claude, Gemini, Devin, Cursor, etc. source data
|
|
10736
13087
|
recall integrations agent command/skill files
|
|
10737
13088
|
|
|
10738
13089
|
Options:
|
|
10739
13090
|
--dry-run show targets and preferences without deleting or importing
|
|
10740
13091
|
--yes skip confirmation
|
|
10741
|
-
--since 30d|90d|all import window; defaults to
|
|
13092
|
+
--since 30d|90d|all import window; defaults to the saved rebuild window, then all
|
|
10742
13093
|
--sources a,b,c override configured sources for this run
|
|
10743
13094
|
--sync upload changed objects after reimport; use sync replace to remove stale remote objects
|
|
10744
13095
|
--no-index skip rebuilding the search index
|
|
@@ -10897,13 +13248,19 @@ module.exports = {
|
|
|
10897
13248
|
_historyWebInternals: {
|
|
10898
13249
|
HISTORY_AUTH_COOKIE,
|
|
10899
13250
|
constantTimeEqual,
|
|
13251
|
+
filterShapeKey,
|
|
10900
13252
|
historyHtml,
|
|
10901
13253
|
isHistoryApiPath,
|
|
13254
|
+
listSnapshotEtag,
|
|
13255
|
+
memoizedStatsPayload,
|
|
10902
13256
|
parseCookies,
|
|
10903
13257
|
readSessionMarkdown,
|
|
10904
13258
|
readSessionView,
|
|
13259
|
+
resolveSessionAttachmentFile,
|
|
10905
13260
|
resumeCommandForSession,
|
|
13261
|
+
repoSessionsFilterKey,
|
|
10906
13262
|
securityHeaders,
|
|
13263
|
+
sessionTreePayload,
|
|
10907
13264
|
sessionViewPayload,
|
|
10908
13265
|
sessionCookie,
|
|
10909
13266
|
statsPayload,
|