@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.
Files changed (39) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/{CompareDrawer-CAhlM_Gq.js → CompareDrawer-C-4ypEWs.js} +1 -1
  3. package/.output/public/assets/ProxyViewerContainer-WRenRpeh.js +101 -0
  4. package/.output/public/assets/{ReplayDialog-Bqu2f5HE.js → ReplayDialog-CyBKOgba.js} +1 -1
  5. package/.output/public/assets/{RequestAnatomy-CpVNH0CD.js → RequestAnatomy-C0IrVQ3q.js} +1 -1
  6. package/.output/public/assets/{ResponseView-B_Gg37Lr.js → ResponseView-MogToC4i.js} +1 -1
  7. package/.output/public/assets/{StreamingChunkSequence-E2M_SS1A.js → StreamingChunkSequence-ClhUhT-s.js} +1 -1
  8. package/.output/public/assets/_sessionId-BO47oA3Z.js +1 -0
  9. package/.output/public/assets/index-BRvz6-L6.css +1 -0
  10. package/.output/public/assets/index-Btw8ec7-.js +1 -0
  11. package/.output/public/assets/{json-viewer-DqhA-ODG.js → json-viewer-BicGakI5.js} +1 -1
  12. package/.output/public/assets/{main-DpH7JlHv.js → main-Be2qqUUW.js} +7 -7
  13. package/.output/server/_libs/lucide-react.mjs +20 -14
  14. package/.output/server/{_sessionId-DcJ0RDNl.mjs → _sessionId-DhKJIdQC.mjs} +3 -3
  15. package/.output/server/_ssr/{CompareDrawer-DajC3x7u.mjs → CompareDrawer-BGUgukJ8.mjs} +5 -5
  16. package/.output/server/_ssr/{ProxyViewerContainer-C2dnFXoC.mjs → ProxyViewerContainer--3K3o3Sm.mjs} +212 -74
  17. package/.output/server/_ssr/{ReplayDialog-BnCLuA5z.mjs → ReplayDialog-Bo86xZI4.mjs} +5 -5
  18. package/.output/server/_ssr/{RequestAnatomy-OHE3iT-f.mjs → RequestAnatomy-jRU5qgwB.mjs} +4 -4
  19. package/.output/server/_ssr/{ResponseView-NPshHwOv.mjs → ResponseView-DdO_-79a.mjs} +5 -5
  20. package/.output/server/_ssr/{StreamingChunkSequence-BfukoR7F.mjs → StreamingChunkSequence-BigLwhh4.mjs} +5 -5
  21. package/.output/server/_ssr/{index-CF8M0tsv.mjs → index-BHG6vOnr.mjs} +3 -3
  22. package/.output/server/_ssr/index.mjs +2 -2
  23. package/.output/server/_ssr/{json-viewer-CHBa-Oas.mjs → json-viewer-B4c_WjXD.mjs} +4 -4
  24. package/.output/server/_ssr/{router-B5hOtKSn.mjs → router-DVixpJO-.mjs} +32 -17
  25. package/.output/server/{_tanstack-start-manifest_v-CFyWvIH6.mjs → _tanstack-start-manifest_v-BbvWUF4v.mjs} +1 -1
  26. package/.output/server/index.mjs +63 -63
  27. package/README.md +109 -59
  28. package/package.json +1 -1
  29. package/src/assets/logos/mcp.png +0 -0
  30. package/src/components/ProxyViewer.tsx +203 -53
  31. package/src/components/ProxyViewerContainer.tsx +24 -9
  32. package/src/components/proxy-viewer/ConversationHeader.tsx +7 -22
  33. package/src/components/ui/mcp-logo.tsx +20 -0
  34. package/src/lib/sessionUrl.ts +44 -0
  35. package/src/routes/session/$sessionId.tsx +5 -57
  36. package/.output/public/assets/ProxyViewerContainer--miVHNPZ.js +0 -101
  37. package/.output/public/assets/_sessionId-P9LgC1bF.js +0 -1
  38. package/.output/public/assets/index-C0wv3YP9.css +0 -1
  39. 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
- // Stable, unfiltered connection. The frontend derives filtered views
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 res = await fetch("/api/logs", { method: "DELETE" });
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 the per-log badges are shown instead). */
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
- // Base64url-encode the session id so the path contains only
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 shows spinner when collapsed and group has pending logs */}
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) + "" + conversationId.slice(-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 deep link to /session/$id */}
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 does not toggle the group's expand state */}
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, sharing the same SSE stream and `useStripConfig` settings
54
- * as the main `/` view.
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: b64urlDecode(params.sessionId),
13
+ sessionId: decodeSessionIdFromPath(params.sessionId),
66
14
  }),
67
15
  stringifyParams: (params) => ({
68
- sessionId: b64urlEncode(params.sessionId),
16
+ sessionId: encodeSessionIdForPath(params.sessionId),
69
17
  }),
70
18
  });
71
19