From 6811ad5c3711dc4c1d87c1b0493b6f1c9da5a858 Mon Sep 17 00:00:00 2001 From: DevMando Date: Wed, 9 Sep 2026 20:21:14 -0700 Subject: [PATCH] Keep the conversation when an agent changes project folder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changing a tab's project root called AIService.ReinitializeAsync, which ends in ClearHistoryAsync. That wipe is meant for model switches, where a different model mid-history really is a different conversation. A folder change is not: it is a navigation step inside one piece of work, so everything said up to that point still applies. Take the history-preserving path instead. RefreshSettingsAsync rebuilds the system prompt, the agent, and the MCP tool set exactly as Reinitialize does, and swaps the system message in place, but leaves the conversation alone — the same trade ChatController.RefreshFromConfigAsync already makes. The tools need no rebuild to follow the move: they hold the live ProjectRootAccessor that is mutated above them, not a copied path. Reload skills before refreshing, since the new folder brings its own project skills and the skill index is baked into the system prompt. Append a note to history recording the move. Without it the model keeps resolving remembered paths against a root that shifted under it — history survives but silently goes stale. The progress line is reworded to say the conversation is kept, and moves into a shared constant: the replay filter matches it verbatim, so rewording one side alone would make a stale notice reappear on every session restore. --- CHANGELOG.md | 6 +++++ .../Controls/ChatTabView.Explorer.cs | 4 +-- .../Services/AgentSession.cs | 27 ++++++++++++++++--- .../Services/TranscriptHtmlBuilder.cs | 9 +++++-- 4 files changed, 39 insertions(+), 7 deletions(-) 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);