Goal / finish line
Every keyboard binding is declared once in packages/web/src/shortcuts/keymap.ts (or a sibling table); shortcuts.registry.ts rows derive their keycaps from it instead of hardcoding keys: [...], and the 12 raw addEventListener("keydown") sites outside packages/web/src/shortcuts/ either move onto useAppShortcut or keep an entry in KEYDOWN_LISTENER_ALLOWLIST with a reason.
Acceptance
- A test asserts every registry row's keycaps come from the keymap (no literal
keys: arrays for bindings the runtime owns).
KEYDOWN_LISTENER_ALLOWLIST in check-agent-constraints.ts shrinks; entries that remain have reasons.
- The
? legend and the runtime cannot disagree: changing a binding in one place changes both.
Context
Today a binding can live in keymap.ts (12 entries), be hardcoded in shortcuts.registry.ts (62 rows, ~50 not derived), sit inline at one of 52 useAppShortcut() calls, or in a raw keydown listener. The legend silently disagrees with runtime for the hardcoded rows.
Package scope
web
Verify commands
bun test:web
bun run test:e2e
Approval boundary
allow
Untrusted input
Goal / finish line
Every keyboard binding is declared once in
packages/web/src/shortcuts/keymap.ts(or a sibling table);shortcuts.registry.tsrows derive their keycaps from it instead of hardcodingkeys: [...], and the 12 rawaddEventListener("keydown")sites outsidepackages/web/src/shortcuts/either move ontouseAppShortcutor keep an entry inKEYDOWN_LISTENER_ALLOWLISTwith a reason.Acceptance
keys:arrays for bindings the runtime owns).KEYDOWN_LISTENER_ALLOWLISTincheck-agent-constraints.tsshrinks; entries that remain have reasons.?legend and the runtime cannot disagree: changing a binding in one place changes both.Context
Today a binding can live in
keymap.ts(12 entries), be hardcoded inshortcuts.registry.ts(62 rows, ~50 not derived), sit inline at one of 52useAppShortcut()calls, or in a raw keydown listener. The legend silently disagrees with runtime for the hardcoded rows.Package scope
web
Verify commands
Approval boundary
allow
Untrusted input