@adecore/agents-react 0.38.0 → 0.40.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 (351) hide show
  1. package/dist/agents/AgentModelLabel.d.ts +10 -0
  2. package/dist/agents/AgentModelLabel.js +98 -0
  3. package/dist/agents/model-name.js +2 -1
  4. package/dist/agents/provider-paths.d.ts +2 -1
  5. package/dist/agents/provider-paths.js +7 -3
  6. package/dist/chat/account-choice.js +57 -28
  7. package/dist/chat/bookmarks.js +2 -1
  8. package/dist/chat/drafts.d.ts +0 -1
  9. package/dist/chat/drafts.js +9 -28
  10. package/dist/chat/logic/picker-entries.js +2 -8
  11. package/dist/chat/logic/resume-compaction.js +2 -5
  12. package/dist/chat/logic/timeline-copy.js +2 -8
  13. package/dist/chat/logic/timeline.js +9 -10
  14. package/dist/chat/logic/workflow.js +2 -4
  15. package/dist/chat/subagent-conversation.js +2 -1
  16. package/dist/chat/ui/Composer.js +385 -360
  17. package/dist/chat/ui/GoalDock.js +2 -1
  18. package/dist/chat/ui/LimitState.js +4 -6
  19. package/dist/chat/ui/Pickers.d.ts +2 -1
  20. package/dist/chat/ui/Pickers.js +126 -123
  21. package/dist/chat/ui/PromptEditor.d.ts +19 -0
  22. package/dist/chat/ui/PromptEditor.js +115 -0
  23. package/dist/chat/ui/Timeline.d.ts +2 -1
  24. package/dist/chat/ui/Timeline.js +13 -1
  25. package/dist/chat/ui/composer/ComposerControls.d.ts +3 -0
  26. package/dist/chat/ui/composer/ComposerControls.js +235 -38
  27. package/dist/chat/ui/composer/key-handler.d.ts +1 -1
  28. package/dist/chat/ui/composer/key-handler.js +3 -11
  29. package/dist/chat/ui/composer/prompt-picks.d.ts +4 -0
  30. package/dist/chat/ui/composer/prompt-picks.js +14 -0
  31. package/dist/chat/ui/composer/use-composer-menus.d.ts +4 -49
  32. package/dist/chat/ui/composer/use-composer-menus.js +65 -519
  33. package/dist/chat/ui/composer/use-continuable-turn.js +0 -1
  34. package/dist/chat/ui/composer/use-notice.js +2 -1
  35. package/dist/chat/ui/composer/use-prompt-history.js +1 -1
  36. package/dist/chat/ui/composer/use-prompt-menus.d.ts +73 -0
  37. package/dist/chat/ui/composer/use-prompt-menus.js +499 -0
  38. package/dist/chat/ui/composer/use-take-back.js +2 -2
  39. package/dist/chat/ui/diff/ApprovalReview.js +1 -1
  40. package/dist/chat/ui/highlighter.d.ts +0 -6
  41. package/dist/chat/ui/highlighter.js +0 -12
  42. package/dist/chat/ui/icons.d.ts +0 -1
  43. package/dist/chat/ui/icons.js +0 -3
  44. package/dist/chat/ui/index.d.ts +3 -0
  45. package/dist/chat/ui/index.js +2 -0
  46. package/dist/chat/ui/rows/MessageRows.js +2 -5
  47. package/dist/chat/ui/rows/row-rhythm.js +2 -5
  48. package/dist/chat/ui/theme-watch.js +3 -7
  49. package/dist/host-hooks.d.ts +2 -1
  50. package/dist/host-hooks.js +6 -0
  51. package/dist/host.d.ts +17 -1
  52. package/dist/index.d.ts +1 -1
  53. package/dist/locales/en/agent-chat.d.ts +1 -0
  54. package/dist/locales/en/agent-chat.js +1 -0
  55. package/dist/locales/nl/agent-chat.d.ts +1 -0
  56. package/dist/locales/nl/agent-chat.js +1 -0
  57. package/dist/prompts/logic/usePromptSession.js +2 -1
  58. package/dist/providers/AddAccountForm.js +2 -1
  59. package/dist/providers/CliDetail.js +104 -130
  60. package/dist/providers/account-actions.js +6 -9
  61. package/dist/providers/index.d.ts +1 -0
  62. package/dist/providers/index.js +1 -0
  63. package/dist/state/chats.d.ts +1 -0
  64. package/dist/state/chats.js +6 -11
  65. package/dist/state/provider-accounts.d.ts +1 -1
  66. package/dist/state/provider-accounts.js +2 -8
  67. package/dist/theme.css +3 -0
  68. package/dist/transport.d.ts +2 -0
  69. package/dist/transport.js +14 -0
  70. package/dist/usage/ChartLogo.d.ts +5 -0
  71. package/dist/usage/ChartLogo.js +48 -0
  72. package/dist/usage/LimitsList.js +6 -16
  73. package/dist/usage/UsageBreakdown.js +17 -40
  74. package/dist/usage/UsageLimits.js +183 -211
  75. package/dist/usage/UsagePage.js +3 -17
  76. package/dist/usage/UsageSummary.js +4 -9
  77. package/dist/usage/limits.js +16 -31
  78. package/dist/usage/summary.d.ts +1 -1
  79. package/dist/usage/summary.js +3 -5
  80. package/package.json +26 -26
  81. package/src/agents/AccountDot.tsx +1 -0
  82. package/src/agents/AgentIcon.tsx +2 -0
  83. package/src/agents/AgentModelLabel.tsx +38 -0
  84. package/src/agents/ModelOptionControl.tsx +8 -0
  85. package/src/agents/account-limits.ts +3 -0
  86. package/src/agents/accounts.ts +12 -0
  87. package/src/agents/model-name.ts +13 -1
  88. package/src/agents/model-options.ts +2 -0
  89. package/src/agents/model-requirement.ts +4 -0
  90. package/src/agents/option-words.ts +2 -0
  91. package/src/agents/provider-paths.ts +8 -3
  92. package/src/agents/status-look.ts +1 -0
  93. package/src/chat/account-choice.ts +25 -19
  94. package/src/chat/actions.ts +5 -0
  95. package/src/chat/attachments.ts +8 -0
  96. package/src/chat/bookmarks.ts +13 -1
  97. package/src/chat/chat-client.ts +63 -0
  98. package/src/chat/chat-references.ts +3 -0
  99. package/src/chat/composer-effort.ts +5 -0
  100. package/src/chat/composer-send.ts +4 -0
  101. package/src/chat/drafts.ts +21 -17
  102. package/src/chat/forks.ts +4 -0
  103. package/src/chat/guards.ts +2 -0
  104. package/src/chat/index.ts +7 -0
  105. package/src/chat/logic/ansi.ts +47 -0
  106. package/src/chat/logic/bookmarks.ts +7 -0
  107. package/src/chat/logic/command-looks.ts +29 -0
  108. package/src/chat/logic/context-usage.ts +3 -0
  109. package/src/chat/logic/continue-after-stop.ts +5 -0
  110. package/src/chat/logic/effort.ts +4 -0
  111. package/src/chat/logic/embedded-code.ts +63 -0
  112. package/src/chat/logic/fork.ts +22 -0
  113. package/src/chat/logic/generated-image.ts +7 -0
  114. package/src/chat/logic/goal.ts +7 -0
  115. package/src/chat/logic/handback.ts +2 -0
  116. package/src/chat/logic/limit.ts +8 -0
  117. package/src/chat/logic/picker-entries.ts +16 -4
  118. package/src/chat/logic/resume-compaction.ts +22 -5
  119. package/src/chat/logic/scrubber.ts +38 -5
  120. package/src/chat/logic/thought.ts +5 -0
  121. package/src/chat/logic/thread-cards.ts +6 -0
  122. package/src/chat/logic/timeline-copy.ts +4 -9
  123. package/src/chat/logic/timeline-target.ts +15 -0
  124. package/src/chat/logic/timeline.ts +113 -10
  125. package/src/chat/logic/tools.ts +30 -0
  126. package/src/chat/logic/visual-height.ts +5 -0
  127. package/src/chat/logic/visual-viewport.ts +1 -0
  128. package/src/chat/logic/welcome.ts +3 -0
  129. package/src/chat/logic/workflow.ts +26 -7
  130. package/src/chat/mentions.ts +56 -0
  131. package/src/chat/pending-prompts.ts +16 -0
  132. package/src/chat/persisted-json.ts +15 -0
  133. package/src/chat/preferences.ts +16 -0
  134. package/src/chat/prompt-hold.ts +2 -0
  135. package/src/chat/quote.ts +7 -0
  136. package/src/chat/reading-width.ts +1 -0
  137. package/src/chat/recent-messages.ts +5 -0
  138. package/src/chat/resume-compaction-dismissals.ts +1 -0
  139. package/src/chat/stash.ts +12 -0
  140. package/src/chat/subagent-conversation.ts +24 -4
  141. package/src/chat/subagent-list.ts +14 -0
  142. package/src/chat/subagent-support.ts +1 -0
  143. package/src/chat/subagent-view.ts +9 -0
  144. package/src/chat/thumbnails.ts +14 -0
  145. package/src/chat/timeline-flash.ts +2 -0
  146. package/src/chat/timeline-scroll.ts +23 -0
  147. package/src/chat/ui/AccountPill.tsx +4 -0
  148. package/src/chat/ui/AnsiOutput.tsx +1 -0
  149. package/src/chat/ui/BookmarkMarker.tsx +5 -0
  150. package/src/chat/ui/BookmarkSubmenu.tsx +1 -0
  151. package/src/chat/ui/ChatActivity.tsx +13 -0
  152. package/src/chat/ui/CodeBlock.tsx +8 -0
  153. package/src/chat/ui/Composer.tsx +27 -3
  154. package/src/chat/ui/ComposerInput.tsx +22 -0
  155. package/src/chat/ui/DiffPool.tsx +3 -0
  156. package/src/chat/ui/EditDiff.tsx +5 -0
  157. package/src/chat/ui/GoalDock.tsx +13 -1
  158. package/src/chat/ui/ImageView.tsx +11 -0
  159. package/src/chat/ui/LimitState.tsx +23 -6
  160. package/src/chat/ui/Markdown.tsx +41 -0
  161. package/src/chat/ui/MermaidBlock.tsx +16 -0
  162. package/src/chat/ui/MessageActions.tsx +8 -0
  163. package/src/chat/ui/ModelMarks.tsx +1 -0
  164. package/src/chat/ui/Pickers.tsx +30 -1
  165. package/src/chat/ui/PromptComposer.tsx +2 -0
  166. package/src/chat/ui/PromptEditor.tsx +136 -0
  167. package/src/chat/ui/QuoteButton.tsx +12 -0
  168. package/src/chat/ui/ReferenceChip.tsx +3 -0
  169. package/src/chat/ui/ResumeCompactionDock.tsx +1 -0
  170. package/src/chat/ui/RowTime.tsx +1 -0
  171. package/src/chat/ui/RunSettings.tsx +38 -0
  172. package/src/chat/ui/Scrubber.tsx +14 -0
  173. package/src/chat/ui/SessionLine.tsx +1 -0
  174. package/src/chat/ui/ShellCommand.tsx +32 -0
  175. package/src/chat/ui/SubagentControls.tsx +7 -0
  176. package/src/chat/ui/SubagentInfo.tsx +1 -0
  177. package/src/chat/ui/SubagentStopButton.tsx +6 -0
  178. package/src/chat/ui/SubagentTimeline.tsx +10 -0
  179. package/src/chat/ui/Timeline.tsx +141 -1
  180. package/src/chat/ui/TimelineMenu.tsx +5 -0
  181. package/src/chat/ui/UnifiedDiff.tsx +6 -0
  182. package/src/chat/ui/UploadThumb.tsx +2 -0
  183. package/src/chat/ui/VisualDialogs.tsx +3 -0
  184. package/src/chat/ui/VisualFrame.tsx +15 -0
  185. package/src/chat/ui/Welcome.tsx +1 -0
  186. package/src/chat/ui/chip-clipboard.ts +20 -0
  187. package/src/chat/ui/code-lines.ts +14 -0
  188. package/src/chat/ui/code-theme.ts +1 -0
  189. package/src/chat/ui/composer/ComposerControls.tsx +70 -15
  190. package/src/chat/ui/composer/ComposerMenu.tsx +12 -0
  191. package/src/chat/ui/composer/DraftParts.tsx +2 -0
  192. package/src/chat/ui/composer/QueuedMessages.tsx +4 -0
  193. package/src/chat/ui/composer/chip-paste.ts +16 -0
  194. package/src/chat/ui/composer/chips.ts +6 -0
  195. package/src/chat/ui/composer/editor.ts +9 -0
  196. package/src/chat/ui/composer/insert.ts +2 -0
  197. package/src/chat/ui/composer/key-handler.ts +42 -9
  198. package/src/chat/ui/composer/keys.ts +23 -0
  199. package/src/chat/ui/composer/placeholder.ts +6 -0
  200. package/src/chat/ui/composer/prompt-picks.ts +18 -0
  201. package/src/chat/ui/composer/surface.ts +5 -0
  202. package/src/chat/ui/composer/use-chat-controls.ts +13 -0
  203. package/src/chat/ui/composer/use-compaction-offer.ts +3 -0
  204. package/src/chat/ui/composer/use-composer-menus.ts +20 -275
  205. package/src/chat/ui/composer/use-composer-submit.ts +27 -0
  206. package/src/chat/ui/composer/use-continuable-turn.ts +3 -5
  207. package/src/chat/ui/composer/use-draft-files.ts +24 -0
  208. package/src/chat/ui/composer/use-effort-stepper.ts +8 -0
  209. package/src/chat/ui/composer/use-notice.ts +4 -1
  210. package/src/chat/ui/composer/use-pending-requests.ts +3 -0
  211. package/src/chat/ui/composer/use-prompt-history.ts +20 -11
  212. package/src/chat/ui/composer/use-prompt-hold.ts +3 -0
  213. package/src/chat/ui/composer/use-prompt-menus.ts +361 -0
  214. package/src/chat/ui/composer/use-take-back.ts +20 -2
  215. package/src/chat/ui/diff/ApprovalReview.tsx +18 -15
  216. package/src/chat/ui/diff/DiffFallbackBoundary.tsx +1 -0
  217. package/src/chat/ui/diff/DiffOmitted.tsx +1 -0
  218. package/src/chat/ui/diff/DiffStack.tsx +2 -0
  219. package/src/chat/ui/diff/DiffUnavailable.tsx +1 -0
  220. package/src/chat/ui/diff/EditorEditDiff.tsx +13 -0
  221. package/src/chat/ui/diff/EditorPatchDiff.tsx +4 -0
  222. package/src/chat/ui/diff/LegacyDiffPool.tsx +2 -0
  223. package/src/chat/ui/diff/LegacyEditDiff.tsx +2 -0
  224. package/src/chat/ui/diff/LegacyUnifiedDiff.tsx +8 -0
  225. package/src/chat/ui/diff/RawDiff.tsx +1 -0
  226. package/src/chat/ui/diff/ReviewDiff.tsx +1 -0
  227. package/src/chat/ui/diff/diff-source-budget.ts +8 -0
  228. package/src/chat/ui/diff/editor-edit-model.ts +4 -0
  229. package/src/chat/ui/diff/review-plan.ts +9 -0
  230. package/src/chat/ui/diff/review-settle.ts +1 -0
  231. package/src/chat/ui/diff/syntax-runtime.ts +52 -0
  232. package/src/chat/ui/diff/use-review-stack.ts +9 -0
  233. package/src/chat/ui/diff-syntax.ts +12 -0
  234. package/src/chat/ui/diff-theme.ts +2 -0
  235. package/src/chat/ui/file-links.ts +2 -0
  236. package/src/chat/ui/find-reveal.ts +1 -0
  237. package/src/chat/ui/full-diff.ts +1 -0
  238. package/src/chat/ui/highlighter.ts +29 -15
  239. package/src/chat/ui/icons.tsx +2 -4
  240. package/src/chat/ui/index.ts +7 -0
  241. package/src/chat/ui/intelligent-ui/UiBlockFrame.tsx +25 -0
  242. package/src/chat/ui/intelligent-ui/UiFallbackPart.tsx +1 -0
  243. package/src/chat/ui/intelligent-ui/UiReply.tsx +63 -0
  244. package/src/chat/ui/intelligent-ui/block-local.ts +5 -0
  245. package/src/chat/ui/intelligent-ui/chart-data.ts +15 -0
  246. package/src/chat/ui/intelligent-ui/node-text.ts +17 -0
  247. package/src/chat/ui/intelligent-ui/renderers/binding.ts +1 -0
  248. package/src/chat/ui/intelligent-ui/renderers/chart.tsx +31 -0
  249. package/src/chat/ui/intelligent-ui/renderers/choices.tsx +4 -0
  250. package/src/chat/ui/intelligent-ui/renderers/content.tsx +18 -0
  251. package/src/chat/ui/intelligent-ui/renderers/data.tsx +11 -0
  252. package/src/chat/ui/intelligent-ui/renderers/inputs.tsx +13 -0
  253. package/src/chat/ui/intelligent-ui/renderers/links.tsx +9 -0
  254. package/src/chat/ui/intelligent-ui/renderers/map-slots.ts +15 -0
  255. package/src/chat/ui/intelligent-ui/renderers/map-style.ts +15 -0
  256. package/src/chat/ui/intelligent-ui/renderers/map-view.tsx +48 -0
  257. package/src/chat/ui/intelligent-ui/renderers/map.tsx +20 -0
  258. package/src/chat/ui/intelligent-ui/renderers/place-point.ts +3 -0
  259. package/src/chat/ui/intelligent-ui/renderers/structure.tsx +3 -0
  260. package/src/chat/ui/intelligent-ui/renderers/text.tsx +8 -0
  261. package/src/chat/ui/intelligent-ui/reply-parts.ts +5 -0
  262. package/src/chat/ui/intelligent-ui/table-data.ts +17 -0
  263. package/src/chat/ui/intelligent-ui/use-in-view.ts +4 -0
  264. package/src/chat/ui/intelligent-ui/use-later.ts +4 -0
  265. package/src/chat/ui/intelligent-ui/use-truncated.ts +5 -0
  266. package/src/chat/ui/intelligent-ui/use-ui-links.ts +29 -0
  267. package/src/chat/ui/intelligent-ui/use-ui-queries.ts +60 -0
  268. package/src/chat/ui/markdown-blocks.ts +64 -0
  269. package/src/chat/ui/mermaid.ts +4 -0
  270. package/src/chat/ui/quote-selection.ts +10 -0
  271. package/src/chat/ui/rehype-chips.ts +5 -0
  272. package/src/chat/ui/rehype-fade.ts +2 -0
  273. package/src/chat/ui/remark-html-as-text.ts +5 -0
  274. package/src/chat/ui/reveal.ts +18 -0
  275. package/src/chat/ui/rows/ForksRow.tsx +5 -0
  276. package/src/chat/ui/rows/GeneratedImageRow.tsx +10 -0
  277. package/src/chat/ui/rows/MessageRows.tsx +29 -5
  278. package/src/chat/ui/rows/RowContainer.tsx +1 -0
  279. package/src/chat/ui/rows/Rows.tsx +2 -0
  280. package/src/chat/ui/rows/SubagentRow.tsx +13 -0
  281. package/src/chat/ui/rows/TaskRow.tsx +7 -0
  282. package/src/chat/ui/rows/VisualRow.tsx +4 -0
  283. package/src/chat/ui/rows/WorkRows.tsx +46 -0
  284. package/src/chat/ui/rows/WorkflowRow.tsx +10 -0
  285. package/src/chat/ui/rows/row-rhythm.ts +7 -5
  286. package/src/chat/ui/theme-watch.ts +8 -7
  287. package/src/chat/ui/thread-references.ts +7 -0
  288. package/src/chat/ui/use-diff-source-tokens.ts +8 -0
  289. package/src/chat/ui/useToggleSet.ts +3 -0
  290. package/src/chat/ui/visual-theme.ts +30 -0
  291. package/src/chat/visual-bridge.ts +23 -0
  292. package/src/chat/visual-viewport.ts +12 -0
  293. package/src/host-hooks.ts +15 -5
  294. package/src/host.ts +29 -1
  295. package/src/index.ts +5 -0
  296. package/src/lazy.tsx +9 -0
  297. package/src/locales/en/agent-chat.ts +1 -0
  298. package/src/locales/nl/agent-chat.ts +1 -0
  299. package/src/mounted-registry.ts +4 -0
  300. package/src/port-transport.ts +32 -0
  301. package/src/prompts/index.ts +2 -0
  302. package/src/prompts/logic/focus.ts +5 -0
  303. package/src/prompts/logic/keys.ts +12 -0
  304. package/src/prompts/logic/prompts.ts +8 -0
  305. package/src/prompts/logic/subjects.ts +8 -0
  306. package/src/prompts/logic/useFocusAfterAnswer.ts +2 -0
  307. package/src/prompts/logic/usePromptSession.ts +13 -1
  308. package/src/prompts/ui/ApprovalBody.tsx +2 -0
  309. package/src/prompts/ui/PromptActions.tsx +2 -0
  310. package/src/prompts/ui/PromptCard.tsx +6 -0
  311. package/src/prompts/ui/PromptView.tsx +6 -0
  312. package/src/prompts/ui/QuestionBody.tsx +11 -0
  313. package/src/providers/AccountColors.tsx +1 -0
  314. package/src/providers/AccountDetail.tsx +7 -0
  315. package/src/providers/AccountVariables.tsx +2 -0
  316. package/src/providers/AddAccountForm.tsx +5 -1
  317. package/src/providers/CliDetail.tsx +6 -5
  318. package/src/providers/InstallPage.tsx +11 -0
  319. package/src/providers/ProviderList.tsx +3 -0
  320. package/src/providers/ProvidersPane.tsx +18 -0
  321. package/src/providers/account-actions.ts +10 -8
  322. package/src/providers/index.ts +1 -0
  323. package/src/providers/install-steps.ts +3 -0
  324. package/src/providers/pane-target.ts +5 -0
  325. package/src/providers/provider-abilities.ts +1 -0
  326. package/src/providers/provider-groups.ts +6 -0
  327. package/src/providers/variables.ts +12 -0
  328. package/src/scope.ts +2 -0
  329. package/src/state/chats.ts +61 -14
  330. package/src/state/index.ts +2 -0
  331. package/src/state/provider-accounts.ts +7 -9
  332. package/src/state/providers.ts +1 -0
  333. package/src/state/usage.ts +6 -0
  334. package/src/storage.ts +18 -0
  335. package/src/testing/prompts.ts +5 -0
  336. package/src/theme.css +3 -0
  337. package/src/transport.ts +18 -0
  338. package/src/usage/ChartLogo.tsx +12 -0
  339. package/src/usage/LimitsList.tsx +7 -8
  340. package/src/usage/UsageBreakdown.tsx +11 -4
  341. package/src/usage/UsageChart.tsx +5 -0
  342. package/src/usage/UsageLimits.tsx +10 -7
  343. package/src/usage/UsageLimitsCard.tsx +2 -0
  344. package/src/usage/UsagePage.tsx +16 -19
  345. package/src/usage/UsageSummary.tsx +4 -5
  346. package/src/usage/UsageTiles.tsx +2 -0
  347. package/src/usage/format.ts +7 -0
  348. package/src/usage/limit-groups.ts +14 -0
  349. package/src/usage/limits.ts +11 -13
  350. package/src/usage/money.ts +1 -0
  351. package/src/usage/summary.ts +15 -7
@@ -1,48 +1,17 @@
1
- import { useEffect, useId, useMemo, useState, type RefObject } from 'react';
1
+ import { useEffect, useState, type RefObject } from 'react';
2
2
  import { useTranslation } from 'react-i18next';
3
- import { ensureSyntaxTree, syntaxTree } from '@codemirror/language';
4
- import type { EditorState } from '@codemirror/state';
5
3
  import type { ChatInfo, ChatSkill, ProviderCapabilities } from '@adecore/agent-contracts';
6
- import { useChatHost } from '../../../host';
7
4
  import { useChatScope } from '../../../scope';
8
- import { referenceSuggestions, referenceTokens, type Referable } from '../../chat-references';
9
- import { usableSlashCommands } from '../../guards';
10
- import {
11
- chipRanges,
12
- findMentionQuery,
13
- findReferenceQuery,
14
- findSkillQuery,
15
- insertMention,
16
- insertReference,
17
- insertSkill,
18
- mentionPick,
19
- NO_MENTION_SEARCH,
20
- queryOutsideChips,
21
- type MentionQuery,
22
- type MentionSearch,
23
- type TextRange
24
- } from '../../mentions';
25
5
  import type { ComposerInputHandle } from '../ComposerInput';
26
6
  import type { ComposerDraft } from './use-composer-draft';
27
- import { inCode } from './keys';
28
- import { clampMenuIndex, composerMenuAria } from './menu-aria';
7
+ import { usePromptMenus, type PromptMenus } from './use-prompt-menus';
29
8
  import { useHostReferables } from '../../../host-hooks';
30
9
 
31
- const SEARCH_DEBOUNCE_MS = 80;
32
- // What fits above the composer without turning the picker into a file tree.
33
- const MENTION_RESULTS = 8;
34
- const REFERENCE_RESULTS = 8;
10
+ export type { ComposerCommand } from './use-prompt-menus';
35
11
 
36
12
  // Commands the composer handles itself; the CLI's own ones are sent through as text.
37
13
  export const LOCAL_COMMANDS = ['model', 'compact', 'clear'];
38
14
 
39
- export interface ComposerCommand {
40
- name: string;
41
- hint: string;
42
- local: boolean;
43
- skill: boolean;
44
- }
45
-
46
15
  interface MenusInput {
47
16
  chatId: string;
48
17
  info: ChatInfo;
@@ -52,62 +21,12 @@ interface MenusInput {
52
21
  inputRef: RefObject<ComposerInputHandle | null>;
53
22
  }
54
23
 
55
- function sameQuery(left: MentionQuery | null, right: MentionQuery | null): boolean {
56
- return left?.start === right?.start && left?.query === right?.query;
57
- }
58
-
59
- /*
60
- * The pickers over the box: `/` for a command, `$` for a skill, `@` for a file and `#` for a view,
61
- * a node or another chat. One is open at a time, and the arrow keys and the editor's ARIA attributes
62
- * follow that one.
63
- */
64
- export function useComposerMenus({ chatId, info, capabilities, providerName, draft: { draft, setDraft, setText }, inputRef }: MenusInput) {
24
+ /* The pickers over a chat's composer, fed by what the chat's CLI has: its skills, its commands and its folder. */
25
+ export function useComposerMenus({ chatId, info, capabilities, providerName, draft, inputRef }: MenusInput): PromptMenus {
65
26
  const { t } = useTranslation('agent-chat');
66
27
  const scope = useChatScope();
67
- const host = useChatHost();
68
- const { searchFiles } = host;
69
28
  const referables = useHostReferables(scope.id, chatId);
70
- const [menuIndex, setMenuIndex] = useState(0);
71
- const [mention, setMention] = useState<MentionQuery | null>(null);
72
- const [referenceQuery, setReferenceQuery] = useState<MentionQuery | null>(null);
73
- const [skillQuery, setSkillQuery] = useState<MentionQuery | null>(null);
74
29
  const [skills, setSkills] = useState<ChatSkill[]>([]);
75
- const [searched, setSearched] = useState<MentionSearch>(NO_MENTION_SEARCH);
76
- const text = draft.text;
77
- const goals = capabilities?.goals;
78
-
79
- // Every keystroke inside an `@word` searches again; a slow answer must not overwrite a newer one.
80
- useEffect(() => {
81
- if (mention === null) {
82
- return;
83
- }
84
- let stale = false;
85
- const search = (): void => {
86
- if (searchFiles === null) {
87
- return;
88
- }
89
- searchFiles(scope.id, info.cwd, mention.query, MENTION_RESULTS)
90
- .then((files) => {
91
- if (!stale) {
92
- setSearched({ query: mention.query, files });
93
- setMenuIndex(0);
94
- }
95
- })
96
- .catch(() => undefined);
97
- };
98
- // A bare `@` is not a keystroke in a query. It opens the list of what is there, at once.
99
- if (mention.query === '') {
100
- search();
101
- return () => {
102
- stale = true;
103
- };
104
- }
105
- const timer = setTimeout(search, SEARCH_DEBOUNCE_MS);
106
- return () => {
107
- stale = true;
108
- clearTimeout(timer);
109
- };
110
- }, [scope, info.cwd, mention, searchFiles]);
111
30
 
112
31
  /*
113
32
  * The host's skill list, once per chat, again once the CLI announced its own after the first message,
@@ -115,6 +34,7 @@ export function useComposerMenus({ chatId, info, capabilities, providerName, dra
115
34
  */
116
35
  useEffect(() => {
117
36
  let stale = false;
37
+
118
38
  scope.chats
119
39
  .listSkills(chatId)
120
40
  .then((found) => {
@@ -123,201 +43,26 @@ export function useComposerMenus({ chatId, info, capabilities, providerName, dra
123
43
  }
124
44
  })
125
45
  .catch(() => undefined);
46
+
126
47
  return () => {
127
48
  stale = true;
128
49
  };
129
50
  }, [scope, chatId, info.skills, info.running]);
130
51
 
131
- const commandQuery = text.startsWith('/') && !text.includes('\n') ? text.slice(1).trim().toLowerCase() : null;
132
- const commands = useMemo((): ComposerCommand[] => {
133
- if (commandQuery === null) {
134
- return [];
135
- }
136
- const known = new Set(skills.map((skill) => skill.name));
137
- const own = LOCAL_COMMANDS.map((name) => ({ name, hint: t(`composer.commands.${name}`), local: true, skill: false }));
138
- // Every CLI with goals reads `/goal`, also one that lists no commands of its own.
139
- const goal = goals === undefined ? [] : [{ name: 'goal', hint: t('composer.commands.goal'), local: false, skill: false }];
140
- const cli = usableSlashCommands(info.slashCommands)
141
- .filter((name) => !LOCAL_COMMANDS.includes(name) && (goals === undefined || name !== 'goal'))
142
- .map((name) => ({
143
- name,
144
- hint: known.has(name) ? t('composer.commands.skill') : t('composer.commands.cli', { cli: providerName ?? t('composer.commands.anyCli') }),
145
- local: false,
146
- skill: known.has(name)
147
- }));
148
- return [...own, ...goal, ...cli].filter((command) => command.name.startsWith(commandQuery)).slice(0, 8);
149
- }, [commandQuery, goals, info.slashCommands, providerName, skills, t]);
150
-
151
- const skillMatches = useMemo(() => {
152
- if (skillQuery === null) {
153
- return [];
154
- }
155
- const query = skillQuery.query.toLowerCase();
156
- return skills.filter((skill) => skill.name.toLowerCase().includes(query)).slice(0, 8);
157
- }, [skillQuery, skills]);
158
-
159
- const commandMenuOpen = commandQuery !== null && commands.length > 0;
160
- const skillMenuOpen = !commandMenuOpen && skillQuery !== null && skillMatches.length > 0;
161
- const mentionMenuOpen = !commandMenuOpen && !skillMenuOpen && mention !== null;
162
- // Results belong to the query that asked for them; a closed picker shows none while the next answer is on its way.
163
- const files = mention === null ? [] : searched.files;
164
- const mentionCount = files.length;
165
- // A host with nothing to point at keeps `#fff` out of the way rather than saying nothing matches.
166
- const offersReferences = referables.items.some((item) => !referables.chatsOnly || item.kind === 'chat');
167
- const referenceMenuOpen = !commandMenuOpen && !skillMenuOpen && !mentionMenuOpen && referenceQuery !== null && offersReferences;
168
- const referenceMatches = referenceMenuOpen
169
- ? referenceSuggestions(referables, referenceQuery.query, [chatId, ...draft.chats, ...draft.references], REFERENCE_RESULTS)
170
- : [];
171
- // What the editor draws as chips and the send checks are still in the text, under the name each was picked with.
172
- const referenceChips = referenceTokens([...draft.chats, ...draft.references], referables.items, draft.referenceNames).map((token) => ({
173
- ...token,
174
- kind: referables.items.find((item) => item.id === token.id)?.kind ?? null
175
- }));
176
- // One menu is open at a time, so they share an id.
177
- const listId = useId();
178
- const menuOpen = commandMenuOpen || skillMenuOpen || mentionMenuOpen || referenceMenuOpen;
179
- const menuCount = commandMenuOpen ? commands.length : skillMenuOpen ? skillMatches.length : mentionMenuOpen ? mentionCount : referenceMatches.length;
180
- // The list can shrink under a stale index; every reader takes the one that still points at an option.
181
- const activeIndex = clampMenuIndex(menuIndex, menuCount);
182
- const inputAria = composerMenuAria({ label: t('composer.inputLabel'), listId, open: menuOpen, activeIndex, count: menuCount });
183
-
184
- /* Which picker the caret opens: `@` for a file, `$` for a skill, `#` for a reference, or none. In code a sigil is just text. */
185
- const trackTriggers = (value: string, selection: TextRange, state: EditorState): void => {
186
- const empty = selection.from === selection.to;
187
- const tree = empty ? (ensureSyntaxTree(state, selection.from, 50) ?? syntaxTree(state)) : null;
188
- const caret = tree !== null && !inCode(tree, value, selection.from) ? selection.from : null;
189
- const chips = caret === null ? [] : chipRanges(value, draft.mentions, draft.skills, [], referenceChips);
190
- const skill = caret === null ? null : queryOutsideChips(findSkillQuery(value, caret), chips);
191
- setSkillQuery((current) => (sameQuery(current, skill) ? current : skill));
52
+ return usePromptMenus({
53
+ draft,
54
+ inputRef,
192
55
  // A CLI that does not expand `@path` gets the text as it is, so the picker stays out of the way.
193
- const next = caret === null || capabilities?.mentions === false ? null : queryOutsideChips(findMentionQuery(value, caret), chips);
194
- setMention((current) => (sameQuery(current, next) ? current : next));
195
- const reference = caret === null ? null : queryOutsideChips(findReferenceQuery(value, caret), chips);
196
- setReferenceQuery((current) => (sameQuery(current, reference) ? current : reference));
197
- };
198
-
199
- // The inserted text reaches the editor with the render it causes, so the caret waits a frame for it.
200
- const moveCaret = (caret: number): void => {
201
- requestAnimationFrame(() => inputRef.current?.setCaret(caret));
202
- };
203
-
204
- const close = (): void => {
205
- setMention(null);
206
- setSkillQuery(null);
207
- setReferenceQuery(null);
208
- };
209
-
210
- const chooseMention = (path: string): void => {
211
- if (!mention) {
212
- return;
213
- }
214
- const result = insertMention(text, mention, path);
215
- setText(result.text, draft.mentions.includes(path) ? draft.mentions : [...draft.mentions, path]);
216
- setMention(null);
217
- moveCaret(result.caret);
218
- };
219
-
220
- const chooseMentionAt = (index: number): void => {
221
- const path = mention === null ? null : mentionPick(index, searched, mention.query);
222
- if (path !== null) {
223
- chooseMention(path);
224
- }
225
- };
226
-
227
- /*
228
- * A picked view, node or chat becomes a `#name` chip in the text. The draft keeps the name it was
229
- * spelled with, since a rename would otherwise unmake the chip. A chat goes apart, as `chats`.
230
- */
231
- const chooseReference = (item: Referable): void => {
232
- if (!referenceQuery) {
233
- return;
234
- }
235
- const result = insertReference(text, referenceQuery, item.name);
236
- const chat = item.kind === 'chat';
237
- const add = (ids: string[]): string[] => (ids.includes(item.id) ? ids : [...ids, item.id]);
238
- setDraft((current) => ({
239
- ...current,
240
- text: result.text,
241
- chats: chat ? add(current.chats) : current.chats,
242
- references: chat ? current.references : add(current.references),
243
- referenceNames: { ...current.referenceNames, [item.id]: item.name }
244
- }));
245
- setReferenceQuery(null);
246
- moveCaret(result.caret);
247
- };
248
-
249
- const chooseReferenceAt = (index: number): void => {
250
- const item = referenceMatches[index] ?? referenceMatches[0];
251
- if (item !== undefined) {
252
- chooseReference(item);
253
- }
254
- };
255
-
256
- /*
257
- * A picked skill becomes a `$name` chip in the text and a name on the send, which is what
258
- * dispatches it. The `/` menu lands here too, so both spellings end up on the same path.
259
- */
260
- const chooseSkill = (name: string): void => {
261
- const result = skillQuery ? insertSkill(text, skillQuery, name) : { text: `$${name} `, caret: name.length + 2 };
262
- setText(result.text, draft.mentions, draft.skills.includes(name) ? draft.skills : [...draft.skills, name]);
263
- setSkillQuery(null);
264
- moveCaret(result.caret);
265
- };
266
-
267
- /* A clicked command; Tab completes one with a space after it instead. */
268
- const pickCommand = (command: ComposerCommand, index: number): void => {
269
- setMenuIndex(index);
270
- if (command.skill) {
271
- chooseSkill(command.name);
272
- return;
273
- }
274
- setText(`/${command.name}`);
275
- inputRef.current?.focus();
276
- };
277
-
278
- /* The arrows walk an open picker round; true when the key was one of them. */
279
- const moveInMenu = (key: string, count: number): boolean => {
280
- if (key === 'ArrowDown') {
281
- setMenuIndex((i) => (i + 1) % count);
282
- return true;
283
- }
284
- if (key === 'ArrowUp') {
285
- setMenuIndex((i) => (i - 1 + count) % count);
286
- return true;
287
- }
288
- return false;
289
- };
290
-
291
- return {
292
- listId,
293
- activeIndex,
294
- inputAria,
295
- hasSkills: skills.length > 0,
296
- skillNames: skills.map((skill) => skill.name),
297
- commandQuery,
298
- commands,
299
- commandMenuOpen,
300
- skillMatches,
301
- skillMenuOpen,
302
- mention,
303
- mentionMenuOpen,
304
- mentionCount,
305
- files,
306
- referenceMenuOpen,
307
- referenceMatches,
56
+ cwd: capabilities?.mentions === false ? null : info.cwd,
57
+ skills,
58
+ slashCommands: info.slashCommands,
59
+ localCommands: LOCAL_COMMANDS,
60
+ goals: capabilities?.goals !== undefined,
61
+ providerName,
308
62
  referables,
309
- referenceChips,
310
- setMenuIndex,
311
- trackTriggers,
312
- close,
313
- chooseMention,
314
- chooseMentionAt,
315
- chooseReference,
316
- chooseReferenceAt,
317
- chooseSkill,
318
- pickCommand,
319
- moveInMenu
320
- };
63
+ ownId: chatId,
64
+ label: t('composer.inputLabel')
65
+ });
321
66
  }
322
67
 
323
- export type ComposerMenus = ReturnType<typeof useComposerMenus>;
68
+ export type ComposerMenus = PromptMenus;
@@ -62,9 +62,11 @@ export function useComposerSubmit({
62
62
  /* Opens the host's new chat once the host took the message; a host that answers nothing took it. */
63
63
  const afterSend = (sent: void | Promise<boolean>, andNew: boolean): void => {
64
64
  const openNewChat = host.openNewChat;
65
+
65
66
  if (!andNew || openNewChat === null) {
66
67
  return;
67
68
  }
69
+
68
70
  void Promise.resolve(sent)
69
71
  .then((taken) => {
70
72
  if (taken !== false) {
@@ -79,39 +81,53 @@ export function useComposerSubmit({
79
81
  const delivery = steerable && (alternate ? sendDelivery === 'queue' : sendDelivery === 'steer') ? 'steer' : 'queue';
80
82
  const { text, quote, quoteSource } = draft;
81
83
  const trimmed = text.trim();
84
+
82
85
  if (isEmptyDraft(draft) || !writable || guard.tooLong) {
83
86
  return;
84
87
  }
88
+
85
89
  // Recheck restored drafts and files that finished reading concurrently.
86
90
  const rejected = checkAttachmentLimits(
87
91
  0,
88
92
  draft.attachments.map((attachment) => ({ ...attachment, bytes: uploadBytes(attachment) }))
89
93
  ).rejected[0];
94
+
90
95
  if (rejected) {
91
96
  setNotice(`${rejected.name}: ${rejected.reason}`);
97
+
92
98
  return;
93
99
  }
100
+
94
101
  hold.release('send');
95
102
  const { commandQuery } = menus;
96
103
  const chosen = menus.commands[menus.activeIndex];
104
+
97
105
  if (commandQuery !== null && chosen?.local && controls.runCommand(chosen.name)) {
98
106
  box.clear();
107
+
99
108
  return;
100
109
  }
110
+
101
111
  if (commandQuery !== null && chosen?.skill) {
102
112
  menus.chooseSkill(chosen.name);
113
+
103
114
  return;
104
115
  }
116
+
105
117
  // Whoever sends wants to see what comes back, wherever they were reading.
106
118
  scrollTimelineToEnd(chatId, 'instant');
119
+
107
120
  if (commandQuery !== null && chosen && !chosen.local) {
108
121
  afterSend(onSend(`/${chosen.name}`, {}), how === 'and-new');
109
122
  box.clear();
123
+
110
124
  return;
111
125
  }
126
+
112
127
  const chats = presentReferences(trimmed, draft.chats, menus.referenceChips);
113
128
  // A machine without `viewReferences` drops the field, so only the chats it does know reach it.
114
129
  const references = menus.referables.chatsOnly ? [] : presentReferences(trimmed, draft.references, menus.referenceChips);
130
+
115
131
  const sent = onSend(withQuote(quote, trimmed), {
116
132
  delivery,
117
133
  mentions: presentMentions(trimmed, draft.mentions),
@@ -121,6 +137,7 @@ export function useComposerSubmit({
121
137
  attachments: draft.attachments,
122
138
  references: references.length > 0 ? references : undefined
123
139
  });
140
+
124
141
  afterSend(sent, how === 'and-new');
125
142
  box.clear();
126
143
  menus.close();
@@ -129,9 +146,11 @@ export function useComposerSubmit({
129
146
 
130
147
  // A menu row means the draft as it stands when it is picked, so the registration reads the latest submit.
131
148
  const submitRef = useRef(submit);
149
+
132
150
  useEffect(() => {
133
151
  submitRef.current = submit;
134
152
  });
153
+
135
154
  useEffect(() => registerComposerSender(scope.keyOf(chatId), (how) => submitRef.current(how)), [scope, chatId]);
136
155
 
137
156
  /*
@@ -142,11 +161,13 @@ export function useComposerSubmit({
142
161
  const restoreStashed = (prompt: StashedPrompt): void => {
143
162
  const stashedQuote = prompt.quote;
144
163
  const here = stashedQuote !== undefined && stashedQuote.chatId === chatId;
164
+
145
165
  const { chats, dropped } = restorableChats(
146
166
  prompt.chats,
147
167
  new Set(menus.referables.items.filter((item) => item.kind === 'chat').map((item) => item.id)),
148
168
  chatId
149
169
  );
170
+
150
171
  setDraft((current) => ({
151
172
  ...current,
152
173
  text: stashedQuote === undefined || here ? prompt.text : withQuote(stashedQuote.text, prompt.text),
@@ -156,9 +177,11 @@ export function useComposerSubmit({
156
177
  references: [...prompt.references],
157
178
  ...(here ? { quote: stashedQuote.text, quoteSource: stashedQuote.itemId } : {})
158
179
  }));
180
+
159
181
  if (dropped > 0) {
160
182
  setNotice(t('composer.notice.stashChatsDropped', { count: dropped }), 'info');
161
183
  }
184
+
162
185
  history.forget();
163
186
  inputRef.current?.focus();
164
187
  };
@@ -168,6 +191,7 @@ export function useComposerSubmit({
168
191
  if (!writable) {
169
192
  return;
170
193
  }
194
+
171
195
  scrollTimelineToEnd(chatId, 'instant');
172
196
  afterSend(onSend(CONTINUE_PROMPT, {}), false);
173
197
  };
@@ -176,11 +200,14 @@ export function useComposerSubmit({
176
200
  const toggleStash = (): void => {
177
201
  if (isEmptyDraft(draft)) {
178
202
  const newest = useStash.getState().prompts[0];
203
+
179
204
  if (newest) {
180
205
  restoreStashed(newest);
181
206
  }
207
+
182
208
  return;
183
209
  }
210
+
184
211
  if (stashDraft(draft, chatId)) {
185
212
  box.clear();
186
213
  menus.close();
@@ -1,4 +1,3 @@
1
- import { useMemo } from 'react';
2
1
  import type { ChatInfo, ChatItem } from '@adecore/agent-contracts';
3
2
  import { useChatRow } from '../../../state/chats';
4
3
  import { continuableTurn } from '../../logic/continue-after-stop';
@@ -7,8 +6,7 @@ import { continuableTurn } from '../../logic/continue-after-stop';
7
6
  export function useContinuableTurn(chatId: string, info: ChatInfo): string | null {
8
7
  const order = useChatRow(chatId, (row) => row?.order);
9
8
  const structure = useChatRow(chatId, (row) => row?.structure);
10
- return useMemo(() => {
11
- const items = (order ?? []).map((id) => structure?.[id]).filter((item): item is ChatItem => item !== undefined);
12
- return continuableTurn(info, items);
13
- }, [info, order, structure]);
9
+ const items = (order ?? []).map((id) => structure?.[id]).filter((item): item is ChatItem => item !== undefined);
10
+
11
+ return continuableTurn(info, items);
14
12
  }
@@ -54,9 +54,11 @@ export function useDraftFiles({
54
54
  /* Paths dragged in from a list of files. With no mention query to insert into, they land at the end of the prompt. */
55
55
  const addMentions = (paths: string[]): void => {
56
56
  const fresh = paths.filter((path) => path !== '');
57
+
57
58
  if (fresh.length === 0 || capabilities?.mentions === false) {
58
59
  return;
59
60
  }
61
+
60
62
  const appended = fresh.map((path) => `@${path}`).join(' ');
61
63
  const before = draft.text.replace(/\s+$/, '');
62
64
  setText(before === '' ? `${appended} ` : `${before} ${appended} `, [...draft.mentions, ...fresh.filter((path) => !draft.mentions.includes(path))]);
@@ -66,21 +68,27 @@ export function useDraftFiles({
66
68
  if (incoming.length === 0) {
67
69
  return;
68
70
  }
71
+
69
72
  if (capabilities?.attachments === false) {
70
73
  setNotice(t('composer.notice.noAttachments', { agent: providerName ?? t('composer.notice.thisAgent') }));
74
+
71
75
  return;
72
76
  }
77
+
73
78
  const checked = checkAttachmentLimits(
74
79
  draft.attachments.length,
75
80
  incoming.map((file) => ({ name: file.name, mime: file.type, bytes: file.size, file })),
76
81
  draft.attachments.reduce((bytes, attachment) => bytes + uploadBytes(attachment), 0)
77
82
  );
83
+
78
84
  if (checked.rejected[0]) {
79
85
  setNotice(t('composer.notice.rejected', { name: checked.rejected[0].name || t('composer.notice.thatFile'), reason: checked.rejected[0].reason }));
80
86
  }
87
+
81
88
  if (checked.accepted.length === 0) {
82
89
  return;
83
90
  }
91
+
84
92
  readAttachments(checked.accepted.map((entry) => entry.file))
85
93
  .then((attachments) => setDraft((current) => ({ ...current, attachments: [...current.attachments, ...attachments] })))
86
94
  .catch(() => setNotice(t('composer.notice.unreadable')));
@@ -94,24 +102,32 @@ export function useDraftFiles({
94
102
  /* Files become attachments and a long text a file of its own; anything else the editor pastes as plain text. */
95
103
  const onPaste = (e: ClipboardEvent, view: EditorView): boolean => {
96
104
  const data = e.clipboardData;
105
+
97
106
  if (!data) {
98
107
  return false;
99
108
  }
109
+
100
110
  const pasted = filesOf(data);
111
+
101
112
  if (pasted.length > 0) {
102
113
  addFiles(pasted);
114
+
103
115
  return true;
104
116
  }
117
+
105
118
  const inline = pasteInline.current;
106
119
  pasteInline.current = false;
107
120
  const clip = data.getData('text/plain');
108
121
  const mentions = capabilities?.mentions === false ? [] : pastedMentions(clip);
122
+
109
123
  if (mentions.length > 0) {
110
124
  // The editor's own change reads the draft as it was rendered, so the mentions join it after that change.
111
125
  view.dispatch(view.state.replaceSelection(clip), { scrollIntoView: true, userEvent: 'input.paste' });
112
126
  setDraft((current) => ({ ...current, mentions: [...current.mentions, ...mentions.filter((path) => !current.mentions.includes(path))] }));
127
+
113
128
  return true;
114
129
  }
130
+
115
131
  // Without attachments there is nowhere else for the text to go, so it pastes as it always did.
116
132
  if (inline || capabilities?.attachments === false || !pasteBecomesAttachment(clip)) {
117
133
  const choices: PasteChoices = {
@@ -120,21 +136,29 @@ export function useDraftFiles({
120
136
  referenced: [...draft.chats, ...draft.references],
121
137
  mentions: capabilities?.mentions !== false
122
138
  };
139
+
123
140
  const chips = pasteWithChips(view, clip, readClipboardChips(data), choices);
141
+
124
142
  if (chips === null) {
125
143
  return false;
126
144
  }
145
+
127
146
  // After the editor's own change, which reads the draft as it was rendered.
128
147
  setDraft((current) => withPastedChips(current, chips, choices));
148
+
129
149
  return true;
130
150
  }
151
+
131
152
  // The paste still replaces what was selected; it just puts nothing in its place.
132
153
  const { from, to } = view.state.selection.main;
154
+
133
155
  if (from !== to) {
134
156
  view.dispatch({ changes: { from, to }, userEvent: 'delete' });
135
157
  }
158
+
136
159
  const name = pastedTextName(draft.attachments.map((attachment) => attachment.name));
137
160
  addFiles([new File([clip], name, { type: 'text/plain' })]);
161
+
138
162
  return true;
139
163
  };
140
164
 
@@ -27,12 +27,16 @@ export function useEffortStepper({ chatId, info, model, writable, controls, setN
27
27
  if (!writable || controls.configurationPending) {
28
28
  return;
29
29
  }
30
+
30
31
  const name = model?.name ?? info.selection.model;
31
32
  const result = stepEffort(model, info.selection, direction);
33
+
32
34
  if (result.kind === 'none') {
33
35
  setNotice(t('composer.notice.noEffort', { model: name }), 'info');
36
+
34
37
  return;
35
38
  }
39
+
36
40
  if (result.kind === 'edge') {
37
41
  setNotice(
38
42
  t(direction === 'up' ? 'composer.notice.effortHighest' : 'composer.notice.effortLowest', {
@@ -41,16 +45,20 @@ export function useEffortStepper({ chatId, info, model, writable, controls, setN
41
45
  }),
42
46
  'info'
43
47
  );
48
+
44
49
  return;
45
50
  }
51
+
46
52
  controls.configure({ selection: result.selection });
47
53
  setNotice(t('composer.notice.effort', { effort: choiceWord(EFFORT_OPTION, result.choice) }), 'info');
48
54
  };
49
55
 
50
56
  // A command means the chat as it stands when it is given, so the registration reads the latest step.
51
57
  const stepRef = useRef(step);
58
+
52
59
  useEffect(() => {
53
60
  stepRef.current = step;
54
61
  });
62
+
55
63
  useEffect(() => registerEffortStepper(scope.keyOf(chatId), (direction) => stepRef.current(direction)), [scope, chatId]);
56
64
  }
@@ -1,4 +1,5 @@
1
1
  import { useEffect, useState } from 'react';
2
+ import { errorMessage } from '../../../transport';
2
3
 
3
4
  const NOTICE_MS = 4000;
4
5
 
@@ -20,7 +21,9 @@ export function useNotice(): ComposerNotice {
20
21
  if (!shown) {
21
22
  return;
22
23
  }
24
+
23
25
  const timer = setTimeout(() => setShown(null), NOTICE_MS);
26
+
24
27
  return () => clearTimeout(timer);
25
28
  }, [shown]);
26
29
 
@@ -30,6 +33,6 @@ export function useNotice(): ComposerNotice {
30
33
  notice: shown?.text ?? null,
31
34
  tone: shown?.tone ?? 'error',
32
35
  setNotice,
33
- showError: (error) => setNotice(error instanceof Error ? error.message : String(error))
36
+ showError: (error) => setNotice(errorMessage(error))
34
37
  };
35
38
  }
@@ -6,10 +6,13 @@ import { ChatPromptFeed } from '../../pending-prompts';
6
6
  export function usePendingRequests(chatId: string): PromptSubject[] {
7
7
  const scope = useChatScope();
8
8
  const [subjects, setSubjects] = useState<PromptSubject[]>([]);
9
+
9
10
  useEffect(() => {
10
11
  const feed = new ChatPromptFeed(scope, chatId, setSubjects);
11
12
  feed.start();
13
+
12
14
  return () => feed.dispose();
13
15
  }, [scope, chatId]);
16
+
14
17
  return subjects;
15
18
  }