Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions apps/web/src/composer-logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,92 @@ describe("composerSubmissionIntentForKey", () => {
).toBe("alternate");
});

it.each(["MacIntel", "Win32", "Linux"])(
"honors the send shortcut when starting a draft on %s",
(platform) => {
const modEnter = {
...enter,
metaKey: platform === "MacIntel",
ctrlKey: platform !== "MacIntel",
};
for (const [sendShortcut, prompt, requiresModifier] of [
["enter", "one line", false],
["enter", "two\nlines", false],
["mod-enter", "one line", true],
["mod-enter", "two\nlines", true],
["mod-enter-multiline", "one line", false],
["mod-enter-multiline", "two\nlines", true],
] as const) {
const draft = { ...input, platform, sendShortcut, prompt, isDraftThread: true };
expect(composerSubmissionIntentForKey({ ...draft, event: modEnter })).toBe(
requiresModifier ? "foreground" : "background",
);
expect(
composerSubmissionIntentForKey({
...draft,
event: { ...modEnter, shiftKey: true },
}),
).toBe(requiresModifier ? "background" : null);
expect(composerSubmissionIntentForKey({ ...draft, event: enter })).toBe(
requiresModifier ? null : "foreground",
);
expect(
composerSubmissionIntentForKey({ ...draft, event: { ...enter, shiftKey: true } }),
).toBeNull();
expect(
composerSubmissionIntentForKey({ ...draft, event: { ...modEnter, altKey: true } }),
).toBe("background");
}
},
);

it("honors the send preference with an existing install's saved background binding", () => {
const keybindings = mergeWithDefaultKeybindings(
compileResolvedKeybindingsConfig([
{
key: "mod+enter",
command: "composer.sendBackground",
when: "composerFocus && draftThreadRoute",
},
]),
);
const draft = {
...input,
keybindings,
isDraftThread: true,
sendShortcut: "mod-enter" as const,
};
expect(composerSubmissionIntentForKey({ ...draft, event: { ...enter, ctrlKey: true } })).toBe(
"foreground",
);
expect(
composerSubmissionIntentForKey({
...draft,
event: { ...enter, ctrlKey: true, shiftKey: true },
}),
).toBe("background");
});

it.each([
["mod+arrowup", { key: "ArrowUp", ctrlKey: true }],
["shift+tab", { key: "Tab", shiftKey: true }],
] as const)("accepts a remapped %s action", (key, event) => {
const keybindings = mergeWithDefaultKeybindings(
compileResolvedKeybindingsConfig([
{ key, command: "composer.sendBackground", when: "composerFocus && draftThreadRoute" },
]),
);
expect(
composerSubmissionIntentForKey({
...input,
keybindings,
sendShortcut: "mod-enter",
isDraftThread: true,
event: { ...enter, ...event },
}),
).toBe("background");
});

it.each(["enter", "mod-enter", "mod-enter-multiline"] as const)(
"uses remapped keys and removes the old action bindings with %s",
(sendShortcut) => {
Expand Down
59 changes: 59 additions & 0 deletions apps/web/src/keybindings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -835,6 +835,65 @@ describe("composer and pull request shortcuts", () => {
},
);

it.each(["MacIntel", "Win32", "Linux"])(
"reserves the normal send chord on new drafts on %s",
(platform) => {
const modifier = {
metaKey: platform === "MacIntel",
ctrlKey: platform !== "MacIntel",
};
for (const composerRequiresModifier of [false, true]) {
const context = {
composerFocus: true,
draftThreadRoute: true,
composerRequiresModifier,
};
assert.strictEqual(
resolveShortcutCommand(
event({ key: "Enter", ...modifier }),
DEFAULT_RESOLVED_KEYBINDINGS,
{
platform,
context,
},
),
composerRequiresModifier ? null : "composer.sendBackground",
);
assert.strictEqual(
resolveShortcutCommand(
event({ key: "Enter", shiftKey: true, ...modifier }),
DEFAULT_RESOLVED_KEYBINDINGS,
{ platform, context },
),
composerRequiresModifier ? "composer.sendBackground" : "thread.steerQueuedMessage",
);
const backgroundShortcuts = effectiveShortcutsForCommand(
DEFAULT_RESOLVED_KEYBINDINGS,
"composer.sendBackground",
{ platform, context },
);
assert.deepEqual(
backgroundShortcuts.map((shortcut) => formatShortcutLabel(shortcut, platform)),
[
formatShortcutLabel(modShortcut("enter", { altKey: true }), platform),
formatShortcutLabel(
modShortcut("enter", { shiftKey: composerRequiresModifier }),
platform,
),
],
);
assert.strictEqual(
resolveShortcutCommand(
event({ key: "Enter", shiftKey: true, ...modifier }),
DEFAULT_RESOLVED_KEYBINDINGS,
{ platform, context: { ...context, composerFocus: false } },
),
"thread.steerQueuedMessage",
);
}
},
);

const altEffortBindings = compileResolvedKeybindingsConfig([
{ key: "mod+alt+e", command: "composer.effort", when: "!terminalFocus" },
]);
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/keybindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,8 @@ function shortcutForContext(binding: ResolvedKeybindingRule, context: ShortcutMa
// Resolve persisted Mod+Enter against send preference without rewriting saved shortcuts.
if (
context.composerRequiresModifier &&
binding.command === "composer.sendAlternate" &&
(binding.command === "composer.sendAlternate" ||
binding.command === "composer.sendBackground") &&
shortcut.key === "enter" &&
shortcut.modKey &&
!shortcut.metaKey &&
Expand Down
22 changes: 15 additions & 7 deletions docs/user/keybindings.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,24 @@ In **Settings → Chat → Send shortcut**, choose whether Enter sends, requires
inserts a new line. This applies to the web and desktop composer at desktop widths.

**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. `mod+Alt+Enter` sends, keeps that thread running in the
background, and opens a fresh new-thread composer. In a new thread, `mod+Enter`
does the same. Change these shortcuts in **Settings → Keybindings** under
`mod+Enter` to do the opposite for one message when Enter sends. If your send
shortcut requires `mod+Enter`, add Shift for the opposite action.

On a new thread, your configured send shortcut starts it in the foreground.
When Enter sends, `mod+Enter` starts it in the background and opens a fresh
new-thread composer. When sending requires `mod+Enter`, use `mod+Shift+Enter`
for that background action instead. In multiline mode, this depends on whether
the draft contains a newline. `mod+Alt+Enter` sends in the background and opens
a fresh composer on both new and existing threads.

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.
or **Composer: Send and Start New Thread**. The default `mod+Enter` bindings
adapt to your send preference; other remapped shortcuts take priority over it.

When a thread has queued messages, `mod+Shift+Enter` sends the first one as a steer
while a turn is running, or as a new turn when the thread is stopped. Change it under
while a turn is running, or as a new turn when the thread is stopped, unless the
focused composer uses that chord for its opposite follow-up action. Change it under
**Queue: Send First Queued Message** in Keybindings.

Use `mod+shift+m` to choose a model and `mod+shift+h` to choose a host.
Expand Down
Loading