Task designs

Change how tasks look while keeping the same storage and operations.

View Markdown

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.

SlotWhat it replacesBuilt-in component
cardA board card or list rowTaskCardDefault
pageThe complete task detail page, including loading and error statesTaskPageDefault
propertiesA card beside the page, empty by defaultNone
actionsControls under the properties, empty by defaultNone
sectionsThe sections below the discussion, by default ActivitySectionsDefault
columnA status column's heading and cardsColumnDefault

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) : null

renderField(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.