From 5c0429b1578cff3307c35c51a305e2749a6dcb6e Mon Sep 17 00:00:00 2001 From: maria Date: Sat, 3 Oct 2026 03:07:55 -0300 Subject: [PATCH] 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> --- apps/web/src/components/ChatView.tsx | 15 ++++++++++++++- .../settings/KeybindingsSettings.logic.ts | 1 + apps/web/src/composer-logic.test.ts | 8 ++++++++ apps/web/src/composer-logic.ts | 1 + docs/user/keybindings.md | 11 ++++++----- packages/contracts/src/keybindings.ts | 1 + packages/shared/src/keybindings.ts | 5 +++++ 7 files changed, 36 insertions(+), 6 deletions(-) diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 233878f5de8e9b5750d839308d1f5ed3b24ad158..8e105a49f37f49ee18f358e88c03716898f62206 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -8511,6 +8511,11 @@ export default function ChatView(props: ChatViewProps) { setComposerDraftThreadContexts(composerDraftTarget, [...records]), resetCursor: (options) => composerRef.current?.resetCursorState(options), }); + } else if ( + submissionIntent === "background" && + currentRouteThreadKeyRef.current === routeThreadKey + ) { + handleNewThreadInActiveProject(); } return; } @@ -8743,7 +8748,9 @@ export default function ChatView(props: ChatViewProps) { } beginLocalDispatch({ preparingWorktree: multipleModelSelections !== null || Boolean(baseBranchForWorktree), - submissionIntent, + // Only a draft has a background submission to hide behind its hero. + submissionIntent: + submissionIntent === "background" && !isLocalDraftThread ? "foreground" : submissionIntent, }); setWorktreeSetupRef( multipleModelSelections === null && baseBranchForWorktree @@ -9316,6 +9323,12 @@ export default function ChatView(props: ChatViewProps) { }), ); } + } else if ( + submissionIntent === "background" && + currentRouteThreadKeyRef.current === routeThreadKey + ) { + // An existing thread keeps running; open a fresh composer like a draft does. + handleNewThreadInActiveProject(); } } } diff --git a/apps/web/src/components/settings/KeybindingsSettings.logic.ts b/apps/web/src/components/settings/KeybindingsSettings.logic.ts index dc31de55d1f8c87ba299518860273c6609e52d29..4897007d8fdacf311841b1a715ebed82c23ba983 100644 --- a/apps/web/src/components/settings/KeybindingsSettings.logic.ts +++ b/apps/web/src/components/settings/KeybindingsSettings.logic.ts @@ -314,6 +314,7 @@ export function buildKeybindingCommandOptions( export function commandLabel(command: KeybindingCommand): string { if (command === "composer.sendAlternate") return "Composer: Opposite Queue or Steer Action"; if (command === "composer.sendBackground") return "Composer: Start in Background"; + if (command === "composer.sendAndNewThread") return "Composer: Send and Start New Thread"; if (command === "thread.steerQueuedMessage") return "Queue: Send First Queued Message as Steer"; if (command === "thread.editQueuedMessage") return "Queue: Edit Last Queued Message"; if (command === "thread.copyReference") return "Pull Request: Copy Link or Thread ID"; diff --git a/apps/web/src/composer-logic.test.ts b/apps/web/src/composer-logic.test.ts index 2da0c9671c73c091e9f96a835e3d57669fdd2ed7..6d81f3be8507556bfbabf0e8ee6d570082a254f0 100644 --- a/apps/web/src/composer-logic.test.ts +++ b/apps/web/src/composer-logic.test.ts @@ -124,6 +124,14 @@ describe("composerSubmissionIntentForKey", () => { event: { ...modEnter, altKey: true }, }), ).toBe("background"); + expect( + composerSubmissionIntentForKey({ + ...input, + platform, + sendShortcut, + event: { ...modEnter, altKey: true }, + }), + ).toBe("background"); expect( composerSubmissionIntentForKey({ ...input, diff --git a/apps/web/src/composer-logic.ts b/apps/web/src/composer-logic.ts index 99567b5a4a68a2dc901f7efc4a4ecb6f364b082b..09c5e714aeb38b43efc06b10baaf6be0056cce77 100644 --- a/apps/web/src/composer-logic.ts +++ b/apps/web/src/composer-logic.ts @@ -59,6 +59,7 @@ export function composerSubmissionIntentForKey(input: { }); if (command === "composer.sendAlternate" && input.isRunning) return "alternate"; if (command === "composer.sendBackground" && input.isDraftThread) return "background"; + if (command === "composer.sendAndNewThread" && !input.isDraftThread) return "background"; if (command !== null || event.key !== "Enter" || event.shiftKey || event.altKey) return null; if ( composerRequiresModifier(input.sendShortcut, input.prompt ?? "") && diff --git a/docs/user/keybindings.md b/docs/user/keybindings.md index 43bd23333f66e11fb2be14dbfcff7a2f68609f85..63c9577767afd9618446fbfeda161684cb6d1134 100644 --- a/docs/user/keybindings.md +++ b/docs/user/keybindings.md @@ -11,11 +11,12 @@ inserts a new line. This applies to the web and desktop composer at desktop widt **Follow-up behavior** chooses Queue or Steer while the agent runs. Use `mod+Enter` to do the opposite for one message, even when the send shortcut -requires a modifier. In a new thread, `mod+Alt+Enter` starts the thread in the -background and opens a fresh composer. Change either shortcut in -**Settings → Keybindings** under **Composer: Opposite Queue or Steer Action** or -**Composer: Start in Background**. These bindings take priority over the send -shortcut. Click the send button to use the configured follow-up behavior. +requires a modifier. `mod+Alt+Enter` sends, keeps that thread running in the +background, and opens a fresh new-thread composer. Change these +shortcuts in **Settings → Keybindings** under **Composer: Opposite Queue or +Steer Action**, **Composer: Start in Background**, or **Composer: Send and Start +New Thread**. These bindings take priority over the send shortcut. Click the +send button to use the configured follow-up behavior. When an active turn has queued messages, `mod+Shift+Enter` sends the first as a steer. Change it under **Queue: Send First Queued Message as Steer** in Keybindings. diff --git a/packages/contracts/src/keybindings.ts b/packages/contracts/src/keybindings.ts index 06ac4fb838200778117ef2300b33fb79daffd84c..e827ba8270fd6a049937955d0ccce850b5d2f007 100644 --- a/packages/contracts/src/keybindings.ts +++ b/packages/contracts/src/keybindings.ts @@ -87,6 +87,7 @@ export const STATIC_KEYBINDING_COMMANDS = [ "composer.stash", "composer.sendAlternate", "composer.sendBackground", + "composer.sendAndNewThread", "composer.host", "composer.effort", "composer.mode", diff --git a/packages/shared/src/keybindings.ts b/packages/shared/src/keybindings.ts index 447d42560365075904a115709b8bb42b15b23fe6..8cb3fee23e022ccdf937b9c6d6020262216d9942 100644 --- a/packages/shared/src/keybindings.ts +++ b/packages/shared/src/keybindings.ts @@ -53,6 +53,11 @@ export const DEFAULT_KEYBINDINGS: ReadonlyArray = [ command: "composer.sendBackground", when: "composerFocus && draftThreadRoute", }, + { + key: "mod+alt+enter", + command: "composer.sendAndNewThread", + when: "composerFocus && !draftThreadRoute", + }, { key: "mod+n", command: "chat.new", when: "!terminalFocus" }, { key: "mod+shift+o", command: "chat.new", when: "!terminalFocus" }, { key: "mod+shift+n", command: "chat.newLocal", when: "!terminalFocus" }, -- 2.54.0