From 1ec27dd2640851b062a2b4fc0f32f4d937cf30c7 Mon Sep 17 00:00:00 2001 From: Test Date: Tue, 3 Mar 2026 14:30:12 +0100 Subject: [PATCH] fix: auto-focus AI Chat input on panel open and close on Escape When AI Chat panel mounts (via Cmd+I), the input field now receives focus automatically so users can type immediately without clicking. Pressing Escape in the input closes the panel. Co-Authored-By: Claude Opus 4.6 --- src/components/AiPanel.test.tsx | 19 ++++++++++++++++++- src/components/AiPanel.tsx | 18 ++++++++++++++++-- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/components/AiPanel.test.tsx b/src/components/AiPanel.test.tsx index ebc752c0..18e08fb4 100644 --- a/src/components/AiPanel.test.tsx +++ b/src/components/AiPanel.test.tsx @@ -1,5 +1,5 @@ import { describe, it, expect, vi } from 'vitest' -import { render, screen, fireEvent } from '@testing-library/react' +import { render, screen, fireEvent, act } from '@testing-library/react' import { AiPanel } from './AiPanel' import type { VaultEntry } from '../types' @@ -133,4 +133,21 @@ describe('AiPanel', () => { const input = screen.getByTestId('agent-input') as HTMLInputElement expect(input.placeholder).toBe('Ask the AI agent...') }) + + it('auto-focuses input on mount', async () => { + vi.useFakeTimers() + render() + await act(() => { vi.advanceTimersByTime(1) }) + const input = screen.getByTestId('agent-input') + expect(document.activeElement).toBe(input) + vi.useRealTimers() + }) + + it('calls onClose when Escape is pressed in input', () => { + const onClose = vi.fn() + render() + const input = screen.getByTestId('agent-input') + fireEvent.keyDown(input, { key: 'Escape' }) + expect(onClose).toHaveBeenCalledOnce() + }) }) diff --git a/src/components/AiPanel.tsx b/src/components/AiPanel.tsx index 58186311..b34e1e60 100644 --- a/src/components/AiPanel.tsx +++ b/src/components/AiPanel.tsx @@ -103,10 +103,10 @@ function MessageHistory({ messages, isActive, onOpenNote, hasContext }: { ) } -function InputBar({ input, onInputChange, onSend, onKeyDown, isActive, hasContext }: { +function InputBar({ input, onInputChange, onSend, onKeyDown, isActive, hasContext, inputRef }: { input: string; onInputChange: (v: string) => void onSend: () => void; onKeyDown: (e: React.KeyboardEvent) => void - isActive: boolean; hasContext: boolean + isActive: boolean; hasContext: boolean; inputRef: React.RefObject }) { const sendDisabled = isActive || !input.trim() return ( @@ -116,6 +116,7 @@ function InputBar({ input, onInputChange, onSend, onKeyDown, isActive, hasContex >
onInputChange(e.target.value)} onKeyDown={onKeyDown} @@ -150,6 +151,13 @@ function InputBar({ input, onInputChange, onSend, onKeyDown, isActive, hasContex export function AiPanel({ onClose, onOpenNote, vaultPath, activeEntry, entries, allContent }: AiPanelProps) { const [input, setInput] = useState('') + const inputRef = useRef(null) + + useEffect(() => { + // Small delay to ensure DOM is ready after panel mount + const timer = setTimeout(() => inputRef.current?.focus(), 0) + return () => clearTimeout(timer) + }, []) const linkedEntries = useMemo(() => { if (!activeEntry || !entries) return [] @@ -173,6 +181,11 @@ export function AiPanel({ onClose, onOpenNote, vaultPath, activeEntry, entries, } const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Escape') { + e.preventDefault() + onClose() + return + } if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault() handleSend() @@ -201,6 +214,7 @@ export function AiPanel({ onClose, onOpenNote, vaultPath, activeEntry, entries, onKeyDown={handleKeyDown} isActive={isActive} hasContext={hasContext} + inputRef={inputRef} /> )