@tonyclaw/llm-inspector 1.14.6 → 1.14.8
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.
- package/.output/nitro.json +1 -1
- package/.output/public/assets/index-CdnotuLh.js +105 -0
- package/.output/public/assets/index-vP91146S.css +1 -0
- package/.output/public/assets/{main-Dp5657Eq.js → main-CJ4MreBr.js} +1 -1
- package/.output/server/_libs/lucide-react.mjs +87 -79
- package/.output/server/_libs/radix-ui__react-id.mjs +1 -1
- package/.output/server/_libs/radix-ui__react-tooltip.mjs +1 -1
- package/.output/server/_ssr/{index-D2yS8VvO.mjs → index-9uTJ4xYR.mjs} +813 -595
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{router-DCfjmmJu.mjs → router-BKnjB_zi.mjs} +2 -2
- package/.output/server/{_tanstack-start-manifest_v-DupqJc5d.mjs → _tanstack-start-manifest_v-IsglLVKy.mjs} +1 -1
- package/.output/server/index.mjs +26 -26
- package/package.json +1 -1
- package/src/components/ProxyViewer.tsx +114 -146
- package/src/components/providers/ImportWizardDialog.tsx +6 -0
- package/src/components/providers/ProviderCard.tsx +79 -26
- package/src/components/providers/ProviderForm.tsx +37 -22
- package/src/components/providers/ProvidersPanel.tsx +118 -58
- package/src/components/providers/SettingsDialog.tsx +25 -15
- package/src/components/proxy-viewer/ConversationGroup.tsx +50 -10
- package/src/components/proxy-viewer/ConversationHeader.tsx +48 -2
- package/src/components/proxy-viewer/LogEntry.tsx +116 -45
- package/src/components/proxy-viewer/LogEntryHeader.tsx +89 -71
- package/src/components/proxy-viewer/ReplayDialog.tsx +16 -6
- package/src/components/proxy-viewer/StreamingChunkSequence.tsx +24 -16
- package/src/components/proxy-viewer/ThreadConnector.tsx +104 -0
- package/src/components/proxy-viewer/index.ts +2 -1
- package/src/components/ui/confirm-dialog.tsx +51 -0
- package/src/lib/stopReason.ts +57 -0
- package/.output/public/assets/index-BFNoWwFI.css +0 -1
- package/.output/public/assets/index-LH-YtFEM.js +0 -105
|
@@ -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-
|
|
201
|
+
const { tsrStartManifest } = await import("../_tanstack-start-manifest_v-IsglLVKy.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-
|
|
770
|
+
const routerEntry = await import("./router-BKnjB_zi.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-
|
|
48
|
+
const appCss = "/assets/index-vP91146S.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-
|
|
72
|
+
const $$splitComponentImporter = () => import("./index-9uTJ4xYR.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-
|
|
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-CJ4MreBr.js"], "assets": [] }, "/": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/index.tsx", "assets": [], "preloads": ["/assets/index-CdnotuLh.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-CJ4MreBr.js" });
|
|
2
2
|
export {
|
|
3
3
|
tsrStartManifest
|
|
4
4
|
};
|
package/.output/server/index.mjs
CHANGED
|
@@ -35,54 +35,54 @@ const headers = ((m) => function headersRouteRule(event) {
|
|
|
35
35
|
}
|
|
36
36
|
});
|
|
37
37
|
const assets = {
|
|
38
|
-
"/assets/minimax-BPMzvuL-.jpeg": {
|
|
39
|
-
"type": "image/jpeg",
|
|
40
|
-
"etag": '"1b06-IwivU89ko5UTMUM1/t7hn4sQK9A"',
|
|
41
|
-
"mtime": "2026-06-11T06:43:41.004Z",
|
|
42
|
-
"size": 6918,
|
|
43
|
-
"path": "../public/assets/minimax-BPMzvuL-.jpeg"
|
|
44
|
-
},
|
|
45
38
|
"/assets/alibaba-TTwafVwX.svg": {
|
|
46
39
|
"type": "image/svg+xml",
|
|
47
40
|
"etag": '"171b-6dyV5K8QjiaY35sN9qNprh9zDIs"',
|
|
48
|
-
"mtime": "2026-06-
|
|
41
|
+
"mtime": "2026-06-11T12:14:10.996Z",
|
|
49
42
|
"size": 5915,
|
|
50
43
|
"path": "../public/assets/alibaba-TTwafVwX.svg"
|
|
51
44
|
},
|
|
45
|
+
"/assets/index-vP91146S.css": {
|
|
46
|
+
"type": "text/css; charset=utf-8",
|
|
47
|
+
"etag": '"14a58-Ts4ulNygZtoNVfw4qwf2tNfAXoU"',
|
|
48
|
+
"mtime": "2026-06-11T12:14:10.997Z",
|
|
49
|
+
"size": 84568,
|
|
50
|
+
"path": "../public/assets/index-vP91146S.css"
|
|
51
|
+
},
|
|
52
52
|
"/assets/zhipuai-BPNAnxo-.svg": {
|
|
53
53
|
"type": "image/svg+xml",
|
|
54
54
|
"etag": '"2bf8-hNaLCTi89nOFCsIIfWpP/jrfo0s"',
|
|
55
|
-
"mtime": "2026-06-
|
|
55
|
+
"mtime": "2026-06-11T12:14:10.996Z",
|
|
56
56
|
"size": 11256,
|
|
57
57
|
"path": "../public/assets/zhipuai-BPNAnxo-.svg"
|
|
58
58
|
},
|
|
59
|
-
"/assets/
|
|
59
|
+
"/assets/minimax-BPMzvuL-.jpeg": {
|
|
60
|
+
"type": "image/jpeg",
|
|
61
|
+
"etag": '"1b06-IwivU89ko5UTMUM1/t7hn4sQK9A"',
|
|
62
|
+
"mtime": "2026-06-11T12:14:10.996Z",
|
|
63
|
+
"size": 6918,
|
|
64
|
+
"path": "../public/assets/minimax-BPMzvuL-.jpeg"
|
|
65
|
+
},
|
|
66
|
+
"/assets/main-CJ4MreBr.js": {
|
|
60
67
|
"type": "text/javascript; charset=utf-8",
|
|
61
|
-
"etag": '"50599-
|
|
62
|
-
"mtime": "2026-06-
|
|
68
|
+
"etag": '"50599-xS7/i8WCDvqJMx9qZW3sZ2pNsvE"',
|
|
69
|
+
"mtime": "2026-06-11T12:14:10.997Z",
|
|
63
70
|
"size": 329113,
|
|
64
|
-
"path": "../public/assets/main-
|
|
71
|
+
"path": "../public/assets/main-CJ4MreBr.js"
|
|
65
72
|
},
|
|
66
73
|
"/assets/qwen-CONDcHqt.png": {
|
|
67
74
|
"type": "image/png",
|
|
68
75
|
"etag": '"572c3-cdJAPaHdOvFCGzuaQjagdgOu6XE"',
|
|
69
|
-
"mtime": "2026-06-
|
|
76
|
+
"mtime": "2026-06-11T12:14:10.996Z",
|
|
70
77
|
"size": 357059,
|
|
71
78
|
"path": "../public/assets/qwen-CONDcHqt.png"
|
|
72
79
|
},
|
|
73
|
-
"/assets/index-
|
|
74
|
-
"type": "text/css; charset=utf-8",
|
|
75
|
-
"etag": '"144e4-grvzhe/fAjYnQnpkCdqNtv4X0xU"',
|
|
76
|
-
"mtime": "2026-06-11T06:43:41.004Z",
|
|
77
|
-
"size": 83172,
|
|
78
|
-
"path": "../public/assets/index-BFNoWwFI.css"
|
|
79
|
-
},
|
|
80
|
-
"/assets/index-LH-YtFEM.js": {
|
|
80
|
+
"/assets/index-CdnotuLh.js": {
|
|
81
81
|
"type": "text/javascript; charset=utf-8",
|
|
82
|
-
"etag": '"
|
|
83
|
-
"mtime": "2026-06-
|
|
84
|
-
"size":
|
|
85
|
-
"path": "../public/assets/index-
|
|
82
|
+
"etag": '"95a74-8hHRRtv/cPIlS0z++Wpqpv18rpY"',
|
|
83
|
+
"mtime": "2026-06-11T12:14:10.999Z",
|
|
84
|
+
"size": 612980,
|
|
85
|
+
"path": "../public/assets/index-CdnotuLh.js"
|
|
86
86
|
}
|
|
87
87
|
};
|
|
88
88
|
function readAsset(id) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type JSX, useCallback, useEffect, useMemo, useState, useRef } from "react";
|
|
2
2
|
import { useVirtualizer } from "@tanstack/react-virtual";
|
|
3
|
-
import { Download,
|
|
3
|
+
import { Download, GitBranch, LayoutGrid, List } from "lucide-react";
|
|
4
|
+
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "./ui/tooltip";
|
|
4
5
|
import type { CapturedLog } from "../proxy/schemas";
|
|
5
6
|
import { exportLogsAsZip } from "../lib/export-logs";
|
|
6
7
|
import packageJson from "../../package.json";
|
|
@@ -8,14 +9,16 @@ import {
|
|
|
8
9
|
ConversationGroup,
|
|
9
10
|
groupLogsByConversation,
|
|
10
11
|
LogEntry,
|
|
12
|
+
ThreadConnector,
|
|
11
13
|
type ConversationGroupData,
|
|
14
|
+
type ViewMode,
|
|
12
15
|
} from "./proxy-viewer";
|
|
16
|
+
import { extractStopReason } from "../lib/stopReason";
|
|
13
17
|
import { CrabLogo } from "./ui/crab-logo";
|
|
14
18
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
|
|
15
19
|
import { SettingsDialog } from "./providers/SettingsDialog";
|
|
16
20
|
import { computeCacheTrends } from "./proxy-viewer/cacheTrend";
|
|
17
21
|
import { CompareDrawer } from "./proxy-viewer/CompareDrawer";
|
|
18
|
-
import { getConversationId } from "./proxy-viewer/ConversationHeader";
|
|
19
22
|
|
|
20
23
|
function truncateSessionId(id: string): string {
|
|
21
24
|
if (id.length <= 30) return id;
|
|
@@ -114,9 +117,8 @@ export function ProxyViewer({
|
|
|
114
117
|
}: ProxyViewerProps): JSX.Element {
|
|
115
118
|
const { totalIn, totalOut } = computeTokenSummary(logs);
|
|
116
119
|
const [groupedView, setGroupedView] = useState(true);
|
|
120
|
+
const [groupViewMode, setGroupViewMode] = useState<ViewMode>("thread");
|
|
117
121
|
const [exporting, setExporting] = useState(false);
|
|
118
|
-
const [selectedLogIds, setSelectedLogIds] = useState<number[]>([]);
|
|
119
|
-
const [compareOpen, setCompareOpen] = useState(false);
|
|
120
122
|
const [comparePair, setComparePair] = useState<[CapturedLog, CapturedLog] | null>(null);
|
|
121
123
|
|
|
122
124
|
const handleExport = useCallback(async () => {
|
|
@@ -129,96 +131,33 @@ export function ProxyViewer({
|
|
|
129
131
|
}, [logs]);
|
|
130
132
|
const parentRef = useRef<HTMLDivElement>(null);
|
|
131
133
|
|
|
132
|
-
|
|
133
|
-
|
|
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.
|
|
134
|
+
// Close the compare drawer when the user changes the session or model
|
|
135
|
+
// filter, since the predecessor relationship may no longer be meaningful.
|
|
150
136
|
useEffect(() => {
|
|
151
|
-
|
|
152
|
-
setCompareOpen(false);
|
|
137
|
+
setComparePair(null);
|
|
153
138
|
}, [selectedSession, selectedModel]);
|
|
154
139
|
|
|
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
140
|
const closeCompare = useCallback(() => {
|
|
169
|
-
|
|
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([]);
|
|
141
|
+
setComparePair(null);
|
|
177
142
|
}, []);
|
|
178
143
|
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
}
|
|
144
|
+
const handleCompareWithPrevious = useCallback(
|
|
145
|
+
(log: CapturedLog) => {
|
|
146
|
+
const idx = logs.indexOf(log);
|
|
147
|
+
if (idx <= 0) return;
|
|
148
|
+
const predecessor = logs[idx - 1];
|
|
149
|
+
if (predecessor === undefined) return;
|
|
150
|
+
setComparePair([predecessor, log]);
|
|
151
|
+
},
|
|
152
|
+
[logs],
|
|
153
|
+
);
|
|
213
154
|
|
|
214
155
|
const groups = useMemo(() => groupLogsByConversation(logs), [logs]);
|
|
215
156
|
const cacheTrends = useMemo(() => computeCacheTrends(groups), [groups]);
|
|
157
|
+
const stopReasons = useMemo(() => logs.map((log) => extractStopReason(log)), [logs]);
|
|
216
158
|
|
|
217
159
|
// Determine what items to render (groups or individual logs)
|
|
218
|
-
const renderGroups =
|
|
219
|
-
logs.length > 0 &&
|
|
220
|
-
groupedView &&
|
|
221
|
-
!(groups.length === 1 && groups[0]?.logs.length === logs.length);
|
|
160
|
+
const renderGroups = logs.length > 0 && groupedView && groups.length > 1;
|
|
222
161
|
|
|
223
162
|
const rowVirtualizer = useVirtualizer({
|
|
224
163
|
count: renderGroups ? groups.length : logs.length,
|
|
@@ -242,30 +181,39 @@ export function ProxyViewer({
|
|
|
242
181
|
<span className="text-xs text-muted-foreground font-mono">v{packageJson.version}</span>
|
|
243
182
|
</span>
|
|
244
183
|
</h1>
|
|
245
|
-
<
|
|
246
|
-
<
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
184
|
+
<TooltipProvider>
|
|
185
|
+
<Tooltip>
|
|
186
|
+
<TooltipTrigger asChild>
|
|
187
|
+
<div className="flex items-center border border-border rounded-md overflow-hidden">
|
|
188
|
+
<button
|
|
189
|
+
type="button"
|
|
190
|
+
onClick={() => onViewModeChange("simple")}
|
|
191
|
+
className={`px-2 py-1 cursor-pointer transition-colors text-xs ${
|
|
192
|
+
viewMode === "simple"
|
|
193
|
+
? "bg-muted text-foreground"
|
|
194
|
+
: "text-muted-foreground hover:bg-muted/50"
|
|
195
|
+
}`}
|
|
196
|
+
>
|
|
197
|
+
Simple
|
|
198
|
+
</button>
|
|
199
|
+
<button
|
|
200
|
+
type="button"
|
|
201
|
+
onClick={() => onViewModeChange("full")}
|
|
202
|
+
className={`px-2 py-1 cursor-pointer transition-colors text-xs ${
|
|
203
|
+
viewMode === "full"
|
|
204
|
+
? "bg-muted text-foreground"
|
|
205
|
+
: "text-muted-foreground hover:bg-muted/50"
|
|
206
|
+
}`}
|
|
207
|
+
>
|
|
208
|
+
Full
|
|
209
|
+
</button>
|
|
210
|
+
</div>
|
|
211
|
+
</TooltipTrigger>
|
|
212
|
+
<TooltipContent>
|
|
213
|
+
Simple shows parsed output; Full adds raw headers and tokens
|
|
214
|
+
</TooltipContent>
|
|
215
|
+
</Tooltip>
|
|
216
|
+
</TooltipProvider>
|
|
269
217
|
<SettingsDialog />
|
|
270
218
|
<span className="text-muted-foreground text-xs font-mono">
|
|
271
219
|
{logs.length} request{logs.length !== 1 ? "s" : ""}
|
|
@@ -356,6 +304,34 @@ export function ProxyViewer({
|
|
|
356
304
|
<List className="size-4" />
|
|
357
305
|
</button>
|
|
358
306
|
</div>
|
|
307
|
+
|
|
308
|
+
{/* Thread/flat mode toggle */}
|
|
309
|
+
<div className="flex items-center border border-border rounded-md overflow-hidden">
|
|
310
|
+
<button
|
|
311
|
+
type="button"
|
|
312
|
+
onClick={() => setGroupViewMode("thread")}
|
|
313
|
+
className={`px-2 py-1.5 cursor-pointer transition-colors ${
|
|
314
|
+
groupViewMode === "thread"
|
|
315
|
+
? "bg-amber-500/15 text-amber-400 border-r border-amber-500/30"
|
|
316
|
+
: "text-muted-foreground hover:bg-muted/50"
|
|
317
|
+
}`}
|
|
318
|
+
title="Thread view (connected timeline)"
|
|
319
|
+
>
|
|
320
|
+
<GitBranch className="size-4" />
|
|
321
|
+
</button>
|
|
322
|
+
<button
|
|
323
|
+
type="button"
|
|
324
|
+
onClick={() => setGroupViewMode("flat")}
|
|
325
|
+
className={`px-2 py-1.5 cursor-pointer transition-colors ${
|
|
326
|
+
groupViewMode === "flat"
|
|
327
|
+
? "bg-muted text-foreground"
|
|
328
|
+
: "text-muted-foreground hover:bg-muted/50"
|
|
329
|
+
}`}
|
|
330
|
+
title="Flat view (card list)"
|
|
331
|
+
>
|
|
332
|
+
<List className="size-4" />
|
|
333
|
+
</button>
|
|
334
|
+
</div>
|
|
359
335
|
</div>
|
|
360
336
|
|
|
361
337
|
{/* Log list */}
|
|
@@ -397,14 +373,15 @@ export function ProxyViewer({
|
|
|
397
373
|
viewMode={viewMode}
|
|
398
374
|
strip={strip}
|
|
399
375
|
cacheTrends={cacheTrends}
|
|
400
|
-
|
|
401
|
-
|
|
376
|
+
onCompareWithPrevious={handleCompareWithPrevious}
|
|
377
|
+
defaultGroupViewMode={groupViewMode}
|
|
402
378
|
/>
|
|
403
379
|
</div>
|
|
404
380
|
);
|
|
405
381
|
} else {
|
|
406
382
|
const log = logs[virtualRow.index];
|
|
407
383
|
if (log === undefined) return null;
|
|
384
|
+
const idx = virtualRow.index;
|
|
408
385
|
return (
|
|
409
386
|
<div
|
|
410
387
|
key={log.id}
|
|
@@ -418,14 +395,33 @@ export function ProxyViewer({
|
|
|
418
395
|
transform: `translateY(${virtualRow.start}px)`,
|
|
419
396
|
}}
|
|
420
397
|
>
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
398
|
+
{groupViewMode === "thread" ? (
|
|
399
|
+
<div className="flex items-stretch ml-3">
|
|
400
|
+
<ThreadConnector
|
|
401
|
+
stopReason={stopReasons[idx] ?? null}
|
|
402
|
+
isPending={log.responseStatus === null}
|
|
403
|
+
isFirst={idx === 0}
|
|
404
|
+
isLast={idx === logs.length - 1}
|
|
405
|
+
/>
|
|
406
|
+
<div className="flex-1 min-w-0 mb-2">
|
|
407
|
+
<LogEntry
|
|
408
|
+
log={log}
|
|
409
|
+
viewMode={viewMode}
|
|
410
|
+
strip={strip}
|
|
411
|
+
cacheTrend={cacheTrends.get(log.id) ?? null}
|
|
412
|
+
onCompareWithPrevious={() => handleCompareWithPrevious(log)}
|
|
413
|
+
/>
|
|
414
|
+
</div>
|
|
415
|
+
</div>
|
|
416
|
+
) : (
|
|
417
|
+
<LogEntry
|
|
418
|
+
log={log}
|
|
419
|
+
viewMode={viewMode}
|
|
420
|
+
strip={strip}
|
|
421
|
+
cacheTrend={cacheTrends.get(log.id) ?? null}
|
|
422
|
+
onCompareWithPrevious={() => handleCompareWithPrevious(log)}
|
|
423
|
+
/>
|
|
424
|
+
)}
|
|
429
425
|
</div>
|
|
430
426
|
);
|
|
431
427
|
}
|
|
@@ -435,36 +431,8 @@ export function ProxyViewer({
|
|
|
435
431
|
)}
|
|
436
432
|
</div>
|
|
437
433
|
|
|
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
434
|
{/* Compare drawer — sibling of the log list, not a route change. */}
|
|
467
|
-
{
|
|
435
|
+
{comparePair !== null && (
|
|
468
436
|
<CompareDrawer left={comparePair[0]} right={comparePair[1]} onClose={closeCompare} />
|
|
469
437
|
)}
|
|
470
438
|
</div>
|
|
@@ -180,6 +180,12 @@ export function ImportWizardDialog({
|
|
|
180
180
|
return;
|
|
181
181
|
}
|
|
182
182
|
const result = parsed.data;
|
|
183
|
+
// Mark imported providers as already existing so they can't be re-selected
|
|
184
|
+
setProviders((prev) =>
|
|
185
|
+
prev.map((p, i) => (selected.has(i) ? { ...p, alreadyExists: true } : p)),
|
|
186
|
+
);
|
|
187
|
+
setSelected(new Set());
|
|
188
|
+
|
|
183
189
|
if (
|
|
184
190
|
result.errors !== undefined &&
|
|
185
191
|
result.errors.length > 0 &&
|