# Files changed · PR \#23915 · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/pull/23915/diffs)

Repository: [pingdotgg/t3code](https://git.cafe/pingdotgg/t3code)

Visibility: public

[feat\(web\): opt-in setting to start new threads in background](https://git.cafe/pingdotgg/t3code/pull/23915.md)

Version: 9

Head commit: 9433d7d7f8b6c40f00e25ef3acfb6d682f795d46

Base commit: 50e151c7a41460649ad7e63acbb00c03e5175c18

Comparison base: 50e151c7a41460649ad7e63acbb00c03e5175c18

Showing up to 20 entries from this revision. If the pull request changes, reopen the first page before continuing.

## apps/web/src/components/chat/ChatComposer.tsx

Status: modified; +8; −0

```diff
@@ -63,6 +63,7 @@
   type ComposerTrigger,
   type ComposerSubmissionIntent,
   collapseExpandedComposerCursor,
+  composerDefaultSendIntent,
   composerRequiresModifier,
   composerStateAtPromptEnd,
   detectComposerTrigger,
@@ -2169,6 +2170,10 @@
     }
   };
   const submitComposerStable = useStableCallback(submitComposer);
+  const defaultSendIntent = composerDefaultSendIntent({
+    isDraftThread: routeKind === "draft",
+    startNewThreadsInBackground: settings.startNewThreadsInBackground,
+  });
   const handleSubmitMessage = (event: React.MouseEvent<HTMLButtonElement>) => {
     event.preventDefault();
     submitComposer(
@@ -2179,6 +2184,7 @@
         activeTurnDefault: followUpBehavior,
         canSteer: !isCompacting,
       }),
+      defaultSendIntent,
     );
   };
   const handleSubmitMessageStable = useStableCallback(handleSubmitMessage);
@@ -2191,6 +2197,7 @@
         activeTurnDefault: followUpBehavior,
         canSteer: !isCompacting,
       }),
+      defaultSendIntent,
     );
   };
   const submitCitationAndSendStable = useStableCallback(submitCitationAndSend);
@@ -2250,6 +2257,7 @@
     phase,
     isCompacting,
     followUpBehavior,
+    startNewThreadsInBackground: settings.startNewThreadsInBackground,
     isSoftwareKeyboardOpen,
     planModeUiEnabled,
     isLiteralPendingAnswer,
```

## apps/web/src/components/chat/useComposerCommandKey.ts

Status: modified; +9; −1

```diff
@@ -14,6 +14,7 @@
   type ComposerSubmissionIntent,
   type ComposerTrigger,
   collapseExpandedComposerCursor,
+  composerDefaultSendIntent,
   composerSubmissionIntentForKey,
 } from "../../composer-logic";
 import { type PendingUserInput } from "../../session-logic";
@@ -34,6 +35,7 @@
   readonly phase: ChatComposerProps["phase"];
   readonly isCompacting: boolean;
   readonly followUpBehavior: ChatComposerProps["settings"]["followUpBehavior"];
+  readonly startNewThreadsInBackground: ChatComposerProps["settings"]["startNewThreadsInBackground"];
   readonly isSoftwareKeyboardOpen: boolean;
   readonly planModeUiEnabled: boolean;
   readonly isLiteralPendingAnswer: boolean;
@@ -86,6 +88,7 @@
     phase,
     isCompacting,
     followUpBehavior,
+    startNewThreadsInBackground,
     isSoftwareKeyboardOpen,
     planModeUiEnabled,
     isLiteralPendingAnswer,
@@ -185,6 +188,7 @@
       isRunning: phase === "running",
       sendShortcut,
       prompt: promptRef.current,
+      startNewThreadsInBackground,
     });
     if (key === "Tab" && event.shiftKey && submissionIntent === null) {
       if (!planModeUiEnabled) return false;
@@ -200,7 +204,11 @@
       composerMenuOpenRef.current = false;
       return true;
     }
-    if (menuIsActive && (submissionIntent === null || submissionIntent === "foreground")) {
+    const plainSendIntent = composerDefaultSendIntent({
+      isDraftThread: routeKind === "draft",
+      startNewThreadsInBackground,
+    });
+    if (menuIsActive && (submissionIntent === null || submissionIntent === plainSendIntent)) {
       const currentItems = composerMenuItemsRef.current;
       const selectedItem = activeComposerMenuItemRef.current ?? currentItems[0];
       if (key === "ArrowDown" && currentItems.length > 0) {
```

## apps/web/src/components/settings/SettingsPanels.tsx

Status: modified; +41; −1

```diff
@@ -482,6 +482,10 @@
     ...(settings.followUpBehavior !== DEFAULT_UNIFIED_SETTINGS.followUpBehavior
       ? ["Follow-up behavior"]
       : []),
+    ...(settings.startNewThreadsInBackground !==
+    DEFAULT_UNIFIED_SETTINGS.startNewThreadsInBackground
+      ? ["Start new threads in background"]
+      : []),
     ...(settings.contextWindowMeterEnabled !== DEFAULT_UNIFIED_SETTINGS.contextWindowMeterEnabled
       ? ["Context window indicator"]
       : []),
@@ -601,6 +605,7 @@
       composerRichTextEnabled: DEFAULT_UNIFIED_SETTINGS.composerRichTextEnabled,
       sendShortcut: DEFAULT_UNIFIED_SETTINGS.sendShortcut,
       followUpBehavior: DEFAULT_UNIFIED_SETTINGS.followUpBehavior,
+      startNewThreadsInBackground: DEFAULT_UNIFIED_SETTINGS.startNewThreadsInBackground,
       contextWindowMeterEnabled: DEFAULT_UNIFIED_SETTINGS.contextWindowMeterEnabled,
       environmentIdentificationMode: DEFAULT_UNIFIED_SETTINGS.environmentIdentificationMode,
       glassOpacity: DEFAULT_UNIFIED_SETTINGS.glassOpacity,
@@ -783,7 +788,9 @@
 }
 
 export function GeneralSettingsPanel() {
+  const isMac = isMacPlatform(navigator.platform);
+  const modifierLabel = isMac ? "⌘" : "Ctrl";
+  const altLabel = isMac ? "Option" : "Alt";
-  const modifierLabel = isMacPlatform(navigator.platform) ? "⌘" : "Ctrl";
   const sendShortcutOptions = [
     { value: "enter", label: "Enter" },
     { value: "mod-enter-multiline", label: `${modifierLabel} + Enter for multiline prompts` },
@@ -1552,6 +1559,39 @@
         />
 
         <SettingsRow
+          {...searchableSetting("start-new-threads-in-background")}
+          description={
+            "Sending from a new thread starts it in the background and keeps the new-thread composer open. " +
+            (settings.sendShortcut === "mod-enter-multiline"
+              ? `Press ${modifierLabel} + Enter for single-line prompts or ${modifierLabel} + ${altLabel} + Enter for multiline prompts to open the thread instead.`
+              : `Press ${modifierLabel}${settings.sendShortcut === "mod-enter" ? ` + ${altLabel}` : ""} + Enter to open the thread instead.`)
+          }
+          resetAction={
+            settings.startNewThreadsInBackground !==
+            DEFAULT_UNIFIED_SETTINGS.startNewThreadsInBackground ? (
+              <SettingResetButton
+                label="start new threads in background"
+                onClick={() =>
+                  updateSettings({
+                    startNewThreadsInBackground:
+                      DEFAULT_UNIFIED_SETTINGS.startNewThreadsInBackground,
+                  })
+                }
+              />
+            ) : null
+          }
+          control={
+            <Switch
+              checked={settings.startNewThreadsInBackground}
+              onCheckedChange={(checked) =>
+                updateSettings({ startNewThreadsInBackground: Boolean(checked) })
+              }
+              aria-label="Start new threads in background"
+            />
+          }
+        />
+
+        <SettingsRow
           serverScoped
           settingKeys={["enableProviderUpdateChecks"]}
           {...searchableSetting("provider-update-checks")}
```

## apps/web/src/components/settings/settingsSearch.ts

Status: modified; +6; −0

```diff
@@ -440,6 +440,12 @@
     searchTerms: ["queue steer running turn send default behavior composer"],
   },
   {
+    id: "start-new-threads-in-background",
+    title: "Start new threads in background",
+    to: "/settings/general",
+    searchTerms: ["background send enter new thread draft keep composer open"],
+  },
+  {
     id: "provider-update-checks",
     title: "Provider update checks",
     to: "/settings/general",
```

## apps/web/src/composer-logic.test.ts

Status: modified; +33; −0

```diff
@@ -203,6 +203,39 @@
     },
   );
 
+  const ctrlEnter = { ...enter, ctrlKey: true };
+  const ctrlAltEnter = { ...enter, ctrlKey: true, altKey: true };
+  it.each([
+    [true, "enter", "one line", true, enter, "background"],
+    [true, "enter", "one line", true, ctrlEnter, "foreground"],
+    [true, "enter", "one line", true, ctrlAltEnter, "foreground"],
+    [true, "enter", "one line", false, enter, "foreground"],
+    [true, "enter", "one line", false, ctrlAltEnter, "background"],
+    [true, "mod-enter", "one line", true, enter, null],
+    [true, "mod-enter", "one line", true, ctrlEnter, "background"],
+    [true, "mod-enter", "one line", true, ctrlAltEnter, "foreground"],
+    [true, "mod-enter-multiline", "two\nlines", true, ctrlEnter, "background"],
+    [true, "mod-enter-multiline", "two\nlines", true, ctrlAltEnter, "foreground"],
+    [true, "mod-enter-multiline", "one line", true, enter, "background"],
+    [true, "mod-enter-multiline", "one line", true, ctrlEnter, "foreground"],
+    [false, "mod-enter", "one line", true, ctrlEnter, "background"],
+    [false, "mod-enter", "one line", true, ctrlAltEnter, "background"],
+  ] as const)(
+    "resolves new-thread sends (in background %s, %s, %j, draft %s, %j)",
+    (startNewThreadsInBackground, sendShortcut, prompt, isDraftThread, event, expected) => {
+      expect(
+        composerSubmissionIntentForKey({
+          ...input,
+          startNewThreadsInBackground,
+          sendShortcut,
+          prompt,
+          isDraftThread,
+          event,
+        }),
+      ).toBe(expected);
+    },
+  );
+
   it.each(["MacIntel", "Win32", "Linux"])(
     "keeps modifier submission available with the software keyboard open on %s",
     (platform) => {
```

## apps/web/src/composer-logic.ts

Status: modified; +28; −9

```diff
@@ -20,6 +20,7 @@
 export type ComposerTriggerKind = "path" | "pull-request" | "slash-command" | "skill";
 export type ComposerSlashCommand = "model" | "plan" | "default";
 export type ComposerSubmissionIntent = "foreground" | "background" | "alternate";
+export type ComposerSendIntent = Exclude<ComposerSubmissionIntent, "alternate">;
 
 export interface ComposerTrigger {
   kind: ComposerTriggerKind;
@@ -56,6 +57,14 @@
   );
 }
 
+/** Intent for a plain send from this route: Enter and the send button. */
+export function composerDefaultSendIntent(input: {
+  isDraftThread: boolean;
+  startNewThreadsInBackground: boolean;
+}): ComposerSendIntent {
+  return input.isDraftThread && input.startNewThreadsInBackground ? "background" : "foreground";
+}
+
 export function composerSubmissionIntentForKey(input: {
   event: ShortcutEventLike & { isComposing?: boolean; keyCode?: number; repeat?: boolean };
   keybindings: ResolvedKeybindingsConfig;
@@ -65,31 +74,41 @@
   isRunning?: boolean;
   sendShortcut?: ClientSettings["sendShortcut"];
   prompt?: string;
+  startNewThreadsInBackground?: boolean;
 }): ComposerSubmissionIntent | null {
   const { event } = input;
   if (event.isComposing || event.keyCode === 229 || event.repeat) return null;
   if (input.isSoftwareKeyboardOpen && event.key === "Enter" && !event.metaKey && !event.ctrlKey)
     return null;
+  const requiresModifier = composerRequiresModifier(input.sendShortcut, input.prompt ?? "");
   const command = resolveShortcutCommand(event, input.keybindings, {
     ...(input.platform === undefined ? {} : { platform: input.platform }),
     context: {
       composerFocus: true,
       draftThreadRoute: input.isDraftThread,
       turnRunning: input.isRunning === true,
+      composerRequiresModifier: requiresModifier,
-      composerRequiresModifier: composerRequiresModifier(input.sendShortcut, input.prompt ?? ""),
     },
   });
+  const defaultIntent = composerDefaultSendIntent({
+    isDraftThread: input.isDraftThread,
+    startNewThreadsInBackground: input.startNewThreadsInBackground === true,
+  });
   if (command === "composer.sendAlternate" && input.isRunning) return "alternate";
+  if (command === "composer.sendBackground" && input.isDraftThread) {
+    // A modifier send shortcut makes the send chord (Mod+Enter) hit this binding, and the send
+    // chord must still start in the background when the setting is on.
+    const isSendChord =
+      event.key === "Enter" &&
+      !event.shiftKey &&
+      !event.altKey &&
-  if (command === "composer.sendBackground" && input.isDraftThread) return "background";
+      requiresModifier === (event.metaKey || event.ctrlKey);
+    return isSendChord || defaultIntent === "foreground" ? "background" : "foreground";
+  }
   if (command === "composer.sendAndNewThread" && !input.isDraftThread) return "background";
   if (command !== null || event.key !== "Enter" || event.shiftKey || event.altKey) return null;
+  if (requiresModifier && !event.metaKey && !event.ctrlKey) return null;
+  return defaultIntent;
-  if (
-    composerRequiresModifier(input.sendShortcut, input.prompt ?? "") &&
-    !event.metaKey &&
-    !event.ctrlKey
-  )
-    return null;
-  return "foreground";
 }
 
 const isInlineTokenSegment = (segment: ComposerPromptSegment): boolean => segment.type !== "text";
```

## docs/user/keybindings.md

Status: modified; +6; −0

```diff
@@ -18,6 +18,12 @@
 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.
 
+Turn on **Settings → General → Start new threads in background** to make sending
+from a new thread start it in the background. `mod+Enter` or `mod+Alt+Enter` then
+opens the thread instead. When your send shortcut requires `mod+Enter`, including
+for a multiline prompt, `mod+Enter` starts the thread in the background, so use
+`mod+Alt+Enter`.
+
 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.
 
```

## packages/contracts/src/settings.ts

Status: modified; +4; −0

```diff
@@ -441,6 +441,9 @@
   followUpBehavior: Schema.Literals(["queue", "steer"]).pipe(
     Schema.withDecodingDefault(Effect.succeed("queue")),
   ),
+  startNewThreadsInBackground: Schema.Boolean.pipe(
+    Schema.withDecodingDefault(Effect.succeed(false)),
+  ),
   proactivePanelsEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),
   showSkillsInSlashMenu: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),
   legacySidebarEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),
@@ -1731,6 +1734,7 @@
   composerRichTextEnabled: Schema.optionalKey(Schema.Boolean),
   sendShortcut: Schema.optionalKey(Schema.Literals(["enter", "mod-enter-multiline", "mod-enter"])),
   followUpBehavior: Schema.optionalKey(Schema.Literals(["queue", "steer"])),
+  startNewThreadsInBackground: Schema.optionalKey(Schema.Boolean),
   proactivePanelsEnabled: Schema.optionalKey(Schema.Boolean),
   showSkillsInSlashMenu: Schema.optionalKey(Schema.Boolean),
   legacySidebarEnabled: Schema.optionalKey(Schema.Boolean),
```
