@dbx-tools/ui-mastra 0.9.118 → 0.9.120

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.
@@ -23,6 +23,7 @@ import type { FeedbackSubmission, FeedbackValue } from "./types.ts";
23
23
  // wires feedback (which only happens when MLflow logging is enabled and
24
24
  // the turn has a captured trace id).
25
25
 
26
+ /** Render per-message rating and comment controls for an MLflow-backed trace. */
26
27
  export const FeedbackControls = ({
27
28
  value,
28
29
  onSubmit,
@@ -18,6 +18,7 @@ import {
18
18
  import { CheckIcon, CopyIcon } from "lucide-react";
19
19
  import React, { useEffect, useRef, useState } from "react";
20
20
  import { Streamdown } from "streamdown";
21
+ import { ExternalLinkDialog } from "./_external-link-dialog.tsx";
21
22
  import { DataGrid, TABLE_WRAPPER_CLASSES, colorizeDelta, type DataRow } from "./data-grid.tsx";
22
23
  import { copyText } from "../support/clipboard.ts";
23
24
 
@@ -183,6 +184,14 @@ const TOOL_MARKDOWN_COMPONENTS = {
183
184
  */
184
185
  const SHIKI_PLUGIN = { code: createShikiPlugin() };
185
186
 
187
+ /** Keep Streamdown's link confirmation while matching the chat's AppKit UI. */
188
+ const LINK_SAFETY = {
189
+ enabled: true,
190
+ renderModal: (props: React.ComponentProps<typeof ExternalLinkDialog>) => (
191
+ <ExternalLinkDialog {...props} />
192
+ ),
193
+ };
194
+
186
195
  /**
187
196
  * Per-word fade-in applied while a reply is still streaming. Streamdown
188
197
  * wraps each newly arrived token in a `[data-sd-animate]` span and only
@@ -235,6 +244,7 @@ export const AssistantMarkdown = ({
235
244
  components={MARKDOWN_COMPONENTS}
236
245
  plugins={SHIKI_PLUGIN}
237
246
  controls={false}
247
+ linkSafety={LINK_SAFETY}
238
248
  animated={animate ? ANIMATE_OPTIONS : false}
239
249
  isAnimating={animate}
240
250
  className={cn(
@@ -269,6 +279,7 @@ export const ToolMarkdown = ({ children, className }: { children: string; classN
269
279
  components={TOOL_MARKDOWN_COMPONENTS}
270
280
  plugins={SHIKI_PLUGIN}
271
281
  controls={false}
282
+ linkSafety={LINK_SAFETY}
272
283
  className={cn(
273
284
  "prose prose-sm dark:prose-invert max-w-none break-words",
274
285
  "text-xs leading-snug text-muted-foreground",
@@ -84,6 +84,8 @@ export interface MastraAssistantProps<TValues extends Record<string, unknown>> e
84
84
  closeIcon?: ReactNode;
85
85
  newConversationIcon?: ReactNode;
86
86
  historyIcon?: ReactNode;
87
+ /** Host controls rendered immediately before the History action and in the sidebar header. */
88
+ historyLeadingActions?: ReactNode;
87
89
  contentClassName?: string;
88
90
  panelClassName?: string;
89
91
  headerClassName?: string;
@@ -205,6 +207,7 @@ export function MastraAssistant<TValues extends Record<string, unknown> = Record
205
207
  closeIcon,
206
208
  newConversationIcon,
207
209
  historyIcon,
210
+ historyLeadingActions,
208
211
  className,
209
212
  contentClassName,
210
213
  panelClassName,
@@ -493,6 +496,7 @@ export function MastraAssistant<TValues extends Record<string, unknown> = Record
493
496
  onCancel={chatView.onCancelThread}
494
497
  newIcon={newConversationIcon}
495
498
  historyIcon={historyIcon}
499
+ historyLeadingActions={historyLeadingActions}
496
500
  actions={closeAction}
497
501
  />
498
502
  ) : (
@@ -550,6 +554,8 @@ export function MastraAssistant<TValues extends Record<string, unknown> = Record
550
554
  threadBarActions={!header ? closeAction : undefined}
551
555
  threadNewIcon={newConversationIcon}
552
556
  threadHistoryIcon={historyIcon}
557
+ threadHistoryLeadingActions={historyLeadingActions}
558
+ threadSidebarHeaderActions={historyLeadingActions}
553
559
  className={cn("min-h-0 flex-1", chatClassName)}
554
560
  />
555
561
  {!header && chatView.threadPlacement !== "top" ? (
@@ -4,6 +4,7 @@ import type { ReasoningEffort } from "@dbx-tools/shared-model";
4
4
  import { useBrand } from "@dbx-tools/ui/branding/react";
5
5
  import type { FileUIPart, UIMessage } from "ai";
6
6
  import { type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from "react";
7
+ import { toChronologicalUiMessages } from "./_history-messages.ts";
7
8
  import { useChatApprovals } from "./chat-approvals.ts";
8
9
  import { useChatFeedback } from "./chat-feedback.ts";
9
10
  import { useChatHistory } from "./chat-history.ts";
@@ -12,6 +13,12 @@ import { useChatStream } from "./chat-stream.ts";
12
13
  import { ChatView } from "./chat-view.tsx";
13
14
  import { dedupeSuggestions } from "./suggestions.ts";
14
15
  import type { ChatViewProps, ThreadPlacement, ThreadSummary } from "./types.ts";
16
+ import {
17
+ activeRunStorageKey,
18
+ readStoredActiveRuns,
19
+ removeStoredActiveRun,
20
+ storeActiveRun,
21
+ } from "../support/active-runs.ts";
15
22
  import type { EmbedResolver, ExportFormat } from "../support/export.ts";
16
23
  import {
17
24
  useMastraClient,
@@ -20,6 +27,7 @@ import {
20
27
  useMastraModels,
21
28
  useMastraSuggestions,
22
29
  useMastraThreads,
30
+ MastraChatRequestError,
23
31
  type MastraMemoryThread,
24
32
  type MastraStreamResponse,
25
33
  } from "../support/mastra-client.ts";
@@ -189,7 +197,7 @@ export interface UseMastraChatOptions<
189
197
  * Where conversation management renders: `"left"` / `"right"` dock the list
190
198
  * to that edge, `"top"` shows the open conversations as an editor-style tab
191
199
  * strip with a history menu, `"disabled"` turns thread management off, and
192
- * the default `"auto"` picks `"left"` while the chat is wide enough for a
200
+ * the default `"auto"` picks `"right"` while the chat is wide enough for a
193
201
  * side panel and `"top"` once it gets too narrow for one (measured on the
194
202
  * chat's own width, so an embedded panel decides on its own space).
195
203
  */
@@ -292,6 +300,7 @@ export const useMastraChat = <TValues extends Record<string, unknown> = Record<s
292
300
  const feedbackAvailable = enableFeedback && mastraClient.feedbackEnabled;
293
301
  const { url: mlflowExperimentUrl } = useMastraMlflowExperiment(feedbackAvailable);
294
302
  const threadKey = threadStorageKey(mastraClient.basePath, agentId);
303
+ const activeRunsKey = activeRunStorageKey(mastraClient.basePath, agentId);
295
304
  const [activeThreadId, setActiveThreadId] = useState<string>(() =>
296
305
  enableThreads ? (readStoredThreadId(threadKey) ?? hash.id()) : hash.id(),
297
306
  );
@@ -471,6 +480,7 @@ export const useMastraChat = <TValues extends Record<string, unknown> = Record<s
471
480
  if (
472
481
  controller.signal.aborted ||
473
482
  getSession(threadId).runToken !== token ||
483
+ initialError instanceof MastraChatRequestError ||
474
484
  !mastraClient.backgroundTurns ||
475
485
  !runId
476
486
  ) {
@@ -478,7 +488,7 @@ export const useMastraChat = <TValues extends Record<string, unknown> = Record<s
478
488
  }
479
489
  logger.warn("chat stream disconnected; reconnecting to background run", {
480
490
  runId,
481
- error: errorUtils.errorMessage(initialError),
491
+ error: errorUtils.toError(initialError),
482
492
  });
483
493
  const messagesBeforeReplay = getSession(threadId).messages;
484
494
  writeMessages(
@@ -510,6 +520,8 @@ export const useMastraChat = <TValues extends Record<string, unknown> = Record<s
510
520
  };
511
521
  });
512
522
  if (getSession(threadId).runToken === token) {
523
+ const completedRunId = runIdRef.current;
524
+ if (completedRunId) removeStoredActiveRun(activeRunsKey, completedRunId);
513
525
  refreshThreadsSoon();
514
526
  // Turn finished on its own: start the oldest queued steer, if any.
515
527
  drainQueueRef.current(threadId);
@@ -518,7 +530,7 @@ export const useMastraChat = <TValues extends Record<string, unknown> = Record<s
518
530
  if (getSession(threadId).runToken !== token) return;
519
531
  if (isStaleMastraResumeError(caught)) {
520
532
  logger.warn("ignored stale mastra resume", {
521
- error: errorUtils.errorMessage(caught),
533
+ error: errorUtils.toError(caught),
522
534
  });
523
535
  updateSession(threadId, (session) => {
524
536
  if (session.runToken !== token) return session;
@@ -531,12 +543,14 @@ export const useMastraChat = <TValues extends Record<string, unknown> = Record<s
531
543
  runId: runIdRef.current,
532
544
  };
533
545
  });
546
+ const staleRunId = runIdRef.current;
547
+ if (staleRunId) removeStoredActiveRun(activeRunsKey, staleRunId);
534
548
  refreshThreadsSoon();
535
549
  drainQueueRef.current(threadId);
536
550
  return;
537
551
  }
538
552
  logger.error("stream error", {
539
- error: errorUtils.errorMessage(caught),
553
+ error: errorUtils.toError(caught),
540
554
  });
541
555
  updateSession(threadId, (session) => ({
542
556
  ...session,
@@ -545,10 +559,13 @@ export const useMastraChat = <TValues extends Record<string, unknown> = Record<s
545
559
  abortController: session.abortController === controller ? null : session.abortController,
546
560
  runId: runIdRef.current,
547
561
  }));
562
+ const failedRunId = runIdRef.current;
563
+ if (failedRunId) removeStoredActiveRun(activeRunsKey, failedRunId);
548
564
  }
549
565
  },
550
566
  [
551
567
  agentId,
568
+ activeRunsKey,
552
569
  getSession,
553
570
  mastraClient,
554
571
  processStream,
@@ -562,6 +579,13 @@ export const useMastraChat = <TValues extends Record<string, unknown> = Record<s
562
579
  (threadId: string, message: UIMessage, requestContext?: MastraRequestContextSnapshot) => {
563
580
  const assistantId = hash.id();
564
581
  const runId = hash.id();
582
+ storeActiveRun(activeRunsKey, {
583
+ threadId,
584
+ runId,
585
+ assistantId,
586
+ userMessage: message,
587
+ startedAt: Date.now(),
588
+ });
565
589
  updateSession(threadId, (session) => ({
566
590
  ...session,
567
591
  assistantId,
@@ -595,9 +619,50 @@ export const useMastraChat = <TValues extends Record<string, unknown> = Record<s
595
619
  });
596
620
  });
597
621
  },
598
- [driveStream, mastraClient, agentId, updateSession],
622
+ [activeRunsKey, driveStream, mastraClient, agentId, updateSession],
599
623
  );
600
624
 
625
+ const recoveredRunsKeyRef = useRef<string | undefined>(undefined);
626
+ useEffect(() => {
627
+ if (!mastraClient.backgroundTurns || recoveredRunsKeyRef.current === activeRunsKey) return;
628
+ recoveredRunsKeyRef.current = activeRunsKey;
629
+ for (const run of readStoredActiveRuns(activeRunsKey)) {
630
+ const session = getSession(run.threadId);
631
+ if (isSessionRunning(session) || session.runId === run.runId) continue;
632
+ updateSession(run.threadId, (current) => ({
633
+ ...current,
634
+ messages: current.messages.some((message) => message.id === run.userMessage.id)
635
+ ? current.messages
636
+ : [...current.messages, run.userMessage],
637
+ assistantId: run.assistantId,
638
+ runId: run.runId,
639
+ runs: {
640
+ ...current.runs,
641
+ [run.runId]: { assistantId: run.assistantId },
642
+ },
643
+ }));
644
+ void driveStream(run.threadId, run.assistantId, (signal) =>
645
+ mastraClient.observeAgentStream({
646
+ agentId,
647
+ runId: run.runId,
648
+ threadId: run.threadId === DEFAULT_THREAD_SESSION_KEY ? undefined : run.threadId,
649
+ signal,
650
+ }),
651
+ ).then(async () => {
652
+ if (run.threadId === DEFAULT_THREAD_SESSION_KEY) return;
653
+ const history = await mastraClient
654
+ .history({
655
+ agentId,
656
+ threadId: run.threadId,
657
+ page: 0,
658
+ perPage: 20,
659
+ })
660
+ .catch(() => undefined);
661
+ if (history) writeMessages(run.threadId, toChronologicalUiMessages(history));
662
+ });
663
+ }
664
+ }, [activeRunsKey, agentId, driveStream, getSession, mastraClient, updateSession, writeMessages]);
665
+
601
666
  // Start the oldest queued steer as the next turn (FIFO drain). Called from
602
667
  // `driveStream` when a turn ends normally; a no-op when the queue is empty.
603
668
  // Kept behind `drainQueueRef` so `driveStream` can reach it without a
@@ -632,10 +697,13 @@ export const useMastraChat = <TValues extends Record<string, unknown> = Record<s
632
697
  runId: current.runId,
633
698
  threadId: streamThreadId,
634
699
  })
700
+ .then((aborted) => {
701
+ if (aborted && current.runId) removeStoredActiveRun(activeRunsKey, current.runId);
702
+ })
635
703
  .catch((error: unknown) => {
636
704
  logger.warn("failed to abort background run", {
637
705
  runId: current.runId,
638
- error: errorUtils.errorMessage(error),
706
+ error: errorUtils.toError(error),
639
707
  });
640
708
  });
641
709
  }
@@ -651,7 +719,7 @@ export const useMastraChat = <TValues extends Record<string, unknown> = Record<s
651
719
  };
652
720
  });
653
721
  },
654
- [activeKey, agentId, getSession, mastraClient, updateSession],
722
+ [activeKey, activeRunsKey, agentId, getSession, mastraClient, updateSession],
655
723
  );
656
724
 
657
725
  const handleApproval = useChatApprovals({
@@ -777,7 +845,7 @@ export const useMastraChat = <TValues extends Record<string, unknown> = Record<s
777
845
  logger.info("history cleared", { threadId });
778
846
  } catch (error) {
779
847
  logger.error("history clear error", {
780
- error: errorUtils.errorMessage(error),
848
+ error: errorUtils.toError(error),
781
849
  });
782
850
  }
783
851
  resetSession(threadId, true);
@@ -818,7 +886,7 @@ export const useMastraChat = <TValues extends Record<string, unknown> = Record<s
818
886
  } catch (error) {
819
887
  logger.error("thread delete error", {
820
888
  threadId,
821
- error: errorUtils.errorMessage(error),
889
+ error: errorUtils.toError(error),
822
890
  });
823
891
  }
824
892
  removeSession(threadId);
@@ -863,7 +931,7 @@ export const useMastraChat = <TValues extends Record<string, unknown> = Record<s
863
931
  } catch (error) {
864
932
  logger.error("thread rename error", {
865
933
  threadId,
866
- error: errorUtils.errorMessage(error),
934
+ error: errorUtils.toError(error),
867
935
  });
868
936
  setRenamedThreads((prev) => {
869
937
  if (!(threadId in prev)) return prev;
@@ -906,7 +974,7 @@ export const useMastraChat = <TValues extends Record<string, unknown> = Record<s
906
974
  .catch((error: unknown) => {
907
975
  logger.error("regenerate cleanup error", {
908
976
  threadId,
909
- error: errorUtils.errorMessage(error),
977
+ error: errorUtils.toError(error),
910
978
  });
911
979
  updateSession(threadId, (session) => ({
912
980
  ...session,
@@ -976,7 +1044,7 @@ export const useMastraChat = <TValues extends Record<string, unknown> = Record<s
976
1044
  } catch (error) {
977
1045
  logger.error("conversation export error", {
978
1046
  format,
979
- error: errorUtils.errorMessage(error),
1047
+ error: errorUtils.toError(error),
980
1048
  });
981
1049
  }
982
1050
  },
@@ -998,7 +1066,7 @@ export const useMastraChat = <TValues extends Record<string, unknown> = Record<s
998
1066
  } catch (error) {
999
1067
  logger.error("message export error", {
1000
1068
  format,
1001
- error: errorUtils.errorMessage(error),
1069
+ error: errorUtils.toError(error),
1002
1070
  });
1003
1071
  }
1004
1072
  },
@@ -1155,6 +1223,10 @@ export interface MastraChatProps<
1155
1223
  modelSelectorActions?: ReactNode;
1156
1224
  /** Host controls placed on the left side of the composer footer. */
1157
1225
  composerLeadingActions?: ReactNode;
1226
+ /** Host controls rendered immediately before the responsive History action. */
1227
+ threadHistoryLeadingActions?: ReactNode;
1228
+ /** Host controls rendered in the full conversation-list header. */
1229
+ threadSidebarHeaderActions?: ReactNode;
1158
1230
  }
1159
1231
 
1160
1232
  /**
@@ -1167,7 +1239,7 @@ export interface MastraChatProps<
1167
1239
  * of the resource's threads with select / new / delete, persisted
1168
1240
  * across reloads) - all with no host wiring. The model picker is opt-in
1169
1241
  * via `showModelPicker`; thread management is on by default, laid out per
1170
- * `threadPlacement` (`auto` docks it left and falls back to a tab strip on a
1242
+ * `threadPlacement` (`auto` docks it right and falls back to a tab strip on a
1171
1243
  * narrow chat) and turned off with `threadPlacement: "disabled"`.
1172
1244
  */
1173
1245
  export const MastraChat = <TValues extends Record<string, unknown> = Record<string, unknown>>({
@@ -1175,6 +1247,8 @@ export const MastraChat = <TValues extends Record<string, unknown> = Record<stri
1175
1247
  composerActions,
1176
1248
  modelSelectorActions,
1177
1249
  composerLeadingActions,
1250
+ threadHistoryLeadingActions,
1251
+ threadSidebarHeaderActions,
1178
1252
  ...options
1179
1253
  }: MastraChatProps<TValues>) => {
1180
1254
  const chat = useMastraChat(options);
@@ -1185,6 +1259,8 @@ export const MastraChat = <TValues extends Record<string, unknown> = Record<stri
1185
1259
  composerActions={composerActions}
1186
1260
  modelSelectorActions={modelSelectorActions}
1187
1261
  composerLeadingActions={composerLeadingActions}
1262
+ threadHistoryLeadingActions={threadHistoryLeadingActions}
1263
+ threadSidebarHeaderActions={threadSidebarHeaderActions}
1188
1264
  />
1189
1265
  );
1190
1266
  };
@@ -5,6 +5,7 @@ import { Button, cn } from "@dbx-tools/ui/react";
5
5
  // the per-message follow-up suggestions use this so the two look and
6
6
  // behave identically.
7
7
 
8
+ /** Props for the shared starter and follow-up question pill row. */
8
9
  export interface SuggestionPillsProps {
9
10
  /** Questions to render as clickable pills. Empty renders nothing. */
10
11
  questions: string[];
@@ -16,7 +16,7 @@ import {
16
16
  SquareIcon,
17
17
  Trash2Icon,
18
18
  } from "lucide-react";
19
- import { useState } from "react";
19
+ import { type ReactNode, useState } from "react";
20
20
  import type { ThreadSummary } from "./types.ts";
21
21
  import { relativeTime, threadTitle } from "../support/thread-labels.ts";
22
22
 
@@ -52,6 +52,8 @@ export interface ThreadSidebarProps {
52
52
  onCancel?: (threadId: string) => void;
53
53
  /** Collapse the sidebar. Renders the hide button in the header when provided. */
54
54
  onHide?: () => void;
55
+ /** Host controls rendered in the conversation-list header. */
56
+ headerActions?: ReactNode;
55
57
  /**
56
58
  * Edge the sidebar is docked to. Only affects framing - which side carries
57
59
  * the divider, and which way the collapse icon points. Defaults to `left`.
@@ -82,6 +84,7 @@ export const ThreadSidebar = ({
82
84
  onRename,
83
85
  onCancel,
84
86
  onHide,
87
+ headerActions,
85
88
  side = "left",
86
89
  className,
87
90
  }: ThreadSidebarProps) => {
@@ -134,7 +137,7 @@ export const ThreadSidebar = ({
134
137
  className,
135
138
  )}
136
139
  >
137
- {(onNew || onHide) && (
140
+ {(onNew || headerActions || onHide) && (
138
141
  <div className="flex items-center gap-2 p-2">
139
142
  {onNew && (
140
143
  <Button
@@ -148,6 +151,7 @@ export const ThreadSidebar = ({
148
151
  New chat
149
152
  </Button>
150
153
  )}
154
+ {headerActions}
151
155
  {onHide && (
152
156
  <Tooltip>
153
157
  <TooltipTrigger asChild>
@@ -44,6 +44,8 @@ export interface ThreadTabsProps extends Omit<ThreadSidebarProps, "onHide" | "si
44
44
  newIcon?: ReactNode;
45
45
  /** Replacement for the default history icon. */
46
46
  historyIcon?: ReactNode;
47
+ /** Host controls rendered immediately before History. */
48
+ historyLeadingActions?: ReactNode;
47
49
  }
48
50
 
49
51
  /** New/history controls reused by the tab strip and persistent assistant header. */
@@ -54,10 +56,17 @@ export interface ThreadActionsProps extends Omit<
54
56
  actions?: ReactNode;
55
57
  newIcon?: ReactNode;
56
58
  historyIcon?: ReactNode;
59
+ historyLeadingActions?: ReactNode;
57
60
  }
58
61
 
59
62
  /** Render New and History controls, plus optional host actions, as one group. */
60
- export const ThreadActions = ({ actions, newIcon, historyIcon, ...list }: ThreadActionsProps) => {
63
+ export const ThreadActions = ({
64
+ actions,
65
+ newIcon,
66
+ historyIcon,
67
+ historyLeadingActions,
68
+ ...list
69
+ }: ThreadActionsProps) => {
61
70
  const { onSelect, onNew } = list;
62
71
  const [historyOpen, setHistoryOpen] = useState(false);
63
72
  return (
@@ -79,6 +88,7 @@ export const ThreadActions = ({ actions, newIcon, historyIcon, ...list }: Thread
79
88
  <TooltipContent>New chat</TooltipContent>
80
89
  </Tooltip>
81
90
  )}
91
+ {historyLeadingActions}
82
92
  <Popover open={historyOpen} onOpenChange={setHistoryOpen}>
83
93
  <Tooltip>
84
94
  <TooltipTrigger asChild>
@@ -136,6 +146,7 @@ export const ThreadTabs = ({
136
146
  showActions = true,
137
147
  newIcon,
138
148
  historyIcon,
149
+ historyLeadingActions,
139
150
  className,
140
151
  ...list
141
152
  }: ThreadTabsProps) => {
@@ -207,7 +218,13 @@ export const ThreadTabs = ({
207
218
  })}
208
219
  </div>
209
220
  {showActions ? (
210
- <ThreadActions {...list} actions={actions} newIcon={newIcon} historyIcon={historyIcon} />
221
+ <ThreadActions
222
+ {...list}
223
+ actions={actions}
224
+ newIcon={newIcon}
225
+ historyIcon={historyIcon}
226
+ historyLeadingActions={historyLeadingActions}
227
+ />
211
228
  ) : null}
212
229
  </div>
213
230
  </div>
@@ -124,8 +124,8 @@ type MessageGroup = {
124
124
  * are resolved out-of-band via the chart cache; suggestions live at
125
125
  * message scope.
126
126
  *
127
- * Genie `thinking` and `text` entries render in the assistant bubble's
128
- * consolidated Thoughts panel and are intentionally omitted here.
127
+ * Genie `thinking` entries render as interleaved Thoughts disclosures in the
128
+ * assistant activity timeline and are intentionally omitted here.
129
129
  *
130
130
  * `groups` preserves first-seen order so the rendered detail view
131
131
  * walks Genie sub-calls in the order the LLM dispatched them.
@@ -190,7 +190,7 @@ const summarizeProgress = (progress: ToolProgress[]): ToolDetailSummary => {
190
190
  break;
191
191
  }
192
192
  case "thinking": {
193
- // Reasoning is rendered once in the assistant bubble's Thoughts panel.
193
+ // Reasoning is rendered once in the assistant activity timeline.
194
194
  break;
195
195
  }
196
196
  case "query": {
@@ -424,6 +424,7 @@ const RawToolPayload = ({
424
424
  );
425
425
  };
426
426
 
427
+ /** Compact source-code preview extracted from a tool request payload. */
427
428
  export type ToolInputPresentation = {
428
429
  label: "Code" | "Command" | "Output";
429
430
  language: "typescript" | "javascript" | "python" | "sql" | "json" | "bash" | "text";
@@ -511,6 +512,7 @@ const ToolInputSource = ({ presentation }: { presentation: ToolInputPresentation
511
512
 
512
513
  type ToolStatusProgress = Extract<ToolProgress, { type: "tool_status" }>;
513
514
 
515
+ /** One native web-search request and the result progress events associated with it. */
514
516
  export type WebSearchProgressGroup = {
515
517
  key: string;
516
518
  search?: ToolStatusProgress;
@@ -265,7 +265,7 @@ export type ChatViewProps = {
265
265
  /**
266
266
  * Where the conversation list renders (or `"disabled"` to hide it even when
267
267
  * {@link threads} and {@link onSelectThread} are wired). Defaults to
268
- * `"auto"`: a left side panel while the chat is wide, a tab strip once it
268
+ * `"auto"`: a right side panel while the chat is wide, a tab strip once it
269
269
  * is too narrow for one. See {@link ThreadPlacement}.
270
270
  */
271
271
  threadPlacement?: ThreadPlacement;
@@ -274,6 +274,10 @@ export type ChatViewProps = {
274
274
  * actions. The persistent assistant uses this for its panel Close button.
275
275
  */
276
276
  threadBarActions?: ReactNode;
277
+ /** Host controls rendered immediately before the top-row History action. */
278
+ threadHistoryLeadingActions?: ReactNode;
279
+ /** Host controls rendered in the docked or drawer conversation-list header. */
280
+ threadSidebarHeaderActions?: ReactNode;
277
281
  /** Whether the top tab row renders its built-in New/History controls. */
278
282
  showThreadBarActions?: boolean;
279
283
  /** Replacement icon for the top tab row's New action. */
@@ -0,0 +1,111 @@
1
+ import type { UIMessage } from "ai";
2
+ import { z } from "zod";
3
+
4
+ const ACTIVE_RUN_MAX_AGE_MS = 24 * 60 * 60 * 1_000;
5
+
6
+ export const StoredActiveRunSchema = z
7
+ .object({
8
+ threadId: z.string().min(1).describe("Mastra thread receiving the background turn."),
9
+ runId: z.string().min(1).describe("Mastra background run identifier."),
10
+ assistantId: z.string().min(1).describe("Client assistant message identifier."),
11
+ userMessage: z
12
+ .object({
13
+ id: z.string().min(1),
14
+ role: z.literal("user"),
15
+ parts: z.array(z.unknown()),
16
+ })
17
+ .passthrough()
18
+ .describe("Submitted user message restored before stream replay."),
19
+ startedAt: z.number().int().nonnegative().describe("Client epoch time when the run started."),
20
+ })
21
+ .strict()
22
+ .describe("Reload-recoverable Mastra background run metadata.");
23
+
24
+ /** Reload-recoverable metadata for one active Mastra background run. */
25
+ export type StoredActiveRun = Omit<z.infer<typeof StoredActiveRunSchema>, "userMessage"> & {
26
+ userMessage: UIMessage;
27
+ };
28
+
29
+ const StoredActiveRunFileSchema = z
30
+ .object({
31
+ version: z.literal(1).describe("Active run storage format version."),
32
+ runs: z.array(StoredActiveRunSchema).describe("Background runs awaiting terminal completion."),
33
+ })
34
+ .strict()
35
+ .describe("Browser-persisted Mastra background run registry.");
36
+
37
+ type StorageLike = Pick<Storage, "getItem" | "setItem" | "removeItem">;
38
+
39
+ /** Build the browser storage key for one Mastra agent's active runs. */
40
+ export const activeRunStorageKey = (basePath: string, agentId: string): string =>
41
+ `dbx-mastra-active-runs:${basePath}:${agentId}`;
42
+
43
+ function browserStorage(): StorageLike | undefined {
44
+ try {
45
+ return typeof window === "undefined" ? undefined : window.localStorage;
46
+ } catch {
47
+ return undefined;
48
+ }
49
+ }
50
+
51
+ /** Read current non-stale background runs, deleting malformed storage. */
52
+ export function readStoredActiveRuns(
53
+ key: string,
54
+ storage: StorageLike | undefined = browserStorage(),
55
+ now = Date.now(),
56
+ ): StoredActiveRun[] {
57
+ if (!storage) return [];
58
+ try {
59
+ const serialized = storage.getItem(key);
60
+ if (!serialized) return [];
61
+ const parsed = StoredActiveRunFileSchema.parse(JSON.parse(serialized));
62
+ const parsedRuns: StoredActiveRun[] = parsed.runs.map((run) => ({
63
+ ...run,
64
+ userMessage: run.userMessage as UIMessage,
65
+ }));
66
+ const runs = parsedRuns.filter((run) => now - run.startedAt <= ACTIVE_RUN_MAX_AGE_MS);
67
+ if (runs.length !== parsed.runs.length) writeStoredActiveRuns(key, runs, storage);
68
+ return runs as StoredActiveRun[];
69
+ } catch {
70
+ try {
71
+ storage.removeItem(key);
72
+ } catch {}
73
+ return [];
74
+ }
75
+ }
76
+
77
+ /** Add or replace one reload-recoverable background run. */
78
+ export function storeActiveRun(
79
+ key: string,
80
+ run: StoredActiveRun,
81
+ storage: StorageLike | undefined = browserStorage(),
82
+ ): void {
83
+ if (!storage) return;
84
+ const runs = readStoredActiveRuns(key, storage).filter(
85
+ (candidate) => candidate.runId !== run.runId,
86
+ );
87
+ writeStoredActiveRuns(key, [...runs, run], storage);
88
+ }
89
+
90
+ /** Remove one terminal or explicitly aborted background run. */
91
+ export function removeStoredActiveRun(
92
+ key: string,
93
+ runId: string,
94
+ storage: StorageLike | undefined = browserStorage(),
95
+ ): void {
96
+ if (!storage) return;
97
+ const runs = readStoredActiveRuns(key, storage).filter((run) => run.runId !== runId);
98
+ writeStoredActiveRuns(key, runs, storage);
99
+ }
100
+
101
+ function writeStoredActiveRuns(key: string, runs: StoredActiveRun[], storage: StorageLike): void {
102
+ try {
103
+ if (runs.length === 0) {
104
+ storage.removeItem(key);
105
+ return;
106
+ }
107
+ storage.setItem(key, JSON.stringify({ version: 1, runs }));
108
+ } catch {
109
+ // Browser persistence is best-effort; the live in-memory run remains valid.
110
+ }
111
+ }
@@ -2,6 +2,7 @@
2
2
  // Regenerated from the exporting modules in ./src.
3
3
  // Hand edits are overwritten on the next watch; this file is read-only.
4
4
 
5
+ export * as activeRuns from "./active-runs.ts";
5
6
  export * as chartOption from "./chart-option.ts";
6
7
  export * as chartTheme from "./chart-theme.ts";
7
8
  export * as clipboard from "./clipboard.ts";
@@ -14,10 +15,12 @@ export * as threadLabels from "./thread-labels.ts";
14
15
  export * as threadSessions from "./thread-sessions.ts";
15
16
  export * as threadTabs from "./thread-tabs.ts";
16
17
  export * as toolEvents from "./tool-events.ts";
18
+ export { StoredActiveRunSchema, activeRunStorageKey } from "./active-runs.ts";
19
+ export type { StoredActiveRun } from "./active-runs.ts";
17
20
  export { LIGHT_CHART_CHROME } from "./chart-theme.ts";
18
21
  export type { ChartChrome } from "./chart-theme.ts";
19
22
  export type { ExportFormat, ExportBrand, EmbedResolver, ExportChatOptions } from "./export.ts";
20
- export { MastraPluginClient, useMastraConfig, useMastraClient, useMastraMlflowExperiment, useMastraModels, useMastraDefaultModel, useMastraSuggestions, useMastraThreads, useChartFetch, useStatementFetch } from "./mastra-client.ts";
23
+ export { MastraChatRequestError, MastraPluginClient, useMastraConfig, useMastraClient, useMastraMlflowExperiment, useMastraModels, useMastraDefaultModel, useMastraSuggestions, useMastraThreads, useChartFetch, useStatementFetch } from "./mastra-client.ts";
21
24
  export type { MastraStreamResponse, MastraMemoryThread, ByIdFetchState } from "./mastra-client.ts";
22
25
  export { defaultReasoningEffort, modelStorageKey, readStoredModel, storeSelectedModel } from "./model-selection.ts";
23
26
  export type { MastraRequestContextInput, MastraRequestContextSource, MastraRequestContextSnapshot } from "./request-context.ts";