A reactivated older conversation wasn't bumped to the top / into Today until a
manual refresh when the viewing tab was backgrounded/unfocused: refreshSessionList
early-returns on document.hidden unless force=true, and the sidebar SSE closes on
blur — but the in-flight/pending coalescing only preserved the refresh REASON
string, dropping the opts (force/refreshActive). So a coalesced 'sessions_changed'
refresh lost its force flag and got skipped on a hidden tab. Now the pending
request preserves merged opts (force/refreshActive OR-merged), and a dedicated
resume-refresh path forces a catch-up when the sidebar regains focus, so
cross-device recency re-sorts land without a manual refresh.
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
The #4151 focus hook reopened both global sidebar streams on `focus` with a
bare `startGatewaySSE()`. But `startGatewaySSE()` is NOT idempotent — it begins
with an unconditional `stopGatewaySSE()` (sessions.js:3778), which closes the
live gateway EventSource, cancels its poll fallback (`stopGatewayPollFallback`),
and resets `_gatewayProbeInFlight` / `_gatewaySSEWarningShown`.
So on a transient blur shorter than the 1s debounce — where the blur-close
timer is cleared on the following focus and the stream was never torn down —
the focus handler still dropped and reconnected the gateway on every
window-switch, cancelling its fallback and resetting probe/warning state. That
is the exact thrash the debounce exists to prevent, in the multi-window PWA
scenario this PR targets. (`ensureSessionEventsSSE()` is already idempotent via
`if(_sessionEventsSSE) return`, so the session-events stream was unaffected;
only the gateway churned. greptile P1.)
Guard the reopen on the gateway actually being closed: `if(!_gatewaySSE)
startGatewaySSE()`, mirroring the session-events idempotency. A sustained blur
(timer fires → both streams closed) still reopens the gateway on focus; a
transient blur leaves the still-open gateway untouched. Verified with a
blur/focus/timer harness (transient → no teardown/reconnect; sustained → close
then reopen; CLI-disabled → stays closed) and a new regression guard
`test_focus_reopen_does_not_thrash_the_gateway_stream`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two side-by-side PWA standalone windows saturate the browser's per-origin
HTTP/1.1 connection limit, causing 'Request timed out' toasts to cycle every
~30s. The idle-SSE close added in #3992/#3996 keys off the Page Visibility API
(visibilitychange / document.hidden), but a PWA standalone window does not
reliably fire visibilitychange when it loses focus to another window of the
same app -- document.hidden only flips on minimize. So both windows stay
'visible', each holds its two global sidebar SSE streams (session-events +
gateway) open, 2x3 = 6 hits the connection cap, and subsequent fetch() calls
(the 30s background polls) queue behind the saturated pool and time out.
The two global sidebar streams now also close on a sustained window blur (gated
on document.hasFocus(), the signal visibilitychange misses) and reopen --
catching up the session list -- on focus, via a shared _sidebarSseBackgrounded()
predicate and a debounced _installSidebarSseFocusHook().
The per-session live stream (messages.js startSessionStream) is intentionally
left visibility-only: it carries live bg_task_complete toasts and
server_turn_started live-view that an unfocused-but-visible window must still
receive (the OS-notification path is gated on document.hidden, so the in-app
toast is the only completion signal a visible-unfocused window gets). Closing
it on blur would regress the multi-window live-view UX.
Reported by @ddiall with a thorough root-cause analysis.
Co-authored-by: ddiall <ddiall@users.noreply.github.com>