termino
~/docs/custom/approval-prompt

ApprovalPromptcustom

Permission dialog with keyboard yes/no and selection state

~/custom/approval-prompt
live
$ 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
proptypedefaultdescription
title / messagestring"allow tool?" / -Dialog copy
onAnswer(allow: boolean) => void-Result callback
focusedbooleantrueKeyboard gate
colorsPartial<NeoColors>NEOBevel palette

##Keymap

~/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.

tsxapproval-prompt.tsx
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";
6
7export interface ApprovalPromptProps {
8 title?: string;
9 message?: string;
10 onAnswer?: (allow: boolean) => void;
11 focused?: boolean;
12 color?: string;
13 colors?: Partial<NeoColors>;
14}
15
16export 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");
26
27 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 });
41
42 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-deps
64 }, [title, message, sel, color, colors]);
65
66 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