@tonyclaw/llm-inspector 1.16.4 → 1.17.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.
- package/.output/nitro.json +1 -1
- package/.output/public/assets/CompareDrawer-C4fie5g5.js +1 -0
- package/.output/public/assets/ReplayDialog-Dme5uOR9.js +1 -0
- package/.output/public/assets/RequestAnatomy-ChBLDNFH.js +1 -0
- package/.output/public/assets/ResponseView-wGeqBzVU.js +1 -0
- package/.output/public/assets/StreamingChunkSequence-zeJZQLqT.js +1 -0
- package/.output/public/assets/index-DoGvsnbA.css +1 -0
- package/.output/public/assets/index-DpbutOvo.js +101 -0
- package/.output/public/assets/json-viewer-BV-WUszW.js +14 -0
- package/.output/public/assets/{main-DbWwVQFh.js → main-DRu10KNQ.js} +1 -1
- package/.output/server/_libs/lucide-react.mjs +105 -85
- package/.output/server/_ssr/CompareDrawer-C4-CQL5w.mjs +1040 -0
- package/.output/server/_ssr/ReplayDialog-BTb1Bam8.mjs +321 -0
- package/.output/server/_ssr/RequestAnatomy-CZFV1IvL.mjs +351 -0
- package/.output/server/_ssr/ResponseView-CTZekh65.mjs +601 -0
- package/.output/server/_ssr/StreamingChunkSequence-C38Ynabd.mjs +301 -0
- package/.output/server/_ssr/{index-C-z-fZtq.mjs → index-Cnu-QzAy.mjs} +1141 -2443
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/json-viewer-DROqpjS9.mjs +510 -0
- package/.output/server/_ssr/{router-CNM9Kbi0.mjs → router-pP4GCTQx.mjs} +42 -18
- package/.output/server/{_tanstack-start-manifest_v-BWfLeIsC.mjs → _tanstack-start-manifest_v-CphS4rZd.mjs} +1 -1
- package/.output/server/index.mjs +69 -27
- package/package.json +1 -1
- package/src/components/OnboardingBanner.tsx +2 -2
- package/src/components/ProxyViewer.tsx +44 -27
- package/src/components/ProxyViewerContainer.tsx +5 -25
- package/src/components/providers/SettingsDialog.tsx +52 -1
- package/src/components/proxy-viewer/ConversationGroup.tsx +5 -1
- package/src/components/proxy-viewer/ConversationHeader.tsx +4 -1
- package/src/components/proxy-viewer/LogEntry.tsx +217 -181
- package/src/components/proxy-viewer/LogEntryHeader.tsx +181 -40
- package/src/components/proxy-viewer/ThreadConnector.tsx +17 -2
- package/src/components/proxy-viewer/TurnGroup.tsx +124 -72
- package/src/components/proxy-viewer/anatomy/RequestAnatomy.tsx +98 -0
- package/src/components/proxy-viewer/anatomy/SegmentBar.tsx +196 -0
- package/src/components/proxy-viewer/anatomy/tokenEstimate.ts +53 -0
- package/src/components/proxy-viewer/anatomy/types.ts +39 -0
- package/src/components/proxy-viewer/anatomy/useAnatomyJump.ts +114 -0
- package/src/components/proxy-viewer/formats/anthropic/ContentBlocks.tsx +3 -23
- package/src/components/proxy-viewer/formats/anthropic/thinkingExtract.ts +21 -0
- package/src/components/proxy-viewer/formats/openai/ResponseView.tsx +5 -3
- package/src/components/proxy-viewer/lazy.ts +37 -0
- package/src/components/proxy-viewer/log-formats/anthropic.ts +146 -0
- package/src/components/proxy-viewer/log-formats/openai.ts +127 -0
- package/src/components/proxy-viewer/log-formats/types.ts +7 -0
- package/src/components/proxy-viewer/log-formats/unknown.ts +4 -0
- package/src/components/proxy-viewer/logEntryVisibility.ts +39 -0
- package/src/components/proxy-viewer/useKeyboardNavigation.ts +190 -0
- package/src/components/proxy-viewer/viewerState.ts +8 -0
- package/src/components/ui/crab-variants.tsx +11 -0
- package/src/components/ui/json-expansion-button.tsx +56 -0
- package/src/components/ui/json-viewer-bulk.ts +97 -0
- package/src/components/ui/json-viewer.tsx +58 -183
- package/src/lib/runtimeConfig.ts +9 -0
- package/src/lib/useOnboarding.ts +7 -1
- package/src/lib/useStripConfig.ts +33 -2
- package/src/lib/utils.ts +2 -3
- package/src/proxy/config.ts +17 -7
- package/src/routes/api/config.ts +7 -0
- package/src/routes/api/logs.stream.ts +26 -16
- package/.output/public/assets/index-DRRCmu5p.css +0 -1
- package/.output/public/assets/index-X7CHS7fS.js +0 -107
|
@@ -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-DRu10KNQ.js"], "assets": [] }, "/": { "filePath": "C:/Users/claw/workspace/llm-inspector/src/routes/index.tsx", "assets": [], "preloads": ["/assets/index-DpbutOvo.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-DRu10KNQ.js" });
|
|
2
2
|
export {
|
|
3
3
|
tsrStartManifest
|
|
4
4
|
};
|
package/.output/server/index.mjs
CHANGED
|
@@ -35,54 +35,96 @@ 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-15T02:31:02.872Z",
|
|
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-16T13:43:26.730Z",
|
|
49
42
|
"size": 5915,
|
|
50
43
|
"path": "../public/assets/alibaba-TTwafVwX.svg"
|
|
51
44
|
},
|
|
52
|
-
"/assets/
|
|
45
|
+
"/assets/CompareDrawer-C4fie5g5.js": {
|
|
46
|
+
"type": "text/javascript; charset=utf-8",
|
|
47
|
+
"etag": '"4a10-3djWuOwKSKXlQPoy4NHzqd2tggE"',
|
|
48
|
+
"mtime": "2026-06-16T13:43:26.730Z",
|
|
49
|
+
"size": 18960,
|
|
50
|
+
"path": "../public/assets/CompareDrawer-C4fie5g5.js"
|
|
51
|
+
},
|
|
52
|
+
"/assets/index-DoGvsnbA.css": {
|
|
53
53
|
"type": "text/css; charset=utf-8",
|
|
54
|
-
"etag": '"
|
|
55
|
-
"mtime": "2026-06-
|
|
56
|
-
"size":
|
|
57
|
-
"path": "../public/assets/index-
|
|
54
|
+
"etag": '"16d26-qw65JIM4oxztXa/jhWYD9PPuvfA"',
|
|
55
|
+
"mtime": "2026-06-16T13:43:26.730Z",
|
|
56
|
+
"size": 93478,
|
|
57
|
+
"path": "../public/assets/index-DoGvsnbA.css"
|
|
58
|
+
},
|
|
59
|
+
"/assets/index-DpbutOvo.js": {
|
|
60
|
+
"type": "text/javascript; charset=utf-8",
|
|
61
|
+
"etag": '"744e2-P9RhN2wY9pxyu6b/wpeHlefe59k"',
|
|
62
|
+
"mtime": "2026-06-16T13:43:26.731Z",
|
|
63
|
+
"size": 476386,
|
|
64
|
+
"path": "../public/assets/index-DpbutOvo.js"
|
|
65
|
+
},
|
|
66
|
+
"/assets/ReplayDialog-Dme5uOR9.js": {
|
|
67
|
+
"type": "text/javascript; charset=utf-8",
|
|
68
|
+
"etag": '"11b1-SKRfqUa9SY8yNi8IxWzCC6VvjQI"',
|
|
69
|
+
"mtime": "2026-06-16T13:43:26.730Z",
|
|
70
|
+
"size": 4529,
|
|
71
|
+
"path": "../public/assets/ReplayDialog-Dme5uOR9.js"
|
|
72
|
+
},
|
|
73
|
+
"/assets/json-viewer-BV-WUszW.js": {
|
|
74
|
+
"type": "text/javascript; charset=utf-8",
|
|
75
|
+
"etag": '"1e60c-26TCWxphlokObc5HBPeY7NRlcXU"',
|
|
76
|
+
"mtime": "2026-06-16T13:43:26.731Z",
|
|
77
|
+
"size": 124428,
|
|
78
|
+
"path": "../public/assets/json-viewer-BV-WUszW.js"
|
|
58
79
|
},
|
|
59
|
-
"/assets/
|
|
80
|
+
"/assets/RequestAnatomy-ChBLDNFH.js": {
|
|
60
81
|
"type": "text/javascript; charset=utf-8",
|
|
61
|
-
"etag": '"
|
|
62
|
-
"mtime": "2026-06-
|
|
63
|
-
"size":
|
|
64
|
-
"path": "../public/assets/
|
|
82
|
+
"etag": '"141b-nxiiCAE0vs7SLaqcUISSgXq1IGo"',
|
|
83
|
+
"mtime": "2026-06-16T13:43:26.730Z",
|
|
84
|
+
"size": 5147,
|
|
85
|
+
"path": "../public/assets/RequestAnatomy-ChBLDNFH.js"
|
|
86
|
+
},
|
|
87
|
+
"/assets/minimax-BPMzvuL-.jpeg": {
|
|
88
|
+
"type": "image/jpeg",
|
|
89
|
+
"etag": '"1b06-IwivU89ko5UTMUM1/t7hn4sQK9A"',
|
|
90
|
+
"mtime": "2026-06-16T13:43:26.730Z",
|
|
91
|
+
"size": 6918,
|
|
92
|
+
"path": "../public/assets/minimax-BPMzvuL-.jpeg"
|
|
93
|
+
},
|
|
94
|
+
"/assets/main-DRu10KNQ.js": {
|
|
95
|
+
"type": "text/javascript; charset=utf-8",
|
|
96
|
+
"etag": '"505ae-BP/aj8/2FZWooG/jXqGZEmVfl3E"',
|
|
97
|
+
"mtime": "2026-06-16T13:43:26.730Z",
|
|
98
|
+
"size": 329134,
|
|
99
|
+
"path": "../public/assets/main-DRu10KNQ.js"
|
|
100
|
+
},
|
|
101
|
+
"/assets/StreamingChunkSequence-zeJZQLqT.js": {
|
|
102
|
+
"type": "text/javascript; charset=utf-8",
|
|
103
|
+
"etag": '"d72-nuJyJopptXOHt0oPHw//D9VSW+k"',
|
|
104
|
+
"mtime": "2026-06-16T13:43:26.731Z",
|
|
105
|
+
"size": 3442,
|
|
106
|
+
"path": "../public/assets/StreamingChunkSequence-zeJZQLqT.js"
|
|
107
|
+
},
|
|
108
|
+
"/assets/ResponseView-wGeqBzVU.js": {
|
|
109
|
+
"type": "text/javascript; charset=utf-8",
|
|
110
|
+
"etag": '"6e82-ZzZo1WT+DlE16b9M9AMC/410RcI"',
|
|
111
|
+
"mtime": "2026-06-16T13:43:26.731Z",
|
|
112
|
+
"size": 28290,
|
|
113
|
+
"path": "../public/assets/ResponseView-wGeqBzVU.js"
|
|
65
114
|
},
|
|
66
115
|
"/assets/zhipuai-BPNAnxo-.svg": {
|
|
67
116
|
"type": "image/svg+xml",
|
|
68
117
|
"etag": '"2bf8-hNaLCTi89nOFCsIIfWpP/jrfo0s"',
|
|
69
|
-
"mtime": "2026-06-
|
|
118
|
+
"mtime": "2026-06-16T13:43:26.730Z",
|
|
70
119
|
"size": 11256,
|
|
71
120
|
"path": "../public/assets/zhipuai-BPNAnxo-.svg"
|
|
72
121
|
},
|
|
73
122
|
"/assets/qwen-CONDcHqt.png": {
|
|
74
123
|
"type": "image/png",
|
|
75
124
|
"etag": '"572c3-cdJAPaHdOvFCGzuaQjagdgOu6XE"',
|
|
76
|
-
"mtime": "2026-06-
|
|
125
|
+
"mtime": "2026-06-16T13:43:26.730Z",
|
|
77
126
|
"size": 357059,
|
|
78
127
|
"path": "../public/assets/qwen-CONDcHqt.png"
|
|
79
|
-
},
|
|
80
|
-
"/assets/index-X7CHS7fS.js": {
|
|
81
|
-
"type": "text/javascript; charset=utf-8",
|
|
82
|
-
"etag": '"9c554-RW/7IDotWnzOK06cvwJGAOWPrdk"',
|
|
83
|
-
"mtime": "2026-06-15T02:31:02.875Z",
|
|
84
|
-
"size": 640340,
|
|
85
|
-
"path": "../public/assets/index-X7CHS7fS.js"
|
|
86
128
|
}
|
|
87
129
|
};
|
|
88
130
|
function readAsset(id) {
|
package/package.json
CHANGED
|
@@ -46,10 +46,10 @@ export function OnboardingBanner(): JSX.Element | null {
|
|
|
46
46
|
onClick={() => {
|
|
47
47
|
void markSeen();
|
|
48
48
|
}}
|
|
49
|
-
className="inline-flex items-center gap-1.5 text-xs
|
|
49
|
+
className="inline-flex items-center gap-1.5 text-xs h-8 px-3 rounded-md border border-amber-500/40 text-amber-700 dark:text-amber-300 hover:bg-amber-500/10 transition-colors shrink-0"
|
|
50
50
|
aria-label="Dismiss onboarding tip"
|
|
51
51
|
>
|
|
52
|
-
<Check className="size-3" />
|
|
52
|
+
<Check className="size-3.5" />
|
|
53
53
|
Got it
|
|
54
54
|
</button>
|
|
55
55
|
<button
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { type JSX, useCallback, useEffect, useMemo, useState } from "react";
|
|
1
|
+
import { type JSX, useCallback, useEffect, useMemo, useRef, useState, Suspense } from "react";
|
|
2
2
|
import { Download } from "lucide-react";
|
|
3
3
|
|
|
4
4
|
import type { CapturedLog } from "../proxy/schemas";
|
|
5
5
|
import { exportLogsAsZip } from "../lib/export-logs";
|
|
6
|
+
import { formatTokens } from "../lib/utils";
|
|
6
7
|
import packageJson from "../../package.json";
|
|
7
8
|
import { ConversationGroup, groupLogsByConversation } from "./proxy-viewer";
|
|
8
9
|
|
|
@@ -11,8 +12,9 @@ import { crabVariants } from "./ui/crab-variants";
|
|
|
11
12
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
|
|
12
13
|
import { SettingsDialog } from "./providers/SettingsDialog";
|
|
13
14
|
import { computeCacheTrends } from "./proxy-viewer/cacheTrend";
|
|
14
|
-
import {
|
|
15
|
+
import { LazyCompareDrawer } from "./proxy-viewer/lazy";
|
|
15
16
|
import { buildValidPredecessors } from "./proxy-viewer/viewerState";
|
|
17
|
+
import { useKeyboardNavigation } from "./proxy-viewer/useKeyboardNavigation";
|
|
16
18
|
|
|
17
19
|
function truncateSessionId(id: string): string {
|
|
18
20
|
if (id.length <= 30) return id;
|
|
@@ -97,6 +99,8 @@ export type ProxyViewerProps = {
|
|
|
97
99
|
onViewModeChange: (mode: "simple" | "full") => void;
|
|
98
100
|
/** Live strip-Claude-Code-billing-header flag, sourced once at the container. */
|
|
99
101
|
strip: boolean;
|
|
102
|
+
/** Slow-response threshold in seconds. `0` disables the warning indicator. */
|
|
103
|
+
slowResponseThresholdSeconds: number;
|
|
100
104
|
};
|
|
101
105
|
|
|
102
106
|
export function ProxyViewer({
|
|
@@ -112,6 +116,7 @@ export function ProxyViewer({
|
|
|
112
116
|
viewMode,
|
|
113
117
|
onViewModeChange,
|
|
114
118
|
strip,
|
|
119
|
+
slowResponseThresholdSeconds,
|
|
115
120
|
}: ProxyViewerProps): JSX.Element {
|
|
116
121
|
const { totalIn, totalOut } = useMemo(() => computeTokenSummary(logs), [logs]);
|
|
117
122
|
const [exporting, setExporting] = useState(false);
|
|
@@ -119,6 +124,9 @@ export function ProxyViewer({
|
|
|
119
124
|
const [crabEntrancePhase, setCrabEntrancePhase] = useState<"hidden" | "playing" | "done">(
|
|
120
125
|
"hidden",
|
|
121
126
|
);
|
|
127
|
+
const logListRef = useRef<HTMLDivElement>(null);
|
|
128
|
+
const logListWrapperRef = useRef<HTMLDivElement>(null);
|
|
129
|
+
useKeyboardNavigation(logListRef, logListWrapperRef);
|
|
122
130
|
|
|
123
131
|
useEffect(() => {
|
|
124
132
|
const perCrabDuration = 400;
|
|
@@ -162,9 +170,9 @@ export function ProxyViewer({
|
|
|
162
170
|
);
|
|
163
171
|
|
|
164
172
|
return (
|
|
165
|
-
<div className="max-w-[
|
|
173
|
+
<div className="max-w-[1400px] xl:max-w-[1600px] 2xl:max-w-[1800px] mx-auto px-6 pb-6">
|
|
166
174
|
{/* Brand row */}
|
|
167
|
-
<div className="flex items-end
|
|
175
|
+
<div className="flex items-end pt-6 pb-8 relative">
|
|
168
176
|
<h1 className="text-lg font-bold flex items-end gap-2 absolute left-1/2 -translate-x-1/2 whitespace-nowrap">
|
|
169
177
|
{/* Crab family — hover to animate together */}
|
|
170
178
|
<span className="flex items-end gap-1 group cursor-default" aria-hidden="true">
|
|
@@ -217,7 +225,7 @@ export function ProxyViewer({
|
|
|
217
225
|
</div>
|
|
218
226
|
|
|
219
227
|
{/* Controls + Filters */}
|
|
220
|
-
<div className="flex items-center gap-3
|
|
228
|
+
<div className="flex items-center gap-3 mb-4">
|
|
221
229
|
<Select value={selectedSession} onValueChange={onSessionChange}>
|
|
222
230
|
<SelectTrigger className="flex-1 max-w-[350px] text-xs">
|
|
223
231
|
<SelectValue placeholder="All sessions" />
|
|
@@ -248,7 +256,7 @@ export function ProxyViewer({
|
|
|
248
256
|
<button
|
|
249
257
|
type="button"
|
|
250
258
|
onClick={() => onViewModeChange("simple")}
|
|
251
|
-
className={`
|
|
259
|
+
className={`h-8 px-3 cursor-pointer transition-colors text-xs ${
|
|
252
260
|
viewMode === "simple"
|
|
253
261
|
? "bg-muted text-foreground"
|
|
254
262
|
: "text-muted-foreground hover:bg-muted/50"
|
|
@@ -259,7 +267,7 @@ export function ProxyViewer({
|
|
|
259
267
|
<button
|
|
260
268
|
type="button"
|
|
261
269
|
onClick={() => onViewModeChange("full")}
|
|
262
|
-
className={`
|
|
270
|
+
className={`h-8 px-3 cursor-pointer transition-colors text-xs ${
|
|
263
271
|
viewMode === "full"
|
|
264
272
|
? "bg-muted text-foreground"
|
|
265
273
|
: "text-muted-foreground hover:bg-muted/50"
|
|
@@ -272,7 +280,7 @@ export function ProxyViewer({
|
|
|
272
280
|
<span className="text-muted-foreground text-xs font-mono">
|
|
273
281
|
{logs.length} request{logs.length !== 1 ? "s" : ""}
|
|
274
282
|
{totalIn > 0 || totalOut > 0
|
|
275
|
-
? ` · ${totalIn
|
|
283
|
+
? ` · ${formatTokens(totalIn)} in / ${formatTokens(totalOut)} out`
|
|
276
284
|
: ""}
|
|
277
285
|
</span>
|
|
278
286
|
{logs.length > 0 && (
|
|
@@ -282,14 +290,14 @@ export function ProxyViewer({
|
|
|
282
290
|
void handleExport();
|
|
283
291
|
}}
|
|
284
292
|
disabled={exporting}
|
|
285
|
-
className="text-xs text-muted-foreground hover:text-foreground transition-colors cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed inline-flex items-center gap-1"
|
|
293
|
+
className="h-8 px-3 text-xs text-muted-foreground hover:text-foreground transition-colors cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed inline-flex items-center gap-1.5 rounded-md hover:bg-muted"
|
|
286
294
|
title="Export all logs as JSON ZIP"
|
|
287
295
|
>
|
|
288
296
|
{exporting ? (
|
|
289
297
|
<span>Exporting...</span>
|
|
290
298
|
) : (
|
|
291
299
|
<>
|
|
292
|
-
<Download className="size-3" />
|
|
300
|
+
<Download className="size-3.5" />
|
|
293
301
|
<span>Export</span>
|
|
294
302
|
</>
|
|
295
303
|
)}
|
|
@@ -298,7 +306,7 @@ export function ProxyViewer({
|
|
|
298
306
|
<button
|
|
299
307
|
type="button"
|
|
300
308
|
onClick={onClearAll}
|
|
301
|
-
className="text-xs text-muted-foreground hover:text-foreground transition-colors cursor-pointer"
|
|
309
|
+
className="h-8 px-3 text-xs text-muted-foreground hover:text-foreground transition-colors cursor-pointer rounded-md hover:bg-muted"
|
|
302
310
|
title="Clear all logs"
|
|
303
311
|
>
|
|
304
312
|
Clear
|
|
@@ -306,7 +314,7 @@ export function ProxyViewer({
|
|
|
306
314
|
</div>
|
|
307
315
|
|
|
308
316
|
{/* Log list */}
|
|
309
|
-
<div
|
|
317
|
+
<div>
|
|
310
318
|
{logs.length === 0 ? (
|
|
311
319
|
<div className="text-center text-muted-foreground py-16 space-y-4">
|
|
312
320
|
<p className="text-sm">No requests captured yet.</p>
|
|
@@ -314,27 +322,36 @@ export function ProxyViewer({
|
|
|
314
322
|
<CopyableCommand command="LLM_BASE_URL=http://localhost:25947/proxy <your-tool>" />
|
|
315
323
|
</div>
|
|
316
324
|
) : (
|
|
317
|
-
<div
|
|
318
|
-
{
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
325
|
+
<div
|
|
326
|
+
ref={logListWrapperRef}
|
|
327
|
+
tabIndex={0}
|
|
328
|
+
className="flex flex-col gap-2 focus:outline-none"
|
|
329
|
+
>
|
|
330
|
+
<div ref={logListRef}>
|
|
331
|
+
{groups.map((group) => (
|
|
332
|
+
<ConversationGroup
|
|
333
|
+
key={group.id}
|
|
334
|
+
group={group}
|
|
335
|
+
viewMode={viewMode}
|
|
336
|
+
strip={strip}
|
|
337
|
+
slowResponseThresholdSeconds={slowResponseThresholdSeconds}
|
|
338
|
+
cacheTrends={cacheTrends}
|
|
339
|
+
onCompareWithPrevious={handleCompareWithPrevious}
|
|
340
|
+
comparisonPredecessors={comparisonPredecessors}
|
|
341
|
+
onClearGroup={onClearGroup}
|
|
342
|
+
standalone={groups.length === 1}
|
|
343
|
+
/>
|
|
344
|
+
))}
|
|
345
|
+
</div>
|
|
331
346
|
</div>
|
|
332
347
|
)}
|
|
333
348
|
</div>
|
|
334
349
|
|
|
335
350
|
{/* Compare drawer — sibling of the log list, not a route change. */}
|
|
336
351
|
{comparePair !== null && (
|
|
337
|
-
<
|
|
352
|
+
<Suspense fallback={null}>
|
|
353
|
+
<LazyCompareDrawer left={comparePair[0]} right={comparePair[1]} onClose={closeCompare} />
|
|
354
|
+
</Suspense>
|
|
338
355
|
)}
|
|
339
356
|
</div>
|
|
340
357
|
);
|
|
@@ -67,8 +67,6 @@ export function ProxyViewerContainer(): JSX.Element {
|
|
|
67
67
|
// connection never re-opens on filter change — we always carry the full
|
|
68
68
|
// set and derive the displayed view with `useMemo` below.
|
|
69
69
|
const [allLogs, setAllLogs] = useState<CapturedLog[]>([]);
|
|
70
|
-
const [sessions, setSessions] = useState<string[]>([]);
|
|
71
|
-
const [models, setModels] = useState<string[]>([]);
|
|
72
70
|
const [selectedSession, setSelectedSession] = useState("__all__");
|
|
73
71
|
const [selectedModel, setSelectedModel] = useState("__all__");
|
|
74
72
|
const [viewMode, setViewMode] = useState<"simple" | "full">("simple");
|
|
@@ -78,7 +76,6 @@ export function ProxyViewerContainer(): JSX.Element {
|
|
|
78
76
|
|
|
79
77
|
// O(1) log lookup by id
|
|
80
78
|
const logIndexRef = useRef<Map<number, number>>(new Map());
|
|
81
|
-
const logsRef = useRef<CapturedLog[]>([]);
|
|
82
79
|
|
|
83
80
|
// Debounce buffer for SSE updates
|
|
84
81
|
const pendingUpdatesRef = useRef<CapturedLog[]>([]);
|
|
@@ -90,6 +87,8 @@ export function ProxyViewerContainer(): JSX.Element {
|
|
|
90
87
|
() => filterLogs(allLogs, selectedSession, selectedModel),
|
|
91
88
|
[allLogs, selectedSession, selectedModel],
|
|
92
89
|
);
|
|
90
|
+
const sessions = useMemo(() => extractSessions(allLogs), [allLogs]);
|
|
91
|
+
const models = useMemo(() => extractModels(allLogs), [allLogs]);
|
|
93
92
|
|
|
94
93
|
const flushUpdates = useCallback(() => {
|
|
95
94
|
flushTimerRef.current = null;
|
|
@@ -99,8 +98,6 @@ export function ProxyViewerContainer(): JSX.Element {
|
|
|
99
98
|
|
|
100
99
|
setAllLogs((prev) => {
|
|
101
100
|
let next = prev;
|
|
102
|
-
let sessionsChanged = false;
|
|
103
|
-
let modelsChanged = false;
|
|
104
101
|
for (const log of updates) {
|
|
105
102
|
const idx = logIndexRef.current.get(log.id);
|
|
106
103
|
if (idx !== undefined) {
|
|
@@ -110,19 +107,10 @@ export function ProxyViewerContainer(): JSX.Element {
|
|
|
110
107
|
logIndexRef.current.set(log.id, next.length);
|
|
111
108
|
next = [...next, log];
|
|
112
109
|
}
|
|
113
|
-
if (log.sessionId !== null && log.sessionId !== "" && !sessions.includes(log.sessionId)) {
|
|
114
|
-
sessionsChanged = true;
|
|
115
|
-
}
|
|
116
|
-
if (log.model !== null && log.model !== "" && !models.includes(log.model)) {
|
|
117
|
-
modelsChanged = true;
|
|
118
|
-
}
|
|
119
110
|
}
|
|
120
|
-
logsRef.current = next;
|
|
121
|
-
if (sessionsChanged) setSessions((s) => extractSessions(next));
|
|
122
|
-
if (modelsChanged) setModels((m) => extractModels(next));
|
|
123
111
|
return next;
|
|
124
112
|
});
|
|
125
|
-
}, [
|
|
113
|
+
}, []);
|
|
126
114
|
|
|
127
115
|
const scheduleUpdate = useCallback(
|
|
128
116
|
(log: CapturedLog) => {
|
|
@@ -173,10 +161,7 @@ export function ProxyViewerContainer(): JSX.Element {
|
|
|
173
161
|
if (log !== undefined) idx.set(log.id, i);
|
|
174
162
|
}
|
|
175
163
|
logIndexRef.current = idx;
|
|
176
|
-
logsRef.current = update.logs;
|
|
177
164
|
setAllLogs(update.logs);
|
|
178
|
-
setSessions(extractSessions(update.logs));
|
|
179
|
-
setModels(extractModels(update.logs));
|
|
180
165
|
setError(null);
|
|
181
166
|
} else if (update.type === "update") {
|
|
182
167
|
scheduleUpdate(update.log);
|
|
@@ -223,10 +208,7 @@ export function ProxyViewerContainer(): JSX.Element {
|
|
|
223
208
|
return;
|
|
224
209
|
}
|
|
225
210
|
logIndexRef.current.clear();
|
|
226
|
-
logsRef.current = [];
|
|
227
211
|
setAllLogs([]);
|
|
228
|
-
setSessions([]);
|
|
229
|
-
setModels([]);
|
|
230
212
|
setError(null);
|
|
231
213
|
} catch (err) {
|
|
232
214
|
setError(err instanceof Error ? err.message : "Unknown error clearing logs");
|
|
@@ -257,9 +239,6 @@ export function ProxyViewerContainer(): JSX.Element {
|
|
|
257
239
|
if (log !== undefined) idx.set(log.id, i);
|
|
258
240
|
}
|
|
259
241
|
logIndexRef.current = idx;
|
|
260
|
-
logsRef.current = remaining;
|
|
261
|
-
setSessions(extractSessions(remaining));
|
|
262
|
-
setModels(extractModels(remaining));
|
|
263
242
|
return remaining;
|
|
264
243
|
});
|
|
265
244
|
setError(null);
|
|
@@ -271,7 +250,7 @@ export function ProxyViewerContainer(): JSX.Element {
|
|
|
271
250
|
|
|
272
251
|
// Read the strip config once at the container so the virtualized list does
|
|
273
252
|
// not need N independent SWR subscriptions per row.
|
|
274
|
-
const { strip } = useStripConfig();
|
|
253
|
+
const { strip, slowResponseThresholdSeconds } = useStripConfig();
|
|
275
254
|
|
|
276
255
|
return (
|
|
277
256
|
<>
|
|
@@ -294,6 +273,7 @@ export function ProxyViewerContainer(): JSX.Element {
|
|
|
294
273
|
viewMode={viewMode}
|
|
295
274
|
onViewModeChange={setViewMode}
|
|
296
275
|
strip={strip}
|
|
276
|
+
slowResponseThresholdSeconds={slowResponseThresholdSeconds}
|
|
297
277
|
/>
|
|
298
278
|
</>
|
|
299
279
|
);
|
|
@@ -6,6 +6,7 @@ import { Button } from "../ui/button";
|
|
|
6
6
|
import { ProvidersPanel } from "./ProvidersPanel";
|
|
7
7
|
import { useProviders } from "../../lib/useProviders";
|
|
8
8
|
import { useStripConfig } from "../../lib/useStripConfig";
|
|
9
|
+
import { MAX_SLOW_RESPONSE_THRESHOLD_SECONDS } from "../../lib/runtimeConfig";
|
|
9
10
|
|
|
10
11
|
export function SettingsDialog(): JSX.Element {
|
|
11
12
|
const [open, setOpen] = useState(false);
|
|
@@ -96,7 +97,13 @@ export function SettingsDialog(): JSX.Element {
|
|
|
96
97
|
}
|
|
97
98
|
|
|
98
99
|
function ProxySettingsTab(): JSX.Element {
|
|
99
|
-
const {
|
|
100
|
+
const {
|
|
101
|
+
strip,
|
|
102
|
+
slowResponseThresholdSeconds,
|
|
103
|
+
isLoading,
|
|
104
|
+
setStrip,
|
|
105
|
+
setSlowResponseThresholdSeconds,
|
|
106
|
+
} = useStripConfig();
|
|
100
107
|
const [error, setError] = useState<string | null>(null);
|
|
101
108
|
const [pending, setPending] = useState(false);
|
|
102
109
|
|
|
@@ -115,6 +122,21 @@ function ProxySettingsTab(): JSX.Element {
|
|
|
115
122
|
[setStrip],
|
|
116
123
|
);
|
|
117
124
|
|
|
125
|
+
const handleThresholdChange = useCallback(
|
|
126
|
+
async (next: number) => {
|
|
127
|
+
setError(null);
|
|
128
|
+
setPending(true);
|
|
129
|
+
try {
|
|
130
|
+
await setSlowResponseThresholdSeconds(next);
|
|
131
|
+
} catch (err) {
|
|
132
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
133
|
+
} finally {
|
|
134
|
+
setPending(false);
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
[setSlowResponseThresholdSeconds],
|
|
138
|
+
);
|
|
139
|
+
|
|
118
140
|
return (
|
|
119
141
|
<div className="space-y-4">
|
|
120
142
|
<div className="space-y-1">
|
|
@@ -145,6 +167,35 @@ function ProxySettingsTab(): JSX.Element {
|
|
|
145
167
|
</span>
|
|
146
168
|
</label>
|
|
147
169
|
|
|
170
|
+
<div className="space-y-1">
|
|
171
|
+
<label htmlFor="slow-response-threshold" className="text-sm font-semibold">
|
|
172
|
+
Slow response threshold
|
|
173
|
+
</label>
|
|
174
|
+
<p className="text-xs text-muted-foreground">
|
|
175
|
+
Logs whose elapsed time exceeds this many seconds show a warning icon next to the
|
|
176
|
+
duration. Set to <code>0</code> to disable the indicator.
|
|
177
|
+
</p>
|
|
178
|
+
<div className="flex items-center gap-2">
|
|
179
|
+
<input
|
|
180
|
+
id="slow-response-threshold"
|
|
181
|
+
type="number"
|
|
182
|
+
min={0}
|
|
183
|
+
max={MAX_SLOW_RESPONSE_THRESHOLD_SECONDS}
|
|
184
|
+
step={1}
|
|
185
|
+
value={slowResponseThresholdSeconds}
|
|
186
|
+
disabled={isLoading || pending}
|
|
187
|
+
onChange={(e) => {
|
|
188
|
+
const next = Number(e.currentTarget.value);
|
|
189
|
+
if (!Number.isInteger(next)) return;
|
|
190
|
+
if (next < 0 || next > MAX_SLOW_RESPONSE_THRESHOLD_SECONDS) return;
|
|
191
|
+
void handleThresholdChange(next);
|
|
192
|
+
}}
|
|
193
|
+
className="h-8 w-24 rounded-md border border-input bg-transparent px-2 text-sm font-mono disabled:cursor-not-allowed disabled:opacity-50"
|
|
194
|
+
/>
|
|
195
|
+
<span className="text-xs text-muted-foreground">seconds</span>
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
|
|
148
199
|
{error !== null && <p className="text-xs text-destructive">Failed to save: {error}</p>}
|
|
149
200
|
</div>
|
|
150
201
|
);
|
|
@@ -16,6 +16,8 @@ export type ConversationGroupProps = {
|
|
|
16
16
|
viewMode?: "simple" | "full";
|
|
17
17
|
/** Live strip-Claude-Code-billing-header flag from the viewer container. */
|
|
18
18
|
strip: boolean;
|
|
19
|
+
/** Slow-response threshold in seconds. `0` disables the warning indicator. */
|
|
20
|
+
slowResponseThresholdSeconds: number;
|
|
19
21
|
/**
|
|
20
22
|
* Pre-computed per-log cache token trend map (keyed by `log.id`) shared
|
|
21
23
|
* across the whole viewer. Each `LogEntry` looks up its own entry.
|
|
@@ -47,6 +49,7 @@ export const ConversationGroup = memo(function ({
|
|
|
47
49
|
group,
|
|
48
50
|
viewMode = "simple",
|
|
49
51
|
strip,
|
|
52
|
+
slowResponseThresholdSeconds,
|
|
50
53
|
cacheTrends,
|
|
51
54
|
onCompareWithPrevious,
|
|
52
55
|
comparisonPredecessors,
|
|
@@ -90,13 +93,14 @@ export const ConversationGroup = memo(function ({
|
|
|
90
93
|
)}
|
|
91
94
|
|
|
92
95
|
{shouldRenderConversationContent(standalone, expanded) && (
|
|
93
|
-
<div
|
|
96
|
+
<div>
|
|
94
97
|
{turnGroups.map((tg) => (
|
|
95
98
|
<TurnGroup
|
|
96
99
|
key={tg.turnIndex}
|
|
97
100
|
entries={tg.entries}
|
|
98
101
|
viewMode={viewMode}
|
|
99
102
|
strip={strip}
|
|
103
|
+
slowResponseThresholdSeconds={slowResponseThresholdSeconds}
|
|
100
104
|
cacheTrends={cacheTrends}
|
|
101
105
|
onCompareWithPrevious={onCompareWithPrevious}
|
|
102
106
|
comparisonPredecessors={comparisonPredecessors}
|
|
@@ -78,11 +78,14 @@ export function ConversationHeader({
|
|
|
78
78
|
<div
|
|
79
79
|
role="button"
|
|
80
80
|
tabIndex={0}
|
|
81
|
+
data-nav-id={`conv-${conversationId}`}
|
|
82
|
+
data-nav-action={expanded ? "collapse" : "expand"}
|
|
81
83
|
className={cn(
|
|
82
84
|
"flex items-center gap-3 px-3 py-2 cursor-pointer transition-colors",
|
|
83
85
|
"hover:bg-muted/50",
|
|
84
86
|
"select-none",
|
|
85
87
|
"border border-border rounded-lg mb-2 bg-background sticky top-0 z-10",
|
|
88
|
+
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:outline-none",
|
|
86
89
|
)}
|
|
87
90
|
onClick={onToggle}
|
|
88
91
|
onKeyDown={(e) => {
|
|
@@ -174,7 +177,7 @@ export function ConversationHeader({
|
|
|
174
177
|
onClick={handleClearClick}
|
|
175
178
|
aria-label={`Clear group (${totalCalls} request${totalCalls !== 1 ? "s" : ""})`}
|
|
176
179
|
title="Clear this group"
|
|
177
|
-
className="text-muted-foreground hover:text-foreground transition-colors shrink-0 inline-flex items-center justify-center size-
|
|
180
|
+
className="text-muted-foreground hover:text-foreground transition-colors shrink-0 inline-flex items-center justify-center size-8 rounded hover:bg-muted cursor-pointer"
|
|
178
181
|
>
|
|
179
182
|
<Trash2 className="size-3.5" />
|
|
180
183
|
</button>
|