feat: allow switching approval modes when prompted to approve an edit/command (#400)
Implements https://github.com/openai/codex/issues/392 When the user is in suggest or auto-edit mode and gets an approval request, they now have an option in the `Shell Command` dialog to: `Switch approval mode (v)` That option brings up the standard `Switch approval mode` dialog, allowing the user to switch into the desired mode, then drops them back to the `Shell Command` dialog's `Allow command?` prompt, allowing them to approve the current command and let the agent continue doing the rest of what it was doing without interruption. ``` ╭──────────────────────────────────────────────────────── │Shell Command │ │$ apply_patch << 'PATCH' │*** Begin Patch │*** Update File: foo.txt │@@ -1 +1 @@ │-foo │+bar │*** End Patch │PATCH │ │ │Allow command? │ │ Yes (y) │ Explain this command (x) │ Edit or give feedback (e) │ Switch approval mode (v) │ No, and keep going (n) │ No, and stop for now (esc) ╰────────────────────────────────────────────────────────╭──────────────────────────────────────────────────────── │ Switch approval mode │ Current mode: suggest │ │ │ │ ❯ suggest │ auto-edit │ full-auto │ type to search · enter to confirm · esc to cancel ╰──────────────────────────────────────────────────────── ```
This commit is contained in:
@@ -15,11 +15,18 @@ const DEFAULT_DENY_MESSAGE =
|
||||
export function TerminalChatCommandReview({
|
||||
confirmationPrompt,
|
||||
onReviewCommand,
|
||||
// callback to switch approval mode overlay
|
||||
onSwitchApprovalMode,
|
||||
explanation: propExplanation,
|
||||
// whether this review Select is active (listening for keys)
|
||||
isActive = true,
|
||||
}: {
|
||||
confirmationPrompt: React.ReactNode;
|
||||
onReviewCommand: (decision: ReviewDecision, customMessage?: string) => void;
|
||||
onSwitchApprovalMode: () => void;
|
||||
explanation?: string;
|
||||
// when false, disable the underlying Select so it won't capture input
|
||||
isActive?: boolean;
|
||||
}): React.ReactElement {
|
||||
const [mode, setMode] = React.useState<"select" | "input" | "explanation">(
|
||||
"select",
|
||||
@@ -70,6 +77,7 @@ export function TerminalChatCommandReview({
|
||||
const opts: Array<
|
||||
| { label: string; value: ReviewDecision }
|
||||
| { label: string; value: "edit" }
|
||||
| { label: string; value: "switch" }
|
||||
> = [
|
||||
{
|
||||
label: "Yes (y)",
|
||||
@@ -93,6 +101,11 @@ export function TerminalChatCommandReview({
|
||||
label: "Edit or give feedback (e)",
|
||||
value: "edit",
|
||||
},
|
||||
// allow switching approval mode
|
||||
{
|
||||
label: "Switch approval mode (s)",
|
||||
value: "switch",
|
||||
},
|
||||
{
|
||||
label: "No, and keep going (n)",
|
||||
value: ReviewDecision.NO_CONTINUE,
|
||||
@@ -106,7 +119,8 @@ export function TerminalChatCommandReview({
|
||||
return opts;
|
||||
}, [showAlwaysApprove]);
|
||||
|
||||
useInput((input, key) => {
|
||||
useInput(
|
||||
(input, key) => {
|
||||
if (mode === "select") {
|
||||
if (input === "y") {
|
||||
onReviewCommand(ReviewDecision.YES);
|
||||
@@ -121,6 +135,9 @@ export function TerminalChatCommandReview({
|
||||
);
|
||||
} else if (input === "a" && showAlwaysApprove) {
|
||||
onReviewCommand(ReviewDecision.ALWAYS);
|
||||
} else if (input === "s") {
|
||||
// switch approval mode
|
||||
onSwitchApprovalMode();
|
||||
} else if (key.escape) {
|
||||
onReviewCommand(ReviewDecision.NO_EXIT);
|
||||
}
|
||||
@@ -143,7 +160,8 @@ export function TerminalChatCommandReview({
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}, { isActive }
|
||||
);
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" gap={1} borderStyle="round" marginTop={1}>
|
||||
@@ -191,9 +209,13 @@ export function TerminalChatCommandReview({
|
||||
<Text>Allow command?</Text>
|
||||
<Box paddingX={2} flexDirection="column" gap={1}>
|
||||
<Select
|
||||
onChange={(value: ReviewDecision | "edit") => {
|
||||
isDisabled={!isActive}
|
||||
visibleOptionCount={approvalOptions.length}
|
||||
onChange={(value: ReviewDecision | "edit" | "switch") => {
|
||||
if (value === "edit") {
|
||||
setMode("input");
|
||||
} else if (value === "switch") {
|
||||
onSwitchApprovalMode();
|
||||
} else {
|
||||
onReviewCommand(value);
|
||||
}
|
||||
|
||||
@@ -407,7 +407,11 @@ export default function TerminalChatInput({
|
||||
<TerminalChatCommandReview
|
||||
confirmationPrompt={confirmationPrompt}
|
||||
onReviewCommand={submitConfirmation}
|
||||
// allow switching approval mode via 'v'
|
||||
onSwitchApprovalMode={openApprovalOverlay}
|
||||
explanation={explanation}
|
||||
// disable when input is inactive (e.g., overlay open)
|
||||
isActive={active}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -346,7 +346,11 @@ export default function TerminalChatInput({
|
||||
<TerminalChatCommandReview
|
||||
confirmationPrompt={confirmationPrompt}
|
||||
onReviewCommand={submitConfirmation}
|
||||
// allow switching approval mode via 'v'
|
||||
onSwitchApprovalMode={openApprovalOverlay}
|
||||
explanation={explanation}
|
||||
// disable when input is inactive (e.g., overlay open)
|
||||
isActive={active}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -206,6 +206,13 @@ export default function TerminalChat({
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// Skip recreating the agent if awaiting a decision on a pending confirmation
|
||||
if (confirmationPrompt != null) {
|
||||
if (isLoggingEnabled()) {
|
||||
log("skip AgentLoop recreation due to pending confirmationPrompt");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (isLoggingEnabled()) {
|
||||
log("creating NEW AgentLoop");
|
||||
log(
|
||||
@@ -293,10 +300,12 @@ export default function TerminalChat({
|
||||
agentRef.current = undefined;
|
||||
forceUpdate(); // re‑render after teardown too
|
||||
};
|
||||
// We intentionally omit 'approvalPolicy' and 'confirmationPrompt' from the deps
|
||||
// so switching modes or showing confirmation dialogs doesn’t tear down the loop.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
model,
|
||||
config,
|
||||
approvalPolicy,
|
||||
requestConfirmation,
|
||||
additionalWritableRoots,
|
||||
]);
|
||||
@@ -580,12 +589,16 @@ export default function TerminalChat({
|
||||
<ApprovalModeOverlay
|
||||
currentMode={approvalPolicy}
|
||||
onSelect={(newMode) => {
|
||||
agent?.cancel();
|
||||
setLoading(false);
|
||||
// update approval policy without cancelling an in-progress session
|
||||
if (newMode === approvalPolicy) {
|
||||
return;
|
||||
}
|
||||
// update state
|
||||
setApprovalPolicy(newMode as ApprovalPolicy);
|
||||
// update existing AgentLoop instance
|
||||
if (agentRef.current) {
|
||||
(agentRef.current as unknown as { approvalPolicy: ApprovalPolicy }).approvalPolicy = newMode as ApprovalPolicy;
|
||||
}
|
||||
setItems((prev) => [
|
||||
...prev,
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user