@tonyclaw/llm-inspector 1.18.1 → 1.18.2
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-CAhlM_Gq.js → CompareDrawer-C-4ypEWs.js} +1 -1
- package/.output/public/assets/ProxyViewerContainer-WRenRpeh.js +101 -0
- package/.output/public/assets/{ReplayDialog-Bqu2f5HE.js → ReplayDialog-CyBKOgba.js} +1 -1
- package/.output/public/assets/{RequestAnatomy-CpVNH0CD.js → RequestAnatomy-C0IrVQ3q.js} +1 -1
- package/.output/public/assets/{ResponseView-B_Gg37Lr.js → ResponseView-MogToC4i.js} +1 -1
- package/.output/public/assets/{StreamingChunkSequence-E2M_SS1A.js → StreamingChunkSequence-ClhUhT-s.js} +1 -1
- package/.output/public/assets/_sessionId-BO47oA3Z.js +1 -0
- package/.output/public/assets/index-BRvz6-L6.css +1 -0
- package/.output/public/assets/index-Btw8ec7-.js +1 -0
- package/.output/public/assets/{json-viewer-DqhA-ODG.js → json-viewer-BicGakI5.js} +1 -1
- package/.output/public/assets/{main-DpH7JlHv.js → main-Be2qqUUW.js} +7 -7
- package/.output/server/_libs/lucide-react.mjs +20 -14
- package/.output/server/{_sessionId-DcJ0RDNl.mjs → _sessionId-DhKJIdQC.mjs} +3 -3
- package/.output/server/_ssr/{CompareDrawer-DajC3x7u.mjs → CompareDrawer-BGUgukJ8.mjs} +5 -5
- package/.output/server/_ssr/{ProxyViewerContainer-C2dnFXoC.mjs → ProxyViewerContainer--3K3o3Sm.mjs} +212 -74
- package/.output/server/_ssr/{ReplayDialog-BnCLuA5z.mjs → ReplayDialog-Bo86xZI4.mjs} +5 -5
- package/.output/server/_ssr/{RequestAnatomy-OHE3iT-f.mjs → RequestAnatomy-jRU5qgwB.mjs} +4 -4
- package/.output/server/_ssr/{ResponseView-NPshHwOv.mjs → ResponseView-DdO_-79a.mjs} +5 -5
- package/.output/server/_ssr/{StreamingChunkSequence-BfukoR7F.mjs → StreamingChunkSequence-BigLwhh4.mjs} +5 -5
- package/.output/server/_ssr/{index-CF8M0tsv.mjs → index-BHG6vOnr.mjs} +3 -3
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{json-viewer-CHBa-Oas.mjs → json-viewer-B4c_WjXD.mjs} +4 -4
- package/.output/server/_ssr/{router-B5hOtKSn.mjs → router-DVixpJO-.mjs} +32 -17
- package/.output/server/{_tanstack-start-manifest_v-CFyWvIH6.mjs → _tanstack-start-manifest_v-BbvWUF4v.mjs} +1 -1
- package/.output/server/index.mjs +63 -63
- package/README.md +109 -59
- package/package.json +1 -1
- package/src/assets/logos/mcp.png +0 -0
- package/src/components/ProxyViewer.tsx +203 -53
- package/src/components/ProxyViewerContainer.tsx +24 -9
- package/src/components/proxy-viewer/ConversationHeader.tsx +7 -22
- package/src/components/ui/mcp-logo.tsx +20 -0
- package/src/lib/sessionUrl.ts +44 -0
- package/src/routes/session/$sessionId.tsx +5 -57
- package/.output/public/assets/ProxyViewerContainer--miVHNPZ.js +0 -101
- package/.output/public/assets/_sessionId-P9LgC1bF.js +0 -1
- package/.output/public/assets/index-C0wv3YP9.css +0 -1
- package/.output/public/assets/index-kboKku6a.js +0 -1
|
@@ -62,6 +62,12 @@ function filterLogs(
|
|
|
62
62
|
|
|
63
63
|
const DEBOUNCE_MS = 50;
|
|
64
64
|
|
|
65
|
+
function buildLogsStreamUrl(sessionId: string | undefined): string {
|
|
66
|
+
if (sessionId === undefined) return "/api/logs/stream";
|
|
67
|
+
const params = new URLSearchParams({ sessionId });
|
|
68
|
+
return `/api/logs/stream?${params.toString()}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
65
71
|
export function ProxyViewerContainer({
|
|
66
72
|
initialSessionId,
|
|
67
73
|
}: {
|
|
@@ -142,12 +148,7 @@ export function ProxyViewerContainer({
|
|
|
142
148
|
eventSourceRef.current.close();
|
|
143
149
|
}
|
|
144
150
|
|
|
145
|
-
|
|
146
|
-
// from the complete set, so the SSE never needs to reopen on filter
|
|
147
|
-
// change. If the in-memory set ever grows past what the client can
|
|
148
|
-
// handle, swap this back to a parameterized URL and reconnect on
|
|
149
|
-
// filter change.
|
|
150
|
-
const es = new EventSource("/api/logs/stream");
|
|
151
|
+
const es = new EventSource(buildLogsStreamUrl(initialSessionId));
|
|
151
152
|
eventSourceRef.current = es;
|
|
152
153
|
|
|
153
154
|
es.onmessage = (event: MessageEvent) => {
|
|
@@ -193,7 +194,7 @@ export function ProxyViewerContainer({
|
|
|
193
194
|
}
|
|
194
195
|
reconnectTimeoutRef.current = setTimeout(connectSSE, 3000);
|
|
195
196
|
};
|
|
196
|
-
}, [scheduleUpdate]);
|
|
197
|
+
}, [initialSessionId, scheduleUpdate]);
|
|
197
198
|
|
|
198
199
|
useEffect(() => {
|
|
199
200
|
connectSSE();
|
|
@@ -214,9 +215,22 @@ export function ProxyViewerContainer({
|
|
|
214
215
|
}, [connectSSE]);
|
|
215
216
|
|
|
216
217
|
const handleClearAll = useCallback(() => {
|
|
218
|
+
if (initialSessionId !== undefined && allLogs.length === 0) return;
|
|
217
219
|
void (async () => {
|
|
218
220
|
try {
|
|
219
|
-
const
|
|
221
|
+
const body =
|
|
222
|
+
initialSessionId === undefined
|
|
223
|
+
? undefined
|
|
224
|
+
: JSON.stringify({ ids: allLogs.map((log) => log.id) });
|
|
225
|
+
const res = await fetch("/api/logs", {
|
|
226
|
+
method: "DELETE",
|
|
227
|
+
...(body === undefined
|
|
228
|
+
? {}
|
|
229
|
+
: {
|
|
230
|
+
headers: { "Content-Type": "application/json" },
|
|
231
|
+
body,
|
|
232
|
+
}),
|
|
233
|
+
});
|
|
220
234
|
if (!res.ok) {
|
|
221
235
|
setError("Failed to clear logs");
|
|
222
236
|
return;
|
|
@@ -228,7 +242,7 @@ export function ProxyViewerContainer({
|
|
|
228
242
|
setError(err instanceof Error ? err.message : "Unknown error clearing logs");
|
|
229
243
|
}
|
|
230
244
|
})();
|
|
231
|
-
}, []);
|
|
245
|
+
}, [allLogs, initialSessionId]);
|
|
232
246
|
|
|
233
247
|
const handleClearGroup = useCallback((ids: number[]) => {
|
|
234
248
|
if (ids.length === 0) return;
|
|
@@ -290,6 +304,7 @@ export function ProxyViewerContainer({
|
|
|
290
304
|
slowResponseThresholdSeconds={slowResponseThresholdSeconds}
|
|
291
305
|
// Session filter is the URL's job when `initialSessionId` was given.
|
|
292
306
|
hideSessionFilter={initialSessionId !== undefined}
|
|
307
|
+
pinnedSessionId={initialSessionId}
|
|
293
308
|
/>
|
|
294
309
|
</>
|
|
295
310
|
);
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
Zap,
|
|
12
12
|
} from "lucide-react";
|
|
13
13
|
import type { JSX } from "react";
|
|
14
|
+
import { getSessionPath } from "../../lib/sessionUrl";
|
|
14
15
|
import { cn, formatTokens } from "../../lib/utils";
|
|
15
16
|
import type { CapturedLog } from "../../proxy/schemas";
|
|
16
17
|
import { Badge } from "../ui/badge";
|
|
@@ -35,7 +36,7 @@ export type ConversationHeaderProps = {
|
|
|
35
36
|
expanded: boolean;
|
|
36
37
|
onToggle: () => void;
|
|
37
38
|
/** Hide the API format badge on the header (used when the group contains
|
|
38
|
-
* mixed formats
|
|
39
|
+
* mixed formats - the per-log badges are shown instead). */
|
|
39
40
|
hideApiFormat?: boolean;
|
|
40
41
|
/** When true and the group is collapsed, show a spinner instead of the
|
|
41
42
|
* expand chevron to indicate an in-flight request inside the group. */
|
|
@@ -78,23 +79,7 @@ export function ConversationHeader({
|
|
|
78
79
|
const handleOpenInNewTab = useCallback(
|
|
79
80
|
(e: React.MouseEvent | React.KeyboardEvent): void => {
|
|
80
81
|
e.stopPropagation();
|
|
81
|
-
|
|
82
|
-
// unreserved characters (A-Z a-z 0-9 - _) that survive Vite's
|
|
83
|
-
// URL normalisation without triggering a redirect loop.
|
|
84
|
-
// Falls back to percent-encoding when the id contains non-Latin-1
|
|
85
|
-
// characters (e.g. CJK paths in PID|folder format) that btoa()
|
|
86
|
-
// rejects.
|
|
87
|
-
let encoded: string;
|
|
88
|
-
try {
|
|
89
|
-
encoded = btoa(conversationId).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
90
|
-
} catch {
|
|
91
|
-
// Non-Latin-1 characters in the id — fall back to percent-encoding.
|
|
92
|
-
// These URLs may not survive Vite's URL normalisation but the
|
|
93
|
-
// situation is rare enough (CJK paths) to accept the edge case.
|
|
94
|
-
encoded = encodeURIComponent(conversationId);
|
|
95
|
-
}
|
|
96
|
-
const url = `/session/${encoded}`;
|
|
97
|
-
window.open(url, "_blank", "noopener,noreferrer");
|
|
82
|
+
window.open(getSessionPath(conversationId), "_blank", "noopener,noreferrer");
|
|
98
83
|
},
|
|
99
84
|
[conversationId],
|
|
100
85
|
);
|
|
@@ -121,7 +106,7 @@ export function ConversationHeader({
|
|
|
121
106
|
}
|
|
122
107
|
}}
|
|
123
108
|
>
|
|
124
|
-
{/* Expand chevron
|
|
109
|
+
{/* Expand chevron - shows spinner when collapsed and group has pending logs */}
|
|
125
110
|
{expanded ? (
|
|
126
111
|
<ChevronDown className="size-4 text-muted-foreground shrink-0" />
|
|
127
112
|
) : isLoading ? (
|
|
@@ -138,7 +123,7 @@ export function ConversationHeader({
|
|
|
138
123
|
{conversationId.startsWith("PID:") || conversationId.includes("|")
|
|
139
124
|
? conversationId
|
|
140
125
|
: conversationId.length > 24
|
|
141
|
-
? conversationId.slice(0, 12) + "
|
|
126
|
+
? conversationId.slice(0, 12) + "..." + conversationId.slice(-12)
|
|
142
127
|
: conversationId}
|
|
143
128
|
</span>
|
|
144
129
|
|
|
@@ -197,7 +182,7 @@ export function ConversationHeader({
|
|
|
197
182
|
{/* Spacer */}
|
|
198
183
|
<span className="flex-1 min-w-0" />
|
|
199
184
|
|
|
200
|
-
{/* Open this session in a new tab
|
|
185
|
+
{/* Open this session in a new tab - deep link to /session/$id */}
|
|
201
186
|
<button
|
|
202
187
|
type="button"
|
|
203
188
|
onClick={handleOpenInNewTab}
|
|
@@ -213,7 +198,7 @@ export function ConversationHeader({
|
|
|
213
198
|
<ExternalLink className="size-3.5" />
|
|
214
199
|
</button>
|
|
215
200
|
|
|
216
|
-
{/* Per-group Clear
|
|
201
|
+
{/* Per-group Clear - does not toggle the group's expand state */}
|
|
217
202
|
{onClear !== undefined && (
|
|
218
203
|
<button
|
|
219
204
|
type="button"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { JSX } from "react";
|
|
2
|
+
import { cn } from "../../lib/utils";
|
|
3
|
+
import McpLogoPng from "../../assets/logos/mcp.png";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Official Model Context Protocol logo (from Wikimedia Commons). The PNG
|
|
7
|
+
* is a black-on-white raster of the official mark — flipped to a white
|
|
8
|
+
* silhouette on the dark header via `invert` so it reads against the
|
|
9
|
+
* app background.
|
|
10
|
+
*/
|
|
11
|
+
export function McpLogo({ className }: { className?: string }): JSX.Element {
|
|
12
|
+
return (
|
|
13
|
+
<img
|
|
14
|
+
src={McpLogoPng}
|
|
15
|
+
alt="Model Context Protocol"
|
|
16
|
+
aria-hidden="true"
|
|
17
|
+
className={cn("inline-block size-8 object-contain invert", className)}
|
|
18
|
+
/>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const B64URL_RE = /^[A-Za-z0-9_-]+$/;
|
|
2
|
+
|
|
3
|
+
function bytesToBinary(bytes: Uint8Array): string {
|
|
4
|
+
let binary = "";
|
|
5
|
+
for (const byte of bytes) {
|
|
6
|
+
binary += String.fromCharCode(byte);
|
|
7
|
+
}
|
|
8
|
+
return binary;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function binaryToBytes(binary: string): Uint8Array {
|
|
12
|
+
const bytes = new Uint8Array(binary.length);
|
|
13
|
+
for (let i = 0; i < binary.length; i++) {
|
|
14
|
+
bytes[i] = binary.charCodeAt(i);
|
|
15
|
+
}
|
|
16
|
+
return bytes;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function encodeSessionIdForPath(sessionId: string): string {
|
|
20
|
+
const bytes = new TextEncoder().encode(sessionId);
|
|
21
|
+
const base64 = btoa(bytesToBinary(bytes));
|
|
22
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function decodeSessionIdFromPath(encoded: string): string {
|
|
26
|
+
if (encoded.startsWith("%")) {
|
|
27
|
+
return decodeURIComponent(encoded);
|
|
28
|
+
}
|
|
29
|
+
if (!B64URL_RE.test(encoded)) {
|
|
30
|
+
return encoded;
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const padded = encoded.padEnd(Math.ceil(encoded.length / 4) * 4, "=");
|
|
34
|
+
const base64 = padded.replace(/-/g, "+").replace(/_/g, "/");
|
|
35
|
+
const binary = atob(base64);
|
|
36
|
+
return new TextDecoder().decode(binaryToBytes(binary));
|
|
37
|
+
} catch {
|
|
38
|
+
return encoded;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function getSessionPath(sessionId: string): string {
|
|
43
|
+
return `/session/${encodeSessionIdForPath(sessionId)}`;
|
|
44
|
+
}
|
|
@@ -1,71 +1,19 @@
|
|
|
1
1
|
import { createFileRoute } from "@tanstack/react-router";
|
|
2
2
|
import { ProxyViewerContainer } from "../../components/ProxyViewerContainer";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Base64url-decode a path segment back to the original conversation id.
|
|
6
|
-
* Base64url uses `-` instead of `+`, `_` instead of `/`, and omits padding.
|
|
7
|
-
*
|
|
8
|
-
* Falls back to `decodeURIComponent` for legacy percent-encoded URLs (e.g.
|
|
9
|
-
* plain UUIDs or "default" that survived the Vite dev-server redirect).
|
|
10
|
-
*/
|
|
11
|
-
/** Only characters that can appear in a base64url-encoded payload. */
|
|
12
|
-
const B64URL_RE = /^[A-Za-z0-9_-]+$/;
|
|
13
|
-
|
|
14
|
-
function b64urlDecode(encoded: string): string {
|
|
15
|
-
// Legacy percent-encoded URLs — may still work for simple ids.
|
|
16
|
-
if (encoded.startsWith("%")) {
|
|
17
|
-
return decodeURIComponent(encoded);
|
|
18
|
-
}
|
|
19
|
-
// Guard against non-base64url input — atob() doesn't always throw on invalid
|
|
20
|
-
// input in all browsers and can return binary garbage.
|
|
21
|
-
if (!B64URL_RE.test(encoded)) {
|
|
22
|
-
return encoded;
|
|
23
|
-
}
|
|
24
|
-
try {
|
|
25
|
-
const base64 = encoded.replace(/-/g, "+").replace(/_/g, "/");
|
|
26
|
-
const decoded = atob(base64);
|
|
27
|
-
// atob() on a random alphanum string can produce binary garbage in
|
|
28
|
-
// browsers that auto-pad. Check that every character is printable
|
|
29
|
-
// (tab, newline, carriage return, and space through DEL are fine;
|
|
30
|
-
// anything else — control chars, high bytes — signals garbage).
|
|
31
|
-
for (let i = 0; i < decoded.length; i++) {
|
|
32
|
-
const c = decoded.charCodeAt(i);
|
|
33
|
-
if (c < 0x09 || c === 0x0b || c === 0x0c || (c > 0x0d && c < 0x20) || c >= 0x7f) {
|
|
34
|
-
return encoded;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
return decoded;
|
|
38
|
-
} catch {
|
|
39
|
-
return encoded;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** Base64url-encode a conversation id so the path survives Vite's URL
|
|
44
|
-
* normalisation (which `decodeURI`s the path and 307-redirects when the
|
|
45
|
-
* result differs — causing an infinite redirect for chars like `{` `"` `}`).
|
|
46
|
-
*/
|
|
47
|
-
function b64urlEncode(raw: string): string {
|
|
48
|
-
return btoa(raw).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
49
|
-
}
|
|
3
|
+
import { decodeSessionIdFromPath, encodeSessionIdForPath } from "../../lib/sessionUrl";
|
|
50
4
|
|
|
51
5
|
/**
|
|
52
6
|
* Per-session deep link: opens a page that is pre-scoped to a single
|
|
53
|
-
* session id
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* The session id is base64url-encoded in the URL so JSON session blobs
|
|
57
|
-
* (e.g. Claude Code's
|
|
58
|
-
* `{"device_id":"...","account_uuid":"","session_id":"..."}`) survive
|
|
59
|
-
* the Vite dev-server's URL normalisation without triggering a redirect
|
|
60
|
-
* loop.
|
|
7
|
+
* session id. The path segment is base64url-encoded so JSON session blobs
|
|
8
|
+
* and Unicode project paths survive dev-server URL normalization.
|
|
61
9
|
*/
|
|
62
10
|
export const Route = createFileRoute("/session/$sessionId")({
|
|
63
11
|
component: SessionViewerRoute,
|
|
64
12
|
parseParams: (params) => ({
|
|
65
|
-
sessionId:
|
|
13
|
+
sessionId: decodeSessionIdFromPath(params.sessionId),
|
|
66
14
|
}),
|
|
67
15
|
stringifyParams: (params) => ({
|
|
68
|
-
sessionId:
|
|
16
|
+
sessionId: encodeSessionIdForPath(params.sessionId),
|
|
69
17
|
}),
|
|
70
18
|
});
|
|
71
19
|
|