@agent-native/toolkit 0.5.1 → 0.7.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 (349) hide show
  1. package/README.md +17 -1
  2. package/dist/chat-history/ChatHistoryList.d.ts +103 -0
  3. package/dist/chat-history/ChatHistoryList.d.ts.map +1 -0
  4. package/dist/chat-history/ChatHistoryList.js +109 -0
  5. package/dist/chat-history/ChatHistoryList.js.map +1 -0
  6. package/dist/chat-history/ChatHistoryList.spec.d.ts +2 -0
  7. package/dist/chat-history/ChatHistoryList.spec.d.ts.map +1 -0
  8. package/dist/chat-history/ChatHistoryList.spec.js +283 -0
  9. package/dist/chat-history/ChatHistoryList.spec.js.map +1 -0
  10. package/dist/chat-history/index.d.ts +2 -0
  11. package/dist/chat-history/index.d.ts.map +1 -0
  12. package/dist/chat-history/index.js +2 -0
  13. package/dist/chat-history/index.js.map +1 -0
  14. package/dist/chat-history.css +314 -0
  15. package/dist/composer/AgentComposerFrame.d.ts +19 -0
  16. package/dist/composer/AgentComposerFrame.d.ts.map +1 -0
  17. package/dist/composer/AgentComposerFrame.js +14 -0
  18. package/dist/composer/AgentComposerFrame.js.map +1 -0
  19. package/dist/composer/ComposerPlusMenu.d.ts +17 -0
  20. package/dist/composer/ComposerPlusMenu.d.ts.map +1 -0
  21. package/dist/composer/ComposerPlusMenu.js +501 -0
  22. package/dist/composer/ComposerPlusMenu.js.map +1 -0
  23. package/dist/composer/MentionPopover.d.ts +29 -0
  24. package/dist/composer/MentionPopover.d.ts.map +1 -0
  25. package/dist/composer/MentionPopover.js +180 -0
  26. package/dist/composer/MentionPopover.js.map +1 -0
  27. package/dist/composer/PastedTextChip.d.ts +9 -0
  28. package/dist/composer/PastedTextChip.d.ts.map +1 -0
  29. package/dist/composer/PastedTextChip.js +47 -0
  30. package/dist/composer/PastedTextChip.js.map +1 -0
  31. package/dist/composer/PromptComposer.d.ts +114 -0
  32. package/dist/composer/PromptComposer.d.ts.map +1 -0
  33. package/dist/composer/PromptComposer.js +302 -0
  34. package/dist/composer/PromptComposer.js.map +1 -0
  35. package/dist/composer/PromptComposer.spec.d.ts +2 -0
  36. package/dist/composer/PromptComposer.spec.d.ts.map +1 -0
  37. package/dist/composer/PromptComposer.spec.js +64 -0
  38. package/dist/composer/PromptComposer.spec.js.map +1 -0
  39. package/dist/composer/RealtimeVoiceMode.d.ts +104 -0
  40. package/dist/composer/RealtimeVoiceMode.d.ts.map +1 -0
  41. package/dist/composer/RealtimeVoiceMode.js +245 -0
  42. package/dist/composer/RealtimeVoiceMode.js.map +1 -0
  43. package/dist/composer/RealtimeVoiceMode.spec.d.ts +2 -0
  44. package/dist/composer/RealtimeVoiceMode.spec.d.ts.map +1 -0
  45. package/dist/composer/RealtimeVoiceMode.spec.js +439 -0
  46. package/dist/composer/RealtimeVoiceMode.spec.js.map +1 -0
  47. package/dist/composer/TiptapComposer.d.ts +221 -0
  48. package/dist/composer/TiptapComposer.d.ts.map +1 -0
  49. package/dist/composer/TiptapComposer.js +1838 -0
  50. package/dist/composer/TiptapComposer.js.map +1 -0
  51. package/dist/composer/TiptapComposer.spec.d.ts +2 -0
  52. package/dist/composer/TiptapComposer.spec.d.ts.map +1 -0
  53. package/dist/composer/TiptapComposer.spec.js +277 -0
  54. package/dist/composer/TiptapComposer.spec.js.map +1 -0
  55. package/dist/composer/VoiceButton.d.ts +25 -0
  56. package/dist/composer/VoiceButton.d.ts.map +1 -0
  57. package/dist/composer/VoiceButton.js +100 -0
  58. package/dist/composer/VoiceButton.js.map +1 -0
  59. package/dist/composer/VoiceButton.spec.d.ts +2 -0
  60. package/dist/composer/VoiceButton.spec.d.ts.map +1 -0
  61. package/dist/composer/VoiceButton.spec.js +19 -0
  62. package/dist/composer/VoiceButton.spec.js.map +1 -0
  63. package/dist/composer/asset-picker-url.d.ts +16 -0
  64. package/dist/composer/asset-picker-url.d.ts.map +1 -0
  65. package/dist/composer/asset-picker-url.js +64 -0
  66. package/dist/composer/asset-picker-url.js.map +1 -0
  67. package/dist/composer/asset-picker-url.spec.d.ts +2 -0
  68. package/dist/composer/asset-picker-url.spec.d.ts.map +1 -0
  69. package/dist/composer/asset-picker-url.spec.js +23 -0
  70. package/dist/composer/asset-picker-url.spec.js.map +1 -0
  71. package/dist/composer/attachment-accept.d.ts +9 -0
  72. package/dist/composer/attachment-accept.d.ts.map +1 -0
  73. package/dist/composer/attachment-accept.js +65 -0
  74. package/dist/composer/attachment-accept.js.map +1 -0
  75. package/dist/composer/attachment-accept.spec.d.ts +2 -0
  76. package/dist/composer/attachment-accept.spec.d.ts.map +1 -0
  77. package/dist/composer/attachment-accept.spec.js +15 -0
  78. package/dist/composer/attachment-accept.spec.js.map +1 -0
  79. package/dist/composer/draft-key.d.ts +2 -0
  80. package/dist/composer/draft-key.d.ts.map +1 -0
  81. package/dist/composer/draft-key.js +8 -0
  82. package/dist/composer/draft-key.js.map +1 -0
  83. package/dist/composer/draft-key.spec.d.ts +2 -0
  84. package/dist/composer/draft-key.spec.d.ts.map +1 -0
  85. package/dist/composer/draft-key.spec.js +13 -0
  86. package/dist/composer/draft-key.spec.js.map +1 -0
  87. package/dist/composer/extensions/FileReference.d.ts +3 -0
  88. package/dist/composer/extensions/FileReference.d.ts.map +1 -0
  89. package/dist/composer/extensions/FileReference.js +36 -0
  90. package/dist/composer/extensions/FileReference.js.map +1 -0
  91. package/dist/composer/extensions/MentionReference.d.ts +3 -0
  92. package/dist/composer/extensions/MentionReference.d.ts.map +1 -0
  93. package/dist/composer/extensions/MentionReference.js +66 -0
  94. package/dist/composer/extensions/MentionReference.js.map +1 -0
  95. package/dist/composer/extensions/SkillReference.d.ts +3 -0
  96. package/dist/composer/extensions/SkillReference.d.ts.map +1 -0
  97. package/dist/composer/extensions/SkillReference.js +40 -0
  98. package/dist/composer/extensions/SkillReference.js.map +1 -0
  99. package/dist/composer/index.d.ts +14 -0
  100. package/dist/composer/index.d.ts.map +1 -0
  101. package/dist/composer/index.js +13 -0
  102. package/dist/composer/index.js.map +1 -0
  103. package/dist/composer/pasted-text.d.ts +31 -0
  104. package/dist/composer/pasted-text.d.ts.map +1 -0
  105. package/dist/composer/pasted-text.js +130 -0
  106. package/dist/composer/pasted-text.js.map +1 -0
  107. package/dist/composer/pasted-text.spec.d.ts +2 -0
  108. package/dist/composer/pasted-text.spec.d.ts.map +1 -0
  109. package/dist/composer/pasted-text.spec.js +107 -0
  110. package/dist/composer/pasted-text.spec.js.map +1 -0
  111. package/dist/composer/prompt-attachments.d.ts +18 -0
  112. package/dist/composer/prompt-attachments.d.ts.map +1 -0
  113. package/dist/composer/prompt-attachments.js +65 -0
  114. package/dist/composer/prompt-attachments.js.map +1 -0
  115. package/dist/composer/prompt-attachments.spec.d.ts +2 -0
  116. package/dist/composer/prompt-attachments.spec.d.ts.map +1 -0
  117. package/dist/composer/prompt-attachments.spec.js +63 -0
  118. package/dist/composer/prompt-attachments.spec.js.map +1 -0
  119. package/dist/composer/realtime-voice-audio-level.d.ts +14 -0
  120. package/dist/composer/realtime-voice-audio-level.d.ts.map +1 -0
  121. package/dist/composer/realtime-voice-audio-level.js +47 -0
  122. package/dist/composer/realtime-voice-audio-level.js.map +1 -0
  123. package/dist/composer/realtime-voice-audio-level.spec.d.ts +2 -0
  124. package/dist/composer/realtime-voice-audio-level.spec.d.ts.map +1 -0
  125. package/dist/composer/realtime-voice-audio-level.spec.js +25 -0
  126. package/dist/composer/realtime-voice-audio-level.spec.js.map +1 -0
  127. package/dist/composer/realtime-voice-transcript.d.ts +26 -0
  128. package/dist/composer/realtime-voice-transcript.d.ts.map +1 -0
  129. package/dist/composer/realtime-voice-transcript.js +103 -0
  130. package/dist/composer/realtime-voice-transcript.js.map +1 -0
  131. package/dist/composer/realtime-voice-transcript.spec.d.ts +2 -0
  132. package/dist/composer/realtime-voice-transcript.spec.d.ts.map +1 -0
  133. package/dist/composer/realtime-voice-transcript.spec.js +76 -0
  134. package/dist/composer/realtime-voice-transcript.spec.js.map +1 -0
  135. package/dist/composer/runtime-adapters.d.ts +163 -0
  136. package/dist/composer/runtime-adapters.d.ts.map +1 -0
  137. package/dist/composer/runtime-adapters.js +130 -0
  138. package/dist/composer/runtime-adapters.js.map +1 -0
  139. package/dist/composer/types.d.ts +60 -0
  140. package/dist/composer/types.d.ts.map +1 -0
  141. package/dist/composer/types.js +2 -0
  142. package/dist/composer/types.js.map +1 -0
  143. package/dist/composer/use-file-search.d.ts +6 -0
  144. package/dist/composer/use-file-search.d.ts.map +1 -0
  145. package/dist/composer/use-file-search.js +53 -0
  146. package/dist/composer/use-file-search.js.map +1 -0
  147. package/dist/composer/use-mention-search.d.ts +6 -0
  148. package/dist/composer/use-mention-search.d.ts.map +1 -0
  149. package/dist/composer/use-mention-search.js +75 -0
  150. package/dist/composer/use-mention-search.js.map +1 -0
  151. package/dist/composer/use-skills.d.ts +7 -0
  152. package/dist/composer/use-skills.d.ts.map +1 -0
  153. package/dist/composer/use-skills.js +40 -0
  154. package/dist/composer/use-skills.js.map +1 -0
  155. package/dist/composer/useRealtimeVoiceMode.d.ts +179 -0
  156. package/dist/composer/useRealtimeVoiceMode.d.ts.map +1 -0
  157. package/dist/composer/useRealtimeVoiceMode.js +1657 -0
  158. package/dist/composer/useRealtimeVoiceMode.js.map +1 -0
  159. package/dist/composer/useRealtimeVoiceMode.spec.d.ts +2 -0
  160. package/dist/composer/useRealtimeVoiceMode.spec.d.ts.map +1 -0
  161. package/dist/composer/useRealtimeVoiceMode.spec.js +618 -0
  162. package/dist/composer/useRealtimeVoiceMode.spec.js.map +1 -0
  163. package/dist/composer/useVoiceDictation.d.ts +57 -0
  164. package/dist/composer/useVoiceDictation.d.ts.map +1 -0
  165. package/dist/composer/useVoiceDictation.js +1004 -0
  166. package/dist/composer/useVoiceDictation.js.map +1 -0
  167. package/dist/composer/useVoiceDictation.spec.d.ts +2 -0
  168. package/dist/composer/useVoiceDictation.spec.d.ts.map +1 -0
  169. package/dist/composer/useVoiceDictation.spec.js +32 -0
  170. package/dist/composer/useVoiceDictation.spec.js.map +1 -0
  171. package/dist/editor/RegistryBlockContext.d.ts +68 -0
  172. package/dist/editor/RegistryBlockContext.d.ts.map +1 -0
  173. package/dist/editor/RegistryBlockContext.js +10 -0
  174. package/dist/editor/RegistryBlockContext.js.map +1 -0
  175. package/dist/editor/RegistryBlockNode.d.ts +66 -0
  176. package/dist/editor/RegistryBlockNode.d.ts.map +1 -0
  177. package/dist/editor/RegistryBlockNode.js +406 -0
  178. package/dist/editor/RegistryBlockNode.js.map +1 -0
  179. package/dist/editor/RegistryBlockNode.spec.d.ts +2 -0
  180. package/dist/editor/RegistryBlockNode.spec.d.ts.map +1 -0
  181. package/dist/editor/RegistryBlockNode.spec.js +215 -0
  182. package/dist/editor/RegistryBlockNode.spec.js.map +1 -0
  183. package/dist/editor/index.d.ts +2 -0
  184. package/dist/editor/index.d.ts.map +1 -1
  185. package/dist/editor/index.js +2 -0
  186. package/dist/editor/index.js.map +1 -1
  187. package/dist/editor/registrySlashCommands.d.ts +68 -0
  188. package/dist/editor/registrySlashCommands.d.ts.map +1 -0
  189. package/dist/editor/registrySlashCommands.js +51 -0
  190. package/dist/editor/registrySlashCommands.js.map +1 -0
  191. package/package.json +32 -1
  192. package/src/app-shell/header-actions.tsx +84 -0
  193. package/src/app-shell/index.ts +1 -0
  194. package/src/chat-history/ChatHistoryList.spec.tsx +453 -0
  195. package/src/chat-history/ChatHistoryList.tsx +469 -0
  196. package/src/chat-history/index.ts +7 -0
  197. package/src/chat-history.css +314 -0
  198. package/src/collab-ui/AgentPresenceChip.tsx +69 -0
  199. package/src/collab-ui/LiveCursorOverlay.tsx +241 -0
  200. package/src/collab-ui/PresenceBar.tsx +346 -0
  201. package/src/collab-ui/RecentEditHighlights.tsx +225 -0
  202. package/src/collab-ui/RemoteSelectionRings.tsx +227 -0
  203. package/src/collab-ui/agent-identity.ts +16 -0
  204. package/src/collab-ui/index.ts +39 -0
  205. package/src/collab-ui/lead-client.ts +32 -0
  206. package/src/collab-ui/types.spec.ts +18 -0
  207. package/src/collab-ui/types.ts +96 -0
  208. package/src/composer/AgentComposerFrame.tsx +59 -0
  209. package/src/composer/ComposerPlusMenu.tsx +983 -0
  210. package/src/composer/MentionPopover.tsx +412 -0
  211. package/src/composer/PastedTextChip.tsx +150 -0
  212. package/src/composer/PromptComposer.spec.ts +73 -0
  213. package/src/composer/PromptComposer.tsx +718 -0
  214. package/src/composer/RealtimeVoiceMode.spec.tsx +771 -0
  215. package/src/composer/RealtimeVoiceMode.tsx +856 -0
  216. package/src/composer/TiptapComposer.spec.ts +390 -0
  217. package/src/composer/TiptapComposer.tsx +2936 -0
  218. package/src/composer/VoiceButton.spec.ts +33 -0
  219. package/src/composer/VoiceButton.tsx +255 -0
  220. package/src/composer/asset-picker-url.spec.ts +56 -0
  221. package/src/composer/asset-picker-url.ts +80 -0
  222. package/src/composer/attachment-accept.spec.ts +22 -0
  223. package/src/composer/attachment-accept.ts +71 -0
  224. package/src/composer/draft-key.spec.ts +19 -0
  225. package/src/composer/draft-key.ts +7 -0
  226. package/src/composer/extensions/FileReference.tsx +55 -0
  227. package/src/composer/extensions/MentionReference.tsx +93 -0
  228. package/src/composer/extensions/SkillReference.tsx +56 -0
  229. package/src/composer/index.ts +79 -0
  230. package/src/composer/pasted-text.spec.ts +162 -0
  231. package/src/composer/pasted-text.ts +160 -0
  232. package/src/composer/prompt-attachments.spec.ts +80 -0
  233. package/src/composer/prompt-attachments.ts +93 -0
  234. package/src/composer/realtime-voice-audio-level.spec.ts +32 -0
  235. package/src/composer/realtime-voice-audio-level.ts +64 -0
  236. package/src/composer/realtime-voice-transcript.spec.ts +95 -0
  237. package/src/composer/realtime-voice-transcript.ts +134 -0
  238. package/src/composer/runtime-adapters.tsx +343 -0
  239. package/src/composer/types.ts +66 -0
  240. package/src/composer/use-file-search.ts +64 -0
  241. package/src/composer/use-mention-search.ts +90 -0
  242. package/src/composer/use-skills.ts +45 -0
  243. package/src/composer/useRealtimeVoiceMode.spec.ts +818 -0
  244. package/src/composer/useRealtimeVoiceMode.tsx +2117 -0
  245. package/src/composer/useVoiceDictation.spec.ts +45 -0
  246. package/src/composer/useVoiceDictation.ts +1191 -0
  247. package/src/context-ui/ContextMeter.tsx +101 -0
  248. package/src/context-ui/ContextSegmentRow.tsx +184 -0
  249. package/src/context-ui/ContextTreemap.tsx +125 -0
  250. package/src/context-ui/ContextXRayPanel.tsx +377 -0
  251. package/src/context-ui/format.ts +42 -0
  252. package/src/context-ui/index.ts +15 -0
  253. package/src/context-ui/types.ts +43 -0
  254. package/src/design-tweaks/index.ts +1 -0
  255. package/src/design-tweaks/visual-style-controls.tsx +1084 -0
  256. package/src/editor/BubbleToolbar.tsx +253 -0
  257. package/src/editor/CodeBlockNode.tsx +254 -0
  258. package/src/editor/DragHandle.spec.ts +588 -0
  259. package/src/editor/DragHandle.ts +1437 -0
  260. package/src/editor/ImageExtension.roundtrip.spec.ts +117 -0
  261. package/src/editor/ImageExtension.ts +306 -0
  262. package/src/editor/RegistryBlockContext.tsx +107 -0
  263. package/src/editor/RegistryBlockNode.spec.ts +279 -0
  264. package/src/editor/RegistryBlockNode.tsx +652 -0
  265. package/src/editor/RichMarkdownEditor.collab.spec.ts +79 -0
  266. package/src/editor/RichMarkdownEditor.roundtrip.spec.ts +226 -0
  267. package/src/editor/RichMarkdownEditor.tsx +81 -0
  268. package/src/editor/RunId.ts +61 -0
  269. package/src/editor/SharedRichEditor.tsx +321 -0
  270. package/src/editor/SlashCommandMenu.tsx +310 -0
  271. package/src/editor/disableHistory.spec.ts +85 -0
  272. package/src/editor/extensions.ts +402 -0
  273. package/src/editor/gfmDoc.spec.ts +230 -0
  274. package/src/editor/gfmDoc.ts +92 -0
  275. package/src/editor/index.ts +93 -0
  276. package/src/editor/registrySlashCommands.ts +131 -0
  277. package/src/editor/structuralReorderUndo.spec.ts +144 -0
  278. package/src/editor/surgical-apply.spec.ts +279 -0
  279. package/src/editor/surgical-apply.ts +192 -0
  280. package/src/editor/useCollabReconcile.concurrent.spec.ts +422 -0
  281. package/src/editor/useCollabReconcile.idempotent.spec.ts +324 -0
  282. package/src/editor/useCollabReconcile.ts +589 -0
  283. package/src/editor.css +510 -0
  284. package/src/hooks/index.ts +2 -0
  285. package/src/hooks/use-mobile.tsx +21 -0
  286. package/src/hooks/use-toast.ts +188 -0
  287. package/src/index.ts +8 -0
  288. package/src/onboarding/index.ts +1 -0
  289. package/src/provider.tsx +47 -0
  290. package/src/sharing/VisibilityBadge.spec.ts +96 -0
  291. package/src/sharing/VisibilityBadge.tsx +37 -0
  292. package/src/sharing/index.ts +4 -0
  293. package/src/styles.css +21 -0
  294. package/src/ui/accordion.tsx +56 -0
  295. package/src/ui/alert-dialog.tsx +141 -0
  296. package/src/ui/alert.tsx +59 -0
  297. package/src/ui/aspect-ratio.tsx +5 -0
  298. package/src/ui/avatar.tsx +48 -0
  299. package/src/ui/badge.tsx +37 -0
  300. package/src/ui/breadcrumb.tsx +115 -0
  301. package/src/ui/button.tsx +78 -0
  302. package/src/ui/calendar.tsx +214 -0
  303. package/src/ui/card.tsx +86 -0
  304. package/src/ui/carousel.tsx +260 -0
  305. package/src/ui/chart.tsx +375 -0
  306. package/src/ui/checkbox.tsx +28 -0
  307. package/src/ui/collapsible.tsx +9 -0
  308. package/src/ui/command.spec.tsx +51 -0
  309. package/src/ui/command.tsx +174 -0
  310. package/src/ui/context-menu.tsx +202 -0
  311. package/src/ui/date-picker.tsx +64 -0
  312. package/src/ui/dialog.spec.tsx +43 -0
  313. package/src/ui/dialog.tsx +146 -0
  314. package/src/ui/drawer.tsx +126 -0
  315. package/src/ui/dropdown-menu.tsx +218 -0
  316. package/src/ui/form.tsx +177 -0
  317. package/src/ui/hover-card.tsx +27 -0
  318. package/src/ui/index.ts +52 -0
  319. package/src/ui/input-otp.tsx +69 -0
  320. package/src/ui/input.tsx +22 -0
  321. package/src/ui/label.tsx +24 -0
  322. package/src/ui/logo.tsx +36 -0
  323. package/src/ui/menubar.tsx +235 -0
  324. package/src/ui/navigation-menu.tsx +128 -0
  325. package/src/ui/pagination.tsx +121 -0
  326. package/src/ui/popover.tsx +44 -0
  327. package/src/ui/progress.tsx +26 -0
  328. package/src/ui/radio-group.tsx +42 -0
  329. package/src/ui/resizable.tsx +43 -0
  330. package/src/ui/scroll-area.tsx +46 -0
  331. package/src/ui/select.tsx +158 -0
  332. package/src/ui/separator.tsx +29 -0
  333. package/src/ui/sheet.tsx +160 -0
  334. package/src/ui/sidebar.tsx +777 -0
  335. package/src/ui/skeleton.tsx +18 -0
  336. package/src/ui/slider.tsx +26 -0
  337. package/src/ui/sonner.tsx +58 -0
  338. package/src/ui/spinner.tsx +17 -0
  339. package/src/ui/switch.tsx +27 -0
  340. package/src/ui/table.tsx +117 -0
  341. package/src/ui/tabs.tsx +53 -0
  342. package/src/ui/textarea.tsx +23 -0
  343. package/src/ui/toast.tsx +127 -0
  344. package/src/ui/toaster.tsx +33 -0
  345. package/src/ui/toggle-group.tsx +59 -0
  346. package/src/ui/toggle.tsx +43 -0
  347. package/src/ui/tooltip.tsx +53 -0
  348. package/src/ui/use-toast.ts +3 -0
  349. package/src/utils.ts +8 -0
@@ -0,0 +1,412 @@
1
+ import {
2
+ IconFile,
3
+ IconFolder,
4
+ IconStack2,
5
+ IconFileText,
6
+ IconCheckbox,
7
+ IconMail,
8
+ IconUser,
9
+ IconPresentation,
10
+ IconMessageChatbot,
11
+ IconTrash,
12
+ IconPlus,
13
+ IconHelp,
14
+ IconHistory,
15
+ IconTerminal2,
16
+ IconClipboardList,
17
+ IconPencil,
18
+ } from "@tabler/icons-react";
19
+ import React, {
20
+ useState,
21
+ useEffect,
22
+ useRef,
23
+ useImperativeHandle,
24
+ forwardRef,
25
+ } from "react";
26
+ import { createPortal } from "react-dom";
27
+
28
+ import type { MentionItem, SkillResult, SlashCommand } from "./types.js";
29
+
30
+ export interface MentionPopoverRef {
31
+ moveUp: () => void;
32
+ moveDown: () => void;
33
+ getSelectedIndex: () => number;
34
+ getSelectedMention: () => MentionItem | null;
35
+ getSelectedCommand: () => SlashCommand | null;
36
+ }
37
+
38
+ interface MentionPopoverProps {
39
+ type: "@" | "/";
40
+ position: { top: number; left: number } | null;
41
+ mentionItems: MentionItem[];
42
+ skills: SkillResult[];
43
+ commands?: SlashCommand[];
44
+ hint?: string;
45
+ isLoading: boolean;
46
+ query: string;
47
+ onSelectMention: (item: MentionItem) => void;
48
+ onSelectSkill: (skill: SkillResult) => void;
49
+ onSelectCommand?: (command: SlashCommand) => void;
50
+ onClose: () => void;
51
+ }
52
+
53
+ const iconProps = { size: 14, className: "shrink-0 text-muted-foreground" };
54
+
55
+ function MentionItemIcon({ icon }: { icon?: string }) {
56
+ switch (icon) {
57
+ case "folder":
58
+ return <IconFolder {...iconProps} />;
59
+ case "document":
60
+ return <IconFileText {...iconProps} />;
61
+ case "form":
62
+ return <IconCheckbox {...iconProps} />;
63
+ case "email":
64
+ return <IconMail {...iconProps} />;
65
+ case "user":
66
+ return <IconUser {...iconProps} />;
67
+ case "deck":
68
+ return <IconPresentation {...iconProps} />;
69
+ case "agent":
70
+ return <IconMessageChatbot {...iconProps} />;
71
+ case "file":
72
+ return <IconFile {...iconProps} />;
73
+ default:
74
+ return <IconFile {...iconProps} />;
75
+ }
76
+ }
77
+
78
+ function CommandIcon({ icon }: { icon?: string }) {
79
+ switch (icon) {
80
+ case "clear":
81
+ return <IconTrash {...iconProps} />;
82
+ case "new":
83
+ return <IconPlus {...iconProps} />;
84
+ case "help":
85
+ return <IconHelp {...iconProps} />;
86
+ case "history":
87
+ return <IconHistory {...iconProps} />;
88
+ case "plan":
89
+ return <IconClipboardList {...iconProps} />;
90
+ case "act":
91
+ return <IconPencil {...iconProps} />;
92
+ default:
93
+ return <IconTerminal2 {...iconProps} />;
94
+ }
95
+ }
96
+
97
+ function HintWithLink({ hint }: { hint: string }) {
98
+ // If hint contains a URL, split it and render the URL as a link
99
+ const urlMatch = hint.match(/(https?:\/\/\S+)/);
100
+ if (!urlMatch) return <>{hint}</>;
101
+ const before = hint.slice(0, urlMatch.index);
102
+ const url = urlMatch[1];
103
+ return (
104
+ <>
105
+ {before}
106
+ <a
107
+ href={url}
108
+ target="_blank"
109
+ rel="noopener noreferrer"
110
+ className="underline hover:text-foreground"
111
+ >
112
+ Learn more
113
+ </a>
114
+ </>
115
+ );
116
+ }
117
+
118
+ function LoadingSkeleton() {
119
+ return (
120
+ <div className="space-y-1 p-1">
121
+ {[1, 2, 3].map((i) => (
122
+ <div key={i} className="flex items-center gap-2 rounded px-2 py-1.5">
123
+ <div className="h-3.5 w-3.5 rounded bg-muted animate-pulse" />
124
+ <div
125
+ className="h-3 rounded bg-muted animate-pulse"
126
+ style={{ width: `${60 + i * 20}px` }}
127
+ />
128
+ </div>
129
+ ))}
130
+ </div>
131
+ );
132
+ }
133
+
134
+ function LoadingSkeletonRow() {
135
+ return (
136
+ <div className="flex items-center gap-2 rounded px-2 py-1.5">
137
+ <div className="h-3.5 w-3.5 rounded bg-muted animate-pulse" />
138
+ <div className="h-3 w-24 rounded bg-muted animate-pulse" />
139
+ </div>
140
+ );
141
+ }
142
+
143
+ export const MentionPopover = forwardRef<
144
+ MentionPopoverRef,
145
+ MentionPopoverProps
146
+ >(function MentionPopover(props, ref) {
147
+ const {
148
+ type,
149
+ position,
150
+ mentionItems,
151
+ skills,
152
+ commands = [],
153
+ hint,
154
+ isLoading,
155
+ query,
156
+ onSelectMention,
157
+ onSelectSkill,
158
+ onSelectCommand,
159
+ onClose,
160
+ } = props;
161
+
162
+ const [selectedIndex, setSelectedIndex] = useState(0);
163
+ const listRef = useRef<HTMLDivElement>(null);
164
+
165
+ const itemCount =
166
+ type === "@" ? mentionItems.length : commands.length + skills.length;
167
+
168
+ // Group mention items by section for @ popover
169
+ const groupedMentions = React.useMemo(() => {
170
+ if (type !== "@") return [];
171
+ const groups = new Map<string, MentionItem[]>();
172
+ for (const item of mentionItems) {
173
+ const section = item.section || "Other";
174
+ if (!groups.has(section)) groups.set(section, []);
175
+ groups.get(section)!.push(item);
176
+ }
177
+ // Sort: Agents first, then Connected Agents, then template-specific,
178
+ // then Files, then Other
179
+ const sorted: { section: string; items: MentionItem[] }[] = [];
180
+ const knownSections = new Set([
181
+ "Agents",
182
+ "Connected Agents",
183
+ "Files",
184
+ "Other",
185
+ ]);
186
+ // Agents first
187
+ if (groups.has("Agents")) {
188
+ sorted.push({ section: "Agents", items: groups.get("Agents")! });
189
+ groups.delete("Agents");
190
+ }
191
+ if (groups.has("Connected Agents")) {
192
+ sorted.push({
193
+ section: "Connected Agents",
194
+ items: groups.get("Connected Agents")!,
195
+ });
196
+ groups.delete("Connected Agents");
197
+ }
198
+ // Template-specific sections (anything not in knownSections)
199
+ for (const [section, items] of groups) {
200
+ if (!knownSections.has(section)) {
201
+ sorted.push({ section, items });
202
+ }
203
+ }
204
+ // Files
205
+ if (groups.has("Files")) {
206
+ sorted.push({ section: "Files", items: groups.get("Files")! });
207
+ }
208
+ // Other
209
+ if (groups.has("Other")) {
210
+ sorted.push({ section: "Other", items: groups.get("Other")! });
211
+ }
212
+ return sorted;
213
+ }, [type, mentionItems]);
214
+
215
+ // Flat list of mention items in section order for keyboard index tracking
216
+ const flatMentionItems = React.useMemo(() => {
217
+ return groupedMentions.flatMap((g) => g.items);
218
+ }, [groupedMentions]);
219
+
220
+ // Reset selection when items change
221
+ useEffect(() => {
222
+ setSelectedIndex(0);
223
+ }, [commands, mentionItems, skills, query]);
224
+
225
+ // Scroll selected item into view
226
+ useEffect(() => {
227
+ const container = listRef.current;
228
+ if (!container) return;
229
+ // Find the actual item element by data attribute
230
+ const selected = container.querySelector(
231
+ `[data-mention-index="${selectedIndex}"]`,
232
+ ) as HTMLElement | undefined;
233
+ if (selected) {
234
+ selected.scrollIntoView({ block: "nearest" });
235
+ }
236
+ }, [selectedIndex]);
237
+
238
+ useImperativeHandle(ref, () => ({
239
+ moveUp: () => {
240
+ setSelectedIndex((prev) =>
241
+ prev <= 0 ? Math.max(0, itemCount - 1) : prev - 1,
242
+ );
243
+ },
244
+ moveDown: () => {
245
+ setSelectedIndex((prev) => (prev >= itemCount - 1 ? 0 : prev + 1));
246
+ },
247
+ getSelectedIndex: () => selectedIndex,
248
+ getSelectedMention: () => flatMentionItems[selectedIndex] ?? null,
249
+ getSelectedCommand: () => {
250
+ if (type !== "/" || selectedIndex >= commands.length) return null;
251
+ return commands[selectedIndex] ?? null;
252
+ },
253
+ }));
254
+
255
+ if (!position) return null;
256
+
257
+ const content = (
258
+ <>
259
+ {/* Backdrop to capture outside clicks */}
260
+ <div className="fixed inset-0 z-[9998]" onClick={onClose} />
261
+ <div
262
+ data-agent-native-composer-popover="true"
263
+ className="fixed z-[9999] w-[320px] overflow-y-auto rounded-lg border border-border bg-popover shadow-lg"
264
+ style={{
265
+ bottom: `calc(100vh - ${position.top}px + 4px)`,
266
+ left: Math.max(8, Math.min(position.left, window.innerWidth - 336)),
267
+ maxHeight: Math.min(320, position.top - 8),
268
+ }}
269
+ >
270
+ {isLoading && itemCount === 0 ? (
271
+ <LoadingSkeleton />
272
+ ) : itemCount === 0 ? (
273
+ <div className="px-3 py-4 text-center text-xs text-muted-foreground">
274
+ {type === "@" ? (
275
+ query ? (
276
+ "No results found"
277
+ ) : (
278
+ "Type to search..."
279
+ )
280
+ ) : hint ? (
281
+ <HintWithLink hint={hint} />
282
+ ) : (
283
+ "No skills available"
284
+ )}
285
+ </div>
286
+ ) : (
287
+ <div ref={listRef} className="p-1">
288
+ {isLoading && <LoadingSkeletonRow />}
289
+ {type === "@"
290
+ ? (() => {
291
+ let flatIndex = 0;
292
+ return groupedMentions.map((group) => (
293
+ <div key={group.section}>
294
+ <div className="px-2 pt-2 pb-1 text-[10px] font-medium text-muted-foreground/70 uppercase tracking-wider">
295
+ {group.section}
296
+ </div>
297
+ {group.items.map((item) => {
298
+ const idx = flatIndex++;
299
+ return (
300
+ <button
301
+ key={item.id}
302
+ data-mention-index={idx}
303
+ className={`flex w-full items-center gap-2 rounded px-2 py-1.5 text-start text-sm ${
304
+ idx === selectedIndex
305
+ ? "bg-accent text-accent-foreground"
306
+ : "hover:bg-accent/50"
307
+ }`}
308
+ onMouseEnter={() => setSelectedIndex(idx)}
309
+ onClick={() => onSelectMention(item)}
310
+ >
311
+ <MentionItemIcon icon={item.icon} />
312
+ <span className="truncate text-sm">
313
+ {item.label}
314
+ </span>
315
+ {item.description && (
316
+ <span className="ms-auto shrink-0 truncate max-w-[160px] text-xs text-muted-foreground">
317
+ {item.description}
318
+ </span>
319
+ )}
320
+ </button>
321
+ );
322
+ })}
323
+ </div>
324
+ ));
325
+ })()
326
+ : (() => {
327
+ let idx = 0;
328
+ return (
329
+ <>
330
+ {commands.length > 0 && (
331
+ <div>
332
+ <div className="px-2 pt-2 pb-1 text-[10px] font-medium text-muted-foreground/70 uppercase tracking-wider">
333
+ Commands
334
+ </div>
335
+ {commands.map((cmd) => {
336
+ const i = idx++;
337
+ return (
338
+ <button
339
+ key={cmd.name}
340
+ data-mention-index={i}
341
+ className={`flex w-full items-center gap-2 rounded px-2 py-1.5 text-start text-sm ${
342
+ i === selectedIndex
343
+ ? "bg-accent text-accent-foreground"
344
+ : "hover:bg-accent/50"
345
+ }`}
346
+ onMouseEnter={() => setSelectedIndex(i)}
347
+ onClick={() => onSelectCommand?.(cmd)}
348
+ >
349
+ <CommandIcon icon={cmd.icon} />
350
+ <span className="min-w-0 flex-1">
351
+ <span className="block truncate text-sm">
352
+ /{cmd.name}
353
+ </span>
354
+ {cmd.description && (
355
+ <span className="block truncate text-xs text-muted-foreground">
356
+ {cmd.description}
357
+ </span>
358
+ )}
359
+ </span>
360
+ </button>
361
+ );
362
+ })}
363
+ </div>
364
+ )}
365
+ {skills.length > 0 && (
366
+ <div>
367
+ {commands.length > 0 && (
368
+ <div className="px-2 pt-2 pb-1 text-[10px] font-medium text-muted-foreground/70 uppercase tracking-wider">
369
+ Skills
370
+ </div>
371
+ )}
372
+ {(skills as SkillResult[]).map((skill) => {
373
+ const i = idx++;
374
+ return (
375
+ <button
376
+ key={skill.path}
377
+ data-mention-index={i}
378
+ className={`flex w-full items-center gap-2 rounded px-2 py-1.5 text-start text-sm ${
379
+ i === selectedIndex
380
+ ? "bg-accent text-accent-foreground"
381
+ : "hover:bg-accent/50"
382
+ }`}
383
+ onMouseEnter={() => setSelectedIndex(i)}
384
+ onClick={() => onSelectSkill(skill)}
385
+ >
386
+ <IconStack2 {...iconProps} />
387
+ <span className="min-w-0 flex-1">
388
+ <span className="block truncate text-sm">
389
+ {skill.name}
390
+ </span>
391
+ {skill.description && (
392
+ <span className="block truncate text-xs text-muted-foreground">
393
+ {skill.description}
394
+ </span>
395
+ )}
396
+ </span>
397
+ </button>
398
+ );
399
+ })}
400
+ </div>
401
+ )}
402
+ </>
403
+ );
404
+ })()}
405
+ </div>
406
+ )}
407
+ </div>
408
+ </>
409
+ );
410
+
411
+ return createPortal(content, document.body);
412
+ });
@@ -0,0 +1,150 @@
1
+ import type { Attachment } from "@assistant-ui/react";
2
+ import * as PopoverPrimitive from "@radix-ui/react-popover";
3
+ import { IconClipboardText, IconX } from "@tabler/icons-react";
4
+ import { useCallback, useEffect, useState } from "react";
5
+
6
+ import { cn } from "../utils.js";
7
+ import { countLines, unwrapAttachmentEnvelope } from "./pasted-text.js";
8
+
9
+ function readAttachmentText(attachment: Attachment): Promise<string> | string {
10
+ if ("file" in attachment && attachment.file instanceof File) {
11
+ return attachment.file.text();
12
+ }
13
+ const textPart = attachment.content?.find(
14
+ (p): p is { type: "text"; text: string } =>
15
+ p.type === "text" && "text" in p && typeof p.text === "string",
16
+ );
17
+ return textPart ? unwrapAttachmentEnvelope(textPart.text) : "";
18
+ }
19
+
20
+ function usePastedAttachmentText(attachment: Attachment): {
21
+ text: string;
22
+ lines: number;
23
+ chars: number;
24
+ } {
25
+ const [text, setText] = useState("");
26
+
27
+ useEffect(() => {
28
+ let cancelled = false;
29
+ const result = readAttachmentText(attachment);
30
+ if (typeof result === "string") {
31
+ setText(result);
32
+ return;
33
+ }
34
+ result
35
+ .then((value) => {
36
+ if (!cancelled) setText(value);
37
+ })
38
+ .catch(() => {});
39
+ return () => {
40
+ cancelled = true;
41
+ };
42
+ }, [attachment]);
43
+
44
+ return { text, lines: countLines(text), chars: text.length };
45
+ }
46
+
47
+ export interface PastedTextChipProps {
48
+ attachment: Attachment;
49
+ onRemove?: (id: string) => void;
50
+ /** Compact variant rendered inside sent user messages. */
51
+ compact?: boolean;
52
+ }
53
+
54
+ export function PastedTextChip({
55
+ attachment,
56
+ onRemove,
57
+ compact = false,
58
+ }: PastedTextChipProps) {
59
+ const [open, setOpen] = useState(false);
60
+ const { text, lines, chars } = usePastedAttachmentText(attachment);
61
+
62
+ const handleRemove = useCallback(
63
+ (event: React.MouseEvent) => {
64
+ event.stopPropagation();
65
+ onRemove?.(attachment.id);
66
+ },
67
+ [attachment.id, onRemove],
68
+ );
69
+
70
+ const summary = lines > 0 ? `${lines} lines` : `${chars} chars`;
71
+
72
+ return (
73
+ <PopoverPrimitive.Root open={open} onOpenChange={setOpen}>
74
+ <PopoverPrimitive.Trigger asChild>
75
+ <button
76
+ type="button"
77
+ className={cn(
78
+ "group relative inline-flex items-center gap-2 rounded-md border border-border/70 bg-muted/50 text-start text-foreground hover:bg-muted/70 cursor-pointer",
79
+ compact
80
+ ? "max-w-[220px] px-2 py-1.5 text-xs"
81
+ : "max-w-[260px] px-2.5 py-2 text-xs",
82
+ onRemove && !compact && "pe-7",
83
+ )}
84
+ aria-label="Preview pasted text"
85
+ >
86
+ <span
87
+ className={cn(
88
+ "flex shrink-0 items-center justify-center rounded bg-background text-muted-foreground",
89
+ compact ? "h-6 w-6" : "h-8 w-8",
90
+ )}
91
+ >
92
+ <IconClipboardText
93
+ className={compact ? "h-3.5 w-3.5" : "h-4 w-4"}
94
+ />
95
+ </span>
96
+ <span className="min-w-0 flex-1">
97
+ <span className="block truncate font-medium">Pasted text</span>
98
+ <span className="block text-[11px] text-muted-foreground">
99
+ {summary}
100
+ </span>
101
+ </span>
102
+ {onRemove && (
103
+ <span
104
+ role="button"
105
+ tabIndex={-1}
106
+ onClick={handleRemove}
107
+ aria-label="Remove pasted text"
108
+ className={cn(
109
+ "flex h-5 w-5 shrink-0 cursor-pointer items-center justify-center rounded text-muted-foreground hover:text-foreground",
110
+ compact ? "" : "absolute end-1.5 top-1.5",
111
+ )}
112
+ >
113
+ <IconX className="h-3 w-3" />
114
+ </span>
115
+ )}
116
+ </button>
117
+ </PopoverPrimitive.Trigger>
118
+ <PopoverPrimitive.Portal>
119
+ <PopoverPrimitive.Content
120
+ side="top"
121
+ align="start"
122
+ sideOffset={6}
123
+ collisionPadding={12}
124
+ className="z-50 w-[min(560px,calc(100vw-32px))] rounded-lg border border-border bg-popover text-popover-foreground shadow-lg origin-[var(--radix-popover-content-transform-origin)] data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=open]:fade-in-0 data-[state=closed]:fade-out-0 data-[state=open]:zoom-in-95 data-[state=closed]:zoom-out-95"
125
+ >
126
+ <div className="flex items-center justify-between gap-2 border-b border-border px-3 py-2">
127
+ <div className="flex items-center gap-2 min-w-0">
128
+ <IconClipboardText className="h-4 w-4 shrink-0 text-muted-foreground" />
129
+ <span className="text-sm font-medium truncate">Pasted text</span>
130
+ <span className="text-[11px] text-muted-foreground shrink-0">
131
+ {lines} lines · {chars} chars
132
+ </span>
133
+ </div>
134
+ <button
135
+ type="button"
136
+ onClick={() => setOpen(false)}
137
+ aria-label="Close preview"
138
+ className="flex h-6 w-6 shrink-0 cursor-pointer items-center justify-center rounded text-muted-foreground hover:text-foreground"
139
+ >
140
+ <IconX className="h-3.5 w-3.5" />
141
+ </button>
142
+ </div>
143
+ <pre className="max-h-[60vh] overflow-auto px-3 py-2 text-[12px] leading-[1.5] whitespace-pre-wrap break-words font-mono text-foreground">
144
+ {text}
145
+ </pre>
146
+ </PopoverPrimitive.Content>
147
+ </PopoverPrimitive.Portal>
148
+ </PopoverPrimitive.Root>
149
+ );
150
+ }
@@ -0,0 +1,73 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ import { describe, expect, it } from "vitest";
4
+
5
+ import { buildPromptComposerSubmission } from "./PromptComposer.js";
6
+
7
+ describe("buildPromptComposerSubmission", () => {
8
+ it("passes images through files only — never inlines base64 into prompt text", async () => {
9
+ // Images are passed to `files` for the host to process through the
10
+ // attachment pipeline. They must NOT be inlined as base64 in `text`
11
+ // (≈700K tokens per MB of image data).
12
+ const file = new File(["fake image"], "sketch.png", {
13
+ type: "image/png",
14
+ });
15
+
16
+ const result = await buildPromptComposerSubmission({
17
+ text: "",
18
+ attachments: [
19
+ {
20
+ id: "sketch.png",
21
+ name: "sketch.png",
22
+ type: "image",
23
+ file,
24
+ },
25
+ ],
26
+ });
27
+
28
+ expect(result.files).toEqual([file]);
29
+ // text must not contain any base64 data or uploaded-image markup
30
+ expect(result.text).not.toContain("data:image");
31
+ expect(result.text).not.toContain("<uploaded-image");
32
+ });
33
+
34
+ it("escapes inline attachment metadata in standalone submissions", async () => {
35
+ const file = new File(["hello"], 'bad"name&.md', {
36
+ type: "text/markdown",
37
+ });
38
+
39
+ const result = await buildPromptComposerSubmission({
40
+ text: "Review this",
41
+ attachments: [
42
+ {
43
+ id: "bad",
44
+ name: file.name,
45
+ type: "document",
46
+ file,
47
+ },
48
+ ],
49
+ });
50
+
51
+ expect(result.text).toContain('name="bad&quot;name&amp;.md"');
52
+ expect(result.text).not.toContain('name="bad"name&.md"');
53
+ });
54
+
55
+ it("does not include image data in prompt text regardless of file size", async () => {
56
+ // Both small and large images stay in `files` only.
57
+ const smallFile = new File(["small image"], "small.png", {
58
+ type: "image/png",
59
+ });
60
+ const largeFile = new File([new Uint8Array(3 * 1024 * 1024)], "large.png", {
61
+ type: "image/png",
62
+ });
63
+
64
+ for (const file of [smallFile, largeFile]) {
65
+ const result = await buildPromptComposerSubmission({
66
+ text: "",
67
+ attachments: [{ id: file.name, name: file.name, type: "image", file }],
68
+ });
69
+ expect(result.files).toEqual([file]);
70
+ expect(result.text).not.toContain("data:image");
71
+ }
72
+ });
73
+ });