Skip to content

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

Closed

Description

@hotlong

Context

objectstack-ai/objectstack#29140 merged the Console's shell mode (card objectstack-ai/objectstack#29039). An acceptance review checked it against this repository's main (8c6f13a): the bridge contract matches on both sides. The review also found that the shell does not yet use the signals the Console now sends.

  • Loading cover. onLoadEnd lifts the cover (components/shell/ConsoleWebView.tsx:316-320), usually before the Console posts ready. So ready has no visible effect.
  • Held route on cold start.
    • markLoaded runs at onLoadEnd and dispatches the held route at once (ConsoleWebView.tsx:146-157).
    • The Console's navigate listener mounts only after its boot round-trips (objectstack ui/apps/console/src/main.tsx:168-176).
    • So the event goes unhandled and the shell falls back to location.assign. A notification tap on cold start reloads the page.
  • Last route. The shell breadcrumbs routeChanged (ConsoleWebView.tsx:233-236) but takes the last route from onNavigationStateChange (:275-281). That callback does not report SPA pushState reliably on every engine.
  • Safe area.
    • The Console adds no padding: nothing under objectstack ui/ consumes --os-native-safe-*, and its README says the shell owns the padding.
    • The shell pads natively (ConsoleWebView.tsx:298-300), so nothing is padded twice.
    • But docs/HYBRID-SHELL.md:187 says the Console should "pad with the safe-area variables", and :196-197 calls native padding a stopgap.

Ask

  1. Cover on ready. Lift the loading cover on the Console's ready event. Keep a timeout fallback (choose it in the PR) for older Consoles and for boot failures where the app never mounts.
  2. Held route on ready. Dispatch a held route after ready, not at load end, with the same timeout fallback.
  3. Last route from routeChanged. Use the routeChanged payload as the source of the last route, which recovery uses after a WebView is re-created. Keep onNavigationStateChange only as the fallback for Consoles that never post routeChanged.
  4. Safe-area ownership. The shell keeps owning the padding.
    • Update docs/HYBRID-SHELL.md (lines 187 and 196-197) to say so, and that the CSS variables are informational.
    • If you find a reason to hand padding to the Console instead, stop and raise it on this card. One such reason: the shell's strip behind the status bar can clash with a dark Console theme. That change needs a handshake, so that an older Console does not lose its padding.

Note: ready currently fires at the Console's first router commit, which may be the Console's own loading fallback. objectstack-ai/objectstack#29330 settles what ready means. Lifting the cover onto a spinner the Console painted is acceptable.

Acceptance

  • Unit tests cover the cover and held-route state machine: ready before and after load end, the timeout fallback, and a failed load.
  • A notification tap on cold start does not reload the page. This is a manual check on a development build, listed in the PR's 维护者速读.
  • docs/HYBRID-SHELL.md matches the shipped behaviour.

Activity

  1. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    Contributor

    repo:mobile#1 · os-justin · session_01QSeKYnW1Xf6HDSWz1eGAqv

    First touch (single-lane repo). Type enhancement, native shell behaviour.

    • Premise, read on main at 7cb17d1: it holds. The line numbers shifted slightly after feat: rebrand the app to ObjectOS (name, objectos scheme, ai.objectos.mobile) #164.
      • onLoadEnd → markLoaded is at components/shell/ConsoleWebView.tsx:316-320.
      • ready also calls markLoaded (:234), so whichever event comes first wins.
      • onNavigationStateChange is at :275.
      • Native padding uses insets.top and insets.bottom (:286, :300).
      • The docs' "pad with the safe-area variables" line is still in docs/HYBRID-SHELL.md.
    • No dependency on the connection-flow cards.
  2. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    Contributor

    repo:mobile#1 · os-justin · session_01QSeKYnW1Xf6HDSWz1eGAqv

    Claim: PM loop round 1
    Session: session_01QSeKYnW1Xf6HDSWz1eGAqv
    Account: os-justin
    Branch: claude/issue-162-ready-route-safe-area
    Worktree: mobile-issue-162
    Domain: n/a — single-lane repo (repo:mobile)
    Seat: repo:mobile#1
    File surface: components/shell/ConsoleWebView.tsx, a state-machine module under lib/shell/ if extracted, __tests__/**, docs/HYBRID-SHELL.md, .changeset/162-ready-route-safe-area.md (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: default tier (opus) — dispatch-gates --tier --repo objectstack-ai/mobile: no path-derived mandate, effort: high
    Clause-②: no
    Metadata-protocol: none
    Responsibility: n/a — not a defect card
    Thread-read: none
    Premise: holds
    Serial constraints cleared: #163 is dispatched in the same round. It touches ConsoleWebView.tsx only to deliver a window event, a different region from the cover and held-route logic. docs/HYBRID-SHELL.md is shared with #154 and #163. Both overlaps are ordinary concurrency: the PR that lands later resolves the conflict.

  3. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    Contributor

    repo:mobile#1 · os-justin · session_01QSeKYnW1Xf6HDSWz1eGAqv
    os-dev-report

    {
      "issue": 162,
      "status": "done",
      "branch": "claude/issue-162-ready-route-safe-area",
      "pr": "https://github.com/objectstack-ai/mobile/pull/166",
      "session": "session_01QSeKYnW1Xf6HDSWz1eGAqv",
      "premise_still_valid": true,
      "summary": "The loading cover, the held route and the last route now run through one pure state machine, lib/shell/console-load.ts: loading, then loaded at WebView load end, then ready on the Console's ready event or READY_TIMEOUT_MS = 8 s after load end, plus failed. ConsoleWebView only performs the effects the machine returns. A route asked for on cold start is now dispatched at ready, not at load end, and a ready that arrives before load end counts. The last route comes from the routeChanged payload (checked with safeConsoleRoute); onNavigationStateChange is used only until the first routeChanged. Safe area: no behaviour change. docs/HYBRID-SHELL.md now says the shell owns the padding and that --os-native-safe-* are informational, and it documents the load states. Ask-4 stop condition not triggered: nothing under objectstack ui/ reads --os-native-safe-*, and the Console's env(safe-area-inset-*) uses resolve inside the natively inset WebView frame. The dark-theme strip clash the card names stays unmeasured; it needs a device. PM assumptions verified on mobile main 7cb17d1: onLoadEnd calls markLoaded at :316-320, ready calls it at :234, onNavigationStateChange is at :275. Platform facts read at objectstack origin/main 2771575, since the local checkout d9885f5 is behind and these files are byte-equal in the two: the Console renders inside Promise.all(...).finally(async ...) at ui/apps/console/src/main.tsx:79-100, with the render at :168-176, so the card's citation holds. useNativeHostRouting (ui/packages/mobile/src/useNativeHost.ts:112-136) posts ready, posts routeChanged and mounts the navigate listener in three effects of one commit, so the listener is mounted by the time ready reaches the shell. routeChanged carries { route }: pathname, search and hash with the /_console basename stripped (ui/apps/console/src/App.tsx:155-158). Device check not run: it is listed under 需要你做的.",
      "tests": "head 2e52b7e. npx jest --ci --maxWorkers=2: exit 0, Test Suites 27 passed of 27, Tests 312 passed of 312. Targeted run: __tests__/lib/shell/console-load.test.ts (new, 20 cases) and __tests__/components/ConsoleWebView.test.tsx (updated, 25 cases): 45 of 45 passed. Ablation, one-time and not committed: the loadEnd transition in lib/shell/console-load.ts was mutated to `return becomeReady(state, [])`. Anchor counts were old=1 new=0 before and old=0 new=1 after. Ablated jest exited 1 with 8 failed and 37 passed of 45, including the cold-start held-route, cover-until-ready, timeout and failed-load cases. Restored with git checkout HEAD; the blob hash equals the HEAD blob 74e29d0 and git diff HEAD is empty. No build or dist step: jest compiles src through babel.",
      "gates": "local, head 2e52b7e, exit codes captured before any pipe: `npx tsc --noEmit` exit=0; `npx eslint . --ext .ts,.tsx` exit=0 with no output; `npx jest --ci --maxWorkers=2` exit=0. PR check-runs at head 2e52b7e, read once and not polled: `Code Quality` queued, `Lint & Type Check` in_progress. Both are in_progress, an honest value. dispatch-gates.mjs --commands/--ran not used: it is objectstack-only, and mobile has no such script. Gate set taken from the dispatch order.",
      "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm",
      "line_budget": "n/a — the diff touches no skills/** and no ratcheted ledger",
      "files_changed": [".changeset/162-ready-route-safe-area.md +19", "__tests__/components/ConsoleWebView.test.tsx +116 -11", "__tests__/lib/shell/console-load.test.ts +180", "components/shell/ConsoleWebView.tsx +85 -58", "docs/HYBRID-SHELL.md +53 -9", "lib/shell/console-load.ts +172"],
      "deviations": ["Local jest and tsc ran without os-verify-lock.sh. The lock script lives in objectstack scripts/pm, and this was a mobile-repo worktree; the runs were bounded to --maxWorkers=2.", "Commit trailers use the model-free pair (Claude-Session plus Co-authored-by: Claude) required by AGENTS.md, not the harness reminder's model-named Co-Authored-By."],
      "mcp_calls": "0",
      "api_writes": "2 — relay writes: (1) fleet-write dispatch pr_create into objectstack-ai/mobile, which created #166, run 41445774753 success, read-back byte-identical; (2) post-stamped.mjs comment os-dev-report on objectstack-ai/mobile#162. git push is not counted.",
      "open_questions": [],
      "security_findings": "none",
      "out_of_scope_findings": ["carrier: #163 · noted, not filed — a window event the shell delivers into the page (#163's pushTokenChanged) is lost the same way a held route was when it is sent before the Console's listeners mount; #163 should gate delivery on this PR's ready phase. Whichever PR lands second resolves the ConsoleWebView.tsx overlap.", "carrier: none · noted, not filed (PR Acceptance notes only) — pre-existing and unchanged: after ready, a later full-document load (for example an older Console's location.assign fallback) does not raise the cover again, and a route asked for during that load is injected at once."]
    }
  4. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    Contributor

    repo:mobile#1 · os-justin · session_01QSeKYnW1Xf6HDSWz1eGAqv

    ACCEPT → PR #166 comment 6577343005 (head 2e52b7e). Ready for review; the maintainer is requested. The maintainer merges.

  5. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    Contributor

    repo:mobile#1 · os-justin · session_01QSeKYnW1Xf6HDSWz1eGAqv

    Landed: the first seat landing under the ruling on #160. #166 merged through the merge queue as 0fd7113; the merge was performed by objectstack-fleet[bot] via auto-merge. The merge_group run on gh-readonly-queue/main/pr-166-… passed both Lint & Type Check and CI. The card closed completed via Fixes #162. pm:dispatched removed.

    The Android onLoadStart re-arm of the pushTokenChanged hold that was on main is gone with this merge. Next: #152 (saved connections) was serial behind this card and is dispatched now.

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

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions