feat: add keyboard shortcuts for cycling agent and thinking variant

This commit is contained in:
Bohdan Triapitsyn
2026-01-08 17:20:33 +02:00
parent 4b0ae1d95f
commit bc4425567d
3 changed files with 122 additions and 63 deletions
@@ -575,7 +575,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
} }
} }
if (e.key === 'Tab' && !showCommandAutocomplete && !showFileMention) { if (e.key === 'Tab' && e.shiftKey && !showCommandAutocomplete && !showAgentAutocomplete && !showFileMention) {
e.preventDefault(); e.preventDefault();
cycleAgent(); cycleAgent();
return; return;
+76 -62
View File
@@ -10,6 +10,8 @@ import { useUIStore } from "@/stores/useUIStore";
import { useConfigStore } from "@/stores/useConfigStore"; import { useConfigStore } from "@/stores/useConfigStore";
import { import {
RiAddLine, RiAddLine,
RiAiAgentLine,
RiAiGenerate2,
RiArrowUpSLine, RiArrowUpSLine,
RiBrainAi3Line, RiBrainAi3Line,
RiCloseCircleLine, RiCloseCircleLine,
@@ -106,9 +108,19 @@ export const HelpDialog: React.FC = () => {
description: "Toggle Session Sidebar", description: "Toggle Session Sidebar",
icon: RiLayoutLeftLine, icon: RiLayoutLeftLine,
}, },
{
keys: ["Shift + Tab"],
description: "Cycle Agent (chat input)",
icon: RiAiAgentLine,
},
{ {
keys: [`Shift + ${mod} + M`], keys: [`Shift + ${mod} + M`],
description: "Open Model Selector", description: "Open Model Selector",
icon: RiAiGenerate2,
},
{
keys: [`Shift + ${mod} + T`],
description: "Cycle Thinking Variant",
icon: RiBrainAi3Line, icon: RiBrainAi3Line,
}, },
], ],
@@ -118,12 +130,12 @@ export const HelpDialog: React.FC = () => {
items: [ items: [
{ {
keys: [`${mod} + N`], keys: [`${mod} + N`],
description: settingsAutoCreateWorktree ? "Create new session in worktree" : "Create New Session", description: settingsAutoCreateWorktree ? "Create New Session in Worktree" : "Create New Session",
icon: settingsAutoCreateWorktree ? RiGitBranchLine : RiAddLine, icon: settingsAutoCreateWorktree ? RiGitBranchLine : RiAddLine,
}, },
{ {
keys: [`Shift + ${mod} + N`], keys: [`Shift + ${mod} + N`],
description: settingsAutoCreateWorktree ? "Create New Session" : "Create new session in worktree", description: settingsAutoCreateWorktree ? "Create New Session" : "Create New Session in Worktree",
icon: settingsAutoCreateWorktree ? RiAddLine : RiGitBranchLine, icon: settingsAutoCreateWorktree ? RiAddLine : RiGitBranchLine,
}, },
{ keys: [`${mod} + I`], description: "Focus Chat Input", icon: RiText }, { keys: [`${mod} + I`], description: "Focus Chat Input", icon: RiText },
@@ -172,8 +184,8 @@ export const HelpDialog: React.FC = () => {
]; ];
return ( return (
<Dialog open={isHelpDialogOpen} onOpenChange={setHelpDialogOpen}> <Dialog open={isHelpDialogOpen} onOpenChange={setHelpDialogOpen}>
<DialogContent className="max-w-2xl"> <DialogContent className="max-w-2xl w-[min(42rem,calc(100vw-1.5rem))] max-h-[calc(100dvh-2rem)] flex flex-col overflow-hidden">
<DialogHeader> <DialogHeader>
<DialogTitle className="flex items-center gap-2"> <DialogTitle className="flex items-center gap-2">
<RiSettings3Line className="h-5 w-5" /> <RiSettings3Line className="h-5 w-5" />
@@ -184,67 +196,69 @@ export const HelpDialog: React.FC = () => {
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-4 mt-3"> <div className="flex-1 overflow-y-auto mt-3 pr-1">
{shortcuts.map((section) => ( <div className="space-y-4">
<div key={section.category}> {shortcuts.map((section) => (
<h3 className="typography-meta font-semibold text-muted-foreground uppercase tracking-wider mb-2"> <div key={section.category}>
{section.category} <h3 className="typography-meta font-semibold text-muted-foreground uppercase tracking-wider mb-2">
</h3> {section.category}
<div className="space-y-1"> </h3>
{section.items.map((shortcut, index) => ( <div className="space-y-1">
<div {section.items.map((shortcut, index) => (
key={index} <div
className="flex items-center justify-between py-1 px-2" key={index}
> className="flex items-center justify-between py-1 px-2"
<div className="flex items-center gap-2"> >
{shortcut.icon && ( <div className="flex items-center gap-2">
<shortcut.icon className="h-3.5 w-3.5 text-muted-foreground" /> {shortcut.icon && (
)} <shortcut.icon className="h-3.5 w-3.5 text-muted-foreground" />
<span className="typography-meta"> )}
{shortcut.description} <span className="typography-meta">
</span> {shortcut.description}
</span>
</div>
<div className="flex items-center gap-1">
{(Array.isArray(shortcut.keys)
? shortcut.keys
: shortcut.keys.split(" / ")
).map((keyCombo: string, i: number) => (
<React.Fragment key={`${keyCombo}-${i}`}>
{i > 0 && (
<span className="typography-meta text-muted-foreground mx-1">
or
</span>
)}
<kbd className="inline-flex items-center gap-1 px-1.5 py-0.5 typography-meta font-mono bg-muted rounded border border-border/20">
{renderKeyCombo(keyCombo)}
</kbd>
</React.Fragment>
))}
</div>
</div> </div>
<div className="flex items-center gap-1"> ))}
{(Array.isArray(shortcut.keys) </div>
? shortcut.keys
: shortcut.keys.split(" / ")
).map((keyCombo: string, i: number) => (
<React.Fragment key={`${keyCombo}-${i}`}>
{i > 0 && (
<span className="typography-meta text-muted-foreground mx-1">
or
</span>
)}
<kbd className="inline-flex items-center gap-1 px-1.5 py-0.5 typography-meta font-mono bg-muted rounded border border-border/20">
{renderKeyCombo(keyCombo)}
</kbd>
</React.Fragment>
))}
</div>
</div>
))}
</div> </div>
</div> ))}
))} </div>
</div>
<div className="mt-4 p-2 bg-muted/30 rounded-xl"> <div className="mt-4 p-2 bg-muted/30 rounded-xl">
<div className="flex items-start gap-2"> <div className="flex items-start gap-2">
<RiQuestionLine className="h-3.5 w-3.5 text-muted-foreground mt-0.5" /> <RiQuestionLine className="h-3.5 w-3.5 text-muted-foreground mt-0.5" />
<div className="typography-meta text-muted-foreground"> <div className="typography-meta text-muted-foreground">
<p className="font-medium mb-1">Pro Tips:</p> <p className="font-medium mb-1">Pro Tips:</p>
<ul className="space-y-0.5 typography-meta"> <ul className="space-y-0.5 typography-meta">
<li> <li>
• Use Command Palette ({mod} + K) to quickly access all • Use Command Palette ({mod} + K) to quickly access all
actions actions
</li> </li>
<li> <li>
• The 5 most recent sessions appear in the Command Palette • The 5 most recent sessions appear in the Command Palette
</li> </li>
<li> <li>
• Theme cycling remembers your preference across sessions • Theme cycling remembers your preference across sessions
</li> </li>
</ul> </ul>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -171,6 +171,51 @@ export const useKeyboardShortcuts = () => {
return; return;
} }
// Cmd/Ctrl+Shift+T: Cycle thinking variant (same gating as Shift+M)
if (hasModifier(e) && e.shiftKey && e.key.toLowerCase() === 't') {
const {
isSettingsDialogOpen,
isCommandPaletteOpen,
isHelpDialogOpen,
isSessionSwitcherOpen,
isAboutDialogOpen,
activeMainTab,
} = useUIStore.getState();
if (isSettingsDialogOpen) {
return;
}
const hasOverlay = isCommandPaletteOpen || isHelpDialogOpen || isSessionSwitcherOpen || isAboutDialogOpen;
const isChatActive = activeMainTab === 'chat';
if (hasOverlay || !isChatActive) {
return;
}
const configState = useConfigStore.getState();
const variants = configState.getCurrentModelVariants();
if (variants.length === 0) {
return;
}
e.preventDefault();
configState.cycleCurrentVariant();
const nextVariant = useConfigStore.getState().currentVariant;
const sessionState = useSessionStore.getState();
const sessionId = sessionState.currentSessionId;
const agentName = useConfigStore.getState().currentAgentName;
const providerId = useConfigStore.getState().currentProviderId;
const modelId = useConfigStore.getState().currentModelId;
if (sessionId && agentName && providerId && modelId) {
sessionState.saveAgentModelVariantForSession(sessionId, agentName, providerId, modelId, nextVariant);
}
return;
}
if (e.key === 'Escape') { if (e.key === 'Escape') {
const { const {
isSettingsDialogOpen, isSettingsDialogOpen,