@tonyclaw/agent-inspector 2.0.6 → 2.0.8
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-Djb4XFZS.js +1 -0
- package/.output/public/assets/ProxyViewerContainer-CWdkPZsJ.js +114 -0
- package/.output/public/assets/ReplayDialog-DBPMIs2I.js +1 -0
- package/.output/public/assets/RequestAnatomy-FBfRNQN7.js +1 -0
- package/.output/public/assets/ResponseView-Db4pJL7U.js +1 -0
- package/.output/public/assets/StreamingChunkSequence-BH175O6R.js +1 -0
- package/.output/public/assets/_sessionId-3ipVoQtW.js +1 -0
- package/.output/public/assets/index-Bo1dGS4R.css +1 -0
- package/.output/public/assets/index-CJgaCJo8.js +1 -0
- package/.output/public/assets/{main-CKnTJ4-O.js → main-CwhqfJqM.js} +8 -8
- package/.output/server/_libs/lucide-react.mjs +170 -108
- package/.output/server/_libs/radix-ui__react-collapsible.mjs +2 -2
- package/.output/server/{_sessionId-B-x9fRY3.mjs → _sessionId-DzBy9gLa.mjs} +58 -5
- package/.output/server/_ssr/{CompareDrawer-BQVNsAY2.mjs → CompareDrawer-C5NBvE2e.mjs} +9 -8
- package/.output/server/_ssr/{ProxyViewerContainer-CYm2Dw19.mjs → ProxyViewerContainer-DiZjOTQT.mjs} +857 -32
- package/.output/server/_ssr/{ReplayDialog-CaMQBc79.mjs → ReplayDialog-EpvTwFvp.mjs} +7 -8
- package/.output/server/_ssr/{RequestAnatomy--P5arRH2.mjs → RequestAnatomy-DaQjhixp.mjs} +512 -7
- package/.output/server/_ssr/{ResponseView-RtFwNvgD.mjs → ResponseView-D8tGHCGY.mjs} +5 -74
- package/.output/server/_ssr/{StreamingChunkSequence-B5HPkzab.mjs → StreamingChunkSequence-DGQnb1QE.mjs} +8 -7
- package/.output/server/_ssr/{index-CZIKZU43.mjs → index-N25hqa7t.mjs} +58 -5
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{router-DGPt3MUc.mjs → router-Bf0m6F0G.mjs} +22 -6
- package/.output/server/{_tanstack-start-manifest_v-BzH4pNaI.mjs → _tanstack-start-manifest_v-DlAyJ5DB.mjs} +1 -1
- package/.output/server/index.mjs +61 -68
- package/package.json +1 -1
- package/src/components/proxy-viewer/LogEntry.tsx +17 -2
- package/src/components/proxy-viewer/RequestToolsPanel.tsx +292 -0
- package/src/components/proxy-viewer/anatomy/RequestAnatomy.tsx +197 -1
- package/src/components/proxy-viewer/anatomy/contextIntelligence.ts +414 -0
- package/src/components/proxy-viewer/requestTools.ts +213 -0
- package/src/routes/__root.tsx +27 -0
- package/.output/public/assets/CompareDrawer-DDmqSAfl.js +0 -1
- package/.output/public/assets/ProxyViewerContainer-Cxpdziwd.js +0 -101
- package/.output/public/assets/ReplayDialog-Bt5DGzlh.js +0 -1
- package/.output/public/assets/RequestAnatomy-BxX3_N9S.js +0 -1
- package/.output/public/assets/ResponseView-Bl_5S9gZ.js +0 -1
- package/.output/public/assets/StreamingChunkSequence-RJMwNf6F.js +0 -1
- package/.output/public/assets/_sessionId-b4isaoDp.js +0 -1
- package/.output/public/assets/index-BZ4x5UI6.js +0 -1
- package/.output/public/assets/index-C624DUk9.css +0 -1
- package/.output/public/assets/json-viewer-CRL_gWEZ.js +0 -14
- package/.output/server/_ssr/json-viewer-d4obyRaA.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
|
+
});
|
|
@@ -2,6 +2,13 @@ import { Info } from "lucide-react";
|
|
|
2
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 {
|
|
6
|
+
analyzeContextIntelligence,
|
|
7
|
+
CONTEXT_USAGE_THRESHOLDS,
|
|
8
|
+
type ContextDiagnostic,
|
|
9
|
+
type ContextIntelligence,
|
|
10
|
+
type ContextRiskLevel,
|
|
11
|
+
} from "./contextIntelligence";
|
|
5
12
|
import { ROLE_COLOR_CLASSES, SegmentBar } from "./SegmentBar";
|
|
6
13
|
import { ANATOMY_ROLE_LABELS, type AnatomyRole, type AnatomySegment } from "./types";
|
|
7
14
|
|
|
@@ -10,6 +17,8 @@ export type RequestAnatomyProps = {
|
|
|
10
17
|
parsed: unknown | null;
|
|
11
18
|
/** Server-reported input token count, or `null` if unknown. */
|
|
12
19
|
inputTokens: number | null;
|
|
20
|
+
/** Model name from the captured log; falls back to the parsed request body when omitted. */
|
|
21
|
+
model?: string | null;
|
|
13
22
|
/** Optional callback fired when the user activates a concrete request segment. */
|
|
14
23
|
onSegmentActivate?: (segment: AnatomySegment) => void;
|
|
15
24
|
/** Pre-computed segments; if provided, `parsed` is ignored. */
|
|
@@ -35,6 +44,20 @@ const VIEW_MODE_OPTIONS: Array<{ value: ContextViewMode; label: string }> = [
|
|
|
35
44
|
{ value: "segment", label: "By Segment" },
|
|
36
45
|
];
|
|
37
46
|
|
|
47
|
+
const RISK_CLASS: Record<ContextRiskLevel, string> = {
|
|
48
|
+
unknown: "bg-muted-foreground/50",
|
|
49
|
+
ok: "bg-emerald-500/70",
|
|
50
|
+
watch: "bg-amber-400/75",
|
|
51
|
+
danger: "bg-red-500/80",
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const RISK_TEXT_CLASS: Record<ContextRiskLevel, string> = {
|
|
55
|
+
unknown: "text-muted-foreground",
|
|
56
|
+
ok: "text-emerald-400",
|
|
57
|
+
watch: "text-amber-300",
|
|
58
|
+
danger: "text-red-300",
|
|
59
|
+
};
|
|
60
|
+
|
|
38
61
|
function formatPercent(value: number): string {
|
|
39
62
|
if (value >= 10) return `${value.toFixed(0)}%`;
|
|
40
63
|
if (value >= 1) return `${value.toFixed(1)}%`;
|
|
@@ -42,6 +65,157 @@ function formatPercent(value: number): string {
|
|
|
42
65
|
return "0%";
|
|
43
66
|
}
|
|
44
67
|
|
|
68
|
+
function formatUsagePercent(value: number | null): string {
|
|
69
|
+
if (value === null) return "Unknown";
|
|
70
|
+
return formatPercent(value * 100);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function formatRemainingTokens(value: number | null): string {
|
|
74
|
+
if (value === null) return "Unknown";
|
|
75
|
+
if (value < 0) return `-${formatTokens(Math.abs(value))}`;
|
|
76
|
+
return formatTokens(value);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function formatContextLimit(intelligence: ContextIntelligence): string {
|
|
80
|
+
if (intelligence.contextWindow.tokens === null) return "Unknown";
|
|
81
|
+
return formatTokens(intelligence.contextWindow.tokens);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function formatContextSource(intelligence: ContextIntelligence): string {
|
|
85
|
+
switch (intelligence.contextWindow.source) {
|
|
86
|
+
case "request":
|
|
87
|
+
return intelligence.contextWindow.label;
|
|
88
|
+
case "model-rule":
|
|
89
|
+
return intelligence.contextWindow.label;
|
|
90
|
+
case "unknown":
|
|
91
|
+
return intelligence.model === null
|
|
92
|
+
? intelligence.contextWindow.label
|
|
93
|
+
: `${intelligence.contextWindow.label}: ${intelligence.model}`;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function diagnosticTone(diagnostic: ContextDiagnostic): string {
|
|
98
|
+
switch (diagnostic.severity) {
|
|
99
|
+
case "watch":
|
|
100
|
+
return "border-amber-400/30 bg-amber-400/8 text-amber-100";
|
|
101
|
+
case "danger":
|
|
102
|
+
return "border-red-400/35 bg-red-500/10 text-red-100";
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function ContextIntelligenceStrip({
|
|
107
|
+
intelligence,
|
|
108
|
+
}: {
|
|
109
|
+
intelligence: ContextIntelligence;
|
|
110
|
+
}): JSX.Element {
|
|
111
|
+
const usageWidth =
|
|
112
|
+
intelligence.usagePercent === null ? 0 : Math.max(1, intelligence.usagePercent * 100);
|
|
113
|
+
const outputReserveLabel =
|
|
114
|
+
intelligence.outputReserveTokens === null
|
|
115
|
+
? "No output reserve"
|
|
116
|
+
: `Output reserve ${formatTokens(intelligence.outputReserveTokens)}`;
|
|
117
|
+
const largestRole = intelligence.largestRole;
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<div className="space-y-2">
|
|
121
|
+
<div className="grid gap-x-4 gap-y-2 border-y border-border/60 py-2 text-xs sm:grid-cols-4">
|
|
122
|
+
<div className="min-w-0">
|
|
123
|
+
<div className="text-[10px] font-medium uppercase text-muted-foreground">Window</div>
|
|
124
|
+
<div className="mt-0.5 truncate font-mono text-foreground">
|
|
125
|
+
{formatContextLimit(intelligence)}
|
|
126
|
+
</div>
|
|
127
|
+
<div className="truncate text-[11px] text-muted-foreground">
|
|
128
|
+
{formatContextSource(intelligence)}
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<div className="min-w-0">
|
|
133
|
+
<div className="text-[10px] font-medium uppercase text-muted-foreground">Used</div>
|
|
134
|
+
<div className={cn("mt-0.5 font-mono", RISK_TEXT_CLASS[intelligence.riskLevel])}>
|
|
135
|
+
{formatUsagePercent(intelligence.usagePercent)}
|
|
136
|
+
</div>
|
|
137
|
+
<div className="truncate text-[11px] text-muted-foreground">{outputReserveLabel}</div>
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
<div className="min-w-0">
|
|
141
|
+
<div className="text-[10px] font-medium uppercase text-muted-foreground">Remaining</div>
|
|
142
|
+
<div className="mt-0.5 font-mono text-foreground">
|
|
143
|
+
{formatRemainingTokens(intelligence.remainingAfterReserveTokens)}
|
|
144
|
+
</div>
|
|
145
|
+
<div className="truncate text-[11px] text-muted-foreground">
|
|
146
|
+
Input only {formatRemainingTokens(intelligence.remainingInputTokens)}
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<div className="min-w-0">
|
|
151
|
+
<div className="text-[10px] font-medium uppercase text-muted-foreground">Largest</div>
|
|
152
|
+
{largestRole === null ? (
|
|
153
|
+
<div className="mt-0.5 text-muted-foreground">Unknown</div>
|
|
154
|
+
) : (
|
|
155
|
+
<>
|
|
156
|
+
<div className="mt-0.5 flex min-w-0 items-center gap-1.5">
|
|
157
|
+
<span
|
|
158
|
+
aria-hidden="true"
|
|
159
|
+
className={cn(
|
|
160
|
+
"size-2.5 shrink-0 rounded-[2px]",
|
|
161
|
+
ROLE_COLOR_CLASSES[largestRole.role],
|
|
162
|
+
)}
|
|
163
|
+
/>
|
|
164
|
+
<span className="truncate text-foreground">{largestRole.label}</span>
|
|
165
|
+
</div>
|
|
166
|
+
<div className="truncate font-mono text-[11px] text-muted-foreground">
|
|
167
|
+
{formatPercent(largestRole.percent * 100)} | ~{formatTokens(largestRole.tokens)}
|
|
168
|
+
</div>
|
|
169
|
+
</>
|
|
170
|
+
)}
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
{intelligence.contextWindow.tokens !== null && (
|
|
175
|
+
<div className="space-y-1.5">
|
|
176
|
+
<div className="relative h-2 overflow-hidden rounded-full bg-muted/40">
|
|
177
|
+
<div
|
|
178
|
+
className={cn("h-full rounded-full", RISK_CLASS[intelligence.riskLevel])}
|
|
179
|
+
style={{ width: `${String(usageWidth)}%` }}
|
|
180
|
+
/>
|
|
181
|
+
<div
|
|
182
|
+
aria-hidden="true"
|
|
183
|
+
className="absolute top-0 h-full w-px bg-amber-200/70"
|
|
184
|
+
style={{ left: `${String(CONTEXT_USAGE_THRESHOLDS.watch * 100)}%` }}
|
|
185
|
+
/>
|
|
186
|
+
<div
|
|
187
|
+
aria-hidden="true"
|
|
188
|
+
className="absolute top-0 h-full w-px bg-red-200/80"
|
|
189
|
+
style={{ left: `${String(CONTEXT_USAGE_THRESHOLDS.danger * 100)}%` }}
|
|
190
|
+
/>
|
|
191
|
+
</div>
|
|
192
|
+
<div className="flex justify-between text-[10px] text-muted-foreground">
|
|
193
|
+
<span>Context window</span>
|
|
194
|
+
<span>
|
|
195
|
+
Watch {formatUsagePercent(CONTEXT_USAGE_THRESHOLDS.watch)} / Danger{" "}
|
|
196
|
+
{formatUsagePercent(CONTEXT_USAGE_THRESHOLDS.danger)}
|
|
197
|
+
</span>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
)}
|
|
201
|
+
|
|
202
|
+
{intelligence.diagnostics.length > 0 && (
|
|
203
|
+
<div className="grid gap-1.5 sm:grid-cols-2">
|
|
204
|
+
{intelligence.diagnostics.map((diagnostic) => (
|
|
205
|
+
<div
|
|
206
|
+
key={`${diagnostic.kind}-${diagnostic.title}`}
|
|
207
|
+
className={cn("rounded border px-2 py-1.5 text-xs", diagnosticTone(diagnostic))}
|
|
208
|
+
>
|
|
209
|
+
<div className="font-medium">{diagnostic.title}</div>
|
|
210
|
+
<div className="mt-0.5 text-[11px] text-muted-foreground">{diagnostic.detail}</div>
|
|
211
|
+
</div>
|
|
212
|
+
))}
|
|
213
|
+
</div>
|
|
214
|
+
)}
|
|
215
|
+
</div>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
45
219
|
function aggregateByRole(segments: AnatomySegment[]): AnatomySegment[] {
|
|
46
220
|
const result: AnatomySegment[] = [];
|
|
47
221
|
for (const role of ROLE_ORDER) {
|
|
@@ -76,6 +250,7 @@ function topContributors(segments: AnatomySegment[]): AnatomySegment[] {
|
|
|
76
250
|
export function RequestAnatomy({
|
|
77
251
|
parsed,
|
|
78
252
|
inputTokens,
|
|
253
|
+
model = null,
|
|
79
254
|
onSegmentActivate,
|
|
80
255
|
segments: providedSegments,
|
|
81
256
|
}: RequestAnatomyProps): JSX.Element | null {
|
|
@@ -93,6 +268,18 @@ export function RequestAnatomy({
|
|
|
93
268
|
() => (segments === null ? [] : topContributors(segments)),
|
|
94
269
|
[segments],
|
|
95
270
|
);
|
|
271
|
+
const intelligence = useMemo(
|
|
272
|
+
() =>
|
|
273
|
+
segments === null
|
|
274
|
+
? null
|
|
275
|
+
: analyzeContextIntelligence({
|
|
276
|
+
segments,
|
|
277
|
+
inputTokens,
|
|
278
|
+
parsed,
|
|
279
|
+
model,
|
|
280
|
+
}),
|
|
281
|
+
[inputTokens, model, parsed, segments],
|
|
282
|
+
);
|
|
96
283
|
|
|
97
284
|
const showDivergenceWarning = useMemo(() => {
|
|
98
285
|
if (segments === null) return false;
|
|
@@ -115,6 +302,12 @@ export function RequestAnatomy({
|
|
|
115
302
|
const segmentCountLabel = `${String(segments.length)} segment${segments.length === 1 ? "" : "s"}`;
|
|
116
303
|
const providerInputLabel =
|
|
117
304
|
inputTokens === null ? "Provider input unknown" : `Provider input ${formatTokens(inputTokens)}`;
|
|
305
|
+
const contextLimitLabel =
|
|
306
|
+
intelligence === null ? "Limit unknown" : `Limit ${formatContextLimit(intelligence)}`;
|
|
307
|
+
const usageLabel =
|
|
308
|
+
intelligence === null
|
|
309
|
+
? "Usage unknown"
|
|
310
|
+
: `${formatUsagePercent(intelligence.usagePercent)} used`;
|
|
118
311
|
|
|
119
312
|
return (
|
|
120
313
|
<TooltipProvider delayDuration={150}>
|
|
@@ -123,7 +316,8 @@ export function RequestAnatomy({
|
|
|
123
316
|
<div className="min-w-0">
|
|
124
317
|
<div className="text-sm font-semibold text-foreground">Request Context</div>
|
|
125
318
|
<div className={cn("mt-0.5 text-xs font-mono tabular-nums", summaryColorClass)}>
|
|
126
|
-
Estimated ~{formatTokens(total)} tokens | {providerInputLabel} | {
|
|
319
|
+
Estimated ~{formatTokens(total)} tokens | {providerInputLabel} | {contextLimitLabel} |{" "}
|
|
320
|
+
{usageLabel} | {segmentCountLabel}
|
|
127
321
|
</div>
|
|
128
322
|
</div>
|
|
129
323
|
|
|
@@ -172,6 +366,8 @@ export function RequestAnatomy({
|
|
|
172
366
|
</div>
|
|
173
367
|
)}
|
|
174
368
|
|
|
369
|
+
{intelligence !== null && <ContextIntelligenceStrip intelligence={intelligence} />}
|
|
370
|
+
|
|
175
371
|
<SegmentBar
|
|
176
372
|
segments={displayedSegments}
|
|
177
373
|
totalTokens={displayedTotal}
|