@dbx-tools/ui-mastra 0.9.106 → 0.9.110

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.106";
6
+ export const PACKAGE_VERSION = "0.9.110";
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.106",
33
- "@dbx-tools/shared-genie": "^0.9.106",
34
- "@dbx-tools/shared-mastra": "^0.9.106",
35
- "@dbx-tools/shared-model": "^0.9.106",
36
- "@dbx-tools/ui": "^0.9.106",
32
+ "@dbx-tools/shared-core": "^0.9.110",
33
+ "@dbx-tools/shared-genie": "^0.9.110",
34
+ "@dbx-tools/shared-mastra": "^0.9.110",
35
+ "@dbx-tools/shared-model": "^0.9.110",
36
+ "@dbx-tools/ui": "^0.9.110",
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.106",
53
+ "version": "0.9.110",
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:
@@ -398,6 +398,108 @@ const RawToolPayload = ({ label, value }: { label: "Request" | "Response"; value
398
398
  </Collapsible>
399
399
  );
400
400
 
401
+ export type ToolInputPresentation = {
402
+ label: "Code" | "Command" | "Output";
403
+ language: "typescript" | "javascript" | "python" | "sql" | "json" | "bash" | "text";
404
+ source: string;
405
+ };
406
+
407
+ const inputRecord = (value: unknown): Record<string, unknown> | undefined =>
408
+ typeof value === "object" && value !== null && !Array.isArray(value)
409
+ ? (value as Record<string, unknown>)
410
+ : undefined;
411
+
412
+ /** Detect the most useful syntax grammar for a code-like tool request. */
413
+ export function detectToolSourceLanguage(
414
+ source: string,
415
+ fallback: ToolInputPresentation["language"],
416
+ ): ToolInputPresentation["language"] {
417
+ const trimmed = source.trim();
418
+ if (/^(?:select|with|insert|update|delete|merge|create|alter)\b/i.test(trimmed)) return "sql";
419
+ if (/^(?:from\s+\S+\s+import|import\s+\S+|def\s+\w+|class\s+\w+):?/m.test(trimmed)) {
420
+ return "python";
421
+ }
422
+ if (/^(?:const|let|var|interface|type|export|import)\b/m.test(trimmed)) return "typescript";
423
+ if (/^(?:#!.*\b(?:ba|z|k)?sh\b|(?:cd|ls|cat|grep|find|bun|npm|pnpm|yarn)\s)/m.test(trimmed)) {
424
+ return "bash";
425
+ }
426
+ if ((trimmed.startsWith("{") || trimmed.startsWith("[")) && trimmed.length > 1) {
427
+ try {
428
+ JSON.parse(trimmed);
429
+ return "json";
430
+ } catch {
431
+ // Keep the tool-specific fallback for incomplete source.
432
+ }
433
+ }
434
+ return fallback;
435
+ }
436
+
437
+ /** Extract a compact code or command preview from a tool request payload. */
438
+ export function toolInputPresentation(
439
+ toolName: string,
440
+ input: unknown,
441
+ ): ToolInputPresentation | undefined {
442
+ const record = inputRecord(input);
443
+ if (!record) return undefined;
444
+ if (typeof record.command === "string" && toolName.endsWith("execute_command")) {
445
+ return {
446
+ label: "Command",
447
+ language: detectToolSourceLanguage(record.command, "bash"),
448
+ source: record.command,
449
+ };
450
+ }
451
+ if (typeof record.code === "string") {
452
+ return {
453
+ label: "Code",
454
+ language: detectToolSourceLanguage(record.code, "typescript"),
455
+ source: record.code,
456
+ };
457
+ }
458
+ return undefined;
459
+ }
460
+
461
+ /** Extract Code Mode's completed result into its own compact output panel. */
462
+ export function toolOutputPresentation(
463
+ toolName: string,
464
+ output: unknown,
465
+ ): ToolInputPresentation | undefined {
466
+ if (toolName !== "execute_typescript") return undefined;
467
+ const record = inputRecord(output);
468
+ if (!record || record.success !== true || !("result" in record)) return undefined;
469
+ const result = record.result;
470
+ const source = typeof result === "string" ? result : formatRawToolPayload(result);
471
+ return {
472
+ label: "Output",
473
+ language: typeof result === "string" ? detectToolSourceLanguage(result, "text") : "json",
474
+ source,
475
+ };
476
+ }
477
+
478
+ const ToolInputSource = ({ presentation }: { presentation: ToolInputPresentation }) => {
479
+ const lineCount = presentation.source.split("\n").length;
480
+ return (
481
+ <Collapsible className="rounded border border-border/60 bg-background/40">
482
+ <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">
483
+ <ChevronDownIcon className="size-3 shrink-0 transition-transform group-data-[state=closed]:-rotate-90" />
484
+ <span className="font-medium text-foreground/80">{presentation.label}</span>
485
+ <span className="rounded bg-muted px-1.5 py-0.5 text-[10px] uppercase leading-none tracking-wide">
486
+ {presentation.language}
487
+ </span>
488
+ <span className="ml-auto text-[10px] tabular-nums">
489
+ {lineCount} {lineCount === 1 ? "line" : "lines"}
490
+ </span>
491
+ </CollapsibleTrigger>
492
+ <CollapsibleContent>
493
+ <SourceBlock
494
+ source={presentation.source}
495
+ language={presentation.language}
496
+ className="max-h-64 border-t border-border/50 px-2 py-1.5 text-foreground"
497
+ />
498
+ </CollapsibleContent>
499
+ </Collapsible>
500
+ );
501
+ };
502
+
401
503
  type ToolStatusProgress = Extract<ToolProgress, { type: "tool_status" }>;
402
504
 
403
505
  export type WebSearchProgressGroup = {
@@ -508,6 +610,10 @@ const ToolCallRow = ({ event }: { event: ToolEvent }) => {
508
610
  const question = askGenieQuestion(event);
509
611
  const summary = summarizeProgress(event.progress ?? []);
510
612
  const expandable = hasExpandableDetails(event);
613
+ const inputPresentation =
614
+ "input" in event ? toolInputPresentation(event.toolName, event.input) : undefined;
615
+ const outputPresentation =
616
+ "output" in event ? toolOutputPresentation(event.toolName, event.output) : undefined;
511
617
  // Inner rows defer to the live wire status when running (e.g.
512
618
  // "Executing query") so users tracking the open pill see the
513
619
  // backend's freshest state - not just the static
@@ -554,10 +660,18 @@ const ToolCallRow = ({ event }: { event: ToolEvent }) => {
554
660
  </CollapsibleTrigger>
555
661
  <CollapsibleContent>
556
662
  <div className="flex flex-col gap-2 px-2 pb-2">
557
- {"input" in event ? <RawToolPayload label="Request" value={event.input} /> : null}
663
+ {inputPresentation ? (
664
+ <ToolInputSource presentation={inputPresentation} />
665
+ ) : "input" in event ? (
666
+ <RawToolPayload label="Request" value={event.input} />
667
+ ) : null}
558
668
  <WebSearchProgressDetails event={event} />
559
669
  <ToolProgressDetails summary={summary} omitQuestion />
560
- {"output" in event ? <RawToolPayload label="Response" value={event.output} /> : null}
670
+ {outputPresentation ? (
671
+ <ToolInputSource presentation={outputPresentation} />
672
+ ) : "output" in event ? (
673
+ <RawToolPayload label="Response" value={event.output} />
674
+ ) : null}
561
675
  </div>
562
676
  </CollapsibleContent>
563
677
  </Collapsible>