@djangocfg/ui-tools 2.1.407 → 2.1.409

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 (297) hide show
  1. package/README.md +9 -10
  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 +8 -13
  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/parts/Meta/TimeDisplay.tsx +2 -5
  67. package/src/tools/Chat/README.md +277 -39
  68. package/src/tools/Chat/composer/Composer.tsx +471 -0
  69. package/src/tools/Chat/composer/ComposerActionBar.tsx +65 -0
  70. package/src/tools/Chat/composer/ComposerBanner.tsx +128 -0
  71. package/src/tools/Chat/composer/ComposerButton.tsx +64 -0
  72. package/src/tools/Chat/composer/ComposerFooter.tsx +90 -0
  73. package/src/tools/Chat/composer/ComposerMenuButton.tsx +62 -0
  74. package/src/tools/Chat/composer/ComposerModelPicker.tsx +104 -0
  75. package/src/tools/Chat/composer/ComposerRichTextarea.tsx +88 -0
  76. package/src/tools/Chat/composer/ComposerToolPill.tsx +95 -0
  77. package/src/tools/Chat/composer/index.ts +45 -0
  78. package/src/tools/Chat/composer/size-context.tsx +26 -0
  79. package/src/tools/Chat/composer/types.ts +143 -0
  80. package/src/tools/Chat/composer/useComposerActions.tsx +164 -0
  81. package/src/tools/Chat/context/ChatProvider.tsx +54 -3
  82. package/src/tools/Chat/core/__tests__/metadata.test.ts +69 -0
  83. package/src/tools/Chat/core/index.ts +23 -1
  84. package/src/tools/Chat/core/markdown.ts +1 -1
  85. package/src/tools/Chat/core/metadata.ts +47 -0
  86. package/src/tools/Chat/core/payload-dispatch.ts +1 -1
  87. package/src/tools/Chat/core/transport/http.ts +71 -32
  88. package/src/tools/Chat/core/transport/sse.ts +18 -10
  89. package/src/tools/Chat/highlight/HighlightOverlay.tsx +101 -0
  90. package/src/tools/Chat/highlight/README.md +103 -0
  91. package/src/tools/Chat/highlight/SpotlightCanvas.tsx +153 -0
  92. package/src/tools/Chat/highlight/__tests__/HighlightOverlay.test.tsx +112 -0
  93. package/src/tools/Chat/highlight/__tests__/resolveRef.test.ts +55 -0
  94. package/src/tools/Chat/highlight/index.ts +21 -0
  95. package/src/tools/Chat/highlight/resolveRef.ts +42 -0
  96. package/src/tools/Chat/highlight/types.ts +49 -0
  97. package/src/tools/Chat/highlight/useHighlightTargets.ts +128 -0
  98. package/src/tools/Chat/hooks/index.ts +0 -5
  99. package/src/tools/Chat/hooks/useAutoFocusOnStreamEnd.ts +28 -47
  100. package/src/tools/Chat/hooks/useChat.ts +47 -14
  101. package/src/tools/Chat/hooks/useChatComposer.ts +2 -2
  102. package/src/tools/Chat/hooks/useChatLayout.ts +1 -1
  103. package/src/tools/Chat/hooks/useStreamEndFocus.ts +54 -0
  104. package/src/tools/Chat/index.ts +25 -219
  105. package/src/tools/Chat/launcher/ChatDock.tsx +1 -1
  106. package/src/tools/Chat/launcher/ChatLauncher.tsx +1 -1
  107. package/src/tools/Chat/launcher/{ChatHeader.tsx → header/ChatHeader.tsx} +24 -11
  108. package/src/tools/Chat/launcher/{ChatHeaderActionButton.tsx → header/ChatHeaderActionButton.tsx} +34 -3
  109. package/src/tools/Chat/launcher/{ChatHeaderLanguageButton.tsx → header/ChatHeaderLanguageButton.tsx} +2 -2
  110. package/src/tools/Chat/launcher/{ChatHeaderModeToggle.tsx → header/ChatHeaderModeToggle.tsx} +1 -1
  111. package/src/tools/Chat/launcher/{ChatHeaderResetButton.tsx → header/ChatHeaderResetButton.tsx} +2 -1
  112. package/src/tools/Chat/launcher/{HeaderSlots.tsx → header/HeaderSlots.tsx} +3 -3
  113. package/src/tools/Chat/launcher/header/index.ts +26 -0
  114. package/src/tools/Chat/launcher/index.ts +3 -10
  115. package/src/tools/Chat/lazy.tsx +38 -284
  116. package/src/tools/Chat/{components → messages}/MessageBubble.tsx +58 -5
  117. package/src/tools/Chat/{components → messages}/MessageList.tsx +8 -25
  118. package/src/tools/Chat/messages/blocks/MessageBlocks.tsx +131 -0
  119. package/src/tools/Chat/messages/blocks/builtin.tsx +91 -0
  120. package/src/tools/Chat/messages/blocks/index.ts +12 -0
  121. package/src/tools/Chat/messages/blocks/registry.tsx +42 -0
  122. package/src/tools/Chat/messages/blocks/renderers/AudioBlock.tsx +20 -0
  123. package/src/tools/Chat/messages/blocks/renderers/CodeBlock.tsx +19 -0
  124. package/src/tools/Chat/messages/blocks/renderers/GalleryBlock.tsx +26 -0
  125. package/src/tools/Chat/messages/blocks/renderers/ImageBlock.tsx +27 -0
  126. package/src/tools/Chat/messages/blocks/renderers/JsonBlock.tsx +12 -0
  127. package/src/tools/Chat/messages/blocks/renderers/LottieBlock.tsx +11 -0
  128. package/src/tools/Chat/messages/blocks/renderers/MapBlock.tsx +36 -0
  129. package/src/tools/Chat/messages/blocks/renderers/MermaidBlock.tsx +11 -0
  130. package/src/tools/Chat/messages/blocks/renderers/VideoBlock.tsx +24 -0
  131. package/src/tools/Chat/messages/blocks/renderers/types.ts +8 -0
  132. package/src/tools/Chat/{components → messages}/index.ts +11 -5
  133. package/src/tools/Chat/public.ts +212 -0
  134. package/src/tools/Chat/shell/ChatRoot.tsx +345 -0
  135. package/src/tools/Chat/{components → shell}/EmptyState.tsx +4 -2
  136. package/src/tools/Chat/shell/index.ts +15 -0
  137. package/src/tools/Chat/types/block.ts +120 -0
  138. package/src/tools/Chat/types/config.ts +0 -5
  139. package/src/tools/Chat/types/index.ts +17 -0
  140. package/src/tools/Chat/types/message.ts +3 -0
  141. package/src/tools/Chat/utils/index.ts +4 -0
  142. package/src/tools/CodeEditor/README.md +4 -6
  143. package/src/tools/CodeEditor/components/DiffEditor.tsx +48 -13
  144. package/src/tools/CodeEditor/components/Editor.tsx +96 -44
  145. package/src/tools/CodeEditor/context/EditorProvider.tsx +34 -17
  146. package/src/tools/CodeEditor/hooks/useEditorTheme.ts +92 -99
  147. package/src/tools/CodeEditor/hooks/useMonaco.ts +37 -22
  148. package/src/tools/CodeEditor/lazy.tsx +6 -0
  149. package/src/tools/CodeEditor/lib/index.ts +1 -1
  150. package/src/tools/CodeEditor/lib/themes.ts +3 -39
  151. package/src/tools/CronScheduler/CronScheduler.client.tsx +230 -61
  152. package/src/tools/CronScheduler/components/CustomInput.tsx +21 -4
  153. package/src/tools/CronScheduler/components/DayChips.tsx +13 -11
  154. package/src/tools/CronScheduler/components/MonthDayGrid.tsx +4 -4
  155. package/src/tools/CronScheduler/components/SchedulePreview.tsx +7 -3
  156. package/src/tools/CronScheduler/components/TimeSelector.tsx +1 -1
  157. package/src/tools/CronScheduler/index.tsx +1 -1
  158. package/src/tools/CronScheduler/types/index.ts +8 -3
  159. package/src/tools/CronScheduler/utils/cron-humanize.ts +61 -16
  160. package/src/tools/CronScheduler/utils/cron-parser.ts +13 -4
  161. package/src/tools/FileIcon/FileIcon.tsx +24 -39
  162. package/src/tools/FileIcon/get-file-icon.ts +73 -0
  163. package/src/tools/FileIcon/icons/icon-data.ts +399 -0
  164. package/src/tools/FileIcon/index.ts +4 -0
  165. package/src/tools/FileIcon/loader.ts +17 -35
  166. package/src/tools/FileIcon/specialFolders.ts +18 -0
  167. package/src/tools/Gallery/components/lightbox/GalleryLightbox.tsx +112 -35
  168. package/src/tools/Gallery/components/media/GalleryVideo.tsx +21 -2
  169. package/src/tools/Gallery/components/preview/GalleryCarousel.tsx +11 -1
  170. package/src/tools/Gallery/hooks/usePreloadImages.ts +54 -7
  171. package/src/tools/ImageViewer/components/ImageInfo.tsx +12 -1
  172. package/src/tools/ImageViewer/components/ImageToolbar.tsx +51 -43
  173. package/src/tools/ImageViewer/components/ImageViewer.tsx +96 -24
  174. package/src/tools/ImageViewer/hooks/useImageLoading.ts +13 -0
  175. package/src/tools/ImageViewer/utils/constants.ts +3 -0
  176. package/src/tools/ImageViewer/utils/index.ts +1 -0
  177. package/src/tools/JsonForm/JsonSchemaForm.tsx +4 -1
  178. package/src/tools/JsonForm/templates/ArrayFieldTemplate.tsx +5 -3
  179. package/src/tools/JsonForm/templates/BaseInputTemplate.tsx +7 -4
  180. package/src/tools/JsonForm/templates/ErrorListTemplate.tsx +3 -1
  181. package/src/tools/JsonForm/templates/ObjectFieldTemplate.tsx +23 -3
  182. package/src/tools/JsonForm/widgets/ColorWidget.tsx +20 -12
  183. package/src/tools/JsonForm/widgets/NumberWidget.tsx +14 -9
  184. package/src/tools/JsonForm/widgets/RadioWidget.tsx +78 -0
  185. package/src/tools/JsonForm/widgets/SelectWidget.tsx +1 -0
  186. package/src/tools/JsonForm/widgets/SliderWidget.tsx +7 -4
  187. package/src/tools/JsonForm/widgets/TextWidget.tsx +41 -17
  188. package/src/tools/JsonForm/widgets/index.ts +1 -0
  189. package/src/tools/JsonTree/components/JsonContent.tsx +115 -40
  190. package/src/tools/LottiePlayer/LottiePlayer.client.tsx +177 -72
  191. package/src/tools/LottiePlayer/index.tsx +14 -4
  192. package/src/tools/LottiePlayer/lazy.tsx +11 -3
  193. package/src/tools/LottiePlayer/types.ts +31 -1
  194. package/src/tools/LottiePlayer/useLottie.ts +32 -9
  195. package/src/tools/LottiePlayer/usePrefersReducedMotion.ts +46 -0
  196. package/src/tools/Map/components/LayerSwitcher.tsx +54 -21
  197. package/src/tools/Map/components/MapCluster.tsx +28 -21
  198. package/src/tools/Map/components/MapContainer.tsx +11 -4
  199. package/src/tools/Map/components/MapLegend.tsx +46 -15
  200. package/src/tools/Map/components/MapMarker.tsx +31 -2
  201. package/src/tools/Map/hooks/useMapEvents.ts +64 -105
  202. package/src/tools/MarkdownEditor/MarkdownEditor.tsx +61 -6
  203. package/src/tools/MarkdownEditor/MentionList.tsx +37 -4
  204. package/src/tools/MarkdownEditor/createMentionSuggestion.ts +11 -0
  205. package/src/tools/MarkdownEditor/lazy.tsx +32 -7
  206. package/src/tools/MarkdownEditor/styles.css +13 -0
  207. package/src/tools/MarkdownMessage/CodeBlock.tsx +40 -17
  208. package/src/tools/MarkdownMessage/MarkdownMessage.tsx +26 -6
  209. package/src/tools/MarkdownMessage/components.tsx +22 -9
  210. package/src/tools/MarkdownMessage/types.ts +24 -1
  211. package/src/tools/Mermaid/Mermaid.client.tsx +27 -5
  212. package/src/tools/Mermaid/components/MermaidErrorPanel.tsx +31 -0
  213. package/src/tools/Mermaid/components/MermaidFullscreenModal.tsx +14 -17
  214. package/src/tools/Mermaid/hooks/useMermaidRenderer.ts +264 -168
  215. package/src/tools/Mermaid/hooks/useMermaidValidation.ts +76 -10
  216. package/src/tools/Mermaid/index.tsx +6 -0
  217. package/src/tools/Mermaid/utils/mermaid-helpers.ts +141 -18
  218. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/FieldRow.tsx +11 -1
  219. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/buildTree.ts +49 -20
  220. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/index.tsx +7 -0
  221. package/src/tools/OpenapiViewer/components/DocsLayout/grouping.ts +7 -4
  222. package/src/tools/OpenapiViewer/constants.ts +3 -0
  223. package/src/tools/OpenapiViewer/hooks/useOpenApiSchema.ts +73 -11
  224. package/src/tools/OpenapiViewer/utils/schemaExport.ts +26 -6
  225. package/src/tools/PrettyCode/PrettyCode.client.tsx +23 -16
  226. package/src/tools/PrettyCode/lazy.tsx +1 -1
  227. package/src/tools/SpeechRecognition/README.md +1 -1
  228. package/src/tools/SpeechRecognition/__tests__/language.test.ts +9 -3
  229. package/src/tools/SpeechRecognition/components/RecordingPulse.tsx +59 -0
  230. package/src/tools/SpeechRecognition/components/index.ts +2 -0
  231. package/src/tools/SpeechRecognition/core/engine/external.ts +24 -7
  232. package/src/tools/SpeechRecognition/core/language.ts +23 -6
  233. package/src/tools/SpeechRecognition/hooks/usePushToTalk.ts +36 -5
  234. package/src/tools/SpeechRecognition/hooks/useSpeechRecognition.ts +18 -11
  235. package/src/tools/SpeechRecognition/widgets/VoiceComposerSlot.tsx +94 -26
  236. package/src/tools/SpeechRecognition/widgets/index.ts +1 -1
  237. package/src/tools/Tree/README.md +4 -8
  238. package/src/tools/Tree/TreeRoot.tsx +22 -10
  239. package/src/tools/Tree/components/TreeContent.tsx +24 -4
  240. package/src/tools/Tree/components/TreeLabel.tsx +8 -2
  241. package/src/tools/Tree/components/TreeRow.tsx +16 -6
  242. package/src/tools/Tree/data/flatten.ts +10 -4
  243. package/src/tools/Tree/types.ts +4 -0
  244. package/src/tools/Uploader/components/UploadAddButton.tsx +29 -6
  245. package/src/tools/Uploader/components/UploadDropzone.tsx +63 -7
  246. package/src/tools/Uploader/components/UploadPageDropOverlay.tsx +19 -5
  247. package/src/tools/Uploader/components/UploadPreviewItem.tsx +47 -17
  248. package/src/tools/Uploader/components/UploadPreviewList.tsx +24 -12
  249. package/src/tools/Uploader/utils/formatters.ts +8 -3
  250. package/src/tools/VideoPlayer/canvas/hls-canvas.tsx +1 -0
  251. package/src/tools/VideoPlayer/canvas/{jsx.d.ts → jsx-augmentation.ts} +12 -19
  252. package/src/tools/VideoPlayer/canvas/vimeo-canvas.tsx +1 -0
  253. package/src/tools/VideoPlayer/canvas/youtube-canvas.tsx +1 -0
  254. package/src/tools/VideoPlayer/parts/fullscreen.tsx +1 -1
  255. package/src/tools/VideoPlayer/parts/pip.tsx +1 -1
  256. package/src/tools/VideoPlayer/parts/playback-rate.tsx +1 -1
  257. package/src/tools/VideoPlayer/parts/seek-bar.tsx +2 -2
  258. package/src/tools/VideoPlayer/parts/volume.tsx +2 -2
  259. package/src/tools/index.ts +2 -1
  260. package/src/tools/Chat/components/AudioToggle.tsx +0 -78
  261. package/src/tools/Chat/components/ChatRoot.tsx +0 -305
  262. package/src/tools/Chat/components/Composer.tsx +0 -216
  263. package/src/tools/Chat/hooks/useChatScroll.ts +0 -145
  264. package/src/tools/Chat/types.ts +0 -9
  265. package/src/tools/JsonTree/components/JsonToolbar.tsx +0 -95
  266. package/src/tools/JsonTree/hooks/useElementCorner.ts +0 -84
  267. package/src/tools/JsonTree/hooks/useNavbarHeight.ts +0 -83
  268. package/src/tools/OpenapiViewer/components/DocsLayout/schemaFields.ts +0 -121
  269. package/src/tools/Tour/README.md +0 -373
  270. package/src/tools/Tour/components/Tour.tsx +0 -12
  271. package/src/tools/Tour/components/TourContent.tsx +0 -171
  272. package/src/tools/Tour/components/TourNavigation.tsx +0 -77
  273. package/src/tools/Tour/components/TourProgress.tsx +0 -88
  274. package/src/tools/Tour/components/TourSpotlight.tsx +0 -199
  275. package/src/tools/Tour/components/index.ts +0 -5
  276. package/src/tools/Tour/context/TourContext.ts +0 -19
  277. package/src/tools/Tour/context/TourProvider.tsx +0 -292
  278. package/src/tools/Tour/context/index.ts +0 -2
  279. package/src/tools/Tour/hooks/index.ts +0 -3
  280. package/src/tools/Tour/hooks/useKeyboardNavigation.ts +0 -59
  281. package/src/tools/Tour/hooks/useStepTarget.ts +0 -121
  282. package/src/tools/Tour/hooks/useTour.ts +0 -42
  283. package/src/tools/Tour/index.ts +0 -38
  284. package/src/tools/Tour/types/index.ts +0 -224
  285. package/src/tools/Tour/utils/dom.ts +0 -98
  286. package/src/tools/Tour/utils/index.ts +0 -3
  287. package/src/tools/Tour/utils/logger.ts +0 -3
  288. package/src/tools/Tour/utils/scrollIntoView.ts +0 -24
  289. /package/src/tools/Chat/{config.ts → constants.ts} +0 -0
  290. /package/src/tools/Chat/launcher/{ChatHeaderAudioToggle.tsx → header/ChatHeaderAudioToggle.tsx} +0 -0
  291. /package/src/tools/Chat/{components → messages}/Attachments.tsx +0 -0
  292. /package/src/tools/Chat/{components → messages}/JumpToLatest.tsx +0 -0
  293. /package/src/tools/Chat/{components → messages}/MessageActions.tsx +0 -0
  294. /package/src/tools/Chat/{components → messages}/Sources.tsx +0 -0
  295. /package/src/tools/Chat/{components → messages}/StreamingIndicator.tsx +0 -0
  296. /package/src/tools/Chat/{components → messages}/ToolCalls.tsx +0 -0
  297. /package/src/tools/Chat/{components → shell}/ErrorBanner.tsx +0 -0
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Compact Semantic Tree (CST) — the snapshot representation.
3
+ *
4
+ * A token-efficient typed tree of a rendered page: interactive elements
5
+ * carry short stable ref ids, containers give structure, prose collapses
6
+ * into Markdown text nodes. ~90–95% smaller than raw HTML.
7
+ *
8
+ */
9
+
10
+ /** Short stable ref of an interactive CST node, e.g. "@e4". */
11
+ export type CSTRefId = `@e${number}`;
12
+
13
+ /** Roles an interactive node can take. */
14
+ export type CSTInteractiveRole =
15
+ | 'button'
16
+ | 'link'
17
+ | 'textbox'
18
+ | 'checkbox'
19
+ | 'radio'
20
+ | 'combobox'
21
+ | 'select'
22
+ | 'textarea'
23
+ | 'slider'
24
+ | 'spinbutton'
25
+ | 'searchbox'
26
+ | 'switch'
27
+ | 'tab'
28
+ | 'menuitem'
29
+ | 'dialog';
30
+
31
+ /** Roles a container node can take. */
32
+ export type CSTContainerRole =
33
+ | 'form'
34
+ | 'table'
35
+ | 'list'
36
+ | 'section'
37
+ | 'grid'
38
+ | 'navigation'
39
+ | 'region';
40
+
41
+ /** Discriminated union of every CST node kind. */
42
+ export type CSTNode =
43
+ | CSTRootNode
44
+ | CSTInteractiveNode
45
+ | CSTContainerNode
46
+ | CSTTextNode;
47
+
48
+ /** Root of a snapshot tree. */
49
+ export interface CSTRootNode {
50
+ type: 'root';
51
+ /** Page title (document.title). */
52
+ title: string;
53
+ /** Page URL at capture time. */
54
+ url: string;
55
+ children: CSTNode[];
56
+ }
57
+
58
+ /**
59
+ * An interactive element — button, input, select, etc.
60
+ * The `ref` is the address the AI uses to point at it later.
61
+ */
62
+ export interface CSTInteractiveNode {
63
+ type: 'interactive';
64
+ role: CSTInteractiveRole;
65
+ /** Stable ref id, unique within the snapshot. */
66
+ ref: CSTRefId;
67
+ /** Accessible name. */
68
+ name: string;
69
+ /** Current value, if any (already redaction-processed). */
70
+ value?: string;
71
+ /** Placeholder text, if any. */
72
+ placeholder?: string;
73
+ disabled?: boolean;
74
+ checked?: boolean;
75
+ expanded?: boolean;
76
+ required?: boolean;
77
+ }
78
+
79
+ /** A structural grouping — form, table, section, etc. */
80
+ export interface CSTContainerNode {
81
+ type: 'container';
82
+ role: CSTContainerRole;
83
+ /** Optional accessible name / heading. */
84
+ name?: string;
85
+ children: CSTNode[];
86
+ }
87
+
88
+ /** A run of static prose, collapsed into compact Markdown. */
89
+ export interface CSTTextNode {
90
+ type: 'text';
91
+ /** Pruned structural text formatted as simplified Markdown. */
92
+ content: string;
93
+ }
94
+
95
+ /** Type guard: is this an interactive node. */
96
+ export function isInteractiveNode(node: CSTNode): node is CSTInteractiveNode {
97
+ return node.type === 'interactive';
98
+ }
99
+
100
+ /** Type guard: is this a container node. */
101
+ export function isContainerNode(node: CSTNode): node is CSTContainerNode {
102
+ return node.type === 'container';
103
+ }
104
+
105
+ /** Type guard: is this a text node. */
106
+ export function isTextNode(node: CSTNode): node is CSTTextNode {
107
+ return node.type === 'text';
108
+ }
109
+
110
+ /** Nodes that can have children. */
111
+ export function hasChildren(
112
+ node: CSTNode,
113
+ ): node is CSTRootNode | CSTContainerNode {
114
+ return node.type === 'root' || node.type === 'container';
115
+ }
@@ -0,0 +1,176 @@
1
+ /**
2
+ * PageSnapshotEngine — the capture orchestrator.
3
+ *
4
+ * Client-side only. Walks the live DOM into a CST snapshot, folds
5
+ * repetitive structure, enforces the token budget, and emits a
6
+ * PageContextPayload plus a ref registry for `point` directives.
7
+ *
8
+ * Pipeline: scope → walk → fold → budget → serialize → metadata.
9
+ */
10
+
11
+ import { enforceBudget, type DegradationLevel } from './capture/budget';
12
+ import { foldSiblings } from './capture/fold';
13
+ import { resolveScope, type ScopeStrategy, type ScopeTier } from './capture/scope';
14
+ import { noopRedact, walkDOM } from './capture/walk';
15
+ import type { RedactValue } from './capture/walk';
16
+ import type { CSTNode, CSTRefId, CSTRootNode } from './cst/types';
17
+ import type { PageContextPayload, SnapshotMetadata } from './cst/payload';
18
+ import { CST_SCHEMA_VERSION } from './cst/payload';
19
+ import { serializeCST } from './cst/serialize';
20
+ import { RedactionAuditor } from './redaction/audit';
21
+ import { createRedactor } from './redaction';
22
+ import { RefRegistry } from './refs/registry';
23
+ import { hashSnapshot } from './staleness/hash';
24
+ import { estimateTokens } from './tokens';
25
+
26
+ /** Default token budget — sized for real data tables. */
27
+ export const DEFAULT_TOKEN_BUDGET = 8_000;
28
+
29
+ /** Options for constructing the engine. */
30
+ export interface CaptureEngineOptions {
31
+ /** Scope strategy (default: 'container' — auto-detect content). */
32
+ scope?: ScopeStrategy;
33
+ /** Token budget for the snapshot (default: DEFAULT_TOKEN_BUDGET). */
34
+ tokenBudget?: number;
35
+ /** Explicit target selector; overrides auto-detection. */
36
+ targetSelector?: string;
37
+ }
38
+
39
+ /** Telemetry from one capture run. */
40
+ export interface CaptureTelemetry {
41
+ /** Wall-clock duration of the capture, ms. */
42
+ executionTimeMs: number;
43
+ /** DOM elements visited. */
44
+ nodesVisited: number;
45
+ /** Estimated tokens of the serialized snapshot. */
46
+ tokenEstimate: number;
47
+ /** Redactions performed. */
48
+ redactedCount: number;
49
+ /** Sibling chains folded. */
50
+ foldedCount: number;
51
+ /** Which scope tier produced the capture root. */
52
+ scopeTier: ScopeTier;
53
+ /** What budget degradation, if any, was applied. */
54
+ degradation: DegradationLevel;
55
+ }
56
+
57
+ /** Result of a capture. */
58
+ export interface CaptureResult {
59
+ payload: PageContextPayload;
60
+ telemetry: CaptureTelemetry;
61
+ /** Per-snapshot ref → element map, for resolving `point` directives. */
62
+ refs: RefRegistry;
63
+ }
64
+
65
+ /** Resolve options with defaults applied. */
66
+ function resolveOptions(
67
+ options: CaptureEngineOptions,
68
+ ): Required<Omit<CaptureEngineOptions, 'targetSelector'>> &
69
+ Pick<CaptureEngineOptions, 'targetSelector'> {
70
+ return {
71
+ scope: options.scope ?? 'container',
72
+ tokenBudget: options.tokenBudget ?? DEFAULT_TOKEN_BUDGET,
73
+ targetSelector: options.targetSelector,
74
+ };
75
+ }
76
+
77
+ /** Monotonic id source for snapshots within a session. */
78
+ let snapshotSeq = 0;
79
+
80
+ /**
81
+ * The capture engine. One instance per consumer (e.g. the chat).
82
+ */
83
+ export class PageSnapshotEngine {
84
+ private readonly options: ReturnType<typeof resolveOptions>;
85
+
86
+ constructor(options: CaptureEngineOptions = {}) {
87
+ this.options = resolveOptions(options);
88
+ }
89
+
90
+ /**
91
+ * Capture a snapshot of the current page.
92
+ */
93
+ capture(): CaptureResult {
94
+ if (typeof window === 'undefined' || typeof document === 'undefined') {
95
+ throw new Error(
96
+ 'PageSnapshotEngine.capture: cannot run outside a browser.',
97
+ );
98
+ }
99
+
100
+ const startTime = performance.now();
101
+ const auditor = new RedactionAuditor();
102
+ const redactValue: RedactValue = this.buildRedactor(auditor);
103
+ const refs = new RefRegistry(`snap-${++snapshotSeq}`);
104
+
105
+ // Scope → walk (each root) → merge.
106
+ const scope = resolveScope({
107
+ strategy: this.options.scope,
108
+ targetSelector: this.options.targetSelector,
109
+ });
110
+
111
+ let walked: CSTNode[] = [];
112
+ let nodesVisited = 0;
113
+ for (const root of scope.roots) {
114
+ const result = walkDOM(root, { redactValue });
115
+ walked = walked.concat(result.children);
116
+ nodesVisited += result.nodesVisited;
117
+ // Carry the walk's refs into the snapshot registry.
118
+ for (const [ref, el] of result.refMap) {
119
+ refs.set(ref as CSTRefId, el);
120
+ }
121
+ }
122
+
123
+ // Fold repetitive structure, then enforce the token budget.
124
+ const folded = foldSiblings(walked);
125
+ const budgeted = enforceBudget(folded.children, this.options.tokenBudget);
126
+
127
+ const root: CSTRootNode = {
128
+ type: 'root',
129
+ title: document.title,
130
+ url: window.location.href,
131
+ children: budgeted.children,
132
+ };
133
+
134
+ const serialized = serializeCST(root);
135
+ const tokenEstimate = estimateTokens(serialized);
136
+ const executionTimeMs = performance.now() - startTime;
137
+
138
+ const metadata: SnapshotMetadata = {
139
+ representation: 'CST',
140
+ tokenEstimate,
141
+ captureTimestamp: Date.now(),
142
+ schemaVersion: CST_SCHEMA_VERSION,
143
+ contentHash: hashSnapshot(serialized),
144
+ redactedCount: auditor.redactedCount,
145
+ foldedCount: folded.foldedCount,
146
+ };
147
+
148
+ const payload: PageContextPayload = {
149
+ url: window.location.href,
150
+ route: window.location.pathname,
151
+ params: {},
152
+ title: document.title,
153
+ snapshot: root,
154
+ metadata,
155
+ };
156
+
157
+ return {
158
+ payload,
159
+ refs,
160
+ telemetry: {
161
+ executionTimeMs,
162
+ nodesVisited,
163
+ tokenEstimate,
164
+ redactedCount: auditor.redactedCount,
165
+ foldedCount: folded.foldedCount,
166
+ scopeTier: scope.tier,
167
+ degradation: budgeted.degradation,
168
+ },
169
+ };
170
+ }
171
+
172
+ /** Build the redaction function bound to this run's auditor. */
173
+ private buildRedactor(auditor: RedactionAuditor): RedactValue {
174
+ return createRedactor(auditor) ?? noopRedact;
175
+ }
176
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * @djangocfg/ui-tools — page-snapshot
3
+ *
4
+ * Client-side capture engine: walks a live React/Next.js DOM into a
5
+ * compact, token-efficient, redacted CST snapshot for an AI chat that
6
+ * understands the page the user is looking at.
7
+ *
8
+ * Internal to `ui-tools` — consumed by `tools/Chat`. Kept as a folder
9
+ * (not a standalone package) while it has a single consumer; promote to
10
+ * a package only when something outside `ui-tools` needs it.
11
+ */
12
+
13
+ // Engine.
14
+ export {
15
+ PageSnapshotEngine,
16
+ DEFAULT_TOKEN_BUDGET,
17
+ type CaptureEngineOptions,
18
+ type CaptureResult,
19
+ type CaptureTelemetry,
20
+ } from './engine';
21
+
22
+ // CST types.
23
+ export type {
24
+ CSTNode,
25
+ CSTRootNode,
26
+ CSTInteractiveNode,
27
+ CSTContainerNode,
28
+ CSTTextNode,
29
+ CSTRefId,
30
+ CSTInteractiveRole,
31
+ CSTContainerRole,
32
+ } from './cst/types';
33
+ export {
34
+ isInteractiveNode,
35
+ isContainerNode,
36
+ isTextNode,
37
+ hasChildren,
38
+ } from './cst/types';
39
+
40
+ // Payload.
41
+ export {
42
+ CST_SCHEMA_VERSION,
43
+ type PageContextPayload,
44
+ type SnapshotMetadata,
45
+ type SnapshotRepresentation,
46
+ } from './cst/payload';
47
+
48
+ // Directives — shared with the chat for AI-driven highlight/focus.
49
+ export {
50
+ isPointDirective,
51
+ type UIDirective,
52
+ type PointDirective,
53
+ type ChatResponseMeta,
54
+ } from './cst/directives';
55
+
56
+ // Redaction (audit surface).
57
+ export {
58
+ RedactionAuditor,
59
+ type RedactionAuditReport,
60
+ type RedactionReason,
61
+ type RedactionEntry,
62
+ } from './redaction/audit';
63
+
64
+ // Refs.
65
+ export { RefRegistry } from './refs/registry';
66
+
67
+ // Utilities.
68
+ export { estimateTokens } from './tokens';
69
+ export { serializeCST } from './cst/serialize';
70
+ export { hashSnapshot } from './staleness/hash';
71
+ export { type ScopeStrategy } from './capture/scope';
72
+
73
+ // React surface.
74
+ export {
75
+ PageSnapshotProvider,
76
+ type PageSnapshotProviderProps,
77
+ } from './react/provider';
78
+ export {
79
+ usePageSnapshot,
80
+ type PageSnapshotContextValue,
81
+ } from './react/use-page-snapshot';
82
+ export {
83
+ usePageSnapshotToggle,
84
+ type PageSnapshotToggle,
85
+ } from './react/use-page-snapshot-toggle';
86
+ export {
87
+ PageSnapshotChip,
88
+ type PageSnapshotChipProps,
89
+ } from './react/PageSnapshotChip';
90
+ export {
91
+ PageSnapshotPreview,
92
+ type PageSnapshotPreviewProps,
93
+ } from './react/PageSnapshotPreview';
@@ -0,0 +1,72 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+
5
+ import { cn } from '@djangocfg/ui-core/lib';
6
+ import { Eye, MonitorUp, RefreshCw } from 'lucide-react';
7
+
8
+ import { PageSnapshotPreview } from './PageSnapshotPreview';
9
+ import { usePageSnapshot } from './use-page-snapshot';
10
+
11
+ /** Props for `PageSnapshotChip`. */
12
+ export interface PageSnapshotChipProps {
13
+ className?: string;
14
+ }
15
+
16
+ /**
17
+ * A small status chip the user sees near the composer when page-context
18
+ * sharing is on. Communicates three things:
19
+ * - that the screen is currently shared ("Screen connected"),
20
+ * - whether the captured context is stale vs the page they now see,
21
+ * - a way to inspect exactly what is shared (opens the preview drawer).
22
+ *
23
+ * Renders nothing when the user has not opted in — no opt-in, no chip.
24
+ */
25
+ export function PageSnapshotChip({ className }: PageSnapshotChipProps) {
26
+ const { isLinked, isStale, refresh } = usePageSnapshot();
27
+ const [previewOpen, setPreviewOpen] = useState(false);
28
+
29
+ // No chip at all until the user opts in.
30
+ if (!isLinked) return null;
31
+
32
+ return (
33
+ <>
34
+ <div
35
+ className={cn(
36
+ 'inline-flex items-center gap-1.5 rounded-full border px-2 py-0.5 text-xs',
37
+ isStale
38
+ ? 'border-amber-500/40 text-amber-600 dark:text-amber-400'
39
+ : 'border-border text-muted-foreground',
40
+ className,
41
+ )}
42
+ >
43
+ <MonitorUp className="size-3.5" aria-hidden />
44
+ <span>{isStale ? 'Screen changed' : 'Screen connected'}</span>
45
+
46
+ {/* Refresh — only meaningful once the context is stale. */}
47
+ {isStale && (
48
+ <button
49
+ type="button"
50
+ onClick={refresh}
51
+ className="hover:text-foreground inline-flex items-center"
52
+ aria-label="Refresh page context"
53
+ >
54
+ <RefreshCw className="size-3.5" aria-hidden />
55
+ </button>
56
+ )}
57
+
58
+ {/* Inspect — opens the preview drawer (the trust surface). */}
59
+ <button
60
+ type="button"
61
+ onClick={() => setPreviewOpen(true)}
62
+ className="hover:text-foreground inline-flex items-center"
63
+ aria-label="Preview shared page context"
64
+ >
65
+ <Eye className="size-3.5" aria-hidden />
66
+ </button>
67
+ </div>
68
+
69
+ <PageSnapshotPreview open={previewOpen} onOpenChange={setPreviewOpen} />
70
+ </>
71
+ );
72
+ }
@@ -0,0 +1,78 @@
1
+ 'use client';
2
+
3
+ import { useMemo } from 'react';
4
+
5
+ import {
6
+ Sheet,
7
+ SheetContent,
8
+ SheetDescription,
9
+ SheetHeader,
10
+ SheetTitle,
11
+ } from '@djangocfg/ui-core/components';
12
+
13
+ import { usePageSnapshot } from './use-page-snapshot';
14
+
15
+ /** Props for `PageSnapshotPreview`. */
16
+ export interface PageSnapshotPreviewProps {
17
+ /** Whether the drawer is open. */
18
+ open: boolean;
19
+ /** Called when the user dismisses the drawer. */
20
+ onOpenChange: (open: boolean) => void;
21
+ }
22
+
23
+ /**
24
+ * The trust surface: a side drawer that shows the user exactly what
25
+ * page-context snapshot would be sent to the AI — already redacted.
26
+ *
27
+ * It captures fresh on open (preview mode, ignoring the opt-in gate) so
28
+ * what the user sees is what the next message would actually carry.
29
+ */
30
+ export function PageSnapshotPreview({
31
+ open,
32
+ onOpenChange,
33
+ }: PageSnapshotPreviewProps) {
34
+ const { generatePreview } = usePageSnapshot();
35
+
36
+ // Capture a preview only while the drawer is open — no point walking
37
+ // the DOM otherwise. Re-runs whenever the drawer is (re)opened.
38
+ const preview = useMemo(
39
+ () => (open ? generatePreview() : null),
40
+ [open, generatePreview],
41
+ );
42
+
43
+ const json = useMemo(
44
+ () =>
45
+ preview
46
+ ? JSON.stringify(preview.payload.snapshot, null, 2)
47
+ : '',
48
+ [preview],
49
+ );
50
+
51
+ const meta = preview?.payload.metadata;
52
+
53
+ return (
54
+ <Sheet open={open} onOpenChange={onOpenChange}>
55
+ <SheetContent side="right" className="flex w-[28rem] max-w-[90vw] flex-col">
56
+ <SheetHeader>
57
+ <SheetTitle>Page context preview</SheetTitle>
58
+ <SheetDescription>
59
+ This is exactly what the assistant sees about your screen.
60
+ Sensitive values are already removed.
61
+ </SheetDescription>
62
+ </SheetHeader>
63
+
64
+ {meta && (
65
+ <div className="text-muted-foreground flex flex-wrap gap-x-4 gap-y-1 text-xs">
66
+ <span>~{meta.tokenEstimate} tokens</span>
67
+ <span>{meta.redactedCount} redacted</span>
68
+ <span>{meta.foldedCount} groups folded</span>
69
+ </div>
70
+ )}
71
+
72
+ <pre className="bg-muted/50 min-h-0 flex-1 overflow-auto rounded-md p-3 text-xs leading-relaxed">
73
+ {json || 'Nothing to preview.'}
74
+ </pre>
75
+ </SheetContent>
76
+ </Sheet>
77
+ );
78
+ }
@@ -0,0 +1,54 @@
1
+ // @vitest-environment jsdom
2
+ import { act, createElement } from 'react';
3
+ import { createRoot, type Root } from 'react-dom/client';
4
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
5
+
6
+ import { PageSnapshotChip } from '../PageSnapshotChip';
7
+ import { PageSnapshotProvider } from '../provider';
8
+
9
+ (globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true;
10
+
11
+ let container: HTMLDivElement;
12
+ let root: Root;
13
+
14
+ beforeEach(() => {
15
+ container = document.createElement('div');
16
+ document.body.appendChild(container);
17
+ root = createRoot(container);
18
+ });
19
+
20
+ afterEach(() => {
21
+ act(() => root.unmount());
22
+ container.remove();
23
+ });
24
+
25
+ async function render(defaultLinked: boolean) {
26
+ await act(async () => {
27
+ root.render(
28
+ createElement(PageSnapshotProvider, {
29
+ defaultLinked,
30
+ children: createElement(PageSnapshotChip, {}),
31
+ }),
32
+ );
33
+ });
34
+ }
35
+
36
+ describe('PageSnapshotChip', () => {
37
+ it('renders nothing when not opted in', async () => {
38
+ await render(false);
39
+ expect(container.textContent).toBe('');
40
+ });
41
+
42
+ it('shows "Screen connected" when opted in', async () => {
43
+ await render(true);
44
+ expect(container.textContent).toContain('Screen connected');
45
+ });
46
+
47
+ it('exposes a preview button when opted in', async () => {
48
+ await render(true);
49
+ const previewBtn = container.querySelector(
50
+ '[aria-label="Preview shared page context"]',
51
+ );
52
+ expect(previewBtn).not.toBeNull();
53
+ });
54
+ });
@@ -0,0 +1,103 @@
1
+ // @vitest-environment jsdom
2
+ import { act } from 'react';
3
+ import { createElement } from 'react';
4
+ import { createRoot, type Root } from 'react-dom/client';
5
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
6
+
7
+ import { PageSnapshotProvider } from '../provider';
8
+ import {
9
+ usePageSnapshot,
10
+ type PageSnapshotContextValue,
11
+ } from '../use-page-snapshot';
12
+
13
+ // React 19 act() environment flag.
14
+ (globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true;
15
+
16
+ let container: HTMLDivElement;
17
+ let root: Root;
18
+ /** Latest context value, captured by the probe component each render. */
19
+ let ctx: PageSnapshotContextValue | null = null;
20
+
21
+ /** Probe component — publishes the context value to the test. */
22
+ function Probe() {
23
+ ctx = usePageSnapshot();
24
+ return null;
25
+ }
26
+
27
+ beforeEach(() => {
28
+ container = document.createElement('div');
29
+ document.body.appendChild(container);
30
+ root = createRoot(container);
31
+ });
32
+
33
+ afterEach(() => {
34
+ act(() => root.unmount());
35
+ container.remove();
36
+ ctx = null;
37
+ });
38
+
39
+ /** Render the provider with the probe inside; flushes effects. */
40
+ async function render(defaultLinked = false) {
41
+ await act(async () => {
42
+ root.render(
43
+ createElement(PageSnapshotProvider, {
44
+ defaultLinked,
45
+ children: createElement(Probe),
46
+ }),
47
+ );
48
+ });
49
+ }
50
+
51
+ describe('PageSnapshotProvider / usePageSnapshot', () => {
52
+ it('defaults to not linked', async () => {
53
+ await render();
54
+ expect(ctx!.isLinked).toBe(false);
55
+ expect(ctx!.lastSnapshot).toBeNull();
56
+ });
57
+
58
+ it('capture() returns null while not linked', async () => {
59
+ await render();
60
+ let result: unknown;
61
+ act(() => {
62
+ result = ctx!.capture();
63
+ });
64
+ expect(result).toBeNull();
65
+ });
66
+
67
+ it('getChatMetadata() returns undefined while not linked', async () => {
68
+ await render();
69
+ expect(ctx!.getChatMetadata()).toBeUndefined();
70
+ });
71
+
72
+ it('generatePreview() works even when not linked (preview gate)', async () => {
73
+ document.body.innerHTML += '<main><h1>Preview Page</h1></main>';
74
+ await render();
75
+ let preview: ReturnType<PageSnapshotContextValue['generatePreview']>;
76
+ act(() => {
77
+ preview = ctx!.generatePreview();
78
+ });
79
+ expect(preview!).not.toBeNull();
80
+ expect(preview!.payload.snapshot.type).toBe('root');
81
+ });
82
+
83
+ it('opting in captures an initial snapshot', async () => {
84
+ document.body.innerHTML += '<main><h1>Linked Page</h1></main>';
85
+ await render();
86
+ await act(async () => {
87
+ ctx!.setIsLinked(true);
88
+ });
89
+ expect(ctx!.isLinked).toBe(true);
90
+ expect(ctx!.lastSnapshot).not.toBeNull();
91
+ });
92
+
93
+ it('getChatMetadata() returns { pageContext } when linked', async () => {
94
+ document.body.innerHTML += '<main><h1>Meta Page</h1></main>';
95
+ await render(true);
96
+ let meta: Record<string, unknown> | undefined;
97
+ await act(async () => {
98
+ meta = ctx!.getChatMetadata();
99
+ });
100
+ expect(meta).toBeDefined();
101
+ expect(meta).toHaveProperty('pageContext');
102
+ });
103
+ });