@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
@@ -21,10 +21,8 @@ export interface ChatState {
21
21
  historyPending?: boolean;
22
22
  items: Record<string, ChatItem>;
23
23
  /*
24
- * The items as the timeline groups them: the same map as `items`, except that a delta growing a
25
- * reply or a thought that already has text, or the output of a running call, leaves it as it was.
26
- * The rows are derived from this, so a word arriving touches the row that draws it and not the
27
- * whole thread; that row reads its text from `items` with `useCurrentItem`.
24
+ * `items`, except that a delta growing a reply or thought that has text, or a running call's output,
25
+ * leaves it as it was. The rows derive from this, so a word redraws only its row (`useCurrentItem`).
28
26
  */
29
27
  structure: Record<string, ChatItem>;
30
28
  order: string[];
@@ -100,21 +98,25 @@ interface ChatsStore {
100
98
  forgetWhere(matches: (key: string) => boolean): void;
101
99
  }
102
100
 
103
- function isWaiting(item: ChatItem | undefined): item is ChatApprovalItem | ChatQuestionItem {
101
+ export function isWaiting(item: ChatItem | undefined): item is ChatApprovalItem | ChatQuestionItem {
104
102
  return (item?.kind === 'approval' && item.decision === 'pending') || (item?.kind === 'question' && item.state === 'pending');
105
103
  }
106
104
 
107
105
  function stateOf(info: ChatInfo, items: ChatItem[], page?: ChatPage): ChatState {
108
106
  const byId = Object.fromEntries(items.map((item) => [item.id, item]));
109
107
  const order = items.map((item) => item.id);
108
+
110
109
  if (page === undefined || page.history.cursor === null) {
111
110
  return { info, items: byId, structure: byId, order };
112
111
  }
112
+
113
113
  const before = page.pending.filter((item) => !(item.id in byId));
114
114
  const subagents = (page.subagents ?? []).filter((item) => !(item.id in byId));
115
+
115
116
  for (const item of [...before, ...subagents]) {
116
117
  byId[item.id] = item;
117
118
  }
119
+
118
120
  return {
119
121
  info,
120
122
  items: byId,
@@ -137,13 +139,17 @@ type ItemWriter = (state: ChatState, item: ChatItem, structural?: boolean) => Ch
137
139
  function withEarlierItem(state: ChatState, item: ChatItem, write: ItemWriter): ChatState {
138
140
  if (item.kind === 'subagent') {
139
141
  const subagents = state.subagentsBefore ?? [];
142
+
140
143
  return { ...write(state, item), subagentsBefore: subagents.includes(item.id) ? subagents : [...subagents, item.id] };
141
144
  }
145
+
142
146
  const waiting = state.waitingBefore ?? [];
143
147
  const held = waiting.includes(item.id);
148
+
144
149
  if (isWaiting(item)) {
145
150
  return { ...write(state, item), waitingBefore: held ? waiting : [...waiting, item.id] };
146
151
  }
152
+
147
153
  return held ? { ...write(state, item), waitingBefore: waiting.filter((id) => id !== item.id) } : state;
148
154
  }
149
155
 
@@ -152,22 +158,29 @@ export function prependPage(state: ChatState, cursor: string, page: ChatHistoryR
152
158
  if (state.history?.cursor !== cursor) {
153
159
  return state;
154
160
  }
161
+
155
162
  const { historyPending: _historyPending, ...loaded } = state;
156
163
  const held = new Set(state.order);
157
164
  const older = page.items.filter((item) => !held.has(item.id));
158
165
  const olderIds = new Set(older.map((item) => item.id));
159
166
  const items = { ...state.items };
167
+
160
168
  for (const item of older) {
161
169
  // A request from before the page that this client already holds came in live, which is never older than the page.
162
170
  items[item.id] = state.items[item.id] ?? item;
163
171
  }
172
+
164
173
  const order = [...olderIds, ...state.order];
174
+
165
175
  if (page.history.cursor === null) {
166
176
  const { history: _history, waitingBefore: _waitingBefore, subagentsBefore: _subagentsBefore, ...rest } = loaded;
177
+
167
178
  return { ...rest, items, structure: items, order };
168
179
  }
180
+
169
181
  const waitingBefore = (state.waitingBefore ?? []).filter((id) => !olderIds.has(id));
170
182
  const subagentsBefore = (state.subagentsBefore ?? []).filter((id) => !olderIds.has(id));
183
+
171
184
  return { ...loaded, items, structure: items, order, history: page.history, waitingBefore, subagentsBefore };
172
185
  }
173
186
 
@@ -178,6 +191,7 @@ export function waitingRequestsOf(chat: Pick<ChatState, 'structure' | 'order' |
178
191
 
179
192
  function withItem(state: ChatState, item: ChatItem, structural = true): ChatState {
180
193
  const items = { ...state.items, [item.id]: item };
194
+
181
195
  return structural ? { ...state, items, structure: items } : { ...state, items };
182
196
  }
183
197
 
@@ -201,18 +215,23 @@ export function applyEvent(state: ChatState, event: ChatEvent): ChatState {
201
215
 
202
216
  function replayEvents(state: ChatState, events: readonly ChatEvent[]): ChatState {
203
217
  let draft: Record<string, ChatItem> | undefined;
218
+
204
219
  const write: ItemWriter = (current, item, structural = true) => {
205
220
  // A text delta must leave the last structural snapshot intact, including within this replay.
206
221
  if (draft !== current.items || (!structural && draft === current.structure)) {
207
222
  draft = { ...current.items };
208
223
  }
224
+
209
225
  // Item ids may be __proto__, so this must define an own data property.
210
226
  Object.defineProperty(draft, item.id, { value: item, enumerable: true, configurable: true, writable: true });
227
+
211
228
  return structural ? { ...current, items: draft, structure: draft } : { ...current, items: draft };
212
229
  };
230
+
213
231
  for (const event of events) {
214
232
  state = applyUpdate(state, event, write);
215
233
  }
234
+
216
235
  return state;
217
236
  }
218
237
 
@@ -222,37 +241,44 @@ function applyUpdate(state: ChatState, event: ChatEvent, write: ItemWriter): Cha
222
241
  if (isBeforePage(state, event.historyIndex)) {
223
242
  return withEarlierItem(state, event.item, write);
224
243
  }
244
+
225
245
  const known = event.item.id in state.items;
246
+
226
247
  return { ...write(state, event.item), order: known ? state.order : [...state.order, event.item.id] };
227
248
  }
228
249
  case 'delta': {
229
250
  const item = state.items[event.itemId];
251
+
230
252
  if (event.uiQueries && item?.kind === 'assistant') {
231
253
  // The text first, as the daemon's thread applies it, then the metadata on what it grew into.
232
254
  const { uiQueries, ...text } = event;
233
255
  const grown = event.text === '' ? state : applyUpdate(state, text, write);
234
256
  const reply = grown.items[event.itemId] ?? item;
257
+
235
258
  return write(grown, { ...reply, uiQueries } as ChatItem, false);
236
259
  }
260
+
237
261
  if (event.ui !== undefined && event.text === '') {
238
262
  if (item?.kind === 'assistant' && item.streaming && event.textLength === item.text.length) {
239
263
  return write(state, { ...item, ui: event.ui }, false);
240
264
  }
265
+
241
266
  return state;
242
267
  }
268
+
243
269
  if (item?.kind === 'assistant' || item?.kind === 'thinking') {
244
270
  const grown = { ...item, text: item.text + event.text };
271
+
245
272
  // The first text is structure after all, since an empty reply that is not streaming has no row.
246
- if (item.text === '') {
247
- return write(state, grown);
248
- }
249
- return write(state, grown, false);
273
+ return write(state, grown, item.text === '');
250
274
  }
275
+
251
276
  if (item?.kind === 'tool' && item.state === 'running') {
252
277
  const progress = item.progress ?? { startedAt: null, description: null, output: null };
253
- const grown = { ...item, progress: { ...progress, output: appendToolOutput(progress.output, event.text) } };
254
- return write(state, grown, false);
278
+
279
+ return write(state, { ...item, progress: { ...progress, output: appendToolOutput(progress.output, event.text) } }, false);
255
280
  }
281
+
256
282
  return state;
257
283
  }
258
284
  case 'info':
@@ -265,9 +291,11 @@ function applyUpdate(state: ChatState, event: ChatEvent, write: ItemWriter): Cha
265
291
  /* Fields of a held chat that are not part of its thread; a chat this client does not hold stays as it is. */
266
292
  function withFields(store: ChatsStore, key: string, fields: Pick<ChatState, 'bookmarks' | 'visuals'>): Partial<ChatsStore> {
267
293
  const current = store.byKey[key];
294
+
268
295
  if (!current) {
269
296
  return {};
270
297
  }
298
+
271
299
  return { byKey: { ...store.byKey, [key]: { ...current, ...fields } } };
272
300
  }
273
301
 
@@ -279,6 +307,7 @@ export const useChats = create<ChatsStore>((set) => ({
279
307
  // The bookmarks and the visuals are not part of the thread; they come in on their own and outlive a reset of it.
280
308
  const { bookmarks, visuals } = s.byKey[key] ?? {};
281
309
  const next = { ...stateOf(info, items, page), ...(bookmarks === undefined ? {} : { bookmarks }), ...(visuals === undefined ? {} : { visuals }) };
310
+
282
311
  return {
283
312
  byKey: { ...s.byKey, [key]: next },
284
313
  statusByKey: withStatus(s.statusByKey, key, info)
@@ -288,48 +317,59 @@ export const useChats = create<ChatsStore>((set) => ({
288
317
  prepend(key, cursor, page) {
289
318
  set((s) => {
290
319
  const current = s.byKey[key];
320
+
291
321
  if (!current) {
292
322
  return {};
293
323
  }
324
+
294
325
  const next = prependPage(current, cursor, page);
326
+
295
327
  return next === current ? {} : { byKey: { ...s.byKey, [key]: next } };
296
328
  });
297
329
  },
298
330
  apply(key, event) {
299
331
  set((s) => {
300
332
  const current = s.byKey[key];
333
+
301
334
  if (!current) {
302
335
  return {};
303
336
  }
337
+
304
338
  const next = applyEvent(current, event);
339
+
305
340
  return { byKey: { ...s.byKey, [key]: next }, statusByKey: withStatus(s.statusByKey, key, next.info) };
306
341
  });
307
342
  },
308
343
  replay(key, events) {
309
344
  set((store) => {
310
345
  const current = store.byKey[key];
346
+
311
347
  if (!current || events.length === 0) {
312
348
  return store;
313
349
  }
350
+
314
351
  const next = replayEvents(current, events);
352
+
315
353
  return next === current ? store : { byKey: { ...store.byKey, [key]: next }, statusByKey: withStatus(store.statusByKey, key, next.info) };
316
354
  });
317
355
  },
318
356
  /*
319
- * The status of a chat this window is not reading, which the host sends for every chat it has
320
- * loaded. The thread stays empty until somebody attaches, since this is what a header, a list and
321
- * the counters need, and they ask the info and never the items.
357
+ * The host sends this for every chat it has loaded. The thread stays empty until somebody attaches,
358
+ * since a header, a list and the counters read the info and never the items.
322
359
  */
323
360
  status(key, info) {
324
361
  set((s) => {
325
362
  const current = s.byKey[key];
363
+
326
364
  if (!current) {
327
365
  return { byKey: { ...s.byKey, [key]: { ...stateOf(info, []), historyPending: true } }, statusByKey: withStatus(s.statusByKey, key, info) };
328
366
  }
367
+
329
368
  // An attached client already had this on `chat.event`; writing it again would redraw the thread.
330
369
  if (sameInfo(current.info, info)) {
331
370
  return {};
332
371
  }
372
+
333
373
  return { byKey: { ...s.byKey, [key]: { ...current, info } }, statusByKey: withStatus(s.statusByKey, key, info) };
334
374
  });
335
375
  },
@@ -345,6 +385,7 @@ export const useChats = create<ChatsStore>((set) => ({
345
385
  delete next[key];
346
386
  const statuses = { ...s.statusByKey };
347
387
  delete statuses[key];
388
+
348
389
  return { byKey: next, statusByKey: statuses };
349
390
  });
350
391
  },
@@ -371,6 +412,7 @@ export function chatSink(keyOf: (chatId: string) => string): ChatSink {
371
412
  /* One chat's thread in the scope this is rendered in. The selector keeps a render tied to the field it reads. */
372
413
  export function useChatRow<T>(chatId: string, select: (row: ChatState | undefined) => T): T {
373
414
  const { keyOf } = useChatScope();
415
+
374
416
  return useChats((s) => select(s.byKey[keyOf(chatId)]));
375
417
  }
376
418
 
@@ -380,15 +422,19 @@ export function useChatRow<T>(chatId: string, select: (row: ChatState | undefine
380
422
  */
381
423
  export function useReportedModels(provider: AgentKind): ReadonlySet<string> {
382
424
  const scope = useContext(ChatScopeContext);
425
+
383
426
  const joined = useChats((s) => {
384
427
  const names = new Set<string>();
428
+
385
429
  for (const [key, state] of Object.entries(s.statusByKey)) {
386
430
  if (scope?.owns(key) === true && state.info.provider === provider && state.info.model !== null) {
387
431
  names.add(state.info.model);
388
432
  }
389
433
  }
434
+
390
435
  return [...names].sort().join('\n');
391
436
  });
437
+
392
438
  return new Set(joined === '' ? [] : joined.split('\n'));
393
439
  }
394
440
 
@@ -401,6 +447,7 @@ export function useAwaitsApproval(chatId: string, toolUseId: string): boolean {
401
447
  export function useCurrentItem<T extends ChatItem>(chatId: string, derived: T): T {
402
448
  return useChatRow(chatId, (row) => {
403
449
  const item = row?.items[derived.id];
450
+
404
451
  return item?.kind === derived.kind ? (item as T) : derived;
405
452
  });
406
453
  }
@@ -12,6 +12,7 @@ export {
12
12
  type ChatHistoryPage,
13
13
  type ChatPage
14
14
  } from './chats';
15
+
15
16
  export {
16
17
  useProviderAccountsStore,
17
18
  useProviderAccounts,
@@ -20,5 +21,6 @@ export {
20
21
  watchProviderAccounts,
21
22
  type ProviderAccountsRow
22
23
  } from './provider-accounts';
24
+
23
25
  export { useProvidersStore, useProviders, providersOf, providerSinkFor, type ProvidersRow } from './providers';
24
26
  export { type UsageMetric, USAGE_PREFERENCES_KEY, dayOf, windowFor, reloadUsagePreferences, useUsageStore, type UsagePeriod, type UsageView } from './usage';
@@ -1,7 +1,7 @@
1
1
  import { create } from 'zustand';
2
2
  import type { ProviderAccounts } from '@adecore/agent-contracts';
3
3
  import { useChatScope } from '../scope';
4
- import type { ChatTransport } from '../transport';
4
+ import { whenOpen, type ChatTransport } from '../transport';
5
5
 
6
6
  export interface ProviderAccountsRow {
7
7
  /* Null until the host answered, and for a host too old to keep accounts. */
@@ -33,6 +33,7 @@ export const useProviderAccountsStore = create<ProviderAccountsStore>((set, get)
33
33
  /* The accounts of the host in scope, like `useProviders`. */
34
34
  export function useProviderAccounts<T>(select: (row: ProviderAccountsRow) => T): T {
35
35
  const { id } = useChatScope();
36
+
36
37
  return useProviderAccountsStore((s) => select(s.byScope[id] ?? NONE));
37
38
  }
38
39
 
@@ -53,8 +54,10 @@ export function watchProviderAccounts(scopeId: string, transport: ChatTransport)
53
54
  const store = useProviderAccountsStore.getState();
54
55
  // A list asked for before a change arrived is older than that change, so it is dropped.
55
56
  let changes = 0;
57
+
56
58
  const load = (): void => {
57
59
  const asked = changes;
60
+
58
61
  transport
59
62
  .request('accounts.list', {})
60
63
  .then((accounts) => {
@@ -69,19 +72,14 @@ export function watchProviderAccounts(scopeId: string, transport: ChatTransport)
69
72
  }
70
73
  });
71
74
  };
75
+
72
76
  const off = [
73
77
  transport.on('accounts.changed', (accounts) => {
74
78
  changes += 1;
75
79
  store.set(scopeId, accounts);
76
80
  }),
77
- transport.subscribeStatus((status) => {
78
- if (status === 'open') {
79
- load();
80
- }
81
- })
81
+ whenOpen(transport, load)
82
82
  ];
83
- if (transport.status === 'open') {
84
- load();
85
- }
83
+
86
84
  return () => off.forEach((stop) => stop());
87
85
  }
@@ -34,6 +34,7 @@ export const useProvidersStore = create<ProvidersStore>((set, get) => ({
34
34
  */
35
35
  export function useProviders<T>(select: (row: ProvidersRow) => T): T {
36
36
  const { id } = useChatScope();
37
+
37
38
  return useProvidersStore((s) => select(s.byScope[id] ?? NONE));
38
39
  }
39
40
 
@@ -19,6 +19,7 @@ export const USAGE_PERIODS: readonly { id: UsagePeriod; days: number }[] = [
19
19
  const storageKey = (): string => chatStorageKey('usage');
20
20
 
21
21
  export let USAGE_PREFERENCES_KEY = storageKey();
22
+
22
23
  onChatStorageConfiguration(() => {
23
24
  USAGE_PREFERENCES_KEY = storageKey();
24
25
  });
@@ -35,6 +36,7 @@ const DEFAULTS: Preferences = { period: '7d', metric: 'cost', currency: 'USD' };
35
36
  function parsePreferences(raw: string | null): Preferences {
36
37
  try {
37
38
  const stored = JSON.parse(raw ?? '{}') as Partial<Preferences>;
39
+
38
40
  return {
39
41
  period: USAGE_PERIODS.some((period) => period.id === stored.period) ? stored.period! : DEFAULTS.period,
40
42
  metric: stored.metric === 'tokens' || stored.metric === 'cost' ? stored.metric : DEFAULTS.metric,
@@ -65,6 +67,7 @@ export function dayOf(date: Date): string {
65
67
  export function windowFor(period: UsagePeriod, now = new Date()): UsageSummaryPayload {
66
68
  const days = USAGE_PERIODS.find((entry) => entry.id === period)?.days ?? 7;
67
69
  const from = new Date(now.getFullYear(), now.getMonth(), now.getDate() - (days - 1));
70
+
68
71
  return {
69
72
  from: dayOf(from),
70
73
  to: dayOf(now),
@@ -129,9 +132,11 @@ export const useUsageStore = createChatStore<UsageStore>(
129
132
  storage.write({ ...chosen(get()), ...change });
130
133
  set(change);
131
134
  };
135
+
132
136
  const patchRow = (scopeId: string, change: Partial<UsageRow>): void => {
133
137
  set({ byScope: { ...get().byScope, [scopeId]: { ...(get().byScope[scopeId] ?? EMPTY), ...change } } });
134
138
  };
139
+
135
140
  return {
136
141
  ...DEFAULTS,
137
142
  byScope: {},
@@ -177,5 +182,6 @@ export type UsageView = Preferences & UsageRow;
177
182
 
178
183
  export function useUsage<T>(select: (view: UsageView) => T): T {
179
184
  const { id } = useChatScope();
185
+
180
186
  return useUsageStore((s) => select({ period: s.period, metric: s.metric, currency: s.currency, ...(s.byScope[id] ?? EMPTY) }));
181
187
  }
package/src/storage.ts CHANGED
@@ -37,12 +37,14 @@ export function configureChatStorage(configuration: ChatStorageOptions): void {
37
37
  if (hydrated) {
38
38
  throw new Error('Configure chat storage before using chat stores');
39
39
  }
40
+
40
41
  options = {
41
42
  ...configuration,
42
43
  keys: { ...configuration.keys },
43
44
  legacyNamespaces: [...(configuration.legacyNamespaces ?? [])],
44
45
  legacyKeys: Object.fromEntries(Object.entries(configuration.legacyKeys ?? {}).map(([key, values]) => [key, [...values]]))
45
46
  };
47
+
46
48
  for (const listener of configurationListeners) {
47
49
  listener();
48
50
  }
@@ -60,6 +62,7 @@ export function chatStorageAdapter(): ChatStorageAdapter | null {
60
62
  if (options.storage !== undefined) {
61
63
  return options.storage;
62
64
  }
65
+
63
66
  try {
64
67
  return globalThis.localStorage;
65
68
  } catch {
@@ -71,7 +74,9 @@ export function hydrateChatStorage(): void {
71
74
  if (hydrated) {
72
75
  return;
73
76
  }
77
+
74
78
  hydrated = true;
79
+
75
80
  for (const hydrate of hydrators) {
76
81
  hydrate();
77
82
  }
@@ -80,34 +85,47 @@ export function hydrateChatStorage(): void {
80
85
  export function createChatStore<T>(initial: StateCreator<T>, hydrate: (store: StoreApi<T>) => void): UseBoundStore<StoreApi<T>> {
81
86
  const store = create<T>(initial);
82
87
  let initialized = false;
88
+
83
89
  const hydrateOnce = (): void => {
84
90
  if (initialized) {
85
91
  return;
86
92
  }
93
+
87
94
  initialized = true;
88
95
  hydrate(store);
89
96
  };
97
+
90
98
  hydrators.push(hydrateOnce);
99
+
91
100
  const ready = (): void => {
92
101
  hydrateChatStorage();
93
102
  hydrateOnce();
94
103
  };
104
+
95
105
  const useStore = ((...args: Parameters<typeof store>) => {
96
106
  ready();
107
+
97
108
  return store(...args);
98
109
  }) as typeof store;
110
+
99
111
  Object.assign(useStore, store);
112
+
100
113
  useStore.getState = () => {
101
114
  ready();
115
+
102
116
  return store.getState();
103
117
  };
118
+
104
119
  useStore.subscribe = (...args) => {
105
120
  ready();
121
+
106
122
  return store.subscribe(...args);
107
123
  };
124
+
108
125
  useStore.setState = ((...args: Parameters<typeof store.setState>) => {
109
126
  ready();
110
127
  store.setState(...args);
111
128
  }) as typeof store.setState;
129
+
112
130
  return useStore;
113
131
  }
@@ -11,6 +11,7 @@ const endpoints: ChatQuestion = {
11
11
  { label: 'Only chat and files', description: 'Keep terminal retries separate' }
12
12
  ]
13
13
  };
14
+
14
15
  const checks: ChatQuestion = {
15
16
  id: 'checks',
16
17
  header: 'Validation',
@@ -22,10 +23,13 @@ const checks: ChatQuestion = {
22
23
  { label: 'Build', description: 'Verify the production bundle' }
23
24
  ]
24
25
  };
26
+
25
27
  const notes: ChatQuestion = { id: 'notes', header: 'Instructions', question: 'What else should the agent keep in mind?', multiSelect: false, choices: [] };
28
+
26
29
  function base(id: string) {
27
30
  return { id: `preview-${id}`, requestId: `preview-${id}`, createdAt: 0, turnId: null };
28
31
  }
32
+
29
33
  function question(id: string, questions: ChatQuestion[], async = false): ChatQuestionItem {
30
34
  return {
31
35
  ...base(id),
@@ -36,6 +40,7 @@ function question(id: string, questions: ChatQuestion[], async = false): ChatQue
36
40
  async
37
41
  };
38
42
  }
43
+
39
44
  function approval(long = false): ChatApprovalItem {
40
45
  return {
41
46
  ...base(long ? 'long' : 'edit'),
package/src/transport.ts CHANGED
@@ -43,8 +43,26 @@ export function errorCode(error: unknown): string | null {
43
43
  return typeof error === 'object' && error !== null && 'code' in error && typeof error.code === 'string' ? error.code : null;
44
44
  }
45
45
 
46
+ /* Runs `run` now while the link is up and again each time it comes back; answers the function that stops it. */
47
+ export function whenOpen(transport: ChatTransport, run: () => void): () => void {
48
+ if (transport.status === 'open') {
49
+ run();
50
+ }
51
+
52
+ return transport.subscribeStatus((status) => {
53
+ if (status === 'open') {
54
+ run();
55
+ }
56
+ });
57
+ }
58
+
59
+ export function errorMessage(error: unknown): string {
60
+ return error instanceof Error ? error.message : String(error);
61
+ }
62
+
46
63
  /* Whether a request failed because the link was down rather than because the host refused it; the reconnect asks again. */
47
64
  export function isConnectionError(error: unknown): boolean {
48
65
  const code = errorCode(error);
66
+
49
67
  return code === 'not-connected' || code === 'disconnected';
50
68
  }
@@ -0,0 +1,12 @@
1
+ import type { UsageProvider } from '@adecore/agent-contracts';
2
+ import { ProviderLogo } from '../agents/ProviderLogo';
3
+ import { PROVIDER_COLORS } from './format';
4
+
5
+ /* A provider's logo in its color of the chart, so it reads as the series it names. */
6
+ export function ChartLogo({ provider, className }: { provider: UsageProvider; className?: string }) {
7
+ return (
8
+ <span className={className} style={{ color: PROVIDER_COLORS[provider] }}>
9
+ <ProviderLogo provider={provider} />
10
+ </span>
11
+ );
12
+ }
@@ -6,8 +6,8 @@ import { limitsAccountId } from '../agents/account-limits';
6
6
  import { AccountDot } from '../agents/AccountDot';
7
7
  import { Tooltip } from '@adecore/ui';
8
8
  import { formatClock, formatWeekdayClock, isSameDay, formatCountdown, formatPercent } from '@adecore/ui/format';
9
- import { ProviderLogo } from '../agents/ProviderLogo';
10
- import { PROVIDER_COLORS, PROVIDER_LABELS } from './format';
9
+ import { ChartLogo } from './ChartLogo';
10
+ import { PROVIDER_LABELS } from './format';
11
11
  import { accountNote, evenPace, explain, isSignedOut, nextReset, paceText, type LimitAccount, type LimitGroup, type PaceStanding } from './limit-groups';
12
12
 
13
13
  /* Red where a window is nearly spent, amber where it is worth knowing; room to spare is not news and takes the text color. */
@@ -115,12 +115,11 @@ export function LimitsList({ limits, now, compact = false }: LimitsListProps) {
115
115
  <div className={clsx('flex flex-col', compact ? 'gap-4' : 'gap-6')}>
116
116
  {limits.providers.map((provider) => {
117
117
  const note = explain(provider);
118
+
118
119
  return (
119
120
  <div key={limitsAccountId(provider)} className="flex flex-col gap-2">
120
121
  <p className="flex items-center gap-2 text-xs font-medium">
121
- <span style={{ color: PROVIDER_COLORS[provider.kind] }}>
122
- <ProviderLogo provider={provider.kind} />
123
- </span>
122
+ <ChartLogo provider={provider.kind} />
124
123
  {PROVIDER_LABELS[provider.kind]}
125
124
  {provider.plan !== null && <span className="text-text-muted">· {provider.plan}</span>}
126
125
  </p>
@@ -145,6 +144,7 @@ function WindowLine({ window, now }: { window: UsageWindow; now: number }) {
145
144
  const { t } = useTranslation('agent-usage');
146
145
  const percent = Math.round(window.used * 100);
147
146
  const mark = evenPace(window, now)?.percent ?? null;
147
+
148
148
  return (
149
149
  <>
150
150
  <span className="text-text-muted">{window.label}</span>
@@ -179,6 +179,7 @@ function AccountLines({ account, now }: { account: LimitAccount; now: number })
179
179
  const windows = signedOut || note !== null ? [] : (account.entry?.windows ?? []);
180
180
  const next = nextReset(windows, now);
181
181
  const resetsIn = next === null ? null : resetInLabel(next.resetsAt, now);
182
+
182
183
  return (
183
184
  <>
184
185
  <p className="col-span-full mt-2 flex min-w-0 items-center gap-2">
@@ -214,9 +215,7 @@ export function AccountLimitsList({ groups, now }: { groups: readonly LimitGroup
214
215
  {groups.map((group) => (
215
216
  <Fragment key={group.kind}>
216
217
  <p className="col-span-full mt-3 flex items-center gap-2 font-medium first:mt-0">
217
- <span style={{ color: PROVIDER_COLORS[group.kind] }}>
218
- <ProviderLogo provider={group.kind} />
219
- </span>
218
+ <ChartLogo provider={group.kind} />
220
219
  {PROVIDER_LABELS[group.kind]}
221
220
  </p>
222
221
  {group.accounts.map((account) => (