import { Box, useInput } from "ink"; import { memo, useMemo, useState } from "react"; import { useProgressIndicator } from "../hooks/useProgressIndicator"; import { useTerminalWidth } from "../hooks/useTerminalWidth"; import { useTextInputCursor } from "../hooks/useTextInputCursor"; import { colors } from "./colors"; import { Text } from "./Text"; type Props = { toolName: string; toolArgs: string; onApprove: () => void; onApproveAlways: (scope: "project" | "session") => void; onDeny: (reason: string) => void; onCancel?: () => void; isFocused?: boolean; approveAlwaysText?: string; allowPersistence?: boolean; showPreview?: boolean; defaultScope?: "project" | "session"; }; // Horizontal line character for Claude Code style const SOLID_LINE = "─"; /** * Format tool arguments for display */ function formatArgs(toolArgs: string): string { try { const parsed = JSON.parse(toolArgs); // Pretty print with 2-space indent, but limit length const formatted = JSON.stringify(parsed, null, 2); // Truncate if too long if (formatted.length > 500) { return `${formatted.slice(0, 500)}\n...`; } return formatted; } catch { // If not valid JSON, return as-is return toolArgs || "(no arguments)"; } } /** * InlineGenericApproval - Renders generic tool approval UI inline * * Used as fallback for any tool not handled by specialized inline components. */ export const InlineGenericApproval = memo( ({ toolName, toolArgs, onApprove, onApproveAlways, onDeny, onCancel, isFocused = true, approveAlwaysText, allowPersistence = true, showPreview = true, defaultScope = "project", }: Props) => { const [selectedOption, setSelectedOption] = useState(0); const { text: customReason, cursorPos, handleKey, clear, } = useTextInputCursor(); const columns = useTerminalWidth(); useProgressIndicator(); // 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()) { onDeny(customReason.trim()); } return; } if (key.escape) { if (customReason) { clear(); } else { onCancel?.(); } return; } // Handle text input (arrows, backspace, typing) if (handleKey(input, key)) return; } // When on regular options if (key.return) { if (selectedOption === 0) { onApprove(); } else if (selectedOption === 1 && allowPersistence) { onApproveAlways(defaultScope); } return; } if (key.escape) { onCancel?.(); return; } // Number keys for quick selection (only for fixed options, not custom text input) if (input === "1") { onApprove(); return; } if (input === "2" && allowPersistence) { onApproveAlways(defaultScope); return; } }, { isActive: isFocused }, ); // Generate horizontal line const solidLine = SOLID_LINE.repeat(Math.max(columns, 10)); const formattedArgs = formatArgs(toolArgs); // Memoize the static tool content so it doesn't re-render on keystroke // This prevents flicker when typing feedback in the custom input field const memoizedToolContent = useMemo( () => ( <> {/* Top solid line */} {solidLine} {/* Header */} Run {toolName}? {/* Arguments preview */} {formattedArgs} ), [toolName, formattedArgs, 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"; const optionsMarginTop = showPreview ? 1 : 0; return ( {/* Static tool content - memoized to prevent re-render on keystroke */} {showPreview && memoizedToolContent} {/* 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 */} {isOnCustomOption ? "❯" : " "} {customOptionIndex + 1}. {customReason ? ( {customReason.slice(0, cursorPos)} {isOnCustomOption && "█"} {customReason.slice(cursorPos)} ) : ( {customOptionPlaceholder} {isOnCustomOption && "█"} )} {/* Hint */} {hintText} ); }, ); InlineGenericApproval.displayName = "InlineGenericApproval";