diff --git a/src/ClaudeDo.Localization/locales/de.json b/src/ClaudeDo.Localization/locales/de.json
index 31281061..c1f3e25d 100644
--- a/src/ClaudeDo.Localization/locales/de.json
+++ b/src/ClaudeDo.Localization/locales/de.json
@@ -473,6 +473,8 @@
"sectionIcon": "ICON",
"icon": "Listen-Icon",
"iconHint": "Wird in der Seitenleiste neben dem Listennamen angezeigt.",
+ "iconSearchWatermark": "Alle Icons durchsuchen (Englisch, z.B. rocket, car, bug)",
+ "iconSearchTruncated": "Die ersten 200 Treffer werden angezeigt — Suche verfeinern.",
"sectionAgent": "AGENT",
"resetAgentSettings": "Agent-Einstellungen zurücksetzen",
"sectionVerify": "VERIFIKATION",
diff --git a/src/ClaudeDo.Localization/locales/en.json b/src/ClaudeDo.Localization/locales/en.json
index fa7467cb..461b1b8b 100644
--- a/src/ClaudeDo.Localization/locales/en.json
+++ b/src/ClaudeDo.Localization/locales/en.json
@@ -473,6 +473,8 @@
"sectionIcon": "ICON",
"icon": "List icon",
"iconHint": "Shown next to the list name in the sidebar.",
+ "iconSearchWatermark": "Search all icons (English, e.g. rocket, car, bug)",
+ "iconSearchTruncated": "Showing the first 200 matches — refine the search.",
"sectionAgent": "AGENT",
"resetAgentSettings": "Reset agent settings",
"sectionVerify": "VERIFICATION",
diff --git a/src/ClaudeDo.Ui/ClaudeDo.Ui.csproj b/src/ClaudeDo.Ui/ClaudeDo.Ui.csproj
index 9de87d15..a0e5a141 100644
--- a/src/ClaudeDo.Ui/ClaudeDo.Ui.csproj
+++ b/src/ClaudeDo.Ui/ClaudeDo.Ui.csproj
@@ -10,6 +10,7 @@
+
diff --git a/src/ClaudeDo.Ui/Converters/IconKeyConverter.cs b/src/ClaudeDo.Ui/Converters/IconKeyConverter.cs
index 39a7c6da..8d0d4e34 100644
--- a/src/ClaudeDo.Ui/Converters/IconKeyConverter.cs
+++ b/src/ClaudeDo.Ui/Converters/IconKeyConverter.cs
@@ -2,27 +2,49 @@ using System.Globalization;
using Avalonia;
using Avalonia.Data.Converters;
using Avalonia.Media;
+using Material.Icons;
namespace ClaudeDo.Ui.Converters;
///
-/// Converts an icon key string (e.g. "Sun") to the matching StreamGeometry resource "Icon.Sun".
+/// Converts an icon key to its geometry: "Sun" → the "Icon.Sun" StreamGeometry resource,
+/// "mdi:RocketLaunch" → the Material Design Icons path of that .
///
public class IconKeyConverter : IValueConverter
{
+ /// Key prefix marking a Material Design Icons kind.
+ public const string MdiPrefix = "mdi:";
+
public static IconKeyConverter Instance { get; } = new();
+ private static readonly Dictionary MdiCache = new();
+
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is not string key || string.IsNullOrEmpty(key))
return null;
+ if (key.StartsWith(MdiPrefix, StringComparison.Ordinal))
+ return MdiGeometry(key[MdiPrefix.Length..]);
+
var resourceKey = $"Icon.{key}";
if (Application.Current?.TryGetResource(resourceKey, null, out var res) == true)
return res as StreamGeometry;
return null;
}
+ /// Parses (and caches) the geometry for an MDI kind name; null for unknown names.
+ private static Geometry? MdiGeometry(string name)
+ {
+ if (MdiCache.TryGetValue(name, out var cached))
+ return cached;
+ var geometry = Enum.TryParse(name, out var kind)
+ ? StreamGeometry.Parse(MaterialIconDataProvider.GetData(kind))
+ : null;
+ MdiCache[name] = geometry;
+ return geometry;
+ }
+
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
=> throw new NotSupportedException();
}
diff --git a/src/ClaudeDo.Ui/ViewModels/Modals/ListSettingsModalViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Modals/ListSettingsModalViewModel.cs
index aa409720..ce86f024 100644
--- a/src/ClaudeDo.Ui/ViewModels/Modals/ListSettingsModalViewModel.cs
+++ b/src/ClaudeDo.Ui/ViewModels/Modals/ListSettingsModalViewModel.cs
@@ -2,12 +2,14 @@ using System.Collections.ObjectModel;
using ClaudeDo.Data;
using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories;
+using ClaudeDo.Ui.Converters;
using ClaudeDo.Ui.Localization;
using ClaudeDo.Ui.Services;
using ClaudeDo.Ui.ViewModels.Agent;
using ClaudeDo.Ui.ViewModels.Modals.Settings;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
+using Material.Icons;
using Microsoft.EntityFrameworkCore;
namespace ClaudeDo.Ui.ViewModels.Modals;
@@ -132,7 +134,66 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
"Box", "Truck", "MapPin", "Camera", "Image", "Barcode", "Skull",
];
- public ObservableCollection Icons { get; } = new(IconOptions);
+ /// Cap on search hits so the picker's WrapPanel stays responsive.
+ public const int MaxIconResults = 200;
+
+ // One entry per distinct MDI icon; the enum carries aliases, all of which stay searchable.
+ private static readonly Lazy<(string Key, string Names)[]> MdiIndex = new(() =>
+ Enum.GetNames()
+ .GroupBy(Enum.Parse)
+ .Select(g => (IconKeyConverter.MdiPrefix + g.First(), string.Join(' ', g)))
+ .ToArray());
+
+ /// Picker items: the curated set, or MDI search hits once the search has 2+ characters.
+ [ObservableProperty] private IReadOnlyList _icons = IconOptions;
+
+ /// Free-text filter over the MDI icon names (English).
+ [ObservableProperty] private string _iconSearch = "";
+
+ /// True when the search hit more than icons.
+ [ObservableProperty] private bool _iconResultsTruncated;
+
+ // Separate from Icon: swapping Icons makes the ListBox push null here, which must not wipe the choice.
+ [ObservableProperty] private string? _selectedIconOption = DefaultIcon;
+
+ /// True for a curated key or an existing "mdi:" kind.
+ public static bool IsKnownIcon(string? key) =>
+ key is not null && (IconOptions.Contains(key)
+ || (key.StartsWith(IconKeyConverter.MdiPrefix, StringComparison.Ordinal)
+ && Enum.TryParse(key[IconKeyConverter.MdiPrefix.Length..], out _)));
+
+ partial void OnIconSearchChanged(string value)
+ {
+ var q = value.Trim();
+ if (q.Length < 2)
+ {
+ IconResultsTruncated = false;
+ // Keep a chosen MDI icon visible (and selected) in the curated view.
+ Icons = IconOptions.Contains(Icon) ? IconOptions : [Icon, .. IconOptions];
+ }
+ else
+ {
+ var hits = MdiIndex.Value
+ .Where(e => e.Names.Contains(q, StringComparison.OrdinalIgnoreCase))
+ .Select(e => e.Key)
+ .Take(MaxIconResults + 1)
+ .ToList();
+ IconResultsTruncated = hits.Count > MaxIconResults;
+ Icons = hits.Take(MaxIconResults).ToList();
+ }
+ SelectedIconOption = Icons.Contains(Icon) ? Icon : null;
+ }
+
+ partial void OnSelectedIconOptionChanged(string? value)
+ {
+ if (value is not null) Icon = value;
+ }
+
+ partial void OnIconChanged(string value)
+ {
+ if (!Icons.Contains(value)) OnIconSearchChanged(IconSearch);
+ else SelectedIconOption = value;
+ }
// The shared agent-config editor (Model / MaxTurns / SystemPrompt / AgentFile),
// scoped to this list (list → global inheritance).
@@ -174,7 +235,7 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
IsFolder = true;
CanToggleFolder = false;
Name = name;
- Icon = IconOptions.Contains(icon) ? icon! : DefaultFolderIcon;
+ Icon = IsKnownIcon(icon) ? icon! : DefaultFolderIcon;
}
public async Task LoadAsync(
@@ -198,7 +259,7 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
FindingsTracked = findingsTracked;
// An unknown/legacy key would leave the picker with no selection and silently reset the
// icon on the next save — fall back to the default instead.
- Icon = IconOptions.Contains(icon) ? icon! : DefaultIcon;
+ Icon = IsKnownIcon(icon) ? icon! : DefaultIcon;
WorkingDir = workingDir ?? "";
DefaultCommitType = string.IsNullOrWhiteSpace(defaultCommitType) ? CommitTypeRegistry.DefaultType : defaultCommitType;
diff --git a/src/ClaudeDo.Ui/Views/Modals/ListSettingsModalView.axaml b/src/ClaudeDo.Ui/Views/Modals/ListSettingsModalView.axaml
index 365111e5..a739efa8 100644
--- a/src/ClaudeDo.Ui/Views/Modals/ListSettingsModalView.axaml
+++ b/src/ClaudeDo.Ui/Views/Modals/ListSettingsModalView.axaml
@@ -143,8 +143,13 @@
+
+
@@ -157,6 +162,7 @@
diff --git a/tests/ClaudeDo.Ui.Tests/ViewModels/ListSettingsModalViewModelTests.cs b/tests/ClaudeDo.Ui.Tests/ViewModels/ListSettingsModalViewModelTests.cs
index 404399fd..2bbf2995 100644
--- a/tests/ClaudeDo.Ui.Tests/ViewModels/ListSettingsModalViewModelTests.cs
+++ b/tests/ClaudeDo.Ui.Tests/ViewModels/ListSettingsModalViewModelTests.cs
@@ -74,6 +74,27 @@ public class ListSettingsModalViewModelTests
Assert.Equal("Star", vm.Icon);
}
+ [Fact]
+ public void IconSearch_FindsMdiIcons_AndClearingKeepsTheChoice()
+ {
+ var vm = new ListSettingsModalViewModel(new CountingWorker(), NewFactory());
+
+ vm.IconSearch = "rocket";
+ Assert.Contains("mdi:RocketLaunch", vm.Icons);
+ Assert.DoesNotContain("Star", vm.Icons);
+
+ vm.SelectedIconOption = "mdi:RocketLaunch";
+ vm.SelectedIconOption = null; // what the ListBox pushes when Icons is swapped
+ Assert.Equal("mdi:RocketLaunch", vm.Icon);
+
+ vm.IconSearch = "";
+ Assert.Equal("mdi:RocketLaunch", vm.Icons[0]);
+ Assert.Equal("mdi:RocketLaunch", vm.SelectedIconOption);
+
+ Assert.True(ListSettingsModalViewModel.IsKnownIcon("mdi:RocketLaunch"));
+ Assert.False(ListSettingsModalViewModel.IsKnownIcon("mdi:NoSuchIcon"));
+ }
+
[Fact]
public async Task LoadAsync_CallsGetListConfig_ExactlyOnce()
{