fix: plan mode flexibility (#517)

Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
Charles Packer
2026-01-11 17:35:51 -08:00
committed by GitHub
parent 2e26bfdc1b
commit 88fa10f0d3
3 changed files with 37 additions and 7 deletions

View File

@@ -1,6 +1,8 @@
// src/cli/App.tsx
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { APIError, APIUserAbortError } from "@letta-ai/letta-client/core/error";
import type {
AgentState,
@@ -6396,10 +6398,11 @@ DO NOT respond to these messages or otherwise consider them in your response unl
const approval = pendingApprovals[currentIndex];
if (approval?.toolName === "ExitPlanMode" && !planFileExists()) {
const planFilePath = permissionMode.getPlanFilePath();
const plansDir = join(homedir(), ".letta", "plans");
handlePlanKeepPlanning(
`You must write your plan to the plan file before exiting plan mode.\n` +
`Plan file path: ${planFilePath || "not set"}\n` +
`Use a write tool (e.g. Write, ApplyPatch, etc.) to create your plan, then call ExitPlanMode again.`,
`You must write your plan to a plan file before exiting plan mode.\n` +
(planFilePath ? `Plan file path: ${planFilePath}\n` : "") +
`Use a write tool to create your plan in ${plansDir}, then use ExitPlanMode to present the plan to the user.`,
);
}
}, [pendingApprovals, approvalResults.length, handlePlanKeepPlanning]);
@@ -6792,6 +6795,18 @@ Plan file path: ${planFilePath}`;
return null;
}
// Skip tool calls that were eagerly committed to staticItems
// (e.g., ExitPlanMode preview) - but only AFTER approval is complete
// We still need to render the approval options while awaiting approval
if (
ln.kind === "tool_call" &&
ln.toolCallId &&
eagerCommittedPreviewsRef.current.has(ln.toolCallId) &&
ln.toolCallId !== currentApproval?.toolCallId
) {
return null;
}
// Check if this tool call matches the current ExitPlanMode approval
const isExitPlanModeApproval =
ln.kind === "tool_call" &&

View File

@@ -1,6 +1,9 @@
// src/permissions/mode.ts
// Permission mode management (default, acceptEdits, plan, bypassPermissions)
import { homedir } from "node:os";
import { join } from "node:path";
import { isReadOnlyShellCommand } from "./readOnlyShell";
export type PermissionMode =
@@ -171,9 +174,12 @@ class PermissionModeManager {
return "allow";
}
// Special case: allow writes to the plan file only
// Special case: allow writes to any plan file in ~/.letta/plans/
// NOTE: We allow writing to ANY plan file, not just the assigned one.
// This is intentional - it allows the agent to "resume" planning after
// plan mode was exited/reset by simply writing to any plan file.
if (writeTools.includes(toolName)) {
const planFilePath = this.getPlanFilePath();
const plansDir = join(homedir(), ".letta", "plans");
let targetPath =
(toolArgs?.file_path as string) || (toolArgs?.path as string);
@@ -192,7 +198,12 @@ class PermissionModeManager {
}
}
if (planFilePath && targetPath && targetPath === planFilePath) {
// Allow if target is any .md file in the plans directory
if (
targetPath &&
targetPath.startsWith(plansDir) &&
targetPath.endsWith(".md")
) {
return "allow";
}
}

View File

@@ -9,6 +9,10 @@ export async function exit_plan_mode(): Promise<{ message: string }> {
// The UI layer checks if the plan file exists and auto-rejects if not
return {
message:
"User has approved your plan. You can now start coding.\nStart with updating your todo list if applicable",
"User has approved your plan. You can now start coding.\n" +
"Start with updating your todo list if applicable.\n\n" +
"Tip: If this plan will be referenced in the future by your future-self, " +
"other agents, or humans, consider renaming the plan file to something easily " +
"identifiable with a timestamp (e.g., `2026-01-auth-refactor.md`) rather than the random name.",
};
}