Skip to content

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

Closed

Description

@objectstack-fleet

Part of the record-page-v2 epic #26676 (child 1 of 4).

Source (origin/main 416a76030e)

  • packages/spec/src/ui/view.zod.ts:2420 NavigationModeSchema = z.enum(['page', 'drawer', 'modal', 'split', 'popover', 'new_window', 'none']), and :2467 mode: NavigationModeSchema.default('page').
  • packages/spec/src/ui/page.zod.ts:1069 recordAction: 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-190 offers Modal as an option (:187).
  • The console guesses: ui/packages/app-shell/src/views/objectViewRecordNavigation.ts resolves an un-authored view to deriveRecordSurface(objectDef) === 'page' ? { mode: 'page' } : { mode: 'drawer', … } (field-heavy objects page by themselves); ui/packages/react/src/hooks/useNavigationOverlay.ts:364 const mode: NavigationMode = navigation?.mode ?? 'page' (the hook's own fallback is page) and :366 isOverlay = 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 in ui/packages/plugin-view/src/ObjectView.tsx:1661 (schema.layout || deriveRecordSurface(...)) and is documented in ui/content/docs/plugins/plugin-view.mdx:301, :356 and ui/packages/plugin-view/README.md:91, :423; ui/packages/types/src/objectql.ts:3336 export type ViewNavigationConfig = NavigationConfig is an alias of the spec type, so the narrowing reds Type Check UI by itself; ui/packages/app-shell/src/console/appObjectScope.ts:274 IN_PAGE_RECORD_OVERLAYS = new Set(['modal', 'split', 'popover']); ObjectView.tsx:1010 / :1133 draw the split face from navOverlay.mode === 'split'; plugin-calendar, plugin-kanban, plugin-gantt honour the authored mode through NavigationOverlay (its docblock in ui/packages/plugin-detail/src/RecordDetailDrawer.tsx says so).
  • HotCRM on 1f3f8880 declares mode: 'page' (lead) and mode: 'drawer' (opportunity, task, case) — none of the retired values; no sibling card is needed there.

Target

  1. NavigationModeSchema is ['drawer', 'page', 'new_window', 'none'], default drawer; modal, split, popover are retired per the spec-property-retirement skill (tombstone through retiredKey() in packages/spec/src/shared/retired-key.ts with the prescription "delete the value; drawer is the default, declare page only 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-base baseline; forms, docs and skills text).
  2. InterfacePageConfigSchema.recordAction is ['drawer', 'page', 'none'], same kit, and page.form.ts drops the Modal option.
  3. The console's resolution order becomes: the authored page wins; drawer otherwise — the deriveRecordSurface field-count guess is retired from the list → record path in objectViewRecordNavigation.ts:81-83 and in plugin-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 at useNavigationOverlay.ts:364 becomes drawer to match the schema default and :366 is trimmed; IN_PAGE_RECORD_OVERLAYS shrinks accordingly. NavigationOverlay (the component) is mounted by nine callers (InterfaceListPage, ObjectDataPage, ObjectView, objectViewRecordOverlay, ObjectGrid, ObjectCalendar, ObjectGantt, ObjectKanban, RecordDetailDrawer): a modal / popover face 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.
  4. The split pane survives as a per-user preference: a small app-shell util (recordOpenPreference — 'drawer' | 'split', default drawer, stored in localStorage) that ObjectView reads in place of navOverlay.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 declaring page shows 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.
  5. AGENTS.md Post-Task Checklist step 4: a removal that reds ui/ ships with the ui/ fix in the same PR — git grep the three values under ui/ before pushing; this card's ui/ 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

  1. safeParse of navigation: { mode: 'split' }, 'modal', 'popover' and recordAction: 'modal' fail with the tombstone prescription (assert code + status + the first sentence of the message); { mode: 'drawer' }, { mode: 'page' }, an absent mode parse, and the parsed default is drawer.
  2. 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).
  3. An object or view declaring page still navigates to the record route; new_window and none unchanged.
  4. With the preference util set to split, ObjectView draws the split face; default draws the drawer.
  5. check:authorable-surface, check:api-surface and the spec retirement gates green; Type Check UI green on the same PR.

Rulings (maintainer, 2026-10-27, chat in the filing session — verbatim, not re-decidable here)

  1. The model, quoted back by the maintainer and ordered built: 「1. 一个对象一个默认打开方式:默认抽屉(带展开整页),page 只给显式声明的对象,modal 退役,split / popover 变成用户个人偏好;抽屉和整页是同一张页在两个宽度。」
  2. On the drawer's top bar: 「左上角的 任务 · 2 / 2 · 分屏 几个字有必要嘛」 — no object name, no mode label on the bar.
  3. On the position counter: 「我觉得记录详情页显示第几条记录 好像也没必要」 — no "n / N" anywhere on a record; the ↑ ↓ previous / next controls stay.
  4. On the right pane: 「右侧应该可以拖动吗?展开为整页那个是不是用图标就可以」 — the pane is drag-resizable and "open as full page" is an icon-only control with a tooltip.
  5. 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

  1. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 (epic #26676, child 1 of 4)
    Session: session_cdb7a717-def7-4e71-9988-f089fbaa18ff
    Account: zhuangjianguo
    Branch: claude/issue-26677-navigation-mode-retirement
    Worktree: objectstack-issue-26677
    Domain: domain:spec
    Seat: 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.tsx and its two doc pages, ui/packages/components/src/custom/navigation-overlay.tsx only 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: xhigh
    Clause-②: no
    Metadata-protocol: narrowed
    Responsibility: n/a — not a defect card
    Thread-read: none
    Premise: holds (read-only premise check on origin/main 416a76030e folded into the body before filing; re-read against 8222fa805d: 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-dev subagent 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

  2. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    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 carry Clause-②: no (narrowing) as the dispatch states, because the level gate refuses Metadata-protocol: narrowed beside 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"
    ]
    }

  3. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Review recorded on the PR: #26804 ## 席位核对 — ACCEPT at head eb20d22070 with the in-seat contract review PASS. Tier H landing: the PR carries needs-user-decision and a review request to the governed approvers; pm:dispatched stays until MERGED; the card closes by the PR's Fixes line. Children #26679 and #26681 are dispatched after this PR lands (shared files, the preference util). — epic PM session_cdb7a717-def7-4e71-9988-f089fbaa18ff, 2026-10-27T17:24Z


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Landing stroke on #26804 (Tier H): APPROVED by os-zhuang on head eb20d22070 at 2026-10-27T22:49Z; pre-landing readings clean (mergeable_state clean, 60 success / 11 skipped check-runs on that head); needs-user-decision cleared 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's Fixes line. — epic PM session_cdb7a717-def7-4e71-9988-f089fbaa18ff, 2026-10-27T23:09Z


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:recordsBusiness objects, records, the views that show data, usable forms, searchdomain:specenhancementNew feature or requestpm:epicpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions