diff --git a/site/src/pages/AgentsPage/components/ChatsSidebar/chats/UserSidebarFooter.tsx b/site/src/pages/AgentsPage/components/ChatsSidebar/chats/UserSidebarFooter.tsx
index 2eb0ae1970..53533f5dbc 100644
--- a/site/src/pages/AgentsPage/components/ChatsSidebar/chats/UserSidebarFooter.tsx
+++ b/site/src/pages/AgentsPage/components/ChatsSidebar/chats/UserSidebarFooter.tsx
@@ -16,11 +16,13 @@ export const UserSidebarFooter: FC = () => {
return (
-
+ {/* This footer is resizable, so child sizing must follow its container width instead of the viewport. */}
+
{
src={user.avatar_url}
size="sm"
/>
-
+
{user.name || user.username}
diff --git a/site/src/pages/AgentsPage/components/UsageIndicator.stories.tsx b/site/src/pages/AgentsPage/components/UsageIndicator.stories.tsx
index 6f024a07fb..dbdb6c4bf4 100644
--- a/site/src/pages/AgentsPage/components/UsageIndicator.stories.tsx
+++ b/site/src/pages/AgentsPage/components/UsageIndicator.stories.tsx
@@ -1,4 +1,4 @@
-import type { Meta, StoryObj } from "@storybook/react-vite";
+import type { Decorator, Meta, StoryObj } from "@storybook/react-vite";
import type { FC } from "react";
import { useQueryClient } from "react-query";
import { expect, userEvent, within } from "storybook/test";
@@ -57,11 +57,22 @@ const withUnavailableWorkspaceCount = (Story: FC) => {
return ;
};
-const withUsageIndicatorFrame = (Story: FC) => (
-
-
-
-);
+// Mirrors the sidebar footer wrapper: a fixed-width container with
+// container-type set so the trigger inside reacts to the wrapper's width
+// instead of the viewport's.
+const withUsageIndicatorFrame = (
+ widthClassName = "w-[320px]",
+ frameTestId?: string,
+): Decorator => {
+ return (Story) => (
+
+
+
+ );
+};
const openUsageMenu = async (canvasElement: HTMLElement) => {
const canvas = within(canvasElement);
@@ -104,7 +115,7 @@ const meta: Meta = {
decorators: [
withAuthProvider,
withDashboardProvider,
- withUsageIndicatorFrame,
+ withUsageIndicatorFrame(),
],
parameters: {
user: MockUserOwner,
@@ -167,6 +178,9 @@ export const WorkspaceQuotaOnly: Story = {
withWorkspaceCount(3),
],
play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+
+ expect(canvas.getByText("30/100")).toBeVisible();
await openUsageMenu(canvasElement);
},
};
@@ -181,12 +195,44 @@ export const UsageAndWorkspaceQuota: Story = {
const canvas = within(canvasElement);
const progressBars = canvas.getAllByRole("progressbar");
- expect(canvas.getByText("Usage")).toBeInTheDocument();
+ expect(canvas.getByRole("button", { name: "Usage" })).toBeVisible();
+ expect(canvas.getByText("$12.50")).toBeVisible();
+ expect(canvas.getByText("30/100")).toBeVisible();
expect(progressBars.map((bar) => bar.getAttribute("aria-label"))).toEqual([
"Monthly spend usage",
"Workspace quota usage",
]);
- await userEvent.click(canvas.getByRole("button"));
+ await openUsageMenu(canvasElement);
+ },
+};
+
+// The tiny story covers the responsive edge case where the trigger keeps
+// the bars visible and drops the numeric details.
+const expectTriggerContentFits = (canvasElement: HTMLElement) => {
+ const canvas = within(canvasElement);
+ const frame = canvas.getByTestId("usage-indicator-frame");
+
+ expect(canvas.getByRole("button", { name: "Usage" })).toBeVisible();
+ expect(frame.scrollWidth).toBeLessThanOrEqual(frame.clientWidth);
+};
+
+const expectTriggerDetailsHidden = (canvasElement: HTMLElement) => {
+ const canvas = within(canvasElement);
+
+ expect(canvas.getByText("$12.50")).not.toBeVisible();
+ expect(canvas.getByText("30/100")).not.toBeVisible();
+};
+
+export const TriggerTiny: Story = {
+ decorators: [
+ withUsageIndicatorFrame("w-[240px]", "usage-indicator-frame"),
+ withUsageLimitStatus(limitedUsageStatus()),
+ withWorkspaceQuota(defaultWorkspaceQuota),
+ withWorkspaceCount(3),
+ ],
+ play: ({ canvasElement }) => {
+ expectTriggerContentFits(canvasElement);
+ expectTriggerDetailsHidden(canvasElement);
},
};
@@ -220,7 +266,7 @@ export const WorkspaceQuotaWithoutBudget: Story = {
name: "Workspace quota usage",
});
- expect(canvas.getByText("Workspace quota")).toBeInTheDocument();
+ expect(canvas.getByText("20")).toBeInTheDocument();
expect(progressbar).toHaveAttribute("aria-valuenow", "100");
await openUsageMenu(canvasElement);
diff --git a/site/src/pages/AgentsPage/components/UsageIndicator.tsx b/site/src/pages/AgentsPage/components/UsageIndicator.tsx
index 2a07878f12..00516e6260 100644
--- a/site/src/pages/AgentsPage/components/UsageIndicator.tsx
+++ b/site/src/pages/AgentsPage/components/UsageIndicator.tsx
@@ -1,5 +1,5 @@
import dayjs from "dayjs";
-import { InfoIcon } from "lucide-react";
+import { CoinsIcon, InfoIcon, ServerIcon } from "lucide-react";
import { type FC, Fragment, type ReactNode } from "react";
import { useQuery } from "react-query";
import { Link } from "react-router";
@@ -36,6 +36,8 @@ type UsageSectionData = {
progressLabel: string;
percent: number;
detail: ReactNode;
+ icon: ReactNode;
+ summaryValue: string;
secondaryDetail?: ReactNode;
tooltip?: ReactNode;
severity?: UsageSeverity;
@@ -79,6 +81,8 @@ export const UsageIndicator: FC = () => {
progressLabel: `${periodLabel} spend usage`,
percent: getPercent(currentSpend, spendLimit),
severity: getSeverity(currentSpend, spendLimit),
+ icon: ,
+ summaryValue: formatCostMicros(currentSpend),
detail: (
<>
{formatCostMicros(currentSpend)} of {formatCostMicros(spendLimit)}{" "}
@@ -112,6 +116,11 @@ export const UsageIndicator: FC = () => {
progressLabel: "Workspace quota usage",
percent: getPercent(creditsConsumed, quota.budget),
severity: getSeverity(creditsConsumed, quota.budget),
+ icon: ,
+ summaryValue:
+ quota.budget > 0
+ ? `${formatNumber(creditsConsumed)}/${formatNumber(quota.budget)}`
+ : formatNumber(creditsConsumed),
detail: quotaDetail,
tooltip:
"Workspaces, stopped or running, may consume credits. Stop or delete unused ones to free quota.",
@@ -128,7 +137,7 @@ export const UsageIndicator: FC = () => {
const UsageMenu: FC<{ sections: readonly UsageSectionData[] }> = ({
sections,
}) => {
- const triggerLabel =
+ const triggerAriaLabel =
sections.length > 1 ? "Usage" : (sections[0]?.title ?? "Usage");
return (
@@ -136,11 +145,9 @@ const UsageMenu: FC<{ sections: readonly UsageSectionData[] }> = ({
-
- {triggerLabel}
-
@@ -166,19 +173,35 @@ const UsageMenu: FC<{ sections: readonly UsageSectionData[] }> = ({
const UsageTriggerProgress: FC<{ sections: readonly UsageSectionData[] }> = ({
sections,
}) => {
- const size = sections.length > 1 ? "compact" : "default";
-
return (
-
+
{sections.map((section) => (
-
+
+
+ {section.icon}
+
+
+
+ {section.summaryValue}
+
+
))}
);