You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(spec): add explicit Action.order for deterministic action ordering (#2670) (#2682)
* feat(spec): add explicit Action.order for deterministic action ordering (#2670)
The record-header renders the first visible `record_header` action as the
primary button and pushes the rest into the `⋯` overflow menu. Which action
wins was decided purely by cross-file `defineStack({ actions })` registration
order, so system Approve/Reject decisions were appended after app actions and
buried in the overflow menu — the approver's first-glance decision was hidden.
Add an optional `Action.order` (number, lower = higher / more prominent,
default 0) and stable-sort every action group by it in
`mergeActionsIntoObjects()` — the single choke point for both `defineStack()`
and `composeStacks()`. The sort is stable and treats unset `order` as 0, so
groups where nobody sets `order` keep their exact registration order and array
reference: the change is a no-op until an author opts in. A plugin (e.g.
plugin-approvals) or app author can now give an action a low `order` to make it
stably hold the primary-button slot, instead of hiding other actions to "make
room".
Includes schema tests, merge-sort regression tests, and Action Protocol docs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018H4wb2JGo68NggKiTh5Dag
* chore(spec): classify Action.order in the liveness gate
The spec property-liveness gate (check:liveness) requires every authorable
property on a governed type to be classified. Classify the new `action/order`
property as `live` (framework stable-sorts each action group by it in
mergeActionsIntoObjects; objectui record_header consumes it for primary-button
selection).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018H4wb2JGo68NggKiTh5Dag
---------
Co-authored-by: Claude <noreply@anthropic.com>
`Action`: add an explicit `order` field so authors and plugins can decide which action holds the record-header primary-button slot, instead of depending on fragile cross-file `defineStack({ actions })` registration order (#2670).
6
+
7
+
`order` is an optional number, **lower = higher / more prominent**, defaulting to `0`. `mergeActionsIntoObjects()` now stable-sorts every action group — each object's `actions` and the top-level `actions` — by `order` at both `defineStack()` and `composeStacks()` time. In `record_header` the first visible action becomes the primary button, so a negative `order` promotes an action into the primary slot and a positive `order` demotes it toward the `⋯` overflow menu. This is the declarative lever a plugin such as plugin-approvals uses to make an `Approve`/`Reject` decision stably outrank app actions, rather than hiding the other actions to "make room".
8
+
9
+
Fully backward compatible: the sort is stable and treats unset `order` as `0`, so action groups where nobody sets `order` keep their exact registration order (and array reference). The record-header renderer (objectui) may additionally prefer a `variant: 'primary'` action when two actions tie on `order`.
Within each location group, actions render in **`order`** sequence — **lower comes first / more prominent**. In `record_header` the first visible action becomes the **primary button** and the rest fall into the `⋯` overflow menu, so `order` is how you decide which action holds the primary slot.
236
+
237
+
```yaml
238
+
# Approve holds the primary button even though the app action registered first.
239
+
name: approve_request
240
+
label: Approve
241
+
locations: [record_header]
242
+
variant: primary
243
+
order: -10 # negative → floats ahead of app actions (order defaults to 0)
244
+
```
245
+
246
+
Semantics:
247
+
248
+
- **`order` defaults to `0`.** An action with a **negative** `order` is promoted toward the primary slot; a **positive** `order` is demoted toward the overflow menu.
249
+
- **The sort is stable.** Actions that leave `order` unset (or tie on the same value) keep their original registration order, so adding `order` to one action never reshuffles the others. Setting `order` on nobody is a no-op — existing screens are unaffected.
250
+
- **No more fragile registration order.** Previously "who becomes the primary button" depended on the cross-file order in which `defineStack({ actions })` merged its arrays. `order` makes it declarative: a plugin such as [plugin-approvals](/docs/plugins/approvals) can inject an `Approve`/`Reject` decision with a low `order` so it stably outranks app actions, instead of the app having to *hide* its other actions to make room.
251
+
- When two actions tie on `order`, the record-header renderer MAY additionally prefer the one with `variant: 'primary'` when choosing the primary button.
252
+
253
+
<Callout type="info">
254
+
`order` sorts actions **within the same location**; it does not move an action between locations. Use `locations` to choose *where* an action appears and `order` to choose *its position there*.
255
+
</Callout>
256
+
232
257
### Visibility & Disabled Rules
233
258
234
259
`visible`and `disabled` are **CEL** predicates (not declarative `{ field, value }` objects). `disabled` may also be a plain boolean.
Copy file name to clipboardExpand all lines: packages/spec/liveness/action.json
+5Lines changed: 5 additions & 0 deletions
Original file line number
Diff line number
Diff line change
@@ -56,6 +56,11 @@
56
56
"status": "live",
57
57
"note": "objectui button styling."
58
58
},
59
+
"order": {
60
+
"status": "live",
61
+
"evidence": "packages/spec/src/stack.zod.ts (mergeActionsIntoObjects stable-sorts each action group by order)",
62
+
"note": "Deterministic action ordering (#2670). Framework stable-sorts every action group by `order` at defineStack/composeStacks time; objectui record_header picks the first as the primary button. Lower = higher; unset = 0."
* Explicit sort order WITHIN a UI location group (lower = higher / more
332
+
* prominent). Controls where the action lands in each `locations` group
333
+
* instead of relying on cross-file `defineStack({ actions })` registration
334
+
* order — which is fragile and couples unrelated features.
335
+
*
336
+
* In `record_header` the first visible action becomes the primary button, so
337
+
* a low (or negative) `order` promotes an action into the primary slot and a
338
+
* high `order` demotes it toward the `⋯` overflow menu. This is the
339
+
* declarative lever a plugin (e.g. plugin-approvals) or app author uses to
340
+
* make a decision like Approve/Reject stably outrank app actions, rather than
341
+
* hiding the other actions to "make room".
342
+
*
343
+
* Honoured by a STABLE sort in `mergeActionsIntoObjects()` (see stack.zod):
344
+
* actions that leave `order` unset are treated as `0` and keep their original
345
+
* registration order, so setting `order` on nobody is a no-op — fully
346
+
* backward compatible. Renderers MAY additionally prefer a `variant:'primary'`
347
+
* action when two actions tie on `order` (see objectui record-header renderer).
348
+
*/
349
+
order: z.number().optional().describe('Sort order within a location group (lower = higher). Promotes/demotes an action toward the record_header primary button; stable, so actions without `order` keep their registration order.'),
350
+
330
351
/** UX Behavior */
331
352
confirmText: I18nLabelSchema.optional().describe('Confirmation message before execution'),
332
353
successMessage: I18nLabelSchema.optional().describe('Success message to show after execution'),
0 commit comments