Task designs
Change how tasks look while keeping the same storage and operations.
A task design is a set of Remix components. Replace one part of the board or detail page and keep the rest of Bud's built-in interface. Storage stays independent: a design renders the fields, statuses and actions declared by your work adapter.
Start with one card
Create a browser module inside your project:
designs/compact-card.tsx
import { defineTaskDesign, TaskCardDefault } from '@bud.sh/app/design'
import type { TaskCardDesignProps } from '@bud.sh/app/design'
import type { Handle, RemixNode } from 'remix/component'
function CompactCard(handle: Handle<TaskCardDesignProps>): () => RemixNode {
return () => <TaskCardDefault {...handle.props} compact />
}
export default defineTaskDesign({ card: CompactCard })Add it with taskDesign(), separately from your storage:
bud.config.ts
import { defineConfig } from '@bud.sh/app'
import { taskDesign } from '@bud.sh/app/plugins'
export default defineConfig({
routes: ['/'],
plugins: [taskDesign('./designs/compact-card.tsx')],
})The card keeps Bud's links, keyboard focus and drag behavior. Columns, dialogs and the task page retain their defaults. Leave taskDesign() out to use the built-in design throughout.
Bring back more fields
Bud's built-in tasks screen is deliberately small: a title, a description, who created the task, its epic, linked tasks, the agent's run, the discussion and activity. Beads records much more, such as priority and assignee, and a design can show it. A properties slot puts a card beside the task's page:
designs/properties.tsx
import { defineTaskDesign } from '@bud.sh/app/design'
export default defineTaskDesign({
properties: (handle) => () => (
<dl class="grid grid-cols-[6rem_1fr] gap-2">
<dt>Priority</dt>
<dd>{handle.props.renderField('priority')}</dd>
<dt>Assignee</dt>
<dd>{handle.props.renderField('assignee')}</dd>
</dl>
),
})priority and assignee are the Beads adapter's field IDs; another adapter may use other IDs, which fieldByRole finds by role. An actions slot adds controls under the properties, such as a button that runs one of the schema's actions through handle.props.operations.act.
The marketing consumer includes this example at apps/web-marketing/designs/compact-card.tsx, disabled by default.
Available slots
Every slot is a Remix component. Its setup runs once; read changing data from handle.props inside the returned render function.
| Slot | What it replaces | Built-in component |
|---|---|---|
card | A board card or list row | TaskCardDefault |
page | The complete task detail page, including loading and error states | TaskPageDefault |
properties | A card beside the page, empty by default | None |
actions | Controls under the properties, empty by default | None |
sections | The sections below the discussion, by default Activity | SectionsDefault |
column | A status column's heading and cards | ColumnDefault |
All defaults are exported from @bud.sh/app/design. Render one inside your own component to compose it. A custom page can render <TaskPageDefault {...handle.props} /> and retain any separately configured properties, actions or section slots.
The workspace keeps the column's drop target. A column receives its status, items and rendered card children; rendering those children preserves your selected card design.
Data and field rendering
Slots receive schema, optional operations, renderField, itemHref, onSelect and a project display identity containing only id and name. Item slots receive the current item. A page's item is null while loading or after a failed read; columns receive items instead.
Cards also receive selection, completion, compact layout, drag and link props. Pages receive the loaded detail, snapshot, discussion slot, loading state, errors, refresh callback and editor drafts. The exported TaskCardDesignProps, TaskPageDesignProps and other slot types describe the complete interface.
Use schema roles when placing content; an adapter chooses its own field IDs. For example:
TSX
import { fieldByRole } from '@bud.sh/app/design'
let title = fieldByRole(handle.props.schema, 'title')
let content = title ? handle.props.renderField(title) : nullrenderField(fieldOrId, value?) uses the shared renderers for text, dates, people, labels, markdown, links and JSON. It reads the item's current value unless you supply one. Plain strings and metadata remain escaped.
Keep operations explicit
Designs receive consumer-owned callbacks rather than storage services. Keep controls conditional on the schema's capabilities and the available operations. Use the revision from the item when saving; preserve a draft after a conflict or uncertain result and let the user review it.
The default editor, menu and sections already handle these cases. Reusing them also retains lazy section loading, cancellation, focus restoration and discussion behavior.
Module loading
Design modules run only in Bud's browser interface. They must remain inside your project, outside node_modules, and cannot be server modules. Bud shares its Remix runtime through the same import map used by custom screens.
Use Bud's components from @bud.sh/app/screen, the design exports, theme tokens or inline styles. Custom modules cannot import CSS. Bud rebuilds installed modules when a local input changes; reload the task screen to load that build. Workspace source development uses Vite's module pipeline.
A missing or unsafe module path prevents startup. A build failure or invalid export appears in Tasks with a retry action. Bud never silently substitutes the default design for a broken configured module.