| author | |
| committer | |
| log | b1508672a63c4799fed6d9091001de00175d9316 |
| tree | e2cafb8d5a04b9849ec16af4da90e3f3fcf907fc |
| parent | 22ccf8a3dfc9094b4ccdb689e128c67bacbbcdf5 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: maria-rcks <254055478+maria-rcks@users.noreply.github.com>4 files changed, 62 insertions(+), 13 deletions(-)
apps/server/src/keybindings.test.ts+17-5| ... | @@ -258,12 +258,16 @@ it.layer(NodeServices.layer)("keybindings", (it) => { | ... | @@ -258,12 +258,16 @@ it.layer(NodeServices.layer)("keybindings", (it) => { |
| 258 | yield* writeKeybindingsConfig(keybindingsConfigPath, [ | 258 | yield* writeKeybindingsConfig(keybindingsConfigPath, [ |
| 259 | { key: "mod+shift+y", command: "terminal.toggle" }, | 259 | { key: "mod+shift+y", command: "terminal.toggle" }, |
| 260 | { key: "mod+shift+r", command: "script.run-tests.run" }, | 260 | { key: "mod+shift+r", command: "script.run-tests.run" }, |
| 261 | { key: "mod+shift+b", command: "script.custom-panel.run" }, | ||
| 262 | { | ||
| 263 | key: "mod+shift+b", | ||
| 264 | command: "script.custom-composer.run", | ||
| 265 | when: "composerFocus", | ||
| 266 | }, | ||
| 261 | ]); | 267 | ]); |
| 262 | 268 | ||
| 263 | yield* Effect.gen(function* () { | 269 | const keybindings = yield* Keybindings.Keybindings; |
| 264 | const keybindings = yield* Keybindings.Keybindings; | 270 | yield* keybindings.syncDefaultKeybindingsOnStartup; |
| 265 | yield* keybindings.syncDefaultKeybindingsOnStartup; | ||
| 266 | }); | ||
| 267 | 271 | ||
| 268 | const persisted = yield* readKeybindingsConfig(keybindingsConfigPath); | 272 | const persisted = yield* readKeybindingsConfig(keybindingsConfigPath); |
| 269 | const byCommand = new Map(persisted.map((entry) => [entry.command, entry])); | 273 | const byCommand = new Map(persisted.map((entry) => [entry.command, entry])); |
| ... | @@ -279,6 +283,14 @@ it.layer(NodeServices.layer)("keybindings", (it) => { | ... | @@ -279,6 +283,14 @@ it.layer(NodeServices.layer)("keybindings", (it) => { |
| 279 | assert.isTrue(byCommand.has(defaultRule.command), `expected ${defaultRule.command}`); | 283 | assert.isTrue(byCommand.has(defaultRule.command), `expected ${defaultRule.command}`); |
| 280 | } | 284 | } |
| 281 | assert.isTrue(byCommand.has("script.run-tests.run")); | 285 | assert.isTrue(byCommand.has("script.run-tests.run")); |
| 286 | |||
| 287 | const configState = yield* keybindings.loadConfigState; | ||
| 288 | assert.deepEqual( | ||
| 289 | configState.keybindings | ||
| 290 | .filter((entry) => entry.shortcut.key === "b" && entry.shortcut.shiftKey) | ||
| 291 | .map((entry) => entry.command), | ||
| 292 | ["threadPanel.toggle", "script.custom-panel.run", "script.custom-composer.run"], | ||
| 293 | ); | ||
| 282 | }).pipe(Effect.provide(layerKeybindings())), | 294 | }).pipe(Effect.provide(layerKeybindings())), |
| 283 | ); | 295 | ); |
| 284 | 296 | ||
| ... | @@ -295,8 +307,8 @@ it.layer(NodeServices.layer)("keybindings", (it) => { | ... | @@ -295,8 +307,8 @@ it.layer(NodeServices.layer)("keybindings", (it) => { |
| 295 | 307 | ||
| 296 | yield* keybindings.syncDefaultKeybindingsOnStartup; | 308 | yield* keybindings.syncDefaultKeybindingsOnStartup; |
| 297 | assert.deepStrictEqual(yield* backgroundRules, [ | 309 | assert.deepStrictEqual(yield* backgroundRules, [ |
| 298 | existing, | ||
| 299 | { key: "mod+enter", command: "composer.sendBackground", when }, | 310 | { key: "mod+enter", command: "composer.sendBackground", when }, |
| 311 | existing, | ||
| 300 | ]); | 312 | ]); |
| 301 | 313 | ||
| 302 | // Removing the added rule later must survive the next startup. | 314 | // Removing the added rule later must survive the next startup. |
apps/server/src/keybindings.ts+3-3| ... | @@ -592,8 +592,8 @@ const make = Effect.gen(function* () { | ... | @@ -592,8 +592,8 @@ const make = Effect.gen(function* () { |
| 592 | } | 592 | } |
| 593 | } | 593 | } |
| 594 | 594 | ||
| 595 | // Startup backfill must never evict persisted user rules: append only | 595 | // Keep only defaults that fit, before persisted rules so existing |
| 596 | // the defaults that fit and skip the rest. | 596 | // custom shortcuts retain priority when their conditions overlap. |
| 597 | const availableSlots = Math.max(0, MAX_KEYBINDINGS_COUNT - customConfig.length); | 597 | const availableSlots = Math.max(0, MAX_KEYBINDINGS_COUNT - customConfig.length); |
| 598 | const defaultsToAppend = missingDefaults.slice(0, availableSlots); | 598 | const defaultsToAppend = missingDefaults.slice(0, availableSlots); |
| 599 | const skippedDefaults = missingDefaults.slice(availableSlots); | 599 | const skippedDefaults = missingDefaults.slice(availableSlots); |
| ... | @@ -605,7 +605,7 @@ const make = Effect.gen(function* () { | ... | @@ -605,7 +605,7 @@ const make = Effect.gen(function* () { |
| 605 | }); | 605 | }); |
| 606 | } | 606 | } |
| 607 | if (defaultsToAppend.length > 0) { | 607 | if (defaultsToAppend.length > 0) { |
| 608 | yield* writeConfigAtomically([...customConfig, ...defaultsToAppend]); | 608 | yield* writeConfigAtomically([...defaultsToAppend, ...customConfig]); |
| 609 | } | 609 | } |
| 610 | // A late default skipped at max entries stays pending for a later start. | 610 | // A late default skipped at max entries stays pending for a later start. |
| 611 | const settledLateDefaults = pendingLateDefaults.filter( | 611 | const settledLateDefaults = pendingLateDefaults.filter( |
apps/web/src/keybindings.test.ts+41-5| ... | @@ -748,16 +748,52 @@ describe("chat/editor shortcuts", () => { | ... | @@ -748,16 +748,52 @@ describe("chat/editor shortcuts", () => { |
| 748 | ); | 748 | ); |
| 749 | }); | 749 | }); |
| 750 | 750 | ||
| 751 | it("resolves a user-configured thread panel shortcut without assigning a default", () => { | 751 | it("toggles the thread panel by default without taking terminal keystrokes", () => { |
| 752 | const bindings = compile([ | 752 | for (const platform of ["MacIntel", "Win32", "Linux"]) { |
| 753 | { shortcut: modShortcut("b", { shiftKey: true }), command: "threadPanel.toggle" }, | 753 | const input = event({ |
| 754 | ]); | 754 | key: "b", |
| 755 | metaKey: platform === "MacIntel", | ||
| 756 | ctrlKey: platform !== "MacIntel", | ||
| 757 | shiftKey: true, | ||
| 758 | }); | ||
| 759 | assert.strictEqual( | ||
| 760 | resolveShortcutCommand(input, DEFAULT_RESOLVED_KEYBINDINGS, { | ||
| 761 | platform, | ||
| 762 | context: { terminalFocus: false, editableFocus: true }, | ||
| 763 | }), | ||
| 764 | "threadPanel.toggle", | ||
| 765 | ); | ||
| 766 | assert.isNull( | ||
| 767 | resolveShortcutCommand(input, DEFAULT_RESOLVED_KEYBINDINGS, { | ||
| 768 | platform, | ||
| 769 | context: { terminalFocus: true }, | ||
| 770 | }), | ||
| 771 | ); | ||
| 772 | } | ||
| 773 | }); | ||
| 774 | |||
| 775 | it("preserves a user-configured thread panel shortcut instead of its default", () => { | ||
| 776 | const bindings = mergeWithDefaultKeybindings( | ||
| 777 | compile([ | ||
| 778 | { | ||
| 779 | shortcut: modShortcut("b", { altKey: true, shiftKey: true }), | ||
| 780 | command: "threadPanel.toggle", | ||
| 781 | }, | ||
| 782 | ]), | ||
| 783 | ); | ||
| 755 | 784 | ||
| 756 | assert.strictEqual( | 785 | assert.strictEqual( |
| 786 | resolveShortcutCommand( | ||
| 787 | event({ key: "b", metaKey: true, altKey: true, shiftKey: true }), | ||
| 788 | bindings, | ||
| 789 | { platform: "MacIntel" }, | ||
| 790 | ), | ||
| 791 | "threadPanel.toggle", | ||
| 792 | ); | ||
| 793 | assert.isNull( | ||
| 757 | resolveShortcutCommand(event({ key: "b", metaKey: true, shiftKey: true }), bindings, { | 794 | resolveShortcutCommand(event({ key: "b", metaKey: true, shiftKey: true }), bindings, { |
| 758 | platform: "MacIntel", | 795 | platform: "MacIntel", |
| 759 | }), | 796 | }), |
| 760 | "threadPanel.toggle", | ||
| 761 | ); | 797 | ); |
| 762 | }); | 798 | }); |
| 763 | 799 |
packages/shared/src/keybindings.ts+1| ... | @@ -126,6 +126,7 @@ export const DEFAULT_KEYBINDINGS: ReadonlyArray<KeybindingRule> = [ | ... | @@ -126,6 +126,7 @@ export const DEFAULT_KEYBINDINGS: ReadonlyArray<KeybindingRule> = [ |
| 126 | { key: "mod+]", command: "navigation.forward", when: "!terminalFocus" }, | 126 | { key: "mod+]", command: "navigation.forward", when: "!terminalFocus" }, |
| 127 | { key: "mod+j", command: "terminal.toggle" }, | 127 | { key: "mod+j", command: "terminal.toggle" }, |
| 128 | { key: "mod+alt+b", command: "rightPanel.toggle" }, | 128 | { key: "mod+alt+b", command: "rightPanel.toggle" }, |
| 129 | { key: "mod+shift+b", command: "threadPanel.toggle", when: "!terminalFocus" }, | ||
| 129 | { key: "mod+t", command: "rightPanel.new", when: "rightPanelOpen && !terminalFocus" }, | 130 | { key: "mod+t", command: "rightPanel.new", when: "rightPanelOpen && !terminalFocus" }, |
| 130 | { key: "mod+d", command: "terminal.split", when: "terminalFocus" }, | 131 | { key: "mod+d", command: "terminal.split", when: "terminalFocus" }, |
| 131 | { key: "mod+shift+d", command: "terminal.splitVertical", when: "terminalFocus" }, | 132 | { key: "mod+shift+d", command: "terminal.splitVertical", when: "terminalFocus" }, |