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>
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.