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.0 KB · 88 lines typescript
Raw Blame History
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