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();
}