@bud.sh/app
Bud is a local companion for reviewing a web project: the app itself, its config, screen, design and adapter APIs, and the companion server a host starts. Its Vite plugin, @bud.sh/app/vite, hosts it beside your Vite dev server, and the package's bud CLI runs it without Vite. Bud browses your live pages, shows your routes side by side on a canvas, lets you browse, edit and review files, and keeps discussions on pages, code and tasks through your selected storage adapters. The interface uses Remix 3; your project can use any framework that runs on Vite.
Features
- Pages - Your configured routes beside a browser of the live site, with tabs and history per workspace tab, and comments on elements you pick
- Canvas - Every configured route as an always-current screenshot in light and dark, with comments whose pins follow their element
- Branches - Screenshots of
mainand every activated branch beside the working tree, each built from its latest commit outside your checkout - Pickers - Comments record the element, its block and, with
bud:react-grab,bud:remixor your own picker, its component and source line - Files - File tree, editor, diffs, history, staging and conflict resolution
- Code comments - Discussions on selected lines that follow edits and stay readable when outdated
- Tasks - Comments become tasks on a board that people and agents change live, with Beads as the default store; task designs can add fields such as priority or assignee
- Agents - Comments become tasks that a coding agent (OpenCode) finishes in a sandbox, a local worktree or boat.dev, on its own
dev/branch: Agent view shows the live preview beside the agent's terminal and chat, the Agents screen lists every session, and nothing is pushed until someone approves - Hosting - The canvas, Pages, tasks and agents for a whole team on Cloudflare, for a GitHub repository
- Custom screens - Your own Remix screens beside the built-ins, any of which you can turn off
- Storage adapters - Beads stores tasks and discussions by default; custom adapters and the compatible Git comment store are explicit selections
Installation
One package holds the app, its Vite plugin and the bud CLI:
npm install -D @bud.sh/app
To try this repository's version instead, build and pack it:
pnpm --filter @bud.sh/app build
pnpm --filter @bud.sh/app pack --out /tmp/bud-app.tgz
# In your project
npm install -D /tmp/bud-app.tgz
Node 26.12 or newer is required.
Usage
Add the Vite plugin:
// vite.config.ts
import { bud } from '@bud.sh/app/vite'
export default {
plugins: [bud()],
}
Add exactly one config file:
// bud.config.ts (or bud.config.js)
import { defineConfig } from '@bud.sh/app'
export default defineConfig({
name: 'My project',
routes: ['/', { id: 'product', path: '/products/123', title: 'Product' }],
// port: 4000, // Bud's own port; omit to pick a free one
// target: { port: 3000 }, // detected from Vite by default
// comments: { remote: 'origin' },
// plugins: [reactGrab()], // from @bud.sh/app/plugins; Bud's DOM picker by default
})
Run your usual Vite dev command and open the printed Bud URL. The plugin only runs in development, and config changes restart it. It starts Bud's companion when Vite starts listening, finds your dev server's address, refreshes previews on hot updates and adds the small dev bridge Pages uses to follow your live pages; the bridge does nothing outside Bud's frames.
Plugins, storage and designs
Stores, the picker, the task design, extra screens and the factory's sandboxes and agent come
from plugins, which @bud.sh/app/plugins exports: beads(), gitComments(), memory(),
reactGrab(), react(), remix(), picker(), taskDesign(), local(), boat(), opencode() and
definePlugin for your own. Tasks and discussions are Beads' unless a plugin provides one;
storage: { work: false } or { threads: false } turns one off. designs.pages sets how the
Pages sidebar names pages: by path (the default) or by title with the path beside it.
import { defineConfig } from '@bud.sh/app'
import { definePlugin, gitComments, taskDesign } from '@bud.sh/app/plugins'
import { tracker } from './bud/tracker.server.ts'
export default defineConfig({
routes: ['/'],
plugins: [
definePlugin({ name: 'tracker', label: 'Tracker', work: tracker }),
gitComments(),
taskDesign('./bud/tasks.tsx'),
],
})
A plugin's store is a fully provided Effect layer using the WorkAdapter or ThreadAdapter
service from @bud.sh/app/server, or a function making one for { project };
memoryAdapterLayer({ schema }) supplies both for fixtures. A shared layer is acquired once,
and its resources close with the companion. Task designs are browser modules ending in .ts,
.tsx, .js or .jsx inside the project, outside node_modules; Bud checks their paths
before listening and their exports when opened. Only each store's public identity and
connection state reach screens through project.storage; plugins and server configuration
stay private.
Task designs default-export defineTaskDesign from @bud.sh/app/design. Its optional
card, page, properties, actions, sections and column slots are Remix components.
Omitted slots retain their built-in parts, which are exported for composition as
TaskCardDefault, TaskPageDefault, SectionsDefault and ColumnDefault; properties and
actions are empty until a design fills them. Slots receive validated work data, field rendering, navigation and
explicit operations. Project context contains only its display id and name.
See the task design guide for the exported props and a compact-card example. The same browser-module registry and shared Remix import map serve screens and designs.
Screens
screens sets Bud's navigation, in order. Leave it out for Pages, Canvas, Tasks, Agents and Files (Agents once the factory is set up). Leave a built-in out to disable it, spread it to change its label or icon, and add your own screens as Remix component modules:
// 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' },
],
})
// 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>,
})
A custom screen's path defaults to /<id> and its icon to layers. Bud shows its loader until load settles and a retry card if it fails. Screens run only in the browser, inside Bud's page, and get the project with Bud's request capability, so treat them like the config. @bud.sh/app/screen also exports Bud's Button, Text, Loader, Spinner and field cards. Screens can't import CSS yet; Bud bundles each one with your project's Vite on first visit and again after you edit it.
Screens can own nested places. Keep the route map in a browser-safe module, then pass the same map as routes in both the config's screen and defineScreen:
// bud/metrics-routes.ts
import { get, route } from 'remix/routes'
export const metricsRoutes = route('/metrics', {
index: get('/'),
report: get('/reports/:id'),
})
metricsRoutes.report.href({ id: 'weekly' }) // /metrics/reports/weekly
import { Text, defineScreen } from '@bud.sh/app/screen'
import { metricsRoutes } from './metrics-routes.ts'
export default defineScreen({
routes: metricsRoutes,
render: (_data, context) => (
<Text>{context.route === 'report' ? context.params.id : 'All reports'}</Text>
),
})
The config metadata allows startup validation without running screen modules on the server. Every route must stay under the screen's path. context.route narrows context.params; context.url carries the complete current location. Navigation within a screen preserves its component state and loaded data. Fetch route-specific updates from the component when its context changes; load runs only when the screen first opens.
Opened screens also stay mounted while hidden. context.active tells a screen when it is visible and owns the browser location. Background reconciliation can update its data, but should defer URL changes until it becomes active again.
Without Vite
npx bud [project-directory] # standalone, needs target.origin or target.port
npx bud doctor [project-directory] # what's set up, and what the rest needs
npx bud comments migrate [project-directory] [--to bud:beads] [--dry-run]
The bud command comes with @bud.sh/app and needs target.origin or target.port in the config to serve. Previews opened this way can't be commented on; use the Vite plugin for that. Bud shows only what a project has set up; bud doctor says what that is and what the rest needs. bud capture is what a hosted Bud's builder runs.
Task links
The board lives at /tasks; each task has a typed /tasks/:id URL. Opening a task preserves the board's filters and scroll position, and Back returns to the same board. Copy link and modified clicks use the same route. Older ?issue= links redirect permanently after ID validation. Dots in hierarchical IDs are encoded by the route builder.
Comments
Comments use the selected thread adapter, which defaults to Beads. Initialize the project's Beads workspace explicitly before writing; Bud never creates one or chooses a hidden fallback. Authors come from your Git name and email on the server. One discussion can link to multiple tasks without duplicating its messages. Opening a discussion loads its messages; counts and truncation stay explicit when a backend returns a bounded view.
Writes use the discussion revision you last read. Conflicts retain the draft for review, and uncertain writes require checking the stored discussion before retrying. Sync is explicit and uses the selected adapter's existing remote configuration.
With gitComments(), discussions use the compatible bud/comments branch. Writes preserve your source branch, index and working files. Sync validates and
merges fetched records, then pushes the exact validated commit without forcing. No other
application storage selection writes this ref.
In Files, select lines and press Ctrl/⌘+Alt+M to comment. Save an editor draft first.
File and review links
File links name the place directly. The route maps shared by the screen and link helpers live in @bud.sh/ui/routes.
| Place | URL |
|---|---|
| File or directory | /files/src/app.ts |
| Working or staged review | /files/review/working/src/app.ts |
| All changed files | /files/review/working |
| Commit comparison | /files/compare/HEAD...<full-object-id>/src/app.ts |
| Selected source lines | /files/src/app.ts#L12-L20 |
| Selected diff lines | /files/review/working/src/app.ts#L12-R14 |
In diffs, L is the old side and R the new side. A copied selection opens its one file, including when copied from an all-files review. Its optional at query value is a SHA-256 content digest used to detect changed source; it is not an addressable Git blob. Compare endpoints accept HEAD, full object IDs and full refs/... names. The typed builder encodes them safely.
Files whose first segment is review, compare, blob or path use /files/path/<actual-path> to avoid route collisions. Existing query links redirect after their project and coordinates are validated. Foreign legacy links stay rejected.
New links belong to the companion's origin, and requests still require its session capability. A bookmark does not assert project identity if a different project later reuses that origin. Project IDs continue to scope local preferences.
Discussions use /comments/:id. Bud reads the selected thread adapter and opens the owning
canvas, file or task screen, including its saved location and conversation. Copy-link controls
use this address; older ?thread= links redirect permanently after validation. The URL carries
only the thread ID, and the preview never receives the API capability.
Migrating Git comments
bud comments migrate --to bud:beads --dry-run
bud comments migrate --to bud:beads
The command loads the project's config and uses its selected thread adapter unless --to
overrides it. Initialize the target store first. Migration does not need a preview server or
target.origin, and it never changes the config or source Git comments ref.
Canvas and code captures, statuses, message order, original authors, timestamps and reply ancestry are preserved. Complete history can exceed the browser's message display limit; the source and target still enforce their configured byte limits. Existing source identities are skipped, so rerunning the command resumes a partial migration without duplicating threads. An uncertain write stops the run and is never retried automatically. Keep the source Git history until you have reviewed the imported discussions.
Security
Bud binds to loopback. Requests need a per-session capability that is never sent to the previewed page. Canvas captures pages in its own headless browser. Pages shows them in sandboxed frames on the project's own origin, which differs from Bud's, and follows them only through the dev bridge's postMessage protocol (@bud.sh/app/bridge); a site served from Bud's own origin is never framed.
Development
pnpm dev # Bud and the marketing site with HMR
pnpm verify # all checks, including an installed-tarball browser test
See AGENTS.md for the app's structure.
Related Packages
@bud.sh/project- Config contract and loading@bud.sh/ui- Components@bud.sh/comments,@bud.sh/files,@bud.sh/beads- Domain services