@adecore/agents-react 0.39.0 → 0.40.1

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 +27 -27
  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
@@ -6,8 +6,8 @@ import type { UsageModel, UsageProject, UsageProvider, UsageSummaryResult } from
6
6
  import { EmptyState, Icon, SectionLabel, Segmented, Tooltip } from '@adecore/ui';
7
7
  import { useChatScope } from '../scope';
8
8
  import type { UsageMetric } from '../state/usage';
9
- import { ProviderLogo } from '../agents/ProviderLogo';
10
9
  import { USAGE_PROVIDERS, totalTokensOf } from '@adecore/agent-contracts';
10
+ import { ChartLogo } from './ChartLogo';
11
11
  import { useModelName } from '../agents/model-name';
12
12
  import { formatCount, formatTokens, PROVIDER_COLORS, PROVIDER_LABELS, shortPath, slotLabel } from './format';
13
13
  import { useMoney } from './money';
@@ -33,9 +33,7 @@ function barWidth(value: number, top: number): number {
33
33
  function ProviderMark({ provider }: { provider: UsageProvider }) {
34
34
  return (
35
35
  <Tooltip label={PROVIDER_LABELS[provider]}>
36
- <span style={{ color: PROVIDER_COLORS[provider] }}>
37
- <ProviderLogo provider={provider} />
38
- </span>
36
+ <ChartLogo provider={provider} />
39
37
  </Tooltip>
40
38
  );
41
39
  }
@@ -47,16 +45,19 @@ function ModelName({ provider, slug }: { provider: UsageProvider; slug: string }
47
45
 
48
46
  function PricedAs({ provider, slug }: { provider: UsageProvider; slug: string }) {
49
47
  const { t } = useTranslation('agent-usage');
48
+
50
49
  return t('breakdown.models.pricedAs', { model: useModelName(provider, slug) });
51
50
  }
52
51
 
53
52
  function ModelRows({ models, metric, total }: { models: readonly UsageModel[]; metric: UsageMetric; total: number }) {
54
53
  const { t } = useTranslation('agent-usage');
55
54
  const money = useMoney();
55
+
56
56
  /* On the tokens metric the biggest row is the one that moved the most, not the dearest. */
57
57
  const sorted = [...models].sort((a, b) =>
58
58
  metric === 'tokens' ? totalTokensOf(b.totals) - totalTokensOf(a.totals) : (b.costUsd ?? -1) - (a.costUsd ?? -1)
59
59
  );
60
+
60
61
  return (
61
62
  <div className={TABLE}>
62
63
  <div className={HEAD}>
@@ -111,6 +112,7 @@ function ModelRows({ models, metric, total }: { models: readonly UsageModel[]; m
111
112
  function ProjectRow({ project, top, money }: { project: UsageProject; top: number; money(usd: number): string }) {
112
113
  // The usage on screen is one host's, so the look of a project is that host's too.
113
114
  const look = useHostProjectLook(useChatScope().id, project.projectId);
115
+
114
116
  return (
115
117
  /* The mark sits on the title rather than between the two lines, so the icons line up with the names. */
116
118
  <div className={`${ROW} h-auto items-start py-1.5`}>
@@ -129,6 +131,7 @@ function ProjectRow({ project, top, money }: { project: UsageProject; top: numbe
129
131
  {USAGE_PROVIDERS.map((provider) => {
130
132
  const share = project.byProvider[provider];
131
133
  const width = barWidth(share?.costUsd ?? 0, top);
134
+
132
135
  return width === 0 ? null : <span key={provider} style={{ width: `${width}%`, background: PROVIDER_COLORS[provider] }} />;
133
136
  })}
134
137
  </span>
@@ -141,6 +144,7 @@ function ProjectRow({ project, top, money }: { project: UsageProject; top: numbe
141
144
  function ProjectRows({ projects }: { projects: readonly UsageProject[] }) {
142
145
  const money = useMoney();
143
146
  const top = Math.max(...projects.map((project) => project.costUsd), 0);
147
+
144
148
  return (
145
149
  <div className={clsx(TABLE, 'gap-1')}>
146
150
  {projects.map((project) => (
@@ -155,9 +159,11 @@ function DayRows({ summary, providers }: { summary: UsageSummaryResult; provider
155
159
  const { t } = useTranslation('agent-usage');
156
160
  const money = useMoney();
157
161
  const rows = deriveDays(summary);
162
+
158
163
  if (rows.length === 0) {
159
164
  return <EmptyState>{t('breakdown.days.empty')}</EmptyState>;
160
165
  }
166
+
161
167
  return (
162
168
  <div className={TABLE}>
163
169
  <div className={HEAD}>
@@ -198,6 +204,7 @@ export function UsageBreakdown({ summary, metric, providers }: UsageBreakdownPro
198
204
  const { t } = useTranslation('agent-usage');
199
205
  const [tab, setTab] = useState<Breakdown>('models');
200
206
  const total = summary.models.reduce((sum, model) => sum + (model.costUsd ?? 0), 0);
207
+
201
208
  return (
202
209
  <section className="flex flex-col gap-3">
203
210
  <div className="flex items-center gap-3">
@@ -48,20 +48,25 @@ export function UsageChart({ slots, providers, format, labelEvery }: UsageChartP
48
48
  <svg width={width} height={HEIGHT} role="presentation">
49
49
  {ticks.map((tick) => {
50
50
  const y = Math.round(HEIGHT - (tick / scale.max) * HEIGHT) + 0.5;
51
+
51
52
  return <line key={tick} x1={0} x2={width} y1={y} y2={y} stroke="var(--border)" strokeWidth={1} />;
52
53
  })}
53
54
  {slots.map((slot, index) => {
54
55
  const left = Math.round(index * band + (band - barWidth) / 2);
55
56
  let bottom = HEIGHT;
57
+
56
58
  return (
57
59
  <g key={slot.slot} opacity={hovered === null || hovered === index ? 1 : DIMMED}>
58
60
  {providers.map((provider) => {
59
61
  const value = slot.byProvider[provider] ?? 0;
62
+
60
63
  if (value <= 0) {
61
64
  return null;
62
65
  }
66
+
63
67
  const height = Math.max(1, Math.round((value / scale.max) * HEIGHT));
64
68
  bottom -= height;
69
+
65
70
  return <rect key={provider} x={left} y={bottom} width={barWidth} height={height} fill={PROVIDER_COLORS[provider]} />;
66
71
  })}
67
72
  </g>
@@ -5,13 +5,13 @@ import type { UsageProvider } from '@adecore/agent-contracts';
5
5
  import { AccountDot } from '../agents/AccountDot';
6
6
  import { Button, Icon, SectionLabel, Segmented, Tooltip, useNow } from '@adecore/ui';
7
7
  import { useChatHost } from '../host';
8
- import { PROVIDER_COLORS, PROVIDER_LABELS } from './format';
8
+ import { ChartLogo } from './ChartLogo';
9
+ import { PROVIDER_LABELS } from './format';
9
10
  import { accountNote, checkedLabel, hasSeveralAccounts, isSignedOut, type LimitAccount } from './limit-groups';
10
11
  import { WindowBar } from './LimitsList';
11
12
  import { useLimitGroups, useUsageLimits } from './limits';
12
- import { useProviderAccountsStore } from '../state/provider-accounts';
13
+ import { useProviderAccounts } from '../state/provider-accounts';
13
14
  import { useChatScope } from '../scope';
14
- import { ProviderLogo } from '../agents/ProviderLogo';
15
15
  import { useHostLoginBlocked } from '../host-hooks';
16
16
 
17
17
  const MINUTE_MS = 60_000;
@@ -26,6 +26,7 @@ interface Login {
26
26
 
27
27
  function LoginButton({ account, login }: { account: LimitAccount; login: Login }) {
28
28
  const { t } = useTranslation('agent-usage');
29
+
29
30
  const button = (
30
31
  <Button
31
32
  variant="secondary"
@@ -38,6 +39,7 @@ function LoginButton({ account, login }: { account: LimitAccount; login: Login }
38
39
  {t('limits.logIn')}
39
40
  </Button>
40
41
  );
42
+
41
43
  return login.blocked === null ? (
42
44
  button
43
45
  ) : (
@@ -54,12 +56,11 @@ function LimitCard({ account, named, now, login }: { account: LimitAccount; name
54
56
  const note = signedOut ? null : accountNote(account);
55
57
  const plan = signedOut ? null : account.entry?.plan;
56
58
  const checked = signedOut || account.entry === null ? null : checkedLabel(account.entry, now);
59
+
57
60
  return (
58
61
  <div className="flex min-w-0 flex-col gap-3.5 rounded-xl border border-border bg-surface bg-clip-padding px-4 py-3.5">
59
62
  <p className="flex min-w-0 items-center gap-2 text-xs font-medium">
60
- <span className="shrink-0" style={{ color: PROVIDER_COLORS[account.kind] }}>
61
- <ProviderLogo provider={account.kind} />
62
- </span>
63
+ <ChartLogo provider={account.kind} className="shrink-0" />
63
64
  <span className="shrink-0">{PROVIDER_LABELS[account.kind]}</span>
64
65
  {named && (
65
66
  <>
@@ -100,7 +101,7 @@ export function UsageLimits() {
100
101
  const groups = useLimitGroups(limits);
101
102
  const now = useNow(MINUTE_MS);
102
103
  const scopeId = useChatScope().id;
103
- const loginCommands = useProviderAccountsStore((s) => s.byScope[scopeId]?.accounts?.loginCommands);
104
+ const loginCommands = useProviderAccounts((row) => row.accounts?.loginCommands);
104
105
  const { openLogin } = host;
105
106
  const blocked = useHostLoginBlocked(scopeId);
106
107
  const [filter, setFilter] = useState<Filter>('all');
@@ -108,9 +109,11 @@ export function UsageLimits() {
108
109
  if (limits === null) {
109
110
  return null;
110
111
  }
112
+
111
113
  const filterable = groups.length > 1 && hasSeveralAccounts(groups);
112
114
  const current = filterable && groups.some((group) => group.kind === filter) ? filter : 'all';
113
115
  const shown = current === 'all' ? groups : groups.filter((group) => group.kind === current);
116
+
114
117
  const loginFor = (kind: UsageProvider): Login | null =>
115
118
  openLogin === null || loginCommands?.[kind] === undefined
116
119
  ? null
@@ -16,6 +16,7 @@ function CardBody() {
16
16
  if (limits === null) {
17
17
  return <p className="text-xs text-text-faint">{i18next.t('agent-usage:limits.loading')}</p>;
18
18
  }
19
+
19
20
  return hasSeveralAccounts(groups) ? <AccountLimitsList groups={groups} now={now} /> : <LimitsList limits={limits} now={now} compact />;
20
21
  }
21
22
 
@@ -25,6 +26,7 @@ function CardBody() {
25
26
  */
26
27
  export function UsageLimitsCard({ children }: { children: ReactElement<Record<string, unknown>> }) {
27
28
  const [opened, setOpened] = useState(false);
29
+
28
30
  return (
29
31
  <PreviewCard.Root onOpenChange={(open) => open && setOpened(true)}>
30
32
  <PreviewCard.Trigger render={children} delay={500} />
@@ -8,7 +8,7 @@ import { useChatHost } from '../host';
8
8
  import { useChatScope } from '../scope';
9
9
  import { useProvidersStore } from '../state/providers';
10
10
  import { askedKey, summaryPayload, USAGE_PERIODS, useUsage, useUsageStore, type UsageMetric, type UsagePeriod } from '../state/usage';
11
- import type { ChatTransport } from '../transport';
11
+ import { whenOpen, type ChatTransport } from '../transport';
12
12
  import { formatClock, formatCount, formatDate, formatTokens, PROVIDER_LABELS } from './format';
13
13
  import { useMoney } from './money';
14
14
  import { deriveUsage, labelEveryFor } from './summary';
@@ -44,9 +44,11 @@ function HostAgents() {
44
44
  const { id } = useChatScope();
45
45
  const row = useProvidersStore((s) => s.byScope[id]);
46
46
  const installed = (row?.providers ?? []).filter((provider) => provider.installed);
47
+
47
48
  if (!row?.loaded) {
48
49
  return null;
49
50
  }
51
+
50
52
  return (
51
53
  <div className="flex flex-col items-center gap-2">
52
54
  <p className="text-xs text-text-muted">
@@ -66,16 +68,9 @@ function HostAgents() {
66
68
  */
67
69
  function useSummary(scopeId: string, transport: ChatTransport, period: UsagePeriod, account: string | null): () => void {
68
70
  useEffect(() => {
69
- const subscribe = (): void => void transport.request('usage.subscribe', {}).catch(() => undefined);
70
- if (transport.status === 'open') {
71
- subscribe();
72
- }
73
71
  // A host knows its followers per link, so every connection has to be told again, on a reconnect and on a move to another host.
74
- const off = transport.subscribeStatus((status) => {
75
- if (status === 'open') {
76
- subscribe();
77
- }
78
- });
72
+ const off = whenOpen(transport, () => void transport.request('usage.subscribe', {}).catch(() => undefined));
73
+
79
74
  return () => {
80
75
  off();
81
76
  void transport.request('usage.unsubscribe', {}).catch(() => undefined);
@@ -86,6 +81,7 @@ function useSummary(scopeId: string, transport: ChatTransport, period: UsagePeri
86
81
  const payload = summaryPayload(period, account);
87
82
  const asked = askedKey(payload);
88
83
  useUsageStore.getState().setLoading(scopeId, true);
84
+
89
85
  transport
90
86
  .request('usage.summary', payload)
91
87
  .then((summary) => useUsageStore.getState().receive(scopeId, asked, summary))
@@ -93,17 +89,11 @@ function useSummary(scopeId: string, transport: ChatTransport, period: UsagePeri
93
89
  }, [transport, scopeId, period, account]);
94
90
 
95
91
  useEffect(() => {
96
- if (transport.status === 'open') {
97
- load();
98
- }
92
+ // Until the other host answers there is nothing to ask, and what is on screen is the host that left.
93
+ const offStatus = whenOpen(transport, load);
99
94
  // A scan that found something is the sign to ask again; nothing else changes the numbers.
100
95
  const offChanged = transport.on('usage.changed', load);
101
- // Until the other host answers there is nothing to ask, and what is on screen is the host that left.
102
- const offStatus = transport.subscribeStatus((status) => {
103
- if (status === 'open') {
104
- load();
105
- }
106
- });
96
+
107
97
  return () => {
108
98
  offChanged();
109
99
  offStatus();
@@ -118,16 +108,21 @@ function Provenance() {
118
108
  const summary = useUsage((s) => s.summary);
119
109
  const currency = useUsage((s) => s.currency);
120
110
  const failed = useUsage((s) => s.failed);
111
+
121
112
  if (failed) {
122
113
  return <FormError className="mt-auto text-center">{t('provenance.failed')}</FormError>;
123
114
  }
115
+
124
116
  if (summary === null) {
125
117
  return null;
126
118
  }
119
+
127
120
  const prices =
128
121
  summary.pricing.fetchedAt === null ? t('provenance.bundledPrices') : t('provenance.fetchedPrices', { date: formatDate(summary.pricing.fetchedAt) });
122
+
129
123
  // The rate is named only when it is being used, so a page in dollars says nothing about euros.
130
124
  const rate = currency === 'USD' || summary.rate === null ? null : t('provenance.rate', { currency: summary.rate.currency, date: summary.rate.date });
125
+
131
126
  return (
132
127
  <p className="mt-auto text-center text-xs text-text-muted">
133
128
  {t('provenance.scanned', { count: summary.scan.files, time: formatClock(summary.scan.at), files: formatCount(summary.scan.files) })} · {prices}
@@ -162,9 +157,11 @@ function LoadingBody() {
162
157
  */
163
158
  function AccountPicker({ accounts, value, onChange }: { accounts: readonly UsageAccount[]; value: string | null; onChange(account: string | null): void }) {
164
159
  const { t } = useTranslation('agent-usage');
160
+
165
161
  if (!USAGE_PROVIDERS.some((kind) => accounts.filter((account) => account.kind === kind).length > 1)) {
166
162
  return null;
167
163
  }
164
+
168
165
  return (
169
166
  <Select
170
167
  value={value ?? ALL_ACCOUNTS}
@@ -1,9 +1,9 @@
1
1
  import { useTranslation } from 'react-i18next';
2
2
  import type { UsageTotals } from '@adecore/agent-contracts';
3
3
  import type { UsageMetric } from '../state/usage';
4
- import { ProviderLogo } from '../agents/ProviderLogo';
5
4
  import { totalTokensOf } from '@adecore/agent-contracts';
6
- import { formatCount, formatTokens, PROVIDER_COLORS, PROVIDER_LABELS } from './format';
5
+ import { ChartLogo } from './ChartLogo';
6
+ import { formatCount, formatTokens, PROVIDER_LABELS } from './format';
7
7
  import { useMoney } from './money';
8
8
  import type { ProviderTotal } from './summary';
9
9
 
@@ -22,6 +22,7 @@ interface UsageSummaryProps {
22
22
  export function UsageSummary({ metric, costUsd, totals, sessions, providers }: UsageSummaryProps) {
23
23
  const { t } = useTranslation('agent-usage');
24
24
  const money = useMoney();
25
+
25
26
  return (
26
27
  <div className="flex flex-col gap-4">
27
28
  <div>
@@ -35,9 +36,7 @@ export function UsageSummary({ metric, costUsd, totals, sessions, providers }: U
35
36
  <div className="flex flex-col gap-2">
36
37
  {providers.map((provider) => (
37
38
  <div key={provider.provider} className="flex items-center gap-2 text-xs">
38
- <span style={{ color: PROVIDER_COLORS[provider.provider] }}>
39
- <ProviderLogo provider={provider.provider} />
40
- </span>
39
+ <ChartLogo provider={provider.provider} />
41
40
  <span className="text-text">{PROVIDER_LABELS[provider.provider]}</span>
42
41
  <span className="ml-auto tabular-nums text-text-muted">
43
42
  {money(provider.costUsd)} · {formatTokens(totalTokensOf(provider.totals))}
@@ -13,6 +13,7 @@ interface UsageTilesProps {
13
13
  export function UsageTiles({ totals, cacheSavingsUsd }: UsageTilesProps) {
14
14
  const { t } = useTranslation('agent-usage');
15
15
  const money = useMoney();
16
+
16
17
  const tiles: { id: string; value: string }[] = [
17
18
  { id: 'processed', value: formatTokens(totalTokensOf(totals)) },
18
19
  { id: 'uncachedInput', value: formatTokens(totals.input) },
@@ -21,6 +22,7 @@ export function UsageTiles({ totals, cacheSavingsUsd }: UsageTilesProps) {
21
22
  { id: 'output', value: formatTokens(totals.output) },
22
23
  { id: 'cacheSavings', value: money(cacheSavingsUsd) }
23
24
  ];
25
+
24
26
  return (
25
27
  <div className="grid grid-cols-2 gap-2 md:grid-cols-6">
26
28
  {tiles.map((tile) => (
@@ -1,5 +1,6 @@
1
1
  import type { UsageProvider, UsageRate } from '@adecore/agent-contracts';
2
2
  import { formatDay, formatHour, formatWeekdayDay, formatMoney, formatNumber, formatTokens } from '@adecore/ui/format';
3
+
3
4
  export { formatClock } from '@adecore/ui/format';
4
5
 
5
6
  /* Dollars are what a price table is in; euros are what the page can be set to. */
@@ -21,8 +22,10 @@ export function moneyFormat(currency: UsageCurrency, rate: UsageRate | null): (u
21
22
  const converts = currency !== 'USD' && rate !== null && rate.currency === currency;
22
23
  const factor = converts ? rate.rate : 1;
23
24
  const code = converts ? currency : 'USD';
25
+
24
26
  return (usd) => {
25
27
  const value = usd * factor;
28
+
26
29
  // Under a cent the two decimals of a currency read as zero, which is the one thing it is not.
27
30
  return formatMoney(value, code, value !== 0 && Math.abs(value) < 0.01 ? 4 : undefined);
28
31
  };
@@ -61,16 +64,19 @@ function hourOfSlot(slot: string): number | null {
61
64
  function atHour(slot: string, hour: number): Date {
62
65
  const day = dateOfSlot(slot);
63
66
  day.setHours(hour);
67
+
64
68
  return day;
65
69
  }
66
70
 
67
71
  export function slotLabel(slot: string): string {
68
72
  const hour = hourOfSlot(slot);
73
+
69
74
  return hour === null ? formatWeekdayDay(dateOfSlot(slot)) : formatHour(atHour(slot, hour));
70
75
  }
71
76
 
72
77
  export function slotAxisLabel(slot: string): string {
73
78
  const hour = hourOfSlot(slot);
79
+
74
80
  return hour === null ? formatDay(dateOfSlot(slot)) : formatHour(atHour(slot, hour));
75
81
  }
76
82
 
@@ -81,5 +87,6 @@ export function formatDate(at: number): string {
81
87
  /* Enough of a path's tail to tell two checkouts of the same repository apart, under a row that already carries the name. */
82
88
  export function shortPath(path: string): string {
83
89
  const parts = path.split('/').filter(Boolean);
90
+
84
91
  return parts.length <= 3 ? path : `…/${parts.slice(-3).join('/')}`;
85
92
  }
@@ -46,12 +46,15 @@ export function limitGroups(snapshot: UsageLimitsSnapshot, accounts: ProviderAcc
46
46
  if (!snapshot.providers.some((entry) => entry.kind === kind)) {
47
47
  return [];
48
48
  }
49
+
49
50
  const known = accountsOfKind(accounts, kind);
51
+
50
52
  const read = snapshot.providers
51
53
  .filter((entry) => entry.kind === kind)
52
54
  .map((entry): LimitAccount => {
53
55
  const id = limitsAccountId(entry);
54
56
  const account = known.find((each) => each.id === id);
57
+
55
58
  return {
56
59
  id,
57
60
  kind,
@@ -61,6 +64,7 @@ export function limitGroups(snapshot: UsageLimitsSnapshot, accounts: ProviderAcc
61
64
  status: account?.status ?? null
62
65
  };
63
66
  });
67
+
64
68
  const unread = known
65
69
  .filter((account) => account.account.enabled !== false && !read.some((row) => row.id === account.id))
66
70
  .map((account): LimitAccount => ({
@@ -71,7 +75,9 @@ export function limitGroups(snapshot: UsageLimitsSnapshot, accounts: ProviderAcc
71
75
  entry: null,
72
76
  status: account.status
73
77
  }));
78
+
74
79
  const rows = [...read, ...unread];
80
+
75
81
  return rows.length === 0 ? [] : [{ kind, accounts: rows }];
76
82
  });
77
83
  }
@@ -88,11 +94,13 @@ export function isSignedOut(account: LimitAccount): boolean {
88
94
  /* The window that resets first from now, the one reset a line per window has no room to repeat. */
89
95
  export function nextReset(windows: readonly UsageWindow[], now: number): (UsageWindow & { resetsAt: number }) | null {
90
96
  let next: (UsageWindow & { resetsAt: number }) | null = null;
97
+
91
98
  for (const window of windows) {
92
99
  if (window.resetsAt !== null && window.resetsAt > now && (next === null || window.resetsAt < next.resetsAt)) {
93
100
  next = { ...window, resetsAt: window.resetsAt };
94
101
  }
95
102
  }
103
+
96
104
  return next;
97
105
  }
98
106
 
@@ -100,6 +108,7 @@ export function nextReset(windows: readonly UsageWindow[], now: number): (UsageW
100
108
  export function paceText(window: UsageWindow, now: number): string {
101
109
  const percent = Math.round(window.used * 100);
102
110
  const pace = evenPace(window, now);
111
+
103
112
  return pace === null
104
113
  ? i18next.t('agent-usage:limits.used', { percent })
105
114
  : i18next.t(`agent-usage:limits.pace.${pace.standing}`, { percent, pace: pace.percent });
@@ -110,8 +119,10 @@ export function checkedLabel(entry: UsageLimitsProvider, now: number): string |
110
119
  if (entry.checkedAt <= 0) {
111
120
  return null;
112
121
  }
122
+
113
123
  const past = now - entry.checkedAt;
114
124
  const when = past < MINUTE ? i18next.t('agent-usage:limits.checked.now') : i18next.t('agent-usage:limits.checked.ago', { duration: formatDuration(past) });
125
+
115
126
  return entry.source === 'event' ? i18next.t('agent-usage:limits.checked.fromTurn', { when }) : when;
116
127
  }
117
128
 
@@ -120,13 +131,16 @@ export function explain(provider: UsageLimitsProvider): string | null {
120
131
  if (provider.unavailable === null) {
121
132
  return provider.windows.length === 0 ? i18next.t('agent-usage:limits.none') : null;
122
133
  }
134
+
123
135
  if (provider.unavailable.reason === 'not-installed') {
124
136
  return i18next.t('agent-usage:limits.notInstalled');
125
137
  }
138
+
126
139
  if (provider.unavailable.reason === 'no-subscription') {
127
140
  // A provider that knows why it has no plan says so; the rest are accounts on an API key.
128
141
  return provider.unavailable.message ?? i18next.t('agent-usage:limits.noSubscription');
129
142
  }
143
+
130
144
  return provider.unavailable.message ?? i18next.t('agent-usage:limits.unreachable');
131
145
  }
132
146
 
@@ -1,8 +1,9 @@
1
1
  import { useEffect } from 'react';
2
2
  import type { UsageLimitsSnapshot } from '@adecore/agent-contracts';
3
3
  import { useChatScope, type ChatScope } from '../scope';
4
- import { useProviderAccountsStore } from '../state/provider-accounts';
4
+ import { useProviderAccounts } from '../state/provider-accounts';
5
5
  import { useUsage, useUsageStore } from '../state/usage';
6
+ import { whenOpen } from '../transport';
6
7
  import { limitGroups, type LimitGroup } from './limit-groups';
7
8
 
8
9
  interface Hold {
@@ -18,21 +19,16 @@ const holds = new Map<string, Hold>();
18
19
  function hold(scope: ChatScope): Hold {
19
20
  const link = scope.transport;
20
21
  const scopeId = scope.id;
21
- const ask = (): void => {
22
+
23
+ const offStatus = whenOpen(link, () => {
22
24
  link.request('usage.limits', {})
23
25
  .then((snapshot) => useUsageStore.getState().setLimits(scopeId, snapshot))
24
26
  .catch(() => undefined);
25
- };
26
- if (link.status === 'open') {
27
- ask();
28
- }
27
+ });
28
+
29
29
  // A running turn reports its own numbers, which is what keeps a bar moving between reads.
30
30
  const offChanged = link.on('usage.limitsChanged', (snapshot) => useUsageStore.getState().setLimits(scopeId, snapshot));
31
- const offStatus = link.subscribeStatus((status) => {
32
- if (status === 'open') {
33
- ask();
34
- }
35
- });
31
+
36
32
  return {
37
33
  readers: 0,
38
34
  release: () => {
@@ -51,8 +47,10 @@ export function useUsageLimits(): UsageLimitsSnapshot | null {
51
47
  const held = holds.get(scope.id) ?? hold(scope);
52
48
  held.readers += 1;
53
49
  holds.set(scope.id, held);
50
+
54
51
  return () => {
55
52
  held.readers -= 1;
53
+
56
54
  if (held.readers === 0) {
57
55
  held.release();
58
56
  holds.delete(scope.id);
@@ -65,7 +63,7 @@ export function useUsageLimits(): UsageLimitsSnapshot | null {
65
63
 
66
64
  /* The snapshot per CLI and account, with the accounts of the same host it is read from. */
67
65
  export function useLimitGroups(limits: UsageLimitsSnapshot | null): LimitGroup[] {
68
- const { id } = useChatScope();
69
- const accounts = useProviderAccountsStore((s) => s.byScope[id]?.accounts ?? null);
66
+ const accounts = useProviderAccounts((row) => row.accounts);
67
+
70
68
  return limits === null ? [] : limitGroups(limits, accounts);
71
69
  }
@@ -5,5 +5,6 @@ import { moneyFormat } from './format';
5
5
  export function useMoney(): (usd: number) => string {
6
6
  const currency = useUsage((s) => s.currency);
7
7
  const rate = useUsage((s) => s.summary?.rate ?? null);
8
+
8
9
  return moneyFormat(currency, rate);
9
10
  }
@@ -1,5 +1,5 @@
1
1
  import type { UsageProvider, UsageSummaryResult, UsageTotals } from '@adecore/agent-contracts';
2
- import type { UsageMetric } from '../state/usage';
2
+ import { dayOf, type UsageMetric } from '../state/usage';
3
3
  import { EMPTY_TOTALS, USAGE_PROVIDERS, addTotals, totalTokensOf } from '@adecore/agent-contracts';
4
4
 
5
5
  /* One bar of the chart, what each provider put in this slot, and the height of the stack. */
@@ -21,10 +21,12 @@ export function niceScale(max: number, ticks = 4): { max: number; step: number }
21
21
  if (!Number.isFinite(max) || max <= 0) {
22
22
  return { max: 1, step: 1 };
23
23
  }
24
+
24
25
  const rough = max / ticks;
25
26
  const magnitude = 10 ** Math.floor(Math.log10(rough));
26
27
  const normalized = rough / magnitude;
27
28
  const step = (normalized <= 1 ? 1 : normalized <= 2 ? 2 : normalized <= 5 ? 5 : 10) * magnitude;
29
+
28
30
  return { max: Math.ceil(max / step) * step, step };
29
31
  }
30
32
 
@@ -38,26 +40,25 @@ export interface DerivedUsage {
38
40
  cacheSavingsUsd: number;
39
41
  }
40
42
 
41
- function pad(value: number): string {
42
- return String(value).padStart(2, '0');
43
- }
44
-
45
43
  /*
46
44
  * Every slot the period has, whether anything happened in it or not: an empty day keeps its place on
47
45
  * the axis, so a gap in the work reads as a gap and not as a shorter week.
48
46
  */
49
47
  export function enumerateSlots(summary: Pick<UsageSummaryResult, 'from' | 'to' | 'resolution'>): string[] {
50
48
  if (summary.resolution === 'hour') {
51
- return Array.from({ length: 24 }, (_, hour) => `${summary.to}T${pad(hour)}`);
49
+ return Array.from({ length: 24 }, (_, hour) => `${summary.to}T${String(hour).padStart(2, '0')}`);
52
50
  }
51
+
53
52
  const slots: string[] = [];
54
53
  const at = new Date(`${summary.from}T00:00:00`);
55
54
  const end = new Date(`${summary.to}T00:00:00`);
55
+
56
56
  // A calendar loop rather than adding milliseconds, since a day is 23 or 25 hours long twice a year.
57
57
  while (at <= end && slots.length < 400) {
58
- slots.push(`${at.getFullYear()}-${pad(at.getMonth() + 1)}-${pad(at.getDate())}`);
58
+ slots.push(dayOf(at));
59
59
  at.setDate(at.getDate() + 1);
60
60
  }
61
+
61
62
  return slots;
62
63
  }
63
64
 
@@ -72,9 +73,11 @@ export function labelEveryFor(slots: number): number {
72
73
  */
73
74
  export function deriveUsage(summary: UsageSummaryResult, metric: UsageMetric): DerivedUsage {
74
75
  const bySlot = new Map<string, ChartSlot>();
76
+
75
77
  for (const slot of enumerateSlots(summary)) {
76
78
  bySlot.set(slot, { slot, byProvider: {}, total: 0 });
77
79
  }
80
+
78
81
  const byProvider = new Map<UsageProvider, ProviderTotal>();
79
82
  let totals = EMPTY_TOTALS;
80
83
  let costUsd = 0;
@@ -83,10 +86,12 @@ export function deriveUsage(summary: UsageSummaryResult, metric: UsageMetric): D
83
86
  for (const bucket of summary.buckets) {
84
87
  const value = metric === 'cost' ? (bucket.costUsd ?? 0) : totalTokensOf(bucket.totals);
85
88
  const slot = bySlot.get(bucket.slot);
89
+
86
90
  if (slot !== undefined) {
87
91
  slot.byProvider[bucket.provider] = (slot.byProvider[bucket.provider] ?? 0) + value;
88
92
  slot.total += value;
89
93
  }
94
+
90
95
  const provider = byProvider.get(bucket.provider) ?? { provider: bucket.provider, costUsd: 0, totals: EMPTY_TOTALS };
91
96
  provider.costUsd += bucket.costUsd ?? 0;
92
97
  provider.totals = addTotals(provider.totals, bucket.totals);
@@ -97,6 +102,7 @@ export function deriveUsage(summary: UsageSummaryResult, metric: UsageMetric): D
97
102
  }
98
103
 
99
104
  const providers = [...byProvider.values()].sort((a, b) => b.costUsd - a.costUsd);
105
+
100
106
  return {
101
107
  slots: [...bySlot.values()],
102
108
  providers,
@@ -122,6 +128,7 @@ export interface UsageDay {
122
128
  */
123
129
  export function deriveDays(summary: UsageSummaryResult): UsageDay[] {
124
130
  const rows = new Map<string, UsageDay>();
131
+
125
132
  for (const bucket of summary.buckets) {
126
133
  const day = bucket.slot.slice(0, 10);
127
134
  const row = rows.get(day) ?? { slot: day, costByProvider: {}, costUsd: 0, tokens: 0 };
@@ -131,5 +138,6 @@ export function deriveDays(summary: UsageSummaryResult): UsageDay[] {
131
138
  row.tokens += totalTokensOf(bucket.totals);
132
139
  rows.set(day, row);
133
140
  }
141
+
134
142
  return [...rows.values()].sort((a, b) => b.slot.localeCompare(a.slot));
135
143
  }