@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
package/README.md
CHANGED
|
@@ -22,6 +22,9 @@ Key features:
|
|
|
22
22
|
|
|
23
23
|
- Drop-in `MastraChat` component that discovers the plugin client config and
|
|
24
24
|
wires itself to the default agent.
|
|
25
|
+
- Reload recovery for background turns: active run metadata is persisted per
|
|
26
|
+
agent and thread, the cached stream reconnects after a page reload, and the
|
|
27
|
+
final persisted history is refreshed when recovery completes.
|
|
25
28
|
- Headless `useMastraChat()` driver for apps that want the same transport logic
|
|
26
29
|
with custom layout.
|
|
27
30
|
- Persistent `MastraAssistant` application shell with a floating launcher,
|
|
@@ -285,6 +288,7 @@ export function ReviewChat(props: ChatViewProps) {
|
|
|
285
288
|
sidebar, transcript, tool progress, approval cards, suggestions, export controls,
|
|
286
289
|
feedback controls, and composer from props. Use it when your app already has a
|
|
287
290
|
transport or needs to combine Mastra messages with another state model.
|
|
291
|
+
|
|
288
292
|
`MastraChat` shows a spinner in the model control while its live default model
|
|
289
293
|
is being resolved. The picker groups endpoints into
|
|
290
294
|
model-family submenus. Its compact first popup shows only the current
|
package/index.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Hand edits are overwritten on the next watch; this file is read-only.
|
|
4
4
|
|
|
5
5
|
export const PACKAGE_IDENTIFIER = "@dbx-tools/ui-mastra";
|
|
6
|
-
export const PACKAGE_VERSION = "0.9.
|
|
6
|
+
export const PACKAGE_VERSION = "0.9.120";
|
|
7
7
|
export * as reactBubbles from "./src/react/bubbles.tsx";
|
|
8
8
|
export * as reactChatApprovals from "./src/react/chat-approvals.ts";
|
|
9
9
|
export * as reactChatComposer from "./src/react/chat-composer.tsx";
|
|
@@ -27,6 +27,7 @@ export * as reactThreadSidebar from "./src/react/thread-sidebar.tsx";
|
|
|
27
27
|
export * as reactThreadTabs from "./src/react/thread-tabs.tsx";
|
|
28
28
|
export * as reactToolPill from "./src/react/tool-pill.tsx";
|
|
29
29
|
export * as reactTypes from "./src/react/types.ts";
|
|
30
|
+
export * as supportActiveRuns from "./src/support/active-runs.ts";
|
|
30
31
|
export * as supportChartOption from "./src/support/chart-option.ts";
|
|
31
32
|
export * as supportChartTheme from "./src/support/chart-theme.ts";
|
|
32
33
|
export * as supportClipboard from "./src/support/clipboard.ts";
|
|
@@ -65,10 +66,12 @@ export type { ThreadTabsProps, ThreadActionsProps } from "./src/react/thread-tab
|
|
|
65
66
|
export { humanizeToolName, ToolSessionPill } from "./src/react/tool-pill.tsx";
|
|
66
67
|
export type { ToolInputPresentation, WebSearchProgressGroup } from "./src/react/tool-pill.tsx";
|
|
67
68
|
export type { ChatStatus, ToolEvent, ToolProgress, ChatModelOption, ChatInputMessage, QueuedSteer, FeedbackValue, FeedbackSubmission, MessageFeedback, ThreadSummary, ThreadPlacement, ChatViewProps, ApprovalDecision, PendingApproval } from "./src/react/types.ts";
|
|
69
|
+
export { StoredActiveRunSchema, activeRunStorageKey } from "./src/support/active-runs.ts";
|
|
70
|
+
export type { StoredActiveRun } from "./src/support/active-runs.ts";
|
|
68
71
|
export { LIGHT_CHART_CHROME } from "./src/support/chart-theme.ts";
|
|
69
72
|
export type { ChartChrome } from "./src/support/chart-theme.ts";
|
|
70
73
|
export type { ExportFormat, ExportBrand, EmbedResolver, ExportChatOptions } from "./src/support/export.ts";
|
|
71
|
-
export { MastraPluginClient, useMastraConfig, useMastraClient, useMastraMlflowExperiment, useMastraModels, useMastraDefaultModel, useMastraSuggestions, useMastraThreads, useChartFetch, useStatementFetch } from "./src/support/mastra-client.ts";
|
|
74
|
+
export { MastraChatRequestError, MastraPluginClient, useMastraConfig, useMastraClient, useMastraMlflowExperiment, useMastraModels, useMastraDefaultModel, useMastraSuggestions, useMastraThreads, useChartFetch, useStatementFetch } from "./src/support/mastra-client.ts";
|
|
72
75
|
export type { MastraStreamResponse, MastraMemoryThread, ByIdFetchState } from "./src/support/mastra-client.ts";
|
|
73
76
|
export { defaultReasoningEffort, modelStorageKey, readStoredModel, storeSelectedModel } from "./src/support/model-selection.ts";
|
|
74
77
|
export type { MastraRequestContextInput, MastraRequestContextSource, MastraRequestContextSnapshot } from "./src/support/request-context.ts";
|
package/package.json
CHANGED
|
@@ -29,11 +29,11 @@
|
|
|
29
29
|
"react-dom": "~19.2.4"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@dbx-tools/shared-core": "^0.9.
|
|
33
|
-
"@dbx-tools/shared-genie": "^0.9.
|
|
34
|
-
"@dbx-tools/shared-mastra": "^0.9.
|
|
35
|
-
"@dbx-tools/shared-model": "^0.9.
|
|
36
|
-
"@dbx-tools/ui": "^0.9.
|
|
32
|
+
"@dbx-tools/shared-core": "^0.9.120",
|
|
33
|
+
"@dbx-tools/shared-genie": "^0.9.120",
|
|
34
|
+
"@dbx-tools/shared-mastra": "^0.9.120",
|
|
35
|
+
"@dbx-tools/shared-model": "^0.9.120",
|
|
36
|
+
"@dbx-tools/ui": "^0.9.120",
|
|
37
37
|
"@mastra/client-js": "1.50.0",
|
|
38
38
|
"@mastra/core": "1.71.0",
|
|
39
39
|
"@tanstack/react-table": "^8.21.3",
|
|
@@ -43,13 +43,14 @@
|
|
|
43
43
|
"lucide-react": "^0.554.0",
|
|
44
44
|
"marked": "^18.0.5",
|
|
45
45
|
"sql-formatter": "^15.6.9",
|
|
46
|
-
"streamdown": "^2.5.0"
|
|
46
|
+
"streamdown": "^2.5.0",
|
|
47
|
+
"zod": "4.3.6"
|
|
47
48
|
},
|
|
48
49
|
"license": "Apache-2.0",
|
|
49
50
|
"publishConfig": {
|
|
50
51
|
"access": "public"
|
|
51
52
|
},
|
|
52
|
-
"version": "0.9.
|
|
53
|
+
"version": "0.9.120",
|
|
53
54
|
"dbxToolsConfig": {
|
|
54
55
|
"tags": [
|
|
55
56
|
"ui"
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { isToolUIPart, type UIMessage } from "ai";
|
|
2
|
+
|
|
3
|
+
import type { ToolEvent } from "./types.ts";
|
|
4
|
+
import { compactReasoningText } from "../support/tool-events.ts";
|
|
5
|
+
|
|
6
|
+
/** One ordered activity entry rendered before an assistant's answer text. */
|
|
7
|
+
export type AssistantActivityItem =
|
|
8
|
+
{ type: "tool"; key: string; event: ToolEvent } | { type: "thought"; key: string; text: string };
|
|
9
|
+
|
|
10
|
+
/** Preserve model reasoning and tool progress in the order they reached the message. */
|
|
11
|
+
export const assistantActivityItems = (
|
|
12
|
+
parts: UIMessage["parts"],
|
|
13
|
+
events: ToolEvent[],
|
|
14
|
+
): AssistantActivityItem[] => {
|
|
15
|
+
const eventById = new Map(events.map((event) => [event.id, event]));
|
|
16
|
+
const renderedEventIds = new Set<string>();
|
|
17
|
+
const items: AssistantActivityItem[] = [];
|
|
18
|
+
|
|
19
|
+
const appendTool = (event: ToolEvent) => {
|
|
20
|
+
if (renderedEventIds.has(event.id)) return;
|
|
21
|
+
renderedEventIds.add(event.id);
|
|
22
|
+
items.push({ type: "tool", key: `tool-${event.id}`, event });
|
|
23
|
+
for (const [index, progress] of (event.progress ?? []).entries()) {
|
|
24
|
+
if (progress.type !== "thinking") continue;
|
|
25
|
+
const text = compactReasoningText(progress.text);
|
|
26
|
+
if (text) items.push({ type: "thought", key: `thought-${event.id}-${index}`, text });
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
for (const [index, part] of parts.entries()) {
|
|
31
|
+
if (part.type === "reasoning") {
|
|
32
|
+
const text = compactReasoningText(part.text);
|
|
33
|
+
if (text) items.push({ type: "thought", key: `reasoning-${index}`, text });
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (!isToolUIPart(part)) continue;
|
|
37
|
+
const event = eventById.get(part.toolCallId);
|
|
38
|
+
if (event) appendTool(event);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
for (const event of events) appendTool(event);
|
|
42
|
+
return items;
|
|
43
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Spinner } from "@dbx-tools/ui/react";
|
|
2
|
+
|
|
3
|
+
import type { ChatStatus } from "./types.ts";
|
|
4
|
+
|
|
5
|
+
/** Whether an assistant bubble should show its active-response status. */
|
|
6
|
+
export const shouldShowAssistantResponseStatus = (
|
|
7
|
+
isLast: boolean,
|
|
8
|
+
status: ChatStatus,
|
|
9
|
+
hasVisibleResponse: boolean,
|
|
10
|
+
): boolean => isLast && hasVisibleResponse && (status === "submitted" || status === "streaming");
|
|
11
|
+
|
|
12
|
+
/** Compact progress affordance for an active assistant response. */
|
|
13
|
+
export const AssistantResponseSpinner = () => (
|
|
14
|
+
<span role="status" aria-live="polite" className="inline-flex items-center">
|
|
15
|
+
<Spinner className="size-3" />
|
|
16
|
+
<span className="sr-only">Response in progress</span>
|
|
17
|
+
</span>
|
|
18
|
+
);
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AlertDialog,
|
|
3
|
+
AlertDialogAction,
|
|
4
|
+
AlertDialogCancel,
|
|
5
|
+
AlertDialogContent,
|
|
6
|
+
AlertDialogDescription,
|
|
7
|
+
AlertDialogFooter,
|
|
8
|
+
AlertDialogHeader,
|
|
9
|
+
AlertDialogTitle,
|
|
10
|
+
Button,
|
|
11
|
+
} from "@dbx-tools/ui/react";
|
|
12
|
+
import { CheckIcon, CopyIcon, ExternalLinkIcon } from "lucide-react";
|
|
13
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
14
|
+
import type { LinkSafetyModalProps } from "streamdown";
|
|
15
|
+
import { copyText } from "../support/clipboard.ts";
|
|
16
|
+
|
|
17
|
+
/** Display and clipboard values for one confirmed chat link. */
|
|
18
|
+
export interface LinkPresentation {
|
|
19
|
+
displayUrl: string;
|
|
20
|
+
absoluteUrl: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Resolve a chat link for display and copying.
|
|
25
|
+
*
|
|
26
|
+
* Same-origin HTTP links are shown as a compact path while clipboard actions
|
|
27
|
+
* keep the absolute URL. External origins and non-HTTP schemes remain fully
|
|
28
|
+
* visible so the confirmation never obscures where the user is going.
|
|
29
|
+
*/
|
|
30
|
+
export function resolveLinkPresentation(url: string, baseUrl?: string): LinkPresentation {
|
|
31
|
+
const fallback = { displayUrl: url, absoluteUrl: url };
|
|
32
|
+
const base = baseUrl ?? (typeof window === "undefined" ? undefined : window.location.href);
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const resolved = base ? new URL(url, base) : new URL(url);
|
|
36
|
+
const baseOrigin = base ? new URL(base).origin : undefined;
|
|
37
|
+
const isWebLink = resolved.protocol === "http:" || resolved.protocol === "https:";
|
|
38
|
+
const displayUrl =
|
|
39
|
+
isWebLink && resolved.origin === baseOrigin
|
|
40
|
+
? `${resolved.pathname}${resolved.search}${resolved.hash}` || "/"
|
|
41
|
+
: resolved.href;
|
|
42
|
+
return { displayUrl, absoluteUrl: resolved.href };
|
|
43
|
+
} catch {
|
|
44
|
+
return fallback;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** AppKit-styled confirmation shown before opening a link from assistant markdown. */
|
|
49
|
+
export const ExternalLinkDialog = ({ isOpen, onClose, onConfirm, url }: LinkSafetyModalProps) => {
|
|
50
|
+
const link = useMemo(() => resolveLinkPresentation(url), [url]);
|
|
51
|
+
const [copied, setCopied] = useState(false);
|
|
52
|
+
const copiedTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
53
|
+
|
|
54
|
+
useEffect(
|
|
55
|
+
() => () => {
|
|
56
|
+
if (copiedTimer.current) clearTimeout(copiedTimer.current);
|
|
57
|
+
},
|
|
58
|
+
[],
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const copyLink = () => {
|
|
62
|
+
void copyText(link.absoluteUrl).then((ok) => {
|
|
63
|
+
if (!ok) return;
|
|
64
|
+
setCopied(true);
|
|
65
|
+
if (copiedTimer.current) clearTimeout(copiedTimer.current);
|
|
66
|
+
copiedTimer.current = setTimeout(() => setCopied(false), 1500);
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<AlertDialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
|
|
72
|
+
<AlertDialogContent className="border-border/60 p-5 shadow-2xl dark:border-white/10 sm:max-w-md">
|
|
73
|
+
<AlertDialogHeader>
|
|
74
|
+
<AlertDialogTitle className="flex items-center gap-2">
|
|
75
|
+
<ExternalLinkIcon className="size-4" aria-hidden="true" />
|
|
76
|
+
Open external link?
|
|
77
|
+
</AlertDialogTitle>
|
|
78
|
+
<AlertDialogDescription>
|
|
79
|
+
You're about to visit a link outside this chat.
|
|
80
|
+
</AlertDialogDescription>
|
|
81
|
+
</AlertDialogHeader>
|
|
82
|
+
<div
|
|
83
|
+
className="max-h-32 overflow-y-auto break-all rounded-md bg-muted px-3 py-2 font-mono text-sm"
|
|
84
|
+
title={link.absoluteUrl}
|
|
85
|
+
>
|
|
86
|
+
{link.displayUrl}
|
|
87
|
+
</div>
|
|
88
|
+
<AlertDialogFooter>
|
|
89
|
+
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
90
|
+
<Button type="button" variant="outline" onClick={copyLink}>
|
|
91
|
+
{copied ? (
|
|
92
|
+
<CheckIcon className="size-4" aria-hidden="true" />
|
|
93
|
+
) : (
|
|
94
|
+
<CopyIcon className="size-4" aria-hidden="true" />
|
|
95
|
+
)}
|
|
96
|
+
{copied ? "Copied" : "Copy link"}
|
|
97
|
+
</Button>
|
|
98
|
+
<AlertDialogAction onClick={onConfirm}>
|
|
99
|
+
<ExternalLinkIcon className="size-4" aria-hidden="true" />
|
|
100
|
+
Open link
|
|
101
|
+
</AlertDialogAction>
|
|
102
|
+
</AlertDialogFooter>
|
|
103
|
+
</AlertDialogContent>
|
|
104
|
+
</AlertDialog>
|
|
105
|
+
);
|
|
106
|
+
};
|
package/src/react/bubbles.tsx
CHANGED
|
@@ -18,7 +18,6 @@ import type { UIMessage } from "ai";
|
|
|
18
18
|
import {
|
|
19
19
|
BugIcon,
|
|
20
20
|
CheckIcon,
|
|
21
|
-
ChevronDownIcon,
|
|
22
21
|
CopyIcon,
|
|
23
22
|
MessageSquareIcon,
|
|
24
23
|
RefreshCcwIcon,
|
|
@@ -26,6 +25,11 @@ import {
|
|
|
26
25
|
XIcon,
|
|
27
26
|
} from "lucide-react";
|
|
28
27
|
import { useState } from "react";
|
|
28
|
+
import { assistantActivityItems } from "./_assistant-activity.ts";
|
|
29
|
+
import {
|
|
30
|
+
AssistantResponseSpinner,
|
|
31
|
+
shouldShowAssistantResponseStatus,
|
|
32
|
+
} from "./_assistant-response-status.tsx";
|
|
29
33
|
import { MarkdownWithEmbeds } from "./embed-slots.tsx";
|
|
30
34
|
import { ExportMenu } from "./export-menu.tsx";
|
|
31
35
|
import { FeedbackControls } from "./feedback-controls.tsx";
|
|
@@ -43,24 +47,12 @@ import type {
|
|
|
43
47
|
ToolEvent,
|
|
44
48
|
} from "./types.ts";
|
|
45
49
|
import { copyText } from "../support/clipboard.ts";
|
|
46
|
-
import {
|
|
47
|
-
compactReasoningText,
|
|
48
|
-
genieReasoningText,
|
|
49
|
-
mergeToolEvents,
|
|
50
|
-
toolEventsFromParts,
|
|
51
|
-
} from "../support/tool-events.ts";
|
|
50
|
+
import { mergeToolEvents, toolEventsFromParts } from "../support/tool-events.ts";
|
|
52
51
|
|
|
53
52
|
// User / assistant message bubbles plus the inline approval card and
|
|
54
53
|
// the helpers that surface approval-gated tool calls out of a message's
|
|
55
54
|
// parts.
|
|
56
55
|
|
|
57
|
-
const getReasoningText = (parts: UIMessage["parts"]): string =>
|
|
58
|
-
parts
|
|
59
|
-
.filter((p): p is { type: "reasoning"; text: string } => p.type === "reasoning")
|
|
60
|
-
.map((p) => compactReasoningText(p.text))
|
|
61
|
-
.filter(Boolean)
|
|
62
|
-
.join("\n");
|
|
63
|
-
|
|
64
56
|
const UserAvatar = () => (
|
|
65
57
|
<Avatar className="size-7">
|
|
66
58
|
<AvatarFallback>
|
|
@@ -69,6 +61,19 @@ const UserAvatar = () => (
|
|
|
69
61
|
</Avatar>
|
|
70
62
|
);
|
|
71
63
|
|
|
64
|
+
const ThoughtsDisclosure = ({ text }: { text: string }) => (
|
|
65
|
+
<Collapsible>
|
|
66
|
+
<CollapsibleTrigger className="w-fit cursor-pointer rounded-md border border-border/40 bg-background/30 px-2 py-1 text-xs text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
|
|
67
|
+
Thoughts
|
|
68
|
+
</CollapsibleTrigger>
|
|
69
|
+
<CollapsibleContent>
|
|
70
|
+
<div className="mt-1 border-l-2 border-border/60 pl-2.5 text-xs text-muted-foreground">
|
|
71
|
+
<ToolMarkdown className="prose-p:my-0 [&_p]:!my-0">{text}</ToolMarkdown>
|
|
72
|
+
</div>
|
|
73
|
+
</CollapsibleContent>
|
|
74
|
+
</Collapsible>
|
|
75
|
+
);
|
|
76
|
+
|
|
72
77
|
/** A tool-call input shaped like an outbound email (the `send_email` tool). */
|
|
73
78
|
type EmailApprovalInput = {
|
|
74
79
|
to?: string | string[];
|
|
@@ -335,6 +340,7 @@ type AssistantBubbleProps = {
|
|
|
335
340
|
mlflowExperimentUrl?: string;
|
|
336
341
|
};
|
|
337
342
|
|
|
343
|
+
/** Render one assistant message with tool progress, actions, and optional feedback. */
|
|
338
344
|
export const AssistantBubble = ({
|
|
339
345
|
message,
|
|
340
346
|
isLast,
|
|
@@ -360,18 +366,7 @@ export const AssistantBubble = ({
|
|
|
360
366
|
? "done"
|
|
361
367
|
: undefined;
|
|
362
368
|
const toolEvents = mergeToolEvents(toolEventsFromParts(message.parts), events, terminalToolState);
|
|
363
|
-
const
|
|
364
|
-
.filter(Boolean)
|
|
365
|
-
.join("\n");
|
|
366
|
-
const isReasoningStreaming =
|
|
367
|
-
isLast &&
|
|
368
|
-
status === "streaming" &&
|
|
369
|
-
(message.parts.at(-1)?.type === "reasoning" ||
|
|
370
|
-
toolEvents.some(
|
|
371
|
-
(event) =>
|
|
372
|
-
event.status === "running" &&
|
|
373
|
-
event.progress?.some((progress) => progress.type === "thinking"),
|
|
374
|
-
));
|
|
369
|
+
const activityItems = assistantActivityItems(message.parts, toolEvents);
|
|
375
370
|
const textParts = message.parts.filter(
|
|
376
371
|
(p): p is { type: "text"; text: string } => p.type === "text",
|
|
377
372
|
);
|
|
@@ -394,6 +389,14 @@ export const AssistantBubble = ({
|
|
|
394
389
|
// `error`, or a newer message has taken over the `isLast` slot.
|
|
395
390
|
const isStreamingThisBubble = isLast && (status === "streaming" || status === "submitted");
|
|
396
391
|
const suggestions = isStreamingThisBubble ? [] : collectSuggestions(events);
|
|
392
|
+
const showResponseStatus = shouldShowAssistantResponseStatus(
|
|
393
|
+
isLast,
|
|
394
|
+
status,
|
|
395
|
+
hasText || activityItems.length > 0 || pendingApprovals.length > 0,
|
|
396
|
+
);
|
|
397
|
+
const hasFooterActions =
|
|
398
|
+
hasText && (isLast || Boolean(onExport) || Boolean(onFeedback) || Boolean(mlflowExperimentUrl));
|
|
399
|
+
const showFooter = showResponseStatus || hasFooterActions;
|
|
397
400
|
// Charts and tables are placed at inline marker positions in
|
|
398
401
|
// the assistant's prose. `prepare_chart` / `render_data` mint
|
|
399
402
|
// a `chartId` and the model embeds `[chart:<chartId>]`; for raw
|
|
@@ -412,26 +415,13 @@ export const AssistantBubble = ({
|
|
|
412
415
|
* `min-width: auto`, which sizes to content and overflows.
|
|
413
416
|
*/}
|
|
414
417
|
<ItemContent className="min-w-0 gap-1.5">
|
|
415
|
-
{/*
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
{toolEvents.length > 0 && <ToolSessionPill events={toolEvents} />}
|
|
423
|
-
{reasoning && (
|
|
424
|
-
<Collapsible>
|
|
425
|
-
<CollapsibleTrigger className="group flex cursor-pointer items-center gap-1 text-xs text-muted-foreground hover:text-foreground">
|
|
426
|
-
<ChevronDownIcon className="size-3 transition-transform group-data-[state=closed]:-rotate-90" />
|
|
427
|
-
<span>{isReasoningStreaming ? "Thinking..." : "Thoughts"}</span>
|
|
428
|
-
</CollapsibleTrigger>
|
|
429
|
-
<CollapsibleContent>
|
|
430
|
-
<div className="mt-1 border-l-2 border-border/60 pl-2.5 text-xs text-muted-foreground">
|
|
431
|
-
<ToolMarkdown className="prose-p:my-0 [&_p]:!my-0">{reasoning}</ToolMarkdown>
|
|
432
|
-
</div>
|
|
433
|
-
</CollapsibleContent>
|
|
434
|
-
</Collapsible>
|
|
418
|
+
{/* Tool calls and their thought events stay in arrival order above the answer text. */}
|
|
419
|
+
{activityItems.map((item) =>
|
|
420
|
+
item.type === "tool" ? (
|
|
421
|
+
<ToolSessionPill key={item.key} events={[item.event]} />
|
|
422
|
+
) : (
|
|
423
|
+
<ThoughtsDisclosure key={item.key} text={item.text} />
|
|
424
|
+
),
|
|
435
425
|
)}
|
|
436
426
|
{/*
|
|
437
427
|
* Render each text part as its own block. A multi-step turn
|
|
@@ -459,9 +449,9 @@ export const AssistantBubble = ({
|
|
|
459
449
|
onResolve={onResolveToolApproval}
|
|
460
450
|
/>
|
|
461
451
|
))}
|
|
462
|
-
{
|
|
463
|
-
<div className="flex items-center gap-1">
|
|
464
|
-
{isLast && regenerate && (
|
|
452
|
+
{showFooter && (
|
|
453
|
+
<div className="flex min-h-7 items-center gap-1">
|
|
454
|
+
{hasText && isLast && regenerate && (
|
|
465
455
|
<Tooltip>
|
|
466
456
|
<TooltipTrigger asChild>
|
|
467
457
|
<Button
|
|
@@ -478,7 +468,7 @@ export const AssistantBubble = ({
|
|
|
478
468
|
<TooltipContent>Retry</TooltipContent>
|
|
479
469
|
</Tooltip>
|
|
480
470
|
)}
|
|
481
|
-
{isLast && (
|
|
471
|
+
{hasText && isLast && (
|
|
482
472
|
<Tooltip>
|
|
483
473
|
<TooltipTrigger asChild>
|
|
484
474
|
<Button
|
|
@@ -495,14 +485,16 @@ export const AssistantBubble = ({
|
|
|
495
485
|
<TooltipContent>{copied ? "Copied" : "Copy"}</TooltipContent>
|
|
496
486
|
</Tooltip>
|
|
497
487
|
)}
|
|
498
|
-
{
|
|
499
|
-
|
|
488
|
+
{hasText && onExport && (
|
|
489
|
+
<ExportMenu onExport={onExport} iconOnly tooltip="Export message" />
|
|
490
|
+
)}
|
|
491
|
+
{hasText && onFeedback && (
|
|
500
492
|
<FeedbackControls
|
|
501
493
|
onSubmit={onFeedback}
|
|
502
494
|
{...(feedbackValue ? { value: feedbackValue } : {})}
|
|
503
495
|
/>
|
|
504
496
|
)}
|
|
505
|
-
{mlflowExperimentUrl && (
|
|
497
|
+
{hasText && mlflowExperimentUrl && (
|
|
506
498
|
<Tooltip>
|
|
507
499
|
<TooltipTrigger asChild>
|
|
508
500
|
<Button asChild type="button" size="icon" variant="ghost" className="size-7">
|
|
@@ -519,6 +511,11 @@ export const AssistantBubble = ({
|
|
|
519
511
|
<TooltipContent>Open MLflow experiment</TooltipContent>
|
|
520
512
|
</Tooltip>
|
|
521
513
|
)}
|
|
514
|
+
{showResponseStatus && (
|
|
515
|
+
<div className="flex items-center text-muted-foreground">
|
|
516
|
+
<AssistantResponseSpinner />
|
|
517
|
+
</div>
|
|
518
|
+
)}
|
|
522
519
|
</div>
|
|
523
520
|
)}
|
|
524
521
|
<SuggestionPills questions={suggestions} onSelect={onSuggestionClick} className="mt-1" />
|
|
@@ -527,6 +524,7 @@ export const AssistantBubble = ({
|
|
|
527
524
|
);
|
|
528
525
|
};
|
|
529
526
|
|
|
527
|
+
/** Render one user message, including text and image attachments. */
|
|
530
528
|
export const UserBubble = ({ message }: { message: UIMessage }) => {
|
|
531
529
|
const text = message.parts
|
|
532
530
|
.filter((p): p is { type: "text"; text: string } => p.type === "text")
|
|
@@ -423,7 +423,7 @@ export const ChatComposer = ({
|
|
|
423
423
|
}}
|
|
424
424
|
placeholder="Send a message..."
|
|
425
425
|
rows={1}
|
|
426
|
-
className="field-sizing-fixed min-h-10 max-h-64 w-full flex-none overflow-y-auto px-4 pb-2 pt-4 text-
|
|
426
|
+
className="field-sizing-fixed min-h-10 max-h-64 w-full flex-none overflow-y-auto px-4 pb-2 pt-4 text-sm"
|
|
427
427
|
/>
|
|
428
428
|
{fileError && (
|
|
429
429
|
<div className="px-4 text-xs text-destructive" role="alert">
|
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
} from "./chat-sessions.ts";
|
|
10
10
|
import type { PendingApproval } from "./types.ts";
|
|
11
11
|
import type { MastraPluginClient } from "../support/mastra-client.ts";
|
|
12
|
+
import { isSessionRunning } from "../support/thread-sessions.ts";
|
|
12
13
|
|
|
13
14
|
const HISTORY_PAGE_SIZE = 20;
|
|
14
15
|
const logger = log.logger("ui-mastra/chat");
|
|
@@ -115,7 +116,7 @@ export function useChatHistory({
|
|
|
115
116
|
.catch((error: unknown) => {
|
|
116
117
|
if ((error as { name?: string }).name === "AbortError") throw error;
|
|
117
118
|
logger.warn("suspended-run discovery error", {
|
|
118
|
-
error: errorUtils.
|
|
119
|
+
error: errorUtils.toError(error),
|
|
119
120
|
});
|
|
120
121
|
return { runs: [], total: 0 };
|
|
121
122
|
});
|
|
@@ -126,7 +127,9 @@ export function useChatHistory({
|
|
|
126
127
|
const messages = toChronologicalUiMessages(response);
|
|
127
128
|
updateSession(threadId, (current) => ({
|
|
128
129
|
...current,
|
|
129
|
-
messages
|
|
130
|
+
messages: isSessionRunning(current)
|
|
131
|
+
? mergeCurrentMessages(messages, current.messages)
|
|
132
|
+
: messages,
|
|
130
133
|
historyLoaded: true,
|
|
131
134
|
hasMoreHistory: response.hasMore ?? false,
|
|
132
135
|
historyPage: 1,
|
|
@@ -137,7 +140,7 @@ export function useChatHistory({
|
|
|
137
140
|
.catch((error: unknown) => {
|
|
138
141
|
if (cancelled || (error as { name?: string }).name === "AbortError") return;
|
|
139
142
|
logger.error("history load error", {
|
|
140
|
-
error: errorUtils.
|
|
143
|
+
error: errorUtils.toError(error),
|
|
141
144
|
});
|
|
142
145
|
updateSession(threadId, (current) => ({
|
|
143
146
|
...current,
|
|
@@ -178,7 +181,7 @@ export function useChatHistory({
|
|
|
178
181
|
.catch((error: unknown) => {
|
|
179
182
|
logger.error("history load-more error", {
|
|
180
183
|
page,
|
|
181
|
-
error: errorUtils.
|
|
184
|
+
error: errorUtils.toError(error),
|
|
182
185
|
});
|
|
183
186
|
updateSession(threadId, (current) => ({
|
|
184
187
|
...current,
|
|
@@ -203,3 +206,8 @@ export function useChatHistory({
|
|
|
203
206
|
loadOlderHistory,
|
|
204
207
|
};
|
|
205
208
|
}
|
|
209
|
+
|
|
210
|
+
function mergeCurrentMessages(history: UIMessage[], current: UIMessage[]): UIMessage[] {
|
|
211
|
+
const ids = new Set(history.map((message) => message.id));
|
|
212
|
+
return [...history, ...current.filter((message) => !ids.has(message.id))];
|
|
213
|
+
}
|
|
@@ -8,13 +8,16 @@ import {
|
|
|
8
8
|
type ThreadSession,
|
|
9
9
|
} from "../support/thread-sessions.ts";
|
|
10
10
|
|
|
11
|
+
/** Read or lazily create the mutable session state for one thread. */
|
|
11
12
|
export type ThreadSessionReader = (threadId: string) => ThreadSession;
|
|
12
13
|
|
|
14
|
+
/** Replace one thread session through a state-producing callback. */
|
|
13
15
|
export type ThreadSessionUpdater = (
|
|
14
16
|
threadId: string,
|
|
15
17
|
updater: (session: ThreadSession) => ThreadSession,
|
|
16
18
|
) => void;
|
|
17
19
|
|
|
20
|
+
/** Replace the transcript stored for one thread. */
|
|
18
21
|
export type ThreadMessageWriter = (threadId: string, messages: UIMessage[]) => void;
|
|
19
22
|
|
|
20
23
|
/** Own the mutable per-thread session registry behind the public chat hook. */
|
package/src/react/chat-stream.ts
CHANGED
|
@@ -127,7 +127,7 @@ export function useChatStream({ getSession, updateSession, writeMessages }: UseC
|
|
|
127
127
|
if (error instanceof StreamAborted || signal.aborted) return;
|
|
128
128
|
if (isStaleMastraResumeError(error)) {
|
|
129
129
|
logger.warn("ignored stale mastra resume", {
|
|
130
|
-
error: errorUtils.
|
|
130
|
+
error: errorUtils.toError(error),
|
|
131
131
|
});
|
|
132
132
|
return;
|
|
133
133
|
}
|
|
@@ -38,6 +38,8 @@ type ChatThreadLayoutProps = {
|
|
|
38
38
|
threads: ChatViewProps["threads"];
|
|
39
39
|
threadPlacement: NonNullable<ChatViewProps["threadPlacement"]>;
|
|
40
40
|
threadBarActions: ChatViewProps["threadBarActions"];
|
|
41
|
+
threadHistoryLeadingActions: ChatViewProps["threadHistoryLeadingActions"];
|
|
42
|
+
threadSidebarHeaderActions: ChatViewProps["threadSidebarHeaderActions"];
|
|
41
43
|
showThreadBarActions: NonNullable<ChatViewProps["showThreadBarActions"]>;
|
|
42
44
|
threadNewIcon: ChatViewProps["threadNewIcon"];
|
|
43
45
|
threadHistoryIcon: ChatViewProps["threadHistoryIcon"];
|
|
@@ -92,6 +94,8 @@ export const ChatThreadLayout = ({
|
|
|
92
94
|
threads,
|
|
93
95
|
threadPlacement,
|
|
94
96
|
threadBarActions,
|
|
97
|
+
threadHistoryLeadingActions,
|
|
98
|
+
threadSidebarHeaderActions,
|
|
95
99
|
showThreadBarActions,
|
|
96
100
|
threadNewIcon,
|
|
97
101
|
threadHistoryIcon,
|
|
@@ -144,6 +148,7 @@ export const ChatThreadLayout = ({
|
|
|
144
148
|
...(onDeleteThread ? { onDelete: onDeleteThread } : {}),
|
|
145
149
|
...(onRenameThread ? { onRename: onRenameThread } : {}),
|
|
146
150
|
...(onCancelThread ? { onCancel: onCancelThread } : {}),
|
|
151
|
+
...(threadSidebarHeaderActions ? { headerActions: threadSidebarHeaderActions } : {}),
|
|
147
152
|
};
|
|
148
153
|
|
|
149
154
|
const [openTabIds, setOpenTabIds] = useState<string[]>([]);
|
|
@@ -195,6 +200,7 @@ export const ChatThreadLayout = ({
|
|
|
195
200
|
showActions={showThreadBarActions}
|
|
196
201
|
newIcon={threadNewIcon}
|
|
197
202
|
historyIcon={threadHistoryIcon}
|
|
203
|
+
historyLeadingActions={threadHistoryLeadingActions}
|
|
198
204
|
/>
|
|
199
205
|
)}
|
|
200
206
|
{showSidebarToggle && (
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
EmptyTitle,
|
|
12
12
|
Spinner,
|
|
13
13
|
} from "@dbx-tools/ui/react";
|
|
14
|
+
import { isToolUIPart } from "ai";
|
|
14
15
|
import { ArrowDownIcon, MessageSquareIcon, RefreshCwIcon, TriangleAlertIcon } from "lucide-react";
|
|
15
16
|
import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
16
17
|
import { AssistantBubble, UserBubble } from "./bubbles.tsx";
|
|
@@ -185,8 +186,10 @@ export const ChatTranscript = ({
|
|
|
185
186
|
const lastAssistantHasContent =
|
|
186
187
|
lastAssistantParts.some(
|
|
187
188
|
(part) =>
|
|
188
|
-
(part.type === "text" || part.type === "reasoning") &&
|
|
189
|
-
|
|
189
|
+
((part.type === "text" || part.type === "reasoning") &&
|
|
190
|
+
Boolean((part as { text?: string }).text)) ||
|
|
191
|
+
isToolUIPart(part) ||
|
|
192
|
+
part.type === "data-tool-call-approval",
|
|
190
193
|
) || (lastEvents?.length ?? 0) > 0;
|
|
191
194
|
const showWaitingIndicator = isRunning && !lastAssistantHasContent;
|
|
192
195
|
|
|
@@ -255,9 +258,13 @@ export const ChatTranscript = ({
|
|
|
255
258
|
return <UserBubble key={message.id} message={message} />;
|
|
256
259
|
})}
|
|
257
260
|
{showWaitingIndicator && (
|
|
258
|
-
<div
|
|
261
|
+
<div
|
|
262
|
+
role="status"
|
|
263
|
+
aria-live="polite"
|
|
264
|
+
className="flex h-7 items-center px-3 text-muted-foreground"
|
|
265
|
+
>
|
|
259
266
|
<Spinner className="size-3" />
|
|
260
|
-
<span className="
|
|
267
|
+
<span className="sr-only">Response in progress</span>
|
|
261
268
|
</div>
|
|
262
269
|
)}
|
|
263
270
|
{status === "error" && (
|
package/src/react/chat-view.tsx
CHANGED
|
@@ -45,6 +45,8 @@ export const ChatView = ({
|
|
|
45
45
|
threads,
|
|
46
46
|
threadPlacement = "auto",
|
|
47
47
|
threadBarActions,
|
|
48
|
+
threadHistoryLeadingActions,
|
|
49
|
+
threadSidebarHeaderActions,
|
|
48
50
|
showThreadBarActions = true,
|
|
49
51
|
threadNewIcon,
|
|
50
52
|
threadHistoryIcon,
|
|
@@ -79,6 +81,8 @@ export const ChatView = ({
|
|
|
79
81
|
threads={threads}
|
|
80
82
|
threadPlacement={threadPlacement}
|
|
81
83
|
threadBarActions={threadBarActions}
|
|
84
|
+
threadHistoryLeadingActions={threadHistoryLeadingActions}
|
|
85
|
+
threadSidebarHeaderActions={threadSidebarHeaderActions}
|
|
82
86
|
showThreadBarActions={showThreadBarActions}
|
|
83
87
|
threadNewIcon={threadNewIcon}
|
|
84
88
|
threadHistoryIcon={threadHistoryIcon}
|
package/src/react/data-grid.tsx
CHANGED
|
@@ -50,6 +50,7 @@ export type DataRow = Record<string, unknown>;
|
|
|
50
50
|
*/
|
|
51
51
|
const DELTA_PATTERN = /^([+\u2212-])\s*\d[\d,.\s]*(?:%|\s*pts?)?$/i;
|
|
52
52
|
|
|
53
|
+
/** Apply success or destructive text color to a signed numeric delta string. */
|
|
53
54
|
export function colorizeDelta(content: React.ReactNode): React.ReactNode {
|
|
54
55
|
if (typeof content !== "string") return content;
|
|
55
56
|
const text = content.trim();
|