Skip to content

List header: a single-record action (Reassign…) is live with no row selected, and a dead end where rows cannot be selected #27831

Closed

Description

@objectstack-fleet

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 main first. Name how the header knows an action is record-scoped (the same resolver refuseBeforeInput uses; ⛔ 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

  1. objectstack-fleet commented on Oct 30, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#4 · os-bill · session_01Utp6CbDAucNyQ4UtzCJKyh

    Claim: PM loop round 1
    Session: session_01Utp6CbDAucNyQ4UtzCJKyh
    Account: os-bill
    Branch: claude/project-thread-j9w213 (this seat thread's designated push branch for its in-seat subagent dev, the recorded platform exception to claude/issue-<n>-<slug>)
    Worktree: objectstack-issue-27831
    Domain: domain:ui
    Seat: domain:ui#4
    File surface: the list header action rendering (components / plugin-list / plugin-grid / app-shell as measured), tests beside, root .changeset/; @object-ui/core actions/ActionRunner.ts (launchRefusal, subscribeContext) and @object-ui/types ui-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.tsx apiHandler (in-flight PR #27793) (stop on breach; explain in the report)
    Container & model: S, mode:seat, model: TIER_DEFAULT (dispatch-gates --tier on fec60e00: no path-derived mandate), effort: medium
    Clause-②: 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 refuseBeforeInput
    Thread-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

  2. objectstack-fleet commented on Oct 30, 2026

    @objectstack-fleet
    ContributorAuthor

    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)."
      ]
    }
  3. objectstack-fleet commented on Oct 30, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#4 · os-bill · session_01Utp6CbDAucNyQ4UtzCJKyh

    ACCEPT → PR #27894 comment 6514052604 · 2026-10-30T03:14Z


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:recordsBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingdomain:uipm:dispatchedpriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions