From 1d334f5dcb803bdcf14f3e52e911ea39a2d9f09c Mon Sep 17 00:00:00 2001 From: Michael Yong Date: Wed, 5 Aug 2026 10:44:33 -0700 Subject: [PATCH 1/6] Fix provider usage limit normalization --- apps/host-daemon/src/provider-usage.test.ts | 67 ++++++++++++++++++- apps/host-daemon/src/provider-usage.ts | 45 ++++++++++++- packages/host-daemon-contract/src/commands.ts | 2 +- .../test/contract.test.ts | 10 +-- 4 files changed, 115 insertions(+), 9 deletions(-) diff --git a/apps/host-daemon/src/provider-usage.test.ts b/apps/host-daemon/src/provider-usage.test.ts index 7bc5538e82..0ac87b2fe9 100644 --- a/apps/host-daemon/src/provider-usage.test.ts +++ b/apps/host-daemon/src/provider-usage.test.ts @@ -87,6 +87,34 @@ describe("normalizeCodexUsage", () => { }); }); + it("labels a weekly primary window from its duration", () => { + const resetAt = 1_786_380_099; + expect( + normalizeCodexUsage({ + plan_type: "pro", + rate_limit: { + primary_window: { + used_percent: 8, + limit_window_seconds: 604_800, + reset_at: resetAt, + }, + secondary_window: null, + }, + }), + ).toEqual({ + status: "ok", + accountEmail: null, + planLabel: "Pro", + windows: [ + { + label: "Weekly limit", + usedPercent: 8, + resetsAt: new Date(resetAt * 1000).toISOString(), + }, + ], + }); + }); + it("flags a malformed payload instead of inventing numbers", () => { const result = normalizeCodexUsage({ rate_limit: { primary_window: { used_percent: "lots" } }, @@ -102,13 +130,29 @@ describe("normalizeClaudeUsage", () => { subscriptionType: "max", }; - it("maps the session and weekly windows and derives the plan label", () => { + it("maps session, weekly, and model-scoped windows and derives the plan label", () => { const result = normalizeClaudeUsage( { five_hour: { utilization: 0, resets_at: "2026-06-19T22:00:00.000Z" }, seven_day: { utilization: 18.4, resets_at: "2026-06-24T14:23:00.000Z" }, - // Model-specific sub-limits are intentionally ignored. seven_day_sonnet: { utilization: 0, resets_at: null }, + limits: [ + { + kind: "session", + scope: null, + percent: 0, + resets_at: "2026-06-19T22:00:00.000Z", + }, + { + kind: "weekly_scoped", + scope: { + model: { id: null, display_name: "Fable" }, + surface: null, + }, + percent: 48.2, + resets_at: "2026-06-24T14:22:59.000Z", + }, + ], }, credentials, "claude@example.com", @@ -129,6 +173,11 @@ describe("normalizeClaudeUsage", () => { usedPercent: 18, resetsAt: "2026-06-24T14:23:00.000Z", }, + { + label: "Fable", + usedPercent: 48, + resetsAt: "2026-06-24T14:22:59.000Z", + }, ], }); }); @@ -138,6 +187,20 @@ describe("normalizeClaudeUsage", () => { { five_hour: { utilization: 7, resets_at: null }, seven_day: { resets_at: "2026-06-24T14:23:00.000Z" }, + limits: [ + { + kind: "weekly_scoped", + scope: { model: null }, + percent: 25, + resets_at: "2026-06-24T14:23:00.000Z", + }, + { + kind: "weekly_scoped", + scope: { model: { display_name: "Fable" } }, + percent: null, + resets_at: "2026-06-24T14:23:00.000Z", + }, + ], }, { accessToken: "token" }, ); diff --git a/apps/host-daemon/src/provider-usage.ts b/apps/host-daemon/src/provider-usage.ts index 7e4f4f177c..380b90a6a3 100644 --- a/apps/host-daemon/src/provider-usage.ts +++ b/apps/host-daemon/src/provider-usage.ts @@ -107,11 +107,13 @@ function codexPlanLabel(planType: string | null | undefined): string | null { function codexWindow( window: z.infer | null | undefined, - label: string, + fallbackLabel: string, ): ProviderUsageWindow | null { if (!window) { return null; } + const label = + window.limit_window_seconds === 604_800 ? "Weekly limit" : fallbackLabel; return { label, usedPercent: clampPercent(window.used_percent), @@ -251,10 +253,26 @@ const claudeUsageWindowSchema = z.object({ resets_at: z.string().nullish(), }); +const claudeScopedUsageLimitSchema = z + .object({ + kind: z.string(), + scope: z + .object({ + model: z + .object({ display_name: z.string().trim().min(1).nullish() }) + .nullish(), + }) + .nullish(), + percent: z.number().nullish(), + resets_at: z.string().nullish(), + }) + .passthrough(); + const claudeUsageResponseSchema = z .object({ five_hour: claudeUsageWindowSchema.nullish(), seven_day: claudeUsageWindowSchema.nullish(), + limits: z.array(claudeScopedUsageLimitSchema).nullish(), }) .passthrough(); @@ -359,6 +377,30 @@ function claudeWindow( }; } +function claudeScopedWindows( + limits: z.infer[] | null | undefined, +): ProviderUsageWindow[] { + // `limits` repeats the aggregate session/week rows and adds model buckets. + // Only the model-scoped weekly rows are additive to the legacy top-level data. + return (limits ?? []).flatMap((limit) => { + const label = limit.scope?.model?.display_name; + if ( + limit.kind !== "weekly_scoped" || + label == null || + limit.percent == null + ) { + return []; + } + return [ + { + label, + usedPercent: clampPercent(limit.percent), + resetsAt: normalizeIsoTimestamp(limit.resets_at), + }, + ]; + }); +} + function normalizeClaudeUsage( raw: unknown, credentials: ClaudeCredentials, @@ -372,6 +414,7 @@ function normalizeClaudeUsage( const windows = [ claudeWindow(parsed.data.five_hour, "Current session"), claudeWindow(parsed.data.seven_day, "Weekly limit"), + ...claudeScopedWindows(parsed.data.limits), ].filter((window): window is ProviderUsageWindow => window !== null); return { diff --git a/packages/host-daemon-contract/src/commands.ts b/packages/host-daemon-contract/src/commands.ts index effee13b1c..5f5554bbf4 100644 --- a/packages/host-daemon-contract/src/commands.ts +++ b/packages/host-daemon-contract/src/commands.ts @@ -35,7 +35,7 @@ import { providerCliStatusResponseSchema, } from "./local.js"; -export const HOST_DAEMON_PROTOCOL_VERSION = 70 as const; +export const HOST_DAEMON_PROTOCOL_VERSION = 71 as const; export { BRANCH_LIST_LIMIT_MAX, diff --git a/packages/host-daemon-contract/test/contract.test.ts b/packages/host-daemon-contract/test/contract.test.ts index dfcbf64616..af9660c78c 100644 --- a/packages/host-daemon-contract/test/contract.test.ts +++ b/packages/host-daemon-contract/test/contract.test.ts @@ -1023,11 +1023,11 @@ describe("host-daemon local schemas", () => { }); describe("host-daemon command schemas", () => { - // Codex structured inference gained a required reasoning-effort field in - // version 70. Older daemons reject that strict command shape, so the bump - // forces an update before the server can send it. - it("uses protocol version 70 for explicit Codex inference reasoning", () => { - expect(HOST_DAEMON_PROTOCOL_VERSION).toBe(70); + // Provider usage gained Claude model-scoped windows and duration-aware Codex + // labels in version 71. Older daemons omit or mislabel them, so the bump + // forces an update before the server requests provider usage. + it("uses protocol version 71 for provider usage normalization", () => { + expect(HOST_DAEMON_PROTOCOL_VERSION).toBe(71); }); it("binds Plan cancellation to a required turn id and typed result", () => { From 9063d464f576aa1d8d9fb3f71e33103d1f1f290b Mon Sep 17 00:00:00 2001 From: Michael Yong Date: Wed, 5 Aug 2026 10:57:39 -0700 Subject: [PATCH 2/6] Clarify provider usage hierarchy --- .../UsageLimitsSettingsSection.test.tsx | 1 + .../settings/UsageLimitsSettingsSection.tsx | 90 +++++++++++++------ 2 files changed, 63 insertions(+), 28 deletions(-) diff --git a/apps/app/src/components/settings/UsageLimitsSettingsSection.test.tsx b/apps/app/src/components/settings/UsageLimitsSettingsSection.test.tsx index 7fed8317b4..0ae0874fe9 100644 --- a/apps/app/src/components/settings/UsageLimitsSettingsSection.test.tsx +++ b/apps/app/src/components/settings/UsageLimitsSettingsSection.test.tsx @@ -63,6 +63,7 @@ describe("UsageLimitsSettingsSectionContent", () => { }); expect(screen.getByRole("heading", { name: "Cursor" })).toBeDefined(); + expect(screen.getByRole("region", { name: "Cursor" })).toBeDefined(); expect(screen.getByText("cursor@example.com")).toBeDefined(); expect(screen.getByText("Plan usage")).toBeDefined(); expect(screen.getByText("50% used")).toBeDefined(); diff --git a/apps/app/src/components/settings/UsageLimitsSettingsSection.tsx b/apps/app/src/components/settings/UsageLimitsSettingsSection.tsx index 9689356f77..0bdf1ca2f7 100644 --- a/apps/app/src/components/settings/UsageLimitsSettingsSection.tsx +++ b/apps/app/src/components/settings/UsageLimitsSettingsSection.tsx @@ -6,7 +6,11 @@ import type { } from "@bb/host-daemon-contract"; import { Button } from "@bb/shared-ui/button"; import { Icon } from "@bb/shared-ui/icon"; -import { SettingsSection } from "@/components/ui/settings-section"; +import { + SettingsBadge, + SettingsRowList, + SettingsSection, +} from "@/components/ui/settings-section"; import { MachineStatusDot } from "@/components/machines/MachineStatusDot"; import { DropdownMenu, @@ -20,11 +24,16 @@ import { useSystemUsageLimits, } from "@/hooks/queries/system-queries"; import { selectPrimaryHost, useHosts } from "@/hooks/queries/host-queries"; +import { + getProviderIconColorClass, + getProviderIconInfo, +} from "@/lib/provider-icon"; import { cn } from "@bb/shared-ui/lib/utils"; interface ProviderConfig { key: "codex" | "claudeCode" | "cursor"; name: string; + providerId: "codex" | "claude-code" | "acp-cursor"; signInHint: string; expiredHint: string; } @@ -33,12 +42,14 @@ const PROVIDERS: ProviderConfig[] = [ { key: "codex", name: "Codex", + providerId: "codex", signInHint: "Run `codex` to sign in and see your usage.", expiredHint: "Your Codex session expired. Run `codex`, then reload usage.", }, { key: "claudeCode", name: "Claude Code", + providerId: "claude-code", signInHint: "Run `claude` to sign in and see your usage.", expiredHint: "Your Claude session expired. Run `claude`, then reload usage.", @@ -46,6 +57,7 @@ const PROVIDERS: ProviderConfig[] = [ { key: "cursor", name: "Cursor", + providerId: "acp-cursor", signInHint: "Run `cursor-agent login` to sign in and see your usage.", expiredHint: "Your Cursor session expired. Run `cursor-agent login`, then reload usage.", @@ -222,29 +234,52 @@ function ProviderUsageBlock({ }: ProviderUsageBlockProps) { const planLabel = usage?.status === "ok" ? usage.planLabel : null; const accountEmail = usage?.status === "ok" ? usage.accountEmail : null; + const iconInfo = getProviderIconInfo(config.providerId); + const ProviderIcon = iconInfo?.icon; + const headingId = `usage-provider-${config.key}`; return ( -
+
-
-

{config.name}

- {accountEmail ? ( -

- {accountEmail} -

+
+ {ProviderIcon ? ( + ) : null} +
+

+ {config.name} +

+ {accountEmail ? ( +

+ {accountEmail} +

+ ) : null} +
- {planLabel ? ( - {planLabel} - ) : null} + {planLabel ? {planLabel} : null}
- -
+
+ +
+
); } @@ -352,18 +387,17 @@ export function UsageLimitsSettingsSectionContent({
} > -
+ {visibleProviders.map((config) => ( -
- -
+ ))} -
+ ); } From 05eceb1604982ee79990d507c559ebb74fe9d627 Mon Sep 17 00:00:00 2001 From: Michael Yong Date: Wed, 5 Aug 2026 11:13:53 -0700 Subject: [PATCH 3/6] Harden Claude scoped usage parsing --- apps/host-daemon/src/provider-usage.test.ts | 73 +++++++++++++++++++++ apps/host-daemon/src/provider-usage.ts | 41 ++++++++---- 2 files changed, 100 insertions(+), 14 deletions(-) diff --git a/apps/host-daemon/src/provider-usage.test.ts b/apps/host-daemon/src/provider-usage.test.ts index 0ac87b2fe9..0ebb13f620 100644 --- a/apps/host-daemon/src/provider-usage.test.ts +++ b/apps/host-daemon/src/provider-usage.test.ts @@ -212,6 +212,79 @@ describe("normalizeClaudeUsage", () => { windows: [{ label: "Current session", usedPercent: 7, resetsAt: null }], }); }); + + it("keeps valid usage when one optional scoped row is malformed", () => { + const result = normalizeClaudeUsage( + { + five_hour: { utilization: 7, resets_at: null }, + seven_day: { utilization: 18, resets_at: null }, + limits: [ + { + kind: "weekly_scoped", + scope: { model: { display_name: 42 }, surface: null }, + percent: "lots", + resets_at: null, + }, + { + kind: "weekly_scoped", + scope: { model: { display_name: "Fable" }, surface: null }, + percent: 48, + resets_at: null, + }, + ], + }, + { accessToken: "token" }, + ); + + expect(result).toEqual({ + status: "ok", + accountEmail: null, + planLabel: null, + windows: [ + { label: "Current session", usedPercent: 7, resetsAt: null }, + { label: "Weekly limit", usedPercent: 18, resetsAt: null }, + { label: "Fable", usedPercent: 48, resetsAt: null }, + ], + }); + }); + + it("drops surface-scoped and duplicate model rows", () => { + const result = normalizeClaudeUsage( + { + limits: [ + { + kind: "weekly_scoped", + scope: { + model: { display_name: "Fable" }, + surface: { display_name: "Claude Code" }, + }, + percent: 20, + resets_at: null, + }, + { + kind: "weekly_scoped", + scope: { model: { display_name: "Fable" }, surface: null }, + percent: 48, + resets_at: null, + }, + { + kind: "weekly_scoped", + scope: { model: { display_name: "fable" }, surface: null }, + percent: 52, + resets_at: null, + }, + ], + }, + { accessToken: "token" }, + ); + + expect(result).toEqual({ + status: "ok", + accountEmail: null, + planLabel: null, + windows: [{ label: "Fable", usedPercent: 48, resetsAt: null }], + }); + }); }); describe("normalizeCursorUsage", () => { diff --git a/apps/host-daemon/src/provider-usage.ts b/apps/host-daemon/src/provider-usage.ts index 380b90a6a3..63da54a6b3 100644 --- a/apps/host-daemon/src/provider-usage.ts +++ b/apps/host-daemon/src/provider-usage.ts @@ -261,6 +261,9 @@ const claudeScopedUsageLimitSchema = z model: z .object({ display_name: z.string().trim().min(1).nullish() }) .nullish(), + // Surface-specific buckets need a distinct display identity. Until the + // provider documents that shape, accept only the aggregate model row. + surface: z.null().optional(), }) .nullish(), percent: z.number().nullish(), @@ -272,7 +275,10 @@ const claudeUsageResponseSchema = z .object({ five_hour: claudeUsageWindowSchema.nullish(), seven_day: claudeUsageWindowSchema.nullish(), - limits: z.array(claudeScopedUsageLimitSchema).nullish(), + limits: z + .array(claudeScopedUsageLimitSchema.nullable().catch(null)) + .nullish() + .catch([]), }) .passthrough(); @@ -378,27 +384,34 @@ function claudeWindow( } function claudeScopedWindows( - limits: z.infer[] | null | undefined, + limits: + | (z.infer | null)[] + | null + | undefined, ): ProviderUsageWindow[] { // `limits` repeats the aggregate session/week rows and adds model buckets. // Only the model-scoped weekly rows are additive to the legacy top-level data. - return (limits ?? []).flatMap((limit) => { - const label = limit.scope?.model?.display_name; + const windows: ProviderUsageWindow[] = []; + const seenLabels = new Set(); + for (const limit of limits ?? []) { + const label = limit?.scope?.model?.display_name; if ( + limit == null || limit.kind !== "weekly_scoped" || label == null || - limit.percent == null + limit.percent == null || + seenLabels.has(label.toLowerCase()) ) { - return []; + continue; } - return [ - { - label, - usedPercent: clampPercent(limit.percent), - resetsAt: normalizeIsoTimestamp(limit.resets_at), - }, - ]; - }); + seenLabels.add(label.toLowerCase()); + windows.push({ + label, + usedPercent: clampPercent(limit.percent), + resetsAt: normalizeIsoTimestamp(limit.resets_at), + }); + } + return windows; } function normalizeClaudeUsage( From 5ba86617b1df815a8da113f67f747e90b393859a Mon Sep 17 00:00:00 2001 From: Michael Yong Date: Wed, 5 Aug 2026 11:49:48 -0700 Subject: [PATCH 4/6] Fix nested router in automations story --- .../tools/Automations.stories.test.tsx | 22 +++++++++++++++++++ .../components/tools/Automations.stories.tsx | 14 ++++++------ 2 files changed, 29 insertions(+), 7 deletions(-) create mode 100644 apps/app/src/components/tools/Automations.stories.test.tsx diff --git a/apps/app/src/components/tools/Automations.stories.test.tsx b/apps/app/src/components/tools/Automations.stories.test.tsx new file mode 100644 index 0000000000..b62fd81d11 --- /dev/null +++ b/apps/app/src/components/tools/Automations.stories.test.tsx @@ -0,0 +1,22 @@ +// @vitest-environment jsdom + +import { cleanup, render, screen } from "@testing-library/react"; +import { MemoryRouter } from "react-router-dom"; +import { afterEach, describe, expect, it } from "vitest"; +import { BreadcrumbNavigation } from "./Automations.stories"; + +afterEach(cleanup); + +describe("Automations stories", () => { + it("uses the preview router for breadcrumb navigation", async () => { + render( + + + , + ); + + expect( + await screen.findByText("/plugins/automations/automations/browse"), + ).toBeDefined(); + }); +}); diff --git a/apps/app/src/components/tools/Automations.stories.tsx b/apps/app/src/components/tools/Automations.stories.tsx index c58e3e3a1a..3f2f250500 100644 --- a/apps/app/src/components/tools/Automations.stories.tsx +++ b/apps/app/src/components/tools/Automations.stories.tsx @@ -1,5 +1,5 @@ -import { useState, type CSSProperties, type ReactNode } from "react"; -import { Link, MemoryRouter, useLocation } from "react-router-dom"; +import { useEffect, useState, type CSSProperties, type ReactNode } from "react"; +import { Link, useLocation, useNavigate } from "react-router-dom"; import { AutomationDetailView } from "bb-plugin-automations/detail-view"; import { AutomationOverviewView, @@ -257,11 +257,11 @@ function BreadcrumbFlowHarness() { } export function BreadcrumbNavigation() { - return ( - - - - ); + const navigate = useNavigate(); + useEffect(() => { + navigate(`${AUTOMATIONS_ROOT}/browse`, { replace: true }); + }, [navigate]); + return ; } const DETAIL_AUTOMATION = automation("nightly-digest", "Nightly digest", { From 5064a6b414899a2b38bda36c8a1c29bf3a43f743 Mon Sep 17 00:00:00 2001 From: Michael Yong Date: Wed, 5 Aug 2026 11:58:59 -0700 Subject: [PATCH 5/6] Add usage settings state gallery --- ...sageLimitsSettingsSection.stories.test.tsx | 36 +++ .../UsageLimitsSettingsSection.stories.tsx | 255 ++++++++++++++++++ .../settings/UsageLimitsSettingsSection.tsx | 4 +- apps/app/src/views/SettingsView.stories.tsx | 4 +- 4 files changed, 294 insertions(+), 5 deletions(-) create mode 100644 apps/app/src/components/settings/UsageLimitsSettingsSection.stories.test.tsx create mode 100644 apps/app/src/components/settings/UsageLimitsSettingsSection.stories.tsx diff --git a/apps/app/src/components/settings/UsageLimitsSettingsSection.stories.test.tsx b/apps/app/src/components/settings/UsageLimitsSettingsSection.stories.test.tsx new file mode 100644 index 0000000000..9b25358b0b --- /dev/null +++ b/apps/app/src/components/settings/UsageLimitsSettingsSection.stories.test.tsx @@ -0,0 +1,36 @@ +// @vitest-environment jsdom + +import { cleanup, render, screen } from "@testing-library/react"; +import { TooltipProvider } from "@bb/shared-ui/tooltip"; +import { afterEach, describe, expect, it } from "vitest"; +import { Usage } from "./UsageLimitsSettingsSection.stories"; + +afterEach(cleanup); + +describe("settings/Settings Page/Usage story", () => { + it("renders the usage state gallery", () => { + render( + + + , + ); + + expect(screen.getByText("complete usage")).toBeDefined(); + expect(screen.getAllByText("Fable").length).toBeGreaterThan(0); + expect(screen.getByText("authentication")).toBeDefined(); + expect(screen.getByText("provider responses")).toBeDefined(); + expect(screen.getByText("loading")).toBeDefined(); + expect(screen.getByText("refreshing")).toBeDefined(); + expect(screen.getByText("unavailable")).toBeDefined(); + expect(screen.getByText("request failed")).toBeDefined(); + expect(screen.getByText("multiple machines")).toBeDefined(); + expect( + screen.getByRole("button", { name: "Usage limits machine" }), + ).toBeDefined(); + + const providerHeadingIds = screen + .getAllByRole("region") + .map((region) => region.getAttribute("aria-labelledby")); + expect(new Set(providerHeadingIds).size).toBe(providerHeadingIds.length); + }); +}); diff --git a/apps/app/src/components/settings/UsageLimitsSettingsSection.stories.tsx b/apps/app/src/components/settings/UsageLimitsSettingsSection.stories.tsx new file mode 100644 index 0000000000..d6bd454f7c --- /dev/null +++ b/apps/app/src/components/settings/UsageLimitsSettingsSection.stories.tsx @@ -0,0 +1,255 @@ +import { useState, type ReactNode } from "react"; +import type { Host } from "@bb/domain"; +import { StoryCard, StoryRow } from "../../../.ladle/story-card"; +import { + UsageLimitsSettingsSectionContent, + type UsageLimitsSettingsSectionContentProps, +} from "./UsageLimitsSettingsSection"; + +export default { + title: "settings/Settings Page", +}; + +type Usage = UsageLimitsSettingsSectionContentProps["usage"]; + +const noop = () => {}; + +function futureIso(minutesFromNow: number): string { + return new Date(Date.now() + minutesFromNow * 60_000).toISOString(); +} + +const HEALTHY_USAGE: Usage = { + codex: { + status: "ok", + accountEmail: "sawyer@example.com", + planLabel: "Pro", + windows: [ + { + label: "Weekly usage limit", + usedPercent: 8, + resetsAt: futureIso(5 * 24 * 60), + }, + ], + }, + claudeCode: { + status: "ok", + accountEmail: "sawyer@example.com", + planLabel: "Max (20x)", + windows: [ + { + label: "Current session", + usedPercent: 53, + resetsAt: futureIso(187), + }, + { + label: "All models", + usedPercent: 25, + resetsAt: futureIso(67), + }, + { + label: "Fable", + usedPercent: 48, + resetsAt: futureIso(67), + }, + ], + }, + cursor: { + status: "ok", + accountEmail: "sawyer@example.com", + planLabel: "Pro", + windows: [ + { + label: "Plan usage", + usedPercent: 72, + resetsAt: futureIso(14 * 24 * 60), + }, + { + label: "On-demand spend", + usedPercent: 25, + resetsAt: futureIso(14 * 24 * 60), + cost: { usedUsdCents: 1_250, limitUsdCents: 5_000 }, + }, + ], + }, +}; + +const AUTH_USAGE: Usage = { + codex: { status: "unauthenticated" }, + claudeCode: { status: "expired" }, + cursor: { status: "not_installed" }, +}; + +const EMPTY_AND_ERROR_USAGE: Usage = { + codex: { + status: "ok", + accountEmail: null, + planLabel: "Team", + windows: [], + }, + claudeCode: { + status: "error", + message: "Claude usage is temporarily unavailable.", + }, + cursor: { status: "not_installed" }, +}; + +const THRESHOLD_USAGE: Usage = { + codex: { + status: "ok", + accountEmail: "sawyer@example.com", + planLabel: "Pro", + windows: [ + { label: "Below warning", usedPercent: 79, resetsAt: null }, + { label: "Warning", usedPercent: 85, resetsAt: null }, + { label: "Critical", usedPercent: 98, resetsAt: null }, + ], + }, + claudeCode: { status: "not_installed" }, + cursor: { status: "not_installed" }, +}; + +const HOSTS: Host[] = [ + { + id: "host-macbook", + name: "MacBook Pro", + type: "persistent", + status: "connected", + lastSeenAt: 1_700_000_000_000, + maxPermissionMode: "full", + lastRejectedProtocolVersion: null, + createdAt: 1, + updatedAt: 2, + }, + { + id: "host-studio", + name: "Mac Studio", + type: "persistent", + status: "connected", + lastSeenAt: 1_700_000_000_000, + maxPermissionMode: "full", + lastRejectedProtocolVersion: null, + createdAt: 1, + updatedAt: 2, + }, + { + id: "host-build", + name: "Build machine", + type: "persistent", + status: "disconnected", + lastSeenAt: 1_700_000_000_000, + maxPermissionMode: "full", + lastRejectedProtocolVersion: null, + createdAt: 1, + updatedAt: 2, + }, +]; + +function Stage({ children }: { children: ReactNode }) { + return
{children}
; +} + +type UsagePreviewProps = Pick & + Partial< + Pick< + UsageLimitsSettingsSectionContentProps, + | "hosts" + | "isError" + | "isFetching" + | "isLoading" + | "onSelectHost" + | "selectedHostId" + > + >; + +function UsagePreview({ + usage, + hosts, + isError = false, + isFetching = false, + isLoading = false, + onSelectHost, + selectedHostId, +}: UsagePreviewProps) { + return ( + + + + ); +} + +function MultipleMachinesPreview() { + const [selectedHostId, setSelectedHostId] = useState(HOSTS[0]?.id ?? null); + + return ( + + ); +} + +export function Usage() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/apps/app/src/components/settings/UsageLimitsSettingsSection.tsx b/apps/app/src/components/settings/UsageLimitsSettingsSection.tsx index 0bdf1ca2f7..70153332f9 100644 --- a/apps/app/src/components/settings/UsageLimitsSettingsSection.tsx +++ b/apps/app/src/components/settings/UsageLimitsSettingsSection.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useId, useState } from "react"; import type { Host } from "@bb/domain"; import type { ProviderUsage, @@ -236,7 +236,7 @@ function ProviderUsageBlock({ const accountEmail = usage?.status === "ok" ? usage.accountEmail : null; const iconInfo = getProviderIconInfo(config.providerId); const ProviderIcon = iconInfo?.icon; - const headingId = `usage-provider-${config.key}`; + const headingId = useId(); return (
- - ); From 4840620f7839ac1707d2091ec2abb38ba91db610 Mon Sep 17 00:00:00 2001 From: Michael Yong Date: Wed, 5 Aug 2026 12:02:53 -0700 Subject: [PATCH 6/6] Tighten usage fallback state spacing --- ...sageLimitsSettingsSection.stories.test.tsx | 2 -- .../UsageLimitsSettingsSection.stories.tsx | 30 ---------------- .../UsageLimitsSettingsSection.test.tsx | 14 ++++++++ .../settings/UsageLimitsSettingsSection.tsx | 34 +++++++++++++------ 4 files changed, 38 insertions(+), 42 deletions(-) diff --git a/apps/app/src/components/settings/UsageLimitsSettingsSection.stories.test.tsx b/apps/app/src/components/settings/UsageLimitsSettingsSection.stories.test.tsx index 9b25358b0b..f3e5b42529 100644 --- a/apps/app/src/components/settings/UsageLimitsSettingsSection.stories.test.tsx +++ b/apps/app/src/components/settings/UsageLimitsSettingsSection.stories.test.tsx @@ -20,8 +20,6 @@ describe("settings/Settings Page/Usage story", () => { expect(screen.getByText("authentication")).toBeDefined(); expect(screen.getByText("provider responses")).toBeDefined(); expect(screen.getByText("loading")).toBeDefined(); - expect(screen.getByText("refreshing")).toBeDefined(); - expect(screen.getByText("unavailable")).toBeDefined(); expect(screen.getByText("request failed")).toBeDefined(); expect(screen.getByText("multiple machines")).toBeDefined(); expect( diff --git a/apps/app/src/components/settings/UsageLimitsSettingsSection.stories.tsx b/apps/app/src/components/settings/UsageLimitsSettingsSection.stories.tsx index d6bd454f7c..5961230614 100644 --- a/apps/app/src/components/settings/UsageLimitsSettingsSection.stories.tsx +++ b/apps/app/src/components/settings/UsageLimitsSettingsSection.stories.tsx @@ -93,21 +93,6 @@ const EMPTY_AND_ERROR_USAGE: Usage = { cursor: { status: "not_installed" }, }; -const THRESHOLD_USAGE: Usage = { - codex: { - status: "ok", - accountEmail: "sawyer@example.com", - planLabel: "Pro", - windows: [ - { label: "Below warning", usedPercent: 79, resetsAt: null }, - { label: "Warning", usedPercent: 85, resetsAt: null }, - { label: "Critical", usedPercent: 98, resetsAt: null }, - ], - }, - claudeCode: { status: "not_installed" }, - cursor: { status: "not_installed" }, -}; - const HOSTS: Host[] = [ { id: "host-macbook", @@ -208,12 +193,6 @@ export function Usage() { > - - - - - - - - - { expect(screen.getByRole("heading", { name: "Codex" })).toBeDefined(); }); + it("keeps states without usage bars with the provider heading", () => { + renderContent({ + usage: { codex: { status: "unauthenticated" } }, + isLoading: false, + isError: false, + isFetching: false, + onRefresh: vi.fn(), + }); + + const heading = screen.getByRole("heading", { name: "Codex" }); + const status = screen.getByText(/Run `codex` to sign in/u); + expect(heading.parentElement?.contains(status)).toBe(true); + }); + it("selects which connected machine supplies usage", () => { const onSelectHost = vi.fn(); renderContent({ diff --git a/apps/app/src/components/settings/UsageLimitsSettingsSection.tsx b/apps/app/src/components/settings/UsageLimitsSettingsSection.tsx index 70153332f9..10742f633a 100644 --- a/apps/app/src/components/settings/UsageLimitsSettingsSection.tsx +++ b/apps/app/src/components/settings/UsageLimitsSettingsSection.tsx @@ -237,6 +237,8 @@ function ProviderUsageBlock({ const iconInfo = getProviderIconInfo(config.providerId); const ProviderIcon = iconInfo?.icon; const headingId = useId(); + const showsUsageWindows = + !isError && usage?.status === "ok" && usage.windows.length > 0; return (
-
+
{ProviderIcon ? ( ) : null} -
+

) : null} + {!showsUsageWindows ? ( +
+ +
+ ) : null}

{planLabel ? {planLabel} : null}
-
- -
+ {showsUsageWindows ? ( +
+ +
+ ) : null}
); }