@dbx-tools/ui-mastra 0.9.116 → 0.9.118

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.116";
6
+ export const PACKAGE_VERSION = "0.9.118";
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";
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.116",
33
- "@dbx-tools/shared-genie": "^0.9.116",
34
- "@dbx-tools/shared-mastra": "^0.9.116",
35
- "@dbx-tools/shared-model": "^0.9.116",
36
- "@dbx-tools/ui": "^0.9.116",
32
+ "@dbx-tools/shared-core": "^0.9.118",
33
+ "@dbx-tools/shared-genie": "^0.9.118",
34
+ "@dbx-tools/shared-mastra": "^0.9.118",
35
+ "@dbx-tools/shared-model": "^0.9.118",
36
+ "@dbx-tools/ui": "^0.9.118",
37
37
  "@mastra/client-js": "1.50.0",
38
38
  "@mastra/core": "1.71.0",
39
39
  "@tanstack/react-table": "^8.21.3",
@@ -49,7 +49,7 @@
49
49
  "publishConfig": {
50
50
  "access": "public"
51
51
  },
52
- "version": "0.9.116",
52
+ "version": "0.9.118",
53
53
  "dbxToolsConfig": {
54
54
  "tags": [
55
55
  "ui"
@@ -16,6 +16,7 @@ import {
16
16
  DropdownMenuTrigger,
17
17
  Slider,
18
18
  Spinner,
19
+ useIsMobile,
19
20
  } from "@dbx-tools/ui/react";
20
21
  import { CheckIcon, ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
21
22
  import { useMemo, useState, type ReactNode } from "react";
@@ -185,6 +186,7 @@ export const ModelSelector = ({
185
186
  onModelChange,
186
187
  actions,
187
188
  }: ModelSelectorProps) => {
189
+ const isMobile = useIsMobile();
188
190
  const [open, setOpen] = useState(false);
189
191
  const [modelPickerOpen, setModelPickerOpen] = useState(false);
190
192
  const [activeFamily, setActiveFamily] = useState<string>();
@@ -242,50 +244,102 @@ export const ModelSelector = ({
242
244
  )}
243
245
  </Button>
244
246
  </DropdownMenuTrigger>
245
- <DropdownMenuContent align="end" className="w-48">
246
- <DropdownMenuSub
247
- open={modelPickerOpen}
248
- onOpenChange={(next) => {
249
- if (!next && open) return;
250
- setModelPickerOpen(next);
251
- if (!next) setActiveFamily(undefined);
252
- }}
253
- >
254
- <DropdownMenuSubTrigger className="mx-1 justify-center rounded-lg bg-accent/60 px-3 py-2 text-xs font-medium">
255
- {showLoading ? (
247
+ <DropdownMenuContent
248
+ align="end"
249
+ collisionPadding={8}
250
+ className="w-48 max-w-[calc(100vw_-_1rem)]"
251
+ >
252
+ {isMobile && modelPickerOpen ? (
253
+ <>
254
+ {!activeFamily && (
256
255
  <>
257
- <Spinner className="size-3 shrink-0" />
258
- <span className="sr-only">Loading default model</span>
256
+ <button
257
+ type="button"
258
+ className="flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-xs font-medium outline-none hover:bg-accent focus-visible:bg-accent"
259
+ onClick={() => setModelPickerOpen(false)}
260
+ >
261
+ <ChevronLeftIcon className="size-3" />
262
+ Options
263
+ </button>
264
+ <DropdownMenuSeparator />
259
265
  </>
266
+ )}
267
+ <FamilyPicker
268
+ families={families}
269
+ selectedModelId={selectedModelId}
270
+ model={model}
271
+ defaultModelName={defaultModelName}
272
+ showLoading={showLoading}
273
+ activeFamily={activeFamily}
274
+ onActiveFamilyChange={setActiveFamily}
275
+ onModelChange={onModelChange}
276
+ />
277
+ </>
278
+ ) : (
279
+ <>
280
+ {isMobile ? (
281
+ <button
282
+ type="button"
283
+ className="mx-1 flex w-[calc(100%_-_0.5rem)] items-center rounded-lg bg-accent/60 px-3 py-2 text-xs font-medium outline-none hover:bg-accent focus-visible:bg-accent"
284
+ onClick={() => setModelPickerOpen(true)}
285
+ >
286
+ {showLoading ? (
287
+ <>
288
+ <Spinner className="size-3 shrink-0" />
289
+ <span className="sr-only">Loading default model</span>
290
+ </>
291
+ ) : (
292
+ <span className="truncate">{triggerLabel}</span>
293
+ )}
294
+ <ChevronRightIcon className="ml-auto size-3 shrink-0 text-muted-foreground" />
295
+ </button>
260
296
  ) : (
261
- triggerLabel
297
+ <DropdownMenuSub
298
+ open={modelPickerOpen}
299
+ onOpenChange={(next) => {
300
+ if (!next && open) return;
301
+ setModelPickerOpen(next);
302
+ if (!next) setActiveFamily(undefined);
303
+ }}
304
+ >
305
+ <DropdownMenuSubTrigger className="mx-1 justify-center rounded-lg bg-accent/60 px-3 py-2 text-xs font-medium">
306
+ {showLoading ? (
307
+ <>
308
+ <Spinner className="size-3 shrink-0" />
309
+ <span className="sr-only">Loading default model</span>
310
+ </>
311
+ ) : (
312
+ triggerLabel
313
+ )}
314
+ </DropdownMenuSubTrigger>
315
+ <DropdownMenuPortal>
316
+ <DropdownMenuSubContent collisionPadding={8} className="w-48">
317
+ <FamilyPicker
318
+ families={families}
319
+ selectedModelId={selectedModelId}
320
+ model={model}
321
+ defaultModelName={defaultModelName}
322
+ showLoading={showLoading}
323
+ activeFamily={activeFamily}
324
+ onActiveFamilyChange={setActiveFamily}
325
+ onModelChange={onModelChange}
326
+ />
327
+ </DropdownMenuSubContent>
328
+ </DropdownMenuPortal>
329
+ </DropdownMenuSub>
330
+ )}
331
+ {(actions || (reasoningEfforts.length > 0 && onReasoningEffortChange)) && (
332
+ <DropdownMenuSeparator />
262
333
  )}
263
- </DropdownMenuSubTrigger>
264
- <DropdownMenuPortal>
265
- <DropdownMenuSubContent className="w-48">
266
- <FamilyPicker
267
- families={families}
268
- selectedModelId={selectedModelId}
269
- model={model}
270
- defaultModelName={defaultModelName}
271
- showLoading={showLoading}
272
- activeFamily={activeFamily}
273
- onActiveFamilyChange={setActiveFamily}
274
- onModelChange={onModelChange}
334
+ {actions && <div className="px-1">{actions}</div>}
335
+ {reasoningEfforts.length > 0 && onReasoningEffortChange && (
336
+ <ReasoningControl
337
+ efforts={reasoningEfforts}
338
+ value={reasoningEffort}
339
+ onChange={onReasoningEffortChange}
275
340
  />
276
- </DropdownMenuSubContent>
277
- </DropdownMenuPortal>
278
- </DropdownMenuSub>
279
- {(actions || (reasoningEfforts.length > 0 && onReasoningEffortChange)) && (
280
- <DropdownMenuSeparator />
281
- )}
282
- {actions && <div className="px-1">{actions}</div>}
283
- {reasoningEfforts.length > 0 && onReasoningEffortChange && (
284
- <ReasoningControl
285
- efforts={reasoningEfforts}
286
- value={reasoningEffort}
287
- onChange={onReasoningEffortChange}
288
- />
341
+ )}
342
+ </>
289
343
  )}
290
344
  </DropdownMenuContent>
291
345
  </DropdownMenu>
@@ -22,7 +22,6 @@ import {
22
22
  CopyIcon,
23
23
  MessageSquareIcon,
24
24
  RefreshCcwIcon,
25
- SparklesIcon,
26
25
  UserIcon,
27
26
  XIcon,
28
27
  } from "lucide-react";
@@ -60,12 +59,12 @@ const getReasoningText = (parts: UIMessage["parts"]): string =>
60
59
  .filter((p): p is { type: "reasoning"; text: string } => p.type === "reasoning")
61
60
  .map((p) => compactReasoningText(p.text))
62
61
  .filter(Boolean)
63
- .join("\n\n");
62
+ .join("\n");
64
63
 
65
- const RoleAvatar = ({ role }: { role: UIMessage["role"] }) => (
64
+ const UserAvatar = () => (
66
65
  <Avatar className="size-7">
67
66
  <AvatarFallback>
68
- {role === "assistant" ? <SparklesIcon className="size-4" /> : <UserIcon className="size-4" />}
67
+ <UserIcon className="size-4" />
69
68
  </AvatarFallback>
70
69
  </Avatar>
71
70
  );
@@ -363,7 +362,7 @@ export const AssistantBubble = ({
363
362
  const toolEvents = mergeToolEvents(toolEventsFromParts(message.parts), events, terminalToolState);
364
363
  const reasoning = [getReasoningText(message.parts), genieReasoningText(toolEvents)]
365
364
  .filter(Boolean)
366
- .join("\n\n");
365
+ .join("\n");
367
366
  const isReasoningStreaming =
368
367
  isLast &&
369
368
  status === "streaming" &&
@@ -405,10 +404,7 @@ export const AssistantBubble = ({
405
404
  // nothing so the prose flows unaffected. Suggested questions
406
405
  // stay gated on settle to avoid pop-in mid-stream.
407
406
  return (
408
- <Item className="items-start gap-2.5 border-none bg-transparent p-0">
409
- <ItemMedia>
410
- <RoleAvatar role="assistant" />
411
- </ItemMedia>
407
+ <Item className="items-start gap-0 border-none bg-transparent p-0">
412
408
  {/*
413
409
  * `min-w-0` is required so wide content (markdown tables, long
414
410
  * code blocks) can shrink to fit instead of pushing the whole
@@ -432,7 +428,7 @@ export const AssistantBubble = ({
432
428
  </CollapsibleTrigger>
433
429
  <CollapsibleContent>
434
430
  <div className="mt-1 border-l-2 border-border/60 pl-2.5 text-xs text-muted-foreground">
435
- <ToolMarkdown>{reasoning}</ToolMarkdown>
431
+ <ToolMarkdown className="prose-p:my-0 [&_p]:!my-0">{reasoning}</ToolMarkdown>
436
432
  </div>
437
433
  </CollapsibleContent>
438
434
  </Collapsible>
@@ -540,7 +536,7 @@ export const UserBubble = ({ message }: { message: UIMessage }) => {
540
536
  return (
541
537
  <Item className="items-start gap-2.5 border-none bg-transparent p-0">
542
538
  <ItemMedia>
543
- <RoleAvatar role="user" />
539
+ <UserAvatar />
544
540
  </ItemMedia>
545
541
  <ItemContent className="min-w-0">
546
542
  <div className="max-w-full rounded-lg bg-muted p-2 text-sm">
@@ -264,7 +264,7 @@ export const AssistantMarkdown = ({
264
264
  * (bold) gets the foreground color so KPI names still pop against
265
265
  * the muted body.
266
266
  */
267
- export const ToolMarkdown = ({ children }: { children: string }) => (
267
+ export const ToolMarkdown = ({ children, className }: { children: string; className?: string }) => (
268
268
  <Streamdown
269
269
  components={TOOL_MARKDOWN_COMPONENTS}
270
270
  plugins={SHIKI_PLUGIN}
@@ -272,12 +272,13 @@ export const ToolMarkdown = ({ children }: { children: string }) => (
272
272
  className={cn(
273
273
  "prose prose-sm dark:prose-invert max-w-none break-words",
274
274
  "text-xs leading-snug text-muted-foreground",
275
- "prose-p:my-0.5 prose-p:leading-snug",
275
+ "prose-p:my-0.5 prose-p:leading-snug [&_p]:!leading-snug",
276
276
  "prose-ul:my-0.5 prose-ul:pl-4 prose-ol:my-0.5 prose-ol:pl-4",
277
277
  "prose-li:my-0 prose-li:leading-snug prose-li:marker:text-muted-foreground/60",
278
278
  "prose-headings:my-1 prose-headings:text-xs prose-headings:font-semibold",
279
279
  "prose-strong:text-foreground/90 prose-strong:font-medium",
280
280
  "prose-code:text-xs prose-code:font-medium",
281
+ className,
281
282
  )}
282
283
  >
283
284
  {children}
@@ -11,7 +11,7 @@ import {
11
11
  cn,
12
12
  } from "@dbx-tools/ui/react";
13
13
  import { CheckIcon, ChevronDownIcon, XIcon } from "lucide-react";
14
- import { SourceBlock, SqlBlock, ToolMarkdown } from "./markdown.tsx";
14
+ import { SourceBlock, ToolMarkdown } from "./markdown.tsx";
15
15
  import type { ToolEvent, ToolProgress } from "./types.ts";
16
16
 
17
17
  // Consolidated tool-session pill and its Genie progress detail view:
@@ -304,16 +304,20 @@ const MessageGroupBody = ({
304
304
  ) : null}
305
305
  </CollapsibleTrigger>
306
306
  <CollapsibleContent>
307
- <div className="flex flex-col gap-2 border-t border-border/50 px-2 pb-2 pt-2 text-xs">
308
- {bucket.query && (
309
- <>
310
- <SqlBlock sql={bucket.query.sql} />
311
- {bucket.query.description && (
307
+ {bucket.query && (
308
+ <>
309
+ <SourceBlock
310
+ source={bucket.query.sql}
311
+ language="sql"
312
+ className="max-h-64 border-t border-border/50 px-2 py-1.5 text-foreground"
313
+ />
314
+ {bucket.query.description && (
315
+ <div className="border-t border-border/50 px-2 py-1.5">
312
316
  <ToolMarkdown>{bucket.query.description}</ToolMarkdown>
313
- )}
314
- </>
315
- )}
316
- </div>
317
+ </div>
318
+ )}
319
+ </>
320
+ )}
317
321
  </CollapsibleContent>
318
322
  </Collapsible>
319
323
  ))}
@@ -608,15 +612,18 @@ const ToolCallDetails = ({
608
612
  "input" in event ? toolInputPresentation(event.toolName, event.input) : undefined;
609
613
  const outputPresentation =
610
614
  "output" in event ? toolOutputPresentation(event.toolName, event.output) : undefined;
615
+ const isAskGenie = event.toolName.startsWith("ask_genie");
611
616
 
612
617
  return (
613
618
  <div className="flex flex-col gap-2">
614
619
  {showQuestion && question ? (
615
- <p className="break-words px-0.5 text-xs italic text-foreground/80">{question}</p>
620
+ <p className="break-words px-0.5 text-[11px] leading-snug text-muted-foreground">
621
+ {question}
622
+ </p>
616
623
  ) : null}
617
624
  {inputPresentation ? (
618
625
  <ToolInputSource presentation={inputPresentation} />
619
- ) : "input" in event ? (
626
+ ) : "input" in event && !isAskGenie ? (
620
627
  <RawToolPayload label="Request" value={event.input} />
621
628
  ) : null}
622
629
  <WebSearchProgressDetails event={event} />
@@ -667,7 +674,9 @@ const ToolCallRow = ({ event }: { event: ToolEvent }) => {
667
674
  {verb}
668
675
  </span>
669
676
  {question && (
670
- <span className="mt-0.5 block break-words italic text-foreground/80">{question}</span>
677
+ <span className="mt-0.5 block break-words text-[11px] leading-snug text-foreground/75">
678
+ {question}
679
+ </span>
671
680
  )}
672
681
  </span>
673
682
  );