From 3fd5d6439d8fd49d173503ecda96500463a39bd2 Mon Sep 17 00:00:00 2001 From: Yash Singh Date: Thu, 17 Sep 2026 19:43:03 -0500 Subject: [PATCH] fix(web): keep numbered jumps from stealing browser tabs (#12315) Co-authored-by: Cursor --- apps/web/src/components/ChatView.tsx | 2 + .../PullRequestDetailPanel.test.tsx | 2 + .../KeybindingsSettings.logic.test.ts | 10 ++- .../settings/KeybindingsSettings.logic.ts | 9 ++- apps/web/src/keybindings.test.ts | 63 ++++++++++++++++++- apps/web/src/keybindings.ts | 5 ++ apps/web/src/routes/_chat.pull-requests.tsx | 2 + docs/user/keybindings.md | 9 ++- packages/shared/src/keybindings.ts | 3 +- pnpm-lock.yaml | 4 +- 10 files changed, 102 insertions(+), 7 deletions(-) diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index e27cc4543b561dfa38c17132b9ab178585628af0..79d393fc8d1f180c147e6b7053c13f61f5fbd66c 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -6652,6 +6652,8 @@ export default function ChatView(props: ChatViewProps) { previewFocus: isPreviewFocused(), previewOpen: previewPanelOpen, modelPickerOpen: composerRef.current?.isModelPickerOpen() ?? false, + isWeb: !isElectron, + isDesktop: isElectron, }), [composerRef, previewPanelOpen, terminalUiState.terminalOpen], ); diff --git a/apps/web/src/components/pullRequest/PullRequestDetailPanel.test.tsx b/apps/web/src/components/pullRequest/PullRequestDetailPanel.test.tsx index 4bdf6ee7d5359a2ba06bf1c1d060708395d5b931..18320ae33566e9363ab0a8ef9ca9e16c6d42bd0a 100644 --- a/apps/web/src/components/pullRequest/PullRequestDetailPanel.test.tsx +++ b/apps/web/src/components/pullRequest/PullRequestDetailPanel.test.tsx @@ -294,6 +294,8 @@ describe.each([ terminalOpen: false, previewFocus: false, previewOpen: false, + isWeb: true, + isDesktop: false, })} />, ); diff --git a/apps/web/src/components/settings/KeybindingsSettings.logic.test.ts b/apps/web/src/components/settings/KeybindingsSettings.logic.test.ts index ff3b30124a738a57fffd5757494d58cb7f4a1eed..10fc154d5b0c584b2dfec9a26c719a1e53805ec6 100644 --- a/apps/web/src/components/settings/KeybindingsSettings.logic.test.ts +++ b/apps/web/src/components/settings/KeybindingsSettings.logic.test.ts @@ -210,7 +210,15 @@ describe("KeybindingsSettings.logic", () => { const options = buildWhenVariableOptions(); expect(options).toEqual( - expect.arrayContaining(["terminalFocus", "terminalOpen", "modelPickerOpen", "true", "false"]), + expect.arrayContaining([ + "terminalFocus", + "terminalOpen", + "isWeb", + "isDesktop", + "modelPickerOpen", + "true", + "false", + ]), ); expect(options).not.toContain("customModeActive"); }); diff --git a/apps/web/src/components/settings/KeybindingsSettings.logic.ts b/apps/web/src/components/settings/KeybindingsSettings.logic.ts index a910c7e5535b9585f89ce913cdda11db678d89dd..dc4d33bc28ba1f8cf33c4669f0185d65bb3cb086 100644 --- a/apps/web/src/components/settings/KeybindingsSettings.logic.ts +++ b/apps/web/src/components/settings/KeybindingsSettings.logic.ts @@ -31,7 +31,14 @@ export interface KeybindingRow { export type WhenVariableOption = string; export type KeybindingCommandOption = KeybindingCommand; -const CORE_WHEN_VARIABLES = ["terminalFocus", "terminalOpen", "true", "false"] as const; +const CORE_WHEN_VARIABLES = [ + "terminalFocus", + "terminalOpen", + "isWeb", + "isDesktop", + "true", + "false", +] as const; const DEFAULT_WHEN_VARIABLES = new Set(CORE_WHEN_VARIABLES); for (const binding of DEFAULT_RESOLVED_KEYBINDINGS) { diff --git a/apps/web/src/keybindings.test.ts b/apps/web/src/keybindings.test.ts index 3bec6007f7070485b0a77308c5c433bb75501bf6..d9c3eccbeb4e81c8e39efbecf50d36b895109ed1 100644 --- a/apps/web/src/keybindings.test.ts +++ b/apps/web/src/keybindings.test.ts @@ -535,6 +535,43 @@ describe("thread navigation helpers", () => { }), ); }); + + it("keeps default thread jumps off the web so the browser can switch tabs", () => { + const input = event({ key: "1", metaKey: true }); + assert.isNull( + resolveShortcutCommand(input, DEFAULT_RESOLVED_KEYBINDINGS, { + platform: "MacIntel", + context: { isDesktop: false }, + }), + ); + assert.strictEqual( + resolveShortcutCommand(input, DEFAULT_RESOLVED_KEYBINDINGS, { + platform: "MacIntel", + context: { isDesktop: true }, + }), + "thread.jump.1", + ); + assert.isFalse( + shouldShowThreadJumpHintsForModifiers( + event({ metaKey: true }), + DEFAULT_RESOLVED_KEYBINDINGS, + { + platform: "MacIntel", + context: { isDesktop: false }, + }, + ), + ); + assert.isTrue( + shouldShowThreadJumpHintsForModifiers( + event({ metaKey: true }), + DEFAULT_RESOLVED_KEYBINDINGS, + { + platform: "MacIntel", + context: { isDesktop: true }, + }, + ), + ); + }); }); describe("model picker navigation helpers", () => { @@ -546,6 +583,30 @@ describe("model picker navigation helpers", () => { assert.strictEqual(modelPickerJumpIndexFromCommand("modelPicker.jump.3"), 2); assert.isNull(modelPickerJumpIndexFromCommand("thread.jump.1")); }); + + it("keeps default model jumps off the web even while the picker is open", () => { + const input = event({ key: "3", metaKey: true }); + assert.isNull( + resolveShortcutCommand(input, DEFAULT_RESOLVED_KEYBINDINGS, { + platform: "MacIntel", + context: { isDesktop: false, modelPickerOpen: true }, + }), + ); + assert.strictEqual( + resolveShortcutCommand(input, DEFAULT_RESOLVED_KEYBINDINGS, { + platform: "MacIntel", + context: { isDesktop: true, modelPickerOpen: true }, + }), + "modelPicker.jump.3", + ); + assert.strictEqual( + resolveShortcutCommand(input, DEFAULT_RESOLVED_KEYBINDINGS, { + platform: "MacIntel", + context: { isDesktop: true, modelPickerOpen: false }, + }), + "thread.jump.3", + ); + }); }); describe("chat/editor shortcuts", () => { @@ -1106,7 +1167,7 @@ describe("composer and pull request shortcuts", () => { } }); - it.each(["terminalOpen", "previewFocus", "previewOpen", "modelPickerOpen"])( + it.each(["terminalOpen", "previewFocus", "previewOpen", "modelPickerOpen", "isWeb", "isDesktop"])( "honors custom PR shortcut conditions for %s", (condition) => { const bindings = compileResolvedKeybindingsConfig([ diff --git a/apps/web/src/keybindings.ts b/apps/web/src/keybindings.ts index 6b5556a1669d14929bd5b99120a5b52bd9d63f28..84d325c38d97aae7e0f9c854e47368c0cea79c58 100644 --- a/apps/web/src/keybindings.ts +++ b/apps/web/src/keybindings.ts @@ -8,6 +8,7 @@ import { type ModelPickerJumpKeybindingCommand, type ThreadJumpKeybindingCommand, } from "@t3tools/contracts"; +import { isElectron } from "./env"; import { isMacPlatform } from "./lib/utils"; export interface ShortcutEventLike { @@ -33,6 +34,8 @@ export interface ShortcutMatchContext { terminalOpen: boolean; previewFocus: boolean; previewOpen: boolean; + isWeb: boolean; + isDesktop: boolean; [key: string]: boolean; } @@ -144,6 +147,8 @@ function resolveContext(options: ShortcutMatchOptions | undefined): ShortcutMatc terminalOpen: false, previewFocus: false, previewOpen: false, + isWeb: !isElectron, + isDesktop: isElectron, ...options?.context, }; } diff --git a/apps/web/src/routes/_chat.pull-requests.tsx b/apps/web/src/routes/_chat.pull-requests.tsx index 71829cacad25d4e4145a746001aaed3eebe37acd..bed2b65da5e4603ca286dab1c3799a1b5311bc97 100644 --- a/apps/web/src/routes/_chat.pull-requests.tsx +++ b/apps/web/src/routes/_chat.pull-requests.tsx @@ -156,6 +156,8 @@ function getShortcutContext() { previewFocus: false, previewOpen: false, modelPickerOpen: false, + isWeb: !isElectron, + isDesktop: isElectron, }; } diff --git a/docs/user/keybindings.md b/docs/user/keybindings.md index f9284811de3a3f62f6305c78810dc96bff3826cf..9c2f92a3e33644f777c76774672a00d586b6921c 100644 --- a/docs/user/keybindings.md +++ b/docs/user/keybindings.md @@ -84,7 +84,14 @@ Join modifiers and a key with `+`, such as `mod+shift+d` or `ctrl+l`. ## When conditions Available context keys are `terminalFocus`, `terminalOpen`, `previewFocus`, -`previewOpen`, and `modelPickerOpen`. Unknown keys evaluate to `false`. +`previewOpen`, `modelPickerOpen`, `isWeb`, and `isDesktop`. `isWeb` is true in a +browser tab. `isDesktop` is true in the desktop app. Unknown keys evaluate to +`false`. + +`mod+1` through `mod+9` jump to the first nine threads, and to models while the +model picker is open. Those defaults use `isDesktop` so they do not steal the +browser's tab-switch shortcuts. Remove that condition in Settings if you want +the same jumps in a browser. Combine keys with `!` for not, `&&` for and, `||` for or, and parentheses: diff --git a/packages/shared/src/keybindings.ts b/packages/shared/src/keybindings.ts index 948212a9aac6539409f5d7d92460e0a15a5faf6a..d76d4c248a9783de819df5cb78d0cf6192f040aa 100644 --- a/packages/shared/src/keybindings.ts +++ b/packages/shared/src/keybindings.ts @@ -65,11 +65,12 @@ export const DEFAULT_KEYBINDINGS: ReadonlyArray = [ ...THREAD_JUMP_KEYBINDING_COMMANDS.map((command, index) => ({ key: `mod+${index + 1}`, command, + when: "isDesktop", })), ...MODEL_PICKER_JUMP_KEYBINDING_COMMANDS.map((command, index) => ({ key: `mod+${index + 1}`, command, - when: "modelPickerOpen", + when: "modelPickerOpen && isDesktop", })), ]; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6feae2edd57ccee8bef1850ac8b925581ca7295f..b9cbc23e976e64ffabcd8adb94df74f84e0ea78b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -795,7 +795,7 @@ importers: version: link:../../packages/shared alchemy: specifier: 2.0.0-beta.76 - version: 2.0.0-beta.76(b3825b36417e56a486ccb5f22a7f3d7e) + version: 2.0.0-beta.76(197b68e0d20fdf20cf352009eca17c9f) drizzle-orm: specifier: 1.0.0-rc.5-ab785fc version: 1.0.0-rc.5-ab785fc(8ab70e2706da13c78d64d8a92fef1884) @@ -16637,7 +16637,7 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 - alchemy@2.0.0-beta.76(b3825b36417e56a486ccb5f22a7f3d7e): + alchemy@2.0.0-beta.76(197b68e0d20fdf20cf352009eca17c9f): dependencies: '@alchemy.run/cloudflare-runtime': 2.0.0-beta.76(@distilled.cloud/cloudflare@1.0.0-rc.8(effect@4.0.0-rc.112(patch_hash=8bef799f35729cf3465eb428196f617b434a7c9f658d56acb2874d74b21b98b2)))(@effect/platform-bun@4.0.0-rc.112(bufferutil@4.1.0)(effect@4.0.0-rc.112(patch_hash=8bef799f35729cf3465eb428196f617b434a7c9f658d56acb2874d74b21b98b2))(utf-8-validate@6.0.6))(@effect/platform-node@4.0.0-rc.112(bufferutil@4.1.0)(effect@4.0.0-rc.112(patch_hash=8bef799f35729cf3465eb428196f617b434a7c9f658d56acb2874d74b21b98b2))(redis@6.2.1)(utf-8-validate@6.0.6))(@types/node@24.12.4)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(effect@4.0.0-rc.112(patch_hash=8bef799f35729cf3465eb428196f617b434a7c9f658d56acb2874d74b21b98b2))(rolldown@1.2.5)(typescript@7.0.2) '@alchemy.run/floci': 2.0.0-beta.76(effect@4.0.0-rc.112(patch_hash=8bef799f35729cf3465eb428196f617b434a7c9f658d56acb2874d74b21b98b2)) -- 2.54.0