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

@@ -70,6 +70,9 @@ public sealed partial class TaskMonitorViewModel : ViewModelBase, IDisposable
private const string RoadblockMarker = "Roadblocks reported during the run:";
public ObservableCollection<string> QueuedMessages { get; } = new();
public bool HasQueuedMessages => QueuedMessages.Count > 0;
// Captured handler delegates for disposal
private readonly Action<string, string> _onTaskMessage;
private readonly Action<string, string, DateTime> _onTaskStarted;
@@ -79,6 +82,8 @@ public sealed partial class TaskMonitorViewModel : ViewModelBase, IDisposable
private readonly Action<string, string> _onTaskQuestionResolved;
private readonly Action<string> _onInteractiveStarted;
private readonly Action<string> _onInteractiveEnded;
private readonly Action<string, IReadOnlyList<string>> _onInteractiveQueueChanged;
private readonly Action<string, string> _onInteractiveMessageSent;
// Interactive composer — active while the worker is in an interactive session.
[ObservableProperty]
@@ -164,9 +169,29 @@ public sealed partial class TaskMonitorViewModel : ViewModelBase, IDisposable
_onInteractiveEnded = taskId =>
{
if (taskId == _subscribedTaskId) { IsInteractiveLive = false; AgentState = "done"; }
if (taskId != _subscribedTaskId) return;
IsInteractiveLive = false;
AgentState = "done";
QueuedMessages.Clear();
OnPropertyChanged(nameof(HasQueuedMessages));
};
_worker.InteractiveSessionEndedEvent += _onInteractiveEnded;
_onInteractiveQueueChanged = (taskId, pending) =>
{
if (taskId != _subscribedTaskId) return;
QueuedMessages.Clear();
foreach (var m in pending) QueuedMessages.Add(m);
OnPropertyChanged(nameof(HasQueuedMessages));
};
_worker.InteractiveQueueChangedEvent += _onInteractiveQueueChanged;
_onInteractiveMessageSent = (taskId, text) =>
{
if (taskId == _subscribedTaskId)
Log.Add(new LogLineViewModel { Kind = LogKind.User, Text = text });
};
_worker.InteractiveMessageSentEvent += _onInteractiveMessageSent;
}
// Surface a pending question (used by live event + re-attach hydration).
@@ -189,7 +214,6 @@ public sealed partial class TaskMonitorViewModel : ViewModelBase, IDisposable
if (string.IsNullOrEmpty(_subscribedTaskId)) return;
var text = ComposerDraft;
if (string.IsNullOrWhiteSpace(text)) return;
Log.Add(new LogLineViewModel { Kind = LogKind.User, Text = text });
ComposerDraft = string.Empty;
await _worker.SendInteractiveMessageAsync(_subscribedTaskId, text);
}
@@ -260,6 +284,8 @@ public sealed partial class TaskMonitorViewModel : ViewModelBase, IDisposable
ClearPendingQuestion();
IsInteractiveLive = false;
ComposerDraft = string.Empty;
QueuedMessages.Clear();
OnPropertyChanged(nameof(HasQueuedMessages));
}
[ObservableProperty]
@@ -485,5 +511,7 @@ public sealed partial class TaskMonitorViewModel : ViewModelBase, IDisposable
_worker.TaskQuestionResolvedEvent -= _onTaskQuestionResolved;
_worker.InteractiveSessionStartedEvent -= _onInteractiveStarted;
_worker.InteractiveSessionEndedEvent -= _onInteractiveEnded;
_worker.InteractiveQueueChangedEvent -= _onInteractiveQueueChanged;
_worker.InteractiveMessageSentEvent -= _onInteractiveMessageSent;
}
}