/** * Terminal-native permission confirmations for `spectral serve`. * * In serve mode there is no TUI, but extensions (notably desktop-control) * still call `ctx.ui.confirm(...)` to gate potentially harmful actions. The * user is chatting via the web UI, while `spectral serve` itself runs in a * real terminal — so the confirmation is rendered there, on serve's stdout, * and answered with a single keypress. * * Behavior: * - Non-TTY stdin: resolves `false` immediately (honest headless deny). * - The prompt is a boxed render with a key legend: Enter/y = Allow, * n/Esc/q = Deny, Ctrl+C = Deny (consumed as data in raw mode — it must * never kill the serve process). * - Stdin is put into raw mode for the duration of the prompt and always * restored afterwards. * - Title and message are sanitized before rendering: ANSI escape sequences * and control characters are stripped (newlines collapse to spaces) so a * malicious extension cannot inject terminal escapes. * - Concurrent confirmations (multiple sessions share one terminal) are * serialized through a module-level queue; queued requests print a short * "waiting" notice. * - Timeouts: `SPECTRAL_CONFIRM_TIMEOUT_MS` env (default 5 min) or the * per-dialog `opts.timeout`. On timeout the confirmation resolves `false`. * - `onOutcome` reports how the dialog ended exactly once, letting callers * (e.g. desktop-control) distinguish an explicit user denial from * auto-deny causes (timeout, non-TTY, abort) that must not lock sessions. * * The prompt logic is pure and takes injectable IO (`write` + `readKey`) so * tests run without a TTY. */ /** Injectable terminal IO for the confirmation prompt. */ export interface ConfirmPromptIO { /** Write text to the terminal (serve's stdout). */ write(text: string): void; /** * Start listening for the next keypress and pass raw stdin data to * `handler`. Returns a cancel function that must detach the listener and * restore any terminal state (raw mode). */ readKey(handler: (data: string) => void): () => void; } /** Minimal shape of a stdin stream the default IO implementation needs. */ export interface ConfirmStdin { readonly isTTY?: boolean; readonly isRaw?: boolean; setRawMode?(mode: boolean): unknown; on(event: "data", listener: (chunk: Buffer | string) => void): unknown; off(event: "data", listener: (chunk: Buffer | string) => void): unknown; resume(): unknown; } export type ConfirmKeyAction = "allow" | "deny" | "ignore"; /** * How a confirmation ended. `deny` is an explicit human decision (deny key); * `timeout`, `no_tty` and `cancelled` mean the dialog ended without a user * answer. Same values as `ExtensionConfirmOutcome`. */ export type TerminalConfirmOutcome = "allow" | "deny" | "timeout" | "no_tty" | "cancelled"; /** Default confirmation timeout: 5 minutes. */ export declare const DEFAULT_CONFIRM_TIMEOUT_MS = 300000; /** * Classify a raw keypress. Only single-byte keys are acted on; multi-byte * sequences (arrow keys, pastes) are ignored so they can't accidentally * answer the prompt. */ export declare function classifyConfirmKey(data: string): ConfirmKeyAction; /** * Strip ANSI escape sequences and control characters from extension-provided * text. Newlines become spaces (the box renders one wrapped block); every * other C0 control character (incl. \r and tab), DEL and C1 are removed, as * is any stray ESC left over from an unterminated sequence. Keeps the prompt * box and result lines safe against terminal escape injection. */ export declare function sanitizeConfirmText(text: string): string; /** * Render the confirmation prompt as a boxed block of text (with trailing * newline). Pure — no IO. */ export declare function renderConfirmBox(title: string, message: string): string; /** Read the confirmation timeout from the environment (ms). */ export declare function parseConfirmTimeoutMs(env?: NodeJS.ProcessEnv): number; export interface TerminalConfirmOptions { title: string; message: string; io: ConfirmPromptIO; /** Whether stdin is an interactive TTY. Defaults to `process.stdin.isTTY`. */ isTTY?: boolean; /** Timeout in ms. Defaults to `parseConfirmTimeoutMs()` (env-driven). */ timeoutMs?: number; /** AbortSignal to programmatically deny the dialog. */ signal?: AbortSignal; /** Called when the prompt is actually rendered (not while queued). */ onShown?: () => void; /** * Called exactly once with the final outcome, before the promise settles. * `deny` means an explicit human denial; `timeout`/`no_tty`/`cancelled` * are auto-deny causes (no user decision was made). */ onOutcome?: (outcome: TerminalConfirmOutcome) => void; } /** * Run a single terminal confirmation. Resolves `true` (allow) or `false` * (deny / timeout / abort / non-TTY). Never throws. */ export declare function runTerminalConfirm(options: TerminalConfirmOptions): Promise; /** * Request a terminal confirmation, serialized with all other pending * confirmations in this process. Queued requests print a short notice. */ export declare function requestTerminalConfirm(options: TerminalConfirmOptions): Promise; /** * Default IO bound to the serve process's real terminal: writes to stdout, * reads raw keypresses from stdin (entering raw mode for the duration and * restoring the previous mode on cancel). */ export declare function createStdinConfirmIO(stdin?: ConfirmStdin): ConfirmPromptIO; /** Wire event shape emitted when a confirm prompt is actually shown. */ export interface BridgeConfirmNotifyEvent { type: "agent_notification"; message: string; level: "info" | "warning" | "error"; } /** Per-dialog options as passed by extensions (`ExtensionUIDialogOptions`). */ export interface BridgeConfirmDialogOptions { /** AbortSignal to programmatically deny the dialog. */ signal?: AbortSignal; /** Per-dialog timeout in ms (auto-deny when it fires). */ timeout?: number; /** Outcome side-channel, forwarded to `runTerminalConfirm.onOutcome`. */ onOutcome?: (outcome: TerminalConfirmOutcome) => void; } /** * Build the `confirm(title, message, opts?)` implementation used by the * serve-mode headless UI context: routes through `requestTerminalConfirm` * (cross-session queue), maps `opts.timeout` → `timeoutMs` and * `opts.signal` → `signal`, forwards `opts.onOutcome` (auto-deny vs explicit * user denial side-channel), and emits an `agent_notification` (level * "warning") once the prompt is rendered so web users know to look at the * terminal. `io` defaults to the real stdin IO; `isTTY` overrides the TTY * probe (defaults to `process.stdin.isTTY`, i.e. headless-safe). */ export declare function createBridgeConfirm(emit: (event: BridgeConfirmNotifyEvent) => void, io?: ConfirmPromptIO, opts?: { isTTY?: boolean; }): (title: string, message: string, dialog?: BridgeConfirmDialogOptions) => Promise; //# sourceMappingURL=terminal-confirm.d.ts.map