authorgravatar for maria@kuuro.netmaria <maria@kuuro.net> 2026-10-03 03:07:55-03:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-03 03:07:55-03:00
log5c0429b1578cff3307c35c51a305e2749a6dcb6e
tree349553ce589d70d4ceadf843bf4142914404dafb
parent5bf19d12b0fd54fc9999579869bcf264bc555874
signature Signed by PGP key B5690EEEBB952194

feat(web): mod+alt+enter on an existing thread sends and opens a new thread (#15050)

Co-authored-by: maria-rcks <254055478+maria-rcks@users.noreply.github.com>

7 files changed, 36 insertions(+), 6 deletions(-)

apps/web/src/components/ChatView.tsx+14-1
......@@ -8511,6 +8511,11 @@ export default function ChatView(props: ChatViewProps) {
85118511 setComposerDraftThreadContexts(composerDraftTarget, [...records]),
85128512 resetCursor: (options) => composerRef.current?.resetCursorState(options),
85138513 });
8514 } else if (
8515 submissionIntent === "background" &&
8516 currentRouteThreadKeyRef.current === routeThreadKey
8517 ) {
8518 handleNewThreadInActiveProject();
85148519 }
85158520 return;
85168521 }
......@@ -8743,7 +8748,9 @@ export default function ChatView(props: ChatViewProps) {
87438748 }
87448749 beginLocalDispatch({
87458750 preparingWorktree: multipleModelSelections !== null || Boolean(baseBranchForWorktree),
8746 submissionIntent,
8751 // Only a draft has a background submission to hide behind its hero.
8752 submissionIntent:
8753 submissionIntent === "background" && !isLocalDraftThread ? "foreground" : submissionIntent,
87478754 });
87488755 setWorktreeSetupRef(
87498756 multipleModelSelections === null && baseBranchForWorktree
......@@ -9316,6 +9323,12 @@ export default function ChatView(props: ChatViewProps) {
93169323 }),
93179324 );
93189325 }
9326 } else if (
9327 submissionIntent === "background" &&
9328 currentRouteThreadKeyRef.current === routeThreadKey
9329 ) {
9330 // An existing thread keeps running; open a fresh composer like a draft does.
9331 handleNewThreadInActiveProject();
93199332 }
93209333 }
93219334 }
apps/web/src/components/settings/KeybindingsSettings.logic.ts+1
......@@ -314,6 +314,7 @@ export function buildKeybindingCommandOptions(
314314export function commandLabel(command: KeybindingCommand): string {
315315 if (command === "composer.sendAlternate") return "Composer: Opposite Queue or Steer Action";
316316 if (command === "composer.sendBackground") return "Composer: Start in Background";
317 if (command === "composer.sendAndNewThread") return "Composer: Send and Start New Thread";
317318 if (command === "thread.steerQueuedMessage") return "Queue: Send First Queued Message as Steer";
318319 if (command === "thread.editQueuedMessage") return "Queue: Edit Last Queued Message";
319320 if (command === "thread.copyReference") return "Pull Request: Copy Link or Thread ID";
apps/web/src/composer-logic.test.ts+8
......@@ -124,6 +124,14 @@ describe("composerSubmissionIntentForKey", () => {
124124 event: { ...modEnter, altKey: true },
125125 }),
126126 ).toBe("background");
127 expect(
128 composerSubmissionIntentForKey({
129 ...input,
130 platform,
131 sendShortcut,
132 event: { ...modEnter, altKey: true },
133 }),
134 ).toBe("background");
127135 expect(
128136 composerSubmissionIntentForKey({
129137 ...input,
apps/web/src/composer-logic.ts+1
......@@ -59,6 +59,7 @@ export function composerSubmissionIntentForKey(input: {
5959 });
6060 if (command === "composer.sendAlternate" && input.isRunning) return "alternate";
6161 if (command === "composer.sendBackground" && input.isDraftThread) return "background";
62 if (command === "composer.sendAndNewThread" && !input.isDraftThread) return "background";
6263 if (command !== null || event.key !== "Enter" || event.shiftKey || event.altKey) return null;
6364 if (
6465 composerRequiresModifier(input.sendShortcut, input.prompt ?? "") &&
docs/user/keybindings.md+6-5
......@@ -11,11 +11,12 @@ inserts a new line. This applies to the web and desktop composer at desktop widt
1111
1212**Follow-up behavior** chooses Queue or Steer while the agent runs. Use
1313`mod+Enter` to do the opposite for one message, even when the send shortcut
14requires a modifier. In a new thread, `mod+Alt+Enter` starts the thread in the
15background and opens a fresh composer. Change either shortcut in
16**Settings → Keybindings** under **Composer: Opposite Queue or Steer Action** or
17**Composer: Start in Background**. These bindings take priority over the send
18shortcut. Click the send button to use the configured follow-up behavior.
14requires a modifier. `mod+Alt+Enter` sends, keeps that thread running in the
15background, and opens a fresh new-thread composer. Change these
16shortcuts in **Settings → Keybindings** under **Composer: Opposite Queue or
17Steer Action**, **Composer: Start in Background**, or **Composer: Send and Start
18New Thread**. These bindings take priority over the send shortcut. Click the
19send button to use the configured follow-up behavior.
1920
2021When an active turn has queued messages, `mod+Shift+Enter` sends the first as a
2122steer. Change it under **Queue: Send First Queued Message as Steer** in Keybindings.
packages/contracts/src/keybindings.ts+1
......@@ -87,6 +87,7 @@ export const STATIC_KEYBINDING_COMMANDS = [
8787 "composer.stash",
8888 "composer.sendAlternate",
8989 "composer.sendBackground",
90 "composer.sendAndNewThread",
9091 "composer.host",
9192 "composer.effort",
9293 "composer.mode",
packages/shared/src/keybindings.ts+5
......@@ -53,6 +53,11 @@ export const DEFAULT_KEYBINDINGS: ReadonlyArray<KeybindingRule> = [
5353 command: "composer.sendBackground",
5454 when: "composerFocus && draftThreadRoute",
5555 },
56 {
57 key: "mod+alt+enter",
58 command: "composer.sendAndNewThread",
59 when: "composerFocus && !draftThreadRoute",
60 },
5661 { key: "mod+n", command: "chat.new", when: "!terminalFocus" },
5762 { key: "mod+shift+o", command: "chat.new", when: "!terminalFocus" },
5863 { key: "mod+shift+n", command: "chat.newLocal", when: "!terminalFocus" },