Skip to content
Open
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
58 changes: 51 additions & 7 deletions PolyPilot.Tests/CommandHistoryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public class CommandHistoryTests
public void Navigate_EmptyHistory_ReturnsNull()
{
var history = new CommandHistory();
Assert.Null(history.Navigate(up: true));
Assert.Null(history.Navigate(up: true, currentText: "draft"));
Assert.Null(history.Navigate(up: false));
}

Expand Down Expand Up @@ -58,19 +58,19 @@ public void Navigate_Up_CyclesThroughAllEntries()
}

[Fact]
public void Navigate_UpThenDown_NavigatesCorrectly()
public void Navigate_UpThenDown_RestoresDraft()
{
var history = new CommandHistory();
history.Add("first");
history.Add("second");
history.Add("third");

Assert.Equal("third", history.Navigate(up: true)!.Value.Text);
Assert.Equal("third", history.Navigate(up: true, currentText: "my draft")!.Value.Text);
Assert.Equal("second", history.Navigate(up: true)!.Value.Text);
// Down goes back toward newest
Assert.Equal("third", history.Navigate(up: false)!.Value.Text);
// Past end clears to empty
Assert.Equal("", history.Navigate(up: false)!.Value.Text);
// Past end restores the original draft
Assert.Equal("my draft", history.Navigate(up: false)!.Value.Text);
}

[Fact]
Expand Down Expand Up @@ -148,14 +148,58 @@ public void IsNavigating_TrueAfterUpFalseAfterReturningToEnd()
}

[Fact]
public void Navigate_Down_PastEnd_ReturnsEmpty()
public void Navigate_Down_PastEnd_RestoresDraftOrEmpty()
{
var history = new CommandHistory();
history.Add("cmd");

// Already past end, navigate down
// Already past end, navigate down — no draft saved, returns empty
var result = history.Navigate(up: false);
Assert.NotNull(result);
Assert.Equal("", result!.Value.Text);
}

[Fact]
public void Navigate_DraftPreservedThroughFullCycle()
{
var history = new CommandHistory();
history.Add("old1");
history.Add("old2");

// User is typing "work in progress", presses up
Assert.Equal("old2", history.Navigate(up: true, currentText: "work in progress")!.Value.Text);
Assert.Equal("old1", history.Navigate(up: true)!.Value.Text);
// All the way back down
Assert.Equal("old2", history.Navigate(up: false)!.Value.Text);
Assert.Equal("work in progress", history.Navigate(up: false)!.Value.Text);
Assert.False(history.IsNavigating);
}

[Fact]
public void Navigate_EmptyDraftPreserved()
{
var history = new CommandHistory();
history.Add("cmd");

// User presses up with empty input
Assert.Equal("cmd", history.Navigate(up: true, currentText: "")!.Value.Text);
// Down restores empty draft
Assert.Equal("", history.Navigate(up: false)!.Value.Text);
}

[Fact]
public void Add_ClearsDraft()
{
var history = new CommandHistory();
history.Add("old");

// Navigate up with a draft
history.Navigate(up: true, currentText: "my draft");
// Send a message — this should clear the draft
history.Add("new message");

// Navigate up then down — draft should be gone (empty, not "my draft")
Assert.Equal("new message", history.Navigate(up: true, currentText: "")!.Value.Text);
Assert.Equal("", history.Navigate(up: false)!.Value.Text);
}
}
6 changes: 3 additions & 3 deletions PolyPilot/Components/Pages/Dashboard.razor
Original file line number Diff line number Diff line change
Expand Up @@ -1000,7 +1000,7 @@
if ((e.key === 'ArrowUp' && atStart) || (e.key === 'ArrowDown' && (atEnd || histNavActive))) {
if (sessionName && window.__dashRef) {
e.preventDefault();
window.__dashRef.invokeMethodAsync('JsNavigateHistory', sessionName, e.key === 'ArrowUp').then(function(isNav) {
window.__dashRef.invokeMethodAsync('JsNavigateHistory', sessionName, e.key === 'ArrowUp', ta.value || '').then(function(isNav) {
if (!window.__histNavActive) window.__histNavActive = {};
window.__histNavActive[sessionName] = isNav;
}).catch(function() {
Expand Down Expand Up @@ -3640,12 +3640,12 @@
}

[JSInvokable]
public async Task<bool> JsNavigateHistory(string sessionName, bool up)
public async Task<bool> JsNavigateHistory(string sessionName, bool up, string? currentText = null)
{
if (!commandHistoryBySession.TryGetValue(sessionName, out var hist))
return false;

var result = hist.Navigate(up);
var result = hist.Navigate(up, currentText);
if (result == null) return false;

var (text, cursorAtStart) = result.Value;
Expand Down
14 changes: 12 additions & 2 deletions PolyPilot/Models/CommandHistory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ namespace PolyPilot.Models;

/// <summary>
/// Manages per-session command history with up/down navigation.
/// Preserves the user's in-progress draft when entering history mode
/// and restores it when navigating back down past the newest entry.
/// </summary>
public class CommandHistory
{
private readonly List<string> _entries = new();
private int _index;
private string? _draft;
private const int MaxEntries = 50;

public int Count => _entries.Count;
Expand All @@ -23,6 +26,7 @@ public void Add(string command)
if (_entries.Count > MaxEntries) _entries.RemoveAt(0);
}
_index = _entries.Count; // past the end = "no selection"
_draft = null; // draft consumed — message was sent
}

/// <summary>
Expand All @@ -31,16 +35,22 @@ public void Add(string command)
/// false when navigating down (so next ArrowDown fires immediately).
/// Returns null if history is empty.
/// </summary>
public (string Text, bool CursorAtStart)? Navigate(bool up)
/// <param name="up">True for ArrowUp (older), false for ArrowDown (newer).</param>
/// <param name="currentText">The current input text — saved as draft on first up-navigation.</param>
public (string Text, bool CursorAtStart)? Navigate(bool up, string? currentText = null)
{
if (_entries.Count == 0) return null;

// Stash the draft when first entering history mode
if (up && !IsNavigating)
_draft = currentText ?? "";

if (up)
_index = Math.Max(0, _index - 1);
else
_index = Math.Min(_entries.Count, _index + 1);

var text = _index < _entries.Count ? _entries[_index] : "";
var text = _index < _entries.Count ? _entries[_index] : (_draft ?? "");
return (text, up);
}
}