@tonyclaw/agent-inspector 2.0.5 → 2.0.7
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-BhRYPp6T.js +1 -0
- package/.output/public/assets/ProxyViewerContainer-BHq0dkQG.js +114 -0
- package/.output/public/assets/ReplayDialog-Ch0gwg6B.js +1 -0
- package/.output/public/assets/RequestAnatomy-CKbvIgaN.js +1 -0
- package/.output/public/assets/ResponseView-hRLar32x.js +1 -0
- package/.output/public/assets/StreamingChunkSequence-DCJ_gUHa.js +1 -0
- package/.output/public/assets/_sessionId-BJTz6t5l.js +1 -0
- package/.output/public/assets/index-Cf9U0Ekq.js +1 -0
- package/.output/public/assets/index-DTdv7nzl.css +1 -0
- package/.output/public/assets/{main-2NlGzgOe.js → main-BlDtK0Nn.js} +2 -2
- package/.output/server/_libs/lucide-react.mjs +170 -108
- package/.output/server/_libs/radix-ui__react-collapsible.mjs +2 -2
- package/.output/server/{_sessionId-DWCTasJU.mjs → _sessionId-Dy7rsLS4.mjs} +58 -5
- package/.output/server/_ssr/{CompareDrawer-DhrN1uC2.mjs → CompareDrawer-CkVX-ARV.mjs} +9 -8
- package/.output/server/_ssr/{ProxyViewerContainer-DRl51s_n.mjs → ProxyViewerContainer-B5a9p3j5.mjs} +856 -32
- package/.output/server/_ssr/{ReplayDialog-BQT_ygxC.mjs → ReplayDialog-hXY6H9fB.mjs} +7 -8
- package/.output/server/_ssr/RequestAnatomy-B6MS7VvA.mjs +576 -0
- package/.output/server/_ssr/{ResponseView-e0kL2C3x.mjs → ResponseView-ByjxwUnp.mjs} +5 -74
- package/.output/server/_ssr/{StreamingChunkSequence-BJG-m7xs.mjs → StreamingChunkSequence-C5Kb8siT.mjs} +8 -7
- package/.output/server/_ssr/{index-Dea3OeRw.mjs → index-BZXtj42m.mjs} +58 -5
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{router-Dl7oh0zx.mjs → router-CVQaVF_3.mjs} +5 -5
- package/.output/server/{_tanstack-start-manifest_v-m-FJNBVf.mjs → _tanstack-start-manifest_v-Bpe5ZC56.mjs} +1 -1
- package/.output/server/index.mjs +58 -65
- package/package.json +1 -1
- package/src/components/proxy-viewer/LogEntry.tsx +18 -4
- package/src/components/proxy-viewer/LogEntryHeader.tsx +3 -3
- package/src/components/proxy-viewer/RequestToolsPanel.tsx +292 -0
- 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/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/requestTools.ts +213 -0
- package/src/components/ui/json-viewer.tsx +1 -1
- package/.output/public/assets/CompareDrawer-3nRwtk8J.js +0 -1
- package/.output/public/assets/ProxyViewerContainer-CbW5VRER.js +0 -101
- package/.output/public/assets/ReplayDialog-Cl62N9PI.js +0 -1
- package/.output/public/assets/RequestAnatomy-DgQWGvjs.js +0 -1
- package/.output/public/assets/ResponseView-Cvc-ct4E.js +0 -1
- package/.output/public/assets/StreamingChunkSequence-BCQaCAIe.js +0 -1
- package/.output/public/assets/_sessionId-CcD_aLGq.js +0 -1
- package/.output/public/assets/index-B_dffD3u.js +0 -1
- package/.output/public/assets/index-CX796gvi.css +0 -1
- package/.output/public/assets/json-viewer-IXejqXB0.js +0 -14
- package/.output/server/_ssr/RequestAnatomy-DS2tZOgq.mjs +0 -353
- package/.output/server/_ssr/json-viewer-DDU55MLK.mjs +0 -515
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Blocks,
|
|
3
|
+
Check,
|
|
4
|
+
ChevronDown,
|
|
5
|
+
ChevronRight,
|
|
6
|
+
Code2,
|
|
7
|
+
Copy,
|
|
8
|
+
FileText,
|
|
9
|
+
Globe,
|
|
10
|
+
MoreHorizontal,
|
|
11
|
+
MousePointerClick,
|
|
12
|
+
Search,
|
|
13
|
+
Terminal,
|
|
14
|
+
Wrench,
|
|
15
|
+
} from "lucide-react";
|
|
16
|
+
import { memo, useMemo, useState, type JSX } from "react";
|
|
17
|
+
import { cn } from "../../lib/utils";
|
|
18
|
+
import { Badge } from "../ui/badge";
|
|
19
|
+
import { Button } from "../ui/button";
|
|
20
|
+
import { Collapsible, CollapsibleContent } from "../ui/collapsible";
|
|
21
|
+
import { JsonViewer } from "../ui/json-viewer";
|
|
22
|
+
import { safeJsonValue } from "../ui/json-viewer-bulk";
|
|
23
|
+
import { ScrollArea } from "../ui/scroll-area";
|
|
24
|
+
import type {
|
|
25
|
+
RequestToolCategory,
|
|
26
|
+
RequestToolDefinition,
|
|
27
|
+
RequestToolsSummary,
|
|
28
|
+
} from "./requestTools";
|
|
29
|
+
import { useCopyFeedback } from "./useCopyFeedback";
|
|
30
|
+
|
|
31
|
+
type RequestToolsProps = {
|
|
32
|
+
summary: RequestToolsSummary | null;
|
|
33
|
+
calledToolNames: readonly string[];
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const CATEGORY_LABELS: Record<RequestToolCategory, string> = {
|
|
37
|
+
file: "file",
|
|
38
|
+
shell: "shell",
|
|
39
|
+
browser: "browser",
|
|
40
|
+
web: "web",
|
|
41
|
+
mcp: "MCP",
|
|
42
|
+
code: "code",
|
|
43
|
+
other: "other",
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const CATEGORY_CLASSES: Record<RequestToolCategory, string> = {
|
|
47
|
+
file: "border-blue-400/20 bg-blue-400/5 text-blue-300",
|
|
48
|
+
shell: "border-teal-400/20 bg-teal-400/5 text-teal-300",
|
|
49
|
+
browser: "border-cyan-400/20 bg-cyan-400/5 text-cyan-300",
|
|
50
|
+
web: "border-sky-400/20 bg-sky-400/5 text-sky-300",
|
|
51
|
+
mcp: "border-violet-400/20 bg-violet-400/5 text-violet-300",
|
|
52
|
+
code: "border-emerald-400/20 bg-emerald-400/5 text-emerald-300",
|
|
53
|
+
other: "border-muted-foreground/20 bg-muted/30 text-muted-foreground",
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
function CategoryIcon({ category }: { category: RequestToolCategory }): JSX.Element {
|
|
57
|
+
switch (category) {
|
|
58
|
+
case "file":
|
|
59
|
+
return <FileText className="size-3.5" />;
|
|
60
|
+
case "shell":
|
|
61
|
+
return <Terminal className="size-3.5" />;
|
|
62
|
+
case "browser":
|
|
63
|
+
return <MousePointerClick className="size-3.5" />;
|
|
64
|
+
case "web":
|
|
65
|
+
return <Globe className="size-3.5" />;
|
|
66
|
+
case "mcp":
|
|
67
|
+
return <Blocks className="size-3.5" />;
|
|
68
|
+
case "code":
|
|
69
|
+
return <Code2 className="size-3.5" />;
|
|
70
|
+
case "other":
|
|
71
|
+
return <MoreHorizontal className="size-3.5" />;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function RequiredParameters({ names }: { names: readonly string[] }): JSX.Element | null {
|
|
76
|
+
if (names.length === 0) return null;
|
|
77
|
+
return (
|
|
78
|
+
<div className="flex min-w-0 flex-wrap items-center gap-1">
|
|
79
|
+
<span className="text-[10px] uppercase tracking-wide text-muted-foreground">required</span>
|
|
80
|
+
{names.slice(0, 4).map((name) => (
|
|
81
|
+
<Badge
|
|
82
|
+
key={name}
|
|
83
|
+
variant="outline"
|
|
84
|
+
className="h-5 max-w-36 truncate rounded-md px-1.5 py-0 font-mono text-[10px]"
|
|
85
|
+
>
|
|
86
|
+
{name}
|
|
87
|
+
</Badge>
|
|
88
|
+
))}
|
|
89
|
+
{names.length > 4 && (
|
|
90
|
+
<span className="font-mono text-[10px] text-muted-foreground">+{names.length - 4}</span>
|
|
91
|
+
)}
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function ToolRow({ tool, called }: { tool: RequestToolDefinition; called: boolean }): JSX.Element {
|
|
97
|
+
const [open, setOpen] = useState(false);
|
|
98
|
+
const nameCopy = useCopyFeedback(tool.name);
|
|
99
|
+
const hasDetails =
|
|
100
|
+
tool.description !== null || tool.requiredParameters.length > 0 || tool.schema !== null;
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<Collapsible open={open} onOpenChange={setOpen}>
|
|
104
|
+
<div className="rounded-md border border-border/60 bg-background/40">
|
|
105
|
+
<div className="flex min-w-0 items-center gap-2 px-2.5 py-2">
|
|
106
|
+
<button
|
|
107
|
+
type="button"
|
|
108
|
+
className="flex min-w-0 flex-1 items-center gap-2 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
109
|
+
onClick={() => setOpen((value) => !value)}
|
|
110
|
+
aria-expanded={open}
|
|
111
|
+
disabled={!hasDetails}
|
|
112
|
+
>
|
|
113
|
+
<CategoryIcon category={tool.category} />
|
|
114
|
+
<span className="min-w-0 truncate font-mono text-xs font-semibold text-foreground/90">
|
|
115
|
+
{tool.name}
|
|
116
|
+
</span>
|
|
117
|
+
{called && (
|
|
118
|
+
<Badge
|
|
119
|
+
variant="outline"
|
|
120
|
+
className="h-5 rounded-md border-emerald-400/20 bg-emerald-400/5 px-1.5 py-0 text-[10px] text-emerald-300"
|
|
121
|
+
>
|
|
122
|
+
called
|
|
123
|
+
</Badge>
|
|
124
|
+
)}
|
|
125
|
+
<span className="font-mono text-[10px] text-muted-foreground">
|
|
126
|
+
{tool.parameterCount} params
|
|
127
|
+
</span>
|
|
128
|
+
{tool.descriptionPreview !== null && (
|
|
129
|
+
<span className="hidden min-w-0 truncate text-xs text-muted-foreground md:inline">
|
|
130
|
+
{tool.descriptionPreview}
|
|
131
|
+
</span>
|
|
132
|
+
)}
|
|
133
|
+
<span className="flex-1" />
|
|
134
|
+
{hasDetails &&
|
|
135
|
+
(open ? (
|
|
136
|
+
<ChevronDown className="size-3.5 shrink-0 text-muted-foreground" />
|
|
137
|
+
) : (
|
|
138
|
+
<ChevronRight className="size-3.5 shrink-0 text-muted-foreground" />
|
|
139
|
+
))}
|
|
140
|
+
</button>
|
|
141
|
+
<button
|
|
142
|
+
type="button"
|
|
143
|
+
className="inline-flex size-7 shrink-0 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
144
|
+
onClick={nameCopy.copy}
|
|
145
|
+
aria-label={nameCopy.copied ? "Copied tool name" : "Copy tool name"}
|
|
146
|
+
>
|
|
147
|
+
{nameCopy.copied ? (
|
|
148
|
+
<Check className="size-3.5 text-emerald-400" />
|
|
149
|
+
) : (
|
|
150
|
+
<Copy className="size-3.5" />
|
|
151
|
+
)}
|
|
152
|
+
</button>
|
|
153
|
+
</div>
|
|
154
|
+
<CollapsibleContent>
|
|
155
|
+
<div className="space-y-2 border-t border-border/60 px-2.5 py-2">
|
|
156
|
+
{tool.description !== null && (
|
|
157
|
+
<p className="text-xs leading-relaxed text-muted-foreground">{tool.description}</p>
|
|
158
|
+
)}
|
|
159
|
+
<RequiredParameters names={tool.requiredParameters} />
|
|
160
|
+
{tool.schema !== null && (
|
|
161
|
+
<ScrollArea className="max-h-64 rounded-md border border-border/60 bg-muted/20 p-2">
|
|
162
|
+
<JsonViewer data={safeJsonValue(tool.schema)} defaultExpandDepth={0} />
|
|
163
|
+
</ScrollArea>
|
|
164
|
+
)}
|
|
165
|
+
</div>
|
|
166
|
+
</CollapsibleContent>
|
|
167
|
+
</div>
|
|
168
|
+
</Collapsible>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function matchesQuery(tool: RequestToolDefinition, query: string): boolean {
|
|
173
|
+
if (query.length === 0) return true;
|
|
174
|
+
const haystack = `${tool.name} ${tool.description ?? ""} ${CATEGORY_LABELS[tool.category]}`
|
|
175
|
+
.toLowerCase()
|
|
176
|
+
.trim();
|
|
177
|
+
return haystack.includes(query);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export const RequestTools = memo(function RequestTools({
|
|
181
|
+
summary,
|
|
182
|
+
calledToolNames,
|
|
183
|
+
}: RequestToolsProps): JSX.Element | null {
|
|
184
|
+
const [expanded, setExpanded] = useState(false);
|
|
185
|
+
const [query, setQuery] = useState("");
|
|
186
|
+
const calledNames = useMemo(() => new Set(calledToolNames), [calledToolNames]);
|
|
187
|
+
const normalizedQuery = query.trim().toLowerCase();
|
|
188
|
+
const visibleTools = useMemo(
|
|
189
|
+
() =>
|
|
190
|
+
summary === null ? [] : summary.tools.filter((tool) => matchesQuery(tool, normalizedQuery)),
|
|
191
|
+
[normalizedQuery, summary],
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
if (summary === null) return null;
|
|
195
|
+
|
|
196
|
+
const previewTools = summary.tools.slice(0, 6);
|
|
197
|
+
const hiddenPreviewCount = summary.tools.length - previewTools.length;
|
|
198
|
+
|
|
199
|
+
return (
|
|
200
|
+
<section className="mb-3 rounded-lg border border-border/70 bg-muted/20">
|
|
201
|
+
<div className="flex min-w-0 flex-wrap items-center gap-2 px-3 py-2">
|
|
202
|
+
<div className="flex min-w-0 flex-1 items-center gap-2">
|
|
203
|
+
<Wrench className="size-4 shrink-0 text-sky-300/80" />
|
|
204
|
+
<div className="min-w-0">
|
|
205
|
+
<div className="flex min-w-0 flex-wrap items-center gap-1.5">
|
|
206
|
+
<span className="text-sm font-semibold text-foreground">Request Tools</span>
|
|
207
|
+
<Badge variant="outline" className="h-5 rounded-md px-1.5 py-0 font-mono text-[10px]">
|
|
208
|
+
{summary.tools.length}
|
|
209
|
+
</Badge>
|
|
210
|
+
{summary.toolChoiceLabel !== null && (
|
|
211
|
+
<Badge
|
|
212
|
+
variant="outline"
|
|
213
|
+
className="h-5 rounded-md px-1.5 py-0 font-mono text-[10px] text-muted-foreground"
|
|
214
|
+
>
|
|
215
|
+
tool_choice: {summary.toolChoiceLabel}
|
|
216
|
+
</Badge>
|
|
217
|
+
)}
|
|
218
|
+
</div>
|
|
219
|
+
<div className="mt-1 flex min-w-0 flex-wrap gap-1">
|
|
220
|
+
{summary.categories.map((category) => (
|
|
221
|
+
<Badge
|
|
222
|
+
key={category.category}
|
|
223
|
+
variant="outline"
|
|
224
|
+
className={cn(
|
|
225
|
+
"h-5 rounded-md px-1.5 py-0 font-mono text-[10px]",
|
|
226
|
+
CATEGORY_CLASSES[category.category],
|
|
227
|
+
)}
|
|
228
|
+
>
|
|
229
|
+
{category.count} {CATEGORY_LABELS[category.category]}
|
|
230
|
+
</Badge>
|
|
231
|
+
))}
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
<Button
|
|
236
|
+
type="button"
|
|
237
|
+
variant="ghost"
|
|
238
|
+
size="sm"
|
|
239
|
+
className="h-7 px-2"
|
|
240
|
+
onClick={() => setExpanded((value) => !value)}
|
|
241
|
+
aria-expanded={expanded}
|
|
242
|
+
>
|
|
243
|
+
{expanded ? <ChevronDown className="size-3.5" /> : <ChevronRight className="size-3.5" />}
|
|
244
|
+
{expanded ? "Hide" : "Show"}
|
|
245
|
+
</Button>
|
|
246
|
+
</div>
|
|
247
|
+
|
|
248
|
+
<div className="flex min-w-0 flex-wrap gap-1.5 border-t border-border/60 px-3 py-2">
|
|
249
|
+
{previewTools.map((tool) => (
|
|
250
|
+
<Badge
|
|
251
|
+
key={tool.name}
|
|
252
|
+
variant="outline"
|
|
253
|
+
className={cn(
|
|
254
|
+
"h-5 max-w-44 truncate rounded-md px-1.5 py-0 font-mono text-[10px]",
|
|
255
|
+
calledNames.has(tool.name) && "border-emerald-400/20 text-emerald-300",
|
|
256
|
+
)}
|
|
257
|
+
>
|
|
258
|
+
{tool.name}
|
|
259
|
+
</Badge>
|
|
260
|
+
))}
|
|
261
|
+
{hiddenPreviewCount > 0 && (
|
|
262
|
+
<span className="font-mono text-[10px] leading-5 text-muted-foreground">
|
|
263
|
+
+{hiddenPreviewCount}
|
|
264
|
+
</span>
|
|
265
|
+
)}
|
|
266
|
+
</div>
|
|
267
|
+
|
|
268
|
+
{expanded && (
|
|
269
|
+
<div className="space-y-2 border-t border-border/60 px-3 py-3">
|
|
270
|
+
<label className="flex h-8 items-center gap-2 rounded-md border border-border/70 bg-background/50 px-2 text-xs">
|
|
271
|
+
<Search className="size-3.5 shrink-0 text-muted-foreground" />
|
|
272
|
+
<input
|
|
273
|
+
value={query}
|
|
274
|
+
onChange={(event) => setQuery(event.target.value)}
|
|
275
|
+
placeholder="Filter tools"
|
|
276
|
+
className="min-w-0 flex-1 bg-transparent font-mono text-xs outline-none placeholder:text-muted-foreground/70"
|
|
277
|
+
/>
|
|
278
|
+
</label>
|
|
279
|
+
<div className="grid gap-1.5">
|
|
280
|
+
{visibleTools.length > 0 ? (
|
|
281
|
+
visibleTools.map((tool) => (
|
|
282
|
+
<ToolRow key={tool.name} tool={tool} called={calledNames.has(tool.name)} />
|
|
283
|
+
))
|
|
284
|
+
) : (
|
|
285
|
+
<p className="py-2 text-xs text-muted-foreground">No matching tools</p>
|
|
286
|
+
)}
|
|
287
|
+
</div>
|
|
288
|
+
</div>
|
|
289
|
+
)}
|
|
290
|
+
</section>
|
|
291
|
+
);
|
|
292
|
+
});
|
|
@@ -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
|
);
|