fix: properly handle ctrl-c during plan mode, and restoring on pending plan approval (#529)

Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
Charles Packer
2026-01-12 22:28:51 -08:00
committed by GitHub
parent a0f604b5f0
commit 3ba63748c3
2 changed files with 48 additions and 7 deletions

View File

@@ -6423,12 +6423,45 @@ DO NOT respond to these messages or otherwise consider them in your response unl
if (approval?.toolName === "ExitPlanMode") {
// First check if plan mode is enabled
if (permissionMode.getMode() !== "plan") {
handlePlanKeepPlanning(
`Plan mode is not currently enabled. Use EnterPlanMode to enter plan mode first, then write your plan and use ExitPlanMode to present it.`,
);
// Plan mode state was lost (e.g., CLI restart) - queue rejection with helpful message
// This is different from immediate rejection because we want the user to see what happened
// and be able to type their next message
// Add status message to explain what happened
const statusId = uid("status");
buffersRef.current.byId.set(statusId, {
kind: "status",
id: statusId,
lines: ["⚠️ Plan mode session expired (use /plan to re-enter)"],
});
buffersRef.current.order.push(statusId);
// Queue denial to send with next message (same pattern as handleCancelApprovals)
const denialResults = [
{
type: "approval" as const,
tool_call_id: approval.toolCallId,
approve: false,
reason:
"Plan mode session expired (CLI restarted). Use EnterPlanMode to re-enter plan mode, or request the user to re-enter plan mode.",
},
];
setQueuedApprovalResults(denialResults);
// Mark tool as cancelled in buffers
markIncompleteToolsAsCancelled(buffersRef.current);
refreshDerived();
// Clear all approval state (same as handleCancelApprovals)
setPendingApprovals([]);
setApprovalContexts([]);
setApprovalResults([]);
setAutoHandledResults([]);
setAutoDeniedApprovals([]);
return;
}
// Then check if plan file exists
// Then check if plan file exists (keep existing behavior - immediate rejection)
// This case means plan mode IS active, but agent forgot to write the plan file
if (!planFileExists()) {
const planFilePath = permissionMode.getPlanFilePath();
const plansDir = join(homedir(), ".letta", "plans");
@@ -6439,7 +6472,12 @@ DO NOT respond to these messages or otherwise consider them in your response unl
);
}
}
}, [pendingApprovals, approvalResults.length, handlePlanKeepPlanning]);
}, [
pendingApprovals,
approvalResults.length,
handlePlanKeepPlanning,
refreshDerived,
]);
const handleQuestionSubmit = useCallback(
async (answers: Record<string, string>) => {
@@ -7024,6 +7062,7 @@ Plan file path: ${planFilePath}`;
handlePlanApprove(true)
}
onKeepPlanning={handlePlanKeepPlanning}
onCancel={handleCancelApprovals}
isFocused={true}
/>
) : isFileEditApproval && fileEditInfo ? (

View File

@@ -9,6 +9,7 @@ type Props = {
onApprove: () => void;
onApproveAndAcceptEdits: () => void;
onKeepPlanning: (reason: string) => void;
onCancel: () => void; // For CTRL-C to queue denial (like other approval screens)
isFocused?: boolean;
};
@@ -28,6 +29,7 @@ export const StaticPlanApproval = memo(
onApprove,
onApproveAndAcceptEdits,
onKeepPlanning,
onCancel,
isFocused = true,
}: Props) => {
const [selectedOption, setSelectedOption] = useState(0);
@@ -50,9 +52,9 @@ export const StaticPlanApproval = memo(
(input, key) => {
if (!isFocused) return;
// CTRL-C: keep planning with cancel message
// CTRL-C: cancel and queue denial (like other approval screens)
if (key.ctrl && input === "c") {
onKeepPlanning("User pressed CTRL-C to cancel");
onCancel();
return;
}