diff --git a/MandoCode b/MandoCode index 6673218..7aede43 160000 --- a/MandoCode +++ b/MandoCode @@ -1 +1 @@ -Subproject commit 667321828d4380e7b76fb8fb0358b4ae77b77b1a +Subproject commit 7aede431fb53ba46db630ae4384fc8189b4ed5b5 diff --git a/src/MandoCode.Desktop.Tests/ResponseStreamerTests.cs b/src/MandoCode.Desktop.Tests/ResponseStreamerTests.cs index fc758c6..16e85cf 100644 --- a/src/MandoCode.Desktop.Tests/ResponseStreamerTests.cs +++ b/src/MandoCode.Desktop.Tests/ResponseStreamerTests.cs @@ -76,6 +76,7 @@ public event Action? OnFunctionCompleted { add { } remo public void AppendUserNote(string text) => throw new NotSupportedException(); public int TryRestoreHistoryJson(string json) => throw new NotSupportedException(); public Task EnterLearnModeAsync() => throw new NotSupportedException(); + public Task CompactHistoryAsync() => throw new NotSupportedException(); public Task ClearHistoryAsync() => throw new NotSupportedException(); public Task> GetHistoryAsync() => throw new NotSupportedException(); } diff --git a/src/MandoCode.Desktop/Assets/web/transcript/transcript.css b/src/MandoCode.Desktop/Assets/web/transcript/transcript.css index 15b4d3c..ff912f4 100644 --- a/src/MandoCode.Desktop/Assets/web/transcript/transcript.css +++ b/src/MandoCode.Desktop/Assets/web/transcript/transcript.css @@ -248,6 +248,73 @@ .red { color: var(--red); } .token-summary { text-align: right; font-size: 12px; } + /* Live plan status is operationally important, not ordinary transcript narration. Give it a + high-opacity themed surface so a bright or busy chat wallpaper cannot wash out the active + step. The panel/border/accent variables keep it native to every appearance; backdrop blur + preserves contrast while still letting a little of the selected image show through. */ + .plan-activity { display: flex; align-items: flex-start; gap: 10px; + max-width: 720px; padding: 10px 12px; border: 1px solid var(--border); + border-left: 3px solid var(--accent); border-radius: 10px; + background: var(--panel); + background: color-mix(in srgb, var(--panel) 82%, transparent); + backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px); + box-shadow: 0 5px 18px rgba(0, 0, 0, 0.20); } + .plan-activity-dot { flex: 0 0 auto; width: 8px; height: 8px; margin-top: 6px; + border-radius: 50%; background: var(--accent); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent); } + .plan-activity-title { color: var(--fg); font-weight: 700; line-height: 1.25; } + .plan-activity-emoji { display: inline-block; margin-right: 6px; font-family: "Segoe UI Emoji", "Segoe UI Symbol", sans-serif; } + .plan-activity-detail { color: var(--fg); margin-top: 2px; font-size: 13px; line-height: 1.35; } + .plan-activity.success { border-left-color: var(--green); } + .plan-activity.success .plan-activity-dot { background: var(--green); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--green) 22%, transparent); } + .plan-activity.warning { border-left-color: var(--gold); } + .plan-activity.warning .plan-activity-dot { background: var(--gold); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--gold) 22%, transparent); } + .plan-activity.error { border-left-color: var(--red); } + .plan-activity.error .plan-activity-dot { background: var(--red); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--red) 22%, transparent); } + .checkpoint-card { background: var(--panel); + background: color-mix(in srgb, var(--panel) 82%, transparent); + backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px); + box-shadow: 0 5px 18px rgba(0, 0, 0, 0.20); } + .assistant.plan-step-result { background: var(--panel); + background: color-mix(in srgb, var(--panel) 82%, transparent); + backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px); + border: 1px solid var(--border); border-radius: 10px; padding: 6px 12px 8px 12px; + box-shadow: 0 5px 18px rgba(0, 0, 0, 0.20); } + + html[data-win98] .plan-activity { background: var(--bg); color: #000; border-radius: 0; + border: 2px solid; border-left: 2px solid; + border-color: #FFFFFF #404040 #404040 #FFFFFF; box-shadow: none; backdrop-filter: none; } + html[data-win98] .plan-activity-title, html[data-win98] .plan-activity-detail { color: #000; } + html[data-win98] .plan-activity-dot { background: #1084D0; box-shadow: none; } + html[data-win98] .assistant.plan-step-result { border-radius: 0; box-shadow: none; + backdrop-filter: none; padding: 0; } + html[data-win98] .checkpoint-card { background: var(--bg); box-shadow: none; + backdrop-filter: none; } + + /* System notices use a smaller sibling of the plan-activity card. They replace the old bare + colored/dim lines, which disappear against bright wallpapers at high image opacity. Pills, + command panels, and normal messages keep their existing presentation. */ + .notice-card { display: flex; align-items: flex-start; gap: 8px; max-width: 720px; + padding: 7px 10px; border: 1px solid var(--border); border-left: 3px solid var(--sky); + border-radius: 8px; background: var(--panel); + background: color-mix(in srgb, var(--panel) 82%, transparent); + backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px); + box-shadow: 0 4px 14px rgba(0, 0, 0, 0.16); } + .notice-emoji { flex: 0 0 auto; margin-top: 1px; + font-family: "Segoe UI Emoji", "Segoe UI Symbol", sans-serif; } + .notice-text { color: var(--fg); white-space: pre-wrap; line-height: 1.4; } + .notice-card.success { border-left-color: var(--green); } + .notice-card.warn { border-left-color: var(--gold); } + .notice-card.error { border-left-color: var(--red); } + .notice-card.dim { border-left-color: var(--border); } + html[data-win98] .notice-card { background: var(--bg); border-radius: 0; box-shadow: none; + backdrop-filter: none; border: 2px solid; border-left: 2px solid; + border-color: #FFFFFF #404040 #404040 #FFFFFF; } + html[data-win98] .notice-text { color: #000; } + /* Status chips — compact pills for session/connection state. A CSS status dot (crisp, theme-aware) replaces status emoji; state = ok | warn | err | neutral. */ /* Centered to match the tool pills: all system/status chrome sits centered, conversation stays left. */ diff --git a/src/MandoCode.Desktop/Controls/ChatTabView.Header.cs b/src/MandoCode.Desktop/Controls/ChatTabView.Header.cs index c5d70d2..21a7a78 100644 --- a/src/MandoCode.Desktop/Controls/ChatTabView.Header.cs +++ b/src/MandoCode.Desktop/Controls/ChatTabView.Header.cs @@ -22,6 +22,7 @@ public sealed partial class ChatTabView public void UpdateHeader() { + InputBox.PlaceholderText = $"Message Agent {Session.Title}… /commands · @file · !shell"; ModelText.Text = _controller.ModelName; ProjectRootText.Text = _controller.ProjectRootPath; ConnectionDot.Fill = new SolidColorBrush( diff --git a/src/MandoCode.Desktop/Controls/ChatTabView.Transcript.cs b/src/MandoCode.Desktop/Controls/ChatTabView.Transcript.cs index 53c2902..d8efe3a 100644 --- a/src/MandoCode.Desktop/Controls/ChatTabView.Transcript.cs +++ b/src/MandoCode.Desktop/Controls/ChatTabView.Transcript.cs @@ -53,7 +53,10 @@ private async Task RestoreJournaledTranscriptAsync() // called by MainWindow after any saved model is re-selected — a model switch clears // history, so restoring memory here would risk it being wiped moments later). _replayedBlockCount = blocks.Count; - await AppendRawAsync(_html.Dim("— restored from your previous session —")); + await AppendRawAsync(_html.StatusCard( + "Previous session restored", + "This transcript was restored from your previous session.", + "success")); } catch { /* a failed replay must never block a fresh conversation */ } } @@ -79,8 +82,10 @@ public async Task RestoreConversationMemoryAsync() var restored = await Task.Run(() => Session.Ai.TryRestoreHistoryJson(historyJson)); if (restored > 0) { - await AppendRawAsync(_html.Dim( - $"Conversation memory restored — the agent remembers this session ({restored} messages).")); + await AppendRawAsync(_html.StatusCard( + "Conversation memory restored", + $"The agent remembers this session ({restored} messages).", + "success")); return; } } diff --git a/src/MandoCode.Desktop/Controls/ChatTabView.xaml b/src/MandoCode.Desktop/Controls/ChatTabView.xaml index 6030d7b..e487cba 100644 --- a/src/MandoCode.Desktop/Controls/ChatTabView.xaml +++ b/src/MandoCode.Desktop/Controls/ChatTabView.xaml @@ -124,8 +124,8 @@ @@ -175,7 +175,7 @@ - diff --git a/src/MandoCode.Desktop/Controls/ChatTabView.xaml.cs b/src/MandoCode.Desktop/Controls/ChatTabView.xaml.cs index 0216f75..452fbd7 100644 --- a/src/MandoCode.Desktop/Controls/ChatTabView.xaml.cs +++ b/src/MandoCode.Desktop/Controls/ChatTabView.xaml.cs @@ -111,6 +111,7 @@ public ChatTabView(Window owner, AgentSession session, TranscriptHtmlBuilder htm _transcript.BlockAdded += OnTranscriptBlock; _transcript.Cleared += OnTranscriptCleared; Session.Busy.Changed += OnBusyChanged; + Session.TitleChanged += OnAgentTitleChanged; _controller.StateChanged += OnControllerStateChanged; _controller.PlanProgressChanged += OnPlanProgress; @@ -119,6 +120,7 @@ public ChatTabView(Window owner, AgentSession session, TranscriptHtmlBuilder htm _controller.ClipboardCopyRequested += OnClipboardCopy; _controller.ExitRequested += OnExitRequested; _controller.SnapshotOfferChanged += OnSnapshotOfferChanged; + _controller.HistoryCompacted += OnHistoryCompacted; UpdateHeader(); } @@ -127,12 +129,15 @@ public ChatTabView(Window owner, AgentSession session, TranscriptHtmlBuilder htm private void OnTranscriptBlock(string html) => OnUi(() => AppendHtml(html)); private void OnTranscriptCleared() => OnUi(ClearTranscript); private void OnBusyChanged(bool busy, string? activity) => OnUi(() => UpdateBusy(busy, activity)); + private void OnAgentTitleChanged(string _) => OnUi(UpdateHeader); private void OnControllerStateChanged() => OnUi(UpdateHeader); private void OnPlanProgress(int done, int total, bool active) => OnUi(() => UpdatePlanProgress(done, total, active)); private void OnSetupNeeded() => OnUi(() => SetupRequested?.Invoke()); private void OnMcpEditorRequested(string? name) => OnUi(() => McpEditorRequested?.Invoke(name)); private void OnClipboardCopy(string text) => OnUi(() => ClipboardCopyRequested?.Invoke(text)); private void OnExitRequested() => OnUi(() => ExitRequested?.Invoke()); + private void OnHistoryCompacted() => _ = Task.Run(() => + SessionHistoryStore.Save(Session.PersistKey, Session.Ai.ExportHistoryJson())); private void OnSnapshotOfferChanged() => OnUi(RefreshSnapshotOffer); private void OnUi(Action action) @@ -329,6 +334,7 @@ public void Shutdown() _transcript.BlockAdded -= OnTranscriptBlock; _transcript.Cleared -= OnTranscriptCleared; Session.Busy.Changed -= OnBusyChanged; + Session.TitleChanged -= OnAgentTitleChanged; _controller.StateChanged -= OnControllerStateChanged; _controller.PlanProgressChanged -= OnPlanProgress; _controller.SetupNeeded -= OnSetupNeeded; @@ -336,6 +342,7 @@ public void Shutdown() _controller.ClipboardCopyRequested -= OnClipboardCopy; _controller.ExitRequested -= OnExitRequested; _controller.SnapshotOfferChanged -= OnSnapshotOfferChanged; + _controller.HistoryCompacted -= OnHistoryCompacted; _controller.CancelActiveRequest(); diff --git a/src/MandoCode.Desktop/Services/AgentSession.cs b/src/MandoCode.Desktop/Services/AgentSession.cs index 1523ff0..e23a7c0 100644 --- a/src/MandoCode.Desktop/Services/AgentSession.cs +++ b/src/MandoCode.Desktop/Services/AgentSession.cs @@ -41,10 +41,19 @@ public sealed class AgentSession public string Title { get => _title; - set { _title = value; Config.AgentName = value; } + set + { + if (_title == value) return; + _title = value; + Config.AgentName = value; + TitleChanged?.Invoke(value); + } } private string _title = ""; + /// Raised when the user renames this tab's agent. + public event Action? TitleChanged; + /// Durable identity across app launches (unlike , a process-local /// counter). Names this session's transcript journal on disk; a restored tab passes its /// saved key back in so it reattaches to its own history. @@ -108,7 +117,6 @@ public AgentSession( // agents working in the same project from overwriting each other's unfinished plans. PlanRunners = new PlanRunnerSelector( Config, - Planner, new AiServicePlanStepExecutor(Ai), PlanHandoff, ProjectRoot, diff --git a/src/MandoCode.Desktop/Services/AiServiceAdapter.cs b/src/MandoCode.Desktop/Services/AiServiceAdapter.cs index 8729c34..1f9a9f1 100644 --- a/src/MandoCode.Desktop/Services/AiServiceAdapter.cs +++ b/src/MandoCode.Desktop/Services/AiServiceAdapter.cs @@ -65,6 +65,7 @@ public Task GeneratePlanAsync(string request, string? revisionCon public void AppendUserNote(string text) => _ai.AppendUserNote(text); public int TryRestoreHistoryJson(string json) => _ai.TryRestoreHistoryJson(json); public Task EnterLearnModeAsync() => _ai.EnterLearnModeAsync(); + public Task CompactHistoryAsync() => _ai.CompactHistoryAsync(); public Task ClearHistoryAsync() => _ai.ClearHistoryAsync(); public Task> GetHistoryAsync() => _ai.GetHistoryAsync(); } diff --git a/src/MandoCode.Desktop/Services/IAiService.cs b/src/MandoCode.Desktop/Services/IAiService.cs index 62fad73..6f8514a 100644 --- a/src/MandoCode.Desktop/Services/IAiService.cs +++ b/src/MandoCode.Desktop/Services/IAiService.cs @@ -57,6 +57,7 @@ void SetRequestContext(string? request) { } void AppendUserNote(string text); int TryRestoreHistoryJson(string json); Task EnterLearnModeAsync(); + Task CompactHistoryAsync(); Task ClearHistoryAsync(); Task> GetHistoryAsync(); } diff --git a/src/MandoCode.Desktop/Services/TranscriptHtmlBuilder.cs b/src/MandoCode.Desktop/Services/TranscriptHtmlBuilder.cs index f1c50de..4942603 100644 --- a/src/MandoCode.Desktop/Services/TranscriptHtmlBuilder.cs +++ b/src/MandoCode.Desktop/Services/TranscriptHtmlBuilder.cs @@ -59,11 +59,28 @@ public string UserEcho(string text) => public string AssistantCard(string markdown, string? speaker = null) => $"
{E(speaker ?? "MandoCode")}
{FromMarkdown(markdown)}
"; - public string Info(string text) => $"
{E(text)}
"; - public string Success(string text) => $"
{E(text)}
"; - public string Warn(string text) => $"
{E(text)}
"; - public string Error(string text) => $"
{E(text)}
"; - public string Dim(string text) => $"
{E(text)}
"; + /// Step results are part of the live-plan surface, so they retain a solid readable + /// panel even when ordinary transcript messages are configured as flat. + public string PlanStepResult(string markdown, string? speaker = null) => + $"
{E(speaker ?? "MandoCode")}
{FromMarkdown(markdown)}
"; + + // System notices need their own surface. A theme can intentionally place a high-opacity image + // behind a flat transcript, so color alone is not enough contrast for actions, warnings, or + // recovery guidance. Keep ordinary user/assistant messages untouched; only these notices use + // the compact card treatment. + public string Info(string text) => NoticeCard(text, "info"); + public string Success(string text) => NoticeCard(text, "success"); + public string Warn(string text) => NoticeCard(text, "warn"); + public string Error(string text) => NoticeCard(text, "error"); + public string Dim(string text) => NoticeCard(text, "dim"); + + /// + /// A high-contrast, live plan-status card. Plan execution and step headers must remain readable + /// even when a user has turned their chat wallpaper opacity up, so they get a themed surface + /// rather than being painted directly on the transcript background. + /// + public string PlanStarted(int totalSteps) => + PlanActivity("Executing plan", $"Preparing {totalSteps} step{(totalSteps == 1 ? "" : "s")}"); /// True for blocks that describe LIVE session state (status chips: connection, /// model ready, MCP counts, pending offers) rather than conversation history. Session @@ -93,6 +110,18 @@ public string StatusChip(string primary, string? secondary = null, string state return sb.ToString(); } + private static string NoticeCard(string text, string state) => + $"
{NoticeEmoji(state)}" + + $"{E(text)}
"; + + private static string NoticeEmoji(string state) => state switch + { + "success" => "✅", + "warn" => "⚠️", + "error" => "❌", + _ => "ℹ️", + }; + /// /// A STATIC tool-call pill (no animation — draws once, costs nothing). Replaces the plain /// "[Function] …" / "[Done] ✓" text lines for non-file tools with something that reads as a @@ -262,7 +291,33 @@ public string OperationCard(OperationDisplayEvent op) public string CheckpointCard(PlanRunState saved) => CheckpointCardHtml.Build(saved); public string StepStarted(int current, int total, string description) => - $"
Step {current}/{total}: {E(description)}
"; + PlanActivity($"Step {current}/{total}", description); + + public string StepCompleted(int current, int total) => + PlanActivity($"Step {current}/{total} complete", "Moving to the next step", "success"); + + public string StepFailed(int current, int total, string message) => + PlanActivity($"Step {current}/{total} needs attention", message, "error"); + + public string PlanFinished(string title, string detail, string state) => + PlanActivity(title, detail, state); + + /// A high-contrast, themed status surface for important system notices. + public string StatusCard(string title, string detail, string state = "") => + PlanActivity(title, detail, state); + + private static string PlanActivity(string title, string detail, string state = "") => + $"
" + + $"
{ActivityEmoji(state)}{E(title)}
" + + $"
{E(detail)}
"; + + private static string ActivityEmoji(string state) => state switch + { + "success" => "✅", + "warning" => "⚠️", + "error" => "❌", + _ => "▶️", + }; public string TokenSummary(string text) => $"
{E(text)}
"; diff --git a/src/MandoCode.Desktop/Services/WinUiApprovalService.cs b/src/MandoCode.Desktop/Services/WinUiApprovalService.cs index 5659aa6..8288d55 100644 --- a/src/MandoCode.Desktop/Services/WinUiApprovalService.cs +++ b/src/MandoCode.Desktop/Services/WinUiApprovalService.cs @@ -75,7 +75,7 @@ public async Task HandleWriteApprovalAsync(string relativePa if (_globalWriteBypass || _approvedFiles.Contains(relativePath)) { - _transcript.Append(_html.Success("✓ Auto-approved")); + _transcript.Append(_html.StatusCard("Auto-approved", $"{fileName} was already approved for this session.", "success")); _busy.Start(); return new DiffApprovalResult { Response = DiffApprovalResponse.Approved }; } @@ -108,21 +108,21 @@ public async Task HandleWriteApprovalAsync(string relativePa DiffApprovalResult result; if (choice == ApproveLabel) { - _transcript.Append(_html.Success("Changes approved.")); + _transcript.Append(_html.StatusCard("Changes approved", $"{fileName} can be updated.", "success")); result = new DiffApprovalResult { Response = DiffApprovalResponse.Approved }; } else if (choice == noAskLabel) { - _transcript.Append(_html.Success("Changes approved.")); + _transcript.Append(_html.StatusCard("Changes approved", $"{fileName} can be updated.", "success")); if (isNewFile) { _globalWriteBypass = true; - _transcript.Append(_html.Dim("All future writes will be auto-approved for this session.")); + _transcript.Append(_html.StatusCard("Auto-approval enabled", "All future writes will be auto-approved for this session.", "warning")); } else { _approvedFiles.Add(relativePath); - _transcript.Append(_html.Dim($"Future modifications to {fileName} will be auto-approved.")); + _transcript.Append(_html.StatusCard("Auto-approval enabled", $"Future modifications to {fileName} will be auto-approved.", "warning")); } result = new DiffApprovalResult { Response = DiffApprovalResponse.ApprovedNoAskAgain }; } @@ -159,7 +159,7 @@ public async Task HandleCommandApprovalAsync(string command) if (_globalWriteBypass) { - _transcript.Append(_html.Success("✓ Auto-approved command")); + _transcript.Append(_html.StatusCard("Command auto-approved", "Commands are currently auto-approved for this session.", "success")); _busy.Start(); return new DiffApprovalResult { Response = DiffApprovalResponse.Approved }; } @@ -189,14 +189,14 @@ public async Task HandleCommandApprovalAsync(string command) DiffApprovalResult result; if (choice == ApproveLabel) { - _transcript.Append(_html.Success("Command approved.")); + _transcript.Append(_html.StatusCard("Command approved", "The command can now run.", "success")); result = new DiffApprovalResult { Response = DiffApprovalResponse.Approved }; } else if (choice == ApproveNoAskRunLabel) { - _transcript.Append(_html.Success("Command approved.")); + _transcript.Append(_html.StatusCard("Command approved", "The command can now run.", "success")); _globalWriteBypass = true; - _transcript.Append(_html.Dim("All future writes, deletions, and commands will be auto-approved for this session.")); + _transcript.Append(_html.StatusCard("Auto-approval enabled", "All future writes, deletions, and commands will be auto-approved for this session.", "warning")); result = new DiffApprovalResult { Response = DiffApprovalResponse.ApprovedNoAskAgain }; } else if (choice == DenyLabel) @@ -261,7 +261,7 @@ public async Task HandleDeleteApprovalAsync(string relativeP if (_globalWriteBypass) { - _transcript.Append(_html.Success("✓ Auto-approved deletion")); + _transcript.Append(_html.StatusCard("Deletion auto-approved", "Deletions are currently auto-approved for this session.", "success")); _busy.Start(); return new DiffApprovalResult { Response = DiffApprovalResponse.Approved }; } @@ -292,14 +292,14 @@ public async Task HandleDeleteApprovalAsync(string relativeP DiffApprovalResult result; if (choice == ApproveDeletionLabel) { - _transcript.Append(_html.Success("Deletion approved.")); + _transcript.Append(_html.StatusCard("Deletion approved", "The deletion can now run.", "success")); result = new DiffApprovalResult { Response = DiffApprovalResponse.Approved }; } else if (choice == ApproveNoAskDeleteLabel) { - _transcript.Append(_html.Success("Deletion approved.")); + _transcript.Append(_html.StatusCard("Deletion approved", "The deletion can now run.", "success")); _globalWriteBypass = true; - _transcript.Append(_html.Dim("All future writes and deletions will be auto-approved for this session.")); + _transcript.Append(_html.StatusCard("Auto-approval enabled", "All future writes and deletions will be auto-approved for this session.", "warning")); result = new DiffApprovalResult { Response = DiffApprovalResponse.ApprovedNoAskAgain }; } else if (choice == DenyLabel) @@ -335,7 +335,7 @@ public async Task HandleMcpApprovalAsync(string serverName, if (_globalWriteBypass) { - _transcript.Append(_html.Success("✓ Auto-approved MCP tool")); + _transcript.Append(_html.StatusCard("MCP tool auto-approved", "MCP tools are currently auto-approved for this session.", "success")); _busy.Start(); return new DiffApprovalResult { Response = DiffApprovalResponse.Approved }; } diff --git a/src/MandoCode.Desktop/ViewModels/ChatController.Plans.cs b/src/MandoCode.Desktop/ViewModels/ChatController.Plans.cs index 04a07d3..799333e 100644 --- a/src/MandoCode.Desktop/ViewModels/ChatController.Plans.cs +++ b/src/MandoCode.Desktop/ViewModels/ChatController.Plans.cs @@ -24,7 +24,9 @@ private void ShowUnfinishedPlanNotice() if (outstanding == 0) return; _transcript.Append(_html.CheckpointCard(saved)); - _transcript.Append(_html.Dim("Resume or discard it here, or use /plan to inspect every step.")); + _transcript.Append(_html.StatusCard( + "Unfinished plan ready", + "Resume or discard it here, or use /plan to inspect every step.")); } private async Task HandlePlanCommandAsync(string action) @@ -35,13 +37,6 @@ private async Task HandlePlanCommandAsync(string action) return; } - if (!_planRunners.SupportsResume) - { - _transcript.Append(_html.Warn("Plan resume requires the workflow planner.")); - _transcript.Append(_html.Dim("Enable it for this agent with: /config set planner workflow")); - return; - } - if (action == "discard") { _planRunners.DiscardResumable(); diff --git a/src/MandoCode.Desktop/ViewModels/ChatController.cs b/src/MandoCode.Desktop/ViewModels/ChatController.cs index 0d91c43..3f582ba 100644 --- a/src/MandoCode.Desktop/ViewModels/ChatController.cs +++ b/src/MandoCode.Desktop/ViewModels/ChatController.cs @@ -139,6 +139,10 @@ private void NoteShellCommand(string cmd, bool failed, string output) /// The UI should put this text on the clipboard (must marshal to UI thread). public event Action? ClipboardCopyRequested; + /// Raised after /compact replaces the model history, so the tab can persist the + /// compacted state immediately instead of restoring stale full history after a restart. + public event Action? HistoryCompacted; + /// /exit — the UI should close the window. public event Action? ExitRequested; @@ -782,7 +786,7 @@ private async Task HandleProposedPlanAsync(TaskPlan plan, CancellationTo } _deferredPlans.Outcome = DeferredPlanOutcome.Executed; - _transcript.Append(_html.Success("Executing plan...")); + _transcript.Append(_html.PlanStarted(plan.Steps.Count)); PlanProgressChanged?.Invoke(0, plan.Steps.Count, true); try @@ -802,17 +806,14 @@ private async Task HandleProposedPlanAsync(TaskPlan plan, CancellationTo PlanProgressChanged?.Invoke(plan.CompletedStepsCount, plan.Steps.Count, false); } - if (plan.Status == TaskPlanStatus.Completed) - _transcript.Append(_html.Success("Plan completed successfully!")); - else if (plan.Status == TaskPlanStatus.CompletedWithIssues) - _transcript.Append(_html.Warn("Plan completed with skipped or failed steps.")); - else if (plan.Status == TaskPlanStatus.Cancelled) - _transcript.Append(_html.Warn("Plan was cancelled.")); - else - _transcript.Append(_html.Error("Plan completed with errors.")); - - if (!string.IsNullOrEmpty(plan.ExecutionSummary)) - _transcript.Append(_html.Dim(plan.ExecutionSummary)); + var (outcomeTitle, outcomeDetail, outcomeState) = plan.Status switch + { + TaskPlanStatus.Completed => ("Plan completed", plan.ExecutionSummary ?? "All steps completed successfully.", "success"), + TaskPlanStatus.CompletedWithIssues => ("Plan completed with issues", plan.ExecutionSummary ?? "Some steps were skipped or failed.", "warning"), + TaskPlanStatus.Cancelled => ("Plan cancelled", plan.ExecutionSummary ?? "No more plan steps will run.", "warning"), + _ => ("Plan completed with errors", plan.ExecutionSummary ?? "One or more steps could not be completed.", "error"), + }; + _transcript.Append(_html.PlanFinished(outcomeTitle, outcomeDetail, outcomeState)); var summary = plan.ExecutionSummary ?? $"Plan finished with status {plan.Status}."; return summary + @@ -944,15 +945,18 @@ private async Task HandleProgressEventAsync( PlanProgressChanged?.Invoke(progressEvent.CurrentStep, progressEvent.TotalSteps, true); if (!string.IsNullOrEmpty(progressEvent.Message)) { - _transcript.Append(_html.AssistantCard(progressEvent.Message, _config.AgentName)); + _transcript.Append(_html.PlanStepResult(progressEvent.Message, _config.AgentName)); ConversationLogger?.Invoke("a", progressEvent.Message!); } - _transcript.Append(_html.Success($"Step {progressEvent.CurrentStep} completed.")); + _transcript.Append(_html.StepCompleted(progressEvent.CurrentStep, progressEvent.TotalSteps)); break; case TaskProgressType.StepFailed: _busy.Stop(); - _transcript.Append(_html.Error($"Step {progressEvent.CurrentStep} failed: {progressEvent.Message ?? "Unknown error"}")); + _transcript.Append(_html.StepFailed( + progressEvent.CurrentStep, + progressEvent.TotalSteps, + progressEvent.Message ?? "Unknown error")); // Cancellation is already a terminal decision, not another failure choice. Asking // retry/skip/cancel here would make a diff-approval cancel look ineffective. @@ -1146,7 +1150,19 @@ private async Task DispatchCommandAsync(string input) await _ai.ClearHistoryAsync(); _lastAiResponse = null; _transcript.Clear(); - _transcript.Append(_html.Dim("Conversation cleared.")); + _transcript.Append(_html.Dim("Conversation context wiped completely. Start a new conversation.")); + return; + + case "compact": + if (await _ai.CompactHistoryAsync()) + { + HistoryCompacted?.Invoke(); + _transcript.Append(_html.Dim("Conversation context compacted into a recap. This transcript remains visible.")); + } + else + { + _transcript.Append(_html.Dim("Not enough conversation context to compact yet.")); + } return; case "help": @@ -1269,7 +1285,8 @@ private void ShowHelp() ("/mcp remove ", "Remove an MCP server from config"), ("/mcp tools [server]", "List tools exposed by connected MCP servers"), ("/mcp-reload", "Restart MCP servers and re-register their tools"), - ("/clear", "Clear conversation history"), + ("/compact", "Compress context into a recap; keeps this transcript"), + ("/clear", "Wipe all conversation context and start fresh"), ("/exit", "Exit MandoCode") }; _transcript.Append(_html.HelpCard(rows));