Skip to content

feat(shortcuts): '?' overlay listing active shortcuts, with chord inspector#298

Open
Stvad wants to merge 8 commits into
masterfrom
claude/global-shortcuts-help-plugin-hp2bt3
Open

feat(shortcuts): '?' overlay listing active shortcuts, with chord inspector#298
Stvad wants to merge 8 commits into
masterfrom
claude/global-shortcuts-help-plugin-hp2bt3

Conversation

@Stvad

@Stvad Stvad commented Jul 2, 2026

Copy link
Copy Markdown
Owner

Doom-style shortcut help, built as a plugin (src/plugins/shortcut-help/, toggle id system:shortcut-help).

What it does

  • ? opens the overlay (global action; Shift+? and bare ? chords). It lists every keyboard binding of the currently active contexts, grouped by context in dispatcher precedence order (compareContexts), with modal shadowing marked via computeInstallableContexts — so it's a truthful mirror of what a keypress would actually do right now, not a hand-maintained cheat sheet. Typing ? inside an editor is untouched (the coordinator's editable-target filter already drops it); from edit mode the overlay is reachable via the command palette, and the palette's edit-mode keepalive dance is reused so EDIT_MODE_CM stays listed and focus returns on close.
  • Pressing any chord while open inspects it instead of running it (capture-phase window listener swallows keydown/keyup ahead of the coordinator). The detail panel shows the winning action, its context, the chord, the contributing plugin (facet contribution source), and the effective handler's source text on demand — plus lower-precedence/shadowed candidates for the same chord. Clicking a row inspects it too.
  • Key sequences get which-key narrowing: pressing a live prefix (g of g g, y of y c/y l) collapses the list to just the continuations; an unbound chord flashes as unmatched. Escape clears inspector state first, then closes.

On "a standard way of doing key sequences": that already exists — tinykeys space-separated chords (keys: 'g g'), used by vim-normal-mode and the y c/y l block actions, with sequence-aware canonicalisation in canonicalizeChord.ts. This PR builds on it (parseChord drives the prefix matching) rather than adding a second mechanism. A possible follow-up is which-key narrowing while the overlay is closed (auto-popup on a pending prefix), which would need the coordinator to expose its sequence state.

How

  • model.ts — pure, DOM-free model: effective actions → per-context binding groups + sequence-aware exact/prefix lookup. Reuses the dispatcher's own precedence/shadowing primitives so the overlay can't drift from dispatch reality. Source attribution comes from raw actionsFacet contributions (runtime.contributionsById).
  • useKeyInspector.ts — capture-phase interception while open; same "raw window listener for a keyboard-capture surface" class as the reconciler's hold-binding observer, not a new UI event bus.
  • ShortcutHelpOverlay.tsxappMountsFacet dialog on a createToggleStore (the blessed toggle-surface pattern); external callers go through runActionById('shortcut_help').

Verification

  • yarn run check green (compile + lint + 4212 tests, incl. 15 new: model grouping/shadowing/sequence matching + inspector capture/Escape semantics).
  • Driven live via Playwright against the dev app (local-only login): ? opens the overlay; Ctrl+K while open shows the palette binding (winner + context + from command-palette + handler-source disclosure) without opening the palette; unbound chords flash unmatched; Escape clears then closes; only genuinely active contexts are listed (fresh workspace = Global only; focusing a block adds its contexts).

🤖 Generated with Claude Code

https://claude.ai/code/session_01H1o2WVuYgBYu1S4yctLrke

…pector

Doom-style shortcut help as a plugin (system:shortcut-help):

- '?' (global action, Shift+?/? chords) opens an overlay listing every
  keyboard binding of the currently-ACTIVE contexts, grouped by context
  in dispatcher precedence order (compareContexts) with modal shadowing
  marked (computeInstallableContexts) — a truthful mirror of what a
  keypress would do, not a hand-maintained cheat sheet.
- While open, a capture-phase window listener swallows keydown/keyup, so
  pressing any chord INSPECTS it instead of running it: a detail panel
  shows the winning action, its context, the contributing plugin (facet
  contribution source), and the effective handler's source on demand,
  plus lower-precedence/shadowed candidates for the same chord.
- Sequence chords ('g g', 'y c') get which-key narrowing: a live prefix
  collapses the list to its continuations; an unbound chord flashes as
  unmatched. Escape clears inspector state first, then closes.
- Same edit-mode keepalive dance as the command palette so opening from
  edit mode keeps EDIT_MODE_CM listed and returns focus on close.

Matching reuses the shared chord canonicaliser (parseChord) over the
canonical chord strings chordFromEvent emits, so the overlay, the
keybindings editor, and the dispatcher agree on chord identity.

Verified live via Playwright against the dev app: overlay opens on '?',
Ctrl+K shows the palette binding without opening the palette, Escape
clears-then-closes, and only genuinely active contexts are listed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H1o2WVuYgBYu1S4yctLrke
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

claude and others added 5 commits July 2, 2026 08:30
…rtcut-help fixes

Behavior-preserving extractions surfaced by the adversarial/quality review
of the shortcut-help plugin, landed first so the fix commit can consume
them:

- toChordArray moves to canonicalizeChord.ts as the single copy;
  applyKeybindingOverrides and keybindingConflicts drop their private
  duplicates (shortcut-help would have been the third).
- modifierPreview is exported from keyCapture.ts next to chordFromEvent —
  the function it must stay in lockstep with — replacing KeyCaptureInput's
  hand-mirrored inline copy (shortcut-help's inspector is the second
  consumer).
- The command palette's edit-mode 'yield-focus' keepalive dance (activeRef
  mirror + open-keyed layout effect + guarded focus return) becomes
  useEditModeYieldKeepalive so the subtle focus-return contract lives in
  exactly one place; the palette and the shortcut-help overlay both call it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H1o2WVuYgBYu1S4yctLrke
…-flight gestures

Adversarial-review fixes for the '?' overlay (three agents, findings
confirmed against the dispatcher before fixing):

- BLOCKER: typing '?' in a note opened the overlay and ate the character.
  The coordinator's dispatch gate is opted in wholesale by any active
  context's eventFilter (EDIT_MODE_CM accepts every keydown inside
  .cm-editor), so the editable-target typing filter never protected the
  bare GLOBAL chord — this was the app's first bare printable GLOBAL
  binding. The handler now declines (sync `false`) on keyboard triggers
  from editable targets, falling through so the character is typed.
- MAJOR: the inspector re-derived chord identity from canonical strings
  and diverged from tinykeys: code-form bindings (Control+Shift+Backquote
  and both Bracket chords in daily-notes) reported "nothing bound" on all
  platforms, literal-Control bindings went dark on Windows/Linux ($mod
  folding), and Win+K falsely claimed the $mod+k palette binding.
  Matching now buffers the real KeyboardEvents and delegates to tinykeys'
  own parseKeybinding/matchKeybindingPress — parity by construction —
  with chordFromEvent kept for display only. A corpus parity test pits
  matchPressedSequence against createKeybindingsHandler per chord shape.
- MAJOR: key lifecycles straddling the open boundary. Armed hold timers
  (date-scrub 's') could fire while the overlay was open (their
  cancelling keyup was swallowed), and a swallowed keyup-phase commit
  could wedge scrub mode. Opening now cancels armed holds via a new
  reconciler hold registry, and keyups are swallowed only for keys
  pressed while open — a release of a key held from before propagates so
  in-flight gestures terminate normally.
- Inspector state resets when the binding set is rebuilt (stale
  HelpBinding identities blanked the narrowed list); the platform copy
  chord with a live selection keeps its native default so the handler
  source is copyable; the modifier preview clears on window blur;
  AltGraph/CapsLock/NumLock/Dead no longer count as chord keys
  (keyCapture's isModifierOnly — also fixes bogus captures in the
  keybindings editor); the match panel no longer claims "lower
  precedence" for phase-different twins.

Deliberate divergences now documented instead of silent: no 1s sequence
timeout while browsing continuations, and dispatch-time gates
(canDispatch/deps/decline fall-through) are not simulated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H1o2WVuYgBYu1S4yctLrke
…ycle edges

Round-2 adversarial findings (all minor/nit — round-1 majors held):

- A press that breaks a pending sequence but has its own binding no
  longer flashes "nothing bound": the dispatcher keeps per-binding
  matcher state ('g' then $mod+k still opens the palette), so the
  inspector retries progressively shorter suffixes of its buffer before
  declaring a chord unbound.
- The pressed-while-open keyup ledger is written before the copy-chord
  early-return, so a $mod+c keyup can't leak past the swallow.
- The overlay subscribes to the keybinding-overrides facet change
  listener (same reason HotkeyReconciler does): overrides arrive in
  place without a runtime identity change, and a mid-open remap would
  otherwise leave the overlay listing and matching stale chords.
- Hold bindings with sequence chords are omitted from the listing —
  installHoldBinding refuses to install them (latent; none in tree).
- holdRegistry rides on CallbackSet per the repo's prefer-callback-set
  rule, which also isolates a throwing cancel from the rest of the
  fan-out.
- Test gaps closed: multi-chord bindings are matched on their second
  chord; the tinykeys parity table pins the expected direction per case
  so matched double-breakage can't read as agreement.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H1o2WVuYgBYu1S4yctLrke
…chord display polish

Quality-review batch, behavior-preserving except chord RENDERING:

- Delete parseChord / ChordSequence / ChordDescriptor / KeyChordDescriptor
  from canonicalizeChord.ts (and their tests). The shortcut-help rewrite
  moved keyboard-event matching onto tinykeys' own parser/matcher, which
  re-orphaned this hand-rolled family — and its divergence from tinykeys
  (no event.code fallback, different $mod resolution) is exactly the bug
  class that rewrite fixed, so leaving it exported invites the next
  consumer to reintroduce it. A module note now points matchers at
  tinykeys; the string canonicalisers and pointer/touch descriptors stay.
- chordFromEvent and modifierPreview now share one modifierParts core —
  the lockstep their doc comments pleaded for becomes structural.
- formatChord: punctuation code names render as their glyphs
  (Control+Shift+Backquote → ⌃⇧` instead of ⌃⇧Backquote), and literal
  Control spells out as 'Ctrl' off-Mac, matching how $mod renders there —
  an overlay list no longer shows the same physical key two ways.
- installHoldBinding's timer body reuses cancel() for the disarm
  bookkeeping instead of duplicating it.
- action.test's keydownOn drops the listener dance (event.target persists
  after dispatch).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H1o2WVuYgBYu1S4yctLrke
@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://Stvad.github.io/knowledge-medium/pr-preview/pr-298/

Built to branch gh-pages at 2026-07-03 19:25 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

claude added 2 commits July 3, 2026 07:33
DialogContent is a grid, and grid items' default min-width:auto let the
handler-source <pre>'s unbreakable lines widen the content track past the
dialog — the match panel, its chord chip, and the second shortcut column
all spilled onto the page. min-w-0 on the dialog's direct children makes
the track hold the dialog width and the <pre> scroll internally instead.

Verified in the live app: with the handler source expanded, the dialog's
scrollWidth equals its clientWidth and the <pre> stays inside its bounds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H1o2WVuYgBYu1S4yctLrke
The `?` overlay's match panel can now remap the inspected action's
keyboard shortcut in place, so discovering a binding and changing it are
the same gesture — no trip to the keybindings settings block.

- Match panel gains Rebind / Reset-to-default / Remove-shortcut controls.
  Rebind puts the inspector into a capture mode: the next chord is
  recorded (not inspected — same swallow rules keep it from firing the
  action) and written to the user's keybinding overrides.
- Before writing, the proposed binding is previewed against the current
  overrides for conflicts; a post-write notice confirms the change,
  warns about any other action that would also fire the chord, and links
  to the full keyboard settings. The notice survives the model rebuild
  the write triggers (the overlay already live-subscribes to override
  changes), so the list refreshes to the new chord underneath it.
- Writes reuse the keybindings-settings storage: a new overrideStore
  module centralises the storage↔facet mapping and the pure
  add/replace/remove transforms (previously duplicated in the editor and
  effect) plus the block read/write I/O, so both surfaces share one
  source of truth for the override format.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H1o2WVuYgBYu1S4yctLrke
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.

2 participants