import { Box, Text, useInput } from "ink"; import { memo, useMemo, useState } from "react"; import { useTerminalWidth } from "../hooks/useTerminalWidth"; import { colors } from "./colors"; type BashInfo = { toolName: string; command: string; description?: string; }; type Props = { bashInfo: BashInfo; onApprove: () => void; onApproveAlways: (scope: "project" | "session") => void; onDeny: (reason: string) => void; onCancel?: () => void; isFocused?: boolean; approveAlwaysText?: string; allowPersistence?: boolean; }; // Horizontal line character for Claude Code style const SOLID_LINE = "─"; /** * InlineBashApproval - Renders bash/shell approval UI inline (Claude Code style) * * Option 3 is an inline text input - when selected, user can type directly * without switching to a separate screen. */ export const InlineBashApproval = memo( ({ bashInfo, onApprove, onApproveAlways, onDeny, onCancel, isFocused = true, approveAlwaysText, allowPersistence = true, }: Props) => { const [selectedOption, setSelectedOption] = useState(0); const [customReason, setCustomReason] = useState(""); const columns = useTerminalWidth(); // Custom option index depends on whether "always" option is shown const customOptionIndex = allowPersistence ? 2 : 1; const maxOptionIndex = customOptionIndex; const isOnCustomOption = selectedOption === customOptionIndex; const customOptionPlaceholder = "No, and tell Letta Code what to do differently"; useInput( (input, key) => { if (!isFocused) return; // CTRL-C: cancel (queue denial, return to input) if (key.ctrl && input === "c") { onCancel?.(); return; } // Arrow navigation always works if (key.upArrow) { setSelectedOption((prev) => Math.max(0, prev - 1)); return; } if (key.downArrow) { setSelectedOption((prev) => Math.min(maxOptionIndex, prev + 1)); return; } // When on custom input option if (isOnCustomOption) { if (key.return) { if (customReason.trim()) { // User typed a reason - send it onDeny(customReason.trim()); } // If empty, do nothing (can't submit empty reason) return; } if (key.escape) { if (customReason) { // Clear text first setCustomReason(""); } else { // No text, cancel (queue denial, return to input) onCancel?.(); } return; } if (key.backspace || key.delete) { setCustomReason((prev) => prev.slice(0, -1)); return; } // Printable characters - append to custom reason if (input && !key.ctrl && !key.meta && input.length === 1) { setCustomReason((prev) => prev + input); } return; } // When on regular options if (key.return) { if (selectedOption === 0) { onApprove(); } else if (selectedOption === 1 && allowPersistence) { onApproveAlways("project"); } return; } if (key.escape) { // Cancel (queue denial, return to input) onCancel?.(); } }, { isActive: isFocused }, ); const solidLine = SOLID_LINE.repeat(Math.max(columns - 2, 10)); // Memoize the static command content so it doesn't re-render on keystroke // This prevents flicker when typing feedback in the custom input field const memoizedCommandContent = useMemo( () => ( <> {/* Top solid line */} {solidLine} {/* Header */} Run this command? {/* Command preview */} {bashInfo.command} {bashInfo.description && ( {bashInfo.description} )} ), [bashInfo.command, bashInfo.description, solidLine], ); // Hint text based on state const hintText = isOnCustomOption ? customReason ? "Enter to submit · Esc to clear" : "Type reason · Esc to cancel" : "Enter to select · Esc to cancel"; return ( {/* Static command content - memoized to prevent re-render on keystroke */} {memoizedCommandContent} {/* Options */} {/* Option 1: Yes */} {selectedOption === 0 ? "❯" : " "} 1. Yes {/* Option 2: Yes, always (only if persistence allowed) */} {allowPersistence && ( {selectedOption === 1 ? "❯" : " "} 2. {approveAlwaysText || "Yes, and don't ask again for this project"} )} {/* Custom input option (3 if persistence, 2 if not) */} {isOnCustomOption ? "❯" : " "} {customOptionIndex + 1}. {customReason ? ( {customReason} {isOnCustomOption && "█"} ) : ( {customOptionPlaceholder} {isOnCustomOption && "█"} )} {/* Hint */} {hintText} ); }, ); InlineBashApproval.displayName = "InlineBashApproval";