@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.
- package/.output/nitro.json +1 -1
- package/.output/public/assets/{CompareDrawer-BCH_fsLm.js → CompareDrawer-DDmqSAfl.js} +1 -1
- package/.output/public/assets/ProxyViewerContainer-Cxpdziwd.js +101 -0
- package/.output/public/assets/ReplayDialog-Bt5DGzlh.js +1 -0
- package/.output/public/assets/RequestAnatomy-BxX3_N9S.js +1 -0
- package/.output/public/assets/ResponseView-Bl_5S9gZ.js +1 -0
- package/.output/public/assets/StreamingChunkSequence-RJMwNf6F.js +1 -0
- package/.output/public/assets/_sessionId-b4isaoDp.js +1 -0
- package/.output/public/assets/index-BZ4x5UI6.js +1 -0
- package/.output/public/assets/{index-CobXD0yH.css → index-C624DUk9.css} +1 -1
- package/.output/public/assets/{json-viewer-BrzjD7qI.js → json-viewer-CRL_gWEZ.js} +1 -1
- package/.output/public/assets/{main-mgxeUdZQ.js → main-CKnTJ4-O.js} +6 -6
- package/.output/server/_libs/lucide-react.mjs +181 -114
- package/.output/server/{_sessionId-C4xsxIWm.mjs → _sessionId-B-x9fRY3.mjs} +3 -3
- package/.output/server/_ssr/{CompareDrawer-DuWEpqQ7.mjs → CompareDrawer-BQVNsAY2.mjs} +6 -6
- package/.output/server/_ssr/{ProxyViewerContainer-Cckz5qKu.mjs → ProxyViewerContainer-CYm2Dw19.mjs} +766 -122
- package/.output/server/_ssr/{ReplayDialog-BDRcr8E5.mjs → ReplayDialog-CaMQBc79.mjs} +240 -14
- package/.output/server/_ssr/{RequestAnatomy-BoO2_Ij0.mjs → RequestAnatomy--P5arRH2.mjs} +236 -66
- package/.output/server/_ssr/{ResponseView-DZiPBxvO.mjs → ResponseView-RtFwNvgD.mjs} +8 -8
- package/.output/server/_ssr/{StreamingChunkSequence-D-be7KEL.mjs → StreamingChunkSequence-B5HPkzab.mjs} +3 -3
- package/.output/server/_ssr/{index-5RImHKfu.mjs → index-CZIKZU43.mjs} +2 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{json-viewer-aJhb93ZK.mjs → json-viewer-d4obyRaA.mjs} +3 -3
- package/.output/server/_ssr/{router-Dgkv5nKP.mjs → router-DGPt3MUc.mjs} +145 -71
- package/.output/server/_tanstack-start-manifest_v-BzH4pNaI.mjs +4 -0
- package/.output/server/index.mjs +64 -64
- package/package.json +1 -1
- package/src/components/OnboardingBanner.tsx +11 -19
- package/src/components/ProxyViewer.tsx +1 -1
- package/src/components/providers/ProviderCard.tsx +6 -20
- package/src/components/providers/SettingsDialog.tsx +95 -2
- package/src/components/proxy-viewer/AgentTraceSummary.tsx +639 -38
- package/src/components/proxy-viewer/CompareDrawer.tsx +4 -2
- package/src/components/proxy-viewer/LogEntry.tsx +4 -4
- package/src/components/proxy-viewer/LogEntryHeader.tsx +15 -25
- package/src/components/proxy-viewer/ReplayDialog.tsx +190 -8
- package/src/components/proxy-viewer/ResponseView.tsx +2 -2
- package/src/components/proxy-viewer/ToolTraceEvents.tsx +37 -16
- package/src/components/proxy-viewer/TurnGroup.tsx +14 -2
- package/src/components/proxy-viewer/anatomy/RequestAnatomy.tsx +196 -45
- package/src/components/proxy-viewer/anatomy/SegmentBar.tsx +92 -67
- package/src/components/proxy-viewer/anatomy/types.ts +15 -13
- package/src/components/proxy-viewer/formats/anthropic/ResponseView.tsx +2 -2
- package/src/components/proxy-viewer/log-formats/anthropic.ts +1 -1
- package/src/components/proxy-viewer/log-formats/openai.ts +1 -1
- package/src/components/proxy-viewer/log-formats/types.ts +1 -1
- package/src/components/proxy-viewer/replayComparison.ts +131 -0
- package/src/components/proxy-viewer/useKeyboardNavigation.ts +64 -22
- package/src/components/proxy-viewer/viewerState.ts +14 -2
- package/src/components/ui/json-viewer.tsx +1 -1
- package/src/knowledge/candidateStore.ts +32 -1
- package/src/routes/api/knowledge.candidates.$candidateId.ts +50 -0
- package/src/routes/api/knowledge.sessions.$sessionId.candidates.ts +12 -2
- package/.output/public/assets/ProxyViewerContainer-D85_UANk.js +0 -101
- package/.output/public/assets/ReplayDialog-DTeaHHit.js +0 -1
- package/.output/public/assets/RequestAnatomy-DZ8grAih.js +0 -1
- package/.output/public/assets/ResponseView-Cldm6RCi.js +0 -1
- package/.output/public/assets/StreamingChunkSequence-3x4p-yT7.js +0 -1
- package/.output/public/assets/_sessionId-YqWFBu6d.js +0 -1
- package/.output/public/assets/index-BIw2H6jO.js +0 -1
- 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
|
|
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
|
|
23
|
-
*
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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-
|
|
67
|
-
<div className="flex items-
|
|
68
|
-
<
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
|
82
|
-
aria-label="Token estimate
|
|
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
|
-
|
|
89
|
-
|
|
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
|
|
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 -
|
|
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
|
-
*
|
|
48
|
-
*
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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}
|
|
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
|
-
|
|
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}
|
|
174
|
+
{overflowStartIndex}-{overflowEndIndex})
|
|
153
175
|
</TooltipContent>
|
|
154
176
|
</Tooltip>
|
|
155
177
|
)}
|
|
156
178
|
</div>
|
|
157
179
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
165
|
-
|
|
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/
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
8
|
-
* - `tools`: the synthetic
|
|
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
|
|
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
|
|
18
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
*/
|