feat: letta code
This commit is contained in:
199
src/cli/components/ApprovalDialog.tsx
Normal file
199
src/cli/components/ApprovalDialog.tsx
Normal file
@@ -0,0 +1,199 @@
|
||||
// Import useInput from vendored Ink for bracketed paste support
|
||||
import { Box, Text, useInput } from "ink";
|
||||
import RawTextInput from "ink-text-input";
|
||||
import { type ComponentType, useMemo, useState } from "react";
|
||||
import { type AdvancedDiffSuccess, computeAdvancedDiff } from "../helpers/diff";
|
||||
import type { ApprovalRequest } from "../helpers/stream";
|
||||
import { AdvancedDiffRenderer } from "./AdvancedDiffRenderer";
|
||||
|
||||
type Props = {
|
||||
approvalRequest: ApprovalRequest;
|
||||
onApprove: () => void;
|
||||
onApproveAlways: () => void;
|
||||
onDeny: (reason: string) => void;
|
||||
};
|
||||
|
||||
export function ApprovalDialog({
|
||||
approvalRequest,
|
||||
onApprove,
|
||||
onApproveAlways,
|
||||
onDeny,
|
||||
}: Props) {
|
||||
const [selectedOption, setSelectedOption] = useState(0);
|
||||
const [isEnteringReason, setIsEnteringReason] = useState(false);
|
||||
const [denyReason, setDenyReason] = useState("");
|
||||
|
||||
const options = [
|
||||
"Approve (once)",
|
||||
"Approve and don't ask again",
|
||||
"Deny and provide feedback",
|
||||
];
|
||||
|
||||
useInput((_input, key) => {
|
||||
if (isEnteringReason) {
|
||||
// When entering reason, only handle enter/escape
|
||||
if (key.return) {
|
||||
onDeny(denyReason);
|
||||
} else if (key.escape) {
|
||||
setIsEnteringReason(false);
|
||||
setDenyReason("");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Navigate with arrow keys
|
||||
if (key.upArrow) {
|
||||
setSelectedOption((prev) => (prev > 0 ? prev - 1 : options.length - 1));
|
||||
} else if (key.downArrow) {
|
||||
setSelectedOption((prev) => (prev < options.length - 1 ? prev + 1 : 0));
|
||||
} else if (key.return) {
|
||||
// Handle selection
|
||||
if (selectedOption === 0) {
|
||||
onApprove();
|
||||
} else if (selectedOption === 1) {
|
||||
onApproveAlways();
|
||||
} else if (selectedOption === 2) {
|
||||
setIsEnteringReason(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Pretty print JSON args
|
||||
let formattedArgs = approvalRequest.toolArgs;
|
||||
let parsedArgs: Record<string, unknown> | null = null;
|
||||
try {
|
||||
parsedArgs = JSON.parse(approvalRequest.toolArgs);
|
||||
formattedArgs = JSON.stringify(parsedArgs, null, 2);
|
||||
} catch {
|
||||
// Keep as-is if not valid JSON
|
||||
}
|
||||
|
||||
// Compute diff for file-editing tools
|
||||
const precomputedDiff = useMemo((): AdvancedDiffSuccess | null => {
|
||||
if (!parsedArgs) return null;
|
||||
|
||||
const toolName = approvalRequest.toolName.toLowerCase();
|
||||
if (toolName === "write") {
|
||||
const result = computeAdvancedDiff({
|
||||
kind: "write",
|
||||
filePath: parsedArgs.file_path as string,
|
||||
content: (parsedArgs.content as string) || "",
|
||||
});
|
||||
return result.mode === "advanced" ? result : null;
|
||||
} else if (toolName === "edit") {
|
||||
const result = computeAdvancedDiff({
|
||||
kind: "edit",
|
||||
filePath: parsedArgs.file_path as string,
|
||||
oldString: (parsedArgs.old_string as string) || "",
|
||||
newString: (parsedArgs.new_string as string) || "",
|
||||
replaceAll: parsedArgs.replace_all as boolean | undefined,
|
||||
});
|
||||
return result.mode === "advanced" ? result : null;
|
||||
} else if (toolName === "multiedit") {
|
||||
const result = computeAdvancedDiff({
|
||||
kind: "multi_edit",
|
||||
filePath: parsedArgs.file_path as string,
|
||||
edits:
|
||||
(parsedArgs.edits as Array<{
|
||||
old_string: string;
|
||||
new_string: string;
|
||||
replace_all?: boolean;
|
||||
}>) || [],
|
||||
});
|
||||
return result.mode === "advanced" ? result : null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [approvalRequest, parsedArgs]);
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Text bold>Tool Approval Required</Text>
|
||||
|
||||
<Box flexDirection="column">
|
||||
<Text>
|
||||
Tool: <Text bold>{approvalRequest.toolName}</Text>
|
||||
</Text>
|
||||
|
||||
{/* Show diff for file-editing tools */}
|
||||
{precomputedDiff && parsedArgs && (
|
||||
<Box paddingLeft={2} flexDirection="column">
|
||||
{approvalRequest.toolName.toLowerCase() === "write" ? (
|
||||
<AdvancedDiffRenderer
|
||||
precomputed={precomputedDiff}
|
||||
kind="write"
|
||||
filePath={parsedArgs.file_path as string}
|
||||
content={(parsedArgs.content as string) || ""}
|
||||
showHeader={false}
|
||||
/>
|
||||
) : approvalRequest.toolName.toLowerCase() === "edit" ? (
|
||||
<AdvancedDiffRenderer
|
||||
precomputed={precomputedDiff}
|
||||
kind="edit"
|
||||
filePath={parsedArgs.file_path as string}
|
||||
oldString={(parsedArgs.old_string as string) || ""}
|
||||
newString={(parsedArgs.new_string as string) || ""}
|
||||
replaceAll={parsedArgs.replace_all as boolean | undefined}
|
||||
showHeader={false}
|
||||
/>
|
||||
) : approvalRequest.toolName.toLowerCase() === "multiedit" ? (
|
||||
<AdvancedDiffRenderer
|
||||
precomputed={precomputedDiff}
|
||||
kind="multi_edit"
|
||||
filePath={parsedArgs.file_path as string}
|
||||
edits={
|
||||
(parsedArgs.edits as Array<{
|
||||
old_string: string;
|
||||
new_string: string;
|
||||
replace_all?: boolean;
|
||||
}>) || []
|
||||
}
|
||||
showHeader={false}
|
||||
/>
|
||||
) : null}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Fallback: Show raw args if no diff */}
|
||||
{!precomputedDiff && (
|
||||
<>
|
||||
<Text dimColor>Arguments:</Text>
|
||||
<Box paddingLeft={2}>
|
||||
<Text dimColor>{formattedArgs}</Text>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box flexDirection="column">
|
||||
{isEnteringReason ? (
|
||||
<Box flexDirection="column">
|
||||
<Text>Enter reason for denial (ESC to cancel):</Text>
|
||||
<Box>
|
||||
<Text dimColor>{"> "}</Text>
|
||||
{(() => {
|
||||
const TextInputAny = RawTextInput as unknown as ComponentType<{
|
||||
value: string;
|
||||
onChange: (s: string) => void;
|
||||
}>;
|
||||
return (
|
||||
<TextInputAny value={denyReason} onChange={setDenyReason} />
|
||||
);
|
||||
})()}
|
||||
</Box>
|
||||
</Box>
|
||||
) : (
|
||||
<>
|
||||
<Text dimColor>Use ↑/↓ to select, Enter to confirm:</Text>
|
||||
{options.map((option) => (
|
||||
<Text key={option}>
|
||||
{selectedOption === options.indexOf(option) ? "→ " : " "}
|
||||
{option}
|
||||
</Text>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user