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/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