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";