How full an agent's context is and what fills it: a stacked bar by part with tokens and share in a legend, room kept for the answer, a plain warning when nearly full or over, and a Compact button that says why when it cannot.
Context
118Kof 200K
- System prompt3.8K2%
- Tools and their schemas14K7%
- Files in the project62K31%
- Conversation38K19%
- Kept free for the answer16K8%
When it fills, the oldest messages are dropped first. Compact summarises them so nothing important is lost.
"use client"
import { ContextWindow } from "@/components/ballmac/context-window"
export default function ContextWindowDemo() {
return (
<div className="w-full max-w-lg">
<ContextWindow
limit={200000}
reserved={16000}
fullNote="When it fills, the oldest messages are dropped first. Compact summarises them so nothing important is lost."
onCompact={() => new Promise((resolve) => setTimeout(resolve, 800))}
segments={[
{ id: "sys", label: "System prompt", tokens: 3800 },
{ id: "tools", label: "Tools and their schemas", tokens: 14200 },
{ id: "files", label: "Files in the project", tokens: 62000 },
{ id: "chat", label: "Conversation", tokens: 38000 },
]}
/>
</div>
)
}Installation
$ pnpm dlx shadcn@latest add @ballmac/context-windowInstall the dependencies.
$ pnpm add lucide-reactAdd the Ballmac items it builds on.
$ pnpm dlx shadcn@latest add @ballmac/i18nCopy the source into your project.
components/ballmac/context-window.tsx// Ballmac UI: Context Window. https://ui.ballmac.com/components/context-window "use client" import * as React from "react" import { Loader2, Minimize2, TriangleAlert } from "lucide-react" import { cn } from "@/lib/utils" import { useMessages } from "@/lib/ballmac/i18n" type ContextSegment = { id: string /** What fills this part, for example "System prompt" or "Files". */ label: string /** Tokens it takes. */ tokens: number } type ContextWindowProps = Omit<React.ComponentProps<"section">, "title"> & { segments: ContextSegment[] /** The model's context window, in tokens. */ limit: number /** Tokens kept free for the answer. Drawn hatched at the end of the bar. */ reserved?: number /** From this share of the limit (0 to 1), the card warns that it is nearly full. */ warnAt?: number /** Heading. */ title?: string /** What happens when it is full, in a sentence. Shown under the legend. */ fullNote?: string /** Called by the Compact button, which summarises the conversation to free space. Reject and the card says why. */ onCompact?: () => void | Promise<void> } const TONES = ["bg-chart-1", "bg-chart-2", "bg-chart-3", "bg-chart-4", "bg-chart-5"] as const /** 142000 → "142K", 1500000 → "1.5M", 950 → "950". */ function formatTokens(n: number) { if (n >= 1_000_000) return `${+(n / 1_000_000).toFixed(1)}M` if (n >= 10_000) return `${Math.round(n / 1000)}K` if (n >= 1000) return `${+(n / 1000).toFixed(1)}K` return String(Math.round(n)) } /** * How full an agent's context window is and what is filling it: a stacked bar by part (system prompt, tools, files, messages), the tokens and share of each in * a legend, a hatched stretch for the room kept for the answer, and a warning in words when it is nearly full or over. Compact asks the app to summarise and * free space, and says so when it cannot. The bar is a meter with a spoken value. */ function ContextWindow({ segments, limit, reserved = 0, warnAt = 0.8, title, fullNote, onCompact, className, ...props }: ContextWindowProps) { const msg = useMessages() const id = React.useId() title ??= msg("context-window.title", "Context") const used = segments.reduce((n, s) => n + Math.max(0, s.tokens), 0) const share = limit > 0 ? used / limit : 0 const over = used > limit const warn = !over && share >= warnAt const pct = (n: number) => `${Math.min(100, Math.max(0, (n / Math.max(limit, 1)) * 100))}%` const [busy, setBusy] = React.useState(false) const [error, setError] = React.useState<string | null>(null) const status = over ? msg("context-window.over", "Over the limit") : warn ? msg("context-window.nearly", "Nearly full") : null const spoken = msg("context-window.spoken", "{used} of {limit} tokens, {percent}% used", { used: formatTokens(used), limit: formatTokens(limit), percent: Math.round(share * 100) }) async function compact() { setBusy(true) setError(null) try { await onCompact?.() } catch (e) { setError(e instanceof Error ? e.message : msg("context-window.compactFailed", "It could not be compacted. Nothing was changed.")) } finally { setBusy(false) } } return ( <section data-slot="context-window" data-state={over ? "over" : warn ? "warn" : "ok"} aria-labelledby={`${id}-t`} className={cn("bg-card text-card-foreground grid gap-4 rounded-xl border p-4 sm:p-5", className)} {...props}> <div className="flex flex-wrap items-baseline justify-between gap-x-4 gap-y-1"> <h3 id={`${id}-t`} className="text-sm font-semibold">{title}</h3> <p className="flex items-center gap-2 text-sm tabular-nums"> {status && ( <span className={cn("inline-flex items-center gap-1 rounded-full border px-2 py-px text-[11px] font-medium", over ? "border-destructive/50 text-destructive" : "text-foreground")}> <TriangleAlert aria-hidden="true" className="size-3" /> {status} </span> )} <span className="font-mono text-base font-semibold">{formatTokens(used)}</span> <span className="text-muted-foreground">{msg("context-window.of", "of {limit}", { limit: formatTokens(limit) })}</span> </p> </div> <div role="meter" aria-labelledby={`${id}-t`} aria-valuemin={0} aria-valuemax={limit} aria-valuenow={Math.min(used, limit)} aria-valuetext={spoken} className="bg-muted relative flex h-3 overflow-hidden rounded-full"> {segments.map((s, i) => ( <span key={s.id} aria-hidden="true" className={cn("h-full border-e border-card transition-[width] duration-500 last:border-e-0 motion-reduce:transition-none", TONES[i % TONES.length])} style={{ width: pct(s.tokens) }} /> ))} {reserved > 0 && <span aria-hidden="true" className="ms-auto h-full bg-[repeating-linear-gradient(135deg,var(--muted-foreground)_0_2px,transparent_2px_6px)] opacity-40" style={{ width: pct(reserved) }} />} </div> <ul className="grid gap-1.5 text-[13px]"> {segments.map((s, i) => ( <li key={s.id} className="flex items-center gap-2.5"> <span aria-hidden="true" className={cn("size-2.5 shrink-0 rounded-sm", TONES[i % TONES.length])} /> <span className="min-w-0 flex-1 truncate">{s.label}</span> <span className="font-mono text-xs tabular-nums text-muted-foreground">{formatTokens(s.tokens)}</span> <span className="w-10 text-end font-mono text-xs tabular-nums text-muted-foreground">{limit > 0 ? `${Math.round((s.tokens / limit) * 100)}%` : "–"}</span> </li> ))} {reserved > 0 && ( <li className="flex items-center gap-2.5 text-muted-foreground"> <span aria-hidden="true" className="size-2.5 shrink-0 rounded-sm bg-[repeating-linear-gradient(135deg,var(--muted-foreground)_0_2px,transparent_2px_4px)] opacity-60" /> <span className="min-w-0 flex-1 truncate">{msg("context-window.reserved", "Kept free for the answer")}</span> <span className="font-mono text-xs tabular-nums">{formatTokens(reserved)}</span> <span className="w-10 text-end font-mono text-xs tabular-nums">{limit > 0 ? `${Math.round((reserved / limit) * 100)}%` : "–"}</span> </li> )} </ul> {(fullNote || onCompact) && ( <div className="flex flex-wrap items-center justify-between gap-3 border-t pt-3"> {fullNote ? <p className="max-w-md text-xs leading-snug text-muted-foreground">{fullNote}</p> : <span />} {onCompact && ( <button type="button" disabled={busy} onClick={() => void compact()} className={cn("inline-flex h-8 items-center gap-1.5 rounded-md border px-3 text-xs font-medium outline-none transition-colors focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:opacity-60 motion-reduce:transition-none", warn || over ? "border-foreground bg-foreground text-background hover:bg-foreground/90" : "bg-background hover:bg-accent")}> {busy ? <Loader2 aria-hidden="true" className="size-3.5 animate-spin" /> : <Minimize2 aria-hidden="true" className="size-3.5" />} {msg("context-window.compact", "Compact conversation")} </button> )} </div> )} {error && <p role="alert" className="text-sm font-medium text-destructive">{error}</p>} </section> ) } export { ContextWindow, formatTokens as formatContextTokens, type ContextSegment, type ContextWindowProps }Update the import paths to match your project setup.
Usage
import { ContextWindow, formatContextTokens } from "@/components/ballmac/context-window"The full example is in the Code tab above.
Examples
Nearly full
Context
Nearly full168Kof 200K
- System prompt3.8K2%
- Tools and their schemas14K7%
- Files in the project98K49%
- Conversation52K26%
- Kept free for the answer16K8%
Compact now, or the next long file will push the start of the task out.
"use client"
import { ContextWindow } from "@/components/ballmac/context-window"
export default function ContextWindowNearly() {
return (
<div className="w-full max-w-lg">
<ContextWindow
limit={200000}
reserved={16000}
onCompact={() => new Promise((resolve) => setTimeout(resolve, 800))}
fullNote="Compact now, or the next long file will push the start of the task out."
segments={[
{ id: "sys", label: "System prompt", tokens: 3800 },
{ id: "tools", label: "Tools and their schemas", tokens: 14200 },
{ id: "files", label: "Files in the project", tokens: 98000 },
{ id: "chat", label: "Conversation", tokens: 52000 },
]}
/>
</div>
)
}Over the limit
Context
Over the limit140Kof 128K
- System prompt3.8K3%
- Files in the project84K66%
- Conversation52K41%
Older messages are already being dropped.
"use client"
import { ContextWindow } from "@/components/ballmac/context-window"
export default function ContextWindowOver() {
return (
<div className="w-full max-w-lg">
<ContextWindow
limit={128000}
onCompact={() => Promise.reject(new Error("The summary model is busy. Nothing was changed."))}
fullNote="Older messages are already being dropped."
segments={[
{ id: "sys", label: "System prompt", tokens: 3800 },
{ id: "files", label: "Files in the project", tokens: 84000 },
{ id: "chat", label: "Conversation", tokens: 52000 },
]}
/>
</div>
)
}API reference
| Prop | Type | Default |
|---|---|---|
segments* | ContextSegment[] | — |
limit*The model's context window, in tokens. | number | — |
reservedTokens kept free for the answer. Drawn hatched at the end of the bar. | number | 0 |
warnAtFrom this share of the limit (0 to 1), the card warns that it is nearly full. | number | 0.8 |
titleHeading. | string | — |
fullNoteWhat happens when it is full, in a sentence. Shown under the legend. | string | — |
onCompactCalled by the Compact button, which summarises the conversation to free space. Reject and the card says why. | () => void | Promise<void> | — |
Also accepts the standard attributes of its root element.
Accessibility
| Key | Action |
|---|---|
| Meter | The bar is role=meter named by the heading, with aria-valuenow, aria-valuemax and a spoken value such as '142K of 200K tokens, 71% used' |
| Warning | Nearly full and Over the limit are written in a pill with an icon, not only coloured |
| Legend | A list of parts with tokens and share, so the bar is never the only way to read it |
| Compact | A native button that shows busy; a failure is an alert |
Use with AI
segments is [{ id, label, tokens }] (required) and limit is the window in tokens (required). reserved keeps room for the answer (drawn hatched), warnAt (default 0.8) is where it warns, title, fullNote (what happens when it is full), onCompact (adds the Compact button; reject and the card says why). The bar is a meter with a spoken value; Nearly full and Over the limit are words, not just colour. With the shadcn MCP server set up (guide), ask your agent:
Add the Ballmac UI Context Window (@ballmac/context-window) to this project with the shadcn MCP, then use it where it fits.
Use it for
- Showing a person how much of the model's memory a conversation or run is using
- Warning before an agent starts forgetting the start of a task
Not for
- Spend in money; use run-ledger or usage-meter
- A single progress value; use progress
Registry JSON: https://ui.ballmac.com/r/context-window.json
Credits
Free to use in personal and commercial projects.
- npm
- lucide-react
- Registry
- @ballmac/i18nshadcn/utils
Pairs well with
Model Picker
A searchable model selector grouped by provider, with capability icons, locked plans and a detail pane that follows the highlighted model with context window, cost and abilities.
Agent Steering
Redirecting an agent while it works: say what to change and when (now, after this step or at the end), and watch each instruction go from queued to applied or dropped. Take back an instruction that has not applied yet.
Plan Tracker
An agent's plan against what happened: every step with its state in words, expected and actual time, a bar of the whole plan, an honest note when steps were added or skipped, and how far a step ran over.
AI Chat
The layout for a chat UI: a message log that sticks to the bottom while replies stream unless the reader scrolls up, a jump-to-latest button, an empty state with suggestions and a footer.
Ballmac UI Pro
Need the whole screen, not one piece?
150 premium blocks (dashboards, settings, calendars, mail, shops, onboarding), a private shadcn registry and Figma tokens. Blocks that show data come with loading, empty and error states built in.
Not ready? Try three Pro blocks free
Leave your email and the blocks appear right here, ready to copy. Yours to use in any project.