Skip to content

Commit 6521a03

Browse files
authored
feat: add global focus shortcut for chat input (#67)
1 parent e45ea88 commit 6521a03

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)