feat(ui): Mission Control als Session-Hub

- "+" öffnet ein Menü mit allen Listen (working dir) statt direkt den
  Ordner-Dialog; "Ordner wählen…" bleibt als Fallback erste Zeile, damit
  das Menü auch bei fehlgeschlagener Listen-Abfrage nutzbar bleibt
- Rail-Rows: Kontextmenü "Sitzung öffnen" (gesperrt für Running — der
  Worktree gehört dort dem autonomen Lauf)
- Aktivitäts-Strip statt reiner Queue: WaitingForReview/Failed/Roadblock
  kommen dazu, Attention zuerst, unterschieden nur über Farbe (Aktionen
  bleiben in der Task-Detailansicht)
This commit is contained in:
mika kuns
2026-08-28 14:17:38 +02:00
parent 8d597df448
commit 22e70ffcb4
6 changed files with 197 additions and 24 deletions
@@ -202,6 +202,66 @@ public class MissionControlViewModelTests : IDisposable
Assert.Equal(new[] { "first", "second" }, vm.Queued.Select(q => q.Title).ToArray());
}
// ── activity strip: attention rows come first and carry a tint ─────────────
[Fact]
public async Task RefreshQueueAsync_IncludesAttentionRows_AttentionFirst_WithTint()
{
await using (var db = NewContext())
{
db.Lists.Add(new ListEntity { Id = "L1", Name = "Work", CreatedAt = DateTime.UtcNow });
db.Tasks.Add(new TaskEntity { Number = TestTaskNumbers.Next(), Id = "q1", ListId = "L1", Title = "queued", Status = TaskStatus.Queued, CreatedAt = DateTime.UtcNow, SortOrder = 0 });
db.Tasks.Add(new TaskEntity { Number = TestTaskNumbers.Next(), Id = "run1", ListId = "L1", Title = "running", Status = TaskStatus.Running, CreatedAt = DateTime.UtcNow, SortOrder = 1 });
db.Tasks.Add(new TaskEntity { Number = TestTaskNumbers.Next(), Id = "rev1", ListId = "L1", Title = "review", Status = TaskStatus.WaitingForReview, CreatedAt = DateTime.UtcNow, SortOrder = 2 });
db.Tasks.Add(new TaskEntity { Number = TestTaskNumbers.Next(), Id = "rb1", ListId = "L1", Title = "roadblocked", Status = TaskStatus.WaitingForReview, RoadblockCount = 2, CreatedAt = DateTime.UtcNow, SortOrder = 3 });
db.Tasks.Add(new TaskEntity { Number = TestTaskNumbers.Next(), Id = "f1", ListId = "L1", Title = "failed", Status = TaskStatus.Failed, CreatedAt = DateTime.UtcNow, SortOrder = 4 });
db.Tasks.Add(new TaskEntity { Number = TestTaskNumbers.Next(), Id = "done1", ListId = "L1", Title = "done", Status = TaskStatus.Done, CreatedAt = DateTime.UtcNow, SortOrder = 5 });
await db.SaveChangesAsync();
}
using var vm = BuildVm(new FakeWorker());
await vm.RefreshQueueAsync();
// Done is not activity; the other five are, ordered failed → review → running → queued.
Assert.Equal(new[] { "f1", "rev1", "rb1", "run1", "q1" }, vm.Queued.Select(q => q.Id).ToArray());
Assert.Equal("ErrorTint", vm.Queued[0].TintKey);
Assert.Equal("DoneTint", vm.Queued[1].TintKey);
Assert.Equal("RoadblockTint", vm.Queued[2].TintKey); // roadblocks outrank plain review
Assert.Equal("RunningTint", vm.Queued[3].TintKey);
Assert.Null(vm.Queued[4].TintKey);
Assert.Equal("RunningTintBorder", vm.Queued[3].TintBorderKey);
// A running task's worktree belongs to the autonomous run — no interactive session on it.
Assert.False(vm.Queued[3].CanOpenSession);
Assert.True(vm.Queued[4].CanOpenSession);
}
[Fact]
public async Task RefreshSessionTargetsAsync_ListsWithWorkingDir_PlusBrowse()
{
await using (var db = NewContext())
{
db.Lists.Add(new ListEntity { Id = "L1", Name = "Alpha", WorkingDir = @"C:\repos\alpha", CreatedAt = DateTime.UtcNow, SortOrder = 1 });
db.Lists.Add(new ListEntity { Id = "L2", Name = "Beta", WorkingDir = @"C:\repos\beta", CreatedAt = DateTime.UtcNow, SortOrder = 0 });
db.Lists.Add(new ListEntity { Id = "L3", Name = "NoDir", CreatedAt = DateTime.UtcNow, SortOrder = 2 });
await db.SaveChangesAsync();
}
using var vm = BuildVm(new FakeWorker());
var browsed = false;
vm.BrowseForSessionRequested = () => browsed = true;
await vm.RefreshSessionTargetsAsync();
// Browse first (survives a failing list query), then the lists in sort order.
Assert.Equal(3, vm.SessionTargets.Count);
Assert.Equal(new[] { "Beta", "Alpha" }, vm.SessionTargets.Skip(1).Select(t => t.Label).ToArray());
vm.SessionTargets[0].StartCommand.Execute(null);
Assert.True(browsed);
}
private async Task SeedQueueAsync()
{
await using var db = NewContext();