Repository navigation
console list page: the saved-view tab strip and the toolbar's four controls share one row — ListView takes a leading toolbar slot as a host prop and the console passes ViewTabBar into it #12440
Description
Filing gate ③, the follow-up the maintainer's dispatched design still owes after #12423 landed its items A–F. The maintainer's words, verbatim (chat, 2026-10-15, session session_019YFzJp4o8Ck4NsT1toC4UP, seat account os-bill): the dispatch order 「按这个方案在 objectui 开一个 issue,使用 dispatch 技能,然后你负责派发开发。开发完了使用右侧的浏览器测试验证并给我看效果。」 and, on the delivered screenshots, 「你觉得达到你设计的改动效果了吗?」. The approved design is the prototype at https://claude.ai/artifact/8JLqDFmcuK82RS1Cb8219f (private to the maintainer): its main artboard draws the saved-view tabs and Filter · Sort · Display · Search on ONE bordered row. The dev of #12423 raised exactly this as the open question of its report (comment 6185623889 on #12423, option B) and the seat's contract review on PR #12436 escalated it here.
Blocked-by: #12427
Serial after #12423's PR #12436: both edit packages/plugin-list/src/ListView.tsx and packages/app-shell/src/views/ObjectView.tsx.
Reader who acts: the domain:ui lane, seat 3 (this session), one dev round across @object-ui/plugin-list and @object-ui/app-shell, after #12436 merges.
Dedupe (MCP search_issues, scope objectstack-ai/objectui, all states, read 2026-10-15T06:40Z): "list view tabs and toolbar share one row ListView leading host slot ViewTabBar console" → 9 (#12423 the parent card; #12418 open, per-user persistence; #753, #809, #724, #833, #7218, #11395 closed, none about the toolbar row). None is this card.
The problem in one screen
After #12436 the Leads list of the HotCRM demo at 1440 px still spends two bordered rows on list chrome: the saved-view tab strip (the console's own row above the list) and the list toolbar (tabs' user filters on the left, the four controls on the right). Measured on the PR head: the first data row sits at y≈242 (base ≈262); the approved prototype puts the tabs and the four controls on one row and gains the remaining ~40 px. The two rows exist because ListView has no place to draw a host's tab strip: its own toolbar row has a left group (user filters) and a right cluster, and the console mounts ViewTabBar in a separate div above the list.
Readings (objectui PR #12436 head 4ba299c, read 2026-10-15T06:30Z; the base lines are the same)
packages/app-shell/src/views/ObjectView.tsx4098–4099: adivwith classeshidden sm:block border-b px-3 sm:px-4 bg-background overflow-x-auto shrink-0wrapping theViewTabBarelement — a row of its own, drawn fromsmup; a secondViewTabBarmount at 4124 is the same strip's other branch. Belowsmthere is no strip: the console registers the app bar's mobile view switcher instead (useMobileViewSwitcherRegistration, 2597–2607).packages/plugin-list/src/ListView.tsx4902–4935: the "Unified toolbar" row — aborder-b … flex items-center justify-betweenrow whose left group holds onlyuser-filters(UserFilters,maxVisible={3}) and whose right cluster is the tool pill. Nothing a host hands in renders on the left.packages/types/src/objectql.ts3140:tabs?: never— the list view's owntabskey was RETIRED from the protocol (objectui#11073); everylistViewsentry is a tab in the saved-view switcher. So the slot must be a HOST prop on theListViewReact component, likeshowRowNumbersonObjectGridin feat(list): Filter · Sort · Display · Search toolbar, width-fitted view tabs, one leading row cell, one-row page header #12436, and never a node key.packages/plugin-view/src/ViewTabBar.tsx(after feat(list): Filter · Sort · Display · Search toolbar, width-fitted view tabs, one leading row cell, one-row page header #12436): the strip isflex min-w-0 flex-1and derives its visible count from its own width, so inside a shared row it folds whatever the right cluster leaves no room for;maxVisibleTabscaps it.
What to build
ListViewtakes a host proptoolbarLeading?: React.ReactNode(name by the convention of the file; a React prop, never a schema key). It renders FIRST in the toolbar row's left group, beforeuser-filters, in amin-w-0 flex-1wrapper so the tab strip's width is what the four controls leave. Absent ⇒ the row is exactly today's.- The console (
ObjectView.tsx4098) stops drawing its own tab row and passes theViewTabBarelement (with the "+" add-view and "Unsaved changes" affordances it already carries) through the slot, fromsmup only — thehidden sm:blockbehaviour moves with it, so belowsmnothing is passed and the app bar's mobile switcher stays the phone's only view control. UserFilterskeeps its place after the tabs. Row height, the right cluster, the Display panel and persistence are untouched.- The view description line (
view-description, drawn above the toolbar when a view has one) is unchanged.
Acceptance
ListViewtest: withtoolbarLeadingpassed, its node renders inside the toolbar row beforeuser-filters; without it, the row's DOM equals today's (an existing toolbar pin stays green unchanged).ObjectViewtest: at a desktop width the page renders no standalone tab row; theview-tab-bartest id is inside the list's toolbar row; theview-tab-overflowfold still works there (theViewTabBar.widthFit-12423pins stay green); at a phone width noview-tab-barrenders, as today.- Browser check by the seat at 1440 on the HotCRM demo: one bordered row carries the tabs on the left and Filter · Sort · Display · Search on the right; the first data row sits about 40 px higher than on feat(list): Filter · Sort · Display · Search toolbar, width-fitted view tabs, one leading row cell, one-row page header #12436's head.
pnpm testfrom the repo root green on the affected files; a changeset (minor) for@object-ui/plugin-listand@object-ui/app-shell.
Out of scope
- Per-user persistence (console lists: per-user list state (view tab order, filter state, sort, column widths) moves from localStorage and shared-view writes onto sys_user_preference ui.* keys; a toolbar sort or drag-reorder no longer rewrites the shared view #12418); the sidebar cards (console shell: the page-header star and the sidebar pin become one "Pin to sidebar" action in the page overflow menu, landing in the Pinned section at the top; the app sidebar's bottom Favorites group goes #12427, console shell: the sidebar collapse toggle moves from the sidebar footer to the app bar's left slot at every width, where the drawer button already sits below lg #12429); any protocol key for the slot; the mobile layout; the breadcrumb row.
Attribution: written by the domain:ui seat in session session_019YFzJp4o8Ck4NsT1toC4UP (account os-bill) on the maintainer's instruction quoted above.
Activity
objectstack-fleet commentedon Oct 15, 2026
Blocked-by update 2026-10-15T07:32Z from the dispatching seat (
session_019YFzJp4o8Ck4NsT1toC4UP, os-bill): #12423 is MERGED (PR #12436, merge commita2225f9), so that line is cleared. This card now waits on #12427's PR #12443: both editpackages/app-shell/src/views/ObjectView.tsx(that PR adds the page-header overflow entry; this card moves theViewTabBarmount into the list toolbar), and the lane runs one dev at a time. Blocked-by: #12427. The label stayspm:blocked; this card is the lane's next dispatch once #12443 merges. Parent: #12444.Thread-read: none
Generated by Claude Code
objectstack-fleet commentedon Oct 15, 2026
Serial note ·
domain:uiseat 1 ·session_01JZnppgHRp4sREcouhkQYDB· 2026-10-15T07:47Z. ⛔ Not a claim; no label change.This card's blocker, objectui#12423, has closed, so the unlock scan will return the card. objectui#12418 (per-user list state) is now claimed (
6187168689) and edits the same two hot files:packages/plugin-list/src/ListView.tsxandpackages/app-shell/src/views/ObjectView.tsx.mainthat carries objectui#12418's landing.ObjectView.tsx'sViewTabBarmount (at:4098–:4099and:4124when this card was filed) andListView.tsx's toolbar row on that ref.origin/mainafter it.Generated by Claude Code
objectstack-fleet commentedon Oct 15, 2026
Regrade 2026-10-15T07:57Z on the maintainer's word (chat, 2026-10-15, verbatim: 「那后续的任务你放回队列,按 p1 标记,开发完了合并之后你验收。」):
priority:p2→priority:p1. The card stayspm:blockedonly until #12427's PR #12443 merges (the two editObjectView.tsx), and returns topm:queuein that landing stroke for the lane's next seat to dispatch; this session no longer dispatches it. Acceptance after merge is the maintainer's, through the session they designate, against the prototype on #12444. Parent: #12444.Thread-read: 6186951798
Generated by Claude Code
objectstack-fleet commentedon Oct 15, 2026
Design reference 2026-10-15T08:07Z from the filing seat, so the dev and the reviewing seat build and check against the same picture: https://claude.ai/artifact/JCzrzHxTxzD4EEKCTtDaNA (page files
main-1440.png,laptop-1024.png; the interactive prototype is linked from it). Board region: the view row on the 1440 and 1024 boards. Targets, measured on the HotCRM Leads list as admin: the saved-view tabs and Filter · Sort · Display · Search share ONE bordered row ≈ 47 px tall, with no standalone tab row above the list; at 1440 the first data row's top edge is ≤ 200 px from the top of the viewport (the console onmainafter #12436 measures ≈ 242; the prototype ≈ 183); at 1024 the strip folds more tabs into "N more" and the four controls keep their labels; belowsmno tab strip renders (the phone picker stays). The acceptance list in the card body stands; this comment names the picture and the numbers it is checked against. Parent: #12444.Thread-read: 6187345641
Generated by Claude Code
objectstack-fleet commentedon Oct 15, 2026
Unblocked 2026-10-15T08:16Z on the maintainer's word (chat, 2026-10-15: 「12429 为什么不能并行?」, applied to this card for the same reason):
pm:blocked→pm:queue, dispatchable now in parallel with #12427's PR #12443. The overlap is the same file, different regions: this card moves theViewTabBarmount inObjectView.tsx(thehidden sm:blockrow around line 4098 onmaina2225f9) into aListViewtoolbar slot; PR #12443 edits that file's page-header actions (the ⋯ favorites entry, around 3894–3950) andRecordDetailView.tsx. Dev rule for whoever takes this: branch fromorigin/main, mergeorigin/mainonce more before opening the PR, and whichever of the two lands second mergesmainagain (a merge commit, never a rebase). The earlier "Blocked-by #12427" line is withdrawn. Parent: #12444; design reference in the comment above.Thread-read: 6187513763
Generated by Claude Code
objectstack-fleet commentedon Oct 15, 2026
Claim: PM loop round 1
Session:
session_01XVp19E1MYUTFM3dnkwz8gBAccount:
baozhoutao(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-12440-list-toolbar-leading-slotWorktree:
objectui-issue-12440Domain:
domain:uiSeat:
domain:ui#2File surface:
packages/plugin-list/src/ListView.tsx(the unified toolbar row's left group and the new host prop only),packages/app-shell/src/views/ObjectView.tsx(theViewTabBarmount rows only,:4098and:4124onmaina2225f9), the tests beside each, the plugin-list README or docs line for the prop if one lists ListView's host props, and one.changeset/12440-*.md(stop on breach; explain in the report)Container & model:
M,mode:subagent,model: opus(default tier;dispatch-gates --tier --repo objectstack-ai/objectuireads no path-derived mandate); contract review atCONTRACT_REVIEW_TIER, owed by Clause-② belowClause-②: yes — a new host prop on the published
ListViewcomponent (@object-ui/plugin-list), levelledminoras the card asksResponsibility:
n/a — not a defect cardThread-read: 6187652736
Serial constraints cleared: seat 1's serial note
6187177562stands: objectui#12418 (claim6187168689, in flight) editsObjectView.tsxandListView.tsxfirst. This seat does not land this card's PR before #12418's PR lands, and mergesmaininto it then (a merge, never a rebase). The regions differ: #12418 namesObjectView.tsx:2381,:2748,:3432,:3755; this card names theViewTabBarrows at:4098/:4124. objectui#12427's PR #12443 edits the page-header actions (:3894–:3950) under the maintainer's parallel ruling in6187652736; whichever lands second mergesmainagain. objectui#12429 (claimed alongside) editsUnifiedSidebar.tsxandAppHeader.tsx, which is disjoint.Generated by Claude Code
objectstack-fleet commentedon Oct 15, 2026
os-dev-report
{
"issue": 12440,
"status": "done",
"branch": "claude/issue-12440-list-toolbar-leading-slot",
"pr": "#12473",
"session": "session_01XVp19E1MYUTFM3dnkwz8gB — the parent seat's id (mode:subagent)",
"premise_still_valid": true,
"summary": "ListView (@object-ui/plugin-list) takes a toolbarLeading?: React.ReactNode host prop. It renders first in the toolbar row's left group, before user-filters, in a min-w-0 flex-1 wrapper (data-testid list-toolbar-leading), and the left group gets flex-1 only when the prop is present. Without it the row's DOM is today's. The prop is destructured so it is not forwarded to the view component. The console ObjectView no longer draws the hidden sm:block tab row: renderListView passes the same ViewTabBar element as toolbarLeading from sm up, decided by a synchronous matchMedia(640px) hook, so below sm nothing is passed. ManageViewsDialog moved to page level. Mechanism findings: A1 is disproved, since there is one ViewTabBar mount and the second element in that row was ManageViewsDialog. A2 holds: ObjectView renders ListView directly in renderListView, with no registry hop. The one exception is deliberate: a server-refused view (objectui#12112) draws no list, so the strip keeps its own row there, because the tabs are the way off that view. The 'Unsaved changes' affordance the card mentions is not wired by the console (neither hasUnsavedChanges nor showSaveAsView is passed), so nothing moved for it. A4 (first data row at 1440 at most 200 px) is NOT MEASURED: no backend or console was run.",
"tests": "New pins: plugin-list ListView.toolbarLeading-12440 (4) and app-shell ObjectView.viewTabsInToolbar-12440 (3, the real ObjectView to plugin ObjectView to ListView to ViewTabBar chain: desktop slot placement, view-tab-overflow fold inside the slot with the strip stubbed at 320px, phone width with no view-tab-bar and no slot). Three existing harnesses adapted, assertions unchanged: viewRefusal-12112, servedViewLabel-11295, viewWriteRefusal-11583. Their ListView stand-ins now draw toolbarLeading. Ablation at committed 8181905 via ablation-replace.mjs ({toolbarLeading} to {null}, anchor 1 to 0, blob beb512a41006 to b69871ab3ab8): 4 red, the expected direction (placement, not-forwarded, both desktop ObjectView pins). 3 stayed green (absent and phone pins). Restore: blob equals HEAD and git diff HEAD is empty. Final head 39d14e0, targeted 7 files: Test Files 7 passed (7), Tests 54 passed (54). Broad run at 6be3e3b (plugin-list/, app-shell/src/views/, ViewTabBar): Test Files 2 failed | 1065 passed (1067). The 2 failures were the servedViewLabel and viewWriteRefusal harnesses, fixed in 374613b and green after. The second merge touched no edited file. The broad run was not repeated at the final head because the lock queue ran past 40 minutes; that is declared to CI. Closure build turbo 28/28. type-check exit 0: plugin-list at 6be3e3b, app-shell at 6be3e3b and 374613b (tsconfig.test.json included). Narrowed eslint on the 7 changed files: 0 errors, warnings per file equal to base 27411ea. Gate battery exit 0: phantom-deps, self-import, unreferenced-sources, doc-example-readers, handler-key-reads, side-effects-array, element-data-source-declaration, spec-symbols, action-forward-parity, designer-field-key-parity, icon-record-names, i18n-keys, i18n-drift, i18n-designer-parity, control-bytes, new-line-citations, check-changeset-fixed.mjs, check-changeset-presence.mjs. check:spec-floors exit 1, the same as on main (objectui#12334). NOT MEASURED: apps/console tests, app-shell suites outside src/views, browser layout (A4).",
"gates": {
"type-check plugin-list": 0,
"type-check app-shell": 0,
"vitest targeted @39d14e00c": 0,
"vitest broad @6be3e3b8c": "1 (2 harness files; fixed in 374613b, rerun 0)",
"eslint narrowed": 0,
"check-changeset-fixed": 0,
"check-changeset-presence": 0,
"pre-tsc battery (15)": 0,
"check:new-line-citations": 0,
"check:spec-floors": "1 (pre-existing on main, objectui#12334)"
},
"files_changed": [
".changeset/12440-list-toolbar-leading-slot.md",
"packages/plugin-list/README.md",
"packages/plugin-list/src/ListView.tsx",
"packages/plugin-list/src/tests/ListView.toolbarLeading-12440.test.tsx",
"packages/app-shell/src/views/ObjectView.tsx",
"packages/app-shell/src/views/ObjectView.viewTabsInToolbar-12440.test.tsx",
"packages/app-shell/src/views/ObjectView.viewRefusal-12112.test.tsx",
"packages/app-shell/src/views/ObjectView.servedViewLabel-11295.test.tsx",
"packages/app-shell/src/views/ObjectView.viewWriteRefusal-11583.test.tsx"
],
"deviations": [
"Three existing app-shell test harnesses beside ObjectView.tsx were edited. Their ListView stand-ins draw toolbarLeading, and the refusal test's plugin-view stand-in calls renderListView. These are tests beside the edited file and no assertion changed.",
"A refused view keeps a standalone tab row, the one case where no list toolbar exists. The ruling's no-standalone-row holds wherever a list is drawn.",
"content/docs guide not updated. It is outside the file surface; only the plugin-list README section was added.",
"The broad suite was not repeated at the final head because of the lock queue (more than 40 min of waiting). Declared to CI."
],
"mcp_calls": "0 — no MCP GitHub tools used; reads were single-card REST GETs (issue, comments, PR read-back)",
"api_writes": "2 — fleet-write relay: POST /repos/objectstack-ai/objectstack/dispatches carrying pr_create (POST /repos/objectstack-ai/objectui/pulls draft + POST issues/12473/assignees), and this os-dev-report comment (POST /repos//issues/12440/comments); git pushes not counted; zero label writes (objectui labeler.yml owns path labels, dispatch named none)",
"open_questions": [],
"out_of_scope_findings": [
"carrier: seat browser pass on PR 12473 · noted, not filed. ViewTabBar's root keeps -mb-px (made for a row whose border the tab underline overlaps). Inside the toolbar row's py-1.5 the active underline may sit about 6px above the row border. This was not observed in a browser, so it is an unreproduced visual observation and goes to the Acceptance notes only.",
"carrier: none · noted, not filed. The card's 'Unsaved changes affordance it already carries' does not hold for the console: ViewTabBar supports hasUnsavedChanges/showSaveAsView, but ObjectView passes neither, so the indicator never draws there. This is an observation, not a reachable defect."
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 15, 2026
Review: ACCEPT — objectui PR #12473 ·
domain:uiseat 2 ·session_01XVp19E1MYUTFM3dnkwz8gB· 2026-10-15T11:25ZReviewed on GitHub at head
6bded35, against the card's "What to build" and Acceptance, the design targets (6187513763) and the claim (6187812227), not against the dev's report.main. Its body opensFixes #12440, the only closing keyword, followed byClause-②: yes. 624 lines (+547 / −77), 9 files.6190415239at39d14e00con one item: the README fence fortoolbarLeadingdid not compile, soRepo Gateswas red. After one patch round (e9b3de6ef, README only, plus a clean merge ofmain), PASS6190903030at this head. The record verifies that the other 8 files are byte-identical to the reviewed head.toolbarLeadingis a host-only React prop. It is destructured ahead of...props, so it never reaches the active view component, and the toolbar row is unchanged when it is absent.ViewTabBarelement and hands it throughrenderListViewfromsmup. The one standalone row left is the refused-view face (objectui#12112), where no list and so no toolbar is drawn.ManageViewsDialogmoved to page level with the same condition and props.viewTabBaris a fresh element on every render and sits inrenderListView's deps. That costs a closure, not a request: plugin-view'sObjectViewcallsrenderListView(...)as a function, and its two effects keyed on it return early when a host supplies it.@object-ui/plugin-list: minor,@object-ui/app-shell: minor.filter=latest, 37 runs), all 16 test shards included.Test (shard 14/16)failed on its first attempt inentryRegistersBuiltinDesigners-11939, a 1000 ms wait for the entry's dynamic-import registration. That is unrelated to this diff, filed as objectui#12478. It passed on the seat's one re-run.Spec Main Shape Gate. Its annotations name onlypackages/types/src/layout.ts:1516(PageNodeSchema.kind, objectui#12464, which this seat is now fixing). It is red onmaintoo, and the workflow says it is advisory and not in the queue's required set. This comment is the record.ViewTabBar's-mb-pxunderline inside the toolbar row;hasUnsavedChangesandshowSaveAsVieware not passed), so nothing moved for it.Next: held, not landed yet. Seat 1's serial note
6187177562puts objectui#12418 (PR #12471, the same two hot files) first. This seat flips this PR ready and enqueues it once PR #12471 has merged, after updating this branch with thatmain(pr_update_branch, a merge) and reading CI green on the result.Generated by Claude Code
objectstack-fleet commentedon Oct 15, 2026
Landing: PR #12473 ·
domain:uiseat 2 ·session_01XVp19E1MYUTFM3dnkwz8gB· 2026-10-15T12:32ZThe hold in the ACCEPT above is released. Seat 1's PR #12471 (objectui#12418) merged as
maincaac3b2.origin/main(62040e2e9, a merge commit) and fixed one feat(app-shell): a list's sort, column layout, filters and tab order are the user's own, not the shared view's (objectui#12418) #12471 test that the merge broke without a text conflict (bd0561b88). Read by the seat:ObjectView.tsxis unchanged except one line:renderListView's dependency list now carries feat(app-shell): a list's sort, column layout, filters and tab order are the user's own, not the shared view's (objectui#12418) #12471'slistIdentityKey,listPrefs.ready,listPrefs.preferenceandlistPrefs.patchView, plus this PR'sviewTabBar.ListView.tsxis byte-identical to the contract-reviewed head, so contract review PASS6190903030still covers the published surface.ObjectView.userListState-12418.test.tsx'sListViewstand-in now drawsprops.toolbarLeading, because the view tabs live in the list toolbar. No assertion changed, and it gives 12/12.bd0561b: 33 success, 3 skipped, 1 failure (filter=latest, 37 runs), all 16 test shards included. The one red is the advisorySpec Main Shape Gate, whose annotations name onlypackages/types/src/layout.ts(PageNodeSchema.kind). It is red onmaintoo, its fix PR fix(types,console): retire the page kind jsx from PageNodeSchema.kind; the console sample moves to html (objectui#12464) #12485 (objectui#12464) is in the merge queue, and the workflow says the gate is advisory and not in the queue's required set. This comment is the record.Next:
pr_ready+automerge_enable.Generated by Claude Code
objectstack-fleet commentedon Oct 15, 2026
Landed ·
domain:uiseat 2 ·session_01XVp19E1MYUTFM3dnkwz8gB· 2026-10-15T12:45Zmaind4bc81e. The card closedcompletedby the PR'sFixes #12440, which is the only closing keyword. No other card closed with it.origin/main, 9 of the PR's 10 files are blob-identical to the accepted headbd0561b.packages/plugin-list/src/ListView.tsx, is the accepted head plus another PR's 4 lines: PR feat(app-shell): Data pillar — object header buttons, seven flat tabs with a 1000 px fold, flush Records body (objectui#12450) #12477 (objectui#12450,7a5f4c1) adds anaria-labelto the search icon button at:5603. NotoolbarLeadingline differs.-mb-pxunderline, the view-description order).pm:dispatchedis removed in the same stroke as this comment.Generated by Claude Code