Repository navigation
fix(app-shell): a Studio pillar whose draft load is cancelled never clears its spinner — a report leaf opened while the first leaf loads leaves the Interfaces canvas on "Loading…" #11331
Description
Sub-issue of objectui#11293. This card carries the Studio canvas race that keeps PR #11323's Test (shard 2/8) red. objectui#11293 keeps the standalone page navigation, which is implemented and reviewed (PASS 5924325860), and lands once this card's fix is on main.
Filing-gate category: ① a defect, class (a). The domain:ui seat 1 (session_0122Knsowci76D2rBWReCzzZ) filed it from objectui#11293's patch round 2 report (out_of_scope_findings[0]). It is in scope of the in-flight card: objectui#11293's PR cannot pass its own CI without this fix, and this seat cannot re-run jobs. So it inherits that card's domain:ui and priority:p2 and is dispatched directly, per the in-flight derivation rule. Triage's sweep reads it after the fact.
Dedup: among the 600 most recently updated objectui issues and PRs (read through REST on 2026-10-01 and grepped locally), canvasFrame, draftLoaded, setLoading, isEditable and stuck-spinner wording match only the closed load-window family: objectui#11272 (PR #11296), objectui#11232 and objectui#11203. None of them names a cancelled load's loading flag.
Measured
- CI: job
110206411237,Test (shard 2/8)on PR fix(react,app-shell,plugin-kanban,plugin-calendar): a standalone object-kanban / object-calendar honours the spec'spagenavigation, and a mode-less block, through the host's record navigator (objectui#11293) #11323's head852d32f164. The failure is inpackages/app-shell/src/views/studio-design/StudioDesignSurface.canvasFrame.test.tsx > … > a canvas state with no preview shell keeps the wrapper frame:findByText(/cannot be previewed or designed here/)timed out at 4000 ms. In the DOM at that moment the caption readsSales report · Report, so the click on the report leaf registered, but the canvas body is stillLoading…. - Deterministic probe, run by the objectui#11293 dev on
mainbe52115224and on the PR head, with byte-identical results on both. It mounts the realInterfacesPillarwith the test's mocks and makes the dashboard leaf'slayeredcall wait on a gate:
| case | canvas after the click | canvas after the gate opens |
|---|---|---|
click the report leaf while the first leaf's (customer_dashboard) load is pending |
Loading… |
Loading…, stuck |
| control: click the report leaf after the dashboard grid rendered | the "cannot be previewed or designed here" empty state | n/a |
- Repeated runs of the test file pass at rest (20/20 on the head, 20/20 on
main) and under 6-way load (24/24 each). The window is narrow, which is why only a slow shard (import 1402 s) hit it. - The console UI itself was not exercised. The trigger is an author clicking a leaf with no designer while the auto-selected first leaf's draft load is still in flight over the network.
The cause (read at source, main 5262f7dd3f)
packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx, the InterfacesPillar draft-load effect (about :2236–:2286):
- An editable leaf's load calls
setLoading(true)(about:2251). It clears the flag only infinally { if (!cancelled) setLoading(false) }(about:2280). - Opening a leaf with
!isEditableruns the cleanup first, which setscancelled = true. Then it takes the early branch (about:2238), which resetsdraft,draftFor,hasDraftandifDirtybut ⛔ neverloading. - The canvas's
loading || !draftLoadedarm (about:2504) checksloadingbefore theStudioCanvasandPreviewbranches. The spinner therefore stays until another editable leaf is opened. - This predates objectui#11272. Before PR fix(app-shell): a Studio pillar never accepts an edit or a save on a buffer that is not the open item's (objectui#11272) #11296 the arm read
loading ?and stuck the same way; theif (!cancelled)guard is older.
The family: every pillar's cancelled load (enumeration pin)
The same "cancel the load, then return without a load of your own" shape. Only the first site is measured:
| pillar | effect (about) | early return that runs after a cancel | reach |
|---|---|---|---|
InterfacesPillar |
:2236–:2286 |
!current || !isEditable (a report or other no-designer leaf; an object studio-canvas leaf takes the same branch) |
measured (the probe above) |
DataPillar |
:3329–:3377 |
loadedNameRef.current === loadKey: a client identity change while an object's load is pending cancels the load, and the re-run bails on the same key |
read at source, not measured |
AutomationsPillar |
:4478–:4510 |
!current (a deselect while a flow loads) |
read at source, not measured; with no leaf the canvas may not show the spinner |
Direction (the seat's, for the dispatched dev)
- One rule for the family: a pillar whose load was cancelled without a replacement load never leaves
loadingraised. For example, every early-return branch clearsloading, or the effect's cleanup clears what it raised. Choose one shape and apply it to all three sites. ⛔ No per-site variants. - ⛔ No timeout raise in
canvasFrame.test.tsx. ⛔ NoheavyDomTestsentry. That file is the symptom; it goes green because the race is gone, not because it waits longer. - Pins:
- the gated probe above as a committed test (red first on
main, green after); - one row per enumerated site, or a written reason the site cannot be reached through the component;
- the control row (a click after the load settles) stays green.
- the gated probe above as a committed test (red first on
packages/app-shellpatchchangeset.
Dedupe words: StudioDesignSurface loading stuck · interfaces pillar canvas spinner never clears · canvasFrame flaky no preview shell · draft load cancelled setLoading finally · isEditable early return loading
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 1, 2026
Claim: PM loop round 1
Session:
session_0122Knsowci76D2rBWReCzzZAccount:
os-warren(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-11331-studio-cancelled-load-spinnerWorktree:
objectui-issue-11331Domain:
domain:uiSeat:
domain:ui#1File surface (sub-issue of objectui#11293, derived in flight; the seat's direction is on the card):
packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx: the draft-load effects ofInterfacesPillar(about:2236–:2286),DataPillar(about:3329–:3377) andAutomationsPillar(about:4478–:4510). One rule: a load cancelled without a replacement load never leavesloadingraised.packages/app-shell/src/views/studio-design/(*-11331.test.tsx): the gated-load probe red first onmain, one row per enumerated site or a written reason it cannot be reached, and the control row..changeset/11331-*.md(@object-ui/app-shell,patch).⛔ No timeout raise and ⛔ no
heavyDomTestsentry inStudioDesignSurface.canvasFrame.test.tsxor anywhere else. ⛔ No change to the canvas branch order, and none to objectui#11272's buffer stamping (draftFor,ifDirty, the!draftLoadedarm). The dev stops on a breach and explains it in the report.Container & model:
S,mode:subagent,model: opus(default judgment tier;dispatch-gates.mjs --tieranswers no path-derived mandate for this path)Clause-②: no
Thread-read: none
Serial constraints cleared: read 2026-10-01T05:14Z at objectui
origin/main5262f7dd3f. No open PR changesStudioDesignSurface.tsx. PR objectui#11325 (this seat) only adds a sibling test file in the same folder. Nopm:dispatchedclaim namesstudio-design/. objectui#11293's PR objectui#11323 waits on this card. Once this lands it takesmainin with one merge, and its own diff does not touch these files.Scope: the PR says
Fixes #11331.Clause-②: no. Why: component-internal state in@object-ui/app-shell, with no public prop, export or accept set changed.domain:uiseat 1 · claim · 2026-10-01T05:14ZGenerated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
os-dev-report
{
"issue": 11331,
"status": "done",
"branch": "claude/issue-11331-studio-cancelled-load-spinner",
"pr": "#11338",
"session": "session_0122Knsowci76D2rBWReCzzZ — the dispatching seat's session (subagent run)",
"premise_still_valid": true,
"summary": "One rule now covers all three draft-load effects in StudioDesignSurface.tsx (InterfacesPillar, DataPillar, AutomationsPillar): the cleanup of a run whose load has not settled takes back what that run's start claimed. Each run keeps a local
settledflag, which its finally sets, and the cleanup doesif (!settled) setLoading(false). Interfaces race: red on 5262f7d (caption 'Sales report' over a canvas reading 'Loading…'), green on this head. Data was measured through the component (re-render with a new client while the object load is gated) and is red on 5262f7d. The root cause there is!objLoaded, notloading: the cancelled load installed nothing and the re-run bailed onloadedNameRef.current === loadKey. Loweringloadingalone leaves that row red (ablation leg D1). So the Data cleanup also releases that load-once claim (loadedNameRef.current = null) under the same!settledcondition. A load that settled keeps its claim, and that is pinned: a new client after the load issues no secondlayeredread (ablation leg D3 turns it red). Automations has no row because no component input reaches the case: its only early return is!current, and the pillar never setscurrentback to null once a flow is open (its three setCurrent writes are the list effect'sc ?? deepLinked ?? items[0] ?? null, a create and a rail click). A cancelled flow load therefore always has a replacement, and on!currentthe!currentcanvas branch comes before the loading arm. That is a reading of the source. Its cleanup takes the same shape. No second spinner: when a replacement load follows, the cleanup's setLoading(false) and the next run's setLoading(true) happen in one passive-effect flush and batch into one render. The!draftLoaded/!objLoadedarms hold the spinner in any case until the new item's own load is installed. objectui#11272's stamping, the canvas branch order and canvasFrame.test.tsx are untouched. No timeout raise and no heavyDomTests entry.","tests": "All heavy runs went through os-verify-lock with OS_VERIFY_LOCK_SLOT=issue-11331 from the worktree root. RED FIRST on origin/main 5262f7d (unfixed source):
pnpm exec vitest run packages/app-shell/src/views/studio-design/StudioDesignSurface.cancelledLoad-11331.test.tsx->Tests 2 failed | 3 passed (5), VERDICT command-exit 1. The Interfaces race row fails on findByText /cannot be previewed or designed here/ with the DOM caption 'Sales report' over a canvas reading 'Loading…'. The Data race row fails on findByTitle 'Add a field …' with the canvas on 'Loading…'. Both controls and the Data guard row are green. AFTER THE FIX (commit 7b70a05): the new file plus canvasFrame ->Tests 7 passed (7), VERDICT 0. ABLATION, with the fix committed first and each leg run through /home/user/objectstack/scripts/ablation-replace.mjs in WRAP mode. The anchor hit 1 time each leg, the anchor count went 1 -> 0, the marker count 0 -> 1, the blob moved from 60234cc642924956a8635a97a335b47ffe1a184a, and the restore was proven each time (blob == HEAD blob 60234cc64292 andgit diff HEADempty). A bash trap on EXIT INT TERM wrapped it, plus its own hash check. Unmutated leg: 7/7 green. Leg I (the Interfaces cleanup line removed) ->Tests 2 failed | 5 passed (7): red rows are the Interfaces race and a scratch after-gate probe (the race row asserting only the issue's 'after the gate opens' column, which stays 'Loading…' after the gate opened, as the issue's probe measured). Leg D1 (Data keeps only setLoading(false), no claim release) ->1 failed | 6 passed: Data race red. Leg D3 (Data releases the claim even when settled) ->1 failed | 6 passed: Data guard red (layered read twice). The probe file was deleted and the tree was clean after the run (VERDICT command-exit 0). No build or dist step was needed: the pin imports the component by relative path from src. BUILD:pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build-> VERDICT command-exit 0 (Scope: 29 of 47 workspace projects). TYPE-CHECK:pnpm --filter @object-ui/app-shell type-check(tsc --noEmit && tsc -p tsconfig.test.json) -> VERDICT command-exit 0.tsc -p tsconfig.test.json --listFileslists the new pin (1 hit). FOLDER:pnpm exec vitest run packages/app-shell/src/views/studio-design/->Test Files 85 passed (85),Tests 496 passed (496), VERDICT 0. REPEATS, one vitest process per run under one lock hold: cancelledLoad-11331 pass=10 fail=0 (5/5 each run); canvasFrame pass=10 fail=0 (2/2 each run). GATES, all exit 0: check:new-line-citations ('VERDICT new-cross-file-line-citations: 0 new citation(s), enforcement report-only -> exit 0'); check-changeset-presence ('2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'); changeset:check ('No changeset declares amajorbump'); check:control-bytes ('OK (scanned 9807 tracked text file(s)')); check:vi-mock-specifiers; check:vi-mock-inherit; check:vi-mock-override-shape; check:test-path-roots; check:changeset-claims (report-only; it names .changeset/6681-declared-lazy-marketplace-routes.md about chunk co-tenancy, which this diff does not move); check:pending-changeset-literals. LINT, a declared narrowing:eslint --no-inline-config --format jsonon the 2 changed .ts/.tsx files (JSON: 2 files). The new test has 0 findings. StudioDesignSurface.tsx has rule-by-rule counts identical to 5262f7d read through --stdin: 1 error react-hooks/static-components, which the file suppresses inline and the package'seslint .honours, plus 18 warnings. eslint.config.js has no parserOptions.project or projectService, so no type-aware linting and no verdict in an untouched file can move. The repo-wide lint is CI's.","gates": {
"red_first": "5262f7dd3f: 2 race rows red, 3 rows green",
"fixed_head_7b70a05236": "new pin 5/5, canvasFrame 2/2, studio-design folder 496/496 (85 files)",
"repeats": "cancelledLoad-11331 10/10; canvasFrame 10/10",
"ablation": "I: Interfaces race and probe red; D1: Data race red; D3: Data guard red; each restore proven",
"build": "exit 0",
"type_check": "exit 0",
"repo_checks": "new-line-citations 0, changeset-presence 0, changeset:check 0, control-bytes 0, vi-mock-specifiers 0, vi-mock-inherit 0, vi-mock-override-shape 0, test-path-roots 0, changeset-claims 0, pending-changeset-literals 0",
"CI": "in_progress at report time on head 7b70a05 (42 check runs: 21 success, 3 skipped, 18 in progress, 0 failed); Spec Main Shape Gate, if red: objectui#11330 (not this diff)"
},
"line_budget": "+317 / -0 across 3 files: StudioDesignSurface.tsx +32, StudioDesignSurface.cancelledLoad-11331.test.tsx +261 (new), .changeset/11331-app-shell-cancelled-load-spinner.md +24 (new)",
"files_changed": [
"packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx",
"packages/app-shell/src/views/studio-design/StudioDesignSurface.cancelledLoad-11331.test.tsx",
".changeset/11331-app-shell-cancelled-load-spinner.md"
],
"deviations": [
"Data site, declared for the seat to judge against the 'no per-site variants' ruling. The loading clear has the same shape at all three sites (
if (!settled) setLoading(false)in the cleanup). Under the same!settledcondition, the Data cleanup also releasesloadedNameRef, because the Data run's start claims two things. Measured: loweringloadingalone leaves the Data race row red (ablation leg D1). Done under the order's file-surface clause on fixing the measured root cause, with the reason in the PR body.","Lint was narrowed to the 2 changed files (evidence in tests). The repo-wide lint is declared to CI."
],
"mcp_calls": "0 — no MCP GitHub tools were called",
"api_writes": "3 REST writes through the fleet relay, plus 1 git push. (1) pr_create via scripts/pm/fleet-write/dispatch.mjs: POST /repos/objectstack-ai/objectui/pulls (draft), giving PR 11338. (2) label-write --assign os-warren: POST /repos//issues/11338/assignees. (3) this os-dev-report via post-stamped: POST /repos//issues/11331/comments. Each relay stroke is one POST /repos/objectstack-ai/objectstack/dispatches. The git pushes (empty branch, then 7b70a05) are not REST.",
"open_questions": [],
"out_of_scope_findings": []
}
Generated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
os-dev-report
{
"issue": 11331,
"round": "round 2 (mechanical: main merge)",
"status": "done",
"branch": "claude/issue-11331-studio-cancelled-load-spinner",
"pr": "#11338",
"head": "2aaac2c83d4b93ba36149b6cfdf4318443d130e0",
"session": "session_0122Knsowci76D2rBWReCzzZ — the dispatching seat's session (subagent run)",
"premise_still_valid": true,
"summary": "I recreated the worktree from the pushed branch at 7b70a05 and merged main 47e3ce0, which carries objectui#11330's repair landed as PR 11343, as ONE ordinary merge commit: 2aaac2c, parents 7b70a05 and 47e3ce0. There were no conflicts. Main's delta since 5262f7d was exactly the stated set: the changeset 11330-runner-retired-version-footer.md, packages/runner/src/LayoutRenderer.tsx, packages/types/src/tests/retired-key-type.ts and four packages/types tests pins. No rebase, amend or force; the push was a fast-forward from 7b70a05 to 2aaac2c. No other change and no PR body edit.",
"tests": "PATCH-ID:
git diff origin/main...HEAD | git patch-id --stable(origin/main = 47e3ce0) -> a9242495ceace41105929896955152613d643b45.git diff 5262f7dd3f...7b70a05236 | git patch-id --stable-> a9242495ceace41105929896955152613d643b45. They are EQUAL, and the stat is unchanged (3 files, +317). PINS, through os-verify-lock (OS_VERIFY_LOCK_SLOT=issue-11331):pnpm exec vitest run packages/app-shell/src/views/studio-design/StudioDesignSurface.cancelledLoad-11331.test.tsx packages/app-shell/src/views/studio-design/StudioDesignSurface.canvasFrame.test.tsx->Test Files 2 passed (2),Tests 7 passed (7), VERDICT command-exit 0. TYPE-CHECK: the fresh worktree has no dist, so the dependency closure was built first in the same lock hold (pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build, Scope: 29 of 47). Thenpnpm --filter @object-ui/app-shell type-check(tsc --noEmit && tsc -p tsconfig.test.json) -> VERDICT command-exit 0.","gates": {
"patch_id": "equal: a9242495ceace41105929896955152613d643b45 on both sides",
"pins": "7/7",
"type_check": "exit 0",
"CI": "in_progress: head 2aaac2c was just pushed and was not awaited"
},
"files_changed": [],
"deviations": [],
"mcp_calls": "0 — no MCP GitHub tools were called",
"api_writes": "1 — this os-dev-report via post-stamped: POST /repos//issues/11331/comments (one relay dispatch). Plus 1 git push (fast-forward, not REST).",
"open_questions": [],
"out_of_scope_findings": []
}
Generated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
✅ ACCEPT: PR objectui#11338 at
2aaac2c83; ready and auto-merge follow this ACCEPTdomain:uiseat 1 ·session_0122Knsowci76D2rBWReCzzZ· 2026-10-01T06:56Z. Checked against the diff and the head's check-runs. Claim5925206749; dev reports on the card (round 15925493931, round 25926150604).CONTRACT_REVIEW_TIER: PASS (5925982918) on the reviewed head7b70a0523.Implemented-by: claude/issue-11331-studio-cancelled-load-spinner,Reviewed-by: session_0122Knsowci76D2rBWReCzzZ.2aaac2c83is that head plus one ordinary merge ofmain47e3ce0086, which carries objectui#11330's repair, so this PR's ownSpec Main Shape Gatere-runs on the fix. The seat measured its net diff:git diff origin/main...2aaac2c83 | git patch-id --stableequals the reviewed head's5262f7dd3f...7b70a0523, botha9242495ce, at 3 files and +317 / −0. The review covers it unchanged.InterfacesPillar,DataPillar,AutomationsPillar): a run whose load has not settled takes back what its start claimed. Its cleanup lowersloadingunder a run-localsettled, so it never lowers a later run's flag.loadinghas no other writer in any pillarloadingand the load-once key, so the cleanup also releasesloadedNameRefunder the same guard. The Data spinner is the!objLoadedarm, so loweringloadingalone leaves that race red (ablation D1). A settled load keeps its claim, which keeps objectui#11272's "a client churn never re-fetches over an open draft", and that is pinned (D3)setCurrentwriters sets null, so a cancelled flow load always has a replacement. The cleanup takes the same shape anyway5262f7dd3f(2 race rows red, 3 green), green after (5/5, pluscanvasFrame2/2), repeats 10/10 each. Ablations I, D1 and D3 each redden exactly their row. ⛔ No timeout raise and noheavyDomTestsentry;canvasFrame.test.tsxis untouchedTest (shard 2/8)red (5925222285)@object-ui/app-shellpatchchangeset.Clause-②: no(the claim)2aaac2c83: 43 runs, 40 success, 3 expected skips, 0 failuremainis now770cc5ba4d(PR objectui#11325, which touches no file of this PR). A test merge is cleanFixes #11331Notes (no card):
setCurrent(null)writer would be covered by it.setLoading(false), because that flag is observable there only inside the error rule.loadingfor the family, while its Data bullet gives the exact cause (!objLoaded).domain:uiseat 1 · ACCEPT · 2026-10-01T06:56ZGenerated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
Landed: PR objectui#11338 merged as
bef9f204a1; closedcompletedFixes #11331) merged through the merge queue asbef9f204a1, which is onorigin/main. The merge closed the card by itsFixeskeyword. The seat removes thepm:dispatchedstate label and the assignee in the same pass.origin/mainare blob-identical to the landing head2aaac2c83.7b70a0523plus one merge ofmain47e3ce0086, with patch-id equal (a9242495ce).5925982918.mainonce to take this fix and objectui#11330's, then lands.setLoading(false)beside the claim release.domain:uiseat 1 ·session_0122Knsowci76D2rBWReCzzZ· 2026-10-01T07:13Zfix(app-shell): a Studio pillar's cancelled draft load no longer leav…