You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
A profile created before InitializeAsync() loses the stored profile's name and description, and resolves duplicate stored chords to a different command than a fresh load (gaps in #206) #247
#206 made InitializeAsync() merge a stored profile into a same-id profile already in memory (CreateDefaultProfile() called early, or a profile manager shared through DI), instead of dropping it. MergeStoredBindings (Keybinding/KeybindingManager.cs:160-170) still loses two things on that path:
Name and description are dropped. Only bindings are merged. The in-memory placeholder's Name/Description ("Default" / "Default keybinding profile") stay, and the next SaveAsync writes them over the user's.
Duplicate stored chords resolve differently. The merge walks stored.GetAllChords() in dictionary (file) order and keeps only the first binding per chord (usedChords.Add). A normal load keeps every duplicate and resolves them in ordinal command-id order (Profile.FindCommand, Keybinding/Models/Profile.cs:254), and SetStoredChord's docs say such legacy data should load unchanged. So the same file runs a different command depending on call order. The binding a fresh load would have run gets dropped, and the next save deletes it from disk.
using(KeybindingManagerm=new(dir)){awaitm.InitializeAsync();m.CreateDefaultProfile();m.Profiles.RenameProfile("default","My Keys","Tuned for vim");awaitm.SaveAsync();}using(KeybindingManagerm=new(dir)){m.CreateDefaultProfile();awaitm.InitializeAsync();// create before initProfilep=m.Profiles.GetProfile("default")!;Console.WriteLine($"{p.Name} / {p.Description}");awaitm.SaveAsync();}
Output: Default / Default keybinding profile, which a third launch also reads back from disk. Expected: My Keys / Tuned for vim.
Both cases lose data silently and permanently on a path #206 accepted as supported. A user's renamed profile reverts. With legacy duplicate data, a shortcut changes which command it runs and one binding disappears from disk.
Suggested fix
In MergeStoredBindings:
Take the stored Name/Description (existing.Rename(stored.Name, stored.Description)), at least when the in-memory profile is still the untouched placeholder.
Copy stored bindings so the result matches a fresh load. Walk them in StringComparer.Ordinal command-id order, as FindCommand does. Or, for a chord the in-memory profile doesn't use, copy every stored binding for it with SetStoredChord.
Acceptance criteria
Tests that call CreateDefaultProfile() before InitializeAsync() and check that (a) a stored renamed profile keeps its name and description through a save and reload, and (b) the duplicate-chord data above runs a.cmd and keeps both bindings, the same as init-first.
Notes: To match a fresh load, walk the stored bindings in ordinal command-id order, or copy every binding for a chord with SetStoredChord. That avoids a second resolution rule.
Activity
matt-edmondson commentedon Nov 12, 2026
Triage
SaveAsyncmakes the loss permanent.KeybindingManager.MergeStoredBindings(follow-up to Creating a profile before InitializeAsync() silently discards the stored profile with that id, and the next SaveAsync overwrites the user's bindings on disk #206)InitializeAsyncreconciles an in-memory placeholder with stored data.SetStoredChord. That avoids a second resolution rule.Generated by Claude Code