From 5c2a1552df8e286c2c64cf0495b3c5fbc4c8922d Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Fri, 16 Oct 2026 04:14:28 +0200 Subject: [PATCH] fix(app): preserve number groups when rebinding navigation shortcuts (#7065) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Recording a digit for tab or workspace navigation replaced the existing 1–9 wildcard with one literal key. Preserve the group when capturing a replacement and display its range after saving and reloading. Adapted the number-group fix from getpaseo/paseo#5237, with independent failing-first renderer reproduction and tab/workspace matcher coverage. Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> Co-authored-by: dbunk903 <74584941+dbunk903@users.noreply.github.com> --- .../src/keyboard/keyboard-shortcuts.test.ts | 47 +++++++++++++++++- .../app/src/keyboard/keyboard-shortcuts.ts | 7 +++ packages/app/src/keyboard/shortcut-string.ts | 1 + .../settings/keyboard-shortcuts-section.tsx | 4 +- .../e2e/keyboard-shortcut-unassign.spec.ts | 48 +++++++++++++++++++ 5 files changed, 104 insertions(+), 3 deletions(-) diff --git a/packages/app/src/keyboard/keyboard-shortcuts.test.ts b/packages/app/src/keyboard/keyboard-shortcuts.test.ts index 571474643b..9e56ef66cd 100644 --- a/packages/app/src/keyboard/keyboard-shortcuts.test.ts +++ b/packages/app/src/keyboard/keyboard-shortcuts.test.ts @@ -4,6 +4,7 @@ import { keyboardEventToComboString } from "./shortcut-string"; import { buildKeyboardShortcutHelpSections, buildEffectiveBindings, + normalizeCapturedShortcutCombo, getBindingIdForAction, getDefaultKeysForAction, getWorkspaceIndexJumpModifierKey, @@ -823,9 +824,9 @@ describe("keyboard-shortcut help sections", () => { expect(rowChord({}, "show-shortcuts")).toEqual([["?"]]); }); - it("replaces the default-only wildcard when the index jump is rebound", () => { + it("keeps the digit range readable when the index jump is rebound", () => { expect(rowChord({ [MAC_INDEX_BINDING]: "Ctrl+Digit" }, "workspace-jump-index")).toEqual([ - ["ctrl", "Digit"], + ["ctrl", "1-9"], ]); }); @@ -1347,3 +1348,45 @@ describe("direct new-tab target shortcuts", () => { ).toEqual([["ctrl", "shift", "H"]]); }); }); + +describe("number-group shortcut capture", () => { + const tabBindingId = "workspace-tab-navigate-index-cmd-alt-digit-mac-desktop"; + it.each([ + { id: tabBindingId, action: "workspace.tab.navigate.index", row: "workspace-tab-jump-index" }, + { + id: "workspace-navigate-index-cmd-digit-mac", + action: "workspace.navigate.index", + row: "workspace-jump-index", + }, + ])("captures Ctrl+1 as the entire number group for $row", ({ id, action, row }) => { + const combo = normalizeCapturedShortcutCombo(id, "Ctrl+1"); + expect(combo).toBe("Ctrl+Digit"); + const overrides = { [id]: combo }; + const bindings = buildEffectiveBindings(overrides); + for (let index = 1; index <= 9; index++) { + const result = resolveShortcut({ + event: { key: String(index), code: `Digit${index}`, ctrlKey: true }, + context: { isMac: true, isDesktop: true, focusScope: "message-input" }, + bindings, + }); + expect(result.match?.action).toBe(action); + expect(result.match?.payload).toEqual({ index }); + } + expect(resolveShortcutKeysForAction(row, overrides, { isMac: true, isDesktop: true })).toEqual([ + ["ctrl", "1-9"], + ]); + expect( + resolveShortcut({ + event: { key: "0", code: "Digit0", ctrlKey: true }, + context: { isMac: true, isDesktop: true }, + bindings, + }).match, + ).toBeNull(); + }); + it("preserves literal numbers for non-index actions and existing nonnumeric combos", () => { + expect(normalizeCapturedShortcutCombo("show-shortcuts", "Ctrl+1")).toBe("Ctrl+1"); + expect(normalizeCapturedShortcutCombo(tabBindingId, "Ctrl+K")).toBe("Ctrl+K"); + expect(normalizeCapturedShortcutCombo(tabBindingId, "Ctrl+0")).toBe("Ctrl+0"); + expect(normalizeCapturedShortcutCombo(tabBindingId, "Ctrl+9")).toBe("Ctrl+Digit"); + }); +}); diff --git a/packages/app/src/keyboard/keyboard-shortcuts.ts b/packages/app/src/keyboard/keyboard-shortcuts.ts index 54d2b59cdb..a009008b85 100644 --- a/packages/app/src/keyboard/keyboard-shortcuts.ts +++ b/packages/app/src/keyboard/keyboard-shortcuts.ts @@ -1210,6 +1210,13 @@ export const DEFAULT_BINDINGS: readonly ParsedShortcutBinding[] = export type ShortcutOverrides = Record; +/** Keep number-group actions bound to all nine indices when recording a new modifier. */ +export function normalizeCapturedShortcutCombo(bindingId: string, combo: string): string { + const binding = DEFAULT_BINDINGS.find((candidate) => candidate.id === bindingId); + if (binding?.payload?.type !== "index") return combo; + return combo.replace(/(^|\+)[1-9]$/, "$1Digit"); +} + export function buildEffectiveBindings(overrides: ShortcutOverrides): ParsedShortcutBinding[] { return DEFAULT_BINDINGS.map(function (binding) { const override = overrides[binding.id]; diff --git a/packages/app/src/keyboard/shortcut-string.ts b/packages/app/src/keyboard/shortcut-string.ts index 8b632befbd..6b80009633 100644 --- a/packages/app/src/keyboard/shortcut-string.ts +++ b/packages/app/src/keyboard/shortcut-string.ts @@ -192,6 +192,7 @@ const MODIFIER_CODES = new Set([ * override on a named key renders as a raw code — `ARROWLEFT`, not `←`. */ const DISPLAY_KEY_ALIASES: Record = { + Digit: "1-9", ArrowLeft: "Left", ArrowRight: "Right", ArrowUp: "Up", diff --git a/packages/app/src/screens/settings/keyboard-shortcuts-section.tsx b/packages/app/src/screens/settings/keyboard-shortcuts-section.tsx index 3798a5f9bd..8a8c4b42b9 100644 --- a/packages/app/src/screens/settings/keyboard-shortcuts-section.tsx +++ b/packages/app/src/screens/settings/keyboard-shortcuts-section.tsx @@ -19,6 +19,7 @@ import { useKeyboardShortcutOverrides } from "@/hooks/use-keyboard-shortcut-over import { buildKeyboardShortcutHelpSections, getBindingIdForAction, + normalizeCapturedShortcutCombo, getDefaultKeysForAction, resolveShortcutKeysForAction, type KeyboardShortcutHelpRow, @@ -389,7 +390,8 @@ export function KeyboardShortcutsSection() { } setHeldModifiers(null); - setCapturedCombos((current) => [...current, comboString]); + const capturedCombo = normalizeCapturedShortcutCombo(capturingBindingId!, comboString); + setCapturedCombos((current) => [...current, capturedCombo]); } window.addEventListener("keydown", handleKeyDown, true); diff --git a/packages/desktop/e2e/keyboard-shortcut-unassign.spec.ts b/packages/desktop/e2e/keyboard-shortcut-unassign.spec.ts index 1d0b829f67..b7063b4565 100644 --- a/packages/desktop/e2e/keyboard-shortcut-unassign.spec.ts +++ b/packages/desktop/e2e/keyboard-shortcut-unassign.spec.ts @@ -1,3 +1,5 @@ +import { clickNewChat, gotoWorkspace } from "../../app/e2e/support/helpers/launcher"; +import { clickSettingsBackToWorkspace } from "../../app/e2e/support/helpers/settings"; import type { Page } from "@playwright/test"; import { test, expect } from "../../app/e2e/support/fixtures"; import { gotoAppShell, openSettings } from "../../app/e2e/support/helpers/app"; @@ -213,3 +215,49 @@ for (const { platform, keys, label, firstCombo, secondCombo } of [ await expectInterruptShortcut(page, label); }); } + +async function rebindTabNumbers(page: Page) { + await captureShortcut(page, "Jump to tab", "workspace-tab-jump-index", "Control+1"); + await page.getByRole("button", { name: "Done", exact: true }).click(); +} + +async function expectTabNumberRange(page: Page) { + const row = page.getByText("Jump to tab", { exact: true }).locator(".."); + await expect(row.getByText("⌃1-9", { exact: true })).toBeVisible(); +} + +async function openTwoDraftTabs(page: Page, workspaceId: string) { + await installDesktopBridge(page); + await gotoWorkspace(page, workspaceId); + await clickNewChat(page); + await clickNewChat(page); + await expect( + page.locator('[data-testid^="workspace-tab-draft_"]').filter({ visible: true }), + ).toHaveCount(2); + await openSettings(page); + await openSettingsSection(page, "shortcuts"); +} + +async function expectTabNumberNavigation(page: Page) { + const tabs = page.locator('[data-testid^="workspace-tab-draft_"]').filter({ visible: true }); + await page.keyboard.press("Control+1"); + await expect(tabs.nth(0)).toHaveAttribute("aria-selected", "true"); + await page.keyboard.press("Control+2"); + await expect(tabs.nth(1)).toHaveAttribute("aria-selected", "true"); +} + +test("rebinding Jump to tab preserves the number group after reload", async ({ + page, + withWorkspace, +}) => { + const workspace = await withWorkspace({ prefix: "tab-number-rebind-" }); + await openTwoDraftTabs(page, workspace.workspaceId); + await rebindTabNumbers(page); + await clickSettingsBackToWorkspace(page); + await expectTabNumberNavigation(page); + await page.reload(); + await expectTabNumberNavigation(page); + await openSettings(page); + await openSettingsSection(page, "shortcuts"); + await expectTabNumberRange(page); +});