/** * KHAELOR * File: src/tui/components/design-panel.ts * Description: The DesignArtifact panel — the workflow's most important surface (TUI v2 §5.2). * * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai */ import { truncateAnsi } from "../renderer/ansi.js"; import type { Theme } from "../theme.js"; export interface DesignPanelData { goal: string; filesTouched: string[]; approach: string; risks: string[]; verification: string; outOfScope: string[]; /** Panel mode: awaiting a decision, or already decided. */ decision: "pending" | "approved" | "rejected" | "auto-approved"; } function wrap(text: string, width: number): string[] { const words = text.split(/\s+/); const lines: string[] = []; let current = ""; for (const word of words) { if (current.length + word.length + 1 > width && current.length > 0) { lines.push(current); current = word; } else { current = current.length === 0 ? word : `${current} ${word}`; } } if (current.length > 0) lines.push(current); return lines; } /** * Bordered panel with the ember gradient on the frame while pending; collapses * to one settled line once decided: `✓ design approuvé · 2 fichiers` (TUI v2 §5.2). */ export function renderDesignPanel(data: DesignPanelData, width: number, theme: Theme): string[] { if (data.decision !== "pending") { const glyph = data.decision === "rejected" ? theme.paint("error", "✗") : theme.paint("success", "✓"); const label = data.decision === "rejected" ? "design rejected" : data.decision === "auto-approved" ? "design auto-approved" : "design approved"; const count = data.filesTouched.length; return [ truncateAnsi( ` ${glyph} ${label} · ${count} file${count === 1 ? "" : "s"} ${theme.paint("dim", `· ${data.goal.slice(0, 60)}`)}`, width, ), ]; } const inner = Math.max(30, Math.min(width - 4, 76)); const border = (s: string): string => theme.paintGradient("ember", s); const row = (label: string, text: string): string[] => wrap(text, inner - 11).map((line, index) => truncateAnsi( ` ${border("│")} ${theme.paint("dim", (index === 0 ? label : "").padEnd(9))}${line.padEnd(inner - 11)} ${border("│")}`, width, ), ); const lines: string[] = []; lines.push(truncateAnsi(` ${border(`╭─ ◑ DESIGN — approval required ${"─".repeat(Math.max(0, inner - 31))}╮`)}`, width)); lines.push(...row("Goal", data.goal)); lines.push(...row("Files", data.filesTouched.join(" · "))); lines.push(...row("Approach", data.approach)); if (data.risks.length > 0) lines.push(...row("Risks", data.risks.map((risk) => `▪ ${risk}`).join(" "))); lines.push(...row("Verify", data.verification)); if (data.outOfScope.length > 0) lines.push(...row("Not doing", data.outOfScope.join(" · "))); lines.push(truncateAnsi(` ${border(`├${"─".repeat(inner)}┤`)}`, width)); lines.push( truncateAnsi( ` ${border("│")} ${theme.paint("bold", "[a]")} approve ${theme.paint("bold", "[r]")} reject${" ".repeat(Math.max(0, inner - 25))} ${border("│")}`, width, ), ); lines.push(truncateAnsi(` ${border(`╰${"─".repeat(inner)}╯`)}`, width)); return lines; }