@tonyclaw/agent-inspector 3.0.14 → 3.0.16

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 (42) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/{CompareDrawer-DDfwHQUi.js → CompareDrawer-BJmxAiM0.js} +1 -1
  3. package/.output/public/assets/ProxyViewerContainer-C40Nig73.js +106 -0
  4. package/.output/public/assets/{ReplayDialog-D-26DMBt.js → ReplayDialog-CkLdOMJr.js} +1 -1
  5. package/.output/public/assets/{RequestAnatomy-B6zNFqNX.js → RequestAnatomy-DemOFfSc.js} +1 -1
  6. package/.output/public/assets/{ResponseView-Ccy95rWI.js → ResponseView-9LhpuDo7.js} +2 -2
  7. package/.output/public/assets/{StreamingChunkSequence-yIiscGuE.js → StreamingChunkSequence-RRWYBOdz.js} +1 -1
  8. package/.output/public/assets/{_sessionId-BnMB0zab.js → _sessionId-CWEh2Jsn.js} +1 -1
  9. package/.output/public/assets/index-HRfrFVYw.css +1 -0
  10. package/.output/public/assets/{index-BbtAIxbg.js → index-Lwir70Rs.js} +1 -1
  11. package/.output/public/assets/index-hjNWJZGE.js +1 -0
  12. package/.output/public/assets/{json-viewer-C838cVmD.js → json-viewer-D8gvbDlB.js} +1 -1
  13. package/.output/public/assets/{main-BfN4culo.js → main-ovrz0tyD.js} +7 -7
  14. package/.output/server/_libs/lucide-react.mjs +215 -201
  15. package/.output/server/{_sessionId-DoprmwZV.mjs → _sessionId-zI_GFX_7.mjs} +2 -2
  16. package/.output/server/_ssr/{CompareDrawer-YMLJNxXR.mjs → CompareDrawer-Ckcx1RVu.mjs} +3 -3
  17. package/.output/server/_ssr/{ProxyViewerContainer-C62eZnva.mjs → ProxyViewerContainer-BgXewlIr.mjs} +729 -378
  18. package/.output/server/_ssr/{ReplayDialog-NmN7Ukjo.mjs → ReplayDialog-wx3nDtZn.mjs} +4 -4
  19. package/.output/server/_ssr/{RequestAnatomy-Dawm2qxe.mjs → RequestAnatomy-DN80zt0G.mjs} +3 -3
  20. package/.output/server/_ssr/{ResponseView-BHyAY83t.mjs → ResponseView-DD7rTu_l.mjs} +3 -3
  21. package/.output/server/_ssr/{StreamingChunkSequence-C4qgcJbg.mjs → StreamingChunkSequence-DAgz6Vvw.mjs} +3 -3
  22. package/.output/server/_ssr/{index-B2Pf6W_T.mjs → index-dcTjp8du.mjs} +14 -5
  23. package/.output/server/_ssr/index.mjs +2 -2
  24. package/.output/server/_ssr/{json-viewer-u67BASf8.mjs → json-viewer-N2cuzGWR.mjs} +3 -3
  25. package/.output/server/_ssr/{router-CZhFVMGN.mjs → router-BfQ0QsD5.mjs} +132 -17
  26. package/.output/server/{_tanstack-start-manifest_v-Dk2HfOoe.mjs → _tanstack-start-manifest_v-BXnYrbcJ.mjs} +1 -1
  27. package/.output/server/index.mjs +75 -75
  28. package/package.json +1 -1
  29. package/src/components/ProxyViewer.tsx +475 -197
  30. package/src/components/ProxyViewerContainer.tsx +145 -17
  31. package/src/components/proxy-viewer/ToolTraceEvents.tsx +36 -10
  32. package/src/components/proxy-viewer/TurnGroup.tsx +4 -1
  33. package/src/lib/chunkLoadRecovery.ts +83 -0
  34. package/src/proxy/formats/openai/codexMetadata.ts +61 -0
  35. package/src/proxy/formats/openai/handler.ts +8 -2
  36. package/src/routes/__root.tsx +6 -3
  37. package/src/routes/index.tsx +25 -1
  38. package/src/routes/session/$sessionId.tsx +1 -0
  39. package/styles/globals.css +54 -0
  40. package/.output/public/assets/ProxyViewerContainer-CbNsaoQw.js +0 -106
  41. package/.output/public/assets/index-49RC4dlD.js +0 -1
  42. package/.output/public/assets/index-DXKupW0r.css +0 -1
@@ -2,7 +2,11 @@ import { useState, useEffect, useCallback, useRef, useMemo, useTransition, type
2
2
  import { z } from "zod";
3
3
  import { CapturedLogSchema, type CapturedLog } from "../contracts";
4
4
  import { fetchJson, fetchJsonWithTimeout } from "../lib/apiClient";
5
- import { InspectorGroupsListResponseSchema, type InspectorGroup } from "../lib/groupContract";
5
+ import {
6
+ DeleteInspectorGroupsResponseSchema,
7
+ InspectorGroupsListResponseSchema,
8
+ type InspectorGroup,
9
+ } from "../lib/groupContract";
6
10
  import { ImportLogsResponseSchema, type ImportLogsResponse } from "../lib/logImportContract";
7
11
  import { useStripConfig } from "../lib/useStripConfig";
8
12
  import { OnboardingBanner } from "./OnboardingBanner";
@@ -58,6 +62,11 @@ const DeleteLogsResponseSchema = z.object({
58
62
  sessionArchiveLogsDeleted: z.number().int().nonnegative().optional(),
59
63
  });
60
64
 
65
+ type GroupCleanupResult = {
66
+ deleted: number;
67
+ ok: boolean;
68
+ };
69
+
61
70
  type SessionPageRequest =
62
71
  | { kind: "newest" }
63
72
  | { kind: "oldest" }
@@ -190,6 +199,50 @@ function fetchSessionLogsPage(
190
199
  );
191
200
  }
192
201
 
202
+ type SessionLogIdCollection = {
203
+ ids: number[];
204
+ truncated: boolean;
205
+ };
206
+
207
+ async function collectSessionLogIds(
208
+ sessionId: string,
209
+ onProgress: (logCount: number, pageCount: number) => void,
210
+ ): Promise<SessionLogIdCollection> {
211
+ const ids = new Set<number>();
212
+ let page = await fetchSessionLogsPage(sessionId, { kind: "oldest" }, SESSION_RELOAD_PAGE_LIMIT);
213
+ for (const log of page.logs) {
214
+ ids.add(log.id);
215
+ }
216
+ onProgress(ids.size, 1);
217
+
218
+ let loadedPages = 1;
219
+ while (page.hasNewer && page.newestLogId !== null && loadedPages < MAX_SESSION_RELOAD_PAGES) {
220
+ page = await fetchSessionLogsPage(
221
+ sessionId,
222
+ { kind: "newer", afterLogId: page.newestLogId },
223
+ SESSION_RELOAD_PAGE_LIMIT,
224
+ );
225
+ for (const log of page.logs) {
226
+ ids.add(log.id);
227
+ }
228
+ loadedPages += 1;
229
+ onProgress(ids.size, loadedPages);
230
+ }
231
+
232
+ return { ids: [...ids], truncated: page.hasNewer };
233
+ }
234
+
235
+ async function deleteAllGroupsForFullHistoryDelete(): Promise<GroupCleanupResult> {
236
+ try {
237
+ const result = await fetchJson("/api/groups", DeleteInspectorGroupsResponseSchema, {
238
+ method: "DELETE",
239
+ });
240
+ return { deleted: result.deleted, ok: true };
241
+ } catch {
242
+ return { deleted: 0, ok: false };
243
+ }
244
+ }
245
+
193
246
  export function mergeLogsById(
194
247
  previousLogs: readonly CapturedLog[],
195
248
  incomingLogs: readonly CapturedLog[],
@@ -685,27 +738,88 @@ export function ProxyViewerContainer({
685
738
  [showToast],
686
739
  );
687
740
 
688
- const handleDeleteCurrentView = useCallback(
689
- async (ids: number[]): Promise<void> => {
690
- if (ids.length === 0) return;
691
- const persistedIds = ids.filter((id) => Number.isInteger(id) && id >= 0);
741
+ const handleDeleteCurrentView = useCallback(async (): Promise<void> => {
742
+ const deleteRequestId = sessionPageRequestIdRef.current + 1;
743
+ sessionPageRequestIdRef.current = deleteRequestId;
744
+ setSessionPageLoading(true);
745
+ stopSessionLoadProgress();
746
+ dismissToast();
747
+
748
+ try {
749
+ if (selectedSession === "__all__") {
750
+ startSessionLoadProgress("Deleting stored history", deleteRequestId);
751
+ const result = await fetchJson("/api/logs", DeleteLogsResponseSchema, {
752
+ method: "DELETE",
753
+ });
754
+ const groupCleanup = await deleteAllGroupsForFullHistoryDelete();
755
+ if (sessionPageRequestIdRef.current !== deleteRequestId) return;
756
+ setAllLogs([]);
757
+ logIndexRef.current.clear();
758
+ setLogPage(null);
759
+ setKnownSessions([]);
760
+ setSessionMemberships([]);
761
+ setSessionPageLoading(false);
762
+ stopSessionLoadProgress();
763
+ const groupMessage = groupCleanup.ok
764
+ ? ` Cleared ${groupCleanup.deleted.toString()} group${
765
+ groupCleanup.deleted === 1 ? "" : "s"
766
+ }.`
767
+ : " Group cleanup failed; open Groups to retry Clear.";
768
+ showToast({
769
+ message: `Deleted ${result.cleared.toString()} stored log${
770
+ result.cleared === 1 ? "" : "s"
771
+ } across all sessions.${groupMessage}`,
772
+ tone: groupCleanup.ok ? "success" : "error",
773
+ });
774
+ return;
775
+ }
776
+
777
+ const sessionId = selectedSession;
778
+ startSessionLoadProgress("Deleting session history", deleteRequestId);
779
+ const collected = await collectSessionLogIds(sessionId, (logCount, pageCount) => {
780
+ updateSessionLoadProgress("Deleting session history", deleteRequestId, logCount, pageCount);
781
+ });
782
+ const idSet = new Set(collected.ids);
783
+ for (const log of allLogs) {
784
+ if (log.sessionId === sessionId) {
785
+ idSet.add(log.id);
786
+ }
787
+ }
788
+ const persistedIds = [...idSet].filter((id) => Number.isInteger(id) && id >= 0);
692
789
  if (persistedIds.length === 0) {
790
+ if (sessionPageRequestIdRef.current !== deleteRequestId) return;
791
+ setAllLogs((prev) => {
792
+ const remaining = prev.filter((log) => log.sessionId !== sessionId);
793
+ logIndexRef.current = buildLogIndex(remaining);
794
+ return remaining;
795
+ });
796
+ setLogPage((prev) =>
797
+ prev === null
798
+ ? null
799
+ : {
800
+ ...prev,
801
+ logs: prev.logs.filter((log) => log.sessionId !== sessionId),
802
+ },
803
+ );
804
+ setKnownSessions((prev) => prev.filter((session) => session !== sessionId));
805
+ setSessionPageLoading(false);
806
+ stopSessionLoadProgress();
693
807
  showToast({
694
- message: "No persisted log records were found for this view.",
695
- tone: "error",
808
+ message: "No stored log records were found for this session.",
809
+ tone: "success",
696
810
  });
697
811
  return;
698
812
  }
699
- const idSet = new Set(persistedIds);
700
- const removed = allLogs.filter((log) => idSet.has(log.id));
813
+
701
814
  const result = await fetchJson("/api/logs", DeleteLogsResponseSchema, {
702
815
  method: "DELETE",
703
816
  headers: { "content-type": "application/json" },
704
817
  body: JSON.stringify({ ids: persistedIds }),
705
818
  });
706
819
 
820
+ if (sessionPageRequestIdRef.current !== deleteRequestId) return;
707
821
  setAllLogs((prev) => {
708
- const remaining = prev.filter((log) => !idSet.has(log.id));
822
+ const remaining = prev.filter((log) => log.sessionId !== sessionId);
709
823
  logIndexRef.current = buildLogIndex(remaining);
710
824
  return remaining;
711
825
  });
@@ -714,19 +828,33 @@ export function ProxyViewerContainer({
714
828
  ? null
715
829
  : {
716
830
  ...prev,
717
- logs: prev.logs.filter((log) => !idSet.has(log.id)),
831
+ logs: prev.logs.filter((log) => log.sessionId !== sessionId),
718
832
  },
719
833
  );
720
- setKnownSessions((prev) => mergeSessionIds(prev, extractSessions(removed)));
834
+ setKnownSessions((prev) => prev.filter((session) => session !== sessionId));
835
+ setSessionPageLoading(false);
836
+ stopSessionLoadProgress();
721
837
  showToast({
722
838
  message: `Deleted ${result.cleared.toString()} stored log${
723
839
  result.cleared === 1 ? "" : "s"
724
- } from the current view.`,
725
- tone: "success",
840
+ } for this session${collected.truncated ? ". More history may remain." : "."}`,
841
+ tone: collected.truncated ? "error" : "success",
726
842
  });
727
- },
728
- [allLogs, showToast],
729
- );
843
+ } finally {
844
+ if (sessionPageRequestIdRef.current === deleteRequestId) {
845
+ setSessionPageLoading(false);
846
+ stopSessionLoadProgress();
847
+ }
848
+ }
849
+ }, [
850
+ allLogs,
851
+ dismissToast,
852
+ selectedSession,
853
+ showToast,
854
+ startSessionLoadProgress,
855
+ stopSessionLoadProgress,
856
+ updateSessionLoadProgress,
857
+ ]);
730
858
 
731
859
  const handleReloadSession = useCallback(() => {
732
860
  if (initialSessionId !== undefined) {
@@ -1,18 +1,21 @@
1
1
  import { type JSX } from "react";
2
- import { Wrench } from "lucide-react";
2
+ import { AlertTriangle, Wrench } from "lucide-react";
3
3
  import type { ToolTraceEvent } from "./viewerState";
4
4
  import { dispatchLogFocusRequest } from "./logFocus";
5
5
 
6
6
  type ToolTraceEventsProps = {
7
7
  events: ToolTraceEvent[];
8
+ warnings?: readonly string[];
8
9
  };
9
10
 
11
+ const VISIBLE_TOOL_LIMIT = 4;
12
+
10
13
  function ToolTraceEventChip({ event }: { event: ToolTraceEvent }): JSX.Element {
11
14
  return (
12
15
  <button
13
16
  type="button"
14
17
  key={event.id}
15
- className="inline-flex min-w-0 items-center gap-1.5 rounded-md bg-white/[0.035] px-2 py-1 text-xs text-muted-foreground transition-colors hover:bg-sky-400/[0.08] hover:text-sky-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
18
+ className="inline-flex h-6 max-w-[11rem] min-w-0 items-center gap-1 rounded-md bg-white/[0.028] px-1.5 text-[11px] text-muted-foreground transition-colors hover:bg-sky-400/[0.08] hover:text-sky-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
16
19
  onClick={() =>
17
20
  dispatchLogFocusRequest({
18
21
  logId: event.logId,
@@ -21,21 +24,44 @@ function ToolTraceEventChip({ event }: { event: ToolTraceEvent }): JSX.Element {
21
24
  })
22
25
  }
23
26
  aria-label={`Open response tool call ${event.name}`}
24
- title={`Open ${event.name} in Response`}
25
27
  >
26
- <Wrench className="size-3.5 shrink-0 text-sky-400/70" />
28
+ <Wrench className="size-3 shrink-0 text-sky-400/65" />
27
29
  <span className="min-w-0 truncate font-mono font-semibold">{event.name}</span>
28
30
  </button>
29
31
  );
30
32
  }
31
33
 
32
- export function ToolTraceEvents({ events }: ToolTraceEventsProps): JSX.Element | null {
33
- if (events.length === 0) return null;
34
+ export function ToolTraceEvents({
35
+ events,
36
+ warnings = [],
37
+ }: ToolTraceEventsProps): JSX.Element | null {
38
+ if (events.length === 0 && warnings.length === 0) return null;
39
+ const visibleEvents = events.slice(0, VISIBLE_TOOL_LIMIT);
40
+ const hiddenCount = Math.max(0, events.length - visibleEvents.length);
34
41
  return (
35
- <div className="mx-3 mb-2 flex flex-wrap gap-1.5">
36
- {events.map((event) => (
37
- <ToolTraceEventChip key={event.id} event={event} />
38
- ))}
42
+ <div className="mx-3 mb-2 flex min-w-0 items-center gap-1.5 overflow-hidden rounded-md bg-black/[0.10] px-2 py-1 shadow-[inset_0_1px_0_rgba(255,255,255,0.025)]">
43
+ {events.length > 0 && (
44
+ <span className="inline-flex h-6 shrink-0 items-center gap-1 rounded-md px-1.5 font-mono text-[10px] font-semibold uppercase tracking-[0.12em] text-sky-300/70">
45
+ <Wrench className="size-3" />
46
+ {events.length}
47
+ </span>
48
+ )}
49
+ {warnings.length > 0 && (
50
+ <span className="inline-flex h-6 shrink-0 items-center gap-1 rounded-md border border-amber-300/20 bg-amber-300/[0.08] px-1.5 font-mono text-[10px] font-semibold text-amber-100">
51
+ <AlertTriangle className="size-3" />
52
+ {warnings.length}
53
+ </span>
54
+ )}
55
+ <div className="flex min-w-0 flex-1 items-center gap-1 overflow-hidden">
56
+ {visibleEvents.map((event) => (
57
+ <ToolTraceEventChip key={event.id} event={event} />
58
+ ))}
59
+ {hiddenCount > 0 && (
60
+ <span className="inline-flex h-6 shrink-0 items-center rounded-md bg-white/[0.025] px-1.5 font-mono text-[11px] font-semibold text-muted-foreground">
61
+ +{hiddenCount}
62
+ </span>
63
+ )}
64
+ </div>
39
65
  </div>
40
66
  );
41
67
  }
@@ -361,7 +361,10 @@ export const TurnGroup = memo(function TurnGroup({
361
361
  comparisonPredecessors.has(log.id) ? onCompareWithPrevious : undefined
362
362
  }
363
363
  />
364
- <ToolTraceEvents events={toolEventsByLogId.get(log.id) ?? []} />
364
+ <ToolTraceEvents
365
+ events={toolEventsByLogId.get(log.id) ?? []}
366
+ warnings={log.warnings ?? []}
367
+ />
365
368
  </div>
366
369
  </div>
367
370
  );
@@ -0,0 +1,83 @@
1
+ const CHUNK_LOAD_RELOAD_AT_KEY = "agent-inspector.chunkLoadReloadAt";
2
+ const CHUNK_LOAD_RELOAD_WINDOW_MS = 15_000;
3
+
4
+ const CHUNK_LOAD_FAILURE_PATTERNS = [
5
+ "failed to fetch dynamically imported module",
6
+ "failed to load module script",
7
+ "importing a module script failed",
8
+ "error loading dynamically imported module",
9
+ "loading chunk",
10
+ "unable to preload css",
11
+ ];
12
+
13
+ function messageFromReason(reason: unknown): string {
14
+ if (reason instanceof Error) {
15
+ return reason.message;
16
+ }
17
+
18
+ if (typeof reason === "string") {
19
+ return reason;
20
+ }
21
+
22
+ return "";
23
+ }
24
+
25
+ function isChunkLoadFailureMessage(message: string): boolean {
26
+ const normalizedMessage = message.toLowerCase();
27
+ return CHUNK_LOAD_FAILURE_PATTERNS.some((pattern) => normalizedMessage.includes(pattern));
28
+ }
29
+
30
+ function readLastReloadAt(): number {
31
+ const storedValue = window.sessionStorage.getItem(CHUNK_LOAD_RELOAD_AT_KEY);
32
+ if (storedValue === null) {
33
+ return 0;
34
+ }
35
+
36
+ const parsedValue = Number(storedValue);
37
+ return Number.isFinite(parsedValue) ? parsedValue : 0;
38
+ }
39
+
40
+ function reloadOnceForChunkFailure(): void {
41
+ const now = Date.now();
42
+ const lastReloadAt = readLastReloadAt();
43
+ if (now - lastReloadAt < CHUNK_LOAD_RELOAD_WINDOW_MS) {
44
+ return;
45
+ }
46
+
47
+ window.sessionStorage.setItem(CHUNK_LOAD_RELOAD_AT_KEY, String(now));
48
+ window.location.reload();
49
+ }
50
+
51
+ export function installChunkLoadRecovery(): () => void {
52
+ if (typeof window === "undefined") {
53
+ return () => {};
54
+ }
55
+
56
+ const handleUnhandledRejection = (event: PromiseRejectionEvent): void => {
57
+ const message = messageFromReason(event.reason);
58
+ if (!isChunkLoadFailureMessage(message)) {
59
+ return;
60
+ }
61
+
62
+ event.preventDefault();
63
+ reloadOnceForChunkFailure();
64
+ };
65
+
66
+ const handleError = (event: ErrorEvent): void => {
67
+ const message = event.message !== "" ? event.message : messageFromReason(event.error);
68
+ if (!isChunkLoadFailureMessage(message)) {
69
+ return;
70
+ }
71
+
72
+ event.preventDefault();
73
+ reloadOnceForChunkFailure();
74
+ };
75
+
76
+ window.addEventListener("unhandledrejection", handleUnhandledRejection);
77
+ window.addEventListener("error", handleError);
78
+
79
+ return () => {
80
+ window.removeEventListener("unhandledrejection", handleUnhandledRejection);
81
+ window.removeEventListener("error", handleError);
82
+ };
83
+ }
@@ -0,0 +1,61 @@
1
+ type CodexClientMetadata = {
2
+ sessionId: string | null;
3
+ threadId: string | null;
4
+ turnId: string | null;
5
+ installationId: string | null;
6
+ windowId: string | null;
7
+ };
8
+
9
+ const EMPTY_CODEX_CLIENT_METADATA: CodexClientMetadata = {
10
+ sessionId: null,
11
+ threadId: null,
12
+ turnId: null,
13
+ installationId: null,
14
+ windowId: null,
15
+ };
16
+
17
+ function readObjectField(value: unknown, field: string): unknown {
18
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
19
+ return Object.getOwnPropertyDescriptor(value, field)?.value;
20
+ }
21
+
22
+ function readStringField(value: unknown, field: string): string | null {
23
+ const fieldValue = readObjectField(value, field);
24
+ return typeof fieldValue === "string" && fieldValue.length > 0 ? fieldValue : null;
25
+ }
26
+
27
+ function parseNestedTurnMetadata(value: unknown): unknown {
28
+ if (typeof value !== "string" || value.length === 0) return null;
29
+ try {
30
+ return JSON.parse(value);
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+
36
+ export function extractCodexClientMetadata(rawRequest: unknown): CodexClientMetadata {
37
+ const clientMetadata = readObjectField(rawRequest, "client_metadata");
38
+ if (clientMetadata === undefined) return EMPTY_CODEX_CLIENT_METADATA;
39
+
40
+ const turnMetadata = parseNestedTurnMetadata(
41
+ readObjectField(clientMetadata, "x-codex-turn-metadata"),
42
+ );
43
+
44
+ return {
45
+ sessionId:
46
+ readStringField(clientMetadata, "session_id") ?? readStringField(turnMetadata, "session_id"),
47
+ threadId:
48
+ readStringField(clientMetadata, "thread_id") ?? readStringField(turnMetadata, "thread_id"),
49
+ turnId: readStringField(clientMetadata, "turn_id") ?? readStringField(turnMetadata, "turn_id"),
50
+ installationId:
51
+ readStringField(clientMetadata, "x-codex-installation-id") ??
52
+ readStringField(turnMetadata, "installation_id"),
53
+ windowId:
54
+ readStringField(clientMetadata, "x-codex-window-id") ??
55
+ readStringField(turnMetadata, "window_id"),
56
+ };
57
+ }
58
+
59
+ export function extractCodexSessionId(rawRequest: unknown): string | null {
60
+ return extractCodexClientMetadata(rawRequest).sessionId;
61
+ }
@@ -7,6 +7,7 @@ import {
7
7
  parseOpenAIResponse,
8
8
  } from "./schemas";
9
9
  import { extractOpenAIStream } from "./stream";
10
+ import { extractCodexSessionId } from "./codexMetadata";
10
11
 
11
12
  function emptyTokens(): TokenUsage {
12
13
  return {
@@ -17,6 +18,11 @@ function emptyTokens(): TokenUsage {
17
18
  };
18
19
  }
19
20
 
21
+ function headerSessionId(headers: Headers | undefined): string | null {
22
+ if (headers === undefined) return null;
23
+ return headers.get("x-agent-inspector-session-id") ?? headers.get("x-session-affinity");
24
+ }
25
+
20
26
  export const OpenAIFormatHandler: FormatHandler = {
21
27
  format: "openai",
22
28
 
@@ -27,14 +33,14 @@ export const OpenAIFormatHandler: FormatHandler = {
27
33
  if (result.success) {
28
34
  return {
29
35
  model: result.data.model,
30
- sessionId: headers?.get("x-session-affinity") ?? null,
36
+ sessionId: extractCodexSessionId(json) ?? headerSessionId(headers),
31
37
  };
32
38
  }
33
39
  const responsesResult = OpenAIResponsesRequestSchema.safeParse(json);
34
40
  if (responsesResult.success) {
35
41
  return {
36
42
  model: responsesResult.data.model,
37
- sessionId: headers?.get("x-session-affinity") ?? null,
43
+ sessionId: extractCodexSessionId(json) ?? headerSessionId(headers),
38
44
  };
39
45
  }
40
46
  return null;
@@ -1,9 +1,10 @@
1
1
  /// <reference types="vite/client" />
2
2
  import { Outlet, createRootRoute, HeadContent, Scripts } from "@tanstack/react-router";
3
- import type { ReactNode } from "react";
3
+ import { useEffect, type ReactNode } from "react";
4
4
  import { SWRConfig } from "swr";
5
5
  import faviconSvg from "../assets/favicon.svg?url";
6
6
  import appCss from "../index.css?url";
7
+ import { installChunkLoadRecovery } from "../lib/chunkLoadRecovery";
7
8
 
8
9
  export const Route = createRootRoute({
9
10
  head: () => ({
@@ -22,6 +23,8 @@ export const Route = createRootRoute({
22
23
  });
23
24
 
24
25
  function RootComponent() {
26
+ useEffect(() => installChunkLoadRecovery(), []);
27
+
25
28
  return (
26
29
  <RootDocument>
27
30
  <Outlet />
@@ -57,11 +60,11 @@ function RootNotFoundComponent() {
57
60
 
58
61
  function RootDocument({ children }: Readonly<{ children: ReactNode }>) {
59
62
  return (
60
- <html lang="en" className="dark">
63
+ <html lang="en" className="dark" suppressHydrationWarning={true}>
61
64
  <head>
62
65
  <HeadContent />
63
66
  </head>
64
- <body>
67
+ <body suppressHydrationWarning={true}>
65
68
  <SWRConfig value={{ revalidateOnFocus: false, revalidateIfStale: false }}>
66
69
  {children}
67
70
  </SWRConfig>
@@ -1,6 +1,30 @@
1
1
  import { createFileRoute } from "@tanstack/react-router";
2
+ import { useEffect, useState, type JSX } from "react";
2
3
  import { ProxyViewerContainer } from "../components/ProxyViewerContainer";
3
4
 
4
5
  export const Route = createFileRoute("/")({
5
- component: ProxyViewerContainer,
6
+ ssr: false,
7
+ component: ClientOnlyProxyViewerRoute,
6
8
  });
9
+
10
+ function ClientOnlyProxyViewerRoute(): JSX.Element {
11
+ const [mounted, setMounted] = useState(false);
12
+
13
+ useEffect(() => {
14
+ setMounted(true);
15
+ }, []);
16
+
17
+ if (!mounted) {
18
+ return (
19
+ <main className="min-h-screen bg-background text-foreground">
20
+ <div className="mx-auto flex min-h-screen w-full max-w-2xl flex-col justify-center px-6 py-16 text-center">
21
+ <div className="rounded-[8px] border border-border bg-card px-4 py-8 shadow-sm">
22
+ <p className="text-sm font-medium text-muted-foreground">Loading Inspector...</p>
23
+ </div>
24
+ </div>
25
+ </main>
26
+ );
27
+ }
28
+
29
+ return <ProxyViewerContainer />;
30
+ }
@@ -9,6 +9,7 @@ import { decodeSessionIdFromPath, encodeSessionIdForPath } from "../../lib/sessi
9
9
  * and Unicode project paths survive dev-server URL normalization.
10
10
  */
11
11
  export const Route = createFileRoute("/session/$sessionId")({
12
+ ssr: false,
12
13
  component: SessionViewerRoute,
13
14
  parseParams: (params) => ({
14
15
  sessionId: decodeSessionIdFromPath(params.sessionId),
@@ -44,6 +44,8 @@
44
44
  --animate-crab-appear: crab-appear 300ms ease-out;
45
45
  --animate-crab-settle: crab-settle 400ms ease-in-out;
46
46
  --animate-crab-piano-pop: crab-piano-pop 400ms cubic-bezier(0.34, 1.56, 0.64, 1) backwards;
47
+ --animate-crab-short-leg-body: crab-short-leg-body 720ms ease-in-out infinite;
48
+ --animate-crab-short-leg-step: crab-short-leg-step 360ms ease-in-out infinite;
47
49
  }
48
50
 
49
51
  :root {
@@ -165,10 +167,42 @@
165
167
  }
166
168
  }
167
169
 
170
+ @keyframes crab-short-leg-body {
171
+ 0%,
172
+ 100% {
173
+ transform: translateY(0) rotate(-1deg);
174
+ }
175
+ 25% {
176
+ transform: translateY(1px) rotate(1.5deg);
177
+ }
178
+ 50% {
179
+ transform: translateY(-1px) rotate(-1.5deg);
180
+ }
181
+ 75% {
182
+ transform: translateY(1px) rotate(1deg);
183
+ }
184
+ }
185
+
186
+ @keyframes crab-short-leg-step {
187
+ 0%,
188
+ 100% {
189
+ transform: translateY(0) scaleY(0.72) rotate(var(--crab-leg-rest, 0deg));
190
+ opacity: 0.42;
191
+ }
192
+ 50% {
193
+ transform: translateY(3px) scaleY(1.15) rotate(var(--crab-leg-step, 0deg));
194
+ opacity: 0.9;
195
+ }
196
+ }
197
+
168
198
  @media (prefers-reduced-motion: reduce) {
169
199
  .animate-crab-piano-pop {
170
200
  animation: none !important;
171
201
  }
202
+ .floating-crab-body,
203
+ .floating-crab-leg {
204
+ animation: none !important;
205
+ }
172
206
  }
173
207
 
174
208
  [data-deep-link-highlight="true"] {
@@ -205,6 +239,26 @@
205
239
  }
206
240
 
207
241
  @layer utilities {
242
+ .floating-crab-body {
243
+ animation: var(--animate-crab-short-leg-body);
244
+ transform-origin: 50% 74%;
245
+ }
246
+
247
+ .floating-crab-leg {
248
+ animation: var(--animate-crab-short-leg-step);
249
+ transform-origin: 50% 0%;
250
+ }
251
+
252
+ .floating-crab-leg-left {
253
+ --crab-leg-rest: -10deg;
254
+ --crab-leg-step: 14deg;
255
+ }
256
+
257
+ .floating-crab-leg-right {
258
+ --crab-leg-rest: 10deg;
259
+ --crab-leg-step: -14deg;
260
+ }
261
+
208
262
  .inspector-scrollbar {
209
263
  scrollbar-color: color-mix(in srgb, var(--muted-foreground) 32%, transparent) transparent;
210
264
  scrollbar-width: thin;