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({
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);
}

View File

@@ -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}
/>
);
}

View File

@@ -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}
/>
);
}

View File

@@ -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(); // 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,
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,
{