@adecore/agents-react 0.23.1 → 0.24.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 (341) hide show
  1. package/README.md +14 -11
  2. package/dist/agents/AccountDot.js +3 -1
  3. package/dist/agents/accounts.d.ts +3 -2
  4. package/dist/agents/accounts.js +5 -6
  5. package/dist/chat/actions.d.ts +2 -2
  6. package/dist/chat/actions.js +8 -5
  7. package/dist/chat/bookmarks.d.ts +5 -3
  8. package/dist/chat/bookmarks.js +13 -14
  9. package/dist/chat/chat-client.d.ts +1 -0
  10. package/dist/chat/chat-client.js +7 -0
  11. package/dist/chat/composer-send.d.ts +3 -0
  12. package/dist/chat/composer-send.js +22 -0
  13. package/dist/chat/forks.js +22 -5
  14. package/dist/chat/index.d.ts +31 -0
  15. package/dist/chat/index.js +28 -0
  16. package/dist/chat/logic/embedded-code.d.ts +8 -0
  17. package/dist/chat/logic/embedded-code.js +371 -0
  18. package/dist/chat/logic/fork.js +1 -0
  19. package/dist/chat/logic/goal.d.ts +6 -0
  20. package/dist/chat/logic/goal.js +26 -0
  21. package/dist/chat/logic/scrubber.d.ts +1 -1
  22. package/dist/chat/logic/scrubber.js +6 -5
  23. package/dist/chat/logic/thought.d.ts +3 -0
  24. package/dist/chat/logic/thought.js +35 -0
  25. package/dist/chat/logic/thread-cards.js +2 -0
  26. package/dist/chat/logic/timeline.d.ts +5 -0
  27. package/dist/chat/logic/timeline.js +22 -1
  28. package/dist/chat/logic/tool-catalog.d.ts +1 -0
  29. package/dist/chat/logic/tool-catalog.js +3 -1
  30. package/dist/chat/logic/tools.d.ts +14 -0
  31. package/dist/chat/logic/tools.js +57 -0
  32. package/dist/chat/preferences.d.ts +6 -0
  33. package/dist/chat/preferences.js +19 -3
  34. package/dist/chat/reading-width.d.ts +9 -0
  35. package/dist/chat/reading-width.js +27 -0
  36. package/dist/chat/send-key.d.ts +10 -0
  37. package/dist/chat/send-key.js +24 -0
  38. package/dist/chat/ui/BookmarkMarker.js +4 -2
  39. package/dist/chat/ui/BookmarkSubmenu.js +4 -3
  40. package/dist/chat/ui/ChatActivity.js +14 -10
  41. package/dist/chat/ui/ChatReferenceChip.js +3 -4
  42. package/dist/chat/ui/CodeBlock.d.ts +2 -1
  43. package/dist/chat/ui/CodeBlock.js +14 -96
  44. package/dist/chat/ui/Composer.d.ts +1 -1
  45. package/dist/chat/ui/Composer.js +117 -906
  46. package/dist/chat/ui/ComposerInput.js +5 -4
  47. package/dist/chat/ui/EditDiff.js +3 -2
  48. package/dist/chat/ui/GoalDock.d.ts +9 -0
  49. package/dist/chat/ui/GoalDock.js +63 -0
  50. package/dist/chat/ui/LimitState.js +5 -3
  51. package/dist/chat/ui/Markdown.d.ts +2 -1
  52. package/dist/chat/ui/Markdown.js +70 -31
  53. package/dist/chat/ui/MermaidBlock.d.ts +3 -0
  54. package/dist/chat/ui/MermaidBlock.js +73 -0
  55. package/dist/chat/ui/MessageActions.js +12 -7
  56. package/dist/chat/ui/Pickers.js +6 -5
  57. package/dist/chat/ui/PromptComposer.js +4 -3
  58. package/dist/chat/ui/RowTime.d.ts +3 -0
  59. package/dist/chat/ui/RowTime.js +12 -0
  60. package/dist/chat/ui/RunSettings.js +5 -4
  61. package/dist/chat/ui/ShellCommand.d.ts +3 -0
  62. package/dist/chat/ui/ShellCommand.js +110 -0
  63. package/dist/chat/ui/SubagentInfo.js +4 -3
  64. package/dist/chat/ui/SubagentStopButton.js +4 -3
  65. package/dist/chat/ui/SubagentTimeline.js +4 -1
  66. package/dist/chat/ui/Timeline.js +70 -40
  67. package/dist/chat/ui/TimelineMenu.d.ts +2 -1
  68. package/dist/chat/ui/TimelineMenu.js +18 -8
  69. package/dist/chat/ui/UnifiedDiff.js +3 -2
  70. package/dist/chat/ui/VisualFrame.js +6 -7
  71. package/dist/chat/ui/code-theme.d.ts +2 -1
  72. package/dist/chat/ui/code-theme.js +4 -4
  73. package/dist/chat/ui/composer/ComposerControls.d.ts +27 -0
  74. package/dist/chat/ui/composer/ComposerControls.js +34 -0
  75. package/dist/chat/ui/composer/ComposerMenu.d.ts +4 -0
  76. package/dist/chat/ui/composer/ComposerMenu.js +46 -0
  77. package/dist/chat/ui/composer/DraftParts.d.ts +18 -0
  78. package/dist/chat/ui/composer/DraftParts.js +25 -0
  79. package/dist/chat/ui/composer/QueuedMessages.d.ts +10 -0
  80. package/dist/chat/ui/composer/QueuedMessages.js +20 -0
  81. package/dist/chat/ui/composer/key-handler.d.ts +20 -0
  82. package/dist/chat/ui/composer/key-handler.js +150 -0
  83. package/dist/chat/ui/composer/keys.d.ts +7 -2
  84. package/dist/chat/ui/composer/keys.js +19 -9
  85. package/dist/chat/ui/composer/placeholder.d.ts +9 -0
  86. package/dist/chat/ui/composer/placeholder.js +38 -0
  87. package/dist/chat/ui/composer/use-chat-controls.d.ts +28 -0
  88. package/dist/chat/ui/composer/use-chat-controls.js +112 -0
  89. package/dist/chat/ui/composer/use-compaction-offer.d.ts +2 -0
  90. package/dist/chat/ui/composer/use-compaction-offer.js +23 -0
  91. package/dist/chat/ui/composer/use-composer-draft.d.ts +14 -0
  92. package/dist/chat/ui/composer/use-composer-draft.js +39 -0
  93. package/dist/chat/ui/composer/use-composer-menus.d.ts +52 -0
  94. package/dist/chat/ui/composer/use-composer-menus.js +232 -0
  95. package/dist/chat/ui/composer/use-composer-submit.d.ts +32 -0
  96. package/dist/chat/ui/composer/use-composer-submit.js +113 -0
  97. package/dist/chat/ui/composer/use-draft-files.d.ts +27 -0
  98. package/dist/chat/ui/composer/use-draft-files.js +111 -0
  99. package/dist/chat/ui/composer/use-notice.d.ts +6 -0
  100. package/dist/chat/ui/composer/use-notice.js +18 -0
  101. package/dist/chat/ui/composer/use-pending-requests.d.ts +2 -0
  102. package/dist/chat/ui/composer/use-pending-requests.js +22 -0
  103. package/dist/chat/ui/composer/use-prompt-history.d.ts +8 -0
  104. package/dist/chat/ui/composer/use-prompt-history.js +57 -0
  105. package/dist/chat/ui/composer/use-prompt-hold.d.ts +6 -0
  106. package/dist/chat/ui/composer/use-prompt-hold.js +48 -0
  107. package/dist/chat/ui/composer/use-take-back.d.ts +13 -0
  108. package/dist/chat/ui/composer/use-take-back.js +82 -0
  109. package/dist/chat/ui/diff/EditorEditDiff.js +4 -3
  110. package/dist/chat/ui/diff/LegacyDiffPool.js +3 -2
  111. package/dist/chat/ui/diff/LegacyEditDiff.js +3 -2
  112. package/dist/chat/ui/diff/LegacyUnifiedDiff.js +3 -2
  113. package/dist/chat/ui/diff/syntax-runtime.d.ts +2 -1
  114. package/dist/chat/ui/diff/syntax-runtime.js +9 -5
  115. package/dist/chat/ui/diff-syntax.d.ts +2 -2
  116. package/dist/chat/ui/diff-syntax.js +1 -2
  117. package/dist/chat/ui/diff-theme.js +10 -9
  118. package/dist/chat/ui/file-links.d.ts +1 -1
  119. package/dist/chat/ui/file-links.js +7 -5
  120. package/dist/chat/ui/highlighter.d.ts +27 -0
  121. package/dist/chat/ui/highlighter.js +151 -0
  122. package/dist/chat/ui/index.d.ts +67 -0
  123. package/dist/chat/ui/index.js +64 -0
  124. package/dist/chat/ui/intelligent-ui/UiReply.js +3 -2
  125. package/dist/chat/ui/intelligent-ui/renderers/content.js +3 -2
  126. package/dist/chat/ui/intelligent-ui/use-ui-links.js +2 -2
  127. package/dist/chat/ui/intelligent-ui/use-ui-queries.js +4 -3
  128. package/dist/chat/ui/markdown-blocks.d.ts +8 -0
  129. package/dist/chat/ui/markdown-blocks.js +260 -12
  130. package/dist/chat/ui/mermaid.d.ts +23 -0
  131. package/dist/chat/ui/mermaid.js +93 -0
  132. package/dist/chat/ui/rows/ForksRow.js +5 -3
  133. package/dist/chat/ui/rows/GeneratedImageRow.js +6 -4
  134. package/dist/chat/ui/rows/MessageRows.d.ts +4 -0
  135. package/dist/chat/ui/rows/MessageRows.js +36 -13
  136. package/dist/chat/ui/rows/Rows.js +3 -1
  137. package/dist/chat/ui/rows/SubagentRow.js +3 -2
  138. package/dist/chat/ui/rows/VisualRow.js +3 -2
  139. package/dist/chat/ui/rows/WorkRows.d.ts +6 -3
  140. package/dist/chat/ui/rows/WorkRows.js +57 -20
  141. package/dist/chat/ui/rows/WorkflowRow.js +3 -2
  142. package/dist/chat/ui/timestamp-format.d.ts +1 -0
  143. package/dist/chat/ui/timestamp-format.js +2 -0
  144. package/dist/chat/ui/use-diff-source-tokens.js +6 -3
  145. package/dist/chat/ui/visual-theme.js +3 -2
  146. package/dist/host.d.ts +9 -6
  147. package/dist/host.js +19 -8
  148. package/dist/index.d.ts +8 -0
  149. package/dist/index.js +9 -0
  150. package/dist/locales/en/agent-chat.d.ts +658 -0
  151. package/dist/locales/en/agent-chat.js +657 -0
  152. package/dist/locales/en/agent-prompts.d.ts +44 -0
  153. package/dist/locales/en/agent-prompts.js +43 -0
  154. package/dist/locales/en/agent-providers.d.ts +132 -0
  155. package/dist/locales/en/agent-providers.js +131 -0
  156. package/dist/locales/en/agent-usage.d.ts +132 -0
  157. package/dist/locales/en/agent-usage.js +131 -0
  158. package/dist/locales/nl/agent-chat.d.ts +658 -0
  159. package/dist/locales/nl/agent-chat.js +657 -0
  160. package/dist/locales/nl/agent-prompts.d.ts +44 -0
  161. package/dist/locales/nl/agent-prompts.js +43 -0
  162. package/dist/locales/nl/agent-providers.d.ts +132 -0
  163. package/dist/locales/nl/agent-providers.js +131 -0
  164. package/dist/locales/nl/agent-usage.d.ts +132 -0
  165. package/dist/locales/nl/agent-usage.js +131 -0
  166. package/dist/locales.js +9 -8
  167. package/dist/prompts/index.d.ts +11 -0
  168. package/dist/prompts/index.js +12 -0
  169. package/dist/prompts/logic/subjects.d.ts +2 -1
  170. package/dist/prompts/logic/subjects.js +6 -4
  171. package/dist/prompts/ui/ApprovalBody.js +6 -4
  172. package/dist/prompts/ui/PromptCard.js +5 -4
  173. package/dist/prompts/ui/PromptView.js +3 -2
  174. package/dist/prompts/ui/QuestionBody.js +3 -2
  175. package/dist/providers/AccountColors.js +2 -2
  176. package/dist/providers/AccountDetail.js +7 -6
  177. package/dist/providers/AccountVariables.js +3 -1
  178. package/dist/providers/AddAccountForm.js +3 -2
  179. package/dist/providers/ProvidersPane.js +4 -3
  180. package/dist/providers/account-actions.d.ts +4 -3
  181. package/dist/providers/account-actions.js +6 -7
  182. package/dist/providers/index.d.ts +16 -0
  183. package/dist/providers/index.js +17 -0
  184. package/dist/providers/provider-abilities.d.ts +2 -1
  185. package/dist/providers/provider-abilities.js +4 -0
  186. package/dist/state/chats.js +4 -2
  187. package/dist/state/index.d.ts +4 -0
  188. package/dist/state/index.js +5 -0
  189. package/dist/testing/index.d.ts +1 -0
  190. package/dist/testing/index.js +2 -0
  191. package/dist/{prompts/logic/prompts.fixtures.d.ts → testing/prompts.d.ts} +1 -1
  192. package/dist/theme.css +13 -3
  193. package/dist/usage/UsageBreakdown.js +3 -2
  194. package/dist/usage/UsageLimits.js +4 -3
  195. package/dist/usage/UsagePage.js +3 -2
  196. package/dist/usage/index.d.ts +15 -0
  197. package/dist/usage/index.js +15 -0
  198. package/package.json +53 -1031
  199. package/src/agents/AccountDot.tsx +3 -1
  200. package/src/agents/accounts.ts +6 -6
  201. package/src/chat/actions.ts +8 -5
  202. package/src/chat/bookmarks.ts +23 -14
  203. package/src/chat/chat-client.ts +8 -0
  204. package/src/chat/composer-send.ts +27 -0
  205. package/src/chat/forks.ts +27 -6
  206. package/src/chat/index.ts +98 -0
  207. package/src/chat/logic/embedded-code.ts +409 -0
  208. package/src/chat/logic/fork.ts +1 -0
  209. package/src/chat/logic/goal.ts +33 -0
  210. package/src/chat/logic/scrubber.ts +6 -5
  211. package/src/chat/logic/thought.ts +40 -0
  212. package/src/chat/logic/thread-cards.ts +2 -0
  213. package/src/chat/logic/timeline.ts +25 -1
  214. package/src/chat/logic/tool-catalog.ts +5 -1
  215. package/src/chat/logic/tools.ts +76 -0
  216. package/src/chat/preferences.ts +26 -3
  217. package/src/chat/reading-width.ts +39 -0
  218. package/src/chat/send-key.ts +38 -0
  219. package/src/chat/ui/BookmarkMarker.tsx +4 -2
  220. package/src/chat/ui/BookmarkSubmenu.tsx +4 -3
  221. package/src/chat/ui/ChatActivity.tsx +14 -10
  222. package/src/chat/ui/ChatReferenceChip.tsx +3 -4
  223. package/src/chat/ui/CodeBlock.tsx +33 -124
  224. package/src/chat/ui/Composer.tsx +162 -1324
  225. package/src/chat/ui/ComposerInput.tsx +5 -4
  226. package/src/chat/ui/EditDiff.tsx +3 -2
  227. package/src/chat/ui/GoalDock.tsx +104 -0
  228. package/src/chat/ui/LimitState.tsx +5 -3
  229. package/src/chat/ui/Markdown.tsx +113 -39
  230. package/src/chat/ui/MermaidBlock.tsx +119 -0
  231. package/src/chat/ui/MessageActions.tsx +13 -8
  232. package/src/chat/ui/Pickers.tsx +8 -7
  233. package/src/chat/ui/PromptComposer.tsx +4 -3
  234. package/src/chat/ui/RowTime.tsx +21 -0
  235. package/src/chat/ui/RunSettings.tsx +9 -4
  236. package/src/chat/ui/ShellCommand.tsx +134 -0
  237. package/src/chat/ui/SubagentInfo.tsx +4 -3
  238. package/src/chat/ui/SubagentStopButton.tsx +4 -3
  239. package/src/chat/ui/SubagentTimeline.tsx +5 -1
  240. package/src/chat/ui/Timeline.tsx +167 -105
  241. package/src/chat/ui/TimelineMenu.tsx +42 -7
  242. package/src/chat/ui/UnifiedDiff.tsx +3 -2
  243. package/src/chat/ui/VisualFrame.tsx +6 -7
  244. package/src/chat/ui/code-theme.ts +4 -4
  245. package/src/chat/ui/composer/ComposerControls.tsx +147 -0
  246. package/src/chat/ui/composer/ComposerMenu.tsx +138 -0
  247. package/src/chat/ui/composer/DraftParts.tsx +86 -0
  248. package/src/chat/ui/composer/QueuedMessages.tsx +64 -0
  249. package/src/chat/ui/composer/key-handler.ts +174 -0
  250. package/src/chat/ui/composer/keys.ts +32 -10
  251. package/src/chat/ui/composer/placeholder.ts +48 -0
  252. package/src/chat/ui/composer/use-chat-controls.ts +134 -0
  253. package/src/chat/ui/composer/use-compaction-offer.ts +25 -0
  254. package/src/chat/ui/composer/use-composer-draft.ts +71 -0
  255. package/src/chat/ui/composer/use-composer-menus.ts +289 -0
  256. package/src/chat/ui/composer/use-composer-submit.ts +163 -0
  257. package/src/chat/ui/composer/use-draft-files.ts +135 -0
  258. package/src/chat/ui/composer/use-notice.ts +28 -0
  259. package/src/chat/ui/composer/use-pending-requests.ts +23 -0
  260. package/src/chat/ui/composer/use-prompt-history.ts +76 -0
  261. package/src/chat/ui/composer/use-prompt-hold.ts +58 -0
  262. package/src/chat/ui/composer/use-take-back.ts +91 -0
  263. package/src/chat/ui/diff/DiffFallbackBoundary.tsx +2 -2
  264. package/src/chat/ui/diff/EditorEditDiff.tsx +4 -3
  265. package/src/chat/ui/diff/LegacyDiffPool.tsx +3 -2
  266. package/src/chat/ui/diff/LegacyEditDiff.tsx +3 -2
  267. package/src/chat/ui/diff/LegacyUnifiedDiff.tsx +3 -2
  268. package/src/chat/ui/diff/syntax-runtime.ts +10 -5
  269. package/src/chat/ui/diff-syntax.ts +2 -2
  270. package/src/chat/ui/diff-theme.ts +10 -9
  271. package/src/chat/ui/file-links.ts +7 -5
  272. package/src/chat/ui/highlighter.ts +185 -0
  273. package/src/chat/ui/index.ts +109 -0
  274. package/src/chat/ui/intelligent-ui/UiReply.tsx +5 -4
  275. package/src/chat/ui/intelligent-ui/renderers/content.tsx +3 -2
  276. package/src/chat/ui/intelligent-ui/use-ui-links.ts +2 -2
  277. package/src/chat/ui/intelligent-ui/use-ui-queries.ts +4 -3
  278. package/src/chat/ui/markdown-blocks.ts +300 -12
  279. package/src/chat/ui/mermaid.ts +118 -0
  280. package/src/chat/ui/rows/ForksRow.tsx +5 -3
  281. package/src/chat/ui/rows/GeneratedImageRow.tsx +6 -4
  282. package/src/chat/ui/rows/MessageRows.tsx +73 -18
  283. package/src/chat/ui/rows/Rows.tsx +14 -1
  284. package/src/chat/ui/rows/SubagentRow.tsx +3 -2
  285. package/src/chat/ui/rows/VisualRow.tsx +3 -2
  286. package/src/chat/ui/rows/WorkRows.tsx +148 -47
  287. package/src/chat/ui/rows/WorkflowRow.tsx +3 -2
  288. package/src/chat/ui/timestamp-format.ts +2 -0
  289. package/src/chat/ui/use-diff-source-tokens.ts +10 -4
  290. package/src/chat/ui/visual-theme.ts +3 -2
  291. package/src/host.ts +31 -15
  292. package/src/index.ts +49 -0
  293. package/src/locales/en/agent-chat.ts +657 -0
  294. package/src/locales/en/agent-prompts.ts +43 -0
  295. package/src/locales/en/agent-providers.ts +131 -0
  296. package/src/locales/en/agent-usage.ts +131 -0
  297. package/src/locales/nl/agent-chat.ts +657 -0
  298. package/src/locales/nl/agent-prompts.ts +43 -0
  299. package/src/locales/nl/agent-providers.ts +131 -0
  300. package/src/locales/nl/agent-usage.ts +132 -0
  301. package/src/locales.ts +9 -8
  302. package/src/prompts/index.ts +35 -0
  303. package/src/prompts/logic/subjects.ts +12 -4
  304. package/src/prompts/ui/ApprovalBody.tsx +6 -4
  305. package/src/prompts/ui/PromptCard.tsx +11 -4
  306. package/src/prompts/ui/PromptView.tsx +3 -2
  307. package/src/prompts/ui/QuestionBody.tsx +3 -2
  308. package/src/providers/AccountColors.tsx +2 -2
  309. package/src/providers/AccountDetail.tsx +13 -7
  310. package/src/providers/AccountVariables.tsx +3 -1
  311. package/src/providers/AddAccountForm.tsx +3 -2
  312. package/src/providers/ProvidersPane.tsx +4 -3
  313. package/src/providers/account-actions.ts +7 -7
  314. package/src/providers/index.ts +17 -0
  315. package/src/providers/provider-abilities.ts +6 -1
  316. package/src/state/chats.ts +4 -2
  317. package/src/state/index.ts +39 -0
  318. package/src/testing/index.ts +2 -0
  319. package/src/{prompts/logic/prompts.fixtures.ts → testing/prompts.ts} +1 -1
  320. package/src/theme.css +13 -3
  321. package/src/usage/UsageBreakdown.tsx +3 -2
  322. package/src/usage/UsageLimits.tsx +4 -3
  323. package/src/usage/UsagePage.tsx +3 -2
  324. package/src/usage/index.ts +16 -0
  325. package/dist/locales/en/agent-chat.json +0 -597
  326. package/dist/locales/en/agent-prompts.json +0 -43
  327. package/dist/locales/en/agent-providers.json +0 -131
  328. package/dist/locales/en/agent-usage.json +0 -131
  329. package/dist/locales/nl/agent-chat.json +0 -597
  330. package/dist/locales/nl/agent-prompts.json +0 -43
  331. package/dist/locales/nl/agent-providers.json +0 -131
  332. package/dist/locales/nl/agent-usage.json +0 -131
  333. package/src/locales/en/agent-chat.json +0 -597
  334. package/src/locales/en/agent-prompts.json +0 -43
  335. package/src/locales/en/agent-providers.json +0 -131
  336. package/src/locales/en/agent-usage.json +0 -131
  337. package/src/locales/nl/agent-chat.json +0 -597
  338. package/src/locales/nl/agent-prompts.json +0 -43
  339. package/src/locales/nl/agent-providers.json +0 -131
  340. package/src/locales/nl/agent-usage.json +0 -131
  341. /package/dist/{prompts/logic/prompts.fixtures.js → testing/prompts.js} +0 -0
@@ -1,11 +1,15 @@
1
1
  import type { SyntaxNode, Tree } from '@lezer/common';
2
+ import { modSends, type ChatSendKey } from '../../send-key';
2
3
 
3
- export type EnterAction = 'send' | 'newline' | 'continue-list' | 'leave-list';
4
+ /* `send-alternate` sends with the delivery a running turn does not get by default; `send-and-new` also opens a new chat once it went. */
5
+ export type EnterAction = 'send' | 'send-alternate' | 'send-and-new' | 'newline' | 'continue-list' | 'leave-list';
4
6
 
5
7
  export interface EnterKeys {
6
8
  shift: boolean;
7
9
  /* Cmd on macOS, Ctrl elsewhere. */
8
10
  mod: boolean;
11
+ /* Option on macOS. */
12
+ alt?: boolean;
9
13
  }
10
14
 
11
15
  export interface ListItem {
@@ -23,6 +27,12 @@ export interface EnterContext {
23
27
  list: ListItem | null;
24
28
  /* The caret's offset in its line. */
25
29
  column: number;
30
+ /* Which key sends; `enter` when left out. */
31
+ sendKey?: ChatSendKey;
32
+ /* The message runs over more than one line, where `mod-enter-multiline` lets Enter add a line. */
33
+ multiline?: boolean;
34
+ /* The host opens a new chat after a send, which Mod+Alt+Enter asks for. */
35
+ startsNewChat?: boolean;
26
36
  }
27
37
 
28
38
  const LIST_ITEM = /^([ \t]*)(?:([-*+])|(\d{1,9})([.)]))([ \t]+)(\[[ xX]\](?:[ \t]+|$))?/;
@@ -41,23 +51,35 @@ export function listItemAt(line: string): ListItem | null {
41
51
  /*
42
52
  * Enter sends, except where whoever types is still writing: inside a fence nobody has closed yet
43
53
  * it adds a line, and in a list it starts the next item, or leaves the list from an empty one, so
44
- * a second Enter breaks out. Shift adds a plain line and Mod sends from anywhere.
54
+ * a second Enter breaks out. Shift adds a plain line and Mod sends from anywhere. Where the send key
55
+ * is Mod+Enter, Enter adds a line instead of sending. Alt sends the other way (queue or steer), and so
56
+ * does Mod+Shift+Enter where Mod+Enter is the send key; Mod+Alt+Enter is the send that opens a new chat.
45
57
  */
46
- export function enterAction(keys: EnterKeys, { inOpenFence, list, column }: EnterContext): EnterAction {
58
+ export function enterAction(
59
+ keys: EnterKeys,
60
+ { inOpenFence, list, column, sendKey = 'enter', multiline = false, startsNewChat = false }: EnterContext
61
+ ): EnterAction {
62
+ const modOnly = modSends(sendKey, multiline);
47
63
  if (keys.mod) {
48
- return 'send';
64
+ if (keys.alt && startsNewChat) {
65
+ return 'send-and-new';
66
+ }
67
+ return keys.alt || (modOnly && keys.shift) ? 'send-alternate' : 'send';
49
68
  }
50
69
  if (keys.shift || inOpenFence) {
51
70
  return 'newline';
52
71
  }
53
- if (list === null) {
54
- return 'send';
72
+ if (list !== null) {
73
+ // A caret in front of the marker moves the item down rather than splitting its marker.
74
+ if (column < list.prefix.length) {
75
+ return 'newline';
76
+ }
77
+ return list.empty ? 'leave-list' : 'continue-list';
55
78
  }
56
- // A caret in front of the marker moves the item down rather than splitting its marker.
57
- if (column < list.prefix.length) {
58
- return 'newline';
79
+ if (keys.alt) {
80
+ return 'send-alternate';
59
81
  }
60
- return list.empty ? 'leave-list' : 'continue-list';
82
+ return modOnly ? 'newline' : 'send';
61
83
  }
62
84
 
63
85
  /* The innermost fenced code block around `pos`, if any. */
@@ -0,0 +1,48 @@
1
+ import { useMemo } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { useChatHost } from '../../../host';
4
+ import { useChatScope } from '../../../scope';
5
+
6
+ /* The empty box names its sigils as key caps, so they read as keys to press rather than as punctuation. */
7
+ function hintedPlaceholder(lead: string, joiner: string, hints: ReadonlyArray<{ key: string; label: string }>): HTMLElement {
8
+ const root = document.createElement('span');
9
+ root.className = 'composer-hints';
10
+ const extra = document.createElement('span');
11
+ extra.className = 'composer-hints-extra';
12
+ extra.append(joiner);
13
+ for (const hint of hints) {
14
+ const cap = document.createElement('kbd');
15
+ cap.textContent = hint.key;
16
+ extra.append(cap, hint.label);
17
+ }
18
+ root.append(lead, extra);
19
+ return root;
20
+ }
21
+
22
+ interface PlaceholderInput {
23
+ chatId: string;
24
+ disabled: boolean;
25
+ readOnly: boolean;
26
+ mentionable: boolean;
27
+ hasSkills: boolean;
28
+ }
29
+
30
+ /* What the empty box says: why nothing can be written, or the host's words and the keys that open a picker. */
31
+ export function useHintedPlaceholder({ chatId, disabled, readOnly, mentionable, hasSkills }: PlaceholderInput): string | HTMLElement {
32
+ const { t } = useTranslation('agent-chat');
33
+ const scope = useChatScope();
34
+ const lead = useChatHost().useComposerPlaceholder(scope.id, chatId);
35
+ return useMemo(() => {
36
+ if (disabled) {
37
+ return t('composer.placeholder.disconnected');
38
+ }
39
+ if (readOnly) {
40
+ return t('composer.placeholder.readOnly');
41
+ }
42
+ return hintedPlaceholder(lead ?? t('composer.placeholder.lead'), t('composer.placeholder.joiner'), [
43
+ { key: '/', label: t('composer.placeholder.commands') },
44
+ ...(mentionable ? [{ key: '@', label: t('composer.placeholder.files') }] : []),
45
+ ...(hasSkills ? [{ key: '$', label: t('composer.placeholder.skills') }] : [])
46
+ ]);
47
+ }, [disabled, readOnly, lead, mentionable, hasSkills, t]);
48
+ }
@@ -0,0 +1,134 @@
1
+ import { useRef, useState } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import type { AgentKind, ChatInfo, ModelSelection, RuntimeMode } from '@adecore/agent-contracts';
4
+ import { useChatHost } from '../../../host';
5
+ import { useChatScope } from '../../../scope';
6
+ import { errorCode } from '../../../transport';
7
+ import { useChatActions } from '../../actions';
8
+ import { rememberChatAccount, rememberChatPreferences, rememberChatSelection } from '../../preferences';
9
+ import { composerStopOf } from '../../subagent-list';
10
+ import type { ComposerNotice } from './use-notice';
11
+
12
+ interface ControlsInput {
13
+ chatId: string;
14
+ info: ChatInfo;
15
+ notice: ComposerNotice;
16
+ onRetarget(provider: AgentKind, selection: ModelSelection): Promise<void>;
17
+ }
18
+
19
+ /* What the composer does to the chat rather than to its draft: its settings, a stop, a clear and a compaction. */
20
+ export function useChatControls({ chatId, info, notice: { setNotice, showError }, onRetarget }: ControlsInput) {
21
+ const { t } = useTranslation('agent-chat');
22
+ const host = useChatHost();
23
+ const scope = useChatScope();
24
+ const actions = useChatActions();
25
+ const configuring = useRef(false);
26
+ const [configurationPending, setConfigurationPending] = useState(false);
27
+ const [modelPickerOpen, setModelPickerOpen] = useState(false);
28
+ const [confirmClear, setConfirmClear] = useState(false);
29
+
30
+ /* One configuration at a time; a second pick while one is on its way is dropped. */
31
+ const runConfiguration = (work: () => Promise<void>): void => {
32
+ if (configuring.current) {
33
+ return;
34
+ }
35
+ configuring.current = true;
36
+ setConfigurationPending(true);
37
+ void work()
38
+ .catch(showError)
39
+ .finally(() => {
40
+ configuring.current = false;
41
+ setConfigurationPending(false);
42
+ });
43
+ };
44
+
45
+ const configure = (patch: { selection?: ModelSelection; runtimeMode?: RuntimeMode; account?: string }): void => {
46
+ runConfiguration(() =>
47
+ actions.configure(chatId, patch).then(() => {
48
+ if (patch.selection) {
49
+ rememberChatSelection(info.provider, patch.selection);
50
+ }
51
+ if (patch.runtimeMode) {
52
+ rememberChatPreferences({ runtimeMode: patch.runtimeMode });
53
+ }
54
+ if (patch.account) {
55
+ rememberChatAccount(scope.id, info.provider, patch.account);
56
+ }
57
+ })
58
+ );
59
+ };
60
+
61
+ /* A model of another CLI re-points the whole chat; one of this CLI's own is a configure. */
62
+ const chooseModel = (provider: AgentKind, slug: string): void => {
63
+ const selection: ModelSelection = { model: slug, options: {} };
64
+ if (provider !== info.provider) {
65
+ runConfiguration(() => onRetarget(provider, selection).then(() => rememberChatSelection(provider, selection)));
66
+ return;
67
+ }
68
+ configure({ selection });
69
+ };
70
+
71
+ const stop = (shiftKey: boolean): void => {
72
+ if (composerStopOf(shiftKey) === 'turn') {
73
+ void actions.stopTurn(chatId, false).catch(showError);
74
+ return;
75
+ }
76
+ host.confirm.stopSubagents(scope.id, chatId, () => void actions.stopTurn(chatId, true).catch(showError));
77
+ };
78
+
79
+ /* The host decides whether a turn is in the way; only its refusal asks the person first. */
80
+ const clearThread = async (): Promise<void> => {
81
+ try {
82
+ await actions.clear(chatId, false);
83
+ } catch (e) {
84
+ if (errorCode(e) === 'chat-busy') {
85
+ setConfirmClear(true);
86
+ return;
87
+ }
88
+ setNotice(t('composer.notice.clearFailed'));
89
+ }
90
+ };
91
+
92
+ const forceClearThread = async (): Promise<void> => {
93
+ await actions.clear(chatId, true);
94
+ setConfirmClear(false);
95
+ };
96
+
97
+ const compact = (): void => {
98
+ void actions.compact(chatId).catch(() => setNotice(t('composer.notice.compactFailed')));
99
+ };
100
+
101
+ /* Runs one of the composer's own commands; false for a name it does not handle. */
102
+ const runCommand = (name: string): boolean => {
103
+ switch (name) {
104
+ case 'model':
105
+ setModelPickerOpen(true);
106
+ return true;
107
+ case 'compact':
108
+ void actions.compact(chatId).catch(() => undefined);
109
+ return true;
110
+ case 'clear':
111
+ void clearThread();
112
+ return true;
113
+ default:
114
+ return false;
115
+ }
116
+ };
117
+
118
+ return {
119
+ configurationPending,
120
+ configure,
121
+ chooseModel,
122
+ chooseAccount: (account: string): void => configure({ account }),
123
+ stop,
124
+ compact,
125
+ runCommand,
126
+ modelPickerOpen,
127
+ setModelPickerOpen,
128
+ confirmClear,
129
+ cancelClear: (): void => setConfirmClear(false),
130
+ forceClearThread
131
+ };
132
+ }
133
+
134
+ export type ChatControls = ReturnType<typeof useChatControls>;
@@ -0,0 +1,25 @@
1
+ import type { ChatInfo, ChatItem, ProviderCapabilities } from '@adecore/agent-contracts';
2
+ import { useNow } from '@adecore/ui';
3
+ import { useChatRow } from '../../../state/chats';
4
+ import { RESUME_COMPACTION_TOKENS, resumeCompactionOffer } from '../../logic/resume-compaction';
5
+ import { useResumeCompactionDismissal } from '../../resume-compaction-dismissals';
6
+
7
+ /* The offer to fold a long context before a chat that sat idle goes on, or null when there is none. */
8
+ export function useCompactionOffer(chatId: string, info: ChatInfo, compaction: ProviderCapabilities['compaction'] | undefined) {
9
+ const order = useChatRow(chatId, (row) => row?.order);
10
+ const items = useChatRow(chatId, (row) => row?.structure);
11
+ const dismissedTurnId = useResumeCompactionDismissal(chatId);
12
+ // The cheap half of the offer, so a chat that can never make one keeps no timer.
13
+ const couldOffer = compaction !== undefined && compaction !== 'none' && info.usage.contextTokens >= RESUME_COMPACTION_TOKENS;
14
+ const now = useNow(60_000, couldOffer);
15
+ if (!couldOffer) {
16
+ return null;
17
+ }
18
+ return resumeCompactionOffer({
19
+ info,
20
+ compaction,
21
+ items: (order ?? []).map((id) => items?.[id]).filter((item): item is ChatItem => item !== undefined),
22
+ dismissedTurnId,
23
+ now
24
+ });
25
+ }
@@ -0,0 +1,71 @@
1
+ import { useContext, useEffect, useRef, useState, type Dispatch, type RefObject, type SetStateAction } from 'react';
2
+ import { EMPTY_DRAFT, joinDraftText, readDraft, takeDraftOffers, writeDraft, type ChatDraft } from '../../drafts';
3
+ import type { ComposerInputHandle } from '../ComposerInput';
4
+ import { QuoteTakerContext } from '../quote-selection';
5
+
6
+ export interface ComposerDraft {
7
+ draft: ChatDraft;
8
+ setDraft: Dispatch<SetStateAction<ChatDraft>>;
9
+ /* Taking a queued message back waits on the machine, and what was typed meanwhile is what it merges with. */
10
+ latest: RefObject<ChatDraft>;
11
+ /* The mentions and skills default to the draft's own as last rendered. */
12
+ setText(next: string, mentions?: string[], skills?: string[]): void;
13
+ clear(): void;
14
+ removeQuote(): void;
15
+ removeChat(id: string): void;
16
+ removeAttachment(index: number): void;
17
+ }
18
+
19
+ /* The chat's unsent prompt, kept in storage as it changes and open to text and quotes handed in from outside. */
20
+ export function useComposerDraft(chatId: string, inputRef: RefObject<ComposerInputHandle | null>): ComposerDraft {
21
+ const [draft, setDraft] = useState<ChatDraft>(() => readDraft(chatId));
22
+ const latest = useRef(draft);
23
+ const registerQuoteTaker = useContext(QuoteTakerContext);
24
+
25
+ useEffect(() => {
26
+ latest.current = draft;
27
+ writeDraft(chatId, draft);
28
+ }, [chatId, draft]);
29
+
30
+ useEffect(
31
+ () =>
32
+ takeDraftOffers(chatId, (offered) => {
33
+ setDraft((current) => ({ ...current, text: joinDraftText(current.text, offered) }));
34
+ inputRef.current?.focus();
35
+ }),
36
+ [chatId, inputRef]
37
+ );
38
+
39
+ useEffect(
40
+ () =>
41
+ registerQuoteTaker?.((selected, itemId) => {
42
+ setDraft((current) => ({ ...current, quote: selected, quoteSource: itemId }));
43
+ inputRef.current?.focus();
44
+ }),
45
+ [registerQuoteTaker, inputRef]
46
+ );
47
+
48
+ const setText = (next: string, mentions = draft.mentions, chosen = draft.skills): void => {
49
+ setDraft((current) => ({ ...current, text: next, mentions, skills: chosen }));
50
+ };
51
+
52
+ const clear = (): void => {
53
+ setDraft(EMPTY_DRAFT);
54
+ };
55
+
56
+ const removeQuote = (): void => {
57
+ setDraft((current) => ({ ...current, quote: '', quoteSource: null }));
58
+ inputRef.current?.focus();
59
+ };
60
+
61
+ const removeChat = (id: string): void => {
62
+ setDraft((current) => ({ ...current, chats: current.chats.filter((entry) => entry !== id) }));
63
+ inputRef.current?.focus();
64
+ };
65
+
66
+ const removeAttachment = (index: number): void => {
67
+ setDraft((current) => ({ ...current, attachments: current.attachments.filter((_, i) => i !== index) }));
68
+ };
69
+
70
+ return { draft, setDraft, latest, setText, clear, removeQuote, removeChat, removeAttachment };
71
+ }
@@ -0,0 +1,289 @@
1
+ import { useEffect, useId, useMemo, useState, type RefObject } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { ensureSyntaxTree, syntaxTree } from '@codemirror/language';
4
+ import type { EditorState } from '@codemirror/state';
5
+ import type { ChatInfo, ChatSkill, ProviderCapabilities } from '@adecore/agent-contracts';
6
+ import { useChatHost } from '../../../host';
7
+ import { useChatScope } from '../../../scope';
8
+ import { chatSuggestions } from '../../chat-references';
9
+ import { usableSlashCommands } from '../../guards';
10
+ import {
11
+ dropQuery,
12
+ findMentionQuery,
13
+ findSkillQuery,
14
+ insertMention,
15
+ insertSkill,
16
+ mentionPick,
17
+ NO_MENTION_SEARCH,
18
+ type MentionQuery,
19
+ type MentionSearch,
20
+ type TextRange
21
+ } from '../../mentions';
22
+ import type { ComposerInputHandle } from '../ComposerInput';
23
+ import type { ComposerDraft } from './use-composer-draft';
24
+ import { inCode } from './keys';
25
+ import { clampMenuIndex, composerMenuAria } from './menu-aria';
26
+
27
+ const SEARCH_DEBOUNCE_MS = 80;
28
+ // What fits above the composer without turning the picker into a file tree.
29
+ const MENTION_RESULTS = 8;
30
+ // The chats go above the files, so a few leave the files in view.
31
+ const CHAT_RESULTS = 4;
32
+
33
+ // Commands the composer handles itself; the CLI's own ones are sent through as text.
34
+ export const LOCAL_COMMANDS = ['model', 'compact', 'clear'];
35
+
36
+ export interface ComposerCommand {
37
+ name: string;
38
+ hint: string;
39
+ local: boolean;
40
+ skill: boolean;
41
+ }
42
+
43
+ interface MenusInput {
44
+ chatId: string;
45
+ info: ChatInfo;
46
+ capabilities: ProviderCapabilities | undefined;
47
+ providerName: string | undefined;
48
+ draft: ComposerDraft;
49
+ inputRef: RefObject<ComposerInputHandle | null>;
50
+ }
51
+
52
+ function sameQuery(left: MentionQuery | null, right: MentionQuery | null): boolean {
53
+ return left?.start === right?.start && left?.query === right?.query;
54
+ }
55
+
56
+ /*
57
+ * The pickers over the box: `/` for a command, `$` for a skill and `@` for a file or another chat.
58
+ * One is open at a time, and the arrow keys and the editor's ARIA attributes follow that one.
59
+ */
60
+ export function useComposerMenus({ chatId, info, capabilities, providerName, draft: { draft, setDraft, setText }, inputRef }: MenusInput) {
61
+ const { t } = useTranslation('agent-chat');
62
+ const scope = useChatScope();
63
+ const host = useChatHost();
64
+ const { searchFiles } = host;
65
+ const chats = host.useReferableChats();
66
+ const [menuIndex, setMenuIndex] = useState(0);
67
+ const [mention, setMention] = useState<MentionQuery | null>(null);
68
+ const [skillQuery, setSkillQuery] = useState<MentionQuery | null>(null);
69
+ const [skills, setSkills] = useState<ChatSkill[]>([]);
70
+ const [searched, setSearched] = useState<MentionSearch>(NO_MENTION_SEARCH);
71
+ const text = draft.text;
72
+ const goals = capabilities?.goals;
73
+
74
+ // Every keystroke inside an `@word` searches again; a slow answer must not overwrite a newer one.
75
+ useEffect(() => {
76
+ if (mention === null) {
77
+ return;
78
+ }
79
+ let stale = false;
80
+ const search = (): void => {
81
+ if (searchFiles === null) {
82
+ return;
83
+ }
84
+ searchFiles(scope.id, info.cwd, mention.query, MENTION_RESULTS)
85
+ .then((files) => {
86
+ if (!stale) {
87
+ setSearched({ query: mention.query, files });
88
+ setMenuIndex(0);
89
+ }
90
+ })
91
+ .catch(() => undefined);
92
+ };
93
+ // A bare `@` is not a keystroke in a query. It opens the list of what is there, at once.
94
+ if (mention.query === '') {
95
+ search();
96
+ return () => {
97
+ stale = true;
98
+ };
99
+ }
100
+ const timer = setTimeout(search, SEARCH_DEBOUNCE_MS);
101
+ return () => {
102
+ stale = true;
103
+ clearTimeout(timer);
104
+ };
105
+ }, [scope, info.cwd, mention, searchFiles]);
106
+
107
+ /*
108
+ * The host's skill list, once per chat, again once the CLI announced its own after the first message,
109
+ * and again when its process comes or goes, which is when a restart reads new skills.
110
+ */
111
+ useEffect(() => {
112
+ let stale = false;
113
+ scope.chats
114
+ .listSkills(chatId)
115
+ .then((found) => {
116
+ if (!stale) {
117
+ setSkills(found);
118
+ }
119
+ })
120
+ .catch(() => undefined);
121
+ return () => {
122
+ stale = true;
123
+ };
124
+ }, [scope, chatId, info.skills, info.running]);
125
+
126
+ const commandQuery = text.startsWith('/') && !text.includes('\n') ? text.slice(1).trim().toLowerCase() : null;
127
+ const commands = useMemo((): ComposerCommand[] => {
128
+ if (commandQuery === null) {
129
+ return [];
130
+ }
131
+ const known = new Set(skills.map((skill) => skill.name));
132
+ const own = LOCAL_COMMANDS.map((name) => ({ name, hint: t(`composer.commands.${name}`), local: true, skill: false }));
133
+ // Every CLI with goals reads `/goal`, also one that lists no commands of its own.
134
+ const goal = goals === undefined ? [] : [{ name: 'goal', hint: t('composer.commands.goal'), local: false, skill: false }];
135
+ const cli = usableSlashCommands(info.slashCommands)
136
+ .filter((name) => !LOCAL_COMMANDS.includes(name) && (goals === undefined || name !== 'goal'))
137
+ .map((name) => ({
138
+ name,
139
+ hint: known.has(name) ? t('composer.commands.skill') : t('composer.commands.cli', { cli: providerName ?? t('composer.commands.anyCli') }),
140
+ local: false,
141
+ skill: known.has(name)
142
+ }));
143
+ return [...own, ...goal, ...cli].filter((command) => command.name.startsWith(commandQuery)).slice(0, 8);
144
+ }, [commandQuery, goals, info.slashCommands, providerName, skills, t]);
145
+
146
+ const skillMatches = useMemo(() => {
147
+ if (skillQuery === null) {
148
+ return [];
149
+ }
150
+ const query = skillQuery.query.toLowerCase();
151
+ return skills.filter((skill) => skill.name.toLowerCase().includes(query)).slice(0, 8);
152
+ }, [skillQuery, skills]);
153
+
154
+ const commandMenuOpen = commandQuery !== null && commands.length > 0;
155
+ const skillMenuOpen = !commandMenuOpen && skillQuery !== null && skillMatches.length > 0;
156
+ const mentionMenuOpen = !commandMenuOpen && !skillMenuOpen && mention !== null;
157
+ // Results belong to the query that asked for them; a closed picker shows none while the next answer is on its way.
158
+ const files = mention === null ? [] : searched.files;
159
+ const chatMatches = useMemo(
160
+ () => (mention === null ? [] : chatSuggestions(chats, mention.query, [chatId, ...draft.chats], CHAT_RESULTS)),
161
+ [chatId, chats, draft.chats, mention]
162
+ );
163
+ const mentionCount = chatMatches.length + files.length;
164
+ // One menu is open at a time, so they share an id.
165
+ const listId = useId();
166
+ const menuOpen = commandMenuOpen || skillMenuOpen || mentionMenuOpen;
167
+ const menuCount = commandMenuOpen ? commands.length : skillMenuOpen ? skillMatches.length : mentionCount;
168
+ // The list can shrink under a stale index; every reader takes the one that still points at an option.
169
+ const activeIndex = clampMenuIndex(menuIndex, menuCount);
170
+ const inputAria = useMemo(
171
+ () => composerMenuAria({ label: t('composer.inputLabel'), listId, open: menuOpen, activeIndex, count: menuCount }),
172
+ [t, listId, menuOpen, activeIndex, menuCount]
173
+ );
174
+
175
+ /* Which picker the caret opens: `@` for a file, `$` for a skill, or neither. In code a sigil is just text. */
176
+ const trackTriggers = (value: string, selection: TextRange, state: EditorState): void => {
177
+ const empty = selection.from === selection.to;
178
+ const tree = empty ? (ensureSyntaxTree(state, selection.from, 50) ?? syntaxTree(state)) : null;
179
+ const caret = tree !== null && !inCode(tree, value, selection.from) ? selection.from : null;
180
+ const skill = caret === null ? null : findSkillQuery(value, caret);
181
+ setSkillQuery((current) => (sameQuery(current, skill) ? current : skill));
182
+ // A CLI that does not expand `@path` gets the text as it is, so the picker stays out of the way.
183
+ const next = caret === null || capabilities?.mentions === false ? null : findMentionQuery(value, caret);
184
+ setMention((current) => (sameQuery(current, next) ? current : next));
185
+ };
186
+
187
+ // The inserted text reaches the editor with the render it causes, so the caret waits a frame for it.
188
+ const moveCaret = (caret: number): void => {
189
+ requestAnimationFrame(() => inputRef.current?.setCaret(caret));
190
+ };
191
+
192
+ const close = (): void => {
193
+ setMention(null);
194
+ setSkillQuery(null);
195
+ };
196
+
197
+ const chooseMention = (path: string): void => {
198
+ if (!mention) {
199
+ return;
200
+ }
201
+ const result = insertMention(text, mention, path);
202
+ setText(result.text, draft.mentions.includes(path) ? draft.mentions : [...draft.mentions, path]);
203
+ setMention(null);
204
+ moveCaret(result.caret);
205
+ };
206
+
207
+ /* A picked chat sits beside the text as a chip, so the `@query` that found it goes. */
208
+ const chooseChat = (id: string): void => {
209
+ if (!mention) {
210
+ return;
211
+ }
212
+ const result = dropQuery(text, mention);
213
+ setDraft((current) => ({ ...current, text: result.text, chats: current.chats.includes(id) ? current.chats : [...current.chats, id] }));
214
+ setMention(null);
215
+ moveCaret(result.caret);
216
+ };
217
+
218
+ const chooseMentionAt = (index: number): void => {
219
+ const pick = mention === null ? null : mentionPick(index, chatMatches, searched, mention.query);
220
+ if (pick?.kind === 'chat') {
221
+ chooseChat(pick.chat.id);
222
+ } else if (pick?.kind === 'file') {
223
+ chooseMention(pick.path);
224
+ }
225
+ };
226
+
227
+ /*
228
+ * A picked skill becomes a `$name` chip in the text and a name on the send, which is what
229
+ * dispatches it. The `/` menu lands here too, so both spellings end up on the same path.
230
+ */
231
+ const chooseSkill = (name: string): void => {
232
+ const result = skillQuery ? insertSkill(text, skillQuery, name) : { text: `$${name} `, caret: name.length + 2 };
233
+ setText(result.text, draft.mentions, draft.skills.includes(name) ? draft.skills : [...draft.skills, name]);
234
+ setSkillQuery(null);
235
+ moveCaret(result.caret);
236
+ };
237
+
238
+ /* A clicked command; Tab completes one with a space after it instead. */
239
+ const pickCommand = (command: ComposerCommand, index: number): void => {
240
+ setMenuIndex(index);
241
+ if (command.skill) {
242
+ chooseSkill(command.name);
243
+ return;
244
+ }
245
+ setText(`/${command.name}`);
246
+ inputRef.current?.focus();
247
+ };
248
+
249
+ /* The arrows walk an open picker round; true when the key was one of them. */
250
+ const moveInMenu = (key: string, count: number): boolean => {
251
+ if (key === 'ArrowDown') {
252
+ setMenuIndex((i) => (i + 1) % count);
253
+ return true;
254
+ }
255
+ if (key === 'ArrowUp') {
256
+ setMenuIndex((i) => (i - 1 + count) % count);
257
+ return true;
258
+ }
259
+ return false;
260
+ };
261
+
262
+ return {
263
+ listId,
264
+ activeIndex,
265
+ inputAria,
266
+ hasSkills: skills.length > 0,
267
+ commandQuery,
268
+ commands,
269
+ commandMenuOpen,
270
+ skillMatches,
271
+ skillMenuOpen,
272
+ mention,
273
+ mentionMenuOpen,
274
+ mentionCount,
275
+ chatMatches,
276
+ files,
277
+ setMenuIndex,
278
+ trackTriggers,
279
+ close,
280
+ chooseMention,
281
+ chooseChat,
282
+ chooseMentionAt,
283
+ chooseSkill,
284
+ pickCommand,
285
+ moveInMenu
286
+ };
287
+ }
288
+
289
+ export type ComposerMenus = ReturnType<typeof useComposerMenus>;