Repository navigation
Commit dc71845
feat(spec,console): a
<!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} -->
_Requested by **Steedos** · [project
thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9UbbK3S53FNy4JF9JjaiJuZu7X)_
Fixes #25800
Clause-②: yes
Metadata-protocol: added
## Summary
A `type: 'flow'` action that creates a record could not take the user to
it: `ActionSchema` refused `onSuccess` beside `type: 'flow'`, and the
page action rows refused it beside a `flow` executor. This PR admits
`flow` into the one executor set both read (`ON_SUCCESS_EXECUTORS` in
`packages/spec/src/ui/action-on-success-executor.ts`), rewords the
refusal prose for `url`, `navigation`, `modal` and `form` to name the
three executors, opens the Studio action form's `onSuccess` row to a
flow action, and regenerates the reference docs.
On the console side, the post-success hop fires after a flow run that
**completes** and never after one that pauses at a screen or ends
refused. On a completed run, `${result.*}` reads the run's answer
(`AutomationResult`), so `${result.output.VAR}` resolves to one of the
flow's `isOutput` variables:
```ts
{
name: 'start_renewal',
type: 'flow',
target: 'renewal_start',
onSuccess: { navigate: '/apps/clm/clm_contract/record/${result.output.renewal.id}' },
}
// the flow declares variables: [{ name: 'renewal', type: 'object', isOutput: true }]
// and its create_record node writes outputVariable: 'renewal'
```
## Mechanism-assumption correction: why `silent` cannot mark a hand-off
The dispatch assumed the ActionRunner needed no change: the launch
judge's `paused` and `refused` arms already answer `silent: true`, so it
looked as if the hop could be keyed off that. Measured, it cannot.
`silent` is a fact about the **toast** alone, and the `done` arm sets it
too. When the flow's own `successMessage` wins the toast
(objectui#12274), `judgeFlowLaunch` returns `{ success: true, data,
reload, silent: true }` for a run that **completed**. A runner that
skipped the hop on `silent` would therefore drop the hop for every
completed flow that authors its own success copy, which is exactly the
record-creating flow this card is about. The three success arms are
indistinguishable on `success` and `silent`, so the runner needs a fact
of its own.
That fact is the new `ActionResult.handedOff`
(`ui/packages/types/src/ui-action.ts`): the success is a hand-off to a
follow-up surface, not a completion. `judgeFlowLaunch` sets it on the
`paused` and `refused` arms, and `ActionRunner.handlePostExecution`
gates the hop on `result.success && !result.handedOff &&
action.onSuccess`. The seat approved the file-surface amendment (types,
core, app-shell) before the console half was written.
## Each launch arm and its hop
`judgeFlowLaunch` in `ui/packages/app-shell/src/utils/flowResponse.ts`
now states the hop conclusion for every arm in its docblock, and each
conclusion is pinned by a test that runs the real `ActionRunner`:
| Arm | Result | Hop | Why |
|---|---|---|---|
| `failed` | `success: false` | none | The runner hops only after a
success. |
| `paused` | `success: true`, `silent`, `handedOff` | none, now or later
| The action only opened the wizard. Navigating away would leave the
screen behind, and the run has not answered with the output the hop
reads. `FlowRunner` finishing the run later does not hop either. |
| `refused` | `success: true`, `silent`, `handedOff` | none | The
Close-only notice is the follow-up, and a hop would navigate past it to
a record the refusal never produced. |
| `done` | `success: true`, `data` = `AutomationResult` (`silent` when
the flow's copy wins) | hops | The run completed. `${result.*}` reads
`data`; `${result.output.VAR}` is an `isOutput` variable. Not
`handedOff`, whether `silent` or not. |
## Landing point and surface delta
The claim named the spec half (`action-on-success-executor.ts`,
`action.zod.ts`, `component.zod.ts`, `action.form.ts`, their tests, the
liveness ledger, the reference docs) and the console tests. The
correction above added three source files to the surface, approved by
the seat: `ui/packages/types/src/ui-action.ts` (`handedOff`),
`ui/packages/core/src/actions/ActionRunner.ts` (the gate) and
`ui/packages/app-shell/src/utils/flowResponse.ts` (the arms). Following
from those:
- `scripts/sdui-manifest.record.json` is re-recorded because the
registry sources moved; `sdui.manifest.json` itself is byte-identical
(106 components). No conflict with #25903 arose, so main was not merged.
- `packages/lint/src/validate-predicate-path-refs.test.ts` recounts the
predicate census (63) because the form gate is now spelled `data.type in
['api','script','flow']`.
- Eight console view tests (`useConsoleActionRuntime.*`,
`RecordDetailView.*`) that pin the paused or refused result with
`toEqual` now include `handedOff: true`.
## Premise check: what `${result.output.VAR}` reads
The card reported a run answering `output: {}`. The runtime is not
dropping anything: `AutomationResult.output` is built from the flow's
`isOutput` variables only
(`packages/services/service-automation/src/engine.ts`, the "Collect
output variables" loop at about line 5643), and both the trigger route
and the action door (`dispatchFlowAction`,
`packages/runtime/src/action-execution.ts`) serve that answer. A
`create_record` node writes the record object into its `outputVariable`,
but the variable reaches `output` only when the flow declares it
`isOutput: true`. The empty `output` is a flow-authoring gap in the
reporting app: declare the variable `isOutput: true` and point
`outputVariable` at it. The spec docblock and the changeset now say so,
and a test pins that an empty `output` resolves the template to
`/apps/clm/clm_contract/record/` rather than throwing.
## Tests
Measured on this branch. The spec and ui core/types suites ran at
`344ac6f5`, which differs from the head only in
`scripts/sdui-manifest.record.json`; the app-shell suite and the
type-checks ran at the head, `9cb24ac6`.
- `pnpm --filter @objectstack/spec test`: Test Files 722 passed (722),
Tests 21877 passed, 1 todo.
- `ui/` vitest over `packages/core` and `packages/types`: Test Files 601
passed (601), Tests 14680 passed, 27 skipped.
- `ui/` vitest over `packages/app-shell`: Test Files 1721 passed, 1
skipped (1722); Tests 15875 passed, 9 skipped.
- `pnpm run type-check` in `ui/packages/types`, `ui/packages/core` and
`ui/packages/app-shell` (after building the app-shell dependency
closure): exit 0, 0 errors each.
- New pins: `ActionRunner.onSuccessNavigation.test.ts` (reads
`${result.output.renewal.id}` to `/apps/clm/clm_contract/record/ctr_18`;
a `silent` completed run still hops; a `handedOff` success does not,
with a positive control on the same runner) and a `flowResponse.test.ts`
block composing `interpretFlowResponse`, `judgeFlowLaunch` and the real
`ActionRunner` for all four arms plus the empty-`output` case.
- Spec: `action-on-success.test.ts` and
`component-action-row-on-success-executor.test.ts` move `flow` from
refused to accepted and parse the renewal template.
## Ablation
`scripts/ablation-replace.mjs` replaced the runner gate `if
(result.success && !result.handedOff && action.onSuccess) {` with `if
(result.success && action.onSuccess) {` (anchor 1 to 0, blob
`3aebb3cab4ea` to `ac1fc1dff4a2`) and ran the two new test files: Test
Files 2 failed (2), Tests 3 failed, 70 passed. The three reds are
exactly the hand-off pins: the core "handed-off success performs no hop"
test, and the paused and refused composition tests. The file was
restored to its HEAD blob `3aebb3cab4ea` with `git diff HEAD` empty.
This was a one-time proof; no permanent test file was added for it.
## Gates
Derived with `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` at `9cb24ac6` (125 commands), each run with
its exit code captured before any pipe, then reconciled with `--ran`:
"125 derived famil(ies) accounted for — 123 run, 2 NOT-MEASURED". The
123 that ran exited 0, among them `check:generated`,
`check:api-surface`, `check:authorable-surface`, `check:docs`,
`check:liveness`, `check:exported-any`, `check-sdui-manifest`,
`check:type-check-debt` (re-measured under the verify lock: "26 raw tsc
error(s) total, none above its recorded number") and `check:nul-bytes`.
Not measured locally, left to CI: `check:dual-build-cjs-loads` (needs a
full workspace `pnpm build`) and `check:i18n` (needs the CLI's build
closure). Both refused on their prerequisite with exit 3 before reading
anything; this diff adds no i18n key and changes no package's build
shape.
Two environment notes. `check-plugin-teardown-shape --self-test` reads a
fixture pinned to commit `621a4876`, which this shallow clone lacked;
after fetching that one commit it exits 0. And an earlier sweep ran
unlocked while the console manifest regeneration rebuilt
`packages/spec/dist`, which produced spurious `check:generated` and
`check:exported-any` reds; that sweep was discarded and the numbers
above come from the clean rerun.
## Acceptance notes
- Two pre-existing runner behaviours still apply to a handed-off
success, unchanged by this PR: an action authored with `refreshAfter:
true` sets `result.reload`, and an authored `chain` runs. Neither is
new, and neither is the hop this card asked about. Noted, not filed.
- A flow action whose target flow starts at a screen will always pause,
so its `onSuccess` can never fire. A publish-time hint for that pairing
would catch the mistake earlier; noted, not filed.
---
_Generated by [Claude
Code](https://claude.ai/code/session_013w86pS2SMm6BU2PfGQ3vJ7)_
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_013w86pS2SMm6BU2PfGQ3vJ7
---------
Co-authored-by: Claude <noreply@anthropic.com>type: 'flow' action may declare onSuccess, and the console navigates after a run that completes (#26011)1 parent 4f7e041 commit dc71845
31 files changed
Lines changed: 393 additions & 122 deletions
File tree
- .changeset
- content/docs/references
- data
- kernel
- ui
- packages
- lint/src
- spec
- liveness
- src/ui
- scripts/sdui-manifest.sources
- ui/packages
- app-shell/src
- hooks/__tests__
- utils
- __tests__
- views
- __tests__
- core/src/actions
- __tests__
- types/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
522 | 522 | | |
523 | 523 | | |
524 | 524 | | |
525 | | - | |
| 525 | + | |
526 | 526 | | |
527 | 527 | | |
528 | 528 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
355 | 355 | | |
356 | 356 | | |
357 | 357 | | |
358 | | - | |
| 358 | + | |
359 | 359 | | |
360 | 360 | | |
361 | 361 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
69 | 69 | | |
70 | 70 | | |
71 | 71 | | |
72 | | - | |
| 72 | + | |
73 | 73 | | |
74 | 74 | | |
75 | 75 | | |
| |||
178 | 178 | | |
179 | 179 | | |
180 | 180 | | |
181 | | - | |
| 181 | + | |
182 | 182 | | |
183 | 183 | | |
184 | 184 | | |
| |||
0 commit comments