@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.
Files changed (43) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/CompareDrawer-Djb4XFZS.js +1 -0
  3. package/.output/public/assets/ProxyViewerContainer-CWdkPZsJ.js +114 -0
  4. package/.output/public/assets/ReplayDialog-DBPMIs2I.js +1 -0
  5. package/.output/public/assets/RequestAnatomy-FBfRNQN7.js +1 -0
  6. package/.output/public/assets/ResponseView-Db4pJL7U.js +1 -0
  7. package/.output/public/assets/StreamingChunkSequence-BH175O6R.js +1 -0
  8. package/.output/public/assets/_sessionId-3ipVoQtW.js +1 -0
  9. package/.output/public/assets/index-Bo1dGS4R.css +1 -0
  10. package/.output/public/assets/index-CJgaCJo8.js +1 -0
  11. package/.output/public/assets/{main-CKnTJ4-O.js → main-CwhqfJqM.js} +8 -8
  12. package/.output/server/_libs/lucide-react.mjs +170 -108
  13. package/.output/server/_libs/radix-ui__react-collapsible.mjs +2 -2
  14. package/.output/server/{_sessionId-B-x9fRY3.mjs → _sessionId-DzBy9gLa.mjs} +58 -5
  15. package/.output/server/_ssr/{CompareDrawer-BQVNsAY2.mjs → CompareDrawer-C5NBvE2e.mjs} +9 -8
  16. package/.output/server/_ssr/{ProxyViewerContainer-CYm2Dw19.mjs → ProxyViewerContainer-DiZjOTQT.mjs} +857 -32
  17. package/.output/server/_ssr/{ReplayDialog-CaMQBc79.mjs → ReplayDialog-EpvTwFvp.mjs} +7 -8
  18. package/.output/server/_ssr/{RequestAnatomy--P5arRH2.mjs → RequestAnatomy-DaQjhixp.mjs} +512 -7
  19. package/.output/server/_ssr/{ResponseView-RtFwNvgD.mjs → ResponseView-D8tGHCGY.mjs} +5 -74
  20. package/.output/server/_ssr/{StreamingChunkSequence-B5HPkzab.mjs → StreamingChunkSequence-DGQnb1QE.mjs} +8 -7
  21. package/.output/server/_ssr/{index-CZIKZU43.mjs → index-N25hqa7t.mjs} +58 -5
  22. package/.output/server/_ssr/index.mjs +2 -2
  23. package/.output/server/_ssr/{router-DGPt3MUc.mjs → router-Bf0m6F0G.mjs} +22 -6
  24. package/.output/server/{_tanstack-start-manifest_v-BzH4pNaI.mjs → _tanstack-start-manifest_v-DlAyJ5DB.mjs} +1 -1
  25. package/.output/server/index.mjs +61 -68
  26. package/package.json +1 -1
  27. package/src/components/proxy-viewer/LogEntry.tsx +17 -2
  28. package/src/components/proxy-viewer/RequestToolsPanel.tsx +292 -0
  29. package/src/components/proxy-viewer/anatomy/RequestAnatomy.tsx +197 -1
  30. package/src/components/proxy-viewer/anatomy/contextIntelligence.ts +414 -0
  31. package/src/components/proxy-viewer/requestTools.ts +213 -0
  32. package/src/routes/__root.tsx +27 -0
  33. package/.output/public/assets/CompareDrawer-DDmqSAfl.js +0 -1
  34. package/.output/public/assets/ProxyViewerContainer-Cxpdziwd.js +0 -101
  35. package/.output/public/assets/ReplayDialog-Bt5DGzlh.js +0 -1
  36. package/.output/public/assets/RequestAnatomy-BxX3_N9S.js +0 -1
  37. package/.output/public/assets/ResponseView-Bl_5S9gZ.js +0 -1
  38. package/.output/public/assets/StreamingChunkSequence-RJMwNf6F.js +0 -1
  39. package/.output/public/assets/_sessionId-b4isaoDp.js +0 -1
  40. package/.output/public/assets/index-BZ4x5UI6.js +0 -1
  41. package/.output/public/assets/index-C624DUk9.css +0 -1
  42. package/.output/public/assets/json-viewer-CRL_gWEZ.js +0 -14
  43. 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} | {segmentCountLabel}
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}