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/permission-panel.ts4 * Description: The inline permission panel — CLAUDE.md §13 verbatim contract; render only, decisions surface as callbacks.5 *6 * Author: Simon-Pierre Boucher7 * Contact: contact@spboucher.ai8 */910import { padEndAnsi, truncateAnsi, visibleWidth } from "../renderer/ansi.js";11import type { Theme } from "../theme.js";1213export interface PermissionPanelData {14 /** Action verb, e.g. `Run`, `Edit`. */15 verb: string;16 /** The subject: the command, or the target path. */17 subject: string;18 /** Working directory (for commands). */19 cwd?: string;20 /** ≤ 8-line diff preview (for edits) — already styled lines. */21 diffPreview?: string[];22 /**23 * Generalized "always allow" pattern from conservative shell-word analysis24 * (PERMISSION_MODEL §3.3). Absent → the [A] row is omitted entirely: no25 * persistable generalization is offered for what the analyzer could not read.26 */27 alwaysPattern?: string;28}2930/**31 * Render the permission panel as live-region overlay lines. Three keys, no32 * typing, milliseconds (TUI_DESIGN §9.1). This component renders only —33 * Enter/A/Esc handling lives in the app's key dispatch.34 */35export function renderPermissionPanel(36 data: PermissionPanelData,37 width: number,38 theme: Theme,39): string[] {40 // Size the box to its widest content row (clamped to the terminal).41 const alwaysRow =42 data.alwaysPattern !== undefined43 ? `[ A ] Always allow "${data.alwaysPattern}" in this project`44 : "";45 const contentWidth = Math.max(46 31, // title + option rows minimum47 visibleWidth(data.subject),48 visibleWidth(data.cwd ?? ""),49 visibleWidth(alwaysRow),50 ...(data.diffPreview ?? []).map((l) => visibleWidth(l)),51 );52 const inner = Math.max(34, Math.min(width - 4, contentWidth + 4));53 const dim = (s: string): string => theme.paint("accentDim", s);5455 const row = (content: string): string =>56 " " + dim("│") + padEndAnsi(truncateAnsi(" " + content, inner - 1), inner) + dim("│");5758 const out: string[] = [];59 const title = "─ KHAELOR requests permission ";60 out.push(" " + dim("╭" + title + "─".repeat(Math.max(0, inner - visibleWidth(title))) + "╮"));61 out.push(row(theme.paint("bold", data.verb)));62 out.push(row(theme.paint("accent", data.subject)));6364 if (data.diffPreview !== undefined && data.diffPreview.length > 0) {65 out.push(row(""));66 for (const line of data.diffPreview.slice(0, 8)) out.push(row(line));67 }6869 if (data.cwd !== undefined) {70 out.push(row(""));71 out.push(row(theme.paint("dim", "Working directory")));72 out.push(row(data.cwd));73 }7475 out.push(row(""));76 out.push(row(`${theme.paint("accent", "[ Enter ]")} Allow once`));77 if (data.alwaysPattern !== undefined) {78 out.push(79 row(80 `${theme.paint("accent", "[ A ]")} Always allow ${theme.paint("bold", `"${data.alwaysPattern}"`)} in this project`,81 ),82 );83 }84 out.push(row(`${theme.paint("accent", "[ Esc ]")} Deny`));85 out.push(" " + dim("╰" + "─".repeat(inner) + "╯"));86 return out;87}88