diff --git a/src/cli/App.tsx b/src/cli/App.tsx index 9a8f5c9..91d6498 100644 --- a/src/cli/App.tsx +++ b/src/cli/App.tsx @@ -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" && diff --git a/src/permissions/mode.ts b/src/permissions/mode.ts index b78303d..c27830f 100644 --- a/src/permissions/mode.ts +++ b/src/permissions/mode.ts @@ -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"; } } diff --git a/src/tools/impl/ExitPlanMode.ts b/src/tools/impl/ExitPlanMode.ts index c6bcaf1..9d7aea4 100644 --- a/src/tools/impl/ExitPlanMode.ts +++ b/src/tools/impl/ExitPlanMode.ts @@ -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.", }; }