fix(x): one hover companion — delete the summoned ask bar, no flash, no dead summons - #878
Conversation
…ne flow for every entry point The ⌥⇧Space hover mode (quick access) had three real bugs: 1. The first summon created the companion window and show()ed it while the page was still loading: index.html painted a theme slab, then the renderer's default summoned layout (the old text bar), then the Skipper. Re-summons flashed the summoned layout for a frame too (the hidden window keeps it, and show() fired before the re-render). 2. startHoverCall held its in-flight guard across the sticky screen share; getDisplayMedia hangs forever when Screen Recording is denied, so after the first summon every later chord was a silent no-op. 3. QuickAskBar called useMemo after conditional early returns — a pill ⇄ card switch (camera toggle) would throw React's hook-count error and blank the window. Fixes: - Reveal protocol: quick-ask:mode carries a seq, the renderer acks quickAsk:modeApplied two frames after committing that role, and main orders the window in at opacity 0 and only makes it opaque / focuses on the ack. Fold pushes first and shrinks on the ack. The renderer paints nothing until its role is known; index.html gives #quick-ask and #screen-pointer a transparent background from the very first paint. - startScreenShare is time-boxed (10s) and fails cleanly; the hover guard is released right after startCall and the share is fire-and-forget; a session that fails to start falls back to the text card. - ONE hover flow: chord, tray, tuck handle, toast "Try it", composer call button and the "Share screen" preset all end in startHoverCall(). A summon with no (or a still-loading) app window recreates it hidden and re-relays on a quickAsk:appReady handshake; closing the app window mid-call unpins the companion. Call-state cache survives fullscreen ⇄ popout (camera calls return as the pill) and is cleared by an explicit idle push at call end. Hooks hoisted above the early returns. - [companion] mode-transition breadcrumbs in main; VIDEO_MODE.md updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ead of answering with the text card The remaining "old quick access comes up instead of hover mode" case: voiceAvailable/ttsAvailable start false and are filled by an async probe (voice:getConfig + oauth:getState) at mount. A chord in the first seconds after reopening the app read that not-yet-resolved false as "no voice configured" and answered with quickAsk:show — the old text card, with no Skipper ever arriving. Reproduced deterministically by delaying the probe: main logged `hidden → summoned (text card (fresh window))`. With this change the same run logs `hidden → pinned (call surface (summoned))`. - App: mirror the probe result into refs (written inside the probe, so a waiter doesn't depend on a re-render), keep the in-flight promise, and have startHoverCall await it (capped at 4s, starting one if nothing has probed yet) before deciding. The in-flight guard now covers that wait, and a call started meanwhile just re-asserts its floating surface. A genuinely unconfigured voice still falls back to the text card. - Bar: the tuck handle is optimistic until its own probe answers — the app window owns the authoritative check and its own fallback, so an early click relays instead of dying as a dimmed no-op. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Follow-up (38ff97d): found one more path to the old text card, the one that matches "reopen the app, then press the shortcut".
Reproduced deterministically by delaying the probe: main logged |
… surface The old Spotlight-style quick-ask bar is gone, not just avoided. It was only ever a fallback surface, and every "hover mode is glitchy" report was that bar appearing where the Skipper belonged. With one role left there is no second layout to flash, race, or get stuck in. Removed: - main: the `summoned` mode, its geometry (positionSummoned, FRAME_*, BOTTOM_MARGIN), showSummonedCard/showQuickAsk/hideQuickAsk, the blur-to-dismiss handler, and the trigger enum. `toggleQuickAsk()` now has exactly one outcome: fold/unfold the Skipper's text when it's up, else relay a summon. An unanswered relay is re-sent once by a watchdog and then logged — never answered with a different surface. - IPC: quickAsk:show / quickAsk:hide / quickAsk:stop + quick-ask:stop / quickAsk:state + quick-ask:state / quickAsk:setOptions + quick-ask:set-options / quickAsk:optionsState + quick-ask:options-state / quick-ask:summoned. CompanionMode is now 'hidden' | 'pinned'. - renderer: the bar layout, its answer panel, local dictation and the ⌥⇧Space hold-to-talk chord, the voice-out and share-without-a-call toggles, dismiss/stage-click-to-dismiss, and the tuck handle's start-a-call role. The window paints NOTHING unless it is pinned. - App: quickAskActiveRef / quickAskOptionsRef / quickAskStartedAtRef / speakTurnRef and the answer-mirror effect. Speech now follows the question's modality alone. A summon that can't become a session (voice unconfigured, or the engine failed to start) is explained in the APP window — brought to the front with a toast that opens Settings — instead of silently becoming a different floating thing. Verified on the dev build: earliest-possible summon after an app start lands as the Skipper (no other layout at any frame), fold/unfold/end/ re-summon cycle clean, hidden window renders 0 bytes, and the no-voice path shows the toast with the companion staying hidden. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
5eeb56c — the old bar is deleted, not just avoided. Per Arjun: it wasn't used anywhere, so the Removed: main's summoned mode + geometry +
Verified on the dev build: earliest-possible summon after an app start lands as the Skipper with no other layout at any frame; fold → 250px mascot, unfold → card, end → hidden (0 bytes), re-summon → Skipper; no-voice path shows the toast with the companion staying hidden. |
Comment- and copy-only follow-up to 5eeb56c: several comments still described the summoned bar's status line, voice toggle, destination chip and geometry, and two user-facing strings still called the feature "Quick Ask". Nothing behavioural. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Summary
The ⌥⇧Space hover mode ("quick access") had three real bugs, found by driving the dev app over CDP and snapshotting the companion window every ~25 ms:
show()ed it while the page was still loading —index.htmlpainted a theme-colored slab, then the renderer's defaultsummonedlayout (the old text bar), then the Skipper. Re-summons flashed the summoned layout for a frame too (the hidden window keeps that layout;show()fired before the re-render).startHoverCallheld its in-flight guard across the sticky screen share, andgetDisplayMediahangs forever when Screen Recording is denied / its prompt is unanswered (the state after every rebuild) — so the guard stayed latched and every later ⌥⇧Space was a silent no-op.QuickAskBarcalleduseMemoafter conditional early returns, so a pill ⇄ card switch (camera toggle mid-call) would throw React's hook-count error and blank the window.Changes
quick-ask.ts⇄quick-ask-bar.tsx,shared/ipc.ts):quick-ask:modecarries aseq; the renderer acksquickAsk:modeAppliedtwo frames after committing that role; main orders the window in at opacity 0 and sets opacity 1 / focuses only on the ack. Fold pushes the layout first and shrinks the window on the ack (no squeezed-card frame). Renderer paints nothing until its role is known;index.htmlgives#quick-ask/#screen-pointera transparent background from the first paint. Timeouts (600 ms / 6 s while loading) keep a wedged renderer from blocking.App.tsx,useVideoMode.ts): guard released right afterstartCall; sticky/sharescreen share is fire-and-forget;startScreenShareis time-boxed (10 s) and fails cleanly with the existing permission dialog; a session that fails to start falls back to the text card (never a silent no-op).startHoverCall(). Tray/toast no longer arm hold-to-talk. A summon with the app window closed or still loading recreates it hidden (initQuickAsk({ ensureAppWindow })) and re-relays on aquickAsk:appReadyhandshake; closing the app window mid-call unpins the companion. Call-state cache survives fullscreen ⇄ popout (camera calls come back as the pill, no card→pill morph) and is cleared by an explicit idle push at call end.QuickAskBar;[companion]mode-transition breadcrumbs in main; settings/toast copy andVIDEO_MODE.mdupdated.Verification
npm run lint,npm run typecheckclean.setOpacityis a no-op on Linux → old behavior there).🤖 Generated with Claude Code