@djangocfg/ui-tools 2.1.404 → 2.1.408

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 (336) hide show
  1. package/README.md +9 -11
  2. package/dist/file-icon/index.cjs +449 -61
  3. package/dist/file-icon/index.cjs.map +1 -1
  4. package/dist/file-icon/index.d.cts +56 -18
  5. package/dist/file-icon/index.d.ts +56 -18
  6. package/dist/file-icon/index.mjs +448 -62
  7. package/dist/file-icon/index.mjs.map +1 -1
  8. package/dist/tree/index.cjs +49 -22
  9. package/dist/tree/index.cjs.map +1 -1
  10. package/dist/tree/index.d.cts +9 -3
  11. package/dist/tree/index.d.ts +9 -3
  12. package/dist/tree/index.mjs +49 -22
  13. package/dist/tree/index.mjs.map +1 -1
  14. package/dist/{types-B_zhyAqR.d.cts → types-eEu8SeiQ.d.cts} +4 -0
  15. package/dist/{types-B_zhyAqR.d.ts → types-eEu8SeiQ.d.ts} +4 -0
  16. package/package.json +13 -16
  17. package/src/components/FloatingToolbar/index.tsx +37 -3
  18. package/src/lib/page-snapshot/__tests__/capture-integration.test.ts +85 -0
  19. package/src/lib/page-snapshot/__tests__/engine.test.ts +36 -0
  20. package/src/lib/page-snapshot/__tests__/redaction-integration.test.ts +99 -0
  21. package/src/lib/page-snapshot/__tests__/tokens.test.ts +17 -0
  22. package/src/lib/page-snapshot/capture/__tests__/budget.test.ts +49 -0
  23. package/src/lib/page-snapshot/capture/__tests__/chrome-filter.test.ts +47 -0
  24. package/src/lib/page-snapshot/capture/__tests__/fold.test.ts +66 -0
  25. package/src/lib/page-snapshot/capture/__tests__/scope.test.ts +74 -0
  26. package/src/lib/page-snapshot/capture/__tests__/walk.test.ts +129 -0
  27. package/src/lib/page-snapshot/capture/accessible-name.ts +73 -0
  28. package/src/lib/page-snapshot/capture/budget.ts +95 -0
  29. package/src/lib/page-snapshot/capture/chrome-filter.ts +81 -0
  30. package/src/lib/page-snapshot/capture/classify.ts +111 -0
  31. package/src/lib/page-snapshot/capture/dom-utils.ts +111 -0
  32. package/src/lib/page-snapshot/capture/fold.ts +96 -0
  33. package/src/lib/page-snapshot/capture/scope.ts +169 -0
  34. package/src/lib/page-snapshot/capture/walk.ts +250 -0
  35. package/src/lib/page-snapshot/cst/__tests__/serialize.test.ts +50 -0
  36. package/src/lib/page-snapshot/cst/directives.ts +47 -0
  37. package/src/lib/page-snapshot/cst/payload.ts +50 -0
  38. package/src/lib/page-snapshot/cst/serialize.ts +84 -0
  39. package/src/lib/page-snapshot/cst/types.ts +115 -0
  40. package/src/lib/page-snapshot/engine.ts +176 -0
  41. package/src/lib/page-snapshot/index.ts +93 -0
  42. package/src/lib/page-snapshot/react/PageSnapshotChip.tsx +72 -0
  43. package/src/lib/page-snapshot/react/PageSnapshotPreview.tsx +78 -0
  44. package/src/lib/page-snapshot/react/__tests__/PageSnapshotChip.test.tsx +54 -0
  45. package/src/lib/page-snapshot/react/__tests__/provider.test.tsx +103 -0
  46. package/src/lib/page-snapshot/react/__tests__/use-page-snapshot-toggle.test.tsx +62 -0
  47. package/src/lib/page-snapshot/react/provider.tsx +162 -0
  48. package/src/lib/page-snapshot/react/use-page-snapshot-toggle.ts +47 -0
  49. package/src/lib/page-snapshot/react/use-page-snapshot.ts +67 -0
  50. package/src/lib/page-snapshot/redaction/__tests__/audit.test.ts +25 -0
  51. package/src/lib/page-snapshot/redaction/__tests__/heuristics.test.ts +73 -0
  52. package/src/lib/page-snapshot/redaction/__tests__/luhn.test.ts +26 -0
  53. package/src/lib/page-snapshot/redaction/__tests__/patterns.test.ts +60 -0
  54. package/src/lib/page-snapshot/redaction/audit.ts +58 -0
  55. package/src/lib/page-snapshot/redaction/heuristics.ts +75 -0
  56. package/src/lib/page-snapshot/redaction/index.ts +75 -0
  57. package/src/lib/page-snapshot/redaction/luhn.ts +25 -0
  58. package/src/lib/page-snapshot/redaction/patterns.ts +111 -0
  59. package/src/lib/page-snapshot/refs/__tests__/registry.test.ts +24 -0
  60. package/src/lib/page-snapshot/refs/registry.ts +46 -0
  61. package/src/lib/page-snapshot/staleness/__tests__/hash.test.ts +34 -0
  62. package/src/lib/page-snapshot/staleness/hash.ts +20 -0
  63. package/src/lib/page-snapshot/tokens.ts +15 -0
  64. package/src/tools/AudioPlayer/context/PlayerProvider.tsx +13 -14
  65. package/src/tools/AudioPlayer/hooks/useAudioElementEvents.ts +55 -6
  66. package/src/tools/AudioPlayer/lazy.tsx +13 -27
  67. package/src/tools/AudioPlayer/parts/Meta/TimeDisplay.tsx +2 -5
  68. package/src/tools/Chat/README.md +267 -39
  69. package/src/tools/Chat/composer/Composer.tsx +471 -0
  70. package/src/tools/Chat/composer/ComposerActionBar.tsx +65 -0
  71. package/src/tools/Chat/composer/ComposerBanner.tsx +128 -0
  72. package/src/tools/Chat/composer/ComposerButton.tsx +64 -0
  73. package/src/tools/Chat/composer/ComposerFooter.tsx +90 -0
  74. package/src/tools/Chat/composer/ComposerMenuButton.tsx +62 -0
  75. package/src/tools/Chat/composer/ComposerModelPicker.tsx +104 -0
  76. package/src/tools/Chat/composer/ComposerRichTextarea.tsx +88 -0
  77. package/src/tools/Chat/composer/ComposerToolPill.tsx +95 -0
  78. package/src/tools/Chat/composer/index.ts +45 -0
  79. package/src/tools/Chat/composer/size-context.tsx +26 -0
  80. package/src/tools/Chat/composer/types.ts +143 -0
  81. package/src/tools/Chat/composer/useComposerActions.tsx +164 -0
  82. package/src/tools/Chat/context/ChatProvider.tsx +54 -3
  83. package/src/tools/Chat/core/__tests__/metadata.test.ts +69 -0
  84. package/src/tools/Chat/core/index.ts +23 -1
  85. package/src/tools/Chat/core/markdown.ts +1 -1
  86. package/src/tools/Chat/core/metadata.ts +47 -0
  87. package/src/tools/Chat/core/payload-dispatch.ts +1 -1
  88. package/src/tools/Chat/core/transport/http.ts +71 -32
  89. package/src/tools/Chat/core/transport/sse.ts +18 -10
  90. package/src/tools/Chat/highlight/HighlightOverlay.tsx +101 -0
  91. package/src/tools/Chat/highlight/README.md +103 -0
  92. package/src/tools/Chat/highlight/SpotlightCanvas.tsx +153 -0
  93. package/src/tools/Chat/highlight/__tests__/HighlightOverlay.test.tsx +112 -0
  94. package/src/tools/Chat/highlight/__tests__/resolveRef.test.ts +55 -0
  95. package/src/tools/Chat/highlight/index.ts +21 -0
  96. package/src/tools/Chat/highlight/resolveRef.ts +42 -0
  97. package/src/tools/Chat/highlight/types.ts +49 -0
  98. package/src/tools/Chat/highlight/useHighlightTargets.ts +128 -0
  99. package/src/tools/Chat/hooks/index.ts +0 -5
  100. package/src/tools/Chat/hooks/useAutoFocusOnStreamEnd.ts +28 -47
  101. package/src/tools/Chat/hooks/useChat.ts +47 -14
  102. package/src/tools/Chat/hooks/useChatComposer.ts +2 -2
  103. package/src/tools/Chat/hooks/useChatLayout.ts +1 -1
  104. package/src/tools/Chat/hooks/useStreamEndFocus.ts +54 -0
  105. package/src/tools/Chat/index.ts +25 -219
  106. package/src/tools/Chat/launcher/ChatDock.tsx +1 -1
  107. package/src/tools/Chat/launcher/ChatLauncher.tsx +1 -1
  108. package/src/tools/Chat/launcher/{ChatHeader.tsx → header/ChatHeader.tsx} +24 -11
  109. package/src/tools/Chat/launcher/{ChatHeaderActionButton.tsx → header/ChatHeaderActionButton.tsx} +34 -3
  110. package/src/tools/Chat/launcher/{ChatHeaderLanguageButton.tsx → header/ChatHeaderLanguageButton.tsx} +2 -2
  111. package/src/tools/Chat/launcher/{ChatHeaderModeToggle.tsx → header/ChatHeaderModeToggle.tsx} +1 -1
  112. package/src/tools/Chat/launcher/{ChatHeaderResetButton.tsx → header/ChatHeaderResetButton.tsx} +2 -1
  113. package/src/tools/Chat/launcher/{HeaderSlots.tsx → header/HeaderSlots.tsx} +3 -3
  114. package/src/tools/Chat/launcher/header/index.ts +26 -0
  115. package/src/tools/Chat/launcher/index.ts +3 -10
  116. package/src/tools/Chat/lazy.tsx +38 -284
  117. package/src/tools/Chat/{components → messages}/MessageBubble.tsx +58 -5
  118. package/src/tools/Chat/{components → messages}/MessageList.tsx +8 -25
  119. package/src/tools/Chat/messages/blocks/MessageBlocks.tsx +131 -0
  120. package/src/tools/Chat/messages/blocks/builtin.tsx +91 -0
  121. package/src/tools/Chat/messages/blocks/index.ts +12 -0
  122. package/src/tools/Chat/messages/blocks/registry.tsx +42 -0
  123. package/src/tools/Chat/messages/blocks/renderers/AudioBlock.tsx +20 -0
  124. package/src/tools/Chat/messages/blocks/renderers/CodeBlock.tsx +19 -0
  125. package/src/tools/Chat/messages/blocks/renderers/GalleryBlock.tsx +26 -0
  126. package/src/tools/Chat/messages/blocks/renderers/ImageBlock.tsx +27 -0
  127. package/src/tools/Chat/messages/blocks/renderers/JsonBlock.tsx +12 -0
  128. package/src/tools/Chat/messages/blocks/renderers/LottieBlock.tsx +11 -0
  129. package/src/tools/Chat/messages/blocks/renderers/MapBlock.tsx +36 -0
  130. package/src/tools/Chat/messages/blocks/renderers/MermaidBlock.tsx +11 -0
  131. package/src/tools/Chat/messages/blocks/renderers/VideoBlock.tsx +24 -0
  132. package/src/tools/Chat/messages/blocks/renderers/types.ts +8 -0
  133. package/src/tools/Chat/{components → messages}/index.ts +11 -5
  134. package/src/tools/Chat/public.ts +212 -0
  135. package/src/tools/Chat/shell/ChatRoot.tsx +326 -0
  136. package/src/tools/Chat/{components → shell}/EmptyState.tsx +4 -2
  137. package/src/tools/Chat/shell/index.ts +15 -0
  138. package/src/tools/Chat/types/block.ts +120 -0
  139. package/src/tools/Chat/types/config.ts +0 -5
  140. package/src/tools/Chat/types/index.ts +17 -0
  141. package/src/tools/Chat/types/message.ts +3 -0
  142. package/src/tools/Chat/utils/index.ts +4 -0
  143. package/src/tools/CodeEditor/README.md +4 -6
  144. package/src/tools/CodeEditor/components/DiffEditor.tsx +48 -13
  145. package/src/tools/CodeEditor/components/Editor.tsx +96 -44
  146. package/src/tools/CodeEditor/context/EditorProvider.tsx +34 -17
  147. package/src/tools/CodeEditor/hooks/useEditorTheme.ts +92 -99
  148. package/src/tools/CodeEditor/hooks/useMonaco.ts +37 -22
  149. package/src/tools/CodeEditor/lazy.tsx +6 -0
  150. package/src/tools/CodeEditor/lib/index.ts +1 -1
  151. package/src/tools/CodeEditor/lib/themes.ts +3 -39
  152. package/src/tools/CronScheduler/CronScheduler.client.tsx +230 -61
  153. package/src/tools/CronScheduler/components/CustomInput.tsx +21 -4
  154. package/src/tools/CronScheduler/components/DayChips.tsx +13 -11
  155. package/src/tools/CronScheduler/components/MonthDayGrid.tsx +4 -4
  156. package/src/tools/CronScheduler/components/SchedulePreview.tsx +7 -3
  157. package/src/tools/CronScheduler/components/TimeSelector.tsx +1 -1
  158. package/src/tools/CronScheduler/index.tsx +1 -1
  159. package/src/tools/CronScheduler/types/index.ts +8 -3
  160. package/src/tools/CronScheduler/utils/cron-humanize.ts +61 -16
  161. package/src/tools/CronScheduler/utils/cron-parser.ts +13 -4
  162. package/src/tools/FileIcon/FileIcon.tsx +24 -39
  163. package/src/tools/FileIcon/get-file-icon.ts +73 -0
  164. package/src/tools/FileIcon/icons/icon-data.ts +399 -0
  165. package/src/tools/FileIcon/index.ts +4 -0
  166. package/src/tools/FileIcon/loader.ts +17 -35
  167. package/src/tools/FileIcon/specialFolders.ts +18 -0
  168. package/src/tools/Gallery/components/lightbox/GalleryLightbox.tsx +112 -35
  169. package/src/tools/Gallery/components/media/GalleryVideo.tsx +21 -2
  170. package/src/tools/Gallery/components/preview/GalleryCarousel.tsx +11 -1
  171. package/src/tools/Gallery/hooks/usePreloadImages.ts +54 -7
  172. package/src/tools/ImageViewer/components/ImageInfo.tsx +12 -1
  173. package/src/tools/ImageViewer/components/ImageToolbar.tsx +51 -43
  174. package/src/tools/ImageViewer/components/ImageViewer.tsx +106 -26
  175. package/src/tools/ImageViewer/hooks/useImageLoading.ts +13 -0
  176. package/src/tools/ImageViewer/utils/constants.ts +3 -0
  177. package/src/tools/ImageViewer/utils/index.ts +1 -0
  178. package/src/tools/JsonForm/JsonSchemaForm.tsx +4 -1
  179. package/src/tools/JsonForm/templates/ArrayFieldTemplate.tsx +5 -3
  180. package/src/tools/JsonForm/templates/BaseInputTemplate.tsx +7 -4
  181. package/src/tools/JsonForm/templates/ErrorListTemplate.tsx +3 -1
  182. package/src/tools/JsonForm/templates/ObjectFieldTemplate.tsx +23 -3
  183. package/src/tools/JsonForm/widgets/ColorWidget.tsx +20 -12
  184. package/src/tools/JsonForm/widgets/NumberWidget.tsx +14 -9
  185. package/src/tools/JsonForm/widgets/RadioWidget.tsx +78 -0
  186. package/src/tools/JsonForm/widgets/SelectWidget.tsx +1 -0
  187. package/src/tools/JsonForm/widgets/SliderWidget.tsx +7 -4
  188. package/src/tools/JsonForm/widgets/TextWidget.tsx +41 -17
  189. package/src/tools/JsonForm/widgets/index.ts +1 -0
  190. package/src/tools/JsonTree/components/JsonContent.tsx +115 -40
  191. package/src/tools/LottiePlayer/LottiePlayer.client.tsx +177 -72
  192. package/src/tools/LottiePlayer/index.tsx +14 -4
  193. package/src/tools/LottiePlayer/lazy.tsx +11 -3
  194. package/src/tools/LottiePlayer/types.ts +31 -1
  195. package/src/tools/LottiePlayer/useLottie.ts +32 -9
  196. package/src/tools/LottiePlayer/usePrefersReducedMotion.ts +46 -0
  197. package/src/tools/Map/components/LayerSwitcher.tsx +54 -21
  198. package/src/tools/Map/components/MapCluster.tsx +28 -21
  199. package/src/tools/Map/components/MapContainer.tsx +11 -4
  200. package/src/tools/Map/components/MapLegend.tsx +46 -15
  201. package/src/tools/Map/components/MapMarker.tsx +31 -2
  202. package/src/tools/Map/hooks/useMapEvents.ts +64 -105
  203. package/src/tools/MarkdownEditor/MarkdownEditor.tsx +61 -6
  204. package/src/tools/MarkdownEditor/MentionList.tsx +37 -4
  205. package/src/tools/MarkdownEditor/createMentionSuggestion.ts +11 -0
  206. package/src/tools/MarkdownEditor/lazy.tsx +32 -7
  207. package/src/tools/MarkdownEditor/styles.css +13 -0
  208. package/src/tools/MarkdownMessage/CodeBlock.tsx +40 -17
  209. package/src/tools/MarkdownMessage/MarkdownMessage.tsx +26 -6
  210. package/src/tools/MarkdownMessage/components.tsx +22 -9
  211. package/src/tools/MarkdownMessage/types.ts +24 -1
  212. package/src/tools/Mermaid/Mermaid.client.tsx +27 -5
  213. package/src/tools/Mermaid/components/MermaidErrorPanel.tsx +31 -0
  214. package/src/tools/Mermaid/components/MermaidFullscreenModal.tsx +14 -17
  215. package/src/tools/Mermaid/hooks/useMermaidRenderer.ts +264 -168
  216. package/src/tools/Mermaid/hooks/useMermaidValidation.ts +76 -10
  217. package/src/tools/Mermaid/index.tsx +6 -0
  218. package/src/tools/Mermaid/utils/mermaid-helpers.ts +141 -18
  219. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/FieldRow.tsx +11 -1
  220. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/buildTree.ts +49 -20
  221. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/index.tsx +7 -0
  222. package/src/tools/OpenapiViewer/components/DocsLayout/grouping.ts +7 -4
  223. package/src/tools/OpenapiViewer/constants.ts +3 -0
  224. package/src/tools/OpenapiViewer/hooks/useOpenApiSchema.ts +73 -11
  225. package/src/tools/OpenapiViewer/utils/schemaExport.ts +26 -6
  226. package/src/tools/PrettyCode/PrettyCode.client.tsx +23 -16
  227. package/src/tools/PrettyCode/lazy.tsx +1 -1
  228. package/src/tools/SpeechRecognition/README.md +1 -1
  229. package/src/tools/SpeechRecognition/__tests__/language.test.ts +9 -3
  230. package/src/tools/SpeechRecognition/components/RecordingPulse.tsx +59 -0
  231. package/src/tools/SpeechRecognition/components/index.ts +2 -0
  232. package/src/tools/SpeechRecognition/core/engine/external.ts +24 -7
  233. package/src/tools/SpeechRecognition/core/language.ts +23 -6
  234. package/src/tools/SpeechRecognition/hooks/usePushToTalk.ts +36 -5
  235. package/src/tools/SpeechRecognition/hooks/useSpeechRecognition.ts +18 -11
  236. package/src/tools/SpeechRecognition/widgets/VoiceComposerSlot.tsx +94 -26
  237. package/src/tools/SpeechRecognition/widgets/index.ts +1 -1
  238. package/src/tools/Tree/README.md +4 -8
  239. package/src/tools/Tree/TreeRoot.tsx +22 -10
  240. package/src/tools/Tree/components/TreeContent.tsx +24 -4
  241. package/src/tools/Tree/components/TreeLabel.tsx +8 -2
  242. package/src/tools/Tree/components/TreeRow.tsx +16 -6
  243. package/src/tools/Tree/data/flatten.ts +10 -4
  244. package/src/tools/Tree/types.ts +4 -0
  245. package/src/tools/Uploader/components/UploadAddButton.tsx +29 -6
  246. package/src/tools/Uploader/components/UploadDropzone.tsx +63 -7
  247. package/src/tools/Uploader/components/UploadPageDropOverlay.tsx +19 -5
  248. package/src/tools/Uploader/components/UploadPreviewItem.tsx +47 -17
  249. package/src/tools/Uploader/components/UploadPreviewList.tsx +24 -12
  250. package/src/tools/Uploader/utils/formatters.ts +8 -3
  251. package/src/tools/VideoPlayer/README.md +87 -230
  252. package/src/tools/VideoPlayer/VideoPlayer.tsx +82 -0
  253. package/src/tools/VideoPlayer/canvas/canvas-dispatcher.tsx +34 -0
  254. package/src/tools/VideoPlayer/canvas/hls-canvas.tsx +39 -0
  255. package/src/tools/VideoPlayer/canvas/iframe-canvas.tsx +33 -0
  256. package/src/tools/VideoPlayer/canvas/index.ts +12 -0
  257. package/src/tools/VideoPlayer/canvas/jsx-augmentation.ts +47 -0
  258. package/src/tools/VideoPlayer/canvas/native-canvas.tsx +38 -0
  259. package/src/tools/VideoPlayer/canvas/vimeo-canvas.tsx +40 -0
  260. package/src/tools/VideoPlayer/canvas/youtube-canvas.tsx +78 -0
  261. package/src/tools/VideoPlayer/index.ts +51 -65
  262. package/src/tools/VideoPlayer/lazy.tsx +11 -54
  263. package/src/tools/VideoPlayer/parts/controls-bar.tsx +35 -0
  264. package/src/tools/VideoPlayer/parts/fullscreen.tsx +19 -0
  265. package/src/tools/VideoPlayer/parts/index.ts +15 -0
  266. package/src/tools/VideoPlayer/parts/pip.tsx +19 -0
  267. package/src/tools/VideoPlayer/parts/play-button.tsx +19 -0
  268. package/src/tools/VideoPlayer/parts/playback-rate.tsx +31 -0
  269. package/src/tools/VideoPlayer/parts/poster.tsx +3 -0
  270. package/src/tools/VideoPlayer/parts/seek-bar.tsx +26 -0
  271. package/src/tools/VideoPlayer/parts/volume.tsx +32 -0
  272. package/src/tools/VideoPlayer/styles/video-player.css +141 -0
  273. package/src/tools/VideoPlayer/types.ts +82 -0
  274. package/src/tools/VideoPlayer/utils/parse-embed-url.ts +70 -0
  275. package/src/tools/VideoPlayer/utils/vimeo-id.ts +24 -0
  276. package/src/tools/VideoPlayer/utils/youtube-id.ts +64 -0
  277. package/src/tools/index.ts +37 -29
  278. package/src/tools/Chat/components/AudioToggle.tsx +0 -78
  279. package/src/tools/Chat/components/ChatRoot.tsx +0 -305
  280. package/src/tools/Chat/components/Composer.tsx +0 -216
  281. package/src/tools/Chat/hooks/useChatScroll.ts +0 -145
  282. package/src/tools/Chat/types.ts +0 -9
  283. package/src/tools/JsonTree/components/JsonToolbar.tsx +0 -95
  284. package/src/tools/JsonTree/hooks/useElementCorner.ts +0 -84
  285. package/src/tools/JsonTree/hooks/useNavbarHeight.ts +0 -83
  286. package/src/tools/OpenapiViewer/components/DocsLayout/schemaFields.ts +0 -121
  287. package/src/tools/Tour/README.md +0 -373
  288. package/src/tools/Tour/components/Tour.tsx +0 -12
  289. package/src/tools/Tour/components/TourContent.tsx +0 -171
  290. package/src/tools/Tour/components/TourNavigation.tsx +0 -77
  291. package/src/tools/Tour/components/TourProgress.tsx +0 -88
  292. package/src/tools/Tour/components/TourSpotlight.tsx +0 -199
  293. package/src/tools/Tour/components/index.ts +0 -5
  294. package/src/tools/Tour/context/TourContext.ts +0 -19
  295. package/src/tools/Tour/context/TourProvider.tsx +0 -292
  296. package/src/tools/Tour/context/index.ts +0 -2
  297. package/src/tools/Tour/hooks/index.ts +0 -3
  298. package/src/tools/Tour/hooks/useKeyboardNavigation.ts +0 -59
  299. package/src/tools/Tour/hooks/useStepTarget.ts +0 -121
  300. package/src/tools/Tour/hooks/useTour.ts +0 -42
  301. package/src/tools/Tour/index.ts +0 -38
  302. package/src/tools/Tour/types/index.ts +0 -224
  303. package/src/tools/Tour/utils/dom.ts +0 -98
  304. package/src/tools/Tour/utils/index.ts +0 -3
  305. package/src/tools/Tour/utils/logger.ts +0 -3
  306. package/src/tools/Tour/utils/scrollIntoView.ts +0 -24
  307. package/src/tools/VideoPlayer/components/VideoControls.tsx +0 -138
  308. package/src/tools/VideoPlayer/components/VideoErrorFallback.tsx +0 -172
  309. package/src/tools/VideoPlayer/components/VideoPlayer.tsx +0 -201
  310. package/src/tools/VideoPlayer/components/index.ts +0 -14
  311. package/src/tools/VideoPlayer/context/VideoPlayerContext.tsx +0 -52
  312. package/src/tools/VideoPlayer/context/index.ts +0 -8
  313. package/src/tools/VideoPlayer/hooks/index.ts +0 -12
  314. package/src/tools/VideoPlayer/hooks/useVideoPlayerSettings.ts +0 -71
  315. package/src/tools/VideoPlayer/hooks/useVideoPositionCache.ts +0 -117
  316. package/src/tools/VideoPlayer/providers/NativeProvider.tsx +0 -284
  317. package/src/tools/VideoPlayer/providers/StreamProvider.tsx +0 -505
  318. package/src/tools/VideoPlayer/providers/VidstackProvider.tsx +0 -397
  319. package/src/tools/VideoPlayer/providers/index.ts +0 -8
  320. package/src/tools/VideoPlayer/types/index.ts +0 -38
  321. package/src/tools/VideoPlayer/types/player.ts +0 -116
  322. package/src/tools/VideoPlayer/types/provider.ts +0 -93
  323. package/src/tools/VideoPlayer/types/sources.ts +0 -97
  324. package/src/tools/VideoPlayer/utils/debug.ts +0 -14
  325. package/src/tools/VideoPlayer/utils/fileSource.ts +0 -78
  326. package/src/tools/VideoPlayer/utils/index.ts +0 -12
  327. package/src/tools/VideoPlayer/utils/resolvers.ts +0 -75
  328. /package/src/tools/Chat/{config.ts → constants.ts} +0 -0
  329. /package/src/tools/Chat/launcher/{ChatHeaderAudioToggle.tsx → header/ChatHeaderAudioToggle.tsx} +0 -0
  330. /package/src/tools/Chat/{components → messages}/Attachments.tsx +0 -0
  331. /package/src/tools/Chat/{components → messages}/JumpToLatest.tsx +0 -0
  332. /package/src/tools/Chat/{components → messages}/MessageActions.tsx +0 -0
  333. /package/src/tools/Chat/{components → messages}/Sources.tsx +0 -0
  334. /package/src/tools/Chat/{components → messages}/StreamingIndicator.tsx +0 -0
  335. /package/src/tools/Chat/{components → messages}/ToolCalls.tsx +0 -0
  336. /package/src/tools/Chat/{components → shell}/ErrorBanner.tsx +0 -0
@@ -0,0 +1,55 @@
1
+ // @vitest-environment jsdom
2
+ import { afterEach, describe, expect, it } from 'vitest';
3
+
4
+ import { resolveRefs, type RefResolver } from '../resolveRef';
5
+ import type { CSTRefId } from '../types';
6
+
7
+ afterEach(() => {
8
+ document.body.innerHTML = '';
9
+ });
10
+
11
+ /** A tiny resolver backed by a plain map, for tests. */
12
+ function makeResolver(map: Record<string, HTMLElement | null>): RefResolver {
13
+ return {
14
+ resolve: (ref: CSTRefId) => map[ref] ?? null,
15
+ };
16
+ }
17
+
18
+ describe('resolveRefs', () => {
19
+ it('returns nothing without a resolver', () => {
20
+ expect(resolveRefs(['@e1'], null)).toEqual([]);
21
+ });
22
+
23
+ it('resolves known refs to connected elements', () => {
24
+ const el = document.createElement('button');
25
+ document.body.appendChild(el);
26
+ const resolver = makeResolver({ '@e1': el });
27
+
28
+ const out = resolveRefs(['@e1'], resolver);
29
+ expect(out).toHaveLength(1);
30
+ expect(out[0].ref).toBe('@e1');
31
+ expect(out[0].element).toBe(el);
32
+ });
33
+
34
+ it('drops refs the resolver does not know', () => {
35
+ const resolver = makeResolver({});
36
+ expect(resolveRefs(['@e9'], resolver)).toEqual([]);
37
+ });
38
+
39
+ it('drops elements detached from the DOM', () => {
40
+ // Created but never appended — not connected.
41
+ const orphan = document.createElement('div');
42
+ const resolver = makeResolver({ '@e2': orphan });
43
+ expect(resolveRefs(['@e2'], resolver)).toEqual([]);
44
+ });
45
+
46
+ it('resolves a batch, keeping only the live ones', () => {
47
+ const live = document.createElement('a');
48
+ document.body.appendChild(live);
49
+ const orphan = document.createElement('a');
50
+ const resolver = makeResolver({ '@e1': live, '@e2': orphan, '@e3': null });
51
+
52
+ const out = resolveRefs(['@e1', '@e2', '@e3'], resolver);
53
+ expect(out.map((o) => o.ref)).toEqual(['@e1']);
54
+ });
55
+ });
@@ -0,0 +1,21 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * Chat highlight — AI-driven `point` directives.
5
+ *
6
+ * When the assistant returns `point` directives, this module resolves
7
+ * the CST refs to live elements and draws a spotlight overlay over
8
+ * them (and optionally focuses one). Read-only: it never changes the
9
+ * user's data.
10
+ */
11
+
12
+ export { HighlightOverlay, type HighlightOverlayProps } from './HighlightOverlay';
13
+ export { SpotlightCanvas, type SpotlightCanvasProps } from './SpotlightCanvas';
14
+ export { useHighlightTargets } from './useHighlightTargets';
15
+ export { resolveRefs, type RefResolver } from './resolveRef';
16
+ export type {
17
+ PointDirective,
18
+ HighlightTarget,
19
+ SpotlightRect,
20
+ CSTRefId,
21
+ } from './types';
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Resolve a CST ref id to a live DOM element.
3
+ *
4
+ * The page-snapshot engine assigns each interactive element a ref
5
+ * (`@e4`) during capture and keeps a `ref → element` registry. When the
6
+ * AI returns a `point` directive citing a ref, the chat resolves it
7
+ * back through that registry.
8
+ *
9
+ * A ref that no longer resolves is "stale" — the element was removed or
10
+ * the user navigated away. Callers treat a null result as stale.
11
+ */
12
+
13
+ import type { CSTRefId } from './types';
14
+
15
+ /**
16
+ * Minimal shape of a snapshot ref registry — `resolve(ref)` returns the
17
+ * element or null. Structurally compatible with the page-snapshot
18
+ * engine's `RefRegistry` without importing it (keeps the highlight
19
+ * module decoupled from the capture engine internals).
20
+ */
21
+ export interface RefResolver {
22
+ resolve(ref: CSTRefId): HTMLElement | null;
23
+ }
24
+
25
+ /**
26
+ * Resolve a batch of refs to elements, dropping the ones that no longer
27
+ * exist. Returns pairs so the caller keeps the ref↔element association.
28
+ */
29
+ export function resolveRefs(
30
+ refs: CSTRefId[],
31
+ resolver: RefResolver | null,
32
+ ): Array<{ ref: CSTRefId; element: HTMLElement }> {
33
+ if (!resolver) return [];
34
+ const out: Array<{ ref: CSTRefId; element: HTMLElement }> = [];
35
+ for (const ref of refs) {
36
+ const element = resolver.resolve(ref);
37
+ if (element && element.isConnected) {
38
+ out.push({ ref, element });
39
+ }
40
+ }
41
+ return out;
42
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Types for the AI-driven highlight overlay.
3
+ *
4
+ * The assistant points at the page by CST ref id; the chat resolves
5
+ * each ref to a live element and this overlay draws a spotlight on it.
6
+ */
7
+
8
+ /** A CST ref id, e.g. "@e4". */
9
+ export type CSTRefId = `@e${number}`;
10
+
11
+ /** A `point` directive as received from the backend SSE stream. */
12
+ export interface PointDirective {
13
+ type: 'point';
14
+ /** CST ref of the target element. */
15
+ ref: CSTRefId;
16
+ /** Draw the highlight overlay (default: true). */
17
+ highlight?: boolean;
18
+ /** Move focus into the element once visible (default: false). */
19
+ focus?: boolean;
20
+ /** Optional short caption shown beside the highlight. */
21
+ label?: string;
22
+ }
23
+
24
+ /**
25
+ * One resolved highlight target — a directive whose ref was found in
26
+ * the live DOM, paired with its element and measured rect.
27
+ */
28
+ export interface HighlightTarget {
29
+ /** The resolved DOM element. */
30
+ element: HTMLElement;
31
+ /** Measured bounding rect (viewport coords). */
32
+ rect: DOMRect;
33
+ /** Caption to render beside the highlight. */
34
+ label?: string;
35
+ /** Whether to move focus into the element. */
36
+ focus: boolean;
37
+ }
38
+
39
+ /**
40
+ * A geometry-only target the SVG renderer consumes — decoupled from the
41
+ * DOM element so the renderer stays a pure presentational component.
42
+ */
43
+ export interface SpotlightRect {
44
+ rect: DOMRect;
45
+ /** Extra px around the element. */
46
+ padding: number;
47
+ /** Corner radius. */
48
+ radius: number;
49
+ }
@@ -0,0 +1,128 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState } from 'react';
4
+
5
+ import { resolveRefs, type RefResolver } from './resolveRef';
6
+ import type { HighlightTarget, PointDirective } from './types';
7
+
8
+ /** Is a rect fully inside the viewport. */
9
+ function isOnScreen(rect: DOMRect): boolean {
10
+ return (
11
+ rect.top >= 0 &&
12
+ rect.left >= 0 &&
13
+ rect.bottom <= window.innerHeight &&
14
+ rect.right <= window.innerWidth
15
+ );
16
+ }
17
+
18
+ /**
19
+ * Resolve `point` directives into live, geometry-tracked highlight
20
+ * targets.
21
+ *
22
+ * - resolves each ref to a DOM element (drops stale ones);
23
+ * - measures rects and keeps them fresh on scroll / resize;
24
+ * - scrolls the first off-screen target into view;
25
+ * - moves focus into a target when the directive asked for it;
26
+ * - drops a target automatically if its element leaves the DOM.
27
+ *
28
+ * One effect owns the whole lifecycle — resolve, side-effects, and the
29
+ * geometry subscription — so there is no ordering ambiguity between
30
+ * separate effects.
31
+ */
32
+ export function useHighlightTargets(
33
+ directives: PointDirective[],
34
+ resolver: RefResolver | null,
35
+ ): HighlightTarget[] {
36
+ const [targets, setTargets] = useState<HighlightTarget[]>([]);
37
+
38
+ useEffect(() => {
39
+ // Resolve every directive that wants a highlight to a live element.
40
+ const wanted = directives.filter((d) => d.highlight !== false);
41
+ const resolved = resolveRefs(
42
+ wanted.map((d) => d.ref),
43
+ resolver,
44
+ );
45
+ const pairs = resolved
46
+ .map(({ ref, element }) => {
47
+ const directive = wanted.find((d) => d.ref === ref);
48
+ return directive ? { element, directive } : null;
49
+ })
50
+ .filter(
51
+ (p): p is { element: HTMLElement; directive: PointDirective } =>
52
+ p !== null,
53
+ );
54
+
55
+ if (pairs.length === 0) {
56
+ setTargets([]);
57
+ return;
58
+ }
59
+
60
+ // Measure all targets — called now and on every scroll / resize.
61
+ const measure = () => {
62
+ const next: HighlightTarget[] = [];
63
+ for (const { element, directive } of pairs) {
64
+ if (!element.isConnected) continue; // element left the DOM
65
+ next.push({
66
+ element,
67
+ rect: element.getBoundingClientRect(),
68
+ label: directive.label,
69
+ focus: directive.focus ?? false,
70
+ });
71
+ }
72
+ setTargets(next);
73
+ };
74
+
75
+ let frame = 0;
76
+ const schedule = () => {
77
+ cancelAnimationFrame(frame);
78
+ frame = requestAnimationFrame(measure);
79
+ };
80
+
81
+ // Scroll the first target into view if it is off-screen.
82
+ const first = pairs[0].element;
83
+ const firstOffScreen = !isOnScreen(first.getBoundingClientRect());
84
+ if (firstOffScreen) {
85
+ first.scrollIntoView({ behavior: 'smooth', block: 'center' });
86
+ }
87
+
88
+ // Focus a target if requested — after any scroll settles.
89
+ const focusPair = pairs.find((p) => p.directive.focus);
90
+ let focusTimer = 0;
91
+ if (focusPair) {
92
+ const el = focusPair.element;
93
+ focusTimer = window.setTimeout(
94
+ () => {
95
+ if (!el.isConnected) return;
96
+ // Make a non-focusable element focusable so focus() lands.
97
+ const nativelyFocusable = [
98
+ 'INPUT',
99
+ 'TEXTAREA',
100
+ 'SELECT',
101
+ 'BUTTON',
102
+ 'A',
103
+ ].includes(el.tagName);
104
+ if (el.tabIndex < 0 && !nativelyFocusable) el.tabIndex = -1;
105
+ el.focus({ preventScroll: true });
106
+ },
107
+ firstOffScreen ? 400 : 0,
108
+ );
109
+ }
110
+
111
+ // Initial measure + keep geometry fresh.
112
+ measure();
113
+ window.addEventListener('scroll', schedule, true);
114
+ window.addEventListener('resize', schedule);
115
+ const observer = new ResizeObserver(schedule);
116
+ for (const { element } of pairs) observer.observe(element);
117
+
118
+ return () => {
119
+ cancelAnimationFrame(frame);
120
+ window.clearTimeout(focusTimer);
121
+ window.removeEventListener('scroll', schedule, true);
122
+ window.removeEventListener('resize', schedule);
123
+ observer.disconnect();
124
+ };
125
+ }, [directives, resolver]);
126
+
127
+ return targets;
128
+ }
@@ -6,11 +6,6 @@ export {
6
6
  type UseChatComposerOptions,
7
7
  type UseChatComposerReturn,
8
8
  } from './useChatComposer';
9
- export {
10
- useChatScroll,
11
- type UseChatScrollOptions,
12
- type UseChatScrollReturn,
13
- } from './useChatScroll';
14
9
  export { useChatHistory, type UseChatHistoryOptions } from './useChatHistory';
15
10
  export {
16
11
  useChatLayout,
@@ -3,13 +3,9 @@
3
3
  import { type RefObject, useEffect, useRef } from 'react';
4
4
 
5
5
  import { useChatContextOptional, type ComposerHandle } from '../context';
6
+ import { useStreamEndFocus, type Focusable } from './useStreamEndFocus';
6
7
 
7
- /** Anything with a `.focus()` method — covers HTMLElement, the
8
- * composer's textareaRef from `useChatComposer`, and any custom
9
- * imperative handle exposing the same shape. */
10
- export interface Focusable {
11
- focus: () => void;
12
- }
8
+ export type { Focusable } from './useStreamEndFocus';
13
9
 
14
10
  export interface UseAutoFocusOnStreamEndOptions {
15
11
  /** True while an assistant reply is streaming. The hook fires the
@@ -43,22 +39,21 @@ export interface UseAutoFocusOnStreamEndOptions {
43
39
  * streaming. Standard chat UX: the user types → sends → reads the
44
40
  * reply → starts typing again without reaching for the mouse.
45
41
  *
46
- * Default (zero-config) usage works the moment a `<Composer>` is
47
- * mounted inside a `<ChatProvider>`:
48
- *
49
- * useAutoFocusOnStreamEnd();
42
+ * **You usually do NOT need to call this.** `<ChatProvider>` runs the
43
+ * stream-end focus internally, so every chat — `ChatRoot`, a hand-rolled
44
+ * `ChatProvider` + `Composer` layout, or a headless setup with a
45
+ * registered composer — gets it for free. Disable it provider-wide with
46
+ * `<ChatProvider autoFocusOnStreamEnd={false}>`.
50
47
  *
51
- * Custom composer / advanced wiring:
48
+ * Call this hook directly only for advanced cases the provider can't
49
+ * cover:
52
50
  *
51
+ * // focus something OTHER than the composer (an approve button):
53
52
  * const ref = useRef<{ focus: () => void } | null>(null);
54
53
  * useAutoFocusOnStreamEnd({ targetRef: ref });
55
54
  *
56
- * Driving stream state yourself:
57
- *
55
+ * // drive stream state from your own store:
58
56
  * useAutoFocusOnStreamEnd({ isStreaming: myExternalStreaming });
59
- *
60
- * Only the true → false transition fires focus — toggling `enabled`
61
- * mid-stream won't steal focus while the user is reading.
62
57
  */
63
58
  export function useAutoFocusOnStreamEnd(
64
59
  options: UseAutoFocusOnStreamEndOptions = {},
@@ -68,38 +63,19 @@ export function useAutoFocusOnStreamEnd(
68
63
  // Prefer the prop (caller knows best), fall back to context.
69
64
  const isStreaming = isStreamingProp ?? ctx?.isStreaming ?? false;
70
65
 
71
- // Keep latest ctx-composer in a ref so the focus effect always
72
- // sees the freshest registered handle without re-firing when
73
- // composers re-mount.
66
+ // Keep latest ctx-composer in a ref so target resolution always sees
67
+ // the freshest registered handle.
74
68
  const composerHandleRef = useRef<ComposerHandle | null>(null);
75
69
  composerHandleRef.current = ctx?.composer ?? null;
76
70
 
77
- const prevStreamingRef = useRef(isStreaming);
78
-
79
- useEffect(() => {
80
- const wasStreaming = prevStreamingRef.current;
81
- prevStreamingRef.current = isStreaming;
82
-
83
- if (!enabled) return;
84
- if (!(wasStreaming && !isStreaming)) return;
85
-
86
- const focusNow = () => {
87
- // Resolve target in priority order: explicit ref > registered
88
- // composer handle. Refs may carry an HTMLElement (raw DOM) or
89
- // any object with `.focus()`; both are handled by the same
90
- // call site below.
91
- const explicit = targetRef?.current as Focusable | null;
92
- const target: Focusable | null = explicit ?? composerHandleRef.current;
93
- target?.focus();
94
- };
95
-
96
- if (delayMs > 0) {
97
- const id = window.setTimeout(focusNow, delayMs);
98
- return () => window.clearTimeout(id);
99
- }
100
- const raf = requestAnimationFrame(focusNow);
101
- return () => cancelAnimationFrame(raf);
102
- }, [isStreaming, enabled, delayMs, targetRef]);
71
+ useStreamEndFocus({
72
+ isStreaming,
73
+ enabled,
74
+ delayMs,
75
+ // Resolve in priority order: explicit ref > registered composer.
76
+ resolveTarget: () =>
77
+ (targetRef?.current as Focusable | null) ?? composerHandleRef.current,
78
+ });
103
79
  }
104
80
 
105
81
  /**
@@ -122,9 +98,14 @@ export function useRegisterComposer(handle: ComposerHandle): void {
122
98
  const register = ctx?.registerComposer;
123
99
  const focus = handle.focus;
124
100
  const moveCursorToEnd = handle.moveCursorToEnd;
101
+ // Forward `getValue/setValue` too — voice dictation reads/writes the
102
+ // draft through them, so dropping them silently broke dictation for
103
+ // custom composers (e.g. the TipTap MarkdownEditor wrapper).
104
+ const getValue = handle.getValue;
105
+ const setValue = handle.setValue;
125
106
  useEffect(() => {
126
107
  if (!register) return;
127
- register({ focus, moveCursorToEnd });
108
+ register({ focus, moveCursorToEnd, getValue, setValue });
128
109
  return () => register(null);
129
- }, [register, focus, moveCursorToEnd]);
110
+ }, [register, focus, moveCursorToEnd, getValue, setValue]);
130
111
  }
@@ -10,7 +10,7 @@ import type {
10
10
  ChatTransport,
11
11
  ChatToolCall,
12
12
  } from '../types';
13
- import { LIMITS } from '../config';
13
+ import { LIMITS } from '../constants';
14
14
  import {
15
15
  type ChatState,
16
16
  initialState,
@@ -20,6 +20,7 @@ import {
20
20
  import { createId } from '../core/ids';
21
21
  import { getChatLogger } from '../core/logger';
22
22
  import { createTokenBuffer } from '../core/markdown';
23
+ import { resolveSendMetadata } from '../core/metadata';
23
24
 
24
25
  export interface UseChatConfig {
25
26
  transport: ChatTransport;
@@ -47,6 +48,17 @@ export interface UseChatConfig {
47
48
  * sees plain text, while the bubble keeps the chip rendering. Plan64.
48
49
  */
49
50
  onBeforeSend?: (content: string) => string | Promise<string>;
51
+ /**
52
+ * Contribute extra transport metadata, computed fresh at send time.
53
+ * Invoked synchronously right before each `transport.stream/send`,
54
+ * and the result is merged over the static `metadata`. Returning
55
+ * `undefined` adds nothing.
56
+ *
57
+ * Use case: the page-context snapshot — captured per message
58
+ * (capture-on-submit) and carried as a separate `metadata` field,
59
+ * never mixed into the message content.
60
+ */
61
+ getDynamicMetadata?: () => Record<string, unknown> | undefined;
50
62
  /**
51
63
  * Enable verbose dev-mode logging (consola, namespace `chat:*`).
52
64
  * Defaults to `isDev` from `@djangocfg/ui-core/lib`. Pass `false` to silence
@@ -228,11 +240,16 @@ export function useChat(config: UseChatConfig): UseChatReturn {
228
240
  abortRef.current = ctrl;
229
241
  const assistantId = createId('a');
230
242
  streamingMsgIdRef.current = assistantId;
243
+ // The message id that subsequent events (chunks, tool calls, done)
244
+ // target. Starts as the freshly-created placeholder; on a
245
+ // `resume_start` it is repointed to the resumed assistant message
246
+ // so STREAM_DONE / TOOL_CALL_* don't land on a removed placeholder.
247
+ let targetId = assistantId;
231
248
 
232
249
  const iterator = transport.stream(sessionId, content, {
233
250
  signal: ctrl.signal,
234
251
  attachments,
235
- metadata: config.metadata,
252
+ metadata: resolveSendMetadata(config.metadata, config.getDynamicMetadata),
236
253
  });
237
254
 
238
255
  // Peek at the first event — if it's `resume_start` we reuse the last assistant
@@ -264,6 +281,19 @@ export function useChat(config: UseChatConfig): UseChatReturn {
264
281
  // just-created empty placeholder and mark the last assistant msg streaming.
265
282
  dispatch({ type: 'STREAM_CANCEL_PLACEHOLDER', id: assistantId });
266
283
  dispatch({ type: 'STREAM_RESUME_EXISTING' });
284
+ // Repoint the event target at the resumed message. Without
285
+ // this, every subsequent chunk / tool call / message_end
286
+ // dispatches against the removed placeholder id and is lost
287
+ // — the resumed bubble would stream forever (isStreaming
288
+ // never cleared) and tool panels would never appear.
289
+ for (let i = stateRef.current.messages.length - 1; i >= 0; i -= 1) {
290
+ const m = stateRef.current.messages[i];
291
+ if (m.role === 'assistant' && m.isStreaming) {
292
+ targetId = m.id;
293
+ streamingMsgIdRef.current = m.id;
294
+ break;
295
+ }
296
+ }
267
297
  } else {
268
298
  peekedEvent = ev;
269
299
  }
@@ -279,10 +309,10 @@ export function useChat(config: UseChatConfig): UseChatReturn {
279
309
 
280
310
  // If transport never emitted message_end, finalize manually.
281
311
  if (stateRef.current.isStreaming) {
282
- dispatch({ type: 'STREAM_DONE', id: assistantId });
312
+ dispatch({ type: 'STREAM_DONE', id: targetId });
283
313
  }
284
314
 
285
- const finalMsg = stateRef.current.messages.find((m) => m.id === assistantId);
315
+ const finalMsg = stateRef.current.messages.find((m) => m.id === targetId);
286
316
  if (finalMsg) config.onMessageEnd?.(finalMsg);
287
317
  log.stream.success('done', {
288
318
  assistantId,
@@ -294,14 +324,14 @@ export function useChat(config: UseChatConfig): UseChatReturn {
294
324
  tokenBuffer.close();
295
325
  if (ctrl.signal.aborted) {
296
326
  const partial =
297
- stateRef.current.messages.find((m) => m.id === assistantId)?.content ?? '';
298
- dispatch({ type: 'STREAM_CANCELLED', id: assistantId, partialText: partial });
299
- log.stream.warn('cancelled', { assistantId, partialChars: partial.length });
327
+ stateRef.current.messages.find((m) => m.id === targetId)?.content ?? '';
328
+ dispatch({ type: 'STREAM_CANCELLED', id: targetId, partialText: partial });
329
+ log.stream.warn('cancelled', { assistantId: targetId, partialChars: partial.length });
300
330
  return;
301
331
  }
302
332
  const e = err instanceof Error ? err : new Error(String(err));
303
333
  lastErrorRef.current = e;
304
- dispatch({ type: 'STREAM_ERROR', id: assistantId, message: e.message });
334
+ dispatch({ type: 'STREAM_ERROR', id: targetId, message: e.message });
305
335
  config.onError?.(e);
306
336
  log.error.error('stream failed', { assistantId, message: e.message });
307
337
  } finally {
@@ -338,7 +368,7 @@ export function useChat(config: UseChatConfig): UseChatReturn {
338
368
  };
339
369
  dispatch({
340
370
  type: 'TOOL_CALL_START',
341
- messageId: assistantId,
371
+ messageId: targetId,
342
372
  toolCall,
343
373
  });
344
374
  log.tools.info('call_start', { toolId: ev.toolId, name: ev.name });
@@ -347,7 +377,7 @@ export function useChat(config: UseChatConfig): UseChatReturn {
347
377
  case 'tool_call_delta':
348
378
  dispatch({
349
379
  type: 'TOOL_CALL_DELTA',
350
- messageId: assistantId,
380
+ messageId: targetId,
351
381
  toolId: ev.toolId,
352
382
  delta: ev.delta,
353
383
  });
@@ -355,7 +385,7 @@ export function useChat(config: UseChatConfig): UseChatReturn {
355
385
  case 'tool_call_end':
356
386
  dispatch({
357
387
  type: 'TOOL_CALL_END',
358
- messageId: assistantId,
388
+ messageId: targetId,
359
389
  toolId: ev.toolId,
360
390
  output: ev.output,
361
391
  status: ev.status,
@@ -366,7 +396,7 @@ export function useChat(config: UseChatConfig): UseChatReturn {
366
396
  tokenBuffer.flush();
367
397
  dispatch({
368
398
  type: 'STREAM_DONE',
369
- id: assistantId,
399
+ id: targetId,
370
400
  tokensIn: ev.tokensIn,
371
401
  tokensOut: ev.tokensOut,
372
402
  sources: ev.sources,
@@ -381,7 +411,7 @@ export function useChat(config: UseChatConfig): UseChatReturn {
381
411
  tokenBuffer.flush();
382
412
  dispatch({
383
413
  type: 'STREAM_ERROR',
384
- id: assistantId,
414
+ id: targetId,
385
415
  message: ev.message,
386
416
  });
387
417
  log.error.error('stream event error', { code: ev.code, message: ev.message });
@@ -402,7 +432,7 @@ export function useChat(config: UseChatConfig): UseChatReturn {
402
432
  const reply = await transport.send(sessionId, content, {
403
433
  signal: ctrl.signal,
404
434
  attachments,
405
- metadata: config.metadata,
435
+ metadata: resolveSendMetadata(config.metadata, config.getDynamicMetadata),
406
436
  });
407
437
  const placeholderId = createId('a');
408
438
  dispatch({ type: 'STREAM_START', id: placeholderId });
@@ -411,6 +441,9 @@ export function useChat(config: UseChatConfig): UseChatReturn {
411
441
  dispatch({ type: 'STREAM_DONE', id: placeholderId });
412
442
  config.onMessageEnd?.(reply);
413
443
  } catch (err) {
444
+ // A user-initiated cancel (cancelStream / newSession) aborts the
445
+ // controller — that's not an error, so don't raise the banner.
446
+ if (ctrl.signal.aborted) return;
414
447
  const e = err instanceof Error ? err : new Error(String(err));
415
448
  lastErrorRef.current = e;
416
449
  dispatch({ type: 'STREAM_ERROR', message: e.message });
@@ -12,8 +12,8 @@ import {
12
12
  } from 'react';
13
13
 
14
14
  import type { ChatAttachment } from '../types';
15
- import { LIMITS } from '../config';
16
- import { sanitizeDraft } from '../utils/sanitizeDraft';
15
+ import { LIMITS } from '../constants';
16
+ import { sanitizeDraft } from '../utils';
17
17
 
18
18
  export interface UseChatComposerOptions {
19
19
  onSubmit: (content: string, attachments: ChatAttachment[]) => void | Promise<void>;
@@ -4,7 +4,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
4
4
 
5
5
  import { useLocalStorage, useMediaQuery } from '@djangocfg/ui-core/hooks';
6
6
 
7
- import { CSS_VARS, DEFAULT_SIDEBAR, STORAGE_KEYS } from '../config';
7
+ import { CSS_VARS, DEFAULT_SIDEBAR, STORAGE_KEYS } from '../constants';
8
8
  import type { ChatDisplayMode } from '../types';
9
9
 
10
10
  export interface UseChatLayoutConfig {
@@ -0,0 +1,54 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useRef } from 'react';
4
+
5
+ /** Anything with a `.focus()` method. */
6
+ export interface Focusable {
7
+ focus: () => void;
8
+ }
9
+
10
+ /**
11
+ * Core stream-end focus effect — context-free on purpose so it can run
12
+ * *inside* `ChatProvider` without an import cycle (`context` →
13
+ * `hooks/useAutoFocusOnStreamEnd` → `context`).
14
+ *
15
+ * Fires `resolveTarget().focus()` on the streaming true → false edge.
16
+ * Both `ChatProvider` and the public `useAutoFocusOnStreamEnd` hook
17
+ * delegate here.
18
+ */
19
+ export function useStreamEndFocus(params: {
20
+ isStreaming: boolean;
21
+ enabled: boolean;
22
+ delayMs: number;
23
+ /** Resolves the focus target lazily, at fire time — so it always
24
+ * sees the freshest registered composer handle. */
25
+ resolveTarget: () => Focusable | null;
26
+ }): void {
27
+ const { isStreaming, enabled, delayMs, resolveTarget } = params;
28
+
29
+ // Keep the resolver in a ref so the effect doesn't re-fire when the
30
+ // caller passes a fresh closure each render.
31
+ const resolveRef = useRef(resolveTarget);
32
+ resolveRef.current = resolveTarget;
33
+
34
+ const prevStreamingRef = useRef(isStreaming);
35
+
36
+ useEffect(() => {
37
+ const wasStreaming = prevStreamingRef.current;
38
+ prevStreamingRef.current = isStreaming;
39
+
40
+ if (!enabled) return;
41
+ // Only the true → false transition fires focus — toggling `enabled`
42
+ // mid-stream won't steal focus while the user is reading.
43
+ if (!(wasStreaming && !isStreaming)) return;
44
+
45
+ const focusNow = () => resolveRef.current()?.focus();
46
+
47
+ if (delayMs > 0) {
48
+ const id = window.setTimeout(focusNow, delayMs);
49
+ return () => window.clearTimeout(id);
50
+ }
51
+ const raf = requestAnimationFrame(focusNow);
52
+ return () => cancelAnimationFrame(raf);
53
+ }, [isStreaming, enabled, delayMs]);
54
+ }