Repository navigation
Description
domain:ui#1 · zhuangjianguo · session_01QsR2jUJ19MstFvFhjgiYTu
Filing gate ②: a decision only the maintainer can make. Part of #28645 (the second of its two PRs).
维护者速读
- 改了什么: 现在每次打开任一应用页面,控制台都会在首屏后拉一遍全部页面和全部仪表板列表,跟上一次比对名字,发现多了或少了就往每个应用的导航里写条目。提议改为在新建、删除、改名页面或仪表板的那一刻,显式地写导航。
- 为什么改: 这两次列表请求是 console: on an app page, answer /meta/view, /meta/page and /meta/dashboard from the app bundle (#28321) instead of three list reads; page/pricing 24 to ~21 calls #28645(应用页少 3 次请求,cloud 预算 24 → 21)剩下的大头;只要比对还在,应用页就最少 23 次。比对本身还是一条隐式写路径:谁让列表“看起来变了”,它就往所有应用写导航。
- 风险与回滚: 改的是“什么时候写导航”。漏接一个新建或删除入口,那个入口建出来的页面就不会自动进导航。回滚即恢复比对。
- 先看:
ui/packages/app-shell/src/hooks/useNavigationSync.ts的NavigationSyncEffect(约 590–740 行),以及同文件里已存在但无人调用的sync…AllApps六个方法(约 215–225、485 行起)。 - 需要你做的: 选 A / B / C。
Background
NavigationSyncEffect is mounted on every app page (AppContent.tsx:1440). After first paint it reads metadata.pages and metadata.dashboards (two GET /meta/{type} list reads), diffs the name sets against the previous render, and for every added or removed name calls syncPageCreated / syncPageDeleted / syncDashboardCreated / syncDashboardDeleted on every syncable app (useNavigationSync.ts ~707-735). The explicit per-event writers syncPageCreatedAllApps, syncDashboardCreatedAllApps, syncPageDeletedAllApps, syncDashboardDeletedAllApps, syncPageRenamedAllApps, syncDashboardRenamedAllApps exist in the same hook and have no caller (git grep -n AllApps origin/main -- ui/packages/app-shell/src).
Measured by #28645's card thread at origin/main 2678600d; full reading with file:line in the project file issue-28645/needs-decision.md.
Governing text: none found (git grep -n -i "navigation sync" -- AGENTS.md docs/adr ui/AGENTS.md → 0 hits on a write-timing rule; ADR-0037 only disables the diff in preview).
Options
| Option | What happens | Customer-visible | Work |
|---|---|---|---|
| A — explicit writes | The create / delete / rename sites in the console call the existing sync…AllApps writers; NavigationSyncEffect stops reading the two lists. |
App pages open with two fewer calls (#28645 reaches ~21). A page or dashboard made outside the console (CLI, package install) is not added to nav by a console visit any more. | ui lane, 1 PR, ~3-5 files; no precedent PR |
| B — keep the diff, scope it | Keep the diff, but run it only on routes that create or delete pages/dashboards (Studio, Setup). | App pages lose the two calls; the diff still writes nav, from fewer places. | ui lane, 1 PR, small |
| C — no change | #28645 lands its first PR only (by-name reads); app pages stay at ~23 calls. | Nothing changes. | none |
Business reading: A is "update the menu when you add the item"; B is "re-count the shelves only in the stock room"; C keeps today's re-count on every visit.
Recommendation: A
Two years out, a mainstream platform writes navigation when the author adds the item (Salesforce Lightning app nav, Airtable interfaces), not by re-listing everything on each visit. A leaves one declared write path and removes an implicit one that writes into every app. Only ① decides: A. ②③④ flip it: no.
Confidence gap: items created outside the console (CLI deploy, package install) would no longer reach nav through a console visit; the card thread checks whether any such path relies on the diff today before writing code.
After a ruling: A or B go to the #28645 card thread as its second PR, Part of #28645; C closes this card not planned and #28645 closes on its first PR.
os-decision-facets
- ① Long-term: A shrinks the special cases (one explicit write per event instead of a diff heuristic with a preview carve-out).
- ② Business pull: measured — cloud's call budget accepted 23 → 24 and tracks the way back down (console: on an app page, answer /meta/view, /meta/page and /meta/dashboard from the app bundle (#28321) instead of three list reads; page/pricing 24 to ~21 calls #28645).
- ③ AI error: A makes nav writes declared at the call site; today an AI-authored page change can trigger nav writes into every app silently.
- ④ No sprawl: A adds no new surface; it calls six writers that already exist and retires one effect.
Prior rulings read: NavigationSyncEffect, "navigation sync" → 1 hit ([finding] an app author's draft baseline is read through the pruned plain read (GET /meta/app/:name?state=draft), and the designers merge it over the whole stored app, so a draft save drops the navigation entries withheld from that author #20290, unrelated, closed); ADR none; thread: console: on an app page, answer /meta/view, /meta/page and /meta/dashboard from the app bundle (#28321) instead of three list reads; page/pricing 24 to ~21 calls #28645 6558417171.
Reader: ui seat 1 (domain:ui#1, #25966) dispatches the ruled option to #28645's card thread.
Dedup: repo:objectstack-ai/objectstack is:issue NavigationSyncEffect → 1 hit, #20290 (closed, a draft-baseline merge issue, not this).
Activity
objectstack-fleet commentedon Nov 1, 2026
domain:ui#1 · zhuangjianguo · session_01QsR2jUJ19MstFvFhjgiYTu
Ruled: A · 2026-11-01T13:38Z
The maintainer picked option A on the decision card in this account's project seat thread "Run second ui lane seat" (Claude Code project "objectstack docs ui"), 2026-11-01T13:37Z, verbatim: 「A Write nav at create/delete」. The card's consequence line read: "The console writes nav when a page or dashboard is created, deleted or renamed; app pages stop re-listing them, reaching ~21 calls."
Claim (the same card thread as #28645)
Claim: PM loop round 1 · 2026-11-01T13:38Z
Session:
session_01QsR2jUjXYpqdaAkEWBHbqG(card thread, os-dev); dispatched by seat sessionsession_01QsR2jUJ19MstFvFhjgiYTuAccount:
zhuangjianguoBranch:
claude/project-thread-6p9fk2(the card thread's only branch; this PR opens after #28645's PR 1 lands, from that branch restarted onorigin/main)Domain:
domain:uiSeat:
domain:ui#1File surface:
ui/packages/app-shell/src/hooks/useNavigationSync.ts(NavigationSyncEffectstops reading the page and dashboard lists and diffing them), the console's page and dashboard create / delete / rename sites that call the existingsync…AllAppswriters, their tests, and a root.changeset(stop on breach; explain in the report)Container & model: M,
mode:thread,model: default tier (dispatch-gates --tier: no path-derived mandate),effort: highClause-②: no
Metadata-protocol: none
Responsibility:
n/a — not a defect cardThread-read: none
Premise: holds (the six
sync…AllAppswriters exist inuseNavigationSync.tswith no caller onorigin/main)Serial constraints cleared: #28645 PR 1 (same thread, lands first)
Before code, the thread checks whether any page or dashboard created outside the console (CLI deploy, package install) relies on the diff to reach navigation today, and reports what it finds.
objectstack-fleet commentedon Nov 1, 2026
domain:ui#1 · zhuangjianguo · session_01QsR2jUjXYpqdaAkEWBHbqG
os-dev-report
{ "issue": 28680, "status": "done", "branch": "claude/project-thread-6p9fk2", "pr": "https://github.com/objectstack-ai/objectstack/pull/28783", "session": "session_01QsR2jUjXYpqdaAkEWBHbqG", "premise_still_valid": true, "summary": "PR 2 of #28645, Fixes #28680 and #28645 (ruling A). NavigationSyncEffect is retired (inert, deprecated export; AppContent no longer mounts it) and reads neither /meta/page nor /meta/dashboard. New useNavigationWrites() keeps the diff's rules (page/dashboard only, no platform artifact, not in preview, writable apps) and is called at ResourceEditPage's first publish (no layer before, unbound) and real delete (no baseline, unbound), and in Publish all for package-less page/dashboard drafts whose layers answered 404 before the publish. syncPageCreated/syncDashboardCreated now skip an app that already references the target. Warm reload on a production build (showcase_app): page 26->24, object list 33->31, dashboard 35->34.", "pre_code_check": "No page or dashboard created outside the console relied on the diff: package installs and CLI deploys carry _packageId (showcase_start_here's layers answer _packageId com.example.showcase, _provenance package on the running server) and Studio saves with a package. Lost: package-less items created or deleted outside this tab mid-session (server-side agent, other tab, raw REST); stated in the PR body and the changeset as the seat asked.", "tests": "295 test files naming the touched modules, 5 locked batches --maxWorkers=3: 295/295 files, 2493 tests passed. New pins: ResourceEditPage.navigationWrites-28680 (5, incl. a second publish adds no second nav item), usePublishAllDrafts.navigationWrites-28680 (2); useNavigationSync.test rewritten (17); 9868, 11201, 12980 moved to the new door. Ablations each red then restored: 11 of 11. turbo type-check app-shell 65/65 exit 0. eslint --quiet on changed files exit 0. Real-server probe: draft-only page /layers 404, after first publish a layer with resettable false.", "gates": "34 derived (dispatch-gates --commands at f31794f6, merged origin/main cd0ec76b), all exit 0; --ran: 34 derived, 34 run, 0 NOT-MEASURED. PR-body gates with GITHUB_EVENT_PATH: check-changeset-no-major 0, check-adr-0087-registration 0, check-closing-keyword-parity 0, check-partof-closing-keyword 0. CI check-runs: not yet reported (PR just opened).", "line_budget": "11 files, +826/-378 (under the 3000-line human-merge threshold)", "deviations": [ "dashboard route 34, not the expected 33: AppContent's route gate (routeReads) waits for the dashboard list and the dashboard view reads it; the effect shared that request. Bundle-first dashboard route is outside the claim's surface; suggested follow-up card", "Publish all from the draft-preview bar writes no navigation (the door skips preview, where apps may be draft bodies); the diff wrote nothing there either", "NavigationSyncEffect kept as an inert @deprecated export rather than deleted, so the app-shell public export list does not change", "useNavTargetLabel.ts keeps a stale comment naming NavigationSyncEffect as a dashboard-list loader; outside the surface, left as is", "writer dedupe (navigationReferences) added in syncPageCreated/syncDashboardCreated beyond the site gate, for the seat's no-second-entry ask" ], "files_changed": [ ".changeset/28680-navigation-writes-at-sites.md", "ui/packages/app-shell/src/console/AppContent.tsx", "ui/packages/app-shell/src/hooks/__tests__/chromeReads.afterFirstPaint-12980.test.tsx", "ui/packages/app-shell/src/hooks/__tests__/useNavigationSync.labelInherits-9868.test.tsx", "ui/packages/app-shell/src/hooks/__tests__/useNavigationSync.standardEntryLocalizes-11201.test.tsx", "ui/packages/app-shell/src/hooks/__tests__/useNavigationSync.test.tsx", "ui/packages/app-shell/src/hooks/useNavigationSync.ts", "ui/packages/app-shell/src/preview/__tests__/usePublishAllDrafts.navigationWrites-28680.test.tsx", "ui/packages/app-shell/src/preview/usePublishAllDrafts.ts", "ui/packages/app-shell/src/views/metadata-admin/ResourceEditPage.navigationWrites-28680.test.tsx", "ui/packages/app-shell/src/views/metadata-admin/ResourceEditPage.tsx" ], "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm", "mcp_calls": "1 read (issue_read get_comments #28680)", "api_writes": "2 — fleet-write relay pr_create (draft #28783) and this os-dev-report comment (post-stamped); plus git push", "open_questions": [], "out_of_scope_findings": [ "dashboard route still reads /meta/dashboard on every open (AppContent routeReads + dashboard view), the last list read of #28645's target · noted for the seat, not filed" ] }Generated by Claude Code
objectstack-fleet commentedon Nov 1, 2026
domain:ui#1 · zhuangjianguo · session_01QsR2jUJ19MstFvFhjgiYTu
Claim amended · 2026-11-01T19:39Z · file surface widened; it adds to claim 6558711281. Branch, session, size and declaration lines are unchanged.
ui/packages/app-shell/src/hooks/index.tsandui/packages/app-shell/src/index.ts(the retiredNavigationSyncEffectis deleted, not kept as an inert deprecated export:@object-ui/app-shellisprivate: trueand nothing outside it imports it), and comment-only rewording where a comment still names it as a live writer or reader:hooks/useNavTargetLabel.ts,chrome/CommandPalette.tsx,preview/PreviewModeContext.tsx,ui/packages/react/src/context/AppShellContext.tsx./meta/dashboardlist read (34 → 33) is console: the dashboard route reads its dashboard from the app bundle, so an app's dashboard open stops reading the /meta/dashboard list #28784, out of this PR.Generated by Claude Code
objectstack-fleet commentedon Nov 1, 2026
domain:ui#1 · zhuangjianguo · session_01QsR2jUJ19MstFvFhjgiYTu
Landed · 2026-11-01T20:47Z · PR #28783 (PR 2 of 2) merged through the merge queue as
c064d3a2at 2026-11-01T20:45:24Z (GitHub's clock). ACCEPT 6563459505 at headafd4ebd6. PR 1 was #28754 (706ac52d).c064d3a2~1)origin/main)NavigationSyncEffectinconsole/AppContent.tsxuseNavigationWritesinmetadata-admin/ResourceEditPage.tsxMeasured by the dev on a production build (
showcase_app, warm reload): page 26 → 24, object list 33 → 31, dashboard 35 → 34. The dashboard route's last/meta/dashboardread is #28784.Fixes #28645andFixes #28680closed both cards;pm:dispatchedcomes off in the same stroke as this note.Generated by Claude Code