fix(webv2): never hydrate the workbench without a project - #205
Open
lstein wants to merge 2 commits into
Open
Conversation
An editor always holds a project: `closeProject` refuses the last tab, and a session with none is the Home screen. Every load path is meant to seed a draft before hydration, but one returns the cached snapshot verbatim -- when a project the canvas gate refused cannot be retained, the primary cache is deliberately left in place and handed over as-is. That cache is projectless whenever the last tab was closed before the reload, so the store hydrates with `projects: []` and its active-project selector answers `undefined`. Nothing catches that: `activeProject` is typed `Project`, and the first consumer to read it dereferences undefined instead of finding an empty editor. That consumer is `BootWidgetHintController`, whose first property access happens to be `widgetRegions` -- so the whole editor fails to mount with "Cannot read properties of undefined (reading 'widgetRegions')" before the shell renders anything, and the only recovery is clearing localStorage. Seed the draft in `normalizeWorkbenchState` instead, the one point every hydration passes through, so no load path can produce a projectless store regardless of which one returned. The draft is built from the hydrated account, like the offline path's replacement draft: an empty cache still owns the account's preset defaults. Repair a dangling `activeProjectId` there too -- it named no project after a refusal, and `updateActiveProject` matches on it, so edits to the project the UI was showing (`projects[0]`, via the selector's fallback) silently went nowhere. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014xFbHnmFTLsHE9e1PAdvme
lstein
marked this pull request as ready for review
September 4, 2026 12:38
lstein
requested review from
JPPhoto,
Pfannkuchensack and
blessedcoolant
as code owners
September 4, 2026 12:38
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The symptom
The editor fails to mount with
Cannot read properties of undefined (reading 'widgetRegions'), and the only recovery is clearing localStorage.Why that message
The store's active project is selected as
projects.find(...) ?? projects[0], typedProjectbutundefinedfor a projectless state.BootWidgetHintControlleris the first child insideWorkbenchProviderand the first thing to read it — and the first property its selector touches iswidgetRegions, so the error names that field. Nothing renders after it, including the shell (whose first access would have saidlayout).How the store ends up projectless
An editor always holds a project —
closeProjectrefuses the last tab, and a session with none is the Home screen. Every load path seeds a draft before hydration, except one: when a project the canvas version gate refused cannot be durably retained,loadFromBackenddeliberately leaves the primary cache in place and returns it verbatim. That cache is{ projects: [], activeProjectId: '' }whenever the last tab was closed before the reload (persistEmptySession).The fix
Seed the draft in
normalizeWorkbenchState— the one point every hydration passes through — so no load path can produce a projectless store regardless of which one returned. The draft is built from the hydrated account, like the offline path's replacement draft: an empty cache still owns the account's preset defaults.The same guard repairs a dangling
activeProjectId, which is a second latent bug: after a refusal it named no open project, the UI showedprojects[0]via the selector's fallback, butupdateActiveProjectmatches on the id — so edits to the project on screen silently went nowhere.The cache-preservation behaviour at the refusal path is unchanged; this only guarantees the invariant at the consumer.
Verification
pnpm lint,pnpm test(509 files / 7429 tests), architecture checks — all green.🤖 Generated with Claude Code
https://claude.ai/code/session_014xFbHnmFTLsHE9e1PAdvme