Skip to content

fix(server): match keybinding rules whose key uses an alias spelling - #5668

Open
ChristmasSun wants to merge 1 commit into
pingdotgg:mainfrom
ChristmasSun:fix/keybinding-alias-replacement
Open

fix(server): match keybinding rules whose key uses an alias spelling#5668
ChristmasSun wants to merge 1 commit into
pingdotgg:mainfrom
ChristmasSun:fix/keybinding-alias-replacement

Conversation

@ChristmasSun

@ChristmasSun ChristmasSun commented Aug 8, 2026

Copy link
Copy Markdown

What changed

Keybinding rule comparison now falls back to comparing the parsed shortcut when two rules spell their key differently.

Why

A key can be written more than one way. parseKeybindingShortcut normalizes "esc""escape" and "space"" ", while the settings UI renders a stored shortcut back using the alias:

// apps/web/src/components/settings/KeybindingsSettings.logic.ts:52
parts.push(shortcut.key === " " ? "space" : shortcut.key === "escape" ? "esc" : shortcut.key);

So editing a rule bound to Escape sends a replace target of "esc" for a rule persisted as "escape". Comparison used raw string equality:

// apps/server/src/keybindings.ts
left.command === right.command && left.key === right.key && ...

"esc" !== "escape", so the replace target never matches, the original rule survives alongside the new one, and the command ends up bound twice.

No default binding currently uses an aliased key, which is why this has gone unnoticed — every shipped default is a mod+… combination whose stored and rendered spellings are identical.

How to reproduce

Note that the recorder cannot capture bare Escape: it uses Escape to cancel recording (KeybindingsSettings.tsx:807), which is the behavior you want. So reaching this state today means writing the rule to disk directly:

  1. In keybindings.json, bind any command to escape (or space — the other aliased key).
  2. In Settings → Keybindings, edit that row and record a different shortcut.
  3. Reopen Settings → Keybindings.

The command now appears twice: once on the original Escape and once on the new shortcut.

This is reachable without hand-editing anything as soon as a default binding uses one of those keys — see #5669, which is how I ran into it.

Test

Added a case that persists a rule as escape and replaces it targeting esc. It fails on main and passes with this change.

before (keybind duplicates after trying to change off of escape):
image

after: (doesnt duplicate as is desired)
image

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • Regression test that fails without the fix

A key can be written more than one way: parseKeybindingShortcut normalizes
"esc" to "escape" and "space" to " ". The settings UI renders a stored
rule back using the alias, so editing a rule bound to Escape sent a replace
target of "esc" for a rule persisted as "escape". Rule comparison used raw
string equality, so the target never matched, the original rule survived, and
the command ended up bound twice.

Compare the parsed shortcut when the raw keys differ. No default binding uses
an aliased key today, which is why this has gone unnoticed.
@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 134ac7ab-d0e0-4de5-928c-25469a57be71

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:S 10-29 changed lines (additions + deletions). labels Aug 8, 2026
return leftKey !== null && leftKey === canonicalKeybindingKey(right);
}

function canonicalKeybindingKey(rule: KeybindingRule): string | null {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 High src/keybindings.ts:115

canonicalKeybindingKey does not canonicalize the " " (literal space) alias for the space key. parseKeybindingShortcut trims each token, so rule.key === " " is parsed as the "+" key, while "space" parses and re-encodes as "space". As a result, isSameKeybindingRule treats "space" and " " as different rules, so an upsert or remove that uses one alias when the stored rule uses the other fails to match the existing entry and leaves the old binding persisted. Consider normalizing the space alias before or within parseKeybindingShortcut so both spellings produce the same canonical key.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/keybindings.ts around line 115:

`canonicalKeybindingKey` does not canonicalize the `" "` (literal space) alias for the space key. `parseKeybindingShortcut` trims each token, so `rule.key === " "` is parsed as the `"+"` key, while `"space"` parses and re-encodes as `"space"`. As a result, `isSameKeybindingRule` treats `"space"` and `" "` as different rules, so an upsert or remove that uses one alias when the stored rule uses the other fails to match the existing entry and leaves the old binding persisted. Consider normalizing the space alias before or within `parseKeybindingShortcut` so both spellings produce the same canonical key.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good catch on the helper in isolation, but I believe this is unreachable through any validated input, so I've left it out to keep the diff minimal.

Every path into isSameKeybindingRule carries keys that passed KeybindingValue, which is TrimmedString.check(isMinLength(1), …) — decode trims, so a literal " " key becomes "" and fails validation before comparison:

  • RPC upsert / replace targets: ServerUpsertKeybindingInput / ServerRemoveKeybindingInput use KeybindingValue
  • persisted config: entries decode through KeybindingRule, which uses the same schema, so a hand-edited " " is dropped as a config issue

That leaves "space" as the only representable spelling — and unlike escape/esc, it round-trips losslessly: shortcutToKeybindingInput renders the parsed space back as "space", so stored and replace-target spellings always agree. The escape/esc pair was the only alias where the round-trip is lossy, which is what this PR fixes.

Happy to add the defensive normalization if maintainers prefer it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

@macroscopeapp

macroscopeapp Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

1 blocking correctness issue found. This keybinding alias fix has a High-severity unresolved finding identifying that the space key alias (" ") won't be properly canonicalized, which would leave the same bug pattern for that specific alias. Additionally, the author is a new contributor to this file.

You can customize Macroscope's approvability policy. Learn more.

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

Labels

size:S 10-29 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant