fix(electron): stop the system menu blanking the window, and define the shell/host seam (0406) - #641
Merged
Merged
Conversation
added 4 commits
July 28, 2026 08:15
The desktop app is not behind the web app; it is a different shell. Web's
entire workbench — islands, explorer, panels, palette, AI chat — lives in
apps/web/src/workbench/, where Electron structurally cannot import it.
The load-bearing finding is that the state model is already shared:
apps/web/src/workbench/layout-tree.ts is a shim re-exporting @xnetjs/plugins,
and a parity test already guards that desktop consumes the same module. Both
surfaces agree on what a workspace is; they disagree only on how to draw it.
0280 named this exact fork ("Electron divergence is the tax on shell-as-code")
and its open question 6 scheduled the ShellFrame port for phase 5, which never
ran. Recommends extracting the renderer into packages/workbench behind a
narrow PlatformPort so routing and native capability are the only things that
differ per surface.
Signed-off-by: xNet Test <test@xnet.dev>
MenuLabel rendered Base UI's Menu.GroupLabel, which throws unless it finds a <Menu.Group> ancestor. The simple Menu has no groups, so opening the desktop system menu — the shell's only navigation affordance — threw during render. With no error boundary above it, the whole React tree unmounted to a black window with no way back but quitting. Make MenuLabel a plain element (grouping semantics live on DropdownMenuGroup + DropdownMenuLabel), and add ShellErrorBoundary so any future shell render failure degrades to a recoverable panel instead of a dead window. The regression test asserts at the primitive, since every simple-Menu consumer shared the fault; verified it throws against the old implementation rather than passing vacuously. Also drops two pieces of dead weight this exposed: the orphaned Sidebar.tsx (no importers) and the @tanstack/react-router dependency the Electron renderer declares but never imports. Signed-off-by: xNet Test <test@xnet.dev>
The workbench is on its way to packages/workbench, shared by web and desktop. Web navigates by URL through TanStack Router; the Electron renderer has no router at all and navigates by transitioning a ShellState reducer. Rather than force one on the other, the shell states intent — "open this node", "go to this surface" — and the host decides what that means. Adds PlatformPort/NavTarget/PlatformCapabilities and the web implementation. The node -> route table moves to the host, since routes are a property of the web host rather than of the shell, and it is now Record<TabNodeType, ...> so a missing entry is a compile error; previously it was a runtime console.warn and the click just did nothing. No consumers yet, and web behaviour is unchanged: migrating the 25 workbench modules off the router is the next item, and a half-migrated port would be worse than none. Also adds the ShellErrorBoundary test, so "a thrown child shows the crash panel, not a blank window" is a verified claim rather than an asserted one. Signed-off-by: xNet Test <test@xnet.dev>
…the desktop web ui fork (8/33) Signed-off-by: xNet Test <test@xnet.dev>
Contributor
🖼️ UI changes in this PRNo visual differences detected in the changed UI. |
Contributor
|
Preview removed for PR #641. |
crs48
added a commit
that referenced
this pull request
Jul 28, 2026
…uter (0406) (#642) Continues exploration **0406** (11/33 after this). Follows #641, which defined the seam; this PR makes the shell actually use it. ## What this does Migrates all 29 workbench modules and 20 app-land consumers off direct `@tanstack/react-router` imports onto the `PlatformPort`. The shell now states intent — "open this node", "go home", "this path" — and the web host resolves it to a URL. **Web behaviour is unchanged**; the point is that the same components can now mount on desktop over a `ShellState`-backed port. Key moves: - `navigation.ts` collapses to intent emission; the node→route switch lives in the host's `routeForTarget`, where a missing entry is a **compile error** instead of a silent dead click. - Preview-tab arming (0284) moves into the host's navigate, extracted as `armPreviewIntent` so its semantics stay pinned by tests through the move. - `navigateToNewDoc` gains a port-shaped sibling, retiring the `as unknown as NavigateLike` casts the compiler could not see into. - The port grows search read/write (`/settings?section=`, `/ai?q=`) and a richer `Link` (drag sources, testids) — requirements the call-site survey found, not guesses. - An ESLint `no-restricted-imports` override turns any future router import in the workbench into a red build; **verified against a probe file**, not assumed. - `test-platform.tsx` replaces router module mocks: components stopped importing the router, so tests stop mocking it and provide a port. ## Verification Beyond 992 green web tests: drove the live app through every migrated path — doc creation via the New menu, settings `?section` round trip (write **and** read), working-set opens, profile-menu navigation, history back/forward. Zero console errors. One anomaly found (palette Enter on "Go to tasks" under synthetic input doesn't navigate) was **stash-tested against the pre-migration baseline and reproduces identically** — pre-existing, not a regression. ## Changelog `skip-changelog`: internal refactor, no user-visible change. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
crs48
added a commit
that referenced
this pull request
Jul 28, 2026
…ch (0406) (#644) Continues exploration **0406** (12/33 after this). Follows #641 (the seam) and #642 (the migration onto it); this PR makes the core physically shareable. ## Why the core first, not the whole directory The workbench's chrome references **52 distinct app-local modules** (views, comms, hooks, search). A whole-directory move would churn web's highest-traffic area for days and conflate relocation with API design. The host-agnostic core, by contrast, has exactly four dependencies — `react`, `zustand`, `lucide-react`, `@xnetjs/plugins` — so it moves cleanly and delivers the thing desktop actually needs first: **the state layer**. `@xnetjs/workbench` now contains: the `PlatformPort`, the workbench store, tab/preview grammar, navigation intent, surfaces, command wiring, focus, route titles, sidebar sections, explorer sort, new-doc identity, and the test port. Private and source-consumed (`main → src/index.ts`, the `canvas-core` pattern) — no build ordering, no vite aliases. Old paths keep one-line shims (the 0280 `layout-tree.ts` pattern, this repo's own precedent). Both roads resolve to one module instance; consumers migrate opportunistically. ## What the move itself surfaced - `state.ts` declared its own `ChromePosture`, a twin of the canonical type in `@xnetjs/plugins` — 0277's copied-type drift in miniature, invisible until a barrel saw both declarations. Fixed at the source rather than papered over in the barrel. - `newDocId`/`CreatableDocType` graduate into the package; `lib/doc-creation` keeps routes + menu chrome and re-exports identity. ## Why this matters for the desktop The parity test's founding condition — *"the moment desktop adopts them it consumes the SAME module as web"* — now covers the entire state layer, not just `layout-tree`. Phase 3 (mounting the shell in Electron) can begin with desktop importing `useWorkbench` and the port directly. ## Verification - Live app: boot, working-set open, and New → Page (URL change + editor mount) all flow through the package modules, zero console errors. - 992 web + 89 package + 149 electron tests green (parity test included); repo-wide typecheck clean; lint 0 errors. - The router ban now also covers `packages/workbench/src`. `skip-changelog`: internal restructuring, no user-visible change. Private package → no changeset. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
crs48
added a commit
that referenced
this pull request
Jul 28, 2026
…649) Continues exploration **0406** (14/33 after this). The desktop's half of the seam #641 defined. `useDesktopPlatformPort` resolves `NavTarget` intents to `ShellState` transitions through the shell's own handlers (home restores the viewport; documents go through type lookup and the canvas glide), and synthesizes pathnames in **web's route grammar** so shared core modules (`tabFromPathname`, route titles) behave identically on both hosts. Unhandled targets return `false` and warn loudly — the silent dead click is the failure mode this exploration exists to kill. `PlatformProvider` now wraps the desktop shell tree: the moment shared chrome mounts here, its navigation already works. Verification: 6 new unit tests pin the pathname grammar and dispatch table (155 electron total, all green); typecheck clean; verified live over CDP that the provider mounts and the shell + Assistant behave unchanged. `skip-changelog`: internal plumbing, no user-visible change. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
crs48
added a commit
that referenced
this pull request
Jul 29, 2026
Every box in exploration 0406 is checked — 23/23 implementation, 10/10 validation — so the filename flips to `[x]`. The arc, for the record: #641/#642/#644 (port + core extraction), #648 (AI panel on desktop), #650/#651 (chrome behind injection points, first Electron mount), #652 (desktop surfaces in the shell), #653 (flag removed, bespoke shell deleted), #655 (cold-open back under the 0.52s baseline), #656 (approval-gated writes — the last blocked item). The desktop and web apps now render one shell from one module, verified live at every step. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
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.
Implements exploration 0406 — partially (8/33). See the doc in this PR for the full plan.
Why the desktop UI looked "minimal"
Desktop is not behind web; it is a different shell. Web's entire workbench — islands, explorer, panels, palette, AI chat, 6,698 LOC — lives in
apps/web/src/workbench/, where Electron structurally cannot import it. Every shell improvement since 0280 landed there and was unavailable to desktop by construction.The load-bearing discovery is that the state model is already shared:
apps/web/src/workbench/layout-tree.tsis a shim re-exporting@xnetjs/plugins, andworkspace-parity.test.tsalready guards that desktop consumes the same module. Both surfaces agree on what a workspace is; they disagree only on how to draw it.0280 named this exact fork ("Electron divergence is the tax on shell-as-code") and its open question 6 scheduled the
ShellFrameport for phase 5, which never ran.What this PR ships
The crash (user-visible)
Opening the desktop system menu — the shell's only navigation affordance — threw during render and unmounted the whole React tree to a black window.
MenuLabelwrapped Base UI'sMenu.GroupLabel, which throws unless it finds a<Menu.Group>ancestor; the simpleMenuhas no groups.A browser tab that white-screens still has a reload button. A packaged desktop window has neither, so this was a dead app recoverable only by quitting.
MenuLabelis now a plain element (real grouping lives onDropdownMenuGroup+DropdownMenuLabel)ShellErrorBoundarydegrades any future shell render failure to a recoverable panelSidebar.tsxand the@tanstack/react-routerdep Electron declares but never importsThe seam (no behaviour change)
PlatformPort/NavTarget/PlatformCapabilitiesplus the web implementation. The shell states intent — "open this node", "go to this surface" — and the host decides what that means: web pushes URLs, desktop will transition itsShellStatereducer.The node→route table moves to the host (routes are a property of the web host, not of the shell) and is now
Record<TabNodeType, …>, so a missing entry is a compile error; previously it was a runtimeconsole.warnand the click silently did nothing.Verification
Menu fix verified in the running desktop app over CDP, not just in tests:
The
MenuLabelregression test was checked against the old implementation first — it throws there, so it is not passing vacuously.Suites: 149 electron · 188 ui · 990 web-dom · typecheck clean.
What is deliberately NOT here
8/33 items. The remaining work is the actual unification — extracting
packages/workbench, mounting it in Electron, and giving the desktop agent bridge a UI.The next item is migrating 25 workbench modules (37 raw
navigatecall sites) off TanStack Router onto the port. I stopped rather than half-migrate: this repo verifies UI by driving the real app rather than with UI tests, and a partly-migrated port through web's highest-churn area is worse than none. The port therefore lands with no consumers yet — a safe intermediate state, unlike a half-migrated one.🤖 Generated with Claude Code