Skip to content

fix(components,app-shell): autoTrigger honours the action's own declared visible gate, and reports the refusal (objectui#4191) - #10272

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-4191-autotrigger-honours-visible
Sep 24, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-4191-autotrigger-honours-visible

Conversation

@os-support-ai

Copy link
Copy Markdown
Collaborator

Fixes #4191

Clause-②: no

What changed

Ruling A (comment 5812953871, maintainer 「objectui 第⑦批 同意」): an action's own declared visible outranks autoTrigger. A hidden action is not run, and the refusal is reported, not swallowed.

Renderers (@object-ui/components). The shared useAutoTriggerOnce in renderers/action/auto-trigger.ts now takes the action plus the renderer's own visible verdict (the same fail-closed useCondition value its early return reads). The "is it hidden?" test is written once in that hook, using hasDeclaredVisibilityGate, the same test the early returns use. If the action is hidden, the hook does not execute it. It shows a toast.warning naming the action (new key actions.notAvailableHere) and, in non-production builds, writes a console.warn diagnostic naming the action and its predicate. Both happen at most once per mounted action. action:button and action:menu both call this one hook, so they refuse identically (the #4162 parity principle). In action:menu, the headless ActionAutoTrigger now computes its action's verdict through the same local hook ActionMenuItem uses (useMenuActionVisible), against the same row.

A refusal does not use up the once-guard. The gate is re-checked on every commit, so an action whose predicate turns true later in the same mount (for example, an ambient scope that arrives after first paint) still runs once. This is pinned in both renderers.

Deep-link preparation (@object-ui/app-shell). ObjectView now arms the runAction nav deep link through a new internal hook, useOfferedNavRunAction in hooks/useNavRunAction.ts. The hook finds the candidate action (same placement check as before, actionRendersAt(a, 'list_toolbar')) and evaluates its visible using the renderer's own predicate, built from the same exported functions with the same inputs: hasDeclaredVisibilityGate, a fail-closed useCondition(toPredicateInput(visible), usePredicateRecordContext(undefined), { throwOnError: true, label }), and the ambient predicate scope read at the same tree position. A list toolbar has no row, because action:bar is mounted there without data. If the candidate is hidden, it is not marked autoTrigger, the param is not stripped (the one-shot intent survives), and the same notice key plus a dev diagnostic are emitted. There is no second evaluator. A parity table drives nine visible shapes through the prep step and through the real action:bar, and requires the same verdict from both: undeclared, empty, true/false, true and false predicates, a throwing predicate (fail-closed), and an ambient-scope predicate both ways. The consume-once logic that useNavRunAction and the new hook share is extracted verbatim into useConsumeOnce. useNavRunAction's behaviour is unchanged, and EnvironmentListToolbar still uses it.

i18n. New key actions.notAvailableHere in all ten locale packs. It is in the actions block, away from seat 1's search.resultsCount* keys. The de quote-pairing count pin in de-quote-pairing-3876.test.ts goes from 62 to 63 because the new de value contains one more matched pair of German quotes.

Not an authorization hole, and this PR does not make it one. Confirm dialogs, param collection, entitlement checks and server-side permissions apply on every execute path, before and after this change. The change protects the author's rule about where an action may be offered.

The pin inversion

The action-overflow-autotrigger.test.tsx test titled "the ACTION's own declared visible gate does not suppress it — and inline agrees with overflow" is rewritten as "…REFUSES it, and says so — inline agrees with overflow (objectui#4191)". It is not deleted. It asserts no execution, one notice naming the action and one dev diagnostic in each of the inline and overflow layouts. Three sibling tests were added: the notice appears once however many re-renders happen, a refusal does not use up the once-guard (checked in both renderers), and an empty predicate is not a gate.

Grep for other pins that assert execute-despite-hidden (tracked files, .changeset/ excluded):

  • old test name gate does not suppress it: 1 hit on base 0427036 (control, lit), 0 on HEAD.
  • execute=1: 1 hit, which is the historical note inside the inverted test.
  • gate does not suppress the trigger: 1 hit, in packages/components/CHANGELOG.md. That is published history and stays unedited.
  • does not suppress: 10 hits. All are unrelated except that same CHANGELOG line.
  • autoTrigger together with visible in *.test.*: every hit is in the inverted file.
    No contrary pin is left.

Measurements

  • Premise, before (base 0427036): action-overflow-autotrigger.test.tsx ran 12/12 green, including the old pin that asserts the hidden action executes, in both renderers. That is the card's rendered="" execute=1 measured on the current tree.
  • Ablation 1, committed state, via ablation-replace.mjs: in auto-trigger.ts, const hidden = hasDeclaredVisibilityGate(action.visible) && !isVisible; was replaced with const hidden = false;. The mutation landed (anchor 1 to 0, blob dcd4daf8cc72 to 5cbfef12012f). Result: 3 failed / 12 passed, all three being the refusal tests. The file was restored (blob equals HEAD, git diff HEAD empty). Components resolve through the vitest source alias, so no build leg was involved.
  • Ablation 2: in useNavRunAction.ts, the arm condition candidate !== undefined && !hidden was replaced with candidate !== undefined. The mutation landed (blob df480aa988ce to e0c4fec930c5). Result: 6 failed / 8 passed: the hidden-candidate test, the once-notice test, and four parity rows (literal false, false predicate, throwing predicate, scope says no). Restored, git diff HEAD empty.

Tests and gates (final HEAD 23cbae7, after git merge origin/main)

  • pnpm exec vitest run packages/components/src/renderers/action/ packages/i18n/ plus 41 app-shell files (every ObjectView importer and every useNavRunAction / autoTrigger test): 130 files / 1903 tests passed.
  • Earlier, at merge commit e64fad5: full packages/components/ + packages/i18n/: 357 files, where the only failure was the de quote-count pin (fixed in 23cbae7, i18n re-run 70/70). Components source is unchanged since.
  • type-check for components, app-shell and i18n: exit 0 after building the app-shell dependency closure with turbo. --listFiles confirms both new or edited test files are in their tsconfig.test.json.
  • lint for the same three packages: exit 0 (0 errors).
  • check:vi-mock-specifiers, check:vi-mock-inherit, check:vi-mock-override-shape, check:changeset-claims, check:i18n-keys, check:i18n-drift, check:i18n-dead-keys, check:i18n-designer-parity, check:new-line-citations, check:control-bytes, check:action-forward-parity, check:test-path-roots, check:unreferenced-sources, check:pending-changeset-literals, check-changeset-presence.mjs, check:eager-locale-catalogues, check:eager-closure: all exit 0. The two eager gates were run after a console build, because they read apps/console/dist.
  • Not run locally: the full test farm and the other check:* gates. CI runs them.

Acceptance notes

  • EnvironmentListToolbar (on sys_environment) arms its own deep link through the unchanged useNavRunAction. It was outside this card's file surface and the ruling's ObjectView clause. If create_environment ever declares a visible that evaluates false, the renderers now refuse it and show the notice, but that toolbar's preparation step still consumes the URL param. I did not measure whether any deployed create_environment declares visible.
  • ObjectView's preparation step still does not apply action:bar's capability gate (requiredPermissions). That behaviour predates this card and is unchanged here.
  • action:icon does not consume autoTrigger at all. Probe on this branch: an inline component: 'action:icon' member gave icon_execute=0, with control button_execute_control=1. This is reported separately and not fixed here.

Generated by Claude Code

…e gate, and reports the refusal (objectui#4191)

Ruling A: the author's declared `visible` outranks the transport flag. The
shared `useAutoTriggerOnce` now takes the renderer's own visible verdict;
a hidden action is refused (not run) with a user-facing notice
(`actions.notAvailableHere`, all ten packs) plus a dev diagnostic.
`action:button` and `action:menu` inherit it identically. The parity pin
in action-overflow-autotrigger.test.tsx is inverted to assert the refusal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LkCKMa5bvrw3L4ezcNXEXW
…nsume an action its author hid (objectui#4191)

`useOfferedNavRunAction` gates the `?runAction` candidate by the same
predicate `action:button` evaluates (hasDeclaredVisibilityGate +
fail-closed useCondition, no row on the list toolbar, ambient scope).
A hidden candidate is not marked autoTrigger, the param stays in the URL,
and the refusal is reported (notice + dev diagnostic). ObjectView uses it;
a parity table pins the prep verdict against the real action:bar.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LkCKMa5bvrw3L4ezcNXEXW
…onents, app-shell and i18n

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LkCKMa5bvrw3L4ezcNXEXW
…actions.notAvailableHere adds

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LkCKMa5bvrw3L4ezcNXEXW
@github-actions

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 3 pending changeset(s) describe a file this change touches

Their bodies publish verbatim into the CHANGELOG at the next release, so this is a request to re-read them against your diff — addressed here because you are the one seat that can answer it without re-deriving anything.

⛔ Nothing here blocks, and nothing here is a verdict on your change. This gate exits 0, is not a required context, and judges name resolution, never meaning: it asked whether a pending body names a file you touched. "Is this sentence still true?" is the one question it will not answer, and the one you are being asked to answer.

.changeset/6306-action-icon-type-resolution.md

  • names action-button.tsx → packages/components/src/renderers/action/action-button.tsx — edited by this change

    Scope is this one renderer. type: schema appears in exactly two files under renderers/action/ — action-button.tsx (already correct) and action-icon.tsx; action:group and action:menu compose their members differently and are untouched.

.changeset/6661-app-launcher-nav-menu-renderers.md

  • names en.ts → packages/i18n/src/locales/en.ts — edited by this change

    Three new strings — the launcher's and the menu's accessible names, and the menu's empty state — are declared under console.nav in en.ts and its nine sibling packs. An inline defaultValue alone is not a fix: it renders English at one call site and leaves the string untranslatable everywhere (objectui#3517).

.changeset/7070-no-invented-gantt-date-fields.md

  • names app-shell/src/views/ObjectView.tsx → packages/app-shell/src/views/ObjectView.tsx — edited by this change

    • app-shell/src/views/ObjectView.tsx — the console object page. The inline branch becomes ganttViewOptions, the sibling of calendarViewOptions and timelineViewOptions: the declared block spread whole, title floored at 'name', no date field invented. - plugin-list/src/ListView.tsx — the render branch AND the capability gate. - plugin-view/src/ObjectView.tsx — generateViewSchema, the authored object-view element route, which bypasses ListView entirely.

Read the paragraph, not the line: both false halves of the objectui#8617 claim sat in one paragraph, and correcting either alone would have left it asserting the same wrong thing.

If a claim did go false, correct the body. That is precedented and prose-only, frontmatter untouched; check-changeset-overwrite.mjs will report the correction as its own case 2 ("correcting a declaration on purpose … legitimate"), which is the intended shape — one gate asks for the read, the other records the write.

Not covered, stated so nobody reads this as more: a born-false claim that spells no line address at all (objectui#9495 coordinated one by ORDINAL — "a grep finds that member first" — and deciding that means reading what the sentence means), a claim spelled as a symbol or a package rather than a backticked file name, and a file named ambiguously.

Compared the checked-out tree with 5f00ff491 (merge-base with origin/main): 18 file(s) changed outside .changeset/, read against 1285 pending declaration(s) that publish a body (1854 pending in total). · run

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3037.6 KB 3104.5 KB
Main entry chunk (gzip) 147.8 KB 350 KB
Entry file index-TKkhdz1e.js —
Status PASS —

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.68KB 6.20KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 547.85KB 131.37KB
core (index.js) 8.94KB 3.59KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 222.47KB 61.83KB
fields (index.js) 253.40KB 63.99KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.22KB 2.26KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 33.36KB 10.88KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.02KB 11.00KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 51.44KB 14.62KB
plugin-charts (index.js) 71.82KB 20.13KB
plugin-chatbot (index.js) 198.27KB 47.18KB
plugin-dashboard (index.js) 133.00KB 35.19KB
plugin-designer (index.js) 215.98KB 44.34KB
plugin-detail (index.js) 257.69KB 67.12KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 144.65KB 37.10KB
plugin-gantt (index.js) 167.99KB 41.37KB
plugin-grid (index.js) 215.46KB 58.88KB
plugin-kanban (index.js) 48.83KB 15.21KB
plugin-list (index.js) 113.90KB 28.11KB
plugin-map (index.js) 21.74KB 7.07KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.68KB 8.95KB
plugin-tree (index.js) 10.58KB 3.72KB
plugin-view (index.js) 85.18KB 21.05KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 106.60KB 35.16KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.38KB 1.98KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.74KB 2.54KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 15.71KB 5.30KB
types (ai.js) 4.11KB 2.06KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 17.15KB 6.32KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 24, 2026 15:15
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 24, 2026
Merged via the queue into main with commit 978507b Sep 24, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-4191-autotrigger-honours-visible branch September 24, 2026 15:26
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 28, 2026
…hook (objectui#10274) (objectstack-ai#10342)

Fixes objectstack-ai#10274

Clause-②: no

## What changed

`action:icon` now runs `autoTrigger` through the shared
`useAutoTriggerOnce(schema, isVisible, handleClick)` in
`renderers/action/auto-trigger.ts`. `action:button` and `action:menu`
already call this hook (PR objectstack-ai#10272). The call sits above the icon's
`visible` early return, as it does in the button.

- `run` is the icon's own `handleClick`, the same function its `onClick`
calls. So an auto-triggered run and a click reach `ActionRunner.execute`
with the same `ActionDef` (pinned: the two defs are compared with
`toEqual`) and pass the same confirm gate (pinned: a declined
`confirmText` runs nothing).
- `isVisible` is the verdict the icon's own early return reads. An icon
its author hid is therefore refused, with the `actions.notAvailableHere`
notice and the dev diagnostic, and not run (objectui#4191).
- One changeset, `patch` for `@object-ui/components`.

No declared key, schema, export or accept set moves. `auto-trigger.ts`
is not edited.

## Readings for the dispatch's mechanism assumptions

- **A1: reproduced on main before the change** (base `1dbb993`, after PR
objectstack-ai#10272 landed). The new test file ran 7 failed / 1 passed. In the card's
probe row the handler was called 1 time, not 2. That one call is the
flagged `action:button` control's: on base, `action:icon` has no path
that executes without a click, so the flagged icon member beside it in
the same bar ran nothing. The hidden-icon row read 0 notices. The one
green row was "an inline icon WITHOUT the flag runs nothing on mount".
After the change: 8/8.
- **A2: the run path is confirmed; the visibility verdict is the icon's
own, and it is not the button's.** The hook gets `handleClick`, so the
auto-triggered run and a click go through the same `execute`.
`isVisible` is computed exactly as the icon's early return computes it.
That differs from `action:button` in one way: the icon evaluates
`visible` without `throwOnError` (fail-soft), while the button uses it
(fail-closed). That policy is pinned as it stands by
`action-record-predicate-root.test.tsx` (the row "`visible` keeps its
EXISTING fail-soft policy on a faulting predicate", whose comment says
the policy is "Not what this PR decides"), and it is tabled in
`action-template-predicate-gate.test.tsx`. This PR keeps the hook's
contract (the verdict is "the same value its early return consults"), so
the trigger follows what the icon renders. The only case where that
matters is a `visible` predicate that THROWS: an inline icon renders and
now runs, where a button hides and refuses. See Acceptance notes.
- **A3: once-only.** Pinned: three re-renders with fresh action objects
and the flag still set, after the run, leave 1 execution. The flag
flipping true later runs exactly once. A throwaway probe (not committed)
measured one more case: unmount and remount with the flag STILL set runs
again (1 then 2), and `action:button` does the same (1 then 2). The
guard is per mounted component, as `auto-trigger.ts` documents. On the
deep-link path the host drops the flag after the first commit
(`useConsumeOnce` returns null once it has consumed the param), so a
later remount has nothing to run.
- **A4: other renderers.** Probe (not committed): one `action:bar`, an
unflagged control plus one member flagged `autoTrigger: true`, varying
only the member's `component`. `action:button`: execute=1, rendered.
`action:icon`: execute=1, rendered (after this change). `action:menu`:
execute=0, and the member is not rendered at all. `action:group`:
execute=0, not rendered at all. This is reported as an out-of-scope
finding in the dev report and is not changed here.

## Tests and gates (final HEAD `3b6341b`)

- `pnpm exec vitest run packages/components/src/renderers/action/` plus
the 20 test files outside that directory whose source mentions
`autoTrigger`, `runAction`, `action:icon` or `action-icon`. That
includes the PR objectstack-ai#10272 pins in `action-overflow-autotrigger.test.tsx`
and `useOfferedNavRunAction.test.tsx` and the environment toolbar
deep-link suites. Result: **41 files, 913 tests passed**.
- Declared narrowing: this is not the whole `@object-ui/components`
suite (290 test files; CI runs all of them). The hook does nothing for
an action without `autoTrigger: true` beyond reading the i18n context,
because its effect returns first. Every test file that names the flag,
the deep link or the icon is in the run. Blind spot: a test that reaches
an icon through a fixture file without naming any of those in its own
source.
- `pnpm --filter @object-ui/components run type-check` (both `tsc
--noEmit` and `tsc -p tsconfig.test.json`): exit 0, run after building
the dependency closure (`pnpm --filter '@object-ui/components^...' run
build`, exit 0). The test program does include the new file: its first
run reported a type error in it, corrected in `3b6341b`.
- eslint on the two touched source files, with the components package
config and inline config honoured as `pnpm lint` runs it: 0 errors.
`action-icon.tsx` has 8 warnings, the same 8 as on base. The new test
file has 0.
- `check-changeset-presence`, `check-changeset-no-major`,
`check-changeset-fixed`, `check:new-line-citations` (0 new),
`check:control-bytes`, `check:test-path-roots`,
`check:action-forward-parity`, `check-changeset-claims`,
`check:pending-changeset-literals` and `check-vi-mock-override-shape`:
all exit 0 at `3b6341b`.

## Ablation (committed state, `ablation-replace.mjs`)

In `action-icon.tsx`, the line `useAutoTriggerOnce(schema, isVisible,
handleClick);` was deleted. The mutation landed (anchor 1 to 0, blob
`8e859d91f90a` to `234f056ef052`). New test file: 7 failed / 1 passed,
the same rows that were red on base. Restored: the blob equals HEAD and
`git diff HEAD` is empty. The test imports the renderer by relative
path, so there is no build leg.

## Pending changesets

- `4191-autotrigger-honours-visible.md` (PR objectstack-ai#10272) names
`action:button` and `action:menu` and says both go through the one
shared hook. That is still true, and it does not claim they are the only
two, so it is left as is. Its sentence that the receiving renderer
"cannot change the outcome" is closer to true after this change than
before it.
- `6306-action-icon-type-resolution.md` names `action-icon.tsx`, so
`check-changeset-claims` asks for a read. Its scope paragraph counts
`type: schema` in two files under `renderers/action/`. This diff does
not change that count (the same files carry it on base and HEAD), so it
is left as is. Separately, and not caused by this diff: that changeset
quotes the button's forward as `schema.actionType || schema.type`, and
objectui#7415 later removed the `|| schema.type` half.

## Acceptance notes

- **The icon's fail-soft `visible` against the fail-closed deep-link
preparation step.** `ObjectView` arms `?runAction=` only after
`useOfferedNavRunAction` evaluates the candidate's `visible`
fail-closed, which is the button's policy. For an action rendered as an
icon whose `visible` predicate throws, the icon is shown (fail-soft)
while the deep link is refused with the "not available on the current
page" notice. This existed before this PR and does not depend on it
(before, the icon ran nothing in either case). It is reachable only
through a predicate that throws, which is an authoring error, and the
icon's policy is pinned as undecided. Not filed.
- **Comments outside this PR's file surface.** `auto-trigger.ts` still
names `action:button` and `action:menu` as the hook's consumers, and
describes each renderer's verdict as "the same fail-closed
`useCondition`". The icon's verdict is fail-soft. `useNavRunAction.ts`
names the same two renderers. These are comments only and are not edited
here, because they are outside the claimed file surface.
- **`action:group`'s inner actions** (its inline buttons and dropdown
items) do not read `autoTrigger`. No host puts the flag on a group's
inner actions today, so nothing reaches that path.

Dispatched dev for seat `domain:ui#4`, session
`https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C`.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(components): autoTrigger executes an action whose own declared visible gate hides it — in both renderers

2 participants