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) {
66526652 previewFocus: isPreviewFocused(),
66536653 previewOpen: previewPanelOpen,
66546654 modelPickerOpen: composerRef.current?.isModelPickerOpen() ?? false,
6655 isWeb: !isElectron,
6656 isDesktop: isElectron,
66556657 }),
66566658 [composerRef, previewPanelOpen, terminalUiState.terminalOpen],
66576659 );
apps/web/src/components/pullRequest/PullRequestDetailPanel.test.tsx+2
......@@ -294,6 +294,8 @@ describe.each([
294294 terminalOpen: false,
295295 previewFocus: false,
296296 previewOpen: false,
297 isWeb: true,
298 isDesktop: false,
297299 })}
298300 />,
299301 );
apps/web/src/components/settings/KeybindingsSettings.logic.test.ts+9-1
......@@ -210,7 +210,15 @@ describe("KeybindingsSettings.logic", () => {
210210 const options = buildWhenVariableOptions();
211211
212212 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 ]),
214222 );
215223 expect(options).not.toContain("customModeActive");
216224 });
apps/web/src/components/settings/KeybindingsSettings.logic.ts+8-1
......@@ -31,7 +31,14 @@ export interface KeybindingRow {
3131export type WhenVariableOption = string;
3232export 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
3643const DEFAULT_WHEN_VARIABLES = new Set<string>(CORE_WHEN_VARIABLES);
3744for (const binding of DEFAULT_RESOLVED_KEYBINDINGS) {
apps/web/src/keybindings.test.ts+62-1
......@@ -535,6 +535,43 @@ describe("thread navigation helpers", () => {
535535 }),
536536 );
537537 });
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 });
538575});
539576
540577describe("model picker navigation helpers", () => {
......@@ -546,6 +583,30 @@ describe("model picker navigation helpers", () => {
546583 assert.strictEqual(modelPickerJumpIndexFromCommand("modelPicker.jump.3"), 2);
547584 assert.isNull(modelPickerJumpIndexFromCommand("thread.jump.1"));
548585 });
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 });
549610});
550611
551612describe("chat/editor shortcuts", () => {
......@@ -1106,7 +1167,7 @@ describe("composer and pull request shortcuts", () => {
11061167 }
11071168 });
11081169
1109 it.each(["terminalOpen", "previewFocus", "previewOpen", "modelPickerOpen"])(
1170 it.each(["terminalOpen", "previewFocus", "previewOpen", "modelPickerOpen", "isWeb", "isDesktop"])(
11101171 "honors custom PR shortcut conditions for %s",
11111172 (condition) => {
11121173 const bindings = compileResolvedKeybindingsConfig([
apps/web/src/keybindings.ts+5
......@@ -8,6 +8,7 @@ import {
88 type ModelPickerJumpKeybindingCommand,
99 type ThreadJumpKeybindingCommand,
1010} from "@t3tools/contracts";
11import { isElectron } from "./env";
1112import { isMacPlatform } from "./lib/utils";
1213
1314export interface ShortcutEventLike {
......@@ -33,6 +34,8 @@ export interface ShortcutMatchContext {
3334 terminalOpen: boolean;
3435 previewFocus: boolean;
3536 previewOpen: boolean;
37 isWeb: boolean;
38 isDesktop: boolean;
3639 [key: string]: boolean;
3740}
3841
......@@ -144,6 +147,8 @@ function resolveContext(options: ShortcutMatchOptions | undefined): ShortcutMatc
144147 terminalOpen: false,
145148 previewFocus: false,
146149 previewOpen: false,
150 isWeb: !isElectron,
151 isDesktop: isElectron,
147152 ...options?.context,
148153 };
149154}
apps/web/src/routes/_chat.pull-requests.tsx+2
......@@ -156,6 +156,8 @@ function getShortcutContext() {
156156 previewFocus: false,
157157 previewOpen: false,
158158 modelPickerOpen: false,
159 isWeb: !isElectron,
160 isDesktop: isElectron,
159161 };
160162}
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`.
8484## When conditions
8585
8686Available 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
8996Combine 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> = [
6565 ...THREAD_JUMP_KEYBINDING_COMMANDS.map((command, index) => ({
6666 key: `mod+${index + 1}`,
6767 command,
68 when: "isDesktop",
6869 })),
6970 ...MODEL_PICKER_JUMP_KEYBINDING_COMMANDS.map((command, index) => ({
7071 key: `mod+${index + 1}`,
7172 command,
72 when: "modelPickerOpen",
73 when: "modelPickerOpen && isDesktop",
7374 })),
7475];
7576
pnpm-lock.yaml+2-2
......@@ -795,7 +795,7 @@ importers:
795795 version: link:../../packages/shared
796796 alchemy:
797797 specifier: 2.0.0-beta.76
798 version: 2.0.0-beta.76(b3825b36417e56a486ccb5f22a7f3d7e)
798 version: 2.0.0-beta.76(197b68e0d20fdf20cf352009eca17c9f)
799799 drizzle-orm:
800800 specifier: 1.0.0-rc.5-ab785fc
801801 version: 1.0.0-rc.5-ab785fc(8ab70e2706da13c78d64d8a92fef1884)
......@@ -16637,7 +16637,7 @@ snapshots:
1663716637 json-schema-traverse: 1.0.0
1663816638 require-from-string: 2.0.2
1663916639
16640 alchemy@2.0.0-beta.76(b3825b36417e56a486ccb5f22a7f3d7e):
16640 alchemy@2.0.0-beta.76(197b68e0d20fdf20cf352009eca17c9f):
1664116641 dependencies:
1664216642 '@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)
1664316643 '@alchemy.run/floci': 2.0.0-beta.76(effect@4.0.0-rc.112(patch_hash=8bef799f35729cf3465eb428196f617b434a7c9f658d56acb2874d74b21b98b2))