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
A list's selection bar still draws a bulk action that a disabled package withholds, as a ghost button that toasts "not executable" #30169
Measured by HotCRM on platform 18.3.0 (hotcrm mainf98025c, fresh dev boot, admin) while finishing hotcrm#2383. The implementer's report is hotcrm#2383 comment 6638927959. objectstack#28877 fixed the record header, the More actions menu and the row menu: with app.objectstack.hotcrm.marketing disabled, create_campaign leaves /meta/action, its by-name read answers 404, and none of those menus draws it. One surface still draws it.
#
Observed on 18.3.0
Seam
Suggestion
1
Marketing disabled. Leads → All Leads, tick a row. The selection bar draws Create Campaign (on All Contacts, Add Contact To Campaign), a humanized name, not the action's label. Clicking it toasts: Action "create_campaign" is not executable: no handler is registered under that name and it declares no api, endpoint, navigate, redirect or onClick. With marketing on, the same bar draws Add to Campaign, which works.
ListView.bulkActions names the action as a bare string. In the renderer, plugin-gridresolveBulkActions keeps a name it cannot resolve (pinned in ui/packages/plugin-grid/src/__tests__/bulkActionMembers-8071.test.tsx row 3), and ObjectGrid draws it, while the server withholds the action (isActionWithheldByDisabledPackage, #28877).
The selection bar drops a bare-string bulk action the server does not serve, as the row menu already does. If keeping an unresolved name is deliberate for some other case, a name the server withholds for a disabled package is the case to drop.
HotCRM declares these bulk actions as bare strings, which the spec accepts, and has nothing to change on its side. hotcrm#2383 closes with its tests and docs. HotCRM re-measures the bar when its pin carries this fix.
Filing gate ① a reproduced defect. Filed by the repo:hotcrm seat (huangyiirene, session session_01J8VVAnPkgK6CWrJa6t7Fk5).
Grade: priority:p3. A customer who turns a module off sees one dead button in a list's selection bar. Nothing is written. Size: S.
Dedupe: searched open objectstack issues for bulkActions unresolved, resolveBulkActions, not executable bulk and withheld action selection bar. The hits are closed bulk-action cards on other questions (#26951, #27050, #22780, #17916) and QA run logs. #28877, now closed, covered the other menus and not the selection bar.
Filed by the repo:hotcrm lane seat from hotcrm#2383's out-of-scope finding.
Claim: PM loop round 4
Session: session_01QSeKYnE9YNkSZen7sijLA3
Account: os-justin
Branch: claude/issue-30169-bulk-bar-withheld-action
Worktree: objectstack-issue-30169
Domain: domain:ui
Seat: domain:ui#2
File surface: ui/packages/plugin-grid/src/resolveBulkActions.ts and ObjectGrid.tsx (the selection bar's bulk-action resolution only), ui/packages/plugin-list/src/ListView.tsx only if the selection bar's action set is read there, their tests including ui/packages/plugin-grid/src/__tests__/bulkActionMembers-8071.test.tsx, one root .changeset/ file; ⛔ no server or spec change (stop on breach; explain in the report)
Container & model: M, mode:subagent, model: TIER_DEFAULT — --tier: "no path-derived mandate"; a pinned row (8071 row 3) is flipped, which needs judgment, effort: high
Clause-②: no
Metadata-protocol: none
Responsibility: ui/packages/plugin-grid resolveBulkActions keeps an unresolvable bare-string name and ObjectGrid draws it | #28877 already drops a withheld action from the header, More menu and row menu | any reader of a list whose app has a disabled package; hit today by HotCRM on 18.3.0
Thread-read: 6640586422
Premise: holds (seat read on origin/mainfec1f586: ObjectGrid.tsx:1260resolveBulkActions returns unresolved; server predicate isActionWithheldByDisabledPackage in packages/metadata-protocol/src/protocol-authoring-refusals.ts:73)
Serial constraints cleared: #30077's PR #30162 (this seat, ACCEPTed, awaiting landing) edits ListView.tsx's Hide Fields list, a different region; if this card touches ListView.tsx, it merges origin/main after #30162 lands. No other open domain:ui claim names plugin-grid (read 2026-11-04T21:40Z).
{
"issue": 30169,
"status": "needs_decision",
"branch": "claude/issue-30169-bulk-bar-withheld-action",
"pr": null,
"session": "session_01QSeKYnE9YNkSZen7sijLA3",
"premise_still_valid": true,
"summary": "The defect holds on origin/main fec1f586. #28877 (commit d0df87310) fixed the header, the More menu and the row menu on the server only: it touches no ui/ file. The withheld action is gone from the served object's `actions`, and those menus have no bare-name fallback, so they draw nothing. The selection bar reads that same served `actions` list. A withheld name is therefore unresolved, and `ObjectGrid` draws unresolved `bulkActions` names by humanized name. That is row 3 of `bulkActionMembers-8071.test.tsx`. The reason row 3 keeps the name is real: a host can register a runner handler under that name. `objectBulkActionDispatch.test.tsx` pins that path ('keeps an unresolvable name alongside the promoted defs', through `ActionProvider handlers`). The PM mechanism assumption is half disproved. A withheld name is absent from the served `/meta/action` set, but so is a handler-only name. A runner handler is client code and never metadata, and the server makes a withheld name read like one that never existed (#28117 ruling A). So the served `/meta/action` set cannot tell a withheld name from a handler-backed one, and nothing in the client reads that set today. What can tell them apart is the runner's handler registry: `ActionRunner.handlers` is private and has no public query (only `launchRefusal`, which reads a handler's `refuseBeforeInput` and not the 'not executable' dead end). Reading it needs a change in `ui/packages/core`, which is outside the claimed file surface. Per the order I stopped there. No code was written.",
"tests": "None run: no diff. Evidence is a code reading at fec1f586: `resolveBulkActions` (the 'Names that resolve to nothing stay in `unresolved`' docblock paragraph), `ObjectGrid` (`effectiveBulkActions` takes `unresolvedBulkActions`; the by-name dispatch `executeAction({ type: action, params: { records } })`), `ActionRunner.execute` (handler, then builtin, then `executeActionSchema`, which gives the toast HotCRM measured), and `ActionProvider` (it registers `handlers` synchronously in its runner `useMemo`). NOT MEASURED: the objectui#3002 issue body. Reason: GitHub API access to objectstack-ai/objectui is not enabled for this session (git-only read), and this checkout is shallow past the ui/ subtree import. The in-tree docblock and the dispatch pin carry the reason.",
"gates": "n/a: no diff, so no derivation was run and nothing was pushed except the empty branch marker.",
"dogfood_battery": "n/a: no PR",
"mcp_calls": "1: mcp__claude-code-remote__add_repo (read access to objectstack-ai/objectui; it answered read_available and attached nothing). GitHub MCP calls: 0.",
"api_writes": "1: POST /repos/objectstack-ai/objectstack/issues/30169/comments (this report, via scripts/pm/post-stamped.mjs). The git push of the empty branch is not a REST write.",
"open_questions": [
{
"question": "How does the selection bar tell a bare `bulkActions` name the server withholds from one a host serves through a registered runner handler? Both are absent from the served object's `actions` and from `/meta/action`.",
"options": [
"A. Ask the runner. Add one public read-only method to `@object-ui/core` `ActionRunner` (for example `hasHandler(name)`, which answers `this.handlers.has(name)`). `ObjectGrid` then keeps an unresolved name only when `useAction().runner` has a handler under it. Withheld, mistyped and other dead names draw no button. The handler path (row 3's reason and the dispatch pin) keeps working. Row 3 flips to two assertions: with no handler, no `bulk-action-NAME` button; with a handler registered, the button is drawn by name. A withheld-trigger test is added (served object without the action: no button; with it: the action's label), plus an ablation of the drop. Cost: the file surface widens by `ui/packages/core/src/actions/ActionRunner.ts` and a core unit test, and a published package gains a public method (patch changeset for `@object-ui/core` and `@object-ui/plugin-grid`). Caveat: a handler a host registers in an effect after the grid's first render is read on the grid's next render, which comes when its rows arrive. Effort S.",
"B. Read the served `/meta/action` set (the order's mechanism). `ObjectGrid` calls `useMetadata().ensureType('action')` and drops an unresolved name that set lacks while `getTypeStatus('action')` is 'ready'. This stays on the claimed surface. But it drops a handler-only name in every host that mounts a MetadataProvider, the Console included, which is the case row 3 protects. The existing pins would stay green only because their harness mounts no MetadataProvider: a false green. It also adds a `/meta/action` fetch that no other surface makes today. Effort S.",
"C. Drop every unresolved name outright. On the surface and smallest, but it removes the handler-by-name capability. That is a product change: the dispatch pin 'keeps an unresolvable name alongside the promoted defs' flips with row 3. Effort XS."
],
"recommendation": "A, because it is the only option that drops the withheld ghost and keeps the pinned handler path. It reuses the runner's own rule (a by-name dispatch with no handler ends in 'not executable'), so no second rule is written. Its one cost is widening the file surface into `@object-ui/core`, and the order told me to stop on that breach. The dispatch carried no decision axes, so this recommendation weighs only the stated cost and effort; paste the axes if you want an axis-by-axis analysis."
}
],
"security_findings": "none",
"out_of_scope_findings": [
"carrier: this card. The row menu's legacy `rowActions` fold (`resolveLegacyRowActions`, whose unresolved names reach the kebab as `customRowActions`) keeps unresolved names on the same registerHandler reasoning, so a withheld bare `rowActions` name would draw the same ghost. This is a read-only inference and was not reproduced (HotCRM places those actions through `locations`). Whatever rule the decision picks for the bulk bar applies to this fold too. Dedupe words: rowActions unresolved, resolveLegacyRowActions withheld, customRowActions ghost."
]
}
Governing text: packages/spec/src/ui/bulk-action.zod.ts:406 — "bulkActions: ['<action_name>'] — names an action the object declares." The renderer's tolerance of a name the object does not declare is in ui/packages/plugin-grid/src/resolveBulkActions.ts:78-82 ("Names that resolve to nothing stay in unresolved"), pinned by bulkActionMembers-8071.test.tsx:202-212 (row 3) and objectBulkActionDispatch.test.tsx:213-228. Authoring already refuses that name: packages/lint/src/validate-action-name-refs.ts (action-name-undefined, severity error, ADR-0049) rejects an undeclared bulkActions[]/rowActions[] name and says "the runtime ships NO built-in action names … a name resolving nowhere is dead, full stop" (:41-50), citing the record:alert/page:header precedent of drawing nothing for an unresolved id (#20105). The resolver's "that path must keep working" comment contradicts lint, and lint carries the ADR. Server rule: isActionWithheldByDisabledPackage (packages/metadata-protocol/src/protocol-authoring-refusals.ts:73, #28877); "disabled has exactly one meaning: hidden, refused on the data plane, data kept" (docs/adr/0070-package-first-authoring.md:66; "ruling A: disabled = hidden" at packages/objectql/src/registry.ts:7796).
Protocol: no packages/spec change in any option. C brings the renderer back to the spec's own sentence. A widens a published ui package's API (@object-ui/core).
前提(re-check,席位与隔离复核亲读)
The bar draws unresolved names: ObjectGrid.tsx:1260 (unresolved: unresolvedBulkActions) and :1287; it dispatches by name at :2020.
No in-repo app uses a handler-only bulk name. Every non-test bulkActions string in examples/, ui/ and packages/ names a declared action (for example examples/app-showcase/src/ui/actions/index.ts:57,179). The only exception is the demo ui/packages/plugin-grid/demo/bulk-actions.tsx:241 (legacy_only_handler). The Console's create/delete/navigate/refresh handlers (useObjectActions.ts:129-159) serve other surfaces; delete is routed away from the runner on purpose (ObjectGrid.tsx:1250-1258). HotCRM is not in this repo; that its bare strings name declared actions rests on A list's selection bar still draws a bulk action that a disabled package withholds, as a ghost button that toasts "not executable" #30169's own text ("With marketing on, the same bar draws Add to Campaign, which works").
ActionRunner's handler map is private (ui/packages/core/src/actions/ActionRunner.ts:1365), with no public query; useAction().runner is public (ui/packages/react/src/context/ActionContext.tsx:65).
The legacy row menu has the same tolerance (resolveLegacyRowActions.ts:35-37 → ObjectGrid.tsx:1002customRowActions).
① 长远:C 把渲染器拉回 spec 原文(bulk-action.zod.ts:406)与 lint action-name-undefined(ADR-0049),去掉一条特例;A 为特例加公开方法;B 误删。
② 拉动:幽灵按钮在 HotCRM 18.3.0 实测;前端接管的批量名字仓内零在用方(只有测试和 demo)。
③ 防错:lint 已在发布前以 error 拒收;C 让运行时一致,开发态警告作第二道网;A 让拼错名字静默消失;B 假绿。
④ 不扩散:C 删一条零在用宽容;A 为零拉动能力新增公开面。
Analysis-tier: seat (TIER_DEFAULT); reviewed by an isolated CONTRACT_REVIEW_TIER review, which returned FAIL with 6 required corrections. All 6 are applied in this text, and the recommendation moved from A to C on the corrected facet ② and the spec sentence it surfaced. A second isolated review of the C draft returned PASS with 5 corrections (the lint rule, the README scope, ADR paths, the HotCRM source, test spans), all applied.
Prior rulings read: resolveBulkActions, unresolved bulk, #28877, #28117 → 1 hit (#28117 ruling record, re-affirmed by letter A); ADR none; thread: none.
Release: claim 6642827748 (session_01QSeKYnE9YNkSZen7sijLA3, seat domain:ui#2) is released into the decision inbox, 2026-11-04T21:59Z. No code was written. Branch claude/issue-30169-bulk-bar-withheld-action is an empty marker at fec1f586 and holds no commits. The dev report is 6642981552, and the seat analysis is 6643188402. After the ruling, any domain:ui seat re-claims the card and dispatches the letter chosen. The analysis names the scope for each letter.
Ruling recorded · #30169 · letter C · 2026-11-05T06:51Z
The maintainer (os-justin), verbatim, in seat domain:ui#2's session: 「同意」. This answers the seat's business explanation of its two pending recommendations, which named this card as C (analysis 6643188402, options A/B/C).
Ruling: option C. The selection bar draws only actions the object declares. An undeclared bare bulkActions name draws nothing, and in development the console logs one warning naming it. The same rule applies to the legacy rowActions fold. This follows packages/spec/src/ui/bulk-action.zod.ts:406 ("names an action the object declares") and lint action-name-undefined (ADR-0049). The renderer's tolerance of handler-only names is retired.
Execution (from the analysis):
ui/packages/plugin-grid/src/resolveBulkActions.ts and resolveLegacyRowActions.ts: drop unresolved names and add the dev-mode warning.
Flip bulkActionMembers-8071.test.tsx row 3 and objectBulkActionDispatch.test.tsx ("keeps an unresolvable name…") to substantive assertions of the new behaviour, with the trigger named in the test.
Update the demo ui/packages/plugin-grid/demo/bulk-actions.tsx (legacy_only_handler) and ui/packages/plugin-grid/README.md (bulkActions: ['delete', 'export']; export is handler-only).
Add a trigger test: with a disabled package's action withheld, the bar draws no button for it; when served, it draws under its label. Ablate the drop.
delete keeps its own path (ObjectGrid.tsxkeptDeleteAction / onBulkDelete) and is untouched.
State: needs-user-decision → pm:queue in this stroke. The card is unassigned; any domain:ui seat may claim it. The branch claude/issue-30169-bulk-bar-withheld-action is an empty marker at fec1f586, with no Resume-from.
Claim: PM loop round 1 · 2026-11-05T07:23Z
Session: session_01Utp6Cb5R6JDdfyvtj3dPR2 (seat); dev = card thread session_01Utp6CZ1rxZPzain2p5bv5x
Account: os-bill
Branch: claude/project-thread-8y2k2m (harness-pinned card thread branch; the claude/issue-N-slug convention does not apply to a project card thread)
Worktree: objectstack-issue-30169
Domain: domain:ui
Seat: domain:ui#2
File surface: ui/packages/plugin-grid/src/** (resolveBulkActions, resolveLegacyRowActions, ObjectGrid only if needed), ui/packages/plugin-grid/demo/**, ui/packages/plugin-grid/README.md, their tests and the consumer tests the flip breaks, one root .changeset/ (stop on breach; explain in the report)
Container & model: M, mode:thread, model: default tier (opus) — dispatch-gates --tier: no path-derived mandate, effort: high
Clause-②: no (narrowing) — the renderer stops drawing an undeclared bulk action name; packages/spec/src/ui/bulk-action.zod.ts:406 already says the name "names an action the object declares"; owes a CONTRACT_REVIEW_TIER review before the queue
Metadata-protocol: narrowed (an undeclared bare bulkActions name no longer draws); the PR body owes Skills-docs:
Responsibility: n/a — UI defect/enhancement in ui/** owned by this lane; no platform path covers it; reached by every console/Studio user
Thread-read: 6651753071
Premise: holds (bulkActionMembers-8071.test.tsx:203 pins the keep-unresolved tolerance, origin/main ff3f83b); ruling 6651753071 (C)
Serial constraints cleared: none (no domain:ui in-flight claim names ui/packages/plugin-grid)
Activity
objectstack-fleet commentedon Nov 4, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: first-touch audit · p3 kept ·
domain:ui·pm:queue· 2026-11-04T19:53Z⛔ Not a claim.
ListView. It should read the same withheld-action set the other menus read.Generated by Claude Code
objectstack-fleet commentedon Nov 4, 2026
domain:ui#2 · os-justin · session_01QSeKYnE9YNkSZen7sijLA3
Claim: PM loop round 4
Session:
session_01QSeKYnE9YNkSZen7sijLA3Account:
os-justinBranch:
claude/issue-30169-bulk-bar-withheld-actionWorktree:
objectstack-issue-30169Domain:
domain:uiSeat:
domain:ui#2File surface:
ui/packages/plugin-grid/src/resolveBulkActions.tsandObjectGrid.tsx(the selection bar's bulk-action resolution only),ui/packages/plugin-list/src/ListView.tsxonly if the selection bar's action set is read there, their tests includingui/packages/plugin-grid/src/__tests__/bulkActionMembers-8071.test.tsx, one root.changeset/file; ⛔ no server or spec change (stop on breach; explain in the report)Container & model:
M,mode:subagent,model: TIER_DEFAULT — --tier: "no path-derived mandate"; a pinned row (8071 row 3) is flipped, which needs judgment,effort: highClause-②: no
Metadata-protocol: none
Responsibility:
ui/packages/plugin-grid resolveBulkActions keeps an unresolvable bare-string name and ObjectGrid draws it | #28877 already drops a withheld action from the header, More menu and row menu | any reader of a list whose app has a disabled package; hit today by HotCRM on 18.3.0Thread-read: 6640586422
Premise: holds (seat read on
origin/mainfec1f586:ObjectGrid.tsx:1260resolveBulkActionsreturnsunresolved; server predicateisActionWithheldByDisabledPackageinpackages/metadata-protocol/src/protocol-authoring-refusals.ts:73)Serial constraints cleared: #30077's PR #30162 (this seat, ACCEPTed, awaiting landing) edits
ListView.tsx's Hide Fields list, a different region; if this card touchesListView.tsx, it mergesorigin/mainafter #30162 lands. No other opendomain:uiclaim namesplugin-grid(read 2026-11-04T21:40Z).objectstack-fleet commentedon Nov 4, 2026
domain:ui#2 · os-justin · session_01QSeKYnE9YNkSZen7sijLA3
os-dev-report
{ "issue": 30169, "status": "needs_decision", "branch": "claude/issue-30169-bulk-bar-withheld-action", "pr": null, "session": "session_01QSeKYnE9YNkSZen7sijLA3", "premise_still_valid": true, "summary": "The defect holds on origin/main fec1f586. #28877 (commit d0df87310) fixed the header, the More menu and the row menu on the server only: it touches no ui/ file. The withheld action is gone from the served object's `actions`, and those menus have no bare-name fallback, so they draw nothing. The selection bar reads that same served `actions` list. A withheld name is therefore unresolved, and `ObjectGrid` draws unresolved `bulkActions` names by humanized name. That is row 3 of `bulkActionMembers-8071.test.tsx`. The reason row 3 keeps the name is real: a host can register a runner handler under that name. `objectBulkActionDispatch.test.tsx` pins that path ('keeps an unresolvable name alongside the promoted defs', through `ActionProvider handlers`). The PM mechanism assumption is half disproved. A withheld name is absent from the served `/meta/action` set, but so is a handler-only name. A runner handler is client code and never metadata, and the server makes a withheld name read like one that never existed (#28117 ruling A). So the served `/meta/action` set cannot tell a withheld name from a handler-backed one, and nothing in the client reads that set today. What can tell them apart is the runner's handler registry: `ActionRunner.handlers` is private and has no public query (only `launchRefusal`, which reads a handler's `refuseBeforeInput` and not the 'not executable' dead end). Reading it needs a change in `ui/packages/core`, which is outside the claimed file surface. Per the order I stopped there. No code was written.", "tests": "None run: no diff. Evidence is a code reading at fec1f586: `resolveBulkActions` (the 'Names that resolve to nothing stay in `unresolved`' docblock paragraph), `ObjectGrid` (`effectiveBulkActions` takes `unresolvedBulkActions`; the by-name dispatch `executeAction({ type: action, params: { records } })`), `ActionRunner.execute` (handler, then builtin, then `executeActionSchema`, which gives the toast HotCRM measured), and `ActionProvider` (it registers `handlers` synchronously in its runner `useMemo`). NOT MEASURED: the objectui#3002 issue body. Reason: GitHub API access to objectstack-ai/objectui is not enabled for this session (git-only read), and this checkout is shallow past the ui/ subtree import. The in-tree docblock and the dispatch pin carry the reason.", "gates": "n/a: no diff, so no derivation was run and nothing was pushed except the empty branch marker.", "dogfood_battery": "n/a: no PR", "mcp_calls": "1: mcp__claude-code-remote__add_repo (read access to objectstack-ai/objectui; it answered read_available and attached nothing). GitHub MCP calls: 0.", "api_writes": "1: POST /repos/objectstack-ai/objectstack/issues/30169/comments (this report, via scripts/pm/post-stamped.mjs). The git push of the empty branch is not a REST write.", "open_questions": [ { "question": "How does the selection bar tell a bare `bulkActions` name the server withholds from one a host serves through a registered runner handler? Both are absent from the served object's `actions` and from `/meta/action`.", "options": [ "A. Ask the runner. Add one public read-only method to `@object-ui/core` `ActionRunner` (for example `hasHandler(name)`, which answers `this.handlers.has(name)`). `ObjectGrid` then keeps an unresolved name only when `useAction().runner` has a handler under it. Withheld, mistyped and other dead names draw no button. The handler path (row 3's reason and the dispatch pin) keeps working. Row 3 flips to two assertions: with no handler, no `bulk-action-NAME` button; with a handler registered, the button is drawn by name. A withheld-trigger test is added (served object without the action: no button; with it: the action's label), plus an ablation of the drop. Cost: the file surface widens by `ui/packages/core/src/actions/ActionRunner.ts` and a core unit test, and a published package gains a public method (patch changeset for `@object-ui/core` and `@object-ui/plugin-grid`). Caveat: a handler a host registers in an effect after the grid's first render is read on the grid's next render, which comes when its rows arrive. Effort S.", "B. Read the served `/meta/action` set (the order's mechanism). `ObjectGrid` calls `useMetadata().ensureType('action')` and drops an unresolved name that set lacks while `getTypeStatus('action')` is 'ready'. This stays on the claimed surface. But it drops a handler-only name in every host that mounts a MetadataProvider, the Console included, which is the case row 3 protects. The existing pins would stay green only because their harness mounts no MetadataProvider: a false green. It also adds a `/meta/action` fetch that no other surface makes today. Effort S.", "C. Drop every unresolved name outright. On the surface and smallest, but it removes the handler-by-name capability. That is a product change: the dispatch pin 'keeps an unresolvable name alongside the promoted defs' flips with row 3. Effort XS." ], "recommendation": "A, because it is the only option that drops the withheld ghost and keeps the pinned handler path. It reuses the runner's own rule (a by-name dispatch with no handler ends in 'not executable'), so no second rule is written. Its one cost is widening the file surface into `@object-ui/core`, and the order told me to stop on that breach. The dispatch carried no decision axes, so this recommendation weighs only the stated cost and effort; paste the axes if you want an axis-by-axis analysis." } ], "security_findings": "none", "out_of_scope_findings": [ "carrier: this card. The row menu's legacy `rowActions` fold (`resolveLegacyRowActions`, whose unresolved names reach the kebab as `customRowActions`) keeps unresolved names on the same registerHandler reasoning, so a withheld bare `rowActions` name would draw the same ghost. This is a read-only inference and was not reproduced (HotCRM places those actions through `locations`). Whatever rule the decision picks for the bulk bar applies to this fold too. Dedupe words: rowActions unresolved, resolveLegacyRowActions withheld, customRowActions ghost." ] }objectstack-fleet commentedon Nov 4, 2026
domain:ui#2 · os-justin · session_01QSeKYnE9YNkSZen7sijLA3
维护者速读 · #30169 选择栏怎么认出「被禁用包扣下的操作」 · 2026-11-04T21:58Z
一句话问题。 一个模块被关掉后,在列表里勾选记录,选择栏仍会出现该模块的批量按钮,点了只弹「不可执行」。要去掉这个按钮,选择栏得分清两种名字:一种是被服务端扣下的操作,另一种是宿主程序自己在前端接管的操作。今天这两种在前端看起来一模一样,所以要定下按什么来区分,或者干脆不再支持后一种。
Governing text:
packages/spec/src/ui/bulk-action.zod.ts:406— "bulkActions: ['<action_name>']— names an action the object declares." The renderer's tolerance of a name the object does not declare is inui/packages/plugin-grid/src/resolveBulkActions.ts:78-82("Names that resolve to nothing stay inunresolved"), pinned bybulkActionMembers-8071.test.tsx:202-212(row 3) andobjectBulkActionDispatch.test.tsx:213-228. Authoring already refuses that name:packages/lint/src/validate-action-name-refs.ts(action-name-undefined, severity error, ADR-0049) rejects an undeclaredbulkActions[]/rowActions[]name and says "the runtime ships NO built-in action names … a name resolving nowhere is dead, full stop" (:41-50), citing therecord:alert/page:headerprecedent of drawing nothing for an unresolved id (#20105). The resolver's "that path must keep working" comment contradicts lint, and lint carries the ADR. Server rule:isActionWithheldByDisabledPackage(packages/metadata-protocol/src/protocol-authoring-refusals.ts:73, #28877); "disabled has exactly one meaning: hidden, refused on the data plane, data kept" (docs/adr/0070-package-first-authoring.md:66; "ruling A: disabled = hidden" atpackages/objectql/src/registry.ts:7796).Protocol: no
packages/specchange in any option. C brings the renderer back to the spec's own sentence. A widens a publisheduipackage's API (@object-ui/core).前提(re-check,席位与隔离复核亲读)
ObjectGrid.tsx:1260(unresolved: unresolvedBulkActions) and:1287; it dispatches by name at:2020.bulkActionsstring inexamples/,ui/andpackages/names a declared action (for exampleexamples/app-showcase/src/ui/actions/index.ts:57,179). The only exception is the demoui/packages/plugin-grid/demo/bulk-actions.tsx:241(legacy_only_handler). The Console'screate/delete/navigate/refreshhandlers (useObjectActions.ts:129-159) serve other surfaces;deleteis routed away from the runner on purpose (ObjectGrid.tsx:1250-1258). HotCRM is not in this repo; that its bare strings name declared actions rests on A list's selection bar still draws a bulk action that a disabled package withholds, as a ghost button that toasts "not executable" #30169's own text ("With marketing on, the same bar draws Add to Campaign, which works").ActionRunner's handler map is private (ui/packages/core/src/actions/ActionRunner.ts:1365), with no public query;useAction().runneris public (ui/packages/react/src/context/ActionContext.tsx:65).resolveLegacyRowActions.ts:35-37→ObjectGrid.tsx:1002customRowActions).选项
ActionRunner加一个只读方法(如hasHandler(name));选择栏只留「有前端接管」的未声明名字@object-ui/core多一个公开方法/meta/action业务含义直译。 A 是问前台「这个分机有人接吗」;B 是只看电话簿,连带有人接的也摘牌;C 是规定只能挂电话簿上的分机,挂错的,装修时就提醒。
推荐:C
只看① 选 C;②③④ 是否翻转:否。
项目长远合理性(领起)。 终态句:两年后,选择栏只出现对象声明过、当前用户真能执行的操作。主流平台都是这样:Salesforce 的 list view mass actions 只列对象上定义的操作,ServiceNow 的 list UI actions 只列表上定义的操作。spec 已经写明裸字符串「指对象声明的操作」,lint 也已按 ADR-0049 在编写阶段把未声明的名字判为 error(「解析不到就是死的」),
record:alert、page:header早就对解析不到的 id 什么都不画。渲染器容忍未声明的名字,是在 spec 之外多出的一条宽容;按「spec 声明优先于实现」,应当把实现拉回 spec,而不是再加一个公开方法去养这条宽容(A)。B 用一份不完整的清单代替判据,会误删。实际业务需求。 幽灵按钮今天就会撞上,HotCRM 在 18.3.0 上实测到。另一边,只靠前端接管的批量名字,仓内没有一个应用在用,只有两个测试和一个 demo,所以 C 取消的是一项有测试、无在用方的能力。
防 AI 写错。 出错时谁看到什么:今天 AI 在
bulkActions里写一个未声明或拼错的名字,最终用户会看到一个点了就报错的按钮。第一道网已经在:lint 的action-name-undefined在发布前以 error 拒收。C 让运行时与之一致,按钮不画;开发态警告只是 lint 之后的第二道网。这是契约收紧,优于消费端宽容。A 保留宽容,拼错的名字反而静默消失。B 会假绿。创业阶段不扩散。 C 删掉一条零在用方的宽容,不加键、不加方法。A 为零在用方的能力新增一个公开方法,等于扩张。
疑属不升级类。 spec 原文与 lint(ADR-0049)都已定下「未声明即死」,渲染器的宽容与之冲突;按「spec 与代码不一致默认改代码对齐」,C 即既有规则。按规则 dev 的
needs_decision一律呈你,所以仍交你裁。置信缺口。 仓外宿主是否依赖前端接管批量名字,未知;如有具名的外部用户证据,应改为 A。行菜单的同类宽容未在浏览器复现,按同规则一并处理。
裁后执行。 选 C:同卡重派一个 dev,范围是
plugin-grid的resolveBulkActions与resolveLegacyRowActions、两个 pin、demo,以及ui/packages/plugin-grid/README.md:552、:771里的bulkActions: ['delete', 'export'](export只靠前端接管),加开发态警告,再补被禁用模块的触发用例与消融。选 A:同卡重派,认领范围加ActionRunner.ts,入队前按扩大公开面过一次契约复核。选 B:同卡重派。os-decision-facets
bulk-action.zod.ts:406)与 lintaction-name-undefined(ADR-0049),去掉一条特例;A 为特例加公开方法;B 误删。Analysis-tier: seat (TIER_DEFAULT); reviewed by an isolated
CONTRACT_REVIEW_TIERreview, which returned FAIL with 6 required corrections. All 6 are applied in this text, and the recommendation moved from A to C on the corrected facet ② and the spec sentence it surfaced. A second isolated review of the C draft returned PASS with 5 corrections (the lint rule, the README scope, ADR paths, the HotCRM source, test spans), all applied.Prior rulings read:
resolveBulkActions,unresolved bulk,#28877,#28117→ 1 hit (#28117 ruling record, re-affirmed by letter A); ADR none; thread: none.objectstack-fleet commentedon Nov 4, 2026
domain:ui#2 · os-justin · session_01QSeKYnE9YNkSZen7sijLA3
Release: claim 6642827748 (
session_01QSeKYnE9YNkSZen7sijLA3, seatdomain:ui#2) is released into the decision inbox, 2026-11-04T21:59Z. No code was written. Branchclaude/issue-30169-bulk-bar-withheld-actionis an empty marker atfec1f586and holds no commits. The dev report is 6642981552, and the seat analysis is 6643188402. After the ruling, anydomain:uiseat re-claims the card and dispatches the letter chosen. The analysis names the scope for each letter.objectstack-fleet commentedon Nov 5, 2026
domain:ui#2 · os-justin · session_01QSeKYnE9YNkSZen7sijLA3
Ruling recorded · #30169 · letter C · 2026-11-05T06:51Z
The maintainer (os-justin), verbatim, in seat
domain:ui#2's session: 「同意」. This answers the seat's business explanation of its two pending recommendations, which named this card as C (analysis 6643188402, options A/B/C).Ruling: option C. The selection bar draws only actions the object declares. An undeclared bare
bulkActionsname draws nothing, and in development the console logs one warning naming it. The same rule applies to the legacyrowActionsfold. This followspackages/spec/src/ui/bulk-action.zod.ts:406("names an action the object declares") and lintaction-name-undefined(ADR-0049). The renderer's tolerance of handler-only names is retired.Execution (from the analysis):
ui/packages/plugin-grid/src/resolveBulkActions.tsandresolveLegacyRowActions.ts: drop unresolved names and add the dev-mode warning.bulkActionMembers-8071.test.tsxrow 3 andobjectBulkActionDispatch.test.tsx("keeps an unresolvable name…") to substantive assertions of the new behaviour, with the trigger named in the test.ui/packages/plugin-grid/demo/bulk-actions.tsx(legacy_only_handler) andui/packages/plugin-grid/README.md(bulkActions: ['delete', 'export'];exportis handler-only).deletekeeps its own path (ObjectGrid.tsxkeptDeleteAction/onBulkDelete) and is untouched.State:
needs-user-decision→pm:queuein this stroke. The card is unassigned; anydomain:uiseat may claim it. The branchclaude/issue-30169-bulk-bar-withheld-actionis an empty marker atfec1f586, with noResume-from.objectstack-fleet commentedon Nov 5, 2026
domain:ui#2 · os-bill · session_01Utp6Cb5R6JDdfyvtj3dPR2
Claim: PM loop round 1 · 2026-11-05T07:23Z
Session:
session_01Utp6Cb5R6JDdfyvtj3dPR2(seat); dev = card threadsession_01Utp6CZ1rxZPzain2p5bv5xAccount:
os-billBranch:
claude/project-thread-8y2k2m(harness-pinned card thread branch; theclaude/issue-N-slugconvention does not apply to a project card thread)Worktree:
objectstack-issue-30169Domain:
domain:uiSeat:
domain:ui#2File surface:
ui/packages/plugin-grid/src/**(resolveBulkActions, resolveLegacyRowActions, ObjectGrid only if needed),ui/packages/plugin-grid/demo/**,ui/packages/plugin-grid/README.md, their tests and the consumer tests the flip breaks, one root.changeset/(stop on breach; explain in the report)Container & model:
M,mode:thread,model: default tier (opus) — dispatch-gates --tier: no path-derived mandate,effort: highClause-②: no (narrowing) — the renderer stops drawing an undeclared bulk action name;
packages/spec/src/ui/bulk-action.zod.ts:406already says the name "names an action the object declares"; owes a CONTRACT_REVIEW_TIER review before the queueMetadata-protocol: narrowed (an undeclared bare bulkActions name no longer draws); the PR body owes Skills-docs:
Responsibility:
n/a — UI defect/enhancement in ui/** owned by this lane; no platform path covers it; reached by every console/Studio userThread-read: 6651753071
Premise: holds (bulkActionMembers-8071.test.tsx:203 pins the keep-unresolved tolerance, origin/main ff3f83b); ruling 6651753071 (C)
Serial constraints cleared: none (no domain:ui in-flight claim names ui/packages/plugin-grid)
Dispatch order:
/mnt/project-files/ui-seat2/dispatch-30169.md(project files).