feat(mission-control): give the list handler its own review task

"Let Claude handle it" now creates one ClaudeDo task per run to host the
ConPTY session (Idle/IsManual, never queued) instead of an untracked
ad-hoc tile, so the run has a real title, diff, and review outcome.
Since the handler merges its own changes straight into the list's
working dir, the task never gets a WorktreeEntity; its review range
lives as new HandlerBaseCommit/HandlerHeadCommit columns on TaskEntity
instead, reusing the existing commit-range diff machinery and keeping
it out of the worktrees overview entirely.
This commit is contained in:
mika kuns
2026-08-05 09:16:34 +02:00
parent 63d8b5c28d
commit c07c1f70a8
28 changed files with 1590 additions and 43 deletions
@@ -523,23 +523,47 @@ public class MissionControlViewModelTests : IDisposable
=> throw new InvalidOperationException("spec failed");
}
private sealed class ThrowingCreateMergeHelperTaskWorker : StubWorkerClient
{
public override Task<string> CreateMergeHelperTaskAsync(
IReadOnlyList<string> taskIds, string listId, string title, string descriptionHeader, CancellationToken ct = default)
=> throw new InvalidOperationException("create failed");
}
private sealed class FixedTaskIdMergeHelperWorker : StubWorkerClient
{
public string TaskId { get; } = "fixed-handler-task";
public int CreateCallCount { get; private set; }
public override Task<string> CreateMergeHelperTaskAsync(
IReadOnlyList<string> taskIds, string listId, string title, string descriptionHeader, CancellationToken ct = default)
{
CreateCallCount++;
return Task.FromResult(TaskId);
}
}
[Fact]
public async Task OpenMergeHelperConPtySessionAsync_AddsAdHocPane()
public async Task OpenMergeHelperConPtySessionAsync_AddsTaskBasedPane()
{
var worker = new FakeWorker();
using var vm = BuildVm(worker);
await vm.OpenMergeHelperConPtySessionAsync("L1", new[] { "t1", "t2" });
Assert.Single(vm.ConPtySessions);
Assert.Null(vm.ConPtySessions[0].TaskId);
var pane = Assert.Single(vm.ConPtySessions);
Assert.NotNull(pane.TaskId);
Assert.True(pane.IsTaskBased);
Assert.Single(vm.Panes);
Assert.Same(vm.ConPtySessions[0], vm.Panes[0]);
}
[Fact]
public async Task OpenMergeHelperConPtySessionAsync_TwoRuns_NeverDeduped()
public async Task OpenMergeHelperConPtySessionAsync_TwoRuns_EachGetsItsOwnTaskAndPane()
{
// Each run creates a brand-new handler task (the stub returns a fresh Guid per call),
// so two runs still yield two distinct, non-deduped panes -- but for a different reason
// than the old ad-hoc pane (never deduped by construction): dedup is now by TaskId, and
// there simply is no shared TaskId across separate runs.
var worker = new FakeWorker();
using var vm = BuildVm(worker);
@@ -547,6 +571,22 @@ public class MissionControlViewModelTests : IDisposable
await vm.OpenMergeHelperConPtySessionAsync("L1", new[] { "t1" });
Assert.Equal(2, vm.ConPtySessions.Count);
Assert.NotEqual(vm.ConPtySessions[0].TaskId, vm.ConPtySessions[1].TaskId);
}
[Fact]
public async Task OpenMergeHelperConPtySessionAsync_SameHandlerTaskId_FocusesExistingPane_NoDuplicate()
{
var worker = new FixedTaskIdMergeHelperWorker();
using var vm = BuildVm(worker);
await vm.OpenMergeHelperConPtySessionAsync("L1", new[] { "t1" });
var firstPane = Assert.Single(vm.ConPtySessions);
await vm.OpenMergeHelperConPtySessionAsync("L1", new[] { "t1" });
Assert.Single(vm.ConPtySessions);
Assert.Same(firstPane, vm.FocusedPane);
}
[Fact]
@@ -577,6 +617,21 @@ public class MissionControlViewModelTests : IDisposable
Assert.NotNull(error);
}
[Fact]
public async Task OpenMergeHelperConPtySessionAsync_TaskCreationThrows_RaisesErrorReported_NoPaneCreated()
{
var worker = new ThrowingCreateMergeHelperTaskWorker();
using var vm = BuildVm(worker);
string? error = null;
vm.ErrorReported += msg => error = msg;
await vm.OpenMergeHelperConPtySessionAsync("L1", new[] { "t1" });
Assert.Empty(vm.ConPtySessions);
Assert.Empty(vm.Panes);
Assert.NotNull(error);
}
[Fact]
public void ToggleLayoutCommand_FlipsIsFocusMode()
{