ns-ui
Meter Context Window
A context-window budget meter: one hairline stacked bar segmenting the window into system prompt / tools / history / current turn, told apart by fill pattern instead of color, that visibly resettles rather than jumps when the window compacts.
Usage
registry/core/meter-context-window/demo.tsx"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import { BallastContext } from "./component";
const CAPACITY = 200_000;
const SYSTEM = 3_400;
// pause ambient growth here — a real agent would compact before running out
const AUTO_GROW_CEILING = 0.94;
type Ctx = { tools: number; history: number; turn: number };
// a mid-session baseline, not an empty specimen — the resting screenshot
// should already show the bar doing its job
const INITIAL: Ctx = { tools: 2_140, history: 96_800, turn: 1_260 };
function rand(min: number, max: number): number {
return min + Math.floor(Math.random() * (max - min));
}
function fmt(n: number): string {
return Math.round(n).toLocaleString("en-US");
}
export default function BallastContextDemo() {
const [ctx, setCtx] = useState<Ctx>(INITIAL);
const [status, setStatus] = useState("session resumed · 51.8% of window in use");
const timeoutsRef = useRef<number[]>([]);
useEffect(() => {
const tick = () => {
const amt = rand(900, 2000);
let bumped = false;
setCtx((prev) => {
const used = SYSTEM + prev.tools + prev.history + prev.turn;
if (used / CAPACITY >= AUTO_GROW_CEILING) return prev;
bumped = true;
return { ...prev, turn: prev.turn + amt };
});
if (bumped) {
setStatus(`assistant turn streaming · +${fmt(amt)} tok`);
const id = window.setTimeout(() => {
setCtx((prev) =>
prev.turn >= amt
? { ...prev, turn: prev.turn - amt, history: prev.history + amt }
: prev
);
setStatus(`turn folded into history · +${fmt(amt)} tok`);
}, 900);
timeoutsRef.current.push(id);
}
};
const intervalId = window.setInterval(tick, 2200);
return () => {
window.clearInterval(intervalId);
for (const id of timeoutsRef.current) window.clearTimeout(id);
timeoutsRef.current = [];
};
}, []);
const callTool = useCallback(() => {
const amt = rand(250, 700);
setCtx((prev) => ({ ...prev, tools: prev.tools + amt }));
setStatus(`tool call returned · +${fmt(amt)} tok`);
}, []);
const compact = useCallback(() => {
const before = ctx.history;
const after = Math.round(before * 0.12);
setCtx((prev) => ({ ...prev, history: after }));
setStatus(`context compacted · history ${fmt(before)} → ${fmt(after)} tok`);
}, [ctx.history]);
// Jump straight to the near-capacity band. The ambient interval reaches it
// on its own after a couple of minutes, but that is far past any screenshot
// — this makes the warning state (pulsing ring, bold readout + triangle,
// hazard hatch on the sliver of free space) reachable in one click, so the
// gate can capture and prove it. Usage lands ~96%, above the auto-grow
// ceiling, so it holds until COMPACT or RESET.
const fillWindow = useCallback(() => {
setCtx({ tools: 4_200, history: 179_600, turn: 4_800 });
setStatus("near capacity · compact before the window fills");
}, []);
const reset = useCallback(() => {
setCtx(INITIAL);
setStatus("session reset · baseline restored");
}, []);
const used = SYSTEM + ctx.tools + ctx.history + ctx.turn;
const pctUsed = Math.round((used / CAPACITY) * 100);
const buttonClass =
"cursor-pointer rounded-sm border border-border px-3 py-1.5 font-mono text-[11px] tracking-widest text-ns-muted transition-colors duration-200 hover:border-foreground/20 hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ns-accent";
return (
<main className="flex min-h-screen items-center justify-center bg-background p-6">
<div className="w-full max-w-xl">
<p className="mb-4 font-mono text-xs tracking-widest text-ns-muted">
ns-ui / meter-context-window
</p>
<div className="overflow-hidden rounded-md border border-border bg-surface">
<header className="flex items-center justify-between gap-4 border-b border-border px-5 py-3">
<span className="font-mono text-xs tracking-widest text-ns-muted">
AGENT SESSION
</span>
<span className="font-mono text-[11px] tabular-nums text-ns-muted">
claude-4.5 · {fmt(CAPACITY)} ctx
</span>
</header>
<div className="px-5 py-5">
<BallastContext
capacity={CAPACITY}
system={SYSTEM}
tools={ctx.tools}
history={ctx.history}
turn={ctx.turn}
ariaLabel="Context window budget for this session"
/>
</div>
<div className="border-t border-border px-5 py-2">
<p className="truncate font-mono text-[11px] text-ns-muted">
<span className="text-foreground">event</span> · {status}
</p>
</div>
<footer className="flex flex-wrap items-center gap-2 border-t border-border px-5 py-3">
<button type="button" onClick={callTool} className={buttonClass}>
CALL TOOL
</button>
<button
type="button"
onClick={compact}
data-ballast-compact
className={buttonClass}
>
COMPACT CONTEXT
</button>
<button
type="button"
onClick={fillWindow}
data-ballast-fill
className={buttonClass}
>
FILL WINDOW
</button>
<button type="button" onClick={reset} className={buttonClass}>
RESET SESSION
</button>
<span className="ml-auto font-mono text-[11px] tabular-nums text-ns-muted">
{pctUsed}% of window
</span>
</footer>
</div>
<p className="mt-3 font-mono text-[11px] text-ns-muted">
the bar is a picture of what the model can see right now — compact
when history dominates and watch it resettle, not jump
</p>
</div>
</main>
);
}Component
registry/core/meter-context-window/component.tsx"use client";
// A context-window budget meter: one hairline stacked bar segmenting the
// window into system prompt / tools / conversation history / current turn,
// plus the unclaimed remainder, with a Geist Mono token readout underneath.
// It reads at rest because the bar IS the thing it describes — no legend
// needed to decode "what does 40% mean," the segment widths are the answer.
//
// Segments carry no color of their own (the palette is monochrome by house
// rule) — they're told apart by a stepped weight ramp (four flat, clearly
// separated color-mix() tones, lightest for system up to heaviest for the
// live turn) plus a bonus pattern once a segment is wide enough to resolve
// it: tools a diagonal hatch, history a stipple, current turn a fine
// vertical hatch that breathes gently while it's live. A 1px hairline
// divider sits between every pair so the boundary reads even when two
// tones land close together. Pure CSS, built from color-mix() against
// --foreground and --background, never a hex literal and never --ns-accent
// (accent is reserved for interaction, not decoration).
//
// Layout is plain flexbox: each segment's flex-basis is its share of
// max(capacity, used) as a percentage, with a min-width floor on any segment
// that has real tokens but would round below a legible sliver. The floor is
// enforced by the browser's own flex-shrink algorithm (larger segments give
// up the room), not by a hand-rolled pixel solver — a segment with tokens
// never disappears, one with none never fakes a sliver it didn't earn.
//
// Approaching capacity is signalled through form: the wrapper gains a
// pulsing inset ring, the readout goes bold and grows a triangle glyph, and
// the free segment (if any width remains) grows a hazard hatch — never a red
// fill, this palette doesn't have one. Compaction is just a prop change:
// react re-renders new percentages and the segments ease into them on a
// shared transition, so the resettle itself carries the news.
//
// All the numbers are real DOM text (a <p> readout plus a legend list), so
// assistive tech gets the actual figures regardless of what the bar looks
// like; the bar itself is aria-hidden as a redundant visual. The readout is
// a polite live region since it changes asynchronously as tokens accrue.
// prefers-reduced-motion drops the transition, the breathing, and the
// hazard pulse — every state stays fully legible, just static.
export interface BallastContextProps {
/** total size of the context window, in tokens */
capacity: number;
/** tokens spent on the system prompt */
system: number;
/** tokens spent on tool/function definitions and results */
tools: number;
/** tokens spent on prior conversation turns */
history: number;
/** tokens spent on the turn currently being read or generated */
turn: number;
/**
* fraction of capacity (0-1) below which the free segment is flagged as
* low headroom. default 0.08 (8%)
*/
lowFreeThreshold?: number;
/** accessible name for the meter. default "Context window budget" */
ariaLabel?: string;
/** extra classes merged onto the rendered root element */
className?: string;
}
const ROLES = [
{ role: "system", label: "System" },
{ role: "tools", label: "Tools" },
{ role: "history", label: "History" },
{ role: "turn", label: "Turn" },
{ role: "free", label: "Free" },
] as const;
function formatTokens(n: number): string {
return Math.round(Math.max(0, n)).toLocaleString("en-US");
}
function formatPct(tokens: number, denom: number): string {
if (tokens <= 0 || denom <= 0) return "0%";
const p = (tokens / denom) * 100;
if (p < 1) return "<1%";
return `${Math.round(p)}%`;
}
export function BallastContext({
capacity,
system,
tools,
history,
turn,
lowFreeThreshold = 0.08,
ariaLabel = "Context window budget",
className = "",
}: BallastContextProps) {
const safeSystem = Math.max(0, system);
const safeTools = Math.max(0, tools);
const safeHistory = Math.max(0, history);
const safeTurn = Math.max(0, turn);
const safeCapacity = Math.max(0, capacity);
const used = safeSystem + safeTools + safeHistory + safeTurn;
const overBudget = safeCapacity > 0 && used > safeCapacity;
const free = Math.max(0, safeCapacity - used);
// denominator the bar fills to 100% against: capacity normally, or the
// used total itself once over budget, so the four segments still sum to
// a full bar instead of visually shrinking below their real weight
const denom = Math.max(safeCapacity, used, 1);
const freeFraction = safeCapacity > 0 ? free / safeCapacity : 0;
const warn = safeCapacity > 0 && (overBudget || freeFraction <= lowFreeThreshold);
const tokensByRole: Record<(typeof ROLES)[number]["role"], number> = {
system: safeSystem,
tools: safeTools,
history: safeHistory,
turn: safeTurn,
free,
};
const pct = safeCapacity > 0 ? Math.round((used / safeCapacity) * 100) : 0;
const statusSuffix = overBudget ? " · over budget" : warn ? " · low headroom" : "";
return (
<div
role="group"
aria-label={ariaLabel}
data-warn={warn}
className={["ns-ballast", className].join(" ")}
>
<style>{`
.ns-ballast-track{transition:box-shadow 300ms ease-out}
.ns-ballast[data-warn="true"] .ns-ballast-track{animation:ns-ballast-ring 1.8s ease-in-out infinite}
.ns-ballast-seg{transition:flex-basis 520ms cubic-bezier(.22,1,.36,1)}
.ns-ballast-track .ns-ballast-seg:not([data-role="free"]){border-right:1px solid var(--background)}
.ns-ballast-seg[data-role="system"]{background:color-mix(in srgb, var(--foreground) 22%, var(--background))}
.ns-ballast-seg[data-role="tools"]{background-color:color-mix(in srgb, var(--foreground) 30%, var(--background));background-image:repeating-linear-gradient(45deg, color-mix(in srgb, var(--foreground) 46%, var(--background)) 0 2px, transparent 2px 5px)}
.ns-ballast-seg[data-role="history"]{background-color:color-mix(in srgb, var(--foreground) 40%, var(--background));background-image:radial-gradient(color-mix(in srgb, var(--foreground) 60%, var(--background)) 1.1px, transparent 1.3px);background-size:5px 5px}
.ns-ballast-seg[data-role="turn"]{background-color:color-mix(in srgb, var(--foreground) 52%, var(--background));background-image:repeating-linear-gradient(90deg, color-mix(in srgb, var(--foreground) 72%, var(--background)) 0 1.5px, transparent 1.5px 4px);animation:ns-ballast-breathe 2.6s ease-in-out infinite}
.ns-ballast-seg[data-role="free"]{background:transparent}
.ns-ballast[data-warn="true"] .ns-ballast-seg[data-role="free"]{background-image:repeating-linear-gradient(135deg, color-mix(in srgb, var(--foreground) 26%, transparent) 0 1px, transparent 1px 6px);animation:ns-ballast-hazard 1.6s ease-in-out infinite}
@keyframes ns-ballast-breathe{0%,100%{opacity:.82}50%{opacity:1}}
@keyframes ns-ballast-hazard{0%,100%{opacity:.5}50%{opacity:1}}
@keyframes ns-ballast-ring{0%,100%{box-shadow:inset 0 0 0 1px var(--border)}50%{box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--foreground) 45%, transparent)}}
@media (prefers-reduced-motion: reduce){
.ns-ballast-seg{transition:none;animation:none !important;opacity:1 !important}
.ns-ballast-track{animation:none !important;box-shadow:inset 0 0 0 1px var(--border) !important}
}
`}</style>
<div
aria-hidden="true"
className="ns-ballast-track flex h-2.5 w-full overflow-hidden rounded-full border border-border bg-background"
>
{ROLES.map(({ role }) => {
const t = tokensByRole[role];
return (
<div
key={role}
data-role={role}
className="ns-ballast-seg h-full shrink"
style={{
flexBasis: `${(t / denom) * 100}%`,
flexGrow: 0,
minWidth: t > 0 ? "3px" : "0px",
}}
/>
);
})}
</div>
<p
aria-live="polite"
className={[
"mt-3 flex items-center gap-1.5 font-mono text-xs tabular-nums text-ns-muted",
warn ? "font-semibold text-foreground" : "",
].join(" ")}
>
{warn ? (
<svg
aria-hidden="true"
width="12"
height="12"
viewBox="0 0 16 16"
fill="none"
className="shrink-0 text-foreground"
>
<path
d="M8 2.4 14.6 13.6H1.4L8 2.4Z"
stroke="currentColor"
strokeWidth="1.3"
strokeLinejoin="round"
/>
<path d="M8 6.6v3.4" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round" />
<circle cx="8" cy="11.9" r="0.85" fill="currentColor" />
</svg>
) : null}
<span>
{formatTokens(used)} / {formatTokens(safeCapacity)} tokens · {pct}%{statusSuffix}
</span>
</p>
<ul className="mt-2.5 flex flex-wrap gap-x-4 gap-y-1.5 font-mono text-[11px] tabular-nums text-ns-muted">
{ROLES.map(({ role, label }) => {
const t = tokensByRole[role];
return (
<li key={role} className="flex items-center gap-1.5">
<span
aria-hidden="true"
data-role={role}
className="ns-ballast-seg h-2.5 w-2.5 shrink-0 rounded-[2px] border border-border"
/>
<span className="tracking-wide text-ns-muted">{label}</span>
<span className="text-foreground">{formatTokens(t)}</span>
<span>{formatPct(t, denom)}</span>
</li>
);
})}
</ul>
</div>
);
}The usage example imports ./component because it sits next to the component file in this repo. Installed, that file lands at components/ui/meter-context-window.tsx, so point the import there.
Use when a read-only budget readout for an already-composed context window, splitting it into system prompt, tools, history and current turn by fill pattern rather than color so the shares survive a monochrome or colorblind read, and resettling rather than jumping when a compaction lands; pick context-prompt-shims instead when the user decides what goes in and in what order, or context-compaction-river when the interesting thing is which turns were compacted away and whether they can come back.
Install
npx shadcn add https://design.helpmarq.com /r/meter-context-window.jsonBuild spec
A context-window budget meter for agent UIs: one hairline stacked bar (rounded-full, 10px tall, 1px --border ring, bg-background bed) segmenting the window into five shares — system prompt, tools, conversation history, current turn, and the unclaimed remainder — plus a font-mono tabular-nums readout and a text legend underneath. Pure DOM/CSS, no canvas, no SVG paint loop, no rAF: each segment is a flex child whose flex-basis is its percentage of max(capacity, used) so the five always sum to a full bar even when usage exceeds capacity, and a min-width floor (3px) on any segment that has real tokens keeps it from vanishing below a legible sliver — the browser's own flex-shrink gives it room by shrinking the larger segments, not a hand-rolled pixel solver, so a segment with zero tokens renders zero width honestly and one with tokens never fakes a mark it didn't earn. The palette is monochrome by house rule, so segments are told apart primarily by a stepped weight ramp (four flat, unmistakably different color-mix() tones against --foreground, never --ns-accent — 22/30/40/52% — so even a 3px sliver reads as a distinct step, with no fill ever falling through to a bare or transparent look that would misread as free space) plus a bonus pattern layered on top of each base tone once a segment is wide enough to resolve it (system stays flat, tools gets a 45deg diagonal hatch, history a stipple, current turn a close vertical hatch that breathes gently — a 2.6s opacity pulse — while it's the live, growing share); a hairline 1px --background divider sits between every adjacent pair so boundaries stay crisp even when two neighboring tones land close together, and the free remainder is bare track. Compaction is just a prop change — react re-renders new percentages and every segment eases into them on a shared 520ms cubic-bezier(0.22,1,0.36,1) flex-basis transition, so the resettle itself is what communicates that compaction happened, never an instant jump. Approaching capacity reads as a warning through form, never a red fill: once free space drops to or below `lowFreeThreshold` (prop, fraction of capacity, default 0.08) or usage exceeds capacity outright, the track gains a slow pulsing inset ring, the readout goes bold and grows a small triangle glyph, and the free segment (if any width remains) grows a diagonal hazard hatch. Props: capacity, system, tools, history, turn (all plain numbers, tokens, fully controlled — the component holds no state of its own), lowFreeThreshold, ariaLabel, className. Accessibility: the bar itself is aria-hidden (a redundant visual against the text that follows); a role=group wraps the whole meter with an accessible name; the token readout is a real aria-live=polite paragraph so async updates get announced; the legend below the bar is plain DOM text — label, exact token count, and a percentage (values under 1% but above zero read '<1%' rather than rounding to a misleading 0%) — for every one of the five shares, so assistive tech gets the real numbers regardless of what the bar renders. The component exposes no interactive controls of its own (display-only, correctly skipped by the tab-reachability check) and renders correctly in both themes since every ink is a CSS custom property, never a literal. prefers-reduced-motion drops the flex-basis transition, the current-turn breathing, and the hazard pulse entirely — every state (empty, mid-fill, near-capacity, over-budget) stays fully legible, just static. Demo: an agent-session card (claude-4.5, 200K-token window) seeded with a realistic mid-fill baseline so the resting screenshot already shows the meter doing its job, plus a live status line, a CALL TOOL button that grows the tools share, a COMPACT CONTEXT button that shrinks history to roughly a ninth of itself and lets the resettle play, a RESET SESSION button, a FILL WINDOW button that jumps usage straight to ~96% so the near-capacity warning state (pulsing ring, bold readout + triangle glyph, hazard hatch on the free sliver) is reachable in one click rather than only after minutes of ambient growth, and a background interval that streams small turns into the current-turn share and folds each one into history after it settles — pausing near 94% usage the way a real agent would compact before running out, so the near-capacity warning state is also reachable just by leaving the demo running.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| capacity | number | — | total size of the context window, in tokens |
| system | number | — | tokens spent on the system prompt |
| tools | number | — | tokens spent on tool/function definitions and results |
| history | number | — | tokens spent on prior conversation turns |
| turn | number | — | tokens spent on the turn currently being read or generated |
| lowFreeThreshold? | number | 0.08 | fraction of capacity (0-1) below which the free segment is flagged as low headroom. default 0.08 (8%) |
| ariaLabel? | string | "Context window budget" | accessible name for the meter. default "Context window budget" |
| className? | string | — | extra classes merged onto the rendered root element |