@dbx-tools/ui-mastra 0.3.7 → 0.3.9
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 +16 -3
- package/package.json +7 -7
- package/src/react/chat-view.tsx +37 -37
- package/src/react/mastra-chat.tsx +113 -42
- package/src/react/thread-sidebar.tsx +36 -6
- package/src/react/types.ts +8 -1
- package/src/support/mastra-client.ts +138 -76
- package/src/support/thread-sessions.ts +22 -0
package/README.md
CHANGED
|
@@ -140,13 +140,26 @@ import {
|
|
|
140
140
|
} from "@dbx-tools/ui-mastra/react";
|
|
141
141
|
|
|
142
142
|
const client = new MastraPluginClient(clientConfig);
|
|
143
|
-
|
|
144
|
-
|
|
143
|
+
|
|
144
|
+
// Routing (thread + model) is passed per call, so concurrent runs on
|
|
145
|
+
// different threads never share state.
|
|
146
|
+
const stream = await client.streamAgent({
|
|
147
|
+
agentId: client.defaultAgent,
|
|
148
|
+
messages: [{ role: "user", content: "Hello" }],
|
|
149
|
+
runId,
|
|
150
|
+
threadId: activeThreadId,
|
|
151
|
+
model: "claude sonnet",
|
|
152
|
+
signal: controller.signal,
|
|
153
|
+
});
|
|
145
154
|
|
|
146
155
|
const models = await client.models();
|
|
147
|
-
const history = await client.history({ page: 0, perPage: 20 });
|
|
156
|
+
const history = await client.history({ threadId: activeThreadId, page: 0, perPage: 20 });
|
|
148
157
|
```
|
|
149
158
|
|
|
159
|
+
Each conversation thread runs independently: start a turn on one thread, switch
|
|
160
|
+
to another and start a second, and both stream concurrently. Cancel one via its
|
|
161
|
+
`AbortSignal` (or the driver's `onCancelThread`) without touching the others.
|
|
162
|
+
|
|
150
163
|
`MastraPluginClient` extends `@mastra/client-js` with the AppKit-Mastra custom
|
|
151
164
|
routes. It uses `credentials: "include"` so session cookies travel with streaming
|
|
152
165
|
and REST calls. The React hooks wrap common route calls for model catalogues,
|
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-
|
|
30
|
-
"@dbx-tools/shared-
|
|
31
|
-
"@dbx-tools/shared-
|
|
32
|
-
"@dbx-tools/ui-appkit": "0.3.
|
|
33
|
-
"@dbx-tools/shared-
|
|
34
|
-
"@dbx-tools/ui-branding": "0.3.
|
|
29
|
+
"@dbx-tools/shared-genie": "0.3.9",
|
|
30
|
+
"@dbx-tools/shared-mastra": "0.3.9",
|
|
31
|
+
"@dbx-tools/shared-model": "0.3.9",
|
|
32
|
+
"@dbx-tools/ui-appkit": "0.3.9",
|
|
33
|
+
"@dbx-tools/shared-core": "0.3.9",
|
|
34
|
+
"@dbx-tools/ui-branding": "0.3.9"
|
|
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.9",
|
|
42
42
|
"types": "index.ts",
|
|
43
43
|
"type": "module",
|
|
44
44
|
"exports": {
|
package/src/react/chat-view.tsx
CHANGED
|
@@ -47,7 +47,7 @@ import React, { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
|
47
47
|
import { AssistantBubble, UserBubble } from "./bubbles";
|
|
48
48
|
import { ExportMenu } from "./export-menu";
|
|
49
49
|
import { SuggestionPills } from "./suggestion-pills";
|
|
50
|
-
import { ThreadSidebar } from "./thread-sidebar";
|
|
50
|
+
import { ThreadSidebar, type ThreadSidebarProps } from "./thread-sidebar";
|
|
51
51
|
import type { ChatViewProps } from "./types";
|
|
52
52
|
|
|
53
53
|
// Controlled, presentational chat shell: the scroll container, header
|
|
@@ -124,6 +124,7 @@ export const ChatView = ({
|
|
|
124
124
|
onNewThread,
|
|
125
125
|
onDeleteThread,
|
|
126
126
|
onRenameThread,
|
|
127
|
+
onCancelThread,
|
|
127
128
|
sidebarOpen: sidebarOpenProp,
|
|
128
129
|
onToggleSidebar,
|
|
129
130
|
onExportConversation,
|
|
@@ -246,11 +247,10 @@ export const ChatView = ({
|
|
|
246
247
|
|
|
247
248
|
const handleSubmit = (e: React.FormEvent) => {
|
|
248
249
|
e.preventDefault();
|
|
249
|
-
// Don't queue a new turn while one is streaming - the Enter-key
|
|
250
|
-
// path would otherwise bypass the disabled Send button.
|
|
251
|
-
if (isRunning) return;
|
|
252
250
|
const text = input.trim();
|
|
253
251
|
if (!text) return;
|
|
252
|
+
// Submitting while a turn streams is a steer: the driver hands the text
|
|
253
|
+
// to the live run (or interrupts + resends). Idle submits start a turn.
|
|
254
254
|
sendMessage({ text });
|
|
255
255
|
setInput("");
|
|
256
256
|
// Sending is an explicit "I want to see the response" action, so
|
|
@@ -359,6 +359,23 @@ export const ChatView = ({
|
|
|
359
359
|
const showHeader = showSidebarToggle;
|
|
360
360
|
const showComposerToolbar = showModelDisplay || showExport || showClear;
|
|
361
361
|
|
|
362
|
+
// Props shared by the mobile drawer and the desktop inline sidebar - the two
|
|
363
|
+
// render the SAME `ThreadSidebar`, differing only in framing (overlay vs.
|
|
364
|
+
// inline) and, on mobile, closing the drawer after select / new. Building
|
|
365
|
+
// the prop bag once keeps the two call sites from drifting.
|
|
366
|
+
const sidebarProps: ThreadSidebarProps = {
|
|
367
|
+
threads: threads ?? [],
|
|
368
|
+
...(activeThreadId ? { activeThreadId } : {}),
|
|
369
|
+
streamingThreadIds,
|
|
370
|
+
isLoading: isLoadingThreads,
|
|
371
|
+
onSelect: (id) => onSelectThread?.(id),
|
|
372
|
+
onHide: toggleSidebar,
|
|
373
|
+
...(onNewThread ? { onNew: onNewThread } : {}),
|
|
374
|
+
...(onDeleteThread ? { onDelete: onDeleteThread } : {}),
|
|
375
|
+
...(onRenameThread ? { onRename: onRenameThread } : {}),
|
|
376
|
+
...(onCancelThread ? { onCancel: onCancelThread } : {}),
|
|
377
|
+
};
|
|
378
|
+
|
|
362
379
|
// Clear confirmation is an AppKit `AlertDialog` (a real modal), plus an
|
|
363
380
|
// in-flight flag so the DELETE can't be double-fired. `clearing` disables
|
|
364
381
|
// the confirm action while `onClear` runs; the dialog closes on settle.
|
|
@@ -394,9 +411,9 @@ export const ChatView = ({
|
|
|
394
411
|
/*
|
|
395
412
|
* Mobile: a fixed overlay drawer with a tap-to-close backdrop, so
|
|
396
413
|
* the conversation list never eats horizontal space from the chat
|
|
397
|
-
* on a phone. Selecting a thread / starting a new one closes
|
|
398
|
-
* the transcript comes back into view. Session-only
|
|
399
|
-
* closed (see `mobileDrawerOpen`).
|
|
414
|
+
* on a phone. Selecting a thread / starting a new one also closes
|
|
415
|
+
* the drawer so the transcript comes back into view. Session-only
|
|
416
|
+
* + default closed (see `mobileDrawerOpen`).
|
|
400
417
|
*/
|
|
401
418
|
mobileDrawerOpen && (
|
|
402
419
|
<div className="fixed inset-0 z-40 flex">
|
|
@@ -406,10 +423,7 @@ export const ChatView = ({
|
|
|
406
423
|
aria-hidden="true"
|
|
407
424
|
/>
|
|
408
425
|
<ThreadSidebar
|
|
409
|
-
|
|
410
|
-
{...(activeThreadId ? { activeThreadId } : {})}
|
|
411
|
-
streamingThreadIds={streamingThreadIds}
|
|
412
|
-
isLoading={isLoadingThreads}
|
|
426
|
+
{...sidebarProps}
|
|
413
427
|
onSelect={(id) => {
|
|
414
428
|
onSelectThread?.(id);
|
|
415
429
|
toggleSidebar();
|
|
@@ -422,9 +436,6 @@ export const ChatView = ({
|
|
|
422
436
|
},
|
|
423
437
|
}
|
|
424
438
|
: {})}
|
|
425
|
-
{...(onDeleteThread ? { onDelete: onDeleteThread } : {})}
|
|
426
|
-
{...(onRenameThread ? { onRename: onRenameThread } : {})}
|
|
427
|
-
onHide={toggleSidebar}
|
|
428
439
|
className="relative z-10 w-[85vw] max-w-xs shadow-xl"
|
|
429
440
|
/>
|
|
430
441
|
</div>
|
|
@@ -432,21 +443,11 @@ export const ChatView = ({
|
|
|
432
443
|
) : (
|
|
433
444
|
/*
|
|
434
445
|
* Desktop: an inline flex child sharing the row with the chat
|
|
435
|
-
* column, using the persisted open/hide preference.
|
|
446
|
+
* column, using the persisted open/hide preference. Same
|
|
447
|
+
* `sidebarProps` as mobile - only the framing + close-on-select
|
|
448
|
+
* differ.
|
|
436
449
|
*/
|
|
437
|
-
desktopSidebarOpen &&
|
|
438
|
-
<ThreadSidebar
|
|
439
|
-
threads={threads ?? []}
|
|
440
|
-
{...(activeThreadId ? { activeThreadId } : {})}
|
|
441
|
-
streamingThreadIds={streamingThreadIds}
|
|
442
|
-
isLoading={isLoadingThreads}
|
|
443
|
-
onSelect={(id) => onSelectThread?.(id)}
|
|
444
|
-
{...(onNewThread ? { onNew: onNewThread } : {})}
|
|
445
|
-
{...(onDeleteThread ? { onDelete: onDeleteThread } : {})}
|
|
446
|
-
{...(onRenameThread ? { onRename: onRenameThread } : {})}
|
|
447
|
-
onHide={toggleSidebar}
|
|
448
|
-
/>
|
|
449
|
-
)
|
|
450
|
+
desktopSidebarOpen && <ThreadSidebar {...sidebarProps} />
|
|
450
451
|
))}
|
|
451
452
|
<div className="flex h-full min-w-0 flex-1 flex-col">
|
|
452
453
|
{showHeader && (
|
|
@@ -625,29 +626,28 @@ export const ChatView = ({
|
|
|
625
626
|
className="max-h-48 text-base md:text-sm"
|
|
626
627
|
/>
|
|
627
628
|
<InputGroupAddon align="inline-end">
|
|
628
|
-
{isRunning && onStop ? (
|
|
629
|
+
{isRunning && onStop && !input.trim() ? (
|
|
630
|
+
// Running with an empty composer: the button stops the turn.
|
|
629
631
|
<InputGroupButton
|
|
630
632
|
type="button"
|
|
631
633
|
size="icon-sm"
|
|
632
634
|
variant="default"
|
|
633
|
-
onClick={onStop}
|
|
635
|
+
onClick={() => onStop()}
|
|
634
636
|
aria-label="Stop response"
|
|
635
637
|
>
|
|
636
638
|
<SquareIcon className="size-3 fill-current" />
|
|
637
639
|
</InputGroupButton>
|
|
638
640
|
) : (
|
|
641
|
+
// Idle, or running with typed text: the button sends. A send
|
|
642
|
+
// mid-run steers the live turn (see the driver's sendMessage).
|
|
639
643
|
<InputGroupButton
|
|
640
644
|
type="submit"
|
|
641
645
|
size="icon-sm"
|
|
642
646
|
variant="default"
|
|
643
|
-
disabled={!input.trim()
|
|
644
|
-
aria-label="Send message"
|
|
647
|
+
disabled={!input.trim()}
|
|
648
|
+
aria-label={isRunning ? "Steer response" : "Send message"}
|
|
645
649
|
>
|
|
646
|
-
|
|
647
|
-
<Spinner className="size-3" />
|
|
648
|
-
) : (
|
|
649
|
-
<SendIcon className="size-3" />
|
|
650
|
-
)}
|
|
650
|
+
<SendIcon className="size-3" />
|
|
651
651
|
</InputGroupButton>
|
|
652
652
|
)}
|
|
653
653
|
</InputGroupAddon>
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
DEFAULT_THREAD_SESSION_KEY,
|
|
22
22
|
isSessionRunning,
|
|
23
23
|
sessionKey,
|
|
24
|
+
terminateRunningToolEvents,
|
|
24
25
|
type ThreadSession,
|
|
25
26
|
} from "../support/thread-sessions";
|
|
26
27
|
import { ChatView } from "./chat-view";
|
|
@@ -246,18 +247,20 @@ export const useMastraChat = (
|
|
|
246
247
|
// using it as a hook dep doesn't refire the initial-history fetch on
|
|
247
248
|
// every parent render.
|
|
248
249
|
const mastraClient = useMastraClient();
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
250
|
+
// The selected model rides each turn as a per-call override (see
|
|
251
|
+
// `runStream`), never stored on the shared client - so two threads can
|
|
252
|
+
// stream under different models without clobbering each other. Mirror it
|
|
253
|
+
// into a ref so the send path reads the latest selection without adding
|
|
254
|
+
// `model` to its dependency list.
|
|
255
|
+
const modelRef = useRef(model);
|
|
256
|
+
modelRef.current = model;
|
|
255
257
|
const agentId = options.agentId ?? mastraClient.defaultAgent;
|
|
256
258
|
// Built-in conversation management. When on, the chat always drives an
|
|
257
259
|
// explicit client thread id (rather than leaning on the per-session
|
|
258
260
|
// cookie) so it can reference, persist, and switch between the
|
|
259
|
-
// conversations a user owns.
|
|
260
|
-
//
|
|
261
|
+
// conversations a user owns. Each call (stream / history / clear) carries
|
|
262
|
+
// its thread id per request, so many threads can run concurrently without
|
|
263
|
+
// sharing routing state.
|
|
261
264
|
const enableThreads = options.enableThreads !== false;
|
|
262
265
|
// Export is opt-in (default off): the host turns it on explicitly.
|
|
263
266
|
const enableExport = options.enableExport === true;
|
|
@@ -693,7 +696,7 @@ export const useMastraChat = (
|
|
|
693
696
|
async (
|
|
694
697
|
threadId: string,
|
|
695
698
|
assistantId: string,
|
|
696
|
-
open: () => Promise<MastraStreamResponse>,
|
|
699
|
+
open: (signal: AbortSignal) => Promise<MastraStreamResponse>,
|
|
697
700
|
) => {
|
|
698
701
|
const controller = new AbortController();
|
|
699
702
|
let token = 0;
|
|
@@ -708,14 +711,14 @@ export const useMastraChat = (
|
|
|
708
711
|
error: null,
|
|
709
712
|
status: "submitted",
|
|
710
713
|
runToken: token,
|
|
714
|
+
// Superseding an in-flight run (a steer that interrupts, or a rapid
|
|
715
|
+
// re-send) stops its stream, so close any pills it left running.
|
|
716
|
+
toolEventsByMessage: terminateRunningToolEvents(session.toolEventsByMessage),
|
|
711
717
|
};
|
|
712
718
|
});
|
|
713
719
|
const runIdRef = { current: getSession(threadId).runId };
|
|
714
720
|
try {
|
|
715
|
-
const
|
|
716
|
-
threadId === DEFAULT_THREAD_SESSION_KEY ? undefined : threadId;
|
|
717
|
-
mastraClient.setThreadId(streamThreadId);
|
|
718
|
-
const stream = await open();
|
|
721
|
+
const stream = await open(controller.signal);
|
|
719
722
|
await processStream(threadId, stream, assistantId, runIdRef, controller.signal);
|
|
720
723
|
updateSession(threadId, (session) => {
|
|
721
724
|
if (session.runToken !== token) return session;
|
|
@@ -745,7 +748,7 @@ export const useMastraChat = (
|
|
|
745
748
|
}));
|
|
746
749
|
}
|
|
747
750
|
},
|
|
748
|
-
[getSession,
|
|
751
|
+
[getSession, processStream, refreshThreadsSoon, updateSession],
|
|
749
752
|
);
|
|
750
753
|
|
|
751
754
|
const runStream = useCallback(
|
|
@@ -757,35 +760,56 @@ export const useMastraChat = (
|
|
|
757
760
|
assistantId,
|
|
758
761
|
runId,
|
|
759
762
|
}));
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
+
// Capture this run's thread + model at send time and pass them per
|
|
764
|
+
// call, so a run keeps its own routing even if the user switches
|
|
765
|
+
// threads or changes the model picker while it streams.
|
|
766
|
+
const streamThreadId =
|
|
767
|
+
threadId === DEFAULT_THREAD_SESSION_KEY ? undefined : threadId;
|
|
768
|
+
const model = modelRef.current || undefined;
|
|
769
|
+
return driveStream(threadId, assistantId, (signal) => {
|
|
770
|
+
const messages = history.flatMap((m) =>
|
|
763
771
|
m.parts
|
|
764
772
|
.filter((p): p is { type: "text"; text: string } => p.type === "text")
|
|
765
773
|
.map((p) => ({ role: m.role, content: p.text })),
|
|
766
|
-
)
|
|
767
|
-
return
|
|
774
|
+
);
|
|
775
|
+
return mastraClient.streamAgent({
|
|
776
|
+
agentId,
|
|
777
|
+
messages,
|
|
768
778
|
runId,
|
|
769
|
-
|
|
779
|
+
threadId: streamThreadId,
|
|
780
|
+
model,
|
|
781
|
+
signal,
|
|
782
|
+
});
|
|
770
783
|
});
|
|
771
784
|
},
|
|
772
785
|
[driveStream, mastraClient, agentId, updateSession],
|
|
773
786
|
);
|
|
774
787
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
788
|
+
// Cancel a thread's in-flight run. Defaults to the active thread (the
|
|
789
|
+
// composer stop button), but takes an explicit thread id so the sidebar
|
|
790
|
+
// can cancel a background thread without switching to it. Aborting one
|
|
791
|
+
// thread's controller leaves every other run streaming - each run owns
|
|
792
|
+
// its own controller and routing.
|
|
793
|
+
const stop = useCallback(
|
|
794
|
+
(threadId?: string) => {
|
|
795
|
+
const key = threadId ?? activeKey;
|
|
796
|
+
updateSession(key, (session) => {
|
|
797
|
+
if (!isSessionRunning(session)) return session;
|
|
798
|
+
session.abortController?.abort();
|
|
799
|
+
return {
|
|
800
|
+
...session,
|
|
801
|
+
abortController: null,
|
|
802
|
+
runToken: session.runToken + 1,
|
|
803
|
+
error: null,
|
|
804
|
+
status: "ready",
|
|
805
|
+
// Cancelling stops the stream, so close any tool pills still marked
|
|
806
|
+
// running - the closing chunks will never arrive.
|
|
807
|
+
toolEventsByMessage: terminateRunningToolEvents(session.toolEventsByMessage),
|
|
808
|
+
};
|
|
809
|
+
});
|
|
810
|
+
},
|
|
811
|
+
[activeKey, updateSession],
|
|
812
|
+
);
|
|
789
813
|
|
|
790
814
|
/**
|
|
791
815
|
* Approve or deny an in-flight `requireApproval` tool call. The
|
|
@@ -830,10 +854,22 @@ export const useMastraChat = (
|
|
|
830
854
|
toolCallId,
|
|
831
855
|
runId,
|
|
832
856
|
});
|
|
833
|
-
|
|
857
|
+
const streamThreadId =
|
|
858
|
+
activeKey === DEFAULT_THREAD_SESSION_KEY ? undefined : activeKey;
|
|
859
|
+
await driveStream(activeKey, assistantId, (signal) =>
|
|
834
860
|
decision.approved
|
|
835
|
-
? mastraClient.approveToolCallStream(agentId, {
|
|
836
|
-
|
|
861
|
+
? mastraClient.approveToolCallStream(agentId, {
|
|
862
|
+
runId,
|
|
863
|
+
toolCallId,
|
|
864
|
+
threadId: streamThreadId,
|
|
865
|
+
signal,
|
|
866
|
+
})
|
|
867
|
+
: mastraClient.declineToolCallStream(agentId, {
|
|
868
|
+
runId,
|
|
869
|
+
toolCallId,
|
|
870
|
+
threadId: streamThreadId,
|
|
871
|
+
signal,
|
|
872
|
+
}),
|
|
837
873
|
);
|
|
838
874
|
},
|
|
839
875
|
[activeKey, driveStream, getSession, mastraClient, agentId, updateSession],
|
|
@@ -856,8 +892,30 @@ export const useMastraChat = (
|
|
|
856
892
|
}
|
|
857
893
|
}
|
|
858
894
|
}
|
|
859
|
-
const
|
|
895
|
+
const before = getSession(threadId);
|
|
896
|
+
const next = [...before.messages, makeUserMessage(text)];
|
|
860
897
|
writeMessages(threadId, next);
|
|
898
|
+
// Steering: if a turn is already streaming on this thread, try to hand
|
|
899
|
+
// the message to the live run so the agent folds it into the current
|
|
900
|
+
// turn (no restart). If the server won't deliver it, fall back to
|
|
901
|
+
// interrupting the run and starting a fresh turn with the message
|
|
902
|
+
// included, so a steer never silently drops.
|
|
903
|
+
if (isSessionRunning(before) && before.runId) {
|
|
904
|
+
const runId = before.runId;
|
|
905
|
+
const steerThreadId =
|
|
906
|
+
threadId === DEFAULT_THREAD_SESSION_KEY ? undefined : threadId;
|
|
907
|
+
void mastraClient
|
|
908
|
+
.queueMessage({ agentId, runId, threadId: steerThreadId, message: text })
|
|
909
|
+
.then((accepted) => {
|
|
910
|
+
if (accepted) {
|
|
911
|
+
logger.info("steer:queued", { runId });
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
logger.info("steer:fallback-interrupt", { runId });
|
|
915
|
+
void runStream(threadId, getSession(threadId).messages);
|
|
916
|
+
});
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
861
919
|
void runStream(threadId, next);
|
|
862
920
|
},
|
|
863
921
|
[
|
|
@@ -866,6 +924,8 @@ export const useMastraChat = (
|
|
|
866
924
|
activeThreadId,
|
|
867
925
|
activeKey,
|
|
868
926
|
getSession,
|
|
927
|
+
mastraClient,
|
|
928
|
+
agentId,
|
|
869
929
|
noteThreadActivity,
|
|
870
930
|
updateSession,
|
|
871
931
|
],
|
|
@@ -882,7 +942,7 @@ export const useMastraChat = (
|
|
|
882
942
|
const handleClear = useCallback(async () => {
|
|
883
943
|
const threadId = activeKey;
|
|
884
944
|
try {
|
|
885
|
-
const result = await mastraClient.clearHistory({ agentId });
|
|
945
|
+
const result = await mastraClient.clearHistory({ agentId, threadId: activeThreadId });
|
|
886
946
|
logger.info("history cleared", { cleared: result.cleared });
|
|
887
947
|
} catch (error) {
|
|
888
948
|
logger.error("history clear error", {
|
|
@@ -1039,7 +1099,6 @@ export const useMastraChat = (
|
|
|
1039
1099
|
// refetching or aborting other threads' runs.
|
|
1040
1100
|
useEffect(() => {
|
|
1041
1101
|
const threadId = activeKey;
|
|
1042
|
-
mastraClient.setThreadId(activeThreadId);
|
|
1043
1102
|
const session = getSession(threadId);
|
|
1044
1103
|
if (session.historyLoaded) {
|
|
1045
1104
|
setIsLoadingHistory(false);
|
|
@@ -1053,6 +1112,7 @@ export const useMastraChat = (
|
|
|
1053
1112
|
mastraClient
|
|
1054
1113
|
.history({
|
|
1055
1114
|
agentId,
|
|
1115
|
+
threadId: activeThreadId,
|
|
1056
1116
|
page: 0,
|
|
1057
1117
|
perPage: HISTORY_PAGE_SIZE,
|
|
1058
1118
|
signal: controller.signal,
|
|
@@ -1096,7 +1156,7 @@ export const useMastraChat = (
|
|
|
1096
1156
|
const page = session.historyPage;
|
|
1097
1157
|
updateSession(threadId, (current) => ({ ...current, historyPage: page + 1 }));
|
|
1098
1158
|
mastraClient
|
|
1099
|
-
.history({ agentId, page, perPage: HISTORY_PAGE_SIZE })
|
|
1159
|
+
.history({ agentId, threadId: activeThreadId, page, perPage: HISTORY_PAGE_SIZE })
|
|
1100
1160
|
.then((response) => {
|
|
1101
1161
|
const uiMessages = response.uiMessages as unknown as UIMessage[];
|
|
1102
1162
|
if (uiMessages.length > 0) {
|
|
@@ -1123,7 +1183,15 @@ export const useMastraChat = (
|
|
|
1123
1183
|
historyInFlightRef.current = false;
|
|
1124
1184
|
setIsLoadingMore(false);
|
|
1125
1185
|
});
|
|
1126
|
-
}, [
|
|
1186
|
+
}, [
|
|
1187
|
+
activeKey,
|
|
1188
|
+
activeThreadId,
|
|
1189
|
+
getSession,
|
|
1190
|
+
mastraClient,
|
|
1191
|
+
agentId,
|
|
1192
|
+
updateSession,
|
|
1193
|
+
writeMessages,
|
|
1194
|
+
]);
|
|
1127
1195
|
|
|
1128
1196
|
// Chat export (opt-in). Resolves `[chart:<id>]` / `[data:<id>]` embeds
|
|
1129
1197
|
// straight off the client so the export inlines the same charts /
|
|
@@ -1356,6 +1424,9 @@ export const useMastraChat = (
|
|
|
1356
1424
|
onNewThread: newThread,
|
|
1357
1425
|
onDeleteThread: deleteThread,
|
|
1358
1426
|
onRenameThread: renameThread,
|
|
1427
|
+
// Cancel a background thread's run from the sidebar without
|
|
1428
|
+
// switching to it.
|
|
1429
|
+
onCancelThread: stop,
|
|
1359
1430
|
// Persisted sidebar visibility, controlled from the driver so
|
|
1360
1431
|
// the show/hide choice survives reloads.
|
|
1361
1432
|
sidebarOpen,
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
MessageSquarePlusIcon,
|
|
14
14
|
PanelLeftIcon,
|
|
15
15
|
PencilIcon,
|
|
16
|
+
SquareIcon,
|
|
16
17
|
Trash2Icon,
|
|
17
18
|
} from "lucide-react";
|
|
18
19
|
import { useState } from "react";
|
|
@@ -42,6 +43,12 @@ export interface ThreadSidebarProps {
|
|
|
42
43
|
onDelete?: (threadId: string) => void;
|
|
43
44
|
/** Rename a thread. Per-row edit affordance (inline text field) hidden when omitted. */
|
|
44
45
|
onRename?: (threadId: string, title: string) => void;
|
|
46
|
+
/**
|
|
47
|
+
* Cancel a thread's in-flight run. When provided, a streaming row (one in
|
|
48
|
+
* {@link streamingThreadIds}) shows a stop button in place of its spinner
|
|
49
|
+
* on hover, so a background run can be cancelled without switching to it.
|
|
50
|
+
*/
|
|
51
|
+
onCancel?: (threadId: string) => void;
|
|
45
52
|
/** Collapse the sidebar. Renders the hide button in the header when provided. */
|
|
46
53
|
onHide?: () => void;
|
|
47
54
|
/** Extra classes merged onto the sidebar root. */
|
|
@@ -67,6 +74,7 @@ export const ThreadSidebar = ({
|
|
|
67
74
|
onNew,
|
|
68
75
|
onDelete,
|
|
69
76
|
onRename,
|
|
77
|
+
onCancel,
|
|
70
78
|
onHide,
|
|
71
79
|
className,
|
|
72
80
|
}: ThreadSidebarProps) => {
|
|
@@ -212,12 +220,34 @@ export const ThreadSidebar = ({
|
|
|
212
220
|
>
|
|
213
221
|
<div className="min-w-0 flex-1">
|
|
214
222
|
<div className="flex items-center gap-1.5 truncate">
|
|
215
|
-
{isStreaming &&
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
223
|
+
{isStreaming &&
|
|
224
|
+
(onCancel ? (
|
|
225
|
+
<Tooltip>
|
|
226
|
+
<TooltipTrigger asChild>
|
|
227
|
+
<Button
|
|
228
|
+
type="button"
|
|
229
|
+
variant="ghost"
|
|
230
|
+
size="icon"
|
|
231
|
+
onClick={(e) => {
|
|
232
|
+
e.stopPropagation();
|
|
233
|
+
onCancel(thread.id);
|
|
234
|
+
}}
|
|
235
|
+
aria-label="Stop generating"
|
|
236
|
+
className="size-4 shrink-0 text-primary"
|
|
237
|
+
>
|
|
238
|
+
{/* Spinner by default; swap to a stop square on hover/focus. */}
|
|
239
|
+
<Loader2Icon className="size-3 animate-spin group-hover:hidden" />
|
|
240
|
+
<SquareIcon className="hidden size-3 fill-current group-hover:block" />
|
|
241
|
+
</Button>
|
|
242
|
+
</TooltipTrigger>
|
|
243
|
+
<TooltipContent>Stop generating</TooltipContent>
|
|
244
|
+
</Tooltip>
|
|
245
|
+
) : (
|
|
246
|
+
<Loader2Icon
|
|
247
|
+
aria-label="Streaming"
|
|
248
|
+
className="size-3 shrink-0 animate-spin text-primary"
|
|
249
|
+
/>
|
|
250
|
+
))}
|
|
221
251
|
<span className="truncate">{threadTitle(thread)}</span>
|
|
222
252
|
</div>
|
|
223
253
|
{thread.updatedAt && (
|
package/src/react/types.ts
CHANGED
|
@@ -99,7 +99,7 @@ export type ChatViewProps = {
|
|
|
99
99
|
* to hide the Stop affordance (the Send button just disables while a
|
|
100
100
|
* response streams).
|
|
101
101
|
*/
|
|
102
|
-
onStop?: () => void;
|
|
102
|
+
onStop?: (threadId?: string) => void;
|
|
103
103
|
/** Extra classes merged onto the root layout container. */
|
|
104
104
|
className?: string;
|
|
105
105
|
/**
|
|
@@ -220,6 +220,13 @@ export type ChatViewProps = {
|
|
|
220
220
|
* affordance that swaps the title into an inline text field.
|
|
221
221
|
*/
|
|
222
222
|
onRenameThread?: (threadId: string, title: string) => void;
|
|
223
|
+
/**
|
|
224
|
+
* Cancel a thread's in-flight run by id, without switching to it. When
|
|
225
|
+
* provided, each running sidebar row shows a stop affordance next to its
|
|
226
|
+
* streaming indicator (see {@link streamingThreadIds}). Isolated to that
|
|
227
|
+
* thread - other threads keep streaming.
|
|
228
|
+
*/
|
|
229
|
+
onCancelThread?: (threadId: string) => void;
|
|
223
230
|
/**
|
|
224
231
|
* Controlled open/closed state for the conversation sidebar. When
|
|
225
232
|
* omitted the view manages its own (session-only) open state; pass
|
|
@@ -72,43 +72,99 @@ export class MastraPluginClient extends MastraClient {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
* (fuzzy-matched server-side) or a model class slug
|
|
80
|
-
* (`ModelClass.ChatFast` / `"chat-thinking"`); pass `undefined` to
|
|
81
|
-
* fall back to the agent's configured default.
|
|
75
|
+
* Build the per-request routing headers for a call: the thread-selection
|
|
76
|
+
* header (`THREAD_ID_HEADER`) so the server pins `RequestContext`'s thread
|
|
77
|
+
* id, and the model-override header (`MODEL_OVERRIDE_HEADER`) so the server
|
|
78
|
+
* swaps the resolved model for that request without an agent redeploy.
|
|
82
79
|
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
80
|
+
* Routing is per-CALL - never stored on the shared client - so concurrent
|
|
81
|
+
* runs on different threads / models can't clobber each other's headers.
|
|
82
|
+
* `threadId` is a conversation id (omit to fall back to the per-session
|
|
83
|
+
* cookie thread); `model` is a concrete endpoint name (fuzzy-matched
|
|
84
|
+
* server-side) or a model-class slug (`"chat-fast"` / `"chat-thinking"`).
|
|
87
85
|
*/
|
|
88
|
-
|
|
89
|
-
const headers
|
|
90
|
-
if (
|
|
91
|
-
|
|
86
|
+
#routingHeaders(routing: { threadId?: string; model?: string }): Record<string, string> {
|
|
87
|
+
const headers: Record<string, string> = {};
|
|
88
|
+
if (routing.threadId) headers[thread.THREAD_ID_HEADER] = routing.threadId;
|
|
89
|
+
if (routing.model) headers[override.MODEL_OVERRIDE_HEADER] = routing.model;
|
|
90
|
+
return headers;
|
|
92
91
|
}
|
|
93
92
|
|
|
94
93
|
/**
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
94
|
+
* Open an agent stream for one conversation turn, routed and cancelled
|
|
95
|
+
* PER CALL. Posts to the agent `/stream` route with this run's own thread
|
|
96
|
+
* id + model as request headers and its own `AbortSignal` on the fetch, so
|
|
97
|
+
* many threads can stream at once and aborting one leaves the others
|
|
98
|
+
* untouched. Reads the SSE directly (like the tool-approval streams) rather
|
|
99
|
+
* than through the inherited `agent.stream()`, whose shared-client headers
|
|
100
|
+
* and single client-level abort signal can't isolate concurrent runs.
|
|
101
|
+
*/
|
|
102
|
+
async streamAgent(params: {
|
|
103
|
+
agentId: string;
|
|
104
|
+
messages: Array<{ role: string; content: string }>;
|
|
105
|
+
runId: string;
|
|
106
|
+
threadId?: string;
|
|
107
|
+
model?: string;
|
|
108
|
+
signal?: AbortSignal;
|
|
109
|
+
}): Promise<MastraStreamResponse> {
|
|
110
|
+
const url = `${this.basePath}/agents/${encodeURIComponent(params.agentId)}/stream`;
|
|
111
|
+
const init: RequestInit = {
|
|
112
|
+
method: "POST",
|
|
113
|
+
credentials: "include",
|
|
114
|
+
headers: {
|
|
115
|
+
"Content-Type": "application/json",
|
|
116
|
+
...this.#routingHeaders(params),
|
|
117
|
+
},
|
|
118
|
+
body: JSON.stringify({ messages: params.messages, runId: params.runId }),
|
|
119
|
+
};
|
|
120
|
+
if (params.signal) init.signal = params.signal;
|
|
121
|
+
const response = await fetch(url, init);
|
|
122
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
123
|
+
return asMastraStreamResponse(response);
|
|
124
|
+
}
|
|
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}.
|
|
101
133
|
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
* these headers, so selecting a thread routes every call at once.
|
|
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.
|
|
107
138
|
*/
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
+
}
|
|
112
168
|
}
|
|
113
169
|
|
|
114
170
|
/**
|
|
@@ -120,7 +176,7 @@ export class MastraPluginClient extends MastraClient {
|
|
|
120
176
|
*/
|
|
121
177
|
async approveToolCallStream(
|
|
122
178
|
agentId: string,
|
|
123
|
-
params: { runId: string; toolCallId: string },
|
|
179
|
+
params: { runId: string; toolCallId: string; threadId?: string; signal?: AbortSignal },
|
|
124
180
|
): Promise<MastraStreamResponse> {
|
|
125
181
|
return this.#toolApprovalStream(agentId, "approve-tool-call", params);
|
|
126
182
|
}
|
|
@@ -131,7 +187,7 @@ export class MastraPluginClient extends MastraClient {
|
|
|
131
187
|
*/
|
|
132
188
|
async declineToolCallStream(
|
|
133
189
|
agentId: string,
|
|
134
|
-
params: { runId: string; toolCallId: string },
|
|
190
|
+
params: { runId: string; toolCallId: string; threadId?: string; signal?: AbortSignal },
|
|
135
191
|
): Promise<MastraStreamResponse> {
|
|
136
192
|
return this.#toolApprovalStream(agentId, "decline-tool-call", params);
|
|
137
193
|
}
|
|
@@ -139,24 +195,22 @@ export class MastraPluginClient extends MastraClient {
|
|
|
139
195
|
async #toolApprovalStream(
|
|
140
196
|
agentId: string,
|
|
141
197
|
route: "approve-tool-call" | "decline-tool-call",
|
|
142
|
-
params: { runId: string; toolCallId: string },
|
|
198
|
+
params: { runId: string; toolCallId: string; threadId?: string; signal?: AbortSignal },
|
|
143
199
|
): Promise<MastraStreamResponse> {
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
200
|
+
const url = `${this.basePath}/agents/${encodeURIComponent(agentId)}/${route}`;
|
|
201
|
+
const init: RequestInit = {
|
|
202
|
+
method: "POST",
|
|
203
|
+
credentials: "include",
|
|
204
|
+
headers: {
|
|
205
|
+
"Content-Type": "application/json",
|
|
206
|
+
...this.#routingHeaders({ threadId: params.threadId }),
|
|
150
207
|
},
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
headers: response.headers,
|
|
158
|
-
}),
|
|
159
|
-
);
|
|
208
|
+
body: JSON.stringify({ runId: params.runId, toolCallId: params.toolCallId }),
|
|
209
|
+
};
|
|
210
|
+
if (params.signal) init.signal = params.signal;
|
|
211
|
+
const response = await fetch(url, init);
|
|
212
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
213
|
+
return asMastraStreamResponse(response);
|
|
160
214
|
}
|
|
161
215
|
|
|
162
216
|
/**
|
|
@@ -209,11 +263,14 @@ export class MastraPluginClient extends MastraClient {
|
|
|
209
263
|
/**
|
|
210
264
|
* Fetch one page of thread history from `GET ${basePath}/history`.
|
|
211
265
|
* Messages come back oldest -> newest so the caller can prepend them
|
|
212
|
-
* to a live transcript.
|
|
266
|
+
* to a live transcript. `threadId` targets a specific conversation
|
|
267
|
+
* (sent as the `?threadId=` query so it doesn't depend on shared client
|
|
268
|
+
* state); omit it to read the per-session cookie thread.
|
|
213
269
|
*/
|
|
214
270
|
async history(
|
|
215
271
|
options: {
|
|
216
272
|
agentId?: string;
|
|
273
|
+
threadId?: string;
|
|
217
274
|
page?: number;
|
|
218
275
|
perPage?: number;
|
|
219
276
|
signal?: AbortSignal;
|
|
@@ -222,6 +279,7 @@ export class MastraPluginClient extends MastraClient {
|
|
|
222
279
|
const params = new URLSearchParams();
|
|
223
280
|
if (options.page !== undefined) params.set("page", String(options.page));
|
|
224
281
|
if (options.perPage !== undefined) params.set("perPage", String(options.perPage));
|
|
282
|
+
if (options.threadId) params.set(thread.THREAD_ID_QUERY, options.threadId);
|
|
225
283
|
const qs = params.toString();
|
|
226
284
|
const base = this.#agentScoped(routes.MASTRA_ROUTES.history, options.agentId);
|
|
227
285
|
return this.#getJson(
|
|
@@ -232,19 +290,26 @@ export class MastraPluginClient extends MastraClient {
|
|
|
232
290
|
}
|
|
233
291
|
|
|
234
292
|
/**
|
|
235
|
-
* Wipe
|
|
236
|
-
*
|
|
237
|
-
*
|
|
293
|
+
* Wipe a thread's history (`DELETE ${basePath}/history`). `threadId`
|
|
294
|
+
* targets a specific conversation via the thread-selection header (so it
|
|
295
|
+
* doesn't depend on shared client state); omit it to clear the per-session
|
|
296
|
+
* cookie thread. The session cookie that anchors the thread id is
|
|
297
|
+
* preserved - only the messages go away. Idempotent: a fresh thread reports
|
|
238
298
|
* `cleared: 0` without erroring.
|
|
239
299
|
*/
|
|
240
300
|
async clearHistory(
|
|
241
|
-
options: { agentId?: string; signal?: AbortSignal } = {},
|
|
301
|
+
options: { agentId?: string; threadId?: string; signal?: AbortSignal } = {},
|
|
242
302
|
): Promise<MastraClearHistoryResponse> {
|
|
243
303
|
return this.#mutateJson(
|
|
244
304
|
this.#agentScoped(routes.MASTRA_ROUTES.history, options.agentId),
|
|
245
305
|
"DELETE",
|
|
246
306
|
wire.MastraClearHistoryResponseSchema,
|
|
247
|
-
|
|
307
|
+
{
|
|
308
|
+
...(options.threadId
|
|
309
|
+
? { headers: { [thread.THREAD_ID_HEADER]: options.threadId } }
|
|
310
|
+
: {}),
|
|
311
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
312
|
+
},
|
|
248
313
|
);
|
|
249
314
|
}
|
|
250
315
|
|
|
@@ -282,9 +347,8 @@ export class MastraPluginClient extends MastraClient {
|
|
|
282
347
|
* clashing with the inherited `MastraClient.deleteThread`, which hits
|
|
283
348
|
* Mastra's stock thread route rather than our OBO-scoped, custom
|
|
284
349
|
* mount). The id is sent as the thread-selection header for this one
|
|
285
|
-
* call
|
|
286
|
-
*
|
|
287
|
-
* while the user stays on another. Idempotent: deleting an unknown /
|
|
350
|
+
* call, so the sidebar can remove any thread while the user stays on
|
|
351
|
+
* another (routing is per call, never shared). Idempotent: deleting an unknown /
|
|
288
352
|
* already-removed thread reports `deleted: false` without erroring.
|
|
289
353
|
*/
|
|
290
354
|
async removeThread(
|
|
@@ -306,9 +370,8 @@ export class MastraPluginClient extends MastraClient {
|
|
|
306
370
|
* Rename a single conversation thread via the plugin's own
|
|
307
371
|
* `PATCH ${basePath}/threads` route. The id travels as the
|
|
308
372
|
* thread-selection header for this one call (mirroring
|
|
309
|
-
* {@link removeThread}, so the sidebar can rename any thread
|
|
310
|
-
*
|
|
311
|
-
* `title` rides in the JSON body. The server enforces ownership and
|
|
373
|
+
* {@link removeThread}, so the sidebar can rename any thread; routing is
|
|
374
|
+
* per call, never shared) and the new `title` rides in the JSON body. The server enforces ownership and
|
|
312
375
|
* echoes back the updated thread, so the caller can reflect the new
|
|
313
376
|
* title immediately. Throws on an unknown / unowned thread (HTTP 404).
|
|
314
377
|
*/
|
|
@@ -384,13 +447,13 @@ export class MastraPluginClient extends MastraClient {
|
|
|
384
447
|
}
|
|
385
448
|
|
|
386
449
|
/**
|
|
387
|
-
* Snapshot of the client's
|
|
388
|
-
*
|
|
389
|
-
*
|
|
390
|
-
*
|
|
391
|
-
*
|
|
392
|
-
*
|
|
393
|
-
*
|
|
450
|
+
* Snapshot of the client's base headers for the custom-route fetches
|
|
451
|
+
* that don't go through `@mastra/client-js`'s own request pipeline.
|
|
452
|
+
* Returns a fresh object each call so a caller can safely add one-off
|
|
453
|
+
* headers without mutating the shared `options.headers`. Routing
|
|
454
|
+
* (thread selection, model override) is passed per call - via a
|
|
455
|
+
* `?threadId=` query or the thread-selection header on the individual
|
|
456
|
+
* request - not stored here, so concurrent calls never share routing.
|
|
394
457
|
*/
|
|
395
458
|
#defaultHeaders(): Record<string, string> {
|
|
396
459
|
return { ...((this.options.headers as Record<string, string>) ?? {}) };
|
|
@@ -416,9 +479,8 @@ export class MastraPluginClient extends MastraClient {
|
|
|
416
479
|
* `POST` / `DELETE` / `PATCH` + JSON-parse + schema-validate for the
|
|
417
480
|
* mutating routes (`clearHistory` / `removeThread` / `renameThread` /
|
|
418
481
|
* `feedback`). A JSON body, when present, sets `Content-Type`;
|
|
419
|
-
* `options.headers` add
|
|
420
|
-
* header for a targeted delete / rename) over the client's
|
|
421
|
-
* override headers.
|
|
482
|
+
* `options.headers` add per-call headers (e.g. the thread-selection
|
|
483
|
+
* header for a targeted delete / rename) over the client's base headers.
|
|
422
484
|
*/
|
|
423
485
|
async #mutateJson<T>(
|
|
424
486
|
url: string,
|
|
@@ -477,12 +539,12 @@ export const useMastraConfig = (): MastraClientConfig =>
|
|
|
477
539
|
/**
|
|
478
540
|
* The single {@link MastraPluginClient} for the plugin, built once from
|
|
479
541
|
* the published `basePath`. Use it for everything: stream a turn with
|
|
480
|
-
* `client.
|
|
481
|
-
* `client.history()`, resolve embeds with
|
|
482
|
-
* Rebuilt only when `basePath` / `defaultAgent`
|
|
483
|
-
* mount), so it's safe as a `useMemo` / `useCallback`
|
|
484
|
-
* dependency;
|
|
485
|
-
*
|
|
542
|
+
* `client.streamAgent({ agentId, messages, runId, threadId, model, signal })`,
|
|
543
|
+
* page history with `client.history()`, resolve embeds with
|
|
544
|
+
* `client.chart(id)`, etc. Rebuilt only when `basePath` / `defaultAgent`
|
|
545
|
+
* change (e.g. a custom mount), so it's safe as a `useMemo` / `useCallback`
|
|
546
|
+
* / `useEffect` dependency; per-turn thread + model routing is passed per
|
|
547
|
+
* call rather than stored on the client, so concurrent runs stay isolated.
|
|
486
548
|
*/
|
|
487
549
|
export const useMastraClient = (): MastraPluginClient => {
|
|
488
550
|
const config = useMastraConfig();
|
|
@@ -50,6 +50,28 @@ export function isSessionRunning(session: ThreadSession): boolean {
|
|
|
50
50
|
return session.status === "submitted" || session.status === "streaming";
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Settle any tool-progress pills still marked `running` to `done`. A cancelled
|
|
55
|
+
* or interrupted turn stops delivering the `tool-result` / `tool-error` chunks
|
|
56
|
+
* that would otherwise close them, so without this a Genie / chart pill would
|
|
57
|
+
* spin forever after the user hits stop. Returns the same map when nothing was
|
|
58
|
+
* running so callers can skip a needless state update.
|
|
59
|
+
*/
|
|
60
|
+
export function terminateRunningToolEvents(
|
|
61
|
+
toolEventsByMessage: Record<string, ToolEvent[]>,
|
|
62
|
+
): Record<string, ToolEvent[]> {
|
|
63
|
+
let changed = false;
|
|
64
|
+
const next: Record<string, ToolEvent[]> = {};
|
|
65
|
+
for (const [messageId, events] of Object.entries(toolEventsByMessage)) {
|
|
66
|
+
next[messageId] = events.map((event) => {
|
|
67
|
+
if (event.status !== "running") return event;
|
|
68
|
+
changed = true;
|
|
69
|
+
return { ...event, status: "done" as const };
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return changed ? next : toolEventsByMessage;
|
|
73
|
+
}
|
|
74
|
+
|
|
53
75
|
export function sessionKey(activeThreadId: string | undefined): string {
|
|
54
76
|
return activeThreadId ?? DEFAULT_THREAD_SESSION_KEY;
|
|
55
77
|
}
|