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({
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(); // re‑render after teardown too
|
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,
|
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,
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user