diff --git a/src/cli/App.tsx b/src/cli/App.tsx index d8276cb..acb1759 100644 --- a/src/cli/App.tsx +++ b/src/cli/App.tsx @@ -156,6 +156,12 @@ NOTE: At any point in time through this workflow you should feel free to ask the `; } +// Check if plan file exists +function planFileExists(): boolean { + const planFilePath = permissionMode.getPlanFilePath(); + return !!planFilePath && existsSync(planFilePath); +} + // Read plan content from the plan file function readPlanFile(): string { const planFilePath = permissionMode.getPlanFilePath(); @@ -825,8 +831,9 @@ export default function App({ // Check permissions for all approvals (including fancy UI tools) const approvalResults = await Promise.all( approvalsToProcess.map(async (approvalItem) => { - // Check if approval is incomplete (missing name or arguments) - if (!approvalItem.toolName || !approvalItem.toolArgs) { + // Check if approval is incomplete (missing name) + // Note: toolArgs can be empty string for tools with no arguments (e.g., EnterPlanMode) + if (!approvalItem.toolName) { return { approval: approvalItem, permission: { @@ -3251,6 +3258,20 @@ ${recentCommits} [pendingApprovals, approvalResults, sendAllResults], ); + // Auto-reject ExitPlanMode if plan file doesn't exist + useEffect(() => { + const currentIndex = approvalResults.length; + const approval = pendingApprovals[currentIndex]; + if (approval?.toolName === "ExitPlanMode" && !planFileExists()) { + const planFilePath = permissionMode.getPlanFilePath(); + handlePlanKeepPlanning( + `You must write your plan to the plan file before exiting plan mode.\n` + + `Plan file path: ${planFilePath || "not set"}\n` + + `Use the Write tool to create your plan, then call ExitPlanMode again.`, + ); + } + }, [pendingApprovals, approvalResults.length, handlePlanKeepPlanning]); + const handleQuestionSubmit = useCallback( async (answers: Record) => { const currentIndex = approvalResults.length; diff --git a/src/cli/components/ToolCallMessageRich.tsx b/src/cli/components/ToolCallMessageRich.tsx index b0b4ac7..432976b 100644 --- a/src/cli/components/ToolCallMessageRich.tsx +++ b/src/cli/components/ToolCallMessageRich.tsx @@ -272,8 +272,9 @@ export const ToolCallMessage = memo(({ line }: { line: ToolCallLine }) => { // Format tool denial errors more user-friendly if (isError && displayText.includes("request to call tool denied")) { - const match = displayText.match(/User reason: (.+)$/); - const reason = match ? match[1] : "(empty)"; + // Use [\s\S]+ to match multiline reasons + const match = displayText.match(/User reason: ([\s\S]+)$/); + const reason = match?.[1]?.trim() || "(empty)"; displayText = `User rejected the tool call with reason: ${reason}`; } diff --git a/src/permissions/mode.ts b/src/permissions/mode.ts index b37e4ff..0160c00 100644 --- a/src/permissions/mode.ts +++ b/src/permissions/mode.ts @@ -124,6 +124,11 @@ class PermissionModeManager { "Grep", "NotebookRead", "TodoWrite", + // Plan mode tools (must allow exit!) + "ExitPlanMode", + "exit_plan_mode", + "AskUserQuestion", + "ask_user_question", // Codex toolset (snake_case) "read_file", "list_dir", diff --git a/src/tools/impl/ExitPlanMode.ts b/src/tools/impl/ExitPlanMode.ts index 5e73259..c6bcaf1 100644 --- a/src/tools/impl/ExitPlanMode.ts +++ b/src/tools/impl/ExitPlanMode.ts @@ -6,6 +6,7 @@ export async function exit_plan_mode(): Promise<{ message: string }> { // Return confirmation message that plan was approved // Note: The plan is read from the plan file by the UI before this return is shown + // 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",