@dbx-tools/ui-mastra 0.3.11 → 0.3.13
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 +7 -5
- package/index.ts +1 -1
- package/package.json +7 -7
- package/src/react/chat-view.tsx +60 -38
- package/src/react/mastra-chat.tsx +65 -34
- package/src/react/types.ts +24 -7
- package/src/support/mastra-client.ts +0 -44
- package/src/support/thread-sessions.ts +19 -0
package/README.md
CHANGED
|
@@ -28,8 +28,9 @@ Key features:
|
|
|
28
28
|
- Concurrent threads: run several conversations at once, switch between them
|
|
29
29
|
while each keeps streaming, and cancel any one independently (per-thread abort
|
|
30
30
|
+ routing, no shared client state).
|
|
31
|
-
- Mid-turn steering:
|
|
32
|
-
|
|
31
|
+
- Mid-turn steering queue: messages submitted while a turn streams stack up as
|
|
32
|
+
pending steers (they drain oldest-first when the turn ends); each queued item
|
|
33
|
+
can be sent now (interrupting the current turn) or removed.
|
|
33
34
|
- Export menu for PDF and Markdown, resolving charts and tables so
|
|
34
35
|
exported conversations remain useful offline.
|
|
35
36
|
|
|
@@ -49,9 +50,10 @@ understand Mastra-specific behavior:
|
|
|
49
50
|
answer.
|
|
50
51
|
- `[chart:<id>]` and `[data:<id>]` assistant markers rendered as ECharts charts
|
|
51
52
|
and sortable tables.
|
|
52
|
-
- Concurrent multi-thread streaming, per-thread cancel, and mid-turn steering
|
|
53
|
-
|
|
54
|
-
one turn at a time and
|
|
53
|
+
- Concurrent multi-thread streaming, per-thread cancel, and a mid-turn steering
|
|
54
|
+
queue (submit while running to enqueue; drain oldest-first, or send any item
|
|
55
|
+
now to interrupt) - the native AppKit chat surface runs one turn at a time and
|
|
56
|
+
has no steering.
|
|
55
57
|
- Conversation export that resolves those embeds into Markdown or PDF.
|
|
56
58
|
|
|
57
59
|
## Add The Styles
|
package/index.ts
CHANGED
|
@@ -25,7 +25,7 @@ export type { DataRow } from "./src/react/data-grid";
|
|
|
25
25
|
export type { UseMastraChatOptions, MastraChatProps } from "./src/react/mastra-chat";
|
|
26
26
|
export type { SuggestionPillsProps } from "./src/react/suggestion-pills";
|
|
27
27
|
export type { ThreadSidebarProps } from "./src/react/thread-sidebar";
|
|
28
|
-
export type { ChatStatus, ToolEvent, ToolProgress, ChatModelOption, FeedbackValue, FeedbackSubmission, MessageFeedback, ThreadSummary, ChatViewProps, ApprovalDecision, PendingApproval } from "./src/react/types";
|
|
28
|
+
export type { ChatStatus, ToolEvent, ToolProgress, ChatModelOption, QueuedSteer, FeedbackValue, FeedbackSubmission, MessageFeedback, ThreadSummary, ChatViewProps, ApprovalDecision, PendingApproval } from "./src/react/types";
|
|
29
29
|
export type { ExportFormat, ExportBrand, EmbedResolver, ExportChatOptions } from "./src/support/export";
|
|
30
30
|
export type { ByIdFetchState } from "./src/support/mastra-client";
|
|
31
31
|
export type { MastraStreamChunk, MastraStreamResponse } from "./src/support/mastra-stream";
|
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/shared-
|
|
32
|
-
"@dbx-tools/shared-
|
|
33
|
-
"@dbx-tools/ui-appkit": "0.3.
|
|
34
|
-
"@dbx-tools/ui-branding": "0.3.
|
|
29
|
+
"@dbx-tools/shared-core": "0.3.13",
|
|
30
|
+
"@dbx-tools/shared-mastra": "0.3.13",
|
|
31
|
+
"@dbx-tools/shared-model": "0.3.13",
|
|
32
|
+
"@dbx-tools/shared-genie": "0.3.13",
|
|
33
|
+
"@dbx-tools/ui-appkit": "0.3.13",
|
|
34
|
+
"@dbx-tools/ui-branding": "0.3.13"
|
|
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.13",
|
|
42
42
|
"types": "index.ts",
|
|
43
43
|
"type": "module",
|
|
44
44
|
"exports": {
|
package/src/react/chat-view.tsx
CHANGED
|
@@ -35,14 +35,15 @@ 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,
|
|
41
|
+
SendHorizontalIcon,
|
|
42
42
|
SendIcon,
|
|
43
43
|
SquareIcon,
|
|
44
44
|
Trash2Icon,
|
|
45
45
|
TriangleAlertIcon,
|
|
46
|
+
XIcon,
|
|
46
47
|
} from "lucide-react";
|
|
47
48
|
import React, { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
48
49
|
import { AssistantBubble, UserBubble } from "./bubbles";
|
|
@@ -101,7 +102,9 @@ export const ChatView = ({
|
|
|
101
102
|
status,
|
|
102
103
|
error,
|
|
103
104
|
sendMessage,
|
|
104
|
-
|
|
105
|
+
queuedSteers = [],
|
|
106
|
+
onSendSteerNow,
|
|
107
|
+
onRemoveSteer,
|
|
105
108
|
regenerate,
|
|
106
109
|
onStop,
|
|
107
110
|
className,
|
|
@@ -278,16 +281,6 @@ export const ChatView = ({
|
|
|
278
281
|
});
|
|
279
282
|
};
|
|
280
283
|
|
|
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
284
|
|
|
292
285
|
const lastMessage = messages.at(-1);
|
|
293
286
|
const lastEvents = lastMessage ? toolEventsByMessage[lastMessage.id] : undefined;
|
|
@@ -635,6 +628,56 @@ export const ChatView = ({
|
|
|
635
628
|
onSubmit={handleSubmit}
|
|
636
629
|
className="mx-auto w-full max-w-4xl px-3 pt-2 pb-[max(1rem,env(safe-area-inset-bottom))] md:px-6"
|
|
637
630
|
>
|
|
631
|
+
{queuedSteers.length > 0 && (
|
|
632
|
+
// Steers submitted while the turn is running, waiting to send.
|
|
633
|
+
// They drain oldest-first when the turn ends; each can be fired
|
|
634
|
+
// now (interrupts) or removed.
|
|
635
|
+
<div className="mb-2 flex flex-col gap-1">
|
|
636
|
+
{queuedSteers.map((steer) => (
|
|
637
|
+
<div
|
|
638
|
+
key={steer.id}
|
|
639
|
+
className="flex items-center gap-1.5 rounded-lg border border-border/70 bg-muted/40 px-2 py-1 text-xs"
|
|
640
|
+
>
|
|
641
|
+
<span className="text-muted-foreground">Queued</span>
|
|
642
|
+
<span className="min-w-0 flex-1 truncate">{steer.text}</span>
|
|
643
|
+
{onSendSteerNow && (
|
|
644
|
+
<Tooltip>
|
|
645
|
+
<TooltipTrigger asChild>
|
|
646
|
+
<Button
|
|
647
|
+
type="button"
|
|
648
|
+
variant="ghost"
|
|
649
|
+
size="icon"
|
|
650
|
+
className="size-6 shrink-0"
|
|
651
|
+
onClick={() => onSendSteerNow(steer.id)}
|
|
652
|
+
aria-label="Send now (interrupts current turn)"
|
|
653
|
+
>
|
|
654
|
+
<SendHorizontalIcon className="size-3" />
|
|
655
|
+
</Button>
|
|
656
|
+
</TooltipTrigger>
|
|
657
|
+
<TooltipContent>Send now — interrupts</TooltipContent>
|
|
658
|
+
</Tooltip>
|
|
659
|
+
)}
|
|
660
|
+
{onRemoveSteer && (
|
|
661
|
+
<Tooltip>
|
|
662
|
+
<TooltipTrigger asChild>
|
|
663
|
+
<Button
|
|
664
|
+
type="button"
|
|
665
|
+
variant="ghost"
|
|
666
|
+
size="icon"
|
|
667
|
+
className="size-6 shrink-0"
|
|
668
|
+
onClick={() => onRemoveSteer(steer.id)}
|
|
669
|
+
aria-label="Remove queued message"
|
|
670
|
+
>
|
|
671
|
+
<XIcon className="size-3" />
|
|
672
|
+
</Button>
|
|
673
|
+
</TooltipTrigger>
|
|
674
|
+
<TooltipContent>Remove</TooltipContent>
|
|
675
|
+
</Tooltip>
|
|
676
|
+
)}
|
|
677
|
+
</div>
|
|
678
|
+
))}
|
|
679
|
+
</div>
|
|
680
|
+
)}
|
|
638
681
|
<InputGroup className="rounded-2xl border-border/80 shadow-sm transition-shadow focus-within:shadow-md">
|
|
639
682
|
<InputGroupTextarea
|
|
640
683
|
ref={textareaRef}
|
|
@@ -665,38 +708,17 @@ export const ChatView = ({
|
|
|
665
708
|
) : (
|
|
666
709
|
<>
|
|
667
710
|
{/*
|
|
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).
|
|
711
|
+
* The primary button sends. Submitting while a turn is
|
|
712
|
+
* running is a "send now": it interrupts the live run and
|
|
713
|
+
* starts a fresh turn with this message immediately (see
|
|
714
|
+
* the driver's sendMessage). Idle, it just sends.
|
|
693
715
|
*/}
|
|
694
716
|
<InputGroupButton
|
|
695
717
|
type="submit"
|
|
696
718
|
size="icon-sm"
|
|
697
719
|
variant="default"
|
|
698
720
|
disabled={!input.trim()}
|
|
699
|
-
aria-label={isRunning ? "
|
|
721
|
+
aria-label={isRunning ? "Send now (interrupts)" : "Send message"}
|
|
700
722
|
>
|
|
701
723
|
<SendIcon className="size-3" />
|
|
702
724
|
</InputGroupButton>
|
|
@@ -19,7 +19,9 @@ import type { MastraStreamResponse } from "../support/mastra-stream";
|
|
|
19
19
|
import {
|
|
20
20
|
createThreadSession,
|
|
21
21
|
DEFAULT_THREAD_SESSION_KEY,
|
|
22
|
+
enqueueSteer,
|
|
22
23
|
isSessionRunning,
|
|
24
|
+
removeSteer as removeSteerFromQueue,
|
|
23
25
|
sessionKey,
|
|
24
26
|
terminateRunningToolEvents,
|
|
25
27
|
type ThreadSession,
|
|
@@ -409,6 +411,11 @@ export const useMastraChat = (
|
|
|
409
411
|
const historyInFlightRef = useRef(false);
|
|
410
412
|
const feedbackByMessageRef = useRef<Record<string, MessageFeedback>>({});
|
|
411
413
|
feedbackByMessageRef.current = activeSession.feedbackByMessage;
|
|
414
|
+
// Drains the next queued steer when a turn ends. Held in a ref because it
|
|
415
|
+
// closes over `runStream`, which is defined below and itself calls
|
|
416
|
+
// `driveStream` (which invokes this) - the ref breaks that cycle without a
|
|
417
|
+
// stale-closure hazard (assigned each render, same pattern as `loadMoreRef`).
|
|
418
|
+
const drainQueueRef = useRef<(threadId: string) => void>(() => {});
|
|
412
419
|
|
|
413
420
|
const writeMessages = useCallback(
|
|
414
421
|
(threadId: string, next: UIMessage[]) => {
|
|
@@ -732,6 +739,8 @@ export const useMastraChat = (
|
|
|
732
739
|
});
|
|
733
740
|
if (getSession(threadId).runToken === token) {
|
|
734
741
|
refreshThreadsSoon();
|
|
742
|
+
// Turn finished on its own: start the oldest queued steer, if any.
|
|
743
|
+
drainQueueRef.current(threadId);
|
|
735
744
|
}
|
|
736
745
|
} catch (caught) {
|
|
737
746
|
if (getSession(threadId).runToken !== token) return;
|
|
@@ -785,6 +794,22 @@ export const useMastraChat = (
|
|
|
785
794
|
[driveStream, mastraClient, agentId, updateSession],
|
|
786
795
|
);
|
|
787
796
|
|
|
797
|
+
// Start the oldest queued steer as the next turn (FIFO drain). Called from
|
|
798
|
+
// `driveStream` when a turn ends normally; a no-op when the queue is empty.
|
|
799
|
+
// Kept behind `drainQueueRef` so `driveStream` can reach it without a
|
|
800
|
+
// definition cycle.
|
|
801
|
+
drainQueueRef.current = (threadId: string) => {
|
|
802
|
+
const queued = getSession(threadId).queuedSteers;
|
|
803
|
+
if (queued.length === 0) return;
|
|
804
|
+
const [head] = queued;
|
|
805
|
+
updateSession(threadId, (session) => ({
|
|
806
|
+
...session,
|
|
807
|
+
queuedSteers: removeSteerFromQueue(session.queuedSteers, head.id),
|
|
808
|
+
}));
|
|
809
|
+
const { next } = appendUserMessage(threadId, head.text);
|
|
810
|
+
void runStream(threadId, next);
|
|
811
|
+
};
|
|
812
|
+
|
|
788
813
|
// Cancel a thread's in-flight run. Defaults to the active thread (the
|
|
789
814
|
// composer stop button), but takes an explicit thread id so the sidebar
|
|
790
815
|
// can cancel a background thread without switching to it. Aborting one
|
|
@@ -901,53 +926,57 @@ export const useMastraChat = (
|
|
|
901
926
|
[activeThreadId, getSession, noteThreadActivity, updateSession, writeMessages],
|
|
902
927
|
);
|
|
903
928
|
|
|
929
|
+
// Send a message on the active thread. Submitting while a turn is already
|
|
930
|
+
// streaming ENQUEUES the message as a steer (it waits, no interrupt) - the
|
|
931
|
+
// queue drains oldest-first when the turn ends, or the user fires one early
|
|
932
|
+
// with `sendSteerNow`. An idle submit starts a turn right away.
|
|
904
933
|
const sendMessage = useCallback<ChatViewProps["sendMessage"]>(
|
|
905
934
|
(message) => {
|
|
906
935
|
const text = message.text ?? "";
|
|
907
936
|
if (!text) return;
|
|
908
937
|
const threadId = activeKey;
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
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
|
-
});
|
|
938
|
+
if (isSessionRunning(getSession(threadId))) {
|
|
939
|
+
updateSession(threadId, (session) => ({
|
|
940
|
+
...session,
|
|
941
|
+
queuedSteers: enqueueSteer(session.queuedSteers, { id: nanoid(), text }),
|
|
942
|
+
}));
|
|
928
943
|
return;
|
|
929
944
|
}
|
|
945
|
+
const { next } = appendUserMessage(threadId, text);
|
|
930
946
|
void runStream(threadId, next);
|
|
931
947
|
},
|
|
932
|
-
[appendUserMessage, runStream, activeKey, getSession,
|
|
948
|
+
[appendUserMessage, runStream, activeKey, getSession, updateSession],
|
|
933
949
|
);
|
|
934
950
|
|
|
935
|
-
//
|
|
936
|
-
//
|
|
937
|
-
//
|
|
938
|
-
//
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
const interrupt = useCallback(
|
|
942
|
-
(message: { text?: string }) => {
|
|
943
|
-
const text = message.text ?? "";
|
|
944
|
-
if (!text) return;
|
|
951
|
+
// Fire a queued steer immediately, out of order: remove it from the queue,
|
|
952
|
+
// append it, and start a turn. `runStream`/`driveStream` supersede any
|
|
953
|
+
// in-flight run (abort + runToken bump + settle running pills), so this
|
|
954
|
+
// interrupts the current turn and sends the chosen steer now.
|
|
955
|
+
const sendSteerNow = useCallback(
|
|
956
|
+
(steerId: string) => {
|
|
945
957
|
const threadId = activeKey;
|
|
946
|
-
const
|
|
947
|
-
|
|
958
|
+
const steer = getSession(threadId).queuedSteers.find((s) => s.id === steerId);
|
|
959
|
+
if (!steer) return;
|
|
960
|
+
updateSession(threadId, (session) => ({
|
|
961
|
+
...session,
|
|
962
|
+
queuedSteers: removeSteerFromQueue(session.queuedSteers, steerId),
|
|
963
|
+
}));
|
|
964
|
+
logger.info("steer:send-now", { threadId });
|
|
965
|
+
const { next } = appendUserMessage(threadId, steer.text);
|
|
948
966
|
void runStream(threadId, next);
|
|
949
967
|
},
|
|
950
|
-
[appendUserMessage, runStream,
|
|
968
|
+
[activeKey, appendUserMessage, getSession, runStream, updateSession],
|
|
969
|
+
);
|
|
970
|
+
|
|
971
|
+
// Drop a queued steer without sending it.
|
|
972
|
+
const removeSteer = useCallback(
|
|
973
|
+
(steerId: string) => {
|
|
974
|
+
updateSession(activeKey, (session) => ({
|
|
975
|
+
...session,
|
|
976
|
+
queuedSteers: removeSteerFromQueue(session.queuedSteers, steerId),
|
|
977
|
+
}));
|
|
978
|
+
},
|
|
979
|
+
[activeKey, updateSession],
|
|
951
980
|
);
|
|
952
981
|
|
|
953
982
|
/**
|
|
@@ -1412,7 +1441,9 @@ export const useMastraChat = (
|
|
|
1412
1441
|
status: activeSession.status,
|
|
1413
1442
|
error: activeSession.error,
|
|
1414
1443
|
sendMessage,
|
|
1415
|
-
|
|
1444
|
+
queuedSteers: activeSession.queuedSteers,
|
|
1445
|
+
onSendSteerNow: sendSteerNow,
|
|
1446
|
+
onRemoveSteer: removeSteer,
|
|
1416
1447
|
regenerate,
|
|
1417
1448
|
onStop: stop,
|
|
1418
1449
|
suggestions,
|
package/src/react/types.ts
CHANGED
|
@@ -41,6 +41,14 @@ export type ToolProgress = GenieWriterEvent;
|
|
|
41
41
|
*/
|
|
42
42
|
export type ChatModelOption = { name: string; displayName?: string };
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* A steer message submitted while a turn was already streaming. It waits in a
|
|
46
|
+
* per-thread queue until the running turn ends (auto-sent oldest-first) or the
|
|
47
|
+
* user fires it early with "send now". `id` is a stable key for rendering /
|
|
48
|
+
* removal.
|
|
49
|
+
*/
|
|
50
|
+
export type QueuedSteer = { id: string; text: string };
|
|
51
|
+
|
|
44
52
|
/** Thumbs reaction a user can leave on an assistant turn. */
|
|
45
53
|
export type FeedbackValue = "up" | "down";
|
|
46
54
|
|
|
@@ -89,16 +97,25 @@ export type ChatViewProps = {
|
|
|
89
97
|
* a host driving `ChatView` itself supplies its own.
|
|
90
98
|
*/
|
|
91
99
|
error?: Error | null;
|
|
100
|
+
/**
|
|
101
|
+
* Send a message on the active thread. Submitting while a turn is streaming
|
|
102
|
+
* ENQUEUES the message as a steer (it waits, no interrupt); the queue drains
|
|
103
|
+
* oldest-first when the turn ends. An idle submit starts a turn immediately.
|
|
104
|
+
*/
|
|
92
105
|
sendMessage: (message: { text: string }) => void;
|
|
93
106
|
/**
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
107
|
+
* Steers submitted mid-turn that are waiting to run (oldest first). When
|
|
108
|
+
* non-empty the composer shows them as chips above the input, each with a
|
|
109
|
+
* "send now" and a remove action.
|
|
110
|
+
*/
|
|
111
|
+
queuedSteers?: QueuedSteer[];
|
|
112
|
+
/**
|
|
113
|
+
* Fire a queued steer immediately, out of order: interrupt the current turn
|
|
114
|
+
* and start a fresh turn with that steer (removing it from the queue).
|
|
100
115
|
*/
|
|
101
|
-
|
|
116
|
+
onSendSteerNow?: (steerId: string) => void;
|
|
117
|
+
/** Drop a queued steer without sending it. */
|
|
118
|
+
onRemoveSteer?: (steerId: string) => void;
|
|
102
119
|
regenerate?: () => void;
|
|
103
120
|
/**
|
|
104
121
|
* 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
|
|
@@ -3,9 +3,12 @@ import type {
|
|
|
3
3
|
ChatStatus,
|
|
4
4
|
MessageFeedback,
|
|
5
5
|
PendingApproval,
|
|
6
|
+
QueuedSteer,
|
|
6
7
|
ToolEvent,
|
|
7
8
|
} from "../react/types";
|
|
8
9
|
|
|
10
|
+
export type { QueuedSteer } from "../react/types";
|
|
11
|
+
|
|
9
12
|
/** Session-scoped transcript + stream state for one conversation thread. */
|
|
10
13
|
export type ThreadSession = {
|
|
11
14
|
messages: UIMessage[];
|
|
@@ -22,6 +25,8 @@ export type ThreadSession = {
|
|
|
22
25
|
hasMoreHistory: boolean;
|
|
23
26
|
historyPage: number;
|
|
24
27
|
lastUserText: string | null;
|
|
28
|
+
/** Steers submitted mid-turn, waiting to run (oldest first). */
|
|
29
|
+
queuedSteers: QueuedSteer[];
|
|
25
30
|
};
|
|
26
31
|
|
|
27
32
|
/** Map key for the classic single-thread chat (no explicit thread id). */
|
|
@@ -43,6 +48,7 @@ export function createThreadSession(): ThreadSession {
|
|
|
43
48
|
hasMoreHistory: false,
|
|
44
49
|
historyPage: 0,
|
|
45
50
|
lastUserText: null,
|
|
51
|
+
queuedSteers: [],
|
|
46
52
|
};
|
|
47
53
|
}
|
|
48
54
|
|
|
@@ -50,6 +56,19 @@ export function isSessionRunning(session: ThreadSession): boolean {
|
|
|
50
56
|
return session.status === "submitted" || session.status === "streaming";
|
|
51
57
|
}
|
|
52
58
|
|
|
59
|
+
/** Append a steer to the queue (oldest first). Returns a new array. */
|
|
60
|
+
export function enqueueSteer(
|
|
61
|
+
queue: QueuedSteer[],
|
|
62
|
+
steer: QueuedSteer,
|
|
63
|
+
): QueuedSteer[] {
|
|
64
|
+
return [...queue, steer];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Remove the steer with `id` from the queue. Returns a new array. */
|
|
68
|
+
export function removeSteer(queue: QueuedSteer[], id: string): QueuedSteer[] {
|
|
69
|
+
return queue.filter((steer) => steer.id !== id);
|
|
70
|
+
}
|
|
71
|
+
|
|
53
72
|
/**
|
|
54
73
|
* Settle any tool-progress pills still marked `running` to `done`. A cancelled
|
|
55
74
|
* or interrupted turn stops delivering the `tool-result` / `tool-error` chunks
|