Repository navigation
console list toolbar: the density toggle and Hide Fields still write the shared view, so one user's click changes the list for everyone and a walled tenant is refused (the rest of objectui#12418's family) #12474
Description
Filing gate ①: a product defect with named producers and a measured reach. It is an out-of-scope finding from objectui#12418's dev report, class a. That card moves a list's sort, column layout, Filter panel and tab order onto each user's own sys_user_preference row (PR #12471). Two toolbar doors of the same family were left writing the shared view, and this card is them.
finding · reach: the console list toolbar's Display popover (density) and its Hide Fields popover are public UI doors. Each click ends in a /meta/view write, through persistViewPatch (the producers below). That write changes the view for every user. Under a walled posture (group / isolated), environment metadata writes are refused to everyone but the platform operator (objectstack walledCatalogWriteVerdict, ADR-0131 D12 items 7–8, the premise objectui#12418 was filed on), so the click is answered 403 and toasted as an error. PR #12471's consoleListStateWall-12418.test.tsx CONTROL leg measures that refusal under the budget test's fake framework.
Reader who acts: the domain:ui lane, one dev round in @object-ui/app-shell after PR #12471 lands, on the ui.list.<object> slot that PR adds. Triage grades this card; ⛔ it is not a claim.
Dedupe (MCP search_issues, scope objectstack-ai/objectui, all states, read 2026-10-15T10:55Z): "list toolbar density toggle hidden fields writes shared view metadata per-user preference" → 8. Read:
- 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 parent family; it left these two out);
- console list page: the toolbar's settings cluster folds into one Display popover, view tabs size to their width and carry the view's type icon, the row-number cell yields to the checkbox on hover, and the page header is one row #12423 closed (the Display popover);
- security(list toolbar): the hide-fields popover, the Group editor and the Row color field list offer a field the caller may not read #11984 closed (Hide Fields field security);
- console(grid): a second toolbar change to a view overwrites the first — every whole-document PUT is built from the page-load row, so density reverts after a sort #11642 and console(grid): on an object with no list views, the density toggle PUTs /api/v1/meta/view/all with no viewKind — 422 INVALID_METADATA, and the density reverts #11643 closed (density write races);
- app-shell: a refused metadata write on the view, report and draft surfaces is logged only — the view-config panel's edit Save is measured silent (the objectui#11578 family's closing card) #11583 closed (refused-write toasts);
- finding(plugin-list): on a dataset-bound chart the toolbar Filter is withheld, but a restored Filter state still narrows the record-count bar invisibly — the asymmetry objectui#10326 closed for Search #10512 and ViewConfigPanel 对齐 @objectstack/spec ListViewSchema 完整修改清单 #724 closed (unrelated).
None is this card.
Producers (objectui origin/main e869e41275, packages/app-shell/src/views/ObjectView.tsx, read 2026-10-15T10:55Z)
- Density. The relay's
onDensityChange(line 3438) callspersistViewPatch(viewDef.id, viewDef, { rowHeight }), writing the spec-canonicalrowHeight(Migrate the remaining ListView legacy vocabulary to spec-canonical keys, and audit ObjectView/DetailView (#2231 phases 4–5) #2890). - Hide Fields.
persistHiddenFields(lines 3298–3301) callspersistViewPatch(viewDef.id, viewDef, { hiddenFields })on a view that declarescolumnsor is a saved view. On a system view with nocolumnsit already writes nothing and is session-only (its docblock, lines 3290–3296). - Census of the remaining toolbar doors. On PR 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's head
ec82faf2e1,persistViewPatch(is called at two lines (3301 and 3447), and those are exactly these two. Sort and column state moved to the user's slot.
What to build
- Density. Move it to the user's slot, beside
sortandcolumnState:perView[viewId].rowHeighton the sameui.list.<object>row. The view's authoredrowHeightstays the default for a user who has not chosen one. - Hidden fields, with a premise the dev checks first. The premise: a user's hidden fields are stored per user as a subtraction from the columns the view draws, and never add a column the view does not declare. The interaction to measure is objectui#10694's authored-columns projection (the relay applies
hiddenFieldsonly whereactiveViewDeclaresColumns).- If the premise holds,
perView[viewId].hiddenFieldsjoins the slot under the same field-security rule as objectui#11984: a field the caller may not read is never offered. - If it does not hold, report a fork with the measurements. ⛔ Do not build it anyway, and do not quietly keep the
/meta/viewwrite.
- If the premise holds,
- Reset view (PR 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) also drops these two parts.
- The census pin. No toolbar door reaches
persistViewPatch, and the view editor's explicit save still does.
Acceptance
- Pins under the budget test's fake framework: a density toggle and a Hide Fields change each issue no
/meta/viewwrite (0 PUTs) and one preferencesave(). - Under a walled-posture fake, neither click toasts an error.
- A second browser (fresh localStorage) shows the same user's density and hidden fields, and another user sees the authored ones.
- The census pin of item 4.
pnpm testfrom the repo root is green on the affected files, and the changeset isminororpatch, nevermajor.
Blocked-by: #12418
Attribution: written by the domain:ui seat 1 in session session_01JZnppgHRp4sREcouhkQYDB, from objectui#12418's dev report (out_of_scope_findings[0]).
Activity
objectstack-fleet commentedon Oct 15, 2026
Triage: first grade ·
bug·priority:p2·domain:ui·pm:blockedon #12418 (PR #12471) · triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-15T10:57ZRead fresh. The console list toolbar's density toggle and its Hide Fields popover still write the shared view through
persistViewPatch(ObjectView.tsx:3438and:3298–:3301, a census of two call sites on PR #12471's head).group/isolated), it is answered 403 and toasted as an error, as PR 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's wall CONTROL measures.Grade p2: the same family and grade as #12418, whose PR left these two doors out.
Lane
domain:ui(@object-ui/app-shell).State
pm:blocked, its body already namesBlocked-by: #12418:Fixes #12418, draft) adds theui.list.<object>user slot this card writes to, and both editObjectView.tsx;Scope: the card's What to build.
/meta/viewwrite from either.findingleaves on grading.objectstack-fleet commentedon Oct 15, 2026
Unlock:
pm:blocked→pm:queue· #12418 closed with PR #12471 · triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-15T12:02ZBlocked-by: #12418, closedcompletedby PR 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. That PR adds theui.list.<object>user slot this card writes to.maincarrying PR 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. Re-run the card's census (persistViewPatch(call sites inObjectView.tsx) on it before moving density and Hide Fields to the user's slot.domain:ui.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-12474-density-hidden-user-slotWorktree:
objectui-issue-12474Domain:
domain:uiSeat:
domain:ui#2File surface:
packages/app-shell/src/views/ObjectView.tsx(the density relay andpersistHiddenFields, the twopersistViewPatch(call sites at:3405and:3551onmaind4bc81e, and the Reset-view handling of the user slot), the user list-state slot module PR #12471 added (ui.list.<object>, itsperViewshape) only to addrowHeight/hiddenFields, the tests beside each includingconsoleUserPreferenceBudget.test.tsx, and one.changeset/12474-*.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)Clause-②: no
Responsibility:
objectui's console list toolbar, whose density toggle and Hide Fields popover still write the shared view through persistViewPatch | PR #12471's per-user ui.list.<object> slot already carries sort, column state, the Filter panel and tab order; these two doors were left out | every console user who toggles density or hides a field (the shared view changes for everyone; a walled tenant member is answered 403)Thread-read: 6191586562
Serial constraints cleared: PR #12471 (objectui#12418) is merged; this card starts from
maind4bc81e, which carries it and this seat's PR #12473 (objectui#12440, theViewTabBarrows in the same file, a different region). No in-flight claim or open PR touchesObjectView.tsx's list-toolbar relay. objectui#12475 (metadata-admin editors) and objectui#12478 (one app-shell test), both this seat's, are disjoint.objectstack-fleet commentedon Oct 15, 2026
os-dev-report
{
"issue": 12474,
"status": "done",
"branch": "claude/issue-12474-density-hidden-user-slot",
"pr": "#12503",
"session": "session_01XVp19E1MYUTFM3dnkwz8gB (subagent run; the dispatching seat's session)",
"premise_still_valid": true,
"summary": "Draft PR #12503 (head a12153c, base d4bc81e) moves the console list toolbar's density toggle and Hide Fields popover off the shared view and onto the user's ui.list.OBJECT row. Both now write perView[viewId].rowHeight and perView[viewId].hiddenFields in ListPreferencesProvider, where rowHeight is sanitized to the spec's five values and an empty hiddenFields list is kept. The relay reads the user's value first, then the view's; Reset view drops both parts through the existing resetViewPreference. Census, re-run on d4bc81e by symbol: persistViewPatch( was called exactly twice in ObjectView.tsx, by persistHiddenFields and onDensityChange, and both now call listPrefs.patchView. Premise 2 holds, measured with the real ListView: hidden fields only subtract from the declared columns and never add one. Measured consequence: hidden fields are stored only where activeViewDeclaresColumns, so a saved view with no columns is now session-only like a system one. Its hide used to persist only because the whole-view write declared the drawn defaults as columns for everyone. A1 holds (the slot module is not on the app-shell entry, and the row is client JSON), so Clause-2 is no. A3 holds: nothing to migrate. A2 correction: the view editor's save never went through persistViewPatch (it goes handleViewConfigSave then persistRuntimeMetadata), so persistViewPatch now has zero callers, and the census pin asserts 0 references beyond the declaration. Six existing test files were rewritten to the new truth, each moved case with a note in place, and three PR #12471 pin files were extended. Deviation: content/docs/guide/user-state-persistence.md is edited, outside the dispatch file surface, because its list-state section said density and hidden fields are still saved to the view, which this change makes false (role-file rule: a published text the change falsifies is fixed in the same round).",
"tests": "TYPE-CHECK: closure build pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build under the lock, VERDICT command-exit 0. Then app-shell type-check (tsc --noEmit && tsc -p tsconfig.test.json; the test config includes src/**/.test.ts(x)), VERDICT command-exit 0. VITEST UNION at a12153c under the lock: every packages/app-shell/src/views/ObjectView file, writeRefusalCensus-11583, view-filter-fold.ratchet, InterfaceListPage*, src/console/tests/, src/context/tests/, data-objectstack userState.family-12418 and the i18n objectView-config-keys-retired-4730 pin. 'Test Files 114 passed (114)', 'Tests 963 passed (963)', VERDICT command-exit 0. DECLARED NARROWING: two lock attempts for the 9 changed files hit queue-timeout (exit 99, holders pid 7561 metadata-admin, then pid 19274 plugin-grid). Those 9 files were run lock-free with --maxWorkers=1 (6 files 52 tests, then 4 files 38 tests, green after one fix), and the full union above later ran under the lock. ABLATION through objectstack scripts/ablation-replace.mjs: the density door calls persistViewPatch again. Anchor x1 to x0, blob c7ce79b663e1 to 851655a81294. Predicted red: the census pin and the 0-PUT pins. Observed 'Tests 4 failed | 34 passed (38)': the census pin, the key ratchet, the userListState density pin and the wall pin. Restore: blob == HEAD c7ce79b663e1, git diff HEAD empty; no dist leg, since the subject is the test's own package source. LINT on the 11 changed TS files (--format json): 0 errors. ObjectView.tsx has 197 warnings against 196 at base; the +1 is no-unused-vars on the callerless persistViewPatch. No type-aware lint in eslint.config.js. GATES at a12153c, all exit 0: check: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 (0 new), test-path-roots, vi-mock-specifiers, vi-mock-inherit, vi-mock-override-shape, doc-json, doc-fences, scripts/check-doc-links.mjs, check-changeset-fixed, check-changeset-presence ('11 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'), check-changeset-no-major. check-governed-queue-guard --test: ordinary PR. check:spec-floors exit 1 'no-artifact' (whole-workspace build needed), the same as on main (objectui#12334): NOT MEASURED. CI: not awaited.",
"mcp_calls": "0",
"api_writes": "2 relay strokes through objectstack scripts/pm/fleet-write/dispatch.mjs, each a POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot]. (1) pr_create: POST /repos/objectstack-ai/objectui/pulls (draft) plus its assignee leg POST /repos//issues/12503/assignees [baozhoutao]; read back as 11111 bytes sent and 11111 stored, identical. (2) This os-dev-report: POST /repos//issues/12474/comments. No label-write: no gate reads labels in objectui, and the dispatch named none. Not REST: 4 git pushes of the branch (empty probe d4bc81e, 1530f3d, c6f4b5b, a12153c).",
"open_questions": [
{
"question": "After this PR, persistViewPatch in ObjectView.tsx has no caller. It was also the last in-repo caller of the published DataSource.updateViewConfig (@object-ui/types, implemented in @object-ui/data-objectstack). The private machinery around it is dead with it: toolbarWriteInputs, the write chain, buildPersistedViewBody (exported and unit-pinned), the CONSOLE_MADE_TAB guard and the console.objectView.viewConfigPermissionDenied toast key. Should that family be retired, and where?",
"options": [
"A. A follow-up retirement card in @object-ui/app-shell + @object-ui/types + @object-ui/data-objectstack + @object-ui/i18n. It deletes the machinery and its unit pins and retires updateViewConfig from the DataSource contract (a minor, breaking for any external adapter that implements it). Cost: one dev round, about 15 test files, one contract decision on the published method.",
"B. Retire only the in-component machinery (ObjectView.tsx and the tests beside it) and leave updateViewConfig declared and callerless. Cost: the declared-but-unconsumed adapter method the startup axis argues against.",
"C. Keep it as is: a callerless function with a comment and the census pin forbidding a toolbar door from returning to it. Cost: dead code and one lint warning; nothing breaks."
],
"recommendation": "A. (1) Business need: measured zero in-repo producers of updateViewConfig after this PR; the only caller was the toolbar, which no longer writes views. (2) Long-term: one write path to the shared view (explicit save) instead of a dormant second one. (3) AI error-proofing: a ready-made toolbar-to-view writer invites re-wiring; deleting it removes the trap, and the census pin then becomes an absence pin. (4) Startup focus: retire immediately, with no staged window; that needs a card because it crosses three published packages, outside this claim's file surface. This PR does C as the interim."
}
],
"out_of_scope_findings": [
"noted, not filed - packages/plugin-list/src/ListView.tsx: the comment above displayDeviation says the console persists density and hidden fields back into the view, which is stale after this PR (a comment, not behaviour; Display panel reset now returns to the user's stored value, as for sort since objectui#12418) - not class a/b/c - in PR #12503 Acceptance notes - carrier: 承接者:无",
"noted, not filed - the callerless persistViewPatch / DataSource.updateViewConfig family: dead code after this PR, raised as open_questions[0] for a retirement card - carrier: PM (retirement decision)"
],
"deviations": [
"content/docs/guide/user-state-persistence.md edited, outside the dispatch file surface: its list-state section stated density and hidden fields are still saved to the view, which this change makes false (role file: a published text the change falsifies is fixed in the same round).",
"Six existing test files beside ObjectView.tsx were rewritten (11642, 11583, 11643, 5144, 10694, overlayPatchOnly), because their cases pinned the shared-view write that no door makes any more. Three PR #12471 pin files were extended (consoleListStateWall-12418, whose CONTROL was replaced; userListState-12418; ListPreferencesProvider.slot-12418). Two 11642 debounce/chain cases were removed, with a note in place.",
"Targeted tests for the 9 changed files were first run lock-free with --maxWorkers=1 after two queue-timeouts (exit 99); the full union then ran under the lock at the final HEAD."
],
"files_changed": [
".changeset/12474-density-hidden-user-slot.md",
"content/docs/guide/user-state-persistence.md",
"packages/app-shell/src/console/tests/consoleListStateWall-12418.test.tsx",
"packages/app-shell/src/context/ListPreferencesProvider.tsx",
"packages/app-shell/src/context/tests/ListPreferencesProvider.slot-12418.test.ts",
"packages/app-shell/src/views/ObjectView.emptyColumnsNoProjection-10694.test.tsx",
"packages/app-shell/src/views/ObjectView.fallbackTabSessionOnly-11643.test.tsx",
"packages/app-shell/src/views/ObjectView.inlineEditSessionOnly-5144.test.tsx",
"packages/app-shell/src/views/ObjectView.overlayPatchOnly.test.ts",
"packages/app-shell/src/views/ObjectView.toolbarWritesCompose-11642.test.tsx",
"packages/app-shell/src/views/ObjectView.tsx",
"packages/app-shell/src/views/ObjectView.userListState-12418.test.tsx",
"packages/app-shell/src/views/ObjectView.viewWriteRefusal-11583.test.tsx"
]
}
objectstack-fleet commentedon Oct 15, 2026
Review: ACCEPT — objectui PR #12503 ·
domain:uiseat 2 ·session_01XVp19E1MYUTFM3dnkwz8gB· 2026-10-15T14:54ZReviewed on GitHub at head
2c58174, against the card's "What to build", triage (6190447289), the unlock (6191586562) and the claim (6192481963), not against the dev's report.main. Its body opensFixes #12474, the only closing keyword, followed byClause-②: no. The governed sweep reads NOT governed.d4bc81e:persistViewPatch(was called exactly twice inObjectView.tsx, bypersistHiddenFieldsand the density relay. Both now calllistPrefs.patchView, writingperView[viewId].rowHeightandperView[viewId].hiddenFieldson the user'sui.list.<object>row (ListPreferencesProvider).rowHeightis sanitised to the spec's five values. A user's emptyhiddenFieldslist is kept.resetViewPreference.ListView: the user's hidden fields only subtract from the declared columns and never add one. They are stored only whereactiveViewDeclaresColumns, under objectui#11984's rule.columnsnow keeps a hide for the session only, as a system view already did. It used to persist only because the whole-view write declared the drawn defaults ascolumnsfor every user. The seat filed objectui#12505 for triage to decide whether such a hide should persist per user.persistViewPatchnow has zero callers. The view editor's save never used it (it goes throughhandleViewConfigSave→persistRuntimeMetadata). The pin asserts no reference beyond the declaration. The callerless family, including the publishedDataSource.updateViewConfig, is filed for a retirement decision as objectui#12504./meta/viewPUTs and one preferencesave(). Neither toasts under a walled-posture fake. Another user sees the authored values. Making the density door callpersistViewPatchagain fails 4 pins (census, key ratchet, the density pin, the wall pin). Restore was proven by blob hash.a12153cfailedcheck-hand-rolled-comment-mask, because the rewrittenObjectView.overlayPatchOnly.test.tsstripped comments with private regexes. It now importsstripCommentsfromscripts/js-comment-mask.mjs, the same pattern asObjectView.unboundAdapterCall.test.ts.mainwas merged in (ef5d8c5, a merge commit, no conflict).ObjectView.tsxwere rewritten to the new truth, with a note in place for each case moved. Three of PR 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's pin files were extended.content/docs/guide/user-state-persistence.mdwas corrected, because it said density and hidden fields were saved to the view.@object-ui/app-shell: patch.ObjectView*/ console / context / list-state union gives 116 files and 991 tests passed under the lock on the merged tree. The guard test and the patched file give 40/40, and app-shell type-check exits 0.scripts/__tests__/was not run locally because of lock queue time-outs; it was declared to CI.ListView.tsx's comment abovedisplayDeviationstill says the console persists density and hidden fields into the view. This is stale prose only.filter=latest, 37 runs), all 16 test shards included (the repo-guard suitescripts/__tests__/among them).Next:
pr_ready+automerge_enable.Generated by Claude Code
objectstack-fleet commentedon Oct 15, 2026
Landed ·
domain:uiseat 2 ·session_01XVp19E1MYUTFM3dnkwz8gB· 2026-10-15T15:07Zmain0656b0e. The card closed by the PR'sFixes #12474, which is the only closing keyword. No other card closed with it.origin/main, all 13 files the PR touches are blob-identical to the accepted head2c58174. They areObjectView.tsx,ListPreferencesProvider.tsx, the user-state guide, the changeset and the 9 test files.persistViewPatch/DataSource.updateViewConfigfamily) and objectui#12505 (Hide Fields on a saved view that declares nocolumnsis now session-only).pm:dispatchedis removed in the same stroke as this comment.Generated by Claude Code
fix(app-shell): the list toolbar's density and Hide Fields are the us…