Skip to content

fix(components): place action:bar members authored action:menu / action:group (objectui#10345) - #10470

Merged
os-litant merged 3 commits into
mainfrom
claude/issue-10345-action-bar-menu-group-placement
Sep 25, 2026
Merged

os-litant merged 3 commits into
mainfrom
claude/issue-10345-action-bar-menu-group-placement

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #10345
Clause-②: no

On an action:bar, a single action's component now decides where it renders (ruling A, comment 5825579152). Before this change, a member authored component: 'action:menu' or 'action:group' was handed alone to a renderer that reads schema.actions. That renderer returned null, so the action vanished and a ?runAction= deep link to it ran nothing.

What changed (all in action-bar.tsx)

  • action:menu: the action goes into the bar's existing single overflow menu (the one "More" button). It is taken out before the maxVisible split, so it never spends an inline slot. page:header already reads the key this way ("forces an action into the ⋯ menu regardless of the count").
  • action:group: the action renders inline, through the action:button renderer, inside a ButtonGroup (the existing custom/button-group primitive, role="group"). The group is shared with its adjacent action:group members.
  • The docblock's opening paragraph now describes this placement. The usage-example line with the trash-2 icon is untouched (draft PR objectui#8941 edits that line).
  • No contract change. action-menu.tsx and action-group.tsx are untouched, and neither renderer is taught to accept a single action.

"Adjacent", as measured and pinned

Adjacent means consecutive in the inline row the bar draws: after ordering (order, then primary), after the maxVisible cut, and with the menu-placed actions already removed.

  • [G1, G2, Plain, G3] gives two groups, [G1 G2] and [G3].
  • [G1, MenuPlaced, G2] gives one group, [G1 G2]. The menu-placed action is not in the row, so it does not split the run.
  • Each group member counts toward maxVisible. A member past the budget spills into the "More" menu like any inline member.

PM mechanism assumptions, measured

  • A1 held. A member routed into combinedOverflow runs its deep link through action:menu's existing ActionAutoTrigger / useAutoTriggerOnce, with no new trigger code. Pinned by the probe's menu row and the deep-link rows.
  • A2: the assumption's risk was real, and it was avoided inside the file surface. action-group.tsx draws its own InlineActionButton. It consumes no autoTrigger, and its visible gate is fail-soft where action:button's is fail-closed. Routing grouped members through the action:group renderer would therefore leave the deep link dead, and would gate them differently from an inline member. So the bar draws the group itself and renders each member through action:button, the renderer an ungrouped member gets. The deep link runs and the gates are identical by construction. action-group.tsx needed no change, so there was no needs-decision fork.
  • A3. A menu-placed member does not count toward maxVisible (pinned with [M1, B1, B2], maxVisible: 2: both B's stay inline). An overflow that holds only menu-placed members renders exactly one "More" (pinned by the child count and the aria-haspopup count).
  • A4. Overflow order: the actions that spilled past maxVisible come first, then the menu-placed ones, each in the bar's own order, then the separator and the system actions. This is the same relative order page:header uses. The pin includes a menu-placed member with order: -1, which sorts first in the bar but still does not claim the primary inline slot.
  • A5. requiredPermissions is filtered before the split, so it gates every placement identically. Pins:
    • a hidden menu-placed member stays hidden in the opened menu, and its deep link is refused with the objectui#4191 notice instead of being run;
    • a hidden grouped member is not drawn while its group sibling is;
    • a disabled menu-placed member is a disabled menu item;
    • a capability the caller lacks hides the member at both placements. Each of those rows has a held-capability control leg, because the defect also reads "absent".

The card's four-row probe, after the fix

member component executions placement
action:button 1 inline
action:icon 1 inline
action:menu 1 the "More" menu
action:group 1 a button group

Verification (at da122c4cf)

  • New suite action-bar-member-placement-10345.test.tsx: 21/21 green.
  • Reverse proof. action-bar.tsx was replaced with its blob at the base commit 0348bc9f1, which removes the placement branch. The on-disk mutation was confirmed with grep counts: inlineRow went from 2 to 0, inlineActions.map from 0 to 1, and the file hash equalled the base blob. The suite then read 19 failed, 2 passed. The 2 are the action:button / action:icon probe rows, which are correct in both worlds. The menu and group probe rows read 0 runs, matching the card's table. The file was restored with git checkout HEAD, and the restore was proved by the hash equalling the HEAD blob and an empty git diff HEAD. The first ablation run showed the capability rows staying green (they were vacuous), so the held control legs were added and the proof was re-run.
  • 38 suites green, 668 tests (pnpm exec vitest run from the repo root). They were: every @object-ui/components suite naming action-bar, action:bar, action:menu, action:group or useAutoTriggerOnce; every file under renderers/action/__tests__/; the app-shell deep-link consumers (EnvironmentListToolbar deep-link suites and useOfferedNavRunAction); and the two script pins that name action:bar.
  • The @object-ui/components dependency closure was built, then pnpm --filter @object-ui/components type-check exited 0. That runs the package's own tsc --noEmit and tsconfig.test.json, and --listFiles shows both include the touched and new files.
  • ESLint on the two touched source files: 0 errors. action-bar.tsx has 5 warnings at the base commit and 5 now, all on untouched lines.
  • check-changeset-presence, check-changeset-no-major, check:new-line-citations (0 new), check:control-bytes, check:changeset-claims ("No pending changeset names a file this change touches"), check-vi-mock-override-shape, check-test-path-roots and check-lint-coverage all exited 0.
  • NOT MEASURED: the button group's rendered look in a real browser. jsdom computes no Tailwind, so the attached-button styling and empty:hidden were not observed.

Changesets

  • New: .changeset/10345-action-bar-menu-group-placement.md (@object-ui/components, patch).
  • Corrected in this PR: the pending objectui#10274 entry said "action:bar renders each inline action with the renderer its component names". An inline action:group member now renders through action:button, so that sentence is narrowed to action:button / action:icon. Only the body changed; the front matter is byte-identical.
  • Read and left alone as still true: the objectui#6306, objectui#7415, objectui#4191 and objectui#5493 entries. Their "renames the type as it spreads a member" and "decided by maxVisible" sentences describe members whose component does not place them, and the literal type: componentType, actionType: action.type spread is kept.

Acceptance notes (observations, not filed)

  • action:group's own leaves (InlineActionButton, DropdownActionItem) consume no autoTrigger, while auto-trigger.ts states that every renderer that can receive an action consumes it. No host sends a flagged action into a standalone action:group today (the deep-link host composes an action:bar), so this is dormant and was not filed.
  • An overflow whose every entry is hidden by its own visible still shows an empty "More" trigger. That was already true for spilled actions before this change.
  • content/docs/guide/slotted-pages.md says page:header uses "the same rules as action:bar", including component: 'action:menu' pinning an action into the ⋯ menu. That sentence was false for action:bar before this change and is true after it.

Dispatched by the domain:ui#4 seat; this run's session is https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C.


Generated by Claude Code

…on:group

On an action:bar, a single action's `component` now decides where it renders
(ruling A). `action:menu` puts it in the bar's one overflow menu without
spending an inline slot; `action:group` renders it as an action:button inside a
button group shared with the adjacent action:group members of the inline row.
Both used to hand the single action to a renderer that reads `schema.actions`,
so the action vanished and a ?runAction= deep link to it ran nothing.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C
…ntrol leg

Without it the lacking-permission rows read "absent", which is also what the
unplaced member read before the fix, so they could not fail.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C
…entence true

The pending objectui#10274 entry said action:bar renders each inline action
with the renderer its `component` names. An inline action:group member now
renders through action:button, so the sentence is narrowed to the two values
it still holds for. Front matter unchanged.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3044.9 KB 3104.5 KB
Main entry chunk (gzip) 147.8 KB 350 KB
Entry file index-cdhRgsUR.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) 27.95KB 10.04KB
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.52KB 3.45KB
auth (index.js) 3.63KB 1.64KB
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) 27.13KB 7.95KB
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) 544.59KB 130.15KB
core (index.js) 9.22KB 3.71KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 223.86KB 62.25KB
fields (index.js) 256.90KB 65.08KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.28KB 11.09KB
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) 16.01KB 3.93KB
plugin-calendar (index.js) 51.22KB 14.56KB
plugin-charts (index.js) 74.76KB 20.82KB
plugin-chatbot (index.js) 198.36KB 47.20KB
plugin-dashboard (index.js) 133.59KB 35.39KB
plugin-designer (index.js) 216.12KB 44.37KB
plugin-detail (index.js) 231.66KB 61.23KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 146.34KB 37.47KB
plugin-gantt (index.js) 168.25KB 41.46KB
plugin-grid (index.js) 215.43KB 58.90KB
plugin-kanban (index.js) 49.30KB 15.39KB
plugin-list (index.js) 114.37KB 28.24KB
plugin-map (index.js) 21.83KB 7.07KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.64KB 8.94KB
plugin-tree (index.js) 10.52KB 3.69KB
plugin-view (index.js) 87.90KB 21.97KB
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) 114.58KB 37.60KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.03KB 1.86KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.78KB 2.56KB
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) 18.27KB 6.20KB
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

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: da122c4cf3967d3d3542777a675b1f0f0a99607a

Rendered by an isolated review subagent spawned by the domain:ui#4 seat; its served tier was checked against its transcript stamps (71 of 71 model stamps at the review tier). Adopted by this seat.

① Derived judgments

  • Inputs. Head da122c4cf3967d3d3542777a675b1f0f0a99607a resolved from the PR's head.sha and matches the fetched refs/pull/10470/head. Merge-base with origin/main is 0348bc9f119b524029a63f2850d11c16b8b3a310 (the dev's stated base); origin/main has since advanced to 34bfd6060d3a8a9e61320259906067e7533252bc. Read: the card body, triage 5820756170, ruling 5825579152 (letter A), claim 5825833974, dev report 5826498127, the PR body, the three commits, and the four blobs at base and head.

  • (a) The ruling, exactly: carried. git diff --stat base..head names exactly four files; action-menu.tsx and action-group.tsx are byte-identical at base and head, and both still return null on an empty schema.actions. No schema, type, registry input or export moved (see g). The dispatch is entirely in action-bar.tsx: action:menu members are filtered out of the row candidates before the maxVisible slice and appended to overflowActions; action:group members are folded into runs in inlineRow and drawn inside the repo's custom/button-group ButtonGroup element, each member rendered through the registered action:button renderer. On the fork question: the ruling says "renders it in a button group with its adjacent action:group members", places the dispatch in action-bar.tsx, bans teaching the renderers a single action, and requires the deep link to run. The registered action:group renderer cannot satisfy that last bullet without an edit to action-group.tsx (its InlineActionButton and DropdownActionItem never import useAutoTriggerOnce), and that file is outside the claimed surface; composing a synthetic multi-action action:group schema would not have been option B, but it would still have left the deep link dead and the visible gate fail-soft. So the bar-drawn group is the compliant in-surface route, the reasoning is stated in the code comment and the PR, and I judge it within the letter, not a quiet fork. Disclosed divergence for the seat: ButtonGroup is NOT the primitive the action:group renderer draws. The renderer's inline mode emits a plain div with flex items-center gap-2 and no role; ButtonGroup emits a div with role="group", data-slot="button-group" and attached-edge classes (inner edges lose their rounding and the shared border). So a bar-drawn group is an attached segmented control while a standalone action:group is a gapped row. The ruling's wording "a button group" is satisfied by the repo primitive of that name; the dev flagged the look as not measured in a browser.

  • (b) Menu placement, A1/A3/A4/A5: hold. A1: action:menu mounts one ActionAutoTrigger per entry of schema.actions outside the dropdown content, and the bar passes combinedOverflow there, so a menu-placed member's autoTrigger reaches useAutoTriggerOnce unchanged; pinned by the probe's menu row and both deep-link rows. A3: menuPlaced is removed before rowCandidates.slice(0, maxVisible), so it spends no inline slot (pinned with [M1, B1, B2], maxVisible: 2); an overflow holding only menu-placed members still yields a non-empty combinedOverflow and exactly one MenuRenderer (pinned by a toolbar child count of 2 and one aria-haspopup). A4: overflowActions is the spilled tail followed by menuPlaced, both filter/slice results of the already-sorted filteredActions, so the order is stable; combinedOverflow then appends the system actions with separator-before on the first, unchanged from base. page:header in containers.tsx builds its menu as the spilled tail, then its action:menu members, then the record_more-only ones, so the relative-order claim is true. The order: -1 menu member sorting first yet not claiming the primary slot is pinned. A5: requiredPermissions is applied in filteredActions before any placement; visible is fail-closed (throwOnError: true plus hasDeclaredVisibilityGate) in both action:button and ActionMenuItem/ActionAutoTrigger; the disabled/enabled ternary is the same three-way expression in both; and data reaches the overflow menu exactly as at base. Separator placement: showSeparator needs a positive index, which holds whenever any business entry precedes the first system action.

  • (c) Group placement. "Adjacent" is defined and pinned as consecutive in inlineActions: after ordering, after the maxVisible cut, after menu-placed members are removed. [G1, G2, Plain, G3] yields two groups; [G1, MenuPlaced, G2] yields one; [G1, G2, G3] at maxVisible: 2 puts G3 in the More menu as a plain menu item, so a group can be cut by the budget and its tail loses group placement. That is stated in the PR, the code comment and the changeset; it is a reading of "every member renders somewhere" the ruling left to the dev. Deep link: a grouped member is rendered by action:button, whose useAutoTriggerOnce(schema, isVisible, handleClick) fires once per mounted action (the re-render loop with fresh objects is pinned). Visibility: a hidden member returns null from action:button, so it is not drawn and its siblings' attached edges reflow; when every member hides, React renders no child nodes, the ButtonGroup div is truly empty, and Tailwind's empty:hidden would remove it from layout and from the accessibility tree in a real browser. That all-hidden case is not pinned and, as the dev says, not observed outside jsdom, where an empty role="group" div would remain.

  • (d) Unchanged behaviour for action:button / action:icon members: confirmed. With no menu or group members, menuPlaced is empty and rowCandidates is filteredActions, so inlineActions is slice(0, maxVisible) and overflowActions is slice(maxVisible). Base returned the whole list plus an empty array when the length did not exceed maxVisible, else the same two slices; the two produce equal contents for every integer, zero, negative and NaN maxVisible (only array identity differs, and nothing compares identity). inlineRow then pushes each non-group member singly, and the render map calls renderMember(entry, entry.component || 'action:button'), which resolves the same registry entry, the same key={action.name}, the same spread { ...action, type: componentType, actionType: action.type, variant: action.variant || schema.variant, size: action.size || schema.size } and the same data={data} as the base inline path. combinedOverflow, the overflow menu props and the toolbar div are unchanged. A bar without menu/group members renders the same element tree.

  • (e) The pins are real and can fail. The suite has 21 cases: probe 4 plus 1, deep link 2 plus 2, menu 3, group 4, gates 1 plus 1 plus 1 plus 2. Walking each against the base blob: the two action:button/action:icon probe rows pass in both worlds; the other 19 fail at base because the member renders null (zero runs, absent placement, no role="group", a missing More trigger, an unmounted ActionAutoTrigger so no refusal toast, and the held-capability legs reading absent). That matches the dev's 19 failed / 2 passed. The two requiredPermissions rows are non-vacuous: the held leg supplies user.systemPermissions: ['manage_users'] through ActionProvider's context, which useHeldCapabilities reads off the runner, and asserts placement menu/group; the lacking leg supplies an empty array (holds nothing, so it gates) and asserts absent. The suite mounts the real registered renderers via module-scope side-effect imports of action-bar, action-button, action-icon, action-menu and action-group, under a real ActionProvider; there is no vi.mock, only spies on toast.warning and console.warn.

  • (f) Changeset truth. .changeset/10345-action-bar-menu-group-placement.md: every sentence checked true at head. The base-behaviour sentences match the base blob (dispatch on action.component || 'action:button', both renderers null on empty actions); the spec quotation matches packages/spec/src/ui/action.zod.ts on objectstack origin/main ("Defaults to 'button' or 'menu_item' based on location, but can be overridden"); the page:header claim matches containers.tsx; the order, adjacency, maxVisible accounting, gate and once-only sentences match the code and the pins; no line-address citations. .changeset/10274-action-icon-auto-trigger.md: the front matter (first line through the closing ---) hashes to sha256 3629c5082899c8a10ec55de11cd160b033644ca37271b96a9afecdc1e85e27e7 at both base and head; the corrected sentence is true at head; the original was true at base and would be false at head without the edit, since an inline action:group member now renders through action:button; every other sentence still describes action:icon, which still calls useAutoTriggerOnce. Correct and forced; the seat can ratify. Other pending changesets naming the terms: 6306 (the spread rename, and "action:group and action:menu compose their members differently and are untouched") still true; 7415 (the literal type: componentType, actionType: action.type spread; declarations composed by the three containers unaffected) still true; 4191 and 5493 still true, since their "decided by maxVisible" clauses describe members without a placing component, and 5493's is past tense; 4202, 5935, 7551, 7727, 9572, 5741, 5622, 4282 and 5610 are unaffected. Observation, outside the claim: the retired sentence still stands as a comment in action-icon.tsx ("action:bar renders an inline member with the renderer its component names") and in the docblock of action-icon-autotrigger.test.tsx. Both are about action:icon members, for which it remains true, but the general wording is now imprecise; a follow-up touch, not a blocker.

  • (g) Clause-② no: correct. @object-ui/components exports is "." and ./style.css; src/index.ts re-exports nothing from action-bar.tsx (only hasDeclaredVisibilityGate and resolveIcon from the action folder); the ActionBarSchema interface changed only in a doc comment; the ComponentRegistry.register('bar', ...) inputs and defaults are unchanged; and ActionComponent is derived from the spec enum in @object-ui/types, untouched. The accepted set is the same: both values were already legal; the bar now honours them.

  • (h) The out-of-scope notes. The auto-trigger.ts docblock states "every renderer that can receive an action consumes it ... through this hook"; at head action:group's two leaves import neither useAutoTriggerOnce nor hasAutoTrigger, so the claim is false at head and was already false at base. Producers are ObjectView and EnvironmentListToolbar, both composing an action:bar, and after this PR no bar member reaches the action:group renderer, so no current producer is affected. It is a real declared-but-unenforced gap (docblock versus code) worth a low-priority card: either route the action:group leaves through the shared hook or narrow the docblock. The second note (an overflow whose entries all hide still shows an empty More trigger) is pre-existing for spilled actions and now also reachable through a menu-placed member; same low-priority class.

② Semver level

patch on @object-ui/components is right. objectui AGENTS.md, 版本号策略: 「objectui 的 major 与 @objectstack(spec/client/formula)的 major 保持一致」; 「minor/patch 独立演进——objectstack 没动时不必跟发;objectui 自己的改动照常用 changeset 推进」; 「changeset 里不要声明 major —— fixed 组任一 major 都会把全组推上去、脱离 objectstack 的节奏 ... objectui 自身的破坏性变更也标 minor(在正文里写清 breaking 语义即可);唯一例外是跟随 objectstack 跨 major 的那一次同步升级」. This change adds no export, widens no accepted input and removes nothing; it makes two already-legal enum values render instead of vanishing. Not breaking, so not minor; never major. The front matter declares '@object-ui/components': patch only, and the Changeset Bump Policy and Changeset Fixed Group Check runs are green.

③ Boundary flags

  • Closing keywords: Fixes #10345 is the only one in the body (a regex over the close/fix/resolve forms found exactly that hit).
  • Model identifiers: none in the title, the body or the three commit messages. The commits end with the repository's mandated co-author and session attribution trailers, and the body cites the session id; neither is a model family or version name.
  • File surface, four files, matching the claim plus the 10274 correction: packages/components/src/renderers/action/action-bar.tsx modified (plus 95, minus 27); packages/components/src/renderers/action/__tests__/action-bar-member-placement-10345.test.tsx added (383 lines); .changeset/10345-action-bar-menu-group-placement.md added (31 lines); .changeset/10274-action-icon-auto-trigger.md modified (plus 3, minus 2, body only).
  • The trash-2 usage-example line is byte-identical at base and head (it moved from line 33 to line 44 because the docblock grew); draft PR chore(deps): lucide-react 1.31.0 -> 1.43.0, with the one retired spelling repaired #8941's hunk on that file touches only that line, so the two are hunk-disjoint.
  • Serial: of the 13 open PRs, only chore(deps): lucide-react 1.31.0 -> 1.43.0, with the one retired spelling repaired #8941 touches any of the four files (action-bar.tsx, the example line above). changeset-release/main (chore: release packages #5400) lists 1000 changeset deletions and none of ours. No other open PR touches renderers/action/.
  • mergeable_state: behind (mergeable: true, draft). The base moved from 0348bc9f... to 34bfd606... after the branch was cut; no conflict reported.
  • Check-runs on the head, polled at 60 s in the foreground from 04:12Z: complete after three polls, at 04:14:36Z. 43 runs: 40 success, 3 skipped, 0 failure, 0 in progress. The non-success runs are all skipped: dependabot, Test (coverage), Test (coverage shard ${{ matrix.shard }}/4). All eight Test (shard n/8) runs, Type Check, Lint, Build & E2E, Line Citation Gate, Changeset Claim Re-read, Changeset Declaration and Changeset Overwrite Report are green.

Implemented-by: claude/issue-10345-action-bar-menu-group-placement
Reviewed-by: session_01BP8CMtACxTdLjqR6rhd33C

VERDICT: PASS


Generated by Claude Code

@os-litant
os-litant marked this pull request as ready for review September 25, 2026 04:20
@os-litant
os-litant added this pull request to the merge queue Sep 25, 2026
Merged via the queue into main with commit cdefa2a Sep 25, 2026
45 checks passed
@os-litant
os-litant deleted the claude/issue-10345-action-bar-menu-group-placement branch September 25, 2026 04:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants