Agent Limits

Animated grouped agent usage limits and token budgets.

Limits

Grouped usage limits and token budgets.

Plan usage limits · Max (5x)
5-hour limitResets in 2 hr 46 min38%
Weekly · all modelsResets Tue 3:00 PM3%
Weekly · frontier modelsResets Tue 3:00 PM5%
function AgentLimitsDemo() {
  return <AgentLimitsCard context={AGENT_LIMITS_CONTEXT} limits={AGENT_PLAN_LIMITS} />;
}
function AgentLimitsDemo() {
  return <AgentLimitsCard context={AGENT_LIMITS_CONTEXT} limits={AGENT_PLAN_LIMITS} />;
}

Installation

npx shadcn@latest add https://boardcn.dev/r/agent-limits-card.json
npx shadcn@latest add https://boardcn.dev/r/agent-limits-card.json

npm packages

  • @remixicon/react
  • motion

BoardCN dependencies

The CLI installs these for you — you do not need to add them yourself.

Source

The file the CLI copies into your project.

components/blocks/agent-limits-card/agent-limits-card.tsx
"use client";

import { useState } from "react";
import {
  RiArrowDownSLine,
  RiArrowRightLine,
  RiArrowRightSLine,
} from "@remixicon/react";
import { AnimatePresence, motion } from "motion/react";

import { cx } from "@/utils/cx";

export type AgentLimitSegment = {
  label: string;
  tokens: number;
  /** Optional CSS color. Falls back to the BoardCN chart palette. */
  color?: string;
  /** Deferred segments appear in the breakdown but do not count toward usage. */
  deferred?: boolean;
};

export type AgentLimitGroupItem = {
  label: string;
  tokens: number;
};

export type AgentLimitGroup = {
  label: string;
  tokens: number;
  items: readonly AgentLimitGroupItem[];
};

export type AgentContextLimit = {
  max: number;
  segments: readonly AgentLimitSegment[];
  groups?: readonly AgentLimitGroup[];
};

export type AgentPlanLimit = {
  label: string;
  /** Used capacity expressed as a ratio from 0 to 1. */
  used: number;
  resets: string;
};

export type AgentLimitsCardProps = {
  context: AgentContextLimit;
  plan?: string;
  planHref?: string;
  limits: readonly AgentPlanLimit[];
  defaultExpanded?: boolean;
  onExpandedChange?: (open: boolean) => void;
  className?: string;
};

const CHART_PALETTE = [2, 6, 5, 3, 8, 7, 4, 1].map((tone) => ({
  color: `var(--color-chart-${tone})`,
  activeColor: `var(--color-chart-${tone}-active)`,
}));

const EXPAND_EASING = [0.22, 1, 0.36, 1] as const;

function resolveTone(index: number, color?: string, activeColor?: string) {
  return color
    ? {
        color,
        activeColor: activeColor ?? `color-mix(in srgb, ${color} 82%, black)`,
      }
    : CHART_PALETTE[index % CHART_PALETTE.length];
}

export function formatTokens(value: number): string {
  const format = (number: number) =>
    Number.isInteger(number) ? String(number) : number.toFixed(1);

  if (value >= 1_000_000) return `${format(value / 1_000_000)}M`;
  if (value >= 1_000) return `${format(value / 1_000)}k`;
  return String(value);
}

function ProgressTrack({
  children,
  className,
}: {
  children: React.ReactNode;
  className?: string;
}) {
  return (
    <div
      className={cx(
        "flex h-1.5 w-full gap-px overflow-hidden rounded-full bg-chart-track",
        className,
      )}
    >
      {children}
    </div>
  );
}

export function AgentLimitsCard({
  context,
  plan = "Max (5x)",
  planHref,
  limits,
  defaultExpanded = false,
  onExpandedChange,
  className,
}: AgentLimitsCardProps) {
  const [expanded, setExpanded] = useState(defaultExpanded);
  const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set());

  const usedTokens = context.segments
    .filter((segment) => !segment.deferred)
    .reduce((total, segment) => total + segment.tokens, 0);
  const freeTokens = Math.max(0, context.max - usedTokens);
  const percentage = (tokens: number) =>
    (tokens / Math.max(1, context.max)) * 100;
  const tones = context.segments.map((segment, index) =>
    resolveTone(index + 1, segment.color),
  );

  const toggleGroup = (label: string) => {
    setExpandedGroups((current) => {
      const next = new Set(current);
      if (next.has(label)) next.delete(label);
      else next.add(label);
      return next;
    });
  };

  return (
    <section
      className={cx(
        "flex w-full min-w-0 flex-col rounded-2xl bg-background-secondary-default px-4 pt-2.5 pb-4",
        className,
      )}
    >
      <button
        type="button"
        onClick={() => {
          const next = !expanded;
          setExpanded(next);
          onExpandedChange?.(next);
        }}
        aria-expanded={expanded}
        className="group -mx-2 flex cursor-pointer items-center justify-between gap-3 rounded-2lg px-2 py-1 text-left outline-none transition-colors duration-150 hover:bg-background-secondary-hover focus-visible:ring-2 focus-visible:ring-border-focus-ring"
      >
        <span className="text-body-medium text-text-secondary">
          Context window
        </span>
        <span className="flex items-center gap-2">
          <span className="text-body-medium whitespace-nowrap text-text-secondary tabular-nums">
            {formatTokens(usedTokens)} / {formatTokens(context.max)}{" "}
            <span className="text-text-primary">
              ({Math.round(percentage(usedTokens))}%)
            </span>
          </span>
          <RiArrowDownSLine
            className={cx(
              "size-4 shrink-0 text-text-tertiary transition-transform duration-200 ease-out group-hover:text-text-secondary",
              expanded && "rotate-180",
            )}
            aria-hidden
          />
        </span>
      </button>

      <ProgressTrack className="mt-1.5">
        {context.segments.map((segment, index) =>
          segment.deferred ? null : (
            <div
              key={segment.label}
              className="h-full shrink-0 transition-[width] duration-500 ease-out"
              style={{
                width: `${percentage(segment.tokens)}%`,
                backgroundColor: tones[index].color,
              }}
              title={`${segment.label} · ${formatTokens(segment.tokens)}`}
            />
          ),
        )}
      </ProgressTrack>

      <AnimatePresence initial={false}>
        {expanded && (
          <motion.div
            key="breakdown"
            initial={{ height: 0, opacity: 0 }}
            animate={{ height: "auto", opacity: 1 }}
            exit={{ height: 0, opacity: 0 }}
            transition={{ duration: 0.32, ease: EXPAND_EASING }}
            className="-mx-2 overflow-hidden px-2"
          >
            <div className="flex flex-col pt-3">
              {context.segments.map((segment, index) => (
                <div
                  key={segment.label}
                  className="flex items-center gap-2 py-[5px]"
                >
                  <span
                    className="size-2.5 shrink-0 rounded-[3px]"
                    style={{
                      backgroundColor: segment.deferred
                        ? "var(--color-chart-cursor)"
                        : tones[index].color,
                    }}
                  />
                  <span className="min-w-0 flex-1 truncate text-body-regular text-text-primary">
                    {segment.label}
                  </span>
                  <span className="text-body-regular text-text-tertiary tabular-nums">
                    {formatTokens(segment.tokens)}
                  </span>
                  <span className="w-14 text-right text-body-medium text-text-primary tabular-nums">
                    {segment.deferred
                      ? "—"
                      : `${percentage(segment.tokens).toFixed(1)}%`}
                  </span>
                </div>
              ))}

              <div className="flex items-center gap-2 py-[5px]">
                <span className="size-2.5 shrink-0 rounded-[3px] bg-chart-track" />
                <span className="min-w-0 flex-1 truncate text-body-regular text-text-primary">
                  Free space
                </span>
                <span className="text-body-regular text-text-tertiary tabular-nums">
                  {formatTokens(freeTokens)}
                </span>
                <span className="w-14 text-right text-body-medium text-text-primary tabular-nums">
                  {percentage(freeTokens).toFixed(1)}%
                </span>
              </div>

              {context.groups && context.groups.length > 0 && (
                <div className="mt-1.5 flex flex-col">
                  {context.groups.map((group) => {
                    const groupExpanded = expandedGroups.has(group.label);

                    return (
                      <div key={group.label} className="flex flex-col">
                        <button
                          type="button"
                          onClick={() => toggleGroup(group.label)}
                          aria-expanded={groupExpanded}
                          className="-mx-2 flex cursor-pointer items-center gap-1.5 rounded-2lg px-2 py-[7px] text-left outline-none transition-colors duration-150 hover:bg-background-secondary-hover focus-visible:ring-2 focus-visible:ring-border-focus-ring"
                        >
                          <RiArrowRightSLine
                            className={cx(
                              "size-4 shrink-0 text-text-tertiary transition-transform duration-200 ease-out",
                              groupExpanded && "rotate-90",
                            )}
                            aria-hidden
                          />
                          <span className="min-w-0 flex-1 truncate text-body-regular text-text-secondary">
                            {group.label}
                          </span>
                          <span className="text-body-regular text-text-tertiary tabular-nums">
                            {formatTokens(group.tokens)}
                          </span>
                          <span className="w-14 text-right text-body-regular text-text-tertiary tabular-nums">
                            {group.items.length}
                          </span>
                        </button>

                        <AnimatePresence initial={false}>
                          {groupExpanded && (
                            <motion.div
                              key="items"
                              initial={{ height: 0, opacity: 0 }}
                              animate={{ height: "auto", opacity: 1 }}
                              exit={{ height: 0, opacity: 0 }}
                              transition={{
                                duration: 0.26,
                                ease: EXPAND_EASING,
                              }}
                              className="overflow-hidden"
                            >
                              <div className="flex flex-col pb-1 pl-[22px]">
                                {group.items.map((item) => (
                                  <div
                                    key={item.label}
                                    className="flex items-center gap-2 py-1"
                                  >
                                    <span className="min-w-0 flex-1 truncate text-body-2-regular text-text-secondary">
                                      {item.label}
                                    </span>
                                    <span className="text-body-2-regular text-text-tertiary tabular-nums">
                                      {formatTokens(item.tokens)}
                                    </span>
                                    <span className="w-14" />
                                  </div>
                                ))}
                              </div>
                            </motion.div>
                          )}
                        </AnimatePresence>
                      </div>
                    );
                  })}
                </div>
              )}
            </div>
          </motion.div>
        )}
      </AnimatePresence>

      <div className="my-3 h-px w-full bg-separator-border-strong" />

      <div className="flex items-center justify-between gap-3 py-1">
        <span className="text-body-medium text-text-secondary">
          Plan usage limits{plan ? ` · ${plan}` : ""}
        </span>
        {planHref && (
          <a
            href={planHref}
            aria-label="Manage plan"
            className="flex size-6 items-center justify-center rounded-md text-text-tertiary outline-none transition-colors duration-150 hover:bg-background-secondary-hover hover:text-text-secondary focus-visible:ring-2 focus-visible:ring-border-focus-ring"
          >
            <RiArrowRightLine className="size-4" aria-hidden />
          </a>
        )}
      </div>

      <div className="flex flex-col gap-3 pt-1">
        {limits.map((limit) => {
          const usedPercentage = Math.round(
            100 * Math.max(0, Math.min(1, limit.used)),
          );

          return (
            <div key={limit.label} className="flex flex-col gap-1.5">
              <div className="flex items-baseline justify-between gap-3">
                <span className="min-w-0 truncate text-body-medium text-text-primary">
                  {limit.label}
                </span>
                <span className="flex shrink-0 items-baseline gap-2">
                  <span className="text-body-regular text-text-tertiary">
                    {limit.resets}
                  </span>
                  <span className="w-9 text-right text-body-medium text-text-primary tabular-nums">
                    {usedPercentage}%
                  </span>
                </span>
              </div>
              <ProgressTrack>
                <div
                  className="h-full rounded-full transition-[width] duration-500 ease-out"
                  style={{
                    width: `${usedPercentage}%`,
                    backgroundColor: "var(--color-chart-6)",
                  }}
                />
              </ProgressTrack>
            </div>
          );
        })}
      </div>
    </section>
  );
}
"use client";

import { useState } from "react";
import {
  RiArrowDownSLine,
  RiArrowRightLine,
  RiArrowRightSLine,
} from "@remixicon/react";
import { AnimatePresence, motion } from "motion/react";

import { cx } from "@/utils/cx";

export type AgentLimitSegment = {
  label: string;
  tokens: number;
  /** Optional CSS color. Falls back to the BoardCN chart palette. */
  color?: string;
  /** Deferred segments appear in the breakdown but do not count toward usage. */
  deferred?: boolean;
};

export type AgentLimitGroupItem = {
  label: string;
  tokens: number;
};

export type AgentLimitGroup = {
  label: string;
  tokens: number;
  items: readonly AgentLimitGroupItem[];
};

export type AgentContextLimit = {
  max: number;
  segments: readonly AgentLimitSegment[];
  groups?: readonly AgentLimitGroup[];
};

export type AgentPlanLimit = {
  label: string;
  /** Used capacity expressed as a ratio from 0 to 1. */
  used: number;
  resets: string;
};

export type AgentLimitsCardProps = {
  context: AgentContextLimit;
  plan?: string;
  planHref?: string;
  limits: readonly AgentPlanLimit[];
  defaultExpanded?: boolean;
  onExpandedChange?: (open: boolean) => void;
  className?: string;
};

const CHART_PALETTE = [2, 6, 5, 3, 8, 7, 4, 1].map((tone) => ({
  color: `var(--color-chart-${tone})`,
  activeColor: `var(--color-chart-${tone}-active)`,
}));

const EXPAND_EASING = [0.22, 1, 0.36, 1] as const;

function resolveTone(index: number, color?: string, activeColor?: string) {
  return color
    ? {
        color,
        activeColor: activeColor ?? `color-mix(in srgb, ${color} 82%, black)`,
      }
    : CHART_PALETTE[index % CHART_PALETTE.length];
}

export function formatTokens(value: number): string {
  const format = (number: number) =>
    Number.isInteger(number) ? String(number) : number.toFixed(1);

  if (value >= 1_000_000) return `${format(value / 1_000_000)}M`;
  if (value >= 1_000) return `${format(value / 1_000)}k`;
  return String(value);
}

function ProgressTrack({
  children,
  className,
}: {
  children: React.ReactNode;
  className?: string;
}) {
  return (
    <div
      className={cx(
        "flex h-1.5 w-full gap-px overflow-hidden rounded-full bg-chart-track",
        className,
      )}
    >
      {children}
    </div>
  );
}

export function AgentLimitsCard({
  context,
  plan = "Max (5x)",
  planHref,
  limits,
  defaultExpanded = false,
  onExpandedChange,
  className,
}: AgentLimitsCardProps) {
  const [expanded, setExpanded] = useState(defaultExpanded);
  const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set());

  const usedTokens = context.segments
    .filter((segment) => !segment.deferred)
    .reduce((total, segment) => total + segment.tokens, 0);
  const freeTokens = Math.max(0, context.max - usedTokens);
  const percentage = (tokens: number) =>
    (tokens / Math.max(1, context.max)) * 100;
  const tones = context.segments.map((segment, index) =>
    resolveTone(index + 1, segment.color),
  );

  const toggleGroup = (label: string) => {
    setExpandedGroups((current) => {
      const next = new Set(current);
      if (next.has(label)) next.delete(label);
      else next.add(label);
      return next;
    });
  };

  return (
    <section
      className={cx(
        "flex w-full min-w-0 flex-col rounded-2xl bg-background-secondary-default px-4 pt-2.5 pb-4",
        className,
      )}
    >
      <button
        type="button"
        onClick={() => {
          const next = !expanded;
          setExpanded(next);
          onExpandedChange?.(next);
        }}
        aria-expanded={expanded}
        className="group -mx-2 flex cursor-pointer items-center justify-between gap-3 rounded-2lg px-2 py-1 text-left outline-none transition-colors duration-150 hover:bg-background-secondary-hover focus-visible:ring-2 focus-visible:ring-border-focus-ring"
      >
        <span className="text-body-medium text-text-secondary">
          Context window
        </span>
        <span className="flex items-center gap-2">
          <span className="text-body-medium whitespace-nowrap text-text-secondary tabular-nums">
            {formatTokens(usedTokens)} / {formatTokens(context.max)}{" "}
            <span className="text-text-primary">
              ({Math.round(percentage(usedTokens))}%)
            </span>
          </span>
          <RiArrowDownSLine
            className={cx(
              "size-4 shrink-0 text-text-tertiary transition-transform duration-200 ease-out group-hover:text-text-secondary",
              expanded && "rotate-180",
            )}
            aria-hidden
          />
        </span>
      </button>

      <ProgressTrack className="mt-1.5">
        {context.segments.map((segment, index) =>
          segment.deferred ? null : (
            <div
              key={segment.label}
              className="h-full shrink-0 transition-[width] duration-500 ease-out"
              style={{
                width: `${percentage(segment.tokens)}%`,
                backgroundColor: tones[index].color,
              }}
              title={`${segment.label} · ${formatTokens(segment.tokens)}`}
            />
          ),
        )}
      </ProgressTrack>

      <AnimatePresence initial={false}>
        {expanded && (
          <motion.div
            key="breakdown"
            initial={{ height: 0, opacity: 0 }}
            animate={{ height: "auto", opacity: 1 }}
            exit={{ height: 0, opacity: 0 }}
            transition={{ duration: 0.32, ease: EXPAND_EASING }}
            className="-mx-2 overflow-hidden px-2"
          >
            <div className="flex flex-col pt-3">
              {context.segments.map((segment, index) => (
                <div
                  key={segment.label}
                  className="flex items-center gap-2 py-[5px]"
                >
                  <span
                    className="size-2.5 shrink-0 rounded-[3px]"
                    style={{
                      backgroundColor: segment.deferred
                        ? "var(--color-chart-cursor)"
                        : tones[index].color,
                    }}
                  />
                  <span className="min-w-0 flex-1 truncate text-body-regular text-text-primary">
                    {segment.label}
                  </span>
                  <span className="text-body-regular text-text-tertiary tabular-nums">
                    {formatTokens(segment.tokens)}
                  </span>
                  <span className="w-14 text-right text-body-medium text-text-primary tabular-nums">
                    {segment.deferred
                      ? "—"
                      : `${percentage(segment.tokens).toFixed(1)}%`}
                  </span>
                </div>
              ))}

              <div className="flex items-center gap-2 py-[5px]">
                <span className="size-2.5 shrink-0 rounded-[3px] bg-chart-track" />
                <span className="min-w-0 flex-1 truncate text-body-regular text-text-primary">
                  Free space
                </span>
                <span className="text-body-regular text-text-tertiary tabular-nums">
                  {formatTokens(freeTokens)}
                </span>
                <span className="w-14 text-right text-body-medium text-text-primary tabular-nums">
                  {percentage(freeTokens).toFixed(1)}%
                </span>
              </div>

              {context.groups && context.groups.length > 0 && (
                <div className="mt-1.5 flex flex-col">
                  {context.groups.map((group) => {
                    const groupExpanded = expandedGroups.has(group.label);

                    return (
                      <div key={group.label} className="flex flex-col">
                        <button
                          type="button"
                          onClick={() => toggleGroup(group.label)}
                          aria-expanded={groupExpanded}
                          className="-mx-2 flex cursor-pointer items-center gap-1.5 rounded-2lg px-2 py-[7px] text-left outline-none transition-colors duration-150 hover:bg-background-secondary-hover focus-visible:ring-2 focus-visible:ring-border-focus-ring"
                        >
                          <RiArrowRightSLine
                            className={cx(
                              "size-4 shrink-0 text-text-tertiary transition-transform duration-200 ease-out",
                              groupExpanded && "rotate-90",
                            )}
                            aria-hidden
                          />
                          <span className="min-w-0 flex-1 truncate text-body-regular text-text-secondary">
                            {group.label}
                          </span>
                          <span className="text-body-regular text-text-tertiary tabular-nums">
                            {formatTokens(group.tokens)}
                          </span>
                          <span className="w-14 text-right text-body-regular text-text-tertiary tabular-nums">
                            {group.items.length}
                          </span>
                        </button>

                        <AnimatePresence initial={false}>
                          {groupExpanded && (
                            <motion.div
                              key="items"
                              initial={{ height: 0, opacity: 0 }}
                              animate={{ height: "auto", opacity: 1 }}
                              exit={{ height: 0, opacity: 0 }}
                              transition={{
                                duration: 0.26,
                                ease: EXPAND_EASING,
                              }}
                              className="overflow-hidden"
                            >
                              <div className="flex flex-col pb-1 pl-[22px]">
                                {group.items.map((item) => (
                                  <div
                                    key={item.label}
                                    className="flex items-center gap-2 py-1"
                                  >
                                    <span className="min-w-0 flex-1 truncate text-body-2-regular text-text-secondary">
                                      {item.label}
                                    </span>
                                    <span className="text-body-2-regular text-text-tertiary tabular-nums">
                                      {formatTokens(item.tokens)}
                                    </span>
                                    <span className="w-14" />
                                  </div>
                                ))}
                              </div>
                            </motion.div>
                          )}
                        </AnimatePresence>
                      </div>
                    );
                  })}
                </div>
              )}
            </div>
          </motion.div>
        )}
      </AnimatePresence>

      <div className="my-3 h-px w-full bg-separator-border-strong" />

      <div className="flex items-center justify-between gap-3 py-1">
        <span className="text-body-medium text-text-secondary">
          Plan usage limits{plan ? ` · ${plan}` : ""}
        </span>
        {planHref && (
          <a
            href={planHref}
            aria-label="Manage plan"
            className="flex size-6 items-center justify-center rounded-md text-text-tertiary outline-none transition-colors duration-150 hover:bg-background-secondary-hover hover:text-text-secondary focus-visible:ring-2 focus-visible:ring-border-focus-ring"
          >
            <RiArrowRightLine className="size-4" aria-hidden />
          </a>
        )}
      </div>

      <div className="flex flex-col gap-3 pt-1">
        {limits.map((limit) => {
          const usedPercentage = Math.round(
            100 * Math.max(0, Math.min(1, limit.used)),
          );

          return (
            <div key={limit.label} className="flex flex-col gap-1.5">
              <div className="flex items-baseline justify-between gap-3">
                <span className="min-w-0 truncate text-body-medium text-text-primary">
                  {limit.label}
                </span>
                <span className="flex shrink-0 items-baseline gap-2">
                  <span className="text-body-regular text-text-tertiary">
                    {limit.resets}
                  </span>
                  <span className="w-9 text-right text-body-medium text-text-primary tabular-nums">
                    {usedPercentage}%
                  </span>
                </span>
              </div>
              <ProgressTrack>
                <div
                  className="h-full rounded-full transition-[width] duration-500 ease-out"
                  style={{
                    width: `${usedPercentage}%`,
                    backgroundColor: "var(--color-chart-6)",
                  }}
                />
              </ProgressTrack>
            </div>
          );
        })}
      </div>
    </section>
  );
}

Props

Generated from the component's TypeScript types. Standard DOM and React Aria props are omitted.

AgentLimitsCard

PropTypeDefaultDescription
contextrequiredAgentContextLimit
limitsrequiredreadonly AgentPlanLimit[]
classNamestring
defaultExpandedbooleanfalse
onExpandedChange(open: boolean) => void
planstringMax (5x)
planHrefstring