@dbx-tools/ui-mastra 0.3.20 → 0.3.22

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
@@ -29,7 +29,7 @@ Key features:
29
29
  background-streaming states, plus a per-row cancel for a running thread.
30
30
  - Concurrent threads: run several conversations at once, switch between them
31
31
  while each keeps streaming, and cancel any one independently (per-thread abort
32
- + routing, no shared client state).
32
+ - routing, no shared client state).
33
33
  - Mid-turn steering queue: messages submitted while a turn streams stack up as
34
34
  pending steers (they drain oldest-first when the turn ends); each queued item
35
35
  can be sent now (interrupting the current turn), removed, or dragged to
package/package.json CHANGED
@@ -26,19 +26,19 @@
26
26
  "shiki": "^3.0.0",
27
27
  "sql-formatter": "^15.6.9",
28
28
  "streamdown": "^2.5.0",
29
- "@dbx-tools/shared-core": "0.3.20",
30
- "@dbx-tools/shared-genie": "0.3.20",
31
- "@dbx-tools/shared-mastra": "0.3.20",
32
- "@dbx-tools/shared-model": "0.3.20",
33
- "@dbx-tools/ui-appkit": "0.3.20",
34
- "@dbx-tools/ui-branding": "0.3.20"
29
+ "@dbx-tools/shared-genie": "0.3.22",
30
+ "@dbx-tools/shared-model": "0.3.22",
31
+ "@dbx-tools/shared-core": "0.3.22",
32
+ "@dbx-tools/ui-branding": "0.3.22",
33
+ "@dbx-tools/shared-mastra": "0.3.22",
34
+ "@dbx-tools/ui-appkit": "0.3.22"
35
35
  },
36
36
  "main": "index.ts",
37
37
  "license": "UNLICENSED",
38
38
  "publishConfig": {
39
39
  "access": "public"
40
40
  },
41
- "version": "0.3.20",
41
+ "version": "0.3.22",
42
42
  "types": "index.ts",
43
43
  "type": "module",
44
44
  "exports": {
@@ -25,9 +25,9 @@ import {
25
25
  } from "lucide-react";
26
26
  import { useState } from "react";
27
27
  import { MarkdownWithEmbeds } from "./embed-slots";
28
- import { ToolMarkdown } from "./markdown";
29
28
  import { ExportMenu } from "./export-menu";
30
29
  import { FeedbackControls } from "./feedback-controls";
30
+ import { ToolMarkdown } from "./markdown";
31
31
  import { SuggestionPills } from "./suggestion-pills";
32
32
  import { collectSuggestions } from "./suggestions";
33
33
  import { ToolSessionPill, humanizeToolName } from "./tool-pill";
@@ -87,11 +87,7 @@ const getReasoningText = (parts: UIMessage["parts"]): string =>
87
87
  const RoleAvatar = ({ role }: { role: UIMessage["role"] }) => (
88
88
  <Avatar className="size-7">
89
89
  <AvatarFallback>
90
- {role === "assistant" ? (
91
- <SparklesIcon className="size-4" />
92
- ) : (
93
- <UserIcon className="size-4" />
94
- )}
90
+ {role === "assistant" ? <SparklesIcon className="size-4" /> : <UserIcon className="size-4" />}
95
91
  </AvatarFallback>
96
92
  </Avatar>
97
93
  );
@@ -395,8 +391,7 @@ export const AssistantBubble = ({
395
391
  // pop in mid-stream. A bubble is "settled" if it isn't the active
396
392
  // streaming target - either the agent has returned to `ready` /
397
393
  // `error`, or a newer message has taken over the `isLast` slot.
398
- const isStreamingThisBubble =
399
- isLast && (status === "streaming" || status === "submitted");
394
+ const isStreamingThisBubble = isLast && (status === "streaming" || status === "submitted");
400
395
  const suggestions = isStreamingThisBubble ? [] : collectSuggestions(events);
401
396
  // Charts and tables are placed at inline marker positions in
402
397
  // the assistant's prose. `prepare_chart` / `render_data` mint
@@ -499,19 +494,13 @@ export const AssistantBubble = ({
499
494
  onClick={handleCopy}
500
495
  aria-label={copied ? "Copied" : "Copy"}
501
496
  >
502
- {copied ? (
503
- <CheckIcon className="size-3" />
504
- ) : (
505
- <CopyIcon className="size-3" />
506
- )}
497
+ {copied ? <CheckIcon className="size-3" /> : <CopyIcon className="size-3" />}
507
498
  </Button>
508
499
  </TooltipTrigger>
509
500
  <TooltipContent>{copied ? "Copied" : "Copy"}</TooltipContent>
510
501
  </Tooltip>
511
502
  )}
512
- {onExport && (
513
- <ExportMenu onExport={onExport} iconOnly tooltip="Export message" />
514
- )}
503
+ {onExport && <ExportMenu onExport={onExport} iconOnly tooltip="Export message" />}
515
504
  {onFeedback && (
516
505
  <FeedbackControls
517
506
  onSubmit={onFeedback}
@@ -520,11 +509,7 @@ export const AssistantBubble = ({
520
509
  )}
521
510
  </div>
522
511
  )}
523
- <SuggestionPills
524
- questions={suggestions}
525
- onSelect={onSuggestionClick}
526
- className="mt-1"
527
- />
512
+ <SuggestionPills questions={suggestions} onSelect={onSuggestionClick} className="mt-1" />
528
513
  </ItemContent>
529
514
  </Item>
530
515
  );
@@ -1,3 +1,4 @@
1
+ import { error as errorUtil } from "@dbx-tools/shared-core";
1
2
  import {
2
3
  Alert,
3
4
  AlertDescription,
@@ -32,7 +33,6 @@ import {
32
33
  TooltipTrigger,
33
34
  cn,
34
35
  } from "@dbx-tools/ui-appkit/react";
35
- import { error as errorUtil } from "@dbx-tools/shared-core";
36
36
  import {
37
37
  ArrowDownIcon,
38
38
  GripVerticalIcon,
@@ -172,9 +172,7 @@ export const ChatView = ({
172
172
  // `scrollHeight`/`scrollTop`; once the new DOM nodes mount we shift
173
173
  // `scrollTop` so the previously-visible content stays in place
174
174
  // (instead of jumping to the bottom of the new transcript).
175
- const prependAnchorRef = useRef<{ scrollHeight: number; scrollTop: number } | null>(
176
- null,
177
- );
175
+ const prependAnchorRef = useRef<{ scrollHeight: number; scrollTop: number } | null>(null);
178
176
  const loadMoreRef = useRef(onLoadMore);
179
177
  loadMoreRef.current = onLoadMore;
180
178
  // Latest queued steers, read by the pointer-drag move handler so it reorders
@@ -267,8 +265,7 @@ export const ChatView = ({
267
265
 
268
266
  const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {
269
267
  const el = e.currentTarget;
270
- const atBottom =
271
- el.scrollHeight - el.scrollTop - el.clientHeight < BOTTOM_THRESHOLD_PX;
268
+ const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < BOTTOM_THRESHOLD_PX;
272
269
  // A scroll we caused (a pin) shouldn't change intent - only reconcile the
273
270
  // button state. A USER scroll sets intent: scrolling up unpins (stop
274
271
  // following); scrolling back to the bottom re-pins (resume following).
@@ -347,7 +344,6 @@ export const ChatView = ({
347
344
  });
348
345
  };
349
346
 
350
-
351
347
  const lastMessage = messages.at(-1);
352
348
  const lastEvents = lastMessage ? toolEventsByMessage[lastMessage.id] : undefined;
353
349
  // Single in-flight indicator for the whole turn: visible from the
@@ -360,8 +356,7 @@ export const ChatView = ({
360
356
  const lastAssistantHasContent =
361
357
  lastAssistantParts.some(
362
358
  (p) =>
363
- (p.type === "text" || p.type === "reasoning") &&
364
- Boolean((p as { text?: string }).text),
359
+ (p.type === "text" || p.type === "reasoning") && Boolean((p as { text?: string }).text),
365
360
  ) || (lastEvents?.length ?? 0) > 0;
366
361
  const hasRunningTool = (lastEvents ?? []).some((e) => e.status === "running");
367
362
  const showWaiting = isRunning;
@@ -491,48 +486,46 @@ export const ChatView = ({
491
486
  */}
492
487
  <div className={cn("flex h-full min-h-0", className)}>
493
488
  {showSidebar &&
494
- (isMobile ? (
495
- /*
496
- * Mobile: a fixed overlay drawer with a tap-to-close backdrop, so
497
- * the conversation list never eats horizontal space from the chat
498
- * on a phone. Selecting a thread / starting a new one also closes
499
- * the drawer so the transcript comes back into view. Session-only
500
- * + default closed (see `mobileDrawerOpen`).
501
- */
502
- mobileDrawerOpen && (
503
- <div className="fixed inset-0 z-40 flex">
504
- <div
505
- className="absolute inset-0 bg-black/50"
506
- onClick={toggleSidebar}
507
- aria-hidden="true"
508
- />
509
- <ThreadSidebar
510
- {...sidebarProps}
511
- onSelect={(id) => {
512
- onSelectThread?.(id);
513
- toggleSidebar();
514
- }}
515
- {...(onNewThread
516
- ? {
517
- onNew: () => {
518
- onNewThread();
519
- toggleSidebar();
520
- },
521
- }
522
- : {})}
523
- className="relative z-10 w-[85vw] max-w-xs shadow-xl"
524
- />
525
- </div>
526
- )
527
- ) : (
528
- /*
529
- * Desktop: an inline flex child sharing the row with the chat
530
- * column, using the persisted open/hide preference. Same
531
- * `sidebarProps` as mobile - only the framing + close-on-select
532
- * differ.
533
- */
534
- desktopSidebarOpen && <ThreadSidebar {...sidebarProps} />
535
- ))}
489
+ (isMobile
490
+ ? /*
491
+ * Mobile: a fixed overlay drawer with a tap-to-close backdrop, so
492
+ * the conversation list never eats horizontal space from the chat
493
+ * on a phone. Selecting a thread / starting a new one also closes
494
+ * the drawer so the transcript comes back into view. Session-only
495
+ * + default closed (see `mobileDrawerOpen`).
496
+ */
497
+ mobileDrawerOpen && (
498
+ <div className="fixed inset-0 z-40 flex">
499
+ <div
500
+ className="absolute inset-0 bg-black/50"
501
+ onClick={toggleSidebar}
502
+ aria-hidden="true"
503
+ />
504
+ <ThreadSidebar
505
+ {...sidebarProps}
506
+ onSelect={(id) => {
507
+ onSelectThread?.(id);
508
+ toggleSidebar();
509
+ }}
510
+ {...(onNewThread
511
+ ? {
512
+ onNew: () => {
513
+ onNewThread();
514
+ toggleSidebar();
515
+ },
516
+ }
517
+ : {})}
518
+ className="relative z-10 w-[85vw] max-w-xs shadow-xl"
519
+ />
520
+ </div>
521
+ )
522
+ : /*
523
+ * Desktop: an inline flex child sharing the row with the chat
524
+ * column, using the persisted open/hide preference. Same
525
+ * `sidebarProps` as mobile - only the framing + close-on-select
526
+ * differ.
527
+ */
528
+ desktopSidebarOpen && <ThreadSidebar {...sidebarProps} />)}
536
529
  <div className="flex h-full min-w-0 flex-1 flex-col">
537
530
  {showHeader && (
538
531
  /*
@@ -594,9 +587,7 @@ export const ChatView = ({
594
587
  <div className="flex items-center justify-center gap-2 py-1 text-xs text-muted-foreground">
595
588
  <Spinner className="size-3" />
596
589
  <span>
597
- {isLoadingHistory
598
- ? "Loading history..."
599
- : "Loading older messages..."}
590
+ {isLoadingHistory ? "Loading history..." : "Loading older messages..."}
600
591
  </span>
601
592
  </div>
602
593
  )}
@@ -621,12 +612,10 @@ export const ChatView = ({
621
612
  : {})}
622
613
  {...(onFeedback && feedbackByMessage[message.id]
623
614
  ? {
624
- onFeedback: (submission) =>
625
- onFeedback(message, submission),
615
+ onFeedback: (submission) => onFeedback(message, submission),
626
616
  ...(feedbackByMessage[message.id]?.value
627
617
  ? {
628
- feedbackValue:
629
- feedbackByMessage[message.id]!.value,
618
+ feedbackValue: feedbackByMessage[message.id]!.value,
630
619
  }
631
620
  : {}),
632
621
  }
@@ -705,93 +694,93 @@ export const ChatView = ({
705
694
  {queuedSteers.map((steer) => {
706
695
  const reorderable = Boolean(onReorderSteers);
707
696
  return (
708
- <div
709
- key={steer.id}
710
- ref={(el) => {
711
- if (el) steerChipRefs.current.set(steer.id, el);
712
- else steerChipRefs.current.delete(steer.id);
713
- }}
714
- className={cn(
715
- "flex items-center gap-1.5 rounded-lg border border-border/70 bg-muted/40 px-2 py-1 text-xs",
716
- draggingSteerId === steer.id && "opacity-50",
717
- )}
718
- >
719
- {reorderable && (
720
- // Drag handle. Pointer Events (not native HTML5 drag) so
721
- // it works on touch: `touch-none` (touch-action: none)
722
- // stops the browser treating the drag as a scroll, and
723
- // pointer capture keeps move/up events flowing to the grip
724
- // even as the finger slides over sibling chips. The active
725
- // id lives in a ref (`draggingIdRef`) so the first
726
- // pointermove isn't dropped waiting for a state re-render.
727
- // `-m-1 p-1` enlarges the tap target to ~28px without
728
- // widening the visible grip - a 12px icon is too small to
729
- // reliably grab on touch.
730
- <span
731
- role="button"
732
- tabIndex={-1}
733
- aria-label="Drag to reorder"
734
- className="-m-1 shrink-0 cursor-grab touch-none p-1 text-muted-foreground active:cursor-grabbing"
735
- onPointerDown={(e) => {
736
- e.preventDefault();
737
- e.currentTarget.setPointerCapture(e.pointerId);
738
- draggingIdRef.current = steer.id;
739
- setDraggingSteerId(steer.id);
740
- }}
741
- onPointerMove={(e) => {
742
- if (draggingIdRef.current !== steer.id) return;
743
- reorderSteersByPointer(steer.id, e.clientY);
744
- }}
745
- onPointerUp={(e) => {
746
- e.currentTarget.releasePointerCapture(e.pointerId);
747
- draggingIdRef.current = null;
748
- setDraggingSteerId(null);
749
- }}
750
- onPointerCancel={() => {
751
- draggingIdRef.current = null;
752
- setDraggingSteerId(null);
753
- }}
754
- >
755
- <GripVerticalIcon className="size-3" aria-hidden="true" />
756
- </span>
757
- )}
758
- <span className="text-muted-foreground">Queued</span>
759
- <span className="min-w-0 flex-1 truncate">{steer.text}</span>
760
- {onSendSteerNow && (
761
- <Tooltip>
762
- <TooltipTrigger asChild>
763
- <Button
764
- type="button"
765
- variant="ghost"
766
- size="icon"
767
- className="size-6 shrink-0"
768
- onClick={() => onSendSteerNow(steer.id)}
769
- aria-label="Send now (interrupts current turn)"
770
- >
771
- <SendHorizontalIcon className="size-3" />
772
- </Button>
773
- </TooltipTrigger>
774
- <TooltipContent>Send now — interrupts</TooltipContent>
775
- </Tooltip>
776
- )}
777
- {onRemoveSteer && (
778
- <Tooltip>
779
- <TooltipTrigger asChild>
780
- <Button
781
- type="button"
782
- variant="ghost"
783
- size="icon"
784
- className="size-6 shrink-0"
785
- onClick={() => onRemoveSteer(steer.id)}
786
- aria-label="Remove queued message"
787
- >
788
- <XIcon className="size-3" />
789
- </Button>
790
- </TooltipTrigger>
791
- <TooltipContent>Remove</TooltipContent>
792
- </Tooltip>
793
- )}
794
- </div>
697
+ <div
698
+ key={steer.id}
699
+ ref={(el) => {
700
+ if (el) steerChipRefs.current.set(steer.id, el);
701
+ else steerChipRefs.current.delete(steer.id);
702
+ }}
703
+ className={cn(
704
+ "flex items-center gap-1.5 rounded-lg border border-border/70 bg-muted/40 px-2 py-1 text-xs",
705
+ draggingSteerId === steer.id && "opacity-50",
706
+ )}
707
+ >
708
+ {reorderable && (
709
+ // Drag handle. Pointer Events (not native HTML5 drag) so
710
+ // it works on touch: `touch-none` (touch-action: none)
711
+ // stops the browser treating the drag as a scroll, and
712
+ // pointer capture keeps move/up events flowing to the grip
713
+ // even as the finger slides over sibling chips. The active
714
+ // id lives in a ref (`draggingIdRef`) so the first
715
+ // pointermove isn't dropped waiting for a state re-render.
716
+ // `-m-1 p-1` enlarges the tap target to ~28px without
717
+ // widening the visible grip - a 12px icon is too small to
718
+ // reliably grab on touch.
719
+ <span
720
+ role="button"
721
+ tabIndex={-1}
722
+ aria-label="Drag to reorder"
723
+ className="-m-1 shrink-0 cursor-grab touch-none p-1 text-muted-foreground active:cursor-grabbing"
724
+ onPointerDown={(e) => {
725
+ e.preventDefault();
726
+ e.currentTarget.setPointerCapture(e.pointerId);
727
+ draggingIdRef.current = steer.id;
728
+ setDraggingSteerId(steer.id);
729
+ }}
730
+ onPointerMove={(e) => {
731
+ if (draggingIdRef.current !== steer.id) return;
732
+ reorderSteersByPointer(steer.id, e.clientY);
733
+ }}
734
+ onPointerUp={(e) => {
735
+ e.currentTarget.releasePointerCapture(e.pointerId);
736
+ draggingIdRef.current = null;
737
+ setDraggingSteerId(null);
738
+ }}
739
+ onPointerCancel={() => {
740
+ draggingIdRef.current = null;
741
+ setDraggingSteerId(null);
742
+ }}
743
+ >
744
+ <GripVerticalIcon className="size-3" aria-hidden="true" />
745
+ </span>
746
+ )}
747
+ <span className="text-muted-foreground">Queued</span>
748
+ <span className="min-w-0 flex-1 truncate">{steer.text}</span>
749
+ {onSendSteerNow && (
750
+ <Tooltip>
751
+ <TooltipTrigger asChild>
752
+ <Button
753
+ type="button"
754
+ variant="ghost"
755
+ size="icon"
756
+ className="size-6 shrink-0"
757
+ onClick={() => onSendSteerNow(steer.id)}
758
+ aria-label="Send now (interrupts current turn)"
759
+ >
760
+ <SendHorizontalIcon className="size-3" />
761
+ </Button>
762
+ </TooltipTrigger>
763
+ <TooltipContent>Send now — interrupts</TooltipContent>
764
+ </Tooltip>
765
+ )}
766
+ {onRemoveSteer && (
767
+ <Tooltip>
768
+ <TooltipTrigger asChild>
769
+ <Button
770
+ type="button"
771
+ variant="ghost"
772
+ size="icon"
773
+ className="size-6 shrink-0"
774
+ onClick={() => onRemoveSteer(steer.id)}
775
+ aria-label="Remove queued message"
776
+ >
777
+ <XIcon className="size-3" />
778
+ </Button>
779
+ </TooltipTrigger>
780
+ <TooltipContent>Remove</TooltipContent>
781
+ </Tooltip>
782
+ )}
783
+ </div>
795
784
  );
796
785
  })}
797
786
  </div>
@@ -850,9 +839,7 @@ export const ChatView = ({
850
839
  (modelChangeable ? (
851
840
  <Select
852
841
  value={model ? model : DEFAULT_MODEL_VALUE}
853
- onValueChange={(v) =>
854
- onModelChange?.(v === DEFAULT_MODEL_VALUE ? "" : v)
855
- }
842
+ onValueChange={(v) => onModelChange?.(v === DEFAULT_MODEL_VALUE ? "" : v)}
856
843
  >
857
844
  <SelectTrigger
858
845
  size="sm"
@@ -861,9 +848,7 @@ export const ChatView = ({
861
848
  <SelectValue placeholder={defaultOptionLabel} />
862
849
  </SelectTrigger>
863
850
  <SelectContent>
864
- <SelectItem value={DEFAULT_MODEL_VALUE}>
865
- {defaultOptionLabel}
866
- </SelectItem>
851
+ <SelectItem value={DEFAULT_MODEL_VALUE}>{defaultOptionLabel}</SelectItem>
867
852
  {sortedModels.map((m) => (
868
853
  <SelectItem key={m.name} value={m.name}>
869
854
  {m.displayName || m.name}
@@ -1,3 +1,4 @@
1
+ import { string } from "@dbx-tools/shared-core";
1
2
  import {
2
3
  Button,
3
4
  DropdownMenu,
@@ -12,7 +13,6 @@ import {
12
13
  TableRow,
13
14
  cn,
14
15
  } from "@dbx-tools/ui-appkit/react";
15
- import { string } from "@dbx-tools/shared-core";
16
16
  import {
17
17
  flexRender,
18
18
  getCoreRowModel,
@@ -56,8 +56,9 @@ export function colorizeDelta(content: React.ReactNode): React.ReactNode {
56
56
  if (!match) return content;
57
57
  const sign = match[1];
58
58
  if (sign === "+") return <span className="font-medium text-success">{content}</span>;
59
- if (sign === "-" || sign === "\u2212")
59
+ if (sign === "-" || sign === "\u2212") {
60
60
  return <span className="font-medium text-destructive">{content}</span>;
61
+ }
61
62
  return content;
62
63
  }
63
64
 
@@ -80,10 +81,7 @@ export function renderDataCell(value: unknown): React.ReactNode {
80
81
  * column id, accessor key, and CSV header, so only the on-screen label
81
82
  * is prettified.
82
83
  */
83
- export function humanizeLabel(
84
- value: string,
85
- options?: string.TokenizeOptions,
86
- ): string {
84
+ export function humanizeLabel(value: string, options?: string.TokenizeOptions): string {
87
85
  return string.toLabel(value, options);
88
86
  }
89
87
 
@@ -275,12 +273,7 @@ export const DataGrid = ({
275
273
  ))}
276
274
  </DropdownMenuContent>
277
275
  </DropdownMenu>
278
- <Button
279
- variant="ghost"
280
- size="sm"
281
- className="h-7 gap-1 px-2 text-xs"
282
- onClick={exportCsv}
283
- >
276
+ <Button variant="ghost" size="sm" className="h-7 gap-1 px-2 text-xs" onClick={exportCsv}>
284
277
  <DownloadIcon className="size-3.5" />
285
278
  Export
286
279
  </Button>
@@ -300,10 +293,7 @@ export const DataGrid = ({
300
293
  className="inline-flex items-center gap-1 hover:text-foreground"
301
294
  onClick={header.column.getToggleSortingHandler()}
302
295
  >
303
- {flexRender(
304
- header.column.columnDef.header,
305
- header.getContext(),
306
- )}
296
+ {flexRender(header.column.columnDef.header, header.getContext())}
307
297
  {sorted === "asc" ? (
308
298
  <ArrowUpIcon className="size-3" />
309
299
  ) : sorted === "desc" ? (
@@ -1,15 +1,12 @@
1
+ import { marker as markers, type ParsedMarker } from "@dbx-tools/shared-mastra";
1
2
  import { Spinner } from "@dbx-tools/ui-appkit/react";
2
- import {
3
- marker as markers,
4
- type ParsedMarker,
5
- } from "@dbx-tools/shared-mastra";
6
3
  import ReactECharts from "echarts-for-react";
7
4
  import { ClockIcon } from "lucide-react";
8
5
  import { useMemo } from "react";
9
- import { normalizeChartOption } from "../support/chart-option";
10
- import { useChartFetch, useStatementFetch } from "../support/mastra-client";
11
6
  import { DataGrid, humanizeLabel } from "./data-grid";
12
7
  import { AssistantMarkdown } from "./markdown";
8
+ import { normalizeChartOption } from "../support/chart-option";
9
+ import { useChartFetch, useStatementFetch } from "../support/mastra-client";
13
10
 
14
11
  // Inline embed slots: chart / data tables resolved from `[chart:<id>]`
15
12
  // and `[data:<id>]` markers in the assistant's prose, plus the splitter
@@ -43,8 +40,7 @@ const ExpiredSlot = ({ type }: { type: string }) => (
43
40
  <div className="not-prose my-3 inline-flex items-center gap-2 rounded-md border border-dashed border-border bg-muted/40 px-3 py-1.5 text-xs text-muted-foreground">
44
41
  <ClockIcon className="size-3.5 shrink-0" />
45
42
  <span>
46
- This {humanizeLabel(type, { capitalize: false })} has expired and is no longer
47
- available.
43
+ This {humanizeLabel(type, { capitalize: false })} has expired and is no longer available.
48
44
  </span>
49
45
  </div>
50
46
  );
@@ -249,9 +245,7 @@ export const MarkdownWithEmbeds = ({
249
245
  if (seg.kind === "chart") {
250
246
  return <ChartSlot key={`c-${i}-${seg.chartId}`} chartId={seg.chartId} />;
251
247
  }
252
- return (
253
- <DataSlot key={`d-${i}-${seg.statementId}`} statementId={seg.statementId} />
254
- );
248
+ return <DataSlot key={`d-${i}-${seg.statementId}`} statementId={seg.statementId} />;
255
249
  })}
256
250
  </>
257
251
  );
@@ -114,12 +114,7 @@ export const FeedbackControls = ({
114
114
  autoFocus
115
115
  />
116
116
  <div className="flex justify-end gap-2">
117
- <Button
118
- type="button"
119
- size="sm"
120
- variant="ghost"
121
- onClick={() => setOpen(false)}
122
- >
117
+ <Button type="button" size="sm" variant="ghost" onClick={() => setOpen(false)}>
123
118
  Cancel
124
119
  </Button>
125
120
  <Button
@@ -16,13 +16,8 @@ import { CheckIcon, CopyIcon } from "lucide-react";
16
16
  import React, { useEffect, useMemo, useRef, useState } from "react";
17
17
  import { format as formatSql } from "sql-formatter";
18
18
  import { Streamdown } from "streamdown";
19
+ import { DataGrid, TABLE_WRAPPER_CLASSES, colorizeDelta, type DataRow } from "./data-grid";
19
20
  import { createShikiPlugin, highlightToHtml } from "../support/shiki-plugin";
20
- import {
21
- DataGrid,
22
- TABLE_WRAPPER_CLASSES,
23
- colorizeDelta,
24
- type DataRow,
25
- } from "./data-grid";
26
21
 
27
22
  // Markdown rendering for the chat: the streaming `Streamdown` engine
28
23
  // wired with shiki highlighting and AppKit table primitives, plus the
@@ -62,9 +57,7 @@ function markdownTableData(
62
57
  if (!node || node.tagName !== "table") return null;
63
58
  const sections = node.children ?? [];
64
59
  const sectionRows = (tag: string): MarkdownNode[] =>
65
- sections
66
- .find((s) => s.tagName === tag)
67
- ?.children?.filter((r) => r.tagName === "tr") ?? [];
60
+ sections.find((s) => s.tagName === tag)?.children?.filter((r) => r.tagName === "tr") ?? [];
68
61
 
69
62
  const headerCells = (sectionRows("thead")[0]?.children ?? []).filter(
70
63
  (c) => c.tagName === "th" || c.tagName === "td",
@@ -82,9 +75,7 @@ function markdownTableData(
82
75
  }
83
76
 
84
77
  const rows: DataRow[] = sectionRows("tbody").map((tr) => {
85
- const cells = (tr.children ?? []).filter(
86
- (c) => c.tagName === "td" || c.tagName === "th",
87
- );
78
+ const cells = (tr.children ?? []).filter((c) => c.tagName === "td" || c.tagName === "th");
88
79
  const row: DataRow = {};
89
80
  columns.forEach((col, i) => {
90
81
  const cell = cells[i];
@@ -102,10 +93,7 @@ function markdownTableData(
102
93
  * renderer tool-detail copy uses unconditionally (a sort/column/export
103
94
  * toolbar would dwarf the tiny inline pills it renders in).
104
95
  */
105
- const plainMarkdownTable = ({
106
- children,
107
- ...rest
108
- }: React.HTMLAttributes<HTMLTableElement>) => (
96
+ const plainMarkdownTable = ({ children, ...rest }: React.HTMLAttributes<HTMLTableElement>) => (
109
97
  <div className={TABLE_WRAPPER_CLASSES}>
110
98
  <Table {...rest}>{children}</Table>
111
99
  </div>
@@ -139,9 +127,7 @@ const MARKDOWN_TABLE_PARTS = {
139
127
  ),
140
128
  td: ({ children, ...rest }: React.HTMLAttributes<HTMLTableCellElement>) => {
141
129
  const colored = Array.isArray(children)
142
- ? children.map((c, i) => (
143
- <React.Fragment key={i}>{colorizeDelta(c)}</React.Fragment>
144
- ))
130
+ ? children.map((c, i) => <React.Fragment key={i}>{colorizeDelta(c)}</React.Fragment>)
145
131
  : colorizeDelta(children as React.ReactNode);
146
132
  return <TableCell {...rest}>{colored}</TableCell>;
147
133
  },