@tonyclaw/agent-inspector 2.0.43 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/{CompareDrawer-CxM1gCfL.js → CompareDrawer-VDpcSNGM.js} +1 -1
  3. package/.output/public/assets/ProxyViewerContainer-fsXSjqtm.js +117 -0
  4. package/.output/public/assets/{ReplayDialog-UseUkucS.js → ReplayDialog-CoDgTzHR.js} +1 -1
  5. package/.output/public/assets/RequestAnatomy-t3plgkPE.js +1 -0
  6. package/.output/public/assets/ResponseView-BYuKiNeG.js +1 -0
  7. package/.output/public/assets/{StreamingChunkSequence-JSQEPeNS.js → StreamingChunkSequence-DOdU38Is.js} +1 -1
  8. package/.output/public/assets/_sessionId-ew5QlyZ2.js +1 -0
  9. package/.output/public/assets/index-DFICWD6o.js +1 -0
  10. package/.output/public/assets/index-zLLCkAnF.css +1 -0
  11. package/.output/public/assets/{main-BpGVJcpB.js → main-DmzLt6ti.js} +2 -2
  12. package/.output/server/_libs/lucide-react.mjs +219 -178
  13. package/.output/server/{_sessionId-DGn-TENM.mjs → _sessionId-CCN1UoEf.mjs} +3 -3
  14. package/.output/server/_ssr/{CompareDrawer-CFElCSYY.mjs → CompareDrawer-DkSY3vxP.mjs} +4 -4
  15. package/.output/server/_ssr/{ProxyViewerContainer-B7SR9mrD.mjs → ProxyViewerContainer-OTh1V_Kh.mjs} +798 -394
  16. package/.output/server/_ssr/{ReplayDialog-DfKapj0k.mjs → ReplayDialog-DhI6yxrR.mjs} +5 -5
  17. package/.output/server/_ssr/{RequestAnatomy-CUxTCg31.mjs → RequestAnatomy-DN1SJVVL.mjs} +4 -4
  18. package/.output/server/_ssr/{ResponseView-BXY0w197.mjs → ResponseView-DW-kpJL5.mjs} +4 -4
  19. package/.output/server/_ssr/{StreamingChunkSequence-CzMnES9H.mjs → StreamingChunkSequence-BAE57Fae.mjs} +4 -4
  20. package/.output/server/_ssr/{index-CVlT-REW.mjs → index-C9ryJEna.mjs} +3 -3
  21. package/.output/server/_ssr/index.mjs +2 -2
  22. package/.output/server/_ssr/{router-B2d1LUx6.mjs → router-DIIJGyML.mjs} +458 -10
  23. package/.output/server/{_tanstack-start-manifest_v-Drpi28BM.mjs → _tanstack-start-manifest_v-C_v9-E5d.mjs} +1 -1
  24. package/.output/server/index.mjs +63 -63
  25. package/package.json +1 -1
  26. package/src/components/OnboardingBanner.tsx +74 -68
  27. package/src/components/ProxyViewer.tsx +869 -363
  28. package/src/components/ProxyViewerContainer.tsx +135 -3
  29. package/src/components/proxy-viewer/LogEntryHeader.tsx +12 -6
  30. package/src/components/ui/crab-logo.tsx +0 -50
  31. package/src/proxy/logIndex.ts +188 -1
  32. package/src/proxy/store.ts +405 -3
  33. package/src/routes/api/logs.ts +36 -0
  34. package/.output/public/assets/ProxyViewerContainer-TtRG-0E7.js +0 -117
  35. package/.output/public/assets/RequestAnatomy-aPxgEJ2L.js +0 -1
  36. package/.output/public/assets/ResponseView-DA-F4F97.js +0 -1
  37. package/.output/public/assets/_sessionId-BEXuCWq5.js +0 -1
  38. package/.output/public/assets/index-Bhsa_2xX.js +0 -1
  39. package/.output/public/assets/index-DOWlRJ0W.css +0 -1
@@ -1,10 +1,15 @@
1
1
  import { useState, useEffect, useCallback, useRef, useMemo, type JSX } from "react";
2
2
  import { z } from "zod";
3
3
  import { CapturedLogSchema, type CapturedLog } from "../contracts";
4
+ import { fetchJson } from "../lib/apiClient";
4
5
  import { InspectorGroupsListResponseSchema, type InspectorGroup } from "../lib/groupContract";
5
6
  import { useStripConfig } from "../lib/useStripConfig";
6
7
  import { OnboardingBanner } from "./OnboardingBanner";
7
- import { ProxyViewer, type SessionMembershipEvidence } from "./ProxyViewer";
8
+ import {
9
+ ProxyViewer,
10
+ type LogPaginationControls,
11
+ type SessionMembershipEvidence,
12
+ } from "./ProxyViewer";
8
13
  import { dispatchLogFocusRequest } from "./proxy-viewer/logFocus";
9
14
 
10
15
  type SSEUpdate =
@@ -28,6 +33,24 @@ const SSEUpdateSchema = z.union([
28
33
  }),
29
34
  ]);
30
35
 
36
+ const LogCursorPageSchema = z.object({
37
+ logs: z.array(CapturedLogSchema),
38
+ total: z.number().int().nonnegative(),
39
+ limit: z.number().int().positive(),
40
+ hasOlder: z.boolean(),
41
+ hasNewer: z.boolean(),
42
+ oldestLogId: z.number().int().positive().nullable(),
43
+ newestLogId: z.number().int().positive().nullable(),
44
+ });
45
+
46
+ type LogCursorPage = z.infer<typeof LogCursorPageSchema>;
47
+
48
+ type SessionPageRequest =
49
+ | { kind: "newest" }
50
+ | { kind: "oldest" }
51
+ | { kind: "older"; beforeLogId: number }
52
+ | { kind: "newer"; afterLogId: number };
53
+
31
54
  function buildSessionMembershipEvidence(
32
55
  groups: readonly InspectorGroup[],
33
56
  sessionId: string,
@@ -89,6 +112,7 @@ const DEBOUNCE_MS = 50;
89
112
  const HASH_SCROLL_ATTEMPTS = 12;
90
113
  const HASH_HIGHLIGHT_MS = 1800;
91
114
  const MAX_CLIENT_LOGS = 500;
115
+ const SESSION_PAGE_LIMIT = 100;
92
116
 
93
117
  function buildLogsStreamUrl(sessionId: string | undefined): string {
94
118
  const params = new URLSearchParams();
@@ -100,6 +124,31 @@ function buildLogsStreamUrl(sessionId: string | undefined): string {
100
124
  return query.length > 0 ? `/api/logs/stream?${query}` : "/api/logs/stream";
101
125
  }
102
126
 
127
+ function buildSessionLogsPageUrl(sessionId: string, request: SessionPageRequest): string {
128
+ const params = new URLSearchParams({
129
+ cursor: "1",
130
+ compact: "1",
131
+ limit: String(SESSION_PAGE_LIMIT),
132
+ sessionId,
133
+ });
134
+
135
+ switch (request.kind) {
136
+ case "oldest":
137
+ params.set("anchor", "oldest");
138
+ break;
139
+ case "older":
140
+ params.set("beforeLogId", String(request.beforeLogId));
141
+ break;
142
+ case "newer":
143
+ params.set("afterLogId", String(request.afterLogId));
144
+ break;
145
+ case "newest":
146
+ break;
147
+ }
148
+
149
+ return `/api/logs?${params.toString()}`;
150
+ }
151
+
103
152
  function buildLogIndex(logs: readonly CapturedLog[]): Map<number, number> {
104
153
  const idx = new Map<number, number>();
105
154
  for (let i = 0; i < logs.length; i++) {
@@ -138,10 +187,13 @@ export function ProxyViewerContainer({
138
187
  const [viewMode, setViewMode] = useState<"simple" | "full">("simple");
139
188
  const [error, setError] = useState<string | null>(null);
140
189
  const [streamInitialized, setStreamInitialized] = useState(initialSessionId === undefined);
190
+ const [logPage, setLogPage] = useState<LogCursorPage | null>(null);
191
+ const [sessionPageLoading, setSessionPageLoading] = useState(initialSessionId !== undefined);
141
192
  const [sessionMemberships, setSessionMemberships] = useState<SessionMembershipEvidence[]>([]);
142
193
  const eventSourceRef = useRef<EventSource | null>(null);
143
194
  const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
144
195
  const handledHashRef = useRef<string | null>(null);
196
+ const sessionPageRequestIdRef = useRef(0);
145
197
 
146
198
  // O(1) log lookup by id
147
199
  const logIndexRef = useRef<Map<number, number>>(new Map());
@@ -245,6 +297,37 @@ export function ProxyViewerContainer({
245
297
  };
246
298
  }, [initialSessionId, scheduleUpdate]);
247
299
 
300
+ const loadSessionPage = useCallback(
301
+ (request: SessionPageRequest) => {
302
+ if (initialSessionId === undefined) return;
303
+
304
+ const requestId = sessionPageRequestIdRef.current + 1;
305
+ sessionPageRequestIdRef.current = requestId;
306
+ setSessionPageLoading(true);
307
+ setError(null);
308
+
309
+ void fetchJson(buildSessionLogsPageUrl(initialSessionId, request), LogCursorPageSchema)
310
+ .then((page) => {
311
+ if (sessionPageRequestIdRef.current !== requestId) return;
312
+ const nextLogs = page.logs;
313
+ logIndexRef.current = buildLogIndex(nextLogs);
314
+ setAllLogs(nextLogs);
315
+ setLogPage(page);
316
+ setSelectedSession(initialSessionId);
317
+ setSessionPageLoading(false);
318
+ setError(null);
319
+ })
320
+ .catch((err: unknown) => {
321
+ if (sessionPageRequestIdRef.current !== requestId) return;
322
+ setAllLogs([]);
323
+ setLogPage(null);
324
+ setSessionPageLoading(false);
325
+ setError(err instanceof Error ? err.message : "Failed to load session logs");
326
+ });
327
+ },
328
+ [initialSessionId],
329
+ );
330
+
248
331
  useEffect(() => {
249
332
  if (initialSessionId === undefined) {
250
333
  setSessionMemberships([]);
@@ -273,6 +356,12 @@ export function ProxyViewerContainer({
273
356
  }, [initialSessionId]);
274
357
 
275
358
  useEffect(() => {
359
+ if (initialSessionId === undefined) return;
360
+ loadSessionPage({ kind: "newest" });
361
+ }, [initialSessionId, loadSessionPage]);
362
+
363
+ useEffect(() => {
364
+ if (initialSessionId !== undefined) return undefined;
276
365
  connectSSE();
277
366
  return () => {
278
367
  if (eventSourceRef.current) {
@@ -288,7 +377,7 @@ export function ProxyViewerContainer({
288
377
  flushTimerRef.current = null;
289
378
  }
290
379
  };
291
- }, [connectSSE]);
380
+ }, [connectSSE, initialSessionId]);
292
381
 
293
382
  useEffect(() => {
294
383
  const hash = window.location.hash;
@@ -402,6 +491,48 @@ export function ProxyViewerContainer({
402
491
  // Read the strip config once at the container so the virtualized list does
403
492
  // not need N independent SWR subscriptions per row.
404
493
  const { strip, captureMode, slowResponseThresholdSeconds, timeDisplayFormat } = useStripConfig();
494
+ const loadOldestPage = useCallback(() => {
495
+ loadSessionPage({ kind: "oldest" });
496
+ }, [loadSessionPage]);
497
+ const loadNewestPage = useCallback(() => {
498
+ loadSessionPage({ kind: "newest" });
499
+ }, [loadSessionPage]);
500
+ const loadOlderPage = useCallback(() => {
501
+ const firstLog = allLogs[0];
502
+ const beforeLogId = logPage?.oldestLogId ?? firstLog?.id ?? null;
503
+ if (beforeLogId === null) return;
504
+ loadSessionPage({ kind: "older", beforeLogId });
505
+ }, [allLogs, loadSessionPage, logPage]);
506
+ const loadNewerPage = useCallback(() => {
507
+ const lastLog = allLogs[allLogs.length - 1];
508
+ const afterLogId = logPage?.newestLogId ?? lastLog?.id ?? null;
509
+ if (afterLogId === null) return;
510
+ loadSessionPage({ kind: "newer", afterLogId });
511
+ }, [allLogs, loadSessionPage, logPage]);
512
+ const pagination = useMemo<LogPaginationControls | undefined>(() => {
513
+ if (initialSessionId === undefined) return undefined;
514
+ return {
515
+ isLoading: sessionPageLoading,
516
+ total: logPage?.total ?? null,
517
+ pageSize: logPage?.limit ?? SESSION_PAGE_LIMIT,
518
+ hasOlder: logPage?.hasOlder ?? false,
519
+ hasNewer: logPage?.hasNewer ?? false,
520
+ oldestLogId: logPage?.oldestLogId ?? null,
521
+ newestLogId: logPage?.newestLogId ?? null,
522
+ onOldest: loadOldestPage,
523
+ onOlder: loadOlderPage,
524
+ onNewer: loadNewerPage,
525
+ onNewest: loadNewestPage,
526
+ };
527
+ }, [
528
+ initialSessionId,
529
+ loadNewestPage,
530
+ loadNewerPage,
531
+ loadOlderPage,
532
+ loadOldestPage,
533
+ logPage,
534
+ sessionPageLoading,
535
+ ]);
405
536
 
406
537
  return (
407
538
  <>
@@ -421,7 +552,8 @@ export function ProxyViewerContainer({
421
552
  onModelChange={setSelectedModel}
422
553
  onClearAll={handleClearAll}
423
554
  onClearGroup={handleClearGroup}
424
- isLoading={initialSessionId !== undefined && !streamInitialized}
555
+ isLoading={initialSessionId === undefined ? !streamInitialized : sessionPageLoading}
556
+ pagination={pagination}
425
557
  sessionMemberships={sessionMemberships}
426
558
  viewMode={viewMode}
427
559
  onViewModeChange={setViewMode}
@@ -170,7 +170,7 @@ export const LogEntryHeader = memo(function ({
170
170
  data-nav-id={`log-${log.id}`}
171
171
  data-nav-action={expanded ? "collapse" : "expand"}
172
172
  className={cn(
173
- "flex items-center gap-2 px-3 py-1 cursor-pointer transition-colors",
173
+ "flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 px-3 py-1.5 cursor-pointer transition-colors",
174
174
  "hover:bg-muted/50",
175
175
  "select-none",
176
176
  "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:outline-none",
@@ -302,17 +302,23 @@ export const LogEntryHeader = memo(function ({
302
302
 
303
303
  {/* Token counts */}
304
304
  {hasTokens && (
305
- <span className="flex items-center gap-1 text-xs shrink-0">
305
+ <span className="flex min-w-0 items-center gap-1 text-xs sm:shrink-0">
306
306
  <Zap className="size-3 text-muted-foreground" />
307
- <span className="font-mono tabular-nums">
307
+ <span className="flex min-w-0 flex-wrap items-center gap-x-1 font-mono tabular-nums">
308
308
  <span
309
- className={log.inputTokens !== null ? "text-blue-400" : "text-muted-foreground"}
309
+ className={cn(
310
+ "whitespace-nowrap",
311
+ log.inputTokens !== null ? "text-blue-400" : "text-muted-foreground",
312
+ )}
310
313
  >
311
314
  IN {log.inputTokens !== null ? formatTokens(log.inputTokens) : "—"}
312
315
  </span>
313
- {" / "}
316
+ <span className="text-muted-foreground">/</span>
314
317
  <span
315
- className={log.outputTokens !== null ? "text-amber-400" : "text-muted-foreground"}
318
+ className={cn(
319
+ "whitespace-nowrap",
320
+ log.outputTokens !== null ? "text-amber-400" : "text-muted-foreground",
321
+ )}
316
322
  >
317
323
  OUT {log.outputTokens !== null ? formatTokens(log.outputTokens) : "—"}
318
324
  </span>
@@ -40,56 +40,6 @@ export function CrabLogo({ className }: { className?: string }): JSX.Element {
40
40
  <line x1="9" y1="17.5" x2="8" y2="20.5" />
41
41
  <line x1="15" y1="17.5" x2="16" y2="20.5" />
42
42
  <line x1="17.5" y1="16" x2="19.5" y2="19.5" />
43
- {/* Duanwu zongzi charms */}
44
- <g>
45
- <path
46
- d="M5.6 17.7 L8.4 15.4 L10.8 18 C9.1 18.9 7.2 18.8 5.6 17.7 Z"
47
- fill="#2f6b3f"
48
- stroke="none"
49
- opacity="0.95"
50
- />
51
- <path
52
- d="M6.9 17.5 L8.4 16 L9.8 17.7 C8.9 18.1 7.8 18.1 6.9 17.5 Z"
53
- fill="#6fb36f"
54
- stroke="none"
55
- opacity="0.95"
56
- />
57
- <path d="M8.4 16 L8.4 18.4" stroke="#d6b45f" strokeWidth="0.34" />
58
- <circle cx="10.6" cy="17.75" r="0.36" fill="#c2412d" stroke="none" />
59
- </g>
60
- <g>
61
- <path
62
- d="M8 16.5 L11.9 13.6 L15.8 16.8 C13.3 18.4 10.4 18.3 8 16.5 Z"
63
- fill="#2f6b3f"
64
- stroke="none"
65
- opacity="0.97"
66
- />
67
- <path
68
- d="M9.8 16.3 L11.9 14.4 L14.3 16.5 C12.8 17.4 11.3 17.4 9.8 16.3 Z"
69
- fill="#6fb36f"
70
- stroke="none"
71
- opacity="0.95"
72
- />
73
- <path d="M11.9 14.3 L11.9 17.8" stroke="#d6b45f" strokeWidth="0.42" />
74
- <path d="M9.8 16.3 C11.2 15.8 12.9 15.9 14.3 16.5" stroke="#9fca78" strokeWidth="0.36" />
75
- <circle cx="15.6" cy="16.75" r="0.43" fill="#c2412d" stroke="none" />
76
- </g>
77
- <g>
78
- <path
79
- d="M13.6 18.1 L16.2 15.7 L18.5 18.2 C17.1 19.1 15.2 19 13.6 18.1 Z"
80
- fill="#2f6b3f"
81
- stroke="none"
82
- opacity="0.95"
83
- />
84
- <path
85
- d="M14.8 17.9 L16.2 16.3 L17.5 18 C16.6 18.4 15.7 18.4 14.8 17.9 Z"
86
- fill="#6fb36f"
87
- stroke="none"
88
- opacity="0.95"
89
- />
90
- <path d="M16.2 16.3 L16.2 18.6" stroke="#d6b45f" strokeWidth="0.34" />
91
- <circle cx="18.35" cy="18.15" r="0.36" fill="#c2412d" stroke="none" />
92
- </g>
93
43
  </svg>
94
44
  );
95
45
  }
@@ -4,12 +4,56 @@ import { join, dirname } from "node:path";
4
4
  import { createInterface } from "node:readline";
5
5
  import { Buffer } from "node:buffer";
6
6
  import { resolveLogDir, logger } from "./logger";
7
+ import type { ApiFormat, CapturedLog } from "./schemas";
8
+
9
+ export type LogIndexSummary = {
10
+ id: number;
11
+ timestamp: string;
12
+ method: string;
13
+ path: string;
14
+ model: string | null;
15
+ sessionId: string | null;
16
+ responseStatus: number | null;
17
+ inputTokens: number | null;
18
+ outputTokens: number | null;
19
+ cacheCreationInputTokens: number | null;
20
+ cacheReadInputTokens: number | null;
21
+ elapsedMs: number | null;
22
+ firstChunkMs: number | null;
23
+ totalStreamMs: number | null;
24
+ tokensPerSecond: number | null;
25
+ streaming: boolean;
26
+ userAgent: string | null;
27
+ origin: string | null;
28
+ apiFormat: ApiFormat;
29
+ isTest: boolean;
30
+ replayOfLogId: number | null;
31
+ providerName: string | null;
32
+ clientPort: number | null;
33
+ clientPid: number | null;
34
+ clientCwd: string | null;
35
+ clientProjectFolder: string | null;
36
+ streamingChunksPath: string | null;
37
+ rawRequestBodyBytes: number | null;
38
+ responseTextBytes: number | null;
39
+ warnings?: string[];
40
+ error: string | null;
41
+ };
7
42
 
8
43
  export type LogIndexEntry = {
9
44
  id: number;
10
45
  file: string;
11
46
  byteOffset: number;
12
47
  byteLength: number;
48
+ sessionId?: string | null;
49
+ model?: string | null;
50
+ summary?: LogIndexSummary;
51
+ };
52
+
53
+ export type LogIndexEntryMetadata = {
54
+ sessionId: string | null;
55
+ model: string | null;
56
+ summary: LogIndexSummary;
13
57
  };
14
58
 
15
59
  type LogIndex = {
@@ -29,6 +73,50 @@ function getIndexPath(): string {
29
73
 
30
74
  let cachedIndex: LogIndex | null = null;
31
75
 
76
+ function textByteLength(value: string | null): number | null {
77
+ return value === null ? null : Buffer.byteLength(value, "utf8");
78
+ }
79
+
80
+ export function createLogIndexEntryMetadata(log: CapturedLog): LogIndexEntryMetadata {
81
+ return {
82
+ sessionId: log.sessionId,
83
+ model: log.model,
84
+ summary: {
85
+ id: log.id,
86
+ timestamp: log.timestamp,
87
+ method: log.method,
88
+ path: log.path,
89
+ model: log.model,
90
+ sessionId: log.sessionId,
91
+ responseStatus: log.responseStatus,
92
+ inputTokens: log.inputTokens,
93
+ outputTokens: log.outputTokens,
94
+ cacheCreationInputTokens: log.cacheCreationInputTokens,
95
+ cacheReadInputTokens: log.cacheReadInputTokens,
96
+ elapsedMs: log.elapsedMs,
97
+ firstChunkMs: log.firstChunkMs ?? null,
98
+ totalStreamMs: log.totalStreamMs ?? null,
99
+ tokensPerSecond: log.tokensPerSecond ?? null,
100
+ streaming: log.streaming,
101
+ userAgent: log.userAgent,
102
+ origin: log.origin,
103
+ apiFormat: log.apiFormat,
104
+ isTest: log.isTest ?? false,
105
+ replayOfLogId: log.replayOfLogId ?? null,
106
+ providerName: log.providerName ?? null,
107
+ clientPort: log.clientPort ?? null,
108
+ clientPid: log.clientPid ?? null,
109
+ clientCwd: log.clientCwd ?? null,
110
+ clientProjectFolder: log.clientProjectFolder ?? null,
111
+ streamingChunksPath: log.streamingChunksPath ?? null,
112
+ rawRequestBodyBytes: log.rawRequestBodyBytes ?? textByteLength(log.rawRequestBody),
113
+ responseTextBytes: log.responseTextBytes ?? textByteLength(log.responseText),
114
+ warnings: log.warnings,
115
+ error: log.error ?? null,
116
+ },
117
+ };
118
+ }
119
+
32
120
  function createEmptyIndex(): LogIndex {
33
121
  return {
34
122
  version: INDEX_VERSION,
@@ -100,9 +188,13 @@ export async function addToIndex(
100
188
  file: string,
101
189
  byteOffset: number,
102
190
  byteLength: number,
191
+ metadata?: LogIndexEntryMetadata,
103
192
  ): Promise<void> {
104
193
  const index = await loadIndex();
105
- index.entries[id] = { id, file, byteOffset, byteLength };
194
+ index.entries[id] =
195
+ metadata === undefined
196
+ ? { id, file, byteOffset, byteLength }
197
+ : { id, file, byteOffset, byteLength, ...metadata };
106
198
  if (id > index.maxId) {
107
199
  index.maxId = id;
108
200
  }
@@ -154,6 +246,98 @@ type FileIndexResult = {
154
246
  maxId: number;
155
247
  };
156
248
 
249
+ function readStringOrNullField(entry: object, field: string): string | null {
250
+ const desc = Object.getOwnPropertyDescriptor(entry, field);
251
+ if (desc === undefined) return null;
252
+ if (typeof desc.value === "string") return desc.value;
253
+ return null;
254
+ }
255
+
256
+ function readStringField(entry: object, field: string): string {
257
+ return readStringOrNullField(entry, field) ?? "";
258
+ }
259
+
260
+ function readNumberOrNullField(entry: object, field: string): number | null {
261
+ const desc = Object.getOwnPropertyDescriptor(entry, field);
262
+ if (desc === undefined) return null;
263
+ return typeof desc.value === "number" ? desc.value : null;
264
+ }
265
+
266
+ function readBooleanField(entry: object, field: string): boolean {
267
+ const desc = Object.getOwnPropertyDescriptor(entry, field);
268
+ if (desc === undefined) return false;
269
+ return typeof desc.value === "boolean" ? desc.value : false;
270
+ }
271
+
272
+ function readApiFormatField(entry: object): ApiFormat {
273
+ const value = readStringOrNullField(entry, "apiFormat");
274
+ switch (value) {
275
+ case "anthropic":
276
+ return "anthropic";
277
+ case "openai":
278
+ return "openai";
279
+ case "unknown":
280
+ case null:
281
+ return "unknown";
282
+ default:
283
+ return "unknown";
284
+ }
285
+ }
286
+
287
+ function readStringArrayField(entry: object, field: string): string[] | undefined {
288
+ const desc = Object.getOwnPropertyDescriptor(entry, field);
289
+ if (desc === undefined || !Array.isArray(desc.value)) return undefined;
290
+
291
+ const values: string[] = [];
292
+ for (const item of desc.value) {
293
+ if (typeof item !== "string") return undefined;
294
+ values.push(item);
295
+ }
296
+ return values;
297
+ }
298
+
299
+ function readTextByteLength(entry: object, bytesField: string, textField: string): number | null {
300
+ const explicitBytes = readNumberOrNullField(entry, bytesField);
301
+ if (explicitBytes !== null && explicitBytes >= 0) return explicitBytes;
302
+ return textByteLength(readStringOrNullField(entry, textField));
303
+ }
304
+
305
+ function readLogIndexSummary(entry: object, id: number): LogIndexSummary {
306
+ return {
307
+ id,
308
+ timestamp: readStringField(entry, "timestamp"),
309
+ method: readStringField(entry, "method"),
310
+ path: readStringField(entry, "path"),
311
+ model: readStringOrNullField(entry, "model"),
312
+ sessionId: readStringOrNullField(entry, "sessionId"),
313
+ responseStatus: readNumberOrNullField(entry, "responseStatus"),
314
+ inputTokens: readNumberOrNullField(entry, "inputTokens"),
315
+ outputTokens: readNumberOrNullField(entry, "outputTokens"),
316
+ cacheCreationInputTokens: readNumberOrNullField(entry, "cacheCreationInputTokens"),
317
+ cacheReadInputTokens: readNumberOrNullField(entry, "cacheReadInputTokens"),
318
+ elapsedMs: readNumberOrNullField(entry, "elapsedMs"),
319
+ firstChunkMs: readNumberOrNullField(entry, "firstChunkMs"),
320
+ totalStreamMs: readNumberOrNullField(entry, "totalStreamMs"),
321
+ tokensPerSecond: readNumberOrNullField(entry, "tokensPerSecond"),
322
+ streaming: readBooleanField(entry, "streaming"),
323
+ userAgent: readStringOrNullField(entry, "userAgent"),
324
+ origin: readStringOrNullField(entry, "origin"),
325
+ apiFormat: readApiFormatField(entry),
326
+ isTest: readBooleanField(entry, "isTest"),
327
+ replayOfLogId: readNumberOrNullField(entry, "replayOfLogId"),
328
+ providerName: readStringOrNullField(entry, "providerName"),
329
+ clientPort: readNumberOrNullField(entry, "clientPort"),
330
+ clientPid: readNumberOrNullField(entry, "clientPid"),
331
+ clientCwd: readStringOrNullField(entry, "clientCwd"),
332
+ clientProjectFolder: readStringOrNullField(entry, "clientProjectFolder"),
333
+ streamingChunksPath: readStringOrNullField(entry, "streamingChunksPath"),
334
+ rawRequestBodyBytes: readTextByteLength(entry, "rawRequestBodyBytes", "rawRequestBody"),
335
+ responseTextBytes: readTextByteLength(entry, "responseTextBytes", "responseText"),
336
+ warnings: readStringArrayField(entry, "warnings"),
337
+ error: readStringOrNullField(entry, "error"),
338
+ };
339
+ }
340
+
157
341
  async function indexFile(filePath: string, file: string): Promise<FileIndexResult> {
158
342
  const entries: Record<number, LogIndexEntry> = {};
159
343
  let maxId = 0;
@@ -179,6 +363,9 @@ async function indexFile(filePath: string, file: string): Promise<FileIndexResul
179
363
  file,
180
364
  byteOffset,
181
365
  byteLength: lineBytes,
366
+ sessionId: readStringOrNullField(entry, "sessionId"),
367
+ model: readStringOrNullField(entry, "model"),
368
+ summary: readLogIndexSummary(entry, entryId),
182
369
  };
183
370
  }
184
371
  }