feat(site): show total agent hours consumption on the licenses page (#27953)

<img width="1195" height="165" alt="Screenshot 2026-08-19 at 5 31 17 PM"
src="https://github.com/user-attachments/assets/d1e49e5f-1af6-4221-87fe-ae40997f0ae7"
/>

Adds a "Total agent hours" consumption panel to the Licenses page,
surfacing the `agent_runtime_hours` entitlement introduced by the #27983
/ #27984 / #27985 stack.

The new `TotalAgentHoursCard` renders full width directly above the
Agent Workspace Builds panel. It shows a usage bar with Used/Limit
labels and an info tooltip next to the title. The bar color and tooltip
copy track the same thresholds the backend uses for license warnings:
green below the soft limit, orange once usage reaches the soft limit,
and red once usage reaches the purchased allocation (values at or beyond
the threshold count as reached, matching
`appendAgentRuntimeHoursWarning`). When a soft limit exists, the tooltip
states the actual soft-limit percentage computed from `soft_limit /
limit`. The panel is hidden entirely when the feature is disabled, which
is how the backend reports a zero-hour allocation.

The usage period dates from `Feature.usage_period` render above the bar
(start left, end right), matching the Agent Workspace Builds panel, so
admins can tell which license period the consumption covers. The backend
always attaches a period to an enabled feature; a missing or unparsable
one just omits the dates row. The CODAGT-855 link to the per-user usage
view is deferred: that view (F3, CODAGT-861) and the API it needs (F2,
CODAGT-857) are not built yet, so there is no route to target. The link
lands with F3.

An enabled feature with `limit` omitted is the unlimited allocation
(license claim `-1`, coder/license#49): the card shows a full neutral
bar with "Limit: Unlimited" and the tooltip without threshold copy,
mirroring `SeatUsageBarCard`'s unlimited state. The invalid-limits error
is kept for negative limits, which can only come from a decoding bug.

Storybook stories cover the green/orange/red states, the capped
over-allocation bar, missing usage data, the missing usage period, the
hidden state, the unlimited state, the invalid-limit error state, and
each tooltip message via play interactions. The page view story verifies
the panel renders above Agent Workspace Builds.

Usage renders with exactly one decimal (e.g. `435.8`, `1,000.0`),
derived from #27985's `actual_ms` field and floored to tenths with
integer math. The same floored value drives the reached states, so the
displayed number and the bar color flip at the same instant; a fraction
past the allocation now trips the red state (`1,000.1 >= 1,000`), pinned
by the `ReachedAllocationByFraction` story. The limit and hard cap
labels stay whole because the license claims are whole hours.

Stacked on #27985.

closes
[CODAGT-855](https://linear.app/codercom/issue/CODAGT-855/f1-runtime-hours-consumption-on-the-licenses-page).
This commit is contained in:
Jaayden Halko
2026-08-20 12:42:51 +07:00
committed by GitHub
parent 676b9bb06b
commit e5deb57079
5 changed files with 772 additions and 1 deletions
@@ -1,7 +1,10 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { expect, fn, within } from "storybook/test";
import type { Feature } from "#/api/typesGenerated";
import { MockLicenseResponse } from "#/testHelpers/entities";
import {
MockAgentRuntimeHoursFeature,
MockLicenseResponse,
} from "#/testHelpers/entities";
import LicensesSettingsPageView from "./LicensesSettingsPageView";
const meta: Meta<typeof LicensesSettingsPageView> = {
@@ -27,6 +30,10 @@ const meta: Meta<typeof LicensesSettingsPageView> = {
enabled: false,
entitlement: "not_entitled",
} satisfies Feature,
agentRuntimeHoursFeature: {
enabled: false,
entitlement: "not_entitled",
} satisfies Feature,
},
};
@@ -71,3 +78,32 @@ export const ActiveAIGovernanceAddOnUsage: Story = {
await expect(canvas.getByText("1,000")).toBeInTheDocument();
},
};
export const TotalAgentHoursUsage: Story = {
args: {
agentRuntimeHoursFeature: {
...MockAgentRuntimeHoursFeature,
limit: 2000,
soft_limit: 1700,
actual: 435,
// 435 hours and 48 minutes: renders as 435.8.
actual_ms: 435 * 3_600_000 + 48 * 60_000,
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const agentHoursHeading = canvas.getByRole("heading", {
name: "Total agent hours",
});
const agentHoursCard = within(agentHoursHeading.closest("section")!);
await expect(agentHoursCard.getByText("435.8")).toBeInTheDocument();
await expect(agentHoursCard.getByText("2,000")).toBeInTheDocument();
const managedAgentsSection = canvas.getByText(
"Agent Workspace Builds Disabled",
);
await expect(
agentHoursHeading.compareDocumentPosition(managedAgentsSection) &
Node.DOCUMENT_POSITION_FOLLOWING,
).toBeTruthy();
},
};
@@ -25,6 +25,7 @@ import { LicenseCard } from "./LicenseCard";
import { LicenseSeatConsumptionChart } from "./LicenseSeatConsumptionChart";
import { ManagedAgentsConsumption } from "./ManagedAgentsConsumption";
import { SeatUsageBarCard } from "./SeatUsageBarCard";
import { TotalAgentHoursCard } from "./TotalAgentHoursCard";
type Props = {
showConfetti: boolean;
@@ -192,6 +193,8 @@ const LicensesSettingsPageView: FC<Props> = ({
/>
</div>
<TotalAgentHoursCard feature={agentRuntimeHoursFeature} />
<ManagedAgentsConsumption
managedAgentFeature={managedAgentFeature}
/>
@@ -0,0 +1,447 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { expect, userEvent, within } from "storybook/test";
import { MockAgentRuntimeHoursFeature } from "#/testHelpers/entities";
import { TotalAgentHoursCard } from "./TotalAgentHoursCard";
const meta: Meta<typeof TotalAgentHoursCard> = {
title:
"pages/DeploymentSettingsPage/LicensesSettingsPage/TotalAgentHoursCard",
component: TotalAgentHoursCard,
args: {
feature: {
...MockAgentRuntimeHoursFeature,
// 400 hours and 18 minutes: renders as 400.3.
actual_ms: 400 * 3_600_000 + 18 * 60_000,
},
},
};
export default meta;
type Story = StoryObj<typeof TotalAgentHoursCard>;
const hoverInfoIcon = async (canvasElement: HTMLElement) => {
const canvas = within(canvasElement);
await userEvent.hover(
canvas.getByRole("button", { name: "Total agent hours information" }),
);
return within(canvasElement.ownerDocument.body);
};
// Radix mounts the role="tooltip" node only while the popover is open.
// The node is the visually hidden copy of the content, so there is no
// visibility to assert.
const expectTooltipText = async (
body: ReturnType<typeof within>,
text: RegExp,
) => {
const tooltip = await body.findByRole("tooltip");
expect(tooltip).toHaveTextContent(text);
};
export const Default: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(canvas.getByText("400.3")).toBeInTheDocument();
await expect(canvas.getByText(/Warning:/)).toBeInTheDocument();
await expect(canvas.getByText("850")).toBeInTheDocument();
await expect(canvas.getByText(/Allocation:/)).toBeInTheDocument();
await expect(canvas.getByText("1,000")).toBeInTheDocument();
await expect(canvas.queryByText(/Limit:/)).not.toBeInTheDocument();
await expect(
canvas.getByText("(June 1, 2026 - May 31, 2027)"),
).toBeInTheDocument();
const body = await hoverInfoIcon(canvasElement);
await expectTooltipText(
body,
/Total time agents have been working across all workspaces this license\. A soft-limit warning appears at 85%/,
);
},
};
export const NoSoftLimit: Story = {
args: {
feature: {
...MockAgentRuntimeHoursFeature,
soft_limit: undefined,
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(canvas.queryByText(/Warning:/)).not.toBeInTheDocument();
const body = await hoverInfoIcon(canvasElement);
await expectTooltipText(
body,
/^Total time agents have been working across all workspaces this license\.$/,
);
},
};
export const ReachedSoftLimit: Story = {
args: {
feature: {
...MockAgentRuntimeHoursFeature,
actual: 850,
actual_ms: 850 * 3_600_000,
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(canvas.getByText("850.0")).toBeInTheDocument();
const body = await hoverInfoIcon(canvasElement);
await expectTooltipText(
body,
/You've used 85% or more of your Total Agent hours for this license\. Agent sessions are still working normally, but you'll want to plan for the 100% limit\./,
);
},
};
export const ReachedAllocation: Story = {
args: {
feature: {
...MockAgentRuntimeHoursFeature,
actual: 1000,
actual_ms: 1000 * 3_600_000,
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(canvas.getByText("1,000.0")).toBeInTheDocument();
await expect(canvas.getByText("1,000")).toBeInTheDocument();
const body = await hoverInfoIcon(canvasElement);
await expectTooltipText(
body,
/You've used 100% of your Total Agent hours for this license\. Contact sales to receive more Agent hours\./,
);
},
};
export const OverAllocation: Story = {
args: {
feature: {
...MockAgentRuntimeHoursFeature,
actual: 1200,
actual_ms: 1200 * 3_600_000,
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(canvas.getByText("1,200.0")).toBeInTheDocument();
await expect(canvas.getByText("1,000")).toBeInTheDocument();
const body = await hoverInfoIcon(canvasElement);
await expectTooltipText(
body,
/You've used 120% of your Total Agent hours for this license\. Contact sales to receive more Agent hours\./,
);
},
};
// The extra 6 minutes alone push the tenths value past the whole-hour
// allocation and flip the reached state.
export const ReachedAllocationByFraction: Story = {
args: {
feature: {
...MockAgentRuntimeHoursFeature,
actual: 1000,
actual_ms: 1000 * 3_600_000 + 6 * 60_000,
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(canvas.getByText("1,000.1")).toBeInTheDocument();
const body = await hoverInfoIcon(canvasElement);
await expectTooltipText(
body,
/You've used 100% of your Total Agent hours for this license\. Contact sales to receive more Agent hours\./,
);
},
};
// Integer claims such as 29/100 must not underflow to 28.9% after
// flooring a binary ratio ((29 / 100) * 100 === 28.999...).
export const SoftLimitExactPercent: Story = {
args: {
feature: {
...MockAgentRuntimeHoursFeature,
limit: 100,
soft_limit: 29,
actual: 10,
actual_ms: 10 * 3_600_000,
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(canvas.getByText("10.0")).toBeInTheDocument();
await expect(canvas.getByText("29")).toBeInTheDocument();
await expect(canvas.getByText("100")).toBeInTheDocument();
const body = await hoverInfoIcon(canvasElement);
await expectTooltipText(
body,
/Total time agents have been working across all workspaces this license\. A soft-limit warning appears at 29%/,
);
},
};
// 99.9% must never read as a false 100%.
export const NearAllocation: Story = {
args: {
feature: {
...MockAgentRuntimeHoursFeature,
soft_limit: 999,
actual: 999,
actual_ms: 999 * 3_600_000,
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(canvas.getByText("999.0")).toBeInTheDocument();
const body = await hoverInfoIcon(canvasElement);
await expectTooltipText(
body,
/You've used 99\.9% or more of your Total Agent hours for this license\. Agent sessions are still working normally, but you'll want to plan for the 100% limit\./,
);
},
};
export const MissingActual: Story = {
args: {
feature: {
...MockAgentRuntimeHoursFeature,
actual: undefined,
actual_ms: undefined,
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(canvas.getByText("N/A")).toBeInTheDocument();
},
};
// Missing usage data must not count as reaching a zero soft limit, so
// the tooltip stays informational.
export const MissingActualZeroSoftLimit: Story = {
args: {
feature: {
...MockAgentRuntimeHoursFeature,
soft_limit: 0,
actual: undefined,
actual_ms: undefined,
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(canvas.getByText("N/A")).toBeInTheDocument();
const body = await hoverInfoIcon(canvasElement);
await expectTooltipText(
body,
/^Total time agents have been working across all workspaces this license\. A soft-limit warning appears at 0%$/,
);
},
};
export const MissingUsagePeriod: Story = {
args: {
feature: {
...MockAgentRuntimeHoursFeature,
usage_period: undefined,
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(canvas.getByText("400.0")).toBeInTheDocument();
await expect(
canvas.queryByText("(June 1, 2026 - May 31, 2027)"),
).not.toBeInTheDocument();
},
};
export const HardCap: Story = {
args: {
feature: {
...MockAgentRuntimeHoursFeature,
hard_limit: 1500,
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(canvas.getByText("400.0")).toBeInTheDocument();
await expect(canvas.getByText(/Warning:/)).toBeInTheDocument();
await expect(canvas.getByText("850")).toBeInTheDocument();
await expect(canvas.getByText(/Allocation:/)).toBeInTheDocument();
await expect(canvas.getByText("1,000")).toBeInTheDocument();
await expect(canvas.getByText(/Limit:/)).toBeInTheDocument();
await expect(canvas.getByText("1,500")).toBeInTheDocument();
await expect(
canvas.queryByText(
"Agent hours limit reached. Concurrent chats are now limited to 5.",
),
).not.toBeInTheDocument();
const body = await hoverInfoIcon(canvasElement);
await expectTooltipText(
body,
/^Total time agents have been working across all workspaces this license\. A soft-limit warning appears at 85%$/,
);
},
};
export const HardCapBetweenSoftLimitAndLimit: Story = {
args: {
feature: {
...MockAgentRuntimeHoursFeature,
hard_limit: 1500,
actual: 900,
actual_ms: 900 * 3_600_000,
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(canvas.getByText("900.0")).toBeInTheDocument();
await expect(
canvas.queryByText(
"Agent hours limit reached. Concurrent chats are now limited to 5.",
),
).not.toBeInTheDocument();
const body = await hoverInfoIcon(canvasElement);
await expectTooltipText(
body,
/You've used 85% or more of your Total Agent hours for this license\. Agent sessions are still working normally, but you'll want to plan for the 100% limit\./,
);
},
};
export const HardCapBetweenLimitAndHardCap: Story = {
args: {
feature: {
...MockAgentRuntimeHoursFeature,
hard_limit: 1500,
actual: 1200,
actual_ms: 1200 * 3_600_000,
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(canvas.getByText("1,200.0")).toBeInTheDocument();
await expect(
canvas.queryByText(
"Agent hours limit reached. Concurrent chats are now limited to 5.",
),
).not.toBeInTheDocument();
const body = await hoverInfoIcon(canvasElement);
await expectTooltipText(
body,
/You've used 120% of your Total Agent hours for this license\. Contact sales to receive more Agent hours\./,
);
},
};
export const ReachedHardCap: Story = {
args: {
feature: {
...MockAgentRuntimeHoursFeature,
hard_limit: 1500,
actual: 1600,
actual_ms: 1600 * 3_600_000,
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(canvas.getByText("1,600.0")).toBeInTheDocument();
await expect(
canvas.getByText(
"Agent hours limit reached. Concurrent chats are now limited to 5.",
),
).toBeInTheDocument();
const body = await hoverInfoIcon(canvasElement);
await expectTooltipText(
body,
/You've used 160% of your Total Agent hours for this license and reached the hard cap of 1,500 hours\. Contact sales to receive more Agent hours\./,
);
},
};
// The backend accepts a hard cap equal to the allocation.
export const ReachedCoincidentHardCap: Story = {
args: {
feature: {
...MockAgentRuntimeHoursFeature,
hard_limit: 1000,
actual: 1000,
actual_ms: 1000 * 3_600_000,
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(canvas.getByText("1,000.0")).toBeInTheDocument();
await expect(
canvas.getByText(
"Agent hours limit reached. Concurrent chats are now limited to 5.",
),
).toBeInTheDocument();
const body = await hoverInfoIcon(canvasElement);
await expectTooltipText(
body,
/You've used 100% of your Total Agent hours for this license and reached the hard cap of 1,000 hours\. Contact sales to receive more Agent hours\./,
);
},
};
export const Disabled: Story = {
args: {
feature: {
...MockAgentRuntimeHoursFeature,
enabled: false,
entitlement: "not_entitled",
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(
canvas.queryByRole("heading", { name: "Total agent hours" }),
).not.toBeInTheDocument();
},
};
export const Unlimited: Story = {
args: {
feature: {
...MockAgentRuntimeHoursFeature,
limit: undefined,
soft_limit: undefined,
actual: 1200,
// 1,200 hours and 30 minutes: renders as 1,200.5.
actual_ms: 1200 * 3_600_000 + 30 * 60_000,
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(canvas.getByText("1,200.5")).toBeInTheDocument();
await expect(canvas.queryByText(/Warning:/)).not.toBeInTheDocument();
await expect(canvas.getByText(/Allocation:/)).toBeInTheDocument();
await expect(canvas.getByText("Unlimited")).toBeInTheDocument();
await expect(
canvas.queryByText("Invalid license usage limits"),
).not.toBeInTheDocument();
const body = await hoverInfoIcon(canvasElement);
await expectTooltipText(
body,
/^Total time agents have been working across all workspaces this license\.$/,
);
},
};
export const ErrorInvalidLimit: Story = {
args: {
feature: {
...MockAgentRuntimeHoursFeature,
// A negative limit can only come from a decoding bug: the
// claim-level unlimited sentinel decodes to an omitted limit,
// never a negative one.
limit: -100,
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(
canvas.getByText("Invalid license usage limits"),
).toBeInTheDocument();
},
};
@@ -0,0 +1,271 @@
import dayjs from "dayjs";
import { BanIcon, InfoIcon } from "lucide-react";
import type { FC } from "react";
import type { Feature } from "#/api/typesGenerated";
import { ErrorAlert } from "#/components/Alert/ErrorAlert";
import { Badge } from "#/components/Badge/Badge";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "#/components/Tooltip/Tooltip";
import { cn } from "#/utils/cn";
type TotalAgentHoursCardProps = {
feature?: Feature;
};
export const TotalAgentHoursCard: FC<TotalAgentHoursCardProps> = ({
feature,
}) => {
// A zero-hour allocation arrives with enabled=false, which hides the
// panel entirely rather than showing an empty bar.
if (!feature?.enabled) {
return null;
}
const {
limit,
soft_limit: softLimit,
hard_limit: hardLimit,
actual_ms: actualMs,
usage_period: usagePeriod,
} = feature;
// An omitted limit means the license grants unlimited runtime hours.
const isUnlimited = limit === undefined;
if (!isUnlimited && limit < 0) {
return (
<section className="border border-solid rounded">
<div className="p-4">
<ErrorAlert error="Invalid license usage limits" />
</div>
</section>
);
}
const meteredLimit = limit ?? 0;
const usedTenths =
actualMs === undefined ? 0 : Math.floor(actualMs / 360_000);
const usedHours = usedTenths / 10;
const hardCap =
!isUnlimited &&
hardLimit !== undefined &&
hardLimit > 0 &&
hardLimit >= meteredLimit
? hardLimit
: undefined;
const reachedAllocation =
!isUnlimited && actualMs !== undefined && usedHours >= meteredLimit;
const reachedHardCap =
hardCap !== undefined && actualMs !== undefined && usedHours >= hardCap;
const reachedSoftLimit =
!isUnlimited &&
!reachedAllocation &&
actualMs !== undefined &&
softLimit !== undefined &&
usedHours >= softLimit;
const barScale = hardCap ?? meteredLimit;
const usagePercentage = isUnlimited
? 100
: barScale > 0
? Math.min((usedHours / barScale) * 100, 100)
: 0;
const allocationMarkerPercent =
hardCap === undefined
? undefined
: Math.min((meteredLimit / hardCap) * 100, 100);
const softMarkerPercent =
!isUnlimited && softLimit !== undefined && barScale > 0
? Math.min((softLimit / barScale) * 100, 100)
: undefined;
const fillClassName = reachedAllocation
? "bg-highlight-red"
: reachedSoftLimit
? "bg-border-warning"
: "bg-highlight-green";
// Already floored to tenths, so rendering one decimal never rounds.
const usedLabel =
actualMs === undefined
? "N/A"
: usedHours.toLocaleString("en-US", {
minimumFractionDigits: 1,
maximumFractionDigits: 1,
});
const warningLabel =
!isUnlimited && softLimit !== undefined
? softLimit.toLocaleString("en-US")
: undefined;
const allocationLabel = isUnlimited
? "Unlimited"
: meteredLimit.toLocaleString("en-US");
const limitLabel = hardCap?.toLocaleString("en-US");
const periodStart = usagePeriod ? dayjs(usagePeriod.start) : undefined;
const periodEnd = usagePeriod ? dayjs(usagePeriod.end) : undefined;
const usagePeriodLabel =
periodStart?.isValid() && periodEnd?.isValid()
? `(${periodStart.format("MMMM D, YYYY")} - ${periodEnd.format("MMMM D, YYYY")})`
: undefined;
const formatPercent = (numerator: number, denominator: number): string =>
(Math.floor((numerator * 1000) / denominator) / 10).toLocaleString("en-US");
const softLimitPercent =
!isUnlimited && softLimit !== undefined && meteredLimit > 0
? formatPercent(softLimit, meteredLimit)
: undefined;
// A fading green fill reads as an unmetered allocation rather than
// 100% usage. The mask needs the -webkit- prefix for Safari.
const unlimitedBarClassName = cn(
"bg-highlight-green",
"[mask-image:linear-gradient(to_right,black_50%,transparent_100%)]",
"[-webkit-mask-image:linear-gradient(to_right,black_50%,transparent_100%)]",
);
let tooltip: string;
if (reachedAllocation) {
const usedPercent =
meteredLimit > 0 ? formatPercent(usedTenths, meteredLimit * 10) : "100";
tooltip = reachedHardCap
? `You've used ${usedPercent}% of your Total Agent hours for this license and reached the hard cap of ${limitLabel} hours. Contact sales to receive more Agent hours.`
: `You've used ${usedPercent}% of your Total Agent hours for this license. Contact sales to receive more Agent hours.`;
} else if (reachedSoftLimit) {
tooltip = `You've used ${softLimitPercent}% or more of your Total Agent hours for this license. Agent sessions are still working normally, but you'll want to plan for the 100% limit.`;
} else if (softLimitPercent !== undefined) {
tooltip = `Total time agents have been working across all workspaces this license. A soft-limit warning appears at ${softLimitPercent}%`;
} else {
tooltip =
"Total time agents have been working across all workspaces this license.";
}
return (
<section className="border border-solid rounded">
<div className="p-4">
<div className="flex flex-col gap-2">
<div className="flex flex-col gap-0.5">
<div className="flex items-center gap-1">
<h3 className="text-md m-0 font-medium">Total agent hours</h3>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
aria-label="Total agent hours information"
className="m-0 inline-flex appearance-none border-0 bg-transparent p-0 text-content-secondary"
>
<InfoIcon className="size-3" />
</button>
</TooltipTrigger>
<TooltipContent side="top" className="max-w-xs">
{tooltip}
</TooltipContent>
</Tooltip>
</div>
{usagePeriodLabel && (
<p className="m-0 text-xs text-content-secondary">
{usagePeriodLabel}
</p>
)}
</div>
{reachedHardCap && (
<div className="flex items-center justify-end">
{/* The concurrency cap re-engages at usage equal to the
hard cap, so the badge shows at the inclusive boundary
and reads "reached" (still true above it) rather than
"exceeded" (false at equality). The sentence outgrows
narrow cards, so this instance wraps instead of
keeping the shared Badge's single-line layout. */}
<Badge
variant="destructive"
size="sm"
className="h-auto rounded-full text-wrap"
>
<BanIcon />
Agent hours limit reached. Concurrent chats are now limited to
5.
</Badge>
</div>
)}
<div
className="relative h-5 w-full overflow-hidden bg-surface-secondary"
aria-hidden="true"
>
{!isUnlimited && (
<>
{softMarkerPercent !== undefined && (
<div
className="absolute inset-y-0 z-0 border-0 border-l-2 border-dotted border-border-warning"
style={{ left: `${softMarkerPercent}%` }}
/>
)}
{hardCap === undefined ? (
// The allocation marker sits at the track's right edge.
<div className="absolute inset-y-0 right-0 z-0 w-0.5 bg-highlight-red" />
) : (
<div
className="absolute inset-y-0 z-0 w-0.5 bg-highlight-red"
style={{ left: `${allocationMarkerPercent}%` }}
/>
)}
</>
)}
{isUnlimited ? (
<div
className={cn(
"relative z-10 h-full w-full",
unlimitedBarClassName,
)}
/>
) : (
<div
className={cn(
"relative z-10 h-full transition-[width] duration-300",
fillClassName,
)}
style={{ width: `${usagePercentage}%` }}
/>
)}
</div>
<div className="flex items-start justify-between gap-3 text-sm font-medium">
<p className="m-0 whitespace-nowrap text-content-primary">
<span className="text-content-secondary">Used: </span>
<span
className={cn({
"text-content-destructive": reachedAllocation,
"text-border-warning": reachedSoftLimit,
})}
>
{usedLabel}
</span>
</p>
<div className="flex flex-wrap items-start justify-end gap-x-3 gap-y-1 whitespace-nowrap text-content-secondary">
{warningLabel !== undefined && (
<p className="m-0">
Warning:{" "}
<span className="text-content-primary">{warningLabel}</span>
</p>
)}
<p className="m-0">
Allocation:{" "}
<span className="text-content-primary">{allocationLabel}</span>
</p>
{limitLabel !== undefined && (
<p className="m-0">
Limit:{" "}
<span className="text-content-primary">{limitLabel}</span>
</p>
)}
</div>
</div>
</div>
</div>
</section>
);
};
+14
View File
@@ -2701,6 +2701,20 @@ export const mockApiError = ({
},
});
export const MockAgentRuntimeHoursFeature: TypesGen.Feature = {
enabled: true,
entitlement: "entitled",
limit: 1000,
soft_limit: 850,
actual: 400,
actual_ms: 400 * 3_600_000,
usage_period: {
issued_at: "June 1, 2026",
start: "June 1, 2026",
end: "May 31, 2027",
},
};
export const MockEntitlements: TypesGen.Entitlements = {
errors: [],
warnings: [],