diff --git a/src/cli/App.tsx b/src/cli/App.tsx index 2582153..331a9c9 100644 --- a/src/cli/App.tsx +++ b/src/cli/App.tsx @@ -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) => { @@ -7024,6 +7062,7 @@ Plan file path: ${planFilePath}`; handlePlanApprove(true) } onKeepPlanning={handlePlanKeepPlanning} + onCancel={handleCancelApprovals} isFocused={true} /> ) : isFileEditApproval && fileEditInfo ? ( diff --git a/src/cli/components/StaticPlanApproval.tsx b/src/cli/components/StaticPlanApproval.tsx index 2e05791..5806116 100644 --- a/src/cli/components/StaticPlanApproval.tsx +++ b/src/cli/components/StaticPlanApproval.tsx @@ -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; }