@tonyclaw/llm-inspector 1.14.9 → 1.15.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.
@@ -198,7 +198,7 @@ function getResponse() {
198
198
  return event.res;
199
199
  }
200
200
  async function getStartManifest(matchedRoutes) {
201
- const { tsrStartManifest } = await import("../_tanstack-start-manifest_v-BVIiyDeJ.mjs");
201
+ const { tsrStartManifest } = await import("../_tanstack-start-manifest_v-C9Wq6YdJ.mjs");
202
202
  const startManifest = tsrStartManifest();
203
203
  const rootRoute = startManifest.routes[rootRouteId] = startManifest.routes[rootRouteId] || {};
204
204
  rootRoute.assets = rootRoute.assets || [];
@@ -767,7 +767,7 @@ let entriesPromise;
767
767
  let baseManifestPromise;
768
768
  let cachedFinalManifestPromise;
769
769
  async function loadEntries() {
770
- const routerEntry = await import("./router-CmanwZJc.mjs").then((n) => n.r);
770
+ const routerEntry = await import("./router-DpLCKk51.mjs").then((n) => n.r);
771
771
  const startEntry = await import("./start-HYkvq4Ni.mjs");
772
772
  return { startEntry, routerEntry };
773
773
  }
@@ -45,7 +45,7 @@ import "../_libs/debounce-fn.mjs";
45
45
  import "../_libs/mimic-function.mjs";
46
46
  import "../_libs/semver.mjs";
47
47
  import "../_libs/uint8array-extras.mjs";
48
- const appCss = "/assets/index-bqeypwJB.css";
48
+ const appCss = "/assets/index-DciyfYBk.css";
49
49
  const Route$k = createRootRoute({
50
50
  head: () => ({
51
51
  meta: [
@@ -69,7 +69,7 @@ function RootDocument({ children }) {
69
69
  ] })
70
70
  ] });
71
71
  }
72
- const $$splitComponentImporter = () => import("./index-_9xcAkkw.mjs");
72
+ const $$splitComponentImporter = () => import("./index-P66uoVEU.mjs");
73
73
  const Route$j = createFileRoute("/")({
74
74
  component: lazyRouteComponent($$splitComponentImporter, "component")
75
75
  });
@@ -1,4 +1,4 @@
1
- const tsrStartManifest = () => ({ "routes": { "__root__": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/__root.tsx", "children": ["/", "/api/config", "/api/health", "/api/logs", "/api/mcp", "/api/models", "/api/providers", "/api/sessions", "/proxy/$"], "preloads": ["/assets/main-C8OUJKbz.js"], "assets": [] }, "/": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/index.tsx", "assets": [], "preloads": ["/assets/index-Dv-dj1xH.js"] }, "/api/config": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/config.ts", "children": ["/api/config/paths"] }, "/api/health": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/health.ts" }, "/api/logs": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/logs.ts", "children": ["/api/logs/$id", "/api/logs/stream"] }, "/api/mcp": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/mcp.ts" }, "/api/models": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/models.ts" }, "/api/providers": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/providers.ts", "children": ["/api/providers/$providerId", "/api/providers/export", "/api/providers/import", "/api/providers/scan"] }, "/api/sessions": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/sessions.ts" }, "/proxy/$": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/proxy/$.ts" }, "/api/config/paths": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/config.paths.ts" }, "/api/logs/$id": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/logs.$id.ts", "children": ["/api/logs/$id/chunks", "/api/logs/$id/replay"] }, "/api/logs/stream": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/logs.stream.ts" }, "/api/providers/$providerId": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/providers.$providerId.ts", "children": ["/api/providers/$providerId/test"] }, "/api/providers/export": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/providers.export.ts" }, "/api/providers/import": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/providers.import.ts" }, "/api/providers/scan": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/providers.scan.ts" }, "/api/logs/$id/chunks": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/logs.$id.chunks.ts" }, "/api/logs/$id/replay": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/logs.$id.replay.ts" }, "/api/providers/$providerId/test": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/providers.$providerId.test.ts", "children": ["/api/providers/$providerId/test/log"] }, "/api/providers/$providerId/test/log": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/providers.$providerId.test.log.ts" } }, "clientEntry": "/assets/main-C8OUJKbz.js" });
1
+ const tsrStartManifest = () => ({ "routes": { "__root__": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/__root.tsx", "children": ["/", "/api/config", "/api/health", "/api/logs", "/api/mcp", "/api/models", "/api/providers", "/api/sessions", "/proxy/$"], "preloads": ["/assets/main-BLYgekFx.js"], "assets": [] }, "/": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/index.tsx", "assets": [], "preloads": ["/assets/index-CMuJQyt1.js"] }, "/api/config": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/config.ts", "children": ["/api/config/paths"] }, "/api/health": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/health.ts" }, "/api/logs": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/logs.ts", "children": ["/api/logs/$id", "/api/logs/stream"] }, "/api/mcp": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/mcp.ts" }, "/api/models": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/models.ts" }, "/api/providers": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/providers.ts", "children": ["/api/providers/$providerId", "/api/providers/export", "/api/providers/import", "/api/providers/scan"] }, "/api/sessions": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/sessions.ts" }, "/proxy/$": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/proxy/$.ts" }, "/api/config/paths": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/config.paths.ts" }, "/api/logs/$id": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/logs.$id.ts", "children": ["/api/logs/$id/chunks", "/api/logs/$id/replay"] }, "/api/logs/stream": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/logs.stream.ts" }, "/api/providers/$providerId": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/providers.$providerId.ts", "children": ["/api/providers/$providerId/test"] }, "/api/providers/export": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/providers.export.ts" }, "/api/providers/import": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/providers.import.ts" }, "/api/providers/scan": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/providers.scan.ts" }, "/api/logs/$id/chunks": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/logs.$id.chunks.ts" }, "/api/logs/$id/replay": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/logs.$id.replay.ts" }, "/api/providers/$providerId/test": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/providers.$providerId.test.ts", "children": ["/api/providers/$providerId/test/log"] }, "/api/providers/$providerId/test/log": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/api/providers.$providerId.test.log.ts" } }, "clientEntry": "/assets/main-BLYgekFx.js" });
2
2
  export {
3
3
  tsrStartManifest
4
4
  };
@@ -35,54 +35,54 @@ const headers = ((m) => function headersRouteRule(event) {
35
35
  }
36
36
  });
37
37
  const assets = {
38
+ "/assets/index-DciyfYBk.css": {
39
+ "type": "text/css; charset=utf-8",
40
+ "etag": '"15867-TOX1Gddns0p3jKt+7co1NuZS+d8"',
41
+ "mtime": "2026-06-12T07:17:56.431Z",
42
+ "size": 88167,
43
+ "path": "../public/assets/index-DciyfYBk.css"
44
+ },
38
45
  "/assets/alibaba-TTwafVwX.svg": {
39
46
  "type": "image/svg+xml",
40
47
  "etag": '"171b-6dyV5K8QjiaY35sN9qNprh9zDIs"',
41
- "mtime": "2026-06-11T13:44:56.125Z",
48
+ "mtime": "2026-06-12T07:17:56.428Z",
42
49
  "size": 5915,
43
50
  "path": "../public/assets/alibaba-TTwafVwX.svg"
44
51
  },
45
- "/assets/index-bqeypwJB.css": {
46
- "type": "text/css; charset=utf-8",
47
- "etag": '"14d2d-fciDL/LvvG4KiFnlq0WNx0FqBmw"',
48
- "mtime": "2026-06-11T13:44:56.125Z",
49
- "size": 85293,
50
- "path": "../public/assets/index-bqeypwJB.css"
51
- },
52
- "/assets/minimax-BPMzvuL-.jpeg": {
53
- "type": "image/jpeg",
54
- "etag": '"1b06-IwivU89ko5UTMUM1/t7hn4sQK9A"',
55
- "mtime": "2026-06-11T13:44:56.125Z",
56
- "size": 6918,
57
- "path": "../public/assets/minimax-BPMzvuL-.jpeg"
52
+ "/assets/main-BLYgekFx.js": {
53
+ "type": "text/javascript; charset=utf-8",
54
+ "etag": '"50599-tks6aTeb/Edw4laR3cdpiWA48mc"',
55
+ "mtime": "2026-06-12T07:17:56.431Z",
56
+ "size": 329113,
57
+ "path": "../public/assets/main-BLYgekFx.js"
58
58
  },
59
59
  "/assets/zhipuai-BPNAnxo-.svg": {
60
60
  "type": "image/svg+xml",
61
61
  "etag": '"2bf8-hNaLCTi89nOFCsIIfWpP/jrfo0s"',
62
- "mtime": "2026-06-11T13:44:56.122Z",
62
+ "mtime": "2026-06-12T07:17:56.431Z",
63
63
  "size": 11256,
64
64
  "path": "../public/assets/zhipuai-BPNAnxo-.svg"
65
65
  },
66
- "/assets/main-C8OUJKbz.js": {
67
- "type": "text/javascript; charset=utf-8",
68
- "etag": '"50599-x4LjDXM7jigK2tdjHCfXl0STwVQ"',
69
- "mtime": "2026-06-11T13:44:56.125Z",
70
- "size": 329113,
71
- "path": "../public/assets/main-C8OUJKbz.js"
66
+ "/assets/minimax-BPMzvuL-.jpeg": {
67
+ "type": "image/jpeg",
68
+ "etag": '"1b06-IwivU89ko5UTMUM1/t7hn4sQK9A"',
69
+ "mtime": "2026-06-12T07:17:56.431Z",
70
+ "size": 6918,
71
+ "path": "../public/assets/minimax-BPMzvuL-.jpeg"
72
72
  },
73
73
  "/assets/qwen-CONDcHqt.png": {
74
74
  "type": "image/png",
75
75
  "etag": '"572c3-cdJAPaHdOvFCGzuaQjagdgOu6XE"',
76
- "mtime": "2026-06-11T13:44:56.125Z",
76
+ "mtime": "2026-06-12T07:17:56.431Z",
77
77
  "size": 357059,
78
78
  "path": "../public/assets/qwen-CONDcHqt.png"
79
79
  },
80
- "/assets/index-Dv-dj1xH.js": {
80
+ "/assets/index-CMuJQyt1.js": {
81
81
  "type": "text/javascript; charset=utf-8",
82
- "etag": '"9600f-3dnGI92f5ekK1BS9qp5iLg0Map4"',
83
- "mtime": "2026-06-11T13:44:56.127Z",
84
- "size": 614415,
85
- "path": "../public/assets/index-Dv-dj1xH.js"
82
+ "etag": '"97f56-eCDTvraNzTc2Un6pdqimYNiZVhw"',
83
+ "mtime": "2026-06-12T07:17:56.431Z",
84
+ "size": 622422,
85
+ "path": "../public/assets/index-CMuJQyt1.js"
86
86
  }
87
87
  };
88
88
  function readAsset(id) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tonyclaw/llm-inspector",
3
- "version": "1.14.9",
3
+ "version": "1.15.0",
4
4
  "type": "module",
5
5
  "description": "LLM API proxy inspector — captures and displays requests/responses from AI coding tools in a web UI",
6
6
  "license": "MIT",
@@ -1,21 +1,15 @@
1
1
  import { type JSX, useCallback, useEffect, useMemo, useState, useRef } from "react";
2
2
  import { useVirtualizer } from "@tanstack/react-virtual";
3
- import { Download, GitBranch, LayoutGrid, List } from "lucide-react";
4
- import { cn } from "../lib/utils";
3
+ import { Download } from "lucide-react";
4
+
5
5
  import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "./ui/tooltip";
6
6
  import type { CapturedLog } from "../proxy/schemas";
7
7
  import { exportLogsAsZip } from "../lib/export-logs";
8
8
  import packageJson from "../../package.json";
9
- import {
10
- ConversationGroup,
11
- groupLogsByConversation,
12
- LogEntry,
13
- ThreadConnector,
14
- type ConversationGroupData,
15
- type ViewMode,
16
- } from "./proxy-viewer";
17
- import { extractStopReason } from "../lib/stopReason";
9
+ import { ConversationGroup, groupLogsByConversation } from "./proxy-viewer";
10
+
18
11
  import { CrabLogo } from "./ui/crab-logo";
12
+ import { crabVariants } from "./ui/crab-variants";
19
13
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
20
14
  import { SettingsDialog } from "./providers/SettingsDialog";
21
15
  import { computeCacheTrends } from "./proxy-viewer/cacheTrend";
@@ -117,8 +111,6 @@ export function ProxyViewer({
117
111
  strip,
118
112
  }: ProxyViewerProps): JSX.Element {
119
113
  const { totalIn, totalOut } = computeTokenSummary(logs);
120
- const [groupedView, setGroupedView] = useState(true);
121
- const [groupViewMode, setGroupViewMode] = useState<ViewMode>("thread");
122
114
  const [exporting, setExporting] = useState(false);
123
115
  const [comparePair, setComparePair] = useState<[CapturedLog, CapturedLog] | null>(null);
124
116
 
@@ -155,25 +147,9 @@ export function ProxyViewer({
155
147
 
156
148
  const groups = useMemo(() => groupLogsByConversation(logs), [logs]);
157
149
  const cacheTrends = useMemo(() => computeCacheTrends(groups), [groups]);
158
- const stopReasons = useMemo(() => logs.map((log) => extractStopReason(log)), [logs]);
159
-
160
- const turnIndices = useMemo(() => {
161
- const indices: number[] = [];
162
- let turn = 0;
163
- for (let i = 0; i < stopReasons.length; i++) {
164
- if (i > 0 && (stopReasons[i - 1] === "end_turn" || stopReasons[i - 1] === "stop")) {
165
- turn++;
166
- }
167
- indices.push(turn);
168
- }
169
- return indices;
170
- }, [stopReasons]);
171
-
172
- // Determine what items to render (groups or individual logs)
173
- const renderGroups = logs.length > 0 && groupedView && groups.length > 1;
174
150
 
175
151
  const rowVirtualizer = useVirtualizer({
176
- count: renderGroups ? groups.length : logs.length,
152
+ count: groups.length,
177
153
  getScrollElement: () => parentRef.current,
178
154
  estimateSize: () => 150,
179
155
  measureElement:
@@ -185,15 +161,75 @@ export function ProxyViewer({
185
161
 
186
162
  return (
187
163
  <div className="max-w-[1200px] mx-auto flex flex-col h-screen" style={{ maxHeight: "100vh" }}>
188
- {/* Header */}
189
- <div className="flex items-center gap-4 mb-4 px-6 pt-6">
190
- <h1 className="text-lg font-bold flex-1 flex items-center gap-2">
191
- <CrabLogo className="size-10 text-amber-500 self-center" />
164
+ {/* Brand row */}
165
+ <div className="flex items-end px-6 pt-6 pb-3 relative">
166
+ <h1 className="text-lg font-bold flex items-end gap-2 absolute left-1/2 -translate-x-1/2 whitespace-nowrap">
167
+ {/* Crab family hover to animate together */}
168
+ <span className="flex items-end gap-1 group cursor-default" aria-hidden="true">
169
+ <CrabLogo className="size-10 text-amber-500 transition-all duration-300 group-hover:scale-125 group-hover:-translate-y-1.5" />
170
+ <span className="flex items-end gap-0.5">
171
+ {crabVariants.map((Crab, i) => (
172
+ <Crab
173
+ key={i}
174
+ className={`size-5 ${
175
+ [
176
+ "text-amber-500",
177
+ "text-rose-500",
178
+ "text-sky-500",
179
+ "text-emerald-500",
180
+ "text-violet-500",
181
+ "text-orange-500",
182
+ "text-cyan-500",
183
+ "text-pink-500",
184
+ "text-lime-500",
185
+ "text-blue-500",
186
+ "text-yellow-500",
187
+ "text-fuchsia-500",
188
+ ][i]
189
+ } transition-all duration-300 ease-out group-hover:scale-125 group-hover:-translate-y-1`}
190
+ style={{ transitionDelay: `${i * 50}ms` }}
191
+ />
192
+ ))}
193
+ </span>
194
+ </span>
192
195
  <span className="flex items-baseline gap-2">
193
196
  LLM Inspector
194
197
  <span className="text-xs text-muted-foreground font-mono">v{packageJson.version}</span>
195
198
  </span>
196
199
  </h1>
200
+ <div className="ml-auto">
201
+ <SettingsDialog />
202
+ </div>
203
+ </div>
204
+
205
+ {/* Controls + Filters */}
206
+ <div className="flex items-center gap-3 px-6 mb-4">
207
+ <Select value={selectedSession} onValueChange={onSessionChange}>
208
+ <SelectTrigger className="flex-1 max-w-[350px] text-xs">
209
+ <SelectValue placeholder="All sessions" />
210
+ </SelectTrigger>
211
+ <SelectContent>
212
+ <SelectItem value="__all__">All sessions</SelectItem>
213
+ {sessions.map((s) => (
214
+ <SelectItem key={s} value={s}>
215
+ {truncateSessionId(s)}
216
+ </SelectItem>
217
+ ))}
218
+ </SelectContent>
219
+ </Select>
220
+ <Select value={selectedModel} onValueChange={onModelChange}>
221
+ <SelectTrigger className="flex-1 max-w-[250px] text-xs">
222
+ <SelectValue placeholder="All models" />
223
+ </SelectTrigger>
224
+ <SelectContent>
225
+ <SelectItem value="__all__">All models</SelectItem>
226
+ {models.map((m) => (
227
+ <SelectItem key={m} value={m}>
228
+ {m}
229
+ </SelectItem>
230
+ ))}
231
+ </SelectContent>
232
+ </Select>
197
233
  <TooltipProvider>
198
234
  <Tooltip>
199
235
  <TooltipTrigger asChild>
@@ -227,7 +263,7 @@ export function ProxyViewer({
227
263
  </TooltipContent>
228
264
  </Tooltip>
229
265
  </TooltipProvider>
230
- <SettingsDialog />
266
+ <div className="flex-1" />
231
267
  <span className="text-muted-foreground text-xs font-mono">
232
268
  {logs.length} request{logs.length !== 1 ? "s" : ""}
233
269
  {totalIn > 0 || totalOut > 0
@@ -249,7 +285,7 @@ export function ProxyViewer({
249
285
  ) : (
250
286
  <>
251
287
  <Download className="size-3" />
252
- <span>Export All</span>
288
+ <span>Export</span>
253
289
  </>
254
290
  )}
255
291
  </button>
@@ -260,97 +296,15 @@ export function ProxyViewer({
260
296
  className="text-xs text-muted-foreground hover:text-foreground transition-colors cursor-pointer"
261
297
  title="Clear all logs"
262
298
  >
263
- Clear All
299
+ Clear
264
300
  </button>
265
301
  </div>
266
302
 
267
- {/* Filters */}
268
- <div className="flex gap-3 px-6 mb-4">
269
- <Select value={selectedSession} onValueChange={onSessionChange}>
270
- <SelectTrigger className="flex-1 max-w-[400px] text-xs">
271
- <SelectValue placeholder="All sessions" />
272
- </SelectTrigger>
273
- <SelectContent>
274
- <SelectItem value="__all__">All sessions</SelectItem>
275
- {sessions.map((s) => (
276
- <SelectItem key={s} value={s}>
277
- {truncateSessionId(s)}
278
- </SelectItem>
279
- ))}
280
- </SelectContent>
281
- </Select>
282
-
283
- <Select value={selectedModel} onValueChange={onModelChange}>
284
- <SelectTrigger className="text-xs">
285
- <SelectValue placeholder="All models" />
286
- </SelectTrigger>
287
- <SelectContent>
288
- <SelectItem value="__all__">All models</SelectItem>
289
- {models.map((m) => (
290
- <SelectItem key={m} value={m}>
291
- {m}
292
- </SelectItem>
293
- ))}
294
- </SelectContent>
295
- </Select>
296
-
297
- {/* View toggle */}
298
- <div className="flex items-center border border-border rounded-md overflow-hidden">
299
- <button
300
- type="button"
301
- onClick={() => setGroupedView(true)}
302
- className={`px-2 py-1.5 cursor-pointer transition-colors ${
303
- groupedView ? "bg-muted text-foreground" : "text-muted-foreground hover:bg-muted/50"
304
- }`}
305
- title="Grouped view"
306
- >
307
- <LayoutGrid className="size-4" />
308
- </button>
309
- <button
310
- type="button"
311
- onClick={() => setGroupedView(false)}
312
- className={`px-2 py-1.5 cursor-pointer transition-colors ${
313
- !groupedView ? "bg-muted text-foreground" : "text-muted-foreground hover:bg-muted/50"
314
- }`}
315
- title="Flat view"
316
- >
317
- <List className="size-4" />
318
- </button>
319
- </div>
320
-
321
- {/* Thread/flat mode toggle */}
322
- <div className="flex items-center border border-border rounded-md overflow-hidden">
323
- <button
324
- type="button"
325
- onClick={() => setGroupViewMode("thread")}
326
- className={`px-2 py-1.5 cursor-pointer transition-colors ${
327
- groupViewMode === "thread"
328
- ? "bg-amber-500/15 text-amber-400 border-r border-amber-500/30"
329
- : "text-muted-foreground hover:bg-muted/50"
330
- }`}
331
- title="Thread view (connected timeline)"
332
- >
333
- <GitBranch className="size-4" />
334
- </button>
335
- <button
336
- type="button"
337
- onClick={() => setGroupViewMode("flat")}
338
- className={`px-2 py-1.5 cursor-pointer transition-colors ${
339
- groupViewMode === "flat"
340
- ? "bg-muted text-foreground"
341
- : "text-muted-foreground hover:bg-muted/50"
342
- }`}
343
- title="Flat view (card list)"
344
- >
345
- <List className="size-4" />
346
- </button>
347
- </div>
348
- </div>
349
-
350
303
  {/* Log list */}
351
304
  <div className="flex-1 min-h-0 px-6 pb-6">
352
305
  {logs.length === 0 ? (
353
306
  <div className="text-center text-muted-foreground py-16 space-y-4">
307
+ <CrabLogo className="size-12 text-muted-foreground/20 mx-auto" />
354
308
  <p className="text-sm">No requests captured yet.</p>
355
309
  <p className="text-xs">Route AI coding tools through the proxy:</p>
356
310
  <CopyableCommand command="LLM_BASE_URL=http://localhost:25947/proxy <your-tool>" />
@@ -365,89 +319,31 @@ export function ProxyViewer({
365
319
  }}
366
320
  >
367
321
  {rowVirtualizer.getVirtualItems().map((virtualRow) => {
368
- if (renderGroups) {
369
- const group = groups[virtualRow.index];
370
- if (group === undefined) return null;
371
- return (
372
- <div
373
- key={group.id}
374
- data-index={virtualRow.index}
375
- ref={rowVirtualizer.measureElement}
376
- style={{
377
- position: "absolute",
378
- top: 0,
379
- left: 0,
380
- width: "100%",
381
- transform: `translateY(${virtualRow.start}px)`,
382
- }}
383
- >
384
- <ConversationGroup
385
- group={group}
386
- viewMode={viewMode}
387
- strip={strip}
388
- cacheTrends={cacheTrends}
389
- onCompareWithPrevious={handleCompareWithPrevious}
390
- defaultGroupViewMode={groupViewMode}
391
- />
392
- </div>
393
- );
394
- } else {
395
- const log = logs[virtualRow.index];
396
- if (log === undefined) return null;
397
- const idx = virtualRow.index;
398
- return (
399
- <div
400
- key={log.id}
401
- data-index={virtualRow.index}
402
- ref={rowVirtualizer.measureElement}
403
- style={{
404
- position: "absolute",
405
- top: 0,
406
- left: 0,
407
- width: "100%",
408
- transform: `translateY(${virtualRow.start}px)`,
409
- }}
410
- >
411
- {groupViewMode === "thread" ? (
412
- <div className="flex items-stretch ml-3">
413
- <ThreadConnector
414
- stopReason={stopReasons[idx] ?? null}
415
- isPending={log.responseStatus === null}
416
- isFirst={idx === 0}
417
- isLast={idx === logs.length - 1}
418
- isTurnStart={
419
- idx === 0 ||
420
- stopReasons[idx - 1] === "end_turn" ||
421
- stopReasons[idx - 1] === "stop"
422
- }
423
- />
424
- <div
425
- className={cn(
426
- "flex-1 min-w-0 mb-2 rounded-lg",
427
- (turnIndices[idx] ?? 0) % 2 === 0 ? "bg-muted/10" : "bg-muted/25",
428
- )}
429
- >
430
- <LogEntry
431
- log={log}
432
- viewMode={viewMode}
433
- strip={strip}
434
- cacheTrend={cacheTrends.get(log.id) ?? null}
435
- onCompareWithPrevious={() => handleCompareWithPrevious(log)}
436
- />
437
- </div>
438
- </div>
439
- ) : (
440
- <LogEntry
441
- log={log}
442
- viewMode={viewMode}
443
- strip={strip}
444
- cacheTrend={cacheTrends.get(log.id) ?? null}
445
- onCompareWithPrevious={() => handleCompareWithPrevious(log)}
446
- />
447
- )}
448
- </div>
449
- );
450
- }
322
+ const group = groups[virtualRow.index];
323
+ if (group === undefined) return null;
324
+ return (
325
+ <div
326
+ key={group.id}
327
+ data-index={virtualRow.index}
328
+ ref={rowVirtualizer.measureElement}
329
+ style={{
330
+ position: "absolute",
331
+ top: 0,
332
+ left: 0,
333
+ width: "100%",
334
+ transform: `translateY(${virtualRow.start}px)`,
335
+ }}
336
+ >
337
+ <ConversationGroup
338
+ group={group}
339
+ viewMode={viewMode}
340
+ strip={strip}
341
+ cacheTrends={cacheTrends}
342
+ onCompareWithPrevious={handleCompareWithPrevious}
343
+ standalone={groups.length === 1}
344
+ />
345
+ </div>
346
+ );
451
347
  })}
452
348
  </div>
453
349
  </div>