Skip to content

feat(workbench): route the shell through the PlatformPort, not the router (0406) - #642

Merged
crs48 merged 2 commits into
mainfrom
claude/0406-workbench-port
Jul 28, 2026
Merged

feat(workbench): route the shell through the PlatformPort, not the router (0406)#642
crs48 merged 2 commits into
mainfrom
claude/0406-workbench-port

Conversation

@crs48

@crs48 crs48 commented Jul 28, 2026

Copy link
Copy Markdown
Owner

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

…uter

Migrates all 29 workbench modules and 20 app-land consumers off direct
@tanstack/react-router imports onto the PlatformPort (0406 phase 1). 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.

What moved where:
- 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 sections, /ai?q=) and a richer
  Link (drag sources, testids) — requirements the 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.
- test-platform.tsx replaces router module mocks in tests: components stopped
  importing the router, so tests stop mocking it and provide a port.

Verified by driving the live app: doc creation, settings ?section round-trip
(write and read), working-set opens, profile-menu settings, history
back/forward — zero console errors. One palette quirk (Enter on "Go to tasks"
under synthetic input) reproduced identically on the pre-migration baseline
via stash, so it is pre-existing, not a regression.

992 web tests green, typecheck clean, lint 0 errors.

Signed-off-by: xNet Test <test@xnet.dev>
@crs48 crs48 added the skip-changelog Exclude this PR from the changelog label Jul 28, 2026
@crs48
crs48 temporarily deployed to pr-642 July 28, 2026 16:43 — with GitHub Actions Inactive
@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

✓ Changelog fragment found — thanks!

@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

🖼️ UI changes in this PR

Screens

✏️ CRM _(SSIM 0.979)_
before after diff
before after diff
✏️ Discover _(SSIM 0.982)_
before after diff
before after diff
✏️ Home _(SSIM 0.982)_
before after diff
before after diff
✏️ Settings _(SSIM 0.965)_
before after diff
before after diff

Interactions

🎬 Open a channel and post a message

Open a channel and post a message

▶ Watch MP4

🎬 Create a page and use the editor

Create a page and use the editor

▶ Watch MP4

🎬 Build a CRM quote (product + line item)

Build a CRM quote (product + line item)

▶ Watch MP4

Auto-captured by CI · run. Informational — not a blocking check.

github-actions Bot added a commit that referenced this pull request Jul 28, 2026
github-actions Bot added a commit that referenced this pull request Jul 28, 2026
@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Preview removed for PR #642.

github-actions Bot added a commit that referenced this pull request Jul 28, 2026
… works again

The editor-ux CI failure was real: the home surface's New → Page silently did
nothing. routes/index.tsx still called the router-shaped navigateToNewDoc with
the port's navigate behind `as unknown as NavigateLike` — the cast the compiler
cannot see into, so the migration's typecheck sweep never surfaced it. At
runtime the port received a router-shaped object with no `kind`, resolved no
route, and dropped the click.

Point routes/index.tsx at the workbench's port-shaped navigateToNewDoc, and
delete the router-shaped original plus NavigateLike from lib/doc-creation —
they had no callers left, and leaving them standing invites the same silent
cast back.

Verified: the exact CI spec set (editor-ux, editor-ux-mobile, safety-ui,
desktop + mobile projects, --fail-on-flaky-tests) now passes locally against
the same 5173 setup CI uses, plus left-nav and the 992-test dom suite.

Signed-off-by: xNet Test <test@xnet.dev>
@crs48
crs48 temporarily deployed to pr-642 July 28, 2026 17:03 — with GitHub Actions Inactive
github-actions Bot added a commit that referenced this pull request Jul 28, 2026
github-actions Bot added a commit that referenced this pull request Jul 28, 2026
@crs48
crs48 merged commit b31e6e0 into main Jul 28, 2026
21 checks passed
@crs48
crs48 deleted the claude/0406-workbench-port branch July 28, 2026 17:16
github-actions Bot added a commit that referenced this pull request Jul 28, 2026
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
…650)

## Summary

The chrome phase of exploration 0406 (one shell, two surfaces): the
entire workbench shell chrome — frames, islands, menus, mobile shell,
sidebar, explorer views, time machine, drafts, calm surface — moves from
\`apps/web/src/workbench\` into \`@xnetjs/workbench\`, behind two
injection points the host app fills at boot:

- **View registry** (\`registerHostedViews\`): the shell renders tab
content through \`hostedView()\`; web registers its complete view table
in \`platform/hosted-views.tsx\` (a full \`Record<TabNodeType, …>\`, so
exhaustiveness stays a compile error). ViewHost drops all 18 view
imports.
- **WorkbenchHost contract** (\`setWorkbenchHost\`): the app services
the chrome consumes but cannot define — workspace data hooks (spaces,
tags, storage, requests, node actions), comms presence, the Today-panel
experiments API, and host-owned components (ShareDialog, GlobalSearch,
WorkspaceCommands, SelfAvatar, coachmarks, error fallback…). Members are
narrowed to what the chrome actually reads, so the desktop host will
implement the minimum. Web binds its existing modules onto the contract
in \`platform/workbench-host.tsx\` — no new behaviour, one file that
knows both names.

Pure modules moved outright (with shims at the old paths, 0280 pattern):
format-bytes, settings-sections, saved-view-registry, tag-view-data,
data-runtime, explorer-items, the source-stamp browser half,
\`DOC_TYPE_META\` (the label/icon half of \`DOC_TYPE_ROUTES\`), and
\`AGENT_LAYOUT_EVENT\`.

What deliberately did **not** move: \`inspect/\` (dev-only
point-and-change overlay, coupled to devkit), and every app component
that binds the router or app data — those stay host-side behind the
contract.

Boot-order rule enforced throughout: **no \`workbenchHost()\` call at
module scope** — the barrel evaluates before the app's boot registration
runs, so module-scope registration (Workbench's builtin views,
WorkspaceSwitcher's coachtips) became lazy-once on first render, and
reading before registration throws loudly instead of rendering an empty
shell.

## Verification

- \`turbo run typecheck\` green across web, desktop, workbench (44
tasks)
- 992 dom tests + 155 electron tests green; \`pnpm lint\` clean
- Live-driven on the dev server: shell boots with zero console errors;
New menu renders all DOC_TYPE_META entries; **New → Page navigates**
(the #642 regression path); ShareDialog opens on a doc; Chats surface
renders through the host wrapper; status bar (sync chips, storage bytes,
What's New) all live
- editor-ux spec set run locally: 7 passed; the one mobile failure
reproduces identically against a baseline \`main\` server — pre-existing
local-env issue, not this change

Part of exploration 0406 (now 15/23 implementation). Next: mount
\`<Workbench/>\` in Electron behind \`XNET_UNIFIED_SHELL\`.

🤖 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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-changelog Exclude this PR from the changelog

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant