diff --git a/src/ClaudeDo.Ui/Converters/IconKeyConverter.cs b/src/ClaudeDo.Ui/Converters/IconKeyConverter.cs
index 8d0d4e34..aa0cb1eb 100644
--- a/src/ClaudeDo.Ui/Converters/IconKeyConverter.cs
+++ b/src/ClaudeDo.Ui/Converters/IconKeyConverter.cs
@@ -7,14 +7,57 @@ using Material.Icons;
namespace ClaudeDo.Ui.Converters;
///
-/// Converts an icon key to its geometry: "Sun" → the "Icon.Sun" StreamGeometry resource,
-/// "mdi:RocketLaunch" → the Material Design Icons path of that .
+/// Converts an icon key to its geometry: "mdi:RocketLaunch" → the Material Design Icons path of that
+/// ; a legacy bare key ("Rocket") → its entry;
+/// any other bare key → the "Icon.<key>" StreamGeometry resource.
///
public class IconKeyConverter : IValueConverter
{
/// Key prefix marking a Material Design Icons kind.
public const string MdiPrefix = "mdi:";
+ /// Bare keys stored in the DB by older versions → the MDI kind that replaces their hand-drawn geometry.
+ public static readonly IReadOnlyDictionary LegacyKinds = new Dictionary
+ {
+ ["Dot"] = MaterialIconKind.CircleMedium,
+ ["Folder"] = MaterialIconKind.FolderOutline,
+ ["Star"] = MaterialIconKind.StarOutline,
+ ["Sun"] = MaterialIconKind.WeatherSunny,
+ ["Calendar"] = MaterialIconKind.CalendarOutline,
+ ["Clock"] = MaterialIconKind.ClockOutline,
+ ["Inbox"] = MaterialIconKind.InboxOutline,
+ ["Mail"] = MaterialIconKind.EmailOutline,
+ ["Text"] = MaterialIconKind.NoteTextOutline,
+ ["Search"] = MaterialIconKind.Magnify,
+ ["Grid"] = MaterialIconKind.ViewGridOutline,
+ ["Code"] = MaterialIconKind.CodeTags,
+ ["Terminal"] = MaterialIconKind.Console,
+ ["Database"] = MaterialIconKind.DatabaseOutline,
+ ["Server"] = MaterialIconKind.ServerOutline,
+ ["Chip"] = MaterialIconKind.Chip,
+ ["Bolt"] = MaterialIconKind.LightningBoltOutline,
+ ["Wand"] = MaterialIconKind.AutoFix,
+ ["Flask"] = MaterialIconKind.FlaskOutline,
+ ["Shield"] = MaterialIconKind.ShieldOutline,
+ ["Activity"] = MaterialIconKind.Pulse,
+ ["Eye"] = MaterialIconKind.EyeOutline,
+ ["Check"] = MaterialIconKind.CheckCircleOutline,
+ ["Warning"] = MaterialIconKind.AlertOutline,
+ ["Chart"] = MaterialIconKind.ChartLineVariant,
+ ["Rocket"] = MaterialIconKind.RocketLaunchOutline,
+ ["Bulb"] = MaterialIconKind.LightbulbOutline,
+ ["Broom"] = MaterialIconKind.Broom,
+ ["AgentSuggested"] = MaterialIconKind.CreationOutline,
+ ["Settings"] = MaterialIconKind.Cog,
+ ["Box"] = MaterialIconKind.PackageVariantClosed,
+ ["Truck"] = MaterialIconKind.TruckOutline,
+ ["MapPin"] = MaterialIconKind.MapMarkerOutline,
+ ["Camera"] = MaterialIconKind.CameraOutline,
+ ["Image"] = MaterialIconKind.ImageOutline,
+ ["Barcode"] = MaterialIconKind.Barcode,
+ ["Skull"] = MaterialIconKind.SkullOutline,
+ };
+
public static IconKeyConverter Instance { get; } = new();
private static readonly Dictionary MdiCache = new();
@@ -27,6 +70,9 @@ public class IconKeyConverter : IValueConverter
if (key.StartsWith(MdiPrefix, StringComparison.Ordinal))
return MdiGeometry(key[MdiPrefix.Length..]);
+ if (LegacyKinds.TryGetValue(key, out var legacy))
+ return MdiGeometry(legacy.ToString());
+
var resourceKey = $"Icon.{key}";
if (Application.Current?.TryGetResource(resourceKey, null, out var res) == true)
return res as StreamGeometry;
diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs
index 8e610a61..4ce086fe 100644
--- a/src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs
+++ b/src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs
@@ -315,13 +315,13 @@ public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
var smart = new[]
{
- new ListNavItemViewModel { Id = "smart:my-day", Name = Loc.T("vm.lists.smartMyDay"), Kind = ListKind.Smart, IconKey = "Sun" },
- new ListNavItemViewModel { Id = NotesListId, Name = Loc.T("vm.lists.smartNotes"), Kind = ListKind.Smart, IconKey = "Text" },
- new ListNavItemViewModel { Id = "smart:important", Name = Loc.T("vm.lists.smartImportant"), Kind = ListKind.Smart, IconKey = "Star" },
- new ListNavItemViewModel { Id = "smart:planned", Name = Loc.T("vm.lists.smartPlanned"), Kind = ListKind.Smart, IconKey = "Calendar" },
- new ListNavItemViewModel { Id = "virtual:queued", Name = Loc.T("vm.lists.virtualQueue"), Kind = ListKind.Virtual, IconKey = "Inbox" },
- new ListNavItemViewModel { Id = "virtual:running", Name = Loc.T("vm.lists.virtualRunning"), Kind = ListKind.Virtual, IconKey = "Activity" },
- new ListNavItemViewModel { Id = "virtual:review", Name = Loc.T("vm.lists.virtualReview"), Kind = ListKind.Virtual, IconKey = "Eye" },
+ new ListNavItemViewModel { Id = "smart:my-day", Name = Loc.T("vm.lists.smartMyDay"), Kind = ListKind.Smart, IconKey = "mdi:WeatherSunny" },
+ new ListNavItemViewModel { Id = NotesListId, Name = Loc.T("vm.lists.smartNotes"), Kind = ListKind.Smart, IconKey = "mdi:NoteTextOutline" },
+ new ListNavItemViewModel { Id = "smart:important", Name = Loc.T("vm.lists.smartImportant"), Kind = ListKind.Smart, IconKey = "mdi:StarOutline" },
+ new ListNavItemViewModel { Id = "smart:planned", Name = Loc.T("vm.lists.smartPlanned"), Kind = ListKind.Smart, IconKey = "mdi:CalendarOutline" },
+ new ListNavItemViewModel { Id = "virtual:queued", Name = Loc.T("vm.lists.virtualQueue"), Kind = ListKind.Virtual, IconKey = "mdi:InboxOutline" },
+ new ListNavItemViewModel { Id = "virtual:running", Name = Loc.T("vm.lists.virtualRunning"), Kind = ListKind.Virtual, IconKey = "mdi:Pulse" },
+ new ListNavItemViewModel { Id = "virtual:review", Name = Loc.T("vm.lists.virtualReview"), Kind = ListKind.Virtual, IconKey = "mdi:EyeOutline" },
};
foreach (var s in smart) { Items.Add(s); SmartLists.Add(s); }
diff --git a/src/ClaudeDo.Ui/ViewModels/Modals/ListSettingsModalViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Modals/ListSettingsModalViewModel.cs
index 88437b05..85f64c37 100644
--- a/src/ClaudeDo.Ui/ViewModels/Modals/ListSettingsModalViewModel.cs
+++ b/src/ClaudeDo.Ui/ViewModels/Modals/ListSettingsModalViewModel.cs
@@ -157,12 +157,11 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
/// Default icon for folders, kept apart from so lists never look like folders.
public const string DefaultFolderIcon = "Folder";
- // Curated subset of the Icon.* geometry library in IslandStyles.axaml — window chrome and
- // pure stroke icons (PathIcon fills, so those render invisible) are deliberately left out.
- // Grouped by theme so the picker grid reads in rows; every entry needs a matching
- // "Icon." StreamGeometry or it renders as an empty slot.
- public static readonly string[] IconOptions =
- [
+ // Curated picker set, grouped by theme so the grid reads in rows. Each legacy key is offered as its
+ // MDI replacement (IconKeyConverter.LegacyKinds); "Dot" and "Folder" stay bare because they are the
+ // default-icon constants the sidebar compares against.
+ public static readonly string[] IconOptions = new[]
+ {
// general
"Dot", "Folder", "Star", "Sun", "Calendar", "Clock", "Inbox", "Mail", "Text", "Search", "Grid",
// dev
@@ -171,7 +170,7 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
"Activity", "Eye", "Check", "Warning", "Chart", "Rocket", "Bulb", "Broom", "AgentSuggested", "Settings",
// things
"Box", "Truck", "MapPin", "Camera", "Image", "Barcode", "Skull",
- ];
+ }.Select(k => k is DefaultIcon or DefaultFolderIcon ? k : IconKeyConverter.MdiPrefix + IconKeyConverter.LegacyKinds[k]).ToArray();
/// Cap on search hits so the picker's WrapPanel stays responsive.
public const int MaxIconResults = 200;
@@ -195,9 +194,10 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
// 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.
+ /// True for a curated key, a legacy bare key still stored in the DB, or an existing "mdi:" kind.
public static bool IsKnownIcon(string? key) =>
key is not null && (IconOptions.Contains(key)
+ || IconKeyConverter.LegacyKinds.ContainsKey(key)
|| (key.StartsWith(IconKeyConverter.MdiPrefix, StringComparison.Ordinal)
&& Enum.TryParse(key[IconKeyConverter.MdiPrefix.Length..], out _)));
diff --git a/tests/ClaudeDo.Ui.Tests/ViewModels/ListSettingsModalViewModelTests.cs b/tests/ClaudeDo.Ui.Tests/ViewModels/ListSettingsModalViewModelTests.cs
index bb36e18d..4397903f 100644
--- a/tests/ClaudeDo.Ui.Tests/ViewModels/ListSettingsModalViewModelTests.cs
+++ b/tests/ClaudeDo.Ui.Tests/ViewModels/ListSettingsModalViewModelTests.cs
@@ -1,6 +1,8 @@
using ClaudeDo.Data;
using ClaudeDo.Localization;
+using ClaudeDo.Ui.Converters;
using ClaudeDo.Ui.Localization;
+using Material.Icons;
using ClaudeDo.Ui.ViewModels.Modals;
using Microsoft.EntityFrameworkCore;
@@ -88,7 +90,7 @@ public class ListSettingsModalViewModelTests
vm.IconSearch = "rocket";
Assert.Contains("mdi:RocketLaunch", vm.Icons);
- Assert.DoesNotContain("Star", vm.Icons);
+ Assert.DoesNotContain("mdi:StarOutline", vm.Icons);
vm.SelectedIconOption = "mdi:RocketLaunch";
vm.SelectedIconOption = null; // what the ListBox pushes when Icons is swapped
@@ -102,6 +104,33 @@ public class ListSettingsModalViewModelTests
Assert.False(ListSettingsModalViewModel.IsKnownIcon("mdi:NoSuchIcon"));
}
+ [Fact]
+ public void IsKnownIcon_AcceptsEveryLegacyBareKeyAndCuratedKey()
+ {
+ Assert.True(ListSettingsModalViewModel.IsKnownIcon("Rocket"));
+ Assert.All(IconKeyConverter.LegacyKinds.Keys, k => Assert.True(ListSettingsModalViewModel.IsKnownIcon(k), k));
+ Assert.All(ListSettingsModalViewModel.IconOptions, k => Assert.True(ListSettingsModalViewModel.IsKnownIcon(k), k));
+ Assert.False(ListSettingsModalViewModel.IsKnownIcon("NoSuchBareKey"));
+ }
+
+ [Fact]
+ public void IconKeys_ResolveToMdiPathData()
+ {
+ // StreamGeometry.Parse (the converter's last step) needs a render interface, i.e. a bootstrapped
+ // Avalonia app, so this checks the step before it: key -> MaterialIconKind -> non-empty path data.
+ var keys = ListSettingsModalViewModel.IconOptions
+ .Concat(IconKeyConverter.LegacyKinds.Keys)
+ .Concat(["mdi:WeatherSunny", "mdi:NoteTextOutline", "mdi:StarOutline", "mdi:CalendarOutline",
+ "mdi:InboxOutline", "mdi:Pulse", "mdi:EyeOutline"]);
+ Assert.All(keys, k =>
+ {
+ var kind = k.StartsWith(IconKeyConverter.MdiPrefix, StringComparison.Ordinal)
+ ? Enum.Parse(k[IconKeyConverter.MdiPrefix.Length..])
+ : IconKeyConverter.LegacyKinds[k];
+ Assert.False(string.IsNullOrEmpty(MaterialIconDataProvider.GetData(kind)), k);
+ });
+ }
+
[Fact]
public void Color_SwatchAndCustomStaySynced()
{