Files
platform/WORKSPACES-NOTES.md
T
pastilhasandClaude Opus 5 4c97e69ecd extract the workspace framework into its own package
Phase 1: the Workspace/Panel system is `src/workspaces/workspaces`, and it no longer
knows Officer exists.

The proof is its test suite. 114 tests mount `WorkspaceView` under a BARE React tree —
no react-query provider, no router, no Officer hook — which is only possible because
the host now supplies everything: a registry mapping `appType` to a component, and a
layout with a way to change it.

Officer is untouched. All 24 `WorkspaceView` sites and 8 `WorkspaceLayout` sites call
exactly what they called before; `Workspace/index.ts` became an adapter that adds back
the three things that are genuinely ours — the app registry, fullscreen persistence in
sessionStorage, and the handshake that stands the nav header down for a fullscreen
panel.

── The shape, and why it is this shape ──

`core/` is the tree and every operation on it: pure, no dependencies, no React. `react/`
is one renderer. That split is phase 2's whole cost saving — a second renderer reuses
`splitPanel`, `movePanel`, `normalizeLayout` and the rest untouched, and what will not
port is the resize INTERACTION rather than the layout engine.

It was already half done: `layout-utils.ts` imported only `./types`, and `WorkspaceView`
already received `{ key, value, setValue, isLoaded }` as a prop and always wrote through
an updater. The real coupling was the registry, fullscreen, and four UI imports.

Also deleted ~175 lines of dead code — `DragOverlay`, `LayoutEditor`, `WorkspaceHeader`
— referenced by nothing.

── Two dependency traps ──

**A second React.** Declaring react/react-dom in the package produced a second copy
under its own node_modules. Two React instances break hooks at runtime and are invisible
until something renders. The package now declares almost nothing while it lives here.

**Two versions of react-resizable-panels.** The root asks ^4.0.15 (4.6.5),
`src/workspaces/components` asks ^2.1.7 (2.1.9), and this code targets 2.x — v4 has no
`PanelGroup`, no `order`, no `ref` on Panel. Inheriting the root resolution produced a
screenful of type errors that read like a broken extraction and were a version
mismatch. Pinned; the duplication itself is worth fixing one day.

── Verified ──

tsgo clean. Package suite 114/114. Full suite 950 pass / 7 fail, unchanged. Frontend
builds, and the running app was screenshotted against the pre-extraction baseline: 9 of
16 byte-identical, the rest live data and the rotating wallpaper. Panel chrome, headers,
splitters and the grid all render as before.

Docs written as part of the work: the package README is the contract, and
WORKSPACES-NOTES.md carries the open items — panel-to-panel channels, the theme
classes the package still assumes, and phase 2.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 00:15:28 +00:00

4.5 KiB

Workspaces extraction — phase 1

Branch workspaces-extraction. The Workspace/Panel framework is now src/workspaces/workspaces, a package that knows nothing about Officer.

Where it got to

The package renders under a bare React tree. Its own test suite (114 tests) mounts WorkspaceView with no provider of any kind — no react-query, no router, no Officer hooks. That is the proof: a host needs to supply a registry and a layout, and nothing else.

Two halves. core/ is the tree and every operation on it — pure, dependency-free, no React. react/ is one renderer. Phase 2 is a second renderer over the same core.

Officer is unchanged. All 24 WorkspaceView sites and 8 WorkspaceLayout sites still call exactly what they called before. officerdev/.../Workspace/index.ts is now an adapter that supplies the three things that are Officer's: the app registry, fullscreen persistence in sessionStorage, and the handshake that hides the nav header for a fullscreen panel.

Persistence was already injected. WorkspaceView always received { key, value, setValue, isLoaded } as a prop and called setValue with an updater. The headline requirement of phase 1 was mostly already true — the coupling was elsewhere.

~175 lines of dead code deleted: DragOverlay, LayoutEditor, WorkspaceHeader, referenced by nothing.

What actually had to move

Coupling Resolution
useAppRegistry registry is now a required prop
useSessionState (fullscreen) controlled/uncontrolled pair; Officer passes its sessionStorage state
panel-fullscreen / useGlobal the package reports via callback; Officer acts
use-panel-memory / useDashboardState stayed in Officer — it is host storage, not layout
@/components/{Card,ErrorBoundary,ui/*} vendored into react/internal/
hooks/useIsMobile vendored (27 lines)
helpers/cn, @/lib/utils vendored (2 lines)

Two traps, both worth knowing before touching dependencies

A second React. Declaring react/react-dom in the package produced a second copy under src/workspaces/workspaces/node_modules. Two React instances break hooks at runtime and are invisible until something renders. While this lives in the monorepo the package declares almost nothing and inherits the root's; publishing standalone means declaring them as peers, aligned with the host.

Two versions of react-resizable-panels. The root asks for ^4.0.15 (4.6.5); src/workspaces/components asks for ^2.1.7 (2.1.9). This code was written against 2.x, whose API differs — v4 has no PanelGroup export, no order prop, no ref on Panel. Inheriting the root's resolution produced dozens of type errors that read like a broken extraction and were a version mismatch. The package pins ^2.1.7.

Worth resolving properly at some point: one repository, two majors of the same library.

Open

1. Panel-to-panel communication. usePanelChannel is a two-line wrapper over useGlobal, so it is react-query underneath. Six channels exist; three are genuine refresh signals (files:refresh-signal, preview:refresh, background-job:changed) and two are SELECTION (chat:selected-session, chat:active-session), which docs/navigation-audit.md says belongs in the URL. Deferred by decision: the mechanics will live in the package, configured from outside, probably as callbacks. The real requirement may be smaller than it looks.

2. The package still speaks Officer's theme. PanelSlot uses duck-teal classes and --panel-* CSS variables. Harmless inside Officer, wrong for a standalone module: a consumer would need those variables defined. Options are to accept a class-name map, ship a minimal stylesheet, or document the variables as part of the contract.

3. cwd/root are in the context. They are Officer concepts (a workspace points at a directory) that the package currently carries. Not harmful, not general.

4. Phase 2. A non-React renderer over core. What will not port is the resize interaction — drag handles, pointer maths, constraints — supplied today by react-resizable-panels. Either find a vanilla equivalent or write one; the tree logic comes across untouched, which was the point of the split.

Verifying

bun test src/workspaces/workspaces/ — 114 tests, no host. Full suite unchanged at 950 pass / 7 fail. Screenshots: 9 of 16 byte-identical to before the extraction, the rest being live data (chat sessions, system metrics) and the rotating wallpaper.