@tonyclaw/agent-inspector 2.1.13 → 2.1.15

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 (92) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/CompareDrawer-BcEd6V-V.js +1 -0
  3. package/.output/public/assets/ProxyViewerContainer-h851qWNp.js +106 -0
  4. package/.output/public/assets/ReplayDialog-BNpC0548.js +1 -0
  5. package/.output/public/assets/{RequestAnatomy-BjlBT-Cy.js → RequestAnatomy-Ds1uRLVB.js} +1 -1
  6. package/.output/public/assets/ResponseView-7KPVqKl5.js +3 -0
  7. package/.output/public/assets/{StreamingChunkSequence-D2_SMhlE.js → StreamingChunkSequence-BHQT261s.js} +1 -1
  8. package/.output/public/assets/_sessionId-DWePGjnS.js +1 -0
  9. package/.output/public/assets/index-CI1-G8ua.js +1 -0
  10. package/.output/public/assets/index-DdhFqPsI.css +1 -0
  11. package/.output/public/assets/index-DjKt8XKe.js +14 -0
  12. package/.output/public/assets/json-viewer-CkCu-rka.js +1 -0
  13. package/.output/public/assets/{main-Dtspb4Ui.js → main-DpD1N0S8.js} +2 -2
  14. package/.output/server/_libs/lucide-react.mjs +194 -200
  15. package/.output/server/_libs/react-markdown.mjs +90 -2
  16. package/.output/server/{_sessionId-CXDcLuvi.mjs → _sessionId-DF9Sy8cP.mjs} +6 -57
  17. package/.output/server/_ssr/{CompareDrawer-CMoCAoeq.mjs → CompareDrawer-BoxztaO7.mjs} +50 -74
  18. package/.output/server/_ssr/{ProxyViewerContainer-BW2vVCBN.mjs → ProxyViewerContainer-CRBkqFlJ.mjs} +1221 -799
  19. package/.output/server/_ssr/{ReplayDialog-ChXL1t8H.mjs → ReplayDialog-Cc1dyDuK.mjs} +18 -62
  20. package/.output/server/_ssr/{RequestAnatomy-DtKzIlfU.mjs → RequestAnatomy-CMGSsz5Z.mjs} +6 -57
  21. package/.output/server/_ssr/{ResponseView-B5I8drzc.mjs → ResponseView-Cp10DM1D.mjs} +139 -61
  22. package/.output/server/_ssr/{StreamingChunkSequence-DvwjQNcO.mjs → StreamingChunkSequence-B1VGxy3A.mjs} +13 -58
  23. package/.output/server/_ssr/{index-D_ZHtRfl.mjs → index-47XVPghS.mjs} +6 -57
  24. package/.output/server/_ssr/index.mjs +2 -2
  25. package/.output/server/_ssr/json-viewer-zDE2rrmJ.mjs +478 -0
  26. package/.output/server/_ssr/{router-DhL9Wp3N.mjs → router-DVeuZFqI.mjs} +680 -174
  27. package/.output/server/{_tanstack-start-manifest_v-BHgoAmxZ.mjs → _tanstack-start-manifest_v-Bp8JxtPW.mjs} +1 -1
  28. package/.output/server/index.mjs +72 -58
  29. package/package.json +1 -1
  30. package/src/components/ProxyViewer.tsx +28 -2
  31. package/src/components/ProxyViewerContainer.tsx +25 -17
  32. package/src/components/clients/ClientLogo.tsx +132 -0
  33. package/src/components/groups/GroupsDialog.tsx +18 -13
  34. package/src/components/providers/ImportWizardDialog.tsx +7 -1
  35. package/src/components/providers/ProviderCard.tsx +10 -1
  36. package/src/components/providers/ProviderForm.tsx +157 -41
  37. package/src/components/providers/ProvidersPanel.tsx +5 -1
  38. package/src/components/proxy-viewer/AgentTraceSummary.tsx +56 -23
  39. package/src/components/proxy-viewer/AnswerMarkdown.tsx +8 -3
  40. package/src/components/proxy-viewer/CompareDrawer.tsx +58 -14
  41. package/src/components/proxy-viewer/ConversationGroup.tsx +11 -15
  42. package/src/components/proxy-viewer/ConversationHeader.tsx +33 -41
  43. package/src/components/proxy-viewer/LogEntry.tsx +41 -12
  44. package/src/components/proxy-viewer/LogEntryHeader.tsx +122 -51
  45. package/src/components/proxy-viewer/ProviderLogoStack.tsx +60 -0
  46. package/src/components/proxy-viewer/ReplayDialog.tsx +8 -2
  47. package/src/components/proxy-viewer/RequestToolsPanel.tsx +19 -13
  48. package/src/components/proxy-viewer/StreamingChunkSequence.tsx +9 -3
  49. package/src/components/proxy-viewer/ThreadConnector.tsx +18 -7
  50. package/src/components/proxy-viewer/TurnGroup.tsx +67 -32
  51. package/src/components/proxy-viewer/TurnGroupList.tsx +153 -0
  52. package/src/components/proxy-viewer/anatomy/sessionContextSummary.ts +74 -2
  53. package/src/components/proxy-viewer/formats/anthropic/ContentBlocks.tsx +13 -7
  54. package/src/components/proxy-viewer/formats/index.tsx +10 -3
  55. package/src/components/proxy-viewer/formats/openai/ResponseView.tsx +185 -17
  56. package/src/components/proxy-viewer/log-formats/openai.ts +107 -15
  57. package/src/components/proxy-viewer/logFocus.ts +15 -2
  58. package/src/components/proxy-viewer/requestTools.ts +7 -3
  59. package/src/components/proxy-viewer/viewerState.ts +77 -5
  60. package/src/components/ui/json-viewer-bulk.ts +41 -6
  61. package/src/components/ui/json-viewer.tsx +9 -8
  62. package/src/contracts/index.ts +15 -1
  63. package/src/contracts/openai.ts +94 -0
  64. package/src/lib/providerContract.ts +1 -0
  65. package/src/lib/providerModelMetadata.ts +7 -1
  66. package/src/lib/sessionInfoContract.ts +1 -0
  67. package/src/lib/stopReason.ts +59 -15
  68. package/src/lib/upstreamUrl.ts +46 -0
  69. package/src/mcp/previewExtractor.ts +73 -3
  70. package/src/mcp/server.ts +2 -0
  71. package/src/mcp/toolHandlers.ts +2 -0
  72. package/src/proxy/constants.ts +2 -0
  73. package/src/proxy/formats/openai/handler.ts +40 -9
  74. package/src/proxy/formats/openai/index.ts +7 -0
  75. package/src/proxy/formats/openai/schemas.ts +15 -1
  76. package/src/proxy/formats/openai/stream.ts +529 -135
  77. package/src/proxy/formats/registry.ts +9 -1
  78. package/src/proxy/providerImporters.ts +45 -5
  79. package/src/proxy/providers.ts +31 -11
  80. package/src/proxy/schemas.ts +6 -0
  81. package/src/proxy/sessionInfo.ts +5 -1
  82. package/src/proxy/toolSchemaWarnings.ts +25 -3
  83. package/src/proxy/upstream.ts +18 -37
  84. package/src/routes/api/providers.$providerId.ts +1 -0
  85. package/src/routes/api/providers.ts +2 -0
  86. package/.output/public/assets/CompareDrawer-Cz_1vIpR.js +0 -1
  87. package/.output/public/assets/ProxyViewerContainer-7QSiluMf.js +0 -117
  88. package/.output/public/assets/ReplayDialog-sBA1KAYD.js +0 -1
  89. package/.output/public/assets/ResponseView-PtEKzml9.js +0 -1
  90. package/.output/public/assets/_sessionId-DZfB4ruK.js +0 -1
  91. package/.output/public/assets/index-B-QQLbpz.js +0 -1
  92. package/.output/public/assets/index-CmtfjQPv.css +0 -1
@@ -1,15 +1,29 @@
1
- import { type JSX, memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
1
+ import {
2
+ type CSSProperties,
3
+ type JSX,
4
+ memo,
5
+ useCallback,
6
+ useEffect,
7
+ useMemo,
8
+ useRef,
9
+ useState,
10
+ } from "react";
2
11
  import { AlertTriangle, ChevronRight, Clock, Zap } from "lucide-react";
3
12
  import { isTurnBoundary } from "../../lib/stopReason";
4
13
  import type { TimeDisplayFormat } from "../../lib/runtimeConfig";
5
14
  import { cn, formatTokens } from "../../lib/utils";
6
15
  import type { CapturedLog } from "../../contracts";
7
16
  import { getCrabVariant } from "../ui/crab-variants";
8
- import { ProviderLogo, detectProvider, type Provider } from "../providers/ProviderLogo";
9
17
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/tooltip";
18
+ import { ROW_CHEVRON_SLOT_CLASS } from "./ProviderLogoStack";
10
19
  import type { CacheTrendEntry } from "./cacheTrend";
11
20
  import { LogEntry } from "./LogEntry";
12
- import { LOG_FOCUS_REQUEST_EVENT, readLogFocusRequest } from "./logFocus";
21
+ import {
22
+ dispatchLogFocusRequest,
23
+ LOG_FOCUS_REQUEST_EVENT,
24
+ readLogFocusRequest,
25
+ type LogFocusRequest,
26
+ } from "./logFocus";
13
27
  import { ThreadConnector } from "./ThreadConnector";
14
28
  import { ToolTraceEvents } from "./ToolTraceEvents";
15
29
  import { extractToolTraceEvents, isTurnCollapsible, type TurnEntry } from "./viewerState";
@@ -19,7 +33,11 @@ function formatElapsed(ms: number): string {
19
33
  return `${(ms / 1000).toFixed(1)}s`;
20
34
  }
21
35
 
22
- type TurnGroupProps = {
36
+ export type PendingTurnFocusRequest = LogFocusRequest & {
37
+ nonce: number;
38
+ };
39
+
40
+ export type TurnGroupProps = {
23
41
  entries: TurnEntry[];
24
42
  viewMode: "simple" | "full";
25
43
  strip: boolean;
@@ -29,6 +47,7 @@ type TurnGroupProps = {
29
47
  comparisonPredecessors: Map<number, CapturedLog>;
30
48
  turnIndex?: number;
31
49
  timeDisplayFormat: TimeDisplayFormat;
50
+ pendingFocusRequest?: PendingTurnFocusRequest;
32
51
  };
33
52
 
34
53
  export const TurnGroup = memo(function TurnGroup({
@@ -41,6 +60,7 @@ export const TurnGroup = memo(function TurnGroup({
41
60
  comparisonPredecessors,
42
61
  turnIndex = 0,
43
62
  timeDisplayFormat,
63
+ pendingFocusRequest,
44
64
  }: TurnGroupProps): JSX.Element {
45
65
  const lastIdx = entries.length - 1;
46
66
  const lastStop = entries[lastIdx]?.stopReason ?? null;
@@ -75,6 +95,22 @@ export const TurnGroup = memo(function TurnGroup({
75
95
  };
76
96
  }, [collapsible, entries]);
77
97
 
98
+ useEffect(() => {
99
+ if (pendingFocusRequest === undefined) return undefined;
100
+ if (!entries.some((entry) => entry.log.id === pendingFocusRequest.logId)) return undefined;
101
+ if (collapsible) setCollapsed(false);
102
+
103
+ const timer = window.setTimeout(() => {
104
+ dispatchLogFocusRequest({
105
+ logId: pendingFocusRequest.logId,
106
+ tab: pendingFocusRequest.tab,
107
+ source: "virtualizer",
108
+ });
109
+ }, 0);
110
+
111
+ return () => window.clearTimeout(timer);
112
+ }, [collapsible, entries, pendingFocusRequest]);
113
+
78
114
  const toggleCollapse = useCallback(() => {
79
115
  if (collapsible) setCollapsed((prev) => !prev);
80
116
  }, [collapsible]);
@@ -106,21 +142,11 @@ export const TurnGroup = memo(function TurnGroup({
106
142
  };
107
143
  }, [entries, lastIdx]);
108
144
 
109
- // Unique providers across all entries (for collapsed model logos)
110
- const uniqueProviders = useMemo(() => {
111
- const seen = new Set<Provider>();
112
- for (const e of entries) {
113
- const p = detectProvider(e.log.model);
114
- if (p !== "unknown") seen.add(p);
115
- }
116
- return [...seen];
117
- }, [entries]);
118
-
119
145
  // Crab variant creators for the dual-crab collapsed layout
120
146
  const StartCrab = useMemo(() => getCrabVariant(entries[0]?.log.id ?? 0), [entries]);
121
147
  const EndCrab = useMemo(() => getCrabVariant(entries[lastIdx]?.log.id ?? 0), [entries, lastIdx]);
122
148
 
123
- const bgClass = turnIndex % 2 === 0 ? "bg-muted/10" : "bg-muted/25";
149
+ const bgClass = turnIndex % 2 === 0 ? "bg-[#070a0f]" : "bg-[#090d12]";
124
150
  const aggregateIsSlow =
125
151
  aggregate.maxElapsed !== null &&
126
152
  slowResponseThresholdSeconds > 0 &&
@@ -154,17 +180,27 @@ export const TurnGroup = memo(function TurnGroup({
154
180
 
155
181
  const firstLogId = entries[0]?.log.id ?? turnIndex;
156
182
  const turnLabel = `Turn ${String(turnIndex + 1)}`;
183
+ const containmentStyle = useMemo<CSSProperties>(
184
+ () => ({
185
+ contentVisibility: "auto",
186
+ containIntrinsicSize: collapsed ? "48px" : "160px",
187
+ }),
188
+ [collapsed],
189
+ );
157
190
 
158
191
  return (
159
192
  <div
160
193
  ref={containerRef}
194
+ style={containmentStyle}
161
195
  tabIndex={collapsed ? undefined : 0}
162
196
  role={collapsed ? undefined : "group"}
163
197
  aria-label={collapsed ? undefined : turnLabel}
164
198
  data-nav-id={collapsed ? undefined : `turn-${String(firstLogId)}`}
165
199
  className={cn(
166
- "border rounded-lg",
167
- isPending ? "border-amber-500/10" : "border-transparent",
200
+ "border rounded-lg transition-[border-color,box-shadow]",
201
+ isPending
202
+ ? "border-amber-300/15 shadow-[0_0_0_1px_rgba(252,211,77,0.04)]"
203
+ : "border-transparent",
168
204
  !collapsed &&
169
205
  "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:outline-none",
170
206
  )}
@@ -252,13 +288,23 @@ export const TurnGroup = memo(function TurnGroup({
252
288
  {entries.length > 1 && (
253
289
  <div
254
290
  className={cn(
255
- "flex-1 min-w-0 mb-0.5 rounded-lg border border-border py-1 px-3 flex items-center gap-3 text-xs",
291
+ "flex-1 min-w-0 mb-0.5 rounded-lg border border-cyan-300/10 py-1.5 px-3 flex items-center gap-3 text-xs shadow-[inset_0_1px_0_rgba(255,255,255,0.04)]",
256
292
  bgClass,
257
293
  )}
258
294
  >
295
+ <span className={ROW_CHEVRON_SLOT_CLASS}>
296
+ <ChevronRight className="size-4 shrink-0" />
297
+ </span>
298
+
259
299
  {/* ID range */}
260
- <span className="text-blue-400/80 font-mono font-semibold tabular-nums shrink-0">
261
- #{entries[0]?.log.id ?? "?"} ~ #{entries[lastIdx]?.log.id ?? "?"}
300
+ <span
301
+ className="text-blue-400/80 font-mono font-semibold tabular-nums shrink-0"
302
+ title={`Log IDs ${String(entries[0]?.log.id ?? "?")} ~ ${String(
303
+ entries[lastIdx]?.log.id ?? "?",
304
+ )}`}
305
+ >
306
+ #{entries[0]?.sessionLogNumber ?? "?"} ~ #
307
+ {entries[lastIdx]?.sessionLogNumber ?? "?"}
262
308
  </span>
263
309
 
264
310
  {/* Request count */}
@@ -266,15 +312,6 @@ export const TurnGroup = memo(function TurnGroup({
266
312
  {entries.length} request{entries.length > 1 ? "s" : ""}
267
313
  </span>
268
314
 
269
- {/* Model logos — one per unique provider */}
270
- {uniqueProviders.length > 0 && (
271
- <span className="flex items-center gap-0.5 shrink-0">
272
- {uniqueProviders.map((p) => (
273
- <ProviderLogo key={p} provider={p} className="size-4" />
274
- ))}
275
- </span>
276
- )}
277
-
278
315
  {/* Elapsed — slowest single request in the turn (not the sum) */}
279
316
  {aggregate.maxElapsed !== null && (
280
317
  <TooltipProvider>
@@ -322,9 +359,6 @@ export const TurnGroup = memo(function TurnGroup({
322
359
 
323
360
  {/* Spacer */}
324
361
  <span className="flex-1 min-w-0" />
325
-
326
- {/* Expand chevron */}
327
- <ChevronRight className="size-4 text-muted-foreground shrink-0" />
328
362
  </div>
329
363
  )}
330
364
  </div>
@@ -358,6 +392,7 @@ export const TurnGroup = memo(function TurnGroup({
358
392
  <div className={cn("flex-1 min-w-0 mb-0.5 rounded-lg", bgClass)}>
359
393
  <LogEntry
360
394
  log={log}
395
+ displayNumber={entry.sessionLogNumber}
361
396
  viewMode={viewMode}
362
397
  strip={strip}
363
398
  slowResponseThresholdSeconds={slowResponseThresholdSeconds}
@@ -0,0 +1,153 @@
1
+ import { useWindowVirtualizer } from "@tanstack/react-virtual";
2
+ import { type JSX, useEffect, useMemo, useState } from "react";
3
+ import type { CapturedLog } from "../../contracts";
4
+ import type { TimeDisplayFormat } from "../../lib/runtimeConfig";
5
+ import type { CacheTrendEntry } from "./cacheTrend";
6
+ import { LOG_FOCUS_REQUEST_EVENT, readLogFocusRequest } from "./logFocus";
7
+ import { TurnGroup } from "./TurnGroup";
8
+ import type { PendingTurnFocusRequest } from "./TurnGroup";
9
+ import type { TurnGroupData } from "./viewerState";
10
+
11
+ const VIRTUALIZE_TURN_THRESHOLD = 24;
12
+ const ESTIMATED_TURN_HEIGHT = 132;
13
+ const VIRTUAL_OVERSCAN = 8;
14
+
15
+ type TurnGroupListProps = {
16
+ turnGroups: TurnGroupData[];
17
+ viewMode: "simple" | "full";
18
+ strip: boolean;
19
+ slowResponseThresholdSeconds: number;
20
+ cacheTrends?: Map<number, CacheTrendEntry>;
21
+ onCompareWithPrevious: (log: CapturedLog) => void;
22
+ comparisonPredecessors: Map<number, CapturedLog>;
23
+ timeDisplayFormat: TimeDisplayFormat;
24
+ };
25
+
26
+ type PendingVirtualFocusRequest = PendingTurnFocusRequest & {
27
+ turnIndex: number;
28
+ };
29
+
30
+ export function shouldVirtualizeTurnGroups(turnCount: number): boolean {
31
+ return turnCount > VIRTUALIZE_TURN_THRESHOLD;
32
+ }
33
+
34
+ export function findTurnGroupIndexForLog(
35
+ turnGroups: readonly TurnGroupData[],
36
+ logId: number,
37
+ ): number | null {
38
+ for (let index = 0; index < turnGroups.length; index++) {
39
+ const group = turnGroups[index];
40
+ if (group === undefined) continue;
41
+ if (group.entries.some((entry) => entry.log.id === logId)) return index;
42
+ }
43
+ return null;
44
+ }
45
+
46
+ function buildLogTurnIndex(turnGroups: readonly TurnGroupData[]): Map<number, number> {
47
+ const result = new Map<number, number>();
48
+ for (let index = 0; index < turnGroups.length; index++) {
49
+ const group = turnGroups[index];
50
+ if (group === undefined) continue;
51
+ for (const entry of group.entries) {
52
+ result.set(entry.log.id, index);
53
+ }
54
+ }
55
+ return result;
56
+ }
57
+
58
+ function renderTurnGroup(
59
+ turnGroup: TurnGroupData,
60
+ props: TurnGroupListProps,
61
+ pendingFocusRequest?: PendingTurnFocusRequest,
62
+ ): JSX.Element {
63
+ return (
64
+ <TurnGroup
65
+ key={turnGroup.turnIndex}
66
+ entries={turnGroup.entries}
67
+ viewMode={props.viewMode}
68
+ strip={props.strip}
69
+ slowResponseThresholdSeconds={props.slowResponseThresholdSeconds}
70
+ timeDisplayFormat={props.timeDisplayFormat}
71
+ cacheTrends={props.cacheTrends}
72
+ onCompareWithPrevious={props.onCompareWithPrevious}
73
+ comparisonPredecessors={props.comparisonPredecessors}
74
+ turnIndex={turnGroup.turnIndex}
75
+ pendingFocusRequest={pendingFocusRequest}
76
+ />
77
+ );
78
+ }
79
+
80
+ export function TurnGroupList(props: TurnGroupListProps): JSX.Element {
81
+ if (!shouldVirtualizeTurnGroups(props.turnGroups.length)) {
82
+ return <>{props.turnGroups.map((turnGroup) => renderTurnGroup(turnGroup, props))}</>;
83
+ }
84
+
85
+ return <VirtualizedTurnGroupList {...props} />;
86
+ }
87
+
88
+ function VirtualizedTurnGroupList(props: TurnGroupListProps): JSX.Element {
89
+ const { turnGroups } = props;
90
+ const [pendingFocusRequest, setPendingFocusRequest] = useState<PendingVirtualFocusRequest | null>(
91
+ null,
92
+ );
93
+
94
+ const virtualizer = useWindowVirtualizer({
95
+ count: turnGroups.length,
96
+ estimateSize: () => ESTIMATED_TURN_HEIGHT,
97
+ overscan: VIRTUAL_OVERSCAN,
98
+ getItemKey: (index) => turnGroups[index]?.turnIndex ?? index,
99
+ });
100
+
101
+ const turnIndexByLogId = useMemo(() => buildLogTurnIndex(turnGroups), [turnGroups]);
102
+
103
+ useEffect(() => {
104
+ const handleLogFocusRequest = (event: Event): void => {
105
+ const request = readLogFocusRequest(event);
106
+ if (request === null) return;
107
+ if (request.source === "virtualizer") return;
108
+
109
+ const turnIndex = turnIndexByLogId.get(request.logId);
110
+ if (turnIndex === undefined) return;
111
+ virtualizer.scrollToIndex(turnIndex, { align: "center" });
112
+ setPendingFocusRequest((previous) => ({
113
+ ...request,
114
+ turnIndex,
115
+ nonce: previous === null ? 1 : previous.nonce + 1,
116
+ }));
117
+ };
118
+
119
+ window.addEventListener(LOG_FOCUS_REQUEST_EVENT, handleLogFocusRequest);
120
+ return () => {
121
+ window.removeEventListener(LOG_FOCUS_REQUEST_EVENT, handleLogFocusRequest);
122
+ };
123
+ }, [turnIndexByLogId, virtualizer]);
124
+
125
+ return (
126
+ <div
127
+ className="relative w-full"
128
+ style={{ height: `${virtualizer.getTotalSize()}px` }}
129
+ data-virtualized-turn-list="true"
130
+ >
131
+ {virtualizer.getVirtualItems().map((virtualItem) => {
132
+ const turnGroup = turnGroups[virtualItem.index];
133
+ if (turnGroup === undefined) return null;
134
+ const itemFocusRequest =
135
+ pendingFocusRequest !== null && pendingFocusRequest.turnIndex === virtualItem.index
136
+ ? pendingFocusRequest
137
+ : undefined;
138
+
139
+ return (
140
+ <div
141
+ key={virtualItem.key}
142
+ data-index={virtualItem.index}
143
+ ref={virtualizer.measureElement}
144
+ className="absolute left-0 top-0 w-full"
145
+ style={{ transform: `translateY(${virtualItem.start}px)` }}
146
+ >
147
+ {renderTurnGroup(turnGroup, props, itemFocusRequest)}
148
+ </div>
149
+ );
150
+ })}
151
+ </div>
152
+ );
153
+ }
@@ -46,6 +46,32 @@ type SummaryAccumulator = {
46
46
  peak: SessionContextSnapshot;
47
47
  };
48
48
 
49
+ type SnapshotCacheEntry = {
50
+ id: number;
51
+ rawRequestBody: string | null;
52
+ inputTokens: number | null;
53
+ model: string | null;
54
+ path: string;
55
+ apiFormat: CapturedLog["apiFormat"];
56
+ timestamp: string;
57
+ providersSignature: string;
58
+ snapshot: SessionContextSnapshot | null;
59
+ };
60
+
61
+ const snapshotCache = new WeakMap<CapturedLog, SnapshotCacheEntry>();
62
+
63
+ function providerSignature(providers: readonly ProviderConfig[]): string {
64
+ return providers
65
+ .map((provider) => {
66
+ const metadata =
67
+ provider.modelMetadata
68
+ ?.map((entry) => `${entry.model}:${String(entry.contextWindow ?? "")}`)
69
+ .join(",") ?? "";
70
+ return `${provider.id}:${provider.updatedAt}:${provider.models.join(",")}:${metadata}`;
71
+ })
72
+ .join("|");
73
+ }
74
+
49
75
  function parseRequestBody(rawBody: string | null): unknown | null {
50
76
  if (rawBody === null) return null;
51
77
  try {
@@ -104,6 +130,41 @@ function snapshotFromLog(
104
130
  };
105
131
  }
106
132
 
133
+ function cachedSnapshotFromLog(
134
+ log: CapturedLog,
135
+ providers: readonly ProviderConfig[],
136
+ providersSignature: string,
137
+ ): SessionContextSnapshot | null {
138
+ const cached = snapshotCache.get(log);
139
+ if (
140
+ cached !== undefined &&
141
+ cached.id === log.id &&
142
+ cached.rawRequestBody === log.rawRequestBody &&
143
+ cached.inputTokens === log.inputTokens &&
144
+ cached.model === log.model &&
145
+ cached.path === log.path &&
146
+ cached.apiFormat === log.apiFormat &&
147
+ cached.timestamp === log.timestamp &&
148
+ cached.providersSignature === providersSignature
149
+ ) {
150
+ return cached.snapshot;
151
+ }
152
+
153
+ const snapshot = snapshotFromLog(log, providers);
154
+ snapshotCache.set(log, {
155
+ id: log.id,
156
+ rawRequestBody: log.rawRequestBody,
157
+ inputTokens: log.inputTokens,
158
+ model: log.model,
159
+ path: log.path,
160
+ apiFormat: log.apiFormat,
161
+ timestamp: log.timestamp,
162
+ providersSignature,
163
+ snapshot,
164
+ });
165
+ return snapshot;
166
+ }
167
+
107
168
  function isHigherPeak(candidate: SessionContextSnapshot, current: SessionContextSnapshot): boolean {
108
169
  if (candidate.usagePercent !== null && current.usagePercent !== null) {
109
170
  return candidate.usagePercent > current.usagePercent;
@@ -149,15 +210,26 @@ function trendPercent(
149
210
  return (latest.usagePercent - first.usagePercent) * 100;
150
211
  }
151
212
 
213
+ function logsInAscendingIdOrder(logs: readonly CapturedLog[]): readonly CapturedLog[] {
214
+ for (let index = 1; index < logs.length; index++) {
215
+ const previous = logs[index - 1];
216
+ const current = logs[index];
217
+ if (previous === undefined || current === undefined) continue;
218
+ if (previous.id > current.id) return [...logs].sort((left, right) => left.id - right.id);
219
+ }
220
+ return logs;
221
+ }
222
+
152
223
  export function buildSessionContextSummary(
153
224
  logs: readonly CapturedLog[],
154
225
  providers: readonly ProviderConfig[] = [],
155
226
  ): SessionContextSummary {
156
- const sortedLogs = [...logs].sort((left, right) => left.id - right.id);
227
+ const sortedLogs = logsInAscendingIdOrder(logs);
228
+ const providersSignature = providerSignature(providers);
157
229
  const byModel = new Map<string, SummaryAccumulator>();
158
230
 
159
231
  for (const log of sortedLogs) {
160
- const snapshot = snapshotFromLog(log, providers);
232
+ const snapshot = cachedSnapshotFromLog(log, providers, providersSignature);
161
233
  if (snapshot === null) continue;
162
234
 
163
235
  const existing = byModel.get(snapshot.model);
@@ -1,12 +1,12 @@
1
1
  import { Brain, ChevronDown, ChevronRight, Terminal } from "lucide-react";
2
- import { type JSX, memo, useState } from "react";
2
+ import { type JSX, memo, Suspense, useState } from "react";
3
3
  import type { ResponseContentBlockType } from "../../../../contracts/anthropic";
4
4
  import { AnswerMarkdown } from "../../AnswerMarkdown";
5
5
  import { Badge } from "../../../ui/badge";
6
6
  import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../../../ui/collapsible";
7
- import { JsonViewer } from "../../../ui/json-viewer";
8
7
  import { safeJsonValue } from "../../../ui/json-viewer-bulk";
9
8
  import { ScrollArea } from "../../../ui/scroll-area";
9
+ import { LazyJsonViewer } from "../../lazy";
10
10
  import { extractThinkingFromContent } from "./thinkingExtract";
11
11
 
12
12
  function assertNever(_value: never): JSX.Element {
@@ -121,11 +121,17 @@ export const ToolUseBlock = memo(function ToolUseBlock({
121
121
  )}
122
122
  </CollapsibleTrigger>
123
123
  <CollapsibleContent>
124
- <div className="px-3 pb-2">
125
- <ScrollArea className="max-h-[60vh]">
126
- <JsonViewer data={safeJsonValue(input)} defaultExpandDepth={0} />
127
- </ScrollArea>
128
- </div>
124
+ {open && (
125
+ <div className="px-3 pb-2">
126
+ <ScrollArea className="max-h-[60vh]">
127
+ <Suspense
128
+ fallback={<div className="text-xs text-muted-foreground">Loading JSON...</div>}
129
+ >
130
+ <LazyJsonViewer data={safeJsonValue(input)} defaultExpandDepth={0} />
131
+ </Suspense>
132
+ </ScrollArea>
133
+ </div>
134
+ )}
129
135
  </CollapsibleContent>
130
136
  </div>
131
137
  </Collapsible>
@@ -1,12 +1,12 @@
1
1
  import type { JSX } from "react";
2
2
  import type { ApiFormat } from "../../../contracts";
3
3
  import type { AnthropicResponse } from "../../../contracts/anthropic";
4
- import type { OpenAIResponse } from "../../../contracts/openai";
4
+ import type { OpenAIResponse, OpenAIResponsesResponse } from "../../../contracts/openai";
5
5
  import { StructuredResponseViewAnthropic } from "./anthropic/ResponseView";
6
- import { OpenAIResponseView } from "./openai/ResponseView";
6
+ import { OpenAIResponseView, OpenAIResponsesResponseView } from "./openai/ResponseView";
7
7
 
8
8
  export { StructuredResponseViewAnthropic } from "./anthropic/ResponseView";
9
- export { OpenAIResponseView } from "./openai/ResponseView";
9
+ export { OpenAIResponseView, OpenAIResponsesResponseView } from "./openai/ResponseView";
10
10
  export { ResponseContentBlockRenderer } from "./anthropic/ContentBlocks";
11
11
 
12
12
  function isRecord(value: unknown): value is Record<string, unknown> {
@@ -17,6 +17,10 @@ function isOpenAIResponse(response: unknown): response is OpenAIResponse {
17
17
  return isRecord(response) && response.object === "chat.completion";
18
18
  }
19
19
 
20
+ function isOpenAIResponsesResponse(response: unknown): response is OpenAIResponsesResponse {
21
+ return isRecord(response) && response.object === "response";
22
+ }
23
+
20
24
  function isAnthropicResponse(response: unknown): response is AnthropicResponse {
21
25
  return isRecord(response) && response.type === "message" && Array.isArray(response.content);
22
26
  }
@@ -28,6 +32,9 @@ export function formatViewFor(
28
32
  if (apiFormat === "openai" && isOpenAIResponse(response)) {
29
33
  return <OpenAIResponseView response={response} />;
30
34
  }
35
+ if (apiFormat === "openai" && isOpenAIResponsesResponse(response)) {
36
+ return <OpenAIResponsesResponseView response={response} />;
37
+ }
31
38
  if (apiFormat === "anthropic" && isAnthropicResponse(response)) {
32
39
  return <StructuredResponseViewAnthropic response={response} />;
33
40
  }