@tonyclaw/llm-inspector 1.11.8 → 1.13.0

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.
@@ -1,5 +1,5 @@
1
1
  import { r as reactExports, j as jsxRuntimeExports, a as React } from "../_libs/react.mjs";
2
- import { C as CapturedLogSchema, a as parseRequest, s as stripClaudeCodeBillingHeader, R as RuntimeConfigSchema, P as ProviderConfigSchema, p as parseOpenAIResponse, I as InspectorResponseSchema, S as StreamingChunkSchema$1 } from "./router-DeW8M_70.mjs";
2
+ import { C as CapturedLogSchema, a as parseRequest, s as stripClaudeCodeBillingHeader, R as RuntimeConfigSchema, P as ProviderConfigSchema, p as parseOpenAIResponse, I as InspectorResponseSchema, S as StreamingChunkSchema$1 } from "./router-D5ccnemB.mjs";
3
3
  import { u as useSWR, a as useSWRConfig } from "../_libs/swr.mjs";
4
4
  import { u as useVirtualizer } from "../_libs/tanstack__react-virtual.mjs";
5
5
  import { J as JSZip } from "../_libs/jszip.mjs";
@@ -9,7 +9,7 @@ import { c as cva } from "../_libs/class-variance-authority.mjs";
9
9
  import { d as diffLines, a as diffJson } from "../_libs/diff.mjs";
10
10
  import { R as Root, T as Trigger$1, C as Content, a as Close, b as Title, P as Portal$1, O as Overlay } from "../_libs/radix-ui__react-dialog.mjs";
11
11
  import { R as Root2, T as Trigger, I as Icon, V as Value, P as Portal, C as Content2, a as Viewport, b as Item, c as ItemIndicator, d as ItemText, S as ScrollUpButton, e as ScrollDownButton } from "../_libs/radix-ui__react-select.mjs";
12
- import { D as Download, L as LayoutGrid, a as List, S as Settings, C as ChevronDown, b as Check, R as RotateCcw, X, U as Upload, P as Plus, c as Copy, d as CircleAlert, e as ChevronUp, f as ChevronRight, g as Clock, M as MessageSquare, Z as Zap, h as LoaderCircle, W as Wrench, G as Globe, i as User, F as FileTerminal, j as Radio, k as GitCompareArrows, l as Rows3, m as Columns2, E as ExternalLink, n as EyeOff, o as Eye, p as RotateCw, q as Pencil, T as Trash2, r as TriangleAlert, s as Minus, t as CircleCheckBig, u as CircleStop, v as CircleQuestionMark, w as Server, x as Gauge, y as Lock, z as Wifi, A as WifiOff, B as ChevronsUp, H as ChevronsDown, I as Brain, J as Terminal } from "../_libs/lucide-react.mjs";
12
+ import { D as Download, L as LayoutGrid, a as List, G as GitCompareArrows, X, S as Settings, C as ChevronDown, b as Check, R as RotateCcw, U as Upload, P as Plus, c as Copy, d as CircleAlert, e as ChevronUp, f as ChevronRight, g as Clock, M as MessageSquare, Z as Zap, h as LoaderCircle, W as Wrench, i as Globe, j as User, F as FileTerminal, k as Radio, l as Rows3, m as Columns2, E as ExternalLink, n as EyeOff, o as Eye, p as RotateCw, q as Pencil, T as Trash2, A as ArrowUp, r as ArrowDown, s as TriangleAlert, t as Minus, u as CircleCheckBig, v as CircleStop, w as CircleQuestionMark, x as Server, y as Gauge, z as Lock, B as Wifi, H as WifiOff, I as ChevronsUp, J as ChevronsDown, K as Brain, N as Terminal } from "../_libs/lucide-react.mjs";
13
13
  import { M as Markdown } from "../_libs/react-markdown.mjs";
14
14
  import { a as array, s as string, u as union, o as object, l as literal, n as number, b as boolean, r as record, _ as _enum } from "../_libs/zod.mjs";
15
15
  import { R as Root2$1, L as List$1, T as Trigger$2, C as Content$1 } from "../_libs/radix-ui__react-tabs.mjs";
@@ -275,7 +275,7 @@ async function exportLogsAsZip(logs) {
275
275
  document.body.removeChild(anchor);
276
276
  URL.revokeObjectURL(url);
277
277
  }
278
- const version = "1.11.8";
278
+ const version = "1.13.0";
279
279
  const packageJson = {
280
280
  version
281
281
  };
@@ -1395,12 +1395,28 @@ const STATUS_BADGE_CLASSES = {
1395
1395
  server_error: "",
1396
1396
  pending: "bg-muted text-muted-foreground border-border"
1397
1397
  };
1398
+ function CacheTrendIndicator({ trend }) {
1399
+ if (trend === null) return null;
1400
+ const isUp = trend.direction === "up";
1401
+ const Icon2 = isUp ? ArrowUp : ArrowDown;
1402
+ const sign = isUp ? "+" : "-";
1403
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex items-center gap-0.5 text-muted-foreground tabular-nums", children: [
1404
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Icon2, { className: isUp ? "size-3 text-emerald-400" : "size-3 text-rose-400" }),
1405
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-mono", children: [
1406
+ sign,
1407
+ formatTokens(trend.delta)
1408
+ ] })
1409
+ ] });
1410
+ }
1398
1411
  const LogEntryHeader = reactExports.memo(function({
1399
1412
  log,
1400
1413
  parsedRequest,
1401
1414
  expanded,
1402
1415
  onToggle,
1403
- suppressApiFormatBadge = false
1416
+ suppressApiFormatBadge = false,
1417
+ cacheTrend = null,
1418
+ isSelected = false,
1419
+ onToggleSelect
1404
1420
  }) {
1405
1421
  const statusCategory = getStatusCategory(log.responseStatus);
1406
1422
  const hasTokens = log.inputTokens !== null || log.outputTokens !== null;
@@ -1424,6 +1440,23 @@ const LogEntryHeader = reactExports.memo(function({
1424
1440
  }
1425
1441
  },
1426
1442
  children: [
1443
+ onToggleSelect !== void 0 && /* @__PURE__ */ jsxRuntimeExports.jsx(
1444
+ "button",
1445
+ {
1446
+ type: "button",
1447
+ onClick: (e) => {
1448
+ e.stopPropagation();
1449
+ onToggleSelect(log.id);
1450
+ },
1451
+ "aria-label": isSelected ? "Deselect for comparison" : "Select for comparison",
1452
+ "aria-pressed": isSelected,
1453
+ className: cn(
1454
+ "shrink-0 size-4 rounded-sm border flex items-center justify-center transition-colors cursor-pointer",
1455
+ isSelected ? "bg-amber-400 border-amber-400 text-amber-950" : "border-muted-foreground/40 hover:border-amber-400 hover:bg-amber-400/10"
1456
+ ),
1457
+ children: isSelected && /* @__PURE__ */ jsxRuntimeExports.jsx(Check, { className: "size-3", strokeWidth: 3 })
1458
+ }
1459
+ ),
1427
1460
  /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-blue-400/80 font-mono text-xs font-semibold tabular-nums shrink-0", children: [
1428
1461
  "#",
1429
1462
  log.id
@@ -1490,14 +1523,20 @@ const LogEntryHeader = reactExports.memo(function({
1490
1523
  )
1491
1524
  ] })
1492
1525
  ] }),
1493
- log.cacheCreationInputTokens !== null && log.cacheCreationInputTokens > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "flex items-center gap-1 text-xs shrink-0", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-mono tabular-nums text-emerald-400", children: [
1494
- "Cache +",
1495
- formatTokens(log.cacheCreationInputTokens)
1496
- ] }) }),
1497
- log.cacheReadInputTokens !== null && log.cacheReadInputTokens > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "flex items-center gap-1 text-xs shrink-0", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-mono tabular-nums text-purple-400", children: [
1498
- "Cache ~",
1499
- formatTokens(log.cacheReadInputTokens)
1500
- ] }) }),
1526
+ log.cacheCreationInputTokens !== null && log.cacheCreationInputTokens > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex items-center gap-1 text-xs shrink-0", children: [
1527
+ /* @__PURE__ */ jsxRuntimeExports.jsx(CacheTrendIndicator, { trend: cacheTrend?.creation ?? null }),
1528
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-mono tabular-nums text-emerald-400", children: [
1529
+ "Cache +",
1530
+ formatTokens(log.cacheCreationInputTokens)
1531
+ ] })
1532
+ ] }),
1533
+ log.cacheReadInputTokens !== null && log.cacheReadInputTokens > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex items-center gap-1 text-xs shrink-0", children: [
1534
+ /* @__PURE__ */ jsxRuntimeExports.jsx(CacheTrendIndicator, { trend: cacheTrend?.read ?? null }),
1535
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-mono tabular-nums text-purple-400", children: [
1536
+ "Cache ~",
1537
+ formatTokens(log.cacheReadInputTokens)
1538
+ ] })
1539
+ ] }),
1501
1540
  messageCount !== null && /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex items-center gap-1 text-muted-foreground text-xs shrink-0", children: [
1502
1541
  /* @__PURE__ */ jsxRuntimeExports.jsx(MessageSquare, { className: "size-3" }),
1503
1542
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-mono tabular-nums", children: messageCount })
@@ -2364,7 +2403,10 @@ const LogEntry = reactExports.memo(function({
2364
2403
  log,
2365
2404
  viewMode = "simple",
2366
2405
  suppressApiFormatBadge = false,
2367
- strip
2406
+ strip,
2407
+ cacheTrend = null,
2408
+ isSelected = false,
2409
+ onToggleSelect
2368
2410
  }) {
2369
2411
  const [expanded, setExpanded] = reactExports.useState(false);
2370
2412
  const [requestCopied, setRequestCopied] = reactExports.useState(false);
@@ -2414,163 +2456,175 @@ const LogEntry = reactExports.memo(function({
2414
2456
  });
2415
2457
  }
2416
2458
  return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
2417
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: cn("border border-border rounded-lg mb-3 overflow-hidden"), children: [
2418
- /* @__PURE__ */ jsxRuntimeExports.jsx(
2419
- LogEntryHeader,
2420
- {
2421
- log,
2422
- parsedRequest,
2423
- expanded,
2424
- onToggle: () => setExpanded(!expanded),
2425
- suppressApiFormatBadge
2426
- }
2427
- ),
2428
- expanded && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { onClick: (e) => e.stopPropagation(), onKeyDown: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsxRuntimeExports.jsxs(Tabs, { defaultValue: "request", children: [
2429
- /* @__PURE__ */ jsxRuntimeExports.jsxs(TabsList, { className: "mx-4 mt-2", children: [
2430
- viewMode === "full" && /* @__PURE__ */ jsxRuntimeExports.jsx(TabsTrigger, { value: "raw-headers", children: "Raw Headers" }),
2431
- viewMode === "full" && /* @__PURE__ */ jsxRuntimeExports.jsx(TabsTrigger, { value: "headers", children: "Headers" }),
2432
- shouldShowRawRequestTab(log.apiFormat, viewMode, strip) && /* @__PURE__ */ jsxRuntimeExports.jsx(TabsTrigger, { value: "raw-request", children: "Raw Request" }),
2433
- /* @__PURE__ */ jsxRuntimeExports.jsx(TabsTrigger, { value: "request", children: "Request" }),
2434
- viewMode === "full" && /* @__PURE__ */ jsxRuntimeExports.jsx(TabsTrigger, { value: "raw", children: "Raw Response" }),
2435
- /* @__PURE__ */ jsxRuntimeExports.jsx(TabsTrigger, { value: "parsed", children: "Response" })
2436
- ] }),
2437
- shouldShowRawRequestTab(log.apiFormat, viewMode, strip) && /* @__PURE__ */ jsxRuntimeExports.jsx(TabsContent, { value: "raw-request", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "px-4 py-3", children: [
2438
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex justify-end mb-2", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
2439
- CopyButton,
2459
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
2460
+ "div",
2461
+ {
2462
+ className: cn(
2463
+ "border border-border rounded-lg mb-3 overflow-hidden",
2464
+ isSelected && "border-l-2 border-l-amber-400"
2465
+ ),
2466
+ children: [
2467
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
2468
+ LogEntryHeader,
2440
2469
  {
2441
- text: log.rawRequestBody,
2442
- label: "Copy Raw Request",
2443
- copied: rawRequestCopied,
2444
- onCopy: handleCopyRawRequest
2470
+ log,
2471
+ parsedRequest,
2472
+ expanded,
2473
+ onToggle: () => setExpanded(!expanded),
2474
+ suppressApiFormatBadge,
2475
+ cacheTrend,
2476
+ isSelected,
2477
+ onToggleSelect
2445
2478
  }
2446
- ) }),
2447
- log.rawRequestBody !== null ? /* @__PURE__ */ jsxRuntimeExports.jsx(JsonViewerFromString, { text: log.rawRequestBody, defaultExpandDepth: 1 }) : /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-muted-foreground italic", children: "No request body" })
2448
- ] }) }),
2449
- /* @__PURE__ */ jsxRuntimeExports.jsx(TabsContent, { value: "request", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "px-4 py-3", children: [
2450
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-end gap-2 mb-2", children: [
2451
- shouldShowRequestDiffButton(
2452
- log.apiFormat,
2453
- viewMode,
2454
- strip,
2455
- log.rawRequestBody !== null
2456
- ) && /* @__PURE__ */ jsxRuntimeExports.jsx(
2457
- DiffToggleButton,
2458
- {
2459
- active: requestDiff,
2460
- onClick: (e) => {
2461
- e.stopPropagation();
2462
- setRequestDiff(!requestDiff);
2479
+ ),
2480
+ expanded && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { onClick: (e) => e.stopPropagation(), onKeyDown: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsxRuntimeExports.jsxs(Tabs, { defaultValue: "request", children: [
2481
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(TabsList, { className: "mx-4 mt-2", children: [
2482
+ viewMode === "full" && /* @__PURE__ */ jsxRuntimeExports.jsx(TabsTrigger, { value: "raw-headers", children: "Raw Headers" }),
2483
+ viewMode === "full" && /* @__PURE__ */ jsxRuntimeExports.jsx(TabsTrigger, { value: "headers", children: "Headers" }),
2484
+ shouldShowRawRequestTab(log.apiFormat, viewMode, strip) && /* @__PURE__ */ jsxRuntimeExports.jsx(TabsTrigger, { value: "raw-request", children: "Raw Request" }),
2485
+ /* @__PURE__ */ jsxRuntimeExports.jsx(TabsTrigger, { value: "request", children: "Request" }),
2486
+ viewMode === "full" && /* @__PURE__ */ jsxRuntimeExports.jsx(TabsTrigger, { value: "raw", children: "Raw Response" }),
2487
+ /* @__PURE__ */ jsxRuntimeExports.jsx(TabsTrigger, { value: "parsed", children: "Response" })
2488
+ ] }),
2489
+ shouldShowRawRequestTab(log.apiFormat, viewMode, strip) && /* @__PURE__ */ jsxRuntimeExports.jsx(TabsContent, { value: "raw-request", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "px-4 py-3", children: [
2490
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex justify-end mb-2", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
2491
+ CopyButton,
2492
+ {
2493
+ text: log.rawRequestBody,
2494
+ label: "Copy Raw Request",
2495
+ copied: rawRequestCopied,
2496
+ onCopy: handleCopyRawRequest
2463
2497
  }
2464
- }
2465
- ),
2466
- /* @__PURE__ */ jsxRuntimeExports.jsxs(
2467
- Button,
2468
- {
2469
- variant: "outline",
2470
- size: "sm",
2471
- className: "h-7 text-xs",
2472
- onClick: (e) => {
2473
- e.stopPropagation();
2474
- setReplayOpen(true);
2475
- },
2476
- children: [
2477
- /* @__PURE__ */ jsxRuntimeExports.jsx(RotateCcw, { className: "size-3 mr-1" }),
2478
- "Replay"
2479
- ]
2480
- }
2481
- ),
2482
- /* @__PURE__ */ jsxRuntimeExports.jsx(
2483
- CopyButton,
2498
+ ) }),
2499
+ log.rawRequestBody !== null ? /* @__PURE__ */ jsxRuntimeExports.jsx(JsonViewerFromString, { text: log.rawRequestBody, defaultExpandDepth: 1 }) : /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-muted-foreground italic", children: "No request body" })
2500
+ ] }) }),
2501
+ /* @__PURE__ */ jsxRuntimeExports.jsx(TabsContent, { value: "request", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "px-4 py-3", children: [
2502
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-end gap-2 mb-2", children: [
2503
+ shouldShowRequestDiffButton(
2504
+ log.apiFormat,
2505
+ viewMode,
2506
+ strip,
2507
+ log.rawRequestBody !== null
2508
+ ) && /* @__PURE__ */ jsxRuntimeExports.jsx(
2509
+ DiffToggleButton,
2510
+ {
2511
+ active: requestDiff,
2512
+ onClick: (e) => {
2513
+ e.stopPropagation();
2514
+ setRequestDiff(!requestDiff);
2515
+ }
2516
+ }
2517
+ ),
2518
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
2519
+ Button,
2520
+ {
2521
+ variant: "outline",
2522
+ size: "sm",
2523
+ className: "h-7 text-xs",
2524
+ onClick: (e) => {
2525
+ e.stopPropagation();
2526
+ setReplayOpen(true);
2527
+ },
2528
+ children: [
2529
+ /* @__PURE__ */ jsxRuntimeExports.jsx(RotateCcw, { className: "size-3 mr-1" }),
2530
+ "Replay"
2531
+ ]
2532
+ }
2533
+ ),
2534
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
2535
+ CopyButton,
2536
+ {
2537
+ text: displayedRequestBody,
2538
+ label: "Copy Request",
2539
+ copied: requestCopied,
2540
+ onCopy: handleCopyRequest
2541
+ }
2542
+ )
2543
+ ] }),
2544
+ requestDiff ? /* @__PURE__ */ jsxRuntimeExports.jsx(
2545
+ DiffView,
2546
+ {
2547
+ result: requestDiffResult,
2548
+ emptyLabel: "No transformation applied — raw and sent request bodies are identical."
2549
+ }
2550
+ ) : displayedRequestBody !== null ? /* @__PURE__ */ jsxRuntimeExports.jsx(JsonViewerFromString, { text: displayedRequestBody, defaultExpandDepth: 1 }) : /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-muted-foreground italic", children: "No request body" })
2551
+ ] }) }),
2552
+ viewMode === "full" && /* @__PURE__ */ jsxRuntimeExports.jsx(TabsContent, { value: "headers", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "px-4 py-3", children: [
2553
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex justify-end gap-2 mb-2", children: shouldShowHeadersDiffButton(
2554
+ viewMode,
2555
+ log.rawHeaders !== void 0 && Object.keys(log.rawHeaders).length > 0
2556
+ ) && /* @__PURE__ */ jsxRuntimeExports.jsx(
2557
+ DiffToggleButton,
2558
+ {
2559
+ active: headersDiff,
2560
+ onClick: (e) => {
2561
+ e.stopPropagation();
2562
+ setHeadersDiff(!headersDiff);
2563
+ }
2564
+ }
2565
+ ) }),
2566
+ headersDiff ? /* @__PURE__ */ jsxRuntimeExports.jsx(
2567
+ DiffView,
2568
+ {
2569
+ result: headersDiffResult,
2570
+ emptyLabel: "No transformation applied — raw and processed headers are identical."
2571
+ }
2572
+ ) : log.headers && Object.keys(log.headers).length > 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "space-y-1 font-mono text-xs", children: Object.entries(log.headers).sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2", children: [
2573
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-blue-600 dark:text-blue-400 font-semibold shrink-0", children: [
2574
+ key,
2575
+ ":"
2576
+ ] }),
2577
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-muted-foreground truncate", title: value, children: value })
2578
+ ] }, key)) }) : /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-muted-foreground italic", children: "No headers captured" })
2579
+ ] }) }),
2580
+ viewMode === "full" && /* @__PURE__ */ jsxRuntimeExports.jsx(TabsContent, { value: "raw-headers", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "px-4 py-3", children: log.rawHeaders && Object.keys(log.rawHeaders).length > 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "space-y-1 font-mono text-xs", children: Object.entries(log.rawHeaders).sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2", children: [
2581
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-blue-600 dark:text-blue-400 font-semibold shrink-0", children: [
2582
+ key,
2583
+ ":"
2584
+ ] }),
2585
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-muted-foreground truncate", title: value, children: value })
2586
+ ] }, key)) }) : /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-muted-foreground italic", children: "No raw headers captured" }) }) }),
2587
+ /* @__PURE__ */ jsxRuntimeExports.jsx(TabsContent, { value: "raw", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "px-4 py-3 space-y-3", children: [
2588
+ log.error !== void 0 && log.error !== null && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "rounded border border-destructive/50 bg-destructive/10 p-3 text-xs", children: [
2589
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-semibold text-destructive mb-1", children: "SSE Error" }),
2590
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-muted-foreground font-mono", children: log.error })
2591
+ ] }),
2592
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
2593
+ CopyButton,
2594
+ {
2595
+ text: log.responseText,
2596
+ label: "Copy Response",
2597
+ copied: responseCopied,
2598
+ onCopy: handleCopyResponse
2599
+ }
2600
+ ) }),
2601
+ log.responseText !== null ? /* @__PURE__ */ jsxRuntimeExports.jsx(JsonViewerFromString, { text: log.responseText, defaultExpandDepth: 1 }) : /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-muted-foreground italic", children: "No response" }),
2602
+ log.streaming === true && /* @__PURE__ */ jsxRuntimeExports.jsx(
2603
+ StreamingChunkSequence,
2604
+ {
2605
+ logId: log.id,
2606
+ truncated: log.streamingChunksPath !== null
2607
+ }
2608
+ )
2609
+ ] }) }),
2610
+ /* @__PURE__ */ jsxRuntimeExports.jsx(TabsContent, { value: "parsed", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "px-4 py-3", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
2611
+ ResponseView,
2484
2612
  {
2485
- text: displayedRequestBody,
2486
- label: "Copy Request",
2487
- copied: requestCopied,
2488
- onCopy: handleCopyRequest
2613
+ responseText: log.responseText,
2614
+ responseStatus: log.responseStatus,
2615
+ streaming: log.streaming,
2616
+ inputTokens: log.inputTokens,
2617
+ outputTokens: log.outputTokens,
2618
+ cacheCreationInputTokens: log.cacheCreationInputTokens,
2619
+ cacheReadInputTokens: log.cacheReadInputTokens,
2620
+ apiFormat: log.apiFormat,
2621
+ error: log.error
2489
2622
  }
2490
- )
2491
- ] }),
2492
- requestDiff ? /* @__PURE__ */ jsxRuntimeExports.jsx(
2493
- DiffView,
2494
- {
2495
- result: requestDiffResult,
2496
- emptyLabel: "No transformation applied — raw and sent request bodies are identical."
2497
- }
2498
- ) : displayedRequestBody !== null ? /* @__PURE__ */ jsxRuntimeExports.jsx(JsonViewerFromString, { text: displayedRequestBody, defaultExpandDepth: 1 }) : /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-muted-foreground italic", children: "No request body" })
2499
- ] }) }),
2500
- viewMode === "full" && /* @__PURE__ */ jsxRuntimeExports.jsx(TabsContent, { value: "headers", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "px-4 py-3", children: [
2501
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex justify-end gap-2 mb-2", children: shouldShowHeadersDiffButton(
2502
- viewMode,
2503
- log.rawHeaders !== void 0 && Object.keys(log.rawHeaders).length > 0
2504
- ) && /* @__PURE__ */ jsxRuntimeExports.jsx(
2505
- DiffToggleButton,
2506
- {
2507
- active: headersDiff,
2508
- onClick: (e) => {
2509
- e.stopPropagation();
2510
- setHeadersDiff(!headersDiff);
2511
- }
2512
- }
2513
- ) }),
2514
- headersDiff ? /* @__PURE__ */ jsxRuntimeExports.jsx(
2515
- DiffView,
2516
- {
2517
- result: headersDiffResult,
2518
- emptyLabel: "No transformation applied — raw and processed headers are identical."
2519
- }
2520
- ) : log.headers && Object.keys(log.headers).length > 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "space-y-1 font-mono text-xs", children: Object.entries(log.headers).sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2", children: [
2521
- /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-blue-600 dark:text-blue-400 font-semibold shrink-0", children: [
2522
- key,
2523
- ":"
2524
- ] }),
2525
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-muted-foreground truncate", title: value, children: value })
2526
- ] }, key)) }) : /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-muted-foreground italic", children: "No headers captured" })
2527
- ] }) }),
2528
- viewMode === "full" && /* @__PURE__ */ jsxRuntimeExports.jsx(TabsContent, { value: "raw-headers", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "px-4 py-3", children: log.rawHeaders && Object.keys(log.rawHeaders).length > 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "space-y-1 font-mono text-xs", children: Object.entries(log.rawHeaders).sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2", children: [
2529
- /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-blue-600 dark:text-blue-400 font-semibold shrink-0", children: [
2530
- key,
2531
- ":"
2532
- ] }),
2533
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-muted-foreground truncate", title: value, children: value })
2534
- ] }, key)) }) : /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-muted-foreground italic", children: "No raw headers captured" }) }) }),
2535
- /* @__PURE__ */ jsxRuntimeExports.jsx(TabsContent, { value: "raw", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "px-4 py-3 space-y-3", children: [
2536
- log.error !== void 0 && log.error !== null && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "rounded border border-destructive/50 bg-destructive/10 p-3 text-xs", children: [
2537
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-semibold text-destructive mb-1", children: "SSE Error" }),
2538
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-muted-foreground font-mono", children: log.error })
2539
- ] }),
2540
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
2541
- CopyButton,
2542
- {
2543
- text: log.responseText,
2544
- label: "Copy Response",
2545
- copied: responseCopied,
2546
- onCopy: handleCopyResponse
2547
- }
2548
- ) }),
2549
- log.responseText !== null ? /* @__PURE__ */ jsxRuntimeExports.jsx(JsonViewerFromString, { text: log.responseText, defaultExpandDepth: 1 }) : /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-muted-foreground italic", children: "No response" }),
2550
- log.streaming === true && /* @__PURE__ */ jsxRuntimeExports.jsx(
2551
- StreamingChunkSequence,
2552
- {
2553
- logId: log.id,
2554
- truncated: log.streamingChunksPath !== null
2555
- }
2556
- )
2557
- ] }) }),
2558
- /* @__PURE__ */ jsxRuntimeExports.jsx(TabsContent, { value: "parsed", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "px-4 py-3", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
2559
- ResponseView,
2560
- {
2561
- responseText: log.responseText,
2562
- responseStatus: log.responseStatus,
2563
- streaming: log.streaming,
2564
- inputTokens: log.inputTokens,
2565
- outputTokens: log.outputTokens,
2566
- cacheCreationInputTokens: log.cacheCreationInputTokens,
2567
- cacheReadInputTokens: log.cacheReadInputTokens,
2568
- apiFormat: log.apiFormat,
2569
- error: log.error
2570
- }
2571
- ) }) })
2572
- ] }) })
2573
- ] }),
2623
+ ) }) })
2624
+ ] }) })
2625
+ ]
2626
+ }
2627
+ ),
2574
2628
  /* @__PURE__ */ jsxRuntimeExports.jsx(ReplayDialog, { log, open: replayOpen, onOpenChange: setReplayOpen })
2575
2629
  ] });
2576
2630
  });
@@ -2586,7 +2640,10 @@ function computeStats(logs) {
2586
2640
  const ConversationGroup = reactExports.memo(function({
2587
2641
  group,
2588
2642
  viewMode = "simple",
2589
- strip
2643
+ strip,
2644
+ cacheTrends,
2645
+ selectedSet,
2646
+ onToggleSelect
2590
2647
  }) {
2591
2648
  const [expanded, setExpanded] = reactExports.useState(false);
2592
2649
  const stats = computeStats(group.logs);
@@ -2616,7 +2673,10 @@ const ConversationGroup = reactExports.memo(function({
2616
2673
  log,
2617
2674
  viewMode,
2618
2675
  suppressApiFormatBadge: !mixed,
2619
- strip
2676
+ strip,
2677
+ cacheTrend: cacheTrends?.get(log.id) ?? null,
2678
+ isSelected: selectedSet.has(log.id),
2679
+ onToggleSelect
2620
2680
  },
2621
2681
  log.id
2622
2682
  )) })
@@ -3873,6 +3933,503 @@ function ProxySettingsTab() {
3873
3933
  ] })
3874
3934
  ] });
3875
3935
  }
3936
+ function computeCacheTrends(groups) {
3937
+ const result = /* @__PURE__ */ new Map();
3938
+ for (const group of groups) {
3939
+ const logs = group.logs;
3940
+ for (let i = 1; i < logs.length; i++) {
3941
+ const prev = logs[i - 1];
3942
+ const curr = logs[i];
3943
+ if (prev === void 0 || curr === void 0) continue;
3944
+ result.set(curr.id, {
3945
+ creation: compareField(prev.cacheCreationInputTokens, curr.cacheCreationInputTokens),
3946
+ read: compareField(prev.cacheReadInputTokens, curr.cacheReadInputTokens)
3947
+ });
3948
+ }
3949
+ }
3950
+ return result;
3951
+ }
3952
+ function compareField(previous, current) {
3953
+ if (current === null) return null;
3954
+ if (previous === null) return null;
3955
+ if (current > previous) return { direction: "up", delta: current - previous };
3956
+ if (current < previous) return { direction: "down", delta: previous - current };
3957
+ return null;
3958
+ }
3959
+ const ROOT_PATH = "";
3960
+ function formatPath(segments) {
3961
+ if (segments.length === 0) return ROOT_PATH;
3962
+ let out = "";
3963
+ for (let i = 0; i < segments.length; i++) {
3964
+ const seg = segments[i];
3965
+ if (seg === void 0) continue;
3966
+ if (typeof seg === "number") {
3967
+ out += `[${seg}]`;
3968
+ } else if (i === 0) {
3969
+ out += seg;
3970
+ } else {
3971
+ out += `.${seg}`;
3972
+ }
3973
+ }
3974
+ return out;
3975
+ }
3976
+ function isPlainObject(value) {
3977
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3978
+ }
3979
+ function normalizeRequest(raw) {
3980
+ if (typeof raw === "string") {
3981
+ try {
3982
+ return toNode(JSON.parse(raw));
3983
+ } catch {
3984
+ return { kind: "primitive", value: raw };
3985
+ }
3986
+ }
3987
+ return toNode(raw);
3988
+ }
3989
+ function toNode(value) {
3990
+ if (value === null) return { kind: "primitive", value: null };
3991
+ if (typeof value === "string") return { kind: "primitive", value };
3992
+ if (typeof value === "number") return { kind: "primitive", value };
3993
+ if (typeof value === "boolean") return { kind: "primitive", value };
3994
+ if (Array.isArray(value)) {
3995
+ return { kind: "array", value: value.map((v) => toNode(v)) };
3996
+ }
3997
+ if (isPlainObject(value)) {
3998
+ const out = {};
3999
+ for (const k of Object.keys(value).sort()) {
4000
+ out[k] = toNode(value[k]);
4001
+ }
4002
+ return { kind: "object", value: out };
4003
+ }
4004
+ return { kind: "primitive", value: null };
4005
+ }
4006
+ function diffTrees(left, right) {
4007
+ const ops = [];
4008
+ walk([], left, right, ops);
4009
+ return ops;
4010
+ }
4011
+ function walk(segments, left, right, out) {
4012
+ const path = formatPath(segments);
4013
+ if (nodeEqual(left, right)) {
4014
+ out.push({ kind: "equal", path, value: left });
4015
+ return;
4016
+ }
4017
+ if (left.kind !== right.kind) {
4018
+ out.push({ kind: "changed", path, left, right });
4019
+ return;
4020
+ }
4021
+ if (left.kind === "primitive" && right.kind === "primitive") {
4022
+ out.push({ kind: "changed", path, left, right });
4023
+ return;
4024
+ }
4025
+ if (left.kind === "object" && right.kind === "object") {
4026
+ const leftKeys = Object.keys(left.value);
4027
+ const rightKeys = Object.keys(right.value);
4028
+ const rightKeySet = new Set(rightKeys);
4029
+ for (const k of leftKeys) {
4030
+ const lChild = left.value[k];
4031
+ if (lChild === void 0) continue;
4032
+ if (!rightKeySet.has(k)) {
4033
+ out.push({
4034
+ kind: "removed",
4035
+ path: formatPath([...segments, k]),
4036
+ value: lChild
4037
+ });
4038
+ } else {
4039
+ const rChild = right.value[k];
4040
+ if (rChild === void 0) continue;
4041
+ walk([...segments, k], lChild, rChild, out);
4042
+ }
4043
+ }
4044
+ for (const k of rightKeys) {
4045
+ if (leftKeys.includes(k)) continue;
4046
+ const rChild = right.value[k];
4047
+ if (rChild === void 0) continue;
4048
+ out.push({
4049
+ kind: "added",
4050
+ path: formatPath([...segments, k]),
4051
+ value: rChild
4052
+ });
4053
+ }
4054
+ return;
4055
+ }
4056
+ if (left.kind === "array" && right.kind === "array") {
4057
+ const minLen = Math.min(left.value.length, right.value.length);
4058
+ for (let i = 0; i < minLen; i++) {
4059
+ const lChild = left.value[i];
4060
+ const rChild = right.value[i];
4061
+ if (lChild === void 0 || rChild === void 0) continue;
4062
+ walk([...segments, i], lChild, rChild, out);
4063
+ }
4064
+ for (let i = minLen; i < right.value.length; i++) {
4065
+ const rChild = right.value[i];
4066
+ if (rChild === void 0) continue;
4067
+ out.push({
4068
+ kind: "added",
4069
+ path: formatPath([...segments, i]),
4070
+ value: rChild
4071
+ });
4072
+ }
4073
+ for (let i = minLen; i < left.value.length; i++) {
4074
+ const lChild = left.value[i];
4075
+ if (lChild === void 0) continue;
4076
+ out.push({
4077
+ kind: "removed",
4078
+ path: formatPath([...segments, i]),
4079
+ value: lChild
4080
+ });
4081
+ }
4082
+ }
4083
+ }
4084
+ function nodeEqual(a, b) {
4085
+ if (a.kind !== b.kind) return false;
4086
+ if (a.kind === "primitive" && b.kind === "primitive") {
4087
+ return a.value === b.value;
4088
+ }
4089
+ if (a.kind === "array" && b.kind === "array") {
4090
+ if (a.value.length !== b.value.length) return false;
4091
+ for (let i = 0; i < a.value.length; i++) {
4092
+ const ai = a.value[i];
4093
+ const bi = b.value[i];
4094
+ if (ai === void 0 || bi === void 0) return false;
4095
+ if (!nodeEqual(ai, bi)) return false;
4096
+ }
4097
+ return true;
4098
+ }
4099
+ if (a.kind === "object" && b.kind === "object") {
4100
+ const aKeys = Object.keys(a.value);
4101
+ const bKeys = Object.keys(b.value);
4102
+ if (aKeys.length !== bKeys.length) return false;
4103
+ for (const k of aKeys) {
4104
+ const av = a.value[k];
4105
+ const bv = b.value[k];
4106
+ if (av === void 0 || bv === void 0) return false;
4107
+ if (!nodeEqual(av, bv)) return false;
4108
+ }
4109
+ return true;
4110
+ }
4111
+ return false;
4112
+ }
4113
+ function previewNode(node, maxLen = 80) {
4114
+ let s;
4115
+ switch (node.kind) {
4116
+ case "primitive":
4117
+ s = node.value === null ? "null" : JSON.stringify(node.value);
4118
+ break;
4119
+ case "array":
4120
+ s = `[… ${node.value.length} items]`;
4121
+ break;
4122
+ case "object":
4123
+ s = `{… ${Object.keys(node.value).length} keys}`;
4124
+ break;
4125
+ }
4126
+ if (s.length > maxLen) s = `${s.slice(0, maxLen - 1)}…`;
4127
+ return s;
4128
+ }
4129
+ function nodeToJsonString(node, indent = 2) {
4130
+ return JSON.stringify(nodeToJsonValue(node), null, indent);
4131
+ }
4132
+ function nodeToJsonValue(node) {
4133
+ switch (node.kind) {
4134
+ case "primitive":
4135
+ return node.value;
4136
+ case "array":
4137
+ return node.value.map(nodeToJsonValue);
4138
+ case "object": {
4139
+ const out = {};
4140
+ for (const [k, v] of Object.entries(node.value)) {
4141
+ out[k] = nodeToJsonValue(v);
4142
+ }
4143
+ return out;
4144
+ }
4145
+ }
4146
+ }
4147
+ function parentPath(path) {
4148
+ if (path === "") return "";
4149
+ for (let i = path.length - 1; i >= 0; i--) {
4150
+ const ch = path[i];
4151
+ if (ch === "." || ch === "[") {
4152
+ return path.substring(0, i);
4153
+ }
4154
+ }
4155
+ return "";
4156
+ }
4157
+ function isDeepEqual(op) {
4158
+ return op.kind === "equal" && (op.value.kind === "object" || op.value.kind === "array");
4159
+ }
4160
+ function groupContiguousEquals(ops) {
4161
+ const out = [];
4162
+ let i = 0;
4163
+ while (i < ops.length) {
4164
+ const op = ops[i];
4165
+ if (op !== void 0 && isDeepEqual(op)) {
4166
+ const startParent = parentPath(op.path);
4167
+ let j = i + 1;
4168
+ while (j < ops.length) {
4169
+ const next = ops[j];
4170
+ if (next === void 0) break;
4171
+ if (!isDeepEqual(next)) break;
4172
+ if (parentPath(next.path) !== startParent) break;
4173
+ j++;
4174
+ }
4175
+ if (j - i > 1) {
4176
+ const equalOps = [];
4177
+ for (let k = i; k < j; k++) {
4178
+ const eop = ops[k];
4179
+ if (eop !== void 0 && eop.kind === "equal") {
4180
+ equalOps.push(eop);
4181
+ }
4182
+ }
4183
+ out.push({ kind: "equal-run", ops: equalOps });
4184
+ i = j;
4185
+ continue;
4186
+ }
4187
+ }
4188
+ if (op !== void 0) {
4189
+ out.push({ kind: "single", op });
4190
+ }
4191
+ i++;
4192
+ }
4193
+ return out;
4194
+ }
4195
+ function EqualRunRow({
4196
+ ops,
4197
+ expanded,
4198
+ onToggle
4199
+ }) {
4200
+ const first = ops[0];
4201
+ const last = ops[ops.length - 1];
4202
+ if (first === void 0 || last === void 0) {
4203
+ return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "col-span-3 text-muted-foreground/40 text-xs", children: "—" });
4204
+ }
4205
+ const firstPath = first.path;
4206
+ const lastPath = last.path;
4207
+ const label = ops.length === 1 ? firstPath : `${firstPath} … ${lastPath}`;
4208
+ const summary = first.value.kind === "array" ? `${ops.length} equal arrays` : first.value.kind === "object" ? `${ops.length} equal objects` : "equal";
4209
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "col-span-3", children: [
4210
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
4211
+ "button",
4212
+ {
4213
+ type: "button",
4214
+ onClick: onToggle,
4215
+ className: "w-full text-left flex items-center gap-2 px-2 py-1 text-xs text-muted-foreground hover:bg-muted/40 rounded cursor-pointer",
4216
+ children: [
4217
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
4218
+ ChevronRight,
4219
+ {
4220
+ className: cn("size-3 transition-transform shrink-0", expanded && "rotate-90")
4221
+ }
4222
+ ),
4223
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-mono truncate flex-1", children: label }),
4224
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-muted-foreground/60 shrink-0", children: [
4225
+ "(",
4226
+ summary,
4227
+ ")"
4228
+ ] })
4229
+ ]
4230
+ }
4231
+ ),
4232
+ expanded && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "ml-5 mt-1 mb-2 space-y-2", children: ops.map((op) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "border border-border/50 rounded p-2 bg-muted/20", children: [
4233
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-mono text-xs text-muted-foreground mb-1", children: op.path }),
4234
+ /* @__PURE__ */ jsxRuntimeExports.jsx(JsonViewerFromString, { text: nodeToJsonString(op.value), defaultExpandDepth: 2 })
4235
+ ] }, op.path)) })
4236
+ ] });
4237
+ }
4238
+ function AddOrRemoveRow({
4239
+ op,
4240
+ kind
4241
+ }) {
4242
+ const accent = kind === "added" ? "border-l-2 border-l-emerald-400/70 bg-emerald-500/5" : "border-l-2 border-l-rose-400/70 bg-rose-500/5";
4243
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: cn("col-span-3 px-2 py-1 rounded text-xs", accent), children: [
4244
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-mono text-xs text-muted-foreground mb-0.5", children: op.path }),
4245
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-mono break-all", children: kind === "added" ? /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-emerald-300/90", children: [
4246
+ "+ ",
4247
+ previewNode(op.value, 400)
4248
+ ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-rose-300/90 line-through", children: [
4249
+ "- ",
4250
+ previewNode(op.value, 400)
4251
+ ] }) })
4252
+ ] });
4253
+ }
4254
+ function ChangedRow({ op }) {
4255
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "col-span-3 px-2 py-1 rounded text-xs border-l-2 border-l-amber-400/70 bg-amber-500/5", children: [
4256
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-mono text-xs text-muted-foreground mb-1", children: op.path }),
4257
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-2 gap-2", children: [
4258
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-mono text-rose-300/90 break-all line-through", children: previewNode(op.left, 400) }),
4259
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-mono text-emerald-300/90 break-all", children: previewNode(op.right, 400) })
4260
+ ] })
4261
+ ] });
4262
+ }
4263
+ function SideSummary({ log, side }) {
4264
+ const conversationId = getConversationId(log);
4265
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 min-w-0 space-y-1 text-xs", children: [
4266
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [
4267
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
4268
+ Badge,
4269
+ {
4270
+ variant: "outline",
4271
+ className: cn(
4272
+ "text-[10px] px-1.5 py-0 h-5 font-mono shrink-0",
4273
+ side === "left" ? "border-rose-500/40 text-rose-400" : "border-emerald-500/40 text-emerald-400"
4274
+ ),
4275
+ children: side === "left" ? "← Left" : "Right →"
4276
+ }
4277
+ ),
4278
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-mono text-blue-400/80", children: [
4279
+ "#",
4280
+ log.id
4281
+ ] }),
4282
+ log.model !== null && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-mono text-muted-foreground truncate", children: log.model })
4283
+ ] }),
4284
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3 text-muted-foreground font-mono", children: [
4285
+ log.cacheCreationInputTokens !== null && log.cacheCreationInputTokens > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-emerald-400", children: [
4286
+ "Cache +",
4287
+ formatTokens(log.cacheCreationInputTokens)
4288
+ ] }),
4289
+ log.cacheReadInputTokens !== null && log.cacheReadInputTokens > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-purple-400", children: [
4290
+ "Cache ~",
4291
+ formatTokens(log.cacheReadInputTokens)
4292
+ ] }),
4293
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "truncate", title: log.timestamp, children: log.timestamp })
4294
+ ] }),
4295
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-muted-foreground/70 font-mono truncate", title: conversationId, children: [
4296
+ "session: ",
4297
+ conversationId
4298
+ ] })
4299
+ ] });
4300
+ }
4301
+ function CompareDrawer({ left, right, onClose }) {
4302
+ const ops = reactExports.useMemo(() => {
4303
+ const l = normalizeRequest(parseRequest(left.rawRequestBody) ?? left.rawRequestBody);
4304
+ const r = normalizeRequest(parseRequest(right.rawRequestBody) ?? right.rawRequestBody);
4305
+ return diffTrees(l, r);
4306
+ }, [left.rawRequestBody, right.rawRequestBody]);
4307
+ const grouped = reactExports.useMemo(() => groupContiguousEquals(ops), [ops]);
4308
+ const [expandedRuns, setExpandedRuns] = reactExports.useState(/* @__PURE__ */ new Set());
4309
+ const toggleRun = (idx) => {
4310
+ setExpandedRuns((prev) => {
4311
+ const next = new Set(prev);
4312
+ if (next.has(idx)) next.delete(idx);
4313
+ else next.add(idx);
4314
+ return next;
4315
+ });
4316
+ };
4317
+ reactExports.useEffect(() => {
4318
+ const onKey = (e) => {
4319
+ if (e.key === "Escape") onClose();
4320
+ };
4321
+ document.addEventListener("keydown", onKey);
4322
+ const prevOverflow = document.body.style.overflow;
4323
+ document.body.style.overflow = "hidden";
4324
+ return () => {
4325
+ document.removeEventListener("keydown", onKey);
4326
+ document.body.style.overflow = prevOverflow;
4327
+ };
4328
+ }, [onClose]);
4329
+ const sameSession = getConversationId(left) === getConversationId(right);
4330
+ const allEqual = ops.length === 1 && ops[0]?.kind === "equal";
4331
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs(
4332
+ "div",
4333
+ {
4334
+ className: "fixed inset-0 z-50 flex justify-end",
4335
+ role: "dialog",
4336
+ "aria-modal": "true",
4337
+ "aria-label": "Compare two log requests",
4338
+ children: [
4339
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
4340
+ "button",
4341
+ {
4342
+ type: "button",
4343
+ onClick: onClose,
4344
+ "aria-label": "Close compare drawer",
4345
+ className: "absolute inset-0 bg-black/40 cursor-default",
4346
+ tabIndex: -1
4347
+ }
4348
+ ),
4349
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
4350
+ "div",
4351
+ {
4352
+ className: cn(
4353
+ "relative bg-background border-l border-border shadow-xl",
4354
+ "w-full md:w-[70vw] max-w-[1100px] flex flex-col h-full"
4355
+ ),
4356
+ onClick: (e) => e.stopPropagation(),
4357
+ onKeyDown: (e) => e.stopPropagation(),
4358
+ children: [
4359
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-start gap-4 px-4 py-3 border-b border-border", children: [
4360
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 flex gap-4 min-w-0", children: [
4361
+ /* @__PURE__ */ jsxRuntimeExports.jsx(SideSummary, { log: left, side: "left" }),
4362
+ /* @__PURE__ */ jsxRuntimeExports.jsx(SideSummary, { log: right, side: "right" })
4363
+ ] }),
4364
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
4365
+ "button",
4366
+ {
4367
+ type: "button",
4368
+ onClick: onClose,
4369
+ "aria-label": "Close",
4370
+ className: "shrink-0 p-1 rounded text-muted-foreground hover:text-foreground hover:bg-muted cursor-pointer",
4371
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx(X, { className: "size-4" })
4372
+ }
4373
+ )
4374
+ ] }),
4375
+ !sameSession && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "px-4 py-1.5 text-xs text-amber-400 bg-amber-500/10 border-b border-border", children: "Heads up: the two selected logs are from different sessions." }),
4376
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 min-h-0 overflow-y-auto", children: allEqual ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "px-4 py-12 text-center text-muted-foreground text-sm", children: "The two Request payloads are identical." }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-[200px_1fr_1fr] gap-x-2 gap-y-0.5 px-3 py-2 text-xs", children: [
4377
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "col-span-3 grid grid-cols-[200px_1fr_1fr] gap-x-2 pb-2 mb-2 border-b border-border text-[10px] uppercase tracking-wider text-muted-foreground", children: [
4378
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Path" }),
4379
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [
4380
+ "Left (Log #",
4381
+ left.id,
4382
+ ")"
4383
+ ] }),
4384
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [
4385
+ "Right (Log #",
4386
+ right.id,
4387
+ ")"
4388
+ ] })
4389
+ ] }),
4390
+ grouped.map((g, i) => {
4391
+ if (g.kind === "equal-run") {
4392
+ return /* @__PURE__ */ jsxRuntimeExports.jsx(
4393
+ EqualRunRow,
4394
+ {
4395
+ ops: g.ops,
4396
+ expanded: expandedRuns.has(i),
4397
+ onToggle: () => toggleRun(i)
4398
+ },
4399
+ `r${i}`
4400
+ );
4401
+ }
4402
+ const op = g.op;
4403
+ if (op.kind === "equal") {
4404
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs(
4405
+ "div",
4406
+ {
4407
+ className: "col-span-3 grid grid-cols-[200px_1fr_1fr] gap-x-2 px-2 py-0.5 text-muted-foreground",
4408
+ children: [
4409
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-mono text-xs truncate", title: op.path, children: op.path }),
4410
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-mono text-xs break-all opacity-60", children: previewNode(op.value, 200) }),
4411
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-mono text-xs break-all opacity-60", children: previewNode(op.value, 200) })
4412
+ ]
4413
+ },
4414
+ `e${i}`
4415
+ );
4416
+ }
4417
+ if (op.kind === "added") {
4418
+ return /* @__PURE__ */ jsxRuntimeExports.jsx(AddOrRemoveRow, { op, kind: "added" }, `a${i}`);
4419
+ }
4420
+ if (op.kind === "removed") {
4421
+ return /* @__PURE__ */ jsxRuntimeExports.jsx(AddOrRemoveRow, { op, kind: "removed" }, `r${i}`);
4422
+ }
4423
+ return /* @__PURE__ */ jsxRuntimeExports.jsx(ChangedRow, { op }, `c${i}`);
4424
+ })
4425
+ ] }) })
4426
+ ]
4427
+ }
4428
+ )
4429
+ ]
4430
+ }
4431
+ );
4432
+ }
3876
4433
  function truncateSessionId(id) {
3877
4434
  if (id.length <= 30) return id;
3878
4435
  return id.slice(0, 12) + "…" + id.slice(-12);
@@ -3953,6 +4510,9 @@ function ProxyViewer({
3953
4510
  const { totalIn, totalOut } = computeTokenSummary(logs);
3954
4511
  const [groupedView, setGroupedView] = reactExports.useState(true);
3955
4512
  const [exporting, setExporting] = reactExports.useState(false);
4513
+ const [selectedLogIds, setSelectedLogIds] = reactExports.useState([]);
4514
+ const [compareOpen, setCompareOpen] = reactExports.useState(false);
4515
+ const [comparePair, setComparePair] = reactExports.useState(null);
3956
4516
  const handleExport = reactExports.useCallback(async () => {
3957
4517
  setExporting(true);
3958
4518
  try {
@@ -3962,7 +4522,75 @@ function ProxyViewer({
3962
4522
  }
3963
4523
  }, [logs]);
3964
4524
  const parentRef = reactExports.useRef(null);
4525
+ const handleToggleSelect = reactExports.useCallback((logId) => {
4526
+ setSelectedLogIds((prev) => {
4527
+ if (prev.includes(logId)) {
4528
+ return prev.filter((id) => id !== logId);
4529
+ }
4530
+ if (prev.length < 2) {
4531
+ return [...prev, logId];
4532
+ }
4533
+ const newer = prev[1];
4534
+ if (newer === void 0) return prev;
4535
+ return [newer, logId];
4536
+ });
4537
+ }, []);
4538
+ reactExports.useEffect(() => {
4539
+ setSelectedLogIds([]);
4540
+ setCompareOpen(false);
4541
+ }, [selectedSession, selectedModel]);
4542
+ const selectedSet = reactExports.useMemo(() => new Set(selectedLogIds), [selectedLogIds]);
4543
+ const openCompare = reactExports.useCallback(() => {
4544
+ if (selectedLogIds.length !== 2) return;
4545
+ const [idA, idB] = selectedLogIds;
4546
+ if (idA === void 0 || idB === void 0) return;
4547
+ const logA = logs.find((l) => l.id === idA);
4548
+ const logB = logs.find((l) => l.id === idB);
4549
+ if (logA === void 0 || logB === void 0) return;
4550
+ setComparePair([logA, logB]);
4551
+ setCompareOpen(true);
4552
+ }, [selectedLogIds, logs]);
4553
+ const closeCompare = reactExports.useCallback(() => {
4554
+ setCompareOpen(false);
4555
+ }, []);
4556
+ const clearSelection = reactExports.useCallback(() => {
4557
+ setSelectedLogIds([]);
4558
+ }, []);
4559
+ const selectedSummary = reactExports.useMemo(() => {
4560
+ if (selectedLogIds.length !== 2) return null;
4561
+ const [idA, idB] = selectedLogIds;
4562
+ if (idA === void 0 || idB === void 0) return null;
4563
+ const logA = logs.find((l) => l.id === idA);
4564
+ const logB = logs.find((l) => l.id === idB);
4565
+ if (logA === void 0 || logB === void 0) return null;
4566
+ const sameSession = getConversationId(logA) === getConversationId(logB);
4567
+ let elapsed = "";
4568
+ if (logA.timestamp !== null && logB.timestamp !== null) {
4569
+ const a = Date.parse(logA.timestamp);
4570
+ const b = Date.parse(logB.timestamp);
4571
+ if (!Number.isNaN(a) && !Number.isNaN(b)) {
4572
+ const ms = Math.abs(b - a);
4573
+ elapsed = formatElapsed2(ms);
4574
+ }
4575
+ }
4576
+ return {
4577
+ logA,
4578
+ logB,
4579
+ sameSession,
4580
+ elapsed
4581
+ };
4582
+ }, [selectedLogIds, logs]);
4583
+ function formatElapsed2(ms) {
4584
+ if (ms < 1e3) return `${ms}ms`;
4585
+ const sec = Math.floor(ms / 1e3);
4586
+ if (sec < 60) return `${sec}s`;
4587
+ const min = Math.floor(sec / 60);
4588
+ if (min < 60) return `${min}m`;
4589
+ const hr = Math.floor(min / 60);
4590
+ return `${hr}h${min % 60}m`;
4591
+ }
3965
4592
  const groups = reactExports.useMemo(() => groupLogsByConversation(logs), [logs]);
4593
+ const cacheTrends = reactExports.useMemo(() => computeCacheTrends(groups), [groups]);
3966
4594
  const renderGroups = logs.length > 0 && groupedView && !(groups.length === 1 && groups[0]?.logs.length === logs.length);
3967
4595
  const rowVirtualizer = useVirtualizer({
3968
4596
  count: renderGroups ? groups.length : logs.length,
@@ -4103,7 +4731,17 @@ function ProxyViewer({
4103
4731
  width: "100%",
4104
4732
  transform: `translateY(${virtualRow.start}px)`
4105
4733
  },
4106
- children: /* @__PURE__ */ jsxRuntimeExports.jsx(ConversationGroup, { group, viewMode, strip })
4734
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx(
4735
+ ConversationGroup,
4736
+ {
4737
+ group,
4738
+ viewMode,
4739
+ strip,
4740
+ cacheTrends,
4741
+ selectedSet,
4742
+ onToggleSelect: handleToggleSelect
4743
+ }
4744
+ )
4107
4745
  },
4108
4746
  group.id
4109
4747
  );
@@ -4122,14 +4760,58 @@ function ProxyViewer({
4122
4760
  width: "100%",
4123
4761
  transform: `translateY(${virtualRow.start}px)`
4124
4762
  },
4125
- children: /* @__PURE__ */ jsxRuntimeExports.jsx(LogEntry, { log, viewMode, strip })
4763
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx(
4764
+ LogEntry,
4765
+ {
4766
+ log,
4767
+ viewMode,
4768
+ strip,
4769
+ cacheTrend: cacheTrends.get(log.id) ?? null,
4770
+ isSelected: selectedSet.has(log.id),
4771
+ onToggleSelect: handleToggleSelect
4772
+ }
4773
+ )
4126
4774
  },
4127
4775
  log.id
4128
4776
  );
4129
4777
  }
4130
4778
  })
4131
4779
  }
4132
- ) }) })
4780
+ ) }) }),
4781
+ selectedSummary !== null && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "fixed bottom-6 left-1/2 -translate-x-1/2 z-40 flex items-center gap-3 bg-background border border-border rounded-lg shadow-lg px-4 py-2 text-xs", children: [
4782
+ /* @__PURE__ */ jsxRuntimeExports.jsx(GitCompareArrows, { className: "size-4 text-amber-400 shrink-0" }),
4783
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-muted-foreground font-mono", children: [
4784
+ "#",
4785
+ selectedSummary.logA.id,
4786
+ " ↔ #",
4787
+ selectedSummary.logB.id,
4788
+ " · ",
4789
+ selectedSummary.sameSession ? "same session" : "different sessions",
4790
+ selectedSummary.elapsed !== "" && ` · ${selectedSummary.elapsed} apart`
4791
+ ] }),
4792
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
4793
+ "button",
4794
+ {
4795
+ type: "button",
4796
+ onClick: clearSelection,
4797
+ className: "text-muted-foreground hover:text-foreground transition-colors cursor-pointer inline-flex items-center gap-1",
4798
+ children: [
4799
+ /* @__PURE__ */ jsxRuntimeExports.jsx(X, { className: "size-3" }),
4800
+ "Clear"
4801
+ ]
4802
+ }
4803
+ ),
4804
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
4805
+ "button",
4806
+ {
4807
+ type: "button",
4808
+ onClick: openCompare,
4809
+ className: "bg-amber-400 text-amber-950 hover:bg-amber-300 transition-colors px-3 py-1 rounded font-medium cursor-pointer",
4810
+ children: "Compare 2 logs"
4811
+ }
4812
+ )
4813
+ ] }),
4814
+ compareOpen && comparePair !== null && /* @__PURE__ */ jsxRuntimeExports.jsx(CompareDrawer, { left: comparePair[0], right: comparePair[1], onClose: closeCompare })
4133
4815
  ] });
4134
4816
  }
4135
4817
  object({