{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"context-usage","type":"registry:ui","title":"Context Usage","description":"How much of the context window is gone and to what — system prompt, files, history — plus what gets dropped first when the next message does not fit.","author":"Scrim UI (https://scrimui.dev)","categories":["files"],"docs":"https://scrimui.dev/components/context-usage","dependencies":[],"registryDependencies":[],"files":[{"path":"src/showcase/context-usage/context-usage.tsx","content":"\"use client\";\n\nimport * as React from \"react\";\n\n/**\n * How much of the context window is gone, and to what.\n *\n * A single \"78% full\" bar is the version everyone builds, and it answers the\n * wrong question. By the time a reader looks at this they are already in\n * trouble; what they need is *what to remove* and *what will be removed for\n * them*.\n *\n * **Reserve room for the reply.** The window is shared between what you send\n * and what comes back. A bar that reads 96% full with no reply reserved is\n * describing a request that cannot succeed, in the cheerful voice of one that\n * nearly can. So `reserve` is subtracted up front and drawn as its own\n * segment — the usable space is what is left after the answer has its room.\n *\n * **Eviction order is the actionable part.** Something gets dropped when the\n * next message does not fit, and the reader is entitled to know what before\n * it happens rather than after they notice the model forgot a file. The\n * segments are listed in the order they will be evicted, and the first one is\n * named in the warning.\n *\n * **Token counts are per-tokenizer.** The same text is a different number of\n * tokens on a different model, so a count carried over from another provider\n * is decoration. Count with the tokenizer of the model you are about to call,\n * or say the figure is an estimate.\n */\n\nexport type ContextSegment = {\n  label: string;\n  tokens: number;\n  /**\n   * Position in the eviction order — lower goes first. Segments that cannot\n   * be evicted (the system prompt, usually) should be left undefined.\n   */\n  evictionRank?: number;\n};\n\nexport type ContextUsageProps = {\n  /** Total window for the model, in tokens. */\n  window: number;\n  segments: ContextSegment[];\n  /** Tokens held back for the reply. */\n  reserve?: number;\n  /** True when the count came from a different tokenizer than the model's. */\n  estimated?: boolean;\n  className?: string;\n};\n\nconst COLORS = [\n  \"bg-blue-500\",\n  \"bg-violet-500\",\n  \"bg-teal-500\",\n  \"bg-amber-500\",\n  \"bg-rose-500\",\n  \"bg-lime-500\",\n];\n\nfunction formatTokens(n: number): string {\n  if (n < 1000) return String(n);\n  if (n < 1_000_000) return `${(n / 1000).toFixed(n < 10_000 ? 1 : 0)}k`;\n  return `${(n / 1_000_000).toFixed(1)}M`;\n}\n\nexport function ContextUsage({\n  window: windowSize,\n  segments,\n  reserve = 0,\n  estimated = false,\n  className = \"\",\n}: ContextUsageProps) {\n  const used = segments.reduce((sum, s) => sum + s.tokens, 0);\n  const usable = Math.max(0, windowSize - reserve);\n  const free = usable - used;\n  const overflowing = free < 0;\n  /* Tight at 85% of the USABLE window, not of the whole one — the difference\n     is exactly the reply, which is the thing that breaks first. */\n  const tight = !overflowing && used / usable > 0.85;\n\n  /* Named here rather than in the warning string so the two cannot disagree:\n     the first segment to go is the lowest eviction rank, and a segment with\n     no rank is not evictable at all. */\n  const evictable = segments\n    .filter((s) => s.evictionRank !== undefined)\n    .sort((a, b) => (a.evictionRank ?? 0) - (b.evictionRank ?? 0));\n  const firstOut = evictable[0];\n\n  return (\n    <div className={`rounded-xl border border-zinc-200 bg-white p-3.5 dark:border-zinc-800 dark:bg-zinc-900 ${className}`}>\n      <div className=\"flex flex-wrap items-baseline gap-x-2 gap-y-1\">\n        <span className=\"text-sm font-medium text-zinc-900 dark:text-zinc-100\">\n          {estimated && \"~\"}\n          {formatTokens(used)}\n        </span>\n        <span className=\"text-[11px] text-zinc-500 dark:text-zinc-400\">\n          of {formatTokens(usable)} usable\n          {reserve > 0 && ` · ${formatTokens(reserve)} held for the reply`}\n        </span>\n        <span\n          className={`ml-auto text-[11px] font-medium tabular-nums ${\n            overflowing\n              ? \"text-red-600 dark:text-red-400\"\n              : tight\n                ? \"text-amber-600 dark:text-amber-500\"\n                : \"text-zinc-400 dark:text-zinc-500\"\n          }`}\n        >\n          {overflowing ? `${formatTokens(-free)} over` : `${formatTokens(free)} left`}\n        </span>\n      </div>\n\n      {/* One track, segmented. Separate bars per segment would let each one\n          look full on its own scale, which is the opposite of the point. */}\n      <div className=\"mt-2 flex h-2 gap-px overflow-hidden rounded-full bg-zinc-100 dark:bg-zinc-800\">\n        {segments.map((s, i) => (\n          <div\n            key={s.label}\n            className={`h-full ${COLORS[i % COLORS.length]}`}\n            style={{ width: `${Math.min(100, (s.tokens / usable) * 100)}%` }}\n            title={`${s.label} — ${formatTokens(s.tokens)}`}\n          />\n        ))}\n        {reserve > 0 && !overflowing && (\n          <div\n            /* Striped rather than solid: it is not used, and it is not free\n               either. A solid block reads as another consumer. */\n            className=\"h-full bg-[repeating-linear-gradient(45deg,rgb(161_161_170/0.5)_0_3px,transparent_3px_6px)]\"\n            style={{ width: `${Math.max(0, (free / usable) * 100)}%` }}\n            title={`${formatTokens(free)} free`}\n          />\n        )}\n      </div>\n\n      <dl className=\"mt-2.5 space-y-1\">\n        {segments.map((s, i) => (\n          <div key={s.label} className=\"flex items-baseline gap-2 text-[11px]\">\n            <span className={`h-2 w-2 shrink-0 rounded-sm ${COLORS[i % COLORS.length]}`} />\n            <dt className=\"text-zinc-600 dark:text-zinc-300\">{s.label}</dt>\n            {s.evictionRank === undefined && (\n              <span className=\"rounded bg-zinc-100 px-1 text-[10px] text-zinc-500 dark:bg-zinc-800 dark:text-zinc-400\">\n                pinned\n              </span>\n            )}\n            <dd className=\"ml-auto shrink-0 tabular-nums text-zinc-500 dark:text-zinc-400\">\n              {formatTokens(s.tokens)}\n            </dd>\n          </div>\n        ))}\n      </dl>\n\n      {(tight || overflowing) && (\n        <p\n          className={`mt-2.5 border-t pt-2 text-[11px] leading-4 ${\n            overflowing\n              ? \"border-red-100 text-red-600 dark:border-red-900/40 dark:text-red-400\"\n              : \"border-zinc-100 text-amber-600 dark:border-zinc-800 dark:text-amber-500\"\n          }`}\n        >\n          {firstOut\n            ? `${overflowing ? \"Does not fit\" : \"Running out\"} — “${firstOut.label}” is dropped first.`\n            : `${overflowing ? \"Does not fit\" : \"Running out\"}, and nothing here is evictable.`}\n        </p>\n      )}\n\n      {estimated && (\n        <p className=\"mt-1.5 text-[11px] leading-4 text-zinc-400 dark:text-zinc-500\">\n          Counted with a different tokenizer than the model uses — treat it as an estimate.\n        </p>\n      )}\n    </div>\n  );\n}\n","type":"registry:ui","target":"components/ui/context-usage.tsx"}]}