diff --git a/src/core/mod.rs b/src/core/mod.rs index 248a03b..280209a 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -3587,6 +3587,26 @@ async fn run_core_loop( "Screen share host died (stdout EOF with the share still advertised)", ); current_sharing = None; + // Pull the ticket off presence FIRST, before the reap: if the + // child only closed stdout and lives on, `stop_host` burns the + // full stop grace before the SIGKILL fallback, and for that + // whole window peers would still see (and click Watch on) a + // share whose host is already gone (Gemini S2-merge review, + // P2-1). + if let Some(session) = &mut active_session { + let self_state = presence.to_state( + is_muted.load(Ordering::Relaxed), + net.endpoint.addr(), + None, + ); + let _ = session.room_state.update_self_state(self_state).await; + } + // Stopped next — it clears the UI's sharing state — so the + // local UI also stops saying "sharing" before the reap wait, + // and the error explaining why comes only after, so the user + // is never left looking at a "sharing" UI with an error + // beside it. + let _ = ui_tx.send(UiEvent::ScreenShareStopped).await; let mut unconfirmed = false; if let Some(session) = &mut active_session { // The child is usually already dead, so this confirms the @@ -3598,19 +3618,7 @@ async fn run_core_loop( session.teardown.stop_host().await, Some(teardown::StopOutcome::Unconfirmed) ); - // Pull the ticket off presence so nobody clicks Watch on a - // dead share. - let self_state = presence.to_state( - is_muted.load(Ordering::Relaxed), - net.endpoint.addr(), - None, - ); - let _ = session.room_state.update_self_state(self_state).await; } - // Stopped first — it clears the UI's sharing state — and only - // then the error explaining why, so the user is never left - // looking at a "sharing" UI with an error beside it. - let _ = ui_tx.send(UiEvent::ScreenShareStopped).await; let detail = if unconfirmed { " Its process also couldn't be confirmed dead — check for a stray pixelpass." } else {