| 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,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(); |
| 211 | 211 | ||
| 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 { |
| 31 | export type WhenVariableOption = string; | 31 | export type WhenVariableOption = string; |
| 32 | export type KeybindingCommandOption = KeybindingCommand; | 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 | const DEFAULT_WHEN_VARIABLES = new Set<string>(CORE_WHEN_VARIABLES); | 43 | const DEFAULT_WHEN_VARIABLES = new Set<string>(CORE_WHEN_VARIABLES); |
| 37 | for (const binding of DEFAULT_RESOLVED_KEYBINDINGS) { | 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,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 | describe("model picker navigation helpers", () => { | 577 | describe("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 | }); |
| 550 | 611 | ||
| 551 | describe("chat/editor shortcuts", () => { | 612 | describe("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 | }); |
| 1108 | 1169 | ||
| 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"; |
| 11 | import { isElectron } from "./env"; | ||
| 11 | import { isMacPlatform } from "./lib/utils"; | 12 | import { isMacPlatform } from "./lib/utils"; |
| 12 | 13 | ||
| 13 | export interface ShortcutEventLike { | 14 | export 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 | } |
| 38 | 41 | ||
| ... | @@ -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 | } |
| 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,7 +84,14 @@ Join modifiers and a key with `+`, such as `mod+shift+d` or `ctrl+l`. |
| 84 | ## When conditions | 84 | ## When conditions |
| 85 | 85 | ||
| 86 | Available context keys are `terminalFocus`, `terminalOpen`, `previewFocus`, | 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 | Combine keys with `!` for not, `&&` for and, `||` for or, and parentheses: | 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,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 | ]; |
| 75 | 76 |
pnpm-lock.yaml+2-2| ... | @@ -795,7 +795,7 @@ importers: | ... | @@ -795,7 +795,7 @@ importers: |
| 795 | version: link:../../packages/shared | 795 | version: link:../../packages/shared |
| 796 | alchemy: | 796 | alchemy: |
| 797 | specifier: 2.0.0-beta.76 | 797 | 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-ab785fc | 800 | 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.0 | 16637 | json-schema-traverse: 1.0.0 |
| 16638 | require-from-string: 2.0.2 | 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 | 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)) |