Context Inspector
A stacked map of the assembled prompt — system, tools, files, memory and history as proportional slices with per-entry token costs, an expandable breakdown that never hides what it didn't itemise, a reply-headroom band that warns before the answer stops fitting, and a what-if preview of every evictable entry.
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/context-inspector.jsonPrompt
Build a React + TypeScript + Tailwind "ContextInspector" component with zod and
lucide-react. It READS an assembled LLM prompt: it never tokenises anything
itself and never edits the prompt — every number is supplied by the caller and
every mutation belongs to the consumer.
Contract
- A zod schema in a sibling contract file is the single source of truth.
Section: { id, kind, label, tokens, items?: Item[] } where kind is
"system" | "tools" | "files" | "memory" | "history" | "other".
Item: { id, label, tokens, evictable?, detail? }.
- section.tokens is what the whole section costs. `items` is an itemisation that
is allowed to be PARTIAL: a 300-turn history is a real section with a real
total, and listing 300 rows would be useless, so you list the last eight and
the difference is rendered as its own "not itemised" line. The panel must
never fold that difference away — an inspector that silently drops 74K tokens
is worse than no inspector.
- `evictable` is a claim about POLICY ("your assembler may drop this under
pressure"), not an action. The component renders the claim and can simulate
it; it never performs it.
- Envelope: status "loading" | "empty" | "error" | "ready", sections, limit,
reserveTokens?. status="error" means the tokenizer never reported back —
a different failure from the window being full, and it reads differently.
- Props: status; sections; limit; reserveTokens (0); reserveLabel ("Reply
headroom"); locale ("en-US"); label ("Context window"); defaultOpenIds?;
itemOrder ("largest-first" | "given"); maxItemsPerSection (6, clamped >= 1);
allowPreview (true); onRetry?; emptyState?; skeletonSections (4, clamped
1..8); className plus the remaining div props, ref forwarded to the root.
- Every figure that enters the arithmetic is sanitised on the way in: NaN,
Infinity and negatives all read as 0, so one bad row cannot make a slice
3000% wide or a total negative.
Behavior
- THE ARITHMETIC IS THE PRODUCT, and it is written on screen:
section total = max(declared tokens, sum of items)
remainder = declared − itemised (its own row, never hidden)
used = Σ section totals
free = limit − used
headroom = min(reserve, free)
shortfall = max(0, reserve − free)
over = max(0, used − limit)
The max() in the first line is deliberate: if the rows add up to MORE than the
declared total, one of the two numbers is wrong, and the safe error is the one
that makes the prompt look bigger. That section also says the two disagree.
- SCALE = max(limit, used), and it is pinned to the un-previewed total. Under
the limit the fill stops short of the right edge; over the limit the bar keeps
growing and the window edge (a marker line) moves LEFT, so the overflow stays
measurable instead of being clipped into invisibility. The edge line is drawn
whenever the scale is bigger than the window — including inside the preview,
where it is what shows the freed slice crossing back over the line.
- THE WHAT-IF PREVIEW is one switch: "Preview without evictable (22.4K)". While
it is on, each section renders total − its evictable tokens, the difference is
drawn as a hatched "would be freed" slice, and every row that would go is
struck through and re-badged "dropped". It is pure derived state — the input
array is never mutated, no callback fires, nothing is removed — and because
the scale stays pinned, the freed space appears as space instead of the whole
chart quietly re-normalising, which is the comparison the switch exists to
make. Sections also print "was 21.4K" next to the previewed figure.
The switch is not rendered at all when nothing is evictable: a control whose
only possible effect is zero is a control that lies.
- Toggling the preview announces the outcome through one always-mounted polite
live region, computed inside the handler from the plan's own figures, so it
describes the window the reader is ABOUT to see rather than the one they left.
- CROSS-HIGHLIGHT: hovering or FOCUSING a section row lights its slice and dims
the others to 25%; hovering a slice highlights its row. Focus is included on
purpose — a mouse-only coupling is a coupling half the users never get.
- The bar is a picture of the list, never a second control surface: slices carry
titles and highlight, but every action (expand, show more, preview) lives in
the list, so there is exactly one tab stop per section instead of two.
- DISCLOSURE is per section, keyed by SECTION ID, so re-rendering with new data
can never apply one section's open state to another. The panel is UNMOUNTED
when closed (a zero-height collapse leaves its buttons in the tab order — an
invisible keyboard trap), aria-controls is set only while open so it never
points at an id that is not in the document, and a section with no itemisation
is not a button at all — its total and its "not itemised" figure are already
on the row, so there is nothing to disclose. "Expand all" / "Collapse all"
drives the same state.
- TRUNCATION IS NEVER SILENT: each section draws maxItemsPerSection rows and
ends in "Show 19 more · 6.4K tokens", with a way back to the capped view.
- PERCENTAGES NEVER LIE BY ROUNDING: a slice that exists prints "<0.1%" instead
of "0%", and every non-zero slice keeps a 2px minimum width — a slice you
cannot see reads as a slice that is not there. Figures above 10,000 are
compact ("22.4K") with the exact grouped number always in the title attribute.
- FOUR STATES, four real branches: loading draws a skeleton shaped like what
replaces it plus an sr-only status; empty says the prompt has not been
assembled yet (not "0 tokens used"); error says the tokenizer never answered
and offers onRetry when given; ready is everything above.
- limit = 0 means "no window configured": no headroom band, no marker line, no
"% full" — shares become shares of what is there, and the panel still answers
"what is in this prompt".
- NO CLOCK, NO TIMERS, NO OBSERVERS. Everything is derived during render from
props plus four pieces of local UI state (open ids, uncapped ids, preview,
highlight), so the server, a screenshot and a replay produce the same pixels
and there is nothing to clean up on unmount.
Rendering & styling
- Semantic tokens only: bg-card / border / text-muted-foreground for chrome;
var(--chart-1..5) cycled BY POSITION for the slices (a kind-keyed palette
would paint two file sections the same colour); bg-muted for the track;
color-mix hatches over --muted-foreground (free space), --destructive (the
reply no longer fits) and --primary (would be freed); text-destructive with
bg-destructive/10 for the two notices; bg-primary for the switch. No hard
coded colours anywhere.
- Colour is the fast path, never the only path: every section also carries a
kind icon, a written percentage and an exact token figure, so the panel
survives a monochrome theme and colour-blind vision.
- The bar is role="img" with a one-sentence accessible label ("Context window:
69,990 of 128,000 tokens used across 5 sections, 58,010 free, 8,000 of 8,000
kept for the reply"). It is NOT a meter: a meter has one value, and this
picture has five. Nothing is lost, because every figure in it is also written
out in the list below.
- @container, not viewport breakpoints — the same panel is a 320px chat sidebar
in one app and a full-width debugging page in another.
- Long tool names and 119-character file paths use wrap-anywhere plus min-w-0,
NOT break-words: only overflow-wrap: anywhere lowers the min-content width,
which is what actually stops a path from widening the card.
- Every transition (slice width, chevron rotation, highlight, switch knob) is
motion-reduce guarded. The animation is decorative; the numbers are not.
- cn() merges the consumer's className into the root, which also spreads the
remaining props and forwards its ref.
Customization levers
- Vocabulary: `label` names the panel, `reserveLabel` renames the reply band
("Answer budget", "Streaming headroom"), and section `label`s are yours —
`kind` only picks the icon and the screen-reader noun, so map your own buckets
onto the six kinds instead of inventing a seventh.
- Budget: `limit` is the model's window and `reserveTokens` is how much of it
you refuse to spend on input. Raise the reserve for a model that writes long
answers; set it to 0 to drop the band entirely; set `limit` to 0 for a
composition-only view.
- Density: `maxItemsPerSection` decides how much of each section is drawn before
the explicit reveal (5–8 in a sidebar, 20+ on a debugging page), and
`defaultOpenIds` opens the sections your surface cares about on arrival.
- Order: `itemOrder="largest-first"` answers "who is eating my window";
`itemOrder="given"` preserves conversation order for a history section.
- Simulation: `allowPreview={false}` removes the what-if switch for a read-only
audit surface. If you want real eviction, keep the inspector read-only and put
the action next to it — the panel is the evidence, not the executioner.
- Chrome: `emptyState` replaces the zero-state block, `onRetry` owns the
tokenizer failure, `skeletonSections` matches the skeleton to your usual
number of sections, `locale` drives every number, `className` reaches the root.Concepts
- Prompt anatomy, not a usage gauge — the question is "what is inside this request", so the primary object is a composition: five or six named parts of one window, each with a share, a colour, an icon and a drill-down. A single used-versus-limit bar answers "how full" and stops there; this one answers "full of what", which is the only version of the question you can act on.
- Partial itemisation is the normal case — a section knows its total; it does not always know, or want to show, every row inside it. So the difference between the declared total and the listed rows becomes an explicit "not itemised" line rather than disappearing. The inverse mismatch (rows adding up to more than the total) is resolved in favour of the larger figure: under-reporting a prompt is the one error that gets someone stuck at send time.
- Reply headroom is space, not content — the tokens you keep free for the answer are never part of the sections, because they are the absence of input. They are drawn as a hatched band at the tail of the bar, and when the free space shrinks below the reserve the band switches to the alarm hatch and the shortfall is named in tokens — before the model truncates its own answer instead of you deciding what to drop.
- The what-if preview is derived, never destructive — one switch recomputes every section as "total minus its evictable entries", strikes the affected rows and draws the difference as a hatched slice. Nothing is removed, no callback fires, and the scale stays pinned to the real total, so the freed space shows up as space instead of the chart re-normalising and looking unchanged. Simulation and mutation live in different components on purpose.
- Cross-highlight makes the legend and the bar one object — hovering or focusing a section row lights its slice and dims the rest to 25%, and the coupling runs both ways. Focus is part of it, not an afterthought: a highlight that only exists under a mouse pointer is a highlight half your users never see.
- A slice you cannot see reads as a slice that is not there — so a section worth 0.05% of the window still keeps a 2px sliver and prints
<0.1%rather than0%, and a capped list always ends in "Show 19 more · 6.4K tokens" instead of quietly stopping. Every cap and every rounding on this panel states its own size.
Throughput Meter
A live tokens-per-second readout — rolling sparkline, time to first token, current-vs-average delta, a stall flag raised by the meter's own clock, and a run summary when the stream ends.
Guardrail Trace
The safety evaluation of one request, rule by rule — a verdict header derived from the rules rather than asserted beside them, matched-span excerpts with per-rule rationale, verdict filter chips, a passing-row budget that can never hide a block, and four data states.