diff --git a/apps/server/src/keybindings.ts b/apps/server/src/keybindings.ts index 165b2edeb0..5cd3a2d5a8 100644 --- a/apps/server/src/keybindings.ts +++ b/apps/server/src/keybindings.ts @@ -69,6 +69,7 @@ export const DEFAULT_KEYBINDINGS: ReadonlyArray = [ { key: "mod+shift+n", command: "chat.newLocal", when: "!terminalFocus" }, { key: "mod+shift+m", command: "modelPicker.toggle", when: "!terminalFocus" }, { key: "mod+o", command: "editor.openFavorite" }, + { key: "mod+.", command: "agent.cycle", when: "!terminalFocus" }, { key: "mod+shift+[", command: "thread.previous" }, { key: "mod+shift+]", command: "thread.next" }, ...THREAD_JUMP_KEYBINDING_COMMANDS.map((command, index) => ({ diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 0c76059b6a..0631f09d9c 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -2292,6 +2292,39 @@ export default function ChatView(props: ChatViewProps) { return; } + if (command === "agent.cycle") { + event.preventDefault(); + event.stopPropagation(); + const ctx = composerRef.current?.getSendContext(); + if (!ctx) return; + const caps = getProviderModelCapabilities( + ctx.selectedProviderModels, + ctx.selectedModel, + ctx.selectedProvider, + ); + const agents = caps.agentOptions ?? []; + if (agents.length < 2) return; + const store = useComposerDraftStore.getState(); + const draft = store.getComposerDraft(composerDraftTarget); + const currentOpts = + (draft?.modelSelectionByProvider?.[ctx.selectedProvider]?.options as + | Record + | undefined) ?? {}; + const rawAgent = (currentOpts.agent as string | undefined) ?? null; + const effectiveAgent = rawAgent + ? agents.find((a) => a.value === rawAgent) + : (agents.find((a) => a.isDefault) ?? agents[0]); + const currentIndex = effectiveAgent ? agents.indexOf(effectiveAgent) : -1; + const nextAgent = agents[(currentIndex + 1) % agents.length]!; + store.setProviderModelOptions( + composerDraftTarget, + ctx.selectedProvider, + { ...currentOpts, agent: nextAgent.value }, + { model: ctx.selectedModel, persistSticky: true }, + ); + return; + } + const scriptId = projectScriptIdFromCommand(command); if (!scriptId || !activeProject) return; const script = activeProject.scripts.find((entry) => entry.id === scriptId); diff --git a/apps/web/src/components/chat/TraitsPicker.tsx b/apps/web/src/components/chat/TraitsPicker.tsx index d30fba832e..6268397b01 100644 --- a/apps/web/src/components/chat/TraitsPicker.tsx +++ b/apps/web/src/components/chat/TraitsPicker.tsx @@ -32,6 +32,9 @@ import { } from "../ui/menu"; import { useComposerDraftStore, DraftId } from "../../composerDraftStore"; import { getProviderModelCapabilities } from "../../providerModels"; +import { shortcutLabelForCommand } from "../../keybindings"; +import { useServerKeybindings } from "../../rpc/serverState"; +import { Kbd } from "../ui/kbd"; import { cn } from "~/lib/utils"; type ProviderOptions = ProviderModelOptions[ProviderKind]; @@ -257,6 +260,8 @@ export const TraitsMenuContent = memo(function TraitsMenuContentImpl({ ...persistence }: TraitsMenuContentProps & TraitsPersistence) { const setProviderModelOptions = useComposerDraftStore((store) => store.setProviderModelOptions); + const keybindings = useServerKeybindings(); + const agentCycleLabel = shortcutLabelForCommand(keybindings, "agent.cycle"); const updateModelOptions = useCallback( (nextOptions: ProviderOptions | undefined) => { if ("onModelOptionsChange" in persistence) { @@ -452,7 +457,14 @@ export const TraitsMenuContent = memo(function TraitsMenuContentImpl({ <> {hasSectionsBeforeAgent ? : null} -
Agent
+
+ Agent + {agentCycleLabel ? ( + + {agentCycleLabel} + + ) : null} +
{ diff --git a/apps/web/src/keybindings.test.ts b/apps/web/src/keybindings.test.ts index 85c14fa0ab..f8867bb7cc 100644 --- a/apps/web/src/keybindings.test.ts +++ b/apps/web/src/keybindings.test.ts @@ -138,6 +138,11 @@ const DEFAULT_BINDINGS = compile([ command: "modelPicker.jump.3", whenAst: whenIdentifier("modelPickerOpen"), }, + { + shortcut: modShortcut("."), + command: "agent.cycle", + whenAst: whenNot(whenIdentifier("terminalFocus")), + }, ]); describe("isTerminalToggleShortcut", () => { @@ -314,6 +319,8 @@ describe("shortcutLabelForCommand", () => { }), "⌘3", ); + assert.strictEqual(shortcutLabelForCommand(DEFAULT_BINDINGS, "agent.cycle", "MacIntel"), "⌘."); + assert.strictEqual(shortcutLabelForCommand(DEFAULT_BINDINGS, "agent.cycle", "Linux"), "Ctrl+."); }); it("returns null for commands shadowed by a later conflicting shortcut", () => { diff --git a/packages/contracts/src/keybindings.ts b/packages/contracts/src/keybindings.ts index d3b85d1cda..a1b1794656 100644 --- a/packages/contracts/src/keybindings.ts +++ b/packages/contracts/src/keybindings.ts @@ -57,6 +57,7 @@ const STATIC_KEYBINDING_COMMANDS = [ "chat.new", "chat.newLocal", "editor.openFavorite", + "agent.cycle", ...MODEL_PICKER_KEYBINDING_COMMANDS, ...THREAD_KEYBINDING_COMMANDS, ] as const;