fix(shortcuts): show effective bindings in the cheat sheet (#2985)

Render and search the shortcut chord that actually fires, including rebinding default-only display tokens such as question mark and index wildcards. Unassigned rows show no empty badge, and named-key overrides use the shared display vocabulary.
This commit is contained in:
Christoph Leiter
2026-08-09 01:56:32 +02:00
committed by GitHub
parent 90aa8bbefc
commit 74cf1931b1
8 changed files with 501 additions and 160 deletions
@@ -6,37 +6,16 @@ import { getIsElectronRuntime } from "@/constants/layout";
import { AdaptiveModalSheet, type SheetHeader } from "@/components/adaptive-modal-sheet";
import { Shortcut } from "@/components/ui/shortcut";
import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store";
import { formatShortcut } from "@/utils/format-shortcut";
import { getShortcutOs } from "@/utils/shortcut-platform";
import {
buildEffectiveBindings,
buildKeyboardShortcutHelpSections,
} from "@/keyboard/keyboard-shortcuts";
import { filterShortcutHelpSections } from "@/keyboard/shortcut-help-search";
import { useKeyboardShortcutOverrides } from "@/hooks/use-keyboard-shortcut-overrides";
const SNAP_POINTS: string[] = ["70%", "92%"];
function shortcutSearchAliases(keys: string[], shortcutOs: "mac" | "non-mac"): string {
const aliases = keys.map((key) => {
if (shortcutOs === "mac") {
if (key === "mod" || key === "meta") return ["cmd", "command"];
if (key === "alt") return ["alt", "option"];
} else {
if (key === "mod" || key === "ctrl") return ["ctrl", "control"];
if (key === "meta") return ["win", "windows"];
}
return [key];
});
const combinations = aliases.reduce<string[][]>(
(prefixes, choices) =>
prefixes.flatMap((prefix) => choices.map((choice) => [...prefix, choice])),
[[]],
);
return combinations
.flatMap((combination) => [combination.join(" "), combination.join("+")])
.join(" ");
}
export function KeyboardShortcutsDialog() {
const { t } = useTranslation();
const open = useKeyboardShortcutsStore((s) => s.shortcutsDialogOpen);
@@ -54,33 +33,10 @@ export function KeyboardShortcutsDialog() {
() => buildKeyboardShortcutHelpSections({ isMac, isDesktop: isDesktopApp }, bindings),
[bindings, isDesktopApp, isMac],
);
const visibleSections = useMemo(() => {
const normalizedQuery = query.trim().toLocaleLowerCase();
if (!normalizedQuery) return sections;
return sections.flatMap((section) => {
const sectionTitle = t(section.titleKey);
if (sectionTitle.toLocaleLowerCase().includes(normalizedQuery)) {
return [section];
}
const rows = section.rows.filter((row) => {
const searchText = [
t(row.labelKey),
row.noteKey ? t(row.noteKey) : row.note,
row.keys.join(" "),
formatShortcut(row.keys, shortcutOs),
shortcutSearchAliases(row.keys, shortcutOs),
]
.filter(Boolean)
.join(" ")
.toLocaleLowerCase();
return searchText.includes(normalizedQuery);
});
return rows.length > 0 ? [{ ...section, rows }] : [];
});
}, [query, sections, shortcutOs, t]);
const visibleSections = useMemo(
() => filterShortcutHelpSections({ sections, query, translate: t, shortcutOs }),
[query, sections, shortcutOs, t],
);
useEffect(() => {
if (!open) setQuery("");
@@ -114,14 +70,18 @@ export function KeyboardShortcutsDialog() {
<Text style={styles.sectionTitle}>{t(section.titleKey)}</Text>
<View style={styles.rows}>
{section.rows.map((row) => (
<View key={row.id} style={styles.row}>
<View key={row.id} style={styles.row} testID={`shortcut-help-row-${row.id}`}>
<View style={styles.rowText}>
<Text style={styles.rowLabel}>{t(row.labelKey)}</Text>
{row.note ? (
<Text style={styles.rowNote}>{row.noteKey ? t(row.noteKey) : row.note}</Text>
) : null}
</View>
<Shortcut keys={row.keys} style={styles.rowShortcut} />
{row.chord === null ? (
<Text style={styles.rowUnassigned}>{t("settings.shortcuts.unassigned")}</Text>
) : (
<Shortcut chord={row.chord} style={styles.rowShortcut} />
)}
</View>
))}
</View>
@@ -179,6 +139,13 @@ const styles = StyleSheet.create((theme) => ({
rowShortcut: {
alignSelf: "flex-start",
},
// Matches the settings row's unassigned label, so the two screens describe the
// same state the same way.
rowUnassigned: {
alignSelf: "flex-start",
fontSize: theme.fontSize.sm,
color: theme.colors.foregroundMuted,
},
empty: {
paddingVertical: theme.spacing[6],
textAlign: "center",
@@ -1,4 +1,5 @@
import { describe, expect, it, vi } from "vitest";
import { formatShortcut } from "@/utils/format-shortcut";
import {
buildKeyboardShortcutHelpSections,
buildEffectiveBindings,
@@ -645,7 +646,11 @@ describe("keyboard-shortcut help sections", () => {
"workspace-tab-new": ["mod", "T"],
"workspace-jump-index": ["mod", "1-9"],
"workspace-tab-jump-index": ["mod", "alt", "1-9"],
"workspace-tab-close-current": ["meta", "W"],
// Derived from `combo: "Cmd+W"`, so the token is `mod` where the row
// used to be hand-authored as `meta`. This binding is mac-only and
// `formatShortcut` renders both as ⌘, so the badge is unchanged — see
// the render assertion below.
"workspace-tab-close-current": ["mod", "W"],
"workspace-pane-split-right": ["mod", "\\"],
"workspace-pane-close": ["mod", "shift", "W"],
},
@@ -659,11 +664,15 @@ describe("keyboard-shortcut help sections", () => {
},
},
{
name: "uses mod+b for the left sidebar and mod+period for both sidebars on non-mac",
name: "uses ctrl+b for the left sidebar and ctrl+period for both sidebars on non-mac",
context: { isMac: false, isDesktop: false },
// Derived from `combo: "Ctrl+B"` / `"Ctrl+."`, so the token is `ctrl` where
// these rows used to be hand-authored as `mod`. Both bindings are non-mac
// only and `formatShortcut` labels either token "Ctrl" there, so the badge
// is unchanged — see the render assertion below.
expectedKeys: {
"toggle-left-sidebar": ["mod", "B"],
"toggle-both-sidebars": ["mod", "."],
"toggle-left-sidebar": ["ctrl", "B"],
"toggle-both-sidebars": ["ctrl", "."],
},
},
];
@@ -672,10 +681,104 @@ describe("keyboard-shortcut help sections", () => {
const sections = buildKeyboardShortcutHelpSections(context);
for (const [id, keys] of Object.entries(expectedKeys)) {
expect(findRow(sections, id)?.keys).toEqual(keys);
expect(findRow(sections, id)?.chord).toEqual([keys]);
}
});
describe("rows derive their keys from the binding that fires", () => {
const macDesktop = { isMac: true, isDesktop: true };
const NEW_WORKSPACE_BINDING = "workspace-new-cmd-n-mac";
const MAC_INDEX_BINDING = "workspace-navigate-index-cmd-digit-mac";
const PANE_FOCUS_LEFT_BINDING = "workspace-pane-focus-left-cmd-shift-left";
const SHOW_SHORTCUTS_BINDING = "shortcuts-dialog-toggle-question-mark";
function rowChord(overrides: ShortcutOverrides, id: string) {
const sections = buildKeyboardShortcutHelpSections(
macDesktop,
buildEffectiveBindings(overrides),
);
return findRow(sections, id)?.chord ?? null;
}
// The reported bug: the cheat sheet advertised the shipped default no matter
// what the user had rebound the shortcut to.
it("shows the override, not the shipped default", () => {
expect(rowChord({}, "new-workspace")).toEqual([["mod", "N"]]);
expect(rowChord({ [NEW_WORKSPACE_BINDING]: "Cmd+Shift+K" }, "new-workspace")).toEqual([
["mod", "shift", "K"],
]);
});
it("shows every step of a multi-step chord override", () => {
expect(rowChord({ [NEW_WORKSPACE_BINDING]: "Cmd+K Cmd+N" }, "new-workspace")).toEqual([
["mod", "K"],
["mod", "N"],
]);
});
it("reports an unassigned row as having no keys at all", () => {
expect(rowChord({ [NEW_WORKSPACE_BINDING]: UNASSIGNED_COMBO }, "new-workspace")).toBeNull();
});
it("returns to the shipped default once the override is cleared", () => {
expect(rowChord({ [NEW_WORKSPACE_BINDING]: "Cmd+Shift+K" }, "new-workspace")).toEqual([
["mod", "shift", "K"],
]);
expect(rowChord({}, "new-workspace")).toEqual([["mod", "N"]]);
});
// An arrow override used to render as the raw `ARROWLEFT` code, because the
// display path uppercased key names past the table that maps them to arrows.
it("renders an arrow override as an arrow", () => {
const chord = rowChord(
{ [PANE_FOCUS_LEFT_BINDING]: "Cmd+Alt+ArrowLeft" },
"workspace-pane-focus-left",
);
expect(chord).toEqual([["mod", "alt", "Left"]]);
expect(formatShortcut(chord?.[0] ?? [], "mac")).toBe("⌥⌘←");
});
// `1-9` and `?` are display-only tokens no combo string can spell, so these
// two rows opt out of default derivation via `help.defaultDisplayKeys`.
it("keeps the wildcard token on the index-jump row", () => {
expect(rowChord({}, "workspace-jump-index")).toEqual([["mod", "1-9"]]);
});
it("keeps the bare ? on the show-shortcuts row rather than deriving Shift+?", () => {
expect(rowChord({}, "show-shortcuts")).toEqual([["?"]]);
});
it("replaces the default-only wildcard when the index jump is rebound", () => {
expect(rowChord({ [MAC_INDEX_BINDING]: "Ctrl+Digit" }, "workspace-jump-index")).toEqual([
["ctrl", "Digit"],
]);
});
it("replaces the default-only ? when show shortcuts is rebound", () => {
expect(rowChord({ [SHOW_SHORTCUTS_BINDING]: "Cmd+K" }, "show-shortcuts")).toEqual([
["mod", "K"],
]);
});
// The authored row said `meta`, the derived row says `mod`. Both render ⌘ on
// mac and this binding is mac-only, so nothing the user sees moved.
it("still renders close-tab as ⌘W after switching to the derived token", () => {
const sections = buildKeyboardShortcutHelpSections(macDesktop);
const chord = findRow(sections, "workspace-tab-close-current")?.chord;
expect(formatShortcut(chord?.[0] ?? [], "mac")).toBe("⌘W");
});
// Same story on the other side: these rows said `mod`, the non-mac combos
// say `Ctrl`, and non-mac labels both as "Ctrl".
it("still renders the sidebar toggles as Ctrl+B and Ctrl+. on non-mac", () => {
const sections = buildKeyboardShortcutHelpSections({ isMac: false, isDesktop: false });
const left = findRow(sections, "toggle-left-sidebar")?.chord;
const both = findRow(sections, "toggle-both-sidebars")?.chord;
expect(formatShortcut(left?.[0] ?? [], "non-mac")).toBe("Ctrl+B");
expect(formatShortcut(both?.[0] ?? [], "non-mac")).toBe("Ctrl+.");
});
});
it("returns stable i18n keys for section titles and help rows", () => {
const sections = buildKeyboardShortcutHelpSections({ isMac: true, isDesktop: true });
const projects = sections.find((section) => section.id === "projects");
@@ -889,9 +992,12 @@ describe("unassigned shortcuts", () => {
});
describe("resolveShortcutKeysForAction", () => {
// `ctrl`, not `mod`: these keys are derived from the non-mac binding's
// `combo: "Ctrl+T"` rather than hand-authored. `formatShortcut` labels both
// tokens "Ctrl" off mac, and this binding is non-mac only.
it("returns the default keys when there is no override", () => {
expect(resolveShortcutKeysForAction("workspace-tab-new", {}, desktopNonMac)).toEqual([
["mod", "T"],
["ctrl", "T"],
]);
});
@@ -924,13 +1030,13 @@ describe("unassigned shortcuts", () => {
{ [TAB_NEW_BINDING]: "Ctrl+Nonsense" },
desktopNonMac,
),
).toEqual([["mod", "T"]]);
).toEqual([["ctrl", "T"]]);
});
it("falls back to the default keys for a non-string stored value", () => {
const overrides = { [TAB_NEW_BINDING]: 42 } as unknown as ShortcutOverrides;
expect(resolveShortcutKeysForAction("workspace-tab-new", overrides, desktopNonMac)).toEqual([
["mod", "T"],
["ctrl", "T"],
]);
});
@@ -944,7 +1050,7 @@ describe("unassigned shortcuts", () => {
const bindings = buildEffectiveBindings({ [TAB_NEW_BINDING]: UNASSIGNED_COMBO });
const sections = buildKeyboardShortcutHelpSections(desktopNonMac, bindings);
expect(findRow(sections, "workspace-tab-new")?.keys).toEqual([]);
expect(findRow(sections, "workspace-tab-new")?.chord).toBeNull();
});
it("keeps the row so the shortcut stays rebindable", () => {
@@ -958,15 +1064,14 @@ describe("unassigned shortcuts", () => {
it("still lists the default keys when nothing is overridden", () => {
const sections = buildKeyboardShortcutHelpSections(desktopNonMac);
expect(findRow(sections, "workspace-tab-new")?.keys).toEqual(["mod", "T"]);
expect(getDefaultKeysForAction("workspace-tab-new", desktopNonMac)).toEqual(["mod", "T"]);
expect(findRow(sections, "workspace-tab-new")?.chord).toEqual([["ctrl", "T"]]);
expect(getDefaultKeysForAction("workspace-tab-new", desktopNonMac)).toEqual([["ctrl", "T"]]);
});
// A binding authored with `combo: ""` has no default. The settings row uses
// this to hide Reset, which would otherwise be a dead button landing on the
// same "Not set" state Clear already produced. `help.keys` stays populated
// on purpose: it is hand-authored, so the empty parsed chord alone has to be
// what marks the binding default-less.
// same "Not set" state Clear already produced. The empty parsed chord is what
// marks the binding default-less.
it("reports no default keys for a binding that ships without a combo", () => {
const bindings = withoutDefaultCombo("workspace-tab-new");
@@ -977,7 +1082,7 @@ describe("unassigned shortcuts", () => {
const bindings = withoutDefaultCombo("workspace-tab-new");
const sections = buildKeyboardShortcutHelpSections(desktopNonMac, bindings);
expect(findRow(sections, "workspace-tab-new")?.keys).toEqual([]);
expect(findRow(sections, "workspace-tab-new")?.chord).toBeNull();
});
});
});
+48 -81
View File
@@ -43,7 +43,8 @@ export interface KeyboardShortcutHelpRow {
id: string;
label: string;
labelKey: string;
keys: ShortcutKey[];
/** The keys that actually fire this action, or `null` when it has none. */
chord: ShortcutKey[][] | null;
note?: string;
noteKey?: string;
}
@@ -88,7 +89,14 @@ interface ShortcutHelp {
id: string;
section: ShortcutSectionId;
label: string;
keys: ShortcutKey[];
/**
* Display keys to show instead of the combo. Set this only when the combo
* cannot express what the row means — the `Digit` wildcard, which stands for
* any of 1-9, and `Shift+?`, whose Shift is implied by the character itself.
* Every other row derives its keys from `combo`, so a rebound shortcut shows
* what it now does rather than what it shipped as.
*/
defaultDisplayKeys?: ShortcutKey[];
note?: string;
}
@@ -194,7 +202,6 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "new-agent",
section: "projects",
label: "Open project",
keys: ["mod", "O"],
},
},
{
@@ -206,7 +213,6 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "new-agent",
section: "projects",
label: "Open project",
keys: ["mod", "O"],
},
},
@@ -220,7 +226,6 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "new-workspace",
section: "projects",
label: "New workspace",
keys: ["mod", "N"],
},
},
{
@@ -232,7 +237,6 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "new-workspace",
section: "projects",
label: "New workspace",
keys: ["mod", "N"],
},
},
@@ -246,7 +250,6 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "switch-project",
section: "projects",
label: "Switch project",
keys: ["mod", "P"],
},
},
{
@@ -258,7 +261,6 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "switch-project",
section: "projects",
label: "Switch project",
keys: ["mod", "P"],
},
},
@@ -274,7 +276,6 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "archive-workspace",
section: "projects",
label: "Archive workspace",
keys: ["mod", "shift", "Backspace"],
},
},
{
@@ -288,7 +289,6 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "archive-workspace",
section: "projects",
label: "Archive workspace",
keys: ["mod", "shift", "Backspace"],
},
},
@@ -302,7 +302,6 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "pin-workspace",
section: "projects",
label: "Pin chat",
keys: ["mod", "shift", "P"],
},
},
{
@@ -314,7 +313,6 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "pin-workspace",
section: "projects",
label: "Pin chat",
keys: ["mod", "shift", "P"],
},
},
@@ -328,7 +326,6 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "workspace-tab-new",
section: "tabs-panes",
label: "New tab",
keys: ["mod", "T"],
},
},
{
@@ -340,7 +337,6 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "workspace-tab-new",
section: "tabs-panes",
label: "New tab",
keys: ["mod", "T"],
},
},
{
@@ -352,7 +348,6 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "workspace-tab-close-current",
section: "tabs-panes",
label: "Close current tab",
keys: ["meta", "W"],
},
},
{
@@ -364,7 +359,6 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "workspace-tab-close-current",
section: "tabs-panes",
label: "Close current tab",
keys: ["ctrl", "W"],
},
},
{
@@ -376,7 +370,6 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "workspace-tab-close-current",
section: "tabs-panes",
label: "Close current tab",
keys: ["alt", "shift", "W"],
},
},
@@ -391,7 +384,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "workspace-jump-index",
section: "navigation",
label: "Jump to workspace",
keys: ["mod", "1-9"],
defaultDisplayKeys: ["mod", "1-9"],
},
},
{
@@ -404,7 +397,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "workspace-jump-index",
section: "navigation",
label: "Jump to workspace",
keys: ["mod", "1-9"],
defaultDisplayKeys: ["mod", "1-9"],
},
},
{
@@ -417,7 +410,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "workspace-jump-index",
section: "navigation",
label: "Jump to workspace",
keys: ["alt", "1-9"],
defaultDisplayKeys: ["alt", "1-9"],
},
},
@@ -432,7 +425,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "workspace-tab-jump-index",
section: "navigation",
label: "Jump to tab",
keys: ["mod", "alt", "1-9"],
defaultDisplayKeys: ["mod", "alt", "1-9"],
},
},
{
@@ -445,7 +438,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "workspace-tab-jump-index",
section: "navigation",
label: "Jump to tab",
keys: ["alt", "1-9"],
defaultDisplayKeys: ["alt", "1-9"],
},
},
{
@@ -458,7 +451,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "workspace-tab-jump-index",
section: "navigation",
label: "Jump to tab",
keys: ["alt", "shift", "1-9"],
defaultDisplayKeys: ["alt", "shift", "1-9"],
},
},
@@ -473,7 +466,6 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "workspace-prev",
section: "navigation",
label: "Previous workspace",
keys: ["mod", "["],
},
},
{
@@ -486,7 +478,6 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "workspace-prev",
section: "navigation",
label: "Previous workspace",
keys: ["mod", "["],
},
},
{
@@ -499,7 +490,6 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "workspace-next",
section: "navigation",
label: "Next workspace",
keys: ["mod", "]"],
},
},
{
@@ -512,7 +502,6 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "workspace-next",
section: "navigation",
label: "Next workspace",
keys: ["mod", "]"],
},
},
{
@@ -525,7 +514,6 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "workspace-prev",
section: "navigation",
label: "Previous workspace",
keys: ["alt", "["],
},
},
{
@@ -538,7 +526,6 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "workspace-next",
section: "navigation",
label: "Next workspace",
keys: ["alt", "]"],
},
},
@@ -553,7 +540,6 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "workspace-tab-prev",
section: "navigation",
label: "Previous tab",
keys: ["alt", "shift", "["],
},
},
{
@@ -566,7 +552,6 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "workspace-tab-next",
section: "navigation",
label: "Next tab",
keys: ["alt", "shift", "]"],
},
},
@@ -580,7 +565,6 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "workspace-pane-split-right",
section: "tabs-panes",
label: "Split pane right",
keys: ["mod", "\\"],
},
},
{
@@ -592,7 +576,6 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "workspace-pane-split-down",
section: "tabs-panes",
label: "Split pane down",
keys: ["mod", "shift", "\\"],
},
},
{
@@ -604,7 +587,6 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "workspace-pane-focus-left",
section: "tabs-panes",
label: "Focus pane left",
keys: ["mod", "shift", "Left"],
},
},
{
@@ -616,7 +598,6 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "workspace-pane-focus-right",
section: "tabs-panes",
label: "Focus pane right",
keys: ["mod", "shift", "Right"],
},
},
{
@@ -628,7 +609,6 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "workspace-pane-focus-up",
section: "tabs-panes",
label: "Focus pane up",
keys: ["mod", "shift", "Up"],
},
},
{
@@ -640,7 +620,6 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "workspace-pane-focus-down",
section: "tabs-panes",
label: "Focus pane down",
keys: ["mod", "shift", "Down"],
},
},
{
@@ -652,7 +631,6 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "workspace-pane-move-tab-left",
section: "tabs-panes",
label: "Move tab left",
keys: ["mod", "shift", "alt", "Left"],
},
},
{
@@ -664,7 +642,6 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "workspace-pane-move-tab-right",
section: "tabs-panes",
label: "Move tab right",
keys: ["mod", "shift", "alt", "Right"],
},
},
{
@@ -676,7 +653,6 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "workspace-pane-move-tab-up",
section: "tabs-panes",
label: "Move tab up",
keys: ["mod", "shift", "alt", "Up"],
},
},
{
@@ -688,7 +664,6 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "workspace-pane-move-tab-down",
section: "tabs-panes",
label: "Move tab down",
keys: ["mod", "shift", "alt", "Down"],
},
},
{
@@ -700,7 +675,6 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "workspace-pane-close",
section: "tabs-panes",
label: "Close pane",
keys: ["mod", "shift", "W"],
},
},
@@ -714,7 +688,6 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "workspace-terminal-new",
section: "panels",
label: "New terminal",
keys: ["mod", "shift", "T"],
},
},
{
@@ -726,7 +699,6 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "workspace-terminal-new",
section: "panels",
label: "New terminal",
keys: ["mod", "shift", "T"],
},
},
@@ -740,7 +712,6 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "toggle-command-center",
section: "panels",
label: "Toggle command center",
keys: ["mod", "K"],
},
},
{
@@ -752,7 +723,6 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "toggle-command-center",
section: "panels",
label: "Toggle command center",
keys: ["mod", "K"],
},
},
@@ -767,7 +737,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "show-shortcuts",
section: "panels",
label: "Show keyboard shortcuts",
keys: ["?"],
defaultDisplayKeys: ["?"],
note: "Available when focus is not in a text field or terminal.",
},
},
@@ -782,7 +752,6 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "toggle-left-sidebar",
section: "panels",
label: "Toggle left sidebar",
keys: ["mod", "B"],
},
},
{
@@ -794,7 +763,6 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "toggle-left-sidebar",
section: "panels",
label: "Toggle left sidebar",
keys: ["mod", "B"],
},
},
{
@@ -806,7 +774,6 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "toggle-right-sidebar",
section: "panels",
label: "Toggle right sidebar",
keys: ["mod", "E"],
},
},
{
@@ -818,7 +785,6 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "toggle-right-sidebar",
section: "panels",
label: "Toggle right sidebar",
keys: ["mod", "E"],
},
},
{
@@ -838,7 +804,6 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "toggle-both-sidebars",
section: "panels",
label: "Toggle both sidebars",
keys: ["mod", "."],
},
},
{
@@ -850,7 +815,6 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "toggle-both-sidebars",
section: "panels",
label: "Toggle both sidebars",
keys: ["mod", "."],
},
},
@@ -864,7 +828,6 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "toggle-settings",
section: "panels",
label: "Toggle settings",
keys: ["mod", ","],
},
},
{
@@ -876,7 +839,6 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "toggle-settings",
section: "panels",
label: "Toggle settings",
keys: ["mod", ","],
},
},
@@ -890,7 +852,6 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "toggle-focus",
section: "panels",
label: "Toggle focus mode",
keys: ["mod", "shift", "F"],
},
},
{
@@ -902,7 +863,6 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "toggle-focus",
section: "panels",
label: "Toggle focus mode",
keys: ["mod", "shift", "F"],
},
},
@@ -916,7 +876,6 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "cycle-theme",
section: "panels",
label: "Cycle theme",
keys: ["mod", "alt", "T"],
},
},
{
@@ -928,7 +887,6 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "cycle-theme",
section: "panels",
label: "Cycle theme",
keys: ["mod", "alt", "T"],
},
},
@@ -943,7 +901,6 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "focus-message-input",
section: "agent-input",
label: "Focus message input",
keys: ["mod", "L"],
},
},
{
@@ -956,7 +913,6 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "focus-message-input",
section: "agent-input",
label: "Focus message input",
keys: ["mod", "L"],
},
},
{
@@ -970,7 +926,6 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "cycle-agent-mode",
section: "agent-input",
label: "Cycle agent mode",
keys: ["shift", "Tab"],
},
},
{
@@ -984,7 +939,6 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "voice-toggle",
section: "agent-input",
label: "Toggle voice mode",
keys: ["mod", "shift", "D"],
},
},
{
@@ -998,7 +952,6 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "voice-toggle",
section: "agent-input",
label: "Toggle voice mode",
keys: ["mod", "shift", "D"],
},
},
{
@@ -1011,7 +964,6 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "dictation-toggle",
section: "agent-input",
label: "Start/stop dictation",
keys: ["mod", "D"],
},
},
{
@@ -1024,7 +976,6 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "dictation-toggle",
section: "agent-input",
label: "Start/stop dictation",
keys: ["mod", "D"],
},
},
{
@@ -1038,7 +989,6 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "agent-interrupt",
section: "agent-input",
label: "Interrupt agent",
keys: ["Esc"],
},
},
{
@@ -1060,7 +1010,6 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
id: "voice-mute-toggle",
section: "agent-input",
label: "Mute/unmute voice mode",
keys: ["Space"],
},
},
];
@@ -1123,7 +1072,11 @@ export function buildEffectiveBindings(overrides: ShortcutOverrides): ParsedShor
if (binding.repeat === false && lastCombo) {
lastCombo.repeat = false;
}
return { ...binding, combo: override, parsedChord };
if (!binding.help?.defaultDisplayKeys) {
return { ...binding, combo: override, parsedChord };
}
const { defaultDisplayKeys: _defaultDisplayKeys, ...help } = binding.help;
return { ...binding, combo: override, parsedChord, help };
});
}
@@ -1422,11 +1375,33 @@ export function getBindingIdForAction(
return null;
}
/**
* The keys to display for one binding, derived from the combo that actually
* fires so a rebound shortcut never advertises the keys it shipped with.
*
* `help.defaultDisplayKeys` wins where it is set, but only on the shipped
* binding. Effective bindings discard it when an override replaces the combo,
* so rebound rows still derive the keys that now fire.
* `parsedChord` is the single source of truth for "has no keys": it is derived
* from `combo`, so it covers both a user-unassigned shortcut and a binding
* authored without a default.
*/
function displayChordForBinding(binding: ParsedShortcutBinding): ShortcutKey[][] | null {
if (binding.parsedChord.length === 0) {
return null;
}
const displayKeys = binding.help?.defaultDisplayKeys;
if (displayKeys) {
return [displayKeys];
}
return chordStringToShortcutKeys(binding.combo);
}
export function getDefaultKeysForAction(
actionId: string,
platform: { isMac: boolean; isDesktop: boolean },
bindings: readonly ParsedShortcutBinding[] = DEFAULT_BINDINGS,
): ShortcutKey[] | null {
): ShortcutKey[][] | null {
for (const binding of bindings) {
if (binding.help?.id !== actionId) {
continue;
@@ -1434,13 +1409,7 @@ export function getDefaultKeysForAction(
if (!helpMatchesPlatform(binding.when, platform)) {
continue;
}
// `help.keys` is hand-authored, so it cannot be trusted to be empty for a
// binding that ships without a default combo. The parsed chord is derived
// from `combo`, so it is the single source of truth for "has no keys".
if (binding.parsedChord.length === 0) {
return null;
}
return binding.help.keys;
return displayChordForBinding(binding);
}
return null;
}
@@ -1464,8 +1433,7 @@ export function resolveShortcutKeysForAction(
return null;
}
const defaultKeys = getDefaultKeysForAction(actionId, platform);
const defaultChord = defaultKeys ? [defaultKeys] : null;
const defaultChord = getDefaultKeysForAction(actionId, platform);
const override = overrides[bindingId];
if (override === UNASSIGNED_COMBO || override === "") {
@@ -1566,8 +1534,7 @@ export function buildKeyboardShortcutHelpSections(
id: help.id,
label: help.label,
labelKey: SHORTCUT_HELP_LABEL_KEYS[help.id] ?? help.label,
// An empty chord has no keys to show, whatever `help.keys` was authored as.
keys: binding.parsedChord.length === 0 ? [] : help.keys,
chord: displayChordForBinding(binding),
...(help.note ? { note: help.note } : {}),
...(SHORTCUT_HELP_NOTE_KEYS[help.id] ? { noteKey: SHORTCUT_HELP_NOTE_KEYS[help.id] } : {}),
});
@@ -0,0 +1,113 @@
import { describe, expect, it } from "vitest";
import {
buildEffectiveBindings,
buildKeyboardShortcutHelpSections,
UNASSIGNED_COMBO,
type ShortcutOverrides,
} from "./keyboard-shortcuts";
import { filterShortcutHelpSections, shortcutSearchText } from "./shortcut-help-search";
const MAC_DESKTOP = { isMac: true, isDesktop: true };
const NEW_WORKSPACE_BINDING = "workspace-new-cmd-n-mac";
/**
* The i18n port, filled with the real English strings the rows use. Translation
* is the only dependency the filter cannot compute, so it is injected rather
* than mocked.
*/
const EN: Record<string, string> = {
"settings.shortcuts.sections.projects": "Projects",
"settings.shortcuts.help.newWorkspace": "New workspace",
"settings.shortcuts.help.openProject": "Open project",
};
function translate(key: string): string {
return EN[key] ?? key;
}
function filter(query: string, overrides: ShortcutOverrides = {}) {
return filterShortcutHelpSections({
sections: buildKeyboardShortcutHelpSections(MAC_DESKTOP, buildEffectiveBindings(overrides)),
query,
translate,
shortcutOs: "mac",
});
}
function matchedRowIds(query: string, overrides: ShortcutOverrides = {}): string[] {
return filter(query, overrides).flatMap((section) => section.rows.map((row) => row.id));
}
describe("shortcutSearchText", () => {
it("covers the tokens, the rendered badge, and the modifier aliases", () => {
const text = shortcutSearchText([["mod", "N"]], "mac");
expect(text).toContain("mod N");
expect(text).toContain("⌘N");
expect(text).toContain("cmd+N");
expect(text).toContain("command+N");
});
// The alias expansion is combinatorial, so a flattened chord would multiply
// the two combos together and invent spellings for keys never pressed at once.
it("keeps a multi-step chord's combos apart", () => {
const text = shortcutSearchText(
[
["mod", "K"],
["mod", "N"],
],
"mac",
);
expect(text).toContain("cmd+K");
expect(text).toContain("cmd+N");
expect(text).not.toContain("cmd+K+cmd+N");
});
});
describe("filterShortcutHelpSections", () => {
it("returns every section for a blank query", () => {
expect(filter(" ")).toHaveLength(buildKeyboardShortcutHelpSections(MAC_DESKTOP).length);
});
it("matches a row by its label", () => {
expect(matchedRowIds("new workspace")).toContain("new-workspace");
});
it("matches a row by the rendered badge", () => {
expect(matchedRowIds("⌘n")).toContain("new-workspace");
});
it("matches a row by a spelled-out modifier", () => {
expect(matchedRowIds("command+n")).toContain("new-workspace");
});
// The reported bug, from the search side: the cheat sheet has to stop finding
// a row by keys the user has rebound away from.
it("finds a rebound row by its new keys and not by the superseded default", () => {
const overrides = { [NEW_WORKSPACE_BINDING]: "Cmd+Shift+K" };
expect(matchedRowIds("cmd+shift+k", overrides)).toContain("new-workspace");
expect(matchedRowIds("cmd+n", overrides)).not.toContain("new-workspace");
});
it("still finds an unassigned row by its label", () => {
const overrides = { [NEW_WORKSPACE_BINDING]: UNASSIGNED_COMBO };
expect(matchedRowIds("new workspace", overrides)).toContain("new-workspace");
expect(matchedRowIds("cmd+n", overrides)).not.toContain("new-workspace");
});
it("keeps every row of a section whose own title matches", () => {
const projects = filter("projects").find((section) => section.id === "projects");
const allProjects = buildKeyboardShortcutHelpSections(MAC_DESKTOP).find(
(section) => section.id === "projects",
);
expect(projects?.rows).toHaveLength(allProjects?.rows.length ?? 0);
});
it("drops a section whose rows all miss", () => {
expect(filter("no shortcut spells this")).toEqual([]);
});
});
@@ -0,0 +1,90 @@
import type { KeyboardShortcutHelpSection } from "@/keyboard/keyboard-shortcuts";
import { formatShortcut, type ShortcutKey, type ShortcutOs } from "@/utils/format-shortcut";
/** Resolves an i18n key to display text. The caller's `t`, injected. */
export type TranslateHelpKey = (key: string) => string;
/**
* Alternative spellings for one combo, so searching "cmd k" and "command+k" both
* find ⌘K. The modifier a user types rarely matches the token the binding
* stores, and never matches the symbol the badge renders.
*/
export function shortcutSearchAliases(
keys: readonly ShortcutKey[],
shortcutOs: ShortcutOs,
): string {
const aliases = keys.map((key) => {
if (shortcutOs === "mac") {
if (key === "mod" || key === "meta") return ["cmd", "command"];
if (key === "alt") return ["alt", "option"];
} else {
if (key === "mod" || key === "ctrl") return ["ctrl", "control"];
if (key === "meta") return ["win", "windows"];
}
return [key];
});
const combinations = aliases.reduce<string[][]>(
(prefixes, choices) =>
prefixes.flatMap((prefix) => choices.map((choice) => [...prefix, choice])),
[[]],
);
return combinations
.flatMap((combination) => [combination.join(" "), combination.join("+")])
.join(" ");
}
/**
* Search text for one row's keys. Built per combo and joined, never over a
* flattened chord: the alias expansion is combinatorial, so flattening a
* multi-step chord into one key list both explodes the combination count and
* invents aliases for combos the user never has to press together.
*/
export function shortcutSearchText(chord: ShortcutKey[][], shortcutOs: ShortcutOs): string {
return chord
.flatMap((combo) => [
combo.join(" "),
formatShortcut(combo, shortcutOs),
shortcutSearchAliases(combo, shortcutOs),
])
.join(" ");
}
/**
* The cheat sheet's sections narrowed to a query, matching on the row's label,
* its note, and every spelling of the keys that actually fire it. A section
* whose own title matches keeps all of its rows.
*/
export function filterShortcutHelpSections({
sections,
query,
translate,
shortcutOs,
}: {
sections: readonly KeyboardShortcutHelpSection[];
query: string;
translate: TranslateHelpKey;
shortcutOs: ShortcutOs;
}): KeyboardShortcutHelpSection[] {
const normalizedQuery = query.trim().toLocaleLowerCase();
if (!normalizedQuery) return [...sections];
return sections.flatMap((section) => {
if (translate(section.titleKey).toLocaleLowerCase().includes(normalizedQuery)) {
return [section];
}
const rows = section.rows.filter((row) => {
const searchText = [
translate(row.labelKey),
row.noteKey ? translate(row.noteKey) : row.note,
row.chord ? shortcutSearchText(row.chord, shortcutOs) : null,
]
.filter(Boolean)
.join(" ")
.toLocaleLowerCase();
return searchText.includes(normalizedQuery);
});
return rows.length > 0 ? [{ ...section, rows }] : [];
});
}
@@ -1,8 +1,11 @@
import { describe, expect, it } from "vitest";
import { formatShortcut } from "@/utils/format-shortcut";
import {
comboStringToShortcutKeys,
keyComboToString,
keyboardEventToComboString,
parseShortcutString,
SHORTCUT_KEY_NAMES,
} from "./shortcut-string";
function keyboardEvent(overrides: Partial<KeyboardEvent>): KeyboardEvent {
@@ -68,3 +71,40 @@ describe("parseShortcutString round-trips punctuation keys", () => {
expect(keyComboToString(combo)).toBe("Cmd+-");
});
});
describe("comboStringToShortcutKeys", () => {
it("maps every modifier spelling onto its display token", () => {
expect(comboStringToShortcutKeys("Cmd+Ctrl+Alt+Shift+K")).toEqual([
"mod",
"ctrl",
"alt",
"shift",
"K",
]);
// `parseShortcutString` accepts `Mod` as well, so the display path has to.
expect(comboStringToShortcutKeys("Mod+K")).toEqual(["mod", "K"]);
});
it("hands named keys to the display table under the names it uses", () => {
expect(comboStringToShortcutKeys("Cmd+Shift+ArrowLeft")).toEqual(["mod", "shift", "Left"]);
expect(formatShortcut(comboStringToShortcutKeys("Cmd+Shift+ArrowLeft"), "mac")).toBe(
"Shift+⌘+←",
);
expect(formatShortcut(comboStringToShortcutKeys("Escape"), "mac")).toBe("Esc");
expect(formatShortcut(comboStringToShortcutKeys("Backspace"), "mac")).toBe("⌫");
});
// The whole class, not just the arrows: uppercasing key names used to push
// every multi-character name past `KEY_DISPLAY`, so an override on one of them
// rendered as a shouty raw code — `ARROWLEFT`, `ESCAPE`, `PAGEDOWN`.
it("never emits a shouty raw code for any key the capture path can produce", () => {
for (const name of SHORTCUT_KEY_NAMES) {
// `F1` and the digits are already all-caps, so only names that contain a
// lowercase letter can exhibit the bug.
if (name === name.toUpperCase()) continue;
const [key] = comboStringToShortcutKeys(name);
const rendered = formatShortcut([key], "non-mac");
expect(rendered, `${name} renders as ${rendered}`).not.toBe(name.toUpperCase());
}
});
});
+31 -1
View File
@@ -74,6 +74,13 @@ for (let i = 1; i <= 12; i++) {
KEY_MAP[`F${i}`] = { code: `F${i}` };
}
/**
* Every key name a combo string can carry. Exported so the display path can be
* driven over the whole vocabulary instead of a hand-copied sample that goes
* stale the moment `KEY_MAP` gains an entry.
*/
export const SHORTCUT_KEY_NAMES: readonly string[] = Object.keys(KEY_MAP);
const CODE_TO_KEY: Record<string, string> = {};
for (const [humanKey, mapping] of Object.entries(KEY_MAP)) {
if (!CODE_TO_KEY[mapping.code]) {
@@ -177,6 +184,21 @@ const MODIFIER_CODES = new Set([
"ShiftRight",
]);
/**
* Key names as combo strings spell them, mapped to the names `KEY_DISPLAY` in
* `format-shortcut` is keyed on. The two vocabularies were never reconciled:
* `keyboardEventToComboString` emits `ArrowLeft` and `Escape` (from `KEY_MAP`),
* while the display table only knows `Left` and `Esc`. Without this bridge an
* override on a named key renders as a raw code — `ARROWLEFT`, not `←`.
*/
const DISPLAY_KEY_ALIASES: Record<string, string> = {
ArrowLeft: "Left",
ArrowRight: "Right",
ArrowUp: "Up",
ArrowDown: "Down",
Escape: "Esc",
};
export function comboStringToShortcutKeys(comboString: string): ShortcutKey[] {
const parts = comboString.split("+");
const keys: ShortcutKey[] = [];
@@ -185,6 +207,11 @@ export function comboStringToShortcutKeys(comboString: string): ShortcutKey[] {
case "Cmd":
keys.push("mod");
break;
// `parseShortcutString` accepts `Mod`, so the display path has to as well
// or a combo it can match would render the literal word.
case "Mod":
keys.push("mod");
break;
case "Ctrl":
keys.push("ctrl");
break;
@@ -195,7 +222,10 @@ export function comboStringToShortcutKeys(comboString: string): ShortcutKey[] {
keys.push("shift");
break;
default:
keys.push(part.toUpperCase());
// Pass the key through unchanged. `normalizeKey` already uppercases
// single characters, and uppercasing here is what used to push
// multi-character names past `KEY_DISPLAY` into raw output.
keys.push(DISPLAY_KEY_ALIASES[part] ?? part);
break;
}
}
@@ -51,6 +51,17 @@ async function openRowMenu(page: Page) {
await expect(page.getByTestId(`shortcut-bind-${SHORTCUTS_ROW}`)).toBeVisible();
}
/** Reachable from the sidebar even when the cheat sheet's own shortcut is gone. */
async function openCheatSheet(page: Page) {
await gotoAppShell(page);
await page.getByTestId("sidebar-help").click();
await expect(page.getByTestId("sidebar-help-menu")).toBeVisible();
await page.getByTestId("sidebar-help-shortcuts").click();
const dialog = page.getByTestId("keyboard-shortcuts-dialog");
await expect(dialog).toBeVisible({ timeout: 10_000 });
return dialog;
}
async function closeRowMenu(page: Page) {
await page.keyboard.press("Escape");
await expect(page.getByTestId(`shortcut-bind-${SHORTCUTS_ROW}`)).toHaveCount(0);
@@ -120,20 +131,38 @@ test("an unassigned shortcut lists no keys in the shortcuts cheat sheet", async
await page.getByTestId(`shortcut-clear-${SHORTCUTS_ROW}`).click();
await expect(page.getByText("Not set", { exact: true })).toBeVisible();
// Reachable from the sidebar even with its own shortcut unassigned.
await gotoAppShell(page);
await page.getByTestId("sidebar-help").click();
await expect(page.getByTestId("sidebar-help-menu")).toBeVisible();
await page.getByTestId("sidebar-help-shortcuts").click();
const dialog = await openCheatSheet(page);
const dialog = page.getByTestId("keyboard-shortcuts-dialog");
await expect(dialog).toBeVisible({ timeout: 10_000 });
const row = dialog
.locator("div")
.filter({ hasText: /^Show keyboard shortcuts$/ })
.first();
// Addressed by testID rather than by filtering on its own text: an anchored
// `hasText` matches only the innermost node holding exactly that label, so a
// row-scoped assertion about anything *else* in the row finds nothing.
const row = dialog.getByTestId(`shortcut-help-row-${SHORTCUTS_ROW}`);
await expect(row).toBeVisible();
// No badge pill, blank or otherwise, for a shortcut with no keys.
await expect(dialog.getByText("?", { exact: true })).toHaveCount(0);
// It says so, rather than leaving a silent gap where the keys were. Same words
// the settings row uses for the same state.
await expect(row.getByText("Not set", { exact: true })).toBeVisible();
});
test("a rebound shortcut lists its new keys in the shortcuts cheat sheet", async ({ page }) => {
await openShortcutsSettings(page);
const defaultKeys = page.getByText("?", { exact: true });
await expect(defaultKeys).toBeVisible();
await page.getByTestId(`shortcut-actions-${SHORTCUTS_ROW}`).click();
await page.getByTestId(`shortcut-bind-${SHORTCUTS_ROW}`).click();
await page.keyboard.press("Alt+Shift+K");
await page.getByText("Done", { exact: true }).click();
const reboundKeys = page.getByText("⌥+Shift+K", { exact: true });
await expect(reboundKeys).toBeVisible();
await expect(defaultKeys).toHaveCount(0);
const dialog = await openCheatSheet(page);
const row = dialog.getByTestId(`shortcut-help-row-${SHORTCUTS_ROW}`);
await expect(row.getByText("⌥+Shift+K", { exact: true })).toBeVisible();
// The whole point: the cheat sheet stops advertising the shipped default.
await expect(row.getByText("?", { exact: true })).toHaveCount(0);
});