@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.
- package/README.md +4 -0
- package/index.ts +5 -2
- package/package.json +8 -7
- package/src/react/_assistant-activity.ts +43 -0
- package/src/react/_assistant-response-status.tsx +18 -0
- package/src/react/_external-link-dialog.tsx +106 -0
- package/src/react/bubbles.tsx +51 -53
- package/src/react/chat-composer.tsx +1 -1
- package/src/react/chat-feedback.ts +1 -1
- package/src/react/chat-history.ts +12 -4
- package/src/react/chat-sessions.ts +3 -0
- package/src/react/chat-stream.ts +1 -1
- package/src/react/chat-thread-layout.tsx +6 -0
- package/src/react/chat-transcript.tsx +11 -4
- package/src/react/chat-view.tsx +4 -0
- package/src/react/data-grid.tsx +1 -0
- package/src/react/feedback-controls.tsx +1 -0
- package/src/react/markdown.tsx +11 -0
- package/src/react/mastra-assistant.tsx +6 -0
- package/src/react/mastra-chat.tsx +90 -14
- package/src/react/suggestion-pills.tsx +1 -0
- package/src/react/thread-sidebar.tsx +6 -2
- package/src/react/thread-tabs.tsx +19 -2
- package/src/react/tool-pill.tsx +5 -3
- package/src/react/types.ts +5 -1
- package/src/support/active-runs.ts +111 -0
- package/src/support/index.ts +4 -1
- package/src/support/mastra-client.ts +45 -0
- package/src/support/thread-sessions.ts +3 -0
|
@@ -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,
|
package/src/react/markdown.tsx
CHANGED
|
@@ -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 `"
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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 = ({
|
|
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
|
|
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>
|
package/src/react/tool-pill.tsx
CHANGED
|
@@ -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`
|
|
128
|
-
*
|
|
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
|
|
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;
|
package/src/react/types.ts
CHANGED
|
@@ -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
|
|
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
|
+
}
|
package/src/support/index.ts
CHANGED
|
@@ -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";
|