diff --git a/src/ClaudeDo.Localization/locales/de.json b/src/ClaudeDo.Localization/locales/de.json
index d34c38f0..062ff962 100644
--- a/src/ClaudeDo.Localization/locales/de.json
+++ b/src/ClaudeDo.Localization/locales/de.json
@@ -277,6 +277,7 @@
"settings": "Einstellungen",
"queue": "Warteschlange",
"blocked": "Blockiert",
+ "running": "Läuft",
"focusMode": "Fokus",
"overviewMode": "Übersicht",
"closeSession": "Sitzung schließen",
diff --git a/src/ClaudeDo.Localization/locales/en.json b/src/ClaudeDo.Localization/locales/en.json
index 8bc0e488..df14b06a 100644
--- a/src/ClaudeDo.Localization/locales/en.json
+++ b/src/ClaudeDo.Localization/locales/en.json
@@ -277,6 +277,7 @@
"settings": "Settings",
"queue": "Queue",
"blocked": "Blocked",
+ "running": "Running",
"focusMode": "Focus",
"overviewMode": "Overview",
"closeSession": "Close session",
diff --git a/src/ClaudeDo.Ui/ViewModels/MissionControlViewModel.cs b/src/ClaudeDo.Ui/ViewModels/MissionControlViewModel.cs
index 650bf90b..d7a751dd 100644
--- a/src/ClaudeDo.Ui/ViewModels/MissionControlViewModel.cs
+++ b/src/ClaudeDo.Ui/ViewModels/MissionControlViewModel.cs
@@ -81,7 +81,7 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
ConPtySessions.CollectionChanged += OnConPtySessionsChanged;
Panes.CollectionChanged += OnPanesChanged;
- _onTaskStarted = (slot, taskId, startedAt) => { EnsureMonitor(taskId); _ = RefreshQueueAsync(); };
+ _onTaskStarted = (slot, taskId, startedAt) => { _ = RefreshQueueAsync(); };
_worker.TaskStartedEvent += _onTaskStarted;
_onTaskFinished = (slot, taskId, status, finishedAt) => _ = RefreshQueueAsync();
@@ -90,10 +90,9 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
_onTaskUpdated = taskId => _ = RefreshQueueAsync();
_worker.TaskUpdatedEvent += _onTaskUpdated;
- _onConnectionRestored = () => { SeedActive(); _ = RefreshQueueAsync(); };
+ _onConnectionRestored = () => { _ = RefreshQueueAsync(); };
_worker.ConnectionRestoredEvent += _onConnectionRestored;
- SeedActive();
_ = RefreshQueueAsync();
}
@@ -103,19 +102,26 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
{
await using var ctx = await _dbFactory.CreateDbContextAsync();
var rows = await ctx.Tasks.AsNoTracking()
- .Where(t => t.Status == ClaudeDo.Data.Models.TaskStatus.Queued)
- .OrderBy(t => t.SortOrder).ThenBy(t => t.CreatedAt)
- .Select(t => new { t.Id, t.Title, t.BlockedByTaskId })
+ .Where(t => t.Status == ClaudeDo.Data.Models.TaskStatus.Queued
+ || t.Status == ClaudeDo.Data.Models.TaskStatus.Running)
+ .OrderBy(t => t.Status == ClaudeDo.Data.Models.TaskStatus.Running ? 0 : 1)
+ .ThenBy(t => t.SortOrder).ThenBy(t => t.CreatedAt)
+ .Select(t => new { t.Id, t.Title, t.BlockedByTaskId, t.Status })
.ToListAsync();
Queued.Clear();
foreach (var r in rows)
+ {
+ var id = r.Id;
Queued.Add(new QueuedTaskViewModel
{
- Id = r.Id,
+ Id = id,
Title = r.Title ?? string.Empty,
IsBlocked = r.BlockedByTaskId != null,
+ IsRunning = r.Status == ClaudeDo.Data.Models.TaskStatus.Running,
+ OpenInAppCommand = new RelayCommand(() => _openInApp?.Invoke(id)),
});
+ }
OnPropertyChanged(nameof(HasQueued));
}
catch { /* best-effort queue refresh */ }
@@ -141,13 +147,15 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
await RefreshQueueAsync();
}
- private void SeedActive()
+ // Kept intentionally — unused since auto-seeding was disabled (2026-07-29).
+ internal void SeedActive()
{
foreach (var a in _worker.GetActiveTasks())
EnsureMonitor(a.TaskId);
}
- private void EnsureMonitor(string taskId)
+ // Kept intentionally — unused since auto-seeding was disabled (2026-07-29).
+ internal void EnsureMonitor(string taskId)
{
if (string.IsNullOrEmpty(taskId)) return;
if (Monitors.Any(m => m.SubscribedTaskId == taskId)) return;
@@ -428,10 +436,12 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
}
}
-/// Read-only display row for a queued task in the Mission Control side strip.
+/// Read-only display row for a queued or running task in the Mission Control side strip.
public sealed class QueuedTaskViewModel
{
public required string Id { get; init; }
public required string Title { get; init; }
public bool IsBlocked { get; init; }
+ public bool IsRunning { get; init; }
+ public IRelayCommand? OpenInAppCommand { get; init; }
}
diff --git a/src/ClaudeDo.Ui/Views/MissionControl/MissionControlView.axaml b/src/ClaudeDo.Ui/Views/MissionControl/MissionControlView.axaml
index 0292abeb..779f7dfe 100644
--- a/src/ClaudeDo.Ui/Views/MissionControl/MissionControlView.axaml
+++ b/src/ClaudeDo.Ui/Views/MissionControl/MissionControlView.axaml
@@ -73,21 +73,35 @@
-
-
-
-
-
-
+
diff --git a/tests/ClaudeDo.Ui.Tests/ViewModels/MissionControlViewModelTests.cs b/tests/ClaudeDo.Ui.Tests/ViewModels/MissionControlViewModelTests.cs
index e82a42d3..f4552136 100644
--- a/tests/ClaudeDo.Ui.Tests/ViewModels/MissionControlViewModelTests.cs
+++ b/tests/ClaudeDo.Ui.Tests/ViewModels/MissionControlViewModelTests.cs
@@ -49,8 +49,10 @@ public class MissionControlViewModelTests : IDisposable
private MissionControlViewModel BuildVm(StubWorkerClient worker)
=> new MissionControlViewModel(new TestDbFactory(NewContext), worker);
+ // ── acceptance criterion (a): TaskStarted must NOT add a pane ──────────────
+
[Fact]
- public void TwoStarts_CreateTwoMonitors_ColumnCountTwo()
+ public void TaskStarted_DoesNotAddPane()
{
var worker = new FakeWorker();
using var vm = BuildVm(worker);
@@ -58,18 +60,140 @@ public class MissionControlViewModelTests : IDisposable
worker.RaiseTaskStarted("slot-1", "t1", DateTime.UtcNow);
worker.RaiseTaskStarted("slot-2", "t2", DateTime.UtcNow);
+ Assert.Empty(vm.Monitors);
+ Assert.Empty(vm.Panes);
+ }
+
+ [Fact]
+ public void Construction_DoesNotSeedMonitorsEvenWithActiveTasks()
+ {
+ var worker = new SeededFakeWorker();
+ using var vm = BuildVm(worker);
+
+ Assert.Empty(vm.Monitors);
+ Assert.Empty(vm.Panes);
+ }
+
+ private sealed class SeededFakeWorker : StubWorkerClient
+ {
+ public override IReadOnlyList GetActiveTasks()
+ => new[] { new ActiveTask("slot-1", "seed1", DateTime.UtcNow) };
+ }
+
+ // ── acceptance criterion (b): RefreshQueueAsync returns Running rows first ─
+
+ [Fact]
+ public async Task RefreshQueueAsync_RunningFirst_WithIsRunningSet()
+ {
+ await using (var db = NewContext())
+ {
+ db.Lists.Add(new ListEntity { Id = "L1", Name = "Work", CreatedAt = DateTime.UtcNow });
+ // Running task has higher SortOrder — must still appear first
+ db.Tasks.Add(new TaskEntity { Id = "r1", ListId = "L1", Title = "running-task", Status = TaskStatus.Running, CreatedAt = DateTime.UtcNow, SortOrder = 10 });
+ db.Tasks.Add(new TaskEntity { Id = "q1", ListId = "L1", Title = "queued-task", Status = TaskStatus.Queued, CreatedAt = DateTime.UtcNow, SortOrder = 0 });
+ await db.SaveChangesAsync();
+ }
+
+ var worker = new FakeWorker();
+ using var vm = BuildVm(worker);
+
+ await vm.RefreshQueueAsync();
+
+ Assert.True(vm.HasQueued);
+ Assert.Equal(2, vm.Queued.Count);
+
+ Assert.Equal("r1", vm.Queued[0].Id);
+ Assert.True(vm.Queued[0].IsRunning);
+
+ Assert.Equal("q1", vm.Queued[1].Id);
+ Assert.False(vm.Queued[1].IsRunning);
+ }
+
+ [Fact]
+ public async Task HasQueued_TrueWhenOnlyRunningTasksExist()
+ {
+ await using (var db = NewContext())
+ {
+ db.Lists.Add(new ListEntity { Id = "L1", Name = "Work", CreatedAt = DateTime.UtcNow });
+ db.Tasks.Add(new TaskEntity { Id = "r1", ListId = "L1", Title = "running", Status = TaskStatus.Running, CreatedAt = DateTime.UtcNow, SortOrder = 0 });
+ await db.SaveChangesAsync();
+ }
+
+ var worker = new FakeWorker();
+ using var vm = BuildVm(worker);
+
+ await vm.RefreshQueueAsync();
+
+ Assert.True(vm.HasQueued);
+ }
+
+ // ── acceptance criterion (c): row click invokes OpenInApp ──────────────────
+
+ [Fact]
+ public async Task Row_Click_InvokesOpenInApp()
+ {
+ await using (var db = NewContext())
+ {
+ db.Lists.Add(new ListEntity { Id = "L1", Name = "Work", CreatedAt = DateTime.UtcNow });
+ db.Tasks.Add(new TaskEntity { Id = "q1", ListId = "L1", Title = "clickable", Status = TaskStatus.Queued, CreatedAt = DateTime.UtcNow, SortOrder = 0 });
+ await db.SaveChangesAsync();
+ }
+
+ var worker = new FakeWorker();
+ using var vm = BuildVm(worker);
+ string? opened = null;
+ vm.OpenInApp = id => opened = id;
+
+ await vm.RefreshQueueAsync();
+ vm.Queued[0].OpenInAppCommand!.Execute(null);
+
+ Assert.Equal("q1", opened);
+ }
+
+ [Fact]
+ public async Task Row_Click_RunningTask_InvokesOpenInApp()
+ {
+ await using (var db = NewContext())
+ {
+ db.Lists.Add(new ListEntity { Id = "L1", Name = "Work", CreatedAt = DateTime.UtcNow });
+ db.Tasks.Add(new TaskEntity { Id = "r1", ListId = "L1", Title = "running", Status = TaskStatus.Running, CreatedAt = DateTime.UtcNow, SortOrder = 0 });
+ await db.SaveChangesAsync();
+ }
+
+ var worker = new FakeWorker();
+ using var vm = BuildVm(worker);
+ string? opened = null;
+ vm.OpenInApp = id => opened = id;
+
+ await vm.RefreshQueueAsync();
+ vm.Queued[0].OpenInAppCommand!.Execute(null);
+
+ Assert.Equal("r1", opened);
+ }
+
+ // ── monitor machinery tests (EnsureMonitor still works when called directly) ─
+
+ [Fact]
+ public void EnsureMonitor_TwoCalls_CreateTwoMonitors_ColumnCountTwo()
+ {
+ var worker = new FakeWorker();
+ using var vm = BuildVm(worker);
+
+ vm.EnsureMonitor("t1");
+ vm.EnsureMonitor("t2");
+
Assert.Equal(2, vm.Monitors.Count);
Assert.Equal(2, vm.ColumnCount);
}
[Fact]
- public void DuplicateStart_DoesNotAddSecondMonitor()
+ public void EnsureMonitor_DuplicateCall_DoesNotAddSecondMonitor()
{
var worker = new FakeWorker();
using var vm = BuildVm(worker);
- worker.RaiseTaskStarted("slot-1", "t1", DateTime.UtcNow);
- worker.RaiseTaskStarted("slot-1", "t1", DateTime.UtcNow);
+ vm.EnsureMonitor("t1");
+ vm.EnsureMonitor("t1");
Assert.Equal(1, vm.Monitors.Count);
}
@@ -80,7 +204,7 @@ public class MissionControlViewModelTests : IDisposable
var worker = new FakeWorker();
using var vm = BuildVm(worker);
- worker.RaiseTaskStarted("slot-1", "t1", DateTime.UtcNow);
+ vm.EnsureMonitor("t1");
worker.RaiseTaskFinished("slot-1", "t1", "done", DateTime.UtcNow);
Assert.Equal(1, vm.Monitors.Count);
@@ -93,8 +217,8 @@ public class MissionControlViewModelTests : IDisposable
var worker = new FakeWorker();
using var vm = BuildVm(worker);
- worker.RaiseTaskStarted("slot-1", "t1", DateTime.UtcNow);
- worker.RaiseTaskStarted("slot-2", "t2", DateTime.UtcNow);
+ vm.EnsureMonitor("t1");
+ vm.EnsureMonitor("t2");
worker.RaiseTaskFinished("slot-1", "t1", "done", DateTime.UtcNow);
vm.ClearFinishedCommand.Execute(null);
@@ -104,22 +228,6 @@ public class MissionControlViewModelTests : IDisposable
Assert.Equal(1, vm.ColumnCount);
}
- [Fact]
- public void SeedsFromActiveTasksOnConstruction()
- {
- var worker = new SeededFakeWorker();
- using var vm = BuildVm(worker);
-
- Assert.Equal(1, vm.Monitors.Count);
- Assert.Equal("seed1", vm.Monitors[0].SubscribedTaskId);
- }
-
- private sealed class SeededFakeWorker : StubWorkerClient
- {
- public override IReadOnlyList GetActiveTasks()
- => new[] { new ActiveTask("slot-1", "seed1", DateTime.UtcNow) };
- }
-
[Fact]
public void OpenInApp_PropagatesToMonitors_AndCommandInvokesHook()
{
@@ -129,7 +237,7 @@ public class MissionControlViewModelTests : IDisposable
string? revealed = null;
vm.OpenInApp = id => revealed = id;
- worker.RaiseTaskStarted("slot-1", "t1", DateTime.UtcNow);
+ vm.EnsureMonitor("t1");
vm.Monitors[0].OpenInAppCommand.Execute(null);
@@ -145,7 +253,7 @@ public class MissionControlViewModelTests : IDisposable
Action? reDock = null;
vm.ShowDetached = (m, rd) => { detached = m; reDock = rd; };
- worker.RaiseTaskStarted("slot-1", "t1", DateTime.UtcNow);
+ vm.EnsureMonitor("t1");
var monitor = vm.Monitors[0];
monitor.DetachCommand.Execute(null);
@@ -162,7 +270,7 @@ public class MissionControlViewModelTests : IDisposable
{
var worker = new FakeWorker();
using var vm = BuildVm(worker);
- worker.RaiseTaskStarted("slot-1", "t1", DateTime.UtcNow);
+ vm.EnsureMonitor("t1");
worker.RaiseTaskFinished("slot-1", "t1", "waiting_for_review", DateTime.UtcNow);
Assert.True(vm.Monitors[0].IsWaitingForReview);
@@ -177,7 +285,7 @@ public class MissionControlViewModelTests : IDisposable
using var vm = BuildVm(worker);
Action? reDock = null;
vm.ShowDetached = (m, rd) => reDock = rd;
- worker.RaiseTaskStarted("slot-1", "t1", DateTime.UtcNow);
+ vm.EnsureMonitor("t1");
var monitor = vm.Monitors[0];
monitor.DetachCommand.Execute(null);
@@ -195,7 +303,7 @@ public class MissionControlViewModelTests : IDisposable
var worker = new FakeWorker();
using var vm = BuildVm(worker);
vm.ShowDetached = (m, rd) => { };
- worker.RaiseTaskStarted("slot-1", "t1", DateTime.UtcNow);
+ vm.EnsureMonitor("t1");
var monitor = vm.Monitors[0];
var closeRequested = false;
@@ -210,9 +318,9 @@ public class MissionControlViewModelTests : IDisposable
{
var worker = new FakeWorker();
using var vm = BuildVm(worker);
- worker.RaiseTaskStarted("s1", "t1", DateTime.UtcNow);
- worker.RaiseTaskStarted("s2", "t2", DateTime.UtcNow);
- worker.RaiseTaskStarted("s3", "t3", DateTime.UtcNow);
+ vm.EnsureMonitor("t1");
+ vm.EnsureMonitor("t2");
+ vm.EnsureMonitor("t3");
vm.MoveMonitor(vm.Monitors[0], vm.Monitors[2]); // move t1 to t3's slot
Assert.Equal(new[] { "t2", "t3", "t1" }, vm.Monitors.Select(m => m.SubscribedTaskId).ToArray());
@@ -320,7 +428,7 @@ public class MissionControlViewModelTests : IDisposable
var worker = new FakeWorker();
using var vm = BuildVm(worker);
- worker.RaiseTaskStarted("slot-1", "t1", DateTime.UtcNow);
+ vm.EnsureMonitor("t1");
await vm.OpenConPtySessionAsync("t2");
Assert.Equal(2, vm.Panes.Count);