diff --git a/src/ClaudeDo.App/App.axaml b/src/ClaudeDo.App/App.axaml
index 8dc920a7..b8171160 100644
--- a/src/ClaudeDo.App/App.axaml
+++ b/src/ClaudeDo.App/App.axaml
@@ -13,7 +13,6 @@
-
diff --git a/src/ClaudeDo.Ui/Converters/BoolToDraftOpacityConverter.cs b/src/ClaudeDo.Ui/Converters/BoolToDraftOpacityConverter.cs
deleted file mode 100644
index 3c582d75..00000000
--- a/src/ClaudeDo.Ui/Converters/BoolToDraftOpacityConverter.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-using System;
-using System.Globalization;
-using Avalonia.Data.Converters;
-
-namespace ClaudeDo.Ui.Converters;
-
-public sealed class BoolToDraftOpacityConverter : IValueConverter
-{
- public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
- => value is true ? 0.7 : 1.0;
- public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
- => throw new NotSupportedException();
-}
diff --git a/src/ClaudeDo.Ui/Converters/BoolToItalicConverter.cs b/src/ClaudeDo.Ui/Converters/BoolToItalicConverter.cs
deleted file mode 100644
index 558c03ff..00000000
--- a/src/ClaudeDo.Ui/Converters/BoolToItalicConverter.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-using System;
-using System.Globalization;
-using Avalonia.Data.Converters;
-using Avalonia.Media;
-
-namespace ClaudeDo.Ui.Converters;
-
-public sealed class BoolToItalicConverter : IValueConverter
-{
- public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
- => value is true ? FontStyle.Italic : FontStyle.Normal;
- public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
- => throw new NotSupportedException();
-}
diff --git a/src/ClaudeDo.Ui/Converters/NotHeaderRowConverter.cs b/src/ClaudeDo.Ui/Converters/NotHeaderRowConverter.cs
deleted file mode 100644
index 9af97f28..00000000
--- a/src/ClaudeDo.Ui/Converters/NotHeaderRowConverter.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using System.Globalization;
-using Avalonia.Data.Converters;
-using ClaudeDo.Ui.ViewModels.Islands;
-
-namespace ClaudeDo.Ui.Converters;
-
-// Drives ListBoxItem.Focusable for TasksIslandView's flat Rows list: a HeaderRow container must
-// not be a tab stop, a TaskRowViewModel container must behave like any other row.
-public class NotHeaderRowConverter : IValueConverter
-{
- public static NotHeaderRowConverter Instance { get; } = new();
-
- public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
- => value is not HeaderRow;
-
- public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
- => throw new NotSupportedException();
-}
diff --git a/src/ClaudeDo.Ui/Converters/NotNullToBoolConverter.cs b/src/ClaudeDo.Ui/Converters/NotNullToBoolConverter.cs
deleted file mode 100644
index fd4604bb..00000000
--- a/src/ClaudeDo.Ui/Converters/NotNullToBoolConverter.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-using System.Globalization;
-using Avalonia.Data.Converters;
-
-namespace ClaudeDo.Ui.Converters;
-
-public class NotNullToBoolConverter : IValueConverter
-{
- public static NotNullToBoolConverter Instance { get; } = new();
-
- public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
- => value is not null;
-
- public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
- => throw new NotSupportedException();
-}
diff --git a/src/ClaudeDo.Ui/Converters/SimpleConverters.cs b/src/ClaudeDo.Ui/Converters/SimpleConverters.cs
new file mode 100644
index 00000000..9f21a857
--- /dev/null
+++ b/src/ClaudeDo.Ui/Converters/SimpleConverters.cs
@@ -0,0 +1,53 @@
+using System.Globalization;
+using Avalonia.Data.Converters;
+using Avalonia.Media;
+using ClaudeDo.Ui.ViewModels.Islands;
+
+namespace ClaudeDo.Ui.Converters;
+
+// One-expression converters. Each is only worth a class because Avalonia XAML can't
+// instantiate a generic FuncValueConverter as a resource; they stay one per type so the
+// x:Key names in App.axaml keep resolving. Anything with real logic gets its own file.
+
+public sealed class UpperCaseConverter : OneWayConverter
+{
+ public static UpperCaseConverter Instance { get; } = new();
+ public override object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ => value?.ToString()?.ToUpperInvariant();
+}
+
+public sealed class StrikeIfTrueConverter : OneWayConverter
+{
+ public static StrikeIfTrueConverter Instance { get; } = new();
+ public override object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ => value is true ? TextDecorations.Strikethrough : null;
+}
+
+public sealed class BoolToItalicConverter : OneWayConverter
+{
+ public override object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ => value is true ? FontStyle.Italic : FontStyle.Normal;
+}
+
+public sealed class BoolToDraftOpacityConverter : OneWayConverter
+{
+ public override object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ => value is true ? 0.7 : 1.0;
+}
+
+// Drives ListBoxItem.Focusable for TasksIslandView's flat Rows list: a HeaderRow container must
+// not be a tab stop, a TaskRowViewModel container must behave like any other row.
+public sealed class NotHeaderRowConverter : OneWayConverter
+{
+ public static NotHeaderRowConverter Instance { get; } = new();
+ public override object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ => value is not HeaderRow;
+}
+
+public abstract class OneWayConverter : IValueConverter
+{
+ public abstract object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture);
+
+ public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+ => throw new NotSupportedException();
+}
diff --git a/src/ClaudeDo.Ui/Converters/StrikeIfTrueConverter.cs b/src/ClaudeDo.Ui/Converters/StrikeIfTrueConverter.cs
deleted file mode 100644
index c67a3965..00000000
--- a/src/ClaudeDo.Ui/Converters/StrikeIfTrueConverter.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using System.Globalization;
-using Avalonia.Data.Converters;
-using Avalonia.Media;
-
-namespace ClaudeDo.Ui.Converters;
-
-public class StrikeIfTrueConverter : IValueConverter
-{
- public static StrikeIfTrueConverter Instance { get; } = new();
-
- public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
- => value is true ? TextDecorations.Strikethrough : null;
-
- public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
- => throw new NotSupportedException();
-}
diff --git a/src/ClaudeDo.Ui/Converters/UpperCaseConverter.cs b/src/ClaudeDo.Ui/Converters/UpperCaseConverter.cs
deleted file mode 100644
index a9f49de1..00000000
--- a/src/ClaudeDo.Ui/Converters/UpperCaseConverter.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-using System.Globalization;
-using Avalonia.Data.Converters;
-
-namespace ClaudeDo.Ui.Converters;
-
-public class UpperCaseConverter : IValueConverter
-{
- public static UpperCaseConverter Instance { get; } = new();
-
- public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
- => value?.ToString()?.ToUpperInvariant();
-
- public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
- => throw new NotSupportedException();
-}
diff --git a/src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml b/src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml
index df95483a..f7c4a332 100644
--- a/src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml
+++ b/src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml
@@ -2,6 +2,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Islands"
xmlns:ctl="using:ClaudeDo.Ui.Views.Controls"
+ xmlns:conv="using:Avalonia.Data.Converters"
xmlns:loc="using:ClaudeDo.Ui.Localization"
x:Class="ClaudeDo.Ui.Views.Islands.TaskRowView"
x:DataType="vm:TaskRowViewModel">
@@ -187,7 +188,7 @@
+ IsVisible="{Binding ChainAfterLabel, Converter={x:Static conv:ObjectConverters.IsNotNull}}">