ContextBar: persistent context-usage indicator above MessageList
Walks chat messages newest-first for the latest ctx_used/ctx_max pair. Color tiers fire against (max - 20k compaction reserve) so the bar warns amber/orange/red at the same boundaries auto-compaction triggers. "Context" → "Ctx" at <640px, (NN%) drops at <380px. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
86
apps/web/src/components/ContextBar.tsx
Normal file
86
apps/web/src/components/ContextBar.tsx
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import type { Message } from '@/api/types';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
messages: Message[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// v1.11.2: persistent context-usage indicator above MessageList. Mirrors the
|
||||||
|
// server-side compaction.usable() formula — color thresholds are computed
|
||||||
|
// against (max - 20k buffer), not raw max, so the bar turns amber/orange
|
||||||
|
// /red at the same boundaries auto-compaction will fire. The popover above
|
||||||
|
// the input (ChatContextPopover) uses raw-% thresholds and is intentionally
|
||||||
|
// kept separate (it's a different surface and a different signal).
|
||||||
|
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. If nothing usable in the chat, caller renders null.
|
||||||
|
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 }: Props) {
|
||||||
|
const pair = latestPair(messages);
|
||||||
|
if (!pair) return null;
|
||||||
|
|
||||||
|
const { used, max } = pair;
|
||||||
|
const usable = Math.max(0, max - COMPACTION_BUFFER);
|
||||||
|
const pct = used / max;
|
||||||
|
const usablePct = usable > 0 ? used / usable : 0;
|
||||||
|
const tier = tierFor(usablePct);
|
||||||
|
|
||||||
|
// Bar fill is 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 > 0 ? Math.round((usable / max) * 100) : 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="border-b px-4 py-1 shrink-0">
|
||||||
|
<div className="max-w-[1000px] mx-auto w-full">
|
||||||
|
<div className="flex items-baseline justify-between text-[10px] font-mono leading-tight">
|
||||||
|
{/* "Context" on >=sm, "Ctx" on phones to save horizontal space. */}
|
||||||
|
<span className={tier.text}>
|
||||||
|
<span className="hidden sm:inline">Context</span>
|
||||||
|
<span className="sm:hidden">Ctx</span>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={tier.text}
|
||||||
|
title={`Auto-compaction at ~${compactionThresholdPct}%`}
|
||||||
|
>
|
||||||
|
{used.toLocaleString()} / {max.toLocaleString()}{' '}
|
||||||
|
<span className="max-[380px]:hidden">({Math.round(pct * 100)}%)</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-1 h-1 rounded-full bg-muted overflow-hidden">
|
||||||
|
<div
|
||||||
|
className={`h-full ${tier.bar} transition-[width] duration-300`}
|
||||||
|
style={{ width: `${fillPct}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import { useChatContextStats } from '@/hooks/useChatContextStats';
|
|||||||
import { MessageList } from '@/components/MessageList';
|
import { MessageList } from '@/components/MessageList';
|
||||||
import { ChatInput } from '@/components/ChatInput';
|
import { ChatInput } from '@/components/ChatInput';
|
||||||
import { ChatContextPopover } from '@/components/ChatContextPopover';
|
import { ChatContextPopover } from '@/components/ChatContextPopover';
|
||||||
|
import { ContextBar } from '@/components/ContextBar';
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@@ -125,6 +126,10 @@ export function ChatPane({ sessionId, chatId, projectId, agentId, onAgentChange,
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full min-h-0">
|
<div className="flex flex-col h-full min-h-0">
|
||||||
|
{/* v1.11.2: persistent context-usage indicator. Renders null when there
|
||||||
|
are no assistant messages yet (fresh chat). shrink-0 keeps it out of
|
||||||
|
the MessageList scroll region — bar stays pinned, list scrolls. */}
|
||||||
|
<ContextBar messages={chatMessages} />
|
||||||
<MessageList messages={chatMessages} sessionChats={sessionChats} />
|
<MessageList messages={chatMessages} sessionChats={sessionChats} />
|
||||||
|
|
||||||
{/* Queued messages */}
|
{/* Queued messages */}
|
||||||
|
|||||||
Reference in New Issue
Block a user