@tonyclaw/llm-inspector 1.16.0 → 1.16.2

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.
@@ -1,5 +1,4 @@
1
1
  import { type JSX, useMemo } from "react";
2
- import { ChevronDown, ChevronRight } from "lucide-react";
3
2
  import { cn } from "../../lib/utils";
4
3
  import type { StopReason } from "../../lib/stopReason";
5
4
  import { getCrabVariant } from "../ui/crab-variants";
@@ -12,20 +11,17 @@ export type ThreadConnectorProps = {
12
11
  isTurnStart: boolean;
13
12
  /** Seed for crab variant selection (0-11). */
14
13
  crabIndex?: number;
15
- /** When true, the boundary marker is clickable to collapse/expand the turn. */
14
+ /** When true the crab is clickable (collapse / expand a turn). */
16
15
  collapsible?: boolean;
17
- collapsed?: boolean;
18
16
  onToggle?: () => void;
19
17
  };
20
18
 
21
19
  /**
22
- * Vertical timeline connector for thread view. Uses flexbox layout (no
23
- * absolute positioning) so the connector naturally tracks its sibling
24
- * LogEntry height no scroll jitter.
25
- *
26
- * Markers use the CrabLogo. At turn starts the crab is static (amber),
27
- * during processing it "crawls" downward with a bounce animation, and
28
- * at turn boundaries it stops with a glow.
20
+ * Vertical timeline connector. Top spacer uses a calc() that
21
+ * mixes rem (py-1 + half-line) and px (border) so the crab
22
+ * centre-line matches the LogEntry `#id` index regardless of the
23
+ * root font-size. The bottom spacer is flex-1 so the outgoing
24
+ * line adapts to whatever height the LogEntry occupies.
29
25
  */
30
26
  export function ThreadConnector({
31
27
  stopReason,
@@ -34,74 +30,82 @@ export function ThreadConnector({
34
30
  isTurnStart,
35
31
  crabIndex = 0,
36
32
  collapsible = false,
37
- collapsed = false,
38
33
  onToggle,
39
34
  }: ThreadConnectorProps): JSX.Element {
40
35
  const isBoundary = stopReason === "end_turn" || stopReason === "stop";
41
36
  const isRunning = isPending && !isBoundary;
42
37
  const Crab = useMemo(() => getCrabVariant(crabIndex), [crabIndex]);
43
38
 
39
+ const interactiveProps =
40
+ collapsible && onToggle
41
+ ? ({
42
+ role: "button" as const,
43
+ tabIndex: 0,
44
+ className: "cursor-pointer",
45
+ onClick: (e: React.MouseEvent) => {
46
+ e.stopPropagation();
47
+ onToggle();
48
+ },
49
+ onKeyDown: (e: React.KeyboardEvent) => {
50
+ if (e.key === "Enter" || e.key === " ") {
51
+ e.preventDefault();
52
+ onToggle();
53
+ }
54
+ },
55
+ } as const)
56
+ : ({} as const);
57
+
44
58
  return (
45
- <div className="flex flex-col items-center w-6 shrink-0">
46
- {/* Top: incoming line */}
47
- <div className="flex justify-center h-2.5">
48
- {!isFirst && <div className="w-0.5 bg-muted-foreground/30" />}
59
+ <div className="flex flex-col items-center w-6 shrink-0 pt-0.5 pb-0.5">
60
+ {/* Top spacer crab centre must land on the LogEntry index midline.
61
+ * Index midline from container top = border(1px) + py-1(0.25rem) + ½line(0.5rem)
62
+ * = 1px + 0.75rem. Crab centre = pt(2px) + spacer + 7px(half crab).
63
+ * ∴ spacer = 0.75rem - 8px. (at 16px root: 12-8=4px; 2+4+7=13px). */}
64
+ <div className="flex justify-center h-[calc(0.75rem-8px)]">
65
+ {!isFirst && <div className="w-0.5 bg-muted-foreground/30 h-full" />}
49
66
  </div>
50
67
 
51
- {/* Collapse toggle sits above the crab, no overlap */}
52
- {collapsible && (
53
- <button
54
- type="button"
55
- onClick={onToggle}
56
- title={collapsed ? "Expand turn" : "Collapse turn"}
57
- className="cursor-pointer flex justify-center py-0.5"
68
+ {/* Crabpinned to the fixed offset above so it never drifts
69
+ * when the LogEntry expands / collapses. Clickable when the
70
+ * turn is collapsible (replaces the old chevron toggle). */}
71
+ {isBoundary ? (
72
+ <span
73
+ title={stopReason === "end_turn" ? "End of Turn (Anthropic)" : "End of Turn (OpenAI)"}
74
+ {...interactiveProps}
58
75
  >
59
- {collapsed ? (
60
- <ChevronRight className="size-3 text-muted-foreground hover:text-foreground transition-colors" />
61
- ) : (
62
- <ChevronDown className="size-3 text-muted-foreground hover:text-foreground transition-colors" />
63
- )}
64
- </button>
65
- )}
66
-
67
- {/* Center marker CrabLogo */}
68
- <div className="flex items-center justify-center py-0.5">
69
- {isBoundary ? (
70
- <span
71
- title={stopReason === "end_turn" ? "End of Turn (Anthropic)" : "End of Turn (OpenAI)"}
72
- >
73
- <Crab
74
- className={cn(
75
- "size-3.5 text-amber-400",
76
- "animate-crab-settle",
77
- "drop-shadow-[0_0_4px_rgba(251,191,36,0.5)]",
78
- )}
79
- />
80
- </span>
81
- ) : isTurnStart ? (
82
- <span title="Start of turn">
83
- <Crab
84
- className={cn(
85
- "size-3.5 text-emerald-400",
86
- "animate-crab-appear",
87
- "drop-shadow-[0_0_4px_rgba(52,211,153,0.5)]",
88
- )}
89
- />
90
- </span>
91
- ) : isRunning ? (
92
- <span title="Processing…">
93
- <Crab className={cn("size-3.5 text-amber-300/80", "animate-crab-crawl")} />
94
- </span>
95
- ) : (
76
+ <Crab
77
+ className={cn(
78
+ "size-3.5 text-amber-400",
79
+ "animate-crab-settle",
80
+ "drop-shadow-[0_0_4px_rgba(251,191,36,0.5)]",
81
+ )}
82
+ />
83
+ </span>
84
+ ) : isTurnStart ? (
85
+ <span title="Start of turn" {...interactiveProps}>
86
+ <Crab
87
+ className={cn(
88
+ "size-3.5 text-emerald-400",
89
+ "animate-crab-appear",
90
+ "drop-shadow-[0_0_4px_rgba(52,211,153,0.5)]",
91
+ )}
92
+ />
93
+ </span>
94
+ ) : isRunning ? (
95
+ <span title="Processing…">
96
+ <Crab className={cn("size-3.5 text-amber-300/80", "animate-crab-crawl")} />
97
+ </span>
98
+ ) : (
99
+ <span>
96
100
  <Crab className="size-3.5 text-muted-foreground/40" />
97
- )}
98
- </div>
101
+ </span>
102
+ )}
99
103
 
100
- {/* Bottom: outgoing line */}
101
- <div className="flex-1 flex justify-center min-h-1">
102
- {isBoundary ? (
103
- <div className="w-0.5 bg-muted-foreground/10 h-4" />
104
- ) : (
104
+ {/* Bottom spacer flex-1 fills whatever height the LogEntry
105
+ * consumes (header + expanded content). Turn boundaries have
106
+ * no outgoing line. */}
107
+ <div className="flex-1 flex justify-center min-h-0">
108
+ {!isBoundary && (
105
109
  <div
106
110
  className={cn(
107
111
  "w-0.5 h-full",
@@ -1,12 +1,20 @@
1
- import { type JSX, memo, useCallback, useMemo, useState } from "react";
1
+ import { type JSX, memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import { ChevronRight, Clock, Zap } from "lucide-react";
2
3
  import { isTurnBoundary } from "../../lib/stopReason";
3
- import { cn } from "../../lib/utils";
4
+ import { cn, formatTokens } from "../../lib/utils";
4
5
  import type { CapturedLog } from "../../proxy/schemas";
6
+ import { getCrabVariant } from "../ui/crab-variants";
7
+ import { ProviderLogo, detectProvider, type Provider } from "../providers/ProviderLogo";
5
8
  import type { CacheTrendEntry } from "./cacheTrend";
6
9
  import { LogEntry } from "./LogEntry";
7
10
  import { ThreadConnector } from "./ThreadConnector";
8
11
  import type { TurnEntry } from "./viewerState";
9
12
 
13
+ function formatElapsed(ms: number): string {
14
+ if (ms < 1000) return `${ms}ms`;
15
+ return `${(ms / 1000).toFixed(1)}s`;
16
+ }
17
+
10
18
  type TurnGroupProps = {
11
19
  entries: TurnEntry[];
12
20
  viewMode: "simple" | "full";
@@ -14,7 +22,6 @@ type TurnGroupProps = {
14
22
  cacheTrends?: Map<number, CacheTrendEntry>;
15
23
  onCompareWithPrevious: (log: CapturedLog) => void;
16
24
  comparisonPredecessors: Map<number, CapturedLog>;
17
- /** Turn index for alternating background colours. */
18
25
  turnIndex?: number;
19
26
  };
20
27
 
@@ -34,54 +41,248 @@ export const TurnGroup = memo(function TurnGroup({
34
41
  const collapsible = isComplete && !isPending;
35
42
  const [collapsed, setCollapsed] = useState(false);
36
43
 
44
+ // Auto-collapse when the turn finishes (transitions from incomplete → complete)
45
+ const prevCompleteRef = useRef(isComplete);
46
+ useEffect(() => {
47
+ if (isComplete && !prevCompleteRef.current) {
48
+ setCollapsed(true);
49
+ }
50
+ prevCompleteRef.current = isComplete;
51
+ }, [isComplete]);
52
+
37
53
  const toggleCollapse = useCallback(() => {
38
54
  if (collapsible) setCollapsed((prev) => !prev);
39
55
  }, [collapsible]);
40
56
 
41
- // When collapsed, only show the boundary entry
42
- const visibleEntries = useMemo(() => {
43
- if (!collapsed) return entries;
44
- const last = entries[lastIdx];
45
- return last !== undefined ? [last] : [];
46
- }, [entries, collapsed, lastIdx]);
57
+ // Aggregate totals for collapsed summary
58
+ const aggregate = useMemo(() => {
59
+ let totalInput = 0;
60
+ let totalOutput = 0;
61
+ let totalElapsed = 0;
62
+ let hasTokens = false;
63
+ let hasElapsed = false;
64
+ for (const e of entries) {
65
+ if (e.log.inputTokens !== null) {
66
+ totalInput += e.log.inputTokens;
67
+ hasTokens = true;
68
+ }
69
+ if (e.log.outputTokens !== null) {
70
+ totalOutput += e.log.outputTokens;
71
+ hasTokens = true;
72
+ }
73
+ if (e.log.elapsedMs !== null) {
74
+ totalElapsed += e.log.elapsedMs;
75
+ hasElapsed = true;
76
+ }
77
+ }
78
+ return {
79
+ totalInput,
80
+ totalOutput,
81
+ hasTokens,
82
+ totalElapsed,
83
+ hasElapsed,
84
+ };
85
+ }, [entries, lastIdx]);
86
+
87
+ // Unique providers across all entries (for collapsed model logos)
88
+ const uniqueProviders = useMemo(() => {
89
+ const seen = new Set<Provider>();
90
+ for (const e of entries) {
91
+ const p = detectProvider(e.log.model);
92
+ if (p !== "unknown") seen.add(p);
93
+ }
94
+ return [...seen];
95
+ }, [entries]);
96
+
97
+ // Crab variant creators for the dual-crab collapsed layout
98
+ const StartCrab = useMemo(() => getCrabVariant(entries[0]?.log.id ?? 0), [entries]);
99
+ const EndCrab = useMemo(() => getCrabVariant(entries[lastIdx]?.log.id ?? 0), [entries, lastIdx]);
47
100
 
48
101
  const bgClass = turnIndex % 2 === 0 ? "bg-muted/10" : "bg-muted/25";
49
102
 
103
+ // ResizeObserver → re-render connectors when any LogEntry height changes
104
+ const [layoutVersion, setLayoutVersion] = useState(0);
105
+ const containerRef = useRef<HTMLDivElement>(null);
106
+ useEffect(() => {
107
+ const el = containerRef.current;
108
+ if (!el) return;
109
+ let raf = 0;
110
+ const ro = new ResizeObserver(() => {
111
+ window.cancelAnimationFrame(raf);
112
+ raf = window.requestAnimationFrame(() => setLayoutVersion((v) => v + 1));
113
+ });
114
+ ro.observe(el);
115
+ return () => {
116
+ ro.disconnect();
117
+ window.cancelAnimationFrame(raf);
118
+ };
119
+ }, []);
120
+
50
121
  return (
51
122
  <div
123
+ ref={containerRef}
52
124
  className={cn("border rounded-lg", isPending ? "border-amber-500/10" : "border-transparent")}
53
125
  >
54
- {visibleEntries.map((entry, visibleIdx) => {
55
- const originalIdx = collapsed ? lastIdx : visibleIdx;
56
- const { log, stopReason: reason } = entry;
57
- const isBoundary = reason === "end_turn" || reason === "stop";
58
-
59
- return (
60
- <div key={log.id} className="flex items-stretch">
61
- <ThreadConnector
62
- stopReason={reason}
63
- isPending={log.responseStatus === null}
64
- isFirst={originalIdx === 0}
65
- isTurnStart={originalIdx === 0}
66
- crabIndex={log.id % 12}
67
- collapsible={collapsible && isBoundary && entries.length > 1}
68
- collapsed={collapsed}
69
- onToggle={toggleCollapse}
70
- />
71
- <div className={cn("flex-1 min-w-0 mb-0.5 rounded-lg", bgClass)}>
72
- <LogEntry
73
- log={log}
74
- viewMode={viewMode}
75
- strip={strip}
76
- cacheTrend={cacheTrends?.get(log.id) ?? null}
77
- onCompareWithPrevious={
78
- comparisonPredecessors.has(log.id) ? onCompareWithPrevious : undefined
126
+ {collapsed ? (
127
+ /* ---- Collapsed: dual-crab + summary ---- */
128
+ <div className="flex items-stretch">
129
+ {/* Dual-crab connector */}
130
+ <div className="w-6 shrink-0 flex flex-col items-center pt-0.5 pb-0.5">
131
+ {/* Start turn crab (clickable to expand) */}
132
+ <div className="flex justify-center h-[calc(0.75rem-8px)]" />
133
+ <span
134
+ role="button"
135
+ tabIndex={0}
136
+ title="Start of turn — click to expand"
137
+ className="cursor-pointer"
138
+ onClick={(e) => {
139
+ e.stopPropagation();
140
+ toggleCollapse();
141
+ }}
142
+ onKeyDown={(e) => {
143
+ if (e.key === "Enter" || e.key === " ") {
144
+ e.preventDefault();
145
+ toggleCollapse();
79
146
  }
147
+ }}
148
+ >
149
+ <StartCrab
150
+ className={cn(
151
+ "size-3.5 text-emerald-400",
152
+ "animate-crab-appear",
153
+ "drop-shadow-[0_0_4px_rgba(52,211,153,0.5)]",
154
+ )}
80
155
  />
156
+ </span>
157
+
158
+ {/* Connecting line between the two crabs */}
159
+ <div className="flex-1 flex justify-center min-h-0">
160
+ <div className="w-0.5 bg-muted-foreground/30 h-full" />
81
161
  </div>
162
+
163
+ {/* End turn crab (clickable to expand) */}
164
+ <span
165
+ role="button"
166
+ tabIndex={0}
167
+ title="End of Turn — click to expand"
168
+ className="cursor-pointer"
169
+ onClick={(e) => {
170
+ e.stopPropagation();
171
+ toggleCollapse();
172
+ }}
173
+ onKeyDown={(e) => {
174
+ if (e.key === "Enter" || e.key === " ") {
175
+ e.preventDefault();
176
+ toggleCollapse();
177
+ }
178
+ }}
179
+ >
180
+ <EndCrab
181
+ className={cn(
182
+ "size-3.5 text-amber-400",
183
+ "animate-crab-settle",
184
+ "drop-shadow-[0_0_4px_rgba(251,191,36,0.5)]",
185
+ )}
186
+ />
187
+ </span>
188
+ </div>
189
+
190
+ {/* Summary content — card-like appearance matching LogEntry */}
191
+ <div
192
+ className={cn(
193
+ "flex-1 min-w-0 mb-0.5 rounded-lg border border-border py-1 px-3 flex items-center gap-3 text-xs cursor-pointer",
194
+ bgClass,
195
+ )}
196
+ onClick={toggleCollapse}
197
+ role="button"
198
+ tabIndex={0}
199
+ onKeyDown={(e) => {
200
+ if (e.key === "Enter" || e.key === " ") {
201
+ e.preventDefault();
202
+ toggleCollapse();
203
+ }
204
+ }}
205
+ >
206
+ {/* ID range */}
207
+ <span className="text-blue-400/80 font-mono font-semibold tabular-nums shrink-0">
208
+ #{entries[0]?.log.id ?? "?"} ~ #{entries[lastIdx]?.log.id ?? "?"}
209
+ </span>
210
+
211
+ {/* Request count */}
212
+ <span className="text-muted-foreground shrink-0">
213
+ {entries.length} request{entries.length > 1 ? "s" : ""}
214
+ </span>
215
+
216
+ {/* Model logos — one per unique provider */}
217
+ {uniqueProviders.length > 0 && (
218
+ <span className="flex items-center gap-0.5 shrink-0">
219
+ {uniqueProviders.map((p) => (
220
+ <ProviderLogo key={p} provider={p} className="size-4" />
221
+ ))}
222
+ </span>
223
+ )}
224
+
225
+ {/* Elapsed */}
226
+ {aggregate.hasElapsed && (
227
+ <span className="hidden xl:flex items-center gap-1 text-muted-foreground shrink-0">
228
+ <Clock className="size-3" />
229
+ <span className="font-mono tabular-nums">
230
+ {formatElapsed(aggregate.totalElapsed)}
231
+ </span>
232
+ </span>
233
+ )}
234
+
235
+ {/* Tokens */}
236
+ {aggregate.hasTokens && (
237
+ <span className="flex items-center gap-1 shrink-0">
238
+ <Zap className="size-3 text-muted-foreground" />
239
+ <span className="font-mono tabular-nums">
240
+ <span className="text-blue-400">IN {formatTokens(aggregate.totalInput)}</span>
241
+ {" / "}
242
+ <span className="text-amber-400">OUT {formatTokens(aggregate.totalOutput)}</span>
243
+ </span>
244
+ </span>
245
+ )}
246
+
247
+ {/* Spacer */}
248
+ <span className="flex-1 min-w-0" />
249
+
250
+ {/* Expand chevron */}
251
+ <ChevronRight className="size-4 text-muted-foreground shrink-0" />
82
252
  </div>
83
- );
84
- })}
253
+ </div>
254
+ ) : (
255
+ /* ---- Expanded: full entries ---- */
256
+ entries.map((entry, visibleIdx) => {
257
+ const { log, stopReason: reason } = entry;
258
+ const isTurnStart = visibleIdx === 0;
259
+
260
+ return (
261
+ <div key={log.id} className="flex items-stretch">
262
+ <ThreadConnector
263
+ stopReason={reason}
264
+ isPending={log.responseStatus === null}
265
+ isFirst={visibleIdx === 0}
266
+ isTurnStart={isTurnStart}
267
+ crabIndex={log.id % 12}
268
+ collapsible={collapsible && entries.length > 1 && isTurnStart}
269
+ onToggle={toggleCollapse}
270
+ />
271
+ <div className={cn("flex-1 min-w-0 mb-0.5 rounded-lg", bgClass)}>
272
+ <LogEntry
273
+ log={log}
274
+ viewMode={viewMode}
275
+ strip={strip}
276
+ cacheTrend={cacheTrends?.get(log.id) ?? null}
277
+ onCompareWithPrevious={
278
+ comparisonPredecessors.has(log.id) ? onCompareWithPrevious : undefined
279
+ }
280
+ />
281
+ </div>
282
+ </div>
283
+ );
284
+ })
285
+ )}
85
286
  </div>
86
287
  );
87
288
  });
@@ -251,8 +251,7 @@ const JsonNode = memo(function JsonNode({
251
251
  const openBracket = dataType === "array" ? "[" : "{";
252
252
  const closeBracket = dataType === "array" ? "]" : "}";
253
253
 
254
- function handleExpandAll(e: React.MouseEvent): void {
255
- e.stopPropagation();
254
+ function toggleDeepExpansion(): void {
256
255
  if (allExpanded) {
257
256
  setExpanded(false);
258
257
  setChildDepthOverride(0);
@@ -266,6 +265,11 @@ const JsonNode = memo(function JsonNode({
266
265
  }
267
266
  }
268
267
 
268
+ function handleExpandAll(e: React.MouseEvent): void {
269
+ e.stopPropagation();
270
+ toggleDeepExpansion();
271
+ }
272
+
269
273
  const effectiveChildDepth = childDepthOverride ?? defaultExpandDepth;
270
274
 
271
275
  return (
@@ -295,6 +299,9 @@ const JsonNode = memo(function JsonNode({
295
299
  }
296
300
  : undefined
297
301
  }
302
+ onDoubleClick={
303
+ expandable && hasExpandableDescendant(value) ? () => toggleDeepExpansion() : undefined
304
+ }
298
305
  role={expandable ? "button" : undefined}
299
306
  tabIndex={expandable ? 0 : undefined}
300
307
  >
@@ -10,6 +10,7 @@ import { z } from "zod";
10
10
  */
11
11
  export const RuntimeConfigSchema = z.object({
12
12
  stripClaudeCodeBillingHeader: z.boolean(),
13
+ hasSeenOnboarding: z.boolean().default(false),
13
14
  });
14
15
 
15
16
  export type RuntimeConfig = z.infer<typeof RuntimeConfigSchema>;
@@ -0,0 +1,74 @@
1
+ import useSWR, { type SWRResponse, useSWRConfig } from "swr";
2
+ import { fetchJson } from "./apiClient";
3
+ import { RuntimeConfigSchema, type RuntimeConfig } from "./runtimeConfig";
4
+
5
+ export const ONBOARDING_SWR_KEY = "/api/config";
6
+
7
+ async function fetcher(url: string): Promise<RuntimeConfig> {
8
+ return fetchJson(
9
+ url,
10
+ RuntimeConfigSchema,
11
+ undefined,
12
+ (response) => `Failed to fetch ${url}: ${response.status}`,
13
+ );
14
+ }
15
+
16
+ async function patchRuntimeConfig(patch: Partial<RuntimeConfig>): Promise<RuntimeConfig> {
17
+ return fetchJson(
18
+ ONBOARDING_SWR_KEY,
19
+ RuntimeConfigSchema,
20
+ {
21
+ method: "PATCH",
22
+ headers: { "content-type": "application/json" },
23
+ body: JSON.stringify(patch),
24
+ },
25
+ (response) => `PATCH failed with status ${response.status}`,
26
+ );
27
+ }
28
+
29
+ export type UseOnboarding = {
30
+ hasSeenOnboarding: boolean;
31
+ isLoading: boolean;
32
+ markSeen: () => Promise<void>;
33
+ };
34
+
35
+ /**
36
+ * Hook for the first-launch onboarding banner. The server's
37
+ * `hasSeenOnboarding` flag is the source of truth — set to `true` on
38
+ * dismissal and persisted in the server config file so it survives
39
+ * across browser sessions.
40
+ *
41
+ * `hasSeenOnboarding` defaults to `false` until the SWR fetch resolves,
42
+ * which means the banner shows briefly on first load. That is
43
+ * acceptable: the fetch is local and resolves within milliseconds.
44
+ */
45
+ export function useOnboarding(): UseOnboarding {
46
+ const response: SWRResponse<RuntimeConfig, Error> = useSWR<RuntimeConfig, Error>(
47
+ ONBOARDING_SWR_KEY,
48
+ fetcher,
49
+ {
50
+ revalidateOnFocus: false,
51
+ revalidateIfStale: false,
52
+ },
53
+ );
54
+ const { mutate: globalMutate } = useSWRConfig();
55
+
56
+ const hasSeenOnboarding = response.data?.hasSeenOnboarding ?? false;
57
+
58
+ const markSeen = async (): Promise<void> => {
59
+ await globalMutate(ONBOARDING_SWR_KEY, patchRuntimeConfig({ hasSeenOnboarding: true }), {
60
+ optimisticData: {
61
+ stripClaudeCodeBillingHeader: response.data?.stripClaudeCodeBillingHeader ?? false,
62
+ hasSeenOnboarding: true,
63
+ },
64
+ rollbackOnError: true,
65
+ revalidate: false,
66
+ });
67
+ };
68
+
69
+ return {
70
+ hasSeenOnboarding,
71
+ isLoading: response.isLoading,
72
+ markSeen,
73
+ };
74
+ }
@@ -73,11 +73,11 @@ function resolveInitialConfig(): RuntimeConfig {
73
73
 
74
74
  // 2. Env var
75
75
  if (process.env["LLM_INSPECTOR_STRIP_CLAUDE_CODE_BILLING_HEADER"] === "1") {
76
- return { stripClaudeCodeBillingHeader: true };
76
+ return { stripClaudeCodeBillingHeader: true, hasSeenOnboarding: false };
77
77
  }
78
78
 
79
79
  // 3. Default off
80
- return { stripClaudeCodeBillingHeader: false };
80
+ return { stripClaudeCodeBillingHeader: false, hasSeenOnboarding: false };
81
81
  }
82
82
 
83
83
  /**
@@ -6,6 +6,7 @@ import { getConfig, setConfig, RuntimeConfigSchema } from "../../proxy/config";
6
6
  const RuntimeConfigPatchSchema = z
7
7
  .object({
8
8
  stripClaudeCodeBillingHeader: z.boolean().optional(),
9
+ hasSeenOnboarding: z.boolean().optional(),
9
10
  })
10
11
  .strict()
11
12
  .refine((v) => Object.keys(v).length > 0, {