@adecore/agents-react 0.31.12 → 0.32.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 (402) hide show
  1. package/dist/agents/AccountDot.js +57 -6
  2. package/dist/agents/AgentIcon.js +88 -12
  3. package/dist/agents/ModelOptionControl.js +153 -18
  4. package/dist/agents/ProviderLogo.js +49 -4
  5. package/dist/agents/account-limits.js +17 -14
  6. package/dist/agents/accounts.js +99 -67
  7. package/dist/agents/agent-marks.js +5 -3
  8. package/dist/agents/model-name.js +42 -31
  9. package/dist/agents/model-options.js +8 -8
  10. package/dist/agents/model-requirement.js +17 -14
  11. package/dist/agents/option-words.d.ts +7 -0
  12. package/dist/agents/option-words.js +35 -0
  13. package/dist/agents/provider-paths.js +6 -6
  14. package/dist/agents/status-look.js +32 -12
  15. package/dist/chat/account-choice.d.ts +13 -0
  16. package/dist/chat/account-choice.js +90 -20
  17. package/dist/chat/actions.js +126 -59
  18. package/dist/chat/activity.js +5 -5
  19. package/dist/chat/attachments.js +74 -53
  20. package/dist/chat/bookmarks.js +58 -48
  21. package/dist/chat/chat-client.js +344 -263
  22. package/dist/chat/chat-references.js +3 -3
  23. package/dist/chat/composer-effort.d.ts +5 -0
  24. package/dist/chat/composer-effort.js +26 -0
  25. package/dist/chat/composer-send.js +13 -13
  26. package/dist/chat/drafts.js +115 -79
  27. package/dist/chat/forks.js +81 -24
  28. package/dist/chat/guards.js +15 -43
  29. package/dist/chat/index.d.ts +3 -1
  30. package/dist/chat/index.js +3 -2
  31. package/dist/chat/logic/ansi.js +170 -167
  32. package/dist/chat/logic/answers.js +2 -2
  33. package/dist/chat/logic/bookmarks.js +19 -19
  34. package/dist/chat/logic/command-looks.js +138 -96
  35. package/dist/chat/logic/context-usage.js +13 -9
  36. package/dist/chat/logic/continue-after-stop.js +15 -19
  37. package/dist/chat/logic/effort.d.ts +14 -0
  38. package/dist/chat/logic/effort.js +34 -0
  39. package/dist/chat/logic/embedded-code.js +387 -326
  40. package/dist/chat/logic/fork.js +134 -110
  41. package/dist/chat/logic/generated-image.js +54 -23
  42. package/dist/chat/logic/goal.js +28 -20
  43. package/dist/chat/logic/handback.js +6 -6
  44. package/dist/chat/logic/json.js +2 -2
  45. package/dist/chat/logic/limit.js +46 -30
  46. package/dist/chat/logic/picker-entries.d.ts +5 -3
  47. package/dist/chat/logic/picker-entries.js +83 -43
  48. package/dist/chat/logic/resume-compaction.d.ts +14 -1
  49. package/dist/chat/logic/resume-compaction.js +93 -24
  50. package/dist/chat/logic/scrubber.js +188 -136
  51. package/dist/chat/logic/thought.js +13 -16
  52. package/dist/chat/logic/thread-cards.js +60 -48
  53. package/dist/chat/logic/timeline-copy.js +30 -38
  54. package/dist/chat/logic/timeline-target.js +92 -63
  55. package/dist/chat/logic/timeline.js +499 -355
  56. package/dist/chat/logic/tool-catalog.js +54 -34
  57. package/dist/chat/logic/tools.js +106 -82
  58. package/dist/chat/logic/visual-height.js +18 -18
  59. package/dist/chat/logic/visual-viewport.js +6 -3
  60. package/dist/chat/logic/welcome.js +9 -9
  61. package/dist/chat/logic/workflow.js +45 -34
  62. package/dist/chat/mentions.js +149 -109
  63. package/dist/chat/persisted-json.js +51 -54
  64. package/dist/chat/preferences.d.ts +4 -0
  65. package/dist/chat/preferences.js +162 -84
  66. package/dist/chat/prompt-hold.js +9 -9
  67. package/dist/chat/quote.js +21 -30
  68. package/dist/chat/reading-width.js +12 -10
  69. package/dist/chat/recent-messages.js +34 -21
  70. package/dist/chat/resume-compaction-dismissals.js +25 -5
  71. package/dist/chat/runtime-modes.js +3 -3
  72. package/dist/chat/send-key.js +11 -7
  73. package/dist/chat/stash.js +89 -69
  74. package/dist/chat/subagent-conversation.js +181 -139
  75. package/dist/chat/subagent-list.js +65 -67
  76. package/dist/chat/subagent-support.js +12 -7
  77. package/dist/chat/subagent-view.js +132 -44
  78. package/dist/chat/thumbnails.js +88 -60
  79. package/dist/chat/timeline-flash.js +24 -12
  80. package/dist/chat/timeline-scroll.js +116 -86
  81. package/dist/chat/ui/AccountPill.js +126 -15
  82. package/dist/chat/ui/AnsiOutput.js +62 -6
  83. package/dist/chat/ui/BookmarkMarker.js +231 -41
  84. package/dist/chat/ui/BookmarkSubmenu.js +165 -18
  85. package/dist/chat/ui/ChatActivity.js +649 -70
  86. package/dist/chat/ui/ChatReferenceChip.js +104 -9
  87. package/dist/chat/ui/CodeBlock.js +132 -58
  88. package/dist/chat/ui/Composer.js +866 -132
  89. package/dist/chat/ui/ComposerInput.js +309 -120
  90. package/dist/chat/ui/DiffPool.js +75 -14
  91. package/dist/chat/ui/EditDiff.js +160 -21
  92. package/dist/chat/ui/FadingWords.js +32 -3
  93. package/dist/chat/ui/GoalDock.js +235 -42
  94. package/dist/chat/ui/ImageView.js +617 -61
  95. package/dist/chat/ui/LimitState.js +446 -72
  96. package/dist/chat/ui/Markdown.js +991 -173
  97. package/dist/chat/ui/MermaidBlock.js +238 -68
  98. package/dist/chat/ui/MessageActions.js +251 -39
  99. package/dist/chat/ui/ModelMarks.js +55 -5
  100. package/dist/chat/ui/Pickers.js +664 -113
  101. package/dist/chat/ui/PromptComposer.js +281 -51
  102. package/dist/chat/ui/QuoteButton.js +151 -57
  103. package/dist/chat/ui/ResumeCompactionDock.d.ts +6 -2
  104. package/dist/chat/ui/ResumeCompactionDock.js +141 -6
  105. package/dist/chat/ui/RowTime.js +59 -5
  106. package/dist/chat/ui/RunSettings.js +1744 -154
  107. package/dist/chat/ui/Scrubber.js +517 -88
  108. package/dist/chat/ui/ShellCommand.js +183 -107
  109. package/dist/chat/ui/SubagentControls.js +212 -24
  110. package/dist/chat/ui/SubagentInfo.js +170 -13
  111. package/dist/chat/ui/SubagentStopButton.js +115 -24
  112. package/dist/chat/ui/SubagentTimeline.js +357 -91
  113. package/dist/chat/ui/Timeline.js +988 -476
  114. package/dist/chat/ui/TimelineMenu.js +552 -37
  115. package/dist/chat/ui/UnifiedDiff.js +234 -27
  116. package/dist/chat/ui/UploadThumb.js +37 -7
  117. package/dist/chat/ui/VisualDialogs.js +251 -20
  118. package/dist/chat/ui/VisualFrame.js +321 -95
  119. package/dist/chat/ui/Welcome.js +72 -7
  120. package/dist/chat/ui/chips.js +1 -1
  121. package/dist/chat/ui/code-lines.js +36 -36
  122. package/dist/chat/ui/code-streaming.js +1 -1
  123. package/dist/chat/ui/code-theme.js +7 -7
  124. package/dist/chat/ui/composer/ComposerControls.js +329 -21
  125. package/dist/chat/ui/composer/ComposerMenu.js +358 -30
  126. package/dist/chat/ui/composer/DraftParts.js +185 -11
  127. package/dist/chat/ui/composer/QueuedMessages.js +138 -13
  128. package/dist/chat/ui/composer/chips.js +29 -19
  129. package/dist/chat/ui/composer/editor.js +79 -63
  130. package/dist/chat/ui/composer/insert.js +26 -12
  131. package/dist/chat/ui/composer/key-handler.js +180 -127
  132. package/dist/chat/ui/composer/keys.js +90 -73
  133. package/dist/chat/ui/composer/menu-aria.js +18 -9
  134. package/dist/chat/ui/composer/placeholder.js +78 -31
  135. package/dist/chat/ui/composer/surface.js +13 -37
  136. package/dist/chat/ui/composer/use-chat-controls.js +230 -90
  137. package/dist/chat/ui/composer/use-compaction-offer.js +50 -18
  138. package/dist/chat/ui/composer/use-composer-draft.js +65 -34
  139. package/dist/chat/ui/composer/use-composer-menus.js +478 -193
  140. package/dist/chat/ui/composer/use-composer-submit.js +251 -109
  141. package/dist/chat/ui/composer/use-continuable-turn.js +33 -6
  142. package/dist/chat/ui/composer/use-draft-files.js +245 -93
  143. package/dist/chat/ui/composer/use-effort-stepper.d.ts +13 -0
  144. package/dist/chat/ui/composer/use-effort-stepper.js +88 -0
  145. package/dist/chat/ui/composer/use-notice.js +61 -16
  146. package/dist/chat/ui/composer/use-pending-requests.js +38 -17
  147. package/dist/chat/ui/composer/use-prompt-history.js +117 -45
  148. package/dist/chat/ui/composer/use-prompt-hold.js +119 -35
  149. package/dist/chat/ui/composer/use-take-back.js +94 -72
  150. package/dist/chat/ui/diff/ApprovalReview.js +101 -23
  151. package/dist/chat/ui/diff/DiffFallbackBoundary.js +21 -13
  152. package/dist/chat/ui/diff/DiffOmitted.js +56 -6
  153. package/dist/chat/ui/diff/DiffStack.js +92 -8
  154. package/dist/chat/ui/diff/DiffUnavailable.js +56 -6
  155. package/dist/chat/ui/diff/EditorEditDiff.js +263 -51
  156. package/dist/chat/ui/diff/EditorPatchDiff.js +39 -14
  157. package/dist/chat/ui/diff/LegacyDiffPool.js +27 -10
  158. package/dist/chat/ui/diff/LegacyEditDiff.js +97 -20
  159. package/dist/chat/ui/diff/LegacyUnifiedDiff.js +172 -42
  160. package/dist/chat/ui/diff/RawDiff.js +65 -5
  161. package/dist/chat/ui/diff/ReviewDiff.js +77 -5
  162. package/dist/chat/ui/diff/diff-props.js +1 -1
  163. package/dist/chat/ui/diff/diff-source-budget.js +46 -35
  164. package/dist/chat/ui/diff/editor-edit-model.js +18 -14
  165. package/dist/chat/ui/diff/review-plan.js +63 -40
  166. package/dist/chat/ui/diff/review-settle.js +24 -9
  167. package/dist/chat/ui/diff/syntax-runtime.js +236 -201
  168. package/dist/chat/ui/diff/use-review-stack.js +146 -25
  169. package/dist/chat/ui/diff-syntax.js +70 -58
  170. package/dist/chat/ui/diff-theme.js +31 -12
  171. package/dist/chat/ui/file-links.js +39 -10
  172. package/dist/chat/ui/find-reveal.js +29 -8
  173. package/dist/chat/ui/full-diff.js +17 -12
  174. package/dist/chat/ui/highlighter.js +143 -113
  175. package/dist/chat/ui/icons.js +16 -7
  176. package/dist/chat/ui/index.js +1 -1
  177. package/dist/chat/ui/intelligent-ui/UiBlockFrame.js +556 -82
  178. package/dist/chat/ui/intelligent-ui/UiFallbackPart.js +73 -5
  179. package/dist/chat/ui/intelligent-ui/UiReply.js +666 -262
  180. package/dist/chat/ui/intelligent-ui/block-local.js +59 -31
  181. package/dist/chat/ui/intelligent-ui/chart-data.js +77 -67
  182. package/dist/chat/ui/intelligent-ui/node-text.js +77 -91
  183. package/dist/chat/ui/intelligent-ui/registry.js +46 -46
  184. package/dist/chat/ui/intelligent-ui/render-context.js +1 -1
  185. package/dist/chat/ui/intelligent-ui/renderers/base-land.js +1 -1
  186. package/dist/chat/ui/intelligent-ui/renderers/binding.js +3 -3
  187. package/dist/chat/ui/intelligent-ui/renderers/chart.js +1223 -123
  188. package/dist/chat/ui/intelligent-ui/renderers/choices.js +312 -34
  189. package/dist/chat/ui/intelligent-ui/renderers/content.js +1241 -86
  190. package/dist/chat/ui/intelligent-ui/renderers/data.js +619 -54
  191. package/dist/chat/ui/intelligent-ui/renderers/inputs.js +868 -67
  192. package/dist/chat/ui/intelligent-ui/renderers/links.js +366 -62
  193. package/dist/chat/ui/intelligent-ui/renderers/map-slots.js +118 -60
  194. package/dist/chat/ui/intelligent-ui/renderers/map-style.js +85 -78
  195. package/dist/chat/ui/intelligent-ui/renderers/map-view.js +282 -217
  196. package/dist/chat/ui/intelligent-ui/renderers/map.js +621 -69
  197. package/dist/chat/ui/intelligent-ui/renderers/place-point.js +15 -8
  198. package/dist/chat/ui/intelligent-ui/renderers/structure.js +262 -20
  199. package/dist/chat/ui/intelligent-ui/renderers/text.js +674 -49
  200. package/dist/chat/ui/intelligent-ui/reply-parts.js +24 -15
  201. package/dist/chat/ui/intelligent-ui/table-data.js +97 -57
  202. package/dist/chat/ui/intelligent-ui/tones.js +31 -31
  203. package/dist/chat/ui/intelligent-ui/use-in-view.js +30 -12
  204. package/dist/chat/ui/intelligent-ui/use-later.js +28 -13
  205. package/dist/chat/ui/intelligent-ui/use-truncated.js +45 -21
  206. package/dist/chat/ui/intelligent-ui/use-ui-links.js +296 -109
  207. package/dist/chat/ui/intelligent-ui/use-ui-queries.js +256 -246
  208. package/dist/chat/ui/markdown-blocks.js +296 -262
  209. package/dist/chat/ui/mermaid.js +60 -72
  210. package/dist/chat/ui/quote-selection.js +32 -29
  211. package/dist/chat/ui/rehype-chips.js +35 -23
  212. package/dist/chat/ui/rehype-fade.js +28 -17
  213. package/dist/chat/ui/remark-html-as-text.js +22 -15
  214. package/dist/chat/ui/reply-context.js +1 -1
  215. package/dist/chat/ui/reveal.js +161 -72
  216. package/dist/chat/ui/rows/ForksRow.js +226 -24
  217. package/dist/chat/ui/rows/GeneratedImageRow.js +541 -87
  218. package/dist/chat/ui/rows/MessageRows.js +1706 -159
  219. package/dist/chat/ui/rows/RowContainer.js +57 -7
  220. package/dist/chat/ui/rows/Rows.js +463 -54
  221. package/dist/chat/ui/rows/SubagentRow.js +577 -60
  222. package/dist/chat/ui/rows/TaskRow.js +359 -40
  223. package/dist/chat/ui/rows/VisualRow.js +233 -11
  224. package/dist/chat/ui/rows/WorkRows.js +1945 -164
  225. package/dist/chat/ui/rows/WorkflowRow.js +293 -34
  226. package/dist/chat/ui/rows/row-rhythm.js +27 -17
  227. package/dist/chat/ui/theme-watch.js +26 -23
  228. package/dist/chat/ui/timestamp-format.js +4 -1
  229. package/dist/chat/ui/use-diff-source-tokens.js +70 -34
  230. package/dist/chat/ui/useToggleSet.js +51 -19
  231. package/dist/chat/ui/visual-theme.js +153 -128
  232. package/dist/chat/visual-bridge.js +123 -123
  233. package/dist/chat/visual-viewport.js +94 -83
  234. package/dist/chat/visuals.js +13 -5
  235. package/dist/host-hooks.d.ts +29 -0
  236. package/dist/host-hooks.js +78 -0
  237. package/dist/host.js +120 -48
  238. package/dist/index.js +1 -1
  239. package/dist/lazy.js +38 -32
  240. package/dist/locales/en/agent-chat.d.ts +34 -0
  241. package/dist/locales/en/agent-chat.js +747 -713
  242. package/dist/locales/en/agent-prompts.js +42 -42
  243. package/dist/locales/en/agent-providers.d.ts +1 -0
  244. package/dist/locales/en/agent-providers.js +166 -165
  245. package/dist/locales/en/agent-usage.d.ts +6 -0
  246. package/dist/locales/en/agent-usage.js +129 -123
  247. package/dist/locales/nl/agent-chat.d.ts +34 -0
  248. package/dist/locales/nl/agent-chat.js +747 -713
  249. package/dist/locales/nl/agent-prompts.js +42 -42
  250. package/dist/locales/nl/agent-providers.d.ts +1 -0
  251. package/dist/locales/nl/agent-providers.js +166 -165
  252. package/dist/locales/nl/agent-usage.d.ts +6 -0
  253. package/dist/locales/nl/agent-usage.js +129 -123
  254. package/dist/locales.js +14 -14
  255. package/dist/mounted-registry.js +31 -30
  256. package/dist/port-transport.js +122 -111
  257. package/dist/prompts/index.js +1 -1
  258. package/dist/prompts/logic/focus.js +15 -14
  259. package/dist/prompts/logic/keys.js +93 -59
  260. package/dist/prompts/logic/prompts.js +27 -14
  261. package/dist/prompts/logic/subjects.js +60 -46
  262. package/dist/prompts/logic/useFocusAfterAnswer.js +60 -20
  263. package/dist/prompts/logic/usePromptSession.js +88 -77
  264. package/dist/prompts/ui/ApprovalBody.js +246 -27
  265. package/dist/prompts/ui/PromptActions.js +215 -9
  266. package/dist/prompts/ui/PromptCard.js +215 -36
  267. package/dist/prompts/ui/PromptView.js +372 -34
  268. package/dist/prompts/ui/QuestionBody.js +269 -73
  269. package/dist/providers/AccountColors.js +36 -5
  270. package/dist/providers/AccountDetail.js +683 -50
  271. package/dist/providers/AccountVariables.js +139 -32
  272. package/dist/providers/AddAccountForm.js +125 -38
  273. package/dist/providers/CliDetail.js +343 -36
  274. package/dist/providers/InstallPage.js +644 -59
  275. package/dist/providers/ProviderList.d.ts +2 -1
  276. package/dist/providers/ProviderList.js +215 -23
  277. package/dist/providers/ProvidersPane.d.ts +4 -2
  278. package/dist/providers/ProvidersPane.js +244 -64
  279. package/dist/providers/account-actions.js +54 -30
  280. package/dist/providers/index.js +1 -1
  281. package/dist/providers/install-steps.js +12 -9
  282. package/dist/providers/pane-target.d.ts +3 -0
  283. package/dist/providers/pane-target.js +34 -13
  284. package/dist/providers/parts.js +62 -12
  285. package/dist/providers/provider-abilities.js +5 -5
  286. package/dist/providers/provider-groups.js +75 -40
  287. package/dist/providers/variables.js +73 -39
  288. package/dist/scope.js +6 -6
  289. package/dist/settings/sections.js +18 -18
  290. package/dist/state/chats.js +404 -192
  291. package/dist/state/index.js +1 -1
  292. package/dist/state/provider-accounts.js +70 -47
  293. package/dist/state/providers.js +45 -16
  294. package/dist/state/usage.js +154 -77
  295. package/dist/storage.js +69 -68
  296. package/dist/testing/index.js +1 -1
  297. package/dist/testing/prompts.js +105 -67
  298. package/dist/transport.js +10 -10
  299. package/dist/ui/PlainTextarea.js +27 -3
  300. package/dist/usage/LimitsList.js +646 -40
  301. package/dist/usage/UsageBreakdown.js +879 -38
  302. package/dist/usage/UsageChart.js +311 -32
  303. package/dist/usage/UsageDialog.js +46 -4
  304. package/dist/usage/UsageLimits.js +467 -34
  305. package/dist/usage/UsageLimitsCard.js +95 -11
  306. package/dist/usage/UsagePage.js +817 -96
  307. package/dist/usage/UsagePane.js +164 -10
  308. package/dist/usage/UsageSummary.js +151 -6
  309. package/dist/usage/UsageTiles.js +164 -14
  310. package/dist/usage/format.js +32 -32
  311. package/dist/usage/index.d.ts +1 -1
  312. package/dist/usage/index.js +2 -1
  313. package/dist/usage/limit-groups.d.ts +2 -0
  314. package/dist/usage/limit-groups.js +77 -59
  315. package/dist/usage/limits.js +78 -43
  316. package/dist/usage/money.js +20 -4
  317. package/dist/usage/summary.js +85 -64
  318. package/package.json +8 -8
  319. package/src/agents/ModelOptionControl.tsx +3 -1
  320. package/src/agents/option-words.ts +27 -0
  321. package/src/chat/account-choice.ts +41 -1
  322. package/src/chat/composer-effort.ts +32 -0
  323. package/src/chat/forks.ts +4 -5
  324. package/src/chat/index.ts +3 -1
  325. package/src/chat/logic/effort.ts +32 -0
  326. package/src/chat/logic/picker-entries.ts +45 -10
  327. package/src/chat/logic/resume-compaction.ts +95 -17
  328. package/src/chat/logic/timeline.ts +11 -3
  329. package/src/chat/preferences.ts +35 -0
  330. package/src/chat/subagent-view.ts +2 -3
  331. package/src/chat/ui/AnsiOutput.tsx +1 -2
  332. package/src/chat/ui/BookmarkSubmenu.tsx +3 -3
  333. package/src/chat/ui/ChatActivity.tsx +14 -22
  334. package/src/chat/ui/ChatReferenceChip.tsx +2 -3
  335. package/src/chat/ui/Composer.tsx +15 -7
  336. package/src/chat/ui/EditDiff.tsx +2 -3
  337. package/src/chat/ui/LimitState.tsx +2 -2
  338. package/src/chat/ui/Markdown.tsx +10 -8
  339. package/src/chat/ui/MermaidBlock.tsx +2 -3
  340. package/src/chat/ui/Pickers.tsx +49 -29
  341. package/src/chat/ui/PromptComposer.tsx +7 -9
  342. package/src/chat/ui/ResumeCompactionDock.tsx +16 -4
  343. package/src/chat/ui/RunSettings.tsx +67 -28
  344. package/src/chat/ui/Scrubber.tsx +3 -3
  345. package/src/chat/ui/ShellCommand.tsx +2 -0
  346. package/src/chat/ui/SubagentInfo.tsx +2 -3
  347. package/src/chat/ui/SubagentTimeline.tsx +12 -16
  348. package/src/chat/ui/Timeline.tsx +6 -6
  349. package/src/chat/ui/UnifiedDiff.tsx +3 -4
  350. package/src/chat/ui/VisualFrame.tsx +5 -8
  351. package/src/chat/ui/code-theme.ts +4 -4
  352. package/src/chat/ui/composer/placeholder.ts +2 -2
  353. package/src/chat/ui/composer/use-compaction-offer.ts +36 -16
  354. package/src/chat/ui/composer/use-composer-menus.ts +4 -9
  355. package/src/chat/ui/composer/use-composer-submit.ts +2 -1
  356. package/src/chat/ui/composer/use-effort-stepper.ts +56 -0
  357. package/src/chat/ui/composer/use-notice.ts +2 -2
  358. package/src/chat/ui/diff/EditorEditDiff.tsx +9 -13
  359. package/src/chat/ui/diff/LegacyEditDiff.tsx +12 -17
  360. package/src/chat/ui/diff/LegacyUnifiedDiff.tsx +13 -21
  361. package/src/chat/ui/diff/use-review-stack.ts +4 -4
  362. package/src/chat/ui/diff-theme.ts +3 -3
  363. package/src/chat/ui/file-links.ts +2 -2
  364. package/src/chat/ui/intelligent-ui/UiReply.tsx +2 -0
  365. package/src/chat/ui/intelligent-ui/renderers/chart.tsx +3 -3
  366. package/src/chat/ui/intelligent-ui/renderers/content.tsx +5 -6
  367. package/src/chat/ui/intelligent-ui/renderers/map-view.tsx +2 -1
  368. package/src/chat/ui/intelligent-ui/renderers/map.tsx +2 -1
  369. package/src/chat/ui/intelligent-ui/use-truncated.ts +16 -19
  370. package/src/chat/ui/intelligent-ui/use-ui-links.ts +3 -3
  371. package/src/chat/ui/rows/ForksRow.tsx +3 -5
  372. package/src/chat/ui/rows/GeneratedImageRow.tsx +2 -1
  373. package/src/chat/ui/rows/MessageRows.tsx +12 -21
  374. package/src/chat/ui/rows/SubagentRow.tsx +3 -3
  375. package/src/chat/ui/rows/TaskRow.tsx +1 -1
  376. package/src/chat/ui/rows/WorkRows.tsx +10 -5
  377. package/src/chat/ui/useToggleSet.ts +15 -18
  378. package/src/chat/ui/visual-theme.ts +2 -3
  379. package/src/host-hooks.ts +113 -0
  380. package/src/lazy.tsx +2 -0
  381. package/src/locales/en/agent-chat.ts +134 -108
  382. package/src/locales/en/agent-prompts.ts +7 -7
  383. package/src/locales/en/agent-providers.ts +35 -34
  384. package/src/locales/en/agent-usage.ts +26 -20
  385. package/src/locales/nl/agent-chat.ts +126 -100
  386. package/src/locales/nl/agent-prompts.ts +5 -5
  387. package/src/locales/nl/agent-providers.ts +33 -32
  388. package/src/locales/nl/agent-usage.ts +25 -20
  389. package/src/providers/CliDetail.tsx +2 -1
  390. package/src/providers/InstallPage.tsx +2 -4
  391. package/src/providers/ProviderList.tsx +12 -2
  392. package/src/providers/ProvidersPane.tsx +45 -15
  393. package/src/providers/pane-target.ts +20 -4
  394. package/src/state/chats.ts +2 -2
  395. package/src/usage/LimitsList.tsx +12 -13
  396. package/src/usage/UsageBreakdown.tsx +17 -6
  397. package/src/usage/UsageLimits.tsx +2 -1
  398. package/src/usage/UsagePage.tsx +3 -3
  399. package/src/usage/index.ts +1 -1
  400. package/src/usage/limit-groups.ts +12 -0
  401. package/src/usage/limits.ts +2 -2
  402. package/src/usage/money.ts +1 -2
@@ -1,4 +1,4 @@
1
- import { useEffect, useMemo, useRef, useState, useSyncExternalStore, type MouseEvent, type PointerEvent } from 'react';
1
+ import { useEffect, useRef, useState, useSyncExternalStore, type MouseEvent, type PointerEvent } from 'react';
2
2
  import { useTranslation } from 'react-i18next';
3
3
  import clsx from 'clsx';
4
4
  import { ChevronDown } from 'lucide-react';
@@ -35,6 +35,7 @@ import { useCompactionOffer } from './composer/use-compaction-offer';
35
35
  import { useComposerDraft } from './composer/use-composer-draft';
36
36
  import { useComposerMenus } from './composer/use-composer-menus';
37
37
  import { useComposerSubmit } from './composer/use-composer-submit';
38
+ import { useEffortStepper } from './composer/use-effort-stepper';
38
39
  import { LEAVE_DROPS_TO_THE_CARD, useDraftFiles } from './composer/use-draft-files';
39
40
  import { useContinuableTurn } from './composer/use-continuable-turn';
40
41
  import { useNotice } from './composer/use-notice';
@@ -43,6 +44,7 @@ import { usePromptHistory } from './composer/use-prompt-history';
43
44
  import { usePromptHold } from './composer/use-prompt-hold';
44
45
  import { surfacePartOf } from './composer/surface';
45
46
  import { useTakeBack } from './composer/use-take-back';
47
+ import { useHostExtraPrompts, useHostSendDelivery } from '../../host-hooks';
46
48
 
47
49
  interface ComposerProps {
48
50
  chatId: string;
@@ -78,7 +80,7 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
78
80
  const { t } = useTranslation('agent-chat');
79
81
  const host = useChatHost();
80
82
  const scope = useChatScope();
81
- const sendDelivery = host.useSendDelivery();
83
+ const sendDelivery = useHostSendDelivery();
82
84
  const sendKey = useChatPreferences((preferences) => preferences.sendKey);
83
85
  const provider = useProviders((row) => row.providers.find((entry) => entry.kind === info.provider));
84
86
  // Absent until the host answered `provider.list`, so only an explicit false hides anything.
@@ -96,11 +98,19 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
96
98
  const menus = useComposerMenus({ chatId, info, capabilities, providerName: provider?.name, draft: box, inputRef });
97
99
  const files = useDraftFiles({ capabilities, providerName: provider?.name, writable, draft: box, inputRef, setNotice: notice.setNotice });
98
100
  const controls = useChatControls({ chatId, info, notice, onRetarget });
101
+ useEffortStepper({
102
+ chatId,
103
+ info,
104
+ model: provider?.models.find((entry) => entry.slug === info.selection.model),
105
+ writable,
106
+ controls,
107
+ setNotice: notice.setNotice
108
+ });
99
109
  const guard = promptGuard(withQuote(draft.quote, draft.text));
100
110
  const continuable = useContinuableTurn(chatId, info);
101
111
  // The stopped turn something went out for, since the chat takes a moment to answer and Continue must not stand there meanwhile.
102
112
  const [sentFor, setSentFor] = useState<string | null>(null);
103
- const extraPrompts = host.prompts.useExtra(scope.id, chatId);
113
+ const extraPrompts = useHostExtraPrompts(scope.id, chatId);
104
114
  const { submit, continueTurn, restoreStashed, toggleStash } = useComposerSubmit({
105
115
  chatId,
106
116
  writable,
@@ -144,10 +154,7 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
144
154
  const pending = usePendingRequests(chatId);
145
155
  const compactionOffer = useCompactionOffer(chatId, info, capabilities?.compaction);
146
156
  const placeholder = useHintedPlaceholder({ chatId, disabled, readOnly, mentionable: capabilities?.mentions !== false, hasSkills: menus.hasSkills });
147
- const editorExtensions = useMemo(
148
- () => [chipDecorations({ mentions: draft.mentions, skills: draft.skills }), LEAVE_DROPS_TO_THE_CARD],
149
- [draft.mentions, draft.skills]
150
- );
157
+ const editorExtensions = [chipDecorations({ mentions: draft.mentions, skills: draft.skills }), LEAVE_DROPS_TO_THE_CARD];
151
158
  /* The timeline owns the scroller; this is the one bit of it the composer needs to know. */
152
159
  const atEnd = useSyncExternalStore(
153
160
  subscribeTimelineEnd,
@@ -232,6 +239,7 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
232
239
  {compactionOffer && (
233
240
  <ResumeCompactionDock
234
241
  tokens={compactionOffer.tokens}
242
+ reason={compactionOffer.reason}
235
243
  onCompact={controls.compact}
236
244
  onDismiss={() => dismissResumeCompaction(scope, chatId, compactionOffer.turnId)}
237
245
  />
@@ -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';
@@ -17,9 +16,9 @@ export default function EditDiff({ change, allowEditor = true, maxHeight }: { ch
17
16
  const host = useChatHost();
18
17
  const { t } = useTranslation('agent-chat');
19
18
  const loadEngine = host.code.editor;
20
- const sourceAllowed = useMemo(() => diffSourceBudget(change.before, change.after), [change.before, change.after]);
19
+ const sourceAllowed = diffSourceBudget(change.before, change.after);
21
20
  const legacyMode = !allowEditor || loadEngine === undefined;
22
- const legacyAllowed = useMemo(() => legacyMode && legacyDiffBudget(change.before, change.after), [legacyMode, change.before, change.after]);
21
+ const legacyAllowed = legacyMode && legacyDiffBudget(change.before, change.after);
23
22
  const omitted = <DiffOmitted path={change.path} />;
24
23
  const fallback = legacyAllowed ? (
25
24
  <DiffFallbackBoundary fallback={omitted} resetKeys={[change.path, change.before, change.after]}>
@@ -14,6 +14,7 @@ import { useChatHost } from '../../host';
14
14
  import { useChatScope, type ChatScope } from '../../scope';
15
15
  import { useChatActions } from '../actions';
16
16
  import { useUsageStore } from '../../state/usage';
17
+ import { useHostResumeAtReset } from '../../host-hooks';
17
18
 
18
19
  // A time a limit names reads as a clock today and with its day after that, so the words follow the minute.
19
20
  const MINUTE_MS = 60_000;
@@ -43,11 +44,10 @@ function readUnreadAccounts(scope: ChatScope): void {
43
44
  * switch off it stands off and says where that switch is.
44
45
  */
45
46
  function ResumeAtResetToggle({ chatId, info }: { chatId: string; info: ChatInfo }) {
46
- const host = useChatHost();
47
47
  const { t } = useTranslation('agent-chat');
48
48
  const { id } = useChatScope();
49
49
  const actions = useChatActions();
50
- const machineAllows = host.useResumeAtReset(id);
50
+ const machineAllows = useHostResumeAtReset(id);
51
51
  const set = (on: boolean): void => {
52
52
  void actions.configure(chatId, { resumeAtReset: on }).catch(() => undefined);
53
53
  };
@@ -1,4 +1,4 @@
1
- import { createContext, memo, useContext, useMemo, type ReactNode } from 'react';
1
+ import { createContext, memo, useContext, type ReactNode } from 'react';
2
2
  import clsx from 'clsx';
3
3
  import { Zap } from 'lucide-react';
4
4
  import ReactMarkdown, { type Components, type ExtraProps, type Options } from 'react-markdown';
@@ -85,7 +85,7 @@ function languageOf(className: string | undefined): string {
85
85
  return /language-([\w-]+)/.exec(className ?? '')?.[1] ?? 'text';
86
86
  }
87
87
 
88
- const INLINE_CODE = 'rounded-sm bg-surface-sunken px-1 py-px font-mono text-code';
88
+ const INLINE_CODE = 'rounded-sm bg-surface-sunken px-1 py-px font-mono text-code box-decoration-clone';
89
89
 
90
90
  /*
91
91
  * A path an agent wrote down. It keeps the shape of the code chip it would otherwise be, so a sentence
@@ -99,7 +99,7 @@ function FileLink({ target, className, children }: { target: FileRef; className?
99
99
  // A button is not selectable on its own, and a thread is copied as often as it is clicked.
100
100
  <button
101
101
  type="button"
102
- className={clsx('cursor-pointer select-text', className)}
102
+ className={clsx('cursor-pointer select-text text-left', className)}
103
103
  data-file-path={target.path}
104
104
  data-file-line={target.line}
105
105
  data-file-column={target.column}
@@ -121,8 +121,10 @@ function InlineCode({ text }: { text: string }) {
121
121
  return <code className={INLINE_CODE}>{text}</code>;
122
122
  }
123
123
  return (
124
- <FileLink target={target} className={`${INLINE_CODE} text-accent hover:underline`}>
125
- {text}
124
+ // The chip sits on a code inside the button, since a button is always a box of its own and its
125
+ // background would fill the whole box once a long path wraps.
126
+ <FileLink target={target} className="text-accent">
127
+ <code className={clsx(INLINE_CODE, 'hover:underline')}>{text}</code>
126
128
  </FileLink>
127
129
  );
128
130
  }
@@ -393,7 +395,7 @@ export const ReplyMarkdown = memo(function ReplyMarkdown({
393
395
  arriving?: boolean;
394
396
  reply?: ShellCodeBlockReply;
395
397
  }) {
396
- const blocks = useMemo(() => splitMarkdownBlocks(text), [text]);
398
+ const blocks = splitMarkdownBlocks(text);
397
399
  return (
398
400
  <div className="chat-markdown prose prose-sm max-w-none text-sm" data-arriving={arriving || undefined}>
399
401
  {blocks.map((block, index) =>
@@ -420,7 +422,7 @@ export const ReplyMarkdown = memo(function ReplyMarkdown({
420
422
  * it was, and the files and skills picked in the composer are chips again.
421
423
  */
422
424
  export const MessageMarkdown = memo(function MessageMarkdown({ text, mentions, skills }: { text: string; mentions?: string[]; skills?: string[] }) {
423
- const rehypePlugins = useMemo((): [typeof rehypeChips, ChipOptions][] => [[rehypeChips, { mentions, skills }]], [mentions, skills]);
425
+ const rehypePlugins: [typeof rehypeChips, ChipOptions][] = [[rehypeChips, { mentions, skills }]];
424
426
  return (
425
427
  <div className="chat-markdown prose prose-sm max-w-none text-sm">
426
428
  <ReactMarkdown remarkPlugins={MESSAGE_PLUGINS} rehypePlugins={rehypePlugins} components={messageComponents}>
@@ -449,7 +451,7 @@ export const Markdown = memo(function Markdown({
449
451
  rehypePlugins?: Options['rehypePlugins'];
450
452
  componentOverrides?: Components;
451
453
  }) {
452
- const renderComponents = useMemo(() => ({ ...(fileLinks ? components : plainComponents), ...componentOverrides }), [fileLinks, componentOverrides]);
454
+ const renderComponents = { ...(fileLinks ? components : plainComponents), ...componentOverrides };
453
455
  return (
454
456
  <div className="chat-markdown prose prose-sm max-w-none text-sm" data-muted={muted || undefined}>
455
457
  <ReactMarkdown remarkPlugins={breaks ? PLUGINS_WITH_BREAKS : PLUGINS} rehypePlugins={rehypePlugins} components={renderComponents}>
@@ -2,11 +2,11 @@ import { useContext, useEffect, useId, useRef, useState } from 'react';
2
2
  import { useTranslation } from 'react-i18next';
3
3
  import { Code, Workflow } from 'lucide-react';
4
4
  import { IconButton } from '@adecore/ui';
5
- import { useChatHost } from '../../host';
6
5
  import { CodeBlock } from './CodeBlock';
7
6
  import { CodeStreamingContext } from './code-streaming';
8
7
  import { MERMAID_TOKENS, mermaidConfig, sizedSvg, svgDataUrl, type MermaidColors, type SizedSvg } from './mermaid';
9
8
  import { watchTheme } from './theme-watch';
9
+ import { useHostCodeMode } from '../../host-hooks';
10
10
 
11
11
  type Drawing = { state: 'drawing' } | { state: 'drawn'; image: SizedSvg } | { state: 'failed' };
12
12
 
@@ -63,10 +63,9 @@ async function drawDiagram(id: string, source: string, look: DiagramLook, dark:
63
63
  * draw, it is the code block it would otherwise be.
64
64
  */
65
65
  export function MermaidBlock({ code }: { code: string }) {
66
- const host = useChatHost();
67
66
  const { t } = useTranslation('agent-chat');
68
67
  const streaming = useContext(CodeStreamingContext);
69
- const dark = host.code.useMode() === 'dark';
68
+ const dark = useHostCodeMode() === 'dark';
70
69
  const id = `mermaid-${useId().replace(/[^\w-]/g, '')}`;
71
70
  const frame = useRef<HTMLDivElement>(null);
72
71
  const [source, setSource] = useState(false);
@@ -1,6 +1,6 @@
1
- import { useContext, useEffect, useMemo, useRef, useState, type ReactNode, type RefObject } from 'react';
1
+ import { useContext, useEffect, useRef, useState, type ReactNode, type RefObject } from 'react';
2
2
  import { useTranslation } from 'react-i18next';
3
- import { Bookmark, ChevronDown, ChevronRight, Search, Trash2 } from 'lucide-react';
3
+ import { Bookmark, ChevronDown, ChevronRight, Search, Star, Trash2 } from 'lucide-react';
4
4
  import type { AgentKind, ModelSelection, ProviderInfo, UsageProvider } from '@adecore/agent-contracts';
5
5
  import { AgentIcon } from '../../agents/AgentIcon';
6
6
  import { ProviderLogo } from '../../agents/ProviderLogo';
@@ -11,8 +11,10 @@ import { ChatScopeContext } from '../../scope';
11
11
  import { useReportedModels } from '../../state/chats';
12
12
  import { ModelMarks } from './ModelMarks';
13
13
  import { pickerEntriesOf, type PickerEntry } from '../logic/picker-entries';
14
+ import { favoriteModelsOf, toggleFavoriteModel, useChatPreferences } from '../preferences';
14
15
  import { forgetStashed, STASH_SHORTCUT, useStash, type StashedPrompt } from '../stash';
15
16
  import { Icon, IconButton, Menu, Popover, Tooltip } from '@adecore/ui';
17
+ import { useHostMachineLabel } from '../../host-hooks';
16
18
 
17
19
  const triggerClass =
18
20
  'flex h-7 shrink-0 items-center gap-1 whitespace-nowrap rounded-md px-2 text-xs text-text-muted hover:bg-surface-hover hover:text-text data-[popup-open]:bg-surface-active data-[popup-open]:text-text';
@@ -74,12 +76,13 @@ export function ModelPicker({
74
76
  const grouped = providers.length > 1;
75
77
  const trimmed = query.trim().toLowerCase();
76
78
 
77
- const entries = useMemo(
78
- () => pickerEntriesOf(providers, trimmed, legacyOpen, expanded, { provider, model: selection.model }),
79
- [providers, trimmed, legacyOpen, expanded, provider, selection.model]
79
+ const favorites = useChatPreferences((preferences) => preferences.favoriteModels);
80
+ const starredHere = (row: PickerEntry | undefined): boolean => row?.kind === 'model' && favorites[row.provider.kind]?.includes(row.model.slug) === true;
81
+ const entries = pickerEntriesOf(providers, trimmed, legacyOpen, expanded, { provider, model: selection.model }, (kind) =>
82
+ favoriteModelsOf(favorites, kind)
80
83
  );
81
84
  const scope = useContext(ChatScopeContext);
82
- const machine = host.useMachineLabel(scope?.id ?? '');
85
+ const machine = useHostMachineLabel(scope?.id ?? '');
83
86
  const reported = useReportedModels(provider);
84
87
  const requirementOf = (entry: PickerEntry & { kind: 'model' }) => modelRequirementOf(entry.model, entry.provider, reported);
85
88
  // A CLI that is too old blocks all its models; one way to its settings under the group says it once.
@@ -226,17 +229,21 @@ export function ModelPicker({
226
229
  const first = grouped && (previous === undefined || previous.kind === 'legacy' || previous.provider.kind !== entry.provider.kind);
227
230
  // The makers read as groups only where the whole list is shown, so a short list is not cut by labels.
228
231
  const vendorOpen = trimmed !== '' || expanded.has(entry.provider.kind);
232
+ // The starred rows lead their CLI without a maker's label; the makers start after them.
229
233
  const vendorFirst =
230
234
  vendorOpen &&
231
235
  entry.model.vendor !== undefined &&
236
+ !starredHere(entry) &&
232
237
  (previous === undefined ||
233
238
  previous.kind !== 'model' ||
234
239
  previous.provider.kind !== entry.provider.kind ||
240
+ starredHere(previous) ||
235
241
  previous.model.vendor !== entry.model.vendor);
236
242
  const requirement = requirementOf(entry);
237
243
  const next = entries[i + 1];
238
244
  const lastOfGroup = next === undefined || next.kind !== 'model' || next.provider.kind !== entry.provider.kind;
239
245
  const chosen = entry.provider.kind === provider && entry.model.slug === selection.model;
246
+ const starred = starredHere(entry);
240
247
  return (
241
248
  <div key={`${entry.provider.kind}/${entry.model.slug}`}>
242
249
  {first && (
@@ -246,30 +253,43 @@ export function ModelPicker({
246
253
  </Menu.Label>
247
254
  )}
248
255
  {vendorFirst && <Menu.Label>{entry.model.vendor}</Menu.Label>}
249
- <button
250
- role="option"
251
- aria-selected={chosen}
252
- aria-disabled={requirement !== null || undefined}
253
- data-active={i === index}
254
- className={`cursor-row flex w-full flex-col items-stretch gap-0.5 rounded-md px-2.5 py-1.5 text-left text-sm ${requirement === null ? 'text-text-muted' : 'text-text-faint'}`}
255
- onMouseEnter={() => setIndex(i)}
256
- onClick={() => choose(entry)}
257
- >
258
- <span className="flex w-full items-center gap-2">
259
- <Menu.Check kind="radio" checked={chosen} />
260
- <span className="min-w-0 truncate">{entry.model.name}</span>
261
- <ModelMarks model={entry.model} />
262
- <span className="grow" />
263
- {entry.model.legacy && <span className="text-xs text-text-faint">{t('pickers.model.legacy')}</span>}
264
- </span>
265
- {requirement !== null && (
266
- <span className="pl-6 text-xs text-text-faint">
267
- {machine === null
268
- ? t('pickers.model.requiresHere', { cli: requirement.cli, version: requirement.minVersion })
269
- : t('pickers.model.requires', { cli: requirement.cli, version: requirement.minVersion, machine })}
256
+ {/* The star sits beside the option rather than in it, since a button holds no other button. */}
257
+ <div className="flex items-start gap-0.5">
258
+ <button
259
+ role="option"
260
+ aria-selected={chosen}
261
+ aria-disabled={requirement !== null || undefined}
262
+ data-active={i === index}
263
+ className={`cursor-row flex min-w-0 grow flex-col items-stretch gap-0.5 rounded-md px-2.5 py-1.5 text-left text-sm ${requirement === null ? 'text-text-muted' : 'text-text-faint'}`}
264
+ onMouseEnter={() => setIndex(i)}
265
+ onClick={() => choose(entry)}
266
+ >
267
+ <span className="flex w-full items-center gap-2">
268
+ <Menu.Check kind="radio" checked={chosen} />
269
+ {/* Wrapped rather than cut, so a long name a provider gives is read whole. */}
270
+ <span className="min-w-0 break-words">{entry.model.name}</span>
271
+ <ModelMarks model={entry.model} />
272
+ <span className="grow" />
273
+ {entry.model.legacy && <span className="text-xs text-text-faint">{t('pickers.model.legacy')}</span>}
270
274
  </span>
271
- )}
272
- </button>
275
+ {requirement !== null && (
276
+ <span className="pl-6 text-xs text-text-faint">
277
+ {machine === null
278
+ ? t('pickers.model.requiresHere', { cli: requirement.cli, version: requirement.minVersion })
279
+ : t('pickers.model.requires', { cli: requirement.cli, version: requirement.minVersion, machine })}
280
+ </span>
281
+ )}
282
+ </button>
283
+ <IconButton
284
+ icon={Star}
285
+ size="sm"
286
+ label={starred ? t('pickers.model.unstar') : t('pickers.model.star')}
287
+ aria-pressed={starred}
288
+ iconClassName={starred ? 'fill-current text-accent' : undefined}
289
+ className="mt-0.5 shrink-0"
290
+ onClick={() => toggleFavoriteModel(entry.provider.kind, entry.model.slug)}
291
+ />
292
+ </div>
273
293
  {lastOfGroup && blockedKinds.has(entry.provider.kind) && (
274
294
  <button
275
295
  type="button"
@@ -1,4 +1,4 @@
1
- import { useEffect, useMemo, useRef, type ReactNode } from 'react';
1
+ import { useEffect, useRef, type ReactNode } from 'react';
2
2
  import { useTranslation } from 'react-i18next';
3
3
  import { ArrowDown, Hand, MessageCircleQuestionMark } from 'lucide-react';
4
4
  import type { HoldRelease } from '../prompt-hold';
@@ -12,6 +12,7 @@ import { useChatHost } from '../../host';
12
12
  import { useChatScope } from '../../scope';
13
13
  import { useChatActions } from '../actions';
14
14
  import { Button, Icon } from '@adecore/ui';
15
+ import { useHostExtraPrompts } from '../../host-hooks';
15
16
 
16
17
  /* Keeps a prompt card behind the draft while the person types; `released` says what lifted the hold last. */
17
18
  export interface PromptHold {
@@ -57,14 +58,11 @@ export function PromptComposer({
57
58
  const actions = useChatActions();
58
59
  const elsewhere = onShowElsewhere !== undefined;
59
60
  // A card the app raises itself waits in the chat beside the CLI's own requests.
60
- const extra = host.prompts.useExtra(id, chatId);
61
- const subjects = useMemo(
62
- (): PromptSubject[] => [
63
- ...pending.map((item): PromptSubject => ({ kind: 'chat', nodeId: chatId, item })),
64
- ...extra.map((prompt): PromptSubject => ({ kind: 'host', nodeId: chatId, prompt }))
65
- ],
66
- [pending, extra, chatId]
67
- );
61
+ const extra = useHostExtraPrompts(id, chatId);
62
+ const subjects: PromptSubject[] = [
63
+ ...pending.map((item): PromptSubject => ({ kind: 'chat', nodeId: chatId, item })),
64
+ ...extra.map((prompt): PromptSubject => ({ kind: 'host', nodeId: chatId, prompt }))
65
+ ];
68
66
  const session = usePromptSession({ prompts: subjects, idOf: promptIdOf, pick: pickSubject, disabled });
69
67
  const ref = useRef<HTMLDivElement>(null);
70
68
  const { active, activeId: activeKey } = session;
@@ -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);