diff --git a/src/ClaudeDo.App/App.axaml b/src/ClaudeDo.App/App.axaml
index dd4a14d..aea3954 100644
--- a/src/ClaudeDo.App/App.axaml
+++ b/src/ClaudeDo.App/App.axaml
@@ -21,6 +21,7 @@
+
diff --git a/src/ClaudeDo.Localization/locales/de.json b/src/ClaudeDo.Localization/locales/de.json
index a9671f3..3381a55 100644
--- a/src/ClaudeDo.Localization/locales/de.json
+++ b/src/ClaudeDo.Localization/locales/de.json
@@ -235,7 +235,8 @@
"composer": {
"placeholder": "Nachricht an die Sitzung…",
"send": "Senden",
- "stop": "Sitzung beenden"
+ "stop": "Sitzung beenden",
+ "interrupt": "Aktuellen Zug unterbrechen"
}
},
"missionControl": {
diff --git a/src/ClaudeDo.Localization/locales/en.json b/src/ClaudeDo.Localization/locales/en.json
index 0115698..82afbc4 100644
--- a/src/ClaudeDo.Localization/locales/en.json
+++ b/src/ClaudeDo.Localization/locales/en.json
@@ -235,7 +235,8 @@
"composer": {
"placeholder": "Message the session…",
"send": "Send",
- "stop": "Stop session"
+ "stop": "Stop session",
+ "interrupt": "Interrupt current turn"
}
},
"missionControl": {
diff --git a/src/ClaudeDo.Ui/Converters/LogKindForegroundConverter.cs b/src/ClaudeDo.Ui/Converters/LogKindForegroundConverter.cs
new file mode 100644
index 0000000..3546100
--- /dev/null
+++ b/src/ClaudeDo.Ui/Converters/LogKindForegroundConverter.cs
@@ -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();
+}
diff --git a/src/ClaudeDo.Ui/Design/IslandStyles.axaml b/src/ClaudeDo.Ui/Design/IslandStyles.axaml
index 194be8c..5d39140 100644
--- a/src/ClaudeDo.Ui/Design/IslandStyles.axaml
+++ b/src/ClaudeDo.Ui/Design/IslandStyles.axaml
@@ -84,6 +84,9 @@
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
+
+ M4 4 H20 V20 H4 Z
+
M4 12l5 5 11-11
diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/TaskMonitorViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/TaskMonitorViewModel.cs
index 9074d89..872b4d7 100644
--- a/src/ClaudeDo.Ui/ViewModels/Islands/TaskMonitorViewModel.cs
+++ b/src/ClaudeDo.Ui/ViewModels/Islands/TaskMonitorViewModel.cs
@@ -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;
diff --git a/src/ClaudeDo.Ui/Views/Islands/Detail/WorkConsole.axaml b/src/ClaudeDo.Ui/Views/Islands/Detail/WorkConsole.axaml
index 1a91f03..e89c096 100644
--- a/src/ClaudeDo.Ui/Views/Islands/Detail/WorkConsole.axaml
+++ b/src/ClaudeDo.Ui/Views/Islands/Detail/WorkConsole.axaml
@@ -267,7 +267,7 @@
only while an interactive session is running for this task. -->
-
+
@@ -306,7 +312,7 @@
Text="{Binding TimestampFormatted}" />
diff --git a/src/ClaudeDo.Ui/Views/Islands/SessionTerminalView.axaml b/src/ClaudeDo.Ui/Views/Islands/SessionTerminalView.axaml
index 7f4b2a8..090c79b 100644
--- a/src/ClaudeDo.Ui/Views/Islands/SessionTerminalView.axaml
+++ b/src/ClaudeDo.Ui/Views/Islands/SessionTerminalView.axaml
@@ -57,7 +57,7 @@
BorderBrush="{DynamicResource LineBrush}"
BorderThickness="0,1,0,0"
Padding="6,5">
-
+
+
@@ -89,7 +96,7 @@
diff --git a/src/ClaudeDo.Ui/Views/Islands/SessionTerminalView.axaml.cs b/src/ClaudeDo.Ui/Views/Islands/SessionTerminalView.axaml.cs
index 12b1937..3d6b7c7 100644
--- a/src/ClaudeDo.Ui/Views/Islands/SessionTerminalView.axaml.cs
+++ b/src/ClaudeDo.Ui/Views/Islands/SessionTerminalView.axaml.cs
@@ -25,6 +25,8 @@ public partial class SessionTerminalView : UserControl
AvaloniaProperty.Register(nameof(ComposerText), defaultBindingMode: BindingMode.TwoWay);
public static readonly StyledProperty SubmitCommandProperty =
AvaloniaProperty.Register(nameof(SubmitCommand));
+ public static readonly StyledProperty InterruptCommandProperty =
+ AvaloniaProperty.Register(nameof(InterruptCommand));
public static readonly StyledProperty ComposerPlaceholderProperty =
AvaloniaProperty.Register(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;
diff --git a/src/ClaudeDo.Ui/Views/MissionControl/MonitorPaneView.axaml b/src/ClaudeDo.Ui/Views/MissionControl/MonitorPaneView.axaml
index fd6dcdc..a9c1406 100644
--- a/src/ClaudeDo.Ui/Views/MissionControl/MonitorPaneView.axaml
+++ b/src/ClaudeDo.Ui/Views/MissionControl/MonitorPaneView.axaml
@@ -111,6 +111,7 @@
IsComposerVisible="{Binding IsInteractiveLive}"
ComposerText="{Binding ComposerDraft, Mode=TwoWay}"
SubmitCommand="{Binding SubmitComposerCommand}"
+ InterruptCommand="{Binding InterruptInteractiveCommand}"
ComposerPlaceholder="{loc:Tr session.composer.placeholder}" />
diff --git a/tests/ClaudeDo.Ui.Tests/ViewModels/TaskMonitorViewModelTests.cs b/tests/ClaudeDo.Ui.Tests/ViewModels/TaskMonitorViewModelTests.cs
index 9841f93..7dc0c8b 100644
--- a/tests/ClaudeDo.Ui.Tests/ViewModels/TaskMonitorViewModelTests.cs
+++ b/tests/ClaudeDo.Ui.Tests/ViewModels/TaskMonitorViewModelTests.cs
@@ -297,4 +297,18 @@ public class TaskMonitorViewModelTests : IDisposable
Assert.Equal(LogKind.User, vm.Log[0].Kind);
Assert.Equal("do the thing", vm.Log[0].Text);
}
+
+ [Fact]
+ public async Task InterruptInteractiveCommand_WhenLive_RecordsOneCall()
+ {
+ var worker = new FakeWorker();
+ using var vm = Build(worker);
+ vm.SetTaskId("t1");
+ worker.RaiseInteractiveStarted("t1");
+
+ await vm.InterruptInteractiveCommand.ExecuteAsync(null);
+
+ Assert.Single(worker.InterruptedInteractive);
+ Assert.Equal("t1", worker.InterruptedInteractive[0]);
+ }
}