@dbx-tools/ui-mastra 0.9.99 → 0.9.105
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 +17 -7
- package/index.ts +2 -2
- package/package.json +7 -7
- package/src/react/bubbles.tsx +13 -12
- package/src/react/chat-composer.tsx +1 -1
- package/src/react/chat-stream.ts +39 -2
- package/src/react/chat-transcript.tsx +1 -1
- package/src/react/markdown.tsx +12 -6
- package/src/react/mastra-chat.tsx +72 -9
- package/src/react/tool-pill.tsx +8 -10
- package/src/support/_shiki-runtime.ts +3 -2
- package/src/support/mastra-client.ts +71 -2
- package/src/support/model-selection.ts +13 -0
- package/src/support/shiki-plugin.ts +43 -19
- package/src/support/tool-events.ts +20 -8
package/README.md
CHANGED
|
@@ -2,11 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
React chat UI for the AppKit-Mastra plugin.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
Add a complete chat surface for
|
|
6
|
+
[`@dbx-tools/appkit-mastra`](../../node/appkit-mastra), including streaming,
|
|
7
|
+
model selection, Genie progress, charts, tables, approvals, history, threads,
|
|
8
|
+
export, and MLflow feedback.
|
|
9
|
+
|
|
10
|
+
## Quick Start
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import { MastraChat } from "@dbx-tools/ui-mastra/react";
|
|
14
|
+
import "@dbx-tools/ui-mastra/styles.css";
|
|
15
|
+
|
|
16
|
+
export function Assistant() {
|
|
17
|
+
return <MastraChat showModelPicker enableExport />;
|
|
18
|
+
}
|
|
19
|
+
```
|
|
10
20
|
|
|
11
21
|
Key features:
|
|
12
22
|
|
|
@@ -47,7 +57,7 @@ Key features:
|
|
|
47
57
|
- Export menu for PDF and Markdown, resolving charts and tables so
|
|
48
58
|
exported conversations remain useful offline.
|
|
49
59
|
|
|
50
|
-
##
|
|
60
|
+
## Choose This UI Or AppKit UI
|
|
51
61
|
|
|
52
62
|
Use native `@databricks/appkit-ui` when you need its general primitives, Genie
|
|
53
63
|
chat component, or Model Serving hooks directly against native AppKit plugins.
|
|
@@ -69,7 +79,7 @@ understand Mastra-specific behavior:
|
|
|
69
79
|
has no steering.
|
|
70
80
|
- Conversation export that resolves those embeds into Markdown or PDF.
|
|
71
81
|
|
|
72
|
-
##
|
|
82
|
+
## Stylesheet Setup
|
|
73
83
|
|
|
74
84
|
```css
|
|
75
85
|
@import "@databricks/appkit-ui/styles.css";
|
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.105";
|
|
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";
|
|
@@ -71,7 +71,7 @@ export type { ChartChrome } from "./src/support/chart-theme.ts";
|
|
|
71
71
|
export type { ExportFormat, ExportBrand, EmbedResolver, ExportChatOptions } from "./src/support/export.ts";
|
|
72
72
|
export { MastraPluginClient, useMastraConfig, useMastraClient, useMastraMlflowExperiment, useMastraModels, useMastraDefaultModel, useMastraSuggestions, useMastraThreads, useChartFetch, useStatementFetch } from "./src/support/mastra-client.ts";
|
|
73
73
|
export type { MastraStreamResponse, MastraMemoryThread, ByIdFetchState } from "./src/support/mastra-client.ts";
|
|
74
|
-
export { modelStorageKey, readStoredModel, storeSelectedModel } from "./src/support/model-selection.ts";
|
|
74
|
+
export { defaultReasoningEffort, modelStorageKey, readStoredModel, storeSelectedModel } from "./src/support/model-selection.ts";
|
|
75
75
|
export type { MastraRequestContextInput, MastraRequestContextSource, MastraRequestContextSnapshot } from "./src/support/request-context.ts";
|
|
76
76
|
export { DEFAULT_THREAD_SESSION_KEY } from "./src/support/thread-sessions.ts";
|
|
77
77
|
export type { SessionQueuedSteer, SessionRun, ThreadSession } from "./src/support/thread-sessions.ts";
|
package/package.json
CHANGED
|
@@ -29,15 +29,15 @@
|
|
|
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.105",
|
|
33
|
+
"@dbx-tools/shared-genie": "^0.9.105",
|
|
34
|
+
"@dbx-tools/shared-mastra": "^0.9.105",
|
|
35
|
+
"@dbx-tools/shared-model": "^0.9.105",
|
|
36
|
+
"@dbx-tools/ui": "^0.9.105",
|
|
37
37
|
"@mastra/client-js": "1.50.0",
|
|
38
38
|
"@mastra/core": "1.71.0",
|
|
39
39
|
"@tanstack/react-table": "^8.21.3",
|
|
40
|
-
"ai": "^
|
|
40
|
+
"ai": "^7.0.0",
|
|
41
41
|
"echarts": "^6.0.0",
|
|
42
42
|
"echarts-for-react": "^3.0.2",
|
|
43
43
|
"lucide-react": "^0.554.0",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"publishConfig": {
|
|
51
51
|
"access": "public"
|
|
52
52
|
},
|
|
53
|
-
"version": "0.9.
|
|
53
|
+
"version": "0.9.105",
|
|
54
54
|
"dbxToolsConfig": {
|
|
55
55
|
"tags": [
|
|
56
56
|
"ui"
|
package/src/react/bubbles.tsx
CHANGED
|
@@ -349,11 +349,17 @@ export const AssistantBubble = ({
|
|
|
349
349
|
feedbackValue,
|
|
350
350
|
mlflowExperimentUrl,
|
|
351
351
|
}: AssistantBubbleProps) => {
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
isLast && status === "error",
|
|
352
|
+
const pendingApprovals = mergePendingApprovals(
|
|
353
|
+
collectPendingApprovals(message.parts),
|
|
354
|
+
externalApprovals,
|
|
356
355
|
);
|
|
356
|
+
const terminalToolState =
|
|
357
|
+
isLast && status === "error"
|
|
358
|
+
? "error"
|
|
359
|
+
: pendingApprovals.length === 0 && (!isLast || status === "ready")
|
|
360
|
+
? "done"
|
|
361
|
+
: undefined;
|
|
362
|
+
const toolEvents = mergeToolEvents(toolEventsFromParts(message.parts), events, terminalToolState);
|
|
357
363
|
const reasoning = [getReasoningText(message.parts), genieReasoningText(toolEvents)]
|
|
358
364
|
.filter(Boolean)
|
|
359
365
|
.join("\n\n");
|
|
@@ -397,13 +403,8 @@ export const AssistantBubble = ({
|
|
|
397
403
|
// {@link DataSlot} inline; unknown / expired ids resolve as
|
|
398
404
|
// nothing so the prose flows unaffected. Suggested questions
|
|
399
405
|
// stay gated on settle to avoid pop-in mid-stream.
|
|
400
|
-
const pendingApprovals = mergePendingApprovals(
|
|
401
|
-
collectPendingApprovals(message.parts),
|
|
402
|
-
externalApprovals,
|
|
403
|
-
);
|
|
404
|
-
|
|
405
406
|
return (
|
|
406
|
-
<Item className="items-start gap-
|
|
407
|
+
<Item className="items-start gap-2.5 border-none bg-transparent p-0">
|
|
407
408
|
<ItemMedia>
|
|
408
409
|
<RoleAvatar role="assistant" />
|
|
409
410
|
</ItemMedia>
|
|
@@ -413,7 +414,7 @@ export const AssistantBubble = ({
|
|
|
413
414
|
* bubble past the chat's `max-w-4xl`. Flex children default to
|
|
414
415
|
* `min-width: auto`, which sizes to content and overflows.
|
|
415
416
|
*/}
|
|
416
|
-
<ItemContent className="min-w-0 gap-
|
|
417
|
+
<ItemContent className="min-w-0 gap-1.5">
|
|
417
418
|
{/*
|
|
418
419
|
* Tool session pill leads the message. Charts and tables are
|
|
419
420
|
* embedded inline in the prose (via `[chart:<id>]` / `[data:<id>]`
|
|
@@ -535,7 +536,7 @@ export const UserBubble = ({ message }: { message: UIMessage }) => {
|
|
|
535
536
|
.map((p) => p.text)
|
|
536
537
|
.join("");
|
|
537
538
|
return (
|
|
538
|
-
<Item className="items-start gap-
|
|
539
|
+
<Item className="items-start gap-2.5 border-none bg-transparent p-0">
|
|
539
540
|
<ItemMedia>
|
|
540
541
|
<RoleAvatar role="user" />
|
|
541
542
|
</ItemMedia>
|
|
@@ -353,7 +353,7 @@ export const ChatComposer = ({
|
|
|
353
353
|
}}
|
|
354
354
|
placeholder="Send a message..."
|
|
355
355
|
rows={1}
|
|
356
|
-
className="field-sizing-fixed min-h-10 max-h-64 w-full flex-none overflow-y-auto px-4 pb-2 pt-4 text-xs"
|
|
356
|
+
className="field-sizing-fixed min-h-10 max-h-64 w-full flex-none overflow-y-auto px-4 pb-2 pt-4 text-base md:text-xs"
|
|
357
357
|
/>
|
|
358
358
|
<InputGroupAddon
|
|
359
359
|
align="block-end"
|
package/src/react/chat-stream.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { errorUtils, log, stringUtils } from "@dbx-tools/shared-core";
|
|
2
2
|
import { feedback } from "@dbx-tools/shared-mastra";
|
|
3
3
|
import { isStaleMastraResumeError } from "@dbx-tools/shared-mastra/resume";
|
|
4
|
-
import { readUIMessageStream, type UIMessage } from "ai";
|
|
4
|
+
import { readUIMessageStream, type UIMessage, type UIMessageChunk } from "ai";
|
|
5
5
|
import { useCallback } from "react";
|
|
6
6
|
import type {
|
|
7
7
|
ThreadMessageWriter,
|
|
@@ -20,6 +20,43 @@ const readMlflowTraceId = (stream: unknown): string | undefined => {
|
|
|
20
20
|
|
|
21
21
|
class StreamAborted extends Error {}
|
|
22
22
|
|
|
23
|
+
/** Stop reading a background run once the AI SDK has delivered its terminal chunk. */
|
|
24
|
+
export function closeOnTerminalChunk(
|
|
25
|
+
stream: ReadableStream<UIMessageChunk>,
|
|
26
|
+
): ReadableStream<UIMessageChunk> {
|
|
27
|
+
let reader: ReadableStreamDefaultReader<UIMessageChunk> | undefined;
|
|
28
|
+
let cancelled = false;
|
|
29
|
+
return new ReadableStream<UIMessageChunk>({
|
|
30
|
+
async start(controller) {
|
|
31
|
+
reader = stream.getReader();
|
|
32
|
+
try {
|
|
33
|
+
while (true) {
|
|
34
|
+
const { done, value } = await reader.read();
|
|
35
|
+
if (done) {
|
|
36
|
+
controller.close();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
controller.enqueue(value);
|
|
40
|
+
if (value.type === "finish" || value.type === "abort") {
|
|
41
|
+
controller.close();
|
|
42
|
+
await reader.cancel("terminal AI SDK chunk received").catch(() => undefined);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (!cancelled) controller.error(error);
|
|
48
|
+
} finally {
|
|
49
|
+
reader.releaseLock();
|
|
50
|
+
reader = undefined;
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
cancel(reason) {
|
|
54
|
+
cancelled = true;
|
|
55
|
+
return reader?.cancel(reason);
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
23
60
|
/** Replace or append one assistant message without disturbing the transcript. */
|
|
24
61
|
function upsertAssistant(messages: UIMessage[], assistant: UIMessage): UIMessage[] {
|
|
25
62
|
const next = [...messages];
|
|
@@ -69,7 +106,7 @@ export function useChatStream({ getSession, updateSession, writeMessages }: UseC
|
|
|
69
106
|
);
|
|
70
107
|
for await (const message of readUIMessageStream({
|
|
71
108
|
...(existing ? { message: existing } : {}),
|
|
72
|
-
stream: stream.stream,
|
|
109
|
+
stream: closeOnTerminalChunk(stream.stream),
|
|
73
110
|
terminateOnError: true,
|
|
74
111
|
})) {
|
|
75
112
|
if (signal.aborted) throw new StreamAborted();
|
|
@@ -219,7 +219,7 @@ export const ChatTranscript = ({
|
|
|
219
219
|
) : (
|
|
220
220
|
<div
|
|
221
221
|
ref={controller.contentRef}
|
|
222
|
-
className="mx-auto flex w-full max-w-4xl flex-col gap-
|
|
222
|
+
className="mx-auto flex w-full max-w-4xl flex-col gap-3 px-4 py-3 md:px-6"
|
|
223
223
|
>
|
|
224
224
|
{(isLoadingMore || isLoadingHistory) && (
|
|
225
225
|
<div className="flex items-center justify-center gap-2 py-1 text-xs text-muted-foreground">
|
package/src/react/markdown.tsx
CHANGED
|
@@ -258,6 +258,8 @@ export const AssistantMarkdown = ({
|
|
|
258
258
|
isAnimating={animate}
|
|
259
259
|
className={cn(
|
|
260
260
|
"prose prose-sm dark:prose-invert max-w-none break-words",
|
|
261
|
+
"leading-relaxed prose-p:my-1.5 prose-p:leading-relaxed",
|
|
262
|
+
"prose-ul:my-1.5 prose-ol:my-1.5 prose-li:my-0.5",
|
|
261
263
|
"prose-headings:font-semibold prose-headings:tracking-tight",
|
|
262
264
|
"prose-h1:text-lg prose-h1:mt-4 prose-h1:mb-2",
|
|
263
265
|
"prose-h2:text-base prose-h2:mt-4 prose-h2:mb-2",
|
|
@@ -288,13 +290,13 @@ export const ToolMarkdown = ({ children }: { children: string }) => (
|
|
|
288
290
|
controls={false}
|
|
289
291
|
className={cn(
|
|
290
292
|
"prose prose-sm dark:prose-invert max-w-none break-words",
|
|
291
|
-
"text-
|
|
293
|
+
"text-xs leading-snug text-muted-foreground",
|
|
292
294
|
"prose-p:my-0.5 prose-p:leading-snug",
|
|
293
295
|
"prose-ul:my-0.5 prose-ul:pl-4 prose-ol:my-0.5 prose-ol:pl-4",
|
|
294
296
|
"prose-li:my-0 prose-li:leading-snug prose-li:marker:text-muted-foreground/60",
|
|
295
297
|
"prose-headings:my-1 prose-headings:text-xs prose-headings:font-semibold",
|
|
296
298
|
"prose-strong:text-foreground/90 prose-strong:font-medium",
|
|
297
|
-
"prose-code:text-
|
|
299
|
+
"prose-code:text-xs prose-code:font-medium",
|
|
298
300
|
)}
|
|
299
301
|
>
|
|
300
302
|
{children}
|
|
@@ -384,11 +386,11 @@ export const SqlBlock = ({ sql }: { sql: string }) => {
|
|
|
384
386
|
value={formatted}
|
|
385
387
|
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"
|
|
386
388
|
/>
|
|
387
|
-
<pre className="max-w-full overflow-x-auto rounded-md border border-border bg-background p-3 font-mono text-
|
|
389
|
+
<pre className="max-w-full overflow-x-auto rounded-md border border-border bg-background p-3 font-mono text-xs leading-relaxed">
|
|
388
390
|
{html === null ? (
|
|
389
391
|
<code>{formatted}</code>
|
|
390
392
|
) : (
|
|
391
|
-
<code dangerouslySetInnerHTML={{ __html: html }} />
|
|
393
|
+
<code data-dbx-highlighted-code dangerouslySetInnerHTML={{ __html: html }} />
|
|
392
394
|
)}
|
|
393
395
|
</pre>
|
|
394
396
|
</div>
|
|
@@ -404,11 +406,15 @@ export const JsonBlock = ({ json, className }: { json: string; className?: strin
|
|
|
404
406
|
return (
|
|
405
407
|
<pre
|
|
406
408
|
className={cn(
|
|
407
|
-
"max-w-full overflow-auto whitespace-pre-wrap break-words font-mono text-
|
|
409
|
+
"max-w-full overflow-auto whitespace-pre-wrap break-words font-mono text-xs leading-relaxed",
|
|
408
410
|
className,
|
|
409
411
|
)}
|
|
410
412
|
>
|
|
411
|
-
{html === null ?
|
|
413
|
+
{html === null ? (
|
|
414
|
+
<code>{json}</code>
|
|
415
|
+
) : (
|
|
416
|
+
<code data-dbx-highlighted-code dangerouslySetInnerHTML={{ __html: html }} />
|
|
417
|
+
)}
|
|
412
418
|
</pre>
|
|
413
419
|
);
|
|
414
420
|
};
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
type MastraStreamResponse,
|
|
25
25
|
} from "../support/mastra-client.ts";
|
|
26
26
|
import {
|
|
27
|
+
defaultReasoningEffort,
|
|
27
28
|
modelStorageKey,
|
|
28
29
|
readStoredModel,
|
|
29
30
|
storeSelectedModel,
|
|
@@ -368,11 +369,14 @@ export const useMastraChat = <TValues extends Record<string, unknown> = Record<s
|
|
|
368
369
|
const activeModel = model || defaultModelId || undefined;
|
|
369
370
|
const activeModelOption = models.find((option) => option.name === activeModel);
|
|
370
371
|
const reasoningEfforts = activeModelOption?.reasoningEfforts ?? EMPTY_REASONING_EFFORTS;
|
|
372
|
+
const reasoningDefault = defaultReasoningEffort(reasoningEfforts);
|
|
373
|
+
const reasoningModelKey = `${activeModel ?? ""}:${reasoningEfforts.join(",")}`;
|
|
374
|
+
const initializedReasoningModelRef = useRef<string | undefined>(undefined);
|
|
371
375
|
useEffect(() => {
|
|
372
|
-
if (
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
}, [
|
|
376
|
+
if (initializedReasoningModelRef.current === reasoningModelKey) return;
|
|
377
|
+
initializedReasoningModelRef.current = reasoningModelKey;
|
|
378
|
+
setReasoningEffort(reasoningDefault);
|
|
379
|
+
}, [reasoningDefault, reasoningModelKey]);
|
|
376
380
|
// Starter suggestions: an explicit `options.suggestions` always
|
|
377
381
|
// wins (including `[]` to force none) and is rendered verbatim;
|
|
378
382
|
// otherwise auto-source the agent's Genie space sample questions.
|
|
@@ -450,8 +454,42 @@ export const useMastraChat = <TValues extends Record<string, unknown> = Record<s
|
|
|
450
454
|
});
|
|
451
455
|
const runIdRef = { current: getSession(threadId).runId };
|
|
452
456
|
try {
|
|
453
|
-
|
|
454
|
-
|
|
457
|
+
try {
|
|
458
|
+
const stream = await open(controller.signal);
|
|
459
|
+
await processStream(threadId, stream, assistantId, runIdRef, controller.signal);
|
|
460
|
+
} catch (initialError) {
|
|
461
|
+
const runId = runIdRef.current;
|
|
462
|
+
if (
|
|
463
|
+
controller.signal.aborted ||
|
|
464
|
+
getSession(threadId).runToken !== token ||
|
|
465
|
+
!mastraClient.backgroundTurns ||
|
|
466
|
+
!runId
|
|
467
|
+
) {
|
|
468
|
+
throw initialError;
|
|
469
|
+
}
|
|
470
|
+
logger.warn("chat stream disconnected; reconnecting to background run", {
|
|
471
|
+
runId,
|
|
472
|
+
error: errorUtils.errorMessage(initialError),
|
|
473
|
+
});
|
|
474
|
+
const messagesBeforeReplay = getSession(threadId).messages;
|
|
475
|
+
writeMessages(
|
|
476
|
+
threadId,
|
|
477
|
+
messagesBeforeReplay.filter((message) => message.id !== assistantId),
|
|
478
|
+
);
|
|
479
|
+
const streamThreadId = threadId === DEFAULT_THREAD_SESSION_KEY ? undefined : threadId;
|
|
480
|
+
try {
|
|
481
|
+
const stream = await mastraClient.observeAgentStream({
|
|
482
|
+
agentId,
|
|
483
|
+
runId,
|
|
484
|
+
threadId: streamThreadId,
|
|
485
|
+
signal: controller.signal,
|
|
486
|
+
});
|
|
487
|
+
await processStream(threadId, stream, assistantId, runIdRef, controller.signal);
|
|
488
|
+
} catch (reconnectError) {
|
|
489
|
+
writeMessages(threadId, messagesBeforeReplay);
|
|
490
|
+
throw reconnectError;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
455
493
|
updateSession(threadId, (session) => {
|
|
456
494
|
if (session.runToken !== token) return session;
|
|
457
495
|
return {
|
|
@@ -500,7 +538,15 @@ export const useMastraChat = <TValues extends Record<string, unknown> = Record<s
|
|
|
500
538
|
}));
|
|
501
539
|
}
|
|
502
540
|
},
|
|
503
|
-
[
|
|
541
|
+
[
|
|
542
|
+
agentId,
|
|
543
|
+
getSession,
|
|
544
|
+
mastraClient,
|
|
545
|
+
processStream,
|
|
546
|
+
refreshThreadsSoon,
|
|
547
|
+
updateSession,
|
|
548
|
+
writeMessages,
|
|
549
|
+
],
|
|
504
550
|
);
|
|
505
551
|
|
|
506
552
|
const runStream = useCallback(
|
|
@@ -567,9 +613,26 @@ export const useMastraChat = <TValues extends Record<string, unknown> = Record<s
|
|
|
567
613
|
const stop = useCallback(
|
|
568
614
|
(threadId?: string) => {
|
|
569
615
|
const key = threadId ?? activeKey;
|
|
616
|
+
const current = getSession(key);
|
|
617
|
+
if (!isSessionRunning(current)) return;
|
|
618
|
+
const streamThreadId = key === DEFAULT_THREAD_SESSION_KEY ? undefined : key;
|
|
619
|
+
if (mastraClient.backgroundTurns && current.runId) {
|
|
620
|
+
void mastraClient
|
|
621
|
+
.abortAgentRun({
|
|
622
|
+
agentId,
|
|
623
|
+
runId: current.runId,
|
|
624
|
+
threadId: streamThreadId,
|
|
625
|
+
})
|
|
626
|
+
.catch((error: unknown) => {
|
|
627
|
+
logger.warn("failed to abort background run", {
|
|
628
|
+
runId: current.runId,
|
|
629
|
+
error: errorUtils.errorMessage(error),
|
|
630
|
+
});
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
current.abortController?.abort();
|
|
570
634
|
updateSession(key, (session) => {
|
|
571
635
|
if (!isSessionRunning(session)) return session;
|
|
572
|
-
session.abortController?.abort();
|
|
573
636
|
return {
|
|
574
637
|
...session,
|
|
575
638
|
abortController: null,
|
|
@@ -579,7 +642,7 @@ export const useMastraChat = <TValues extends Record<string, unknown> = Record<s
|
|
|
579
642
|
};
|
|
580
643
|
});
|
|
581
644
|
},
|
|
582
|
-
[activeKey, updateSession],
|
|
645
|
+
[activeKey, agentId, getSession, mastraClient, updateSession],
|
|
583
646
|
);
|
|
584
647
|
|
|
585
648
|
const handleApproval = useChatApprovals({
|
package/src/react/tool-pill.tsx
CHANGED
|
@@ -277,7 +277,7 @@ const MessageGroupBody = ({
|
|
|
277
277
|
return (
|
|
278
278
|
<div className="flex flex-col gap-1.5">
|
|
279
279
|
{!omitQuestion && group.question && (
|
|
280
|
-
<div className="rounded border-l-2 border-primary/40 bg-background/40 px-2 py-1 text-
|
|
280
|
+
<div className="rounded border-l-2 border-primary/40 bg-background/40 px-2 py-1 text-xs italic leading-snug text-muted-foreground">
|
|
281
281
|
{group.question}
|
|
282
282
|
</div>
|
|
283
283
|
)}
|
|
@@ -290,13 +290,11 @@ const MessageGroupBody = ({
|
|
|
290
290
|
// for the same reason: code is the heaviest content here,
|
|
291
291
|
// and most readers only want a glance.
|
|
292
292
|
<Collapsible key={bucket.key} className="rounded border border-border/60 bg-background/40">
|
|
293
|
-
<CollapsibleTrigger className="group flex w-full items-center gap-1.5 px-2 py-1 text-left text-
|
|
293
|
+
<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">
|
|
294
294
|
<ChevronDownIcon className="size-3 shrink-0 transition-transform group-data-[state=closed]:-rotate-90" />
|
|
295
295
|
<span>{queryBuckets.length > 1 ? `Query ${i + 1}` : "Query"}</span>
|
|
296
296
|
{bucket.query?.title ? (
|
|
297
|
-
<span className="min-w-0 flex-1 truncate normal-case
|
|
298
|
-
{bucket.query.title}
|
|
299
|
-
</span>
|
|
297
|
+
<span className="min-w-0 flex-1 truncate normal-case">{bucket.query.title}</span>
|
|
300
298
|
) : null}
|
|
301
299
|
</CollapsibleTrigger>
|
|
302
300
|
<CollapsibleContent>
|
|
@@ -387,7 +385,7 @@ export function formatRawToolPayload(value: unknown): string {
|
|
|
387
385
|
|
|
388
386
|
const RawToolPayload = ({ label, value }: { label: "Request" | "Response"; value: unknown }) => (
|
|
389
387
|
<Collapsible className="rounded border border-border/60 bg-background/40">
|
|
390
|
-
<CollapsibleTrigger className="group flex w-full items-center gap-1.5 px-2 py-1 text-left text-
|
|
388
|
+
<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">
|
|
391
389
|
<ChevronDownIcon className="size-3 shrink-0 transition-transform group-data-[state=closed]:-rotate-90" />
|
|
392
390
|
<span>{label}</span>
|
|
393
391
|
</CollapsibleTrigger>
|
|
@@ -458,7 +456,7 @@ const WebSearchProgressDetails = ({ event }: { event: ToolEvent }) => {
|
|
|
458
456
|
return (
|
|
459
457
|
<div
|
|
460
458
|
key={group.key}
|
|
461
|
-
className="flex min-w-0 items-center gap-1.5 text-
|
|
459
|
+
className="flex min-w-0 items-center gap-1.5 text-xs leading-4"
|
|
462
460
|
aria-label={done ? [query, results].filter(Boolean).join(", ") : `Searching ${query}`}
|
|
463
461
|
>
|
|
464
462
|
{done ? (
|
|
@@ -605,7 +603,7 @@ export const ToolSessionPill = ({ events }: { events: ToolEvent[] }) => {
|
|
|
605
603
|
return (
|
|
606
604
|
<div
|
|
607
605
|
className={cn(
|
|
608
|
-
"rounded-md border bg-background/30 px-2 py-1
|
|
606
|
+
"rounded-md border bg-background/30 px-2 py-1 transition-colors",
|
|
609
607
|
// Loud-but-not-jarring "in flight" treatment when any row
|
|
610
608
|
// is running: primary border + soft ring. Failed sessions
|
|
611
609
|
// (no longer running) pick up the destructive border;
|
|
@@ -634,7 +632,7 @@ export const ToolSessionPill = ({ events }: { events: ToolEvent[] }) => {
|
|
|
634
632
|
)}
|
|
635
633
|
>
|
|
636
634
|
{verb}
|
|
637
|
-
{countSuffix
|
|
635
|
+
{countSuffix}
|
|
638
636
|
</span>
|
|
639
637
|
{/*
|
|
640
638
|
* Trailing "live" pip on the right edge while any tool is
|
|
@@ -650,7 +648,7 @@ export const ToolSessionPill = ({ events }: { events: ToolEvent[] }) => {
|
|
|
650
648
|
)}
|
|
651
649
|
</CollapsibleTrigger>
|
|
652
650
|
<CollapsibleContent>
|
|
653
|
-
<div className="mt-
|
|
651
|
+
<div className="mt-1.5 flex flex-col gap-1">
|
|
654
652
|
{events.map((event) => (
|
|
655
653
|
<ToolCallRow key={event.id} event={event} />
|
|
656
654
|
))}
|
|
@@ -14,12 +14,13 @@ import python from "shiki/langs/python.mjs";
|
|
|
14
14
|
import sql from "shiki/langs/sql.mjs";
|
|
15
15
|
import typescript from "shiki/langs/typescript.mjs";
|
|
16
16
|
import yaml from "shiki/langs/yaml.mjs";
|
|
17
|
-
import
|
|
17
|
+
import githubDarkHighContrast from "shiki/themes/github-dark-high-contrast.mjs";
|
|
18
|
+
import githubLightHighContrast from "shiki/themes/github-light-high-contrast.mjs";
|
|
18
19
|
|
|
19
20
|
/** Create the shared highlighter with the exact configured language set. */
|
|
20
21
|
export function createHighlighter() {
|
|
21
22
|
return createHighlighterCore({
|
|
22
|
-
themes: [
|
|
23
|
+
themes: [githubLightHighContrast, githubDarkHighContrast],
|
|
23
24
|
langs: [sql, python, typescript, javascript, json, bash, yaml, markdown],
|
|
24
25
|
engine: createOnigurumaEngine(import("shiki/wasm")),
|
|
25
26
|
});
|
|
@@ -72,6 +72,8 @@ export class MastraPluginClient extends MastraClient {
|
|
|
72
72
|
* mode) keeps the OBO-gated behavior.
|
|
73
73
|
*/
|
|
74
74
|
readonly chatAlwaysAvailable: boolean;
|
|
75
|
+
/** Whether turns continue server-side after the initiating stream disconnects. */
|
|
76
|
+
readonly backgroundTurns: boolean;
|
|
75
77
|
|
|
76
78
|
constructor(config: MastraClientConfig) {
|
|
77
79
|
super({
|
|
@@ -83,8 +85,9 @@ export class MastraPluginClient extends MastraClient {
|
|
|
83
85
|
this.basePath = config.basePath;
|
|
84
86
|
this.defaultAgent = config.defaultAgent;
|
|
85
87
|
this.agents = config.agents;
|
|
86
|
-
this.feedbackEnabled = config.feedbackEnabled;
|
|
87
|
-
this.chatAlwaysAvailable = config.chatAlwaysAvailable;
|
|
88
|
+
this.feedbackEnabled = config.feedbackEnabled ?? false;
|
|
89
|
+
this.chatAlwaysAvailable = config.chatAlwaysAvailable ?? false;
|
|
90
|
+
this.backgroundTurns = config.backgroundTurns ?? false;
|
|
88
91
|
}
|
|
89
92
|
|
|
90
93
|
/**
|
|
@@ -137,6 +140,33 @@ export class MastraPluginClient extends MastraClient {
|
|
|
137
140
|
return { headers, stream };
|
|
138
141
|
}
|
|
139
142
|
|
|
143
|
+
async #uiReconnect(options: {
|
|
144
|
+
path: string;
|
|
145
|
+
chatId: string;
|
|
146
|
+
routing: { threadId?: string; model?: string };
|
|
147
|
+
signal?: AbortSignal;
|
|
148
|
+
}): Promise<MastraStreamResponse> {
|
|
149
|
+
let headers = new Headers();
|
|
150
|
+
const origin = typeof window !== "undefined" ? window.location.origin : "http://localhost";
|
|
151
|
+
const transport = new DefaultChatTransport<UIMessage>({
|
|
152
|
+
api: new URL(options.path, origin).toString(),
|
|
153
|
+
credentials: "include",
|
|
154
|
+
headers: this.#routingHeaders(options.routing),
|
|
155
|
+
prepareReconnectToStreamRequest: ({ api }) => ({ api }),
|
|
156
|
+
fetch: async (input, init) => {
|
|
157
|
+
const response = await fetch(input, init);
|
|
158
|
+
headers = new Headers(response.headers);
|
|
159
|
+
return response;
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
const stream = await transport.reconnectToStream({
|
|
163
|
+
chatId: options.chatId,
|
|
164
|
+
abortSignal: options.signal,
|
|
165
|
+
});
|
|
166
|
+
if (!stream) throw new Error(`Run ${options.chatId} is no longer available`);
|
|
167
|
+
return { headers, stream };
|
|
168
|
+
}
|
|
169
|
+
|
|
140
170
|
#requestClient(
|
|
141
171
|
routing: { threadId?: string; model?: string },
|
|
142
172
|
signal?: AbortSignal,
|
|
@@ -188,6 +218,45 @@ export class MastraPluginClient extends MastraClient {
|
|
|
188
218
|
});
|
|
189
219
|
}
|
|
190
220
|
|
|
221
|
+
/** Reconnect to the cached event stream for one background agent run. */
|
|
222
|
+
async observeAgentStream(params: {
|
|
223
|
+
agentId: string;
|
|
224
|
+
runId: string;
|
|
225
|
+
threadId?: string;
|
|
226
|
+
model?: string;
|
|
227
|
+
signal?: AbortSignal;
|
|
228
|
+
}): Promise<MastraStreamResponse> {
|
|
229
|
+
return this.#uiReconnect({
|
|
230
|
+
path: `${this.basePath}${routes.MASTRA_ROUTES.chat}/${encodeURIComponent(params.agentId)}${routes.MASTRA_ROUTES.runs}/${encodeURIComponent(params.runId)}`,
|
|
231
|
+
chatId: params.runId,
|
|
232
|
+
routing: params,
|
|
233
|
+
signal: params.signal,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Explicitly stop one background agent run on the server. */
|
|
238
|
+
async abortAgentRun(params: {
|
|
239
|
+
agentId: string;
|
|
240
|
+
runId: string;
|
|
241
|
+
threadId?: string;
|
|
242
|
+
signal?: AbortSignal;
|
|
243
|
+
}): Promise<boolean> {
|
|
244
|
+
const origin = typeof window !== "undefined" ? window.location.origin : "http://localhost";
|
|
245
|
+
const response = await this.#mutateJson(
|
|
246
|
+
new URL(
|
|
247
|
+
`${this.basePath}${routes.MASTRA_ROUTES.chat}/${encodeURIComponent(params.agentId)}${routes.MASTRA_ROUTES.runs}/${encodeURIComponent(params.runId)}${routes.MASTRA_ROUTES.abort}`,
|
|
248
|
+
origin,
|
|
249
|
+
).toString(),
|
|
250
|
+
"POST",
|
|
251
|
+
wire.MastraRunAbortResponseSchema,
|
|
252
|
+
{
|
|
253
|
+
headers: this.#routingHeaders({ threadId: params.threadId }),
|
|
254
|
+
signal: params.signal,
|
|
255
|
+
},
|
|
256
|
+
);
|
|
257
|
+
return response.aborted;
|
|
258
|
+
}
|
|
259
|
+
|
|
191
260
|
/**
|
|
192
261
|
* Resume a suspended `requireApproval` tool through the same official chat
|
|
193
262
|
* route using Mastra's native `resumeData` contract.
|
|
@@ -1,5 +1,18 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ReasoningEffort as ReasoningEffortValue,
|
|
3
|
+
type ReasoningEffort,
|
|
4
|
+
} from "@dbx-tools/shared-model";
|
|
5
|
+
|
|
1
6
|
type StorageLike = Pick<Storage, "getItem" | "setItem">;
|
|
2
7
|
|
|
8
|
+
/** Default reasoning on for capable models while preserving an explicit off choice. */
|
|
9
|
+
export const defaultReasoningEffort = (
|
|
10
|
+
efforts: readonly ReasoningEffort[],
|
|
11
|
+
): ReasoningEffort | undefined =>
|
|
12
|
+
efforts.includes(ReasoningEffortValue.Medium)
|
|
13
|
+
? ReasoningEffortValue.Medium
|
|
14
|
+
: efforts.find((effort) => effort !== ReasoningEffortValue.None);
|
|
15
|
+
|
|
3
16
|
/** Namespace the last-selected model by plugin mount and agent. */
|
|
4
17
|
export const modelStorageKey = (basePath: string, agentId: string): string =>
|
|
5
18
|
`dbx-mastra-model:${basePath}:${agentId}`;
|
|
@@ -27,13 +27,11 @@ const LANGUAGES = [
|
|
|
27
27
|
"markdown",
|
|
28
28
|
] as const;
|
|
29
29
|
|
|
30
|
-
/**
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
*/
|
|
36
|
-
const THEME: BundledTheme = "github-light";
|
|
30
|
+
/** High-contrast theme pair used for every syntax surface. */
|
|
31
|
+
const THEMES = [
|
|
32
|
+
"github-light-high-contrast",
|
|
33
|
+
"github-dark-high-contrast",
|
|
34
|
+
] as const satisfies readonly [BundledTheme, BundledTheme];
|
|
37
35
|
|
|
38
36
|
/** Languages we can tokenize, as a set for O(1) support checks. */
|
|
39
37
|
const SUPPORTED = new Set<string>(LANGUAGES);
|
|
@@ -52,13 +50,40 @@ function loadHighlighter(): Promise<HighlighterCore> {
|
|
|
52
50
|
return _loading;
|
|
53
51
|
}
|
|
54
52
|
|
|
55
|
-
|
|
53
|
+
function splitThemeColor(
|
|
54
|
+
value: string | undefined,
|
|
55
|
+
darkVariable: "--shiki-dark" | "--shiki-dark-bg",
|
|
56
|
+
): { light?: string; dark?: string } {
|
|
57
|
+
if (!value) return {};
|
|
58
|
+
const marker = `;${darkVariable}:`;
|
|
59
|
+
const index = value.indexOf(marker);
|
|
60
|
+
if (index < 0) return { light: value };
|
|
61
|
+
return {
|
|
62
|
+
light: value.slice(0, index),
|
|
63
|
+
dark: value.slice(index + marker.length),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Tokenize `code` with the active themes into Streamdown's result shape. */
|
|
56
68
|
function highlightTokens(h: HighlighterCore, code: string, language: string) {
|
|
57
69
|
const { tokens, fg, bg, rootStyle } = h.codeToTokens(code, {
|
|
58
70
|
lang: language as BundledLanguage,
|
|
59
|
-
|
|
71
|
+
themes: { light: THEMES[0], dark: THEMES[1] },
|
|
60
72
|
});
|
|
61
|
-
|
|
73
|
+
const foreground = splitThemeColor(fg, "--shiki-dark");
|
|
74
|
+
const background = splitThemeColor(bg, "--shiki-dark-bg");
|
|
75
|
+
const darkStyles = [
|
|
76
|
+
foreground.dark ? `--shiki-dark:${foreground.dark}` : "",
|
|
77
|
+
background.dark ? `--shiki-dark-bg:${background.dark}` : "",
|
|
78
|
+
]
|
|
79
|
+
.filter(Boolean)
|
|
80
|
+
.join(";");
|
|
81
|
+
return {
|
|
82
|
+
tokens,
|
|
83
|
+
fg: foreground.light,
|
|
84
|
+
bg: background.light,
|
|
85
|
+
rootStyle: [rootStyle, darkStyles].filter(Boolean).join(";"),
|
|
86
|
+
};
|
|
62
87
|
}
|
|
63
88
|
|
|
64
89
|
/** Escape HTML-significant characters (from the shared string utils). */
|
|
@@ -76,17 +101,16 @@ export async function highlightToHtml(code: string, language: string): Promise<s
|
|
|
76
101
|
if (!SUPPORTED.has(language)) return escapeHtml(code);
|
|
77
102
|
const h = await loadHighlighter();
|
|
78
103
|
try {
|
|
79
|
-
const { tokens } = h
|
|
80
|
-
lang: language as BundledLanguage,
|
|
81
|
-
theme: THEME,
|
|
82
|
-
});
|
|
104
|
+
const { tokens } = highlightTokens(h, code, language);
|
|
83
105
|
return tokens
|
|
84
106
|
.map((line) =>
|
|
85
107
|
line
|
|
86
|
-
.map(
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
108
|
+
.map((token) => {
|
|
109
|
+
const light = token.htmlStyle?.color ?? token.color ?? "inherit";
|
|
110
|
+
const dark = token.htmlStyle?.["--shiki-dark"] ?? light;
|
|
111
|
+
const style = `--sdm-c:${light};--shiki-dark:${dark}`;
|
|
112
|
+
return `<span style="${escapeHtml(style)}">${escapeHtml(token.content)}</span>`;
|
|
113
|
+
})
|
|
90
114
|
.join(""),
|
|
91
115
|
)
|
|
92
116
|
.join("\n");
|
|
@@ -109,7 +133,7 @@ export function createShikiPlugin(): CodeHighlighterPlugin {
|
|
|
109
133
|
name: "shiki",
|
|
110
134
|
type: "code-highlighter",
|
|
111
135
|
getSupportedLanguages: () => [...LANGUAGES],
|
|
112
|
-
getThemes: () => [
|
|
136
|
+
getThemes: () => [...THEMES],
|
|
113
137
|
supportsLanguage: (language) => isSupported(language),
|
|
114
138
|
highlight: (options, callback) => {
|
|
115
139
|
if (!isSupported(options.language)) return null;
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
TOOL_PROGRESS_PART_TYPE,
|
|
5
5
|
ToolProgressPartDataSchema,
|
|
6
6
|
} from "@dbx-tools/shared-mastra/wire";
|
|
7
|
-
import { getToolOrDynamicToolName,
|
|
7
|
+
import { getToolOrDynamicToolName, isToolUIPart, type UIMessage } from "ai";
|
|
8
8
|
import type { ToolEvent } from "../react/types.ts";
|
|
9
9
|
|
|
10
10
|
/** Collapse one reasoning event into a compact paragraph. */
|
|
@@ -32,7 +32,7 @@ export function genieReasoningText(events: ToolEvent[]): string {
|
|
|
32
32
|
if (text) sections.push(text);
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
-
return sections.join("\n
|
|
35
|
+
return sections.join("\n");
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
/** Drop markdown table rows so query samples never land in Thoughts. */
|
|
@@ -46,7 +46,7 @@ function stripMarkdownTables(text: string): string {
|
|
|
46
46
|
|
|
47
47
|
/** Project native AI SDK tool parts from persisted messages onto pill state. */
|
|
48
48
|
export function toolEventsFromParts(parts: UIMessage["parts"]): ToolEvent[] {
|
|
49
|
-
const events = parts.filter(
|
|
49
|
+
const events = parts.filter(isToolUIPart).map<ToolEvent>((part) => {
|
|
50
50
|
const error = part.state === "output-error";
|
|
51
51
|
const done = part.state === "output-available";
|
|
52
52
|
return {
|
|
@@ -78,21 +78,33 @@ export function toolEventsFromParts(parts: UIMessage["parts"]): ToolEvent[] {
|
|
|
78
78
|
export function mergeToolEvents(
|
|
79
79
|
persisted: ToolEvent[],
|
|
80
80
|
live: ToolEvent[] | undefined,
|
|
81
|
-
|
|
81
|
+
terminalState?: "done" | "error",
|
|
82
82
|
): ToolEvent[] {
|
|
83
83
|
const merged = new Map(persisted.map((event) => [event.id, event]));
|
|
84
84
|
for (const event of live ?? []) {
|
|
85
85
|
const prior = merged.get(event.id);
|
|
86
|
-
merged.set(
|
|
86
|
+
merged.set(
|
|
87
|
+
event.id,
|
|
88
|
+
prior
|
|
89
|
+
? {
|
|
90
|
+
...prior,
|
|
91
|
+
...event,
|
|
92
|
+
status: prior.status === "running" ? event.status : prior.status,
|
|
93
|
+
...(prior.output !== undefined ? { output: prior.output } : {}),
|
|
94
|
+
}
|
|
95
|
+
: event,
|
|
96
|
+
);
|
|
87
97
|
}
|
|
88
98
|
const events = [...merged.values()];
|
|
89
|
-
if (!
|
|
99
|
+
if (!terminalState) return events;
|
|
90
100
|
return events.map((event) =>
|
|
91
101
|
event.status === "running"
|
|
92
102
|
? {
|
|
93
103
|
...event,
|
|
94
|
-
status:
|
|
95
|
-
|
|
104
|
+
status: terminalState,
|
|
105
|
+
...(terminalState === "error"
|
|
106
|
+
? { output: { error: "The assistant stream ended before this tool returned." } }
|
|
107
|
+
: {}),
|
|
96
108
|
}
|
|
97
109
|
: event,
|
|
98
110
|
);
|