import { useEffect, useState } from 'react'; import { useContext } from '../../hooks/useContext'; import { useSession } from '../../hooks/useSession'; import type { TokenUsage } from '../../types'; import { Pill } from '../shared/Pill'; import { ClearRibbon } from './ClearRibbon'; import { CompactionRibbon } from './CompactionRibbon'; import { MessageBubble } from './MessageBubble'; import { UndoRibbon } from './UndoRibbon'; interface ContextTabProps { sessionId: string; /** Override starting agentId; defaults to 'main'. */ initialAgentId?: string; } export function ContextTab({ sessionId, initialAgentId = 'main' }: ContextTabProps) { const [agentId, setAgentId] = useState(initialAgentId); const [history, setHistory] = useState<'model' | 'full'>('model'); // Re-sync on session OR agent id change — see WireTab for the same // rationale (session navigation must reset a stale subagent pick). useEffect(() => { setAgentId(initialAgentId); }, [sessionId, initialAgentId]); const { data: detail } = useSession(sessionId); const { data: ctx, isLoading, error } = useContext(sessionId, agentId, history); const agents = detail?.agents ?? []; const messages = ctx?.messages ?? []; const sessionUsage = ctx?.usage.byScope.session ?? EMPTY_USAGE; const turnUsage = ctx?.usage.byScope.turn ?? EMPTY_USAGE; const cumulativeUsage: TokenUsage = { inputOther: sessionUsage.inputOther + turnUsage.inputOther, output: sessionUsage.output + turnUsage.output, inputCacheRead: sessionUsage.inputCacheRead + turnUsage.inputCacheRead, inputCacheCreation: sessionUsage.inputCacheCreation + turnUsage.inputCacheCreation, }; // Live context-window fill (latest step.end usage), distinct from the // cumulative session-scoped + turn-scoped spend the bar breaks down. const contextTokens = ctx?.contextTokens ?? 0; const config = ctx?.config ?? {}; const permissionMode = ctx?.permission.mode ?? null; const planActive = ctx?.planMode.active ?? false; const goal = ctx?.goal ?? null; const swarmActive = ctx?.swarm.active ?? false; return (
{/* Toolbar — agent selector + status pills */}
{messages.length} messages {config.modelAlias ? ( model{' '} {config.modelAlias} ) : null}
{/* History view toggle — 'model' (post-compaction, what the model sees) vs 'full' (full reconstructed history for debugging). */}
{permissionMode ? ( permission: {permissionMode} ) : null} {planActive ? ( plan mode ) : null} {swarmActive ? ( swarm mode ) : null}
{/* Full-history hint — clarifies that this is the reconstructed history and the model itself only sees the compacted view. */} {history === 'full' ? (
full reconstructed history — the model actually sees the compacted view
) : null} {/* Live context-window fill (contextTokens) + the 4-segment cumulative session-scoped and turn-scoped usage breakdown. */} {/* Message stream */}
{goal ? (
goal {goal.status ? {goal.status} : null}
{goal.objective}
{goal.completionCriterion ? (
done when: {goal.completionCriterion}
) : null}
) : null} {config.systemPrompt ? : null} {isLoading ? (
loading context…
) : error ? (
{(error as Error).message}
) : messages.length === 0 ? (
no messages — session has only lifecycle/config records so far.
) : ( messages.map((m) => { if (m.source === 'compaction_summary') { return ; } if (m.source === 'undo') { return ; } if (m.source === 'clear') { return ; } return ; }) )}
); } const EMPTY_USAGE: TokenUsage = { inputOther: 0, output: 0, inputCacheRead: 0, inputCacheCreation: 0, }; // Colors are chosen from the existing semantic palette so the bar reads // coherently with the rest of the app: // inputCacheRead = success (cache hit — the "good" share) // inputOther = info (billed input) // output = assistant (what the model produced) // inputCacheCreation = warning (billed once, amortised next call) const SEG_COLORS = { inputCacheRead: 'var(--color-sev-success)', inputOther: 'var(--color-sev-info)', output: 'var(--color-assistant)', inputCacheCreation: 'var(--color-sev-warning)', } as const; function TokenBar({ usage, contextTokens }: { usage: TokenUsage; contextTokens: number }) { const total = usage.inputOther + usage.output + usage.inputCacheRead + usage.inputCacheCreation; return (
{contextTokens > 0 ? (
context {contextTokens.toLocaleString()} tok
) : null} {total === 0 ? (
) : (
{usage.inputCacheRead > 0 ? (
) : null} {usage.inputOther > 0 ? (
) : null} {usage.output > 0 ? (
) : null} {usage.inputCacheCreation > 0 ? (
) : null}
)}
); } function seg(n: number, total: number): number { return (n / total) * 100; } function SystemPromptBubble({ text }: { text: string }) { const [open, setOpen] = useState(false); return (
          {text}
        
{!open ? (
); }