@tonyclaw/llm-inspector 1.13.0 → 1.14.1

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 (38) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/index-DEUddp_2.css +1 -0
  3. package/.output/public/assets/index-ax85pt2A.js +105 -0
  4. package/.output/public/assets/{main-C3tLo75s.js → main-BzK2SzIB.js} +3 -3
  5. package/.output/server/_libs/cfworker__json-schema.mjs +1 -0
  6. package/.output/server/_libs/lucide-react.mjs +60 -54
  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-C8VC13EA.mjs → index-Cso39vJc.mjs} +715 -178
  10. package/.output/server/_ssr/index.mjs +2 -2
  11. package/.output/server/_ssr/{router-D5ccnemB.mjs → router-B8X3GXM2.mjs} +669 -39
  12. package/.output/server/_tanstack-start-manifest_v-vO4aM6jK.mjs +4 -0
  13. package/.output/server/index.mjs +29 -29
  14. package/README.md +98 -0
  15. package/package.json +3 -1
  16. package/src/components/providers/ProviderCard.tsx +28 -5
  17. package/src/components/providers/ProviderForm.tsx +172 -66
  18. package/src/components/providers/ProvidersPanel.tsx +73 -47
  19. package/src/components/proxy-viewer/CompareDrawer.tsx +583 -91
  20. package/src/components/proxy-viewer/formats/openai/ResponseView.tsx +74 -4
  21. package/src/lib/mask.ts +4 -0
  22. package/src/lib/providerContract.ts +1 -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/providers.ts +16 -4
  32. package/src/proxy/schemas.ts +1 -0
  33. package/src/routes/api/mcp.ts +25 -0
  34. package/src/routes/api/providers.$providerId.ts +1 -0
  35. package/src/routes/api/providers.ts +6 -4
  36. package/.output/public/assets/index-B0anmGQr.css +0 -1
  37. package/.output/public/assets/index-H_thmL2_.js +0 -105
  38. package/.output/server/_tanstack-start-manifest_v-DUbXa1lt.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-D5ccnemB.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-B8X3GXM2.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, 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";
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 EyeOff, q as Eye, r as ExternalLink, 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.13.0";
279
+ const version = "1.14.1";
279
280
  const packageJson = {
280
281
  version
281
282
  };
@@ -1888,9 +1889,37 @@ function StructuredResponseViewAnthropic({
1888
1889
  ] })
1889
1890
  ] });
1890
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
+ }
1891
1919
  function OpenAIResponseView({ response }) {
1892
1920
  const choice = response.choices[0];
1893
1921
  const message = choice?.message;
1922
+ const toolCalls = message?.tool_calls ?? [];
1894
1923
  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-3", children: [
1895
1924
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [
1896
1925
  /* @__PURE__ */ jsxRuntimeExports.jsx(Badge, { variant: "secondary", className: "text-[10px] px-1.5 py-0 h-5 font-mono", children: response.model }),
@@ -1927,6 +1956,10 @@ function OpenAIResponseView({ response }) {
1927
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 }) })
1928
1957
  ] });
1929
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
+ )),
1930
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: [
1931
1964
  /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs text-blue-400 font-mono mb-1", children: "function_call" }),
1932
1965
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "font-mono text-xs", children: [
@@ -1938,7 +1971,7 @@ function OpenAIResponseView({ response }) {
1938
1971
  ] })
1939
1972
  ] })
1940
1973
  ] }),
1941
- (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" })
1942
1975
  ] })
1943
1976
  ] });
1944
1977
  }
@@ -2828,13 +2861,13 @@ function SelectScrollDownButton({
2828
2861
  }
2829
2862
  );
2830
2863
  }
2831
- const KNOWN_PROVIDER_DOCS = {
2832
- deepseek: "https://api-docs.deepseek.com/zh-cn/"
2833
- };
2834
2864
  function maskApiKey(apiKey) {
2835
2865
  if (apiKey.length <= 8) return "••••••••";
2836
2866
  return apiKey.slice(0, 4) + "••••••••" + apiKey.slice(-4);
2837
2867
  }
2868
+ const KNOWN_PROVIDER_DOCS = {
2869
+ deepseek: "https://api-docs.deepseek.com/zh-cn/"
2870
+ };
2838
2871
  function hasSuccessField(result) {
2839
2872
  return Object.prototype.hasOwnProperty.call(result, "success");
2840
2873
  }
@@ -2962,6 +2995,13 @@ function ProviderCard({
2962
2995
  onTest
2963
2996
  }) {
2964
2997
  const [showApiKey, setShowApiKey] = reactExports.useState(false);
2998
+ const [copied, setCopied] = reactExports.useState(false);
2999
+ function handleCopy() {
3000
+ navigator.clipboard.writeText(provider.apiKey).catch(() => {
3001
+ });
3002
+ setCopied(true);
3003
+ setTimeout(() => setCopied(false), 2e3);
3004
+ }
2965
3005
  const docsUrl = provider.apiDocsUrl ?? Object.entries(KNOWN_PROVIDER_DOCS).find(
2966
3006
  ([keyword]) => provider.name.toLowerCase().includes(keyword)
2967
3007
  )?.[1];
@@ -2971,7 +3011,11 @@ function ProviderCard({
2971
3011
  className: `border rounded-lg p-4 flex flex-col gap-3 bg-card transition-all duration-500 ${isHighlighted === true ? "ring-2 ring-primary shadow-md" : ""}`,
2972
3012
  children: [
2973
3013
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-start justify-between gap-2", children: [
2974
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center gap-2 min-w-0", children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-medium truncate", children: provider.model !== void 0 && provider.model !== "" ? `${provider.model} (${provider.name})` : provider.name }) }),
3014
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 min-w-0", children: [
3015
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-medium truncate", children: provider.model !== void 0 && provider.model !== "" ? `${provider.model} (${provider.name})` : provider.name }),
3016
+ provider.source === "company" && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs px-1.5 py-0.5 rounded bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400 shrink-0", children: "公司" }),
3017
+ provider.source === "personal" && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs px-1.5 py-0.5 rounded bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400 shrink-0", children: "个人" })
3018
+ ] }),
2975
3019
  docsUrl !== void 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs(
2976
3020
  "a",
2977
3021
  {
@@ -2998,6 +3042,16 @@ function ProviderCard({
2998
3042
  "aria-label": showApiKey ? "Hide API key" : "Show API key",
2999
3043
  children: showApiKey ? /* @__PURE__ */ jsxRuntimeExports.jsx(EyeOff, { className: "size-4" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Eye, { className: "size-4" })
3000
3044
  }
3045
+ ),
3046
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
3047
+ "button",
3048
+ {
3049
+ type: "button",
3050
+ onClick: handleCopy,
3051
+ className: "text-muted-foreground hover:text-foreground transition-colors p-1",
3052
+ "aria-label": "Copy API key",
3053
+ children: copied ? /* @__PURE__ */ jsxRuntimeExports.jsx(Check, { className: "size-4 text-green-500" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Copy, { className: "size-4" })
3054
+ }
3001
3055
  )
3002
3056
  ] }),
3003
3057
  provider.anthropicBaseUrl !== void 0 && provider.anthropicBaseUrl !== "" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between gap-2", children: [
@@ -3091,33 +3145,47 @@ const ALIBABA_MODELS = ["glm-5", "glm-5.1", "qwen3.6-plus", "qwen3.7-max"];
3091
3145
  function ProviderForm({ provider, onSubmit, onCancel }) {
3092
3146
  const [name, setName] = reactExports.useState(provider?.name ?? "Provider Name");
3093
3147
  const [apiKey, setApiKey] = reactExports.useState(provider?.apiKey ?? "");
3148
+ const [showApiKey, setShowApiKey] = reactExports.useState(false);
3149
+ const [copied, setCopied] = reactExports.useState(false);
3094
3150
  const [model, setModel] = reactExports.useState(provider?.model ?? "");
3095
- const [format, setFormat] = reactExports.useState(provider?.format ?? "anthropic");
3096
- const [baseUrl, setBaseUrl] = reactExports.useState(provider?.baseUrl ?? "");
3151
+ const [activeTab, setActiveTab] = reactExports.useState("anthropic");
3152
+ const [anthropicBaseUrl, setAnthropicBaseUrl] = reactExports.useState(provider?.anthropicBaseUrl ?? "");
3153
+ const [openaiBaseUrl, setOpenaiBaseUrl] = reactExports.useState(provider?.openaiBaseUrl ?? "");
3097
3154
  const [apiDocsUrl, setApiDocsUrl] = reactExports.useState(provider?.apiDocsUrl ?? "");
3155
+ const [source, setSource] = reactExports.useState(provider?.source);
3098
3156
  const [errors, setErrors] = reactExports.useState({});
3099
3157
  const [isSubmitting, setIsSubmitting] = reactExports.useState(false);
3100
- const [manualBaseUrlOverride, setManualBaseUrlOverride] = reactExports.useState(false);
3158
+ const [manualAnthropicUrlOverride, setManualAnthropicUrlOverride] = reactExports.useState(false);
3159
+ const [manualOpenaiUrlOverride, setManualOpenaiUrlOverride] = reactExports.useState(false);
3101
3160
  const isMiniMax = name.toLowerCase().includes("minimax");
3102
3161
  const isAlibaba = name.toLowerCase().includes("alibaba");
3162
+ function handleCopy() {
3163
+ navigator.clipboard.writeText(apiKey).catch(() => {
3164
+ });
3165
+ setCopied(true);
3166
+ setTimeout(() => setCopied(false), 2e3);
3167
+ }
3103
3168
  reactExports.useEffect(() => {
3104
3169
  if (provider) {
3105
3170
  setName(provider.name);
3106
3171
  setApiKey(provider.apiKey);
3107
3172
  setModel(provider.model ?? "");
3108
- setFormat(provider.format ?? "anthropic");
3109
- setBaseUrl(provider.baseUrl ?? "");
3173
+ setAnthropicBaseUrl(provider.anthropicBaseUrl ?? "");
3174
+ setOpenaiBaseUrl(provider.openaiBaseUrl ?? "");
3110
3175
  setApiDocsUrl(provider.apiDocsUrl ?? "");
3111
- setManualBaseUrlOverride(false);
3176
+ setSource(provider.source);
3177
+ setManualAnthropicUrlOverride(false);
3178
+ setManualOpenaiUrlOverride(false);
3112
3179
  }
3113
3180
  }, [provider]);
3114
3181
  reactExports.useEffect(() => {
3115
3182
  const lowerName = name.toLowerCase();
3116
3183
  for (const [keyword, preset] of Object.entries(KNOWN_PROVIDER_PRESETS)) {
3117
3184
  if (lowerName.includes(keyword)) {
3118
- if (!manualBaseUrlOverride) {
3119
- setFormat(preset.format);
3120
- setBaseUrl(preset.baseUrl);
3185
+ if (preset.format === "anthropic" && !manualAnthropicUrlOverride) {
3186
+ setAnthropicBaseUrl(preset.baseUrl);
3187
+ } else if (preset.format === "openai" && !manualOpenaiUrlOverride) {
3188
+ setOpenaiBaseUrl(preset.baseUrl);
3121
3189
  }
3122
3190
  if (preset.apiDocsUrl !== void 0 && !apiDocsUrl) {
3123
3191
  setApiDocsUrl(preset.apiDocsUrl);
@@ -3143,10 +3211,14 @@ function ProviderForm({ provider, onSubmit, onCancel }) {
3143
3211
  if (!model.trim()) {
3144
3212
  newErrors.model = "Model is required";
3145
3213
  }
3146
- if (!baseUrl.trim()) {
3147
- newErrors.baseUrl = "Base URL is required";
3148
- } else if (!isValidUrl(baseUrl.trim())) {
3149
- newErrors.baseUrl = "Invalid URL format";
3214
+ if (anthropicBaseUrl.trim() && !isValidUrl(anthropicBaseUrl.trim())) {
3215
+ newErrors.anthropicBaseUrl = "Invalid URL format";
3216
+ }
3217
+ if (openaiBaseUrl.trim() && !isValidUrl(openaiBaseUrl.trim())) {
3218
+ newErrors.openaiBaseUrl = "Invalid URL format";
3219
+ }
3220
+ if (!anthropicBaseUrl.trim() && !openaiBaseUrl.trim()) {
3221
+ newErrors.format = "At least one format URL (Anthropic or OpenAI) is required";
3150
3222
  }
3151
3223
  setErrors(newErrors);
3152
3224
  return Object.keys(newErrors).length === 0;
@@ -3168,9 +3240,10 @@ function ProviderForm({ provider, onSubmit, onCancel }) {
3168
3240
  name: name.trim(),
3169
3241
  apiKey: apiKey.trim(),
3170
3242
  model: model.trim() || void 0,
3171
- format,
3172
- baseUrl: baseUrl.trim() || void 0,
3173
- apiDocsUrl: apiDocsUrl.trim() || void 0
3243
+ anthropicBaseUrl: anthropicBaseUrl.trim() || void 0,
3244
+ openaiBaseUrl: openaiBaseUrl.trim() || void 0,
3245
+ apiDocsUrl: apiDocsUrl.trim() || void 0,
3246
+ source
3174
3247
  });
3175
3248
  } finally {
3176
3249
  setIsSubmitting(false);
@@ -3195,22 +3268,68 @@ function ProviderForm({ provider, onSubmit, onCancel }) {
3195
3268
  ),
3196
3269
  errors.name !== void 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-destructive", children: errors.name })
3197
3270
  ] }),
3271
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2", children: [
3272
+ /* @__PURE__ */ jsxRuntimeExports.jsx("label", { htmlFor: "provider-source", className: "text-sm font-medium", children: "Token Source" }),
3273
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
3274
+ "select",
3275
+ {
3276
+ id: "provider-source",
3277
+ value: source ?? "",
3278
+ onChange: (e) => setSource(
3279
+ e.target.value === "company" || e.target.value === "personal" ? e.target.value : void 0
3280
+ ),
3281
+ className: "w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:border-ring focus-visible:outline-ring focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
3282
+ children: [
3283
+ /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "", children: "—" }),
3284
+ /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "personal", children: "个人 (Personal)" }),
3285
+ /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "company", children: "公司 (Company)" })
3286
+ ]
3287
+ }
3288
+ ),
3289
+ /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-muted-foreground", children: "Label whether this API key is company-provided or personal." })
3290
+ ] }),
3198
3291
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2", children: [
3199
3292
  /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { htmlFor: "provider-apikey", className: "text-sm font-medium", children: [
3200
3293
  "API Key ",
3201
3294
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-destructive", children: "*" })
3202
3295
  ] }),
3203
- /* @__PURE__ */ jsxRuntimeExports.jsx(
3204
- "input",
3205
- {
3206
- id: "provider-apikey",
3207
- type: "password",
3208
- value: apiKey,
3209
- onChange: (e) => setApiKey(e.target.value),
3210
- placeholder: "sk-ant-...",
3211
- className: "w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:border-ring focus-visible:outline-ring focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50"
3212
- }
3213
- ),
3296
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [
3297
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
3298
+ "input",
3299
+ {
3300
+ id: "provider-apikey",
3301
+ type: "text",
3302
+ value: showApiKey || apiKey.length === 0 ? apiKey : maskApiKey(apiKey),
3303
+ onChange: (e) => setApiKey(e.target.value),
3304
+ onFocus: () => {
3305
+ if (!showApiKey && apiKey.length > 0) setShowApiKey(true);
3306
+ },
3307
+ placeholder: "sk-ant-...",
3308
+ readOnly: !showApiKey && apiKey.length > 0,
3309
+ className: "flex-1 rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:border-ring focus-visible:outline-ring focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50"
3310
+ }
3311
+ ),
3312
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
3313
+ "button",
3314
+ {
3315
+ type: "button",
3316
+ onClick: () => setShowApiKey((s) => !s),
3317
+ className: "text-muted-foreground hover:text-foreground transition-colors p-1 shrink-0",
3318
+ "aria-label": showApiKey ? "Hide API key" : "Show API key",
3319
+ children: showApiKey ? /* @__PURE__ */ jsxRuntimeExports.jsx(EyeOff, { className: "size-4" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Eye, { className: "size-4" })
3320
+ }
3321
+ ),
3322
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
3323
+ "button",
3324
+ {
3325
+ type: "button",
3326
+ onClick: handleCopy,
3327
+ className: "text-muted-foreground hover:text-foreground transition-colors p-1 shrink-0",
3328
+ "aria-label": "Copy API key",
3329
+ children: copied ? /* @__PURE__ */ jsxRuntimeExports.jsx(Check, { className: "size-4 text-green-500" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Copy, { className: "size-4" })
3330
+ }
3331
+ )
3332
+ ] }),
3214
3333
  errors.apiKey !== void 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-destructive", children: errors.apiKey })
3215
3334
  ] }),
3216
3335
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2", children: [
@@ -3241,46 +3360,65 @@ function ProviderForm({ provider, onSubmit, onCancel }) {
3241
3360
  errors.model !== void 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-destructive", children: errors.model })
3242
3361
  ] }),
3243
3362
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2", children: [
3244
- /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { htmlFor: "provider-format", className: "text-sm font-medium", children: [
3245
- "API Format ",
3246
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-destructive", children: "*" })
3363
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-1 border-b border-border", children: [
3364
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
3365
+ "button",
3366
+ {
3367
+ type: "button",
3368
+ onClick: () => setActiveTab("anthropic"),
3369
+ className: `px-3 py-2 text-sm font-medium border-b-2 transition-colors ${activeTab === "anthropic" ? "border-primary text-primary" : "border-transparent text-muted-foreground hover:text-foreground"}`,
3370
+ children: "Anthropic Format"
3371
+ }
3372
+ ),
3373
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
3374
+ "button",
3375
+ {
3376
+ type: "button",
3377
+ onClick: () => setActiveTab("openai"),
3378
+ className: `px-3 py-2 text-sm font-medium border-b-2 transition-colors ${activeTab === "openai" ? "border-primary text-primary" : "border-transparent text-muted-foreground hover:text-foreground"}`,
3379
+ children: "OpenAI Format"
3380
+ }
3381
+ )
3247
3382
  ] }),
3248
- /* @__PURE__ */ jsxRuntimeExports.jsxs(
3249
- "select",
3383
+ errors.format !== void 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-destructive", children: errors.format })
3384
+ ] }),
3385
+ activeTab === "anthropic" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2", children: [
3386
+ /* @__PURE__ */ jsxRuntimeExports.jsx("label", { htmlFor: "provider-anthropic-base-url", className: "text-sm font-medium", children: "Anthropic Base URL" }),
3387
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
3388
+ "input",
3250
3389
  {
3251
- id: "provider-format",
3252
- value: format,
3253
- onChange: (e) => setFormat(e.target.value === "anthropic" ? "anthropic" : "openai"),
3254
- className: "w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:border-ring focus-visible:outline-ring focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
3255
- children: [
3256
- /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "anthropic", children: "Anthropic (/v1/messages)" }),
3257
- /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "openai", children: "OpenAI (/v1/chat/completions)" })
3258
- ]
3390
+ id: "provider-anthropic-base-url",
3391
+ type: "text",
3392
+ value: anthropicBaseUrl,
3393
+ onChange: (e) => {
3394
+ setManualAnthropicUrlOverride(true);
3395
+ setAnthropicBaseUrl(e.target.value);
3396
+ },
3397
+ placeholder: "https://api.anthropic.com",
3398
+ className: "w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:border-ring focus-visible:outline-ring focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50"
3259
3399
  }
3260
3400
  ),
3261
- /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-muted-foreground", children: format === "anthropic" ? "Use Anthropic format for /v1/messages endpoint" : "Use OpenAI format for /v1/chat/completions endpoint" })
3401
+ errors.anthropicBaseUrl !== void 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-destructive", children: errors.anthropicBaseUrl }),
3402
+ /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-muted-foreground", children: "Anthropic-compatible endpoint URL. Leave empty if this provider does not support Anthropic format." })
3262
3403
  ] }),
3263
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2", children: [
3264
- /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { htmlFor: "provider-base-url", className: "text-sm font-medium", children: [
3265
- "Base URL ",
3266
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-destructive", children: "*" })
3267
- ] }),
3404
+ activeTab === "openai" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2", children: [
3405
+ /* @__PURE__ */ jsxRuntimeExports.jsx("label", { htmlFor: "provider-openai-base-url", className: "text-sm font-medium", children: "OpenAI Base URL" }),
3268
3406
  /* @__PURE__ */ jsxRuntimeExports.jsx(
3269
3407
  "input",
3270
3408
  {
3271
- id: "provider-base-url",
3409
+ id: "provider-openai-base-url",
3272
3410
  type: "text",
3273
- value: baseUrl,
3411
+ value: openaiBaseUrl,
3274
3412
  onChange: (e) => {
3275
- setManualBaseUrlOverride(true);
3276
- setBaseUrl(e.target.value);
3413
+ setManualOpenaiUrlOverride(true);
3414
+ setOpenaiBaseUrl(e.target.value);
3277
3415
  },
3278
- placeholder: format === "anthropic" ? "https://api.anthropic.com" : "https://api.openai.com/v1",
3416
+ placeholder: "https://api.openai.com/v1",
3279
3417
  className: "w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:border-ring focus-visible:outline-ring focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50"
3280
3418
  }
3281
3419
  ),
3282
- errors.baseUrl !== void 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-destructive", children: errors.baseUrl }),
3283
- /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-muted-foreground", children: "Base URL for the provider API." })
3420
+ errors.openaiBaseUrl !== void 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-destructive", children: errors.openaiBaseUrl }),
3421
+ /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-muted-foreground", children: "OpenAI-compatible endpoint URL. Leave empty if this provider does not support OpenAI format." })
3284
3422
  ] }),
3285
3423
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2", children: [
3286
3424
  /* @__PURE__ */ jsxRuntimeExports.jsx("label", { htmlFor: "provider-api-docs-url", className: "text-sm font-medium", children: "API Docs URL" }),
@@ -3297,7 +3435,7 @@ function ProviderForm({ provider, onSubmit, onCancel }) {
3297
3435
  ),
3298
3436
  /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-muted-foreground", children: "Optional API documentation URL. If not set, uses known provider docs." })
3299
3437
  ] }),
3300
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2 justify-end pt-2", children: [
3438
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "sticky bottom-0 bg-card border-t flex gap-2 justify-end pt-2 pb-2", children: [
3301
3439
  /* @__PURE__ */ jsxRuntimeExports.jsx(Button, { type: "button", variant: "outline", onClick: onCancel, disabled: isSubmitting, children: "Cancel" }),
3302
3440
  /* @__PURE__ */ jsxRuntimeExports.jsx(Button, { type: "submit", disabled: isSubmitting, children: isSubmitting ? "Saving..." : provider ? "Update Provider" : "Add Provider" })
3303
3441
  ] })
@@ -3396,9 +3534,10 @@ function createProviderPayload(data) {
3396
3534
  name: data.name,
3397
3535
  apiKey: data.apiKey,
3398
3536
  model: data.model,
3399
- format: data.format,
3400
- anthropicBaseUrl: data.format === "anthropic" ? data.baseUrl : void 0,
3401
- openaiBaseUrl: data.format === "openai" ? data.baseUrl : void 0
3537
+ anthropicBaseUrl: (data.anthropicBaseUrl?.length ?? 0) > 0 ? data.anthropicBaseUrl : void 0,
3538
+ openaiBaseUrl: (data.openaiBaseUrl?.length ?? 0) > 0 ? data.openaiBaseUrl : void 0,
3539
+ apiDocsUrl: (data.apiDocsUrl?.length ?? 0) > 0 ? data.apiDocsUrl : void 0,
3540
+ source: data.source
3402
3541
  };
3403
3542
  }
3404
3543
  function ProvidersPanel({
@@ -3423,9 +3562,11 @@ function ProvidersPanel({
3423
3562
  const [configPath, setConfigPath] = reactExports.useState(null);
3424
3563
  const [configPathCopied, setConfigPathCopied] = reactExports.useState(false);
3425
3564
  const [highlightedProviderId, setHighlightedProviderId] = reactExports.useState(null);
3565
+ const [sourceFilter, setSourceFilter] = reactExports.useState("all");
3426
3566
  const listScrollRef = reactExports.useRef(null);
3427
3567
  const highlightTimeoutRef = reactExports.useRef(null);
3428
3568
  const providers = externalProviders ?? [];
3569
+ const filteredProviders = sourceFilter === "all" ? providers : providers.filter((p) => p.source === sourceFilter);
3429
3570
  const testResults = externalTestResults ?? internalTestResults;
3430
3571
  const testingProviders = externalTestingProviders ?? internalTestingProviders;
3431
3572
  const testingTimeLeft = externalTestingTimeLeft ?? internalTestingTimeLeft;
@@ -3553,51 +3694,50 @@ function ProvidersPanel({
3553
3694
  );
3554
3695
  function handleAddProvider(data) {
3555
3696
  void (async () => {
3556
- let res;
3557
3697
  try {
3558
- res = await fetch("/api/providers", {
3698
+ const res = await fetch("/api/providers", {
3559
3699
  method: "POST",
3560
3700
  headers: { "Content-Type": "application/json" },
3561
3701
  body: JSON.stringify(createProviderPayload(data))
3562
3702
  });
3703
+ if (!res.ok) {
3704
+ setError(await readApiError(res, "Failed to add provider"));
3705
+ return;
3706
+ }
3707
+ const newProvider = await parseJsonResponse(res, ProviderConfigSchema);
3708
+ setShowForm(false);
3709
+ triggerHighlight(newProvider.id);
3710
+ refreshProviders();
3711
+ await runTest(newProvider.id);
3563
3712
  } catch {
3564
3713
  setError(NETWORK_ERROR_MESSAGE);
3565
- return;
3566
3714
  }
3567
- if (!res.ok) {
3568
- setError(await readApiError(res, "Failed to add provider"));
3569
- return;
3570
- }
3571
- const newProvider = await parseJsonResponse(res, ProviderConfigSchema);
3572
- setShowForm(false);
3573
- triggerHighlight(newProvider.id);
3574
- refreshProviders();
3575
- await runTest(newProvider.id);
3576
3715
  })();
3577
3716
  }
3578
3717
  function handleUpdateProvider(data) {
3579
3718
  if (!editingProvider) return;
3580
3719
  void (async () => {
3581
- let res;
3582
3720
  try {
3583
- res = await fetch(`/api/providers/${editingProvider.id}`, {
3721
+ const res = await fetch(`/api/providers/${editingProvider.id}`, {
3584
3722
  method: "PUT",
3585
3723
  headers: { "Content-Type": "application/json" },
3586
3724
  body: JSON.stringify(createProviderPayload(data))
3587
3725
  });
3726
+ if (!res.ok) {
3727
+ setError(await readApiError(res, "Failed to update provider"));
3728
+ return;
3729
+ }
3730
+ const updated = await parseJsonResponse(res, ProviderConfigSchema);
3731
+ setEditingProvider(void 0);
3732
+ triggerHighlight(updated.id);
3733
+ refreshProviders();
3734
+ const criticalFieldsChanged = (data.apiKey ?? "") !== (editingProvider.apiKey ?? "") || (data.model ?? "") !== (editingProvider.model ?? "") || (data.anthropicBaseUrl ?? "") !== (editingProvider.anthropicBaseUrl ?? "") || (data.openaiBaseUrl ?? "") !== (editingProvider.openaiBaseUrl ?? "");
3735
+ if (criticalFieldsChanged) {
3736
+ await runTest(updated.id);
3737
+ }
3588
3738
  } catch {
3589
3739
  setError(NETWORK_ERROR_MESSAGE);
3590
- return;
3591
- }
3592
- if (!res.ok) {
3593
- setError(await readApiError(res, "Failed to update provider"));
3594
- return;
3595
3740
  }
3596
- const updated = await parseJsonResponse(res, ProviderConfigSchema);
3597
- setEditingProvider(void 0);
3598
- triggerHighlight(updated.id);
3599
- refreshProviders();
3600
- await runTest(updated.id);
3601
3741
  })();
3602
3742
  }
3603
3743
  function handleDeleteProvider(providerId) {
@@ -3766,22 +3906,34 @@ function ProvidersPanel({
3766
3906
  /* @__PURE__ */ jsxRuntimeExports.jsx(Plus, { className: "size-4" }),
3767
3907
  "Add Your First Provider"
3768
3908
  ] })
3769
- ] }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { ref: listScrollRef, className: "space-y-3", children: providers.map((provider) => /* @__PURE__ */ jsxRuntimeExports.jsx(
3770
- ProviderCard,
3771
- {
3772
- provider,
3773
- testResults: testResults[provider.id],
3774
- isTesting: testingProviders.has(provider.id),
3775
- testingTimeLeft: testingTimeLeft[provider.id],
3776
- isHighlighted: provider.id === highlightedProviderId,
3777
- onEdit: (p) => setEditingProvider(p),
3778
- onDelete: handleDeleteProvider,
3779
- onTest: (id) => {
3780
- void runTest(id);
3781
- }
3782
- },
3783
- provider.id
3784
- )) })
3909
+ ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-3", children: [
3910
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex gap-1 border-b border-border", children: ["all", "personal", "company"].map((tab) => /* @__PURE__ */ jsxRuntimeExports.jsx(
3911
+ "button",
3912
+ {
3913
+ type: "button",
3914
+ onClick: () => setSourceFilter(tab),
3915
+ className: `px-3 py-2 text-sm font-medium border-b-2 transition-colors ${sourceFilter === tab ? "border-primary text-primary" : "border-transparent text-muted-foreground hover:text-foreground"}`,
3916
+ children: tab === "all" ? "All" : tab === "personal" ? "Personal" : "Company"
3917
+ },
3918
+ tab
3919
+ )) }),
3920
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { ref: listScrollRef, className: "space-y-3", children: filteredProviders.map((provider) => /* @__PURE__ */ jsxRuntimeExports.jsx(
3921
+ ProviderCard,
3922
+ {
3923
+ provider,
3924
+ testResults: testResults[provider.id],
3925
+ isTesting: testingProviders.has(provider.id),
3926
+ testingTimeLeft: testingTimeLeft[provider.id],
3927
+ isHighlighted: provider.id === highlightedProviderId,
3928
+ onEdit: (p) => setEditingProvider(p),
3929
+ onDelete: handleDeleteProvider,
3930
+ onTest: (id) => {
3931
+ void runTest(id);
3932
+ }
3933
+ },
3934
+ provider.id
3935
+ )) })
3936
+ ] })
3785
3937
  ] });
3786
3938
  }
3787
3939
  async function fetcher(url) {
@@ -4192,6 +4344,36 @@ function groupContiguousEquals(ops) {
4192
4344
  }
4193
4345
  return out;
4194
4346
  }
4347
+ const KIND_VISUAL = {
4348
+ added: {
4349
+ icon: Plus,
4350
+ accent: "text-emerald-600 dark:text-emerald-400",
4351
+ bg: "bg-emerald-500/5 hover:bg-emerald-500/10",
4352
+ border: "border-l-emerald-500",
4353
+ label: "ADDED"
4354
+ },
4355
+ removed: {
4356
+ icon: Minus,
4357
+ accent: "text-rose-600 dark:text-rose-400",
4358
+ bg: "bg-rose-500/5 hover:bg-rose-500/10",
4359
+ border: "border-l-rose-500",
4360
+ label: "REMOVED"
4361
+ },
4362
+ changed: {
4363
+ icon: Pencil,
4364
+ accent: "text-amber-600 dark:text-amber-400",
4365
+ bg: "bg-amber-500/5 hover:bg-amber-500/10",
4366
+ border: "border-l-amber-500",
4367
+ label: "CHANGED"
4368
+ },
4369
+ equal: {
4370
+ icon: Equal,
4371
+ accent: "text-muted-foreground/70",
4372
+ bg: "bg-muted/20 hover:bg-muted/30",
4373
+ border: "border-l-muted-foreground/20",
4374
+ label: "EQUAL"
4375
+ }
4376
+ };
4195
4377
  function EqualRunRow({
4196
4378
  ops,
4197
4379
  expanded,
@@ -4200,19 +4382,20 @@ function EqualRunRow({
4200
4382
  const first = ops[0];
4201
4383
  const last = ops[ops.length - 1];
4202
4384
  if (first === void 0 || last === void 0) {
4203
- return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "col-span-3 text-muted-foreground/40 text-xs", children: "—" });
4385
+ return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-muted-foreground/40 text-xs", children: "—" });
4204
4386
  }
4205
4387
  const firstPath = first.path;
4206
4388
  const lastPath = last.path;
4207
4389
  const label = ops.length === 1 ? firstPath : `${firstPath} … ${lastPath}`;
4208
4390
  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: [
4391
+ const v = KIND_VISUAL.equal;
4392
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: cn("border-l-4 rounded-sm", v.border, v.bg), children: [
4210
4393
  /* @__PURE__ */ jsxRuntimeExports.jsxs(
4211
4394
  "button",
4212
4395
  {
4213
4396
  type: "button",
4214
4397
  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",
4398
+ className: "w-full text-left flex items-center gap-2 px-3 py-1.5 text-xs text-muted-foreground cursor-pointer",
4216
4399
  children: [
4217
4400
  /* @__PURE__ */ jsxRuntimeExports.jsx(
4218
4401
  ChevronRight,
@@ -4220,7 +4403,9 @@ function EqualRunRow({
4220
4403
  className: cn("size-3 transition-transform shrink-0", expanded && "rotate-90")
4221
4404
  }
4222
4405
  ),
4223
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-mono truncate flex-1", children: label }),
4406
+ /* @__PURE__ */ jsxRuntimeExports.jsx(v.icon, { className: cn("size-3 shrink-0", v.accent) }),
4407
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-mono truncate flex-1", title: `${firstPath} … ${lastPath}`, children: label }),
4408
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: cn("text-[10px] uppercase tracking-wider shrink-0", v.accent), children: v.label }),
4224
4409
  /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-muted-foreground/60 shrink-0", children: [
4225
4410
  "(",
4226
4411
  summary,
@@ -4229,37 +4414,252 @@ function EqualRunRow({
4229
4414
  ]
4230
4415
  }
4231
4416
  ),
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: [
4417
+ 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: [
4233
4418
  /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-mono text-xs text-muted-foreground mb-1", children: op.path }),
4234
4419
  /* @__PURE__ */ jsxRuntimeExports.jsx(JsonViewerFromString, { text: nodeToJsonString(op.value), defaultExpandDepth: 2 })
4235
4420
  ] }, op.path)) })
4236
4421
  ] });
4237
4422
  }
4238
- function AddOrRemoveRow({
4423
+ function UnifiedOpRow({
4239
4424
  op,
4240
- kind
4425
+ idx,
4426
+ copiedPath,
4427
+ onCopyPath,
4428
+ expanded,
4429
+ onToggle
4241
4430
  }) {
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
- ] });
4431
+ const v = KIND_VISUAL[op.kind];
4432
+ const Icon2 = v.icon;
4433
+ 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;
4434
+ const preview = op.kind === "changed" ? [
4435
+ {
4436
+ text: previewNode(op.left, 400),
4437
+ tone: "text-rose-700 dark:text-rose-300 line-through"
4438
+ },
4439
+ { text: previewNode(op.right, 400), tone: "text-emerald-700 dark:text-emerald-300" }
4440
+ ] : op.kind === "removed" ? [
4441
+ {
4442
+ text: previewNode(op.value, 400),
4443
+ tone: "text-rose-700 dark:text-rose-300 line-through"
4444
+ }
4445
+ ] : 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" }];
4446
+ const justCopied = copiedPath === op.path && op.path !== "";
4447
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs(
4448
+ "div",
4449
+ {
4450
+ "data-diff-idx": idx,
4451
+ "data-diff-kind": op.kind,
4452
+ className: cn("border-l-4 rounded-sm px-3 py-2 my-0.5 transition-colors", v.border, v.bg),
4453
+ children: [
4454
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
4455
+ "button",
4456
+ {
4457
+ type: "button",
4458
+ onClick: onToggle,
4459
+ disabled: !isExpandable2,
4460
+ className: cn(
4461
+ "w-full flex items-center gap-2 text-xs text-left rounded-sm",
4462
+ isExpandable2 ? "cursor-pointer" : "cursor-default"
4463
+ ),
4464
+ "aria-expanded": isExpandable2 ? expanded : void 0,
4465
+ "aria-label": isExpandable2 ? expanded ? `Collapse ${op.path || "root"}` : `Expand ${op.path || "root"}` : void 0,
4466
+ children: [
4467
+ isExpandable2 ? /* @__PURE__ */ jsxRuntimeExports.jsx(
4468
+ ChevronRight,
4469
+ {
4470
+ className: cn(
4471
+ "size-3 shrink-0 transition-transform",
4472
+ v.accent,
4473
+ expanded && "rotate-90"
4474
+ )
4475
+ }
4476
+ ) : /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "size-3 shrink-0", "aria-hidden": "true" }),
4477
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Icon2, { className: cn("size-3.5 shrink-0", v.accent), strokeWidth: 2.5 }),
4478
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-mono truncate flex-1 min-w-0", title: op.path || "(root)", children: op.path === "" ? "(root)" : op.path }),
4479
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
4480
+ "span",
4481
+ {
4482
+ className: cn(
4483
+ "text-[9px] font-bold uppercase tracking-wider shrink-0 px-1.5 py-0.5 rounded",
4484
+ v.accent,
4485
+ op.kind === "equal" ? "bg-muted/40" : "bg-background/60"
4486
+ ),
4487
+ children: v.label
4488
+ }
4489
+ ),
4490
+ op.path !== "" && /* @__PURE__ */ jsxRuntimeExports.jsx(
4491
+ "span",
4492
+ {
4493
+ role: "button",
4494
+ tabIndex: 0,
4495
+ onClick: (e) => {
4496
+ e.stopPropagation();
4497
+ onCopyPath(op.path);
4498
+ },
4499
+ onKeyDown: (e) => {
4500
+ if (e.key === "Enter" || e.key === " ") {
4501
+ e.stopPropagation();
4502
+ e.preventDefault();
4503
+ onCopyPath(op.path);
4504
+ }
4505
+ },
4506
+ className: cn(
4507
+ "shrink-0 p-1 rounded transition-colors cursor-pointer inline-flex items-center justify-center",
4508
+ justCopied ? "text-emerald-500" : "text-muted-foreground/50 hover:text-foreground hover:bg-muted"
4509
+ ),
4510
+ "aria-label": justCopied ? "Copied" : "Copy path",
4511
+ title: justCopied ? "Copied!" : "Copy path",
4512
+ children: justCopied ? /* @__PURE__ */ jsxRuntimeExports.jsx(Check, { className: "size-3" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Copy, { className: "size-3" })
4513
+ }
4514
+ )
4515
+ ]
4516
+ }
4517
+ ),
4518
+ preview.map((p, i) => (
4519
+ // biome-ignore lint/suspicious/noArrayIndexKey: preview list is rebuilt on every render and is positional
4520
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: cn("font-mono text-xs mt-1 break-all pl-5", p.tone), children: p.text }, i)
4521
+ )),
4522
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
4523
+ "div",
4524
+ {
4525
+ className: "overflow-hidden transition-all duration-200",
4526
+ style: { maxHeight: expanded && isExpandable2 ? "2000px" : "0" },
4527
+ "aria-hidden": !expanded,
4528
+ children: expanded && isExpandable2 && op.kind !== "equal" ? /* @__PURE__ */ jsxRuntimeExports.jsx(ExpandedSubtree, { op }) : null
4529
+ }
4530
+ )
4531
+ ]
4532
+ }
4533
+ );
4253
4534
  }
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) })
4535
+ function ExpandedSubtree({ op }) {
4536
+ if (op.kind === "added" || op.kind === "removed") {
4537
+ 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 }) });
4538
+ }
4539
+ const leftIsStructured = op.left.kind === "object" || op.left.kind === "array";
4540
+ const rightIsStructured = op.right.kind === "object" || op.right.kind === "array";
4541
+ if (!leftIsStructured && !rightIsStructured) {
4542
+ return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "pl-5 mt-2 text-xs text-muted-foreground/70 italic", children: "Primitive values are shown inline above." });
4543
+ }
4544
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "pl-5 mt-2 grid grid-cols-1 md:grid-cols-2 gap-2", children: [
4545
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "border border-rose-500/30 rounded p-2 bg-rose-500/5", children: [
4546
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-[10px] uppercase tracking-wider text-rose-500 mb-1", children: "Old" }),
4547
+ /* @__PURE__ */ jsxRuntimeExports.jsx(JsonViewerFromString, { text: nodeToJsonString(op.left), defaultExpandDepth: 2 })
4548
+ ] }),
4549
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "border border-emerald-500/30 rounded p-2 bg-emerald-500/5", children: [
4550
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-[10px] uppercase tracking-wider text-emerald-500 mb-1", children: "New" }),
4551
+ /* @__PURE__ */ jsxRuntimeExports.jsx(JsonViewerFromString, { text: nodeToJsonString(op.right), defaultExpandDepth: 2 })
4260
4552
  ] })
4261
4553
  ] });
4262
4554
  }
4555
+ function SummaryChips({
4556
+ counts,
4557
+ onJumpTo
4558
+ }) {
4559
+ const total = counts.added + counts.removed + counts.changed;
4560
+ 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: [
4561
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-muted-foreground font-medium", children: [
4562
+ total,
4563
+ " ",
4564
+ total === 1 ? "change" : "changes"
4565
+ ] }),
4566
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
4567
+ "button",
4568
+ {
4569
+ type: "button",
4570
+ onClick: () => onJumpTo("removed"),
4571
+ disabled: counts.removed === 0,
4572
+ className: cn(
4573
+ "inline-flex items-center gap-1 px-2 py-0.5 rounded-full border cursor-pointer transition-colors",
4574
+ 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"
4575
+ ),
4576
+ title: counts.removed > 0 ? "Jump to first removed" : "No removals",
4577
+ children: [
4578
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Minus, { className: "size-3" }),
4579
+ counts.removed,
4580
+ " removed"
4581
+ ]
4582
+ }
4583
+ ),
4584
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
4585
+ "button",
4586
+ {
4587
+ type: "button",
4588
+ onClick: () => onJumpTo("added"),
4589
+ disabled: counts.added === 0,
4590
+ className: cn(
4591
+ "inline-flex items-center gap-1 px-2 py-0.5 rounded-full border cursor-pointer transition-colors",
4592
+ 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"
4593
+ ),
4594
+ title: counts.added > 0 ? "Jump to first added" : "No additions",
4595
+ children: [
4596
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Plus, { className: "size-3" }),
4597
+ counts.added,
4598
+ " added"
4599
+ ]
4600
+ }
4601
+ ),
4602
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
4603
+ "button",
4604
+ {
4605
+ type: "button",
4606
+ onClick: () => onJumpTo("changed"),
4607
+ disabled: counts.changed === 0,
4608
+ className: cn(
4609
+ "inline-flex items-center gap-1 px-2 py-0.5 rounded-full border cursor-pointer transition-colors",
4610
+ 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"
4611
+ ),
4612
+ title: counts.changed > 0 ? "Jump to first changed" : "No changes",
4613
+ children: [
4614
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Pencil, { className: "size-3" }),
4615
+ counts.changed,
4616
+ " changed"
4617
+ ]
4618
+ }
4619
+ )
4620
+ ] });
4621
+ }
4622
+ function ModeToggle({
4623
+ mode,
4624
+ onChange
4625
+ }) {
4626
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "inline-flex rounded-md border border-border overflow-hidden", children: [
4627
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
4628
+ "button",
4629
+ {
4630
+ type: "button",
4631
+ onClick: () => onChange("unified"),
4632
+ "aria-pressed": mode === "unified",
4633
+ className: cn(
4634
+ "flex items-center gap-1 px-2 py-1 text-xs transition-colors cursor-pointer",
4635
+ mode === "unified" ? "bg-muted text-foreground" : "hover:bg-muted/50 text-muted-foreground"
4636
+ ),
4637
+ title: "Unified view (single column, emphasized diffs)",
4638
+ children: [
4639
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Rows3, { className: "size-3" }),
4640
+ "Unified"
4641
+ ]
4642
+ }
4643
+ ),
4644
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
4645
+ "button",
4646
+ {
4647
+ type: "button",
4648
+ onClick: () => onChange("split"),
4649
+ "aria-pressed": mode === "split",
4650
+ className: cn(
4651
+ "flex items-center gap-1 px-2 py-1 text-xs transition-colors border-l border-border cursor-pointer",
4652
+ mode === "split" ? "bg-muted text-foreground" : "hover:bg-muted/50 text-muted-foreground"
4653
+ ),
4654
+ title: "Split view (path | left | right)",
4655
+ children: [
4656
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Columns2, { className: "size-3" }),
4657
+ "Split"
4658
+ ]
4659
+ }
4660
+ )
4661
+ ] });
4662
+ }
4263
4663
  function SideSummary({ log, side }) {
4264
4664
  const conversationId = getConversationId(log);
4265
4665
  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 min-w-0 space-y-1 text-xs", children: [
@@ -4305,6 +4705,26 @@ function CompareDrawer({ left, right, onClose }) {
4305
4705
  return diffTrees(l, r);
4306
4706
  }, [left.rawRequestBody, right.rawRequestBody]);
4307
4707
  const grouped = reactExports.useMemo(() => groupContiguousEquals(ops), [ops]);
4708
+ const counts = reactExports.useMemo(() => {
4709
+ let added = 0;
4710
+ let removed = 0;
4711
+ let changed = 0;
4712
+ for (const g of grouped) {
4713
+ if (g.kind !== "single") continue;
4714
+ switch (g.op.kind) {
4715
+ case "added":
4716
+ added++;
4717
+ break;
4718
+ case "removed":
4719
+ removed++;
4720
+ break;
4721
+ case "changed":
4722
+ changed++;
4723
+ break;
4724
+ }
4725
+ }
4726
+ return { added, removed, changed };
4727
+ }, [grouped]);
4308
4728
  const [expandedRuns, setExpandedRuns] = reactExports.useState(/* @__PURE__ */ new Set());
4309
4729
  const toggleRun = (idx) => {
4310
4730
  setExpandedRuns((prev) => {
@@ -4314,6 +4734,44 @@ function CompareDrawer({ left, right, onClose }) {
4314
4734
  return next;
4315
4735
  });
4316
4736
  };
4737
+ const [expandedRows, setExpandedRows] = reactExports.useState(/* @__PURE__ */ new Set());
4738
+ const toggleRow = (idx) => {
4739
+ setExpandedRows((prev) => {
4740
+ const next = new Set(prev);
4741
+ if (next.has(idx)) next.delete(idx);
4742
+ else next.add(idx);
4743
+ return next;
4744
+ });
4745
+ };
4746
+ reactExports.useEffect(() => {
4747
+ setExpandedRows(/* @__PURE__ */ new Set());
4748
+ }, [left.id, right.id]);
4749
+ const [mode, setMode] = reactExports.useState("unified");
4750
+ const bodyRef = reactExports.useRef(null);
4751
+ const [copiedPath, setCopiedPath] = reactExports.useState(null);
4752
+ const copyResetTimer = reactExports.useRef(null);
4753
+ const onCopyPath = (path) => {
4754
+ void window.navigator.clipboard.writeText(path).then(() => {
4755
+ setCopiedPath(path);
4756
+ if (copyResetTimer.current !== null) clearTimeout(copyResetTimer.current);
4757
+ copyResetTimer.current = setTimeout(() => setCopiedPath(null), 1500);
4758
+ });
4759
+ };
4760
+ reactExports.useEffect(() => {
4761
+ return () => {
4762
+ if (copyResetTimer.current !== null) clearTimeout(copyResetTimer.current);
4763
+ };
4764
+ }, []);
4765
+ const jumpToKind = (kind) => {
4766
+ const idx = grouped.findIndex((g) => g.kind === "single" && g.op.kind === kind);
4767
+ if (idx === -1) return;
4768
+ const root = bodyRef.current;
4769
+ if (root === null) return;
4770
+ const el = root.querySelector(`[data-diff-idx="${idx}"]`);
4771
+ if (el !== null) {
4772
+ el.scrollIntoView({ behavior: "smooth", block: "center" });
4773
+ }
4774
+ };
4317
4775
  reactExports.useEffect(() => {
4318
4776
  const onKey = (e) => {
4319
4777
  if (e.key === "Escape") onClose();
@@ -4361,33 +4819,24 @@ function CompareDrawer({ left, right, onClose }) {
4361
4819
  /* @__PURE__ */ jsxRuntimeExports.jsx(SideSummary, { log: left, side: "left" }),
4362
4820
  /* @__PURE__ */ jsxRuntimeExports.jsx(SideSummary, { log: right, side: "right" })
4363
4821
  ] }),
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
- )
4822
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 shrink-0", children: [
4823
+ /* @__PURE__ */ jsxRuntimeExports.jsx(ModeToggle, { mode, onChange: setMode }),
4824
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
4825
+ "button",
4826
+ {
4827
+ type: "button",
4828
+ onClick: onClose,
4829
+ "aria-label": "Close",
4830
+ className: "p-1 rounded text-muted-foreground hover:text-foreground hover:bg-muted cursor-pointer",
4831
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx(X, { className: "size-4" })
4832
+ }
4833
+ )
4834
+ ] })
4374
4835
  ] }),
4375
4836
  !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) => {
4837
+ 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: [
4838
+ /* @__PURE__ */ jsxRuntimeExports.jsx(SummaryChips, { counts, onJumpTo: jumpToKind }),
4839
+ /* @__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) => {
4391
4840
  if (g.kind === "equal-run") {
4392
4841
  return /* @__PURE__ */ jsxRuntimeExports.jsx(
4393
4842
  EqualRunRow,
@@ -4400,29 +4849,20 @@ function CompareDrawer({ left, right, onClose }) {
4400
4849
  );
4401
4850
  }
4402
4851
  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
- ] }) })
4852
+ return /* @__PURE__ */ jsxRuntimeExports.jsx(
4853
+ UnifiedOpRow,
4854
+ {
4855
+ op,
4856
+ idx: i,
4857
+ copiedPath,
4858
+ onCopyPath,
4859
+ expanded: expandedRows.has(i),
4860
+ onToggle: () => toggleRow(i)
4861
+ },
4862
+ `o${i}`
4863
+ );
4864
+ }) }) : /* @__PURE__ */ jsxRuntimeExports.jsx(SplitBody, { grouped, left, right }) })
4865
+ ] })
4426
4866
  ]
4427
4867
  }
4428
4868
  )
@@ -4430,6 +4870,103 @@ function CompareDrawer({ left, right, onClose }) {
4430
4870
  }
4431
4871
  );
4432
4872
  }
4873
+ function SplitBody({
4874
+ grouped,
4875
+ left,
4876
+ right
4877
+ }) {
4878
+ 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: [
4879
+ /* @__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: [
4880
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Path" }),
4881
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [
4882
+ "Left (Log #",
4883
+ left.id,
4884
+ ")"
4885
+ ] }),
4886
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [
4887
+ "Right (Log #",
4888
+ right.id,
4889
+ ")"
4890
+ ] })
4891
+ ] }),
4892
+ grouped.map((g, i) => {
4893
+ if (g.kind === "equal-run") {
4894
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs(
4895
+ "div",
4896
+ {
4897
+ className: "col-span-3 px-2 py-1 text-xs text-muted-foreground/60",
4898
+ children: [
4899
+ g.ops.length,
4900
+ " equal siblings collapsed — switch to Unified to expand"
4901
+ ]
4902
+ },
4903
+ i
4904
+ );
4905
+ }
4906
+ const op = g.op;
4907
+ if (op.kind === "equal") {
4908
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs(
4909
+ "div",
4910
+ {
4911
+ className: "col-span-3 grid grid-cols-[200px_1fr_1fr] gap-x-2 px-2 py-0.5 text-muted-foreground",
4912
+ children: [
4913
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-mono text-xs truncate", title: op.path, children: op.path }),
4914
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-mono text-xs break-all opacity-60", children: previewNode(op.value, 200) }),
4915
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-mono text-xs break-all opacity-60", children: previewNode(op.value, 200) })
4916
+ ]
4917
+ },
4918
+ i
4919
+ );
4920
+ }
4921
+ if (op.kind === "added") {
4922
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs(
4923
+ "div",
4924
+ {
4925
+ className: "col-span-3 px-2 py-1 rounded text-xs border-l-2 border-l-emerald-400/70 bg-emerald-500/5",
4926
+ children: [
4927
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-mono text-xs text-muted-foreground mb-0.5", children: op.path }),
4928
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "font-mono break-all text-emerald-300/90", children: [
4929
+ "+ ",
4930
+ previewNode(op.value, 400)
4931
+ ] })
4932
+ ]
4933
+ },
4934
+ i
4935
+ );
4936
+ }
4937
+ if (op.kind === "removed") {
4938
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs(
4939
+ "div",
4940
+ {
4941
+ className: "col-span-3 px-2 py-1 rounded text-xs border-l-2 border-l-rose-400/70 bg-rose-500/5",
4942
+ children: [
4943
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-mono text-xs text-muted-foreground mb-0.5", children: op.path }),
4944
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "font-mono break-all text-rose-300/90 line-through", children: [
4945
+ "− ",
4946
+ previewNode(op.value, 400)
4947
+ ] })
4948
+ ]
4949
+ },
4950
+ i
4951
+ );
4952
+ }
4953
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs(
4954
+ "div",
4955
+ {
4956
+ className: "col-span-3 px-2 py-1 rounded text-xs border-l-2 border-l-amber-400/70 bg-amber-500/5",
4957
+ children: [
4958
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-mono text-xs text-muted-foreground mb-1", children: op.path }),
4959
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-2 gap-2", children: [
4960
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-mono text-rose-300/90 break-all line-through", children: previewNode(op.left, 400) }),
4961
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-mono text-emerald-300/90 break-all", children: previewNode(op.right, 400) })
4962
+ ] })
4963
+ ]
4964
+ },
4965
+ i
4966
+ );
4967
+ })
4968
+ ] });
4969
+ }
4433
4970
  function truncateSessionId(id) {
4434
4971
  if (id.length <= 30) return id;
4435
4972
  return id.slice(0, 12) + "…" + id.slice(-12);