@tonyclaw/llm-inspector 1.16.5 → 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-C1w4KUGZ.js → CompareDrawer-C4fie5g5.js} +1 -1
- package/.output/public/assets/{ReplayDialog-DR2Sgq_g.js → ReplayDialog-Dme5uOR9.js} +1 -1
- package/.output/public/assets/{RequestAnatomy-DAre35kj.js → RequestAnatomy-ChBLDNFH.js} +1 -1
- package/.output/public/assets/{ResponseView-ackes7_g.js → ResponseView-wGeqBzVU.js} +1 -1
- package/.output/public/assets/{StreamingChunkSequence-GrXwIGKA.js → StreamingChunkSequence-zeJZQLqT.js} +1 -1
- package/.output/public/assets/index-DoGvsnbA.css +1 -0
- package/.output/public/assets/index-DpbutOvo.js +101 -0
- package/.output/public/assets/{json-viewer-C_QUhGeu.js → json-viewer-BV-WUszW.js} +1 -1
- package/.output/public/assets/{main-CDMdNDY_.js → main-DRu10KNQ.js} +1 -1
- package/.output/server/_libs/lucide-react.mjs +6 -6
- package/.output/server/_ssr/{CompareDrawer-ftkJxyk6.mjs → CompareDrawer-C4-CQL5w.mjs} +4 -4
- package/.output/server/_ssr/{ReplayDialog-DcmE3lj5.mjs → ReplayDialog-BTb1Bam8.mjs} +4 -4
- package/.output/server/_ssr/{RequestAnatomy-rK_LNMdG.mjs → RequestAnatomy-CZFV1IvL.mjs} +2 -2
- package/.output/server/_ssr/{ResponseView-CbQ4n-aJ.mjs → ResponseView-CTZekh65.mjs} +4 -4
- package/.output/server/_ssr/{StreamingChunkSequence-84FZkIzv.mjs → StreamingChunkSequence-C38Ynabd.mjs} +3 -3
- package/.output/server/_ssr/{index-CDjLoMsk.mjs → index-Cnu-QzAy.mjs} +159 -32
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{json-viewer-B-qpM5xC.mjs → json-viewer-DROqpjS9.mjs} +2 -2
- package/.output/server/_ssr/{router-BrdjOUEW.mjs → router-pP4GCTQx.mjs} +20 -6
- package/.output/server/{_tanstack-start-manifest_v-DmOZEcJ3.mjs → _tanstack-start-manifest_v-CphS4rZd.mjs} +1 -1
- package/.output/server/index.mjs +58 -58
- package/package.json +1 -1
- package/src/components/ProxyViewer.tsx +6 -1
- package/src/components/ProxyViewerContainer.tsx +2 -1
- package/src/components/providers/SettingsDialog.tsx +52 -1
- package/src/components/proxy-viewer/ConversationGroup.tsx +4 -0
- package/src/components/proxy-viewer/LogEntry.tsx +4 -0
- package/src/components/proxy-viewer/LogEntryHeader.tsx +47 -4
- package/src/components/proxy-viewer/TurnGroup.tsx +36 -7
- package/src/lib/runtimeConfig.ts +9 -0
- package/src/lib/useOnboarding.ts +7 -1
- package/src/lib/useStripConfig.ts +33 -2
- package/src/proxy/config.ts +17 -7
- package/src/routes/api/config.ts +7 -0
- package/.output/public/assets/index-BGzHFOEX.css +0 -1
- package/.output/public/assets/index-DX88k9br.js +0 -101
package/.output/server/index.mjs
CHANGED
|
@@ -38,91 +38,91 @@ const assets = {
|
|
|
38
38
|
"/assets/alibaba-TTwafVwX.svg": {
|
|
39
39
|
"type": "image/svg+xml",
|
|
40
40
|
"etag": '"171b-6dyV5K8QjiaY35sN9qNprh9zDIs"',
|
|
41
|
-
"mtime": "2026-06-
|
|
41
|
+
"mtime": "2026-06-16T13:43:26.730Z",
|
|
42
42
|
"size": 5915,
|
|
43
43
|
"path": "../public/assets/alibaba-TTwafVwX.svg"
|
|
44
44
|
},
|
|
45
|
-
"/assets/
|
|
46
|
-
"type": "image/jpeg",
|
|
47
|
-
"etag": '"1b06-IwivU89ko5UTMUM1/t7hn4sQK9A"',
|
|
48
|
-
"mtime": "2026-06-15T12:23:59.073Z",
|
|
49
|
-
"size": 6918,
|
|
50
|
-
"path": "../public/assets/minimax-BPMzvuL-.jpeg"
|
|
51
|
-
},
|
|
52
|
-
"/assets/CompareDrawer-C1w4KUGZ.js": {
|
|
45
|
+
"/assets/CompareDrawer-C4fie5g5.js": {
|
|
53
46
|
"type": "text/javascript; charset=utf-8",
|
|
54
|
-
"etag": '"4a10-
|
|
55
|
-
"mtime": "2026-06-
|
|
47
|
+
"etag": '"4a10-3djWuOwKSKXlQPoy4NHzqd2tggE"',
|
|
48
|
+
"mtime": "2026-06-16T13:43:26.730Z",
|
|
56
49
|
"size": 18960,
|
|
57
|
-
"path": "../public/assets/CompareDrawer-
|
|
58
|
-
},
|
|
59
|
-
"/assets/json-viewer-C_QUhGeu.js": {
|
|
60
|
-
"type": "text/javascript; charset=utf-8",
|
|
61
|
-
"etag": '"1e60c-NxhepLRPlTKqcPUc59jhJtOyGpg"',
|
|
62
|
-
"mtime": "2026-06-15T12:23:59.074Z",
|
|
63
|
-
"size": 124428,
|
|
64
|
-
"path": "../public/assets/json-viewer-C_QUhGeu.js"
|
|
50
|
+
"path": "../public/assets/CompareDrawer-C4fie5g5.js"
|
|
65
51
|
},
|
|
66
|
-
"/assets/index-
|
|
52
|
+
"/assets/index-DoGvsnbA.css": {
|
|
67
53
|
"type": "text/css; charset=utf-8",
|
|
68
|
-
"etag": '"
|
|
69
|
-
"mtime": "2026-06-
|
|
70
|
-
"size":
|
|
71
|
-
"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"
|
|
72
58
|
},
|
|
73
|
-
"/assets/
|
|
59
|
+
"/assets/index-DpbutOvo.js": {
|
|
74
60
|
"type": "text/javascript; charset=utf-8",
|
|
75
|
-
"etag": '"
|
|
76
|
-
"mtime": "2026-06-
|
|
77
|
-
"size":
|
|
78
|
-
"path": "../public/assets/
|
|
61
|
+
"etag": '"744e2-P9RhN2wY9pxyu6b/wpeHlefe59k"',
|
|
62
|
+
"mtime": "2026-06-16T13:43:26.731Z",
|
|
63
|
+
"size": 476386,
|
|
64
|
+
"path": "../public/assets/index-DpbutOvo.js"
|
|
79
65
|
},
|
|
80
|
-
"/assets/
|
|
66
|
+
"/assets/ReplayDialog-Dme5uOR9.js": {
|
|
81
67
|
"type": "text/javascript; charset=utf-8",
|
|
82
|
-
"etag": '"
|
|
83
|
-
"mtime": "2026-06-
|
|
84
|
-
"size":
|
|
85
|
-
"path": "../public/assets/
|
|
68
|
+
"etag": '"11b1-SKRfqUa9SY8yNi8IxWzCC6VvjQI"',
|
|
69
|
+
"mtime": "2026-06-16T13:43:26.730Z",
|
|
70
|
+
"size": 4529,
|
|
71
|
+
"path": "../public/assets/ReplayDialog-Dme5uOR9.js"
|
|
86
72
|
},
|
|
87
|
-
"/assets/
|
|
88
|
-
"type": "
|
|
89
|
-
"etag": '"
|
|
90
|
-
"mtime": "2026-06-
|
|
91
|
-
"size":
|
|
92
|
-
"path": "../public/assets/
|
|
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"
|
|
93
79
|
},
|
|
94
|
-
"/assets/RequestAnatomy-
|
|
80
|
+
"/assets/RequestAnatomy-ChBLDNFH.js": {
|
|
95
81
|
"type": "text/javascript; charset=utf-8",
|
|
96
|
-
"etag": '"141b
|
|
97
|
-
"mtime": "2026-06-
|
|
82
|
+
"etag": '"141b-nxiiCAE0vs7SLaqcUISSgXq1IGo"',
|
|
83
|
+
"mtime": "2026-06-16T13:43:26.730Z",
|
|
98
84
|
"size": 5147,
|
|
99
|
-
"path": "../public/assets/RequestAnatomy-
|
|
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"
|
|
100
93
|
},
|
|
101
|
-
"/assets/main-
|
|
94
|
+
"/assets/main-DRu10KNQ.js": {
|
|
102
95
|
"type": "text/javascript; charset=utf-8",
|
|
103
|
-
"etag": '"505ae-
|
|
104
|
-
"mtime": "2026-06-
|
|
96
|
+
"etag": '"505ae-BP/aj8/2FZWooG/jXqGZEmVfl3E"',
|
|
97
|
+
"mtime": "2026-06-16T13:43:26.730Z",
|
|
105
98
|
"size": 329134,
|
|
106
|
-
"path": "../public/assets/main-
|
|
99
|
+
"path": "../public/assets/main-DRu10KNQ.js"
|
|
107
100
|
},
|
|
108
|
-
"/assets/StreamingChunkSequence-
|
|
101
|
+
"/assets/StreamingChunkSequence-zeJZQLqT.js": {
|
|
109
102
|
"type": "text/javascript; charset=utf-8",
|
|
110
|
-
"etag": '"d72-
|
|
111
|
-
"mtime": "2026-06-
|
|
103
|
+
"etag": '"d72-nuJyJopptXOHt0oPHw//D9VSW+k"',
|
|
104
|
+
"mtime": "2026-06-16T13:43:26.731Z",
|
|
112
105
|
"size": 3442,
|
|
113
|
-
"path": "../public/assets/StreamingChunkSequence-
|
|
106
|
+
"path": "../public/assets/StreamingChunkSequence-zeJZQLqT.js"
|
|
114
107
|
},
|
|
115
|
-
"/assets/
|
|
108
|
+
"/assets/ResponseView-wGeqBzVU.js": {
|
|
116
109
|
"type": "text/javascript; charset=utf-8",
|
|
117
|
-
"etag": '"
|
|
118
|
-
"mtime": "2026-06-
|
|
119
|
-
"size":
|
|
120
|
-
"path": "../public/assets/
|
|
110
|
+
"etag": '"6e82-ZzZo1WT+DlE16b9M9AMC/410RcI"',
|
|
111
|
+
"mtime": "2026-06-16T13:43:26.731Z",
|
|
112
|
+
"size": 28290,
|
|
113
|
+
"path": "../public/assets/ResponseView-wGeqBzVU.js"
|
|
114
|
+
},
|
|
115
|
+
"/assets/zhipuai-BPNAnxo-.svg": {
|
|
116
|
+
"type": "image/svg+xml",
|
|
117
|
+
"etag": '"2bf8-hNaLCTi89nOFCsIIfWpP/jrfo0s"',
|
|
118
|
+
"mtime": "2026-06-16T13:43:26.730Z",
|
|
119
|
+
"size": 11256,
|
|
120
|
+
"path": "../public/assets/zhipuai-BPNAnxo-.svg"
|
|
121
121
|
},
|
|
122
122
|
"/assets/qwen-CONDcHqt.png": {
|
|
123
123
|
"type": "image/png",
|
|
124
124
|
"etag": '"572c3-cdJAPaHdOvFCGzuaQjagdgOu6XE"',
|
|
125
|
-
"mtime": "2026-06-
|
|
125
|
+
"mtime": "2026-06-16T13:43:26.730Z",
|
|
126
126
|
"size": 357059,
|
|
127
127
|
"path": "../public/assets/qwen-CONDcHqt.png"
|
|
128
128
|
}
|
package/package.json
CHANGED
|
@@ -3,6 +3,7 @@ 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
|
|
|
@@ -98,6 +99,8 @@ export type ProxyViewerProps = {
|
|
|
98
99
|
onViewModeChange: (mode: "simple" | "full") => void;
|
|
99
100
|
/** Live strip-Claude-Code-billing-header flag, sourced once at the container. */
|
|
100
101
|
strip: boolean;
|
|
102
|
+
/** Slow-response threshold in seconds. `0` disables the warning indicator. */
|
|
103
|
+
slowResponseThresholdSeconds: number;
|
|
101
104
|
};
|
|
102
105
|
|
|
103
106
|
export function ProxyViewer({
|
|
@@ -113,6 +116,7 @@ export function ProxyViewer({
|
|
|
113
116
|
viewMode,
|
|
114
117
|
onViewModeChange,
|
|
115
118
|
strip,
|
|
119
|
+
slowResponseThresholdSeconds,
|
|
116
120
|
}: ProxyViewerProps): JSX.Element {
|
|
117
121
|
const { totalIn, totalOut } = useMemo(() => computeTokenSummary(logs), [logs]);
|
|
118
122
|
const [exporting, setExporting] = useState(false);
|
|
@@ -276,7 +280,7 @@ export function ProxyViewer({
|
|
|
276
280
|
<span className="text-muted-foreground text-xs font-mono">
|
|
277
281
|
{logs.length} request{logs.length !== 1 ? "s" : ""}
|
|
278
282
|
{totalIn > 0 || totalOut > 0
|
|
279
|
-
? ` · ${totalIn
|
|
283
|
+
? ` · ${formatTokens(totalIn)} in / ${formatTokens(totalOut)} out`
|
|
280
284
|
: ""}
|
|
281
285
|
</span>
|
|
282
286
|
{logs.length > 0 && (
|
|
@@ -330,6 +334,7 @@ export function ProxyViewer({
|
|
|
330
334
|
group={group}
|
|
331
335
|
viewMode={viewMode}
|
|
332
336
|
strip={strip}
|
|
337
|
+
slowResponseThresholdSeconds={slowResponseThresholdSeconds}
|
|
333
338
|
cacheTrends={cacheTrends}
|
|
334
339
|
onCompareWithPrevious={handleCompareWithPrevious}
|
|
335
340
|
comparisonPredecessors={comparisonPredecessors}
|
|
@@ -250,7 +250,7 @@ export function ProxyViewerContainer(): JSX.Element {
|
|
|
250
250
|
|
|
251
251
|
// Read the strip config once at the container so the virtualized list does
|
|
252
252
|
// not need N independent SWR subscriptions per row.
|
|
253
|
-
const { strip } = useStripConfig();
|
|
253
|
+
const { strip, slowResponseThresholdSeconds } = useStripConfig();
|
|
254
254
|
|
|
255
255
|
return (
|
|
256
256
|
<>
|
|
@@ -273,6 +273,7 @@ export function ProxyViewerContainer(): JSX.Element {
|
|
|
273
273
|
viewMode={viewMode}
|
|
274
274
|
onViewModeChange={setViewMode}
|
|
275
275
|
strip={strip}
|
|
276
|
+
slowResponseThresholdSeconds={slowResponseThresholdSeconds}
|
|
276
277
|
/>
|
|
277
278
|
</>
|
|
278
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,
|
|
@@ -97,6 +100,7 @@ export const ConversationGroup = memo(function ({
|
|
|
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}
|
|
@@ -45,6 +45,8 @@ export type LogEntryProps = {
|
|
|
45
45
|
* cost).
|
|
46
46
|
*/
|
|
47
47
|
strip: boolean;
|
|
48
|
+
/** Slow-response threshold in seconds. `0` disables the warning indicator. */
|
|
49
|
+
slowResponseThresholdSeconds: number;
|
|
48
50
|
/**
|
|
49
51
|
* Per-log cache token trend, looked up in the viewer-level trend map.
|
|
50
52
|
* `null` (or absent) means the header should render with no arrows.
|
|
@@ -180,6 +182,7 @@ export const LogEntry = memo(function ({
|
|
|
180
182
|
log,
|
|
181
183
|
viewMode = "simple",
|
|
182
184
|
strip,
|
|
185
|
+
slowResponseThresholdSeconds,
|
|
183
186
|
cacheTrend = null,
|
|
184
187
|
onCompareWithPrevious,
|
|
185
188
|
}: LogEntryProps): JSX.Element {
|
|
@@ -242,6 +245,7 @@ export const LogEntry = memo(function ({
|
|
|
242
245
|
onToggle={() => setExpanded(!expanded)}
|
|
243
246
|
responseToolNames={responseAnalysis.toolNames}
|
|
244
247
|
cacheTrend={cacheTrend}
|
|
248
|
+
slowResponseThresholdSeconds={slowResponseThresholdSeconds}
|
|
245
249
|
onReplay={
|
|
246
250
|
onCompareWithPrevious === undefined
|
|
247
251
|
? undefined
|
|
@@ -41,6 +41,14 @@ function formatElapsed(ms: number): string {
|
|
|
41
41
|
return `${(ms / 1000).toFixed(1)}s`;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
function formatTimestamp(iso: string): string {
|
|
45
|
+
const d = new Date(iso);
|
|
46
|
+
const hh = String(d.getHours()).padStart(2, "0");
|
|
47
|
+
const mm = String(d.getMinutes()).padStart(2, "0");
|
|
48
|
+
const ss = String(d.getSeconds()).padStart(2, "0");
|
|
49
|
+
return `${hh}:${mm}:${ss}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
44
52
|
/**
|
|
45
53
|
* Inline trend indicator: small arrow (green up / red down) plus the absolute
|
|
46
54
|
* delta in compact form. Returns `null` when there is no trend to display.
|
|
@@ -91,6 +99,8 @@ export type LogEntryHeaderProps = {
|
|
|
91
99
|
isExpanded: boolean;
|
|
92
100
|
isPending: boolean;
|
|
93
101
|
} | null;
|
|
102
|
+
/** Slow-response threshold in seconds. `0` disables the warning indicator. */
|
|
103
|
+
slowResponseThresholdSeconds?: number;
|
|
94
104
|
};
|
|
95
105
|
|
|
96
106
|
export const LogEntryHeader = memo(function ({
|
|
@@ -106,8 +116,13 @@ export const LogEntryHeader = memo(function ({
|
|
|
106
116
|
requestCopied = false,
|
|
107
117
|
onToggleRequestExpansion,
|
|
108
118
|
requestExpansionState = null,
|
|
119
|
+
slowResponseThresholdSeconds = 0,
|
|
109
120
|
}: LogEntryHeaderProps): JSX.Element {
|
|
110
121
|
const statusCategory = getStatusCategory(log.responseStatus);
|
|
122
|
+
const isSlowResponse =
|
|
123
|
+
log.elapsedMs !== null &&
|
|
124
|
+
slowResponseThresholdSeconds > 0 &&
|
|
125
|
+
log.elapsedMs > slowResponseThresholdSeconds * 1000;
|
|
111
126
|
|
|
112
127
|
const hasTokens = log.inputTokens !== null || log.outputTokens !== null;
|
|
113
128
|
const toolNamesJoined = useMemo(() => responseToolNames?.join(", ") ?? null, [responseToolNames]);
|
|
@@ -138,6 +153,17 @@ export const LogEntryHeader = memo(function ({
|
|
|
138
153
|
#{log.id}
|
|
139
154
|
</span>
|
|
140
155
|
|
|
156
|
+
{/* Timestamp */}
|
|
157
|
+
<Tooltip>
|
|
158
|
+
<TooltipTrigger asChild>
|
|
159
|
+
<span className="flex items-center gap-1 text-muted-foreground text-xs shrink-0">
|
|
160
|
+
<Clock className="size-3" />
|
|
161
|
+
<span className="font-mono tabular-nums">{formatTimestamp(log.timestamp)}</span>
|
|
162
|
+
</span>
|
|
163
|
+
</TooltipTrigger>
|
|
164
|
+
<TooltipContent>{log.timestamp}</TooltipContent>
|
|
165
|
+
</Tooltip>
|
|
166
|
+
|
|
141
167
|
{/* Model — logo icon only, model name in tooltip */}
|
|
142
168
|
{log.model !== null && (
|
|
143
169
|
<Tooltip>
|
|
@@ -174,10 +200,27 @@ export const LogEntryHeader = memo(function ({
|
|
|
174
200
|
|
|
175
201
|
{/* Elapsed time */}
|
|
176
202
|
{log.elapsedMs !== null && (
|
|
177
|
-
<
|
|
178
|
-
<
|
|
179
|
-
|
|
180
|
-
|
|
203
|
+
<Tooltip>
|
|
204
|
+
<TooltipTrigger asChild>
|
|
205
|
+
<span
|
|
206
|
+
className={cn(
|
|
207
|
+
"flex items-center gap-1 text-xs shrink-0",
|
|
208
|
+
isSlowResponse ? "text-amber-400" : "text-muted-foreground",
|
|
209
|
+
)}
|
|
210
|
+
>
|
|
211
|
+
<Clock className="size-3" />
|
|
212
|
+
<span className="font-mono tabular-nums">{formatElapsed(log.elapsedMs)}</span>
|
|
213
|
+
{isSlowResponse && <AlertTriangle className="size-3" aria-label="Slow response" />}
|
|
214
|
+
</span>
|
|
215
|
+
</TooltipTrigger>
|
|
216
|
+
<TooltipContent>
|
|
217
|
+
{isSlowResponse
|
|
218
|
+
? `Slow response: ${formatElapsed(log.elapsedMs)} exceeds ${formatElapsed(
|
|
219
|
+
slowResponseThresholdSeconds * 1000,
|
|
220
|
+
)}`
|
|
221
|
+
: "Elapsed response time"}
|
|
222
|
+
</TooltipContent>
|
|
223
|
+
</Tooltip>
|
|
181
224
|
)}
|
|
182
225
|
|
|
183
226
|
{/* Token counts */}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { type JSX, memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
-
import { ChevronRight, Clock, Zap } from "lucide-react";
|
|
2
|
+
import { AlertTriangle, ChevronRight, Clock, Zap } from "lucide-react";
|
|
3
3
|
import { isTurnBoundary } from "../../lib/stopReason";
|
|
4
4
|
import { cn, formatTokens } from "../../lib/utils";
|
|
5
5
|
import type { CapturedLog } from "../../proxy/schemas";
|
|
6
6
|
import { getCrabVariant } from "../ui/crab-variants";
|
|
7
7
|
import { ProviderLogo, detectProvider, type Provider } from "../providers/ProviderLogo";
|
|
8
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/tooltip";
|
|
8
9
|
import type { CacheTrendEntry } from "./cacheTrend";
|
|
9
10
|
import { LogEntry } from "./LogEntry";
|
|
10
11
|
import { ThreadConnector } from "./ThreadConnector";
|
|
@@ -19,6 +20,7 @@ type TurnGroupProps = {
|
|
|
19
20
|
entries: TurnEntry[];
|
|
20
21
|
viewMode: "simple" | "full";
|
|
21
22
|
strip: boolean;
|
|
23
|
+
slowResponseThresholdSeconds: number;
|
|
22
24
|
cacheTrends?: Map<number, CacheTrendEntry>;
|
|
23
25
|
onCompareWithPrevious: (log: CapturedLog) => void;
|
|
24
26
|
comparisonPredecessors: Map<number, CapturedLog>;
|
|
@@ -29,6 +31,7 @@ export const TurnGroup = memo(function TurnGroup({
|
|
|
29
31
|
entries,
|
|
30
32
|
viewMode,
|
|
31
33
|
strip,
|
|
34
|
+
slowResponseThresholdSeconds,
|
|
32
35
|
cacheTrends,
|
|
33
36
|
onCompareWithPrevious,
|
|
34
37
|
comparisonPredecessors,
|
|
@@ -102,6 +105,10 @@ export const TurnGroup = memo(function TurnGroup({
|
|
|
102
105
|
const EndCrab = useMemo(() => getCrabVariant(entries[lastIdx]?.log.id ?? 0), [entries, lastIdx]);
|
|
103
106
|
|
|
104
107
|
const bgClass = turnIndex % 2 === 0 ? "bg-muted/10" : "bg-muted/25";
|
|
108
|
+
const aggregateIsSlow =
|
|
109
|
+
aggregate.hasElapsed &&
|
|
110
|
+
slowResponseThresholdSeconds > 0 &&
|
|
111
|
+
aggregate.totalElapsed > slowResponseThresholdSeconds * 1000;
|
|
105
112
|
|
|
106
113
|
// ResizeObserver → re-render connectors when any LogEntry height changes
|
|
107
114
|
const [layoutVersion, setLayoutVersion] = useState(0);
|
|
@@ -234,12 +241,33 @@ export const TurnGroup = memo(function TurnGroup({
|
|
|
234
241
|
|
|
235
242
|
{/* Elapsed */}
|
|
236
243
|
{aggregate.hasElapsed && (
|
|
237
|
-
<
|
|
238
|
-
<
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
244
|
+
<TooltipProvider>
|
|
245
|
+
<Tooltip>
|
|
246
|
+
<TooltipTrigger asChild>
|
|
247
|
+
<span
|
|
248
|
+
className={cn(
|
|
249
|
+
"flex items-center gap-1 shrink-0",
|
|
250
|
+
aggregateIsSlow ? "text-amber-400" : "text-muted-foreground",
|
|
251
|
+
)}
|
|
252
|
+
>
|
|
253
|
+
<Clock className="size-3" />
|
|
254
|
+
<span className="font-mono tabular-nums">
|
|
255
|
+
{formatElapsed(aggregate.totalElapsed)}
|
|
256
|
+
</span>
|
|
257
|
+
{aggregateIsSlow && (
|
|
258
|
+
<AlertTriangle className="size-3" aria-label="Slow response" />
|
|
259
|
+
)}
|
|
260
|
+
</span>
|
|
261
|
+
</TooltipTrigger>
|
|
262
|
+
<TooltipContent>
|
|
263
|
+
{aggregateIsSlow
|
|
264
|
+
? `Slow response: ${formatElapsed(
|
|
265
|
+
aggregate.totalElapsed,
|
|
266
|
+
)} exceeds ${formatElapsed(slowResponseThresholdSeconds * 1000)}`
|
|
267
|
+
: "Total elapsed response time"}
|
|
268
|
+
</TooltipContent>
|
|
269
|
+
</Tooltip>
|
|
270
|
+
</TooltipProvider>
|
|
243
271
|
)}
|
|
244
272
|
|
|
245
273
|
{/* Tokens */}
|
|
@@ -296,6 +324,7 @@ export const TurnGroup = memo(function TurnGroup({
|
|
|
296
324
|
log={log}
|
|
297
325
|
viewMode={viewMode}
|
|
298
326
|
strip={strip}
|
|
327
|
+
slowResponseThresholdSeconds={slowResponseThresholdSeconds}
|
|
299
328
|
cacheTrend={cacheTrends?.get(log.id) ?? null}
|
|
300
329
|
onCompareWithPrevious={
|
|
301
330
|
comparisonPredecessors.has(log.id) ? onCompareWithPrevious : undefined
|
package/src/lib/runtimeConfig.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
|
+
export const DEFAULT_SLOW_RESPONSE_THRESHOLD_SECONDS = 10;
|
|
4
|
+
export const MAX_SLOW_RESPONSE_THRESHOLD_SECONDS = 600;
|
|
5
|
+
|
|
3
6
|
/**
|
|
4
7
|
* Schema for the runtime proxy config. Shared between server
|
|
5
8
|
* (src/proxy/config.ts) and client (src/lib/useStripConfig.ts) so that
|
|
@@ -11,6 +14,12 @@ import { z } from "zod";
|
|
|
11
14
|
export const RuntimeConfigSchema = z.object({
|
|
12
15
|
stripClaudeCodeBillingHeader: z.boolean(),
|
|
13
16
|
hasSeenOnboarding: z.boolean().default(false),
|
|
17
|
+
slowResponseThresholdSeconds: z
|
|
18
|
+
.number()
|
|
19
|
+
.int()
|
|
20
|
+
.min(0)
|
|
21
|
+
.max(MAX_SLOW_RESPONSE_THRESHOLD_SECONDS)
|
|
22
|
+
.default(DEFAULT_SLOW_RESPONSE_THRESHOLD_SECONDS),
|
|
14
23
|
});
|
|
15
24
|
|
|
16
25
|
export type RuntimeConfig = z.infer<typeof RuntimeConfigSchema>;
|
package/src/lib/useOnboarding.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import useSWR, { type SWRResponse, useSWRConfig } from "swr";
|
|
2
2
|
import { fetchJson } from "./apiClient";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_SLOW_RESPONSE_THRESHOLD_SECONDS,
|
|
5
|
+
RuntimeConfigSchema,
|
|
6
|
+
type RuntimeConfig,
|
|
7
|
+
} from "./runtimeConfig";
|
|
4
8
|
|
|
5
9
|
export const ONBOARDING_SWR_KEY = "/api/config";
|
|
6
10
|
|
|
@@ -60,6 +64,8 @@ export function useOnboarding(): UseOnboarding {
|
|
|
60
64
|
optimisticData: {
|
|
61
65
|
stripClaudeCodeBillingHeader: response.data?.stripClaudeCodeBillingHeader ?? false,
|
|
62
66
|
hasSeenOnboarding: true,
|
|
67
|
+
slowResponseThresholdSeconds:
|
|
68
|
+
response.data?.slowResponseThresholdSeconds ?? DEFAULT_SLOW_RESPONSE_THRESHOLD_SECONDS,
|
|
63
69
|
},
|
|
64
70
|
rollbackOnError: true,
|
|
65
71
|
revalidate: false,
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import useSWR, { type SWRResponse, useSWRConfig } from "swr";
|
|
2
2
|
import { fetchJson } from "./apiClient";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_SLOW_RESPONSE_THRESHOLD_SECONDS,
|
|
5
|
+
RuntimeConfigSchema,
|
|
6
|
+
type RuntimeConfig,
|
|
7
|
+
} from "./runtimeConfig";
|
|
4
8
|
|
|
5
9
|
export const STRIP_CONFIG_SWR_KEY = "/api/config";
|
|
6
10
|
|
|
@@ -31,15 +35,18 @@ export async function setRuntimeConfig(patch: Partial<RuntimeConfig>): Promise<R
|
|
|
31
35
|
|
|
32
36
|
export type UseStripConfig = {
|
|
33
37
|
strip: boolean;
|
|
38
|
+
slowResponseThresholdSeconds: number;
|
|
34
39
|
isLoading: boolean;
|
|
35
40
|
isError: boolean;
|
|
36
41
|
setStrip: (next: boolean) => Promise<void>;
|
|
42
|
+
setSlowResponseThresholdSeconds: (next: number) => Promise<void>;
|
|
37
43
|
};
|
|
38
44
|
|
|
39
45
|
/**
|
|
40
46
|
* Hook reading the runtime config from /api/config.
|
|
41
47
|
*
|
|
42
48
|
* - `strip` is the live value from the server (or the optimistic value mid-PATCH).
|
|
49
|
+
* - `slowResponseThresholdSeconds` controls the UI's slow-response indicator.
|
|
43
50
|
* - `setStrip(next)` PATCHes the server with optimistic update; on failure
|
|
44
51
|
* it throws and SWR rolls back the optimistic value.
|
|
45
52
|
*/
|
|
@@ -55,13 +62,35 @@ export function useStripConfig(): UseStripConfig {
|
|
|
55
62
|
const { mutate: globalMutate } = useSWRConfig();
|
|
56
63
|
|
|
57
64
|
const strip = response.data?.stripClaudeCodeBillingHeader ?? false;
|
|
65
|
+
const slowResponseThresholdSeconds =
|
|
66
|
+
response.data?.slowResponseThresholdSeconds ?? DEFAULT_SLOW_RESPONSE_THRESHOLD_SECONDS;
|
|
67
|
+
|
|
68
|
+
const optimisticConfig = (patch: Partial<RuntimeConfig>): RuntimeConfig => ({
|
|
69
|
+
stripClaudeCodeBillingHeader: response.data?.stripClaudeCodeBillingHeader ?? false,
|
|
70
|
+
hasSeenOnboarding: response.data?.hasSeenOnboarding ?? false,
|
|
71
|
+
slowResponseThresholdSeconds:
|
|
72
|
+
response.data?.slowResponseThresholdSeconds ?? DEFAULT_SLOW_RESPONSE_THRESHOLD_SECONDS,
|
|
73
|
+
...patch,
|
|
74
|
+
});
|
|
58
75
|
|
|
59
76
|
const setStrip = async (next: boolean): Promise<void> => {
|
|
60
77
|
await globalMutate(
|
|
61
78
|
STRIP_CONFIG_SWR_KEY,
|
|
62
79
|
setRuntimeConfig({ stripClaudeCodeBillingHeader: next }),
|
|
63
80
|
{
|
|
64
|
-
optimisticData: { stripClaudeCodeBillingHeader: next },
|
|
81
|
+
optimisticData: optimisticConfig({ stripClaudeCodeBillingHeader: next }),
|
|
82
|
+
rollbackOnError: true,
|
|
83
|
+
revalidate: false,
|
|
84
|
+
},
|
|
85
|
+
);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const setSlowResponseThresholdSeconds = async (next: number): Promise<void> => {
|
|
89
|
+
await globalMutate(
|
|
90
|
+
STRIP_CONFIG_SWR_KEY,
|
|
91
|
+
setRuntimeConfig({ slowResponseThresholdSeconds: next }),
|
|
92
|
+
{
|
|
93
|
+
optimisticData: optimisticConfig({ slowResponseThresholdSeconds: next }),
|
|
65
94
|
rollbackOnError: true,
|
|
66
95
|
revalidate: false,
|
|
67
96
|
},
|
|
@@ -70,8 +99,10 @@ export function useStripConfig(): UseStripConfig {
|
|
|
70
99
|
|
|
71
100
|
return {
|
|
72
101
|
strip,
|
|
102
|
+
slowResponseThresholdSeconds,
|
|
73
103
|
isLoading: response.isLoading,
|
|
74
104
|
isError: response.error !== undefined,
|
|
75
105
|
setStrip,
|
|
106
|
+
setSlowResponseThresholdSeconds,
|
|
76
107
|
};
|
|
77
108
|
}
|