spb/khaelor Public
KHAELOR — a terminal-native autonomous engineering agent powered by Anthropic.
TypeScript 82.9%
HTML 14.9%
CSS 1.1%
JavaScript 0.7%
1/**2 * KHAELOR3 * File: src/tui/components/status-bar.ts4 * Description: The bottom status bar — real segments only, width-responsive degradation (TUI_DESIGN §11).5 *6 * Author: Simon-Pierre Boucher7 * Contact: contact@spboucher.ai8 */910import { truncateAnsi } from "../renderer/ansi.js";11import type { Theme } from "../theme.js";1213export interface StatusBarData {14 /** Git branch (repo watcher). Absent when not in a repo. */15 branch?: string;16 /** Dirty counts as `+added −removed` (absent when clean or unknown). */17 dirty?: { added: number; removed: number };18 /** Configured model id. */19 model?: string;20 /** Short alias for narrow widths (falls back to a heuristic shortening). */21 modelAlias?: string;22 /** Real context utilization percent (usage vs usable window). */23 contextPct?: number;24 /** Real session cost from API usage metadata — absent when unpriceable. */25 costUsd?: number;26 /** Running background processes. */27 processCount?: number;28 /** Queued steering messages. */29 queuedCount?: number;30}3132function shortModel(data: StatusBarData): string | undefined {33 if (data.modelAlias !== undefined) return data.modelAlias;34 if (data.model === undefined) return undefined;35 // "claude-sonnet-4-5" → "sonnet"; unknown shapes keep their id.36 const m = /^claude-([a-z]+)/.exec(data.model);37 return m ? (m[1] as string) : data.model;38}3940function contextSegment(data: StatusBarData, theme: Theme, withWord: boolean): string | undefined {41 if (data.contextPct === undefined) return undefined;42 const pct = Math.round(data.contextPct);43 let text = withWord ? `context ${pct}%` : `${pct}%`;44 if (pct >= 80) text += " · /compact";45 if (pct >= 90) return theme.paint("error", text);46 if (pct >= 80) return theme.paint("warning", text);47 return theme.paint("teal", text);48}4950/**51 * Render the status bar for the current width. Segments with no real data52 * are absent, not zeroed (Absolute Rule #4). Lower-priority segments drop53 * whole as width shrinks — never truncated mid-token. Each segment carries54 * its own accent (branch cyan · model violet · context teal · cost orange);55 * separators stay dim, monochrome text is unchanged.56 */57export function renderStatusBar(data: StatusBarData, width: number, theme: Theme): string {58 const segments: string[] = [];5960 const branchFull =61 data.branch !== undefined62 ? data.dirty && (data.dirty.added > 0 || data.dirty.removed > 0)63 ? `${theme.paint("cyan", data.branch)} ${theme.paint("success", `+${data.dirty.added}`)} ${theme.paint("error", `−${data.dirty.removed}`)}`64 : theme.paint("cyan", data.branch)65 : undefined;6667 if (width < 60) {68 if (data.branch !== undefined) segments.push(theme.paint("cyan", data.branch));69 const ctx = contextSegment(data, theme, false);70 if (ctx !== undefined) segments.push(ctx);71 } else if (width < 80) {72 if (branchFull !== undefined) segments.push(branchFull);73 const alias = shortModel(data);74 if (alias !== undefined) segments.push(theme.paint("violet", alias));75 const ctx = contextSegment(data, theme, false);76 if (ctx !== undefined) segments.push(ctx);77 } else {78 if (branchFull !== undefined) segments.push(branchFull);79 const model = width < 100 ? shortModel(data) : data.model;80 if (model !== undefined) segments.push(theme.paint("violet", model));81 const ctx = contextSegment(data, theme, true);82 if (ctx !== undefined) segments.push(ctx);83 if (data.costUsd !== undefined) segments.push(theme.paint("orange", `$${data.costUsd.toFixed(2)}`));84 if (data.processCount !== undefined && data.processCount > 0) {85 segments.push(`${theme.paint("accent", "●")} ${data.processCount}`);86 }87 if (data.queuedCount !== undefined && data.queuedCount > 0) {88 segments.push(theme.paint("warning", `⋯ ${data.queuedCount} queued`));89 }90 }9192 const bar = segments.join(theme.paint("dim", " │ "));93 return truncateAnsi(" " + bar, width);94}95