fix(mobile): prevent pushState -> popstate infinite loop in MobileApp URL sync #663

Closed
app-fe wants to merge 2 commits from fix/mobile-chat-max-update-depth into main

2 Commits

Author SHA1 Message Date
7cf4e74dc6 fix(mobile): prevent pushState → popstate infinite loop in MobileApp URL sync
Some checks failed
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 13s
Lint curl status-code capture / Scan workflows for curl status-capture pollution (pull_request) Successful in 13s
Harness Replays / detect-changes (pull_request) Successful in 16s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 13s
E2E API Smoke Test / detect-changes (pull_request) Successful in 44s
gate-check-v3 / gate-check (pull_request) Successful in 25s
CI / Detect changes (pull_request) Successful in 50s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 46s
Runtime PR-Built Compatibility / detect-changes (pull_request) Successful in 36s
qa-review / approved (pull_request) Failing after 17s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 49s
security-review / approved (pull_request) Failing after 11s
sop-tier-check / tier-check (pull_request) Successful in 10s
Harness Replays / Harness Replays (pull_request) Successful in 4s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 8s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 18s
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Successful in 6s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 7s
audit-force-merge / audit (pull_request) Has been skipped
CI / Platform (Go) (pull_request) Failing after 6m33s
CI / Python Lint & Test (pull_request) Successful in 6m59s
CI / Canvas (Next.js) (pull_request) Successful in 7m32s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
CI / all-required (pull_request) Failing after 3s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 7m44s
React error #185 (Maximum update depth exceeded) on mobile when navigating
to ?m=chat&a=... was caused by a render loop in the URL sync effect.

Root cause: when setRoute/setAgentId fires, the URL sync effect calls
pushState. Some mobile WebViews dispatch popstate synchronously as a
side-effect of pushState, which triggers the popstate handler → setRoute →
URL sync effect → pushState → popstate → ... infinite loop.

Fix: track what we just pushed in a prevPushedRef. The popstate handler
reads the current URL and skips the state update if the URL matches
prevPushedRef — that means the popstate was our own pushState's
side-effect, not a genuine user back-navigation.

Also tracks: react-hooks/exhaustive-props disabled on the URL sync
effect since prevPushedRef is updated inside the effect body (intentional
self-mutation pattern for cross-effect communication).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-12 04:32:25 +00:00
c71de18ac1 test(canvas/chat): add AttachmentLightbox coverage (13 cases)
Covers:
- Does not render when open=false
- Renders dialog with role=dialog and aria-modal
- Renders with provided aria-label
- Close button has aria-label="Close preview"
- Click backdrop calls onClose; content click does not
- Escape key calls onClose; other keys do not
- Focus moves to close button when opened
- Focus restores to previous element when closed
- Reduced-motion class on backdrop
- Renders children inside the modal

Per RFC #2991 Phase 2: this is the third-caller justification for
the abstraction (image, PDF, future video-fullscreen all want the
same modal contract).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-12 03:54:01 +00:00