@digithings-ai/ui 0.0.0-stage → 0.1.0

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 (424) hide show
  1. package/README.md +48 -2
  2. package/package.json +143 -4
  3. package/src/components/NavShell.tsx +776 -0
  4. package/src/components/SocialRow.tsx +87 -0
  5. package/src/components/Spinner.tsx +27 -0
  6. package/src/components/StackLogo.tsx +40 -0
  7. package/src/components/Terminal.tsx +110 -0
  8. package/src/components/TerminalManifest.tsx +212 -0
  9. package/src/components/ThemeProvider.tsx +271 -0
  10. package/src/components/account/AuthCard.tsx +309 -0
  11. package/src/components/assistant-ui/elements/mermaid-diagram.aui.tsx +63 -0
  12. package/src/components/assistant-ui/elements/mermaid-diagram.tsx +340 -0
  13. package/src/components/brand/BrandWordmark.tsx +32 -0
  14. package/src/components/changelog/ReleaseRail.tsx +81 -0
  15. package/src/components/changelog/index.ts +1 -0
  16. package/src/components/chat/ChatCodeBlock.tsx +67 -0
  17. package/src/components/chat/ChatMarkdown.tsx +66 -0
  18. package/src/components/chat/ChatMarkdownSource.tsx +203 -0
  19. package/src/components/chat/ChatMermaidBlock.tsx +202 -0
  20. package/src/components/chat/ChatMessage.tsx +77 -0
  21. package/src/components/chat/ChatPlayback.tsx +422 -0
  22. package/src/components/chat/ChatStreamCursor.tsx +25 -0
  23. package/src/components/chat/ChatThinking.tsx +113 -0
  24. package/src/components/chat/ChatToolCall.tsx +190 -0
  25. package/src/components/chat/ChatTranscript.tsx +60 -0
  26. package/src/components/chat/ChatWidgetFrame.tsx +107 -0
  27. package/src/components/chat/DigichatBootLoader.tsx +683 -0
  28. package/src/components/chat/DigichatLauncher.tsx +361 -0
  29. package/src/components/chat/DigichatThread.tsx +118 -0
  30. package/src/components/chat/DigichatThreadList.tsx +54 -0
  31. package/src/components/chat/DigichatWordmark.tsx +24 -0
  32. package/src/components/chat/DotMatrix.tsx +650 -0
  33. package/src/components/chat/boot-lab.tsx +766 -0
  34. package/src/components/chat/boot-signal.ts +144 -0
  35. package/src/components/chat/chat-surfaces.ts +52 -0
  36. package/src/components/chat/connection-status.ts +40 -0
  37. package/src/components/chat/digichat-wordmark.ts +355 -0
  38. package/src/components/chat/disclosure-chevron.tsx +49 -0
  39. package/src/components/chat/followable-href.ts +9 -0
  40. package/src/components/chat/gallery-thread/action-icons.tsx +25 -0
  41. package/src/components/chat/gallery-thread/attachment.aui.tsx +262 -0
  42. package/src/components/chat/gallery-thread/block-caret.tsx +439 -0
  43. package/src/components/chat/gallery-thread/cn.ts +1 -0
  44. package/src/components/chat/gallery-thread/composer-keyboard.ts +106 -0
  45. package/src/components/chat/gallery-thread/composer-trigger-popover.aui.tsx +235 -0
  46. package/src/components/chat/gallery-thread/css.d.ts +6 -0
  47. package/src/components/chat/gallery-thread/file.tsx +238 -0
  48. package/src/components/chat/gallery-thread/follow-up-suggestions.aui.tsx +92 -0
  49. package/src/components/chat/gallery-thread/format-json-dump.ts +112 -0
  50. package/src/components/chat/gallery-thread/hooks/use-attachment-src.ts +46 -0
  51. package/src/components/chat/gallery-thread/hooks/use-copy-to-clipboard.ts +9 -0
  52. package/src/components/chat/gallery-thread/image.tsx +519 -0
  53. package/src/components/chat/gallery-thread/markdown-text.tsx +278 -0
  54. package/src/components/chat/gallery-thread/message-error.aui.tsx +105 -0
  55. package/src/components/chat/gallery-thread/message-timing.aui.tsx +143 -0
  56. package/src/components/chat/gallery-thread/private-work-step.tsx +188 -0
  57. package/src/components/chat/gallery-thread/reasoning.tsx +332 -0
  58. package/src/components/chat/gallery-thread/scroll-mode.ts +174 -0
  59. package/src/components/chat/gallery-thread/source.tsx +76 -0
  60. package/src/components/chat/gallery-thread/thread.aui.tsx +1619 -0
  61. package/src/components/chat/gallery-thread/tooltip-icon-button.tsx +53 -0
  62. package/src/components/chat/gallery-thread/typed-welcome-copy.tsx +18 -0
  63. package/src/components/chat/gallery-thread/ui/tooltip.tsx +22 -0
  64. package/src/components/chat/gallery-thread/work-chain-summary.tsx +48 -0
  65. package/src/components/chat/gallery-thread/work-chain.tsx +110 -0
  66. package/src/components/chat/gallery-thread/work-status.ts +66 -0
  67. package/src/components/chat/overlay-scrollbar.tsx +86 -0
  68. package/src/components/chat/sanitize-mermaid-svg.ts +38 -0
  69. package/src/components/chat/skins/SOURCE.md +36 -0
  70. package/src/components/chat/skins/base/attachment.aui.tsx +287 -0
  71. package/src/components/chat/skins/base/aui-default/README.md +45 -0
  72. package/src/components/chat/skins/base/aui-default/collapsible-root.tsx +19 -0
  73. package/src/components/chat/skins/base/aui-default/collapsible.tsx +21 -0
  74. package/src/components/chat/skins/base/aui-default/reasoning.aui.tsx +120 -0
  75. package/src/components/chat/skins/base/aui-default/reasoning.tsx +331 -0
  76. package/src/components/chat/skins/base/aui-default/surfaces.ts +7 -0
  77. package/src/components/chat/skins/base/aui-default/tool-fallback.aui.tsx +772 -0
  78. package/src/components/chat/skins/base/aui-default/tool-group.aui.tsx +241 -0
  79. package/src/components/chat/skins/base/docs-model-options.ts +6 -0
  80. package/src/components/chat/skins/base/elements/assistant-trace.aui.tsx +169 -0
  81. package/src/components/chat/skins/base/elements/composer-trigger-popover.aui.tsx +271 -0
  82. package/src/components/chat/skins/base/elements/directive-text.aui.tsx +29 -0
  83. package/src/components/chat/skins/base/elements/directive-text.tsx +76 -0
  84. package/src/components/chat/skins/base/elements/message-timing.aui.tsx +104 -0
  85. package/src/components/chat/skins/base/elements/model-selector.aui.tsx +140 -0
  86. package/src/components/chat/skins/base/elements/model-selector.tsx +720 -0
  87. package/src/components/chat/skins/base/elements/quote.aui.tsx +290 -0
  88. package/src/components/chat/skins/base/elements/work-chain.tsx +100 -0
  89. package/src/components/chat/skins/base/markdown-text.tsx +294 -0
  90. package/src/components/chat/skins/base/model.ts +1 -0
  91. package/src/components/chat/skins/base/thread.tsx +1050 -0
  92. package/src/components/chat/skins/base/tooltip-icon-button.tsx +56 -0
  93. package/src/components/chat/skins/base/ui/avatar.tsx +115 -0
  94. package/src/components/chat/skins/base/ui/badge.tsx +51 -0
  95. package/src/components/chat/skins/base/ui/button.tsx +67 -0
  96. package/src/components/chat/skins/base/ui/command.tsx +185 -0
  97. package/src/components/chat/skins/base/ui/dialog.tsx +168 -0
  98. package/src/components/chat/skins/base/ui/dot-matrix.tsx +267 -0
  99. package/src/components/chat/skins/base/ui/popover.tsx +97 -0
  100. package/src/components/chat/skins/base/ui/skeleton.tsx +20 -0
  101. package/src/components/chat/skins/base/ui/textarea.tsx +24 -0
  102. package/src/components/chat/skins/base/ui/tooltip.tsx +73 -0
  103. package/src/components/chat/skins/base-assistant-ui/config-provider.tsx +90 -0
  104. package/src/components/chat/skins/base-assistant-ui/defaults.ts +419 -0
  105. package/src/components/chat/skins/base-assistant-ui/index.tsx +44 -0
  106. package/src/components/chat/skins/chatgpt.tsx +448 -0
  107. package/src/components/chat/skins/claude.tsx +380 -0
  108. package/src/components/chat/skins/digichat-composer-toolbar.tsx +151 -0
  109. package/src/components/chat/skins/digichat-context-meter-shares.ts +158 -0
  110. package/src/components/chat/skins/digichat-context-meter.tsx +243 -0
  111. package/src/components/chat/skins/digichat-meter-tone.ts +26 -0
  112. package/src/components/chat/skins/digichat-usage-meter.tsx +103 -0
  113. package/src/components/chat/skins/digichat.tsx +144 -0
  114. package/src/components/chat/skins/expo-react-native.tsx +29 -0
  115. package/src/components/chat/skins/gemini.tsx +322 -0
  116. package/src/components/chat/skins/grok-icon.tsx +34 -0
  117. package/src/components/chat/skins/grok.tsx +369 -0
  118. package/src/components/chat/skins/index.ts +46 -0
  119. package/src/components/chat/skins/perplexity.tsx +462 -0
  120. package/src/components/chat/skins/product-page-assistant/assistant-modal.tsx +73 -0
  121. package/src/components/chat/skins/product-page-assistant/index.tsx +19 -0
  122. package/src/components/chat/skins/product-page-assistant/product-dashboard.tsx +164 -0
  123. package/src/components/chat/skins/react-ink.tsx +97 -0
  124. package/src/components/chat/skins/thread-skin-host-contract.ts +102 -0
  125. package/src/components/chat/skins/thread-skin-view.tsx +156 -0
  126. package/src/components/chat/skins/thread-skins.ts +143 -0
  127. package/src/components/chat/skins/tooltip-icon-button.tsx +35 -0
  128. package/src/components/chat/skins/ui/dropdown-menu.tsx +272 -0
  129. package/src/components/chat/skins/webpage-assistant/assistant-config.ts +238 -0
  130. package/src/components/chat/skins/webpage-assistant/assistant-modal.tsx +78 -0
  131. package/src/components/chat/skins/webpage-assistant/config-provider.tsx +132 -0
  132. package/src/components/chat/skins/webpage-assistant/docs-app.tsx +106 -0
  133. package/src/components/chat/skins/webpage-assistant/docs-article.tsx +109 -0
  134. package/src/components/chat/skins/webpage-assistant/docs-assistant.tsx +73 -0
  135. package/src/components/chat/skins/webpage-assistant/docs-nav.tsx +81 -0
  136. package/src/components/chat/skins/webpage-assistant/host-ui.ts +79 -0
  137. package/src/components/chat/skins/webpage-assistant/index.tsx +12 -0
  138. package/src/components/chat/skins/webpage-assistant/markdown-page.tsx +11 -0
  139. package/src/components/chat/skins/webpage-assistant/pages.ts +9 -0
  140. package/src/components/chat/skins/webpage-assistant/types.ts +178 -0
  141. package/src/components/chat/skins/webpage-assistant/use-mobile.ts +19 -0
  142. package/src/components/chat/stock/SOURCE.md +90 -0
  143. package/src/components/chat/stock/artifacts-panel.tsx +305 -0
  144. package/src/components/chat/stock/attachment.aui.tsx +280 -0
  145. package/src/components/chat/stock/chat-notice.tsx +143 -0
  146. package/src/components/chat/stock/chat-private-mode.tsx +64 -0
  147. package/src/components/chat/stock/deploy-ui-context.tsx +89 -0
  148. package/src/components/chat/stock/embed-chat-prefs.tsx +471 -0
  149. package/src/components/chat/stock/file.tsx +265 -0
  150. package/src/components/chat/stock/follow-up-suggestions.aui.tsx +83 -0
  151. package/src/components/chat/stock/image.tsx +531 -0
  152. package/src/components/chat/stock/index.ts +129 -0
  153. package/src/components/chat/stock/json-view.tsx +140 -0
  154. package/src/components/chat/stock/markdown-text.tsx +319 -0
  155. package/src/components/chat/stock/message-error.aui.tsx +96 -0
  156. package/src/components/chat/stock/message-export-menu.tsx +156 -0
  157. package/src/components/chat/stock/palette-row-preview.tsx +26 -0
  158. package/src/components/chat/stock/reasoning.aui.tsx +120 -0
  159. package/src/components/chat/stock/reasoning.tsx +335 -0
  160. package/src/components/chat/stock/skin-chrome.tsx +81 -0
  161. package/src/components/chat/stock/skin-credit.tsx +63 -0
  162. package/src/components/chat/stock/skin-palette-bridge.tsx +235 -0
  163. package/src/components/chat/stock/skin-palette-context.ts +16 -0
  164. package/src/components/chat/stock/skin-runtime.tsx +52 -0
  165. package/src/components/chat/stock/skin-slash-palette.tsx +393 -0
  166. package/src/components/chat/stock/skin-types.ts +217 -0
  167. package/src/components/chat/stock/stock-chat-prefs-host.tsx +501 -0
  168. package/src/components/chat/stock/stock-send-gate.tsx +94 -0
  169. package/src/components/chat/stock/stock-shape.tsx +39 -0
  170. package/src/components/chat/stock/thread.aui.tsx +645 -0
  171. package/src/components/chat/stock/tool-display.ts +6 -0
  172. package/src/components/chat/stock/tool-fallback.aui.tsx +866 -0
  173. package/src/components/chat/stock/tool-group.aui.tsx +244 -0
  174. package/src/components/chat/stock/tool-surface.ts +120 -0
  175. package/src/components/chat/stock/tooltip-icon-button.tsx +49 -0
  176. package/src/components/chat/stock/ui/avatar.tsx +108 -0
  177. package/src/components/chat/stock/ui/button.tsx +81 -0
  178. package/src/components/chat/stock/ui/collapsible.tsx +21 -0
  179. package/src/components/chat/stock/ui/dialog.tsx +163 -0
  180. package/src/components/chat/stock/ui/skeleton.tsx +13 -0
  181. package/src/components/chat/stock/ui/textarea.tsx +20 -0
  182. package/src/components/chat/stock/ui/tooltip.tsx +68 -0
  183. package/src/components/chat/stock/use-attachment-src.ts +42 -0
  184. package/src/components/chat/stock/use-copy-to-clipboard.ts +29 -0
  185. package/src/components/chat/strip-model-boilerplate.ts +42 -0
  186. package/src/components/chat/suggestion-cycle.tsx +230 -0
  187. package/src/components/chat/suggestion-scroll-row.tsx +81 -0
  188. package/src/components/chat/trace-grouping/chain-group-by.ts +44 -0
  189. package/src/components/chat/trace-grouping/chain-state.ts +141 -0
  190. package/src/components/chat/trace-grouping/index.ts +32 -0
  191. package/src/components/chat/trace-grouping/tool-row.ts +74 -0
  192. package/src/components/chat/trace-grouping/use-deploy-work-disclosure.ts +35 -0
  193. package/src/components/chat/trace-grouping/use-work-chain.ts +103 -0
  194. package/src/components/chat/trace-i18n/core.ts +48 -0
  195. package/src/components/chat/trace-i18n/index.tsx +23 -0
  196. package/src/components/chat/trace-i18n/locales/en.ts +24 -0
  197. package/src/components/chat/trace-i18n/locales/fr.ts +28 -0
  198. package/src/components/chat/trace-i18n/types.ts +35 -0
  199. package/src/components/chat/trace-spacing.ts +249 -0
  200. package/src/components/chat/transcript/activity-view.ts +375 -0
  201. package/src/components/chat/transcript/chat-export.ts +133 -0
  202. package/src/components/chat/transcript/index.ts +10 -0
  203. package/src/components/chat/transcript/save-file.ts +209 -0
  204. package/src/components/chat/transcript/transcript-markdown.ts +428 -0
  205. package/src/components/chat/transcript/types.ts +81 -0
  206. package/src/components/chat/turn-outcome.ts +52 -0
  207. package/src/components/chat/use-warmup-phase.ts +18 -0
  208. package/src/components/chat/work-status-lead.tsx +49 -0
  209. package/src/components/chrome.tsx +264 -0
  210. package/src/components/command-palette/CommandPalette.tsx +404 -0
  211. package/src/components/command-palette/index.ts +6 -0
  212. package/src/components/contact/ContactMailto.tsx +112 -0
  213. package/src/components/contact/index.ts +5 -0
  214. package/src/components/conviction/ConvictionMeter.tsx +40 -0
  215. package/src/components/conviction/SignedConvictionBadge.tsx +26 -0
  216. package/src/components/conviction/index.ts +7 -0
  217. package/src/components/data-layout/BentoGrid.tsx +52 -0
  218. package/src/components/data-layout/CardRail.tsx +150 -0
  219. package/src/components/data-layout/DotMatrixStat.tsx +65 -0
  220. package/src/components/data-layout/FeatureCell.tsx +80 -0
  221. package/src/components/data-layout/MediaFrame.tsx +125 -0
  222. package/src/components/data-layout/ModuleGrid.tsx +735 -0
  223. package/src/components/data-layout/Odometer.tsx +149 -0
  224. package/src/components/data-layout/ProductFrame.tsx +79 -0
  225. package/src/components/data-layout/TestimonialWall.tsx +139 -0
  226. package/src/components/data-layout/index.ts +19 -0
  227. package/src/components/data-layout/treemap.ts +368 -0
  228. package/src/components/deck/DeckStack.tsx +243 -0
  229. package/src/components/desk/DataTable.tsx +373 -0
  230. package/src/components/desk/KpiGrid.tsx +80 -0
  231. package/src/components/desk/Sparkline.tsx +63 -0
  232. package/src/components/desk/StateBlock.tsx +132 -0
  233. package/src/components/desk/index.ts +18 -0
  234. package/src/components/diagrams/ArchitectureDiagram.tsx +350 -0
  235. package/src/components/diagrams/ArchitectureTour.tsx +912 -0
  236. package/src/components/diagrams/TerminalSchematic.tsx +192 -0
  237. package/src/components/diagrams/architecture-svg.tsx +384 -0
  238. package/src/components/diagrams/index.ts +30 -0
  239. package/src/components/diagrams/mermaid-theme.ts +115 -0
  240. package/src/components/diagrams/tour-camera.ts +171 -0
  241. package/src/components/docs/CodeTabs.tsx +90 -0
  242. package/src/components/docs/CopyCommand.tsx +151 -0
  243. package/src/components/docs/DocsLayout.tsx +197 -0
  244. package/src/components/docs/DocsSearch.tsx +97 -0
  245. package/src/components/docs/Endpoint.tsx +135 -0
  246. package/src/components/document/DocumentFrame.tsx +43 -0
  247. package/src/components/document/GlyphList.tsx +51 -0
  248. package/src/components/document/PageTitle.tsx +44 -0
  249. package/src/components/document/Prose.tsx +38 -0
  250. package/src/components/document/Section.tsx +55 -0
  251. package/src/components/document/index.ts +13 -0
  252. package/src/components/effects-chrome/AnnouncementBar.tsx +88 -0
  253. package/src/components/effects-chrome/HorizontalScrollTrack.tsx +461 -0
  254. package/src/components/effects-chrome/Pipeline.tsx +226 -0
  255. package/src/components/effects-chrome/RotatingPrompts.tsx +116 -0
  256. package/src/components/effects-chrome/StackingPanels.tsx +101 -0
  257. package/src/components/effects-chrome/TabStrip.tsx +232 -0
  258. package/src/components/effects-chrome/ToastStack.tsx +201 -0
  259. package/src/components/effects-chrome/horizontal-track-core.ts +70 -0
  260. package/src/components/effects-chrome/index.ts +35 -0
  261. package/src/components/emblems.tsx +188 -0
  262. package/src/components/faviconTheme.ts +23 -0
  263. package/src/components/figure/Figure.tsx +38 -0
  264. package/src/components/figure/index.ts +2 -0
  265. package/src/components/finance-charts/DrawdownPlot.tsx +76 -0
  266. package/src/components/finance-charts/EquityCurve.tsx +71 -0
  267. package/src/components/finance-charts/PriceChart.tsx +119 -0
  268. package/src/components/finance-charts/chart-host.tsx +236 -0
  269. package/src/components/finance-charts/chart-scale.ts +93 -0
  270. package/src/components/finance-charts/demo-data.ts +88 -0
  271. package/src/components/finance-charts/index.ts +46 -0
  272. package/src/components/finance-charts/lw-chart.tsx +292 -0
  273. package/src/components/finance-composites/MarketBar.tsx +243 -0
  274. package/src/components/finance-composites/OrderBook.tsx +133 -0
  275. package/src/components/finance-composites/PerformanceDashboard.tsx +185 -0
  276. package/src/components/finance-composites/SortableTable.tsx +144 -0
  277. package/src/components/finance-composites/StockTicker.tsx +65 -0
  278. package/src/components/finance-composites/SyncedTearsheet.tsx +226 -0
  279. package/src/components/finance-composites/index.ts +28 -0
  280. package/src/components/finance-tearsheet/KpiStrip.tsx +49 -0
  281. package/src/components/finance-tearsheet/LiveBadge.tsx +29 -0
  282. package/src/components/finance-tearsheet/ReturnsMatrix.tsx +321 -0
  283. package/src/components/finance-tearsheet/StrategyTearsheet.tsx +99 -0
  284. package/src/components/finance-tearsheet/TearsheetCard.tsx +54 -0
  285. package/src/components/finance-tearsheet/TradeLogTable.tsx +73 -0
  286. package/src/components/finance-tearsheet/charts.tsx +2624 -0
  287. package/src/components/finance-tearsheet/demo-data.ts +177 -0
  288. package/src/components/finance-tearsheet/format.ts +41 -0
  289. package/src/components/finance-tearsheet/index.ts +120 -0
  290. package/src/components/finance-tearsheet/print.ts +52 -0
  291. package/src/components/finance-tearsheet/risk-bands.ts +48 -0
  292. package/src/components/finance-tearsheet/stats.ts +35 -0
  293. package/src/components/finance-tearsheet/strategy-sheet-model.ts +238 -0
  294. package/src/components/finance-tearsheet/types.ts +58 -0
  295. package/src/components/footer/FooterCells.tsx +109 -0
  296. package/src/components/footer/index.ts +6 -0
  297. package/src/components/graph.tsx +158 -0
  298. package/src/components/icons.tsx +98 -0
  299. package/src/components/landing/PixelWordmark.tsx +36 -0
  300. package/src/components/logos.ts +39 -0
  301. package/src/components/marquee/Marquee.tsx +204 -0
  302. package/src/components/metrics/PerfMetrics.tsx +68 -0
  303. package/src/components/metrics/StatCounter.tsx +104 -0
  304. package/src/components/page-geometry.tsx +177 -0
  305. package/src/components/pricing/Pricing.tsx +174 -0
  306. package/src/components/pricing/PricingMatrix.tsx +219 -0
  307. package/src/components/prose/index.ts +2 -0
  308. package/src/components/prose/prose.tsx +86 -0
  309. package/src/components/repo-activity/RepoActivity.tsx +599 -0
  310. package/src/components/repo-activity/RepoHeatmap.tsx +225 -0
  311. package/src/components/repo-activity/demo.ts +201 -0
  312. package/src/components/repo-activity/fetch.ts +211 -0
  313. package/src/components/repo-activity/heatmap.ts +123 -0
  314. package/src/components/repo-activity/index.ts +28 -0
  315. package/src/components/repo-activity/types.ts +99 -0
  316. package/src/components/stages/NumberedStages.tsx +116 -0
  317. package/src/components/symbols/marks.tsx +116 -0
  318. package/src/components/symbols/terminal-lockup.tsx +136 -0
  319. package/src/components/symbols/terminal-marks.tsx +226 -0
  320. package/src/components/terminal/ChatResponseLoader.tsx +67 -0
  321. package/src/components/terminal/ContainerBootLoader.tsx +82 -0
  322. package/src/components/terminal/TerminalStepCaret.tsx +202 -0
  323. package/src/components/themeInit.ts +24 -0
  324. package/src/components/typography/MotionReveal.tsx +108 -0
  325. package/src/components/typography/TypewriterReveal.tsx +101 -0
  326. package/src/components/typography/WordReveal.tsx +143 -0
  327. package/src/data/modules.ts +378 -0
  328. package/src/data/subsystems.ts +101 -0
  329. package/src/index.ts +587 -0
  330. package/src/lib/base-path.ts +60 -0
  331. package/src/lib/cx.ts +25 -0
  332. package/src/lib/gloomberb.ts +69 -0
  333. package/src/lib/record.ts +19 -0
  334. package/src/lib/stub-payload.ts +183 -0
  335. package/src/lib/useBodyScrollLock.ts +44 -0
  336. package/src/lib/utils.ts +6 -0
  337. package/src/motion/primitives.tsx +105 -0
  338. package/src/motion/scroll-glide.ts +62 -0
  339. package/src/motion/scrolly-core.ts +38 -0
  340. package/src/motion/scrolly.tsx +95 -0
  341. package/src/navigation/HashScrollManager.tsx +167 -0
  342. package/src/navigation/hashScroll.ts +78 -0
  343. package/src/replay/browser.ts +280 -0
  344. package/src/replay/chat-export-run.ts +121 -0
  345. package/src/replay/export-format.ts +25 -0
  346. package/src/replay/export-mount.ts +32 -0
  347. package/src/replay/filename.ts +54 -0
  348. package/src/replay/index.ts +21 -0
  349. package/src/replay/redact-dom.ts +178 -0
  350. package/src/replay/redact.ts +113 -0
  351. package/src/replay/snapshot.ts +659 -0
  352. package/src/styles/account-auth.css +339 -0
  353. package/src/styles/brand-wordmark.css +11 -0
  354. package/src/styles/card-rail.css +120 -0
  355. package/src/styles/chat-aui.css +1143 -0
  356. package/src/styles/chat-core.css +469 -0
  357. package/src/styles/chat-digichat.css +2531 -0
  358. package/src/styles/chat-math.css +70 -0
  359. package/src/styles/chat-skin-palette.css +138 -0
  360. package/src/styles/chat-stock-menu.css +37 -0
  361. package/src/styles/chat-widgets.css +190 -0
  362. package/src/styles/command-palette.css +372 -0
  363. package/src/styles/controls-core.css +2261 -0
  364. package/src/styles/controls-overlay.css +603 -0
  365. package/src/styles/data-layout.css +288 -0
  366. package/src/styles/deck.css +141 -0
  367. package/src/styles/diagrams.css +1860 -0
  368. package/src/styles/digichat-app-theme.css +250 -0
  369. package/src/styles/digichat-boot-loader.css +487 -0
  370. package/src/styles/digichat-launcher.css +377 -0
  371. package/src/styles/docs.css +165 -0
  372. package/src/styles/effects-chrome.css +888 -0
  373. package/src/styles/finance-composites.css +214 -0
  374. package/src/styles/finance-tearsheet-overlay.css +90 -0
  375. package/src/styles/finance-tearsheet.css +945 -0
  376. package/src/styles/marquee.css +168 -0
  377. package/src/styles/metrics.css +82 -0
  378. package/src/styles/nav-shell.css +551 -0
  379. package/src/styles/pricing.css +79 -0
  380. package/src/styles/repo-activity.css +575 -0
  381. package/src/styles/stages.css +69 -0
  382. package/src/styles/terminal-loaders.css +122 -0
  383. package/src/styles/terminal-manifest.css +228 -0
  384. package/src/styles/web-theme.css +1007 -0
  385. package/src/styles/word-reveal.css +204 -0
  386. package/src/theme/apply-theme.ts +214 -0
  387. package/src/theme/index.ts +27 -0
  388. package/src/theme/palette-registry.ts +855 -0
  389. package/src/theme/tui-prefs.ts +64 -0
  390. package/src/ui/alert.tsx +78 -0
  391. package/src/ui/avatar.tsx +104 -0
  392. package/src/ui/badge.tsx +75 -0
  393. package/src/ui/breadcrumb.tsx +61 -0
  394. package/src/ui/button.tsx +67 -0
  395. package/src/ui/card.tsx +139 -0
  396. package/src/ui/checkbox.tsx +27 -0
  397. package/src/ui/collapsible.tsx +32 -0
  398. package/src/ui/date-pager.tsx +325 -0
  399. package/src/ui/dialog.tsx +157 -0
  400. package/src/ui/dropdown-menu.tsx +267 -0
  401. package/src/ui/empty-state.tsx +165 -0
  402. package/src/ui/field.tsx +104 -0
  403. package/src/ui/form.tsx +36 -0
  404. package/src/ui/icon-button.tsx +36 -0
  405. package/src/ui/index.ts +34 -0
  406. package/src/ui/input.tsx +33 -0
  407. package/src/ui/kbd.tsx +30 -0
  408. package/src/ui/label.tsx +32 -0
  409. package/src/ui/pager.tsx +119 -0
  410. package/src/ui/pagination.tsx +126 -0
  411. package/src/ui/radio-group.tsx +51 -0
  412. package/src/ui/search-bar.tsx +85 -0
  413. package/src/ui/segmented-control.tsx +76 -0
  414. package/src/ui/select.tsx +257 -0
  415. package/src/ui/separator.tsx +24 -0
  416. package/src/ui/sheet.tsx +141 -0
  417. package/src/ui/skeleton.tsx +76 -0
  418. package/src/ui/slider.tsx +76 -0
  419. package/src/ui/switch.tsx +31 -0
  420. package/src/ui/table.tsx +179 -0
  421. package/src/ui/tabs.tsx +89 -0
  422. package/src/ui/tags-input.tsx +168 -0
  423. package/src/ui/textarea.tsx +17 -0
  424. package/src/ui/tooltip.tsx +71 -0
@@ -0,0 +1,1619 @@
1
+ "use client";
2
+
3
+ import {
4
+ ComposerAddAttachment,
5
+ ComposerAttachments,
6
+ HiddenAttachmentNamesProvider,
7
+ UserMessageAttachments,
8
+ useHiddenAttachmentNames,
9
+ } from "./attachment.aui";
10
+ import {
11
+ composerHasText,
12
+ dismissComposerKeyboard,
13
+ dismissKeyboardIfCoarse,
14
+ isCoarsePointer,
15
+ } from "./composer-keyboard";
16
+ import { File } from "./file";
17
+ import { ThreadFollowupSuggestions } from "./follow-up-suggestions.aui";
18
+ import { Image } from "./image";
19
+ import { MarkdownText } from "./markdown-text";
20
+ import {
21
+ Reasoning,
22
+ ReasoningContent,
23
+ ReasoningRoot,
24
+ ReasoningText,
25
+ ReasoningTrigger,
26
+ } from "../stock/reasoning.aui";
27
+ import { MessageTiming } from "./message-timing.aui";
28
+ import {
29
+ DEFAULT_SCROLL_MODE,
30
+ scrollModeProps,
31
+ useFollowStream,
32
+ type ScrollMode,
33
+ } from "./scroll-mode";
34
+ import { WorkChain } from "./work-chain";
35
+ import {
36
+ chainFollowedByText,
37
+ chainGroupBy,
38
+ chainHoldsWork,
39
+ chainToolCount,
40
+ messageHasContent,
41
+ messageHasWork,
42
+ reasoningGroupDisclosure,
43
+ toolGroupDisclosure,
44
+ } from "../trace-grouping";
45
+ import { WorkChainSummary } from "./work-chain-summary";
46
+ import { PrivateToolCallStep, PrivateWorkStep } from "./private-work-step";
47
+ import {
48
+ resolveWorkDisclosure,
49
+ type WorkVisibility,
50
+ } from "../stock/skin-types";
51
+ import { BackendNotice } from "../stock/chat-notice";
52
+ import { ToolFallback } from "../stock/tool-fallback.aui";
53
+ import {
54
+ ToolGroupContent,
55
+ ToolGroupRoot,
56
+ ToolGroupTrigger,
57
+ } from "../stock/tool-group.aui";
58
+ import {
59
+ CheckActionIcon,
60
+ CopyActionIcon,
61
+ } from "./action-icons";
62
+ import { TooltipIconButton } from "./tooltip-icon-button";
63
+ import { MessageExportMenu } from "../stock/message-export-menu";
64
+ import { Button } from "../../../ui/button";
65
+ import { DotMatrix } from "../DotMatrix";
66
+ import { connectionStatusText } from "../connection-status";
67
+ import { WorkStatusLead } from "../work-status-lead";
68
+ import { Skeleton } from "../../../ui/skeleton";
69
+ import { cn } from "./cn";
70
+ import { useSuggestionGroup } from "../suggestion-cycle";
71
+ import { SkinCredit } from "../stock/skin-credit";
72
+ import {
73
+ ActionBarPrimitive,
74
+ AuiIf,
75
+ type AssistantState,
76
+ BranchPickerPrimitive,
77
+ ComposerPrimitive,
78
+ MessagePrimitive,
79
+ SuggestionPrimitive,
80
+ ThreadPrimitive,
81
+ type FileMessagePartComponent,
82
+ type ImageMessagePartComponent,
83
+ type SourceMessagePartComponent,
84
+ type ToolCallMessagePartComponent,
85
+ useAui,
86
+ useAuiState,
87
+ } from "@assistant-ui/react";
88
+ import {
89
+ createContext,
90
+ useCallback,
91
+ useContext,
92
+ useEffect,
93
+ useMemo,
94
+ useRef,
95
+ useState,
96
+ type ComponentPropsWithoutRef,
97
+ type ComponentType,
98
+ type FC,
99
+ type FormEvent,
100
+ type MouseEvent as ReactMouseEvent,
101
+ type PropsWithChildren,
102
+ type ReactNode,
103
+ } from "react";
104
+ import { ComposerTriggerPopover } from "./composer-trigger-popover.aui";
105
+ import { MessageError } from "./message-error.aui";
106
+ import { ComposerBlockCaret } from "./block-caret";
107
+ import { TypedWelcomeCopy } from "./typed-welcome-copy";
108
+ import { useDigichatWarmupPhase } from "../use-warmup-phase";
109
+ import { OverlayScrollbar } from "../overlay-scrollbar";
110
+
111
+ export type ThreadGroupPart = MessagePrimitive.GroupedParts.GroupPart;
112
+
113
+ /**
114
+ * Disclosure behavior for a grouped chain of reasoning / tool-call steps.
115
+ * `off` hides the group entirely, `collapsed` (default) renders a closed
116
+ * dropdown, `balanced` opens while the group is still streaming and
117
+ * auto-collapses when it completes, `expanded` starts open, and
118
+ * `locked_open` pins it open.
119
+ */
120
+ export type GroupDisclosureMode =
121
+ | "off"
122
+ | "collapsed"
123
+ | "balanced"
124
+ | "expanded"
125
+ | "locked_open";
126
+
127
+ /**
128
+ * Optional component overrides for the thread. `AssistantMessage` and
129
+ * `Welcome` replace whole sections; the remaining slots override how the
130
+ * assistant message renders tool calls and part groups. Tool UIs registered
131
+ * by name (toolkit `render`, `useAssistantDataUI`) take precedence over
132
+ * `ToolFallback`.
133
+ */
134
+ export type ThreadComponents = {
135
+ AssistantMessage?: ComponentType | undefined;
136
+ Welcome?: ComponentType | undefined;
137
+ ToolFallback?: ToolCallMessagePartComponent | undefined;
138
+ /** Citation row (`source` parts): provider web search, RAG documents (#4552). */
139
+ Source?: SourceMessagePartComponent | undefined;
140
+ /**
141
+ * Optional footer slot below the composer; absent means no footer.
142
+ */
143
+ Footer?: ComponentType | undefined;
144
+ ToolGroup?:
145
+ | ComponentType<PropsWithChildren<{ group: ThreadGroupPart }>>
146
+ | undefined;
147
+ ReasoningGroup?:
148
+ | ComponentType<PropsWithChildren<{ group: ThreadGroupPart }>>
149
+ | undefined;
150
+ };
151
+
152
+ /** Expanded: input, then a row of attach + send. Compact: attach | input | send. */
153
+ export type ComposerLayout = "expanded" | "compact";
154
+
155
+ export type ThreadActions = {
156
+ /** Restore previous assistant branch. Default on. */
157
+ undo?: boolean | undefined;
158
+ /** Thumbs up / down. Off when absent. */
159
+ feedback?: boolean | undefined;
160
+ };
161
+
162
+ export type ThreadProps = {
163
+ components?: ThreadComponents | undefined;
164
+ autoFocus?: boolean | undefined;
165
+ /** `chrome.placeholder` in deploy YAML. */
166
+ placeholder?: string | undefined;
167
+ /** `chrome.composerLayout`. Default expanded (toolbar under the input). */
168
+ composerLayout?: ComposerLayout | undefined;
169
+ /** `features.undo` / `features.feedback`. */
170
+ actions?: ThreadActions | undefined;
171
+ /** `chrome.welcome.title`. Used by the default Welcome slot. */
172
+ welcome?: string | undefined;
173
+ /** `chrome.welcome.body`. */
174
+ welcomeBody?: readonly string[] | undefined;
175
+ /**
176
+ * `chrome.suggestions` in deploy YAML. Static prompts for the new-chat
177
+ * welcome state; when non-empty they take precedence over the runtime's own
178
+ * suggestion adapter, which is the fallback.
179
+ */
180
+ suggestions?: readonly string[] | undefined;
181
+ className?: string | undefined;
182
+ /** Embed send-gate / system page-context attach. */
183
+ onComposerSubmit?: (event: FormEvent<HTMLFormElement>) => void;
184
+ /**
185
+ * Native assistant-ui slash adapter (`unstable_useSlashCommandAdapter`).
186
+ * Mounted when the product skin has session prefs (embed, modal, and app).
187
+ */
188
+ slash?: ThreadSlashTrigger | undefined;
189
+ /** Native `@` mention adapter (`unstable_useMentionAdapter`). */
190
+ mention?: ThreadMentionTrigger | undefined;
191
+ /**
192
+ * Attachment names that render no chip in the composer or sent messages.
193
+ * The attachment still lives on the runtime (its file part reaches the
194
+ * model) — only the UI is suppressed. Generic; callers pass system names
195
+ * such as the embed's `page-context.html` in `silent` mode.
196
+ */
197
+ hiddenAttachmentNames?: readonly string[] | undefined;
198
+ /** Disclosure mode for grouped reasoning runs. Default collapsed. */
199
+ reasoningMode?: GroupDisclosureMode | undefined;
200
+ /** Disclosure mode for grouped tool-call runs. Default collapsed. */
201
+ toolCallsMode?: GroupDisclosureMode | undefined;
202
+ /**
203
+ * How much of the agent's work the user sees: `quiet` (the titles only,
204
+ * with the folded header as a live status line), `balanced` (open while it
205
+ * runs, collapsed when it finishes) or `verbose` (stays open). The client's
206
+ * `features.view` picks the default; the user can change it per session.
207
+ */
208
+ workVisibility?: WorkVisibility | undefined;
209
+ /**
210
+ * Private session. Reasoning text and tool-call payloads are user data, so
211
+ * the rows show the step (kind, tool name, state) in the normal chain but
212
+ * never open — while streaming as well as at rest. Default off; other
213
+ * hosts are unaffected.
214
+ */
215
+ privateMode?: boolean | undefined;
216
+ /**
217
+ * Rendered as a row inside the composer border, under the input. The
218
+ * digichat skin passes its model / tools / search / view / context chips;
219
+ * other hosts leave it unset.
220
+ */
221
+ composerToolbar?: ReactNode;
222
+ /** `always` reserves a scrollbar gutter; `auto` shows bars only on scroll. */
223
+ scrollbars?: "always" | "auto";
224
+ /** Worked-chain rhythm. `default` is the fixed spacing. */
225
+ density?: "compact" | "default" | "airy";
226
+ /**
227
+ * `follow` anchors the sent question at the top and chases the stream;
228
+ * `free` never moves the viewport. See `scroll-mode.ts`.
229
+ */
230
+ scrollMode?: ScrollMode;
231
+ };
232
+
233
+ /** `{ adapter, action }` from `unstable_useSlashCommandAdapter`. */
234
+ type SlashPopoverProps = ComponentPropsWithoutRef<typeof ComposerTriggerPopover>;
235
+ export type ThreadSlashTrigger = {
236
+ adapter: SlashPopoverProps["adapter"];
237
+ action: NonNullable<SlashPopoverProps["action"]>;
238
+ iconMap?: SlashPopoverProps["iconMap"];
239
+ matcher?: SlashPopoverProps["matcher"];
240
+ };
241
+
242
+ export type ThreadMentionTrigger = {
243
+ adapter: SlashPopoverProps["adapter"];
244
+ directive: NonNullable<SlashPopoverProps["directive"]>;
245
+ iconMap?: SlashPopoverProps["iconMap"];
246
+ };
247
+
248
+ const DEFAULT_THREAD_ACTIONS: Required<ThreadActions> = {
249
+ undo: true,
250
+ feedback: false,
251
+ };
252
+
253
+ const ThreadActionsContext = createContext<Required<ThreadActions>>(
254
+ DEFAULT_THREAD_ACTIONS,
255
+ );
256
+
257
+ const EMPTY_COMPONENTS: ThreadComponents = {};
258
+
259
+ const ThreadComponentsContext =
260
+ createContext<ThreadComponents>(EMPTY_COMPONENTS);
261
+
262
+ type ThreadGroupDisclosure = {
263
+ reasoning: GroupDisclosureMode;
264
+ toolCalls: GroupDisclosureMode;
265
+ visibility: WorkVisibility;
266
+ };
267
+
268
+ const DEFAULT_GROUP_DISCLOSURE: ThreadGroupDisclosure = {
269
+ reasoning: "collapsed",
270
+ toolCalls: "collapsed",
271
+ visibility: "balanced",
272
+ };
273
+
274
+ const ThreadGroupDisclosureContext = createContext<ThreadGroupDisclosure>(
275
+ DEFAULT_GROUP_DISCLOSURE,
276
+ );
277
+
278
+ /**
279
+ * Private session. The work renders exactly as in a normal session — the same
280
+ * "Worked" chain, the same "N tool calls" group, the same rows, labels and
281
+ * icons, under the same `reasoningMode` / `toolCallsMode` / `workVisibility`
282
+ * rules — except that each reasoning and tool row is a `PrivateWorkStep`,
283
+ * which cannot be opened: the reasoning text and tool payloads are stripped
284
+ * server side and must not mount here either. Default false, so every skin
285
+ * that does not pass `privateMode` renders exactly as before.
286
+ */
287
+ const ThreadPrivateModeContext = createContext(false);
288
+
289
+ type ThreadChrome = {
290
+ welcome: string;
291
+ welcomeBody: readonly string[];
292
+ suggestions: readonly string[];
293
+ onComposerSubmit?: (event: FormEvent<HTMLFormElement>) => void;
294
+ className?: string;
295
+ slash?: ThreadSlashTrigger;
296
+ mention?: ThreadMentionTrigger;
297
+ };
298
+
299
+ const EMPTY_SUGGESTIONS: readonly string[] = [];
300
+
301
+ const DEFAULT_CHROME: ThreadChrome = {
302
+ welcome: "How can I help you today?",
303
+ welcomeBody: [],
304
+ suggestions: EMPTY_SUGGESTIONS,
305
+ };
306
+
307
+ const ThreadChromeContext = createContext<ThreadChrome>(DEFAULT_CHROME);
308
+
309
+ /** The scroll viewport element, for footer widgets that live outside it. */
310
+ const ThreadViewportElContext = createContext<HTMLElement | null>(null);
311
+
312
+ /**
313
+ * Edit-in-composer: clicking edit on a user message loads its text into the
314
+ * main composer (the only place messages are typed or changed). Resubmitting
315
+ * replaces that message and regenerates from it; unchanged text is a no-op
316
+ * (treated as a misclick), as is an empty or whitespace-only composer.
317
+ * Escape or a click outside the composer cancels and restores whatever draft
318
+ * was there before. The composer itself carries no editing chrome: it looks
319
+ * like a normal text box while a message is being edited.
320
+ */
321
+ type ComposerEditState = {
322
+ messageId: string;
323
+ original: string;
324
+ savedDraft: string;
325
+ };
326
+ type ComposerEditApi = {
327
+ editing: ComposerEditState | null;
328
+ begin: (messageId: string, text: string) => void;
329
+ cancel: () => void;
330
+ commit: (text: string) => void;
331
+ };
332
+ const ComposerEditContext = createContext<ComposerEditApi>({
333
+ editing: null,
334
+ begin: () => {},
335
+ cancel: () => {},
336
+ commit: () => {},
337
+ });
338
+
339
+ export function isUnchangedEdit(original: string, next: string): boolean {
340
+ return original.trim() === next.trim();
341
+ }
342
+
343
+ /**
344
+ * Should an in-composer edit be sent at all?
345
+ *
346
+ * No when the composer is empty or holds only whitespace (there is nothing to
347
+ * say), and no when the text is the message being edited (an edit that
348
+ * changes nothing is not worth a resend). Both cases end the edit without
349
+ * touching the message.
350
+ */
351
+ export function shouldCommitEdit(original: string, next: string): boolean {
352
+ if (!next.trim()) return false;
353
+ return !isUnchangedEdit(original, next);
354
+ }
355
+
356
+ const ComposerEditProvider: FC<PropsWithChildren> = ({ children }) => {
357
+ const aui = useAui();
358
+ const [editing, setEditing] = useState<ComposerEditState | null>(null);
359
+ const editingRef = useRef<ComposerEditState | null>(null);
360
+ editingRef.current = editing;
361
+
362
+ const begin = useCallback(
363
+ (messageId: string, text: string) => {
364
+ const prev = editingRef.current;
365
+ const savedDraft = prev ? prev.savedDraft : aui.composer().getState().text;
366
+ aui.composer().setText(text);
367
+ setEditing({ messageId, original: text, savedDraft });
368
+ },
369
+ [aui],
370
+ );
371
+ const cancel = useCallback(() => {
372
+ const cur = editingRef.current;
373
+ if (!cur) return;
374
+ aui.composer().setText(cur.savedDraft);
375
+ setEditing(null);
376
+ }, [aui]);
377
+ const commit = useCallback(
378
+ (text: string) => {
379
+ const cur = editingRef.current;
380
+ if (!cur) return;
381
+ if (!shouldCommitEdit(cur.original, text)) {
382
+ cancel();
383
+ return;
384
+ }
385
+ const mc = aui.thread().message({ id: cur.messageId }).composer();
386
+ mc.beginEdit();
387
+ mc.setText(text);
388
+ mc.send();
389
+ aui.composer().setText(cur.savedDraft);
390
+ setEditing(null);
391
+ },
392
+ [aui, cancel],
393
+ );
394
+ const value = useMemo(
395
+ () => ({ editing, begin, cancel, commit }),
396
+ [editing, begin, cancel, commit],
397
+ );
398
+ return (
399
+ <ComposerEditContext.Provider value={value}>
400
+ {children}
401
+ </ComposerEditContext.Provider>
402
+ );
403
+ };
404
+ // Startup exposes a loading placeholder thread; treat it as a new chat so
405
+ // Welcome docks in the footer above the composer. Loads after startup keep
406
+ // the docked layout.
407
+ const isNewChatView = (s: AssistantState) =>
408
+ s.thread.messages.length === 0 &&
409
+ (!s.thread.isLoading || s.threads.isLoading);
410
+
411
+ // A switched thread that is still fetching its history: skeleton, not welcome.
412
+ const isHistoryLoadingView = (s: AssistantState) =>
413
+ s.thread.messages.length === 0 &&
414
+ s.thread.isLoading &&
415
+ !s.thread.isDisabled &&
416
+ !s.threads.isLoading;
417
+
418
+ const ThreadHistorySkeleton: FC = () => (
419
+ <div
420
+ data-slot="aui_thread-history-skeleton"
421
+ role="status"
422
+ className="animate-in fade-in fill-mode-both flex flex-col gap-y-6 [animation-delay:150ms] [animation-duration:200ms]"
423
+ >
424
+ <span className="sr-only">Loading conversation</span>
425
+ <Skeleton variant="block" className="ms-auto h-9 w-2/5 rounded-none motion-reduce:animate-none" />
426
+ <div className="flex flex-col gap-y-2">
427
+ <Skeleton variant="line" className="h-4 w-11/12 motion-reduce:animate-none" />
428
+ <Skeleton variant="line" className="h-4 w-4/5 motion-reduce:animate-none" />
429
+ <Skeleton variant="line" className="h-4 w-3/5 motion-reduce:animate-none" />
430
+ </div>
431
+ <Skeleton variant="block" className="ms-auto h-9 w-1/3 rounded-none motion-reduce:animate-none" />
432
+ <div className="flex flex-col gap-y-2">
433
+ <Skeleton variant="line" className="h-4 w-10/12 motion-reduce:animate-none" />
434
+ <Skeleton variant="line" className="h-4 w-2/3 motion-reduce:animate-none" />
435
+ </div>
436
+ </div>
437
+ );
438
+
439
+ const EMPTY_HIDDEN_ATTACHMENTS: readonly string[] = [];
440
+
441
+ export const Thread: FC<ThreadProps> = ({
442
+ components = EMPTY_COMPONENTS,
443
+ autoFocus = true,
444
+ placeholder,
445
+ composerLayout = "expanded",
446
+ actions,
447
+ welcome,
448
+ welcomeBody,
449
+ suggestions,
450
+ className,
451
+ onComposerSubmit,
452
+ slash,
453
+ mention,
454
+ hiddenAttachmentNames,
455
+ reasoningMode = "collapsed",
456
+ toolCallsMode = "collapsed",
457
+ workVisibility = "balanced",
458
+ privateMode = false,
459
+ composerToolbar,
460
+ scrollbars = "always",
461
+ density = "default",
462
+ scrollMode = DEFAULT_SCROLL_MODE,
463
+ }) => {
464
+ const resolvedActions: Required<ThreadActions> = {
465
+ undo: actions?.undo !== false,
466
+ feedback: actions?.feedback === true,
467
+ };
468
+ const chrome: ThreadChrome = {
469
+ welcome: welcome ?? DEFAULT_CHROME.welcome,
470
+ welcomeBody: welcomeBody ?? DEFAULT_CHROME.welcomeBody,
471
+ suggestions: suggestions ?? EMPTY_SUGGESTIONS,
472
+ onComposerSubmit,
473
+ className,
474
+ slash,
475
+ mention,
476
+ };
477
+ return (
478
+ <HiddenAttachmentNamesProvider names={hiddenAttachmentNames ?? EMPTY_HIDDEN_ATTACHMENTS}>
479
+ <ThreadChromeContext.Provider value={chrome}>
480
+ <ThreadActionsContext.Provider value={resolvedActions}>
481
+ <ThreadComponentsContext.Provider value={components}>
482
+ <ThreadGroupDisclosureContext.Provider
483
+ value={{
484
+ reasoning: reasoningMode,
485
+ toolCalls: toolCallsMode,
486
+ visibility: workVisibility,
487
+ }}
488
+ >
489
+ <ComposerEditProvider>
490
+ <ThreadPrivateModeContext.Provider value={privateMode}>
491
+ <ThreadRoot
492
+ autoFocus={autoFocus}
493
+ placeholder={placeholder}
494
+ composerLayout={composerLayout}
495
+ composerToolbar={composerToolbar}
496
+ scrollbars={scrollbars}
497
+ density={density}
498
+ scrollMode={scrollMode}
499
+ />
500
+ </ThreadPrivateModeContext.Provider>
501
+ </ComposerEditProvider>
502
+ </ThreadGroupDisclosureContext.Provider>
503
+ </ThreadComponentsContext.Provider>
504
+ </ThreadActionsContext.Provider>
505
+ </ThreadChromeContext.Provider>
506
+ </HiddenAttachmentNamesProvider>
507
+ );
508
+ };
509
+
510
+ /**
511
+ * Default branding footer: "powered by digichat" in normal flow below the
512
+ * composer in every state. This footer's `sticky bottom-0 mt-auto` docks it
513
+ * to the page bottom unconditionally, so in-flow is already bottom-pinned —
514
+ * an absolute overlay would paint over the composer (it did; the textbox
515
+ * cut the credit off in app, product, and embed modes). Hosts can still
516
+ * override the whole Footer slot.
517
+ */
518
+ const ThreadFooter: FC = () => <SkinCredit />;
519
+
520
+ /**
521
+ * Keeps the viewport on the stream while following. A component rather than a
522
+ * hook call in `ThreadRoot` because the message count lives in the thread
523
+ * store, which only exists inside `ThreadPrimitive.Root`.
524
+ */
525
+ const ScrollFollower: FC<{ viewport: HTMLElement | null; enabled: boolean }> = ({
526
+ viewport,
527
+ enabled,
528
+ }) => {
529
+ const messageCount = useAuiState((s) => s.thread.messages.length);
530
+ useFollowStream(viewport, enabled, messageCount);
531
+ return null;
532
+ };
533
+
534
+ const ThreadRoot: FC<{
535
+ autoFocus: boolean;
536
+ placeholder?: string | undefined;
537
+ composerLayout: ComposerLayout;
538
+ composerToolbar?: ReactNode;
539
+ scrollbars: "always" | "auto";
540
+ density: "compact" | "default" | "airy";
541
+ scrollMode: ScrollMode;
542
+ }> = ({ autoFocus, placeholder, composerLayout, composerToolbar, scrollbars, density, scrollMode }) => {
543
+ const { Welcome = ThreadWelcome, Footer = ThreadFooter } =
544
+ useContext(ThreadComponentsContext);
545
+ const { className } = useContext(ThreadChromeContext);
546
+ const [viewportEl, setViewportEl] = useState<HTMLElement | null>(null);
547
+ const viewportRef = useRef<HTMLElement | null>(null);
548
+ const attachViewport = useCallback((node: HTMLElement | null) => {
549
+ viewportRef.current = node;
550
+ setViewportEl(node);
551
+ }, []);
552
+
553
+ const viewportProps = scrollModeProps(scrollMode);
554
+
555
+ return (
556
+ <ThreadPrimitive.Root
557
+ className={cn(
558
+ "aui-root aui-thread-root digichat-thread bg-surface @container flex h-full flex-col",
559
+ className,
560
+ )}
561
+ data-user-align="left"
562
+ // The skin scope marker: `chat-digichat.css` carries the whole
563
+ // first-party theme under `:is(.aui-theme-stage, [data-thread-skin="digichat"])`
564
+ // and the portal mirrors under `html:has([data-thread-skin="digichat"])`.
565
+ // Declaring it here means every mount path gets the same theme — the
566
+ // /baseline catalog, the embed shell, and the production shells — instead
567
+ // of each surface having to remember to wrap the Thread.
568
+ data-thread-skin="digichat"
569
+ data-scrollbars={scrollbars}
570
+ data-density={density}
571
+ style={{
572
+ ["--thread-max-width" as string]: "44rem",
573
+ // `--card` (not `--color-card`) so a scoped palette wins: the
574
+ // Tailwind bridge declares `--color-card` at :root, so its var()
575
+ // resolves there and ignores `.digichat-thread`'s own `--card`.
576
+ ["--composer-bg" as string]: "var(--card)",
577
+ ["--composer-radius" as string]: "0",
578
+ // `--composer-padding` is a density token (chat-aui.css), so it is not
579
+ // pinned inline here — an inline value would win over the mode.
580
+ }}
581
+ >
582
+ {/* The footer (composer + credit) sits outside the scroll viewport so
583
+ messages end above the text box. ViewportProvider keeps the
584
+ scroll-to-bottom control wired to the viewport store. */}
585
+ <ThreadPrimitive.ViewportProvider>
586
+ <ThreadViewportElContext.Provider value={viewportEl}>
587
+ <div className="relative flex min-h-0 flex-1 flex-col">
588
+ <ThreadPrimitive.Viewport
589
+ ref={attachViewport}
590
+ {...viewportProps}
591
+ // The top-anchored message carries `padding-top: var(--density-gap-4)` (chat-aui.css, taken back as margin)
592
+ // so it does not sit flush against the viewport top. `tallerThan` scores
593
+ // the anchor's offsetHeight, which includes that padding, so it is
594
+ // raised by the same 1.5rem to keep the set of fully-pinned messages
595
+ // unchanged from before the padding was added.
596
+ topAnchorMessageClamp={{ tallerThan: "11.5em", visibleHeight: "6em" }}
597
+ data-slot="aui_thread-viewport"
598
+ className="relative flex min-h-0 flex-1 flex-col overflow-x-hidden overflow-y-scroll scroll-smooth digichat-thread__viewport"
599
+ >
600
+ <ScrollFollower viewport={viewportEl} enabled={scrollMode === "follow"} />
601
+ <div className="mx-auto flex w-full max-w-(--thread-max-width) flex-1 flex-col px-4 pt-4">
602
+ <AuiIf condition={isHistoryLoadingView}>
603
+ <ThreadHistorySkeleton />
604
+ </AuiIf>
605
+
606
+ <div
607
+ data-slot="aui_message-group"
608
+ className="flex flex-col empty:hidden"
609
+ >
610
+ <ThreadPrimitive.Messages>
611
+ {() => <ThreadMessage />}
612
+ </ThreadPrimitive.Messages>
613
+ </div>
614
+ </div>
615
+ </ThreadPrimitive.Viewport>
616
+ <OverlayScrollbar targetRef={viewportRef} width={10} />
617
+ </div>
618
+ <div className="mx-auto flex w-full max-w-(--thread-max-width) shrink-0 flex-col px-4">
619
+ <div className="aui-thread-viewport-footer digichat-thread__footer bg-surface relative flex flex-col gap-4 overflow-visible pt-2 pb-2 md:pb-3">
620
+ <ThreadScrollToBottom />
621
+ <ThreadFollowupSuggestions />
622
+ <AuiIf condition={isNewChatView}>
623
+ <div
624
+ data-slot="aui_thread-empty"
625
+ className="aui-thread-empty flex flex-col gap-3"
626
+ >
627
+ <Welcome />
628
+ <ThreadSuggestions />
629
+ </div>
630
+ </AuiIf>
631
+ <Composer
632
+ autoFocus={autoFocus}
633
+ placeholder={placeholder}
634
+ layout={composerLayout}
635
+ toolbar={composerToolbar}
636
+ />
637
+ {Footer ? <Footer /> : null}
638
+ </div>
639
+ </div>
640
+ </ThreadViewportElContext.Provider>
641
+ </ThreadPrimitive.ViewportProvider>
642
+ </ThreadPrimitive.Root>
643
+ );
644
+ };
645
+
646
+ const ThreadMessage: FC = () => {
647
+ const { AssistantMessage: AssistantMessageComponent = AssistantMessage } =
648
+ useContext(ThreadComponentsContext);
649
+ const role = useAuiState((s) => s.message.role);
650
+ // Edits happen in the main composer (ComposerEditProvider); a message's own
651
+ // edit composer is only opened transiently to submit, so never render it.
652
+ if (role === "user") return <UserMessage />;
653
+ return <AssistantMessageComponent />;
654
+ };
655
+
656
+ const ThreadScrollToBottom: FC = () => {
657
+ const viewportEl = useContext(ThreadViewportElContext);
658
+ const [revealSettled, setRevealSettled] = useState(false);
659
+ const [scrolledAway, setScrolledAway] = useState(false);
660
+
661
+ // Arm only after the bottom-up entrance has finished - the button used to
662
+ // pop in mid-animation.
663
+ useEffect(() => {
664
+ const owner = viewportEl?.closest("[data-stock-product]");
665
+ if (!owner) {
666
+ setRevealSettled(true);
667
+ return;
668
+ }
669
+ let timer: ReturnType<typeof setTimeout> | undefined;
670
+ const arm = () => {
671
+ if (owner.getAttribute("data-boot-reveal") !== "true") return;
672
+ observer.disconnect();
673
+ timer = setTimeout(() => setRevealSettled(true), 950);
674
+ };
675
+ const observer = new MutationObserver(arm);
676
+ observer.observe(owner, {
677
+ attributes: true,
678
+ attributeFilter: ["data-boot-reveal"],
679
+ });
680
+ arm();
681
+ return () => {
682
+ observer.disconnect();
683
+ if (timer !== undefined) clearTimeout(timer);
684
+ };
685
+ }, [viewportEl]);
686
+
687
+ // Show only when content is actually hidden under the composer: the message
688
+ // list's bottom must sit below the top edge of the composer shell. A scroll
689
+ // of a few pixels with everything still visible (e.g. after the entrance
690
+ // settle) must not surface it.
691
+ useEffect(() => {
692
+ if (!viewportEl) return;
693
+ let observedGroup: HTMLElement | null = null;
694
+ let observedComposer: HTMLElement | null = null;
695
+ const measure = () => {
696
+ // Re-query each run: neither node is guaranteed to be mounted yet (and
697
+ // either may remount), so a captured reference could go stale.
698
+ const group = viewportEl.querySelector<HTMLElement>(
699
+ '[data-slot="aui_message-group"]',
700
+ );
701
+ // The composer sits below the scroll viewport, on the thread root.
702
+ const composer = (
703
+ viewportEl.closest(".aui-thread-root") ?? viewportEl
704
+ ).querySelector<HTMLElement>('[data-slot="aui_composer-shell"]');
705
+ if (!group || !composer) {
706
+ setScrolledAway(false);
707
+ return;
708
+ }
709
+ if (group !== observedGroup) {
710
+ if (observedGroup) ro.unobserve(observedGroup);
711
+ ro.observe(group);
712
+ observedGroup = group;
713
+ }
714
+ if (composer !== observedComposer) {
715
+ if (observedComposer) ro.unobserve(observedComposer);
716
+ ro.observe(composer);
717
+ observedComposer = composer;
718
+ }
719
+ setScrolledAway(
720
+ group.getBoundingClientRect().bottom >
721
+ composer.getBoundingClientRect().top + 1,
722
+ );
723
+ };
724
+ const ro = new ResizeObserver(measure);
725
+ measure();
726
+ viewportEl.addEventListener("scroll", measure, { passive: true });
727
+ ro.observe(viewportEl);
728
+ return () => {
729
+ viewportEl.removeEventListener("scroll", measure);
730
+ ro.disconnect();
731
+ };
732
+ }, [viewportEl]);
733
+
734
+ if (!revealSettled || !scrolledAway) return null;
735
+
736
+ return (
737
+ <ThreadPrimitive.ScrollToBottom asChild>
738
+ <TooltipIconButton
739
+ tooltip="Scroll to bottom"
740
+ variant="outline"
741
+ className="aui-thread-scroll-to-bottom border-hair bg-surface hover:bg-surface-2 absolute -top-12 z-10 self-center rounded-none p-4 disabled:invisible"
742
+ >
743
+ <DotMatrix state="scroll" decorative className="size-3.5" />
744
+ </TooltipIconButton>
745
+ </ThreadPrimitive.ScrollToBottom>
746
+ );
747
+ };
748
+
749
+ const ThreadWelcome: FC = () => {
750
+ const { welcome, welcomeBody } = useContext(ThreadChromeContext);
751
+ return (
752
+ <div
753
+ data-slot="aui_thread-welcome"
754
+ className="aui-thread-welcome-root flex flex-col items-start text-start"
755
+ >
756
+ <h1 className="aui-thread-welcome-message-inner fade-in slide-in-from-bottom-1 animate-in fill-mode-both text-2xl font-medium tracking-tight duration-(--duration-hover)">
757
+ {welcome}
758
+ </h1>
759
+ <TypedWelcomeCopy lines={welcomeBody} />
760
+ </div>
761
+ );
762
+ };
763
+
764
+ /**
765
+ * Shared look for a welcome prompt row. The runtime-backed item and the static
766
+ * fallback render the same markup so a skin's examples look identical whether
767
+ * they came from the runtime or from `chrome.suggestions`.
768
+ */
769
+ const SUGGESTION_BUTTON_CLASS =
770
+ "aui-thread-welcome-suggestion fade-in slide-in-from-bottom-1 animate-in fill-mode-both grid w-full cursor-pointer grid-cols-[1.25rem_minmax(0,1fr)] items-start gap-x-[0.55rem] rounded-none border-0 bg-transparent px-0 py-0.5 text-start text-sm font-normal duration-(--duration-hover)";
771
+
772
+ const ThreadSuggestions: FC = () => {
773
+ const { suggestions } = useContext(ThreadChromeContext);
774
+ // Keyed by the cycle's group offset so a new group remounts and re-runs the
775
+ // chips' entrance fade; hover/focus handlers hold the cycle while it is read.
776
+ const { groupKey, handlers } = useSuggestionGroup();
777
+ return (
778
+ <div
779
+ key={groupKey}
780
+ className="aui-thread-welcome-suggestions flex w-full flex-col items-stretch gap-0.5"
781
+ {...handlers}
782
+ >
783
+ {suggestions.length > 0 ? (
784
+ suggestions.map((prompt, index) => (
785
+ <StaticSuggestionItem key={`${index}:${prompt}`} prompt={prompt} />
786
+ ))
787
+ ) : (
788
+ <ThreadPrimitive.Suggestions>
789
+ {() => <ThreadSuggestionItem />}
790
+ </ThreadPrimitive.Suggestions>
791
+ )}
792
+ </div>
793
+ );
794
+ };
795
+
796
+ /** Static `chrome.suggestions` prompt. Appends the text like a typed message. */
797
+ const StaticSuggestionItem: FC<{ prompt: string }> = ({ prompt }) => {
798
+ const aui = useAui();
799
+ return (
800
+ <button
801
+ type="button"
802
+ onClick={() => {
803
+ if (aui.thread.getState().isRunning) return;
804
+ // A tapped example is a send: on iOS a button does not take focus, so
805
+ // the composer would keep the keyboard up over the answer.
806
+ dismissKeyboardIfCoarse();
807
+ aui.thread.append({
808
+ content: [{ type: "text", text: prompt }],
809
+ runConfig: aui.composer.getState().runConfig,
810
+ });
811
+ }}
812
+ className={SUGGESTION_BUTTON_CLASS}
813
+ >
814
+ <span className="aui-msg-marker" aria-hidden="true">
815
+ <DotMatrix
816
+ state="example"
817
+ decorative
818
+ className="aui-thread-welcome-suggestion-mark size-3.5"
819
+ />
820
+ </span>
821
+ <span className="aui-thread-welcome-suggestion-text min-w-0">
822
+ <span className="aui-thread-welcome-suggestion-text-1">{prompt}</span>
823
+ </span>
824
+ </button>
825
+ );
826
+ };
827
+
828
+ const ThreadSuggestionItem: FC = () => {
829
+ return (
830
+ <SuggestionPrimitive.Trigger send asChild>
831
+ <button
832
+ type="button"
833
+ className={SUGGESTION_BUTTON_CLASS}
834
+ onClick={() => dismissKeyboardIfCoarse()}
835
+ >
836
+ <span className="aui-msg-marker" aria-hidden="true">
837
+ <DotMatrix
838
+ state="example"
839
+ decorative
840
+ className="aui-thread-welcome-suggestion-mark size-3.5"
841
+ />
842
+ </span>
843
+ <span className="aui-thread-welcome-suggestion-text min-w-0">
844
+ <SuggestionPrimitive.Title className="aui-thread-welcome-suggestion-text-1" />
845
+ <SuggestionPrimitive.Description className="aui-thread-welcome-suggestion-text-2 empty:hidden" />
846
+ </span>
847
+ </button>
848
+ </SuggestionPrimitive.Trigger>
849
+ );
850
+ };
851
+
852
+ const Composer: FC<{
853
+ autoFocus: boolean;
854
+ placeholder?: string | undefined;
855
+ layout: ComposerLayout;
856
+ toolbar?: ReactNode;
857
+ }> = ({ autoFocus, placeholder = "Send a message...", layout, toolbar }) => {
858
+ const compact = layout === "compact";
859
+ const { onComposerSubmit, slash, mention } = useContext(ThreadChromeContext);
860
+ const edit = useContext(ComposerEditContext);
861
+ const aui = useAui();
862
+ const inputWrapRef = useRef<HTMLDivElement | null>(null);
863
+ const composerRootRef = useRef<HTMLFormElement | null>(null);
864
+ const isEditingMessage = edit.editing !== null;
865
+ // Read after mount, not during render: the server has no matchMedia, and a
866
+ // phone reporting "coarse" on its first render would hydrate a different
867
+ // tree than the one that was sent.
868
+ const [coarsePointer, setCoarsePointer] = useState(false);
869
+ useEffect(() => {
870
+ setCoarsePointer(isCoarsePointer());
871
+ }, []);
872
+
873
+ const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
874
+ if (edit.editing) {
875
+ event.preventDefault();
876
+ edit.commit(aui.composer().getState().text);
877
+ // An edit is a send: the corrected text goes off and the answer starts
878
+ // streaming. Leaving the keyboard up would hide it, so the same
879
+ // dismissal runs here as on a normal submit.
880
+ dismissKeyboardIfCoarse();
881
+ return;
882
+ }
883
+ // Captured before the send clears the textarea — see composerHasText.
884
+ const sent = composerHasText(composerRootRef.current);
885
+ onComposerSubmit?.(event);
886
+ // On a phone the keyboard would otherwise stay up over the streaming
887
+ // answer; blurring retracts it and leaves the thread in full view. A
888
+ // no-op on a fine pointer, so a desktop keeps its caret for the next turn.
889
+ if (sent) dismissComposerKeyboard(composerRootRef.current);
890
+ };
891
+
892
+ // Click outside the composer cancels an in-progress edit. The edit button
893
+ // itself is excluded so re-clicking edit on another message switches target.
894
+ useEffect(() => {
895
+ if (!isEditingMessage) return;
896
+ const onPointerDown = (event: PointerEvent) => {
897
+ const target = event.target as Element | null;
898
+ if (!target) return;
899
+ if (composerRootRef.current?.contains(target)) return;
900
+ if (target.closest(".aui-user-action-edit, [data-radix-popper-content-wrapper]")) return;
901
+ edit.cancel();
902
+ };
903
+ document.addEventListener("pointerdown", onPointerDown, true);
904
+ return () => document.removeEventListener("pointerdown", onPointerDown, true);
905
+ }, [isEditingMessage, edit]);
906
+
907
+ // Focus the composer when an edit starts.
908
+ useEffect(() => {
909
+ if (!isEditingMessage) return;
910
+ const area = inputWrapRef.current?.querySelector<HTMLTextAreaElement>("textarea");
911
+ if (!area) return;
912
+ area.focus({ preventScroll: true });
913
+ const end = area.value.length;
914
+ area.setSelectionRange(end, end);
915
+ }, [isEditingMessage, edit.editing?.messageId]);
916
+ const focusComposerInput = () => {
917
+ inputWrapRef.current
918
+ ?.querySelector<HTMLTextAreaElement>("textarea")
919
+ ?.focus({ preventScroll: true });
920
+ };
921
+
922
+ // Focus the composer on mount. The runtime's own autoFocus effect can miss
923
+ // when the embed hydrates inside an iframe, and a retry covers engines that
924
+ // ignore focus() until the first paint.
925
+ useEffect(() => {
926
+ if (!autoFocus) return;
927
+ let cancelled = false;
928
+ let attempts = 0;
929
+ const focus = () => {
930
+ if (cancelled) return;
931
+ const area =
932
+ inputWrapRef.current?.querySelector<HTMLTextAreaElement>("textarea");
933
+ if (!area || document.activeElement === area) return;
934
+ const active = document.activeElement;
935
+ if (active instanceof HTMLElement && active !== document.body) return;
936
+ area.focus({ preventScroll: true });
937
+ if (document.activeElement !== area && attempts < 6) {
938
+ attempts += 1;
939
+ window.requestAnimationFrame(focus);
940
+ }
941
+ };
942
+ const frame = window.requestAnimationFrame(focus);
943
+ return () => {
944
+ cancelled = true;
945
+ window.cancelAnimationFrame(frame);
946
+ };
947
+ }, [autoFocus]);
948
+
949
+ const handleShellMouseDown = (event: ReactMouseEvent<HTMLDivElement>) => {
950
+ const target = event.target as HTMLElement | null;
951
+ if (
952
+ target?.closest(
953
+ "button, a, input, textarea, select, [contenteditable='true']",
954
+ )
955
+ ) {
956
+ return;
957
+ }
958
+ event.preventDefault();
959
+ focusComposerInput();
960
+ };
961
+ const bar = (
962
+ <ComposerPrimitive.Root
963
+ ref={composerRootRef}
964
+ className="aui-composer-root relative flex w-full flex-col"
965
+ onSubmit={handleSubmit}
966
+ onKeyDownCapture={(event) => {
967
+ if (isEditingMessage && event.key === "Escape") {
968
+ event.preventDefault();
969
+ event.stopPropagation();
970
+ edit.cancel();
971
+ }
972
+ }}
973
+ >
974
+ <ComposerPrimitive.AttachmentDropzone asChild>
975
+ <div
976
+ data-slot="aui_composer-shell"
977
+ data-layout={layout}
978
+ onMouseDown={handleShellMouseDown}
979
+ className="border-hair data-[dragging=true]:border-accent flex w-full cursor-text flex-col gap-2 rounded-(--composer-radius) border bg-(--composer-bg) p-(--composer-padding,0.5rem) data-[dragging=true]:border-dashed data-[dragging=true]:bg-[color-mix(in_oklab,var(--accent)_50%,var(--bg))]"
980
+ >
981
+ <ComposerAttachments />
982
+ {/* One row: attach, input, send. The toolbar used to sit on a second
983
+ row inside the same border, which read as a two-line text box. */}
984
+ <div className="aui-composer-main flex min-w-0 items-end gap-1">
985
+ <ComposerAddAttachment />
986
+ <div
987
+ ref={inputWrapRef}
988
+ className="aui-composer-input-wrap relative min-w-0 flex-1"
989
+ >
990
+ <ComposerPrimitive.Input
991
+ placeholder={placeholder}
992
+ className={cn(
993
+ "aui-composer-input placeholder:text-ink-mute w-full resize-none overflow-x-hidden overflow-y-auto break-words whitespace-pre-wrap bg-transparent outline-none",
994
+ compact
995
+ ? "min-h-7 max-h-[8.75rem] px-1.5 py-0 text-sm leading-7"
996
+ : "min-h-[1.375rem] max-h-[7.75rem] px-2.5 py-0.5 text-base leading-6",
997
+ )}
998
+ rows={1}
999
+ maxRows={5}
1000
+ autoFocus={autoFocus}
1001
+ submitMode="enter"
1002
+ enterKeyHint="send"
1003
+ // assistant-ui refocuses the composer when a run starts and
1004
+ // when the viewport scrolls back to the bottom. On a phone
1005
+ // that reopens the keyboard over the answer we just revealed,
1006
+ // so both are off there; on a desktop the caret is welcome.
1007
+ unstable_focusOnRunStart={!coarsePointer}
1008
+ unstable_focusOnScrollToBottom={!coarsePointer}
1009
+ aria-label="Message"
1010
+ />
1011
+ <ComposerBlockCaret containerRef={inputWrapRef} />
1012
+ </div>
1013
+ <ComposerSendControls />
1014
+ </div>
1015
+ </div>
1016
+ </ComposerPrimitive.AttachmentDropzone>
1017
+ {toolbar ? (
1018
+ <div data-slot="aui_composer-toolbar" className="aui-composer-toolbar">
1019
+ {toolbar}
1020
+ </div>
1021
+ ) : null}
1022
+ {slash ? (
1023
+ <ComposerTriggerPopover
1024
+ char="/"
1025
+ adapter={slash.adapter}
1026
+ action={slash.action}
1027
+ iconMap={slash.iconMap}
1028
+ matcher={slash.matcher}
1029
+ emptyItemsLabel="No matching commands"
1030
+ />
1031
+ ) : null}
1032
+ {mention ? (
1033
+ <ComposerTriggerPopover
1034
+ char="@"
1035
+ adapter={mention.adapter}
1036
+ directive={mention.directive}
1037
+ iconMap={mention.iconMap}
1038
+ emptyItemsLabel="No matching tools"
1039
+ />
1040
+ ) : null}
1041
+ </ComposerPrimitive.Root>
1042
+ );
1043
+ if (!slash && !mention) return bar;
1044
+ return (
1045
+ <ComposerPrimitive.Unstable_TriggerPopoverRoot>
1046
+ {bar}
1047
+ </ComposerPrimitive.Unstable_TriggerPopoverRoot>
1048
+ );
1049
+ };
1050
+
1051
+ const ComposerSendControls: FC = () => {
1052
+ const aui = useAui();
1053
+ // Mirrors `composerSendDisabled` from `@assistant-ui/core`'s primitive
1054
+ // predicates. Inlined rather than imported: the predicate is not re-exported
1055
+ // from `@assistant-ui/react` (the only assistant-ui package this one depends
1056
+ // on), so a deep import would resolve only thanks to npm hoisting.
1057
+ const sendDisabled = useAuiState(
1058
+ (s) => !s.composer.canSend || (s.thread.isRunning && !s.thread.capabilities.queue),
1059
+ );
1060
+ // A `type="submit"` button already submits its form on activation, so the
1061
+ // click must not also do it: `preventDefault()` suppresses that implicit
1062
+ // submission, and the explicit `requestSubmit()` below replaces it with one
1063
+ // we control. Without this the form's `submit` fires twice per click, and the
1064
+ // free-turn gate mounted on `onSubmit` runs twice — holding or charging the
1065
+ // turn more than once.
1066
+ const submitComposer = (event: ReactMouseEvent<HTMLButtonElement>) => {
1067
+ event.preventDefault();
1068
+ const form = event.currentTarget.closest("form");
1069
+ if (form) {
1070
+ form.requestSubmit();
1071
+ return;
1072
+ }
1073
+ aui.composer.send();
1074
+ };
1075
+
1076
+ return (
1077
+ <div className="aui-composer-send-controls ml-auto flex shrink-0 items-center gap-1.5">
1078
+ <AuiIf condition={(s) => s.thread.capabilities.dictation}>
1079
+ <AuiIf condition={(s) => s.composer.dictation == null}>
1080
+ <ComposerPrimitive.Dictate asChild>
1081
+ <TooltipIconButton
1082
+ tooltip="Voice input"
1083
+ side="bottom"
1084
+ type="button"
1085
+ variant="ghost"
1086
+ size="icon"
1087
+ className="aui-composer-dictate text-ink-mute hover:text-ink size-7 rounded-none"
1088
+ aria-label="Start voice input"
1089
+ >
1090
+ <DotMatrix
1091
+ state="dictate"
1092
+ decorative
1093
+ className="aui-composer-dictate-icon size-3.5"
1094
+ />
1095
+ </TooltipIconButton>
1096
+ </ComposerPrimitive.Dictate>
1097
+ </AuiIf>
1098
+ <AuiIf condition={(s) => s.composer.dictation != null}>
1099
+ <ComposerPrimitive.StopDictation asChild>
1100
+ <TooltipIconButton
1101
+ tooltip="Stop dictation"
1102
+ side="bottom"
1103
+ type="button"
1104
+ variant="ghost"
1105
+ size="icon"
1106
+ className="aui-composer-stop-dictation text-danger size-7 rounded-none"
1107
+ aria-label="Stop voice input"
1108
+ >
1109
+ <DotMatrix
1110
+ state="stop"
1111
+ decorative
1112
+ className="aui-composer-stop-dictation-icon size-3.5"
1113
+ />
1114
+ </TooltipIconButton>
1115
+ </ComposerPrimitive.StopDictation>
1116
+ </AuiIf>
1117
+ </AuiIf>
1118
+ <AuiIf condition={(s) => !s.thread.isRunning}>
1119
+ {/* No tooltip/dropdown on send: the button submits on click and on
1120
+ Enter, so the "Send (Enter)" popover was noise. */}
1121
+ <Button
1122
+ dress="chat"
1123
+ type="submit"
1124
+ variant="ghost"
1125
+ size="icon"
1126
+ className="aui-composer-send size-7 rounded-none"
1127
+ aria-label="Send message"
1128
+ disabled={sendDisabled}
1129
+ onClick={submitComposer}
1130
+ >
1131
+ <DotMatrix
1132
+ state="send"
1133
+ decorative
1134
+ className="aui-composer-send-icon size-3.5"
1135
+ />
1136
+ </Button>
1137
+ </AuiIf>
1138
+ <AuiIf condition={(s) => s.thread.isRunning}>
1139
+ <ComposerPrimitive.Cancel asChild>
1140
+ <Button dress="chat"
1141
+ type="button"
1142
+ variant="default"
1143
+ size="icon"
1144
+ className="aui-composer-cancel size-7 rounded-none"
1145
+ aria-label="Stop generating"
1146
+ >
1147
+ <DotMatrix
1148
+ state="stop"
1149
+ decorative
1150
+ className="aui-composer-cancel-icon size-3.5"
1151
+ />
1152
+ </Button>
1153
+ </ComposerPrimitive.Cancel>
1154
+ </AuiIf>
1155
+ </div>
1156
+ );
1157
+ };
1158
+
1159
+ /** Thread-level cubes only when no tool is already animating its own matrix. */
1160
+ const AssistantWorkingIndicator: FC = () => {
1161
+ const toolRunning = useAuiState((s) =>
1162
+ s.message.parts.some(
1163
+ (part) => part.type === "tool-call" && part.status.type === "running",
1164
+ ),
1165
+ );
1166
+ // Text only when the server sent a status message (DIG-4459): a bare
1167
+ // `connecting` is the plain wait for a response, so the row is the
1168
+ // animation alone. No client-side default copy.
1169
+ const statusText = useAuiState((s) => connectionStatusText(s.message.parts));
1170
+ const wakePhase = useDigichatWarmupPhase();
1171
+ if (toolRunning) return null;
1172
+ const statusLabel = wakePhase === "waking" ? "Waking up services…" : statusText;
1173
+ return (
1174
+ <span
1175
+ data-slot="aui_assistant-message-indicator"
1176
+ className="aui-work-status"
1177
+ >
1178
+ <WorkStatusLead
1179
+ marker="loading"
1180
+ markerLabel="Assistant is working"
1181
+ label={statusLabel}
1182
+ />
1183
+ </span>
1184
+ );
1185
+ };
1186
+
1187
+ const AssistantMessage: FC = () => {
1188
+ const {
1189
+ ToolFallback: ToolFallbackComponent = ToolFallback,
1190
+ ToolGroup,
1191
+ ReasoningGroup,
1192
+ } = useContext(ThreadComponentsContext);
1193
+ const {
1194
+ reasoning: reasoningMode,
1195
+ toolCalls: toolCallsMode,
1196
+ visibility,
1197
+ } = useContext(ThreadGroupDisclosureContext);
1198
+ const privateMode = useContext(ThreadPrivateModeContext);
1199
+ const disclosure = resolveWorkDisclosure(visibility);
1200
+ // The whole turn, not just the group: the tools answer faster than the
1201
+ // chain can fold, and a group that closes between two of them flickers.
1202
+ const turnRunning = useAuiState((s) => s.thread.isRunning);
1203
+ // The message parts, to tell a chain that holds real work from one that
1204
+ // only holds a source or a status part.
1205
+ const messageParts = useAuiState((s) => s.message.parts);
1206
+ // Only text ends a work run: a chain with a text part after it has
1207
+ // answered, so its work is over. A chain without one is still the agent's
1208
+ // current work, even if the model streams another run of tool calls.
1209
+ const hasWork = messageHasWork(messageParts);
1210
+ // Anything real that arrived from the agent — reasoning, a tool call, the
1211
+ // answer itself. A status part (the connection state) is not content: it is
1212
+ // the pre-response phase, which is exactly when the loading row belongs.
1213
+ const hasContent = messageHasContent(messageParts);
1214
+ const followedByText = (indices: readonly number[]) =>
1215
+ chainFollowedByText(messageParts, indices);
1216
+
1217
+ const ACTION_BAR_PT = "pt-1.5";
1218
+ // Keep the action bar inside the contained root's paint box, then cancel its reserved space in flow.
1219
+ const ACTION_BAR_HEIGHT = `min-h-7.5 ${ACTION_BAR_PT}`;
1220
+
1221
+ return (
1222
+ <MessagePrimitive.Root
1223
+ data-slot="aui_assistant-message-root"
1224
+ data-role="assistant"
1225
+ className="aui-msg fade-in slide-in-from-bottom-1 animate-in relative grid grid-cols-[minmax(0,1fr)] items-start -mb-7.5 pb-7.5 duration-(--dur-fast)"
1226
+ >
1227
+ <div
1228
+ data-slot="aui_assistant-message-content"
1229
+ className="aui-assistant-message-content text-ink min-w-0 leading-relaxed wrap-break-word"
1230
+ >
1231
+ <MessagePrimitive.GroupedParts
1232
+ groupBy={chainGroupBy}
1233
+ >
1234
+ {({ part, children }) => {
1235
+ switch (part.type) {
1236
+ case "group-chainOfThought": {
1237
+ // A status part is not work. The grouping puts the
1238
+ // connection state into the chain, so without this guard the
1239
+ // chain header appears before the model has said anything —
1240
+ // right next to the loading row, which is the double
1241
+ // animation we do not want. No reasoning and no tool call in
1242
+ // the group means no work block: the loading row covers that
1243
+ // phase on its own.
1244
+ const holdsWork = chainHoldsWork(messageParts, part.indices);
1245
+ // The parts still render bare: a backend notice (DIG-4459)
1246
+ // rides a data part and must not vanish with the header.
1247
+ if (!holdsWork) return <>{children}</>;
1248
+ // A private session folds the chain exactly like a normal one:
1249
+ // the same header, the same view rules. Only the rows inside
1250
+ // differ (see `PrivateWorkStep`).
1251
+ // The chain is the one disclosure boundary: every work part
1252
+ // nests under it (see `chainGroupBy`), so the level's fold
1253
+ // rule here reaches a reasoning-only, a tool-only and a
1254
+ // combined turn alike. The rows inside take their own rule
1255
+ // from the same disclosure. See the decision note in
1256
+ // packages/ui/ARCHITECTURE.md.
1257
+ //
1258
+ // The `hidden` level this branch served is retired: the
1259
+ // `summaryOnly` field is always false now, and a quiet
1260
+ // client gets the folded chain below instead of a summary
1261
+ // line. The branch stays until the field goes.
1262
+ if (disclosure.summaryOnly) {
1263
+ return (
1264
+ <div data-slot="aui_chain-of-thought">
1265
+ <WorkChainSummary indices={part.indices} />
1266
+ </div>
1267
+ );
1268
+ }
1269
+ // One header for the whole chain: the reasoning and the tool
1270
+ // calls collapse together.
1271
+ return (
1272
+ <div data-slot="aui_chain-of-thought">
1273
+ <WorkChain
1274
+ indices={part.indices}
1275
+ openAtRest={disclosure.openAtRest}
1276
+ foldOnAnswer={disclosure.foldOnAnswer}
1277
+ running={part.status.type === "running"}
1278
+ followedByText={followedByText(part.indices)}
1279
+ >
1280
+ {children}
1281
+ </WorkChain>
1282
+ </div>
1283
+ );
1284
+ }
1285
+ case "group-tool": {
1286
+ // A private session keeps the "N tool calls" group: opening it
1287
+ // lists the rows, and only the rows themselves stay shut.
1288
+ if (toolCallsMode === "off") return null;
1289
+ if (ToolGroup) {
1290
+ return <ToolGroup group={part}>{children}</ToolGroup>;
1291
+ }
1292
+ const running = part.status.type === "running";
1293
+ // The group can hold the source and status parts that sit
1294
+ // between two tool runs; the count is the tool calls only.
1295
+ const toolCount = chainToolCount(messageParts, part.indices);
1296
+ if (toolCount === 0) {
1297
+ return <div data-slot="aui_chain-of-thought">{children}</div>;
1298
+ }
1299
+ const group = toolGroupDisclosure(
1300
+ disclosure,
1301
+ toolCallsMode,
1302
+ running || turnRunning,
1303
+ );
1304
+ const locked = group.locked;
1305
+ return (
1306
+ <ToolGroupRoot
1307
+ variant="ghost"
1308
+ defaultOpen={group.defaultOpen}
1309
+ streamingOpen={group.streamingOpen}
1310
+ {...(locked ? { open: true, onOpenChange: () => {} } : {})}
1311
+ >
1312
+ <ToolGroupTrigger
1313
+ count={toolCount}
1314
+ active={part.status.type === "running"}
1315
+ disabled={locked}
1316
+ />
1317
+ <ToolGroupContent>{children}</ToolGroupContent>
1318
+ </ToolGroupRoot>
1319
+ );
1320
+ }
1321
+ case "group-reasoning": {
1322
+ if (reasoningMode === "off") return null;
1323
+ // Private session: the same "Reasoning" row, but not a
1324
+ // disclosure. The server sends no reasoning text, so there
1325
+ // is no body under it and the parts inside do not render.
1326
+ if (privateMode) {
1327
+ return (
1328
+ <PrivateWorkStep
1329
+ kind="reasoning"
1330
+ statusType={part.status.type === "running" ? "running" : "complete"}
1331
+ />
1332
+ );
1333
+ }
1334
+ if (ReasoningGroup) {
1335
+ return (
1336
+ <ReasoningGroup group={part}>{children}</ReasoningGroup>
1337
+ );
1338
+ }
1339
+ const running = part.status.type === "running";
1340
+ const box = reasoningGroupDisclosure(
1341
+ disclosure,
1342
+ reasoningMode,
1343
+ running || turnRunning,
1344
+ );
1345
+ const locked = box.locked;
1346
+ return (
1347
+ <ReasoningRoot
1348
+ variant="ghost"
1349
+ streaming={box.streamingOpen}
1350
+ defaultOpen={box.defaultOpen}
1351
+ {...(locked ? { open: true, onOpenChange: () => {} } : {})}
1352
+ >
1353
+ {/* Upstream's structure: the reasoning is a sub-step with
1354
+ its own disclosure inside the chain. */}
1355
+ <ReasoningTrigger active={running} disabled={locked} />
1356
+ <ReasoningContent aria-busy={running}>
1357
+ <ReasoningText>{children}</ReasoningText>
1358
+ </ReasoningContent>
1359
+ </ReasoningRoot>
1360
+ );
1361
+ }
1362
+ case "text":
1363
+ return <MarkdownText />;
1364
+ case "reasoning": {
1365
+ if (reasoningMode === "off") return null;
1366
+ // Private session: the group row stands for it; no body.
1367
+ if (privateMode) return null;
1368
+ return <Reasoning {...part} />;
1369
+ }
1370
+ case "tool-call":
1371
+ if (toolCallsMode === "off") return null;
1372
+ // Private session: the normal row's look, but it never
1373
+ // opens, and a tool's own renderer is skipped — it would
1374
+ // render the (stripped) arguments and result.
1375
+ if (privateMode) {
1376
+ return (
1377
+ <PrivateToolCallStep
1378
+ toolName={part.toolName}
1379
+ status={part.status}
1380
+ isError={part.isError}
1381
+ result={part.result}
1382
+ />
1383
+ );
1384
+ }
1385
+ return part.toolUI ?? <ToolFallbackComponent {...part} />;
1386
+ case "data":
1387
+ // DIG-4459: a backend notice (data-notice, or a data-status
1388
+ // with a message) shows as a boxed status/error line.
1389
+ return part.dataRendererUI ?? <BackendNotice part={part} />;
1390
+ case "file":
1391
+ return (
1392
+ <div data-slot="aui_assistant-message-file" className="py-1">
1393
+ <File {...part} />
1394
+ </div>
1395
+ );
1396
+ case "image":
1397
+ return (
1398
+ <div data-slot="aui_assistant-message-image" className="py-1">
1399
+ <Image {...part} />
1400
+ </div>
1401
+ );
1402
+ case "indicator":
1403
+ // One loading affordance per turn. Before the agent answers,
1404
+ // the row stands alone (with the phase text when we know it:
1405
+ // waking, connecting, warming up). The moment anything real
1406
+ // arrives — reasoning, a tool call, the answer — the work
1407
+ // block takes over and this row is gone, so the two never
1408
+ // stack.
1409
+ return hasContent ? null : <AssistantWorkingIndicator />;
1410
+ case "source":
1411
+ // Citations are inline markdown links in the answer (DIG-1210);
1412
+ // rendering source parts here stacked duplicate rows between
1413
+ // tool groups on every retrieve span.
1414
+ return null;
1415
+ default:
1416
+ return null;
1417
+ }
1418
+ }}
1419
+ </MessagePrimitive.GroupedParts>
1420
+ <MessageError />
1421
+ </div>
1422
+
1423
+ <div
1424
+ data-slot="aui_assistant-message-footer"
1425
+ className={cn("col-start-1 flex items-center", ACTION_BAR_HEIGHT)}
1426
+ >
1427
+ <AuiIf
1428
+ condition={(s) =>
1429
+ !(
1430
+ s.message.status?.type === "incomplete" &&
1431
+ s.message.status.reason === "error"
1432
+ )
1433
+ }
1434
+ >
1435
+ <BranchPicker />
1436
+ <AssistantActionBar />
1437
+ </AuiIf>
1438
+ </div>
1439
+ </MessagePrimitive.Root>
1440
+ );
1441
+ };
1442
+
1443
+ const AssistantActionBar: FC = () => {
1444
+ const { undo, feedback } = useContext(ThreadActionsContext);
1445
+ const privateMode = useContext(ThreadPrivateModeContext);
1446
+ return (
1447
+ <ActionBarPrimitive.Root
1448
+ hideWhenRunning
1449
+ autohide="not-last"
1450
+ // Earlier turns: the bar is unmounted until the turn is hovered, then it
1451
+ // takes its footer row back in flow. It cannot float in the message gap —
1452
+ // the bar is 24px tall and the gap is 12/8/20px by density, so a float
1453
+ // paints over the next turn's first line. The empty-footer collapse in
1454
+ // chat-digichat.css keeps the un-hovered turns at one message gap.
1455
+ className="aui-assistant-action-bar-root text-ink-mute animate-in fade-in col-start-3 row-start-2 -ms-1 flex gap-1 duration-(--duration-hover)"
1456
+ >
1457
+ <ActionBarPrimitive.Copy asChild>
1458
+ <TooltipIconButton tooltip="Copy">
1459
+ <AuiIf condition={(s) => s.message.isCopied}>
1460
+ <CheckActionIcon className="size-3.5" />
1461
+ </AuiIf>
1462
+ <AuiIf condition={(s) => !s.message.isCopied}>
1463
+ <CopyActionIcon className="size-3.5" />
1464
+ </AuiIf>
1465
+ </TooltipIconButton>
1466
+ </ActionBarPrimitive.Copy>
1467
+ {undo ? (
1468
+ <AuiIf
1469
+ condition={(s) =>
1470
+ (s.message.branchCount ?? 1) > 1 && (s.message.branchNumber ?? 1) > 1
1471
+ }
1472
+ >
1473
+ <BranchPickerPrimitive.Previous asChild>
1474
+ <TooltipIconButton tooltip="Undo">
1475
+ <DotMatrix state="prev" decorative className="size-3.5" />
1476
+ </TooltipIconButton>
1477
+ </BranchPickerPrimitive.Previous>
1478
+ </AuiIf>
1479
+ ) : null}
1480
+ <ActionBarPrimitive.Reload asChild>
1481
+ <TooltipIconButton tooltip="Redo">
1482
+ <DotMatrix state="refresh" decorative className="size-3.5" />
1483
+ </TooltipIconButton>
1484
+ </ActionBarPrimitive.Reload>
1485
+ {feedback ? (
1486
+ <>
1487
+ <ActionBarPrimitive.FeedbackPositive asChild>
1488
+ <TooltipIconButton tooltip="Good response">
1489
+ <DotMatrix state="thumbsUp" decorative className="size-3.5" />
1490
+ </TooltipIconButton>
1491
+ </ActionBarPrimitive.FeedbackPositive>
1492
+ <ActionBarPrimitive.FeedbackNegative asChild>
1493
+ <TooltipIconButton tooltip="Bad response">
1494
+ <DotMatrix state="thumbsDown" decorative className="size-3.5" />
1495
+ </TooltipIconButton>
1496
+ </ActionBarPrimitive.FeedbackNegative>
1497
+ </>
1498
+ ) : null}
1499
+ {/* Private session: the stored messages still hold the real typed text,
1500
+ which the mode exists to keep out of a file someone else may see. */}
1501
+ {privateMode ? null : (
1502
+ <MessageExportMenu
1503
+ trigger={
1504
+ <TooltipIconButton tooltip="More">
1505
+ <DotMatrix state="more" decorative className="size-3.5" />
1506
+ </TooltipIconButton>
1507
+ }
1508
+ icon={<DotMatrix state="export" decorative className="size-3.5" />}
1509
+ contentClassName="aui-action-bar-more-content z-[110] min-w-[8rem] overflow-hidden border p-1"
1510
+ itemClassName="aui-action-bar-more-item flex cursor-pointer items-center gap-2 bg-transparent px-2 py-1.5 outline-none select-none hover:bg-transparent focus:bg-transparent data-highlighted:bg-transparent data-highlighted:text-ink"
1511
+ />
1512
+ )}
1513
+ <MessageTiming />
1514
+ </ActionBarPrimitive.Root>
1515
+ );
1516
+ };
1517
+
1518
+ const UserFilePart: FileMessagePartComponent = (part) => {
1519
+ const hiddenNames = useHiddenAttachmentNames();
1520
+ if (part.filename && hiddenNames.includes(part.filename)) return null;
1521
+ return (
1522
+ <div data-slot="aui_user-message-file" className="py-1">
1523
+ <File {...part} />
1524
+ </div>
1525
+ );
1526
+ };
1527
+
1528
+ const UserImagePart: ImageMessagePartComponent = (part) => (
1529
+ <div data-slot="aui_user-message-image" className="py-1">
1530
+ <Image {...part} />
1531
+ </div>
1532
+ );
1533
+
1534
+ const UserMessage: FC = () => {
1535
+ return (
1536
+ <MessagePrimitive.Root
1537
+ data-slot="aui_user-message-root"
1538
+ className="aui-msg fade-in slide-in-from-bottom-1 animate-in grid grid-cols-[1.25rem_minmax(0,1fr)_1.75rem] items-start gap-x-[0.55rem] gap-y-2 duration-(--dur-fast)"
1539
+ data-role="user"
1540
+ >
1541
+ <span className="aui-msg-marker" aria-hidden="true">
1542
+ <DotMatrix state="user" label="User" className="size-3.5" />
1543
+ </span>
1544
+ <div className="aui-user-message-content-wrapper relative min-w-0">
1545
+ <UserMessageAttachments />
1546
+ <div className="aui-user-message-content text-ink wrap-break-word empty:hidden">
1547
+ <MessagePrimitive.Parts
1548
+ components={{ File: UserFilePart, Image: UserImagePart }}
1549
+ />
1550
+ </div>
1551
+ </div>
1552
+ <div className="aui-user-action-bar-slot col-start-3 row-start-1 self-start justify-self-end">
1553
+ <UserActionBar />
1554
+ </div>
1555
+
1556
+ <BranchPicker
1557
+ data-slot="aui_user-branch-picker"
1558
+ className="col-start-2 -me-1 justify-start"
1559
+ />
1560
+ </MessagePrimitive.Root>
1561
+ );
1562
+ };
1563
+
1564
+ const UserActionBar: FC = () => {
1565
+ const aui = useAui();
1566
+ const edit = useContext(ComposerEditContext);
1567
+ const onEdit = () => {
1568
+ const msg = aui.message().getState();
1569
+ const text = msg.parts
1570
+ .map((p) => (p.type === "text" ? p.text : ""))
1571
+ .join("");
1572
+ edit.begin(msg.id, text);
1573
+ };
1574
+ return (
1575
+ <ActionBarPrimitive.Root
1576
+ hideWhenRunning
1577
+ autohide="not-last"
1578
+ className="aui-user-action-bar-root flex flex-row items-center"
1579
+ >
1580
+ <TooltipIconButton
1581
+ tooltip="Edit"
1582
+ className="aui-user-action-edit"
1583
+ onClick={onEdit}
1584
+ >
1585
+ <DotMatrix state="edit" decorative className="size-3.5" />
1586
+ </TooltipIconButton>
1587
+ </ActionBarPrimitive.Root>
1588
+ );
1589
+ };
1590
+
1591
+ const BranchPicker: FC<BranchPickerPrimitive.Root.Props> = ({
1592
+ className,
1593
+ ...rest
1594
+ }) => {
1595
+ return (
1596
+ <BranchPickerPrimitive.Root
1597
+ hideWhenSingleBranch
1598
+ className={cn(
1599
+ "aui-branch-picker-root text-ink-mute -ms-2 me-2 inline-flex items-center text-xs",
1600
+ className,
1601
+ )}
1602
+ {...rest}
1603
+ >
1604
+ <BranchPickerPrimitive.Previous asChild>
1605
+ <TooltipIconButton tooltip="Previous">
1606
+ <DotMatrix state="prev" decorative className="size-3.5" />
1607
+ </TooltipIconButton>
1608
+ </BranchPickerPrimitive.Previous>
1609
+ <span className="aui-branch-picker-state font-medium">
1610
+ <BranchPickerPrimitive.Number /> / <BranchPickerPrimitive.Count />
1611
+ </span>
1612
+ <BranchPickerPrimitive.Next asChild>
1613
+ <TooltipIconButton tooltip="Next">
1614
+ <DotMatrix state="next" decorative className="size-3.5" />
1615
+ </TooltipIconButton>
1616
+ </BranchPickerPrimitive.Next>
1617
+ </BranchPickerPrimitive.Root>
1618
+ );
1619
+ };