feat(ui): highlight user chat messages + opt-in interrupt (stop) button
LogKindForegroundConverter drives the log message foreground via a local binding (beats the dim local value), so user messages render in the accent color instead of vanishing into the transcript. Adds a small stop (Icon.Stop) button next to Send in both composers (SessionTerminalView + WorkConsole) wired to InterruptInteractiveCommand → InterruptInteractiveSessionAsync. Adds session.composer.interrupt (en/de).
This commit is contained in:
43
src/ClaudeDo.Ui/Converters/LogKindForegroundConverter.cs
Normal file
43
src/ClaudeDo.Ui/Converters/LogKindForegroundConverter.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using System.Globalization;
|
||||
using Avalonia;
|
||||
using Avalonia.Data.Converters;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Styling;
|
||||
using ClaudeDo.Ui.ViewModels.Islands;
|
||||
|
||||
namespace ClaudeDo.Ui.Converters;
|
||||
|
||||
public sealed class LogKindForegroundConverter : IValueConverter
|
||||
{
|
||||
private static IBrush? Resolve(string key)
|
||||
{
|
||||
if (Application.Current is { } app &&
|
||||
app.Resources.TryGetResource(key, app.ActualThemeVariant, out var res) &&
|
||||
res is IBrush brush)
|
||||
{
|
||||
return brush;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
var key = value is LogKind kind ? kind switch
|
||||
{
|
||||
LogKind.Sys => "TextMuteBrush",
|
||||
LogKind.Tool => "SageBrush",
|
||||
LogKind.Claude => "TextBrush",
|
||||
LogKind.Stdout => "TextDimBrush",
|
||||
LogKind.Stderr => "BloodBrush",
|
||||
LogKind.Done => "MossBrightBrush",
|
||||
LogKind.Msg => "TextDimBrush",
|
||||
LogKind.User => "AccentBrush",
|
||||
_ => "TextDimBrush",
|
||||
} : "TextDimBrush";
|
||||
|
||||
return Resolve(key) ?? AvaloniaProperty.UnsetValue;
|
||||
}
|
||||
|
||||
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) =>
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
@@ -84,6 +84,9 @@
|
||||
<!-- Icon.X — filled X outline (PathIcon fills, so a stroke-only X renders invisible) -->
|
||||
<StreamGeometry x:Key="Icon.X">M6.4 4.6 L12 10.2 L17.6 4.6 L19.4 6.4 L13.8 12 L19.4 17.6 L17.6 19.4 L12 13.8 L6.4 19.4 L4.6 17.6 L10.2 12 L4.6 6.4 Z</StreamGeometry>
|
||||
|
||||
<!-- Icon.Stop — filled square (stop / interrupt) -->
|
||||
<StreamGeometry x:Key="Icon.Stop">M4 4 H20 V20 H4 Z</StreamGeometry>
|
||||
|
||||
<!-- Icon.Check -->
|
||||
<StreamGeometry x:Key="Icon.Check">M4 12l5 5 11-11</StreamGeometry>
|
||||
|
||||
|
||||
@@ -203,6 +203,13 @@ public sealed partial class TaskMonitorViewModel : ViewModelBase, IDisposable
|
||||
await _worker.StopInteractiveSessionAsync(_subscribedTaskId);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async System.Threading.Tasks.Task InterruptInteractive()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_subscribedTaskId) && IsInteractiveLive)
|
||||
await _worker.InterruptInteractiveSessionAsync(_subscribedTaskId);
|
||||
}
|
||||
|
||||
private void ClearPendingQuestion()
|
||||
{
|
||||
PendingQuestionId = null;
|
||||
|
||||
@@ -267,7 +267,7 @@
|
||||
only while an interactive session is running for this task. -->
|
||||
<Grid DockPanel.Dock="Bottom"
|
||||
IsVisible="{Binding Monitor.IsInteractiveLive}"
|
||||
ColumnDefinitions="Auto,*,Auto"
|
||||
ColumnDefinitions="Auto,*,Auto,Auto"
|
||||
Margin="12,2,12,8">
|
||||
<TextBlock Grid.Column="0" Text="❯"
|
||||
FontFamily="{StaticResource MonoFont}"
|
||||
@@ -288,8 +288,14 @@
|
||||
<KeyBinding Gesture="Enter" Command="{Binding Monitor.SubmitComposerCommand}" />
|
||||
</TextBox.KeyBindings>
|
||||
</TextBox>
|
||||
<Button Grid.Column="2" Classes="prompt-action accent" Content="[Send]"
|
||||
<Button Grid.Column="2" Classes="prompt-action"
|
||||
VerticalAlignment="Center" Margin="12,0,0,0"
|
||||
Command="{Binding Monitor.InterruptInteractiveCommand}"
|
||||
ToolTip.Tip="{loc:Tr session.composer.interrupt}">
|
||||
<PathIcon Data="{StaticResource Icon.Stop}" Width="10" Height="10"/>
|
||||
</Button>
|
||||
<Button Grid.Column="3" Classes="prompt-action accent" Content="[Send]"
|
||||
VerticalAlignment="Center" Margin="4,0,0,0"
|
||||
Command="{Binding Monitor.SubmitComposerCommand}" />
|
||||
</Grid>
|
||||
|
||||
@@ -306,7 +312,7 @@
|
||||
Text="{Binding TimestampFormatted}" />
|
||||
<SelectableTextBlock Grid.Column="1"
|
||||
Text="{Binding Text}" Tag="{Binding ClassName}"
|
||||
Foreground="{DynamicResource TextDimBrush}"
|
||||
Foreground="{Binding Kind, Converter={StaticResource LogKindForeground}}"
|
||||
TextWrapping="Wrap" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
BorderBrush="{DynamicResource LineBrush}"
|
||||
BorderThickness="0,1,0,0"
|
||||
Padding="6,5">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<Grid ColumnDefinitions="*,Auto,Auto">
|
||||
<TextBox Grid.Column="0"
|
||||
Text="{Binding #Root.ComposerText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
PlaceholderText="{Binding #Root.ComposerPlaceholder}"
|
||||
@@ -67,6 +67,13 @@
|
||||
</TextBox.KeyBindings>
|
||||
</TextBox>
|
||||
<Button Grid.Column="1"
|
||||
Margin="6,0,0,0"
|
||||
Classes="title-ctrl"
|
||||
Command="{Binding #Root.InterruptCommand}"
|
||||
ToolTip.Tip="{loc:Tr session.composer.interrupt}">
|
||||
<PathIcon Data="{StaticResource Icon.Stop}" Width="10" Height="10"/>
|
||||
</Button>
|
||||
<Button Grid.Column="2"
|
||||
Margin="6,0,0,0"
|
||||
Content="{loc:Tr session.composer.send}"
|
||||
Command="{Binding #Root.SubmitCommand}"/>
|
||||
@@ -89,7 +96,7 @@
|
||||
<!-- Message text — selectable so the user can copy raw output -->
|
||||
<SelectableTextBlock Grid.Column="1"
|
||||
Text="{Binding Text}" Tag="{Binding ClassName}"
|
||||
Foreground="{DynamicResource TextDimBrush}"
|
||||
Foreground="{Binding Kind, Converter={StaticResource LogKindForeground}}"
|
||||
TextWrapping="Wrap"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
@@ -25,6 +25,8 @@ public partial class SessionTerminalView : UserControl
|
||||
AvaloniaProperty.Register<SessionTerminalView, string?>(nameof(ComposerText), defaultBindingMode: BindingMode.TwoWay);
|
||||
public static readonly StyledProperty<ICommand?> SubmitCommandProperty =
|
||||
AvaloniaProperty.Register<SessionTerminalView, ICommand?>(nameof(SubmitCommand));
|
||||
public static readonly StyledProperty<ICommand?> InterruptCommandProperty =
|
||||
AvaloniaProperty.Register<SessionTerminalView, ICommand?>(nameof(InterruptCommand));
|
||||
public static readonly StyledProperty<string?> ComposerPlaceholderProperty =
|
||||
AvaloniaProperty.Register<SessionTerminalView, string?>(nameof(ComposerPlaceholder));
|
||||
|
||||
@@ -36,6 +38,7 @@ public partial class SessionTerminalView : UserControl
|
||||
public bool IsComposerVisible { get => GetValue(IsComposerVisibleProperty); set => SetValue(IsComposerVisibleProperty, value); }
|
||||
public string? ComposerText { get => GetValue(ComposerTextProperty); set => SetValue(ComposerTextProperty, value); }
|
||||
public ICommand? SubmitCommand { get => GetValue(SubmitCommandProperty); set => SetValue(SubmitCommandProperty, value); }
|
||||
public ICommand? InterruptCommand { get => GetValue(InterruptCommandProperty); set => SetValue(InterruptCommandProperty, value); }
|
||||
public string? ComposerPlaceholder { get => GetValue(ComposerPlaceholderProperty); set => SetValue(ComposerPlaceholderProperty, value); }
|
||||
|
||||
private INotifyCollectionChanged? _subscribedCollection;
|
||||
|
||||
@@ -111,6 +111,7 @@
|
||||
IsComposerVisible="{Binding IsInteractiveLive}"
|
||||
ComposerText="{Binding ComposerDraft, Mode=TwoWay}"
|
||||
SubmitCommand="{Binding SubmitComposerCommand}"
|
||||
InterruptCommand="{Binding InterruptInteractiveCommand}"
|
||||
ComposerPlaceholder="{loc:Tr session.composer.placeholder}" />
|
||||
|
||||
</DockPanel>
|
||||
|
||||
Reference in New Issue
Block a user