@tonyclaw/llm-inspector 1.16.4 → 1.16.5

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 (56) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/CompareDrawer-C1w4KUGZ.js +1 -0
  3. package/.output/public/assets/ReplayDialog-DR2Sgq_g.js +1 -0
  4. package/.output/public/assets/RequestAnatomy-DAre35kj.js +1 -0
  5. package/.output/public/assets/ResponseView-ackes7_g.js +1 -0
  6. package/.output/public/assets/StreamingChunkSequence-GrXwIGKA.js +1 -0
  7. package/.output/public/assets/index-BGzHFOEX.css +1 -0
  8. package/.output/public/assets/index-DX88k9br.js +101 -0
  9. package/.output/public/assets/json-viewer-C_QUhGeu.js +14 -0
  10. package/.output/public/assets/{main-DbWwVQFh.js → main-CDMdNDY_.js} +1 -1
  11. package/.output/server/_libs/lucide-react.mjs +104 -84
  12. package/.output/server/_ssr/CompareDrawer-ftkJxyk6.mjs +1040 -0
  13. package/.output/server/_ssr/ReplayDialog-DcmE3lj5.mjs +321 -0
  14. package/.output/server/_ssr/RequestAnatomy-rK_LNMdG.mjs +351 -0
  15. package/.output/server/_ssr/ResponseView-CbQ4n-aJ.mjs +601 -0
  16. package/.output/server/_ssr/StreamingChunkSequence-84FZkIzv.mjs +301 -0
  17. package/.output/server/_ssr/{index-C-z-fZtq.mjs → index-CDjLoMsk.mjs} +1026 -2455
  18. package/.output/server/_ssr/index.mjs +2 -2
  19. package/.output/server/_ssr/json-viewer-B-qpM5xC.mjs +510 -0
  20. package/.output/server/_ssr/{router-CNM9Kbi0.mjs → router-BrdjOUEW.mjs} +24 -14
  21. package/.output/server/{_tanstack-start-manifest_v-BWfLeIsC.mjs → _tanstack-start-manifest_v-DmOZEcJ3.mjs} +1 -1
  22. package/.output/server/index.mjs +68 -26
  23. package/package.json +1 -1
  24. package/src/components/OnboardingBanner.tsx +2 -2
  25. package/src/components/ProxyViewer.tsx +38 -26
  26. package/src/components/ProxyViewerContainer.tsx +3 -24
  27. package/src/components/proxy-viewer/ConversationGroup.tsx +1 -1
  28. package/src/components/proxy-viewer/ConversationHeader.tsx +4 -1
  29. package/src/components/proxy-viewer/LogEntry.tsx +213 -181
  30. package/src/components/proxy-viewer/LogEntryHeader.tsx +134 -36
  31. package/src/components/proxy-viewer/ThreadConnector.tsx +17 -2
  32. package/src/components/proxy-viewer/TurnGroup.tsx +94 -71
  33. package/src/components/proxy-viewer/anatomy/RequestAnatomy.tsx +98 -0
  34. package/src/components/proxy-viewer/anatomy/SegmentBar.tsx +196 -0
  35. package/src/components/proxy-viewer/anatomy/tokenEstimate.ts +53 -0
  36. package/src/components/proxy-viewer/anatomy/types.ts +39 -0
  37. package/src/components/proxy-viewer/anatomy/useAnatomyJump.ts +114 -0
  38. package/src/components/proxy-viewer/formats/anthropic/ContentBlocks.tsx +3 -23
  39. package/src/components/proxy-viewer/formats/anthropic/thinkingExtract.ts +21 -0
  40. package/src/components/proxy-viewer/formats/openai/ResponseView.tsx +5 -3
  41. package/src/components/proxy-viewer/lazy.ts +37 -0
  42. package/src/components/proxy-viewer/log-formats/anthropic.ts +146 -0
  43. package/src/components/proxy-viewer/log-formats/openai.ts +127 -0
  44. package/src/components/proxy-viewer/log-formats/types.ts +7 -0
  45. package/src/components/proxy-viewer/log-formats/unknown.ts +4 -0
  46. package/src/components/proxy-viewer/logEntryVisibility.ts +39 -0
  47. package/src/components/proxy-viewer/useKeyboardNavigation.ts +190 -0
  48. package/src/components/proxy-viewer/viewerState.ts +8 -0
  49. package/src/components/ui/crab-variants.tsx +11 -0
  50. package/src/components/ui/json-expansion-button.tsx +56 -0
  51. package/src/components/ui/json-viewer-bulk.ts +97 -0
  52. package/src/components/ui/json-viewer.tsx +58 -183
  53. package/src/lib/utils.ts +2 -3
  54. package/src/routes/api/logs.stream.ts +26 -16
  55. package/.output/public/assets/index-DRRCmu5p.css +0 -1
  56. package/.output/public/assets/index-X7CHS7fS.js +0 -107
@@ -35,54 +35,96 @@ const headers = ((m) => function headersRouteRule(event) {
35
35
  }
36
36
  });
37
37
  const assets = {
38
+ "/assets/alibaba-TTwafVwX.svg": {
39
+ "type": "image/svg+xml",
40
+ "etag": '"171b-6dyV5K8QjiaY35sN9qNprh9zDIs"',
41
+ "mtime": "2026-06-15T12:23:59.073Z",
42
+ "size": 5915,
43
+ "path": "../public/assets/alibaba-TTwafVwX.svg"
44
+ },
38
45
  "/assets/minimax-BPMzvuL-.jpeg": {
39
46
  "type": "image/jpeg",
40
47
  "etag": '"1b06-IwivU89ko5UTMUM1/t7hn4sQK9A"',
41
- "mtime": "2026-06-15T02:31:02.872Z",
48
+ "mtime": "2026-06-15T12:23:59.073Z",
42
49
  "size": 6918,
43
50
  "path": "../public/assets/minimax-BPMzvuL-.jpeg"
44
51
  },
45
- "/assets/alibaba-TTwafVwX.svg": {
46
- "type": "image/svg+xml",
47
- "etag": '"171b-6dyV5K8QjiaY35sN9qNprh9zDIs"',
48
- "mtime": "2026-06-15T02:31:02.873Z",
49
- "size": 5915,
50
- "path": "../public/assets/alibaba-TTwafVwX.svg"
52
+ "/assets/CompareDrawer-C1w4KUGZ.js": {
53
+ "type": "text/javascript; charset=utf-8",
54
+ "etag": '"4a10-U7vRVdbG90QCagHyB/PsN2Edv9Y"',
55
+ "mtime": "2026-06-15T12:23:59.073Z",
56
+ "size": 18960,
57
+ "path": "../public/assets/CompareDrawer-C1w4KUGZ.js"
58
+ },
59
+ "/assets/json-viewer-C_QUhGeu.js": {
60
+ "type": "text/javascript; charset=utf-8",
61
+ "etag": '"1e60c-NxhepLRPlTKqcPUc59jhJtOyGpg"',
62
+ "mtime": "2026-06-15T12:23:59.074Z",
63
+ "size": 124428,
64
+ "path": "../public/assets/json-viewer-C_QUhGeu.js"
51
65
  },
52
- "/assets/index-DRRCmu5p.css": {
66
+ "/assets/index-BGzHFOEX.css": {
53
67
  "type": "text/css; charset=utf-8",
54
- "etag": '"15b6a-YPwntwMQpIU2AWIj8lgGWZpCwQw"',
55
- "mtime": "2026-06-15T02:31:02.875Z",
56
- "size": 88938,
57
- "path": "../public/assets/index-DRRCmu5p.css"
68
+ "etag": '"16d02-Bx7KW/5wmN1RfMrbWqhaqF7lGHk"',
69
+ "mtime": "2026-06-15T12:23:59.073Z",
70
+ "size": 93442,
71
+ "path": "../public/assets/index-BGzHFOEX.css"
72
+ },
73
+ "/assets/ReplayDialog-DR2Sgq_g.js": {
74
+ "type": "text/javascript; charset=utf-8",
75
+ "etag": '"11b1-xYgR1cdDnesImArvyIDX3s2xRMU"',
76
+ "mtime": "2026-06-15T12:23:59.073Z",
77
+ "size": 4529,
78
+ "path": "../public/assets/ReplayDialog-DR2Sgq_g.js"
58
79
  },
59
- "/assets/main-DbWwVQFh.js": {
80
+ "/assets/ResponseView-ackes7_g.js": {
60
81
  "type": "text/javascript; charset=utf-8",
61
- "etag": '"50599-mPNNNxfSpwFWn5i6kPquWEqJjGk"',
62
- "mtime": "2026-06-15T02:31:02.875Z",
63
- "size": 329113,
64
- "path": "../public/assets/main-DbWwVQFh.js"
82
+ "etag": '"6e82-UP1WD/TqChwiuI7DV8LDxr8mcoI"',
83
+ "mtime": "2026-06-15T12:23:59.074Z",
84
+ "size": 28290,
85
+ "path": "../public/assets/ResponseView-ackes7_g.js"
65
86
  },
66
87
  "/assets/zhipuai-BPNAnxo-.svg": {
67
88
  "type": "image/svg+xml",
68
89
  "etag": '"2bf8-hNaLCTi89nOFCsIIfWpP/jrfo0s"',
69
- "mtime": "2026-06-15T02:31:02.873Z",
90
+ "mtime": "2026-06-15T12:23:59.073Z",
70
91
  "size": 11256,
71
92
  "path": "../public/assets/zhipuai-BPNAnxo-.svg"
72
93
  },
94
+ "/assets/RequestAnatomy-DAre35kj.js": {
95
+ "type": "text/javascript; charset=utf-8",
96
+ "etag": '"141b-+jsq3YAE5FysxZARhUPCzQUpXJE"',
97
+ "mtime": "2026-06-15T12:23:59.074Z",
98
+ "size": 5147,
99
+ "path": "../public/assets/RequestAnatomy-DAre35kj.js"
100
+ },
101
+ "/assets/main-CDMdNDY_.js": {
102
+ "type": "text/javascript; charset=utf-8",
103
+ "etag": '"505ae-1nSD340FKDRNgunwXT0ErZchYkU"',
104
+ "mtime": "2026-06-15T12:23:59.074Z",
105
+ "size": 329134,
106
+ "path": "../public/assets/main-CDMdNDY_.js"
107
+ },
108
+ "/assets/StreamingChunkSequence-GrXwIGKA.js": {
109
+ "type": "text/javascript; charset=utf-8",
110
+ "etag": '"d72-eczKwdvLd8GmG1QNwNn/d+VEOq0"',
111
+ "mtime": "2026-06-15T12:23:59.074Z",
112
+ "size": 3442,
113
+ "path": "../public/assets/StreamingChunkSequence-GrXwIGKA.js"
114
+ },
115
+ "/assets/index-DX88k9br.js": {
116
+ "type": "text/javascript; charset=utf-8",
117
+ "etag": '"738e0-qSt/VXgdC2y0xa/XDlbeCzFrP6k"',
118
+ "mtime": "2026-06-15T12:23:59.073Z",
119
+ "size": 473312,
120
+ "path": "../public/assets/index-DX88k9br.js"
121
+ },
73
122
  "/assets/qwen-CONDcHqt.png": {
74
123
  "type": "image/png",
75
124
  "etag": '"572c3-cdJAPaHdOvFCGzuaQjagdgOu6XE"',
76
- "mtime": "2026-06-15T02:31:02.875Z",
125
+ "mtime": "2026-06-15T12:23:59.073Z",
77
126
  "size": 357059,
78
127
  "path": "../public/assets/qwen-CONDcHqt.png"
79
- },
80
- "/assets/index-X7CHS7fS.js": {
81
- "type": "text/javascript; charset=utf-8",
82
- "etag": '"9c554-RW/7IDotWnzOK06cvwJGAOWPrdk"',
83
- "mtime": "2026-06-15T02:31:02.875Z",
84
- "size": 640340,
85
- "path": "../public/assets/index-X7CHS7fS.js"
86
128
  }
87
129
  };
88
130
  function readAsset(id) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tonyclaw/llm-inspector",
3
- "version": "1.16.4",
3
+ "version": "1.16.5",
4
4
  "type": "module",
5
5
  "description": "LLM API proxy inspector — captures and displays requests/responses from AI coding tools in a web UI",
6
6
  "license": "MIT",
@@ -46,10 +46,10 @@ export function OnboardingBanner(): JSX.Element | null {
46
46
  onClick={() => {
47
47
  void markSeen();
48
48
  }}
49
- className="inline-flex items-center gap-1.5 text-xs px-2.5 py-1.5 rounded-md border border-amber-500/40 text-amber-700 dark:text-amber-300 hover:bg-amber-500/10 transition-colors shrink-0"
49
+ className="inline-flex items-center gap-1.5 text-xs h-8 px-3 rounded-md border border-amber-500/40 text-amber-700 dark:text-amber-300 hover:bg-amber-500/10 transition-colors shrink-0"
50
50
  aria-label="Dismiss onboarding tip"
51
51
  >
52
- <Check className="size-3" />
52
+ <Check className="size-3.5" />
53
53
  Got it
54
54
  </button>
55
55
  <button
@@ -1,4 +1,4 @@
1
- import { type JSX, useCallback, useEffect, useMemo, useState } from "react";
1
+ import { type JSX, useCallback, useEffect, useMemo, useRef, useState, Suspense } from "react";
2
2
  import { Download } from "lucide-react";
3
3
 
4
4
  import type { CapturedLog } from "../proxy/schemas";
@@ -11,8 +11,9 @@ import { crabVariants } from "./ui/crab-variants";
11
11
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
12
12
  import { SettingsDialog } from "./providers/SettingsDialog";
13
13
  import { computeCacheTrends } from "./proxy-viewer/cacheTrend";
14
- import { CompareDrawer } from "./proxy-viewer/CompareDrawer";
14
+ import { LazyCompareDrawer } from "./proxy-viewer/lazy";
15
15
  import { buildValidPredecessors } from "./proxy-viewer/viewerState";
16
+ import { useKeyboardNavigation } from "./proxy-viewer/useKeyboardNavigation";
16
17
 
17
18
  function truncateSessionId(id: string): string {
18
19
  if (id.length <= 30) return id;
@@ -119,6 +120,9 @@ export function ProxyViewer({
119
120
  const [crabEntrancePhase, setCrabEntrancePhase] = useState<"hidden" | "playing" | "done">(
120
121
  "hidden",
121
122
  );
123
+ const logListRef = useRef<HTMLDivElement>(null);
124
+ const logListWrapperRef = useRef<HTMLDivElement>(null);
125
+ useKeyboardNavigation(logListRef, logListWrapperRef);
122
126
 
123
127
  useEffect(() => {
124
128
  const perCrabDuration = 400;
@@ -162,9 +166,9 @@ export function ProxyViewer({
162
166
  );
163
167
 
164
168
  return (
165
- <div className="max-w-[1200px] mx-auto flex flex-col h-screen" style={{ maxHeight: "100vh" }}>
169
+ <div className="max-w-[1400px] xl:max-w-[1600px] 2xl:max-w-[1800px] mx-auto px-6 pb-6">
166
170
  {/* Brand row */}
167
- <div className="flex items-end px-6 pt-6 pb-8 relative">
171
+ <div className="flex items-end pt-6 pb-8 relative">
168
172
  <h1 className="text-lg font-bold flex items-end gap-2 absolute left-1/2 -translate-x-1/2 whitespace-nowrap">
169
173
  {/* Crab family — hover to animate together */}
170
174
  <span className="flex items-end gap-1 group cursor-default" aria-hidden="true">
@@ -217,7 +221,7 @@ export function ProxyViewer({
217
221
  </div>
218
222
 
219
223
  {/* Controls + Filters */}
220
- <div className="flex items-center gap-3 px-6 mb-4">
224
+ <div className="flex items-center gap-3 mb-4">
221
225
  <Select value={selectedSession} onValueChange={onSessionChange}>
222
226
  <SelectTrigger className="flex-1 max-w-[350px] text-xs">
223
227
  <SelectValue placeholder="All sessions" />
@@ -248,7 +252,7 @@ export function ProxyViewer({
248
252
  <button
249
253
  type="button"
250
254
  onClick={() => onViewModeChange("simple")}
251
- className={`px-2 py-1 cursor-pointer transition-colors text-xs ${
255
+ className={`h-8 px-3 cursor-pointer transition-colors text-xs ${
252
256
  viewMode === "simple"
253
257
  ? "bg-muted text-foreground"
254
258
  : "text-muted-foreground hover:bg-muted/50"
@@ -259,7 +263,7 @@ export function ProxyViewer({
259
263
  <button
260
264
  type="button"
261
265
  onClick={() => onViewModeChange("full")}
262
- className={`px-2 py-1 cursor-pointer transition-colors text-xs ${
266
+ className={`h-8 px-3 cursor-pointer transition-colors text-xs ${
263
267
  viewMode === "full"
264
268
  ? "bg-muted text-foreground"
265
269
  : "text-muted-foreground hover:bg-muted/50"
@@ -282,14 +286,14 @@ export function ProxyViewer({
282
286
  void handleExport();
283
287
  }}
284
288
  disabled={exporting}
285
- className="text-xs text-muted-foreground hover:text-foreground transition-colors cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed inline-flex items-center gap-1"
289
+ className="h-8 px-3 text-xs text-muted-foreground hover:text-foreground transition-colors cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed inline-flex items-center gap-1.5 rounded-md hover:bg-muted"
286
290
  title="Export all logs as JSON ZIP"
287
291
  >
288
292
  {exporting ? (
289
293
  <span>Exporting...</span>
290
294
  ) : (
291
295
  <>
292
- <Download className="size-3" />
296
+ <Download className="size-3.5" />
293
297
  <span>Export</span>
294
298
  </>
295
299
  )}
@@ -298,7 +302,7 @@ export function ProxyViewer({
298
302
  <button
299
303
  type="button"
300
304
  onClick={onClearAll}
301
- className="text-xs text-muted-foreground hover:text-foreground transition-colors cursor-pointer"
305
+ className="h-8 px-3 text-xs text-muted-foreground hover:text-foreground transition-colors cursor-pointer rounded-md hover:bg-muted"
302
306
  title="Clear all logs"
303
307
  >
304
308
  Clear
@@ -306,7 +310,7 @@ export function ProxyViewer({
306
310
  </div>
307
311
 
308
312
  {/* Log list */}
309
- <div className="flex-1 min-h-0 px-6 pb-6">
313
+ <div>
310
314
  {logs.length === 0 ? (
311
315
  <div className="text-center text-muted-foreground py-16 space-y-4">
312
316
  <p className="text-sm">No requests captured yet.</p>
@@ -314,27 +318,35 @@ export function ProxyViewer({
314
318
  <CopyableCommand command="LLM_BASE_URL=http://localhost:25947/proxy <your-tool>" />
315
319
  </div>
316
320
  ) : (
317
- <div className="overflow-y-auto h-full flex flex-col gap-2">
318
- {groups.map((group) => (
319
- <ConversationGroup
320
- key={group.id}
321
- group={group}
322
- viewMode={viewMode}
323
- strip={strip}
324
- cacheTrends={cacheTrends}
325
- onCompareWithPrevious={handleCompareWithPrevious}
326
- comparisonPredecessors={comparisonPredecessors}
327
- onClearGroup={onClearGroup}
328
- standalone={groups.length === 1}
329
- />
330
- ))}
321
+ <div
322
+ ref={logListWrapperRef}
323
+ tabIndex={0}
324
+ className="flex flex-col gap-2 focus:outline-none"
325
+ >
326
+ <div ref={logListRef}>
327
+ {groups.map((group) => (
328
+ <ConversationGroup
329
+ key={group.id}
330
+ group={group}
331
+ viewMode={viewMode}
332
+ strip={strip}
333
+ cacheTrends={cacheTrends}
334
+ onCompareWithPrevious={handleCompareWithPrevious}
335
+ comparisonPredecessors={comparisonPredecessors}
336
+ onClearGroup={onClearGroup}
337
+ standalone={groups.length === 1}
338
+ />
339
+ ))}
340
+ </div>
331
341
  </div>
332
342
  )}
333
343
  </div>
334
344
 
335
345
  {/* Compare drawer — sibling of the log list, not a route change. */}
336
346
  {comparePair !== null && (
337
- <CompareDrawer left={comparePair[0]} right={comparePair[1]} onClose={closeCompare} />
347
+ <Suspense fallback={null}>
348
+ <LazyCompareDrawer left={comparePair[0]} right={comparePair[1]} onClose={closeCompare} />
349
+ </Suspense>
338
350
  )}
339
351
  </div>
340
352
  );
@@ -67,8 +67,6 @@ export function ProxyViewerContainer(): JSX.Element {
67
67
  // connection never re-opens on filter change — we always carry the full
68
68
  // set and derive the displayed view with `useMemo` below.
69
69
  const [allLogs, setAllLogs] = useState<CapturedLog[]>([]);
70
- const [sessions, setSessions] = useState<string[]>([]);
71
- const [models, setModels] = useState<string[]>([]);
72
70
  const [selectedSession, setSelectedSession] = useState("__all__");
73
71
  const [selectedModel, setSelectedModel] = useState("__all__");
74
72
  const [viewMode, setViewMode] = useState<"simple" | "full">("simple");
@@ -78,7 +76,6 @@ export function ProxyViewerContainer(): JSX.Element {
78
76
 
79
77
  // O(1) log lookup by id
80
78
  const logIndexRef = useRef<Map<number, number>>(new Map());
81
- const logsRef = useRef<CapturedLog[]>([]);
82
79
 
83
80
  // Debounce buffer for SSE updates
84
81
  const pendingUpdatesRef = useRef<CapturedLog[]>([]);
@@ -90,6 +87,8 @@ export function ProxyViewerContainer(): JSX.Element {
90
87
  () => filterLogs(allLogs, selectedSession, selectedModel),
91
88
  [allLogs, selectedSession, selectedModel],
92
89
  );
90
+ const sessions = useMemo(() => extractSessions(allLogs), [allLogs]);
91
+ const models = useMemo(() => extractModels(allLogs), [allLogs]);
93
92
 
94
93
  const flushUpdates = useCallback(() => {
95
94
  flushTimerRef.current = null;
@@ -99,8 +98,6 @@ export function ProxyViewerContainer(): JSX.Element {
99
98
 
100
99
  setAllLogs((prev) => {
101
100
  let next = prev;
102
- let sessionsChanged = false;
103
- let modelsChanged = false;
104
101
  for (const log of updates) {
105
102
  const idx = logIndexRef.current.get(log.id);
106
103
  if (idx !== undefined) {
@@ -110,19 +107,10 @@ export function ProxyViewerContainer(): JSX.Element {
110
107
  logIndexRef.current.set(log.id, next.length);
111
108
  next = [...next, log];
112
109
  }
113
- if (log.sessionId !== null && log.sessionId !== "" && !sessions.includes(log.sessionId)) {
114
- sessionsChanged = true;
115
- }
116
- if (log.model !== null && log.model !== "" && !models.includes(log.model)) {
117
- modelsChanged = true;
118
- }
119
110
  }
120
- logsRef.current = next;
121
- if (sessionsChanged) setSessions((s) => extractSessions(next));
122
- if (modelsChanged) setModels((m) => extractModels(next));
123
111
  return next;
124
112
  });
125
- }, [sessions, models]);
113
+ }, []);
126
114
 
127
115
  const scheduleUpdate = useCallback(
128
116
  (log: CapturedLog) => {
@@ -173,10 +161,7 @@ export function ProxyViewerContainer(): JSX.Element {
173
161
  if (log !== undefined) idx.set(log.id, i);
174
162
  }
175
163
  logIndexRef.current = idx;
176
- logsRef.current = update.logs;
177
164
  setAllLogs(update.logs);
178
- setSessions(extractSessions(update.logs));
179
- setModels(extractModels(update.logs));
180
165
  setError(null);
181
166
  } else if (update.type === "update") {
182
167
  scheduleUpdate(update.log);
@@ -223,10 +208,7 @@ export function ProxyViewerContainer(): JSX.Element {
223
208
  return;
224
209
  }
225
210
  logIndexRef.current.clear();
226
- logsRef.current = [];
227
211
  setAllLogs([]);
228
- setSessions([]);
229
- setModels([]);
230
212
  setError(null);
231
213
  } catch (err) {
232
214
  setError(err instanceof Error ? err.message : "Unknown error clearing logs");
@@ -257,9 +239,6 @@ export function ProxyViewerContainer(): JSX.Element {
257
239
  if (log !== undefined) idx.set(log.id, i);
258
240
  }
259
241
  logIndexRef.current = idx;
260
- logsRef.current = remaining;
261
- setSessions(extractSessions(remaining));
262
- setModels(extractModels(remaining));
263
242
  return remaining;
264
243
  });
265
244
  setError(null);
@@ -90,7 +90,7 @@ export const ConversationGroup = memo(function ({
90
90
  )}
91
91
 
92
92
  {shouldRenderConversationContent(standalone, expanded) && (
93
- <div className="max-h-[70vh] overflow-y-auto">
93
+ <div>
94
94
  {turnGroups.map((tg) => (
95
95
  <TurnGroup
96
96
  key={tg.turnIndex}
@@ -78,11 +78,14 @@ export function ConversationHeader({
78
78
  <div
79
79
  role="button"
80
80
  tabIndex={0}
81
+ data-nav-id={`conv-${conversationId}`}
82
+ data-nav-action={expanded ? "collapse" : "expand"}
81
83
  className={cn(
82
84
  "flex items-center gap-3 px-3 py-2 cursor-pointer transition-colors",
83
85
  "hover:bg-muted/50",
84
86
  "select-none",
85
87
  "border border-border rounded-lg mb-2 bg-background sticky top-0 z-10",
88
+ "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:outline-none",
86
89
  )}
87
90
  onClick={onToggle}
88
91
  onKeyDown={(e) => {
@@ -174,7 +177,7 @@ export function ConversationHeader({
174
177
  onClick={handleClearClick}
175
178
  aria-label={`Clear group (${totalCalls} request${totalCalls !== 1 ? "s" : ""})`}
176
179
  title="Clear this group"
177
- className="text-muted-foreground hover:text-foreground transition-colors shrink-0 inline-flex items-center justify-center size-6 rounded hover:bg-muted cursor-pointer"
180
+ className="text-muted-foreground hover:text-foreground transition-colors shrink-0 inline-flex items-center justify-center size-8 rounded hover:bg-muted cursor-pointer"
178
181
  >
179
182
  <Trash2 className="size-3.5" />
180
183
  </button>