@tonyclaw/agent-inspector 2.0.2 → 2.0.4

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 (60) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/{CompareDrawer-Bp7_x-5N.js → CompareDrawer-BCH_fsLm.js} +1 -1
  3. package/.output/public/assets/ProxyViewerContainer-D85_UANk.js +101 -0
  4. package/.output/public/assets/{ReplayDialog-DFHCd0yx.js → ReplayDialog-DTeaHHit.js} +1 -1
  5. package/.output/public/assets/RequestAnatomy-DZ8grAih.js +1 -0
  6. package/.output/public/assets/ResponseView-Cldm6RCi.js +1 -0
  7. package/.output/public/assets/{StreamingChunkSequence-Bjs4Lqwn.js → StreamingChunkSequence-3x4p-yT7.js} +1 -1
  8. package/.output/public/assets/_sessionId-YqWFBu6d.js +1 -0
  9. package/.output/public/assets/index-BIw2H6jO.js +1 -0
  10. package/.output/public/assets/index-CobXD0yH.css +1 -0
  11. package/.output/public/assets/{json-viewer-6uV_YXws.js → json-viewer-BrzjD7qI.js} +1 -1
  12. package/.output/public/assets/{main-FSGUGtEL.js → main-mgxeUdZQ.js} +2 -2
  13. package/.output/server/_libs/lucide-react.mjs +8 -8
  14. package/.output/server/{_sessionId-_bf9vUww.mjs → _sessionId-C4xsxIWm.mjs} +2 -2
  15. package/.output/server/_ssr/{CompareDrawer-DIth2DQM.mjs → CompareDrawer-DuWEpqQ7.mjs} +4 -4
  16. package/.output/server/_ssr/{ProxyViewerContainer-249bTH-T.mjs → ProxyViewerContainer-Cckz5qKu.mjs} +519 -89
  17. package/.output/server/_ssr/{ReplayDialog-C1aGx0y1.mjs → ReplayDialog-BDRcr8E5.mjs} +4 -4
  18. package/.output/server/_ssr/{RequestAnatomy-D2bCiEJn.mjs → RequestAnatomy-BoO2_Ij0.mjs} +5 -5
  19. package/.output/server/_ssr/{ResponseView-DP6k4Xs_.mjs → ResponseView-DZiPBxvO.mjs} +21 -17
  20. package/.output/server/_ssr/{StreamingChunkSequence-HyXZV-b5.mjs → StreamingChunkSequence-D-be7KEL.mjs} +3 -3
  21. package/.output/server/_ssr/{index-Bt47f9pn.mjs → index-5RImHKfu.mjs} +2 -2
  22. package/.output/server/_ssr/index.mjs +2 -2
  23. package/.output/server/_ssr/{json-viewer-Co-YRwUP.mjs → json-viewer-aJhb93ZK.mjs} +2 -2
  24. package/.output/server/_ssr/{router-to_OJirX.mjs → router-Dgkv5nKP.mjs} +38 -99
  25. package/.output/server/{_tanstack-start-manifest_v-Bd-2YRWo.mjs → _tanstack-start-manifest_v-B8rrWXjr.mjs} +1 -1
  26. package/.output/server/index.mjs +63 -63
  27. package/README.md +5 -2
  28. package/package.json +1 -1
  29. package/src/components/ProxyViewer.tsx +25 -15
  30. package/src/components/ProxyViewerContainer.tsx +2 -1
  31. package/src/components/providers/SettingsDialog.tsx +45 -1
  32. package/src/components/proxy-viewer/AgentTraceSummary.tsx +276 -0
  33. package/src/components/proxy-viewer/AnswerMarkdown.tsx +16 -0
  34. package/src/components/proxy-viewer/ConversationGroup.tsx +18 -0
  35. package/src/components/proxy-viewer/ConversationHeader.tsx +6 -6
  36. package/src/components/proxy-viewer/LogEntry.tsx +5 -5
  37. package/src/components/proxy-viewer/LogEntryHeader.tsx +9 -14
  38. package/src/components/proxy-viewer/ResponseView.tsx +2 -6
  39. package/src/components/proxy-viewer/ToolTraceEvents.tsx +32 -0
  40. package/src/components/proxy-viewer/TurnGroup.tsx +15 -1
  41. package/src/components/proxy-viewer/anatomy/SegmentBar.tsx +2 -2
  42. package/src/components/proxy-viewer/formats/anthropic/ContentBlocks.tsx +6 -12
  43. package/src/components/proxy-viewer/formats/openai/ResponseView.tsx +10 -14
  44. package/src/components/proxy-viewer/viewerState.ts +177 -0
  45. package/src/lib/runtimeConfig.ts +6 -0
  46. package/src/lib/timeDisplay.ts +22 -0
  47. package/src/lib/useOnboarding.ts +2 -0
  48. package/src/lib/useStripConfig.ts +16 -0
  49. package/src/proxy/chunkStorage.ts +3 -4
  50. package/src/proxy/config.ts +3 -0
  51. package/src/proxy/logger.ts +8 -15
  52. package/src/proxy/store.ts +8 -16
  53. package/src/routes/api/config.ts +5 -1
  54. package/src/routes/api/providers.$providerId.test.log.ts +0 -79
  55. package/.output/public/assets/ProxyViewerContainer-USuxPy-K.js +0 -101
  56. package/.output/public/assets/RequestAnatomy-ehyrskxt.js +0 -1
  57. package/.output/public/assets/ResponseView-BNGyc8e_.js +0 -1
  58. package/.output/public/assets/_sessionId-D_SeK_qp.js +0 -1
  59. package/.output/public/assets/index-BGGOWR7A.js +0 -1
  60. package/.output/public/assets/index-CIL46Z2y.css +0 -1
@@ -1,7 +1,7 @@
1
1
  import { Brain, ChevronDown, ChevronRight, Terminal } from "lucide-react";
2
2
  import { type JSX, memo, useState } from "react";
3
- import ReactMarkdown from "react-markdown";
4
3
  import type { ResponseContentBlockType } from "../../../../proxy/schemas";
4
+ import { AnswerMarkdown } from "../../AnswerMarkdown";
5
5
  import { Badge } from "../../../ui/badge";
6
6
  import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../../../ui/collapsible";
7
7
  import { JsonViewer } from "../../../ui/json-viewer";
@@ -30,9 +30,7 @@ function SystemReminderBlock({ text }: { text: string }): JSX.Element {
30
30
  </CollapsibleTrigger>
31
31
  <CollapsibleContent>
32
32
  <div className="pl-4 pt-1">
33
- <div className="prose prose-sm dark:prose-invert max-w-none [&_pre]:bg-muted [&_pre]:text-foreground [&_code]:text-[0.8em] [&_p]:my-1 [&_ul]:my-1 [&_ol]:my-1">
34
- <ReactMarkdown>{text}</ReactMarkdown>
35
- </div>
33
+ <AnswerMarkdown text={text} />
36
34
  </div>
37
35
  </CollapsibleContent>
38
36
  </Collapsible>
@@ -50,11 +48,7 @@ export const TextBlock = memo(function TextBlock({ text }: { text: string }): JS
50
48
  return (
51
49
  <div className="space-y-2">
52
50
  {thinking !== null && <ThinkingBlock thinking={thinking} />}
53
- {remainingText.length > 0 && (
54
- <div className="prose prose-sm dark:prose-invert max-w-none [&_pre]:bg-muted [&_pre]:text-foreground [&_code]:text-[0.8em] [&_p]:my-1 [&_ul]:my-1 [&_ol]:my-1">
55
- <ReactMarkdown>{remainingText}</ReactMarkdown>
56
- </div>
57
- )}
51
+ {remainingText.length > 0 && <AnswerMarkdown text={remainingText} />}
58
52
  {thinking === null && remainingText.length === 0 && (
59
53
  <p className="text-xs text-muted-foreground italic">Empty text block</p>
60
54
  )}
@@ -113,9 +107,9 @@ export const ToolUseBlock = memo(function ToolUseBlock({
113
107
 
114
108
  return (
115
109
  <Collapsible open={open} onOpenChange={setOpen}>
116
- <div className="border-l-2 border-blue-500/40 my-1">
117
- <CollapsibleTrigger className="flex items-center gap-1.5 px-3 py-1 w-full text-left cursor-pointer hover:bg-blue-500/5 transition-colors rounded-r-sm group">
118
- <Terminal className="size-3.5 text-blue-400 shrink-0" />
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">
112
+ <Terminal className="size-3.5 text-sky-400/70 shrink-0" />
119
113
  <Badge variant="outline" className="text-[10px] font-mono px-1.5 py-0 h-4">
120
114
  {name}
121
115
  </Badge>
@@ -1,8 +1,8 @@
1
1
  import { StopCircle, Terminal, Zap } from "lucide-react";
2
2
  import { memo, useState, type JSX } from "react";
3
- import ReactMarkdown from "react-markdown";
4
3
  import type { OpenAIResponse, OpenAIToolCall } from "../../../../proxy/schemas";
5
4
  import { formatTokens } from "../../../../lib/utils";
5
+ import { AnswerMarkdown } from "../../AnswerMarkdown";
6
6
  import { Badge } from "../../../ui/badge";
7
7
  import { JsonViewer } from "../../../ui/json-viewer";
8
8
  import { safeJsonValue } from "../../../ui/json-viewer-bulk";
@@ -29,8 +29,8 @@ function parseToolArguments(raw: string | undefined): unknown {
29
29
  }
30
30
 
31
31
  /** One collapsible tool_use row, mirroring the Anthropic ToolUseBlock visual
32
- * treatment (Terminal icon, blue accent, name as a Badge, JSON input in a
33
- * scrollable JsonViewer). */
32
+ * treatment (Terminal icon, soft tool accent, name as a Badge, JSON input in
33
+ * a scrollable JsonViewer). */
34
34
  function OpenAIToolCallBlock({ call }: { call: OpenAIToolCall }): JSX.Element {
35
35
  const [open, setOpen] = useState(false);
36
36
  const name = call.function.name ?? "(unnamed tool)";
@@ -38,9 +38,9 @@ function OpenAIToolCallBlock({ call }: { call: OpenAIToolCall }): JSX.Element {
38
38
 
39
39
  return (
40
40
  <Collapsible open={open} onOpenChange={setOpen}>
41
- <div className="border-l-2 border-blue-500/40 my-1">
42
- <CollapsibleTrigger className="flex items-center gap-1.5 px-3 py-1 w-full text-left cursor-pointer hover:bg-blue-500/5 transition-colors rounded-r-sm group">
43
- <Terminal className="size-3.5 text-blue-400 shrink-0" />
41
+ <div className="border-l-2 border-sky-400/25 my-1">
42
+ <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">
43
+ <Terminal className="size-3.5 text-sky-400/70 shrink-0" />
44
44
  <Badge variant="outline" className="text-[10px] font-mono px-1.5 py-0 h-4">
45
45
  {name}
46
46
  </Badge>
@@ -133,11 +133,7 @@ export const OpenAIResponseView = memo(function OpenAIResponseView({
133
133
  <div className="space-y-2">
134
134
  {/* Show thinking from tags only if no reasoning_content field */}
135
135
  {thinking !== null && !hasReasoningField && <ThinkingBlock thinking={thinking} />}
136
- {remainingText.length > 0 && (
137
- <div className="prose prose-sm dark:prose-invert max-w-none [&_pre]:bg-muted [&_pre]:text-foreground [&_code]:text-[0.8em] [&_p]:my-1 [&_ul]:my-1 [&_ol]:my-1">
138
- <ReactMarkdown>{remainingText}</ReactMarkdown>
139
- </div>
140
- )}
136
+ {remainingText.length > 0 && <AnswerMarkdown text={remainingText} />}
141
137
  </div>
142
138
  );
143
139
  })()}
@@ -146,10 +142,10 @@ export const OpenAIResponseView = memo(function OpenAIResponseView({
146
142
  <OpenAIToolCallBlock key={call.id ?? `tc-${i}`} call={call} />
147
143
  ))}
148
144
  {message?.function_call !== null && message?.function_call !== undefined && (
149
- <div className="border border-blue-500/30 rounded-md p-3 bg-blue-500/5">
150
- <div className="text-xs text-blue-400 font-mono mb-1">function_call</div>
145
+ <div className="border border-sky-400/20 rounded-md p-3 bg-muted/20">
146
+ <div className="text-xs text-sky-400/80 font-mono mb-1">function_call</div>
151
147
  <div className="font-mono text-xs">
152
- <span className="text-blue-300">{message.function_call.name}</span>
148
+ <span className="text-foreground/80">{message.function_call.name}</span>
153
149
  <span className="text-muted-foreground">({message.function_call.arguments})</span>
154
150
  </div>
155
151
  </div>
@@ -1,4 +1,5 @@
1
1
  import { extractStopReason, isTurnBoundary, type StopReason } from "../../lib/stopReason";
2
+ import { safeGetOwnProperty } from "../../lib/objectUtils";
2
3
  import type { CapturedLog } from "../../proxy/schemas";
3
4
  import { resolveLogFormat } from "./log-formats";
4
5
 
@@ -16,6 +17,34 @@ type ConversationLike = {
16
17
  logs: CapturedLog[];
17
18
  };
18
19
 
20
+ export type TraceSummary = {
21
+ llmCallCount: number;
22
+ toolCallCount: number;
23
+ failedCallCount: number;
24
+ pendingCallCount: number;
25
+ slowCallCount: number;
26
+ totalInputTokens: number;
27
+ totalOutputTokens: number;
28
+ totalCacheCreationInputTokens: number;
29
+ totalCacheReadInputTokens: number;
30
+ totalElapsedMs: number;
31
+ maxElapsedMs: number | null;
32
+ startedAt: string | null;
33
+ endedAt: string | null;
34
+ knowledgeCandidateCount: number;
35
+ };
36
+
37
+ export type ToolTraceEvent = {
38
+ id: string;
39
+ logId: number;
40
+ index: number;
41
+ provider: "anthropic" | "openai";
42
+ name: string;
43
+ argumentsPreview: string | null;
44
+ };
45
+
46
+ const PREVIEW_LIMIT = 180;
47
+
19
48
  export function shouldRenderConversationContent(standalone: boolean, expanded: boolean): boolean {
20
49
  return standalone || expanded;
21
50
  }
@@ -64,3 +93,151 @@ export function buildValidPredecessors(groups: ConversationLike[]): Map<number,
64
93
 
65
94
  return predecessors;
66
95
  }
96
+
97
+ function parseJsonResponse(responseText: string | null): unknown {
98
+ if (responseText === null) return null;
99
+ try {
100
+ const parsed: unknown = JSON.parse(responseText);
101
+ if (typeof parsed === "string") {
102
+ return JSON.parse(parsed);
103
+ }
104
+ return parsed;
105
+ } catch {
106
+ return null;
107
+ }
108
+ }
109
+
110
+ function previewValue(value: unknown): string | null {
111
+ if (value === undefined || value === null) return null;
112
+ const raw = typeof value === "string" ? value : JSON.stringify(value);
113
+ if (raw === undefined) return null;
114
+ const normalized = raw.replace(/\s+/g, " ").trim();
115
+ if (normalized.length === 0) return null;
116
+ return normalized.length > PREVIEW_LIMIT
117
+ ? `${normalized.slice(0, PREVIEW_LIMIT - 1)}...`
118
+ : normalized;
119
+ }
120
+
121
+ function extractAnthropicToolTraceEvents(log: CapturedLog): ToolTraceEvent[] {
122
+ const parsed = parseJsonResponse(log.responseText);
123
+ const content = safeGetOwnProperty(parsed, "content");
124
+ if (!Array.isArray(content)) return [];
125
+
126
+ const events: ToolTraceEvent[] = [];
127
+ for (const block of content) {
128
+ const type = safeGetOwnProperty(block, "type");
129
+ if (type !== "tool_use") continue;
130
+ const name = safeGetOwnProperty(block, "name");
131
+ if (typeof name !== "string" || name.length === 0) continue;
132
+ events.push({
133
+ id: `${String(log.id)}-anthropic-tool-${String(events.length)}`,
134
+ logId: log.id,
135
+ index: events.length,
136
+ provider: "anthropic",
137
+ name,
138
+ argumentsPreview: previewValue(safeGetOwnProperty(block, "input")),
139
+ });
140
+ }
141
+ return events;
142
+ }
143
+
144
+ function extractOpenAIToolTraceEvents(log: CapturedLog): ToolTraceEvent[] {
145
+ const parsed = parseJsonResponse(log.responseText);
146
+ const choices = safeGetOwnProperty(parsed, "choices");
147
+ if (!Array.isArray(choices)) return [];
148
+
149
+ const events: ToolTraceEvent[] = [];
150
+ for (const choice of choices) {
151
+ const message = safeGetOwnProperty(choice, "message");
152
+ const toolCalls = safeGetOwnProperty(message, "tool_calls");
153
+ if (!Array.isArray(toolCalls)) continue;
154
+ for (const call of toolCalls) {
155
+ const fn = safeGetOwnProperty(call, "function");
156
+ const name = safeGetOwnProperty(fn, "name");
157
+ if (typeof name !== "string" || name.length === 0) continue;
158
+ events.push({
159
+ id: `${String(log.id)}-openai-tool-${String(events.length)}`,
160
+ logId: log.id,
161
+ index: events.length,
162
+ provider: "openai",
163
+ name,
164
+ argumentsPreview: previewValue(safeGetOwnProperty(fn, "arguments")),
165
+ });
166
+ }
167
+ }
168
+ return events;
169
+ }
170
+
171
+ export function extractToolTraceEvents(log: CapturedLog): ToolTraceEvent[] {
172
+ const format = resolveLogFormat(log);
173
+ switch (format) {
174
+ case "anthropic":
175
+ return extractAnthropicToolTraceEvents(log);
176
+ case "openai":
177
+ return extractOpenAIToolTraceEvents(log);
178
+ case "unknown":
179
+ return [];
180
+ }
181
+ }
182
+
183
+ export function buildTraceSummary(
184
+ logs: CapturedLog[],
185
+ slowResponseThresholdSeconds: number,
186
+ knowledgeCandidateCount = 0,
187
+ ): TraceSummary {
188
+ let failedCallCount = 0;
189
+ let pendingCallCount = 0;
190
+ let slowCallCount = 0;
191
+ let totalInputTokens = 0;
192
+ let totalOutputTokens = 0;
193
+ let totalCacheCreationInputTokens = 0;
194
+ let totalCacheReadInputTokens = 0;
195
+ let totalElapsedMs = 0;
196
+ let maxElapsedMs: number | null = null;
197
+ let toolCallCount = 0;
198
+
199
+ for (const log of logs) {
200
+ if (log.responseStatus === null) {
201
+ pendingCallCount += 1;
202
+ } else if (log.responseStatus >= 400) {
203
+ failedCallCount += 1;
204
+ }
205
+ if (
206
+ log.elapsedMs !== null &&
207
+ slowResponseThresholdSeconds > 0 &&
208
+ log.elapsedMs > slowResponseThresholdSeconds * 1000
209
+ ) {
210
+ slowCallCount += 1;
211
+ }
212
+ if (log.inputTokens !== null) totalInputTokens += log.inputTokens;
213
+ if (log.outputTokens !== null) totalOutputTokens += log.outputTokens;
214
+ if (log.cacheCreationInputTokens !== null) {
215
+ totalCacheCreationInputTokens += log.cacheCreationInputTokens;
216
+ }
217
+ if (log.cacheReadInputTokens !== null) {
218
+ totalCacheReadInputTokens += log.cacheReadInputTokens;
219
+ }
220
+ if (log.elapsedMs !== null) {
221
+ totalElapsedMs += log.elapsedMs;
222
+ maxElapsedMs = maxElapsedMs === null ? log.elapsedMs : Math.max(maxElapsedMs, log.elapsedMs);
223
+ }
224
+ toolCallCount += extractToolTraceEvents(log).length;
225
+ }
226
+
227
+ return {
228
+ llmCallCount: logs.length,
229
+ toolCallCount,
230
+ failedCallCount,
231
+ pendingCallCount,
232
+ slowCallCount,
233
+ totalInputTokens,
234
+ totalOutputTokens,
235
+ totalCacheCreationInputTokens,
236
+ totalCacheReadInputTokens,
237
+ totalElapsedMs,
238
+ maxElapsedMs,
239
+ startedAt: logs[0]?.timestamp ?? null,
240
+ endedAt: logs[logs.length - 1]?.timestamp ?? null,
241
+ knowledgeCandidateCount,
242
+ };
243
+ }
@@ -2,6 +2,11 @@ import { z } from "zod";
2
2
 
3
3
  export const DEFAULT_SLOW_RESPONSE_THRESHOLD_SECONDS = 10;
4
4
  export const MAX_SLOW_RESPONSE_THRESHOLD_SECONDS = 600;
5
+ export const DEFAULT_TIME_DISPLAY_FORMAT = "time";
6
+
7
+ export const TimeDisplayFormatSchema = z.enum(["time", "full"]);
8
+
9
+ export type TimeDisplayFormat = z.infer<typeof TimeDisplayFormatSchema>;
5
10
 
6
11
  /**
7
12
  * Schema for the runtime proxy config. Shared between server
@@ -20,6 +25,7 @@ export const RuntimeConfigSchema = z.object({
20
25
  .min(0)
21
26
  .max(MAX_SLOW_RESPONSE_THRESHOLD_SECONDS)
22
27
  .default(DEFAULT_SLOW_RESPONSE_THRESHOLD_SECONDS),
28
+ timeDisplayFormat: TimeDisplayFormatSchema.default(DEFAULT_TIME_DISPLAY_FORMAT),
23
29
  });
24
30
 
25
31
  export type RuntimeConfig = z.infer<typeof RuntimeConfigSchema>;
@@ -0,0 +1,22 @@
1
+ import type { TimeDisplayFormat } from "./runtimeConfig";
2
+
3
+ export function formatTimestamp(iso: string, format: TimeDisplayFormat): string {
4
+ switch (format) {
5
+ case "full":
6
+ return iso;
7
+ case "time":
8
+ return new Date(iso).toLocaleTimeString([], {
9
+ hour: "2-digit",
10
+ minute: "2-digit",
11
+ second: "2-digit",
12
+ });
13
+ }
14
+ }
15
+
16
+ export function formatTimestampRange(
17
+ startedAt: string,
18
+ endedAt: string,
19
+ format: TimeDisplayFormat,
20
+ ): string {
21
+ return `${formatTimestamp(startedAt, format)} - ${formatTimestamp(endedAt, format)}`;
22
+ }
@@ -2,6 +2,7 @@ import useSWR, { type SWRResponse, useSWRConfig } from "swr";
2
2
  import { fetchJson } from "./apiClient";
3
3
  import {
4
4
  DEFAULT_SLOW_RESPONSE_THRESHOLD_SECONDS,
5
+ DEFAULT_TIME_DISPLAY_FORMAT,
5
6
  RuntimeConfigSchema,
6
7
  type RuntimeConfig,
7
8
  } from "./runtimeConfig";
@@ -66,6 +67,7 @@ export function useOnboarding(): UseOnboarding {
66
67
  hasSeenOnboarding: true,
67
68
  slowResponseThresholdSeconds:
68
69
  response.data?.slowResponseThresholdSeconds ?? DEFAULT_SLOW_RESPONSE_THRESHOLD_SECONDS,
70
+ timeDisplayFormat: response.data?.timeDisplayFormat ?? DEFAULT_TIME_DISPLAY_FORMAT,
69
71
  },
70
72
  rollbackOnError: true,
71
73
  revalidate: false,
@@ -2,7 +2,9 @@ import useSWR, { type SWRResponse, useSWRConfig } from "swr";
2
2
  import { fetchJson } from "./apiClient";
3
3
  import {
4
4
  DEFAULT_SLOW_RESPONSE_THRESHOLD_SECONDS,
5
+ DEFAULT_TIME_DISPLAY_FORMAT,
5
6
  RuntimeConfigSchema,
7
+ type TimeDisplayFormat,
6
8
  type RuntimeConfig,
7
9
  } from "./runtimeConfig";
8
10
 
@@ -36,10 +38,12 @@ export async function setRuntimeConfig(patch: Partial<RuntimeConfig>): Promise<R
36
38
  export type UseStripConfig = {
37
39
  strip: boolean;
38
40
  slowResponseThresholdSeconds: number;
41
+ timeDisplayFormat: TimeDisplayFormat;
39
42
  isLoading: boolean;
40
43
  isError: boolean;
41
44
  setStrip: (next: boolean) => Promise<void>;
42
45
  setSlowResponseThresholdSeconds: (next: number) => Promise<void>;
46
+ setTimeDisplayFormat: (next: TimeDisplayFormat) => Promise<void>;
43
47
  };
44
48
 
45
49
  /**
@@ -64,12 +68,14 @@ export function useStripConfig(): UseStripConfig {
64
68
  const strip = response.data?.stripClaudeCodeBillingHeader ?? false;
65
69
  const slowResponseThresholdSeconds =
66
70
  response.data?.slowResponseThresholdSeconds ?? DEFAULT_SLOW_RESPONSE_THRESHOLD_SECONDS;
71
+ const timeDisplayFormat = response.data?.timeDisplayFormat ?? DEFAULT_TIME_DISPLAY_FORMAT;
67
72
 
68
73
  const optimisticConfig = (patch: Partial<RuntimeConfig>): RuntimeConfig => ({
69
74
  stripClaudeCodeBillingHeader: response.data?.stripClaudeCodeBillingHeader ?? false,
70
75
  hasSeenOnboarding: response.data?.hasSeenOnboarding ?? false,
71
76
  slowResponseThresholdSeconds:
72
77
  response.data?.slowResponseThresholdSeconds ?? DEFAULT_SLOW_RESPONSE_THRESHOLD_SECONDS,
78
+ timeDisplayFormat: response.data?.timeDisplayFormat ?? DEFAULT_TIME_DISPLAY_FORMAT,
73
79
  ...patch,
74
80
  });
75
81
 
@@ -97,12 +103,22 @@ export function useStripConfig(): UseStripConfig {
97
103
  );
98
104
  };
99
105
 
106
+ const setTimeDisplayFormat = async (next: TimeDisplayFormat): Promise<void> => {
107
+ await globalMutate(STRIP_CONFIG_SWR_KEY, setRuntimeConfig({ timeDisplayFormat: next }), {
108
+ optimisticData: optimisticConfig({ timeDisplayFormat: next }),
109
+ rollbackOnError: true,
110
+ revalidate: false,
111
+ });
112
+ };
113
+
100
114
  return {
101
115
  strip,
102
116
  slowResponseThresholdSeconds,
117
+ timeDisplayFormat,
103
118
  isLoading: response.isLoading,
104
119
  isError: response.error !== undefined,
105
120
  setStrip,
106
121
  setSlowResponseThresholdSeconds,
122
+ setTimeDisplayFormat,
107
123
  };
108
124
  }
@@ -30,11 +30,10 @@ const StreamingChunksDataSchema = z.object({
30
30
  truncated: z.boolean().optional(),
31
31
  });
32
32
 
33
- const CHUNKS_DIR_ENV = process.env["CHUNKS_DIR"];
34
-
35
33
  export function getChunksDir(): string {
36
- if (CHUNKS_DIR_ENV !== undefined) {
37
- return isAbsolute(CHUNKS_DIR_ENV) ? CHUNKS_DIR_ENV : join(getDataDir(), CHUNKS_DIR_ENV);
34
+ const chunksDirEnv = process.env["CHUNKS_DIR"];
35
+ if (chunksDirEnv !== undefined && chunksDirEnv !== "") {
36
+ return isAbsolute(chunksDirEnv) ? chunksDirEnv : join(getDataDir(), chunksDirEnv);
38
37
  }
39
38
  return join(getDataDir(), "chunks");
40
39
  }
@@ -12,6 +12,7 @@ import { logger } from "./logger";
12
12
  import { getDataDir } from "./dataDir";
13
13
  import {
14
14
  DEFAULT_SLOW_RESPONSE_THRESHOLD_SECONDS,
15
+ DEFAULT_TIME_DISPLAY_FORMAT,
15
16
  RuntimeConfigSchema,
16
17
  type RuntimeConfig,
17
18
  } from "../lib/runtimeConfig";
@@ -79,6 +80,7 @@ function resolveInitialConfig(): RuntimeConfig {
79
80
  stripClaudeCodeBillingHeader: true,
80
81
  hasSeenOnboarding: false,
81
82
  slowResponseThresholdSeconds: DEFAULT_SLOW_RESPONSE_THRESHOLD_SECONDS,
83
+ timeDisplayFormat: DEFAULT_TIME_DISPLAY_FORMAT,
82
84
  };
83
85
  }
84
86
 
@@ -87,6 +89,7 @@ function resolveInitialConfig(): RuntimeConfig {
87
89
  stripClaudeCodeBillingHeader: false,
88
90
  hasSeenOnboarding: false,
89
91
  slowResponseThresholdSeconds: DEFAULT_SLOW_RESPONSE_THRESHOLD_SECONDS,
92
+ timeDisplayFormat: DEFAULT_TIME_DISPLAY_FORMAT,
90
93
  };
91
94
  }
92
95
 
@@ -3,23 +3,14 @@ import { writeFileSync, mkdirSync } from "node:fs";
3
3
  import path from "node:path";
4
4
  import { getDataDir } from "./dataDir";
5
5
 
6
- const LOG_DIR_ENV = process.env["LOG_DIR"];
7
6
  const RETENTION_DAYS = Number(process.env["LOG_RETENTION_DAYS"] ?? "7");
8
- const LOG_FILE_ENV = process.env["AGENT_INSPECTOR_LOG_FILE"];
9
-
10
- let resolvedLogDir: string | null = null;
11
7
 
12
8
  export function resolveLogDir(): string {
13
- if (resolvedLogDir !== null) return resolvedLogDir;
14
-
15
- if (LOG_DIR_ENV !== undefined) {
16
- resolvedLogDir = path.isAbsolute(LOG_DIR_ENV)
17
- ? LOG_DIR_ENV
18
- : path.join(getDataDir(), LOG_DIR_ENV);
19
- } else {
20
- resolvedLogDir = path.join(getDataDir(), "logs");
9
+ const logDirEnv = process.env["LOG_DIR"];
10
+ if (logDirEnv !== undefined && logDirEnv !== "") {
11
+ return path.isAbsolute(logDirEnv) ? logDirEnv : path.join(getDataDir(), logDirEnv);
21
12
  }
22
- return resolvedLogDir;
13
+ return path.join(getDataDir(), "logs");
23
14
  }
24
15
 
25
16
  export function getLogFilePath(): string {
@@ -31,8 +22,9 @@ export function getLogFilePath(): string {
31
22
  }
32
23
 
33
24
  function getInspectorLogPath(): string {
34
- if (LOG_FILE_ENV !== undefined) {
35
- return LOG_FILE_ENV;
25
+ const logFileEnv = process.env["AGENT_INSPECTOR_LOG_FILE"];
26
+ if (logFileEnv !== undefined && logFileEnv !== "") {
27
+ return logFileEnv;
36
28
  }
37
29
  return path.join(getDataDir(), "logs", "inspector.log");
38
30
  }
@@ -43,6 +35,7 @@ export async function initLogger(): Promise<void> {
43
35
  const cutoff = Date.now() - retentionMs;
44
36
 
45
37
  try {
38
+ await mkdir(dir, { recursive: true });
46
39
  const entries = await readdir(dir);
47
40
  for (const entry of entries) {
48
41
  if (!entry.endsWith(".jsonl")) continue;
@@ -5,14 +5,7 @@ import { createInterface } from "node:readline";
5
5
  import { Buffer } from "node:buffer";
6
6
  import { join } from "node:path";
7
7
  import { appendLogEntry, resolveLogDir, logger } from "./logger";
8
- import {
9
- addToIndex,
10
- findInIndex,
11
- getNextLogId,
12
- getCurrentLogFile,
13
- saveIndex,
14
- loadIndex,
15
- } from "./logIndex";
8
+ import { addToIndex, findInIndex, getNextLogId, getCurrentLogFile } from "./logIndex";
16
9
  import { writeChunks } from "./chunkStorage";
17
10
  import type { CapturedLog } from "./schemas";
18
11
  import { CapturedLogSchema } from "./schemas";
@@ -67,17 +60,11 @@ function removeFromCache(id: number): void {
67
60
  }
68
61
 
69
62
  /**
70
- * Add a test log entry directly to the in-memory store (for dashboard display).
71
- * This is used by the provider test endpoint alongside appendLogEntry (file logging).
63
+ * Add a test log entry to the in-memory store and persistent log file.
64
+ * This is used by the provider test endpoint to seed the provider-test session.
72
65
  */
73
66
  export async function addTestLogEntry(entry: Omit<CapturedLog, "id">): Promise<CapturedLog> {
74
67
  const id = await getNextLogId();
75
- // Update the index with the new maxId so subsequent calls get unique IDs
76
- const index = await loadIndex();
77
- if (id > index.maxId) {
78
- index.maxId = id;
79
- await saveIndex(index);
80
- }
81
68
 
82
69
  // Persist streaming chunks to disk if present
83
70
  let streamingChunksPath: string | null = null;
@@ -100,6 +87,11 @@ export async function addTestLogEntry(entry: Omit<CapturedLog, "id">): Promise<C
100
87
  sessionId: session.id,
101
88
  streamingChunksPath,
102
89
  };
90
+
91
+ const logFile = getCurrentLogFile();
92
+ appendLogEntry(log);
93
+ await addToIndex(id, logFile, -1, -1);
94
+
103
95
  addToCache(log);
104
96
  observeSessionLog(log, session.source);
105
97
  emitLogUpdate(log);
@@ -1,6 +1,9 @@
1
1
  import { createFileRoute } from "@tanstack/react-router";
2
2
  import { z } from "zod";
3
- import { MAX_SLOW_RESPONSE_THRESHOLD_SECONDS } from "../../lib/runtimeConfig";
3
+ import {
4
+ MAX_SLOW_RESPONSE_THRESHOLD_SECONDS,
5
+ TimeDisplayFormatSchema,
6
+ } from "../../lib/runtimeConfig";
4
7
  import { getConfig, setConfig, RuntimeConfigSchema } from "../../proxy/config";
5
8
 
6
9
  // Partial schema for PATCH: at least one known field must be present.
@@ -14,6 +17,7 @@ const RuntimeConfigPatchSchema = z
14
17
  .min(0)
15
18
  .max(MAX_SLOW_RESPONSE_THRESHOLD_SECONDS)
16
19
  .optional(),
20
+ timeDisplayFormat: TimeDisplayFormatSchema.optional(),
17
21
  })
18
22
  .strict()
19
23
  .refine((v) => Object.keys(v).length > 0, {