Repository navigation
Turn the repo into an npm workspaces monorepo - #106
Merged
Merged
Conversation
Move each product into its own workspace so shared code can later be
extracted into packages/ without splitting repositories:
- apps/studio Studio (Electron + web) and the Hub viewer, with its tests,
build configs and tools
- apps/vscode VS Code extension (was tools/vscode-radical)
- apps/web marketing site (was website/)
- content/hub Hub concept catalogue (was hub/)
Root scripts (dev, build, build:web, dist, test, typecheck) forward to the
workspaces, so CI commands stay the same; workflows point at the new
output paths.
The Studio package keeps the name "radical-model": Electron derives the
userData directory from it, and renaming it would hide users' saved
documents. Electron is pinned to the locked 29.4.6 because
electron-builder cannot resolve a version range from hoisted node_modules.
The extension no longer commits its webview build or .vsix files; CI
copies the Studio web build in at package time.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Move the UI-free parts of Studio into workspace packages so the MCP server and other tools can reuse them: - @radical/common: C4 and metamodel types, hub document format, Structurizr import, markdown-folder persistence, Gherkin export, the query language, DiagramFacade and the AI tool catalogue. No dependencies; type-checks without the DOM lib. - @radical/layout: Smart Layout and the headless engines (ELK ensemble, Radical layout, crossing minimisation, annealing, port allocation, edge routing). Depends on @radical/common and elkjs only. To make the boundary hold: - HubImportRecord moves from hubStore into hubFormat, so c4.ts no longer imports the store. - The layout's node sides use their own Position type with reactflow's string values instead of importing reactflow; RelationEdge casts at the reactflow boundary. - The browser download for Gherkin files stays in Studio. - Tests that only exercise package code move with it; two latent type errors in them are fixed now that packages type-check their tests. Packages ship .ts source (no build step) and Studio lists them as devDependencies, so they are bundled into the renderer and stay out of app.asar. hubCatalogue.ts imports hubFormat by relative path because the Vite config loader cannot run a bare-imported .ts package. Smart Layout output is byte-identical before and after the move on 25 fixtures (fintech sample, all hub blueprints and patterns). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
runSmartLayout moved into the Web Worker runner in #105, so the harness crashed on import. runSmartLayoutCore takes the same arguments and returns the same result shape. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Placeholder for the MCP server, wired to @radical/common and @radical/layout. The README records the plan: serve the shared AI tool catalogue over a headless DiagramFacade. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Hub was a second HTML entry in Studio's bundle. It is now a workspace
of its own with its own Vite build and deploy output:
- apps/hub/catalogue the concept catalogue (was content/hub)
- apps/hub/src the read-only viewer (HubApp, landing, nav, routing,
concept-to-diagram mapping)
- apps/hub/tools the catalogue plugin that validates it and emits
hub/index.json and hub-data.json
- apps/hub/tests viewer and catalogue tests
The viewer still runs on Studio's canvas, panels and stores. Studio exposes
them through one narrow entry point, `radical-model/viewer` (plus
`radical-model/styles.css`), so the coupling is explicit and marks what a
later packages/ui extraction has to carry.
Studio no longer builds hub.html. It still bundles the catalogue under hub/
so Hub import works offline and in development. deploy.yml now builds the
Hub with `npm run build:hub` and uploads apps/hub/out.
Hub tests are not type-checked, matching Studio; hubCatalogue.test.ts has
latent type errors that predate the move.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ireframes The canvas, Wiki and properties panel only need to sanitise and display a mockup's stored SVG, but they imported it from ai/mockupWireframe, which pulls in every AI provider adapter. That put the Anthropic, OpenAI and Gemini clients into the read-only Hub bundle. - sanitizeWireframeSvg, wireframeDataUri and the size constants move to @radical/common/wireframe: they operate on model content and have no dependencies. - MockupWireframe loads the generator with a dynamic import when the user clicks Generate, so Vite emits it as a separate chunk. The Hub's main bundle no longer contains any provider endpoint. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@radical/common held the whole ai/types module: provider ids, AISettings, provider adapters and the chat wire format. Those describe Studio's LLM client. The shared tool catalogue only needs ToolDef, and an MCP server is called by a model rather than calling one. ToolDef moves next to the tool handlers in @radical/common/ai/tools; the rest of ai/types moves back to apps/studio and re-exports ToolDef. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… public APIs Both packages exported "./*", so every module was public, including Smart Layout internals such as the annealing stage and score weights. exports now lists exactly the entry points apps use. Package tests import internals by relative path. Smart Layout output is unchanged on the 25 fixtures. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
liveColaLayout imports d3-dispatch, d3-drag and d3-timer, which only resolved because reactflow's copies were hoisted. They are now declared at the versions already in the lockfile. hubTheme defined its own copy of HubCategory; it now re-exports the one from @radical/common/hubFormat. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Studio and the Hub depended on each other: the Hub imported Studio's viewer
kit, and Studio's Vite configs and tests reached into apps/hub for the
catalogue and its plugin. The catalogue is now a package both apps use:
- packages/hub-catalogue/catalogue the .radical concepts
- @radical/hub-catalogue readCatalogue / validateCatalogue /
buildIndex / CATALOGUE_DIR
- @radical/hub-catalogue/vite hubCataloguePlugin(), defaulting to the
package's own catalogue
The `./vite` entry is a small CommonJS file that loads the TypeScript
plugin through tsx. The Vite and electron-vite config loaders hand bare
imports to Node, which cannot run .ts; this replaces the relative import
into packages/common/src.
The Hub's remaining links to Studio now go through the package too:
viewer.ts references the preload globals it depends on, and the test
setup is exported as `radical-model/testing/setup`.
The "studio round-trip" test checks Studio's store, so it moves to
apps/studio. The catalogue tests are type-checked now; their loosely typed
relation fields get explicit String() conversions.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tools/check-workspaces.mjs (npm run check:workspaces, run in CI and before deploys) checks that: - packages depend only on packages, and apps only on packages, except the app-to-app links listed with a reason (Hub -> Studio viewer kit, VS Code extension -> Studio web build); - relative imports stay inside their workspace; - every imported package is declared; - tsconfig includes and scripts stay inside their workspace; - every workspace with source or tests has typecheck / test scripts, since the root runs them with --if-present. Imports are read with TypeScript's pre-processor rather than regexes. It found two issues, fixed here: - apps/studio/tools/generate-sample.js imports elkjs, which Studio stopped declaring when layout moved out; elkjs is back as a dev dependency. - The VS Code extension copied the Studio build through ../studio. It now declares radical-model and resolves the build through it. The extension also gets a typecheck script, so CI type-checks it for the first time. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Studio's renderer called window.electronAPI directly in 31 places. The contract was typed only in Studio's preload .d.ts, and the VS Code extension re-implemented part of it as an untyped script pasted into the webview HTML. @radical/host-bridge now defines the contract: - HostBridge / HostCapabilities, with every capability optional and failures resolved as success: false; - ./vscode: the webview <-> extension message protocol, the webview-side host, and vscodeBootScript. - Studio reaches its host only through host() in platform/host.ts. It is resolved on each call, because tests install a fake Electron API after import. The VS Code host is cached because acquireVsCodeApi() can only be called once. - The Electron preload is typed with `satisfies Required<HostCapabilities>`. It drops the generic send / on / invoke / platform members, which the renderer never used. - The extension injects only boot data (the edited file) and types its message handlers with the shared protocol. Two bugs fixed along the way: - In VS Code, a failed readFile rejected the promise, where Electron returns success: false. loadDocument threw instead of returning null. - Toolbar treated any host with electronAPI as VS Code. In the desktop app the theme always started dark and the user's choice was never saved. Verified the VS Code path in headless Chrome, with a fake acquireVsCodeApi serving a model: Studio reads the boot file, renders it and writes it back. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The rules and edits behind every model change lived inside Studio's zustand store, so the AI tool catalogue could only run against the live UI. They now live in @radical/common/model: - check* functions return the refusal Studio shows (metamodel cardinality, parent containment, allowed relation pairs), with the same wording; - insert / patch / delete functions for nodes, relations and views work on a plain object or an immer draft (defaults materialised, relation types inferred, deletes cascade to descendants, relations and view references); - documentMetamodel() resolves a document's stored metamodel. Studio's store calls them and keeps its own effects: undo, milestones, notifications, canvas sync, live layout, fitting parents. createModelFacade (@radical/common/ai/modelFacade) implements DiagramFacade over a plain DiagramData, so the MCP server can run the same tools. It records why a change was refused in lastError, and carries over the document fields it does not edit. apps/studio's modelFacadeParity test starts both facades from the same Hub blueprint and runs 16 tool calls, including three the metamodel refuses. Tool results and the resulting model must match, ignoring ids and geometry. It fails when the headless facade skips validation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The components the Hub viewer shares with Studio pulled in Studio's documents, host bridge and AI providers through the diagram store and MockupWireframe. Two seams make them optional: - store/documentBackend.ts: the store reads the document it starts from, and lazily loaded node bodies, through a backend the app configures before the store module is evaluated. Studio configures one on top of documentStore (persistence/configureDocuments.ts); the Hub configures none. - The autosave block (save on change, load on boot and on document switch, flush on page hide, follow the host's watched file) moves unchanged from the store into Studio's persistence/autosave.ts. main.tsx imports it first, and the tests load it through a second setup file, because imports run before setup.ts stubs window. - components/wireframeGeneration.ts: MockupWireframe offers generation only when the app registers a generator. Studio registers its AI-backed one (ai/wireframeGenerator.ts). The viewer entry's import closure goes from 34 files to 25, with no AI, document or host code left in it. Without the persistence setup, 4 tests fail (lazy node bodies, presenter-mode autosave), so the tests exercise the moved code. The Studio web build still seeds and renders its document in headless Chrome. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With documents and AI injected, the viewer kit's import closure no longer depends on Studio. It moves to packages/ui: the Canvas, properties panel, Wiki and Table views, node and edge renderers, the diagram and hub stores, the Smart Layout worker runner, live cola layout and the global stylesheet. - The Hub imports @radical/ui/viewer and @radical/ui/styles.css, so it no longer depends on Studio. Its app-to-app exception is removed from the workspace boundary check. The only one left is the VS Code extension, which ships Studio's web build. - Studio imports the shared parts from @radical/ui and keeps its own: documents, autosave, AI, the toolbar, extra views and modals. - The test stubs move to @radical/ui/testing/setup and both apps use them. - Studio drops immer and @radical/layout, which it no longer imports. @radical/ui exports explicit entry points and type-checks on its own, without Studio's preload globals. The Hub's main bundle contains no Electron bridge, File System Access, VS Code bridge, document index or AI provider code, and shrinks from 2.15 to 2.12 MB (most of what remains is ELK and reactflow). The Hub, the Studio web build and the simulated VS Code webview all render in headless Chrome. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nothing imports any of this: - PropertiesPanel, Sidebar and ViewBar, orphaned since 17f9ec1; TimeTravelBar and ExportMenu; DevSampleToolbar, whose removal fe346d1 announced but did not carry out. - The developer sample-override chain that only DevSampleToolbar used: loadFintechSample, the devSaveSample / devLoadSample host capabilities, their preload entries, the dev:saveSample / dev:loadSample IPC handlers, and the __DEV_SAMPLE_DATA_PATH__ define, which nothing read. - PresentationBar's SlidesColumn (PresenterDock still uses its helpers), layoutFinalize's fitParentsToChildren wrapper, and ai/types' contentToText. - hubTheme's buildHubTheme / renderHubThemeScript and the comment about a generator (tools/generate-hub-theme.ts, npm run hub:theme) that is not in the repository's history. Smart Layout output is unchanged on the 25 fixtures. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
182 rules in the shared stylesheet styled class names that appear nowhere in the code of Studio, the Hub or @radical/ui. Among them are the dynamic view shell (its components went in d7575e6), the old sidebar, view bar, slide strip, toolbar dropdowns and snapshot list. Also removed are 3 @Keyframes nothing refers to (welcome-spin, tm-zoom-in, tm-zoom-out) and 7 section comments left with no rules. index.css shrinks from 184 to 156 KB. A class counts as used when its name appears anywhere in the sources, including dynamic prefixes (`name-${x}`, 'name-' + x). The remaining stylesheet is an ordered subset of the original: no rule is changed or reordered. Computed styles of about 30,000 elements across 12 Studio and Hub screens (canvas, wiki, table, matrix, treemap, sequence, viewer, welcome; hub landing, browse, concept, blueprint) match the previous build. The only differences are sub-pixel column widths of animating panels, which also differ between two runs of the same build. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
467 rules in @radical/ui's stylesheet styled only Studio's own UI: the app layout, toolbar, document manager, Forge, the Matrix, Treemap and Sequence views, the welcome screen and modals. They move, with their section headings and 8 @Keyframes, to apps/studio/src/renderer/src/studio.css, which main.tsx loads right after @radical/ui/styles.css. The shared stylesheet goes from 156 to 85 KB, and so does the Hub's CSS bundle (166 KB before this cleanup, 86 KB now). Every rule survives exactly once: both files are ordered subsets of the original. No @Keyframes name is defined twice, so moving the @Keyframes blocks cannot change which definition wins. Checked with computed styles, transitions and animations disabled, of about 29,500 elements across 12 Studio and Hub screens. Measured against the build before the previous commit, they are identical, and two runs of the same build agree exactly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
15 test files in apps/studio exercised only @radical/ui and @radical/common: the diagram model, views, collapse, undo, sequences, milestones, presentations, presenter mode, auto-fit, connections and the hub round-trip. They move to packages/ui/tests and import the package's modules by relative path. The package gets its own vitest config on its test stubs. They pass without Studio's document persistence installed, so they did not depend on it. The 144 tests that stay in Studio cover documents, autosave, AI, routing and the host bridge. Type-checking the moved tests turned up two issues, fixed here: an Array.prototype.at call (ES2022, above the package's target) and a partial node passed to addNode, now cast explicitly. _group_collapse_debug.test.ts is renamed to groupCollapse.test.ts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Part 2 of docs/IMPROVEMENTS.md still listed work this branch finished (dead components and CSS, TimeTravelBar, the CSS split, store tests, persistence out of the store) and linked to pre-monorepo paths. It now separates what is done from what is still open, with current paths and sizes. The dynamic-views appendix is removed. It documented src/renderer/src/dynamicViews.ts and tests/dynamicViews.test.ts, which exist in no branch of the repository; sequences cover that ground in the model. Part 1 (functional ideas) is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
Summary
Turns the repository into an npm workspaces monorepo with clear boundaries between the apps and the code they share. This replaces the multi-repo plan in #98: the Hub viewer reuses Studio's canvas and store, and the VS Code extension ships Studio's web build, so separate repos would turn every change to the shared format into a publish plus version bumps in several places.
Layout
apps/studioradical-modelapps/hub@radical/hubapps/vscodevscode-radicalapps/web@radical/webapps/mcp@radical/mcppackages/common@radical/commonpackages/layout@radical/layoutpackages/ui@radical/uipackages/host-bridge@radical/host-bridgepackages/hub-catalogue@radical/hub-catalogueBoundaries
npm run check:workspacesruns in CI and fails when:@radical/commonand@radical/layoutcompile without DOM types, so browser code there fails the typecheck.Also in this branch
host()from@radical/host-bridge.createModelFacade. A parity test keeps them in step.documentBackendandwireframeGeneration.apps/studio.docs/IMPROVEMENTS.mdlists what is done and what is still open.Related issue
Supersedes #98.
Type of change
Checklist
npm testpassesnpm run typecheckpasses with no errorsHow this was checked
Every workflow was replayed on a clean clone with
npm ciand Node 24:vsce packagesucceed.npm run distbuilds both macOS DMGs. The packaged app contains the main process, preload, renderer and its runtime dependency.Not checked: the Windows and Linux installers, and the uploads to S3, CloudFront and the Marketplace.
Before merging
Merging deploys. A push to
mainruns the deploy workflow, which rebuilds and publishes studio.radical.tools, hub.radical.tools and radical.tools together.Known issues, not fixed here
These came up in a review of the new structure. None are regressions from this branch.
v*tag filter also matchesvscode-v*. The first extension release will start a desktop release that fails on the version check.v[0-9]*fixes it.nullorvscode-webview://origins, and the local/hub/fallback is an absolute path that no production build can reach.setWindowOpenHandlerpasses any URL toshell.openExternal.@radical/uistill talk throughwindowglobals such as__radicalFlushPersist, which bypassesdocumentBackend.main.🤖 Generated with Claude Code