import { Box, Text, useInput } from "ink";
import { memo, useMemo, useState } from "react";
import { useTerminalWidth } from "../hooks/useTerminalWidth";
import { useTextInputCursor } from "../hooks/useTextInputCursor";
import { colors } from "./colors";
type Props = {
taskInfo: {
subagentType: string;
description: string;
prompt: string;
model?: string;
};
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 = "─";
/**
* Truncate text to max length with ellipsis
*/
function truncate(text: string, maxLength: number): string {
if (text.length <= maxLength) return text;
return `${text.slice(0, maxLength - 3)}...`;
}
/**
* InlineTaskApproval - Renders Task tool approval UI inline with pretty formatting
*
* Shows subagent type, description, and prompt in a readable format.
*/
export const InlineTaskApproval = memo(
({
taskInfo,
onApprove,
onApproveAlways,
onDeny,
onCancel,
isFocused = true,
approveAlwaysText,
allowPersistence = true,
}: Props) => {
const [selectedOption, setSelectedOption] = useState(0);
const {
text: customReason,
cursorPos,
handleKey,
clear,
} = useTextInputCursor();
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()) {
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("session");
}
return;
}
if (key.escape) {
onCancel?.();
}
},
{ isActive: isFocused },
);
// Generate horizontal line
const solidLine = SOLID_LINE.repeat(Math.max(columns - 2, 10));
const contentWidth = Math.max(0, columns - 4); // 2 padding on each side
// Memoize the static task content so it doesn't re-render on keystroke
const memoizedTaskContent = useMemo(() => {
const { subagentType, description, prompt, model } = taskInfo;
// Truncate prompt if too long (show first ~200 chars)
const truncatedPrompt = truncate(prompt, 300);
return (
<>
{/* Top solid line */}
{solidLine}
{/* Header */}
Run Task?
{/* Task details */}
{/* Subagent type */}
Type:
{subagentType}
{/* Model (if specified) */}
{model && (
Model:
{model}
)}
{/* Description */}
Description:
{description}
{/* Prompt */}
Prompt:
{truncatedPrompt}
>
);
}, [taskInfo, solidLine, contentWidth]);
// 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";
// Generate "always" text for Task tool
const alwaysText =
approveAlwaysText || "Yes, allow Task operations during this session";
return (
{/* Static task content - memoized to prevent re-render on keystroke */}
{memoizedTaskContent}
{/* Options */}
{/* Option 1: Yes */}
{selectedOption === 0 ? "❯" : " "} 1.
Yes
{/* Option 2: Yes, always (only if persistence allowed) */}
{allowPersistence && (
{selectedOption === 1 ? "❯" : " "} 2.
{alwaysText}
)}
{/* Custom input option */}
{isOnCustomOption ? "❯" : " "} {customOptionIndex + 1}.
{customReason ? (
{customReason.slice(0, cursorPos)}
{isOnCustomOption && "█"}
{customReason.slice(cursorPos)}
) : (
{customOptionPlaceholder}
{isOnCustomOption && "█"}
)}
{/* Hint */}
{hintText}
);
},
);
InlineTaskApproval.displayName = "InlineTaskApproval";