# Configuration

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

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

```ts title="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:

| Field     | Required | Description                                                |
| --------- | -------- | ---------------------------------------------------------- |
| `path`    | Yes      | A local path starting with `/`. Queries and hashes work.   |
| `title`   | Yes      | The label above the artboard.                              |
| `id`      | No       | A stable identity. Defaults to the path.                   |
| `pattern` | No       | The dynamic route the path renders, such as `/blog/:slug`. |
| `group`   | No       | A 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:

```ts title="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:

```ts title="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:

```ts title="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'],
    },
  ],
})
```

```ts title="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](/docs/app/guides/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:

```ts title="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' },
  ],
})
```

| Field    | Required | Description                                                            |
| -------- | -------- | ---------------------------------------------------------------------- |
| `id`     | Yes      | Lowercase letters, digits and hyphens, up to 32 characters.            |
| `label`  | Yes      | The name in the navigation and the page title.                         |
| `module` | Yes      | A `./` path to a `.ts`, `.tsx`, `.js` or `.jsx` file in your project.  |
| `path`   | No       | One segment, such as `/metrics`. Defaults to `/<id>`.                  |
| `icon`   | No       | A 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:

```tsx title="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

| Option                  | Default                     | Description                                                                              |
| ----------------------- | --------------------------- | ---------------------------------------------------------------------------------------- |
| `name`                  | `"Your project"`            | The project name shown in Bud.                                                           |
| `logo`                  | Bud's mark                  | Your mark in the navigation and on the tab. See [Logo](#logo).                           |
| `theme`                 | Bud's colors                | Your palette for light and dark. See [Theme](#theme).                                    |
| `locale`                | `"en"`                      | Bud's language: `"en"` or `"de"`.                                                        |
| `factory`               | `false`                     | The software factory's statuses; see [Factory](#factory).                                |
| `plugins`               | –                           | Stores, picker, task design, screens and sandboxes. See [Plugins](#plugins).             |
| `routes`                | –                           | The pages on the canvas. Required.                                                       |
| `screens`               | Pages, Canvas, Tasks, Files | Bud's navigation. See [Screens](#screens).                                               |
| `port`                  | `0`                         | Bud'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>`.                                                 |
| `storage`               | Both on                     | `false` turns tasks or discussions off. See [Storage](/docs/app/guides/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.scale`        | `2`                         | Screenshot pixel density, `1` to `3`. See [Canvas](/docs/app/guides/canvas).             |
| `previews.themes`       | `["light", "dark"]`         | Color schemes to capture, in toolbar order.                                              |
| `previews.viewports`    | One 1440px `Desktop`        | Screen sizes to capture. See [Viewports](#viewports).                                    |
| `previews.ready`        | –                           | What a page shows once it is ready. See [Ready pages](#ready-pages).                     |
| `previews.readyTimeout` | `60000`                     | How long a capture waits for `ready`, in milliseconds.                                   |
| `previews.css`          | –                           | CSS added to every captured page. See [Hiding overlays](#hiding-overlays).               |
| `files.edit`            | `true`                      | Set `false` to make Files view-only. See [Files](/docs/app/guides/files).                |
| `branches.main`         | `main`, else `master`       | The branch always captured. See [Branches](#branches).                                   |
| `branches.build`        | Your Vite dev server        | How a branch is installed, built and served. See [Branches](#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:

```ts title="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:

```ts title="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

`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:

```ts title="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:

```ts title="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`:

```ts title="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()],
})
```

| Plugin                   | Provides                                                                                        |
| ------------------------ | ----------------------------------------------------------------------------------------------- |
| `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](/docs/app/guides/storage).    |
| `memory()`               | Tasks and discussions in memory, for tests and examples.                                        |
| `react()`, `reactGrab()` | The React pickers. See [Picker](#picker).                                                       |
| `picker('./picker.ts')`  | Your own picker. See [Pickers](/docs/app/guides/pickers).                                       |
| `taskDesign('./x.tsx')`  | Your task design. See [Task designs](/docs/app/guides/task-designs).                            |
| `local()`, `boat()`      | The factory's sandboxes. See the [Factory guide](/docs/app/guides/factory).                     |
| `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](/docs/app/guides/factory) covers the flow, its options and security. Its steps are statuses in your task store, so the board has a column for each:

| Step           | Status           | Kind        |
| -------------- | ---------------- | ----------- |
| Open           | `open`           | not started |
| In progress    | `in_progress`    | started     |
| Needs approval | `needs_approval` | started     |
| Ready to merge | `ready_to_merge` | started     |
| Merged         | `merged`         | done        |

```ts
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:

```ts
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](/docs/app/guides/factory#turning-it-on).

## 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:

```ts title="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:

| Plugin                  | Finds                                                                                   |
| ----------------------- | --------------------------------------------------------------------------------------- |
| 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](/docs/app/guides/pickers).        |

```ts title="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:

```ts title="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](/docs/app/guides/canvas#branches).

## 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:

```ts title="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.
