feat(ui): interactive chat composer state on the session monitor VM
TaskMonitorViewModel gains IsInteractiveLive + ComposerDraft + SubmitComposer (optimistic LogKind.User echo, then SendInteractiveMessageAsync) + StopInteractive, driven by the InteractiveSessionStarted/Ended events. Since DetailsIslandViewModel embeds this monitor, both task detail and Mission Control get the composer. Mission Control auto-creates a monitor on InteractiveSessionStarted. Adds LogKind.User.
This commit is contained in:
@@ -57,6 +57,9 @@ 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 virtual bool IsConnected => false;
|
||||
public virtual bool IsReconnecting => false;
|
||||
public virtual string? LastApproveTarget => null;
|
||||
@@ -135,8 +138,18 @@ 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;
|
||||
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> StoppedInteractive { get; } = new();
|
||||
public virtual Task StopInteractiveSessionAsync(string taskId)
|
||||
{
|
||||
StoppedInteractive.Add(taskId);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
protected void RaisePropertyChanged(string name) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||
}
|
||||
|
||||
@@ -189,4 +189,112 @@ 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_AddsUserLogLine_ClearsDraft_CallsClient()
|
||||
{
|
||||
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);
|
||||
Assert.Single(vm.Log);
|
||||
Assert.Equal(LogKind.User, vm.Log[0].Kind);
|
||||
Assert.Equal("do the thing", vm.Log[0].Text);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user