feat(app): cycle agent modes with Shift+Tab

This commit is contained in:
Mohamed Boudra
2026-06-24 14:41:14 +07:00
parent 7a817774b7
commit 6fe320055d
20 changed files with 310 additions and 108 deletions
+2 -1
View File
@@ -12,7 +12,8 @@ export type MessageInputKeyboardActionKind =
| "dictation-cancel"
| "dictation-confirm"
| "voice-toggle"
| "voice-mute-toggle";
| "voice-mute-toggle"
| "mode-cycle";
export type KeyboardActionId =
| "agent.interrupt"
@@ -9,6 +9,7 @@ export type KeyboardActionId =
| "message-input.dictation-confirm"
| "message-input.voice-toggle"
| "message-input.voice-mute-toggle"
| "message-input.mode-cycle"
| "workspace.tab.new"
| "workspace.tab.close-current"
| "workspace.tab.navigate-index"
@@ -39,6 +40,7 @@ export type KeyboardActionDefinition =
| { id: "message-input.dictation-confirm"; scope: KeyboardActionScope }
| { id: "message-input.voice-toggle"; scope: KeyboardActionScope }
| { id: "message-input.voice-mute-toggle"; scope: KeyboardActionScope }
| { id: "message-input.mode-cycle"; scope: KeyboardActionScope }
| { id: "workspace.tab.new"; scope: KeyboardActionScope }
| { id: "workspace.tab.close-current"; scope: KeyboardActionScope }
| { id: "workspace.tab.navigate-index"; scope: KeyboardActionScope; index: number }
@@ -299,6 +299,13 @@ describe("keyboard-shortcuts", () => {
action: "message-input.action",
payload: { kind: "dictation-toggle" },
},
{
name: "routes Shift+Tab to cycle agent mode from the message input",
event: { key: "Tab", code: "Tab", shiftKey: true },
context: { focusScope: "message-input" },
action: "message-input.action",
payload: { kind: "mode-cycle" },
},
{
name: "routes space to voice mute toggle outside editable scopes",
event: { key: " ", code: "Space" },
@@ -427,6 +434,16 @@ describe("keyboard-shortcuts", () => {
event: { key: "d", code: "KeyD", metaKey: true },
context: { isMac: true, focusScope: "terminal" },
},
{
name: "does not cycle agent mode outside the message input",
event: { key: "Tab", code: "Tab", shiftKey: true },
context: { focusScope: "other" },
},
{
name: "does not repeat agent mode cycling while Shift+Tab is held",
event: { key: "Tab", code: "Tab", shiftKey: true, repeat: true },
context: { focusScope: "message-input" },
},
{
name: "does not bind Cmd+Enter as a rebindable message queue shortcut",
event: { key: "Enter", code: "Enter", metaKey: true },
@@ -570,6 +587,7 @@ describe("keyboard-shortcut help sections", () => {
"workspace-tab-close-current": ["alt", "shift", "W"],
"workspace-pane-split-right": ["mod", "\\"],
"workspace-pane-close": ["mod", "shift", "W"],
"cycle-agent-mode": ["shift", "Tab"],
},
},
{
@@ -617,12 +635,14 @@ describe("keyboard-shortcut help sections", () => {
const projects = sections.find((section) => section.id === "projects");
const panels = sections.find((section) => section.id === "panels");
const openProject = findRow(sections, "new-agent");
const cycleAgentMode = findRow(sections, "cycle-agent-mode");
const showShortcuts = findRow(sections, "show-shortcuts");
expect(projects?.titleKey).toBe("settings.shortcuts.sections.projects");
expect(panels?.titleKey).toBe("settings.shortcuts.sections.panels");
expect(openProject?.labelKey).toBe("settings.shortcuts.help.openProject");
expect(openProject?.label).toBe("Open project");
expect(cycleAgentMode?.labelKey).toBe("settings.shortcuts.help.cycleAgentMode");
expect(showShortcuts?.noteKey).toBe("settings.shortcuts.helpNotes.showKeyboardShortcuts");
});
@@ -152,6 +152,7 @@ const SHORTCUT_HELP_LABEL_KEYS: Record<string, string> = {
"toggle-focus": "settings.shortcuts.help.toggleFocusMode",
"cycle-theme": "settings.shortcuts.help.cycleTheme",
"focus-message-input": "settings.shortcuts.help.focusMessageInput",
"cycle-agent-mode": "settings.shortcuts.help.cycleAgentMode",
"voice-toggle": "settings.shortcuts.help.toggleVoiceMode",
"dictation-toggle": "settings.shortcuts.help.startStopDictation",
"agent-interrupt": "settings.shortcuts.help.interruptAgent",
@@ -910,6 +911,20 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
keys: ["mod", "L"],
},
},
{
id: "message-input-mode-cycle-shift-tab",
action: "message-input.action",
combo: "Shift+Tab",
repeat: false,
when: { commandCenter: false, focusScope: "message-input" },
payload: { type: "message-input", kind: "mode-cycle" },
help: {
id: "cycle-agent-mode",
section: "agent-input",
label: "Cycle agent mode",
keys: ["shift", "Tab"],
},
},
{
id: "message-input-voice-toggle-cmd-shift-d-mac",
action: "message-input.action",
@@ -276,6 +276,7 @@ describe("routeKeyboardShortcut — message-input.action", () => {
["dictation-confirm", "message-input.dictation-confirm"],
["voice-toggle", "message-input.voice-toggle"],
["voice-mute-toggle", "message-input.voice-mute-toggle"],
["mode-cycle", "message-input.mode-cycle"],
] as const)("kind=%s → dispatch %s", (kind, id) => {
expect(
routeKeyboardShortcut({ action: "message-input.action", payload: { kind } }, makeCtx()),
@@ -82,6 +82,7 @@ const MESSAGE_INPUT_DISPATCH: Record<
"dictation-confirm": { id: "message-input.dictation-confirm", scope: "message-input" },
"voice-toggle": { id: "message-input.voice-toggle", scope: "message-input" },
"voice-mute-toggle": { id: "message-input.voice-mute-toggle", scope: "message-input" },
"mode-cycle": { id: "message-input.mode-cycle", scope: "message-input" },
};
function hasPayloadKey<K extends "index" | "delta" | "kind">(