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 <noreply@anthropic.com>
This commit is contained in:
Test
2026-03-03 14:30:12 +01:00
parent 10e6d7b366
commit 1ec27dd264
2 changed files with 34 additions and 3 deletions

View File

@@ -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(<AiPanel onClose={vi.fn()} vaultPath="/tmp/vault" />)
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(<AiPanel onClose={onClose} vaultPath="/tmp/vault" />)
const input = screen.getByTestId('agent-input')
fireEvent.keyDown(input, { key: 'Escape' })
expect(onClose).toHaveBeenCalledOnce()
})
})

View File

@@ -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<HTMLInputElement | null>
}) {
const sendDisabled = isActive || !input.trim()
return (
@@ -116,6 +116,7 @@ function InputBar({ input, onInputChange, onSend, onKeyDown, isActive, hasContex
>
<div className="flex items-end gap-2">
<input
ref={inputRef}
value={input}
onChange={e => 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<HTMLInputElement>(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}
/>
</aside>
)