Repository navigation
feat(shortcuts): recordable recently-used-tabs gesture and a Search everywhere chord - #398
Merged
Merged
Conversation
AppsOfUse
force-pushed
the
feat/recordable-shortcuts
branch
from
October 12, 2026 12:43
755838f to
ed53bbb
Compare
AppsOfUse
marked this pull request as ready for review
October 13, 2026 07:44
simion
approved these changes
Oct 13, 2026
simion
left a comment
Owner
There was a problem hiding this comment.
Thanks, nice addition. Appreciate you jumping in with two PRs already 🧡
Owner
|
Approved, but this now has merge conflicts (likely from #400 landing). Needs a rebase before merge. |
…hord Recently used tabs and Search everywhere were fixed gestures with only a mode select. Both are now the user's to set. Recently used tabs records a HoldGesture: one modifier to hold (Ctrl, Option/Alt, or Cmd on macOS) and a key to tap, stored as JSON in the new synced pref `recentTabsGesture`. The tap key is recorded by KeyboardEvent.code, because Option turns ` into a dead key and letters into other characters, so `key` would never match its own next press. Absent means Ctrl+Tab, so no migration; a stored value this platform cannot use (Cmd synced to Linux) reads as the default without being rewritten. The recorder refuses two or no modifiers, the OS's own chords (Cmd+Tab, Cmd+Space, Ctrl+Space; Alt+Tab and the Win/Super key elsewhere), Ctrl+letter (readline's) and Option+letter (it types). The walk keeps its window capture listener and matches on code, lands on the held modifier's release, and remembers the gesture it started with. Search everywhere becomes a SHORTCUT_DEFS entry with a chord that ships unbound (defaultBinding: null, BindingMap values nullable), keeps its double-Shift select on the same row, is in PASS_TO_APP so the chord works from a terminal, and shows its chord in the command palette. Conflicts move into a pure `conflicts()`: chord vs chord as before, and gesture vs chord while the gesture is on, flagged on both rows. The mode select reads Off / On, since the keys now sit on the recorder. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…h chord The earlier code-navigation cases leave navme.ts in front, and an editor tab hides the agent's terminal, so the case found no visible terminal to focus. It now waits for the agent, selects its tab, waits for the terminal to lay out, and puts the previous tab back afterwards. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A run showed the command palette over the recently-used-tabs snaps. Why it was open is not established: the settings-navigation case that opens it already closes it through the store, and the Tasks snap taken just before these describes (task-port-range-settings.png) shows no palette. So this is a guard, not a fix for a known cause: both describes now close the palette and wait for it to be gone before they draw anything, and the Search everywhere row is snapped with its recorded chord. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…minal On macOS PASS_TO_APP handed a key to the app whenever it matched a binding, and bindingMatches folds Ctrl into cmd. With Search everywhere bound to Cmd+R, Ctrl+R in a terminal was dropped by xterm, then useShortcuts bailed on the Mac Ctrl chord, so reverse search reached neither the shell nor the dialog. Ctrl+P had the same hole with the shipped Cmd+P. The predicate moves to terminalPassesToApp in lib/shortcuts.ts and on macOS now requires metaKey. Off macOS the rule is unchanged (Shift or Alt on the chord). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"Hold Cmd, tap C" was recorded, and the walk's capture listener then preventDefaulted every Cmd+C: copy was dead app-wide, and the conflict check stayed silent because Cmd+C and Cmd+V are not SHORTCUT_DEFS. The same went for V, X, Z, Q, W and the rest. holdGestureError now refuses Cmd with any letter or digit (holdAppKey). The key refusals carry the held modifier and the keys that do record with it (holdAllowedKeys), so the message leads somewhere that works. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The refusals covered letters and digits only. Ctrl+[ (ESC), Ctrl+\ (SIGQUIT), Ctrl+] and Ctrl+/ were accepted, so were Option+punctuation (it types, and Option+` is the dead key for accented letters), every arrow (word and line navigation, Mission Control), Alt+F4, Alt+` and Alt+punctuation off macOS (Meta to the terminal), and Cmd+`. holdGestureError now refuses, per modifier and platform: - Ctrl: letters, digits and [ \ ] / - ` everywhere - Option (macOS): every key with a character - Alt (off macOS): every key with a character, and Tab, F4, Space, `, F1, F2, F7, F8, F10 as the desktop's - Cmd (macOS): letters, digits, Tab, Space, ` - any: arrows, Home, End, Page Up / Down Enter, Space and Backspace get a message of their own instead of "hold exactly one modifier". Every key refusal names the keys that do record with that modifier (holdAllowedKeys), and a test records each of them, so the advice cannot lead to another refusal. holdTypes used to say "Pick Tab or a symbol key", which was false off macOS and is false now everywhere. Option + ` is refused on macOS too. preventDefault during a walk would stop the dead-key composition, but claiming the key takes accented typing away from every editor, which is the harm the rule is about. A stored Option + ` now reads as the default; the settings e2e case that records it is updated in a later commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The keydown handler pinned the gesture (held.current) and suspended recent-places recording before stepCtrlTab, which returns an idle walk for a repeat. Only a landing undoes either, and a landing needs a live walk. Holding Ctrl+Tab while a dialog closed therefore left the gesture pinned (a rebind in Settings was ignored) and recording suspended for the rest of the session. Both now happen only once the step leaves the walk active. The three listeners move out of the hook into recentTabsWalkHandlers so the key handling is testable without rendering. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The gesture is stored by KeyboardEvent.code, which is right for matching, but it was also displayed and checked for conflicts through the US label of that code. On a German layout Cmd+ö read as Cmd+; and was compared with a Cmd+; chord, a key that is Shift+comma there. The recorder now also keeps the character the key typed (HoldGesture .key, when it is one plain character and Shift or Option did not change it). holdKeyGlyph prints it, and conflicts compares it with the chords, which bindingFromEvent records by character too. A stored value without the field still loads and falls back to the US label; a bad field is dropped rather than discarding the gesture. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… Cmd chord conflicts() folded Ctrl into Cmd for the gesture check, so on macOS a Ctrl+; walk flagged a Cmd+; chord on both rows. The walk takes only the Ctrl press there, and the chord's own Cmd press keeps working, so the warning described nothing. The check now uses the modifier a cmd chord is pressed with on the platform: Cmd on macOS, Ctrl elsewhere. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
holdModifierName said "Win" off macOS, so a Linux user pressing Super+Tab in the recorder was told "Win+Tab belongs to the system". It now says Super on Linux, Win on Windows, matching the terminal note. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A synced gesture this platform cannot use (Cmd from a Mac on Linux, or one recorded before a rule refused it) fell back to Ctrl+Tab without being rewritten, which is right, but Settings showed Ctrl+Tab as the untouched default with no reset and no word about why. readHoldGesture now returns the refused gesture beside the one in use; prefs keeps it as recentTabsGestureRefused. The row prints a short note naming both and shows the reset button, and the reset writes the default over the stored value even though it is the gesture already running. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
loadShortcuts reads a stored null as "no override", which is what keeps search-everywhere unbound across a restart (the whole map is written on every change, so its null is always there) and lets a null on a bound command fall back to its default. Nothing pinned it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd stale - docs/shortcuts.md said both non-chord triggers are now the user's to set, and later that double-Shift cannot be rebound. It now says which part of each is the user's. - The useShortcuts header says where a recorded search chord works from a terminal (macOS; elsewhere only with Shift or Alt). - prefs.ctrlTabMode's doc still described a fixed Ctrl+Tab. - ShortcutsHelpDialog's Row.fixed held the double-Shift mode label, and its comment explained a recorder that no longer is missing. Renamed to doubleShiftLabel. - docs/shortcuts.md wrapped to 78 columns (tables excepted). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
settings.e2e.ts: the gesture recorder refuses Ctrl+R, Ctrl+Enter, Ctrl+[, Cmd+C and Option/Alt+` with the new messages (each naming the keys that do work), records Cmd+o-umlaut (Ctrl off macOS) by code and prints the character, flags a chord on that character and not on the US label, and shows the note and reset for a saved gesture that cannot be used here. The Option+` recording case is gone: that gesture is now refused. The closePalette comment no longer claims a cause nobody established. codenav.e2e.ts: the search chord case uses a pure Cmd+Enter on macOS. xterm sends Enter for Cmd and cancels the event, so the case fails if search-everywhere is dropped from PASS_TO_APP; a Cmd+letter would bubble past xterm and pass either way. New macOS case: with Cmd+R bound, Ctrl+R reaches the PTY as 0x12 and Cmd+R opens the dialog. tabs-layout.e2e.ts: the rebound walk uses an Option hold the recorder accepts (Option+Tab on macOS, Alt+F9 elsewhere), and a new case checks that walking on it writes no byte to the PTY, after a typed-x control proves the rig sees writes. The settle before each "the walk did not move" assertion is written at the call site instead of inside expectStillOn, and fixedWaits counts all four (it said one while the helper ran four times). app.e2e.ts: the sheet prints a gesture recorded on a German layout. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…f waiting for it waitGone on the palette input timed out in some full-file runs (14 cases failed from there on), so the guard now closes it via dismissOverlays and hides leftover nodes; cause not proven. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The chord recorder accepts Option with any key, so the hold gesture should not be stricter. On macOS the recorder again refuses Option with a letter or digit only (holdTypes), and accepts Option + punctuation, Option + ` (the dead key) included. Off macOS the Alt rules stay as they were. Option + punctuation types a character, and Option + ` is the dead key for accented letters: while such a gesture is on, the walk claims the key and the character is gone from every editor and prompt. That is a trade-off the user makes by recording it, and docs/shortcuts.md now says so plainly. holdAllowedKeys names Tab ` ; ' , . = for Option, the holdTypes message says "a letter or digit", and a stored Option + ` loads on macOS again. The settings e2e case records Option + ` on macOS (refuses Alt + ` elsewhere), and the unusable-gesture case seeds Option / Alt + E, which every platform refuses. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI failed both on other platforms: - The Option-hold PTY case got ESC [ O / ESC [ I on Windows. Those are xterm's focus reports from switching tasks, not the tapped key; CI's Windows window has focus, the occluded local macOS one does not. The assertion now drops focus reports before checking for bytes. - The search-everywhere recorder read "Not set\n" on Linux and macOS CI (innerText), so the two Not set checks compare trimmed text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e rebase main's matchesTerminalAppBinding took Binding | undefined, and this branch lets a stored binding be null (unbound). Widen it to match bindingMatches, and drop the FIXED_SHORTCUTS import the branch renamed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AppsOfUse
force-pushed
the
feat/recordable-shortcuts
branch
from
October 13, 2026 13:07
814a962 to
06f3ebc
Compare
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.
Summary
Recently used tabs and Search everywhere were fixed gestures with only a mode select.
Both are now the user's to set in Settings > Shortcuts.
Recently used tabs records a hold gesture: one modifier to hold (Ctrl, Option/Alt, or Cmd on macOS) and a key to tap, stored in the new synced pref
recentTabsGesture.The tap key is recorded by
KeyboardEvent.code, because Option turns`into a dead key and letters into other characters, sokeywould never match its own next press.Absent means Ctrl+Tab, so there is no migration.
The recorder refuses, and says why, naming keys that do work with that modifier:
two or no modifiers; the OS's own chords (Cmd+Tab, Cmd+Space, Cmd+
, Ctrl+Space; Alt+Tab, Alt+F4, Alt+and Win/Super elsewhere);Ctrl with a letter, digit, or a key that makes a control character (
[ \ ] / -, readline's and the terminal's);Cmd with a letter or digit (copy, paste, quit...);
Option with a letter or digit on macOS (it types), and Alt with any character key elsewhere (Meta to the terminal);
arrows, Home, End and Page keys; Enter, Space and Backspace.
Option with punctuation is accepted on macOS, matching the chord recorder; Option+
then takes the à/è dead key while the gesture is on, whichdocs/shortcuts.md` states as the user's trade-off.A synced gesture this platform cannot use falls back to Ctrl+Tab without rewriting the stored value, and the row says so with a reset.
Search everywhere becomes a normal shortcut entry with a chord that ships unbound, and keeps its double-Shift select on the same row.
It is in
PASS_TO_APP, so on macOS the chord works from inside a terminal (elsewhere only with Shift or Alt, as before), and the command palette shows it.Conflicts move into a pure
conflicts(): chord vs chord as before, plus gesture vs chord while the gesture is on, flagged on both rows.The gesture's mode select now reads Off / On, since the keys sit on the recorder.
Docs:
docs/shortcuts.md.Review follow-ups
A review found defects in the new code; all are fixed here, each with a unit test that failed before its fix:
PASS_TO_APPalso swallowed the same Ctrl key in a terminal (bind Cmd+R, and Ctrl+R was dead).terminalPassesToAppnow requires Cmd on macOS. This also fixes Ctrl+P in a terminal, which was dead on main because of Cmd+P.conflicts()is platform-aware (on macOS only a Cmd hold conflicts with a Cmd chord); the logo key is Super on Linux.docs/shortcuts.mdwrapped.Testing
Manually tested by the author in the dev app: rebinding Recently used tabs and recording a Search everywhere chord. Both behaved as expected.
That pass was before the review follow-ups above, which have not been exercised by hand yet.
npm test(3233 passed),npx tsc -b,npm run typecheck:e2esettings.e2e.ts123 passing on three consecutive runs,codenav.e2e.ts31,tabs-layout.e2e.ts45,app.e2e.ts40.search-everywherefromPASS_TO_APPturns "opens Search Everywhere on a recorded chord, from a focused terminal" red; restoring the old predicate interminalPassesToAppturns "leaves ⌃R to the shell when ⌘R is bound" red. Both green again with the code as committed.tabs-layout.e2e.ts).Pre-existing on main, found during review, not addressed here
shortcutBindingsblob falls into the outercatch {}ofloadShortcuts, and the next save wipes every customisation.shortcuts.defs.tabPrev.hint).shortcutHintcompares against the key with itssettings:prefix, which i18next does not return on a miss, so a missing hint is never detected (since feat(i18n): the shortcut rows speak Chinese #391).Note: #397 also edits
tabs-layout.e2e.ts; whichever merges second needs thefixedWaits.test.tscount rechecked.🤖 Generated with Claude Code