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) {...@@ -8511,6 +8511,11 @@ export default function ChatView(props: ChatViewProps) {
8511 setComposerDraftThreadContexts(composerDraftTarget, [...records]),8511 setComposerDraftThreadContexts(composerDraftTarget, [...records]),
8512 resetCursor: (options) => composerRef.current?.resetCursorState(options),8512 resetCursor: (options) => composerRef.current?.resetCursorState(options),
8513 });8513 });
8514 } else if (
8515 submissionIntent === "background" &&
8516 currentRouteThreadKeyRef.current === routeThreadKey
8517 ) {
8518 handleNewThreadInActiveProject();
8514 }8519 }
8515 return;8520 return;
8516 }8521 }
...@@ -8743,7 +8748,9 @@ export default function ChatView(props: ChatViewProps) {...@@ -8743,7 +8748,9 @@ export default function ChatView(props: ChatViewProps) {
8743 }8748 }
8744 beginLocalDispatch({8749 beginLocalDispatch({
8745 preparingWorktree: multipleModelSelections !== null || Boolean(baseBranchForWorktree),8750 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,
8747 });8754 });
8748 setWorktreeSetupRef(8755 setWorktreeSetupRef(
8749 multipleModelSelections === null && baseBranchForWorktree8756 multipleModelSelections === null && baseBranchForWorktree
...@@ -9316,6 +9323,12 @@ export default function ChatView(props: ChatViewProps) {...@@ -9316,6 +9323,12 @@ export default function ChatView(props: ChatViewProps) {
9316 }),9323 }),
9317 );9324 );
9318 }9325 }
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();
9319 }9332 }
9320 }9333 }
9321 }9334 }
apps/web/src/components/settings/KeybindingsSettings.logic.ts+1
...@@ -314,6 +314,7 @@ export function buildKeybindingCommandOptions(...@@ -314,6 +314,7 @@ export function buildKeybindingCommandOptions(
314export function commandLabel(command: KeybindingCommand): string {314export function commandLabel(command: KeybindingCommand): string {
315 if (command === "composer.sendAlternate") return "Composer: Opposite Queue or Steer Action";315 if (command === "composer.sendAlternate") return "Composer: Opposite Queue or Steer Action";
316 if (command === "composer.sendBackground") return "Composer: Start in Background";316 if (command === "composer.sendBackground") return "Composer: Start in Background";
317 if (command === "composer.sendAndNewThread") return "Composer: Send and Start New Thread";
317 if (command === "thread.steerQueuedMessage") return "Queue: Send First Queued Message as Steer";318 if (command === "thread.steerQueuedMessage") return "Queue: Send First Queued Message as Steer";
318 if (command === "thread.editQueuedMessage") return "Queue: Edit Last Queued Message";319 if (command === "thread.editQueuedMessage") return "Queue: Edit Last Queued Message";
319 if (command === "thread.copyReference") return "Pull Request: Copy Link or Thread ID";320 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", () => {...@@ -124,6 +124,14 @@ describe("composerSubmissionIntentForKey", () => {
124 event: { ...modEnter, altKey: true },124 event: { ...modEnter, altKey: true },
125 }),125 }),
126 ).toBe("background");126 ).toBe("background");
127 expect(
128 composerSubmissionIntentForKey({
129 ...input,
130 platform,
131 sendShortcut,
132 event: { ...modEnter, altKey: true },
133 }),
134 ).toBe("background");
127 expect(135 expect(
128 composerSubmissionIntentForKey({136 composerSubmissionIntentForKey({
129 ...input,137 ...input,
apps/web/src/composer-logic.ts+1
...@@ -59,6 +59,7 @@ export function composerSubmissionIntentForKey(input: {...@@ -59,6 +59,7 @@ export function composerSubmissionIntentForKey(input: {
59 });59 });
60 if (command === "composer.sendAlternate" && input.isRunning) return "alternate";60 if (command === "composer.sendAlternate" && input.isRunning) return "alternate";
61 if (command === "composer.sendBackground" && input.isDraftThread) return "background";61 if (command === "composer.sendBackground" && input.isDraftThread) return "background";
62 if (command === "composer.sendAndNewThread" && !input.isDraftThread) return "background";
62 if (command !== null || event.key !== "Enter" || event.shiftKey || event.altKey) return null;63 if (command !== null || event.key !== "Enter" || event.shiftKey || event.altKey) return null;
63 if (64 if (
64 composerRequiresModifier(input.sendShortcut, input.prompt ?? "") &&65 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...@@ -11,11 +11,12 @@ inserts a new line. This applies to the web and desktop composer at desktop widt
1111
12**Follow-up behavior** chooses Queue or Steer while the agent runs. Use12**Follow-up behavior** chooses Queue or Steer while the agent runs. Use
13`mod+Enter` to do the opposite for one message, even when the send shortcut13`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 the14requires a modifier. `mod+Alt+Enter` sends, keeps that thread running in the
15background and opens a fresh composer. Change either shortcut in15background, and opens a fresh new-thread composer. Change these
16**Settings → Keybindings** under **Composer: Opposite Queue or Steer Action** or16shortcuts in **Settings → Keybindings** under **Composer: Opposite Queue or
17**Composer: Start in Background**. These bindings take priority over the send17Steer Action**, **Composer: Start in Background**, or **Composer: Send and Start
18shortcut. Click the send button to use the configured follow-up behavior.18New Thread**. These bindings take priority over the send shortcut. Click the
19send button to use the configured follow-up behavior.
1920
20When an active turn has queued messages, `mod+Shift+Enter` sends the first as a21When an active turn has queued messages, `mod+Shift+Enter` sends the first as a
21steer. Change it under **Queue: Send First Queued Message as Steer** in Keybindings.22steer. 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 = [...@@ -87,6 +87,7 @@ export const STATIC_KEYBINDING_COMMANDS = [
87 "composer.stash",87 "composer.stash",
88 "composer.sendAlternate",88 "composer.sendAlternate",
89 "composer.sendBackground",89 "composer.sendBackground",
90 "composer.sendAndNewThread",
90 "composer.host",91 "composer.host",
91 "composer.effort",92 "composer.effort",
92 "composer.mode",93 "composer.mode",
packages/shared/src/keybindings.ts+5
...@@ -53,6 +53,11 @@ export const DEFAULT_KEYBINDINGS: ReadonlyArray<KeybindingRule> = [...@@ -53,6 +53,11 @@ export const DEFAULT_KEYBINDINGS: ReadonlyArray<KeybindingRule> = [
53 command: "composer.sendBackground",53 command: "composer.sendBackground",
54 when: "composerFocus && draftThreadRoute",54 when: "composerFocus && draftThreadRoute",
55 },55 },
56 {
57 key: "mod+alt+enter",
58 command: "composer.sendAndNewThread",
59 when: "composerFocus && !draftThreadRoute",
60 },
56 { key: "mod+n", command: "chat.new", when: "!terminalFocus" },61 { key: "mod+n", command: "chat.new", when: "!terminalFocus" },
57 { key: "mod+shift+o", command: "chat.new", when: "!terminalFocus" },62 { key: "mod+shift+o", command: "chat.new", when: "!terminalFocus" },
58 { key: "mod+shift+n", command: "chat.newLocal", when: "!terminalFocus" },63 { key: "mod+shift+n", command: "chat.newLocal", when: "!terminalFocus" },