@tonyclaw/agent-inspector 2.0.4 → 2.0.6

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 (61) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/{CompareDrawer-BCH_fsLm.js → CompareDrawer-DDmqSAfl.js} +1 -1
  3. package/.output/public/assets/ProxyViewerContainer-Cxpdziwd.js +101 -0
  4. package/.output/public/assets/ReplayDialog-Bt5DGzlh.js +1 -0
  5. package/.output/public/assets/RequestAnatomy-BxX3_N9S.js +1 -0
  6. package/.output/public/assets/ResponseView-Bl_5S9gZ.js +1 -0
  7. package/.output/public/assets/StreamingChunkSequence-RJMwNf6F.js +1 -0
  8. package/.output/public/assets/_sessionId-b4isaoDp.js +1 -0
  9. package/.output/public/assets/index-BZ4x5UI6.js +1 -0
  10. package/.output/public/assets/{index-CobXD0yH.css → index-C624DUk9.css} +1 -1
  11. package/.output/public/assets/{json-viewer-BrzjD7qI.js → json-viewer-CRL_gWEZ.js} +1 -1
  12. package/.output/public/assets/{main-mgxeUdZQ.js → main-CKnTJ4-O.js} +6 -6
  13. package/.output/server/_libs/lucide-react.mjs +181 -114
  14. package/.output/server/{_sessionId-C4xsxIWm.mjs → _sessionId-B-x9fRY3.mjs} +3 -3
  15. package/.output/server/_ssr/{CompareDrawer-DuWEpqQ7.mjs → CompareDrawer-BQVNsAY2.mjs} +6 -6
  16. package/.output/server/_ssr/{ProxyViewerContainer-Cckz5qKu.mjs → ProxyViewerContainer-CYm2Dw19.mjs} +766 -122
  17. package/.output/server/_ssr/{ReplayDialog-BDRcr8E5.mjs → ReplayDialog-CaMQBc79.mjs} +240 -14
  18. package/.output/server/_ssr/{RequestAnatomy-BoO2_Ij0.mjs → RequestAnatomy--P5arRH2.mjs} +236 -66
  19. package/.output/server/_ssr/{ResponseView-DZiPBxvO.mjs → ResponseView-RtFwNvgD.mjs} +8 -8
  20. package/.output/server/_ssr/{StreamingChunkSequence-D-be7KEL.mjs → StreamingChunkSequence-B5HPkzab.mjs} +3 -3
  21. package/.output/server/_ssr/{index-5RImHKfu.mjs → index-CZIKZU43.mjs} +2 -2
  22. package/.output/server/_ssr/index.mjs +2 -2
  23. package/.output/server/_ssr/{json-viewer-aJhb93ZK.mjs → json-viewer-d4obyRaA.mjs} +3 -3
  24. package/.output/server/_ssr/{router-Dgkv5nKP.mjs → router-DGPt3MUc.mjs} +145 -71
  25. package/.output/server/_tanstack-start-manifest_v-BzH4pNaI.mjs +4 -0
  26. package/.output/server/index.mjs +64 -64
  27. package/package.json +1 -1
  28. package/src/components/OnboardingBanner.tsx +11 -19
  29. package/src/components/ProxyViewer.tsx +1 -1
  30. package/src/components/providers/ProviderCard.tsx +6 -20
  31. package/src/components/providers/SettingsDialog.tsx +95 -2
  32. package/src/components/proxy-viewer/AgentTraceSummary.tsx +639 -38
  33. package/src/components/proxy-viewer/CompareDrawer.tsx +4 -2
  34. package/src/components/proxy-viewer/LogEntry.tsx +4 -4
  35. package/src/components/proxy-viewer/LogEntryHeader.tsx +15 -25
  36. package/src/components/proxy-viewer/ReplayDialog.tsx +190 -8
  37. package/src/components/proxy-viewer/ResponseView.tsx +2 -2
  38. package/src/components/proxy-viewer/ToolTraceEvents.tsx +37 -16
  39. package/src/components/proxy-viewer/TurnGroup.tsx +14 -2
  40. package/src/components/proxy-viewer/anatomy/RequestAnatomy.tsx +196 -45
  41. package/src/components/proxy-viewer/anatomy/SegmentBar.tsx +92 -67
  42. package/src/components/proxy-viewer/anatomy/types.ts +15 -13
  43. package/src/components/proxy-viewer/formats/anthropic/ResponseView.tsx +2 -2
  44. package/src/components/proxy-viewer/log-formats/anthropic.ts +1 -1
  45. package/src/components/proxy-viewer/log-formats/openai.ts +1 -1
  46. package/src/components/proxy-viewer/log-formats/types.ts +1 -1
  47. package/src/components/proxy-viewer/replayComparison.ts +131 -0
  48. package/src/components/proxy-viewer/useKeyboardNavigation.ts +64 -22
  49. package/src/components/proxy-viewer/viewerState.ts +14 -2
  50. package/src/components/ui/json-viewer.tsx +1 -1
  51. package/src/knowledge/candidateStore.ts +32 -1
  52. package/src/routes/api/knowledge.candidates.$candidateId.ts +50 -0
  53. package/src/routes/api/knowledge.sessions.$sessionId.candidates.ts +12 -2
  54. package/.output/public/assets/ProxyViewerContainer-D85_UANk.js +0 -101
  55. package/.output/public/assets/ReplayDialog-DTeaHHit.js +0 -1
  56. package/.output/public/assets/RequestAnatomy-DZ8grAih.js +0 -1
  57. package/.output/public/assets/ResponseView-Cldm6RCi.js +0 -1
  58. package/.output/public/assets/StreamingChunkSequence-3x4p-yT7.js +0 -1
  59. package/.output/public/assets/_sessionId-YqWFBu6d.js +0 -1
  60. package/.output/public/assets/index-BIw2H6jO.js +0 -1
  61. package/.output/server/_tanstack-start-manifest_v-B8rrWXjr.mjs +0 -4
@@ -1,27 +1,77 @@
1
1
  import { Info } from "lucide-react";
2
- import { type JSX, useMemo } from "react";
2
+ import { type JSX, useMemo, useState } from "react";
3
3
  import { cn, formatTokens } from "../../../lib/utils";
4
4
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../../ui/tooltip";
5
- import { SegmentBar } from "./SegmentBar";
6
- import type { AnatomySegment } from "./types";
5
+ import { ROLE_COLOR_CLASSES, SegmentBar } from "./SegmentBar";
6
+ import { ANATOMY_ROLE_LABELS, type AnatomyRole, type AnatomySegment } from "./types";
7
7
 
8
8
  export type RequestAnatomyProps = {
9
9
  /** Parsed request body, or `null` if it cannot be parsed. */
10
10
  parsed: unknown | null;
11
11
  /** Server-reported input token count, or `null` if unknown. */
12
12
  inputTokens: number | null;
13
- /** Optional callback fired when the user activates a segment. */
13
+ /** Optional callback fired when the user activates a concrete request segment. */
14
14
  onSegmentActivate?: (segment: AnatomySegment) => void;
15
15
  /** Pre-computed segments; if provided, `parsed` is ignored. */
16
16
  segments?: AnatomySegment[] | null;
17
17
  };
18
18
 
19
+ type ContextViewMode = "role" | "segment";
20
+
19
21
  const DIVERGENCE_AMBER_THRESHOLD = 0.25;
22
+ const TOP_CONTRIBUTOR_COUNT = 5;
23
+ const ROLE_ORDER: AnatomyRole[] = ["system", "user", "assistant", "tool", "tools"];
24
+
25
+ const ROLE_DESCRIPTIONS: Record<AnatomyRole, string> = {
26
+ system: "instructions",
27
+ user: "user turns",
28
+ assistant: "assistant turns",
29
+ tool: "tool results",
30
+ tools: "tool schemas",
31
+ };
32
+
33
+ const VIEW_MODE_OPTIONS: Array<{ value: ContextViewMode; label: string }> = [
34
+ { value: "role", label: "By Role" },
35
+ { value: "segment", label: "By Segment" },
36
+ ];
37
+
38
+ function formatPercent(value: number): string {
39
+ if (value >= 10) return `${value.toFixed(0)}%`;
40
+ if (value >= 1) return `${value.toFixed(1)}%`;
41
+ if (value > 0) return "<1%";
42
+ return "0%";
43
+ }
44
+
45
+ function aggregateByRole(segments: AnatomySegment[]): AnatomySegment[] {
46
+ const result: AnatomySegment[] = [];
47
+ for (const role of ROLE_ORDER) {
48
+ const matching = segments.filter((segment) => segment.role === role);
49
+ if (matching.length === 0) continue;
50
+ const size = matching.reduce((sum, segment) => sum + segment.size, 0);
51
+ const characters = matching.reduce((sum, segment) => sum + segment.characters, 0);
52
+ const label = ANATOMY_ROLE_LABELS[role];
53
+ const text = `${matching.length} segment${matching.length === 1 ? "" : "s"} grouped as ${
54
+ ROLE_DESCRIPTIONS[role]
55
+ }`;
56
+ result.push({
57
+ role,
58
+ label,
59
+ size,
60
+ characters,
61
+ text,
62
+ path: `role:${role}`,
63
+ });
64
+ }
65
+ return result;
66
+ }
67
+
68
+ function topContributors(segments: AnatomySegment[]): AnatomySegment[] {
69
+ return [...segments].sort((a, b) => b.size - a.size).slice(0, TOP_CONTRIBUTOR_COUNT);
70
+ }
20
71
 
21
72
  /**
22
- * Render the request anatomy view: a one-line summary plus the
23
- * stacked segment bar. Returns `null` when the request cannot be
24
- * parsed or the segment list is empty.
73
+ * Render a request context breakdown: headline metrics, a stacked token bar,
74
+ * role legend, and the largest individual context blocks.
25
75
  */
26
76
  export function RequestAnatomy({
27
77
  parsed,
@@ -29,69 +79,170 @@ export function RequestAnatomy({
29
79
  onSegmentActivate,
30
80
  segments: providedSegments,
31
81
  }: RequestAnatomyProps): JSX.Element | null {
32
- // Caller is expected to pre-compute segments in production (see
33
- // LogEntry). When called standalone, fall back to deriving from
34
- // `parsed` directly using the unknown adapter is not appropriate —
35
- // the caller is responsible for choosing the right adapter.
82
+ const [viewMode, setViewMode] = useState<ContextViewMode>("role");
36
83
  const segments = useMemo(() => providedSegments ?? null, [providedSegments]);
37
- const total = useMemo(() => (segments ?? []).reduce((sum, s) => sum + s.size, 0), [segments]);
84
+ const total = useMemo(
85
+ () => (segments ?? []).reduce((sum, segment) => sum + segment.size, 0),
86
+ [segments],
87
+ );
88
+ const roleSegments = useMemo(
89
+ () => (segments === null ? [] : aggregateByRole(segments)),
90
+ [segments],
91
+ );
92
+ const topSegments = useMemo(
93
+ () => (segments === null ? [] : topContributors(segments)),
94
+ [segments],
95
+ );
38
96
 
39
- // Show divergence warning when both estimate and server numbers exist.
40
97
  const showDivergenceWarning = useMemo(() => {
41
- if (segments === null || segments === undefined) return false;
98
+ if (segments === null) return false;
42
99
  if (inputTokens === null) return false;
43
100
  if (total === 0) return false;
44
101
  const ratio = Math.abs(inputTokens - total) / Math.max(inputTokens, total);
45
102
  return ratio >= DIVERGENCE_AMBER_THRESHOLD;
46
103
  }, [inputTokens, segments, total]);
47
104
 
48
- const summaryColorClass = useMemo(() => {
49
- if (inputTokens === null) return "text-muted-foreground";
50
- if (showDivergenceWarning) return "text-amber-400";
51
- return "text-muted-foreground";
52
- }, [inputTokens, showDivergenceWarning]);
105
+ if (segments === null) return null;
106
+ if (segments.length === 0) return null;
107
+ if (parsed === null && providedSegments === undefined) return null;
53
108
 
54
- if (segments === null || segments === undefined) {
55
- return null;
56
- }
57
- if (segments.length === 0) {
58
- return null;
59
- }
60
- if (parsed === null && providedSegments === undefined) {
61
- return null;
62
- }
109
+ const summaryColorClass =
110
+ inputTokens !== null && showDivergenceWarning ? "text-amber-400" : "text-muted-foreground";
111
+ const displayedSegments = viewMode === "role" ? roleSegments : segments;
112
+ const displayedTotal =
113
+ total > 0 ? total : displayedSegments.reduce((sum, item) => sum + item.size, 0);
114
+ const activateSegment = viewMode === "segment" ? onSegmentActivate : undefined;
115
+ const segmentCountLabel = `${String(segments.length)} segment${segments.length === 1 ? "" : "s"}`;
116
+ const providerInputLabel =
117
+ inputTokens === null ? "Provider input unknown" : `Provider input ${formatTokens(inputTokens)}`;
63
118
 
64
119
  return (
65
120
  <TooltipProvider delayDuration={150}>
66
- <div className="px-4 py-3 space-y-2" data-testid="anatomy-root">
67
- <div className="flex items-center gap-2 text-xs">
68
- <span className="font-mono tabular-nums text-foreground">
69
- ~{formatTokens(total)} tokens
70
- </span>
71
- {inputTokens !== null && (
72
- <span className={cn("font-mono tabular-nums", summaryColorClass)}>
73
- (server: {formatTokens(inputTokens)})
74
- </span>
75
- )}
76
- {showDivergenceWarning && (
121
+ <div className="px-4 py-3 space-y-3" data-testid="anatomy-root">
122
+ <div className="flex flex-wrap items-start justify-between gap-3">
123
+ <div className="min-w-0">
124
+ <div className="text-sm font-semibold text-foreground">Request Context</div>
125
+ <div className={cn("mt-0.5 text-xs font-mono tabular-nums", summaryColorClass)}>
126
+ Estimated ~{formatTokens(total)} tokens | {providerInputLabel} | {segmentCountLabel}
127
+ </div>
128
+ </div>
129
+
130
+ <div
131
+ className="inline-flex shrink-0 rounded border border-border bg-muted/20 p-0.5"
132
+ role="group"
133
+ aria-label="Context breakdown mode"
134
+ >
135
+ {VIEW_MODE_OPTIONS.map((option) => (
136
+ <button
137
+ key={option.value}
138
+ type="button"
139
+ aria-pressed={viewMode === option.value}
140
+ onClick={() => setViewMode(option.value)}
141
+ className={cn(
142
+ "h-6 rounded-sm px-2 text-[11px] font-medium transition-colors",
143
+ viewMode === option.value
144
+ ? "bg-background text-foreground shadow-sm"
145
+ : "text-muted-foreground hover:text-foreground",
146
+ )}
147
+ >
148
+ {option.label}
149
+ </button>
150
+ ))}
151
+ </div>
152
+ </div>
153
+
154
+ {showDivergenceWarning && (
155
+ <div className="inline-flex items-center gap-1.5 text-xs text-amber-400">
77
156
  <Tooltip>
78
157
  <TooltipTrigger asChild>
79
158
  <button
80
159
  type="button"
81
- className="inline-flex items-center text-amber-400 hover:text-amber-300"
82
- aria-label="Token estimate diverges from server"
160
+ className="inline-flex items-center hover:text-amber-300"
161
+ aria-label="Token estimate differs from provider input"
83
162
  >
84
163
  <Info className="size-3.5" />
85
164
  </button>
86
165
  </TooltipTrigger>
87
166
  <TooltipContent className="max-w-xs text-xs">
88
- Bar uses a token estimate heuristic (~4 ASCII chars / token, ~1 CJK / emoji char per
89
- token). The server&apos;s reported value is the source of truth for cost.
167
+ The bar uses a local token estimate. Provider input tokens remain the source of
168
+ truth for billing and context-window usage.
90
169
  </TooltipContent>
91
170
  </Tooltip>
92
- )}
171
+ Estimate differs from provider-reported input.
172
+ </div>
173
+ )}
174
+
175
+ <SegmentBar
176
+ segments={displayedSegments}
177
+ totalTokens={displayedTotal}
178
+ showLabels={viewMode === "segment"}
179
+ onActivate={activateSegment}
180
+ />
181
+
182
+ <div className="flex flex-wrap items-center gap-x-3 gap-y-1.5 text-[11px] text-muted-foreground">
183
+ {roleSegments.map((segment) => {
184
+ const percent = total > 0 ? (segment.size / total) * 100 : 0;
185
+ return (
186
+ <div key={segment.role} className="inline-flex items-center gap-1.5">
187
+ <span
188
+ aria-hidden="true"
189
+ className={cn("size-2.5 rounded-[2px]", ROLE_COLOR_CLASSES[segment.role])}
190
+ />
191
+ <span>{segment.label}</span>
192
+ <span className="font-mono text-muted-foreground/70">{formatPercent(percent)}</span>
193
+ </div>
194
+ );
195
+ })}
196
+ </div>
197
+
198
+ <div className="space-y-1.5">
199
+ <div className="text-xs font-medium text-foreground">Top Contributors</div>
200
+ <div className="grid gap-1">
201
+ {topSegments.map((segment, index) => {
202
+ const percent = total > 0 ? (segment.size / total) * 100 : 0;
203
+ const activate = onSegmentActivate;
204
+ const content = (
205
+ <>
206
+ <span className="w-5 shrink-0 text-right font-mono text-muted-foreground/70">
207
+ {String(index + 1)}
208
+ </span>
209
+ <span
210
+ aria-hidden="true"
211
+ className={cn(
212
+ "size-2.5 shrink-0 rounded-[2px]",
213
+ ROLE_COLOR_CLASSES[segment.role],
214
+ )}
215
+ />
216
+ <span className="min-w-0 flex-1 truncate text-left">{segment.label}</span>
217
+ <span className="shrink-0 font-mono text-muted-foreground">
218
+ {formatPercent(percent)} | ~{formatTokens(segment.size)}
219
+ </span>
220
+ </>
221
+ );
222
+ if (activate !== undefined) {
223
+ return (
224
+ <button
225
+ key={`${segment.path}-${index}`}
226
+ type="button"
227
+ onClick={() => activate(segment)}
228
+ className="flex h-7 items-center gap-2 rounded px-1.5 text-xs text-muted-foreground hover:bg-muted/40 hover:text-foreground"
229
+ title="Jump to this request block"
230
+ >
231
+ {content}
232
+ </button>
233
+ );
234
+ }
235
+ return (
236
+ <div
237
+ key={`${segment.path}-${index}`}
238
+ className="flex h-7 items-center gap-2 rounded px-1.5 text-xs text-muted-foreground"
239
+ >
240
+ {content}
241
+ </div>
242
+ );
243
+ })}
244
+ </div>
93
245
  </div>
94
- <SegmentBar segments={segments} totalTokens={total} onActivate={onSegmentActivate} />
95
246
  </div>
96
247
  </TooltipProvider>
97
248
  );
@@ -1,9 +1,9 @@
1
1
  import { type JSX, memo, useMemo } from "react";
2
2
  import { cn, formatTokens } from "../../../lib/utils";
3
3
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../../ui/tooltip";
4
- import type { AnatomyRole, AnatomySegment } from "./types";
4
+ import { ANATOMY_ROLE_LABELS, type AnatomyRole, type AnatomySegment } from "./types";
5
5
 
6
- const ROLE_COLOR_CLASSES: Record<AnatomyRole, string> = {
6
+ export const ROLE_COLOR_CLASSES: Record<AnatomyRole, string> = {
7
7
  system: "bg-sky-500/70",
8
8
  user: "bg-emerald-500/70",
9
9
  assistant: "bg-violet-500/70",
@@ -21,59 +21,69 @@ const ROLE_FOCUS_RING: Record<AnatomyRole, string> = {
21
21
 
22
22
  const MAX_VISIBLE_SEGMENTS = 12;
23
23
  const MIN_SEGMENT_PERCENT = 1;
24
-
25
24
  const TOOLTIP_PREVIEW_LIMIT = 80;
26
25
  const LABEL_TRUNCATE_LIMIT = 24;
27
26
 
28
27
  function truncateLabel(label: string): string {
29
28
  if (label.length <= LABEL_TRUNCATE_LIMIT) return label;
30
- return `${label.slice(0, LABEL_TRUNCATE_LIMIT - 1)}…`;
29
+ return `${label.slice(0, LABEL_TRUNCATE_LIMIT - 3)}...`;
31
30
  }
32
31
 
33
32
  function truncatePreview(text: string): string {
34
33
  const singleLine = text.replace(/\s+/g, " ").trim();
35
34
  if (singleLine.length <= TOOLTIP_PREVIEW_LIMIT) return singleLine;
36
- return `${singleLine.slice(0, TOOLTIP_PREVIEW_LIMIT)}…`;
35
+ return `${singleLine.slice(0, TOOLTIP_PREVIEW_LIMIT)}...`;
36
+ }
37
+
38
+ function formatPercent(value: number): string {
39
+ if (value >= 10) return `${value.toFixed(0)}%`;
40
+ if (value >= 1) return `${value.toFixed(1)}%`;
41
+ if (value > 0) return "<1%";
42
+ return "0%";
37
43
  }
38
44
 
39
45
  export type SegmentBarProps = {
40
46
  segments: ReadonlyArray<AnatomySegment>;
41
47
  totalTokens: number;
48
+ showLabels?: boolean;
42
49
  onActivate?: (segment: AnatomySegment) => void;
43
50
  };
44
51
 
45
52
  /**
46
- * Render a horizontal stacked bar showing the relative size of each
47
- * request segment, with role-based color, per-segment labels, and
48
- * an overflow pill that aggregates any segments beyond the visible
49
- * cap. Click or keyboard activation (Enter / Space) calls `onActivate`.
53
+ * Render a horizontal stacked bar showing the relative size of each request
54
+ * context segment. Segment mode can activate a JSON path; aggregate modes stay
55
+ * static so they do not imply a misleading jump target.
50
56
  */
51
57
  export const SegmentBar = memo(function SegmentBar({
52
58
  segments,
53
59
  totalTokens,
60
+ showLabels = true,
54
61
  onActivate,
55
62
  }: SegmentBarProps): JSX.Element {
56
63
  const total = useMemo(() => {
57
64
  if (totalTokens > 0) return totalTokens;
58
- return segments.reduce((sum, s) => sum + s.size, 0);
65
+ return segments.reduce((sum, segment) => sum + segment.size, 0);
59
66
  }, [segments, totalTokens]);
60
67
 
61
68
  const visibleSegments = segments.slice(0, MAX_VISIBLE_SEGMENTS);
62
69
  const overflowSegments = segments.slice(MAX_VISIBLE_SEGMENTS);
63
- const overflowSize = overflowSegments.reduce((sum, s) => sum + s.size, 0);
70
+ const overflowSize = overflowSegments.reduce((sum, segment) => sum + segment.size, 0);
64
71
  const overflowCount = overflowSegments.length;
65
72
  const overflowStartIndex = MAX_VISIBLE_SEGMENTS;
66
73
  const overflowEndIndex = overflowStartIndex + overflowCount - 1;
67
74
  const hasOverflow = overflowCount > 0;
75
+ const interactive = onActivate !== undefined;
68
76
 
69
77
  const visibleTotal = useMemo(
70
- () => visibleSegments.reduce((sum, s) => sum + s.size, 0),
78
+ () => visibleSegments.reduce((sum, segment) => sum + segment.size, 0),
71
79
  [visibleSegments],
72
80
  );
73
81
 
74
82
  const ariaLabel = useMemo(
75
83
  () =>
76
- `Request anatomy: ~${formatTokens(total)} tokens across ${segments.length} segment${segments.length === 1 ? "" : "s"}`,
84
+ `Request context: ~${formatTokens(total)} tokens across ${segments.length} segment${
85
+ segments.length === 1 ? "" : "s"
86
+ }`,
77
87
  [segments.length, total],
78
88
  );
79
89
 
@@ -91,36 +101,48 @@ export const SegmentBar = memo(function SegmentBar({
91
101
  {visibleSegments.map((segment, index) => {
92
102
  const rawPercent = total > 0 ? (segment.size / total) * 100 : 0;
93
103
  const percent = Math.max(MIN_SEGMENT_PERCENT, rawPercent);
104
+ const segmentClassName = cn(
105
+ "h-full border-r border-background/80 last:border-r-0",
106
+ interactive
107
+ ? "opacity-90 hover:opacity-100 focus:opacity-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-offset-background"
108
+ : "opacity-90",
109
+ ROLE_COLOR_CLASSES[segment.role],
110
+ interactive ? ROLE_FOCUS_RING[segment.role] : "",
111
+ );
112
+ const segmentStyle = { width: `${percent}%` };
113
+ const ariaText = `${segment.label}, ${formatPercent(rawPercent)}, ~${formatTokens(
114
+ segment.size,
115
+ )} tokens`;
94
116
  return (
95
117
  <Tooltip key={`${segment.path}-${index}`}>
96
118
  <TooltipTrigger asChild>
97
- <button
98
- type="button"
99
- role="button"
100
- tabIndex={0}
101
- onClick={() => onActivate?.(segment)}
102
- onKeyDown={(e) => {
103
- if (e.key === "Enter" || e.key === " ") {
104
- e.preventDefault();
105
- onActivate?.(segment);
106
- }
107
- }}
108
- data-anatomy-path={segment.path}
109
- aria-label={`${segment.label}, ~${formatTokens(segment.size)} tokens`}
110
- className={cn(
111
- "h-full border-r border-background/80 last:border-r-0",
112
- "opacity-90 hover:opacity-100 focus:opacity-100",
113
- "focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-offset-background",
114
- ROLE_COLOR_CLASSES[segment.role],
115
- ROLE_FOCUS_RING[segment.role],
116
- )}
117
- style={{ width: `${percent}%` }}
118
- />
119
+ {interactive ? (
120
+ <button
121
+ type="button"
122
+ role="button"
123
+ tabIndex={0}
124
+ onClick={() => onActivate(segment)}
125
+ onKeyDown={(event) => {
126
+ if (event.key === "Enter" || event.key === " ") {
127
+ event.preventDefault();
128
+ onActivate(segment);
129
+ }
130
+ }}
131
+ data-anatomy-path={segment.path}
132
+ aria-label={ariaText}
133
+ className={segmentClassName}
134
+ style={segmentStyle}
135
+ />
136
+ ) : (
137
+ <span aria-label={ariaText} className={segmentClassName} style={segmentStyle} />
138
+ )}
119
139
  </TooltipTrigger>
120
140
  <TooltipContent side="bottom" className="max-w-sm text-xs p-2 space-y-0.5">
121
141
  <div className="font-semibold">
122
- {segment.label} · ~{formatTokens(segment.size)} tokens
142
+ {segment.label} - {formatPercent(rawPercent)} - ~{formatTokens(segment.size)}{" "}
143
+ tokens
123
144
  </div>
145
+ <div className="text-muted-foreground">{ANATOMY_ROLE_LABELS[segment.role]}</div>
124
146
  <div className="text-muted-foreground">
125
147
  {segment.characters.toLocaleString()} chars
126
148
  </div>
@@ -144,51 +166,54 @@ export const SegmentBar = memo(function SegmentBar({
144
166
  width: `${Math.max(MIN_SEGMENT_PERCENT, (overflowSize / total) * 100)}%`,
145
167
  }}
146
168
  >
147
- +{overflowCount}
169
+ ... +{overflowCount}
148
170
  </div>
149
171
  </TooltipTrigger>
150
172
  <TooltipContent side="bottom" className="text-xs">
151
173
  {overflowCount} more segment{overflowCount === 1 ? "" : "s"} (indices{" "}
152
- {overflowStartIndex}{overflowEndIndex})
174
+ {overflowStartIndex}-{overflowEndIndex})
153
175
  </TooltipContent>
154
176
  </Tooltip>
155
177
  )}
156
178
  </div>
157
179
 
158
- <div className="flex w-full gap-1 text-[10px] text-muted-foreground">
159
- {visibleSegments.map((segment, index) => {
160
- const rawPercent = total > 0 ? (segment.size / total) * 100 : 0;
161
- const percent = Math.max(MIN_SEGMENT_PERCENT, rawPercent);
162
- return (
180
+ {showLabels && (
181
+ <div className="flex w-full gap-1 text-[10px] text-muted-foreground">
182
+ {visibleSegments.map((segment, index) => {
183
+ const rawPercent = total > 0 ? (segment.size / total) * 100 : 0;
184
+ const percent = Math.max(MIN_SEGMENT_PERCENT, rawPercent);
185
+ return (
186
+ <div
187
+ key={`label-${segment.path}-${index}`}
188
+ className="flex flex-col gap-0.5 truncate"
189
+ style={{ width: `${percent}%` }}
190
+ title={`${segment.label} - ${formatPercent(rawPercent)} - ~${formatTokens(
191
+ segment.size,
192
+ )} tokens`}
193
+ >
194
+ <span className="truncate font-mono text-foreground/80">
195
+ {truncateLabel(segment.label)}
196
+ </span>
197
+ <span className="truncate font-mono text-muted-foreground/70">
198
+ {formatPercent(rawPercent)} - ~{formatTokens(segment.size)}
199
+ </span>
200
+ </div>
201
+ );
202
+ })}
203
+ {hasOverflow && (
163
204
  <div
164
- key={`label-${segment.path}-${index}`}
165
- className="flex flex-col gap-0.5 truncate"
166
- style={{ width: `${percent}%` }}
167
- title={`${segment.label} · ~${formatTokens(segment.size)} tokens`}
205
+ className="flex flex-col gap-0.5 truncate text-muted-foreground"
206
+ style={{ width: `${Math.max(MIN_SEGMENT_PERCENT, (overflowSize / total) * 100)}%` }}
168
207
  >
169
- <span className="truncate font-mono text-foreground/80">
170
- {truncateLabel(segment.label)}
171
- </span>
172
- <span className="truncate font-mono text-muted-foreground/70">
173
- ~{formatTokens(segment.size)}
208
+ <span className="truncate font-mono text-foreground/60">... +{overflowCount}</span>
209
+ <span className="truncate font-mono text-muted-foreground/60">
210
+ ~{formatTokens(overflowSize)}
174
211
  </span>
175
212
  </div>
176
- );
177
- })}
178
- {hasOverflow && (
179
- <div
180
- className="flex flex-col gap-0.5 truncate text-muted-foreground"
181
- style={{ width: `${Math.max(MIN_SEGMENT_PERCENT, (overflowSize / total) * 100)}%` }}
182
- >
183
- <span className="truncate font-mono text-foreground/60">… +{overflowCount}</span>
184
- <span className="truncate font-mono text-muted-foreground/60">
185
- ~{formatTokens(overflowSize)}
186
- </span>
187
- </div>
188
- )}
189
- </div>
213
+ )}
214
+ </div>
215
+ )}
190
216
 
191
- {/* Spacer to maintain minimum bar height for tiny segments */}
192
217
  {visibleTotal < total * 0.1 && <div className="h-0" />}
193
218
  </div>
194
219
  </TooltipProvider>
@@ -1,23 +1,21 @@
1
1
  /**
2
- * Role of a request segment. Drives the segment's color in the anatomy bar.
2
+ * Role of a request segment. Drives the segment's color in the context bar.
3
3
  *
4
4
  * - `system`: the system prompt (Anthropic `system`, or leading system-role message in OpenAI)
5
5
  * - `user`: a user-role message
6
6
  * - `assistant`: an assistant-role message
7
- * - `tool`: a tool-role message (OpenAI only Anthropic uses tool_result blocks inside user messages)
8
- * - `tools`: the synthetic "tool definitions" segment that aggregates the `tools` array
7
+ * - `tool`: a tool-role message (OpenAI only; Anthropic uses tool_result blocks inside user messages)
8
+ * - `tools`: the synthetic tool definitions segment that aggregates the `tools` array
9
9
  */
10
10
  export type AnatomyRole = "system" | "user" | "assistant" | "tool" | "tools";
11
11
 
12
12
  /**
13
- * A single segment of a request as visualized in the Anatomy tab.
13
+ * A single segment of a request as visualized in the request context tab.
14
14
  *
15
15
  * `path` is a JSON-pointer-style path into the parsed body
16
16
  * (e.g. `/system`, `/messages/0`, `/tools`). The path is opaque to
17
- * consumers it is matched against the same path emitted by the
18
- * request analyzer, and is used to drive click-to-jump in the
19
- * `JsonViewer`. A `null` path is allowed for the synthetic `tools`
20
- * segment in the rare case where the tools array cannot be located.
17
+ * consumers: it is matched against the same path emitted by the request
18
+ * analyzer and is used to drive click-to-jump in the `JsonViewer`.
21
19
  */
22
20
  export type AnatomySegment = {
23
21
  /** Stable role used for color and label prefix. */
@@ -30,10 +28,14 @@ export type AnatomySegment = {
30
28
  characters: number;
31
29
  /** Raw text of the segment used to compute `size`. Used in the tooltip preview. */
32
30
  text: string;
33
- /**
34
- * JSON-pointer-style path into the parsed body (e.g. `/messages/3`).
35
- * `null` when the segment does not correspond to a single node
36
- * (e.g. the synthetic `tools` segment that aggregates the array).
37
- */
31
+ /** JSON-pointer-style path into the parsed body (e.g. `/messages/3`). */
38
32
  path: string;
39
33
  };
34
+
35
+ export const ANATOMY_ROLE_LABELS: Record<AnatomyRole, string> = {
36
+ system: "System",
37
+ user: "User",
38
+ assistant: "Assistant",
39
+ tool: "Tool Results",
40
+ tools: "Tool Definitions",
41
+ };
@@ -38,14 +38,14 @@ export const StructuredResponseViewAnthropic = memo(function StructuredResponseV
38
38
  response.usage.cache_creation_input_tokens !== null &&
39
39
  response.usage.cache_creation_input_tokens > 0 && (
40
40
  <span className="font-mono tabular-nums text-emerald-400">
41
- Cache +{formatTokens(response.usage.cache_creation_input_tokens)}
41
+ KV Cache +{formatTokens(response.usage.cache_creation_input_tokens)}
42
42
  </span>
43
43
  )}
44
44
  {response.usage.cache_read_input_tokens !== undefined &&
45
45
  response.usage.cache_read_input_tokens !== null &&
46
46
  response.usage.cache_read_input_tokens > 0 && (
47
47
  <span className="font-mono tabular-nums text-purple-400">
48
- Cache ~{formatTokens(response.usage.cache_read_input_tokens)}
48
+ KV Cache ~{formatTokens(response.usage.cache_read_input_tokens)}
49
49
  </span>
50
50
  )}
51
51
  </span>
@@ -155,7 +155,7 @@ export const anthropicLogFormatAdapter: LogFormatAdapter = {
155
155
  if (parsed === null || typeof parsed !== "object") return null;
156
156
  // We deliberately skip AnthropicRequestSchema validation here:
157
157
  // real Anthropic requests accept `system` as either a string or
158
- // an array, and we want the Anatomy view to render even when the
158
+ // an array, and we want the Context view to render even when the
159
159
  // body shape is slightly off-schema (e.g. system: "string" vs
160
160
  // system: [{type:"text",text:"..."}]).
161
161
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
@@ -128,7 +128,7 @@ export const openAILogFormatAdapter: LogFormatAdapter = {
128
128
  anatomySegments(parsed) {
129
129
  if (parsed === null || typeof parsed !== "object") return null;
130
130
  // We deliberately skip OpenAIRequestSchema validation here for the
131
- // same reason as the Anthropic adapter: the Anatomy view should
131
+ // same reason as the Anthropic adapter: the Context view should
132
132
  // render even when the body shape is slightly off-schema. We only
133
133
  // need the top-level `messages` and `tools` arrays.
134
134
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
@@ -18,7 +18,7 @@ export type LogFormatAdapter = {
18
18
  analyzeRequest(rawBody: string | null): RequestAnalysis;
19
19
  analyzeResponse(responseText: string | null): ResponseAnalysis;
20
20
  /**
21
- * Derive the ordered list of segments shown in the Anatomy tab for a
21
+ * Derive the ordered list of segments shown in the Context tab for a
22
22
  * parsed request body. Returns `null` when the body is `null` or fails
23
23
  * the format's schema (e.g. unknown format).
24
24
  */