diff --git a/docs/keybinds.md b/docs/keybinds.md index 8991fed6b0..49bb5c52a9 100644 --- a/docs/keybinds.md +++ b/docs/keybinds.md @@ -24,6 +24,7 @@ When documentation shows `Ctrl`, it means: | Action | Shortcut | | ---------------------- | ------------- | +| Focus chat input | `a` or `i` | | Send message | `Enter` | | New line in message | `Shift+Enter` | | Jump to bottom of chat | `Shift+G` | diff --git a/src/components/ChatInput.tsx b/src/components/ChatInput.tsx index d97fe97572..5d585b30cf 100644 --- a/src/components/ChatInput.tsx +++ b/src/components/ChatInput.tsx @@ -18,7 +18,7 @@ import { type SlashSuggestion, } from "@/utils/slashCommands/suggestions"; import { TooltipWrapper, Tooltip, HelpIndicator } from "./Tooltip"; -import { matchesKeybind, formatKeybind, KEYBINDS } from "@/utils/ui/keybinds"; +import { matchesKeybind, formatKeybind, KEYBINDS, isEditableElement } from "@/utils/ui/keybinds"; import { defaultModel } from "@/utils/ai/models"; import { ModelSelector, type ModelSelectorRef } from "./ModelSelector"; import { useModelLRU } from "@/hooks/useModelLRU"; @@ -332,6 +332,47 @@ export const ChatInput: React.FC = ({ const [mode, setMode] = useMode(); const { recentModels } = useModelLRU(); + const focusMessageInput = useCallback(() => { + const element = inputRef.current; + if (!element || element.disabled) { + return; + } + + element.focus(); + + requestAnimationFrame(() => { + const cursor = element.value.length; + element.selectionStart = cursor; + element.selectionEnd = cursor; + element.style.height = "auto"; + element.style.height = Math.min(element.scrollHeight, 200) + "px"; + }); + }, []); + + useEffect(() => { + const handleGlobalKeyDown = (event: KeyboardEvent) => { + if (isEditableElement(event.target)) { + return; + } + + if (matchesKeybind(event, KEYBINDS.FOCUS_INPUT_I)) { + event.preventDefault(); + focusMessageInput(); + return; + } + + if (matchesKeybind(event, KEYBINDS.FOCUS_INPUT_A)) { + event.preventDefault(); + focusMessageInput(); + } + }; + + window.addEventListener("keydown", handleGlobalKeyDown); + return () => { + window.removeEventListener("keydown", handleGlobalKeyDown); + }; + }, [focusMessageInput]); + // When entering editing mode, populate input with message content useEffect(() => { if (editingMessage) { @@ -593,19 +634,19 @@ export const ChatInput: React.FC = ({ // Handle cancel/escape if (matchesKeybind(e, KEYBINDS.CANCEL)) { + const isFocused = document.activeElement === inputRef.current; e.preventDefault(); // Priority 1: Cancel editing if in edit mode if (editingMessage && onCancelEdit) { onCancelEdit(); - return; + } else if (canInterrupt) { + // Priority 2: Interrupt streaming if active + void window.api.workspace.sendMessage(workspaceId, ""); } - // Priority 2: Interrupt streaming if active - if (canInterrupt) { - // Send empty message to trigger interrupt - void window.api.workspace.sendMessage(workspaceId, ""); - return; + if (isFocused) { + inputRef.current?.blur(); } return; diff --git a/src/utils/ui/keybinds.ts b/src/utils/ui/keybinds.ts index 62ffa020c6..05ba056b09 100644 --- a/src/utils/ui/keybinds.ts +++ b/src/utils/ui/keybinds.ts @@ -124,6 +124,12 @@ export const KEYBINDS = { /** Cancel current action / Close modal / Interrupt streaming */ CANCEL: { key: "Escape" }, + /** Focus chat input */ + FOCUS_INPUT_I: { key: "i" }, + + /** Focus chat input (alternate) */ + FOCUS_INPUT_A: { key: "a" }, + /** Create new workspace for current project */ NEW_WORKSPACE: { key: "n", ctrl: true },