@tonyclaw/llm-inspector 1.12.0 → 1.14.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.
Files changed (35) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/index-B5q3Llgm.css +1 -0
  3. package/.output/public/assets/index-C6tbslcs.js +105 -0
  4. package/.output/public/assets/{main-BYCM7aJx.js → main-C1k6vRnH.js} +3 -3
  5. package/.output/server/_libs/cfworker__json-schema.mjs +1 -0
  6. package/.output/server/_libs/lucide-react.mjs +64 -58
  7. package/.output/server/_libs/modelcontextprotocol__server.mjs +9738 -0
  8. package/.output/server/_libs/zod.mjs +79 -16
  9. package/.output/server/_ssr/{index-DhChP_jV.mjs → index-AxruZp16.mjs} +1201 -165
  10. package/.output/server/_ssr/index.mjs +2 -2
  11. package/.output/server/_ssr/{router-PZjNwOcw.mjs → router-DtleGqN8.mjs} +650 -29
  12. package/.output/server/_tanstack-start-manifest_v-B1WAHWIa.mjs +4 -0
  13. package/.output/server/index.mjs +24 -24
  14. package/README.md +98 -0
  15. package/package.json +3 -1
  16. package/src/components/ProxyViewer.tsx +126 -2
  17. package/src/components/proxy-viewer/CompareDrawer.tsx +880 -0
  18. package/src/components/proxy-viewer/ConversationGroup.tsx +8 -0
  19. package/src/components/proxy-viewer/LogEntry.tsx +14 -1
  20. package/src/components/proxy-viewer/LogEntryHeader.tsx +28 -0
  21. package/src/components/proxy-viewer/formats/openai/ResponseView.tsx +74 -4
  22. package/src/components/proxy-viewer/requestDiff.ts +277 -0
  23. package/src/lib/serverPort.ts +41 -0
  24. package/src/mcp/loopback.ts +76 -0
  25. package/src/mcp/previewExtractor.ts +166 -0
  26. package/src/mcp/server.ts +320 -0
  27. package/src/mcp/toolHandlers.ts +259 -0
  28. package/src/proxy/formats/openai/schemas.ts +19 -0
  29. package/src/proxy/handler.ts +23 -2
  30. package/src/proxy/openaiOrphanToolStrip.ts +148 -0
  31. package/src/proxy/schemas.ts +1 -0
  32. package/src/routes/api/mcp.ts +25 -0
  33. package/.output/public/assets/index-DVgdkDgq.js +0 -105
  34. package/.output/public/assets/index-DZx2yk8v.css +0 -1
  35. package/.output/server/_tanstack-start-manifest_v-l1kWkG0h.mjs +0 -4
@@ -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-DtleGqN8.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,9 +9,10 @@ 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 "../_libs/modelcontextprotocol__server.mjs";
13
+ 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, n as Minus, o as Pencil, E as Equal, p as ExternalLink, q as EyeOff, r as Eye, s as RotateCw, T as Trash2, A as ArrowUp, t as ArrowDown, u as TriangleAlert, v as CircleCheckBig, w as CircleStop, x as CircleQuestionMark, y as Server, z as Gauge, B as Lock, H as Wifi, I as WifiOff, J as ChevronsUp, K as ChevronsDown, N as Brain, O as Terminal } from "../_libs/lucide-react.mjs";
13
14
  import { M as Markdown } from "../_libs/react-markdown.mjs";
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
+ import { a as array, b as string, u as union, d as object, l as literal, n as number, c as boolean, r as record, _ as _enum } from "../_libs/zod.mjs";
15
16
  import { R as Root2$1, L as List$1, T as Trigger$2, C as Content$1 } from "../_libs/radix-ui__react-tabs.mjs";
16
17
  import { S as Slot } from "../_libs/radix-ui__react-slot.mjs";
17
18
  import { P as Provider, R as Root3, T as Trigger$3, a as Portal$2, C as Content2$1, A as Arrow2 } from "../_libs/radix-ui__react-tooltip.mjs";
@@ -275,7 +276,7 @@ async function exportLogsAsZip(logs) {
275
276
  document.body.removeChild(anchor);
276
277
  URL.revokeObjectURL(url);
277
278
  }
278
- const version = "1.12.0";
279
+ const version = "1.14.0";
279
280
  const packageJson = {
280
281
  version
281
282
  };
@@ -1414,7 +1415,9 @@ const LogEntryHeader = reactExports.memo(function({
1414
1415
  expanded,
1415
1416
  onToggle,
1416
1417
  suppressApiFormatBadge = false,
1417
- cacheTrend = null
1418
+ cacheTrend = null,
1419
+ isSelected = false,
1420
+ onToggleSelect
1418
1421
  }) {
1419
1422
  const statusCategory = getStatusCategory(log.responseStatus);
1420
1423
  const hasTokens = log.inputTokens !== null || log.outputTokens !== null;
@@ -1438,6 +1441,23 @@ const LogEntryHeader = reactExports.memo(function({
1438
1441
  }
1439
1442
  },
1440
1443
  children: [
1444
+ onToggleSelect !== void 0 && /* @__PURE__ */ jsxRuntimeExports.jsx(
1445
+ "button",
1446
+ {
1447
+ type: "button",
1448
+ onClick: (e) => {
1449
+ e.stopPropagation();
1450
+ onToggleSelect(log.id);
1451
+ },
1452
+ "aria-label": isSelected ? "Deselect for comparison" : "Select for comparison",
1453
+ "aria-pressed": isSelected,
1454
+ className: cn(
1455
+ "shrink-0 size-4 rounded-sm border flex items-center justify-center transition-colors cursor-pointer",
1456
+ isSelected ? "bg-amber-400 border-amber-400 text-amber-950" : "border-muted-foreground/40 hover:border-amber-400 hover:bg-amber-400/10"
1457
+ ),
1458
+ children: isSelected && /* @__PURE__ */ jsxRuntimeExports.jsx(Check, { className: "size-3", strokeWidth: 3 })
1459
+ }
1460
+ ),
1441
1461
  /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-blue-400/80 font-mono text-xs font-semibold tabular-nums shrink-0", children: [
1442
1462
  "#",
1443
1463
  log.id
@@ -1869,9 +1889,37 @@ function StructuredResponseViewAnthropic({
1869
1889
  ] })
1870
1890
  ] });
1871
1891
  }
1892
+ function parseToolArguments(raw) {
1893
+ if (raw === void 0 || raw === "") return {};
1894
+ try {
1895
+ return JSON.parse(raw);
1896
+ } catch {
1897
+ return null;
1898
+ }
1899
+ }
1900
+ function OpenAIToolCallBlock({ call }) {
1901
+ const [open, setOpen] = reactExports.useState(false);
1902
+ const name = call.function.name ?? "(unnamed tool)";
1903
+ const parsed = parseToolArguments(call.function.arguments);
1904
+ return /* @__PURE__ */ jsxRuntimeExports.jsx(Collapsible, { open, onOpenChange: setOpen, children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "border-l-2 border-blue-500/40 my-1", children: [
1905
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(CollapsibleTrigger, { className: "flex items-center gap-1.5 px-3 py-1 w-full text-left cursor-pointer hover:bg-blue-500/5 transition-colors rounded-r-sm group", children: [
1906
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Terminal, { className: "size-3.5 text-blue-400 shrink-0" }),
1907
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Badge, { variant: "outline", className: "text-[10px] font-mono px-1.5 py-0 h-4", children: name }),
1908
+ call.id !== void 0 && call.id !== "" && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] font-mono text-muted-foreground/60 truncate", children: call.id }),
1909
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "flex-1" }),
1910
+ open ? /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronDown, { className: "size-3 text-muted-foreground" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronRight, { className: "size-3 text-muted-foreground" })
1911
+ ] }),
1912
+ /* @__PURE__ */ jsxRuntimeExports.jsx(CollapsibleContent, { children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "px-3 pb-2", children: /* @__PURE__ */ jsxRuntimeExports.jsx(ScrollArea, { className: "max-h-[60vh]", children: parsed === null ? (
1913
+ // JSON.parse failed — show the raw string so the user can
1914
+ // still see what the model tried to call.
1915
+ /* @__PURE__ */ jsxRuntimeExports.jsx("pre", { className: "font-mono text-xs whitespace-pre-wrap break-words text-rose-300/90", children: call.function.arguments })
1916
+ ) : /* @__PURE__ */ jsxRuntimeExports.jsx(JsonViewer, { data: safeJsonValue(parsed), defaultExpandDepth: 2 }) }) }) })
1917
+ ] }) });
1918
+ }
1872
1919
  function OpenAIResponseView({ response }) {
1873
1920
  const choice = response.choices[0];
1874
1921
  const message = choice?.message;
1922
+ const toolCalls = message?.tool_calls ?? [];
1875
1923
  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-3", children: [
1876
1924
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [
1877
1925
  /* @__PURE__ */ jsxRuntimeExports.jsx(Badge, { variant: "secondary", className: "text-[10px] px-1.5 py-0 h-5 font-mono", children: response.model }),
@@ -1908,6 +1956,10 @@ function OpenAIResponseView({ response }) {
1908
1956
  remainingText.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "prose prose-sm dark:prose-invert max-w-none [&_pre]:bg-muted [&_pre]:text-foreground [&_code]:text-[0.8em] [&_p]:my-1 [&_ul]:my-1 [&_ol]:my-1", children: /* @__PURE__ */ jsxRuntimeExports.jsx(Markdown, { children: remainingText }) })
1909
1957
  ] });
1910
1958
  })(),
1959
+ toolCalls.map((call, i) => (
1960
+ // biome-ignore lint/suspicious/noArrayIndexKey: tool_calls is the positionally stable list from the response
1961
+ /* @__PURE__ */ jsxRuntimeExports.jsx(OpenAIToolCallBlock, { call }, call.id ?? `tc-${i}`)
1962
+ )),
1911
1963
  message?.function_call !== null && message?.function_call !== void 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "border border-blue-500/30 rounded-md p-3 bg-blue-500/5", children: [
1912
1964
  /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs text-blue-400 font-mono mb-1", children: "function_call" }),
1913
1965
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "font-mono text-xs", children: [
@@ -1919,7 +1971,7 @@ function OpenAIResponseView({ response }) {
1919
1971
  ] })
1920
1972
  ] })
1921
1973
  ] }),
1922
- (message?.content === null || message?.content === void 0 || message.content.length === 0) && (message?.reasoning_content === null || message?.reasoning_content === void 0 || message.reasoning_content.length === 0) && (message?.function_call === null || message?.function_call === void 0) && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-muted-foreground italic", children: "Empty response content" })
1974
+ (message?.content === null || message?.content === void 0 || message.content.length === 0) && (message?.reasoning_content === null || message?.reasoning_content === void 0 || message.reasoning_content.length === 0) && (message?.function_call === null || message?.function_call === void 0) && toolCalls.length === 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-muted-foreground italic", children: "Empty response content" })
1923
1975
  ] })
1924
1976
  ] });
1925
1977
  }
@@ -2385,7 +2437,9 @@ const LogEntry = reactExports.memo(function({
2385
2437
  viewMode = "simple",
2386
2438
  suppressApiFormatBadge = false,
2387
2439
  strip,
2388
- cacheTrend = null
2440
+ cacheTrend = null,
2441
+ isSelected = false,
2442
+ onToggleSelect
2389
2443
  }) {
2390
2444
  const [expanded, setExpanded] = reactExports.useState(false);
2391
2445
  const [requestCopied, setRequestCopied] = reactExports.useState(false);
@@ -2435,164 +2489,175 @@ const LogEntry = reactExports.memo(function({
2435
2489
  });
2436
2490
  }
2437
2491
  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,
2492
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
2493
+ "div",
2494
+ {
2495
+ className: cn(
2496
+ "border border-border rounded-lg mb-3 overflow-hidden",
2497
+ isSelected && "border-l-2 border-l-amber-400"
2498
+ ),
2499
+ children: [
2500
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
2501
+ LogEntryHeader,
2462
2502
  {
2463
- text: log.rawRequestBody,
2464
- label: "Copy Raw Request",
2465
- copied: rawRequestCopied,
2466
- onCopy: handleCopyRawRequest
2503
+ log,
2504
+ parsedRequest,
2505
+ expanded,
2506
+ onToggle: () => setExpanded(!expanded),
2507
+ suppressApiFormatBadge,
2508
+ cacheTrend,
2509
+ isSelected,
2510
+ onToggleSelect
2467
2511
  }
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);
2512
+ ),
2513
+ expanded && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { onClick: (e) => e.stopPropagation(), onKeyDown: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsxRuntimeExports.jsxs(Tabs, { defaultValue: "request", children: [
2514
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(TabsList, { className: "mx-4 mt-2", children: [
2515
+ viewMode === "full" && /* @__PURE__ */ jsxRuntimeExports.jsx(TabsTrigger, { value: "raw-headers", children: "Raw Headers" }),
2516
+ viewMode === "full" && /* @__PURE__ */ jsxRuntimeExports.jsx(TabsTrigger, { value: "headers", children: "Headers" }),
2517
+ shouldShowRawRequestTab(log.apiFormat, viewMode, strip) && /* @__PURE__ */ jsxRuntimeExports.jsx(TabsTrigger, { value: "raw-request", children: "Raw Request" }),
2518
+ /* @__PURE__ */ jsxRuntimeExports.jsx(TabsTrigger, { value: "request", children: "Request" }),
2519
+ viewMode === "full" && /* @__PURE__ */ jsxRuntimeExports.jsx(TabsTrigger, { value: "raw", children: "Raw Response" }),
2520
+ /* @__PURE__ */ jsxRuntimeExports.jsx(TabsTrigger, { value: "parsed", children: "Response" })
2521
+ ] }),
2522
+ shouldShowRawRequestTab(log.apiFormat, viewMode, strip) && /* @__PURE__ */ jsxRuntimeExports.jsx(TabsContent, { value: "raw-request", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "px-4 py-3", children: [
2523
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex justify-end mb-2", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
2524
+ CopyButton,
2525
+ {
2526
+ text: log.rawRequestBody,
2527
+ label: "Copy Raw Request",
2528
+ copied: rawRequestCopied,
2529
+ onCopy: handleCopyRawRequest
2485
2530
  }
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,
2531
+ ) }),
2532
+ 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" })
2533
+ ] }) }),
2534
+ /* @__PURE__ */ jsxRuntimeExports.jsx(TabsContent, { value: "request", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "px-4 py-3", children: [
2535
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-end gap-2 mb-2", children: [
2536
+ shouldShowRequestDiffButton(
2537
+ log.apiFormat,
2538
+ viewMode,
2539
+ strip,
2540
+ log.rawRequestBody !== null
2541
+ ) && /* @__PURE__ */ jsxRuntimeExports.jsx(
2542
+ DiffToggleButton,
2543
+ {
2544
+ active: requestDiff,
2545
+ onClick: (e) => {
2546
+ e.stopPropagation();
2547
+ setRequestDiff(!requestDiff);
2548
+ }
2549
+ }
2550
+ ),
2551
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
2552
+ Button,
2553
+ {
2554
+ variant: "outline",
2555
+ size: "sm",
2556
+ className: "h-7 text-xs",
2557
+ onClick: (e) => {
2558
+ e.stopPropagation();
2559
+ setReplayOpen(true);
2560
+ },
2561
+ children: [
2562
+ /* @__PURE__ */ jsxRuntimeExports.jsx(RotateCcw, { className: "size-3 mr-1" }),
2563
+ "Replay"
2564
+ ]
2565
+ }
2566
+ ),
2567
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
2568
+ CopyButton,
2569
+ {
2570
+ text: displayedRequestBody,
2571
+ label: "Copy Request",
2572
+ copied: requestCopied,
2573
+ onCopy: handleCopyRequest
2574
+ }
2575
+ )
2576
+ ] }),
2577
+ requestDiff ? /* @__PURE__ */ jsxRuntimeExports.jsx(
2578
+ DiffView,
2579
+ {
2580
+ result: requestDiffResult,
2581
+ emptyLabel: "No transformation applied — raw and sent request bodies are identical."
2582
+ }
2583
+ ) : 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" })
2584
+ ] }) }),
2585
+ viewMode === "full" && /* @__PURE__ */ jsxRuntimeExports.jsx(TabsContent, { value: "headers", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "px-4 py-3", children: [
2586
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex justify-end gap-2 mb-2", children: shouldShowHeadersDiffButton(
2587
+ viewMode,
2588
+ log.rawHeaders !== void 0 && Object.keys(log.rawHeaders).length > 0
2589
+ ) && /* @__PURE__ */ jsxRuntimeExports.jsx(
2590
+ DiffToggleButton,
2591
+ {
2592
+ active: headersDiff,
2593
+ onClick: (e) => {
2594
+ e.stopPropagation();
2595
+ setHeadersDiff(!headersDiff);
2596
+ }
2597
+ }
2598
+ ) }),
2599
+ headersDiff ? /* @__PURE__ */ jsxRuntimeExports.jsx(
2600
+ DiffView,
2601
+ {
2602
+ result: headersDiffResult,
2603
+ emptyLabel: "No transformation applied — raw and processed headers are identical."
2604
+ }
2605
+ ) : 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: [
2606
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-blue-600 dark:text-blue-400 font-semibold shrink-0", children: [
2607
+ key,
2608
+ ":"
2609
+ ] }),
2610
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-muted-foreground truncate", title: value, children: value })
2611
+ ] }, key)) }) : /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-muted-foreground italic", children: "No headers captured" })
2612
+ ] }) }),
2613
+ 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: [
2614
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-blue-600 dark:text-blue-400 font-semibold shrink-0", children: [
2615
+ key,
2616
+ ":"
2617
+ ] }),
2618
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-muted-foreground truncate", title: value, children: value })
2619
+ ] }, key)) }) : /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-muted-foreground italic", children: "No raw headers captured" }) }) }),
2620
+ /* @__PURE__ */ jsxRuntimeExports.jsx(TabsContent, { value: "raw", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "px-4 py-3 space-y-3", children: [
2621
+ 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: [
2622
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-semibold text-destructive mb-1", children: "SSE Error" }),
2623
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-muted-foreground font-mono", children: log.error })
2624
+ ] }),
2625
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
2626
+ CopyButton,
2627
+ {
2628
+ text: log.responseText,
2629
+ label: "Copy Response",
2630
+ copied: responseCopied,
2631
+ onCopy: handleCopyResponse
2632
+ }
2633
+ ) }),
2634
+ 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" }),
2635
+ log.streaming === true && /* @__PURE__ */ jsxRuntimeExports.jsx(
2636
+ StreamingChunkSequence,
2637
+ {
2638
+ logId: log.id,
2639
+ truncated: log.streamingChunksPath !== null
2640
+ }
2641
+ )
2642
+ ] }) }),
2643
+ /* @__PURE__ */ jsxRuntimeExports.jsx(TabsContent, { value: "parsed", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "px-4 py-3", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
2644
+ ResponseView,
2506
2645
  {
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);
2646
+ responseText: log.responseText,
2647
+ responseStatus: log.responseStatus,
2648
+ streaming: log.streaming,
2649
+ inputTokens: log.inputTokens,
2650
+ outputTokens: log.outputTokens,
2651
+ cacheCreationInputTokens: log.cacheCreationInputTokens,
2652
+ cacheReadInputTokens: log.cacheReadInputTokens,
2653
+ apiFormat: log.apiFormat,
2654
+ error: log.error
2533
2655
  }
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
- ] }),
2656
+ ) }) })
2657
+ ] }) })
2658
+ ]
2659
+ }
2660
+ ),
2596
2661
  /* @__PURE__ */ jsxRuntimeExports.jsx(ReplayDialog, { log, open: replayOpen, onOpenChange: setReplayOpen })
2597
2662
  ] });
2598
2663
  });
@@ -2609,7 +2674,9 @@ const ConversationGroup = reactExports.memo(function({
2609
2674
  group,
2610
2675
  viewMode = "simple",
2611
2676
  strip,
2612
- cacheTrends
2677
+ cacheTrends,
2678
+ selectedSet,
2679
+ onToggleSelect
2613
2680
  }) {
2614
2681
  const [expanded, setExpanded] = reactExports.useState(false);
2615
2682
  const stats = computeStats(group.logs);
@@ -2640,7 +2707,9 @@ const ConversationGroup = reactExports.memo(function({
2640
2707
  viewMode,
2641
2708
  suppressApiFormatBadge: !mixed,
2642
2709
  strip,
2643
- cacheTrend: cacheTrends?.get(log.id) ?? null
2710
+ cacheTrend: cacheTrends?.get(log.id) ?? null,
2711
+ isSelected: selectedSet.has(log.id),
2712
+ onToggleSelect
2644
2713
  },
2645
2714
  log.id
2646
2715
  )) })
@@ -3920,6 +3989,865 @@ function compareField(previous, current) {
3920
3989
  if (current < previous) return { direction: "down", delta: previous - current };
3921
3990
  return null;
3922
3991
  }
3992
+ const ROOT_PATH = "";
3993
+ function formatPath(segments) {
3994
+ if (segments.length === 0) return ROOT_PATH;
3995
+ let out = "";
3996
+ for (let i = 0; i < segments.length; i++) {
3997
+ const seg = segments[i];
3998
+ if (seg === void 0) continue;
3999
+ if (typeof seg === "number") {
4000
+ out += `[${seg}]`;
4001
+ } else if (i === 0) {
4002
+ out += seg;
4003
+ } else {
4004
+ out += `.${seg}`;
4005
+ }
4006
+ }
4007
+ return out;
4008
+ }
4009
+ function isPlainObject(value) {
4010
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4011
+ }
4012
+ function normalizeRequest(raw) {
4013
+ if (typeof raw === "string") {
4014
+ try {
4015
+ return toNode(JSON.parse(raw));
4016
+ } catch {
4017
+ return { kind: "primitive", value: raw };
4018
+ }
4019
+ }
4020
+ return toNode(raw);
4021
+ }
4022
+ function toNode(value) {
4023
+ if (value === null) return { kind: "primitive", value: null };
4024
+ if (typeof value === "string") return { kind: "primitive", value };
4025
+ if (typeof value === "number") return { kind: "primitive", value };
4026
+ if (typeof value === "boolean") return { kind: "primitive", value };
4027
+ if (Array.isArray(value)) {
4028
+ return { kind: "array", value: value.map((v) => toNode(v)) };
4029
+ }
4030
+ if (isPlainObject(value)) {
4031
+ const out = {};
4032
+ for (const k of Object.keys(value).sort()) {
4033
+ out[k] = toNode(value[k]);
4034
+ }
4035
+ return { kind: "object", value: out };
4036
+ }
4037
+ return { kind: "primitive", value: null };
4038
+ }
4039
+ function diffTrees(left, right) {
4040
+ const ops = [];
4041
+ walk([], left, right, ops);
4042
+ return ops;
4043
+ }
4044
+ function walk(segments, left, right, out) {
4045
+ const path = formatPath(segments);
4046
+ if (nodeEqual(left, right)) {
4047
+ out.push({ kind: "equal", path, value: left });
4048
+ return;
4049
+ }
4050
+ if (left.kind !== right.kind) {
4051
+ out.push({ kind: "changed", path, left, right });
4052
+ return;
4053
+ }
4054
+ if (left.kind === "primitive" && right.kind === "primitive") {
4055
+ out.push({ kind: "changed", path, left, right });
4056
+ return;
4057
+ }
4058
+ if (left.kind === "object" && right.kind === "object") {
4059
+ const leftKeys = Object.keys(left.value);
4060
+ const rightKeys = Object.keys(right.value);
4061
+ const rightKeySet = new Set(rightKeys);
4062
+ for (const k of leftKeys) {
4063
+ const lChild = left.value[k];
4064
+ if (lChild === void 0) continue;
4065
+ if (!rightKeySet.has(k)) {
4066
+ out.push({
4067
+ kind: "removed",
4068
+ path: formatPath([...segments, k]),
4069
+ value: lChild
4070
+ });
4071
+ } else {
4072
+ const rChild = right.value[k];
4073
+ if (rChild === void 0) continue;
4074
+ walk([...segments, k], lChild, rChild, out);
4075
+ }
4076
+ }
4077
+ for (const k of rightKeys) {
4078
+ if (leftKeys.includes(k)) continue;
4079
+ const rChild = right.value[k];
4080
+ if (rChild === void 0) continue;
4081
+ out.push({
4082
+ kind: "added",
4083
+ path: formatPath([...segments, k]),
4084
+ value: rChild
4085
+ });
4086
+ }
4087
+ return;
4088
+ }
4089
+ if (left.kind === "array" && right.kind === "array") {
4090
+ const minLen = Math.min(left.value.length, right.value.length);
4091
+ for (let i = 0; i < minLen; i++) {
4092
+ const lChild = left.value[i];
4093
+ const rChild = right.value[i];
4094
+ if (lChild === void 0 || rChild === void 0) continue;
4095
+ walk([...segments, i], lChild, rChild, out);
4096
+ }
4097
+ for (let i = minLen; i < right.value.length; i++) {
4098
+ const rChild = right.value[i];
4099
+ if (rChild === void 0) continue;
4100
+ out.push({
4101
+ kind: "added",
4102
+ path: formatPath([...segments, i]),
4103
+ value: rChild
4104
+ });
4105
+ }
4106
+ for (let i = minLen; i < left.value.length; i++) {
4107
+ const lChild = left.value[i];
4108
+ if (lChild === void 0) continue;
4109
+ out.push({
4110
+ kind: "removed",
4111
+ path: formatPath([...segments, i]),
4112
+ value: lChild
4113
+ });
4114
+ }
4115
+ }
4116
+ }
4117
+ function nodeEqual(a, b) {
4118
+ if (a.kind !== b.kind) return false;
4119
+ if (a.kind === "primitive" && b.kind === "primitive") {
4120
+ return a.value === b.value;
4121
+ }
4122
+ if (a.kind === "array" && b.kind === "array") {
4123
+ if (a.value.length !== b.value.length) return false;
4124
+ for (let i = 0; i < a.value.length; i++) {
4125
+ const ai = a.value[i];
4126
+ const bi = b.value[i];
4127
+ if (ai === void 0 || bi === void 0) return false;
4128
+ if (!nodeEqual(ai, bi)) return false;
4129
+ }
4130
+ return true;
4131
+ }
4132
+ if (a.kind === "object" && b.kind === "object") {
4133
+ const aKeys = Object.keys(a.value);
4134
+ const bKeys = Object.keys(b.value);
4135
+ if (aKeys.length !== bKeys.length) return false;
4136
+ for (const k of aKeys) {
4137
+ const av = a.value[k];
4138
+ const bv = b.value[k];
4139
+ if (av === void 0 || bv === void 0) return false;
4140
+ if (!nodeEqual(av, bv)) return false;
4141
+ }
4142
+ return true;
4143
+ }
4144
+ return false;
4145
+ }
4146
+ function previewNode(node, maxLen = 80) {
4147
+ let s;
4148
+ switch (node.kind) {
4149
+ case "primitive":
4150
+ s = node.value === null ? "null" : JSON.stringify(node.value);
4151
+ break;
4152
+ case "array":
4153
+ s = `[… ${node.value.length} items]`;
4154
+ break;
4155
+ case "object":
4156
+ s = `{… ${Object.keys(node.value).length} keys}`;
4157
+ break;
4158
+ }
4159
+ if (s.length > maxLen) s = `${s.slice(0, maxLen - 1)}…`;
4160
+ return s;
4161
+ }
4162
+ function nodeToJsonString(node, indent = 2) {
4163
+ return JSON.stringify(nodeToJsonValue(node), null, indent);
4164
+ }
4165
+ function nodeToJsonValue(node) {
4166
+ switch (node.kind) {
4167
+ case "primitive":
4168
+ return node.value;
4169
+ case "array":
4170
+ return node.value.map(nodeToJsonValue);
4171
+ case "object": {
4172
+ const out = {};
4173
+ for (const [k, v] of Object.entries(node.value)) {
4174
+ out[k] = nodeToJsonValue(v);
4175
+ }
4176
+ return out;
4177
+ }
4178
+ }
4179
+ }
4180
+ function parentPath(path) {
4181
+ if (path === "") return "";
4182
+ for (let i = path.length - 1; i >= 0; i--) {
4183
+ const ch = path[i];
4184
+ if (ch === "." || ch === "[") {
4185
+ return path.substring(0, i);
4186
+ }
4187
+ }
4188
+ return "";
4189
+ }
4190
+ function isDeepEqual(op) {
4191
+ return op.kind === "equal" && (op.value.kind === "object" || op.value.kind === "array");
4192
+ }
4193
+ function groupContiguousEquals(ops) {
4194
+ const out = [];
4195
+ let i = 0;
4196
+ while (i < ops.length) {
4197
+ const op = ops[i];
4198
+ if (op !== void 0 && isDeepEqual(op)) {
4199
+ const startParent = parentPath(op.path);
4200
+ let j = i + 1;
4201
+ while (j < ops.length) {
4202
+ const next = ops[j];
4203
+ if (next === void 0) break;
4204
+ if (!isDeepEqual(next)) break;
4205
+ if (parentPath(next.path) !== startParent) break;
4206
+ j++;
4207
+ }
4208
+ if (j - i > 1) {
4209
+ const equalOps = [];
4210
+ for (let k = i; k < j; k++) {
4211
+ const eop = ops[k];
4212
+ if (eop !== void 0 && eop.kind === "equal") {
4213
+ equalOps.push(eop);
4214
+ }
4215
+ }
4216
+ out.push({ kind: "equal-run", ops: equalOps });
4217
+ i = j;
4218
+ continue;
4219
+ }
4220
+ }
4221
+ if (op !== void 0) {
4222
+ out.push({ kind: "single", op });
4223
+ }
4224
+ i++;
4225
+ }
4226
+ return out;
4227
+ }
4228
+ const KIND_VISUAL = {
4229
+ added: {
4230
+ icon: Plus,
4231
+ accent: "text-emerald-600 dark:text-emerald-400",
4232
+ bg: "bg-emerald-500/5 hover:bg-emerald-500/10",
4233
+ border: "border-l-emerald-500",
4234
+ label: "ADDED"
4235
+ },
4236
+ removed: {
4237
+ icon: Minus,
4238
+ accent: "text-rose-600 dark:text-rose-400",
4239
+ bg: "bg-rose-500/5 hover:bg-rose-500/10",
4240
+ border: "border-l-rose-500",
4241
+ label: "REMOVED"
4242
+ },
4243
+ changed: {
4244
+ icon: Pencil,
4245
+ accent: "text-amber-600 dark:text-amber-400",
4246
+ bg: "bg-amber-500/5 hover:bg-amber-500/10",
4247
+ border: "border-l-amber-500",
4248
+ label: "CHANGED"
4249
+ },
4250
+ equal: {
4251
+ icon: Equal,
4252
+ accent: "text-muted-foreground/70",
4253
+ bg: "bg-muted/20 hover:bg-muted/30",
4254
+ border: "border-l-muted-foreground/20",
4255
+ label: "EQUAL"
4256
+ }
4257
+ };
4258
+ function EqualRunRow({
4259
+ ops,
4260
+ expanded,
4261
+ onToggle
4262
+ }) {
4263
+ const first = ops[0];
4264
+ const last = ops[ops.length - 1];
4265
+ if (first === void 0 || last === void 0) {
4266
+ return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-muted-foreground/40 text-xs", children: "—" });
4267
+ }
4268
+ const firstPath = first.path;
4269
+ const lastPath = last.path;
4270
+ const label = ops.length === 1 ? firstPath : `${firstPath} … ${lastPath}`;
4271
+ const summary = first.value.kind === "array" ? `${ops.length} equal arrays` : first.value.kind === "object" ? `${ops.length} equal objects` : "equal";
4272
+ const v = KIND_VISUAL.equal;
4273
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: cn("border-l-4 rounded-sm", v.border, v.bg), children: [
4274
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
4275
+ "button",
4276
+ {
4277
+ type: "button",
4278
+ onClick: onToggle,
4279
+ className: "w-full text-left flex items-center gap-2 px-3 py-1.5 text-xs text-muted-foreground cursor-pointer",
4280
+ children: [
4281
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
4282
+ ChevronRight,
4283
+ {
4284
+ className: cn("size-3 transition-transform shrink-0", expanded && "rotate-90")
4285
+ }
4286
+ ),
4287
+ /* @__PURE__ */ jsxRuntimeExports.jsx(v.icon, { className: cn("size-3 shrink-0", v.accent) }),
4288
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-mono truncate flex-1", title: `${firstPath} … ${lastPath}`, children: label }),
4289
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: cn("text-[10px] uppercase tracking-wider shrink-0", v.accent), children: v.label }),
4290
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-muted-foreground/60 shrink-0", children: [
4291
+ "(",
4292
+ summary,
4293
+ ")"
4294
+ ] })
4295
+ ]
4296
+ }
4297
+ ),
4298
+ expanded && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "ml-5 mt-1 mb-2 space-y-2 pr-2", children: ops.map((op) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "border border-border/50 rounded p-2 bg-muted/20", children: [
4299
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-mono text-xs text-muted-foreground mb-1", children: op.path }),
4300
+ /* @__PURE__ */ jsxRuntimeExports.jsx(JsonViewerFromString, { text: nodeToJsonString(op.value), defaultExpandDepth: 2 })
4301
+ ] }, op.path)) })
4302
+ ] });
4303
+ }
4304
+ function UnifiedOpRow({
4305
+ op,
4306
+ idx,
4307
+ copiedPath,
4308
+ onCopyPath,
4309
+ expanded,
4310
+ onToggle
4311
+ }) {
4312
+ const v = KIND_VISUAL[op.kind];
4313
+ const Icon2 = v.icon;
4314
+ const isExpandable2 = op.kind === "added" || op.kind === "removed" ? op.value.kind === "object" || op.value.kind === "array" : op.kind === "changed" ? op.left.kind === "object" || op.left.kind === "array" || op.right.kind === "object" || op.right.kind === "array" : false;
4315
+ const preview = op.kind === "changed" ? [
4316
+ {
4317
+ text: previewNode(op.left, 400),
4318
+ tone: "text-rose-700 dark:text-rose-300 line-through"
4319
+ },
4320
+ { text: previewNode(op.right, 400), tone: "text-emerald-700 dark:text-emerald-300" }
4321
+ ] : op.kind === "removed" ? [
4322
+ {
4323
+ text: previewNode(op.value, 400),
4324
+ tone: "text-rose-700 dark:text-rose-300 line-through"
4325
+ }
4326
+ ] : op.kind === "added" ? [{ text: previewNode(op.value, 400), tone: "text-emerald-700 dark:text-emerald-300" }] : [{ text: previewNode(op.value, 400), tone: "text-muted-foreground" }];
4327
+ const justCopied = copiedPath === op.path && op.path !== "";
4328
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs(
4329
+ "div",
4330
+ {
4331
+ "data-diff-idx": idx,
4332
+ "data-diff-kind": op.kind,
4333
+ className: cn("border-l-4 rounded-sm px-3 py-2 my-0.5 transition-colors", v.border, v.bg),
4334
+ children: [
4335
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
4336
+ "button",
4337
+ {
4338
+ type: "button",
4339
+ onClick: onToggle,
4340
+ disabled: !isExpandable2,
4341
+ className: cn(
4342
+ "w-full flex items-center gap-2 text-xs text-left rounded-sm",
4343
+ isExpandable2 ? "cursor-pointer" : "cursor-default"
4344
+ ),
4345
+ "aria-expanded": isExpandable2 ? expanded : void 0,
4346
+ "aria-label": isExpandable2 ? expanded ? `Collapse ${op.path || "root"}` : `Expand ${op.path || "root"}` : void 0,
4347
+ children: [
4348
+ isExpandable2 ? /* @__PURE__ */ jsxRuntimeExports.jsx(
4349
+ ChevronRight,
4350
+ {
4351
+ className: cn(
4352
+ "size-3 shrink-0 transition-transform",
4353
+ v.accent,
4354
+ expanded && "rotate-90"
4355
+ )
4356
+ }
4357
+ ) : /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "size-3 shrink-0", "aria-hidden": "true" }),
4358
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Icon2, { className: cn("size-3.5 shrink-0", v.accent), strokeWidth: 2.5 }),
4359
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-mono truncate flex-1 min-w-0", title: op.path || "(root)", children: op.path === "" ? "(root)" : op.path }),
4360
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
4361
+ "span",
4362
+ {
4363
+ className: cn(
4364
+ "text-[9px] font-bold uppercase tracking-wider shrink-0 px-1.5 py-0.5 rounded",
4365
+ v.accent,
4366
+ op.kind === "equal" ? "bg-muted/40" : "bg-background/60"
4367
+ ),
4368
+ children: v.label
4369
+ }
4370
+ ),
4371
+ op.path !== "" && /* @__PURE__ */ jsxRuntimeExports.jsx(
4372
+ "span",
4373
+ {
4374
+ role: "button",
4375
+ tabIndex: 0,
4376
+ onClick: (e) => {
4377
+ e.stopPropagation();
4378
+ onCopyPath(op.path);
4379
+ },
4380
+ onKeyDown: (e) => {
4381
+ if (e.key === "Enter" || e.key === " ") {
4382
+ e.stopPropagation();
4383
+ e.preventDefault();
4384
+ onCopyPath(op.path);
4385
+ }
4386
+ },
4387
+ className: cn(
4388
+ "shrink-0 p-1 rounded transition-colors cursor-pointer inline-flex items-center justify-center",
4389
+ justCopied ? "text-emerald-500" : "text-muted-foreground/50 hover:text-foreground hover:bg-muted"
4390
+ ),
4391
+ "aria-label": justCopied ? "Copied" : "Copy path",
4392
+ title: justCopied ? "Copied!" : "Copy path",
4393
+ children: justCopied ? /* @__PURE__ */ jsxRuntimeExports.jsx(Check, { className: "size-3" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Copy, { className: "size-3" })
4394
+ }
4395
+ )
4396
+ ]
4397
+ }
4398
+ ),
4399
+ preview.map((p, i) => (
4400
+ // biome-ignore lint/suspicious/noArrayIndexKey: preview list is rebuilt on every render and is positional
4401
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: cn("font-mono text-xs mt-1 break-all pl-5", p.tone), children: p.text }, i)
4402
+ )),
4403
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
4404
+ "div",
4405
+ {
4406
+ className: "overflow-hidden transition-all duration-200",
4407
+ style: { maxHeight: expanded && isExpandable2 ? "2000px" : "0" },
4408
+ "aria-hidden": !expanded,
4409
+ children: expanded && isExpandable2 && op.kind !== "equal" ? /* @__PURE__ */ jsxRuntimeExports.jsx(ExpandedSubtree, { op }) : null
4410
+ }
4411
+ )
4412
+ ]
4413
+ }
4414
+ );
4415
+ }
4416
+ function ExpandedSubtree({ op }) {
4417
+ if (op.kind === "added" || op.kind === "removed") {
4418
+ return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "pl-5 mt-2 border border-border/50 rounded p-2 bg-muted/20", children: /* @__PURE__ */ jsxRuntimeExports.jsx(JsonViewerFromString, { text: nodeToJsonString(op.value), defaultExpandDepth: 2 }) });
4419
+ }
4420
+ const leftIsStructured = op.left.kind === "object" || op.left.kind === "array";
4421
+ const rightIsStructured = op.right.kind === "object" || op.right.kind === "array";
4422
+ if (!leftIsStructured && !rightIsStructured) {
4423
+ return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "pl-5 mt-2 text-xs text-muted-foreground/70 italic", children: "Primitive values are shown inline above." });
4424
+ }
4425
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "pl-5 mt-2 grid grid-cols-1 md:grid-cols-2 gap-2", children: [
4426
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "border border-rose-500/30 rounded p-2 bg-rose-500/5", children: [
4427
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-[10px] uppercase tracking-wider text-rose-500 mb-1", children: "Old" }),
4428
+ /* @__PURE__ */ jsxRuntimeExports.jsx(JsonViewerFromString, { text: nodeToJsonString(op.left), defaultExpandDepth: 2 })
4429
+ ] }),
4430
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "border border-emerald-500/30 rounded p-2 bg-emerald-500/5", children: [
4431
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-[10px] uppercase tracking-wider text-emerald-500 mb-1", children: "New" }),
4432
+ /* @__PURE__ */ jsxRuntimeExports.jsx(JsonViewerFromString, { text: nodeToJsonString(op.right), defaultExpandDepth: 2 })
4433
+ ] })
4434
+ ] });
4435
+ }
4436
+ function SummaryChips({
4437
+ counts,
4438
+ onJumpTo
4439
+ }) {
4440
+ const total = counts.added + counts.removed + counts.changed;
4441
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "px-4 py-2 border-b border-border bg-muted/20 flex items-center gap-2 text-xs flex-wrap", children: [
4442
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-muted-foreground font-medium", children: [
4443
+ total,
4444
+ " ",
4445
+ total === 1 ? "change" : "changes"
4446
+ ] }),
4447
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
4448
+ "button",
4449
+ {
4450
+ type: "button",
4451
+ onClick: () => onJumpTo("removed"),
4452
+ disabled: counts.removed === 0,
4453
+ className: cn(
4454
+ "inline-flex items-center gap-1 px-2 py-0.5 rounded-full border cursor-pointer transition-colors",
4455
+ counts.removed > 0 ? "border-rose-500/40 text-rose-600 dark:text-rose-400 bg-rose-500/10 hover:bg-rose-500/20" : "border-border text-muted-foreground/40 cursor-not-allowed"
4456
+ ),
4457
+ title: counts.removed > 0 ? "Jump to first removed" : "No removals",
4458
+ children: [
4459
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Minus, { className: "size-3" }),
4460
+ counts.removed,
4461
+ " removed"
4462
+ ]
4463
+ }
4464
+ ),
4465
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
4466
+ "button",
4467
+ {
4468
+ type: "button",
4469
+ onClick: () => onJumpTo("added"),
4470
+ disabled: counts.added === 0,
4471
+ className: cn(
4472
+ "inline-flex items-center gap-1 px-2 py-0.5 rounded-full border cursor-pointer transition-colors",
4473
+ counts.added > 0 ? "border-emerald-500/40 text-emerald-600 dark:text-emerald-400 bg-emerald-500/10 hover:bg-emerald-500/20" : "border-border text-muted-foreground/40 cursor-not-allowed"
4474
+ ),
4475
+ title: counts.added > 0 ? "Jump to first added" : "No additions",
4476
+ children: [
4477
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Plus, { className: "size-3" }),
4478
+ counts.added,
4479
+ " added"
4480
+ ]
4481
+ }
4482
+ ),
4483
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
4484
+ "button",
4485
+ {
4486
+ type: "button",
4487
+ onClick: () => onJumpTo("changed"),
4488
+ disabled: counts.changed === 0,
4489
+ className: cn(
4490
+ "inline-flex items-center gap-1 px-2 py-0.5 rounded-full border cursor-pointer transition-colors",
4491
+ counts.changed > 0 ? "border-amber-500/40 text-amber-600 dark:text-amber-400 bg-amber-500/10 hover:bg-amber-500/20" : "border-border text-muted-foreground/40 cursor-not-allowed"
4492
+ ),
4493
+ title: counts.changed > 0 ? "Jump to first changed" : "No changes",
4494
+ children: [
4495
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Pencil, { className: "size-3" }),
4496
+ counts.changed,
4497
+ " changed"
4498
+ ]
4499
+ }
4500
+ )
4501
+ ] });
4502
+ }
4503
+ function ModeToggle({
4504
+ mode,
4505
+ onChange
4506
+ }) {
4507
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "inline-flex rounded-md border border-border overflow-hidden", children: [
4508
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
4509
+ "button",
4510
+ {
4511
+ type: "button",
4512
+ onClick: () => onChange("unified"),
4513
+ "aria-pressed": mode === "unified",
4514
+ className: cn(
4515
+ "flex items-center gap-1 px-2 py-1 text-xs transition-colors cursor-pointer",
4516
+ mode === "unified" ? "bg-muted text-foreground" : "hover:bg-muted/50 text-muted-foreground"
4517
+ ),
4518
+ title: "Unified view (single column, emphasized diffs)",
4519
+ children: [
4520
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Rows3, { className: "size-3" }),
4521
+ "Unified"
4522
+ ]
4523
+ }
4524
+ ),
4525
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
4526
+ "button",
4527
+ {
4528
+ type: "button",
4529
+ onClick: () => onChange("split"),
4530
+ "aria-pressed": mode === "split",
4531
+ className: cn(
4532
+ "flex items-center gap-1 px-2 py-1 text-xs transition-colors border-l border-border cursor-pointer",
4533
+ mode === "split" ? "bg-muted text-foreground" : "hover:bg-muted/50 text-muted-foreground"
4534
+ ),
4535
+ title: "Split view (path | left | right)",
4536
+ children: [
4537
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Columns2, { className: "size-3" }),
4538
+ "Split"
4539
+ ]
4540
+ }
4541
+ )
4542
+ ] });
4543
+ }
4544
+ function SideSummary({ log, side }) {
4545
+ const conversationId = getConversationId(log);
4546
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 min-w-0 space-y-1 text-xs", children: [
4547
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [
4548
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
4549
+ Badge,
4550
+ {
4551
+ variant: "outline",
4552
+ className: cn(
4553
+ "text-[10px] px-1.5 py-0 h-5 font-mono shrink-0",
4554
+ side === "left" ? "border-rose-500/40 text-rose-400" : "border-emerald-500/40 text-emerald-400"
4555
+ ),
4556
+ children: side === "left" ? "← Left" : "Right →"
4557
+ }
4558
+ ),
4559
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-mono text-blue-400/80", children: [
4560
+ "#",
4561
+ log.id
4562
+ ] }),
4563
+ log.model !== null && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-mono text-muted-foreground truncate", children: log.model })
4564
+ ] }),
4565
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3 text-muted-foreground font-mono", children: [
4566
+ log.cacheCreationInputTokens !== null && log.cacheCreationInputTokens > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-emerald-400", children: [
4567
+ "Cache +",
4568
+ formatTokens(log.cacheCreationInputTokens)
4569
+ ] }),
4570
+ log.cacheReadInputTokens !== null && log.cacheReadInputTokens > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-purple-400", children: [
4571
+ "Cache ~",
4572
+ formatTokens(log.cacheReadInputTokens)
4573
+ ] }),
4574
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "truncate", title: log.timestamp, children: log.timestamp })
4575
+ ] }),
4576
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-muted-foreground/70 font-mono truncate", title: conversationId, children: [
4577
+ "session: ",
4578
+ conversationId
4579
+ ] })
4580
+ ] });
4581
+ }
4582
+ function CompareDrawer({ left, right, onClose }) {
4583
+ const ops = reactExports.useMemo(() => {
4584
+ const l = normalizeRequest(parseRequest(left.rawRequestBody) ?? left.rawRequestBody);
4585
+ const r = normalizeRequest(parseRequest(right.rawRequestBody) ?? right.rawRequestBody);
4586
+ return diffTrees(l, r);
4587
+ }, [left.rawRequestBody, right.rawRequestBody]);
4588
+ const grouped = reactExports.useMemo(() => groupContiguousEquals(ops), [ops]);
4589
+ const counts = reactExports.useMemo(() => {
4590
+ let added = 0;
4591
+ let removed = 0;
4592
+ let changed = 0;
4593
+ for (const g of grouped) {
4594
+ if (g.kind !== "single") continue;
4595
+ switch (g.op.kind) {
4596
+ case "added":
4597
+ added++;
4598
+ break;
4599
+ case "removed":
4600
+ removed++;
4601
+ break;
4602
+ case "changed":
4603
+ changed++;
4604
+ break;
4605
+ }
4606
+ }
4607
+ return { added, removed, changed };
4608
+ }, [grouped]);
4609
+ const [expandedRuns, setExpandedRuns] = reactExports.useState(/* @__PURE__ */ new Set());
4610
+ const toggleRun = (idx) => {
4611
+ setExpandedRuns((prev) => {
4612
+ const next = new Set(prev);
4613
+ if (next.has(idx)) next.delete(idx);
4614
+ else next.add(idx);
4615
+ return next;
4616
+ });
4617
+ };
4618
+ const [expandedRows, setExpandedRows] = reactExports.useState(/* @__PURE__ */ new Set());
4619
+ const toggleRow = (idx) => {
4620
+ setExpandedRows((prev) => {
4621
+ const next = new Set(prev);
4622
+ if (next.has(idx)) next.delete(idx);
4623
+ else next.add(idx);
4624
+ return next;
4625
+ });
4626
+ };
4627
+ reactExports.useEffect(() => {
4628
+ setExpandedRows(/* @__PURE__ */ new Set());
4629
+ }, [left.id, right.id]);
4630
+ const [mode, setMode] = reactExports.useState("unified");
4631
+ const bodyRef = reactExports.useRef(null);
4632
+ const [copiedPath, setCopiedPath] = reactExports.useState(null);
4633
+ const copyResetTimer = reactExports.useRef(null);
4634
+ const onCopyPath = (path) => {
4635
+ void window.navigator.clipboard.writeText(path).then(() => {
4636
+ setCopiedPath(path);
4637
+ if (copyResetTimer.current !== null) clearTimeout(copyResetTimer.current);
4638
+ copyResetTimer.current = setTimeout(() => setCopiedPath(null), 1500);
4639
+ });
4640
+ };
4641
+ reactExports.useEffect(() => {
4642
+ return () => {
4643
+ if (copyResetTimer.current !== null) clearTimeout(copyResetTimer.current);
4644
+ };
4645
+ }, []);
4646
+ const jumpToKind = (kind) => {
4647
+ const idx = grouped.findIndex((g) => g.kind === "single" && g.op.kind === kind);
4648
+ if (idx === -1) return;
4649
+ const root = bodyRef.current;
4650
+ if (root === null) return;
4651
+ const el = root.querySelector(`[data-diff-idx="${idx}"]`);
4652
+ if (el !== null) {
4653
+ el.scrollIntoView({ behavior: "smooth", block: "center" });
4654
+ }
4655
+ };
4656
+ reactExports.useEffect(() => {
4657
+ const onKey = (e) => {
4658
+ if (e.key === "Escape") onClose();
4659
+ };
4660
+ document.addEventListener("keydown", onKey);
4661
+ const prevOverflow = document.body.style.overflow;
4662
+ document.body.style.overflow = "hidden";
4663
+ return () => {
4664
+ document.removeEventListener("keydown", onKey);
4665
+ document.body.style.overflow = prevOverflow;
4666
+ };
4667
+ }, [onClose]);
4668
+ const sameSession = getConversationId(left) === getConversationId(right);
4669
+ const allEqual = ops.length === 1 && ops[0]?.kind === "equal";
4670
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs(
4671
+ "div",
4672
+ {
4673
+ className: "fixed inset-0 z-50 flex justify-end",
4674
+ role: "dialog",
4675
+ "aria-modal": "true",
4676
+ "aria-label": "Compare two log requests",
4677
+ children: [
4678
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
4679
+ "button",
4680
+ {
4681
+ type: "button",
4682
+ onClick: onClose,
4683
+ "aria-label": "Close compare drawer",
4684
+ className: "absolute inset-0 bg-black/40 cursor-default",
4685
+ tabIndex: -1
4686
+ }
4687
+ ),
4688
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
4689
+ "div",
4690
+ {
4691
+ className: cn(
4692
+ "relative bg-background border-l border-border shadow-xl",
4693
+ "w-full md:w-[70vw] max-w-[1100px] flex flex-col h-full"
4694
+ ),
4695
+ onClick: (e) => e.stopPropagation(),
4696
+ onKeyDown: (e) => e.stopPropagation(),
4697
+ children: [
4698
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-start gap-4 px-4 py-3 border-b border-border", children: [
4699
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 flex gap-4 min-w-0", children: [
4700
+ /* @__PURE__ */ jsxRuntimeExports.jsx(SideSummary, { log: left, side: "left" }),
4701
+ /* @__PURE__ */ jsxRuntimeExports.jsx(SideSummary, { log: right, side: "right" })
4702
+ ] }),
4703
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 shrink-0", children: [
4704
+ /* @__PURE__ */ jsxRuntimeExports.jsx(ModeToggle, { mode, onChange: setMode }),
4705
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
4706
+ "button",
4707
+ {
4708
+ type: "button",
4709
+ onClick: onClose,
4710
+ "aria-label": "Close",
4711
+ className: "p-1 rounded text-muted-foreground hover:text-foreground hover:bg-muted cursor-pointer",
4712
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx(X, { className: "size-4" })
4713
+ }
4714
+ )
4715
+ ] })
4716
+ ] }),
4717
+ !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." }),
4718
+ allEqual ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 min-h-0 overflow-y-auto flex items-center justify-center text-muted-foreground text-sm", children: "The two Request payloads are identical." }) : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
4719
+ /* @__PURE__ */ jsxRuntimeExports.jsx(SummaryChips, { counts, onJumpTo: jumpToKind }),
4720
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { ref: bodyRef, className: "flex-1 min-h-0 overflow-y-auto", children: mode === "unified" ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "px-3 py-2 space-y-0.5", children: grouped.map((g, i) => {
4721
+ if (g.kind === "equal-run") {
4722
+ return /* @__PURE__ */ jsxRuntimeExports.jsx(
4723
+ EqualRunRow,
4724
+ {
4725
+ ops: g.ops,
4726
+ expanded: expandedRuns.has(i),
4727
+ onToggle: () => toggleRun(i)
4728
+ },
4729
+ `r${i}`
4730
+ );
4731
+ }
4732
+ const op = g.op;
4733
+ return /* @__PURE__ */ jsxRuntimeExports.jsx(
4734
+ UnifiedOpRow,
4735
+ {
4736
+ op,
4737
+ idx: i,
4738
+ copiedPath,
4739
+ onCopyPath,
4740
+ expanded: expandedRows.has(i),
4741
+ onToggle: () => toggleRow(i)
4742
+ },
4743
+ `o${i}`
4744
+ );
4745
+ }) }) : /* @__PURE__ */ jsxRuntimeExports.jsx(SplitBody, { grouped, left, right }) })
4746
+ ] })
4747
+ ]
4748
+ }
4749
+ )
4750
+ ]
4751
+ }
4752
+ );
4753
+ }
4754
+ function SplitBody({
4755
+ grouped,
4756
+ left,
4757
+ right
4758
+ }) {
4759
+ return /* @__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: [
4760
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-[200px_1fr_1fr] gap-x-2 col-span-3 pb-2 mb-2 border-b border-border text-[10px] uppercase tracking-wider text-muted-foreground", children: [
4761
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Path" }),
4762
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [
4763
+ "Left (Log #",
4764
+ left.id,
4765
+ ")"
4766
+ ] }),
4767
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [
4768
+ "Right (Log #",
4769
+ right.id,
4770
+ ")"
4771
+ ] })
4772
+ ] }),
4773
+ grouped.map((g, i) => {
4774
+ if (g.kind === "equal-run") {
4775
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs(
4776
+ "div",
4777
+ {
4778
+ className: "col-span-3 px-2 py-1 text-xs text-muted-foreground/60",
4779
+ children: [
4780
+ g.ops.length,
4781
+ " equal siblings collapsed — switch to Unified to expand"
4782
+ ]
4783
+ },
4784
+ i
4785
+ );
4786
+ }
4787
+ const op = g.op;
4788
+ if (op.kind === "equal") {
4789
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs(
4790
+ "div",
4791
+ {
4792
+ className: "col-span-3 grid grid-cols-[200px_1fr_1fr] gap-x-2 px-2 py-0.5 text-muted-foreground",
4793
+ children: [
4794
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-mono text-xs truncate", title: op.path, children: op.path }),
4795
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-mono text-xs break-all opacity-60", children: previewNode(op.value, 200) }),
4796
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-mono text-xs break-all opacity-60", children: previewNode(op.value, 200) })
4797
+ ]
4798
+ },
4799
+ i
4800
+ );
4801
+ }
4802
+ if (op.kind === "added") {
4803
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs(
4804
+ "div",
4805
+ {
4806
+ className: "col-span-3 px-2 py-1 rounded text-xs border-l-2 border-l-emerald-400/70 bg-emerald-500/5",
4807
+ children: [
4808
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-mono text-xs text-muted-foreground mb-0.5", children: op.path }),
4809
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "font-mono break-all text-emerald-300/90", children: [
4810
+ "+ ",
4811
+ previewNode(op.value, 400)
4812
+ ] })
4813
+ ]
4814
+ },
4815
+ i
4816
+ );
4817
+ }
4818
+ if (op.kind === "removed") {
4819
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs(
4820
+ "div",
4821
+ {
4822
+ className: "col-span-3 px-2 py-1 rounded text-xs border-l-2 border-l-rose-400/70 bg-rose-500/5",
4823
+ children: [
4824
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-mono text-xs text-muted-foreground mb-0.5", children: op.path }),
4825
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "font-mono break-all text-rose-300/90 line-through", children: [
4826
+ "− ",
4827
+ previewNode(op.value, 400)
4828
+ ] })
4829
+ ]
4830
+ },
4831
+ i
4832
+ );
4833
+ }
4834
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs(
4835
+ "div",
4836
+ {
4837
+ className: "col-span-3 px-2 py-1 rounded text-xs border-l-2 border-l-amber-400/70 bg-amber-500/5",
4838
+ children: [
4839
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-mono text-xs text-muted-foreground mb-1", children: op.path }),
4840
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-2 gap-2", children: [
4841
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-mono text-rose-300/90 break-all line-through", children: previewNode(op.left, 400) }),
4842
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-mono text-emerald-300/90 break-all", children: previewNode(op.right, 400) })
4843
+ ] })
4844
+ ]
4845
+ },
4846
+ i
4847
+ );
4848
+ })
4849
+ ] });
4850
+ }
3923
4851
  function truncateSessionId(id) {
3924
4852
  if (id.length <= 30) return id;
3925
4853
  return id.slice(0, 12) + "…" + id.slice(-12);
@@ -4000,6 +4928,9 @@ function ProxyViewer({
4000
4928
  const { totalIn, totalOut } = computeTokenSummary(logs);
4001
4929
  const [groupedView, setGroupedView] = reactExports.useState(true);
4002
4930
  const [exporting, setExporting] = reactExports.useState(false);
4931
+ const [selectedLogIds, setSelectedLogIds] = reactExports.useState([]);
4932
+ const [compareOpen, setCompareOpen] = reactExports.useState(false);
4933
+ const [comparePair, setComparePair] = reactExports.useState(null);
4003
4934
  const handleExport = reactExports.useCallback(async () => {
4004
4935
  setExporting(true);
4005
4936
  try {
@@ -4009,6 +4940,73 @@ function ProxyViewer({
4009
4940
  }
4010
4941
  }, [logs]);
4011
4942
  const parentRef = reactExports.useRef(null);
4943
+ const handleToggleSelect = reactExports.useCallback((logId) => {
4944
+ setSelectedLogIds((prev) => {
4945
+ if (prev.includes(logId)) {
4946
+ return prev.filter((id) => id !== logId);
4947
+ }
4948
+ if (prev.length < 2) {
4949
+ return [...prev, logId];
4950
+ }
4951
+ const newer = prev[1];
4952
+ if (newer === void 0) return prev;
4953
+ return [newer, logId];
4954
+ });
4955
+ }, []);
4956
+ reactExports.useEffect(() => {
4957
+ setSelectedLogIds([]);
4958
+ setCompareOpen(false);
4959
+ }, [selectedSession, selectedModel]);
4960
+ const selectedSet = reactExports.useMemo(() => new Set(selectedLogIds), [selectedLogIds]);
4961
+ const openCompare = reactExports.useCallback(() => {
4962
+ if (selectedLogIds.length !== 2) return;
4963
+ const [idA, idB] = selectedLogIds;
4964
+ if (idA === void 0 || idB === void 0) return;
4965
+ const logA = logs.find((l) => l.id === idA);
4966
+ const logB = logs.find((l) => l.id === idB);
4967
+ if (logA === void 0 || logB === void 0) return;
4968
+ setComparePair([logA, logB]);
4969
+ setCompareOpen(true);
4970
+ }, [selectedLogIds, logs]);
4971
+ const closeCompare = reactExports.useCallback(() => {
4972
+ setCompareOpen(false);
4973
+ }, []);
4974
+ const clearSelection = reactExports.useCallback(() => {
4975
+ setSelectedLogIds([]);
4976
+ }, []);
4977
+ const selectedSummary = reactExports.useMemo(() => {
4978
+ if (selectedLogIds.length !== 2) return null;
4979
+ const [idA, idB] = selectedLogIds;
4980
+ if (idA === void 0 || idB === void 0) return null;
4981
+ const logA = logs.find((l) => l.id === idA);
4982
+ const logB = logs.find((l) => l.id === idB);
4983
+ if (logA === void 0 || logB === void 0) return null;
4984
+ const sameSession = getConversationId(logA) === getConversationId(logB);
4985
+ let elapsed = "";
4986
+ if (logA.timestamp !== null && logB.timestamp !== null) {
4987
+ const a = Date.parse(logA.timestamp);
4988
+ const b = Date.parse(logB.timestamp);
4989
+ if (!Number.isNaN(a) && !Number.isNaN(b)) {
4990
+ const ms = Math.abs(b - a);
4991
+ elapsed = formatElapsed2(ms);
4992
+ }
4993
+ }
4994
+ return {
4995
+ logA,
4996
+ logB,
4997
+ sameSession,
4998
+ elapsed
4999
+ };
5000
+ }, [selectedLogIds, logs]);
5001
+ function formatElapsed2(ms) {
5002
+ if (ms < 1e3) return `${ms}ms`;
5003
+ const sec = Math.floor(ms / 1e3);
5004
+ if (sec < 60) return `${sec}s`;
5005
+ const min = Math.floor(sec / 60);
5006
+ if (min < 60) return `${min}m`;
5007
+ const hr = Math.floor(min / 60);
5008
+ return `${hr}h${min % 60}m`;
5009
+ }
4012
5010
  const groups = reactExports.useMemo(() => groupLogsByConversation(logs), [logs]);
4013
5011
  const cacheTrends = reactExports.useMemo(() => computeCacheTrends(groups), [groups]);
4014
5012
  const renderGroups = logs.length > 0 && groupedView && !(groups.length === 1 && groups[0]?.logs.length === logs.length);
@@ -4157,7 +5155,9 @@ function ProxyViewer({
4157
5155
  group,
4158
5156
  viewMode,
4159
5157
  strip,
4160
- cacheTrends
5158
+ cacheTrends,
5159
+ selectedSet,
5160
+ onToggleSelect: handleToggleSelect
4161
5161
  }
4162
5162
  )
4163
5163
  },
@@ -4184,7 +5184,9 @@ function ProxyViewer({
4184
5184
  log,
4185
5185
  viewMode,
4186
5186
  strip,
4187
- cacheTrend: cacheTrends.get(log.id) ?? null
5187
+ cacheTrend: cacheTrends.get(log.id) ?? null,
5188
+ isSelected: selectedSet.has(log.id),
5189
+ onToggleSelect: handleToggleSelect
4188
5190
  }
4189
5191
  )
4190
5192
  },
@@ -4193,7 +5195,41 @@ function ProxyViewer({
4193
5195
  }
4194
5196
  })
4195
5197
  }
4196
- ) }) })
5198
+ ) }) }),
5199
+ 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: [
5200
+ /* @__PURE__ */ jsxRuntimeExports.jsx(GitCompareArrows, { className: "size-4 text-amber-400 shrink-0" }),
5201
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-muted-foreground font-mono", children: [
5202
+ "#",
5203
+ selectedSummary.logA.id,
5204
+ " ↔ #",
5205
+ selectedSummary.logB.id,
5206
+ " · ",
5207
+ selectedSummary.sameSession ? "same session" : "different sessions",
5208
+ selectedSummary.elapsed !== "" && ` · ${selectedSummary.elapsed} apart`
5209
+ ] }),
5210
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
5211
+ "button",
5212
+ {
5213
+ type: "button",
5214
+ onClick: clearSelection,
5215
+ className: "text-muted-foreground hover:text-foreground transition-colors cursor-pointer inline-flex items-center gap-1",
5216
+ children: [
5217
+ /* @__PURE__ */ jsxRuntimeExports.jsx(X, { className: "size-3" }),
5218
+ "Clear"
5219
+ ]
5220
+ }
5221
+ ),
5222
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
5223
+ "button",
5224
+ {
5225
+ type: "button",
5226
+ onClick: openCompare,
5227
+ className: "bg-amber-400 text-amber-950 hover:bg-amber-300 transition-colors px-3 py-1 rounded font-medium cursor-pointer",
5228
+ children: "Compare 2 logs"
5229
+ }
5230
+ )
5231
+ ] }),
5232
+ compareOpen && comparePair !== null && /* @__PURE__ */ jsxRuntimeExports.jsx(CompareDrawer, { left: comparePair[0], right: comparePair[1], onClose: closeCompare })
4197
5233
  ] });
4198
5234
  }
4199
5235
  object({