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 && (