import type { Message } from '@/api/types'; interface Props { messages: Message[]; // v1.11.5: model's full context window from chat.model_context_limit // (server-side getModelContext lookup). Lets us render a meaningful // zero-state (0 / max, muted) before any assistant message has run. // null/undefined means lookup failed — bar still renders, but with an // "Context — / —" placeholder rather than misleading 0/0 math. modelContextLimit?: number | null; } // v1.11.5.1: inline persistent context-usage indicator. Lives in the same // horizontal row as the agent picker (was a separate row above; user // pointed at the empty space next to "Code Reviewer ▾ +" and asked for // the bar there). Caller wraps in a flex container and ContextBar takes // the remaining width via `flex-1 min-w-0`. Color tiers fire against // (max - 20k compaction reserve) so the bar warns amber/orange/red at // the same boundaries the server's auto-compaction triggers. const COMPACTION_BUFFER = 20_000; // Take the latest ctx_used and the latest ctx_max INDEPENDENTLY (newest-first). // They needn't be on the same message: ctx_max is the model's context window — a // constant per model — while some agents report it only intermittently (the claude // SDK populates modelUsage.contextWindow on some turns, not all) yet report // ctx_used every turn. Pairing the latest of each gives a correct used/max even // when the most recent turn omitted the window. Native BooChat sets both on the // same assistant message, so this is identical there. Returns null until BOTH a // used and a positive max have been seen at least once. function latestPair(messages: Message[]): { used: number; max: number } | null { let used: number | null = null; let max: number | null = null; for (let i = messages.length - 1; i >= 0; i--) { const m = messages[i]!; if (used === null && m.ctx_used != null) used = m.ctx_used; if (max === null && m.ctx_max != null && m.ctx_max > 0) max = m.ctx_max; if (used !== null && max !== null) break; } return used !== null && max !== null ? { used, max } : null; } interface ColorTier { // Tailwind utility for the label / numbers. Uses literal palette names // rather than design tokens because we want three distinct severities // (amber → orange → red) and BooCode only defines one warning token // (`destructive`). Literal classes keep the gradation explicit. text: string; bar: string; } function tierFor(usablePct: number): ColorTier { if (usablePct >= 0.95) return { text: 'text-red-600 dark:text-red-400', bar: 'bg-red-500' }; if (usablePct >= 0.80) return { text: 'text-orange-600 dark:text-orange-400', bar: 'bg-orange-500' }; if (usablePct >= 0.60) return { text: 'text-amber-600 dark:text-amber-400', bar: 'bg-amber-500' }; return { text: 'text-muted-foreground', bar: 'bg-muted-foreground/40' }; } export function ContextBar({ messages, modelContextLimit }: Props) { // Resolve which of the three render branches applies: // 1. real pair — actual usage from the latest assistant message // 2. zero-state — no usage yet but we know the model's limit // 3. unknown — neither usage nor limit; render placeholder // The component NEVER returns null per v1.11.5 spec — the bar is // persistent so the user knows where it lives. const pair = latestPair(messages); const usable: number | null = pair ? Math.max(0, pair.max - COMPACTION_BUFFER) : modelContextLimit && modelContextLimit > 0 ? Math.max(0, modelContextLimit - COMPACTION_BUFFER) : null; const used = pair?.used ?? 0; const max = pair?.max ?? (modelContextLimit && modelContextLimit > 0 ? modelContextLimit : null); // pct/usablePct only meaningful when max is known. The unknown branch // sets fill width to 0 and tier to muted regardless. const pct = max ? used / max : 0; const usablePct = usable && usable > 0 ? used / usable : 0; const tier = tierFor(usablePct); // Bar fill clamped to [0, 100]. Over-budget cases (usable < used) still // show the bar at 100% red rather than overflowing the track visually. const fillPct = Math.min(100, Math.max(0, pct * 100)); const compactionThresholdPct = max && usable && usable > 0 ? Math.round((usable / max) * 100) : null; const tooltipText = compactionThresholdPct !== null ? `Auto-compaction at ~${compactionThresholdPct}%` : 'Model context unknown.'; // `flex-1 min-w-0` lets the bar consume the remaining width inside the // picker row's flex container while preventing the numbers (whitespace- // nowrap) from pushing the bar out of bounds. Two-element row: track on // the left, numbers on the right. return (