fix(tui): gracefully continue ExitPlanMode after mode cycling (#1308)

Co-authored-by: Letta Code <noreply@letta.com>
This commit is contained in:
jnjpng
2026-03-10 12:17:49 -06:00
committed by GitHub
parent d9eae7b0e3
commit 4fda294a99

View File

@@ -746,14 +746,14 @@ ${SYSTEM_REMINDER_CLOSE}
} }
// Check if plan file exists // Check if plan file exists
function planFileExists(): boolean { function planFileExists(fallbackPlanFilePath?: string | null): boolean {
const planFilePath = permissionMode.getPlanFilePath(); const planFilePath = permissionMode.getPlanFilePath() ?? fallbackPlanFilePath;
return !!planFilePath && existsSync(planFilePath); return !!planFilePath && existsSync(planFilePath);
} }
// Read plan content from the plan file // Read plan content from the plan file
function _readPlanFile(): string { function _readPlanFile(fallbackPlanFilePath?: string | null): string {
const planFilePath = permissionMode.getPlanFilePath(); const planFilePath = permissionMode.getPlanFilePath() ?? fallbackPlanFilePath;
if (!planFilePath) { if (!planFilePath) {
return "No plan file path set."; return "No plan file path set.";
} }
@@ -12467,18 +12467,37 @@ ${SYSTEM_REMINDER_CLOSE}
[pendingApprovals, approvalResults, sendAllResults], [pendingApprovals, approvalResults, sendAllResults],
); );
// Auto-reject ExitPlanMode if plan mode is not enabled or plan file doesn't exist // Guard ExitPlanMode:
// - If not in plan mode, allow graceful continuation when we still have a known plan file path
// - Otherwise reject with an expiry message
// - If in plan mode but no plan file exists, keep planning
useEffect(() => { useEffect(() => {
const currentIndex = approvalResults.length; const currentIndex = approvalResults.length;
const approval = pendingApprovals[currentIndex]; const approval = pendingApprovals[currentIndex];
if (approval?.toolName === "ExitPlanMode") { if (approval?.toolName === "ExitPlanMode") {
// First check if plan mode is enabled const mode = permissionMode.getMode();
if (permissionMode.getMode() !== "plan") { const activePlanPath = permissionMode.getPlanFilePath();
// Plan mode state was lost (e.g., CLI restart) - queue rejection with helpful message const fallbackPlanPath = lastPlanFilePathRef.current;
// This is different from immediate rejection because we want the user to see what happened const hasUsablePlan = planFileExists(fallbackPlanPath);
// and be able to type their next message
// Add status message to explain what happened if (mode !== "plan") {
if (hasUsablePlan) {
// User likely cycled out of plan mode (e.g., Shift+Tab to acceptEdits/yolo)
// Keep approval flow alive and let ExitPlanMode proceed using fallback plan path.
const statusId = uid("status");
buffersRef.current.byId.set(statusId, {
kind: "status",
id: statusId,
lines: [
" Plan mode switched, continuing ExitPlanMode with saved plan file",
],
});
buffersRef.current.order.push(statusId);
refreshDerived();
return;
}
// Plan mode state was lost and no plan file is recoverable (e.g., CLI restart)
const statusId = uid("status"); const statusId = uid("status");
buffersRef.current.byId.set(statusId, { buffersRef.current.byId.set(statusId, {
kind: "status", kind: "status",
@@ -12494,7 +12513,7 @@ ${SYSTEM_REMINDER_CLOSE}
tool_call_id: approval.toolCallId, tool_call_id: approval.toolCallId,
approve: false, approve: false,
reason: reason:
"Plan mode session expired (CLI restarted). Use EnterPlanMode to re-enter plan mode, or request the user to re-enter plan mode.", "Plan mode session expired (CLI restarted or no recoverable plan file). Use EnterPlanMode to re-enter plan mode, or request the user to re-enter plan mode.",
}, },
]; ];
queueApprovalResults(denialResults); queueApprovalResults(denialResults);
@@ -12515,10 +12534,10 @@ ${SYSTEM_REMINDER_CLOSE}
setAutoDeniedApprovals([]); setAutoDeniedApprovals([]);
return; return;
} }
// 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 // Mode is plan: require an existing plan file (active or fallback)
if (!planFileExists()) { if (!hasUsablePlan) {
const planFilePath = permissionMode.getPlanFilePath(); const planFilePath = activePlanPath ?? fallbackPlanPath;
const plansDir = join(homedir(), ".letta", "plans"); const plansDir = join(homedir(), ".letta", "plans");
handlePlanKeepPlanning( handlePlanKeepPlanning(
`You must write your plan to a plan file before exiting plan mode.\n` + `You must write your plan to a plan file before exiting plan mode.\n` +
@@ -13116,12 +13135,13 @@ If using apply_patch, use this exact relative patch path: ${applyPatchRelativePa
showPreview={showApprovalPreview} showPreview={showApprovalPreview}
planContent={ planContent={
currentApproval.toolName === "ExitPlanMode" currentApproval.toolName === "ExitPlanMode"
? _readPlanFile() ? _readPlanFile(lastPlanFilePathRef.current)
: undefined : undefined
} }
planFilePath={ planFilePath={
currentApproval.toolName === "ExitPlanMode" currentApproval.toolName === "ExitPlanMode"
? (permissionMode.getPlanFilePath() ?? ? (permissionMode.getPlanFilePath() ??
lastPlanFilePathRef.current ??
undefined) undefined)
: undefined : undefined
} }
@@ -13212,12 +13232,14 @@ If using apply_patch, use this exact relative patch path: ${applyPatchRelativePa
showPreview={showApprovalPreview} showPreview={showApprovalPreview}
planContent={ planContent={
currentApproval.toolName === "ExitPlanMode" currentApproval.toolName === "ExitPlanMode"
? _readPlanFile() ? _readPlanFile(lastPlanFilePathRef.current)
: undefined : undefined
} }
planFilePath={ planFilePath={
currentApproval.toolName === "ExitPlanMode" currentApproval.toolName === "ExitPlanMode"
? (permissionMode.getPlanFilePath() ?? undefined) ? (permissionMode.getPlanFilePath() ??
lastPlanFilePathRef.current ??
undefined)
: undefined : undefined
} }
agentName={agentName ?? undefined} agentName={agentName ?? undefined}