153 lines
4.5 KiB
TypeScript
153 lines
4.5 KiB
TypeScript
import { Box, Text, useInput } from "ink";
|
||
import { memo, useState } from "react";
|
||
import { resolvePlaceholders } from "../helpers/pasteRegistry";
|
||
import { useProgressIndicator } from "../hooks/useProgressIndicator";
|
||
import { colors } from "./colors";
|
||
import { MarkdownDisplay } from "./MarkdownDisplay";
|
||
import { PasteAwareTextInput } from "./PasteAwareTextInput";
|
||
|
||
type Props = {
|
||
plan: string;
|
||
onApprove: () => void;
|
||
onApproveAndAcceptEdits: () => void;
|
||
onKeepPlanning: (reason: string) => void;
|
||
};
|
||
|
||
const OptionsRenderer = memo(
|
||
({
|
||
options,
|
||
selectedOption,
|
||
}: {
|
||
options: Array<{ label: string }>;
|
||
selectedOption: number;
|
||
}) => {
|
||
return (
|
||
<Box flexDirection="column">
|
||
{options.map((option, index) => {
|
||
const isSelected = index === selectedOption;
|
||
const color = isSelected ? colors.approval.header : undefined;
|
||
return (
|
||
<Box key={option.label} flexDirection="row">
|
||
<Text color={color}>
|
||
{isSelected ? "❯" : " "} {index + 1}. {option.label}
|
||
</Text>
|
||
</Box>
|
||
);
|
||
})}
|
||
</Box>
|
||
);
|
||
},
|
||
);
|
||
|
||
OptionsRenderer.displayName = "OptionsRenderer";
|
||
|
||
export const PlanModeDialog = memo(
|
||
({ plan, onApprove, onApproveAndAcceptEdits, onKeepPlanning }: Props) => {
|
||
const [selectedOption, setSelectedOption] = useState(0);
|
||
const [isEnteringReason, setIsEnteringReason] = useState(false);
|
||
const [denyReason, setDenyReason] = useState("");
|
||
useProgressIndicator();
|
||
|
||
const options = [
|
||
{ label: "Yes, and auto-accept edits", action: onApproveAndAcceptEdits },
|
||
{ label: "Yes, and manually approve edits", action: onApprove },
|
||
{ label: "No, keep planning", action: () => {} }, // Handled via setIsEnteringReason
|
||
];
|
||
|
||
useInput((_input, key) => {
|
||
// CTRL-C: immediately exit plan approval (closest to cancel)
|
||
if (key.ctrl && _input === "c") {
|
||
onKeepPlanning("User pressed CTRL-C to cancel");
|
||
return;
|
||
}
|
||
|
||
if (isEnteringReason) {
|
||
// When entering reason, only handle enter/escape
|
||
if (key.return) {
|
||
// Resolve placeholders before sending reason
|
||
const resolvedReason = resolvePlaceholders(denyReason);
|
||
onKeepPlanning(resolvedReason);
|
||
setIsEnteringReason(false);
|
||
setDenyReason("");
|
||
} else if (key.escape) {
|
||
setIsEnteringReason(false);
|
||
setDenyReason("");
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (key.upArrow) {
|
||
setSelectedOption((prev) => Math.max(0, prev - 1));
|
||
} else if (key.downArrow) {
|
||
setSelectedOption((prev) => Math.min(options.length - 1, prev + 1));
|
||
} else if (key.return) {
|
||
// Check if this is the "keep planning" option (last option)
|
||
if (selectedOption === options.length - 1) {
|
||
setIsEnteringReason(true);
|
||
} else {
|
||
options[selectedOption]?.action();
|
||
}
|
||
} else if (key.escape) {
|
||
setIsEnteringReason(true); // ESC also goes to denial input
|
||
}
|
||
});
|
||
|
||
// Show denial input screen if entering reason
|
||
if (isEnteringReason) {
|
||
return (
|
||
<Box flexDirection="column">
|
||
<Box
|
||
borderStyle="round"
|
||
borderColor={colors.approval.border}
|
||
width="100%"
|
||
flexDirection="column"
|
||
paddingX={1}
|
||
>
|
||
<Text bold>
|
||
Enter feedback to continue planning (ESC to cancel):
|
||
</Text>
|
||
<Box height={1} />
|
||
<Box>
|
||
<Text dimColor>{"> "}</Text>
|
||
<PasteAwareTextInput
|
||
value={denyReason}
|
||
onChange={setDenyReason}
|
||
/>
|
||
</Box>
|
||
</Box>
|
||
<Box height={1} />
|
||
</Box>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<Box
|
||
flexDirection="column"
|
||
borderStyle="round"
|
||
borderColor={colors.approval.border}
|
||
paddingX={1}
|
||
>
|
||
<Text bold color={colors.approval.header}>
|
||
Ready to code?
|
||
</Text>
|
||
<Box height={1} />
|
||
<Text>Here's the proposed plan:</Text>
|
||
<Box height={1} />
|
||
|
||
{/* Nested box for plan content */}
|
||
<Box borderStyle="round" paddingX={1}>
|
||
<MarkdownDisplay text={plan} />
|
||
</Box>
|
||
|
||
<Box height={1} />
|
||
<Text>Would you like to proceed?</Text>
|
||
<Box height={1} />
|
||
|
||
<OptionsRenderer options={options} selectedOption={selectedOption} />
|
||
</Box>
|
||
);
|
||
},
|
||
);
|
||
|
||
PlanModeDialog.displayName = "PlanModeDialog";
|