Uh oh!
There was an error while loading. Please reload this page.
feat(types,layout,app-shell): consume the declared nav runAction slot; retire the private ?runAction= convention - #5354
Conversation
…#5216) `ObjectNavItemSchema.runAction` (objectstack#7253) declares which action an object nav entry auto-runs on arrival at its list surface. objectui now reads that declaration and retires the private `?runAction=` string convention it replaces. The convention had two hand-written definitions and no declaration: `CloudOnboardingNext` built the query by string concatenation, and `EnvironmentListToolbar` matched it by reading a bare 'runAction' literal off window.location.search and comparing it to a hard-coded action name. Nothing declared it, `objectui validate` stripped it, and RESERVED_URL_PARAMS did not list the name, so a page could have repurposed it uncaught. - types: NavigationItem declares `runAction`, derived from the spec's object-nav variant; objectui's own nav zod schema declares it too, so `objectui validate` stops silently discarding the deep link. - layout: NAV_RUN_ACTION_PARAM is the param name's one definition, sited with resolveHref (its only writer). resolveHref encodes the slot onto the LIST landings only -- a recordId entry resolves to a record page, which has no list toolbar to answer it. - app-shell: useNavRunAction is the one implementation of read-once / consume-once. The deep link is now honoured on every object list, not just the environments list, and arms only on an action actually present at list_toolbar (#4123: arming spends a one-shot intent). urlParams registers the name by re-export rather than restating it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RV6yuVCxymHYE16PL9vQkE
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
os-support-ai
commented
Aug 19, 2026
ACCEPT — PM review, round 17.
Zone 0 passed as a reading, not an inheritance
Fork condition: none, and measured rather than assumed. What the private convention could express that the declared slot cannot is nothing on the nav path — it was produced in exactly one file and consumed in exactly one, comparing a bare literal against a hard-coded The declaration change is the right kind, and it is worth naming whyAdding Two consequences that improve on the card: the deep link is now honoured on every object list, not just Two measurements that contradict the card, both reported rather than smoothed over
VerificationTwo ablation legs, predicted before running, both matching by name and count: Leg A (revert the Leg B's second half is the load-bearing part, and it is the same shape #5068 and #5120 measured this round: the pre-existing deep-link suites pass happily against the restored private convention. They cannot be what retires it — which is exactly why the source-scan pin exists. Artifact position answered per leg: no The cross-repo follow-up is mine, and the dev improved its shapeThe liveness-ledger row flip needs two evidence pointers, not the single Merging via the queue. Generated by Claude Code |
Fixes#5216
Zone 0 — the premise gate, measured
The blocker objectstack#7253 merged 2026-08-10, pre-GA, so the declared slot was expected in the
17.0.0pin. Measured against the installed package, with counter-probes through the same import:The counter-probes discriminate: a known-present key reads
true, a known-absent key readsfalse, so thetrueonrunActionis a reading and not an artefact of a wrong import path. Premise valid — proceeded.Two further measurements against the same pin, both of which changed the implementation:
ObjectNavItemSchemaacceptsrunActiontogether withrecordId. The card's description of a parse-level exclusivity rule is not true of this pin, so the list-surface-only precedence below is load-bearing rather than merely defensive.runActionnproducesunrecognized_keys... "Did you mean ... runAction?"), andrunAction: ''is accepted — hence the empty-string-is-absent handling.What the private convention could express that the declared slot cannot
Nothing, on the nav path. Measured: the string was produced in exactly one place (
CloudOnboardingNext.tsx, hand-concatenating onto a route from page metadata) and consumed in exactly one (EnvironmentListToolbar.tsx, comparing a bare'runAction'literal against a hard-codedcreate_environment). It was a single-value private protocol between two files — strictly less expressive than the declared slot, which carries any action name on any object.The one genuine gap is that the welcome CTA is not a nav item: it is a page widget whose target route arrives as page metadata, so no
NavigationItemexists to hang a slot on. That is not a fork, because the slot's contribution is the param name and encoding, and the widget now takes both from it while sourcing the action name from its own declared page metadata (properties.createAction). One contract, two declared producers — not two contracts. No spec change is needed and none is made (Clause-2: no).No compatibility shim. No producer outside this repo emits the string: cloud supplies the bare
environmentsRouteproperty and the query was concatenated here, so producer and consumer both moved in this PR.Changes
packages/types—NavigationItem.runAction, derived from the spec's object-nav variant per the objectstack#4171 的三处 inverted pin 在 spec 17.0.0-rc.1 翻转了 ——NavigationItem/FormField/ConditionalValidation该做 burn-down 了 #3177 burn-down. Also declared in objectui's own nav Zod schema: that schema strips unknown keys, so before this an entry carrying a deep link validated clean throughobjectui validatewith the deep link discarded (the objectstack#4115 failure class).packages/layout—NAV_RUN_ACTION_PARAMis the param name's one definition, sited withresolveHref, its only writer.resolveHrefencodes the slot onto the list landings only (bare / named view / filters slice, joining with&where a query already exists). ArecordIdentry resolves to a record page, which has no list toolbar to answer it, so encoding there would spend a one-shot intent on a surface that never had the action.packages/app-shell—useNavRunActionis the single implementation of read-once / consume-once. The deep link is now honoured on every object list, not just the environments list.urlParamsregisters the name by re-export, so the reserved-param collision check is complete without a second spelling.Arming stays destructive-aware (#4123): the action must actually be present at
list_toolbar. A name no action answers to runs nothing and deliberately leaves the URL intact, so a later mount with fresher metadata can still honour it — the loud rejection of an undefined reference already happened upstream at authoring time, where the author can act on it (defineStack: "deep-link references action '...' (via runAction)").Tests and reverse-verification
Run from the repo root, at
f1eb6dafe.vitest run packages/layout/ packages/types/ packages/app-shell/... packages/core/src/actions/— 407 files, 4299 passed, 1 skipped, exit 0.type-checkon all three packages green;eslinton the diff: 0 errors.check:control-bytes,check:self-import,check:esm-specifiers,check:phantom-deps,check:spec-symbols,check-changeset-presence,check-changeset-no-major: all green.No build artifact sits between the edit and any vitest leg —
vitest.config.mtsaliases@object-ui/layout/types/app-shelltosrc, so an ablation cannot go falsely green through a staledist. Thetype-checkleg does readdist/*.d.ts; the dependency closure was built first and the artifact was confirmed to carry the new export (packages/layout/dist/NavigationRenderer.d.tsdeclaresNAV_RUN_ACTION_PARAM).Two legs, predicted before running:
withRunActionapplication inresolveHref(constant kept, so failures are assertion-level)resolveHref.runAction.test.ts— the four positive encodings plus the unresolved-template fallback; absence cases stay green; other two files greenEnvironmentListToolbar+CloudOnboardingNexttoorigin/mainit.eachfile cases + 2 named cases); ObjectView/hook cases and all "one definition" cases green; existing deep-link suites greenLeg 2's second half is the point of the retirement pin: the existing deep-link suites pass happily against the restored private convention, so they cannot be what retires it.
Cross-repo follow-up (not in this PR)
Step 4 of the card — flipping the framework liveness-ledger row
apps.navigation.children.runActionfromplanned/authorWarntolive— lands inobjectstack. The evidence pointer that row needs ispackages/layout/src/NavigationRenderer.tsx(resolveHref/NAV_RUN_ACTION_PARAM) for the producer andpackages/app-shell/src/hooks/useNavRunAction.tsfor the consumer.Generated by Claude Code