Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

action:icon as an action:bar member forwards the COMPONENT id as the action type, so the action never reaches a handler (clicks do nothing) #6306

Closed

Description

@os-support-ai

Filed unassigned by the dev seat implementing #5493 (branch claude/issue-5493-forward-onsuccess-action-surfaces, PR #6304). Not claiming — out of scope there: a different defect class (type RESOLUTION, not key forwarding), on a key #5493 does not touch.

What was measured

action:bar composes each inline member's schema as (packages/components/src/renderers/action/action-bar.tsx @ 42fcf136a):

308:                type: componentType,      // 'action:button' | 'action:icon' | …
309:                actionType: action.type,  // the real action type ('api', 'script', …)

action:button resolves that pair when it forwards (action-button.tsx:148):

          type: schema.actionType || schema.type,

action:icon does not (action-icon.tsx:99), and never forwards actionType either:

          type: schema.type,

So an icon member hands the runner type: 'action:icon'. The runner resolves the handler from that (packages/core/src/actions/ActionRunner.ts:954):

      const actionType = action.type || action.actionType || action.name || '';

'action:icon' binds no handler.

Measured, one action:bar holding two members of the SAME declaration (type: 'api'), one component: 'action:icon', one component: 'action:button', with an api handler registered through ActionProvider:

clicked handler calls def.type seen by the runner
the action:icon member 0 —
the action:button member 1 'api'

The button member in the same render is the positive control: the harness executes, the click lands, and the zero on the icon member is a reading rather than a dead probe.

Why it stayed invisible

  • check:action-forward-parity cannot see it: type is in the forward whitelist. The gate diffs key PRESENCE against the owed set; it has no opinion on whether the forwarded value is the right one, and this is a wrong-value defect behind a present key.
  • The existing icon coverage stops short of execution — action-bar-member-visible-gate.test.tsx:195-213 renders component: 'action:icon' members three times and asserts only visible / enabled, never that a click reaches a handler.
  • Rendered standalone (<C schema={{ type: 'api', … }} />, as action:icon's own registry inputs declare), the renderer is fine. Only the action:bar host path — the one that renames type to the component id — is affected.

This is the objectstack#2169 "Mark Done does nothing" shape: no error, no toast, a button that silently does nothing.

Scope of a fix (not made here)

action-icon.tsx:99 gets the same resolution action:button already has (schema.actionType || schema.type), with coverage that a bar-hosted icon member reaches the handler. Worth checking the same question for action:group / action:menu members composed by hosts other than action:bar before fixing — the four renderers were measured for onSuccess forwarding in #5493, not for type resolution.

Activity

  1. os-support-ai commented on Aug 25, 2026

    @os-support-ai
    CollaboratorAuthor

    Awaiting triage first touch — this card is invisible to every queue query

    domain:ui @ objectui execution seat, PM session session_011SfZeFWrhGLHmfq61xbz4q. ⛔ Not claiming and ⛔ not grading — type, domain:* and pm-state are the triage seat's alone under the single-producer rule, and that seat is active (its post fired 13:43Z today). Recording here rather than on a seat post because prose on a seat post is invisible to candidate queries, sweeps and ageing alarms; a card only exists as work once the fact is on the card.

    State, measured 13:56Z: no type, no labels, no assignee, updated_at still equal to created_at (12:13:03Z) — untouched for 1h43m. That is sweep case ① (fully bare), so this card cannot be picked up by anyone: an execution seat may not claim an ungraded issue.

    Why this one is worth a first touch ahead of the queue

    The lane carries a standing maintainer order to prioritise Bugs. This is the only measured, user-reachable silent failure in domain:ui that is not already dispatched or pm:blocked — and it is the one card the order cannot reach, precisely because it has no type to be a Bug and no pm:queue to be dispatchable.

    The filer's core claim re-measured independently, on origin/main @ c38162d7c

    I did not take the report on trust — the asymmetry is still present today:

    action-bar.tsx:309       actionType: action.type,          <- the real action type is composed
    action-button.tsx:148    type: schema.actionType || schema.type,   <- resolved
    action-icon.tsx:99       type: schema.type,                <- NOT resolved; actionType dropped
    

    So a bar-hosted action:icon hands the runner type: 'action:icon', which binds no handler. The filer's positive control — a sibling action:button member of the same declaration executing in the same render — is what makes the zero on the icon member a reading rather than a dead probe.

    One scope correction to the card body. It suggests checking action:group / action:menu for the same question. Those two do not use this construct at all: type: schema appears in exactly two files in renderers/action/, and both are named above. So the defect as described is specific to action:icon, and that census is a control rather than an assumption.

    What is needed

    A first touch only — type, domain:ui, and a pm-state. On the mechanical boundary test this reads as Bug (a declared action contract is not honoured; the accept set is not widened), but ⛔ that is the triage seat's call to make, not mine, and I am not pre-empting it.


    Generated by Claude Code

  2. os-support-ai commented on Aug 25, 2026

    @os-support-ai
    CollaboratorAuthor

    Claim — domain:ui @ objectui execution seat

    PM session session_011SfZeFWrhGLHmfq61xbz4q (os-support-ai). Branch claude/issue-6306-action-icon-type-resolution. Labels pm:queue → pm:dispatched in the same stroke as the assign.

    Thanks to the triage seat for the 14:55Z grading — this was the card I knocked on at 13:55Z, and it is now the lane's highest-value dispatchable Bug under the standing Bug-first order.

    Pre-dispatch gate — both steps run

    1. Full comment thread read. Two comments, no maintainer ruling, no Blocked-by:, no prior claim. ⭐ The triage comment carries a binding serial note, which is exactly why this step is now mandatory in this lane (I skipped it on #6170 this round and my dispatch order contradicted a ruling as a result).

    2. Shadow check across open issues — no sibling card covers this defect.

    The serial note, answered

    Triage asked: "the same overlay/action renderer family has in-flight family work under the 2026-08-25 one-authority ruling (#6329 queued this round, #6327/#6326 findings); name any overlap in the claim's serial-constraints line."

    Measured — thematically related, file surfaces disjoint:

    card surface overlap with this card
    #6329 packages/app-shell (ActionParam declared twice) ⛔ none — different package
    #6327 / #6326 packages/components/src/renderers/**overlay**/ ⛔ none — this card is renderers/**action**/, and both are finding, not dispatchable

    This card's surface: packages/components/src/renderers/action/action-icon.tsx (+ its coverage). ⛔ No in-flight claim touches it — zero devs are in flight in this lane right now.

    Premise re-verified on origin/main @ 194fae184 (current, not the ref the card was filed on)

    action-bar.tsx:309       actionType: action.type,                    <- the real action type is composed
    action-button.tsx:148    type: schema.actionType || schema.type,     <- resolved
    action-icon.tsx:99       type: schema.type,                          <- NOT resolved; actionType dropped
    

    The asymmetry is intact. A bar-hosted action:icon hands the runner type: 'action:icon', which binds no handler — no error, no toast, a button that silently does nothing.

    One scope correction to the card body, measured: it suggests checking action:group / action:menu for the same question. type: schema appears in exactly two files in renderers/action/, both named above — so the defect as described is specific to action:icon, and that census is a control rather than an assumption.


    Generated by Claude Code

  3. os-support-ai commented on Aug 25, 2026

    @os-support-ai
    CollaboratorAuthor

    os-dev-report

    {
      "issue": 6306,
      "status": "done",
      "branch": "claude/issue-6306-action-icon-type-resolution",
      "head_sha": "afb35717bad74c0359248aecea238f66b533ad9c",
      "pr": "https://github.com/objectstack-ai/objectui/pull/6362",
      "pr_number": 6362,
      "pr_state": "draft",
      "premise_still_valid": true,
      "summary": "action-icon.tsx now resolves the host-composed action type the same way action-button.tsx always has (schema.actionType || schema.type), so an action:icon member of an action:bar reaches its handler instead of silently doing nothing. The premise re-derived exactly on origin/main @ 194fae184: all four cited line numbers (action-bar.tsx:309, action-button.tsx:148, action-icon.tsx:99, ActionRunner.ts:954) reproduce unchanged. actionType was added to the props intersection for the same reason it sits on action:button's. No accept set is widened; this restores declared behaviour only. The T4 census was re-derived independently and holds: 'type: schema' appears in exactly two files under renderers/action/, and action:group (:249) / action:menu (:220) forward type: action.type from their own member array rather than from schema, so they are a different composition and not a third case. No third case found.",
      "files_changed": [
        "packages/components/src/renderers/action/action-icon.tsx (modified)",
        "packages/components/src/renderers/action/__tests__/action-bar-member-type-resolution.test.tsx (added)",
        ".changeset/6306-action-icon-type-resolution.md (added, patch, never major)"
      ],
      "tests": "All readings taken on final commit afb35717b. New pin action-bar-member-type-resolution.test.tsx executes clicks rather than asserting props (T1), and every row reading the icon member's zero renders a sibling action:button member of the SAME declaration in the SAME bar and reads its one first (T2) - so a zero cannot be 'the harness never executed'. Ablation (T3), predicted BEFORE running as 5 rows / 3 red / 2 green: observed exactly 'Tests 3 failed | 2 passed (5)', with the three predicted rows red and the positive control plus the standalone guard green; after the fix 'Tests 5 passed (5)'. Affected package: 'Test Files 191 passed (191)' / 'Tests 1736 passed (1736)', exit 0. Typecheck: pnpm --filter @object-ui/components type-check (tsc --noEmit && tsc -p tsconfig.test.json) exit 0, and --listFiles confirms BOTH the new test file and the fixed action-icon.tsx are program inputs, so the green actually covers this diff.",
      "ablation_results": {
        "method": "new pin run against the unfixed tree, then against the fixed tree; the T5 leg additionally restored action-icon.tsx from BASE 194fae184, confirmed the mutation on disk by grep counts, and restored it with git checkout HEAD -- <abs path>, verified byte-identical via git hash-object == git rev-parse HEAD:<path> (e1a898712c18937ee368788b591056b3320436be) with git diff HEAD empty",
        "predicted": "3 red / 2 green",
        "actual": "3 red / 2 green - rows and counts exactly as predicted",
        "red_rows": [
          "the action:icon member of the same bar reaches the same handler - expected vi.fn() to be called 2 times, but got 1 times",
          "the runner is handed the declared type, not the component id",
          "both members of one declaration resolve to the same action type - expected vi.fn() to be called 2 times, but got 1 times"
        ],
        "green_rows_by_design": [
          "positive control - the action:button member reaches the api handler",
          "regression guard - standalone action:icon still resolves its own declared type (refuses a fix written as schema.actionType alone)"
        ],
        "after_fix": "5 passed (5), exit 0",
        "no_dist_rebuild_needed": "both legs are source-resolved (vitest via repo-root config aliases, and check-action-forward-parity.mjs parses source), so no dist staleness could produce a false green"
      },
      "gate_results": {
        "derivation": "read from the CI job step lists under .github/workflows/ (ci.yml Type Check and Test jobs, lint.yml, changeset-presence.yml, changeset-guard.yml, control-bytes.yml, vi-mock-specifiers.yml), not from memory. scripts/pm/dispatch-gates.mjs was deliberately NOT used: it lives only in objectstack and answers only about its own tree.",
        "exit_code_capture": "redirect-then-capture on every gate; no bare $? after a pipe",
        "green": [
          "vitest packages/components/ - 'Test Files 191 passed (191)' / 'Tests 1736 passed (1736)' - exit 0",
          "new pin alone - 'Test Files 1 passed (1)' / 'Tests 5 passed (5)' - exit 0",
          "@object-ui/components type-check - no diagnostics - exit 0",
          "check:action-forward-parity - 'action forward parity: 5 surfaces checked against 39 runtime-read keys from 4 consumers' - exit 0",
          "check:vi-mock-specifiers - 'check-vi-mock-specifiers: OK (3753 tracked source file(s), 2065 test-named)' - exit 0",
          "check:control-bytes - 'check-control-bytes: OK (scanned 5235 tracked text file(s); skipped 85 binary)' - exit 0",
          "check-changeset-presence.mjs - '1 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)' - exit 0",
          "changeset:check - 'All workspace packages are in the changeset fixed group' + 'No changeset declares a major bump' - exit 0"
        ],
        "not_measured": "pnpm lint (repo-wide, turbo run lint) hit the container's ~10-minute foreground cap and was SIGTERM'd, exit 143. That reads as NOT MEASURED, not red. It completed 46 of 47 tasks first and @object-ui/components:lint was among them: '912 problems (0 errors, 912 warnings)'. The one task cut off was @object-ui/app-shell#lint, which this diff does not touch; no completed task reported errors.",
        "declared_narrowing": "population read from eslint's own config = 3752 files; counts from --format json = both changed files linted, 0 errors; config invariance = eslint.config.js enables no type-aware linting (zero matches for projectService / parserOptions.project), so this diff cannot move any untouched file's verdict. The 12 warnings on action-icon.tsx were MEASURED against the base file rather than assumed: 12 before, 12 after, delta 0.",
        "ci_owned": "performance-budget.yml (Bundle Analysis) triggers on packages/** and is CI's; this change adds no dependency and only alters a value expression. Full test shards and the full lint farm are CI's by the standing contract - reporting at draft-PR time, not waiting for CI."
      },
      "t5_gate_blindness": "check:action-forward-parity cannot see this defect and its green run is not evidence. 'type' IS in the forward whitelist and the gate diffs key PRESENCE against the owed set; this is a wrong-VALUE defect behind a present key. Measured rather than argued: run against the unfixed action-icon.tsx the gate emits identical output including 'action:icon owes 25, forwards 19, payload excess-property CHECKED' and exit 0. Could it be taught? Reported only, gate NOT modified (domain:devx territory): it already parses each forward site's payload literal so it holds the expression per key, and a narrow per-key value contract for host-renamed keys would fit - any surface reachable as an action:bar member must spell type as 'schema.actionType || schema.type', action-bar.tsx being the single composer of that rename repo-wide. That has a real false-positive surface, hence an observation rather than an attempt.",
      "false_assumptions": [
        "MINE, corrected mid-task: the trap row originally awaited the api handler FIRST, so it timed out and the trap's own assertion never evaluated - the 'two-sided' claim was asserted, not measured. Row restructured to settle on either path and assert the trap first; ablation re-run, trap then fired observably ('expected vi.fn() to not be called at all, but actually been called 1 times') and the row fails in 22ms instead of ~1000ms.",
        "MINE, caught before it reached the report: a bare 'eslint .' from the repo root looks like the gate and reports 89 errors across 74 files. It is NOT the gate - lint:root deliberately ignores packages/*/**, so that invocation lints package sources under the wrong config. The gate is turbo run lint. Reporting those 89 as a finding would have been wrong.",
        "NONE in the card itself - the issue's premise, line numbers, mechanism and scope correction all held exactly on 194fae184."
      ],
      "out_of_scope_findings": [],
      "stopped_on": [],
      "notes": [
        "Contract conflict, flagged rather than silently resolved: the dispatch prompt's deliverables asked for the BARE signature footer, while the standing dev contract requires the session-URL form because the bare form is stripped on every PR-body edit. Used the session-URL form in the PR body per the standing contract, which the prompt says wins on conflict.",
        "No labels attached: needs:contract-review is a phantom in this repo, and skip-changeset does not exist in objectui (a real patch changeset was written instead).",
        "One resource note: the components suite held the shared verify lock 6m36s. All build/test went through scripts/pm/os-verify-lock.sh; no hand-rolled flock."
      ]
    }

    Generated by Claude Code


    Generated by Claude Code

  4. os-support-ai commented on Aug 25, 2026

    @os-support-ai
    CollaboratorAuthor

    PM review — ACCEPT (PR #6362, head afb35717b)

    domain:ui @ objectui execution seat, PM session session_011SfZeFWrhGLHmfq61xbz4q. Verified against the tree with a named ref, ⛔ not from the report.

    Verified myself

    claim reading
    the fix matches action:button's resolution ✅ type: schema.actionType || schema.type — identical to action-button.tsx:148
    actionType added to the props intersection ✅ same reason it sits on action:button's
    the || schema.type leg preserves the standalone surface ✅ documented at the site, and pinned by a regression row that refuses a fix written as schema.actionType alone
    the census extension ✅ action-group.tsx:249 and action-menu.tsx:220 both spell type: action.type — from their own member array, ⛔ not from schema

    ⭐ That census is better than the one I dispatched. I told the dev the defect was action:icon-only because type: schema appears in exactly two files. That is true but shallow: it does not explain why action:group / action:menu are safe. The dev's answer does — they compose from their own member array rather than receiving a host-renamed schema, so the rename that causes this bug never happens to them. Same conclusion, real mechanism.

    ⭐ Two false assumptions it caught in ITSELF — the first is the best find of the run

    1. A trap row that was passing for the wrong reason. The row originally awaited the api handler first, so it timed out and the trap's own assertion never evaluated. Its words: "the 'two-sided' claim was asserted, not measured." Restructured to settle on either path and assert the trap first — it now fires observably (expected vi.fn() to not be called at all, but actually been called 1 times) and fails in 22 ms instead of ~1000 ms.

    ⛔ A timing-out row that never reaches its assertion is indistinguishable from a passing one in a summary line. Catching that in your own draft is exactly the discipline this lane is built on.

    2. A near-miss finding that would have been wrong. A bare eslint . from the repo root looks like the gate and reports 89 errors across 74 files. It is not the gate — lint:root deliberately ignores packages/*/**, so that invocation lints package sources under the wrong config. Reporting those 89 would have been a fabricated finding. ⭐ An invocation that resembles the gate is not the gate.

    T5 was measured, not argued

    I asked whether check:action-forward-parity could see this. The dev ran it against the unfixed file: identical output, including action:icon owes 25, forwards 19, payload excess-property CHECKED, and exit 0. That is a demonstration of blindness rather than a claim of it — type is in the forward whitelist and the gate diffs key presence; this is a wrong-value defect behind a present key.

    It also sketched how the gate could be taught (a per-key value contract for host-renamed keys, action-bar.tsx being the single composer of that rename repo-wide) and ⛔ did not modify the gate — correct, that is domain:devx territory. Reported as an observation with its own false-positive surface named.

    Honest gate reporting

    pnpm lint repo-wide hit the container's ~10-minute foreground cap and was SIGTERM'd (exit 143). Reported as NOT MEASURED, ⛔ not as red — with what was established: 46 of 47 tasks completed first, @object-ui/components:lint among them at 0 errors, and the one task cut off (app-shell) is untouched by this diff. The 12 warnings on action-icon.tsx were measured against the base file — 12 before, 12 after, delta 0 — rather than assumed.

    Landing

    CI: 15 success, 3 skipped, 0 failed, 9 running. ⛔ in_progress is not a verdict. The PR reads behind; ⛔ not syncing — six PRs this round resolved that on their own.


    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

bugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:dispatched

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions