@dbx-tools/ui-mastra 0.9.120 → 0.9.122
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/index.ts +1 -1
- package/package.json +6 -6
- package/src/react/_external-link-dialog.tsx +16 -0
- package/src/react/bubbles.tsx +8 -3
- package/src/react/chat-transcript.tsx +3 -3
- package/src/react/markdown.tsx +11 -9
- package/src/react/mastra-chat.tsx +33 -12
- package/src/react/thread-sidebar.tsx +6 -4
- package/src/react/thread-tabs.tsx +30 -4
- package/src/react/tool-pill.tsx +58 -27
- package/src/support/mastra-client.ts +5 -2
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.122";
|
|
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";
|
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.122",
|
|
33
|
+
"@dbx-tools/shared-genie": "^0.9.122",
|
|
34
|
+
"@dbx-tools/shared-mastra": "^0.9.122",
|
|
35
|
+
"@dbx-tools/shared-model": "^0.9.122",
|
|
36
|
+
"@dbx-tools/ui": "^0.9.122",
|
|
37
37
|
"@mastra/client-js": "1.50.0",
|
|
38
38
|
"@mastra/core": "1.71.0",
|
|
39
39
|
"@tanstack/react-table": "^8.21.3",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"publishConfig": {
|
|
51
51
|
"access": "public"
|
|
52
52
|
},
|
|
53
|
-
"version": "0.9.
|
|
53
|
+
"version": "0.9.122",
|
|
54
54
|
"dbxToolsConfig": {
|
|
55
55
|
"tags": [
|
|
56
56
|
"ui"
|
|
@@ -20,6 +20,22 @@ export interface LinkPresentation {
|
|
|
20
20
|
absoluteUrl: string;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/** Return whether a chat URL belongs to the current app's Sandbox route space. */
|
|
24
|
+
export function isSandboxLink(url: string, baseUrl?: string): boolean {
|
|
25
|
+
const base = baseUrl ?? (typeof window === "undefined" ? undefined : window.location.href);
|
|
26
|
+
if (!base) return false;
|
|
27
|
+
try {
|
|
28
|
+
const resolved = new URL(url, base);
|
|
29
|
+
const current = new URL(base);
|
|
30
|
+
return (
|
|
31
|
+
resolved.origin === current.origin &&
|
|
32
|
+
(resolved.pathname === "/sandbox" || resolved.pathname.startsWith("/sandbox/"))
|
|
33
|
+
);
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
23
39
|
/**
|
|
24
40
|
* Resolve a chat link for display and copying.
|
|
25
41
|
*
|
package/src/react/bubbles.tsx
CHANGED
|
@@ -407,14 +407,14 @@ export const AssistantBubble = ({
|
|
|
407
407
|
// nothing so the prose flows unaffected. Suggested questions
|
|
408
408
|
// stay gated on settle to avoid pop-in mid-stream.
|
|
409
409
|
return (
|
|
410
|
-
<Item className="items-start gap-0 border-none bg-transparent p-0">
|
|
410
|
+
<Item className="min-w-0 max-w-full items-start gap-0 overflow-hidden border-none bg-transparent p-0">
|
|
411
411
|
{/*
|
|
412
412
|
* `min-w-0` is required so wide content (markdown tables, long
|
|
413
413
|
* code blocks) can shrink to fit instead of pushing the whole
|
|
414
414
|
* bubble past the chat's `max-w-4xl`. Flex children default to
|
|
415
415
|
* `min-width: auto`, which sizes to content and overflows.
|
|
416
416
|
*/}
|
|
417
|
-
<ItemContent className="min-w-0 gap-1.5">
|
|
417
|
+
<ItemContent className="min-w-0 max-w-full overflow-hidden gap-1.5">
|
|
418
418
|
{/* Tool calls and their thought events stay in arrival order above the answer text. */}
|
|
419
419
|
{activityItems.map((item) =>
|
|
420
420
|
item.type === "tool" ? (
|
|
@@ -551,7 +551,12 @@ export const UserBubble = ({ message }: { message: UIMessage }) => {
|
|
|
551
551
|
</div>
|
|
552
552
|
)}
|
|
553
553
|
{text && (
|
|
554
|
-
<div
|
|
554
|
+
<div
|
|
555
|
+
className={cn(
|
|
556
|
+
"whitespace-pre-wrap break-words px-1 leading-snug sm:leading-normal",
|
|
557
|
+
files.length > 0 && "pt-2",
|
|
558
|
+
)}
|
|
559
|
+
>
|
|
555
560
|
{text}
|
|
556
561
|
</div>
|
|
557
562
|
)}
|
|
@@ -194,11 +194,11 @@ export const ChatTranscript = ({
|
|
|
194
194
|
const showWaitingIndicator = isRunning && !lastAssistantHasContent;
|
|
195
195
|
|
|
196
196
|
return (
|
|
197
|
-
<div className="relative flex flex-1 flex-col overflow-hidden">
|
|
197
|
+
<div className="relative flex min-w-0 max-w-full flex-1 flex-col overflow-hidden">
|
|
198
198
|
<div
|
|
199
199
|
ref={controller.scrollRef}
|
|
200
200
|
onScroll={controller.handleScroll}
|
|
201
|
-
className="flex-1 overflow-y-auto overflow-x-
|
|
201
|
+
className="min-w-0 max-w-full flex-1 overflow-y-auto overflow-x-clip overscroll-contain [overflow-anchor:none] [scrollbar-gutter:stable]"
|
|
202
202
|
>
|
|
203
203
|
{messages.length === 0 && !isLoadingHistory ? (
|
|
204
204
|
<Empty className="mx-auto h-full w-full max-w-4xl px-4 md:px-6">
|
|
@@ -217,7 +217,7 @@ export const ChatTranscript = ({
|
|
|
217
217
|
) : (
|
|
218
218
|
<div
|
|
219
219
|
ref={controller.contentRef}
|
|
220
|
-
className="mx-auto flex w-full max-w-4xl flex-col gap-3 px-4 py-3 md:px-6"
|
|
220
|
+
className="mx-auto flex min-w-0 w-full max-w-4xl flex-col gap-3 overflow-x-clip px-4 py-3 md:px-6"
|
|
221
221
|
>
|
|
222
222
|
{(isLoadingMore || isLoadingHistory) && (
|
|
223
223
|
<div className="flex items-center justify-center gap-2 py-1 text-xs text-muted-foreground">
|
package/src/react/markdown.tsx
CHANGED
|
@@ -18,7 +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
|
+
import { ExternalLinkDialog, isSandboxLink } from "./_external-link-dialog.tsx";
|
|
22
22
|
import { DataGrid, TABLE_WRAPPER_CLASSES, colorizeDelta, type DataRow } from "./data-grid.tsx";
|
|
23
23
|
import { copyText } from "../support/clipboard.ts";
|
|
24
24
|
|
|
@@ -187,6 +187,7 @@ const SHIKI_PLUGIN = { code: createShikiPlugin() };
|
|
|
187
187
|
/** Keep Streamdown's link confirmation while matching the chat's AppKit UI. */
|
|
188
188
|
const LINK_SAFETY = {
|
|
189
189
|
enabled: true,
|
|
190
|
+
onLinkCheck: isSandboxLink,
|
|
190
191
|
renderModal: (props: React.ComponentProps<typeof ExternalLinkDialog>) => (
|
|
191
192
|
<ExternalLinkDialog {...props} />
|
|
192
193
|
),
|
|
@@ -248,10 +249,11 @@ export const AssistantMarkdown = ({
|
|
|
248
249
|
animated={animate ? ANIMATE_OPTIONS : false}
|
|
249
250
|
isAnimating={animate}
|
|
250
251
|
className={cn(
|
|
251
|
-
"prose prose-sm dark:prose-invert max-w-
|
|
252
|
-
"leading-
|
|
252
|
+
"prose prose-sm dark:prose-invert min-w-0 max-w-full overflow-x-hidden break-words",
|
|
253
|
+
"leading-snug prose-p:my-1.5 prose-p:leading-snug sm:leading-relaxed sm:prose-p:leading-relaxed",
|
|
253
254
|
"prose-ul:my-1.5 prose-ol:my-1.5 prose-li:my-0.5",
|
|
254
255
|
"prose-headings:font-semibold prose-headings:tracking-tight",
|
|
256
|
+
"prose-a:font-medium prose-a:text-primary prose-a:underline prose-a:decoration-primary/50 prose-a:underline-offset-4 hover:prose-a:decoration-primary",
|
|
255
257
|
"prose-h1:text-lg prose-h1:mt-4 prose-h1:mb-2",
|
|
256
258
|
"prose-h2:text-base prose-h2:mt-4 prose-h2:mb-2",
|
|
257
259
|
"prose-h3:text-sm prose-h3:mt-3 prose-h3:mb-1.5 prose-h3:text-muted-foreground prose-h3:uppercase prose-h3:tracking-wider",
|
|
@@ -281,11 +283,11 @@ export const ToolMarkdown = ({ children, className }: { children: string; classN
|
|
|
281
283
|
controls={false}
|
|
282
284
|
linkSafety={LINK_SAFETY}
|
|
283
285
|
className={cn(
|
|
284
|
-
"prose prose-sm dark:prose-invert max-w-
|
|
285
|
-
"text-xs leading-
|
|
286
|
-
"prose-p:my-0.5 prose-p:leading-snug [&_p]:!leading-snug",
|
|
286
|
+
"prose prose-sm dark:prose-invert min-w-0 max-w-full overflow-x-hidden break-words",
|
|
287
|
+
"text-xs leading-tight text-muted-foreground sm:leading-snug",
|
|
288
|
+
"prose-p:my-0.5 prose-p:leading-tight [&_p]:!leading-tight sm:prose-p:leading-snug sm:[&_p]:!leading-snug",
|
|
287
289
|
"prose-ul:my-0.5 prose-ul:pl-4 prose-ol:my-0.5 prose-ol:pl-4",
|
|
288
|
-
"prose-li:my-0 prose-li:leading-
|
|
290
|
+
"prose-li:my-0 prose-li:leading-tight prose-li:marker:text-muted-foreground/60 sm:prose-li:leading-snug",
|
|
289
291
|
"prose-headings:my-1 prose-headings:text-xs prose-headings:font-semibold",
|
|
290
292
|
"prose-strong:text-foreground/90 prose-strong:font-medium",
|
|
291
293
|
"prose-code:text-xs prose-code:font-medium",
|
|
@@ -379,7 +381,7 @@ export const SqlBlock = ({ sql }: { sql: string }) => {
|
|
|
379
381
|
value={formatted}
|
|
380
382
|
className="absolute right-1.5 top-1.5 z-10 bg-background/70 opacity-0 backdrop-blur transition-opacity group-hover:opacity-100 focus-visible:opacity-100"
|
|
381
383
|
/>
|
|
382
|
-
<pre className="max-w-full overflow-x-auto rounded-md border border-border bg-background p-3 font-mono text-xs leading-relaxed">
|
|
384
|
+
<pre className="max-w-full overflow-x-auto rounded-md border border-border bg-background p-3 font-mono text-xs leading-tight sm:leading-relaxed">
|
|
383
385
|
{html === null ? (
|
|
384
386
|
<code>{formatted}</code>
|
|
385
387
|
) : (
|
|
@@ -404,7 +406,7 @@ export const SourceBlock = ({
|
|
|
404
406
|
return (
|
|
405
407
|
<pre
|
|
406
408
|
className={cn(
|
|
407
|
-
"max-w-full overflow-auto whitespace-pre font-mono text-xs leading-snug",
|
|
409
|
+
"max-w-full overflow-auto whitespace-pre font-mono text-xs leading-tight sm:leading-snug",
|
|
408
410
|
className,
|
|
409
411
|
)}
|
|
410
412
|
>
|
|
@@ -356,6 +356,9 @@ export const useMastraChat = <TValues extends Record<string, unknown> = Record<s
|
|
|
356
356
|
// as soon as the server's auto-generated title lands (see the prune
|
|
357
357
|
// effect below). A manual rename (`renamedThreads`) still wins.
|
|
358
358
|
const [provisionalTitles, setProvisionalTitles] = useState<Record<string, string>>({});
|
|
359
|
+
// Threads the user just deleted. The server list can lag, so hide them
|
|
360
|
+
// immediately and drop the overlay once the listing no longer returns them.
|
|
361
|
+
const [removedThreadIds, setRemovedThreadIds] = useState<ReadonlySet<string>>(() => new Set());
|
|
359
362
|
// Surface the active thread in the sidebar immediately (called when its
|
|
360
363
|
// first message is sent). Upserts an untitled row stamped "now" so it
|
|
361
364
|
// sorts to the top; the server row replaces it on the next refresh.
|
|
@@ -880,15 +883,7 @@ export const useMastraChat = <TValues extends Record<string, unknown> = Record<s
|
|
|
880
883
|
|
|
881
884
|
const deleteThread = useCallback(
|
|
882
885
|
async (threadId: string) => {
|
|
883
|
-
|
|
884
|
-
await mastraClient.removeThread(threadId, { agentId });
|
|
885
|
-
logger.info("thread deleted", { threadId });
|
|
886
|
-
} catch (error) {
|
|
887
|
-
logger.error("thread delete error", {
|
|
888
|
-
threadId,
|
|
889
|
-
error: errorUtils.toError(error),
|
|
890
|
-
});
|
|
891
|
-
}
|
|
886
|
+
setRemovedThreadIds((prev) => new Set(prev).add(threadId));
|
|
892
887
|
removeSession(threadId);
|
|
893
888
|
setOptimisticThreads((prev) => {
|
|
894
889
|
if (!prev[threadId]) return prev;
|
|
@@ -905,7 +900,22 @@ export const useMastraChat = <TValues extends Record<string, unknown> = Record<s
|
|
|
905
900
|
resetSession(id, true);
|
|
906
901
|
setActiveThreadId(id);
|
|
907
902
|
}
|
|
908
|
-
|
|
903
|
+
try {
|
|
904
|
+
await mastraClient.removeThread(threadId, { agentId });
|
|
905
|
+
logger.info("thread deleted", { threadId });
|
|
906
|
+
refreshThreads();
|
|
907
|
+
} catch (error) {
|
|
908
|
+
logger.error("thread delete error", {
|
|
909
|
+
threadId,
|
|
910
|
+
error: errorUtils.toError(error),
|
|
911
|
+
});
|
|
912
|
+
setRemovedThreadIds((prev) => {
|
|
913
|
+
if (!prev.has(threadId)) return prev;
|
|
914
|
+
const next = new Set(prev);
|
|
915
|
+
next.delete(threadId);
|
|
916
|
+
return next;
|
|
917
|
+
});
|
|
918
|
+
}
|
|
909
919
|
},
|
|
910
920
|
[activeThreadId, agentId, mastraClient, refreshThreads, removeSession, resetSession],
|
|
911
921
|
);
|
|
@@ -1094,10 +1104,21 @@ export const useMastraChat = <TValues extends Record<string, unknown> = Record<s
|
|
|
1094
1104
|
.filter((t) => !serverIds.has(t.id))
|
|
1095
1105
|
.map(withOverlay)
|
|
1096
1106
|
.sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? ""));
|
|
1097
|
-
return [...pending, ...server];
|
|
1098
|
-
}, [threads, optimisticThreads, renamedThreads, provisionalTitles]);
|
|
1107
|
+
return [...pending, ...server].filter((thread) => !removedThreadIds.has(thread.id));
|
|
1108
|
+
}, [threads, optimisticThreads, renamedThreads, provisionalTitles, removedThreadIds]);
|
|
1099
1109
|
// Once the server list includes a thread we were tracking optimistically,
|
|
1100
1110
|
// drop the optimistic copy so the map doesn't grow without bound.
|
|
1111
|
+
useEffect(() => {
|
|
1112
|
+
if (removedThreadIds.size === 0) return;
|
|
1113
|
+
const serverIds = new Set(threads.map((t) => t.id));
|
|
1114
|
+
const settled = [...removedThreadIds].filter((id) => !serverIds.has(id));
|
|
1115
|
+
if (settled.length === 0) return;
|
|
1116
|
+
setRemovedThreadIds((prev) => {
|
|
1117
|
+
const next = new Set(prev);
|
|
1118
|
+
for (const id of settled) next.delete(id);
|
|
1119
|
+
return next;
|
|
1120
|
+
});
|
|
1121
|
+
}, [threads, removedThreadIds]);
|
|
1101
1122
|
useEffect(() => {
|
|
1102
1123
|
const serverIds = new Set(threads.map((t) => t.id));
|
|
1103
1124
|
const stale = Object.keys(optimisticThreads).filter((id) => serverIds.has(id));
|
|
@@ -100,6 +100,7 @@ export const ThreadSidebar = ({
|
|
|
100
100
|
const [draftTitle, setDraftTitle] = useState("");
|
|
101
101
|
|
|
102
102
|
const handleDeleteClick = (e: React.MouseEvent, threadId: string) => {
|
|
103
|
+
e.preventDefault();
|
|
103
104
|
e.stopPropagation();
|
|
104
105
|
if (!onDelete) return;
|
|
105
106
|
if (confirmDeleteId !== threadId) {
|
|
@@ -229,7 +230,10 @@ export const ThreadSidebar = ({
|
|
|
229
230
|
>
|
|
230
231
|
<button
|
|
231
232
|
type="button"
|
|
232
|
-
onClick={() =>
|
|
233
|
+
onClick={() => {
|
|
234
|
+
setConfirmDeleteId(null);
|
|
235
|
+
onSelect(thread.id);
|
|
236
|
+
}}
|
|
233
237
|
aria-current={isActive ? "page" : undefined}
|
|
234
238
|
className={cn(
|
|
235
239
|
"min-w-0 flex-1 cursor-pointer text-left transition-[padding] duration-150",
|
|
@@ -309,10 +313,8 @@ export const ThreadSidebar = ({
|
|
|
309
313
|
type="button"
|
|
310
314
|
variant={isConfirming ? "destructive" : "ghost"}
|
|
311
315
|
size="icon"
|
|
316
|
+
onMouseDown={(e) => e.preventDefault()}
|
|
312
317
|
onClick={(e) => handleDeleteClick(e, thread.id)}
|
|
313
|
-
onBlur={() =>
|
|
314
|
-
setConfirmDeleteId((id) => (id === thread.id ? null : id))
|
|
315
|
-
}
|
|
316
318
|
aria-label={
|
|
317
319
|
isConfirming
|
|
318
320
|
? "Confirm delete conversation"
|
|
@@ -89,7 +89,7 @@ export const ThreadActions = ({
|
|
|
89
89
|
</Tooltip>
|
|
90
90
|
)}
|
|
91
91
|
{historyLeadingActions}
|
|
92
|
-
<Popover open={historyOpen} onOpenChange={setHistoryOpen}>
|
|
92
|
+
<Popover open={historyOpen} onOpenChange={setHistoryOpen} modal>
|
|
93
93
|
<Tooltip>
|
|
94
94
|
<TooltipTrigger asChild>
|
|
95
95
|
<PopoverTrigger asChild>
|
|
@@ -106,7 +106,23 @@ export const ThreadActions = ({
|
|
|
106
106
|
</TooltipTrigger>
|
|
107
107
|
<TooltipContent>Conversation history</TooltipContent>
|
|
108
108
|
</Tooltip>
|
|
109
|
-
<PopoverContent
|
|
109
|
+
<PopoverContent
|
|
110
|
+
align="end"
|
|
111
|
+
aria-label="Conversation history"
|
|
112
|
+
className="w-72 p-0"
|
|
113
|
+
onOpenAutoFocus={(event) => event.preventDefault()}
|
|
114
|
+
onCloseAutoFocus={(event) => event.preventDefault()}
|
|
115
|
+
onFocusOutside={(event) => event.preventDefault()}
|
|
116
|
+
onPointerDownOutside={(event) => {
|
|
117
|
+
const target = event.target;
|
|
118
|
+
if (
|
|
119
|
+
target instanceof Element &&
|
|
120
|
+
target.closest("[data-radix-popper-content-wrapper]")
|
|
121
|
+
) {
|
|
122
|
+
event.preventDefault();
|
|
123
|
+
}
|
|
124
|
+
}}
|
|
125
|
+
>
|
|
110
126
|
<ThreadSidebar
|
|
111
127
|
{...list}
|
|
112
128
|
onSelect={(id) => {
|
|
@@ -151,9 +167,19 @@ export const ThreadTabs = ({
|
|
|
151
167
|
...list
|
|
152
168
|
}: ThreadTabsProps) => {
|
|
153
169
|
const { threads, activeThreadId, streamingThreadIds = [], onSelect } = list;
|
|
170
|
+
const tabStripRef = useRef<HTMLDivElement | null>(null);
|
|
154
171
|
const activeTabRef = useRef<HTMLDivElement | null>(null);
|
|
155
172
|
useEffect(() => {
|
|
156
|
-
|
|
173
|
+
const container = tabStripRef.current;
|
|
174
|
+
const activeTab = activeTabRef.current;
|
|
175
|
+
if (!container || !activeTab) return;
|
|
176
|
+
const containerRect = container.getBoundingClientRect();
|
|
177
|
+
const tabRect = activeTab.getBoundingClientRect();
|
|
178
|
+
if (tabRect.left < containerRect.left) {
|
|
179
|
+
container.scrollBy({ left: tabRect.left - containerRect.left, behavior: "smooth" });
|
|
180
|
+
} else if (tabRect.right > containerRect.right) {
|
|
181
|
+
container.scrollBy({ left: tabRect.right - containerRect.right, behavior: "smooth" });
|
|
182
|
+
}
|
|
157
183
|
}, [activeThreadId]);
|
|
158
184
|
|
|
159
185
|
return (
|
|
@@ -161,7 +187,7 @@ export const ThreadTabs = ({
|
|
|
161
187
|
// in the same centered column as the transcript and composer.
|
|
162
188
|
<div className={cn("w-full border-b border-border text-xs", className)}>
|
|
163
189
|
<div className="mx-auto flex w-full max-w-4xl items-center gap-1 px-2 py-1 md:px-4">
|
|
164
|
-
<div className="flex min-w-0 flex-1 items-center gap-1 overflow-x-auto">
|
|
190
|
+
<div ref={tabStripRef} className="flex min-w-0 flex-1 items-center gap-1 overflow-x-auto">
|
|
165
191
|
{openThreadIds.map((id) => {
|
|
166
192
|
const thread = threads.find((t) => t.id === id) ?? { id };
|
|
167
193
|
const isActive = id === activeThreadId;
|
package/src/react/tool-pill.tsx
CHANGED
|
@@ -61,6 +61,10 @@ export const humanizeToolName = (toolName: string): string => {
|
|
|
61
61
|
* server-side label generator.
|
|
62
62
|
*/
|
|
63
63
|
const runningLabelFor = (event: ToolEvent): string => {
|
|
64
|
+
const toolStatus = [...(event.progress ?? [])]
|
|
65
|
+
.reverse()
|
|
66
|
+
.find((p): p is Extract<ToolProgress, { type: "tool_status" }> => p.type === "tool_status");
|
|
67
|
+
if (toolStatus?.message.trim()) return toolStatus.message.trim();
|
|
64
68
|
const latest = [...(event.progress ?? [])]
|
|
65
69
|
.reverse()
|
|
66
70
|
.find((p): p is Extract<ToolProgress, { type: "status" }> => p.type === "status");
|
|
@@ -285,7 +289,7 @@ const MessageGroupBody = ({
|
|
|
285
289
|
return (
|
|
286
290
|
<div className="flex flex-col gap-1.5">
|
|
287
291
|
{!omitQuestion && group.question && (
|
|
288
|
-
<div className="rounded border-l-2 border-primary/40 bg-background/40 px-2 py-1 text-xs italic leading-
|
|
292
|
+
<div className="rounded border-l-2 border-primary/40 bg-background/40 px-2 py-1 text-xs italic leading-tight text-muted-foreground sm:leading-snug">
|
|
289
293
|
{group.question}
|
|
290
294
|
</div>
|
|
291
295
|
)}
|
|
@@ -309,7 +313,7 @@ const MessageGroupBody = ({
|
|
|
309
313
|
<SourceBlock
|
|
310
314
|
source={bucket.query.sql}
|
|
311
315
|
language="sql"
|
|
312
|
-
className="max-h-
|
|
316
|
+
className="max-h-48 overflow-auto overscroll-contain border-t border-border/50 px-2 py-1.5 text-foreground sm:max-h-64"
|
|
313
317
|
/>
|
|
314
318
|
{bucket.query.description && (
|
|
315
319
|
<div className="border-t border-border/50 px-2 py-1.5">
|
|
@@ -402,23 +406,25 @@ const RawToolPayload = ({
|
|
|
402
406
|
}) => {
|
|
403
407
|
const structured = structuredToolPayload(value);
|
|
404
408
|
return (
|
|
405
|
-
<Collapsible className="rounded border border-border/60 bg-background/40">
|
|
406
|
-
<CollapsibleTrigger className="group flex w-full items-center gap-1.5 px-2 py-1 text-left text-xs uppercase tracking-wide text-muted-foreground hover:text-foreground">
|
|
409
|
+
<Collapsible className="min-w-0 max-w-full overflow-hidden rounded border border-border/60 bg-background/40">
|
|
410
|
+
<CollapsibleTrigger className="group flex w-full min-w-0 items-center gap-1.5 px-2 py-1 text-left text-xs uppercase tracking-wide text-muted-foreground hover:text-foreground">
|
|
407
411
|
<ChevronDownIcon className="size-3 shrink-0 transition-transform group-data-[state=closed]:-rotate-90" />
|
|
408
412
|
<span>{label}</span>
|
|
409
413
|
</CollapsibleTrigger>
|
|
410
|
-
<CollapsibleContent>
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
414
|
+
<CollapsibleContent className="min-w-0 max-w-full overflow-hidden">
|
|
415
|
+
<div className="max-h-48 max-w-full overflow-auto overscroll-contain sm:max-h-64">
|
|
416
|
+
{structured === undefined ? (
|
|
417
|
+
<JsonBlock
|
|
418
|
+
json={formatRawToolPayload(value)}
|
|
419
|
+
className="border-t border-border/50 px-2 py-2 leading-tight text-foreground sm:leading-relaxed"
|
|
420
|
+
/>
|
|
421
|
+
) : (
|
|
422
|
+
<RecordPreview
|
|
423
|
+
value={structured}
|
|
424
|
+
className="max-w-full border-t border-border/50 bg-transparent text-foreground"
|
|
425
|
+
/>
|
|
426
|
+
)}
|
|
427
|
+
</div>
|
|
422
428
|
</CollapsibleContent>
|
|
423
429
|
</Collapsible>
|
|
424
430
|
);
|
|
@@ -488,8 +494,8 @@ export function toolOutputPresentation(
|
|
|
488
494
|
const ToolInputSource = ({ presentation }: { presentation: ToolInputPresentation }) => {
|
|
489
495
|
const lineCount = presentation.source.split("\n").length;
|
|
490
496
|
return (
|
|
491
|
-
<Collapsible className="rounded border border-border/60 bg-background/40">
|
|
492
|
-
<CollapsibleTrigger className="group flex w-full items-center gap-1.5 px-2 py-1 text-left text-xs text-muted-foreground hover:text-foreground">
|
|
497
|
+
<Collapsible className="min-w-0 max-w-full overflow-hidden rounded border border-border/60 bg-background/40">
|
|
498
|
+
<CollapsibleTrigger className="group flex w-full min-w-0 items-center gap-1.5 px-2 py-1 text-left text-xs text-muted-foreground hover:text-foreground">
|
|
493
499
|
<ChevronDownIcon className="size-3 shrink-0 transition-transform group-data-[state=closed]:-rotate-90" />
|
|
494
500
|
<span className="font-medium text-foreground/80">{presentation.label}</span>
|
|
495
501
|
<span className="rounded bg-muted px-1.5 py-0.5 text-[10px] uppercase leading-none tracking-wide">
|
|
@@ -499,11 +505,11 @@ const ToolInputSource = ({ presentation }: { presentation: ToolInputPresentation
|
|
|
499
505
|
{lineCount} {lineCount === 1 ? "line" : "lines"}
|
|
500
506
|
</span>
|
|
501
507
|
</CollapsibleTrigger>
|
|
502
|
-
<CollapsibleContent>
|
|
508
|
+
<CollapsibleContent className="min-w-0 max-w-full overflow-hidden">
|
|
503
509
|
<SourceBlock
|
|
504
510
|
source={presentation.source}
|
|
505
511
|
language={presentation.language}
|
|
506
|
-
className="max-h-
|
|
512
|
+
className="max-h-48 overflow-auto overscroll-contain border-t border-border/50 px-2 py-1.5 text-foreground sm:max-h-64"
|
|
507
513
|
/>
|
|
508
514
|
</CollapsibleContent>
|
|
509
515
|
</Collapsible>
|
|
@@ -586,6 +592,25 @@ const WebSearchProgressDetails = ({ event }: { event: ToolEvent }) => {
|
|
|
586
592
|
);
|
|
587
593
|
};
|
|
588
594
|
|
|
595
|
+
/** Latest cumulative detail emitted by a non-search tool. */
|
|
596
|
+
const ToolStatusProgressDetails = ({ event }: { event: ToolEvent }) => {
|
|
597
|
+
if (event.toolName === "web_search") return null;
|
|
598
|
+
const detail = [...(event.progress ?? [])]
|
|
599
|
+
.reverse()
|
|
600
|
+
.find(
|
|
601
|
+
(progress): progress is ToolStatusProgress =>
|
|
602
|
+
progress.type === "tool_status" && Boolean(progress.detail?.trim()),
|
|
603
|
+
)?.detail;
|
|
604
|
+
if (!detail) return null;
|
|
605
|
+
return (
|
|
606
|
+
<SourceBlock
|
|
607
|
+
source={detail}
|
|
608
|
+
language="text"
|
|
609
|
+
className="max-h-48 overflow-auto overscroll-contain whitespace-pre-wrap break-words rounded border border-border/50 bg-background/40 p-2 text-foreground sm:max-h-64"
|
|
610
|
+
/>
|
|
611
|
+
);
|
|
612
|
+
};
|
|
613
|
+
|
|
589
614
|
/**
|
|
590
615
|
* True when a tool event has anything worth expanding for. The
|
|
591
616
|
* question text already rides on the row header (see
|
|
@@ -619,7 +644,7 @@ const ToolCallDetails = ({
|
|
|
619
644
|
return (
|
|
620
645
|
<div className="flex flex-col gap-2">
|
|
621
646
|
{showQuestion && question ? (
|
|
622
|
-
<p className="break-words px-0.5 text-[11px] leading-
|
|
647
|
+
<p className="break-words px-0.5 text-[11px] leading-tight text-muted-foreground sm:leading-snug">
|
|
623
648
|
{question}
|
|
624
649
|
</p>
|
|
625
650
|
) : null}
|
|
@@ -629,6 +654,7 @@ const ToolCallDetails = ({
|
|
|
629
654
|
<RawToolPayload label="Request" value={event.input} />
|
|
630
655
|
) : null}
|
|
631
656
|
<WebSearchProgressDetails event={event} />
|
|
657
|
+
<ToolStatusProgressDetails event={event} />
|
|
632
658
|
<ToolProgressDetails summary={summary} omitQuestion />
|
|
633
659
|
{outputPresentation?.language === "json" ? (
|
|
634
660
|
<RawToolPayload label="Output" value={outputPresentation.source} />
|
|
@@ -676,7 +702,7 @@ const ToolCallRow = ({ event }: { event: ToolEvent }) => {
|
|
|
676
702
|
{verb}
|
|
677
703
|
</span>
|
|
678
704
|
{question && (
|
|
679
|
-
<span className="mt-0.5 block break-words text-[11px] leading-
|
|
705
|
+
<span className="mt-0.5 block break-words text-[11px] leading-tight text-foreground/75 sm:leading-snug">
|
|
680
706
|
{question}
|
|
681
707
|
</span>
|
|
682
708
|
)}
|
|
@@ -698,7 +724,7 @@ const ToolCallRow = ({ event }: { event: ToolEvent }) => {
|
|
|
698
724
|
}
|
|
699
725
|
|
|
700
726
|
return (
|
|
701
|
-
<Collapsible className="rounded border border-border/40 bg-background/30">
|
|
727
|
+
<Collapsible className="min-w-0 max-w-full overflow-hidden rounded border border-border/40 bg-background/30">
|
|
702
728
|
<CollapsibleTrigger className="group flex w-full items-start gap-2 px-2 py-1 text-left text-xs text-muted-foreground cursor-pointer hover:text-foreground">
|
|
703
729
|
<ChevronDownIcon className="mt-0.5 size-3 shrink-0 transition-transform group-data-[state=open]:rotate-180" />
|
|
704
730
|
<span className="mt-0.5 shrink-0">
|
|
@@ -707,7 +733,7 @@ const ToolCallRow = ({ event }: { event: ToolEvent }) => {
|
|
|
707
733
|
{header}
|
|
708
734
|
</CollapsibleTrigger>
|
|
709
735
|
<CollapsibleContent>
|
|
710
|
-
<div className="px-2 pb-2">
|
|
736
|
+
<div className="min-w-0 max-w-full overflow-hidden px-2 pb-2">
|
|
711
737
|
<ToolCallDetails event={event} />
|
|
712
738
|
</div>
|
|
713
739
|
</CollapsibleContent>
|
|
@@ -754,7 +780,7 @@ export const ToolSessionPill = ({ events }: { events: ToolEvent[] }) => {
|
|
|
754
780
|
return (
|
|
755
781
|
<div
|
|
756
782
|
className={cn(
|
|
757
|
-
"rounded-md border bg-background/30 px-2 py-1 transition-colors",
|
|
783
|
+
"min-w-0 max-w-full overflow-hidden rounded-md border bg-background/30 px-2 py-1 transition-colors",
|
|
758
784
|
// Loud-but-not-jarring "in flight" treatment when any row
|
|
759
785
|
// is running: primary border + soft ring. Failed sessions
|
|
760
786
|
// (no longer running) pick up the destructive border;
|
|
@@ -798,8 +824,13 @@ export const ToolSessionPill = ({ events }: { events: ToolEvent[] }) => {
|
|
|
798
824
|
</span>
|
|
799
825
|
)}
|
|
800
826
|
</CollapsibleTrigger>
|
|
801
|
-
<CollapsibleContent>
|
|
802
|
-
<div
|
|
827
|
+
<CollapsibleContent className="min-w-0 max-w-full overflow-hidden">
|
|
828
|
+
<div
|
|
829
|
+
className={cn(
|
|
830
|
+
"mt-1.5 min-w-0 max-w-full overflow-hidden",
|
|
831
|
+
events.length > 1 && "flex flex-col gap-1",
|
|
832
|
+
)}
|
|
833
|
+
>
|
|
803
834
|
{events.length === 1 ? (
|
|
804
835
|
<ToolCallDetails event={latest} showQuestion />
|
|
805
836
|
) : (
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
type ListMemoryThreadsResponse,
|
|
23
23
|
} from "@mastra/client-js";
|
|
24
24
|
import { DefaultChatTransport, type UIMessage, type UIMessageChunk } from "ai";
|
|
25
|
-
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
25
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
26
26
|
import type { MastraRequestContextSnapshot } from "./request-context.ts";
|
|
27
27
|
|
|
28
28
|
type NativeAgentClient = ReturnType<MastraClient["getAgent"]>;
|
|
@@ -916,19 +916,22 @@ export const useMastraThreads = (
|
|
|
916
916
|
// the natural deps (client / agentId / enabled).
|
|
917
917
|
const [nonce, setNonce] = useState(0);
|
|
918
918
|
const refresh = useCallback(() => setNonce((n) => n + 1), []);
|
|
919
|
+
const hasLoadedRef = useRef(false);
|
|
919
920
|
|
|
920
921
|
useEffect(() => {
|
|
921
922
|
if (!enabled) {
|
|
923
|
+
hasLoadedRef.current = false;
|
|
922
924
|
setLoading(false);
|
|
923
925
|
setThreads([]);
|
|
924
926
|
return;
|
|
925
927
|
}
|
|
926
928
|
const controller = new AbortController();
|
|
927
|
-
setLoading(true);
|
|
929
|
+
if (!hasLoadedRef.current) setLoading(true);
|
|
928
930
|
client
|
|
929
931
|
.threads({ agentId, signal: controller.signal })
|
|
930
932
|
.then((res) => {
|
|
931
933
|
if (controller.signal.aborted) return;
|
|
934
|
+
hasLoadedRef.current = true;
|
|
932
935
|
setThreads(res.threads);
|
|
933
936
|
setError(null);
|
|
934
937
|
})
|