SPB Git

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%
3.9 KB · 95 lines typescript
Raw Blame History
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