Architecture

How the pieces fit together. The short version: one audio scheduler shared globally, state in Zustand, AI behind a rate-limited API, every code change is a proposal until the user confirms.

Components

  • Editor

    CodeMirror 6 · @strudel/codemirror (StrudelMirror)

    Where code is written and evaluated

    Autocomplete, hover docs, multi-cursor, vim/vscode/emacs keybindings. onChange is wired to the Zustand store so code stays in sync with persistence.

  • Audio scheduler

    @strudel/core Cyclist · Web Audio API

    Single global clock, shared across the app

    The scheduler is module-level, not per-component. Navigating /radio → /studio doesn't stop audio — the pattern keeps playing until something replaces it. That's what enables 'live from radio' sync.

  • Zustand store

    zustand with persist middleware

    State for the whole studio session

    Persists: mode, code, trackTitle, editor settings, DJ deck. Not persisted: playing flag, broadcast state (radio). localStorage key: pc.store.

  • AI backend

    Vercel AI SDK · @ai-sdk/anthropic · @ai-sdk/groq

    Compose, evolve, chat

    Routes at /api/compose, /api/evolve, /api/chat, /api/patterns. Server-side keys. Rate limited per IP. Model router picks Haiku for small changes and Sonnet for composition.

  • Chat diff flow

    Custom LCS line diff · CodeDiff component

    AI changes are proposals, not commits

    AI returns JSON with { code, message }. Chat extracts the code and renders it as a diff. Listen temporarily swaps via editor.setCode + evaluate, saving a snapshot. Keep finalizes, Reject restores the snapshot.

  • DJ deck

    buildDeckComposePrompt (lib/ai/deck-prompt.ts)

    Structured prompt builder

    Turns UI state (genre + key + BPM + energy/space/brightness sliders) into a deterministic prompt string that gets passed to /api/compose.

  • Radio

    Self-contained React page · shares the global scheduler

    A page that composes and broadcasts live

    Composes on Play, evolves every 30s, writes broadcastCode/broadcastTitle/broadcastActive into the store. Studio reads these on mount to auto-tune to the live pattern. First edit forks.

  • Supabase

    @supabase/ssr · Postgres with RLS

    Auth + pattern persistence

    Graceful fallback when env vars are missing — the app still works with localStorage-only state. Auth via email / Google OAuth. Patterns are private by default; toggle Public for /p/[id] links.

A typical flow

  1. 1

    User hits Play

    Transport bar calls editor.toggle(). First-time: AudioContext resumes (user gesture), prebake promise awaits sample loading + evalScope registration.

  2. 2

    Code evaluates

    Strudel's transpiler rewrites the code, evalScope executes it, the resulting pattern is installed in the global scheduler. Hap highlighting kicks in on played notes.

  3. 3

    User types a chat message

    ChatPanel POSTs to /api/chat with { mode, currentCode }. Model router picks a model. Response streams back as JSON with { message, code }.

  4. 4

    Diff renders

    CodeDiff computes line-level LCS between current code and AI's code, renders +/-/context with line numbers. Three buttons: Listen / Keep / Reject.

  5. 5

    User clicks Listen

    Studio saves a snapshot ref, calls editor.setCode(proposed), editor.evaluate(). The scheduler swaps patterns at the next cycle boundary — no audio glitch.

  6. 6

    User clicks Keep or Reject

    Keep: snapshot cleared, proposed code stays. Reject: editor.setCode(snapshot), editor.evaluate(), scheduler restores original.

Design principles

  • State survives navigationZustand + persist means refreshing the page keeps the user where they are, with their code intact. No redirects to landing.
  • One scheduler, many viewsThe audio clock is shared. /radio and /studio aren't separate audio apps — they're two views onto the same pattern engine. That's what makes radio → studio feel seamless.
  • AI is a collaborator, not a narratorEvery code change from the AI is a diff the user can Listen to before committing. No surprises, no irreversible rewrites.
  • Fallbacks over failuresSupabase env missing? Fall back to localStorage. AI rate-limited? Return a graceful message. Audio init fails? Editor still works.
← Quick startStudio guide →