@dbx-tools/ui-mastra
React chat UI for the AppKit-Mastra plugin.
Add a complete chat surface for
@dbx-tools/appkit-mastra, including streaming,
model selection, Genie progress, charts, tables, approvals, history, threads,
export, and MLflow feedback.
Quick Start
Section titled “Quick Start”import { MastraChat } from "@dbx-tools/ui-mastra/react";import "@dbx-tools/ui-mastra/styles.css";
export function Assistant() { return <MastraChat showModelPicker enableExport />;}Key features:
- Drop-in
MastraChatcomponent that discovers the plugin client config and wires itself to the default agent. - Reload recovery for background turns: active run metadata is persisted per agent and thread, the cached stream reconnects after a page reload, and the final persisted history is refreshed when recovery completes.
- Headless
useMastraChat()driver for apps that want the same transport logic with custom layout. - Persistent
MastraAssistantapplication shell with a floating launcher, dock/overlay modes, every edge, optional resizing, external controls, and route-stable chat state. - Controlled
ChatViewfor hosts that own messages, streaming, model state, and route calls themselves. MastraPluginClientcombines Mastra’s official AI SDK chat route, native memory APIs, and AppKit-Mastra model, suggestion, feedback, chart, and statement routes.- Tool-approval support for suspended Mastra
requireApprovalcalls, including resumed-stream handling. An email-shaped input (thesend_emailtool) renders as a formatted To / Cc / Subject / Markdown-body preview rather than raw JSON. - Complete native tool input/results retained behind expandable, syntax-highlighted JSON Request and Response rows in each tool pill.
- Typed per-turn application context through Mastra’s native
RequestContextand server-siderequestContextSchema. - Inline embed rendering for
[chart:<id>]and[data:<id>]markers produced by the server plugin. - Conversation sidebar with new, select, rename, delete, active-thread, and background-streaming states, plus a per-row cancel for a running thread.
- Placeable conversation UI: dock the list left or right, switch to an
editor-style tab strip across the top, turn it off, or let
autopick between a side panel and tabs from the chat’s own width. - Concurrent threads: run several conversations at once, switch between them
while each keeps streaming, and cancel any one independently (per-thread abort
- routing, no shared client state).
- Mid-turn steering queue: messages submitted while a turn streams stack up as pending steers (they drain oldest-first when the turn ends); each queued item can be sent now (interrupting the current turn), removed, or dragged to reorder the queue.
- Export menu for PDF and Markdown, resolving charts and tables so exported conversations remain useful offline.
Choose This UI Or AppKit UI
Section titled “Choose This UI Or AppKit UI”Use native @databricks/appkit-ui when you need its general primitives, Genie
chat component, or Model Serving hooks directly against native AppKit plugins.
Use this package when the server is
@dbx-tools/appkit-mastra and the UI needs to
understand Mastra-specific behavior:
- Mastra’s official AI SDK chat stream, native resource-scoped memory APIs, and the plugin’s model, suggestion, feedback, chart, and statement routes.
- Suspended
requireApprovaltool calls resumed through nativeresumeData. - Genie writer events rendered as inline tool progress, not just a terminal answer.
[chart:<id>]and[data:<id>]assistant markers rendered as ECharts charts and sortable tables.- Concurrent multi-thread streaming, per-thread cancel, and a mid-turn steering queue (submit while running to enqueue; drain oldest-first, or send any item now to interrupt) - the native AppKit chat surface runs one turn at a time and has no steering.
- Conversation export that resolves those embeds into Markdown or PDF.
Stylesheet Setup
Section titled “Stylesheet Setup”@import "@databricks/appkit-ui/styles.css";@import "@dbx-tools/ui-mastra/styles.css";The stylesheet imports the shared @dbx-tools/ui foundation and registers
this package’s React files with Tailwind. It does not define design tokens; the
chat UI uses AppKit semantic tokens from the host app.
Add A Persistent Assistant
Section titled “Add A Persistent Assistant”Mount MastraAssistant inside the app’s existing authentication gate and above
the route outlet. Closing the panel hides its presentation without unmounting
the chat driver, so drafts, threads, and active streams survive page navigation.
import { MastraAssistant, useMastraAssistant } from "@dbx-tools/ui-mastra/react";
const AskAboutStore = ({ storeId }: { storeId: string }) => { const assistant = useMastraAssistant<{ storeId: string; route: string }>(); return ( <button onClick={() => assistant.open({ storeId, route: window.location.pathname })}> Ask assistant </button> );};
export function App() { return ( <MastraAssistant mode="overlay" side="right" resizable={{ defaultSize: 480, minSize: 360, storageKey: "assistant-size" }} title="Workflow assistant" launcher={{ position: "bottom-right", label: "Open assistant" }} chat={{ showModelPicker: true, enableExport: true, threadPlacement: "top" }} > <RouterAndAuthenticatedApplication /> </MastraAssistant> );}mode="dock" reflows the child application; mode="overlay" overlaps it.
side accepts top, right, bottom, or left. Set resizable={false} to
lock size, launcher={false} when every launch comes from
useMastraAssistant(), header={false} to place New, History, and Close beside
the conversation pills. With the header enabled those controls move into it and
the conversation pills remain on their own second row. Replace icon, closeIcon,
newConversationIcon, historyIcon, and launcher.icon. Below
mobileBreakpoint the panel becomes a focus-contained modal overlay with Escape
dismissal while the covered application is inert. A storageKey retains
horizontal and vertical sizes independently.
This shell intentionally remains mounted instead of wrapping AppKit Sheet/Dialog/Resizable. Those overlays unmount their portal subtree when closed, and native resizable panels persist percentages rather than this component’s independent horizontal/vertical pixel sizes; either change would lose drafts, scroll state, active streams, or sizing parity.
Render A Drop-In Chat
Section titled “Render A Drop-In Chat”import { MastraChat } from "@dbx-tools/ui-mastra/react";
export function App() { return ( <MastraChat agentId="analyst" showModelPicker threadPlacement="auto" enableExport className="h-dvh" /> );}MastraChat is the quickest client for the AppKit-Mastra plugin. It reads the
plugin’s published client config, creates a MastraPluginClient, streams turns
through Mastra’s official AI SDK chat route, hydrates native memory, and renders
the controlled ChatView. When Mastra returns persisted messages without a
precomputed uiMessages projection, history hydration uses Mastra’s native
convertMessages(...).to("AIV5.UI") conversion.
Useful options:
agentIdselects a registered agent; defaults to the plugin default agent.showModelPickerfetches/modelsand sendsX-Mastra-Modeloverrides.composerActionsplaces host-owned per-turn controls beside the model selector without replacing the built-in composer.modelSelectorActionsplaces host-owned controls inside the model menu after the endpoint choices. Supply it only when the active agent supports that option, such as a Genie transport toggle.composerLeadingActionsplaces attachment or app actions on the left side of the fixed composer footer. The textarea grows above it to a cap, then scrolls; model and Send/Stop stay pinned bottom-right.suggestionsoverrides Genie starter questions; omit it to auto-fetch/suggestions, or pass[]to hide suggestions.threadPlacementchooses where conversation management renders, or turns it off. See Place The Conversation List.enableThreads: falseis the older shorthand forthreadPlacement: "disabled".enableExportadds whole-conversation and per-message export affordances.- Feedback defaults to auto: thumbs, dislike, and comment controls appear when
the server reports MLflow feedback is available and a turn produced a trace
id. Set
enableFeedback={false}to hide them. Viewers withCAN_MANAGEon the experiment also get an experiment debug link at the end of the action row.
Place The Conversation List
Section titled “Place The Conversation List”Conversation management is on by default. Where it lives is one option:
<MastraChat threadPlacement="top" />threadPlacement |
Layout |
|---|---|
auto (default) |
left while the chat is wide, top once it is too narrow for a panel |
left / right |
List docked to that edge, collapsing to an overlay drawer on a narrow chat |
top |
Tab strip of open conversations, with a history menu for the rest |
disabled |
No thread UI - the classic single-thread chat on the session cookie |
The docked placements render the sidebar as an inline column with a persisted show/hide toggle. Below 768px of chat width the panel becomes an overlay drawer on the same edge, so the transcript never loses room; the drawer is session-scoped and starts closed, and closes itself after a selection.
The top placement is the editor-tab model: each open conversation is a tab with
its title, a spinner while it streams in the background, and a close affordance
that only takes the tab off the strip (the conversation stays in history). A +
starts a fresh conversation and a history button opens the full list - the same
sidebar, framed as a menu - so rename, delete, and cancel still work on anything
not currently tabbed. Closing the active tab moves to a neighbouring one, or
starts a fresh conversation when it was the last tab open. The open set is
session state; the strip reseeds from the most recent conversations on the next
load.
Selecting a conversation fetches only the newest 20 native memory messages. Scrolling upward requests the next page and prepends it without moving the reader’s viewport. Agent turns still send only the new user message; Mastra loads the remembered transcript server-side.
auto measures the chat’s own element rather than the viewport, so a chat
embedded in a split view or side panel switches to tabs on the space it actually
has instead of waiting for the window to shrink.
Use The Headless Driver
Section titled “Use The Headless Driver”import { ChatView, useMastraChat } from "@dbx-tools/ui-mastra/react";
export function CustomChat() { const chat = useMastraChat({ agentId: "analyst", showModelPicker: true, threadPlacement: "left", });
return <ChatView {...chat} className="h-full" />;}Use useMastraChat() when the stock behavior is right but the surrounding layout
belongs to your app. The hook owns streaming, aborts, history paging, thread
selection, model overrides, suggestions, exports, approvals, and feedback state.
Pass Typed Application Context
Section titled “Pass Typed Application Context”requestContext accepts a typed plain record, Mastra RequestContext<T>, or a
per-turn resolver. The driver snapshots it when a message is submitted and
keeps that snapshot with queued steers, regeneration, and approval continuation.
type StoreContext = { storeId: string; route: string };
const chat = useMastraChat<StoreContext>({ requestContext: () => ({ storeId: selectedStore.id, route: location.pathname, }),});On the server, declare the same contract with Mastra’s native
requestContextSchema on createAgent<StoreContext>(). Application context
cannot override the authenticated memory resource, thread, user, scopes, model,
or trace fields stamped by AppKit-Mastra.
Build A Controlled Chat Surface
Section titled “Build A Controlled Chat Surface”import { ChatView, type ChatViewProps } from "@dbx-tools/ui-mastra/react";
export function ReviewChat(props: ChatViewProps) { return <ChatView {...props} className="h-[640px]" />;}ChatView is presentational. It renders the header, model picker, conversation
sidebar, transcript, tool progress, approval cards, suggestions, export controls,
feedback controls, and composer from props. Use it when your app already has a
transport or needs to combine Mastra messages with another state model.
MastraChat shows a spinner in the model control while its live default model
is being resolved. The picker groups endpoints into
model-family submenus. Its compact first popup shows only the current
model, host-owned actions such as Genie Agent Mode, and a reasoning slider when
the selected endpoint supports it. The current model opens the family picker,
so the long endpoint catalogue stays out of the primary control popup. Family
and model menus use the same compact utility type as the composer actions,
omit repeated headers, and omit the already-selected model. The model menu puts
Default (<resolved model>) first, keeps Other last, and makes a one-model
family directly selectable instead of opening another submenu.
Call Plugin Routes Directly
Section titled “Call Plugin Routes Directly”import { MastraPluginClient, useMastraClient, useMastraDefaultModel, useMastraModels, useMastraSuggestions, useMastraThreads,} from "@dbx-tools/ui-mastra/react";
const client = new MastraPluginClient(clientConfig);
// Routing (thread + model) is passed per call, so concurrent runs on// different threads never share state.const stream = await client.streamAgent({ agentId: client.defaultAgent, messages: [ { id: "user-1", role: "user", parts: [{ type: "text", text: "Hello" }], }, ], runId, threadId: activeThreadId, model: "claude sonnet", signal: controller.signal,});
const models = await client.models();const history = await client.history({ threadId: activeThreadId, page: 0, perPage: 20 });Each conversation thread runs independently: start a turn on one thread, switch
to another and start a second, and both stream concurrently. Cancel one via its
AbortSignal (or the driver’s onCancelThread) without touching the others.
MastraPluginClient extends @mastra/client-js with the official AI SDK
chatRoute transport and AppKit-Mastra additions. It uses
credentials: "include" so session cookies travel with streaming and REST
calls. The React hooks wrap native memory plus model catalogues, suggestions,
chart fetches, and statement-data fetches.
Approvals, Embeds, And Feedback
Section titled “Approvals, Embeds, And Feedback”The UI understands the extra events produced by
@dbx-tools/appkit-mastra:
- Approval chunks become inline approval cards and continue the official chat
route with Mastra’s native
{ approved, reason? }resume data. - Native tool-call and tool-result payloads remain complete. Expanding a tool row reveals default-closed Request and Response viewers; large values scroll without string or collection truncation.
- Genie
thinkingevents stream into the assistant’s collapsed Thoughts panel. Intermediate answer attachments and query-result tables stay on tool progress, not in Thoughts. Other writer events remain attached to tool progress, including SQL, row counts, result summaries, and chart/data markers. [chart:<id>]markers long-poll the chart cache and render ECharts inline.[data:<id>]markers fetch statement rows and render a sortable table with column toggles and CSV export.- MLflow trace headers enable per-message feedback controls when the server reports feedback is available.
Chart Theming
Section titled “Chart Theming”An ECharts chart draws to a canvas, so it inherits nothing from CSS the way its
frame does: a dark theme re-skins the border around the chart while the axis
labels, grid lines, and tooltip inside keep whatever colors the spec was born
with. And the spec is born on the server, in
@dbx-tools/appkit-mastra, which cannot know the
reader’s theme.
So the two halves are split. The planner inlines only what is
theme-independent - the brand’s series palette and font stack. Everything
theme-dependent is resolved here at render time by src/support/chart-theme.ts,
which reads AppKit’s --chart-axis-label, --chart-axis-title, --chart-grid,
and --chart-tooltip-bg (plus --popover-foreground and --border for the
tooltip) into a ChartChrome, and normalizeChartOption paints it onto the
axes, title, legend, and tooltip. Nothing to configure: a chart follows the
theme wherever it renders.
Two details worth knowing if you embed the chat:
- Tokens are read off the chart’s own element, not
:root, so a theme scoped to a subtree - a chat panel inside an otherwise-light app - wins. The value re-resolves when a.dark/.lightclass lands on the document root and when the OSprefers-color-schemeflips, so a live theme switch recolors charts in place. - AppKit darkens its tokens under
@media (prefers-color-scheme: dark)for any:rootthat is not explicitly.light. If your host renders its own light chrome, pin.light(or.dark) on:rootrather than leaving AppKit on the OS preference, or the chat will read as half-dark while everything around it stays light.
The PDF export is the one caller that does not follow the page: it pins
LIGHT_CHART_CHROME, because its document forces color-scheme: light on a
white body for printing.
Export Conversations
Section titled “Export Conversations”<MastraChat enableExport />Exports support Markdown downloads and PDF (rendered through a hidden print iframe, so the browser’s Save-as-PDF dialog opens with no popup tab). Chart and data markers are resolved during export: charts are rendered to inline SVG with ECharts’ server renderer, and data markers become real tables. Expired or missing embeds are skipped so old transcripts still export cleanly.
Copying And Downloading
Section titled “Copying And Downloading”Copy and download affordances appear in several places - message bubbles, code blocks, conversation export, and the data-grid CSV button - and they behave the same everywhere because each is backed by one internal module rather than per-component code.
Copying works on hosts served over plain HTTP. An insecure context has no
Clipboard API, so a bare navigator.clipboard.writeText silently does nothing on
a local or intranet deployment; the shared helper falls back to a hidden
<textarea> plus document.execCommand("copy") and reports whether the copy
actually succeeded. Downloads revoke their object URL after the temporary anchor
fires, so a long session does not accumulate blobs.
If you build a controlled surface with ChatView, you get both without wiring
anything. Adding a new copy or download button anywhere in this package should
reuse src/support/clipboard.ts and src/support/download.ts instead of
touching navigator.clipboard or URL.createObjectURL again.
Modules
Section titled “Modules”MastraChat- self-contained drop-in chat component.MastraAssistant/useMastraAssistant- persistent application shell and external open/close/context controller.useMastraChat- headless driver that returnsChatViewprops.ChatView- controlled presentational chat shell.MastraPluginClient- native@mastra/client-jsmemory/agent APIs plus AppKit-Mastra model, suggestion, feedback, and embed routes.useMastraClient,useMastraConfig,useMastraModels,useMastraDefaultModel,useMastraSuggestions,useMastraThreads,useChartFetch,useStatementFetch- route/config hooks for controlled clients.ThreadSidebar- controlled conversation list, dockable to either edge.ThreadTabs/ThreadActions- conversation pills plus reusable New/History controls for the top strip or persistent assistant header.ExportMenu- shared export format menu.src/support/thread-tabs.ts- pure open-tab bookkeeping (syncThreadTabs,closeThreadTab,nextActiveThreadTab) so the strip’s state is testable without a DOM.src/support/thread-labels.ts-threadTitle/relativeTime, shared by the sidebar and the tab strip so a row and a tab never disagree about how an untitled or freshly-updated conversation reads.src/support/request-context.ts- immutable per-run snapshots around Mastra’s native typedRequestContext.src/support/tool-events.ts- projection of native AI SDK persisted tool parts onto the live pill view model.src/support/chart-theme.ts-ChartChrome,LIGHT_CHART_CHROME,resolveChartChrome, and theuseChartChromehook that keeps an inline chart’s colors in step with the active theme.src/support/chart-option.ts-normalizeChartOption, the pure pass that patches layout (compact ticks, axis-name placement, title/grid spacing) and an optionalChartChromeinto a planner spec, shared by the inline chart and the PDF export so both read the same.- Types -
ChatViewProps,MastraChatProps,UseMastraChatOptions,MastraAssistantProps,MastraRequestContextInput,ThreadPlacement,ThreadSummary,ToolEvent,ToolProgress,PendingApproval,FeedbackSubmission, and related UI contract types.
Server-side routes and event production live in
@dbx-tools/appkit-mastra. Browser-safe route,
marker, feedback, and wire schemas live in
@dbx-tools/shared-mastra.