Skip to content

Add Cmd-K command palette and keyboard router#59

Open
Cellcote wants to merge 1 commit intomainfrom
feat/keyboard_shortcuts
Open

Add Cmd-K command palette and keyboard router#59
Cellcote wants to merge 1 commit intomainfrom
feat/keyboard_shortcuts

Conversation

@Cellcote
Copy link
Copy Markdown
Owner

@Cellcote Cellcote commented May 2, 2026

Summary

  • New Commands/ namespace: AppCommand records, a CommandRegistry, a chord-based KeyMap with VS Code-flavoured parsing (cmd → Meta on macOS, Ctrl elsewhere), and a window-level KeyRouter that grabs modifier-bearing chords on the tunnel phase so they win over a focused TextBox.
  • CommandPaletteModal follows the existing modal pattern (lazy-instantiated by MainWindow, backdrop dismiss, Escape to close). Subsequence fuzzy match, ListBox-driven selection, auto-focused query input, Up/Down/Enter/Esc.
  • ShellVm gains a small initial command catalog (10 actions: new session, cancel turn, view switch, prefs, about, ...) plus dynamic "Switch to " entries materialised on demand by the palette VM.
  • Only one default binding ships: Cmd-Kpalette.open. Designing the rest of the shortcut catalog is intentionally deferred until we can see the palette in action and decide what's worth a hotkey.
  • User overrides are read from a new keybindings.json settings key via JsonDocument (manual walk, AOT-clean — no reflection-based deserialiser).

Out of scope (future work)

  • Vim-style sidebar nav and chord sequences — KeyChord is a single chord today; the structure leaves room for chord sequences without forcing them now.
  • Composer ergonomics (history recall, @ mentions, vim mode toggle) — separate stage.
  • A keybindings settings UI — overrides work via JSON in the settings table for now; a UI can come once the catalog stabilises.

A new Commands/ namespace centralises a registry of invokable actions, a
chord-based KeyMap with VS Code-flavoured parsing (cmd → Meta on macOS,
Ctrl elsewhere), and a window-level KeyRouter that grabs modifier-bearing
chords on the tunnel phase so they win over a focused TextBox. The palette
modal is mouse-optional: subsequence fuzzy match, ListBox selection,
auto-focused query, Up/Down/Enter/Esc.

Initial bindings ship just Cmd-K → palette.open. The catalog wires up ten
existing ShellVm actions (new session, cancel turn, view switch, prefs,
about, ...) plus dynamic "Switch to <session>" entries the palette
materialises on demand. User overrides are read from a new keybindings.json
settings key via JsonDocument so we stay AOT-clean.
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 2, 2026

Greptile Summary

Adds a Cmd-K command palette with subsequence fuzzy search, a chord-based KeyMap with VS Code-style parsing, a tunnel/bubble KeyRouter, and a 10-entry command catalog in ShellVm. The implementation follows the existing modal pattern and is AOT-clean (manual JSON walk for keybinding overrides, no reflection-based deserialization).

Four P2 findings worth addressing before the UX stabilises:

  • palette.open is registered with CanExecute = Always, so it appears in its own results and selecting it closes then immediately reopens the palette.
  • Keyboard focus is not restored to the previously focused control after the palette is dismissed.
  • IsModifierKey in KeyRouter may be missing Key.LeftMeta/Key.RightMeta for macOS Command key variants.
  • Empty-query scoring returns 1 for every entry, leaving the default open-state order non-deterministic across .NET sort implementations.

Confidence Score: 4/5

Safe to merge; all findings are P2 quality-of-life issues, none are blocking correctness or security.

No P0 or P1 issues found. The four P2 findings (palette.open self-appearance, missing focus restore, possible LeftMeta gap, non-deterministic empty-query order) are real usability rough edges but none cause data loss, crashes, or broken core behaviour. P2-only ceiling is 4/5.

src/Conclave.App/ViewModels/ShellVm.cs (palette.open CanExecute), src/Conclave.App/Commands/KeyRouter.cs (IsModifierKey completeness), src/Conclave.App/Views/Shell/CommandPaletteModal.axaml.cs (focus restore)

Important Files Changed

Filename Overview
src/Conclave.App/Commands/KeyRouter.cs Tunnel/bubble split correctly intercepts modifier chords; IsModifierKey may be missing Key.LeftMeta/Key.RightMeta for macOS Command key.
src/Conclave.App/Commands/FuzzyMatch.cs Subsequence scorer with bonus weights; empty-query short-circuit returns score=1 for all candidates, making the default palette order non-deterministic.
src/Conclave.App/ViewModels/CommandPaletteVm.cs Manages query/results/selection; deferred Post on execute is sound, but palette.open command is visible inside its own results and can cause a close-reopen loop.
src/Conclave.App/ViewModels/ShellVm.cs Adds Commands, KeyMap, palette open/close state, and RegisterCommands; palette.open registered with CanExecute=Always so it appears in its own results.
src/Conclave.App/Views/Shell/CommandPaletteModal.axaml.cs Focus-on-open via deferred Post is correct; focus is not restored to the previously focused element on palette close.
src/Conclave.App/Commands/KeyMap.cs AOT-safe manual JSON walk for user overrides; defaults are minimal and correct.
src/Conclave.App/Commands/AppCommand.cs New record type representing a single invokable command; clean and minimal.
src/Conclave.App/Commands/CommandRegistry.cs Simple dictionary-backed catalog; TryExecute correctly gates on CanExecute before invoking.

Comments Outside Diff (2)

  1. src/Conclave.App/ViewModels/ShellVm.cs, line 592 (link)

    P2 palette.open appears in its own results list

    The palette.open command has CanExecute = Always, so "Open command palette" is visible as a result while the palette is already open. Selecting it triggers CloseCommandPalette() followed by a deferred Post(OpenCommandPalette), so the palette closes and immediately reopens — an unexpected interaction. A simple fix is to gate on !IsCommandPaletteOpen:

    Commands.Register(C("palette.open", "Open command palette", "View",
        () => !IsCommandPaletteOpen, OpenCommandPalette));
  2. src/Conclave.App/Commands/FuzzyMatch.cs, line 65-67 (link)

    P2 Default sort order is undefined when query is empty

    FuzzyMatch.Score("", candidate) returns (1, []) for every candidate — it short-circuits before computing any bonus. When the palette opens with an empty query, all commands and sessions land in pool with Score = 1, and List<T>.Sort uses an unstable comparison that offers no ordering guarantee across runtimes or runs. Users may see the list reorder unexpectedly across opens. Consider using a stable tie-break (e.g., registration order via an index, or an explicit Priority field on AppCommand) so the default presentation is deterministic.

Reviews (1): Last reviewed commit: "Add Cmd-K command palette and keyboard r..." | Re-trigger Greptile

Comment on lines +33 to +36
private void OnBackdropPressed(object? sender, PointerPressedEventArgs e)
{
if (DataContext is ShellVm shell) shell.CloseCommandPalette();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Focus not restored after palette dismissal

CloseCommandPalette() (invoked by backdrop press, Escape, or ExecuteSelected) removes focus from QueryInput but doesn't return it to whatever was focused before the palette opened. After dismissal the user must click or Tab to re-engage the text composer or any other input, which interrupts keyboard-driven workflows. Consider saving the previously focused element before FocusOnOpen() posts the focus change and restoring it in each close path.

Comment on lines +42 to +46
private static bool IsModifierKey(Key k) => k
is Key.LeftCtrl or Key.RightCtrl
or Key.LeftShift or Key.RightShift
or Key.LeftAlt or Key.RightAlt
or Key.LWin or Key.RWin;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Key.LeftMeta / Key.RightMeta missing from IsModifierKey

Avalonia exposes Key.LeftMeta and Key.RightMeta for the macOS Command key (in addition to Key.LWin / Key.RWin). If either value is what Avalonia actually emits when the user holds Command alone on macOS, IsModifierKey returns false, KeyChord.FromEvent builds a chord whose Key is LeftMeta/RightMeta, map.Lookup finds nothing, and the event falls through harmlessly — but the guard's intent (skip bare-modifier keystrokes entirely) is violated. Adding both values keeps the intent sound across platform variations:

private static bool IsModifierKey(Key k) => k
    is Key.LeftCtrl or Key.RightCtrl
    or Key.LeftShift or Key.RightShift
    or Key.LeftAlt or Key.RightAlt
    or Key.LWin or Key.RWin
    or Key.LeftMeta or Key.RightMeta;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant