authorgravatar for maria@kuuro.netmaria <maria@kuuro.net> 2026-10-09 12:54:10-03:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-09 12:54:10-03:00
logb1508672a63c4799fed6d9091001de00175d9316
treee2cafb8d5a04b9849ec16af4da90e3f3fcf907fc
parent22ccf8a3dfc9094b4ccdb689e128c67bacbbcdf5
signature Signed by PGP key B5690EEEBB952194

feat(web): assign a thread details panel shortcut (#16694)

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 ]);
262268
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 });
267271
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 );
284296
...@@ -295,8 +307,8 @@ it.layer(NodeServices.layer)("keybindings", (it) => {...@@ -295,8 +307,8 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
295307
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 ]);
301313
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 }
594594
595 // Startup backfill must never evict persisted user rules: append only595 // 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 });
750750
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 );
755784
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 });
763799
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" },