Repository navigation
spec(+ui): navigation.mode keeps page | drawer | new_window | none — modal, split and popover are retired; the default becomes drawer in the schema and in the console (no field-count guess); list-page recordAction drops modal; the console's split pane becomes a per-user preference read #26677
Description
Part of the record-page-v2 epic #26676 (child 1 of 4).
Source (origin/main 416a76030e)
packages/spec/src/ui/view.zod.ts:2420NavigationModeSchema = z.enum(['page', 'drawer', 'modal', 'split', 'popover', 'new_window', 'none']), and:2467mode: NavigationModeSchema.default('page').packages/spec/src/ui/page.zod.ts:1069recordAction: z.enum(['drawer', 'page', 'modal', 'none'])(describe says "Default: drawer"), reached also through the aliases at:1016(onRecordClick,rowAction,openIn);packages/spec/src/ui/page.form.ts:182-190offersModalas an option (:187).- The console guesses:
ui/packages/app-shell/src/views/objectViewRecordNavigation.tsresolves an un-authored view toderiveRecordSurface(objectDef) === 'page' ? { mode: 'page' } : { mode: 'drawer', … }(field-heavy objects page by themselves);ui/packages/react/src/hooks/useNavigationOverlay.ts:364const mode: NavigationMode = navigation?.mode ?? 'page'(the hook's own fallback ispage) and:366isOverlay = mode === 'drawer' || mode === 'modal' || mode === 'split' || mode === 'popover'— the hook has no other per-mode branch for the three values; the same guess also sits inui/packages/plugin-view/src/ObjectView.tsx:1661(schema.layout || deriveRecordSurface(...)) and is documented inui/content/docs/plugins/plugin-view.mdx:301,:356andui/packages/plugin-view/README.md:91,:423;ui/packages/types/src/objectql.ts:3336export type ViewNavigationConfig = NavigationConfigis an alias of the spec type, so the narrowing redsType Check UIby itself;ui/packages/app-shell/src/console/appObjectScope.ts:274IN_PAGE_RECORD_OVERLAYS = new Set(['modal', 'split', 'popover']);ObjectView.tsx:1010/:1133draw the split face fromnavOverlay.mode === 'split';plugin-calendar,plugin-kanban,plugin-gantthonour the authored mode throughNavigationOverlay(its docblock inui/packages/plugin-detail/src/RecordDetailDrawer.tsxsays so). - HotCRM on
1f3f8880declaresmode: 'page'(lead) andmode: 'drawer'(opportunity, task, case) — none of the retired values; no sibling card is needed there.
Target
NavigationModeSchemais['drawer', 'page', 'new_window', 'none'], defaultdrawer;modal,split,popoverare retired per thespec-property-retirementskill (tombstone throughretiredKey()inpackages/spec/src/shared/retired-key.tswith the prescription "delete the value;draweris the default, declarepageonly for an object whose record should open as a full page; a side-by-side pane is a reader's own preference in the list's Display menu, not metadata"; ADR-0087 disposition in the changeset;gen:authorable-surface-basebaseline; forms, docs and skills text).InterfacePageConfigSchema.recordActionis['drawer', 'page', 'none'], same kit, andpage.form.tsdrops theModaloption.- The console's resolution order becomes: the authored
pagewins;drawerotherwise — thederiveRecordSurfacefield-count guess is retired from the list → record path inobjectViewRecordNavigation.ts:81-83and inplugin-view/src/ObjectView.tsx:1661, and the two doc pages stop teaching it (mobile still pages, that is viewport, not declaration). The hook's fallback atuseNavigationOverlay.ts:364becomesdrawerto match the schema default and:366is trimmed;IN_PAGE_RECORD_OVERLAYSshrinks accordingly.NavigationOverlay(the component) is mounted by nine callers (InterfaceListPage, ObjectDataPage, ObjectView, objectViewRecordOverlay, ObjectGrid, ObjectCalendar, ObjectGantt, ObjectKanban, RecordDetailDrawer): amodal/popoverface that no caller can reach once the values are gone from metadata is deleted; a face still reached by a non-metadata caller stays — measure the callers, do not delete by name. Out of scope: the record FORM's modal-vs-page decision (recordFormNavigation.ts,AppContent.tsx:889) is a different surface and stays as it is. - The split pane survives as a per-user preference: a small app-shell util (
recordOpenPreference—'drawer' | 'split', defaultdrawer, stored inlocalStorage) thatObjectViewreads in place ofnavOverlay.mode === 'split'; the Display-menu control that writes it is console list Display menu: 「打开记录」 (drawer | split) as a per-user preference; an object or view declaringpageshows the declaration instead; the name cell's hover ↗ opens the record as a full page #26681, so this card ships the read side and a test, not the control. - AGENTS.md Post-Task Checklist step 4: a removal that reds
ui/ships with theui/fix in the same PR —git grepthe three values underui/before pushing; this card'sui/fix is authorized by ruling 1.
File surface
packages/spec/src/ui/view.zod.ts, page.zod.ts, page.form.ts, the retirement kit files (re-read the kit: dbf7e04adf of 2026-10-27 changed how kits age out), spec tests; ui/packages/react/src/hooks/useNavigationOverlay.ts; ui/packages/app-shell/src/views/{ObjectView,objectViewRecordNavigation,objectViewRecordOverlay}.tsx|ts, ui/packages/app-shell/src/console/appObjectScope.ts; ui/packages/plugin-view/src/ObjectView.tsx and its deriveRecordSurface docs (ui/content/docs/plugins/plugin-view.mdx, ui/packages/plugin-view/README.md); ui/packages/components/src/custom/navigation-overlay.tsx only for faces measured unreachable; plugin-grid / plugin-calendar / plugin-kanban / plugin-gantt navigation reads; tests beside; changesets (narrowed, BREAKING with migration).
Acceptance
safeParseofnavigation: { mode: 'split' },'modal','popover'andrecordAction: 'modal'fail with the tombstone prescription (assertcode+status+ the first sentence of the message);{ mode: 'drawer' },{ mode: 'page' }, an absentmodeparse, and the parsed default isdrawer.- A list of an object with no authored navigation opens a record in the drawer whatever its field count (the pin that asserted the field-count page is flipped and carries the new semantics, not just deleted).
- An object or view declaring
pagestill navigates to the record route;new_windowandnoneunchanged. - With the preference util set to
split,ObjectViewdraws the split face; default draws the drawer. check:authorable-surface,check:api-surfaceand the spec retirement gates green;Type Check UIgreen on the same PR.
Rulings (maintainer, 2026-10-27, chat in the filing session — verbatim, not re-decidable here)
- The model, quoted back by the maintainer and ordered built: 「1. 一个对象一个默认打开方式:默认抽屉(带展开整页),
page只给显式声明的对象,modal退役,split / popover 变成用户个人偏好;抽屉和整页是同一张页在两个宽度。」 - On the drawer's top bar: 「左上角的 任务 · 2 / 2 · 分屏 几个字有必要嘛」 — no object name, no mode label on the bar.
- On the position counter: 「我觉得记录详情页显示第几条记录 好像也没必要」 — no "n / N" anywhere on a record; the ↑ ↓ previous / next controls stay.
- On the right pane: 「右侧应该可以拖动吗?展开为整页那个是不是用图标就可以」 — the pane is drag-resizable and "open as full page" is an icon-only control with a tooltip.
- On related records, the maintainer confirmed the in-place model: 「所以关联记录是一层一层的往下点,然后可以一层一层的反悔吗」 — one level down per click, one level back per ← / Esc, never a second drawer.
Filed by the epic PM session session_cdb7a717-def7-4e71-9988-f089fbaa18ff on 2026-10-27, from the record-page / list-page assessment and the clickable prototype the maintainer walked and ruled on in that session (objectstack main 416a76030e, HotCRM 1f3f8880 on platform 18.2.0). The maintainer's instruction to build it, verbatim: 「你直接立卡派发处理这个开发,然后审核。」 Design evidence (never merged): branch claude/record-page-v2-evidence-20261027b at commit 345ec777220843ce1fd7f95b6cc0b2b6dcc81bf8, folder docs/design/record-page-v2-2026-10-27/ (prototype.html, artboards/, assessment.md).
Activity
objectstack-fleet commentedon Oct 27, 2026
Claim: PM loop round 1 (epic #26676, child 1 of 4)
Session:
session_cdb7a717-def7-4e71-9988-f089fbaa18ffAccount:
zhuangjianguoBranch:
claude/issue-26677-navigation-mode-retirementWorktree:
objectstack-issue-26677Domain:
domain:specSeat: epic #26676 (the epic PM session, not a domain seat; the spec child is claimed from this session on the maintainer's instruction quoted in the card footer, 「你直接立卡派发处理这个开发,然后审核。」, chat 2026-10-27)
File surface:
packages/spec/src/ui/{view.zod,page.zod,page.form}.ts+ the retirement kit files,ui/packages/react/src/hooks/useNavigationOverlay.ts,ui/packages/app-shell/src/views/{ObjectView,objectViewRecordNavigation,objectViewRecordOverlay}.*,ui/packages/app-shell/src/console/appObjectScope.ts,ui/packages/plugin-view/src/ObjectView.tsxand its two doc pages,ui/packages/components/src/custom/navigation-overlay.tsxonly for faces measured unreachable, plugin-grid / calendar / kanban / gantt navigation reads, tests beside, changesets (stop on breach; explain in the report)Container & model:
L,mode:subagent(this seat is a desktop session on the maintainer's macOS, not a cloud container: the build-heavy spec+ui card runs here as the only build-heavy subagent at a time, verify lock in declared-unlocked mode),model: TIER_DEFAULT(dispatch-gates --tier 2026-10-27T12:46Z: no path-derived mandate;packages/spec/src/**is a clause-② SUSPECT surface → in-seat contract review at CONTRACT_REVIEW_TIER before the queue, and this seat runs at that tier),effort: xhighClause-②: no
Metadata-protocol: narrowed
Responsibility: n/a — not a defect card
Thread-read: none
Premise: holds (read-only premise check on origin/main
416a76030efolded into the body before filing; re-read against8222fa805d: no batch surface touched)Serial constraints cleared: #26598 (draft, chore ui-merge leftovers) touches
packages/spec/src/ui/page.zod.ts— merge main before opening the PR and again if it lands first; #26637 / #26613 (domain:ui seat 4) edit plugin-list only; #26678 (this epic, in parallel) edits plugin-detail synth / DetailTabs / RecordDetailView, disjoint; no other open PR touches the surface (scan 2026-10-27T12:46Z)Dispatched at 2026-10-27T12:46Z as an
os-devsubagent of this session; the dispatch order lives in the subagent's opening prompt, its three sections (rulings / PM mechanism assumptions / suggested route) are summarized in the card body's Target and Rulings.Generated by Claude Code
objectstack-fleet commentedon Oct 27, 2026
os-dev-report
{
"issue": 26677,
"status": "done",
"branch": "claude/issue-26677-navigation-mode-retirement",
"pr": "#26804",
"session": "session_cdb7a717-def7-4e71-9988-f089fbaa18ff (the dispatching PM session; this run is its os-dev subagent)",
"premise_still_valid": true,
"summary": "Spec: NavigationModeSchema is drawer | page | new_window | none via enumWithRetiredValues (modal / split / popover refused by name with a prescription), NavigationConfigSchema.mode defaults to drawer, InterfacePageConfigSchema.recordAction is drawer | page | none, page.form.ts drops Modal; value-level retirement kit: D2 conversion navigation-mode-overlay-values-removed (retiredFromLoadPath, retiredAfter 18.2.0, reaches view list payloads, object listViews, the eight page list blocks and interfaceConfig.recordAction), D3 ui-record-open-overlay-modes-retired, STEP18 rationale fragment, DEFAULT_CHANGES_BY_MAJOR row, removal-version JOIN row, changeset (minor, BREAKING, registered disposition). Console: useNavigationOverlay falls back to drawer and treats drawer as the one overlay; NavigationOverlay loses its modal and popover faces plus popoverTrigger / popoverAnchorRef / useOverlayAnchor (measured unreachable); five renderers drop split and popover plumbing; the field-count guess leaves objectViewRecordNavigation.ts and plugin-view ObjectView.tsx (mobile still pages in plugin-view); IN_PAGE_RECORD_OVERLAYS removed; split becomes the reader preference app-shell/src/utils/recordOpenPreference.ts (os:record-open-mode) read by the console ObjectView only for a drawer-mode record. Every pin that rested on the old default or the retired faces was flipped to assert the new semantics; two plugin-view split-heading test files were deleted with their face; docs, registry descriptions, sdui.manifest.json and the objectstack-ui skill bullet updated. PM mechanism assumption 3 was off by count: 19 NavigationOverlay mounts, not nine.",
"tests": "spec: pnpm --filter @objectstack/spec test 730 files passed, 1 failed (check-liveness.test.ts JSON truncated near 64 KB, macOS async pipe, known local-only); test:repo 79 passed, 1 failed (build-schemas-check-mode generation stamp, known local-only); typecheck exit 0. New pin packages/spec/src/ui/navigation-mode-retirement.test.ts 29 tests green (repo project). UI: turbo type-check over 14 packages 77/77 tasks; vitest packages/app-shell/ 1759 files / 16173 tests passed; twelve other touched packages 1799 files passed + 10 failures, all fixed and re-run green except 3 git grep -E word-boundary LIT CONTROLs (BSD regex, local-only); post-merge rerun of all changed ui test files + plugin-grid/ + types tests 674 passed with the same 3. Ablations (committed first, scripts/ablation-replace.mjs, restored to the HEAD blob, git diff HEAD empty): enumWithRetiredValues to z.enum reds 12 prescription rows; default('drawer') to default('page') reds 2; hook ?? 'drawer' to ?? 'page' reds 4 hook rows. Reverse verification: the first ui type-check read TS2322 'modal' not assignable to the four-member union. e2e toast-under-modal NOT MEASURED locally (cached Playwright build 1234, runner wants 1243, nothing installed); CI Build & E2E UI green.",
"gates": "Local derived union at eb20d22 (node scripts/pm/dispatch-gates.mjs --commands, 125 families) run with exit codes captured to files before any pipe; --ran reconciliation: 125 derived, 125 run, 0 NOT-MEASURED (derived zero). 123 exit 0; 2 exit 1, both local-env: pnpm check:bash32-floor (self-test 7 of 179 cases, host bash 3.2.57 cannot parse bash 4 probes; real tree 0 findings) and pnpm --dir ui exec vitest run scripts/ (check-i18n-dead-keys timed out under load, re-run with --testTimeout 140/140 pass; select-affected-tests-wiring needs mapfile, absent in bash 3.2). Extra ui checks exit 0: check:spec-symbols, check:doc-example-readers, check:unreferenced-sources, type-check:coverage, check:handler-key-reads, check:phantom-deps; ui eslint on changed files 0 errors. CI check-runs at eb20d22: 62 runs, 0 non-success; required nine all success: Lint & Repo Gates, TypeScript Type Check, Test Core, Build Core, Governed Surface Queue Guard, Lint UI, Type Check UI, Test UI, Build & E2E UI. All verification ran in DECLARED UNLOCKED MODE (no flock on macOS), disclosed in the PR body.",
"line_budget": "skills/objectstack-ui/rules/navigation.md 162 -> 162; skills/objectstack-ui/references/react-blocks.md (generated) 115 -> 115; all skills SKILL.md 4606 -> 4606; skills/objectstack-ui every file 2259 -> 2259. No re-wrap, no growth.",
"files_changed": "110 files, +2134 / -1920 (git diff --shortstat origin/main...HEAD at eb20d22): packages/spec (view.zod.ts, page.zod.ts, page.form.ts, component.zod.ts, react-blocks.ts, conversions/registry.ts, migrations/registry.ts.template, migrations/entries/semantic/18.ui-record-open-overlay-modes-retired.ts, scripts/lib/default-changes.ts, authorable-defaults/ui.json, vitest.repo-tests.json, two tests), content/docs/references (5 generated), skills/objectstack-ui (2), .changeset/26677-navigation-mode-retirement.md, sdui.manifest.json + scripts/sdui-manifest.record.json, ui/ (react hook, components overlay, 8 plugins, plugin-view, app-shell incl. new utils/recordOpenPreference.ts, types, docs and READMEs, e2e/toast-under-modal.spec.ts, tests).",
"deviations": [
"PR opened through scripts/pm/with-fleet.sh direct (gh api POST /pulls draft, then POST /issues/26804/assignees) because fleet-write/dispatch.mjs refused the local session id (SESSION_SHAPE ^session_[A-Za-z0-9]{6,}$ rejects the hyphenated id), the fallback the dispatch named.",
"Clause-② line: the claim reads
Clause-②: no; the PR body and the changeset carryClause-②: no (narrowing)as the dispatch states, because the level gate refusesMetadata-protocol: narrowedbeside a line with no (narrowing) arm (scripts/check-adr-0087-registration.mjs docblock). os-dev.md says copy the claim's line; the conflict is reported, not silently resolved.","Worktree at /Users/zhuangjianguo/Documents/GitHub/objectstack-issue-26677 (sibling of the main checkout, per AGENTS.md) rather than ../ from the PM's nested worktree.",
"Suggested-route downstream sweep pnpm --filter '...@objectstack/spec' typecheck not run; replaced by a source census (no package outside ui/ names a retired member, control 39 hits) and CI TypeScript Type Check (success).",
"Zero label writes: the dispatch named no label and skip-changeset does not apply (a changeset ships).",
"Two main merges into the branch (clean, then one conflict in ui/packages/types/src/zod/objectql.zod.ts resolved by keeping both sides); the os-regen deferral was discharged by a regeneration commit.",
"Diff is 4054 changed lines and touches skills/**: Tier H landing; the maintainer quick-read section is included with the seat-opinion field left empty."
],
"mcp_calls": "0",
"api_writes": "3 — POST /repos/objectstack-ai/objectstack/pulls (draft #26804); POST /repos//issues/26804/assignees (zhuangjianguo); POST /repos//issues/26677/comments (this report, via post-stamped). git push not counted.",
"open_questions": [
{
"question": "The retired-member prescriptions (and the D3 entry) say a side-by-side pane is a reader's preference 'in the list's Display menu', the card's own wording; that control is #26681. Keep the clause if #26681 lands in the same release, or drop it?",
"options": [
"A keep it and land #26681 before 18.3.0 publishes",
"B drop the Display-menu clause now (one-line edit in view.zod.ts, the D3 entry and the changeset)"
],
"recommendation": "A, because the card prescribes that wording and #26681 is the same epic; B only if #26681 slips past the release cut."
}
],
"out_of_scope_findings": [
"carrier: #26681 · the prescription names the Display-menu control #26681 builds; noted in Acceptance notes, not filed",
"carrier: none · macOS-only test-infra failures (git grep -E word-boundary LIT CONTROLs in ui/packages/types, check-liveness pipe truncation, build-schemas generation stamp, bash 3.2 probes in check:bash32-floor and select-affected-tests-wiring); green in CI; Acceptance notes only",
"carrier: none · useNavigationOverlay absent-navigation branch still hands a click to the host onNavigate (documented as how a host withholds navigation); Acceptance notes only"
]
}
objectstack-fleet commentedon Oct 27, 2026
Review recorded on the PR: #26804
## 席位核对— ACCEPT at headeb20d22070with the in-seat contract review PASS. Tier H landing: the PR carriesneeds-user-decisionand a review request to the governed approvers;pm:dispatchedstays until MERGED; the card closes by the PR'sFixesline. Children #26679 and #26681 are dispatched after this PR lands (shared files, the preference util). — epic PMsession_cdb7a717-def7-4e71-9988-f089fbaa18ff, 2026-10-27T17:24ZGenerated by Claude Code
objectstack-fleet commentedon Oct 27, 2026
Landing stroke on #26804 (Tier H): APPROVED by
os-zhuangon headeb20d22070at 2026-10-27T22:49Z; pre-landing readings clean (mergeable_state clean, 60 success / 11 skipped check-runs on that head);needs-user-decisioncleared and auto-merge enabled by the seat on the maintainer's authorization in the epic session, verbatim: 「#26804 我自己批,批完你落地」. The queue decides the rest; this card closes by the PR'sFixesline. — epic PMsession_cdb7a717-def7-4e71-9988-f089fbaa18ff, 2026-10-27T23:09ZGenerated by Claude Code