Skip to content

fix(console): make a paused screen flow completable, and stop the runner from tearing down its host (framework#3528) - #2830

Merged
os-zhuang merged 1 commit into
mainfrom
claude/screen-flow-submit-resume-v9dqjx
Jul 27, 2026
Merged

fix(console): make a paused screen flow completable, and stop the runner from tearing down its host (framework#3528)#2830
os-zhuang merged 1 commit into
mainfrom
claude/screen-flow-submit-resume-v9dqjx

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

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/console 16.1.0 build in Chromium. The record and list surfaces already resume correctly — trigger → screen → POST .../runs/:runId/resume → 200, including the two-step object-form lead-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 a paused row in Recent Runs.

2. 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. Traced live: the runner opened and was unmounted in the same tick.

[DBG] execute result {"success":true,"status":"paused","runId":"run_02a9…"}
[DBG] opening FlowRunner for run run_02a9…
[DBG] FlowTestRunner UNMOUNTED ← dialog gone, run still paused
[DBG] FlowTestRunner MOUNTED

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

  • console — a paused test run opens the same FlowRunner the record and list surfaces use, so the screen renders for real (flat fields, multi-step wizards, object-form steps 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. paused gets its own status badge instead of the unknown-status style.
  • app-shellFlowRunner owns 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.
  • app-shell — 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 via DesignedScreenSpec.
  • app-shellFlowRunner and its types are exported from the package so surfaces outside views/ mount the one runner instead of reimplementing it.

Test plan

  • New 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 no fields renders and resumes, and the resume body carries the collected values.
  • Full affected suites: FlowRunsPage, FlowRunner, ScreenPreview, FlowSimulatorPanel — 26 passed.
  • pnpm --filter @object-ui/app-shell type-check clean; eslint on all changed files: 0 errors.
  • Browser, against a live examples/app-crm server: Flow Runs → Run Flow on crm_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: DashboardRenderer header actions never dispatch flow (they fall through to console.warn("Unknown header actionType")) and have no ActionProvider in their subtree; and the console-root <ActionProvider> in ConsoleShell is mounted with no handlers map, so any action:button outside 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

…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_01LX9ut3MK3KykE11S9bJmv5
@vercel

vercelBot commented Jul 27, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectuiIgnoredIgnoredJul 27, 2026 4:18am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)28.0 KB350 KB
Entry fileindex-CjXvK2Be.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)8.20KB2.97KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)7.57KB2.97KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)21.70KB4.21KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.12KB3.41KB
auth (LoginForm.js)17.86KB5.29KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.43KB2.09KB
auth (SocialSignInButtons.js)9.60KB3.89KB
auth (UserMenu.js)3.40KB1.22KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)33.74KB8.53KB
auth (createAuthenticatedFetch.js)4.37KB1.69KB
auth (index.js)1.83KB0.79KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)4.86KB0.85KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)18.38KB4.49KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)3.65KB1.42KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.25KB0.53KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)450.70KB98.15KB
core (index.js)1.86KB0.63KB
create-plugin (index.js)9.28KB2.98KB
data-objectstack (index.js)127.29KB31.96KB
fields (index.js)214.60KB52.67KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.32KB1.77KB
i18n (index.js)2.46KB0.96KB
i18n (pickLocalized.js)1.70KB0.83KB
i18n (provider.js)5.37KB1.72KB
i18n (useObjectLabel.js)25.17KB5.80KB
i18n (useSafeTranslation.js)2.87KB1.28KB
layout (index.js)38.45KB10.67KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.74KB
mobile (index.js)1.50KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)4.42KB1.27KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.71KB0.42KB
mobile (useResponsiveConfig.js)1.36KB0.63KB
mobile (useSpecGesture.js)1.77KB0.77KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)6.84KB2.42KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)3.67KB1.12KB
permissions (evaluator.js)4.00KB1.23KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.55KB0.71KB
plugin-ai (index.js)15.71KB3.79KB
plugin-calendar (index.js)45.37KB12.48KB
plugin-charts (index.js)46.90KB13.26KB
plugin-chatbot (index.js)179.53KB42.79KB
plugin-dashboard (index.js)108.71KB28.00KB
plugin-designer (index.js)210.92KB42.69KB
plugin-detail (index.js)214.78KB52.42KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)103.47KB25.10KB
plugin-gantt (index.js)162.33KB39.53KB
plugin-grid (index.js)176.51KB46.38KB
plugin-kanban (index.js)47.82KB13.18KB
plugin-list (index.js)98.71KB23.32KB
plugin-map (index.js)16.80KB5.24KB
plugin-markdown (index.js)13.65KB4.67KB
plugin-report (index.js)37.07KB9.81KB
plugin-timeline (index.js)25.37KB7.20KB
plugin-tree (index.js)8.36KB2.81KB
plugin-view (index.js)85.70KB20.87KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.55KB0.67KB
providers (UploadProvider.js)11.71KB3.53KB
providers (index.js)0.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)3.19KB1.38KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)18.70KB6.09KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.00KB0.55KB
sdui-parser (codegen.js)4.09KB1.74KB
sdui-parser (index.js)2.16KB0.94KB
sdui-parser (parse.js)10.04KB2.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)4.69KB1.48KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)0.77KB0.41KB
types (disclosure.js)0.20KB0.18KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (index.js)1.97KB0.93KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)0.20KB0.18KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.04KB1.93KB
types (system-fields.js)2.39KB1.17KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)0.75KB0.46KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-zhuang
os-zhuang marked this pull request as ready for review July 27, 2026 04:23
@os-zhuang
os-zhuang merged commit 072330d into mainJul 27, 2026
14 checks passed
@os-zhuang
os-zhuang deleted the claude/screen-flow-submit-resume-v9dqjx branch July 27, 2026 04:23
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>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-zhuang@zhuangjianguo