Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/MandoCode.Desktop/Controls/ChatTabView.Explorer.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Collections.ObjectModel;
using System.Collections.ObjectModel;
using System.Text.Json;
using MandoCode.Models;
using MandoCode.Desktop.Services;
Expand Down Expand Up @@ -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.
Expand Down
27 changes: 24 additions & 3 deletions src/MandoCode.Desktop/Services/AgentSession.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using MandoCode.Desktop.ViewModels;
using MandoCode.Desktop.ViewModels;
using MandoCode.Models;
using MandoCode.Services;
using Microsoft.Extensions.DependencyInjection;
Expand Down Expand Up @@ -284,14 +284,35 @@ public void ResetConfigToDefaults(ConfigCoordinator configs)
_persistedConfigJson = AgentConfigStore.Fingerprint(Config);
}

/// <summary>Repoints this tab at a different project folder and rebuilds its AI session.</summary>
/// <summary>
/// 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.
/// </summary>
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)
Expand Down
9 changes: 7 additions & 2 deletions src/MandoCode.Desktop/Services/TranscriptHtmlBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Net;
using System.Net;
using System.Text;
using MandoCode.Models;
using MandoCode.Services;
Expand Down Expand Up @@ -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")}");

/// <summary>The "switching folders" progress line. Lives here as a const because
/// <see cref="IsEphemeralStatus"/> matches on it verbatim — reworded in one place only, the
/// notice would silently start replaying on session restore.</summary>
public const string ProjectSwitchNotice = "Switching to the new project — this conversation is kept…";

/// <summary>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
Expand All @@ -102,7 +107,7 @@ public static bool IsEphemeralStatus(string blockHtml) =>
|| blockHtml.StartsWith("<div class=\"panel checkpoint-card\"", StringComparison.Ordinal)
// Boot/progress narration — true only while it was happening. ("Project root
// changed to: X" is deliberately NOT here: that's a real event, kept as history.)
|| blockHtml.Contains(">Rebuilding the AI session for the new project…<", StringComparison.Ordinal)
|| blockHtml.Contains($">{ProjectSwitchNotice}<", StringComparison.Ordinal)
|| blockHtml.Contains(">✓ Ready.<", StringComparison.Ordinal)
|| ModelNoticeReplay.IsTransient(blockHtml);

Expand Down
Loading