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-line.ts4 * Description: The single agent status line — real states, real elapsed timers, the 2 Hz pulse (TUI_DESIGN §5).5 *6 * Author: Simon-Pierre Boucher7 * Contact: contact@spboucher.ai8 */910import { truncateAnsi } from "../renderer/ansi.js";11import type { StyleRole, Theme } from "../theme.js";1213export type AgentStateKind =14 | "idle"15 | "thinking"16 | "reading"17 | "searching"18 | "editing"19 | "running"20 | "waiting"21 | "verifying"22 | "stopping";2324export interface AgentStatus {25 kind: AgentStateKind;26 /** What, concretely: a path, a command, a query. Never fabricated. */27 detail?: string;28 /** Real timestamp when this state began (elapsed timer source). */29 startedAt: number;30 /** Real extracted progress like "41/148" — only when a parser produced it. */31 progress?: string;32}3334const LABELS: Record<Exclude<AgentStateKind, "idle" | "stopping">, string> = {35 thinking: "Thinking",36 reading: "Reading",37 searching: "Searching",38 editing: "Editing",39 running: "Running",40 waiting: "Waiting for permission",41 verifying: "Verifying",42};4344/** One saturated color per agent state — the word still carries the meaning. */45const STATE_COLOR: Record<Exclude<AgentStateKind, "idle" | "stopping">, StyleRole> = {46 thinking: "violet",47 reading: "cyan",48 searching: "teal",49 editing: "warning",50 running: "success",51 waiting: "orange",52 verifying: "magenta",53};5455/**56 * Render the one compact dynamic status line, or null when idle (the line57 * collapses and the composer moves up a row). Every element is real data:58 * elapsed comes from `now - startedAt`; `progress` appears only when actually59 * extracted. `●` pulses between two shades at ~2 Hz — the only animation in60 * the product; in monochrome the glyph itself carries the state.61 */62export function renderStatusLine(63 status: AgentStatus,64 now: number,65 theme: Theme,66 width: number,67): string | null {68 if (status.kind === "idle") return null;69 if (status.kind === "stopping") {70 return truncateAnsi(` ${theme.paint("warning", "◌")} Stopping…`, width);71 }7273 const pulseOn = Math.floor(now / 250) % 2 === 0;74 const color = STATE_COLOR[status.kind];75 const dot = theme.paint(pulseOn ? color : "accentDim", "●");7677 const parts: string[] = [theme.paint(color, LABELS[status.kind])];78 if (status.detail !== undefined && status.detail !== "") parts.push(status.detail);79 if (status.progress !== undefined && status.progress !== "") {80 parts.push(status.progress);81 } else {82 const elapsed = Math.max(0, now - status.startedAt) / 1000;83 if (elapsed >= 0.1) parts.push(`${elapsed.toFixed(1)}s`);84 }8586 const body = parts.join(theme.paint("dim", " · "));87 return truncateAnsi(` ${dot} ${body}`, width);88}89