feat(shortcuts): '?' overlay listing active shortcuts, with chord inspector#298
Open
Stvad wants to merge 8 commits into
Open
feat(shortcuts): '?' overlay listing active shortcuts, with chord inspector#298Stvad wants to merge 8 commits into
Stvad wants to merge 8 commits into
Conversation
…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
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
…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
|
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Doom-style shortcut help, built as a plugin (
src/plugins/shortcut-help/, toggle idsystem: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 viacomputeInstallableContexts— 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.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.gofg g,yofy 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 they c/y lblock actions, with sequence-aware canonicalisation incanonicalizeChord.ts. This PR builds on it (parseChorddrives 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 rawactionsFacetcontributions (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.tsx—appMountsFacetdialog on acreateToggleStore(the blessed toggle-surface pattern); external callers go throughrunActionById('shortcut_help').Verification
yarn run checkgreen (compile + lint + 4212 tests, incl. 15 new: model grouping/shadowing/sequence matching + inspector capture/Escape semantics).?opens the overlay;Ctrl+Kwhile 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