Repository navigation
Commit 9d1a465
[maintainer] refactor(app-shell): move the Interfaces nav rail out of StudioDesignSurface.tsx (slice 3) (#30014)
<!-- claude-projects-attribution: {"github_login":"os-support-ai"} -->
_Requested by **Yibo Wang** · [project
thread](https://claude.ai/code/project/chan_01BVUyyPie8gHwoPM18iABwB?thread=cmsg_01BVUyyPie8gHwoPM18iABwBCE4YXYCrA6G1yrthEDVvNJ)_
Refs #29988
## 维护者速读
- 改了什么:把 Studio「界面」支柱左侧的导航栏(导航树及分组、自定义套件贡献的行、页面/仪表盘/报表/视图的「不在导航里」分区、两个栏位
context、类型名与类型图标,共 442 行)原样搬进同目录新文件 `InterfacesNavRail.tsx`,主文件 11,205 →
10,757 行;顺带把两条测试注释里还指着旧文件的出处改指 `PackageSwitcher.tsx`。
- 为什么改:#29988 按裁定 #24586 A 逐片只搬不改,把这个文件拆到 1 万行以下;这是第 3 片(接缝表第 3 行),第 1、2
片是 #30002、#30008。
- 风险与回滚:无行为变化,搬走的两段代码逐字节一致,只多了 import 和十一个 `export`;对外类型声明 701 个文件中 700
个逐字节不变,另 1 个(`ErrorBoundary.d.ts`)只是联合类型成员顺序不同;回滚即 revert 本 PR。
- 先看:`StudioDesignSurface.tsx` 的 import 段;`InterfacesNavRail.tsx` 的
import 段与十一个 `export`;两个按路径读取的测试各加的一行。
- 两分钟验证:在本分支跑下方「Verification」里的两条 `cmp` 命令,预期都无任何输出(逐字节相同)。
- 需要你做的:无,席位落地。
Clause-②: no
Metadata-protocol: none
## Seam table
Carried forward from #30008. Base lines below are at `origin/main`
`eee41490` (this PR's base; `StudioDesignSurface.tsx` is byte-identical
at `73a3ef9f`, where #30008 landed), where
`ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx`
is 11,205 lines. "Left" is the file's line count after that row and
every row above it: rows 1–3 are measured, the rest are estimates (lines
removed, less about two import lines per slice). Every seam is
module-scope; no seam closes over another component's state.
| # | Seam | Base lines | Size | Reads from the rest of the module ·
hooks it calls | Imported by | Risk | Left |
|---|---|---|---|---|---|---|---|
| 1 | Nav item inspector (`StudioNavItemInspector`, nav target helpers,
`StudioPicker`) | landed | 978 | — | — | **landed in #30002** | 11,884
(measured) |
| 2 | `PackageSwitcher` with `PACKAGE_LIST_TOAST_ID` and
`STUDIO_LANDING_PATH` | landed | 663 | — | — | **landed in #30008** |
11,205 (measured) |
| **3** | **Interfaces nav tree rail: `NavTree`, `NavTreeGroup`,
`ContributedNavRows`, `OffNavSections`, the two rail contexts,
`navKindName`, `navOpenedItems`, with `KIND_ICON` / `navIcon`** |
663–679, 2249–2673 | **442** (17 + 425) | nothing · `useMetadataLocale`,
`useNavTargetLabel`, `useOpenContributor` | `InterfacesPillar` only |
low: self-contained | **10,757** (measured, this PR) |
| 4 | Flow rail helpers: `FlowRuntimeState` … `readFlowCopiedArrival`,
incl. `FlowStatusDot`, glyph tables, `pairIdOf` | 8061–8408 | 348 |
nothing · no hooks | `AutomationsPillar`; `AccessPillar` (`pairIdOf`); 2
tests import `FlowStatusDot` | low: pure helpers plus one prop-only
component; carries the spec-symbol ledger's `FlowRuntimeState` site | ≈
10,411 |
| 5 | Pillar kit: `useHeldEditReport`, `confirmDropUnsavedWork`,
`findTakenIdentifier`, `CreateTakenNotice`, `StudioHeldNotice`, the
"Show me" focus helpers, `announceDraftSaved`, `StudioReadOnlyLink`,
`StudioBaseCustomization` | 508–647, 695–773, 786–816, 818–865,
2681–2706 | 320 | nothing · `useStudioTabRowFold` | all four pillars and
`StudioDesignSurfaceBody` | low: five non-contiguous blocks; lets rows
7–10 move without an import cycle | ≈ 10,093 |
| 6 | Data pillar helpers (`nextFieldName` … `formPreviewSource`, the
OWD option keys) | 447–464, 5583–5823 | 259 | nothing · no hooks |
`DataPillar` only; 1 test imports `renderStudioGridList` | low: pure
helpers | ≈ 9,836 (under 10,000) |
| 7 | `AccessPillar` with its arrival reader and `StudioNameLabel` |
2223–2247, 10262–11203 | 967 | rows 4, 5 · 11 hooks |
`StudioDesignSurfaceBody`; 7 test files | medium: whole stateful
component; after rows 4–5 | ≈ 8,871 |
| 8 | `AutomationsPillar` | 8410–10260 | 1,851 | rows 4, 5 (and
`StudioPicker`, row 1) · 21 hooks | `StudioDesignSurfaceBody`; 23+ test
files | medium: after rows 4–5 | ≈ 7,022 |
| 9 | `DataPillar` | 5827–8059 | 2,233 | rows 2, 5, 6 (and
`StudioPicker`) · 17 hooks | `StudioDesignSurfaceBody`; 38+ test files |
medium: holds three `clickableNonControlCensus-13461` rows keyed by this
file | ≈ 4,791 |
| 10 | `InterfacesPillar` with `interfacesSaveBody`, `leafKeyOf`,
`customizeItemKey` | 488–506, 2678–2679, 2708–5581 | 2,895 | rows 1, 3,
5 · 23 hooks | `StudioDesignSurfaceBody`; 29+ test files | medium: close
to the 3,000-line mark; holds one census row | ≈ 1,898 |
| 11 | `StudioDesignSurface` / `StudioDesignSurfaceBody`, `PILLARS`,
`METADATA_AREA_TAB`, `PILLAR_FOR_SURFACE_TYPE`, publish-refusal and
backfill helpers | 423–446, 465–487, 649–661, 775–784, 867–2221 | ≈
1,425 | rows 2, 5, 7–10 · 11 hooks | `index.ts`, the console's lazy
Studio route, 51 test files | stays: this is the file | — |
Unchanged from #30002: nothing below the whole-component level of rows
7–11 moves without a logic change, and none of it is needed, because
rows 3–6 bring the file under 10,000. Serial order: rows 4–6 in any
order, then rows 7–10 only after row 5.
## What changes
- `ui/packages/app-shell/src/views/studio-design/InterfacesNavRail.tsx`
(new): row 3, base lines 663–679 and 2249–2673, each byte for byte,
under a copyright line, a module comment and the imports the two blocks
read. The block-level edits are `export` on the eleven names
`InterfacesPillar` reads: `navIcon`, `navKindName`,
`InterfacesRailMarksContext`, `InterfacesNavContributionsContext`,
`ContributedNavRows`, `NavTree`, `OFF_NAV_TYPES`, `OffNavType`,
`itemKeyOf`, `navOpenedItems`, `OffNavSections`. `KIND_ICON`,
`navTreeHoldsActive`, `NavTreeGroup` and `OffNavSection` are read only
inside the module and stay unexported. The two contexts move as they
were (`React.createContext` with the same defaults); `InterfacesPillar`
still provides them, so no logic changed.
-
`ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx`:
both blocks are gone. It imports the eleven names from the new module
beside the slice 2 import, and drops the import bindings only the rail
used (`Badge`, `Separator`, `FileText`, `BarChart3`, `Table2`, `Folder`,
`Compass`, `Link as LinkIcon`, `Puzzle`, `BookOpen`, `Minus`,
`listViewTargetOf`, `isSameSurface`, `inMenuOrder`, `isNavEntryType`,
`NavTargetLabelResolver`, `RailCustomizations`,
`NavContributionPlacement`, `TopLevelContributedNav`). Nothing in the
row was exported before, and no file outside `InterfacesPillar` reads
any of it (the other mentions are comments in `servedNavText.ts`,
`AppNavCanvas.tsx` and `StudioDesignSurface.railLabels.test.tsx`), so
there is no re-export.
- By-path readers (ruling 6597555122 item 3):
-
`ui/packages/app-shell/src/__tests__/homeRecoveryRedirectsFollowDeclaration-7373.test.ts`:
the Studio site's `alsoReads` gains `InterfacesNavRail.tsx`.
- `ui/scripts/__tests__/spec-symbol-ledger-app-shell-7265.test.ts`:
`SITES` gains `InterfacesNavRail.tsx`.
- Two test comments now name the module that holds the text they cite,
which moved in slice 2 (#30008 flagged them):
`PackagesPage.confirmFlows-11784.test.tsx` ("`PackageSwitcher`'s
eviction in `PackageSwitcher.tsx`") and
`packages-io.inflightShare-11797.test.tsx` ("the `PACKAGE_LIST_TOAST_ID`
doc in `PackageSwitcher.tsx`"). Comment text only.
- `.changeset/29988-studio-interfaces-nav-rail-module.md`:
`@object-ui/app-shell` patch, "internal: move-only".
## Tests
No behaviour changes, so no new behaviour test. The existing suites run
unchanged; results are in Verification.
## Acceptance notes
- **Readers re-grepped.** `git grep` of `StudioDesignSurface.tsx` /
`studio-design/StudioDesignSurface` over `scripts/**`, `ui/scripts/**`
and every ui test file finds the same by-path readers as slices 1–2. The
two whole-text scanners gained the new module (above).
`clickableNonControlCensus-13461` keys rows by file and handler; none of
its rows is a rail handler (the rail's clicks are on `StudioRailRow`, a
control), so it needs no edit. `openPathObjectReaders-12811` classifies
readers of `useMetadata().objects`; the rail reads none. The directory
walkers (`noRetiredSetupLink-28578`, `noSetupMetadataLink-28578`,
`CreateItemDialog.oneLabel-11787`) pick the new file up by themselves.
No gate under `scripts/**` names the file.
- **Checklist rows re-read, none moved.** Every
`StudioDesignSurface.tsx` citation under `docs/qa/` was read against the
moved blocks, and none cites rail text: the Interfaces citations name
the pillar's app load, its save into the pair and its unsaved-work
guard, and the dashboards citation quotes the file's header comment
("Interfaces: the real App navigation tree"), all of which stay. So
`docs/qa/` is untouched and no revision row is added;
`check-platform-checklist` still exits 0 full and with `--changed-from
origin/main`.
- **Serial check.** No open PR touched `StudioDesignSurface.tsx`, the
new module, either extended reader or the two commented test files when
the seat claimed (round 3 Claim) or right before this push (see
Verification). The branch was restarted from `origin/main` after #30008
merged and is rebased on `eee41490`, which touches only
`packages/spec/`.
## Verification
<details><summary>Move-only proof: byte-compare and the moved-lines
diff</summary>
```
B=ui/packages/app-shell/src/views/studio-design
git show eee4149:$B/StudioDesignSurface.tsx | sed -n 663,679p > /tmp/blockA
git show eee4149:$B/StudioDesignSurface.tsx | sed -n 2249,2673p > /tmp/blockB
sed -n 47,63p $B/InterfacesNavRail.tsx | sed 's/^export const navIcon = /const navIcon = /' | cmp /tmp/blockA -
tail -n +65 $B/InterfacesNavRail.tsx | sed -E 's/^export (function|const|type) /\1 /' | cmp /tmp/blockB -
```
Both silent (identical): block A 17 lines, sha256 `44978201e446c1bb…`;
block B 425 lines, sha256 `73d3528850a14d73…`.
`git diff --color=always --color-moved=zebra eee4149 HEAD --
ui/packages/app-shell/src/views/studio-design/`: 440 lines removed and
440 added are coloured as moved (the block lines less the eleven
`export` lines, plus import specifier lines that recur). Apart from
import lines, the new module's header comment and the `packages-io`
comment line, every line NOT coloured as moved:
```
+export const navIcon = (type?: string): LucideIcon => KIND_ICON[type ?? ''] ?? Compass;
+
+export function navKindName(node: Pick<NavNode, 'type' | 'viewName'>, locale: string): string {
+export const InterfacesRailMarksContext = React.createContext<RailCustomizations | null>(null);
+export const InterfacesNavContributionsContext = React.createContext<NavContributionPlacement>({ byGroup: new Map(), top: [] });
+export function ContributedNavRows({
+export function NavTree({
+export const OFF_NAV_TYPES = ['page', 'dashboard', 'report', 'view'] as const;
+export type OffNavType = (typeof OFF_NAV_TYPES)[number];
+export const itemKeyOf = (type: string, name: string): string => `${type}:${name}`;
+export function navOpenedItems(nodes: readonly NavNode[], locale: string): Set<string> {
+export function OffNavSections({
-const navIcon = (type?: string): LucideIcon => KIND_ICON[type ?? ''] ?? Compass;
-
-function navKindName(node: Pick<NavNode, 'type' | 'viewName'>, locale: string): string {
-const InterfacesRailMarksContext = React.createContext<RailCustomizations | null>(null);
-const InterfacesNavContributionsContext = React.createContext<NavContributionPlacement>({ byGroup: new Map(), top: [] });
-function ContributedNavRows({
-function NavTree({
-const OFF_NAV_TYPES = ['page', 'dashboard', 'report', 'view'] as const;
-type OffNavType = (typeof OFF_NAV_TYPES)[number];
-const itemKeyOf = (type: string, name: string): string => `${type}:${name}`;
-function navOpenedItems(nodes: readonly NavNode[], locale: string): Set<string> {
-function OffNavSections({
-
```
(the added blank line joins the two blocks in the new module; the
removed blank lines are the separators after each cut block).
</details>
<details><summary>Declaration files (ruling 6348891160's bar)</summary>
Base declarations emitted from the base source into a scratch `outDir`
under the lock, tree restored (`git diff HEAD` empty). Of the 701 base
`.d.ts` files, 700 are byte-identical at head,
`StudioDesignSurface.d.ts` included (sha256 `d0f9665b1e9ed6fc…`, the
same as at slice 2): the rail was never exported, so no declaration
moved. The one difference is `chrome/ErrorBoundary.d.ts`, where the
members of the `render()` return union print in another order; with
every union's members sorted (the ruling's normalisation) the two files
are identical. The one addition is
`views/studio-design/InterfacesNavRail.d.ts` / `.js`, reachable from no
entry in the package's `exports` map (`"."` only). Declared-surface
digest of `dist/index.d.ts` plus `StudioDesignSurface.d.ts`, root path
masked: 245 exports, `ce63368a1cf8becf…` on both sides.
</details>
<details><summary>Build, type-check, lint and tests (head 376a48cf; the
rebased head 36954c6 has a byte-identical ui/ and changeset, checked
with git diff --quiet)</summary>
All under scripts/pm/os-verify-lock.sh, one locked chain: VERDICT
command-exit 0.
- npx turbo run build --filter='@object-ui/app-shell...'
--concurrency=2: exit 0.
- pnpm --dir ui --filter @object-ui/app-shell type-check (tsc --noEmit,
then tsconfig.test.json): exit 0.
- pnpm --dir ui run type-check:scripts: exit 0.
- npx eslint on the two source files, the 7373 test and
PackagesPage.confirmFlows-11784, from packages/app-shell: exit 0.
- pnpm --dir ui exec vitest run --maxWorkers=3
packages/app-shell/src/views/studio-design/ plus the 7373 test,
PermissionMatrixEditor.saveSurface-12725,
builtinDesigners.lateArrival-11939, observableRegistries-11939,
PackagesPage.confirmFlows-11784, clickableNonControlCensus-13461 and
spec-symbol-ledger-app-shell-7265: exit 0, Test Files 329 passed (329),
Tests 2446 passed (2446).
- Narrowed, and declared: the full @object-ui/app-shell suite does not
finish locally on the shared box; the run covers every test in the moved
file's directory and every test outside it that imports the file, reads
it by path or names the moved rail. CI's Test UI runs the rest.
- node scripts/check-platform-checklist.mjs: exit 0; with --changed-from
origin/main: exit 0.
</details>
<details><summary>Ablation of the two extended readers (one-time,
through scripts/ablation-replace.mjs; tree restored, git diff HEAD
empty)</summary>
The moved rail holds neither reader's needle, so each leg plants one in
the new module and then takes the module out of the reader's list.
| Leg | Mutation | Expected | Observed |
|---|---|---|---|
| B0 | none: the 7373 test as in this PR | green | exit 0: 15 passed |
| B1 | a `'/home'` literal planted in InterfacesNavRail.tsx, alsoReads
as in this PR | red on the Studio site | exit 1: "does not hard-code the
launcher path" fails for the Studio site; 1 failed, 14 passed |
| B1b | same mutation, InterfacesNavRail.tsx removed from the Studio
site's alsoReads | green, i.e. blind | exit 0: 15 passed |
| B2 | a local interface ObjectLike added to InterfacesNavRail.tsx,
SITES as in this PR | red | exit 1: "rule 1 finds no spec-named local
declaration in ...InterfacesNavRail.tsx" fails; 1 failed, 21 passed |
| B2b | same mutation, the InterfacesNavRail.tsx SITES row removed |
green, i.e. blind | exit 0: 21 passed |
Each mutation's landing was proven by the tool (anchor count 1 to 0,
blob hashes) and by a grep count of the changed text.
</details>
<details><summary>Derived gates: node scripts/pm/dispatch-gates.mjs
--commands, run at b4129e71, whose tree head 36954c6 keeps (the amend
rewrote only the commit trailer) (35 commands)</summary>
```
node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-empty-changeset.mjs --base origin/main :: exit 0
node scripts/check-empty-changeset.mjs --self-test :: exit 0
node scripts/check-sdui-manifest.mjs :: exit 0
node scripts/check-sdui-manifest.mjs --self-test :: exit 0
node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0
node scripts/release-pending-publish.mjs --self-test :: exit 0
pnpm --dir ui exec vitest run scripts/ :: exit 0
pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:closed-ceremony-text :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:dispatcher-error-vocabulary :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:engine-double-contract :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:published-files :: exit 0
pnpm check:query-options-erasure :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:registry-log-declared :: exit 0
pnpm check:rest-log-spy-declared :: exit 0
pnpm check:skill-identifier-liveness :: exit 0
pnpm check:test-source-alias :: exit 0
pnpm check:tier-file-adoption :: exit 0
pnpm check:type-check-coverage :: exit 0
pnpm check:type-check-debt :: exit 0
pnpm check:undeclared-dep-imports :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:where-matcher :: exit 0
```
node scripts/pm/dispatch-gates.mjs --ran: "35 derived famil(ies)
accounted for — 35 run, 0 NOT-MEASURED". No docs/adr or docs/qa path
changed, so the ADR-link and platform-checklist families are not derived
this time; check-platform-checklist was still run by hand (Build,
type-check, lint and tests, above).
</details>
<details><summary>Serial check before push</summary>
The 17 open PRs at 2026-11-04T05:50Z: each PR's changed files were read
(seven PRs from their fetched heads against `origin/main`, three-dot;
#29979, #29866 and the eight `pm:mailbox` PRs through the PR file list
or their fetched head; every mailbox PR lists no file). None touches
StudioDesignSurface.tsx, InterfacesNavRail.tsx,
homeRecoveryRedirectsFollowDeclaration-7373.test.ts,
spec-symbol-ledger-app-shell-7265.test.ts,
PackagesPage.confirmFlows-11784.test.tsx or
packages-io.inflightShare-11797.test.tsx.
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_01WGufzedfGwVcebpN5t3ETE)_
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01WGufzedfGwVcebpN5t3ETE
Co-authored-by: Claude <noreply@anthropic.com>1 parent eee4149 commit 9d1a465
7 files changed
Lines changed: 518 additions & 465 deletions
File tree
- .changeset
- ui
- packages/app-shell/src
- __tests__
- views
- metadata-admin
- studio-design
- scripts/__tests__
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
139 | 139 | | |
140 | 140 | | |
141 | 141 | | |
| 142 | + | |
142 | 143 | | |
143 | 144 | | |
144 | 145 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
392 | 392 | | |
393 | 393 | | |
394 | 394 | | |
395 | | - | |
| 395 | + | |
396 | 396 | | |
397 | 397 | | |
398 | 398 | | |
| |||
0 commit comments