Skip to content

retire(core): drop the legacy params.newTab fallback on url actions (#4097) - #4262

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-4097-retire-params-reads
Aug 11, 2026
Merged

retire(core): drop the legacy params.newTab fallback on url actions (#4097)#4262
yinlianghui merged 1 commit into
mainfrom
claude/issue-4097-retire-params-reads

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Part of #4097

Executes the consumer half of the objectstack#6828 maintainer ruling (2026-08-10) — but only one of the card's two removals. The card's own binding measurement clause fired: the other half is not dead, and its retirement needs a decision. Details below.

What this PR removes

ActionRunner.navigateTo read a legacy params.newTab escape hatch, below openIn and above the external-URL heuristic:

constnewTab=openIn==='new-tab' ? true
: openIn==='self' ? false
: (source.newTab??action.params?.newTab??isExternal);// ← the retired read

That read is gone. openIn: 'self' | 'new-tab', the legacy navigate.newTab modifier on the navigation shape, and the external/relative default are all unchanged.

Nothing that ever validated can regress.params is declared z.array(ActionParamSchema), so an object-form params has always failed the props parse — the fallback could only fire on a stack the spec refuses. Removing it also closes a collision hazard: a params dialog declaring a field named newTab had the user's own collected input silently steering navigation.

The internal-producer sweep (the card's binding measurement clause)

The card asked whether any objectui-INTERNAL caller synthesizes a non-array params — the corpus grep behind the ruling covered authored metadata only. It does. Eight sites, in four packages:

#SiteShape it synthesizes
1packages/core/src/actions/ActionRunner.ts:836action.params = { ...priorParams, ...collected } — the params dialog's collected values, written back onto the action
2packages/plugin-grid/src/ObjectGrid.tsx:2076params: { ...params, _selectedIds: ids } — aggregate bulk dispatch
3packages/plugin-grid/src/ObjectGrid.tsx:1721dispatch.params = { _rowRecord: r }
4packages/components/src/renderers/layout/containers.tsx:1287 / :1289dispatch.params = { _rowRecord: record } / { ...rawParams, _rowRecord: record }
5packages/app-shell/src/hooks/useNavActionDispatch.ts:93dispatch.params = { ...actionDef.params } — nav-item value bag
6packages/components/src/renderers/action/action-button.tsx:94Array.isArray(schema.params) ? { actionParams } : { params }
7packages/components/src/renderers/action/action-group.tsx:251, action-icon.tsx:83, action-menu.tsx:233forward action.params as Record

Effect on the two halves of the card, which is opposite for each:

  • params.newTab (removed here). None of these bags ever carries a navigation directive — they carry collected dialog values, _selectedIds, _rowRecord. Sites 5–7 copy an authored object params through, and that shape is spec-refused. So params.newTab is genuinely unreachable from any legitimate producer. Removal is safe.
  • The ${param.X} interpolation scope (NOT removed — see below). Sites 1–4 exist precisely to feed it. It is the live read point for runtime-injected param values.

Why the interpolation-scope half is not in this PR

interpolateTarget's non-array params branch is live, not dead vocabulary. Three independent confirmations, all already in the repo or in the shipped contract:

  1. The spec's own refusal message, shipped by objectstack PR #7375 and measured against the installed @objectstack/spec@17.0.0-rc.6, names the mechanism as surviving: "${param.X} interpolates a value collected by the params dialog".
  2. Two existing tests pin it on a url/navigation action carrying object-form paramsActionRunner.resultDialog.test.ts:238 (${param._selectedIds} injected by an aggregate bulk dispatch) and ActionRunner.test.ts:660 (${param.owner} on the navigation alias). Deleting the branch turns both red.
  3. ActionRunner.bodyExtra.test.ts:190 already documents the host-stashed bag as legitimate and explicitly not an authored payload.

The branch cannot distinguish an authored object from a runtime-injected one, because ActionRunner writes the collected values into the sameaction.params field the authored shape would occupy. Separating them needs a non-authorable channel for injected param VALUES threaded through all four packages above — and it would also have to move executeAPI's body base (asRecord(action.params)), executeModal's params?.schema, and serverActionHandler. That is a cross-package runtime-contract change, not a retirement, and the ruling supplies no spelling to move to: its sanctioned spellings (target interpolation, openIn) are authoring spellings, while these are internal injection channels that never pass through the spec parse.

Per the card's clause — "any producer found must move to the sanctioned spelling in this change, or if that move is non-trivial, STOP and report" — that half is reported for a ruling rather than guessed at. Part of #4097, not Fixes, for exactly this reason.

Pins

New file packages/core/src/actions/__tests__/ActionRunner.urlParamsRetirement.test.ts, 10 cases:

  • Sanctioned, positiveopenIn: 'new-tab' opens a new tab; openIn: 'self' keeps an external url in the same tab.
  • The retirement, negative — object-form params: { newTab: true } gets no new tab; and it cannot override openIn: 'self'.
  • Not touched — the external-url default still opens a new tab; the legacy navigate.newTab escape hatch survives.
  • The live mechanism, positive${param.X} interpolates values the params dialog collected (URL-encoded), and degrades an uncollected token to empty string.
  • Control${ctx.recordId} / ${ctx.user.id} / ${ctx.selection.ids} untouched.

Verification

  • Reverse verification (revert ActionRunner.ts only, restore after): predicted and measured 1 red / 9 green — only the negative retirement pin goes red; every positive and control stays green through the revert, which is the proof they pin live mechanisms this change does not alter.
  • vitest packages/core/ packages/components/src/renderers/action/ packages/app-shell/src/hooks/ packages/plugin-grid/src165 files / 2636 tests pass.
  • pnpm --filter @object-ui/core type-check — clean (tsc --noEmit + typetests).
  • eslint --no-inline-config on changed files — 0 errors (25 pre-existing no-explicit-any warnings, none in the new file).
  • check:control-bytes — OK (3977 files); plus a targeted control-byte self-scan of the changed files, clean.
  • check:action-forward-parity (ci(gate): diff each action renderer's forward whitelist against the keys the runtime reads (#4050, #4192) #4207) — green, and the delta is ZERO. Measured on both sides of the revert: identical "41 runtime-read keys from 3 consumers; 19 justified omissions, 7 known gaps" and identical per-surface owes/forwards. Expected: the gate extracts property accesses bound to the ActionDef parameter, so action.params?.newTab contributed params, and params is still read at four other sites.
  • No i18n change — the removal touches no user-facing copy (the authoring guidance for this lives in the spec's refusal message, shipped in objectstack PR #7375).

Changeset: @object-ui/core patch.


Generated by Claude Code

…#4097)
Executes the objectstack#6828 maintainer ruling of 2026-08-10 (contract half
shipped in objectstack PR #7375): the url-side readings of an object-form
`params` are RETIRED, not renamed. `openIn: 'new-tab'` is the sanctioned
spelling and already outranked the fallback.
The fallback could only ever fire on an object-form `params`, which the spec
has always refused (`params` is `z.array(ActionParamSchema)`), so no validated
stack could reach it. Removing it also closes the collision hazard where a
params dialog declaring a field named `newTab` had the user's own input
silently steering navigation.
The `interpolateTarget` ${param.X} scope half of the card is NOT removed —
four internal callers synthesize a non-array `params` as the runtime value bag
it reads. See the PR body and #4097 for the measurement.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
@vercel

vercelBot commented Aug 11, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectuiIgnoredIgnoredAug 11, 2026 9:45am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)28.5 KB350 KB
Entry fileindex-B01ldxSq.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)8.88KB3.25KB
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)22.10KB4.37KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.13KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.64KB2.21KB
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)35.76KB9.11KB
auth (createAuthenticatedFetch.js)4.37KB1.69KB
auth (index.js)2.35KB1.07KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)4.91KB0.87KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)26.07KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.65KB0.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)488.62KB108.26KB
core (index.js)3.04KB1.15KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)150.04KB39.79KB
fields (index.js)228.45KB56.62KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.32KB1.77KB
i18n (index.js)2.65KB1.06KB
i18n (pickLocalized.js)1.70KB0.83KB
i18n (provider.js)16.38KB5.47KB
i18n (useObjectLabel.js)27.59KB6.63KB
i18n (useSafeTranslation.js)4.52KB1.96KB
layout (index.js)38.98KB10.85KB
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)6.96KB1.98KB
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)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)8.75KB3.06KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)3.67KB1.12KB
permissions (evaluator.js)4.41KB1.44KB
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.23KB12.45KB
plugin-charts (index.js)61.52KB17.49KB
plugin-chatbot (index.js)180.33KB42.79KB
plugin-dashboard (index.js)118.58KB30.71KB
plugin-designer (index.js)210.91KB42.67KB
plugin-detail (index.js)238.88KB59.71KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)114.58KB27.68KB
plugin-gantt (index.js)164.14KB39.98KB
plugin-grid (index.js)187.97KB49.90KB
plugin-kanban (index.js)48.60KB13.41KB
plugin-list (index.js)110.31KB26.76KB
plugin-map (index.js)17.00KB5.32KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)40.58KB10.58KB
plugin-timeline (index.js)26.21KB7.52KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.03KB20.55KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.71KB3.53KB
providers (index.js)0.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.67KB2.37KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)23.71KB7.96KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.23KB0.66KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)4.09KB1.74KB
sdui-parser (index.js)4.47KB2.03KB
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 (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-retry.js)4.32KB2.02KB
types (index.js)3.05KB1.52KB
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 (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@yinlianghui@claude