▚ApprovalPromptcustom
Permission dialog with keyboard yes/no and selection state
$ termino approval-prompt — permission dialog
╭──────────────────────────────────────────╮
│? allow tool? │
│ run `git push` on current branch │
│ [y]es [n]o ←/→ select · enter confirm│
╰──────────────────────────────────────────╯
live auto-answers every 4s — real keys in terminal
##API
Beveled confirm dialog: `?` glyph + title, dimmed message, and a [y]es/[n]o key-cap row where the selected cap inverts (light bg, dark fg). Fires `onAnswer(bool)`.
properties
| prop | type | default | description |
|---|---|---|---|
| title / message | string | "allow tool?" / - | Dialog copy |
| onAnswer | (allow: boolean) => void | - | Result callback |
| focused | boolean | true | Keyboard gate |
| colors | Partial<NeoColors> | NEO | Bevel palette |
##Keymap
yAllow (yes)
n / escDeny (no)
← / → (h / l)Move selection between caps
enterConfirm current selection
##Notes
- ▸The key caps reuse the bevel colors — selection reads as a physical key pressed in.
- ▸Render it centered over a dimmed overlay (pair with Modal) for a real permission modal.
##Source
Real implementation — this is the code that runs in your terminal.
1import { createElement as h, useMemo, useState } from "react";2import { useKeyboard } from "@opentui/react";3import { Canvas } from "./canvas";4import { cellsWidth } from "./chart";5import { NEO, neoPanel, type NeoColors } from "./neo";67export interface ApprovalPromptProps {8 title?: string;9 message?: string;10 onAnswer?: (allow: boolean) => void;11 focused?: boolean;12 color?: string;13 colors?: Partial<NeoColors>;14}1516export function ApprovalPrompt({17 title = "allow tool?",18 message = "run `git push` on current branch",19 onAnswer,20 focused = true,21 color = "#f0c674",22 colors,23}: Readonly<ApprovalPromptProps>) {24 const c = { ...NEO, ...colors };25 const [sel, setSel] = useState<"y" | "n">("y");2627 useKeyboard((key) => {28 if (!focused) return;29 if (key.name === "left" || key.name === "h") {30 setSel("y");31 } else if (key.name === "right" || key.name === "l") {32 setSel("n");33 } else if (key.name === "y") {34 onAnswer?.(true);35 } else if (key.name === "n" || key.name === "escape") {36 onAnswer?.(false);37 } else if (key.name === "return") {38 onAnswer?.(sel === "y");39 }40 });4142 const rows = useMemo(() => {43 const titleRow: { ch: string; fg: string; bg?: string }[] = [44 { ch: "?", fg: color },45 { ch: ` ${title}`, fg: c.text },46 ];47 const msgRow: { ch: string; fg: string; bg?: string }[] = [48 { ch: ` ${message ?? ""}`, fg: c.dim },49 ];50 const key = (label: string, active: boolean) => ({51 ch: ` ${label} `,52 fg: active ? c.dark : c.text,53 bg: active ? c.light : undefined,54 });55 const hintRow: { ch: string; fg: string; bg?: string }[] = [56 { ch: " ", fg: c.dim },57 key("[y]es", sel === "y"),58 { ch: " ", fg: c.dim },59 key("[n]o", sel === "n"),60 { ch: " ←/→ select · enter confirm", fg: c.dim },61 ];62 return [titleRow, msgRow, hintRow];63 // eslint-disable-next-line react-hooks/exhaustive-deps64 }, [title, message, sel, color, colors]);6566 const panel = neoPanel(rows, c);67 const width = cellsWidth(panel[0] ?? []);68 return h("box", { flexDirection: "column", gap: 0, width }, h(Canvas, { rows: panel, width }));69}70