Repository navigation
Description
domain:ui#4 · os-bill · session_01Utp6CbDAucNyQ4UtzCJKyh
Filing gate: ③ triage-directed split. Triage on #27520 (6495077487) said: 「Each row is either taken, with its own card where it needs code」. This card is row 5.
Business reason: on a list, a record-scoped action in the header (Tasks → Reassign…) looks ready with nothing selected and explains itself only after a click. For a member whose grid has no row checkboxes, the button can never work.
Reader who acts: domain:ui (the console's list header actions).
Duplicate search: search_issues "list header single-record action enabled with nothing selected select a row first disabled tooltip" → #27604, #25883 and #23032, none of them this. New. The early refusal ("This action runs on a single record — select a row first.", refuseBeforeInput, objectui#13384) is prior work this card builds on, not a duplicate: it stops the dispatch, but the button still invites the click.
What happens (row 5 of #27520, 18.3.0 RC walkthrough at c4c515ad)
Admin and member, desktop, Tasks. Reassign… in the list header is enabled with nothing selected. Only after a click does it say "This action runs on a single record — select a row first." The member's grid has no row checkboxes, so for the member it is a dead end.
Expected (the row's own suggestion)
- With no row selected, or more than one, a record-scoped header action is drawn disabled. Its tooltip is the same reason the early refusal already gives, in the session's language, from the existing keys.
- With exactly one row selected it is enabled, and it behaves as today.
- Where the grid offers no row selection at all, a record-scoped header action is not drawn.
- The early refusal stays as the backstop for any path that still dispatches.
Acceptance
- One measured repro on current
mainfirst. Name how the header knows an action is record-scoped (the same resolverrefuseBeforeInputuses; ⛔ no second rule) and where the selection count lives. - Tests:
- 0 rows: disabled, with the reason as the tooltip.
- 1 row: enabled, and it dispatches.
- 2 rows: disabled, with the "exactly one row" reason.
- No-selection grid: not drawn.
- Control: a non-record-scoped header action (e.g. New) is unaffected.
- An ablation.
- No new i18n key expected. Reuse the refusal keys; if a new one is needed, it is
Clause-②: yes (widening).
Links back: #27520 (row 5)
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 30, 2026
domain:ui#4 · os-bill · session_01Utp6CbDAucNyQ4UtzCJKyh
Claim: PM loop round 1
Session:
session_01Utp6CbDAucNyQ4UtzCJKyhAccount:
os-billBranch:
claude/project-thread-j9w213(this seat thread's designated push branch for its in-seat subagent dev, the recorded platform exception toclaude/issue-<n>-<slug>)Worktree:
objectstack-issue-27831Domain:
domain:uiSeat:
domain:ui#4File surface: the list header action rendering (components / plugin-list / plugin-grid / app-shell as measured), tests beside, root
.changeset/;@object-ui/coreactions/ActionRunner.ts(launchRefusal,subscribeContext) and@object-ui/typesui-action.ts(ActionContext.selectionAvailable) (amended 2026-10-30T02:56Z: the header asks the runner's own early refusal instead of restating its rule, which needs two public runner members and one optional context field); ⛔useConsoleActionRuntime.tsx/RecordDetailView.tsxapiHandler(in-flight PR #27793) (stop on breach; explain in the report)Container & model:
S,mode:seat,model: TIER_DEFAULT(dispatch-gates --tieronfec60e00: no path-derived mandate),effort: mediumClause-②: yes (widening) (amended 2026-10-30T02:56Z: the three additive members above)
Metadata-protocol: none
Responsibility: a record-scoped list header action is disabled with the early-refusal reason as tooltip unless exactly one row is selected, and not drawn where rows cannot be selected; same resolver as
refuseBeforeInputThread-read: none
Premise: holds
Serial constraints cleared: in-flight: PR #27793 (api handlers, excluded)
Dispatched 2026-10-30T01:07Z to the seat's in-seat subagent on the seat thread's branch. ⛔ The subagent posts no claim and writes no assignee.
Generated by Claude Code
objectstack-fleet commentedon Oct 30, 2026
domain:ui#4 · os-bill · session_01Utp6CbDAucNyQ4UtzCJKyh
os-dev-report
{ "issue": 27831, "status": "done", "branch": "claude/project-thread-j9w213", "pr": "https://github.com/objectstack-ai/objectstack/pull/27894", "pr_number": 27894, "head_sha": "d78f7954564325823c5853f19326730a6eafb0e2", "session": "session_01Utp6CbDAucNyQ4UtzCJKyh", "premise_still_valid": true, "summary": "Measured on main fec60e00: a record-scoped list_toolbar action (the showcase Reassign shape) is enabled with 0 and 2 rows and drawn on a no-selection grid (new suite: 4 failed | 3 passed on base). The ObjectView header now draws every schema-driven action through ListHeaderActionBar, which asks the runner's new ActionRunner.launchRefusal (the same refuseBeforeInput execute asks, i.e. resolveServerActionRecordId, translated by errorKey) and sets disabled + disabledReason on a refused action, or leaves it out when the grid publishes selectionAvailable false. ObjectGrid publishes selectionAvailable beside selectedRecords (its selection-mode block moved, unchanged, above the early returns); ActionRunner.subscribeContext lets the header redraw. The early refusal stays as the backstop. No new i18n key.", "files_changed": [ ".changeset/27831-list-header-selection-gate.md", "ui/packages/app-shell/src/views/ObjectView.headerSelectionGate-27831.test.tsx", "ui/packages/app-shell/src/views/ObjectView.toolbarScriptSelection-12331.test.tsx", "ui/packages/app-shell/src/views/objectViewHeaderChrome.tsx", "ui/packages/app-shell/src/views/objectViewHeaderLaunchGate.tsx", "ui/packages/app-shell/src/views/objectViewPageHeader.tsx", "ui/packages/core/src/actions/ActionRunner.ts", "ui/packages/core/src/actions/__tests__/ActionRunner.launchRefusal-27831.test.ts", "ui/packages/plugin-grid/src/ObjectGrid.tsx", "ui/packages/types/src/ui-action.ts" ], "line_budget": "10 files, +699 / -95 vs merge-base (ObjectGrid.tsx +98/-86 is the moved selection-mode block plus a 13-line effect); under the 3000-line class (c) bound", "tests": "Repro on base fec60e00 (five non-test sources restored from base, restored from HEAD after, git diff HEAD empty): ObjectView.headerSelectionGate-27831 = 4 failed | 3 passed (0 rows / 2 rows / zh not disabled; no-selection list still draws it). On HEAD: 7/7. Ablation via scripts/ablation-replace.mjs (launchRefusal call replaced by undefined; anchor 1->0, blob 3c7bbfc1872a->51a18c64a2c5): 4 failed | 3 passed, restored blob == HEAD, git diff HEAD empty; tests resolve @object-ui/* to src via ui/vitest.config.mts aliases, no dist leg. Pre-merge: vitest core/src/actions + plugin-grid 307 files / 2967 passed; 127 targeted app-shell/components/react/plugin-list files 1440 passed; type-check types/core/plugin-grid/app-shell exit 0. Final head d78f7954: 43 targeted files / 665 passed (27831, 12331, 12462, launchRefusal-27831, recordRowGuard-13384, flowRowGuard-13397, all action:* renderer suites); app-shell type-check exit 0; ui vitest scripts/ 169 passed | 2 skipped.", "gates": "Local, final head d78f7954: dispatch-gates --commands derived 35 families, each run with exit captured before any pipe, all exit 0 (incl. node scripts/check-sdui-manifest.mjs and --self-test, check-changeset-no-major --base origin/main, check-adr-0087-registration, check:nul-bytes, check:type-check-debt, check:query-options-erasure, pnpm --dir ui exec vitest run scripts/); --ran reconciliation: 35 derived, 35 run, 0 NOT-MEASURED, 0 UNRUN. Extra: pnpm --dir ui check:new-line-citations exit 0 (0 new). CI on PR at report time: filter success, Check PR Size success, Governed Surface Queue Guard success, PR-body guards success; Lint/Type Check/Test Core/Build UI Deps/Live E2E UI/Check Changeset in_progress.", "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm", "mcp_calls": "0", "api_writes": "2 — POST /repos/objectstack-ai/objectstack/dispatches (fleet-write relay pr_create, PR #27894, read-back identical 11466/11466 bytes, assignees none); POST /repos/objectstack-ai/objectstack/issues/27831/comments (this report, via post-stamped). git push not counted.", "deviations": [ "Clause-②: yes (widening) where the claim says no: two new public ActionRunner members (launchRefusal, subscribeContext) and ActionContext.selectionAvailable are needed for the header to ask the runner's private refusal and redraw on selection; the dispatch allows this arm. Changeset level patch, Metadata-protocol none.", "Report comment: line 1 is the dispatch's signature line and os-dev-report is line 2 (os-dev asks for os-dev-report as the first line; the dispatch order names the signature first).", "PR body line 2 is the dispatch-mandated claude-projects-attribution HTML comment (os-dev bans dev-authored HTML comments except the harness attribution line).", "Commit trailers are the AGENTS.md model-free pair (Claude-Session + Co-authored-by: Claude), not the harness reminder's model-named Co-Authored-By line.", "Local test scope narrowed and declared: app-shell has 1922 test files; ran the 127 that name list_toolbar / header chrome / selectedRecords / selectionAvailable / ObjectView plus action renderers, and core actions + plugin-grid in full; the full Test UI shards are CI's.", "First local gate pass on the pre-merge head used a 280s per-command timeout: check:query-options-erasure and check:type-check-debt recorded exit 124 there, re-run without the timeout exit 0; the final-head pass has all 35 at exit 0." ], "open_questions": [], "out_of_scope_findings": [ "carrier: none · noted, not filed — phone card view: ObjectGrid's card list draws no checkboxes while its selection mode stays on, so a record-scoped header action is drawn disabled with its reason instead of left out (PR Acceptance notes).", "carrier: none · noted, not filed — EnvironmentListToolbar (sys_environment) draws its own bar from headerToolbarActions and is not routed through the gate (PR Acceptance notes).", "carrier: none · noted, not filed — a ?runAction= deep link naming a record-scoped action on a no-selection list is consumed by useOfferedNavRunAction (reads the ungated list) while the button is not drawn; read from code, not measured (PR Acceptance notes)." ] }objectstack-fleet commentedon Oct 30, 2026
domain:ui#4 · os-bill · session_01Utp6CbDAucNyQ4UtzCJKyh
ACCEPT → PR #27894 comment 6514052604 · 2026-10-30T03:14Z
Generated by Claude Code