@dbx-tools/ui-mastra 0.6.218 → 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/bubbles.tsx +4 -1
- package/src/react/chat-approvals.ts +9 -4
- package/src/react/chat-composer.tsx +80 -73
- 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
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import { error as sharedError, hash, log } from "@dbx-tools/shared-core";
|
|
2
|
-
import type { MastraThread } from "@dbx-tools/shared-mastra";
|
|
3
2
|
import { useBrand } from "@dbx-tools/ui-branding/react";
|
|
4
3
|
import type { UIMessage } from "ai";
|
|
5
4
|
import { type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
6
5
|
import { useChatApprovals } from "./chat-approvals.ts";
|
|
7
|
-
import { ChatView } from "./chat-view.tsx";
|
|
8
6
|
import { useChatFeedback } from "./chat-feedback.ts";
|
|
9
7
|
import { useChatHistory } from "./chat-history.ts";
|
|
10
8
|
import { useChatSessions } from "./chat-sessions.ts";
|
|
11
9
|
import { useChatStream } from "./chat-stream.ts";
|
|
10
|
+
import { ChatView } from "./chat-view.tsx";
|
|
12
11
|
import { dedupeSuggestions } from "./suggestions.ts";
|
|
13
12
|
import type { ChatViewProps, ThreadPlacement, ThreadSummary } from "./types.ts";
|
|
14
13
|
import type { EmbedResolver, ExportFormat } from "../support/export.ts";
|
|
@@ -18,21 +17,25 @@ import {
|
|
|
18
17
|
useMastraModels,
|
|
19
18
|
useMastraSuggestions,
|
|
20
19
|
useMastraThreads,
|
|
20
|
+
type MastraMemoryThread,
|
|
21
|
+
type MastraStreamResponse,
|
|
21
22
|
} from "../support/mastra-client.ts";
|
|
22
|
-
import type { MastraStreamResponse } from "../support/mastra-stream.ts";
|
|
23
23
|
import {
|
|
24
24
|
modelStorageKey,
|
|
25
25
|
readStoredModel,
|
|
26
26
|
storeSelectedModel,
|
|
27
27
|
} from "../support/model-selection.ts";
|
|
28
|
+
import {
|
|
29
|
+
snapshotRequestContext,
|
|
30
|
+
type MastraRequestContextSnapshot,
|
|
31
|
+
type MastraRequestContextSource,
|
|
32
|
+
} from "../support/request-context.ts";
|
|
28
33
|
import {
|
|
29
34
|
DEFAULT_THREAD_SESSION_KEY,
|
|
30
35
|
enqueueSteer,
|
|
31
36
|
isSessionRunning,
|
|
32
37
|
removeSteer as removeSteerFromQueue,
|
|
33
38
|
reorderSteers as reorderSteerQueue,
|
|
34
|
-
terminateRunningToolEvents,
|
|
35
|
-
type ThreadSession,
|
|
36
39
|
} from "../support/thread-sessions.ts";
|
|
37
40
|
|
|
38
41
|
const _loadChatExport = () => import("../support/export.ts");
|
|
@@ -40,21 +43,20 @@ const _loadChatExport = () => import("../support/export.ts");
|
|
|
40
43
|
// Self-contained drop-in chat. `useMastraChat` drives the conversation
|
|
41
44
|
// over `@mastra/client-js`: `agent.stream()` returns a Response
|
|
42
45
|
// augmented with `processDataStream()`, which pushes typed Mastra
|
|
43
|
-
// chunks
|
|
44
|
-
//
|
|
46
|
+
// chunks that `@mastra/ai-sdk` projects into native AI SDK `UIMessage`
|
|
47
|
+
// parts for `ChatView` to render.
|
|
45
48
|
//
|
|
46
49
|
// Approval gates ride the same channel: a paused `requireApproval: true`
|
|
47
50
|
// tool call emits a `tool-call-approval` chunk carrying
|
|
48
|
-
// `{ runId, payload: { toolCallId, toolName, args } }`.
|
|
49
|
-
//
|
|
51
|
+
// `{ runId, payload: { toolCallId, toolName, args } }`. The native stream
|
|
52
|
+
// converter preserves that data part and wires
|
|
50
53
|
// `onResolveToolApproval` to {@link MastraPluginClient.approveToolCallStream}
|
|
51
|
-
// / `declineToolCallStream
|
|
52
|
-
//
|
|
53
|
-
// fresh stream Response we run through the same chunk handler.
|
|
54
|
+
// / `declineToolCallStream`. Both use the native per-request Mastra client and
|
|
55
|
+
// return a fresh native stream Response we run through the same chunk handler.
|
|
54
56
|
//
|
|
55
57
|
// On mount the transcript hydrates with the most recent page of thread
|
|
56
|
-
// history from
|
|
57
|
-
//
|
|
58
|
+
// history from Mastra's resource-scoped memory endpoint; scrolling near the
|
|
59
|
+
// top lazy-loads and prepends the next older page, with `ChatView`
|
|
58
60
|
// preserving the visual scroll position across the prepend.
|
|
59
61
|
|
|
60
62
|
const logger = log.logger("ui-mastra/chat");
|
|
@@ -65,11 +67,11 @@ const makeUserMessage = (text: string): UIMessage => ({
|
|
|
65
67
|
parts: [{ type: "text", text }],
|
|
66
68
|
});
|
|
67
69
|
|
|
68
|
-
/** Project a
|
|
69
|
-
const toThreadSummary = (thread:
|
|
70
|
+
/** Project a native Mastra memory thread down to the sidebar's view. */
|
|
71
|
+
const toThreadSummary = (thread: MastraMemoryThread): ThreadSummary => ({
|
|
70
72
|
id: thread.id,
|
|
71
73
|
...(thread.title ? { title: thread.title } : {}),
|
|
72
|
-
updatedAt: thread.updatedAt,
|
|
74
|
+
updatedAt: new Date(thread.updatedAt).toISOString(),
|
|
73
75
|
});
|
|
74
76
|
|
|
75
77
|
/** Max characters of the first user message used as a provisional thread title. */
|
|
@@ -142,7 +144,9 @@ const storeStoredSidebarOpen = (key: string, open: boolean): void => {
|
|
|
142
144
|
};
|
|
143
145
|
|
|
144
146
|
/** Options for {@link useMastraChat}. */
|
|
145
|
-
export interface UseMastraChatOptions
|
|
147
|
+
export interface UseMastraChatOptions<
|
|
148
|
+
TValues extends Record<string, unknown> = Record<string, unknown>,
|
|
149
|
+
> {
|
|
146
150
|
/**
|
|
147
151
|
* Agent to converse with. Defaults to the Mastra plugin's registered
|
|
148
152
|
* default agent (`clientConfig().defaultAgent`).
|
|
@@ -206,6 +210,11 @@ export interface UseMastraChatOptions {
|
|
|
206
210
|
* without MLflow tracing is a safe no-op.
|
|
207
211
|
*/
|
|
208
212
|
enableFeedback?: boolean;
|
|
213
|
+
/**
|
|
214
|
+
* Typed application metadata forwarded through Mastra's native
|
|
215
|
+
* `requestContext` field. A resolver is read when each turn is submitted.
|
|
216
|
+
*/
|
|
217
|
+
requestContext?: MastraRequestContextSource<TValues>;
|
|
209
218
|
}
|
|
210
219
|
|
|
211
220
|
/**
|
|
@@ -216,8 +225,8 @@ export interface UseMastraChatOptions {
|
|
|
216
225
|
* consumes. Use this when you want the drop-in behaviour but need to
|
|
217
226
|
* render the view yourself; otherwise reach for {@link MastraChat}.
|
|
218
227
|
*/
|
|
219
|
-
export const useMastraChat = (
|
|
220
|
-
options: UseMastraChatOptions = {},
|
|
228
|
+
export const useMastraChat = <TValues extends Record<string, unknown> = Record<string, unknown>>(
|
|
229
|
+
options: UseMastraChatOptions<TValues> = {},
|
|
221
230
|
): Omit<ChatViewProps, "className"> => {
|
|
222
231
|
// One client drives both the agent stream and the plugin's custom
|
|
223
232
|
// routes (history / threads / models / suggestions / feedback /
|
|
@@ -247,6 +256,8 @@ export const useMastraChat = (
|
|
|
247
256
|
// `model` to its dependency list.
|
|
248
257
|
const modelRef = useRef(model);
|
|
249
258
|
modelRef.current = model;
|
|
259
|
+
const requestContextRef = useRef(options.requestContext);
|
|
260
|
+
requestContextRef.current = options.requestContext;
|
|
250
261
|
// Built-in conversation management. When on, the chat always drives an
|
|
251
262
|
// explicit client thread id (rather than leaning on the per-session
|
|
252
263
|
// cookie) so it can reference, persist, and switch between the
|
|
@@ -266,8 +277,8 @@ export const useMastraChat = (
|
|
|
266
277
|
const enableFeedback = options.enableFeedback ?? enableExport;
|
|
267
278
|
const feedbackAvailable = enableFeedback && mastraClient.feedbackEnabled;
|
|
268
279
|
const threadKey = threadStorageKey(mastraClient.basePath, agentId);
|
|
269
|
-
const [activeThreadId, setActiveThreadId] = useState<string
|
|
270
|
-
enableThreads ? (readStoredThreadId(threadKey) ?? hash.id()) :
|
|
280
|
+
const [activeThreadId, setActiveThreadId] = useState<string>(() =>
|
|
281
|
+
enableThreads ? (readStoredThreadId(threadKey) ?? hash.id()) : hash.id(),
|
|
271
282
|
);
|
|
272
283
|
const {
|
|
273
284
|
threads,
|
|
@@ -371,10 +382,16 @@ export const useMastraChat = (
|
|
|
371
382
|
updateSession,
|
|
372
383
|
writeMessages,
|
|
373
384
|
} = useChatSessions(activeThreadId);
|
|
385
|
+
const historyAvailability = isLoadingThreads
|
|
386
|
+
? "loading"
|
|
387
|
+
: threads.some((thread) => thread.id === activeThreadId) || optimisticThreads[activeThreadId]
|
|
388
|
+
? "present"
|
|
389
|
+
: "missing";
|
|
374
390
|
const { isLoadingHistory, isLoadingMore, loadOlderHistory } = useChatHistory({
|
|
375
391
|
activeKey,
|
|
376
392
|
activeThreadId,
|
|
377
393
|
agentId,
|
|
394
|
+
availability: historyAvailability,
|
|
378
395
|
getSession,
|
|
379
396
|
mastraClient,
|
|
380
397
|
updateSession,
|
|
@@ -412,9 +429,6 @@ export const useMastraChat = (
|
|
|
412
429
|
error: null,
|
|
413
430
|
status: "submitted",
|
|
414
431
|
runToken: token,
|
|
415
|
-
// Superseding an in-flight run (a steer that interrupts, or a rapid
|
|
416
|
-
// re-send) stops its stream, so close any pills it left running.
|
|
417
|
-
toolEventsByMessage: terminateRunningToolEvents(session.toolEventsByMessage),
|
|
418
432
|
};
|
|
419
433
|
});
|
|
420
434
|
const runIdRef = { current: getSession(threadId).runId };
|
|
@@ -454,13 +468,15 @@ export const useMastraChat = (
|
|
|
454
468
|
);
|
|
455
469
|
|
|
456
470
|
const runStream = useCallback(
|
|
457
|
-
(threadId: string,
|
|
471
|
+
(threadId: string, message: UIMessage, requestContext?: MastraRequestContextSnapshot) => {
|
|
458
472
|
const assistantId = hash.id();
|
|
459
473
|
const runId = hash.id();
|
|
460
474
|
updateSession(threadId, (session) => ({
|
|
461
475
|
...session,
|
|
462
476
|
assistantId,
|
|
463
477
|
runId,
|
|
478
|
+
runRequestContext: requestContext,
|
|
479
|
+
lastRequestContext: requestContext,
|
|
464
480
|
}));
|
|
465
481
|
// Capture this run's thread + model at send time and pass them per
|
|
466
482
|
// call, so a run keeps its own routing even if the user switches
|
|
@@ -468,17 +484,13 @@ export const useMastraChat = (
|
|
|
468
484
|
const streamThreadId = threadId === DEFAULT_THREAD_SESSION_KEY ? undefined : threadId;
|
|
469
485
|
const model = modelRef.current || undefined;
|
|
470
486
|
return driveStream(threadId, assistantId, (signal) => {
|
|
471
|
-
const messages = history.flatMap((m) =>
|
|
472
|
-
m.parts
|
|
473
|
-
.filter((p): p is { type: "text"; text: string } => p.type === "text")
|
|
474
|
-
.map((p) => ({ role: m.role, content: p.text })),
|
|
475
|
-
);
|
|
476
487
|
return mastraClient.streamAgent({
|
|
477
488
|
agentId,
|
|
478
|
-
messages,
|
|
489
|
+
messages: [message],
|
|
479
490
|
runId,
|
|
480
491
|
threadId: streamThreadId,
|
|
481
492
|
model,
|
|
493
|
+
requestContext,
|
|
482
494
|
signal,
|
|
483
495
|
});
|
|
484
496
|
});
|
|
@@ -498,8 +510,8 @@ export const useMastraChat = (
|
|
|
498
510
|
...session,
|
|
499
511
|
queuedSteers: removeSteerFromQueue(session.queuedSteers, head.id),
|
|
500
512
|
}));
|
|
501
|
-
const {
|
|
502
|
-
void runStream(threadId,
|
|
513
|
+
const { message } = appendUserMessage(threadId, head.text);
|
|
514
|
+
void runStream(threadId, message, head.requestContext);
|
|
503
515
|
};
|
|
504
516
|
|
|
505
517
|
// Cancel a thread's in-flight run. Defaults to the active thread (the
|
|
@@ -519,9 +531,6 @@ export const useMastraChat = (
|
|
|
519
531
|
runToken: session.runToken + 1,
|
|
520
532
|
error: null,
|
|
521
533
|
status: "ready",
|
|
522
|
-
// Cancelling stops the stream, so close any tool pills still marked
|
|
523
|
-
// running - the closing chunks will never arrive.
|
|
524
|
-
toolEventsByMessage: terminateRunningToolEvents(session.toolEventsByMessage),
|
|
525
534
|
};
|
|
526
535
|
});
|
|
527
536
|
},
|
|
@@ -537,13 +546,10 @@ export const useMastraChat = (
|
|
|
537
546
|
updateSession,
|
|
538
547
|
});
|
|
539
548
|
|
|
540
|
-
// Append a user message to a thread's transcript,
|
|
541
|
-
//
|
|
542
|
-
// the pre-append session (so callers can see whether a run was in flight)
|
|
543
|
-
// and the new message list. Shared by send + interrupt.
|
|
549
|
+
// Append a user message to a thread's transcript, update thread activity,
|
|
550
|
+
// and derive a provisional title for a brand-new thread.
|
|
544
551
|
const appendUserMessage = useCallback(
|
|
545
|
-
(threadId: string, text: string): {
|
|
546
|
-
updateSession(threadId, (session) => ({ ...session, lastUserText: text }));
|
|
552
|
+
(threadId: string, text: string): { message: UIMessage; next: UIMessage[] } => {
|
|
547
553
|
if (activeThreadId) {
|
|
548
554
|
noteThreadActivity(activeThreadId);
|
|
549
555
|
if (getSession(threadId).messages.length === 0) {
|
|
@@ -555,10 +561,11 @@ export const useMastraChat = (
|
|
|
555
561
|
}
|
|
556
562
|
}
|
|
557
563
|
}
|
|
558
|
-
const
|
|
559
|
-
const
|
|
564
|
+
const session = getSession(threadId);
|
|
565
|
+
const message = makeUserMessage(text);
|
|
566
|
+
const next = [...session.messages, message];
|
|
560
567
|
writeMessages(threadId, next);
|
|
561
|
-
return {
|
|
568
|
+
return { message, next };
|
|
562
569
|
},
|
|
563
570
|
[activeThreadId, getSession, noteThreadActivity, updateSession, writeMessages],
|
|
564
571
|
);
|
|
@@ -572,15 +579,20 @@ export const useMastraChat = (
|
|
|
572
579
|
const text = message.text ?? "";
|
|
573
580
|
if (!text) return;
|
|
574
581
|
const threadId = activeKey;
|
|
582
|
+
const requestContext = snapshotRequestContext(requestContextRef.current);
|
|
575
583
|
if (isSessionRunning(getSession(threadId))) {
|
|
576
584
|
updateSession(threadId, (session) => ({
|
|
577
585
|
...session,
|
|
578
|
-
queuedSteers: enqueueSteer(session.queuedSteers, {
|
|
586
|
+
queuedSteers: enqueueSteer(session.queuedSteers, {
|
|
587
|
+
id: hash.id(),
|
|
588
|
+
text,
|
|
589
|
+
...(requestContext ? { requestContext } : {}),
|
|
590
|
+
}),
|
|
579
591
|
}));
|
|
580
592
|
return;
|
|
581
593
|
}
|
|
582
|
-
const {
|
|
583
|
-
void runStream(threadId,
|
|
594
|
+
const { message: userMessage } = appendUserMessage(threadId, text);
|
|
595
|
+
void runStream(threadId, userMessage, requestContext);
|
|
584
596
|
},
|
|
585
597
|
[appendUserMessage, runStream, activeKey, getSession, updateSession],
|
|
586
598
|
);
|
|
@@ -599,8 +611,8 @@ export const useMastraChat = (
|
|
|
599
611
|
queuedSteers: removeSteerFromQueue(session.queuedSteers, steerId),
|
|
600
612
|
}));
|
|
601
613
|
logger.info("steer:send-now", { threadId });
|
|
602
|
-
const {
|
|
603
|
-
void runStream(threadId,
|
|
614
|
+
const { message } = appendUserMessage(threadId, steer.text);
|
|
615
|
+
void runStream(threadId, message, steer.requestContext);
|
|
604
616
|
},
|
|
605
617
|
[activeKey, appendUserMessage, getSession, runStream, updateSession],
|
|
606
618
|
);
|
|
@@ -630,17 +642,14 @@ export const useMastraChat = (
|
|
|
630
642
|
|
|
631
643
|
/**
|
|
632
644
|
* Wipe the current thread on the server and reset every piece of
|
|
633
|
-
* client-side state that mirrored it. The
|
|
634
|
-
*
|
|
635
|
-
* turn opens against the same (now empty) thread - no reload
|
|
636
|
-
* needed. Suspended approval cards belong to the cleared turns
|
|
637
|
-
* and would be unresolvable anyway, so we drop them too.
|
|
645
|
+
* client-side state that mirrored it. The next turn recreates the
|
|
646
|
+
* same client-owned thread id without a reload.
|
|
638
647
|
*/
|
|
639
648
|
const handleClear = useCallback(async () => {
|
|
640
649
|
const threadId = activeKey;
|
|
641
650
|
try {
|
|
642
|
-
|
|
643
|
-
logger.info("history cleared", {
|
|
651
|
+
await mastraClient.clearHistory({ agentId, threadId: activeThreadId });
|
|
652
|
+
logger.info("history cleared", { threadId });
|
|
644
653
|
} catch (error) {
|
|
645
654
|
logger.error("history clear error", {
|
|
646
655
|
error: sharedError.errorMessage(error),
|
|
@@ -679,8 +688,8 @@ export const useMastraChat = (
|
|
|
679
688
|
const deleteThread = useCallback(
|
|
680
689
|
async (threadId: string) => {
|
|
681
690
|
try {
|
|
682
|
-
|
|
683
|
-
logger.info("thread deleted", { threadId
|
|
691
|
+
await mastraClient.removeThread(threadId, { agentId });
|
|
692
|
+
logger.info("thread deleted", { threadId });
|
|
684
693
|
} catch (error) {
|
|
685
694
|
logger.error("thread delete error", {
|
|
686
695
|
threadId,
|
|
@@ -744,28 +753,52 @@ export const useMastraChat = (
|
|
|
744
753
|
|
|
745
754
|
const regenerate = useCallback(() => {
|
|
746
755
|
const threadId = activeKey;
|
|
747
|
-
const lastUserText = getSession(threadId).lastUserText;
|
|
748
|
-
if (!lastUserText) return;
|
|
749
756
|
const prev = getSession(threadId).messages;
|
|
750
757
|
const lastAssistant = prev.length > 0 && prev.at(-1)?.role === "assistant" ? prev.at(-1) : null;
|
|
751
758
|
const trimmed = lastAssistant ? prev.slice(0, -1) : prev;
|
|
759
|
+
const lastUser = [...trimmed].reverse().find((message) => message.role === "user");
|
|
760
|
+
if (!lastUser) return;
|
|
752
761
|
if (lastAssistant) {
|
|
753
762
|
updateSession(threadId, (session) => {
|
|
754
|
-
const { [lastAssistant.id]: _events, ...toolEvents } = session.toolEventsByMessage;
|
|
755
763
|
const { [lastAssistant.id]: _approvals, ...pendingApprovals } =
|
|
756
764
|
session.pendingApprovalsByMessage;
|
|
757
765
|
const { [lastAssistant.id]: _feedback, ...feedback } = session.feedbackByMessage;
|
|
758
766
|
return {
|
|
759
767
|
...session,
|
|
760
|
-
toolEventsByMessage: toolEvents,
|
|
761
768
|
pendingApprovalsByMessage: pendingApprovals,
|
|
762
769
|
feedbackByMessage: feedback,
|
|
763
770
|
};
|
|
764
771
|
});
|
|
765
772
|
}
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
773
|
+
void mastraClient
|
|
774
|
+
.deleteMessages(activeThreadId, [lastUser.id, ...(lastAssistant ? [lastAssistant.id] : [])], {
|
|
775
|
+
agentId,
|
|
776
|
+
})
|
|
777
|
+
.then(() => {
|
|
778
|
+
writeMessages(threadId, trimmed);
|
|
779
|
+
void runStream(threadId, lastUser, getSession(threadId).lastRequestContext);
|
|
780
|
+
})
|
|
781
|
+
.catch((error: unknown) => {
|
|
782
|
+
logger.error("regenerate cleanup error", {
|
|
783
|
+
threadId,
|
|
784
|
+
error: sharedError.errorMessage(error),
|
|
785
|
+
});
|
|
786
|
+
updateSession(threadId, (session) => ({
|
|
787
|
+
...session,
|
|
788
|
+
error: sharedError.toError(error),
|
|
789
|
+
status: "error",
|
|
790
|
+
}));
|
|
791
|
+
});
|
|
792
|
+
}, [
|
|
793
|
+
activeKey,
|
|
794
|
+
activeThreadId,
|
|
795
|
+
agentId,
|
|
796
|
+
getSession,
|
|
797
|
+
mastraClient,
|
|
798
|
+
runStream,
|
|
799
|
+
updateSession,
|
|
800
|
+
writeMessages,
|
|
801
|
+
]);
|
|
769
802
|
|
|
770
803
|
// Chat export (opt-in). Resolves `[chart:<id>]` / `[data:<id>]` embeds
|
|
771
804
|
// straight off the client so the export inlines the same charts /
|
|
@@ -922,7 +955,6 @@ export const useMastraChat = (
|
|
|
922
955
|
regenerate,
|
|
923
956
|
onStop: stop,
|
|
924
957
|
suggestions,
|
|
925
|
-
toolEventsByMessage: activeSession.toolEventsByMessage,
|
|
926
958
|
pendingApprovalsByMessage: activeSession.pendingApprovalsByMessage,
|
|
927
959
|
onResolveToolApproval: handleApproval,
|
|
928
960
|
// Picker is opt-in: only hand ChatView the catalogue + change
|
|
@@ -982,11 +1014,15 @@ export const useMastraChat = (
|
|
|
982
1014
|
};
|
|
983
1015
|
|
|
984
1016
|
/** Props for {@link MastraChat}. */
|
|
985
|
-
export interface MastraChatProps
|
|
1017
|
+
export interface MastraChatProps<
|
|
1018
|
+
TValues extends Record<string, unknown> = Record<string, unknown>,
|
|
1019
|
+
> extends UseMastraChatOptions<TValues> {
|
|
986
1020
|
/** Extra classes merged onto the chat's root layout container. */
|
|
987
1021
|
className?: string;
|
|
988
|
-
/** Host controls placed beside the model picker in the composer
|
|
1022
|
+
/** Host controls placed beside the model picker in the composer footer. */
|
|
989
1023
|
composerActions?: ReactNode;
|
|
1024
|
+
/** Host controls placed on the left side of the composer footer. */
|
|
1025
|
+
composerLeadingActions?: ReactNode;
|
|
990
1026
|
}
|
|
991
1027
|
|
|
992
1028
|
/**
|
|
@@ -1002,7 +1038,19 @@ export interface MastraChatProps extends UseMastraChatOptions {
|
|
|
1002
1038
|
* `threadPlacement` (`auto` docks it left and falls back to a tab strip on a
|
|
1003
1039
|
* narrow chat) and turned off with `threadPlacement: "disabled"`.
|
|
1004
1040
|
*/
|
|
1005
|
-
export const MastraChat =
|
|
1041
|
+
export const MastraChat = <TValues extends Record<string, unknown> = Record<string, unknown>>({
|
|
1042
|
+
className,
|
|
1043
|
+
composerActions,
|
|
1044
|
+
composerLeadingActions,
|
|
1045
|
+
...options
|
|
1046
|
+
}: MastraChatProps<TValues>) => {
|
|
1006
1047
|
const chat = useMastraChat(options);
|
|
1007
|
-
return
|
|
1048
|
+
return (
|
|
1049
|
+
<ChatView
|
|
1050
|
+
{...chat}
|
|
1051
|
+
className={className}
|
|
1052
|
+
composerActions={composerActions}
|
|
1053
|
+
composerLeadingActions={composerLeadingActions}
|
|
1054
|
+
/>
|
|
1055
|
+
);
|
|
1008
1056
|
};
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { wire } from "@dbx-tools/shared-mastra";
|
|
2
1
|
import {
|
|
3
2
|
Button,
|
|
4
3
|
Input,
|
|
@@ -192,7 +191,6 @@ export const ThreadSidebar = ({
|
|
|
192
191
|
<Input
|
|
193
192
|
autoFocus
|
|
194
193
|
value={draftTitle}
|
|
195
|
-
maxLength={wire.MASTRA_THREAD_TITLE_MAX}
|
|
196
194
|
aria-label="Conversation name"
|
|
197
195
|
onChange={(e) => setDraftTitle(e.target.value)}
|
|
198
196
|
onKeyDown={(e) => {
|
|
@@ -214,59 +212,51 @@ export const ThreadSidebar = ({
|
|
|
214
212
|
return (
|
|
215
213
|
<li key={thread.id}>
|
|
216
214
|
<div
|
|
217
|
-
role="button"
|
|
218
|
-
tabIndex={0}
|
|
219
|
-
onClick={() => onSelect(thread.id)}
|
|
220
|
-
onKeyDown={(e) => {
|
|
221
|
-
if (e.key === "Enter" || e.key === " ") {
|
|
222
|
-
e.preventDefault();
|
|
223
|
-
onSelect(thread.id);
|
|
224
|
-
}
|
|
225
|
-
}}
|
|
226
215
|
className={cn(
|
|
227
|
-
"group flex
|
|
216
|
+
"group flex items-center gap-2 rounded-md px-2 py-1.5 text-sm",
|
|
228
217
|
"hover:bg-accent hover:text-accent-foreground",
|
|
229
218
|
isActive && "bg-accent text-accent-foreground",
|
|
230
219
|
)}
|
|
231
220
|
>
|
|
232
|
-
<
|
|
221
|
+
<button
|
|
222
|
+
type="button"
|
|
223
|
+
onClick={() => onSelect(thread.id)}
|
|
224
|
+
aria-current={isActive ? "page" : undefined}
|
|
225
|
+
className="min-w-0 flex-1 cursor-pointer text-left"
|
|
226
|
+
>
|
|
233
227
|
<div className="flex items-center gap-1.5 truncate">
|
|
234
|
-
{isStreaming &&
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
variant="ghost"
|
|
241
|
-
size="icon"
|
|
242
|
-
onClick={(e) => {
|
|
243
|
-
e.stopPropagation();
|
|
244
|
-
onCancel(thread.id);
|
|
245
|
-
}}
|
|
246
|
-
aria-label="Stop generating"
|
|
247
|
-
className="size-4 shrink-0 text-primary"
|
|
248
|
-
>
|
|
249
|
-
{/* Spinner by default; swap to a stop square on hover/focus. */}
|
|
250
|
-
<Loader2Icon className="size-3 animate-spin group-hover:hidden" />
|
|
251
|
-
<SquareIcon className="hidden size-3 fill-current group-hover:block" />
|
|
252
|
-
</Button>
|
|
253
|
-
</TooltipTrigger>
|
|
254
|
-
<TooltipContent>Stop generating</TooltipContent>
|
|
255
|
-
</Tooltip>
|
|
256
|
-
) : (
|
|
257
|
-
<Loader2Icon
|
|
258
|
-
aria-label="Streaming"
|
|
259
|
-
className="size-3 shrink-0 animate-spin text-primary"
|
|
260
|
-
/>
|
|
261
|
-
))}
|
|
228
|
+
{isStreaming && !onCancel ? (
|
|
229
|
+
<Loader2Icon
|
|
230
|
+
aria-label="Streaming"
|
|
231
|
+
className="size-3 shrink-0 animate-spin text-primary"
|
|
232
|
+
/>
|
|
233
|
+
) : null}
|
|
262
234
|
<span className="truncate">{threadTitle(thread)}</span>
|
|
263
235
|
</div>
|
|
264
236
|
{thread.updatedAt && (
|
|
265
|
-
<div className="truncate text-xs text-
|
|
237
|
+
<div className="truncate text-xs text-foreground/70">
|
|
266
238
|
{relativeTime(thread.updatedAt)}
|
|
267
239
|
</div>
|
|
268
240
|
)}
|
|
269
|
-
</
|
|
241
|
+
</button>
|
|
242
|
+
{isStreaming && onCancel ? (
|
|
243
|
+
<Tooltip>
|
|
244
|
+
<TooltipTrigger asChild>
|
|
245
|
+
<Button
|
|
246
|
+
type="button"
|
|
247
|
+
variant="ghost"
|
|
248
|
+
size="icon"
|
|
249
|
+
onClick={() => onCancel(thread.id)}
|
|
250
|
+
aria-label="Stop generating"
|
|
251
|
+
className="size-6 shrink-0 text-primary"
|
|
252
|
+
>
|
|
253
|
+
<Loader2Icon className="size-3 animate-spin group-hover:hidden" />
|
|
254
|
+
<SquareIcon className="hidden size-3 fill-current group-hover:block" />
|
|
255
|
+
</Button>
|
|
256
|
+
</TooltipTrigger>
|
|
257
|
+
<TooltipContent>Stop generating</TooltipContent>
|
|
258
|
+
</Tooltip>
|
|
259
|
+
) : null}
|
|
270
260
|
{onRename && (
|
|
271
261
|
<Tooltip>
|
|
272
262
|
<TooltipTrigger asChild>
|