diff --git a/src/ClaudeDo.Localization/locales/de.json b/src/ClaudeDo.Localization/locales/de.json index ee2a7e19..f41e2bb4 100644 --- a/src/ClaudeDo.Localization/locales/de.json +++ b/src/ClaudeDo.Localization/locales/de.json @@ -268,6 +268,7 @@ "overviewMode": "Übersicht", "closeSession": "Sitzung schließen", "conptyLaunchFailed": "ConPTY-Sitzung konnte nicht geöffnet werden: {0}", + "mergeHelperTitle": "Merge-Helfer", "submitForReviewFailed": "Einreichen zum Review fehlgeschlagen: {0}", "submitForReview": "Zum Review einreichen", "submitForReviewTip": "Diesen Worktree committen und den Task ins Review bringen, damit er gemergt werden kann", diff --git a/src/ClaudeDo.Localization/locales/en.json b/src/ClaudeDo.Localization/locales/en.json index 72cdde68..a5baca26 100644 --- a/src/ClaudeDo.Localization/locales/en.json +++ b/src/ClaudeDo.Localization/locales/en.json @@ -268,6 +268,7 @@ "overviewMode": "Overview", "closeSession": "Close session", "conptyLaunchFailed": "Couldn't open ConPTY session: {0}", + "mergeHelperTitle": "Merge Helper", "submitForReviewFailed": "Couldn't submit for review: {0}", "submitForReview": "Submit for review", "submitForReviewTip": "Commit this worktree and move the task to review so it can be merged", diff --git a/src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs b/src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs index 1d531061..7ba0ed6b 100644 --- a/src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs @@ -238,6 +238,11 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable OpenMissionControl(); _ = MissionControl.OpenPlanningConPtySessionAsync(taskId, resume); }; + Lists.LetClaudeHandleRequested += req => + { + OpenMissionControl(); + _ = MissionControl.OpenMergeHelperConPtySessionAsync(req.ListId, req.TaskIds); + }; Tasks.TasksChanged += (_, _) => _ = Lists.RefreshCountsAsync(); Tasks.OpenListSettingsRequested += (_, _) => { diff --git a/src/ClaudeDo.Ui/ViewModels/MissionControlViewModel.cs b/src/ClaudeDo.Ui/ViewModels/MissionControlViewModel.cs index 6ed5ee7d..ce73af7e 100644 --- a/src/ClaudeDo.Ui/ViewModels/MissionControlViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/MissionControlViewModel.cs @@ -322,6 +322,40 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable } } + // Merge-helper session over a hand-picked set of tasks ("Let Claude handle it"). + // Ad-hoc style: no owning task, never deduped — every run opens a fresh pane. + public async System.Threading.Tasks.Task OpenMergeHelperConPtySessionAsync(string? listId, IReadOnlyList taskIds) + { + if (taskIds is not { Count: > 0 }) return; + + var title = Loc.T("missionControl.mergeHelperTitle"); + if (listId is not null) + { + try + { + await using var ctx = await _dbFactory.CreateDbContextAsync(); + var list = await ctx.Lists.AsNoTracking().FirstOrDefaultAsync(l => l.Id == listId); + if (list?.Name is { Length: > 0 } name) title = $"{title} — {name}"; + } + catch { /* best-effort title lookup */ } + } + + try + { + var spec = await _worker.GetMergeHelperLaunchSpecAsync(taskIds, listId); + var descriptor = new TerminalLaunchDescriptor(spec.Cwd, spec.Exe, spec.Args, spec.Env); + var pane = ConPtyPaneViewModel.CreateAdHoc(title, descriptor); + pane.ErrorReported += OnConPtyPaneError; + pane.CloseRequested += CloseConPtySession; + pane.SubmitForReviewRequested += OnPaneSubmitForReview; + ConPtySessions.Add(pane); + } + catch (Exception ex) + { + ErrorReported?.Invoke(Loc.T("missionControl.conptyLaunchFailed", ex.Message)); + } + } + private void OnConPtyPaneError(string message) => ErrorReported?.Invoke(message); // Submit a task's hand-driven ConPTY work for review, then close the pane (the interactive diff --git a/tests/ClaudeDo.Ui.Tests/ViewModels/MissionControlViewModelTests.cs b/tests/ClaudeDo.Ui.Tests/ViewModels/MissionControlViewModelTests.cs index 4268ce92..acd9e881 100644 --- a/tests/ClaudeDo.Ui.Tests/ViewModels/MissionControlViewModelTests.cs +++ b/tests/ClaudeDo.Ui.Tests/ViewModels/MissionControlViewModelTests.cs @@ -406,6 +406,65 @@ public class MissionControlViewModelTests : IDisposable Assert.Empty(vm.Panes); } + private sealed class ThrowingMergeHelperLaunchSpecWorker : StubWorkerClient + { + public override Task GetMergeHelperLaunchSpecAsync(IReadOnlyList taskIds, string? listId, CancellationToken ct = default) + => throw new InvalidOperationException("spec failed"); + } + + [Fact] + public async Task OpenMergeHelperConPtySessionAsync_AddsAdHocPane() + { + var worker = new FakeWorker(); + using var vm = BuildVm(worker); + + await vm.OpenMergeHelperConPtySessionAsync(null, new[] { "t1", "t2" }); + + Assert.Single(vm.ConPtySessions); + Assert.Null(vm.ConPtySessions[0].TaskId); + Assert.Single(vm.Panes); + Assert.Same(vm.ConPtySessions[0], vm.Panes[0]); + } + + [Fact] + public async Task OpenMergeHelperConPtySessionAsync_TwoRuns_NeverDeduped() + { + var worker = new FakeWorker(); + using var vm = BuildVm(worker); + + await vm.OpenMergeHelperConPtySessionAsync(null, new[] { "t1" }); + await vm.OpenMergeHelperConPtySessionAsync(null, new[] { "t1" }); + + Assert.Equal(2, vm.ConPtySessions.Count); + } + + [Fact] + public async Task OpenMergeHelperConPtySessionAsync_EmptySelection_NoPane() + { + var worker = new FakeWorker(); + using var vm = BuildVm(worker); + + await vm.OpenMergeHelperConPtySessionAsync(null, Array.Empty()); + + Assert.Empty(vm.ConPtySessions); + Assert.Empty(vm.Panes); + } + + [Fact] + public async Task OpenMergeHelperConPtySessionAsync_WorkerThrows_RaisesErrorReported_NoPaneAdded() + { + var worker = new ThrowingMergeHelperLaunchSpecWorker(); + using var vm = BuildVm(worker); + string? error = null; + vm.ErrorReported += msg => error = msg; + + await vm.OpenMergeHelperConPtySessionAsync(null, new[] { "t1" }); + + Assert.Empty(vm.ConPtySessions); + Assert.Empty(vm.Panes); + Assert.NotNull(error); + } + [Fact] public void ToggleLayoutCommand_FlipsIsFocusMode() {