Skip to content

Extending the Dashboard

The dashboard's own panels aren't special-cased — they're registered through a public plugin contract, @stepbook/plugin-api. The same contract is open to you: you can contribute your own canvas views and bottom-drawer panels.

Status

The plugin contract (@stepbook/plugin-api) is stable and is what the built-in panels are built on. What's not yet wired is loading third-party plugins from your project config — today the active plugin set is compiled in. So you can author against this contract and the types are firm, but dropping a plugin into a running dashboard via config is a planned next step, not a turnkey feature yet. This page documents the contract so you can build against it now.

The contract

Install (or reference) the types-only package:

ts
import type {
  StepbookPlugin,
  CanvasViewContribution,
  AddonPanelContribution,
  AddonTabBadge,
} from '@stepbook/plugin-api'

A plugin is a bundle of contributions with a stable id:

ts
interface StepbookPlugin {
  id: string // stable id, e.g. 'my-org/coverage' — used for dedup
  name?: string // human-readable, shown in dev tools / settings
  addonPanels?: AddonPanelContribution[]
  canvasViews?: CanvasViewContribution[]
  Provider?: ComponentType<{ children: ReactNode }> // optional plugin-owned state
}

There are two surfaces you can contribute to — the same two the dashboard itself uses (see The Dashboard):

Canvas views (per step)

A view in the main area that follows the selected step:

ts
interface CanvasViewContribution {
  key: string // becomes the route value, e.g. 'my-org/coverage'
  label: string // tab label
  Component: ComponentType // propless — reads dashboard context for its data
  order?: number // lower renders first; builtins use 10–60, use 100+
}

Addon panels (whole run)

A bottom-drawer tab that reflects the run as a whole, optionally with a live badge on its tab:

ts
interface AddonPanelContribution {
  key: string // builtins are bare ('checks'); namespace yours ('my-org/x')
  label: string
  Component: ComponentType // propless
  useBadge?: () => AddonTabBadge | undefined // optional live tab badge
  order?: number // builtins use 10/20/30; default 100
}

interface AddonTabBadge {
  text: string
  tone?: 'fail' | 'warn' | 'default'
}

Components are propless

This is the key design rule: a contribution's Component takes no props. It reads everything it needs from the dashboard's React context — the selected step, the current run, the step's output, and so on. That keeps contributions decoupled from the shell's internal wiring; you subscribe to the context you care about and render.

If your panel needs its own state that should live above the app (a shared store, a client to some service), provide it via the plugin's optional Provider, which the dashboard composes into the context tree.

A minimal example

A plugin that adds one addon panel showing a coverage summary, with a badge when coverage is below target:

tsx
import type { StepbookPlugin, AddonTabBadge } from '@stepbook/plugin-api'

function CoveragePanel() {
  // Read what you need from dashboard context (selected run, outputs, …),
  // compute, and render. No props.
  return <div>/* coverage view */</div>
}

function useCoverageBadge(): AddonTabBadge | undefined {
  const pct = /* derive from context */ 82
  return pct < 90 ? { text: `${pct}%`, tone: 'warn' } : undefined
}

export const coveragePlugin: StepbookPlugin = {
  id: 'my-org/coverage',
  name: 'Coverage',
  addonPanels: [
    {
      key: 'my-org/coverage',
      label: 'Coverage',
      Component: CoveragePanel,
      useBadge: useCoverageBadge,
      order: 100, // after the builtins
    },
  ],
}

How the builtins use it

The dashboard's own panels are a single StepbookPlugin (id: 'stepbook/builtin'). Its canvasViews are Input, Output, Code, Compare, Stability, and Query (orders 10–60); its addonPanels are Checks, History, and Telemetry (orders 10–30). That's the whole reason the contract is trustworthy — the first-party UI is built on exactly the API you'd use.

Conventions

  • Namespace your keys and ids (my-org/thing) so they don't collide with builtins or other plugins. Builtin keys are bare (checks, output).
  • Use order ≥ 100 so your contributions render after the builtins by default.
  • Keep components propless and read from context.

Released under the MIT License.