- Ember theme (Obsidian charcoal + #ff7a18 orange), now DEFAULT_THEME_ID; server theme_id whitelist gains 'ember' - Brand banner: transparent Westie mascot + >_BooCode wordmark, big/edge-to-edge (flood-filled to transparency + cropped) - Coder panes are multi-tab: + opens a BooCode tab, split opens a pane (shared ChatTabBar via tabKind + createCoderTab; closeOtherTabs/tab-numbering extended to coder) - Model-attribution: new messages.model column stamped at finalizeCompletion (BooChat/native coder) + dispatcher assistant-row creation (external coder); surfaced via view + wire types + live frame; rendered as a subtle shortened-name chip (shortenModelName) - Composer Web toggle moved into a boxed focus-ringed input; glowing accent dot on tool rows - Claude SDK follow-ups (1M context, follow-up-message fix, collapsed thinking/tool chips) + CLAUDE_SDK_BACKEND=1 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
123 lines
5.5 KiB
TypeScript
123 lines
5.5 KiB
TypeScript
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 (
|
|
<div className="flex items-center gap-2 flex-1 min-w-0">
|
|
<div className="flex-1 h-2 rounded-full bg-muted overflow-hidden min-w-0">
|
|
<div
|
|
className={`h-full ${tier.bar} transition-[width] duration-300`}
|
|
style={{ width: `${fillPct}%` }}
|
|
/>
|
|
</div>
|
|
<span
|
|
className={`${tier.text} text-[10px] font-mono whitespace-nowrap shrink-0`}
|
|
title={tooltipText}
|
|
>
|
|
{max !== null ? (
|
|
<>
|
|
{/* Absolute counts hidden on very narrow viewports so the
|
|
percentage always has room. Tooltip carries full detail. */}
|
|
<span className="max-[480px]:hidden">
|
|
{used.toLocaleString()} / {max.toLocaleString()}{' '}
|
|
</span>
|
|
({Math.round(pct * 100)}%)
|
|
</>
|
|
) : (
|
|
<>— / —</>
|
|
)}
|
|
</span>
|
|
</div>
|
|
);
|
|
}
|