@dbx-tools/ui-mastra 0.3.10 → 0.3.12
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 +10 -1
- package/package.json +7 -7
- package/src/react/chat-view.tsx +5 -38
- package/src/react/mastra-chat.tsx +8 -38
- package/src/react/types.ts +4 -8
- package/src/support/mastra-client.ts +0 -44
package/README.md
CHANGED
|
@@ -24,7 +24,13 @@ Key features:
|
|
|
24
24
|
- Inline embed rendering for `[chart:<id>]` and `[data:<id>]` markers produced by
|
|
25
25
|
the server plugin.
|
|
26
26
|
- Conversation sidebar with new, select, rename, delete, active-thread, and
|
|
27
|
-
background-streaming states.
|
|
27
|
+
background-streaming states, plus a per-row cancel for a running thread.
|
|
28
|
+
- Concurrent threads: run several conversations at once, switch between them
|
|
29
|
+
while each keeps streaming, and cancel any one independently (per-thread abort
|
|
30
|
+
+ routing, no shared client state).
|
|
31
|
+
- Mid-turn steering: submit a message while a turn streams to "send now" - it
|
|
32
|
+
interrupts the in-flight run and starts a fresh turn with your message
|
|
33
|
+
immediately.
|
|
28
34
|
- Export menu for PDF and Markdown, resolving charts and tables so
|
|
29
35
|
exported conversations remain useful offline.
|
|
30
36
|
|
|
@@ -44,6 +50,9 @@ understand Mastra-specific behavior:
|
|
|
44
50
|
answer.
|
|
45
51
|
- `[chart:<id>]` and `[data:<id>]` assistant markers rendered as ECharts charts
|
|
46
52
|
and sortable tables.
|
|
53
|
+
- Concurrent multi-thread streaming, per-thread cancel, and mid-turn steering
|
|
54
|
+
("send now" interrupts the live run and restarts with your message) - the
|
|
55
|
+
native AppKit chat surface runs one turn at a time and has no steering.
|
|
47
56
|
- Conversation export that resolves those embeds into Markdown or PDF.
|
|
48
57
|
|
|
49
58
|
## Add The Styles
|
package/package.json
CHANGED
|
@@ -26,19 +26,19 @@
|
|
|
26
26
|
"shiki": "^3.0.0",
|
|
27
27
|
"sql-formatter": "^15.6.9",
|
|
28
28
|
"streamdown": "^2.5.0",
|
|
29
|
-
"@dbx-tools/shared-core": "0.3.
|
|
30
|
-
"@dbx-tools/shared-
|
|
31
|
-
"@dbx-tools/ui-appkit": "0.3.
|
|
32
|
-
"@dbx-tools/
|
|
33
|
-
"@dbx-tools/
|
|
34
|
-
"@dbx-tools/shared-
|
|
29
|
+
"@dbx-tools/shared-core": "0.3.12",
|
|
30
|
+
"@dbx-tools/shared-model": "0.3.12",
|
|
31
|
+
"@dbx-tools/ui-appkit": "0.3.12",
|
|
32
|
+
"@dbx-tools/shared-mastra": "0.3.12",
|
|
33
|
+
"@dbx-tools/ui-branding": "0.3.12",
|
|
34
|
+
"@dbx-tools/shared-genie": "0.3.12"
|
|
35
35
|
},
|
|
36
36
|
"main": "index.ts",
|
|
37
37
|
"license": "UNLICENSED",
|
|
38
38
|
"publishConfig": {
|
|
39
39
|
"access": "public"
|
|
40
40
|
},
|
|
41
|
-
"version": "0.3.
|
|
41
|
+
"version": "0.3.12",
|
|
42
42
|
"types": "index.ts",
|
|
43
43
|
"type": "module",
|
|
44
44
|
"exports": {
|
package/src/react/chat-view.tsx
CHANGED
|
@@ -35,7 +35,6 @@ import {
|
|
|
35
35
|
import { error as errorUtil } from "@dbx-tools/shared-core";
|
|
36
36
|
import {
|
|
37
37
|
ArrowDownIcon,
|
|
38
|
-
FastForwardIcon,
|
|
39
38
|
MessageSquareIcon,
|
|
40
39
|
PanelLeftIcon,
|
|
41
40
|
RefreshCwIcon,
|
|
@@ -101,7 +100,6 @@ export const ChatView = ({
|
|
|
101
100
|
status,
|
|
102
101
|
error,
|
|
103
102
|
sendMessage,
|
|
104
|
-
onInterrupt,
|
|
105
103
|
regenerate,
|
|
106
104
|
onStop,
|
|
107
105
|
className,
|
|
@@ -278,16 +276,6 @@ export const ChatView = ({
|
|
|
278
276
|
});
|
|
279
277
|
};
|
|
280
278
|
|
|
281
|
-
// Interrupt-submit: force an immediate course-correction (abort the current
|
|
282
|
-
// run, resend with this text) rather than queuing to the live turn.
|
|
283
|
-
const handleInterrupt = () => {
|
|
284
|
-
const text = input.trim();
|
|
285
|
-
if (!text || !onInterrupt) return;
|
|
286
|
-
onInterrupt({ text });
|
|
287
|
-
setInput("");
|
|
288
|
-
setIsAtBottom(true);
|
|
289
|
-
pinToBottom();
|
|
290
|
-
};
|
|
291
279
|
|
|
292
280
|
const lastMessage = messages.at(-1);
|
|
293
281
|
const lastEvents = lastMessage ? toolEventsByMessage[lastMessage.id] : undefined;
|
|
@@ -665,38 +653,17 @@ export const ChatView = ({
|
|
|
665
653
|
) : (
|
|
666
654
|
<>
|
|
667
655
|
{/*
|
|
668
|
-
*
|
|
669
|
-
*
|
|
670
|
-
*
|
|
671
|
-
*
|
|
672
|
-
*/}
|
|
673
|
-
{isRunning && onInterrupt && input.trim() && (
|
|
674
|
-
<Tooltip>
|
|
675
|
-
<TooltipTrigger asChild>
|
|
676
|
-
<InputGroupButton
|
|
677
|
-
type="button"
|
|
678
|
-
size="icon-sm"
|
|
679
|
-
variant="outline"
|
|
680
|
-
onClick={handleInterrupt}
|
|
681
|
-
aria-label="Interrupt and send now"
|
|
682
|
-
>
|
|
683
|
-
<FastForwardIcon className="size-3" />
|
|
684
|
-
</InputGroupButton>
|
|
685
|
-
</TooltipTrigger>
|
|
686
|
-
<TooltipContent>Interrupt & send now</TooltipContent>
|
|
687
|
-
</Tooltip>
|
|
688
|
-
)}
|
|
689
|
-
{/*
|
|
690
|
-
* Idle, or running with typed text: the primary button
|
|
691
|
-
* sends. A send mid-run steers the live turn (see the
|
|
692
|
-
* driver's sendMessage).
|
|
656
|
+
* The primary button sends. Submitting while a turn is
|
|
657
|
+
* running is a "send now": it interrupts the live run and
|
|
658
|
+
* starts a fresh turn with this message immediately (see
|
|
659
|
+
* the driver's sendMessage). Idle, it just sends.
|
|
693
660
|
*/}
|
|
694
661
|
<InputGroupButton
|
|
695
662
|
type="submit"
|
|
696
663
|
size="icon-sm"
|
|
697
664
|
variant="default"
|
|
698
665
|
disabled={!input.trim()}
|
|
699
|
-
aria-label={isRunning ? "
|
|
666
|
+
aria-label={isRunning ? "Send now (interrupts)" : "Send message"}
|
|
700
667
|
>
|
|
701
668
|
<SendIcon className="size-3" />
|
|
702
669
|
</InputGroupButton>
|
|
@@ -901,52 +901,23 @@ export const useMastraChat = (
|
|
|
901
901
|
[activeThreadId, getSession, noteThreadActivity, updateSession, writeMessages],
|
|
902
902
|
);
|
|
903
903
|
|
|
904
|
+
// Send a message, steering the active thread. Submitting while a turn is
|
|
905
|
+
// already streaming is a "send now": it interrupts the live run and starts a
|
|
906
|
+
// fresh turn that includes the new message, immediately. `runStream` /
|
|
907
|
+
// `driveStream` supersede the prior run (abort + runToken bump + settle any
|
|
908
|
+
// running tool pills), so the interrupted turn stops cleanly and the new one
|
|
909
|
+
// takes over. An idle submit just starts a turn.
|
|
904
910
|
const sendMessage = useCallback<ChatViewProps["sendMessage"]>(
|
|
905
911
|
(message) => {
|
|
906
912
|
const text = message.text ?? "";
|
|
907
913
|
if (!text) return;
|
|
908
914
|
const threadId = activeKey;
|
|
909
915
|
const { before, next } = appendUserMessage(threadId, text);
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
// (no restart). If the server won't deliver it, fall back to
|
|
913
|
-
// interrupting + resending so a steer never silently drops.
|
|
914
|
-
if (isSessionRunning(before) && before.runId) {
|
|
915
|
-
const runId = before.runId;
|
|
916
|
-
const steerThreadId =
|
|
917
|
-
threadId === DEFAULT_THREAD_SESSION_KEY ? undefined : threadId;
|
|
918
|
-
void mastraClient
|
|
919
|
-
.queueMessage({ agentId, runId, threadId: steerThreadId, message: text })
|
|
920
|
-
.then((accepted) => {
|
|
921
|
-
if (accepted) {
|
|
922
|
-
logger.info("steer:queued", { runId });
|
|
923
|
-
return;
|
|
924
|
-
}
|
|
925
|
-
logger.info("steer:fallback-interrupt", { runId });
|
|
926
|
-
void runStream(threadId, getSession(threadId).messages);
|
|
927
|
-
});
|
|
928
|
-
return;
|
|
916
|
+
if (isSessionRunning(before)) {
|
|
917
|
+
logger.info("steer:interrupt", { threadId });
|
|
929
918
|
}
|
|
930
919
|
void runStream(threadId, next);
|
|
931
920
|
},
|
|
932
|
-
[appendUserMessage, runStream, activeKey, getSession, mastraClient, agentId],
|
|
933
|
-
);
|
|
934
|
-
|
|
935
|
-
// Interrupt-and-resend: unconditionally abort any in-flight run on the
|
|
936
|
-
// active thread and start a fresh turn with `text` included immediately.
|
|
937
|
-
// The composer offers this alongside the queue-steer send so the user can
|
|
938
|
-
// force an immediate course-correction rather than waiting for the current
|
|
939
|
-
// turn to fold in a queued message. `runStream`/`driveStream` supersede the
|
|
940
|
-
// prior run (abort + runToken bump + pill cleanup).
|
|
941
|
-
const interrupt = useCallback(
|
|
942
|
-
(message: { text?: string }) => {
|
|
943
|
-
const text = message.text ?? "";
|
|
944
|
-
if (!text) return;
|
|
945
|
-
const threadId = activeKey;
|
|
946
|
-
const { next } = appendUserMessage(threadId, text);
|
|
947
|
-
logger.info("steer:interrupt", { threadId });
|
|
948
|
-
void runStream(threadId, next);
|
|
949
|
-
},
|
|
950
921
|
[appendUserMessage, runStream, activeKey],
|
|
951
922
|
);
|
|
952
923
|
|
|
@@ -1412,7 +1383,6 @@ export const useMastraChat = (
|
|
|
1412
1383
|
status: activeSession.status,
|
|
1413
1384
|
error: activeSession.error,
|
|
1414
1385
|
sendMessage,
|
|
1415
|
-
onInterrupt: interrupt,
|
|
1416
1386
|
regenerate,
|
|
1417
1387
|
onStop: stop,
|
|
1418
1388
|
suggestions,
|
package/src/react/types.ts
CHANGED
|
@@ -89,16 +89,12 @@ export type ChatViewProps = {
|
|
|
89
89
|
* a host driving `ChatView` itself supplies its own.
|
|
90
90
|
*/
|
|
91
91
|
error?: Error | null;
|
|
92
|
-
sendMessage: (message: { text: string }) => void;
|
|
93
92
|
/**
|
|
94
|
-
* Send a message
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
* to the queue-steer Send, so the user can force an immediate
|
|
98
|
-
* course-correction instead of folding the message into the live turn.
|
|
99
|
-
* Omit to offer only the queue-steer send while running.
|
|
93
|
+
* Send a message on the active thread. Submitting while a turn is streaming
|
|
94
|
+
* is a "send now": it interrupts the in-flight run and starts a fresh turn
|
|
95
|
+
* including this message immediately.
|
|
100
96
|
*/
|
|
101
|
-
|
|
97
|
+
sendMessage: (message: { text: string }) => void;
|
|
102
98
|
regenerate?: () => void;
|
|
103
99
|
/**
|
|
104
100
|
* Abort the in-flight response. When provided and the chat is running
|
|
@@ -123,50 +123,6 @@ export class MastraPluginClient extends MastraClient {
|
|
|
123
123
|
return asMastraStreamResponse(response);
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
/**
|
|
127
|
-
* Steer a live run by handing it another message mid-turn ("queue"), via
|
|
128
|
-
* the agent `/queue-message` route. `behavior` controls what happens when
|
|
129
|
-
* the run is active: `deliver` (default) folds the message into the current
|
|
130
|
-
* turn so the agent picks it up without restarting; `persist` holds it for
|
|
131
|
-
* the next turn; `discard` drops it. Routed per call (own thread header, no
|
|
132
|
-
* shared client state) like {@link streamAgent}.
|
|
133
|
-
*
|
|
134
|
-
* Resolves `true` when the server accepted the message for delivery, `false`
|
|
135
|
-
* otherwise (unknown run / route unsupported / non-2xx) - never throws for a
|
|
136
|
-
* rejected steer, so the caller can fall back to interrupting + resending.
|
|
137
|
-
* Queue / signal routes are `@experimental` in Mastra.
|
|
138
|
-
*/
|
|
139
|
-
async queueMessage(params: {
|
|
140
|
-
agentId: string;
|
|
141
|
-
runId: string;
|
|
142
|
-
threadId?: string;
|
|
143
|
-
message: string;
|
|
144
|
-
behavior?: "deliver" | "persist" | "discard";
|
|
145
|
-
}): Promise<boolean> {
|
|
146
|
-
const url = `${this.basePath}/agents/${encodeURIComponent(params.agentId)}/queue-message`;
|
|
147
|
-
try {
|
|
148
|
-
const response = await fetch(url, {
|
|
149
|
-
method: "POST",
|
|
150
|
-
credentials: "include",
|
|
151
|
-
headers: {
|
|
152
|
-
"Content-Type": "application/json",
|
|
153
|
-
...this.#routingHeaders({ threadId: params.threadId }),
|
|
154
|
-
},
|
|
155
|
-
body: JSON.stringify({
|
|
156
|
-
runId: params.runId,
|
|
157
|
-
...(params.threadId ? { threadId: params.threadId } : {}),
|
|
158
|
-
message: params.message,
|
|
159
|
-
ifActive: { behavior: params.behavior ?? "deliver" },
|
|
160
|
-
}),
|
|
161
|
-
});
|
|
162
|
-
if (!response.ok) return false;
|
|
163
|
-
const body = (await response.json().catch(() => null)) as { accepted?: boolean } | null;
|
|
164
|
-
return body?.accepted === true;
|
|
165
|
-
} catch {
|
|
166
|
-
return false;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
126
|
/**
|
|
171
127
|
* Resume a suspended `requireApproval` tool via `approve-tool-call`.
|
|
172
128
|
* Reads SSE directly instead of `agent.approveToolCall()` so the
|