Skip to content

@dbx-tools/ui-mastra

React chat UI for the AppKit-Mastra plugin.

Import this package when a Databricks App needs a production-ready chat surface for @dbx-tools/appkit-mastra: streaming assistant responses, model selection, Genie progress events, inline charts/data tables, tool approvals, conversation history, thread management, export, and MLflow feedback.

Key features:

  • Drop-in MastraChat component that discovers the plugin client config and wires itself to the default agent.
  • Headless useMastraChat() driver for apps that want the same transport logic with custom layout.
  • Controlled ChatView for hosts that own messages, streaming, model state, and route calls themselves.
  • MastraPluginClient wrapper around @mastra/client-js with AppKit-Mastra routes for history, threads, model lists, suggestions, feedback, charts, and statement data.
  • Tool-approval support for suspended Mastra requireApproval calls, including direct resumed-stream handling. An email-shaped input (the send_email tool) renders as a formatted To / Cc / Subject / Markdown-body preview rather than raw JSON.
  • 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 auto pick 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.

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/client-js agent streaming plus the plugin’s custom history, threads, models, suggestions, feedback, chart, and statement routes.
  • Suspended requireApproval tool calls and resumed approve/deny streams.
  • 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.
@import "@databricks/appkit-ui/styles.css";
@import "@dbx-tools/ui-mastra/styles.css";

The stylesheet imports the shared @dbx-tools/ui-appkit 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.

import { MastraChat } from "@dbx-tools/ui-mastra/react";
export function App() {
return (
<MastraChat
agentId="analyst"
showModelPicker
threadPlacement="auto"
enableExport
enableFeedback
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 agent.stream(), hydrates the latest history page, and renders the controlled ChatView.

Useful options:

  • agentId selects a registered agent; defaults to the plugin default agent.
  • showModelPicker fetches /models and sends X-Mastra-Model overrides.
  • suggestions overrides Genie starter questions; omit it to auto-fetch /suggestions, or pass [] to hide suggestions.
  • threadPlacement chooses where conversation management renders, or turns it off. See Place The Conversation List.
  • enableThreads: false is the older shorthand for threadPlacement: "disabled".
  • enableExport adds whole-conversation and per-message export affordances.
  • enableFeedback enables thumbs/comment controls when the server reports MLflow feedback is available and a turn produced a trace id.

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.

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.

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.

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.

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: [{ role: "user", content: "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 AppKit-Mastra custom routes. It uses credentials: "include" so session cookies travel with streaming and REST calls. The React hooks wrap common route calls for model catalogues, suggestions, thread lists, chart fetches, and statement-data fetches.

The UI understands the extra events produced by @dbx-tools/appkit-mastra:

  • tool-call-approval chunks become inline approval cards and call approve-tool-call / decline-tool-call when the user decides.
  • Genie writer events render as tool progress, including thinking text, 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.

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 / .light class lands on the document root and when the OS prefers-color-scheme flips, so a live theme switch recolors charts in place.
  • AppKit darkens its tokens under @media (prefers-color-scheme: dark) for any :root that is not explicitly .light. If your host renders its own light chrome, pin .light (or .dark) on :root rather 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.

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

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.

  • MastraChat - self-contained drop-in chat component.
  • useMastraChat - headless driver that returns ChatView props.
  • ChatView - controlled presentational chat shell.
  • MastraPluginClient - @mastra/client-js plus AppKit-Mastra custom routes.
  • useMastraClient, useMastraConfig, useMastraModels, useMastraDefaultModel, useMastraSuggestions, useMastraThreads, useChartFetch, useStatementFetch - route/config hooks for controlled clients.
  • ThreadSidebar - controlled conversation list, dockable to either edge.
  • ThreadTabs - conversation tab strip plus history menu for the top placement; reuses ThreadSidebar for the menu itself.
  • 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/chart-theme.ts - ChartChrome, LIGHT_CHART_CHROME, resolveChartChrome, and the useChartChrome hook 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 optional ChartChrome into a planner spec, shared by the inline chart and the PDF export so both read the same.
  • Types - ChatViewProps, MastraChatProps, UseMastraChatOptions, 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.