{"slug":"context-usage","category":"indicators","job":"oversee","added":"2026-11-07T23:06Z","title":"Context Usage","description":"A context-window meter: tokens used against the limit, a segmented bar of what fills it, a legend to inspect each source and a Compact action to free space.","url":"https://ai-patterns.app/patterns/indicators/context-usage","markdown":"https://ai-patterns.app/patterns/indicators/context-usage.md","json":"https://ai-patterns.app/api/patterns/indicators/context-usage","seenIn":[{"team":"Claude","source":"https://code.claude.com/docs/en/slash-commands"}],"spec":"# Context Usage\n\n## Summary\nA card that shows how full the model's context window is: a token count against the limit, a segmented bar of what's taking the space (chat, files, tools, system prompt, memory), a legend that picks out each part, and a Compact action that summarises older content to free room before the window runs out.\n\n## When to use\n- Long-running chats, coding agents and research sessions where the context fills up and the model starts forgetting or the product starts truncating.\n- Products that let people add large inputs (files, repos, tool output) and need them to see the cost of each.\n- Anywhere you offer compaction or summarisation, so people can choose when it happens instead of being surprised by it.\n\n## When not to use\n- Short, casual chats that never come close to the limit. A meter there is noise.\n- To show what a run will cost in money or credits — use Cost Estimate.\n- To show usage caps over time (messages per hour) — use Rate Limit.\n\n## Anatomy\n- Header: a title (\"Context\") and the Compact action.\n- Total: the tokens used in large type (\"175.6K\"), with the limit beside it (\"of 200K\").\n- Segmented bar: one block per source, sized by its share of the window, followed by a neutral block for free space. The whole bar is the window.\n- Legend: one chip per source with its colour swatch and name.\n- Low-space note: appears near the limit, saying how much is left and what compacting will do.\n\n## Behavior\n- Segments resize as content is added or removed, and the total animates to its new value.\n- Hovering or focusing a legend chip (or hovering a segment) dims the other segments and swaps the total for that source's tokens and share (\"39.2K · files · 20%\").\n- Past a threshold (90% by default) the low-space note appears and Compact becomes the primary action.\n- Compact shows a pending state (\"Compacting…\"), then the bar shrinks as older turns are replaced by a summary and stale tool output is dropped. System prompt and memory are kept.\n- Always offer compaction before the product does it on its own; when it does auto-compact, say so in the note.\n\n## Content guidelines\n- Format token counts compactly with one decimal (\"175.6K\", \"1.2M\"); never show raw counts like 175,612.\n- Name sources in the user's terms (Chat, Files, Tools), not internal ones (messages array, tool_results).\n- Keep the source list short — five or so. Fold rare sources into \"Other\".\n- The low-space note says what's at stake, not just a number: \"24.4K left — compact to keep earlier details from being dropped.\"\n\n## Accessibility\n- The bar is `role=\"meter\"` with `aria-valuetext` (\"175.6K of 200K tokens used\").\n- Legend chips are buttons with `aria-pressed` and a visually hidden token count, so each source can be inspected from the keyboard.\n- The total and low-space note are in an `aria-live=\"polite\"` region that also describes the Compact button.\n- Colour is never the only cue: every segment has a labelled chip, and the warning is text.\n- Animations are skipped when the user prefers reduced motion.\n\n## Related patterns\n- **Cost Estimate** — Use it instead when the constraint is credits or money for a single run, not space in the window.\n- **Rate Limit** — Use it instead for caps on how much people can use over time.\n- **Memory Panel** — Use it as well to let people see and edit what's kept across sessions, which also takes up context.\n","code":"\"use client\";\n\nimport * as React from \"react\";\nimport { motion, useReducedMotion, useSpring, useTransform } from \"motion/react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport interface ContextSegment {\n  /** Stable id, also used as the React key. */\n  id: string;\n  label: string;\n  tokens: number;\n  /** Tailwind background class for the segment and its legend dot. */\n  color: string;\n}\n\nexport interface ContextUsageProps {\n  /** What fills the context window, in the order it's drawn. */\n  segments: ContextSegment[];\n  /** Size of the context window, in tokens. */\n  limit: number;\n  /** Share of the window (0–1) at which the meter warns that it's nearly full. */\n  warnAt?: number;\n  title?: string;\n  compactLabel?: string;\n  /** Summarise older content to free space. Return a promise to show a pending state. */\n  onCompact?: () => void | Promise<void>;\n  className?: string;\n}\n\nexport function formatTokens(n: number) {\n  if (n >= 1_000_000) return `${trim(n / 1_000_000)}M`;\n  if (n >= 1_000) return `${trim(n / 1_000)}K`;\n  return `${Math.round(n)}`;\n}\n\nfunction trim(v: number) {\n  return v.toFixed(1).replace(/\\.0$/, \"\");\n}\n\nconst spring = { type: \"spring\", stiffness: 200, damping: 28 } as const;\n\nexport function ContextUsage({\n  segments,\n  limit,\n  warnAt = 0.9,\n  title = \"Context\",\n  compactLabel = \"Compact\",\n  onCompact,\n  className,\n}: ContextUsageProps) {\n  const reduceMotion = !!useReducedMotion();\n  const id = React.useId();\n  const [active, setActive] = React.useState<string | null>(null);\n  const [compacting, setCompacting] = React.useState(false);\n\n  const used = segments.reduce((sum, s) => sum + s.tokens, 0);\n  const share = Math.min(1, used / Math.max(limit, 1));\n  const free = Math.max(0, limit - used);\n  const warn = share >= warnAt;\n  const focused = segments.find((s) => s.id === active);\n  const transition = reduceMotion ? { duration: 0 } : spring;\n\n  async function compact() {\n    if (!onCompact || compacting) return;\n    setCompacting(true);\n    try {\n      await onCompact();\n    } finally {\n      setCompacting(false);\n    }\n  }\n\n  return (\n    <div className={cn(\"flex w-full flex-col gap-4 rounded-3xl border bg-card p-5\", className)}>\n      <div className=\"flex items-center justify-between gap-3\">\n        <h3 className=\"text-base font-medium\">{title}</h3>\n        {onCompact && (\n          <button\n            type=\"button\"\n            onClick={compact}\n            disabled={compacting}\n            aria-describedby={`${id}-status`}\n            className={cn(\n              \"rounded-full px-3.5 py-1.5 text-sm font-medium transition-colors disabled:cursor-progress disabled:opacity-60\",\n              warn ? \"bg-foreground text-background hover:opacity-90\" : \"bg-muted hover:bg-muted-foreground/15\"\n            )}\n          >\n            {compacting ? \"Compacting…\" : compactLabel}\n          </button>\n        )}\n      </div>\n\n      <div id={`${id}-status`} aria-live=\"polite\" className=\"flex flex-col gap-1\">\n        <p className=\"flex flex-wrap items-baseline gap-x-2\">\n          <span className=\"text-4xl font-medium tracking-tight tabular-nums\">\n            <AnimatedTokens value={focused ? focused.tokens : used} instant={reduceMotion} />\n          </span>\n          <span className=\"text-sm text-muted-foreground\">\n            {focused ? `${focused.label.toLowerCase()} · ${Math.round((focused.tokens / limit) * 100)}%` : `of ${formatTokens(limit)}`}\n          </span>\n        </p>\n        {warn && !focused && (\n          <p className=\"text-xs font-medium text-amber-700 dark:text-amber-400\">\n            {formatTokens(free)} left — compact to keep earlier details from being dropped.\n          </p>\n        )}\n      </div>\n\n      <div\n        role=\"meter\"\n        aria-label={`${title} window used`}\n        aria-valuemin={0}\n        aria-valuemax={limit}\n        aria-valuenow={Math.min(used, limit)}\n        aria-valuetext={`${formatTokens(used)} of ${formatTokens(limit)} tokens used`}\n        className=\"flex h-11 w-full gap-1\"\n        onMouseLeave={() => setActive(null)}\n      >\n        {segments.map((s) => (\n          <motion.div\n            key={s.id}\n            title={`${s.label}: ${formatTokens(s.tokens)}`}\n            onMouseEnter={() => setActive(s.id)}\n            className={cn(\"h-full min-w-0 rounded-md transition-opacity\", s.color, active && active !== s.id && \"opacity-30\")}\n            initial={false}\n            animate={{ flexGrow: s.tokens, flexBasis: 0 }}\n            style={{ flexShrink: 1, minWidth: s.tokens > 0 ? 4 : 0 }}\n            transition={transition}\n          />\n        ))}\n        <motion.div\n          aria-hidden\n          className=\"h-full rounded-md bg-muted\"\n          initial={false}\n          animate={{ flexGrow: free, flexBasis: 0 }}\n          transition={transition}\n        />\n      </div>\n\n      <ul className=\"flex flex-wrap gap-2\" aria-label=\"What's in context\">\n        {segments.map((s) => (\n          <li key={s.id}>\n            <button\n              type=\"button\"\n              aria-pressed={active === s.id}\n              onMouseEnter={() => setActive(s.id)}\n              onMouseLeave={() => setActive(null)}\n              onFocus={() => setActive(s.id)}\n              onBlur={() => setActive(null)}\n              onClick={() => setActive((a) => (a === s.id ? null : s.id))}\n              className={cn(\n                \"flex items-center gap-2 rounded-full bg-muted px-3 py-1.5 text-sm transition-colors outline-none\",\n                \"hover:bg-muted-foreground/15 focus-visible:ring-2 focus-visible:ring-ring\",\n                active === s.id && \"bg-muted-foreground/15\"\n              )}\n            >\n              <span className={cn(\"size-2.5 rounded-[3px]\", s.color)} aria-hidden />\n              {s.label}\n              <span className=\"sr-only\">: {formatTokens(s.tokens)} tokens</span>\n            </button>\n          </li>\n        ))}\n      </ul>\n    </div>\n  );\n}\n\nfunction AnimatedTokens({ value, instant }: { value: number; instant: boolean }) {\n  const motionValue = useSpring(value, { stiffness: 220, damping: 30 });\n  const text = useTransform(motionValue, (v) => formatTokens(v));\n\n  React.useEffect(() => {\n    if (instant) motionValue.jump(value);\n    else motionValue.set(value);\n  }, [motionValue, value, instant]);\n\n  return <motion.span>{text}</motion.span>;\n}\n","dependencies":["motion"],"localImports":["@/lib/utils"]}