agentel 0.2.5 → 0.2.6

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/src/cli.js CHANGED
@@ -24,6 +24,7 @@ const { runSupervisorForeground, startSupervisorDetached, stopSupervisor, superv
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
30
  const SESSION_WEB_PAYLOAD_VERSION = 3;
@@ -2185,21 +2186,20 @@ async function importCommand(args, flags, env) {
2185
2186
  printImportResults([result]);
2186
2187
  return;
2187
2188
  }
2188
- if (sub === "claude-web" || sub === "chatgpt") {
2189
- let importFile = flags.file || args[1] || "";
2190
- if (!importFile && process.stdin.isTTY) {
2191
- printSection(`${sub} Export`);
2192
- printMuted("Choose the official export JSON, ZIP, or extracted folder.");
2193
- importFile = (await ask(" Export path: ")).trim();
2189
+ if (sub === "claude-web" || sub === "claude_web" || sub === "chatgpt") {
2190
+ const instructions = webExportInstructions(sub);
2191
+ if (flags.instructions || flags.instruction || flags.docs) {
2192
+ return printWebExportInstructions(instructions, flags);
2194
2193
  }
2195
- if (!importFile) throw new Error(`usage: agentlog import ${sub} <path> --username <name> [--display-name <name>] [--account-id <id>] [--scope local|team]`);
2194
+ let importFile = flags.file || args[1] || "";
2195
+ if (!importFile) return printWebExportInstructions(instructions, flags);
2196
2196
  let username = flags.username || "";
2197
2197
  let displayName = flags["display-name"] || flags.displayName || "";
2198
2198
  if (!username && process.stdin.isTTY) {
2199
2199
  username = (await ask(`${sub} account username/email: `)).trim();
2200
2200
  if (!displayName) displayName = (await ask(`Display name [${username}]: `)).trim() || username;
2201
2201
  }
2202
- const result = importWebChat(sub, path.resolve(importFile), {
2202
+ const result = importWebChat(instructions.source, path.resolve(importFile), {
2203
2203
  scope: flags.scope || "local",
2204
2204
  dryRun: flags["dry-run"],
2205
2205
  username,
@@ -2827,10 +2827,13 @@ function compareHistorySessionsForTree(a, b) {
2827
2827
 
2828
2828
  function statsPayload(filters, env) {
2829
2829
  const sessions = listHistorySessions({ ...filters, limit: 5000 }, env);
2830
- return statsPayloadForSessions(sessions);
2830
+ return statsPayloadForSessions(sessions, { includeSdk: statsFiltersTargetSdk(filters) });
2831
2831
  }
2832
2832
 
2833
2833
  function statsPayloadForSessions(sessions, options = {}) {
2834
+ const allSessions = Array.isArray(sessions) ? sessions : [];
2835
+ const sdkSessions = allSessions.filter(isSdkStatsSession);
2836
+ const statsSessions = options.includeSdk ? allSessions : allSessions.filter((session) => !isSdkStatsSession(session));
2834
2837
  const providerSet = new Set();
2835
2838
  const companySet = new Set();
2836
2839
  const modelGroupSet = new Set();
@@ -2853,7 +2856,7 @@ function statsPayloadForSessions(sessions, options = {}) {
2853
2856
  let totalUserMessages = 0;
2854
2857
  let peakSessionTokens = 0;
2855
2858
  let peakSessionLabel = "";
2856
- for (const session of sessions) {
2859
+ for (const session of statsSessions) {
2857
2860
  const provider = String(session.provider || "unknown");
2858
2861
  const modelGroup = statsSessionPrimaryModel(session);
2859
2862
  const companyGroup = statsSessionCompany(session, provider, modelGroup);
@@ -3114,15 +3117,25 @@ function statsPayloadForSessions(sessions, options = {}) {
3114
3117
  const avgMessagesPerConversation =
3115
3118
  totalConversations > 0 ? totalMessages / totalConversations : 0;
3116
3119
  const splitStats = options.includeSplit === false ? null : {
3117
- agent: statsPayloadForSessions(sessions.filter((session) => statsSessionCategory(session) === "agent"), { includeSplit: false, category: "agent" }),
3118
- chat: statsPayloadForSessions(sessions.filter((session) => statsSessionCategory(session) === "chat"), { includeSplit: false, category: "chat" })
3120
+ agent: statsPayloadForSessions(allSessions.filter((session) => !isSdkStatsSession(session) && statsSessionCategory(session) === "agent"), { includeSplit: false, category: "agent" }),
3121
+ chat: statsPayloadForSessions(allSessions.filter((session) => !isSdkStatsSession(session) && statsSessionCategory(session) === "chat"), { includeSplit: false, category: "chat" }),
3122
+ sdk: statsPayloadForSessions(sdkSessions, { includeSplit: false, includeSdk: true, category: "sdk" })
3119
3123
  };
3124
+ const sdkStats = splitStats?.sdk || null;
3120
3125
  return {
3121
3126
  category: options.category || "all",
3122
3127
  generated_at: new Date().toISOString(),
3123
3128
  session_count: totalConversations,
3124
3129
  agent_session_count: splitStats ? splitStats.agent.session_count : undefined,
3125
3130
  chat_session_count: splitStats ? splitStats.chat.session_count : undefined,
3131
+ sdk_session_count: sdkStats ? sdkStats.session_count : undefined,
3132
+ sdk_message_count: sdkStats ? sdkStats.message_count : undefined,
3133
+ sdk_user_message_count: sdkStats ? sdkStats.user_message_count : undefined,
3134
+ sdk_total_tokens: sdkStats ? sdkStats.total_tokens : undefined,
3135
+ sdk_total_input_tokens: sdkStats ? sdkStats.total_input_tokens : undefined,
3136
+ sdk_total_output_tokens: sdkStats ? sdkStats.total_output_tokens : undefined,
3137
+ sdk_total_cache_tokens: sdkStats ? sdkStats.total_cache_tokens : undefined,
3138
+ sdk_total_estimated_tokens: sdkStats ? sdkStats.total_estimated_tokens : undefined,
3126
3139
  message_count: totalMessages,
3127
3140
  user_message_count: totalUserMessages,
3128
3141
  total_tokens: totalTokens,
@@ -3221,6 +3234,20 @@ function statsSessionCategory(session) {
3221
3234
  return provider === "chatgpt" || provider === "claude_web" ? "chat" : "agent";
3222
3235
  }
3223
3236
 
3237
+ const SDK_STATS_SOURCE_TYPES = new Set(["codex-sdk-history", "claude-sdk-history"]);
3238
+
3239
+ function isSdkStatsSession(session) {
3240
+ const provider = String(session?.provider || "").toLowerCase();
3241
+ const sourceType = String(session?.sourceType || session?.source_type || "").toLowerCase();
3242
+ return provider === "claude_sdk" || SDK_STATS_SOURCE_TYPES.has(sourceType);
3243
+ }
3244
+
3245
+ function statsFiltersTargetSdk(filters = {}) {
3246
+ const provider = String(filters.provider || filters.source || "").trim().toLowerCase().replace(/[-\s]+/g, "_");
3247
+ const sourceType = String(filters.sourceType || filters.source_type || "").trim().toLowerCase();
3248
+ return provider === "codex_sdk" || provider === "claude_sdk" || SDK_STATS_SOURCE_TYPES.has(sourceType);
3249
+ }
3250
+
3224
3251
  function isWebChatStatsProvider(provider) {
3225
3252
  return provider === "chatgpt" || provider === "claude_web";
3226
3253
  }
@@ -3513,7 +3540,7 @@ function serverCommand(flags, env) {
3513
3540
  }
3514
3541
 
3515
3542
  const RECALL_TARGET_SOURCE_MAP = {
3516
- codex: ["codex-cli", "codex-desktop"],
3543
+ codex: ["codex-cli", "codex-desktop", "codex-sdk"],
3517
3544
  claude: ["claude", "claude-code-desktop", "claude-workspace"],
3518
3545
  gemini: ["gemini-cli"],
3519
3546
  antigravity: ["antigravity"],
@@ -4037,7 +4064,7 @@ function recallArchiveHints() {
4037
4064
  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
4065
  - 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
4066
  - 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\`).
4067
+ - 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
4068
  - If the user is asking about the current repository, start without \`--repo\` unless results are noisy; current-repo matches are already weighted higher.`;
4042
4069
  }
4043
4070
 
@@ -4305,6 +4332,10 @@ function printDiscovery(label, result) {
4305
4332
 
4306
4333
  function printImportResults(results, options = {}) {
4307
4334
  for (const result of results) {
4335
+ if (result.instructions) {
4336
+ printWebExportInstructionBlock(result.instructions);
4337
+ continue;
4338
+ }
4308
4339
  const detailText = result.details ? formatDetails(result.details) : "";
4309
4340
  const details = detailText ? ` ${detailText}` : "";
4310
4341
  printCheck(
@@ -4326,6 +4357,37 @@ function printImportResults(results, options = {}) {
4326
4357
  }
4327
4358
  }
4328
4359
 
4360
+ function printWebExportInstructions(instructions, flags = {}) {
4361
+ if (!instructions) throw new Error("unknown web export instruction source");
4362
+ if (flags.json) {
4363
+ console.log(JSON.stringify({
4364
+ provider: instructions.provider,
4365
+ source: instructions.source,
4366
+ manual: true,
4367
+ instructions
4368
+ }, null, 2));
4369
+ return;
4370
+ }
4371
+ printPageTitle("agentlog import", `${instructions.source} export instructions`);
4372
+ printWebExportInstructionBlock(instructions);
4373
+ }
4374
+
4375
+ function printWebExportInstructionBlock(instructions) {
4376
+ printSection(`${instructions.label} Export`);
4377
+ printMuted(`Request and download the ${instructions.fileDescription}, then import it from disk.`);
4378
+ printCheck("Request page", instructions.requestUrl);
4379
+ printCheck("Help", instructions.helpUrl);
4380
+ printSection("Steps");
4381
+ (instructions.steps || []).forEach((step, index) => {
4382
+ console.log(` ${index + 1}. ${step}`);
4383
+ });
4384
+ if (instructions.notes?.length) {
4385
+ printSection("Notes");
4386
+ for (const note of instructions.notes) console.log(` - ${note}`);
4387
+ }
4388
+ printCommand("Import after download", instructions.importCommand);
4389
+ }
4390
+
4329
4391
  function printPageTitle(title, subtitle = "") {
4330
4392
  if (!process.stdout.isTTY) {
4331
4393
  console.log(title);
@@ -5089,6 +5151,14 @@ function importSourceOptions(discovered) {
5089
5151
  description: "Codex desktop app conversations from the local Codex state database.",
5090
5152
  defaultSelected: Boolean(discovered.codexDesktop?.sessions)
5091
5153
  },
5154
+ {
5155
+ source: "codex-sdk",
5156
+ label: "Codex SDK jobs",
5157
+ count: discovered.codexSdk?.sessions || 0,
5158
+ summary: sourceSummary(discovered.codexSdk),
5159
+ description: "Codex exec and SDK-style batch runs from the local Codex state database; disabled by default because volume can be high.",
5160
+ defaultSelected: false
5161
+ },
5092
5162
  {
5093
5163
  source: "claude",
5094
5164
  label: "Claude Code CLI",
@@ -5607,6 +5677,12 @@ mark.search-match.search-match-current{background:#fde047;color:#422006;box-shad
5607
5677
  .unsupported-device-block:last-child{margin-bottom:0}
5608
5678
  .unsupported-device-block:before{content:"";width:6px;height:6px;border-radius:999px;background:#94a3b8;flex:0 0 auto}
5609
5679
  .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}
5680
+ .context-line{display:flex;align-items:center;gap:8px;min-height:28px;margin:2px 0;color:#64748b;font-size:12px;line-height:1.35}
5681
+ .context-line .context-glyph{width:22px;height:22px}
5682
+ .context-line .context-title{flex:0 0 auto}
5683
+ .context-line .context-meta{flex:1 1 auto}
5684
+ .context-line .context-end{margin-left:auto}
5685
+ .context-line .message-copy{opacity:1;width:22px;height:22px;color:#94a3b8}
5610
5686
  .session-summary-message{margin-bottom:14px}
5611
5687
  .session-summary-card{border-left-color:#D97757;background:#fff}
5612
5688
  .session-summary-body{padding:11px 13px 12px;color:#334155;font-size:13px;line-height:1.55;background:#fff}
@@ -5648,13 +5724,31 @@ mark.search-match.search-match-current{background:#fde047;color:#422006;box-shad
5648
5724
  .md-table{border-collapse:collapse;width:max-content;max-width:100%;font-size:13px}
5649
5725
  .md-table th,.md-table td{border:1px solid #e5e7eb;padding:5px 7px;text-align:left;vertical-align:top}
5650
5726
  .md-table th{background:#f8fafc;font-weight:600}
5651
- .tool-callout{display:flex;align-items:flex-start;gap:10px;margin:0;padding:8px 11px 9px 11px;border:1px solid #e5e7eb;border-radius:8px;background:#fff;color:#0f172a;transition:background .15s ease}
5652
- .tool-callout:hover{background:#fafbfc}
5653
- .tool-stack{display:grid;gap:5px;margin-top:4px}
5727
+ .tool-group-card{margin:0;border:0;background:transparent}
5728
+ .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}
5729
+ .tool-group-card > summary::-webkit-details-marker{display:none}
5730
+ .tool-group-prefix{display:inline-flex;align-items:center;gap:7px;flex:0 0 auto}
5731
+ .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}
5732
+ .tool-group-card[open] .tool-group-caret{transform:rotate(90deg)}
5733
+ .tool-group-title{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
5734
+ .tool-group-card[open] > .tool-stack{gap:0;border:1px solid #e5e7eb;border-radius:8px;background:#fff;overflow:hidden}
5735
+ .tool-group-card[open] > .tool-stack > .tool-callout{border:0;border-radius:0;border-bottom:1px solid #eef2f7}
5736
+ .tool-group-card[open] > .tool-stack > .tool-callout:last-child{border-bottom:0}
5737
+ .tool-group-card[open] > .tool-stack > .tool-callout > summary{min-height:26px;padding:2px 8px}
5738
+ .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}
5739
+ .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}
5740
+ .tool-callout > summary::-webkit-details-marker{display:none}
5741
+ .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}
5742
+ .tool-callout[open] > summary:before{transform:rotate(90deg)}
5743
+ .tool-callout:hover,.tool-callout[open]{border-color:#dbe3ef}
5744
+ .tool-callout > summary:hover{background:#fafbfc}
5745
+ .tool-callout[open] > summary{border-bottom:1px solid #f1f5f9}
5746
+ .tool-stack{display:grid;gap:3px;margin-top:3px}
5654
5747
  .tool-stack:first-child{margin-top:0}
5655
5748
  .tool-body + .tool-stack{margin-top:8px}
5656
- .tool-glyph{display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;width:26px;height:26px;border-radius:6px;background:#f1f5f9;color:#475569;border:0}
5657
- .tool-glyph svg{width:14px;height:14px}
5749
+ .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}
5750
+ .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}
5751
+ .tool-glyph svg{width:13px;height:13px}
5658
5752
  .tool-callout.shell .tool-glyph{background:#f1f5f9;color:#334155}
5659
5753
  .tool-callout.edit .tool-glyph{background:#fef3c7;color:#92400e}
5660
5754
  .tool-callout.read .tool-glyph{background:#e0f2fe;color:#075985}
@@ -5663,16 +5757,21 @@ mark.search-match.search-match-current{background:#fde047;color:#422006;box-shad
5663
5757
  .tool-callout.task .tool-glyph{background:#dcfce7;color:#166534}
5664
5758
  .tool-callout.mcp .tool-glyph{background:#ffedd5;color:#9a3412}
5665
5759
  .tool-callout.skill .tool-glyph{background:#e0e7ff;color:#3730a3}
5666
- .tool-copy{display:grid;gap:3px;min-width:0;flex:1 1 auto}
5667
- .tool-title{display:flex;align-items:center;gap:6px;flex-wrap:wrap;font-size:13px;font-weight:600;line-height:1.3;color:#0f172a;letter-spacing:-0.005em}
5760
+ .tool-call-line{display:flex;align-items:baseline;gap:5px;min-width:0;flex:1 1 auto}
5761
+ .tool-action{flex:0 0 auto;color:#0f172a;font-size:12.5px;font-weight:600;line-height:1.3}
5762
+ .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}
5763
+ .tool-callout.shell .tool-subject{font:12px/1.3 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;color:#475569}
5764
+ .tool-callout-body{display:grid;gap:6px;min-width:0;padding:6px 8px 8px 12px;background:#fff}
5765
+ .tool-call-meta{display:flex;align-items:center;gap:5px;flex-wrap:wrap;min-width:0}
5668
5766
  .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
5767
  .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
5768
  .tool-status.completed{background:#dcfce7;color:#166534}
5671
5769
  .tool-status.pending{background:#fef9c3;color:#854d0e}
5672
5770
  .tool-status.failed,.tool-status.error{background:#fee2e2;color:#991b1b}
5673
- .tool-detail{color:#475569;font-size:12px;line-height:1.4;overflow-wrap:anywhere}
5674
- .tool-target{display:block;margin-top:3px;color:#64748b;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px}
5675
- .tool-preview{display:block;color:#1e293b;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;white-space:pre-wrap;overflow-wrap:anywhere}
5771
+ .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}
5772
+ .tool-paired-result{display:grid;gap:4px;min-width:0}
5773
+ .tool-result-meta{display:flex;align-items:center;gap:8px;min-width:0;color:#64748b;font-size:11px;font-weight:600}
5774
+ .tool-result-meta span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
5676
5775
  .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
5776
  .tool-diff[open] .tool-diff-summary{border-bottom:1px solid #f1f5f9}
5678
5777
  .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 +5780,28 @@ mark.search-match.search-match-current{background:#fde047;color:#422006;box-shad
5681
5780
  .tool-diff[open] .tool-diff-summary:before{transform:rotate(90deg)}
5682
5781
  .tool-diff-summary .add-count{color:#16a34a}
5683
5782
  .tool-diff-summary .del-count{color:#dc2626}
5684
- .tool-diff-body{display:block;max-height:360px;overflow:auto;background:#f8fafc}
5783
+ .tool-diff-body{display:block;overflow-x:hidden;overflow-y:visible;background:#f8fafc}
5685
5784
  .tool-diff-block{display:block}
5686
5785
  .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}
5786
+ .tool-diff-line{display:block;padding:0 10px;white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word;color:#0f172a}
5688
5787
  .tool-diff-line.add{background:#e6ffec;color:#1a7f37}
5689
5788
  .tool-diff-line.del{background:#ffebe9;color:#a40e26}
5690
5789
  .tool-diff-line.ctx{color:#6b7280}
5691
5790
  .tool-diff-line.meta{color:#6b7280;font-weight:600;background:#f1f5f9}
5692
5791
  .tool-diff-line.hunk{color:#7c3aed;background:#faf5ff;font-weight:600}
5693
5792
  .tool-result{margin:0;border:1px solid #e5e7eb;border-radius:8px;background:#fff;overflow:hidden}
5793
+ .tool-result.inline{margin-top:1px;border-radius:7px;background:#f8fafc}
5694
5794
  .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}
5795
+ .tool-result.inline summary{min-height:30px;padding:5px 9px;background:#fafbfc}
5695
5796
  .tool-result summary:hover{background:#fafbfc}
5797
+ .tool-result.inline summary:hover{background:#f1f5f9}
5696
5798
  .tool-result[open] summary{border-bottom:1px solid #f1f5f9}
5697
5799
  .tool-result summary::-webkit-details-marker{display:none}
5698
5800
  .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
5801
  .tool-result[open] summary:before{transform:rotate(90deg)}
5700
5802
  .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:22px;height:22px;border-radius:6px}
5803
+ .tool-result-kind .tool-glyph{width:20px;height:20px;border-radius:6px}
5804
+ .tool-result.inline .tool-result-kind .tool-glyph{width:20px;height:20px}
5702
5805
  .tool-result-kind .tool-glyph svg{width:12px;height:12px}
5703
5806
  .tool-result[data-category="shell"] .tool-result-kind .tool-glyph{background:#f1f5f9;color:#334155}
5704
5807
  .tool-result[data-category="edit"] .tool-result-kind .tool-glyph{background:#fef3c7;color:#92400e}
@@ -5709,7 +5812,12 @@ mark.search-match.search-match-current{background:#fde047;color:#422006;box-shad
5709
5812
  .tool-result[data-category="mcp"] .tool-result-kind .tool-glyph{background:#ffedd5;color:#9a3412}
5710
5813
  .tool-result-detail{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#64748b;font-size:12px;font-weight:500}
5711
5814
  .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%;max-height:520px;margin:0;padding:10px 12px;background:#f8fafc;color:#0f172a;overflow:auto;font:12px/1.55 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;white-space:pre}
5815
+ .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}
5816
+ .tool-result.inline .tool-output{background:#fff}
5817
+ .tool-output-lines{display:grid;gap:0;padding:5px 0}
5818
+ .tool-output-line{display:grid;grid-template-columns:minmax(2.4em,max-content) minmax(0,1fr);gap:10px;min-width:0;padding:0 7px}
5819
+ .tool-line-number{color:#94a3b8;text-align:right;user-select:none}
5820
+ .tool-line-text{min-width:0;white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word}
5713
5821
  .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
5822
  .skill-mark{font:700 11px/1 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;color:#4338ca}
5715
5823
  .skill-name,.skill-path{min-width:0;overflow:hidden;text-overflow:ellipsis}
@@ -5750,6 +5858,7 @@ mark.search-match.search-match-current{background:#fde047;color:#422006;box-shad
5750
5858
  <button class="select-option active" type="button" data-value="">All sources</button>
5751
5859
  <button class="select-option" type="button" data-value="codex-cli">Codex CLI</button>
5752
5860
  <button class="select-option" type="button" data-value="codex-desktop">Codex Desktop</button>
5861
+ <button class="select-option" type="button" data-value="codex-sdk">Codex SDK jobs</button>
5753
5862
  <button class="select-option" type="button" data-value="chatgpt">ChatGPT</button>
5754
5863
  <button class="select-option" type="button" data-value="claude">Claude Code CLI</button>
5755
5864
  <button class="select-option" type="button" data-value="claude-code-desktop">Claude Code Desktop</button>
@@ -5879,6 +5988,11 @@ mark.search-match.search-match-current{background:#fde047;color:#422006;box-shad
5879
5988
  <div id="statsChatActivitySub" class="stats-card-meta" hidden></div>
5880
5989
  <div id="statsChatHeatmap" class="stats-heatmap-wrap"></div>
5881
5990
  </div>
5991
+ <div class="stats-card stats-card--compact">
5992
+ <div class="stats-card-title">SDK jobs</div>
5993
+ <div id="statsSdkActivitySub" class="stats-card-meta" hidden></div>
5994
+ <div id="statsSdkHeatmap" class="stats-heatmap-wrap"></div>
5995
+ </div>
5882
5996
  </div>
5883
5997
  <div class="stats-section">
5884
5998
  <div class="stats-section-head stats-section-head--breakdown">
@@ -6033,7 +6147,7 @@ function brandIconSvg(kind, className) {
6033
6147
 
6034
6148
  function brandKeyForSourceValue(value) {
6035
6149
  const key = String(value || '').trim().toLowerCase();
6036
- if (['codex-cli', 'codex-desktop', 'chatgpt'].includes(key)) return 'openai';
6150
+ if (['codex-cli', 'codex-desktop', 'codex-sdk', 'chatgpt'].includes(key)) return 'openai';
6037
6151
  if (['claude', 'claude-code-desktop', 'claude-workspace', 'claude-web', 'claude-sdk'].includes(key)) return 'claude';
6038
6152
  if (['gemini-cli', 'antigravity'].includes(key)) return 'gemini';
6039
6153
  if (key === 'devin-cli') return 'devin';
@@ -6360,8 +6474,8 @@ function companyColor(company) {
6360
6474
  }
6361
6475
 
6362
6476
  const MODEL_COLOR_PALETTES = {
6363
- openai: { family: 'openai', light: '#7DD3FC', base: '#0284C7', dark: PROVIDER_COLORS.codex, darker: '#4F46E5', top: '#3730A3' },
6364
- claude: { family: 'claude', light: '#FDBA74', base: '#F97316', dark: PROVIDER_COLORS.claude_code, darker: '#B45309', top: '#7F1D1D' },
6477
+ openai: { family: 'openai', light: '#BAE6FD', base: '#06B6D4', dark: PROVIDER_COLORS.codex, darker: '#1D4ED8', top: '#172554' },
6478
+ claude: { family: 'claude', light: '#FED7AA', base: '#FB923C', dark: PROVIDER_COLORS.claude_code, darker: '#9A3412', top: '#831843' },
6365
6479
  gemini: { family: 'gemini', light: '#C4B5FD', base: PROVIDER_COLORS.gemini_cli, dark: '#7E22CE', darker: '#581C87' },
6366
6480
  devin: { family: 'devin', light: '#5EEAD4', base: PROVIDER_COLORS.devin, dark: '#0F766E', darker: '#115E59' },
6367
6481
  cursor: { family: 'cursor', light: '#475569', base: PROVIDER_COLORS.cursor, dark: '#020617', darker: '#020617' },
@@ -6409,6 +6523,9 @@ function modelCapabilityColor(text, palette) {
6409
6523
  }
6410
6524
 
6411
6525
  function openAiModelColor(text, palette) {
6526
+ if (text.includes('gpt-5.5') || text.includes('gpt-5-5')) return palette.top;
6527
+ if (text.includes('gpt-5.4') || text.includes('gpt-5-4')) return palette.darker;
6528
+ if (text.includes('gpt-5.3') || text.includes('gpt-5-3')) return palette.dark;
6412
6529
  if (text.includes('gpt-5.1') || text.includes('gpt-5-1')) return palette.top;
6413
6530
  if (text.includes('gpt-5') || modelHasToken(text, ['o3'])) return palette.darker;
6414
6531
  if (text.includes('gpt-4.1') || modelHasToken(text, ['o4'])) return palette.dark;
@@ -6417,10 +6534,12 @@ function openAiModelColor(text, palette) {
6417
6534
  }
6418
6535
 
6419
6536
  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
6537
  if (modelHasToken(text, ['haiku'])) return palette.light;
6538
+ if (text.includes('claude-4.6') && modelHasToken(text, ['opus'])) return palette.top;
6539
+ if (text.includes('claude-4.5') && modelHasToken(text, ['opus'])) return palette.darker;
6540
+ if (text.includes('high-thinking') || text.includes('high_thinking')) return palette.darker;
6541
+ if (modelHasToken(text, ['opus'])) return palette.dark;
6542
+ if (modelHasToken(text, ['sonnet'])) return palette.base;
6424
6543
  return palette.base;
6425
6544
  }
6426
6545
 
@@ -6429,6 +6548,91 @@ function modelHasToken(text, tokens) {
6429
6548
  return new RegExp('(^|[^a-z0-9])(' + escaped + ')([^a-z0-9]|$)').test(text);
6430
6549
  }
6431
6550
 
6551
+ const CANONICAL_COMPANY_ORDER = ['openai', 'anthropic', 'google', 'cognition', 'cursor', 'stealth', 'unknown'];
6552
+ const CANONICAL_PROVIDER_ORDER = ['codex', 'chatgpt', 'claude_code', 'claude_desktop', 'claude_sdk', 'claude_web', 'gemini_cli', 'antigravity', 'devin', 'windsurf', 'cursor', 'cline', 'opencode', 'aider', 'unknown'];
6553
+
6554
+ function canonicalCompanyRank(company) {
6555
+ const idx = CANONICAL_COMPANY_ORDER.indexOf(String(company || '').toLowerCase());
6556
+ return idx < 0 ? CANONICAL_COMPANY_ORDER.length : idx;
6557
+ }
6558
+
6559
+ function canonicalProviderRank(provider) {
6560
+ const idx = CANONICAL_PROVIDER_ORDER.indexOf(String(provider || '').toLowerCase());
6561
+ return idx < 0 ? CANONICAL_PROVIDER_ORDER.length : idx;
6562
+ }
6563
+
6564
+ function companyForModel(text) {
6565
+ const value = String(text || '').toLowerCase();
6566
+ if (value.startsWith('composer-') || value.startsWith('cursor-') || value === 'auto' || value === 'default') return 'cursor';
6567
+ const palette = modelFamilyPalette(value);
6568
+ if (!palette) return 'unknown';
6569
+ if (palette.family === 'openai') return 'openai';
6570
+ if (palette.family === 'claude') return 'anthropic';
6571
+ if (palette.family === 'gemini' || palette.family === 'antigravity') return 'google';
6572
+ if (palette.family === 'devin' || palette.family === 'windsurf') return 'cognition';
6573
+ if (palette.family === 'cursor') return 'cursor';
6574
+ return 'unknown';
6575
+ }
6576
+
6577
+ function canonicalModelRank(model) {
6578
+ const text = String(model || '').toLowerCase();
6579
+ const company = companyForModel(text);
6580
+ const companyRank = canonicalCompanyRank(company);
6581
+ let withinRank = 950;
6582
+ if (company === 'openai') {
6583
+ if (modelHasToken(text, ['nano'])) withinRank = 920;
6584
+ else if (modelHasToken(text, ['mini'])) withinRank = 900;
6585
+ else if (text.includes('gpt-4') && !text.includes('gpt-4.1')) withinRank = 700;
6586
+ else if (text.includes('gpt-4.1')) withinRank = 600;
6587
+ else if (modelHasToken(text, ['o1'])) withinRank = 590;
6588
+ else if (modelHasToken(text, ['o3', 'o4'])) withinRank = 580;
6589
+ else if (text.includes('gpt-5.5') || text.includes('gpt-5-5')) withinRank = 100;
6590
+ else if (text.includes('gpt-5.4') || text.includes('gpt-5-4')) withinRank = 200;
6591
+ else if (text.includes('gpt-5.3') || text.includes('gpt-5-3')) withinRank = 300;
6592
+ else if (text.includes('gpt-5.1') || text.includes('gpt-5-1')) withinRank = 400;
6593
+ else if (text.includes('gpt-5')) withinRank = 500;
6594
+ } else if (company === 'anthropic') {
6595
+ if (modelHasToken(text, ['haiku'])) withinRank = 920;
6596
+ else if (text.includes('opus-4-7') || text.includes('claude-4.7')) withinRank = 100;
6597
+ else if (text.includes('claude-4.6') && modelHasToken(text, ['opus'])) withinRank = 200;
6598
+ else if (text.includes('claude-4.5') && modelHasToken(text, ['opus'])) withinRank = 300;
6599
+ else if (text.includes('claude-4') && modelHasToken(text, ['opus'])) withinRank = 400;
6600
+ else if (modelHasToken(text, ['opus'])) withinRank = 500;
6601
+ else if (text.includes('claude-4.5') && modelHasToken(text, ['sonnet'])) withinRank = 600;
6602
+ else if (text.includes('claude-4') && modelHasToken(text, ['sonnet'])) withinRank = 700;
6603
+ else if (modelHasToken(text, ['sonnet'])) withinRank = 800;
6604
+ } else if (company === 'google') {
6605
+ // Major version dictates the bucket (newer first); flash variants get +50 within their generation.
6606
+ let base = 700;
6607
+ if (text.includes('gemini-3')) base = 100;
6608
+ else if (text.includes('gemini-2')) base = 300;
6609
+ else if (text.includes('gemini-1.5')) base = 500;
6610
+ else if (text.includes('gemini-1')) base = 600;
6611
+ let modifier = 0;
6612
+ if (text.includes('flash-lite')) modifier = 80;
6613
+ else if (text.includes('flash')) modifier = 50;
6614
+ withinRank = base + modifier;
6615
+ } else if (company === 'cursor') {
6616
+ if (text === 'composer-1.5') withinRank = 100;
6617
+ else if (text === 'composer-1') withinRank = 200;
6618
+ else if (text.startsWith('composer-')) withinRank = 300;
6619
+ else if (text === 'auto') withinRank = 700;
6620
+ else if (text === 'default') withinRank = 800;
6621
+ else if (text.startsWith('cursor-')) withinRank = 600;
6622
+ }
6623
+ return companyRank * 10000 + withinRank;
6624
+ }
6625
+
6626
+ function canonicalGroupRank(group) {
6627
+ if (statsBreakdownMode === 'model') return canonicalModelRank(group);
6628
+ if (statsBreakdownMode === 'company') return canonicalCompanyRank(group);
6629
+ return canonicalProviderRank(group);
6630
+ }
6631
+
6632
+ function statsCanonicalOrderedGroups(groups) {
6633
+ return [...groups].sort((a, b) => canonicalGroupRank(a) - canonicalGroupRank(b) || String(a).localeCompare(String(b)));
6634
+ }
6635
+
6432
6636
  function statsBreakdownLabel(group) {
6433
6637
  if (statsBreakdownMode === 'model') return modelLabel(group);
6434
6638
  if (statsBreakdownMode === 'company') return companyLabel(group);
@@ -7129,7 +7333,8 @@ function renderMessages(messages, sessionSummary) {
7129
7333
  const visibleMessages = summaryText
7130
7334
  ? (messages || []).filter((message) => message?.metadata?.summaryKind !== 'conversation_summary')
7131
7335
  : (messages || []);
7132
- if (!visibleMessages.length && !summaryText) {
7336
+ const renderItems = pairedToolRenderItems(visibleMessages);
7337
+ if (!renderItems.length && !summaryText) {
7133
7338
  readableView.className = 'inline-empty';
7134
7339
  readableView.textContent = 'No transcript messages are available for this session.';
7135
7340
  return;
@@ -7144,16 +7349,17 @@ function renderMessages(messages, sessionSummary) {
7144
7349
  const renderChunk = () => {
7145
7350
  if (renderSerial !== renderMessagesSerial) return;
7146
7351
  const fragment = document.createDocumentFragment();
7147
- const end = Math.min(visibleMessages.length, index + MESSAGE_RENDER_CHUNK_SIZE);
7352
+ const end = Math.min(renderItems.length, index + MESSAGE_RENDER_CHUNK_SIZE);
7148
7353
  for (; index < end; index++) {
7149
- const message = visibleMessages[index];
7354
+ const item = renderItems[index];
7355
+ const message = item.message;
7150
7356
  const gap = renderTimeGap(previousTimestamp, message.timestamp);
7151
7357
  if (gap) fragment.appendChild(gap);
7152
- fragment.appendChild(messageElement(message));
7153
- if (message.timestamp) previousTimestamp = message.timestamp;
7358
+ fragment.appendChild(messageElement(message, item));
7359
+ previousTimestamp = item.lastTimestamp || message.timestamp || previousTimestamp;
7154
7360
  }
7155
7361
  readableView.appendChild(fragment);
7156
- if (index < visibleMessages.length) {
7362
+ if (index < renderItems.length) {
7157
7363
  scheduleNext(renderChunk);
7158
7364
  return;
7159
7365
  }
@@ -7168,6 +7374,185 @@ function renderMessages(messages, sessionSummary) {
7168
7374
  renderChunk();
7169
7375
  }
7170
7376
 
7377
+ function pairedToolRenderItems(messages) {
7378
+ const pairings = pairedToolResultIndexes(messages);
7379
+ const skippedResultIndexes = new Set(pairings.duplicateResultIndexes);
7380
+ const result = [];
7381
+ for (let index = 0; index < messages.length; index++) {
7382
+ if (skippedResultIndexes.has(index)) continue;
7383
+ const message = messages[index];
7384
+ const calls = messageToolCalls(message);
7385
+ if (!calls.length) {
7386
+ result.push({ type: 'message', message });
7387
+ continue;
7388
+ }
7389
+ const pairedResults = (pairings.byCallMessage.get(index) || [])
7390
+ .filter((resultIndex) => !skippedResultIndexes.has(resultIndex))
7391
+ .map((resultIndex) => {
7392
+ skippedResultIndexes.add(resultIndex);
7393
+ return messages[resultIndex];
7394
+ });
7395
+ let next = index + 1;
7396
+ while (pairedResults.length < calls.length && next < messages.length && isPairableToolResultMessage(messages[next])) {
7397
+ if (skippedResultIndexes.has(next)) {
7398
+ next += 1;
7399
+ continue;
7400
+ }
7401
+ pairedResults.push(messages[next]);
7402
+ skippedResultIndexes.add(next);
7403
+ next += 1;
7404
+ }
7405
+ if (pairedResults.length) {
7406
+ result.push({
7407
+ type: 'message',
7408
+ message,
7409
+ pairedToolResults: pairedResults,
7410
+ lastTimestamp: pairedResults[pairedResults.length - 1].timestamp || message.timestamp
7411
+ });
7412
+ index = next - 1;
7413
+ } else {
7414
+ result.push({ type: 'message', message });
7415
+ }
7416
+ }
7417
+ return groupConsecutiveToolItems(result);
7418
+ }
7419
+
7420
+ function groupConsecutiveToolItems(items) {
7421
+ const grouped = [];
7422
+ let run = [];
7423
+ const flush = () => {
7424
+ if (run.length > 1) {
7425
+ grouped.push({
7426
+ type: 'tool-group',
7427
+ message: run[0].message,
7428
+ items: run,
7429
+ lastTimestamp: run[run.length - 1].lastTimestamp || run[run.length - 1].message.timestamp || run[0].message.timestamp
7430
+ });
7431
+ } else if (run.length === 1) {
7432
+ grouped.push(run[0]);
7433
+ }
7434
+ run = [];
7435
+ };
7436
+ for (const item of items) {
7437
+ if (!isToolActivityRenderItem(item)) {
7438
+ flush();
7439
+ grouped.push(item);
7440
+ continue;
7441
+ }
7442
+ run.push(item);
7443
+ }
7444
+ flush();
7445
+ return grouped;
7446
+ }
7447
+
7448
+ function isToolActivityRenderItem(item) {
7449
+ if (!item || item.type !== 'message') return false;
7450
+ const message = item.message || {};
7451
+ const role = String(message.role || '').toLowerCase();
7452
+ if (role === 'tool') return Boolean(parseToolResult(message.content || '', message));
7453
+ if (role !== 'assistant') return false;
7454
+ const calls = messageToolCalls(message, item.pairedToolResults || []);
7455
+ if (!calls.length) return false;
7456
+ return !stripToolInvocationLines(message.content || '').trim();
7457
+ }
7458
+
7459
+ function pairedToolResultIndexes(messages) {
7460
+ const calls = [];
7461
+ const callsByEventId = new Map();
7462
+ const callsById = new Map();
7463
+ const callsByKind = new Map();
7464
+ const addCallKey = (map, key, callIndex) => {
7465
+ if (!key) return;
7466
+ if (!map.has(key)) map.set(key, []);
7467
+ map.get(key).push(callIndex);
7468
+ };
7469
+ for (let messagePosition = 0; messagePosition < messages.length; messagePosition++) {
7470
+ const toolCalls = messageToolCalls(messages[messagePosition]);
7471
+ for (const call of toolCalls) {
7472
+ const callIndex = calls.length;
7473
+ calls.push({ messagePosition, call });
7474
+ addCallKey(callsByEventId, call.eventId || '', callIndex);
7475
+ addCallKey(callsById, normalizedToolId(call.id), callIndex);
7476
+ addCallKey(callsByKind, normalizeToolToken(call.kind || call.name || call.title || ''), callIndex);
7477
+ }
7478
+ }
7479
+
7480
+ const resultRecords = preferredToolResultRecords(messages);
7481
+ const byCallMessage = new Map();
7482
+ const usedCalls = new Set();
7483
+ for (const record of resultRecords.records) {
7484
+ const parentEventId = record.event?.parentEventId || '';
7485
+ let callIndex = firstUnusedIndex(callsByEventId.get(parentEventId), usedCalls);
7486
+ if (callIndex < 0) callIndex = firstUnusedIndex(callsById.get(normalizedToolId(record.result.id)), usedCalls);
7487
+ if (callIndex < 0) callIndex = firstUnusedIndex(callsByKind.get(normalizeToolToken(record.result.name || record.result.kind || record.result.title || '')), usedCalls);
7488
+ if (callIndex < 0) continue;
7489
+ usedCalls.add(callIndex);
7490
+ const messagePosition = calls[callIndex].messagePosition;
7491
+ if (!byCallMessage.has(messagePosition)) byCallMessage.set(messagePosition, []);
7492
+ byCallMessage.get(messagePosition).push(record.index);
7493
+ }
7494
+ return { byCallMessage, duplicateResultIndexes: resultRecords.duplicateResultIndexes };
7495
+ }
7496
+
7497
+ function preferredToolResultRecords(messages) {
7498
+ const records = [];
7499
+ const byId = new Map();
7500
+ const duplicateResultIndexes = new Set();
7501
+ for (let index = 0; index < messages.length; index++) {
7502
+ if (!isPairableToolResultMessage(messages[index])) continue;
7503
+ const result = parseToolResult(messages[index]?.content || '', messages[index]);
7504
+ if (!result) continue;
7505
+ const event = canonicalEventsForMessage(messages[index], 'tool.completed')[0] || null;
7506
+ const record = { index, result, event };
7507
+ records.push(record);
7508
+ const id = normalizedToolId(result.id);
7509
+ if (!id) continue;
7510
+ const existing = byId.get(id);
7511
+ if (!existing) {
7512
+ byId.set(id, record);
7513
+ continue;
7514
+ }
7515
+ const preferred = preferredToolResultRecord(existing, record);
7516
+ const discarded = preferred === existing ? record : existing;
7517
+ duplicateResultIndexes.add(discarded.index);
7518
+ byId.set(id, preferred);
7519
+ }
7520
+ return {
7521
+ records: records.filter((record) => !duplicateResultIndexes.has(record.index)),
7522
+ duplicateResultIndexes
7523
+ };
7524
+ }
7525
+
7526
+ function preferredToolResultRecord(left, right) {
7527
+ return toolResultDisplayScore(right.result) > toolResultDisplayScore(left.result) ? right : left;
7528
+ }
7529
+
7530
+ function toolResultDisplayScore(result) {
7531
+ const rawCategory = normalizeToolToken(result?.rawCategory || '');
7532
+ const kind = normalizeToolToken(result?.kind || '');
7533
+ const category = normalizeToolToken(result?.category || '');
7534
+ let score = 0;
7535
+ if (['exec_command_end', 'patch_apply_end', 'mcp_tool_call_end', 'web_search_end', 'tool_result', 'tool_output'].includes(rawCategory)) score += 2000;
7536
+ if (rawCategory === 'function_call_output' || rawCategory === 'custom_tool_call_output') score -= 1000;
7537
+ if (category && category !== 'function') score += 250;
7538
+ if (kind && !kind.startsWith('call_')) score += 150;
7539
+ if (/^\$\s/.test(String(result?.detail || result?.output || ''))) score += 250;
7540
+ score += Math.min(200, String(result?.output || '').length / 200);
7541
+ return score;
7542
+ }
7543
+
7544
+ function firstUnusedIndex(indexes, used) {
7545
+ if (!Array.isArray(indexes)) return -1;
7546
+ for (const index of indexes) {
7547
+ if (!used.has(index)) return index;
7548
+ }
7549
+ return -1;
7550
+ }
7551
+
7552
+ function isPairableToolResultMessage(message) {
7553
+ return String(message?.role || '').toLowerCase() === 'tool' && Boolean(parseToolResult(message?.content || '', message));
7554
+ }
7555
+
7171
7556
  function sessionSummaryText(sessionSummary) {
7172
7557
  if (!sessionSummary || typeof sessionSummary !== 'object') return '';
7173
7558
  return String(
@@ -7216,11 +7601,7 @@ function summarySourceLabel(source) {
7216
7601
  }
7217
7602
 
7218
7603
  function renderTimeGap(previous, current) {
7219
- if (!previous || !current) return null;
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;
7604
+ const diffSec = timeGapSeconds(previous, current);
7224
7605
  if (diffSec < 5) return null;
7225
7606
  const label = formatGapLabel(diffSec);
7226
7607
  if (!label) return null;
@@ -7230,6 +7611,14 @@ function renderTimeGap(previous, current) {
7230
7611
  return node;
7231
7612
  }
7232
7613
 
7614
+ function timeGapSeconds(previous, current) {
7615
+ if (!previous || !current) return 0;
7616
+ const prev = new Date(previous).getTime();
7617
+ const next = new Date(current).getTime();
7618
+ if (!Number.isFinite(prev) || !Number.isFinite(next)) return 0;
7619
+ return Math.max(0, (next - prev) / 1000);
7620
+ }
7621
+
7233
7622
  function formatGapLabel(diffSec) {
7234
7623
  if (diffSec < 60) return Math.round(diffSec) + 's';
7235
7624
  if (diffSec < 3600) return Math.round(diffSec / 60) + 'm';
@@ -7302,12 +7691,13 @@ function highlightSearchMatches(root, term) {
7302
7691
  walk(root);
7303
7692
  }
7304
7693
 
7305
- function messageElement(message) {
7694
+ function messageElement(message, options = {}) {
7695
+ if (options.type === 'tool-group') return toolGroupElement(options);
7306
7696
  const context = generatedContextForMessage(message);
7307
7697
  if (context) return contextMessageElement(message, context);
7308
7698
  const role = String(message.role || 'unknown').toLowerCase();
7309
7699
  const content = String(message.content || '');
7310
- const toolCalls = messageToolCalls(message);
7700
+ const toolCalls = messageToolCalls(message, options.pairedToolResults || []);
7311
7701
  const toolResult = role === 'tool' ? parseToolResult(content, message) : null;
7312
7702
  const contentWithoutTools = toolCalls.length ? stripToolInvocationLines(content) : content;
7313
7703
  const toolOnly = toolCalls.length && !contentWithoutTools.trim();
@@ -7346,7 +7736,22 @@ function messageElement(message) {
7346
7736
  return row;
7347
7737
  }
7348
7738
 
7739
+ function toolGroupElement(group) {
7740
+ const calls = [];
7741
+ for (const item of group.items || []) {
7742
+ calls.push(...toolCardsForRenderItem(item));
7743
+ }
7744
+ const row = document.createElement('article');
7745
+ row.className = 'message tool tool-call-turn tool-group-turn';
7746
+ const bubble = document.createElement('div');
7747
+ bubble.className = 'bubble';
7748
+ bubble.innerHTML = renderToolGroupCard(calls);
7749
+ row.appendChild(bubble);
7750
+ return row;
7751
+ }
7752
+
7349
7753
  function contextMessageElement(message, context) {
7754
+ if (context.kind === 'task_notification') return contextLineElement(message, context);
7350
7755
  const row = document.createElement('article');
7351
7756
  row.className = 'message context context-' + escClass(context.kind || 'metadata');
7352
7757
  const bubble = document.createElement('div');
@@ -7371,6 +7776,25 @@ function contextMessageElement(message, context) {
7371
7776
  return row;
7372
7777
  }
7373
7778
 
7779
+ function contextLineElement(message, context) {
7780
+ const row = document.createElement('article');
7781
+ row.className = 'message context context-line-turn context-' + escClass(context.kind || 'metadata');
7782
+ const bubble = document.createElement('div');
7783
+ bubble.className = 'bubble';
7784
+ const line = document.createElement('div');
7785
+ line.className = 'context-line';
7786
+ line.innerHTML =
7787
+ '<span class="context-glyph">' + contextIconSvg(context.kind) + '</span>' +
7788
+ '<span class="context-title">' + esc(contextTitle(context.kind)) + '</span>' +
7789
+ '<span class="context-meta">' + context.chips.map((chip) => '<span class="context-chip">' + esc(chip) + '</span>').join('') + '</span>' +
7790
+ '<span class="context-end"><span class="context-time"' + timeAttr(message.timestamp) + '>' + esc(relativeTime(message.timestamp)) + '</span></span>';
7791
+ const end = line.querySelector('.context-end');
7792
+ if (end) end.appendChild(messageCopyButton(message.content || ''));
7793
+ bubble.appendChild(line);
7794
+ row.appendChild(bubble);
7795
+ return row;
7796
+ }
7797
+
7374
7798
  function generatedContextForMessage(message) {
7375
7799
  const metadata = message?.metadata || {};
7376
7800
  const content = String(message?.content || '').trim();
@@ -7495,10 +7919,55 @@ function xmlTagText(text, tag) {
7495
7919
 
7496
7920
  function renderMessageBodyWithTools(className, content, toolCalls) {
7497
7921
  const body = className === 'user' ? renderPlainText(content) : renderRichText(content);
7498
- const stack = toolCalls.length ? '<div class="tool-stack">' + toolCalls.map(renderToolCallout).join('') + '</div>' : '';
7922
+ const stack = toolCalls.length > 1
7923
+ ? renderToolGroupCard(toolCalls)
7924
+ : toolCalls.length
7925
+ ? '<div class="tool-stack">' + toolCalls.map(renderToolCallout).join('') + '</div>'
7926
+ : '';
7499
7927
  return body ? '<div class="tool-body">' + body + '</div>' + stack : stack;
7500
7928
  }
7501
7929
 
7930
+ function renderToolGroupCard(calls) {
7931
+ const tools = Array.isArray(calls) ? calls : [];
7932
+ if (!tools.length) return '';
7933
+ return '<details class="tool-group-card">' +
7934
+ '<summary><span class="tool-group-prefix"><span class="tool-group-caret"></span><span class="tool-glyph">' +
7935
+ renderToolIcon({ category: dominantToolCategory(tools), kind: 'Tool activity' }) +
7936
+ '</span></span><span class="tool-group-title">' +
7937
+ esc(toolStackSummary(tools) || 'Used ' + tools.length + ' tools') +
7938
+ '</span></summary>' +
7939
+ '<div class="tool-stack">' + tools.map(renderToolCallout).join('') + '</div></details>';
7940
+ }
7941
+
7942
+ function toolCardsForRenderItem(item) {
7943
+ const message = item?.message || {};
7944
+ if (String(message.role || '').toLowerCase() === 'tool') {
7945
+ const result = parseToolResult(message.content || '', message);
7946
+ return result ? [toolCardFromResult(result)] : [];
7947
+ }
7948
+ return messageToolCalls(message, item?.pairedToolResults || []);
7949
+ }
7950
+
7951
+ function toolCardFromResult(result) {
7952
+ const category = normalizedToolCategory(result.category, result.kind);
7953
+ const detail = result.detail || result.summary || firstLine(result.output || '') || result.kind || 'Tool output';
7954
+ return toolCard({
7955
+ kind: result.name || result.kind || 'Tool output',
7956
+ title: result.kind || result.title || '',
7957
+ category,
7958
+ categoryLabel: result.categoryLabel || toolCategoryLabel(category),
7959
+ icon: result.icon || toolIcon(category, result.kind || ''),
7960
+ status: result.status || 'completed',
7961
+ argument: detail,
7962
+ rawInputSummary: detail,
7963
+ inputPreview: '',
7964
+ target: result.target || '',
7965
+ id: result.id || '',
7966
+ result,
7967
+ resultOnly: true
7968
+ });
7969
+ }
7970
+
7502
7971
  function copyIconSvg() {
7503
7972
  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
7973
  }
@@ -7547,13 +8016,13 @@ function detectToolName(text) {
7547
8016
  return tool ? tool.label : '';
7548
8017
  }
7549
8018
 
7550
- function messageToolCalls(message) {
8019
+ function messageToolCalls(message, pairedResultMessages) {
7551
8020
  const eventCalls = canonicalEventsForMessage(message, 'tool.called').map(toolCallFromEvent).filter(Boolean);
7552
- if (eventCalls.length) return eventCalls;
8021
+ if (eventCalls.length) return attachPairedToolResults(eventCalls, pairedResultMessages);
7553
8022
  const metadataCalls = Array.isArray(message?.metadata?.toolCalls) ? message.metadata.toolCalls : [];
7554
8023
  const textCalls = toolInvocationsFromText(message?.content || '');
7555
8024
  if (metadataCalls.length) {
7556
- return metadataCalls.map((meta, index) => {
8025
+ const calls = metadataCalls.map((meta, index) => {
7557
8026
  const text = textCalls[index] || {};
7558
8027
  const kind = meta.displayName || meta.name || text.kind || 'Tool';
7559
8028
  const argument = meta.argument || meta.rawInputSummary || text.argument || '';
@@ -7569,10 +8038,11 @@ function messageToolCalls(message) {
7569
8038
  categoryLabel: meta.categoryLabel || '',
7570
8039
  icon: meta.icon || '',
7571
8040
  rawCategory: meta.rawCategory || '',
7572
- id: meta.id || '',
8041
+ id: meta.id || meta.callId || meta.call_id || meta.toolCallId || meta.tool_call_id || meta.toolUseId || meta.tool_use_id || '',
7573
8042
  arguments: meta.arguments || null
7574
8043
  });
7575
8044
  }).filter((call) => call.kind || call.title || call.argument);
8045
+ return attachPairedToolResults(calls, pairedResultMessages);
7576
8046
  }
7577
8047
  const total = Math.max(metadataCalls.length, textCalls.length);
7578
8048
  const calls = [];
@@ -7592,11 +8062,67 @@ function messageToolCalls(message) {
7592
8062
  categoryLabel: meta.categoryLabel || '',
7593
8063
  icon: meta.icon || '',
7594
8064
  rawCategory: meta.rawCategory || '',
7595
- id: meta.id || '',
8065
+ id: meta.id || meta.callId || meta.call_id || meta.toolCallId || meta.tool_call_id || meta.toolUseId || meta.tool_use_id || '',
7596
8066
  arguments: meta.arguments || null
7597
8067
  }));
7598
8068
  }
7599
- return calls.filter((call) => call.kind || call.title || call.argument);
8069
+ return attachPairedToolResults(calls.filter((call) => call.kind || call.title || call.argument), pairedResultMessages);
8070
+ }
8071
+
8072
+ function attachPairedToolResults(calls, pairedResultMessages) {
8073
+ if (!Array.isArray(pairedResultMessages) || !pairedResultMessages.length) return calls;
8074
+ const results = pairedResultMessages
8075
+ .map((message) => parseToolResult(message?.content || '', message))
8076
+ .filter(Boolean);
8077
+ if (!results.length) return calls;
8078
+ const remaining = results.slice();
8079
+ return calls.map((call) => {
8080
+ const matchIndex = pairedToolResultIndex(call, remaining);
8081
+ if (matchIndex < 0) return call;
8082
+ const [result] = remaining.splice(matchIndex, 1);
8083
+ return { ...call, result };
8084
+ });
8085
+ }
8086
+
8087
+ function pairedToolResultIndex(call, results) {
8088
+ const callId = normalizedToolId(call?.id);
8089
+ if (callId) {
8090
+ const idMatch = results.findIndex((result) => normalizedToolId(result?.id) === callId);
8091
+ if (idMatch >= 0) return idMatch;
8092
+ }
8093
+ const callKind = normalizeToolToken(call?.kind || call?.name || call?.title || '');
8094
+ if (callKind) {
8095
+ const kindMatch = results.findIndex((result) => normalizeToolToken(result?.name || result?.kind || result?.title || '') === callKind);
8096
+ if (kindMatch >= 0) return kindMatch;
8097
+ }
8098
+ return results.length ? 0 : -1;
8099
+ }
8100
+
8101
+ function toolStackSummary(tools) {
8102
+ const counts = {};
8103
+ for (const tool of tools) {
8104
+ const category = normalizedToolCategory(tool.category, tool.kind);
8105
+ counts[category] = (counts[category] || 0) + 1;
8106
+ }
8107
+ const parts = [];
8108
+ if (counts.read) parts.push('explored ' + counts.read + ' ' + (counts.read === 1 ? 'file' : 'files'));
8109
+ if (counts.search) parts.push('searched ' + counts.search + ' ' + (counts.search === 1 ? 'time' : 'times'));
8110
+ if (counts.shell) parts.push('ran ' + counts.shell + ' ' + (counts.shell === 1 ? 'command' : 'commands'));
8111
+ if (counts.edit) parts.push('edited ' + counts.edit + ' ' + (counts.edit === 1 ? 'file' : 'files'));
8112
+ const known = ['read', 'search', 'shell', 'edit'];
8113
+ const other = Object.entries(counts).filter(([key]) => !known.includes(key)).reduce((sum, [, count]) => sum + count, 0);
8114
+ if (other) parts.push('used ' + other + ' ' + (other === 1 ? 'tool' : 'tools'));
8115
+ const text = parts.join(', ');
8116
+ return text ? text.charAt(0).toUpperCase() + text.slice(1) : '';
8117
+ }
8118
+
8119
+ function dominantToolCategory(tools) {
8120
+ const counts = {};
8121
+ for (const tool of tools || []) {
8122
+ const category = normalizedToolCategory(tool.category, tool.kind);
8123
+ counts[category] = (counts[category] || 0) + 1;
8124
+ }
8125
+ return Object.entries(counts).sort((a, b) => b[1] - a[1])[0]?.[0] || 'tool';
7600
8126
  }
7601
8127
 
7602
8128
  function toolCallFromEvent(event) {
@@ -7615,7 +8141,8 @@ function toolCallFromEvent(event) {
7615
8141
  categoryLabel: meta.categoryLabel || '',
7616
8142
  icon: meta.icon || event.indexed?.toolIcon || '',
7617
8143
  rawCategory: meta.rawCategory || '',
7618
- id: meta.id || '',
8144
+ id: meta.id || meta.callId || meta.call_id || meta.toolCallId || meta.tool_call_id || meta.toolUseId || meta.tool_use_id || '',
8145
+ eventId: event.eventId || '',
7619
8146
  arguments: meta.arguments || null
7620
8147
  });
7621
8148
  }
@@ -7682,6 +8209,10 @@ function normalizeToolToken(value) {
7682
8209
  return String(value || '').trim().toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '');
7683
8210
  }
7684
8211
 
8212
+ function normalizedToolId(value) {
8213
+ return String(value || '').trim().toLowerCase();
8214
+ }
8215
+
7685
8216
  function toolCategoryLabel(category) {
7686
8217
  const labels = {
7687
8218
  shell: 'Shell',
@@ -7710,18 +8241,92 @@ function escClass(value) {
7710
8241
  function renderToolCallout(tool) {
7711
8242
  const card = toolCard(tool);
7712
8243
  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
8244
  const diff = renderToolDiff(card);
7719
- const preview = diff ? '' : '<span class="tool-preview">' + esc(card.inputPreview || card.argument || card.kind) + '</span>';
7720
- const detail = preview + target + diff;
7721
- return '<div class="tool-callout ' + escClass(card.category || 'tool') + (isSkill ? ' skill' : '') + '">' +
7722
- '<span class="tool-glyph">' + renderToolIcon(card) + '</span>' +
7723
- '<div class="tool-copy"><div class="tool-title">' + esc(title) + category + status + '</div>' +
7724
- '<div class="tool-detail">' + detail + '</div></div></div>';
8245
+ const category = normalizedToolCategory(card.category, card.kind);
8246
+ return '<details class="tool-callout ' + escClass(category) + (isSkill ? ' skill' : '') + (card.result ? ' has-result' : '') + '" data-category="' + esc(escClass(category)) + '">' +
8247
+ renderToolActivitySummary(card) +
8248
+ renderToolCalloutBody(card, diff) +
8249
+ '</details>';
8250
+ }
8251
+
8252
+ function renderToolActivitySummary(card) {
8253
+ const label = toolActivityLabel(card);
8254
+ const count = card.result?.count || '';
8255
+ const statusClass = humanToolStatusClass(card.status);
8256
+ const status = !card.result && card.status ? '<span class="tool-status' + (statusClass ? ' ' + statusClass : '') + '">' + esc(humanToolStatus(card.status)) + '</span>' : '';
8257
+ return '<summary><span class="tool-glyph">' + renderToolIcon(card) + '</span>' +
8258
+ '<span class="tool-call-line"><span class="tool-action">' + esc(label.action) + '</span>' +
8259
+ (label.subject ? '<span class="tool-subject">' + esc(label.subject) + '</span>' : '') +
8260
+ '</span>' + status +
8261
+ (count ? '<span class="tool-result-count">' + esc(count) + '</span>' : '') +
8262
+ '</summary>';
8263
+ }
8264
+
8265
+ function renderToolCalloutBody(card, diff) {
8266
+ const rows = [];
8267
+ const meta = [];
8268
+ if (card.categoryLabel) meta.push(card.categoryLabel);
8269
+ if (card.status) meta.push(humanToolStatus(card.status));
8270
+ if (card.target) meta.push(card.target);
8271
+ if (meta.length) rows.push('<div class="tool-call-meta">' + meta.map((item) => '<span class="tool-chip">' + esc(item) + '</span>').join('') + '</div>');
8272
+ const preview = card.resultOnly ? '' : String(card.inputPreview || card.argument || '').trim();
8273
+ if (preview) rows.push('<pre class="tool-preview">' + esc(preview) + '</pre>');
8274
+ if (diff) rows.push(diff);
8275
+ if (card.result) rows.push(renderPairedToolResult(card.result));
8276
+ return rows.length ? '<div class="tool-callout-body">' + rows.join('') + '</div>' : '';
8277
+ }
8278
+
8279
+ function renderPairedToolResult(result) {
8280
+ const category = normalizedToolCategory(result.category, result.kind);
8281
+ const meta = [result.kind, result.detail, result.count].filter(Boolean);
8282
+ return '<div class="tool-paired-result" data-category="' + esc(escClass(category)) + '">' +
8283
+ (meta.length ? '<div class="tool-result-meta">' + meta.map((item) => '<span>' + esc(item) + '</span>').join('') + '</div>' : '') +
8284
+ renderToolOutput(result.output || '', { lineStart: result.lineStart }) + '</div>';
8285
+ }
8286
+
8287
+ function toolActivityLabel(card) {
8288
+ const category = normalizedToolCategory(card.category, card.kind);
8289
+ const fallback = toolSubjectFallback(card);
8290
+ if (category === 'shell') return { action: 'Ran', subject: shellCommandText(card) || fallback };
8291
+ if (category === 'read') return { action: 'Read', subject: compactPathLabel(card.target || fallback) };
8292
+ if (category === 'search') return { action: 'Searched', subject: fallback };
8293
+ if (category === 'edit') return { action: 'Edited', subject: compactPathLabel(card.target || fallback) };
8294
+ if (category === 'web') return { action: webToolVerb(card), subject: fallback };
8295
+ if (category === 'task') return { action: 'Ran', subject: fallback };
8296
+ if (category === 'skill') return { action: 'Loaded', subject: fallback };
8297
+ if (category === 'mcp') return { action: 'Called', subject: fallback };
8298
+ return { action: card.title || card.kind || 'Used', subject: fallback && fallback !== (card.title || card.kind) ? fallback : '' };
8299
+ }
8300
+
8301
+ function shellCommandText(card) {
8302
+ const args = card.arguments && typeof card.arguments === 'object' && !Array.isArray(card.arguments) ? card.arguments : {};
8303
+ return firstToolString(args.cmd, args.command, args.script, card.inputPreview, card.argument);
8304
+ }
8305
+
8306
+ function webToolVerb(card) {
8307
+ const key = normalizeToolToken(card.kind || card.title || '');
8308
+ if (key.includes('fetch')) return 'Fetched';
8309
+ if (key.includes('search')) return 'Searched';
8310
+ if (key.includes('open')) return 'Opened';
8311
+ return 'Opened';
8312
+ }
8313
+
8314
+ function toolSubjectFallback(card) {
8315
+ return firstToolString(card.target, card.inputPreview, card.argument, card.title, card.kind);
8316
+ }
8317
+
8318
+ function compactPathLabel(value) {
8319
+ const text = String(value || '').trim();
8320
+ if (!text || /\s/.test(text) || !text.includes('/')) return text;
8321
+ return text.split('/').filter(Boolean).pop() || text;
8322
+ }
8323
+
8324
+ function firstToolString() {
8325
+ for (const value of arguments) {
8326
+ if (typeof value === 'string' && value.trim()) return value.trim();
8327
+ if (Array.isArray(value) && value.length) return value.map((item) => String(item || '').trim()).filter(Boolean).join(' ');
8328
+ }
8329
+ return '';
7725
8330
  }
7726
8331
 
7727
8332
  function hasPatchMarker(value) {
@@ -7818,7 +8423,7 @@ function renderToolDiff(card) {
7818
8423
  if (adds) summaryParts.push('<span class="add-count">+' + adds + '</span>');
7819
8424
  if (dels) summaryParts.push('<span class="del-count">-' + dels + '</span>');
7820
8425
  const summaryLabel = editCount > 1 ? editCount + ' edits' : label;
7821
- return '<details class="tool-diff" open>' +
8426
+ return '<details class="tool-diff">' +
7822
8427
  '<summary class="tool-diff-summary">' + esc(summaryLabel) + summaryParts.join('') + '</summary>' +
7823
8428
  '<div class="tool-diff-body">' + blocks.join('') + '</div>' +
7824
8429
  '</details>';
@@ -7966,6 +8571,9 @@ function toolResultFromMetadata(result, indexed, fallbackText) {
7966
8571
  if (!output && !result.summary && !indexed.summary) return null;
7967
8572
  const category = normalizedToolCategory(result.category || indexed.toolCategory || result.rawCategory || '', result.kind || indexed.title || '');
7968
8573
  return {
8574
+ id: result.id || result.callId || result.call_id || result.toolCallId || result.tool_call_id || result.toolUseId || result.tool_use_id || '',
8575
+ name: result.name || result.toolName || indexed.toolName || '',
8576
+ rawCategory: result.rawCategory || '',
7969
8577
  header: 'Tool result' + (result.title || indexed.title ? ' · ' + (result.title || indexed.title) : ''),
7970
8578
  kind: result.kind || indexed.title || 'Tool output',
7971
8579
  category,
@@ -7974,6 +8582,7 @@ function toolResultFromMetadata(result, indexed, fallbackText) {
7974
8582
  detail: result.summary || indexed.summary || firstLine(output),
7975
8583
  count: result.lineCount ? result.lineCount + ' line' + (Number(result.lineCount) === 1 ? '' : 's') : lineCountLabel(output || result.summary || indexed.summary || ''),
7976
8584
  output: output || result.summary || indexed.summary || '',
8585
+ lineStart: Number(result.startLine || result.start_line || result.lineStart || result.line_start || 0) || 0,
7977
8586
  collapsed: Boolean(result.collapsed) || String(output).split('\\n').length > 18
7978
8587
  };
7979
8588
  }
@@ -7993,6 +8602,7 @@ function parseFileViewResult(text) {
7993
8602
  detail: [attrs.path || basename, lineLabel].filter(Boolean).join(' · '),
7994
8603
  count: lineCountLabel(code),
7995
8604
  output: code,
8605
+ lineStart: Number(attrs.start_line || 0) || 0,
7996
8606
  collapsed: code.split('\\n').length > 24
7997
8607
  };
7998
8608
  }
@@ -8050,14 +8660,25 @@ function genericToolResult(text) {
8050
8660
  };
8051
8661
  }
8052
8662
 
8053
- function renderToolResult(result) {
8054
- const open = result.collapsed ? '' : ' open';
8663
+ function renderToolResult(result, options) {
8664
+ const inline = Boolean(options && options.inline);
8055
8665
  const category = normalizedToolCategory(result.category, result.kind);
8056
- return '<details class="tool-result" data-category="' + esc(escClass(category)) + '"' + open + '>' +
8666
+ return '<details class="tool-result' + (inline ? ' inline' : '') + '" data-category="' + esc(escClass(category)) + '">' +
8057
8667
  '<summary><span class="tool-result-kind"><span class="tool-glyph">' + renderToolIcon({ icon: result.icon, category, kind: result.kind }) + '</span>' + esc(result.kind) + '</span>' +
8058
8668
  (result.detail ? '<span class="tool-result-detail">' + esc(result.detail) + '</span>' : '') +
8059
8669
  (result.count ? '<span class="tool-result-count">' + esc(result.count) + '</span>' : '') +
8060
- '</summary><pre class="tool-output">' + esc(result.output || '') + '</pre></details>';
8670
+ '</summary>' + renderToolOutput(result.output || '', { lineStart: result.lineStart }) + '</details>';
8671
+ }
8672
+
8673
+ function renderToolOutput(output, options = {}) {
8674
+ const text = String(output || '');
8675
+ const lines = text.split('\\n');
8676
+ if (lines.length <= 1) return '<pre class="tool-output">' + esc(text) + '</pre>';
8677
+ const start = Number(options.lineStart || 0) || 1;
8678
+ return '<div class="tool-output tool-output-lines">' + lines.map((line, index) =>
8679
+ '<div class="tool-output-line"><span class="tool-line-number">' + esc(start + index) + '</span><span class="tool-line-text">' +
8680
+ (line ? esc(line) : '&nbsp;') + '</span></div>'
8681
+ ).join('') + '</div>';
8061
8682
  }
8062
8683
 
8063
8684
  function parseXmlishAttrs(value) {
@@ -8710,7 +9331,7 @@ setupStatsActivityControls();
8710
9331
  setupStatsBreakdownControls();
8711
9332
 
8712
9333
  async function loadStats() {
8713
- const elements = ['chartTokensPerDay', 'chartSessionsPerDay', 'chartTokensPerRepo', 'chartSessionsPerRepo', 'statsAgentHeatmap', 'statsChatHeatmap'].map((id) => document.getElementById(id));
9334
+ const elements = ['chartTokensPerDay', 'chartSessionsPerDay', 'chartTokensPerRepo', 'chartSessionsPerRepo', 'statsAgentHeatmap', 'statsChatHeatmap', 'statsSdkHeatmap'].map((id) => document.getElementById(id));
8714
9335
  for (const el of elements) {
8715
9336
  if (el) {
8716
9337
  el.classList.add('stats-empty');
@@ -9003,8 +9624,9 @@ function renderStatsDailyCharts() {
9003
9624
  if (empty2) { empty2.classList.add('stats-empty'); empty2.textContent = 'No days in this range.'; }
9004
9625
  return;
9005
9626
  }
9006
- renderDailyChart('chartTokensPerDay', densified, groups, usageMetric, 336);
9007
- renderDailyChart('chartSessionsPerDay', densified, groups, activityMetric, 336);
9627
+ const canonicalGroups = statsCanonicalOrderedGroups(groups);
9628
+ renderDailyChart('chartTokensPerDay', densified, canonicalGroups, usageMetric, 336);
9629
+ renderDailyChart('chartSessionsPerDay', densified, canonicalGroups, activityMetric, 336);
9008
9630
  }
9009
9631
 
9010
9632
  function renderStats(payload) {
@@ -9019,8 +9641,9 @@ function renderStats(payload) {
9019
9641
  renderStatsLegend(groups, breakdownTotals);
9020
9642
  renderStatsDailyCharts();
9021
9643
  const repoRows = statsRepoRowsForRange(payload);
9022
- renderRepoChart('chartTokensPerRepo', repoRows, groups, statsTokenMetric());
9023
- renderRepoChart('chartSessionsPerRepo', repoRows, groups, statsActivityMetric());
9644
+ const canonicalGroupsForRepo = statsCanonicalOrderedGroups(groups);
9645
+ renderRepoChart('chartTokensPerRepo', repoRows, canonicalGroupsForRepo, statsTokenMetric());
9646
+ renderRepoChart('chartSessionsPerRepo', repoRows, canonicalGroupsForRepo, statsActivityMetric());
9024
9647
  }
9025
9648
 
9026
9649
  function statsTokenMetric() {
@@ -9392,6 +10015,10 @@ function renderStatsMetrics(payload) {
9392
10015
  const totalOutTok = Number(payload.total_output_tokens || 0);
9393
10016
  const totalCacheTok = Number(payload.total_cache_tokens || 0);
9394
10017
  const totalEstimatedTok = Number(payload.total_estimated_tokens || 0);
10018
+ const sdkSessions = Number(payload.sdk_session_count || 0);
10019
+ const sdkTokens = Number(payload.sdk_total_tokens || 0);
10020
+ const sdkInputTokens = Number(payload.sdk_total_input_tokens || 0);
10021
+ const sdkOutputTokens = Number(payload.sdk_total_output_tokens || 0);
9395
10022
  const um = Number(payload.user_message_count || 0);
9396
10023
  const tm = Number(payload.message_count || 0);
9397
10024
  const avgMsgs = Number(payload.avg_messages_per_conversation);
@@ -9416,6 +10043,13 @@ function renderStatsMetrics(payload) {
9416
10043
  value: formatFullNumber(payload.session_count || 0),
9417
10044
  sub: formatFullNumber(payload.agent_session_count || 0) + ' agent · ' + formatFullNumber(payload.chat_session_count || 0) + ' chat'
9418
10045
  },
10046
+ sdkSessions || sdkTokens ? {
10047
+ label: 'SDK jobs',
10048
+ value: formatFullNumber(sdkSessions),
10049
+ sub: (sdkTokens ? formatCompactNumber(sdkTokens) + ' tokens' : '0 tokens') +
10050
+ (sdkInputTokens || sdkOutputTokens ? ' · ' + formatCompactNumber(sdkInputTokens) + ' in / ' + formatCompactNumber(sdkOutputTokens) + ' out' : '') +
10051
+ ' · kept separate'
10052
+ } : null,
9419
10053
  {
9420
10054
  label: 'Messages',
9421
10055
  value: formatFullNumber(tm),
@@ -9472,6 +10106,7 @@ function renderStatsMetrics(payload) {
9472
10106
  }
9473
10107
  ];
9474
10108
  container.innerHTML = metrics
10109
+ .filter(Boolean)
9475
10110
  .map(function (metric) {
9476
10111
  const vt =
9477
10112
  metric.valueHtml == null &&
@@ -9545,7 +10180,11 @@ function renderHeatmapSection(payload) {
9545
10180
  renderSecondaryHeatmap(payload.split_stats && payload.split_stats.agent, 'statsAgentActivitySub', 'statsAgentHeatmap', range, metric);
9546
10181
  renderSecondaryHeatmap(payload.split_stats && payload.split_stats.chat, 'statsChatActivitySub', 'statsChatHeatmap', range, metric, {
9547
10182
  emptySubText: '',
9548
- emptyText: 'No chat activity yet. Import ChatGPT or Claude.ai exports with agentlog import chatgpt <path> --scope local or agentlog import claude-web <path> --scope local.'
10183
+ emptyText: 'No chat activity yet. Run agentlog import chatgpt or agentlog import claude-web for official export instructions.'
10184
+ });
10185
+ renderSecondaryHeatmap(payload.split_stats && payload.split_stats.sdk, 'statsSdkActivitySub', 'statsSdkHeatmap', range, metric, {
10186
+ emptySubText: '',
10187
+ emptyText: 'No SDK jobs imported.'
9549
10188
  });
9550
10189
  }
9551
10190
 
@@ -9780,15 +10419,11 @@ function renderDailyChart(elementId, daily, providers, metric, height) {
9780
10419
  return '<line class="stats-axis-line" x1="' + padding.left + '" x2="' + (padding.left + innerW) + '" y1="' + y.toFixed(2) + '" y2="' + y.toFixed(2) + '"/>'
9781
10420
  + '<text class="stats-axis-label" x="' + (padding.left - 6) + '" y="' + (y + 3).toFixed(2) + '" text-anchor="end">' + esc(formatCompactNumber(value)) + '</text>';
9782
10421
  }).join('');
9783
- let prevYm = '';
9784
- const xLabels = visibleDaily.map((day, idx) => {
9785
- const ym = day.date.slice(0, 7);
9786
- if (ym === prevYm) return '';
9787
- prevYm = ym;
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('');
10422
+ const xLabels = dailyChartMonthTicks(visibleDaily, xStep, padding.left, innerW)
10423
+ .map((tickEntry) =>
10424
+ '<text class="stats-axis-label" x="' + tickEntry.x.toFixed(2) + '" y="' + (padding.top + innerH + 18) + '" text-anchor="middle">' + esc(tickEntry.label) + '</text>'
10425
+ )
10426
+ .join('');
9792
10427
  const hits = visibleDaily.map((day, dayIdx) => {
9793
10428
  const xHit = padding.left + dayIdx * xStep;
9794
10429
  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 +10435,33 @@ function renderDailyChart(elementId, daily, providers, metric, height) {
9800
10435
  bindDailyChartHover(el);
9801
10436
  }
9802
10437
 
10438
+ function dailyChartMonthTicks(daily, xStep, left, innerW) {
10439
+ const monthStarts = [];
10440
+ let prevYm = '';
10441
+ for (let idx = 0; idx < daily.length; idx += 1) {
10442
+ const day = daily[idx];
10443
+ const ym = String(day?.date || '').slice(0, 7);
10444
+ if (!/^\\d{4}-\\d{2}$/.test(ym) || ym === prevYm) continue;
10445
+ prevYm = ym;
10446
+ monthStarts.push({ idx, date: day.date, ym });
10447
+ }
10448
+ const maxLabels = Math.max(2, Math.floor(innerW / 58));
10449
+ const step = Math.max(1, Math.ceil(monthStarts.length / maxLabels));
10450
+ const selected = [];
10451
+ for (let idx = 0; idx < monthStarts.length; idx += step) selected.push(monthStarts[idx]);
10452
+ const last = monthStarts[monthStarts.length - 1];
10453
+ if (last && selected[selected.length - 1] !== last) {
10454
+ const previous = selected[selected.length - 1];
10455
+ if (previous && monthStarts.indexOf(previous) >= monthStarts.length - step) selected[selected.length - 1] = last;
10456
+ else selected.push(last);
10457
+ }
10458
+ return selected
10459
+ .map((entry) => ({
10460
+ x: left + entry.idx * xStep + xStep / 2,
10461
+ label: formatChartMonthTick(entry.date)
10462
+ }));
10463
+ }
10464
+
9803
10465
  function trimEmptyDailyChartEdges(daily, providers, metric) {
9804
10466
  const rows = Array.isArray(daily) ? daily : [];
9805
10467
  const hasValue = (day) => providers.some((provider) => statsMetricValue(day?.providers?.[provider], metric) > 0);
@@ -10246,8 +10908,8 @@ Start here:
10246
10908
  Archive and import:
10247
10909
  init interactive setup and optional first import
10248
10910
  import import local Codex, Claude, Gemini, Devin, Cursor, Cline, OpenCode, Aider, and Antigravity history
10249
- import chatgpt <path> import a ChatGPT export JSON/ZIP/folder
10250
- import claude-web <path> import a Claude.ai export JSON/ZIP/folder
10911
+ import chatgpt [path] show ChatGPT export instructions; with path, import a ZIP/folder
10912
+ import claude-web [path] show Claude.ai export instructions; with path, import a ZIP/folder
10251
10913
  import windsurf <path> import downloaded Windsurf trajectory Markdown file/folder
10252
10914
  import accounts list or rename ChatGPT/Claude.ai export accounts
10253
10915
  sync choose a remote, preview, confirm, then upload archive objects
@@ -10303,8 +10965,8 @@ agentlog import
10303
10965
  Usage:
10304
10966
  agentlog import --source <source> [--since 30d|all]
10305
10967
  agentlog import --sources <a,b,c> [--since 30d|all]
10306
- agentlog import chatgpt <path> [--username <name>] [--scope local|team]
10307
- agentlog import claude-web <path> [--username <name>] [--scope local|team]
10968
+ agentlog import chatgpt [path] [--username <name>] [--scope local|team]
10969
+ agentlog import claude-web [path] [--username <name>] [--scope local|team]
10308
10970
  agentlog import windsurf <file-or-folder>
10309
10971
  agentlog import accounts list
10310
10972
  agentlog import accounts rename <provider> <account-id-or-username> --display-name <name>
@@ -10312,6 +10974,7 @@ Usage:
10312
10974
  Import sources:
10313
10975
  codex-cli terminal Codex sessions from Codex state and rollout files
10314
10976
  codex-desktop Codex desktop app sessions from Codex state and rollout files
10977
+ codex-sdk high-volume Codex exec/SDK batch jobs; opt-in
10315
10978
  claude interactive Claude Code CLI JSONL transcripts
10316
10979
  claude-code-desktop Claude Code sessions launched from the Claude desktop app
10317
10980
  claude-workspace Claude app workspace/local-agent sessions
@@ -10329,13 +10992,14 @@ Import sources:
10329
10992
  all configured default local sources
10330
10993
 
10331
10994
  Web export sources:
10332
- chatgpt official ChatGPT export JSON/ZIP
10333
- claude-web official Claude.ai export JSON/ZIP
10995
+ chatgpt instructions for requesting a ChatGPT export; imports downloaded ZIP/folder
10996
+ claude-web instructions for requesting a Claude.ai export; imports downloaded ZIP/folder
10334
10997
  windsurf downloaded Cascade trajectory Markdown file/folder
10335
10998
 
10336
10999
  Examples:
10337
11000
  agentlog import --source codex-cli --since 30d
10338
11001
  agentlog import --source codex-desktop --since all
11002
+ agentlog import --source codex-sdk --since all
10339
11003
  agentlog import --source claude --since 30d
10340
11004
  agentlog import --source claude-code-desktop --since all
10341
11005
  agentlog import --source claude-workspace --since all
@@ -10351,6 +11015,8 @@ Examples:
10351
11015
  agentlog import --source cursor --since all --explain-skips
10352
11016
  agentlog import --source cursor --since all --explain-skips --json
10353
11017
  agentlog import status --json
11018
+ agentlog import chatgpt
11019
+ agentlog import claude-web
10354
11020
  agentlog import chatgpt ~/Downloads/chatgpt-export.zip --username you@example.com
10355
11021
  agentlog import claude-web ~/Downloads/claude-export --username brian --display-name Brian --scope local
10356
11022
  agentlog import windsurf ~/Downloads/cascade-chat-conversation.md
@@ -10363,7 +11029,8 @@ Details:
10363
11029
  --dry-run shows what would be imported without writing archive files.
10364
11030
  --explain-skips includes per-session skip reasons for supported sources.
10365
11031
  --json prints machine-readable import results.
10366
- In a terminal, web and Windsurf imports prompt for the export path when omitted.
11032
+ ChatGPT and Claude.ai imports print export instructions when the path is omitted.
11033
+ Windsurf imports prompt for the export path when omitted in a terminal.
10367
11034
  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
11035
  See docs/history-source-handling.md for source-specific storage paths.
10369
11036
  `;