feat(ui): remove a queued interactive message with a ✕
Queued rows are now QueuedMessageViewModel (Text + RemoveCommand); each shows a ✕ (Icon.WinClose) that calls RemoveQueuedInteractiveMessageAsync(taskId, text). The worker re-broadcasts the queue, rebuilding the strip without the removed message. Adds session.composer.unqueue (en/de).
This commit is contained in:
@@ -237,7 +237,8 @@
|
|||||||
"send": "Senden",
|
"send": "Senden",
|
||||||
"stop": "Sitzung beenden",
|
"stop": "Sitzung beenden",
|
||||||
"interrupt": "Aktuellen Zug unterbrechen",
|
"interrupt": "Aktuellen Zug unterbrechen",
|
||||||
"queued": "Wartet — wird nach dem aktuellen Zug gesendet"
|
"queued": "Wartet — wird nach dem aktuellen Zug gesendet",
|
||||||
|
"unqueue": "Aus Warteschlange entfernen"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"missionControl": {
|
"missionControl": {
|
||||||
|
|||||||
@@ -237,7 +237,8 @@
|
|||||||
"send": "Send",
|
"send": "Send",
|
||||||
"stop": "Stop session",
|
"stop": "Stop session",
|
||||||
"interrupt": "Interrupt current turn",
|
"interrupt": "Interrupt current turn",
|
||||||
"queued": "Queued — sends after the current turn"
|
"queued": "Queued — sends after the current turn",
|
||||||
|
"unqueue": "Remove from queue"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"missionControl": {
|
"missionControl": {
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ public sealed partial class TaskMonitorViewModel : ViewModelBase, IDisposable
|
|||||||
|
|
||||||
private const string RoadblockMarker = "Roadblocks reported during the run:";
|
private const string RoadblockMarker = "Roadblocks reported during the run:";
|
||||||
|
|
||||||
public ObservableCollection<string> QueuedMessages { get; } = new();
|
public ObservableCollection<QueuedMessageViewModel> QueuedMessages { get; } = new();
|
||||||
public bool HasQueuedMessages => QueuedMessages.Count > 0;
|
public bool HasQueuedMessages => QueuedMessages.Count > 0;
|
||||||
|
|
||||||
// Captured handler delegates for disposal
|
// Captured handler delegates for disposal
|
||||||
@@ -181,7 +181,15 @@ public sealed partial class TaskMonitorViewModel : ViewModelBase, IDisposable
|
|||||||
{
|
{
|
||||||
if (taskId != _subscribedTaskId) return;
|
if (taskId != _subscribedTaskId) return;
|
||||||
QueuedMessages.Clear();
|
QueuedMessages.Clear();
|
||||||
foreach (var m in pending) QueuedMessages.Add(m);
|
foreach (var m in pending)
|
||||||
|
{
|
||||||
|
var text = m;
|
||||||
|
QueuedMessages.Add(new QueuedMessageViewModel
|
||||||
|
{
|
||||||
|
Text = text,
|
||||||
|
RemoveCommand = new CommunityToolkit.Mvvm.Input.RelayCommand(() => _ = RemoveQueuedAsync(text)),
|
||||||
|
});
|
||||||
|
}
|
||||||
OnPropertyChanged(nameof(HasQueuedMessages));
|
OnPropertyChanged(nameof(HasQueuedMessages));
|
||||||
};
|
};
|
||||||
_worker.InteractiveQueueChangedEvent += _onInteractiveQueueChanged;
|
_worker.InteractiveQueueChangedEvent += _onInteractiveQueueChanged;
|
||||||
@@ -234,6 +242,12 @@ public sealed partial class TaskMonitorViewModel : ViewModelBase, IDisposable
|
|||||||
await _worker.InterruptInteractiveSessionAsync(_subscribedTaskId);
|
await _worker.InterruptInteractiveSessionAsync(_subscribedTaskId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async System.Threading.Tasks.Task RemoveQueuedAsync(string text)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(_subscribedTaskId))
|
||||||
|
await _worker.RemoveQueuedInteractiveMessageAsync(_subscribedTaskId, text);
|
||||||
|
}
|
||||||
|
|
||||||
private void ClearPendingQuestion()
|
private void ClearPendingQuestion()
|
||||||
{
|
{
|
||||||
PendingQuestionId = null;
|
PendingQuestionId = null;
|
||||||
@@ -515,3 +529,9 @@ public sealed partial class TaskMonitorViewModel : ViewModelBase, IDisposable
|
|||||||
_worker.InteractiveMessageSentEvent -= _onInteractiveMessageSent;
|
_worker.InteractiveMessageSentEvent -= _onInteractiveMessageSent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class QueuedMessageViewModel
|
||||||
|
{
|
||||||
|
public required string Text { get; init; }
|
||||||
|
public required System.Windows.Input.ICommand RemoveCommand { get; init; }
|
||||||
|
}
|
||||||
|
|||||||
@@ -276,19 +276,30 @@
|
|||||||
Foreground="{DynamicResource TextMuteBrush}" />
|
Foreground="{DynamicResource TextMuteBrush}" />
|
||||||
<ItemsControl ItemsSource="{Binding Monitor.QueuedMessages}">
|
<ItemsControl ItemsSource="{Binding Monitor.QueuedMessages}">
|
||||||
<ItemsControl.ItemTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate x:DataType="vm:QueuedMessageViewModel">
|
||||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
<Grid ColumnDefinitions="Auto,*,Auto" Margin="0,1">
|
||||||
<TextBlock Text="⧗"
|
<TextBlock Grid.Column="0"
|
||||||
|
Text="⧗"
|
||||||
Foreground="{DynamicResource TextMuteBrush}"
|
Foreground="{DynamicResource TextMuteBrush}"
|
||||||
FontFamily="{StaticResource MonoFont}"
|
FontFamily="{StaticResource MonoFont}"
|
||||||
FontSize="{StaticResource FontSizeMono}"
|
FontSize="{StaticResource FontSizeMono}"
|
||||||
VerticalAlignment="Center" />
|
VerticalAlignment="Center"
|
||||||
<TextBlock Text="{Binding}"
|
Margin="0,0,6,0" />
|
||||||
|
<TextBlock Grid.Column="1"
|
||||||
|
Text="{Binding Text}"
|
||||||
Foreground="{DynamicResource TextDimBrush}"
|
Foreground="{DynamicResource TextDimBrush}"
|
||||||
FontFamily="{StaticResource MonoFont}"
|
FontFamily="{StaticResource MonoFont}"
|
||||||
FontSize="{StaticResource FontSizeMono}"
|
FontSize="{StaticResource FontSizeMono}"
|
||||||
TextTrimming="CharacterEllipsis" />
|
TextTrimming="CharacterEllipsis"
|
||||||
</StackPanel>
|
VerticalAlignment="Center" />
|
||||||
|
<Button Grid.Column="2"
|
||||||
|
Classes="title-ctrl"
|
||||||
|
Command="{Binding RemoveCommand}"
|
||||||
|
ToolTip.Tip="{loc:Tr session.composer.unqueue}"
|
||||||
|
Margin="4,0,0,0">
|
||||||
|
<PathIcon Data="{StaticResource Icon.WinClose}" Width="8" Height="8"/>
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ItemsControl.ItemTemplate>
|
</ItemsControl.ItemTemplate>
|
||||||
</ItemsControl>
|
</ItemsControl>
|
||||||
|
|||||||
@@ -64,19 +64,30 @@
|
|||||||
Foreground="{DynamicResource TextMuteBrush}" />
|
Foreground="{DynamicResource TextMuteBrush}" />
|
||||||
<ItemsControl ItemsSource="{Binding #Root.QueuedMessages}">
|
<ItemsControl ItemsSource="{Binding #Root.QueuedMessages}">
|
||||||
<ItemsControl.ItemTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate x:DataType="vm:QueuedMessageViewModel">
|
||||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
<Grid ColumnDefinitions="Auto,*,Auto" Margin="0,1">
|
||||||
<TextBlock Text="⧗"
|
<TextBlock Grid.Column="0"
|
||||||
|
Text="⧗"
|
||||||
Foreground="{DynamicResource TextMuteBrush}"
|
Foreground="{DynamicResource TextMuteBrush}"
|
||||||
FontFamily="{StaticResource MonoFont}"
|
FontFamily="{StaticResource MonoFont}"
|
||||||
FontSize="{StaticResource FontSizeMono}"
|
FontSize="{StaticResource FontSizeMono}"
|
||||||
VerticalAlignment="Center" />
|
VerticalAlignment="Center"
|
||||||
<TextBlock Text="{Binding}"
|
Margin="0,0,6,0" />
|
||||||
|
<TextBlock Grid.Column="1"
|
||||||
|
Text="{Binding Text}"
|
||||||
Foreground="{DynamicResource TextDimBrush}"
|
Foreground="{DynamicResource TextDimBrush}"
|
||||||
FontFamily="{StaticResource MonoFont}"
|
FontFamily="{StaticResource MonoFont}"
|
||||||
FontSize="{StaticResource FontSizeMono}"
|
FontSize="{StaticResource FontSizeMono}"
|
||||||
TextTrimming="CharacterEllipsis" />
|
TextTrimming="CharacterEllipsis"
|
||||||
</StackPanel>
|
VerticalAlignment="Center" />
|
||||||
|
<Button Grid.Column="2"
|
||||||
|
Classes="title-ctrl"
|
||||||
|
Command="{Binding RemoveCommand}"
|
||||||
|
ToolTip.Tip="{loc:Tr session.composer.unqueue}"
|
||||||
|
Margin="4,0,0,0">
|
||||||
|
<PathIcon Data="{StaticResource Icon.WinClose}" Width="8" Height="8"/>
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ItemsControl.ItemTemplate>
|
</ItemsControl.ItemTemplate>
|
||||||
</ItemsControl>
|
</ItemsControl>
|
||||||
|
|||||||
@@ -333,6 +333,8 @@ public class TaskMonitorViewModelTests : IDisposable
|
|||||||
worker.RaiseInteractiveQueueChanged("t1", new[] { "msg1", "msg2" });
|
worker.RaiseInteractiveQueueChanged("t1", new[] { "msg1", "msg2" });
|
||||||
|
|
||||||
Assert.Equal(2, vm.QueuedMessages.Count);
|
Assert.Equal(2, vm.QueuedMessages.Count);
|
||||||
|
Assert.Equal("msg1", vm.QueuedMessages[0].Text);
|
||||||
|
Assert.Equal("msg2", vm.QueuedMessages[1].Text);
|
||||||
Assert.True(vm.HasQueuedMessages);
|
Assert.True(vm.HasQueuedMessages);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -378,6 +380,22 @@ public class TaskMonitorViewModelTests : IDisposable
|
|||||||
Assert.False(vm.HasQueuedMessages);
|
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]
|
[Fact]
|
||||||
public async Task InterruptInteractiveCommand_WhenLive_RecordsOneCall()
|
public async Task InterruptInteractiveCommand_WhenLive_RecordsOneCall()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user