/** * KHAELOR * File: src/tui/components/status-line.ts * Description: The single agent status line — real states, real elapsed timers, the 2 Hz pulse (TUI_DESIGN §5). * * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai */ import { truncateAnsi } from "../renderer/ansi.js"; import type { StyleRole, Theme } from "../theme.js"; export type AgentStateKind = | "idle" | "thinking" | "reading" | "searching" | "editing" | "running" | "waiting" | "verifying" | "stopping"; export interface AgentStatus { kind: AgentStateKind; /** What, concretely: a path, a command, a query. Never fabricated. */ detail?: string; /** Real timestamp when this state began (elapsed timer source). */ startedAt: number; /** Real extracted progress like "41/148" — only when a parser produced it. */ progress?: string; } const LABELS: Record, string> = { thinking: "Thinking", reading: "Reading", searching: "Searching", editing: "Editing", running: "Running", waiting: "Waiting for permission", verifying: "Verifying", }; /** One saturated color per agent state — the word still carries the meaning. */ const STATE_COLOR: Record, StyleRole> = { thinking: "violet", reading: "cyan", searching: "teal", editing: "warning", running: "success", waiting: "orange", verifying: "magenta", }; /** * Render the one compact dynamic status line, or null when idle (the line * collapses and the composer moves up a row). Every element is real data: * elapsed comes from `now - startedAt`; `progress` appears only when actually * extracted. `●` pulses between two shades at ~2 Hz — the only animation in * the product; in monochrome the glyph itself carries the state. */ export function renderStatusLine( status: AgentStatus, now: number, theme: Theme, width: number, ): string | null { if (status.kind === "idle") return null; if (status.kind === "stopping") { return truncateAnsi(` ${theme.paint("warning", "◌")} Stopping…`, width); } const pulseOn = Math.floor(now / 250) % 2 === 0; const color = STATE_COLOR[status.kind]; const dot = theme.paint(pulseOn ? color : "accentDim", "●"); const parts: string[] = [theme.paint(color, LABELS[status.kind])]; if (status.detail !== undefined && status.detail !== "") parts.push(status.detail); if (status.progress !== undefined && status.progress !== "") { parts.push(status.progress); } else { const elapsed = Math.max(0, now - status.startedAt) / 1000; if (elapsed >= 0.1) parts.push(`${elapsed.toFixed(1)}s`); } const body = parts.join(theme.paint("dim", " · ")); return truncateAnsi(` ${dot} ${body}`, width); }