@vertesia/ui 1.4.0 → 1.4.1

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.
Files changed (196) hide show
  1. package/lib/core/components/SidePanel.js +1 -1
  2. package/lib/core/components/SidePanel.js.map +1 -1
  3. package/lib/core/components/shadcn/button.d.ts +10 -3
  4. package/lib/core/components/shadcn/button.d.ts.map +1 -1
  5. package/lib/core/components/shadcn/button.js +41 -9
  6. package/lib/core/components/shadcn/button.js.map +1 -1
  7. package/lib/core/components/shadcn/modal/dialog.js +1 -1
  8. package/lib/core/components/shadcn/modal/dialog.js.map +1 -1
  9. package/lib/core/components/shadcn/resizeable.d.ts +1 -1
  10. package/lib/core/components/shadcn/resizeable.d.ts.map +1 -1
  11. package/lib/core/components/shadcn/resizeable.js +12 -2
  12. package/lib/core/components/shadcn/resizeable.js.map +1 -1
  13. package/lib/core/components/shadcn/tabs.d.ts +12 -1
  14. package/lib/core/components/shadcn/tabs.d.ts.map +1 -1
  15. package/lib/core/components/shadcn/tabs.js +12 -4
  16. package/lib/core/components/shadcn/tabs.js.map +1 -1
  17. package/lib/core/components/shadcn/theme/ThemeSwitcher.js +1 -1
  18. package/lib/core/components/shadcn/theme/ThemeSwitcher.js.map +1 -1
  19. package/lib/core/hooks/useIntersectionObserver.d.ts.map +1 -1
  20. package/lib/core/hooks/useIntersectionObserver.js +3 -1
  21. package/lib/core/hooks/useIntersectionObserver.js.map +1 -1
  22. package/lib/features/agent/chat/AgentChatPlaybackControls.d.ts.map +1 -1
  23. package/lib/features/agent/chat/AgentChatPlaybackControls.js +2 -2
  24. package/lib/features/agent/chat/AgentChatPlaybackControls.js.map +1 -1
  25. package/lib/features/agent/chat/AgentRequestInputOverlay.d.ts.map +1 -1
  26. package/lib/features/agent/chat/AgentRequestInputOverlay.js +2 -2
  27. package/lib/features/agent/chat/AgentRequestInputOverlay.js.map +1 -1
  28. package/lib/features/agent/chat/AgentRightPanel.js +1 -1
  29. package/lib/features/agent/chat/AgentRightPanel.js.map +1 -1
  30. package/lib/features/agent/chat/AskUserWidget.d.ts.map +1 -1
  31. package/lib/features/agent/chat/AskUserWidget.js +4 -3
  32. package/lib/features/agent/chat/AskUserWidget.js.map +1 -1
  33. package/lib/features/agent/chat/ImageLightbox.d.ts.map +1 -1
  34. package/lib/features/agent/chat/ImageLightbox.js +1 -1
  35. package/lib/features/agent/chat/ImageLightbox.js.map +1 -1
  36. package/lib/features/agent/chat/ModernAgentConversation.d.ts.map +1 -1
  37. package/lib/features/agent/chat/ModernAgentConversation.js +57 -12
  38. package/lib/features/agent/chat/ModernAgentConversation.js.map +1 -1
  39. package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.d.ts +8 -1
  40. package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.d.ts.map +1 -1
  41. package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.js +45 -36
  42. package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.js.map +1 -1
  43. package/lib/features/agent/chat/ModernAgentOutput/Header.d.ts.map +1 -1
  44. package/lib/features/agent/chat/ModernAgentOutput/Header.js +4 -2
  45. package/lib/features/agent/chat/ModernAgentOutput/Header.js.map +1 -1
  46. package/lib/features/agent/chat/ModernAgentOutput/MessageInput.d.ts.map +1 -1
  47. package/lib/features/agent/chat/ModernAgentOutput/MessageInput.js +7 -3
  48. package/lib/features/agent/chat/ModernAgentOutput/MessageInput.js.map +1 -1
  49. package/lib/features/agent/chat/ModernAgentOutput/MessageItem.d.ts.map +1 -1
  50. package/lib/features/agent/chat/ModernAgentOutput/MessageItem.js +2 -1
  51. package/lib/features/agent/chat/ModernAgentOutput/MessageItem.js.map +1 -1
  52. package/lib/features/agent/chat/ModernAgentOutput/WorkstreamTabs.d.ts.map +1 -1
  53. package/lib/features/agent/chat/ModernAgentOutput/WorkstreamTabs.js +158 -8
  54. package/lib/features/agent/chat/ModernAgentOutput/WorkstreamTabs.js.map +1 -1
  55. package/lib/features/agent/chat/ModernAgentOutput/requestInputMessages.d.ts +6 -0
  56. package/lib/features/agent/chat/ModernAgentOutput/requestInputMessages.d.ts.map +1 -1
  57. package/lib/features/agent/chat/ModernAgentOutput/requestInputMessages.js +15 -1
  58. package/lib/features/agent/chat/ModernAgentOutput/requestInputMessages.js.map +1 -1
  59. package/lib/features/agent/chat/hooks/useFileProcessing.d.ts +1 -0
  60. package/lib/features/agent/chat/hooks/useFileProcessing.d.ts.map +1 -1
  61. package/lib/features/agent/chat/hooks/useFileProcessing.js +37 -3
  62. package/lib/features/agent/chat/hooks/useFileProcessing.js.map +1 -1
  63. package/lib/features/magic-pdf/AnnotatedImageSlider.js +13 -13
  64. package/lib/features/magic-pdf/AnnotatedImageSlider.js.map +1 -1
  65. package/lib/features/magic-pdf/DownloadPopover.js +2 -2
  66. package/lib/features/magic-pdf/DownloadPopover.js.map +1 -1
  67. package/lib/features/magic-pdf/MagicPdfView.js +3 -3
  68. package/lib/features/magic-pdf/MagicPdfView.js.map +1 -1
  69. package/lib/features/oauth/McpConnectionsButton.d.ts.map +1 -1
  70. package/lib/features/oauth/McpConnectionsButton.js +2 -2
  71. package/lib/features/oauth/McpConnectionsButton.js.map +1 -1
  72. package/lib/features/oauth/McpConnectionsDialog.d.ts.map +1 -1
  73. package/lib/features/oauth/McpConnectionsDialog.js +1 -1
  74. package/lib/features/oauth/McpConnectionsDialog.js.map +1 -1
  75. package/lib/features/oauth/RemoteMcpConnectionButton.d.ts.map +1 -1
  76. package/lib/features/oauth/RemoteMcpConnectionButton.js +1 -1
  77. package/lib/features/oauth/RemoteMcpConnectionButton.js.map +1 -1
  78. package/lib/features/pdf-viewer/PdfPageSlider.js +2 -2
  79. package/lib/features/pdf-viewer/PdfPageSlider.js.map +1 -1
  80. package/lib/features/pdf-viewer/SimplePdfViewer.d.ts.map +1 -1
  81. package/lib/features/pdf-viewer/SimplePdfViewer.js +3 -3
  82. package/lib/features/pdf-viewer/SimplePdfViewer.js.map +1 -1
  83. package/lib/features/store/objects/DocumentQuickFilter.d.ts.map +1 -1
  84. package/lib/features/store/objects/DocumentQuickFilter.js +5 -5
  85. package/lib/features/store/objects/DocumentQuickFilter.js.map +1 -1
  86. package/lib/features/store/objects/DocumentSearchResults.d.ts.map +1 -1
  87. package/lib/features/store/objects/DocumentSearchResults.js +2 -2
  88. package/lib/features/store/objects/DocumentSearchResults.js.map +1 -1
  89. package/lib/features/store/objects/components/ContentDispositionButton.d.ts.map +1 -1
  90. package/lib/features/store/objects/components/ContentDispositionButton.js +1 -1
  91. package/lib/features/store/objects/components/ContentDispositionButton.js.map +1 -1
  92. package/lib/features/store/objects/components/ContentOverview.js +3 -3
  93. package/lib/features/store/objects/components/ContentOverview.js.map +1 -1
  94. package/lib/features/store/objects/components/DocumentIcon.js +1 -1
  95. package/lib/features/store/objects/components/VectorSearchWidget.js +1 -1
  96. package/lib/features/store/objects/components/VectorSearchWidget.js.map +1 -1
  97. package/lib/features/store/objects/layout/renderers.d.ts.map +1 -1
  98. package/lib/features/store/objects/layout/renderers.js +2 -2
  99. package/lib/features/store/objects/layout/renderers.js.map +1 -1
  100. package/lib/features/store/objects/selection/SelectionActions.js +1 -1
  101. package/lib/features/store/objects/selection/SelectionActions.js.map +1 -1
  102. package/lib/features/store/objects/selection/actions/StartWorkflowComponent.js +4 -4
  103. package/lib/features/store/objects/selection/actions/StartWorkflowComponent.js.map +1 -1
  104. package/lib/features/store/types/ContentObjectTypesTable.d.ts.map +1 -1
  105. package/lib/features/store/types/ContentObjectTypesTable.js +4 -4
  106. package/lib/features/store/types/ContentObjectTypesTable.js.map +1 -1
  107. package/lib/features/store/types/SelectContentTypeModal.js +1 -1
  108. package/lib/features/store/types/SelectContentTypeModal.js.map +1 -1
  109. package/lib/features/user/UserInfo.js +7 -3
  110. package/lib/features/user/UserInfo.js.map +1 -1
  111. package/lib/router/HistoryNavigator.d.ts.map +1 -1
  112. package/lib/router/HistoryNavigator.js +5 -0
  113. package/lib/router/HistoryNavigator.js.map +1 -1
  114. package/lib/router/Nav.d.ts.map +1 -1
  115. package/lib/router/Nav.js +9 -1
  116. package/lib/router/Nav.js.map +1 -1
  117. package/lib/vertesia-ui-core.js +1 -1
  118. package/lib/vertesia-ui-core.js.map +1 -1
  119. package/lib/vertesia-ui-features.js +1 -1
  120. package/lib/vertesia-ui-features.js.map +1 -1
  121. package/lib/vertesia-ui-router.js +1 -1
  122. package/lib/vertesia-ui-router.js.map +1 -1
  123. package/lib/vertesia-ui-widgets.js +1 -1
  124. package/lib/vertesia-ui-widgets.js.map +1 -1
  125. package/lib/widgets/form/Form.js +1 -1
  126. package/lib/widgets/form/Form.js.map +1 -1
  127. package/lib/widgets/json-view/JSONSwitcher.js +1 -1
  128. package/lib/widgets/json-view/JSONSwitcher.js.map +1 -1
  129. package/lib/widgets/json-view/JSONView.js +6 -6
  130. package/lib/widgets/json-view/JSONView.js.map +1 -1
  131. package/lib/widgets/monacoEditor/MonacoEditor.d.ts.map +1 -1
  132. package/lib/widgets/monacoEditor/MonacoEditor.js +29 -0
  133. package/lib/widgets/monacoEditor/MonacoEditor.js.map +1 -1
  134. package/lib/widgets/xml-viewer/hooks/useXMLViewer.d.ts +9 -0
  135. package/lib/widgets/xml-viewer/hooks/useXMLViewer.d.ts.map +1 -1
  136. package/lib/widgets/xml-viewer/hooks/useXMLViewer.js +20 -17
  137. package/lib/widgets/xml-viewer/hooks/useXMLViewer.js.map +1 -1
  138. package/package.json +29 -30
  139. package/src/__tests__/a11y.test.tsx +73 -0
  140. package/src/core/components/SidePanel.tsx +1 -1
  141. package/src/core/components/shadcn/button.tsx +67 -12
  142. package/src/core/components/shadcn/modal/dialog.tsx +1 -1
  143. package/src/core/components/shadcn/resizeable.test.tsx +77 -0
  144. package/src/core/components/shadcn/resizeable.tsx +28 -2
  145. package/src/core/components/shadcn/tabs.test.tsx +65 -0
  146. package/src/core/components/shadcn/tabs.tsx +25 -3
  147. package/src/core/components/shadcn/theme/ThemeSwitcher.tsx +1 -1
  148. package/src/core/hooks/useIntersectionObserver.tsx +3 -1
  149. package/src/features/agent/chat/AgentChatPlaybackControls.tsx +0 -5
  150. package/src/features/agent/chat/AgentRequestInputOverlay.test.tsx +31 -0
  151. package/src/features/agent/chat/AgentRequestInputOverlay.tsx +6 -2
  152. package/src/features/agent/chat/AgentRightPanel.tsx +1 -1
  153. package/src/features/agent/chat/AskUserWidget.test.tsx +49 -0
  154. package/src/features/agent/chat/AskUserWidget.tsx +32 -22
  155. package/src/features/agent/chat/ImageLightbox.tsx +0 -1
  156. package/src/features/agent/chat/ModernAgentConversation.test.tsx +246 -1
  157. package/src/features/agent/chat/ModernAgentConversation.tsx +62 -17
  158. package/src/features/agent/chat/ModernAgentOutput/AllMessagesMixed.test.tsx +6 -1
  159. package/src/features/agent/chat/ModernAgentOutput/AllMessagesMixed.tsx +81 -49
  160. package/src/features/agent/chat/ModernAgentOutput/Header.tsx +3 -3
  161. package/src/features/agent/chat/ModernAgentOutput/MessageInput.test.tsx +25 -0
  162. package/src/features/agent/chat/ModernAgentOutput/MessageInput.tsx +5 -3
  163. package/src/features/agent/chat/ModernAgentOutput/MessageItem.tsx +4 -1
  164. package/src/features/agent/chat/ModernAgentOutput/WorkstreamTabs.tsx +328 -42
  165. package/src/features/agent/chat/ModernAgentOutput/requestInputMessages.test.ts +80 -0
  166. package/src/features/agent/chat/ModernAgentOutput/requestInputMessages.ts +21 -1
  167. package/src/features/agent/chat/hooks/useFileProcessing.test.tsx +70 -0
  168. package/src/features/agent/chat/hooks/useFileProcessing.ts +37 -6
  169. package/src/features/magic-pdf/AnnotatedImageSlider.tsx +62 -66
  170. package/src/features/magic-pdf/DownloadPopover.tsx +2 -2
  171. package/src/features/magic-pdf/MagicPdfView.tsx +3 -3
  172. package/src/features/oauth/McpConnectionsButton.tsx +0 -2
  173. package/src/features/oauth/McpConnectionsDialog.tsx +0 -1
  174. package/src/features/oauth/RemoteMcpConnectionButton.tsx +0 -1
  175. package/src/features/pdf-viewer/PdfPageSlider.tsx +5 -5
  176. package/src/features/pdf-viewer/SimplePdfViewer.tsx +11 -12
  177. package/src/features/store/objects/DocumentQuickFilter.tsx +13 -15
  178. package/src/features/store/objects/DocumentSearchResults.tsx +3 -6
  179. package/src/features/store/objects/components/ContentDispositionButton.tsx +5 -1
  180. package/src/features/store/objects/components/ContentOverview.tsx +11 -11
  181. package/src/features/store/objects/components/DocumentIcon.tsx +1 -1
  182. package/src/features/store/objects/components/VectorSearchWidget.tsx +2 -2
  183. package/src/features/store/objects/layout/renderers.tsx +1 -2
  184. package/src/features/store/objects/selection/SelectionActions.tsx +1 -1
  185. package/src/features/store/objects/selection/actions/StartWorkflowComponent.tsx +6 -5
  186. package/src/features/store/types/ContentObjectTypesTable.tsx +12 -14
  187. package/src/features/store/types/SelectContentTypeModal.tsx +2 -2
  188. package/src/features/user/UserInfo.tsx +7 -7
  189. package/src/router/HistoryNavigator.ts +5 -0
  190. package/src/router/Nav.tsx +12 -3
  191. package/src/widgets/form/Form.tsx +1 -1
  192. package/src/widgets/json-view/JSONSwitcher.tsx +2 -2
  193. package/src/widgets/json-view/JSONView.tsx +7 -7
  194. package/src/widgets/monacoEditor/MonacoEditor.tsx +29 -0
  195. package/src/widgets/xml-viewer/hooks/useXMLViewer.test.ts +26 -0
  196. package/src/widgets/xml-viewer/hooks/useXMLViewer.ts +23 -18
@@ -104,6 +104,8 @@ const VARIANT_ICONS = {
104
104
  success: CheckCircle,
105
105
  };
106
106
 
107
+ const SCROLLABLE_PROMPT_CLASS = 'max-h-80 overflow-y-auto overscroll-contain pe-2';
108
+
107
109
  /**
108
110
  * AskUserWidget - A styled component for displaying agent prompts/questions to users
109
111
  *
@@ -197,10 +199,14 @@ export function AskUserWidget({
197
199
  if (answered) {
198
200
  return (
199
201
  <div className={cn('my-1.5 font-sans', className)}>
200
- {compactQuestion}
201
- {description && (
202
- <p className={cn('mt-1 text-xs leading-5 text-muted', descriptionClassName)}>{description}</p>
203
- )}
202
+ <div className={SCROLLABLE_PROMPT_CLASS}>
203
+ {compactQuestion}
204
+ {description && (
205
+ <p className={cn('mt-1 text-xs leading-5 text-muted', descriptionClassName)}>
206
+ {description}
207
+ </p>
208
+ )}
209
+ </div>
204
210
  </div>
205
211
  );
206
212
  }
@@ -214,12 +220,14 @@ export function AskUserWidget({
214
220
  <div className={cn('mt-1 flex-shrink-0 text-attention', iconClassName)}>{iconNode}</div>
215
221
  )}
216
222
  <div className="min-w-0 flex-1">
217
- {compactQuestion}
218
- {description && (
219
- <p className={cn('mt-1 text-xs leading-5 text-muted', descriptionClassName)}>
220
- {description}
221
- </p>
222
- )}
223
+ <div className={SCROLLABLE_PROMPT_CLASS}>
224
+ {compactQuestion}
225
+ {description && (
226
+ <p className={cn('mt-1 text-xs leading-5 text-muted', descriptionClassName)}>
227
+ {description}
228
+ </p>
229
+ )}
230
+ </div>
223
231
  </div>
224
232
  </div>
225
233
  </div>
@@ -355,19 +363,21 @@ export function AskUserWidget({
355
363
  {icon || <DefaultIcon className="size-5" />}
356
364
  </div>
357
365
  )}
358
- <div className="flex-1 min-w-0">
359
- <div
360
- className={`prose prose-sm dark:prose-invert max-w-none text-gray-900 dark:text-gray-100 ${questionClassName || ''}`}
361
- >
362
- <MarkdownRenderer>{question}</MarkdownRenderer>
363
- </div>
364
- {description && (
365
- <p
366
- className={`mt-1 text-sm text-gray-600 dark:text-gray-400 ${descriptionClassName || ''}`}
366
+ <div className="min-w-0 flex-1">
367
+ <div className={SCROLLABLE_PROMPT_CLASS}>
368
+ <div
369
+ className={`agent-ask-question prose prose-sm dark:prose-invert max-w-none text-gray-900 dark:text-gray-100 ${questionClassName || ''}`}
367
370
  >
368
- {description}
369
- </p>
370
- )}
371
+ <MarkdownRenderer>{question}</MarkdownRenderer>
372
+ </div>
373
+ {description && (
374
+ <p
375
+ className={`mt-1 text-sm text-gray-600 dark:text-gray-400 ${descriptionClassName || ''}`}
376
+ >
377
+ {description}
378
+ </p>
379
+ )}
380
+ </div>
371
381
  </div>
372
382
  </div>
373
383
  </div>
@@ -64,7 +64,6 @@ export function ImageLightboxProvider({ children }: ImageLightboxProviderProps)
64
64
  className="absolute top-2 end-2 text-white bg-black/50 rounded-full p-2 hover:bg-black/70 transition-colors"
65
65
  onClick={closeImage}
66
66
  title={t('agent.close')}
67
- aria-label={t('agent.close')}
68
67
  >
69
68
  <X className="size-6" />
70
69
  </Button>
@@ -1,5 +1,5 @@
1
1
  import { act, fireEvent, screen, waitFor } from '@testing-library/react';
2
- import { type AgentMessage, AgentMessageType } from '@vertesia/common';
2
+ import { type AgentMessage, AgentMessageType, type ConversationFile, FileProcessingStatus } from '@vertesia/common';
3
3
  import type React from 'react';
4
4
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
5
5
  import { renderWithProviders } from '../../../__tests__/test-utils.js';
@@ -245,6 +245,7 @@ describe('ModernAgentConversation send handling', () => {
245
245
  hasProcessingFiles: false,
246
246
  handleFileUpload: vi.fn(),
247
247
  removeProcessingFile: vi.fn(),
248
+ clearProcessingFiles: vi.fn(),
248
249
  });
249
250
  });
250
251
 
@@ -285,6 +286,53 @@ describe('ModernAgentConversation send handling', () => {
285
286
  expect(mocks.updateOptimisticMessageStatus).toHaveBeenCalledWith(expect.any(String), 'received');
286
287
  });
287
288
 
289
+ it('binds ready uploaded files into the sent message and clears them after send', async () => {
290
+ const clearProcessingFiles = vi.fn();
291
+ const readyFile: ConversationFile = {
292
+ id: 'file-1',
293
+ name: 'report.pdf',
294
+ content_type: 'application/pdf',
295
+ size: 10,
296
+ status: FileProcessingStatus.READY,
297
+ artifact_path: 'files/report.pdf',
298
+ reference: 'artifact:files/report.pdf',
299
+ started_at: 1_000,
300
+ };
301
+ mocks.useFileProcessing.mockReturnValue({
302
+ processingFiles: new Map([[readyFile.id, readyFile]]),
303
+ hasProcessingFiles: false,
304
+ handleFileUpload: vi.fn(),
305
+ removeProcessingFile: vi.fn(),
306
+ clearProcessingFiles,
307
+ });
308
+ mockStreamState({
309
+ messages: [createMessage(AgentMessageType.COMPLETE, 'done')],
310
+ agentRunStatus: 'COMPLETED',
311
+ });
312
+
313
+ renderConversation({ onRestart: vi.fn() });
314
+
315
+ fireEvent.click(screen.getByRole('button', { name: 'inline send' }));
316
+
317
+ await waitFor(() => {
318
+ expect(mocks.sendSignal).toHaveBeenCalledWith(
319
+ 'agent-run-1',
320
+ 'UserInput',
321
+ expect.objectContaining({
322
+ message: expect.stringContaining('Uploaded artifacts:'),
323
+ }),
324
+ );
325
+ });
326
+
327
+ const userInputCall = mocks.sendSignal.mock.calls.find((call) => call[1] === 'UserInput');
328
+ expect((userInputCall?.[2] as { message: string }).message).toContain(
329
+ '[report.pdf](artifact:files/report.pdf)',
330
+ );
331
+ await waitFor(() => {
332
+ expect(clearProcessingFiles).toHaveBeenCalledTimes(1);
333
+ });
334
+ });
335
+
288
336
  it('does not restart or send from a terminal run that cannot continue', () => {
289
337
  mockStreamState({
290
338
  messages: [createMessage(AgentMessageType.COMPLETE, 'done')],
@@ -425,6 +473,155 @@ describe('ModernAgentConversation send handling', () => {
425
473
  expect(screen.queryByText('Switch to full control?')).toBeNull();
426
474
  });
427
475
 
476
+ it('orders a follow-up user message after an in-flight approval mode change', async () => {
477
+ let resolveModeSignal: (() => void) | undefined;
478
+ mocks.retrieve.mockResolvedValue({
479
+ tool_approval_mode: 'ask',
480
+ interactive: true,
481
+ disabled_mcp_collections: undefined,
482
+ });
483
+ mocks.sendSignal.mockImplementation((_runId: string, signalName: string) => {
484
+ if (signalName === 'ToolApprovalModeChanged') {
485
+ return new Promise<void>((resolve) => {
486
+ resolveModeSignal = resolve;
487
+ });
488
+ }
489
+ return Promise.resolve({});
490
+ });
491
+ mockStreamState({
492
+ messages: [createMessage(AgentMessageType.REQUEST_INPUT, 'What should I do next?')],
493
+ isCompleted: false,
494
+ agentRunStatus: 'RUNNING',
495
+ });
496
+
497
+ renderConversation({ hideMessageInput: false, interactive: true, allowWorkflowControl: true });
498
+
499
+ const selector = await screen.findByRole('button', { name: 'Agent approval mode' });
500
+ fireEvent.pointerDown(selector, {
501
+ button: 0,
502
+ ctrlKey: false,
503
+ });
504
+ fireEvent.click(await screen.findByRole('menuitemradio', { name: /Full control/ }));
505
+
506
+ await waitFor(() => {
507
+ expect(mocks.sendSignal).toHaveBeenCalledWith('agent-run-1', 'ToolApprovalModeChanged', {
508
+ mode: 'full_control',
509
+ });
510
+ expect(resolveModeSignal).toBeDefined();
511
+ });
512
+
513
+ fireEvent.click(screen.getByRole('button', { name: 'composer send' }));
514
+
515
+ expect(mocks.sendSignal.mock.calls.some((call) => call[1] === 'UserInput')).toBe(false);
516
+
517
+ await act(async () => {
518
+ resolveModeSignal?.();
519
+ });
520
+
521
+ await waitFor(() => {
522
+ expect(mocks.sendSignal).toHaveBeenCalledWith(
523
+ 'agent-run-1',
524
+ 'UserInput',
525
+ expect.objectContaining({ message: 'follow up' }),
526
+ );
527
+ });
528
+
529
+ const modeCallIndex = mocks.sendSignal.mock.calls.findIndex((call) => call[1] === 'ToolApprovalModeChanged');
530
+ const userInputCallIndex = mocks.sendSignal.mock.calls.findIndex((call) => call[1] === 'UserInput');
531
+ expect(modeCallIndex).toBeGreaterThanOrEqual(0);
532
+ expect(userInputCallIndex).toBeGreaterThan(modeCallIndex);
533
+ });
534
+
535
+ it('keeps a local approval mode change when stale run metadata loads later', async () => {
536
+ let resolveRetrieve: (run: {
537
+ tool_approval_mode: 'ask';
538
+ interactive: true;
539
+ disabled_mcp_collections: undefined;
540
+ }) => void = () => {};
541
+ mocks.retrieve.mockReturnValue(
542
+ new Promise((resolve) => {
543
+ resolveRetrieve = resolve;
544
+ }),
545
+ );
546
+ mockStreamState({
547
+ messages: [createMessage(AgentMessageType.ANSWER, 'still running')],
548
+ isCompleted: false,
549
+ agentRunStatus: 'RUNNING',
550
+ });
551
+
552
+ renderConversation({
553
+ hideMessageInput: false,
554
+ interactive: true,
555
+ allowWorkflowControl: true,
556
+ initialToolApprovalMode: 'ask',
557
+ });
558
+
559
+ expect(await screen.findByText('Ask for approval')).not.toBeNull();
560
+ fireEvent.pointerDown(screen.getByRole('button', { name: 'Agent approval mode' }), {
561
+ button: 0,
562
+ ctrlKey: false,
563
+ });
564
+ fireEvent.click(await screen.findByRole('menuitemradio', { name: /Full control/ }));
565
+
566
+ await waitFor(() => {
567
+ expect(mocks.sendSignal).toHaveBeenCalledWith('agent-run-1', 'ToolApprovalModeChanged', {
568
+ mode: 'full_control',
569
+ });
570
+ });
571
+
572
+ resolveRetrieve({
573
+ tool_approval_mode: 'ask',
574
+ interactive: true,
575
+ disabled_mcp_collections: undefined,
576
+ });
577
+
578
+ await waitFor(() => {
579
+ expect(screen.getByText('Full control')).not.toBeNull();
580
+ });
581
+ });
582
+
583
+ it('reverts the local approval mode when the change signal fails', async () => {
584
+ let rejectSignal: (error: Error) => void = () => {};
585
+ mocks.retrieve.mockResolvedValue({
586
+ tool_approval_mode: 'ask',
587
+ interactive: true,
588
+ disabled_mcp_collections: undefined,
589
+ });
590
+ mocks.sendSignal.mockReturnValueOnce(
591
+ new Promise((_resolve, reject) => {
592
+ rejectSignal = reject;
593
+ }),
594
+ );
595
+ mockStreamState({
596
+ messages: [createMessage(AgentMessageType.ANSWER, 'still running')],
597
+ isCompleted: false,
598
+ agentRunStatus: 'RUNNING',
599
+ });
600
+
601
+ renderConversation({ hideMessageInput: false, interactive: true, allowWorkflowControl: true });
602
+
603
+ const selector = await screen.findByRole('button', { name: 'Agent approval mode' });
604
+ expect(selector.textContent).toContain('Ask for approval');
605
+ fireEvent.pointerDown(selector, {
606
+ button: 0,
607
+ ctrlKey: false,
608
+ });
609
+ fireEvent.click(await screen.findByRole('menuitemradio', { name: /Full control/ }));
610
+
611
+ await waitFor(() => {
612
+ expect(mocks.sendSignal).toHaveBeenCalledWith('agent-run-1', 'ToolApprovalModeChanged', {
613
+ mode: 'full_control',
614
+ });
615
+ expect(selector.textContent).toContain('Full control');
616
+ });
617
+
618
+ rejectSignal(new Error('Signal failed'));
619
+
620
+ await waitFor(() => {
621
+ expect(selector.textContent).toContain('Ask for approval');
622
+ });
623
+ });
624
+
428
625
  it('does not show a full-control fallback before active run mode metadata loads', () => {
429
626
  mocks.retrieve.mockReturnValue(new Promise(() => {}));
430
627
  mockStreamState({
@@ -1203,6 +1400,54 @@ describe('ModernAgentConversation send handling', () => {
1203
1400
  );
1204
1401
  });
1205
1402
 
1403
+ it('passes approval option metadata through the request overlay signal', async () => {
1404
+ mockStreamState({
1405
+ messages: [
1406
+ {
1407
+ ...createMessage(AgentMessageType.REQUEST_INPUT, 'Approve Write Artifact: quotes.md?'),
1408
+ details: {
1409
+ tool_approval: {
1410
+ tool_name: 'write_artifact',
1411
+ approval_key: 'write_artifact:name:quotes.md',
1412
+ },
1413
+ ux: {
1414
+ options: [
1415
+ { id: 'allow_once', label: 'Allow once' },
1416
+ { id: 'allow_for_run', label: 'Allow this action for this run' },
1417
+ { id: 'deny', label: 'Deny' },
1418
+ ],
1419
+ },
1420
+ },
1421
+ },
1422
+ ],
1423
+ isCompleted: false,
1424
+ agentRunStatus: 'RUNNING',
1425
+ });
1426
+
1427
+ renderConversation();
1428
+
1429
+ fireEvent.click(screen.getByRole('button', { name: 'Allow this action for this run' }));
1430
+
1431
+ await waitFor(() => {
1432
+ expect(mocks.sendSignal).toHaveBeenCalledTimes(1);
1433
+ });
1434
+ expect(mocks.sendSignal).toHaveBeenCalledWith(
1435
+ 'agent-run-1',
1436
+ 'UserInput',
1437
+ expect.objectContaining({
1438
+ message: 'allow_for_run',
1439
+ metadata: expect.objectContaining({
1440
+ tool_approval_response: {
1441
+ decision: 'allow_for_run',
1442
+ approval_key: 'write_artifact:name:quotes.md',
1443
+ },
1444
+ id: expect.any(String),
1445
+ _messageId: expect.any(String),
1446
+ }),
1447
+ }),
1448
+ );
1449
+ });
1450
+
1206
1451
  it('sends directly without restart while the run is active', async () => {
1207
1452
  mockStreamState({
1208
1453
  messages: [createMessage(AgentMessageType.ANSWER, 'still running')],
@@ -7,6 +7,7 @@ import {
7
7
  type CompletedWorkstreamEntry,
8
8
  type ConversationFile,
9
9
  type ConversationFileRef,
10
+ FileProcessingStatus,
10
11
  type McpConnectUxConfig,
11
12
  normalizeAgentToolApprovalMode,
12
13
  type Plan,
@@ -1105,7 +1106,6 @@ function StartWorkflowView({
1105
1106
  size="xs"
1106
1107
  variant="ghost"
1107
1108
  onClick={onClose}
1108
- aria-label={t('agent.close')}
1109
1109
  title={t('agent.close')}
1110
1110
  className="text-muted hover:text-foreground"
1111
1111
  >
@@ -1211,7 +1211,6 @@ function StartWorkflowView({
1211
1211
  'disabled:bg-mixer-muted/25 disabled:text-muted disabled:opacity-100',
1212
1212
  )}
1213
1213
  title={resolvedStartButtonText}
1214
- aria-label={resolvedStartButtonText}
1215
1214
  >
1216
1215
  {isSending ? <Spinner size="sm" /> : <ArrowUpIcon className="size-4" />}
1217
1216
  </Button>
@@ -1359,12 +1358,8 @@ function ModernAgentConversationInner({
1359
1358
  updateDocumentTitle,
1360
1359
  } = useDocumentPanel(messages);
1361
1360
 
1362
- const { processingFiles, hasProcessingFiles, handleFileUpload, removeProcessingFile } = useFileProcessing(
1363
- client,
1364
- agentRunId,
1365
- serverFileUpdates,
1366
- toast,
1367
- );
1361
+ const { processingFiles, hasProcessingFiles, handleFileUpload, removeProcessingFile, clearProcessingFiles } =
1362
+ useFileProcessing(client, agentRunId, serverFileUpdates, toast);
1368
1363
  const canUploadFiles = interactive && !hideFileUpload;
1369
1364
 
1370
1365
  const handleRemoveProcessingFile = useCallback(
@@ -1416,6 +1411,8 @@ function ModernAgentConversationInner({
1416
1411
  const workstreamFetchFailedRef = useRef(false);
1417
1412
  const dragCounterRef = useRef(0);
1418
1413
  const pendingPlaybackScrollRef = useRef(false);
1414
+ const toolApprovalModeChangeVersionRef = useRef(0);
1415
+ const pendingToolApprovalModeChangeRef = useRef<Promise<void> | null>(null);
1419
1416
 
1420
1417
  useEffect(() => {
1421
1418
  setToolApprovalMode(initialResolvedToolApprovalMode);
@@ -1426,6 +1423,8 @@ function ModernAgentConversationInner({
1426
1423
  isSendingRef.current = isSending;
1427
1424
  const hasProcessingFilesRef = useRef(hasProcessingFiles);
1428
1425
  hasProcessingFilesRef.current = hasProcessingFiles;
1426
+ const processingFilesRef = useRef(processingFiles);
1427
+ processingFilesRef.current = processingFiles;
1429
1428
 
1430
1429
  const lastMainMessage = useMemo(() => {
1431
1430
  const mainMessages = messages.filter((m) => (m.workstream_id || 'main') === 'main');
@@ -1909,6 +1908,13 @@ function ModernAgentConversationInner({
1909
1908
  // Handlers
1910
1909
  // ────────────────────────────────────────────
1911
1910
 
1911
+ const waitForPendingToolApprovalModeChange = useCallback(async () => {
1912
+ const pendingChange = pendingToolApprovalModeChangeRef.current;
1913
+ if (pendingChange) {
1914
+ await pendingChange;
1915
+ }
1916
+ }, []);
1917
+
1912
1918
  // Send a message to the agent
1913
1919
  const handleSendMessage = useCallback(
1914
1920
  (message: string, inputMetadata?: Record<string, unknown>) => {
@@ -1942,6 +1948,24 @@ function ModernAgentConversationInner({
1942
1948
  messageContent = [trimmed, '', 'Attachments:', ...lines].join('\n');
1943
1949
  }
1944
1950
 
1951
+ // Bind ready uploaded files to this message: embed them as an "Uploaded artifacts:"
1952
+ // markdown block so they render underneath the sent bubble (parsed by
1953
+ // parseUserMessageAttachments) and get cleared from the composer on send.
1954
+ const uploadedArtifactLines = Array.from(processingFilesRef.current.values())
1955
+ .filter((file) => file.status === FileProcessingStatus.READY)
1956
+ .map((file) => {
1957
+ const href = file.reference?.startsWith('artifact:')
1958
+ ? file.reference
1959
+ : file.artifact_path
1960
+ ? `artifact:${file.artifact_path}`
1961
+ : undefined;
1962
+ return href ? `[${file.name}](${href})` : undefined;
1963
+ })
1964
+ .filter((line): line is string => Boolean(line));
1965
+ if (uploadedArtifactLines.length > 0) {
1966
+ messageContent = [messageContent, '', 'Uploaded artifacts:', ...uploadedArtifactLines].join('\n');
1967
+ }
1968
+
1945
1969
  const messageId = `msg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
1946
1970
 
1947
1971
  const optimisticMessage: AgentMessage = {
@@ -1973,6 +1997,7 @@ function ModernAgentConversationInner({
1973
1997
  const markReceived = () => {
1974
1998
  updateOptimisticMessageStatus(messageId, 'received');
1975
1999
  onAttachmentsSent?.();
2000
+ clearProcessingFiles();
1976
2001
  };
1977
2002
 
1978
2003
  // When the workflow has already completed, restart it first so it resumes
@@ -1980,12 +2005,15 @@ function ModernAgentConversationInner({
1980
2005
  // buffers the signal until the new run is ready to receive it. We reconnect
1981
2006
  // the stream in place (rather than remounting via onRestart) so the existing
1982
2007
  // timeline is preserved and the new exchange appends seamlessly at the bottom.
1983
- const deliver = isWorkflowTerminalRef.current
1984
- ? client.agents.restart(agentRunId).then(() => {
1985
- reconnectStream();
1986
- return sendUserInput().then(markReceived);
1987
- })
1988
- : sendUserInput().then(markReceived);
2008
+ const deliver = (async () => {
2009
+ await waitForPendingToolApprovalModeChange();
2010
+ if (isWorkflowTerminalRef.current) {
2011
+ await client.agents.restart(agentRunId);
2012
+ reconnectStream();
2013
+ }
2014
+ await sendUserInput();
2015
+ markReceived();
2016
+ })();
1989
2017
 
1990
2018
  deliver
1991
2019
  .catch((err) => {
@@ -2008,10 +2036,12 @@ function ModernAgentConversationInner({
2008
2036
  getAttachedDocs,
2009
2037
  getMessageContext,
2010
2038
  onAttachmentsSent,
2039
+ clearProcessingFiles,
2011
2040
  reconnectStream,
2012
2041
  addOptimisticMessage,
2013
2042
  updateOptimisticMessageStatus,
2014
2043
  t,
2044
+ waitForPendingToolApprovalModeChange,
2015
2045
  ],
2016
2046
  );
2017
2047
 
@@ -2040,12 +2070,16 @@ function ModernAgentConversationInner({
2040
2070
  const [mcpDisabled, setMcpDisabled] = useState<string[] | undefined>(undefined);
2041
2071
  useEffect(() => {
2042
2072
  let cancelled = false;
2073
+ const requestVersion = toolApprovalModeChangeVersionRef.current;
2043
2074
  client.agents
2044
2075
  .retrieve(agentRunId)
2045
2076
  .then((run) => {
2046
2077
  if (!cancelled) {
2047
2078
  setMcpDisabled(run.disabled_mcp_collections);
2048
- if (run.tool_approval_mode !== undefined || run.interactive !== undefined) {
2079
+ if (
2080
+ toolApprovalModeChangeVersionRef.current === requestVersion &&
2081
+ (run.tool_approval_mode !== undefined || run.interactive !== undefined)
2082
+ ) {
2049
2083
  setToolApprovalMode(normalizeAgentToolApprovalMode(run.tool_approval_mode, run.interactive));
2050
2084
  }
2051
2085
  }
@@ -2081,10 +2115,14 @@ function ModernAgentConversationInner({
2081
2115
  const handleToolApprovalModeChange = useCallback(
2082
2116
  (mode: AgentToolApprovalMode) => {
2083
2117
  const nextMode = normalizeAgentToolApprovalMode(mode, interactive);
2118
+ const previousMode = toolApprovalMode;
2119
+ toolApprovalModeChangeVersionRef.current += 1;
2084
2120
  setToolApprovalMode(nextMode);
2085
- client.agents
2121
+ const signalPromise = client.agents
2086
2122
  .sendSignal(agentRunId, 'ToolApprovalModeChanged', { mode: nextMode })
2123
+ .then(() => undefined)
2087
2124
  .catch((err: unknown) => {
2125
+ setToolApprovalMode((currentMode) => (currentMode === nextMode ? previousMode : currentMode));
2088
2126
  toast({
2089
2127
  status: 'error',
2090
2128
  title: t('agent.approvalMode.changeFailed'),
@@ -2092,8 +2130,14 @@ function ModernAgentConversationInner({
2092
2130
  duration: 3000,
2093
2131
  });
2094
2132
  });
2133
+ pendingToolApprovalModeChangeRef.current = signalPromise;
2134
+ void signalPromise.finally(() => {
2135
+ if (pendingToolApprovalModeChangeRef.current === signalPromise) {
2136
+ pendingToolApprovalModeChangeRef.current = null;
2137
+ }
2138
+ });
2095
2139
  },
2096
- [agentRunId, client, interactive, t, toast],
2140
+ [agentRunId, client, interactive, t, toast, toolApprovalMode],
2097
2141
  );
2098
2142
 
2099
2143
  // Drag and drop handlers for full-panel file upload
@@ -2418,6 +2462,7 @@ function ModernAgentConversationInner({
2418
2462
  ) : (
2419
2463
  <AllMessagesMixed
2420
2464
  messages={displayedMessages}
2465
+ workstreamSourceMessages={messages}
2421
2466
  bottomRef={bottomRef as React.RefObject<HTMLDivElement>}
2422
2467
  isCompleted={displayedIsCompleted}
2423
2468
  plan={getActivePlan.plan}
@@ -399,7 +399,12 @@ describe('AllMessagesMixed summary view', () => {
399
399
 
400
400
  fireEvent.click(screen.getByRole('button', { name: /Allow once/ }));
401
401
 
402
- expect(onSendMessage).toHaveBeenCalledWith('allow_once');
402
+ expect(onSendMessage).toHaveBeenCalledWith('allow_once', {
403
+ tool_approval_response: {
404
+ decision: 'allow_once',
405
+ approval_key: 'create_document:name:News Headlines Today',
406
+ },
407
+ });
403
408
  });
404
409
 
405
410
  it('keeps an approved tool approval continuation in the same active work row', () => {