feat(ui): show queued interactive messages above the composer

A queued message now appears in a pending strip above the input box (driven by
InteractiveQueueChanged), not optimistically in the transcript. The transcript
user line is added on delivery via InteractiveMessageSent. SessionTerminalView
gains QueuedMessages/HasQueuedMessages styled props (Mission Control); WorkConsole
binds Monitor.* (task detail). Adds session.composer.queued (en/de).
This commit is contained in:
Mika Kuns
2026-06-26 11:04:25 +02:00
parent 84034e8395
commit 7c9ff18ced
8 changed files with 250 additions and 70 deletions

View File

@@ -280,7 +280,7 @@ public class TaskMonitorViewModelTests : IDisposable
}
[Fact]
public async Task SubmitComposer_AddsUserLogLine_ClearsDraft_CallsClient()
public async Task SubmitComposer_CallsClient_ClearsDraft_DoesNotAddLogLine()
{
var worker = new FakeWorker();
using var vm = Build(worker);
@@ -293,9 +293,89 @@ public class TaskMonitorViewModelTests : IDisposable
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("do the thing", vm.Log[0].Text);
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.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]