diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index c5e9c15..fa58706 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -22,6 +22,7 @@ All notable changes to MandoCode will be documented in this file. - **Startup recovery for unavailable cloud models.** A missing or retired configured cloud model opens the picker automatically. One compact warning explains what to do; a valid replacement clears it. ### Changed +- **Plan mode is explicit in the CLI.** Only /plan starts structured plan generation and approval. Ordinary prompts run directly without automatic complexity detection or model-initiated plan proposals. Existing plan review and resume flows remain available; Desktop behavior is unchanged. - **Organize service files.** Group command output, project directory state, session recovery, token tracking, and update/version services by responsibility. Remove obsolete tip-progress counters and configuration writes when switching agents. - **Prioritize everyday startup commands.** Show planning, new agents with the Alt+N shortcut, and the keyboard reference alongside help, model selection, configuration, clear, music, and exit. Connection recovery commands remain in failure guidance; learning and compaction remain available in help and autocomplete. - **Keep the project directory visible.** Each agent shows a folder icon and its project directory in purple beneath the input area, outside the scrolling conversation, including during menus and approvals. A dedicated single row keeps the directory visible in split panes, showing the full path when it fits and otherwise shortening the middle while preserving the drive and trailing folders. Click the folder icon or path to open the full project directory in the system file browser without changing agent focus. Startup no longer repeats the directory. diff --git a/docs/cli-agent-panes.md b/docs/cli-agent-panes.md index a7d359d..92669b6 100644 --- a/docs/cli-agent-panes.md +++ b/docs/cli-agent-panes.md @@ -99,3 +99,8 @@ existing agents when the grid expands or a closed slot is reused. Incremental-re checks exercise closing Agent 1 with two, three, or four agents, including App initialization and disposal. Live terminal testing is still needed for focus, mouse controls, terminal shortcuts, resizing, and concurrent tool approvals. + + +## Explicit plan mode + +Use /plan to generate a structured plan for review. Regular prompts run directly, even when they include multiple steps or ask for an outline in prose. The model cannot activate plan mode during an ordinary CLI turn. Existing plan editing, approval, execution, and /plan-resume remain available. diff --git a/src/MandoCode/Components/App.razor b/src/MandoCode/Components/App.razor index b204965..6be393a 100644 --- a/src/MandoCode/Components/App.razor +++ b/src/MandoCode/Components/App.razor @@ -158,7 +158,6 @@ private bool _gitChangesOpen; private TaskCompletionSource? _directorySelection; private MandoCodeConfig Config { get; set; } = default!; - private TaskPlannerService TaskPlanner { get; set; } = default!; private PlanRunnerSelector PlanRunners { get; set; } = default!; private PlanHandoff PlanHandoff { get; set; } = default!; private FileAutocompleteProvider FileProvider { get; set; } = default!; @@ -368,8 +367,8 @@ { var agentServices = Pane?.Services ?? Services; AI = agentServices.GetRequiredService(); + AI.UseExplicitPlanningOnly(); Config = agentServices.GetRequiredService(); - TaskPlanner = agentServices.GetRequiredService(); PlanRunners = agentServices.GetRequiredService(); PlanHandoff = agentServices.GetRequiredService(); FileProvider = agentServices.GetRequiredService(); @@ -1554,23 +1553,13 @@ PlayMatrixMoment(input); } - // Check if this request needs task planning BEFORE injecting file content, - // so that attached file size doesn't inflate the complexity heuristics - var planning = TaskPlanner.GetPlanningDecision(input); - // Process @file references and inject file content var processedInput = ProcessFileReferences(input); // Add user message to chat history _messages.Add(new ChatMsg { Role = "user", Text = input }); - if (planning.Required) - { - AnsiConsole.MarkupLine($"[dim]Planning automatically: {Spectre.Console.Markup.Escape(planning.Reason ?? "multi-step request")}.[/]"); - await ForcePlanAsync(processedInput, input); - } - else - await ProcessDirectRequestAsync(processedInput); + await ProcessDirectRequestAsync(processedInput); } } diff --git a/src/MandoCode/Services/Ai/AIService.cs b/src/MandoCode/Services/Ai/AIService.cs index 6f18aa3..fa6ff11 100644 --- a/src/MandoCode/Services/Ai/AIService.cs +++ b/src/MandoCode/Services/Ai/AIService.cs @@ -332,6 +332,15 @@ private void RebuildSystemPrompt() { var skillIndex = SystemPrompts.BuildSkillIndex(_skillLoader.GetAll()); _systemPrompt = SystemPrompts.BuildMandoCodeAssistant(_config.EnableWebSearch, _config.AgentName) + "\n\n" + ShellEnvironment.SystemPromptRules; + if (_explicitPlanningOnly) + { + var start = _systemPrompt.IndexOf("MULTI-STEP PLANNING:", StringComparison.Ordinal); + var end = _systemPrompt.IndexOf("FILE PATH RULES", start, StringComparison.Ordinal); + _systemPrompt = _systemPrompt.Remove(start, end - start).Insert(start, + "PLAN MODE:\nOnly the user's /plan command activates structured plan mode. For normal requests, carry out the work directly using available tools. You may explain an approach in prose, but do not call propose_plan or wait for plan approval.\n\n"); + _systemPrompt = _systemPrompt.Replace("If the work spans several files or systems, use propose_plan (see MULTI-STEP PLANNING below) rather than narrating a plan in prose — a proposed plan is reviewable and gets executed for you; a described one is neither.", + "Carry out multi-step requests directly; structured plan mode is entered only through /plan.", StringComparison.Ordinal); + } _systemPrompt += "\n\n" + VisionSupport.AgentInstruction(); if (!string.IsNullOrEmpty(skillIndex)) { @@ -456,6 +465,16 @@ public async Task AttachMcpPluginsAsync(CancellationToken cancellationToken = de /// Replaces the optional tools supplied by the host application and rebuilds the agent so /// they participate in normal MAF function calling and fallback execution. /// + private bool _explicitPlanningOnly; + + /// CLI hosts reserve structured planning for /plan; explicit generation remains available. + public void UseExplicitPlanningOnly() + { + _explicitPlanningOnly = true; + RebuildSystemPrompt(); + BuildAgent(); + } + public void SetHostTools(IEnumerable? tools) { _hostTools = tools?.ToArray() ?? Array.Empty(); @@ -525,7 +544,7 @@ private void BuildAgent() tools.Add(NamedTool(webSearchPlugin.FetchWebpage, "fetch_webpage")); } - if (_config.EnableTaskPlanning) + if (_config.EnableTaskPlanning && !_explicitPlanningOnly) { var planningPlugin = new PlanningPlugin(); tools.Add(NamedTool(planningPlugin.ProposePlan, "propose_plan")); diff --git a/tests/MandoCode.Tests/ExplicitPlanningTests.cs b/tests/MandoCode.Tests/ExplicitPlanningTests.cs new file mode 100644 index 0000000..3a2dd05 --- /dev/null +++ b/tests/MandoCode.Tests/ExplicitPlanningTests.cs @@ -0,0 +1,42 @@ +using System.Reflection; +using MandoCode.Models; +using MandoCode.Services; +using Microsoft.Extensions.AI; +using Xunit; + +namespace MandoCode.Tests; + +public class ExplicitPlanningTests +{ + private static AIService Create() + { + var config = new MandoCodeConfig { EnableTaskPlanning = true, AllowPersistence = false }; + var root = new ProjectRootAccessor(Path.GetTempPath()); + return new AIService(root, config, new TokenTrackingService(), new PlanHandoff(), + new SkillLoader(config, root), new McpClientManager(config), new McpApprovalGate(config), new SpinnerService()); + } + + private static IEnumerable Tools(AIService ai) => + (IEnumerable)typeof(AIService).GetField("_agentFunctions", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(ai)!; + + [Fact] + public void DesktopHostsRetainModelInitiatedPlanning() + { + var ai = Create(); + Assert.Contains(Tools(ai), tool => tool.Name == "propose_plan"); + } + + [Fact] + public void CliNormalToolsAndPromptExcludeAutomaticPlanningAcrossRebuilds() + { + var ai = Create(); + ai.UseExplicitPlanningOnly(); + ai.SetHostTools([]); // Model/config/tool rebuilds must not re-enable planning. + Assert.DoesNotContain(Tools(ai), tool => tool.Name == "propose_plan"); + Assert.Contains(Tools(ai), tool => tool.Name == "read_file_contents"); + var prompt = (string)typeof(AIService).GetField("_systemPrompt", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(ai)!; + Assert.Contains("Only the user's /plan command", prompt); + Assert.DoesNotContain("call the propose_plan function BEFORE", prompt); + Assert.DoesNotContain("use propose_plan (see MULTI-STEP", prompt); + } +}