Skip to content

Client UI

Suggest Edits

Client entries use registerPlugin() from @makinbakin/sdk. Keep UI contributions predictable and built from SDK components where practical. Plugin UI should feel like part of Bakin: dense enough for repeated work, accessible, and clear about loading, empty, error, and permission states.

The tested minimal client entry lives at docs/snippets/plugin-basic/client.tsx.

Source: docs/snippets/plugin-basic/client.tsx

import { registerPlugin } from '@makinbakin/sdk'
function DocsBasicPage() {
return <div>Hello from a Bakin plugin.</div>
}
registerPlugin({
id: 'docs-basic',
navItems: [
{
id: 'docs-basic',
label: 'Docs Basic',
icon: 'Puzzle',
href: '/docs-basic',
order: 100,
},
],
routes: {
'/docs-basic': DocsBasicPage,
},
})

Navigation items should be stable and specific to the plugin. Use lucide icon names. Include order only when the plugin has a strong placement requirement.

FieldMeaning
idStable item ID. Prefix with the plugin ID.
labelSidebar label.
iconLucide icon name.
hrefRoute path.
orderOptional sort order. Defaults to 100.
childrenNested nav items.

Use routes for plugin-owned pages. The host catch-all route renders registered plugin routes and passes route params into the component.

registerPlugin({
id: 'docs-basic',
routes: {
'/docs-basic': DocsBasicPage,
'/docs-basic/[id]': DocsBasicDetailPage,
},
})

Patterns support exact paths and dynamic segments in :id, [id], or $id form. If a route is visible in navigation, also declare it in bakin-plugin.json contributes.clientRoutes.

Use page:/... slots when the host already owns the route and the plugin fills that route. Core plugins use this for built-in pages such as Tasks, Assets, Schedule, Team, Models, Health, Workflows, and Memory.

registerPlugin({
id: 'tasks',
slots: {
'page:/tasks': KanbanBoard,
},
})

For a new route owned by your plugin, prefer routes.

Slots let plugins add focused UI to existing Bakin workflows.

SlotUse it for
asset-previewCustom asset card preview content.
asset-detail-modalAsset detail panels.
task-assetsTask drawer asset attachments.
task-sidebarTask-specific side panels.
home-widgetDashboard widgets.
page:/<route>Host-owned page mount.

Register with registerSlot() directly when you need a custom order. Lower order renders first.

Import common UI from @makinbakin/sdk/ui and shared app components from @makinbakin/sdk/components.

import { Button } from '@makinbakin/sdk/ui'
import { PluginHeader } from '@makinbakin/sdk/components'

Custom UI is fine when the domain needs it, but keep Bakin conventions: small radii, clear tables and filters, keyboard-friendly controls, visible empty states, and no layout shift when data loads.

Use IntegratedBrainstorm when a plugin needs a durable agent chat panel inside its own page. Keep the plugin-owned record as the source of truth for visible messages and pass a stable thread id to the server route so the runtime adapter can preserve conversation continuity.

transformAssistantMessage lets the plugin render structured artifacts below assistant text without forking the chat component. For example, a content planning plugin can parse proposal ids from an assistant message and render review cards inline:

import { IntegratedBrainstorm } from '@makinbakin/sdk/components'
function PlanningChat({ sessionId, agentId, proposalByMessageId }) {
return (
<IntegratedBrainstorm
endpoint={`/api/plugins/messaging/sessions/${sessionId}/messages`}
agentId={agentId}
transformAssistantMessage={(message) => {
const proposals = proposalByMessageId.get(message.id) ?? []
return (
<>
<p>{message.content}</p>
{proposals.map((proposal) => (
<PlanProposalCard key={proposal.id} proposal={proposal} />
))}
</>
)
}}
/>
)
}

Persist activity rows and parsed artifacts in plugin storage for reloads. Do not replay the whole stored transcript into every agent call unless the agent task explicitly needs that context.

During development, Bakin can unregister and reload client contributions. If a plugin maintains a client-side registry outside registerPlugin(), enroll cleanup with registerPluginCleanup(id, fn).

import { registerPluginCleanup } from '@makinbakin/sdk'
registerPluginCleanup('docs-basic', () => {
// Clear plugin-owned client registries here.
})

Import supported surfaces only:

import { registerPlugin } from '@makinbakin/sdk'
import { Button } from '@makinbakin/sdk/ui'
import type { NavItem } from '@makinbakin/sdk'

Host internals can change without warning. SDK exports are the contract.