authorgravatar for saiansh2525@gmail.comYash Singh <saiansh2525@gmail.com> 2026-09-17 19:43:03-05:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-17 19:43:03-05:00
log3fd5d6439d8fd49d173503ecda96500463a39bd2
tree6f179a784fbd68e60458ceb82cc8ecb0c387a6b6
parent9051ed9c3923b7b5fb88d99fe9267380d69baa51
signature Signed by PGP key B5690EEEBB952194

fix(web): keep numbered jumps from stealing browser tabs (#12315)

Co-authored-by: Cursor <cursoragent@cursor.com>

10 files changed, 102 insertions(+), 7 deletions(-)

apps/web/src/components/ChatView.tsx+2
...@@ -6652,6 +6652,8 @@ export default function ChatView(props: ChatViewProps) {...@@ -6652,6 +6652,8 @@ export default function ChatView(props: ChatViewProps) {
6652 previewFocus: isPreviewFocused(),6652 previewFocus: isPreviewFocused(),
6653 previewOpen: previewPanelOpen,6653 previewOpen: previewPanelOpen,
6654 modelPickerOpen: composerRef.current?.isModelPickerOpen() ?? false,6654 modelPickerOpen: composerRef.current?.isModelPickerOpen() ?? false,
6655 isWeb: !isElectron,
6656 isDesktop: isElectron,
6655 }),6657 }),
6656 [composerRef, previewPanelOpen, terminalUiState.terminalOpen],6658 [composerRef, previewPanelOpen, terminalUiState.terminalOpen],
6657 );6659 );
apps/web/src/components/pullRequest/PullRequestDetailPanel.test.tsx+2
...@@ -294,6 +294,8 @@ describe.each([...@@ -294,6 +294,8 @@ describe.each([
294 terminalOpen: false,294 terminalOpen: false,
295 previewFocus: false,295 previewFocus: false,
296 previewOpen: false,296 previewOpen: false,
297 isWeb: true,
298 isDesktop: false,
297 })}299 })}
298 />,300 />,
299 );301 );
apps/web/src/components/settings/KeybindingsSettings.logic.test.ts+9-1
...@@ -210,7 +210,15 @@ describe("KeybindingsSettings.logic", () => {...@@ -210,7 +210,15 @@ describe("KeybindingsSettings.logic", () => {
210 const options = buildWhenVariableOptions();210 const options = buildWhenVariableOptions();
211211
212 expect(options).toEqual(212 expect(options).toEqual(
213 expect.arrayContaining(["terminalFocus", "terminalOpen", "modelPickerOpen", "true", "false"]),213 expect.arrayContaining([
214 "terminalFocus",
215 "terminalOpen",
216 "isWeb",
217 "isDesktop",
218 "modelPickerOpen",
219 "true",
220 "false",
221 ]),
214 );222 );
215 expect(options).not.toContain("customModeActive");223 expect(options).not.toContain("customModeActive");
216 });224 });
apps/web/src/components/settings/KeybindingsSettings.logic.ts+8-1
...@@ -31,7 +31,14 @@ export interface KeybindingRow {...@@ -31,7 +31,14 @@ export interface KeybindingRow {
31export type WhenVariableOption = string;31export type WhenVariableOption = string;
32export type KeybindingCommandOption = KeybindingCommand;32export type KeybindingCommandOption = KeybindingCommand;
3333
34const CORE_WHEN_VARIABLES = ["terminalFocus", "terminalOpen", "true", "false"] as const;34const CORE_WHEN_VARIABLES = [
35 "terminalFocus",
36 "terminalOpen",
37 "isWeb",
38 "isDesktop",
39 "true",
40 "false",
41] as const;
3542
36const DEFAULT_WHEN_VARIABLES = new Set<string>(CORE_WHEN_VARIABLES);43const DEFAULT_WHEN_VARIABLES = new Set<string>(CORE_WHEN_VARIABLES);
37for (const binding of DEFAULT_RESOLVED_KEYBINDINGS) {44for (const binding of DEFAULT_RESOLVED_KEYBINDINGS) {
apps/web/src/keybindings.test.ts+62-1
...@@ -535,6 +535,43 @@ describe("thread navigation helpers", () => {...@@ -535,6 +535,43 @@ describe("thread navigation helpers", () => {
535 }),535 }),
536 );536 );
537 });537 });
538
539 it("keeps default thread jumps off the web so the browser can switch tabs", () => {
540 const input = event({ key: "1", metaKey: true });
541 assert.isNull(
542 resolveShortcutCommand(input, DEFAULT_RESOLVED_KEYBINDINGS, {
543 platform: "MacIntel",
544 context: { isDesktop: false },
545 }),
546 );
547 assert.strictEqual(
548 resolveShortcutCommand(input, DEFAULT_RESOLVED_KEYBINDINGS, {
549 platform: "MacIntel",
550 context: { isDesktop: true },
551 }),
552 "thread.jump.1",
553 );
554 assert.isFalse(
555 shouldShowThreadJumpHintsForModifiers(
556 event({ metaKey: true }),
557 DEFAULT_RESOLVED_KEYBINDINGS,
558 {
559 platform: "MacIntel",
560 context: { isDesktop: false },
561 },
562 ),
563 );
564 assert.isTrue(
565 shouldShowThreadJumpHintsForModifiers(
566 event({ metaKey: true }),
567 DEFAULT_RESOLVED_KEYBINDINGS,
568 {
569 platform: "MacIntel",
570 context: { isDesktop: true },
571 },
572 ),
573 );
574 });
538});575});
539576
540describe("model picker navigation helpers", () => {577describe("model picker navigation helpers", () => {
...@@ -546,6 +583,30 @@ describe("model picker navigation helpers", () => {...@@ -546,6 +583,30 @@ describe("model picker navigation helpers", () => {
546 assert.strictEqual(modelPickerJumpIndexFromCommand("modelPicker.jump.3"), 2);583 assert.strictEqual(modelPickerJumpIndexFromCommand("modelPicker.jump.3"), 2);
547 assert.isNull(modelPickerJumpIndexFromCommand("thread.jump.1"));584 assert.isNull(modelPickerJumpIndexFromCommand("thread.jump.1"));
548 });585 });
586
587 it("keeps default model jumps off the web even while the picker is open", () => {
588 const input = event({ key: "3", metaKey: true });
589 assert.isNull(
590 resolveShortcutCommand(input, DEFAULT_RESOLVED_KEYBINDINGS, {
591 platform: "MacIntel",
592 context: { isDesktop: false, modelPickerOpen: true },
593 }),
594 );
595 assert.strictEqual(
596 resolveShortcutCommand(input, DEFAULT_RESOLVED_KEYBINDINGS, {
597 platform: "MacIntel",
598 context: { isDesktop: true, modelPickerOpen: true },
599 }),
600 "modelPicker.jump.3",
601 );
602 assert.strictEqual(
603 resolveShortcutCommand(input, DEFAULT_RESOLVED_KEYBINDINGS, {
604 platform: "MacIntel",
605 context: { isDesktop: true, modelPickerOpen: false },
606 }),
607 "thread.jump.3",
608 );
609 });
549});610});
550611
551describe("chat/editor shortcuts", () => {612describe("chat/editor shortcuts", () => {
...@@ -1106,7 +1167,7 @@ describe("composer and pull request shortcuts", () => {...@@ -1106,7 +1167,7 @@ describe("composer and pull request shortcuts", () => {
1106 }1167 }
1107 });1168 });
11081169
1109 it.each(["terminalOpen", "previewFocus", "previewOpen", "modelPickerOpen"])(1170 it.each(["terminalOpen", "previewFocus", "previewOpen", "modelPickerOpen", "isWeb", "isDesktop"])(
1110 "honors custom PR shortcut conditions for %s",1171 "honors custom PR shortcut conditions for %s",
1111 (condition) => {1172 (condition) => {
1112 const bindings = compileResolvedKeybindingsConfig([1173 const bindings = compileResolvedKeybindingsConfig([
apps/web/src/keybindings.ts+5
...@@ -8,6 +8,7 @@ import {...@@ -8,6 +8,7 @@ import {
8 type ModelPickerJumpKeybindingCommand,8 type ModelPickerJumpKeybindingCommand,
9 type ThreadJumpKeybindingCommand,9 type ThreadJumpKeybindingCommand,
10} from "@t3tools/contracts";10} from "@t3tools/contracts";
11import { isElectron } from "./env";
11import { isMacPlatform } from "./lib/utils";12import { isMacPlatform } from "./lib/utils";
1213
13export interface ShortcutEventLike {14export interface ShortcutEventLike {
...@@ -33,6 +34,8 @@ export interface ShortcutMatchContext {...@@ -33,6 +34,8 @@ export interface ShortcutMatchContext {
33 terminalOpen: boolean;34 terminalOpen: boolean;
34 previewFocus: boolean;35 previewFocus: boolean;
35 previewOpen: boolean;36 previewOpen: boolean;
37 isWeb: boolean;
38 isDesktop: boolean;
36 [key: string]: boolean;39 [key: string]: boolean;
37}40}
3841
...@@ -144,6 +147,8 @@ function resolveContext(options: ShortcutMatchOptions | undefined): ShortcutMatc...@@ -144,6 +147,8 @@ function resolveContext(options: ShortcutMatchOptions | undefined): ShortcutMatc
144 terminalOpen: false,147 terminalOpen: false,
145 previewFocus: false,148 previewFocus: false,
146 previewOpen: false,149 previewOpen: false,
150 isWeb: !isElectron,
151 isDesktop: isElectron,
147 ...options?.context,152 ...options?.context,
148 };153 };
149}154}
apps/web/src/routes/_chat.pull-requests.tsx+2
...@@ -156,6 +156,8 @@ function getShortcutContext() {...@@ -156,6 +156,8 @@ function getShortcutContext() {
156 previewFocus: false,156 previewFocus: false,
157 previewOpen: false,157 previewOpen: false,
158 modelPickerOpen: false,158 modelPickerOpen: false,
159 isWeb: !isElectron,
160 isDesktop: isElectron,
159 };161 };
160}162}
161163
docs/user/keybindings.md+8-1
...@@ -84,7 +84,14 @@ Join modifiers and a key with `+`, such as `mod+shift+d` or `ctrl+l`....@@ -84,7 +84,14 @@ Join modifiers and a key with `+`, such as `mod+shift+d` or `ctrl+l`.
84## When conditions84## When conditions
8585
86Available context keys are `terminalFocus`, `terminalOpen`, `previewFocus`,86Available context keys are `terminalFocus`, `terminalOpen`, `previewFocus`,
87`previewOpen`, and `modelPickerOpen`. Unknown keys evaluate to `false`.87`previewOpen`, `modelPickerOpen`, `isWeb`, and `isDesktop`. `isWeb` is true in a
88browser tab. `isDesktop` is true in the desktop app. Unknown keys evaluate to
89`false`.
90
91`mod+1` through `mod+9` jump to the first nine threads, and to models while the
92model picker is open. Those defaults use `isDesktop` so they do not steal the
93browser's tab-switch shortcuts. Remove that condition in Settings if you want
94the same jumps in a browser.
8895
89Combine keys with `!` for not, `&&` for and, `||` for or, and parentheses:96Combine keys with `!` for not, `&&` for and, `||` for or, and parentheses:
9097
packages/shared/src/keybindings.ts+2-1
...@@ -65,11 +65,12 @@ export const DEFAULT_KEYBINDINGS: ReadonlyArray<KeybindingRule> = [...@@ -65,11 +65,12 @@ export const DEFAULT_KEYBINDINGS: ReadonlyArray<KeybindingRule> = [
65 ...THREAD_JUMP_KEYBINDING_COMMANDS.map((command, index) => ({65 ...THREAD_JUMP_KEYBINDING_COMMANDS.map((command, index) => ({
66 key: `mod+${index + 1}`,66 key: `mod+${index + 1}`,
67 command,67 command,
68 when: "isDesktop",
68 })),69 })),
69 ...MODEL_PICKER_JUMP_KEYBINDING_COMMANDS.map((command, index) => ({70 ...MODEL_PICKER_JUMP_KEYBINDING_COMMANDS.map((command, index) => ({
70 key: `mod+${index + 1}`,71 key: `mod+${index + 1}`,
71 command,72 command,
72 when: "modelPickerOpen",73 when: "modelPickerOpen && isDesktop",
73 })),74 })),
74];75];
7576
pnpm-lock.yaml+2-2
...@@ -795,7 +795,7 @@ importers:...@@ -795,7 +795,7 @@ importers:
795 version: link:../../packages/shared795 version: link:../../packages/shared
796 alchemy:796 alchemy:
797 specifier: 2.0.0-beta.76797 specifier: 2.0.0-beta.76
798 version: 2.0.0-beta.76(b3825b36417e56a486ccb5f22a7f3d7e)798 version: 2.0.0-beta.76(197b68e0d20fdf20cf352009eca17c9f)
799 drizzle-orm:799 drizzle-orm:
800 specifier: 1.0.0-rc.5-ab785fc800 specifier: 1.0.0-rc.5-ab785fc
801 version: 1.0.0-rc.5-ab785fc(8ab70e2706da13c78d64d8a92fef1884)801 version: 1.0.0-rc.5-ab785fc(8ab70e2706da13c78d64d8a92fef1884)
...@@ -16637,7 +16637,7 @@ snapshots:...@@ -16637,7 +16637,7 @@ snapshots:
16637 json-schema-traverse: 1.0.016637 json-schema-traverse: 1.0.0
16638 require-from-string: 2.0.216638 require-from-string: 2.0.2
1663916639
16640 alchemy@2.0.0-beta.76(b3825b36417e56a486ccb5f22a7f3d7e):16640 alchemy@2.0.0-beta.76(197b68e0d20fdf20cf352009eca17c9f):
16641 dependencies:16641 dependencies:
16642 '@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)16642 '@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)
16643 '@alchemy.run/floci': 2.0.0-beta.76(effect@4.0.0-rc.112(patch_hash=8bef799f35729cf3465eb428196f617b434a7c9f658d56acb2874d74b21b98b2))16643 '@alchemy.run/floci': 2.0.0-beta.76(effect@4.0.0-rc.112(patch_hash=8bef799f35729cf3465eb428196f617b434a7c9f658d56acb2874d74b21b98b2))