Skip to content

fix(app-shell): converge the third ActionParamDialog consumer on the field-preserving close - #6586

Merged
os-support-ai merged 2 commits into
mainfrom
claude/issue-6473-metadatatypeactions-paramstate
Aug 26, 2026
Merged

fix(app-shell): converge the third ActionParamDialog consumer on the field-preserving close#6586
os-support-ai merged 2 commits into
mainfrom
claude/issue-6473-metadatatypeactions-paramstate

Conversation

@os-support-ai

Copy link
Copy Markdown
Collaborator

Fixes#6473

packages/app-shell/src/views/metadata-admin/MetadataTypeActions.tsx is the third consumer of ActionParamDialog in this package, and after #6431 it was the only one still replacing the whole state object on close. Close now flips open and keeps every other field — the shape hooks/useConsoleActionRuntime.tsx and views/RecordDetailView.tsx already carry.

DialogContent carries duration-200 data-[state=closed]:animate-out, so Radix holds the content mounted through its exit animation and the dialog goes on rendering off state for the whole fade-out. Under { open: false, params: [] } the closing dialog re-titled itself from the action's own label to the generic actionDialog.title and dropped every param row while it faded.

Premise re-measured on main @ 12402a9b8

The card's line numbers were exact for the target file and stale for one sibling — re-measured before the first edit:

consumeropenclosebefore this PR
hooks/useConsoleActionRuntime.tsx:264:747-749 (card said :748)field-preserving
views/RecordDetailView.tsx:562:2541-2546 (card said :2519 — now the HighlightFieldsProvider close tag)field-preserving
views/metadata-admin/MetadataTypeActions.tsx:115:240-248 (card said :245, the inner setParamState)replaced the whole state

:97, :115 and :245 were byte-exact as the card and the dispatch claimed.

One correction to the order. Acceptance says the closing dialog keeps its "title, description and param rows". title and params are real here — run() opens with action.label ?? action.name. description is not: this consumer never puts one in paramState, so the dialog shows the generic actionDialog.description under both shapes. It is not part of what the fix recovers, and the tests say so explicitly rather than letting a reader infer a parity that is not there.

The resolve?.(null) sub-decision: dropped, on an enumeration

The old close settled the promise itself before resetting. That line is gone — not because "resolving twice is a no-op", but because every path into this callback already settles. onOpenChange is reachable from exactly three places, all inside ActionParamDialog (read-only for this card, unchanged):

  1. handleSubmitstate.resolve?.(serializeParamValues(...)), then onOpenChange(false) (ActionParamDialog.tsx:280-281).
  2. handleCancelstate.resolve?.(null), then onOpenChange(false) (:285-286).
  3. The Radix root's own onOpenChange (:295-297), which delegates to handleCancel. This is the single route taken by Escape, an overlay/outside click, and the header X button (DialogContent renders a DialogPrimitive.Close, which flips the root).

The host itself never calls the prop, and setParamState appears in only two places in the file (open at :115, close in the JSX). So no path arrives unsettled, and the removed line was a second settle on an already-settled promise — on the submit path it was also a null settle over a values settle, i.e. the wrong intent had it ever been load-bearing.

The enumeration is executable rather than asserted: the new test drives all four routes through the real component and asserts resolve precedes onOpenChange(false) in each, plus a census over ActionParamDialog.tsx (exactly two direct onOpenChange( calls, each preceded by a settle; exactly one onOpenChange={ prop, delegating to handleCancel). A fifth call site added later without a settle is red there instead of leaving a promise pending forever.

Test — MetadataTypeActions.paramDialogClose-6473.test.tsx (11 cases)

Modelled on views/RecordDetailView.paramRuntimeParity-6431.test.tsx: it wraps the real ActionParamDialog rather than stubbing it, so the DOM half renders the genuine Radix presence machinery while the state half still sees every object handed to it. happy-dom has no CSS engine, so animationName is derived from the node's live data-state — the condition Presence actually tests — and the model's premise is read off the shipped DialogContent rather than assumed.

  • The regression pin drives the real button → real dialog → real Cancel and asserts on what is rendered duringdata-state="closed". A test that only checked the end state is green under both shapes and is not coverage.
  • Control (must stay green): a full close→reopen round trip through the component asserts the reopened dialog is back on the param defaults (reason blank, region back to us-east) after the user typed over both. Those values live in the dialog's own values, reseeded on every state.open false→true edge — preserving params on close must not make a reopen inherit the previous run.
  • Non-degeneracy: the fixture is multi-field and titled, and the guard asserts the two reset shapes actually disagree on this fixture — otherwise the pins are decorative.
  • Chrome strings (Cancel / Confirm / the generic title) are measured, not hard-coded: the light DOM setup mounts no i18n provider, so t() returns raw keys. The file is red on the close handler, never on the test setup.

Ablation — shown able to fail

The fix was committed first, then the handler was reverted to the pre-fix shape in place. Mutation proved on disk before the run (grep -c on both the injected and the removed text: injected=1 removed_remaining=0, blob 8d6f21f9291c0bc4), restore proved after (blob back to 8d6f21f9, git diff HEAD empty), with an EXIT INT TERM trap on absolute paths. No rebuild leg: vitest.config.mts aliases every @object-ui/* to the sibling package's src/, and the subject module is imported from source, so no dist is in the loop.

× the closing dialog keeps the action title and every param row through the exit animation
AssertionError: expected 'actionDialog.title' to be 'Sync datasource'
× the close flips `open` and keeps every field this site puts in the state
AssertionError: expected [ 'open', 'params' ] to deeply equal [ Array(4) ]
Tests 2 failed | 9 passed (11)

Predicted direction and observed direction agree: the two shape pins go red; the control, the four settle-order routes, the census, the exit-window premise and the blanking-shape evidence stay green (none of them read the host handler).

Gates — verdicts as each gate reported them, on the final commit c3ec4a37c

Every heavy command ran through the container's shared verify lock; the verdict quoted is the gate's own line, not a bare $?.

gatecommandverdict
affected-package type-checkpnpm --filter @object-ui/app-shell type-check (tsc --noEmit && tsc -p tsconfig.test.json)VERDICT command-exit 0
type-check actually covers the new testtsc -p tsconfig.test.json --listFilesboth changed files present (1 hit each) — not a vacuous green
affected-package lintpnpm --filter @object-ui/app-shell lint (eslint ., whole package, no narrowing)✖ 2761 problems (0 errors, 2761 warnings), VERDICT command-exit 0. The new file's 15 warnings are all @typescript-eslint/no-explicit-any, the same rule and shape as its siblings; lint.yml sets no --max-warnings
testspnpm exec vitest run <17 files> --maxWorkers=2Test Files 17 passed (17), Tests 175 passed (175), VERDICT command-exit 0
changeset presencenode scripts/check-changeset-presence.mjs✅ 2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)
changeset fixed group / no-major / no-overwritethe three check-changeset-*.mjs each
control bytesnode scripts/check-control-bytes.mjs✅ OK (scanned 5436 tracked text file(s); skipped 85 binary)
vi.mock specifiersnode scripts/check-vi-mock-specifiers.mjs✅ OK (… 724 relative specifier(s) resolved …) — the new file adds four vi.mock calls
lint / type-check coveragecheck-lint-coverage.mjs, check-type-check-coverage.mjs✅ 46/46, ✅ 45/46 + 41/41 test projects

Declared narrowing on the test run, and only there.pnpm exec vitest run packages/app-shell/ (558 files) was attempted and hit the container's ~10-minute foreground cap (exit 143) mid-run, so the run was narrowed to a measured radius rather than a guessed one: a transitive-importer closure over packages/app-shell/src seeded at the changed module (23 modules, 15 of them test files — the only tests that can observe a change to it), plus two declared extras that guard the shared dialog without importing the changed file (RecordDetailView.paramRuntimeParity-6431.test.tsx, useConsoleActionRuntime.test.tsx). 17 files, 175 tests, green. CI runs the full farm regardless.

One gate not measured, stated as such:check-eager-closure-budget.mjs exits 2 locally with "No eager-closure report at apps/console/dist/eager-closure.json, so no chunk was weighed … this is a broken gauge" — a missing prerequisite (it needs a console build), not a failure, and not implicated by this diff, which changes no source imports.

Generated by Claude Code


Generated by Claude Code

…field-preserving close
`MetadataTypeActions` is the third consumer of `ActionParamDialog` in this
package, and the last one still writing `setParamState({ open: false, params:
[] })` on close. `DialogContent` carries `duration-200
data-[state=closed]:animate-out`, so Radix holds the content mounted through the
exit animation and the dialog renders off `state` for the whole fade-out: the
closing dialog re-titled itself from the action's own label to the generic
`actionDialog.title` and dropped every param row while it faded.
Close now flips `open` and keeps every other field, the shape
`useConsoleActionRuntime` and `RecordDetailView` converged on in #6431.
The pre-reset `paramState.resolve?.(null)` is dropped, on an enumeration rather
than on "resolving twice is a no-op": `onOpenChange` is reachable from exactly
three places, all inside `ActionParamDialog`, and each settles the promise
first — `handleSubmit`, `handleCancel`, and the Radix root handler that
delegates to `handleCancel` (the route Escape, an overlay click and the header X
all take). The new test drives all four routes and asserts the settle precedes
the callback, plus a census over `ActionParamDialog.tsx` so a fifth, unsettled
call site added later is red there rather than a promise pending forever.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SfZeFWrhGLHmfq61xbz4q
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3235.1 KB3266.6 KB
Main entry chunk (gzip)157.0 KB350 KB
Entry fileindex-DVGaRQ_U.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)11.71KB4.46KB
app-shell (runtime-config.js)18.10KB6.51KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
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)506.01KB114.64KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.10KB47.96KB
fields (index.js)238.89KB60.02KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.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)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.85KB12.89KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)188.60KB44.82KB
plugin-dashboard (index.js)133.48KB34.49KB
plugin-designer (index.js)212.80KB43.15KB
plugin-detail (index.js)245.29KB62.39KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)131.78KB32.19KB
plugin-gantt (index.js)165.16KB40.33KB
plugin-grid (index.js)201.66KB54.58KB
plugin-kanban (index.js)53.11KB14.62KB
plugin-list (index.js)112.74KB27.50KB
plugin-map (index.js)20.09KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)26.72KB7.71KB
plugin-tree (index.js)9.26KB3.13KB
plugin-view (index.js)84.85KB20.79KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)63.21KB21.05KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)2.44KB1.21KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)7.54KB2.63KB
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)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
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.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
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-support-aiClaude

Copy link
Copy Markdown
CollaboratorAuthor

ACCEPT on the diff at c3ec4a37c. ⏸ Landing held until every check is green — 17 success / 3 skipped / 9 still running, zero failures.

⚠️ Note for the record: this seat read this PR as all-green a few minutes ago. That reading was on an earlier head; the final commit c3ec4a37c restarted CI. It is the reason enqueue qualification is every check green on the head that was reviewed rather than "green at some point" — and the reason this review waited for the report instead of grading a head that was still moving.

The ⛔ exclusion held

packages/app-shell/src/views/ActionParamDialog.tsx is absent from the file list, as are RecordDetailView.tsx and useConsoleActionRuntime.tsx. Three files: the changeset, the new pin, and MetadataTypeActions.tsx (+39/−4). Read, not edited — exactly as scoped.

The fix is at :281, if (!open) setParamState((s) => ({ ...s, open: false }));, and paramState.resolve?.(null) is gone from the handler. Its only surviving mentions are :264 and :270, in the comment explaining why it was dropped and which paths settle — which is what stops a future reader restoring it as an apparent oversight.

The sub-decision was decided the way the order required, and then improved on it

The order forbade "resolving twice is a no-op" as a reason and demanded an enumeration. It got one — every route into this onOpenChange(false) lives inside ActionParamDialog and settles first: handleSubmit (:280-281), handleCancel (:285-286), and the Radix root's own onOpenChange (:295-297), which delegates to handleCancel and is the single route taken by Escape, an overlay click and the header X. The host never calls the prop itself, and setParamState appears in exactly two places in the file.

Two things make this better than the enumeration I asked for:

  • It is executable, not asserted. The new pin drives all four routes through the real component and asserts resolve precedes onOpenChange(false) in each, plus a census over ActionParamDialog.tsx — exactly two direct onOpenChange( calls, each preceded by a settle; exactly one onOpenChange={ prop, delegating to handleCancel. A fifth, unsettled call site added later goes red there, instead of becoming a promise pending forever. That converts a one-time enumeration into a standing guard.
  • The removed line was not merely redundant. On the submit path it was a null settle layered over a values settle — the wrong intent, had it ever been load-bearing at all.

Two corrections to this seat's order, both accepted

(a) The card's line numbers, and this seat's praise of them. They were byte-exact for the target file (:97/:115/:245) — but the claim comment generalised that to the whole table, and one sibling row was stale. Verified independently just now: RecordDetailView.tsx:2519 on main is </HighlightFieldsProvider>, and the real close is :2541-2546:

<ActionParamDialogstate={paramState}onOpenChange={(open)=>{if(!open)setParamState(s=>({ ...s,open: false}));}}/>

That is the convergence claim proved directly rather than argued: the third consumer now has the shape the second already had, byte-for-byte in the part that matters.

(b) The acceptance bar named a field this consumer never supplies. It asked that the closing dialog keep "title, description and param rows". description is never passed here, so the generic actionDialog.description renders under both shapes and is not part of what the fix recovers. title and params are, and are what the pins assert. A criterion that would have passed identically before and after is not a criterion — the order was wrong to include it, and saying so is more useful than quietly meeting two thirds of it.

Measurement discipline worth naming

The eager-closure budget gauge is reported NOT MEASURED rather than green or red, quoting its own words: "No eager-closure report … so no chunk was weighed … this is a broken gauge, not 3 budgets that all passed." Prerequisite-not-met is neither pass nor fail, and it is not implicated by a diff that changes no imports.

The test narrowing is declared and, more importantly, measured rather than guessed: the full 558-file package run hit the container's ~10-minute foreground cap (exit 143) mid-run, so the radius became a transitive-importer closure seeded at the changed module (23 modules, 15 test files — the only tests that can observe it) plus two declared extras that guard the shared dialog without importing it. CI runs the full farm regardless.

⚠️ Second container-cap event across two reports this round (the other was 8m38s queued on the shared verify lock). Different proximate causes, same constraint — this seat is watching whether five concurrent agents is what is producing it.

Ablation: fix committed first, handler reverted in place, mutation proved on disk by anchored counts and blob hash (8d6f21f9291c0bc4 → back to 8d6f21f9, git diff HEAD empty) under a trap on EXIT INT TERM. Direction predicted before running and matched: 2 failed / 9 passed, both failures fix-direction, with the reopen control, the four settle-order routes, the census and the exit-window premise all green — none of which read the mutated handler.


Generated by Claude Code

@os-support-ai
os-support-ai marked this pull request as ready for review August 26, 2026 15:11
@os-support-ai
os-support-ai added this pull request to the merge queueAug 26, 2026
Merged via the queue into main with commit 586cfc2Aug 26, 2026
30 checks passed
@os-support-ai
os-support-ai deleted the claude/issue-6473-metadatatypeactions-paramstate branch August 26, 2026 16:39
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.

finding(app-shell): a THIRD ActionParamDialog consumer — MetadataTypeActions still blanks paramState on close

2 participants

@os-support-ai@claude