fix(ui): close the outgoing ConPTY pane on merge-helper handoff

Each handoff tile is a live claude process with the full mcp__claudedo__* surface; leaving the
outgoing phase's pane open leaked one process per phase. Close it via CloseConPtySession before
opening the next phase's tile, restoring the one-pane-per-TaskId invariant so
OpenConPtySessionAsync's dedupe and OnPaneSubmitForReview can go back to FirstOrDefault.
This commit is contained in:
mika kuns
2026-08-11 16:55:25 +02:00
parent 79b35801ae
commit e343baf21a
3 changed files with 73 additions and 14 deletions
@@ -608,6 +608,53 @@ public class MissionControlViewModelTests : IDisposable
Assert.NotNull(error);
}
[Fact]
public async Task OpenMergeHelperHandoffConPtySessionAsync_ExistingPaneForTaskId_ClosesOldPane_OpensNewFocusedPane()
{
var worker = new FakeWorker();
using var vm = BuildVm(worker);
await vm.OpenMergeHelperConPtySessionAsync("L1", new[] { "t1" });
var outgoingPane = Assert.Single(vm.ConPtySessions);
var taskId = outgoingPane.TaskId!;
await vm.OpenMergeHelperHandoffConPtySessionAsync(taskId, new[] { "t1" }, "wait");
var survivingPane = Assert.Single(vm.ConPtySessions);
Assert.Equal(taskId, survivingPane.TaskId);
Assert.NotSame(outgoingPane, survivingPane);
Assert.Same(survivingPane, vm.FocusedPane);
Assert.Single(vm.Panes);
Assert.Same(survivingPane, vm.Panes[0]);
// The outgoing pane went through CloseConPtySession (not just spliced out of the
// collection) -- it unsubscribed its event handlers, same signal the
// CloseConPtySession_UnsubscribesSubmitForReviewRequested test relies on.
Assert.Equal(0, SubscriberCount(outgoingPane, "SubmitForReviewRequested"));
}
[Fact]
public async Task OpenMergeHelperHandoffConPtySessionAsync_SubmitForReview_ResolvesToSurvivingPane()
{
var worker = new BlockingSubmitWorker();
using var vm = BuildVm(worker);
await vm.OpenMergeHelperConPtySessionAsync("L1", new[] { "t1" });
var outgoingPane = Assert.Single(vm.ConPtySessions);
var taskId = outgoingPane.TaskId!;
await vm.OpenMergeHelperHandoffConPtySessionAsync(taskId, new[] { "t1" }, "wait");
var survivingPane = Assert.Single(vm.ConPtySessions);
survivingPane.Terminal.IsRunning = true; // simulate a live hand-driven session
survivingPane.SubmitForReviewCommand.Execute(null);
Assert.Equal(1, worker.CallCount);
Assert.True(survivingPane.IsSubmitPending);
worker.Gate.SetResult(null);
await Task.Delay(20);
Assert.Empty(vm.ConPtySessions);
}
private sealed class BlockingSubmitWorker : StubWorkerClient
{
public int CallCount { get; private set; }