You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Decision] Readers can hide list columns but not add one: should a reader be able to add an object's field to their list? #30077
The maintainer: 「等我决定的都要立卡,并且从业务的角度给我具体解释和我沟通」 (2026-11-04). Measured by the seat on main63b86b385d, on the example CRM unless noted. No domain:* label is set: triage routes this card.
The question
显示 → 隐藏字段 lists only the columns the view already has. A rep who wants 负责人 or 距成交天数 on their list has to ask an administrator to change the view for everyone. #28971's first version excluded a field picker.
Governing text: #28971 「第一版不包含:…字段选择器」, #28912. A reader's list state lives in sys_user_preference (perView.columnState) and personal views in sys_personal_view.
Measured
The opportunity list's 隐藏字段 lists 商机名称, 客户, 阶段, 金额, 赢单概率, 预期收入, 预计成交日期. The object also has 负责人, 距成交天数, 折扣 and others, offered in filter and sort but not here.
Options
A. Add columns, personal only: the panel lists every field the reader may read, and an added column is stored like a hidden one. Reset removes it.
B. Add columns only in My views, not on a shared view's personalization.
The maintainer, 2026-11-04: 「30079这种不是应该在元数据应用中自己去开发自定义操作吗?你的意见是批量换负责人放到内核吗?其他全部同意」.
Ruling: option A. A reader can add any field they may read to their own list, stored with their personalization and cleared by reset. needs-user-decision is removed; triage routes this card.
Read fresh. The ruling recorded on this card (A, 「其他全部同意」) is the scope. A reader can add any field they may read to their own list, stored with their personalization. p2 kept.
Claim: PM loop round 1
Session: session_01QSeKYnE9YNkSZen7sijLA3
Account: os-justin
Branch: claude/issue-30077-list-add-columns
Worktree: objectstack-issue-30077
Domain: domain:ui
Seat: domain:ui#2
File surface: ui/packages/plugin-list/src/ListView.tsx (the 隐藏字段 panel, allFields and its popover), ui/packages/plugin-list/src/ViewSettingsPopover.tsx (its Hide Fields section, if it is the same door), the reader's personalization store path for perView.columnState in ui/packages/app-shell/src/views/** only where the added column is persisted, their tests, one root .changeset/ file (stop on breach; explain in the report)
Container & model: M, mode:subagent, model: TIER_DEFAULT — --tier: "no path-derived mandate: the surface hits none of the 4 declared glob(s)", effort: high
Clause-②: no
Metadata-protocol: none
Responsibility: n/a — not a defect card
Thread-read: 6631370029
Premise: holds (the card's re-check names the wrong file: zero hits in objectViewListUrlState.ts; the panel is ui/packages/plugin-list/src/ListView.tsx, allFields built only from schema.columns)
Serial constraints cleared: none — no open domain:uipm:dispatched claim names plugin-list (read 2026-11-04T15:54Z)
Ruling-ref: 6631126993 (option A, the maintainer's 「其他全部同意」)
{
"issue": 30077,
"status": "done",
"branch": "claude/issue-30077-list-add-columns",
"pr": "https://github.com/objectstack-ai/objectstack/pull/30162",
"session": "session_01QSeKYnE9YNkSZen7sijLA3",
"premise_still_valid": true,
"summary": "Ruling A implemented. Both hide-fields lists (the wide toolbar popover and the Display panel's Hide Fields section in ViewSettingsPopover, which is the same door: it writes through the same updateHiddenFields callback) now list, after the view's columns, every other field of the object definition the caller may read (canReadField) and the definition does not mark hidden, unticked; ticking calls the new ListView host prop onAddedFieldsChange, unticking a reader-added column removes it instead of hiding it. The console stores the list as a new addedFields part of the reader's ui.list.OBJECT preference (beside hiddenFields), the host appends it to the columns rung (withAddedListColumns), so $select/$expand/render treat it as a declared column (lookups expand with no server change, pinned), Reset to view drops it with the entry, and a stored addition draws the Personalized marker as the existing 'columns' part. Mechanism assumptions measured: the card's re-check file has zero hits as the PM said; allFields was columns-only; the store's write path is ui/packages/app-shell/src/context/ListPreferencesProvider.tsx (not views/**), whose sanitizer drops undeclared parts. No spec change, no metadata key, no new i18n key.",
"tests": "On 916241ae1 (post-merge): 3 touched test files Test Files 3 passed (3), Tests 84 passed (84); plugin-list + app-shell type-check exit 0; turbo build of the app-shell closure 67/67. Pre-merge: full plugin-list suite Test Files 1 failed | 188 passed (189) — the one failure was this PR's own new lookup pin asserting a grid header the harness does not render; narrowed and re-run, ListView.fieldListRead-11984.test.tsx 31/31. app-shell 101 related files (all ObjectView*/InterfaceListPage* plus git-grep hits for ListPreferences/listPrefs/personaliz/onHiddenFieldsChange): 101/101 files, 880/880 tests. Reverse verification of the cross-package type: addedFields={42} via scripts/ablation-replace.mjs (anchor 1->0, blob 91185df42a4e->8287f4f432e4), app-shell tsc --noEmit TSC_EXIT=2 with TS2322 'number' not assignable to 'readonly string[]'; restored, blob == HEAD 91185df42a4e and git diff HEAD empty. No ablation: no permanent behaviour-test mutation needed beyond the reverse type check.",
"gates": "dispatch-gates --commands --repo objectstack-ai/objectstack on 916241ae1 derived 36; 35 re-run on 916241ae1 each exit 0 (exit captured before any pipe); pnpm --dir ui exec vitest run scripts/ exit 0 (170 passed | 2 skipped) on the pre-merge head, merge brings no ui/scripts change. --ran: 36 derived, 36 run, 0 NOT-MEASURED, 0 UNRUN (derived zero). Extra: check:changeset-fixed exit 0 (roster under .changeset), ui check:new-line-citations exit 0 (0 new), ui check:doc-types/doc-json/doc-fences/doc-example-readers exit 0. NOT MEASURED: ui check:doc-snippets and check:doc-examples, reason: prerequisite scoped 33-package build (exit 2), left to CI. CI check-runs: in_progress at report time (PR just opened); full app-shell suite, Lint UI, Type Check UI and the CI-only type-check lanes declared to CI. Raw control-byte self-scan of the 10 changed files: zero hits.",
"dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm",
"mcp_calls": "0 — no MCP GitHub tool called",
"api_writes": "2 — relay pr_create (repository_dispatch -> POST /repos/objectstack-ai/objectstack/pulls, draft, no labels, no assignees; read-back 9961 bytes identical) and post-stamped os-dev-report comment (POST /repos/objectstack-ai/objectstack/issues/30077/comments); plus git push of the branch (not REST)",
"files_changed": [
".changeset/30077-list-add-columns.md",
"ui/packages/app-shell/src/context/ListPreferencesProvider.tsx",
"ui/packages/app-shell/src/context/__tests__/ListPreferencesProvider.slot-12418.test.ts",
"ui/packages/app-shell/src/views/ObjectView.userListState-12418.test.tsx",
"ui/packages/app-shell/src/views/objectViewListRenderer.tsx",
"ui/packages/app-shell/src/views/objectViewListUrlState.ts",
"ui/packages/plugin-list/README.md",
"ui/packages/plugin-list/src/ListView.tsx",
"ui/packages/plugin-list/src/__tests__/ListView.fieldListRead-11984.test.tsx",
"ui/packages/plugin-list/src/components/ViewSettingsPopover.tsx"
],
"line_budget": "n/a — no skills/** file touched",
"deviations": [
"File surface: the claim scoped the store to ui/packages/app-shell/src/views/**; the store is ui/packages/app-shell/src/context/ListPreferencesProvider.tsx (the claim's named object, 'the reader's personalization store path'), so it was edited rather than stopping; ui/packages/plugin-list/README.md added for ui/AGENTS.md #2 (docs-driven). Both named in the PR's Acceptance notes.",
"app-shell suite narrowed locally to 101 related files of 2172; the full sharded run is CI's Test UI.",
"The full plugin-list suite was not re-run after the lookup-pin fix and the main merge; the changed file was (31/31 pre-merge, 3-file set 84/84 post-merge).",
"Commit trailers use AGENTS.md's model-free pair instead of the harness reminder's model-named Co-Authored-By line; the merge commit carries no trailer.",
"No browser dogfood: the grid header of an added column is not measured in a real app (the jsdom harness does not render the lazily registered grid)."
],
"open_questions": [],
"security_findings": "none",
"out_of_scope_findings": []
}
{
"issue": 30077,
"status": "done",
"branch": "claude/issue-30077-list-add-columns",
"pr": "https://github.com/objectstack-ai/objectstack/pull/30162",
"session": "session_01QSeKYnE9YNkSZen7sijLA3",
"premise_still_valid": true,
"summary": "Rework round 1: I drove the outcome through a real browser, and every step passed (14/14 assertions). No code change was needed, so the branch is unchanged at 916241ae1 and main was not merged. The backend was the example CRM on :4377 (fresh DB, seeded admin) and the Console was Vite from this branch's ui/ source on :5377, driven with Playwright Chromium, real clicks, en-US. On the opportunity list, Display > Hide Fields offers Owner (lookup) and Days to Close unticked. Ticking both adds two grid columns: Owner cells read 'Dev Admin', not an id, and Days to Close has values. Both survive a reload, and Reset to view removes them. Unticking an added column removes it from addedFields; it is not hidden, and after a reload it is still offered unticked. The 'Browser check' section for the PR body is in pr_body_section; I did not PATCH the body (see deviations).",
"tests": "Browser check, head 916241ae1, script /tmp/claude-0/-home-user-objectstack/f9f51d50-8b75-58d2-a29b-04e76abe5f26/scratchpad/issue-30077/pw/check.cjs. Log: browser-run2.log in the same folder, 'SUMMARY 14/14 passed', exit 0. The first run (browser-run1.log) stopped at its first click because the organization's first-run 'Set the default currency' dialog covered the page. The script now answers that dialog once; no product change was needed. Server truth was read through /api/v1/data/sys_user_preference at each step: after ticking, addedFields [owner_id, days_to_close]; after reset, {}; after the untick, [owner_id] and no hiddenFields. Screenshots 01-list-before, 02-panel-offered, 03-panel-ticked, 04-grid-added, 05-after-reload, 06-personalized-marker, 07-after-reset, 08-panel-untick, 09-grid-after-untick and 10-reload-after-untick (.png) are under issue-30077/shots/. Earlier round's local tests stand unchanged (no new commit).",
"gates": "No new commit, so the earlier round's local gate record stands for 916241ae1: 36 derived, 36 run, 0 NOT-MEASURED. PR check-runs on 916241ae1, read at report time: Lint & Repo Gates success, TypeScript Type Check success, Test Core success, Governed Surface Queue Guard success, Lint UI success, Type Check UI success, Build & E2E UI success, Live E2E UI success, Check Changeset success, Build Core skipped. Test UI shards: 3, 4, 5, 7 and 13 of 16 success, the rest in_progress.",
"dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm",
"mcp_calls": "0 — no MCP GitHub tool called (check-runs read through gh api, read only)",
"api_writes": "1 this round — post-stamped os-dev-report comment on #30077 (POST /repos/objectstack-ai/objectstack/issues/30077/comments via relay); no push, no PR body write",
"files_changed": [],
"line_budget": "n/a — no skills/** file touched",
"deviations": [
"PR body not edited. The rework order asked me to add the Browser check section to the PR body through the relay, but os-dev.md rule 3 says the dev writes the PR body once, at pr_create, never PATCHes it, and names any later change in the report for the seat to write. os-dev.md says it wins over a dispatch when they conflict, so I followed it and am flagging the conflict here. The exact section text is in pr_body_section, ready to append before the footer.",
"Screenshots stay in the scratchpad (issue-30077/shots/) and are not uploaded anywhere.",
"The first browser run stopped at the organization's first-run currency dialog (a fresh-DB fixture state). It was answered in the script, and the second run is the measurement."
],
"pr_body_section": "## Browser check\n\nReal Chromium (Playwright, real clicks and real typing), head `916241ae1`. Example CRM backend (`objectstack dev --seed-admin --fresh -p 4377` in `examples/app-crm`), Console built from this branch's `ui/` source (`vite --port 5377 --strictPort`, `DEV_PROXY_TARGET=http://localhost:4377`), locale en-US, signed in as the seeded admin, on Opportunities / All Opportunities (`crm_opportunity.all`). The organization's first-run \"Set the default currency\" dialog was answered once. 14 of 14 assertions passed. The screenshots are kept in the seat's scratchpad, not uploaded.\n\n1. **List opens on the view's columns.** Headers: Opportunity Name, Account, Stage, Deal Value, Win Probability (%), Expected Revenue, Expected Close. PASS (`01-list-before.png`)\n2. **Offered unticked.** In Display, Hide Fields, Owner (`owner_id`, a lookup, system tier) and Days to Close (a plain formula field) are listed after the view's columns, unticked, in the add list. Discount (%), Approval Status, Renewal Of and Products Total are offered too. PASS (`02-panel-offered.png`)\n3. **Tick both.** Both become ticked columns of the list. The grid header gains Owner and Days to Close. Owner cells read \"Dev Admin\" (the lookup's display name, not the id) and Days to Close cells carry values (45, -110, -30, 30). The reader's preference row `ui.list.crm_opportunity` reads `perView.crm_opportunity.all.addedFields = [\"owner_id\",\"days_to_close\"]`. PASS (`03-panel-ticked.png`, `04-grid-added.png`)\n4. **Reload.** Both columns are still there, and Owner still reads \"Dev Admin\". PASS (`05-after-reload.png`)\n5. **Reset to view.** The Personalized marker names \"Column layout\". After the reset both columns are gone and the stored row is `{}`. PASS (`06-personalized-marker.png`, `07-after-reset.png`)\n6. **Untick an added column.** Add both again, then untick Days to Close. It goes back to the add list unticked, and the grid keeps Owner and drops Days to Close. The stored row is `addedFields = [\"owner_id\"]`, with no `hiddenFields` and no `days_to_close` anywhere. After a reload it is still absent from the grid and still offered unticked in the add list, so it was removed, not hidden. PASS (`08-panel-untick.png`, `09-grid-after-untick.png`, `10-reload-after-untick.png`)\n\nSeen on the way, and not changed by this PR: two of the view's own columns are listed under their machine names in the Hide Fields list (`account`, `expected_revenue`), while the grid header reads Account and Expected Revenue. Reported to the seat as a finding.",
"open_questions": [],
"security_findings": "none",
"out_of_scope_findings": [
"class: a · reach: public door, the Console. On the example CRM's Opportunities / All Opportunities list (columns declared as { field } with no label), Display > Hide Fields lists the view's own columns 'account' and 'expected_revenue' under their machine names, while the grid header and the Filter/Sort lists read 'Account' and 'Expected Revenue' (screenshot 03-panel-ticked.png). Cause read from source: allFields falls back to the bare field name when a declared column has no label and the translation bundle has no entry, and it does not use the object definition's label, which the grid uses. This PR uses the definition's label only for reader-added columns. The fix lands in ui/packages/plugin-list/src/ListView.tsx (allFields). Not fixed here: the card is not a defect card, and the code was accepted as is. Dedupe words: hide fields machine name, column label fallback, allFields label, field name instead of label"
]
}
{
"issue": 30077,
"status": "done",
"branch": "claude/issue-30077-list-add-columns",
"pr": "https://github.com/objectstack-ai/objectstack/pull/30162",
"session": "session_01QSeKYnE9YNkSZen7sijLA3",
"premise_still_valid": true,
"summary": "Rework round 2: the out-of-scope label finding is folded in. In ui/packages/plugin-list/src/ListView.tsx, allFields now falls back to the object definition's field label for every column with no label of its own; before, only reader-added columns did. The full order is: translation, then the column's own label, then the definition's label, then the bare name. That order does NOT fully match the grid header, and I kept it as it was on purpose; see deviations. One pin was added, ablated red, and restored green. I re-ran the Playwright label step on the CRM: Hide Fields now lists Account and Expected Revenue, item for item equal to the grid header row (11-labels.png). The changeset has one new paragraph about the label. Head is cfcc48813; main was not merged.",
"tests": "Head cfcc48813 for everything below unless marked. 4 test files (3 touched plus the row-color pin): Test Files 4 passed (4), Tests 90 passed (90). Ablation of the new fallback: I replaced 'tFieldLabel(name, f.label || definitionLabel(name) || name)' with 'tFieldLabel(name, f.label || name)' using scripts/ablation-replace.mjs (anchor 1->0, blob 1554e6f48579->85fbe0a9998e). The pin ran with -t 'listed under the definition': Tests 1 failed | 31 skipped, 'expected [ title, status, Priority ] to include Status'. Restored: blob == HEAD 1554e6f48579, git diff HEAD empty, and the same pin re-run gave Tests 1 passed | 31 skipped. No build or dist step is needed, because the test imports ../ListView (src). Full plugin-list suite on ca9765862: Test Files 1 failed | 188 passed (189). The failure was ListView.rowColorSelect-12376, whose pin spelled the old raw label 'subject'; it now expects 'Subject' (spelling only, pin intent unchanged) and passes in the 4-file run. plugin-list type-check exit 0 (also on 0070816a3). Browser, Playwright Chromium, CRM on :4377 and Console Vite on :5377 from this branch, en-US: labels.cjs gives 'SUMMARY 3/3 passed' (log browser-run3.log). The declared columns read the same list as the grid header: Opportunity Name, Account, Stage, Deal Value, Win Probability (%), Expected Revenue, Expected Close. Screenshot /tmp/claude-0/-home-user-objectstack/f9f51d50-8b75-58d2-a29b-04e76abe5f26/scratchpad/issue-30077/shots/11-labels.png. Both servers were stopped by recorded PID.",
"gates": "dispatch-gates --commands --repo objectstack-ai/objectstack on cfcc48813 derived 36, the same set as before. All 36 ran on cfcc48813 with their exit codes captured: 35 exit 0, and pnpm --dir ui exec vitest run scripts/ exit 0 (170 passed | 2 skipped). --ran: 36 derived, 36 run, 0 NOT-MEASURED, 0 UNRUN (a derived zero). Raw control-byte scan of the round-2 files: zero hits. PR check-runs on cfcc48813, read at report time: 44 success, 11 skipped, none failed. Lint & Repo Gates success, TypeScript Type Check success, Test Core success, Governed Surface Queue Guard success, Lint UI success, Type Check UI success, Test UI success, Build & E2E UI success, Live E2E UI success, Check Changeset success, Build Core skipped.",
"dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm",
"mcp_calls": "0 — no MCP GitHub tool called (check-runs read through gh api, read only)",
"api_writes": "1 this round — post-stamped os-dev-report comment on #30077 (POST /repos/objectstack-ai/objectstack/issues/30077/comments via relay); git push of 3 commits (not REST); no PR body write",
"files_changed": [
".changeset/30077-list-add-columns.md",
"ui/packages/plugin-list/src/ListView.tsx",
"ui/packages/plugin-list/src/__tests__/ListView.fieldListRead-11984.test.tsx",
"ui/packages/plugin-list/src/__tests__/ListView.rowColorSelect-12376.test.tsx"
],
"line_budget": "n/a — no skills/** file touched",
"deviations": [
"Label order: the round-2 order spelled 'column label, then translation, then definition label, then name', and called that the existing order. The existing code is translation first: tFieldLabel(name, f.label || name) returns the translation when the bundle has one, and the column label only as its fallback. I kept that top order unchanged, as the order asked, and only replaced the last fallback (definition label, then name). Compared with the grid header (objectGridGenerateColumns.tsx: authoredHeader = resolveColumnLabel(col.label) wins; else resolveFieldLabel(translation, fallback = definition label || prettified name)), the order does NOT fully match. The grid puts the column's own label before the translation and prettifies the bare name ('Expected revenue'); the list puts the translation first and falls back to the raw name. The two agree whenever a column has no label of its own, which is the case this round fixes. Swapping the top two in allFields, to match the grid (objectui#12110 item 1), is a separate behaviour change; I left it for the seat to decide.",
"ListView.rowColorSelect-12376.test.tsx is a fourth test file, outside the three named. Its pin spelled the old raw label; this is a fixture triage (spelling of the expected label only), allowed under the claim's 'their tests'.",
"The full plugin-list suite was not re-run after the row-color fix. The 4-file set and the type-check were run on the final head.",
"PR body not edited (os-dev rule 3); the seat writes pr_body_section, which now covers the label fix."
],
"pr_body_section": "## Browser check\n\nReal Chromium (Playwright, real clicks and real typing), head `916241ae1`. Example CRM backend (`objectstack dev --seed-admin --fresh -p 4377` in `examples/app-crm`), Console built from this branch's `ui/` source (`vite --port 5377 --strictPort`, `DEV_PROXY_TARGET=http://localhost:4377`), locale en-US, signed in as the seeded admin, on Opportunities / All Opportunities (`crm_opportunity.all`). The organization's first-run \"Set the default currency\" dialog was answered once. 14 of 14 assertions passed. The screenshots are kept in the seat's scratchpad, not uploaded.\n\n1. **List opens on the view's columns.** Headers: Opportunity Name, Account, Stage, Deal Value, Win Probability (%), Expected Revenue, Expected Close. PASS (`01-list-before.png`)\n2. **Offered unticked.** In Display, Hide Fields, Owner (`owner_id`, a lookup, system tier) and Days to Close (a plain formula field) are listed after the view's columns, unticked, in the add list. Discount (%), Approval Status, Renewal Of and Products Total are offered too. PASS (`02-panel-offered.png`)\n3. **Tick both.** Both become ticked columns of the list. The grid header gains Owner and Days to Close. Owner cells read \"Dev Admin\" (the lookup's display name, not the id) and Days to Close cells carry values (45, -110, -30, 30). The reader's preference row `ui.list.crm_opportunity` reads `perView.crm_opportunity.all.addedFields = [\"owner_id\",\"days_to_close\"]`. PASS (`03-panel-ticked.png`, `04-grid-added.png`)\n4. **Reload.** Both columns are still there, and Owner still reads \"Dev Admin\". PASS (`05-after-reload.png`)\n5. **Reset to view.** The Personalized marker names \"Column layout\". After the reset both columns are gone and the stored row is `{}`. PASS (`06-personalized-marker.png`, `07-after-reset.png`)\n6. **Untick an added column.** Add both again, then untick Days to Close. It goes back to the add list unticked, and the grid keeps Owner and drops Days to Close. The stored row is `addedFields = [\"owner_id\"]`, with no `hiddenFields` and no `days_to_close` anywhere. After a reload it is still absent from the grid and still offered unticked in the add list, so it was removed, not hidden. PASS (`08-panel-untick.png`, `09-grid-after-untick.png`, `10-reload-after-untick.png`)\n\n**Rework round 2: labels** (head `cfcc48813`). The first run (head `916241ae1`) listed two of the view's own label-less columns under their machine names (`account`, `expected_revenue`) while the grid header read Account and Expected Revenue. `allFields` in `ListView.tsx` now falls back to the object definition's field label for every column with no label of its own, not only for reader-added ones. The order is: translation, then the column's own label, then the definition's label, then the field name.\n\n7. **Labels.** Display > Hide Fields lists the view's columns as Opportunity Name, Account, Stage, Deal Value, Win Probability (%), Expected Revenue, Expected Close. That is equal, item for item, to the grid header row, with no machine name left. Owner and Days to Close are still offered unticked. 3 of 3 assertions passed. PASS (`11-labels.png`)\n\nPin: `ListView.fieldListRead-11984.test.tsx`, test 'a column with no label of its own is listed under the definition's label, not its machine name' (`{ field: 'title' }`, `{ field: 'status' }` and `'priority'` list as Title, Status, Priority). The fallback was ablated and the pin turned red with `expected [ 'title', 'status', 'Priority' ] to include 'Status'`, then restored and green again. The Group and Row color pickers share this list, so they read the definition's label too. The one existing pin that spelled the old raw name (`ListView.rowColorSelect-12376.test.tsx`, `subject` becomes `Subject`) was updated, with its intent unchanged.",
"open_questions": [],
"security_findings": "none",
"out_of_scope_findings": []
}
Waits on: #30069 (PR #30156, seat domain:ui#3) landing first, per its Notify: serial · #30077 behind #30069; then a merge of origin/main and the maintainer's landing authorization in the seat session.
Activity
objectstack-fleet commentedon Nov 4, 2026
epic:#26676#1 · zhuangjianguo · session_cdb7a717def74e719988f089fbaa18ff
The maintainer, 2026-11-04: 「30079这种不是应该在元数据应用中自己去开发自定义操作吗?你的意见是批量换负责人放到内核吗?其他全部同意」.
Ruling: option A. A reader can add any field they may read to their own list, stored with their personalization and cleared by reset.
needs-user-decisionis removed; triage routes this card.objectstack-fleet commentedon Nov 4, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: first-touch audit · p2 kept ·
domain:ui· 2026-11-04T13:04Z⛔ Not a claim.
Generated by Claude Code
objectstack-fleet commentedon Nov 4, 2026
domain:ui#2 · os-justin · session_01QSeKYnE9YNkSZen7sijLA3
Claim: PM loop round 1
Session:
session_01QSeKYnE9YNkSZen7sijLA3Account:
os-justinBranch:
claude/issue-30077-list-add-columnsWorktree:
objectstack-issue-30077Domain:
domain:uiSeat:
domain:ui#2File surface:
ui/packages/plugin-list/src/ListView.tsx(the 隐藏字段 panel,allFieldsand its popover),ui/packages/plugin-list/src/ViewSettingsPopover.tsx(its Hide Fields section, if it is the same door), the reader's personalization store path forperView.columnStateinui/packages/app-shell/src/views/**only where the added column is persisted, their tests, one root.changeset/file (stop on breach; explain in the report)Container & model:
M,mode:subagent,model: TIER_DEFAULT — --tier: "no path-derived mandate: the surface hits none of the 4 declared glob(s)",effort: highClause-②: no
Metadata-protocol: none
Responsibility:
n/a — not a defect cardThread-read: 6631370029
Premise: holds (the card's re-check names the wrong file: zero hits in
objectViewListUrlState.ts; the panel isui/packages/plugin-list/src/ListView.tsx,allFieldsbuilt only fromschema.columns)Serial constraints cleared: none — no open
domain:uipm:dispatchedclaim namesplugin-list(read 2026-11-04T15:54Z)Ruling-ref: 6631126993 (option A, the maintainer's 「其他全部同意」)
objectstack-fleet commentedon Nov 4, 2026
domain:ui#2 · os-justin · session_01QSeKYnE9YNkSZen7sijLA3
os-dev-report
{ "issue": 30077, "status": "done", "branch": "claude/issue-30077-list-add-columns", "pr": "https://github.com/objectstack-ai/objectstack/pull/30162", "session": "session_01QSeKYnE9YNkSZen7sijLA3", "premise_still_valid": true, "summary": "Ruling A implemented. Both hide-fields lists (the wide toolbar popover and the Display panel's Hide Fields section in ViewSettingsPopover, which is the same door: it writes through the same updateHiddenFields callback) now list, after the view's columns, every other field of the object definition the caller may read (canReadField) and the definition does not mark hidden, unticked; ticking calls the new ListView host prop onAddedFieldsChange, unticking a reader-added column removes it instead of hiding it. The console stores the list as a new addedFields part of the reader's ui.list.OBJECT preference (beside hiddenFields), the host appends it to the columns rung (withAddedListColumns), so $select/$expand/render treat it as a declared column (lookups expand with no server change, pinned), Reset to view drops it with the entry, and a stored addition draws the Personalized marker as the existing 'columns' part. Mechanism assumptions measured: the card's re-check file has zero hits as the PM said; allFields was columns-only; the store's write path is ui/packages/app-shell/src/context/ListPreferencesProvider.tsx (not views/**), whose sanitizer drops undeclared parts. No spec change, no metadata key, no new i18n key.", "tests": "On 916241ae1 (post-merge): 3 touched test files Test Files 3 passed (3), Tests 84 passed (84); plugin-list + app-shell type-check exit 0; turbo build of the app-shell closure 67/67. Pre-merge: full plugin-list suite Test Files 1 failed | 188 passed (189) — the one failure was this PR's own new lookup pin asserting a grid header the harness does not render; narrowed and re-run, ListView.fieldListRead-11984.test.tsx 31/31. app-shell 101 related files (all ObjectView*/InterfaceListPage* plus git-grep hits for ListPreferences/listPrefs/personaliz/onHiddenFieldsChange): 101/101 files, 880/880 tests. Reverse verification of the cross-package type: addedFields={42} via scripts/ablation-replace.mjs (anchor 1->0, blob 91185df42a4e->8287f4f432e4), app-shell tsc --noEmit TSC_EXIT=2 with TS2322 'number' not assignable to 'readonly string[]'; restored, blob == HEAD 91185df42a4e and git diff HEAD empty. No ablation: no permanent behaviour-test mutation needed beyond the reverse type check.", "gates": "dispatch-gates --commands --repo objectstack-ai/objectstack on 916241ae1 derived 36; 35 re-run on 916241ae1 each exit 0 (exit captured before any pipe); pnpm --dir ui exec vitest run scripts/ exit 0 (170 passed | 2 skipped) on the pre-merge head, merge brings no ui/scripts change. --ran: 36 derived, 36 run, 0 NOT-MEASURED, 0 UNRUN (derived zero). Extra: check:changeset-fixed exit 0 (roster under .changeset), ui check:new-line-citations exit 0 (0 new), ui check:doc-types/doc-json/doc-fences/doc-example-readers exit 0. NOT MEASURED: ui check:doc-snippets and check:doc-examples, reason: prerequisite scoped 33-package build (exit 2), left to CI. CI check-runs: in_progress at report time (PR just opened); full app-shell suite, Lint UI, Type Check UI and the CI-only type-check lanes declared to CI. Raw control-byte self-scan of the 10 changed files: zero hits.", "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm", "mcp_calls": "0 — no MCP GitHub tool called", "api_writes": "2 — relay pr_create (repository_dispatch -> POST /repos/objectstack-ai/objectstack/pulls, draft, no labels, no assignees; read-back 9961 bytes identical) and post-stamped os-dev-report comment (POST /repos/objectstack-ai/objectstack/issues/30077/comments); plus git push of the branch (not REST)", "files_changed": [ ".changeset/30077-list-add-columns.md", "ui/packages/app-shell/src/context/ListPreferencesProvider.tsx", "ui/packages/app-shell/src/context/__tests__/ListPreferencesProvider.slot-12418.test.ts", "ui/packages/app-shell/src/views/ObjectView.userListState-12418.test.tsx", "ui/packages/app-shell/src/views/objectViewListRenderer.tsx", "ui/packages/app-shell/src/views/objectViewListUrlState.ts", "ui/packages/plugin-list/README.md", "ui/packages/plugin-list/src/ListView.tsx", "ui/packages/plugin-list/src/__tests__/ListView.fieldListRead-11984.test.tsx", "ui/packages/plugin-list/src/components/ViewSettingsPopover.tsx" ], "line_budget": "n/a — no skills/** file touched", "deviations": [ "File surface: the claim scoped the store to ui/packages/app-shell/src/views/**; the store is ui/packages/app-shell/src/context/ListPreferencesProvider.tsx (the claim's named object, 'the reader's personalization store path'), so it was edited rather than stopping; ui/packages/plugin-list/README.md added for ui/AGENTS.md #2 (docs-driven). Both named in the PR's Acceptance notes.", "app-shell suite narrowed locally to 101 related files of 2172; the full sharded run is CI's Test UI.", "The full plugin-list suite was not re-run after the lookup-pin fix and the main merge; the changed file was (31/31 pre-merge, 3-file set 84/84 post-merge).", "Commit trailers use AGENTS.md's model-free pair instead of the harness reminder's model-named Co-Authored-By line; the merge commit carries no trailer.", "No browser dogfood: the grid header of an added column is not measured in a real app (the jsdom harness does not render the lazily registered grid)." ], "open_questions": [], "security_findings": "none", "out_of_scope_findings": [] }objectstack-fleet commentedon Nov 4, 2026
domain:ui#2 · os-justin · session_01QSeKYnE9YNkSZen7sijLA3
os-dev-report
{ "issue": 30077, "status": "done", "branch": "claude/issue-30077-list-add-columns", "pr": "https://github.com/objectstack-ai/objectstack/pull/30162", "session": "session_01QSeKYnE9YNkSZen7sijLA3", "premise_still_valid": true, "summary": "Rework round 1: I drove the outcome through a real browser, and every step passed (14/14 assertions). No code change was needed, so the branch is unchanged at 916241ae1 and main was not merged. The backend was the example CRM on :4377 (fresh DB, seeded admin) and the Console was Vite from this branch's ui/ source on :5377, driven with Playwright Chromium, real clicks, en-US. On the opportunity list, Display > Hide Fields offers Owner (lookup) and Days to Close unticked. Ticking both adds two grid columns: Owner cells read 'Dev Admin', not an id, and Days to Close has values. Both survive a reload, and Reset to view removes them. Unticking an added column removes it from addedFields; it is not hidden, and after a reload it is still offered unticked. The 'Browser check' section for the PR body is in pr_body_section; I did not PATCH the body (see deviations).", "tests": "Browser check, head 916241ae1, script /tmp/claude-0/-home-user-objectstack/f9f51d50-8b75-58d2-a29b-04e76abe5f26/scratchpad/issue-30077/pw/check.cjs. Log: browser-run2.log in the same folder, 'SUMMARY 14/14 passed', exit 0. The first run (browser-run1.log) stopped at its first click because the organization's first-run 'Set the default currency' dialog covered the page. The script now answers that dialog once; no product change was needed. Server truth was read through /api/v1/data/sys_user_preference at each step: after ticking, addedFields [owner_id, days_to_close]; after reset, {}; after the untick, [owner_id] and no hiddenFields. Screenshots 01-list-before, 02-panel-offered, 03-panel-ticked, 04-grid-added, 05-after-reload, 06-personalized-marker, 07-after-reset, 08-panel-untick, 09-grid-after-untick and 10-reload-after-untick (.png) are under issue-30077/shots/. Earlier round's local tests stand unchanged (no new commit).", "gates": "No new commit, so the earlier round's local gate record stands for 916241ae1: 36 derived, 36 run, 0 NOT-MEASURED. PR check-runs on 916241ae1, read at report time: Lint & Repo Gates success, TypeScript Type Check success, Test Core success, Governed Surface Queue Guard success, Lint UI success, Type Check UI success, Build & E2E UI success, Live E2E UI success, Check Changeset success, Build Core skipped. Test UI shards: 3, 4, 5, 7 and 13 of 16 success, the rest in_progress.", "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm", "mcp_calls": "0 — no MCP GitHub tool called (check-runs read through gh api, read only)", "api_writes": "1 this round — post-stamped os-dev-report comment on #30077 (POST /repos/objectstack-ai/objectstack/issues/30077/comments via relay); no push, no PR body write", "files_changed": [], "line_budget": "n/a — no skills/** file touched", "deviations": [ "PR body not edited. The rework order asked me to add the Browser check section to the PR body through the relay, but os-dev.md rule 3 says the dev writes the PR body once, at pr_create, never PATCHes it, and names any later change in the report for the seat to write. os-dev.md says it wins over a dispatch when they conflict, so I followed it and am flagging the conflict here. The exact section text is in pr_body_section, ready to append before the footer.", "Screenshots stay in the scratchpad (issue-30077/shots/) and are not uploaded anywhere.", "The first browser run stopped at the organization's first-run currency dialog (a fresh-DB fixture state). It was answered in the script, and the second run is the measurement." ], "pr_body_section": "## Browser check\n\nReal Chromium (Playwright, real clicks and real typing), head `916241ae1`. Example CRM backend (`objectstack dev --seed-admin --fresh -p 4377` in `examples/app-crm`), Console built from this branch's `ui/` source (`vite --port 5377 --strictPort`, `DEV_PROXY_TARGET=http://localhost:4377`), locale en-US, signed in as the seeded admin, on Opportunities / All Opportunities (`crm_opportunity.all`). The organization's first-run \"Set the default currency\" dialog was answered once. 14 of 14 assertions passed. The screenshots are kept in the seat's scratchpad, not uploaded.\n\n1. **List opens on the view's columns.** Headers: Opportunity Name, Account, Stage, Deal Value, Win Probability (%), Expected Revenue, Expected Close. PASS (`01-list-before.png`)\n2. **Offered unticked.** In Display, Hide Fields, Owner (`owner_id`, a lookup, system tier) and Days to Close (a plain formula field) are listed after the view's columns, unticked, in the add list. Discount (%), Approval Status, Renewal Of and Products Total are offered too. PASS (`02-panel-offered.png`)\n3. **Tick both.** Both become ticked columns of the list. The grid header gains Owner and Days to Close. Owner cells read \"Dev Admin\" (the lookup's display name, not the id) and Days to Close cells carry values (45, -110, -30, 30). The reader's preference row `ui.list.crm_opportunity` reads `perView.crm_opportunity.all.addedFields = [\"owner_id\",\"days_to_close\"]`. PASS (`03-panel-ticked.png`, `04-grid-added.png`)\n4. **Reload.** Both columns are still there, and Owner still reads \"Dev Admin\". PASS (`05-after-reload.png`)\n5. **Reset to view.** The Personalized marker names \"Column layout\". After the reset both columns are gone and the stored row is `{}`. PASS (`06-personalized-marker.png`, `07-after-reset.png`)\n6. **Untick an added column.** Add both again, then untick Days to Close. It goes back to the add list unticked, and the grid keeps Owner and drops Days to Close. The stored row is `addedFields = [\"owner_id\"]`, with no `hiddenFields` and no `days_to_close` anywhere. After a reload it is still absent from the grid and still offered unticked in the add list, so it was removed, not hidden. PASS (`08-panel-untick.png`, `09-grid-after-untick.png`, `10-reload-after-untick.png`)\n\nSeen on the way, and not changed by this PR: two of the view's own columns are listed under their machine names in the Hide Fields list (`account`, `expected_revenue`), while the grid header reads Account and Expected Revenue. Reported to the seat as a finding.", "open_questions": [], "security_findings": "none", "out_of_scope_findings": [ "class: a · reach: public door, the Console. On the example CRM's Opportunities / All Opportunities list (columns declared as { field } with no label), Display > Hide Fields lists the view's own columns 'account' and 'expected_revenue' under their machine names, while the grid header and the Filter/Sort lists read 'Account' and 'Expected Revenue' (screenshot 03-panel-ticked.png). Cause read from source: allFields falls back to the bare field name when a declared column has no label and the translation bundle has no entry, and it does not use the object definition's label, which the grid uses. This PR uses the definition's label only for reader-added columns. The fix lands in ui/packages/plugin-list/src/ListView.tsx (allFields). Not fixed here: the card is not a defect card, and the code was accepted as is. Dedupe words: hide fields machine name, column label fallback, allFields label, field name instead of label" ] }objectstack-fleet commentedon Nov 4, 2026
domain:ui#2 · os-justin · session_01QSeKYnE9YNkSZen7sijLA3
os-dev-report
{ "issue": 30077, "status": "done", "branch": "claude/issue-30077-list-add-columns", "pr": "https://github.com/objectstack-ai/objectstack/pull/30162", "session": "session_01QSeKYnE9YNkSZen7sijLA3", "premise_still_valid": true, "summary": "Rework round 2: the out-of-scope label finding is folded in. In ui/packages/plugin-list/src/ListView.tsx, allFields now falls back to the object definition's field label for every column with no label of its own; before, only reader-added columns did. The full order is: translation, then the column's own label, then the definition's label, then the bare name. That order does NOT fully match the grid header, and I kept it as it was on purpose; see deviations. One pin was added, ablated red, and restored green. I re-ran the Playwright label step on the CRM: Hide Fields now lists Account and Expected Revenue, item for item equal to the grid header row (11-labels.png). The changeset has one new paragraph about the label. Head is cfcc48813; main was not merged.", "tests": "Head cfcc48813 for everything below unless marked. 4 test files (3 touched plus the row-color pin): Test Files 4 passed (4), Tests 90 passed (90). Ablation of the new fallback: I replaced 'tFieldLabel(name, f.label || definitionLabel(name) || name)' with 'tFieldLabel(name, f.label || name)' using scripts/ablation-replace.mjs (anchor 1->0, blob 1554e6f48579->85fbe0a9998e). The pin ran with -t 'listed under the definition': Tests 1 failed | 31 skipped, 'expected [ title, status, Priority ] to include Status'. Restored: blob == HEAD 1554e6f48579, git diff HEAD empty, and the same pin re-run gave Tests 1 passed | 31 skipped. No build or dist step is needed, because the test imports ../ListView (src). Full plugin-list suite on ca9765862: Test Files 1 failed | 188 passed (189). The failure was ListView.rowColorSelect-12376, whose pin spelled the old raw label 'subject'; it now expects 'Subject' (spelling only, pin intent unchanged) and passes in the 4-file run. plugin-list type-check exit 0 (also on 0070816a3). Browser, Playwright Chromium, CRM on :4377 and Console Vite on :5377 from this branch, en-US: labels.cjs gives 'SUMMARY 3/3 passed' (log browser-run3.log). The declared columns read the same list as the grid header: Opportunity Name, Account, Stage, Deal Value, Win Probability (%), Expected Revenue, Expected Close. Screenshot /tmp/claude-0/-home-user-objectstack/f9f51d50-8b75-58d2-a29b-04e76abe5f26/scratchpad/issue-30077/shots/11-labels.png. Both servers were stopped by recorded PID.", "gates": "dispatch-gates --commands --repo objectstack-ai/objectstack on cfcc48813 derived 36, the same set as before. All 36 ran on cfcc48813 with their exit codes captured: 35 exit 0, and pnpm --dir ui exec vitest run scripts/ exit 0 (170 passed | 2 skipped). --ran: 36 derived, 36 run, 0 NOT-MEASURED, 0 UNRUN (a derived zero). Raw control-byte scan of the round-2 files: zero hits. PR check-runs on cfcc48813, read at report time: 44 success, 11 skipped, none failed. Lint & Repo Gates success, TypeScript Type Check success, Test Core success, Governed Surface Queue Guard success, Lint UI success, Type Check UI success, Test UI success, Build & E2E UI success, Live E2E UI success, Check Changeset success, Build Core skipped.", "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm", "mcp_calls": "0 — no MCP GitHub tool called (check-runs read through gh api, read only)", "api_writes": "1 this round — post-stamped os-dev-report comment on #30077 (POST /repos/objectstack-ai/objectstack/issues/30077/comments via relay); git push of 3 commits (not REST); no PR body write", "files_changed": [ ".changeset/30077-list-add-columns.md", "ui/packages/plugin-list/src/ListView.tsx", "ui/packages/plugin-list/src/__tests__/ListView.fieldListRead-11984.test.tsx", "ui/packages/plugin-list/src/__tests__/ListView.rowColorSelect-12376.test.tsx" ], "line_budget": "n/a — no skills/** file touched", "deviations": [ "Label order: the round-2 order spelled 'column label, then translation, then definition label, then name', and called that the existing order. The existing code is translation first: tFieldLabel(name, f.label || name) returns the translation when the bundle has one, and the column label only as its fallback. I kept that top order unchanged, as the order asked, and only replaced the last fallback (definition label, then name). Compared with the grid header (objectGridGenerateColumns.tsx: authoredHeader = resolveColumnLabel(col.label) wins; else resolveFieldLabel(translation, fallback = definition label || prettified name)), the order does NOT fully match. The grid puts the column's own label before the translation and prettifies the bare name ('Expected revenue'); the list puts the translation first and falls back to the raw name. The two agree whenever a column has no label of its own, which is the case this round fixes. Swapping the top two in allFields, to match the grid (objectui#12110 item 1), is a separate behaviour change; I left it for the seat to decide.", "ListView.rowColorSelect-12376.test.tsx is a fourth test file, outside the three named. Its pin spelled the old raw label; this is a fixture triage (spelling of the expected label only), allowed under the claim's 'their tests'.", "The full plugin-list suite was not re-run after the row-color fix. The 4-file set and the type-check were run on the final head.", "PR body not edited (os-dev rule 3); the seat writes pr_body_section, which now covers the label fix." ], "pr_body_section": "## Browser check\n\nReal Chromium (Playwright, real clicks and real typing), head `916241ae1`. Example CRM backend (`objectstack dev --seed-admin --fresh -p 4377` in `examples/app-crm`), Console built from this branch's `ui/` source (`vite --port 5377 --strictPort`, `DEV_PROXY_TARGET=http://localhost:4377`), locale en-US, signed in as the seeded admin, on Opportunities / All Opportunities (`crm_opportunity.all`). The organization's first-run \"Set the default currency\" dialog was answered once. 14 of 14 assertions passed. The screenshots are kept in the seat's scratchpad, not uploaded.\n\n1. **List opens on the view's columns.** Headers: Opportunity Name, Account, Stage, Deal Value, Win Probability (%), Expected Revenue, Expected Close. PASS (`01-list-before.png`)\n2. **Offered unticked.** In Display, Hide Fields, Owner (`owner_id`, a lookup, system tier) and Days to Close (a plain formula field) are listed after the view's columns, unticked, in the add list. Discount (%), Approval Status, Renewal Of and Products Total are offered too. PASS (`02-panel-offered.png`)\n3. **Tick both.** Both become ticked columns of the list. The grid header gains Owner and Days to Close. Owner cells read \"Dev Admin\" (the lookup's display name, not the id) and Days to Close cells carry values (45, -110, -30, 30). The reader's preference row `ui.list.crm_opportunity` reads `perView.crm_opportunity.all.addedFields = [\"owner_id\",\"days_to_close\"]`. PASS (`03-panel-ticked.png`, `04-grid-added.png`)\n4. **Reload.** Both columns are still there, and Owner still reads \"Dev Admin\". PASS (`05-after-reload.png`)\n5. **Reset to view.** The Personalized marker names \"Column layout\". After the reset both columns are gone and the stored row is `{}`. PASS (`06-personalized-marker.png`, `07-after-reset.png`)\n6. **Untick an added column.** Add both again, then untick Days to Close. It goes back to the add list unticked, and the grid keeps Owner and drops Days to Close. The stored row is `addedFields = [\"owner_id\"]`, with no `hiddenFields` and no `days_to_close` anywhere. After a reload it is still absent from the grid and still offered unticked in the add list, so it was removed, not hidden. PASS (`08-panel-untick.png`, `09-grid-after-untick.png`, `10-reload-after-untick.png`)\n\n**Rework round 2: labels** (head `cfcc48813`). The first run (head `916241ae1`) listed two of the view's own label-less columns under their machine names (`account`, `expected_revenue`) while the grid header read Account and Expected Revenue. `allFields` in `ListView.tsx` now falls back to the object definition's field label for every column with no label of its own, not only for reader-added ones. The order is: translation, then the column's own label, then the definition's label, then the field name.\n\n7. **Labels.** Display > Hide Fields lists the view's columns as Opportunity Name, Account, Stage, Deal Value, Win Probability (%), Expected Revenue, Expected Close. That is equal, item for item, to the grid header row, with no machine name left. Owner and Days to Close are still offered unticked. 3 of 3 assertions passed. PASS (`11-labels.png`)\n\nPin: `ListView.fieldListRead-11984.test.tsx`, test 'a column with no label of its own is listed under the definition's label, not its machine name' (`{ field: 'title' }`, `{ field: 'status' }` and `'priority'` list as Title, Status, Priority). The fallback was ablated and the pin turned red with `expected [ 'title', 'status', 'Priority' ] to include 'Status'`, then restored and green again. The Group and Row color pickers share this list, so they read the definition's label too. The one existing pin that spelled the old raw name (`ListView.rowColorSelect-12376.test.tsx`, `subject` becomes `Subject`) was updated, with its intent unchanged.", "open_questions": [], "security_findings": "none", "out_of_scope_findings": [] }objectstack-fleet commentedon Nov 4, 2026
domain:ui#2 · os-justin · session_01QSeKYnE9YNkSZen7sijLA3
ACCEPT → PR #30162 comment 6639399305 (head
cfcc4881, 2026-11-04T19:00Z).Waits on: #30069 (PR #30156, seat
domain:ui#3) landing first, per itsNotify: serial · #30077 behind #30069; then a merge oforigin/mainand the maintainer's landing authorization in the seat session.objectstack-fleet commentedon Nov 5, 2026
domain:ui#2 · os-justin · session_01QSeKYnE9YNkSZen7sijLA3
Landed · 2026-11-05T01:42Z
dfafae8132, after #30069 (PR #30156) as the serial note requiredgit grep -c "addedFields" origin/main -- ui/packages/app-shell/src/context/ListPreferencesProvider.tsxState: card closed
completedbyFixes #30077;pm:dispatchedremoved in this stroke.