@dbx-tools/ui-mastra 0.6.218 → 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 CHANGED
@@ -14,15 +14,22 @@ Key features:
14
14
  wires itself to the default agent.
15
15
  - Headless `useMastraChat()` driver for apps that want the same transport logic
16
16
  with custom layout.
17
+ - Persistent `MastraAssistant` application shell with a floating launcher,
18
+ dock/overlay modes, every edge, optional resizing, external controls, and
19
+ route-stable chat state.
17
20
  - Controlled `ChatView` for hosts that own messages, streaming, model state, and
18
21
  route calls themselves.
19
- - `MastraPluginClient` wrapper around `@mastra/client-js` with AppKit-Mastra
20
- routes for history, threads, model lists, suggestions, feedback, charts, and
21
- statement data.
22
+ - `MastraPluginClient` combines Mastra's official AI SDK chat route, native
23
+ memory APIs, and AppKit-Mastra model, suggestion, feedback, chart, and
24
+ statement routes.
22
25
  - Tool-approval support for suspended Mastra `requireApproval` calls, including
23
- direct resumed-stream handling. An email-shaped input (the `send_email` tool)
26
+ resumed-stream handling. An email-shaped input (the `send_email` tool)
24
27
  renders as a formatted To / Cc / Subject / Markdown-body preview rather than
25
28
  raw JSON.
29
+ - Complete native tool input/results retained behind expandable Request and
30
+ Response rows in each tool pill.
31
+ - Typed per-turn application context through Mastra's native `RequestContext`
32
+ and server-side `requestContextSchema`.
26
33
  - Inline embed rendering for `[chart:<id>]` and `[data:<id>]` markers produced by
27
34
  the server plugin.
28
35
  - Conversation sidebar with new, select, rename, delete, active-thread, and
@@ -49,9 +56,9 @@ Use this package when the server is
49
56
  [`@dbx-tools/appkit-mastra`](../../node/appkit-mastra) and the UI needs to
50
57
  understand Mastra-specific behavior:
51
58
 
52
- - `@mastra/client-js` agent streaming plus the plugin's custom history, threads,
53
- models, suggestions, feedback, chart, and statement routes.
54
- - Suspended `requireApproval` tool calls and resumed approve/deny streams.
59
+ - Mastra's official AI SDK chat stream, native resource-scoped memory APIs, and
60
+ the plugin's model, suggestion, feedback, chart, and statement routes.
61
+ - Suspended `requireApproval` tool calls resumed through native `resumeData`.
55
62
  - Genie writer events rendered as inline tool progress, not just a terminal
56
63
  answer.
57
64
  - `[chart:<id>]` and `[data:<id>]` assistant markers rendered as ECharts charts
@@ -73,6 +80,49 @@ The stylesheet imports the shared `@dbx-tools/ui-appkit` foundation and register
73
80
  this package's React files with Tailwind. It does not define design tokens; the
74
81
  chat UI uses AppKit semantic tokens from the host app.
75
82
 
83
+ ## Add A Persistent Assistant
84
+
85
+ Mount `MastraAssistant` inside the app's existing authentication gate and above
86
+ the route outlet. Closing the panel hides its presentation without unmounting
87
+ the chat driver, so drafts, threads, and active streams survive page navigation.
88
+
89
+ ```tsx
90
+ import { MastraAssistant, useMastraAssistant } from "@dbx-tools/ui-mastra/react";
91
+
92
+ const AskAboutStore = ({ storeId }: { storeId: string }) => {
93
+ const assistant = useMastraAssistant<{ storeId: string; route: string }>();
94
+ return (
95
+ <button onClick={() => assistant.open({ storeId, route: window.location.pathname })}>
96
+ Ask assistant
97
+ </button>
98
+ );
99
+ };
100
+
101
+ export function App() {
102
+ return (
103
+ <MastraAssistant
104
+ mode="overlay"
105
+ side="right"
106
+ resizable={{ defaultSize: 480, minSize: 360, storageKey: "assistant-size" }}
107
+ title="Workflow assistant"
108
+ launcher={{ position: "bottom-right", label: "Open assistant" }}
109
+ chat={{ showModelPicker: true, enableExport: true, threadPlacement: "top" }}
110
+ >
111
+ <RouterAndAuthenticatedApplication />
112
+ </MastraAssistant>
113
+ );
114
+ }
115
+ ```
116
+
117
+ `mode="dock"` reflows the child application; `mode="overlay"` overlaps it.
118
+ `side` accepts `top`, `right`, `bottom`, or `left`. Set `resizable={false}` to
119
+ lock size, `launcher={false}` when every launch comes from
120
+ `useMastraAssistant()`, `header={false}` to place New, History, and Close beside
121
+ the conversation pills. With the header enabled those controls move into it and
122
+ the conversation pills remain on their own second row. Replace `icon`, `closeIcon`,
123
+ `newConversationIcon`, `historyIcon`, and `launcher.icon`. Below
124
+ `mobileBreakpoint` the panel becomes a full overlay.
125
+
76
126
  ## Render A Drop-In Chat
77
127
 
78
128
  ```tsx
@@ -94,8 +144,8 @@ export function App() {
94
144
 
95
145
  `MastraChat` is the quickest client for the AppKit-Mastra plugin. It reads the
96
146
  plugin's published client config, creates a `MastraPluginClient`, streams turns
97
- through `agent.stream()`, hydrates the latest history page, and renders the
98
- controlled `ChatView`.
147
+ through Mastra's official AI SDK chat route, hydrates native memory, and renders
148
+ the controlled `ChatView`.
99
149
 
100
150
  Useful options:
101
151
 
@@ -103,6 +153,9 @@ Useful options:
103
153
  - `showModelPicker` fetches `/models` and sends `X-Mastra-Model` overrides.
104
154
  - `composerActions` places host-owned per-turn controls beside the model
105
155
  selector without replacing the built-in composer.
156
+ - `composerLeadingActions` places attachment or app actions on the left side of
157
+ the fixed composer footer. The textarea grows above it to a cap, then scrolls;
158
+ model and Send/Stop stay pinned bottom-right.
106
159
  - `suggestions` overrides Genie starter questions; omit it to auto-fetch
107
160
  `/suggestions`, or pass `[]` to hide suggestions.
108
161
  - `threadPlacement` chooses where conversation management renders, or turns it
@@ -167,6 +220,28 @@ Use `useMastraChat()` when the stock behavior is right but the surrounding layou
167
220
  belongs to your app. The hook owns streaming, aborts, history paging, thread
168
221
  selection, model overrides, suggestions, exports, approvals, and feedback state.
169
222
 
223
+ ## Pass Typed Application Context
224
+
225
+ `requestContext` accepts a typed plain record, Mastra `RequestContext<T>`, or a
226
+ per-turn resolver. The driver snapshots it when a message is submitted and
227
+ keeps that snapshot with queued steers, regeneration, and approval continuation.
228
+
229
+ ```tsx
230
+ type StoreContext = { storeId: string; route: string };
231
+
232
+ const chat = useMastraChat<StoreContext>({
233
+ requestContext: () => ({
234
+ storeId: selectedStore.id,
235
+ route: location.pathname,
236
+ }),
237
+ });
238
+ ```
239
+
240
+ On the server, declare the same contract with Mastra's native
241
+ `requestContextSchema` on `createAgent<StoreContext>()`. Application context
242
+ cannot override the authenticated memory resource, thread, user, scopes, model,
243
+ or trace fields stamped by AppKit-Mastra.
244
+
170
245
  ## Build A Controlled Chat Surface
171
246
 
172
247
  ```tsx
@@ -200,7 +275,13 @@ const client = new MastraPluginClient(clientConfig);
200
275
  // different threads never share state.
201
276
  const stream = await client.streamAgent({
202
277
  agentId: client.defaultAgent,
203
- messages: [{ role: "user", content: "Hello" }],
278
+ messages: [
279
+ {
280
+ id: "user-1",
281
+ role: "user",
282
+ parts: [{ type: "text", text: "Hello" }],
283
+ },
284
+ ],
204
285
  runId,
205
286
  threadId: activeThreadId,
206
287
  model: "claude sonnet",
@@ -215,18 +296,22 @@ Each conversation thread runs independently: start a turn on one thread, switch
215
296
  to another and start a second, and both stream concurrently. Cancel one via its
216
297
  `AbortSignal` (or the driver's `onCancelThread`) without touching the others.
217
298
 
218
- `MastraPluginClient` extends `@mastra/client-js` with the AppKit-Mastra custom
219
- routes. It uses `credentials: "include"` so session cookies travel with streaming
220
- and REST calls. The React hooks wrap common route calls for model catalogues,
221
- suggestions, thread lists, chart fetches, and statement-data fetches.
299
+ `MastraPluginClient` extends `@mastra/client-js` with the official AI SDK
300
+ `chatRoute` transport and AppKit-Mastra additions. It uses
301
+ `credentials: "include"` so session cookies travel with streaming and REST
302
+ calls. The React hooks wrap native memory plus model catalogues, suggestions,
303
+ chart fetches, and statement-data fetches.
222
304
 
223
305
  ## Approvals, Embeds, And Feedback
224
306
 
225
307
  The UI understands the extra events produced by
226
308
  [`@dbx-tools/appkit-mastra`](../../node/appkit-mastra):
227
309
 
228
- - `tool-call-approval` chunks become inline approval cards and call
229
- `approve-tool-call` / `decline-tool-call` when the user decides.
310
+ - Approval chunks become inline approval cards and continue the official chat
311
+ route with Mastra's native `{ approved, reason? }` resume data.
312
+ - Native tool-call and tool-result payloads remain complete. Expanding a tool
313
+ row reveals default-closed Request and Response viewers; large values scroll
314
+ without string or collection truncation.
230
315
  - Genie writer events render as tool progress, including thinking text, SQL, row
231
316
  counts, result summaries, and chart/data markers.
232
317
  - `[chart:<id>]` markers long-poll the chart cache and render ECharts inline.
@@ -303,16 +388,19 @@ touching `navigator.clipboard` or `URL.createObjectURL` again.
303
388
  ## Modules
304
389
 
305
390
  - `MastraChat` - self-contained drop-in chat component.
391
+ - `MastraAssistant` / `useMastraAssistant` - persistent application shell and
392
+ external open/close/context controller.
306
393
  - `useMastraChat` - headless driver that returns `ChatView` props.
307
394
  - `ChatView` - controlled presentational chat shell.
308
- - `MastraPluginClient` - `@mastra/client-js` plus AppKit-Mastra custom routes.
395
+ - `MastraPluginClient` - native `@mastra/client-js` memory/agent APIs plus
396
+ AppKit-Mastra model, suggestion, feedback, and embed routes.
309
397
  - `useMastraClient`, `useMastraConfig`, `useMastraModels`,
310
398
  `useMastraDefaultModel`, `useMastraSuggestions`, `useMastraThreads`,
311
399
  `useChartFetch`, `useStatementFetch` - route/config hooks for controlled
312
400
  clients.
313
401
  - `ThreadSidebar` - controlled conversation list, dockable to either edge.
314
- - `ThreadTabs` - conversation tab strip plus history menu for the `top`
315
- placement; reuses `ThreadSidebar` for the menu itself.
402
+ - `ThreadTabs` / `ThreadActions` - conversation pills plus reusable New/History
403
+ controls for the top strip or persistent assistant header.
316
404
  - `ExportMenu` - shared export format menu.
317
405
  - `src/support/thread-tabs.ts` - pure open-tab bookkeeping (`syncThreadTabs`,
318
406
  `closeThreadTab`, `nextActiveThreadTab`) so the strip's state is testable
@@ -320,6 +408,10 @@ touching `navigator.clipboard` or `URL.createObjectURL` again.
320
408
  - `src/support/thread-labels.ts` - `threadTitle` / `relativeTime`, shared by
321
409
  the sidebar and the tab strip so a row and a tab never disagree about how an
322
410
  untitled or freshly-updated conversation reads.
411
+ - `src/support/request-context.ts` - immutable per-run snapshots around Mastra's
412
+ native typed `RequestContext`.
413
+ - `src/support/tool-events.ts` - projection of native AI SDK persisted tool
414
+ parts onto the live pill view model.
323
415
  - `src/support/chart-theme.ts` - `ChartChrome`, `LIGHT_CHART_CHROME`,
324
416
  `resolveChartChrome`, and the `useChartChrome` hook that keeps an inline
325
417
  chart's colors in step with the active theme.
@@ -328,8 +420,9 @@ touching `navigator.clipboard` or `URL.createObjectURL` again.
328
420
  an optional `ChartChrome` into a planner spec, shared by the inline chart and
329
421
  the PDF export so both read the same.
330
422
  - Types - `ChatViewProps`, `MastraChatProps`, `UseMastraChatOptions`,
331
- `ThreadPlacement`, `ThreadSummary`, `ToolEvent`, `ToolProgress`,
332
- `PendingApproval`, `FeedbackSubmission`, and related UI contract types.
423
+ `MastraAssistantProps`, `MastraRequestContextInput`, `ThreadPlacement`,
424
+ `ThreadSummary`, `ToolEvent`, `ToolProgress`, `PendingApproval`,
425
+ `FeedbackSubmission`, and related UI contract types.
333
426
 
334
427
  Server-side routes and event production live in
335
428
  [`@dbx-tools/appkit-mastra`](../../node/appkit-mastra). Browser-safe route,
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.218";
6
+ export const PACKAGE_VERSION = "0.6.219";
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";
@@ -11,7 +11,6 @@ export * as reactChatFeedback from "./src/react/chat-feedback.ts";
11
11
  export * as reactChatHistory from "./src/react/chat-history.ts";
12
12
  export * as reactChatSessions from "./src/react/chat-sessions.ts";
13
13
  export * as reactChatStream from "./src/react/chat-stream.ts";
14
- export * as reactChatStreamReducer from "./src/react/chat-stream-reducer.ts";
15
14
  export * as reactChatThreadLayout from "./src/react/chat-thread-layout.tsx";
16
15
  export * as reactChatTranscript from "./src/react/chat-transcript.tsx";
17
16
  export * as reactChatView from "./src/react/chat-view.tsx";
@@ -20,6 +19,7 @@ export * as reactEmbedSlots from "./src/react/embed-slots.tsx";
20
19
  export * as reactExportMenu from "./src/react/export-menu.tsx";
21
20
  export * as reactFeedbackControls from "./src/react/feedback-controls.tsx";
22
21
  export * as reactMarkdown from "./src/react/markdown.tsx";
22
+ export * as reactMastraAssistant from "./src/react/mastra-assistant.tsx";
23
23
  export * as reactMastraChat from "./src/react/mastra-chat.tsx";
24
24
  export * as reactSuggestionPills from "./src/react/suggestion-pills.tsx";
25
25
  export * as reactSuggestions from "./src/react/suggestions.ts";
@@ -33,16 +33,16 @@ export * as supportClipboard from "./src/support/clipboard.ts";
33
33
  export * as supportDownload from "./src/support/download.ts";
34
34
  export * as supportExport from "./src/support/export.ts";
35
35
  export * as supportMastraClient from "./src/support/mastra-client.ts";
36
- export * as supportMastraStream from "./src/support/mastra-stream.ts";
37
36
  export * as supportModelSelection from "./src/support/model-selection.ts";
37
+ export * as supportRequestContext from "./src/support/request-context.ts";
38
38
  export * as supportShikiPlugin from "./src/support/shiki-plugin.ts";
39
39
  export * as supportThreadLabels from "./src/support/thread-labels.ts";
40
40
  export * as supportThreadSessions from "./src/support/thread-sessions.ts";
41
41
  export * as supportThreadTabs from "./src/support/thread-tabs.ts";
42
+ export * as supportToolEvents from "./src/support/tool-events.ts";
42
43
  export { AssistantBubble, UserBubble } from "./src/react/bubbles.tsx";
43
44
  export { ChatComposer } from "./src/react/chat-composer.tsx";
44
45
  export type { ThreadSessionReader, ThreadSessionUpdater, ThreadMessageWriter } from "./src/react/chat-sessions.ts";
45
- export type { ChatStreamState, ChatStreamChanges, ChatStreamReduction } from "./src/react/chat-stream-reducer.ts";
46
46
  export { ChatThreadLayout } from "./src/react/chat-thread-layout.tsx";
47
47
  export { useChatTranscriptController, ChatTranscript } from "./src/react/chat-transcript.tsx";
48
48
  export type { ChatTranscriptController } from "./src/react/chat-transcript.tsx";
@@ -53,6 +53,7 @@ export { MarkdownWithEmbeds } from "./src/react/embed-slots.tsx";
53
53
  export { ExportMenu } from "./src/react/export-menu.tsx";
54
54
  export { FeedbackControls } from "./src/react/feedback-controls.tsx";
55
55
  export { AssistantMarkdown, ToolMarkdown, SqlBlock } from "./src/react/markdown.tsx";
56
+ export type { MastraAssistantMode, MastraAssistantSide, MastraAssistantLauncherPosition, MastraAssistantResizeOptions, MastraAssistantLauncherOptions, MastraAssistantController, MastraAssistantProps } from "./src/react/mastra-assistant.tsx";
56
57
  export { useMastraChat, MastraChat } from "./src/react/mastra-chat.tsx";
57
58
  export type { UseMastraChatOptions, MastraChatProps } from "./src/react/mastra-chat.tsx";
58
59
  export { SuggestionPills } from "./src/react/suggestion-pills.tsx";
@@ -60,17 +61,17 @@ export type { SuggestionPillsProps } from "./src/react/suggestion-pills.tsx";
60
61
  export { dedupeSuggestions, collectSuggestions } from "./src/react/suggestions.ts";
61
62
  export { ThreadSidebar } from "./src/react/thread-sidebar.tsx";
62
63
  export type { ThreadSidebarProps } from "./src/react/thread-sidebar.tsx";
63
- export { ThreadTabs } from "./src/react/thread-tabs.tsx";
64
- export type { ThreadTabsProps } from "./src/react/thread-tabs.tsx";
64
+ export { ThreadActions, ThreadTabs } from "./src/react/thread-tabs.tsx";
65
+ export type { ThreadTabsProps, ThreadActionsProps } from "./src/react/thread-tabs.tsx";
65
66
  export { humanizeToolName, ToolSessionPill } from "./src/react/tool-pill.tsx";
66
67
  export type { ChatStatus, ToolEvent, ToolProgress, ChatModelOption, QueuedSteer, FeedbackValue, FeedbackSubmission, MessageFeedback, ThreadSummary, ThreadPlacement, ChatViewProps, ApprovalDecision, PendingApproval } from "./src/react/types.ts";
67
68
  export { LIGHT_CHART_CHROME } from "./src/support/chart-theme.ts";
68
69
  export type { ChartChrome } from "./src/support/chart-theme.ts";
69
70
  export type { ExportFormat, ExportBrand, EmbedResolver, ExportChatOptions } from "./src/support/export.ts";
70
71
  export { MastraPluginClient, useMastraConfig, useMastraClient, useMastraModels, useMastraDefaultModel, useMastraSuggestions, useMastraThreads, useChartFetch, useStatementFetch } from "./src/support/mastra-client.ts";
71
- export type { ByIdFetchState } from "./src/support/mastra-client.ts";
72
- export type { MastraStreamResponse } from "./src/support/mastra-stream.ts";
72
+ export type { MastraStreamResponse, MastraMemoryThread, ByIdFetchState } from "./src/support/mastra-client.ts";
73
73
  export { modelStorageKey, readStoredModel, storeSelectedModel } from "./src/support/model-selection.ts";
74
+ export type { MastraRequestContextInput, MastraRequestContextSource, MastraRequestContextSnapshot } from "./src/support/request-context.ts";
74
75
  export { DEFAULT_THREAD_SESSION_KEY } from "./src/support/thread-sessions.ts";
75
- export type { ThreadSession } from "./src/support/thread-sessions.ts";
76
+ export type { SessionQueuedSteer, ThreadSession } from "./src/support/thread-sessions.ts";
76
77
  export { THREAD_TAB_SEED_MAX } from "./src/support/thread-tabs.ts";
package/package.json CHANGED
@@ -31,13 +31,13 @@
31
31
  "react-dom": "~19.2.4"
32
32
  },
33
33
  "dependencies": {
34
- "@dbx-tools/shared-core": "0.6.218",
35
- "@dbx-tools/shared-genie": "0.6.218",
36
- "@dbx-tools/shared-mastra": "0.6.218",
37
- "@dbx-tools/shared-model": "0.6.218",
38
- "@dbx-tools/ui-appkit": "0.6.218",
39
- "@dbx-tools/ui-branding": "0.6.218",
40
- "@mastra/client-js": "1.46.0",
34
+ "@dbx-tools/shared-core": "0.6.219",
35
+ "@dbx-tools/shared-genie": "0.6.219",
36
+ "@dbx-tools/shared-mastra": "0.6.219",
37
+ "@dbx-tools/shared-model": "0.6.219",
38
+ "@dbx-tools/ui-appkit": "0.6.219",
39
+ "@dbx-tools/ui-branding": "0.6.219",
40
+ "@mastra/client-js": "1.50.0",
41
41
  "@tanstack/react-table": "^8.21.3",
42
42
  "ai": "^5.0.0",
43
43
  "echarts": "^6.0.0",
@@ -52,7 +52,7 @@
52
52
  "publishConfig": {
53
53
  "access": "public"
54
54
  },
55
- "version": "0.6.218",
55
+ "version": "0.6.219",
56
56
  "type": "module",
57
57
  "exports": {
58
58
  "./react": "./src/react/index.ts",
@@ -41,6 +41,7 @@ import type {
41
41
  ToolEvent,
42
42
  } from "./types.ts";
43
43
  import { copyText } from "../support/clipboard.ts";
44
+ import { mergeToolEvents, toolEventsFromParts } from "../support/tool-events.ts";
44
45
 
45
46
  // User / assistant message bubbles plus the inline approval card and
46
47
  // the helpers that surface approval-gated tool calls out of a message's
@@ -374,6 +375,7 @@ export const AssistantBubble = ({
374
375
  collectPendingApprovals(message.parts),
375
376
  externalApprovals,
376
377
  );
378
+ const toolEvents = mergeToolEvents(toolEventsFromParts(message.parts), events);
377
379
 
378
380
  return (
379
381
  <Item className="items-start gap-3 border-none bg-transparent p-0">
@@ -394,7 +396,7 @@ export const AssistantBubble = ({
394
396
  * pill above the text parts guarantees the "what the agent did"
395
397
  * summary always sits before its resulting charts and answer.
396
398
  */}
397
- {events && events.length > 0 && <ToolSessionPill events={events} />}
399
+ {toolEvents.length > 0 && <ToolSessionPill events={toolEvents} />}
398
400
  {reasoning && (
399
401
  <Collapsible defaultOpen={isReasoningStreaming}>
400
402
  <CollapsibleTrigger className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground">
@@ -444,6 +446,7 @@ export const AssistantBubble = ({
444
446
  variant="ghost"
445
447
  className="size-7"
446
448
  onClick={() => regenerate()}
449
+ aria-label="Retry response"
447
450
  >
448
451
  <RefreshCcwIcon className="size-3" />
449
452
  </Button>
@@ -1,10 +1,9 @@
1
1
  import { log } from "@dbx-tools/shared-core";
2
2
  import { useCallback } from "react";
3
- import type { MastraPluginClient } from "../support/mastra-client.ts";
4
- import type { MastraStreamResponse } from "../support/mastra-stream.ts";
5
- import { DEFAULT_THREAD_SESSION_KEY } from "../support/thread-sessions.ts";
6
3
  import type { ThreadSessionReader, ThreadSessionUpdater } from "./chat-sessions.ts";
7
4
  import type { ApprovalDecision } from "./types.ts";
5
+ import type { MastraPluginClient, MastraStreamResponse } from "../support/mastra-client.ts";
6
+ import { DEFAULT_THREAD_SESSION_KEY } from "../support/thread-sessions.ts";
8
7
 
9
8
  const logger = log.logger("ui-mastra/chat");
10
9
 
@@ -36,7 +35,10 @@ export function useChatApprovals({
36
35
  async (decision: ApprovalDecision) => {
37
36
  const { runId: decisionRunId, toolCallId, toolName } = decision;
38
37
  const session = getSession(activeKey);
39
- const assistantId = session.assistantId;
38
+ const assistantId =
39
+ Object.entries(session.pendingApprovalsByMessage).find(([, approvals]) =>
40
+ approvals.some((approval) => approval.toolCallId === toolCallId),
41
+ )?.[0] ?? session.assistantId;
40
42
  const runId = decisionRunId ?? session.runId;
41
43
  if (!runId || !assistantId) {
42
44
  logger.warn("approval missing runId or assistantId, cannot resume", {
@@ -77,12 +79,15 @@ export function useChatApprovals({
77
79
  runId,
78
80
  toolCallId,
79
81
  threadId: streamThreadId,
82
+ requestContext: session.runRequestContext,
80
83
  signal,
81
84
  })
82
85
  : mastraClient.declineToolCallStream(agentId, {
83
86
  runId,
84
87
  toolCallId,
85
88
  threadId: streamThreadId,
89
+ reason: decision.reason,
90
+ requestContext: session.runRequestContext,
86
91
  signal,
87
92
  }),
88
93
  );
@@ -31,7 +31,7 @@ import {
31
31
  Trash2Icon,
32
32
  XIcon,
33
33
  } from "lucide-react";
34
- import { useCallback, useLayoutEffect, useRef, useState } from "react";
34
+ import { useCallback, useId, useLayoutEffect, useRef, useState } from "react";
35
35
  import { autosizeComposerTextarea, observeComposerWidth } from "./_composer-autosize.ts";
36
36
  import { ExportMenu } from "./export-menu.tsx";
37
37
  import { SuggestionPills } from "./suggestion-pills.tsx";
@@ -251,6 +251,7 @@ type ChatComposerProps = {
251
251
  onModelChange: ChatViewProps["onModelChange"];
252
252
  defaultModelName: ChatViewProps["defaultModelName"];
253
253
  composerActions: ChatViewProps["composerActions"];
254
+ composerLeadingActions: ChatViewProps["composerLeadingActions"];
254
255
  isLoadingHistory: NonNullable<ChatViewProps["isLoadingHistory"]>;
255
256
  onClear: ChatViewProps["onClear"];
256
257
  onExportConversation: ChatViewProps["onExportConversation"];
@@ -273,12 +274,14 @@ export const ChatComposer = ({
273
274
  onModelChange,
274
275
  defaultModelName,
275
276
  composerActions,
277
+ composerLeadingActions,
276
278
  isLoadingHistory,
277
279
  onClear,
278
280
  onExportConversation,
279
281
  onResumeFollow,
280
282
  }: ChatComposerProps) => {
281
283
  const [input, setInput] = useState("");
284
+ const inputId = useId();
282
285
  const inputGroupRef = useRef<HTMLDivElement>(null);
283
286
  const textareaRef = useRef<HTMLTextAreaElement>(null);
284
287
 
@@ -316,12 +319,6 @@ export const ChatComposer = ({
316
319
  sensitivity: "base",
317
320
  }),
318
321
  );
319
- const showToolbar =
320
- showModelDisplay ||
321
- Boolean(composerActions) ||
322
- Boolean(onExportConversation) ||
323
- Boolean(onClear);
324
-
325
322
  return (
326
323
  <>
327
324
  {isEmpty && (
@@ -350,6 +347,8 @@ export const ChatComposer = ({
350
347
  className="rounded-2xl border-border/80 shadow-sm transition-shadow focus-within:shadow-md"
351
348
  >
352
349
  <InputGroupTextarea
350
+ id={`${inputId}-message`}
351
+ name="message"
353
352
  ref={textareaRef}
354
353
  value={input}
355
354
  onChange={(event) => setInput(event.target.value)}
@@ -362,76 +361,84 @@ export const ChatComposer = ({
362
361
  placeholder={isLoadingHistory ? "Loading history..." : "Send a message..."}
363
362
  rows={1}
364
363
  disabled={isLoadingHistory}
365
- className="field-sizing-fixed max-h-48 overflow-y-auto text-base md:text-sm"
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"
366
365
  />
367
- <InputGroupAddon align="inline-end">
368
- {isRunning && onStop && !input.trim() ? (
369
- <InputGroupButton
370
- type="button"
371
- size="icon-sm"
372
- variant="default"
373
- onClick={() => onStop()}
374
- aria-label="Stop response"
375
- >
376
- <SquareIcon className="size-3 fill-current" />
377
- </InputGroupButton>
378
- ) : (
379
- <InputGroupButton
380
- type="submit"
381
- size="icon-sm"
382
- variant="default"
383
- disabled={!input.trim() || isLoadingHistory}
384
- aria-label={isRunning ? "Send now (interrupts)" : "Send message"}
385
- >
386
- <SendIcon className="size-3" />
387
- </InputGroupButton>
388
- )}
389
- </InputGroupAddon>
390
- </InputGroup>
391
- {showToolbar && (
392
- <div className="mt-2 flex items-center gap-2 text-xs text-muted-foreground">
393
- {showModelDisplay &&
394
- (modelChangeable ? (
395
- <Select
396
- value={model ? model : DEFAULT_MODEL_VALUE}
397
- onValueChange={(value) =>
398
- onModelChange?.(value === DEFAULT_MODEL_VALUE ? "" : value)
399
- }
366
+ <InputGroupAddon
367
+ align="block-end"
368
+ className="flex w-full flex-row flex-wrap items-center justify-between gap-2 px-3 pb-3 pt-0"
369
+ >
370
+ <div className="flex min-w-0 max-w-full flex-wrap items-center gap-2">
371
+ {composerLeadingActions}
372
+ {onExportConversation && (
373
+ <ExportMenu
374
+ onExport={(format) => void onExportConversation(format)}
375
+ tooltip="Export conversation"
400
376
  disabled={isLoadingHistory}
401
- >
402
- <SelectTrigger
403
- size="sm"
404
- className="h-7 w-auto max-w-[200px] gap-1 rounded-full px-2.5 text-xs [&_svg]:size-3"
377
+ />
378
+ )}
379
+ {onClear && (
380
+ <ClearConversationAction onClear={onClear} isLoadingHistory={isLoadingHistory} />
381
+ )}
382
+ </div>
383
+ <div className="ml-auto flex min-w-0 max-w-full items-center gap-1.5">
384
+ {composerActions}
385
+ {showModelDisplay &&
386
+ (modelChangeable ? (
387
+ <Select
388
+ name="mastra-model"
389
+ value={model ? model : DEFAULT_MODEL_VALUE}
390
+ onValueChange={(value) =>
391
+ onModelChange?.(value === DEFAULT_MODEL_VALUE ? "" : value)
392
+ }
393
+ disabled={isLoadingHistory}
405
394
  >
406
- <SelectValue placeholder={defaultOptionLabel} />
407
- </SelectTrigger>
408
- <SelectContent>
409
- <SelectItem value={DEFAULT_MODEL_VALUE}>{defaultOptionLabel}</SelectItem>
410
- {sortedModels.map((option) => (
411
- <SelectItem key={option.name} value={option.name}>
412
- {option.displayName || option.name}
413
- </SelectItem>
414
- ))}
415
- </SelectContent>
416
- </Select>
395
+ <SelectTrigger
396
+ size="sm"
397
+ aria-label="Model"
398
+ className="h-7 w-auto max-w-[180px] gap-1 truncate border-0 bg-transparent px-2 text-xs shadow-none hover:bg-accent [&_svg]:size-3"
399
+ >
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>
411
+ ) : (
412
+ <span className="max-w-[180px] truncate px-2 text-xs text-muted-foreground">
413
+ {currentModelLabel}
414
+ </span>
415
+ ))}
416
+ {isRunning && onStop && !input.trim() ? (
417
+ <InputGroupButton
418
+ type="button"
419
+ size="icon-sm"
420
+ variant="default"
421
+ onClick={() => onStop()}
422
+ aria-label="Stop response"
423
+ className="shrink-0 rounded-full"
424
+ >
425
+ <SquareIcon className="size-3 fill-current" />
426
+ </InputGroupButton>
417
427
  ) : (
418
- <span className="max-w-[200px] truncate px-2.5 text-xs text-muted-foreground">
419
- {currentModelLabel}
420
- </span>
421
- ))}
422
- {composerActions}
423
- {onExportConversation && (
424
- <ExportMenu
425
- onExport={(format) => void onExportConversation(format)}
426
- tooltip="Export conversation"
427
- disabled={isLoadingHistory}
428
- />
429
- )}
430
- {onClear && (
431
- <ClearConversationAction onClear={onClear} isLoadingHistory={isLoadingHistory} />
432
- )}
433
- </div>
434
- )}
428
+ <InputGroupButton
429
+ type="submit"
430
+ size="icon-sm"
431
+ variant="default"
432
+ disabled={!input.trim() || isLoadingHistory}
433
+ aria-label={isRunning ? "Send now (interrupts)" : "Send message"}
434
+ className="shrink-0 rounded-full"
435
+ >
436
+ <SendIcon className="size-3" />
437
+ </InputGroupButton>
438
+ )}
439
+ </div>
440
+ </InputGroupAddon>
441
+ </InputGroup>
435
442
  </form>
436
443
  </>
437
444
  );
@@ -1,9 +1,9 @@
1
1
  import { error as sharedError, log } from "@dbx-tools/shared-core";
2
2
  import type { UIMessage } from "ai";
3
3
  import { useCallback, useRef } from "react";
4
- import type { MastraPluginClient } from "../support/mastra-client.ts";
5
- import type { FeedbackSubmission, MessageFeedback } from "./types.ts";
6
4
  import type { ThreadSessionUpdater } from "./chat-sessions.ts";
5
+ import type { FeedbackSubmission, MessageFeedback } from "./types.ts";
6
+ import type { MastraPluginClient } from "../support/mastra-client.ts";
7
7
 
8
8
  const logger = log.logger("ui-mastra/chat");
9
9