From 9effddeb2c57a335523876f9c6749889c0cfac7f Mon Sep 17 00:00:00 2001 From: Mika Kuns Date: Fri, 26 Jun 2026 09:22:53 +0200 Subject: [PATCH] feat(ui): worker client surface for in-app interactive sessions Adds SendInteractiveMessageAsync/StopInteractiveSessionAsync and the InteractiveSessionStarted/Ended events to IWorkerClient + WorkerClient (UI-thread dispatch mirroring TaskQuestionAsked). Updates the IWorkerClient fakes in both test projects. --- .../Services/Interfaces/IWorkerClient.cs | 5 ++++ src/ClaudeDo.Ui/Services/WorkerClient.cs | 24 +++++++++++++++++++ tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs | 4 ++++ .../UiVm/TasksIslandViewModelPlanningTests.cs | 4 ++++ 4 files changed, 37 insertions(+) diff --git a/src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs b/src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs index d438c03..d33626f 100644 --- a/src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs +++ b/src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs @@ -25,6 +25,9 @@ public interface IWorkerClient : INotifyPropertyChanged /// A pending question was answered, timed out, or the run ended: (taskId, questionId). event Action? TaskQuestionResolvedEvent; + event Action? InteractiveSessionStartedEvent; + event Action? InteractiveSessionEndedEvent; + event Action? PrepStartedEvent; event Action? PrepLineEvent; event Action? PrepFinishedEvent; @@ -46,6 +49,8 @@ public interface IWorkerClient : INotifyPropertyChanged Task ContinueTaskAsync(string taskId, string followUpPrompt); /// Answer a question a running task raised via AskUser. Task AnswerTaskQuestionAsync(string taskId, string questionId, string answer); + Task SendInteractiveMessageAsync(string taskId, string text); + Task StopInteractiveSessionAsync(string taskId); /// The question a running task is currently blocked on, if any (for re-attach). Task GetPendingQuestionAsync(string taskId); Task ResetTaskAsync(string taskId); diff --git a/src/ClaudeDo.Ui/Services/WorkerClient.cs b/src/ClaudeDo.Ui/Services/WorkerClient.cs index dfe0355..02197d9 100644 --- a/src/ClaudeDo.Ui/Services/WorkerClient.cs +++ b/src/ClaudeDo.Ui/Services/WorkerClient.cs @@ -49,6 +49,8 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC public event Action? TaskUpdatedEvent; public event Action? TaskQuestionAskedEvent; public event Action? TaskQuestionResolvedEvent; + public event Action? InteractiveSessionStartedEvent; + public event Action? InteractiveSessionEndedEvent; public event Action? ConnectionRestoredEvent; public event Action? WorktreeUpdatedEvent; public event Action? ListUpdatedEvent; @@ -148,6 +150,16 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC Dispatcher.UIThread.Post(() => TaskQuestionResolvedEvent?.Invoke(taskId, questionId)); }); + _hub.On("InteractiveSessionStarted", taskId => + { + Dispatcher.UIThread.Post(() => InteractiveSessionStartedEvent?.Invoke(taskId)); + }); + + _hub.On("InteractiveSessionEnded", taskId => + { + Dispatcher.UIThread.Post(() => InteractiveSessionEndedEvent?.Invoke(taskId)); + }); + _hub.On("WorktreeUpdated", taskId => { Dispatcher.UIThread.Post(() => WorktreeUpdatedEvent?.Invoke(taskId)); @@ -279,6 +291,18 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC catch { /* offline or already resolved — the UI clears optimistically */ } } + public async Task SendInteractiveMessageAsync(string taskId, string text) + { + try { await _hub.InvokeAsync("SendInteractiveMessage", taskId, text); } + catch { /* offline or session already ended */ } + } + + public async Task StopInteractiveSessionAsync(string taskId) + { + try { await _hub.InvokeAsync("StopInteractiveSession", taskId); } + catch { /* offline */ } + } + public Task GetPendingQuestionAsync(string taskId) => TryInvokeAsync("GetPendingQuestion", taskId); diff --git a/tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs b/tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs index 52d18b5..0157c3f 100644 --- a/tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs +++ b/tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs @@ -25,6 +25,8 @@ public abstract class StubWorkerClient : IWorkerClient public event Action? WorkerLogReceivedEvent; public event Action? TaskQuestionAskedEvent; public event Action? TaskQuestionResolvedEvent; + public event Action? InteractiveSessionStartedEvent; + public event Action? InteractiveSessionEndedEvent; public event Action? PrepStartedEvent; public event Action? PrepLineEvent; public event Action? PrepFinishedEvent; @@ -133,6 +135,8 @@ public abstract class StubWorkerClient : IWorkerClient public virtual Task SetOnlineInboxConfigAsync(OnlineInboxConfigInputDto input) => Task.CompletedTask; public virtual Task SetOnlineInboxAuthAsync(string refreshToken) => Task.CompletedTask; public virtual Task ClearOnlineInboxAuthAsync() => Task.CompletedTask; + public virtual Task SendInteractiveMessageAsync(string taskId, string text) => Task.CompletedTask; + public virtual Task StopInteractiveSessionAsync(string taskId) => Task.CompletedTask; protected void RaisePropertyChanged(string name) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); } diff --git a/tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs b/tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs index 1d544ed..7b5afff 100644 --- a/tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs +++ b/tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs @@ -36,6 +36,8 @@ sealed class FakeWorkerClient : IWorkerClient public event Action? WorkerLogReceivedEvent; public event Action? TaskQuestionAskedEvent; public event Action? TaskQuestionResolvedEvent; + public event Action? InteractiveSessionStartedEvent; + public event Action? InteractiveSessionEndedEvent; public void RaiseTaskUpdated(string taskId) => TaskUpdatedEvent?.Invoke(taskId); public void RaiseWorktreeUpdated(string taskId) => WorktreeUpdatedEvent?.Invoke(taskId); public void RaiseTaskMessage(string taskId, string line) => TaskMessageEvent?.Invoke(taskId, line); @@ -122,6 +124,8 @@ sealed class FakeWorkerClient : IWorkerClient public Task SetOnlineInboxConfigAsync(OnlineInboxConfigInputDto input) => Task.CompletedTask; public Task SetOnlineInboxAuthAsync(string refreshToken) => Task.CompletedTask; public Task ClearOnlineInboxAuthAsync() => Task.CompletedTask; + public Task SendInteractiveMessageAsync(string taskId, string text) => Task.CompletedTask; + public Task StopInteractiveSessionAsync(string taskId) => Task.CompletedTask; public IReadOnlyList GetActiveTasks() => System.Array.Empty(); }