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