From 4da273ba3c6ad917b518ccc431f7a744d3520cf7 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Thu, 19 Mar 2026 17:40:58 -0400 Subject: [PATCH] feat(site): add usage indicator to agents sidebar (#23307) --- site/src/api/api.ts | 8 ++ site/src/api/queries/chats.ts | 11 ++ site/src/pages/AgentsPage/AgentsSidebar.tsx | 8 +- .../pages/AgentsPage/ChatCostSummaryView.tsx | 2 +- .../AgentsPage/UsageIndicator.stories.tsx | 92 +++++++++++++++ site/src/pages/AgentsPage/UsageIndicator.tsx | 110 ++++++++++++++++++ 6 files changed, 227 insertions(+), 4 deletions(-) create mode 100644 site/src/pages/AgentsPage/UsageIndicator.stories.tsx create mode 100644 site/src/pages/AgentsPage/UsageIndicator.tsx diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 6b28f637e0..b5ba39a16d 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -3307,6 +3307,14 @@ class ApiMethods { return response.data; }; + getChatUsageLimitStatus = + async (): Promise => { + const response = await this.axios.get( + "/api/experimental/chats/usage-limits/status", + ); + return response.data; + }; + updateChatUsageLimitConfig = async ( req: TypesGen.ChatUsageLimitConfig, ): Promise => { diff --git a/site/src/api/queries/chats.ts b/site/src/api/queries/chats.ts index 45d371363d..8cac0be230 100644 --- a/site/src/api/queries/chats.ts +++ b/site/src/api/queries/chats.ts @@ -560,6 +560,17 @@ export const prInsights = (params?: { staleTime: 60_000, }); +export const chatUsageLimitStatusKey = [ + ...chatsKey, + "usageLimitStatus", +] as const; + +export const chatUsageLimitStatus = () => ({ + queryKey: chatUsageLimitStatusKey, + queryFn: () => API.getChatUsageLimitStatus(), + refetchInterval: 60_000, +}); + const chatUsageLimitConfigKey = [...chatsKey, "usageLimitConfig"] as const; export const chatUsageLimitConfig = () => ({ diff --git a/site/src/pages/AgentsPage/AgentsSidebar.tsx b/site/src/pages/AgentsPage/AgentsSidebar.tsx index b2c582862d..72956f8ed6 100644 --- a/site/src/pages/AgentsPage/AgentsSidebar.tsx +++ b/site/src/pages/AgentsPage/AgentsSidebar.tsx @@ -72,6 +72,7 @@ import { cn } from "utils/cn"; import { shortRelativeTime } from "utils/time"; import { getNormalizedModelRef } from "./modelOptions"; import { getTimeGroup, TIME_GROUPS } from "./timeGroups"; +import { UsageIndicator } from "./UsageIndicator"; type SidebarView = | { panel: "chats" } @@ -898,8 +899,9 @@ export const AgentsSidebar: FC = (props) => {
-
+
+ {" "}
- - {/* ── Panel 2: Sub-navigation (Settings) ── */} + {/* ── Panel 2: Sub-navigation (Settings) ── */}{" "}
{ if (!period) { diff --git a/site/src/pages/AgentsPage/UsageIndicator.stories.tsx b/site/src/pages/AgentsPage/UsageIndicator.stories.tsx new file mode 100644 index 0000000000..149804817f --- /dev/null +++ b/site/src/pages/AgentsPage/UsageIndicator.stories.tsx @@ -0,0 +1,92 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { chatUsageLimitStatusKey } from "api/queries/chats"; +import type { FC } from "react"; +import { useQueryClient } from "react-query"; +import { UsageIndicator } from "./UsageIndicator"; + +const withUsageLimitStatus = + (status: { + is_limited: boolean; + period?: "day" | "week" | "month"; + spend_limit_micros?: number; + current_spend: number; + period_start?: string; + period_end?: string; + }) => + (Story: FC) => { + const queryClient = useQueryClient(); + queryClient.setQueryData(chatUsageLimitStatusKey, status); + return ; + }; + +const periodStart = new Date().toISOString(); +const periodEnd = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(); + +const meta: Meta = { + title: "pages/AgentsPage/UsageIndicator", + component: UsageIndicator, +}; + +export default meta; +type Story = StoryObj; + +export const LowUsage: Story = { + decorators: [ + withUsageLimitStatus({ + is_limited: true, + period: "month", + spend_limit_micros: 50_000_000, + current_spend: 12_500_000, + period_start: periodStart, + period_end: periodEnd, + }), + ], +}; + +export const MediumUsage: Story = { + decorators: [ + withUsageLimitStatus({ + is_limited: true, + period: "week", + spend_limit_micros: 20_000_000, + current_spend: 16_000_000, + period_start: periodStart, + period_end: periodEnd, + }), + ], +}; + +export const HighUsage: Story = { + decorators: [ + withUsageLimitStatus({ + is_limited: true, + period: "day", + spend_limit_micros: 10_000_000, + current_spend: 9_500_000, + period_start: periodStart, + period_end: periodEnd, + }), + ], +}; + +export const LimitExceeded: Story = { + decorators: [ + withUsageLimitStatus({ + is_limited: true, + period: "month", + spend_limit_micros: 30_000_000, + current_spend: 32_000_000, + period_start: periodStart, + period_end: periodEnd, + }), + ], +}; + +export const NotLimited: Story = { + decorators: [ + withUsageLimitStatus({ + is_limited: false, + current_spend: 0, + }), + ], +}; diff --git a/site/src/pages/AgentsPage/UsageIndicator.tsx b/site/src/pages/AgentsPage/UsageIndicator.tsx new file mode 100644 index 0000000000..2598ff1087 --- /dev/null +++ b/site/src/pages/AgentsPage/UsageIndicator.tsx @@ -0,0 +1,110 @@ +import { chatUsageLimitStatus } from "api/queries/chats"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "components/DropdownMenu/DropdownMenu"; +import dayjs from "dayjs"; +import type { FC } from "react"; +import { useQuery } from "react-query"; +import { Link } from "react-router"; +import { formatCostMicros } from "utils/currency"; +import { getUsageLimitPeriodLabel } from "./ChatCostSummaryView"; + +export const UsageIndicator: FC = () => { + const { data, isLoading, isError } = useQuery(chatUsageLimitStatus()); + + if (isLoading || isError || !data?.is_limited) { + return null; + } + + const spendLimit = data.spend_limit_micros ?? 0; + const currentSpend = data.current_spend; + const percent = + spendLimit > 0 ? Math.min((currentSpend / spendLimit) * 100, 100) : 0; + const roundedPercent = Math.round(percent); + const exceeded = spendLimit > 0 && currentSpend >= spendLimit; + const periodLabel = getUsageLimitPeriodLabel(data.period); + + return ( + + + + + + + {/* Header */} +
+ + {periodLabel} Usage + + + {roundedPercent}% + +
+ + {/* Progress bar */} +
+
+
+
+
+ + {/* Spend detail */} +
+ {formatCostMicros(currentSpend)} of {formatCostMicros(spendLimit)}{" "} + used + {exceeded && ( + + — limit exceeded + + )} +
+ + {data.period_end && ( +
+ Resets {dayjs(data.period_end).format("MMM D, YYYY")} +
+ )} + + + + + View details + + + + ); +};