@dbx-tools/ui-mastra 0.9.108 → 0.9.111

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.
package/index.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  // Hand edits are overwritten on the next watch; this file is read-only.
4
4
 
5
5
  export const PACKAGE_IDENTIFIER = "@dbx-tools/ui-mastra";
6
- export const PACKAGE_VERSION = "0.9.108";
6
+ export const PACKAGE_VERSION = "0.9.111";
7
7
  export * as reactBubbles from "./src/react/bubbles.tsx";
8
8
  export * as reactChatApprovals from "./src/react/chat-approvals.ts";
9
9
  export * as reactChatComposer from "./src/react/chat-composer.tsx";
@@ -52,7 +52,7 @@ export type { DataRow } from "./src/react/data-grid.tsx";
52
52
  export { MarkdownWithEmbeds } from "./src/react/embed-slots.tsx";
53
53
  export { ExportMenu } from "./src/react/export-menu.tsx";
54
54
  export { FeedbackControls } from "./src/react/feedback-controls.tsx";
55
- export { AssistantMarkdown, ToolMarkdown, SqlBlock, JsonBlock } from "./src/react/markdown.tsx";
55
+ export { AssistantMarkdown, ToolMarkdown, SqlBlock, JsonBlock, SourceBlock } from "./src/react/markdown.tsx";
56
56
  export type { MastraAssistantMode, MastraAssistantSide, MastraAssistantLauncherPosition, MastraAssistantResizeOptions, MastraAssistantLauncherOptions, MastraAssistantController, MastraAssistantProps } from "./src/react/mastra-assistant.tsx";
57
57
  export { useMastraChat, MastraChat } from "./src/react/mastra-chat.tsx";
58
58
  export type { UseMastraChatOptions, MastraChatProps } from "./src/react/mastra-chat.tsx";
@@ -64,7 +64,7 @@ export type { ThreadSidebarProps } from "./src/react/thread-sidebar.tsx";
64
64
  export { ThreadActions, ThreadTabs } from "./src/react/thread-tabs.tsx";
65
65
  export type { ThreadTabsProps, ThreadActionsProps } from "./src/react/thread-tabs.tsx";
66
66
  export { humanizeToolName, ToolSessionPill } from "./src/react/tool-pill.tsx";
67
- export type { WebSearchProgressGroup } from "./src/react/tool-pill.tsx";
67
+ export type { ToolInputPresentation, WebSearchProgressGroup } from "./src/react/tool-pill.tsx";
68
68
  export type { ChatStatus, ToolEvent, ToolProgress, ChatModelOption, QueuedSteer, FeedbackValue, FeedbackSubmission, MessageFeedback, ThreadSummary, ThreadPlacement, ChatViewProps, ApprovalDecision, PendingApproval } from "./src/react/types.ts";
69
69
  export { LIGHT_CHART_CHROME } from "./src/support/chart-theme.ts";
70
70
  export type { ChartChrome } from "./src/support/chart-theme.ts";
package/package.json CHANGED
@@ -29,11 +29,11 @@
29
29
  "react-dom": "~19.2.4"
30
30
  },
31
31
  "dependencies": {
32
- "@dbx-tools/shared-core": "^0.9.108",
33
- "@dbx-tools/shared-genie": "^0.9.108",
34
- "@dbx-tools/shared-mastra": "^0.9.108",
35
- "@dbx-tools/shared-model": "^0.9.108",
36
- "@dbx-tools/ui": "^0.9.108",
32
+ "@dbx-tools/shared-core": "^0.9.111",
33
+ "@dbx-tools/shared-genie": "^0.9.111",
34
+ "@dbx-tools/shared-mastra": "^0.9.111",
35
+ "@dbx-tools/shared-model": "^0.9.111",
36
+ "@dbx-tools/ui": "^0.9.111",
37
37
  "@mastra/client-js": "1.50.0",
38
38
  "@mastra/core": "1.71.0",
39
39
  "@tanstack/react-table": "^8.21.3",
@@ -50,7 +50,7 @@
50
50
  "publishConfig": {
51
51
  "access": "public"
52
52
  },
53
- "version": "0.9.108",
53
+ "version": "0.9.111",
54
54
  "dbxToolsConfig": {
55
55
  "tags": [
56
56
  "ui"
@@ -23,6 +23,7 @@ class StreamAborted extends Error {}
23
23
  /** Stop reading a background run once the AI SDK has delivered its terminal chunk. */
24
24
  export function closeOnTerminalChunk(
25
25
  stream: ReadableStream<UIMessageChunk>,
26
+ onTerminal?: () => void,
26
27
  ): ReadableStream<UIMessageChunk> {
27
28
  let reader: ReadableStreamDefaultReader<UIMessageChunk> | undefined;
28
29
  let cancelled = false;
@@ -38,6 +39,7 @@ export function closeOnTerminalChunk(
38
39
  }
39
40
  controller.enqueue(value);
40
41
  if (value.type === "finish" || value.type === "abort") {
42
+ onTerminal?.();
41
43
  controller.close();
42
44
  await reader.cancel("terminal AI SDK chunk received").catch(() => undefined);
43
45
  return;
@@ -101,12 +103,15 @@ export function useChatStream({ getSession, updateSession, writeMessages }: UseC
101
103
  }
102
104
 
103
105
  try {
106
+ let terminalReceived = false;
104
107
  const existing = getSession(threadId).messages.find(
105
108
  (message) => message.id === assistantId,
106
109
  );
107
110
  for await (const message of readUIMessageStream({
108
111
  ...(existing ? { message: existing } : {}),
109
- stream: closeOnTerminalChunk(stream.stream),
112
+ stream: closeOnTerminalChunk(stream.stream, () => {
113
+ terminalReceived = true;
114
+ }),
110
115
  terminateOnError: true,
111
116
  })) {
112
117
  if (signal.aborted) throw new StreamAborted();
@@ -115,7 +120,7 @@ export function useChatStream({ getSession, updateSession, writeMessages }: UseC
115
120
  updateSession(threadId, (current) => ({
116
121
  ...current,
117
122
  runId: runIdRef.current,
118
- status: "streaming",
123
+ status: terminalReceived ? "ready" : "streaming",
119
124
  }));
120
125
  }
121
126
  } catch (error) {
@@ -418,3 +418,30 @@ export const JsonBlock = ({ json, className }: { json: string; className?: strin
418
418
  </pre>
419
419
  );
420
420
  };
421
+
422
+ /** Compact syntax-highlighted source block for tool request previews. */
423
+ export const SourceBlock = ({
424
+ source,
425
+ language,
426
+ className,
427
+ }: {
428
+ source: string;
429
+ language: string;
430
+ className?: string;
431
+ }) => {
432
+ const html = useHighlightedHtml(source, language);
433
+ return (
434
+ <pre
435
+ className={cn(
436
+ "max-w-full overflow-auto whitespace-pre font-mono text-xs leading-snug",
437
+ className,
438
+ )}
439
+ >
440
+ {html === null ? (
441
+ <code>{source}</code>
442
+ ) : (
443
+ <code data-dbx-highlighted-code dangerouslySetInnerHTML={{ __html: html }} />
444
+ )}
445
+ </pre>
446
+ );
447
+ };
@@ -202,6 +202,12 @@ export interface UseMastraChatOptions<
202
202
  * inlined into the export so it renders reliably offline.
203
203
  */
204
204
  enableExport?: boolean;
205
+ /**
206
+ * Show the destructive Clear conversation action. Off by default because
207
+ * thread management already provides new/delete controls and most chats do
208
+ * not need a second way to erase the active conversation.
209
+ */
210
+ enableClear?: boolean;
205
211
  /**
206
212
  * Surface per-message feedback controls (thumbs up/down + a comment
207
213
  * modal) that log to MLflow as trace assessments.
@@ -277,6 +283,9 @@ export const useMastraChat = <TValues extends Record<string, unknown> = Record<s
277
283
  const enableThreads = threadPlacement !== "disabled";
278
284
  // Export is opt-in (default off): the host turns it on explicitly.
279
285
  const enableExport = options.enableExport === true;
286
+ // Clearing is destructive and redundant with managed thread deletion, so
287
+ // the drop-in exposes it only when a host explicitly opts in.
288
+ const enableClear = options.enableClear === true;
280
289
  // Feedback defaults to automatic server capability detection. An explicit
281
290
  // false hides it; true keeps it requested but cannot bypass the server gate.
282
291
  const enableFeedback = options.enableFeedback !== false;
@@ -1080,7 +1089,7 @@ export const useMastraChat = <TValues extends Record<string, unknown> = Record<s
1080
1089
  isLoadingMore,
1081
1090
  hasMore: activeSession.hasMoreHistory,
1082
1091
  isLoadingHistory,
1083
- onClear: handleClear,
1092
+ ...(enableClear ? { onClear: handleClear } : {}),
1084
1093
  threadPlacement,
1085
1094
  // Conversation management: hand ChatView the thread list + handlers
1086
1095
  // only when enabled, so the sidebar stays hidden for the classic
@@ -8,7 +8,7 @@ import {
8
8
  cn,
9
9
  } from "@dbx-tools/ui/react";
10
10
  import { CheckIcon, ChevronDownIcon, XIcon } from "lucide-react";
11
- import { JsonBlock, SqlBlock, ToolMarkdown } from "./markdown.tsx";
11
+ import { JsonBlock, SourceBlock, SqlBlock, ToolMarkdown } from "./markdown.tsx";
12
12
  import type { ToolEvent, ToolProgress } from "./types.ts";
13
13
 
14
14
  // Consolidated tool-session pill and its Genie progress detail view:
@@ -22,6 +22,9 @@ import type { ToolEvent, ToolProgress } from "./types.ts";
22
22
  * `Execute Command` rather than `Mastra Workspace Execute Command`.
23
23
  */
24
24
  const MASTRA_WORKSPACE_TOOL_PREFIX = "mastra_workspace_";
25
+ const TOOL_LABELS: Readonly<Record<string, string>> = {
26
+ execute_typescript: "Run Workflow",
27
+ };
25
28
 
26
29
  /**
27
30
  * Turn a snake/camel tool id into a Title Cased label the user can
@@ -38,6 +41,8 @@ const MASTRA_WORKSPACE_TOOL_PREFIX = "mastra_workspace_";
38
41
  * `myCoolTool` -> `My Cool Tool`
39
42
  */
40
43
  export const humanizeToolName = (toolName: string): string => {
44
+ const explicitLabel = TOOL_LABELS[toolName];
45
+ if (explicitLabel) return explicitLabel;
41
46
  const label = toolName.startsWith(MASTRA_WORKSPACE_TOOL_PREFIX)
42
47
  ? toolName.slice(MASTRA_WORKSPACE_TOOL_PREFIX.length)
43
48
  : toolName;
@@ -398,6 +403,108 @@ const RawToolPayload = ({ label, value }: { label: "Request" | "Response"; value
398
403
  </Collapsible>
399
404
  );
400
405
 
406
+ export type ToolInputPresentation = {
407
+ label: "Code" | "Command" | "Output";
408
+ language: "typescript" | "javascript" | "python" | "sql" | "json" | "bash" | "text";
409
+ source: string;
410
+ };
411
+
412
+ const inputRecord = (value: unknown): Record<string, unknown> | undefined =>
413
+ typeof value === "object" && value !== null && !Array.isArray(value)
414
+ ? (value as Record<string, unknown>)
415
+ : undefined;
416
+
417
+ /** Detect the most useful syntax grammar for a code-like tool request. */
418
+ export function detectToolSourceLanguage(
419
+ source: string,
420
+ fallback: ToolInputPresentation["language"],
421
+ ): ToolInputPresentation["language"] {
422
+ const trimmed = source.trim();
423
+ if (/^(?:select|with|insert|update|delete|merge|create|alter)\b/i.test(trimmed)) return "sql";
424
+ if (/^(?:from\s+\S+\s+import|import\s+\S+|def\s+\w+|class\s+\w+):?/m.test(trimmed)) {
425
+ return "python";
426
+ }
427
+ if (/^(?:const|let|var|interface|type|export|import)\b/m.test(trimmed)) return "typescript";
428
+ if (/^(?:#!.*\b(?:ba|z|k)?sh\b|(?:cd|ls|cat|grep|find|bun|npm|pnpm|yarn)\s)/m.test(trimmed)) {
429
+ return "bash";
430
+ }
431
+ if ((trimmed.startsWith("{") || trimmed.startsWith("[")) && trimmed.length > 1) {
432
+ try {
433
+ JSON.parse(trimmed);
434
+ return "json";
435
+ } catch {
436
+ // Keep the tool-specific fallback for incomplete source.
437
+ }
438
+ }
439
+ return fallback;
440
+ }
441
+
442
+ /** Extract a compact code or command preview from a tool request payload. */
443
+ export function toolInputPresentation(
444
+ toolName: string,
445
+ input: unknown,
446
+ ): ToolInputPresentation | undefined {
447
+ const record = inputRecord(input);
448
+ if (!record) return undefined;
449
+ if (typeof record.command === "string" && toolName.endsWith("execute_command")) {
450
+ return {
451
+ label: "Command",
452
+ language: detectToolSourceLanguage(record.command, "bash"),
453
+ source: record.command,
454
+ };
455
+ }
456
+ if (typeof record.code === "string") {
457
+ return {
458
+ label: "Code",
459
+ language: detectToolSourceLanguage(record.code, "typescript"),
460
+ source: record.code,
461
+ };
462
+ }
463
+ return undefined;
464
+ }
465
+
466
+ /** Extract Code Mode's completed result into its own compact output panel. */
467
+ export function toolOutputPresentation(
468
+ toolName: string,
469
+ output: unknown,
470
+ ): ToolInputPresentation | undefined {
471
+ if (toolName !== "execute_typescript") return undefined;
472
+ const record = inputRecord(output);
473
+ if (!record || record.success !== true || !("result" in record)) return undefined;
474
+ const result = record.result;
475
+ const source = typeof result === "string" ? result : formatRawToolPayload(result);
476
+ return {
477
+ label: "Output",
478
+ language: typeof result === "string" ? detectToolSourceLanguage(result, "text") : "json",
479
+ source,
480
+ };
481
+ }
482
+
483
+ const ToolInputSource = ({ presentation }: { presentation: ToolInputPresentation }) => {
484
+ const lineCount = presentation.source.split("\n").length;
485
+ return (
486
+ <Collapsible className="rounded border border-border/60 bg-background/40">
487
+ <CollapsibleTrigger className="group flex w-full items-center gap-1.5 px-2 py-1 text-left text-xs text-muted-foreground hover:text-foreground">
488
+ <ChevronDownIcon className="size-3 shrink-0 transition-transform group-data-[state=closed]:-rotate-90" />
489
+ <span className="font-medium text-foreground/80">{presentation.label}</span>
490
+ <span className="rounded bg-muted px-1.5 py-0.5 text-[10px] uppercase leading-none tracking-wide">
491
+ {presentation.language}
492
+ </span>
493
+ <span className="ml-auto text-[10px] tabular-nums">
494
+ {lineCount} {lineCount === 1 ? "line" : "lines"}
495
+ </span>
496
+ </CollapsibleTrigger>
497
+ <CollapsibleContent>
498
+ <SourceBlock
499
+ source={presentation.source}
500
+ language={presentation.language}
501
+ className="max-h-64 border-t border-border/50 px-2 py-1.5 text-foreground"
502
+ />
503
+ </CollapsibleContent>
504
+ </Collapsible>
505
+ );
506
+ };
507
+
401
508
  type ToolStatusProgress = Extract<ToolProgress, { type: "tool_status" }>;
402
509
 
403
510
  export type WebSearchProgressGroup = {
@@ -488,6 +595,41 @@ const hasExpandableDetails = (event: ToolEvent): boolean => {
488
595
  );
489
596
  };
490
597
 
598
+ const ToolCallDetails = ({
599
+ event,
600
+ showQuestion = false,
601
+ }: {
602
+ event: ToolEvent;
603
+ showQuestion?: boolean;
604
+ }) => {
605
+ const question = askGenieQuestion(event);
606
+ const summary = summarizeProgress(event.progress ?? []);
607
+ const inputPresentation =
608
+ "input" in event ? toolInputPresentation(event.toolName, event.input) : undefined;
609
+ const outputPresentation =
610
+ "output" in event ? toolOutputPresentation(event.toolName, event.output) : undefined;
611
+
612
+ return (
613
+ <div className="flex flex-col gap-2">
614
+ {showQuestion && question ? (
615
+ <p className="break-words px-0.5 text-xs italic text-foreground/80">{question}</p>
616
+ ) : null}
617
+ {inputPresentation ? (
618
+ <ToolInputSource presentation={inputPresentation} />
619
+ ) : "input" in event ? (
620
+ <RawToolPayload label="Request" value={event.input} />
621
+ ) : null}
622
+ <WebSearchProgressDetails event={event} />
623
+ <ToolProgressDetails summary={summary} omitQuestion />
624
+ {outputPresentation ? (
625
+ <ToolInputSource presentation={outputPresentation} />
626
+ ) : "output" in event ? (
627
+ <RawToolPayload label="Response" value={event.output} />
628
+ ) : null}
629
+ </div>
630
+ );
631
+ };
632
+
491
633
  /**
492
634
  * One row inside {@link ToolSessionPill}. Always-visible header
493
635
  * shows the status icon, the tool's verb (`Called Ask Genie`,
@@ -506,7 +648,6 @@ const ToolCallRow = ({ event }: { event: ToolEvent }) => {
506
648
  const isRunning = event.status === "running";
507
649
  const isError = event.status === "error";
508
650
  const question = askGenieQuestion(event);
509
- const summary = summarizeProgress(event.progress ?? []);
510
651
  const expandable = hasExpandableDetails(event);
511
652
  // Inner rows defer to the live wire status when running (e.g.
512
653
  // "Executing query") so users tracking the open pill see the
@@ -553,11 +694,8 @@ const ToolCallRow = ({ event }: { event: ToolEvent }) => {
553
694
  {header}
554
695
  </CollapsibleTrigger>
555
696
  <CollapsibleContent>
556
- <div className="flex flex-col gap-2 px-2 pb-2">
557
- {"input" in event ? <RawToolPayload label="Request" value={event.input} /> : null}
558
- <WebSearchProgressDetails event={event} />
559
- <ToolProgressDetails summary={summary} omitQuestion />
560
- {"output" in event ? <RawToolPayload label="Response" value={event.output} /> : null}
697
+ <div className="px-2 pb-2">
698
+ <ToolCallDetails event={event} />
561
699
  </div>
562
700
  </CollapsibleContent>
563
701
  </Collapsible>
@@ -648,10 +786,12 @@ export const ToolSessionPill = ({ events }: { events: ToolEvent[] }) => {
648
786
  )}
649
787
  </CollapsibleTrigger>
650
788
  <CollapsibleContent>
651
- <div className="mt-1.5 flex flex-col gap-1">
652
- {events.map((event) => (
653
- <ToolCallRow key={event.id} event={event} />
654
- ))}
789
+ <div className={cn("mt-1.5", events.length > 1 && "flex flex-col gap-1")}>
790
+ {events.length === 1 ? (
791
+ <ToolCallDetails event={latest} showQuestion />
792
+ ) : (
793
+ events.map((event) => <ToolCallRow key={event.id} event={event} />)
794
+ )}
655
795
  </div>
656
796
  </CollapsibleContent>
657
797
  </Collapsible>