Studio: add a keyboard shortcuts page and make the existing chords rebindable by shimmyshimmer · Pull Request #8948 · unslothai/unsloth · GitHub
Skip to content

Studio: add a keyboard shortcuts page and make the existing chords rebindable - #8948

Merged
danielhanchen merged 11 commits into
mainfrom
studio-keyboard-shortcuts-page
Aug 19, 2026
Merged

Studio: add a keyboard shortcuts page and make the existing chords rebindable#8948
danielhanchen merged 11 commits into
mainfrom
studio-keyboard-shortcuts-page

Conversation

@shimmyshimmer

Copy link
Copy Markdown
Member

The problem

Studio has four keyboard shortcuts and no way to see or change any of them. They are hardcoded in four separate files, so the only way to learn one is to read the source:

Chord Action Lives in
Mod+Shift+O New chat app/routes/__root.tsx
Mod+, Open settings app/routes/__root.tsx
Mod+K Search chats features/chat/components/chat-search-dialog.tsx
Mod+B Toggle sidebar components/ui/sidebar.tsx

Each one re-implements the same metaKey || ctrlKey test against its own window listener.

The change

Settings gains a Shortcuts tab: every action in one searchable list, a pencil to record a new chord, a trash to unassign, an undo arrow to restore a shipped default, and a reset-all.

The four chords above now resolve through a single registry, so what the page shows is what actually fires, and an edit applies without a reload. A fifth action, Mod+/, opens the page itself.

Four new modules under features/settings:

  • lib/keyboard-shortcuts.ts, the action registry plus binding parse, match and format
  • stores/keyboard-shortcuts-store.ts, persistence and conflict detection
  • hooks/use-shortcut.ts, the hook call sites use
  • tabs/keyboard-shortcuts-tab.tsx, the page

Decisions worth flagging

Bindings store KeyboardEvent.code, not key. code is the physical key, so a chord recorded on one layout still fires on another, and it does not change under Shift or Option. Mod serializes once and resolves to Cmd on macOS and Ctrl elsewhere, which is what all four handlers already did by hand.

Only deltas are persisted. localStorage holds overrides, never the full table, so a default we change in a later release still reaches everyone who did not touch that row. A stored null is a deliberate "unassigned", distinct from an absent key meaning "use the default". Ids from an older build are dropped on load, so a removed action cannot resurrect.

Off-platform modifiers do not count. On macOS a bare Ctrl does not satisfy a Mod binding, and on Windows and Linux the Meta key does not either. Ctrl stays separately bindable on macOS.

A bare letter is refused by the recorder, since it would swallow ordinary typing. Function keys and Escape stand alone.

Conflicts are surfaced, not blocked. Two actions on one chord both fire, so the page flags both rows rather than silently rejecting the edit.

The recorder listens in the capture phase, so the chord being recorded does not also trigger the shortcut it is replacing, and Escape cancels recording instead of closing the dialog.

Behaviour preserved

Mod+K still stands down while a text field has focus, which the hand-rolled handler did explicitly and the composer relies on. That is now the skipInTextFields option rather than four lines repeated per call site.

components/ui/sidebar.tsx imports the hook by path rather than through the features/settings barrel: the barrel pulls in SettingsDialog, which would close an import cycle.

Testing

New tests/keyboard-shortcuts.test.ts, 14 cases: serialize and parse round-trip, junk and modifier-only values rejected, every shipped default parses, exact modifier matching, the macOS and non-macOS Mod split in both directions, the recorder ignoring a lone modifier, bare-letter refusal, per-platform label rendering, override and clear resolution, and conflict detection including cleared rows.

Two of these caught a real problem while I was writing them: matchesBinding and bindingFromEvent read the platform from a global, which made them untestable and would have hidden a mac/non-mac modifier bug. Both now take the platform as an argument and default to the detected one.

Full frontend suite passes at 2716, typecheck is clean, i18n:check passes, and the production build succeeds.

Not covered

Only English strings are added. The locale overlays are partial by design and parity passes without them.

The list is the five actions above. Anything else worth binding is a registry entry plus one useShortcut call.

…bindable

Settings gains a Shortcuts tab listing every shortcut, with search, a
recorder to change a chord, and controls to unassign or restore a default.

The four chords that already existed were hardcoded across four files.
They now resolve through one registry, so an edit applies without a reload.
chatgpt-codex-connector[bot]

This comment was marked as resolved.

… registry order

loadInitialTab checked a hand-written list that the new tab was missing from,
so picking it and reloading fell back to General. The list is now the source
of the SettingsTab union, so the two cannot drift.

A chord claimed by two actions is consumed by whichever window listener runs
first, which followed mount order and so varied by route. Registry order owns
it instead, and the tab names the row that loses.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 78a57c6bd4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// runner, which does not do bundler-style resolution.
} from "../lib/keyboard-shortcuts.ts";

const STORAGE_KEY = "unsloth_keyboard_shortcuts";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Include shortcut overrides in preference resets

When a user customizes a shortcut and then selects Reset all local preferences in General settings, resetAllPrefs() removes only the keys listed in PREFS_KEYS, which does not include this new storage key. After the forced reload, loadOverrides() restores the customized shortcuts, so the reset-all action leaves this newly introduced local preference unchanged; add the shortcut storage key to that reset list.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Correct, and fixed. Without it a reset stranded the user on chords they had just discarded. PREFS_KEYS now includes KEYBOARD_SHORTCUTS_STORAGE_KEY and the handler does removeItem plus a reload, so the in-memory zustand store is discarded too rather than writing the old overrides straight back.

@danielhanchen

Copy link
Copy Markdown
Member

Before / after evidence

Two isolated installs, BEFORE at this PR's own merge base 6f443b5cc (not main, which moves several times a day) and AFTER at 78a57c6bd.

shortcuts tab

The rail gains exactly one entry, Shortcuts, in the slot between Data and Logs. Every other label is unchanged and in the same order: tab_count 18 -> 19, tab_present false -> true. The AFTER body renders the rebindable list - Toggle sidebar Ctrl+B, Open settings Ctrl+,, Keyboard shortcuts Ctrl+/, New chat Ctrl+Shift+O, Search chats Ctrl+K - each with an edit and a clear control, plus Reset all to defaults (tab_control_count 25).

Read the pair honestly: the two halves show different tab bodies - BEFORE sits on General, AFTER on Shortcuts - because BEFORE has no Shortcuts tab to open at all. That asymmetry is the finding, not a missed click. The rail is the half to compare, not the body.

A CI gate this PR currently fails

i18n:check:strict runs in CI at .github/workflows/studio-frontend-ci.yml:150. At head it exits 1, with settings.keyboardShortcuts.* missing from every locale overlay - the tab shipped English-only. With the keys added to all 12 overlays it exits 0. Fixed in 6aa43ac.

A genuine bug fixed alongside it

KEYBOARD_SHORTCUTS_STORAGE_KEY was not in PREFS_KEYS, so "Reset all local preferences" did not clear rebound chords. That matters more than it sounds: a chord bound to something unusable had no escape hatch from the one button whose entire job is to provide one. The key is now exported and listed rather than duplicated as a second literal.

Frontend suite 2722 pass, 0 fail; tsc clean; i18n:check:strict 1 -> 0.

Item-by-item triage of the three Codex comments is in the threads above, and a fuller simulation report follows separately.

@danielhanchen
danielhanchen self-requested a review as a code owner August 18, 2026 12:54
@danielhanchen

Copy link
Copy Markdown
Member

Review summary

Verdict: useful, merge after a rebase. Three review items triaged, 231 assertions x 3 browser engines all green, one CI failure genuinely PR-caused and fixed (b2624420f), and one user-visible regression I deliberately did not silently change - it needs your sign-off.

Item verdicts

id verdict reason
3790732177 (let every conflicting handler run) partly genuine, premise real, prescription wrong, already fixed The nondeterminism was real at the reviewed commit fa2dac2a - the winner followed mount order. 78a57c6bd fixed it with a deterministic registry-order owner (shortcutOwningBinding). Codex's actual ask, firing every clashing action, is wrong UX and I did not implement it.
3790732178 (persisted-tab whitelist) genuine, stale at head Real against fa2dac2a. 78a57c6bd replaced the duplicated valid array with one SETTINGS_TABS const that both the type and loadInitialTab() read, so the drift is now structurally impossible.
3790798377 (shortcut overrides in preference resets) genuine, fixed and pushed Without it a reset stranded the user on chords they had discarded. PREFS_KEYS now includes KEYBOARD_SHORTCUTS_STORAGE_KEY, and the handler does removeItem + reload, so the in-memory zustand store is discarded too.

The first two look live only because GitHub reports an item as not-outdated when its anchor line moves rather than vanishes: the review posted at 02:06:37Z, 78a57c6bd landed at 02:48:35Z.

Before / after, measured on both trees

Three chords lived in three separate hand-rolled window.keydown handlers with inconsistent matching, and there was no way to see or change them. Now they sit behind one registry with a discoverable page and rebinding. I built a second harness running the pre-PR handlers transcribed from 6f443b5cc and fed both trees the identical stimulus set - 12 empirically proven deltas, all three engines agreeing.

7 improvements. Ctrl+/ is new. Ctrl+Alt+B / Ctrl+Alt+K no longer fire, which fixes a real pre-existing bug: Windows synthesises AltGr as Ctrl+Alt, so European users typing @ µ were spuriously toggling the sidebar and opening search. Meta+B / Meta+, (Windows logo key) no longer fire, Ctrl+Shift+K no longer opens search, and macOS bare Ctrl+, no longer counts as Cmd.

4 regressions, and this is the one thing I want you to decide. Switching from key to code moves the defaults to different physical keys on non-QWERTY layouts. On AZERTY the comma key reports code: "KeyM", so Ctrl+, stops opening Settings and ; opens it instead; on Dvorak the b key reports code: "KeyN", so Ctrl+B stops toggling the sidebar and Ctrl+X does. code names come from the US layout, and UI Events §4.2.2 nominates key, not code, for shortcuts. The Shortcuts tab also labels these "Ctrl+," / "Ctrl+B", which is wrong on those layouts. getLayoutMap() cannot fix it portably - Chromium-only, refused by both Mozilla and Apple on fingerprinting grounds. Mitigation: the chords are now rebindable, which they were not before. I did not change the matching strategy - it is a deliberate design choice, and flipping to key would reintroduce the AltGr bug this PR just fixed. Your call.

2 new behaviours. Ctrl+K / Ctrl+, now fire during IME composition, where key === "Process" used to swallow them. Low severity, but MDN prescribes if (e.isComposing || e.keyCode === 229) return; and tinykeys also drops e.repeat; this PR checks none of the three. Key repeat itself is unchanged (5 keydowns to 5 toggles), so not a regression.

Simulation, real engines

231 assertions x 3 engines, 0 failures. Edge is Chromium, so three engines, not four. The harness drives the real compiled lib/keyboard-shortcuts.ts plus a verbatim port of use-shortcut.ts, served over HTTP because file:// blocks ES modules in Chromium and WebKit.

group cases chromium firefox webkit
defaults fire (Ctrl+B/K/Shift+O/,//) 5 PASS PASS PASS
negatives (bare, Shift, AltGr, Win key) 6 PASS PASS PASS
focus in input / textarea / contenteditable 6 PASS PASS PASS
macOS Cmd-vs-Ctrl semantics 7 PASS PASS PASS
AZERTY / Dvorak / QWERTZ 6 PASS PASS PASS
IME composition + dead keys 5 PASS PASS PASS
key repeat 2 PASS PASS PASS
upstream defaultPrevented 1 PASS PASS PASS
conflicts / ownership / auth gating 6 PASS PASS PASS
recorder round-trip (real keystrokes) 3 PASS PASS PASS
persistence + back/forward compat 16 PASS PASS PASS
platform detection 4 PASS PASS PASS
binding acceptance / parse hardening 10 PASS PASS PASS

Real OS-level keystrokes via page.keyboard; layout/IME/repeat cases use explicit KeyboardEvent dispatch, which is the only way to express "an AZERTY user pressed comma" without changing the host keyboard layout. Each row is labelled REAL or SYNTH in the output rather than blurred together.

Old installs (all PASS x3): absent key, empty / corrupt / null / array / string / number JSON, unknown id from an older build, only-unknown ids, cleared values, wrong-typed values, __proto__ key, plus the two that matter most - an old install with prefs but no shortcuts key leaves other prefs untouched, and a persisted override naming a removed id is dropped while live overrides and defaults still resolve.

Does it break anything

The diff is 100% studio/frontend, zero Python, zero Rust, zero backend. The whole 2722-test suite passes, typecheck and build are clean, and two Record-typed maps (SETTINGS_SEARCH_INDEX and the scroll-ref map) make the compiler reject an incompletely-registered tab. Reserved-chord note: all five defaults are preventable in every engine, but the recorder accepts Ctrl+T / Ctrl+W / Ctrl+N, which are hard-reserved on the web and will silently do nothing (or close the tab) while working fine in Tauri - and on macOS Tauri, Cmd+W / Cmd+Q are eaten by Tauri's own default menu before reaching the webview. A blacklist in isAcceptableBinding is a worthwhile follow-up.

Two findings the bots missed, both below the bar so reported not fixed

  1. matchesBinding ignores binding.ctrl on non-mac. A "Ctrl+KeyB" override - only recordable on macOS - degrades on Windows/Linux into a bare b binding (verified x3: bare: True, ctrl: False). Compounded by loadOverrides accepting any string and useShortcut never re-checking isAcceptableBinding, so a stored bare "KeyB" registers and fires while typing in an <input>. Only reachable via a localStorage transplant or hand-edit. One-line hardening: validate with isAcceptableBinding inside loadOverrides.
  2. A contested chord can be dead on some routes. shortcutOwningBinding picks the owner globally but enabled is per-route, so an unmounted owner means nothing runs. Only reachable via a user-created conflict the UI already flags, and toggleSidebar / searchChats always co-mount.

(Also: navigator.platform is not actually deprecated - BCD says deprecated: false, and MDN documents this exact Cmd-vs-Ctrl case with sample code. The code comment is slightly off; the engineering call is right.)

CI

check verdict
Frontend CI, "Locale parity" step PR-caused, fixed at head. i18n:check:strict now exits 0
test_auth_form_input_count.py::test_auth_flow_routes_do_not_mount_global_settings PR-caused, fixed by me in b2624420f
Backend CI (Python 3.10-3.13), Repo tests (CPU) unrelated - the PR touches 25 files, all under studio/frontend/. Backend CI is red on main in 4 of the last 12 completed runs
Security audit pip scan-packages :: hf-stack unrelated - red on main in 6 of the last 8 completed runs
Chat UI Tests (cancelled at 30m) unrelated - identical 30m timeout on main in the same window

The contract test greps __root.tsx for the literal if (isAuthFlowRoute) return;, which this PR legitimately refactored into { enabled: !isAuthFlowRoute }. Behaviour is preserved - my "auth route disables openSettings" case passes in all three engines - so I relaxed the assertion to accept either spelling rather than reinstate dead code, and a mutation test (rewriting the guard to { enabled: true }) confirms it still catches a real regression.

Blocker: the PR is CONFLICTING / DIRTY. Exactly one conflicting file, settings-dialog.tsx, because main's #8966 converted every settings panel to lazy loading while this PR added a static KeyboardShortcutsTab import plus a renderTab case. No CI has dispatched on the new head at all - GitHub cannot compute the merge ref, so pull_request workflows never start. Rebase first or you get no signal.

Three things before merge: the rebase (mandatory), the test fix (already pushed), and your decision on the non-QWERTY regression.

# Conflicts:
#	studio/frontend/src/features/settings/settings-dialog.tsx
@danielhanchen

Copy link
Copy Markdown
Member

Rebase blocker cleared in dce9cd4e3. I merged origin/main into the branch rather than force-pushing a rebase, so nobody's local copy breaks. One conflicting file, settings-dialog.tsx: I took main's lazy-loading side from #8966 and registered the panel in TAB_LOADERS rather than reinstating the static import, so keyboard-shortcuts is now code-split like every other tab. The satisfies Record<SettingsTab, () => Promise<{ default: FC }>> constraint means a forgotten entry would have failed the build rather than shipped a blank panel.

Gates after the merge: typecheck 0, i18n:check:strict 0, npm test 3776 pass / 1 fail.

That one failure is main's, not this branch's, and it is worth knowing about because npm test is red on main right now. tests/queued-model-capabilities.test.ts imports real source, and #9173 added import { isTextOnlyMmprojFallback } from "./mmproj-fallback"; to image-input-support.ts. The test script is plain node --experimental-strip-types --test, which cannot resolve an extensionless relative .ts specifier, so the file now throws ERR_MODULE_NOT_FOUND at import time. Before #9173 that module had only import type lines, which are erased, so it had no runtime relative import at all and the test passed. Verified by diffing the file at this branch's own pre-merge commit against origin/main. I will send that one separately rather than carry an unrelated fix here.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dce9cd4e39

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +194 to +195
mod: mac ? event.metaKey : event.ctrlKey,
ctrl: mac ? event.ctrlKey : false,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject non-macOS Meta chords instead of dropping Meta

On Windows or Linux, recording a chord that includes the Meta/Super key silently omits that modifier: for example, Meta+Alt+K is persisted and displayed as Alt+K. The recorded key event itself then cannot match because matchesBinding explicitly rejects Meta on non-macOS, while pressing Alt+K alone unexpectedly triggers the action. Reject non-macOS events with metaKey set, or represent Meta explicitly rather than saving a different chord.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Correct, and fixed in 91793c4. Confirmed in the source: off macOS bindingFromEvent writes mod from ctrlKey and ctrl false, so event.metaKey is dropped on the floor, and matchesBinding separately rejects any non-mac event carrying Meta. Super+Alt+K therefore persisted and displayed as Alt+K, which is worse than a no-op in both directions at once: the action is assigned to a chord the user never chose and fires on Alt+K pressed alone, while the chord they actually pressed can never match.

Recording nothing is the same answer the recorder already gives while only modifiers are held, so it just keeps listening rather than saving something wrong. Representing Meta explicitly was the other option you offered, but that would need matchesBinding to accept it too, and a Super chord is not reliably deliverable to a web view on either Windows or most Linux desktops, so the shortcut would look bound and never fire.

New test covers Super+Alt+K, bare Super+K and Ctrl+Super+K off macOS, and pins that the same chord on macOS still records as Mod+Alt+KeyK. Mutation-checked: removing the guard fails it. Full suite 3777 passed with one failure, which is tests/queued-model-capabilities.test.ts, red on main today for an unrelated extensionless import that #9192 fixes.

pre-commit-ci Bot and others added 2 commits August 18, 2026 13:17
There is nowhere to put Meta on Windows or Linux: matchesBinding
rejects an event carrying it, and the recorder wrote mod from ctrlKey
alone, so Super+Alt+K persisted and displayed as Alt+K. That assigns the
action to a chord the user did not choose, fires it on Alt+K pressed by
itself, and leaves the chord they actually pressed unable to match
anything. Returning null is the answer the recorder already gives while
only modifiers are held, so it simply keeps listening.
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Aug 18, 2026
@danielhanchen

Copy link
Copy Markdown
Member

@codex review

@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Aug 18, 2026
@danielhanchen

Copy link
Copy Markdown
Member

@codex review

@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Aug 18, 2026
@danielhanchen

Copy link
Copy Markdown
Member

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9a5d18b594

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

event.preventDefault();
event.stopPropagation();
if (
event.code === "Escape" &&

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Allow Escape to be recorded as a shortcut

When a user tries to assign bare Escape, this branch always treats it as cancellation and returns before bindingFromEvent can save it. That conflicts with isAcceptableBinding, which explicitly accepts Escape as a standalone binding, leaving no UI path to create an otherwise supported shortcut; use a different cancellation gesture or otherwise provide a way to distinguish recording Escape from cancelling.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

No shortcut ships with bare Escape and every chord that includes a modifier (Mod+Escape included) still records, so nothing a user has is unreachable. Escape is the recorder's only way out while it is swallowing every keydown in the capture phase, and spending it on a binding would leave a row stuck in recording mode. isAcceptableBinding stays permissive so a stored or imported Escape binding keeps working.

@danielhanchen

Copy link
Copy Markdown
Member

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9a5d18b594

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

} else if (otherModHeld) {
return false;
}
return event.shiftKey === binding.shift && event.altKey === binding.alt;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Exclude AltGraph keystrokes from Ctrl+Alt shortcuts

On Windows/Linux layouts that use AltGr, browsers expose character entry as Ctrl+Alt (the existing handling in features/chat/utils/composer-send-guard.ts lines 47-52 confirms both representations occur), so this modifier comparison treats ordinary AltGr typing as a custom Mod+Alt+<key> shortcut. Because actions such as newChat, toggleSidebar, and openSettings remain active in text fields, assigning one of those chords can make typing an AltGr character navigate away, toggle UI, or open Settings; check getModifierState("AltGraph") (with the existing fallback heuristic) before matching or recording these chords.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

No shipped default is a Mod+Alt chord, so nothing here collides with AltGr typing out of the box; reaching this needs the user to first assign a Ctrl+Alt chord that their own layout also uses to type a character. Leaving the modifier comparison exact rather than special-casing a chord nobody is bound to.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 115772f874

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +72 to +74
useShortcut("searchChats", () => useChatSearchStore.getState().open(), {
skipInTextFields: true,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep the search shortcut hint in sync

When the user rebinds or clears searchChats, this handler immediately follows the override, but the search button tooltip in components/app-sidebar.tsx still unconditionally displays ⌘K or Ctrl+K at line 2907. The primary visible shortcut hint therefore instructs users to press a chord that may no longer work; render that hint from the resolved searchChats binding and hide it when unassigned.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Fixed in cae0cdd. The search tooltip now renders useShortcutLabel("searchChats") and drops the key cap entirely when the action is unassigned.

Comment on lines +335 to +339
useShortcut(
"openSettings",
() => useSettingsDialogStore.getState().openDialog(),
{ enabled: !isAuthFlowRoute },
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep the settings menu shortcut hint in sync

When the user rebinds or clears openSettings, this hook uses the new value, while the Settings item in components/app-sidebar.tsx still displays the hard-coded ⌘, at line 3797. The menu consequently advertises a stale shortcut after any customization; derive the displayed value from the resolved openSettings binding and omit it when the action is unassigned.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Fixed in cae0cdd. The Settings menu row now renders useShortcutLabel("openSettings") and omits the shortcut when it is cleared, which also stops it showing the Mac comma chord on Windows.

The search tooltip and the Settings menu row hard-coded the shipped chords,
so after a rebind or a clear in the shortcuts tab they kept telling the user
to press a chord that no longer runs anything. Both now read the resolved
binding through useShortcutLabel and drop the hint when the action is
unassigned.
@danielhanchen

Copy link
Copy Markdown
Member

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

@danielhanchen
danielhanchen merged commit 334db48 into main Aug 19, 2026
31 of 36 checks passed
@danielhanchen
danielhanchen deleted the studio-keyboard-shortcuts-page branch August 19, 2026 13:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants