diff --git a/src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs b/src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs index 3e0f8ebf..9b78e4bc 100644 --- a/src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs +++ b/src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs @@ -85,6 +85,9 @@ public interface IWorkerClient : INotifyPropertyChanged /// Launch spec for an ad-hoc interactive session in an arbitrary directory -- /// no task, no worktree. Task GetAdHocLaunchSpecAsync(string directory, CancellationToken ct = default); + /// Launch spec for an embedded ConPTY "merge helper" session that drives the given + /// tasks to a merged/Done state. listId scopes the session to that list; null = all lists. + Task GetMergeHelperLaunchSpecAsync(IReadOnlyList taskIds, string? listId, CancellationToken ct = default); /// Starts a planning session and returns the launch spec for an embedded ConPTY /// planning terminal (replaces StartPlanningSessionAsync's external wt window). Task GetPlanningStartLaunchSpecAsync(string taskId, CancellationToken ct = default); diff --git a/src/ClaudeDo.Ui/Services/WorkerClient.cs b/src/ClaudeDo.Ui/Services/WorkerClient.cs index 5ce7adf6..16a1c971 100644 --- a/src/ClaudeDo.Ui/Services/WorkerClient.cs +++ b/src/ClaudeDo.Ui/Services/WorkerClient.cs @@ -522,6 +522,9 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC public async Task GetAdHocLaunchSpecAsync(string directory, CancellationToken ct = default) => await _hub.InvokeAsync("GetAdHocLaunchSpec", directory, ct); + public async Task GetMergeHelperLaunchSpecAsync(IReadOnlyList taskIds, string? listId, CancellationToken ct = default) + => await _hub.InvokeAsync("GetMergeHelperLaunchSpec", taskIds, listId, ct); + public async Task GetPlanningStartLaunchSpecAsync(string taskId, CancellationToken ct = default) => await _hub.InvokeAsync("GetPlanningStartLaunchSpec", taskId, ct); diff --git a/src/ClaudeDo.Worker/Hub/WorkerHub.cs b/src/ClaudeDo.Worker/Hub/WorkerHub.cs index 7b2731a5..93cfc43c 100644 --- a/src/ClaudeDo.Worker/Hub/WorkerHub.cs +++ b/src/ClaudeDo.Worker/Hub/WorkerHub.cs @@ -676,6 +676,16 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub return _interactiveLaunchSpec.BuildForDirectoryAsync(directory, Context.ConnectionAborted); }); + // Builds the launch spec for an embedded ConPTY "merge helper" session that drives the given + // tasks to a merged/Done state via the mcp__claudedo__* tools. listId scopes the brief label + // and cwd to that list; null means all lists. + public Task GetMergeHelperLaunchSpec(string[] taskIds, string? listId) => HubGuard(() => + { + if (_interactiveLaunchSpec is null) + throw new InvalidOperationException("Interactive launch spec service is not configured."); + return _interactiveLaunchSpec.BuildForMergeHelperAsync(taskIds, listId, Context.ConnectionAborted); + }); + // Starts a planning session (worktree + prompt files + token, task -> Planning) and returns // the launch spec for an embedded ConPTY planning terminal -- the ConPTY replacement for // StartPlanningSessionAsync's external wt window. On any spec-build failure the just-started diff --git a/tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs b/tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs index e13cafe8..7faea86c 100644 --- a/tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs +++ b/tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs @@ -100,6 +100,8 @@ public abstract class StubWorkerClient : IWorkerClient => Task.FromResult(new LaunchSpec(".", "claude", Array.Empty(), new Dictionary())); public virtual Task GetAdHocLaunchSpecAsync(string directory, CancellationToken ct = default) => Task.FromResult(new LaunchSpec(directory, "claude", Array.Empty(), new Dictionary())); + public virtual Task GetMergeHelperLaunchSpecAsync(IReadOnlyList taskIds, string? listId, CancellationToken ct = default) + => Task.FromResult(new LaunchSpec(".", "claude", Array.Empty(), new Dictionary())); public virtual Task GetPlanningStartLaunchSpecAsync(string taskId, CancellationToken ct = default) => Task.FromResult(new LaunchSpec(".", "claude", Array.Empty(), new Dictionary())); public virtual Task GetPlanningResumeLaunchSpecAsync(string taskId, CancellationToken ct = default) diff --git a/tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs b/tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs index 60cdd73f..1bc6598c 100644 --- a/tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs +++ b/tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs @@ -75,6 +75,8 @@ sealed class FakeWorkerClient : IWorkerClient public Task SubmitTaskForReviewAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask; public Task GetInteractiveLaunchSpecAsync(string taskId, CancellationToken ct = default) => Task.FromResult(new LaunchSpec(".", "claude", Array.Empty(), new Dictionary())); + public Task GetMergeHelperLaunchSpecAsync(IReadOnlyList taskIds, string? listId, CancellationToken ct = default) + => Task.FromResult(new LaunchSpec(".", "claude", Array.Empty(), new Dictionary())); public Task GetAdHocLaunchSpecAsync(string directory, CancellationToken ct = default) => Task.FromResult(new LaunchSpec(directory, "claude", Array.Empty(), new Dictionary())); public int PlanningStartSpecCalls { get; private set; }