@agent-native/toolkit 0.228.0-nightly-20261111212500 → 0.228.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 (361) hide show
  1. package/agent-native.eject.json +2 -0
  2. package/dist/app/agentkit/react/approval-card.d.ts.map +1 -1
  3. package/dist/app/agentkit/react/approval-card.js +5 -3
  4. package/dist/app/agentkit/react/approval-card.js.map +1 -1
  5. package/dist/app/agentkit/react/code-block.d.ts +32 -0
  6. package/dist/app/agentkit/react/code-block.d.ts.map +1 -0
  7. package/dist/app/agentkit/react/code-block.js +125 -0
  8. package/dist/app/agentkit/react/code-block.js.map +1 -0
  9. package/dist/app/agentkit/react/components.d.ts +33 -14
  10. package/dist/app/agentkit/react/components.d.ts.map +1 -1
  11. package/dist/app/agentkit/react/components.js +843 -168
  12. package/dist/app/agentkit/react/components.js.map +1 -1
  13. package/dist/app/agentkit/react/context-usage.d.ts +45 -0
  14. package/dist/app/agentkit/react/context-usage.d.ts.map +1 -0
  15. package/dist/app/agentkit/react/context-usage.js +93 -0
  16. package/dist/app/agentkit/react/context-usage.js.map +1 -0
  17. package/dist/app/agentkit/react/context.d.ts +42 -3
  18. package/dist/app/agentkit/react/context.d.ts.map +1 -1
  19. package/dist/app/agentkit/react/context.js +46 -1
  20. package/dist/app/agentkit/react/context.js.map +1 -1
  21. package/dist/app/agentkit/react/disclosure-content.d.ts +12 -0
  22. package/dist/app/agentkit/react/disclosure-content.d.ts.map +1 -0
  23. package/dist/app/agentkit/react/disclosure-content.js +33 -0
  24. package/dist/app/agentkit/react/disclosure-content.js.map +1 -0
  25. package/dist/app/agentkit/react/duration.d.ts +6 -0
  26. package/dist/app/agentkit/react/duration.d.ts.map +1 -0
  27. package/dist/app/agentkit/react/duration.js +21 -0
  28. package/dist/app/agentkit/react/duration.js.map +1 -0
  29. package/dist/app/agentkit/react/file-change.d.ts +33 -0
  30. package/dist/app/agentkit/react/file-change.d.ts.map +1 -0
  31. package/dist/app/agentkit/react/file-change.js +83 -0
  32. package/dist/app/agentkit/react/file-change.js.map +1 -0
  33. package/dist/app/agentkit/react/file-diff.d.ts +17 -0
  34. package/dist/app/agentkit/react/file-diff.d.ts.map +1 -0
  35. package/dist/app/agentkit/react/file-diff.js +94 -0
  36. package/dist/app/agentkit/react/file-diff.js.map +1 -0
  37. package/dist/app/agentkit/react/index.d.ts +7 -0
  38. package/dist/app/agentkit/react/index.d.ts.map +1 -1
  39. package/dist/app/agentkit/react/index.js +7 -0
  40. package/dist/app/agentkit/react/index.js.map +1 -1
  41. package/dist/app/agentkit/react/question-card.d.ts +67 -0
  42. package/dist/app/agentkit/react/question-card.d.ts.map +1 -0
  43. package/dist/app/agentkit/react/question-card.js +198 -0
  44. package/dist/app/agentkit/react/question-card.js.map +1 -0
  45. package/dist/app/agentkit/react/root.d.ts +3 -2
  46. package/dist/app/agentkit/react/root.d.ts.map +1 -1
  47. package/dist/app/agentkit/react/root.js +2 -2
  48. package/dist/app/agentkit/react/root.js.map +1 -1
  49. package/dist/app/agentkit/react/roving-focus.d.ts +4 -0
  50. package/dist/app/agentkit/react/roving-focus.d.ts.map +1 -0
  51. package/dist/app/agentkit/react/roving-focus.js +75 -0
  52. package/dist/app/agentkit/react/roving-focus.js.map +1 -0
  53. package/dist/app/agentkit/react/stack-trace.d.ts +34 -0
  54. package/dist/app/agentkit/react/stack-trace.d.ts.map +1 -0
  55. package/dist/app/agentkit/react/stack-trace.js +121 -0
  56. package/dist/app/agentkit/react/stack-trace.js.map +1 -0
  57. package/dist/app/agentkit/react/stop-button.d.ts.map +1 -1
  58. package/dist/app/agentkit/react/stop-button.js +22 -6
  59. package/dist/app/agentkit/react/stop-button.js.map +1 -1
  60. package/dist/app/agentkit/react/styles.css +1306 -395
  61. package/dist/app/agentkit/react/terminal.d.ts +28 -0
  62. package/dist/app/agentkit/react/terminal.d.ts.map +1 -0
  63. package/dist/app/agentkit/react/terminal.js +84 -0
  64. package/dist/app/agentkit/react/terminal.js.map +1 -0
  65. package/dist/app/agentkit/react/trace.d.ts +31 -0
  66. package/dist/app/agentkit/react/trace.d.ts.map +1 -0
  67. package/dist/app/agentkit/react/trace.js +73 -0
  68. package/dist/app/agentkit/react/trace.js.map +1 -0
  69. package/dist/app/blocks/library/HighlightedCode.d.ts +2 -1
  70. package/dist/app/blocks/library/HighlightedCode.d.ts.map +1 -1
  71. package/dist/app/blocks/library/HighlightedCode.js +8 -111
  72. package/dist/app/blocks/library/HighlightedCode.js.map +1 -1
  73. package/dist/app/chat/AgentKitAssistantChat.d.ts.map +1 -1
  74. package/dist/app/chat/AgentKitAssistantChat.js +23 -245
  75. package/dist/app/chat/AgentKitAssistantChat.js.map +1 -1
  76. package/dist/app/chat/agentkit-chat/guided-questions.d.ts +15 -1
  77. package/dist/app/chat/agentkit-chat/guided-questions.d.ts.map +1 -1
  78. package/dist/app/chat/agentkit-chat/guided-questions.js +142 -2
  79. package/dist/app/chat/agentkit-chat/guided-questions.js.map +1 -1
  80. package/dist/app/chat/agentkit-chat/history.js +1 -1
  81. package/dist/app/chat/agentkit-chat/history.js.map +1 -1
  82. package/dist/app/chat/agentkit-chat/index.d.ts +1 -1
  83. package/dist/app/chat/agentkit-chat/index.d.ts.map +1 -1
  84. package/dist/app/chat/agentkit-chat/index.js +1 -1
  85. package/dist/app/chat/agentkit-chat/index.js.map +1 -1
  86. package/dist/app/chat/agentkit-chat/labels.d.ts +5 -0
  87. package/dist/app/chat/agentkit-chat/labels.d.ts.map +1 -0
  88. package/dist/app/chat/agentkit-chat/labels.js +245 -0
  89. package/dist/app/chat/agentkit-chat/labels.js.map +1 -0
  90. package/dist/app/chat/agentkit-chat/questions.d.ts +1 -1
  91. package/dist/app/chat/agentkit-chat/questions.d.ts.map +1 -1
  92. package/dist/app/chat/agentkit-chat/questions.js +1 -1
  93. package/dist/app/chat/agentkit-chat/questions.js.map +1 -1
  94. package/dist/app/chat/agentkit-chat/root.d.ts.map +1 -1
  95. package/dist/app/chat/agentkit-chat/root.js +5 -64
  96. package/dist/app/chat/agentkit-chat/root.js.map +1 -1
  97. package/dist/app/chat/agentkit-chat/work-step-labels.d.ts +2 -0
  98. package/dist/app/chat/agentkit-chat/work-step-labels.d.ts.map +1 -1
  99. package/dist/app/chat/agentkit-chat/work-step-labels.js +31 -0
  100. package/dist/app/chat/agentkit-chat/work-step-labels.js.map +1 -1
  101. package/dist/app/chat/chat/markdown-renderer.d.ts +2 -12
  102. package/dist/app/chat/chat/markdown-renderer.d.ts.map +1 -1
  103. package/dist/app/chat/chat/markdown-renderer.js +2 -41
  104. package/dist/app/chat/chat/markdown-renderer.js.map +1 -1
  105. package/dist/app/chat/chat/tool-call-display.d.ts.map +1 -1
  106. package/dist/app/chat/chat/tool-call-display.js +2 -2
  107. package/dist/app/chat/chat/tool-call-display.js.map +1 -1
  108. package/dist/app/chat/conversation/AgentConversation.d.ts.map +1 -1
  109. package/dist/app/chat/conversation/AgentConversation.js +2 -37
  110. package/dist/app/chat/conversation/AgentConversation.js.map +1 -1
  111. package/dist/app/chat/tool-cells/BashCell.d.ts +3 -1
  112. package/dist/app/chat/tool-cells/BashCell.d.ts.map +1 -1
  113. package/dist/app/chat/tool-cells/BashCell.js +4 -40
  114. package/dist/app/chat/tool-cells/BashCell.js.map +1 -1
  115. package/dist/app/chat/tool-cells/EditCell.d.ts.map +1 -1
  116. package/dist/app/chat/tool-cells/EditCell.js +5 -108
  117. package/dist/app/chat/tool-cells/EditCell.js.map +1 -1
  118. package/dist/app/chat/tool-cells/FilesChangedSummary.d.ts.map +1 -1
  119. package/dist/app/chat/tool-cells/FilesChangedSummary.js +23 -81
  120. package/dist/app/chat/tool-cells/FilesChangedSummary.js.map +1 -1
  121. package/dist/app/chat/tool-cells/WriteCell.d.ts.map +1 -1
  122. package/dist/app/chat/tool-cells/WriteCell.js +5 -24
  123. package/dist/app/chat/tool-cells/WriteCell.js.map +1 -1
  124. package/dist/app/code-highlighter.d.ts +11 -0
  125. package/dist/app/code-highlighter.d.ts.map +1 -0
  126. package/dist/app/code-highlighter.js +122 -0
  127. package/dist/app/code-highlighter.js.map +1 -0
  128. package/dist/app/context-xray/ThreadContextRing.js +3 -3
  129. package/dist/app/context-xray/ThreadContextRing.js.map +1 -1
  130. package/dist/app/i18n/catalogs/ar-SA.d.ts.map +1 -1
  131. package/dist/app/i18n/catalogs/ar-SA.js +22 -0
  132. package/dist/app/i18n/catalogs/ar-SA.js.map +1 -1
  133. package/dist/app/i18n/catalogs/de-DE.d.ts.map +1 -1
  134. package/dist/app/i18n/catalogs/de-DE.js +22 -0
  135. package/dist/app/i18n/catalogs/de-DE.js.map +1 -1
  136. package/dist/app/i18n/catalogs/en-US.d.ts +22 -0
  137. package/dist/app/i18n/catalogs/en-US.d.ts.map +1 -1
  138. package/dist/app/i18n/catalogs/en-US.js +22 -0
  139. package/dist/app/i18n/catalogs/en-US.js.map +1 -1
  140. package/dist/app/i18n/catalogs/es-ES.d.ts.map +1 -1
  141. package/dist/app/i18n/catalogs/es-ES.js +22 -0
  142. package/dist/app/i18n/catalogs/es-ES.js.map +1 -1
  143. package/dist/app/i18n/catalogs/fr-FR.d.ts.map +1 -1
  144. package/dist/app/i18n/catalogs/fr-FR.js +22 -0
  145. package/dist/app/i18n/catalogs/fr-FR.js.map +1 -1
  146. package/dist/app/i18n/catalogs/hi-IN.d.ts.map +1 -1
  147. package/dist/app/i18n/catalogs/hi-IN.js +22 -0
  148. package/dist/app/i18n/catalogs/hi-IN.js.map +1 -1
  149. package/dist/app/i18n/catalogs/ja-JP.d.ts.map +1 -1
  150. package/dist/app/i18n/catalogs/ja-JP.js +22 -0
  151. package/dist/app/i18n/catalogs/ja-JP.js.map +1 -1
  152. package/dist/app/i18n/catalogs/ko-KR.d.ts.map +1 -1
  153. package/dist/app/i18n/catalogs/ko-KR.js +22 -0
  154. package/dist/app/i18n/catalogs/ko-KR.js.map +1 -1
  155. package/dist/app/i18n/catalogs/pt-BR.d.ts.map +1 -1
  156. package/dist/app/i18n/catalogs/pt-BR.js +22 -0
  157. package/dist/app/i18n/catalogs/pt-BR.js.map +1 -1
  158. package/dist/app/i18n/catalogs/zh-CN.d.ts.map +1 -1
  159. package/dist/app/i18n/catalogs/zh-CN.js +22 -0
  160. package/dist/app/i18n/catalogs/zh-CN.js.map +1 -1
  161. package/dist/app/i18n/catalogs/zh-TW.d.ts.map +1 -1
  162. package/dist/app/i18n/catalogs/zh-TW.js +22 -0
  163. package/dist/app/i18n/catalogs/zh-TW.js.map +1 -1
  164. package/dist/app/onboarding/first-run-first-paint.d.ts +10 -0
  165. package/dist/app/onboarding/first-run-first-paint.d.ts.map +1 -0
  166. package/dist/app/onboarding/first-run-first-paint.js +22 -0
  167. package/dist/app/onboarding/first-run-first-paint.js.map +1 -0
  168. package/dist/app/onboarding/first-run-startup-gate.d.ts +3 -1
  169. package/dist/app/onboarding/first-run-startup-gate.d.ts.map +1 -1
  170. package/dist/app/onboarding/first-run-startup-gate.js +30 -6
  171. package/dist/app/onboarding/first-run-startup-gate.js.map +1 -1
  172. package/dist/app/providers/AppProviders.d.ts.map +1 -1
  173. package/dist/app/providers/AppProviders.js +9 -2
  174. package/dist/app/providers/AppProviders.js.map +1 -1
  175. package/dist/app/providers/BuilderSignupActivation.d.ts +2 -1
  176. package/dist/app/providers/BuilderSignupActivation.d.ts.map +1 -1
  177. package/dist/app/providers/BuilderSignupActivation.js +6 -3
  178. package/dist/app/providers/BuilderSignupActivation.js.map +1 -1
  179. package/dist/app/shared/AppShellSkeleton.d.ts.map +1 -1
  180. package/dist/app/shared/AppShellSkeleton.js +11 -1
  181. package/dist/app/shared/AppShellSkeleton.js.map +1 -1
  182. package/dist/app/styles/agent-native.css +2 -2
  183. package/dist/app/styles/tokens/agent-kit.css +20 -3
  184. package/dist/composer/AgentSuggestionBar.d.ts.map +1 -1
  185. package/dist/composer/AgentSuggestionBar.js +2 -2
  186. package/dist/composer/AgentSuggestionBar.js.map +1 -1
  187. package/dist/composer/ComposerContextMenu.js +4 -4
  188. package/dist/composer/ComposerContextMenu.js.map +1 -1
  189. package/dist/composer/ComposerContextPicker.d.ts.map +1 -1
  190. package/dist/composer/ComposerContextPicker.js +2 -2
  191. package/dist/composer/ComposerContextPicker.js.map +1 -1
  192. package/dist/composer/ComposerContextPickerDialog.d.ts.map +1 -1
  193. package/dist/composer/ComposerContextPickerDialog.js +2 -2
  194. package/dist/composer/ComposerContextPickerDialog.js.map +1 -1
  195. package/dist/composer/ComposerModePicker.d.ts.map +1 -1
  196. package/dist/composer/ComposerModePicker.js +2 -1
  197. package/dist/composer/ComposerModePicker.js.map +1 -1
  198. package/dist/composer/ComposerPlusMenu.js +1 -1
  199. package/dist/composer/ComposerPlusMenu.js.map +1 -1
  200. package/dist/composer/MentionChip.js +1 -1
  201. package/dist/composer/MentionChip.js.map +1 -1
  202. package/dist/composer/MentionPopover.d.ts +3 -0
  203. package/dist/composer/MentionPopover.d.ts.map +1 -1
  204. package/dist/composer/MentionPopover.js +34 -15
  205. package/dist/composer/MentionPopover.js.map +1 -1
  206. package/dist/composer/MessageQueueDrawer.d.ts.map +1 -1
  207. package/dist/composer/MessageQueueDrawer.js +14 -9
  208. package/dist/composer/MessageQueueDrawer.js.map +1 -1
  209. package/dist/composer/PastedTextChip.d.ts.map +1 -1
  210. package/dist/composer/PastedTextChip.js +14 -18
  211. package/dist/composer/PastedTextChip.js.map +1 -1
  212. package/dist/composer/PromptBar.js +1 -1
  213. package/dist/composer/PromptBar.js.map +1 -1
  214. package/dist/composer/PromptComposer.d.ts.map +1 -1
  215. package/dist/composer/PromptComposer.js +11 -9
  216. package/dist/composer/PromptComposer.js.map +1 -1
  217. package/dist/composer/RealtimeVoiceMode.js +2 -2
  218. package/dist/composer/RealtimeVoiceMode.js.map +1 -1
  219. package/dist/composer/TiptapComposer.d.ts.map +1 -1
  220. package/dist/composer/TiptapComposer.js +52 -89
  221. package/dist/composer/TiptapComposer.js.map +1 -1
  222. package/dist/composer/VoiceButton.d.ts +3 -1
  223. package/dist/composer/VoiceButton.d.ts.map +1 -1
  224. package/dist/composer/VoiceButton.js +10 -8
  225. package/dist/composer/VoiceButton.js.map +1 -1
  226. package/dist/composer/composer-notice.js +3 -3
  227. package/dist/composer/composer-notice.js.map +1 -1
  228. package/dist/composer/extensions/FileReference.js +1 -1
  229. package/dist/composer/extensions/FileReference.js.map +1 -1
  230. package/dist/composer/extensions/SkillReference.js +1 -1
  231. package/dist/composer/extensions/SkillReference.js.map +1 -1
  232. package/dist/composer/popover-surface.d.ts +1 -1
  233. package/dist/composer/popover-surface.d.ts.map +1 -1
  234. package/dist/composer/popover-surface.js +1 -1
  235. package/dist/composer/popover-surface.js.map +1 -1
  236. package/dist/context-ui/ContextUsageRing.d.ts +1 -45
  237. package/dist/context-ui/ContextUsageRing.d.ts.map +1 -1
  238. package/dist/context-ui/ContextUsageRing.js +2 -87
  239. package/dist/context-ui/ContextUsageRing.js.map +1 -1
  240. package/dist/context-ui/index.d.ts +2 -2
  241. package/dist/context-ui/index.d.ts.map +1 -1
  242. package/dist/context-ui/index.js +2 -2
  243. package/dist/context-ui/index.js.map +1 -1
  244. package/package.json +4 -4
  245. package/src/app/agentkit/react/approval-card.tsx +14 -2
  246. package/src/app/agentkit/react/code-block.spec.tsx +154 -0
  247. package/src/app/agentkit/react/code-block.tsx +317 -0
  248. package/src/app/agentkit/react/components.interaction.spec.tsx +7 -0
  249. package/src/app/agentkit/react/components.papercuts.spec.tsx +143 -2
  250. package/src/app/agentkit/react/components.spec.tsx +40 -47
  251. package/src/app/agentkit/react/components.tsx +1533 -413
  252. package/src/app/agentkit/react/composer-context.spec.tsx +35 -0
  253. package/src/app/agentkit/react/context-usage.tsx +353 -0
  254. package/src/app/agentkit/react/context.tsx +93 -1
  255. package/src/app/agentkit/react/disclosure-content.ts +42 -0
  256. package/src/app/agentkit/react/disclosures.spec.tsx +62 -4
  257. package/src/app/agentkit/react/duration.ts +22 -0
  258. package/src/app/agentkit/react/file-change.spec.tsx +124 -0
  259. package/src/app/agentkit/react/file-change.tsx +228 -0
  260. package/src/app/agentkit/react/file-diff.ts +110 -0
  261. package/src/app/agentkit/react/index.ts +7 -0
  262. package/src/app/agentkit/react/lifecycle.spec.tsx +565 -20
  263. package/src/app/agentkit/react/markdown.spec.tsx +18 -8
  264. package/src/app/agentkit/react/message-actions.spec.tsx +55 -1
  265. package/src/app/agentkit/react/notices.spec.tsx +4 -4
  266. package/src/app/agentkit/react/optimistic-send.spec.tsx +43 -0
  267. package/src/app/agentkit/react/question-card.spec.tsx +276 -0
  268. package/src/app/agentkit/react/question-card.tsx +533 -0
  269. package/src/app/agentkit/react/root.tsx +4 -0
  270. package/src/app/agentkit/react/roving-focus.spec.tsx +114 -0
  271. package/src/app/agentkit/react/roving-focus.ts +82 -0
  272. package/src/app/agentkit/react/stack-trace.spec.tsx +177 -0
  273. package/src/app/agentkit/react/stack-trace.tsx +281 -0
  274. package/src/app/agentkit/react/stop-button.spec.tsx +40 -0
  275. package/src/app/agentkit/react/stop-button.tsx +22 -5
  276. package/src/app/agentkit/react/styles.css +1306 -395
  277. package/src/app/agentkit/react/subagent-step.spec.tsx +554 -0
  278. package/src/app/agentkit/react/terminal.spec.tsx +228 -0
  279. package/src/app/agentkit/react/terminal.tsx +228 -0
  280. package/src/app/agentkit/react/trace.spec.tsx +120 -0
  281. package/src/app/agentkit/react/trace.tsx +239 -0
  282. package/src/app/blocks/library/HighlightedCode.tsx +14 -126
  283. package/src/app/chat/AgentKitAssistantChat.spec.tsx +28 -12
  284. package/src/app/chat/AgentKitAssistantChat.tsx +27 -253
  285. package/src/app/chat/agentkit-chat/approval-card.spec.tsx +1 -0
  286. package/src/app/chat/agentkit-chat/guided-questions.flow.spec.tsx +381 -0
  287. package/src/app/chat/agentkit-chat/guided-questions.tsx +236 -3
  288. package/src/app/chat/agentkit-chat/history.tsx +1 -1
  289. package/src/app/chat/agentkit-chat/index.ts +2 -0
  290. package/src/app/chat/agentkit-chat/labels.ts +259 -0
  291. package/src/app/chat/agentkit-chat/questions.ts +2 -0
  292. package/src/app/chat/agentkit-chat/root.spec.tsx +18 -0
  293. package/src/app/chat/agentkit-chat/root.tsx +5 -64
  294. package/src/app/chat/agentkit-chat/work-step-labels.ts +42 -0
  295. package/src/app/chat/chat/markdown-renderer.tsx +2 -59
  296. package/src/app/chat/chat/tool-call-display.spec.tsx +26 -14
  297. package/src/app/chat/chat/tool-call-display.tsx +2 -1
  298. package/src/app/chat/conversation/AgentConversation.tsx +2 -50
  299. package/src/app/chat/tool-cells/BashCell.tsx +14 -136
  300. package/src/app/chat/tool-cells/EditCell.tsx +11 -248
  301. package/src/app/chat/tool-cells/FilesChangedSummary.spec.tsx +62 -5
  302. package/src/app/chat/tool-cells/FilesChangedSummary.tsx +31 -162
  303. package/src/app/chat/tool-cells/WriteCell.tsx +11 -121
  304. package/src/app/code-highlighter.ts +131 -0
  305. package/src/app/context-xray/ThreadContextRing.tsx +7 -7
  306. package/src/app/i18n/catalogs/ar-SA.ts +22 -0
  307. package/src/app/i18n/catalogs/de-DE.ts +22 -0
  308. package/src/app/i18n/catalogs/en-US.ts +22 -0
  309. package/src/app/i18n/catalogs/es-ES.ts +22 -0
  310. package/src/app/i18n/catalogs/fr-FR.ts +22 -0
  311. package/src/app/i18n/catalogs/hi-IN.ts +22 -0
  312. package/src/app/i18n/catalogs/ja-JP.ts +22 -0
  313. package/src/app/i18n/catalogs/ko-KR.ts +22 -0
  314. package/src/app/i18n/catalogs/pt-BR.ts +22 -0
  315. package/src/app/i18n/catalogs/zh-CN.ts +22 -0
  316. package/src/app/i18n/catalogs/zh-TW.ts +22 -0
  317. package/src/app/onboarding/first-run-first-paint.spec.ts +39 -0
  318. package/src/app/onboarding/first-run-first-paint.ts +24 -0
  319. package/src/app/onboarding/first-run-startup-gate.spec.tsx +125 -13
  320. package/src/app/onboarding/first-run-startup-gate.tsx +48 -5
  321. package/src/app/providers/AppProviders.tsx +18 -0
  322. package/src/app/providers/BuilderSignupActivation.tsx +8 -2
  323. package/src/app/shared/AppShellSkeleton.spec.tsx +60 -1
  324. package/src/app/shared/AppShellSkeleton.tsx +31 -1
  325. package/src/app/styles/agent-native.css +2 -2
  326. package/src/app/styles/agent-native.spec.ts +2 -2
  327. package/src/app/styles/tokens/agent-kit.css +20 -3
  328. package/src/composer/AgentSuggestionBar.spec.tsx +3 -1
  329. package/src/composer/AgentSuggestionBar.tsx +5 -2
  330. package/src/composer/ComposerContextMenu.tsx +4 -4
  331. package/src/composer/ComposerContextPicker.tsx +8 -2
  332. package/src/composer/ComposerContextPickerDialog.tsx +5 -2
  333. package/src/composer/ComposerModePicker.tsx +2 -1
  334. package/src/composer/ComposerPlusMenu.tsx +1 -1
  335. package/src/composer/MentionChip.tsx +1 -1
  336. package/src/composer/MentionPopover.spec.tsx +108 -2
  337. package/src/composer/MentionPopover.tsx +93 -17
  338. package/src/composer/MessageQueueDrawer.spec.tsx +5 -5
  339. package/src/composer/MessageQueueDrawer.tsx +19 -12
  340. package/src/composer/PastedTextChip.spec.tsx +36 -0
  341. package/src/composer/PastedTextChip.tsx +52 -58
  342. package/src/composer/PromptBar.tsx +1 -1
  343. package/src/composer/PromptComposer.context.spec.tsx +2 -2
  344. package/src/composer/PromptComposer.submission.spec.tsx +27 -0
  345. package/src/composer/PromptComposer.tsx +17 -9
  346. package/src/composer/RealtimeVoiceMode.tsx +2 -2
  347. package/src/composer/TiptapComposer.model-picker.spec.tsx +23 -82
  348. package/src/composer/TiptapComposer.tsx +68 -145
  349. package/src/composer/VoiceButton.tsx +12 -7
  350. package/src/composer/composer-notice.tsx +3 -3
  351. package/src/composer/extensions/FileReference.tsx +1 -1
  352. package/src/composer/extensions/SkillReference.tsx +1 -1
  353. package/src/composer/popover-surface.ts +1 -1
  354. package/src/context-ui/ContextUsageRing.spec.tsx +33 -8
  355. package/src/context-ui/ContextUsageRing.tsx +13 -348
  356. package/src/context-ui/index.ts +7 -1
  357. package/dist/composer/ModelPickerParts.d.ts +0 -20
  358. package/dist/composer/ModelPickerParts.d.ts.map +0 -1
  359. package/dist/composer/ModelPickerParts.js +0 -77
  360. package/dist/composer/ModelPickerParts.js.map +0 -1
  361. package/src/composer/ModelPickerParts.tsx +0 -188
@@ -0,0 +1,533 @@
1
+ import {
2
+ IconHelpCircle,
3
+ IconPaperclip,
4
+ IconPencil,
5
+ IconX,
6
+ } from "@tabler/icons-react";
7
+ import {
8
+ useEffect,
9
+ useId,
10
+ useRef,
11
+ useState,
12
+ type DragEvent,
13
+ type KeyboardEvent,
14
+ type ReactNode,
15
+ } from "react";
16
+
17
+ import { ActionButton, IconButton } from "../../../agentkit.js";
18
+ import { useAgentKitLabels } from "./context.js";
19
+
20
+ const SESSION_REPLAY_MASK_PROPS = { "data-an-mask": "" } as const;
21
+ /** Matches the composer toolbar's controls. */
22
+ const QUESTION_CONTROL_CLASS =
23
+ "h-6 rounded-[var(--agent-kit-radius-xs,0.25rem)] px-2 text-xs";
24
+
25
+ export interface AgentKitQuestionOption {
26
+ value: string;
27
+ label: string;
28
+ recommended?: boolean;
29
+ /** A CSS color for a swatch before the label. */
30
+ color?: string;
31
+ }
32
+
33
+ export interface AgentKitQuestion {
34
+ id: string;
35
+ question: string;
36
+ description?: string;
37
+ /** None, with no `range` or `files`, makes it a written-answer question. */
38
+ options: readonly AgentKitQuestionOption[];
39
+ /** Answered on a slider. */
40
+ range?: { min: number; max: number; step: number; initial: number };
41
+ /** Files the user chooses or drops on the card. */
42
+ files?: boolean;
43
+ multiSelect?: boolean;
44
+ /** Offers the written response beside the options. On by default. */
45
+ allowOther?: boolean;
46
+ required?: boolean;
47
+ /** Picking an option answers this question without pressing Next. */
48
+ submitOnSelect?: boolean;
49
+ placeholder?: string;
50
+ }
51
+
52
+ export interface AgentKitQuestionAnswer {
53
+ values: string[];
54
+ other?: string;
55
+ files?: File[];
56
+ }
57
+
58
+ /** By question id; a skipped question has no entry. */
59
+ export type AgentKitQuestionAnswers = Record<string, AgentKitQuestionAnswer>;
60
+
61
+ export interface QuestionCardProps {
62
+ questions: readonly AgentKitQuestion[];
63
+ /** All answers at once, after the last question. */
64
+ onSubmit: (answers: AgentKitQuestionAnswers) => void;
65
+ /** Closing the card, or skipping every question. */
66
+ onDismiss: () => void;
67
+ title?: string;
68
+ /** Shown under the title, for every question. */
69
+ description?: string;
70
+ /** The last step's send label, such as "Generate". */
71
+ submitLabel?: string;
72
+ skipLabel?: string;
73
+ busy?: boolean;
74
+ /** Locks the answers too. */
75
+ disabled?: boolean;
76
+ /** Answers can be picked, but nothing is sent or skipped. */
77
+ submitBlocked?: boolean;
78
+ /** Shown under the actions, such as a setup notice. */
79
+ footer?: ReactNode;
80
+ /** A control beside the written response, such as dictation. */
81
+ renderResponseControl?: (control: {
82
+ append: (text: string) => void;
83
+ disabled: boolean;
84
+ }) => ReactNode;
85
+ className?: string;
86
+ }
87
+
88
+ function hasAnswer(answer: AgentKitQuestionAnswer | undefined): boolean {
89
+ return Boolean(
90
+ answer &&
91
+ (answer.values.length > 0 ||
92
+ answer.other?.trim() ||
93
+ (answer.files?.length ?? 0) > 0),
94
+ );
95
+ }
96
+
97
+ function recommendedFirst(
98
+ options: readonly AgentKitQuestionOption[],
99
+ ): AgentKitQuestionOption[] {
100
+ return [
101
+ ...options.filter((option) => option.recommended),
102
+ ...options.filter((option) => !option.recommended),
103
+ ];
104
+ }
105
+
106
+ /** Asks one question at a time and sends every answer in one message. */
107
+ export function QuestionCard({
108
+ questions,
109
+ onSubmit,
110
+ onDismiss,
111
+ title,
112
+ description,
113
+ submitLabel,
114
+ skipLabel,
115
+ busy = false,
116
+ disabled = false,
117
+ submitBlocked = false,
118
+ footer,
119
+ renderResponseControl,
120
+ className,
121
+ }: QuestionCardProps) {
122
+ const labels = useAgentKitLabels();
123
+ const [step, setStep] = useState(0);
124
+ const [answers, setAnswers] = useState<AgentKitQuestionAnswers>({});
125
+ const rootRef = useRef<HTMLElement>(null);
126
+ const optionRefs = useRef<Array<HTMLButtonElement | null>>([]);
127
+ const otherRef = useRef<HTMLInputElement>(null);
128
+ const rangeRef = useRef<HTMLInputElement>(null);
129
+ const fileInputRef = useRef<HTMLInputElement>(null);
130
+ const chooseFilesRef = useRef<HTMLButtonElement>(null);
131
+ const focusStepRef = useRef(false);
132
+ const titleId = useId();
133
+ const questionId = useId();
134
+ const total = questions.length;
135
+ const question = questions.at(Math.min(step, total - 1));
136
+
137
+ // Only when the old step had focus, so a late reply doesn't steal it.
138
+ useEffect(() => {
139
+ if (!focusStepRef.current) return;
140
+ focusStepRef.current = false;
141
+ const selected = optionRefs.current.find(
142
+ (button) => button?.getAttribute("aria-checked") === "true",
143
+ );
144
+ (
145
+ selected ??
146
+ optionRefs.current[0] ??
147
+ rangeRef.current ??
148
+ chooseFilesRef.current ??
149
+ otherRef.current
150
+ )?.focus();
151
+ }, [step]);
152
+
153
+ if (!question) return null;
154
+ const options = recommendedFirst(question.options);
155
+ const multiSelect = question.multiSelect === true;
156
+ const range = question.range;
157
+ // A slider's shown value is its answer, even untouched.
158
+ const answer = answers[question.id] ?? {
159
+ values: range ? [String(range.initial)] : [],
160
+ };
161
+ const showOther =
162
+ (options.length === 0 && !range && !question.files) ||
163
+ question.allowOther !== false;
164
+ const last = step >= total - 1;
165
+ const locked = busy || disabled;
166
+ const sendLocked = locked || submitBlocked;
167
+ const answered = hasAnswer(answer);
168
+
169
+ const setAnswer = (next: AgentKitQuestionAnswer) =>
170
+ setAnswers((current) => ({ ...current, [question.id]: next }));
171
+ const choose = (value: string, { fromArrow = false } = {}) => {
172
+ if (locked) return;
173
+ if (!multiSelect) {
174
+ // Single choice: an option and a written answer replace each other.
175
+ const picked = { values: [value] };
176
+ setAnswer(picked);
177
+ // Arrows only move the choice; a click or a number key answers.
178
+ if (question.submitOnSelect && !fromArrow && !(last && sendLocked)) {
179
+ advance({ ...answers, [question.id]: picked });
180
+ }
181
+ return;
182
+ }
183
+ const values = answer.values.includes(value)
184
+ ? answer.values.filter((item) => item !== value)
185
+ : [...answer.values, value];
186
+ setAnswer({ ...answer, values });
187
+ };
188
+ const write = (text: string) =>
189
+ setAnswer(
190
+ multiSelect ? { ...answer, other: text } : { values: [], other: text },
191
+ );
192
+ const append = (text: string) => {
193
+ const added = text.trim();
194
+ if (!added || locked) return;
195
+ const current = answer.other?.trimEnd() ?? "";
196
+ write(current ? `${current} ${added}` : added);
197
+ otherRef.current?.focus();
198
+ };
199
+ const advance = (next: AgentKitQuestionAnswers) => {
200
+ if (!last) {
201
+ focusStepRef.current = Boolean(
202
+ rootRef.current?.contains(document.activeElement),
203
+ );
204
+ setStep(step + 1);
205
+ return;
206
+ }
207
+ const given = Object.fromEntries(
208
+ Object.entries(next).filter(([, value]) => hasAnswer(value)),
209
+ );
210
+ if (Object.keys(given).length === 0) onDismiss();
211
+ else onSubmit(given);
212
+ };
213
+ const next = () => {
214
+ if (locked || !answered || (last && sendLocked)) return;
215
+ advance({ ...answers, [question.id]: answer });
216
+ };
217
+ const addFiles = (incoming: File[]) => {
218
+ if (locked || incoming.length === 0) return;
219
+ setAnswer({ ...answer, files: [...(answer.files ?? []), ...incoming] });
220
+ };
221
+ const removeFile = (index: number) =>
222
+ setAnswer({
223
+ ...answer,
224
+ files: (answer.files ?? []).filter((_, item) => item !== index),
225
+ });
226
+ const skip = () => {
227
+ if (locked || question.required || (last && sendLocked)) return;
228
+ const rest = Object.fromEntries(
229
+ Object.entries(answers).filter(([id]) => id !== question.id),
230
+ );
231
+ setAnswers(rest);
232
+ advance(rest);
233
+ };
234
+ const onOptionKeyDown = (
235
+ event: KeyboardEvent<HTMLButtonElement>,
236
+ index: number,
237
+ ) => {
238
+ const delta =
239
+ event.key === "ArrowDown" || event.key === "ArrowRight"
240
+ ? 1
241
+ : event.key === "ArrowUp" || event.key === "ArrowLeft"
242
+ ? -1
243
+ : 0;
244
+ if (delta === 0) return;
245
+ event.preventDefault();
246
+ const target = (index + delta + options.length) % options.length;
247
+ optionRefs.current[target]?.focus();
248
+ // Arrows move the choice in a radio group, not only the focus.
249
+ const option = options.at(target);
250
+ if (!multiSelect && option) choose(option.value, { fromArrow: true });
251
+ };
252
+ const onCardKeyDown = (event: KeyboardEvent<HTMLElement>) => {
253
+ const target = event.target;
254
+ if (
255
+ target instanceof HTMLInputElement ||
256
+ target instanceof HTMLTextAreaElement ||
257
+ event.altKey ||
258
+ event.ctrlKey ||
259
+ event.metaKey
260
+ ) {
261
+ return;
262
+ }
263
+ if (!/^[1-9]$/.test(event.key)) return;
264
+ const index = Number(event.key) - 1;
265
+ const option = options.at(index);
266
+ if (!option) return;
267
+ event.preventDefault();
268
+ choose(option.value);
269
+ optionRefs.current[index]?.focus();
270
+ };
271
+ const tabStop = multiSelect
272
+ ? -1
273
+ : Math.max(
274
+ 0,
275
+ options.findIndex((option) => answer.values.includes(option.value)),
276
+ );
277
+
278
+ return (
279
+ <section
280
+ ref={rootRef}
281
+ className={["agentkit-question", className].filter(Boolean).join(" ")}
282
+ aria-labelledby={titleId}
283
+ aria-busy={busy || undefined}
284
+ data-step={step + 1}
285
+ data-total={total}
286
+ onKeyDown={onCardKeyDown}
287
+ {...(question.files
288
+ ? {
289
+ onDragOver: (event: DragEvent<HTMLElement>) =>
290
+ event.preventDefault(),
291
+ onDrop: (event: DragEvent<HTMLElement>) => {
292
+ event.preventDefault();
293
+ addFiles(Array.from(event.dataTransfer.files));
294
+ },
295
+ }
296
+ : {})}
297
+ >
298
+ <header className="agentkit-question-header">
299
+ <IconHelpCircle aria-hidden="true" className="agentkit-icon" />
300
+ <h2 id={titleId} className="agentkit-question-title">
301
+ {title ?? labels.question}
302
+ {total > 1 ? (
303
+ <span className="agentkit-visually-hidden">
304
+ {` ${labels.questionProgressLabel
305
+ .replace("{{current}}", () => String(step + 1))
306
+ .replace("{{total}}", () => String(total))}`}
307
+ </span>
308
+ ) : null}
309
+ </h2>
310
+ {total > 1 ? (
311
+ <span className="agentkit-question-progress" aria-hidden="true">
312
+ {labels.questionProgress
313
+ .replace("{{current}}", () => String(step + 1))
314
+ .replace("{{total}}", () => String(total))}
315
+ </span>
316
+ ) : null}
317
+ <IconButton
318
+ size="compact"
319
+ intent="neutral"
320
+ emphasis="ghost"
321
+ label={labels.questionDismiss}
322
+ disabled={busy || submitBlocked}
323
+ className="agentkit-question-dismiss size-6 rounded-[var(--agent-kit-radius-xs,0.25rem)] text-muted-foreground"
324
+ icon={<IconX aria-hidden="true" className="size-3.5" />}
325
+ onPress={onDismiss}
326
+ />
327
+ </header>
328
+ {description ? (
329
+ <p
330
+ {...SESSION_REPLAY_MASK_PROPS}
331
+ className="agentkit-question-description"
332
+ >
333
+ {description}
334
+ </p>
335
+ ) : null}
336
+ <p
337
+ {...SESSION_REPLAY_MASK_PROPS}
338
+ id={questionId}
339
+ className="agentkit-question-text"
340
+ >
341
+ {question.question}
342
+ </p>
343
+ {question.description ? (
344
+ <p
345
+ {...SESSION_REPLAY_MASK_PROPS}
346
+ className="agentkit-question-description"
347
+ >
348
+ {question.description}
349
+ </p>
350
+ ) : null}
351
+ {options.length ? (
352
+ <div
353
+ {...SESSION_REPLAY_MASK_PROPS}
354
+ className="agentkit-question-options"
355
+ role={multiSelect ? "group" : "radiogroup"}
356
+ aria-labelledby={questionId}
357
+ >
358
+ {options.map((option, index) => {
359
+ const selected = answer.values.includes(option.value);
360
+ return (
361
+ <button
362
+ key={`${question.id}:${option.value}`}
363
+ ref={(element) => {
364
+ optionRefs.current[index] = element;
365
+ }}
366
+ type="button"
367
+ role={multiSelect ? "checkbox" : "radio"}
368
+ aria-checked={selected}
369
+ aria-disabled={locked || undefined}
370
+ tabIndex={multiSelect || index === tabStop ? 0 : -1}
371
+ className="agentkit-question-option"
372
+ data-selected={selected ? "true" : "false"}
373
+ onClick={() => choose(option.value)}
374
+ onKeyDown={(event) => onOptionKeyDown(event, index)}
375
+ >
376
+ <span className="agentkit-question-key" aria-hidden="true">
377
+ {index + 1}
378
+ </span>
379
+ {option.color ? (
380
+ <span
381
+ className="agentkit-question-swatch"
382
+ aria-hidden="true"
383
+ // The agent's color is the content, so it is not a token.
384
+ style={{ background: option.color }}
385
+ />
386
+ ) : null}
387
+ <span className="agentkit-question-option-label">
388
+ {option.label}
389
+ </span>
390
+ {option.recommended ? (
391
+ <span className="agentkit-question-recommended">
392
+ {labels.questionRecommended}
393
+ </span>
394
+ ) : null}
395
+ </button>
396
+ );
397
+ })}
398
+ </div>
399
+ ) : null}
400
+ {range ? (
401
+ <div className="agentkit-question-range">
402
+ <span aria-hidden="true">{range.min}</span>
403
+ <input
404
+ ref={rangeRef}
405
+ type="range"
406
+ min={range.min}
407
+ max={range.max}
408
+ step={range.step}
409
+ value={answer.values[0] ?? String(range.initial)}
410
+ disabled={locked}
411
+ aria-labelledby={questionId}
412
+ onChange={(event) => setAnswer({ values: [event.target.value] })}
413
+ />
414
+ <span aria-hidden="true">{range.max}</span>
415
+ <output className="agentkit-question-range-value">
416
+ {answer.values[0] ?? String(range.initial)}
417
+ </output>
418
+ </div>
419
+ ) : null}
420
+ {question.files ? (
421
+ <div className="agentkit-question-files">
422
+ <ActionButton
423
+ size="compact"
424
+ intent="neutral"
425
+ emphasis="outline"
426
+ className={QUESTION_CONTROL_CLASS}
427
+ elementRef={chooseFilesRef}
428
+ disabled={locked}
429
+ leadingIcon={
430
+ <IconPaperclip aria-hidden="true" className="size-3.5" />
431
+ }
432
+ onPress={() => fileInputRef.current?.click()}
433
+ >
434
+ {labels.questionChooseFiles}
435
+ </ActionButton>
436
+ <input
437
+ ref={fileInputRef}
438
+ type="file"
439
+ multiple
440
+ hidden
441
+ tabIndex={-1}
442
+ onChange={(event) => {
443
+ if (event.target.files) addFiles(Array.from(event.target.files));
444
+ event.currentTarget.value = "";
445
+ }}
446
+ />
447
+ {(answer.files ?? []).map((file, index) => (
448
+ <span
449
+ key={`${file.name}:${index}`}
450
+ {...SESSION_REPLAY_MASK_PROPS}
451
+ className="agentkit-question-file"
452
+ >
453
+ <span className="agentkit-question-file-name">{file.name}</span>
454
+ <button
455
+ type="button"
456
+ className="agentkit-question-file-remove"
457
+ disabled={locked}
458
+ aria-label={labels.questionRemoveFile.replace(
459
+ "{{name}}",
460
+ () => file.name,
461
+ )}
462
+ onClick={() => removeFile(index)}
463
+ >
464
+ <IconX aria-hidden="true" className="agentkit-icon" />
465
+ </button>
466
+ </span>
467
+ ))}
468
+ </div>
469
+ ) : null}
470
+ <div className="agentkit-question-footer">
471
+ {showOther ? (
472
+ <label className="agentkit-question-other">
473
+ <IconPencil aria-hidden="true" className="agentkit-icon" />
474
+ <input
475
+ {...SESSION_REPLAY_MASK_PROPS}
476
+ ref={otherRef}
477
+ type="text"
478
+ value={answer.other ?? ""}
479
+ disabled={locked}
480
+ placeholder={
481
+ options.length
482
+ ? labels.questionOwnResponse
483
+ : (question.placeholder ?? labels.questionAnswer)
484
+ }
485
+ aria-label={
486
+ options.length ? labels.questionOwnResponse : question.question
487
+ }
488
+ onChange={(event) => write(event.target.value)}
489
+ onKeyDown={(event) => {
490
+ if (event.key !== "Enter" || event.nativeEvent.isComposing) {
491
+ return;
492
+ }
493
+ event.preventDefault();
494
+ next();
495
+ }}
496
+ />
497
+ </label>
498
+ ) : (
499
+ <span className="agentkit-question-spacer" />
500
+ )}
501
+ <div className="agentkit-question-actions">
502
+ {showOther && renderResponseControl
503
+ ? renderResponseControl({ append, disabled: locked })
504
+ : null}
505
+ <ActionButton
506
+ size="compact"
507
+ intent="neutral"
508
+ emphasis="outline"
509
+ className={QUESTION_CONTROL_CLASS}
510
+ disabled={
511
+ locked || question.required === true || (last && sendLocked)
512
+ }
513
+ onPress={skip}
514
+ >
515
+ {skipLabel ?? labels.questionSkip}
516
+ </ActionButton>
517
+ <ActionButton
518
+ size="compact"
519
+ intent="primary"
520
+ emphasis="solid"
521
+ className={QUESTION_CONTROL_CLASS}
522
+ disabled={locked || !answered || (last && sendLocked)}
523
+ pending={busy && last}
524
+ onPress={next}
525
+ >
526
+ {last ? (submitLabel ?? labels.questionSend) : labels.questionNext}
527
+ </ActionButton>
528
+ </div>
529
+ </div>
530
+ {footer}
531
+ </section>
532
+ );
533
+ }
@@ -32,6 +32,7 @@ import {
32
32
  type AgentKitBranchNavigation,
33
33
  type AgentKitCopyMessageHandler,
34
34
  type AgentKitFeedbackReportBuilder,
35
+ type AgentKitProviderProps,
35
36
  type AgentKitRegistry,
36
37
  type AgentKitRenderFailure,
37
38
  type AgentKitRunUsageLoader,
@@ -99,6 +100,7 @@ export interface AgentKitRootBaseProps {
99
100
  workedForRowLayout?: AgentChatWorkedForRowLayout;
100
101
  dataChartLayout?: AgentChatDataChartLayout;
101
102
  formatWorkedForDuration?: (durationMs: number) => string;
103
+ formatAgentInteractionDetail?: AgentKitProviderProps["formatAgentInteractionDetail"];
102
104
  /** Display name for a model id in run details; the id shows without it. */
103
105
  formatModelName?: (model: string) => string;
104
106
  onClientEffect?: (effect: {
@@ -144,6 +146,7 @@ export function AgentKitRoot({
144
146
  workedForRowLayout,
145
147
  dataChartLayout,
146
148
  formatWorkedForDuration,
149
+ formatAgentInteractionDetail,
147
150
  formatModelName,
148
151
  onClientEffect,
149
152
  load = "auto",
@@ -430,6 +433,7 @@ export function AgentKitRoot({
430
433
  workedForRowLayout={workedForRowLayout}
431
434
  dataChartLayout={dataChartLayout}
432
435
  formatWorkedForDuration={formatWorkedForDuration}
436
+ formatAgentInteractionDetail={formatAgentInteractionDetail}
433
437
  formatModelName={formatModelName}
434
438
  onClientEffect={onClientEffect}
435
439
  >
@@ -0,0 +1,114 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ import { act, useRef, useState } from "react";
4
+ import { createRoot, type Root } from "react-dom/client";
5
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
+
7
+ import { useRovingFocus } from "./roving-focus.js";
8
+
9
+ function Toolbar({
10
+ dir,
11
+ disabledMiddle = false,
12
+ }: {
13
+ dir?: "rtl";
14
+ disabledMiddle?: boolean;
15
+ }) {
16
+ const ref = useRef<HTMLDivElement>(null);
17
+ const [extra, setExtra] = useState(false);
18
+ useRovingFocus(ref);
19
+ return (
20
+ <div ref={ref} role="toolbar" dir={dir}>
21
+ <button type="button">Copy</button>
22
+ <button type="button" disabled={disabledMiddle}>
23
+ Good
24
+ </button>
25
+ <button type="button" onClick={() => setExtra(true)}>
26
+ More
27
+ </button>
28
+ <div inert>
29
+ <button type="button">Hidden</button>
30
+ </div>
31
+ {extra ? <button type="button">Fork</button> : null}
32
+ </div>
33
+ );
34
+ }
35
+
36
+ describe("useRovingFocus", () => {
37
+ let container: HTMLDivElement;
38
+ let root: Root;
39
+
40
+ beforeEach(() => {
41
+ vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
42
+ container = document.createElement("div");
43
+ document.body.appendChild(container);
44
+ root = createRoot(container);
45
+ });
46
+
47
+ afterEach(() => {
48
+ act(() => root.unmount());
49
+ container.remove();
50
+ vi.unstubAllGlobals();
51
+ });
52
+
53
+ const button = (name: string) => {
54
+ const match = Array.from(container.querySelectorAll("button")).find(
55
+ (item) => item.textContent === name,
56
+ );
57
+ if (!match) throw new Error(`No ${name} button`);
58
+ return match;
59
+ };
60
+ const tabStops = () =>
61
+ Array.from(container.querySelectorAll("button"))
62
+ .filter((item) => item.tabIndex === 0)
63
+ .map((item) => item.textContent);
64
+ const press = (key: string) =>
65
+ act(() => {
66
+ document.activeElement?.dispatchEvent(
67
+ new KeyboardEvent("keydown", { key, bubbles: true }),
68
+ );
69
+ });
70
+
71
+ it("gives the toolbar one tab stop and moves it with the arrow keys", async () => {
72
+ await act(async () => root.render(<Toolbar />));
73
+ expect(tabStops()).toEqual(["Copy"]);
74
+
75
+ act(() => button("Copy").focus());
76
+ await press("ArrowRight");
77
+ expect(document.activeElement).toBe(button("Good"));
78
+ expect(tabStops()).toEqual(["Good"]);
79
+
80
+ await press("End");
81
+ expect(document.activeElement).toBe(button("More"));
82
+ await press("ArrowRight");
83
+ expect(document.activeElement).toBe(button("Copy"));
84
+ await press("ArrowLeft");
85
+ expect(document.activeElement).toBe(button("More"));
86
+ await press("Home");
87
+ expect(document.activeElement).toBe(button("Copy"));
88
+ });
89
+
90
+ it("skips disabled and inert items", async () => {
91
+ await act(async () => root.render(<Toolbar disabledMiddle />));
92
+ act(() => button("Copy").focus());
93
+ await press("ArrowRight");
94
+ expect(document.activeElement).toBe(button("More"));
95
+ expect(button("Hidden").tabIndex).not.toBe(0);
96
+ });
97
+
98
+ it("follows reading direction in a right-to-left toolbar", async () => {
99
+ await act(async () => root.render(<Toolbar dir="rtl" />));
100
+ act(() => button("Copy").focus());
101
+ await press("ArrowLeft");
102
+ expect(document.activeElement).toBe(button("Good"));
103
+ });
104
+
105
+ it("keeps a button added later out of the tab order", async () => {
106
+ await act(async () => root.render(<Toolbar />));
107
+ await act(async () => button("More").click());
108
+ await act(async () => {
109
+ await new Promise((resolve) => setTimeout(resolve, 0));
110
+ });
111
+ expect(tabStops()).toEqual(["Copy"]);
112
+ expect(button("Fork").tabIndex).toBe(-1);
113
+ });
114
+ });