fix(site/src/pages/AgentsPage): clarify empty context usage popover state (#27389)

## Summary

The context usage badge popover showed "Context usage unavailable"
before any assistant message reported token usage, which read like an
error. This changes the empty-state copy to "Context usage will appear
after sending a message." and fixes a spacing bug that state exposed:
the pinned context list's top margin was conditional on a usage
percentage being present, so without usage data the list sat flush
against the message.

## Changes

- `ContextUsageIndicator`: new empty-state copy; unconditional `mt-2` on
the context list.
- Stories: `NoUsage` (message only) and `NoUsageWithContext` (message
plus pinned resource list), both asserting the new copy via hover
interactions.

## Testing

- Storybook interaction stories cover both empty states.
- `biome check` and `tsc --noEmit` pass; verified visually in Storybook.

> This PR was authored by Mux, an AI coding agent, acting on Mike's
behalf.
This commit is contained in:
Michael Suchacz
2026-07-21 16:24:35 +00:00
committed by GitHub
parent 2b2a5c963a
commit dcf3cce51c
2 changed files with 74 additions and 7 deletions
@@ -204,6 +204,65 @@ export const Dirty: Story = {
},
};
// Before any assistant message reports token usage there is no percentage,
// so the popover explains when the numbers will appear.
export const NoUsage: Story = {
args: {
usage: null,
},
play: async ({ canvasElement }) => {
const button = within(canvasElement).getByRole("button");
await userEvent.hover(button);
const body = within(document.body);
await waitFor(() =>
expect(
body.getByText("Context usage will appear after sending a message."),
).toBeVisible(),
);
},
};
// Some providers report usage without token counts, so no percentage can be
// computed even though a message was sent. The popover must say the usage is
// unavailable instead of promising numbers after the next message.
export const UsageWithoutTokenCounts: Story = {
args: {
usage: {
contextLimitTokens: 200_000,
},
},
play: async ({ canvasElement }) => {
const button = within(canvasElement).getByRole("button");
await userEvent.hover(button);
const body = within(document.body);
await waitFor(() =>
expect(body.getByText("Context usage unavailable")).toBeVisible(),
);
},
};
// A fresh chat has pinned context before any assistant message reports token
// usage, so the popover pairs the empty-usage copy with the resource list.
export const NoUsageWithContext: Story = {
args: {
usage: {
context: MockChatContextClean,
},
},
play: async ({ canvasElement }) => {
const button = within(canvasElement).getByRole("button");
await userEvent.hover(button);
const body = within(document.body);
await waitFor(() =>
expect(
body.getByText("Context usage will appear after sending a message."),
).toBeVisible(),
);
expect(body.getByText("Context files")).toBeVisible();
expect(body.getByText("AGENTS.md")).toBeVisible();
},
};
// Snapshot-level error: the ring shows a distinct error treatment and the
// popover surfaces the error message.
export const SnapshotError: Story = {
@@ -229,6 +229,17 @@ export const ContextUsageIndicator: FC<{
? (usedTokens / contextLimitTokens) * 100
: null;
const hasPercent = percentUsed !== null;
// Providers may report usage without token counts. Only a chat with no
// reported usage at all should promise numbers after the next message.
const hasReportedUsage = [
usage?.usedTokens,
usage?.contextLimitTokens,
usage?.inputTokens,
usage?.outputTokens,
usage?.cacheReadTokens,
usage?.cacheCreationTokens,
usage?.reasoningTokens,
].some(hasFiniteTokenValue);
const percentLabel =
percentUsed === null ? "--" : `${Math.round(percentUsed)}%`;
const clampedPercent = hasPercent
@@ -337,7 +348,9 @@ export const ContextUsageIndicator: FC<{
<div className="text-xs text-content-primary">
{hasPercent
? `${percentLabel} - ${formatTokenCountCompact(usedTokens)} / ${formatTokenCountCompact(contextLimitTokens)} context used`
: "Context usage unavailable"}
: hasReportedUsage
? "Context usage unavailable"
: "Context usage will appear after sending a message."}
{hasPercent &&
usage?.compressionThreshold !== undefined &&
usage.compressionThreshold > 0 && (
@@ -346,12 +359,7 @@ export const ContextUsageIndicator: FC<{
</div>
)}
{hasContextList && (
<div
className={cn(
"flex flex-col gap-2 text-content-secondary",
hasPercent && "mt-2",
)}
>
<div className="mt-2 flex flex-col gap-2 text-content-secondary">
{fileItems.length > 0 && (
<div className="flex flex-col gap-1">
<span className="font-medium text-content-primary">