@dbx-tools/ui-mastra 0.6.224 → 0.6.226
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 +21 -1
- package/index.ts +1 -1
- package/package.json +8 -7
- package/src/react/_history-messages.ts +11 -0
- package/src/react/_model-selector.tsx +292 -0
- package/src/react/chat-composer.tsx +36 -50
- package/src/react/chat-history.ts +1 -6
- package/src/react/chat-view.tsx +10 -0
- package/src/react/mastra-chat.tsx +28 -1
- package/src/react/thread-sidebar.tsx +90 -69
- package/src/react/types.ts +20 -1
- package/src/support/mastra-client.ts +26 -8
package/README.md
CHANGED
|
@@ -147,7 +147,9 @@ export function App() {
|
|
|
147
147
|
`MastraChat` is the quickest client for the AppKit-Mastra plugin. It reads the
|
|
148
148
|
plugin's published client config, creates a `MastraPluginClient`, streams turns
|
|
149
149
|
through Mastra's official AI SDK chat route, hydrates native memory, and renders
|
|
150
|
-
the controlled `ChatView`.
|
|
150
|
+
the controlled `ChatView`. When Mastra returns persisted `messages` without a
|
|
151
|
+
precomputed `uiMessages` projection, history hydration uses Mastra's native
|
|
152
|
+
`convertMessages(...).to("AIV5.UI")` conversion.
|
|
151
153
|
|
|
152
154
|
Useful options:
|
|
153
155
|
|
|
@@ -155,6 +157,9 @@ Useful options:
|
|
|
155
157
|
- `showModelPicker` fetches `/models` and sends `X-Mastra-Model` overrides.
|
|
156
158
|
- `composerActions` places host-owned per-turn controls beside the model
|
|
157
159
|
selector without replacing the built-in composer.
|
|
160
|
+
- `modelSelectorActions` places host-owned controls inside the model menu after
|
|
161
|
+
the endpoint choices. Supply it only when the active agent supports that
|
|
162
|
+
option, such as a Genie transport toggle.
|
|
158
163
|
- `composerLeadingActions` places attachment or app actions on the left side of
|
|
159
164
|
the fixed composer footer. The textarea grows above it to a cap, then scrolls;
|
|
160
165
|
model and Send/Stop stay pinned bottom-right.
|
|
@@ -198,6 +203,11 @@ starts a fresh conversation when it was the last tab open. The open set is
|
|
|
198
203
|
session state; the strip reseeds from the most recent conversations on the next
|
|
199
204
|
load.
|
|
200
205
|
|
|
206
|
+
Selecting a conversation fetches only the newest 20 native memory messages.
|
|
207
|
+
Scrolling upward requests the next page and prepends it without moving the
|
|
208
|
+
reader's viewport. Agent turns still send only the new user message; Mastra
|
|
209
|
+
loads the remembered transcript server-side.
|
|
210
|
+
|
|
201
211
|
`auto` measures the chat's own element rather than the viewport, so a chat
|
|
202
212
|
embedded in a split view or side panel switches to tabs on the space it actually
|
|
203
213
|
has instead of waiting for the window to shrink.
|
|
@@ -258,6 +268,16 @@ export function ReviewChat(props: ChatViewProps) {
|
|
|
258
268
|
sidebar, transcript, tool progress, approval cards, suggestions, export controls,
|
|
259
269
|
feedback controls, and composer from props. Use it when your app already has a
|
|
260
270
|
transport or needs to combine Mastra messages with another state model.
|
|
271
|
+
`MastraChat` shows a spinner in the model control while its live default model
|
|
272
|
+
is being resolved. The picker groups endpoints into
|
|
273
|
+
Rust-derived family submenus. Its compact first popup shows only the current
|
|
274
|
+
model, host-owned actions such as Genie Agent Mode, and a reasoning slider when
|
|
275
|
+
the selected endpoint supports it. The current model opens the family picker,
|
|
276
|
+
so the long endpoint catalogue stays out of the primary control popup. Family
|
|
277
|
+
and model menus use the same compact utility type as the composer actions,
|
|
278
|
+
omit repeated headers, and omit the already-selected model. The model menu puts
|
|
279
|
+
`Default (<resolved model>)` first, keeps `Other` last, and makes a one-model
|
|
280
|
+
family directly selectable instead of opening another submenu.
|
|
261
281
|
|
|
262
282
|
## Call Plugin Routes Directly
|
|
263
283
|
|
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.6.
|
|
6
|
+
export const PACKAGE_VERSION = "0.6.226";
|
|
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";
|
package/package.json
CHANGED
|
@@ -31,13 +31,14 @@
|
|
|
31
31
|
"react-dom": "~19.2.4"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@dbx-tools/shared-core": "0.6.
|
|
35
|
-
"@dbx-tools/shared-genie": "0.6.
|
|
36
|
-
"@dbx-tools/shared-mastra": "0.6.
|
|
37
|
-
"@dbx-tools/shared-model": "0.6.
|
|
38
|
-
"@dbx-tools/ui-appkit": "0.6.
|
|
39
|
-
"@dbx-tools/ui-branding": "0.6.
|
|
34
|
+
"@dbx-tools/shared-core": "0.6.226",
|
|
35
|
+
"@dbx-tools/shared-genie": "0.6.226",
|
|
36
|
+
"@dbx-tools/shared-mastra": "0.6.226",
|
|
37
|
+
"@dbx-tools/shared-model": "0.6.226",
|
|
38
|
+
"@dbx-tools/ui-appkit": "0.6.226",
|
|
39
|
+
"@dbx-tools/ui-branding": "0.6.226",
|
|
40
40
|
"@mastra/client-js": "1.50.0",
|
|
41
|
+
"@mastra/core": "1.71.0",
|
|
41
42
|
"@tanstack/react-table": "^8.21.3",
|
|
42
43
|
"ai": "^5.0.0",
|
|
43
44
|
"echarts": "^6.0.0",
|
|
@@ -52,7 +53,7 @@
|
|
|
52
53
|
"publishConfig": {
|
|
53
54
|
"access": "public"
|
|
54
55
|
},
|
|
55
|
-
"version": "0.6.
|
|
56
|
+
"version": "0.6.226",
|
|
56
57
|
"type": "module",
|
|
57
58
|
"exports": {
|
|
58
59
|
"./react": "./src/react/index.ts",
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { convertMessages } from "@mastra/core/agent/message-list";
|
|
2
|
+
import type { ListMemoryThreadMessagesResponse } from "@mastra/client-js";
|
|
3
|
+
import type { UIMessage } from "ai";
|
|
4
|
+
|
|
5
|
+
/** Convert a newest-first native memory page to chronological AI SDK UI messages. */
|
|
6
|
+
export function toChronologicalUiMessages(response: ListMemoryThreadMessagesResponse): UIMessage[] {
|
|
7
|
+
if (response.uiMessages) {
|
|
8
|
+
return [...response.uiMessages].reverse() as UIMessage[];
|
|
9
|
+
}
|
|
10
|
+
return convertMessages(response.messages).to("AIV5.UI") as UIMessage[];
|
|
11
|
+
}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { string } from "@dbx-tools/shared-core";
|
|
2
|
+
import {
|
|
3
|
+
Button,
|
|
4
|
+
DropdownMenu,
|
|
5
|
+
DropdownMenuContent,
|
|
6
|
+
DropdownMenuItem,
|
|
7
|
+
DropdownMenuLabel,
|
|
8
|
+
DropdownMenuPortal,
|
|
9
|
+
DropdownMenuRadioGroup,
|
|
10
|
+
DropdownMenuRadioItem,
|
|
11
|
+
DropdownMenuSeparator,
|
|
12
|
+
DropdownMenuSub,
|
|
13
|
+
DropdownMenuSubContent,
|
|
14
|
+
DropdownMenuSubTrigger,
|
|
15
|
+
DropdownMenuTrigger,
|
|
16
|
+
Slider,
|
|
17
|
+
Spinner,
|
|
18
|
+
} from "@dbx-tools/ui-appkit/react";
|
|
19
|
+
import { CheckIcon, ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
|
|
20
|
+
import { useMemo, useState, type ReactNode } from "react";
|
|
21
|
+
import type { ChatModelOption, ChatViewProps } from "./types.ts";
|
|
22
|
+
|
|
23
|
+
const OTHER_FAMILY = "other";
|
|
24
|
+
|
|
25
|
+
interface ModelSelectorProps {
|
|
26
|
+
models: ChatModelOption[];
|
|
27
|
+
model?: string;
|
|
28
|
+
defaultModelName?: string;
|
|
29
|
+
defaultModelId?: string;
|
|
30
|
+
defaultModelLoading?: boolean;
|
|
31
|
+
reasoningEffort: ChatViewProps["reasoningEffort"];
|
|
32
|
+
onReasoningEffortChange: ChatViewProps["onReasoningEffortChange"];
|
|
33
|
+
onModelChange: NonNullable<ChatViewProps["onModelChange"]>;
|
|
34
|
+
actions?: ReactNode;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Convert a normalized family token into a compact menu label. */
|
|
38
|
+
function familyLabel(family: string): string {
|
|
39
|
+
return family.length <= 3 ? family.toUpperCase() : string.toLabel(family);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Resolve the label shown for a model endpoint. */
|
|
43
|
+
function modelLabel(model: ChatModelOption): string {
|
|
44
|
+
return model.displayName || model.name;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Reasoning selector backed only by efforts published for the active model. */
|
|
48
|
+
const ReasoningControl = ({
|
|
49
|
+
efforts,
|
|
50
|
+
value,
|
|
51
|
+
onChange,
|
|
52
|
+
}: {
|
|
53
|
+
efforts: string[];
|
|
54
|
+
value: ChatViewProps["reasoningEffort"];
|
|
55
|
+
onChange: NonNullable<ChatViewProps["onReasoningEffortChange"]>;
|
|
56
|
+
}) => {
|
|
57
|
+
const selectedIndex = value === undefined ? 0 : Math.max(0, efforts.indexOf(value) + 1);
|
|
58
|
+
const selectedLabel = value === undefined ? "Auto" : string.toLabel(value);
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div className="space-y-2 px-2 py-1.5">
|
|
62
|
+
<div className="flex items-center justify-between gap-3 text-xs">
|
|
63
|
+
<span className="font-medium text-foreground">Reasoning</span>
|
|
64
|
+
<span className="text-muted-foreground">{selectedLabel}</span>
|
|
65
|
+
</div>
|
|
66
|
+
<Slider
|
|
67
|
+
aria-label="Reasoning effort"
|
|
68
|
+
min={0}
|
|
69
|
+
max={efforts.length}
|
|
70
|
+
step={1}
|
|
71
|
+
value={[selectedIndex]}
|
|
72
|
+
onValueChange={([index]) => onChange(index === 0 ? undefined : efforts[index - 1])}
|
|
73
|
+
/>
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/** Family menu that opens one scoped endpoint submenu at a time. */
|
|
79
|
+
const FamilyPicker = ({
|
|
80
|
+
families,
|
|
81
|
+
selectedModelId,
|
|
82
|
+
model,
|
|
83
|
+
defaultModelName,
|
|
84
|
+
showLoading,
|
|
85
|
+
activeFamily,
|
|
86
|
+
onActiveFamilyChange,
|
|
87
|
+
onModelChange,
|
|
88
|
+
}: {
|
|
89
|
+
families: Array<[string, ChatModelOption[]]>;
|
|
90
|
+
selectedModelId?: string;
|
|
91
|
+
model?: string;
|
|
92
|
+
defaultModelName?: string;
|
|
93
|
+
showLoading: boolean;
|
|
94
|
+
activeFamily?: string;
|
|
95
|
+
onActiveFamilyChange: (family: string | undefined) => void;
|
|
96
|
+
onModelChange: NonNullable<ChatViewProps["onModelChange"]>;
|
|
97
|
+
}) => {
|
|
98
|
+
if (activeFamily) {
|
|
99
|
+
const options = families.find(([family]) => family === activeFamily)?.[1] ?? [];
|
|
100
|
+
const choices = options.filter((option) => option.name !== selectedModelId);
|
|
101
|
+
return (
|
|
102
|
+
<>
|
|
103
|
+
<button
|
|
104
|
+
type="button"
|
|
105
|
+
className="flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-xs font-medium outline-none hover:bg-accent focus-visible:bg-accent"
|
|
106
|
+
onClick={() => onActiveFamilyChange(undefined)}
|
|
107
|
+
>
|
|
108
|
+
<ChevronLeftIcon className="size-3" />
|
|
109
|
+
Models
|
|
110
|
+
</button>
|
|
111
|
+
<DropdownMenuSeparator />
|
|
112
|
+
<DropdownMenuRadioGroup onValueChange={onModelChange}>
|
|
113
|
+
{choices.map((option) => (
|
|
114
|
+
<DropdownMenuRadioItem key={option.name} value={option.name} className="text-xs">
|
|
115
|
+
{modelLabel(option)}
|
|
116
|
+
</DropdownMenuRadioItem>
|
|
117
|
+
))}
|
|
118
|
+
</DropdownMenuRadioGroup>
|
|
119
|
+
</>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<>
|
|
125
|
+
<DropdownMenuLabel className="text-xs">Models</DropdownMenuLabel>
|
|
126
|
+
<DropdownMenuItem className="gap-2 text-xs" onSelect={() => onModelChange("")}>
|
|
127
|
+
{showLoading ? (
|
|
128
|
+
<>
|
|
129
|
+
<Spinner className="size-3 shrink-0" />
|
|
130
|
+
<span className="sr-only">Loading default model</span>
|
|
131
|
+
</>
|
|
132
|
+
) : (
|
|
133
|
+
<span>{defaultModelName ? `Default (${defaultModelName})` : "Default"}</span>
|
|
134
|
+
)}
|
|
135
|
+
{!model && !showLoading && <CheckIcon className="ml-auto size-3" />}
|
|
136
|
+
</DropdownMenuItem>
|
|
137
|
+
<DropdownMenuSeparator />
|
|
138
|
+
{families.map(([family, options]) => {
|
|
139
|
+
if (options.length === 1) {
|
|
140
|
+
const [option] = options;
|
|
141
|
+
if (!option) return null;
|
|
142
|
+
return (
|
|
143
|
+
<DropdownMenuItem
|
|
144
|
+
key={family}
|
|
145
|
+
className="gap-2 text-xs"
|
|
146
|
+
onSelect={() => onModelChange(option.name)}
|
|
147
|
+
>
|
|
148
|
+
<span>{modelLabel(option)}</span>
|
|
149
|
+
{option.name === selectedModelId && <CheckIcon className="ml-auto size-3" />}
|
|
150
|
+
</DropdownMenuItem>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
const choices = options.filter((option) => option.name !== selectedModelId);
|
|
154
|
+
if (choices.length === 0) return null;
|
|
155
|
+
return (
|
|
156
|
+
<button
|
|
157
|
+
key={family}
|
|
158
|
+
type="button"
|
|
159
|
+
className="flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-xs outline-none hover:bg-accent focus-visible:bg-accent"
|
|
160
|
+
onClick={() => onActiveFamilyChange(family)}
|
|
161
|
+
>
|
|
162
|
+
<span>{familyLabel(family)}</span>
|
|
163
|
+
<span className="ml-auto tabular-nums text-muted-foreground">{options.length}</span>
|
|
164
|
+
<ChevronRightIcon className="size-3 text-muted-foreground" />
|
|
165
|
+
</button>
|
|
166
|
+
);
|
|
167
|
+
})}
|
|
168
|
+
</>
|
|
169
|
+
);
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Family-first model picker with fixed model controls above the scrollable
|
|
174
|
+
* family submenus.
|
|
175
|
+
*/
|
|
176
|
+
export const ModelSelector = ({
|
|
177
|
+
models,
|
|
178
|
+
model,
|
|
179
|
+
defaultModelName,
|
|
180
|
+
defaultModelId,
|
|
181
|
+
defaultModelLoading = false,
|
|
182
|
+
reasoningEffort,
|
|
183
|
+
onReasoningEffortChange,
|
|
184
|
+
onModelChange,
|
|
185
|
+
actions,
|
|
186
|
+
}: ModelSelectorProps) => {
|
|
187
|
+
const [open, setOpen] = useState(false);
|
|
188
|
+
const [modelPickerOpen, setModelPickerOpen] = useState(false);
|
|
189
|
+
const [activeFamily, setActiveFamily] = useState<string>();
|
|
190
|
+
const selectedModelId = model || defaultModelId;
|
|
191
|
+
const selectedModel = models.find((option) => option.name === selectedModelId);
|
|
192
|
+
const reasoningEfforts = selectedModel?.reasoningEfforts ?? [];
|
|
193
|
+
const showLoading = defaultModelLoading && !model;
|
|
194
|
+
const triggerLabel = model
|
|
195
|
+
? modelLabel(models.find((option) => option.name === model) ?? { name: model })
|
|
196
|
+
: defaultModelName || "Default";
|
|
197
|
+
const families = useMemo(() => {
|
|
198
|
+
const grouped = new Map<string, ChatModelOption[]>();
|
|
199
|
+
for (const option of models) {
|
|
200
|
+
const family = option.family || OTHER_FAMILY;
|
|
201
|
+
const current = grouped.get(family);
|
|
202
|
+
if (current) current.push(option);
|
|
203
|
+
else grouped.set(family, [option]);
|
|
204
|
+
}
|
|
205
|
+
return [...grouped.entries()].sort(([left], [right]) => {
|
|
206
|
+
if (left === OTHER_FAMILY) return 1;
|
|
207
|
+
if (right === OTHER_FAMILY) return -1;
|
|
208
|
+
return 0;
|
|
209
|
+
});
|
|
210
|
+
}, [models]);
|
|
211
|
+
|
|
212
|
+
return (
|
|
213
|
+
<DropdownMenu
|
|
214
|
+
open={open}
|
|
215
|
+
onOpenChange={(next) => {
|
|
216
|
+
setOpen(next);
|
|
217
|
+
if (!next) {
|
|
218
|
+
setModelPickerOpen(false);
|
|
219
|
+
setActiveFamily(undefined);
|
|
220
|
+
}
|
|
221
|
+
}}
|
|
222
|
+
>
|
|
223
|
+
<DropdownMenuTrigger asChild>
|
|
224
|
+
<Button
|
|
225
|
+
type="button"
|
|
226
|
+
variant="ghost"
|
|
227
|
+
size="sm"
|
|
228
|
+
className="h-7 max-w-[190px] gap-1.5 px-2 text-xs font-normal"
|
|
229
|
+
aria-label={showLoading ? "Loading default model" : `Model: ${triggerLabel}`}
|
|
230
|
+
>
|
|
231
|
+
{showLoading ? (
|
|
232
|
+
<>
|
|
233
|
+
<Spinner className="size-3 shrink-0" />
|
|
234
|
+
<span className="sr-only">Loading default model</span>
|
|
235
|
+
</>
|
|
236
|
+
) : (
|
|
237
|
+
<>
|
|
238
|
+
<span className="truncate">{triggerLabel}</span>
|
|
239
|
+
<ChevronDownIcon className="size-3 shrink-0 text-muted-foreground" />
|
|
240
|
+
</>
|
|
241
|
+
)}
|
|
242
|
+
</Button>
|
|
243
|
+
</DropdownMenuTrigger>
|
|
244
|
+
<DropdownMenuContent align="end" className="w-48">
|
|
245
|
+
<DropdownMenuSub
|
|
246
|
+
open={modelPickerOpen}
|
|
247
|
+
onOpenChange={(next) => {
|
|
248
|
+
if (!next && open) return;
|
|
249
|
+
setModelPickerOpen(next);
|
|
250
|
+
if (!next) setActiveFamily(undefined);
|
|
251
|
+
}}
|
|
252
|
+
>
|
|
253
|
+
<DropdownMenuSubTrigger className="mx-1 justify-center rounded-lg bg-accent/60 px-3 py-2 text-xs font-medium">
|
|
254
|
+
{showLoading ? (
|
|
255
|
+
<>
|
|
256
|
+
<Spinner className="size-3 shrink-0" />
|
|
257
|
+
<span className="sr-only">Loading default model</span>
|
|
258
|
+
</>
|
|
259
|
+
) : (
|
|
260
|
+
triggerLabel
|
|
261
|
+
)}
|
|
262
|
+
</DropdownMenuSubTrigger>
|
|
263
|
+
<DropdownMenuPortal>
|
|
264
|
+
<DropdownMenuSubContent className="w-48">
|
|
265
|
+
<FamilyPicker
|
|
266
|
+
families={families}
|
|
267
|
+
selectedModelId={selectedModelId}
|
|
268
|
+
model={model}
|
|
269
|
+
defaultModelName={defaultModelName}
|
|
270
|
+
showLoading={showLoading}
|
|
271
|
+
activeFamily={activeFamily}
|
|
272
|
+
onActiveFamilyChange={setActiveFamily}
|
|
273
|
+
onModelChange={onModelChange}
|
|
274
|
+
/>
|
|
275
|
+
</DropdownMenuSubContent>
|
|
276
|
+
</DropdownMenuPortal>
|
|
277
|
+
</DropdownMenuSub>
|
|
278
|
+
{(actions || (reasoningEfforts.length > 0 && onReasoningEffortChange)) && (
|
|
279
|
+
<DropdownMenuSeparator />
|
|
280
|
+
)}
|
|
281
|
+
{actions && <div className="px-1">{actions}</div>}
|
|
282
|
+
{reasoningEfforts.length > 0 && onReasoningEffortChange && (
|
|
283
|
+
<ReasoningControl
|
|
284
|
+
efforts={reasoningEfforts}
|
|
285
|
+
value={reasoningEffort}
|
|
286
|
+
onChange={onReasoningEffortChange}
|
|
287
|
+
/>
|
|
288
|
+
)}
|
|
289
|
+
</DropdownMenuContent>
|
|
290
|
+
</DropdownMenu>
|
|
291
|
+
);
|
|
292
|
+
};
|
|
@@ -12,11 +12,6 @@ import {
|
|
|
12
12
|
InputGroupAddon,
|
|
13
13
|
InputGroupButton,
|
|
14
14
|
InputGroupTextarea,
|
|
15
|
-
Select,
|
|
16
|
-
SelectContent,
|
|
17
|
-
SelectItem,
|
|
18
|
-
SelectTrigger,
|
|
19
|
-
SelectValue,
|
|
20
15
|
Spinner,
|
|
21
16
|
Tooltip,
|
|
22
17
|
TooltipContent,
|
|
@@ -34,11 +29,10 @@ import {
|
|
|
34
29
|
import { useCallback, useId, useLayoutEffect, useRef, useState } from "react";
|
|
35
30
|
import { autosizeComposerTextarea, observeComposerWidth } from "./_composer-autosize.ts";
|
|
36
31
|
import { ExportMenu } from "./export-menu.tsx";
|
|
32
|
+
import { ModelSelector } from "./_model-selector.tsx";
|
|
37
33
|
import { SuggestionPills } from "./suggestion-pills.tsx";
|
|
38
34
|
import type { ChatViewProps } from "./types.ts";
|
|
39
35
|
|
|
40
|
-
const DEFAULT_MODEL_VALUE = "__default__";
|
|
41
|
-
|
|
42
36
|
type QueuedSteerListProps = {
|
|
43
37
|
queuedSteers: NonNullable<ChatViewProps["queuedSteers"]>;
|
|
44
38
|
onSendSteerNow: ChatViewProps["onSendSteerNow"];
|
|
@@ -250,7 +244,12 @@ type ChatComposerProps = {
|
|
|
250
244
|
model: ChatViewProps["model"];
|
|
251
245
|
onModelChange: ChatViewProps["onModelChange"];
|
|
252
246
|
defaultModelName: ChatViewProps["defaultModelName"];
|
|
247
|
+
defaultModelId: ChatViewProps["defaultModelId"];
|
|
248
|
+
defaultModelLoading: ChatViewProps["defaultModelLoading"];
|
|
249
|
+
reasoningEffort: ChatViewProps["reasoningEffort"];
|
|
250
|
+
onReasoningEffortChange: ChatViewProps["onReasoningEffortChange"];
|
|
253
251
|
composerActions: ChatViewProps["composerActions"];
|
|
252
|
+
modelSelectorActions: ChatViewProps["modelSelectorActions"];
|
|
254
253
|
composerLeadingActions: ChatViewProps["composerLeadingActions"];
|
|
255
254
|
isLoadingHistory: NonNullable<ChatViewProps["isLoadingHistory"]>;
|
|
256
255
|
onClear: ChatViewProps["onClear"];
|
|
@@ -273,7 +272,12 @@ export const ChatComposer = ({
|
|
|
273
272
|
model,
|
|
274
273
|
onModelChange,
|
|
275
274
|
defaultModelName,
|
|
275
|
+
defaultModelId,
|
|
276
|
+
defaultModelLoading,
|
|
277
|
+
reasoningEffort,
|
|
278
|
+
onReasoningEffortChange,
|
|
276
279
|
composerActions,
|
|
280
|
+
modelSelectorActions,
|
|
277
281
|
composerLeadingActions,
|
|
278
282
|
isLoadingHistory,
|
|
279
283
|
onClear,
|
|
@@ -308,24 +312,13 @@ export const ChatComposer = ({
|
|
|
308
312
|
onResumeFollow();
|
|
309
313
|
};
|
|
310
314
|
|
|
311
|
-
const showModelDisplay = Boolean(onModelChange);
|
|
312
315
|
const modelChangeable = Boolean(models && models.length > 0);
|
|
313
|
-
const defaultOptionLabel = defaultModelName || "Default";
|
|
314
|
-
const currentModelLabel =
|
|
315
|
-
(model ? models?.find((option) => option.name === model)?.displayName : undefined) ||
|
|
316
|
-
defaultOptionLabel;
|
|
317
|
-
const sortedModels = [...(models ?? [])].sort((left, right) =>
|
|
318
|
-
(left.displayName || left.name).localeCompare(right.displayName || right.name, undefined, {
|
|
319
|
-
sensitivity: "base",
|
|
320
|
-
}),
|
|
321
|
-
);
|
|
322
316
|
return (
|
|
323
317
|
<>
|
|
324
|
-
{isEmpty && (
|
|
318
|
+
{isEmpty && !isLoadingHistory && (
|
|
325
319
|
<SuggestionPills
|
|
326
320
|
questions={suggestions}
|
|
327
321
|
onSelect={(text) => sendMessage({ text })}
|
|
328
|
-
disabled={isLoadingHistory}
|
|
329
322
|
className="mx-auto w-full max-w-4xl px-4 pb-2 md:px-6"
|
|
330
323
|
/>
|
|
331
324
|
)}
|
|
@@ -358,14 +351,13 @@ export const ChatComposer = ({
|
|
|
358
351
|
submit();
|
|
359
352
|
}
|
|
360
353
|
}}
|
|
361
|
-
placeholder=
|
|
354
|
+
placeholder="Send a message..."
|
|
362
355
|
rows={1}
|
|
363
|
-
|
|
364
|
-
className="field-sizing-fixed max-h-64 w-full flex-none overflow-y-auto px-4 pb-2 pt-4 text-base md:text-sm"
|
|
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"
|
|
365
357
|
/>
|
|
366
358
|
<InputGroupAddon
|
|
367
359
|
align="block-end"
|
|
368
|
-
className="flex w-full flex-row flex-wrap items-center justify-between gap-2 px-3 pb-3 pt-
|
|
360
|
+
className="flex w-full flex-row flex-wrap items-center justify-between gap-2 px-3 pb-3 pt-1"
|
|
369
361
|
>
|
|
370
362
|
<div className="flex min-w-0 max-w-full flex-wrap items-center gap-2">
|
|
371
363
|
{composerLeadingActions}
|
|
@@ -382,35 +374,29 @@ export const ChatComposer = ({
|
|
|
382
374
|
</div>
|
|
383
375
|
<div className="ml-auto flex min-w-0 max-w-full items-center gap-1.5">
|
|
384
376
|
{composerActions}
|
|
385
|
-
{
|
|
377
|
+
{onModelChange &&
|
|
386
378
|
(modelChangeable ? (
|
|
387
|
-
<
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
>
|
|
399
|
-
<span className="sr-only">Model:</span>
|
|
400
|
-
<SelectValue placeholder={defaultOptionLabel} />
|
|
401
|
-
</SelectTrigger>
|
|
402
|
-
<SelectContent>
|
|
403
|
-
<SelectItem value={DEFAULT_MODEL_VALUE}>{defaultOptionLabel}</SelectItem>
|
|
404
|
-
{sortedModels.map((option) => (
|
|
405
|
-
<SelectItem key={option.name} value={option.name}>
|
|
406
|
-
{option.displayName || option.name}
|
|
407
|
-
</SelectItem>
|
|
408
|
-
))}
|
|
409
|
-
</SelectContent>
|
|
410
|
-
</Select>
|
|
379
|
+
<ModelSelector
|
|
380
|
+
models={models ?? []}
|
|
381
|
+
model={model}
|
|
382
|
+
defaultModelName={defaultModelName}
|
|
383
|
+
defaultModelId={defaultModelId}
|
|
384
|
+
defaultModelLoading={defaultModelLoading}
|
|
385
|
+
reasoningEffort={reasoningEffort}
|
|
386
|
+
onReasoningEffortChange={onReasoningEffortChange}
|
|
387
|
+
onModelChange={onModelChange}
|
|
388
|
+
actions={modelSelectorActions}
|
|
389
|
+
/>
|
|
411
390
|
) : (
|
|
412
|
-
<span className="max-w-[180px] truncate px-2 text-xs text-muted-foreground">
|
|
413
|
-
{
|
|
391
|
+
<span className="flex max-w-[180px] items-center gap-1.5 truncate px-2 text-xs text-muted-foreground">
|
|
392
|
+
{defaultModelLoading ? (
|
|
393
|
+
<>
|
|
394
|
+
<Spinner className="size-3 shrink-0" />
|
|
395
|
+
<span className="sr-only">Loading default model</span>
|
|
396
|
+
</>
|
|
397
|
+
) : (
|
|
398
|
+
defaultModelName || "Default"
|
|
399
|
+
)}
|
|
414
400
|
</span>
|
|
415
401
|
))}
|
|
416
402
|
{isRunning && onStop && !input.trim() ? (
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { error as sharedError, log } from "@dbx-tools/shared-core";
|
|
2
|
-
import type { ListMemoryThreadMessagesResponse } from "@mastra/client-js";
|
|
3
2
|
import type { UIMessage } from "ai";
|
|
4
3
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
5
4
|
import type {
|
|
@@ -8,6 +7,7 @@ import type {
|
|
|
8
7
|
ThreadSessionUpdater,
|
|
9
8
|
} from "./chat-sessions.ts";
|
|
10
9
|
import type { PendingApproval } from "./types.ts";
|
|
10
|
+
import { toChronologicalUiMessages } from "./_history-messages.ts";
|
|
11
11
|
import type { MastraPluginClient } from "../support/mastra-client.ts";
|
|
12
12
|
|
|
13
13
|
const HISTORY_PAGE_SIZE = 20;
|
|
@@ -26,11 +26,6 @@ interface UseChatHistoryOptions {
|
|
|
26
26
|
|
|
27
27
|
type SuspendedRuns = Awaited<ReturnType<MastraPluginClient["suspendedRuns"]>>["runs"];
|
|
28
28
|
|
|
29
|
-
/** Read a newest-first native memory page as chronological UI messages. */
|
|
30
|
-
function toChronologicalUiMessages(response: ListMemoryThreadMessagesResponse): UIMessage[] {
|
|
31
|
-
return [...(response.uiMessages ?? [])].reverse() as UIMessage[];
|
|
32
|
-
}
|
|
33
|
-
|
|
34
29
|
/** Recover actionable approval cards from Mastra's persisted suspended runs. */
|
|
35
30
|
function pendingApprovals(
|
|
36
31
|
messages: UIMessage[],
|
package/src/react/chat-view.tsx
CHANGED
|
@@ -27,8 +27,13 @@ export const ChatView = ({
|
|
|
27
27
|
models,
|
|
28
28
|
model,
|
|
29
29
|
onModelChange,
|
|
30
|
+
reasoningEffort,
|
|
31
|
+
onReasoningEffortChange,
|
|
30
32
|
defaultModelName,
|
|
33
|
+
defaultModelId,
|
|
34
|
+
defaultModelLoading = false,
|
|
31
35
|
composerActions,
|
|
36
|
+
modelSelectorActions,
|
|
32
37
|
composerLeadingActions,
|
|
33
38
|
onLoadMore,
|
|
34
39
|
isLoadingMore = false,
|
|
@@ -117,8 +122,13 @@ export const ChatView = ({
|
|
|
117
122
|
models={models}
|
|
118
123
|
model={model}
|
|
119
124
|
onModelChange={onModelChange}
|
|
125
|
+
reasoningEffort={reasoningEffort}
|
|
126
|
+
onReasoningEffortChange={onReasoningEffortChange}
|
|
120
127
|
defaultModelName={defaultModelName}
|
|
128
|
+
defaultModelId={defaultModelId}
|
|
129
|
+
defaultModelLoading={defaultModelLoading}
|
|
121
130
|
composerActions={composerActions}
|
|
131
|
+
modelSelectorActions={modelSelectorActions}
|
|
122
132
|
composerLeadingActions={composerLeadingActions}
|
|
123
133
|
isLoadingHistory={isLoadingHistory}
|
|
124
134
|
onClear={onClear}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { error as sharedError, hash, log } from "@dbx-tools/shared-core";
|
|
2
|
+
import type { ReasoningEffort } from "@dbx-tools/shared-model";
|
|
2
3
|
import { useBrand } from "@dbx-tools/ui-branding/react";
|
|
3
4
|
import type { UIMessage } from "ai";
|
|
4
5
|
import { type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
@@ -39,6 +40,7 @@ import {
|
|
|
39
40
|
} from "../support/thread-sessions.ts";
|
|
40
41
|
|
|
41
42
|
const _loadChatExport = () => import("../support/export.ts");
|
|
43
|
+
const EMPTY_REASONING_EFFORTS: readonly ReasoningEffort[] = [];
|
|
42
44
|
|
|
43
45
|
// Self-contained drop-in chat. `useMastraChat` drives the conversation
|
|
44
46
|
// over `@mastra/client-js`: `agent.stream()` returns a Response
|
|
@@ -239,6 +241,7 @@ export const useMastraChat = <TValues extends Record<string, unknown> = Record<s
|
|
|
239
241
|
const showModelPicker = Boolean(options.showModelPicker);
|
|
240
242
|
const modelKey = modelStorageKey(mastraClient.basePath, agentId);
|
|
241
243
|
const [model, setModel] = useState(() => (showModelPicker ? readStoredModel(modelKey) : ""));
|
|
244
|
+
const [reasoningEffort, setReasoningEffort] = useState<ReasoningEffort | undefined>();
|
|
242
245
|
const handleModelChange = useCallback(
|
|
243
246
|
(nextModel: string) => {
|
|
244
247
|
setModel(nextModel);
|
|
@@ -256,6 +259,8 @@ export const useMastraChat = <TValues extends Record<string, unknown> = Record<s
|
|
|
256
259
|
// `model` to its dependency list.
|
|
257
260
|
const modelRef = useRef(model);
|
|
258
261
|
modelRef.current = model;
|
|
262
|
+
const reasoningEffortRef = useRef(reasoningEffort);
|
|
263
|
+
reasoningEffortRef.current = reasoningEffort;
|
|
259
264
|
const requestContextRef = useRef(options.requestContext);
|
|
260
265
|
requestContextRef.current = options.requestContext;
|
|
261
266
|
// Built-in conversation management. When on, the chat always drives an
|
|
@@ -355,7 +360,19 @@ export const useMastraChat = <TValues extends Record<string, unknown> = Record<s
|
|
|
355
360
|
// The humanized name of the model the active agent falls back to when no
|
|
356
361
|
// model is pinned, so the picker can label its default option. Fetched only
|
|
357
362
|
// when the picker is shown; `null` when the agent's model is dynamic.
|
|
358
|
-
const {
|
|
363
|
+
const {
|
|
364
|
+
defaultModel: defaultModelName,
|
|
365
|
+
defaultModelId,
|
|
366
|
+
loading: defaultModelLoading,
|
|
367
|
+
} = useMastraDefaultModel(agentId, showModelPicker);
|
|
368
|
+
const activeModel = model || defaultModelId || undefined;
|
|
369
|
+
const activeModelOption = models.find((option) => option.name === activeModel);
|
|
370
|
+
const reasoningEfforts = activeModelOption?.reasoningEfforts ?? EMPTY_REASONING_EFFORTS;
|
|
371
|
+
useEffect(() => {
|
|
372
|
+
if (reasoningEffort && !reasoningEfforts.includes(reasoningEffort)) {
|
|
373
|
+
setReasoningEffort(undefined);
|
|
374
|
+
}
|
|
375
|
+
}, [reasoningEffort, reasoningEfforts]);
|
|
359
376
|
// Starter suggestions: an explicit `options.suggestions` always
|
|
360
377
|
// wins (including `[]` to force none) and is rendered verbatim;
|
|
361
378
|
// otherwise auto-source the agent's Genie space sample questions.
|
|
@@ -490,6 +507,7 @@ export const useMastraChat = <TValues extends Record<string, unknown> = Record<s
|
|
|
490
507
|
// threads or changes the model picker while it streams.
|
|
491
508
|
const streamThreadId = threadId === DEFAULT_THREAD_SESSION_KEY ? undefined : threadId;
|
|
492
509
|
const model = modelRef.current || undefined;
|
|
510
|
+
const turnReasoningEffort = reasoningEffortRef.current;
|
|
493
511
|
return driveStream(threadId, assistantId, (signal) => {
|
|
494
512
|
return mastraClient.streamAgent({
|
|
495
513
|
agentId,
|
|
@@ -497,6 +515,7 @@ export const useMastraChat = <TValues extends Record<string, unknown> = Record<s
|
|
|
497
515
|
runId,
|
|
498
516
|
threadId: streamThreadId,
|
|
499
517
|
model,
|
|
518
|
+
reasoningEffort: turnReasoningEffort,
|
|
500
519
|
requestContext,
|
|
501
520
|
signal,
|
|
502
521
|
});
|
|
@@ -970,7 +989,11 @@ export const useMastraChat = <TValues extends Record<string, unknown> = Record<s
|
|
|
970
989
|
models: showModelPicker ? models : undefined,
|
|
971
990
|
model,
|
|
972
991
|
onModelChange: showModelPicker ? handleModelChange : undefined,
|
|
992
|
+
reasoningEffort,
|
|
993
|
+
onReasoningEffortChange: showModelPicker ? setReasoningEffort : undefined,
|
|
973
994
|
defaultModelName: showModelPicker ? (defaultModelName ?? undefined) : undefined,
|
|
995
|
+
defaultModelId: showModelPicker ? (defaultModelId ?? undefined) : undefined,
|
|
996
|
+
defaultModelLoading: showModelPicker && defaultModelLoading,
|
|
974
997
|
onLoadMore: loadOlderHistory,
|
|
975
998
|
isLoadingMore,
|
|
976
999
|
hasMore: activeSession.hasMoreHistory,
|
|
@@ -1028,6 +1051,8 @@ export interface MastraChatProps<
|
|
|
1028
1051
|
className?: string;
|
|
1029
1052
|
/** Host controls placed beside the model picker in the composer footer. */
|
|
1030
1053
|
composerActions?: ReactNode;
|
|
1054
|
+
/** Host controls shown inside the model selector after its endpoint choices. */
|
|
1055
|
+
modelSelectorActions?: ReactNode;
|
|
1031
1056
|
/** Host controls placed on the left side of the composer footer. */
|
|
1032
1057
|
composerLeadingActions?: ReactNode;
|
|
1033
1058
|
}
|
|
@@ -1048,6 +1073,7 @@ export interface MastraChatProps<
|
|
|
1048
1073
|
export const MastraChat = <TValues extends Record<string, unknown> = Record<string, unknown>>({
|
|
1049
1074
|
className,
|
|
1050
1075
|
composerActions,
|
|
1076
|
+
modelSelectorActions,
|
|
1051
1077
|
composerLeadingActions,
|
|
1052
1078
|
...options
|
|
1053
1079
|
}: MastraChatProps<TValues>) => {
|
|
@@ -1057,6 +1083,7 @@ export const MastraChat = <TValues extends Record<string, unknown> = Record<stri
|
|
|
1057
1083
|
{...chat}
|
|
1058
1084
|
className={className}
|
|
1059
1085
|
composerActions={composerActions}
|
|
1086
|
+
modelSelectorActions={modelSelectorActions}
|
|
1060
1087
|
composerLeadingActions={composerLeadingActions}
|
|
1061
1088
|
/>
|
|
1062
1089
|
);
|
|
@@ -184,6 +184,10 @@ export const ThreadSidebar = ({
|
|
|
184
184
|
const isStreaming = streamingThreadIds.includes(thread.id);
|
|
185
185
|
const isConfirming = confirmDeleteId === thread.id;
|
|
186
186
|
const isEditing = editingId === thread.id;
|
|
187
|
+
const actionCount =
|
|
188
|
+
Number(Boolean(isStreaming && onCancel)) +
|
|
189
|
+
Number(Boolean(onRename)) +
|
|
190
|
+
Number(Boolean(onDelete));
|
|
187
191
|
if (isEditing) {
|
|
188
192
|
return (
|
|
189
193
|
<li key={thread.id}>
|
|
@@ -213,8 +217,9 @@ export const ThreadSidebar = ({
|
|
|
213
217
|
<li key={thread.id}>
|
|
214
218
|
<div
|
|
215
219
|
className={cn(
|
|
216
|
-
"group flex items-center
|
|
220
|
+
"group relative flex items-center rounded-md px-2 py-1.5 text-sm",
|
|
217
221
|
"hover:bg-accent hover:text-accent-foreground",
|
|
222
|
+
"focus-within:bg-accent focus-within:text-accent-foreground",
|
|
218
223
|
isActive && "bg-accent text-accent-foreground",
|
|
219
224
|
)}
|
|
220
225
|
>
|
|
@@ -222,7 +227,15 @@ export const ThreadSidebar = ({
|
|
|
222
227
|
type="button"
|
|
223
228
|
onClick={() => onSelect(thread.id)}
|
|
224
229
|
aria-current={isActive ? "page" : undefined}
|
|
225
|
-
className=
|
|
230
|
+
className={cn(
|
|
231
|
+
"min-w-0 flex-1 cursor-pointer text-left transition-[padding] duration-150",
|
|
232
|
+
actionCount === 1 && "group-hover:pr-8 group-focus-within:pr-8",
|
|
233
|
+
actionCount === 2 && "group-hover:pr-14 group-focus-within:pr-14",
|
|
234
|
+
actionCount >= 3 && "group-hover:pr-20 group-focus-within:pr-20",
|
|
235
|
+
(isStreaming || isConfirming) && actionCount === 1 && "pr-8",
|
|
236
|
+
(isStreaming || isConfirming) && actionCount === 2 && "pr-14",
|
|
237
|
+
(isStreaming || isConfirming) && actionCount >= 3 && "pr-20",
|
|
238
|
+
)}
|
|
226
239
|
>
|
|
227
240
|
<div className="flex items-center gap-1.5 truncate">
|
|
228
241
|
{isStreaming && !onCancel ? (
|
|
@@ -239,73 +252,81 @@ export const ThreadSidebar = ({
|
|
|
239
252
|
</div>
|
|
240
253
|
)}
|
|
241
254
|
</button>
|
|
242
|
-
{
|
|
243
|
-
<
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
255
|
+
{actionCount > 0 && (
|
|
256
|
+
<div
|
|
257
|
+
className={cn(
|
|
258
|
+
"pointer-events-none absolute inset-y-0 right-1 flex items-center gap-0.5",
|
|
259
|
+
"bg-gradient-to-l from-accent via-accent to-transparent pl-3",
|
|
260
|
+
"opacity-0 transition-opacity duration-150",
|
|
261
|
+
"group-hover:pointer-events-auto group-hover:opacity-100",
|
|
262
|
+
"group-focus-within:pointer-events-auto group-focus-within:opacity-100",
|
|
263
|
+
(isStreaming || isConfirming) && "pointer-events-auto opacity-100",
|
|
264
|
+
)}
|
|
265
|
+
>
|
|
266
|
+
{isStreaming && onCancel ? (
|
|
267
|
+
<Tooltip>
|
|
268
|
+
<TooltipTrigger asChild>
|
|
269
|
+
<Button
|
|
270
|
+
type="button"
|
|
271
|
+
variant="ghost"
|
|
272
|
+
size="icon"
|
|
273
|
+
onClick={() => onCancel(thread.id)}
|
|
274
|
+
aria-label="Stop generating"
|
|
275
|
+
className="size-6 text-primary"
|
|
276
|
+
>
|
|
277
|
+
<Loader2Icon className="size-3 animate-spin group-hover:hidden" />
|
|
278
|
+
<SquareIcon className="hidden size-3 fill-current group-hover:block" />
|
|
279
|
+
</Button>
|
|
280
|
+
</TooltipTrigger>
|
|
281
|
+
<TooltipContent>Stop generating</TooltipContent>
|
|
282
|
+
</Tooltip>
|
|
283
|
+
) : null}
|
|
284
|
+
{onRename && (
|
|
285
|
+
<Tooltip>
|
|
286
|
+
<TooltipTrigger asChild>
|
|
287
|
+
<Button
|
|
288
|
+
type="button"
|
|
289
|
+
variant="ghost"
|
|
290
|
+
size="icon"
|
|
291
|
+
onClick={(e) => startEdit(e, thread)}
|
|
292
|
+
aria-label="Rename conversation"
|
|
293
|
+
className="size-6"
|
|
294
|
+
>
|
|
295
|
+
<PencilIcon className="size-3.5" />
|
|
296
|
+
</Button>
|
|
297
|
+
</TooltipTrigger>
|
|
298
|
+
<TooltipContent>Rename conversation</TooltipContent>
|
|
299
|
+
</Tooltip>
|
|
300
|
+
)}
|
|
301
|
+
{onDelete && (
|
|
302
|
+
<Tooltip>
|
|
303
|
+
<TooltipTrigger asChild>
|
|
304
|
+
<Button
|
|
305
|
+
type="button"
|
|
306
|
+
variant={isConfirming ? "destructive" : "ghost"}
|
|
307
|
+
size="icon"
|
|
308
|
+
onClick={(e) => handleDeleteClick(e, thread.id)}
|
|
309
|
+
onBlur={() =>
|
|
310
|
+
setConfirmDeleteId((id) => (id === thread.id ? null : id))
|
|
311
|
+
}
|
|
312
|
+
aria-label={
|
|
313
|
+
isConfirming
|
|
314
|
+
? "Confirm delete conversation"
|
|
315
|
+
: "Delete conversation"
|
|
316
|
+
}
|
|
317
|
+
className="size-6"
|
|
318
|
+
>
|
|
319
|
+
<Trash2Icon className="size-3.5" />
|
|
320
|
+
</Button>
|
|
321
|
+
</TooltipTrigger>
|
|
322
|
+
<TooltipContent>
|
|
323
|
+
{isConfirming
|
|
324
|
+
? "Click again to delete this conversation"
|
|
325
|
+
: "Delete conversation"}
|
|
326
|
+
</TooltipContent>
|
|
327
|
+
</Tooltip>
|
|
328
|
+
)}
|
|
329
|
+
</div>
|
|
309
330
|
)}
|
|
310
331
|
</div>
|
|
311
332
|
</li>
|
package/src/react/types.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { GenieWriterEvent } from "@dbx-tools/shared-mastra";
|
|
2
|
+
import type { ReasoningEffort } from "@dbx-tools/shared-model";
|
|
2
3
|
import type { UIMessage } from "ai";
|
|
3
4
|
import type { ReactNode } from "react";
|
|
4
5
|
import type { ExportFormat } from "../support/export.ts";
|
|
@@ -44,7 +45,12 @@ export type ToolProgress = GenieWriterEvent;
|
|
|
44
45
|
* `/models` endpoint derives (Databricks-provided name, else a title-cased
|
|
45
46
|
* rendering of `name`). The picker shows `displayName ?? name`.
|
|
46
47
|
*/
|
|
47
|
-
export type ChatModelOption = {
|
|
48
|
+
export type ChatModelOption = {
|
|
49
|
+
name: string;
|
|
50
|
+
displayName?: string;
|
|
51
|
+
family?: string;
|
|
52
|
+
reasoningEfforts?: ReasoningEffort[];
|
|
53
|
+
};
|
|
48
54
|
|
|
49
55
|
/**
|
|
50
56
|
* A steer message submitted while a turn was already streaming. It waits in a
|
|
@@ -168,6 +174,10 @@ export type ChatViewProps = {
|
|
|
168
174
|
/** Currently selected model name; empty string means "use server default". */
|
|
169
175
|
model?: string;
|
|
170
176
|
onModelChange?: (model: string) => void;
|
|
177
|
+
/** Optional reasoning effort applied to the next turn. Undefined uses the provider default. */
|
|
178
|
+
reasoningEffort?: ReasoningEffort;
|
|
179
|
+
/** Update the per-turn reasoning effort. */
|
|
180
|
+
onReasoningEffortChange?: (effort: ReasoningEffort | undefined) => void;
|
|
171
181
|
/**
|
|
172
182
|
* The serving-endpoint fallback the server uses when no model is pinned,
|
|
173
183
|
* already humanized (the server's `displayName`, e.g. "Claude Sonnet 4.6").
|
|
@@ -175,12 +185,21 @@ export type ChatViewProps = {
|
|
|
175
185
|
* a neutral "Default". Never a raw endpoint id.
|
|
176
186
|
*/
|
|
177
187
|
defaultModelName?: string;
|
|
188
|
+
/** Endpoint id behind the unpinned default, used to resolve family and reasoning metadata. */
|
|
189
|
+
defaultModelId?: string;
|
|
190
|
+
/** True while the server resolves the unpinned agent's live default model. */
|
|
191
|
+
defaultModelLoading?: boolean;
|
|
178
192
|
/**
|
|
179
193
|
* Host-owned controls rendered in the composer footer immediately before the
|
|
180
194
|
* model selector. Use this for per-turn options that belong beside model
|
|
181
195
|
* routing without forking the chat surface.
|
|
182
196
|
*/
|
|
183
197
|
composerActions?: ReactNode;
|
|
198
|
+
/**
|
|
199
|
+
* Host-owned controls rendered inside the model selector after the endpoint
|
|
200
|
+
* choices. Omit when the active agent has no matching model-adjacent option.
|
|
201
|
+
*/
|
|
202
|
+
modelSelectorActions?: ReactNode;
|
|
184
203
|
/**
|
|
185
204
|
* Host-owned actions rendered on the left side of the composer's fixed
|
|
186
205
|
* footer, before Export and Clear.
|
|
@@ -6,12 +6,13 @@ import {
|
|
|
6
6
|
thread,
|
|
7
7
|
wire,
|
|
8
8
|
type Chart,
|
|
9
|
+
type DefaultModelResponse,
|
|
9
10
|
type MastraClientConfig,
|
|
10
11
|
type MastraFeedbackRequest,
|
|
11
12
|
type MastraFeedbackResponse,
|
|
12
13
|
type StatementData,
|
|
13
14
|
} from "@dbx-tools/shared-mastra";
|
|
14
|
-
import type { ServingEndpointSummary } from "@dbx-tools/shared-model";
|
|
15
|
+
import type { ReasoningEffort, ServingEndpointSummary } from "@dbx-tools/shared-model";
|
|
15
16
|
import { usePluginClientConfig } from "@dbx-tools/ui-appkit/react";
|
|
16
17
|
import {
|
|
17
18
|
MastraClient,
|
|
@@ -166,6 +167,7 @@ export class MastraPluginClient extends MastraClient {
|
|
|
166
167
|
runId: string;
|
|
167
168
|
threadId?: string;
|
|
168
169
|
model?: string;
|
|
170
|
+
reasoningEffort?: ReasoningEffort;
|
|
169
171
|
requestContext?: MastraRequestContextSnapshot;
|
|
170
172
|
signal?: AbortSignal;
|
|
171
173
|
}): Promise<MastraStreamResponse> {
|
|
@@ -175,6 +177,9 @@ export class MastraPluginClient extends MastraClient {
|
|
|
175
177
|
messages: params.messages,
|
|
176
178
|
body: {
|
|
177
179
|
runId: params.runId,
|
|
180
|
+
...(params.reasoningEffort
|
|
181
|
+
? { providerOptions: { openai: { reasoningEffort: params.reasoningEffort } } }
|
|
182
|
+
: {}),
|
|
178
183
|
...(params.requestContext ? { requestContext: params.requestContext } : {}),
|
|
179
184
|
},
|
|
180
185
|
routing: params,
|
|
@@ -271,12 +276,18 @@ export class MastraPluginClient extends MastraClient {
|
|
|
271
276
|
* arrives. Defaults to the server's default agent when `agentId` is omitted.
|
|
272
277
|
*/
|
|
273
278
|
async defaultModel(agentId?: string, signal?: AbortSignal): Promise<string | null> {
|
|
279
|
+
const payload = await this.defaultModelDetails(agentId, signal);
|
|
280
|
+
return payload.displayName ?? payload.model;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Fetch the complete resolved default-model descriptor for an agent. */
|
|
284
|
+
async defaultModelDetails(agentId?: string, signal?: AbortSignal): Promise<DefaultModelResponse> {
|
|
274
285
|
const payload = await this.#getJson(
|
|
275
286
|
this.#agentScoped(routes.MASTRA_ROUTES.defaultModel, agentId),
|
|
276
287
|
wire.DefaultModelResponseSchema,
|
|
277
288
|
signal,
|
|
278
289
|
);
|
|
279
|
-
return payload
|
|
290
|
+
return payload;
|
|
280
291
|
}
|
|
281
292
|
|
|
282
293
|
/**
|
|
@@ -619,27 +630,34 @@ export const useMastraModels = (
|
|
|
619
630
|
export const useMastraDefaultModel = (
|
|
620
631
|
agentId?: string,
|
|
621
632
|
enabled = true,
|
|
622
|
-
): { defaultModel: string | null; loading: boolean } => {
|
|
633
|
+
): { defaultModel: string | null; defaultModelId: string | null; loading: boolean } => {
|
|
623
634
|
const client = useMastraClient();
|
|
624
635
|
const [model, setModel] = useState<string | null>(null);
|
|
636
|
+
const [modelId, setModelId] = useState<string | null>(null);
|
|
625
637
|
const [loading, setLoading] = useState(enabled);
|
|
626
638
|
|
|
627
639
|
useEffect(() => {
|
|
628
640
|
if (!enabled) {
|
|
629
641
|
setModel(null);
|
|
642
|
+
setModelId(null);
|
|
630
643
|
setLoading(false);
|
|
631
644
|
return;
|
|
632
645
|
}
|
|
633
646
|
const controller = new AbortController();
|
|
634
647
|
setLoading(true);
|
|
635
648
|
client
|
|
636
|
-
.
|
|
637
|
-
.then((
|
|
638
|
-
if (
|
|
649
|
+
.defaultModelDetails(agentId, controller.signal)
|
|
650
|
+
.then((details) => {
|
|
651
|
+
if (controller.signal.aborted) return;
|
|
652
|
+
setModel(details.displayName ?? details.model);
|
|
653
|
+
setModelId(details.model);
|
|
639
654
|
})
|
|
640
655
|
.catch(() => {
|
|
641
656
|
// Non-critical: a missing default just yields a plain "Default".
|
|
642
|
-
if (!controller.signal.aborted)
|
|
657
|
+
if (!controller.signal.aborted) {
|
|
658
|
+
setModel(null);
|
|
659
|
+
setModelId(null);
|
|
660
|
+
}
|
|
643
661
|
})
|
|
644
662
|
.finally(() => {
|
|
645
663
|
if (!controller.signal.aborted) setLoading(false);
|
|
@@ -647,7 +665,7 @@ export const useMastraDefaultModel = (
|
|
|
647
665
|
return () => controller.abort();
|
|
648
666
|
}, [client, agentId, enabled]);
|
|
649
667
|
|
|
650
|
-
return { defaultModel: model, loading };
|
|
668
|
+
return { defaultModel: model, defaultModelId: modelId, loading };
|
|
651
669
|
};
|
|
652
670
|
|
|
653
671
|
/**
|