Skip to content

@object-ui/mobile: shell mode — detect window.ObjectStackNative and adapt the Console when the mobile shell hosts it #29039

Closed

Description

@hotlong

Context

objectstack-ai/mobile 2.0 is now a native shell around the web Console (objectstack-ai/mobile#144). It loads https://<server>/_console/ in a WebView and injects a bridge, window.ObjectStackNative, before the Console's own scripts run. Today the Console does not know it is hosted, so it shows desktop chrome inside a phone frame. The shell also pads the WebView with the safe area itself and lifts its loading cover only on the WebView's load event.

The bridge contract is in docs/HYBRID-SHELL.md of the mobile repository (section "Native bridge"):

  • ObjectStackNative.version, platform, appVersion, capabilities, has(capability);
  • safeArea, also published as CSS variables --os-native-safe-top|bottom|left|right;
  • events ready() and routeChanged(route);
  • calls getPushToken(), haptic(kind), openExternal(url), navigate(route), resetServer(), each returning a Promise.

Ask

In ui/packages/mobile (@object-ui/mobile):

  1. Add a useNativeHost() hook. It returns the bridge when window.ObjectStackNative is present, otherwise null. Check lazily at use time, not once at module load: on older Android WebViews the injection can land after the first page scripts.
  2. In shell mode the Console:
    • hides the desktop-only chrome;
    • pads with the --os-native-safe-* variables;
    • calls ready() once the first screen is interactive;
    • calls routeChanged(route) on every SPA navigation.
  3. Typed wrappers for the bridge calls, so Console features feature-detect with has('haptic') and never call a missing method.

Acceptance

  • With a fake window.ObjectStackNative installed before boot, the Console renders shell mode and ready is posted once.
  • Without the global, the Console behaves as today.
  • Unit tests cover detection, late injection, and the ready / routeChanged posts.

Governing text: ADR-0026 is not involved (no third-party UI code). The bridge contract lives in the mobile repository, docs/HYBRID-SHELL.md.

Activity

  1. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    Contributor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: p2 · domain:ui · pm:queue · 2026-11-02T06:35Z

    ⛔ Not a claim.

    • Read fresh. The mobile shell (objectstack-ai/mobile 2.0) hosts the web Console and injects window.ObjectStackNative. This card makes the Console detect it (useNativeHost() in ui/packages/mobile) and adapt its chrome. The work is entirely in ui/**, so it is domain:ui: the mobile project's cards in this repo go to the existing lanes. The bridge contract is owned by the mobile repo (docs/HYBRID-SHELL.md), and nothing here adds an ObjectStack protocol key, so p2. Console: register the mobile shell's native push token after sign-in, unregister it on sign-out #29042 is blocked on it.

    Generated by Claude Code

  2. added
    domain:uiDomain lane — seat card indexed by label:pm:seat
    enhancementNew feature or request
    pm:queueReady for the PM dispatch loop
    on Nov 2, 2026
  3. hotlong commented on Nov 2, 2026

    @hotlong
    ContributorAuthor

    Scope addition from the mobile side (objectstack-ai/mobile#144, patch round in progress):

    When the shell opens a Console route (a notification tap, an app link, or a bridge navigate call), it no longer reloads the page. It dispatches a cancelable objectstack-native:navigate event on window, with detail.route set to a Console route that the shell has already checked against path escape. It falls back to a full location.assign only when no listener calls preventDefault().

    The shell-mode work in this card should therefore also:

    • listen for objectstack-native:navigate;
    • navigate with the Console's own router to detail.route;
    • call preventDefault(), so the Console bundle is not reloaded on every notification tap.

    Acceptance addition: dispatching the event in shell mode moves the router without a document reload, and outside shell mode the listener is not installed.


    Generated by Claude Code

  4. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    Contributor

    domain:ui#4 · zhuangjianguo · session_01QsR2jR7ZFkpqmm4U8S1f9y

    Claim: PM loop round 10 · 2026-11-02T06:54Z
    Session: session_01QsR2jT77n7bRx8GRbrHKo7 (card thread, os-dev); dispatched by seat session session_01QsR2jR7ZFkpqmm4U8S1f9y
    Account: zhuangjianguo
    Branch: claude/project-thread-88oszd (platform-assigned to the card thread; it can push only this branch)
    Worktree: objectstack-issue-29039
    Domain: domain:ui
    Seat: domain:ui#4
    File surface: new files in ui/packages/mobile/src/ (useNativeHost, the bridge type and typed wrappers), index.ts exports, README.md, the console wiring in ui/apps/console/src/main.tsx / App.tsx / AppContent.tsx, shell-mode chrome and safe-area padding in ui/packages/app-shell/src/layout/** if needed, their tests, and a changeset (stop on breach; explain in the report)
    Container & model: M, mode:thread, model: default tier (dispatch-gates --tier: no path-derived mandate), effort: default
    Clause-②: yes (widening)
    Metadata-protocol: none
    Responsibility: the mobile shell 2.0 hosts the Console in a WebView, but the Console does not know it is hosted and shows desktop chrome inside a phone frame, and the shell lifts its loading cover only on the load event | @object-ui/mobile already owns the responsive hooks and the PWA wiring, so the bridge reader belongs beside them | every user of the mobile app
    Thread-read: 6570768628
    Premise: holds (at origin/main, nothing in ui/ reads ObjectStackNative; ui/apps/console/src/main.tsx:12 imports MobileProvider from @object-ui/mobile)
    Serial constraints cleared: none in flight on these files (every open PR checked)


    Generated by Claude Code

  5. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 2, 2026
  6. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    Contributor

    domain:ui#4 · zhuangjianguo · session_01QsR2jT77n7bRx8GRbrHKo7
    os-dev-report

    {
      "issue": 29039,
      "status": "done",
      "branch": "claude/project-thread-88oszd",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/29140",
      "head": "1205cf10",
      "session": "session_01QsR2jT77n7bRx8GRbrHKo7",
      "premise_still_valid": true,
      "summary": "@object-ui/mobile gains ObjectStackNative (exactly the HYBRID-SHELL.md bridge members), getNativeHost/useNativeHost with lazy detection (render read, mount re-read, DOMContentLoaded, load, and re-checks at 100/500/2000 ms after load), five native* call wrappers that check has(method) and the method and resolve a NativeCallResult (unavailable | ok | failed) without ever throwing, and useNativeHostRouting. The Console renders NativeShellRouting beside the Routes element inside ConsoleShell: ready() once per page load after the first route commits, routeChanged(pathname+search+hash) once per route change, and the maintainer's objectstack-native:navigate scope addition (in-app paths only, preventDefault only after acceptance, no listener outside shell mode). Shell mode drops the PWA manifest and marks the root with data-os-native-shell; per seat ruling B it adds no safe-area padding.",
      "tests": "At 1205cf10, from ui/, --maxWorkers=2, under os-verify-lock: packages/mobile/ + every apps/console App.*.test.tsx + i18nAfterSignIn-12034 + internalFormShell: 14/14 files, 113/113 tests (new: nativeHost.test.tsx 40, App.nativeShellMode-29039.test.tsx 4). vitest run scripts/: 169 passed + 2 skipped files, 4900 passed + 2 skipped tests. turbo type-check mobile+console 72/72 exit 0; eslint touched files 0 problems; check:new-line-citations 0 new. Ablations via ablation-replace.mjs (landed by anchor 1->0 and blob hash, restored byte-identical): late-injection watch off 4 red; readyPosted guard off 2 red; preventDefault off 2 red; wrapper has()+method check reduced to bridge-present 11 red. Real browser (Playwright Chromium 390x844, console Vite dev server, no backend): shell events ready@/login once then route:/login, attr ios, no manifest, body padding 0px; navigate event to /forgot-password prevented, same boot id, route posted once; browser control: no events, no attr, blob manifest present, event not prevented; bridge injected 50 ms after load still got ready.",
      "gates": "dispatch-gates --commands: 34 derived; 34/34 exit 0 at 1205cf10 (check-sdui-manifest, check-sdui-manifest --self-test, check-changeset-no-major, check-adr-0087-registration, check:type-check-debt under the lock, vitest run scripts/ among them); --ran reconciliation: 34 derived, 34 run, 0 NOT-MEASURED, 0 UNRUN. PR-body gates on the exact body: check-changeset-no-major (GITHUB_EVENT_PATH) 0, check-adr-0087-registration 0, check-closing-keyword-parity 0, check-partof-closing-keyword 0. CI check-runs on 29140: in_progress at report time.",
      "line_budget": "9 files, +1068 / -22 = 1090 changed lines, under 3000",
      "files_changed": [
        ".changeset/29039-mobile-native-shell-mode.md",
        "ui/apps/console/src/App.tsx",
        "ui/apps/console/src/main.tsx",
        "ui/apps/console/src/__tests__/App.nativeShellMode-29039.test.tsx",
        "ui/packages/mobile/README.md",
        "ui/packages/mobile/src/index.ts",
        "ui/packages/mobile/src/nativeHost.ts",
        "ui/packages/mobile/src/useNativeHost.ts",
        "ui/packages/mobile/src/__tests__/nativeHost.test.tsx"
      ],
      "deviations": [
        "Safe area: the brief's bullet (pad with --os-native-safe-*) replaced by the seat's ruling B after the measured premise failed: mobile ConsoleWebView.tsx at 443207c pads the WebView top/bottom unconditionally; named in the PR body as a departure from Ask 2.",
        "Scope addition: hotlong's card comment 6571060625 (objectstack-native:navigate) implemented per the seat's two rulings; named in the PR body with the link.",
        "main moved 8 commits after the merged head 1205cf10 (origin/main 5d3667e2) before pr_create; fetched and checked: none touches ui/packages/mobile, the console files here, or ObjectStackNative, so not re-merged; gate readings name 1205cf10.",
        "No content/docs/frontend page exists for @object-ui/mobile, so docs are the package README only (ui/AGENTS.md #2).",
        "PR opened without assignees or reviewers per the brief, though the harness asks to assign; the harness trailer wording replaced by the model-free pair the pre-push check requires."
      ],
      "dogfood_battery": "n/a — no ! in the PR title and the Clause-② arm is (widening)",
      "mcp_calls": "0 GitHub MCP calls",
      "api_writes": "2 — fleet-write relay pr_create (POST /repos/objectstack-ai/objectstack/dispatches, run 41411011866, body read back identical, 14068 bytes) and this os-dev-report comment via post-stamped",
      "open_questions": [],
      "out_of_scope_findings": [
        "class: b · reach: named producer (objectstack-ai/mobile docs/HYBRID-SHELL.md, Console-side integration, says the Console should pad with the safe-area variables; ConsoleWebView.tsx at 443207c pads the WebView with insets unconditionally) · evidence: the two quoted texts; a Console that pads doubles the insets · dedupe words: safe area padding shell console double inset · carrier: seat 4 (raise with the maintainer on the card; not filed in mobile, outside the lane)",
        "noted, not filed: in a shell WebView 768 px wide or more (iPad) the Console draws desktop chrome, because the phone layout follows viewport width; ruling keeps the phone layout as is · carrier: #29040",
        "noted, not filed: the contract's push-token registration after sign-in (getPushToken to a server device registry) is outside this card's Ask; nativeGetPushToken is ready for it · carrier: none"
      ]
    }
  7. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    Contributor

    domain:ui#4 · zhuangjianguo · session_01QsR2jR7ZFkpqmm4U8S1f9y

    For the maintainer · safe-area padding · 2026-11-02T08:13Z. ⛔ Not a claim. @hotlong, PR #29140 does not take one line of the Ask, and this records why.

    • Ask 2 says the Console "pads with the --os-native-safe-* variables", following docs/HYBRID-SHELL.md in objectstack-ai/mobile.
    • Measured: the os-dev read feat!: rebuild as a hybrid shell around the web Console (v2.0) mobile#144 at 443207c. components/shell/ConsoleWebView.tsx wraps the WebView in a View with paddingTop: insets.top and paddingBottom: insets.bottom, unconditionally.
    • Seat's ruling: if the Console also padded, the top and bottom insets would double. So the shell stays the single owner of the safe area, and the Console pads nothing in shell mode. The variables stay readable for any later surface that opts in.
    • For the mobile side: either HYBRID-SHELL.md drops the line about the Console padding with the variables, or the shell stops padding its WebView and the Console takes it over. Seat 4 cannot change the mobile repository. If you want the Console to own the padding, say so here and a follow-up card does it.

    Generated by Claude Code

  8. hotlong commented on Nov 2, 2026

    @hotlong
    ContributorAuthor

    Acceptance addition: keep shell mode platform-neutral.

    The mobile shell is the first host of window.ObjectStackNative, but not necessarily the last. A desktop shell (Tauri, same bridge) is the recorded future shape if a desktop need ever appears. To keep that free for the Console:

    • Never infer "phone" from the presence of the bridge. Read ObjectStackNative.platform for the host, and use the viewport and breakpoints, not the host, for layout decisions.
    • Gate every native call on ObjectStackNative.has(capability), never on platform name.
    • Treat platform as an open set on the Console side (today ios / android; desktop may follow), with no exhaustive switch that breaks on a new value.

    Acceptance addition: a fake bridge reporting an unknown platform with a subset of capabilities renders shell mode without errors, and only the announced capabilities are used.


    Generated by Claude Code

  9. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    Contributor

    domain:ui#4 · zhuangjianguo · session_01QsR2jR7ZFkpqmm4U8S1f9y

    Seat review · 2026-11-02T08:43Z · PR #29140 at 1205cf10: contract review (TIER_DEFAULT) PASS 6572291131 and ACCEPT (on the PR). It goes to the merge queue now.


    Generated by Claude Code

  10. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    Contributor

    domain:ui#4 · zhuangjianguo · session_01QsR2jR7ZFkpqmm4U8S1f9y

    Landed · 2026-11-02T10:02Z · PR #29140 merged through the merge queue as 45e9b2f1. Records: contract review (TIER_DEFAULT) PASS 6572291131 and ACCEPT 6572825986, both at 1205cf10.

    probe before (45e9b2f1~1) now (origin/main)
    export function useNativeHost in ui/packages/mobile/src/useNativeHost.ts 0 2
    postNativeReady in ui/packages/mobile/src/nativeHost.ts 0 1
    NativeShellRouting in ui/apps/console/src/App.tsx 0 2
    getNativeHost in ui/apps/console/src/main.tsx 0 2

    The card closed through Fixes. pm:dispatched comes off with this note. #29155 (PWA manifest and CSP, domain:ui#4) merges main now and writes its main.tsx registration on top of this shell-mode branch. Tracker #29040 keeps its remaining children.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

domain:uiDomain lane — seat card indexed by label:pm:seatenhancementNew feature or requestpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions