@tonyclaw/llm-inspector 1.11.8 → 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-DkqH07iU.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-DeW8M_70.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-DiYqfnSp.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-RTuHuCHN.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-GI0Ezr88.js"], "assets": [] }, "/": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/index.tsx", "assets": [], "preloads": ["/assets/index-KwQ0pWoC.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-GI0Ezr88.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
  };
@@ -35,54 +35,54 @@ const headers = ((m) => function headersRouteRule(event) {
35
35
  }
36
36
  });
37
37
  const assets = {
38
- "/assets/index-DiYqfnSp.css": {
39
- "type": "text/css; charset=utf-8",
40
- "etag": '"11755-g+paI7gxmWkWiKXnB74OdQXg0jE"',
41
- "mtime": "2026-06-08T11:21:47.160Z",
42
- "size": 71509,
43
- "path": "../public/assets/index-DiYqfnSp.css"
44
- },
45
- "/assets/minimax-BPMzvuL-.jpeg": {
46
- "type": "image/jpeg",
47
- "etag": '"1b06-IwivU89ko5UTMUM1/t7hn4sQK9A"',
48
- "mtime": "2026-06-08T11:21:47.159Z",
49
- "size": 6918,
50
- "path": "../public/assets/minimax-BPMzvuL-.jpeg"
38
+ "/assets/alibaba-TTwafVwX.svg": {
39
+ "type": "image/svg+xml",
40
+ "etag": '"171b-6dyV5K8QjiaY35sN9qNprh9zDIs"',
41
+ "mtime": "2026-06-09T09:07:57.962Z",
42
+ "size": 5915,
43
+ "path": "../public/assets/alibaba-TTwafVwX.svg"
51
44
  },
52
45
  "/assets/zhipuai-BPNAnxo-.svg": {
53
46
  "type": "image/svg+xml",
54
47
  "etag": '"2bf8-hNaLCTi89nOFCsIIfWpP/jrfo0s"',
55
- "mtime": "2026-06-08T11:21:47.160Z",
48
+ "mtime": "2026-06-09T09:07:57.965Z",
56
49
  "size": 11256,
57
50
  "path": "../public/assets/zhipuai-BPNAnxo-.svg"
58
51
  },
59
- "/assets/main-GI0Ezr88.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": {
60
60
  "type": "text/javascript; charset=utf-8",
61
- "etag": '"50599-QWdqtG8xhv3YNUdqpICG85M3U+c"',
62
- "mtime": "2026-06-08T11:21:47.160Z",
61
+ "etag": '"50599-nkxkxzEGV0G+Y8N4Lyzxy/ivv4w"',
62
+ "mtime": "2026-06-09T09:07:57.965Z",
63
63
  "size": 329113,
64
- "path": "../public/assets/main-GI0Ezr88.js"
64
+ "path": "../public/assets/main-C3tLo75s.js"
65
65
  },
66
66
  "/assets/qwen-CONDcHqt.png": {
67
67
  "type": "image/png",
68
68
  "etag": '"572c3-cdJAPaHdOvFCGzuaQjagdgOu6XE"',
69
- "mtime": "2026-06-08T11:21:47.159Z",
69
+ "mtime": "2026-06-09T09:07:57.965Z",
70
70
  "size": 357059,
71
71
  "path": "../public/assets/qwen-CONDcHqt.png"
72
72
  },
73
- "/assets/alibaba-TTwafVwX.svg": {
74
- "type": "image/svg+xml",
75
- "etag": '"171b-6dyV5K8QjiaY35sN9qNprh9zDIs"',
76
- "mtime": "2026-06-08T11:21:47.159Z",
77
- "size": 5915,
78
- "path": "../public/assets/alibaba-TTwafVwX.svg"
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
79
  },
80
- "/assets/index-KwQ0pWoC.js": {
80
+ "/assets/index-H_thmL2_.js": {
81
81
  "type": "text/javascript; charset=utf-8",
82
- "etag": '"8a3c6-FXAKLjOLw88ZPk0YxXa9P2/jGDE"',
83
- "mtime": "2026-06-08T11:21:47.160Z",
84
- "size": 566214,
85
- "path": "../public/assets/index-KwQ0pWoC.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.11.8",
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";
@@ -13,6 +13,9 @@ import {
13
13
  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
+ import { computeCacheTrends } from "./proxy-viewer/cacheTrend";
17
+ import { CompareDrawer } from "./proxy-viewer/CompareDrawer";
18
+ import { getConversationId } from "./proxy-viewer/ConversationHeader";
16
19
 
17
20
  function truncateSessionId(id: string): string {
18
21
  if (id.length <= 30) return id;
@@ -112,6 +115,9 @@ export function ProxyViewer({
112
115
  const { totalIn, totalOut } = computeTokenSummary(logs);
113
116
  const [groupedView, setGroupedView] = useState(true);
114
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);
115
121
 
116
122
  const handleExport = useCallback(async () => {
117
123
  setExporting(true);
@@ -123,7 +129,90 @@ export function ProxyViewer({
123
129
  }, [logs]);
124
130
  const parentRef = useRef<HTMLDivElement>(null);
125
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
+
126
214
  const groups = useMemo(() => groupLogsByConversation(logs), [logs]);
215
+ const cacheTrends = useMemo(() => computeCacheTrends(groups), [groups]);
127
216
 
128
217
  // Determine what items to render (groups or individual logs)
129
218
  const renderGroups =
@@ -303,7 +392,14 @@ export function ProxyViewer({
303
392
  transform: `translateY(${virtualRow.start}px)`,
304
393
  }}
305
394
  >
306
- <ConversationGroup group={group} viewMode={viewMode} strip={strip} />
395
+ <ConversationGroup
396
+ group={group}
397
+ viewMode={viewMode}
398
+ strip={strip}
399
+ cacheTrends={cacheTrends}
400
+ selectedSet={selectedSet}
401
+ onToggleSelect={handleToggleSelect}
402
+ />
307
403
  </div>
308
404
  );
309
405
  } else {
@@ -322,7 +418,14 @@ export function ProxyViewer({
322
418
  transform: `translateY(${virtualRow.start}px)`,
323
419
  }}
324
420
  >
325
- <LogEntry log={log} viewMode={viewMode} strip={strip} />
421
+ <LogEntry
422
+ log={log}
423
+ viewMode={viewMode}
424
+ strip={strip}
425
+ cacheTrend={cacheTrends.get(log.id) ?? null}
426
+ isSelected={selectedSet.has(log.id)}
427
+ onToggleSelect={handleToggleSelect}
428
+ />
326
429
  </div>
327
430
  );
328
431
  }
@@ -331,6 +434,39 @@ export function ProxyViewer({
331
434
  </div>
332
435
  )}
333
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
+ )}
334
470
  </div>
335
471
  );
336
472
  }