131 lines
4.2 KiB
TypeScript
131 lines
4.2 KiB
TypeScript
import { useEffect, useRef } from 'react'
|
|
|
|
import { useI18n } from '@/i18n'
|
|
import {
|
|
initQuickEntryBridge,
|
|
QUICK_TARGET_CURRENT,
|
|
QUICK_TARGET_NEW,
|
|
type QuickEntrySessionOption,
|
|
setQuickEntrySubmitHandler
|
|
} from '@/store/quick-entry'
|
|
import { $gatewayState, $sessions } from '@/store/session'
|
|
import { sessionTileDelegate } from '@/store/session-states'
|
|
import { isAuxiliaryWindow } from '@/store/windows'
|
|
|
|
interface QuickEntryBridgeParams {
|
|
startFreshSessionDraft: () => void
|
|
submitText: (text: string) => Promise<unknown> | unknown
|
|
}
|
|
|
|
// The picker is a capture aid, not a session browser — a handful of recent
|
|
// rows is the whole point.
|
|
const QUICK_ENTRY_SESSION_OPTIONS = 5
|
|
|
|
function sessionOptions(): QuickEntrySessionOption[] {
|
|
return $sessions
|
|
.get()
|
|
.filter(session => !session.archived)
|
|
.slice(0, QUICK_ENTRY_SESSION_OPTIONS)
|
|
.map(session => ({
|
|
id: session.id,
|
|
title: session.title?.trim() || session.preview?.trim() || session.id
|
|
}))
|
|
}
|
|
|
|
/**
|
|
* Wires the global-hotkey Quick Entry window back into the app, both ways:
|
|
*
|
|
* - **Inbound:** text captured there is routed by target and submitted through
|
|
* THIS window's normal prompt machinery — current chat rides `submitText`, a
|
|
* picked stored session rides the session-tile delegate (resume + submit,
|
|
* background, without touching the primary view — the same path tiled
|
|
* sessions use), and "new session" is a fresh draft + submit, exactly what
|
|
* clicking New Chat and typing does. One submit pipeline, no bespoke RPC.
|
|
* - **Outbound:** gateway connection state + the recent-session list are pushed
|
|
* to the quick window (via main, which caches the latest push), so its input
|
|
* disables with a reconnect hint whenever the backend is unreachable.
|
|
*
|
|
* Handlers register ONCE through refs tracking the latest callbacks —
|
|
* re-registering on identity churn leaves a nulled-handler window that can drop
|
|
* a submit (the same bug shape use-pet-bridge guards). Primary window only: a
|
|
* secondary session window must not also claim the global capture channel, or
|
|
* one keystroke would send N prompts.
|
|
*/
|
|
export function useQuickEntryBridge({ startFreshSessionDraft, submitText }: QuickEntryBridgeParams): void {
|
|
const { locale } = useI18n()
|
|
const submitTextRef = useRef(submitText)
|
|
submitTextRef.current = submitText
|
|
const startFreshRef = useRef(startFreshSessionDraft)
|
|
startFreshRef.current = startFreshSessionDraft
|
|
|
|
useEffect(() => {
|
|
if (isAuxiliaryWindow()) {
|
|
return
|
|
}
|
|
|
|
setQuickEntrySubmitHandler(({ target, text }) => {
|
|
if (target === QUICK_TARGET_NEW) {
|
|
// Same as the user clicking New Chat and typing: fresh draft, then the
|
|
// normal submit creates the backend session.
|
|
startFreshRef.current()
|
|
void submitTextRef.current(text)
|
|
|
|
return
|
|
}
|
|
|
|
if (target !== QUICK_TARGET_CURRENT) {
|
|
// A picked stored session: resume + submit in the background through
|
|
// the session-tile delegate so the primary view stays where it is.
|
|
const delegate = sessionTileDelegate()
|
|
|
|
if (delegate) {
|
|
void delegate
|
|
.resumeTile(target)
|
|
.then(runtimeId => delegate.submitToSession(runtimeId, text))
|
|
// A dead/undeliverable target must not swallow the prompt.
|
|
.catch(() => void submitTextRef.current(text))
|
|
|
|
return
|
|
}
|
|
}
|
|
|
|
void submitTextRef.current(text)
|
|
})
|
|
|
|
const dispose = initQuickEntryBridge()
|
|
|
|
return () => {
|
|
setQuickEntrySubmitHandler(null)
|
|
dispose()
|
|
}
|
|
}, [])
|
|
|
|
// Push gateway truth into the quick window whenever it changes: connection
|
|
// state gates its input; the recent-session list feeds its target picker.
|
|
useEffect(() => {
|
|
if (isAuxiliaryWindow()) {
|
|
return
|
|
}
|
|
|
|
const api = window.hermesDesktop?.quickEntry
|
|
|
|
if (!api?.pushState) {
|
|
return
|
|
}
|
|
|
|
const push = () => {
|
|
api.pushState({ connected: $gatewayState.get() === 'open', locale, sessions: sessionOptions() })
|
|
}
|
|
|
|
push()
|
|
|
|
const offGateway = $gatewayState.listen(push)
|
|
const offSessions = $sessions.listen(push)
|
|
|
|
return () => {
|
|
offGateway()
|
|
offSessions()
|
|
}
|
|
}, [locale])
|
|
}
|