feat: add keyboard shortcuts for cycling agent and thinking variant
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user