Files
openswarm/backend/apps/outputs/app_builder_skill.md
T

29 KiB
Raw Blame History

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

  1. Delete everything under frontend/src/ (index.tsx, app/, pages/, shared/). Vite serves frontend/index.html directly when there's no module graph to crawl, so the pre-bundle step is skipped entirely.
  2. Replace frontend/index.html with a single self-contained document. Inline <style> and <script>. Pull libraries from esm.sh / unpkg via <script type="importmap"> or plain <script src=...>.
  3. Leave frontend/package.json, frontend/vite.config.ts, run.sh, .env, meta.json alone — vite still needs them.
  4. Don't run bash backend_init.sh — lightweight mode has no backend.
  5. Agent control still works without the template: window.OPENSWARM_APP is injected by the app shell, so even here you can make the app agent-operable by calling window.OPENSWARM_APP.register({ rules, controls, getState, invoke }) from your inline <script> (see the bridge section below). This is optional, the agent can also play any app via native keyboard/mouse, but for a game/canvas a registered getState (e.g. score, alive) makes it far more reliable. Don't wire up the bridge object yourself; it already exists.

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)
├── restart.sh             # Run this to restart the app runtime (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/ (ships with a "Brewing your app" placeholder — overwrite first)
  • src/pages/about.tsx/about
  • src/pages/users/index.tsx/users
  • src/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 sx prop 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 ~1015 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';

This template is MUI v7. For layout use import Grid from '@mui/material/Grid' (the v7 Grid takes size={{ xs: 12, md: 6 }}). Do NOT import @mui/material/Grid2 or @mui/material/Unstable_Grid2 — those are v5/v6 paths and DO NOT EXIST here, they crash the build with "Failed to resolve import". When unsure a component or path exists, prefer plain Box with fl*/grid sx instead of guessing a package path.

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_PORT in both .env and .env.example. Then run bash restart.sh so the runtime restarts and brings the backend up.

You MUST NOT roll your own backend. Do not:

  • Hand-write a backend/main.py from scratch.
  • Use Flask, Django, or any framework other than the FastAPI scaffold the script gives you.
  • Install your own venv or pip install manually.
  • Edit backend/run.sh or the SubApp framework.

Persist anything the user comes back to. Your process is disposable. OpenSwarm freezes this app's process when its card closes and fully kills it after ~15 minutes idle, on quit, and on crash. A module-level list or dict is therefore data loss on a timer. The scaffold ships a durable store; use it (or your own files under backend/data/):

from backend.apps.store.store import load_store, save_store

data = load_store()              # {} on first run, never raises
data["items"] = [*data.get("items", []), new_item]
save_store(data)                 # atomic write; a kill mid-write keeps the old data

Holding state only in memory is a bug, not a style choice.

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';

The host SDK — the user's models, workflows, and agents

The workspace ships pre-wired helpers for calling the OpenSwarm HOST from inside the app: LLM completions through the user's own subscription (any provider, model selectable), listing + firing the user's workflows and reading run results, and spawning real agent cards on the canvas (optionally positioned). Read SDK.md at the workspace root before building any feature that needs intelligence, automation, or agents — the helpers are:

  • Frontend: import { llm, listWorkflows, runWorkflow, spawnAgent } from '@/openswarmHost'
  • Backend: from backend.apps.openswarm_host.openswarm_host import llm, run_workflow, spawn_agent

Auth is automatic (host-injected token); never hand-roll fetches against host routes. The SDK works in preview and installed apps; for features that must survive PUBLISHING to the public web, use window.OUTPUT_LLM / window.OUTPUT_COMPUTE below instead.

The workspace also vendors the full tool-ui widget set at src/toolui/ (data-table, chart, code-block, code-diff, terminal, geo-map, media, social-post cards, and 15 more): reach for import { DataTable } from '@toolui/components/data-table' before hand-building any table, chart, or code view. The catalog + usage patterns are in SDK.md.

Publishable AI + compute — window.OUTPUT_LLM / window.OUTPUT_COMPUTE

The FastAPI backend above runs in preview but is not hosted when an app is published to the web. For features that should keep working on a published {slug}.openswarm.host link, use these two runtime calls instead of a backend. They run on the published site (same-origin, no credentials). In the App Builder preview they throw a clear "available once published" error, preview can't run them without embedding a credential into your app, so test these by publishing.

AI (Claude): call window.OUTPUT_LLM with an Anthropic-style messages body. The model is chosen for you (a cheap default), so don't pass one.

const res = await window.OUTPUT_LLM({
  messages: [{ role: 'user', content: prompt }],
  max_tokens: 512,
});
const data = await res.json();
const text = data.content[0].text;

Data-shaping compute: put pure Python (json/math/csv/datetime only — no network, no files) in a top-level backend.py that reads input_data and assigns result, then call window.OUTPUT_COMPUTE(input):

# backend.py
result = {"total": sum(input_data["nums"])}
const out = await window.OUTPUT_COMPUTE({ nums: [1, 2, 3] }); // -> { total: 6 }

Rule of thumb: if the app should be publishable, reach for OUTPUT_LLM / OUTPUT_COMPUTE first; only use the FastAPI backend for preview-only tools or things those two can't do (it won't be there once published).


Make the app agent-operable: the OPENSWARM_APP bridge

An agent can drive this app on the user's behalf (e.g. "graph y=x^2 on my Desmos app"). It does NOT do that by clicking pixels or scraping the DOM (slow, and an app's DOM is often a bare <canvas>). Instead it reads and acts through window.OPENSWARM_APP, a bridge the template already ships for you (src/agentBridge.ts, installed before your app mounts).

You do not wire up the bridge; you register() into it. Call window.OPENSWARM_APP.register({ rules, controls, getState, invoke }) once your app's core object exists (e.g. in a mount useEffect). This is REQUIRED for every app: the runtime verifies it and the agent's first action fails loudly with BRIDGE MISSING if you forget. Pass:

  • rules (string) - what the app is and its objective, in plain prose. This is what the agent reads to understand the app (e.g. "Flappy Bird. Keep the bird airborne through the pipe gaps; the game ends on a collision.").
  • controls - an array of { name, args?, description?, keys? }, OR a function returning that array when controls are dynamic. keys is an optional human-style hint (e.g. "Space = flap"). Return only what's available now.
  • getState() - a small JSON snapshot used to verify an action landed. Keep it compact; this is the latency budget.
  • invoke(name, args) - perform the named action and return a result (or throw a string the agent will read).

Keep args shapes simple (strings, numbers, booleans, small objects). The agent only ever calls actions that controls listed; it never edits your code. When dynamic controls change, call window.OPENSWARM_APP.refresh() so the agent knows to re-read them (it bumps the __rev the agent watches).

// `calc` here is the app's own API (Desmos example); use whatever yours exposes.
function registerAgentBridge(calc: any) {
  window.OPENSWARM_APP!.register({
    rules: 'A graphing calculator. Plot and remove expressions like y=x^2 or y=sin(x).',
    controls() {
      const controls = [
        { name: 'addExpr', args: { latex: 'string' }, description: 'Add a graph expression, e.g. y=x^2' },
        { name: 'clear', description: 'Remove all expressions' },
      ];
      // Dynamic: only offer removeExpr when something is on the graph.
      if (calc.getExpressions().length > 0) {
        controls.push({ name: 'removeExpr', args: { id: 'string' }, description: 'Remove one expression by id' });
      }
      return controls;
    },
    getState() {
      return { expressions: calc.getExpressions().map((e: any) => ({ id: e.id, latex: e.latex })) };
    },
    invoke(name: string, args: any = {}) {
      if (name === 'addExpr') { const id = String(Date.now()); calc.setExpression({ id, latex: args.latex }); window.OPENSWARM_APP!.refresh(); return { id }; }
      if (name === 'removeExpr') { calc.removeExpression({ id: args.id }); window.OPENSWARM_APP!.refresh(); return { ok: true }; }
      if (name === 'clear') { calc.setBlank(); window.OPENSWARM_APP!.refresh(); return { ok: true }; }
      throw `Unknown action: ${name}`;
    },
  });
}

Real-time games need a high-level action, not per-frame controls

An agent acts in discrete tool calls separated by network + model latency (hundreds of ms to seconds). It physically CANNOT hit frame-timing, so exposing only invoke('flap') makes a reflex game like Flappy Bird understandable but unwinnable: by the time the agent decides to flap, the bird has already fallen. For anything real-time, expose a high-level action the app executes on its own tick loop, e.g. invoke('autopilot', { on: true }) that runs the optimal input internally, or invoke('setDifficulty', ...). Let the agent set intent; let the app handle the milliseconds. Non-real-time apps (tools, forms, a Spotify-style player) don't need this: their actions are already at agent cadence.


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 card's Terminal view 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 card's webview-preload bridge. Same chronological stream as [BACKEND] lines, so you can correlate cause and effect across the two halves of your stack.

Read the terminal yourself: .openswarm/terminal.log at the workspace root is a live tee of everything the Terminal pane shows — [BACKEND] / [BACKEND:stderr] stdout+stderr, [RUNTIME] events, and [FRONTEND] / [FRONTEND:warn] / [FRONTEND:error] console lines from the running app. It resets on every app (re)start. When something misbehaves, don't guess — tail -100 .openswarm/terminal.log (or grep it for error) and look at what actually happened.


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-redux
  • framer-motion — for animations
  • react-router-dom@7
  • vite-plugin-pages — file-based routing (already configured)

⚠️ Don't

  • Don't rename index.html or run.sh — the runtime needs both at fixed paths.
  • Don't edit vite.config.ts unless you know exactly why. The /api proxy and vite-plugin-pages config are load-bearing.
  • Don't write a standalone HTML file at the workspace root. There's no longer a serve/index.html endpoint 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. Use Box, 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.
  • bash restart.sh restarts the app runtime yourself — backend + vite, no user action needed. Use it after bash backend_init.sh, after editing .env, or whenever backend code must reload (uvicorn runs WITHOUT --reload, so backend edits do NOT hot-apply). Never ask the user to restart for you, and never try to kill/rerun run.sh — the harness owns the process. If restart.sh is missing (older app), mkdir -p .openswarm && touch .openswarm/restart-requested does the same thing.
  • After a restart, wait a few seconds and check .openswarm/terminal.log to confirm the boot looked clean.
  • meta.json at workspace root drives the app's name + description in the OpenSwarm sidebar and on the app's live card on the dashboard. Write it FIRST when starting a new app (see step 1 of the Quick start checklist), and revise it any time the app's purpose shifts.

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 before saying "done" (tail -50 .openswarm/terminal.log). If you see one, fix it before claiming the app is ready.

The three most common ways agent edits crash a React preview:

  1. Lost import after MultiEdit / Edit. When you delete or rename a symbol's usage inside a file but don't update the corresponding import line, the file references an undefined name at runtime. Symptom in Terminal: [FRONTEND] ReferenceError: HomeIcon is not defined or 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.

  2. Invalid hook call from a duplicate React copy. The single most common way to break a workspace. Symptom in Terminal: [FRONTEND] Cannot read properties of null (reading 'useState') AND Invalid hook call ... You might have more than one copy of React in the same app. Happens when an npm install brought in a package that bundles its own React (instead of declaring it as a peer), so there are now TWO React instances in the workspace's node_modules and the two copies' hook dispatchers can't see each other.

    Fix (one shot): from the workspace root run

    rm -rf frontend/node_modules && rm -rf frontend/.vite-cache
    

    Then run bash restart.sh. The workspace's frontend/node_modules will re-symlink to the shared warm cache on next vite boot — only ONE React copy exists across all OpenSwarm apps, so the duplicate is gone. The .vite-cache wipe is important because vite caches pre-bundled deps including the duplicate React.

    How to avoid causing it in the first place:

    • Never npm install react / react-dom — the template already has them via the symlinked warm cache.
    • Before npm install <pkg>, check the package's peerDependencies on npmjs.com. If react is listed under peerDependencies (good) install it. If react is in dependencies (bad), find a different package or pin a version known to use peer deps.
    • Common offenders: older react-pdf, react-pdf-viewer, some @react-* UI kits, anything from a tutorial published before 2020.
  3. Hooks called outside a component body or after a conditional return. useState/useEffect/useMemo must run in the same order on every render. Adding an if (...) return null BEFORE 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:

  1. WRITE meta.json FIRST, before any other tool call. Put a 1-3 word product name (Title Case) in name and a one-sentence description in description. The sidebar and the app's dashboard card show this name to the user; until you write it, both surfaces sit at "Untitled App". Don't wait until the end of the turn to fill it in, pick a name from the user's prompt and ship it now. Example: prompt "make doodle jump" → {"name": "Doodle Jumper", "description": "Endless platform-hopper inspired by Doodle Jump."}. You can revise it later if the app's purpose shifts.
  2. REPLACE frontend/src/pages/index.tsx. The starter ships with a "Brewing your app" placeholder — this is intentional, it's what the user sees between React mounting and your first edit landing, and it must disappear the moment your real home page is ready. Rewrite the whole file with your app's actual <Home> component. (There's also an even earlier inline splash in index.html that paints before any JS bundle loads — leave that alone; React's first commit clears it automatically.)
  3. Sidebar / shell is OPT-IN. Main.tsx no longer wraps pages in <AppShell>. If your app needs a sidebar (SaaS-style dashboards, multi-page apps), import AppShell from @/app/components/Layout/AppShell and wrap your page in it yourself:
    import AppShell from '@/app/components/Layout/AppShell';
    export default function Home() {
      return <AppShell><YourContent /></AppShell>;
    }
    
    Most apps DON'T want a sidebar (games, canvases, single-screen tools, previewers, full-bleed visualizations) — just render your content directly and the page will be full-bleed. Don't add a shell out of habit.
  4. Add additional pages under frontend/src/pages/.
  5. If using a sidebar, update its nav entries in frontend/src/app/components/Layout/Sidebar.tsx.
  6. Style with useClaudeTokens() and MUI's sx. The moment you type useClaudeTokens() in a file, add its import to that SAME file: import { useClaudeTokens } from '@/shared/styles/ThemeContext'; — you are rewriting pages/index.tsx from scratch (step 2), so the import the starter had is GONE. A missing import here is the #1 way a rewritten page throws "useClaudeTokens is not defined" and the whole app fails to boot.
  7. If you need a backend: bash backend_init.sh, then add a SubApp under backend/apps/<name>/.