Skip to content

fix(console): a native push route is hosted under an app, as the inbox hosts it - #29810

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-vrlmvd
Nov 3, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-vrlmvd

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Steedos · project thread
Fixes #29750

Clause-②: no
Metadata-protocol: none

维护者速读

What changes

The push contract (packages/spec/src/api/push-delivery.zod.ts, #29218) says a push route is the notification's actionUrl, app-relative (/opportunity/123) or a Console route (/apps/crm/opportunity/123), and that the tap resolves it "as the inbox does, never verbatim". The Console's native navigate listener navigated it verbatim, so /opportunity/123 matched no route and the catch-all sent the person to Home. Two legs now host it, both through the inbox's own resolver, resolveNotificationTarget, with resolveHostAppSegment. Nothing is re-implemented.

Leg 1, the listener (NativeShellRouting in ui/apps/console/src/App.tsx). It hands useNativeHostRouting a wrapped navigate (pure logic in the new ui/apps/console/src/nativeShellRoute.ts, nativeShellRouteTarget):

  • Inside an app, it hosts the route under the app the URL names (/apps/:appName/*, already the app's route segment). /opportunity/123 on /apps/crm/... goes to /apps/crm/opportunity/123; /apps/x/y/1 passes through.
  • Anywhere else (Home, or a cold start before any app opened), it hands the route to the router as given, and leg 2 hosts it. Naming the app there needs the app list, and the listener renders beside the routes, outside every MetadataProvider. The shell's published currentAppName is an app NAME; only the list turns it into its route segment. Measured live: hosting under the name (/apps/crm_app/... for an app addressed as /apps/com.example.crm) made the app route re-address the page four times before it settled.
  • The resolver's null (blank url) and external (scheme or //host) answers navigate nothing, and the event stays cancelled. Neither is reachable through the listener by construction: isInAppRoute in @object-ui/mobile refuses a blank route, a scheme and a second leading slash before the Console sees the event. Both are pinned at unit level.

Leg 2, the catch-all (path="*", new element ui/apps/console/src/components/UnmatchedRoute.tsx):

  • A path whose first segment is a Console route served outside an app (CONSOLE_ROOT_SEGMENTS, the table hostInConsolePath already keeps and the agent-chat route-table test pins against App.tsx) or apps keeps today's redirect to /.
  • Any other unmatched path goes under ProtectedRoute requireOrganization={false} (the same guard as /), waits until the app list has settled (isAppListConclusive, the landing route's own reading), and redirects to resolveNotificationTarget(path + query + hash, resolveHostAppSegment(apps, currentAppName)): the app the person last had open while it is still active, else the first active app. A failed app list keeps the old redirect to /, whose landing resolver owns the retry. A signed-out visitor goes through login and comes back to the same path.
  • This covers the three ways the mobile shell delivers a route as the page's FIRST URL (read on objectstack-ai/mobile main ddcf2d5, lib/shell/console-load.ts and lib/shell/bridge/scripts.ts): a re-created WebView starts at the held route; the 8 s ready timeout on a Console whose listener is not mounted falls back to location.assign of the route; a server switch starts at its route. A warm tap is held until ready and dispatched as the event, which the listener takes.

@object-ui/app-shell exports resolveNotificationTarget, type NotificationTarget and CONSOLE_ROOT_SEGMENTS from the package root (they were on utils/ only). The package is private, so nothing is published. ui/packages/mobile is unchanged.

Why

From day one of push (#29041), every notification whose link is app-relative (the synthesized /OBJECT/ID shape) would open Home instead of its record, which is the defect objectui#5179 fixed for the inbox doors.

Tests

  • App.nativeShellMode-29039.test.tsx (real App under the real BrowserRouter, fake bridge), two new top-level describe blocks. Listener: hosted under the URL's app with query and hash kept, event cancelled; /apps/x/y/1 passes through; with no app open, the route is handed on and the catch-all hosts it under the default app; away from an app, it is hosted under the app last open, by its route segment (_packageId). Catch-all: first active app wins over an inactive one; last-open app re-checked; waits while loading; waits while a session-cached list is still re-read (status loading, measured live); a failed list keeps /; controls /dev/nope, /home/nope, /apps keep /; a real /home is rendered by its own route and never rewritten.
  • New nativeShellRoute-29750.test.ts (8 cases): the listener's pure answer, including the unreachable null / external branches, and the unmatched-path predicate's table.
  • App.agentChatLink-10640.test.tsx: its RootLandingRedirect mock now spreads importOriginal (one line, no assertion change), because its control URL reaches the catch-all, which reads isAppListConclusive from that module.
  • Ablations (each with scripts/ablation-replace.mjs, mutation landed by anchor count and blob, restored to the HEAD blob with git diff HEAD empty): the wrapper navigating verbatim reds "an app-relative route is hosted under the app in the URL"; the catch-all always redirecting to / reds the six hosting and waiting cases, controls stay green.

Acceptance notes

  • Behaviour change on the web (scope ruled by the seat): an unknown deep link whose first segment names no Console route (/foo/bar) now opens /apps/APP/foo/bar instead of Home. Measured live on app-crm: the app shows its own "Page not found. The URL you followed does not match any view in this app. Go back", inside the app's chrome, no loop, no page error.
  • Deviation from the ruled leg 1, reported to the seat before the PR: the listener's host hint is the URL's :appName only, not currentAppName ?? :appName. currentAppName is an app name; it reaches the catch-all instead, which re-checks it against the app list exactly as the inbox does.
  • Observation, not filed (carrier: none): an /apps/NAME/... URL that addresses an app by its name rather than its _packageId segment lands correctly but re-addresses the page several times on the way (live history: alternating crm_app and com.example.crm replaces). Nothing here produces that form any more.
  • Cold-start reading is from the public mobile repository's main (ddcf2d5); nothing in that repository changes.

Verification

Final head b1ed0110 (main merged twice; neither merge touched this PR's files). On that head: closure rebuild (70 tasks), @object-ui/console and @object-ui/app-shell type-check Done, and the 13 console test files that mock the root landing or use @object-ui/mobile plus the new unit file: 13 files, 62 tests, all passed. eslint on every touched file: exit 0.

Derived gate sweep (dispatch-gates, 35 families, run at 93b08ba, reconciled)

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 35 commands; every one exited 0, including node scripts/check-sdui-manifest.mjs, pnpm --dir ui exec vitest run scripts/, pnpm check:type-check-debt, pnpm check:test-source-alias, pnpm check:cross-package-test-inputs and the changeset gates. dispatch-gates --ran: 35 derived, 35 run, 0 NOT-MEASURED. The changeset and manifest gates were re-run on b1ed0110 before the PR opened.

Live run (app-crm --fresh plus the Vite Console on this branch, Playwright, fake bridge)
  • /apps/com.example.crm/foo/bar: the app's "Page not found" state.
  • First URL /foo/bar: /apps/com.example.crm/foo/bar. First URL /crm_opportunity/ID: /apps/com.example.crm/crm_opportunity/record/ID, the record. /dev/nope: Home, as before.
  • Push tap /crm_opportunity/ID from a fresh Home, from inside the app, and from Home after visiting the app: all land on /apps/com.example.crm/crm_opportunity/record/ID, event cancelled. Inside the app the history is one push plus the app's own record replace.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (not touched)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes (eslint on the touched files; CI runs the full lint).
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset (.changeset/29750-native-push-route-hosted.md).
  • 6. packages/spec touched: n/a.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English (the 维护者速读 section is the maintainer's Chinese summary).

Generated by Claude Code

…x hosts it

The native navigate listener resolves the shell's route through the
inbox's resolveNotificationTarget under the app the person is in or last
had open, and the catch-all hosts an unmatched app-relative path under the
default app instead of dropping it on Home. app-shell publishes the
resolver and the console root-segment table from its root.

Claude-Session: https://claude.ai/code/session_01QsR2jVL7fzYGjxa3ani6k8
Co-Authored-By: Claude <noreply@anthropic.com>
…g module's predicate

Claude-Session: https://claude.ai/code/session_01QsR2jVL7fzYGjxa3ani6k8
Co-Authored-By: Claude <noreply@anthropic.com>
…s, and the catch-all waits for a settled app list

Measured live: a session-cached app list stops `loading` while its re-read
still reads `loading`, so the catch-all redirected to Home; and hosting under
`currentAppName` (an app name) made the app route re-address the page several
times. The listener now hands a route on whenever the URL names no app.

Claude-Session: https://claude.ai/code/session_01QsR2jVL7fzYGjxa3ani6k8
Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Nov 3, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#2 · zhuangjianguo · session_01QsR2jRFuJz9QBZqPVHYVhJ

维护者速读

席位意见: 解析用的是收件箱同一个函数,没有另写一份。冷启动时 App 还没打开任何应用,监听器拿不到应用列表,所以交给兜底路由等列表到齐再放;这条路也顺带覆盖了手机壳以首个网址送达的三种情况。/foo/bar 不再回首页是本席裁定的取舍,已实测落在应用自己的「页面未找到」,没有循环跳转。

席位核对 — ACCEPT · domain:ui#2 · head b1ed0110 · 2026-11-03T05:59Z

  • 形态: draft PR on main, Fixes #29750 on its own line. 8 files, +451/−10. Tier S: check-governed-merges --pr 29810 says not governed, under 3000 lines. The surface matches claim 6595211090 / amendment 6595284896, plus the two console-local files the seat approved before code (nativeShellRoute.ts, components/UnmatchedRoute.tsx) and the one-line importOriginal spread in App.agentChatLink-10640.test.tsx's mock (no assertion change). ui/packages/mobile/** is untouched. The @object-ui/app-shell root gains resolveNotificationTarget, type NotificationTarget and CONSOLE_ROOT_SEGMENTS.
  • 前提实测: on main 6f094645, useNativeHostRouting navigates detail.route verbatim, so /opportunity/123 falls to the catch-all and lands on /. Live on this branch (app-crm, Vite Console, fake bridge): a tap on /crm_opportunity/ID from a fresh Home, from inside the app, and from Home after visiting the app all land on /apps/com.example.crm/crm_opportunity/record/ID with the event cancelled. A first URL /crm_opportunity/ID lands on the same record, and /dev/nope still goes Home. The scope-A condition was measured: /apps/com.example.crm/foo/bar shows the app's own "Page not found", with no loop and no page error. Screenshots: /mnt/project-files/issue-29750/live/.
  • 门禁 check-runs (pinned to head b1ed011099df377a131cf1d0c820d9427db0b88b): 55 runs. At this record, every finished run is success or path-skipped, including Lint & Repo Gates, TypeScript Type Check, Type Check UI, Test UI (16/16 plus the aggregate), Build & E2E UI, Live E2E UI, Check Changeset, Governed Surface Queue Guard and PR-body guards. Lint UI was still running; ready waits for it. Dev local gates: 35 derived, 35 run, all exit 0, 0 NOT-MEASURED; the changeset and sdui-manifest gates were re-run on this head. Console 13 files / 62 tests. Ablations: verbatim navigate reds the in-app hosting case; a catch-all that always goes to / reds the six hosting and waiting cases, with the controls green.
  • Code read: both legs call the inbox's resolveNotificationTarget, so the shape is not re-implemented. The listener hosts only under the URL's own :appName, which is already the route segment; anywhere else it hands the route on. The catch-all first refuses Console root segments and apps (CONSOLE_ROOT_SEGMENTS, the table pinned against App.tsx's routes). It then waits under the same ProtectedRoute as / until the app list's status settles, so a session-cached list being re-read is not taken as an answer. A failed list keeps the old /. A signed-out visitor goes through login and returns to the same path.
  • 接受的偏差: the leg-1 host hint is the URL's :appName only, and currentAppName is re-checked in the catch-all. The live measure showed currentAppName is an app NAME and hosting under it re-addressed the page four times. The resolver's null / external branches are pinned at unit level and are unreachable through the listener by construction (isInAppRoute refuses first).
  • 范围外去向: the observation that an /apps/NAME/... URL addressed by the app name re-addresses the page several times before settling is recorded here and not filed. This PR no longer produces that form, and the app route recovers on its own. No Skills-docs: line is owed.
  • 落地条款: Clause-②: no (@object-ui/app-shell and the Console are private, so the new root exports publish nothing), Metadata-protocol: none, patch. No contract review is owed. Land by pr_ready + automerge_enable once Lint UI is green on this head.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 3, 2026 06:00
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 3, 2026 06:00
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 3, 2026
Merged via the queue into main with commit b0ef25f Nov 3, 2026
56 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-vrlmvd branch November 3, 2026 06:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant