Skip to content

Console: resolve an app-relative push route in the native navigate listener, as the inbox does #29750

Closed

Description

@hotlong

Context

The push delivery contract merged in #29218 (packages/spec/src/api/push-delivery.zod.ts, around lines 28–39 and 82–83). It says route is the notification's actionUrl:

  • either app-relative, /opportunity/123;
  • or a Console route, /apps/crm/opportunity/123.

It also says the tap is resolved "as the inbox does, never verbatim": an app-relative path is hosted under /apps/APP/, and an /apps/… route passes through.

Nothing resolves it today:

  • Mobile shell. It hands the route to the Console with the objectstack-native:navigate event, and on a cold start falls back to <server>/_console<route>. The shell has no app context and does no resolving. A search of objectstack-ai/mobile main for resolveNotificationTarget and app-relative hosting finds nothing.
  • Console listener. ui/packages/mobile/src/useNativeHost.ts:153-163 checks isInAppRoute(target) and then calls navigate(target) verbatim.

So a push for /opportunity/123 falls to the Console's catch-all and lands on the home page, not the record. This is the defect objectui#5179 fixed for the inbox (ui/packages/app-shell/src/utils/appRoute.ts:328-399, resolveNotificationTarget).

The bug is not visible yet, because no push channel delivers today (#29041). It will be on day one of push.

Ask

  1. In the native navigate listener, resolve the route with the same resolver every inbox door uses before navigating: resolveNotificationTarget(route, hostAppSegment), as openInboxNotification.ts does.
    • /apps/… passes through.
    • /{object}/{id} is hosted under the current app, or the default app when none is open.
    • A route that resolves to nothing leaves the user where they are, rather than going to the catch-all.
  2. The cold-start fallback in the shell (location.assign of the raw route) must not bypass this. Once the shell waits for ready (mobile 🔗 Broken links detected in documentation #162, landed), the event path is the normal one. Confirm that the cold-start tap goes through the listener, and add a Console-side guard that resolves an app-relative path arriving as the initial URL.

Acceptance

  • Unit test on the listener: /opportunity/123 with host app crm → /apps/crm/opportunity/123; /apps/x/y/1 passes through; an unresolvable path does not navigate.
  • The same resolver function is imported, not re-implemented.

Activity

  1. objectstack-fleet commented on Nov 3, 2026

    @objectstack-fleet
    Contributor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: first-touch audit · p2 kept · domain:ui kept · pm:queue · 2026-11-03T03:58Z

    ⛔ Not a claim.


    Generated by Claude Code

  2. objectstack-fleet commented on Nov 3, 2026

    @objectstack-fleet
    Contributor

    domain:ui#2 · zhuangjianguo · session_01QsR2jRFuJz9QBZqPVHYVhJ

    Claim: PM loop round 12 · 2026-11-03T04:08Z
    Session: session_01QsR2jVL7fzYGjxa3ani6k8 (card thread, os-dev); dispatched by seat session session_01QsR2jRFuJz9QBZqPVHYVhJ
    Account: zhuangjianguo
    Branch: claude/project-thread-vrlmvd (platform-assigned to the card thread; it can push only this branch)
    Worktree: objectstack-issue-29750
    Domain: domain:ui
    Seat: domain:ui#2
    File surface: ui/apps/console/src/App.tsx (NativeShellRouting and an initial-URL guard for an app-relative path), ui/packages/mobile/src/useNativeHost.ts only if the design needs it, the @object-ui/mobile README line if behaviour docs change, tests, changeset(s). Design report to the seat before code; the seat amends this claim if a published member is added. Stop on breach; explain in the report
    Container & model: S, mode:thread, model: default tier (dispatch-gates --tier on the final file list), effort: high
    Clause-②: no (provisional: a new published @object-ui/app-shell export or useNativeHostRouting option makes it yes (widening), amended before code)
    Metadata-protocol: none
    Responsibility: the native navigate listener navigates to a push route verbatim | an app-relative push route such as /opportunity/123 lands on the Console home instead of the record (visible from day one of push, #29041) | people opening Console notifications from the mobile shell
    Thread-read: none
    Premise: the seat's reading on main 6f094645: useNativeHostRouting (useNativeHost.ts ~L153–163) calls navigate(target) after isInAppRoute; @object-ui/app-shell depends on @object-ui/mobile, so the resolver cannot be imported from mobile; the one caller is the Console's NativeShellRouting.
    Serial constraints cleared: none known on these files; the os-dev checks open PRs touching useNativeHost.ts and App.tsx and merges main before pr_create

  3. objectstack-fleet commented on Nov 3, 2026

    @objectstack-fleet
    Contributor

    domain:ui#2 · zhuangjianguo · session_01QsR2jRFuJz9QBZqPVHYVhJ

    Claim amendment (to 6595211090) · 2026-11-03T04:14Z · on the os-dev's design report; session, branch and seat unchanged.

    Clause-②: no
    Metadata-protocol: none

    • Design ruled: two legs, one resolver. Leg 1, the Console's NativeShellRouting wraps the navigate it hands useNativeHostRouting: with a host hint (currentAppName, else the URL's :appName) it navigates resolveNotificationTarget(route, resolveHostAppSegment(apps, hint)); with none it hands the route on. Leg 2, the catch-all (path="*"): a path whose first segment is a Console root segment or apps keeps today's redirect to /; any other unmatched path waits for the app list under the same guard as / and redirects to the resolved target under the default app. Every unmatched non-root path is hosted (scope A), so the shell's initial-URL starts (recreate, the 8 s fallback, switch) and a cold-start push's login bounce all reach the record.
    • File surface (amended): ui/apps/console/src/App.tsx (NativeShellRouting and the catch-all), ui/packages/app-shell/src/index.ts (root exports resolveNotificationTarget, type NotificationTarget, CONSOLE_ROOT_SEGMENTS), tests, changeset. ui/packages/mobile/** unchanged.
    • Clause-② stays no: @object-ui/app-shell and the Console are private packages, so the new root exports publish nothing. No new hook option, no server or spec change.
  4. objectstack-fleet commented on Nov 3, 2026

    @objectstack-fleet
    Contributor

    domain:ui#2 · zhuangjianguo · session_01QsR2jVL7fzYGjxa3ani6k8
    os-dev-report

    {
     "issue": 29750,
     "status": "done",
     "branch": "claude/project-thread-vrlmvd",
     "pr": "https://github.com/objectstack-ai/objectstack/pull/29810",
     "head": "b1ed0110",
     "session": "session_01QsR2jVL7fzYGjxa3ani6k8",
     "premise_still_valid": true,
     "summary": "Two legs, one resolver. Leg 1: the Console's NativeShellRouting wraps the navigate it hands useNativeHostRouting; inside an app it navigates resolveNotificationTarget(route, resolveHostAppSegment(apps, URL :appName)); elsewhere it hands the route on. Leg 2: the catch-all is UnmatchedRoute; a first segment in CONSOLE_ROOT_SEGMENTS or apps keeps the redirect to /, any other unmatched path waits (ProtectedRoute, settled app list) and redirects to resolveNotificationTarget(path, resolveHostAppSegment(apps, currentAppName)). app-shell root exports resolveNotificationTarget, NotificationTarget, CONSOLE_ROOT_SEGMENTS (private package). Premise additions: the listener has no app list (outside every MetadataProvider); no in-app route is unresolvable (isInAppRoute refuses first); mobile main ddcf2d5 still delivers a route as the initial URL on recreate, the 8 s fallback and switch.",
     "clause_2": "no",
     "metadata_protocol": "none",
     "files_changed": [
      ".changeset/29750-native-push-route-hosted.md",
      "ui/apps/console/src/App.tsx",
      "ui/apps/console/src/__tests__/App.agentChatLink-10640.test.tsx",
      "ui/apps/console/src/__tests__/App.nativeShellMode-29039.test.tsx",
      "ui/apps/console/src/__tests__/nativeShellRoute-29750.test.ts",
      "ui/apps/console/src/components/UnmatchedRoute.tsx",
      "ui/apps/console/src/nativeShellRoute.ts",
      "ui/packages/app-shell/src/index.ts"
     ],
     "line_budget": "8 files, +451 / -10 vs merge base (dispatch-gates count, generated included), under 3000",
     "tests": "b1ed0110: closure rebuild 70/70 tasks; turbo type-check @object-ui/console + @object-ui/app-shell Done; vitest 13 console files (every test mocking components/RootLandingRedirect or using @object-ui/mobile, plus nativeShellRoute-29750) 62/62 passed; eslint on the 7 touched source/test files exit 0. Ablation A1 (App.tsx wrapper navigates verbatim, anchor 1->0, blob restored == HEAD): 1 red, 14 green. Ablation A2 (UnmatchedRoute always redirects to /): 6 red (hosting + waiting), 9 green incl. controls; restored == HEAD. Live (app-crm --fresh + Vite Console, Playwright, fake bridge): /apps/com.example.crm/foo/bar shows the app's Page not found; first URL /foo/bar -> /apps/com.example.crm/foo/bar; /crm_opportunity/ID -> the record; /dev/nope -> Home; push tap from fresh Home, in-app, Home-after-app -> /apps/com.example.crm/crm_opportunity/record/ID.",
     "gates": "dispatch-gates --commands at 93b08baf: 35 derived, all exit 0 (incl. check-sdui-manifest, vitest run scripts/, check:type-check-debt, check:test-source-alias, check:cross-package-test-inputs, changeset gates); --ran: 35 run, 0 NOT-MEASURED. Re-run on b1ed0110 with the PR body as GITHUB_EVENT_PATH: check-changeset-no-major 0, check-adr-0087-registration 0, check-closing-keyword-parity 0, check-empty-changeset 0, check-sdui-manifest 0, check-partof-closing-keyword 0. dispatch-gates --tier: no path-derived mandate. CI check-runs: in_progress at report time (the seat reads them at ACCEPT).",
     "dogfood_battery": "n/a: no ! in the PR title and no (narrowing) arm",
     "deviations": [
      "Leg 1 host hint is the URL :appName only, not currentAppName ?? :appName as ruled: measured live, currentAppName is the app NAME (crm_app) and hosting under it made the app route re-address the page 4 times; the catch-all re-checks currentAppName against the app list instead. Reported to the seat before pr_create.",
      "Catch-all waits while the app status is idle/loading (not only while loading): measured live, a session-cached list ends loading while its re-read still reads loading, and the first build redirected to Home there. Only status error keeps the redirect to /.",
      "Surface additions approved by the seat before code: ui/apps/console/src/nativeShellRoute.ts, ui/apps/console/src/components/UnmatchedRoute.tsx, and a one-line importOriginal spread in App.agentChatLink-10640.test.tsx's RootLandingRedirect mock.",
      "Mobile cold-start path read from the public objectstack-ai/mobile main ddcf2d5 via a shallow clone in the scratchpad (read only)."
     ],
     "mcp_calls": "3: mcp__github__issue_read (get #29750, get_comments #29750 twice). No MCP write.",
     "api_writes": "2: pr_create via scripts/pm/fleet-write relay (objectstack#29810, read back byte-identical, assignees none); this os-dev-report comment via post-stamped. Plus git pushes of the branch (not REST writes).",
     "open_questions": [],
     "security_findings": "none",
     "out_of_scope_findings": [
      "carrier: none · noted, not filed: an /apps/NAME/... URL addressing an app by name rather than its _packageId segment lands right but re-addresses the page several times (alternating crm_app / com.example.crm replaces, live). Nothing in this PR produces that form."
     ]
    }
  5. objectstack-fleet commented on Nov 3, 2026

    @objectstack-fleet
    Contributor

    domain:ui#2 · zhuangjianguo · session_01QsR2jRFuJz9QBZqPVHYVhJ

    Landed · 2026-11-03T07:05Z · PR #29810 merged through the queue as b0ef25f5. The merge commit carries all 8 files of the accepted head b1ed0110: App.tsx NativeShellRouting, nativeShellRoute.ts, components/UnmatchedRoute.tsx, three app-shell root exports, tests, and the changeset. Delivered in full: a push route from the mobile shell is hosted the way the inbox hosts it, through resolveNotificationTarget. That covers the in-app listener and a first URL or cold-start hand-off through the catch-all. Closed by the PR's Fixes; pm:dispatched removed.

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

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions