Uh oh!
There was an error while loading. Please reload this page.
fix(console): make a paused screen flow completable, and stop the runner from tearing down its host (framework#3528) - #2830
Merged
Conversation
…ner from tearing down its host (framework#3528)
Two defects on the screen-flow path, both ending in "Submit never resumes".
**Flow Runs test runner had no second half.** Developer → Flow Runs triggers a
flow and renders the result. For a screen flow that result is not a result — it
is `{ status: 'paused', runId, screen }`, and the run sits suspended until
something posts to its resume endpoint. The panel dumped the envelope as JSON
and stopped: no screen, no Submit, no resume call, and an orphaned `paused` row
in Recent Runs for every test run. It now hands the pause to the same
`FlowRunner` the record and list surfaces use, so the screen renders for real
(flat fields, multi-step wizards, and `object-form` steps with their
master-detail grids). Dismissing the runner no longer strands the run: the
suspension is durable, so a "Continue run" affordance reopens the pending
screen. `paused` also gets its own status badge instead of falling through to
the unknown-status style.
**FlowRunner tore down its host.** An `object-form` step mounts ObjectForm,
whose field widgets are lazy. That suspension unwound to the *host's* nearest
`<Suspense>`; where that is the route-level boundary, React swapped the whole
page for the fallback and remounted it, destroying the host's state and this
dialog with it. The screen vanished before it could be filled in and the run
stayed paused with no resume call — the reported symptom exactly. The runner
now owns a boundary around its screen body, so the lazy load is local and no
host can be torn down by it. Verified in a browser against a live server: the
two-step Convert Lead wizard now creates the account, resumes, and renders
step 2.
Also: a screen payload without `fields` no longer throws. `fields` is optional
on the wire (a message-only screen, or an executor that omits it) but was read
unguarded as the dialog mounted; reads go through a `screenFields()` helper and
the design-time builder keeps its exhaustive shape. `FlowRunner` and its types
are exported from the package so surfaces outside `views/` can mount the one
runner instead of reimplementing it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LX9ut3MK3KykE11S9bJmv5The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
os-zhuang
marked this pull request as ready for review
July 27, 2026 04:23
Uh oh!
There was an error while loading. Please reload this page.
os-zhuang pushed a commit
that referenced
this pull request
Jul 27, 2026
…reen-flow round trip (framework#3528) Follow-up to #2830, which fixed the resume half. These are the two launch-side holes found while mapping every path that dispatches a `type: 'flow'` action — on both, a screen flow could not even be started. **Dashboard header actions never dispatched a flow.** The click handler allow-listed `modal` / `script`; `flow`, `api`, `form` and `navigation` fell through to `console.warn("Unknown header actionType")` and did nothing at all. Everything that is not a raw `url` navigation now goes through the ActionRunner, which owns the type registry — there is nothing for the renderer to second-guess. **The console-root ActionProvider had no handlers.** It exists to give every field widget a modal handler, but an ActionProvider also decides what a `useAction()` consumer below it can dispatch, so any `action:button` outside ObjectView / RecordDetailView / PageView / DeclaredActionsBar bound to a runner that could only open modals: a flow action there failed with "Flow handler not registered", and api/script were equally dead. The root now carries the shared console runtime's api / flow / script handlers plus its confirm / param / result / screen-flow dialogs. `modal` deliberately stays on the client-side `useActionModal` handler — putting it in `handlers` would take precedence over `onModal` and reroute the inline-create affordance to `/api/v1/actions/...`. Both fixes ship with regression tests that were verified to fail without them. The screen-flow seam itself had no coverage, which is how the host-teardown bug survived to production, so this also adds: - `FlowRunner.suspense.test.tsx` — a lazily-loaded screen body must not unwind past the dialog. Reproduces the real shape (lazy body, route-level boundary above the host, host state that must survive) and fails against the pre-boundary runner. - `e2e/live/screen-flow.spec.ts` — the live round trip: a row flow action triggers the run, the paused screen renders, Submit POSTs to `/automation/{flow}/runs/{runId}/resume` with the collected values, and the flow's downstream `update_record` shows up in the list. Verified passing against a real backend (showcase app + console dev server). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LX9ut3MK3KykE11S9bJmv5
os-zhuang added a commit
that referenced
this pull request
Jul 27, 2026
…reen-flow round trip (framework#3528) (#2833) Follow-up to #2830, which fixed the resume half. These are the two launch-side holes found while mapping every path that dispatches a `type: 'flow'` action — on both, a screen flow could not even be started. **Dashboard header actions never dispatched a flow.** The click handler allow-listed `modal` / `script`; `flow`, `api`, `form` and `navigation` fell through to `console.warn("Unknown header actionType")` and did nothing at all. Everything that is not a raw `url` navigation now goes through the ActionRunner, which owns the type registry — there is nothing for the renderer to second-guess. **The console-root ActionProvider had no handlers.** It exists to give every field widget a modal handler, but an ActionProvider also decides what a `useAction()` consumer below it can dispatch, so any `action:button` outside ObjectView / RecordDetailView / PageView / DeclaredActionsBar bound to a runner that could only open modals: a flow action there failed with "Flow handler not registered", and api/script were equally dead. The root now carries the shared console runtime's api / flow / script handlers plus its confirm / param / result / screen-flow dialogs. `modal` deliberately stays on the client-side `useActionModal` handler — putting it in `handlers` would take precedence over `onModal` and reroute the inline-create affordance to `/api/v1/actions/...`. Both fixes ship with regression tests that were verified to fail without them. The screen-flow seam itself had no coverage, which is how the host-teardown bug survived to production, so this also adds: - `FlowRunner.suspense.test.tsx` — a lazily-loaded screen body must not unwind past the dialog. Reproduces the real shape (lazy body, route-level boundary above the host, host state that must survive) and fails against the pre-boundary runner. - `e2e/live/screen-flow.spec.ts` — the live round trip: a row flow action triggers the run, the paused screen renders, Submit POSTs to `/automation/{flow}/runs/{runId}/resume` with the collected values, and the flow's downstream `update_record` shows up in the list. Verified passing against a real backend (showcase app + console dev server). Claude-Session: https://claude.ai/code/session_01LX9ut3MK3KykE11S9bJmv5 Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Console half of objectstack#3528 ("screen-flow Submit never calls the resume endpoint"). SDK half: objectstack#3552.
What I found
I drove the reported path against a live server using the published
@objectstack/console16.1.0 build in Chromium. The record and list surfaces already resume correctly — trigger → screen →POST .../runs/:runId/resume→ 200, including the two-stepobject-formlead-conversion wizard. But two real defects on the same path both end in "Submit never resumes":1. The Flow Runs test runner has no second half. Developer → Flow Runs triggers a flow and renders the result. For a screen flow that result is not a result — it is
{ status: 'paused', runId, screen }, and the run sits suspended until something posts to its resume endpoint. The panel dumped that envelope as JSON and stopped: no screen, no Submit, no resume call. Every test run of a screen flow orphaned apausedrow in Recent Runs.2.
FlowRunnertore down its host. Anobject-formstep mountsObjectForm, whose field widgets are lazy. That suspension unwound to the host's nearest<Suspense>; where that is the route-level boundary, React swapped the whole page for the fallback and remounted it — destroying the host's state and this dialog with it. Traced live: the runner opened and was unmounted in the same tick.The screen vanished before it could be filled in and no resume was ever issued — the reported symptom exactly, from a cause that has nothing to do with the Submit handler.
Changes
FlowRunnerthe record and list surfaces use, so the screen renders for real (flat fields, multi-step wizards,object-formsteps with their master-detail grids) and Submit posts to/automation/:flow/runs/:runId/resume. Dismissing the runner no longer strands the run: the suspension is durable, so a "Continue run" affordance reopens the pending screen.pausedgets its own status badge instead of the unknown-status style.FlowRunnerowns a<Suspense>boundary around its screen body, so a lazy screen body can never tear down its host. Fixed at the source rather than per-surface.fieldsno longer throws.fieldsis optional on the wire (a message-only screen, or an executor that omits it) but was read unguarded as the dialog mounted; reads go through ascreenFields()helper, and the design-time builder keeps its exhaustive shape viaDesignedScreenSpec.FlowRunnerand its types are exported from the package so surfaces outsideviews/mount the one runner instead of reimplementing it.Test plan
apps/console/src/pages/developer/FlowRunsPage.test.tsx— asserts the pause opens the runner and that Submit posts to the run's resume endpoint, and that "Continue run" reopens a dismissed screen.FlowRunner.test.tsx— two new cases: a screen payload with nofieldsrenders and resumes, and the resume body carries the collected values.FlowRunsPage,FlowRunner,ScreenPreview,FlowSimulatorPanel— 26 passed.pnpm --filter @object-ui/app-shell type-checkclean; eslint on all changed files: 0 errors.examples/app-crmserver: Flow Runs → Run Flow oncrm_convert_lead_wizard→ step 1 form renders → Save & Continue creates the account → resume 200 → step 2 renders with the account prefilled.Not in this PR
Two adjacent launch-side gaps found while mapping every path that dispatches a
type: 'flow'action:DashboardRendererheader actions never dispatchflow(they fall through toconsole.warn("Unknown header actionType")) and have noActionProviderin their subtree; and the console-root<ActionProvider>inConsoleShellis mounted with nohandlersmap, so anyaction:buttonoutside ObjectView / RecordDetailView / PageView / DeclaredActionsBar hits a runner with no flow handler. Both deserve their own change.🤖 Generated with Claude Code
https://claude.ai/code/session_01LX9ut3MK3KykE11S9bJmv5
Generated by Claude Code