refactor(interactive): remove streaming interactive stack (superseded by ConPTY)

The embedded ConPTY terminal replaced the in-app streaming interactive session, so
delete the dead stack: StreamingClaudeSession, InteractiveSessionService,
ProcessClaudeStreamTransport, IClaudeStreamTransport, ILiveSession, LiveSessionRegistry,
IdleSessionReaper (+ WorkerConfig.InteractiveIdleTimeoutMinutes), the WorkerHub
interactive methods + HubBroadcaster events, IWorkerClient interactive members, the
TaskMonitorViewModel composer + SessionTerminalView composer markup, and the old
'Run interactively' entry. AskUser/PendingQuestionRegistry, the autonomous path,
planning, ResumeTaskInTerminal, and all ConPTY code are kept. Localization pruned.
This commit is contained in:
mika kuns
2026-07-23 16:47:16 +02:00
parent d8194ad57e
commit c412a84fdf
40 changed files with 23 additions and 2338 deletions
@@ -25,10 +25,6 @@ public abstract class StubWorkerClient : IWorkerClient
public event Action<WorkerLogEntry>? WorkerLogReceivedEvent;
public event Action<string, string, string>? TaskQuestionAskedEvent;
public event Action<string, string>? TaskQuestionResolvedEvent;
public event Action<string>? InteractiveSessionStartedEvent;
public event Action<string>? InteractiveSessionEndedEvent;
public event Action<string, IReadOnlyList<string>>? InteractiveQueueChangedEvent;
public event Action<string, string>? InteractiveMessageSentEvent;
public event Action? PrepStartedEvent;
public event Action<string>? PrepLineEvent;
public event Action<bool>? PrepFinishedEvent;
@@ -59,11 +55,6 @@ public abstract class StubWorkerClient : IWorkerClient
public void RaisePrepLine(string line) => PrepLineEvent?.Invoke(line);
public void RaisePrepFinished(bool ok) => PrepFinishedEvent?.Invoke(ok);
public void RaiseInteractiveStarted(string taskId) => InteractiveSessionStartedEvent?.Invoke(taskId);
public void RaiseInteractiveEnded(string taskId) => InteractiveSessionEndedEvent?.Invoke(taskId);
public void RaiseInteractiveQueueChanged(string taskId, IReadOnlyList<string> pending) => InteractiveQueueChangedEvent?.Invoke(taskId, pending);
public void RaiseInteractiveMessageSent(string taskId, string text) => InteractiveMessageSentEvent?.Invoke(taskId, text);
public virtual bool IsConnected => false;
public virtual bool IsReconnecting => false;
public virtual string? LastApproveTarget => null;
@@ -103,7 +94,6 @@ public abstract class StubWorkerClient : IWorkerClient
public virtual Task<MergeResultDto> ContinueConflictMergeAsync(string taskId) => Task.FromResult(new MergeResultDto("merged", System.Array.Empty<string>(), null));
public virtual Task AbortConflictMergeAsync(string taskId) => Task.CompletedTask;
public virtual Task StartPlanningSessionAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
public virtual Task OpenInteractiveTerminalAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
public virtual Task ResumeTaskInTerminalAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
public virtual Task<LaunchSpec> GetInteractiveLaunchSpecAsync(string taskId, CancellationToken ct = default)
=> Task.FromResult(new LaunchSpec(".", "claude", Array.Empty<string>(), new Dictionary<string, string>()));
@@ -151,30 +141,6 @@ 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 List<(string TaskId, string Text)> SentInteractive { get; } = new();
public virtual Task SendInteractiveMessageAsync(string taskId, string text)
{
SentInteractive.Add((taskId, text));
return Task.CompletedTask;
}
public List<(string TaskId, string Text)> RemovedQueued { get; } = new();
public virtual Task RemoveQueuedInteractiveMessageAsync(string taskId, string text)
{
RemovedQueued.Add((taskId, text));
return Task.CompletedTask;
}
public List<string> StoppedInteractive { get; } = new();
public virtual Task StopInteractiveSessionAsync(string taskId)
{
StoppedInteractive.Add(taskId);
return Task.CompletedTask;
}
public List<string> InterruptedInteractive { get; } = new();
public virtual Task InterruptInteractiveSessionAsync(string taskId)
{
InterruptedInteractive.Add(taskId);
return Task.CompletedTask;
}
protected void RaisePropertyChanged(string name) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
@@ -189,224 +189,4 @@ public class TaskMonitorViewModelTests : IDisposable
Assert.False(vm.HasPendingQuestion);
}
// ── Interactive composer ──────────────────────────────────────────────────
[Fact]
public void InteractiveStarted_ForSubscribedTask_SetsIsInteractiveLive()
{
var worker = new FakeWorker();
using var vm = Build(worker);
vm.SetTaskId("t1");
worker.RaiseInteractiveStarted("t1");
Assert.True(vm.IsInteractiveLive);
Assert.Equal("running", vm.AgentState);
}
[Fact]
public void InteractiveStarted_ForOtherTask_IsIgnored()
{
var worker = new FakeWorker();
using var vm = Build(worker);
vm.SetTaskId("t1");
worker.RaiseInteractiveStarted("other");
Assert.False(vm.IsInteractiveLive);
}
[Fact]
public void InteractiveEnded_ForSubscribedTask_ClearsIsInteractiveLive()
{
var worker = new FakeWorker();
using var vm = Build(worker);
vm.SetTaskId("t1");
worker.RaiseInteractiveStarted("t1");
worker.RaiseInteractiveEnded("t1");
Assert.False(vm.IsInteractiveLive);
Assert.Equal("done", vm.AgentState);
}
[Fact]
public void InteractiveEnded_ForOtherTask_IsIgnored()
{
var worker = new FakeWorker();
using var vm = Build(worker);
vm.SetTaskId("t1");
worker.RaiseInteractiveStarted("t1");
worker.RaiseInteractiveEnded("other");
Assert.True(vm.IsInteractiveLive); // unchanged
}
[Fact]
public void SubmitComposerCommand_CanExecute_FalseWhenNotLive()
{
var worker = new FakeWorker();
using var vm = Build(worker);
vm.SetTaskId("t1");
vm.ComposerDraft = "hello";
Assert.False(vm.SubmitComposerCommand.CanExecute(null));
}
[Fact]
public void SubmitComposerCommand_CanExecute_FalseWhenLiveButDraftWhitespace()
{
var worker = new FakeWorker();
using var vm = Build(worker);
vm.SetTaskId("t1");
worker.RaiseInteractiveStarted("t1");
vm.ComposerDraft = " ";
Assert.False(vm.SubmitComposerCommand.CanExecute(null));
}
[Fact]
public void SubmitComposerCommand_CanExecute_TrueWhenLiveAndDraftSet()
{
var worker = new FakeWorker();
using var vm = Build(worker);
vm.SetTaskId("t1");
worker.RaiseInteractiveStarted("t1");
vm.ComposerDraft = "hello";
Assert.True(vm.SubmitComposerCommand.CanExecute(null));
}
[Fact]
public async Task SubmitComposer_CallsClient_ClearsDraft_DoesNotAddLogLine()
{
var worker = new FakeWorker();
using var vm = Build(worker);
vm.SetTaskId("t1");
worker.RaiseInteractiveStarted("t1");
vm.ComposerDraft = "do the thing";
await vm.SubmitComposerCommand.ExecuteAsync(null);
Assert.Single(worker.SentInteractive);
Assert.Equal(("t1", "do the thing"), worker.SentInteractive[0]);
Assert.Equal(string.Empty, vm.ComposerDraft);
// Log must NOT be updated by submit itself; it updates on InteractiveMessageSent
Assert.Empty(vm.Log);
}
[Fact]
public void InteractiveMessageSent_ForSubscribedTask_AddsUserLogLine()
{
var worker = new FakeWorker();
using var vm = Build(worker);
vm.SetTaskId("t1");
worker.RaiseInteractiveMessageSent("t1", "hello from event");
Assert.Single(vm.Log);
Assert.Equal(LogKind.User, vm.Log[0].Kind);
Assert.Equal("hello from event", vm.Log[0].Text);
}
[Fact]
public void InteractiveMessageSent_ForOtherTask_IsIgnored()
{
var worker = new FakeWorker();
using var vm = Build(worker);
vm.SetTaskId("t1");
worker.RaiseInteractiveMessageSent("other", "not mine");
Assert.Empty(vm.Log);
}
[Fact]
public void InteractiveQueueChanged_ForSubscribedTask_PopulatesQueue()
{
var worker = new FakeWorker();
using var vm = Build(worker);
vm.SetTaskId("t1");
worker.RaiseInteractiveQueueChanged("t1", new[] { "msg1", "msg2" });
Assert.Equal(2, vm.QueuedMessages.Count);
Assert.Equal("msg1", vm.QueuedMessages[0].Text);
Assert.Equal("msg2", vm.QueuedMessages[1].Text);
Assert.True(vm.HasQueuedMessages);
}
[Fact]
public void InteractiveQueueChanged_EmptyList_ClearsQueue()
{
var worker = new FakeWorker();
using var vm = Build(worker);
vm.SetTaskId("t1");
worker.RaiseInteractiveQueueChanged("t1", new[] { "msg1" });
worker.RaiseInteractiveQueueChanged("t1", Array.Empty<string>());
Assert.Empty(vm.QueuedMessages);
Assert.False(vm.HasQueuedMessages);
}
[Fact]
public void InteractiveQueueChanged_ForOtherTask_IsIgnored()
{
var worker = new FakeWorker();
using var vm = Build(worker);
vm.SetTaskId("t1");
worker.RaiseInteractiveQueueChanged("other", new[] { "msg1" });
Assert.Empty(vm.QueuedMessages);
Assert.False(vm.HasQueuedMessages);
}
[Fact]
public void InteractiveEnded_ClearsQueuedMessages()
{
var worker = new FakeWorker();
using var vm = Build(worker);
vm.SetTaskId("t1");
worker.RaiseInteractiveStarted("t1");
worker.RaiseInteractiveQueueChanged("t1", new[] { "pending msg" });
worker.RaiseInteractiveEnded("t1");
Assert.Empty(vm.QueuedMessages);
Assert.False(vm.HasQueuedMessages);
}
[Fact]
public async Task QueuedMessageViewModel_RemoveCommand_RecordsRemoveCall()
{
var worker = new FakeWorker();
using var vm = Build(worker);
vm.SetTaskId("t1");
worker.RaiseInteractiveQueueChanged("t1", new[] { "a", "b" });
vm.QueuedMessages[0].RemoveCommand.Execute(null);
// RemoveQueuedAsync is fire-and-forget; yield to let the async continuation run
await System.Threading.Tasks.Task.Yield();
Assert.Single(worker.RemovedQueued);
Assert.Equal(("t1", "a"), worker.RemovedQueued[0]);
}
[Fact]
public async Task InterruptInteractiveCommand_WhenLive_RecordsOneCall()
{
var worker = new FakeWorker();
using var vm = Build(worker);
vm.SetTaskId("t1");
worker.RaiseInteractiveStarted("t1");
await vm.InterruptInteractiveCommand.ExecuteAsync(null);
Assert.Single(worker.InterruptedInteractive);
Assert.Equal("t1", worker.InterruptedInteractive[0]);
}
}