@dbx-tools/ui-mastra 0.6.217 → 0.6.219
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 +114 -21
- package/index.ts +10 -9
- package/package.json +8 -8
- package/src/react/_composer-autosize.ts +41 -0
- package/src/react/bubbles.tsx +4 -1
- package/src/react/chat-approvals.ts +9 -4
- package/src/react/chat-composer.tsx +98 -79
- package/src/react/chat-feedback.ts +2 -2
- package/src/react/chat-history.ts +79 -10
- package/src/react/chat-stream.ts +29 -59
- package/src/react/chat-thread-layout.tsx +17 -1
- package/src/react/chat-view.tsx +10 -0
- package/src/react/export-menu.tsx +8 -1
- package/src/react/index.ts +18 -2
- package/src/react/mastra-assistant.tsx +509 -0
- package/src/react/mastra-chat.tsx +120 -72
- package/src/react/thread-sidebar.tsx +33 -43
- package/src/react/thread-tabs.tsx +141 -117
- package/src/react/tool-pill.tsx +24 -1
- package/src/react/types.ts +32 -22
- package/src/support/mastra-client.ts +203 -171
- package/src/support/request-context.ts +30 -0
- package/src/support/thread-sessions.ts +16 -38
- package/src/support/tool-events.ts +41 -0
- package/src/react/chat-stream-reducer.ts +0 -243
- package/src/support/mastra-stream.ts +0 -59
|
@@ -1,243 +0,0 @@
|
|
|
1
|
-
import { error as sharedError } from "@dbx-tools/shared-core";
|
|
2
|
-
import { GenieWriterEventSchema, type MastraStreamChunk } from "@dbx-tools/shared-mastra";
|
|
3
|
-
import type { UIMessage } from "ai";
|
|
4
|
-
import type { PendingApproval, ToolEvent } from "./types.ts";
|
|
5
|
-
|
|
6
|
-
/** Pure accumulated state for one assistant response stream. */
|
|
7
|
-
export type ChatStreamState = {
|
|
8
|
-
textSegments: string[];
|
|
9
|
-
reasoning: string;
|
|
10
|
-
toolEvents: ToolEvent[];
|
|
11
|
-
pendingApprovals: PendingApproval[];
|
|
12
|
-
runId: string | null;
|
|
13
|
-
streaming: boolean;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
/** Session fields changed by one stream reduction. */
|
|
17
|
-
export type ChatStreamChanges = {
|
|
18
|
-
runId: boolean;
|
|
19
|
-
status: boolean;
|
|
20
|
-
toolEvents: boolean;
|
|
21
|
-
pendingApprovals: boolean;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
/** Result of reducing one validated stream chunk. */
|
|
25
|
-
export type ChatStreamReduction = {
|
|
26
|
-
state: ChatStreamState;
|
|
27
|
-
changes: ChatStreamChanges;
|
|
28
|
-
assistantChanged: boolean;
|
|
29
|
-
error?: string;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
/** Seed pure stream state from an existing assistant message and session data. */
|
|
33
|
-
export function createChatStreamState(options: {
|
|
34
|
-
existing?: UIMessage;
|
|
35
|
-
pendingApprovals: PendingApproval[];
|
|
36
|
-
runId: string | null;
|
|
37
|
-
toolEvents: ToolEvent[];
|
|
38
|
-
}): ChatStreamState {
|
|
39
|
-
const textSegments: string[] = [];
|
|
40
|
-
let reasoning = "";
|
|
41
|
-
for (const part of options.existing?.parts ?? []) {
|
|
42
|
-
if (part.type === "text") textSegments.push(part.text);
|
|
43
|
-
else if (part.type === "reasoning") reasoning += (part as { text?: string }).text ?? "";
|
|
44
|
-
}
|
|
45
|
-
return {
|
|
46
|
-
textSegments,
|
|
47
|
-
reasoning,
|
|
48
|
-
toolEvents: options.toolEvents,
|
|
49
|
-
pendingApprovals: options.pendingApprovals,
|
|
50
|
-
runId: options.runId,
|
|
51
|
-
streaming: false,
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** Materialize the assistant message represented by accumulated stream state. */
|
|
56
|
-
export function chatStreamAssistantMessage(assistantId: string, state: ChatStreamState): UIMessage {
|
|
57
|
-
const parts: UIMessage["parts"] = [];
|
|
58
|
-
if (state.reasoning) parts.push({ type: "reasoning", text: state.reasoning });
|
|
59
|
-
for (const segment of state.textSegments) {
|
|
60
|
-
if (segment.length > 0) parts.push({ type: "text", text: segment });
|
|
61
|
-
}
|
|
62
|
-
return {
|
|
63
|
-
id: assistantId,
|
|
64
|
-
role: "assistant",
|
|
65
|
-
parts: parts.length > 0 ? parts : [{ type: "text", text: "" }],
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const withRunId = (
|
|
70
|
-
state: ChatStreamState,
|
|
71
|
-
chunk: MastraStreamChunk,
|
|
72
|
-
): { state: ChatStreamState; changed: boolean } => {
|
|
73
|
-
if (!chunk.runId || state.runId) return { state, changed: false };
|
|
74
|
-
return { state: { ...state, runId: chunk.runId }, changed: true };
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
const reduction = (
|
|
78
|
-
state: ChatStreamState,
|
|
79
|
-
options: {
|
|
80
|
-
runIdChanged: boolean;
|
|
81
|
-
assistantChanged?: boolean;
|
|
82
|
-
statusChanged?: boolean;
|
|
83
|
-
toolEventsChanged?: boolean;
|
|
84
|
-
pendingApprovalsChanged?: boolean;
|
|
85
|
-
error?: string;
|
|
86
|
-
},
|
|
87
|
-
): ChatStreamReduction => ({
|
|
88
|
-
state,
|
|
89
|
-
changes: {
|
|
90
|
-
runId: options.runIdChanged,
|
|
91
|
-
status: options.statusChanged ?? false,
|
|
92
|
-
toolEvents: options.toolEventsChanged ?? false,
|
|
93
|
-
pendingApprovals: options.pendingApprovalsChanged ?? false,
|
|
94
|
-
},
|
|
95
|
-
assistantChanged: options.assistantChanged ?? false,
|
|
96
|
-
...(options.error ? { error: options.error } : {}),
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
/** Reduce one validated chunk into immutable assistant and session state. */
|
|
100
|
-
export function reduceChatStreamChunk(
|
|
101
|
-
previous: ChatStreamState,
|
|
102
|
-
chunk: MastraStreamChunk,
|
|
103
|
-
): ChatStreamReduction {
|
|
104
|
-
const run = withRunId(previous, chunk);
|
|
105
|
-
const state = run.state;
|
|
106
|
-
const markStreaming = !state.streaming;
|
|
107
|
-
|
|
108
|
-
switch (chunk.type) {
|
|
109
|
-
case "unknown":
|
|
110
|
-
case "text-start":
|
|
111
|
-
case "text-end":
|
|
112
|
-
return reduction(state, { runIdChanged: run.changed });
|
|
113
|
-
|
|
114
|
-
case "text-delta": {
|
|
115
|
-
const textSegments = state.textSegments.length > 0 ? [...state.textSegments] : [""];
|
|
116
|
-
textSegments[textSegments.length - 1] += chunk.payload.text;
|
|
117
|
-
return reduction(
|
|
118
|
-
{ ...state, textSegments, streaming: true },
|
|
119
|
-
{
|
|
120
|
-
runIdChanged: run.changed,
|
|
121
|
-
assistantChanged: true,
|
|
122
|
-
statusChanged: markStreaming,
|
|
123
|
-
},
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
case "reasoning-delta":
|
|
128
|
-
return reduction(
|
|
129
|
-
{
|
|
130
|
-
...state,
|
|
131
|
-
reasoning: state.reasoning + chunk.payload.text,
|
|
132
|
-
streaming: true,
|
|
133
|
-
},
|
|
134
|
-
{
|
|
135
|
-
runIdChanged: run.changed,
|
|
136
|
-
assistantChanged: true,
|
|
137
|
-
statusChanged: markStreaming,
|
|
138
|
-
},
|
|
139
|
-
);
|
|
140
|
-
|
|
141
|
-
case "tool-call":
|
|
142
|
-
return reduction(
|
|
143
|
-
{
|
|
144
|
-
...state,
|
|
145
|
-
streaming: true,
|
|
146
|
-
toolEvents: [
|
|
147
|
-
...state.toolEvents,
|
|
148
|
-
{
|
|
149
|
-
id: chunk.payload.toolCallId,
|
|
150
|
-
toolName: chunk.payload.toolName,
|
|
151
|
-
status: "running",
|
|
152
|
-
},
|
|
153
|
-
],
|
|
154
|
-
},
|
|
155
|
-
{
|
|
156
|
-
runIdChanged: run.changed,
|
|
157
|
-
assistantChanged: true,
|
|
158
|
-
statusChanged: markStreaming,
|
|
159
|
-
toolEventsChanged: true,
|
|
160
|
-
},
|
|
161
|
-
);
|
|
162
|
-
|
|
163
|
-
case "tool-call-approval": {
|
|
164
|
-
const approvalRunId = chunk.runId ?? chunk.payload.runId ?? state.runId;
|
|
165
|
-
if (!approvalRunId) {
|
|
166
|
-
return reduction(state, { runIdChanged: run.changed });
|
|
167
|
-
}
|
|
168
|
-
const approval: PendingApproval = {
|
|
169
|
-
toolName: chunk.payload.toolName,
|
|
170
|
-
toolCallId: chunk.payload.toolCallId,
|
|
171
|
-
runId: approvalRunId,
|
|
172
|
-
input: chunk.payload.args,
|
|
173
|
-
};
|
|
174
|
-
const exists = state.pendingApprovals.some(
|
|
175
|
-
(current) => current.toolCallId === approval.toolCallId,
|
|
176
|
-
);
|
|
177
|
-
return reduction(
|
|
178
|
-
{
|
|
179
|
-
...state,
|
|
180
|
-
streaming: true,
|
|
181
|
-
pendingApprovals: exists ? state.pendingApprovals : [...state.pendingApprovals, approval],
|
|
182
|
-
},
|
|
183
|
-
{
|
|
184
|
-
runIdChanged: run.changed,
|
|
185
|
-
assistantChanged: true,
|
|
186
|
-
statusChanged: markStreaming,
|
|
187
|
-
pendingApprovalsChanged: !exists,
|
|
188
|
-
},
|
|
189
|
-
);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
case "tool-result":
|
|
193
|
-
case "tool-error":
|
|
194
|
-
return reduction(
|
|
195
|
-
{
|
|
196
|
-
...state,
|
|
197
|
-
toolEvents: state.toolEvents.map((event) =>
|
|
198
|
-
event.id === chunk.payload.toolCallId
|
|
199
|
-
? { ...event, status: chunk.type === "tool-result" ? "done" : "error" }
|
|
200
|
-
: event,
|
|
201
|
-
),
|
|
202
|
-
},
|
|
203
|
-
{
|
|
204
|
-
runIdChanged: run.changed,
|
|
205
|
-
toolEventsChanged: true,
|
|
206
|
-
},
|
|
207
|
-
);
|
|
208
|
-
|
|
209
|
-
case "tool-output": {
|
|
210
|
-
const progress = GenieWriterEventSchema.safeParse(chunk.payload.output);
|
|
211
|
-
if (!progress.success) return reduction(state, { runIdChanged: run.changed });
|
|
212
|
-
return reduction(
|
|
213
|
-
{
|
|
214
|
-
...state,
|
|
215
|
-
toolEvents: state.toolEvents.map((event) =>
|
|
216
|
-
event.id === chunk.payload.toolCallId
|
|
217
|
-
? { ...event, progress: [...(event.progress ?? []), progress.data] }
|
|
218
|
-
: event,
|
|
219
|
-
),
|
|
220
|
-
},
|
|
221
|
-
{
|
|
222
|
-
runIdChanged: run.changed,
|
|
223
|
-
toolEventsChanged: true,
|
|
224
|
-
},
|
|
225
|
-
);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
case "error": {
|
|
229
|
-
const detail = chunk.payload?.error ?? chunk.payload?.message;
|
|
230
|
-
return reduction(state, {
|
|
231
|
-
runIdChanged: run.changed,
|
|
232
|
-
error: detail
|
|
233
|
-
? sharedError.errorMessage(detail)
|
|
234
|
-
: "The assistant stream reported an error.",
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
default: {
|
|
239
|
-
const exhaustive: never = chunk;
|
|
240
|
-
return exhaustive;
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
}
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { json } from "@dbx-tools/shared-core";
|
|
2
|
-
import { MastraStreamChunkSchema, type MastraStreamChunk } from "@dbx-tools/shared-mastra";
|
|
3
|
-
|
|
4
|
-
export type { MastraStreamChunk } from "@dbx-tools/shared-mastra";
|
|
5
|
-
|
|
6
|
-
/** Response from agent streaming endpoints with {@link processMastraStream}. */
|
|
7
|
-
export type MastraStreamResponse = Response & {
|
|
8
|
-
processDataStream: (options: {
|
|
9
|
-
onChunk: (chunk: MastraStreamChunk) => void | Promise<void>;
|
|
10
|
-
}) => Promise<void>;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Parse Mastra agent SSE (`data: …` lines) and invoke `onChunk` per event.
|
|
15
|
-
* Mirrors `@mastra/client-js`'s internal reader without the
|
|
16
|
-
* `processChatResponse_vNext` side channel that throws when a resumed
|
|
17
|
-
* `approve-tool-call` stream emits `tool-result` without a new `tool-call`.
|
|
18
|
-
*/
|
|
19
|
-
export async function processMastraStream(options: {
|
|
20
|
-
stream: ReadableStream<Uint8Array>;
|
|
21
|
-
onChunk: (chunk: MastraStreamChunk) => void | Promise<void>;
|
|
22
|
-
signal?: AbortSignal;
|
|
23
|
-
}): Promise<void> {
|
|
24
|
-
const reader = options.stream.getReader();
|
|
25
|
-
const decoder = new TextDecoder();
|
|
26
|
-
let buffer = "";
|
|
27
|
-
const abort = () => void reader.cancel();
|
|
28
|
-
if (options.signal?.aborted) abort();
|
|
29
|
-
else options.signal?.addEventListener("abort", abort, { once: true });
|
|
30
|
-
try {
|
|
31
|
-
for (;;) {
|
|
32
|
-
const { done, value } = await reader.read();
|
|
33
|
-
if (done) break;
|
|
34
|
-
buffer += decoder.decode(value, { stream: true });
|
|
35
|
-
const lines = buffer.split("\n\n");
|
|
36
|
-
buffer = lines.pop() ?? "";
|
|
37
|
-
for (const line of lines) {
|
|
38
|
-
if (!line.startsWith("data: ")) continue;
|
|
39
|
-
const data = line.slice(6);
|
|
40
|
-
if (data === "[DONE]") return;
|
|
41
|
-
const chunk = MastraStreamChunkSchema.parse(json.parse(data));
|
|
42
|
-
await options.onChunk(chunk);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
} finally {
|
|
46
|
-
options.signal?.removeEventListener("abort", abort);
|
|
47
|
-
reader.releaseLock();
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** Attach {@link processMastraStream} to a fetch `Response`. */
|
|
52
|
-
export function asMastraStreamResponse(response: Response): MastraStreamResponse {
|
|
53
|
-
const streamResponse = response as MastraStreamResponse;
|
|
54
|
-
streamResponse.processDataStream = async ({ onChunk }) => {
|
|
55
|
-
if (!response.body) throw new Error("No response body");
|
|
56
|
-
await processMastraStream({ stream: response.body, onChunk });
|
|
57
|
-
};
|
|
58
|
-
return streamResponse;
|
|
59
|
-
}
|