@tonyclaw/llm-inspector 1.12.0 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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-l1kWkG0h.mjs");
201
+ const { tsrStartManifest } = await import("../_tanstack-start-manifest_v-DUbXa1lt.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-PZjNwOcw.mjs").then((n) => n.r);
770
+ const routerEntry = await import("./router-D5ccnemB.mjs").then((n) => n.r);
771
771
  const startEntry = await import("./start-HYkvq4Ni.mjs");
772
772
  return { startEntry, routerEntry };
773
773
  }
@@ -44,7 +44,7 @@ import "../_libs/debounce-fn.mjs";
44
44
  import "../_libs/mimic-function.mjs";
45
45
  import "../_libs/semver.mjs";
46
46
  import "../_libs/uint8array-extras.mjs";
47
- const appCss = "/assets/index-DZx2yk8v.css";
47
+ const appCss = "/assets/index-B0anmGQr.css";
48
48
  const Route$h = createRootRoute({
49
49
  head: () => ({
50
50
  meta: [
@@ -68,7 +68,7 @@ function RootDocument({ children }) {
68
68
  ] })
69
69
  ] });
70
70
  }
71
- const $$splitComponentImporter = () => import("./index-DhChP_jV.mjs");
71
+ const $$splitComponentImporter = () => import("./index-C8VC13EA.mjs");
72
72
  const Route$g = createFileRoute("/")({
73
73
  component: lazyRouteComponent($$splitComponentImporter, "component")
74
74
  });
@@ -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/models", "/api/providers", "/api/sessions", "/proxy/$"], "preloads": ["/assets/main-BYCM7aJx.js"], "assets": [] }, "/": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/index.tsx", "assets": [], "preloads": ["/assets/index-DVgdkDgq.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/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/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/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" } }, "clientEntry": "/assets/main-BYCM7aJx.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/models", "/api/providers", "/api/sessions", "/proxy/$"], "preloads": ["/assets/main-C3tLo75s.js"], "assets": [] }, "/": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/index.tsx", "assets": [], "preloads": ["/assets/index-H_thmL2_.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/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/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/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" } }, "clientEntry": "/assets/main-C3tLo75s.js" });
2
2
  export {
3
3
  tsrStartManifest
4
4
  };
@@ -38,51 +38,51 @@ const assets = {
38
38
  "/assets/alibaba-TTwafVwX.svg": {
39
39
  "type": "image/svg+xml",
40
40
  "etag": '"171b-6dyV5K8QjiaY35sN9qNprh9zDIs"',
41
- "mtime": "2026-06-09T08:02:30.828Z",
41
+ "mtime": "2026-06-09T09:07:57.962Z",
42
42
  "size": 5915,
43
43
  "path": "../public/assets/alibaba-TTwafVwX.svg"
44
44
  },
45
- "/assets/minimax-BPMzvuL-.jpeg": {
46
- "type": "image/jpeg",
47
- "etag": '"1b06-IwivU89ko5UTMUM1/t7hn4sQK9A"',
48
- "mtime": "2026-06-09T08:02:30.828Z",
49
- "size": 6918,
50
- "path": "../public/assets/minimax-BPMzvuL-.jpeg"
51
- },
52
- "/assets/index-DZx2yk8v.css": {
53
- "type": "text/css; charset=utf-8",
54
- "etag": '"1177b-93l31JbQrAbdmLoUOxZGbqTQwcc"',
55
- "mtime": "2026-06-09T08:02:30.830Z",
56
- "size": 71547,
57
- "path": "../public/assets/index-DZx2yk8v.css"
58
- },
59
45
  "/assets/zhipuai-BPNAnxo-.svg": {
60
46
  "type": "image/svg+xml",
61
47
  "etag": '"2bf8-hNaLCTi89nOFCsIIfWpP/jrfo0s"',
62
- "mtime": "2026-06-09T08:02:30.828Z",
48
+ "mtime": "2026-06-09T09:07:57.965Z",
63
49
  "size": 11256,
64
50
  "path": "../public/assets/zhipuai-BPNAnxo-.svg"
65
51
  },
66
- "/assets/main-BYCM7aJx.js": {
52
+ "/assets/index-B0anmGQr.css": {
53
+ "type": "text/css; charset=utf-8",
54
+ "etag": '"12a87-6IlHwgxaeugM2Z9VI5/+fVbaA40"',
55
+ "mtime": "2026-06-09T09:07:57.965Z",
56
+ "size": 76423,
57
+ "path": "../public/assets/index-B0anmGQr.css"
58
+ },
59
+ "/assets/main-C3tLo75s.js": {
67
60
  "type": "text/javascript; charset=utf-8",
68
- "etag": '"50599-LT3fOJsfcl49n+Hp+rSe62uyiuk"',
69
- "mtime": "2026-06-09T08:02:30.830Z",
61
+ "etag": '"50599-nkxkxzEGV0G+Y8N4Lyzxy/ivv4w"',
62
+ "mtime": "2026-06-09T09:07:57.965Z",
70
63
  "size": 329113,
71
- "path": "../public/assets/main-BYCM7aJx.js"
64
+ "path": "../public/assets/main-C3tLo75s.js"
72
65
  },
73
66
  "/assets/qwen-CONDcHqt.png": {
74
67
  "type": "image/png",
75
68
  "etag": '"572c3-cdJAPaHdOvFCGzuaQjagdgOu6XE"',
76
- "mtime": "2026-06-09T08:02:30.830Z",
69
+ "mtime": "2026-06-09T09:07:57.965Z",
77
70
  "size": 357059,
78
71
  "path": "../public/assets/qwen-CONDcHqt.png"
79
72
  },
80
- "/assets/index-DVgdkDgq.js": {
73
+ "/assets/minimax-BPMzvuL-.jpeg": {
74
+ "type": "image/jpeg",
75
+ "etag": '"1b06-IwivU89ko5UTMUM1/t7hn4sQK9A"',
76
+ "mtime": "2026-06-09T09:07:57.965Z",
77
+ "size": 6918,
78
+ "path": "../public/assets/minimax-BPMzvuL-.jpeg"
79
+ },
80
+ "/assets/index-H_thmL2_.js": {
81
81
  "type": "text/javascript; charset=utf-8",
82
- "etag": '"8a87f-jt4qwgWrMm8cZtArzPx7qX8p12k"',
83
- "mtime": "2026-06-09T08:02:30.830Z",
84
- "size": 567423,
85
- "path": "../public/assets/index-DVgdkDgq.js"
82
+ "etag": '"8daed-n1W4t2jQHmBQs8e35+QecepRw4w"',
83
+ "mtime": "2026-06-09T09:07:57.965Z",
84
+ "size": 580333,
85
+ "path": "../public/assets/index-H_thmL2_.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.12.0",
3
+ "version": "1.13.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,6 +1,6 @@
1
- import { type JSX, useCallback, useMemo, useState, useRef } from "react";
1
+ import { type JSX, useCallback, useEffect, useMemo, useState, useRef } from "react";
2
2
  import { useVirtualizer } from "@tanstack/react-virtual";
3
- import { Download, LayoutGrid, List } from "lucide-react";
3
+ import { Download, GitCompareArrows, LayoutGrid, List, X } from "lucide-react";
4
4
  import type { CapturedLog } from "../proxy/schemas";
5
5
  import { exportLogsAsZip } from "../lib/export-logs";
6
6
  import packageJson from "../../package.json";
@@ -14,6 +14,8 @@ import { CrabLogo } from "./ui/crab-logo";
14
14
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
15
15
  import { SettingsDialog } from "./providers/SettingsDialog";
16
16
  import { computeCacheTrends } from "./proxy-viewer/cacheTrend";
17
+ import { CompareDrawer } from "./proxy-viewer/CompareDrawer";
18
+ import { getConversationId } from "./proxy-viewer/ConversationHeader";
17
19
 
18
20
  function truncateSessionId(id: string): string {
19
21
  if (id.length <= 30) return id;
@@ -113,6 +115,9 @@ export function ProxyViewer({
113
115
  const { totalIn, totalOut } = computeTokenSummary(logs);
114
116
  const [groupedView, setGroupedView] = useState(true);
115
117
  const [exporting, setExporting] = useState(false);
118
+ const [selectedLogIds, setSelectedLogIds] = useState<number[]>([]);
119
+ const [compareOpen, setCompareOpen] = useState(false);
120
+ const [comparePair, setComparePair] = useState<[CapturedLog, CapturedLog] | null>(null);
116
121
 
117
122
  const handleExport = useCallback(async () => {
118
123
  setExporting(true);
@@ -124,6 +129,88 @@ export function ProxyViewer({
124
129
  }, [logs]);
125
130
  const parentRef = useRef<HTMLDivElement>(null);
126
131
 
132
+ const handleToggleSelect = useCallback((logId: number) => {
133
+ setSelectedLogIds((prev) => {
134
+ if (prev.includes(logId)) {
135
+ return prev.filter((id) => id !== logId);
136
+ }
137
+ if (prev.length < 2) {
138
+ return [...prev, logId];
139
+ }
140
+ // FIFO eviction: drop the oldest, append the new id.
141
+ const newer = prev[1];
142
+ if (newer === undefined) return prev;
143
+ return [newer, logId];
144
+ });
145
+ }, []);
146
+
147
+ // Reset the selection (and close the compare drawer) whenever the user
148
+ // changes the session or model filter, since the selected logs may no
149
+ // longer be in the visible list.
150
+ useEffect(() => {
151
+ setSelectedLogIds([]);
152
+ setCompareOpen(false);
153
+ }, [selectedSession, selectedModel]);
154
+
155
+ const selectedSet = useMemo(() => new Set(selectedLogIds), [selectedLogIds]);
156
+
157
+ const openCompare = useCallback(() => {
158
+ if (selectedLogIds.length !== 2) return;
159
+ const [idA, idB] = selectedLogIds;
160
+ if (idA === undefined || idB === undefined) return;
161
+ const logA = logs.find((l) => l.id === idA);
162
+ const logB = logs.find((l) => l.id === idB);
163
+ if (logA === undefined || logB === undefined) return;
164
+ setComparePair([logA, logB]);
165
+ setCompareOpen(true);
166
+ }, [selectedLogIds, logs]);
167
+
168
+ const closeCompare = useCallback(() => {
169
+ setCompareOpen(false);
170
+ // Keep `comparePair` so the selection survives across the drawer being
171
+ // closed and re-opened; it is replaced the next time the user opens
172
+ // the drawer with a different pair.
173
+ }, []);
174
+
175
+ const clearSelection = useCallback(() => {
176
+ setSelectedLogIds([]);
177
+ }, []);
178
+
179
+ const selectedSummary = useMemo(() => {
180
+ if (selectedLogIds.length !== 2) return null;
181
+ const [idA, idB] = selectedLogIds;
182
+ if (idA === undefined || idB === undefined) return null;
183
+ const logA = logs.find((l) => l.id === idA);
184
+ const logB = logs.find((l) => l.id === idB);
185
+ if (logA === undefined || logB === undefined) return null;
186
+ const sameSession = getConversationId(logA) === getConversationId(logB);
187
+ let elapsed = "";
188
+ if (logA.timestamp !== null && logB.timestamp !== null) {
189
+ const a = Date.parse(logA.timestamp);
190
+ const b = Date.parse(logB.timestamp);
191
+ if (!Number.isNaN(a) && !Number.isNaN(b)) {
192
+ const ms = Math.abs(b - a);
193
+ elapsed = formatElapsed(ms);
194
+ }
195
+ }
196
+ return {
197
+ logA,
198
+ logB,
199
+ sameSession,
200
+ elapsed,
201
+ };
202
+ }, [selectedLogIds, logs]);
203
+
204
+ function formatElapsed(ms: number): string {
205
+ if (ms < 1000) return `${ms}ms`;
206
+ const sec = Math.floor(ms / 1000);
207
+ if (sec < 60) return `${sec}s`;
208
+ const min = Math.floor(sec / 60);
209
+ if (min < 60) return `${min}m`;
210
+ const hr = Math.floor(min / 60);
211
+ return `${hr}h${min % 60}m`;
212
+ }
213
+
127
214
  const groups = useMemo(() => groupLogsByConversation(logs), [logs]);
128
215
  const cacheTrends = useMemo(() => computeCacheTrends(groups), [groups]);
129
216
 
@@ -310,6 +397,8 @@ export function ProxyViewer({
310
397
  viewMode={viewMode}
311
398
  strip={strip}
312
399
  cacheTrends={cacheTrends}
400
+ selectedSet={selectedSet}
401
+ onToggleSelect={handleToggleSelect}
313
402
  />
314
403
  </div>
315
404
  );
@@ -334,6 +423,8 @@ export function ProxyViewer({
334
423
  viewMode={viewMode}
335
424
  strip={strip}
336
425
  cacheTrend={cacheTrends.get(log.id) ?? null}
426
+ isSelected={selectedSet.has(log.id)}
427
+ onToggleSelect={handleToggleSelect}
337
428
  />
338
429
  </div>
339
430
  );
@@ -343,6 +434,39 @@ export function ProxyViewer({
343
434
  </div>
344
435
  )}
345
436
  </div>
437
+
438
+ {/* Floating action bar — shown only when 2 logs are selected. */}
439
+ {selectedSummary !== null && (
440
+ <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">
441
+ <GitCompareArrows className="size-4 text-amber-400 shrink-0" />
442
+ <span className="text-muted-foreground font-mono">
443
+ #{selectedSummary.logA.id} ↔ #{selectedSummary.logB.id}
444
+ {" · "}
445
+ {selectedSummary.sameSession ? "same session" : "different sessions"}
446
+ {selectedSummary.elapsed !== "" && ` · ${selectedSummary.elapsed} apart`}
447
+ </span>
448
+ <button
449
+ type="button"
450
+ onClick={clearSelection}
451
+ className="text-muted-foreground hover:text-foreground transition-colors cursor-pointer inline-flex items-center gap-1"
452
+ >
453
+ <X className="size-3" />
454
+ Clear
455
+ </button>
456
+ <button
457
+ type="button"
458
+ onClick={openCompare}
459
+ className="bg-amber-400 text-amber-950 hover:bg-amber-300 transition-colors px-3 py-1 rounded font-medium cursor-pointer"
460
+ >
461
+ Compare 2 logs
462
+ </button>
463
+ </div>
464
+ )}
465
+
466
+ {/* Compare drawer — sibling of the log list, not a route change. */}
467
+ {compareOpen && comparePair !== null && (
468
+ <CompareDrawer left={comparePair[0]} right={comparePair[1]} onClose={closeCompare} />
469
+ )}
346
470
  </div>
347
471
  );
348
472
  }