@cueplusplus/ui 0.3.0 → 0.5.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 (488) hide show
  1. package/CHANGELOG.md +2229 -0
  2. package/dist/agent-runtime/_glyphs.js +43 -0
  3. package/dist/agent-runtime/branch-picker.d.ts +45 -0
  4. package/dist/agent-runtime/branch-picker.js +87 -0
  5. package/dist/agent-runtime/index.d.ts +4 -0
  6. package/dist/agent-runtime/index.js +4 -0
  7. package/dist/agent-runtime/tool-call-card.d.ts +69 -0
  8. package/dist/agent-runtime/tool-call-card.js +187 -0
  9. package/dist/agent-runtime/use-agent-thread.d.ts +164 -0
  10. package/dist/agent-runtime/use-agent-thread.js +251 -0
  11. package/dist/chat/index.d.ts +2 -2
  12. package/dist/chat/message-list.d.ts +6 -0
  13. package/dist/chat/message-list.js +35 -0
  14. package/dist/chrome/app-shell.d.ts +52 -2
  15. package/dist/chrome/app-shell.js +80 -8
  16. package/dist/chrome/index.d.ts +2 -2
  17. package/dist/configurator/_export.js +3 -2
  18. package/dist/configurator/_overrides.d.ts +1 -1
  19. package/dist/configurator/token-editor.js +4 -1
  20. package/dist/date/_segments.js +37 -2
  21. package/dist/elements/activity-graph.d.ts +40 -0
  22. package/dist/elements/activity-graph.js +81 -0
  23. package/dist/elements/agent-card.d.ts +49 -0
  24. package/dist/elements/agent-card.js +91 -0
  25. package/dist/elements/agent-handoff.d.ts +24 -0
  26. package/dist/elements/agent-handoff.js +53 -0
  27. package/dist/elements/agent-mode-badge.d.ts +72 -0
  28. package/dist/elements/agent-mode-badge.js +110 -0
  29. package/dist/elements/agent-plan.d.ts +24 -0
  30. package/dist/elements/agent-plan.js +74 -0
  31. package/dist/elements/agent-status.d.ts +29 -0
  32. package/dist/elements/agent-status.js +46 -0
  33. package/dist/elements/agent-surface.d.ts +65 -0
  34. package/dist/elements/agent-surface.js +68 -0
  35. package/dist/elements/animated-number.d.ts +86 -0
  36. package/dist/elements/animated-number.js +118 -0
  37. package/dist/elements/approval-card.d.ts +40 -0
  38. package/dist/elements/approval-card.js +78 -0
  39. package/dist/elements/artifact-card.d.ts +30 -0
  40. package/dist/elements/artifact-card.js +58 -0
  41. package/dist/elements/background-inbox.d.ts +39 -0
  42. package/dist/elements/background-inbox.js +60 -0
  43. package/dist/elements/canvas-split.d.ts +77 -0
  44. package/dist/elements/canvas-split.js +138 -0
  45. package/dist/elements/chart.d.ts +41 -0
  46. package/dist/elements/chart.js +117 -0
  47. package/dist/elements/chat-panel.d.ts +46 -0
  48. package/dist/elements/chat-panel.js +104 -0
  49. package/dist/elements/checkpoint-history.d.ts +31 -0
  50. package/dist/elements/checkpoint-history.js +65 -0
  51. package/dist/elements/clamp.d.ts +90 -0
  52. package/dist/elements/clamp.js +109 -0
  53. package/dist/elements/code-diff.d.ts +42 -0
  54. package/dist/elements/code-diff.js +69 -0
  55. package/dist/elements/code-runner.d.ts +38 -0
  56. package/dist/elements/code-runner.js +84 -0
  57. package/dist/elements/command-palette.d.ts +48 -0
  58. package/dist/elements/command-palette.js +129 -0
  59. package/dist/elements/compaction-row.d.ts +63 -0
  60. package/dist/elements/compaction-row.js +110 -0
  61. package/dist/elements/comparison-card.d.ts +47 -0
  62. package/dist/elements/comparison-card.js +69 -0
  63. package/dist/elements/composer.d.ts +274 -0
  64. package/dist/elements/composer.js +505 -0
  65. package/dist/elements/computer-use.d.ts +40 -0
  66. package/dist/elements/computer-use.js +92 -0
  67. package/dist/elements/confidence-marker.d.ts +39 -0
  68. package/dist/elements/confidence-marker.js +64 -0
  69. package/dist/elements/connection-state.d.ts +29 -0
  70. package/dist/elements/connection-state.js +66 -0
  71. package/dist/elements/context-breakdown.d.ts +30 -0
  72. package/dist/elements/context-breakdown.js +86 -0
  73. package/dist/elements/context-usage.d.ts +99 -0
  74. package/dist/elements/context-usage.js +225 -0
  75. package/dist/elements/conversation-search.d.ts +48 -0
  76. package/dist/elements/conversation-search.js +88 -0
  77. package/dist/elements/cost-meter.d.ts +33 -0
  78. package/dist/elements/cost-meter.js +75 -0
  79. package/dist/elements/data-table.d.ts +31 -0
  80. package/dist/elements/data-table.js +67 -0
  81. package/dist/elements/day-separator.d.ts +34 -0
  82. package/dist/elements/day-separator.js +50 -0
  83. package/dist/elements/diagram.d.ts +34 -0
  84. package/dist/elements/diagram.js +75 -0
  85. package/dist/elements/document-reference.d.ts +37 -0
  86. package/dist/elements/document-reference.js +66 -0
  87. package/dist/elements/draft-restore.d.ts +27 -0
  88. package/dist/elements/draft-restore.js +54 -0
  89. package/dist/elements/edit-message.d.ts +36 -0
  90. package/dist/elements/edit-message.js +77 -0
  91. package/dist/elements/elements.css +459 -0
  92. package/dist/elements/elicitation-form.d.ts +48 -0
  93. package/dist/elements/elicitation-form.js +106 -0
  94. package/dist/elements/empty-state.d.ts +52 -0
  95. package/dist/elements/empty-state.js +98 -0
  96. package/dist/elements/end-of-turn-summary.d.ts +60 -0
  97. package/dist/elements/end-of-turn-summary.js +75 -0
  98. package/dist/elements/error-state.d.ts +29 -0
  99. package/dist/elements/error-state.js +55 -0
  100. package/dist/elements/feedback-dialog.d.ts +36 -0
  101. package/dist/elements/feedback-dialog.js +81 -0
  102. package/dist/elements/file-tree.d.ts +45 -0
  103. package/dist/elements/file-tree.js +84 -0
  104. package/dist/elements/flow-graph.d.ts +45 -0
  105. package/dist/elements/flow-graph.js +78 -0
  106. package/dist/elements/generative/chart.js +200 -0
  107. package/dist/elements/generative/faults.js +111 -0
  108. package/dist/elements/generative/icons.d.ts +41 -0
  109. package/dist/elements/generative/icons.js +190 -0
  110. package/dist/elements/generative/intrinsics.js +483 -0
  111. package/dist/elements/generative/spec.d.ts +110 -0
  112. package/dist/elements/generative/spec.js +366 -0
  113. package/dist/elements/generative.css +1145 -0
  114. package/dist/elements/generative.d.ts +144 -0
  115. package/dist/elements/generative.js +126 -0
  116. package/dist/elements/guardrail-notice.d.ts +29 -0
  117. package/dist/elements/guardrail-notice.js +63 -0
  118. package/dist/elements/image-generation.d.ts +18 -0
  119. package/dist/elements/image-generation.js +64 -0
  120. package/dist/elements/index.d.ts +116 -0
  121. package/dist/elements/index.js +115 -0
  122. package/dist/elements/inline-citation.d.ts +43 -0
  123. package/dist/elements/inline-citation.js +93 -0
  124. package/dist/elements/job-progress.d.ts +49 -0
  125. package/dist/elements/job-progress.js +74 -0
  126. package/dist/elements/launcher-bubble.d.ts +34 -0
  127. package/dist/elements/launcher-bubble.js +72 -0
  128. package/dist/elements/live-region-announcer.d.ts +130 -0
  129. package/dist/elements/live-region-announcer.js +230 -0
  130. package/dist/elements/loading-state.d.ts +27 -0
  131. package/dist/elements/loading-state.js +36 -0
  132. package/dist/elements/map-answer.d.ts +38 -0
  133. package/dist/elements/map-answer.js +93 -0
  134. package/dist/elements/markdown.d.ts +131 -0
  135. package/dist/elements/markdown.js +528 -0
  136. package/dist/elements/math-block.d.ts +51 -0
  137. package/dist/elements/math-block.js +70 -0
  138. package/dist/elements/mcp-server-panel.d.ts +41 -0
  139. package/dist/elements/mcp-server-panel.js +115 -0
  140. package/dist/elements/memory-chips.d.ts +30 -0
  141. package/dist/elements/memory-chips.js +43 -0
  142. package/dist/elements/message-actions.d.ts +38 -0
  143. package/dist/elements/message-actions.js +71 -0
  144. package/dist/elements/message-attachment.d.ts +48 -0
  145. package/dist/elements/message-attachment.js +63 -0
  146. package/dist/elements/message-branches.d.ts +25 -0
  147. package/dist/elements/message-branches.js +65 -0
  148. package/dist/elements/message-pair.d.ts +33 -0
  149. package/dist/elements/message-pair.js +65 -0
  150. package/dist/elements/message-queue.d.ts +31 -0
  151. package/dist/elements/message-queue.js +80 -0
  152. package/dist/elements/message-timing.d.ts +33 -0
  153. package/dist/elements/message-timing.js +37 -0
  154. package/dist/elements/mobile-composer.d.ts +44 -0
  155. package/dist/elements/mobile-composer.js +88 -0
  156. package/dist/elements/model-picker.d.ts +38 -0
  157. package/dist/elements/model-picker.js +69 -0
  158. package/dist/elements/number-ticker.d.ts +18 -0
  159. package/dist/elements/number-ticker.js +47 -0
  160. package/dist/elements/onboarding.d.ts +30 -0
  161. package/dist/elements/onboarding.js +75 -0
  162. package/dist/elements/permission-grant.d.ts +35 -0
  163. package/dist/elements/permission-grant.js +86 -0
  164. package/dist/elements/permission-scopes.d.ts +215 -0
  165. package/dist/elements/permission-scopes.js +249 -0
  166. package/dist/elements/prompt-library.d.ts +43 -0
  167. package/dist/elements/prompt-library.js +117 -0
  168. package/dist/elements/queue-dock.d.ts +101 -0
  169. package/dist/elements/queue-dock.js +132 -0
  170. package/dist/elements/quota-banner.d.ts +26 -0
  171. package/dist/elements/quota-banner.js +66 -0
  172. package/dist/elements/quote-reply.d.ts +44 -0
  173. package/dist/elements/quote-reply.js +79 -0
  174. package/dist/elements/range.d.ts +29 -0
  175. package/dist/elements/range.js +45 -0
  176. package/dist/elements/read-aloud.d.ts +45 -0
  177. package/dist/elements/read-aloud.js +75 -0
  178. package/dist/elements/reasoning-effort.d.ts +36 -0
  179. package/dist/elements/reasoning-effort.js +67 -0
  180. package/dist/elements/reasoning-panel.d.ts +46 -0
  181. package/dist/elements/reasoning-panel.js +75 -0
  182. package/dist/elements/recommendation-card.d.ts +45 -0
  183. package/dist/elements/recommendation-card.js +83 -0
  184. package/dist/elements/regenerate-menu.d.ts +45 -0
  185. package/dist/elements/regenerate-menu.js +51 -0
  186. package/dist/elements/replay.d.ts +217 -0
  187. package/dist/elements/replay.js +498 -0
  188. package/dist/elements/research-report.d.ts +38 -0
  189. package/dist/elements/research-report.js +69 -0
  190. package/dist/elements/retrieval-chunks.d.ts +43 -0
  191. package/dist/elements/retrieval-chunks.js +81 -0
  192. package/dist/elements/revert-dock.d.ts +159 -0
  193. package/dist/elements/revert-dock.js +327 -0
  194. package/dist/elements/reviewable-diff.d.ts +45 -0
  195. package/dist/elements/reviewable-diff.js +110 -0
  196. package/dist/elements/risk-badge.d.ts +52 -0
  197. package/dist/elements/risk-badge.js +91 -0
  198. package/dist/elements/schedule-card.d.ts +42 -0
  199. package/dist/elements/schedule-card.js +90 -0
  200. package/dist/elements/score-breakdown.d.ts +47 -0
  201. package/dist/elements/score-breakdown.js +82 -0
  202. package/dist/elements/scroll-anchor.d.ts +32 -0
  203. package/dist/elements/scroll-anchor.js +121 -0
  204. package/dist/elements/settings-panel.d.ts +52 -0
  205. package/dist/elements/settings-panel.js +107 -0
  206. package/dist/elements/shared-conversation.d.ts +39 -0
  207. package/dist/elements/shared-conversation.js +68 -0
  208. package/dist/elements/sources.d.ts +33 -0
  209. package/dist/elements/sources.js +61 -0
  210. package/dist/elements/speaker-identity.d.ts +32 -0
  211. package/dist/elements/speaker-identity.js +51 -0
  212. package/dist/elements/spec-sheet.d.ts +31 -0
  213. package/dist/elements/spec-sheet.js +45 -0
  214. package/dist/elements/stopped-run.d.ts +22 -0
  215. package/dist/elements/stopped-run.js +50 -0
  216. package/dist/elements/streaming-text.d.ts +31 -0
  217. package/dist/elements/streaming-text.js +45 -0
  218. package/dist/elements/subagent-list.d.ts +37 -0
  219. package/dist/elements/subagent-list.js +79 -0
  220. package/dist/elements/suggestions.d.ts +30 -0
  221. package/dist/elements/suggestions.js +36 -0
  222. package/dist/elements/surfaces.d.ts +85 -0
  223. package/dist/elements/surfaces.js +107 -0
  224. package/dist/elements/tail-status.d.ts +131 -0
  225. package/dist/elements/tail-status.js +94 -0
  226. package/dist/elements/terminal-block.d.ts +29 -0
  227. package/dist/elements/terminal-block.js +54 -0
  228. package/dist/elements/thinking-indicator.d.ts +18 -0
  229. package/dist/elements/thinking-indicator.js +36 -0
  230. package/dist/elements/thread-list.d.ts +35 -0
  231. package/dist/elements/thread-list.js +68 -0
  232. package/dist/elements/thread-search.d.ts +42 -0
  233. package/dist/elements/thread-search.js +99 -0
  234. package/dist/elements/timeline.d.ts +37 -0
  235. package/dist/elements/timeline.js +46 -0
  236. package/dist/elements/todo-list.d.ts +42 -0
  237. package/dist/elements/todo-list.js +59 -0
  238. package/dist/elements/tool-call.d.ts +41 -0
  239. package/dist/elements/tool-call.js +85 -0
  240. package/dist/elements/tool-error.d.ts +39 -0
  241. package/dist/elements/tool-error.js +76 -0
  242. package/dist/elements/tool-group.d.ts +45 -0
  243. package/dist/elements/tool-group.js +71 -0
  244. package/dist/elements/tool-timeline.d.ts +65 -0
  245. package/dist/elements/tool-timeline.js +88 -0
  246. package/dist/elements/trace-waterfall.d.ts +37 -0
  247. package/dist/elements/trace-waterfall.js +69 -0
  248. package/dist/elements/transcript-rules.d.ts +67 -0
  249. package/dist/elements/transcript-rules.js +95 -0
  250. package/dist/elements/turn-footer.d.ts +74 -0
  251. package/dist/elements/turn-footer.js +101 -0
  252. package/dist/elements/typing-indicator.d.ts +16 -0
  253. package/dist/elements/typing-indicator.js +51 -0
  254. package/dist/elements/verdict-row.d.ts +89 -0
  255. package/dist/elements/verdict-row.js +150 -0
  256. package/dist/elements/vocabulary.d.ts +612 -0
  257. package/dist/elements/vocabulary.js +611 -0
  258. package/dist/elements/voice-conversation.d.ts +49 -0
  259. package/dist/elements/voice-conversation.js +123 -0
  260. package/dist/elements/web-preview.d.ts +32 -0
  261. package/dist/elements/web-preview.js +66 -0
  262. package/dist/elements/web-search.d.ts +37 -0
  263. package/dist/elements/web-search.js +66 -0
  264. package/dist/elements/work-collapse.d.ts +103 -0
  265. package/dist/elements/work-collapse.js +131 -0
  266. package/dist/icons/containers.d.ts +89 -0
  267. package/dist/icons/containers.js +295 -0
  268. package/dist/icons/index.d.ts +2 -0
  269. package/dist/index.d.ts +6 -4
  270. package/dist/index.js +2 -1
  271. package/dist/instruments/two-step-button.js +25 -6
  272. package/dist/layout/sidebar.d.ts +19 -1
  273. package/dist/layout/sidebar.js +19 -1
  274. package/dist/primitives/button.js +11 -1
  275. package/dist/primitives/chip.d.ts +35 -9
  276. package/dist/primitives/chip.js +97 -11
  277. package/dist/system/agent-skin.d.ts +27 -0
  278. package/dist/system/agent-skin.js +20 -0
  279. package/dist/system/portal.d.ts +5 -3
  280. package/dist/system/portal.js +19 -5
  281. package/dist/system/theme-provider.js +54 -9
  282. package/dist/system/use-density.d.ts +9 -0
  283. package/dist/system/use-density.js +9 -0
  284. package/dist/system/use-isomorphic-layout-effect.js +19 -1
  285. package/dist/theming/_presets.d.ts +1 -1
  286. package/dist/theming/_presets.js +71 -8
  287. package/dist/theming/contrast.d.ts +16 -4
  288. package/dist/theming/contrast.js +33 -6
  289. package/dist/theming/create-theme.d.ts +6 -2
  290. package/dist/theming/create-theme.js +32 -5
  291. package/dist/theming/serialize.d.ts +10 -1
  292. package/dist/theming/serialize.js +62 -8
  293. package/manifest/components/activity-graph.json +88 -0
  294. package/manifest/components/agent-card.json +120 -0
  295. package/manifest/components/agent-handoff.json +87 -0
  296. package/manifest/components/agent-mode-badge.json +84 -0
  297. package/manifest/components/agent-plan.json +67 -0
  298. package/manifest/components/agent-status.json +73 -0
  299. package/manifest/components/agent-surface.json +78 -0
  300. package/manifest/components/alert-dialog.json +1 -1
  301. package/manifest/components/animated-number.json +67 -0
  302. package/manifest/components/app-shell.json +99 -7
  303. package/manifest/components/approval-card.json +106 -0
  304. package/manifest/components/artifact-card.json +81 -0
  305. package/manifest/components/audience-icon.json +52 -0
  306. package/manifest/components/background-inbox.json +71 -0
  307. package/manifest/components/branch-picker.json +124 -0
  308. package/manifest/components/button.json +1 -0
  309. package/manifest/components/canvas-split-body.json +62 -0
  310. package/manifest/components/canvas-split-document.json +54 -0
  311. package/manifest/components/canvas-split-header.json +83 -0
  312. package/manifest/components/canvas-split-line.json +62 -0
  313. package/manifest/components/canvas-split-message.json +62 -0
  314. package/manifest/components/canvas-split-thread.json +54 -0
  315. package/manifest/components/canvas-split.json +60 -0
  316. package/manifest/components/catalogue-icon.json +52 -0
  317. package/manifest/components/channel-beta-icon.json +52 -0
  318. package/manifest/components/channel-released-icon.json +52 -0
  319. package/manifest/components/chart.json +93 -0
  320. package/manifest/components/chat-empty-state.json +55 -0
  321. package/manifest/components/chat-panel-assistant-message.json +51 -0
  322. package/manifest/components/chat-panel-composer.json +66 -0
  323. package/manifest/components/chat-panel-messages.json +51 -0
  324. package/manifest/components/chat-panel-typing.json +51 -0
  325. package/manifest/components/chat-panel-user-message.json +51 -0
  326. package/manifest/components/chat-panel.json +57 -0
  327. package/manifest/components/checkpoint-history.json +80 -0
  328. package/manifest/components/chip.json +24 -12
  329. package/manifest/components/clamp.json +102 -0
  330. package/manifest/components/cli-tool-icon.json +52 -0
  331. package/manifest/components/code-diff.json +87 -0
  332. package/manifest/components/code-runner.json +97 -0
  333. package/manifest/components/compaction-row.json +100 -0
  334. package/manifest/components/comparison-card.json +83 -0
  335. package/manifest/components/composer-actions.json +65 -0
  336. package/manifest/components/composer-attach-button.json +65 -0
  337. package/manifest/components/composer-attachment-chip.json +75 -0
  338. package/manifest/components/composer-attachments.json +60 -0
  339. package/manifest/components/composer-bar.json +73 -0
  340. package/manifest/components/composer-command-item.json +79 -0
  341. package/manifest/components/composer-context.json +67 -0
  342. package/manifest/components/composer-input.json +73 -0
  343. package/manifest/components/composer-menu-item.json +72 -0
  344. package/manifest/components/composer-menu.json +79 -0
  345. package/manifest/components/composer-model-item.json +75 -0
  346. package/manifest/components/composer-model-trigger.json +75 -0
  347. package/manifest/components/composer-person-item.json +79 -0
  348. package/manifest/components/composer-send.json +80 -0
  349. package/manifest/components/composer-toolbar.json +65 -0
  350. package/manifest/components/composer-voice-button.json +68 -0
  351. package/manifest/components/composer-voice.json +75 -0
  352. package/manifest/components/computer-use.json +84 -0
  353. package/manifest/components/confidence-marker.json +74 -0
  354. package/manifest/components/connection-state.json +84 -0
  355. package/manifest/components/context-breakdown.json +65 -0
  356. package/manifest/components/context-usage.json +85 -0
  357. package/manifest/components/conversation-search.json +92 -0
  358. package/manifest/components/cost-meter.json +74 -0
  359. package/manifest/components/cue-portal-frame.json +1 -1
  360. package/manifest/components/day-separator.json +58 -0
  361. package/manifest/components/delegation-card.json +1 -1
  362. package/manifest/components/density.json +1 -1
  363. package/manifest/components/diagram.json +100 -0
  364. package/manifest/components/document-reference.json +90 -0
  365. package/manifest/components/draft-restore.json +82 -0
  366. package/manifest/components/drawer.json +1 -1
  367. package/manifest/components/dropdown-menu.json +1 -1
  368. package/manifest/components/edit-message.json +105 -0
  369. package/manifest/components/elements-command-palette.json +100 -0
  370. package/manifest/components/elements-composer.json +71 -0
  371. package/manifest/components/elements-data-table.json +68 -0
  372. package/manifest/components/elements-timeline.json +69 -0
  373. package/manifest/components/elicitation-form.json +103 -0
  374. package/manifest/components/empty-state-composer.json +64 -0
  375. package/manifest/components/empty-state-greeting.json +49 -0
  376. package/manifest/components/empty-state-suggestion.json +57 -0
  377. package/manifest/components/empty-state-suggestions.json +49 -0
  378. package/manifest/components/end-of-turn-summary.json +77 -0
  379. package/manifest/components/error-state.json +80 -0
  380. package/manifest/components/feedback-dialog.json +107 -0
  381. package/manifest/components/file-tree.json +83 -0
  382. package/manifest/components/flow-graph.json +75 -0
  383. package/manifest/components/folder-icon.json +52 -0
  384. package/manifest/components/frac.json +59 -0
  385. package/manifest/components/generation-loader.json +64 -0
  386. package/manifest/components/generative-ui.json +93 -0
  387. package/manifest/components/guardrail-notice.json +91 -0
  388. package/manifest/components/hover-card.json +1 -1
  389. package/manifest/components/image-generation.json +67 -0
  390. package/manifest/components/info-tip.json +1 -1
  391. package/manifest/components/inline-citation.json +77 -0
  392. package/manifest/components/job-progress.json +96 -0
  393. package/manifest/components/launcher-bubble.json +105 -0
  394. package/manifest/components/live-region-announcer.json +79 -0
  395. package/manifest/components/map-answer.json +83 -0
  396. package/manifest/components/markdown-text.json +94 -0
  397. package/manifest/components/math-block.json +74 -0
  398. package/manifest/components/mcp-server-icon.json +52 -0
  399. package/manifest/components/mcp-server-panel.json +87 -0
  400. package/manifest/components/memory-chips.json +65 -0
  401. package/manifest/components/message-actions.json +99 -0
  402. package/manifest/components/message-attachments.json +63 -0
  403. package/manifest/components/message-branches.json +71 -0
  404. package/manifest/components/message-list.json +1 -1
  405. package/manifest/components/message-pair.json +86 -0
  406. package/manifest/components/message-queue.json +74 -0
  407. package/manifest/components/message-timing.json +62 -0
  408. package/manifest/components/mobile-composer.json +126 -0
  409. package/manifest/components/model-picker.json +76 -0
  410. package/manifest/components/node-card.json +1 -1
  411. package/manifest/components/node-handle.json +1 -1
  412. package/manifest/components/number-ticker.json +61 -0
  413. package/manifest/components/onboarding.json +84 -0
  414. package/manifest/components/otp-field.json +7 -7
  415. package/manifest/components/permission-grant.json +92 -0
  416. package/manifest/components/permission-scopes.json +139 -0
  417. package/manifest/components/popover.json +1 -1
  418. package/manifest/components/prompt-library.json +99 -0
  419. package/manifest/components/queue-dock.json +110 -0
  420. package/manifest/components/quota-banner.json +94 -0
  421. package/manifest/components/quote-reply.json +105 -0
  422. package/manifest/components/read-aloud.json +111 -0
  423. package/manifest/components/reasoning-effort.json +81 -0
  424. package/manifest/components/reasoning-panel.json +108 -0
  425. package/manifest/components/recommendation-card.json +107 -0
  426. package/manifest/components/regenerate-menu.json +89 -0
  427. package/manifest/components/replay-player.json +102 -0
  428. package/manifest/components/research-report.json +76 -0
  429. package/manifest/components/retrieval-chunks.json +84 -0
  430. package/manifest/components/revert-dock.json +150 -0
  431. package/manifest/components/reviewable-diff.json +93 -0
  432. package/manifest/components/risk-badge.json +74 -0
  433. package/manifest/components/schedule-card.json +101 -0
  434. package/manifest/components/score-breakdown.json +91 -0
  435. package/manifest/components/scroll-anchor.json +72 -0
  436. package/manifest/components/scroll-area.json +7 -7
  437. package/manifest/components/search-input.json +7 -7
  438. package/manifest/components/segmented-control.json +21 -21
  439. package/manifest/components/settings-panel.json +118 -0
  440. package/manifest/components/shared-conversation.json +89 -0
  441. package/manifest/components/shimmer-label.json +59 -0
  442. package/manifest/components/skill-icon.json +52 -0
  443. package/manifest/components/sources.json +83 -0
  444. package/manifest/components/speaker-identity.json +61 -0
  445. package/manifest/components/spec-sheet.json +80 -0
  446. package/manifest/components/stack-icon.json +52 -0
  447. package/manifest/components/stacks-matrix-icon.json +52 -0
  448. package/manifest/components/stat.json +1 -1
  449. package/manifest/components/status-dot.json +1 -1
  450. package/manifest/components/stopped-run.json +81 -0
  451. package/manifest/components/streaming-text.json +71 -0
  452. package/manifest/components/sub.json +44 -0
  453. package/manifest/components/subagent-list.json +88 -0
  454. package/manifest/components/suggestions.json +84 -0
  455. package/manifest/components/sup.json +44 -0
  456. package/manifest/components/swap-label.json +71 -0
  457. package/manifest/components/tail-status.json +109 -0
  458. package/manifest/components/terminal-block.json +91 -0
  459. package/manifest/components/theme-provider.json +2 -2
  460. package/manifest/components/thinking-indicator.json +63 -0
  461. package/manifest/components/thread-list.json +76 -0
  462. package/manifest/components/thread-search.json +90 -0
  463. package/manifest/components/threshold-rail.json +2 -2
  464. package/manifest/components/time-boundary.json +62 -0
  465. package/manifest/components/timeline.json +1 -1
  466. package/manifest/components/todo-list.json +69 -0
  467. package/manifest/components/toggle-group.json +7 -7
  468. package/manifest/components/token-editor.json +1 -1
  469. package/manifest/components/tool-call-card.json +200 -0
  470. package/manifest/components/tool-call.json +119 -0
  471. package/manifest/components/tool-error.json +114 -0
  472. package/manifest/components/tool-group.json +83 -0
  473. package/manifest/components/tool-timeline.json +119 -0
  474. package/manifest/components/toolbar.json +7 -7
  475. package/manifest/components/tooltip.json +1 -1
  476. package/manifest/components/trace-waterfall.json +74 -0
  477. package/manifest/components/turn-footer.json +105 -0
  478. package/manifest/components/typing-indicator.json +53 -0
  479. package/manifest/components/unread-divider.json +55 -0
  480. package/manifest/components/usage-chart.json +1 -1
  481. package/manifest/components/verdict-row.json +91 -0
  482. package/manifest/components/voice-conversation.json +109 -0
  483. package/manifest/components/web-preview.json +87 -0
  484. package/manifest/components/web-search.json +92 -0
  485. package/manifest/components/work-collapse.json +102 -0
  486. package/manifest/manifest.json +2670 -33
  487. package/manifest/tokens.json +22 -1
  488. package/package.json +46 -4
package/CHANGELOG.md CHANGED
@@ -1,5 +1,2234 @@
1
1
  # @cueplusplus/ui
2
2
 
3
+ ## 0.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - f71a189: ## The hooks are documented API
8
+
9
+ `useAgentThread` had no page. Neither did `useTheme`, `useToast`, `useCueTable`
10
+ or the eight others — not because they are private, but because the generator
11
+ that writes this library's documentation only ever looked for components. A
12
+ symbol you are told to import, whose shape decides how you write the call, was
13
+ reaching you as a name in a changelog and a type in your editor and nothing
14
+ else. An agent reading the manifest could not answer "what does `useCueTable`
15
+ take" at all.
16
+
17
+ `manifest.json` now carries a `hooks` array beside `components`, with every
18
+ `use*` an entry subpath exports:
19
+
20
+ ```
21
+ useAgentThread @cueplusplus/ui/agent-runtime
22
+ useCarousel @cueplusplus/ui/layout/carousel
23
+ useControlHeight @cueplusplus/ui
24
+ useCuePortalProps @cueplusplus/ui
25
+ useCueTable @cueplusplus/ui/instruments/data-table
26
+ useDensity @cueplusplus/ui
27
+ useFlowTheme @cueplusplus/ui/flow
28
+ useSidebar @cueplusplus/ui
29
+ useTheme @cueplusplus/ui
30
+ useToast @cueplusplus/ui
31
+ useZoom @cueplusplus/ui/configurator
32
+ ```
33
+
34
+ Each entry carries what the source already knew and had nowhere to put: the
35
+ `signature` TypeScript resolves for it (`(size?: ControlSize): number`, not a
36
+ prop table with nothing in it), the description and summary, `@param`,
37
+ `@returns` and `@throws` where the JSDoc has them, every `@example` — all of
38
+ them, so `useFlowTheme`'s second one survives — plus `group`, `importPath`,
39
+ `peerDependencies`, `clientOnly` and the page it lives on.
40
+
41
+ The array is **inline**. A component's row points at a companion file because
42
+ its props, parts, variants and tokens do not fit; a hook's whole documentation
43
+ does, so `manifest.json` answers about a hook with no second fetch. That is why
44
+ a hook row has `url` and `mdUrl` but no `jsonUrl` — there is no companion to
45
+ name.
46
+
47
+ Every hook is `clientOnly: true`, and every one of them says so on its page. The
48
+ seven internal look-alikes — `useComposedRefs`, `useDebouncedValue`,
49
+ `useEdgeScroll` and the rest — are a component's own machinery, no entry exports
50
+ them, and none of them appears. A new one showing up in this array is a
51
+ decision, not a diff.
52
+
53
+ ## Two hooks that could not have carried a page
54
+
55
+ **`useDensity` now shows the read it exists for.** Its description always said
56
+ this is the JS read for code that cannot ask CSS — virtualised row heights,
57
+ canvas drawing, layout math — and then showed you none of it. It carries the
58
+ virtualiser idiom now: the level, the measured row height beside it, and the
59
+ overscan that has to move with the level because shorter rows mean more of them
60
+ on screen.
61
+
62
+ **`useSidebar` was one sentence long.** It said it reads the collapse state and
63
+ stopped, which left the two things a caller actually needs undocumented: that it
64
+ hands back `toggle` as well as `collapsed` — the same valve `Sidebar.Rail`
65
+ pulls, so a collapse control does not have to live on the rail — and what
66
+ happens outside a `Sidebar.Root`. It does not throw there. It reports
67
+ `collapsed: false` and a `toggle` that does nothing, because a row rendered on
68
+ its own in a test or a docs page is a legitimate thing and the honest answer for
69
+ it is that nothing is collapsed. Its example swaps a label for an icon and keeps
70
+ the `aria-label` across both.
71
+
72
+ Behaviour is unchanged in both. This is JSDoc.
73
+
74
+ ## Finding one
75
+
76
+ Hooks share the components namespace on the docs site — `/docs/components/use-agent-thread`,
77
+ and the markdown twin at `…/use-agent-thread.md` — so a link to a hook looks
78
+ like a link to anything else, the sidebar lists them under their group, and
79
+ `llms.txt` and `llms-full.txt` carry them. `useAgentThread`'s page mounts the
80
+ live scripted thread that already ran under **Agent runtime**, because the bench
81
+ now names the hook it was built to show.
82
+
83
+ The skills references list them per group, and the lookup script answers:
84
+
85
+ ```
86
+ node scripts/lookup.mjs --props useAgentThread # signature, params, returns, example
87
+ node scripts/lookup.mjs --hooks # all eleven, with their import paths
88
+ ```
89
+
90
+ ## What did not change
91
+
92
+ `schemaVersion` is still `1` — this is an addition, and additions never bump it.
93
+ `components` still means components: every count, badge, table and `--list` that
94
+ said "155 components" says it still, and none of them quietly grew by eleven. No
95
+ component's documentation moved, no runtime code changed, and nothing new is
96
+ required of a consumer who never reads the manifest.
97
+
98
+ - 8991fa3: ## Every state watchable
99
+
100
+ An agent UI is mostly states nobody can get to. A tool call that is _running_
101
+ holds that shape for four hundred milliseconds; a run _waiting on a person_
102
+ holds it until somebody answers; a _cancelled_ turn is a picture you have to
103
+ break something to see. This library has ninety-five components for those
104
+ states and, until now, two ways of showing them: a still, or a script that plays
105
+ once and cannot be stopped, rewound or linked to.
106
+
107
+ Now every state a run passes through is watchable — the run has a transport, the
108
+ transport has a scrubber, and the frame you stopped on has an address. `t=8` in
109
+ a gallery link opens the run paused on the approval, in your theme, at your
110
+ density, on the device you picked.
111
+
112
+ **`ReplayPlayer`** ships on its own subpath, `@cueplusplus/ui/elements/replay`,
113
+ with `useReplay` beside it for a console that wants the transport and not the
114
+ chrome.
115
+
116
+ ```tsx
117
+ import {
118
+ ReplayPlayer,
119
+ type ReplayScript,
120
+ } from "@cueplusplus/ui/elements/replay";
121
+
122
+ const SCRIPT: ReplayScript = {
123
+ title: "Patching universe 3",
124
+ frames: [
125
+ {
126
+ scene: (
127
+ <ToolCall
128
+ label="Read"
129
+ activeLabel="Reading the plot"
130
+ query="plot.md"
131
+ request='{"path":"plot.md"}'
132
+ result=""
133
+ running
134
+ open={false}
135
+ onOpenChange={() => {}}
136
+ />
137
+ ),
138
+ hold: 600,
139
+ label: "tool-call",
140
+ },
141
+ {
142
+ scene: (
143
+ <ApprovalCard
144
+ state="request"
145
+ command="cue write --cue 1"
146
+ title="Write the first cue"
147
+ subtitle="Universe 3, channel 1"
148
+ />
149
+ ),
150
+ hold: 1600,
151
+ label: "awaiting-approval",
152
+ },
153
+ { scene: <MarkdownText>{answer}</MarkdownText>, hold: 0, label: "done" },
154
+ ],
155
+ };
156
+
157
+ <ReplayPlayer script={SCRIPT} />;
158
+ <ReplayPlayer script={SCRIPT} startAt={17} autoPlay={false} rate={0.5} />;
159
+ ```
160
+
161
+ ### A controller over a filmstrip, not an animation
162
+
163
+ Every frame carries the **whole scene** rather than the part that changed, so
164
+ the component's entire rendering rule is `frames[index].scene`. That is what
165
+ makes the position controls honest: frame 12 reached by scrubbing is the same
166
+ DOM as frame 12 reached by watching, because there is no accumulated state for
167
+ the two paths to disagree about. A player whose frames accumulated — the way a
168
+ simulated terminal accumulates printed lines inside its own closure — can be
169
+ played and it can be restarted, but it can never be scrubbed.
170
+
171
+ ### One clock, so pause is exact
172
+
173
+ The two older players in this library pace themselves with a chain of
174
+ `setTimeout`s, which makes pause impossible: the time already spent inside the
175
+ in-flight timeout is unrecoverable, so a resume can only start the hold again.
176
+ Here the playhead is a number of _script_ milliseconds and wall time reaches it
177
+ through one monotonic origin — `base + (now() - origin) * rate` — so pausing is
178
+ "write down the playhead", resuming is "take a new origin", and the unspent
179
+ 380 ms of a 400 ms hold is still 380 ms on the other side of the pause. The same
180
+ identity is what makes the rate multiplier exact rather than approximate, and
181
+ what makes a seek and a play land on the same frame.
182
+
183
+ The transport is play, pause, step either way, scrub, restart and rate, built
184
+ from shipped cue components — a `Slider` for the scrubber, `IconButton`s, a
185
+ `Chip` for the state — and painted in cue's own metrics even when the scene
186
+ inside it is an `AgentSurface` island at upstream fidelity. The chrome belongs
187
+ to the console showing the run, not to the run.
188
+
189
+ ### Reduced motion is a mode, not an ending
190
+
191
+ Under `prefers-reduced-motion: reduce` **nothing is ever scheduled** — not
192
+ scheduled and hidden, not scheduled at speed, not scheduled at all — and the
193
+ player opens on the resting frame. Every control except play and rate keeps
194
+ working, so a reader who asked for stillness can still step through the run one
195
+ frame at a time and read every state it passes through. That is strictly more
196
+ than "show the end and stop", which is what this library offered before.
197
+
198
+ ### Legible from outside
199
+
200
+ The root publishes `data-replay-state` (the current frame's authored label),
201
+ `data-replay-playing` and `data-replay-frame`, the way `TerminalFrame` publishes
202
+ `data-simulating` — enough for a stylesheet, a screenshot harness or a test to
203
+ say "show me the awaiting-approval frame" without reaching into React.
204
+
205
+ State labels are **authored, not derived**: nothing in this tree emits a
206
+ run-state event and a scripted thread has no clock, so each frame carries its
207
+ own word from a set of seven — `queued`, `thinking`, `tool-call`,
208
+ `awaiting-approval`, `streaming`, `cancelled`, `done`. Two kinds of waiting, two
209
+ kinds of work, the one state that is waiting on a _human_, and the two endings a
210
+ transcript really has.
211
+
212
+ ### No peer, and no icon set
213
+
214
+ `./elements/replay` declares an empty peer row in the dist contract, and that is
215
+ a claim being checked rather than an omission: the five transport glyphs are
216
+ drawn in the file at 16px. A player is chrome, and chrome that drags an icon set
217
+ in behind it is a dependency nobody chose.
218
+
219
+ ## One run, written down once
220
+
221
+ A player needs something to play, and eight example pages hand-authoring their
222
+ own transcripts would have produced eight different fictions — eight inventions
223
+ of what a `grep` returns, eight decisions about what a cancelled call says, and
224
+ a reader who cannot compare two of them because they are not looking at the same
225
+ run.
226
+
227
+ So the run is data, in the corpus (`@repo/specimens`) rather than in the
228
+ package: fourteen tool payloads — read, glob, grep, bash, edit, write, patch,
229
+ plan, todowrite, task, webfetch, websearch, question, mcp — each readable at any
230
+ of six statuses through one total function, `withStatus(fixture, status)`, whose
231
+ totality the suite pins at all eighty-four pairs. Beside them, fourteen message
232
+ samples: turns with mentions and attachments, three reasoning blocks, five
233
+ answers including the one with a table and the one with sources, and `streamed`
234
+ for the part arriving a word at a time.
235
+
236
+ `CANONICAL_TRANSCRIPT` is what they compose into — the dark-mode-toggle run the
237
+ Cursor evidence recorded, in twenty-three beats over two turns: read, grep,
238
+ glob, plan, edit, shell, web search, a markdown answer, then a follow-up whose
239
+ edit streams in. A transcript is not a script; it is the _content_ of one, with
240
+ no opinion about how a beat is drawn. `transcriptScript(transcript, scene)` is
241
+ the join: hand it the way your surface draws a beat and it hands back a
242
+ `ReplayScript` with the frame count, the state labels and the captions already
243
+ right.
244
+
245
+ ## Eight studies, at fidelity, named
246
+
247
+ The gallery gains eight pages, all playing that same twenty-three-beat run, so
248
+ the only thing that differs between any two of them is the chrome.
249
+
250
+ Five are **fidelity studies of named consumer products** — ChatGPT, Claude,
251
+ Gemini, Grok and Perplexity — drawn to the trade dress the research recorded:
252
+ ChatGPT's always-visible action bar against Claude's hover-and-focus one,
253
+ Gemini's gradient-carried surface with no avatars, Grok's near-monochrome fold
254
+ of consecutive calls into one group, Perplexity's answer-engine shape where the
255
+ question becomes a heading over a strip of sources. Three are **studies of
256
+ IDE-style consoles** — Claude Code's timeline rail and number-keyed permission
257
+ card, Cursor's five-level conversation-density switch, Hermes's composer status
258
+ stack with approval answered inline.
259
+
260
+ These are internal reference and they say so on their own faces: every one of
261
+ the eight carries a header note naming the product and its maker, stating that
262
+ the page is a fidelity study drawn from the elements family, that it is **not a
263
+ cue product surface**, and that it is not affiliated with the maker — beside a
264
+ chip that says whether the palette was quoted from the record or matched to it.
265
+ Nothing here is a template to ship a product on. They exist so the family can be
266
+ judged against the surfaces it will be asked to imitate.
267
+
268
+ Two gallery axes make them linkable: `surface` (`cue` or `aui`, the value-set
269
+ the page's `AgentSurface` island paints in) and `t`, the frame the replay opens
270
+ on. Both compose with theme, density, mode and device rather than overriding
271
+ any, and `t` is optional rather than defaulted — a stamped `t=0` pauses the run
272
+ on its first frame, while no `t` at all lets it play, and defaulting would have
273
+ turned every copied link into the paused one.
274
+
275
+ The corpus and the site are `private: true` and none of this reaches the
276
+ tarball. It is here because it is the evidence for the minor above: a transport
277
+ nobody has driven through twenty-three states on eight different chromes is a
278
+ transport nobody has tested.
279
+
280
+ ## What did not change
281
+
282
+ The root barrel and `@cueplusplus/ui/elements` gained nothing — no export, no
283
+ peer, no bytes. The transport is chrome _around_ the elements family rather than
284
+ a member of it, so the group's import graph stays exactly as the vendor sync
285
+ emitted it. No component's props, structure or rendering moved; `elements.css`,
286
+ the island, the tokens and the sync pin are untouched.
287
+
288
+ - 7fb3004: ## Model output, rendered
289
+
290
+ Every component in this library has been able to say one thing about a string a
291
+ model wrote: it goes into the DOM as characters. `Message` places it,
292
+ `ToolCallCard` places it, all ninety-four elements place it, and the `Chat`
293
+ group's doctrine header said so in a line — **text is text**. That line was
294
+ doing real work. A component that quietly parsed model output would put every
295
+ consumer of this library one prompt away from an injection, because the
296
+ practical shape of prompt injection is not "the model turned evil", it is "the
297
+ model faithfully relayed bytes an attacker left on a page it fetched".
298
+
299
+ Verbatim is still the default, everywhere, unchanged. What this release adds is
300
+ the one exception, and everything about how it ships is an argument that it
301
+ should stay one: **`MarkdownText`**, on its own subpath
302
+ `@cueplusplus/ui/elements/markdown`, with its own two peers, not re-exported
303
+ from `@cueplusplus/ui/elements` and never going to be.
304
+
305
+ ```tsx
306
+ import { Message } from "@cueplusplus/ui";
307
+ import { MarkdownText } from "@cueplusplus/ui/elements/markdown";
308
+
309
+ <Message agent="researcher">
310
+ <MarkdownText streaming={turn.status === "running"}>
311
+ {turn.text}
312
+ </MarkdownText>
313
+ </Message>;
314
+ ```
315
+
316
+ `Message` did not change to make that work. It places what it is given, and what
317
+ it is given is a string until a caller writes the line above — which is the
318
+ whole design: rendering markup is a line of code somebody wrote, not a behaviour
319
+ they inherited.
320
+
321
+ ### What renders
322
+
323
+ Twenty tags, written out in the source rather than inherited from
324
+ `hast-util-sanitize`'s `defaultSchema`, because a spread schema means the set of
325
+ tags this library vouches for is whatever a transitive dependency decided this
326
+ week.
327
+
328
+ | | |
329
+ | ------ | ---------------------------------------------------------------------------------------------------------------------- |
330
+ | blocks | paragraphs, ATX and setext headings, bullet and ordered lists, block quotes, thematic breaks, fenced and indented code |
331
+ | inline | emphasis, strong, strikethrough, inline code, links, images, hard breaks |
332
+
333
+ Headings start at `h2` and clamp at `h6` — model output is a passage inside a
334
+ page that already owns its `h1` — and `headingLevel` moves the base. Fenced
335
+ blocks are `<pre><code>` containing the model's characters and nothing else
336
+ until `renderCodeBlock` is passed: a highlighter is a second parser over the
337
+ same untrusted string, usually one that emits HTML, so opting into it is
338
+ deliberate. Every part carries a `data-slot`, so an app restyles a heading or a
339
+ quote from the outside without a prop for it.
340
+
341
+ ### What is dropped
342
+
343
+ **Raw HTML has no path to the DOM, and the first reason is an absence rather
344
+ than a setting.** `react-markdown` spreads its own `allowDangerousHtml: true`
345
+ over whatever `remarkRehypeOptions` it is given, so that option cannot be turned
346
+ off from here and `<img onerror=…>` does become a `raw` node. A `raw` node is a
347
+ span of source text rather than an element, and the one thing that turns it back
348
+ into markup is `rehype-raw`. Nothing in this package depends on that plugin, and
349
+ a test asserts it never will — a `pnpm add rehype-raw` is a failing build, not a
350
+ quiet demotion of defence-in-depth to defence. `rehype-sanitize` then drops the
351
+ `raw` node type outright, and `skipHtml` removes whatever survived. Three gates,
352
+ each of them disabled in turn in the suite to prove they are redundant rather
353
+ than load-bearing one at a time.
354
+
355
+ None of the three is a deny-list of dangerous tags. A `<script>`, an `<iframe>`,
356
+ an `<svg>`, a `<form>` and an `onclick=` are the same non-event: there is
357
+ nothing in the tree for them to be.
358
+
359
+ **Attributes are enumerated, not filtered.** An element outside the subset
360
+ renders nothing; an element inside it renders only the attributes named beside
361
+ it — `href`/`title` on a link, `src`/`alt`/`title` on an image, `start` on an
362
+ ordered list, a single `language-…` class on a `code`. Everything else gets the
363
+ empty list. No element in the subset accepts `id`, `style`, `srcset`,
364
+ `formaction` or any `on*` handler, so none of them has an attribute to arrive
365
+ on. Comments and doctypes are refused, and DOM clobbering has no attribute to
366
+ travel on.
367
+
368
+ **URLs are allow-listed twice, and to three protocols.** `http`, `https` and
369
+ `mailto`, on both `href` and `src`. A relative URL stays relative; a
370
+ scheme-relative `//host` does not. A `javascript:` link, a `data:text/html`
371
+ image, `vbscript:`, `blob:`, `file:` and the `irc:`/`xmpp:` handlers React's
372
+ default pipeline permits all lose the attribute and leave an inert element
373
+ behind — an anchor with no destination, not an anchor to the page you are on.
374
+
375
+ **Every link carries `rel="noopener noreferrer nofollow"`, and that is not a
376
+ prop.** Reverse tabnabbing, a private console URL travelling in a `Referer`
377
+ header, and SEO laundering through somebody's transcript are not things a caller
378
+ should be able to turn off by accident. `target` is configurable (`_blank` by
379
+ default, because a transcript that navigates away loses the run); `rel` is not.
380
+ Images are `loading="lazy"`, `decoding="async"`, `referrerPolicy="no-referrer"`.
381
+
382
+ No GFM plugin is enabled, so there are no tables, task lists, footnotes or
383
+ autolink literals — an explicit `<https://…>` autolink still works. Task lists
384
+ are absent on purpose as much as by omission: a checkbox inside a transcript is
385
+ a control a model drew.
386
+
387
+ The threat model above is on the component, in its JSDoc, which means it is on
388
+ the docs page, in the manifest and in a consumer's hover — not in a wiki that
389
+ drifts from the code. It is held by 122 adversarial assertions run three ways:
390
+ settled, streaming, and at every prefix of a hostile stream.
391
+
392
+ ### Streaming
393
+
394
+ Pass `streaming` while the turn is arriving and the token the model is halfway
395
+ through typing is hidden rather than rendered as punctuation. Streaming markdown
396
+ is not malformed markdown — CommonMark has an answer for every truncation, and
397
+ that answer is the problem: `**bo` is a paragraph containing two literal
398
+ asterisks, and three frames later it is bold text and the asterisks are gone.
399
+ The visible result is punctuation flickering through every emphasis, link and
400
+ code span as it arrives, with a reflow behind it.
401
+
402
+ Six rules, covering unclosed fences, odd inline-code runs, half-written links
403
+ and autolinks, and unmatched emphasis openers — the words appear the moment the
404
+ model writes them and only their formatting waits for the closing token. None of
405
+ it is a second parser and none of it can widen what renders: every rule only
406
+ ever _deletes_ characters before the source reaches the pipeline, so a sealed
407
+ stream passes the same three gates as a settled document.
408
+
409
+ ### The line in `Chat` that changed
410
+
411
+ One doctrine bullet, from "**Text is text.** Nothing here parses markdown or
412
+ HTML" to "**Text is text, unless somebody says otherwise.**" — the same default,
413
+ now named as a default rather than as the only option, pointing at the renderer
414
+ for when a consumer wants it. No component in that group changed.
415
+
416
+ ## The vocabulary a model composes
417
+
418
+ The second half of this release is the other end of the same problem. A model
419
+ that can only answer in prose answers a flight query in prose. `GenerativeUI`
420
+ takes a tree of JSON a model emitted — 27 intrinsic names, nested — and draws
421
+ it.
422
+
423
+ ```tsx
424
+ import { GenerativeUI } from "@cueplusplus/ui/elements/generative";
425
+ import "@cueplusplus/ui/elements/generative.css";
426
+
427
+ // A `GenerativeAction` carries the intrinsic that fired it as `type`, so it is
428
+ // named rather than spread over a message discriminator of the same name.
429
+ <GenerativeUI
430
+ spec={JSON.parse(toolResult)}
431
+ onAction={({ action, type, value }) =>
432
+ send({ type: "ui-action", action, intrinsic: type, value })
433
+ }
434
+ onFault={(fault) => telemetry.warn("generative", fault)}
435
+ />;
436
+ ```
437
+
438
+ | | the 27 |
439
+ | -------------- | --------------------------------------------------------------------- |
440
+ | layout (7) | `Card` `Row` `Col` `Box` `Spacer` `Divider` `Carousel` |
441
+ | typography (4) | `Header` `Text` `Caption` `Markdown` |
442
+ | data (4) | `Fact` `Table` `Chart` `Badge` |
443
+ | media (2) | `Image` `Icon` |
444
+ | controls (7) | `Button` `Select` `Input` `DatePicker` `Checkbox` `RadioGroup` `Form` |
445
+ | lists (2) | `ListView` `ListViewItem` |
446
+ | feedback (1) | `Alert` |
447
+
448
+ Every other component here is handed props by a programmer, and a wrong one
449
+ costs a type error at the call site. This one is handed a tree by a language
450
+ model over a network, so `spec` is typed `unknown` — a signature demanding a
451
+ node would push every consumer into the same cast, which is a claim about the
452
+ bytes nobody is in a position to make — and the interesting question is not what
453
+ a correct spec renders as. It is what an incorrect one does, and the answer is
454
+ never an exception:
455
+
456
+ - a `$type` from a newer catalogue draws an in-place notice and its siblings
457
+ still render;
458
+ - a spec that is not a component tree draws a legible error state, never a blank
459
+ box that looks like a message still streaming;
460
+ - a tree deeper than 32 levels stops and says so rather than exhausting the
461
+ stack and taking the console with it;
462
+ - a component that throws anyway is caught by a boundary the renderer mounts
463
+ above its own walk.
464
+
465
+ All four also arrive as a stream on `onFault` — `not-a-node`, `unknown-type`,
466
+ `missing-prop`, `too-deep`, each with a dotted path — because the person who
467
+ needs to know this console is a version behind the model driving it is usually
468
+ not the person looking at it.
469
+
470
+ Two gates keep a widget from becoming a beacon. `Box` and `Card` take a
471
+ `background` and a size the model writes freely, and those land in an inline
472
+ style: colours are allow-listed by _shape_ (a hex triple, a numeric colour
473
+ function, a bare keyword) so `url(…)` and `image-set(…)` cannot reach a
474
+ `background`, and lengths must be a plain number-and-unit, which is enough for
475
+ `50%`, `12rem` and `320px` and not enough for `calc(…)` or `var(…)`.
476
+
477
+ ### It needs no Tailwind
478
+
479
+ The stylesheet is plain `[data-aui]` CSS and the tree it draws carries no
480
+ `class` attribute anywhere. That is upstream's arrangement and it is worth
481
+ keeping precisely: a widget a model composed has to draw inside whatever console
482
+ it was sent to, and a console is very often somebody else's app with no utility
483
+ framework, no shadcn and no design system of its own. Every hook is an attribute
484
+ rather than a class, because a class is a claim on a name in a host's one global
485
+ namespace and `[data-aui="card"]` collides with nothing.
486
+
487
+ The consequence runs all the way down. The 24 icons are hand-drawn SVG rather
488
+ than `lucide-react`; the chart is hand-drawn SVG rather than `recharts`; the
489
+ carousel snaps with CSS rather than a script. **`./elements/generative` declares
490
+ no optional peer at all** — an empty row in the dist-contract's peer map, which
491
+ is that claim checked against the built package — so there is no package in the
492
+ tree whose absence turns a weather card into a stack trace.
493
+
494
+ The sheet has two honest modes, and one seam between them. Exactly one rule in
495
+ 1,100 lines reads `--cue-*` tokens, and every reference in it carries a literal
496
+ fallback: with `@cueplusplus/ui/styles.css` present the vocabulary is cue's
497
+ palette, type ladder and density; without it, it is a plain legible widget that
498
+ a `<link>` away works in a host that has never heard of cue. Every other rule
499
+ spends `--aui-*` and nothing else, which is what makes a re-skin forty
500
+ declarations in one place rather than a hunt through two hundred rules.
501
+
502
+ ### Both skins reach it, two ways
503
+
504
+ The vocabulary answers the same two axes `AgentSurface` publishes, and it
505
+ answers them whether they arrive as props or by inheritance:
506
+
507
+ ```tsx
508
+ <GenerativeUI spec={spec} skin="aui" fidelity="upstream" />;
509
+
510
+ <AgentSurface skin="aui">
511
+ <GenerativeUI spec={spec} /> {/* no props: the stamp is on an ancestor */}
512
+ </AgentSurface>;
513
+ ```
514
+
515
+ Every island rule that lands on the root is written with both selectors — the
516
+ stamp on an ancestor, and the stamp on the root itself — so a tree that is its
517
+ own island and a tree inside a transcript paint identically.
518
+
519
+ - **`skin`** — `cue` by default. `aui` is upstream's own
520
+ `data-aui-theme="elements"` layer, transcribed: a 20px card, a 12px borderless
521
+ field on a filled ground, a pill button, a shadow instead of a hairline, and
522
+ labels that stop shouting. Geometry only — the colour re-pointing arrives
523
+ through `elements.css`, so an `aui`-skinned widget still tracks the app's
524
+ light and dark rather than pinning a palette of its own.
525
+ - **`fidelity`** — `cue-metrics` by default, which drives the five density knobs
526
+ upstream names (`--aui-control-height`, `--aui-control-font-size`,
527
+ `--aui-button-padding-x`, `--aui-field-padding-x`, `--aui-card-padding`) and
528
+ the five type steps off cue's ladder. `upstream` pins assistant-ui's own
529
+ numbers instead — a 36px control, 14px control type, `px-4`, `px-3`, `p-5` —
530
+ so density stops at the island edge. The two blocks are line for line, and a
531
+ test keeps them that way: a property one pins and the other does not restore
532
+ is a value that survives a nesting, and the subtree would draw at neither
533
+ fidelity.
534
+
535
+ ### Where the two halves meet
536
+
537
+ Once, through one prop. The `Markdown` intrinsic renders its source verbatim —
538
+ as characters — unless `renderMarkdown` is given, because the generative subpath
539
+ ships no parser and will not acquire one:
540
+
541
+ ```tsx
542
+ <GenerativeUI
543
+ spec={spec}
544
+ renderMarkdown={(md) => <MarkdownText>{md}</MarkdownText>}
545
+ />
546
+ ```
547
+
548
+ Neither subpath depends on the other. A console drawing weather cards installs
549
+ no sanitizer; a console rendering markdown installs no vocabulary.
550
+
551
+ ## Where to look
552
+
553
+ Benched at the states that matter rather than at the happy path. `MarkdownText`
554
+ on `elements-thread`: a settled document exercising the whole subset, whose last
555
+ line is a real injection shape and renders as nothing; a turn cut mid-token; a
556
+ fence still filling. `GenerativeUI` on `elements-knowledge`: a composed
557
+ dashboard, a form with all six control types, a chart, an unknown component, and
558
+ a spec that is not a tree at all. Pages are at `/docs/components/markdown-text`
559
+ and `/docs/components/generative-ui`.
560
+
561
+ ## What did not change
562
+
563
+ The root barrel, and `@cueplusplus/ui/elements`. Both entries gained nothing —
564
+ no export, no peer, no bytes. `react-markdown` and `rehype-sanitize` are
565
+ optional peers of `./elements/markdown` alone, externalised rather than inlined
566
+ for the usual reason and one more: bundling a sanitizer would freeze a security
567
+ boundary at whatever version was in the store the day this package was built,
568
+ where a consumer's `pnpm update` could never reach it.
569
+
570
+ No component's props, structure or rendering moved. The `Chat` group is the same
571
+ six components it was; `Message`, `AskBox`, `ToolCallCard` and all ninety-four
572
+ elements still render what they are given as characters. `elements.css`, the
573
+ island, the tokens and the sync pin are untouched.
574
+
575
+ One accessibility fix, found by running axe over the specs a model can emit
576
+ rather than over the benches: a `ListViewItem` a model put outside a `ListView`
577
+ drew as `role="listitem"`, which requires a `list` parent it did not have. A
578
+ stray row now draws as a row and says nothing about where it is, which is true.
579
+
580
+ `schemaVersion` is still `1`.
581
+
582
+ - 42e0672: ## One voice at the foot of the transcript
583
+
584
+ Two elements in this family are live regions. `TailStatus` is a `role="status"`,
585
+ because that is what makes the row at the foot of a transcript reach a reader
586
+ who cannot see the spinner. `LiveRegionAnnouncer` is a polite region with a
587
+ queue, and the nine transitions in `ANNOUNCEMENTS` are the same states that row
588
+ narrates.
589
+
590
+ A console that mounted both — the ordinary shape of an agent console, and the
591
+ one the benches show — had every state read out twice, out of step, because the
592
+ two regions update at different moments. Neither file mentioned the other.
593
+
594
+ They compose now, and the rule is that **the announcer speaks and the row
595
+ draws**:
596
+
597
+ ```tsx
598
+ <TailStatus action={TAIL_ACTIONS.searchingCode} detail="composer.tsx" />;
599
+ <LiveRegionAnnouncer messages={log} />;
600
+ ```
601
+
602
+ Mounting an announcer takes the speech off the row automatically. The words stay
603
+ on the screen, the `role="status"` goes, and `data-announces="false"` says which
604
+ of the two it is doing. Nothing to wire: the row notices on its own, wherever in
605
+ the tree either of them sits.
606
+
607
+ The row is the half that defers because the announcer is the better speaker. It
608
+ covers transitions the row can never show — an error, a permission request, a
609
+ finished answer. It is mounted empty and stays mounted, which is the only way a
610
+ live region is announced reliably, while a tail row comes and goes with the run.
611
+ And its whole mechanism — 400ms of spacing, five deep, paused while the page is
612
+ hidden or a modal has the reader — exists because narration that changes several
613
+ times a second is unusable when it is spoken, which is exactly what the row is.
614
+
615
+ Two ways to decide it by hand:
616
+
617
+ - `<TailStatus announce />` — the row speaks even with an announcer mounted, for
618
+ a console whose announcer is fed something other than these actions.
619
+ - `<TailStatus announce={false} />` — the row never speaks, for a console that
620
+ narrates the foot of the transcript some other way.
621
+
622
+ And `useAnnouncerPresent()` is exported, so a console's own status row can ask
623
+ the same question before becoming a second live region.
624
+
625
+ - 51e5046: ## The amber carries its own ink too
626
+
627
+ This is the amber half of the argument `the-red-carries-its-own-ink` already
628
+ made, and it is the same argument: the tone a control is _filled_ with decides
629
+ what can be read on it, and no other token in the palette knows that.
630
+
631
+ `TwoStepButton` is the one control in the library whose whole reason for
632
+ existing is that the press is consequential — arm, then confirm. Its `warn`
633
+ confirm painted `bg-warn text-black`. Black on an amber is a good guess, and on
634
+ the ten dark blocks it is right by a distance: 7.11:1 to 12.41:1. It is wrong on
635
+ the light ones, because a light theme darkens its amber to clear the paper and a
636
+ darkened amber wants paper back. **3.58:1** on requestport, **3.74:1** on dusk,
637
+ **4.22:1** on cue's own light block — three of the twenty preset x mode blocks
638
+ under AA, on the half of the control that does the thing.
639
+
640
+ It could not be closed before: there was no `--cue-warn-fg` to close it with,
641
+ and the queued note for the red said so in as many words. There is one now.
642
+
643
+ ### `--cue-warn-fg`
644
+
645
+ A new authored anchor, per preset, per mode, in the colour contract beside the
646
+ tone it is chosen against, with `text-warn-fg` / `bg-warn-fg` bridged into
647
+ Tailwind like every other colour token:
648
+
649
+ ```css
650
+ [data-theme="cue"] {
651
+ --cue-warn: oklch(0.8 0.17 75);
652
+ --cue-warn-fg: #000000;
653
+ }
654
+ [data-theme="cue"][data-mode="light"] {
655
+ --cue-warn: oklch(0.55 0.115 70);
656
+ --cue-warn-fg: #fcfcfc;
657
+ }
658
+ ```
659
+
660
+ Not one value is hand-picked. Each is the same `pickInk` `createTheme()` already
661
+ uses for `accent-fg` and `danger-fg`, run over the extremes the theme itself
662
+ owns — its page, its deepest well, its type — falling to pure black or white
663
+ only where none of the three reaches AA. Run over the reds instead, it
664
+ reproduces all twenty shipped `danger-fg` values exactly, spelling included,
665
+ which is the check that this is the same derivation rather than a second one
666
+ wearing its name.
667
+
668
+ The amber does not answer the way the red did, and that is the argument for
669
+ deriving against each tone instead of writing one rule about ink on a status
670
+ colour. All ten dark blocks take their deepest ground. The light ten split three
671
+ ways: cue, dusk and requestport take their paper; hivehub, luma, signal and
672
+ terminal take the theme's own type; quotamate, snuffle and venu take plain black
673
+ because nothing they own reaches AA on their amber. The worst amber pair in the
674
+ system is **4.77:1** (luma, light). The worst pair anywhere is still the red's
675
+ 4.59:1.
676
+
677
+ `CONTRAST_REQUIREMENTS` gains an eleventh pair, `warn-fg/warn` at 4.5:1, so a
678
+ generated palette is judged on it too, and `createTheme()` derives it for one.
679
+ The configurator shows it beside the amber in the Status group. The assistant-ui
680
+ island declares its own, because that block re-tints `--cue-warn` and an ink
681
+ chosen against the app's amber is an ink chosen against a colour nobody is
682
+ looking at in there.
683
+
684
+ ### The sweep covers the amber now
685
+
686
+ `test/destructive-ink.test.ts` reads the source and measures what the components
687
+ _spend_ rather than what the palette _offers_. It swept the red and said in its
688
+ own prose that it did not sweep the amber, because sweeping a defect nobody
689
+ could fix turns a missing token into a red suite instead of into a decision. The
690
+ token exists, so the amber is in: every class string that makes `bg-warn` a
691
+ ground and puts an ink on it, every rule in the package's own stylesheets that
692
+ does the same, measured on all twenty blocks at 4.5:1, with the fills pinned by
693
+ name per tone so a new one has to choose its ink in the commit that paints it. A
694
+ tint keeps its exemption — `bg-warn/10 text-warn` and the search highlight's
695
+ `bg-warn/35 text-fg` are read against the page, and the report already owns
696
+ those pairs — and the file says out loud what that exemption does not cover.
697
+
698
+ One number beside it got more honest. Both that sweep and
699
+ `everywhere-the-red-is-spent` said the right ink "falls to 3.95:1 on quotamate's
700
+ dark" under `bg-danger/90` without naming the ground the alpha let through. It
701
+ is **3.94:1** over `--cue-sunken` and **3.97:1** over `--cue-bg`. That the two
702
+ grounds disagree at all is the reason an alpha is refused rather than measured.
703
+
704
+ ### And the amber's pair is swept on its own, the way the red's is
705
+
706
+ `destructive-ink.test.ts` covers `warn-fg` on `warn` across all twenty blocks
707
+ for exactly as long as `TwoStepButton` keeps painting that string, because it
708
+ measures the call site. The red does not depend on that: it has a second,
709
+ unconditional sweep of the token pair beside `RevertDock` in
710
+ `elements/canon.test.tsx`. The amber had no equivalent, and the gap is not
711
+ theoretical — with the confirm moved off the amber in the way a later refactor
712
+ legitimately might, and that file's pinned list updated to match, a `warn-fg`
713
+ re-tinted down to **1.62:1** passed every contrast guard in the package.
714
+
715
+ So `instruments/two-step-button.test.tsx` gains the sweep the red has, asking
716
+ nothing about what any component currently paints, plus the spot check that says
717
+ this confirm takes the amber at full strength in the amber's own ink rather than
718
+ the `text-black` it used to.
719
+
720
+ ### If you ship your own theme block
721
+
722
+ A hand-written `[data-theme="acme"]` block should declare `--cue-warn-fg`
723
+ alongside `--cue-warn`, the same way it now declares `--cue-danger-fg` alongside
724
+ `--cue-danger`. Without it, `text-warn-fg` resolves to nothing on your theme.
725
+ Themes generated by `createTheme()` and the configurator's exports already carry
726
+ it.
727
+
728
+ - 704f9e5: ## The canon
729
+
730
+ The elements family this library spent four releases adopting is ninety-five
731
+ files deep and it is one product's answer to what a transcript needs. A very
732
+ good one — and one. So before closing the adoption, four teardowns went reading
733
+ the others: Cursor and Trae's local bundles, Claude Code's editor panel, the
734
+ Codex extension, and the open-source field — opencode, Cline, Roo, Continue,
735
+ Void, Zed, OpenHands, Hermes. One question each. **Which rows do three or more
736
+ shipping agent UIs have, that no component catalogue ships at all?**
737
+
738
+ Seventeen answers, and they are in this release. They live in
739
+ `@cueplusplus/ui/elements` beside the vendored family, in the same barrel,
740
+ inside the same island, on the same `data-slot` conventions — but they are
741
+ cue's, hand-written in house style rather than fetched and rewritten, and the
742
+ sync codemod never sees them, so there is nothing here for a re-sync to revert.
743
+ Every file opens with a comment naming the products that justify it and the
744
+ report section that recorded it.
745
+
746
+ ```tsx
747
+ import {
748
+ CompactionRow,
749
+ TailStatus,
750
+ TurnFooter,
751
+ WorkCollapse,
752
+ verbFor,
753
+ } from "@cueplusplus/ui/elements";
754
+
755
+ <WorkCollapse
756
+ durationMs={252_000}
757
+ toolCalls={14}
758
+ open={open}
759
+ onOpenChange={setOpen}
760
+ >
761
+ {rows}
762
+ </WorkCollapse>;
763
+
764
+ <CompactionRow
765
+ state="completed"
766
+ tokensBefore={128_000}
767
+ tokensAfter={24_000}
768
+ messagesBefore={214}
769
+ messagesAfter={31}
770
+ />;
771
+
772
+ <TurnFooter
773
+ onCopy={copy}
774
+ onFork={fork}
775
+ timestamp={{
776
+ label: "2m ago",
777
+ title: "Today at 2:14 PM",
778
+ subtitle: "Worked for 2m 15s",
779
+ }}
780
+ />;
781
+
782
+ <TailStatus
783
+ action={verbFor("shell", "active")}
784
+ detail="pnpm test"
785
+ onCancel={stop}
786
+ />;
787
+ ```
788
+
789
+ ### The seventeen, and who else ships them
790
+
791
+ | | | |
792
+ | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
793
+ | `TailStatus` | the live row at the foot: an action, a detail that truncates on its own, an inline Cancel that exists only when there is something to cancel | Cursor's `AgentTranscriptTailStatus`, opencode's action vocabulary |
794
+ | `WorkCollapse` | `Worked for 4m 12s and made 14 tool calls`, expanding back to the rows it folded | Cline's `WorkActivity`, Hermes's run collapsing |
795
+ | `TimeBoundary` / `UnreadDivider` | the two transcript rules nobody documents — one row each, placed by the caller | Cursor's row union (the only exhaustive one in any shipping product) |
796
+ | `TurnFooter` / `EndOfTurnSummary` | copy / reply / fork with a three-level timestamp, and `3 files changed +48 −12` | Cursor; the diff-stat half also Zed, Roo, Cline |
797
+ | `CompactionRow` | `128k → 24k tokens · 214 → 31 messages`, in all five states | Cline, Zed, Roo, Continue |
798
+ | `QueueDock` | what you typed while it was working — foldable, with edit / send-now / remove on every entry | opencode's `followupDock`, Cursor, Cline, Roo, Zed |
799
+ | `RevertDock` | restore, on the files-versus-conversation axis, behind a second press | Roo's `CheckpointMenu`, Cline, Cursor, opencode |
800
+ | `ContextUsage` | a segmented bar, a `Category / Tokens / Usage` table, and the stat rows that turn a gauge into a bill | Cursor's tray, opencode's context tab, Claude Code's modal |
801
+ | `PermissionScopes` | approval whose "always allow" writes its own label and names the file it writes to | Claude Code's scope cycler, Zed's granularity dropdown, Roo, Cline, Void |
802
+ | `AgentModeBadge` | how much authority the run is holding, as a chip and as `data-agent-mode` | Claude Code's mode-tinted chrome, Cursor's mode tokens |
803
+ | `RiskBadge` | an explicit `unknown` / `low` / `medium` / `high` on the call, with the qualifier beside it | OpenHands' `SecurityRisk`; the qualifier from Cline, Roo, Zed |
804
+ | `VerdictRow` | a scored verification result on a finished turn — rating, score, and what is most likely wrong | OpenHands' critic display, generalised past the critic |
805
+ | `AnimatedNumber` | a token, cost or duration readout that counts to its new value in tabular numerals | Claude Code's live thinking-token counter |
806
+ | `Clamp` | the truncation trio — a height, a fixed-pixel dissolve, a way back | Claude Code's five separate truncations |
807
+ | `LiveRegionAnnouncer` | one polite atomic region and the queue that makes it audible | Claude Code's announcement queue, Codex's announcement strings |
808
+
809
+ Nothing here shipped on taste. The rule for the phase was that a component
810
+ which could not point at a report section did not ship, and the two that are
811
+ gaps rather than crowds say so in their own files: `RiskBadge` exists because
812
+ six of the seven open-source UIs encode risk _implicitly_ — you reconstruct how
813
+ dangerous a call is from which approval bucket its tool happened to land in —
814
+ and one, OpenHands, makes it a property of the action. `VerdictRow` exists
815
+ because every agent runtime now scores its own work somewhere and, in six of
816
+ seven, that judgement reaches the reader as prose or not at all.
817
+
818
+ ### Copy is data, not markup
819
+
820
+ The tense triad is the single most repeated pattern in the whole study.
821
+ `Run` / `Running` / `Ran`. Cursor keeps it in `tool-action-labels.js`, Void as
822
+ `{proposed, running, done}`, Continue as `{wouldLikeTo, isCurrently,
823
+ hasAlready}`, Hermes as five category verbs, opencode inside an i18n dictionary
824
+ its own `AGENTS.md` forbids anyone to bypass. Six products, the same three
825
+ strings, private to the app every single time.
826
+
827
+ Here they are tables. `VERB_TRIADS` with `verbFor()` over it — which falls back
828
+ to `Use` / `Using` / `Used` for a kind it does not know, because an MCP server
829
+ names its own tools and a row forced to choose between `undefined` and the raw
830
+ wire name will render the wire name. Beside it: `TAIL_ACTIONS` (opencode's ten
831
+ status phrases), `TURN_ACTION_LABELS`, `COMPACTION_LABELS`, `QUEUE_LABELS`, the
832
+ revert triple, the context set, the authority ladder, the permission pair, the
833
+ two judgements, `CLAMP_LABELS` and the nine `ANNOUNCEMENTS`.
834
+
835
+ ```tsx
836
+ import { VERB_TRIADS, verbFor } from "@cueplusplus/ui/elements";
837
+
838
+ // Your product's agent renders rather than uses.
839
+ const VERBS = {
840
+ ...VERB_TRIADS,
841
+ render: { proposed: "Render", active: "Rendering", past: "Rendered" },
842
+ };
843
+ ```
844
+
845
+ Every component that draws one of these takes the copy as a prop and defaults
846
+ to the table. Localising a console, or writing for a product whose agent
847
+ "edits" where this one "patches", is spreading a table — not forking a
848
+ component to change a word.
849
+
850
+ ### The mode ladder is a token contract, not a colour prop
851
+
852
+ The most distinctive systemic idea in the study is not a component. Claude
853
+ Code's panel puts one attribute on the composer, the send button and the
854
+ spinner, and each re-derives its accent from it — six modes painted in four
855
+ colours, because what a mode _means_ is how much authority the run is holding,
856
+ and that ladder has four rungs everywhere. Cursor reaches the same place from
857
+ the token side: a quadruple per mode that the chip, the composer border and the
858
+ transcript accent all read.
859
+
860
+ So `@cueplusplus/ui/elements.css` gains sixteen properties — four per rung,
861
+ declared under `[data-agent-mode]` — and four aliases pointing at whichever
862
+ rung the attribute names:
863
+
864
+ ```css
865
+ .composer:focus-within {
866
+ border-color: var(--cue-elements-mode-icon);
867
+ }
868
+ .send-button {
869
+ background: var(--cue-elements-mode-icon);
870
+ color: var(--cue-elements-mode-background);
871
+ }
872
+ .turn-rail {
873
+ background: var(--cue-elements-mode-border);
874
+ }
875
+ ```
876
+
877
+ Stamp `data-agent-mode="unsafe"` on the composer and all three follow, with no
878
+ prop threading and no branch in any component. `AgentModeBadge` is the readout
879
+ over the same four properties, not the API.
880
+
881
+ They are declared under the attribute rather than at `:root` on purpose: a
882
+ custom property's `var()` references resolve where they are declared, so a rung
883
+ declared at `:root` would paint a badge inside an `<AgentSurface skin="aui">`
884
+ in the console's palette while everything around it painted in the island's.
885
+ The default rung is the family's neutral, not `manual`, because an unmapped
886
+ mode painted brand-orange would be claiming the run asks before it acts, and
887
+ that is the one claim nobody should make on a guess. Those same four neutrals
888
+ are the badge's inline fallbacks, so a consumer who never imports
889
+ `elements.css` gets a legible chip that makes no claim rather than an invisible
890
+ one — and the unpainted and the unmapped chip look alike deliberately.
891
+
892
+ ### The controls that are not allowed to lie
893
+
894
+ Three of the seventeen sit where a control that misstates what it is about to
895
+ do is a defect rather than a polish item.
896
+
897
+ **`PermissionScopes`** is the one control in an agent UI whose label has to be
898
+ computed, because what it grants is different every time it appears:
899
+ `Yes, allow Bash(cue patch:*) for this project (just you)`, with the file that
900
+ grant is saved to in a sentence underneath. Two departures from the reference,
901
+ both in the same direction. Nothing is elided — the reference shortens a rule
902
+ past twenty characters and strips the `:*` off a prefix rule, and both cuts
903
+ land on exactly the part that says how far the grant reaches, so this label
904
+ wraps instead — inside the word where it has to, since a rule and a directory
905
+ are one word to a line breaker and an underscored path gives it nowhere else
906
+ to go, and so does every other string the caller hands it: the subject beside
907
+ the question and the sentence naming the file the grant is written into break
908
+ the same way, because the account of where a permission lands is no more
909
+ allowed off the edge of the card than the account of what it grants. And when
910
+ there are more rules than the sentence can name, every one of them is listed
911
+ underneath, because `and 3 more` is a count and a count is not an account. No scope means no always-allow answer at all: a standing
912
+ grant that cannot say where it is saved is not one this element will offer.
913
+ The scope table ships the four reaches in words and a file path for none of
914
+ them — where a grant lands is the product's answer, and a design system that
915
+ guessed at it would commit the very defect this element exists to prevent.
916
+
917
+ **`RevertDock`** is the family's one destructive control and is built like one.
918
+ The axis first, because a run changes the files and the conversation about them
919
+ and putting one back is not putting the other back. Then the gate: choosing a
920
+ scope only arms the dock, `onRestore` can fire from the second separate press
921
+ alone, and `This action cannot be undone.` is on the screen the whole time the
922
+ dock is open rather than in a props table somebody reads afterwards. Then the
923
+ geometry, which is the half a gate cannot do on its own: arming does not
924
+ replace the scope list — it marks the chosen scope, disables the list where it
925
+ stands and appends the question _underneath_ it, so the destructive control is
926
+ never drawn over the control that armed it and a double click cannot walk
927
+ through both steps. Its confirm also refuses presses for 400 ms after it
928
+ arrives, and fires once per arm. The way out is `TwoStepButton`'s ruling rather
929
+ than a second one: focus lands on `Keep everything` and never on the
930
+ destructive half, that button comes first in the DOM as well as on the screen,
931
+ `Escape` disarms and stops there rather than closing whatever the dock is
932
+ inside, and folding the dock away disarms it — an armed state nobody can see is
933
+ one somebody re-opens into.
934
+
935
+ **`RiskBadge`** treats `unknown` as a first-class value drawn in the neutral,
936
+ never in the low-risk green: a runtime with no scorer must not be made to look
937
+ like one that scored everything safe. And the qualifier renders beside the
938
+ label rather than hiding in a `title`, because `High risk` alone is a
939
+ temperature while `High risk · outside your workspace` is a fact somebody can
940
+ act on.
941
+
942
+ ### Three that are contracts more than components
943
+
944
+ **`AnimatedNumber`** counts to its new value instead of jumping to it, in
945
+ tabular numerals — both halves lifted from one line of a shipping bundle, and
946
+ the second is not cosmetic: a proportional `1` is narrower than a `0`, so a
947
+ stepping counter nudges everything to its right on every update, which is what
948
+ makes a busy transcript feel unstable. The value stays a prop and every tween
949
+ lands exactly on it. Mid-tween the digits are `aria-hidden` with the settled
950
+ value carried beside them, so a screen reader hears what the run reported and
951
+ never a frame of the animation. Reduced motion is answered by _arriving_: no
952
+ frame is ever scheduled, because a readout that stopped where it was would be
953
+ wrong rather than merely still.
954
+
955
+ **`Clamp`** is a height, a dissolve of a fixed number of pixels, and a way
956
+ back. The fixed depth is the part worth copying — ten pixels of dissolve is the
957
+ same cut over a two-line row and a forty-line one, which is why the reference's
958
+ five separate truncations read as one idea. The fade is a **mask**, not a
959
+ gradient to the surface colour, which is the bug the reference shipped: its
960
+ diff variant hard-codes `#1e1e1e` and smears on a light theme, while a mask
961
+ fades the content itself and has no colour to get wrong in any theme, either
962
+ mode, or inside an island. The trio is published as
963
+ `--cue-elements-clamp-height` / `-fade`, so a height overridden in a stylesheet
964
+ moves the dissolve with it. The reveal is always in the DOM and always
965
+ focusable — a `Show more` chip that appears on hover only is unreachable for
966
+ anyone not holding a pointer.
967
+
968
+ **`LiveRegionAnnouncer`** is the accessibility surface, and it is a queue
969
+ rather than a live region wired to state, because the naive version announces
970
+ almost nothing: polite announcements replace each other, so a region bound to a
971
+ transcript that changes four times a second speaks once and swallows the rest.
972
+ Four rules make it audible — 400 ms of spacing, five deep with the _oldest_
973
+ dropped (behind a busy run the newest is the true one), held while the page is
974
+ hidden or a modal has the reader, and an identical sentence re-announced by
975
+ appending a space, which changes the string without changing a word of it. The
976
+ hold is in force from the first render rather than the first effect, so an
977
+ announcer that mounts into a background tab holding a backlog — a restored
978
+ session, which is the whole reason `announceOnMount` exists — does not spend a
979
+ line of it finding out where it is. The
980
+ region mounts empty and stays mounted, since a live region added at the same
981
+ moment as its text is one most assistive technologies never announce. One
982
+ decision moves to the caller: `paused` is a prop rather than an internal
983
+ ref-count of open overlays, because this library has its own portal contract
984
+ and an announcer holding a second opinion about what is on screen would be
985
+ wrong exactly when it mattered.
986
+
987
+ ### What did not change
988
+
989
+ No new subpath, no new peer, no new group. The seventeen are exports of
990
+ `@cueplusplus/ui/elements`, drawn with the icon set that subpath already
991
+ declares, and the root barrel gained nothing. The ninety-five vendored files
992
+ are untouched and the sync pin, the codemod and `elements-sync.lock.json` are
993
+ exactly where P0 left them — a re-sync at a newer upstream SHA rewrites the
994
+ vendored tree and cannot touch a line of this. The tokens are untouched:
995
+ everything here paints from the contract the flagship release already shipped,
996
+ and `elements.css` gained one section rather than a new dependency for anyone
997
+ in cue mode.
998
+
999
+ ***
1000
+
1001
+ **This closes the adoption.** `@cueplusplus/ui/elements` is now the ninety-five
1002
+ vendored components, the island that can paint them in either value-set, the
1003
+ sanitized markdown surface and generative vocabulary, the replay transport with
1004
+ its fixture catalogue and eight gallery studies — and the seventeen rows the
1005
+ field study found that nobody was shipping.
1006
+
1007
+ - 06eb4ac: ## The flagship answers in colour
1008
+
1009
+ `cue` is a monochrome brand, and until this release it carried the monochrome
1010
+ all the way through to the one vocabulary that exists to be told apart at a
1011
+ glance. On the default theme `--cue-ok`, `--cue-busy`, `--cue-warn` and
1012
+ `--cue-info` were all `#ffffff` on dark and all `#0a0a0a` on light: four
1013
+ different meanings, one colour, distinguishable only by the glyph or the word
1014
+ beside them. It read as restraint on a settings page. It reads as a defect on a
1015
+ transcript where three tool calls are in three different states and the only
1016
+ question is which of them is still moving.
1017
+
1018
+ They are chromatic now.
1019
+
1020
+ | token | before (dark / light) | after (dark / light) |
1021
+ | -------------- | --------------------- | ----------------------------------------------- |
1022
+ | `--cue-ok` | `#ffffff` / `#0a0a0a` | `oklch(0.72 0.17 149)` / `oklch(0.53 0.14 149)` |
1023
+ | `--cue-busy` | `#ffffff` / `#0a0a0a` | `oklch(0.78 0.13 220)` / `oklch(0.54 0.1 225)` |
1024
+ | `--cue-warn` | `#ffffff` / `#0a0a0a` | `oklch(0.8 0.17 75)` / `oklch(0.55 0.115 70)` |
1025
+ | `--cue-info` | `#ffffff` / `#0a0a0a` | `oklch(0.75 0.12 252)` / `oklch(0.55 0.16 258)` |
1026
+ | `--cue-danger` | `#ff5f57` / `#d03030` | unchanged |
1027
+
1028
+ `ok` and `warn` are assistant-ui's own semantic pair; `busy` is a console cyan
1029
+ for in-flight and `info` a calmer blue for news. `danger` does not move — it was
1030
+ the one tone already chromatic, and it is the one nobody should have to
1031
+ re-learn.
1032
+
1033
+ The light row is darker than these hues usually run, and that is the point: a
1034
+ tone is not only a dot here. `Chip`, `StatusDot`, `Toast` and the rest of the
1035
+ tone family draw `text-<tone>` at 11px uppercase — text, judged at 4.5:1, not at
1036
+ the 3:1 a signal is held to. Every light value above clears 4.5:1 on `--cue-bg`
1037
+ (4.79 to 4.85, beside `danger`'s 4.94) and every one of them is inside sRGB, so
1038
+ the measured ratio is the painted one. Dark was never in question: it runs 8.5
1039
+ to 10.3:1.
1040
+
1041
+ The chrome is untouched. Ink, ground, surfaces, borders, `accent`, `focus` and
1042
+ `selection` are the same monochrome they have always been, on both modes. This
1043
+ change is the status vocabulary and the live hue below it, and nothing else.
1044
+
1045
+ The other nine presets do not move at all: `terminal`, `signal`, `venu`,
1046
+ `hivehub`, `dusk`, `luma`, `snuffle`, `quotamate` and `requestport` were already
1047
+ chromatic here, and an app on any of them sees nothing new in this section.
1048
+
1049
+ ### Migrating
1050
+
1051
+ **A console on the default theme that relied on monochrome statuses will now
1052
+ see colour.** No API moved and nothing needs editing to keep working, but
1053
+ pixels change, and they change in more places than a status pill: `Chip`,
1054
+ `StatusDot`, `Meter`, `Toast`, `Stat`, `StatusBar`, `LogViewer`, `Sparkline`,
1055
+ `GroupBar`, `TwoStepButton`, `Tree`, `TerminalFrame`, `AppWindowFrame`,
1056
+ `UsageChart`, `FileUpload`, `EnvVarInput`, `AskBox`, `DelegationCard` and
1057
+ `ToolCallCard` all spend these four tokens, along with the MIDI and DMX
1058
+ instruments. Anywhere a `tone` prop was chosen and then quietly rendered white,
1059
+ it now renders the tone. Screenshot tests on `data-theme="cue"` will need new
1060
+ baselines; a design that used `tone="info"` as a way of saying "plain" will now
1061
+ say "blue".
1062
+
1063
+ If a product deliberately wants the monochrome back, it is four declarations —
1064
+ the soft fill and the rim derive from the base with `color-mix`, so overriding
1065
+ the base moves the whole family with it:
1066
+
1067
+ ```css
1068
+ @import "tailwindcss";
1069
+ @import "@cueplusplus/ui/styles.css";
1070
+
1071
+ [data-theme="cue"] {
1072
+ --cue-ok: #ffffff;
1073
+ --cue-busy: #ffffff;
1074
+ --cue-warn: #ffffff;
1075
+ --cue-info: #ffffff;
1076
+ }
1077
+ [data-theme="cue"][data-mode="light"] {
1078
+ --cue-ok: #0a0a0a;
1079
+ --cue-busy: #0a0a0a;
1080
+ --cue-warn: #0a0a0a;
1081
+ --cue-info: #0a0a0a;
1082
+ }
1083
+ ```
1084
+
1085
+ Two selectors, not one, and the block goes _after_ the import — the preset's own
1086
+ rules are `[data-theme="cue"]` and `[data-theme="cue"][data-mode="light"]`, so
1087
+ an override has to match their specificity and win on source order. An app that
1088
+ runs `mode="system"` mirrors these onto
1089
+ `[data-theme="cue"][data-mode="system"]` inside the two `prefers-color-scheme`
1090
+ queries, exactly as the generated sheet does.
1091
+
1092
+ ## `--cue-stream`, the live hue
1093
+
1094
+ The colour contract gains one token and its two derivations —
1095
+ `--cue-stream`, `--cue-stream-soft` and `--cue-stream-border` — on all ten
1096
+ presets, in both modes, at the same 12% and 35% mixes the statuses use. The
1097
+ colour contract is 42 custom properties where it was 39.
1098
+
1099
+ It is the colour of something still arriving: a token mid-stream, a tool still
1100
+ running, a cursor that has not stopped. It is deliberately **not** a sixth
1101
+ status. A status is something a `Chip` or a `StatusDot` can be _told_ to be, and
1102
+ "streaming" is not a tone a caller picks — it is a property of a surface while
1103
+ the model is talking. There is no `tone="stream"` and there is not going to be
1104
+ one. What it shares with the statuses is the shape of its family, which is why
1105
+ it is derived beside them and bridged beside them:
1106
+
1107
+ ```
1108
+ --color-stream → text-stream bg-stream/15 border-stream/30
1109
+ ```
1110
+
1111
+ The bridge carries the tone at full strength and lets Tailwind's alpha modifier
1112
+ do the washes, the way `Chip` already writes `bg-busy/10`.
1113
+
1114
+ The flagship pair is assistant-ui's `--aui-live` — `blue-400` on ink verbatim,
1115
+ and on paper `blue-500`'s hue taken down to `oklch(0.55 0.21 259.815)`, because
1116
+ the elements draw the live hue as text and `blue-500` itself lands at 3.6:1
1117
+ there. Every other preset answers in its own blue, except `terminal`, which
1118
+ already owned a live hue and keeps it.
1119
+
1120
+ Three things that hold a second copy of the contract moved with it, so nothing
1121
+ needs to be told about the new token twice: `createTheme()` resolves and derives
1122
+ `stream` like any other tone and darkens an invented light one to the non-text
1123
+ AA floor; the contrast report checks `stream/bg` against that same floor — every
1124
+ preset clears it with room to spare, the tightest being `terminal`'s light block
1125
+ at 4.89:1; and the configurator edits and exports it beside the statuses rather
1126
+ than in the miscellany. A theme somebody generated before this release and pasted into their
1127
+ own CSS keeps working: `stream` falls back through the same resolution order
1128
+ every other invented token does.
1129
+
1130
+ ## `@cueplusplus/ui/elements`
1131
+
1132
+ A new group, and the largest one this library has added at once: the agent's own
1133
+ surface. Fifteen elements, plus the island and the two label primitives the
1134
+ family is built out of, at `/docs/components/elements`.
1135
+
1136
+ | element | what it is |
1137
+ | ----------------- | ----------------------------------------------------------- |
1138
+ | `ToolCall` | one invocation, request and result behind a disclosure |
1139
+ | `ToolGroup` | calls that went out together, collapsed to one row |
1140
+ | `ToolError` | one call failed, with the error legible |
1141
+ | `TerminalBlock` | output streaming line by line, ending in an exit status |
1142
+ | `CodeDiff` | a unified diff, tinted, sized for a chat column |
1143
+ | `ReviewableDiff` | the same diff where each hunk is a decision |
1144
+ | `FileTree` | everything a run touched, with the churn per file |
1145
+ | `ApprovalCard` | the agent asking before it does something with side effects |
1146
+ | `PermissionGrant` | granting a capability rather than approving an action |
1147
+ | `AgentPlan` | a checklist the agent works through |
1148
+ | `TodoList` | the agent's working list, rewritten mid-run |
1149
+ | `AgentStatus` | one pill: what it is doing, and for how long |
1150
+ | `SubagentList` | parallel workers, their models and their progress |
1151
+ | `TraceWaterfall` | every span in a run on one nested time axis |
1152
+ | `ChatPanel` | the whole family working together, with its five parts |
1153
+
1154
+ Beside them: `ShimmerLabel` and `SwapLabel`, the eighteen surface recipes the
1155
+ family shares as plain class strings (`paper`, `floating`, `field`, `mono`,
1156
+ `live`, `codeSurface`, `collapsePanel`, `pressable`, the swap pairs and the
1157
+ rest), and `range`'s six functions — `at`, `clamp`, `indexIn`, `pct`,
1158
+ `progressOf`, `take` — which are the arithmetic a replayed transcript is made
1159
+ of.
1160
+
1161
+ Two conventions run through the group and both are worth knowing before you read
1162
+ the source:
1163
+
1164
+ **State is a prop, and time is the caller's.** Nothing here holds a run.
1165
+ `ToolCall` is _told_ `running` and `open`; `TerminalBlock` is told how many of
1166
+ its lines are visible. Replaying a conversation is arithmetic on props, which is
1167
+ what makes every one of these benchable at every state.
1168
+
1169
+ **These components are written upstream's way, not this library's.** Plain
1170
+ exported functions with no `forwardRef`, a `data-slot` attribute on every root,
1171
+ `className` merged last through `cn`. That is deliberate rather than sloppy: it
1172
+ keeps a re-synced diff readable, and `data-slot` is what lets an app restyle a
1173
+ part from the outside without a prop for it. `AgentSurface` — cue's own — is the
1174
+ one component in the group written in the house style.
1175
+
1176
+ `lucide-react` is an **optional peer** of this entry, which is why the group is
1177
+ its own subpath instead of part of the root barrel: the family draws its own
1178
+ icons rather than taking them from a prop. Import nothing from `/elements` and
1179
+ it costs nothing — no install, no bundle, no resolution error.
1180
+
1181
+ `@cueplusplus/ui/elements.css` ships beside it and is **optional even for this
1182
+ subpath**, because the family is painted by the token layer already:
1183
+
1184
+ ```css
1185
+ @import "tailwindcss";
1186
+ @import "@cueplusplus/ui/styles.css";
1187
+ @import "@cueplusplus/ui/elements.css"; /* motion, and the supersede island */
1188
+ ```
1189
+
1190
+ It carries the two vocabularies upstream never shipped as anything but
1191
+ dependencies — the shimmer sweep and the nine entrance utilities, both stilled
1192
+ under `prefers-reduced-motion` — and the island values below. Skip it and the
1193
+ elements still render in cue's palette; you lose the motion and `skin="aui"`
1194
+ becomes a no-op.
1195
+
1196
+ **Every class it declares is namespaced `cue-elements-`**, and that is a
1197
+ compatibility promise rather than a house style. The entrance utilities are
1198
+ `tw-animate-css`'s by name — `animate-in`, `fade-in`, `slide-in-from-*` — and
1199
+ `tw-animate-css` is what shadcn installs by default, so a stylesheet declaring
1200
+ them again would give such an app two `.animate-in` rules with import order
1201
+ picking the winner, silently. The vendoring codemod renames them at emit, the
1202
+ lockfile records the table as `classRenames`, and a test fails on any class in
1203
+ this stylesheet that a third-party utility package could also own.
1204
+
1205
+ ## The island: one DOM, two value-sets
1206
+
1207
+ ```tsx
1208
+ import { AgentSurface, ToolCall } from "@cueplusplus/ui/elements";
1209
+
1210
+ <AgentSurface skin="aui" fidelity="upstream">
1211
+ <ToolCall
1212
+ label="Searched"
1213
+ activeLabel="Searching"
1214
+ query="composer"
1215
+ request={request}
1216
+ result={result}
1217
+ running
1218
+ open={open}
1219
+ onOpenChange={setOpen}
1220
+ />
1221
+ </AgentSurface>;
1222
+ ```
1223
+
1224
+ `AgentSurface` renders a plain `div` and stamps `data-cue-skin` and
1225
+ `data-cue-fidelity` on it. `elements.css` re-declares, under those attributes,
1226
+ the `--cue-*` tokens the family reads, and custom properties inherit — so the
1227
+ whole subtree repaints with no prop threading and no branch inside any
1228
+ component.
1229
+
1230
+ The two axes are independent, which is the switchable fidelity the spec asked
1231
+ for:
1232
+
1233
+ - **`skin`** — `cue` (the default) or `aui`, which installs assistant-ui's four
1234
+ shadcn variables and four literal hues as cue tokens, in light and dark.
1235
+ - **`fidelity`** — `cue-metrics` (the default) keeps cue's type scale and
1236
+ declares exactly one metric of its own:
1237
+ `--spacing: calc(0.25rem * var(--cue-density, 1))`. That is the lever that
1238
+ carries the density ladder into five hundred-odd vendored geometry utilities,
1239
+ none of which names a `--cue-space-*` token. `upstream` pins assistant-ui's
1240
+ radii, their 13.5/12/11px type and a flat `--spacing: 0.25rem` instead, so
1241
+ **density stops at the island edge** and the subtree keeps upstream's
1242
+ proportions whatever the console around it is set to.
1243
+
1244
+ It is not a theme. `data-theme`, `data-mode` and `data-density` are untouched,
1245
+ and a light app gets a light island.
1246
+
1247
+ Overlays follow. The portal stamp gains both axes, so a popover or dialog opened
1248
+ from inside an island — which mounts on `<body>`, far from the attributes —
1249
+ paints like the transcript it came from rather than like the page. Outside an
1250
+ island the two attributes are _absent_ rather than defaulted, so nothing that
1251
+ does not use this pays for it.
1252
+
1253
+ ## Where this code came from
1254
+
1255
+ Elements are not a package. `@assistant-ui/ui` is private at `0.0.0` and never
1256
+ published; the only channel is a shadcn registry whose index carries no version,
1257
+ no date and no hash, rebuilt from upstream `main` on every deploy. Copy-in is
1258
+ the intended use — so this release also ships the only thing that can answer
1259
+ "which Elements do we have".
1260
+
1261
+ `scripts/elements-sync/` is pinned to upstream commit
1262
+ `31a049fcfa846a76da7c8e2c0bcd62960825dbd4` (resolved 2026-08-22). It fetches
1263
+ all 96 `elements-*` registry items, verifies every allowlisted file against
1264
+ `raw.githubusercontent.com` at that pin, records every byte it read in
1265
+ `elements-sync.lock.json`, and runs a committed codemod that rewrites the fetched
1266
+ TSX before it lands: the ink ramp onto cue's four stops, the four literal
1267
+ Tailwind hues onto `stream`/`ok`/`danger`/`warn` with their alphas intact,
1268
+ literal pixel sizes onto the type scale, radii onto the three rungs, and every
1269
+ `dark:` variant resolved away because cue's mode axis is `[data-mode]` rather
1270
+ than `prefers-color-scheme`. Geometry is deliberately left alone: it all compiles
1271
+ through `--spacing`, which is the island's lever. Upstream's own 679 documented
1272
+ prop rows are merged into JSDoc on the way out, so the props table on each page
1273
+ is upstream's description of upstream's prop.
1274
+
1275
+ What that buys a consumer: the elements arrive as source this library owns and
1276
+ can fix, not as a dependency that can move underneath you; the "version" you have
1277
+ is a SHA you can read in `NOTICE.md` and diff against; and the licence obligation
1278
+ travels with the package. Upstream's MIT notice — Copyright (c) 2025 AgentbaseAI
1279
+ Inc. — is preserved verbatim in `packages/ui/src/elements/NOTICE.md` and named in
1280
+ the group's own documentation.
1281
+
1282
+ Two consequences worth stating plainly. **The directory is generated.** Every
1283
+ file in `src/elements/` except `surfaces.tsx` and `agent-surface.tsx` is output;
1284
+ a bug there is fixed by changing a codemod rule and re-running the sync, never by
1285
+ editing the file, because the next re-sync deletes a hand edit without telling
1286
+ anyone. And **six upstream export names are renamed at emit**, because docgen
1287
+ slugs are globally unique and the `Chat` family stays authoritative:
1288
+ `Composer → ElementsComposer`, `CommandPalette → ElementsCommandPalette`,
1289
+ `DataTable → ElementsDataTable`, `EmptyState → ChatEmptyState`,
1290
+ `Timeline → ElementsTimeline`, and `Source → CitationSource` (that one collides
1291
+ inside upstream's own family). None of the six is in this release — they land
1292
+ with the rest of the catalogue — but the table is generated by the sync, ships in
1293
+ the group barrel, and is the answer to "why is the import name not the one on
1294
+ assistant-ui's site".
1295
+
1296
+ ## What did not change
1297
+
1298
+ Outside the `cue` preset's four status values and its light `stream`, no colour
1299
+ moved. No component's
1300
+ props, structure or rendering changed, with two additions that are worth naming
1301
+ because they are additions rather than nothing: the portal stamp — both
1302
+ `useCuePortalProps` and `CuePortalFrame` — now carries `data-cue-skin` and
1303
+ `data-cue-fidelity` alongside the four axes it already carried, and both are
1304
+ _absent_ outside an island rather than defaulted; and the configurator's
1305
+ `TokenEditor` lists `stream` in its Status group, so a generated theme edits it
1306
+ where you would look for it.
1307
+
1308
+ The root barrel is untouched, so an app that never imports `/elements` gets no
1309
+ new peer, no new stylesheet and no new bytes, and `tw-shimmer` and
1310
+ `tw-animate-css` were reimplemented as CSS in this package rather than taken as
1311
+ dependencies. The `Chat` group is unchanged and stays authoritative on every
1312
+ name the two families both wanted.
1313
+
1314
+ Text is still text. Nothing in this group parses model output as markdown or
1315
+ HTML; the elements render what they are given, for the same reason `Message`
1316
+ does. Sanitized markdown is a documented, opt-in surface arriving later, with its
1317
+ own peer and its own entry.
1318
+
1319
+ `schemaVersion` is still `1`.
1320
+
1321
+ - 4e510e7: ## The red carries its own ink
1322
+
1323
+ `RevertDock`'s confirm is the one solid destructive fill in the elements family
1324
+ and the one control in it that does something irreversible. It painted
1325
+ `bg-danger/90 text-accent-fg`, and both halves of that were wrong.
1326
+
1327
+ `--cue-accent-fg` is the ink authored against `--cue-accent`. It has no
1328
+ relationship to `--cue-danger`, and on the ten reds this system ships it lands
1329
+ anywhere between **3.58:1** (venu, dark) and **6.67:1** (requestport, light).
1330
+ The `/90` then let the paper through and took roughly another 0.65 off the
1331
+ ratio. In cue's own light theme the label on that button measured **4.29:1** at
1332
+ the 12px this row is set in — under AA, on the press that cannot be undone.
1333
+ Inside an `<AgentSurface skin="aui">` in light mode it was **3.82:1**.
1334
+
1335
+ There is no existing token that fixes it. Every candidate the palette already
1336
+ owns fails somewhere: `accent-fg` fails terminal and venu in dark, `bg` fails
1337
+ terminal and hivehub in dark, the theme's own `fg` fails every light block. The
1338
+ ink a red can carry is a property of _that red_, not of the mode — so the red
1339
+ now has one.
1340
+
1341
+ ### `--cue-danger-fg`
1342
+
1343
+ A new authored anchor, per preset, per mode, in the colour contract beside the
1344
+ tone it is chosen against:
1345
+
1346
+ ```css
1347
+ [data-theme="cue"] {
1348
+ --cue-danger: #ff5f57;
1349
+ --cue-danger-fg: #000000;
1350
+ }
1351
+ [data-theme="cue"][data-mode="light"] {
1352
+ --cue-danger: #d03030;
1353
+ --cue-danger-fg: #fcfcfc;
1354
+ }
1355
+ ```
1356
+
1357
+ with `text-danger-fg` / `bg-danger-fg` bridged into Tailwind like every other
1358
+ colour token. `ok`, `busy`, `info` and `stream` have no such ink, and that is
1359
+ deliberate: they are read as signals — a dot, a rim, a 12% wash — and the text
1360
+ near them sits on a surface. The red is a tone this system fills a control with,
1361
+ which is what makes text on it text. (The amber turns out to be the other one —
1362
+ _The amber carries its own ink too_, later in this same release.)
1363
+
1364
+ Each preset's value is the extreme it already owns, measured: every light block
1365
+ takes its paper, seven dark blocks take their deepest ground, and the two whose
1366
+ red sits in the middle of the range (terminal, hivehub) take white, because
1367
+ nothing else reaches AA on it. The worst pair in the system is now **4.59:1**;
1368
+ it was **3.58:1**.
1369
+
1370
+ `createTheme()` derives the same token for a generated theme the way it derives
1371
+ `accent-fg` — best of the theme's ground, its deepest well and its ink, with
1372
+ pure black or white only when none of the three can be read — and
1373
+ `CONTRAST_REQUIREMENTS` gained a tenth pair, `danger-fg/danger` at 4.5:1, so a
1374
+ generated palette is judged on it too. The configurator shows it next to the red
1375
+ in the Status group.
1376
+
1377
+ A sweep in `@cueplusplus/ui`'s own suite now measures the shipped pairing across
1378
+ all ten presets in both modes and fails under 4.5:1, and a second one measures
1379
+ it inside the assistant-ui island, where the two halves legitimately disagree
1380
+ about which ink their red wants.
1381
+
1382
+ ### If you ship your own theme block
1383
+
1384
+ A hand-written `[data-theme="acme"]` block should declare `--cue-danger-fg`
1385
+ alongside `--cue-danger`. Without it, `text-danger-fg` resolves to nothing on
1386
+ your theme. Themes generated by `createTheme()` and the configurator's exports
1387
+ already carry it.
1388
+
1389
+ - b9ab713: ## The rest of the family
1390
+
1391
+ P0 shipped fifteen elements and the machinery that makes more of them. This
1392
+ release finishes the catalogue. **All 96 of assistant-ui's `elements-*` registry
1393
+ items now emit** — 94 elements, the eighteen shared surface recipes, and
1394
+ `range`'s six functions — through the same pinned sync, the same codemod and the
1395
+ same island. `@cueplusplus/ui/elements` goes from 23 exported components to
1396
+ **132**, plus two hooks, and is now the largest group in the library.
1397
+
1398
+ None of it was typed. The 79 new elements arrived the way the first fifteen did:
1399
+ `scripts/elements-sync/` fetches the registry at upstream
1400
+ `31a049fcfa846a76da7c8e2c0bcd62960825dbd4`, verifies every file against
1401
+ `raw.githubusercontent.com` at that pin, hashes what it read into
1402
+ `elements-sync.lock.json`, and runs a committed codemod that rewrites the fetched
1403
+ TSX before it lands. Adding an element is a line in an allowlist plus whatever
1404
+ rule its source needed; the emitted files are never edited by hand, because the
1405
+ next re-sync would delete the edit without telling anyone.
1406
+
1407
+ What that means for a consumer is the sentence the whole phase exists for: **the
1408
+ entire assistant-ui Elements catalogue is now cue-owned source.** Not a
1409
+ dependency that can move underneath you — there is no package to depend on;
1410
+ `@assistant-ui/ui` is private at `0.0.0` and the registry carries no version
1411
+ field. Every one of the 94 is painted by `--cue-*` tokens and answers to
1412
+ `data-theme`, `data-mode` and `data-density` like everything else here, and every
1413
+ one of them re-skins to assistant-ui's own values inside
1414
+ `<AgentSurface skin="aui">` without a prop being threaded anywhere.
1415
+
1416
+ | what landed | elements | what it covers |
1417
+ | ----------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1418
+ | thread & reasoning | 20 | reasoning revealed a beat at a time, text arriving a token at a time, and everything that hangs off one turn — actions, attachments, branches, timing, the queue, editing, quoting, regenerating, reading aloud |
1419
+ | knowledge & structured output | 21 | where an answer came from and how far to trust it — sources, inline citations, retrieved passages, the document it quotes, a confidence marker — and what it arrives as when prose is not enough: tables, charts, diagrams, spec sheets, maps, maths, canvases |
1420
+ | agents & observability | 18 | who is doing the work and what it costs — agent cards, handoffs, background runs, checkpoints, job progress, spend, context, quota — plus a session's failure modes and the four drawings it is legible in afterwards |
1421
+ | composer, thread list & shell | 20 | the application around the conversation — the composer and its twenty-one facets, the palette, the model picker, the prompt library, the thread list and its searches, settings, onboarding, the blank screen, the launcher bubble, the call |
1422
+
1423
+ Six upstream names are renamed at emit, because docgen slugs are globally unique
1424
+ and the `Chat` family stays authoritative: `Composer → ElementsComposer`,
1425
+ `CommandPalette → ElementsCommandPalette`, `DataTable → ElementsDataTable`,
1426
+ `EmptyState → ChatEmptyState`, `Timeline → ElementsTimeline`, and
1427
+ `Source → CitationSource`, which collides inside upstream's own family. The table
1428
+ is generated by the sync, ships in the group barrel's JSDoc, and is the answer to
1429
+ "why is the import name not the one on assistant-ui's site".
1430
+
1431
+ `composer` is the family's one compound: twenty-one exports out of a single file,
1432
+ all of them published, because upstream's own examples compose the facets rather
1433
+ than configuring a root — and a composer exported without `useSlashMatches` and
1434
+ `useMentionMatches` is a composer whose two menus nobody can open.
1435
+
1436
+ ## Four new kitchen-sink sections
1437
+
1438
+ The corpus is split the way the batches were, not lumped into one page:
1439
+ `elements-thread`, `elements-knowledge`, `elements-agents` and
1440
+ `elements-composer` join `elements` on `/kitchen-sink`, each with its own
1441
+ heading and its own axe case. One section holding a hundred and thirty benches
1442
+ is one unreadable file and one slow accessibility run; five sections are five of
1443
+ each. Every element is benched at its real states — a streaming element shows
1444
+ streaming _and_ settled, an error element shows the error — which is what makes
1445
+ the props table on each component page evidence rather than a claim.
1446
+
1447
+ ## `heat-graph`, a second optional peer
1448
+
1449
+ `ActivityGraph` draws a calendar of runs, and it draws it with `heat-graph` —
1450
+ the headless grid assistant-ui publishes beside the elements (MIT, same author).
1451
+ It ships as an **optional peer** at `^0.0.15`, the shape `recharts` and
1452
+ `@xyflow/react` already have: declared in `peerDependencies`, marked optional,
1453
+ externalised by the bundler, named in this entry's expected-peer list, and
1454
+ installed here only as a devDependency. One component of 132 reaches it, so an
1455
+ app that never renders a heat calendar installs nothing for it and sees no
1456
+ resolution error.
1457
+
1458
+ **This is a deliberate departure from the spec.** §B of
1459
+ `docs/specs/2026-08-22-agent-elements.md` says `heat-graph` is "vendored as a
1460
+ helper, not a peer". That line was written before the pipeline existed. Vendoring
1461
+ it now would mean hand-writing eight React components into a directory whose
1462
+ entire contract is that every file in it is generated — a hand-written tree
1463
+ inside a generated one, invisible to the lockfile, unreachable by the codemod,
1464
+ and the first thing a re-sync would trip over. The plan's decision 3, which asks
1465
+ for a peer with its four coordinated edits, is what was followed instead. The
1466
+ reason is recorded in `rules.mjs` beside the batch it belongs to, so the next
1467
+ person to read the spec finds the disagreement where the code is.
1468
+
1469
+ ## Two elements carry cue runtime, and that is the point
1470
+
1471
+ Two of the 96 needed more than a class rewrite, and both were held back through
1472
+ their own batch for a decision rather than improvised inside one. The decision:
1473
+ **the codemod may write cue's own code into a vendored file.**
1474
+
1475
+ - **`InlineCitation`** is the family's one overlay — a Base UI preview card
1476
+ behind each numbered marker — and its `Portal` now spreads
1477
+ `useCuePortalProps()`. A portaled subtree mounts on `<body>`, where the token
1478
+ layer resolves the document's values instead of the island's, so without the
1479
+ stamp a hover card opened inside an `<AgentSurface skin="aui">` would paint in
1480
+ your console's palette while the sentence behind it painted in assistant-ui's.
1481
+ That is precisely the bug P0 gave `PortalStamp` its `skin` / `fidelity`
1482
+ carriage to prevent. `@base-ui/react` was already a required peer, so this
1483
+ costs nothing new to install.
1484
+ - **`ScrollAnchor`** is the one element in all 96 that drives its own clock — a
1485
+ `setInterval` appending a turn every 1.3s — and the one whose viewport scrolls
1486
+ itself. Its effect now returns early under `usePrefersReducedMotion()`, after
1487
+ painting the still frame: the _finished_ transcript, pinned to the bottom,
1488
+ rather than a scene frozen three messages in. Its two imperative smooth
1489
+ scrolls lost their `behavior: "smooth"` option, which no browser will still for
1490
+ `prefers-reduced-motion`; the element already carries `scroll-smooth` on the
1491
+ same viewport, so the scroll stays smooth from CSS and one
1492
+ `motion-reduce:scroll-auto` twin now turns all three of them off.
1493
+
1494
+ Read as a leak, four lines of cue runtime inside vendored source look like the
1495
+ vendoring failing. They are the vendoring working. These files are cue-owned
1496
+ source — that is the whole premise, since Elements ship as copy-in behind a
1497
+ registry with no version — so `packages/ui/src/elements/` is this repository's
1498
+ code, generated rather than typed. The codemod already rewrites every class in it
1499
+ onto cue's tokens; making a vendored element obey cue's _contracts_ is the same
1500
+ act performed one layer up. An element that portals without the island stamp, or
1501
+ re-arms a callback without asking whether the reader wants motion, is broken in
1502
+ this system's terms whatever it does upstream.
1503
+
1504
+ And it is a rule, never a hand edit. `RUNTIME_INJECTIONS` in `rules.mjs` names
1505
+ the file, the hook, the module, the binding, the component and the exact lines
1506
+ that change, each with its reason; the codemod throws on every anchor it cannot
1507
+ find, `sync.test.mjs` fails on an injection that stopped reaching the emitted
1508
+ tree, and both files carry the reasoning in a comment where a reader meets the
1509
+ import.
1510
+
1511
+ ## Six text inputs get a focus ring back
1512
+
1513
+ `ElementsComposer`'s input, `ElementsCommandPalette`'s, `PromptLibrary`'s,
1514
+ `ThreadSearch`'s, `ConversationSearch`'s and `MobileComposer`'s are bare
1515
+ `<input>`s whose entire upstream class string is a placeholder colour, a size,
1516
+ `bg-transparent` and `outline-none`. They removed the browser's focus outline and
1517
+ put nothing in its place — a keyboard user tabbing into a command palette had no
1518
+ idea they had arrived. It is not a ring the codemod dropped; upstream never wrote
1519
+ one.
1520
+
1521
+ The rewrite restores cue's ring only where something in the class string says the
1522
+ element is focusable, and its signal was interaction-state variants and
1523
+ `group`/`peer` labels — none of which these six carry. `::placeholder` and
1524
+ `caret-color` apply to `input` and `textarea` and to nothing else in CSS, so
1525
+ either one is proof the element takes a caret, and an element that takes a caret
1526
+ is focusable. Both now count, and the six come back with the same
1527
+ `focus-visible:outline-2 / solid / offset-1 / accent` recipe every other control
1528
+ in this package uses.
1529
+
1530
+ **This is a WCAG 2.4.7 (Focus Visible, AA) fix you inherit by upgrading.** No
1531
+ prop moved and nothing needs editing; if you render any of those six, a visible
1532
+ focus indicator appears where there was none. The fix is in `rules.mjs`, so a
1533
+ re-sync reproduces it byte for byte rather than losing it.
1534
+
1535
+ ## `llms.txt` is an index again
1536
+
1537
+ The machine surfaces on `ui.cueplusplus.com` re-tier, because one of them was a
1538
+ release away from failing its own cap: `/llms.txt` was 386 lines against a
1539
+ ceiling of 400, and 308 of those lines were a component's name, its URL and a
1540
+ one-liner. What grows that file is the catalogue, and the catalogue is what the
1541
+ library is for, so raising the cap would have bought exactly one release.
1542
+
1543
+ `/llms.txt` now lists **groups** — nineteen lines, each with the group's page,
1544
+ its component and hook counts, the specifier it imports from and what it is for —
1545
+ and points at the sheet for everything inside them. 61 non-blank lines, from 386,
1546
+ with the same facts still coming off the manifest.
1547
+
1548
+ Nothing lost information. `/llms-components.txt` was already exhaustive and stays
1549
+ exhaustive: one line per component with its full props signature and its import
1550
+ specifier, one per hook with its call signature, and a header stating the rule for
1551
+ reaching any component's page
1552
+ (`https://ui.cueplusplus.com/docs/components/<slug>.md`). The index stopped
1553
+ duplicating the sheet, which is the only thing it was doing with 80% of its lines.
1554
+
1555
+ If you point an agent at these files, point it at both, in this order: `/llms.txt`
1556
+ first — what the library is, the rules a props table cannot show, and which group
1557
+ a thing lives in — then `/llms-components.txt` before it writes a call, for the
1558
+ specifier and the full props literal. `/llms-full.txt` and `/kitchen-sink.md` are
1559
+ unchanged.
1560
+
1561
+ ## What did not change
1562
+
1563
+ No token moved. `@cueplusplus/tokens` is untouched by this release — the colour
1564
+ contract is the same 42 custom properties the last one shipped — and no existing
1565
+ component's props, structure or rendering changed. The root barrel is still
1566
+ untouched, so an app that never imports `/elements` gets no new peer, no new
1567
+ stylesheet and no new bytes; `heat-graph` and `lucide-react` are both optional
1568
+ and both reached only from this subpath.
1569
+
1570
+ `@cueplusplus/ui/elements.css` gains exactly one declaration — a
1571
+ `cue-elements-zoom-in-95` entrance class, namespaced like the other nine so that
1572
+ an app which also installs `tw-animate-css` never has two stylesheets quietly
1573
+ arguing over `.animate-in` by import order. It remains optional even for this
1574
+ subpath: skip it and the elements still render in cue's palette, you lose the
1575
+ motion and `skin="aui"` becomes a no-op.
1576
+
1577
+ Text is still text. Nothing in this group parses model output as markdown or
1578
+ HTML — 132 components and not one of them decides your markup for you. Sanitized
1579
+ markdown remains a documented, opt-in surface arriving later, with its own peer
1580
+ and its own entry.
1581
+
1582
+ `schemaVersion` is still `1`.
1583
+
1584
+ - 854ad96: ## A streaming layer under the Chat group
1585
+
1586
+ The Chat group renders a turn that has finished arriving. It has never had a
1587
+ story for one that is _still_ arriving: no token assembly, no cancelling a run
1588
+ mid-sentence, no regenerating a turn beside the one it replaces, and nowhere to
1589
+ put the model's own tool call while the rest of the answer is still coming.
1590
+
1591
+ `@cueplusplus/ui/agent-runtime` is that story, and it is one hook wide.
1592
+ `useAgentThread(runtime)` takes any `@assistant-ui/react` runtime — the one you
1593
+ build with `useExternalStoreRuntime(adapter)`, or the AI-SDK or LangGraph ones —
1594
+ and gives back the props `Message`, `MessageList` and `Composer` already take.
1595
+ Every field is named for the prop it feeds: `busy`, `disabled`, `onSend` and
1596
+ `onStop` to `Composer`; `role`, `agent`, `pending` and `text` to `Message`;
1597
+ `toolCalls[]` onto `ToolCallCard`; `branch` and `switchBranch` to
1598
+ `BranchPicker`.
1599
+
1600
+ Two components ship in the entry with it. **`ToolCallCard`** is the model's own
1601
+ call: the tool name in mono, an optional risk chip in `AskBox`'s exact
1602
+ vocabulary, the status spelled in words beside a spinner rather than in colour
1603
+ alone, and the arguments and result behind a disclosure that mounts and unmounts
1604
+ instead of animating a height. It is neither of its neighbours — `AskBox` is a
1605
+ question put to a human, `DelegationCard` is a sub-agent's work, and a tool call
1606
+ is the model's own action. **`BranchPicker`** is a `‹ 2 / 3 ›` stepper for an
1607
+ agent turn's footer: two ghost icon buttons around a mono readout that reads
1608
+ itself aloud as "branch 2 of 3", clamped so it can never point past either end,
1609
+ and drawing nothing at all while there is only one branch.
1610
+
1611
+ `@assistant-ui/react@^0.15.16` is an **optional** peer, declared and
1612
+ externalised the way `@xyflow/react` is under `/flow`. Import nothing from
1613
+ `/agent-runtime` and it costs you nothing: no install, no bundle, no resolution
1614
+ error — the root barrel's import graph never reaches it, and neither does
1615
+ `/chat`'s. Import the entry and you do need it. Nothing in the shipped modules
1616
+ imports `@assistant-ui/react` at runtime — the hook's own file names only its
1617
+ types — but you cannot build the `AssistantRuntime` the hook takes without it,
1618
+ so install it. Installing it also installs its own dependencies — `radix-ui`
1619
+ and several `@radix-ui/*` parts, `zustand`, `zod`, `assistant-stream`,
1620
+ `@assistant-ui/core` — about 16 MB on disk; the subpath boundary keeps all of
1621
+ it out of an app that never imports the entry.
1622
+
1623
+ ```tsx
1624
+ import { Composer, Message, MessageList } from "@cueplusplus/ui";
1625
+ import {
1626
+ BranchPicker,
1627
+ ToolCallCard,
1628
+ useAgentThread,
1629
+ } from "@cueplusplus/ui/agent-runtime";
1630
+ import {
1631
+ useExternalStoreRuntime,
1632
+ type ThreadMessageLike,
1633
+ } from "@assistant-ui/react";
1634
+
1635
+ // Outside the component: the runtime caches the conversion against this
1636
+ // function, and a new one every render is an update loop.
1637
+ const convertMessage = (m: ThreadMessageLike) => m;
1638
+
1639
+ const runtime = useExternalStoreRuntime({
1640
+ messages,
1641
+ isRunning,
1642
+ convertMessage,
1643
+ onNew,
1644
+ onCancel,
1645
+ onReload,
1646
+ setMessages,
1647
+ });
1648
+ const thread = useAgentThread(runtime, { agent: "planner" });
1649
+
1650
+ <MessageList aria-label="Thread">
1651
+ {thread.turns.map((t) => (
1652
+ <Message
1653
+ key={t.id}
1654
+ role={t.role}
1655
+ agent={t.agent}
1656
+ pending={t.pending}
1657
+ footer={
1658
+ t.branch.count > 1 && thread.switchBranch ? (
1659
+ <BranchPicker
1660
+ index={t.branch.index}
1661
+ count={t.branch.count}
1662
+ onNavigate={(d) => thread.switchBranch?.(t.id, d)}
1663
+ disabled={thread.busy}
1664
+ />
1665
+ ) : undefined
1666
+ }
1667
+ >
1668
+ {t.text}
1669
+ {t.toolCalls.map((c) => (
1670
+ <ToolCallCard
1671
+ key={c.id}
1672
+ tool={c.tool}
1673
+ args={c.args}
1674
+ result={c.result}
1675
+ status={c.status}
1676
+ />
1677
+ ))}
1678
+ </Message>
1679
+ ))}
1680
+ </MessageList>;
1681
+ <Composer
1682
+ onSend={thread.onSend}
1683
+ busy={thread.busy}
1684
+ disabled={thread.disabled || thread.busy}
1685
+ onStop={thread.onStop}
1686
+ />;
1687
+ ```
1688
+
1689
+ ## What did not change
1690
+
1691
+ The six Chat components — `AgentPile`, `AskBox`, `Composer`, `DelegationCard`,
1692
+ `Message`, `MessageList` — render what they rendered before, with the one
1693
+ exception below. Nothing is restyled and no prop moved.
1694
+
1695
+ Text is still text. `ToolCallCard` renders arguments and results verbatim, never
1696
+ as markdown and never as HTML, for the same reason `Message` does: an argument
1697
+ string is whatever a model decided to emit, and the moment a component parses it
1698
+ the model chooses your markup. The hook joins text parts into strings rather
1699
+ than handing you a node tree to trust. No markdown package is added, opt-in or
1700
+ otherwise.
1701
+
1702
+ Nothing of assistant-ui's rendering is adopted. The hook is a
1703
+ `useSyncExternalStore` over the runtime's thread and nothing else — no
1704
+ `AssistantRuntimeProvider` anywhere in your tree, no primitive mounted, no Radix
1705
+ DOM, no borrowed context. Its `asChild` composition model is not adapted and not
1706
+ exposed: this library's public API is still 100% `render`-prop. assistant-ui
1707
+ owns the state machine, this library owns 100% of the render.
1708
+
1709
+ ## The one fix inside Chat
1710
+
1711
+ **`MessageList` now follows a turn that is still growing.** Its re-pin ran on the
1712
+ number of children changing, so a _new_ turn kept the view at the bottom but
1713
+ tokens streaming into a bubble that was already there did not — the list quietly
1714
+ stopped following mid-sentence, exactly when following matters most. It now also
1715
+ watches the items container and re-pins **when that container grows**, while
1716
+ following and pinned.
1717
+
1718
+ Only growth moves the view. A tool-call card the reader folds away, an image
1719
+ that failed to load, a footer that disappears — anything that makes the list
1720
+ shorter leaves the scroll position exactly where it was. Scrolling up still lets
1721
+ go, and a turn that grows while you are up there offers the "jump to latest"
1722
+ pill rather than pulling you down. `follow={false}` switches the whole mode off,
1723
+ growth included.
1724
+
1725
+ This is new behaviour on a component you already ship, so it is worth a moment
1726
+ if you have a `MessageList` whose children change height in place: it will now
1727
+ stay at the bottom where before it drifted up. There is no new prop and no API
1728
+ change, and an engine without `ResizeObserver` behaves exactly as it did.
1729
+
1730
+ ## Things worth knowing
1731
+
1732
+ **A branch switch is host-owned under an ExternalStore runtime.** `switchBranch`
1733
+ does not move the thread itself. It arrives at your adapter's `setMessages` with
1734
+ the newly visible list, and your host writes it back into whatever holds the
1735
+ messages. A host that ignores `setMessages` gets a picker that clicks and does
1736
+ nothing — which looks like a bug in the picker and is not one.
1737
+
1738
+ **`onReload` takes two arguments**, `(parentId, config)`. Regenerating re-runs
1739
+ the turn's _parent_, which is what puts the new answer beside the old one rather
1740
+ than after it — and is what gives `BranchPicker` a second branch to show at all.
1741
+ The picker only moves between branches that already exist, so the control that
1742
+ asks for a new one is yours to draw: a ghost `Button` in the turn's footer
1743
+ calling `regenerate(turn.id)`, as the docs demo does.
1744
+
1745
+ **Pass `convertMessage`, and declare it outside the component.** Without it your
1746
+ messages have to already be complete runtime messages, and a missing `metadata`
1747
+ throws; `convertMessage: (m) => m` lets the runtime fill the defaults in around
1748
+ the messages you already have. Give it a stable identity, though: the runtime
1749
+ caches the conversion against that function, so an arrow written inline in the
1750
+ adapter re-converts the whole thread on every render — and the next time your
1751
+ message list changes, that becomes React's "Maximum update depth exceeded"
1752
+ rather than a slow render.
1753
+
1754
+ **`onStop`, `regenerate` and `switchBranch` are `undefined`** until the adapter
1755
+ has the matching callback — `onCancel`, `onReload`, `setMessages`. That is
1756
+ deliberate rather than defensive: `Composer` keeps drawing Send while `onStop`
1757
+ is missing, so a control that could not work is never drawn.
1758
+
1759
+ **The peer range is pinned on purpose.** `^0.15.16`, not `^0.15.0`: 0.15.9
1760
+ through 0.15.16 shipped inside twelve days. What this entry reads are the stable
1761
+ members of the runtime object — `thread.getState()`, `subscribe`, `append`,
1762
+ `cancelRun`, `getMessageById`, `switchToBranch` — and never an `unstable_*` one,
1763
+ but a floor that recent is worth saying out loud rather than discovering.
1764
+
1765
+ The live thread, the four tool-call states and the branch stepper are on the
1766
+ docs site under **Agent runtime**, at `/docs/components/agent-runtime`.
1767
+
1768
+ ### Patch Changes
1769
+
1770
+ - e5fc161: ## Everywhere the red is spent
1771
+
1772
+ `--cue-danger-fg` arrived with `RevertDock`'s confirm and stopped there. The two
1773
+ destructive controls the rest of the library is actually built out of never got
1774
+ it, so the token shipped alongside the defect it was authored to close.
1775
+
1776
+ Three fills change, and all three are contrast fixes rather than restyles:
1777
+
1778
+ - **`Button` `variant="danger"`** — `bg-danger text-white` becomes
1779
+ `bg-danger text-danger-fg`.
1780
+ - **`TwoStepButton`'s armed `danger` confirm** — the same swap, on the control
1781
+ that exists because the action is irreversible.
1782
+ - **`VoiceConversation`'s end-call button** — `bg-danger/90 text-accent-fg`
1783
+ becomes `bg-danger text-danger-fg`. That element is vendored from
1784
+ assistant-ui, so the fix is a rule in `scripts/elements-sync/rules.mjs` and
1785
+ the file is re-emitted by the sync; a hand edit would have lasted until the
1786
+ next SHA bump.
1787
+
1788
+ `text-white` on the red measures **2.99:1** on cue's dark block — the default
1789
+ theme, under AA text and under the 3:1 floor a non-text control gets — and it is
1790
+ under AA on eight of the ten dark blocks this system ships. `text-danger-fg`
1791
+ measures **7.03:1** in the same place and clears 4.5:1 on all twenty
1792
+ preset x mode blocks. `text-accent-fg` is the ink authored against
1793
+ `--cue-accent`, and on the red it lands anywhere from 3.58:1 to 6.67:1; the
1794
+ `/90` then let the surface through and took the pair down with it.
1795
+
1796
+ Nothing you pass changes. A `danger` button, an armed confirm and an end-call
1797
+ button are the same elements with the same props; the label on them is legible
1798
+ now on every theme in both modes.
1799
+
1800
+ ### The reason all three stayed green
1801
+
1802
+ The sweep that arrived with the token measures **the token pair** — twenty
1803
+ blocks of `danger-fg` on `danger` — and a token pair is only the answer for a
1804
+ control that asks for it. These three asked for something else and sat outside
1805
+ the measurement.
1806
+
1807
+ `test/destructive-ink.test.ts` reads the source instead. It finds every class
1808
+ string in the package that makes the red a ground and puts an ink on it, takes
1809
+ the ink out of that string, and measures it on that theme's red across all
1810
+ twenty blocks at 4.5:1. It refuses an alpha on the fill, because
1811
+ `--cue-danger-fg` on `bg-danger/90` falls to 3.94:1 on quotamate's dark over
1812
+ `--cue-sunken` and 3.97:1 over `--cue-bg` — and that the two grounds disagree at
1813
+ all is why the alpha is refused rather than measured. It fails on an ink whose
1814
+ colour it cannot read rather than skipping it. The list of fills is pinned by
1815
+ name: a new destructive fill has to choose its ink in the commit that paints
1816
+ it.
1817
+
1818
+ `TwoStepButton`'s `warn` confirm keeps the `text-black` literal through this
1819
+ change, and its JSDoc stops claiming more than it can. At this point there was
1820
+ no `--cue-warn-fg` to give it, that pairing floored at 3.58:1 on requestport's
1821
+ light block, and the sweep said in writing that the amber was the one it did not
1822
+ cover. _The amber carries its own ink too_, in this same release, is where that
1823
+ gets closed.
1824
+
1825
+ - 58eb41a: ## The red the sheets draw
1826
+
1827
+ The generative vocabulary fills a control with the red too. `GenerativeUI`
1828
+ renders `[data-aui="button"][data-aui-style="danger"]` from a model's spec, and
1829
+ that button painted `--aui-bg` — the page under the widget — on a solid
1830
+ `--aui-danger`. It reads **4.03:1** on terminal's dark block and **4.12:1** on
1831
+ hivehub's, both under AA, and **3.82:1** on the `aui` skin's light half, which
1832
+ is the number `elements.css` already writes down beside `--cue-danger-fg` as
1833
+ the reason that token exists.
1834
+
1835
+ It takes `--aui-danger-fg` now, one more line on the seam
1836
+ (`var(--cue-danger-fg, #000000)`, a literal fallback like every other reference
1837
+ in that block), and it clears 4.5:1 on all twenty shipped preset x mode blocks
1838
+ and on both halves of the island. Nothing a spec can say changes: a `danger`
1839
+ button is the same intrinsic with the same props.
1840
+
1841
+ ### Why the sweep beside it did not see this one
1842
+
1843
+ `test/destructive-ink.test.ts` reads class strings, and this sheet has none.
1844
+ `generative.css` is plain `[data-aui]` CSS on purpose — no Tailwind, no utility
1845
+ to grep — so a fill drawn there sat outside a guard whose own name said it
1846
+ found every one.
1847
+
1848
+ It reads the package's stylesheets too now. For every rule that makes
1849
+ `--cue-danger` a whole-value ground and puts a `color` on it, it resolves both
1850
+ through the `--aui-*` seam and measures the pair on the same twenty blocks at
1851
+ the same 4.5:1, refuses an ink it cannot resolve to a theme colour rather than
1852
+ skipping it, and pins the rules it found by selector. A `color-mix()` does not
1853
+ resolve, which is the same exemption the class sweep gives `bg-danger/10`.
1854
+
1855
+ One hole in the class half closed with it: `text-[#7f1d1d]` was read as "not a
1856
+ colour" and took its whole call site out of the corpus in silence. Only lengths
1857
+ are excluded now — the six this package actually writes — so an arbitrary ink
1858
+ on the red fails by name and asks to be taught.
1859
+
1860
+ - Updated dependencies [51e5046]
1861
+ - Updated dependencies [06eb4ac]
1862
+ - Updated dependencies [4e510e7]
1863
+ - @cueplusplus/tokens@0.5.0
1864
+
1865
+ ## 0.4.0
1866
+
1867
+ ### Minor Changes
1868
+
1869
+ - e558992: Ten container icons, for the navigation column that just learned to rail
1870
+
1871
+ A navigation column is a list of places, and a place is a word until the column
1872
+ narrows. `AppShell` can rail now — the column drops to `3.5rem` and every row
1873
+ keeps its glyph and loses its name — which turns "does this row have an icon"
1874
+ from a styling question into whether the row still exists. This library had a
1875
+ public component for the marks (`CueMark`, `CueLogotype`, `Plussie`) and nine
1876
+ private `_glyphs` modules for its own chevrons and carets, and nothing at all
1877
+ for the things a catalogue actually sorts into.
1878
+
1879
+ ```tsx
1880
+ import { CatalogueIcon, ChannelBetaIcon, SkillIcon } from "@cueplusplus/ui";
1881
+
1882
+ <Sidebar.Item icon={CatalogueIcon}>All skills</Sidebar.Item>
1883
+ <Sidebar.Item icon={ChannelBetaIcon}>Our skills · beta</Sidebar.Item>
1884
+ ```
1885
+
1886
+ | Icon | The place it marks |
1887
+ | --------------------- | ------------------------------------------------------ |
1888
+ | `CatalogueIcon` | everything there is, before anything has been narrowed |
1889
+ | `StackIcon` | one person's stack |
1890
+ | `StacksMatrixIcon` | every stack at once, one column each |
1891
+ | `AudienceIcon` | a named group of people something is published to |
1892
+ | `ChannelReleasedIcon` | the released channel |
1893
+ | `ChannelBetaIcon` | the beta channel |
1894
+ | `FolderIcon` | a shelf somebody filed by hand |
1895
+ | `McpServerIcon` | an MCP server in the registry |
1896
+ | `CliToolIcon` | a CLI tool in the registry |
1897
+ | `SkillIcon` | a skill — the thing all the others hold |
1898
+
1899
+ ## It is a set, which is a stronger claim than ten icons
1900
+
1901
+ They are read in one column, one after another, so a grid or a weight that
1902
+ drifts on any one of them is read as a mistake on _that row_ rather than as a
1903
+ style. Every one is drawn from a single shared chassis and the suite holds all
1904
+ three of its rules: one grid (`0 0 16 16` at `stroke-width` 1.5), no size of
1905
+ its own, no colour of its own. There is also a test that no two of them draw
1906
+ the same thing, because a set that ships one mark twice has a hole in it where
1907
+ a reader will look for a distinction.
1908
+
1909
+ Two pairs carry the weight of that. `ChannelReleasedIcon` and `ChannelBetaIcon`
1910
+ are **one ring told twice** — the same `r="5.5"` circle, filled solid and
1911
+ filled halfway — so nobody has to learn which of two unrelated pictures means
1912
+ "finished". And `StackIcon` is plates seen edge-on while `StacksMatrixIcon` is
1913
+ a table with a head rule and a name column, because the question the matrix
1914
+ answers has two axes and the question a stack answers has one.
1915
+
1916
+ ## Sized and inked by the caller, on purpose
1917
+
1918
+ No `size` prop, no `tone` prop. `className="size-icon-md"` reads
1919
+ `--cue-icon-md`, so a glyph tracks the density island it is in — an icon that
1920
+ measured itself would be the one thing in a compact subtree that did not move.
1921
+ Ink is `currentColor` throughout, so a glyph in a muted row is muted and the
1922
+ same glyph in the current row is not, with nothing to pass down.
1923
+
1924
+ They are decorative by default — `aria-hidden` — because they mark a row that
1925
+ already carries its name, and a second announcement of the same word is noise.
1926
+ Props spread last, so `aria-hidden={false} role="img"` with a `<title>` turns
1927
+ one back into a picture where a glyph really is the only name for something.
1928
+
1929
+ ## Why 16 and not 24
1930
+
1931
+ `lucide-react` draws on a 24 grid, and it remains the answer for everything
1932
+ outside this vocabulary — it is still an optional peer, every `icon` slot here
1933
+ still takes a component, and the two mix freely in one column. But `1.5` on a
1934
+ 24 grid renders at two-thirds the weight of `1.5` on a 16 grid in the same box,
1935
+ and every glyph this library already draws is on 16. Matching lucide's number
1936
+ would have meant not matching its own line, and the line is the thing a reader
1937
+ sees. An app that wants these to sit at exactly lucide's weight passes
1938
+ `strokeWidth` — it overrides, like every other attribute.
1939
+
1940
+ ## Ten exports rather than one `Icons` object
1941
+
1942
+ Deliberate, and it costs ten manifest entries instead of one with ten parts. A
1943
+ frozen namespace object does not tree-shake per member: a bundler cannot drop
1944
+ an unused property of an exported object literal, so a console that used two of
1945
+ these would have shipped all ten. Named exports of one module shake to exactly
1946
+ what is imported.
1947
+
1948
+ They ship in the root barrel with no subpath of their own, on the same ruling
1949
+ `brand/` records: the group pulls no optional peer and weighs two files.
1950
+ **Nothing here changes an existing component.** `packages/ui/manifest/**` is
1951
+ regenerated, not hand-edited.
1952
+
1953
+ - 85448d8: ## Three hairlines, derived from the foreground
1954
+
1955
+ A list is not a stack of boxes, and until now the token vocabulary had no way to
1956
+ say the difference.
1957
+
1958
+ `--cue-border` is an authored anchor: every preset picks one, and what every
1959
+ preset picks is a **box edge** — the line around a card, an input, a panel. It is
1960
+ the right weight for exactly that. Repeat it down thirty rows of a table and the
1961
+ page stops reading as a list and starts reading as thirty boxes stacked on each
1962
+ other. The line _between_ things wants to be lighter than the line _around_ a
1963
+ thing, and there was no token for it, so every surface that needed one either
1964
+ borrowed `--cue-border` and looked heavy, or invented a `color-mix()` of its own
1965
+ and stopped being themeable.
1966
+
1967
+ Three new tokens, in every preset, in both modes:
1968
+
1969
+ | Token | Value | What it is |
1970
+ | ------------------- | ------------------------------------------------------ | --------------------------------------------------------------------------------------------- |
1971
+ | `--cue-hair` | `color-mix(in oklch, var(--cue-fg) 6%, transparent)` | **Hairline rule** — between rows, between sections, under a caption |
1972
+ | `--cue-hair-strong` | `color-mix(in oklch, var(--cue-fg) 12%, transparent)` | **Strong hairline** — the same line where it carries weight: a table head, a quiet tag's edge |
1973
+ | `--cue-row-hover` | `color-mix(in oklch, var(--cue-fg) 3.5%, transparent)` | **Row hover wash** — the faintest fill that still reads as one row picked out of a list |
1974
+
1975
+ They are mixed from `--cue-fg`, not authored per preset, and that is the whole
1976
+ point of them. A share of the foreground keeps its relationship to the type it
1977
+ separates whatever the palette turned out to be — the rules lighten with the text
1978
+ in a light block, they hold at `terminal`'s cyan-on-black and at `luma`'s warm
1979
+ paper, and a preset added tomorrow gets all three for free without authoring a
1980
+ thing. Asking ten presets to hand-pick a hairline would have produced ten
1981
+ slightly different answers to a question nobody should have to answer.
1982
+
1983
+ They are emitted **inside** every theme block rather than once on `:root`, like
1984
+ every other derived colour here: a custom property's `var()` references are
1985
+ substituted at the element that declares it, so a `:root`-level mix would freeze
1986
+ the default preset's foreground into every nested theme.
1987
+
1988
+ ## Spending them
1989
+
1990
+ They are not Tailwind utilities, on the same ruling as `--cue-accent-soft`'s
1991
+ status siblings — arbitrary-property syntax reads the token directly:
1992
+
1993
+ ```tsx
1994
+ <div className="border-t border-(--cue-hair) hover:bg-(--cue-row-hover)" />
1995
+ ```
1996
+
1997
+ ## `--cue-row-hover` does not replace the surface ramp
1998
+
1999
+ Worth being exact, because there are now two answers to "how does a row show
2000
+ hover" and only one of them is new. A row that sits **on a surface** still lifts
2001
+ to the next rung — `hover:bg-surface-2` — and that is what this library's own
2002
+ rows do: `Sidebar.Item`, `Tree`, `Pagination`, `Toggle`, `NumberField`. None of
2003
+ them change, and none of them should.
2004
+
2005
+ `--cue-row-hover` is for rows drawn **directly on `--cue-bg`**, where the next
2006
+ rung of the ramp is a step you can see from across the room and the row you are
2007
+ pointing at ends up looking selected rather than hovered. If the ramp fits your
2008
+ row, use the ramp.
2009
+
2010
+ ## `createTheme()` derives them too — and now it cannot forget to
2011
+
2012
+ `@cueplusplus/ui` carries its own copy of the derivation table, because
2013
+ `createTheme()` runs in a browser over a palette somebody just typed while the
2014
+ token build runs at publish time over DTCG files under Style Dictionary, and
2015
+ neither package can import the other's. So a preset you generate gets the three
2016
+ hairlines exactly as a shipped preset does.
2017
+
2018
+ The copy was the real hazard here, and it was already sitting in the tree: **no
2019
+ test compared the two lists.** A derivation added to the build alone would have
2020
+ been missing from every theme a consumer generated, and the only symptom would
2021
+ have been an unstyled rule in somebody else's app weeks later. There is now a
2022
+ test that compares `DERIVED_TOKEN_TEMPLATES` against the compiled stylesheet in
2023
+ both directions, so a one-sided edit fails in this repository instead.
2024
+
2025
+ **Nothing changes for existing consumers.** Both packages gain tokens and neither
2026
+ loses or moves one; no component's rendered output differs.
2027
+
2028
+ - 0cf94cb: Chip takes a third variant, `tag` — a hairline and micro type, no fill at rest
2029
+
2030
+ A list row's name is usually followed by two or three flat facts about it: what
2031
+ kind of thing it is, which channel it came from, what scope it lives in, whether
2032
+ it needs an account before it will do anything. They are not states and nobody
2033
+ is going to click them. Until now the only shape this system offered for them
2034
+ was a `Chip`, and a chip is a container — a tone tint at low alpha inside a tone
2035
+ rim, sized for a status readout that has to be seen from across the room. Put
2036
+ three on a row and repeat the row thirty times and the list stops reading as a
2037
+ list. It reads as ninety small buttons, and the names they were annotating come
2038
+ second to them.
2039
+
2040
+ `variant="tag"` is the same recipe read quietly. No fill. A `--cue-hair-strong`
2041
+ rim instead of `--cue-border` — the token that landed for exactly this, the rule
2042
+ _between_ things rather than the edge _around_ one. Micro type, and the chip's
2043
+ `0.15em` tracking closed to `0.06em`, because tracking cut for label-size type
2044
+ holds a four-letter word at micro apart far enough to read as four letters.
2045
+
2046
+ ```tsx
2047
+ <Chip variant="tag">mcp</Chip>
2048
+ <Chip tone="accent" variant="tag">beta</Chip>
2049
+ <Chip tone="danger" variant="tag">needs auth</Chip>
2050
+ ```
2051
+
2052
+ ## What it is not
2053
+
2054
+ It is not a small chip. The height is still `h-chip` and the radius is still
2055
+ `--cue-radius-control`, so all three variants ride the density ladder together
2056
+ and a row that mixes a tag with a chip has one baseline rather than two. What
2057
+ changed is the volume, not the size — and that is the decision to make when
2058
+ choosing between them. A tag whose job is to be seen is a chip.
2059
+
2060
+ The six toned rims are unchanged at `/35` across all three variants, so a
2061
+ `danger` tag and a `danger` outline chip agree on what danger looks like, which
2062
+ is the whole point of a shared tone vocabulary. Only `neutral` moves, because
2063
+ the neutral line is the one that has two weights now.
2064
+
2065
+ An interactive tag brightens its rim on hover and never grows a fill. The
2066
+ existing tone hover rows are now keyed on the variant so that stays true; a
2067
+ `tinted` or `outline` chip hovers exactly as it did.
2068
+
2069
+ ## The invariant that made room for it
2070
+
2071
+ The recipe already refused to emit a fill and then cancel it — that is why the
2072
+ tinted fills live in `compoundVariants`. Adding a third variant extended the
2073
+ same rule to everything else that varies: the rim, the ink, the gutter, the
2074
+ tracking and the type size are each emitted from exactly one place now, so no
2075
+ call to `chipVariants` ever produces two utilities competing for one property.
2076
+
2077
+ That matters more than it looks. `chipVariants` is exported, and a caller who
2078
+ uses it without `cn` gets a raw class string — where the winner is decided by
2079
+ the order the rules happen to sit in the stylesheet, not by which utility was
2080
+ written last. `tailwind-merge` would not have saved that string either: it
2081
+ cannot see `h-chip h-4` as a conflict at all, because `chip` is a token name and
2082
+ not a scale step. So the base string gave up `px`, the type size and the
2083
+ tracking to the `variant` map, and `neutral` gave up its rim to a compound —
2084
+ `--cue-border` for the two chip variants, `--cue-hair-strong` for the tag. The
2085
+ suite holds the invariant over the whole 7 × 3 × 2 matrix.
2086
+
2087
+ **Nothing here changes an existing chip.** Every tinted and outline chip emits
2088
+ the same set of utilities it did before; only which line of the recipe they come
2089
+ from moved. `packages/ui/manifest/**` is regenerated, not hand-edited.
2090
+
2091
+ - 2a21229: AppShell rails its navigation column, and the header cell over it moves with it
2092
+
2093
+ A console's navigation is set once and then wanted out of the way, and the shape
2094
+ that answers that is an icon rail: the column narrows to a strip of glyphs and
2095
+ the work area takes the 10rem back. `Sidebar` has been able to do the narrowing
2096
+ since it shipped — controlled `collapsed`, `data-collapsed`, an `sr-only` label
2097
+ so an icon-only row keeps its accessible name, a width transition that switches
2098
+ off under `prefers-reduced-motion`. What it could not do was tell anything
2099
+ outside itself, and the thing that needs telling is the cell in the bar directly
2100
+ above it.
2101
+
2102
+ That cell did not exist here at all. `TitleBar` and `AppBar` are full-width
2103
+ strips with leading, centre and trailing slots; neither knows the column's
2104
+ width, and a console with an app mark over its navigation has been hand-rolling
2105
+ one. Hand-rolling it is where the defect lives: the cell is a child of the bar
2106
+ and the column is a child of the body, two different branches of the tree, and
2107
+ two boxes that are meant to read as one column will drift the first time one of
2108
+ them changes width. A rail changes it on every click.
2109
+
2110
+ ## One length, written once
2111
+
2112
+ `AppShell.Root` now writes `--cue-nav-width` on the frame — `navWidth`
2113
+ (`"14rem"`) while expanded, `railWidth` (`"3.5rem"`) while railed — and both
2114
+ boxes read it. Neither holds a width of its own, so railing rewrites one length
2115
+ and the cell and the column arrive together rather than a frame apart. It is
2116
+ also the handle a consumer's own CSS aligns to: a sticky rule, a toolbar offset,
2117
+ a drop shadow that should stop where the column does.
2118
+
2119
+ `3.5rem` is a `--cue-icon-md` glyph centred in a `--cue-control-md` row with a
2120
+ gutter either side that still reads as a column rather than a strip. It is half
2121
+ a rem wider than the `3rem` `Sidebar.Root` collapses to on its own, which is the
2122
+ one visible change to an existing console: a shell whose column is collapsed
2123
+ from its hairline rail is now 8px wider there. Set `railWidth="3rem"` on
2124
+ `AppShell.Root` to keep the old measure exactly.
2125
+
2126
+ ## The new surface
2127
+ - **`AppShell.Root`** takes `rail` / `defaultRail` / `onRailChange` — the same
2128
+ controlled-prop bargain `navOpen` already makes — plus `navWidth` and
2129
+ `railWidth`. It stamps `data-rail` on itself and declares `group/app-shell`
2130
+ beside it, so anything in the frame can answer in plain Tailwind:
2131
+ `group-data-[rail]/app-shell:hidden` on a wordmark, the way `Sidebar` already
2132
+ publishes `data-collapsed` for its own parts.
2133
+ - **`AppShell.Bar`** is the bar divided by the navigation column: a `brand` slot
2134
+ exactly the column's width, wearing the column's rim, and the rest of the bar
2135
+ beside it. Omit `brand` and it is one undivided strip — an empty bordered cell
2136
+ over the column is worse than no cell. It stays a `<div>`, like `TitleBar`
2137
+ beside it, so a frame that also carries an `AppBar` does not end up with two
2138
+ `banner` landmarks; a page that wants one says `role="banner"`.
2139
+ - **`AppShell.Sidebar`** hands the frame's rail down to `Sidebar.Root` as its
2140
+ controlled `collapsed` and writes it back whenever the column's own hairline
2141
+ rail is pressed, so the two states cannot disagree and there is nothing to
2142
+ keep in sync.
2143
+
2144
+ The rail is off by default and every part of this is additive. A `collapsed` or
2145
+ `defaultCollapsed` passed to `AppShell.Sidebar` is the pre-rail spelling and
2146
+ still means exactly what it did — the column keeps its own state and its own two
2147
+ lengths, and the frame stays out of it rather than half-driving a column it
2148
+ could not have seeded, because a child cannot seed its parent's state. Reach for
2149
+ `defaultRail` on the frame when both boxes should move. A `width` passed to
2150
+ `AppShell.Sidebar` likewise still wins, for the column alone, which is a
2151
+ misalignment worth knowing about before writing it.
2152
+
2153
+ ## What is deliberately not here
2154
+
2155
+ **The toggle.** `Sidebar.Rail` — the 4px hit strip the shell already draws on
2156
+ the column's edge — rails the frame now, and any other control is one you render
2157
+ and wire, because where a rail button lives is a decision about your bar. The
2158
+ guidance ships the pattern: an `IconButton` with `aria-pressed` rather than
2159
+ `aria-expanded` (the column is still there either way, so it is a two-state
2160
+ toggle and not a disclosure) and `aria-controls` pointed at the `<nav>`.
2161
+
2162
+ **Persistence.** A rail is set once and expected to be remembered, and _where_
2163
+ it is remembered is a decision about hydration and privacy that a layout
2164
+ component has no business making. Read it from a cookie on the server and the
2165
+ first painted frame is already railed; read it from `localStorage` in an effect
2166
+ and the column jumps once after hydration. That is the whole reason the choice
2167
+ is yours and not the shell's.
2168
+
2169
+ The `md` breakpoint now wears a third face — the header cell stops measuring
2170
+ itself from a column that below 48rem is not in the layout at all, which keeps
2171
+ 14rem of a 390px bar from being walled off for nothing. It is the same
2172
+ navigation-shape decision the column and the drawer trigger already make, and
2173
+ the library's breakpoint budget is still that one decision and nothing else.
2174
+
2175
+ ### Patch Changes
2176
+
2177
+ - ad9a0e9: ## One space in a `TimeField` was throwing away the server render
2178
+
2179
+ A segmented date or time field prints text that `Intl.DateTimeFormat` produced,
2180
+ and `Intl` answers from the ICU data of whatever is running it. Today, `en-US` at
2181
+ half past seven in the evening is `7:30 PM` from Node 24 (CLDR 48) and
2182
+ `7:30 PM` from Chrome 151 — a narrow no-break space against an ordinary one.
2183
+ Identical on screen. Not identical to React.
2184
+
2185
+ A hydration mismatch is not a warning. React discards the server's HTML and
2186
+ re-renders the whole document on the client, and that re-mounts `<html>` — which
2187
+ React treats as a _singleton_ and therefore strips every attribute off before
2188
+ re-applying the ones the server rendered. `data-theme`, `data-density`,
2189
+ `data-font`, `data-mode` and `color-scheme` are not among them: they are written
2190
+ by `prepaintScript()`, client-side, before React exists. So the document spends
2191
+ the whole client render wearing bare `:root` — the library's default preset, no
2192
+ density, no pairing — and then snaps back.
2193
+
2194
+ Measured on this design system's own kitchen sink, in headless Chrome with the
2195
+ cache disabled: **208 ms** with no axis on the document, 6,423 elements resolving
2196
+ to a monospace nobody had selected, and **40 KB** of webfont fetched for it that
2197
+ then painted zero glyphs — on _every_ pairing, including the two that download
2198
+ nothing at all.
2199
+
2200
+ The literal separators now carry their text in an element of their own, marked
2201
+ `suppressHydrationWarning`. React only honours that flag on the element directly
2202
+ containing the differing text, and React Aria's `filterDOMProps` drops it before
2203
+ the segment's own element sees it, which is why the extra element exists. Only
2204
+ literal segments are wrapped — they are `aria-hidden` and not focusable. The
2205
+ value segments React Aria makes `contentEditable` are untouched, and digits and
2206
+ `AM`/`PM` are spelled the same by every ICU there has been.
2207
+
2208
+ **If you style literal separators**, `[data-type="literal"]` still selects them
2209
+ and still carries the text; a child-element selector under one now finds a
2210
+ `<span>`.
2211
+
2212
+ ## `ThemeProvider` stops waiting a commit to dress the document
2213
+
2214
+ Related, and found on the way. The `<html>` stamp used to wait for the provider's
2215
+ persisted preferences to be restored — a commit later — because on the first one
2216
+ its state is still the props, and writing those would put the app's defaults on a
2217
+ document the visitor had already chosen for. Waiting closed that hole by leaving
2218
+ another one: a subtree could be measured before the stamp landed.
2219
+
2220
+ It now writes on the first commit, from the same storage `prepaintScript()` read,
2221
+ and it writes in an _insertion_ effect, which React runs before any layout effect
2222
+ in the tree — so nothing measures the document before it is dressed. A value that
2223
+ is already correct is not rewritten, so the usual case is no DOM mutation at all.
2224
+
2225
+ No API moved. An app that ships the pre-paint script and the provider together,
2226
+ as the getting-started page has always said to, sees only that the first frame is
2227
+ the right one more often.
2228
+
2229
+ - Updated dependencies [85448d8]
2230
+ - @cueplusplus/tokens@0.4.0
2231
+
3
2232
  ## 0.3.0
4
2233
 
5
2234
  ### Minor Changes