@dbx-tools/ui-mastra 0.6.218 → 0.6.220

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,51 @@ 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 focus-contained modal overlay with Escape
125
+ dismissal while the covered application is inert. A `storageKey` retains
126
+ horizontal and vertical sizes independently.
127
+
76
128
  ## Render A Drop-In Chat
77
129
 
78
130
  ```tsx
@@ -94,8 +146,8 @@ export function App() {
94
146
 
95
147
  `MastraChat` is the quickest client for the AppKit-Mastra plugin. It reads the
96
148
  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`.
149
+ through Mastra's official AI SDK chat route, hydrates native memory, and renders
150
+ the controlled `ChatView`.
99
151
 
100
152
  Useful options:
101
153
 
@@ -103,6 +155,9 @@ Useful options:
103
155
  - `showModelPicker` fetches `/models` and sends `X-Mastra-Model` overrides.
104
156
  - `composerActions` places host-owned per-turn controls beside the model
105
157
  selector without replacing the built-in composer.
158
+ - `composerLeadingActions` places attachment or app actions on the left side of
159
+ the fixed composer footer. The textarea grows above it to a cap, then scrolls;
160
+ model and Send/Stop stay pinned bottom-right.
106
161
  - `suggestions` overrides Genie starter questions; omit it to auto-fetch
107
162
  `/suggestions`, or pass `[]` to hide suggestions.
108
163
  - `threadPlacement` chooses where conversation management renders, or turns it
@@ -167,6 +222,28 @@ Use `useMastraChat()` when the stock behavior is right but the surrounding layou
167
222
  belongs to your app. The hook owns streaming, aborts, history paging, thread
168
223
  selection, model overrides, suggestions, exports, approvals, and feedback state.
169
224
 
225
+ ## Pass Typed Application Context
226
+
227
+ `requestContext` accepts a typed plain record, Mastra `RequestContext<T>`, or a
228
+ per-turn resolver. The driver snapshots it when a message is submitted and
229
+ keeps that snapshot with queued steers, regeneration, and approval continuation.
230
+
231
+ ```tsx
232
+ type StoreContext = { storeId: string; route: string };
233
+
234
+ const chat = useMastraChat<StoreContext>({
235
+ requestContext: () => ({
236
+ storeId: selectedStore.id,
237
+ route: location.pathname,
238
+ }),
239
+ });
240
+ ```
241
+
242
+ On the server, declare the same contract with Mastra's native
243
+ `requestContextSchema` on `createAgent<StoreContext>()`. Application context
244
+ cannot override the authenticated memory resource, thread, user, scopes, model,
245
+ or trace fields stamped by AppKit-Mastra.
246
+
170
247
  ## Build A Controlled Chat Surface
171
248
 
172
249
  ```tsx
@@ -200,7 +277,13 @@ const client = new MastraPluginClient(clientConfig);
200
277
  // different threads never share state.
201
278
  const stream = await client.streamAgent({
202
279
  agentId: client.defaultAgent,
203
- messages: [{ role: "user", content: "Hello" }],
280
+ messages: [
281
+ {
282
+ id: "user-1",
283
+ role: "user",
284
+ parts: [{ type: "text", text: "Hello" }],
285
+ },
286
+ ],
204
287
  runId,
205
288
  threadId: activeThreadId,
206
289
  model: "claude sonnet",
@@ -215,18 +298,22 @@ Each conversation thread runs independently: start a turn on one thread, switch
215
298
  to another and start a second, and both stream concurrently. Cancel one via its
216
299
  `AbortSignal` (or the driver's `onCancelThread`) without touching the others.
217
300
 
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.
301
+ `MastraPluginClient` extends `@mastra/client-js` with the official AI SDK
302
+ `chatRoute` transport and AppKit-Mastra additions. It uses
303
+ `credentials: "include"` so session cookies travel with streaming and REST
304
+ calls. The React hooks wrap native memory plus model catalogues, suggestions,
305
+ chart fetches, and statement-data fetches.
222
306
 
223
307
  ## Approvals, Embeds, And Feedback
224
308
 
225
309
  The UI understands the extra events produced by
226
310
  [`@dbx-tools/appkit-mastra`](../../node/appkit-mastra):
227
311
 
228
- - `tool-call-approval` chunks become inline approval cards and call
229
- `approve-tool-call` / `decline-tool-call` when the user decides.
312
+ - Approval chunks become inline approval cards and continue the official chat
313
+ route with Mastra's native `{ approved, reason? }` resume data.
314
+ - Native tool-call and tool-result payloads remain complete. Expanding a tool
315
+ row reveals default-closed Request and Response viewers; large values scroll
316
+ without string or collection truncation.
230
317
  - Genie writer events render as tool progress, including thinking text, SQL, row
231
318
  counts, result summaries, and chart/data markers.
232
319
  - `[chart:<id>]` markers long-poll the chart cache and render ECharts inline.
@@ -303,16 +390,19 @@ touching `navigator.clipboard` or `URL.createObjectURL` again.
303
390
  ## Modules
304
391
 
305
392
  - `MastraChat` - self-contained drop-in chat component.
393
+ - `MastraAssistant` / `useMastraAssistant` - persistent application shell and
394
+ external open/close/context controller.
306
395
  - `useMastraChat` - headless driver that returns `ChatView` props.
307
396
  - `ChatView` - controlled presentational chat shell.
308
- - `MastraPluginClient` - `@mastra/client-js` plus AppKit-Mastra custom routes.
397
+ - `MastraPluginClient` - native `@mastra/client-js` memory/agent APIs plus
398
+ AppKit-Mastra model, suggestion, feedback, and embed routes.
309
399
  - `useMastraClient`, `useMastraConfig`, `useMastraModels`,
310
400
  `useMastraDefaultModel`, `useMastraSuggestions`, `useMastraThreads`,
311
401
  `useChartFetch`, `useStatementFetch` - route/config hooks for controlled
312
402
  clients.
313
403
  - `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.
404
+ - `ThreadTabs` / `ThreadActions` - conversation pills plus reusable New/History
405
+ controls for the top strip or persistent assistant header.
316
406
  - `ExportMenu` - shared export format menu.
317
407
  - `src/support/thread-tabs.ts` - pure open-tab bookkeeping (`syncThreadTabs`,
318
408
  `closeThreadTab`, `nextActiveThreadTab`) so the strip's state is testable
@@ -320,6 +410,10 @@ touching `navigator.clipboard` or `URL.createObjectURL` again.
320
410
  - `src/support/thread-labels.ts` - `threadTitle` / `relativeTime`, shared by
321
411
  the sidebar and the tab strip so a row and a tab never disagree about how an
322
412
  untitled or freshly-updated conversation reads.
413
+ - `src/support/request-context.ts` - immutable per-run snapshots around Mastra's
414
+ native typed `RequestContext`.
415
+ - `src/support/tool-events.ts` - projection of native AI SDK persisted tool
416
+ parts onto the live pill view model.
323
417
  - `src/support/chart-theme.ts` - `ChartChrome`, `LIGHT_CHART_CHROME`,
324
418
  `resolveChartChrome`, and the `useChartChrome` hook that keeps an inline
325
419
  chart's colors in step with the active theme.
@@ -328,8 +422,9 @@ touching `navigator.clipboard` or `URL.createObjectURL` again.
328
422
  an optional `ChartChrome` into a planner spec, shared by the inline chart and
329
423
  the PDF export so both read the same.
330
424
  - Types - `ChatViewProps`, `MastraChatProps`, `UseMastraChatOptions`,
331
- `ThreadPlacement`, `ThreadSummary`, `ToolEvent`, `ToolProgress`,
332
- `PendingApproval`, `FeedbackSubmission`, and related UI contract types.
425
+ `MastraAssistantProps`, `MastraRequestContextInput`, `ThreadPlacement`,
426
+ `ThreadSummary`, `ToolEvent`, `ToolProgress`, `PendingApproval`,
427
+ `FeedbackSubmission`, and related UI contract types.
333
428
 
334
429
  Server-side routes and event production live in
335
430
  [`@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.220";
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, SessionRun, 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.220",
35
+ "@dbx-tools/shared-genie": "0.6.220",
36
+ "@dbx-tools/shared-mastra": "0.6.220",
37
+ "@dbx-tools/shared-model": "0.6.220",
38
+ "@dbx-tools/ui-appkit": "0.6.220",
39
+ "@dbx-tools/ui-branding": "0.6.220",
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.220",
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
@@ -134,6 +135,7 @@ const EmailApprovalPreview = ({ email }: { email: EmailApprovalInput }) => {
134
135
  * {@link ApprovalDecision} to the parent resume handler.
135
136
  */
136
137
  const ToolApprovalCard = ({
138
+ messageId,
137
139
  toolName,
138
140
  toolCallId,
139
141
  runId,
@@ -141,6 +143,7 @@ const ToolApprovalCard = ({
141
143
  onResolve,
142
144
  disabled,
143
145
  }: {
146
+ messageId: string;
144
147
  toolName: string;
145
148
  toolCallId: string;
146
149
  runId?: string;
@@ -157,9 +160,10 @@ const ToolApprovalCard = ({
157
160
  setDecision(approved ? "approved" : "denied");
158
161
  Promise.resolve(
159
162
  approved
160
- ? onResolve({ approved: true, toolName, toolCallId, runId, input })
163
+ ? onResolve({ approved: true, messageId, toolName, toolCallId, runId, input })
161
164
  : onResolve({
162
165
  approved: false,
166
+ messageId,
163
167
  toolName,
164
168
  toolCallId,
165
169
  runId,
@@ -374,6 +378,7 @@ export const AssistantBubble = ({
374
378
  collectPendingApprovals(message.parts),
375
379
  externalApprovals,
376
380
  );
381
+ const toolEvents = mergeToolEvents(toolEventsFromParts(message.parts), events);
377
382
 
378
383
  return (
379
384
  <Item className="items-start gap-3 border-none bg-transparent p-0">
@@ -394,7 +399,7 @@ export const AssistantBubble = ({
394
399
  * pill above the text parts guarantees the "what the agent did"
395
400
  * summary always sits before its resulting charts and answer.
396
401
  */}
397
- {events && events.length > 0 && <ToolSessionPill events={events} />}
402
+ {toolEvents.length > 0 && <ToolSessionPill events={toolEvents} />}
398
403
  {reasoning && (
399
404
  <Collapsible defaultOpen={isReasoningStreaming}>
400
405
  <CollapsibleTrigger className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground">
@@ -426,6 +431,7 @@ export const AssistantBubble = ({
426
431
  {pendingApprovals.map((p) => (
427
432
  <ToolApprovalCard
428
433
  key={p.toolCallId}
434
+ messageId={message.id}
429
435
  toolName={p.toolName}
430
436
  toolCallId={p.toolCallId}
431
437
  runId={p.runId}
@@ -444,6 +450,7 @@ export const AssistantBubble = ({
444
450
  variant="ghost"
445
451
  className="size-7"
446
452
  onClick={() => regenerate()}
453
+ aria-label="Retry response"
447
454
  >
448
455
  <RefreshCcwIcon className="size-3" />
449
456
  </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, type ThreadSession } from "../support/thread-sessions.ts";
8
7
 
9
8
  const logger = log.logger("ui-mastra/chat");
10
9
 
@@ -23,6 +22,35 @@ interface UseChatApprovalsOptions {
23
22
  updateSession: ThreadSessionUpdater;
24
23
  }
25
24
 
25
+ /** Resolve one approval against the exact run and assistant message that created it. */
26
+ export function resolveApprovalContinuation(
27
+ session: ThreadSession,
28
+ decision: ApprovalDecision,
29
+ ):
30
+ | {
31
+ assistantId: string;
32
+ runId: string;
33
+ requestContext: ThreadSession["runRequestContext"];
34
+ }
35
+ | undefined {
36
+ const runId = decision.runId ?? session.runId;
37
+ if (!runId) return undefined;
38
+ const run = session.runs[runId];
39
+ const assistantId =
40
+ decision.messageId ??
41
+ run?.assistantId ??
42
+ Object.entries(session.pendingApprovalsByMessage).find(([, approvals]) =>
43
+ approvals.some((approval) => approval.toolCallId === decision.toolCallId),
44
+ )?.[0] ??
45
+ session.assistantId;
46
+ if (!assistantId) return undefined;
47
+ return {
48
+ assistantId,
49
+ runId,
50
+ requestContext: run?.requestContext,
51
+ };
52
+ }
53
+
26
54
  /** Resume an approval-gated tool call and remove its pending UI state. */
27
55
  export function useChatApprovals({
28
56
  activeKey,
@@ -34,19 +62,19 @@ export function useChatApprovals({
34
62
  }: UseChatApprovalsOptions) {
35
63
  return useCallback(
36
64
  async (decision: ApprovalDecision) => {
37
- const { runId: decisionRunId, toolCallId, toolName } = decision;
65
+ const { toolCallId, toolName } = decision;
38
66
  const session = getSession(activeKey);
39
- const assistantId = session.assistantId;
40
- const runId = decisionRunId ?? session.runId;
41
- if (!runId || !assistantId) {
67
+ const continuation = resolveApprovalContinuation(session, decision);
68
+ if (!continuation) {
42
69
  logger.warn("approval missing runId or assistantId, cannot resume", {
43
70
  tool: toolName,
44
71
  toolCallId,
45
- hasRunId: Boolean(runId),
46
- hasAssistantId: Boolean(assistantId),
72
+ hasRunId: Boolean(decision.runId ?? session.runId),
73
+ hasAssistantId: Boolean(decision.messageId ?? session.assistantId),
47
74
  });
48
75
  return;
49
76
  }
77
+ const { assistantId, runId, requestContext } = continuation;
50
78
 
51
79
  updateSession(activeKey, (current) => {
52
80
  const existing = current.pendingApprovalsByMessage[assistantId];
@@ -77,12 +105,15 @@ export function useChatApprovals({
77
105
  runId,
78
106
  toolCallId,
79
107
  threadId: streamThreadId,
108
+ requestContext,
80
109
  signal,
81
110
  })
82
111
  : mastraClient.declineToolCallStream(agentId, {
83
112
  runId,
84
113
  toolCallId,
85
114
  threadId: streamThreadId,
115
+ reason: decision.reason,
116
+ requestContext,
86
117
  signal,
87
118
  }),
88
119
  );