@vertesia/ui 1.5.0-dev.20260722.120446Z → 1.5.0-dev.20260725.083715Z

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 (307) hide show
  1. package/lib/core/components/shadcn/filters/filterBar.d.ts.map +1 -1
  2. package/lib/core/components/shadcn/filters/filterBar.js +1 -1
  3. package/lib/core/components/shadcn/filters/filterBar.js.map +1 -1
  4. package/lib/core/components/shadcn/modal/dialog.d.ts +1 -1
  5. package/lib/core/components/shadcn/modal/dialog.d.ts.map +1 -1
  6. package/lib/core/components/shadcn/modal/dialog.js +2 -0
  7. package/lib/core/components/shadcn/modal/dialog.js.map +1 -1
  8. package/lib/core/components/shadcn/tabs.d.ts +12 -1
  9. package/lib/core/components/shadcn/tabs.d.ts.map +1 -1
  10. package/lib/core/components/shadcn/tabs.js +12 -4
  11. package/lib/core/components/shadcn/tabs.js.map +1 -1
  12. package/lib/features/agent/chat/AgentRightPanel.d.ts +5 -1
  13. package/lib/features/agent/chat/AgentRightPanel.d.ts.map +1 -1
  14. package/lib/features/agent/chat/AgentRightPanel.js +2 -2
  15. package/lib/features/agent/chat/AgentRightPanel.js.map +1 -1
  16. package/lib/features/agent/chat/ArtifactsTab.d.ts +5 -1
  17. package/lib/features/agent/chat/ArtifactsTab.d.ts.map +1 -1
  18. package/lib/features/agent/chat/ArtifactsTab.js +48 -11
  19. package/lib/features/agent/chat/ArtifactsTab.js.map +1 -1
  20. package/lib/features/agent/chat/DocumentEditingActionCard.d.ts +6 -0
  21. package/lib/features/agent/chat/DocumentEditingActionCard.d.ts.map +1 -0
  22. package/lib/features/agent/chat/DocumentEditingActionCard.js +129 -0
  23. package/lib/features/agent/chat/DocumentEditingActionCard.js.map +1 -0
  24. package/lib/features/agent/chat/ModernAgentConversation.d.ts +19 -1
  25. package/lib/features/agent/chat/ModernAgentConversation.d.ts.map +1 -1
  26. package/lib/features/agent/chat/ModernAgentConversation.js +125 -27
  27. package/lib/features/agent/chat/ModernAgentConversation.js.map +1 -1
  28. package/lib/features/agent/chat/ModernAgentOutput/AgentMarkdownAnchor.d.ts +45 -0
  29. package/lib/features/agent/chat/ModernAgentOutput/AgentMarkdownAnchor.d.ts.map +1 -0
  30. package/lib/features/agent/chat/ModernAgentOutput/AgentMarkdownAnchor.js +40 -0
  31. package/lib/features/agent/chat/ModernAgentOutput/AgentMarkdownAnchor.js.map +1 -0
  32. package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.d.ts +3 -1
  33. package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.d.ts.map +1 -1
  34. package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.js +94 -49
  35. package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.js.map +1 -1
  36. package/lib/features/agent/chat/ModernAgentOutput/AttachmentPreview.d.ts +2 -1
  37. package/lib/features/agent/chat/ModernAgentOutput/AttachmentPreview.d.ts.map +1 -1
  38. package/lib/features/agent/chat/ModernAgentOutput/AttachmentPreview.js +9 -5
  39. package/lib/features/agent/chat/ModernAgentOutput/AttachmentPreview.js.map +1 -1
  40. package/lib/features/agent/chat/ModernAgentOutput/CollapsibleAgentMarkdown.d.ts +25 -0
  41. package/lib/features/agent/chat/ModernAgentOutput/CollapsibleAgentMarkdown.d.ts.map +1 -0
  42. package/lib/features/agent/chat/ModernAgentOutput/CollapsibleAgentMarkdown.js +33 -0
  43. package/lib/features/agent/chat/ModernAgentOutput/CollapsibleAgentMarkdown.js.map +1 -0
  44. package/lib/features/agent/chat/ModernAgentOutput/MessageItem.d.ts +3 -1
  45. package/lib/features/agent/chat/ModernAgentOutput/MessageItem.d.ts.map +1 -1
  46. package/lib/features/agent/chat/ModernAgentOutput/MessageItem.js +22 -24
  47. package/lib/features/agent/chat/ModernAgentOutput/MessageItem.js.map +1 -1
  48. package/lib/features/agent/chat/ModernAgentOutput/ResourceChangeSummary.d.ts +14 -0
  49. package/lib/features/agent/chat/ModernAgentOutput/ResourceChangeSummary.d.ts.map +1 -0
  50. package/lib/features/agent/chat/ModernAgentOutput/ResourceChangeSummary.js +21 -0
  51. package/lib/features/agent/chat/ModernAgentOutput/ResourceChangeSummary.js.map +1 -0
  52. package/lib/features/agent/chat/ModernAgentOutput/ResourceChip.d.ts +30 -0
  53. package/lib/features/agent/chat/ModernAgentOutput/ResourceChip.d.ts.map +1 -0
  54. package/lib/features/agent/chat/ModernAgentOutput/ResourceChip.js +53 -0
  55. package/lib/features/agent/chat/ModernAgentOutput/ResourceChip.js.map +1 -0
  56. package/lib/features/agent/chat/ModernAgentOutput/ToolCallGroup.d.ts.map +1 -1
  57. package/lib/features/agent/chat/ModernAgentOutput/ToolCallGroup.js +21 -2
  58. package/lib/features/agent/chat/ModernAgentOutput/ToolCallGroup.js.map +1 -1
  59. package/lib/features/agent/chat/ModernAgentOutput/resourceLinks.d.ts +6 -0
  60. package/lib/features/agent/chat/ModernAgentOutput/resourceLinks.d.ts.map +1 -0
  61. package/lib/features/agent/chat/ModernAgentOutput/resourceLinks.js +29 -0
  62. package/lib/features/agent/chat/ModernAgentOutput/resourceLinks.js.map +1 -0
  63. package/lib/features/agent/chat/ModernAgentOutput/resourceSummary.d.ts +33 -0
  64. package/lib/features/agent/chat/ModernAgentOutput/resourceSummary.d.ts.map +1 -0
  65. package/lib/features/agent/chat/ModernAgentOutput/resourceSummary.js +129 -0
  66. package/lib/features/agent/chat/ModernAgentOutput/resourceSummary.js.map +1 -0
  67. package/lib/features/agent/chat/ModernAgentOutput/utils.d.ts +7 -0
  68. package/lib/features/agent/chat/ModernAgentOutput/utils.d.ts.map +1 -1
  69. package/lib/features/agent/chat/ModernAgentOutput/utils.js +11 -1
  70. package/lib/features/agent/chat/ModernAgentOutput/utils.js.map +1 -1
  71. package/lib/features/agent/chat/hooks/useAgentStream.d.ts +1 -1
  72. package/lib/features/agent/chat/hooks/useAgentStream.d.ts.map +1 -1
  73. package/lib/features/agent/chat/hooks/useAgentStream.js +24 -9
  74. package/lib/features/agent/chat/hooks/useAgentStream.js.map +1 -1
  75. package/lib/features/agent/chat/index.d.ts +2 -1
  76. package/lib/features/agent/chat/index.d.ts.map +1 -1
  77. package/lib/features/agent/chat/index.js +3 -0
  78. package/lib/features/agent/chat/index.js.map +1 -1
  79. package/lib/features/store/objects/DocumentSearchResults.js +2 -2
  80. package/lib/features/store/objects/DocumentSearchResults.js.map +1 -1
  81. package/lib/features/store/objects/components/ContentOverview.d.ts +2 -1
  82. package/lib/features/store/objects/components/ContentOverview.d.ts.map +1 -1
  83. package/lib/features/store/objects/components/ContentOverview.js +86 -15
  84. package/lib/features/store/objects/components/ContentOverview.js.map +1 -1
  85. package/lib/features/store/objects/components/DocumentEditingConfigurationSelector.d.ts +16 -0
  86. package/lib/features/store/objects/components/DocumentEditingConfigurationSelector.d.ts.map +1 -0
  87. package/lib/features/store/objects/components/DocumentEditingConfigurationSelector.js +116 -0
  88. package/lib/features/store/objects/components/DocumentEditingConfigurationSelector.js.map +1 -0
  89. package/lib/features/store/objects/components/DocumentEditingLockStatus.d.ts +7 -0
  90. package/lib/features/store/objects/components/DocumentEditingLockStatus.d.ts.map +1 -0
  91. package/lib/features/store/objects/components/DocumentEditingLockStatus.js +13 -0
  92. package/lib/features/store/objects/components/DocumentEditingLockStatus.js.map +1 -0
  93. package/lib/features/store/objects/components/DocumentEditingPanel.d.ts +48 -0
  94. package/lib/features/store/objects/components/DocumentEditingPanel.d.ts.map +1 -0
  95. package/lib/features/store/objects/components/DocumentEditingPanel.js +720 -0
  96. package/lib/features/store/objects/components/DocumentEditingPanel.js.map +1 -0
  97. package/lib/features/store/objects/components/TextEditorPanel.d.ts.map +1 -1
  98. package/lib/features/store/objects/components/TextEditorPanel.js +22 -9
  99. package/lib/features/store/objects/components/TextEditorPanel.js.map +1 -1
  100. package/lib/features/store/objects/components/documentArtifactRefs.d.ts +26 -0
  101. package/lib/features/store/objects/components/documentArtifactRefs.d.ts.map +1 -0
  102. package/lib/features/store/objects/components/documentArtifactRefs.js +52 -0
  103. package/lib/features/store/objects/components/documentArtifactRefs.js.map +1 -0
  104. package/lib/features/store/objects/components/documentEditingAgentConfig.d.ts +4 -0
  105. package/lib/features/store/objects/components/documentEditingAgentConfig.d.ts.map +1 -0
  106. package/lib/features/store/objects/components/documentEditingAgentConfig.js +40 -0
  107. package/lib/features/store/objects/components/documentEditingAgentConfig.js.map +1 -0
  108. package/lib/features/store/objects/components/documentEditingPrompts.d.ts +3 -0
  109. package/lib/features/store/objects/components/documentEditingPrompts.d.ts.map +1 -0
  110. package/lib/features/store/objects/components/documentEditingPrompts.js +13 -0
  111. package/lib/features/store/objects/components/documentEditingPrompts.js.map +1 -0
  112. package/lib/features/store/objects/components/documentEditingRun.d.ts +27 -0
  113. package/lib/features/store/objects/components/documentEditingRun.d.ts.map +1 -0
  114. package/lib/features/store/objects/components/documentEditingRun.js +126 -0
  115. package/lib/features/store/objects/components/documentEditingRun.js.map +1 -0
  116. package/lib/features/store/objects/components/documentEditingTarget.d.ts +20 -0
  117. package/lib/features/store/objects/components/documentEditingTarget.d.ts.map +1 -0
  118. package/lib/features/store/objects/components/documentEditingTarget.js +40 -0
  119. package/lib/features/store/objects/components/documentEditingTarget.js.map +1 -0
  120. package/lib/features/store/objects/components/index.d.ts +1 -0
  121. package/lib/features/store/objects/components/index.d.ts.map +1 -1
  122. package/lib/features/store/objects/components/index.js +1 -0
  123. package/lib/features/store/objects/components/index.js.map +1 -1
  124. package/lib/features/store/objects/components/useContentPanelHooks.d.ts +5 -1
  125. package/lib/features/store/objects/components/useContentPanelHooks.d.ts.map +1 -1
  126. package/lib/features/store/objects/components/useContentPanelHooks.js +53 -29
  127. package/lib/features/store/objects/components/useContentPanelHooks.js.map +1 -1
  128. package/lib/features/store/objects/components/useDownloadFile.d.ts +2 -1
  129. package/lib/features/store/objects/components/useDownloadFile.d.ts.map +1 -1
  130. package/lib/features/store/objects/components/useDownloadFile.js +3 -0
  131. package/lib/features/store/objects/components/useDownloadFile.js.map +1 -1
  132. package/lib/features/store/objects/selection/actions/StartWorkflowButton.js +1 -1
  133. package/lib/features/store/objects/selection/actions/StartWorkflowButton.js.map +1 -1
  134. package/lib/i18n/locales/ar.json +118 -1
  135. package/lib/i18n/locales/de.json +118 -1
  136. package/lib/i18n/locales/en.json +118 -0
  137. package/lib/i18n/locales/es.json +118 -1
  138. package/lib/i18n/locales/fr.json +118 -1
  139. package/lib/i18n/locales/it.json +118 -1
  140. package/lib/i18n/locales/ja.json +118 -1
  141. package/lib/i18n/locales/ko.json +118 -1
  142. package/lib/i18n/locales/pt.json +118 -1
  143. package/lib/i18n/locales/ru.json +118 -1
  144. package/lib/i18n/locales/tr.json +118 -1
  145. package/lib/i18n/locales/zh-TW.json +118 -1
  146. package/lib/i18n/locales/zh.json +118 -1
  147. package/lib/rich-text/EditorToolbar.d.ts +37 -0
  148. package/lib/rich-text/EditorToolbar.d.ts.map +1 -0
  149. package/lib/rich-text/EditorToolbar.js +215 -0
  150. package/lib/rich-text/EditorToolbar.js.map +1 -0
  151. package/lib/rich-text/VertesiaRichTextEditor.d.ts +26 -0
  152. package/lib/rich-text/VertesiaRichTextEditor.d.ts.map +1 -0
  153. package/lib/rich-text/VertesiaRichTextEditor.js +67 -0
  154. package/lib/rich-text/VertesiaRichTextEditor.js.map +1 -0
  155. package/lib/rich-text/index.d.ts +2 -0
  156. package/lib/rich-text/index.d.ts.map +1 -0
  157. package/lib/rich-text/index.js +2 -0
  158. package/lib/rich-text/index.js.map +1 -0
  159. package/lib/vertesia-ui-core.js +2 -2
  160. package/lib/vertesia-ui-core.js.map +1 -1
  161. package/lib/vertesia-ui-features.js +35 -30
  162. package/lib/vertesia-ui-features.js.map +1 -1
  163. package/lib/vertesia-ui-i18n.js +14 -14
  164. package/lib/vertesia-ui-i18n.js.map +1 -1
  165. package/lib/vertesia-ui-rich-text.js +5 -0
  166. package/lib/vertesia-ui-rich-text.js.map +1 -0
  167. package/lib/vertesia-ui-widgets.js +25 -7
  168. package/lib/vertesia-ui-widgets.js.map +1 -1
  169. package/lib/widgets/markdown/AgentResourceResolver.d.ts +31 -0
  170. package/lib/widgets/markdown/AgentResourceResolver.d.ts.map +1 -0
  171. package/lib/widgets/markdown/AgentResourceResolver.js +39 -0
  172. package/lib/widgets/markdown/AgentResourceResolver.js.map +1 -0
  173. package/lib/widgets/markdown/ArtifactEditingSurface.d.ts +63 -0
  174. package/lib/widgets/markdown/ArtifactEditingSurface.d.ts.map +1 -0
  175. package/lib/widgets/markdown/ArtifactEditingSurface.js +582 -0
  176. package/lib/widgets/markdown/ArtifactEditingSurface.js.map +1 -0
  177. package/lib/widgets/markdown/CollaborativeMarkdownRenderer.d.ts +69 -0
  178. package/lib/widgets/markdown/CollaborativeMarkdownRenderer.d.ts.map +1 -0
  179. package/lib/widgets/markdown/CollaborativeMarkdownRenderer.js +374 -0
  180. package/lib/widgets/markdown/CollaborativeMarkdownRenderer.js.map +1 -0
  181. package/lib/widgets/markdown/MarkdownLink.d.ts +3 -1
  182. package/lib/widgets/markdown/MarkdownLink.d.ts.map +1 -1
  183. package/lib/widgets/markdown/MarkdownLink.js +41 -14
  184. package/lib/widgets/markdown/MarkdownLink.js.map +1 -1
  185. package/lib/widgets/markdown/MarkdownRenderer.d.ts +8 -1
  186. package/lib/widgets/markdown/MarkdownRenderer.d.ts.map +1 -1
  187. package/lib/widgets/markdown/MarkdownRenderer.js +20 -4
  188. package/lib/widgets/markdown/MarkdownRenderer.js.map +1 -1
  189. package/lib/widgets/markdown/index.d.ts +5 -1
  190. package/lib/widgets/markdown/index.d.ts.map +1 -1
  191. package/lib/widgets/markdown/index.js +5 -1
  192. package/lib/widgets/markdown/index.js.map +1 -1
  193. package/lib/widgets/markdown/normalizeCustomSchemeLinks.d.ts.map +1 -1
  194. package/lib/widgets/markdown/normalizeCustomSchemeLinks.js +14 -2
  195. package/lib/widgets/markdown/normalizeCustomSchemeLinks.js.map +1 -1
  196. package/lib/widgets/markdown/textDiff.d.ts +52 -0
  197. package/lib/widgets/markdown/textDiff.d.ts.map +1 -0
  198. package/lib/widgets/markdown/textDiff.js +362 -0
  199. package/lib/widgets/markdown/textDiff.js.map +1 -0
  200. package/lib/widgets/markdown/useArtifactContent.d.ts +5 -3
  201. package/lib/widgets/markdown/useArtifactContent.d.ts.map +1 -1
  202. package/lib/widgets/markdown/useArtifactContent.js +6 -3
  203. package/lib/widgets/markdown/useArtifactContent.js.map +1 -1
  204. package/lib/widgets/markdown/useResolvedUrl.d.ts +1 -5
  205. package/lib/widgets/markdown/useResolvedUrl.d.ts.map +1 -1
  206. package/lib/widgets/markdown/useResolvedUrl.js +19 -31
  207. package/lib/widgets/markdown/useResolvedUrl.js.map +1 -1
  208. package/lib/widgets/xml-viewer/hooks/useXMLViewer.d.ts +9 -0
  209. package/lib/widgets/xml-viewer/hooks/useXMLViewer.d.ts.map +1 -1
  210. package/lib/widgets/xml-viewer/hooks/useXMLViewer.js +20 -17
  211. package/lib/widgets/xml-viewer/hooks/useXMLViewer.js.map +1 -1
  212. package/package.json +13 -9
  213. package/src/__tests__/a11y.test.tsx +4 -1
  214. package/src/__tests__/axe-helper.ts +17 -7
  215. package/src/__tests__/setup.ts +25 -0
  216. package/src/core/components/shadcn/filters/filterBar.tsx +1 -0
  217. package/src/core/components/shadcn/modal/dialog.tsx +3 -1
  218. package/src/core/components/shadcn/tabs.test.tsx +65 -0
  219. package/src/core/components/shadcn/tabs.tsx +25 -3
  220. package/src/features/agent/chat/AgentRightPanel.tsx +16 -1
  221. package/src/features/agent/chat/ArtifactsTab.test.tsx +55 -0
  222. package/src/features/agent/chat/ArtifactsTab.tsx +203 -52
  223. package/src/features/agent/chat/DocumentEditingActionCard.test.tsx +105 -0
  224. package/src/features/agent/chat/DocumentEditingActionCard.tsx +210 -0
  225. package/src/features/agent/chat/ModernAgentConversation.test.tsx +237 -40
  226. package/src/features/agent/chat/ModernAgentConversation.tsx +247 -75
  227. package/src/features/agent/chat/ModernAgentOutput/AgentMarkdownAnchor.test.tsx +79 -0
  228. package/src/features/agent/chat/ModernAgentOutput/AgentMarkdownAnchor.tsx +94 -0
  229. package/src/features/agent/chat/ModernAgentOutput/AllMessagesMixed.test.tsx +257 -34
  230. package/src/features/agent/chat/ModernAgentOutput/AllMessagesMixed.tsx +217 -105
  231. package/src/features/agent/chat/ModernAgentOutput/AttachmentPreview.tsx +17 -0
  232. package/src/features/agent/chat/ModernAgentOutput/CollapsibleAgentMarkdown.test.tsx +44 -0
  233. package/src/features/agent/chat/ModernAgentOutput/CollapsibleAgentMarkdown.tsx +88 -0
  234. package/src/features/agent/chat/ModernAgentOutput/MessageItem.tsx +30 -46
  235. package/src/features/agent/chat/ModernAgentOutput/ResourceChangeSummary.tsx +40 -0
  236. package/src/features/agent/chat/ModernAgentOutput/ResourceChip.test.tsx +148 -0
  237. package/src/features/agent/chat/ModernAgentOutput/ResourceChip.tsx +141 -0
  238. package/src/features/agent/chat/ModernAgentOutput/ToolCallGroup.tsx +33 -1
  239. package/src/features/agent/chat/ModernAgentOutput/resourceLinks.tsx +43 -0
  240. package/src/features/agent/chat/ModernAgentOutput/resourceSummary.test.ts +229 -0
  241. package/src/features/agent/chat/ModernAgentOutput/resourceSummary.ts +152 -0
  242. package/src/features/agent/chat/ModernAgentOutput/utils.test.ts +17 -0
  243. package/src/features/agent/chat/ModernAgentOutput/utils.ts +14 -1
  244. package/src/features/agent/chat/hooks/useAgentStream.test.tsx +188 -0
  245. package/src/features/agent/chat/hooks/useAgentStream.ts +28 -8
  246. package/src/features/agent/chat/index.ts +11 -0
  247. package/src/features/store/objects/DocumentSearchResults.tsx +2 -2
  248. package/src/features/store/objects/components/ContentOverview.tsx +216 -65
  249. package/src/features/store/objects/components/DocumentEditingConfigurationSelector.test.ts +34 -0
  250. package/src/features/store/objects/components/DocumentEditingConfigurationSelector.tsx +211 -0
  251. package/src/features/store/objects/components/DocumentEditingLockStatus.test.tsx +32 -0
  252. package/src/features/store/objects/components/DocumentEditingLockStatus.tsx +45 -0
  253. package/src/features/store/objects/components/DocumentEditingPanel.test.tsx +336 -0
  254. package/src/features/store/objects/components/DocumentEditingPanel.tsx +1236 -0
  255. package/src/features/store/objects/components/TextEditorPanel.test.tsx +48 -0
  256. package/src/features/store/objects/components/TextEditorPanel.tsx +41 -20
  257. package/src/features/store/objects/components/documentArtifactRefs.test.ts +65 -0
  258. package/src/features/store/objects/components/documentArtifactRefs.ts +72 -0
  259. package/src/features/store/objects/components/documentEditingAgentConfig.test.ts +28 -0
  260. package/src/features/store/objects/components/documentEditingAgentConfig.ts +41 -0
  261. package/src/features/store/objects/components/documentEditingPrompts.test.ts +26 -0
  262. package/src/features/store/objects/components/documentEditingPrompts.ts +12 -0
  263. package/src/features/store/objects/components/documentEditingRun.test.ts +176 -0
  264. package/src/features/store/objects/components/documentEditingRun.ts +184 -0
  265. package/src/features/store/objects/components/documentEditingTarget.test.ts +96 -0
  266. package/src/features/store/objects/components/documentEditingTarget.ts +63 -0
  267. package/src/features/store/objects/components/index.ts +1 -0
  268. package/src/features/store/objects/components/useContentPanelHooks.ts +52 -27
  269. package/src/features/store/objects/components/useDownloadFile.test.ts +20 -0
  270. package/src/features/store/objects/components/useDownloadFile.ts +5 -1
  271. package/src/features/store/objects/selection/actions/StartWorkflowButton.tsx +2 -2
  272. package/src/i18n/locales/ar.json +118 -1
  273. package/src/i18n/locales/de.json +118 -1
  274. package/src/i18n/locales/en.json +118 -0
  275. package/src/i18n/locales/es.json +118 -1
  276. package/src/i18n/locales/fr.json +118 -1
  277. package/src/i18n/locales/it.json +118 -1
  278. package/src/i18n/locales/ja.json +118 -1
  279. package/src/i18n/locales/ko.json +118 -1
  280. package/src/i18n/locales/pt.json +118 -1
  281. package/src/i18n/locales/ru.json +118 -1
  282. package/src/i18n/locales/tr.json +118 -1
  283. package/src/i18n/locales/zh-TW.json +118 -1
  284. package/src/i18n/locales/zh.json +118 -1
  285. package/src/rich-text/EditorToolbar.tsx +708 -0
  286. package/src/rich-text/VertesiaRichTextEditor.test.tsx +211 -0
  287. package/src/rich-text/VertesiaRichTextEditor.tsx +241 -0
  288. package/src/rich-text/index.ts +9 -0
  289. package/src/session/UserSession.test.ts +11 -11
  290. package/src/widgets/markdown/AgentResourceResolver.test.ts +24 -0
  291. package/src/widgets/markdown/AgentResourceResolver.tsx +70 -0
  292. package/src/widgets/markdown/ArtifactEditingSurface.test.tsx +537 -0
  293. package/src/widgets/markdown/ArtifactEditingSurface.tsx +878 -0
  294. package/src/widgets/markdown/CollaborativeMarkdownRenderer.test.tsx +484 -0
  295. package/src/widgets/markdown/CollaborativeMarkdownRenderer.tsx +825 -0
  296. package/src/widgets/markdown/MarkdownLink.test.tsx +122 -0
  297. package/src/widgets/markdown/MarkdownLink.tsx +102 -17
  298. package/src/widgets/markdown/MarkdownRenderer.tsx +30 -3
  299. package/src/widgets/markdown/index.ts +39 -1
  300. package/src/widgets/markdown/normalizeCustomSchemeLinks.ts +15 -2
  301. package/src/widgets/markdown/textDiff.test.ts +156 -0
  302. package/src/widgets/markdown/textDiff.ts +399 -0
  303. package/src/widgets/markdown/useArtifactContent.ts +9 -3
  304. package/src/widgets/markdown/useResolvedUrl.test.ts +29 -40
  305. package/src/widgets/markdown/useResolvedUrl.ts +33 -35
  306. package/src/widgets/xml-viewer/hooks/useXMLViewer.test.ts +26 -0
  307. package/src/widgets/xml-viewer/hooks/useXMLViewer.ts +23 -18
@@ -0,0 +1,878 @@
1
+ import { Button, Center, cn, errorMessage, MessageBox, Spinner, useToast, VTooltip } from '@vertesia/ui/core';
2
+ import { useUITranslation } from '@vertesia/ui/i18n';
3
+ import { useUserSession } from '@vertesia/ui/session';
4
+ import { Check, GitCommitHorizontal } from 'lucide-react';
5
+ import { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react';
6
+ import {
7
+ applyMarkdownEditingChange,
8
+ CollaborativeMarkdownRenderer,
9
+ type MarkdownEditingAction,
10
+ } from './CollaborativeMarkdownRenderer.js';
11
+ import { getTextLineChangeRegions, rebaseTextChanges } from './textDiff.js';
12
+
13
+ const HYDRATION_RETRY_DELAY_MS = 500;
14
+ const HYDRATION_RETRY_LIMIT = 60;
15
+ const DOCUMENT_SAVE_DEBOUNCE_MS = 400;
16
+
17
+ const MarkdownDocumentEditor = lazy(() =>
18
+ import('@vertesia/ui/rich-text').then((module) => ({ default: module.VertesiaMarkdownDocumentEditor })),
19
+ );
20
+
21
+ export function isArtifactRefreshEvent(details: Record<string, unknown> | undefined, path: string): boolean {
22
+ if (details?.path !== path) return false;
23
+ if (details.event_class === 'artifact_updated') return true;
24
+
25
+ // Compatibility for runs handled by workers that predate the dedicated
26
+ // artifact_updated event. Their path-bearing completion is still reliable.
27
+ return (
28
+ details.event_class === 'activity' &&
29
+ details.tool_status === 'completed' &&
30
+ (details.tool === 'edit_artifact' || details.tool === 'write_artifact')
31
+ );
32
+ }
33
+
34
+ export function applyArtifactRefreshChanges(
35
+ content: string,
36
+ details: Record<string, unknown> | undefined,
37
+ ): string | undefined {
38
+ if (!Array.isArray(details?.changes) || details.changes.length === 0) return undefined;
39
+
40
+ let updated = content;
41
+ for (const value of details.changes) {
42
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
43
+ const change = value as Record<string, unknown>;
44
+ if (change.operation !== 'replace' || typeof change.before !== 'string' || typeof change.after !== 'string') {
45
+ return undefined;
46
+ }
47
+
48
+ if (!updated.includes(change.before)) return undefined;
49
+ if (change.replace_all === true) {
50
+ updated = updated.split(change.before).join(change.after);
51
+ } else {
52
+ if (updated.indexOf(change.before) !== updated.lastIndexOf(change.before)) return undefined;
53
+ updated = updated.replace(change.before, change.after);
54
+ }
55
+ }
56
+
57
+ return updated;
58
+ }
59
+
60
+ function getErrorStatus(error: unknown): number | undefined {
61
+ if (!error || typeof error !== 'object' || !('status' in error)) return undefined;
62
+ return typeof error.status === 'number' ? error.status : undefined;
63
+ }
64
+
65
+ function MarkdownChangeRuler({
66
+ regions,
67
+ totalLines,
68
+ onNavigate,
69
+ }: {
70
+ regions: MarkdownChangeRegion[];
71
+ totalLines: number;
72
+ onNavigate: (line: number) => void;
73
+ }) {
74
+ const { t } = useUITranslation();
75
+ if (regions.length === 0) return null;
76
+
77
+ return (
78
+ <nav
79
+ className="absolute inset-y-12 end-1 z-10 w-3 rounded-full border border-mixer-muted/25 bg-muted/20"
80
+ aria-label={t('agent.changeRuler')}
81
+ >
82
+ {regions.map((region) => {
83
+ const top = (region.startLine / Math.max(1, totalLines)) * 100;
84
+ const height = Math.max(1.5, ((region.endLine - region.startLine + 1) / Math.max(1, totalLines)) * 100);
85
+ return (
86
+ <VTooltip
87
+ key={`${region.startLine}:${region.endLine}`}
88
+ description={t('agent.changedRegion', { line: region.startLine + 1 })}
89
+ asChild
90
+ >
91
+ <button
92
+ type="button"
93
+ className="absolute inset-x-0 rounded-full bg-attention transition-colors hover:bg-info focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-info"
94
+ style={{ top: `${Math.min(98.5, top)}%`, height: `${Math.min(100 - top, height)}%` }}
95
+ onClick={() => onNavigate(region.startLine)}
96
+ aria-label={t('agent.changedRegion', { line: region.startLine + 1 })}
97
+ />
98
+ </VTooltip>
99
+ );
100
+ })}
101
+ </nav>
102
+ );
103
+ }
104
+
105
+ export interface ArtifactEditingSurfaceDocumentEdit {
106
+ /** Working-copy content the agent last knew (hydration, its own edits, or the last hand-off). */
107
+ previous: string;
108
+ /** Persisted working-copy content after the user's direct edits. */
109
+ current: string;
110
+ }
111
+
112
+ interface ArtifactDocumentConflict {
113
+ baseContent: string;
114
+ localContent: string;
115
+ remoteContent?: string;
116
+ remoteGeneration?: string;
117
+ pendingAction?: MarkdownEditingAction;
118
+ }
119
+
120
+ interface MarkdownDocumentEditorHandle {
121
+ getMarkdown(): string;
122
+ commands: {
123
+ setContent(content: string, options: { contentType: 'markdown'; emitUpdate: boolean }): boolean;
124
+ };
125
+ }
126
+
127
+ /** Working-copy save state of the document editor. `idle` means nothing to show. */
128
+ export type ArtifactSaveStatus = 'idle' | 'saving' | 'saved';
129
+
130
+ export interface ArtifactEditingSurfaceProps {
131
+ runId?: string;
132
+ path: string;
133
+ initialContent?: string;
134
+ refreshKey?: number;
135
+ refreshDetails?: Record<string, unknown>;
136
+ readOnly?: boolean;
137
+ /** Keep comment controls enabled when content mutations are read-only. */
138
+ allowComments?: boolean;
139
+ viewMode?: 'components' | 'document';
140
+ /** Canonical Markdown used by the full editor's change ruler. */
141
+ baselineContent?: string;
142
+ className?: string;
143
+ onAction?: (action: MarkdownEditingAction) => void | Promise<void>;
144
+ /** Send a composed batch of span comments to the agent as one conversation message. */
145
+ onSendMessage?: (message: string) => void | Promise<void>;
146
+ /** Hand persisted full-document edits to the active agent. */
147
+ onSendChangesToAgent?: () => void | Promise<void>;
148
+ hasUnsentChanges?: boolean;
149
+ isSendingChanges?: boolean;
150
+ sendChangesDisabled?: boolean;
151
+ /** Compact status rendered beside the full editor's document hand-off action. */
152
+ toolbarStatus?: React.ReactNode;
153
+ onContentChange?: (content: string, generation?: string) => void;
154
+ onDocumentEdit?: () => void;
155
+ /**
156
+ * Reports document-editor save state so a parent can render its own indicator
157
+ * (e.g. next to the file name). When provided, the built-in floating badge is
158
+ * suppressed and the parent owns the display.
159
+ */
160
+ onSaveStatusChange?: (status: ArtifactSaveStatus) => void;
161
+ /**
162
+ * Imperative flush used before handing direct full-document edits to the agent.
163
+ * Resolves false when the flush failed; otherwise returns the delta since the
164
+ * agent's last known state and advances that baseline to the flushed content.
165
+ */
166
+ flushChangesRef?: React.MutableRefObject<(() => Promise<false | ArtifactEditingSurfaceDocumentEdit>) | null>;
167
+ }
168
+
169
+ export interface MarkdownChangeRegion {
170
+ startLine: number;
171
+ endLine: number;
172
+ }
173
+
174
+ /** Maps structured diff hunks onto current-document line numbers. */
175
+ export function getMarkdownChangeRegions(before: string, after: string): MarkdownChangeRegion[] {
176
+ return getTextLineChangeRegions(before, after);
177
+ }
178
+
179
+ /**
180
+ * Shared artifact-backed Markdown editor used by both agent workspaces and
181
+ * document editing sessions. The artifact is updated conditionally using its
182
+ * backend-neutral blob generation; the previously rendered content stays in
183
+ * place while a newer generation is loading.
184
+ */
185
+ export function ArtifactEditingSurface({
186
+ runId,
187
+ path,
188
+ initialContent = '',
189
+ refreshKey = 0,
190
+ refreshDetails,
191
+ readOnly = false,
192
+ allowComments = false,
193
+ viewMode = 'components',
194
+ baselineContent,
195
+ className,
196
+ onAction,
197
+ onSendMessage,
198
+ onSendChangesToAgent,
199
+ hasUnsentChanges = false,
200
+ isSendingChanges = false,
201
+ sendChangesDisabled = false,
202
+ toolbarStatus,
203
+ onContentChange,
204
+ onDocumentEdit,
205
+ onSaveStatusChange,
206
+ flushChangesRef,
207
+ }: ArtifactEditingSurfaceProps) {
208
+ const { client } = useUserSession();
209
+ const { t } = useUITranslation();
210
+ const toast = useToast();
211
+ const [content, setContent] = useState(initialContent);
212
+ const [generation, setGeneration] = useState<string | undefined>();
213
+ const [highlightChangesFrom, setHighlightChangesFrom] = useState<string | undefined>();
214
+ const [highlightVersion, setHighlightVersion] = useState(0);
215
+ const [isLoading, setIsLoading] = useState(Boolean(runId));
216
+ const [isSavingDocument, setIsSavingDocument] = useState(false);
217
+ const [isDocumentSavePending, setIsDocumentSavePending] = useState(false);
218
+ const [loadError, setLoadError] = useState<string | undefined>();
219
+ const [documentConflict, setDocumentConflict] = useState<ArtifactDocumentConflict>();
220
+ const [isResolvingConflict, setIsResolvingConflict] = useState(false);
221
+ const commentsEnabled = !readOnly || allowComments;
222
+
223
+ // Working-copy save state for the document editor, shared with the built-in badge and
224
+ // optionally reported to a parent that renders its own indicator.
225
+ const saveStatus: ArtifactSaveStatus =
226
+ viewMode !== 'document' || documentConflict
227
+ ? 'idle'
228
+ : isSavingDocument || isDocumentSavePending
229
+ ? 'saving'
230
+ : generation
231
+ ? 'saved'
232
+ : 'idle';
233
+ useEffect(() => {
234
+ onSaveStatusChange?.(saveStatus);
235
+ }, [saveStatus, onSaveStatusChange]);
236
+
237
+ const contentRef = useRef(content);
238
+ const generationRef = useRef(generation);
239
+ const loadRequestRef = useRef(0);
240
+ const documentSaveTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
241
+ const pendingDocumentContentRef = useRef<string | undefined>(undefined);
242
+ const documentSaveInFlightRef = useRef(false);
243
+ const documentSavePromiseRef = useRef<Promise<boolean> | undefined>(undefined);
244
+ const persistPendingDocumentContentRef = useRef<() => Promise<boolean>>(() => Promise.resolve(false));
245
+ const documentEditorFocusedRef = useRef(false);
246
+ // Latches true once the user focuses the editor. The editor re-serializes (normalizes) the
247
+ // loaded Markdown when it mounts — e.g. after switching into edit mode — which fires an onChange
248
+ // that is NOT a user edit. We ignore those pre-interaction changes so they don't autosave the
249
+ // artifact or post a spurious "working copy updated" message to the chat.
250
+ const documentEditorInteractedRef = useRef(false);
251
+ const documentEditorRef = useRef<MarkdownDocumentEditorHandle | null>(null);
252
+ const documentBaseGenerationRef = useRef<string | undefined>(generation);
253
+ const documentBaseContentRef = useRef(initialContent);
254
+ const documentConflictRef = useRef<ArtifactDocumentConflict | undefined>(undefined);
255
+ // Working-copy content the agent is known to have seen: hydration and its own
256
+ // edits (loadContent) plus explicit hand-offs (flushDocumentChanges). Direct user
257
+ // edits are diffed against this baseline when they are sent to the agent.
258
+ const agentKnownContentRef = useRef(initialContent);
259
+ const documentEditorContainerRef = useRef<HTMLDivElement | null>(null);
260
+ const changeRegions = useMemo(
261
+ () => getMarkdownChangeRegions(baselineContent ?? content, content),
262
+ [baselineContent, content],
263
+ );
264
+ const documentLineCount = useMemo(() => content.split('\n').length, [content]);
265
+
266
+ const updateDocumentConflict = useCallback((conflict: ArtifactDocumentConflict | undefined) => {
267
+ documentConflictRef.current = conflict;
268
+ setDocumentConflict(conflict);
269
+ }, []);
270
+
271
+ useEffect(() => {
272
+ contentRef.current = content;
273
+ }, [content]);
274
+
275
+ useEffect(() => {
276
+ generationRef.current = generation;
277
+ }, [generation]);
278
+
279
+ // Entering edit mode mounts a fresh editor that re-serializes the loaded content. Require a new
280
+ // user focus before those serialization changes count as edits. Keyed on viewMode (a stable
281
+ // trigger) — not the per-render onEditor callback, which would reset the latch mid-typing.
282
+ useEffect(() => {
283
+ if (viewMode !== 'document') return;
284
+ documentEditorInteractedRef.current = false;
285
+ documentEditorFocusedRef.current = false;
286
+ }, [viewMode]);
287
+
288
+ const loadContent = useCallback(
289
+ async (allowHydrationRetry: boolean) => {
290
+ if (!runId) return;
291
+ const requestId = ++loadRequestRef.current;
292
+ setIsLoading(true);
293
+ setLoadError(undefined);
294
+
295
+ for (let attempt = 0; attempt <= HYDRATION_RETRY_LIMIT; attempt++) {
296
+ try {
297
+ const response = await client.agents.getArtifactContent(runId, path);
298
+ if (loadRequestRef.current !== requestId) return;
299
+ const previousContent = contentRef.current;
300
+ if (previousContent && previousContent !== response.content) {
301
+ setHighlightChangesFrom(previousContent);
302
+ setHighlightVersion((value) => value + 1);
303
+ }
304
+ contentRef.current = response.content;
305
+ generationRef.current = response.generation;
306
+ // Server states reached outside the user's typing pipeline come from
307
+ // hydration or the agent's own tools, so the agent knows them.
308
+ agentKnownContentRef.current = response.content;
309
+ if (
310
+ !documentEditorFocusedRef.current &&
311
+ pendingDocumentContentRef.current === undefined &&
312
+ !documentSaveInFlightRef.current
313
+ ) {
314
+ documentBaseGenerationRef.current = response.generation;
315
+ documentBaseContentRef.current = response.content;
316
+ }
317
+ setContent(response.content);
318
+ setGeneration(response.generation);
319
+ setLoadError(undefined);
320
+ setIsLoading(false);
321
+ onContentChange?.(response.content, response.generation);
322
+ return;
323
+ } catch (error: unknown) {
324
+ if (
325
+ loadRequestRef.current !== requestId ||
326
+ !allowHydrationRetry ||
327
+ getErrorStatus(error) !== 404 ||
328
+ attempt === HYDRATION_RETRY_LIMIT
329
+ ) {
330
+ if (loadRequestRef.current !== requestId) return;
331
+ setLoadError(errorMessage(error, t('agent.failedToLoadArtifact')));
332
+ setIsLoading(false);
333
+ return;
334
+ }
335
+ await new Promise((resolve) => setTimeout(resolve, HYDRATION_RETRY_DELAY_MS));
336
+ }
337
+ }
338
+ },
339
+ [client, onContentChange, path, runId, t],
340
+ );
341
+
342
+ useEffect(() => {
343
+ loadRequestRef.current++;
344
+ contentRef.current = initialContent;
345
+ generationRef.current = undefined;
346
+ agentKnownContentRef.current = initialContent;
347
+ documentBaseGenerationRef.current = undefined;
348
+ documentBaseContentRef.current = initialContent;
349
+ documentConflictRef.current = undefined;
350
+ documentEditorFocusedRef.current = false;
351
+ documentEditorInteractedRef.current = false;
352
+ setContent(initialContent);
353
+ setGeneration(undefined);
354
+ setHighlightChangesFrom(undefined);
355
+ setLoadError(undefined);
356
+ setDocumentConflict(undefined);
357
+ setIsResolvingConflict(false);
358
+ setIsDocumentSavePending(false);
359
+ setIsLoading(Boolean(runId));
360
+ if (runId) void loadContent(true);
361
+ return () => {
362
+ loadRequestRef.current++;
363
+ };
364
+ }, [initialContent, loadContent, runId]);
365
+
366
+ useEffect(() => {
367
+ if (!runId || refreshKey <= 0) return;
368
+ if (documentConflictRef.current) return;
369
+
370
+ const previousContent = contentRef.current;
371
+ const changedContent = applyArtifactRefreshChanges(previousContent, refreshDetails);
372
+ if (changedContent !== undefined && changedContent !== previousContent) {
373
+ contentRef.current = changedContent;
374
+ generationRef.current = undefined;
375
+ setHighlightChangesFrom(previousContent);
376
+ setHighlightVersion((value) => value + 1);
377
+ setContent(changedContent);
378
+ setGeneration(undefined);
379
+ onContentChange?.(changedContent);
380
+ }
381
+
382
+ void loadContent(false);
383
+ }, [loadContent, onContentChange, refreshDetails, refreshKey, runId]);
384
+
385
+ const applyResolvedDocumentContent = useCallback(
386
+ (nextContent: string, nextGeneration: string) => {
387
+ const previousContent = contentRef.current;
388
+ contentRef.current = nextContent;
389
+ generationRef.current = nextGeneration;
390
+ documentBaseContentRef.current = nextContent;
391
+ documentBaseGenerationRef.current = nextGeneration;
392
+ pendingDocumentContentRef.current = undefined;
393
+ updateDocumentConflict(undefined);
394
+ if (documentEditorRef.current?.getMarkdown() !== nextContent) {
395
+ documentEditorRef.current?.commands.setContent(nextContent, {
396
+ contentType: 'markdown',
397
+ emitUpdate: false,
398
+ });
399
+ }
400
+ if (previousContent !== nextContent) {
401
+ setHighlightChangesFrom(previousContent);
402
+ setHighlightVersion((value) => value + 1);
403
+ }
404
+ setContent(nextContent);
405
+ setGeneration(nextGeneration);
406
+ setIsDocumentSavePending(false);
407
+ onContentChange?.(nextContent, nextGeneration);
408
+ },
409
+ [onContentChange, updateDocumentConflict],
410
+ );
411
+
412
+ const reconcileDocumentConflict = useCallback(
413
+ async (
414
+ baseContent: string,
415
+ localContent: string,
416
+ pendingAction?: MarkdownEditingAction,
417
+ ): Promise<false | { content: string; generation: string }> => {
418
+ if (!runId) return false;
419
+
420
+ // Preserve the unsaved side immediately. No reload path may replace it
421
+ // until a merge succeeds or the user explicitly leaves the editor.
422
+ contentRef.current = localContent;
423
+ pendingDocumentContentRef.current = undefined;
424
+ setContent(localContent);
425
+ setIsDocumentSavePending(false);
426
+ setIsResolvingConflict(true);
427
+
428
+ let remoteContent: string | undefined;
429
+ let remoteGeneration: string | undefined;
430
+ try {
431
+ const remote = await client.agents.getArtifactContent(runId, path);
432
+ remoteContent = remote.content;
433
+ remoteGeneration = remote.generation;
434
+ agentKnownContentRef.current = remote.content;
435
+
436
+ const rebased = rebaseTextChanges(baseContent, localContent, remote.content);
437
+ if (rebased.status === 'conflict') {
438
+ updateDocumentConflict({
439
+ baseContent,
440
+ localContent,
441
+ remoteContent: remote.content,
442
+ remoteGeneration: remote.generation,
443
+ pendingAction,
444
+ });
445
+ return false;
446
+ }
447
+
448
+ let nextGeneration = remote.generation;
449
+ if (rebased.content !== remote.content) {
450
+ try {
451
+ const response = await client.agents.updateArtifactContent(runId, path, {
452
+ content: rebased.content,
453
+ generation: remote.generation,
454
+ });
455
+ nextGeneration = response.generation;
456
+ } catch (error: unknown) {
457
+ if (getErrorStatus(error) === 412) {
458
+ updateDocumentConflict({
459
+ baseContent: remote.content,
460
+ localContent: rebased.content,
461
+ remoteContent: remote.content,
462
+ remoteGeneration: remote.generation,
463
+ pendingAction,
464
+ });
465
+ return false;
466
+ }
467
+ throw error;
468
+ }
469
+ }
470
+
471
+ applyResolvedDocumentContent(rebased.content, nextGeneration);
472
+ toast({ status: 'success', title: t('agent.artifactConflictRebased'), duration: 3000 });
473
+ return { content: rebased.content, generation: nextGeneration };
474
+ } catch (error: unknown) {
475
+ updateDocumentConflict({
476
+ baseContent,
477
+ localContent,
478
+ remoteContent,
479
+ remoteGeneration,
480
+ pendingAction,
481
+ });
482
+ toast({
483
+ status: 'error',
484
+ title: t('agent.artifactConflictTitle'),
485
+ description: errorMessage(error, t('agent.failedToLoadArtifact')),
486
+ duration: 5000,
487
+ });
488
+ return false;
489
+ } finally {
490
+ setIsResolvingConflict(false);
491
+ }
492
+ },
493
+ [applyResolvedDocumentContent, client.agents, path, runId, t, toast, updateDocumentConflict],
494
+ );
495
+
496
+ const handleAction = useCallback(
497
+ async (action: MarkdownEditingAction) => {
498
+ if (action.action === 'comment') {
499
+ await onAction?.(action);
500
+ return;
501
+ }
502
+
503
+ if (documentConflictRef.current) {
504
+ toast({ status: 'warning', title: t('agent.artifactConflictTitle'), duration: 3000 });
505
+ return;
506
+ }
507
+
508
+ if (!runId || !generationRef.current) {
509
+ toast({ status: 'warning', title: t('agent.artifactEditingUnavailable'), duration: 3000 });
510
+ return;
511
+ }
512
+
513
+ const previousContent = contentRef.current;
514
+ const nextContent = applyMarkdownEditingChange(previousContent, action);
515
+ if (nextContent === undefined) {
516
+ toast({
517
+ status: 'error',
518
+ title: t('agent.documentEditApplyFailed'),
519
+ description: t('agent.documentEditApplyFailedDescription'),
520
+ duration: 5000,
521
+ });
522
+ return;
523
+ }
524
+
525
+ try {
526
+ const response = await client.agents.updateArtifactContent(runId, path, {
527
+ content: nextContent,
528
+ generation: generationRef.current,
529
+ });
530
+ contentRef.current = nextContent;
531
+ generationRef.current = response.generation;
532
+ documentBaseContentRef.current = nextContent;
533
+ documentBaseGenerationRef.current = response.generation;
534
+ setHighlightChangesFrom(previousContent);
535
+ setHighlightVersion((value) => value + 1);
536
+ setContent(nextContent);
537
+ setGeneration(response.generation);
538
+ onContentChange?.(nextContent, response.generation);
539
+ await onAction?.({ ...action, applied: true, base_version: response.generation });
540
+ agentKnownContentRef.current = nextContent;
541
+ } catch (error: unknown) {
542
+ const isConflict = getErrorStatus(error) === 412;
543
+ if (isConflict) {
544
+ const resolved = await reconcileDocumentConflict(previousContent, nextContent, action);
545
+ if (resolved) {
546
+ await onAction?.({ ...action, applied: true, base_version: resolved.generation });
547
+ agentKnownContentRef.current = resolved.content;
548
+ }
549
+ return;
550
+ }
551
+ toast({
552
+ status: 'error',
553
+ title: t('agent.documentEditApplyFailed'),
554
+ description: errorMessage(error, t('store.errorSavingTextDefault')),
555
+ duration: 5000,
556
+ });
557
+ }
558
+ },
559
+ [client, onAction, onContentChange, path, reconcileDocumentConflict, runId, t, toast],
560
+ );
561
+
562
+ const persistPendingDocumentContent = useCallback((): Promise<boolean> => {
563
+ if (!runId) return Promise.resolve(false);
564
+ if (documentSavePromiseRef.current) return documentSavePromiseRef.current;
565
+
566
+ const savePromise = (async () => {
567
+ documentSaveInFlightRef.current = true;
568
+ setIsSavingDocument(true);
569
+ try {
570
+ while (pendingDocumentContentRef.current !== undefined) {
571
+ const nextContent = pendingDocumentContentRef.current;
572
+ pendingDocumentContentRef.current = undefined;
573
+ const expectedGeneration = documentBaseGenerationRef.current;
574
+ if (!expectedGeneration) {
575
+ throw new Error(t('agent.artifactEditingUnavailable'));
576
+ }
577
+
578
+ const response = await client.agents.updateArtifactContent(runId, path, {
579
+ content: nextContent,
580
+ generation: expectedGeneration,
581
+ });
582
+ generationRef.current = response.generation;
583
+ documentBaseGenerationRef.current = response.generation;
584
+ documentBaseContentRef.current = nextContent;
585
+ setGeneration(response.generation);
586
+ onContentChange?.(contentRef.current, response.generation);
587
+ }
588
+ setIsDocumentSavePending(false);
589
+ return true;
590
+ } catch (error: unknown) {
591
+ const isConflict = getErrorStatus(error) === 412;
592
+ if (isConflict) {
593
+ const resolved = await reconcileDocumentConflict(
594
+ documentBaseContentRef.current,
595
+ contentRef.current,
596
+ );
597
+ return Boolean(resolved);
598
+ }
599
+ toast({
600
+ status: 'error',
601
+ title: t('agent.documentEditApplyFailed'),
602
+ description: errorMessage(error, t('store.errorSavingTextDefault')),
603
+ duration: 5000,
604
+ });
605
+ return false;
606
+ } finally {
607
+ documentSaveInFlightRef.current = false;
608
+ setIsSavingDocument(false);
609
+ }
610
+ })();
611
+
612
+ documentSavePromiseRef.current = savePromise;
613
+ void savePromise.finally(() => {
614
+ if (documentSavePromiseRef.current === savePromise) documentSavePromiseRef.current = undefined;
615
+ });
616
+ return savePromise;
617
+ }, [client.agents, onContentChange, path, reconcileDocumentConflict, runId, t, toast]);
618
+ persistPendingDocumentContentRef.current = persistPendingDocumentContent;
619
+
620
+ const flushDocumentChanges = useCallback(async (): Promise<false | ArtifactEditingSurfaceDocumentEdit> => {
621
+ if (documentSaveTimeoutRef.current !== undefined) {
622
+ clearTimeout(documentSaveTimeoutRef.current);
623
+ documentSaveTimeoutRef.current = undefined;
624
+ }
625
+
626
+ const latestEditorContent = documentEditorRef.current?.getMarkdown();
627
+ if (latestEditorContent !== undefined && latestEditorContent !== contentRef.current) {
628
+ contentRef.current = latestEditorContent;
629
+ setContent(latestEditorContent);
630
+ onContentChange?.(latestEditorContent, generationRef.current);
631
+ pendingDocumentContentRef.current = latestEditorContent;
632
+ }
633
+
634
+ const activeConflict = documentConflictRef.current;
635
+ if (activeConflict) {
636
+ const resolved = await reconcileDocumentConflict(activeConflict.baseContent, contentRef.current);
637
+ if (!resolved) return false;
638
+ }
639
+
640
+ while (pendingDocumentContentRef.current !== undefined || documentSavePromiseRef.current) {
641
+ const saved = await persistPendingDocumentContent();
642
+ if (!saved) return false;
643
+ }
644
+ if (!runId || !generationRef.current) return false;
645
+
646
+ const previous = agentKnownContentRef.current;
647
+ agentKnownContentRef.current = contentRef.current;
648
+ return { previous, current: contentRef.current };
649
+ }, [onContentChange, persistPendingDocumentContent, reconcileDocumentConflict, runId]);
650
+
651
+ useEffect(() => {
652
+ if (!flushChangesRef) return;
653
+ flushChangesRef.current = flushDocumentChanges;
654
+ return () => {
655
+ if (flushChangesRef.current === flushDocumentChanges) flushChangesRef.current = null;
656
+ };
657
+ }, [flushChangesRef, flushDocumentChanges]);
658
+
659
+ const handleDocumentChange = useCallback(
660
+ (nextContent: string) => {
661
+ if (nextContent === contentRef.current) return;
662
+ // A focused editor intentionally keeps its local document mounted while an agent
663
+ // refresh updates the backing artifact. Ignore the editor's stale base-content
664
+ // emission so it cannot be mistaken for a user edit and saved over the refresh.
665
+ if (
666
+ documentEditorFocusedRef.current &&
667
+ pendingDocumentContentRef.current === undefined &&
668
+ !documentSaveInFlightRef.current &&
669
+ nextContent === documentBaseContentRef.current
670
+ ) {
671
+ return;
672
+ }
673
+ // The editor normalizes loaded Markdown on mount, firing onChange before the user has
674
+ // touched it. Adopt that serialization as the baseline silently — no save, no message.
675
+ if (!documentEditorInteractedRef.current) {
676
+ contentRef.current = nextContent;
677
+ setContent(nextContent);
678
+ return;
679
+ }
680
+ contentRef.current = nextContent;
681
+ setContent(nextContent);
682
+ onDocumentEdit?.();
683
+ onContentChange?.(nextContent, generationRef.current);
684
+
685
+ const activeConflict = documentConflictRef.current;
686
+ if (activeConflict) {
687
+ updateDocumentConflict({ ...activeConflict, localContent: nextContent });
688
+ setIsDocumentSavePending(false);
689
+ return;
690
+ }
691
+
692
+ setIsDocumentSavePending(true);
693
+ pendingDocumentContentRef.current = nextContent;
694
+ if (documentSaveTimeoutRef.current !== undefined) clearTimeout(documentSaveTimeoutRef.current);
695
+ documentSaveTimeoutRef.current = setTimeout(() => {
696
+ documentSaveTimeoutRef.current = undefined;
697
+ void persistPendingDocumentContent();
698
+ }, DOCUMENT_SAVE_DEBOUNCE_MS);
699
+ },
700
+ [onContentChange, onDocumentEdit, persistPendingDocumentContent, updateDocumentConflict],
701
+ );
702
+
703
+ const handleDocumentFocusChange = useCallback((focused: boolean) => {
704
+ documentEditorFocusedRef.current = focused;
705
+ if (focused) documentEditorInteractedRef.current = true;
706
+ if (focused) {
707
+ documentBaseGenerationRef.current = generationRef.current;
708
+ documentBaseContentRef.current = contentRef.current;
709
+ } else if (pendingDocumentContentRef.current === undefined && !documentSaveInFlightRef.current) {
710
+ documentBaseGenerationRef.current = generationRef.current;
711
+ documentBaseContentRef.current = contentRef.current;
712
+ }
713
+ }, []);
714
+
715
+ const retryDocumentConflict = useCallback(async () => {
716
+ const activeConflict = documentConflictRef.current;
717
+ if (!activeConflict || isResolvingConflict) return;
718
+ const resolved = await reconcileDocumentConflict(
719
+ activeConflict.baseContent,
720
+ contentRef.current,
721
+ activeConflict.pendingAction,
722
+ );
723
+ if (resolved && activeConflict.pendingAction) {
724
+ await onAction?.({
725
+ ...activeConflict.pendingAction,
726
+ applied: true,
727
+ base_version: resolved.generation,
728
+ });
729
+ agentKnownContentRef.current = resolved.content;
730
+ }
731
+ }, [isResolvingConflict, onAction, reconcileDocumentConflict]);
732
+
733
+ const navigateToChangedLine = useCallback(
734
+ (line: number) => {
735
+ const scrollContainer = documentEditorContainerRef.current?.querySelector<HTMLElement>(
736
+ '.vertesia-markdown-document-editor-content',
737
+ );
738
+ if (!scrollContainer) return;
739
+ const ratio = Math.min(1, line / Math.max(1, documentLineCount - 1));
740
+ scrollContainer.scrollTo({
741
+ top: ratio * Math.max(0, scrollContainer.scrollHeight - scrollContainer.clientHeight),
742
+ behavior: 'smooth',
743
+ });
744
+ },
745
+ [documentLineCount],
746
+ );
747
+
748
+ useEffect(
749
+ () => () => {
750
+ if (documentSaveTimeoutRef.current !== undefined) clearTimeout(documentSaveTimeoutRef.current);
751
+ const latestEditorContent = documentEditorRef.current?.getMarkdown();
752
+ if (latestEditorContent !== undefined && latestEditorContent !== contentRef.current) {
753
+ contentRef.current = latestEditorContent;
754
+ pendingDocumentContentRef.current = latestEditorContent;
755
+ }
756
+ if (pendingDocumentContentRef.current !== undefined) {
757
+ void persistPendingDocumentContentRef.current();
758
+ }
759
+ },
760
+ [],
761
+ );
762
+
763
+ return (
764
+ <div className={className ?? 'relative h-full min-h-0 overflow-y-auto px-4 py-3'}>
765
+ {documentConflict ? (
766
+ <div className="absolute inset-x-4 top-3 z-30" role="alert">
767
+ <MessageBox status="warning" title={t('agent.artifactConflictTitle')} className="shadow-lg">
768
+ <div className="space-y-3">
769
+ <p>{t('agent.artifactConflictDescription')}</p>
770
+ <div className="flex justify-end">
771
+ <Button
772
+ variant="outline"
773
+ size="sm"
774
+ disabled={isResolvingConflict}
775
+ onClick={() => void retryDocumentConflict()}
776
+ >
777
+ {isResolvingConflict ? <Spinner size="sm" /> : null}
778
+ {t('agent.artifactConflictRetry')}
779
+ </Button>
780
+ </div>
781
+ </div>
782
+ </MessageBox>
783
+ </div>
784
+ ) : null}
785
+ {isLoading && !content ? (
786
+ <Center className="h-full min-h-[200px]">
787
+ <Spinner size="lg" />
788
+ </Center>
789
+ ) : loadError && !content ? (
790
+ <Center className="h-full min-h-[200px] flex-col gap-3 text-center text-muted">
791
+ <span>{loadError}</span>
792
+ <Button variant="outline" size="sm" onClick={() => void loadContent(false)}>
793
+ {t('agent.retry')}
794
+ </Button>
795
+ </Center>
796
+ ) : viewMode === 'document' ? (
797
+ <Suspense
798
+ fallback={
799
+ <Center className="h-full min-h-[200px]">
800
+ <Spinner size="lg" />
801
+ </Center>
802
+ }
803
+ >
804
+ <div className="flex h-full min-h-0 flex-col">
805
+ <div ref={documentEditorContainerRef} className="relative min-h-0 flex-1">
806
+ <MarkdownDocumentEditor
807
+ value={content}
808
+ onChange={handleDocumentChange}
809
+ artifactRunId={runId}
810
+ editable={!readOnly && Boolean(runId)}
811
+ externalValueSync="when-blurred"
812
+ onFocusChange={handleDocumentFocusChange}
813
+ onEditor={(editor) => {
814
+ documentEditorRef.current =
815
+ editor as unknown as MarkdownDocumentEditorHandle | null;
816
+ }}
817
+ onSendComment={
818
+ onSendMessage && runId && !documentConflict && commentsEnabled
819
+ ? onSendMessage
820
+ : undefined
821
+ }
822
+ onSendChangesToAgent={onSendChangesToAgent}
823
+ hasUnsentChanges={hasUnsentChanges}
824
+ isSendingChanges={isSendingChanges}
825
+ sendChangesDisabled={sendChangesDisabled || !runId || Boolean(documentConflict)}
826
+ toolbarStatus={toolbarStatus}
827
+ contentClassName="pe-5"
828
+ />
829
+ <MarkdownChangeRuler
830
+ regions={changeRegions}
831
+ totalLines={documentLineCount}
832
+ onNavigate={navigateToChangedLine}
833
+ />
834
+ {!onSaveStatusChange && saveStatus !== 'idle' ? (
835
+ <div
836
+ className={cn(
837
+ 'pointer-events-none absolute bottom-3 end-6 flex items-center gap-1.5 rounded-full',
838
+ 'border border-mixer-muted/25 bg-background/90 px-2.5 py-1 text-[11px] shadow-sm',
839
+ isSavingDocument || isDocumentSavePending ? 'text-muted' : 'text-success',
840
+ )}
841
+ >
842
+ {isSavingDocument || isDocumentSavePending ? (
843
+ <Spinner size="sm" />
844
+ ) : (
845
+ <Check className="size-3" />
846
+ )}
847
+ {isSavingDocument || isDocumentSavePending
848
+ ? t('agent.savingWorkingCopy')
849
+ : t('agent.savedToWorkingCopy')}
850
+ </div>
851
+ ) : null}
852
+ </div>
853
+ </div>
854
+ </Suspense>
855
+ ) : (
856
+ <div className="vprose prose-sm mx-auto max-w-5xl">
857
+ <CollaborativeMarkdownRenderer
858
+ artifactRunId={runId}
859
+ resource={{ kind: 'agent_artifact', run_id: runId ?? 'pending', path }}
860
+ baseVersion={generation}
861
+ readOnly={readOnly || !runId || Boolean(documentConflict)}
862
+ allowComments={commentsEnabled && Boolean(runId) && !documentConflict}
863
+ highlightChangesFrom={highlightChangesFrom}
864
+ highlightVersion={highlightVersion}
865
+ onAction={handleAction}
866
+ >
867
+ {content}
868
+ </CollaborativeMarkdownRenderer>
869
+ </div>
870
+ )}
871
+ {isLoading && content ? (
872
+ <div className="absolute end-3 top-3 rounded-full bg-background/80 p-2 shadow-sm">
873
+ <GitCommitHorizontal className="size-4 animate-pulse text-muted" />
874
+ </div>
875
+ ) : null}
876
+ </div>
877
+ );
878
+ }