@dbx-tools/ui-mastra 0.9.60 → 0.9.61

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/README.md CHANGED
@@ -178,7 +178,7 @@ Useful options:
178
178
  - Feedback defaults to auto: thumbs, dislike, and comment controls appear when
179
179
  the server reports MLflow feedback is available and a turn produced a trace
180
180
  id. Set `enableFeedback={false}` to hide them. Viewers with `CAN_MANAGE` on
181
- the experiment also get an experiment debug link beside copy and export.
181
+ the experiment also get an experiment debug link at the end of the action row.
182
182
 
183
183
  ## Place The Conversation List
184
184
 
@@ -341,8 +341,11 @@ The UI understands the extra events produced by
341
341
  - Native tool-call and tool-result payloads remain complete. Expanding a tool
342
342
  row reveals default-closed Request and Response viewers; large values scroll
343
343
  without string or collection truncation.
344
- - Genie writer events render as tool progress, including thinking text, SQL, row
345
- counts, result summaries, and chart/data markers.
344
+ - Genie `thinking` events stream into the assistant's collapsed Thoughts
345
+ panel. Intermediate answer attachments and query-result tables stay on tool
346
+ progress, not in Thoughts. Other writer events remain attached to tool
347
+ progress, including SQL, row counts, result summaries, and chart/data
348
+ markers.
346
349
  - `[chart:<id>]` markers long-poll the chart cache and render ECharts inline.
347
350
  - `[data:<id>]` markers fetch statement rows and render a sortable table with
348
351
  column toggles and CSV export.
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.60";
6
+ export const PACKAGE_VERSION = "0.9.61";
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.60",
33
- "@dbx-tools/shared-genie": "^0.9.60",
34
- "@dbx-tools/shared-mastra": "^0.9.60",
35
- "@dbx-tools/shared-model": "^0.9.60",
36
- "@dbx-tools/ui": "^0.9.60",
32
+ "@dbx-tools/shared-core": "^0.9.61",
33
+ "@dbx-tools/shared-genie": "^0.9.61",
34
+ "@dbx-tools/shared-mastra": "^0.9.61",
35
+ "@dbx-tools/shared-model": "^0.9.61",
36
+ "@dbx-tools/ui": "^0.9.61",
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.60",
53
+ "version": "0.9.61",
54
54
  "dbxToolsConfig": {
55
55
  "tags": [
56
56
  "ui"
@@ -42,7 +42,11 @@ import type {
42
42
  ToolEvent,
43
43
  } from "./types.ts";
44
44
  import { copyText } from "../support/clipboard.ts";
45
- import { mergeToolEvents, toolEventsFromParts } from "../support/tool-events.ts";
45
+ import {
46
+ genieReasoningText,
47
+ mergeToolEvents,
48
+ toolEventsFromParts,
49
+ } from "../support/tool-events.ts";
46
50
 
47
51
  // User / assistant message bubbles plus the inline approval card and
48
52
  // the helpers that surface approval-gated tool calls out of a message's
@@ -344,9 +348,19 @@ export const AssistantBubble = ({
344
348
  feedbackValue,
345
349
  mlflowExperimentUrl,
346
350
  }: AssistantBubbleProps) => {
347
- const reasoning = getReasoningText(message.parts);
351
+ const toolEvents = mergeToolEvents(toolEventsFromParts(message.parts), events);
352
+ const reasoning = [getReasoningText(message.parts), genieReasoningText(toolEvents)]
353
+ .filter(Boolean)
354
+ .join("\n\n");
348
355
  const isReasoningStreaming =
349
- isLast && status === "streaming" && message.parts.at(-1)?.type === "reasoning";
356
+ isLast &&
357
+ status === "streaming" &&
358
+ (message.parts.at(-1)?.type === "reasoning" ||
359
+ toolEvents.some(
360
+ (event) =>
361
+ event.status === "running" &&
362
+ event.progress?.some((progress) => progress.type === "thinking"),
363
+ ));
350
364
  const textParts = message.parts.filter(
351
365
  (p): p is { type: "text"; text: string } => p.type === "text",
352
366
  );
@@ -382,7 +396,6 @@ export const AssistantBubble = ({
382
396
  collectPendingApprovals(message.parts),
383
397
  externalApprovals,
384
398
  );
385
- const toolEvents = mergeToolEvents(toolEventsFromParts(message.parts), events);
386
399
 
387
400
  return (
388
401
  <Item className="items-start gap-3 border-none bg-transparent p-0">
@@ -405,14 +418,14 @@ export const AssistantBubble = ({
405
418
  */}
406
419
  {toolEvents.length > 0 && <ToolSessionPill events={toolEvents} />}
407
420
  {reasoning && (
408
- <Collapsible defaultOpen={isReasoningStreaming}>
409
- <CollapsibleTrigger className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground">
410
- <ChevronDownIcon className="size-3" />
421
+ <Collapsible>
422
+ <CollapsibleTrigger className="group flex cursor-pointer items-center gap-1 text-xs text-muted-foreground hover:text-foreground">
423
+ <ChevronDownIcon className="size-3 transition-transform group-data-[state=closed]:-rotate-90" />
411
424
  <span>{isReasoningStreaming ? "Thinking..." : "Thoughts"}</span>
412
425
  </CollapsibleTrigger>
413
426
  <CollapsibleContent>
414
- <div className="mt-1 border-l-2 border-border/60 pl-3 text-xs text-muted-foreground whitespace-pre-wrap">
415
- {reasoning}
427
+ <div className="mt-1 border-l-2 border-border/60 pl-3 text-xs text-muted-foreground">
428
+ <ToolMarkdown>{reasoning}</ToolMarkdown>
416
429
  </div>
417
430
  </CollapsibleContent>
418
431
  </Collapsible>
@@ -480,6 +493,12 @@ export const AssistantBubble = ({
480
493
  </Tooltip>
481
494
  )}
482
495
  {onExport && <ExportMenu onExport={onExport} iconOnly tooltip="Export message" />}
496
+ {onFeedback && (
497
+ <FeedbackControls
498
+ onSubmit={onFeedback}
499
+ {...(feedbackValue ? { value: feedbackValue } : {})}
500
+ />
501
+ )}
483
502
  {mlflowExperimentUrl && (
484
503
  <Tooltip>
485
504
  <TooltipTrigger asChild>
@@ -497,12 +516,6 @@ export const AssistantBubble = ({
497
516
  <TooltipContent>Open MLflow experiment</TooltipContent>
498
517
  </Tooltip>
499
518
  )}
500
- {onFeedback && (
501
- <FeedbackControls
502
- onSubmit={onFeedback}
503
- {...(feedbackValue ? { value: feedbackValue } : {})}
504
- />
505
- )}
506
519
  </div>
507
520
  )}
508
521
  <SuggestionPills questions={suggestions} onSelect={onSuggestionClick} className="mt-1" />
@@ -46,15 +46,8 @@ const runningLabelFor = (event: ToolEvent): string => {
46
46
  };
47
47
 
48
48
  /**
49
- * One Genie attachment's worth of progress: the reasoning thoughts
50
- * that landed on it, the SQL query if any, and the per-attachment
51
- * text response Genie produced (a "text" attachment carries prose;
52
- * a "query" attachment can co-emit prose via the same path).
53
- * Bucketing thinking + query + text together by `attachment_id`
54
- * keeps the rendered detail view aligned with the underlying wire
55
- * structure (data sourcing, description, steps, SQL, and the prose
56
- * interpretation all stay next to each other for the same query
57
- * instead of intermixing across queries).
49
+ * One Genie attachment's worth of SQL progress. Reasoning and intermediate
50
+ * text attachments render in the assistant bubble's Thoughts panel.
58
51
  *
59
52
  * The `key` is the wire `attachment_id` when Genie supplied one;
60
53
  * for the rare anonymous attachment (Genie's "main answer" text
@@ -65,21 +58,12 @@ const runningLabelFor = (event: ToolEvent): string => {
65
58
  */
66
59
  type AttachmentBucket = {
67
60
  key: string;
68
- thinking: Extract<ToolProgress, { type: "thinking" }>[];
69
61
  /**
70
62
  * Genie emits one SQL string per query attachment. When it
71
63
  * rewrites the SQL mid-turn the later event supersedes the
72
64
  * earlier one (so the bucket always shows the final SQL).
73
65
  */
74
66
  query?: Extract<ToolProgress, { type: "query" }>;
75
- /**
76
- * Genie may emit one text snapshot per attachment (typed
77
- * "text" attachments, plus prose that accompanies a query).
78
- * Later snapshots supersede earlier ones - we render the final
79
- * value, which matches how the SDK presents it on
80
- * `attachment.text.content`.
81
- */
82
- text?: Extract<ToolProgress, { type: "text" }>;
83
67
  };
84
68
 
85
69
  /**
@@ -116,10 +100,8 @@ type MessageGroup = {
116
100
  * are resolved out-of-band via the chart cache; suggestions live at
117
101
  * message scope.
118
102
  *
119
- * `thinking` entries are de-duplicated server-side already
120
- * (`packages/js/shared/genie/src/event.ts` keys on
121
- * `(thought_type, content)` per attachment) so we can render them
122
- * verbatim without an extra dedupe pass here.
103
+ * Genie `thinking` and `text` entries render in the assistant bubble's
104
+ * consolidated Thoughts panel and are intentionally omitted here.
123
105
  *
124
106
  * `groups` preserves first-seen order so the rendered detail view
125
107
  * walks Genie sub-calls in the order the LLM dispatched them.
@@ -167,7 +149,7 @@ const summarizeProgress = (progress: ToolProgress[]): ToolDetailSummary => {
167
149
  const key = attachmentId ?? ANON_ATTACHMENT_KEY;
168
150
  let bucket = group.attachments.find((b) => b.key === key);
169
151
  if (!bucket) {
170
- bucket = { key, thinking: [] };
152
+ bucket = { key };
171
153
  group.attachments.push(bucket);
172
154
  }
173
155
  return bucket;
@@ -184,8 +166,7 @@ const summarizeProgress = (progress: ToolProgress[]): ToolDetailSummary => {
184
166
  break;
185
167
  }
186
168
  case "thinking": {
187
- const g = groupFor(p.message_id);
188
- bucketFor(g, p.attachment_id).thinking.push(p);
169
+ // Reasoning is rendered once in the assistant bubble's Thoughts panel.
189
170
  break;
190
171
  }
191
172
  case "query": {
@@ -194,8 +175,7 @@ const summarizeProgress = (progress: ToolProgress[]): ToolDetailSummary => {
194
175
  break;
195
176
  }
196
177
  case "text": {
197
- const g = groupFor(p.message_id);
198
- bucketFor(g, p.attachment_id).text = p;
178
+ // Intermediate answers render in the assistant bubble's Thoughts panel.
199
179
  break;
200
180
  }
201
181
  case "error": {
@@ -233,32 +213,16 @@ const askGenieQuestion = (event: ToolEvent): string | undefined => {
233
213
  return undefined;
234
214
  };
235
215
 
236
- /**
237
- * Strip Genie's `THOUGHT_TYPE_*` prefix and turn the remaining
238
- * upper-snake into a Title Cased label users can read at a glance.
239
- *
240
- * Examples:
241
- * `THOUGHT_TYPE_DESCRIPTION` -> `Description`
242
- * `THOUGHT_TYPE_DATA_SOURCING` -> `Data Sourcing`
243
- * `THOUGHT_TYPE_UNDERSTANDING` -> `Understanding`
244
- */
245
- const humanizeThoughtType = (kind: string): string =>
246
- stringUtils.toLabel(kind.replace(/^THOUGHT_TYPE_/i, ""));
247
-
248
216
  /**
249
217
  * Genie attaches one of three payload kinds per attachment slot:
250
218
  * `query` (SQL), `text` (prose answer), or `suggested_questions`.
251
219
  * `isQueryAttachment` true means this bucket got a SQL query, so
252
- * it earns a numbered "Query N" card in the UI; prose-only or
253
- * thinking-only buckets render as plain markdown below the
254
- * numbered queries because labelling them "Query N" misleads
255
- * users into thinking Genie ran an extra query when it didn't.
220
+ * it earns a numbered "Query N" card in the UI.
256
221
  */
257
222
  const isQueryAttachment = (b: AttachmentBucket): boolean => Boolean(b.query);
258
223
 
259
- /** True when a bucket has any renderable content (thinking, SQL, or prose). */
260
- const isAttachmentRenderable = (b: AttachmentBucket): boolean =>
261
- b.thinking.length > 0 || Boolean(b.query) || Boolean(b.text);
224
+ /** True when a bucket has SQL not already shown in the Thoughts panel. */
225
+ const isAttachmentRenderable = (b: AttachmentBucket): boolean => Boolean(b.query);
262
226
 
263
227
  /** True when a group has any renderable content (question, attachments, or errors). */
264
228
  const isGroupRenderable = (g: MessageGroup): boolean =>
@@ -275,13 +239,8 @@ const isGroupRenderable = (g: MessageGroup): boolean =>
275
239
  * 1. The LLM's sub-question for this Genie call, always
276
240
  * visible, styled as a blockquote so it reads as provenance.
277
241
  * 2. One numbered "Query N" Collapsible per query attachment
278
- * (or just "Query" when there's one). Opens to reveal
279
- * reasoning thoughts, an inner SQL Collapsible, and any
280
- * prose Genie attached directly to the query.
281
- * 3. One "Answer N" Collapsible per prose-only attachment
282
- * (Genie's natural-language summary, follow-up questions).
283
- * Opens to reveal thinking + the markdown body.
284
- * 4. Errors that landed under this `message_id`, always
242
+ * (or just "Query" when there's one). Opens to reveal SQL.
243
+ * 3. Errors that landed under this `message_id`, always
285
244
  * visible (red text - users need to see failures, not click
286
245
  * to find them).
287
246
  *
@@ -297,12 +256,8 @@ const MessageGroupBody = ({
297
256
  omitQuestion?: boolean;
298
257
  }) => {
299
258
  const renderableAttachments = group.attachments.filter(isAttachmentRenderable);
300
- // Split renderable attachments into the two visual lanes: numbered
301
- // query cards vs flat prose. The partition preserves first-seen
302
- // order within each lane so queries still render in the order
303
- // Genie produced them and the prose still reads in dispatch order.
259
+ // Query cards remain attached to their originating Genie sub-call.
304
260
  const queryBuckets = renderableAttachments.filter(isQueryAttachment);
305
- const proseBuckets = renderableAttachments.filter((b) => !isQueryAttachment(b));
306
261
  return (
307
262
  <div className="flex flex-col gap-1.5">
308
263
  {!omitQuestion && group.question && (
@@ -313,8 +268,8 @@ const MessageGroupBody = ({
313
268
  {queryBuckets.map((bucket, i) => (
314
269
  // Each query bucket is a single Collapsible (default
315
270
  // closed) so the expanded group reads as just the
316
- // question + a short stack of "Query N" / "Answer" rows.
317
- // Click any row to drill into its thinking + SQL + text.
271
+ // question + a short stack of "Query N" rows.
272
+ // Click any row to drill into its SQL.
318
273
  // SQL itself stays a nested Collapsible (default closed)
319
274
  // for the same reason: code is the heaviest content here,
320
275
  // and most readers only want a glance.
@@ -330,18 +285,6 @@ const MessageGroupBody = ({
330
285
  </CollapsibleTrigger>
331
286
  <CollapsibleContent>
332
287
  <div className="flex flex-col gap-2 px-2 pb-2 text-xs">
333
- {bucket.thinking.length > 0 && (
334
- <ul className="flex flex-col gap-1.5 border-l-2 border-border/60 pl-3 text-muted-foreground">
335
- {bucket.thinking.map((p, j) => (
336
- <li key={`think-${j}`} className="whitespace-pre-wrap break-words leading-snug">
337
- <span className="font-medium text-foreground/80">
338
- {humanizeThoughtType(p.thought_type)}:
339
- </span>{" "}
340
- {p.text}
341
- </li>
342
- ))}
343
- </ul>
344
- )}
345
288
  {bucket.query && (
346
289
  <Collapsible className="rounded border border-border/60 bg-background/30">
347
290
  <CollapsibleTrigger className="group flex w-full items-center gap-1 px-2 py-1 text-left text-muted-foreground hover:text-foreground">
@@ -360,38 +303,6 @@ const MessageGroupBody = ({
360
303
  </CollapsibleContent>
361
304
  </Collapsible>
362
305
  )}
363
- {bucket.text && <ToolMarkdown>{bucket.text.text}</ToolMarkdown>}
364
- </div>
365
- </CollapsibleContent>
366
- </Collapsible>
367
- ))}
368
- {proseBuckets.map((bucket, i) => (
369
- // Prose-only attachments (Genie's natural-language answer
370
- // and clarifying follow-up question attachments) get the
371
- // same Collapsible treatment as queries. Label as "Answer"
372
- // when there's only one prose bucket; multiple buckets
373
- // (rare - typically interpretation + a follow-up question)
374
- // get numbered so each row is addressable.
375
- <Collapsible key={bucket.key} className="rounded border border-border/60 bg-background/40">
376
- <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">
377
- <ChevronDownIcon className="size-3 shrink-0 transition-transform group-data-[state=closed]:-rotate-90" />
378
- <span>{proseBuckets.length > 1 ? `Answer ${i + 1}` : "Answer"}</span>
379
- </CollapsibleTrigger>
380
- <CollapsibleContent>
381
- <div className="flex flex-col gap-1 px-2 pb-2">
382
- {bucket.thinking.length > 0 && (
383
- <ul className="flex flex-col gap-1 text-[11px] text-muted-foreground">
384
- {bucket.thinking.map((p, j) => (
385
- <li key={`think-${j}`} className="whitespace-pre-wrap break-words leading-snug">
386
- <span className="font-medium text-foreground/80">
387
- {humanizeThoughtType(p.thought_type)}:
388
- </span>{" "}
389
- {p.text}
390
- </li>
391
- ))}
392
- </ul>
393
- )}
394
- {bucket.text && <ToolMarkdown>{bucket.text.text}</ToolMarkdown>}
395
306
  </div>
396
307
  </CollapsibleContent>
397
308
  </Collapsible>
@@ -495,7 +406,7 @@ const hasExpandableDetails = (event: ToolEvent): boolean => {
495
406
  * the question text the central agent passed to Genie so users
496
407
  * can see each sub-question at a glance without expanding.
497
408
  *
498
- * Rows with extra wire detail (Genie SQL, thinking, prose
409
+ * Rows with extra wire detail (Genie SQL, prose
499
410
  * answers, errors) expand to reveal the per-attachment cards
500
411
  * built by {@link ToolProgressDetails}. Tools with no extra
501
412
  * detail (`get_statement`, `prepare_chart`) render as a flat
@@ -2,6 +2,34 @@ import { GENIE_PROGRESS_PART_TYPE, GenieProgressPartDataSchema } from "@dbx-tool
2
2
  import { getToolOrDynamicToolName, isToolOrDynamicToolUIPart, type UIMessage } from "ai";
3
3
  import type { ToolEvent } from "../react/types.ts";
4
4
 
5
+ /**
6
+ * Collect Genie `thinking` events for the assistant Thoughts panel.
7
+ *
8
+ * Intermediate `text` attachments stay out: Agent Mode often puts query
9
+ * result tables there, and those belong in tool progress / the final answer,
10
+ * not in reasoning.
11
+ */
12
+ export function genieReasoningText(events: ToolEvent[]): string {
13
+ const sections: string[] = [];
14
+ for (const event of events) {
15
+ for (const progress of event.progress ?? []) {
16
+ if (progress.type !== "thinking") continue;
17
+ const text = stripMarkdownTables(progress.text).trim();
18
+ if (text) sections.push(text);
19
+ }
20
+ }
21
+ return sections.join("\n\n");
22
+ }
23
+
24
+ /** Drop markdown table rows so query samples never land in Thoughts. */
25
+ function stripMarkdownTables(text: string): string {
26
+ return text
27
+ .split("\n")
28
+ .filter((line) => !/^\s*\|/.test(line))
29
+ .join("\n")
30
+ .replace(/\n{3,}/g, "\n\n");
31
+ }
32
+
5
33
  /** Project native AI SDK tool parts from persisted messages onto pill state. */
6
34
  export function toolEventsFromParts(parts: UIMessage["parts"]): ToolEvent[] {
7
35
  const events = parts.filter(isToolOrDynamicToolUIPart).map<ToolEvent>((part) => {