Skip to content
Closed
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
29 changes: 29 additions & 0 deletions src/CodeShellManager/Services/SessionOrder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
namespace CodeShellManager.Services;

/// <summary>WPF-free ordering rules between the sidebar model and the live session list.</summary>
public static class SessionOrder
{
/// <summary>
/// Index at which a (re)launched session belongs in the live list
/// (<c>MainViewModel.Sessions</c>) so it keeps its place relative to the model order
/// (<c>SessionManager.Sessions</c>), or -1 if the session isn't in the model.
/// </summary>
/// <remarks>
/// The two lists are different index spaces: the model also holds dormant sessions and
/// sessions the restore loop hasn't launched yet. Restart and wake re-insert a VM whose
/// sidebar row never moved, so the raw model index is wrong by one for every such session
/// above it — the pane drifted down the grid, often to the end, while the row stayed put.
/// The answer is the number of live sessions that precede it in the model.
/// </remarks>
public static int LiveInsertIndex(IReadOnlyList<string> modelOrder, IReadOnlyList<string> liveIds, string id)
{
var live = new HashSet<string>(liveIds);
int count = 0;
foreach (var modelId in modelOrder)
{
if (modelId == id) return count;
if (live.Contains(modelId)) count++;
}
return -1;
}
}
15 changes: 8 additions & 7 deletions src/CodeShellManager/ViewModels/MainViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -361,13 +361,14 @@ void Promote()
};
}

// Mirror SessionManager order so insert-after-parent (CreateSession with afterSessionId)
// also lands the VM at the matching slot — otherwise the model order would be correct
// but the sidebar would still show the new entry at the bottom.
int idx = -1;
for (int i = 0; i < _sessionManager.Sessions.Count; i++)
if (_sessionManager.Sessions[i].Id == vm.Id) { idx = i; break; }
if (idx >= 0 && idx <= Sessions.Count) Sessions.Insert(idx, vm);
// Mirror SessionManager order so insert-after-parent (CreateSession with afterSessionId),
// restart and wake all land the VM at the slot matching its sidebar row. The model
// index can't be used directly — see SessionOrder.LiveInsertIndex.
int idx = SessionOrder.LiveInsertIndex(
_sessionManager.Sessions.Select(s => s.Id).ToList(),
Sessions.Select(s => s.Id).ToList(),
vm.Id);
if (idx >= 0) Sessions.Insert(idx, vm);
else Sessions.Add(vm);
ActiveSession = vm;
_ = SaveStateAsync();
Expand Down
79 changes: 79 additions & 0 deletions tests/CodeShellManager.Tests/SessionOrderTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using CodeShellManager.Services;
using Xunit;

namespace CodeShellManager.Tests;

/// <summary>
/// Where a (re)launched session's VM goes in <c>MainViewModel.Sessions</c> (live only),
/// given its position in <c>SessionManager.Sessions</c> (live + dormant, all groups).
/// Restart and wake both re-insert a VM whose sidebar row never moved; using the raw
/// model index against the live list pushed the pane one slot further down for every
/// dormant or not-yet-relaunched session above it.
/// </summary>
public class SessionOrderTests
{
[Fact]
public void Restart_with_a_dormant_session_above_lands_in_its_sidebar_position()
{
// Sidebar: A, D(dormant), B, C. Restarting B: the live list is [A, C] meanwhile.
string[] model = ["A", "D", "B", "C"];
string[] live = ["A", "C"];

Assert.Equal(1, SessionOrder.LiveInsertIndex(model, live, "B"));
}

[Fact]
public void Restart_of_the_first_session_goes_first()
{
string[] model = ["D", "A", "B"];
string[] live = ["B"];

Assert.Equal(0, SessionOrder.LiveInsertIndex(model, live, "A"));
}

[Fact]
public void Restart_of_the_last_session_goes_last()
{
string[] model = ["A", "D1", "B", "D2", "C"];
string[] live = ["A", "B"];

Assert.Equal(2, SessionOrder.LiveInsertIndex(model, live, "C"));
}

[Fact]
public void Several_dormant_sessions_above_do_not_accumulate_an_offset()
{
string[] model = ["D1", "D2", "D3", "A", "B", "C"];
string[] live = ["A", "C"];

Assert.Equal(1, SessionOrder.LiveInsertIndex(model, live, "B"));
}

[Fact]
public void Sessions_still_launching_above_are_skipped()
{
// Restore loop: A and B are in the model but not yet live when C registers.
string[] model = ["A", "B", "C", "E"];
string[] live = ["E"];

Assert.Equal(0, SessionOrder.LiveInsertIndex(model, live, "C"));
}

[Fact]
public void With_nothing_dormant_it_matches_the_model_index()
{
string[] model = ["A", "B", "C"];
string[] live = ["A", "C"];

Assert.Equal(1, SessionOrder.LiveInsertIndex(model, live, "B"));
}

[Fact]
public void A_session_missing_from_the_model_appends()
{
string[] model = ["A", "B"];
string[] live = ["A", "B"];

Assert.Equal(-1, SessionOrder.LiveInsertIndex(model, live, "X"));
}
}