@tonyclaw/llm-inspector 1.12.0 → 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-PZjNwOcw.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, 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";
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.12.0";
278
+ const version = "1.13.0";
279
279
  const packageJson = {
280
280
  version
281
281
  };
@@ -1414,7 +1414,9 @@ const LogEntryHeader = reactExports.memo(function({
1414
1414
  expanded,
1415
1415
  onToggle,
1416
1416
  suppressApiFormatBadge = false,
1417
- cacheTrend = null
1417
+ cacheTrend = null,
1418
+ isSelected = false,
1419
+ onToggleSelect
1418
1420
  }) {
1419
1421
  const statusCategory = getStatusCategory(log.responseStatus);
1420
1422
  const hasTokens = log.inputTokens !== null || log.outputTokens !== null;
@@ -1438,6 +1440,23 @@ const LogEntryHeader = reactExports.memo(function({
1438
1440
  }
1439
1441
  },
1440
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
+ ),
1441
1460
  /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-blue-400/80 font-mono text-xs font-semibold tabular-nums shrink-0", children: [
1442
1461
  "#",
1443
1462
  log.id
@@ -2385,7 +2404,9 @@ const LogEntry = reactExports.memo(function({
2385
2404
  viewMode = "simple",
2386
2405
  suppressApiFormatBadge = false,
2387
2406
  strip,
2388
- cacheTrend = null
2407
+ cacheTrend = null,
2408
+ isSelected = false,
2409
+ onToggleSelect
2389
2410
  }) {
2390
2411
  const [expanded, setExpanded] = reactExports.useState(false);
2391
2412
  const [requestCopied, setRequestCopied] = reactExports.useState(false);
@@ -2435,164 +2456,175 @@ const LogEntry = reactExports.memo(function({
2435
2456
  });
2436
2457
  }
2437
2458
  return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
2438
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: cn("border border-border rounded-lg mb-3 overflow-hidden"), children: [
2439
- /* @__PURE__ */ jsxRuntimeExports.jsx(
2440
- LogEntryHeader,
2441
- {
2442
- log,
2443
- parsedRequest,
2444
- expanded,
2445
- onToggle: () => setExpanded(!expanded),
2446
- suppressApiFormatBadge,
2447
- cacheTrend
2448
- }
2449
- ),
2450
- expanded && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { onClick: (e) => e.stopPropagation(), onKeyDown: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsxRuntimeExports.jsxs(Tabs, { defaultValue: "request", children: [
2451
- /* @__PURE__ */ jsxRuntimeExports.jsxs(TabsList, { className: "mx-4 mt-2", children: [
2452
- viewMode === "full" && /* @__PURE__ */ jsxRuntimeExports.jsx(TabsTrigger, { value: "raw-headers", children: "Raw Headers" }),
2453
- viewMode === "full" && /* @__PURE__ */ jsxRuntimeExports.jsx(TabsTrigger, { value: "headers", children: "Headers" }),
2454
- shouldShowRawRequestTab(log.apiFormat, viewMode, strip) && /* @__PURE__ */ jsxRuntimeExports.jsx(TabsTrigger, { value: "raw-request", children: "Raw Request" }),
2455
- /* @__PURE__ */ jsxRuntimeExports.jsx(TabsTrigger, { value: "request", children: "Request" }),
2456
- viewMode === "full" && /* @__PURE__ */ jsxRuntimeExports.jsx(TabsTrigger, { value: "raw", children: "Raw Response" }),
2457
- /* @__PURE__ */ jsxRuntimeExports.jsx(TabsTrigger, { value: "parsed", children: "Response" })
2458
- ] }),
2459
- shouldShowRawRequestTab(log.apiFormat, viewMode, strip) && /* @__PURE__ */ jsxRuntimeExports.jsx(TabsContent, { value: "raw-request", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "px-4 py-3", children: [
2460
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex justify-end mb-2", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
2461
- 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,
2462
2469
  {
2463
- text: log.rawRequestBody,
2464
- label: "Copy Raw Request",
2465
- copied: rawRequestCopied,
2466
- onCopy: handleCopyRawRequest
2470
+ log,
2471
+ parsedRequest,
2472
+ expanded,
2473
+ onToggle: () => setExpanded(!expanded),
2474
+ suppressApiFormatBadge,
2475
+ cacheTrend,
2476
+ isSelected,
2477
+ onToggleSelect
2467
2478
  }
2468
- ) }),
2469
- 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" })
2470
- ] }) }),
2471
- /* @__PURE__ */ jsxRuntimeExports.jsx(TabsContent, { value: "request", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "px-4 py-3", children: [
2472
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-end gap-2 mb-2", children: [
2473
- shouldShowRequestDiffButton(
2474
- log.apiFormat,
2475
- viewMode,
2476
- strip,
2477
- log.rawRequestBody !== null
2478
- ) && /* @__PURE__ */ jsxRuntimeExports.jsx(
2479
- DiffToggleButton,
2480
- {
2481
- active: requestDiff,
2482
- onClick: (e) => {
2483
- e.stopPropagation();
2484
- 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
2485
2497
  }
2486
- }
2487
- ),
2488
- /* @__PURE__ */ jsxRuntimeExports.jsxs(
2489
- Button,
2490
- {
2491
- variant: "outline",
2492
- size: "sm",
2493
- className: "h-7 text-xs",
2494
- onClick: (e) => {
2495
- e.stopPropagation();
2496
- setReplayOpen(true);
2497
- },
2498
- children: [
2499
- /* @__PURE__ */ jsxRuntimeExports.jsx(RotateCcw, { className: "size-3 mr-1" }),
2500
- "Replay"
2501
- ]
2502
- }
2503
- ),
2504
- /* @__PURE__ */ jsxRuntimeExports.jsx(
2505
- 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,
2506
2612
  {
2507
- text: displayedRequestBody,
2508
- label: "Copy Request",
2509
- copied: requestCopied,
2510
- onCopy: handleCopyRequest
2511
- }
2512
- )
2513
- ] }),
2514
- requestDiff ? /* @__PURE__ */ jsxRuntimeExports.jsx(
2515
- DiffView,
2516
- {
2517
- result: requestDiffResult,
2518
- emptyLabel: "No transformation applied — raw and sent request bodies are identical."
2519
- }
2520
- ) : 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" })
2521
- ] }) }),
2522
- viewMode === "full" && /* @__PURE__ */ jsxRuntimeExports.jsx(TabsContent, { value: "headers", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "px-4 py-3", children: [
2523
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex justify-end gap-2 mb-2", children: shouldShowHeadersDiffButton(
2524
- viewMode,
2525
- log.rawHeaders !== void 0 && Object.keys(log.rawHeaders).length > 0
2526
- ) && /* @__PURE__ */ jsxRuntimeExports.jsx(
2527
- DiffToggleButton,
2528
- {
2529
- active: headersDiff,
2530
- onClick: (e) => {
2531
- e.stopPropagation();
2532
- setHeadersDiff(!headersDiff);
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
2533
2622
  }
2534
- }
2535
- ) }),
2536
- headersDiff ? /* @__PURE__ */ jsxRuntimeExports.jsx(
2537
- DiffView,
2538
- {
2539
- result: headersDiffResult,
2540
- emptyLabel: "No transformation applied — raw and processed headers are identical."
2541
- }
2542
- ) : 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: [
2543
- /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-blue-600 dark:text-blue-400 font-semibold shrink-0", children: [
2544
- key,
2545
- ":"
2546
- ] }),
2547
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-muted-foreground truncate", title: value, children: value })
2548
- ] }, key)) }) : /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-muted-foreground italic", children: "No headers captured" })
2549
- ] }) }),
2550
- 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: [
2551
- /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-blue-600 dark:text-blue-400 font-semibold shrink-0", children: [
2552
- key,
2553
- ":"
2554
- ] }),
2555
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-muted-foreground truncate", title: value, children: value })
2556
- ] }, key)) }) : /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-muted-foreground italic", children: "No raw headers captured" }) }) }),
2557
- /* @__PURE__ */ jsxRuntimeExports.jsx(TabsContent, { value: "raw", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "px-4 py-3 space-y-3", children: [
2558
- 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: [
2559
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-semibold text-destructive mb-1", children: "SSE Error" }),
2560
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-muted-foreground font-mono", children: log.error })
2561
- ] }),
2562
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
2563
- CopyButton,
2564
- {
2565
- text: log.responseText,
2566
- label: "Copy Response",
2567
- copied: responseCopied,
2568
- onCopy: handleCopyResponse
2569
- }
2570
- ) }),
2571
- 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" }),
2572
- log.streaming === true && /* @__PURE__ */ jsxRuntimeExports.jsx(
2573
- StreamingChunkSequence,
2574
- {
2575
- logId: log.id,
2576
- truncated: log.streamingChunksPath !== null
2577
- }
2578
- )
2579
- ] }) }),
2580
- /* @__PURE__ */ jsxRuntimeExports.jsx(TabsContent, { value: "parsed", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "px-4 py-3", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
2581
- ResponseView,
2582
- {
2583
- responseText: log.responseText,
2584
- responseStatus: log.responseStatus,
2585
- streaming: log.streaming,
2586
- inputTokens: log.inputTokens,
2587
- outputTokens: log.outputTokens,
2588
- cacheCreationInputTokens: log.cacheCreationInputTokens,
2589
- cacheReadInputTokens: log.cacheReadInputTokens,
2590
- apiFormat: log.apiFormat,
2591
- error: log.error
2592
- }
2593
- ) }) })
2594
- ] }) })
2595
- ] }),
2623
+ ) }) })
2624
+ ] }) })
2625
+ ]
2626
+ }
2627
+ ),
2596
2628
  /* @__PURE__ */ jsxRuntimeExports.jsx(ReplayDialog, { log, open: replayOpen, onOpenChange: setReplayOpen })
2597
2629
  ] });
2598
2630
  });
@@ -2609,7 +2641,9 @@ const ConversationGroup = reactExports.memo(function({
2609
2641
  group,
2610
2642
  viewMode = "simple",
2611
2643
  strip,
2612
- cacheTrends
2644
+ cacheTrends,
2645
+ selectedSet,
2646
+ onToggleSelect
2613
2647
  }) {
2614
2648
  const [expanded, setExpanded] = reactExports.useState(false);
2615
2649
  const stats = computeStats(group.logs);
@@ -2640,7 +2674,9 @@ const ConversationGroup = reactExports.memo(function({
2640
2674
  viewMode,
2641
2675
  suppressApiFormatBadge: !mixed,
2642
2676
  strip,
2643
- cacheTrend: cacheTrends?.get(log.id) ?? null
2677
+ cacheTrend: cacheTrends?.get(log.id) ?? null,
2678
+ isSelected: selectedSet.has(log.id),
2679
+ onToggleSelect
2644
2680
  },
2645
2681
  log.id
2646
2682
  )) })
@@ -3920,6 +3956,480 @@ function compareField(previous, current) {
3920
3956
  if (current < previous) return { direction: "down", delta: previous - current };
3921
3957
  return null;
3922
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
+ }
3923
4433
  function truncateSessionId(id) {
3924
4434
  if (id.length <= 30) return id;
3925
4435
  return id.slice(0, 12) + "…" + id.slice(-12);
@@ -4000,6 +4510,9 @@ function ProxyViewer({
4000
4510
  const { totalIn, totalOut } = computeTokenSummary(logs);
4001
4511
  const [groupedView, setGroupedView] = reactExports.useState(true);
4002
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);
4003
4516
  const handleExport = reactExports.useCallback(async () => {
4004
4517
  setExporting(true);
4005
4518
  try {
@@ -4009,6 +4522,73 @@ function ProxyViewer({
4009
4522
  }
4010
4523
  }, [logs]);
4011
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
+ }
4012
4592
  const groups = reactExports.useMemo(() => groupLogsByConversation(logs), [logs]);
4013
4593
  const cacheTrends = reactExports.useMemo(() => computeCacheTrends(groups), [groups]);
4014
4594
  const renderGroups = logs.length > 0 && groupedView && !(groups.length === 1 && groups[0]?.logs.length === logs.length);
@@ -4157,7 +4737,9 @@ function ProxyViewer({
4157
4737
  group,
4158
4738
  viewMode,
4159
4739
  strip,
4160
- cacheTrends
4740
+ cacheTrends,
4741
+ selectedSet,
4742
+ onToggleSelect: handleToggleSelect
4161
4743
  }
4162
4744
  )
4163
4745
  },
@@ -4184,7 +4766,9 @@ function ProxyViewer({
4184
4766
  log,
4185
4767
  viewMode,
4186
4768
  strip,
4187
- cacheTrend: cacheTrends.get(log.id) ?? null
4769
+ cacheTrend: cacheTrends.get(log.id) ?? null,
4770
+ isSelected: selectedSet.has(log.id),
4771
+ onToggleSelect: handleToggleSelect
4188
4772
  }
4189
4773
  )
4190
4774
  },
@@ -4193,7 +4777,41 @@ function ProxyViewer({
4193
4777
  }
4194
4778
  })
4195
4779
  }
4196
- ) }) })
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 })
4197
4815
  ] });
4198
4816
  }
4199
4817
  object({