import { useRef, useState } from "react"; import { FoldVertical, Loader2 } from "lucide-react"; import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; import { timeAgo } from "@/lib/timeAgo"; import { formatContextTokens } from "@/lib/context-tokens"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; export { formatContextTokens }; /** * Latest context-window snapshot for the agent's live session. Built from * provider usage events (Codex `thread/tokenUsage/updated`, ACP `usage_update`). */ export interface ContextUsage { /** Tokens currently occupying the context window. */ usedTokens: number; /** Model context window size in tokens. */ contextWindow: number; /** Token count at which the harness compacts on its own, when known. */ autoCompactTokens?: number | null; /** Session totals, when the provider reports them. */ totals?: { inputTokens?: number | null; cachedInputTokens?: number | null; outputTokens?: number | null; reasoningOutputTokens?: number | null; } | null; /** Most recent model request, when the provider reports it. */ lastTurn?: { inputTokens?: number | null; outputTokens?: number | null } | null; costUsd?: number | null; /** Number of compactions so far in this session. */ compactions?: number; /** When the provider reported this reading (ISO timestamp). */ observedAt?: string | null; } export type ContextUsageLevel = "normal" | "near" | "full"; /** Amber once the context is within reach of auto-compaction, red when nearly full. */ export function contextUsageLevel(usage: ContextUsage): ContextUsageLevel { const fraction = usage.usedTokens / usage.contextWindow; if (fraction >= 0.95) return "full"; const threshold = usage.autoCompactTokens && usage.autoCompactTokens > 0 ? usage.autoCompactTokens / usage.contextWindow : 0.9; return fraction >= threshold - 0.1 ? "near" : "normal"; } const LEVEL_TEXT: Record = { normal: "text-foreground", near: "text-(--status-agent-paused)", full: "text-(--status-agent-error)", }; const LEVEL_FILL: Record = { normal: "bg-muted-foreground", near: "bg-(--status-agent-paused)", full: "bg-(--status-agent-error)", }; function Bar({ percent, level, className }: { percent: number; level: ContextUsageLevel; className?: string }) { return (
); } function Row({ label, value, muted }: { label: string; value: string; muted?: boolean }) { return (
{label}
{value}
); } export type ContextCompactState = /** The agent's session can be compacted now. */ | { status: "available"; onCompact: () => void } /** A compaction was requested and has not finished yet. */ | { status: "pending" } /** Shown disabled with the reason, e.g. the agent is mid-run. */ | { status: "unavailable"; reason: string }; function CompactAction({ compact, level }: { compact: ContextCompactState; level: ContextUsageLevel }) { return (

{compact.status === "unavailable" ? compact.reason : compact.status === "pending" ? "The agent is summarizing its conversation. This usually takes under a minute." : "Summarizes the conversation so far so the agent has room to keep working."}

); } /** The hover card body; exported for Storybook galleries. */ export function ContextUsageCard({ usage, compact }: { usage: ContextUsage; compact?: ContextCompactState | null }) { const percent = Math.min(100, Math.round((usage.usedTokens / usage.contextWindow) * 100)); const level = contextUsageLevel(usage); const totals = usage.totals; return (
Context window {percent}%

{formatContextTokens(usage.usedTokens)} of {formatContextTokens(usage.contextWindow)} tokens {usage.observedAt ? <> · updated {timeAgo(usage.observedAt)} : null}

{level !== "normal" ? (

{usage.autoCompactTokens ? `Compacts automatically near ${formatContextTokens(usage.autoCompactTokens)}.` : "Close to full. The agent will compact its context soon."}

) : null} {totals || usage.lastTurn || usage.costUsd != null || usage.compactions ? (
{totals?.inputTokens != null ? ( ) : null} {totals?.outputTokens != null ? : null} {totals?.reasoningOutputTokens ? ( ) : null} {usage.lastTurn ? ( ) : null} {usage.costUsd != null ? : null} {usage.compactions ? : null}
) : null} {compact ? : null}
); } /** * Context row value for the task Properties panel: a thin fill bar and * "64% of 272k". Hover or tap opens the token breakdown and, when the * agent's harness supports it, Compact now. */ export function ContextUsageMeter({ usage, compact, defaultOpen = false, }: { usage: ContextUsage; compact?: ContextCompactState | null; defaultOpen?: boolean; }) { const [open, setOpen] = useState(defaultOpen); const closeTimer = useRef | null>(null); const percent = Math.min(100, Math.round((usage.usedTokens / usage.contextWindow) * 100)); const level = contextUsageLevel(usage); const windowLabel = formatContextTokens(usage.contextWindow); const show = () => { if (closeTimer.current) clearTimeout(closeTimer.current); setOpen(true); }; // A short grace period lets the pointer travel from the row into the card. const hide = () => { if (closeTimer.current) clearTimeout(closeTimer.current); closeTimer.current = setTimeout(() => setOpen(false), 150); }; return ( event.preventDefault()} onMouseEnter={show} onMouseLeave={hide} > ); }