Skip to content

fix(app-shell): a Studio pillar's cancelled draft load no longer leaves the canvas on "Loading…" (objectui#11331) - #11338

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-11331-studio-cancelled-load-spinner
Oct 1, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-11331-studio-cancelled-load-spinner

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #11331
Clause-②: no

A Studio pillar whose draft load is cancelled, with no load to replace it, no longer leaves its canvas on "Loading…". The Interfaces race removed here is the one that keeps Test (shard 2/8) red on PR #11323 (StudioDesignSurface.canvasFrame.test.tsx, "a canvas state with no preview shell keeps the wrapper frame"). That file is unchanged here. It goes green because the race is gone, not because it waits longer.

The rule, one shape for the three draft loads

Each draft-load effect (InterfacesPillar, DataPillar, AutomationsPillar in StudioDesignSurface.tsx) raised loading as its load started. Only the load's own finally lowered it, behind if (!cancelled). The rule now: the cleanup of a run whose load has not settled takes back what that run's start claimed. Each run keeps a local settled flag, which its finally sets. Its cleanup runs if (!settled) and does the following:

  • all three pillars: setLoading(false).
  • Data only: it also releases the load-once claim (loadedNameRef.current = null). This is the same rule applied to a second claim, the only other claim a run's start makes. The Data run's start claims two things: loading and loadedNameRef. The measurement below shows that lowering loading alone leaves the Data case stuck.

Why it cannot raise a second spinner or flash stale content: when a replacement load follows, the old run's cleanup setLoading(false) and the new run's setLoading(true) happen in the same passive-effect flush, so React batches them into one render. Even without that batching, the canvas's loading || !draftLoaded arm (or !objLoaded in Data) keeps the new item on the spinner until its own load is installed, because the objectui#11272 buffer stamp names the item it was loaded for. The stamping (draftFor, ifDirty, hasDraft, the !draftLoaded arm and its error && !loading ? null rule) is unchanged, and the canvas branch order is unchanged.

What was measured (Zone 2)

  1. Interfaces, red first on origin/main 5262f7dd3f. The gated probe is now a committed pin. It mounts the real InterfacesPillar with canvasFrame.test.tsx's mocks, holds the dashboard leaf's layered read on a gate, and clicks the report leaf while that read is in flight. On 5262f7dd3f the race row fails with the DOM caption Sales report over a canvas body that reads Loading…. A scratch probe asserted only the issue's "after the gate opens" column, and on the ablated tree it fails the same way after the gate opened. The control row, a click after the dashboard grid rendered, shows "cannot be previewed or designed here" on both trees.
  2. Data: reached through the component. The pin re-renders the pillar with a new client object between the start of the load and its release. On 5262f7dd3f the object stays on Loading… after the gate opens. The cause is !objLoaded, not loading. The cancelled load installed nothing, and the re-run bailed on loadedNameRef.current === loadKey. Lowering loading alone does not fix this site (ablation leg D1 below: the race row stays red). Releasing the unsettled claim is what fixes it. A claim whose load settled is kept, and a pin row says so: a new client after the object loaded issues no second layered read.
  3. Automations: not reachable through the component, so it has no row. Its effect returns early only on !current, and the pillar never sets current back to null once a flow is open. Its three setCurrent writes are the list effect's c ?? deepLinked ?? items[0] ?? null, a create and a rail click, all of them non-null once a flow is open. So a cancelled flow load always has a replacement. On !current the canvas's !current branch comes before the loading arm, so no spinner is visible with no leaf. That is a reading of the source; nothing in the tree re-derives it. The cleanup takes the same shape here as at the other two sites.
  4. One shape: the settled-gated cleanup above, chosen over clearing in every early-return branch. A branch-by-branch clear covers only the early returns that exist today, and it does not cover the Data bail at all. The cleanup is the one place every cancelled run passes through.

Pins: StudioDesignSurface.cancelledLoad-11331.test.tsx

row 5262f7dd3f (no fix) this head
Interfaces race: report leaf clicked while the dashboard load is in flight red (Loading… under the Sales report caption) green
Interfaces control: the same click after the dashboard rendered green green
Data race: a new client while the object load is in flight red (Loading…, no Add field) green
Data control: the same gated load with no new client green green
Data guard: a new client after the object loaded reads nothing again green green

Ablation (fix committed first, each leg mutated through ablation-replace.mjs, restore proven by blob hash and an empty git diff HEAD)

leg mutation red rows green rows
I Interfaces cleanup line removed Interfaces race, and the scratch probe's after-gate race row the other 4 pin rows, and the probe's control
D1 Data keeps only setLoading(false), so the claim is not released Data race the other 4 pin rows, and both probe rows
D3 Data releases the claim even when the load settled Data guard (layered read twice) the other 4 pin rows, and both probe rows

Local verification (head 7b70a05236)

  • pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build: exit 0. Then pnpm --filter @object-ui/app-shell type-check (tsc --noEmit && tsc -p tsconfig.test.json): exit 0. --listFiles shows the test project compiles the new pin.
  • pnpm exec vitest run packages/app-shell/src/views/studio-design/: Test Files 85 passed (85), Tests 496 passed (496).
  • Repeated runs, one vitest process each: the new file passed 10 of 10 runs and StudioDesignSurface.canvasFrame.test.tsx passed 10 of 10.
  • Gates, each at exit 0: check:new-line-citations (0 new), check-changeset-presence, changeset:check, check:control-bytes, check:vi-mock-specifiers, check:vi-mock-inherit, check:vi-mock-override-shape, check:test-path-roots, check:changeset-claims (report-only; the pending changeset it names is about chunk co-tenancy, which this diff does not move), check:pending-changeset-literals.
  • ESLint on the two changed files only (--no-inline-config): the new test has 0 findings. StudioDesignSurface.tsx has the same rule-by-rule counts as 5262f7dd3f read through --stdin, which is 1 error (react-hooks/static-components) and 18 warnings, so nothing was added. The config enables no type-aware linting, so this diff cannot move a verdict in an untouched file. The repo-wide lint is CI's.
  • ⛔ No timeout was raised and no heavyDomTests entry was added.

Acceptance notes

  • Changeset: .changeset/11331-app-shell-cancelled-load-spinner.md, @object-ui/app-shell patch.
  • CI was not awaited (the seat owns convergence). Spec Main Shape Gate is expected red from objectui#11330, which this diff does not touch.
  • Reach of the Data site: the pin reaches it through a useMetadataClient re-mint, which the hook's useMemo can hand out (AGENTS.md [WIP] Enhance every detail of the designer #10). No console flow that re-mints the client mid-load was exercised.

Session: https://claude.ai/code/session_0122Knsowci76D2rBWReCzzZ


Generated by Claude Code

…es the canvas on "Loading…"

A pillar's draft-load effect raised `loading` as a load started and lowered
it only from the load's own `finally`, behind `if (!cancelled)`. A load the
cleanup cancelled never lowered it, so a next run that started no load of its
own left the canvas spinning for good: an Interfaces leaf with no designer
opened while the first leaf loaded, and a Data object whose load a new client
cancelled (the re-run bailed on the load-once claim the cancelled load had
taken but never honoured).

One rule for the three draft loads (Interfaces, Data, Automations): the
cleanup of a run whose load has not settled takes back what its start
claimed, `loading` everywhere and the Data pillar's load-once claim too.

Pinned by StudioDesignSurface.cancelledLoad-11331.test.tsx: gated race rows
red on 5262f7d and green here, with controls.

Claude-Session: https://claude.ai/code/session_0122Knsowci76D2rBWReCzzZ
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 1 pending changeset(s) describe a file this change touches

Their bodies publish verbatim into the CHANGELOG at the next release, so this is a request to re-read them against your diff — addressed here because you are the one seat that can answer it without re-deriving anything.

⛔ Nothing here blocks, and nothing here is a verdict on your change. This gate exits 0, is not a required context, and judges name resolution, never meaning: it asked whether a pending body names a file you touched. "Is this sentence still true?" is the one question it will not answer, and the one you are being asked to answer.

.changeset/6681-declared-lazy-marketplace-routes.md

  • names views/studio-design/StudioDesignSurface.tsx → packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx — edited by this change

    Both marketplace chunks were held by CHUNK CO-TENANCY, not by any import of the page: rolldown had parked components/SuggestedBindingsPanel.tsx (statically imported by the eager views/studio-design/StudioDesignSurface.tsx) in the first, and console/marketplace/InstalledListWidget.tsx (bare-imported by the package barrel for its SDUI registration) in the second. An app-shell-eager-leaves group in apps/console/vite.config.ts isolates those co-tenants so the three declared-lazy pages chunk by their own dynamic-only reachability. The console eager closure moves 3180.2 KB → 3171.5 KB gzipped (−8,888 bytes, 48 → 45 eager chunks) with the three per-chunk ceilings unmoved.

Read the paragraph, not the line: both false halves of the objectui#8617 claim sat in one paragraph, and correcting either alone would have left it asserting the same wrong thing.

If a claim did go false, correct the body. That is precedented and prose-only, frontmatter untouched; check-changeset-overwrite.mjs will report the correction as its own case 2 ("correcting a declaration on purpose … legitimate"), which is the intended shape — one gate asks for the read, the other records the write.

Not covered, stated so nobody reads this as more: a born-false claim that spells no line address at all (objectui#9495 coordinated one by ORDINAL — "a grep finds that member first" — and deciding that means reading what the sentence means), a claim spelled as a symbol or a package rather than a backticked file name, and a file named ambiguously.

Compared the checked-out tree with 770cc5ba4 (merge-base with origin/main): 2 file(s) changed outside .changeset/, read against 1897 pending declaration(s) that publish a body (2510 pending in total). · run

@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 330 chunks) 3585.7 KB 3607.4 KB
Main entry chunk (gzip) 150.5 KB 350 KB
Entry file index-MBh4rUaq.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.88KB 6.25KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.17KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.13KB 7.95KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 571.28KB 136.83KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 229.96KB 63.80KB
fields (index.js) 261.05KB 66.23KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.49KB 9.23KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 40.95KB 11.48KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.01KB 3.93KB
plugin-calendar (index.js) 52.98KB 15.39KB
plugin-charts (index.js) 84.09KB 22.93KB
plugin-chatbot (index.js) 198.22KB 46.97KB
plugin-dashboard (index.js) 139.34KB 37.30KB
plugin-designer (index.js) 216.45KB 44.60KB
plugin-detail (index.js) 244.79KB 64.47KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 173.70KB 44.63KB
plugin-gantt (index.js) 173.11KB 43.10KB
plugin-grid (index.js) 231.53KB 63.58KB
plugin-kanban (index.js) 49.32KB 15.48KB
plugin-list (index.js) 116.63KB 28.97KB
plugin-map (index.js) 23.54KB 7.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.04KB 12.21KB
plugin-timeline (index.js) 33.91KB 10.00KB
plugin-tree (index.js) 11.20KB 3.89KB
plugin-view (index.js) 90.32KB 22.76KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.81KB 3.58KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 119.55KB 39.23KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.06KB 2.68KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 21.42KB 7.05KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 3.19KB 1.62KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.26KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 21.59KB 7.71KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Spec Main Shape Gate red: not this PR's (objectui#11330, p0, in flight)

Job 110235909441 on head 7b70a0523 compiled against objectstack main b3d7a70864a7 and reported the same 5 diagnostics in the same 5 files as objectui#11330:

  • four packages/types retired-key pins (first error chartconfig-aria-retired-4044.test.ts:69 TS2322);
  • packages/runner/src/LayoutRenderer.tsx:194.

This PR touches none of them. The cause is objectstack d830d71f (objectstack-ai/objectstack#21023) branding retired-key types.

  • Fix: none exists yet to port. objectui#11330 is claimed (5925455871) and its dev is working the repair now, as a lane p0.
  • Next: once that repair is on main, this branch merges main once, the gate re-runs on that commit, and the seat's contract review and landing follow. ⛔ No re-run before then; this seat has no means to re-run jobs.

domain:ui seat 1 · session_0122Knsowci76D2rBWReCzzZ · 2026-10-01T05:51Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 7b70a05236de84d1c3d98b4c3c28a5a7c48a7f47
Local-runs: none

Read: card objectui#11331 (body, claim 5925206749, dev report 5925493931), PR objectui#11338 (body, its 3-file list, the net diff against main 5262f7dd3f, which is the merge-base: +317/−0, one commit), the head's 43 check-runs, and read-only objectui#11293's patch round 2 report (5925143481) and PR objectui#11296 (objectui#11272). Nothing was built, run or re-run; git reads and REST GETs only. Line numbers below are the head's, all "about".

① Derived judgments

Public surface and accept sets: none move. The diff adds no export, prop, type, registry entry or spec key. StudioDesignSurface.tsx gains 32 lines, all inside the bodies of three existing useEffects; the other two files are a new pin and a changeset. Right.

(a) The rule as implemented. The three draft-load effects (InterfacesPillar :2237, DataPillar :3341, AutomationsPillar :4504) each keep a local settled, set it as the first statement of finally, and their cleanup does cancelled = true; if (!settled) setLoading(false). Right, and it holds:

  • It clears exactly what a cancelled run raised, and never a later run's flag. The cleanup closes over its own run's settled; React runs a run's cleanup before the next run's setup, so the lower is enqueued before any later raise and no later run exists at cleanup time. In each pillar loading has no writer outside its draft-load effect (:2031/:2253/:2283/:2296, :3190/:3354/:3386/:3399, :4397/:4509/:4531/:4540).
  • settled is true at cleanup time only if finally already ran, and it ran with cancelled false (the cleanup is the only writer of cancelled), so finally had already lowered loading. For loading the guard is an idempotence guard at all three sites; it is load-bearing for the Data claim in (b).
  • StrictMode's double effect: setup, cleanup (unsettled, so lower), setup (raise), batched in one flush. The first load's finally sees cancelled and lowers nothing; the second load's lowers. In Data the cleanup also nulls the claim, so the second setup loads instead of bailing; at the base the second setup would have bailed on a load the first cleanup cancelled.
  • A reload of the same leaf (publishNonce or client in the deps): the cleanup lowers if unsettled and the re-run raises in the same flush. In Data the key carries the nonce, so a publish is a new key either way.
  • Error path: catch sets the error only if (!cancelled); finally sets settled and lowers only if (!cancelled). A cancelled run's error never lands and its loading is lowered by the cleanup; a settled error already lowered it. The canvas's error && !loading ? null rule (:2520, :3878, :4760) is unchanged.
  • Unmount (the pillars are keyed by packageId): the cleanup's setLoading(false) on an unmounting component is a no-op.

(b) The Data site's extra release. Under the same !settled guard the Data cleanup also does loadedNameRef.current = null (:3400). The ref has no other writer (:3350 the claim, :3400 the release). One rule, not a per-site variant: the shape is the same at the three sites, and the Data run's start makes two claims, loading and the load-once key (:3349–:3354); "a cancelled run takes back what its start claimed" lists both. The card's own Data row names the bail on the same key as that site's early return, and the Data spinner is the !objLoaded arm (:3875; Data reads loading only inside that arm's error rule, :3878), so neither of the card's two example shapes makes the Data row green without the release; the dev measured that (D1 red). Right. It does not reopen objectui#11272's guarantee: a settled load keeps its claim, so a client change after the load bails as before (:3349); inside the unsettled window the open item has no buffer of its own to clobber (objLoaded is loadedFor === target, false until this load installs objDraftFor at :3370; sending claims nothing; the cancelled run installs nothing past if (cancelled) return at :3364). The D3 pin is sound: the guard row proves the new client reached the pillar (its rail list call), then asserts exactly one layered read of the object; an unconditional release turns it red with two reads.

(c) Automations. The source reading is true at this head. current's writers in AutomationsPillar are three: the list effect's c ?? deepLinked ?? items[0] ?? null (:4462, which keeps a non-null c), the create (:4490, the new item) and the rail click (:4716, f); there is no setCurrent(null) and setCurrent is handed to no child. The draft-load effect's only early return is !current (:4505) and it has no load-once guard, so every re-run with a flow open loads: a cancelled flow load always has a replacement within one mount, and a package switch unmounts the pillar. The canvas's !current branch (:4755) precedes loading || !flowLoaded (:4757), so no spinner shows with no flow. The written reason the card allows in place of a row stands. Applying the same cleanup there anyway is right: it is the family rule, and the replacement run re-raises in the same flush. The cost is a limit, not a defect: the site is unpinned, so a future writer that nulls current mid-load would be covered by the rule and not by a row.

(d) No second spinner. True. The cleanup's lower and the replacement run's raise both run in the passive-effects phase of one commit and batch into one render. Independently of that, the arms hold the spinner for a new item until its own load installs: Interfaces loading || !draftLoaded (:2516, draftFor stamped on install at :2276), Data !objLoaded (:3875, objDraftFor at :3370), Automations loading || !flowLoaded (:4757, draftFor at :4524). A leaf with no designer takes the early branch, which stamps draftFor to itself (:2245), so with loading lowered the canvas falls through to its own state, which is what the symptom file waits for.

(e) Pins (StudioDesignSurface.cancelledLoad-11331.test.tsx: 5 rows, gated layered reads, the real pillars under canvasFrame.test.tsx's mocks). The red-first mechanism matches the base source: the Interfaces race row waits for the no-designer text under the report caption, which the stuck loading || !draftLoaded arm never shows; the Data race row waits for add-field, which the bail leaves behind !objLoaded. The dev's 2 failed / 3 passed at 5262f7dd3f, 5/5 plus canvasFrame 2/2 after, 10/10 repeats and legs I, D1 and D3 are consistent with that reading; the head's eight test shards, all green with shard 2/8 among them (the shard the race kept red on objectui#11323), are the gate verdict. The plausible wrong fix: clearing loading only in the Interfaces early-return branch is one of the card's two allowed shapes, and the Interfaces rows cannot tell it from the cleanup, which is right; the same branch-only shape in Data leaves !objLoaded standing and the Data race row red, so a family-wide branch-only fix fails the pin. Not caught, named as limits: a Data cleanup that releases the claim but omits setLoading(false) (the flag is unobservable there outside the error rule), and anything at the Automations site. The control row's 4000 ms wait for the dashboard grid is the same wait canvasFrame.test.tsx uses for its own grid (:110). canvasFrame.test.tsx is unchanged, and the diff holds no heavyDomTests entry.

Canvas branch order and objectui#11272's stamping: unchanged. The diff is additions only; it touches no draftFor, ifDirty, hasDraft or objDraftFor write and no arm.

(g) Check-runs on the head: 43, all completed, none in progress: 39 success, 3 skipped (Test (coverage), Test (coverage shard), dependabot), 1 failure. The failure is Spec Main Shape Gate (job 110235909441): objectui#11330, objectstack d830d71f's branded retired-key types, failing in packages/types pins and packages/runner/src/LayoutRenderer.tsx, none of which this diff touches (the seat's 5925588822 on the PR); its fix is PR objectui#11343, landing now. Named, not counted against this PR. Every other gate is green on its own conclusion: Lint, Type Check, Build & E2E, Test (dist pins), Test (shard 1/8) through Test (shard 8/8), Changeset Declaration, Changeset Bump Policy, Changeset Fixed Group Check, Changeset Claim Re-read, Line Citation Gate, Inert vi.mock Specifier Check, Bundle Analysis (console eager closure 3585.7 KB of a 3607.4 KB budget, PASS).

② Semver level

.changeset/11331-app-shell-cancelled-load-spinner.md: @object-ui/app-shell patch. Right: a bug fix in component-internal state; no export, prop, type or accept set moves, and the one released package touched is the one declared (Changeset Declaration and Changeset Bump Policy green). Clause-②: no is right for the same reason. The text is true: each pillar raised loading at the start and lowered it only in finally behind if (!cancelled) (the base source, all three sites); the Interfaces bullet is the measured race; the Data bullet names its own cause (the re-run skipped the load, the cancelled load installed nothing); the closing paragraph states the rule, the Data claim and the settled-claim guarantee as the head's source has them. One nuance, not a falsehood: the lead sentence attributes the family's spinner to loading, while in Data the visible arm is !objLoaded; the Data bullet carries the accurate cause. The pending .changeset/6681-declared-lazy-marketplace-routes.md that Changeset Claim Re-read named stays true: this diff moves no import of StudioDesignSurface.tsx.

③ Boundary flags

  • Dev deviation 1, the Data claim release: answered in ① (b). One rule, declared where the card asked, measured both ways (D1 red without it, D3 red when over-applied). Not a per-site variant.
  • Dev deviation 2, lint narrowed to the two changed files: answered by the head's Lint check-run, green.
  • PR acceptance note, the Data site's reach: the pin reaches it through a useMetadataClient re-mint, and no console flow that re-mints the client mid-load was exercised. Accepted as a declared limit: the card listed the Data site as read at source and not measured, and the pin now measures it through the component.
  • open_questions: none. out_of_scope_findings: none.
  • Reviewer's own: the Automations site is unpinned on a source reading that is true at this head (① (c)); the card allows the written reason. Not escalated.
  • The card's ⛔ lines: no timeout raise in canvasFrame.test.tsx (unchanged), no heavyDomTests entry, canvas branch order unchanged, objectui#11272's stamping untouched. All held. The PR is still a draft; readiness is the seat's.

Implemented-by: claude/issue-11331-studio-cancelled-load-spinner
Reviewed-by: session_0122Knsowci76D2rBWReCzzZ

VERDICT: PASS

…inner

Takes objectui#11330's Spec Main Shape Gate repair. No conflicts; this branch's diff is unchanged.

Claude-Session: https://claude.ai/code/session_0122Knsowci76D2rBWReCzzZ
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 330 chunks) 3585.6 KB 3607.4 KB
Main entry chunk (gzip) 150.4 KB 350 KB
Entry file index-D564RIhS.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.88KB 6.25KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.17KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.13KB 7.95KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 571.28KB 136.83KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 229.96KB 63.80KB
fields (index.js) 261.05KB 66.23KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.49KB 9.23KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 40.55KB 11.41KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.01KB 3.93KB
plugin-calendar (index.js) 52.98KB 15.39KB
plugin-charts (index.js) 84.09KB 22.93KB
plugin-chatbot (index.js) 198.22KB 46.97KB
plugin-dashboard (index.js) 139.34KB 37.30KB
plugin-designer (index.js) 216.54KB 44.64KB
plugin-detail (index.js) 244.79KB 64.47KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 173.70KB 44.63KB
plugin-gantt (index.js) 173.11KB 43.10KB
plugin-grid (index.js) 231.53KB 63.58KB
plugin-kanban (index.js) 49.32KB 15.48KB
plugin-list (index.js) 116.63KB 28.97KB
plugin-map (index.js) 23.54KB 7.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.04KB 12.21KB
plugin-timeline (index.js) 33.91KB 10.00KB
plugin-tree (index.js) 11.20KB 3.89KB
plugin-view (index.js) 90.32KB 22.76KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.81KB 3.58KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 119.55KB 39.23KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.06KB 2.68KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 21.42KB 7.05KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 3.19KB 1.62KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.26KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 21.59KB 7.71KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 1, 2026 06:57
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 1, 2026 06:57
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 1, 2026
Merged via the queue into main with commit bef9f20 Oct 1, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-11331-studio-cancelled-load-spinner branch October 1, 2026 07:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants