Skip to content

Keep one Back guard per open mobile layer - #442

Open
IvoryHeart wants to merge 1 commit into
powerfooI:mainfrom
IvoryHeart:mobile-back-guards
Open

IvoryHeart wants to merge 1 commit into
powerfooI:mainfrom
IvoryHeart:mobile-back-guards

Conversation

@IvoryHeart

Copy link
Copy Markdown
Contributor

Summary

Follow-up to #433. With two mobile layers open (for example the input dock and the tab sheet), the phone's Back closes the first one correctly. In Chrome on Android, the second Back then leaves Roamgate instead of closing the remaining layer.

The cause: after Back closes the top layer, reconcile re-pushes the shared guard while the popstate is being handled. Chrome's back-button intervention skips history entries that a page adds without a user gesture. It flags the base entry that pushed the guard, so the next Back skips past the page. Tests drive Back with history.back(), where the intervention does not apply, so the existing suite passes.

The change:

  • Each open layer has its own guard entry. The guards are pushed when the layer opens, which follows the tap that opened it, and are numbered by depth (roamgateBack: n).
  • The user's Back consumes one guard and closes the topmost layer. The remaining layers still have their guards, so handling Back never pushes an entry.
  • Layers closed with their on-screen controls take their guards with them, one history.back() at a time, as before. The next Back still leaves the page in one press.
  • Pairing with the base entry, crossing an empty guard with Forward, and the pagehide/pageshow handling are unchanged. A guard from before this change (roamgateBack: true) still reads as depth 1.

Tests

  • web/src/systemBack.test.ts: a new case opens two layers, presses Back twice, and asserts that each press closes one layer and that pushState is never called while Back is handled. It fails on main (one push) and passes here. All existing cases pass unchanged.
  • web/src/components/ThemedSelect.test.ts: its simulated Back used to land directly on the unguarded base, which modelled a single guard. It now pops one guard per press, like a browser does with one guard per layer.

Validation

  • bun run format:check, bun run lint and bun run typecheck:quick are clean.
  • bun run test:quick: everything passes except three cases in server/src/http/auth-process.test.ts. They fail the same way on a clean main checkout in this environment and are unrelated.
  • bun run build:web: initial JS 803.9/804 KiB, gzip 246.0/256 KiB. Within the current budget, with no bump needed.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DfhSCw8PA5ALciQTEYHAAP

Closing the top layer with the phone's Back re-pushed the shared guard
while handling Back. Chrome skips history entries a page adds without a
user gesture, so the next Back left the page instead of closing the next
layer. Each open layer now has its own guard, pushed when it opens; Back
consumes one per layer and never pushes, and layers closed on screen take
theirs with them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DfhSCw8PA5ALciQTEYHAAP
@github-actions github-actions Bot added the enhancement New feature or request label Oct 17, 2026

This branch has not been deployed

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

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant