mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user