@dbx-tools/ui-mastra 0.6.217 → 0.6.219

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.
@@ -31,7 +31,8 @@ import {
31
31
  Trash2Icon,
32
32
  XIcon,
33
33
  } from "lucide-react";
34
- import { useCallback, useLayoutEffect, useRef, useState } from "react";
34
+ import { useCallback, useId, useLayoutEffect, useRef, useState } from "react";
35
+ import { autosizeComposerTextarea, observeComposerWidth } from "./_composer-autosize.ts";
35
36
  import { ExportMenu } from "./export-menu.tsx";
36
37
  import { SuggestionPills } from "./suggestion-pills.tsx";
37
38
  import type { ChatViewProps } from "./types.ts";
@@ -250,6 +251,7 @@ type ChatComposerProps = {
250
251
  onModelChange: ChatViewProps["onModelChange"];
251
252
  defaultModelName: ChatViewProps["defaultModelName"];
252
253
  composerActions: ChatViewProps["composerActions"];
254
+ composerLeadingActions: ChatViewProps["composerLeadingActions"];
253
255
  isLoadingHistory: NonNullable<ChatViewProps["isLoadingHistory"]>;
254
256
  onClear: ChatViewProps["onClear"];
255
257
  onExportConversation: ChatViewProps["onExportConversation"];
@@ -272,20 +274,30 @@ export const ChatComposer = ({
272
274
  onModelChange,
273
275
  defaultModelName,
274
276
  composerActions,
277
+ composerLeadingActions,
275
278
  isLoadingHistory,
276
279
  onClear,
277
280
  onExportConversation,
278
281
  onResumeFollow,
279
282
  }: ChatComposerProps) => {
280
283
  const [input, setInput] = useState("");
284
+ const inputId = useId();
285
+ const inputGroupRef = useRef<HTMLDivElement>(null);
281
286
  const textareaRef = useRef<HTMLTextAreaElement>(null);
282
287
 
288
+ const resizeTextarea = useCallback(() => {
289
+ autosizeComposerTextarea(inputGroupRef.current, textareaRef.current);
290
+ }, []);
291
+
292
+ useLayoutEffect(() => {
293
+ resizeTextarea();
294
+ }, [input, isLoadingHistory, resizeTextarea]);
295
+
283
296
  useLayoutEffect(() => {
284
- const element = textareaRef.current;
285
- if (!element) return;
286
- element.style.height = "auto";
287
- element.style.height = `${element.scrollHeight}px`;
288
- }, [input]);
297
+ const group = inputGroupRef.current;
298
+ if (!group) return;
299
+ return observeComposerWidth(group, resizeTextarea);
300
+ }, [resizeTextarea]);
289
301
 
290
302
  const isRunning = status === "submitted" || status === "streaming";
291
303
  const submit = () => {
@@ -307,12 +319,6 @@ export const ChatComposer = ({
307
319
  sensitivity: "base",
308
320
  }),
309
321
  );
310
- const showToolbar =
311
- showModelDisplay ||
312
- Boolean(composerActions) ||
313
- Boolean(onExportConversation) ||
314
- Boolean(onClear);
315
-
316
322
  return (
317
323
  <>
318
324
  {isEmpty && (
@@ -336,8 +342,13 @@ export const ChatComposer = ({
336
342
  onRemoveSteer={onRemoveSteer}
337
343
  onReorderSteers={onReorderSteers}
338
344
  />
339
- <InputGroup className="rounded-2xl border-border/80 shadow-sm transition-shadow focus-within:shadow-md">
345
+ <InputGroup
346
+ ref={inputGroupRef}
347
+ className="rounded-2xl border-border/80 shadow-sm transition-shadow focus-within:shadow-md"
348
+ >
340
349
  <InputGroupTextarea
350
+ id={`${inputId}-message`}
351
+ name="message"
341
352
  ref={textareaRef}
342
353
  value={input}
343
354
  onChange={(event) => setInput(event.target.value)}
@@ -350,76 +361,84 @@ export const ChatComposer = ({
350
361
  placeholder={isLoadingHistory ? "Loading history..." : "Send a message..."}
351
362
  rows={1}
352
363
  disabled={isLoadingHistory}
353
- className="max-h-48 text-base md:text-sm"
364
+ className="field-sizing-fixed max-h-64 w-full flex-none overflow-y-auto px-4 pb-2 pt-4 text-base md:text-sm"
354
365
  />
355
- <InputGroupAddon align="inline-end">
356
- {isRunning && onStop && !input.trim() ? (
357
- <InputGroupButton
358
- type="button"
359
- size="icon-sm"
360
- variant="default"
361
- onClick={() => onStop()}
362
- aria-label="Stop response"
363
- >
364
- <SquareIcon className="size-3 fill-current" />
365
- </InputGroupButton>
366
- ) : (
367
- <InputGroupButton
368
- type="submit"
369
- size="icon-sm"
370
- variant="default"
371
- disabled={!input.trim() || isLoadingHistory}
372
- aria-label={isRunning ? "Send now (interrupts)" : "Send message"}
373
- >
374
- <SendIcon className="size-3" />
375
- </InputGroupButton>
376
- )}
377
- </InputGroupAddon>
378
- </InputGroup>
379
- {showToolbar && (
380
- <div className="mt-2 flex items-center gap-2 text-xs text-muted-foreground">
381
- {showModelDisplay &&
382
- (modelChangeable ? (
383
- <Select
384
- value={model ? model : DEFAULT_MODEL_VALUE}
385
- onValueChange={(value) =>
386
- onModelChange?.(value === DEFAULT_MODEL_VALUE ? "" : value)
387
- }
366
+ <InputGroupAddon
367
+ align="block-end"
368
+ className="flex w-full flex-row flex-wrap items-center justify-between gap-2 px-3 pb-3 pt-0"
369
+ >
370
+ <div className="flex min-w-0 max-w-full flex-wrap items-center gap-2">
371
+ {composerLeadingActions}
372
+ {onExportConversation && (
373
+ <ExportMenu
374
+ onExport={(format) => void onExportConversation(format)}
375
+ tooltip="Export conversation"
388
376
  disabled={isLoadingHistory}
389
- >
390
- <SelectTrigger
391
- size="sm"
392
- className="h-7 w-auto max-w-[200px] gap-1 rounded-full px-2.5 text-xs [&_svg]:size-3"
377
+ />
378
+ )}
379
+ {onClear && (
380
+ <ClearConversationAction onClear={onClear} isLoadingHistory={isLoadingHistory} />
381
+ )}
382
+ </div>
383
+ <div className="ml-auto flex min-w-0 max-w-full items-center gap-1.5">
384
+ {composerActions}
385
+ {showModelDisplay &&
386
+ (modelChangeable ? (
387
+ <Select
388
+ name="mastra-model"
389
+ value={model ? model : DEFAULT_MODEL_VALUE}
390
+ onValueChange={(value) =>
391
+ onModelChange?.(value === DEFAULT_MODEL_VALUE ? "" : value)
392
+ }
393
+ disabled={isLoadingHistory}
393
394
  >
394
- <SelectValue placeholder={defaultOptionLabel} />
395
- </SelectTrigger>
396
- <SelectContent>
397
- <SelectItem value={DEFAULT_MODEL_VALUE}>{defaultOptionLabel}</SelectItem>
398
- {sortedModels.map((option) => (
399
- <SelectItem key={option.name} value={option.name}>
400
- {option.displayName || option.name}
401
- </SelectItem>
402
- ))}
403
- </SelectContent>
404
- </Select>
395
+ <SelectTrigger
396
+ size="sm"
397
+ aria-label="Model"
398
+ className="h-7 w-auto max-w-[180px] gap-1 truncate border-0 bg-transparent px-2 text-xs shadow-none hover:bg-accent [&_svg]:size-3"
399
+ >
400
+ <SelectValue placeholder={defaultOptionLabel} />
401
+ </SelectTrigger>
402
+ <SelectContent>
403
+ <SelectItem value={DEFAULT_MODEL_VALUE}>{defaultOptionLabel}</SelectItem>
404
+ {sortedModels.map((option) => (
405
+ <SelectItem key={option.name} value={option.name}>
406
+ {option.displayName || option.name}
407
+ </SelectItem>
408
+ ))}
409
+ </SelectContent>
410
+ </Select>
411
+ ) : (
412
+ <span className="max-w-[180px] truncate px-2 text-xs text-muted-foreground">
413
+ {currentModelLabel}
414
+ </span>
415
+ ))}
416
+ {isRunning && onStop && !input.trim() ? (
417
+ <InputGroupButton
418
+ type="button"
419
+ size="icon-sm"
420
+ variant="default"
421
+ onClick={() => onStop()}
422
+ aria-label="Stop response"
423
+ className="shrink-0 rounded-full"
424
+ >
425
+ <SquareIcon className="size-3 fill-current" />
426
+ </InputGroupButton>
405
427
  ) : (
406
- <span className="max-w-[200px] truncate px-2.5 text-xs text-muted-foreground">
407
- {currentModelLabel}
408
- </span>
409
- ))}
410
- {composerActions}
411
- {onExportConversation && (
412
- <ExportMenu
413
- onExport={(format) => void onExportConversation(format)}
414
- tooltip="Export conversation"
415
- disabled={isLoadingHistory}
416
- />
417
- )}
418
- {onClear && (
419
- <ClearConversationAction onClear={onClear} isLoadingHistory={isLoadingHistory} />
420
- )}
421
- </div>
422
- )}
428
+ <InputGroupButton
429
+ type="submit"
430
+ size="icon-sm"
431
+ variant="default"
432
+ disabled={!input.trim() || isLoadingHistory}
433
+ aria-label={isRunning ? "Send now (interrupts)" : "Send message"}
434
+ className="shrink-0 rounded-full"
435
+ >
436
+ <SendIcon className="size-3" />
437
+ </InputGroupButton>
438
+ )}
439
+ </div>
440
+ </InputGroupAddon>
441
+ </InputGroup>
423
442
  </form>
424
443
  </>
425
444
  );
@@ -1,9 +1,9 @@
1
1
  import { error as sharedError, log } from "@dbx-tools/shared-core";
2
2
  import type { UIMessage } from "ai";
3
3
  import { useCallback, useRef } from "react";
4
- import type { MastraPluginClient } from "../support/mastra-client.ts";
5
- import type { FeedbackSubmission, MessageFeedback } from "./types.ts";
6
4
  import type { ThreadSessionUpdater } from "./chat-sessions.ts";
5
+ import type { FeedbackSubmission, MessageFeedback } from "./types.ts";
6
+ import type { MastraPluginClient } from "../support/mastra-client.ts";
7
7
 
8
8
  const logger = log.logger("ui-mastra/chat");
9
9
 
@@ -1,32 +1,76 @@
1
1
  import { error as sharedError, log } from "@dbx-tools/shared-core";
2
+ import type { ListMemoryThreadMessagesResponse } from "@mastra/client-js";
2
3
  import type { UIMessage } from "ai";
3
4
  import { useCallback, useEffect, useRef, useState } from "react";
4
- import type { MastraPluginClient } from "../support/mastra-client.ts";
5
5
  import type {
6
6
  ThreadMessageWriter,
7
7
  ThreadSessionReader,
8
8
  ThreadSessionUpdater,
9
9
  } from "./chat-sessions.ts";
10
+ import type { PendingApproval } from "./types.ts";
11
+ import type { MastraPluginClient } from "../support/mastra-client.ts";
10
12
 
11
13
  const HISTORY_PAGE_SIZE = 20;
12
14
  const logger = log.logger("ui-mastra/chat");
13
15
 
14
16
  interface UseChatHistoryOptions {
15
17
  activeKey: string;
16
- activeThreadId: string | undefined;
18
+ activeThreadId: string;
17
19
  agentId: string;
18
20
  getSession: ThreadSessionReader;
21
+ availability: "loading" | "present" | "missing";
19
22
  mastraClient: MastraPluginClient;
20
23
  updateSession: ThreadSessionUpdater;
21
24
  writeMessages: ThreadMessageWriter;
22
25
  }
23
26
 
27
+ type SuspendedRuns = Awaited<ReturnType<MastraPluginClient["suspendedRuns"]>>["runs"];
28
+
29
+ /** Read a newest-first native memory page as chronological UI messages. */
30
+ function toChronologicalUiMessages(response: ListMemoryThreadMessagesResponse): UIMessage[] {
31
+ return [...(response.uiMessages ?? [])].reverse() as UIMessage[];
32
+ }
33
+
34
+ /** Recover actionable approval cards from Mastra's persisted suspended runs. */
35
+ function pendingApprovals(
36
+ messages: UIMessage[],
37
+ runs: SuspendedRuns,
38
+ ): Record<string, PendingApproval[]> {
39
+ const approvals: Record<string, PendingApproval[]> = {};
40
+ const assistantMessages = messages.filter((message) => message.role === "assistant");
41
+ for (const run of runs) {
42
+ for (const toolCall of run.toolCalls) {
43
+ if (!toolCall.requiresApproval || !toolCall.toolCallId || !toolCall.toolName) continue;
44
+ const owner =
45
+ [...assistantMessages]
46
+ .reverse()
47
+ .find((message) =>
48
+ message.parts.some(
49
+ (part) => (part as { toolCallId?: unknown }).toolCallId === toolCall.toolCallId,
50
+ ),
51
+ ) ?? assistantMessages.at(-1);
52
+ if (!owner) continue;
53
+ approvals[owner.id] = [
54
+ ...(approvals[owner.id] ?? []),
55
+ {
56
+ runId: run.runId,
57
+ toolCallId: toolCall.toolCallId,
58
+ toolName: toolCall.toolName,
59
+ input: toolCall.args,
60
+ },
61
+ ];
62
+ }
63
+ }
64
+ return approvals;
65
+ }
66
+
24
67
  /** Hydrate and page the active thread while other thread sessions keep running. */
25
68
  export function useChatHistory({
26
69
  activeKey,
27
70
  activeThreadId,
28
71
  agentId,
29
72
  getSession,
73
+ availability,
30
74
  mastraClient,
31
75
  updateSession,
32
76
  writeMessages,
@@ -44,6 +88,19 @@ export function useChatHistory({
44
88
  setIsLoadingHistory(false);
45
89
  return;
46
90
  }
91
+ if (availability === "loading") {
92
+ setIsLoadingHistory(true);
93
+ return;
94
+ }
95
+ if (availability === "missing") {
96
+ updateSession(threadId, (current) => ({
97
+ ...current,
98
+ historyLoaded: true,
99
+ hasMoreHistory: false,
100
+ }));
101
+ setIsLoadingHistory(false);
102
+ return;
103
+ }
47
104
 
48
105
  let cancelled = false;
49
106
  const controller = new AbortController();
@@ -57,16 +114,28 @@ export function useChatHistory({
57
114
  perPage: HISTORY_PAGE_SIZE,
58
115
  signal: controller.signal,
59
116
  })
60
- .then((response) => {
117
+ .then(async (response) => {
118
+ const suspended = await mastraClient
119
+ .suspendedRuns(agentId, activeThreadId, controller.signal)
120
+ .catch((error: unknown) => {
121
+ if ((error as { name?: string }).name === "AbortError") throw error;
122
+ logger.warn("suspended-run discovery error", {
123
+ error: sharedError.errorMessage(error),
124
+ });
125
+ return { runs: [], total: 0 };
126
+ });
127
+ return [response, suspended] as const;
128
+ })
129
+ .then(([response, suspended]) => {
61
130
  if (cancelled) return;
131
+ const messages = toChronologicalUiMessages(response);
62
132
  updateSession(threadId, (current) => ({
63
133
  ...current,
64
- messages: response.uiMessages as unknown as UIMessage[],
134
+ messages,
65
135
  historyLoaded: true,
66
- hasMoreHistory: response.hasMore,
136
+ hasMoreHistory: response.hasMore ?? false,
67
137
  historyPage: 1,
68
- toolEventsByMessage: {},
69
- pendingApprovalsByMessage: {},
138
+ pendingApprovalsByMessage: pendingApprovals(messages, suspended.runs),
70
139
  feedbackByMessage: {},
71
140
  }));
72
141
  })
@@ -89,7 +158,7 @@ export function useChatHistory({
89
158
  cancelled = true;
90
159
  controller.abort();
91
160
  };
92
- }, [activeKey, activeThreadId, agentId, getSession, mastraClient, updateSession]);
161
+ }, [activeKey, activeThreadId, agentId, availability, getSession, mastraClient, updateSession]);
93
162
 
94
163
  const loadOlderHistory = useCallback(() => {
95
164
  const threadId = activeKey;
@@ -102,13 +171,13 @@ export function useChatHistory({
102
171
  mastraClient
103
172
  .history({ agentId, threadId: activeThreadId, page, perPage: HISTORY_PAGE_SIZE })
104
173
  .then((response) => {
105
- const uiMessages = response.uiMessages as unknown as UIMessage[];
174
+ const uiMessages = toChronologicalUiMessages(response);
106
175
  if (uiMessages.length > 0) {
107
176
  writeMessages(threadId, [...uiMessages, ...getSession(threadId).messages]);
108
177
  }
109
178
  updateSession(threadId, (current) => ({
110
179
  ...current,
111
- hasMoreHistory: response.hasMore,
180
+ hasMoreHistory: response.hasMore ?? false,
112
181
  }));
113
182
  })
114
183
  .catch((error: unknown) => {
@@ -1,16 +1,12 @@
1
1
  import { feedback } from "@dbx-tools/shared-mastra";
2
+ import { readUIMessageStream, type UIMessage } from "ai";
2
3
  import { useCallback } from "react";
3
- import type { MastraStreamResponse } from "../support/mastra-stream.ts";
4
4
  import type {
5
5
  ThreadMessageWriter,
6
6
  ThreadSessionReader,
7
7
  ThreadSessionUpdater,
8
8
  } from "./chat-sessions.ts";
9
- import {
10
- chatStreamAssistantMessage,
11
- createChatStreamState,
12
- reduceChatStreamChunk,
13
- } from "./chat-stream-reducer.ts";
9
+ import type { MastraStreamResponse } from "../support/mastra-client.ts";
14
10
 
15
11
  /** Read the MLflow trace id captured by the server on a stream response. */
16
12
  const readMlflowTraceId = (stream: unknown): string | undefined => {
@@ -20,13 +16,22 @@ const readMlflowTraceId = (stream: unknown): string | undefined => {
20
16
 
21
17
  class StreamAborted extends Error {}
22
18
 
19
+ /** Replace or append one assistant message without disturbing the transcript. */
20
+ function upsertAssistant(messages: UIMessage[], assistant: UIMessage): UIMessage[] {
21
+ const next = [...messages];
22
+ const index = next.findIndex((message) => message.id === assistant.id);
23
+ if (index === -1) next.push(assistant);
24
+ else next[index] = assistant;
25
+ return next;
26
+ }
27
+
23
28
  interface UseChatStreamOptions {
24
29
  getSession: ThreadSessionReader;
25
30
  updateSession: ThreadSessionUpdater;
26
31
  writeMessages: ThreadMessageWriter;
27
32
  }
28
33
 
29
- /** Translate one validated Mastra data stream into thread messages and tool state. */
34
+ /** Project one Mastra stream through the native AI SDK UI message converter. */
30
35
  export function useChatStream({ getSession, updateSession, writeMessages }: UseChatStreamOptions) {
31
36
  return useCallback(
32
37
  async (
@@ -54,59 +59,24 @@ export function useChatStream({ getSession, updateSession, writeMessages }: UseC
54
59
  );
55
60
  }
56
61
 
57
- const session = getSession(threadId);
58
- const existing = session.messages.find((message) => message.id === assistantId);
59
- let state = createChatStreamState({
60
- ...(existing ? { existing } : {}),
61
- pendingApprovals: session.pendingApprovalsByMessage[assistantId] ?? [],
62
- runId: runIdRef.current,
63
- toolEvents: session.toolEventsByMessage[assistantId] ?? [],
64
- });
65
-
66
- const upsertAssistant = () => {
67
- const next = [...getSession(threadId).messages];
68
- const index = next.findIndex((message) => message.id === assistantId);
69
- const message = chatStreamAssistantMessage(assistantId, state);
70
- if (index === -1) next.push(message);
71
- else next[index] = message;
72
- writeMessages(threadId, next);
73
- };
74
-
75
62
  try {
76
- await stream.processDataStream({
77
- onChunk: async (chunk) => {
78
- if (signal.aborted) throw new StreamAborted();
79
- const next = reduceChatStreamChunk(state, chunk);
80
- state = next.state;
81
- runIdRef.current = state.runId;
82
-
83
- if (Object.values(next.changes).some(Boolean)) {
84
- updateSession(threadId, (current) => ({
85
- ...current,
86
- ...(next.changes.runId ? { runId: state.runId } : {}),
87
- ...(next.changes.status ? { status: "streaming" as const } : {}),
88
- ...(next.changes.toolEvents
89
- ? {
90
- toolEventsByMessage: {
91
- ...current.toolEventsByMessage,
92
- [assistantId]: state.toolEvents,
93
- },
94
- }
95
- : {}),
96
- ...(next.changes.pendingApprovals
97
- ? {
98
- pendingApprovalsByMessage: {
99
- ...current.pendingApprovalsByMessage,
100
- [assistantId]: state.pendingApprovals,
101
- },
102
- }
103
- : {}),
104
- }));
105
- }
106
- if (next.assistantChanged) upsertAssistant();
107
- if (next.error) throw new Error(next.error);
108
- },
109
- });
63
+ const existing = getSession(threadId).messages.find(
64
+ (message) => message.id === assistantId,
65
+ );
66
+ for await (const message of readUIMessageStream({
67
+ ...(existing ? { message: existing } : {}),
68
+ stream: stream.stream,
69
+ terminateOnError: true,
70
+ })) {
71
+ if (signal.aborted) throw new StreamAborted();
72
+ const assistant = message.id === assistantId ? message : { ...message, id: assistantId };
73
+ writeMessages(threadId, upsertAssistant(getSession(threadId).messages, assistant));
74
+ updateSession(threadId, (current) => ({
75
+ ...current,
76
+ runId: runIdRef.current,
77
+ status: "streaming",
78
+ }));
79
+ }
110
80
  } catch (error) {
111
81
  if (error instanceof StreamAborted || signal.aborted) return;
112
82
  throw error;
@@ -37,6 +37,10 @@ type ChatThreadLayoutProps = {
37
37
  className: ChatViewProps["className"];
38
38
  threads: ChatViewProps["threads"];
39
39
  threadPlacement: NonNullable<ChatViewProps["threadPlacement"]>;
40
+ threadBarActions: ChatViewProps["threadBarActions"];
41
+ showThreadBarActions: NonNullable<ChatViewProps["showThreadBarActions"]>;
42
+ threadNewIcon: ChatViewProps["threadNewIcon"];
43
+ threadHistoryIcon: ChatViewProps["threadHistoryIcon"];
40
44
  activeThreadId: ChatViewProps["activeThreadId"];
41
45
  streamingThreadIds: NonNullable<ChatViewProps["streamingThreadIds"]>;
42
46
  isLoadingThreads: NonNullable<ChatViewProps["isLoadingThreads"]>;
@@ -87,6 +91,10 @@ export const ChatThreadLayout = ({
87
91
  className,
88
92
  threads,
89
93
  threadPlacement,
94
+ threadBarActions,
95
+ showThreadBarActions,
96
+ threadNewIcon,
97
+ threadHistoryIcon,
90
98
  activeThreadId,
91
99
  streamingThreadIds,
92
100
  isLoadingThreads,
@@ -179,7 +187,15 @@ export const ChatThreadLayout = ({
179
187
  ))}
180
188
  <div className="flex h-full min-w-0 flex-1 flex-col">
181
189
  {tabbedThreads && (
182
- <ThreadTabs {...threadListProps} openThreadIds={openTabIds} onCloseTab={closeTab} />
190
+ <ThreadTabs
191
+ {...threadListProps}
192
+ openThreadIds={openTabIds}
193
+ onCloseTab={closeTab}
194
+ actions={threadBarActions}
195
+ showActions={showThreadBarActions}
196
+ newIcon={threadNewIcon}
197
+ historyIcon={threadHistoryIcon}
198
+ />
183
199
  )}
184
200
  {showSidebarToggle && (
185
201
  <div
@@ -29,6 +29,7 @@ export const ChatView = ({
29
29
  onModelChange,
30
30
  defaultModelName,
31
31
  composerActions,
32
+ composerLeadingActions,
32
33
  onLoadMore,
33
34
  isLoadingMore = false,
34
35
  hasMore = false,
@@ -38,6 +39,10 @@ export const ChatView = ({
38
39
  onClear,
39
40
  threads,
40
41
  threadPlacement = "auto",
42
+ threadBarActions,
43
+ showThreadBarActions = true,
44
+ threadNewIcon,
45
+ threadHistoryIcon,
41
46
  activeThreadId,
42
47
  streamingThreadIds = [],
43
48
  isLoadingThreads = false,
@@ -67,6 +72,10 @@ export const ChatView = ({
67
72
  className={className}
68
73
  threads={threads}
69
74
  threadPlacement={threadPlacement}
75
+ threadBarActions={threadBarActions}
76
+ showThreadBarActions={showThreadBarActions}
77
+ threadNewIcon={threadNewIcon}
78
+ threadHistoryIcon={threadHistoryIcon}
70
79
  activeThreadId={activeThreadId}
71
80
  streamingThreadIds={streamingThreadIds}
72
81
  isLoadingThreads={isLoadingThreads}
@@ -110,6 +119,7 @@ export const ChatView = ({
110
119
  onModelChange={onModelChange}
111
120
  defaultModelName={defaultModelName}
112
121
  composerActions={composerActions}
122
+ composerLeadingActions={composerLeadingActions}
113
123
  isLoadingHistory={isLoadingHistory}
114
124
  onClear={onClear}
115
125
  onExportConversation={onExportConversation}
@@ -45,7 +45,14 @@ export const ExportMenu = ({
45
45
  disabled?: boolean;
46
46
  }) => {
47
47
  const trigger = iconOnly ? (
48
- <Button type="button" size="icon" variant="ghost" className="size-7" disabled={disabled}>
48
+ <Button
49
+ type="button"
50
+ size="icon"
51
+ variant="ghost"
52
+ className="size-7"
53
+ disabled={disabled}
54
+ aria-label={tooltip}
55
+ >
49
56
  <DownloadIcon className="size-3" />
50
57
  </Button>
51
58
  ) : (
@@ -2,6 +2,8 @@
2
2
  //
3
3
  // - `MastraChat` / `useMastraChat`: the self-contained drop-in (and its
4
4
  // headless driver) that wire themselves from the Mastra plugin config.
5
+ // - `MastraAssistant` / `useMastraAssistant`: the persistent app shell and
6
+ // controller for docked/overlay chat launched from anywhere below it.
5
7
  // - `ChatView`: the controlled, presentational shell for callers that
6
8
  // own message state and transport themselves.
7
9
  // - The Mastra plugin client + hooks (model catalogue, history paging,
@@ -27,13 +29,27 @@ export {
27
29
  export type { ByIdFetchState } from "../support/mastra-client.ts";
28
30
  export { ChatView } from "./chat-view.tsx";
29
31
  export { ExportMenu } from "./export-menu.tsx";
32
+ export { MastraAssistant, useMastraAssistant } from "./mastra-assistant.tsx";
33
+ export type {
34
+ MastraAssistantController,
35
+ MastraAssistantLauncherOptions,
36
+ MastraAssistantLauncherPosition,
37
+ MastraAssistantMode,
38
+ MastraAssistantProps,
39
+ MastraAssistantResizeOptions,
40
+ MastraAssistantSide,
41
+ } from "./mastra-assistant.tsx";
42
+ export type {
43
+ MastraRequestContextInput,
44
+ MastraRequestContextSource,
45
+ } from "../support/request-context.ts";
30
46
  export { dedupeSuggestions } from "./suggestions.ts";
31
47
  export { MastraChat, useMastraChat } from "./mastra-chat.tsx";
32
48
  export type { MastraChatProps, UseMastraChatOptions } from "./mastra-chat.tsx";
33
49
  export { ThreadSidebar } from "./thread-sidebar.tsx";
34
50
  export type { ThreadSidebarProps } from "./thread-sidebar.tsx";
35
- export { ThreadTabs } from "./thread-tabs.tsx";
36
- export type { ThreadTabsProps } from "./thread-tabs.tsx";
51
+ export { ThreadActions, ThreadTabs } from "./thread-tabs.tsx";
52
+ export type { ThreadActionsProps, ThreadTabsProps } from "./thread-tabs.tsx";
37
53
  export type {
38
54
  ApprovalDecision,
39
55
  ChatModelOption,