What's wrong
The typical app startup is await manager.InitializeAsync(); manager.RegisterCommands(appCommands);.
InitializeAsync (Keybinding/KeybindingManager.cs:~82-87) registers every command loaded from commands.json first.
CommandRegistry.RegisterCommand (Services/CommandRegistry.cs:23-28) is _commands.TryAdd(...), so each later registration from the app's own code returns false and is ignored.
SaveAsync (KeybindingManager.cs:~118-119) writes the whole registry back, so the stale copy is persisted again.
As a result, the persisted copy of a command always wins over the definition in code, and nothing ever removes a command the app stopped defining.
Reproduction
- v1 of the app: registers
file.save (name "Save", category "File") and tools.legacy bound to Ctrl+L, then calls SaveAsync.
- v2 of the app: calls
InitializeAsync, then registers file.save as name "Save File", category "Document", and no longer defines tools.legacy.
Observed in v2:
RegisterCommands(...) returns 0.
GetCommand("file.save") still returns name Save, category File.
GetCommandsByCategory("Document") returns no commands.
ExecuteChord(Ctrl+L) / FindCommandByChord still return tools.legacy, a command the app no longer has a handler for.
This effectively undoes the intent of #136, which skips unregistered commands: a command removed from code stays "registered" forever through persistence. Users also never see renamed commands or reorganised categories in a settings UI.
Suggested fix
Either:
- Treat the command set as defined by code rather than as persisted state. Persist and restore only bindings and profiles. Loaded commands are used only to keep bindings for ids the app registers again. Or:
- Make a registration from code an upsert that replaces the stored copy, and have
SaveAsync drop commands that weren't registered by the app this session.
Acceptance criteria
- After restart,
RegisterCommand with changed name, description or category updates what GetCommand and GetCommandsByCategory return.
- A command that the app doesn't register after
InitializeAsync isn't returned by ExecuteChord / FindCommandByChord, and isn't written back by SaveAsync. Its binding may be retained or dropped, but the choice is documented.
- A test covers the v1 → v2 sequence above.
What's wrong
The typical app startup is
await manager.InitializeAsync(); manager.RegisterCommands(appCommands);.InitializeAsync(Keybinding/KeybindingManager.cs:~82-87) registers every command loaded fromcommands.jsonfirst.CommandRegistry.RegisterCommand(Services/CommandRegistry.cs:23-28) is_commands.TryAdd(...), so each later registration from the app's own code returnsfalseand is ignored.SaveAsync(KeybindingManager.cs:~118-119) writes the whole registry back, so the stale copy is persisted again.As a result, the persisted copy of a command always wins over the definition in code, and nothing ever removes a command the app stopped defining.
Reproduction
file.save(name "Save", category "File") andtools.legacybound to Ctrl+L, then callsSaveAsync.InitializeAsync, then registersfile.saveas name "Save File", category "Document", and no longer definestools.legacy.Observed in v2:
RegisterCommands(...)returns0.GetCommand("file.save")still returns nameSave, categoryFile.GetCommandsByCategory("Document")returns no commands.ExecuteChord(Ctrl+L)/FindCommandByChordstill returntools.legacy, a command the app no longer has a handler for.This effectively undoes the intent of #136, which skips unregistered commands: a command removed from code stays "registered" forever through persistence. Users also never see renamed commands or reorganised categories in a settings UI.
Suggested fix
Either:
SaveAsyncdrop commands that weren't registered by the app this session.Acceptance criteria
RegisterCommandwith changed name, description or category updates whatGetCommandandGetCommandsByCategoryreturn.InitializeAsyncisn't returned byExecuteChord/FindCommandByChord, and isn't written back bySaveAsync. Its binding may be retained or dropped, but the choice is documented.