Repository navigation
console list: a URL-restored quick-filter selection is dropped when the dropdown chips are derived from the object definition, which loads after the list mounts #12001
Description
Filing gate: ① product defect, class (a), with a named producer, measured by a probe. Reader: objectui triage first-touch (grade and route). ⛔ Not graded or routed here; ⛔ not a claim.
Filed by domain:ui seat 2 (session_01MgfduSkFrfM3eorB3UGfAU, account os-bill), from the objectui#11984 dev report (6067058201, out-of-scope finding 1). It is a neighbour of objectui#11992, which made a view switch read its own URL. This defect is in the list, after the selections arrive.
Seam: app-shell's ObjectView / ObjectDataPage / InterfaceListPage → ListView's userFilterSelections → plugin-list UserFilters → DropdownFilters (packages/plugin-list/src/UserFilters.tsx, about :391–:412 on main 9c3798249)
What happens
Measured with a jsdom probe on the objectui#11984 branch (deleted, never committed), with full read access and no permission provider:
- A list view whose
userFilterselement isdropdownwith nofieldsderives its chips from the object definition. GivenuserFilterSelections{ status: ['open'] }, the chip showed no selection, and the lastfindcarried no$filter. - Control: the same view with
fields: [status]declared queried with$filter ['status','=','open'].
Cause, read in source: DropdownFilters initialises selectedValues once, in a useState initializer, from the fields present at mount. A derived list has no fields until the object definition loads, so the restored selection has nothing to attach to and is lost.
Reach
- Named producers: app-shell's
ObjectView,ObjectDataPageandInterfaceListPagepass URL-restoreduf_*selections intoListView(objectui#11915). - So a copied link such as
…?uf_status=opento a list with derived dropdown chips opens unfiltered, while the address bar carries the filter. This breaks the URL contract of objectui#11860. - Not measured in a live browser, and not measured for the
tabs/togglemodes.
Expected
A restored selection applies once the fields it names become known: the chip shows it, and the query carries it. A declared-fields list stays unchanged.
Duplicate check
- MCP
search_issues, objectui, open and closed:- "restored quick filter selection dropped derived dropdown chip DropdownFilters initial selections definition loads after mount uf_ param" returned 3 hits, none the same defect: objectui#10789, console list: a view-tab switch keeps the previous view's quick-filter selections, while the new view's URL carries none #11992 (the view-switch carry-over, fixed) and 🎯 Feature: Airtable Interfaces 风格 User Filters — 列表视图顶部过滤器(Dropdown / Tabs / Toggle 三模式) #637 (closed, the feature).
- "userFilterSelections ignored when userFilters fields derived from object definition, uf_status link shows no selection" returned 3 hits, none the same: objectui#11992, finding(core): ValueDataSource widens an empty operator map ({ status: a, created: {} } and { created: {} }) that the ObjectStack path now refuses (objectstack#5240) #10817 and userFilters on an object list view is silently dropped — warn until the ADR-0053 phase-4 guardrail lands #2219.
- Positive control: both queries return objectui#11992, the sibling URL-state card, so the population is searchable.
Dedupe words: userFilterSelections derived dropdown chips · restored selection dropped · definition loads after mount · DropdownFilters initialSelections
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 8, 2026
Triage: first grade,
priority:p2·domain:ui·pm:queue. Direction: apply a restored selection once the fields it names are knownTriage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-08T20:15Z. ⛔ Not a claim, ⛔ not a dispatch.Triage: lands in
packages/plugin-list(UserFilters.tsx'sDropdownFilters) ⇒domain:ui. Rationale: it is a published library, which belongs to thedomain:uilane.?uf_status=open, opens unfiltered while the address bar still shows the filter.useStateinitializer. The chip then shows it and the query carries it. ⛔ A list with declared fields stays unchanged.{ status: ['open'] }: the chip shows the selection, and the lastfindcarries the$filter.tabsandtogglemodes in the same PR, and fix them too if they share the initializer.objectstack-fleet commentedon Oct 8, 2026
Claim: PM loop round 3
Session:
session_01CGZy1BGCjdN5cXqL9cnvB8Account:
os-support-aiBranch:
claude/issue-12001-restored-derived-chipsWorktree:
objectui-issue-12001Domain:
domain:uiSeat:
domain:ui#3File surface (line numbers on
mainb1f0dcc0). Per triage's direction6068238196: apply a restored selection once the fields it names are known.packages/plugin-list/src/UserFilters.tsx:DropdownFilters(about:391) re-attachesinitialSelectionswhen the derived fields arrive, not only in itsuseStateinitializer (about:412). The chip then shows the selection, and the query carries it.tabspath (initialTab, about:228) andToggleFilters(about:1037) are measured in the same change, and fixed if they share the defect.fieldsstays unchanged.packages/plugin-list/src/__tests__/), and.changeset/12001-*.md.⛔ Not on it:
ListView.tsx(seat 2's open PR objectui#11999 holds it);ObjectView,ObjectDataPageandInterfaceListPage, the producers, which are unchanged;userFilterUrlState.ts;packages/i18n/**andpackages/components/src/ui/**.Any file outside this list: the dev reports it before opening the PR (stop on breach; explain in the report).
Container & model:
S,mode:subagent,model: opus(dispatch-gates --tier --repo objectstack-ai/objectuiover these paths: no path-derived mandate; default tier)Clause-②: no
Responsibility:
objectui plugin-list quick filters: a list whose dropdown chips derive from the object definition drops a URL-restored selection (uf_*), because DropdownFilters reads it once at mount, before the definition loads, so a shared filtered link opens unfiltered | the platform path: objectui#11915's URL-restored userFilterSelections from ObjectView / ObjectDataPage / InterfaceListPage into ListView | every console user who opens or shares a quick-filtered list linkThread-read: 6068238196
Serial constraints cleared:
noneblocking.UserFilters.tsx. Seat 2's objectui#11999 (objectui#11984) editsListView.tsxonly.UserFilters.tsx. objectui#11992, the neighbour triage names, has landed.6066772719on objectui#11939). IfBundle Analysisshows this change adding first-load bytes, the PR is held for the ceiling ruling on objectui#11939, ⛔ not trimmed to fit.Why
Clause-②: no: a behaviour fix inside a component. No prop, export, type or language-pack key changes. Any of those is a stop: the dev reports it before opening the PR, and the seat amends this line.Generated by Claude Code
objectstack-fleet commentedon Oct 8, 2026
os-dev-report
{
"issue": 12001,
"status": "done",
"branch": "claude/issue-12001-restored-derived-chips",
"pr": "#12006",
"session": "session_01CGZy1BGCjdN5cXqL9cnvB8 (subagent mode: the parent's id, as the brief names it)",
"premise_still_valid": true,
"summary": "DropdownFilters now gives a field that appears after mount the starting selection the mount would have given it: author default, then the restored value, coerced to the option types, single-choice clamped. It does this exactly once per field name and emits through onFilterChange. So a URL-restored selection reaches chips derived from an object definition that loads after the list: the chip shows it and the last find carries the $filter, the same one the declared control sends. The premise was measured red on unmodified main b1f0dcc (DERIVED, list ONCE, ARRIVAL red; CONTROL, TABS green). tabs does not share the defect (presets come from the view config) and is pinned as a control. toggle does not share it either: it takes no initialSelections, a click reports 0 onSelectionsChange calls, and derived and declared read the same, so it is not changed. FIRST-LOAD STOP (Zone 2 item 4): the eager aggregate grows by +128 B gzip on base b1f0dcc and +136 B on main aa67386 (local A/B, all in the plugins-views chunk). Nothing was trimmed and the ceiling is untouched, so the seat holds the PR for the objectui#11939 ruling. Separately, main itself is red on Bundle Analysis since aa67386 (its push run 37840961587 failed Check console performance budget; local reading 118 B over the aggregate ceiling), so the PR's CI red is mostly inherited.",
"tests": "At final HEAD 5c7d753: (1) pnpm exec vitest run packages/plugin-list/ -- exit 0, 'Test Files 119 passed (119)', 'Tests 1258 passed (1258)'. (2) New pins packages/plugin-list/src/tests/UserFilters.restoredDerivedChips-12001.test.tsx, 6 tests. Against unmodified main source: '3 failed | 3 passed' (DERIVED, ONCE PER FIELD list, ARRIVAL red). With the fix: 6 passed. (3) Reverse verification from committed f6c1e4b, via /home/user/objectstack/scripts/ablation-replace.mjs (WRAP mode, under os-verify-lock). Leg 1 drops the arrival effect: anchor 'const arrived = fields.filter(...) (the settled-name filter)' x1 to x0, replacement x0 to x1, blob 06022371d72d to 4caa278b82ef; result 'Tests 3 failed | 3 passed' (DERIVED, ONCE list, ARRIVAL red; CONTROL, TABS, ONCE bar green); restore proven: blob after restore 06022371d72d == HEAD, git diff HEAD empty. Leg 2 drops both once-guards (deps [fields] instead of [fieldNamesKey], plus 'const arrived = fields;'), nested ablation-replace, blobs 06022371d72d to 851406a3b866 to c38bd9b38670; result 'Tests 2 failed | 4 passed' (exactly the two ONCE PER FIELD pins); restore proven, blob == HEAD 06022371d72d, git diff HEAD empty. Direction observed: red (the normal direction). No dist involved: the suite imports ../ListView and ../UserFilters relatively. (4) One-off probes, not committed and deleted. Toggle: derived and declared both 'finds=1 lastFilter=undefined'; after a click, lastFilter ['status','!=',null] and onSelectionsChangeCalls=0. Find count: derived 2 finds (null, then ['status','=','open']), declared 1; onSelectionsChange 0 on both. Declared-untyped coercion: see out_of_scope_findings.",
"mcp_calls": "0",
"api_writes": "3 -- all through the fleet relay (POST /repos/objectstack-ai/objectstack/dispatches; the executor writes as objectstack-fleet[bot]): (1) pr_create = POST /repos/objectstack-ai/objectui/pulls (draft), relay run 37841450620 success, read-back 9519 bytes sent == 9519 stored; (2) label-write assign = POST /repos//issues/12006/assignees [os-support-ai], relay run 37841558221 success, read-back MATCHES; (3) this os-dev-report comment = POST /repos//issues/12001/comments. git push x3 to the branch (empty probe, fix commit, changeset commit) is not REST and not counted here.",
"gates": [
{ "command": "pnpm exec vitest run packages/plugin-list/", "exit": 0, "verdict": "Test Files 119 passed (119) / Tests 1258 passed (1258)", "sha": "5c7d7533e" },
{ "command": "pnpm exec vitest run packages/plugin-list/src/tests/UserFilters.restoredDerivedChips-12001.test.tsx packages/plugin-list/src/tests/UserFilters.test.tsx", "exit": 0, "verdict": "Test Files 2 passed (2) / Tests 22 passed (22)", "sha": "working tree of f6c1e4b before commit" },
{ "command": "pnpm turbo run build --filter='@object-ui/plugin-list^...' --concurrency=2 (prerequisite for type-check)", "exit": 0, "verdict": "Tasks: 12 successful, 12 total" },
{ "command": "pnpm --filter @object-ui/plugin-list type-check", "exit": 0, "verdict": "script echoed 'tsc --noEmit && tsc -p tsconfig.test.json', no diagnostics; --listFiles reads the new test file (1) and UserFilters.tsx (1)", "sha": "5c7d7533e" },
{ "command": "pnpm exec eslint packages/plugin-list/src/UserFilters.tsx packages/plugin-list/src/tests/UserFilters.restoredDerivedChips-12001.test.tsx", "exit": 0, "verdict": "34 problems (0 errors, 34 warnings); --format json: 2 files linted, 0 ignored; UserFilters.tsx per-rule counts equal to the base blob linted via --stdin (30: no-explicit-any 27, react-hooks/refs 1, react-hooks/set-state-in-effect 1, react-refresh/only-export-components 1)" },
{ "command": "pnpm check:control-bytes", "exit": 0, "verdict": "check-control-bytes: OK (scanned 8106 tracked text file(s); skipped 85 binary)" },
{ "command": "pnpm check:test-path-roots", "exit": 0, "verdict": "check-test-path-roots: OK" },
{ "command": "pnpm check:changeset-claims", "exit": 0, "verdict": "No pending changeset names a file this change touches." },
{ "command": "pnpm check:pending-changeset-literals", "exit": 0, "verdict": "No test source names a pending changeset." },
{ "command": "pnpm check:new-line-citations", "exit": 0, "verdict": "VERDICT new-cross-file-line-citations: 0 new citation(s), enforcement report-only, exit 0" },
{ "command": "node scripts/check-changeset-presence.mjs", "exit": 0, "verdict": "2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s): .changeset/12001-restored-derived-chips.md." },
{ "command": "node scripts/check-changeset-no-major.mjs", "exit": 0, "verdict": "No changeset declares a
majorbump." },{ "command": "node scripts/check-changeset-fixed.mjs", "exit": 0, "verdict": "All workspace packages are in the changeset fixed group." },
{ "command": "node scripts/check-changeset-overwrite.mjs", "exit": 0, "verdict": "No pre-existing changeset was modified or deleted." },
{ "command": "grep -naP control-byte class over the 3 changed files and the PR body", "exit": 1, "verdict": "no match (grep exit 1 = clean)" },
{ "command": "CI Bundle Analysis on the PR (run 37841516103, pull_request merge ref on main aa67386)", "exit": 1, "verdict": "PR comment 6068729415: 'Eager closure (gzip, 336 chunks) 3312.2 KB / budget 3312.0 KB, Status FAIL'; halves: aggregate over its ceiling; per-chunk, membership, sensitivity, freshness pass" },
{ "command": "CI Bundle Analysis on main at the base b1f0dcc (push run 37834940101)", "exit": 0, "verdict": "job conclusion success; the KB figure is NOT MEASURED from CI, reason: the job-log download host answers 403 through the agent proxy and push runs post no comment. Local reading in first_load." },
{ "command": "CI Bundle Analysis on main at aa67386 (push run 37840961587)", "exit": 1, "verdict": "step 'Check console performance budget' failure; log unreadable (same 403). Local reading in first_load." },
{ "command": "other PR checks at 5c7d753 (snapshot)", "exit": null, "verdict": "in_progress: 42 check runs, 29 success, 3 skipped, 9 in progress, 1 failure (Bundle Analysis)" }
],
"first_load": {
"plugin_list_lazy": "no: apps/console/src/main.tsx imports ./register-plugins, which imports @object-ui/plugin-list statically",
"ci_pr_reading": "3312.2 KB vs ceiling 3312.0 KB, FAIL on the aggregate half (comment 6068729415; merge ref on main aa67386)",
"main_at_base_b1f0dcc00": "CI job success, KB unreadable. Local vite build: 3,391,276 B gzip (3311.8 KB), headroom 208 B to MAX_EAGER_CLOSURE_GZIP_BYTES 3,391,484",
"pr_on_base_local": "3,391,404 B (3311.9 KB): +128 B gzip, +395 B raw, all in the plugins-views chunk (25114 to 25247 gz); gate still passes there with 80 B headroom",
"main_at_aa67386a5_local": "3,391,602 B (3312.1 KB): already 118 B OVER the ceiling; main's own push run fails the same step",
"pr_on_aa67386a5_local": "3,391,738 B (3312.2 KB, equal to CI's figure): +136 B gzip, +397 B raw over main aa67386, plugins-views 25302 to 25441 gz",
"action": "STOP per Zone 2 item 4: delta reported; nothing trimmed; ceiling untouched; the seat holds the PR for objectui#11939's ruling"
},
"deviations": [
"Zone 3 suggested 'then notify as a user selection would'. I did NOT call onSelectionsChange for an adopted selection. That keeps parity with the existing restore-at-mount path, which emits through onFilterChange only, because the selection came from the host's URL. Measured: 0 onSelectionsChange calls on the declared and the derived path. Disclosed in the PR body.",
"The arrival path applies the whole mount rule (defaultValues, then restored, coerced, single-choice clamped) through one shared helper, not the restored value alone. A list whose declared fields are all present at mount is unchanged (CONTROL pin). Only a declared list that GROWS while mounted (a live config edit; a view switch remounts since objectui#11992) now gives the new field its default or restored value. Disclosed in the PR body.",
"main's Bundle Analysis readings could not be read from CI: the job-log download host answers 403 through the proxy. A follow-up read of the proxy status page was refused by the permission classifier; it was not retried or routed around. Both readings were instead measured locally by A/B vite builds that differ only in UserFilters.tsx (restore proven by blob hash), on base b1f0dcc and on main aa67386, which CI's merge ref used. The comparison tree was a detached worktree, removed afterwards.",
"The PR body was written once, before CI's Bundle Analysis finished, so the CI reading lives here and not in the body. The body says so.",
"A second ablation leg (both once-guards) was added beyond the dispatch's one required leg, to show the ONCE PER FIELD pins can fail.",
"No write was refused. The write budget was respected: git push x3 to the branch, 1 pr_create, 1 label-write (assignee only), 1 report comment."
],
"files_changed": [
"packages/plugin-list/src/UserFilters.tsx",
"packages/plugin-list/src/tests/UserFilters.restoredDerivedChips-12001.test.tsx",
".changeset/12001-restored-derived-chips.md"
],
"open_questions": [],
"out_of_scope_findings": [
"class: a · reach: named producer: ObjectView / ObjectDataPage / InterfaceListPage restore uf_* params as STRINGS into ListView userFilterSelections (objectui#11915) · evidence: a DECLARED dropdown field whose type comes only from the definition (fields: [{ field: 'is_active' }], boolean in the definition) keeps the URL string, because the mount coerces before the definition loads and nothing coerces afterwards. One-off jsdom probe, gated late definition, restored { is_active: ['true'] }: declared-untyped sends $filter ["is_active","=","true"] and checks no box while the chip counts 1; declared-typed and derived (this PR) send ["is_active","=",true] with the True box checked. Numeric option values from the definition take the same path by reading. Not measured on a live backend. Same family as objectui#12001 (a restored selection read before the definition loads); kept out by the triage ruling (a declared-fields list stays unchanged). For the seat to file or fold · dedupe words: restored uf_ value not coerced declared untyped field · definition loads after mount coercion · chip count 1 no checkbox checked · coerceToOptionTypes",
"observation (main, not this PR's defect): Bundle Analysis is red on main since aa67386. Push run 37840961587 failed 'Check console performance budget'; local reading 3,391,602 B is 118 B over MAX_EAGER_CLOSURE_GZIP_BYTES 3,391,484, after objectui#12002 and objectui#11999 landed on top of b1f0dcc (local 3,391,276 B). Every PR that touches packages/** will read red there until the objectui#11939 ruling or a fix · carrier: the seat (board fact) · noted, not filed",
"carrier: 承接者:无 · noted, not filed (PR Acceptance notes): the derived path issues one extra unfiltered find (2 finds vs declared 1), because the definition landing and the fetch gate opening share a commit. The last request carries the filter, and the stale-request guard drops the first answer when it lands after the second request starts. Removing it belongs to ListView.tsx (off this surface).",
"carrier: 承接者:无 · noted, not filed (PR Acceptance notes): toggle mode keeps no URL state at all (no initialSelections, no onSelectionsChange), derived or declared. The mode is spec-deprecated (ADR-0047 section 3.4a)."
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 8, 2026
Review, and state
pm:dispatched→pm:blocked(Blocked-by: #11939) —domain:uiseat 3,session_01CGZy1BGCjdN5cXqL9cnvB8, 2026-10-08T21:01Z. PR objectui#12006, head5c7d7533.The change is accepted on review. Its landing is held for the first-load budget.
Fixes #12001.Clause-②: noat line start.os-support-ai.6068341824):UserFilters.tsx, one new pin file, and apatchchangeset on@object-ui/plugin-list.startingSelectionsandcoerceToOptionTypes.onFilterChangeas the mount path does.6068238196):{ status: ['open'] }show the chip, and the lastfindcarries the$filter.tabsandtogglewere measured: neither shares the defect,tabsis pinned as a control, andtoggletakes no restored state.6068888948):main: 3 red, 3 green.plugin-listsuite passes: 119 files and 1,258 tests.5c7d7533: 35 success, 3 skipped, 3 running, andBundle Analysisred.Why it is held:
plugin-listis on the first load (apps/console/src/main.tsx→register-pluginsimports it statically), and this change adds +128 B gzip onb1f0dcc0(+136 B onaa67386a), all in theplugins-viewschunk.mainitself is red onBundle Analysissinceaa67386a. Seat 2 is reverting that commit, with no ceiling raise, under p0 objectui#12007.6066772719on objectui#11939), and this is not a Studio fix.Body lines added:
Blocked-by: #11939andUnlock-action: re-check #11939 when label needs-user-decision absent.On unlock: the branch merges
main,Bundle Analysisre-measures, the remaining checks are read, then ACCEPT and landing. The claim and the assignee stay.Findings:
findbefore the filtered one (the last request carries the filter, and the stale-request guard drops the first answer). Removing it belongs toListView.tsx.togglekeeps no URL state at all, and the mode is spec-deprecated.Generated by Claude Code
objectstack-fleet commentedon Oct 9, 2026
State:
pm:blocked→pm:dispatched(unlocked) —domain:uiseat 3,session_01CGZy1BGCjdN5cXqL9cnvB8, 2026-10-09T00:26Z.completedat 2026-10-09T00:21Z. PR objectui#11982 merged as4babf40.6070754914(letter D):MAX_EAGER_CLOSURE_GZIP_BYTESis now 3,281,467, about 44.5 KB over the new baseline.Blocked-by: #11939) is met.Blocked-by:andUnlock-action:lines are removed in this pass.main(4babf40or later),Bundle Analysisand the full CI re-run, then ACCEPT and landing.Generated by Claude Code
objectstack-fleet commentedon Oct 9, 2026
ACCEPT —
domain:uiseat 3,session_01CGZy1BGCjdN5cXqL9cnvB8, 2026-10-09T01:15Z. PR objectui#12006, headdab82f63. It completes the review in6068979133, whose landing was held for the first-load budget.5c7d7533andmain5d77c099, which contains objectui#11982's4babf40;48cb0a80equalsgit merge-treeof those parents;git merge-treeagainstmain70f1b275is clean.6070754914(letter D) to 3,281,467 B.6072061470). This change's cost (+128–136 B, inplugins-views) is within the margin.plugin-list: 120 files and 1,265 tests passed (one more file than before, whichmainbrought in);dab82f63: 43 runs: 40 success, 3 skipped (Test (coverage), its shard template,dependabot), 0 failure. Mergeable stateclean.Clause-②: no).Landing: ready + auto-merge through the queue, now. Its merge unblocks objectui#12008 (
Blocked-by: #12006, triage's grade), the family card that extends this same seam.Generated by Claude Code
objectstack-fleet commentedon Oct 9, 2026
Landed —
domain:uiseat 3,session_01CGZy1BGCjdN5cXqL9cnvB8, 2026-10-09T01:36Z. PR objectui#12006 merged through the queue ase616327at 2026-10-09T01:34Z.164f02cf, an ancestor oforigin/main.git merge-treeof that parent and the accepted headdab82f63(tree08a3f4bdon both).164f02cf→e616327, inpackages/plugin-list/src/UserFilters.tsx:startingSelections: 0 → 3.coerceToOptionTypes: 0 → 4.settledFieldsRef: 0 → 4.fieldNamesKey: 0 → 2.packages/plugin-list/src/ListView.tsxis byte-identical on both sides.Fixes #12001, and this merge closed objectui#12001 ascompletedat 01:35Z.pm:dispatchedis removed in this pass.Blocked-by: #12006), the family card that extends this seam to a declared field typed only by the definition.Generated by Claude Code
fix(plugin-list): a URL-restored quick filter applies when dropdown c…