Elements

Elements · AUI connected · AUI

Context display

Model context usage as a ring, bar, or text value with a detailed hover view.

Ring
Low (42%)
Warning (72%)
Critical (91%)
Bar
53.8k (42%)
92.2k (72%)
116.5k (91%)
Text
53.8k / 128.0k
92.2k / 128.0k
116.5k / 128.0k
On hover
Usage72%
Input72.3k
Cached41.2k
Output12.7k
Reasoning8.4k
Total92.2k / 128.0k
fig. 01

Installation

npx shadcn@latest add "@assistant-ui/context-display"
First time? Set up a runtime

Runtime components read their state from an assistant-ui runtime. Add one to an existing project:

npx assistant-ui@latest init

Then wrap your app in a runtime provider:

import { AssistantRuntimeProvider } from "@assistant-ui/react";
import { useChatRuntime, AssistantChatTransport } from "@assistant-ui/ai-sdk";

export default function App() {
  const runtime = useChatRuntime({
    transport: new AssistantChatTransport({ api: "/api/chat" }),
  });

  return (
    <AssistantRuntimeProvider runtime={runtime}>
      {/* your components */}
    </AssistantRuntimeProvider>
  );
}

The installation guide covers new projects, templates, and API routes.

Context display turns a model's token usage into a small gauge with three faces: a ring, a bar, or a plain text fraction, each opening the same tooltip breakdown on hover. With a runtime it reads usage off the latest assistant message; standalone you hand it a usage object yourself.

Getting started

Runtime usage needs two things: your server actually returning token counts, and a preset mounted somewhere in the UI.

Forward token usage from your route handler

useThreadTokenUsage() reads usage off the latest assistant message's metadata, so your AI SDK route has to attach it. Return usage on the finish step and modelId on finish-step through messageMetadata:

app/api/chat/route.ts
import { streamText, convertToModelMessages } from "ai";

export async function POST(req: Request) {
  const { messages } = await req.json();
  const result = streamText({
    model: getModel(),
    messages: await convertToModelMessages(messages),
  });
  return result.toUIMessageStreamResponse({
    messageMetadata: ({ part }) => {
      if (part.type === "finish") return { usage: part.totalUsage };
      if (part.type === "finish-step")
        return { modelId: part.response.modelId };
      return undefined;
    },
  });
}

Without this step every preset reads a total of zero and the tooltip's breakdown stays empty.

Mount a preset

Pick Ring, Bar, or Text and pass your model's modelContextWindow. Each preset fetches usage internally through useThreadTokenUsage() and restarts its running total whenever the active thread's id changes.

components/assistant-ui/elements/thread.aui.tsx
import { ContextDisplay } from "@/components/assistant-ui/elements/context-display.aui";

function ThreadFooter() {
  return (
    <div className="flex items-center justify-end px-3 py-1.5">
      <ContextDisplay.Bar modelContextWindow={128000} />
    </div>
  );
}

Thread does not mount a preset by default; place one in a footer, the composer rail, or a sidebar.

Anatomy

Every preset is the same shape: a trigger wrapped in a shared tooltip.

<button data-slot="context-display-trigger">
  {/* Ring: SVG donut + percent. Bar: fill bar + token count. Text: "12k / 128k" */}
</button>
<div data-slot="context-display-popover">
  <div>Context usage   <span>{/* used of total */}</span></div>
  <div>{/* progress bar, min-width 1px once usage is nonzero */}</div>
  <div>Input           <span>{/* only when > 0 */}</span></div>
  <div>Cached input    <span>{/* only when > 0 */}</span></div>
  <div>Output          <span>{/* only when > 0 */}</span></div>
  <div>Reasoning       <span>{/* only when > 0 */}</span></div>
</div>

The breakdown only lists segments with a nonzero token count, so a usage object that carries only totalTokens shows the summary line with no rows underneath it. Percent is clamped to 100 even when usage exceeds the context window. The running total is sticky rather than reactive to every update: it only moves when the incoming total is itself nonzero, or when resetKey changes. A momentary usage of undefined (or zero) between turns does not flash the number back down, but a changed resetKey snaps it immediately to whatever the new usage reports.

Examples

Three presets

Each preset wraps Root, Trigger, and Content with one specific visual. Ring draws an SVG donut with a percent label, Bar draws a fill bar with a token count beside it, and Text prints a plain fraction with no severity color at all. Ring and Bar both shift color at the same two thresholds: the default tone below 65% usage, amber from 65% to 85%, and red above 85%.

<ContextDisplay.Ring modelContextWindow={128000} />
<ContextDisplay.Bar modelContextWindow={128000} />
<ContextDisplay.Text modelContextWindow={128000} />

Any preset also accepts usage directly. Supplying it skips the internal useThreadTokenUsage() fetch and, along with it, the automatic thread-id reset key. This is useful when you already have usage from elsewhere and want to drive the badge yourself without giving up the runtime wiring for anything else on the page.

Compose your own visual

Root, Trigger, and Content are exported on their own for a fully custom trigger visual. Root computes the shared percent and segment breakdown for Content's tooltip, but that computation is internal to the module. A custom child passed to Trigger renders whatever you put there, so pair it with your own modelContextWindow/usage math rather than expecting it to read Root's numbers.

<ContextDisplay.Root modelContextWindow={128000}>
  <ContextDisplay.Trigger aria-label="Context usage">
    <MyCustomGauge />
  </ContextDisplay.Trigger>
  <ContextDisplay.Content side="top" />
</ContextDisplay.Root>

Restyle the trigger

Both lanes take className on the trigger, and side controls which edge the popover opens toward.

<ContextDisplay.Bar className="px-1" side="bottom" modelContextWindow={128000} />

API reference

Preset props (Ring, Bar, Text)

PropTypeDefaultDescription
modelContextWindownumberrequiredToken limit used to compute the percentage.
classNamestringMerged onto the trigger.
side"top" | "bottom" | "left" | "right""top"Tooltip placement.
usageTokenUsageSupply usage directly to skip the internal fetch; when set, the preset also skips its automatic thread-id reset key.

Composable Root

PropTypeDescription
modelContextWindownumberToken limit used to compute the percentage.
childrenReactNodeRequired; typically Trigger and Content.
usageTokenUsageSame override behavior as on the presets above.

Trigger and Content take the exact same props in both lanes; see their tables under Standalone below.

Thread state

SelectorTypeDescription
useThreadTokenUsage()ThreadTokenUsage | undefinedUsage extracted from the latest assistant message that carries any, read from metadata.usage, a legacy metadata.custom.usage, or summed across metadata.steps.
s.threadListItem.idstringUsed internally as each preset's reset key, so switching threads restarts the running total.