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.2 KB · 91 lines typescript
Raw Blame History
1/**2 * KHAELOR3 * File: src/tui/components/design-panel.ts4 * Description: The DesignArtifact panel — the workflow's most important surface (TUI v2 §5.2).5 *6 * Author: Simon-Pierre Boucher7 * Contact: contact@spboucher.ai8 */910import { truncateAnsi } from "../renderer/ansi.js";11import type { Theme } from "../theme.js";1213export interface DesignPanelData {14  goal: string;15  filesTouched: string[];16  approach: string;17  risks: string[];18  verification: string;19  outOfScope: string[];20  /** Panel mode: awaiting a decision, or already decided. */21  decision: "pending" | "approved" | "rejected" | "auto-approved";22}2324function wrap(text: string, width: number): string[] {25  const words = text.split(/\s+/);26  const lines: string[] = [];27  let current = "";28  for (const word of words) {29    if (current.length + word.length + 1 > width && current.length > 0) {30      lines.push(current);31      current = word;32    } else {33      current = current.length === 0 ? word : `${current} ${word}`;34    }35  }36  if (current.length > 0) lines.push(current);37  return lines;38}3940/**41 * Bordered panel with the ember gradient on the frame while pending; collapses42 * to one settled line once decided: `✓ design approuvé · 2 fichiers` (TUI v2 §5.2).43 */44export function renderDesignPanel(data: DesignPanelData, width: number, theme: Theme): string[] {45  if (data.decision !== "pending") {46    const glyph =47      data.decision === "rejected" ? theme.paint("error", "✗") : theme.paint("success", "✓");48    const label =49      data.decision === "rejected"50        ? "design rejected"51        : data.decision === "auto-approved"52          ? "design auto-approved"53          : "design approved";54    const count = data.filesTouched.length;55    return [56      truncateAnsi(57        ` ${glyph} ${label} · ${count} file${count === 1 ? "" : "s"} ${theme.paint("dim", `· ${data.goal.slice(0, 60)}`)}`,58        width,59      ),60    ];61  }6263  const inner = Math.max(30, Math.min(width - 4, 76));64  const border = (s: string): string => theme.paintGradient("ember", s);65  const row = (label: string, text: string): string[] =>66    wrap(text, inner - 11).map((line, index) =>67      truncateAnsi(68        ` ${border("│")} ${theme.paint("dim", (index === 0 ? label : "").padEnd(9))}${line.padEnd(inner - 11)} ${border("│")}`,69        width,70      ),71    );7273  const lines: string[] = [];74  lines.push(truncateAnsi(` ${border(`╭─ ◑ DESIGN — approval required ${"─".repeat(Math.max(0, inner - 31))}╮`)}`, width));75  lines.push(...row("Goal", data.goal));76  lines.push(...row("Files", data.filesTouched.join(" · ")));77  lines.push(...row("Approach", data.approach));78  if (data.risks.length > 0) lines.push(...row("Risks", data.risks.map((risk) => `▪ ${risk}`).join("  ")));79  lines.push(...row("Verify", data.verification));80  if (data.outOfScope.length > 0) lines.push(...row("Not doing", data.outOfScope.join(" · ")));81  lines.push(truncateAnsi(` ${border(`├${"─".repeat(inner)}┤`)}`, width));82  lines.push(83    truncateAnsi(84      ` ${border("│")} ${theme.paint("bold", "[a]")} approve   ${theme.paint("bold", "[r]")} reject${" ".repeat(Math.max(0, inner - 25))} ${border("│")}`,85      width,86    ),87  );88  lines.push(truncateAnsi(` ${border(`╰${"─".repeat(inner)}╯`)}`, width));89  return lines;90}91