Repository navigation
console record page: the related column loads counts first, then the first three rows of only the visible cards, and never blocks the details — one batched request, viewport-lazy cards, a measured budget #27056
Description
epic:#26676#1 · zhuangjianguo · session_cdb7a717def74e719988f089fbaa18ff
Unblocked 2026-10-29T09:06Z — upstream #27055 closed (the record page v3 layout and its round 2 are on main at 273137d227; #27053's tabs retirement and #26679's drawer are there too). Back in the ui lane queue; re-read the card's Source against current main at dispatch — the v3 files it names moved in PR #27310 / #27473.
Part of the record-page-v3 epic #26676 (v3 child 3 of 8). Runs after #27055 (the compact cards it loads).
Source (origin/main 7bb8084590)
Every record:related_list node mounts RelatedList, which runs its own query on mount (pageSize default 10, the child's list columns); the column tier (#26758) mounts all of them at once, so a record with N related lists opens with N concurrent queries racing the record's own read (ruling 3). The folded rail in the two-column tier reads nothing until opened.
Target
- Counts first: on open, ONE request answers the count of every related list for this parent (one aggregate per child object's FK, batched through the data source's batch door if main has one, else the
aggregateAPI per list in onePromise.allbehind a single loading state — measure which exists). Card headers render from counts; a count of 0 folds the card to one line and skips its rows. - Rows on demand: a card fetches at most 3 rows (
limit) with a projection of the name field plus one status / date field (fromrelatedListColumnswhen declared, else the first status-like or date field), ordered by the inherited sort; only cards in the viewport fetch (IntersectionObserver),primarycards first; a folded 「动态」 card fetches nothing until opened. - Details first: the details column renders from the record read alone; the related column shows skeleton cards until counts land, then rows; the page's interactive moment does not wait on related data.
- Prefetch on step: in the drawer, ↑ ↓ (console record drawer v2: the bar is ↑ ↓ · ⤢ · ✕ (no object name, no counter); a related row or a lookup opens its record in place with a trail, ← / Esc go back one level; ⤢ carries the trail into the page; on the record page a related row opens a drawer over the page #26679) prefetches the next record's counts.
- Budget, measured in the PR body on HotCRM (fresh DB, an account with ≥ 8 related lists): details interactive ≤ 300 ms after the record read returns; all card headers ≤ 500 ms; total related requests on open ≤ 2 round trips. Numbers from the browser's network panel, pasted.
File surface
ui/packages/plugin-detail/src/RelatedList.tsx (lazy + limit + projection), relatedListCompact.ts, a related-counts hook beside it, ui/packages/app-shell/src/views/RecordDetailView.tsx (the counts request and skeleton gating), the data-source batch call if present (ui/packages/data-objectstack), tests beside, changesets; ⛔ packages/spec/** (no new key: limit is an existing node prop).
Acceptance
- Tests: a record with 6 related lists issues one counts call and rows only for the 2 cards in view; a 0-count card never queries rows; the details render before any related response resolves.
- The measured budget table in the PR body; screenshots not required.
Rulings (maintainer, chat in the filing session, 2026-10-28 — verbatim, not re-decidable here)
- Record-page model (2026-10-27): 「1. 一个对象一个默认打开方式:默认抽屉(带展开整页),
page只给显式声明的对象,modal退役,split / popover 变成用户个人偏好;抽屉和整页是同一张页在两个宽度。」 - On the resident timeline column: 「时间线是不是太突出了,这个只适合部分对象吧?适合做所有业务对象的默认布局吗」 and then 「时间线是否显示,是否可以切换,比如当前版本的 Claude code,顶部有很多切换图标」 — the column's presence is a per-user toggle with an object-derived default, never metadata.
- On the related column's cost: 「关联都放在右侧,记录打开时是不是会很慢,因为需要加载所有的关联子表。」 — the related column loads counts first, rows on demand, and never blocks the details.
- On mobile: 「还要考虑手机端的效果。」
- On the definition surfaces: the PM's keep / narrow / retire table was put to the maintainer (keep object roles, relationship flags, six slotted slots and
kind: full; retire thetabsslot and the deadBuildPageOptionshints; fold the quick-look path into the one record page) and approved with the instruction in the footer.
Filed by the epic PM session session_cdb7a717-def7-4e71-9988-f089fbaa18ff on 2026-10-28 under epic #26676 (record page v2 → v3), from the record-page definition-surface inventory on objectstack main 7bb8084590 and the maintainer's rulings in that session. The maintainer's instruction to file and dispatch, verbatim: 「同意,以上界面相关的需要我确认的你负责立卡派发,如果有可能可以启动云会话,开发完你来确认。」 Design evidence: the design canvas boards 「记录页 v3」 row (RecordPageV3, RecordPageV3NoTimeline, RecordPageRef, RecordDrawerV3, RecordMobile) — exported to the never-merged branch named in the epic parent once this batch is filed.
Activity
objectstack-fleet commentedon Oct 29, 2026
epic:#26676#1 · zhuangjianguo · session_cdb7a717def74e719988f089fbaa18ff
Unlock scan 2026-10-29T09:07Z: every
Blocked-by:upstream of this card is closed — #27055 by PR #27310 (merged by the maintainer) and PR #27473 (the seat landed it through the queue), #26679 by PR #27038, #27053 by PR #27219. Statepm:blocked→pm:queue(one replace), theBlocked-by:lines replaced by the unblocked line in the body. The card's file surface is re-read againstmain273137d227: the record page v3 layout (frameless sections, the merged timeline stream, compact related cards with the reader's arrangement, body tables, in-place enlarge, the segmented panel control in the header, the 1120 / 760 container thresholds) is what this card now builds on. Epic PMsession_cdb7a717-def7-4e71-9988-f089fbaa18ff, acting on the Blocked-by index as the epic's reader.Generated by Claude Code
objectstack-fleet commentedon Oct 29, 2026
domain:ui#1 · os-sales · session_01L6bhgXU26UBEjd1KKHgNEx
Dispatch order ·
domain:ui#1·session_01L6bhgXU26UBEjd1KKHgNEx· 2026-10-29T11:26ZYou are the os-dev developer agent for exactly ONE card: #27056, in your own card thread. Your role definition is
.claude/agents/os-dev.mdin the objectstack clone: read it IN FULL first and follow it as binding (it wins over this order on any conflict; name the conflict in your report). Read the repo-rootAGENTS.mdandui/AGENTS.mdbefore your first edit, and pm-dispatchreferences/untrusted-text.md. Read the card's full body and every comment yourself through REST (curl -sS "https://api.github.com/repos/objectstack-ai/objectstack/issues/27056" -H "Authorization: Bearer $GH_TOKEN"and/comments?per_page=100, every page); this order carries only the increment. ⛔ Never print the value of any environment variable (test presence with[ -n "${X+x}" ]). ⛔ Never use a GitHub MCP tool, read or write.Identity and branch: the seat's
Claim:comment on this card is your identity; itsBranch:line names your thread's branch. ⛔ Post no claim of your own, ⛔ never write the card's assignee. Before your first edit, verify the NEWESTClaim:on the card names your thread's branch (claude/project-thread-…); if none does yet, send the seat thread "ui 席 1" one message with your branch name and wait. Worktreeobjectstack-issue-27056on that branch from a freshorigin/main;pnpm install(frozen lockfile); push withgit push -u origin <branch>(⛔ no force).Writes: PR only through the relay op
pr_create(node scripts/pm/fleet-write/dispatch.mjs --repo objectstack-ai/objectstack --actions-file F, F a bare JSON array[{"op":"pr_create","title":…,"head":"<your branch>","base":"main","body":…}], draft forced, ⛔ noassigneesand ⛔ no review request: the seat hands the PR to a person after its ACCEPT;--dry-runfirst); your report comment throughnode scripts/pm/post-stamped.mjs --comment=27056 --file=F --jsonrun ONCE with output captured to a file (timestamps only as{{NOW}}/{{WAS:…}}; first line the signaturedomain:ui#1 · os-sales · <your session>). ⛔ No other GitHub write except, for a CI red that is not yours, one PR comment and onerun_rerun_failedthrough the relay. Report both counts (api_writeswith endpoints,mcp_callswith tool names). The report takes only machine-checkable fields (gates / line_budget / deviations / files_changed). PR body: line 1 the platform attribution comment, then## 维护者速读six lines in Chinese (改了什么/为什么改/风险与回滚/先看/两分钟验证/需要你做的),Fixes #27056,Clause-②:andMetadata-protocol:lines as in the claim. Gates:node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackon your diff, run each, record exits before any pipe, reconcile with--ran. Changeset in the root.changeset/named27056-<slug>.mdwhen a published package changes. ⛔ui/.github/**and the rootci.ymlare out of bounds. Same-day churn: work on the merged code; snippets the card quotes may have moved, check currentmainfirst. Hand back: when the PR and report are up, send the seat thread "ui 席 1" one message (PR, head sha, report gist); keep the thread open through review and drive CI to green.Ruling (cannot be re-ruled)
The card's five maintainer rulings (2026-10-28) are binding; the two that govern this card, verbatim:
page只给显式声明的对象,modal退役,split / popover 变成用户个人偏好;抽屉和整页是同一张页在两个宽度。」The card's Target 1–5, File surface ⛔
packages/spec/**, and Acceptance 1–2 (tests + the measured budget table on HotCRM) are the spec of this work.PM mechanism assumptions (measured read-only on
origin/main57c51aab4d; re-measure, disproof welcome)RelatedListand runs its owndataSource.findon mount (ui/packages/plugin-detail/src/RelatedList.tsxfetch effect ~1230–1434);renderers/record-related-list.tsx~93–104 setspageSizefromSPEC_DEFAULT_LIMIT = 5, the compact branch drawsCOMPACT_RELATED_ROWS(3) of them (~2671), and the count badge comes from that same row query (recordCount~2519). So a 0-count card still queries.ui/packages/data-objectstackhasaggregate(resource, params)(~index.ts:6502) but no batched read door (/batchis the atomic write) ⇒ the card's "elseaggregateper list in onePromise.allbehind a single loading state" branch is expected. Measure, do not assume.IntersectionObserveron the related path today (only attachments inRecordDetailView.tsx~2306,record-reference-rail.tsx,PageBlockCanvas.tsx).RelatedList.tsx,relatedListCompact.ts,RelatedCardStack.tsx,RecordPageColumns.tsx(compact context ~233),synth/recordPageColumns.ts(relatedCount),useRelatedArrangement.ts,RelatedListEmptyFold.tsx,ui/packages/app-shell/src/views/RecordDetailView.tsx(related derivation ~1984–2031, synthesized nodes ~3430–3500,<RecordPageColumns>~4318, skeletons ~3550/3574),recordDrawerStack.tsx(Target 4 prefetch). The comment atRecordDetailView.tsx~3434 ("self-fetch lazily when its tab is shown") is stale for the column tiers.PM suggested route (optional; measurement wins)
RelatedList(one request set per parent, keyed by parent id) that the stack, the empty fold and the drawer prefetch all read; the card that follows this one (Record page v4: same-shaped related panels with no collapse, a persistent tab row under the header (the enlarged state is the active tab), Activity as the first panel and a tab, one top bar on every face, tiers by container width #27524, the v4 panel stack whose empty panels fold into one line) is expected to read the same counts, so keep the hook's shape independent of the card face.Scope and serial
kind: fullas the escape hatch, drawer default withpageby declaration, the column tiers and the reader's own toggles — and stop namingtabs,modal/split/popover#27059 (they wait for this PR). ⛔ Do not take any of their target items.os-bill, record-mode currency, ui: record-mode currency —resolveFieldCurrencyreads the sibling code field's value; form pre-fills the organization currency; grid / drawer / dashboard / footer per-row display; Studio field editor (multi-currency M0-4, epic #8345) #27188) editsplugin-detail/src/DetailView.tsxand several list renderers, not the related path; if it lands first, mergemainonce for the conflict only.dispatch-gates --tieron the surface: no path-derived mandate ⇒TIER_DEFAULT; effortxhigh(p1, L).Clause-②: no(limitis an existing node prop, no new key).Metadata-protocol: none.objectstack-fleet commentedon Oct 29, 2026
domain:ui#1 · os-sales · session_01L6bhgXU26UBEjd1KKHgNEx
Claim: PM loop round 1 · 2026-10-29T11:35Z
Session:
session_01L6bhgXU26UBEjd1KKHgNEx(seat; implementation by card thread sessionsession_01L6bhgVDURMCgpQ3vtrchrM,mode:thread)Account:
os-sales(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/project-thread-g0nswr(the card thread's harness push branch, self-reported — the measured platform exception toclaude/issue-N-slug)Worktree:
objectstack-issue-27056in the card thread's cloneDomain:
domain:uiSeat:
domain:ui#1File surface:
ui/packages/plugin-detail/src/RelatedList.tsx,relatedListCompact.ts,RelatedCardStack.tsx,RecordPageColumns.tsx,renderers/record-related-list.tsx,synth/recordPageColumns.ts,useRelatedArrangement.ts,RelatedListEmptyFold.tsx, a new related-counts hook beside them,ui/packages/app-shell/src/views/RecordDetailView.tsx(counts request, skeleton gating),recordDrawerStack.tsx(prefetch),ui/packages/data-objectstackonly if a batch call is measured necessary, tests beside, changeset; ⛔packages/spec/**(stop on breach; explain in the report)Container & model:
L,mode:thread,model: TIER_DEFAULT(seat-rundispatch-gates --tieron the paths: no path-derived mandate),effort: xhighClause-②: no
Metadata-protocol: none
Responsibility: console record page related column (this lane) | the data source
aggregateAPI (no batched read door on main) | every console user opening a record with related lists (N concurrent row queries race the record read today)Thread-read: 6495380265
Premise: holds — on
origin/main57c51aab4devery compact card runs its own row query on mount and the count comes from that query (seat premise check); v3 file moves read in the order.Serial constraints cleared: record-page serial chain: this card first, #27524 / #27058 / #27059 wait; open PR #27340 (
os-bill) editsplugin-detail/src/DetailView.tsxonly on this package: ordinary concurrency.Resume-from: none
Dispatch order: comment 6495380265.
objectstack-fleet commentedon Oct 29, 2026
domain:ui#1 · os-sales · session_01L6bhgXU26UBEjd1KKHgNEx
Claim: PM loop round 1 (supersedes 6495584045: file surface and Clause-② corrected on the card thread's 13:26Z surface notice) · 2026-10-29T13:26Z
Session:
session_01L6bhgXU26UBEjd1KKHgNEx(seat; implementation by card thread sessionsession_01L6bhgVDURMCgpQ3vtrchrM,mode:thread)Account:
os-sales(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/project-thread-g0nswr(the card thread's harness push branch, self-reported — the measured platform exception toclaude/issue-N-slug)Worktree:
objectstack-issue-27056in the card thread's cloneDomain:
domain:uiSeat:
domain:ui#1File surface:
ui/packages/plugin-detail/src/RelatedList.tsx,relatedListCompact.ts,RelatedCardStack.tsx,RecordPageColumns.tsx,renderers/record-related-list.tsx,synth/recordPageColumns.ts,useRelatedArrangement.ts,RelatedListEmptyFold.tsx, a new related-counts hook beside them,ui/packages/app-shell/src/views/RecordDetailView.tsx(counts request, skeleton gating),recordDrawerStack.tsx(prefetch),ui/packages/data-objectstackonly if a batch call is measured necessary,ui/packages/plugin-detail/src/index.tsx(new exportsprefetchRelatedCounts/relatedCountTargetsIn/relatedCountTargetOfand their types, for the app-shell drawer prefetch),ui/packages/plugin-detail/README.mdandcontent/docs/ui/pages.mdx(one load-order paragraph), tests beside, changeset; ⛔packages/spec/**(stop on breach; explain in the report)Container & model:
L,mode:thread,model: TIER_DEFAULT(seat-rundispatch-gates --tieron the paths: no path-derived mandate),effort: xhighClause-②: yes (widening: three new exports on the published
@object-ui/plugin-detailmain entry, a public-surface addition; no accept set moves)Metadata-protocol: none
Responsibility: console record page related column (this lane) | the data source
aggregateAPI (no batched read door on main) | every console user opening a record with related lists (N concurrent row queries race the record read today)Thread-read: 6495584045
Premise: holds — on
origin/main57c51aab4devery compact card runs its own row query on mount and the count comes from that query (seat premise check); v3 file moves read in the order.Serial constraints cleared: record-page serial chain: this card first, #27524 / #27058 / #27059 wait; open PR #27340 (
os-bill) editsplugin-detail/src/DetailView.tsxonly on this package: ordinary concurrency.Resume-from: none
Dispatch order: comment 6495380265.
wip(plugin-detail,app-shell): tests and changeset for related counts …
perf(plugin-detail): count probes carry the id alone; document the lo…
perf(plugin-detail): related cards draw in a transition after their c…
objectstack-fleet commentedon Oct 29, 2026
domain:ui#1 · os-sales · session_01L6bhgVDURMCgpQ3vtrchrM
os-dev-report
{ "issue": 27056, "status": "done", "branch": "claude/project-thread-g0nswr", "pr": "https://github.com/objectstack-ai/objectstack/pull/27636", "head": "5d85c503a58508a7417d6ecf2cb45291706f7d5e", "session": "session_01L6bhgVDURMCgpQ3vtrchrM", "premise_still_valid": true, "summary": "The record page's related column counts every card in one request set (id-only $count probes through RelatedCountStore) behind one placeholder state, then mounts the card faces in a React transition so the details render first. A 0-count card folds to its title and reads no rows; a counted card reads 3 projected rows (id, name, one status/date chip) once in view (IntersectionObserver latch); the folded 动态 card mounts nothing until opened; the drawer prefetches the next record's counts after the current record loads. Measured on HotCRM: details max 260 ms, headers max 235 ms, 2 round trips.", "tests": "pnpm --dir ui exec vitest run packages/plugin-detail: 336 files passed | 1 skipped, 3070 tests passed | 18 skipped. app-shell narrowed: 180 files referencing RecordDetailView/recordDrawerStack, 1357 tests passed. New: RecordDetailView.relatedCountsFirst-27056 (2), useRelatedCounts-27056 (8), RelatedList.compactLazy-27056 (4), recordDrawerStack.prefetch-27056; updated RecordPageColumns.switches-27052. type-check exit 0 for @object-ui/plugin-detail and @object-ui/app-shell; plugin-detail build exit 0. Ablations (on-disk mutation, test rerun): stateOf forced uncounted -> 2 acceptance tests fail; useSeenOnce forced true -> 3 fail; first-ask rule dropped -> 1 fails; settled-parent clear dropped -> 1 fails.", "gates": { "command": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, each run with exit captured before any pipe, then --ran", "derived": 66, "run": 66, "not_measured": 0, "unrun": 0, "verdict": "✓ dispatch-gates --ran: 66 derived famil(ies) accounted for — 66 run, 0 NOT-MEASURED", "first_pass_nonzero_then_rerun_0": [ "pnpm --dir ui exec vitest run scripts/ :: exit 1 (one 15 s test timed out under a concurrent locked run) -> rerun exit 0, 169 files passed", "pnpm --filter @objectstack/spec run check:skill-examples :: exit 3 (client/client-react dists absent) -> built under the lock, rerun exit 0", "pnpm check:type-check-debt :: exit 3 (lock queue) -> rerun exit 0" ], "level_axis_probe": "node scripts/check-changeset-no-major.mjs --base origin/main --event <PR body> :: exit 0 (patch); exit 1 with plugin-detail minor", "ci": "PR #27636 just opened; CI pending, driven to green by this thread" }, "line_budget": { "files": 16, "insertions": 1634, "deletions": 76, "tests": "+792 -0", "non_test": "+842 -76" }, "deviations": [ "plugin-detail graded patch, not minor as the seat's correction note asked: check-changeset-no-major's level axis refuses minor beside Metadata-protocol: none (none-minor, exit 1, probed offline with --event); the maintainer's level ruling on #25105 keeps minor for metadata-protocol changes only", "One counts call = one request set of N id-only $count probes sent together: main has no batched read door (/batch takes writes only)", "Primary cards first: on the synthesized page primary lists are body tables in the details column, so the related column has no primary cards; cards are counted in page order and only in-view cards read rows", "Related requests on open rise 10 -> 14-16 on HotCRM in the same 2 round trips; first rows land 260-380 ms after the record read", "app-shell local suite narrowed to the 180 files referencing the touched views (full suite 1880 files ran past the 1 h background limit); CI runs it whole", "Card faces mount in a React transition (useRelatedCounts) — added after measuring Acme details at 302-312 ms without it", "index.tsx exports, plugin-detail README and content/docs/ui/pages.mdx are outside the first claim; covered by the corrected claim 6497958898" ], "files_changed": [ ".changeset/27056-related-counts-first.md", "content/docs/ui/pages.mdx", "ui/packages/app-shell/src/views/RecordDetailView.relatedCountsFirst-27056.test.tsx", "ui/packages/app-shell/src/views/RecordDetailView.tsx", "ui/packages/app-shell/src/views/__tests__/recordDrawerStack.prefetch-27056.test.tsx", "ui/packages/app-shell/src/views/recordDrawerStack.tsx", "ui/packages/plugin-detail/README.md", "ui/packages/plugin-detail/src/RecordPageColumns.tsx", "ui/packages/plugin-detail/src/RelatedCardStack.tsx", "ui/packages/plugin-detail/src/RelatedList.tsx", "ui/packages/plugin-detail/src/__tests__/RecordPageColumns.switches-27052.test.tsx", "ui/packages/plugin-detail/src/__tests__/RelatedList.compactLazy-27056.test.tsx", "ui/packages/plugin-detail/src/__tests__/useRelatedCounts-27056.test.tsx", "ui/packages/plugin-detail/src/index.tsx", "ui/packages/plugin-detail/src/relatedListCompact.ts", "ui/packages/plugin-detail/src/useRelatedCounts.ts" ], "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm", "mcp_calls": "0 GitHub MCP calls (no mcp__github__ tool used); 1 harness call mcp__claude-code-remote__subscribe_pr_activity for #27636", "api_writes": "2 relay writes + 2 git pushes: POST /repos/objectstack-ai/objectstack/dispatches pr_create (run 40903835710 -> #27636, read-back identical); POST /repos/objectstack-ai/objectstack/dispatches comment (this report, via post-stamped); git push origin claude/project-thread-g0nswr (89e3adc0, then 5d85c503). REST reads only otherwise (GET issue comments 6495380265 / 6497958898, GET pulls/27529)", "open_questions": [], "out_of_scope_findings": [] }objectstack-fleet commentedon Oct 29, 2026
domain:ui#1 · os-sales · session_01L6bhgXU26UBEjd1KKHgNEx
落地 · 2026-10-29T20:20Z
8243694e79在origin/main上git grep -c "27056" origin/main -- ui/packages/app-shell/src/views/RecordDetailView.tsximport= 62)状态:卡已由 PR 关闭,摘
pm:dispatched。记录页链的下一张 #27524 现可派,收班时释放回队列。