import { getCodexCollaborationModeLabel, getPermissionModeLabel, getPermissionModeTone, isPermissionModeAllowedForFlavor } from '@hapi/protocol' import type { PermissionModeTone } from '@hapi/protocol' import { useMemo } from 'react' import type { AgentState, CodexCollaborationMode, PermissionMode } from '@/types/api' import type { ConversationStatus } from '@/realtime/types' import type { ThreadGoal } from '@/types/api' import { getContextBudgetTokens } from '@/chat/modelConfig' import { formatCodexReasoningLabel, shouldShowCodexReasoningLabel } from '@/lib/codexStatusLabels' import { isFastServiceTier } from './codexFastMode' import { useTranslation } from '@/lib/use-translation' // Vibing messages for thinking state const VIBING_MESSAGES = [ "Accomplishing", "Actioning", "Actualizing", "Baking", "Booping", "Brewing", "Calculating", "Cerebrating", "Channelling", "Churning", "Clauding", "Coalescing", "Cogitating", "Computing", "Combobulating", "Concocting", "Conjuring", "Considering", "Contemplating", "Cooking", "Crafting", "Creating", "Crunching", "Deciphering", "Deliberating", "Determining", "Discombobulating", "Divining", "Doing", "Effecting", "Elucidating", "Enchanting", "Envisioning", "Finagling", "Flibbertigibbeting", "Forging", "Forming", "Frolicking", "Generating", "Germinating", "Hatching", "Herding", "Honking", "Ideating", "Imagining", "Incubating", "Inferring", "Manifesting", "Marinating", "Meandering", "Moseying", "Mulling", "Mustering", "Musing", "Noodling", "Percolating", "Perusing", "Philosophising", "Pontificating", "Pondering", "Processing", "Puttering", "Puzzling", "Reticulating", "Ruminating", "Scheming", "Schlepping", "Shimmying", "Simmering", "Smooshing", "Spelunking", "Spinning", "Stewing", "Sussing", "Synthesizing", "Thinking", "Tinkering", "Transmuting", "Unfurling", "Unravelling", "Vibing", "Wandering", "Whirring", "Wibbling", "Wizarding", "Working", "Wrangling" ] const PERMISSION_TONE_CLASSES: Record = { neutral: 'text-[var(--app-hint)]', info: 'text-blue-500', warning: 'text-amber-500', danger: 'text-red-500' } function getConnectionStatus( active: boolean, thinking: boolean, agentState: AgentState | null | undefined, voiceStatus: ConversationStatus | undefined, backgroundTaskCount: number, t: (key: string) => string ): { text: string; color: string; dotColor: string; isPulsing: boolean } { const hasPermissions = agentState?.requests && Object.keys(agentState.requests).length > 0 // Voice connecting takes priority if (voiceStatus === 'connecting') { return { text: t('voice.connecting'), color: 'text-[#007AFF]', dotColor: 'bg-[#007AFF]', isPulsing: true } } if (!active) { return { text: t('misc.offline'), color: 'text-[#999]', dotColor: 'bg-[#999]', isPulsing: false } } if (hasPermissions) { return { text: t('misc.permissionRequired'), color: 'text-[#FF9500]', dotColor: 'bg-[#FF9500]', isPulsing: true } } if (thinking) { const vibingMessage = VIBING_MESSAGES[Math.floor(Math.random() * VIBING_MESSAGES.length)].toLowerCase() + '…' return { text: vibingMessage, color: 'text-[#007AFF]', dotColor: 'bg-[#007AFF]', isPulsing: true } } if (backgroundTaskCount > 0) { return { text: `${backgroundTaskCount} background task${backgroundTaskCount > 1 ? 's' : ''} running`, color: 'text-[#007AFF]', dotColor: 'bg-[#007AFF]', isPulsing: true } } return { text: t('misc.online'), color: 'text-[#34C759]', dotColor: 'bg-[#34C759]', isPulsing: false } } function getContextWarning(contextSize: number, maxContextSize: number, t: (key: string, params?: Record) => string): { text: string; color: string } | null { const percentageUsed = (contextSize / maxContextSize) * 100 const percentageRemaining = Math.max(0, 100 - percentageUsed) const percent = Math.round(percentageRemaining) if (percentageRemaining <= 5) { return { text: t('misc.percentLeft', { percent }), color: 'text-red-500' } } else if (percentageRemaining <= 10) { return { text: t('misc.percentLeft', { percent }), color: 'text-amber-500' } } else { return { text: t('misc.percentLeft', { percent }), color: 'text-[var(--app-hint)]' } } } function formatTokenCount(value: number): string { if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M` if (value >= 1_000) return `${Math.round(value / 1_000)}k` return String(value) } export function shouldShowCodexFastBadge( agentFlavor: string | null | undefined, serviceTier: string | null | undefined ): boolean { return agentFlavor === 'codex' && isFastServiceTier(serviceTier) } export function StatusBar(props: { active: boolean thinking: boolean agentState: AgentState | null | undefined backgroundTaskCount?: number contextSize?: number contextCacheRead?: number contextWindow?: number | null model?: string | null modelReasoningEffort?: string | null serviceTier?: string | null permissionMode?: PermissionMode collaborationMode?: CodexCollaborationMode threadGoal?: ThreadGoal | null agentFlavor?: string | null voiceStatus?: ConversationStatus }) { const { t } = useTranslation() const connectionStatus = useMemo( () => getConnectionStatus(props.active, props.thinking, props.agentState, props.voiceStatus, props.backgroundTaskCount ?? 0, t), [props.active, props.thinking, props.agentState, props.voiceStatus, props.backgroundTaskCount, t] ) const contextWarning = useMemo( () => { if (props.contextSize === undefined) return null const maxContextSize = props.contextWindow ?? getContextBudgetTokens(props.model, props.agentFlavor) if (!maxContextSize) return null return getContextWarning(props.contextSize, maxContextSize, t) }, [props.contextSize, props.contextWindow, props.model, props.agentFlavor, t] ) const contextUsageLabel = useMemo(() => { if (props.contextSize === undefined) return null const maxContextSize = props.contextWindow ?? getContextBudgetTokens(props.model, props.agentFlavor) if (!maxContextSize) return `ctx ${formatTokenCount(props.contextSize)}` const percentageUsed = Math.min(100, Math.round((props.contextSize / maxContextSize) * 100)) return `ctx ${formatTokenCount(props.contextSize)}/${formatTokenCount(maxContextSize)} (${percentageUsed}%)` }, [props.contextSize, props.contextWindow, props.model, props.agentFlavor]) const compactContextUsageLabel = useMemo(() => { if (props.contextSize === undefined) return null const maxContextSize = props.contextWindow ?? getContextBudgetTokens(props.model, props.agentFlavor) if (!maxContextSize) return `ctx ${formatTokenCount(props.contextSize)}` const percentageLeft = Math.max(0, Math.round(100 - (props.contextSize / maxContextSize) * 100)) return `ctx ${formatTokenCount(maxContextSize).toUpperCase()}, ${percentageLeft}% left` }, [props.contextSize, props.contextWindow, props.model, props.agentFlavor]) const cacheHitLabel = useMemo(() => { if (!props.contextCacheRead || props.contextCacheRead <= 0) return null return `cache ${formatTokenCount(props.contextCacheRead)}` }, [props.contextCacheRead]) const permissionMode = props.permissionMode const displayPermissionMode = permissionMode && permissionMode !== 'default' && isPermissionModeAllowedForFlavor(permissionMode, props.agentFlavor) ? permissionMode : null const permissionModeLabel = displayPermissionMode ? getPermissionModeLabel(displayPermissionMode) : null const permissionModeTone = displayPermissionMode ? getPermissionModeTone(displayPermissionMode) : null const permissionModeColor = permissionModeTone ? PERMISSION_TONE_CLASSES[permissionModeTone] : 'text-[var(--app-hint)]' const displayCollaborationMode = props.agentFlavor === 'codex' && props.collaborationMode === 'plan' ? props.collaborationMode : null const collaborationModeLabel = displayCollaborationMode ? getCodexCollaborationModeLabel(displayCollaborationMode) : null const codexReasoningLabel = shouldShowCodexReasoningLabel(props.agentFlavor) ? formatCodexReasoningLabel(props.modelReasoningEffort) : null const codexFastMode = shouldShowCodexFastBadge(props.agentFlavor, props.serviceTier) const goalLabel = props.agentFlavor === 'codex' && props.threadGoal ? props.threadGoal.status === 'active' ? 'goal' : `goal ${props.threadGoal.status === 'budgetLimited' ? 'limited' : props.threadGoal.status}` : null return (
{connectionStatus.text}
{contextUsageLabel ? ( {compactContextUsageLabel} {contextUsageLabel}{contextWarning ? ` · ${contextWarning.text}` : ''} ) : null} {cacheHitLabel ? ( {cacheHitLabel} ) : null}
{codexReasoningLabel ? ( {codexReasoningLabel} ) : null} {codexFastMode ? ( fast ) : null} {goalLabel ? ( {goalLabel} ) : null} {collaborationModeLabel ? ( {collaborationModeLabel} ) : null} {displayPermissionMode ? ( {permissionModeLabel} ) : null}
) }