fix(desktop): stop voice status leaking across session switch (#46194)#46279
fix(desktop): stop voice status leaking across session switch (#46194)#46279harjothkhara wants to merge 1 commit into
Conversation
|
✅ Verified — Desktop voice status leaking across session switch Reviewed the diff for correctness of the session-switch detection logic and teardown coverage.
Clean fix with thorough test coverage. No issues found. |
|
@teknium1 — green/mergeable fix for #46194: the desktop composer's voice status (in-flight listening + the global read-aloud |
|
@teknium1 @OutThisLife gentle review ping here. This was opened on June 14, so it is about 6 days old now. Importance: this is a focused desktop session-isolation bug fix. It stops voice status from leaking across a session switch, with dedicated hook coverage, so the UI does not show stale recording/voice state for the wrong conversation. |
2e8105c to
0794709
Compare
|
I opened #61448 to consolidate this with related work across several related issues — it aims to fold in your change alongside the others so the maintainer can land one clean PR. Flagging for visibility; no action needed from you, and your PR stays open. |
…search#46194) The composer's voice state surfaced under the wrong session after a switch: its in-flight voice conversation (listening) and the global read-aloud playback status (`$voicePlayback` → `Preparing audio` / `Speaking`) were not reset, so session B's status area showed session A's carried-over voice state. Handle both switch paths: - Warm switch (composer stays mounted): a colocated hook, useEndVoiceOnSessionSwitch, fires on a real session change and tears the voice state down — it disables the conversation (so the idle effect can't auto-restart listening under the new session) and calls conversation.end() (cancels recording, stops playback). The direct end() is load-bearing for the read-aloud-only case, where flipping voiceConversationActive is a no-op and won't trigger the conversation hook's own enabled→false teardown. - Cold switch (resuming a session with no warm runtime unmounts the composer): the component-local conversation state dies with the unmount, but the module-level read-aloud singleton survives, so useStopVoicePlaybackOnUnmount stops it on teardown. The guard skips the initial mount and the null→id persist of the session already in view (mirrors the composer's placeholder-reset), treating undefined and null as the same "no session"; leaving a session (id→null) and moving between two real sessions both count as a switch. Scope: this fixes the *displayed* voice-status leak only. The queued follow-up state (NousResearch#45414/NousResearch#43959) and transcript path are already keyed per session on main, and the second-hand "wrong chat contents" report is not reproducible there. A separate async leak remains and is left for follow-up (NousResearch#46194 stays open): a dictation or in-flight voice turn whose transcribe/submit resolves after the switch can still land in the newly active session (the stale-submit cross-binding via ensureSessionState → rewriteOptimistic). Tests: use-voice-session-reset.test.ts covers mount / unchanged / switch / null→id / id→null / undefined↔null / latest-callback and the cold-path unmount teardown (renderHook). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
0794709 to
c4157b4
Compare
What does this PR do?
Stops the desktop composer's voice status from leaking across a session switch. After switching sessions, the prior session's in-flight voice conversation (listening) and the global read-aloud playback status (
$voicePlayback→Preparing audio/Speaking) were not reset, so session B's status area showed session A's carried-over voice state.Both switch paths are handled:
useEndVoiceOnSessionSwitch, fires on a real session change and tears the voice state down — it disables the conversation (so the idle effect can't auto-restart listening under the new session) and callsconversation.end()(cancels recording, stops playback). The directend()is load-bearing for the read-aloud-only case, where flippingvoiceConversationActiveis a no-op and won't trigger the conversation hook's ownenabled→falseteardown.useStopVoicePlaybackOnUnmountstops it on teardown.The switch guard skips the initial mount and the
null→idpersist of the session already in view (mirrors the composer's existing placeholder-reset), treatingundefinedandnullas the same "no session". Leaving a session (id→null) and moving between two real sessions both count as a switch.Related Issue
#46194 (scoped — see below; the issue should stay open for the remaining async leak).
This fixes the displayed voice-status slice only. The other symptoms named in the issue are already handled or out of scope here:
main($queuedPromptsBySession, fix(desktop): stop stranding queued prompts across backend bounces #45414/fix(desktop): per-thread composer drafts on decoupled lifecycle (salvage #43660, supersedes #43939) #43959).use-message-stream.ts); the second-hand "both sessions show the same content" report is not reproducible on currentmain.ensureSessionState → rewriteOptimistic). That needs epoch-guarding the async voice continuations and is intentionally left out of this focused fix.Type of Change
Changes Made
apps/desktop/src/app/chat/composer/hooks/use-voice-session-reset.ts—useEndVoiceOnSessionSwitch(warm) +useStopVoicePlaybackOnUnmount(cold).apps/desktop/src/app/chat/composer/index.tsx(theChatBarcomposer).use-voice-session-reset.test.ts— 9 cases.How to Test
npm --workspace apps/desktop run test:ui -- src/app/chat/composer/hooks/use-voice-session-reset.test.tsManual: start a read-aloud (or voice conversation) in session A, switch to session B before it settles — B's composer no longer shows A's
Preparing audio/Speaking/listening state. Repeat with a cold resume of B (no warm runtime) to exercise the unmount path.Checklist
tsc+eslintclean; only the pre-existingtrigger-popover.test.tsx@assistant-ui/tappackaging failure is unrelated)🤖 Generated with Claude Code