diff --git a/packages/elements/__tests__/context.test.tsx b/packages/elements/__tests__/context.test.tsx
index 2c2d6fbc..da0a357c 100644
--- a/packages/elements/__tests__/context.test.tsx
+++ b/packages/elements/__tests__/context.test.tsx
@@ -33,6 +33,48 @@ describe("context", () => {
expect(screen.getByText("50%")).toBeInTheDocument();
});
+ it.each([
+ { maxTokens: 0, usedTokens: 0 },
+ { maxTokens: 0, usedTokens: 50 },
+ { maxTokens: 100, usedTokens: -1 },
+ ])(
+ "clamps to 0% for usedTokens=$usedTokens maxTokens=$maxTokens",
+ ({ maxTokens, usedTokens }) => {
+ const { container } = render(
+
+
+
+ );
+ expect(screen.getByText("0%")).toBeInTheDocument();
+ const ring = container.querySelector("circle[stroke-dashoffset]");
+ // Empty ring: offset equals the full circumference (2π × r=10 ≈ 62.83)
+ expect(Number(ring?.getAttribute("stroke-dashoffset"))).toBeCloseTo(
+ 62.83,
+ 1
+ );
+ }
+ );
+
+ it("clamps over-budget usage to a full 100% indicator", () => {
+ const { container } = render(
+
+
+
+
+
+
+ );
+ expect(screen.getAllByText("100%").length).toBeGreaterThan(0);
+ const ring = container.querySelector("circle[stroke-dashoffset]");
+ // Full ring: zero offset
+ expect(Number(ring?.getAttribute("stroke-dashoffset"))).toBeCloseTo(0);
+ const bar = document.querySelector(
+ '[data-slot="progress-indicator"]'
+ );
+ // Full bar: no leftward translation (150% used to yield translateX(50%))
+ expect(bar?.style.transform).toBe("translateX(0%)");
+ });
+
it("throws error when components used outside Context provider", () => {
// Suppress console.error for this test
const spy = vi.spyOn(console, "error").mockImplementation(vi.fn());
diff --git a/packages/elements/src/context.tsx b/packages/elements/src/context.tsx
index 53e28851..6daf0000 100644
--- a/packages/elements/src/context.tsx
+++ b/packages/elements/src/context.tsx
@@ -1,5 +1,8 @@
"use client";
+import type { LanguageModelUsage } from "ai";
+import type { ComponentProps } from "react";
+
import { Button } from "@repo/shadcn-ui/components/ui/button";
import {
HoverCard,
@@ -8,8 +11,6 @@ import {
} from "@repo/shadcn-ui/components/ui/hover-card";
import { Progress } from "@repo/shadcn-ui/components/ui/progress";
import { cn } from "@repo/shadcn-ui/lib/utils";
-import type { LanguageModelUsage } from "ai";
-import type { ComponentProps } from "react";
import { createContext, useContext, useMemo } from "react";
import { getUsage } from "tokenlens";
@@ -19,6 +20,15 @@ const ICON_VIEWBOX = 24;
const ICON_CENTER = 12;
const ICON_STROKE_WIDTH = 2;
+const clampUsedPercent = (usedTokens: number, maxTokens: number) => {
+ if (
+ !(Number.isFinite(usedTokens) && Number.isFinite(maxTokens)) ||
+ maxTokens <= 0
+ ) {
+ return 0;
+ }
+ return Math.min(Math.max(usedTokens / maxTokens, 0), 1);
+};
type ModelId = string;
interface ContextSchema {
@@ -64,7 +74,7 @@ export const Context = ({
const ContextIcon = () => {
const { usedTokens, maxTokens } = useContextValue();
const circumference = 2 * Math.PI * ICON_RADIUS;
- const usedPercent = usedTokens / maxTokens;
+ const usedPercent = clampUsedPercent(usedTokens, maxTokens);
const dashOffset = circumference * (1 - usedPercent);
return (
@@ -106,7 +116,7 @@ export type ContextTriggerProps = ComponentProps;
export const ContextTrigger = ({ children, ...props }: ContextTriggerProps) => {
const { usedTokens, maxTokens } = useContextValue();
- const usedPercent = usedTokens / maxTokens;
+ const usedPercent = clampUsedPercent(usedTokens, maxTokens);
const renderedPercent = new Intl.NumberFormat("en-US", {
maximumFractionDigits: 1,
style: "percent",
@@ -146,7 +156,7 @@ export const ContextContentHeader = ({
...props
}: ContextContentHeaderProps) => {
const { usedTokens, maxTokens } = useContextValue();
- const usedPercent = usedTokens / maxTokens;
+ const usedPercent = clampUsedPercent(usedTokens, maxTokens);
const displayPct = new Intl.NumberFormat("en-US", {
maximumFractionDigits: 1,
style: "percent",