/** * KHAELOR * File: src/tui/components/permission-panel.ts * Description: The inline permission panel — CLAUDE.md §13 verbatim contract; render only, decisions surface as callbacks. * * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai */ import { padEndAnsi, truncateAnsi, visibleWidth } from "../renderer/ansi.js"; import type { Theme } from "../theme.js"; export interface PermissionPanelData { /** Action verb, e.g. `Run`, `Edit`. */ verb: string; /** The subject: the command, or the target path. */ subject: string; /** Working directory (for commands). */ cwd?: string; /** ≤ 8-line diff preview (for edits) — already styled lines. */ diffPreview?: string[]; /** * Generalized "always allow" pattern from conservative shell-word analysis * (PERMISSION_MODEL §3.3). Absent → the [A] row is omitted entirely: no * persistable generalization is offered for what the analyzer could not read. */ alwaysPattern?: string; } /** * Render the permission panel as live-region overlay lines. Three keys, no * typing, milliseconds (TUI_DESIGN §9.1). This component renders only — * Enter/A/Esc handling lives in the app's key dispatch. */ export function renderPermissionPanel( data: PermissionPanelData, width: number, theme: Theme, ): string[] { // Size the box to its widest content row (clamped to the terminal). const alwaysRow = data.alwaysPattern !== undefined ? `[ A ] Always allow "${data.alwaysPattern}" in this project` : ""; const contentWidth = Math.max( 31, // title + option rows minimum visibleWidth(data.subject), visibleWidth(data.cwd ?? ""), visibleWidth(alwaysRow), ...(data.diffPreview ?? []).map((l) => visibleWidth(l)), ); const inner = Math.max(34, Math.min(width - 4, contentWidth + 4)); const dim = (s: string): string => theme.paint("accentDim", s); const row = (content: string): string => " " + dim("│") + padEndAnsi(truncateAnsi(" " + content, inner - 1), inner) + dim("│"); const out: string[] = []; const title = "─ KHAELOR requests permission "; out.push(" " + dim("╭" + title + "─".repeat(Math.max(0, inner - visibleWidth(title))) + "╮")); out.push(row(theme.paint("bold", data.verb))); out.push(row(theme.paint("accent", data.subject))); if (data.diffPreview !== undefined && data.diffPreview.length > 0) { out.push(row("")); for (const line of data.diffPreview.slice(0, 8)) out.push(row(line)); } if (data.cwd !== undefined) { out.push(row("")); out.push(row(theme.paint("dim", "Working directory"))); out.push(row(data.cwd)); } out.push(row("")); out.push(row(`${theme.paint("accent", "[ Enter ]")} Allow once`)); if (data.alwaysPattern !== undefined) { out.push( row( `${theme.paint("accent", "[ A ]")} Always allow ${theme.paint("bold", `"${data.alwaysPattern}"`)} in this project`, ), ); } out.push(row(`${theme.paint("accent", "[ Esc ]")} Deny`)); out.push(" " + dim("╰" + "─".repeat(inner) + "╯")); return out; }