diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 151bbfb..938e8cb 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -10,6 +10,18 @@ All notable changes to MandoCode will be documented in this file. and the answer that follows as a second block. Before, everything waited until the end and came out as one merged block. Auto-continued turns also print as each one finishes. Apps built on the engine, like MandoCode Desktop, can now follow a reply as it streams. +- **Watch the reply being written.** While a reply streams, its latest lines show dimmed above the + spinner, so a long answer no longer looks like a frozen "Thinking…". The finished reply still + prints as formatted markdown. +- **Long diffs no longer bury the approval menu.** A change over 30 lines now shows in a scrollable + window above the approve/deny choices (PgUp/PgDn, Home/End to scroll), with a one-line summary + left in your scrollback. Before, hundreds of diff lines pushed the menu off screen. Short diffs + print as they always have. +- **A context meter above the prompt.** A bar shows how full the model's context window is, turning + yellow and then red as it fills, so you can `/compact` before a small model starts forgetting the + start of the conversation. Cloud models, which manage their own window, show the token count. +- **`/history` to look back.** Browse this session's conversation in a scrollable window and + filter it as you type. ### Fixed - **Numbered lists keep each number beside its text.** When a model left blank lines between list diff --git a/src/MandoCode/Components/App.razor b/src/MandoCode/Components/App.razor index d252175..24f8175 100644 --- a/src/MandoCode/Components/App.razor +++ b/src/MandoCode/Components/App.razor @@ -53,6 +53,11 @@ Hidden while an instruction prompt is active so the two text inputs don't fight for focus. *@ @if (_showPrompt && !InstructionCoordinator.IsActive) { + @if (Config.EnableTokenTracking && TokenTracker.LastOperation is { } lastOp) + { + + } 0) + { + + } + OnSubmit="HandleApprovalMenuSubmit" + OnScroll="HandleApprovalPreviewScroll" /> +} + +@* /history — this session's conversation in a scrollable, filterable window. Idle-only, like the + prompt it replaces while open. *@ +@if (_historyActive) +{ + } @* Wizard primitives — bare RazorConsole TextInput / Select that bypass PromptInput's slash/at @@ -830,20 +852,31 @@ private void CloseReply() { lock (_replyLock) _replyOpen = false; + Spinner.UpdatePreview(null); } + // The spinner's live preview only needs the tail of the reply, and copying the whole buffer on + // every chunk would make a long reply quadratic. + private const int PreviewTailChars = 4000; + private void OnResponseTextDelta(string text) { + string preview; lock (_replyLock) { - if (_replyOpen) _replyUnprinted.Append(text); + if (!_replyOpen) return; + _replyUnprinted.Append(text); + var start = Math.Max(0, _replyUnprinted.Length - PreviewTailChars); + preview = _replyUnprinted.ToString(start, _replyUnprinted.Length - start); } + Spinner.UpdatePreview(preview); } // A retried model call streams its reply again from the start. private void OnResponseStreamStarted() { lock (_replyLock) _replyUnprinted.Clear(); + Spinner.UpdatePreview(null); } /// @@ -863,6 +896,7 @@ if (text.Length == 0) return; _replyPrinted.Add(text); } + Spinner.UpdatePreview(null); AnsiConsole.MarkupLine("[green]MandoCode:[/]"); RenderMarkdownGuarded(text); } @@ -878,6 +912,7 @@ _replyPrinted.Clear(); _replyUnprinted.Clear(); } + Spinner.UpdatePreview(null); return ReplyText.Unshown(turnText, printed); } @@ -1068,6 +1103,12 @@ continue; } + if (command == "history") + { + await ShowHistoryAsync(); + continue; + } + if (command == "help") { Console.WriteLine(); @@ -1108,6 +1149,7 @@ table.AddRow("/plan-discard", "Forget an unfinished plan"); table.AddRow("/compact", "Compress context into a recap; keeps the transcript"); table.AddRow("/clear", "Wipe all conversation context and start fresh"); + table.AddRow("/history", "Browse this session's conversation, filtered as you type"); table.AddRow("/exit", "Exit MandoCode"); AnsiConsole.Write(table); AnsiConsole.WriteLine(); @@ -1377,14 +1419,79 @@ /// re-renders. Done here (on state change) rather than inline in markup so the option /// list isn't rebuilt on every unrelated render pass. /// + private bool _historyActive; + private IReadOnlyList _historyMessages = Array.Empty(); + private TaskCompletionSource? _historyTcs; + + /// Opens /history and waits for it to close. The loop is idle while it's open, so the + /// view owns the screen's live region the way the prompt normally does. + private async Task ShowHistoryAsync() + { + if (_messages.Count == 0) + { + AnsiConsole.MarkupLine("[dim]Nothing in this session's history yet.[/]"); + AnsiConsole.WriteLine(); + return; + } + + // A snapshot: the view builds its lines once, and the list mustn't change under it. + _historyMessages = _messages.ToList(); + // Async continuations, so closing from a Blazor event resumes the loop off the dispatcher + // thread (see WaitForVdomInputAsync). + _historyTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + _historyActive = true; + _showPrompt = false; + await InvokeAsync(StateHasChanged); + + await _historyTcs.Task; + + _historyActive = false; + _historyMessages = Array.Empty(); + await InvokeAsync(StateHasChanged); + // Let the VDOM remove the view before anything prints below it. + await Task.Delay(100); + } + + private void CloseHistory() => _historyTcs?.TrySetResult(); + private void OnApprovalMenuStateChanged() { _approvalMenuOptions = ApprovalMenu.Options .Select(o => new ApprovalSelect.Option(o.Text, o.Color)) .ToList(); + _approvalPreviewOffset = 0; // every request starts at the top of its preview InvokeAsync(StateHasChanged); } + private int _approvalPreviewOffset; + + // Room for the menu's options, the panel's borders and footer, and the question above it. + private static int ApprovalPreviewPageSize + { + get + { + try { return Math.Clamp(Console.WindowHeight - 16, 6, 30); } + catch { return 20; } + } + } + + private void HandleApprovalPreviewScroll(ApprovalSelect.ScrollKey key) + { + var count = ApprovalMenu.Preview.Count; + if (count == 0) return; + + var page = ApprovalPreviewPageSize; + var max = Math.Max(0, count - page); + _approvalPreviewOffset = key switch + { + ApprovalSelect.ScrollKey.PageUp => Math.Max(0, _approvalPreviewOffset - page), + ApprovalSelect.ScrollKey.PageDown => Math.Min(max, _approvalPreviewOffset + page), + ApprovalSelect.ScrollKey.Home => 0, + _ => max, + }; + StateHasChanged(); + } + /// /// Bound to the wizard's Select.ValueChanged — fires when the user presses Enter on a /// highlighted option (RazorConsole's Select uses Enter as the commit signal). diff --git a/src/MandoCode/Components/ApprovalSelect.razor b/src/MandoCode/Components/ApprovalSelect.razor index fb833dc..ef5690b 100644 --- a/src/MandoCode/Components/ApprovalSelect.razor +++ b/src/MandoCode/Components/ApprovalSelect.razor @@ -40,6 +40,12 @@ [Parameter] public IReadOnlyList