Configuration

Choose the routes on your canvas, Bud's screens and how Bud connects to your app.

View Markdown

Bud reads exactly one bud.config.ts or bud.config.js from your Vite root. Changing it restarts Bud.

bud.config.ts

import { defineConfig } from '@bud.sh/app'

export default defineConfig({
  name: 'Studio',
  routes: [
    '/',
    { id: 'collection', path: '/collection?view=grid', title: 'Collection' },
    { id: 'detail', path: '/collection/vase', title: 'Product' },
  ],
})

Routes

Pick a few pages that represent your product, not every URL. Each route is a string or an object:

FieldRequiredDescription
pathYesA local path starting with /. Queries and hashes work.
titleYesThe label above the artboard.
idNoA stable identity. Defaults to the path.
patternNoThe dynamic route the path renders, such as /blog/:slug.
groupNoA named group the artboard sits in on the canvas.

Use real URLs for dynamic routes: /collection/vase, not /collection/:id. A string route is titled by its path, and / is titled Home.

Dynamic routes

Routes that share a pattern take one artboard. Its label gets a menu to switch between them, and only the chosen page loads:

bud.config.ts

export default defineConfig({
  routes: [
    { id: 'blog', path: '/blog', title: 'Blog' },
    { id: 'post-launch', path: '/blog/launch', title: 'Launch', pattern: '/blog/:slug' },
    { id: 'post-roadmap', path: '/blog/roadmap', title: 'Roadmap', pattern: '/blog/:slug' },
  ],
})

The artboard sits under the pattern's static part, so the canvas keeps its shape whichever page is open. The config is code, so you can also generate the list, for example from your content folder.

Groups

The canvas arranges artboards by their paths: /blog/launch sits under /blog, joined by a line. Pages whose paths share no prefix, such as /imprint and /privacy, can still share a parent with group. Their own paths keep nesting inside it:

bud.config.ts

export default defineConfig({
  routes: [
    { id: 'imprint', path: '/imprint', title: 'Imprint', group: 'Legal' },
    { id: 'privacy', path: '/privacy', title: 'Privacy', group: 'Legal' },
    { id: 'terms', path: '/terms', title: 'Terms', group: 'Legal' },
    { id: 'terms-b2b', path: '/terms/b2b', title: 'Terms B2B', group: 'Legal' },
  ],
})

Canvas comments are stored with the route id. Give long-lived pages an explicit id before you leave feedback, so renaming a path doesn't orphan its comments.

Flows

Some screens only exist by clicking through them, such as a booking: choosing a date, adding travellers, filling in their details. A flow pictures each of them. Your script drives the page from start and awaits step(id) at each screen; Bud captures the page there, at every viewport, and shows the steps as a group right after / on the canvas and in Pages:

bud.config.ts

export default defineConfig({
  routes: ['/', '/tickets'],
  flows: [
    {
      id: 'booking',
      title: 'Booking',
      start: '/tickets/day-pass/',
      script: './bud/flows/booking.ts',
      steps: [
        { id: 'offers', title: 'Offers' },
        { id: 'travellers', title: 'Travellers' },
        { id: 'details', title: 'Details' },
      ],
      // Stopped, and the walk fails, if it ever tries to send one.
      block: ['/reservations'],
    },
  ],
})

bud/flows/booking.ts

import type { Page } from 'playwright'

export async function walk({ page, step }: { page: Page; step(id: string): Promise<void> }) {
  await page.getByRole('button', { name: 'Book' }).click()
  await step('offers')
  await page.getByRole('button', { name: 'Add traveller' }).click()
  await step('travellers')
  await page.getByLabel('First name').fill('Anna')
  await step('details')
}

A flow never books anything: list the requests that would in block. A step the walk doesn't reach shows why, and a changed script pictures its steps again. Each step is a route with the id <flow>/<step>, so its comments, differences and pictures work like any page's.

Screens

screens lists Bud's navigation in order. Leave it out to get Pages, Canvas, Tasks, Agents and Files; Tasks shows once a task store is set up, and Agents once the factory is. Leave a built-in out to turn it off, spread it to change its label or icon, and add screens of your own:

bud.config.ts

import { canvas, defineConfig, files, pages } from '@bud.sh/app'

export default defineConfig({
  routes: ['/'],
  screens: [
    pages(),
    canvas(),
    { ...files(), label: 'Code' },
    { id: 'metrics', label: 'Metrics', icon: 'chart', module: './bud/metrics.tsx' },
  ],
})
FieldRequiredDescription
idYesLowercase letters, digits and hyphens, up to 32 characters.
labelYesThe name in the navigation and the page title.
moduleYesA ./ path to a .ts, .tsx, .js or .jsx file in your project.
pathNoOne segment, such as /metrics. Defaults to /<id>.
iconNoA Bud icon, such as chart, gauge or users. Defaults to layers.

Built-ins keep their own id and path (Pages at /pages, Canvas at /canvas; / opens the first screen), so links into them keep working. Bud checks every screen when it starts.

A screen module exports its screen as the default. load runs once while Bud shows its loader, and render gets the result:

bud/metrics.tsx

import { Text, defineScreen } from '@bud.sh/app/screen'

export default defineScreen({
  load: async ({ project, signal }) => {
    let response = await fetch(new URL('/metrics.json', project.targetOrigin), { signal })
    return (await response.json()) as { visits: number }
  },
  render: (metrics) => <Text as="p">{metrics.visits} visits this week</Text>,
})

Screens run only in the browser, inside Bud, with the same access as Bud itself, so treat them like your config. They share Bud's remix/component, and @bud.sh/app/screen also exports Bud's Button, Text, Loader, Spinner and field cards. Screens can't import CSS yet. Bud bundles each screen with your project's Vite when it's first opened and again after you change it; reload to see an edit.

Options

OptionDefaultDescription
name"Your project"The project name shown in Bud.
logoBud's markYour mark in the navigation and on the tab. See Logo.
themeBud's colorsYour palette for light and dark. See Theme.
locale"en"Bud's language: "en" or "de".
factoryfalseThe software factory's statuses; see Factory.
plugins–Stores, picker, task design, screens and sandboxes. See Plugins.
routes–The pages on the canvas. Required.
screensPages, Canvas, Tasks, FilesBud's navigation. See Screens.
port0Bud's own port, so its address stays the same. 0 picks a free one.
target.origin–Your app's origin, such as http://127.0.0.1:3000.
target.port–Shorthand for http://127.0.0.1:<port>.
storageBoth onfalse turns tasks or discussions off. See Storage.
designs.pages"path"How the Pages sidebar names pages: "path" alone, or "title" with the path beside it.
comments.remote"origin"The Git remote for gitComments().
previews.scale2Screenshot pixel density, 1 to 3. See Canvas.
previews.themes["light", "dark"]Color schemes to capture, in toolbar order.
previews.viewportsOne 1440px DesktopScreen sizes to capture. See Viewports.
previews.ready–What a page shows once it is ready. See Ready pages.
previews.readyTimeout60000How long a capture waits for ready, in milliseconds.
previews.css–CSS added to every captured page. See Hiding overlays.
files.edittrueSet false to make Files view-only. See Files.
branches.mainmain, else masterThe branch always captured. See Branches.
branches.buildYour Vite dev serverHow a branch is installed, built and served. See Branches.

With Vite, leave target out: Bud uses the port Vite actually bound, including a fallback port. Set port when you open Bud from a bookmark or another device, so its address doesn't change on every restart.

Bud captures each route as a screenshot for the canvas. If your site has no dark style, capture only what it has:

bud.config.ts

import { defineConfig } from '@bud.sh/app'

export default defineConfig({
  routes: ['/', '/pricing'],
  previews: { themes: ['light'] },
})

Viewports

Bud captures every route at one 1440px desktop width by default. List previews.viewports to capture other screen sizes instead, up to four, in toolbar order:

bud.config.ts

import { defineConfig } from '@bud.sh/app'

export default defineConfig({
  routes: ['/', '/pricing'],
  previews: {
    viewports: [
      { name: 'Desktop', width: 1440 },
      { name: 'Mobile', width: 390 },
    ],
  },
})

Each viewport has a name and a width from 240 to 3840 pixels. A viewport narrower than 800 pixels is captured as a phone, with touch and your page's viewport meta tag, and 844 pixels tall by default; set mobile and height to choose yourself. Listing viewports replaces the default, so leave Desktop out to capture only a phone.

With more than one viewport, the canvas toolbar has a button for each and one that shows them all side by side, each artboard labelled with its width. Comments stay on the viewport they were made on.

logo shows your own mark in Bud's navigation and as its tab icon, instead of Bud's. Give an image path relative to the config, or a separate one for dark mode:

bud.config.ts

import { defineConfig } from '@bud.sh/app'

export default defineConfig({
  routes: ['/'],
  logo: { light: 'public/logo.svg', dark: 'public/logo-dark.svg' },
})

SVG, PNG, WebP, JPEG, GIF and ICO files up to 1 MiB work. The image must be inside your project, and it keeps its own colors in both themes.

Theme

theme puts Bud in your palette. Give it your main colors, background, foreground and primary, for light, dark or both, and Bud derives the rest from them: surfaces, borders, hover and selection, muted text, scrollbars, tooltips, the text on your primary color, and Agent view's terminal and agent interface:

bud.config.ts

import { defineConfig } from '@bud.sh/app'

export default defineConfig({
  routes: ['/'],
  theme: {
    light: {
      background: 'hsl(0 0% 100%)',
      foreground: 'hsl(330 91% 13%)',
      primary: 'hsl(44 100% 49%)',
    },
    dark: {
      background: 'hsl(320 12% 7%)',
      foreground: 'hsl(340 18% 94%)',
      primary: 'hsl(44 100% 49%)',
    },
  },
})

Set any other token too, and it wins over the derived one. A shell of your own, such as a warmer one around the app, also tints the rest: controls, cards, hover, borders and scrollbars follow it instead of the background. A scheme without main colors keeps Bud's own for whatever you leave out.

Values are CSS colors or shadows. The tokens are background, shell, main, canvas, panel, board, editor, editor-tabs, foreground, muted, primary, primary-hover, primary-foreground, border, tab-divider, hover, selected, control, control-active, control-selected, card, menu, menu-hover, card-frame, card-surface, card-border, card-frame-border, card-badge, overlay-shadow, tooltip, tooltip-border, tooltip-shadow, board-control, board-control-hover, board-card, board-segment, frame-control, scrollbar, scrollbar-hover, focus, and the elevations control-elevation, board-elevation, board-card-elevation, card-elevation, board-card-lift, modal-elevation, segment-elevation and control-open-elevation. Agent view's terminal takes terminal, terminal-foreground, terminal-cursor, terminal-selection and the ANSI colors terminal-black, terminal-red, terminal-green, terminal-yellow, terminal-blue, terminal-magenta, terminal-cyan and terminal-white, each with a terminal-bright- form; the agent's interface builds its theme from those and primary, muted and border, in both schemes. A server hosting Bud can pass its own palette instead (createNodeHost({ theme })).

Plugins

Everything you add to Bud is a plugin: where tasks and discussions live, how comments find elements, how tasks look, extra screens and where the software factory's agents work. List them in plugins, imported from @bud.sh/app/plugins:

bud.config.ts

import { defineConfig } from '@bud.sh/app'
import { local, reactGrab, taskDesign } from '@bud.sh/app/plugins'

export default defineConfig({
  routes: ['/'],
  factory: {},
  plugins: [reactGrab(), taskDesign('./bud/tasks.tsx'), local()],
})
PluginProvides
beads()Tasks and discussions in Beads. Bud's default, so you only list it to pick one with { only }.
gitComments()Discussions on the project's bud/comments branch. See Storage.
memory()Tasks and discussions in memory, for tests and examples.
react(), reactGrab()The React pickers. See Picker.
picker('./picker.ts')Your own picker. See Pickers.
taskDesign('./x.tsx')Your task design. See Task designs.
local(), boat()The factory's sandboxes. See the Factory guide.
opencode()The factory's agent and its model; OpenCode on a free model by default.

Only one plugin may provide each store, the picker, the task design and the sandboxes. Your own plugins use definePlugin from the same entry: an object with a name, the parts it provides (work, threads, picker, taskDesign, screens, sandbox, harness) and, as in Vite, an optional config(config) hook whose returned settings Bud merges in before it checks the config. enforce: 'pre' or 'post' runs a hook before or after the others.

Language

locale sets the language of Bud's interface: English ("en", the default) or German ("de"). A server hosting Bud can choose it for each person instead, with fetch(request, { locale }).

Factory

The software factory turns comments into tasks that coding agents finish in sandboxes; the Factory guide covers the flow, its options and security. Its steps are statuses in your task store, so the board has a column for each:

StepStatusKind
Openopennot started
In progressin_progressstarted
Needs approvalneeds_approvalstarted
Ready to mergeready_to_mergestarted
Mergedmergeddone

TypeScript

factory: {},

turns it on with these Beads names; agents work in the sandboxes of a sandbox plugin, local() or boat(). If your task store calls them something else, map them:

TypeScript

factory: { statuses: { needsApproval: 'review', merged: 'shipped' } },

When the task store lacks some of them, Bud adds them itself: with Beads, that's bd config set status.custom, merged with the custom statuses you already have, which stay as they are. A status that exists as another kind, such as a merged that isn't done, has to be fixed in the store or mapped to another name; until then the factory stays off, and bud doctor says which status is in the way.

The factory's other options, such as the sandbox's setup and dev server, the agent's instructions and herdr: true for live agent status, are in the Factory guide.

Hiding overlays

Development tools that draw over your site, such as React Grab, show up in screenshots too. previews.css adds CSS to every page Bud captures, and never to your site in your own browser:

bud.config.ts

import { defineConfig } from '@bud.sh/app'

export default defineConfig({
  routes: ['/', '/pricing'],
  previews: { css: '[data-react-grab] { display: none !important }' },
})

Changing it takes every picture again.

Picker

Comments on the canvas and in Pages remember the element they point at, not only a point. The picker finds it:

PluginFinds
None (default)The element, the card or section around it and its text, from the page's DOM.
react()The same, plus the React component and its source line, from React's development build.
reactGrab()The same as react(), picking in Pages with your project's React Grab.
picker('./picker.ts')Your own browser module in the project. See Pickers.

bud.config.ts

import { defineConfig } from '@bud.sh/app'
import { reactGrab } from '@bud.sh/app/plugins'

export default defineConfig({
  routes: ['/', '/collection'],
  plugins: [reactGrab()],
  previews: { css: '[data-react-grab] { display: none !important }' },
})

reactGrab() needs react-grab installed in your project and loaded in development. Pickers run only in your pages during development, never in your build.

Branches

The canvas keeps screenshots of main beside your working tree, and of every branch you activate in its sidebar. When one of them gets a new commit, Bud builds that commit in a folder outside your checkout and captures every route again; your working tree, staging area and branches never change. The folder is a Git checkout of the commit, so builds that read HEAD or the log work as usual. By default a build uses your project's node_modules (and those of its workspace packages) and runs your Vite dev server. Projects that build or serve differently give the shell commands:

bud.config.ts

import { defineConfig } from '@bud.sh/app'

export default defineConfig({
  routes: ['/', '/pricing'],
  branches: {
    main: 'main',
    build: {
      install: 'npm ci',
      serve: 'npx vite --host 127.0.0.1 --port $PORT --strictPort',
    },
  },
})

serve must listen on 127.0.0.1:$PORT. Bud's Vite plugin stays off in branch builds, and a branch's build stops once its screenshots are taken. See Canvas.

Ready pages

Bud captures a page once its requests have settled and everything it shows has loaded, including images and sections that appear only when scrolled to. A page that renders after hydration or after fetching its data should also say when it is ready, so the canvas never shows a loading state. Point previews.ready at a CSS selector that matches only then, or at a JavaScript expression that becomes true:

bud.config.ts

import { defineConfig } from '@bud.sh/app'

export default defineConfig({
  routes: [
    '/',
    { path: '/map', title: 'Map', ready: '[data-map-ready="true"]' },
    { path: '/embed', title: 'Embed', ready: false },
  ],
  previews: { ready: '#root[data-client-ready]' },
})

A route's own ready replaces the shared one, and false turns it off. Bud waits up to previews.readyTimeout before every capture. A page that never gets ready is still captured, and its artboard shows a warning naming the condition.

JavaScript

bud.config.js takes the same shape. The file is loaded by Node, not Vite, so path aliases don't apply and relative imports need extensions.