@tonyclaw/agent-inspector 2.1.21 → 2.1.23

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 (48) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/{CompareDrawer-Pii_cIqn.js → CompareDrawer-Cvgl_Jmi.js} +1 -1
  3. package/.output/public/assets/ProxyViewerContainer-CNLLZq37.js +106 -0
  4. package/.output/public/assets/{ReplayDialog-BqnYC8ze.js → ReplayDialog-DeEELx0X.js} +1 -1
  5. package/.output/public/assets/{RequestAnatomy-I8iBshz1.js → RequestAnatomy-CrLSqdiA.js} +1 -1
  6. package/.output/public/assets/ResponseView-CFIqDL5E.js +3 -0
  7. package/.output/public/assets/{StreamingChunkSequence-BrSx21IQ.js → StreamingChunkSequence-DSRjbgqb.js} +1 -1
  8. package/.output/public/assets/_sessionId-ZuH6-ZX5.js +1 -0
  9. package/.output/public/assets/index-B26q0-ch.js +1 -0
  10. package/.output/public/assets/{index-Dw-jXKKY.js → index-BZxLBVYd.js} +1 -1
  11. package/.output/public/assets/index-CxszCIFm.css +1 -0
  12. package/.output/public/assets/{json-viewer-D5mzV0oV.js → json-viewer-DERQNZRV.js} +1 -1
  13. package/.output/public/assets/{main-B7OIQMeA.js → main-BoshTI0W.js} +2 -2
  14. package/.output/server/_libs/lucide-react.mjs +42 -42
  15. package/.output/server/{_sessionId-DeU_JdSX.mjs → _sessionId-BYGtH1pK.mjs} +18 -3
  16. package/.output/server/_ssr/{CompareDrawer-DgDbG4oF.mjs → CompareDrawer-OchnKiac.mjs} +3 -3
  17. package/.output/server/_ssr/{ProxyViewerContainer-BOszhOJD.mjs → ProxyViewerContainer-C2lwYwDu.mjs} +2747 -2639
  18. package/.output/server/_ssr/{ReplayDialog-Cj4gWQv5.mjs → ReplayDialog-BYwaF14u.mjs} +4 -4
  19. package/.output/server/_ssr/{RequestAnatomy-CVzNAA03.mjs → RequestAnatomy-DTueiqT5.mjs} +2 -2
  20. package/.output/server/_ssr/{ResponseView-DLNpai6P.mjs → ResponseView-CqB9NKHB.mjs} +202 -55
  21. package/.output/server/_ssr/{StreamingChunkSequence-BiH8EEQB.mjs → StreamingChunkSequence-DENB9VUj.mjs} +2 -2
  22. package/.output/server/_ssr/{index-DaWXoUgf.mjs → index-BR3ea3Zb.mjs} +2 -2
  23. package/.output/server/_ssr/index.mjs +2 -2
  24. package/.output/server/_ssr/{json-viewer-CqqNYbmS.mjs → json-viewer-BU9ZUaHK.mjs} +3 -3
  25. package/.output/server/_ssr/{router-Di7D0Bcm.mjs → router-DT6BcrEg.mjs} +4 -4
  26. package/.output/server/{_tanstack-start-manifest_v-BR0YvW29.mjs → _tanstack-start-manifest_v-xQ1BRoZw.mjs} +1 -1
  27. package/.output/server/index.mjs +75 -75
  28. package/package.json +1 -1
  29. package/src/components/ProxyViewer.tsx +71 -15
  30. package/src/components/ProxyViewerContainer.tsx +1 -1
  31. package/src/components/proxy-viewer/ConversationGroupList.tsx +23 -7
  32. package/src/components/proxy-viewer/ConversationHeader.tsx +14 -7
  33. package/src/components/proxy-viewer/LogEntry.tsx +10 -0
  34. package/src/components/proxy-viewer/ResponseView.tsx +5 -1
  35. package/src/components/proxy-viewer/ToolTraceEvents.tsx +19 -31
  36. package/src/components/proxy-viewer/TurnGroup.tsx +21 -7
  37. package/src/components/proxy-viewer/formats/anthropic/ContentBlocks.tsx +37 -4
  38. package/src/components/proxy-viewer/formats/anthropic/ResponseView.tsx +17 -3
  39. package/src/components/proxy-viewer/formats/index.tsx +8 -3
  40. package/src/components/proxy-viewer/formats/openai/ResponseView.tsx +122 -16
  41. package/src/components/proxy-viewer/logFocus.ts +11 -3
  42. package/src/components/proxy-viewer/viewerState.ts +22 -7
  43. package/src/routes/session/$sessionId.tsx +27 -1
  44. package/.output/public/assets/ProxyViewerContainer-BxselkhN.js +0 -106
  45. package/.output/public/assets/ResponseView-wZzskUas.js +0 -3
  46. package/.output/public/assets/_sessionId-ByxbBMbE.js +0 -1
  47. package/.output/public/assets/index-BjzVMXJq.css +0 -1
  48. package/.output/public/assets/index-DRCRDo_E.js +0 -1
@@ -1,6 +1,7 @@
1
1
  import { Brain, ChevronDown, ChevronRight, Terminal } from "lucide-react";
2
- import { type JSX, memo, Suspense, useState } from "react";
2
+ import { type JSX, memo, Suspense, useEffect, useRef, useState } from "react";
3
3
  import type { ResponseContentBlockType } from "../../../../contracts/anthropic";
4
+ import type { ResponseToolFocus } from "../index";
4
5
  import { AnswerMarkdown } from "../../AnswerMarkdown";
5
6
  import { Badge } from "../../../ui/badge";
6
7
  import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../../../ui/collapsible";
@@ -99,16 +100,37 @@ export const ThinkingBlock = memo(function ThinkingBlock({
99
100
  export const ToolUseBlock = memo(function ToolUseBlock({
100
101
  name,
101
102
  input,
103
+ toolIndex,
104
+ toolFocus,
102
105
  }: {
103
106
  name: string;
104
107
  input: Record<string, unknown>;
108
+ toolIndex: number;
109
+ toolFocus?: ResponseToolFocus;
105
110
  }): JSX.Element {
106
111
  const [open, setOpen] = useState(false);
112
+ const triggerRef = useRef<HTMLButtonElement | null>(null);
113
+ const shouldFocus = toolFocus?.focusedToolIndex === toolIndex;
114
+
115
+ useEffect(() => {
116
+ if (!shouldFocus || toolFocus === undefined || toolFocus.toolFocusNonce <= 0) return undefined;
117
+ setOpen(true);
118
+ const timer = window.setTimeout(() => {
119
+ const trigger = triggerRef.current;
120
+ if (trigger === null) return;
121
+ trigger.scrollIntoView({ block: "center", behavior: "smooth" });
122
+ trigger.focus({ preventScroll: true });
123
+ }, 0);
124
+ return () => window.clearTimeout(timer);
125
+ }, [shouldFocus, toolFocus, toolFocus?.toolFocusNonce]);
107
126
 
108
127
  return (
109
128
  <Collapsible open={open} onOpenChange={setOpen}>
110
- <div className="border-l-2 border-sky-400/25 my-1">
111
- <CollapsibleTrigger className="flex items-center gap-1.5 px-3 py-1 w-full text-left cursor-pointer hover:bg-sky-400/[0.04] transition-colors rounded-r-sm group">
129
+ <div className="border-l-2 border-sky-400/25 my-1" data-response-tool-index={toolIndex}>
130
+ <CollapsibleTrigger
131
+ ref={triggerRef}
132
+ className="flex items-center gap-1.5 px-3 py-1 w-full text-left cursor-pointer hover:bg-sky-400/[0.04] transition-colors rounded-r-sm group focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
133
+ >
112
134
  <Terminal className="size-3.5 text-sky-400/70 shrink-0" />
113
135
  <Badge variant="outline" className="text-[10px] font-mono px-1.5 py-0 h-4">
114
136
  {name}
@@ -140,8 +162,12 @@ export const ToolUseBlock = memo(function ToolUseBlock({
140
162
 
141
163
  export const ResponseContentBlockRenderer = memo(function ResponseContentBlockRenderer({
142
164
  block,
165
+ toolIndex,
166
+ toolFocus,
143
167
  }: {
144
168
  block: ResponseContentBlockType;
169
+ toolIndex?: number;
170
+ toolFocus?: ResponseToolFocus;
145
171
  }): JSX.Element {
146
172
  switch (block.type) {
147
173
  case "text":
@@ -150,7 +176,14 @@ export const ResponseContentBlockRenderer = memo(function ResponseContentBlockRe
150
176
  case "think":
151
177
  return <ThinkingBlock thinking={block.thinking} />;
152
178
  case "tool_use":
153
- return <ToolUseBlock name={block.name} input={block.input} />;
179
+ return (
180
+ <ToolUseBlock
181
+ name={block.name}
182
+ input={block.input}
183
+ toolIndex={toolIndex ?? 0}
184
+ toolFocus={toolFocus}
185
+ />
186
+ );
154
187
  default:
155
188
  return assertNever(block);
156
189
  }
@@ -4,13 +4,18 @@ import type { AnthropicResponse } from "../../../../contracts/anthropic";
4
4
  import { formatTokens } from "../../../../lib/utils";
5
5
  import { Badge } from "../../../ui/badge";
6
6
  import { Separator } from "../../../ui/separator";
7
+ import type { ResponseToolFocus } from "../index";
7
8
  import { ResponseContentBlockRenderer } from "./ContentBlocks";
8
9
 
9
10
  export const StructuredResponseViewAnthropic = memo(function StructuredResponseViewAnthropic({
10
11
  response,
12
+ toolFocus,
11
13
  }: {
12
14
  response: AnthropicResponse;
15
+ toolFocus?: ResponseToolFocus;
13
16
  }): JSX.Element {
17
+ let toolIndex = 0;
18
+
14
19
  return (
15
20
  <div className="space-y-3">
16
21
  <div className="flex items-center gap-2 flex-wrap">
@@ -54,9 +59,18 @@ export const StructuredResponseViewAnthropic = memo(function StructuredResponseV
54
59
  <Separator className="opacity-50" />
55
60
 
56
61
  <div className="space-y-2">
57
- {response.content.map((block, i) => (
58
- <ResponseContentBlockRenderer key={i} block={block} />
59
- ))}
62
+ {response.content.map((block, i) => {
63
+ const currentToolIndex = block.type === "tool_use" ? toolIndex : undefined;
64
+ if (block.type === "tool_use") toolIndex += 1;
65
+ return (
66
+ <ResponseContentBlockRenderer
67
+ key={i}
68
+ block={block}
69
+ toolIndex={currentToolIndex}
70
+ toolFocus={toolFocus}
71
+ />
72
+ );
73
+ })}
60
74
  {response.content.length === 0 && (
61
75
  <p className="text-xs text-muted-foreground italic">Empty response content</p>
62
76
  )}
@@ -28,15 +28,20 @@ function isAnthropicResponse(response: unknown): response is AnthropicResponse {
28
28
  export function formatViewFor(
29
29
  apiFormat: ApiFormat | undefined,
30
30
  response: unknown,
31
+ toolFocus?: ResponseToolFocus,
31
32
  ): JSX.Element | null {
32
33
  if (apiFormat === "openai" && isOpenAIResponse(response)) {
33
- return <OpenAIResponseView response={response} />;
34
+ return <OpenAIResponseView response={response} toolFocus={toolFocus} />;
34
35
  }
35
36
  if (apiFormat === "openai" && isOpenAIResponsesResponse(response)) {
36
- return <OpenAIResponsesResponseView response={response} />;
37
+ return <OpenAIResponsesResponseView response={response} toolFocus={toolFocus} />;
37
38
  }
38
39
  if (apiFormat === "anthropic" && isAnthropicResponse(response)) {
39
- return <StructuredResponseViewAnthropic response={response} />;
40
+ return <StructuredResponseViewAnthropic response={response} toolFocus={toolFocus} />;
40
41
  }
41
42
  return null;
42
43
  }
44
+ export type ResponseToolFocus = {
45
+ focusedToolIndex: number | null;
46
+ toolFocusNonce: number;
47
+ };
@@ -1,5 +1,5 @@
1
1
  import { StopCircle, Terminal, Zap } from "lucide-react";
2
- import { memo, Suspense, useState, type JSX } from "react";
2
+ import { memo, Suspense, useEffect, useRef, useState, type JSX, type RefObject } from "react";
3
3
  import type {
4
4
  OpenAIResponse,
5
5
  OpenAIResponsesResponse,
@@ -17,6 +17,7 @@ import { ChevronDown, ChevronRight } from "lucide-react";
17
17
  import { ThinkingBlock } from "../anthropic/ContentBlocks";
18
18
  import { extractThinkingFromContent } from "../anthropic/thinkingExtract";
19
19
  import { LazyJsonViewer } from "../../lazy";
20
+ import type { ResponseToolFocus } from "../index";
20
21
 
21
22
  // Re-export for use in other components
22
23
  export { extractThinkingFromContent } from "../anthropic/thinkingExtract";
@@ -36,15 +37,95 @@ function parseToolArguments(raw: string | undefined): unknown {
36
37
  /** One collapsible tool_use row, mirroring the Anthropic ToolUseBlock visual
37
38
  * treatment (Terminal icon, soft tool accent, name as a Badge, JSON input in
38
39
  * a scrollable JsonViewer). */
39
- function OpenAIToolCallBlock({ call }: { call: OpenAIToolCall }): JSX.Element {
40
+ function useToolFocus({
41
+ toolIndex,
42
+ toolFocus,
43
+ setOpen,
44
+ }: {
45
+ toolIndex: number;
46
+ toolFocus?: ResponseToolFocus;
47
+ setOpen: (open: boolean) => void;
48
+ }): RefObject<HTMLButtonElement | null> {
49
+ const triggerRef = useRef<HTMLButtonElement | null>(null);
50
+ const shouldFocus = toolFocus?.focusedToolIndex === toolIndex;
51
+
52
+ useEffect(() => {
53
+ if (!shouldFocus || toolFocus === undefined || toolFocus.toolFocusNonce <= 0) return undefined;
54
+ setOpen(true);
55
+ const timer = window.setTimeout(() => {
56
+ const trigger = triggerRef.current;
57
+ if (trigger === null) return;
58
+ trigger.scrollIntoView({ block: "center", behavior: "smooth" });
59
+ trigger.focus({ preventScroll: true });
60
+ }, 0);
61
+ return () => window.clearTimeout(timer);
62
+ }, [setOpen, shouldFocus, toolFocus, toolFocus?.toolFocusNonce]);
63
+
64
+ return triggerRef;
65
+ }
66
+
67
+ function OpenAIFunctionCallBlock({
68
+ name,
69
+ argumentsText,
70
+ toolIndex,
71
+ toolFocus,
72
+ }: {
73
+ name: string | undefined;
74
+ argumentsText: string | undefined;
75
+ toolIndex: number;
76
+ toolFocus?: ResponseToolFocus;
77
+ }): JSX.Element {
78
+ const blockRef = useRef<HTMLDivElement | null>(null);
79
+ const shouldFocus = toolFocus?.focusedToolIndex === toolIndex;
80
+
81
+ useEffect(() => {
82
+ if (!shouldFocus || toolFocus === undefined || toolFocus.toolFocusNonce <= 0) return undefined;
83
+ const timer = window.setTimeout(() => {
84
+ const block = blockRef.current;
85
+ if (block === null) return;
86
+ block.scrollIntoView({ block: "center", behavior: "smooth" });
87
+ block.focus({ preventScroll: true });
88
+ }, 0);
89
+ return () => window.clearTimeout(timer);
90
+ }, [shouldFocus, toolFocus, toolFocus?.toolFocusNonce]);
91
+
92
+ return (
93
+ <div
94
+ ref={blockRef}
95
+ tabIndex={-1}
96
+ className="border border-sky-400/20 rounded-md p-3 bg-muted/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
97
+ data-response-tool-index={toolIndex}
98
+ >
99
+ <div className="text-xs text-sky-400/80 font-mono mb-1">function_call</div>
100
+ <div className="font-mono text-xs">
101
+ <span className="text-foreground/80">{name}</span>
102
+ <span className="text-muted-foreground">({argumentsText})</span>
103
+ </div>
104
+ </div>
105
+ );
106
+ }
107
+
108
+ function OpenAIToolCallBlock({
109
+ call,
110
+ toolIndex,
111
+ toolFocus,
112
+ }: {
113
+ call: OpenAIToolCall;
114
+ toolIndex: number;
115
+ toolFocus?: ResponseToolFocus;
116
+ }): JSX.Element {
40
117
  const [open, setOpen] = useState(false);
41
118
  const name = call.function.name ?? "(unnamed tool)";
42
119
  const parsed = open ? parseToolArguments(call.function.arguments) : {};
120
+ const triggerRef = useToolFocus({ toolIndex, toolFocus, setOpen });
43
121
 
44
122
  return (
45
123
  <Collapsible open={open} onOpenChange={setOpen}>
46
- <div className="border-l-2 border-sky-400/25 my-1">
47
- <CollapsibleTrigger className="flex items-center gap-1.5 px-3 py-1 w-full text-left cursor-pointer hover:bg-sky-400/[0.04] transition-colors rounded-r-sm group">
124
+ <div className="border-l-2 border-sky-400/25 my-1" data-response-tool-index={toolIndex}>
125
+ <CollapsibleTrigger
126
+ ref={triggerRef}
127
+ className="flex items-center gap-1.5 px-3 py-1 w-full text-left cursor-pointer hover:bg-sky-400/[0.04] transition-colors rounded-r-sm group focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
128
+ >
48
129
  <Terminal className="size-3.5 text-sky-400/70 shrink-0" />
49
130
  <Badge variant="outline" className="text-[10px] font-mono px-1.5 py-0 h-4">
50
131
  {name}
@@ -136,15 +217,27 @@ function extractResponsesFunctionCalls(response: OpenAIResponsesResponse): Respo
136
217
  return calls;
137
218
  }
138
219
 
139
- function ResponsesFunctionCallBlock({ call }: { call: ResponsesFunctionCall }): JSX.Element {
220
+ function ResponsesFunctionCallBlock({
221
+ call,
222
+ toolIndex,
223
+ toolFocus,
224
+ }: {
225
+ call: ResponsesFunctionCall;
226
+ toolIndex: number;
227
+ toolFocus?: ResponseToolFocus;
228
+ }): JSX.Element {
140
229
  const [open, setOpen] = useState(false);
141
230
  const parsed = open ? parseToolArguments(call.argumentsText) : {};
142
231
  const identity = call.callId ?? call.id;
232
+ const triggerRef = useToolFocus({ toolIndex, toolFocus, setOpen });
143
233
 
144
234
  return (
145
235
  <Collapsible open={open} onOpenChange={setOpen}>
146
- <div className="border-l-2 border-sky-400/25 my-1">
147
- <CollapsibleTrigger className="flex items-center gap-1.5 px-3 py-1 w-full text-left cursor-pointer hover:bg-sky-400/[0.04] transition-colors rounded-r-sm group">
236
+ <div className="border-l-2 border-sky-400/25 my-1" data-response-tool-index={toolIndex}>
237
+ <CollapsibleTrigger
238
+ ref={triggerRef}
239
+ className="flex items-center gap-1.5 px-3 py-1 w-full text-left cursor-pointer hover:bg-sky-400/[0.04] transition-colors rounded-r-sm group focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
240
+ >
148
241
  <Terminal className="size-3.5 text-sky-400/70 shrink-0" />
149
242
  <Badge variant="outline" className="text-[10px] font-mono px-1.5 py-0 h-4">
150
243
  {call.name}
@@ -192,8 +285,10 @@ function ResponsesFunctionCallBlock({ call }: { call: ResponsesFunctionCall }):
192
285
 
193
286
  export const OpenAIResponseView = memo(function OpenAIResponseView({
194
287
  response,
288
+ toolFocus,
195
289
  }: {
196
290
  response: OpenAIResponse;
291
+ toolFocus?: ResponseToolFocus;
197
292
  }): JSX.Element {
198
293
  const choice = response.choices[0];
199
294
  const message = choice?.message;
@@ -253,16 +348,20 @@ export const OpenAIResponseView = memo(function OpenAIResponseView({
253
348
  })()}
254
349
  {toolCalls.map((call, i) => (
255
350
  // biome-ignore lint/suspicious/noArrayIndexKey: tool_calls is the positionally stable list from the response
256
- <OpenAIToolCallBlock key={call.id ?? `tc-${i}`} call={call} />
351
+ <OpenAIToolCallBlock
352
+ key={call.id ?? `tc-${i}`}
353
+ call={call}
354
+ toolIndex={i}
355
+ toolFocus={toolFocus}
356
+ />
257
357
  ))}
258
358
  {message?.function_call !== null && message?.function_call !== undefined && (
259
- <div className="border border-sky-400/20 rounded-md p-3 bg-muted/20">
260
- <div className="text-xs text-sky-400/80 font-mono mb-1">function_call</div>
261
- <div className="font-mono text-xs">
262
- <span className="text-foreground/80">{message.function_call.name}</span>
263
- <span className="text-muted-foreground">({message.function_call.arguments})</span>
264
- </div>
265
- </div>
359
+ <OpenAIFunctionCallBlock
360
+ name={message.function_call.name}
361
+ argumentsText={message.function_call.arguments}
362
+ toolIndex={toolCalls.length}
363
+ toolFocus={toolFocus}
364
+ />
266
365
  )}
267
366
  {(message?.content === null ||
268
367
  message?.content === undefined ||
@@ -281,8 +380,10 @@ export const OpenAIResponseView = memo(function OpenAIResponseView({
281
380
 
282
381
  export const OpenAIResponsesResponseView = memo(function OpenAIResponsesResponseView({
283
382
  response,
383
+ toolFocus,
284
384
  }: {
285
385
  response: OpenAIResponsesResponse;
386
+ toolFocus?: ResponseToolFocus;
286
387
  }): JSX.Element {
287
388
  const text = extractResponsesOutputText(response);
288
389
  const calls = extractResponsesFunctionCalls(response);
@@ -323,7 +424,12 @@ export const OpenAIResponsesResponseView = memo(function OpenAIResponsesResponse
323
424
  {text.length > 0 && <AnswerMarkdown text={text} />}
324
425
  {calls.map((call, i) => (
325
426
  // biome-ignore lint/suspicious/noArrayIndexKey: response output order is stable
326
- <ResponsesFunctionCallBlock key={call.callId ?? call.id ?? `call-${i}`} call={call} />
427
+ <ResponsesFunctionCallBlock
428
+ key={call.callId ?? call.id ?? `call-${i}`}
429
+ call={call}
430
+ toolIndex={i}
431
+ toolFocus={toolFocus}
432
+ />
327
433
  ))}
328
434
  {text.length === 0 && calls.length === 0 && (
329
435
  <p className="text-xs text-muted-foreground italic">Empty response content</p>
@@ -2,13 +2,14 @@ import { safeGetOwnProperty } from "../../lib/objectUtils";
2
2
 
3
3
  export const LOG_FOCUS_REQUEST_EVENT = "agent-inspector:focus-log";
4
4
 
5
- export type LogFocusTab = "anatomy" | "request";
5
+ export type LogFocusTab = "anatomy" | "request" | "response";
6
6
 
7
7
  export type LogFocusSource = "virtualizer";
8
8
 
9
9
  export type LogFocusRequest = {
10
10
  logId: number;
11
11
  tab: LogFocusTab;
12
+ toolCallIndex?: number;
12
13
  source?: LogFocusSource;
13
14
  };
14
15
 
@@ -27,6 +28,7 @@ export function readLogFocusRequest(event: Event): LogFocusRequest | null {
27
28
  switch (tab) {
28
29
  case "anatomy":
29
30
  case "request":
31
+ case "response":
30
32
  break;
31
33
  case undefined:
32
34
  break;
@@ -35,11 +37,17 @@ export function readLogFocusRequest(event: Event): LogFocusRequest | null {
35
37
  }
36
38
 
37
39
  const source = safeGetOwnProperty(detail, "source");
40
+ const toolCallIndex = safeGetOwnProperty(detail, "toolCallIndex");
41
+ const focusRequest =
42
+ typeof toolCallIndex === "number" && Number.isInteger(toolCallIndex) && toolCallIndex >= 0
43
+ ? { logId, tab: tab ?? "request", toolCallIndex }
44
+ : { logId, tab: tab ?? "request" };
45
+
38
46
  switch (source) {
39
47
  case "virtualizer":
40
- return { logId, tab: tab ?? "request", source };
48
+ return { ...focusRequest, source };
41
49
  case undefined:
42
- return { logId, tab: tab ?? "request" };
50
+ return focusRequest;
43
51
  default:
44
52
  return null;
45
53
  }
@@ -181,18 +181,33 @@ function extractOpenAIToolTraceEvents(log: CapturedLog): ToolTraceEvent[] {
181
181
  for (const choice of choices) {
182
182
  const message = safeGetOwnProperty(choice, "message");
183
183
  const toolCalls = safeGetOwnProperty(message, "tool_calls");
184
- if (!Array.isArray(toolCalls)) continue;
185
- for (const call of toolCalls) {
186
- const fn = safeGetOwnProperty(call, "function");
187
- const name = safeGetOwnProperty(fn, "name");
188
- if (typeof name !== "string" || name.length === 0) continue;
189
- const args = safeGetOwnProperty(fn, "arguments");
184
+ if (Array.isArray(toolCalls)) {
185
+ for (const call of toolCalls) {
186
+ const fn = safeGetOwnProperty(call, "function");
187
+ const name = safeGetOwnProperty(fn, "name");
188
+ if (typeof name !== "string" || name.length === 0) continue;
189
+ const args = safeGetOwnProperty(fn, "arguments");
190
+ events.push({
191
+ id: `${String(log.id)}-openai-tool-${String(events.length)}`,
192
+ logId: log.id,
193
+ index: events.length,
194
+ provider: "openai",
195
+ name,
196
+ argumentsText: copyValue(args),
197
+ argumentsPreview: previewValue(args),
198
+ });
199
+ }
200
+ }
201
+ const functionCall = safeGetOwnProperty(message, "function_call");
202
+ const functionCallName = safeGetOwnProperty(functionCall, "name");
203
+ if (typeof functionCallName === "string" && functionCallName.length > 0) {
204
+ const args = safeGetOwnProperty(functionCall, "arguments");
190
205
  events.push({
191
206
  id: `${String(log.id)}-openai-tool-${String(events.length)}`,
192
207
  logId: log.id,
193
208
  index: events.length,
194
209
  provider: "openai",
195
- name,
210
+ name: functionCallName,
196
211
  argumentsText: copyValue(args),
197
212
  argumentsPreview: previewValue(args),
198
213
  });
@@ -1,4 +1,5 @@
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
  import { decodeSessionIdFromPath, encodeSessionIdForPath } from "../../lib/sessionUrl";
4
5
 
@@ -17,7 +18,32 @@ export const Route = createFileRoute("/session/$sessionId")({
17
18
  }),
18
19
  });
19
20
 
20
- function SessionViewerRoute(): React.JSX.Element {
21
+ function SessionViewerRoute(): JSX.Element {
21
22
  const { sessionId } = Route.useParams();
23
+ return <ClientOnlySessionViewer sessionId={sessionId} />;
24
+ }
25
+
26
+ function ClientOnlySessionViewer({ sessionId }: { sessionId: string }): JSX.Element {
27
+ const [mounted, setMounted] = useState(false);
28
+
29
+ useEffect(() => {
30
+ setMounted(true);
31
+ }, []);
32
+
33
+ if (!mounted) {
34
+ return (
35
+ <main className="min-h-screen bg-background text-foreground">
36
+ <div className="mx-auto flex min-h-screen w-full max-w-2xl flex-col justify-center px-6 py-16 text-center">
37
+ <div className="border border-border bg-card shadow-sm rounded-[8px] px-4 py-8">
38
+ <p className="text-sm font-medium text-muted-foreground">Loading session data...</p>
39
+ <p className="mt-3 inline-block max-w-full rounded bg-muted px-3 py-1 font-mono text-xs break-all text-muted-foreground">
40
+ {sessionId}
41
+ </p>
42
+ </div>
43
+ </div>
44
+ </main>
45
+ );
46
+ }
47
+
22
48
  return <ProxyViewerContainer key={sessionId} initialSessionId={sessionId} />;
23
49
  }