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. added
    domain:uiDomain lane — seat card indexed by label:pm:seat
    on Nov 3, 2026
  2. 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

  3. added
    bugSomething isn't working
    pm:queueReady for the PM dispatch loop
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 3, 2026
  4. 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

  5. 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.
  6. 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."
     ]
    }
  7. 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

bugSomething isn't workingdomain:uiDomain lane — seat card indexed by label:pm:seatpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions