From 3189bba3623bc2bd35e08e8eec56f04f8cf77286 Mon Sep 17 00:00:00 2001 From: Cheng Peng Date: Thu, 5 Nov 2026 23:52:50 -0800 Subject: [PATCH 1/2] fix(channels): leave Option+Up/Down to text fields outside the composer Previous and next conversation (Option+Up/Down, with Shift for unread only) ran from any text field, so Option+Up/Down in the Canvas editor or a channel details field switched channels instead of moving the caret. Stepping now also checks inFieldOutsideComposer(), the check Escape's mark-read already uses, so it still runs from the message composer and terminals still keep the key. Co-authored-by: Claude Opus 5.5 Signed-off-by: Cheng Peng Co-authored-by: Cursor --- docs/channels.md | 3 ++- src/bundled/channels/index.test.tsx | 14 ++++++++++++++ src/bundled/channels/index.tsx | 11 ++++++++--- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/docs/channels.md b/docs/channels.md index ad30fe0ebc..c8a4d68d07 100644 --- a/docs/channels.md +++ b/docs/channels.md @@ -1709,7 +1709,8 @@ Escape and Option+Arrow for the shell. conversation is not among the rows, next starts from the top and previous from the bottom. A conversation listed in two sections is stepped over once. The shortcuts are unavailable, leaving the key alone, while the sidebar renders no rows (for - example behind another page's own sidebar). Stepping activates the row's own + example behind another page's own sidebar) or focus is in a text field outside the + composer, which keeps Option+Arrow for editing. Stepping activates the row's own button, so it takes the exact path of a click: the saved selection, navigation intent, session staleness guard and Solo's immediate activation. The list scrolls just far enough to show the row, and nothing above it moves. diff --git a/src/bundled/channels/index.test.tsx b/src/bundled/channels/index.test.tsx index 051314835a..469afde679 100644 --- a/src/bundled/channels/index.test.tsx +++ b/src/bundled/channels/index.test.tsx @@ -72,6 +72,20 @@ it("steps only while the sidebar renders rows, and not from a terminal", () => { expect(next.when()).toBe(false); }); +it("steps from the message composer but leaves Option+Arrow to other text fields", () => { + const next = registered().get("next-conversation"); + sidebar("a", "*b", "c"); + document.body.insertAdjacentHTML( + "beforeend", + ` +
`, + ); + document.querySelector("textarea")?.focus(); + expect(next.when()).toBe(false); + document.querySelector('[role="textbox"]')?.focus(); + expect(next.when()).toBe(true); +}); + it("registers a plain Escape that waits for an open conversation to mark read", () => { const shortcut = registered().get("mark-read"); expect(shortcut).toMatchObject({ diff --git a/src/bundled/channels/index.tsx b/src/bundled/channels/index.tsx index ae54b251ed..449d20246e 100644 --- a/src/bundled/channels/index.tsx +++ b/src/bundled/channels/index.tsx @@ -12,6 +12,7 @@ import { isApplePlatform } from "../../features/shortcuts/format"; import type { ArchiveShortcut, ReadShortcut } from "./ChannelsPage"; import { conversationRows, + inFieldOutsideComposer, inTerminal, stepConversation, } from "./conversation-rows"; @@ -72,8 +73,9 @@ export const apply: PluginModule["apply"] = (ctx) => { when: () => !!archive.current?.available(), run: () => archive.current?.run(), }); - // Slack's sidebar stepping. Terminals keep Option+Arrow, and stepping is - // unavailable (the key is left alone) while the sidebar renders no rows. + // Slack's sidebar stepping. Terminals and text fields outside the composer + // keep Option+Arrow, and stepping is unavailable (the key is left alone) + // while the sidebar renders no rows. const stepping = [ ["previous-conversation", "Previous conversation", "ArrowUp", false, 30], ["next-conversation", "Next conversation", "ArrowDown", false, 31], @@ -99,7 +101,10 @@ export const apply: PluginModule["apply"] = (ctx) => { binding: { key, alt: true, shift: unreadOnly }, order, allowInEditable: true, - when: () => !inTerminal() && conversationRows().length > 0, + when: () => + !inTerminal() && + !inFieldOutsideComposer() && + conversationRows().length > 0, run: () => stepConversation(key === "ArrowUp" ? "previous" : "next", unreadOnly), }); From 2e4535ecb457789618df3e9cce902b4cf7847730 Mon Sep 17 00:00:00 2001 From: Cheng Peng Date: Fri, 6 Nov 2026 00:41:30 -0800 Subject: [PATCH 2/2] test(channels): name the stepping field test after what it checks The test checks that stepping stays available in the message composer and is unavailable in other text fields. It never presses a key, and on macOS the composer handles plain Option+Up/Down itself, so the old name ("steps from the message composer") promised more than the test checks. Co-authored-by: Claude Opus 5.5 Signed-off-by: Cheng Peng Co-authored-by: Cursor --- src/bundled/channels/index.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bundled/channels/index.test.tsx b/src/bundled/channels/index.test.tsx index 469afde679..2bd29be279 100644 --- a/src/bundled/channels/index.test.tsx +++ b/src/bundled/channels/index.test.tsx @@ -72,7 +72,7 @@ it("steps only while the sidebar renders rows, and not from a terminal", () => { expect(next.when()).toBe(false); }); -it("steps from the message composer but leaves Option+Arrow to other text fields", () => { +it("stays available in the message composer but not in other text fields", () => { const next = registered().get("next-conversation"); sidebar("a", "*b", "c"); document.body.insertAdjacentHTML(