@adecore/agents-react 0.39.0 → 0.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (317) hide show
  1. package/dist/agents/AgentModelLabel.js +2 -1
  2. package/dist/agents/model-name.js +2 -1
  3. package/dist/agents/provider-paths.d.ts +2 -1
  4. package/dist/agents/provider-paths.js +7 -3
  5. package/dist/chat/account-choice.js +57 -28
  6. package/dist/chat/bookmarks.js +2 -1
  7. package/dist/chat/drafts.d.ts +0 -1
  8. package/dist/chat/drafts.js +9 -28
  9. package/dist/chat/logic/picker-entries.js +2 -8
  10. package/dist/chat/logic/resume-compaction.js +2 -5
  11. package/dist/chat/logic/timeline-copy.js +2 -8
  12. package/dist/chat/logic/timeline.js +9 -10
  13. package/dist/chat/logic/workflow.js +2 -4
  14. package/dist/chat/subagent-conversation.js +2 -1
  15. package/dist/chat/ui/GoalDock.js +2 -1
  16. package/dist/chat/ui/LimitState.js +4 -6
  17. package/dist/chat/ui/composer/use-continuable-turn.js +0 -1
  18. package/dist/chat/ui/composer/use-notice.js +2 -1
  19. package/dist/chat/ui/composer/use-prompt-history.js +1 -1
  20. package/dist/chat/ui/composer/use-prompt-menus.js +216 -253
  21. package/dist/chat/ui/composer/use-take-back.js +2 -2
  22. package/dist/chat/ui/diff/ApprovalReview.js +1 -1
  23. package/dist/chat/ui/highlighter.d.ts +0 -6
  24. package/dist/chat/ui/highlighter.js +0 -12
  25. package/dist/chat/ui/icons.d.ts +0 -1
  26. package/dist/chat/ui/icons.js +0 -3
  27. package/dist/chat/ui/rows/MessageRows.js +2 -5
  28. package/dist/chat/ui/rows/row-rhythm.js +2 -5
  29. package/dist/chat/ui/theme-watch.js +3 -7
  30. package/dist/prompts/logic/usePromptSession.js +2 -1
  31. package/dist/providers/AddAccountForm.js +2 -1
  32. package/dist/providers/CliDetail.js +104 -130
  33. package/dist/providers/account-actions.js +6 -9
  34. package/dist/state/chats.d.ts +1 -0
  35. package/dist/state/chats.js +6 -11
  36. package/dist/state/provider-accounts.d.ts +1 -1
  37. package/dist/state/provider-accounts.js +2 -8
  38. package/dist/transport.d.ts +2 -0
  39. package/dist/transport.js +14 -0
  40. package/dist/usage/ChartLogo.d.ts +5 -0
  41. package/dist/usage/ChartLogo.js +48 -0
  42. package/dist/usage/LimitsList.js +6 -16
  43. package/dist/usage/UsageBreakdown.js +17 -40
  44. package/dist/usage/UsageLimits.js +183 -211
  45. package/dist/usage/UsagePage.js +3 -17
  46. package/dist/usage/UsageSummary.js +4 -9
  47. package/dist/usage/limits.js +16 -31
  48. package/dist/usage/summary.d.ts +1 -1
  49. package/dist/usage/summary.js +3 -5
  50. package/package.json +26 -26
  51. package/src/agents/AccountDot.tsx +1 -0
  52. package/src/agents/AgentIcon.tsx +2 -0
  53. package/src/agents/AgentModelLabel.tsx +4 -6
  54. package/src/agents/ModelOptionControl.tsx +8 -0
  55. package/src/agents/account-limits.ts +3 -0
  56. package/src/agents/accounts.ts +12 -0
  57. package/src/agents/model-name.ts +13 -1
  58. package/src/agents/model-options.ts +2 -0
  59. package/src/agents/model-requirement.ts +4 -0
  60. package/src/agents/option-words.ts +2 -0
  61. package/src/agents/provider-paths.ts +8 -3
  62. package/src/agents/status-look.ts +1 -0
  63. package/src/chat/account-choice.ts +25 -19
  64. package/src/chat/actions.ts +5 -0
  65. package/src/chat/attachments.ts +8 -0
  66. package/src/chat/bookmarks.ts +13 -1
  67. package/src/chat/chat-client.ts +63 -0
  68. package/src/chat/chat-references.ts +3 -0
  69. package/src/chat/composer-effort.ts +5 -0
  70. package/src/chat/composer-send.ts +4 -0
  71. package/src/chat/drafts.ts +21 -17
  72. package/src/chat/forks.ts +4 -0
  73. package/src/chat/guards.ts +2 -0
  74. package/src/chat/index.ts +7 -0
  75. package/src/chat/logic/ansi.ts +47 -0
  76. package/src/chat/logic/bookmarks.ts +7 -0
  77. package/src/chat/logic/command-looks.ts +29 -0
  78. package/src/chat/logic/context-usage.ts +3 -0
  79. package/src/chat/logic/continue-after-stop.ts +5 -0
  80. package/src/chat/logic/effort.ts +4 -0
  81. package/src/chat/logic/embedded-code.ts +63 -0
  82. package/src/chat/logic/fork.ts +22 -0
  83. package/src/chat/logic/generated-image.ts +7 -0
  84. package/src/chat/logic/goal.ts +7 -0
  85. package/src/chat/logic/handback.ts +2 -0
  86. package/src/chat/logic/limit.ts +8 -0
  87. package/src/chat/logic/picker-entries.ts +16 -4
  88. package/src/chat/logic/resume-compaction.ts +22 -5
  89. package/src/chat/logic/scrubber.ts +38 -5
  90. package/src/chat/logic/thought.ts +5 -0
  91. package/src/chat/logic/thread-cards.ts +6 -0
  92. package/src/chat/logic/timeline-copy.ts +4 -9
  93. package/src/chat/logic/timeline-target.ts +15 -0
  94. package/src/chat/logic/timeline.ts +113 -10
  95. package/src/chat/logic/tools.ts +30 -0
  96. package/src/chat/logic/visual-height.ts +5 -0
  97. package/src/chat/logic/visual-viewport.ts +1 -0
  98. package/src/chat/logic/welcome.ts +3 -0
  99. package/src/chat/logic/workflow.ts +26 -7
  100. package/src/chat/mentions.ts +56 -0
  101. package/src/chat/pending-prompts.ts +16 -0
  102. package/src/chat/persisted-json.ts +15 -0
  103. package/src/chat/preferences.ts +16 -0
  104. package/src/chat/prompt-hold.ts +2 -0
  105. package/src/chat/quote.ts +7 -0
  106. package/src/chat/reading-width.ts +1 -0
  107. package/src/chat/recent-messages.ts +5 -0
  108. package/src/chat/resume-compaction-dismissals.ts +1 -0
  109. package/src/chat/stash.ts +12 -0
  110. package/src/chat/subagent-conversation.ts +24 -4
  111. package/src/chat/subagent-list.ts +14 -0
  112. package/src/chat/subagent-support.ts +1 -0
  113. package/src/chat/subagent-view.ts +9 -0
  114. package/src/chat/thumbnails.ts +14 -0
  115. package/src/chat/timeline-flash.ts +2 -0
  116. package/src/chat/timeline-scroll.ts +23 -0
  117. package/src/chat/ui/AccountPill.tsx +4 -0
  118. package/src/chat/ui/AnsiOutput.tsx +1 -0
  119. package/src/chat/ui/BookmarkMarker.tsx +5 -0
  120. package/src/chat/ui/BookmarkSubmenu.tsx +1 -0
  121. package/src/chat/ui/ChatActivity.tsx +13 -0
  122. package/src/chat/ui/CodeBlock.tsx +8 -0
  123. package/src/chat/ui/Composer.tsx +15 -0
  124. package/src/chat/ui/ComposerInput.tsx +22 -0
  125. package/src/chat/ui/DiffPool.tsx +3 -0
  126. package/src/chat/ui/EditDiff.tsx +5 -0
  127. package/src/chat/ui/GoalDock.tsx +13 -1
  128. package/src/chat/ui/ImageView.tsx +11 -0
  129. package/src/chat/ui/LimitState.tsx +23 -6
  130. package/src/chat/ui/Markdown.tsx +41 -0
  131. package/src/chat/ui/MermaidBlock.tsx +16 -0
  132. package/src/chat/ui/MessageActions.tsx +8 -0
  133. package/src/chat/ui/ModelMarks.tsx +1 -0
  134. package/src/chat/ui/Pickers.tsx +26 -0
  135. package/src/chat/ui/PromptComposer.tsx +2 -0
  136. package/src/chat/ui/PromptEditor.tsx +6 -0
  137. package/src/chat/ui/QuoteButton.tsx +12 -0
  138. package/src/chat/ui/ReferenceChip.tsx +3 -0
  139. package/src/chat/ui/ResumeCompactionDock.tsx +1 -0
  140. package/src/chat/ui/RowTime.tsx +1 -0
  141. package/src/chat/ui/RunSettings.tsx +38 -0
  142. package/src/chat/ui/Scrubber.tsx +14 -0
  143. package/src/chat/ui/SessionLine.tsx +1 -0
  144. package/src/chat/ui/ShellCommand.tsx +32 -0
  145. package/src/chat/ui/SubagentControls.tsx +7 -0
  146. package/src/chat/ui/SubagentInfo.tsx +1 -0
  147. package/src/chat/ui/SubagentStopButton.tsx +6 -0
  148. package/src/chat/ui/SubagentTimeline.tsx +10 -0
  149. package/src/chat/ui/Timeline.tsx +119 -0
  150. package/src/chat/ui/TimelineMenu.tsx +5 -0
  151. package/src/chat/ui/UnifiedDiff.tsx +6 -0
  152. package/src/chat/ui/UploadThumb.tsx +2 -0
  153. package/src/chat/ui/VisualDialogs.tsx +3 -0
  154. package/src/chat/ui/VisualFrame.tsx +15 -0
  155. package/src/chat/ui/Welcome.tsx +1 -0
  156. package/src/chat/ui/chip-clipboard.ts +20 -0
  157. package/src/chat/ui/code-lines.ts +14 -0
  158. package/src/chat/ui/code-theme.ts +1 -0
  159. package/src/chat/ui/composer/ComposerControls.tsx +5 -0
  160. package/src/chat/ui/composer/ComposerMenu.tsx +12 -0
  161. package/src/chat/ui/composer/DraftParts.tsx +2 -0
  162. package/src/chat/ui/composer/QueuedMessages.tsx +4 -0
  163. package/src/chat/ui/composer/chip-paste.ts +16 -0
  164. package/src/chat/ui/composer/chips.ts +6 -0
  165. package/src/chat/ui/composer/editor.ts +9 -0
  166. package/src/chat/ui/composer/insert.ts +2 -0
  167. package/src/chat/ui/composer/key-handler.ts +39 -0
  168. package/src/chat/ui/composer/keys.ts +23 -0
  169. package/src/chat/ui/composer/placeholder.ts +6 -0
  170. package/src/chat/ui/composer/prompt-picks.ts +1 -0
  171. package/src/chat/ui/composer/surface.ts +5 -0
  172. package/src/chat/ui/composer/use-chat-controls.ts +13 -0
  173. package/src/chat/ui/composer/use-compaction-offer.ts +3 -0
  174. package/src/chat/ui/composer/use-composer-menus.ts +2 -0
  175. package/src/chat/ui/composer/use-composer-submit.ts +27 -0
  176. package/src/chat/ui/composer/use-continuable-turn.ts +3 -5
  177. package/src/chat/ui/composer/use-draft-files.ts +24 -0
  178. package/src/chat/ui/composer/use-effort-stepper.ts +8 -0
  179. package/src/chat/ui/composer/use-notice.ts +4 -1
  180. package/src/chat/ui/composer/use-pending-requests.ts +3 -0
  181. package/src/chat/ui/composer/use-prompt-history.ts +20 -11
  182. package/src/chat/ui/composer/use-prompt-hold.ts +3 -0
  183. package/src/chat/ui/composer/use-prompt-menus.ts +34 -14
  184. package/src/chat/ui/composer/use-take-back.ts +20 -2
  185. package/src/chat/ui/diff/ApprovalReview.tsx +18 -15
  186. package/src/chat/ui/diff/DiffFallbackBoundary.tsx +1 -0
  187. package/src/chat/ui/diff/DiffOmitted.tsx +1 -0
  188. package/src/chat/ui/diff/DiffStack.tsx +2 -0
  189. package/src/chat/ui/diff/DiffUnavailable.tsx +1 -0
  190. package/src/chat/ui/diff/EditorEditDiff.tsx +13 -0
  191. package/src/chat/ui/diff/EditorPatchDiff.tsx +4 -0
  192. package/src/chat/ui/diff/LegacyDiffPool.tsx +2 -0
  193. package/src/chat/ui/diff/LegacyEditDiff.tsx +2 -0
  194. package/src/chat/ui/diff/LegacyUnifiedDiff.tsx +8 -0
  195. package/src/chat/ui/diff/RawDiff.tsx +1 -0
  196. package/src/chat/ui/diff/ReviewDiff.tsx +1 -0
  197. package/src/chat/ui/diff/diff-source-budget.ts +8 -0
  198. package/src/chat/ui/diff/editor-edit-model.ts +4 -0
  199. package/src/chat/ui/diff/review-plan.ts +9 -0
  200. package/src/chat/ui/diff/review-settle.ts +1 -0
  201. package/src/chat/ui/diff/syntax-runtime.ts +52 -0
  202. package/src/chat/ui/diff/use-review-stack.ts +9 -0
  203. package/src/chat/ui/diff-syntax.ts +12 -0
  204. package/src/chat/ui/diff-theme.ts +2 -0
  205. package/src/chat/ui/file-links.ts +2 -0
  206. package/src/chat/ui/find-reveal.ts +1 -0
  207. package/src/chat/ui/full-diff.ts +1 -0
  208. package/src/chat/ui/highlighter.ts +29 -15
  209. package/src/chat/ui/icons.tsx +2 -4
  210. package/src/chat/ui/index.ts +4 -0
  211. package/src/chat/ui/intelligent-ui/UiBlockFrame.tsx +25 -0
  212. package/src/chat/ui/intelligent-ui/UiFallbackPart.tsx +1 -0
  213. package/src/chat/ui/intelligent-ui/UiReply.tsx +63 -0
  214. package/src/chat/ui/intelligent-ui/block-local.ts +5 -0
  215. package/src/chat/ui/intelligent-ui/chart-data.ts +15 -0
  216. package/src/chat/ui/intelligent-ui/node-text.ts +17 -0
  217. package/src/chat/ui/intelligent-ui/renderers/binding.ts +1 -0
  218. package/src/chat/ui/intelligent-ui/renderers/chart.tsx +31 -0
  219. package/src/chat/ui/intelligent-ui/renderers/choices.tsx +4 -0
  220. package/src/chat/ui/intelligent-ui/renderers/content.tsx +18 -0
  221. package/src/chat/ui/intelligent-ui/renderers/data.tsx +11 -0
  222. package/src/chat/ui/intelligent-ui/renderers/inputs.tsx +13 -0
  223. package/src/chat/ui/intelligent-ui/renderers/links.tsx +9 -0
  224. package/src/chat/ui/intelligent-ui/renderers/map-slots.ts +15 -0
  225. package/src/chat/ui/intelligent-ui/renderers/map-style.ts +15 -0
  226. package/src/chat/ui/intelligent-ui/renderers/map-view.tsx +48 -0
  227. package/src/chat/ui/intelligent-ui/renderers/map.tsx +20 -0
  228. package/src/chat/ui/intelligent-ui/renderers/place-point.ts +3 -0
  229. package/src/chat/ui/intelligent-ui/renderers/structure.tsx +3 -0
  230. package/src/chat/ui/intelligent-ui/renderers/text.tsx +8 -0
  231. package/src/chat/ui/intelligent-ui/reply-parts.ts +5 -0
  232. package/src/chat/ui/intelligent-ui/table-data.ts +17 -0
  233. package/src/chat/ui/intelligent-ui/use-in-view.ts +4 -0
  234. package/src/chat/ui/intelligent-ui/use-later.ts +4 -0
  235. package/src/chat/ui/intelligent-ui/use-truncated.ts +5 -0
  236. package/src/chat/ui/intelligent-ui/use-ui-links.ts +29 -0
  237. package/src/chat/ui/intelligent-ui/use-ui-queries.ts +60 -0
  238. package/src/chat/ui/markdown-blocks.ts +64 -0
  239. package/src/chat/ui/mermaid.ts +4 -0
  240. package/src/chat/ui/quote-selection.ts +10 -0
  241. package/src/chat/ui/rehype-chips.ts +5 -0
  242. package/src/chat/ui/rehype-fade.ts +2 -0
  243. package/src/chat/ui/remark-html-as-text.ts +5 -0
  244. package/src/chat/ui/reveal.ts +18 -0
  245. package/src/chat/ui/rows/ForksRow.tsx +5 -0
  246. package/src/chat/ui/rows/GeneratedImageRow.tsx +10 -0
  247. package/src/chat/ui/rows/MessageRows.tsx +29 -5
  248. package/src/chat/ui/rows/RowContainer.tsx +1 -0
  249. package/src/chat/ui/rows/Rows.tsx +2 -0
  250. package/src/chat/ui/rows/SubagentRow.tsx +13 -0
  251. package/src/chat/ui/rows/TaskRow.tsx +7 -0
  252. package/src/chat/ui/rows/VisualRow.tsx +4 -0
  253. package/src/chat/ui/rows/WorkRows.tsx +46 -0
  254. package/src/chat/ui/rows/WorkflowRow.tsx +10 -0
  255. package/src/chat/ui/rows/row-rhythm.ts +7 -5
  256. package/src/chat/ui/theme-watch.ts +8 -7
  257. package/src/chat/ui/thread-references.ts +7 -0
  258. package/src/chat/ui/use-diff-source-tokens.ts +8 -0
  259. package/src/chat/ui/useToggleSet.ts +3 -0
  260. package/src/chat/ui/visual-theme.ts +30 -0
  261. package/src/chat/visual-bridge.ts +23 -0
  262. package/src/chat/visual-viewport.ts +12 -0
  263. package/src/host-hooks.ts +6 -4
  264. package/src/host.ts +2 -0
  265. package/src/index.ts +3 -0
  266. package/src/lazy.tsx +9 -0
  267. package/src/mounted-registry.ts +4 -0
  268. package/src/port-transport.ts +32 -0
  269. package/src/prompts/index.ts +2 -0
  270. package/src/prompts/logic/focus.ts +5 -0
  271. package/src/prompts/logic/keys.ts +12 -0
  272. package/src/prompts/logic/prompts.ts +8 -0
  273. package/src/prompts/logic/subjects.ts +8 -0
  274. package/src/prompts/logic/useFocusAfterAnswer.ts +2 -0
  275. package/src/prompts/logic/usePromptSession.ts +13 -1
  276. package/src/prompts/ui/ApprovalBody.tsx +2 -0
  277. package/src/prompts/ui/PromptActions.tsx +2 -0
  278. package/src/prompts/ui/PromptCard.tsx +6 -0
  279. package/src/prompts/ui/PromptView.tsx +6 -0
  280. package/src/prompts/ui/QuestionBody.tsx +11 -0
  281. package/src/providers/AccountColors.tsx +1 -0
  282. package/src/providers/AccountDetail.tsx +7 -0
  283. package/src/providers/AccountVariables.tsx +2 -0
  284. package/src/providers/AddAccountForm.tsx +5 -1
  285. package/src/providers/CliDetail.tsx +6 -5
  286. package/src/providers/InstallPage.tsx +11 -0
  287. package/src/providers/ProviderList.tsx +3 -0
  288. package/src/providers/ProvidersPane.tsx +18 -0
  289. package/src/providers/account-actions.ts +10 -8
  290. package/src/providers/install-steps.ts +3 -0
  291. package/src/providers/pane-target.ts +5 -0
  292. package/src/providers/provider-abilities.ts +1 -0
  293. package/src/providers/provider-groups.ts +6 -0
  294. package/src/providers/variables.ts +12 -0
  295. package/src/scope.ts +2 -0
  296. package/src/state/chats.ts +61 -14
  297. package/src/state/index.ts +2 -0
  298. package/src/state/provider-accounts.ts +7 -9
  299. package/src/state/providers.ts +1 -0
  300. package/src/state/usage.ts +6 -0
  301. package/src/storage.ts +18 -0
  302. package/src/testing/prompts.ts +5 -0
  303. package/src/transport.ts +18 -0
  304. package/src/usage/ChartLogo.tsx +12 -0
  305. package/src/usage/LimitsList.tsx +7 -8
  306. package/src/usage/UsageBreakdown.tsx +11 -4
  307. package/src/usage/UsageChart.tsx +5 -0
  308. package/src/usage/UsageLimits.tsx +10 -7
  309. package/src/usage/UsageLimitsCard.tsx +2 -0
  310. package/src/usage/UsagePage.tsx +16 -19
  311. package/src/usage/UsageSummary.tsx +4 -5
  312. package/src/usage/UsageTiles.tsx +2 -0
  313. package/src/usage/format.ts +7 -0
  314. package/src/usage/limit-groups.ts +14 -0
  315. package/src/usage/limits.ts +11 -13
  316. package/src/usage/money.ts +1 -0
  317. package/src/usage/summary.ts +15 -7
@@ -92,9 +92,11 @@ interface AccountPick {
92
92
 
93
93
  function optionSummary(option: ModelOptionDescriptor, selection: ModelSelection): string | null {
94
94
  const value = selection.options[option.id];
95
+
95
96
  if (option.type === 'select') {
96
97
  return option.choices.find((choice) => choice.id === (value ?? option.defaultChoice))?.label ?? null;
97
98
  }
99
+
98
100
  return (value ?? option.defaultValue) === true ? option.label : null;
99
101
  }
100
102
 
@@ -148,16 +150,21 @@ export function RunSettings({
148
150
  const current = modelName(selection.model, owner?.models);
149
151
  const short = shortModelName(current, owner?.models);
150
152
  const options = orderOptions((model?.options ?? []).map(localizedOption));
153
+
151
154
  const summary = options.flatMap((option) => {
152
155
  const label = optionSummary(option, selection);
156
+
153
157
  return label === null ? [] : [{ id: option.id, label, ring: option.id === CONTEXT_OPTION }];
154
158
  });
159
+
155
160
  // A CLI without a context choice still fills one, so the ring stands on its own next to the size.
156
161
  const hasContextOption = options.some((option) => option.id === CONTEXT_OPTION);
157
162
  const standaloneContext = !hasContextOption && Boolean(usage.contextWindow);
163
+
158
164
  if (standaloneContext) {
159
165
  summary.push({ id: CONTEXT_OPTION, label: formatTokens(usage.contextWindow ?? 0), ring: true });
160
166
  }
167
+
161
168
  const grouped = providers.length > 1;
162
169
  const live = modelGroupsOf(providers, false);
163
170
  const legacy = modelGroupsOf(providers, true);
@@ -176,7 +183,9 @@ export function RunSettings({
176
183
 
177
184
  const searchable = wantsModelSearch(providers);
178
185
  const favorites = useChatPreferences((preferences) => preferences.favoriteModels);
186
+
179
187
  const favoritesOf = (kind: AgentKind): ReadonlySet<string> => favoriteModelsOf(favorites, kind);
188
+
180
189
  const found = query.trim() === '' ? null : searchModels(inPickerOrder(providers), query, favoritesOf);
181
190
 
182
191
  // The menu moves the focus to its popup as it opens, so the search field takes it a frame later.
@@ -184,7 +193,9 @@ export function RunSettings({
184
193
  if (!open || !searchable) {
185
194
  return;
186
195
  }
196
+
187
197
  const frame = requestAnimationFrame(() => searchRef.current?.focus());
198
+
188
199
  return () => cancelAnimationFrame(frame);
189
200
  }, [open, searchable]);
190
201
 
@@ -192,6 +203,7 @@ export function RunSettings({
192
203
  if (!next) {
193
204
  setQuery('');
194
205
  }
206
+
195
207
  onOpenChange(next);
196
208
  };
197
209
 
@@ -200,10 +212,13 @@ export function RunSettings({
200
212
  if (!searchable || event.target === searchRef.current || event.metaKey || event.ctrlKey || event.altKey) {
201
213
  return;
202
214
  }
215
+
203
216
  const typed = event.key.length === 1 && event.key !== ' ';
217
+
204
218
  if (!typed && event.key !== 'Backspace') {
205
219
  return;
206
220
  }
221
+
207
222
  event.preventDefault();
208
223
  event.stopPropagation();
209
224
  setQuery((now) => (typed ? now + event.key : now.slice(0, -1)));
@@ -216,17 +231,22 @@ export function RunSettings({
216
231
  event.preventDefault();
217
232
  event.stopPropagation();
218
233
  setQuery('');
234
+
219
235
  return;
220
236
  }
237
+
221
238
  if (event.key === 'Enter') {
222
239
  event.preventDefault();
223
240
  const first = found?.[0];
241
+
224
242
  if (first !== undefined) {
225
243
  onModel(first.entry.kind, first.models[0]!.slug);
226
244
  changeOpen(false);
227
245
  }
246
+
228
247
  return;
229
248
  }
249
+
230
250
  if (event.key !== 'Escape' && event.key !== 'ArrowDown' && event.key !== 'ArrowUp' && event.key !== 'Tab') {
231
251
  event.stopPropagation();
232
252
  }
@@ -235,6 +255,7 @@ export function RunSettings({
235
255
  // The name wraps rather than truncates, so a long name a provider gives is read whole.
236
256
  const modelRow = (entry: ProviderInfo, row: ModelInfo) => {
237
257
  const starred = favoritesOf(entry.kind).has(row.slug);
258
+
238
259
  return (
239
260
  <Menu.Row key={row.slug} aria-label={row.name}>
240
261
  <Menu.RadioItem value={modelValue(entry.kind, row.slug)} closeOnClick>
@@ -256,10 +277,13 @@ export function RunSettings({
256
277
  // Every model of one provider, the starred ones first and the rest under a label per maker where the provider names them.
257
278
  const byMaker = (entry: ProviderInfo, models: ModelInfo[]) => {
258
279
  const starred = favoritesOf(entry.kind);
280
+
259
281
  return favoritesFirst(models, starred).map((row, i, sorted) => {
260
282
  const previous = sorted[i - 1];
283
+
261
284
  const labeled =
262
285
  row.vendor !== undefined && !starred.has(row.slug) && (previous === undefined || starred.has(previous.slug) || previous.vendor !== row.vendor);
286
+
263
287
  return (
264
288
  <Fragment key={row.slug}>
265
289
  {labeled && <Menu.GroupLabel>{row.vendor}</Menu.GroupLabel>}
@@ -273,9 +297,11 @@ export function RunSettings({
273
297
  const providerModels = (entry: ProviderInfo, models: ModelInfo[]) => {
274
298
  const starred = favoritesOf(entry.kind);
275
299
  const shortlist = shortlistOf(entry.kind, models, { provider, model: selection.model }, starred);
300
+
276
301
  if (shortlist === null) {
277
302
  return [...models.filter((row) => starred.has(row.slug)), ...models.filter((row) => !starred.has(row.slug))].map((row) => modelRow(entry, row));
278
303
  }
304
+
279
305
  return (
280
306
  <>
281
307
  {shortlist.map((row) => modelRow(entry, row))}
@@ -468,6 +494,7 @@ function AccountSubmenu({ provider, account }: { provider: AgentKind; account: A
468
494
  const host = useChatHost();
469
495
  const { t } = useTranslation('agent-chat');
470
496
  const { choice, started, onPick } = account;
497
+
471
498
  return (
472
499
  <Menu.SubmenuRoot>
473
500
  <Menu.SubmenuTrigger className="text-text-muted">
@@ -502,9 +529,11 @@ function AccountRows({ choice, locked }: { choice: AccountChoice; locked(id: str
502
529
  const { t } = useTranslation('agent-chat');
503
530
  const limits = useUsageLimits();
504
531
  const now = useNow(MINUTE_MS);
532
+
505
533
  return choice.offered.map((entry) => {
506
534
  const session = sessionWindow(limitsOfAccount(limits, entry.id));
507
535
  const disabled = locked(entry.id);
536
+
508
537
  const row = (
509
538
  <Menu.RadioItem key={entry.id} value={entry.id} disabled={disabled} closeOnClick className="items-start data-disabled:opacity-50">
510
539
  <span className="flex min-w-0 grow flex-col">
@@ -516,6 +545,7 @@ function AccountRows({ choice, locked }: { choice: AccountChoice; locked(id: str
516
545
  </span>
517
546
  </Menu.RadioItem>
518
547
  );
548
+
519
549
  return disabled ? (
520
550
  <Tooltip key={entry.id} label={t('pickers.account.forkToSwitch', { account: choice.nameOf(entry) })} side="right" sideOffset={12}>
521
551
  {row}
@@ -529,6 +559,7 @@ function AccountRows({ choice, locked }: { choice: AccountChoice; locked(id: str
529
559
  /* How full the context is, drawn small enough to sit in the pill beside the window's size. */
530
560
  function ContextRing({ usage }: { usage: ChatUsage }) {
531
561
  const fraction = contextFraction(usage);
562
+
532
563
  return (
533
564
  <svg width="14" height="14" viewBox="0 0 14 14" className={clsx('shrink-0', contextTone(fraction))} aria-hidden="true">
534
565
  <circle cx="7" cy="7" r={RING_RADIUS} fill="none" strokeWidth="2" className="stroke-border-strong" />
@@ -554,6 +585,7 @@ function ContextUsage({ usage, disabled, onCompact }: { usage: ChatUsage; disabl
554
585
  const percent = Math.round(fraction * 100);
555
586
  const segments = contextSegments(usage);
556
587
  const used = formatTokens(usage.contextTokens);
588
+
557
589
  return (
558
590
  <Menu.Group className="flex flex-col gap-2 px-2.5 py-1.5">
559
591
  <div className="flex items-center gap-2">
@@ -623,14 +655,17 @@ function ContextUsage({ usage, disabled, onCompact }: { usage: ChatUsage; disabl
623
655
  */
624
656
  function OptionRow({ option, selection, onChange }: { option: ModelOptionDescriptor; selection: ModelSelection; onChange(value: string | boolean): void }) {
625
657
  const icon = <Icon icon={optionIcon(option)} size={14} className="shrink-0 text-text-faint" />;
658
+
626
659
  if (option.type === 'boolean') {
627
660
  const checked = (selection.options[option.id] ?? option.defaultValue) === true;
661
+
628
662
  const item = (
629
663
  <Menu.CheckboxItem indicator="end" className="text-text-muted" checked={checked} onCheckedChange={onChange} closeOnClick={false}>
630
664
  {icon}
631
665
  <span className="min-w-0 grow truncate">{option.label}</span>
632
666
  </Menu.CheckboxItem>
633
667
  );
668
+
634
669
  return option.description ? (
635
670
  <Tooltip label={option.description} side="right" sideOffset={12}>
636
671
  {item}
@@ -639,7 +674,9 @@ function OptionRow({ option, selection, onChange }: { option: ModelOptionDescrip
639
674
  item
640
675
  );
641
676
  }
677
+
642
678
  const value = String(selection.options[option.id] ?? option.defaultChoice);
679
+
643
680
  if (option.choices.length > INLINE_CHOICES) {
644
681
  return (
645
682
  <Menu.SubmenuRoot>
@@ -663,6 +700,7 @@ function OptionRow({ option, selection, onChange }: { option: ModelOptionDescrip
663
700
  </Menu.SubmenuRoot>
664
701
  );
665
702
  }
703
+
666
704
  return (
667
705
  <div className="flex items-center gap-2.5 px-2.5 py-1.5 text-sm text-text-muted">
668
706
  {icon}
@@ -99,8 +99,10 @@ export const Scrubber = memo(function Scrubber({
99
99
  if (strip === null || typeof ResizeObserver === 'undefined') {
100
100
  return;
101
101
  }
102
+
102
103
  const observer = new ResizeObserver(() => setHeight(strip.offsetHeight));
103
104
  observer.observe(strip);
105
+
104
106
  return () => observer.disconnect();
105
107
  }, [strip]);
106
108
 
@@ -115,13 +117,16 @@ export const Scrubber = memo(function Scrubber({
115
117
 
116
118
  const anchor = useMemo(() => {
117
119
  const element = strip;
120
+
118
121
  if (anchorY === null || element === null) {
119
122
  return undefined;
120
123
  }
124
+
121
125
  return {
122
126
  contextElement: element,
123
127
  getBoundingClientRect: () => {
124
128
  const rect = element.getBoundingClientRect();
129
+
125
130
  return DOMRect.fromRect({ x: rect.left, y: rect.top + anchorY + TICK_HEIGHT_PX / 2, width: rect.width, height: 0 });
126
131
  }
127
132
  };
@@ -131,10 +136,13 @@ export const Scrubber = memo(function Scrubber({
131
136
 
132
137
  const actionsFor = (tick: ScrubberTick | null): CardAction[] =>
133
138
  tick === null ? [] : Object.values(CARD_ACTIONS).filter((action) => action.offers(tick, chat));
139
+
134
140
  const actions = actionsFor(hoveredTick);
141
+
135
142
  /* The message a bookmark goes on: the one it is on already, or the person's own. A wake is the machine's and holds none. */
136
143
  const markableOf = (tick: ScrubberTick | null): string | null =>
137
144
  tick === null || chat === null ? null : (tick.bookmark?.itemId ?? (tick.kind === 'person' ? tick.id : null));
145
+
138
146
  const menuTick = menuIndex === null ? null : (ticks[menuIndex] ?? null);
139
147
  const menuMarkable = markableOf(menuTick);
140
148
 
@@ -142,10 +150,13 @@ export const Scrubber = memo(function Scrubber({
142
150
  // The hover card is a child here as well, and a click in it is about no tick.
143
151
  const index = strip === null || cameThroughPortal(e) ? null : messageAt(layout, e.clientY - strip.getBoundingClientRect().top);
144
152
  const tick = index === null ? null : (ticks[index] ?? null);
153
+
145
154
  if (tick === null || (actionsFor(tick).length === 0 && markableOf(tick) === null)) {
146
155
  e.preventBaseUIHandler();
156
+
147
157
  return;
148
158
  }
159
+
149
160
  setMenuIndex(index);
150
161
  };
151
162
 
@@ -165,6 +176,7 @@ export const Scrubber = memo(function Scrubber({
165
176
  onPointerMove={(e) => {
166
177
  setPointing(true);
167
178
  const index = pointAt(e);
179
+
168
180
  if (index !== hovered) {
169
181
  setHovered(index);
170
182
  }
@@ -172,6 +184,7 @@ export const Scrubber = memo(function Scrubber({
172
184
  onPointerLeave={() => setPointing(false)}
173
185
  onClick={(e) => {
174
186
  const index = pointAt(e);
187
+
175
188
  if (index !== null) {
176
189
  onPick(index);
177
190
  }
@@ -183,6 +196,7 @@ export const Scrubber = memo(function Scrubber({
183
196
  const distance = pointing && hoveredSlot !== null ? Math.abs(index - hoveredSlot) : null;
184
197
  // A bookmark is a place to find again from anywhere, so it never dims with the rest.
185
198
  const bright = slot.marked || slotInView(slot, inView) || distance === 0;
199
+
186
200
  return (
187
201
  <Fragment key={slot.first}>
188
202
  <span
@@ -20,6 +20,7 @@ export function SessionLine({ window, now }: { window: UsageWindow; now: number
20
20
  const tone = sessionTone(window.used);
21
21
  const mark = evenPace(window, now)?.percent ?? null;
22
22
  const resetsAt = window.resetsAt;
23
+
23
24
  return (
24
25
  <span className="flex items-center gap-2 text-xs text-text-faint tabular-nums">
25
26
  <span aria-hidden className="relative h-1 w-14 shrink-0 rounded-full bg-surface-sunken">
@@ -32,6 +32,7 @@ const cache = new Map<string, Run[]>();
32
32
  function runsOf(highlighter: Highlighter, code: string, language: string, theme: string, offset: number): Run[] {
33
33
  const fg = highlighter.getTheme(theme).fg.toLowerCase();
34
34
  const { tokens } = highlighter.codeToTokens(code, { lang: language as BundledLanguage, theme });
35
+
35
36
  return tokens.flat().map((token) => ({
36
37
  start: offset + token.offset,
37
38
  end: offset + token.offset + token.content.length,
@@ -44,18 +45,23 @@ function runsOf(highlighter: Highlighter, code: string, language: string, theme:
44
45
  /* `base` with `[start, end)` cut out of it and `inner` in its place. */
45
46
  function overlay(base: readonly Run[], start: number, end: number, inner: readonly Run[]): Run[] {
46
47
  const kept: Run[] = [];
48
+
47
49
  for (const run of base) {
48
50
  if (run.end <= start || run.start >= end) {
49
51
  kept.push(run);
52
+
50
53
  continue;
51
54
  }
55
+
52
56
  if (run.start < start) {
53
57
  kept.push({ ...run, end: start, content: run.content.slice(0, start - run.start) });
54
58
  }
59
+
55
60
  if (run.end > end) {
56
61
  kept.push({ ...run, start: end, content: run.content.slice(end - run.start) });
57
62
  }
58
63
  }
64
+
59
65
  return [...kept, ...inner].sort((left, right) => left.start - right.start);
60
66
  }
61
67
 
@@ -74,25 +80,34 @@ function languagesOf(command: string, extra: readonly EmbeddedCode[]): Set<strin
74
80
  function highlight(command: string, extra: readonly EmbeddedCode[], theme: string, code: CodeHighlighter): Run[] | null {
75
81
  const key = `${theme}\n${extra.map(({ start, end, language }) => `${start}:${end}:${language}`).join(',')}\n${command}`;
76
82
  const known = cache.get(key);
83
+
77
84
  if (known !== undefined) {
78
85
  // Touched entries move to the end, so the oldest falls out first.
79
86
  cache.delete(key);
80
87
  cache.set(key, known);
88
+
81
89
  return known;
82
90
  }
91
+
83
92
  const embedded = codeIn(command, extra);
84
93
  const highlighter = code.ready([SHELL_LANGUAGE, ...embedded.map(({ language }) => language)], theme);
94
+
85
95
  if (highlighter === null) {
86
96
  return null;
87
97
  }
98
+
88
99
  let runs = runsOf(highlighter, command, code.language(SHELL_LANGUAGE), theme, 0);
100
+
89
101
  for (const part of embedded) {
90
102
  runs = overlay(runs, part.start, part.end, runsOf(highlighter, command.slice(part.start, part.end), code.language(part.language), theme, part.start));
91
103
  }
104
+
92
105
  cache.set(key, runs);
106
+
93
107
  if (cache.size > CACHE_LIMIT) {
94
108
  cache.delete(cache.keys().next().value!);
95
109
  }
110
+
96
111
  return runs;
97
112
  }
98
113
 
@@ -101,20 +116,26 @@ function draw(command: string, runs: readonly Run[] | null, from: number, to: nu
101
116
  if (runs === null) {
102
117
  return [command.slice(from, to)];
103
118
  }
119
+
104
120
  const parts: ReactNode[] = [];
105
121
  let at = from;
122
+
106
123
  for (const run of runs) {
107
124
  if (run.end <= from || run.start >= to) {
108
125
  continue;
109
126
  }
127
+
110
128
  const start = Math.max(run.start, from);
111
129
  const end = Math.min(run.end, to);
130
+
112
131
  // Shiki leaves the line breaks out of its tokens; they come from the command itself.
113
132
  if (start > at) {
114
133
  parts.push(command.slice(at, start));
115
134
  }
135
+
116
136
  const content = run.content.slice(start - run.start, end - run.start);
117
137
  const style = tokenStyle(run);
138
+
118
139
  parts.push(
119
140
  style === undefined ? (
120
141
  content
@@ -124,11 +145,14 @@ function draw(command: string, runs: readonly Run[] | null, from: number, to: nu
124
145
  </span>
125
146
  )
126
147
  );
148
+
127
149
  at = end;
128
150
  }
151
+
129
152
  if (at < to) {
130
153
  parts.push(command.slice(at, to));
131
154
  }
155
+
132
156
  return parts;
133
157
  }
134
158
 
@@ -148,14 +172,18 @@ function Name({ named }: { named: NamedWord }) {
148
172
  function drawNamed(command: string, runs: readonly Run[] | null, names: readonly NamedWord[]): ReactNode[] {
149
173
  const parts: ReactNode[] = [];
150
174
  let at = 0;
175
+
151
176
  for (const named of names) {
152
177
  if (named.start < at) {
153
178
  continue;
154
179
  }
180
+
155
181
  parts.push(...draw(command, runs, at, named.start), <Name key={`name-${named.start}`} named={named} />);
156
182
  at = named.end;
157
183
  }
184
+
158
185
  parts.push(...draw(command, runs, at, command.length));
186
+
159
187
  return parts;
160
188
  }
161
189
 
@@ -180,10 +208,13 @@ export function ShellCommand({ command, code = NO_CODE, names = NO_NAMES }: { co
180
208
  if (runs !== null || tooLong) {
181
209
  return;
182
210
  }
211
+
183
212
  let cancelled = false;
213
+
184
214
  Promise.all([highlighter.loadTheme(theme, custom), ...[...languagesOf(command, code)].map((lang) => highlighter.loadLanguage(lang))])
185
215
  .then(() => !cancelled && setLoads((count) => count + 1))
186
216
  .catch(() => undefined);
217
+
187
218
  return () => {
188
219
  cancelled = true;
189
220
  };
@@ -192,5 +223,6 @@ export function ShellCommand({ command, code = NO_CODE, names = NO_NAMES }: { co
192
223
  if (names.length > 0) {
193
224
  return <>{drawNamed(command, runs, names)}</>;
194
225
  }
226
+
195
227
  return <>{runs === null ? command : draw(command, runs, 0, command.length)}</>;
196
228
  }
@@ -11,9 +11,11 @@ const CRUMB_LINK = 'truncate text-text-muted hover:text-text';
11
11
  /* The chat's own title in the bar; while a sub-agent stands in the chat's place, pressing it goes back to the main agent. */
12
12
  export function SubagentTitleCrumb({ chatId, className, children }: { chatId: string; className?: string; children: ReactNode }) {
13
13
  const { trail, show } = useSubagentTrail(chatId);
14
+
14
15
  if (trail.length === 0) {
15
16
  return <>{children}</>;
16
17
  }
18
+
17
19
  return (
18
20
  <button type="button" className={clsx('flex min-w-0 items-center', className)} onClick={() => show(MAIN_AGENT)}>
19
21
  {children}
@@ -25,9 +27,11 @@ export function SubagentTitleCrumb({ chatId, className, children }: { chatId: st
25
27
  export function SubagentBreadcrumb({ chatId, className }: { chatId: string; className?: string }) {
26
28
  const { t } = useTranslation('agent-chat');
27
29
  const { trail, show } = useSubagentTrail(chatId);
30
+
28
31
  if (trail.length === 0) {
29
32
  return null;
30
33
  }
34
+
31
35
  return (
32
36
  <nav aria-label={t('subagents.title')} className={clsx('flex min-w-0 items-center gap-1', className)}>
33
37
  {breadcrumbOf(trail).map((step) => (
@@ -53,11 +57,14 @@ export function SubagentActions({ chatId }: { chatId: string }) {
53
57
  const { t } = useTranslation('agent-chat');
54
58
  const { trail, show } = useSubagentTrail(chatId);
55
59
  const subagents = useOpenableSubagents(chatId);
60
+
56
61
  if (trail.length === 0) {
57
62
  return null;
58
63
  }
64
+
59
65
  // Only a row of the chat's own thread can be stopped; a grandchild belongs to the sub-agent that opened it.
60
66
  const shown = trail.length === 1 ? subagents.find((item) => item.toolUseId === trail[0]!.toolUseId) : undefined;
67
+
61
68
  return (
62
69
  <div className="flex shrink-0 items-center gap-1">
63
70
  {shown !== undefined && <SubagentStopButton chatId={chatId} item={shown} size="sm" />}
@@ -24,6 +24,7 @@ export function SubagentInfo({ chatId, toolUseId }: { chatId: string; toolUseId:
24
24
  const now = useNow(1000, item?.status === 'running');
25
25
  const word = item === null ? null : statusWordOf(item, task);
26
26
  const facts = item === null ? [] : subagentFacts(item, item.model === undefined ? null : modelName);
27
+
27
28
  return (
28
29
  <section aria-label={t('subagents.info.label')} className="shrink-0 border-b border-border py-1.5 pr-1.5 pl-4">
29
30
  <div className="flex min-w-0 items-center gap-2 text-xs text-text-faint">
@@ -26,9 +26,11 @@ export function SubagentStopButton({
26
26
  const actions = useChatActions();
27
27
  const turnRunning = useChatRow(chatId, (row) => (row?.info.activeTurnId ?? null) !== null);
28
28
  const stop = stopOf(item, turnRunning);
29
+
29
30
  if (stop === null) {
30
31
  return null;
31
32
  }
33
+
32
34
  const send = (): void => {
33
35
  void actions.stopSubagent(chatId, item.toolUseId).catch((e: unknown) => {
34
36
  const stale = e instanceof Error && 'code' in e && e.code === 'subagent-stale';
@@ -36,12 +38,16 @@ export function SubagentStopButton({
36
38
  host.notify({ kind: 'error', title: t('subagents.stopFailed'), description });
37
39
  });
38
40
  };
41
+
39
42
  const run = (): void => {
40
43
  if (stop === 'task' && item.childId !== undefined) {
41
44
  host.confirm.stopTask(id, item.childId, subagentTitle(item), send);
45
+
42
46
  return;
43
47
  }
48
+
44
49
  send();
45
50
  };
51
+
46
52
  return <IconButton icon={Square} size={size} label={stopLabel(stop)} className={className} onClick={run} />;
47
53
  }
@@ -38,9 +38,11 @@ export function SubagentTimeline({ chatId, toolUseId }: { chatId: string; toolUs
38
38
  const threadRef = useRef<HTMLDivElement>(null);
39
39
  const [target, setTarget] = useState<TimelineTarget>(EMPTY_TARGET);
40
40
  const followRef = useRef(true);
41
+
41
42
  const stopFollowing = () => {
42
43
  followRef.current = false;
43
44
  };
45
+
44
46
  const groups = useToggleSet(stopFollowing);
45
47
  const subagents = useToggleSet(stopFollowing);
46
48
  // The row being read when an earlier page was asked for, so it stays where it was once that page is in.
@@ -52,6 +54,7 @@ export function SubagentTimeline({ chatId, toolUseId }: { chatId: string; toolUs
52
54
  const conversation = new SubagentConversation(scope.transport, chatId, toolUseId, setState);
53
55
  controller.current = conversation;
54
56
  void conversation.start();
57
+
55
58
  return () => {
56
59
  conversation.dispose();
57
60
  controller.current = null;
@@ -74,17 +77,21 @@ export function SubagentTimeline({ chatId, toolUseId }: { chatId: string; toolUs
74
77
 
75
78
  useLayoutEffect(() => {
76
79
  const element = scrollRef.current;
80
+
77
81
  if (element === null) {
78
82
  return;
79
83
  }
84
+
80
85
  const head = state.items[0]?.id;
81
86
  const moved = head !== headRef.current;
82
87
  headRef.current = head;
88
+
83
89
  if (moved && anchorRef.current !== null && !followRef.current) {
84
90
  restoreAnchor(element, anchorRef.current);
85
91
  } else if (followRef.current) {
86
92
  element.scrollTop = element.scrollHeight;
87
93
  }
94
+
88
95
  if (moved) {
89
96
  anchorRef.current = null;
90
97
  }
@@ -92,9 +99,11 @@ export function SubagentTimeline({ chatId, toolUseId }: { chatId: string; toolUs
92
99
 
93
100
  const loadEarlier = useCallback(() => {
94
101
  const element = scrollRef.current;
102
+
95
103
  if (element === null || state.cursor === null || !wantsEarlier(element)) {
96
104
  return;
97
105
  }
106
+
98
107
  // Taken on every call, since the reader may scroll on while a page is on its way.
99
108
  anchorRef.current = firstRowInView(element);
100
109
  void controller.current?.loadEarlier();
@@ -153,6 +162,7 @@ export function SubagentTimeline({ chatId, toolUseId }: { chatId: string; toolUs
153
162
  {rows.length === 0 && !state.live && <EmptyState icon={Bot}>{t('rows.subagent.nothingYet')}</EmptyState>}
154
163
  {rows.map((row, index) => {
155
164
  const previous = index > 0 ? rows[index - 1]! : null;
165
+
156
166
  return (
157
167
  <div key={row.id} data-item-id={row.id} className={rowRhythm(row, previous)}>
158
168
  <Row