| author | |
| committer | |
| log | 3fd5d6439d8fd49d173503ecda96500463a39bd2 |
| tree | 6f179a784fbd68e60458ceb82cc8ecb0c387a6b6 |
| parent | 9051ed9c3923b7b5fb88d99fe9267380d69baa51 |
| signature | Signed by PGP key B5690EEEBB952194 |
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 | 6652 | previewFocus: isPreviewFocused(), |
| 6653 | 6653 | previewOpen: previewPanelOpen, |
| 6654 | 6654 | modelPickerOpen: composerRef.current?.isModelPickerOpen() ?? false, |
| 6655 | isWeb: !isElectron, | |
| 6656 | isDesktop: isElectron, | |
| 6655 | 6657 | }), |
| 6656 | 6658 | [composerRef, previewPanelOpen, terminalUiState.terminalOpen], |
| 6657 | 6659 | ); |
apps/web/src/components/pullRequest/PullRequestDetailPanel.test.tsx+2| ... | ... | @@ -294,6 +294,8 @@ describe.each([ |
| 294 | 294 | terminalOpen: false, |
| 295 | 295 | previewFocus: false, |
| 296 | 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 | 210 | const options = buildWhenVariableOptions(); |
| 211 | 211 | |
| 212 | 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 | 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 | 31 | export type WhenVariableOption = string; |
| 32 | 32 | export type KeybindingCommandOption = KeybindingCommand; |
| 33 | 33 | |
| 34 | const CORE_WHEN_VARIABLES = ["terminalFocus", "terminalOpen", "true", "false"] as const; | |
| 34 | const CORE_WHEN_VARIABLES = [ | |
| 35 | "terminalFocus", | |
| 36 | "terminalOpen", | |
| 37 | "isWeb", | |
| 38 | "isDesktop", | |
| 39 | "true", | |
| 40 | "false", | |
| 41 | ] as const; | |
| 35 | 42 | |
| 36 | 43 | const DEFAULT_WHEN_VARIABLES = new Set<string>(CORE_WHEN_VARIABLES); |
| 37 | 44 | for (const binding of DEFAULT_RESOLVED_KEYBINDINGS) { |
apps/web/src/keybindings.test.ts+62-1| ... | ... | @@ -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 | }); |
| 539 | 576 | |
| 540 | 577 | describe("model picker navigation helpers", () => { |
| ... | ... | @@ -546,6 +583,30 @@ describe("model picker navigation helpers", () => { |
| 546 | 583 | assert.strictEqual(modelPickerJumpIndexFromCommand("modelPicker.jump.3"), 2); |
| 547 | 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 | }); |
| 550 | 611 | |
| 551 | 612 | describe("chat/editor shortcuts", () => { |
| ... | ... | @@ -1106,7 +1167,7 @@ describe("composer and pull request shortcuts", () => { |
| 1106 | 1167 | } |
| 1107 | 1168 | }); |
| 1108 | 1169 | |
| 1109 | it.each(["terminalOpen", "previewFocus", "previewOpen", "modelPickerOpen"])( | |
| 1170 | it.each(["terminalOpen", "previewFocus", "previewOpen", "modelPickerOpen", "isWeb", "isDesktop"])( | |
| 1110 | 1171 | "honors custom PR shortcut conditions for %s", |
| 1111 | 1172 | (condition) => { |
| 1112 | 1173 | const bindings = compileResolvedKeybindingsConfig([ |
apps/web/src/keybindings.ts+5| ... | ... | @@ -8,6 +8,7 @@ import { |
| 8 | 8 | type ModelPickerJumpKeybindingCommand, |
| 9 | 9 | type ThreadJumpKeybindingCommand, |
| 10 | 10 | } from "@t3tools/contracts"; |
| 11 | import { isElectron } from "./env"; | |
| 11 | 12 | import { isMacPlatform } from "./lib/utils"; |
| 12 | 13 | |
| 13 | 14 | export interface ShortcutEventLike { |
| ... | ... | @@ -33,6 +34,8 @@ export interface ShortcutMatchContext { |
| 33 | 34 | terminalOpen: boolean; |
| 34 | 35 | previewFocus: boolean; |
| 35 | 36 | previewOpen: boolean; |
| 37 | isWeb: boolean; | |
| 38 | isDesktop: boolean; | |
| 36 | 39 | [key: string]: boolean; |
| 37 | 40 | } |
| 38 | 41 | |
| ... | ... | @@ -144,6 +147,8 @@ function resolveContext(options: ShortcutMatchOptions | undefined): ShortcutMatc |
| 144 | 147 | terminalOpen: false, |
| 145 | 148 | previewFocus: false, |
| 146 | 149 | previewOpen: false, |
| 150 | isWeb: !isElectron, | |
| 151 | isDesktop: isElectron, | |
| 147 | 152 | ...options?.context, |
| 148 | 153 | }; |
| 149 | 154 | } |
apps/web/src/routes/_chat.pull-requests.tsx+2| ... | ... | @@ -156,6 +156,8 @@ function getShortcutContext() { |
| 156 | 156 | previewFocus: false, |
| 157 | 157 | previewOpen: false, |
| 158 | 158 | modelPickerOpen: false, |
| 159 | isWeb: !isElectron, | |
| 160 | isDesktop: isElectron, | |
| 159 | 161 | }; |
| 160 | 162 | } |
| 161 | 163 |
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 | 84 | ## When conditions |
| 85 | 85 | |
| 86 | 86 | Available 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 | |
| 88 | browser 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 | |
| 92 | model picker is open. Those defaults use `isDesktop` so they do not steal the | |
| 93 | browser's tab-switch shortcuts. Remove that condition in Settings if you want | |
| 94 | the same jumps in a browser. | |
| 88 | 95 | |
| 89 | 96 | Combine keys with `!` for not, `&&` for and, `||` for or, and parentheses: |
| 90 | 97 |
packages/shared/src/keybindings.ts+2-1| ... | ... | @@ -65,11 +65,12 @@ export const DEFAULT_KEYBINDINGS: ReadonlyArray<KeybindingRule> = [ |
| 65 | 65 | ...THREAD_JUMP_KEYBINDING_COMMANDS.map((command, index) => ({ |
| 66 | 66 | key: `mod+${index + 1}`, |
| 67 | 67 | command, |
| 68 | when: "isDesktop", | |
| 68 | 69 | })), |
| 69 | 70 | ...MODEL_PICKER_JUMP_KEYBINDING_COMMANDS.map((command, index) => ({ |
| 70 | 71 | key: `mod+${index + 1}`, |
| 71 | 72 | command, |
| 72 | when: "modelPickerOpen", | |
| 73 | when: "modelPickerOpen && isDesktop", | |
| 73 | 74 | })), |
| 74 | 75 | ]; |
| 75 | 76 |
pnpm-lock.yaml+2-2| ... | ... | @@ -795,7 +795,7 @@ importers: |
| 795 | 795 | version: link:../../packages/shared |
| 796 | 796 | alchemy: |
| 797 | 797 | specifier: 2.0.0-beta.76 |
| 798 | version: 2.0.0-beta.76(b3825b36417e56a486ccb5f22a7f3d7e) | |
| 798 | version: 2.0.0-beta.76(197b68e0d20fdf20cf352009eca17c9f) | |
| 799 | 799 | drizzle-orm: |
| 800 | 800 | specifier: 1.0.0-rc.5-ab785fc |
| 801 | 801 | version: 1.0.0-rc.5-ab785fc(8ab70e2706da13c78d64d8a92fef1884) |
| ... | ... | @@ -16637,7 +16637,7 @@ snapshots: |
| 16637 | 16637 | json-schema-traverse: 1.0.0 |
| 16638 | 16638 | require-from-string: 2.0.2 |
| 16639 | 16639 | |
| 16640 | alchemy@2.0.0-beta.76(b3825b36417e56a486ccb5f22a7f3d7e): | |
| 16640 | alchemy@2.0.0-beta.76(197b68e0d20fdf20cf352009eca17c9f): | |
| 16641 | 16641 | dependencies: |
| 16642 | 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 | 16643 | '@alchemy.run/floci': 2.0.0-beta.76(effect@4.0.0-rc.112(patch_hash=8bef799f35729cf3465eb428196f617b434a7c9f658d56acb2874d74b21b98b2)) |