Skip to content

feat(shell): act on the Console's ready and routeChanged signals; the shell owns the safe area - #166

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-162-ready-route-safe-area
Nov 2, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-162-ready-route-safe-area

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #162

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:壳的加载遮罩、冷启动时暂存的路由、以及系统轮换后暂存的推送令牌(feat(bridge): stable installId and pushTokenChanged for device registration; document the no-Service-Worker rule #167 的 pushTokenChanged),现在都由同一个加载状态机决定何时放行:等 Console 发出 ready,加载结束后 8 秒超时兜底;WebView 重建后回到的「上次路由」改以 Console 的 routeChanged 为准;文档写明安全区内边距由壳负责。
  • 为什么改:冷启动点通知时,壳在页面加载结束就派发路由,但 Console 的导航监听要等启动请求完成后才挂上,事件没人接,壳只能整页重载。与 feat(bridge): stable installId and pushTokenChanged for device registration; document the no-Service-Worker rule #167 合并后只保留一个「Console 是否就绪」的来源;feat(bridge): stable installId and pushTokenChanged for device registration; document the no-Service-Worker rule #167 原先用 onLoadStart 重新挂起令牌,但 Android 上 SPA 内跳转也会触发它,之后不会再有 ready,令牌会一直扣着。
  • 风险与回滚:旧版 Console 不发 ready,遮罩会在加载结束后多停最多 8 秒,暂存的路由与令牌也在那时才送出;页面自己发起的整页跳转(旧版回退的 location.assign、第三方登录往返)不会重新挂起,期间送出的令牌或路由会随旧页面丢失。回滚即 revert 本 PR,不涉及数据或协议版本。
  • 先看:lib/shell/console-load.ts 的 becomeReady 与 loadEnd / ready / pushToken 分支(:113-175,决定遮罩、路由与令牌何时放行);components/shell/ConsoleWebView.tsx 的 dispatch 与效果执行(:155-191,其中 recreate 效果在 :183 递增 feat(bridge): scan() — native barcode / QR scanner for Console fields #170 的文档计数)、令牌接入(:279-290);docs/HYBRID-SHELL.md 的 Load states 一节。
  • 两分钟验证:npx jest --ci __tests__/lib/shell/console-load.test.ts __tests__/components/ConsoleWebView.test.tsx,预期 2 个套件、61 个用例全部通过。
  • 需要你做的:在开发版真机上冷启动(先杀掉 App)后点一条推送通知,确认直接打开目标记录而页面没有整页重载(没有第二次白屏或加载遮罩)。

What changes

  • One state machine for the cover, the held route, the held push token and the last route (lib/shell/console-load.ts, pure): loading then loaded (WebView load end) then ready (the Console's ready, or READY_TIMEOUT_MS = 8 s after load end), plus failed. ConsoleWebView keeps it in a ref and performs the effects it returns: inject a route, send a push token, arm or clear the ready timer, re-create the WebView.
  • Cover on ready. The cover stays up past load end until ready. A ready that arrives before load end counts. The timeout runs only from load end, and never on a failed load.
  • Held route on ready. A route asked for before ready (a notification tap on cold start) is dispatched once, at ready or at the timeout. Load end no longer dispatches it.
  • Push token (reconciled with feat(bridge): stable installId and pushTokenChanged for device registration; document the no-Service-Worker rule #167). A token the OS reports while the machine is not ready is held, the latest only, and sent on entering ready: by the event or by the timeout. A held token survives a failure and a re-created WebView. The shell's own reload() and a re-created WebView re-arm the hold.
  • Bridge answers (reconciled with feat(bridge): scan() — native barcode / QR scanner for Console fields #170). feat(bridge): scan() — native barcode / QR scanner for Console fields #170's documentRef and the onMessage guard are unchanged: an answer whose WebView was re-created meanwhile is dropped. The counter is now bumped in the machine's recreate effect, the one place a new WebView is created. Nothing tied to onLoadStart bumps it; onLoadStart is still unwired. feat(bridge): scan() — native barcode / QR scanner for Console fields #170's capabilities and scan wiring are unchanged.
  • Last route from routeChanged. The payload's route passes safeConsoleRoute and becomes the last route. After the first one, onNavigationStateChange no longer moves it; before that it is the fallback.
  • Safe area. No behaviour change: the shell keeps padding natively. docs/HYBRID-SHELL.md now says the shell owns the padding and that --os-native-safe-* are informational. It drops the "pad with the safe-area variables" line and the "stopgap" paragraph. Its load-states section and the push-token delivery rule now point at the state machine.
  • Why 8 s: the Console's boot after its document loads is three concurrent round-trips (one capped at about 500 ms) and a locale chunk, then the first render. 8 s covers that on a slow cellular link, and bounds how long an older Console without ready sits behind the cover.

Platform facts this rests on, read at objectstack origin/main 2771575:

  • The Console renders only inside Promise.all([initRuntimeConfig, preflightAuth, seedTenantLanguage]).finally(async …), after await preloadBootstrapLocale (ui/apps/console/src/main.tsx:79-100, render at :168-176). So its navigate listener mounts after the document's load. The card's citation holds.
  • useNativeHostRouting (ui/packages/mobile/src/useNativeHost.ts:112-136) posts ready, posts routeChanged, and adds the objectstack-native:navigate listener in three effects of one component, flushed in the same commit. By the time the shell receives ready, the listener is mounted.
  • routeChanged carries { route }: the router's pathname (basename /_console stripped), search and hash (ui/apps/console/src/App.tsx:155-158). That is the shell's Console-route form.
  • Nothing under ui/ reads --os-native-safe-*. The Console's few env(safe-area-inset-*) uses resolve inside the natively inset WebView frame, so nothing is padded twice.

Tests

  • __tests__/lib/shell/console-load.test.ts (24 cases) covers the machine:
    • ready after load end and before it, the timeout fallback, and a timeout outside loaded;
    • a failed load: the held route survives and becomes the new WebView's start route, and a stray ready lifts nothing;
    • timer clearing, and the newest held route winning;
    • last route from routeChanged over navigation state;
    • push token: the latest held and sent once at ready after the held route, sent at the timeout, sent straight when ready, held again after a reload, and kept through a failure and a re-created WebView.
  • __tests__/components/ConsoleWebView.test.tsx (37 cases). The cover now waits for ready. New cases cover:
    • the cold-start held route dispatched at ready and not at load end;
    • ready before load end, and a ready from another origin ignored;
    • the timeout with fake timers (no lift at timeout minus 1 ms, lift and one dispatch at the timeout), and no timeout before load end or into a failed load;
    • re-creation at the routeChanged route, and unsafe routeChanged payloads dropped;
    • after the merge: a token held before ready delivered on the ready timeout, and a token after an in-app navigation delivered at once.
  • Two of feat(bridge): stable installId and pushTokenChanged for device registration; document the no-Service-Worker rule #167's tests were adapted, only where the readiness signal moved; their assertions are unchanged:
    • "holds a token reported while the failure page is up, for the recovered page": the onLoadStart call after "Try again" is removed, because the re-created WebView re-arms the hold itself. It still asserts nothing is delivered while failed, and fcm-3 is delivered at the recovered page's ready.
    • "re-arms the hold when a new document starts loading": the reload is the shell's ref.reload(), with a check that the WebView's reload ran, instead of a bare onLoadStart. It still asserts nothing is delivered before the new ready and apns-4 is delivered at it.
  • Two of feat(bridge): scan() — native barcode / QR scanner for Console fields #170's answer tests (in "an answer whose document is gone") were adapted, only where the load-start trigger moved; their assertions are unchanged. A new loadStart() helper delivers a load start the way react-native-webview's WebViewShared.onLoadingStart does: onLoadStart when the host wires one, then onNavigationStateChange. That replaces a bare call to the now-unwired onLoadStart prop.
    • The openScan() setup: "is injected into the page that asked while that page is still there" and "is dropped when the WebView was re-created meanwhile" run through it.
    • "still settles the call after a load start in the same WebView (an in-app navigation)": it still asserts that the d1.1 answer is injected after a same-WebView load start. Ablation leg 3b shows it goes red if a load start bumps the counter.
  • All other feat(bridge): stable installId and pushTokenChanged for device registration; document the no-Service-Worker rule #167 and feat(bridge): scan() — native barcode / QR scanner for Console fields #170 tests are untouched and pass.
Gate readings at b82e022
npx tsc --noEmit                          exit=0
npx eslint . --ext .ts,.tsx               exit=0 (no output)
npx jest --ci --coverage                  exit=0
npx jest --ci --coverage --no-cache       exit=0 (cold transform cache)
Test Suites: 34 passed, 34 total          (both runs)
Tests:       553 passed, 553 total        (both runs)
All files: 82.9 stmts, 79.09 branch, 71.97 funcs, 82.22 lines
Ablations (one-time, not committed)

Leg 1, cover and route (at 2e52b7e): the loadEnd transition was mutated to "ready at load end" (return becomeReady(state, [])). 8 of 45 failed, including "holds a route asked for on cold start past load end, and dispatches it on ready". Restored; the blob hash matched HEAD.

Leg 2, token flush (at 7246e1e): the sendPushToken effect in becomeReady was removed.

anchor before: old=1 new=0 ; after mutate: old=0 new=1
ablated jest exit=1 -- Tests: 7 failed, 51 passed, 58 total
  ConsoleWebView: delivers a token held before ready on the ready timeout (a Console that never posts ready)
  ConsoleWebView: holds a token reported before ready -- after load end too -- and delivers the latest at ready
  ConsoleWebView: holds a token reported while the failure page is up, for the recovered page
  ConsoleWebView: re-arms the hold when a new document starts loading
  console load state machine: holds the latest token until ready, then sends it once, after the held route
  console load state machine: keeps the held token through a failure and a re-created WebView
  console load state machine: sends the held token on the ready timeout
restored via git checkout HEAD: blob hash equal to HEAD (5674e86), git diff HEAD empty

Leg 3, the answer counter (at b82e022, ConsoleWebView suite, 37 cases). Both legs were restored by git checkout HEAD; the blob hash matched HEAD (096d123) and git diff HEAD was empty.

3a: bump removed from the recreate effect   -> 1 failed: is dropped when the WebView was re-created meanwhile
3b: bump added to onNavigationStateChange   -> 1 failed: still settles the call after a load start in the same WebView (an in-app navigation)

Acceptance notes

  • Not a new document to the shell: a full-document load the page starts by itself after ready, such as an older Console's location.assign fallback or an identity-provider round trip.
    • The cover stays down. A route or token sent during that load is lost with the old document.
    • onLoadStart cannot be that signal (see What changes).
    • Bridge answers are still guarded for that case by the bootstrap's per-document call ids (feat(bridge): scan() — native barcode / QR scanner for Console fields #170).
    • A reliable one would be a bootstrap-posted document-start event, which is a bridge-protocol addition left out of this PR.
  • ready currently fires at the Console's first router commit, which can be its own loading fallback. Native host: make ObjectStackNative.platform an open set and pin it with an App-level test (follow-up to #29140) objectstack#29330 settles that. Lifting the cover onto a spinner the Console painted is accepted by the card.
  • The Android onLoadStart reading comes from the react-native-webview 13.16 source (RNCWebViewClient.java). It was not measured on a device.

Verification

  • Not verified on a device. The cold-start notification tap is the maintainer's check above.

Generated by Claude Code

… ready

The loading cover and a route held on cold start now wait for the Console's
`ready` bridge event instead of the WebView's load end, with an 8 s timeout
after load end for Consoles that never post it. The last route a re-created
WebView resumes at comes from `routeChanged`; navigation state is only the
fallback for Consoles that never post it. One pure state machine
(lib/shell/console-load.ts) drives all three.

Claude-Session: https://claude.ai/code/session_01QSeKYnW1Xf6HDSWz1eGAqv
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

repo:mobile#1 · os-justin · session_01QSeKYnW1Xf6HDSWz1eGAqv

Seat verdict: ACCEPT — Head-sha: 2e52b7e4a649912ef673f2c4ad5ab3e2ec1337c3

维护者速读

  • 改了什么: 加载遮罩、冷启动暂存的路由、「上次路由」现在由一个纯状态机(lib/shell/console-load.ts)统一管理:
    • 遮罩和暂存路由等 Console 发出 ready 才放行,加载结束 8 秒后超时兜底。
    • 上次路由以 Console 的 routeChanged 为准。
    • 文档写明安全区留白由壳负责,--os-native-safe-* 变量只作信息。
  • 为什么改: 以前冷启动点通知时,壳在页面加载结束就派发路由,那时 Console 的导航监听还没挂上,壳只能整页重载。遮罩也总在 Console 能用之前就撤掉了。
  • 风险与回滚: 不发 ready 的旧版 Console,遮罩最多多停 8 秒;Console 启动失败时也是 8 秒后撤遮罩。不涉及数据或协议版本。回滚就是 revert 本 PR。
  • 先看: lib/shell/console-load.ts 的 stepConsoleLoad(loadEnd / ready / navigate 三个分支)→ components/shell/ConsoleWebView.tsx 的效果执行和超时计时器。
  • 两分钟验证: 运行 npx jest --ci __tests__/lib/shell/console-load.test.ts __tests__/components/ConsoleWebView.test.tsx,应 45 个用例全过。
  • 需要你做的: ① 在开发包上冷启动 App,点一条带路由的通知,确认直接到达那一页、页面不整页重载。另请顺带看一眼深色主题下状态栏那条底色是否突兀(卡面提到,未实测)。本 PR 未上真机。② 审批并合并。

席位意见: 实现收敛成一个可单测的纯状态机。消融实测:把 loadEnd 改成立即 ready,45 个用例里 8 个转红,用例确实钉住了行为。安全区按卡面维持现状,停止条件未触发,依据是 ui/ 下无人读 --os-native-safe-*。建议合并。

席位核对

  1. Shape: draft → main. The first line is Fixes #162. Clause-②: no and Metadata-protocol: none are in the body and the changeset (.changeset/162-ready-route-safe-area.md).
  2. Premise, measured:
    • On main at 7cb17d1, onLoadEnd called markLoaded (ConsoleWebView.tsx:316-320), and so did ready (:234). Whichever came first released the cover and the route.
    • The dev read objectstack ui/packages/mobile/src/useNativeHost.ts:112-136. ready, routeChanged and the navigate listener mount in one commit, so gating on ready is sound.
    • routeChanged payloads pass through safeConsoleRoute before they can become a recreate start route. Read in routeChangedRoute.
  3. Gates, check-runs pinned to head 2e52b7e: Code Quality success; Lint & Type Check success. mergeable_state is clean. Local: tsc 0, eslint 0, jest 27/27 suites, 312 tests.
  4. Accepted deviations: local gates ran without os-verify-lock.sh (objectstack-only tooling), bounded to --maxWorkers=2.
  5. Out of scope:
  6. Landing: path surface NOT governed (check-governed-merges --pr objectstack-ai/mobile#166: 0 of 6), +625 / −78. The maintainer merges (lane charter). The seat marks the PR ready and requests review, with no auto-merge. Bridge: stable install id and pushTokenChanged for device registration; document the no-Service-Worker rule #163 shares ConsoleWebView.tsx: whichever PR lands second merges main and reconciles.

@objectstack-fleet objectstack-fleet Bot added the needs-user-decision Blocked on a maintainer decision — do not dispatch; on a PR: awaiting the maintainer's review label Nov 2, 2026
@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 2, 2026 12:38
Resolves the docs/HYBRID-SHELL.md Testing-list conflict with #165: both
entries kept (the server probe and picker tests, and the load state machine
test). #165's Server selection section, Session-and-auth wording and Layout
entries merged cleanly beside this branch's Load states and Safe area sections.

Claude-Session: https://claude.ai/code/session_01QSeKYnW1Xf6HDSWz1eGAqv
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

repo:mobile#1 · os-justin · session_01QSeKYnW1Xf6HDSWz1eGAqv

ACCEPT re-pinned to head 9f7d5648c9d3f4ce12396586931d762ae60da56a. The earlier ACCEPT (comment 6577343005) was on 2e52b7e.

The head moved by one merge of main (83d4716, #165). The only conflict was the Testing list in docs/HYBRID-SHELL.md. Both sides were kept: #165's Server selection section, its Session-and-auth wording and its Layout entries stand beside this PR's Load states and Safe area sections. No code changed.

Check-runs on 9f7d564: Code Quality success, Lint & Type Check success. mergeable_state is clean. The verdict and 维护者速读 are unchanged.

Reconciles #167 (installId, pushTokenChanged) with the load state machine:
one source of readiness. The held push token moves into
lib/shell/console-load.ts as state plus a sendPushToken effect, sent on
entering `ready` (the event or the ready timeout) and re-armed by the shell's
reload() and a re-created WebView. #167's consoleReadyRef, heldPushTokenRef
and onLoadStart re-arm are dropped: on Android react-native-webview 13.16
fires onLoadStart from doUpdateVisitedHistory, i.e. for SPA history changes
too. hostInfo and bootstrap stay exactly as #167 wrote them. Docs stack both
sides.

Claude-Session: https://claude.ai/code/session_01QSeKYnW1Xf6HDSWz1eGAqv
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

repo:mobile#1 · os-justin · session_01QSeKYnW1Xf6HDSWz1eGAqv

ACCEPT re-pinned to head 7246e1e10dabb568e6d3bf1da991b9e86ad48d26. This is the reconcile with #167, now merged as 29c25df. The earlier ACCEPT (comment 6577343005) and the first re-pin (6577493746) are superseded.

维护者速读(本轮增量)

席位核对(增量)

Reconciles #170 (bridge scan(), per-WebView answer guard) with the load
state machine. #170's documentRef and the onMessage answer guard stay; the
counter is bumped in the machine's `recreate` effect, the one place a new
WebView is created, and nowhere tied to onLoadStart (still unwired, as in the
#167 reconcile). Two #170 tests drove a load start through the
`onLoadStart` prop; they now deliver it the way react-native-webview's
WebViewShared does (onLoadStart if wired, then onNavigationStateChange).
Docs stack #170's Scanning codes and wire-format text beside Load states.

Claude-Session: https://claude.ai/code/session_01QSeKYnW1Xf6HDSWz1eGAqv
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

repo:mobile#1 · os-justin · session_01QSeKYnW1Xf6HDSWz1eGAqv

ACCEPT re-pinned to head b82e022478af72f065e6dcc0e82ad5e85e7156f2. This is the reconcile with #170 (bridge scan()), now on main as 92a0a26. It supersedes re-pin 6577799698.

维护者速读(本轮增量)

席位核对(增量)

  • Merge: a real merge commit, parents 7246e1e and 92a0a26. ⛔ No rebase, no force-push.
  • Gates on head b82e022: check-runs Code Quality success and Lint & Type Check success. A local git merge-tree against main is clean.
  • feat(bridge): scan() — native barcode / QR scanner for Console fields #170's surface, verified by the seat:
    • lib/shell/bridge/scripts.ts and handler.ts are byte-identical to main.
    • The onMessage guard is unchanged (ConsoleWebView.tsx:314).
    • The counter's only bump is in the recreate effect (:183).
    • Nothing tied to onLoadStart remains wired.
  • feat(bridge): scan() — native barcode / QR scanner for Console fields #170's tests: two were adapted, only where the load-start trigger moved, through a loadStart() helper that mirrors react-native-webview's own dispatch; their assertions are unchanged. "still settles the call after a load start in the same WebView" stays green.
  • Ablation: removing the recreate bump turns 1 test red; adding a load-start bump turns 1 test red.
  • PR body: the seat rewrote it with a signed trailer. Fixes #162 is the only closing keyword.

@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 2, 2026
@objectstack-fleet objectstack-fleet Bot removed the needs-user-decision Blocked on a maintainer decision — do not dispatch; on a PR: awaiting the maintainer's review label Nov 2, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

repo:mobile#1 · os-justin · session_01QSeKYnW1Xf6HDSWz1eGAqv

Seat landing under the maintainer ruling recorded on the seat post (#160): 「非受管 PR 由席位落地」.

Pre-landing reading Value
Head b82e022478af72f065e6dcc0e82ad5e85e7156f2 (ACCEPT re-pinned in 6578493385)
Check-runs Code Quality success, Lint & Type Check success
mergeable_state clean
Path surface NOT governed (0 of the register's mobile surfaces)
EAS guard (main 92a0a26) Build and publish steps skipped, so the merge publishes nothing

Action: auto-merge enabled, which enqueues the PR in the merge queue behind #172 (the merge_group trigger fix). The needs-user-decision label is removed. The device checks in 维护者速读 stay with the maintainer.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Shell: act on the Console's ready and routeChanged signals, and settle who pads the safe area

1 participant