Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
7d5289c
build: turn the repo into an npm workspaces monorepo
Sep 29, 2026
89cff8e
refactor: extract @radical/common and @radical/layout packages
Sep 29, 2026
0bca2e4
fix(layout): run the visual harness through runSmartLayoutCore
Sep 29, 2026
57ae034
feat(mcp): add an empty @radical/mcp workspace
Sep 29, 2026
c1984f9
refactor(hub): make the Architecture Hub its own app in apps/hub
Sep 29, 2026
aa8867f
refactor(wireframe): keep AI providers out of the views that render w…
Sep 29, 2026
d930563
refactor(ai): keep Studio's AI client types in Studio
Sep 29, 2026
3041cd1
refactor(packages): give @radical/common and @radical/layout explicit…
Sep 29, 2026
b0f9f9f
chore: declare Studio's d3 dependencies and share HubCategory
Sep 29, 2026
90dfc87
refactor(hub): extract the concept catalogue into @radical/hub-catalogue
Sep 29, 2026
97f03a3
build: enforce workspace boundaries in CI
Sep 29, 2026
6fa2b60
refactor(host): give Studio one typed contract with its host
Sep 29, 2026
9e190be
refactor(model): share model edits between Studio and a headless facade
Sep 29, 2026
c1d2b4c
refactor(store): take documents and AI out of the viewer's import graph
Sep 29, 2026
b495243
refactor(ui): move the shared canvas, panels and store into @radical/ui
Sep 29, 2026
c8d4ffb
chore: remove dead code
Sep 29, 2026
6fb02ec
style: remove CSS for UI that no longer exists
Sep 29, 2026
c33277a
style: move Studio-only CSS out of @radical/ui
Sep 29, 2026
32c517d
test(ui): move the store and shared component tests into @radical/ui
Sep 29, 2026
4fc4424
docs: bring the technical improvement list up to date
Sep 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion .claude/skills/radical-diagram/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: Create or edit .radical diagram files (C4 architecture models for r

# Creating .radical diagram files

A `.radical` file is a JSON document (`DiagramData` in `src/renderer/src/types/c4.ts`) that radical.tools opens directly. You can author one by hand from a structure description — no UI needed.
A `.radical` file is a JSON document (`DiagramData` in `packages/common/src/c4.ts`) that radical.tools opens directly. You can author one by hand from a structure description — no UI needed.

## Workflow

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# .radical file reference

Full schema lives in `src/renderer/src/types/c4.ts` (`DiagramData`) and `src/renderer/src/types/metamodel.ts` (`Metamodel`). This file summarises the optional sections beyond `nodes` + `relations`.
Full schema lives in `packages/common/src/c4.ts` (`DiagramData`) and `packages/common/src/metamodel/types.ts` (`Metamodel`). This file summarises the optional sections beyond `nodes` + `relations`.

## Top-level shape (`DiagramData`)

Expand Down Expand Up @@ -95,7 +95,7 @@ Available when using an extended built-in metamodel. Set `"metamodel": { "id": "

## Custom metamodel

Embed a full custom `metamodel` only when built-in presets do not provide the needed types. It needs `id`, `name`, `nodeTypes`, and `relationTypes`. Each node type needs `id`, `label`, `color`, `fg`, `iconPath` (16×16 SVG path), `width`, and `height`; add optional `collapsedWidth`, `collapsedHeight`, `allowedParents`, `allowedAtRoot`, and `properties` as needed. Each relation type needs `id`, `label`, and `allowedPairs: [{from,to}]`. Use `src/renderer/src/types/metamodel.ts` as the authoritative shape.
Embed a full custom `metamodel` only when built-in presets do not provide the needed types. It needs `id`, `name`, `nodeTypes`, and `relationTypes`. Each node type needs `id`, `label`, `color`, `fg`, `iconPath` (16×16 SVG path), `width`, and `height`; add optional `collapsedWidth`, `collapsedHeight`, `allowedParents`, `allowedAtRoot`, and `properties` as needed. Each relation type needs `id`, `label`, and `allowedPairs: [{from,to}]`. Use `packages/common/src/metamodel/types.ts` as the authoritative shape.

## Collapsed sizes

Expand Down
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@ jobs:
node-version: 24
cache: npm
- run: npm ci
- run: npm run check:workspaces
- run: npm run typecheck
- run: npm test
39 changes: 24 additions & 15 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ jobs:
- name: Install dependencies
run: npm ci

- name: Check workspace boundaries
run: npm run check:workspaces

- name: Type-check
run: npm run typecheck

Expand All @@ -35,9 +38,14 @@ jobs:

- name: Build web renderer
run: npm run build:web
# Output: out/renderer/{index.html,hub.html,hub-data.json} + out/renderer/assets/**
# + out/renderer/hub/{index.json,<category>/<id>.radical}
# index.html → studio.radical.tools, hub.html → hub.radical.tools (same bundle)
# Output: apps/studio/out/renderer/index.html + apps/studio/out/renderer/assets/**
# → studio.radical.tools

- name: Build Architecture Hub
run: npm run build:hub
# Output: apps/hub/out/{index.html,hub-data.json} + apps/hub/out/assets/**
# + apps/hub/out/hub/{index.json,<category>/<id>.radical}
# → hub.radical.tools

- name: Configure AWS credentials (OIDC — no long-lived keys)
uses: aws-actions/configure-aws-credentials@v4
Expand All @@ -48,15 +56,15 @@ jobs:
- name: Sync static assets to S3
run: |
# Vite content-hashes asset filenames → cache forever (immutable)
aws s3 sync out/renderer/assets/ \
aws s3 sync apps/studio/out/renderer/assets/ \
s3://${{ secrets.S3_BUCKET }}/assets/ \
--cache-control "public, max-age=31536000, immutable" \
--delete

- name: Upload index.html (no-cache)
run: |
# index.html is the SPA entry point — must never be served stale
aws s3 cp out/renderer/index.html \
aws s3 cp apps/studio/out/renderer/index.html \
s3://${{ secrets.S3_BUCKET }}/index.html \
--cache-control "no-store"

Expand All @@ -71,21 +79,22 @@ jobs:
run: |
# HTML references landing.css / style.css / theme.js as `?v=__BUILD__`.
# Stamping the commit sha busts browser + CDN caches on every deploy.
sed -i "s/__BUILD__/${GITHUB_SHA::8}/g" website/*.html
sed -i "s/__BUILD__/${GITHUB_SHA::8}/g" apps/web/*.html

- name: Sync marketing website to S3
run: |
# Static site under website/ — HTML/CSS/screenshots.
# Static site under apps/web/ — HTML/CSS/screenshots.
# HTML is uploaded separately below with no-cache headers.
aws s3 sync website/ \
aws s3 sync apps/web/ \
s3://${{ secrets.WEB_S3_BUCKET }}/ \
--delete \
--exclude "*.html" \
--exclude "package.json" \
--cache-control "public, max-age=3600"

- name: Upload website HTML (no-cache)
run: |
for f in website/*.html; do
for f in apps/web/*.html; do
aws s3 cp "$f" \
"s3://${{ secrets.WEB_S3_BUCKET }}/$(basename "$f")" \
--cache-control "no-store" \
Expand All @@ -99,18 +108,18 @@ jobs:
--paths "/index.html" "/manual.html" "/style.css" "/landing.css" "/theme.js"

# ── Architecture Hub (hub.radical.tools) ─────────────────────────
# The hub is the same renderer bundle booted from hub.html (embedded
# viewer profile). Assets are content-hashed and shared with the studio.
# Built from apps/hub (the viewer runs on Studio's canvas and panels,
# bundled in). Assets are content-hashed.
- name: Sync hub assets to S3
run: |
aws s3 sync out/renderer/assets/ \
aws s3 sync apps/hub/out/assets/ \
s3://${{ secrets.HUB_S3_BUCKET }}/assets/ \
--cache-control "public, max-age=31536000, immutable" \
--delete

- name: Upload hub index.html (no-cache)
run: |
aws s3 cp out/renderer/hub.html \
aws s3 cp apps/hub/out/index.html \
s3://${{ secrets.HUB_S3_BUCKET }}/index.html \
--cache-control "no-store" \
--content-type "text/html; charset=utf-8"
Expand All @@ -119,7 +128,7 @@ jobs:
run: |
# hub/index.json + one .radical (JSON) document per concept.
# Served as JSON so fetch() content-type checks pass for .radical.
aws s3 sync out/renderer/hub/ \
aws s3 sync apps/hub/out/hub/ \
s3://${{ secrets.HUB_S3_BUCKET }}/hub/ \
--delete \
--cache-control "no-store" \
Expand All @@ -128,7 +137,7 @@ jobs:
- name: Upload legacy hub-data.json (no-cache)
run: |
# Single-file catalogue kept for older desktop builds.
aws s3 cp out/renderer/hub-data.json \
aws s3 cp apps/hub/out/hub-data.json \
s3://${{ secrets.HUB_S3_BUCKET }}/hub-data.json \
--cache-control "no-store" \
--content-type "application/json; charset=utf-8"
Expand Down
13 changes: 7 additions & 6 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ on:
- 'v*'
pull_request:
paths:
- electron-builder.yml
- apps/studio/electron-builder.yml
- apps/studio/package.json
- .github/workflows/release.yml
- build/**
- apps/studio/build/**
workflow_dispatch:

permissions:
Expand All @@ -37,7 +38,7 @@ jobs:
- name: Tag matches package.json version
if: startsWith(github.ref, 'refs/tags/v')
run: |
PKG="v$(node -p "require('./package.json').version")"
PKG="v$(node -p "require('./apps/studio/package.json').version")"
if [ "$PKG" != "$GITHUB_REF_NAME" ]; then
echo "::error::Tag $GITHUB_REF_NAME does not match package.json version $PKG"
exit 1
Expand Down Expand Up @@ -71,9 +72,9 @@ jobs:
with:
name: installers-${{ runner.os }}
path: |
dist/*.dmg
dist/*.exe
dist/*.AppImage
apps/studio/dist/*.dmg
apps/studio/dist/*.exe
apps/studio/dist/*.AppImage
if-no-files-found: error
retention-days: 14

Expand Down
20 changes: 8 additions & 12 deletions .github/workflows/vscode-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
cache: npm

# ── Quality gate ────────────────────────────────────────────────────────
- name: Install root dependencies
- name: Install dependencies (all workspaces)
run: npm ci

- name: Type-check
Expand All @@ -47,42 +47,38 @@ jobs:
# ── Build web renderer ───────────────────────────────────────────────────
- name: Build web renderer
run: npm run build:web
# Produces: out/renderer/index.html + out/renderer/assets/**
# Produces: apps/studio/out/renderer/index.html + apps/studio/out/renderer/assets/**

# ── Generate icon (idempotent) ───────────────────────────────────────────
- name: Generate extension icon
run: node tools/generate-icon.js

# ── Extension ────────────────────────────────────────────────────────────
- name: Install extension dependencies
working-directory: tools/vscode-radical
run: npm ci

- name: Bundle web renderer into extension
working-directory: tools/vscode-radical
working-directory: apps/vscode
run: npm run bundle-webview
# Copies out/renderer → tools/vscode-radical/webview/
# Copies apps/studio/out/renderer → apps/vscode/webview/

- name: Compile extension
working-directory: tools/vscode-radical
working-directory: apps/vscode
run: npm run compile

# ── Package (always) ─────────────────────────────────────────────────────
- name: Package VSIX
working-directory: tools/vscode-radical
working-directory: apps/vscode
run: npx @vscode/vsce package --no-dependencies

- name: Upload VSIX artifact
uses: actions/upload-artifact@v4
with:
name: vscode-radical-vsix
path: tools/vscode-radical/*.vsix
path: apps/vscode/*.vsix
retention-days: 30

# ── Publish (skipped on dry-run) ─────────────────────────────────────────
- name: Publish to VS Code Marketplace
if: ${{ !inputs.dry_run }}
working-directory: tools/vscode-radical
working-directory: apps/vscode
env:
VSCE_PAT: ${{ secrets.VSCE_PAT }}
run: npx @vscode/vsce publish --no-dependencies --packagePath *.vsix
11 changes: 8 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,13 @@ out/
.playwright-mcp/

# Visual layout harness output (regenerated locally; large binaries)
tests/visual/out-*.svg
tests/visual/out-*.png
packages/layout/tests/visual/out-*.svg
packages/layout/tests/visual/out-*.png

# VS Code extension: the webview is the Studio web build, copied in by
# `npm run bundle-webview -w vscode-radical`; packaged .vsix files are CI artifacts
apps/vscode/webview/
*.vsix

# Editor / OS
.vscode/
Expand All @@ -24,5 +29,5 @@ infra/tfplan
infra/override.tf
infra/override.tf.json
# Note: infra/.terraform.lock.hcl IS committed (pins provider versions)
# Note: hub/**/*.radical (repo root) IS committed — it is the canonical
# Note: packages/hub-catalogue/catalogue/**/*.radical IS committed — it is the canonical
# concept catalogue served at hub.radical.tools/hub/ (index.json is generated)
50 changes: 37 additions & 13 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Thank you for your interest in contributing! This document explains how to get i
- **Request features** — open a [feature request](https://github.com/radicaltools/radical.tools/issues/new?template=feature_request.md)
- **Submit code** — fork the repo and open a pull request
- **Improve documentation** — fix typos, clarify explanations, add examples
- **Share architecture patterns** — contribute to the [Architecture Hub](https://hub.radical.tools)
- **Share architecture patterns** — contribute to the [Architecture Hub](https://hub.radical.tools) by adding a `.radical` concept under `packages/hub-catalogue/catalogue/`

## Development setup

Expand All @@ -25,20 +25,44 @@ npm run typecheck # Type-check

## Project structure

npm workspaces monorepo; run commands from the repo root.

```
src/
main/ — Electron main process
preload/ — Electron preload bridge
renderer/
src/
ai/ — AI provider integrations and query language
components/ — React UI components
hooks/ — Custom React hooks
store/ — Zustand state management
types/ — Shared TypeScript types
tests/ — Vitest unit tests
apps/
studio/ — Radical Studio (Electron + web) and the Hub viewer
src/main/ — Electron main process
src/preload/ — Electron preload bridge
src/renderer/src/
ai/ — AI chat, providers and Forge
components/ — Studio's own UI (toolbar, extra views, modals)
persistence/ — Autosave between the diagram store and documents
store/ — documentStore
tests/ — Vitest unit tests
vscode/ — VS Code extension
hub/ — Architecture Hub read-only viewer
mcp/ — MCP server (empty for now)
web/ — Marketing site (radical.tools)
packages/
common/ — @radical/common: model types, metamodels, formats, AI tools (no UI deps)
layout/ — @radical/layout: Smart Layout and headless layout engines
ui/ — @radical/ui: canvas, panels, views and the diagram store (Studio + Hub)
host-bridge/ — @radical/host-bridge: Studio ↔ host contract (Electron, VS Code, browser)
hub-catalogue/ — @radical/hub-catalogue: Hub concepts (.radical files), validation, Vite plugin
```

## Workspace boundaries

`npm run check:workspaces` (run in CI) enforces these rules:

- Packages (`packages/*`) depend only on other packages. Apps (`apps/*`) depend
on packages, never on each other. The exceptions are listed with a reason in
`tools/check-workspaces.mjs`.
- Reach another workspace through its package name and the entry points in
its `exports`, never through a relative path.
- Declare every package you import in your workspace's `package.json`.
- Every workspace with `src/` has a `typecheck` script, and every workspace
with tests has a `test` script.

## Daily workflow (for maintainers)

The `main` branch is protected — direct pushes are rejected. All changes go through a PR.
Expand Down Expand Up @@ -76,7 +100,7 @@ Branch naming: `feature/<name>`, `hotfix/<name>`, `docs/<name>`.

- TypeScript strict mode; no `any` unless unavoidable
- React functional components with hooks
- State via Zustand + Immer slices in `src/renderer/src/store/`
- State via Zustand + Immer: the diagram store in `packages/ui/src/store/`, documents in `apps/studio/src/renderer/src/store/`
- Tests live in `tests/` and use Vitest

## Commit style
Expand Down
Loading
Loading