@dbx-tools/ui-mastra 0.3.21 → 0.3.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,13 +1,21 @@
1
- import {
2
- feedback,
3
- type MastraThread,
4
- } from "@dbx-tools/shared-mastra";
5
1
  import { error as errorUtil, log } from "@dbx-tools/shared-core";
2
+ import { feedback, type MastraThread } from "@dbx-tools/shared-mastra";
3
+ import { useBrand } from "@dbx-tools/ui-branding/react";
6
4
  import type { UIMessage } from "ai";
7
5
  import { nanoid } from "nanoid";
8
6
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
7
+ import { ChatView } from "./chat-view";
8
+ import { dedupeSuggestions } from "./suggestions";
9
+ import type {
10
+ ApprovalDecision,
11
+ ChatViewProps,
12
+ FeedbackSubmission,
13
+ MessageFeedback,
14
+ ThreadSummary,
15
+ ToolEvent,
16
+ ToolProgress,
17
+ } from "./types";
9
18
  import { exportChat, type EmbedResolver, type ExportFormat } from "../support/export";
10
- import { useBrand } from "@dbx-tools/ui-branding/react";
11
19
  import {
12
20
  useMastraClient,
13
21
  useMastraDefaultModel,
@@ -27,17 +35,6 @@ import {
27
35
  terminateRunningToolEvents,
28
36
  type ThreadSession,
29
37
  } from "../support/thread-sessions";
30
- import { ChatView } from "./chat-view";
31
- import { dedupeSuggestions } from "./suggestions";
32
- import type {
33
- ApprovalDecision,
34
- ChatViewProps,
35
- FeedbackSubmission,
36
- MessageFeedback,
37
- ThreadSummary,
38
- ToolEvent,
39
- ToolProgress,
40
- } from "./types";
41
38
 
42
39
  // Self-contained drop-in chat. `useMastraChat` drives the conversation
43
40
  // over `@mastra/client-js`: `agent.stream()` returns a Response
@@ -88,9 +85,7 @@ const TITLE_PREVIEW_MAX = 60;
88
85
  */
89
86
  const deriveThreadTitle = (text: string): string => {
90
87
  const clean = text.trim().replace(/\s+/g, " ");
91
- return clean.length > TITLE_PREVIEW_MAX
92
- ? `${clean.slice(0, TITLE_PREVIEW_MAX - 1)}…`
93
- : clean;
88
+ return clean.length > TITLE_PREVIEW_MAX ? `${clean.slice(0, TITLE_PREVIEW_MAX - 1)}…` : clean;
94
89
  };
95
90
 
96
91
  /**
@@ -155,8 +150,7 @@ const storeStoredSidebarOpen = (key: string, open: boolean): void => {
155
150
  * when absent - which is the "no feedback for this turn" signal.
156
151
  */
157
152
  const readMlflowTraceId = (stream: unknown): string | undefined => {
158
- const headers = (stream as { headers?: { get?: (name: string) => string | null } })
159
- ?.headers;
153
+ const headers = (stream as { headers?: { get?: (name: string) => string | null } })?.headers;
160
154
  const raw = headers?.get?.(feedback.MLFLOW_TRACE_ID_HEADER);
161
155
  return raw?.trim() || undefined;
162
156
  };
@@ -316,9 +310,7 @@ export const useMastraChat = (
316
310
  // without this the sidebar wouldn't show a brand-new conversation
317
311
  // until the delayed post-turn refresh. Keyed by thread id; each entry
318
312
  // is pruned once the real server row supersedes it.
319
- const [optimisticThreads, setOptimisticThreads] = useState<
320
- Record<string, ThreadSummary>
321
- >({});
313
+ const [optimisticThreads, setOptimisticThreads] = useState<Record<string, ThreadSummary>>({});
322
314
  // Optimistic title overrides for threads the user just renamed, keyed
323
315
  // by thread id. Applied over the server rows in `sidebarThreads` so the
324
316
  // new name shows instantly; each entry is dropped once the server list
@@ -329,9 +321,7 @@ export const useMastraChat = (
329
321
  // first question so the row stops reading "New conversation"; dropped
330
322
  // as soon as the server's auto-generated title lands (see the prune
331
323
  // effect below). A manual rename (`renamedThreads`) still wins.
332
- const [provisionalTitles, setProvisionalTitles] = useState<Record<string, string>>(
333
- {},
334
- );
324
+ const [provisionalTitles, setProvisionalTitles] = useState<Record<string, string>>({});
335
325
  // Surface the active thread in the sidebar immediately (called when its
336
326
  // first message is sent). Upserts an untitled row stamped "now" so it
337
327
  // sorts to the top; the server row replaces it on the next refresh.
@@ -395,10 +385,7 @@ export const useMastraChat = (
395
385
  [getSession, notifySessions],
396
386
  );
397
387
  const activeKey = sessionKey(activeThreadId);
398
- const activeSession = useMemo(
399
- () => getSession(activeKey),
400
- [activeKey, getSession, sessionsTick],
401
- );
388
+ const activeSession = useMemo(() => getSession(activeKey), [activeKey, getSession, sessionsTick]);
402
389
  const streamingThreadIds = useMemo(() => {
403
390
  const ids: string[] = [];
404
391
  for (const [id, session] of sessionsRef.current.entries()) {
@@ -469,8 +456,9 @@ export const useMastraChat = (
469
456
  let assistantReasoning = "";
470
457
  if (existing) {
471
458
  for (const part of existing.parts) {
472
- if (part.type === "text") textSegments.push(part.text);
473
- else if (part.type === "reasoning") {
459
+ if (part.type === "text") {
460
+ textSegments.push(part.text);
461
+ } else if (part.type === "reasoning") {
474
462
  assistantReasoning += (part as { text?: string }).text ?? "";
475
463
  }
476
464
  }
@@ -519,9 +507,7 @@ export const useMastraChat = (
519
507
  if (started) return;
520
508
  started = true;
521
509
  updateSession(threadId, (session) =>
522
- session.status === "streaming"
523
- ? session
524
- : { ...session, status: "streaming" },
510
+ session.status === "streaming" ? session : { ...session, status: "streaming" },
525
511
  );
526
512
  };
527
513
 
@@ -600,8 +586,7 @@ export const useMastraChat = (
600
586
  break;
601
587
  }
602
588
  updateSession(threadId, (session) => {
603
- const existingApprovals =
604
- session.pendingApprovalsByMessage[assistantId] ?? [];
589
+ const existingApprovals = session.pendingApprovalsByMessage[assistantId] ?? [];
605
590
  if (existingApprovals.some((a) => a.toolCallId === toolCallId)) {
606
591
  return session;
607
592
  }
@@ -647,9 +632,7 @@ export const useMastraChat = (
647
632
  const toolCallId = chunk.payload?.toolCallId;
648
633
  if (typeof toolCallId !== "string") break;
649
634
  patchToolEvents((list) =>
650
- list.map((e) =>
651
- e.id === toolCallId ? { ...e, status: "error" } : e,
652
- ),
635
+ list.map((e) => (e.id === toolCallId ? { ...e, status: "error" } : e)),
653
636
  );
654
637
  break;
655
638
  }
@@ -664,9 +647,7 @@ export const useMastraChat = (
664
647
  if (!isToolProgress(output)) break;
665
648
  patchToolEvents((list) =>
666
649
  list.map((e) =>
667
- e.id === toolCallId
668
- ? { ...e, progress: [...(e.progress ?? []), output] }
669
- : e,
650
+ e.id === toolCallId ? { ...e, progress: [...(e.progress ?? []), output] } : e,
670
651
  ),
671
652
  );
672
653
  break;
@@ -752,8 +733,7 @@ export const useMastraChat = (
752
733
  ...session,
753
734
  error: errorUtil.toError(caught),
754
735
  status: "error",
755
- abortController:
756
- session.abortController === controller ? null : session.abortController,
736
+ abortController: session.abortController === controller ? null : session.abortController,
757
737
  runId: runIdRef.current,
758
738
  }));
759
739
  }
@@ -773,8 +753,7 @@ export const useMastraChat = (
773
753
  // Capture this run's thread + model at send time and pass them per
774
754
  // call, so a run keeps its own routing even if the user switches
775
755
  // threads or changes the model picker while it streams.
776
- const streamThreadId =
777
- threadId === DEFAULT_THREAD_SESSION_KEY ? undefined : threadId;
756
+ const streamThreadId = threadId === DEFAULT_THREAD_SESSION_KEY ? undefined : threadId;
778
757
  const model = modelRef.current || undefined;
779
758
  return driveStream(threadId, assistantId, (signal) => {
780
759
  const messages = history.flatMap((m) =>
@@ -880,8 +859,7 @@ export const useMastraChat = (
880
859
  toolCallId,
881
860
  runId,
882
861
  });
883
- const streamThreadId =
884
- activeKey === DEFAULT_THREAD_SESSION_KEY ? undefined : activeKey;
862
+ const streamThreadId = activeKey === DEFAULT_THREAD_SESSION_KEY ? undefined : activeKey;
885
863
  await driveStream(activeKey, assistantId, (signal) =>
886
864
  decision.approved
887
865
  ? mastraClient.approveToolCallStream(agentId, {
@@ -1103,9 +1081,7 @@ export const useMastraChat = (
1103
1081
  if (!title) return;
1104
1082
  setRenamedThreads((prev) => ({ ...prev, [threadId]: title }));
1105
1083
  setOptimisticThreads((prev) =>
1106
- prev[threadId]
1107
- ? { ...prev, [threadId]: { ...prev[threadId], id: threadId, title } }
1108
- : prev,
1084
+ prev[threadId] ? { ...prev, [threadId]: { ...prev[threadId], id: threadId, title } } : prev,
1109
1085
  );
1110
1086
  try {
1111
1087
  await mastraClient.renameThread(threadId, title, { agentId });
@@ -1131,17 +1107,14 @@ export const useMastraChat = (
1131
1107
  const lastUserText = getSession(threadId).lastUserText;
1132
1108
  if (!lastUserText) return;
1133
1109
  const prev = getSession(threadId).messages;
1134
- const lastAssistant =
1135
- prev.length > 0 && prev.at(-1)?.role === "assistant" ? prev.at(-1) : null;
1110
+ const lastAssistant = prev.length > 0 && prev.at(-1)?.role === "assistant" ? prev.at(-1) : null;
1136
1111
  const trimmed = lastAssistant ? prev.slice(0, -1) : prev;
1137
1112
  if (lastAssistant) {
1138
1113
  updateSession(threadId, (session) => {
1139
- const { [lastAssistant.id]: _events, ...toolEvents } =
1140
- session.toolEventsByMessage;
1114
+ const { [lastAssistant.id]: _events, ...toolEvents } = session.toolEventsByMessage;
1141
1115
  const { [lastAssistant.id]: _approvals, ...pendingApprovals } =
1142
1116
  session.pendingApprovalsByMessage;
1143
- const { [lastAssistant.id]: _feedback, ...feedback } =
1144
- session.feedbackByMessage;
1117
+ const { [lastAssistant.id]: _feedback, ...feedback } = session.feedbackByMessage;
1145
1118
  return {
1146
1119
  ...session,
1147
1120
  toolEventsByMessage: toolEvents,
@@ -1244,15 +1217,7 @@ export const useMastraChat = (
1244
1217
  historyInFlightRef.current = false;
1245
1218
  setIsLoadingMore(false);
1246
1219
  });
1247
- }, [
1248
- activeKey,
1249
- activeThreadId,
1250
- getSession,
1251
- mastraClient,
1252
- agentId,
1253
- updateSession,
1254
- writeMessages,
1255
- ]);
1220
+ }, [activeKey, activeThreadId, getSession, mastraClient, agentId, updateSession, writeMessages]);
1256
1221
 
1257
1222
  // Chat export (opt-in). Resolves `[chart:<id>]` / `[data:<id>]` embeds
1258
1223
  // straight off the client so the export inlines the same charts /
@@ -1291,8 +1256,7 @@ export const useMastraChat = (
1291
1256
  const exportConversation = useCallback(
1292
1257
  async (format: ExportFormat) => {
1293
1258
  const title =
1294
- (activeThreadId && threads.find((t) => t.id === activeThreadId)?.title) ||
1295
- "Conversation";
1259
+ (activeThreadId && threads.find((t) => t.id === activeThreadId)?.title) || "Conversation";
1296
1260
  try {
1297
1261
  await exportChat({
1298
1262
  messages: getSession(activeKey).messages,
@@ -1309,15 +1273,7 @@ export const useMastraChat = (
1309
1273
  });
1310
1274
  }
1311
1275
  },
1312
- [
1313
- exportResolver,
1314
- activeThreadId,
1315
- activeKey,
1316
- getSession,
1317
- threads,
1318
- exportUserLabel,
1319
- exportBrand,
1320
- ],
1276
+ [exportResolver, activeThreadId, activeKey, getSession, threads, exportUserLabel, exportBrand],
1321
1277
  );
1322
1278
  const exportMessage = useCallback(
1323
1279
  async (message: UIMessage, format: ExportFormat) => {
@@ -1364,9 +1320,7 @@ export const useMastraChat = (
1364
1320
  try {
1365
1321
  const result = await mastraClient.feedback({
1366
1322
  traceId,
1367
- ...(submission.value !== undefined
1368
- ? { value: submission.value === "up" }
1369
- : {}),
1323
+ ...(submission.value !== undefined ? { value: submission.value === "up" } : {}),
1370
1324
  ...(submission.comment ? { comment: submission.comment } : {}),
1371
1325
  });
1372
1326
  if (!result.ok) {
@@ -1424,9 +1378,7 @@ export const useMastraChat = (
1424
1378
  // changes aren't masked by a stale override.
1425
1379
  useEffect(() => {
1426
1380
  const settled = threads
1427
- .filter(
1428
- (t) => renamedThreads[t.id] !== undefined && t.title === renamedThreads[t.id],
1429
- )
1381
+ .filter((t) => renamedThreads[t.id] !== undefined && t.title === renamedThreads[t.id])
1430
1382
  .map((t) => t.id);
1431
1383
  if (settled.length === 0) return;
1432
1384
  setRenamedThreads((prev) => {
@@ -20,11 +20,7 @@ export interface SuggestionPillsProps {
20
20
  * capsule shape that scales cleanly when text wraps to multiple lines.
21
21
  * Disabled when no `onSelect` is provided.
22
22
  */
23
- export const SuggestionPills = ({
24
- questions,
25
- onSelect,
26
- className,
27
- }: SuggestionPillsProps) => {
23
+ export const SuggestionPills = ({ questions, onSelect, className }: SuggestionPillsProps) => {
28
24
  if (questions.length === 0) return null;
29
25
  return (
30
26
  <div className={cn("flex flex-wrap gap-1.5", className)}>
@@ -1,3 +1,4 @@
1
+ import { wire } from "@dbx-tools/shared-mastra";
1
2
  import {
2
3
  Button,
3
4
  Input,
@@ -7,7 +8,6 @@ import {
7
8
  TooltipTrigger,
8
9
  cn,
9
10
  } from "@dbx-tools/ui-appkit/react";
10
- import { wire } from "@dbx-tools/shared-mastra";
11
11
  import {
12
12
  Loader2Icon,
13
13
  MessageSquarePlusIcon,
@@ -178,7 +178,6 @@ export const ThreadSidebar = ({
178
178
  <li key={thread.id}>
179
179
  <div className="px-2 py-1">
180
180
  <Input
181
- // eslint-disable-next-line jsx-a11y/no-autofocus -- focus the field the user just opened
182
181
  autoFocus
183
182
  value={draftTitle}
184
183
  maxLength={wire.MASTRA_THREAD_TITLE_MAX}
@@ -288,9 +287,7 @@ export const ThreadSidebar = ({
288
287
  setConfirmDeleteId((id) => (id === thread.id ? null : id))
289
288
  }
290
289
  aria-label={
291
- isConfirming
292
- ? "Confirm delete conversation"
293
- : "Delete conversation"
290
+ isConfirming ? "Confirm delete conversation" : "Delete conversation"
294
291
  }
295
292
  className={cn(
296
293
  "size-6 shrink-0",
@@ -1,3 +1,4 @@
1
+ import { genieModel } from "@dbx-tools/shared-genie";
1
2
  import {
2
3
  Collapsible,
3
4
  CollapsibleContent,
@@ -5,7 +6,6 @@ import {
5
6
  Spinner,
6
7
  cn,
7
8
  } from "@dbx-tools/ui-appkit/react";
8
- import { genieModel } from "@dbx-tools/shared-genie";
9
9
  import { CheckIcon, ChevronDownIcon, XIcon } from "lucide-react";
10
10
  import { SqlBlock, ToolMarkdown } from "./markdown";
11
11
  import type { ToolEvent, ToolProgress } from "./types";
@@ -176,10 +176,7 @@ const summarizeProgress = (progress: ToolProgress[]): ToolDetailSummary => {
176
176
  // different `message_id`s can collide on `attachment_id` (Genie
177
177
  // restarts numbering per message), so the bucket map is owned by
178
178
  // the group, not the summary.
179
- const bucketFor = (
180
- group: MessageGroup,
181
- attachmentId: string | undefined,
182
- ): AttachmentBucket => {
179
+ const bucketFor = (group: MessageGroup, attachmentId: string | undefined): AttachmentBucket => {
183
180
  const key = attachmentId ?? ANON_ATTACHMENT_KEY;
184
181
  let bucket = group.attachments.find((b) => b.key === key);
185
182
  if (!bucket) {
@@ -284,9 +281,7 @@ const isAttachmentRenderable = (b: AttachmentBucket): boolean =>
284
281
 
285
282
  /** True when a group has any renderable content (question, attachments, or errors). */
286
283
  const isGroupRenderable = (g: MessageGroup): boolean =>
287
- Boolean(g.question) ||
288
- g.attachments.some(isAttachmentRenderable) ||
289
- g.errors.length > 0;
284
+ Boolean(g.question) || g.attachments.some(isAttachmentRenderable) || g.errors.length > 0;
290
285
 
291
286
  /**
292
287
  * Body of one Genie sub-call (one `message_id` group). Pulled out
@@ -342,10 +337,7 @@ const MessageGroupBody = ({
342
337
  // SQL itself stays a nested Collapsible (default closed)
343
338
  // for the same reason: code is the heaviest content here,
344
339
  // and most readers only want a glance.
345
- <Collapsible
346
- key={bucket.key}
347
- className="rounded border border-border/60 bg-background/40"
348
- >
340
+ <Collapsible key={bucket.key} className="rounded border border-border/60 bg-background/40">
349
341
  <CollapsibleTrigger className="group flex w-full items-center gap-1.5 px-2 py-1 text-left text-[11px] uppercase tracking-wide text-muted-foreground hover:text-foreground">
350
342
  <ChevronDownIcon className="size-3 shrink-0 transition-transform group-data-[state=closed]:-rotate-90" />
351
343
  <span>{queryBuckets.length > 1 ? `Query ${i + 1}` : "Query"}</span>
@@ -360,10 +352,7 @@ const MessageGroupBody = ({
360
352
  {bucket.thinking.length > 0 && (
361
353
  <ul className="flex flex-col gap-1.5 border-l-2 border-border/60 pl-3 text-muted-foreground">
362
354
  {bucket.thinking.map((p, j) => (
363
- <li
364
- key={`think-${j}`}
365
- className="whitespace-pre-wrap break-words leading-snug"
366
- >
355
+ <li key={`think-${j}`} className="whitespace-pre-wrap break-words leading-snug">
367
356
  <span className="font-medium text-foreground/80">
368
357
  {humanizeThoughtType(p.thought_type)}:
369
358
  </span>{" "}
@@ -402,10 +391,7 @@ const MessageGroupBody = ({
402
391
  // when there's only one prose bucket; multiple buckets
403
392
  // (rare - typically interpretation + a follow-up question)
404
393
  // get numbered so each row is addressable.
405
- <Collapsible
406
- key={bucket.key}
407
- className="rounded border border-border/60 bg-background/40"
408
- >
394
+ <Collapsible key={bucket.key} className="rounded border border-border/60 bg-background/40">
409
395
  <CollapsibleTrigger className="group flex w-full items-center gap-1.5 px-2 py-1 text-left text-[11px] uppercase tracking-wide text-muted-foreground hover:text-foreground">
410
396
  <ChevronDownIcon className="size-3 shrink-0 transition-transform group-data-[state=closed]:-rotate-90" />
411
397
  <span>{proseBuckets.length > 1 ? `Answer ${i + 1}` : "Answer"}</span>
@@ -415,10 +401,7 @@ const MessageGroupBody = ({
415
401
  {bucket.thinking.length > 0 && (
416
402
  <ul className="flex flex-col gap-1 text-[11px] text-muted-foreground">
417
403
  {bucket.thinking.map((p, j) => (
418
- <li
419
- key={`think-${j}`}
420
- className="whitespace-pre-wrap break-words leading-snug"
421
- >
404
+ <li key={`think-${j}`} className="whitespace-pre-wrap break-words leading-snug">
422
405
  <span className="font-medium text-foreground/80">
423
406
  {humanizeThoughtType(p.thought_type)}:
424
407
  </span>{" "}
@@ -534,18 +517,11 @@ const ToolCallRow = ({ event }: { event: ToolEvent }) => {
534
517
 
535
518
  const header = (
536
519
  <span className="min-w-0 flex-1">
537
- <span
538
- className={cn(
539
- "block truncate",
540
- isRunning && "animate-pulse text-foreground/90",
541
- )}
542
- >
520
+ <span className={cn("block truncate", isRunning && "animate-pulse text-foreground/90")}>
543
521
  {verb}
544
522
  </span>
545
523
  {question && (
546
- <span className="mt-0.5 block break-words italic text-foreground/80">
547
- {question}
548
- </span>
524
+ <span className="mt-0.5 block break-words italic text-foreground/80">{question}</span>
549
525
  )}
550
526
  </span>
551
527
  );
@@ -603,11 +579,7 @@ export const ToolSessionPill = ({ events }: { events: ToolEvent[] }) => {
603
579
  const latest = events[events.length - 1]!;
604
580
  const anyRunning = events.some((e) => e.status === "running");
605
581
  const anyError = events.some((e) => e.status === "error");
606
- const tone: "running" | "error" | "done" = anyRunning
607
- ? "running"
608
- : anyError
609
- ? "error"
610
- : "done";
582
+ const tone: "running" | "error" | "done" = anyRunning ? "running" : anyError ? "error" : "done";
611
583
 
612
584
  // Outer header uses the tool name verb (not the live wire
613
585
  // status) per the "show the most recent tool call" contract.
@@ -654,9 +626,7 @@ export const ToolSessionPill = ({ events }: { events: ToolEvent[] }) => {
654
626
  )}
655
627
  >
656
628
  {verb}
657
- {countSuffix && (
658
- <span className="text-muted-foreground/70">{countSuffix}</span>
659
- )}
629
+ {countSuffix && <span className="text-muted-foreground/70">{countSuffix}</span>}
660
630
  </span>
661
631
  {/*
662
632
  * Trailing "live" pip on the right edge while any tool is
@@ -300,10 +300,7 @@ export type ChatViewProps = {
300
300
  * the bubble shows thumbs up/down plus a comment affordance. The
301
301
  * handler logs the feedback to MLflow via the plugin's feedback route.
302
302
  */
303
- onFeedback?: (
304
- message: UIMessage,
305
- submission: FeedbackSubmission,
306
- ) => void | Promise<void>;
303
+ onFeedback?: (message: UIMessage, submission: FeedbackSubmission) => void | Promise<void>;
307
304
  };
308
305
 
309
306
  /** Payload {@link ChatViewProps.onResolveToolApproval} receives. */
@@ -48,9 +48,7 @@ const isObj = object.isRecord;
48
48
  /** True when `title` (object or array) carries any non-empty `text`. */
49
49
  function hasTitleText(title: unknown): boolean {
50
50
  const entries = Array.isArray(title) ? title : [title];
51
- return entries.some(
52
- (t) => isObj(t) && typeof t.text === "string" && t.text.trim().length > 0,
53
- );
51
+ return entries.some((t) => isObj(t) && typeof t.text === "string" && t.text.trim().length > 0);
54
52
  }
55
53
 
56
54
  /** Pin a title to the top-center so it clears the plot / axis names. */
@@ -109,8 +107,7 @@ function normalizeAxis(axis: Obj, pos: "x" | "y"): Obj {
109
107
 
110
108
  /** Apply {@link normalizeAxis} across an axis field (object or array). */
111
109
  function normalizeAxisField(axis: unknown, pos: "x" | "y"): unknown {
112
- if (Array.isArray(axis))
113
- return axis.map((a) => (isObj(a) ? normalizeAxis(a, pos) : a));
110
+ if (Array.isArray(axis)) return axis.map((a) => (isObj(a) ? normalizeAxis(a, pos) : a));
114
111
  return isObj(axis) ? normalizeAxis(axis, pos) : axis;
115
112
  }
116
113
 
@@ -23,12 +23,8 @@
23
23
  * @module
24
24
  */
25
25
 
26
- import {
27
- marker as markers,
28
- type Chart,
29
- type StatementData,
30
- } from "@dbx-tools/shared-mastra";
31
26
  import { string } from "@dbx-tools/shared-core";
27
+ import { marker as markers, type Chart, type StatementData } from "@dbx-tools/shared-mastra";
32
28
  import type { UIMessage } from "ai";
33
29
  import * as echarts from "echarts";
34
30
  import { marked } from "marked";
@@ -188,9 +184,7 @@ function printViaHiddenIframe(html: string, downloadName: string): void {
188
184
 
189
185
  /** One slice of a message: prose, a chart embed, or a data embed. */
190
186
  type Segment =
191
- | { kind: "text"; text: string }
192
- | { kind: "chart"; id: string }
193
- | { kind: "data"; id: string };
187
+ { kind: "text"; text: string } | { kind: "chart"; id: string } | { kind: "data"; id: string };
194
188
 
195
189
  /**
196
190
  * Split message text into prose / chart / data segments at
@@ -268,19 +262,14 @@ async function buildHtmlDocument(
268
262
  }
269
263
 
270
264
  /** Render one message's body (prose + inlined charts / tables) to HTML. */
271
- async function messageBodyHtml(
272
- message: UIMessage,
273
- resolver: EmbedResolver,
274
- ): Promise<string> {
265
+ async function messageBodyHtml(message: UIMessage, resolver: EmbedResolver): Promise<string> {
275
266
  const isAssistant = message.role === "assistant";
276
267
  const parts: string[] = [];
277
268
  for (const seg of splitSegments(messageText(message))) {
278
269
  if (seg.kind === "text") {
279
270
  if (!seg.text.trim()) continue;
280
271
  parts.push(
281
- isAssistant
282
- ? markdownToHtml(seg.text)
283
- : `<p class="plain">${escapeHtml(seg.text)}</p>`,
272
+ isAssistant ? markdownToHtml(seg.text) : `<p class="plain">${escapeHtml(seg.text)}</p>`,
284
273
  );
285
274
  continue;
286
275
  }
@@ -320,9 +309,7 @@ async function chartSvg(resolver: EmbedResolver, id: string): Promise<string | n
320
309
  height: CHART_HEIGHT_PX,
321
310
  });
322
311
  try {
323
- instance.setOption(
324
- normalizeChartOption(chart.result.option) as echarts.EChartsCoreOption,
325
- );
312
+ instance.setOption(normalizeChartOption(chart.result.option) as echarts.EChartsCoreOption);
326
313
  return instance.renderToSVGString();
327
314
  } finally {
328
315
  instance.dispose();
@@ -333,21 +320,14 @@ async function chartSvg(resolver: EmbedResolver, id: string): Promise<string | n
333
320
  }
334
321
 
335
322
  /** Resolve a data id and render its rows to an HTML table. `null` on miss. */
336
- async function dataTableHtml(
337
- resolver: EmbedResolver,
338
- id: string,
339
- ): Promise<string | null> {
323
+ async function dataTableHtml(resolver: EmbedResolver, id: string): Promise<string | null> {
340
324
  const data = await safeStatement(resolver, id);
341
325
  if (!data || data.rows.length === 0) return null;
342
- const head = data.columns
343
- .map((c) => `<th>${escapeHtml(string.toLabel(c))}</th>`)
344
- .join("");
326
+ const head = data.columns.map((c) => `<th>${escapeHtml(string.toLabel(c))}</th>`).join("");
345
327
  const body = data.rows
346
328
  .map(
347
329
  (row) =>
348
- `<tr>${data.columns
349
- .map((c) => `<td>${escapeHtml(cellText(row[c]))}</td>`)
350
- .join("")}</tr>`,
330
+ `<tr>${data.columns.map((c) => `<td>${escapeHtml(cellText(row[c]))}</td>`).join("")}</tr>`,
351
331
  )
352
332
  .join("");
353
333
  const note = data.truncated
@@ -379,10 +359,7 @@ async function buildMarkdown(
379
359
  }
380
360
 
381
361
  /** Render one message's body to Markdown (charts noted, tables as GFM). */
382
- async function messageBodyMarkdown(
383
- message: UIMessage,
384
- resolver: EmbedResolver,
385
- ): Promise<string> {
362
+ async function messageBodyMarkdown(message: UIMessage, resolver: EmbedResolver): Promise<string> {
386
363
  const parts: string[] = [];
387
364
  for (const seg of splitSegments(messageText(message))) {
388
365
  if (seg.kind === "text") {
@@ -405,17 +382,13 @@ function chartTitle(chart: Chart): string {
405
382
  const option = chart.result?.option as { title?: unknown } | undefined;
406
383
  const title = option?.title;
407
384
  const text =
408
- (Array.isArray(title) ? title[0]?.text : (title as { text?: unknown })?.text) ??
409
- undefined;
385
+ (Array.isArray(title) ? title[0]?.text : (title as { text?: unknown })?.text) ?? undefined;
410
386
  const label = typeof text === "string" ? text.trim() : "";
411
387
  return label || `${chart.result?.chartType ?? "chart"} chart`;
412
388
  }
413
389
 
414
390
  /** Render statement rows to a GFM table. `null` on miss / empty. */
415
- async function dataTableMarkdown(
416
- resolver: EmbedResolver,
417
- id: string,
418
- ): Promise<string | null> {
391
+ async function dataTableMarkdown(resolver: EmbedResolver, id: string): Promise<string | null> {
419
392
  const data = await safeStatement(resolver, id);
420
393
  if (!data || data.rows.length === 0) return null;
421
394
  const header = `| ${data.columns.map((c) => mdCell(string.toLabel(c))).join(" | ")} |`;
@@ -423,19 +396,14 @@ async function dataTableMarkdown(
423
396
  const rows = data.rows.map(
424
397
  (row) => `| ${data.columns.map((c) => mdCell(cellText(row[c]))).join(" | ")} |`,
425
398
  );
426
- const note = data.truncated
427
- ? `\n\n_Showing ${data.rows.length} of ${data.rowCount} rows._`
428
- : "";
399
+ const note = data.truncated ? `\n\n_Showing ${data.rows.length} of ${data.rowCount} rows._` : "";
429
400
  return `${[header, sep, ...rows].join("\n")}${note}`;
430
401
  }
431
402
 
432
403
  /* -------------------------------- helpers -------------------------------- */
433
404
 
434
405
  /** Resolve a chart id, swallowing errors (best-effort export). */
435
- async function safeChart(
436
- resolver: EmbedResolver,
437
- id: string,
438
- ): Promise<Chart | undefined> {
406
+ async function safeChart(resolver: EmbedResolver, id: string): Promise<Chart | undefined> {
439
407
  try {
440
408
  return await resolver.chart(id);
441
409
  } catch {