@adecore/agents-react 0.31.12 → 0.33.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 (409) hide show
  1. package/README.md +1 -1
  2. package/dist/agents/AccountDot.js +57 -6
  3. package/dist/agents/AgentIcon.js +88 -12
  4. package/dist/agents/ModelOptionControl.js +153 -18
  5. package/dist/agents/ProviderLogo.js +49 -4
  6. package/dist/agents/account-limits.js +17 -14
  7. package/dist/agents/accounts.js +99 -67
  8. package/dist/agents/agent-marks.js +5 -3
  9. package/dist/agents/model-name.js +42 -31
  10. package/dist/agents/model-options.js +8 -8
  11. package/dist/agents/model-requirement.js +17 -14
  12. package/dist/agents/option-words.d.ts +7 -0
  13. package/dist/agents/option-words.js +35 -0
  14. package/dist/agents/provider-paths.js +6 -6
  15. package/dist/agents/status-look.js +32 -12
  16. package/dist/chat/account-choice.d.ts +13 -0
  17. package/dist/chat/account-choice.js +90 -20
  18. package/dist/chat/actions.js +126 -59
  19. package/dist/chat/activity.js +5 -5
  20. package/dist/chat/attachments.js +74 -53
  21. package/dist/chat/bookmarks.js +58 -48
  22. package/dist/chat/chat-client.js +344 -263
  23. package/dist/chat/chat-references.js +3 -3
  24. package/dist/chat/composer-effort.d.ts +5 -0
  25. package/dist/chat/composer-effort.js +26 -0
  26. package/dist/chat/composer-send.js +13 -13
  27. package/dist/chat/drafts.js +115 -79
  28. package/dist/chat/forks.js +81 -24
  29. package/dist/chat/guards.js +15 -43
  30. package/dist/chat/index.d.ts +3 -1
  31. package/dist/chat/index.js +3 -2
  32. package/dist/chat/logic/ansi.js +170 -167
  33. package/dist/chat/logic/answers.js +2 -2
  34. package/dist/chat/logic/bookmarks.js +19 -19
  35. package/dist/chat/logic/command-looks.js +138 -96
  36. package/dist/chat/logic/context-usage.js +13 -9
  37. package/dist/chat/logic/continue-after-stop.js +15 -19
  38. package/dist/chat/logic/effort.d.ts +14 -0
  39. package/dist/chat/logic/effort.js +34 -0
  40. package/dist/chat/logic/embedded-code.js +387 -326
  41. package/dist/chat/logic/fork.js +134 -110
  42. package/dist/chat/logic/generated-image.js +54 -23
  43. package/dist/chat/logic/goal.js +28 -20
  44. package/dist/chat/logic/handback.js +6 -6
  45. package/dist/chat/logic/json.js +2 -2
  46. package/dist/chat/logic/limit.js +46 -30
  47. package/dist/chat/logic/picker-entries.d.ts +5 -3
  48. package/dist/chat/logic/picker-entries.js +83 -43
  49. package/dist/chat/logic/resume-compaction.d.ts +14 -1
  50. package/dist/chat/logic/resume-compaction.js +93 -24
  51. package/dist/chat/logic/scrubber.js +188 -136
  52. package/dist/chat/logic/thought.js +13 -16
  53. package/dist/chat/logic/thread-cards.js +60 -48
  54. package/dist/chat/logic/timeline-copy.js +30 -38
  55. package/dist/chat/logic/timeline-target.js +92 -63
  56. package/dist/chat/logic/timeline.js +499 -355
  57. package/dist/chat/logic/tool-catalog.js +54 -34
  58. package/dist/chat/logic/tools.js +106 -82
  59. package/dist/chat/logic/visual-height.js +18 -18
  60. package/dist/chat/logic/visual-viewport.js +6 -3
  61. package/dist/chat/logic/welcome.js +9 -9
  62. package/dist/chat/logic/workflow.d.ts +10 -0
  63. package/dist/chat/logic/workflow.js +130 -32
  64. package/dist/chat/mentions.js +149 -109
  65. package/dist/chat/persisted-json.js +51 -54
  66. package/dist/chat/preferences.d.ts +4 -0
  67. package/dist/chat/preferences.js +162 -84
  68. package/dist/chat/prompt-hold.js +9 -9
  69. package/dist/chat/quote.js +21 -30
  70. package/dist/chat/reading-width.js +12 -10
  71. package/dist/chat/recent-messages.js +34 -21
  72. package/dist/chat/resume-compaction-dismissals.js +25 -5
  73. package/dist/chat/runtime-modes.js +3 -3
  74. package/dist/chat/send-key.js +11 -7
  75. package/dist/chat/stash.js +89 -69
  76. package/dist/chat/subagent-conversation.js +181 -139
  77. package/dist/chat/subagent-list.js +65 -67
  78. package/dist/chat/subagent-support.js +12 -7
  79. package/dist/chat/subagent-view.js +132 -44
  80. package/dist/chat/thumbnails.js +88 -60
  81. package/dist/chat/timeline-flash.js +24 -12
  82. package/dist/chat/timeline-scroll.js +116 -86
  83. package/dist/chat/ui/AccountPill.js +126 -15
  84. package/dist/chat/ui/AnsiOutput.js +62 -6
  85. package/dist/chat/ui/BookmarkMarker.js +231 -41
  86. package/dist/chat/ui/BookmarkSubmenu.js +165 -18
  87. package/dist/chat/ui/ChatActivity.js +649 -70
  88. package/dist/chat/ui/ChatReferenceChip.js +104 -9
  89. package/dist/chat/ui/CodeBlock.js +132 -58
  90. package/dist/chat/ui/Composer.js +866 -132
  91. package/dist/chat/ui/ComposerInput.js +309 -120
  92. package/dist/chat/ui/DiffPool.js +75 -14
  93. package/dist/chat/ui/EditDiff.js +160 -21
  94. package/dist/chat/ui/FadingWords.js +32 -3
  95. package/dist/chat/ui/GoalDock.js +235 -42
  96. package/dist/chat/ui/ImageView.js +617 -61
  97. package/dist/chat/ui/LimitState.js +446 -72
  98. package/dist/chat/ui/Markdown.js +991 -173
  99. package/dist/chat/ui/MermaidBlock.js +285 -79
  100. package/dist/chat/ui/MessageActions.js +251 -39
  101. package/dist/chat/ui/ModelMarks.js +55 -5
  102. package/dist/chat/ui/Pickers.js +664 -113
  103. package/dist/chat/ui/PromptComposer.js +281 -51
  104. package/dist/chat/ui/QuoteButton.js +151 -57
  105. package/dist/chat/ui/ResumeCompactionDock.d.ts +6 -2
  106. package/dist/chat/ui/ResumeCompactionDock.js +141 -6
  107. package/dist/chat/ui/RowTime.js +59 -5
  108. package/dist/chat/ui/RunSettings.js +1744 -154
  109. package/dist/chat/ui/Scrubber.js +517 -88
  110. package/dist/chat/ui/ShellCommand.js +183 -107
  111. package/dist/chat/ui/SubagentControls.js +212 -24
  112. package/dist/chat/ui/SubagentInfo.js +170 -13
  113. package/dist/chat/ui/SubagentStopButton.js +115 -24
  114. package/dist/chat/ui/SubagentTimeline.js +357 -91
  115. package/dist/chat/ui/Timeline.js +988 -476
  116. package/dist/chat/ui/TimelineMenu.js +552 -37
  117. package/dist/chat/ui/UnifiedDiff.js +234 -27
  118. package/dist/chat/ui/UploadThumb.js +37 -7
  119. package/dist/chat/ui/VisualDialogs.js +251 -20
  120. package/dist/chat/ui/VisualFrame.js +321 -95
  121. package/dist/chat/ui/Welcome.js +72 -7
  122. package/dist/chat/ui/chips.js +1 -1
  123. package/dist/chat/ui/code-lines.js +36 -36
  124. package/dist/chat/ui/code-streaming.js +1 -1
  125. package/dist/chat/ui/code-theme.js +7 -7
  126. package/dist/chat/ui/composer/ComposerControls.js +329 -21
  127. package/dist/chat/ui/composer/ComposerMenu.js +358 -30
  128. package/dist/chat/ui/composer/DraftParts.js +185 -11
  129. package/dist/chat/ui/composer/QueuedMessages.js +138 -13
  130. package/dist/chat/ui/composer/chips.js +29 -19
  131. package/dist/chat/ui/composer/editor.js +79 -63
  132. package/dist/chat/ui/composer/insert.js +26 -12
  133. package/dist/chat/ui/composer/key-handler.js +180 -127
  134. package/dist/chat/ui/composer/keys.js +90 -73
  135. package/dist/chat/ui/composer/menu-aria.js +18 -9
  136. package/dist/chat/ui/composer/placeholder.js +78 -31
  137. package/dist/chat/ui/composer/surface.js +13 -37
  138. package/dist/chat/ui/composer/use-chat-controls.js +230 -90
  139. package/dist/chat/ui/composer/use-compaction-offer.js +50 -18
  140. package/dist/chat/ui/composer/use-composer-draft.js +65 -34
  141. package/dist/chat/ui/composer/use-composer-menus.js +478 -193
  142. package/dist/chat/ui/composer/use-composer-submit.js +251 -109
  143. package/dist/chat/ui/composer/use-continuable-turn.js +33 -6
  144. package/dist/chat/ui/composer/use-draft-files.js +245 -93
  145. package/dist/chat/ui/composer/use-effort-stepper.d.ts +13 -0
  146. package/dist/chat/ui/composer/use-effort-stepper.js +88 -0
  147. package/dist/chat/ui/composer/use-notice.js +61 -16
  148. package/dist/chat/ui/composer/use-pending-requests.js +38 -17
  149. package/dist/chat/ui/composer/use-prompt-history.js +117 -45
  150. package/dist/chat/ui/composer/use-prompt-hold.js +119 -35
  151. package/dist/chat/ui/composer/use-take-back.js +94 -72
  152. package/dist/chat/ui/diff/ApprovalReview.js +101 -23
  153. package/dist/chat/ui/diff/DiffFallbackBoundary.js +21 -13
  154. package/dist/chat/ui/diff/DiffOmitted.js +56 -6
  155. package/dist/chat/ui/diff/DiffStack.js +92 -8
  156. package/dist/chat/ui/diff/DiffUnavailable.js +56 -6
  157. package/dist/chat/ui/diff/EditorEditDiff.js +263 -51
  158. package/dist/chat/ui/diff/EditorPatchDiff.js +39 -14
  159. package/dist/chat/ui/diff/LegacyDiffPool.js +27 -10
  160. package/dist/chat/ui/diff/LegacyEditDiff.js +97 -20
  161. package/dist/chat/ui/diff/LegacyUnifiedDiff.js +172 -42
  162. package/dist/chat/ui/diff/RawDiff.js +65 -5
  163. package/dist/chat/ui/diff/ReviewDiff.js +77 -5
  164. package/dist/chat/ui/diff/diff-props.js +1 -1
  165. package/dist/chat/ui/diff/diff-source-budget.js +46 -35
  166. package/dist/chat/ui/diff/editor-edit-model.js +18 -14
  167. package/dist/chat/ui/diff/review-plan.js +63 -40
  168. package/dist/chat/ui/diff/review-settle.js +24 -9
  169. package/dist/chat/ui/diff/syntax-runtime.js +236 -201
  170. package/dist/chat/ui/diff/use-review-stack.js +146 -25
  171. package/dist/chat/ui/diff-syntax.js +70 -58
  172. package/dist/chat/ui/diff-theme.js +31 -12
  173. package/dist/chat/ui/file-links.js +39 -10
  174. package/dist/chat/ui/find-reveal.js +29 -8
  175. package/dist/chat/ui/full-diff.js +17 -12
  176. package/dist/chat/ui/highlighter.js +143 -113
  177. package/dist/chat/ui/icons.js +16 -7
  178. package/dist/chat/ui/index.js +1 -1
  179. package/dist/chat/ui/intelligent-ui/UiBlockFrame.js +556 -82
  180. package/dist/chat/ui/intelligent-ui/UiFallbackPart.js +73 -5
  181. package/dist/chat/ui/intelligent-ui/UiReply.js +666 -262
  182. package/dist/chat/ui/intelligent-ui/block-local.js +59 -31
  183. package/dist/chat/ui/intelligent-ui/chart-data.js +77 -67
  184. package/dist/chat/ui/intelligent-ui/node-text.js +77 -91
  185. package/dist/chat/ui/intelligent-ui/registry.js +46 -46
  186. package/dist/chat/ui/intelligent-ui/render-context.js +1 -1
  187. package/dist/chat/ui/intelligent-ui/renderers/base-land.js +1 -1
  188. package/dist/chat/ui/intelligent-ui/renderers/binding.js +3 -3
  189. package/dist/chat/ui/intelligent-ui/renderers/chart.js +1223 -123
  190. package/dist/chat/ui/intelligent-ui/renderers/choices.js +312 -34
  191. package/dist/chat/ui/intelligent-ui/renderers/content.js +1241 -86
  192. package/dist/chat/ui/intelligent-ui/renderers/data.js +619 -54
  193. package/dist/chat/ui/intelligent-ui/renderers/inputs.js +868 -67
  194. package/dist/chat/ui/intelligent-ui/renderers/links.js +366 -62
  195. package/dist/chat/ui/intelligent-ui/renderers/map-slots.js +118 -60
  196. package/dist/chat/ui/intelligent-ui/renderers/map-style.js +85 -78
  197. package/dist/chat/ui/intelligent-ui/renderers/map-view.js +282 -217
  198. package/dist/chat/ui/intelligent-ui/renderers/map.js +621 -69
  199. package/dist/chat/ui/intelligent-ui/renderers/place-point.js +15 -8
  200. package/dist/chat/ui/intelligent-ui/renderers/structure.js +262 -20
  201. package/dist/chat/ui/intelligent-ui/renderers/text.js +674 -49
  202. package/dist/chat/ui/intelligent-ui/reply-parts.js +24 -15
  203. package/dist/chat/ui/intelligent-ui/table-data.js +97 -57
  204. package/dist/chat/ui/intelligent-ui/tones.js +31 -31
  205. package/dist/chat/ui/intelligent-ui/use-in-view.js +30 -12
  206. package/dist/chat/ui/intelligent-ui/use-later.js +28 -13
  207. package/dist/chat/ui/intelligent-ui/use-truncated.js +45 -21
  208. package/dist/chat/ui/intelligent-ui/use-ui-links.js +296 -109
  209. package/dist/chat/ui/intelligent-ui/use-ui-queries.js +256 -246
  210. package/dist/chat/ui/markdown-blocks.js +296 -262
  211. package/dist/chat/ui/mermaid.d.ts +8 -20
  212. package/dist/chat/ui/mermaid.js +47 -84
  213. package/dist/chat/ui/quote-selection.js +32 -29
  214. package/dist/chat/ui/rehype-chips.js +35 -23
  215. package/dist/chat/ui/rehype-fade.js +28 -17
  216. package/dist/chat/ui/remark-html-as-text.js +22 -15
  217. package/dist/chat/ui/reply-context.js +1 -1
  218. package/dist/chat/ui/reveal.js +161 -72
  219. package/dist/chat/ui/rows/ForksRow.js +226 -24
  220. package/dist/chat/ui/rows/GeneratedImageRow.js +541 -87
  221. package/dist/chat/ui/rows/MessageRows.js +1706 -159
  222. package/dist/chat/ui/rows/RowContainer.js +57 -7
  223. package/dist/chat/ui/rows/Rows.js +463 -54
  224. package/dist/chat/ui/rows/SubagentRow.js +577 -60
  225. package/dist/chat/ui/rows/TaskRow.js +359 -40
  226. package/dist/chat/ui/rows/VisualRow.js +233 -11
  227. package/dist/chat/ui/rows/WorkRows.d.ts +3 -0
  228. package/dist/chat/ui/rows/WorkRows.js +1945 -164
  229. package/dist/chat/ui/rows/WorkflowRow.js +756 -37
  230. package/dist/chat/ui/rows/row-rhythm.js +27 -17
  231. package/dist/chat/ui/theme-watch.js +26 -23
  232. package/dist/chat/ui/timestamp-format.js +4 -1
  233. package/dist/chat/ui/use-diff-source-tokens.js +70 -34
  234. package/dist/chat/ui/useToggleSet.js +51 -19
  235. package/dist/chat/ui/visual-theme.js +153 -128
  236. package/dist/chat/visual-bridge.js +123 -123
  237. package/dist/chat/visual-viewport.js +94 -83
  238. package/dist/chat/visuals.js +13 -5
  239. package/dist/host-hooks.d.ts +29 -0
  240. package/dist/host-hooks.js +78 -0
  241. package/dist/host.js +120 -48
  242. package/dist/index.js +1 -1
  243. package/dist/lazy.js +38 -32
  244. package/dist/locales/en/agent-chat.d.ts +42 -0
  245. package/dist/locales/en/agent-chat.js +755 -713
  246. package/dist/locales/en/agent-prompts.js +42 -42
  247. package/dist/locales/en/agent-providers.d.ts +1 -0
  248. package/dist/locales/en/agent-providers.js +166 -165
  249. package/dist/locales/en/agent-usage.d.ts +6 -0
  250. package/dist/locales/en/agent-usage.js +129 -123
  251. package/dist/locales/nl/agent-chat.d.ts +42 -0
  252. package/dist/locales/nl/agent-chat.js +755 -713
  253. package/dist/locales/nl/agent-prompts.js +42 -42
  254. package/dist/locales/nl/agent-providers.d.ts +1 -0
  255. package/dist/locales/nl/agent-providers.js +166 -165
  256. package/dist/locales/nl/agent-usage.d.ts +6 -0
  257. package/dist/locales/nl/agent-usage.js +129 -123
  258. package/dist/locales.js +14 -14
  259. package/dist/mounted-registry.js +31 -30
  260. package/dist/port-transport.js +122 -111
  261. package/dist/prompts/index.js +1 -1
  262. package/dist/prompts/logic/focus.js +15 -14
  263. package/dist/prompts/logic/keys.js +93 -59
  264. package/dist/prompts/logic/prompts.js +27 -14
  265. package/dist/prompts/logic/subjects.js +60 -46
  266. package/dist/prompts/logic/useFocusAfterAnswer.js +60 -20
  267. package/dist/prompts/logic/usePromptSession.js +88 -77
  268. package/dist/prompts/ui/ApprovalBody.js +246 -27
  269. package/dist/prompts/ui/PromptActions.js +215 -9
  270. package/dist/prompts/ui/PromptCard.js +215 -36
  271. package/dist/prompts/ui/PromptView.js +372 -34
  272. package/dist/prompts/ui/QuestionBody.js +269 -73
  273. package/dist/providers/AccountColors.js +36 -5
  274. package/dist/providers/AccountDetail.js +683 -50
  275. package/dist/providers/AccountVariables.js +139 -32
  276. package/dist/providers/AddAccountForm.js +125 -38
  277. package/dist/providers/CliDetail.js +343 -36
  278. package/dist/providers/InstallPage.js +644 -59
  279. package/dist/providers/ProviderList.d.ts +2 -1
  280. package/dist/providers/ProviderList.js +215 -23
  281. package/dist/providers/ProvidersPane.d.ts +4 -2
  282. package/dist/providers/ProvidersPane.js +244 -64
  283. package/dist/providers/account-actions.js +54 -30
  284. package/dist/providers/index.js +1 -1
  285. package/dist/providers/install-steps.js +12 -9
  286. package/dist/providers/pane-target.d.ts +3 -0
  287. package/dist/providers/pane-target.js +34 -13
  288. package/dist/providers/parts.js +62 -12
  289. package/dist/providers/provider-abilities.js +5 -5
  290. package/dist/providers/provider-groups.js +75 -40
  291. package/dist/providers/variables.js +73 -39
  292. package/dist/scope.js +6 -6
  293. package/dist/settings/sections.js +18 -18
  294. package/dist/state/chats.js +404 -192
  295. package/dist/state/index.js +1 -1
  296. package/dist/state/provider-accounts.js +70 -47
  297. package/dist/state/providers.js +45 -16
  298. package/dist/state/usage.js +154 -77
  299. package/dist/storage.js +69 -68
  300. package/dist/testing/index.js +1 -1
  301. package/dist/testing/prompts.js +105 -67
  302. package/dist/transport.js +10 -10
  303. package/dist/ui/PlainTextarea.js +27 -3
  304. package/dist/usage/LimitsList.js +646 -40
  305. package/dist/usage/UsageBreakdown.js +879 -38
  306. package/dist/usage/UsageChart.js +311 -32
  307. package/dist/usage/UsageDialog.js +46 -4
  308. package/dist/usage/UsageLimits.js +467 -34
  309. package/dist/usage/UsageLimitsCard.js +95 -11
  310. package/dist/usage/UsagePage.js +817 -96
  311. package/dist/usage/UsagePane.js +164 -10
  312. package/dist/usage/UsageSummary.js +151 -6
  313. package/dist/usage/UsageTiles.js +164 -14
  314. package/dist/usage/format.js +32 -32
  315. package/dist/usage/index.d.ts +1 -1
  316. package/dist/usage/index.js +2 -1
  317. package/dist/usage/limit-groups.d.ts +2 -0
  318. package/dist/usage/limit-groups.js +77 -59
  319. package/dist/usage/limits.js +78 -43
  320. package/dist/usage/money.js +20 -4
  321. package/dist/usage/summary.js +85 -64
  322. package/package.json +9 -9
  323. package/src/agents/ModelOptionControl.tsx +3 -1
  324. package/src/agents/option-words.ts +27 -0
  325. package/src/chat/account-choice.ts +41 -1
  326. package/src/chat/composer-effort.ts +32 -0
  327. package/src/chat/forks.ts +4 -5
  328. package/src/chat/index.ts +3 -1
  329. package/src/chat/logic/effort.ts +32 -0
  330. package/src/chat/logic/picker-entries.ts +45 -10
  331. package/src/chat/logic/resume-compaction.ts +95 -17
  332. package/src/chat/logic/timeline.ts +11 -3
  333. package/src/chat/logic/workflow.ts +86 -4
  334. package/src/chat/preferences.ts +35 -0
  335. package/src/chat/subagent-view.ts +2 -3
  336. package/src/chat/ui/AnsiOutput.tsx +1 -2
  337. package/src/chat/ui/BookmarkSubmenu.tsx +3 -3
  338. package/src/chat/ui/ChatActivity.tsx +14 -22
  339. package/src/chat/ui/ChatReferenceChip.tsx +2 -3
  340. package/src/chat/ui/Composer.tsx +15 -7
  341. package/src/chat/ui/EditDiff.tsx +2 -3
  342. package/src/chat/ui/LimitState.tsx +2 -2
  343. package/src/chat/ui/Markdown.tsx +10 -8
  344. package/src/chat/ui/MermaidBlock.tsx +57 -49
  345. package/src/chat/ui/Pickers.tsx +49 -29
  346. package/src/chat/ui/PromptComposer.tsx +7 -9
  347. package/src/chat/ui/ResumeCompactionDock.tsx +16 -4
  348. package/src/chat/ui/RunSettings.tsx +67 -28
  349. package/src/chat/ui/Scrubber.tsx +3 -3
  350. package/src/chat/ui/ShellCommand.tsx +2 -0
  351. package/src/chat/ui/SubagentInfo.tsx +2 -3
  352. package/src/chat/ui/SubagentTimeline.tsx +12 -16
  353. package/src/chat/ui/Timeline.tsx +6 -6
  354. package/src/chat/ui/UnifiedDiff.tsx +3 -4
  355. package/src/chat/ui/VisualFrame.tsx +5 -8
  356. package/src/chat/ui/code-theme.ts +4 -4
  357. package/src/chat/ui/composer/placeholder.ts +2 -2
  358. package/src/chat/ui/composer/use-compaction-offer.ts +36 -16
  359. package/src/chat/ui/composer/use-composer-menus.ts +4 -9
  360. package/src/chat/ui/composer/use-composer-submit.ts +2 -1
  361. package/src/chat/ui/composer/use-effort-stepper.ts +56 -0
  362. package/src/chat/ui/composer/use-notice.ts +2 -2
  363. package/src/chat/ui/diff/EditorEditDiff.tsx +9 -13
  364. package/src/chat/ui/diff/LegacyEditDiff.tsx +12 -17
  365. package/src/chat/ui/diff/LegacyUnifiedDiff.tsx +13 -21
  366. package/src/chat/ui/diff/use-review-stack.ts +4 -4
  367. package/src/chat/ui/diff-theme.ts +3 -3
  368. package/src/chat/ui/file-links.ts +2 -2
  369. package/src/chat/ui/intelligent-ui/UiReply.tsx +2 -0
  370. package/src/chat/ui/intelligent-ui/renderers/chart.tsx +3 -3
  371. package/src/chat/ui/intelligent-ui/renderers/content.tsx +5 -6
  372. package/src/chat/ui/intelligent-ui/renderers/map-view.tsx +2 -1
  373. package/src/chat/ui/intelligent-ui/renderers/map.tsx +2 -1
  374. package/src/chat/ui/intelligent-ui/use-truncated.ts +16 -19
  375. package/src/chat/ui/intelligent-ui/use-ui-links.ts +3 -3
  376. package/src/chat/ui/mermaid.ts +52 -96
  377. package/src/chat/ui/rows/ForksRow.tsx +3 -5
  378. package/src/chat/ui/rows/GeneratedImageRow.tsx +2 -1
  379. package/src/chat/ui/rows/MessageRows.tsx +12 -21
  380. package/src/chat/ui/rows/SubagentRow.tsx +3 -3
  381. package/src/chat/ui/rows/TaskRow.tsx +1 -1
  382. package/src/chat/ui/rows/WorkRows.tsx +11 -6
  383. package/src/chat/ui/rows/WorkflowRow.tsx +144 -54
  384. package/src/chat/ui/useToggleSet.ts +15 -18
  385. package/src/chat/ui/visual-theme.ts +2 -3
  386. package/src/host-hooks.ts +113 -0
  387. package/src/lazy.tsx +2 -0
  388. package/src/locales/en/agent-chat.ts +142 -108
  389. package/src/locales/en/agent-prompts.ts +7 -7
  390. package/src/locales/en/agent-providers.ts +35 -34
  391. package/src/locales/en/agent-usage.ts +26 -20
  392. package/src/locales/nl/agent-chat.ts +134 -100
  393. package/src/locales/nl/agent-prompts.ts +5 -5
  394. package/src/locales/nl/agent-providers.ts +33 -32
  395. package/src/locales/nl/agent-usage.ts +25 -20
  396. package/src/providers/CliDetail.tsx +2 -1
  397. package/src/providers/InstallPage.tsx +2 -4
  398. package/src/providers/ProviderList.tsx +12 -2
  399. package/src/providers/ProvidersPane.tsx +45 -15
  400. package/src/providers/pane-target.ts +20 -4
  401. package/src/state/chats.ts +2 -2
  402. package/src/usage/LimitsList.tsx +12 -13
  403. package/src/usage/UsageBreakdown.tsx +17 -6
  404. package/src/usage/UsageLimits.tsx +2 -1
  405. package/src/usage/UsagePage.tsx +3 -3
  406. package/src/usage/index.ts +1 -1
  407. package/src/usage/limit-groups.ts +12 -0
  408. package/src/usage/limits.ts +2 -2
  409. package/src/usage/money.ts +1 -2
@@ -2,20 +2,32 @@ import { useTranslation } from 'react-i18next';
2
2
  import { Minimize2 } from 'lucide-react';
3
3
  import { formatTokens } from '@adecore/ui/format';
4
4
  import { Button, Icon } from '@adecore/ui';
5
+ import type { ResumeCompactionReason } from '../logic/resume-compaction';
6
+
7
+ interface ResumeCompactionDockProps {
8
+ tokens: number;
9
+ /* Why the next message pays for the whole thread; without it the chat only lay still. */
10
+ reason?: ResumeCompactionReason;
11
+ onCompact(): void;
12
+ onDismiss(): void;
13
+ }
5
14
 
6
15
  /*
7
- * The offer at the top of the composer, for a chat that lay still long enough that the next message
16
+ * The offer at the top of the composer, for a chat whose prompt cache went cold, so the next message
8
17
  * pays for the whole thread again. It takes no keys and blocks nothing: typing and sending keeps the
9
18
  * full history, which is what a person who ignores this does.
10
19
  */
11
- export function ResumeCompactionDock({ tokens, onCompact, onDismiss }: { tokens: number; onCompact(): void; onDismiss(): void }) {
20
+ export function ResumeCompactionDock({ tokens, reason = 'idle', onCompact, onDismiss }: ResumeCompactionDockProps) {
12
21
  const { t } = useTranslation('agent-chat');
22
+ const cold = reason === 'cache-expired';
13
23
  return (
14
24
  <div className="flex items-center gap-2 border-b border-border px-3.5 py-2.5">
15
25
  <Icon icon={Minimize2} size={16} className="shrink-0 text-text-faint" />
16
26
  <span className="flex min-w-0 grow flex-col">
17
- <span className="truncate text-sm text-text">{t('resumeCompaction.title')}</span>
18
- <span className="truncate text-xs tabular-nums text-text-faint">{t('resumeCompaction.carried', { tokens: formatTokens(tokens) })}</span>
27
+ <span className="truncate text-sm text-text">{cold ? t('resumeCompaction.cacheExpired.title') : t('resumeCompaction.title')}</span>
28
+ <span className="truncate text-xs tabular-nums text-text-faint">
29
+ {t(cold ? 'resumeCompaction.cacheExpired.carried' : 'resumeCompaction.carried', { tokens: formatTokens(tokens) })}
30
+ </span>
19
31
  </span>
20
32
  <Button size="sm" onClick={onDismiss}>
21
33
  {t('resumeCompaction.keep')}
@@ -15,9 +15,10 @@ import {
15
15
  Shield,
16
16
  ShieldAlert,
17
17
  SlidersHorizontal,
18
+ Star,
18
19
  type LucideIcon
19
20
  } from 'lucide-react';
20
- import type { AgentKind, ChatUsage, ModelInfo, ModelOptionDescriptor, ModelSelection, ProviderInfo, RuntimeMode } from '@adecore/agent-contracts';
21
+ import type { AgentKind, ChatUsage, ModelInfo, ModelOptionDescriptor, ModelSelection, ProviderInfo, RuntimeMode, UsageWindow } from '@adecore/agent-contracts';
21
22
  import { AccountDot } from '../../agents/AccountDot';
22
23
  import { limitsOfAccount, sessionWindow } from '../../agents/account-limits';
23
24
  import { canContinueOn } from '../../agents/accounts';
@@ -25,12 +26,15 @@ import { AgentIcon } from '../../agents/AgentIcon';
25
26
  import type { AccountChoice } from '../account-choice';
26
27
  import { modelName, shortModelName } from '../../agents/model-name';
27
28
  import { ModelMarks } from './ModelMarks';
28
- import { modelsByVendor, searchModels, shortlistOf, wantsModelSearch } from '../logic/picker-entries';
29
+ import { favoritesFirst, searchModels, shortlistOf, wantsModelSearch } from '../logic/picker-entries';
30
+ import { favoriteModelsOf, toggleFavoriteModel, useChatPreferences } from '../preferences';
29
31
  import { CONTEXT_OPTION, contextFraction, contextSegments, orderOptions, type ContextPart } from '../logic/context-usage';
30
32
  import { RUNTIME_MODES } from '../runtime-modes';
33
+ import { localizedOption } from '../../agents/option-words';
31
34
  import { formatClock, formatWeekdayClock, isSameDay, formatPercent, formatTokens } from '@adecore/ui/format';
32
35
  import { Icon, Menu, Tooltip, useNow } from '@adecore/ui';
33
36
  import { useChatHost } from '../../host';
37
+ import { evenPace, paceText } from '../../usage/limit-groups';
34
38
  import { useUsageLimits } from '../../usage/limits';
35
39
 
36
40
  const MINUTE_MS = 60_000;
@@ -143,7 +147,7 @@ export function RunSettings({
143
147
  const owner = providers.find((entry) => entry.kind === provider);
144
148
  const current = modelName(selection.model, owner?.models);
145
149
  const short = shortModelName(current, owner?.models);
146
- const options = orderOptions(model?.options ?? []);
150
+ const options = orderOptions((model?.options ?? []).map(localizedOption));
147
151
  const summary = options.flatMap((option) => {
148
152
  const label = optionSummary(option, selection);
149
153
  return label === null ? [] : [{ id: option.id, label, ring: option.id === CONTEXT_OPTION }];
@@ -171,7 +175,9 @@ export function RunSettings({
171
175
  };
172
176
 
173
177
  const searchable = wantsModelSearch(providers);
174
- const found = query.trim() === '' ? null : searchModels(providers, query);
178
+ const favorites = useChatPreferences((preferences) => preferences.favoriteModels);
179
+ const favoritesOf = (kind: AgentKind): ReadonlySet<string> => favoriteModelsOf(favorites, kind);
180
+ const found = query.trim() === '' ? null : searchModels(providers, query, favoritesOf);
175
181
 
176
182
  // The menu moves the focus to its popup as it opens, so the search field takes it a frame later.
177
183
  useEffect(() => {
@@ -226,28 +232,49 @@ export function RunSettings({
226
232
  }
227
233
  };
228
234
 
229
- const modelRow = (entry: ProviderInfo, row: ModelInfo) => (
230
- <Menu.RadioItem key={row.slug} value={modelValue(entry.kind, row.slug)} closeOnClick>
231
- <AgentIcon kind={entry.kind} size={14} className="shrink-0" />
232
- <span className="min-w-0 truncate">{row.name}</span>
233
- <ModelMarks model={row} />
234
- </Menu.RadioItem>
235
- );
235
+ // The name wraps rather than truncates, so a long name a provider gives is read whole.
236
+ const modelRow = (entry: ProviderInfo, row: ModelInfo) => {
237
+ const starred = favoritesOf(entry.kind).has(row.slug);
238
+ return (
239
+ <Menu.Row key={row.slug} aria-label={row.name}>
240
+ <Menu.RadioItem value={modelValue(entry.kind, row.slug)} closeOnClick>
241
+ <AgentIcon kind={entry.kind} size={14} className="shrink-0" />
242
+ <span className="min-w-0 break-words">{row.name}</span>
243
+ <ModelMarks model={row} />
244
+ </Menu.RadioItem>
245
+ <Menu.RowAction
246
+ icon={Star}
247
+ label={starred ? t('pickers.model.unstar') : t('pickers.model.star')}
248
+ iconClassName={starred ? 'fill-current text-accent' : 'text-text-faint'}
249
+ closeOnClick={false}
250
+ onClick={() => toggleFavoriteModel(entry.kind, row.slug)}
251
+ />
252
+ </Menu.Row>
253
+ );
254
+ };
236
255
 
237
- // Every model of one provider, under a label per maker where the provider names them.
238
- const byMaker = (entry: ProviderInfo, models: ModelInfo[]) =>
239
- modelsByVendor(models).map((row, i, sorted) => (
240
- <Fragment key={row.slug}>
241
- {row.vendor !== undefined && sorted[i - 1]?.vendor !== row.vendor && <Menu.GroupLabel>{row.vendor}</Menu.GroupLabel>}
242
- {modelRow(entry, row)}
243
- </Fragment>
244
- ));
256
+ // Every model of one provider, the starred ones first and the rest under a label per maker where the provider names them.
257
+ const byMaker = (entry: ProviderInfo, models: ModelInfo[]) => {
258
+ const starred = favoritesOf(entry.kind);
259
+ return favoritesFirst(models, starred).map((row, i, sorted) => {
260
+ const previous = sorted[i - 1];
261
+ const labeled =
262
+ row.vendor !== undefined && !starred.has(row.slug) && (previous === undefined || starred.has(previous.slug) || previous.vendor !== row.vendor);
263
+ return (
264
+ <Fragment key={row.slug}>
265
+ {labeled && <Menu.GroupLabel>{row.vendor}</Menu.GroupLabel>}
266
+ {modelRow(entry, row)}
267
+ </Fragment>
268
+ );
269
+ });
270
+ };
245
271
 
246
- // A provider with many models shows the default and the chosen one, with all of them one submenu away.
272
+ // A provider with many models shows the starred ones, the default and the chosen one, with all of them one submenu away.
247
273
  const providerModels = (entry: ProviderInfo, models: ModelInfo[]) => {
248
- const shortlist = shortlistOf(entry.kind, models, { provider, model: selection.model });
274
+ const starred = favoritesOf(entry.kind);
275
+ const shortlist = shortlistOf(entry.kind, models, { provider, model: selection.model }, starred);
249
276
  if (shortlist === null) {
250
- return models.map((row) => modelRow(entry, row));
277
+ return [...models.filter((row) => starred.has(row.slug)), ...models.filter((row) => !starred.has(row.slug))].map((row) => modelRow(entry, row));
251
278
  }
252
279
  return (
253
280
  <>
@@ -488,7 +515,7 @@ function AccountRows({ choice, locked }: { choice: AccountChoice; locked(id: str
488
515
  <AccountDot color={entry.account.color} />
489
516
  <span className="truncate">{choice.nameOf(entry)}</span>
490
517
  </span>
491
- {session !== null && <SessionLine used={session.used} resetsAt={session.resetsAt} now={now} />}
518
+ {session !== null && <SessionLine window={session} now={now} />}
492
519
  </span>
493
520
  </Menu.RadioItem>
494
521
  );
@@ -502,16 +529,28 @@ function AccountRows({ choice, locked }: { choice: AccountChoice; locked(id: str
502
529
  });
503
530
  }
504
531
 
505
- function SessionLine({ used, resetsAt, now }: { used: number; resetsAt: number | null; now: number }) {
532
+ function SessionLine({ window, now }: { window: UsageWindow; now: number }) {
506
533
  const { t } = useTranslation('agent-chat');
507
- const percent = Math.round(used * 100);
508
- const tone = sessionTone(used);
534
+ const percent = Math.round(window.used * 100);
535
+ const tone = sessionTone(window.used);
536
+ const mark = evenPace(window, now)?.percent ?? null;
537
+ const resetsAt = window.resetsAt;
509
538
  return (
510
539
  <span className="flex items-center gap-2 text-xs text-text-faint tabular-nums">
511
- <span aria-hidden className="relative h-1 w-14 shrink-0 overflow-hidden rounded-full bg-surface-sunken">
540
+ <span aria-hidden className="relative h-1 w-14 shrink-0 rounded-full bg-surface-sunken">
512
541
  <span className={clsx('absolute inset-y-0 left-0 rounded-full', tone.bar)} style={{ width: `${Math.min(100, percent)}%` }} />
542
+ {/* Where an even spread over the window would stand, as the usage bars mark it. */}
543
+ {mark !== null && (
544
+ <span
545
+ className={clsx('absolute -inset-y-0.5 w-px -translate-x-1/2', mark <= percent ? 'bg-surface-raised' : 'bg-text-muted')}
546
+ style={{ left: `${mark}%` }}
547
+ />
548
+ )}
549
+ </span>
550
+ <span aria-hidden className={tone.text}>
551
+ {formatPercent(percent)}
513
552
  </span>
514
- <span className={tone.text}>{formatPercent(percent)}</span>
553
+ <span className="sr-only">{paceText(window, now)}</span>
515
554
  {resetsAt !== null && (
516
555
  <span className="whitespace-nowrap">
517
556
  · {t('pickers.account.resets', { time: isSameDay(resetsAt, now) ? formatClock(resetsAt) : formatWeekdayClock(resetsAt) })}
@@ -104,9 +104,9 @@ export const Scrubber = memo(function Scrubber({
104
104
  return () => observer.disconnect();
105
105
  }, [strip]);
106
106
 
107
- const kinds = useMemo(() => ticks.map((tick) => tick.kind), [ticks]);
108
- const marked = useMemo(() => ticks.map((tick) => tick.bookmark !== null), [ticks]);
109
- const layout = useMemo(() => layoutTicks(kinds, height, marked, found), [kinds, height, marked, found]);
107
+ const kinds = ticks.map((tick) => tick.kind);
108
+ const marked = ticks.map((tick) => tick.bookmark !== null);
109
+ const layout = layoutTicks(kinds, height, marked, found);
110
110
  const currentSlot = foundCurrent === null ? null : slotOf(layout, foundCurrent);
111
111
  const inView = firstInView === null || lastInView === null ? null : { first: firstInView, last: lastInView };
112
112
  const hoveredTick = hovered === null ? null : (ticks[hovered] ?? null);
@@ -166,6 +166,8 @@ function drawNamed(command: string, runs: readonly Run[] | null, names: readonly
166
166
  * caller knows, and `names` draws words as the names a host gave them, both as offsets into `command`.
167
167
  */
168
168
  export function ShellCommand({ command, code = NO_CODE, names = NO_NAMES }: { command: string; code?: readonly EmbeddedCode[]; names?: readonly NamedWord[] }) {
169
+ // The highlighter's cache fills in place; compiled, the runs it gave before its grammars loaded would stay.
170
+ 'use no memo';
169
171
  const theme = useCodeTheme();
170
172
  const { custom } = useChatHost().code;
171
173
  const highlighter = useCodeHighlighter();
@@ -7,20 +7,19 @@ import { SubagentActions, SubagentBreadcrumb } from './SubagentControls';
7
7
  import { useNow } from '@adecore/ui';
8
8
  import { useModelName } from '../../agents/model-name';
9
9
  import { useChatRow } from '../../state/chats';
10
- import { useChatHost } from '../../host';
11
10
  import { useChatScope } from '../../scope';
11
+ import { useHostTask } from '../../host-hooks';
12
12
 
13
13
  /*
14
14
  * What a sub-agent is, over its conversation: the way down to it, its state and time, the model it runs on
15
15
  * and what it spent, and the way out. Only the way down and out for a conversation whose row the thread lost.
16
16
  */
17
17
  export function SubagentInfo({ chatId, toolUseId }: { chatId: string; toolUseId: string }) {
18
- const host = useChatHost();
19
18
  const { t } = useTranslation('agent-chat');
20
19
  const { id } = useChatScope();
21
20
  const item = useSubagentItem(chatId, toolUseId);
22
21
  const provider = useChatRow(chatId, (row) => row?.info.provider);
23
- const task = host.tasks.useTask(id, item === null ? null : taskIdOf(item));
22
+ const task = useHostTask(id, item === null ? null : taskIdOf(item));
24
23
  const modelName = useModelName(provider, item?.model ?? '');
25
24
  const now = useNow(1000, item?.status === 'running');
26
25
  const word = item === null ? null : statusWordOf(item, task);
@@ -1,4 +1,4 @@
1
- import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
1
+ import { useCallback, useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore } from 'react';
2
2
  import { useTranslation } from 'react-i18next';
3
3
  import { Bot, LoaderCircle } from 'lucide-react';
4
4
  import { deriveTimelineRows } from '../logic/timeline';
@@ -34,8 +34,8 @@ export function SubagentTimeline({ chatId, toolUseId }: { chatId: string; toolUs
34
34
  const scope = useChatScope();
35
35
  const { commandLooks } = useChatHost();
36
36
  const connected = useSyncExternalStore(
37
- useCallback((changed) => scope.transport.subscribeStatus(changed), [scope]),
38
- useCallback(() => scope.transport.status === 'open', [scope]),
37
+ (changed) => scope.transport.subscribeStatus(changed),
38
+ () => scope.transport.status === 'open',
39
39
  () => false
40
40
  );
41
41
  const { trail, show } = useSubagentTrail(chatId);
@@ -46,9 +46,9 @@ export function SubagentTimeline({ chatId, toolUseId }: { chatId: string; toolUs
46
46
  const threadRef = useRef<HTMLDivElement>(null);
47
47
  const [target, setTarget] = useState<TimelineTarget>(EMPTY_TARGET);
48
48
  const followRef = useRef(true);
49
- const stopFollowing = useCallback(() => {
49
+ const stopFollowing = () => {
50
50
  followRef.current = false;
51
- }, []);
51
+ };
52
52
  const groups = useToggleSet(stopFollowing);
53
53
  const subagents = useToggleSet(stopFollowing);
54
54
  // The row being read when an earlier page was asked for, so it stays where it was once that page is in.
@@ -72,17 +72,13 @@ export function SubagentTimeline({ chatId, toolUseId }: { chatId: string; toolUs
72
72
  }
73
73
  }, [state.unsupported, scope]);
74
74
 
75
- const rows = useMemo(
76
- () =>
77
- deriveTimelineRows(state.items, {
78
- expandedGroups: groups.ids,
79
- expandedTurns: NO_TURNS,
80
- expandedSubagents: subagents.ids,
81
- activeTurnId: null,
82
- commandLooks
83
- }),
84
- [state.items, groups.ids, subagents.ids, commandLooks]
85
- );
75
+ const rows = deriveTimelineRows(state.items, {
76
+ expandedGroups: groups.ids,
77
+ expandedTurns: NO_TURNS,
78
+ expandedSubagents: subagents.ids,
79
+ activeTurnId: null,
80
+ commandLooks
81
+ });
86
82
 
87
83
  useLayoutEffect(() => {
88
84
  const element = scrollRef.current;
@@ -79,6 +79,7 @@ import { FileLinkContext } from './file-links';
79
79
  import { AgentIcon } from '../../agents/AgentIcon';
80
80
  import { useModelName } from '../../agents/model-name';
81
81
  import { ContextMenu, EmptyState, ErrorBoundary, Icon, SectionLabel } from '@adecore/ui';
82
+ import { useHostContextSources, useHostHidesFolder, useHostThreadCards, useHostTimelineFind, useHostWelcome } from '../../host-hooks';
82
83
 
83
84
  const ESTIMATED_ROW_PX = 56;
84
85
  // How long a jump waits for a thread that is still loading before it calls the item missing.
@@ -95,12 +96,11 @@ function showRowAtTop(virtualizer: Virtualizer<HTMLDivElement, Element>, followR
95
96
  * and the folder it works in, so a first question can lean on all three.
96
97
  */
97
98
  function EmptyThread({ chatId }: { chatId: string }) {
98
- const host = useChatHost();
99
99
  const { t } = useTranslation('agent-chat');
100
100
  const info = useChatRow(chatId, (row) => row?.info ?? null);
101
101
  const model = useModelName(info?.provider, info?.selection.model ?? '');
102
- const sources = host.useContextSources(chatId);
103
- const hidesFolder = host.useHidesFolder(chatId);
102
+ const sources = useHostContextSources(chatId);
103
+ const hidesFolder = useHostHidesFolder(chatId);
104
104
  return (
105
105
  <div className="flex grow items-center justify-center">
106
106
  <div className="flex max-w-sm flex-col items-center gap-3 px-6 py-8 text-center">
@@ -262,7 +262,7 @@ export function Timeline({ chatId, composer, overlay }: { chatId: string; compos
262
262
  }
263
263
  );
264
264
  }, [order, items, groups.ids, turns.ids, subagents.ids, activeTurnId, forkedTurns, host.commandLooks]);
265
- const cards = host.useThreadCards(scopeId, chatId);
265
+ const cards = useHostThreadCards(scopeId, chatId);
266
266
  const drawsVisuals = host.visuals !== null;
267
267
  // Where the part of the thread this client holds begins, while there is a page before it.
268
268
  const heldFrom = cursor === null ? null : (items?.[order?.[0] ?? '']?.createdAt ?? null);
@@ -270,7 +270,7 @@ export function Timeline({ chatId, composer, overlay }: { chatId: string; compos
270
270
  () => withTimedRows(threadRows, timedRowsOf(cards, drawsVisuals ? (visuals ?? []) : null, heldFrom)),
271
271
  [threadRows, cards, drawsVisuals, visuals, heldFrom]
272
272
  );
273
- const welcomes = host.useWelcome(chatId);
273
+ const welcomes = useHostWelcome(chatId);
274
274
 
275
275
  const empty = rows.length === 0;
276
276
  // A chat that took a turn before is one whose thread has not come in yet, not one to greet.
@@ -475,7 +475,7 @@ export function Timeline({ chatId, composer, overlay }: { chatId: string; compos
475
475
  }),
476
476
  [chatId, onMainAgent, rows, items, order, virtualizer, turns.add, groups.add, subagents.add, stopFollowing]
477
477
  );
478
- const chatFind = host.useTimelineFind(findOptions);
478
+ const chatFind = useHostTimelineFind(findOptions);
479
479
  const searching = chatFind.open;
480
480
  // A find covers the whole thread, so every page before this one comes in while it is open.
481
481
  useEffect(() => {
@@ -1,4 +1,3 @@
1
- import { useMemo } from 'react';
2
1
  import { LazyBoundary } from '@adecore/ui';
3
2
  import { useTranslation } from 'react-i18next';
4
3
  import { lazyNamed } from '../../lazy';
@@ -20,11 +19,11 @@ export default function UnifiedDiff(props: UnifiedDiffProps) {
20
19
  const loadEngine = host.code.editor;
21
20
  const oldText = contents?.old ?? '';
22
21
  const newText = contents?.new ?? '';
23
- const sourceAllowed = useMemo(() => diffSourceBudget(oldText, newText) && patchSourceBudget(change.diff), [oldText, newText, change.diff]);
22
+ const sourceAllowed = diffSourceBudget(oldText, newText) && patchSourceBudget(change.diff);
24
23
  const legacyMode = !allowEditor || loadEngine === undefined;
25
24
  const hasHunks = /^@@/m.test(change.diff);
26
- const legacyAllowed = useMemo(() => legacyMode && legacyDiffBudget(oldText, newText, change.diff), [legacyMode, oldText, newText, change.diff]);
27
- const rawAllowed = useMemo(() => hasHunks || legacyDiffBudget('', '', change.diff), [hasHunks, change.diff]);
25
+ const legacyAllowed = legacyMode && legacyDiffBudget(oldText, newText, change.diff);
26
+ const rawAllowed = hasHunks || legacyDiffBudget('', '', change.diff);
28
27
  const omitted = <DiffOmitted path={change.path} />;
29
28
  const fallback = legacyAllowed ? (
30
29
  <DiffFallbackBoundary fallback={omitted} resetKeys={[change.path, change.diff, oldText, newText, diffStyle]}>
@@ -1,4 +1,4 @@
1
- import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
1
+ import { useEffect, useLayoutEffect, useRef, useState } from 'react';
2
2
  import { useTranslation } from 'react-i18next';
3
3
  import clsx from 'clsx';
4
4
  import { visualThemeFragment, type ChatVisual, type VisualAppearance, type VisualTheme } from '@adecore/agent-contracts/visual';
@@ -44,13 +44,10 @@ export function VisualFrame({ chatId, visual, fill = false, className }: VisualF
44
44
  const controllerRef = useRef<VisualViewportController | null>(null);
45
45
  const [viewportReady, setViewportReady] = useState(false);
46
46
  const [measure, width] = useMeasuredWidth();
47
- const setBox = useCallback(
48
- (node: HTMLDivElement | null) => {
49
- boxRef.current = node;
50
- measure(node);
51
- },
52
- [measure]
53
- );
47
+ const setBox = (node: HTMLDivElement | null) => {
48
+ boxRef.current = node;
49
+ measure(node);
50
+ };
54
51
  const theme = useVisualTheme(boxRef);
55
52
  const widthRef = useRef(width);
56
53
  const [reported, setReported] = useState<number | null>(null);
@@ -1,11 +1,11 @@
1
1
  import type { ThemeRegistration } from 'shiki';
2
- import { useChatHost, type ChatHost } from '../../host';
2
+ import { type ChatHost } from '../../host';
3
+ import { useHostCodeMode, useHostCodeThemes } from '../../host-hooks';
3
4
 
4
5
  /* The Shiki theme code is drawn in under the app's current light or dark. */
5
6
  export function useCodeTheme(): string {
6
- const { code } = useChatHost();
7
- const mode = code.useMode();
8
- const themes = code.useThemes();
7
+ const mode = useHostCodeMode();
8
+ const themes = useHostCodeThemes();
9
9
  return mode === 'dark' ? themes.dark : themes.light;
10
10
  }
11
11
 
@@ -1,7 +1,7 @@
1
1
  import { useMemo } from 'react';
2
2
  import { useTranslation } from 'react-i18next';
3
- import { useChatHost } from '../../../host';
4
3
  import { useChatScope } from '../../../scope';
4
+ import { useHostComposerPlaceholder } from '../../../host-hooks';
5
5
 
6
6
  /* The empty box names its sigils as key caps, so they read as keys to press rather than as punctuation. */
7
7
  function hintedPlaceholder(lead: string, joiner: string, hints: ReadonlyArray<{ key: string; label: string }>): HTMLElement {
@@ -31,7 +31,7 @@ interface PlaceholderInput {
31
31
  export function useHintedPlaceholder({ chatId, disabled, readOnly, mentionable, hasSkills }: PlaceholderInput): string | HTMLElement {
32
32
  const { t } = useTranslation('agent-chat');
33
33
  const scope = useChatScope();
34
- const lead = useChatHost().useComposerPlaceholder(scope.id, chatId);
34
+ const lead = useHostComposerPlaceholder(scope.id, chatId);
35
35
  return useMemo(() => {
36
36
  if (disabled) {
37
37
  return t('composer.placeholder.disconnected');
@@ -1,25 +1,45 @@
1
+ import { useEffect, useState } from 'react';
1
2
  import type { ChatInfo, ChatItem, ProviderCapabilities } from '@adecore/agent-contracts';
2
- import { useNow } from '@adecore/ui';
3
3
  import { useChatRow } from '../../../state/chats';
4
- import { RESUME_COMPACTION_TOKENS, resumeCompactionOffer } from '../../logic/resume-compaction';
4
+ import { RESUME_COMPACTION_TOKENS, resumeCompactionDueAt, resumeCompactionOffer, watchDueAt, type DueClock } from '../../logic/resume-compaction';
5
5
  import { useResumeCompactionDismissal } from '../../resume-compaction-dismissals';
6
6
 
7
- /* The offer to fold a long context before a chat that sat idle goes on, or null when there is none. */
7
+ const PAGE_CLOCK: DueClock = {
8
+ now: () => Date.now(),
9
+ setTimer: (run, ms) => setTimeout(run, ms),
10
+ clearTimer: (timer) => clearTimeout(timer as ReturnType<typeof setTimeout>),
11
+ onWake: (wake) => {
12
+ if (typeof window === 'undefined') {
13
+ return () => {};
14
+ }
15
+ window.addEventListener('focus', wake);
16
+ document.addEventListener('visibilitychange', wake);
17
+ return () => {
18
+ window.removeEventListener('focus', wake);
19
+ document.removeEventListener('visibilitychange', wake);
20
+ };
21
+ }
22
+ };
23
+
24
+ /* The offer to fold a long context before a chat whose cache went cold goes on, or null when there is none. */
8
25
  export function useCompactionOffer(chatId: string, info: ChatInfo, compaction: ProviderCapabilities['compaction'] | undefined) {
9
26
  const order = useChatRow(chatId, (row) => row?.order);
10
- const items = useChatRow(chatId, (row) => row?.structure);
27
+ const structure = useChatRow(chatId, (row) => row?.structure);
11
28
  const dismissedTurnId = useResumeCompactionDismissal(chatId);
12
- // The cheap half of the offer, so a chat that can never make one keeps no timer.
29
+ const [now, setNow] = useState(Date.now);
30
+ // The cheap half of the offer, so a chat that can never make one walks no thread.
13
31
  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
- });
32
+ const items = couldOffer ? (order ?? []).map((id) => structure?.[id]).filter((item): item is ChatItem => item !== undefined) : [];
33
+ const input = { info, compaction, items, dismissedTurnId };
34
+ const dueAt = couldOffer ? resumeCompactionDueAt(input) : null;
35
+
36
+ // A watch for the moment the offer comes due, so a chat that can never make one keeps none.
37
+ useEffect(() => {
38
+ if (dueAt === null) {
39
+ return;
40
+ }
41
+ return watchDueAt(dueAt, setNow, PAGE_CLOCK);
42
+ }, [dueAt]);
43
+
44
+ return dueAt === null ? null : resumeCompactionOffer({ ...input, now });
25
45
  }
@@ -23,6 +23,7 @@ import type { ComposerInputHandle } from '../ComposerInput';
23
23
  import type { ComposerDraft } from './use-composer-draft';
24
24
  import { inCode } from './keys';
25
25
  import { clampMenuIndex, composerMenuAria } from './menu-aria';
26
+ import { useHostReferableChats } from '../../../host-hooks';
26
27
 
27
28
  const SEARCH_DEBOUNCE_MS = 80;
28
29
  // What fits above the composer without turning the picker into a file tree.
@@ -62,7 +63,7 @@ export function useComposerMenus({ chatId, info, capabilities, providerName, dra
62
63
  const scope = useChatScope();
63
64
  const host = useChatHost();
64
65
  const { searchFiles } = host;
65
- const chats = host.useReferableChats();
66
+ const chats = useHostReferableChats();
66
67
  const [menuIndex, setMenuIndex] = useState(0);
67
68
  const [mention, setMention] = useState<MentionQuery | null>(null);
68
69
  const [skillQuery, setSkillQuery] = useState<MentionQuery | null>(null);
@@ -156,10 +157,7 @@ export function useComposerMenus({ chatId, info, capabilities, providerName, dra
156
157
  const mentionMenuOpen = !commandMenuOpen && !skillMenuOpen && mention !== null;
157
158
  // Results belong to the query that asked for them; a closed picker shows none while the next answer is on its way.
158
159
  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
- );
160
+ const chatMatches = mention === null ? [] : chatSuggestions(chats, mention.query, [chatId, ...draft.chats], CHAT_RESULTS);
163
161
  const mentionCount = chatMatches.length + files.length;
164
162
  // One menu is open at a time, so they share an id.
165
163
  const listId = useId();
@@ -167,10 +165,7 @@ export function useComposerMenus({ chatId, info, capabilities, providerName, dra
167
165
  const menuCount = commandMenuOpen ? commands.length : skillMenuOpen ? skillMatches.length : mentionCount;
168
166
  // The list can shrink under a stale index; every reader takes the one that still points at an option.
169
167
  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
- );
168
+ const inputAria = composerMenuAria({ label: t('composer.inputLabel'), listId, open: menuOpen, activeIndex, count: menuCount });
174
169
 
175
170
  /* Which picker the caret opens: `@` for a file, `$` for a skill, or neither. In code a sigil is just text. */
176
171
  const trackTriggers = (value: string, selection: TextRange, state: EditorState): void => {
@@ -19,6 +19,7 @@ import type { ComposerDraft } from './use-composer-draft';
19
19
  import type { ComposerMenus } from './use-composer-menus';
20
20
  import type { PromptHistory } from './use-prompt-history';
21
21
  import type { ComposerPromptHold } from './use-prompt-hold';
22
+ import { useHostReferableChats } from '../../../host-hooks';
22
23
 
23
24
  interface SubmitInput {
24
25
  chatId: string;
@@ -58,7 +59,7 @@ export function useComposerSubmit({
58
59
  const host = useChatHost();
59
60
  const scope = useChatScope();
60
61
  const { draft, setDraft } = box;
61
- const referable = host.useReferableChats();
62
+ const referable = useHostReferableChats();
62
63
 
63
64
  /* Opens the host's new chat once the host took the message; a host that answers nothing took it. */
64
65
  const afterSend = (sent: void | Promise<boolean>, andNew: boolean): void => {
@@ -0,0 +1,56 @@
1
+ import { useEffect, useRef } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import type { ChatInfo, ModelInfo } from '@adecore/agent-contracts';
4
+ import { useChatScope } from '../../../scope';
5
+ import { choiceWord } from '../../../agents/option-words';
6
+ import { registerEffortStepper } from '../../composer-effort';
7
+ import { EFFORT_OPTION, stepEffort, type EffortStep } from '../../logic/effort';
8
+ import type { ChatControls } from './use-chat-controls';
9
+ import type { ComposerNotice } from './use-notice';
10
+
11
+ interface EffortStepperInput {
12
+ chatId: string;
13
+ info: ChatInfo;
14
+ model: ModelInfo | undefined;
15
+ writable: boolean;
16
+ controls: ChatControls;
17
+ setNotice: ComposerNotice['setNotice'];
18
+ }
19
+
20
+ /* Lets a command of the host step this chat's reasoning effort, as picking it in the pill's menu does, and says where it landed. */
21
+ export function useEffortStepper({ chatId, info, model, writable, controls, setNotice }: EffortStepperInput): void {
22
+ const { t } = useTranslation('agent-chat');
23
+ const scope = useChatScope();
24
+
25
+ const step = (direction: EffortStep): void => {
26
+ // The pill is disabled then too, and a second configuration would be dropped anyway.
27
+ if (!writable || controls.configurationPending) {
28
+ return;
29
+ }
30
+ const name = model?.name ?? info.selection.model;
31
+ const result = stepEffort(model, info.selection, direction);
32
+ if (result.kind === 'none') {
33
+ setNotice(t('composer.notice.noEffort', { model: name }), 'info');
34
+ return;
35
+ }
36
+ if (result.kind === 'edge') {
37
+ setNotice(
38
+ t(direction === 'up' ? 'composer.notice.effortHighest' : 'composer.notice.effortLowest', {
39
+ effort: choiceWord(EFFORT_OPTION, result.choice),
40
+ model: name
41
+ }),
42
+ 'info'
43
+ );
44
+ return;
45
+ }
46
+ controls.configure({ selection: result.selection });
47
+ setNotice(t('composer.notice.effort', { effort: choiceWord(EFFORT_OPTION, result.choice) }), 'info');
48
+ };
49
+
50
+ // A command means the chat as it stands when it is given, so the registration reads the latest step.
51
+ const stepRef = useRef(step);
52
+ useEffect(() => {
53
+ stepRef.current = step;
54
+ });
55
+ useEffect(() => registerEffortStepper(scope.keyOf(chatId), (direction) => stepRef.current(direction)), [scope, chatId]);
56
+ }
@@ -1,4 +1,4 @@
1
- import { useCallback, useEffect, useState } from 'react';
1
+ import { useEffect, useState } from 'react';
2
2
 
3
3
  const NOTICE_MS = 4000;
4
4
 
@@ -24,7 +24,7 @@ export function useNotice(): ComposerNotice {
24
24
  return () => clearTimeout(timer);
25
25
  }, [shown]);
26
26
 
27
- const setNotice = useCallback((notice: string | null, tone: NoticeTone = 'error'): void => setShown(notice ? { text: notice, tone } : null), []);
27
+ const setNotice = (notice: string | null, tone: NoticeTone = 'error'): void => setShown(notice ? { text: notice, tone } : null);
28
28
 
29
29
  return {
30
30
  notice: shown?.text ?? null,