@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.
- package/README.md +114 -21
- package/index.ts +10 -9
- package/package.json +8 -8
- package/src/react/_composer-autosize.ts +41 -0
- package/src/react/bubbles.tsx +4 -1
- package/src/react/chat-approvals.ts +9 -4
- package/src/react/chat-composer.tsx +98 -79
- package/src/react/chat-feedback.ts +2 -2
- package/src/react/chat-history.ts +79 -10
- package/src/react/chat-stream.ts +29 -59
- package/src/react/chat-thread-layout.tsx +17 -1
- package/src/react/chat-view.tsx +10 -0
- package/src/react/export-menu.tsx +8 -1
- package/src/react/index.ts +18 -2
- package/src/react/mastra-assistant.tsx +509 -0
- package/src/react/mastra-chat.tsx +120 -72
- package/src/react/thread-sidebar.tsx +33 -43
- package/src/react/thread-tabs.tsx +141 -117
- package/src/react/tool-pill.tsx +24 -1
- package/src/react/types.ts +32 -22
- package/src/support/mastra-client.ts +203 -171
- package/src/support/request-context.ts +30 -0
- package/src/support/thread-sessions.ts +16 -38
- package/src/support/tool-events.ts +41 -0
- package/src/react/chat-stream-reducer.ts +0 -243
- package/src/support/mastra-stream.ts +0 -59
|
@@ -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
|
|
285
|
-
if (!
|
|
286
|
-
|
|
287
|
-
|
|
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
|
|
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-
|
|
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
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
-
<
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
<
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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
|
-
<
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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
|
|
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
|
|
134
|
+
messages,
|
|
65
135
|
historyLoaded: true,
|
|
66
|
-
hasMoreHistory: response.hasMore,
|
|
136
|
+
hasMoreHistory: response.hasMore ?? false,
|
|
67
137
|
historyPage: 1,
|
|
68
|
-
|
|
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
|
|
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) => {
|
package/src/react/chat-stream.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
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
|
package/src/react/chat-view.tsx
CHANGED
|
@@ -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
|
|
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
|
) : (
|
package/src/react/index.ts
CHANGED
|
@@ -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,
|