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:
Scott Leibrand
2025-04-19 07:21:19 -07:00
committed by GitHub
parent 6c7fbc7b94
commit 9eeb78e54f
4 changed files with 49 additions and 6 deletions

View File

@@ -15,11 +15,18 @@ const DEFAULT_DENY_MESSAGE =
export function TerminalChatCommandReview({ export function TerminalChatCommandReview({
confirmationPrompt, confirmationPrompt,
onReviewCommand, onReviewCommand,
// callback to switch approval mode overlay
onSwitchApprovalMode,
explanation: propExplanation, explanation: propExplanation,
// whether this review Select is active (listening for keys)
isActive = true,
}: { }: {
confirmationPrompt: React.ReactNode; confirmationPrompt: React.ReactNode;
onReviewCommand: (decision: ReviewDecision, customMessage?: string) => void; onReviewCommand: (decision: ReviewDecision, customMessage?: string) => void;
onSwitchApprovalMode: () => void;
explanation?: string; explanation?: string;
// when false, disable the underlying Select so it won't capture input
isActive?: boolean;
}): React.ReactElement { }): React.ReactElement {
const [mode, setMode] = React.useState<"select" | "input" | "explanation">( const [mode, setMode] = React.useState<"select" | "input" | "explanation">(
"select", "select",
@@ -70,6 +77,7 @@ export function TerminalChatCommandReview({
const opts: Array< const opts: Array<
| { label: string; value: ReviewDecision } | { label: string; value: ReviewDecision }
| { label: string; value: "edit" } | { label: string; value: "edit" }
| { label: string; value: "switch" }
> = [ > = [
{ {
label: "Yes (y)", label: "Yes (y)",
@@ -93,6 +101,11 @@ export function TerminalChatCommandReview({
label: "Edit or give feedback (e)", label: "Edit or give feedback (e)",
value: "edit", value: "edit",
}, },
// allow switching approval mode
{
label: "Switch approval mode (s)",
value: "switch",
},
{ {
label: "No, and keep going (n)", label: "No, and keep going (n)",
value: ReviewDecision.NO_CONTINUE, value: ReviewDecision.NO_CONTINUE,
@@ -106,7 +119,8 @@ export function TerminalChatCommandReview({
return opts; return opts;
}, [showAlwaysApprove]); }, [showAlwaysApprove]);
useInput((input, key) => { useInput(
(input, key) => {
if (mode === "select") { if (mode === "select") {
if (input === "y") { if (input === "y") {
onReviewCommand(ReviewDecision.YES); onReviewCommand(ReviewDecision.YES);
@@ -121,6 +135,9 @@ export function TerminalChatCommandReview({
); );
} else if (input === "a" && showAlwaysApprove) { } else if (input === "a" && showAlwaysApprove) {
onReviewCommand(ReviewDecision.ALWAYS); onReviewCommand(ReviewDecision.ALWAYS);
} else if (input === "s") {
// switch approval mode
onSwitchApprovalMode();
} else if (key.escape) { } else if (key.escape) {
onReviewCommand(ReviewDecision.NO_EXIT); onReviewCommand(ReviewDecision.NO_EXIT);
} }
@@ -143,7 +160,8 @@ export function TerminalChatCommandReview({
); );
} }
} }
}); }, { isActive }
);
return ( return (
<Box flexDirection="column" gap={1} borderStyle="round" marginTop={1}> <Box flexDirection="column" gap={1} borderStyle="round" marginTop={1}>
@@ -191,9 +209,13 @@ export function TerminalChatCommandReview({
<Text>Allow command?</Text> <Text>Allow command?</Text>
<Box paddingX={2} flexDirection="column" gap={1}> <Box paddingX={2} flexDirection="column" gap={1}>
<Select <Select
onChange={(value: ReviewDecision | "edit") => { isDisabled={!isActive}
visibleOptionCount={approvalOptions.length}
onChange={(value: ReviewDecision | "edit" | "switch") => {
if (value === "edit") { if (value === "edit") {
setMode("input"); setMode("input");
} else if (value === "switch") {
onSwitchApprovalMode();
} else { } else {
onReviewCommand(value); onReviewCommand(value);
} }

View File

@@ -407,7 +407,11 @@ export default function TerminalChatInput({
<TerminalChatCommandReview <TerminalChatCommandReview
confirmationPrompt={confirmationPrompt} confirmationPrompt={confirmationPrompt}
onReviewCommand={submitConfirmation} onReviewCommand={submitConfirmation}
// allow switching approval mode via 'v'
onSwitchApprovalMode={openApprovalOverlay}
explanation={explanation} explanation={explanation}
// disable when input is inactive (e.g., overlay open)
isActive={active}
/> />
); );
} }

View File

@@ -346,7 +346,11 @@ export default function TerminalChatInput({
<TerminalChatCommandReview <TerminalChatCommandReview
confirmationPrompt={confirmationPrompt} confirmationPrompt={confirmationPrompt}
onReviewCommand={submitConfirmation} onReviewCommand={submitConfirmation}
// allow switching approval mode via 'v'
onSwitchApprovalMode={openApprovalOverlay}
explanation={explanation} explanation={explanation}
// disable when input is inactive (e.g., overlay open)
isActive={active}
/> />
); );
} }

View File

@@ -206,6 +206,13 @@ export default function TerminalChat({
} }
useEffect(() => { 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()) { if (isLoggingEnabled()) {
log("creating NEW AgentLoop"); log("creating NEW AgentLoop");
log( log(
@@ -293,10 +300,12 @@ export default function TerminalChat({
agentRef.current = undefined; agentRef.current = undefined;
forceUpdate(); // rerender after teardown too forceUpdate(); // rerender after teardown too
}; };
// We intentionally omit 'approvalPolicy' and 'confirmationPrompt' from the deps
// so switching modes or showing confirmation dialogs doesnt tear down the loop.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ }, [
model, model,
config, config,
approvalPolicy,
requestConfirmation, requestConfirmation,
additionalWritableRoots, additionalWritableRoots,
]); ]);
@@ -580,12 +589,16 @@ export default function TerminalChat({
<ApprovalModeOverlay <ApprovalModeOverlay
currentMode={approvalPolicy} currentMode={approvalPolicy}
onSelect={(newMode) => { onSelect={(newMode) => {
agent?.cancel(); // update approval policy without cancelling an in-progress session
setLoading(false);
if (newMode === approvalPolicy) { if (newMode === approvalPolicy) {
return; return;
} }
// update state
setApprovalPolicy(newMode as ApprovalPolicy); setApprovalPolicy(newMode as ApprovalPolicy);
// update existing AgentLoop instance
if (agentRef.current) {
(agentRef.current as unknown as { approvalPolicy: ApprovalPolicy }).approvalPolicy = newMode as ApprovalPolicy;
}
setItems((prev) => [ setItems((prev) => [
...prev, ...prev,
{ {