diff --git a/CHANGELOG.md b/CHANGELOG.md index bba78db..1fb7150 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -203,6 +203,12 @@ for every approved plan. Desktop's version follows the engine generation, so it user's decision instead of showing retry choices or reporting an unexpected failure afterward. - **Approval and recovery cards stay out of persisted transcript history.** They are live controls, not conversation messages, so stale actions are not replayed into a restored session. +- **Changing an agent's project folder no longer erases the conversation.** Pointing an agent at a + different folder rebuilt its session from scratch and cleared everything said up to that moment, + so a folder change part-way through a task lost all context. The conversation is now kept. The + agent still repoints its tools, system prompt, and project skills at the new folder, and is told + the folder moved so it re-reads files rather than reusing paths from before the change. Only + folder changes from here on benefit — conversations already cleared cannot be recovered. ### Test coverage 330 Desktop tests pass. New host-level coverage exercises deferred plan execution, instruction diff --git a/src/MandoCode.Desktop/Controls/ChatTabView.Explorer.cs b/src/MandoCode.Desktop/Controls/ChatTabView.Explorer.cs index 9255e8a..9e04073 100644 --- a/src/MandoCode.Desktop/Controls/ChatTabView.Explorer.cs +++ b/src/MandoCode.Desktop/Controls/ChatTabView.Explorer.cs @@ -1,4 +1,4 @@ -using System.Collections.ObjectModel; +using System.Collections.ObjectModel; using System.Text.Json; using MandoCode.Models; using MandoCode.Desktop.Services; @@ -269,7 +269,7 @@ private async void OpenFolderButton_Click(object sender, RoutedEventArgs e) if (folder == null) return; _transcript.Append(_html.Info($"Project root changed to: {folder.Path}")); - _transcript.Append(_html.Dim("Rebuilding the AI session for the new project…")); + _transcript.Append(_html.Dim(TranscriptHtmlBuilder.ProjectSwitchNotice)); // Retargets THIS tab only — its own ProjectRootAccessor, file cache, and kernel. // Other agents keep working in their own folders. diff --git a/src/MandoCode.Desktop/Services/AgentSession.cs b/src/MandoCode.Desktop/Services/AgentSession.cs index 6da1e8c..07e8e48 100644 --- a/src/MandoCode.Desktop/Services/AgentSession.cs +++ b/src/MandoCode.Desktop/Services/AgentSession.cs @@ -1,4 +1,4 @@ -using MandoCode.Desktop.ViewModels; +using MandoCode.Desktop.ViewModels; using MandoCode.Models; using MandoCode.Services; using Microsoft.Extensions.DependencyInjection; @@ -284,14 +284,35 @@ public void ResetConfigToDefaults(ConfigCoordinator configs) _persistedConfigJson = AgentConfigStore.Fingerprint(Config); } - /// Repoints this tab at a different project folder and rebuilds its AI session. + /// + /// Repoints this tab at a different project folder, KEEPING the conversation. Changing folders + /// is a navigation step inside one piece of work — "now look at this repo" — not the start of a + /// new one, so everything said up to here still applies. + /// public async Task ChangeProjectRootAsync(string folder) { // The tab keeps its name ("Agent N" or a user rename) across a folder change — the folder // is shown in the header, so the label doesn't need to track it. ProjectRoot.ProjectRoot = folder; FileProvider.RefreshCache(); - await Ai.ReinitializeAsync(Config); + + // The new folder brings its own project skills, and the skill index is baked into the + // system prompt — so the rescan has to happen BEFORE the prompt is recomposed below. + Skills.Reload(); + + // RefreshSettingsAsync, not ReinitializeAsync: both rebuild the system prompt, the agent, + // and the MCP tool set, but ReinitializeAsync ends in ClearHistoryAsync — which is what + // used to wipe the conversation on every folder change. The tools themselves need no + // rebuild to follow the move: they hold the live ProjectRootAccessor mutated above, not a + // copied path. Same trade as ChatController.RefreshFromConfigAsync. + await Ai.RefreshSettingsAsync(Config); + + // The model has just been handed a new working folder while still holding a conversation + // about the old one. Without this it keeps resolving remembered paths against a root that + // moved out from under it — the history survives, but silently goes stale. + Ai.AppendUserNote( + $"[Project root changed to: {folder}. Earlier messages refer to the previous folder — " + + "re-read any file you need rather than reusing paths or contents from before this point.]"); } private static string FolderLabel(string path) diff --git a/src/MandoCode.Desktop/Services/TranscriptHtmlBuilder.cs b/src/MandoCode.Desktop/Services/TranscriptHtmlBuilder.cs index 4d82e8f..c01077c 100644 --- a/src/MandoCode.Desktop/Services/TranscriptHtmlBuilder.cs +++ b/src/MandoCode.Desktop/Services/TranscriptHtmlBuilder.cs @@ -1,4 +1,4 @@ -using System.Net; +using System.Net; using System.Text; using MandoCode.Models; using MandoCode.Services; @@ -91,6 +91,11 @@ public string ApprovalNotice(string text, string state = "success") => public string PlanStarted(int totalSteps) => PlanActivity("Executing plan", $"Preparing {totalSteps} step{(totalSteps == 1 ? "" : "s")}"); + /// The "switching folders" progress line. Lives here as a const because + /// matches on it verbatim — reworded in one place only, the + /// notice would silently start replaying on session restore. + public const string ProjectSwitchNotice = "Switching to the new project — this conversation is kept…"; + /// True for blocks that describe LIVE session state (status chips: connection, /// model ready, MCP counts, pending offers) rather than conversation history. Session /// restore replays journaled transcripts — replaying a dead process's state pills next @@ -102,7 +107,7 @@ public static bool IsEphemeralStatus(string blockHtml) => || blockHtml.StartsWith("
Rebuilding the AI session for the new project…<", StringComparison.Ordinal) + || blockHtml.Contains($">{ProjectSwitchNotice}<", StringComparison.Ordinal) || blockHtml.Contains(">✓ Ready.<", StringComparison.Ordinal) || ModelNoticeReplay.IsTransient(blockHtml);