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; // Walk newest-first; first message with both ctx_used and ctx_max non-null // AND ctx_max > 0 wins. Older messages may have ctx_used but missing ctx_max // (early v1 before llama-swap's n_ctx capture worked) — skip them and keep // walking. Returns null when no usable pair exists in the chat. function latestPair(messages: Message[]): { used: number; max: number } | null { for (let i = messages.length - 1; i >= 0; i--) { const m = messages[i]!; if (m.ctx_used == null || m.ctx_max == null) continue; if (m.ctx_max <= 0) continue; return { used: m.ctx_used, max: m.ctx_max }; } return 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 (
{max !== null ? ( <> {/* Absolute counts hidden on very narrow viewports so the percentage always has room. Tooltip carries full detail. */} {used.toLocaleString()} / {max.toLocaleString()}{' '} ({Math.round(pct * 100)}%) ) : ( <>— / — )}
); }