From a6aada24f5162ae8bfa63b87bdcd86e588c32278 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Fri, 26 Jun 2026 22:38:21 -0500 Subject: [PATCH] fix(desktop): handle wake.detected on the canonical event pipeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The GUI armed the detector (wake.start) and the gateway fired wake.detected, but the desktop never reacted: detection was wired through a side-registered gatewayRef.current.on('wake.detected', …) listener that was instance/timing-fragile (and silently dead across reconnects/HMR), even though the raw events were arriving on the socket. Route wake.detected through handleGatewayEventWithWake — the same onEvent pipeline every gateway socket already feeds via useGatewayBoot — and open a fresh session + start back-and-forth voice there. Drop the separate .on() listener; the open-effect now only arms wake.start. --- apps/desktop/src/app/desktop-controller.tsx | 33 ++++++++++++--------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/apps/desktop/src/app/desktop-controller.tsx b/apps/desktop/src/app/desktop-controller.tsx index 08e27c0cf22..c6d9086608b 100644 --- a/apps/desktop/src/app/desktop-controller.tsx +++ b/apps/desktop/src/app/desktop-controller.tsx @@ -739,6 +739,21 @@ export function DesktopController() { updateSessionState }) + // "Hey Hermes": handle the wake event on the canonical onEvent pipeline (the + // path every gateway socket already feeds), not a side-registered listener — + // open a fresh session and begin back-and-forth voice. + const handleGatewayEventWithWake = useCallback( + (event: Parameters[0]) => { + if (event.type === 'wake.detected') { + startFreshSessionDraft() + requestVoiceStart() + return + } + handleDesktopGatewayEvent(event) + }, + [handleDesktopGatewayEvent, startFreshSessionDraft] + ) + // Single global listener for every rebindable hotkey (incl. profile switching) // plus the on-screen keybind editor's capture mode. useKeybinds({ @@ -987,7 +1002,7 @@ export function DesktopController() { }, []) useGatewayBoot({ - handleGatewayEvent: handleDesktopGatewayEvent, + handleGatewayEvent: handleGatewayEventWithWake, onConnectionReady: c => { connectionRef.current = c }, @@ -1007,24 +1022,14 @@ export function DesktopController() { }, [gatewayState, refreshCurrentModel, refreshSessions]) // "Hey Hermes" wake word: arm the server-side detector for this surface - // (gated on config) and open a fresh session when it fires. Idempotent and - // self-cleaning across reconnects. + // (gated on config). Detection arrives as a wake.detected event handled in + // handleGatewayEventWithWake. Idempotent server-side, so reconnects are safe. useEffect(() => { if (gatewayState !== 'open') { return } void requestGateway('wake.start', { surface: 'gui' }).catch(() => undefined) - const gw = gatewayRef.current - if (!gw) { - return - } - return gw.on('wake.detected', () => { - startFreshSessionDraft() - // Begin hands-free back-and-forth voice in the fresh session. The bus - // defers a tick, so the new composer is mounted before voice starts. - requestVoiceStart() - }) - }, [gatewayState, requestGateway, gatewayRef, startFreshSessionDraft]) + }, [gatewayState, requestGateway]) // Keep the cron jobs section live without a user action: the scheduler ticks // in the background (advancing next-run/state and creating runs), so poll the