import { Box, Text, 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 { MarkdownDisplay } from "./MarkdownDisplay"; type Props = { plan: string; onApprove: () => void; onApproveAndAcceptEdits: () => void; onKeepPlanning: (reason: string) => void; isFocused?: boolean; }; // Horizontal line characters for Claude Code style const SOLID_LINE = "─"; const DOTTED_LINE = "╌"; /** * InlinePlanApproval - Renders plan approval UI inline (Claude Code style) * * Uses horizontal lines instead of boxes for visual styling: * - ──── solid line at top * - ╌╌╌╌ dotted line around plan content * - Approval options below */ export const InlinePlanApproval = memo( ({ plan, onApprove, onApproveAndAcceptEdits, onKeepPlanning, isFocused = true, }: Props) => { const [selectedOption, setSelectedOption] = useState(0); const { text: customReason, cursorPos, handleKey, clear, } = useTextInputCursor(); const columns = useTerminalWidth(); useProgressIndicator(); const customOptionIndex = 2; const maxOptionIndex = customOptionIndex; const isOnCustomOption = selectedOption === customOptionIndex; const customOptionPlaceholder = "Type here to tell Letta Code what to change"; useInput( (input, key) => { if (!isFocused) return; // CTRL-C: keep planning with cancel message if (key.ctrl && input === "c") { onKeepPlanning("User pressed CTRL-C to cancel"); 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()) { onKeepPlanning(customReason.trim()); } return; } if (key.escape) { if (customReason) { clear(); } else { onKeepPlanning("User cancelled"); } return; } // Handle text input (arrows, backspace, typing) if (handleKey(input, key)) return; } // When on regular options if (key.return) { if (selectedOption === 0) { onApproveAndAcceptEdits(); } else if (selectedOption === 1) { onApprove(); } return; } if (key.escape) { onKeepPlanning("User cancelled"); } }, { isActive: isFocused }, ); // Generate horizontal lines const solidLine = SOLID_LINE.repeat(Math.max(columns - 2, 10)); const dottedLine = DOTTED_LINE.repeat(Math.max(columns - 2, 10)); // Memoize the static plan content so it doesn't re-render on keystroke // This prevents flicker when typing feedback in the custom input field const memoizedPlanContent = useMemo( () => ( <> {/* Top solid line */} {solidLine} {/* Header */} Ready to code? Here is your plan: {/* Dotted separator before plan content */} {dottedLine} {/* Plan content - no indentation, just like Claude Code */} {/* Dotted separator after plan content */} {dottedLine} ), [plan, solidLine, dottedLine], ); // Hint text based on state const hintText = isOnCustomOption ? customReason ? "Enter to submit · Esc to clear" : "Type feedback · Esc to cancel" : "Enter to select · Esc to cancel"; return ( {/* Static plan content - memoized to prevent re-render on keystroke */} {memoizedPlanContent} {/* Question */} Would you like to proceed? {/* Options */} {/* Option 1: Yes, and auto-accept edits */} {selectedOption === 0 ? "❯" : " "} 1. Yes, and auto-accept edits {/* Option 2: Yes, and manually approve edits */} {selectedOption === 1 ? "❯" : " "} 2. Yes, and manually approve edits {/* Option 3: Custom input */} {isOnCustomOption ? "❯" : " "} 3. {customReason ? ( {customReason.slice(0, cursorPos)} {isOnCustomOption && "█"} {customReason.slice(cursorPos)} ) : ( {customOptionPlaceholder} {isOnCustomOption && "█"} )} {/* Hint */} {hintText} ); }, ); InlinePlanApproval.displayName = "InlinePlanApproval";