16 KiB
App Builder — Platform Reference
You are building an App inside OpenSwarm. The workspace you're working
in is a React 18 + TypeScript + Vite project (with an optional FastAPI
backend you can opt into on demand). It's served live to a webview, so it
behaves like a real browser tab — cross-origin fetch, popups, mic/camera,
clipboard, anything a normal web page does.
STEP 0 — pick the right shape for the app
Before writing any code, decide whether this app should be workspace
(full React/MUI, the default) or lightweight (one self-contained
index.html). Picking wrong wastes the user's time: the workspace path
spends ~10-30 s pre-bundling MUI and React on first preview, which is
pointless when the app is a 200-line Three.js demo.
Lightweight when ALL apply:
- One page, no route navigation
- No persisted server state (no DB-shaped data the user comes back to)
- No real backend logic (just CDN libraries, in-memory state)
- The whole UI is essentially one of: canvas/WebGL scene, single-file visualization (D3/Plotly/Chart.js), single-purpose tool (formatter, calculator, color picker), tiny game or simulator
Workspace (this document's default) when ANY apply:
- Multiple pages with sidebar/route navigation
- Multiple distinct UI sections with their own state
- Real backend (FastAPI endpoints, file uploads with server processing, auth, persisted user data)
- Real-time updates (WS/SSE)
- The user is likely to ask for more features later (chat, dashboards, CRUD apps — these grow)
Examples — lightweight: "rotating Three.js cube", "Pomodoro timer", "JSON formatter", "Mandelbrot explorer", "CSV → bar chart (no save)", "first-person Minecraft-style demo", "color picker", "regex tester".
Examples — workspace: "chat app", "PDF previewer with annotations", "task manager with categories", "recipe app", "weather dashboard with saved cities", "Slack-style team chat with channels".
If you're unsure, lean workspace — it's strictly more capable and the boot cost only hits once per app, then warm cache makes subsequent boots fast.
Lightweight — how
- Delete everything under
frontend/src/(index.tsx,app/,pages/,shared/). Vite servesfrontend/index.htmldirectly when there's no module graph to crawl, so the pre-bundle step is skipped entirely. - Replace
frontend/index.htmlwith a single self-contained document. Inline<style>and<script>. Pull libraries fromesm.sh/unpkgvia<script type="importmap">or plain<script src=...>. - Leave
frontend/package.json,frontend/vite.config.ts,run.sh,.env,meta.jsonalone — vite still needs them. - Don't run
bash backend_init.sh— lightweight mode has no backend.
The rest of this document covers workspace mode. If you picked lightweight, only the "Debugging" section (frontend console logs in the Terminal pane) is relevant; skip everything else.
You are NOT writing a single HTML file or vanilla JS inside a workspace. If you picked workspace mode above, match the codebase's patterns described below.
Workspace layout
workspace/
├── .env # FRONTEND_PORT, BACKEND_PORT (NONE by default)
├── .env.example # Mirror of .env (LLM-consistency — edit both
│ # when you change either)
├── run.sh # OpenSwarm's runtime spawns this; you don't
├── backend_init.sh # Run this when you need a backend (see below)
├── SKILL.md # This document
└── frontend/
├── package.json # React 18, MUI v7, Redux Toolkit, Framer
│ # Motion, react-router v7
├── vite.config.ts # Vite config — DO NOT edit unless you know why
├── tsconfig.json # `@/*` → `src/*` path alias
├── index.html
└── src/
├── index.tsx # ReactDOM entry; mounts <Main />
├── app/
│ ├── Main.tsx # Redux + Theme + BrowserRouter + AppShell
│ └── components/
│ └── Layout/
│ ├── AppShell.tsx # Sidebar + scrollable content
│ └── Sidebar.tsx # Nav, theme toggle
├── pages/ # FILE-BASED ROUTING — see below
│ ├── index.tsx # /
│ └── health.tsx # /health
└── shared/
├── hooks.ts # useAppDispatch, useAppSelector
├── state/
│ ├── store.ts # Redux store config
│ ├── tempStateSlice.ts # Sample slice — replace or extend
│ └── API_ENDPOINTS.ts # ALL backend URL constants
└── styles/
└── ThemeContext.tsx # Design tokens — USE THESE
If a backend is enabled (after bash backend_init.sh), you'll also have:
└── backend/
├── pyproject.toml # FastAPI + typeguard (+ swarm_debug)
├── main.py # FastAPI app entry — registers SubApps
├── apps/ # Each feature is a SubApp
│ └── health/
│ └── health.py # GET /api/health/check
└── config/Apps.py # SubApp / MainApp plugin framework
File-based routing
vite-plugin-pages auto-registers every .tsx file under frontend/src/pages/
as a route. You don't touch any router config. Just create the file.
src/pages/index.tsx→/src/pages/about.tsx→/aboutsrc/pages/users/index.tsx→/userssrc/pages/users/[id].tsx→/users/:id(dynamic segment)src/pages/users/$id.tsx→/users/:id(alternate dynamic syntax, same plugin)
Each page is a default-exported React component:
// src/pages/about.tsx
export default function About() {
return <Box sx={{ p: 4 }}>About this app</Box>;
}
Add a sidebar link via frontend/src/app/components/Layout/Sidebar.tsx.
Styling — MUST use the design token system
The template ships a complete design system at frontend/src/shared/styles/ThemeContext.tsx.
Use tokens via the useClaudeTokens() hook (or whatever the template exposes — check the file).
Don't hand-roll hex colors or pixel values.
Patterns:
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
export default function Card() {
const c = useClaudeTokens();
return (
<Box sx={{
bgcolor: c.bg.surface,
border: `1px solid ${c.border.subtle}`,
borderRadius: 2,
p: 3,
}}>
<Typography variant="h2" sx={{ color: c.text.primary }}>
Hello
</Typography>
</Box>
);
}
- Use MUI components (
Box,Typography,Button,IconButton,Tooltip,Stack, etc.) — never write raw<div>for layout. - Use the
sxprop for styles, not separate CSS files. - Don't add Tailwind, Bootstrap, or any other CSS framework.
MUI imports — ALWAYS use path imports, NEVER barrel imports
This is non-negotiable. Vite pre-bundles every entry in a barrel import,
which means a single import { Button } from '@mui/material' forces Vite
to optimize 200+ MUI sub-modules — adding ~10–15 seconds to every cold
boot of the workspace's preview. MUI's own performance guide
(https://mui.com/material-ui/guides/minimizing-bundle-size/) recommends
path imports for exactly this reason.
// ✅ DO — path imports, one per component
import Button from '@mui/material/Button';
import Box from '@mui/material/Box';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
// ❌ DON'T — barrel imports drag in all of @mui/material
import { Button, Box, Stack, Typography } from '@mui/material';
Same rule for icons — even more important there because
@mui/icons-material re-exports thousands of SVG components:
// ✅ DO
import AddIcon from '@mui/icons-material/Add';
import DeleteIcon from '@mui/icons-material/Delete';
// ❌ DON'T
import { Add, Delete } from '@mui/icons-material';
Icon discipline: keep icon imports to the minimum the UI actually uses. If a page only needs 4 icons, import 4 — don't pre-import 20 for "maybe later." Each icon import is another module Vite has to pre-bundle on first boot.
Check frontend/DESIGN.md for the complete design system spec.
State management — Redux Toolkit
Store is at frontend/src/shared/state/store.ts. Add new slices following
the tempStateSlice.ts pattern (createSlice, named action creators, register
the reducer in the store).
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
function MyComponent() {
const items = useAppSelector(s => s.myFeature.items);
const dispatch = useAppDispatch();
// ...
}
For server data, use plain async thunks (createAsyncThunk) or fetch
directly inside useEffect — no react-query in the template (yet).
Backend — opt-in, never roll your own
The workspace starts without a backend. If your app needs server-side code (API endpoints, secrets, server-managed state):
bash backend_init.sh
This script COPIES the canonical backend scaffold (FastAPI + SubApp pattern
- swarm-debug pre-installed) into your workspace, allocates a free port,
and flips
BACKEND_PORTin both.envand.env.example. Then hard- reload the preview (right-click the reload button) so the runtime restarts and brings the backend up.
You MUST NOT roll your own backend. Do not:
- Hand-write a
backend/main.pyfrom scratch. - Use Flask, Django, or any framework other than the FastAPI scaffold the script gives you.
- Install your own venv or
pip installmanually. - Edit
backend/run.shor the SubApp framework.
Adding a new endpoint is just adding a new SubApp:
# backend/apps/jobs/jobs.py
from contextlib import asynccontextmanager
from backend.config.Apps import SubApp
from swarm_debug import debug
@asynccontextmanager
async def jobs_lifespan():
debug("jobs SubApp lifespan starting")
yield
jobs = SubApp("jobs", jobs_lifespan)
@jobs.router.get("/list")
async def list_jobs():
return {"jobs": [...]}
Then register it in backend/main.py:
from backend.apps.jobs.jobs import jobs
main_app = MainApp([health, jobs])
Routes are auto-prefixed: jobs.router.get("/list") becomes
GET /api/jobs/list — accessible from the frontend at fetch('/api/jobs/list').
Frontend ↔ Backend wiring
Vite proxies /api/* calls from the frontend to the workspace's own
backend (on BACKEND_PORT). Always call /api/... from frontend code
— never hardcode localhost:<port>. The proxy is configured in
vite.config.ts and reads BACKEND_PORT from .env automatically.
// frontend/src/pages/jobs.tsx
import { useEffect, useState } from 'react';
export default function Jobs() {
const [jobs, setJobs] = useState([]);
useEffect(() => {
fetch('/api/jobs/list')
.then(r => r.json())
.then(data => setJobs(data.jobs));
}, []);
return <>{/* render jobs */}</>;
}
Keep ALL backend URL paths in frontend/src/shared/state/API_ENDPOINTS.ts
so refactors are one-file edits:
export const JOBS_LIST = '/api/jobs/list';
Debugging — use swarm_debug, not print()
The backend has swarm_debug pre-installed. It's a colored frame-aware
logger that lands in the App Builder's Terminal tab under [BACKEND].
from swarm_debug import debug
debug(value) # [endpoint_name] : value = ...
debug(a, b, c) # logs all three with labels
debug(err) # red + ❌ if variable is an exception
See the swarm-debug Logger built-in skill (Skills page) for the full
reference. print() works too but lacks the variable-name inference and
colorization.
Frontend console.log/warn/error calls land in the Terminal pane under
[FRONTEND] via the App Builder's webview-preload bridge. Same chronological
stream as [BACKEND] lines, so you can correlate cause and effect across
the two halves of your stack.
Adding npm packages
Just npm install <package> in the workspace's frontend/ directory.
Vite picks it up on the next HMR cycle.
cd frontend && npm install lodash @types/lodash
Then import normally — Vite resolves it.
Common deps already in the template:
@mui/material,@mui/icons-material— use these for any UI primitive@reduxjs/toolkit,react-reduxframer-motion— for animationsreact-router-dom@7vite-plugin-pages— file-based routing (already configured)
⚠️ Don't
- Don't rename
index.htmlorrun.sh— the runtime needs both at fixed paths. - Don't edit
vite.config.tsunless you know exactly why. The/apiproxy andvite-plugin-pagesconfig are load-bearing. - Don't write a standalone HTML file at the workspace root. There's no longer a
serve/index.htmlendpoint for new-mode workspaces — the webview points at Vite's dev server. - Don't hand-roll a backend. Use
bash backend_init.sh. - Don't bypass MUI with raw
<div>+ custom CSS. UseBox,Stack,sx. - Don't hardcode
localhost:<port>. Use relative/api/...paths so the Vite proxy handles routing.
Workflow tips
- Edits are auto-saved. As soon as you write a file via the Edit/Write tool, it's on disk. Vite HMR re-renders the preview within ~100ms.
- Hard Reload (right-click the reload button) restarts the runtime — useful after you
bash backend_init.shor change.envvalues. meta.jsonat workspace root is shown in the OpenSwarm Apps page UI. Update itsnameanddescriptionwhen the app's purpose changes.
Verify before declaring done — runtime errors are silent in the preview
The preview iframe is wrapped in an ErrorBoundary that surfaces React
runtime errors as a visible red error card AND mirrors the error into
the Terminal pane as a [FRONTEND] line tagged [openswarm:app-error].
After substantial edits — especially anything that touches imports,
hooks, or React state — always check the most recent [FRONTEND]
lines in your Terminal output before saying "done". If you see one,
fix it before claiming the app is ready.
The three most common ways agent edits crash a React preview:
-
Lost import after MultiEdit / Edit. When you delete or rename a symbol's usage inside a file but don't update the corresponding
importline, the file references an undefined name at runtime. Symptom in Terminal:[FRONTEND] ReferenceError: HomeIcon is not definedor similar. Always re-read the imports block of any file you edited and confirm every imported name is still used and every used name is still imported. -
Invalid hook callfrom a duplicate React copy. Runningnpm install reactornpm install some-package-that-bundles-reactinside the workspace adds a second React tonode_modules, and the two copies' hook dispatchers can't see each other → everyuseStatecall throws on mount. The template'snode_modulesis symlinked to a shared warm cache; only add packages whosepeerDependenciesdeclare a non-bundled React. If you seeCannot read properties of null (reading 'useState'), suspect a duplicate React first. -
Hooks called outside a component body or after a conditional return.
useState/useEffect/useMemomust run in the same order on every render. Adding anif (...) return nullBEFORE a hook, or calling a hook inside a callback, breaks the rule. The ErrorBoundary will print the offending component name in the surfaced stack — start there.
When in doubt, read the file you just edited end-to-end one more time. Re-reading is cheap; sending a half-broken preview back to the user is not.
Quick start checklist
When making a new app from scratch:
- Replace
frontend/src/pages/index.tsxwith your home page. - Add additional pages under
frontend/src/pages/. - Add a sidebar nav entry in
frontend/src/app/components/Layout/Sidebar.tsx. - Style with
useClaudeTokens()and MUI'ssx. - If you need a backend:
bash backend_init.sh, then add a SubApp underbackend/apps/<name>/. - Update
meta.jsonwith the app's name + description.