From dcf3cce51c80f59a1e5f52c289e3975cebd73708 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Tue, 21 Jul 2026 18:24:35 +0200 Subject: [PATCH] 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. --- .../ContextUsageIndicator.stories.tsx | 59 +++++++++++++++++++ .../components/ContextUsageIndicator.tsx | 22 ++++--- 2 files changed, 74 insertions(+), 7 deletions(-) diff --git a/site/src/pages/AgentsPage/components/ContextUsageIndicator.stories.tsx b/site/src/pages/AgentsPage/components/ContextUsageIndicator.stories.tsx index 22b8c3ed58..50f29774f9 100644 --- a/site/src/pages/AgentsPage/components/ContextUsageIndicator.stories.tsx +++ b/site/src/pages/AgentsPage/components/ContextUsageIndicator.stories.tsx @@ -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 = { diff --git a/site/src/pages/AgentsPage/components/ContextUsageIndicator.tsx b/site/src/pages/AgentsPage/components/ContextUsageIndicator.tsx index 0111490ef4..f50a61c3b1 100644 --- a/site/src/pages/AgentsPage/components/ContextUsageIndicator.tsx +++ b/site/src/pages/AgentsPage/components/ContextUsageIndicator.tsx @@ -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<{
{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<{
)} {hasContextList && ( -
+
{fileItems.length > 0 && (