Skip to content

Commit 108152c

Browse files
committed
feat: add global focus shortcut for chat input
Introduce global `a` and `i` keybinds that focus the chat input when no editable element is active. Move focus to the textarea with caret at the end and resize height to match content. Update escape handling to blur the input when interrupting or editing does not consume the shortcut, and document the new shortcuts in the keybinds reference.
1 parent e45ea88 commit 108152c

File tree

3 files changed

+55
-7
lines changed

3 files changed

+55
-7
lines changed

docs/keybinds.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ When documentation shows `Ctrl`, it means:
2424

2525
| Action | Shortcut |
2626
| ---------------------- | ------------- |
27+
| Focus chat input | `a` or `i` |
2728
| Send message | `Enter` |
2829
| New line in message | `Shift+Enter` |
2930
| Jump to bottom of chat | `Shift+G` |

src/components/ChatInput.tsx

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
type SlashSuggestion,
1919
} from "@/utils/slashCommands/suggestions";
2020
import { TooltipWrapper, Tooltip, HelpIndicator } from "./Tooltip";
21-
import { matchesKeybind, formatKeybind, KEYBINDS } from "@/utils/ui/keybinds";
21+
import { matchesKeybind, formatKeybind, KEYBINDS, isEditableElement } from "@/utils/ui/keybinds";
2222
import { defaultModel } from "@/utils/ai/models";
2323
import { ModelSelector, type ModelSelectorRef } from "./ModelSelector";
2424
import { useModelLRU } from "@/hooks/useModelLRU";
@@ -332,6 +332,47 @@ export const ChatInput: React.FC<ChatInputProps> = ({
332332
const [mode, setMode] = useMode();
333333
const { recentModels } = useModelLRU();
334334

335+
const focusMessageInput = useCallback(() => {
336+
const element = inputRef.current;
337+
if (!element || element.disabled) {
338+
return;
339+
}
340+
341+
element.focus();
342+
343+
requestAnimationFrame(() => {
344+
const cursor = element.value.length;
345+
element.selectionStart = cursor;
346+
element.selectionEnd = cursor;
347+
element.style.height = "auto";
348+
element.style.height = Math.min(element.scrollHeight, 200) + "px";
349+
});
350+
}, []);
351+
352+
useEffect(() => {
353+
const handleGlobalKeyDown = (event: KeyboardEvent) => {
354+
if (isEditableElement(event.target)) {
355+
return;
356+
}
357+
358+
if (matchesKeybind(event, KEYBINDS.FOCUS_INPUT_I)) {
359+
event.preventDefault();
360+
focusMessageInput();
361+
return;
362+
}
363+
364+
if (matchesKeybind(event, KEYBINDS.FOCUS_INPUT_A)) {
365+
event.preventDefault();
366+
focusMessageInput();
367+
}
368+
};
369+
370+
window.addEventListener("keydown", handleGlobalKeyDown);
371+
return () => {
372+
window.removeEventListener("keydown", handleGlobalKeyDown);
373+
};
374+
}, [focusMessageInput]);
375+
335376
// When entering editing mode, populate input with message content
336377
useEffect(() => {
337378
if (editingMessage) {
@@ -593,19 +634,19 @@ export const ChatInput: React.FC<ChatInputProps> = ({
593634

594635
// Handle cancel/escape
595636
if (matchesKeybind(e, KEYBINDS.CANCEL)) {
637+
const isFocused = document.activeElement === inputRef.current;
596638
e.preventDefault();
597639

598640
// Priority 1: Cancel editing if in edit mode
599641
if (editingMessage && onCancelEdit) {
600642
onCancelEdit();
601-
return;
643+
} else if (canInterrupt) {
644+
// Priority 2: Interrupt streaming if active
645+
void window.api.workspace.sendMessage(workspaceId, "");
602646
}
603647

604-
// Priority 2: Interrupt streaming if active
605-
if (canInterrupt) {
606-
// Send empty message to trigger interrupt
607-
void window.api.workspace.sendMessage(workspaceId, "");
608-
return;
648+
if (isFocused) {
649+
inputRef.current?.blur();
609650
}
610651

611652
return;

src/utils/ui/keybinds.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,12 @@ export const KEYBINDS = {
124124
/** Cancel current action / Close modal / Interrupt streaming */
125125
CANCEL: { key: "Escape" },
126126

127+
/** Focus chat input */
128+
FOCUS_INPUT_I: { key: "i" },
129+
130+
/** Focus chat input (alternate) */
131+
FOCUS_INPUT_A: { key: "a" },
132+
127133
/** Create new workspace for current project */
128134
NEW_WORKSPACE: { key: "n", ctrl: true },
129135

0 commit comments

Comments
 (0)