Context Compaction Viz

An interactive visualisation of how production LLM systems manage conversation memory. As turns accumulate, the token count climbs against a 16K context budget; when the user fires Compact (or the auto-threshold trips at 70%), the older messages collapse into a six-bullet summary block and the bar drops back into the safe zone. A What was lost? toggle reveals the original messages side-by-side with the summary so the trade-off is concrete.

The insight (JetBrains, NeurIPS 2025): observation masking — dropping tool outputs while keeping reasoning — performs as well as LLM-based summarisation, and proactive compaction at 50–60% beats waiting for auto-compaction at 70%.

User85 tok
I need to cancel my enterprise subscript…
Assistant420 tok
I can help with that. Let me look up you…
16K
505
3% used
505 of 16K tokens used across 2 turns. 3 percent of budget.

Each API call packs the full conversation into the context window. Click "Add Turn" to watch the token count grow.

Customize
Budget
16,000
60%
70%
Compaction
12
800
2

Installation

npx shadcn@latest add https://craftbits.dev/r/context-compaction-viz.json

Usage

import { ContextCompactionViz } from "@craft-bits/viz/context-compaction-viz";
 
<ContextCompactionViz />

Bring your own transcript:

<ContextCompactionViz
  messages={[
    { id: 1, role: "user", text: "Why is my deploy failing?", tokens: 40 },
    { id: 2, role: "assistant", text: "Let me check the logs.", tokens: 220 },
    { id: 3, role: "tool", text: '{"build_id": "b-9214", "error": "OOM"}', tokens: 980 },
    /* … */
  ]}
  summaryFacts={["Build b-9214 OOM at link step", "Memory peak: 8.2GB / 8GB limit"]}
/>

Subscribe to the compaction event:

<ContextCompactionViz
  onCompact={({ tokensBefore, summaryTokens, tokensSaved }) => {
    /* lift the new totals into an external chart */
  }}
/>

Understanding the component

  1. The timeline. A scrollable column of role-coloured message bubbles (user, assistant, tool). Each bubble shows the role, the token cost, and a 40-character preview of the body. The full text stays in the data — visual truncation is purely for layout.
  2. The budget bar. A vertical fill on the right tracks totalTokens / tokenBudget. The fill colour shifts on three thresholds — green under 50%, warning between compactThreshold and autoCompactThreshold, error above the auto threshold. Two dashed lines mark the proactive (compactThreshold) and reactive (autoCompactThreshold) trigger points.
  3. Compaction. When the user clicks Compact (or the auto-threshold is crossed without intervention), the first compactSplit messages animate out and a summary block animates in. The recent messages remain untouched. The bar drops and the savings indicator surfaces underneath.
  4. What was lost. The toggle drops a side-by-side panel: every original message in the left column, the summary plus a Dropped callout in the right.
  5. Reduced motion. Under prefers-reduced-motion: reduce, every entrance, the compaction sequence, the bar-fill spring, and the savings indicator collapse to instant transitions.

Props

PropTypeDefaultDescription
messagesreadonly ContextCompactionVizMessage[]20-turn customer transcriptPre-loaded conversation. Each entry needs id, role, text, tokens.
summaryFactsreadonly string[]6 factsBullet facts rendered inside the post-compaction summary block.
tokenBudgetnumber16000Upper bound for the token-budget bar.
compactThresholdnumber0.6Fraction of tokenBudget at which the Compact button appears.
autoCompactThresholdnumber0.7Fraction of tokenBudget at which compaction auto-fires.
compactSplitnumber12Number of leading messages folded into the summary block.
summaryTokensnumber800Token cost of the summary block.
initialVisiblenumber2Messages visible on first render.
autoPlayIntervalMsnumber800Milliseconds between auto-play turns.
transitionTransitionSPRINGS.smoothOverride bubble / bar / savings spring.
onCompact(summary) => voidFires when compaction completes with { messagesCompacted, tokensBefore, summaryTokens, tokensSaved }.
onVisibleCountChange(visibleCount: number) => voidFires every time the visible-message count changes.
classNamestringMerged onto the root via cn().

Accessibility

  • The visualization root is role="figure" with an aria-label summarising current token usage and turn count so screen-reader users get the headline without needing the visual.
  • The budget bar is role="progressbar" with aria-valuemin / aria-valuemax / aria-valuenow reflecting live token usage, and an aria-label describing the bar's purpose.
  • A polite live region announces the running token / turn state on every change, and again after compaction with the new totals.
  • All four control buttons have visible focus rings, hit-target ≥ 32×32px, and pressed states (aria-pressed) on the toggles.
  • Colour is never the only signal — every role pill carries its label (User / Assistant / Tool Result), every threshold line has a percentage label, and the savings indicator includes the word saved.
  • Motion respects prefers-reduced-motion: reduce — every entrance and the compaction sequence collapse to instant transitions, and the bar fill snaps with duration: 0.

Credits

  • Extracted from: craftingattention (app/src/lessons/primitives/systems/ContextCompactionViz.tsx). The source was a lesson component using per-track palette tokens (--color-accent-400, --color-warn-400, --color-success-400, --color-fail-400, --color-ink-*, hard-coded oklch() role colours) and CA-specific spring aliases (SPRINGS.snappy, SPRINGS.gentle, STAGGER.tight). The craft-bits extract re-keys every colour to the semantic --cb-* palette (var(--cb-accent) / var(--cb-warning) / var(--cb-success) / var(--cb-error) / var(--cb-fg-*)) so consumer themes repaint freely, routes every transition through the canonical SPRINGS.snap / SPRINGS.smooth from @craft-bits/core/motion, and replaces STAGGER.tight with the library-wide STAGGER constant. The hard-coded transcript and summary facts are now props (messages, summaryFacts) with the original 20-turn customer-success story as the default, so the component teaches the same scene out of the box but accepts any conversation. The reduced-motion fallback was tightened to disable the bar's fill spring as well as the bubble entrances, the setTimeout-in-render auto-compaction was moved to an effect, and forwardRef + cn() + spread ...props were added to match the library's interactive-component API.