Two dependency problems, and only one of them was real. ── Real: two majors of the same library ── The root asked for `react-resizable-panels@^4.0.15` (4.6.5) while `src/workspaces/components` asked for `^2.1.7` (2.1.9). The workspace code targets 2.x, whose API differs — v4 has no `PanelGroup` export, no `order` prop and no `ref` on `Panel` — so a file that resolved the root's copy failed to typecheck in a way that read like a broken extraction. Nothing imported the root's version. It was vestigial, and removing it collapses the repository to one: every importer now resolves 2.1.9 and the lockfile has zero v4 entries. ── Not real: the second React ── Declaring react/react-dom in the package created `src/workspaces/workspaces/node_modules/react`, which looks exactly like the duplicate-instance bug that silently breaks hooks. It is not one. Both locations resolve to the SAME file in bun's store — the local entry is a symlink — which I established by resolving the specifier from each place instead of reading directory names, having already reported it as a hazard on the strength of an `ls`. Declared as optional peers regardless: that is the right metadata for a consumer, and it stops bun installing peers it does not need to. Notes corrected to match, since the wrong version of that story was written down. tsgo clean, package 114/114, full suite 950 pass / 7 fail unchanged, frontend builds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
4.7 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 — which turned out not to be one. Declaring react/react-dom created
src/workspaces/workspaces/node_modules/react, and a directory of that name looks exactly like the
duplicate-instance bug that breaks hooks. It was not: both locations RESOLVE to the same file in bun's
store (.bun/react@19.2.4/…/react/index.js), because the local entry is a symlink. Checked by resolving
the specifier from both places rather than by reading directory names.
They are declared as peerDependencies with peerDependenciesMeta.optional anyway — correct metadata for
a consumer, and it keeps bun from installing peers it does not need to.
Two versions of react-resizable-panels — real, and now fixed. The root asked for ^4.0.15 (4.6.5)
while src/workspaces/components asked for ^2.1.7 (2.1.9), and this code targets 2.x, whose API differs
(v4 has no PanelGroup export, no order prop, no ref on Panel). Inheriting the root's resolution
produced a screenful of type errors that read like a broken extraction.
Nothing imported the root's copy — it was vestigial. Removed, so the repository now carries one version and the lockfile has zero v4 entries.
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.