Skip to content

refactor(app-shell): type RecordDetailView's confirm handler as the published ConfirmationHandler - #6035

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-5835-recorddetailview-confirm-handler-type
Aug 24, 2026
Merged

refactor(app-shell): type RecordDetailView's confirm handler as the published ConfirmationHandler#6035
yinlianghui merged 1 commit into
mainfrom
claude/issue-5835-recorddetailview-confirm-handler-type

Conversation

@yinlianghui

@yinlianghuiyinlianghui commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Fixes#5835

RecordDetailView runs its own confirm runtime rather than consuming useConsoleActionRuntime, and declared its handler with an inline re-spelling of the published type's shape. This adopts ConfirmationHandler, and pins that both runtimes hand ActionConfirmDialog the same field set.

Gates below were run on the branch head 004b68ed7.

A note on spelling in this body: this repo's body sanitizer eats short angle-bracket fragments, and it ate every generic in the first version of this description — turning useCallback + its type argument into a bare useCallback and Promise + its type argument into a bare Promise, which made the diff snippet read as something I did not write. Generics below are therefore spelled with spaces inside the brackets, e.g. useCallback< ConfirmationHandler >. That is still valid TypeScript; the committed source has no such spaces.

What changed

1. The published type replaces the inline shapepackages/app-shell/src/views/RecordDetailView.tsx:

- const confirmHandler = useCallback((message: string, options?: { title?: string; confirmText?: string; cancelText?: string }) => {
+ const confirmHandler = useCallback< ConfirmationHandler >((message, options) => {

2. A parity pinRecordDetailView.confirmRuntimeParity-5835.test.tsx, 4 tests.

3. A changeset with empty frontmatter — this releases nothing, measured rather than asserted (below).

packages/app-shell/src/hooks/useConsoleActionRuntime.tsx is untouched. packages/app-shell/src/index.ts is untouched — this card needed no export change. Nothing under packages/app-shell/src/views/metadata-admin/ was read or written.

"Structurally identical" — verified, not inherited

Both shapes, side by side on the merge-base 53dc89db8:

published ConfirmationHandlerthe inline shape it replaced
param 1message: stringmessage: string
param 2options?: { title?: string; confirmText?: string; cancelText?: string }options?: { title?: string; confirmText?: string; cancelText?: string }
returnPromise< boolean >Promise< boolean > (inferred from the new Promise< boolean > the body constructs)

Member for member, optionality for optionality, return type for return type: identical. The published type is not wider and not narrower, so adopting it is a structural no-op and there is no accept/reject behaviour to describe. Nothing here is a divergence to stop and report on.

The change is enforced at the type boundary only, and that boundary is silent

Reverse verification, direction predicted before running: restore the inline shape, expect type-check to stay GREEN, because the two shapes are structurally identical.

Observed: GREENREV_TYPECHECK_EXIT=0. Stating that plainly, as the honest reading: the parity pin carries the entire enforceable change. Adopting the published type buys future coupling — the compiler now keeps this declaration in step with ConfirmationHandler instead of leaving a near-duplicate free to drift — but it moves no gate today.

Under erasure a type-only change also cannot move a runtime test, so no runtime ablation was manufactured for it. Instead the claim was measured: transpiling RecordDetailView.tsx before and after with the workspace esbuild gives byte-identical output, 64527 bytes both sides. That is also what makes the empty-frontmatter changeset the correct declaration rather than a convenient one.

Mutation proved on disk both ways: the injected text (the inline message: string, options?: spelling) grepped present at 1, the removed text (the useCallback< ConfirmationHandler > spelling) grepped absent at 0. Restored under trap ... EXIT INT TERM; the restored file is byte-identical to the pre-probe copy (diff -q clean).

Deliverable 2: the pin asserts what ARRIVES at the dialog

ActionConfirmDialog is doubled once, for both importers — the hook imports ../views/ActionConfirmDialog.js, the view imports ./ActionConfirmDialog.js, one module, one double. Each runtime is then driven for real and the assertions run against the state object the dialog actually received. No test compares the two declarations to each other.

Both arities are pinned, because the parameter is live on only one path:

  • one argument — the runner's arity, verified in the runner's own source (packages/core/src/actions/ActionRunner.ts: await this.confirmHandler(this.evaluator.evaluate(action.confirmText) as string) — a single argument, confirming the card). This is the only way RecordDetailView's handler is ever reached: it goes to ActionProvider as onConfirm and nowhere else.
  • two arguments — the out-of-runner producer, the shape handleDeleteView in ObjectView.tsx uses.

Both runtimes supply the same field set on both arities: message, open, options, resolve.

Counter-probe: the pin can go red

Two perturbations of one runtime's field set (the RecordDetailView side; the hook was left untouched), each restored under trap:

probeperturbationpredictedobserved
Adrop options from the state literal3 red, 1 green3 failed, 1 passedexpected [ 'message', 'open', 'resolve' ] to deeply equal [ 'message', 'open', 'options', ...(1) ]
Binject an extra field into the state literal3 red, 1 green3 failed, 1 passedexpected [ Array(5) ] to deeply equal [ 'message', 'open', 'options', ...(1) ]

The single survivor in both probes is the console-runtime test — the untouched side — which is the discriminating shape rather than a blanket failure.

Note that probe A leaves type-checkgreen: ConfirmDialogState.options is optional, so a runtime that silently stops forwarding the bag is invisible to the compiler. That is precisely the gap the pin fills.

The vacuous pass is closed explicitly: the expected field set is written out as a literal tied to what ActionConfirmDialog.tsx genuinely reads, so "both sides supply nothing" fails rather than passes.

Mutation proved on disk for every leg, injected text and removed text grepped separately; git diff HEAD --stat carries no probe residue afterwards, and the injectedDrift5835 marker greps absent.

Gates, by name with exit codes — all at 004b68ed7

gateresult
pnpm --filter @object-ui/app-shell type-check (script name echoed: @object-ui/app-shell@17.6.0 type-check)TYPECHECK_EXIT=0
pnpm exec vitest run packages/app-shell/src/views/RecordDetailView.confirmRuntimeParity-5835.test.tsxVITEST_EXIT=0 — Test Files 1 passed (1), Tests 4 passed (4)
pnpm exec eslint src/views/RecordDetailView.tsx src/views/RecordDetailView.confirmRuntimeParity-5835.test.tsx (plain eslint, no --no-inline-config)ESLINT_EXIT=0, warnings only, all pre-existing in kind
node scripts/check-changeset-presence.mjsexit 0 — empty frontmatter accepted as the explicit exemption

Merge-base delta stated: 53dc89db8, two files changed plus the changeset. app-shell was run path-filtered — the whole-package suite was not run.

Out of scope, reported not fixed

The typing alignment surfaced no divergence in the handlers. Measuring them did surface one next door, in the dialog's close path, which I filed rather than touched — the two runtimes reset confirmState differently on close. Filed as #6034 (finding, unassigned): the hook replaces the whole state and blanks message, the view spreads and flips one flag. Same drift family, out of this card's fence, and neither side is obviously the correct one.


Generated by Claude Code

…ublished ConfirmationHandler
`RecordDetailView` runs its own confirm runtime rather than consuming
`useConsoleActionRuntime`, and declared its handler with an inline
re-spelling of the published type's shape. Adopt `ConfirmationHandler`
instead, and pin that both runtimes hand `ActionConfirmDialog` the same
field set.
The inline shape and the published type are member-for-member and
return-type identical, so the annotation is a structural no-op: the
emitted JS is byte-identical. What the swap buys is that the compiler now
keeps this declaration in step with the published type instead of leaving
a near-duplicate to drift.
`options` stays inert on this path and that is not a reason to narrow it:
the runner calls a confirm handler with one argument, while
`handleDeleteView` in `ObjectView.tsx` calls a `ConfirmationHandler`
directly with all three fields localized.
The parity pin asserts the state ARRIVING at `ActionConfirmDialog` from
each runtime, through one shared double, rather than comparing the two
declarations to each other.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSoz9uGhaaSgiq3hshtN7L
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3219.5 KB3990.2 KB
Main entry chunk (gzip)153.6 KB350 KB
Entry fileindex-B1Sfqp8R.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)10.38KB3.90KB
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)505.23KB114.56KB
core (index.js)4.92KB1.97KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)165.30KB45.79KB
fields (index.js)238.40KB59.89KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)23.13KB7.63KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)7.77KB3.13KB
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.62KB12.83KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)188.21KB44.67KB
plugin-dashboard (index.js)133.35KB34.44KB
plugin-designer (index.js)212.30KB42.80KB
plugin-detail (index.js)244.12KB61.87KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)125.63KB30.64KB
plugin-gantt (index.js)164.15KB39.88KB
plugin-grid (index.js)200.79KB54.26KB
plugin-kanban (index.js)52.93KB14.60KB
plugin-list (index.js)111.86KB27.22KB
plugin-map (index.js)20.11KB6.64KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.49KB11.93KB
plugin-timeline (index.js)26.49KB7.59KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.57KB20.74KB
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)52.40KB17.45KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.35KB0.70KB
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)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.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.49KB2.14KB
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

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] RecordDetailView carries a second confirm runtime whose options bag no feeder ever fills

2 participants

@yinlianghui@claude