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