import { useEffect, useReducer, useRef } from 'react' import { normalizeLocale, useI18n } from '@/i18n' import { initialQuickComposerState, QUICK_TARGET_CURRENT, QUICK_TARGET_NEW, type QuickComposerEvent, quickComposerReducer, type QuickComposerState } from '@/store/quick-entry' /** * The Quick Entry composer — the whole renderer surface of the global-hotkey * mini window. Deliberately one input plus a session-target picker and nothing * else: this is a capture surface, not a second chat. * * All behavior rides `quickComposerReducer` (pure, unit-tested): submit sends * the trimmed text + target through the shell and asks to hide; an empty submit * does neither so a stray Enter can't make the window vanish; Escape and losing * focus dismiss without sending; a dead gateway disables the input entirely * (the reducer refuses the send AND the input paints the reconnect hint). * * The window itself has no gateway connection. Its view of backend truth — is * the gateway up, which recent sessions exist — is pushed in by the primary * renderer through main (`onState`), and its text goes back the same road to * the primary renderer's normal prompt-submit path. */ export function QuickEntryApp() { const { t, setLocale } = useI18n() const copy = t.surfaces.quickEntry const inputRef = useRef(null) // The reducer returns { send, state }; this wrapper performs the side effect // (hand the payload to the shell, ask to hide) and stores the next state, so // the decision stays pure and testable while the effects stay in one place. const [state, dispatch] = useReducer((current: QuickComposerState, event: QuickComposerEvent) => { const { send, state: next } = quickComposerReducer(current, event) const api = window.hermesDesktop?.quickEntry if (send) { api?.submit(send) } else if (!next.visible && current.visible) { api?.dismiss() } return next }, initialQuickComposerState) // Re-summoned by the chord: the shell reuses the window, so reset the draft // and take the keyboard back for a fresh capture. Also adopt gateway-state // pushes (connection + recent sessions) relayed from the primary renderer. useEffect(() => { const api = window.hermesDesktop?.quickEntry const offShown = api?.onShown(() => { dispatch({ type: 'shown' }) requestAnimationFrame(() => inputRef.current?.focus()) }) const offState = api?.onState(payload => { void setLocale(normalizeLocale(payload?.locale)) dispatch({ connected: payload?.connected === true, sessions: Array.isArray(payload?.sessions) ? payload.sessions : [], type: 'state' }) }) inputRef.current?.focus() return () => { offShown?.() offState?.() } }, [setLocale]) return (
› { // Moving focus to the target picker is not leaving the window. if (!event.relatedTarget) { dispatch({ type: 'blur' }) } }} onChange={event => dispatch({ draft: event.target.value, type: 'edit' })} onKeyDown={event => { if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault() dispatch({ type: 'submit' }) } else if (event.key === 'Escape') { event.preventDefault() dispatch({ type: 'dismiss' }) } }} placeholder={state.connected ? copy.ask : copy.disconnected} ref={inputRef} spellCheck={false} style={{ background: 'transparent', border: 'none', color: 'var(--foreground, #eee)', flex: 1, fontFamily: 'inherit', fontSize: 15, minWidth: 0, opacity: state.connected ? 1 : 0.55, outline: 'none' }} value={state.draft} />
) }