Skip to content

fix(webv2): never hydrate the workbench without a project - #205

Open
lstein wants to merge 2 commits into
mainfrom
fix/webv2-projectless-hydrate
Open

fix(webv2): never hydrate the workbench without a project#205
lstein wants to merge 2 commits into
mainfrom
fix/webv2-projectless-hydrate

Conversation

@lstein

@lstein lstein commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

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], typed Project but undefined for a projectless state. BootWidgetHintController is the first child inside WorkbenchProvider and the first thing to read it — and the first property its selector touches is widgetRegions, so the error names that field. Nothing renders after it, including the shell (whose first access would have said layout).

How the store ends up projectless

An editor always holds a project — closeProject refuses 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, loadFromBackend deliberately 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 showed projects[0] via the selector's fallback, but updateActiveProject matches 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

  • 4 new reducer tests: projectless hydrate seeds one draft, the draft inherits the cached account's preset override, a dangling active id is repaired, a populated session is untouched.
  • pnpm lint, pnpm test (509 files / 7429 tests), architecture checks — all green.

🤖 Generated with Claude Code

https://claude.ai/code/session_014xFbHnmFTLsHE9e1PAdvme

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
@github-actions github-actions Bot added the python label Sep 3, 2026
@lstein
lstein marked this pull request as ready for review September 4, 2026 12:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant