Skip to content

fix(app): refresh state when returning from background on mobile - #29987

Closed
CodeDoes wants to merge 1 commit into
anomalyco:devfrom
CodeDoes:dev
Closed

CodeDoes wants to merge 1 commit into
anomalyco:devfrom
CodeDoes:dev

Conversation

@CodeDoes

@CodeDoes CodeDoes commented May 30, 2026 •

Copy link
Copy Markdown

Problem

On mobile browsers (iOS Safari, Chrome Android), when a user switches from the browser to another app, the OS may suspend or freeze the browser tab to conserve memory. When the user returns to the browser:

  1. The tab resumes but has missed server-sent events (SSE) while suspended
  2. The UI displays stale/outdated state (old session status, missing messages, stale project list)
  3. The only recovery is manually reloading the page, which disrupts the user's workflow

This is particularly painful on mobile where tab switching is frequent and users expect apps to stay up-to-date when they return.

Root Cause

The web app maintains a persistent SSE connection to receive real-time updates. When the browser tab is backgrounded:

  • The browser may throttle or suspend JavaScript execution
  • The SSE connection can drop without the client noticing
  • Events fired while suspended are never received
  • The React Query cache has refetchOnWindowFocus: false (set in QueryProvider), so returning to the tab doesn't trigger any refreshes
  • The existing visibilitychange handlers only flush scroll positions or abort stale connections — none refresh data

How to Reproduce

  1. Open OpenCode web on a mobile browser
  2. Start or observe a session
  3. Switch to another app (e.g., messages, email)
  4. Wait 10-30 seconds (long enough for the OS to suspend the tab)
  5. Switch back to the browser
  6. Observe that the session state is stale — new messages may be missing, status may be outdated

Solution

Track tab visibility in ServerSyncContext and trigger a bootstrap refetch when returning from a long suspension.

What Changed

File: packages/app/src/context/server-sync.tsx

Inside the existing onMount() hook that starts the SSE event listener, added:

let hiddenAt = 0
const HIDDEN_THRESHOLD_MS = 2000

const handleVisibility = () => {
  if (document.visibilityState === "hidden") {
    hiddenAt = Date.now()
    return
  }
  if (hiddenAt === 0) return
  const awayTime = Date.now() - hiddenAt
  hiddenAt = 0
  if (awayTime < HIDDEN_THRESHOLD_MS) return
  void bootstrap.refetch()
}

document.addEventListener("visibilitychange", handleVisibility)
onCleanup(() => document.removeEventListener("visibilitychange", handleVisibility))

Why This Approach

  1. Minimal and targeted: Only touches server-sync.tsx, which already owns the bootstrap lifecycle
  2. Uses existing infrastructure: Calls bootstrap.refetch() which already handles refreshing global config, providers, path, and all directory data — no new fetch logic needed
  3. Conservative threshold: The 2-second minimum prevents refreshes on quick tab switches (e.g., checking a notification and immediately returning)
  4. SolidJS lifecycle-safe: Properly registers cleanup via onCleanup() so the listener is removed when the component unmounts
  5. Non-blocking: Uses void so the refetch doesn't block other visibility change handlers

Why Not Other Approaches

  • refetchOnWindowFocus: true in QueryClient: Would trigger on every window focus event, causing excessive refreshes when the user is just alt-tabbing on desktop or tapping between fields on mobile
  • Page reload (location.reload()): Destroys all client-side state (scroll position, input focus, modal state) unnecessarily
  • SSE reconnection only: Would restore the connection but wouldn't fetch missed events; the server doesn't buffer events for disconnected clients
  • Polling: Increases server load and doesn't solve the "missed events while suspended" problem

Trade-offs

  • Extra request on mobile return: One bootstrap.refetch() call after a long suspension. Acceptable given the user just returned to the app and expects fresh data
  • Doesn't distinguish "missed events" from "no events": If no events occurred while suspended, the refetch is redundant but harmless — the cache will be updated with identical data

Testing

Manual test on mobile:

  1. Open OpenCode web on iOS Safari or Chrome Android
  2. Navigate to a session
  3. Switch to another app for 10+ seconds
  4. Return to browser
  5. Verify bootstrap.refetch() fires (can be confirmed in Network tab or by adding a console log)
  6. Verify session state is current

Desktop verification:

  • Switching browser tabs quickly (< 2s) should NOT trigger a refetch
  • Switching away for 5+ seconds should trigger a refetch

Checklist

  • Change is minimal and focused
  • No breaking changes
  • Proper cleanup on unmount
  • Conservative threshold to avoid excessive refreshes

When the browser tab is hidden on mobile (user switches to another app),
the OS may suspend it to save memory. When the user returns, the tab
shows stale state because events were missed while suspended.

Add a visibilitychange listener that tracks when the tab was hidden.
When it becomes visible again after >2 seconds, refetch the bootstrap
data to ensure the UI reflects the latest server state.

Fixes mobile state-loss bug where users had to manually reload
the page after switching back from another app.
@CodeDoes
CodeDoes requested a review from adamdotdevin as a code owner May 30, 2026 11:45
@github-actions github-actions Bot added the needs:compliance This means the issue will auto-close after 2 hours. label May 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

This PR doesn't fully meet our contributing guidelines and PR template.

What needs to be fixed:

  • PR description is missing required template sections. Please use the PR template.

Please edit this PR description to address the above within 2 hours, or it will be automatically closed.

If you believe this was flagged incorrectly, please let a maintainer know.

@github-actions

Copy link
Copy Markdown
Contributor

Thanks for your contribution!

This PR doesn't have a linked issue. All PRs must reference an existing issue.

Please:

  1. Open an issue describing the bug/feature (if one doesn't exist)
  2. Add Fixes #<number> or Closes #<number> to this PR description

See CONTRIBUTING.md for details.

@CodeDoes

Copy link
Copy Markdown
Author

Closing - not ready for review yet, using for CI testing only

@github-actions

Copy link
Copy Markdown
Contributor

This pull request has been automatically closed because it was not updated to meet our contributing guidelines within the 2-hour window.

Feel free to open a new pull request that follows our guidelines.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant