@ruimte/agents-react 0.0.0 → 0.7.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 (312) hide show
  1. package/LICENSE +110 -0
  2. package/README.md +142 -1
  3. package/dist/agents/AccountDot.d.ts +4 -0
  4. package/dist/agents/AccountDot.js +8 -0
  5. package/dist/agents/AgentIcon.d.ts +6 -0
  6. package/dist/agents/AgentIcon.js +20 -0
  7. package/dist/agents/ProviderLogo.d.ts +8 -0
  8. package/dist/agents/ProviderLogo.js +8 -0
  9. package/dist/agents/account-limits.d.ts +7 -0
  10. package/dist/agents/account-limits.js +26 -0
  11. package/dist/agents/accounts.d.ts +22 -0
  12. package/dist/agents/accounts.js +103 -0
  13. package/dist/agents/model-name.d.ts +6 -0
  14. package/dist/agents/model-name.js +44 -0
  15. package/dist/agents/provider-paths.d.ts +2 -0
  16. package/dist/agents/provider-paths.js +8 -0
  17. package/dist/agents/status-look.d.ts +13 -0
  18. package/dist/agents/status-look.js +22 -0
  19. package/dist/chat/account-choice.d.ts +11 -0
  20. package/dist/chat/account-choice.js +30 -0
  21. package/dist/chat/actions.d.ts +6 -0
  22. package/dist/chat/actions.js +64 -0
  23. package/dist/chat/activity.d.ts +5 -0
  24. package/dist/chat/activity.js +4 -0
  25. package/dist/chat/attachments.d.ts +23 -0
  26. package/dist/chat/attachments.js +70 -0
  27. package/dist/chat/bookmarks.d.ts +14 -0
  28. package/dist/chat/bookmarks.js +73 -0
  29. package/dist/chat/chat-client.d.ts +53 -0
  30. package/dist/chat/chat-client.js +232 -0
  31. package/dist/chat/chat-references.d.ts +5 -0
  32. package/dist/chat/chat-references.js +5 -0
  33. package/dist/chat/drafts.d.ts +29 -0
  34. package/dist/chat/drafts.js +104 -0
  35. package/dist/chat/forks.d.ts +2 -0
  36. package/dist/chat/forks.js +26 -0
  37. package/dist/chat/guards.d.ts +12 -0
  38. package/dist/chat/guards.js +63 -0
  39. package/dist/chat/logic/answers.d.ts +3 -0
  40. package/dist/chat/logic/answers.js +14 -0
  41. package/dist/chat/logic/bookmarks.d.ts +5 -0
  42. package/dist/chat/logic/bookmarks.js +31 -0
  43. package/dist/chat/logic/context-usage.d.ts +12 -0
  44. package/dist/chat/logic/context-usage.js +20 -0
  45. package/dist/chat/logic/fork.d.ts +44 -0
  46. package/dist/chat/logic/fork.js +138 -0
  47. package/dist/chat/logic/handback.d.ts +2 -0
  48. package/dist/chat/logic/handback.js +8 -0
  49. package/dist/chat/logic/json.d.ts +1 -0
  50. package/dist/chat/logic/json.js +2 -0
  51. package/dist/chat/logic/limit.d.ts +7 -0
  52. package/dist/chat/logic/limit.js +34 -0
  53. package/dist/chat/logic/resume-compaction.d.ts +14 -0
  54. package/dist/chat/logic/resume-compaction.js +47 -0
  55. package/dist/chat/logic/scrubber.d.ts +50 -0
  56. package/dist/chat/logic/scrubber.js +216 -0
  57. package/dist/chat/logic/timeline-copy.d.ts +4 -0
  58. package/dist/chat/logic/timeline-copy.js +51 -0
  59. package/dist/chat/logic/timeline-target.d.ts +12 -0
  60. package/dist/chat/logic/timeline-target.js +34 -0
  61. package/dist/chat/logic/timeline.d.ts +110 -0
  62. package/dist/chat/logic/timeline.js +339 -0
  63. package/dist/chat/logic/tool-catalog.d.ts +10 -0
  64. package/dist/chat/logic/tool-catalog.js +40 -0
  65. package/dist/chat/logic/tools.d.ts +15 -0
  66. package/dist/chat/logic/tools.js +79 -0
  67. package/dist/chat/logic/workflow.d.ts +14 -0
  68. package/dist/chat/logic/workflow.js +47 -0
  69. package/dist/chat/mentions.d.ts +46 -0
  70. package/dist/chat/mentions.js +128 -0
  71. package/dist/chat/persisted-json.d.ts +5 -0
  72. package/dist/chat/persisted-json.js +24 -0
  73. package/dist/chat/preferences.d.ts +23 -0
  74. package/dist/chat/preferences.js +110 -0
  75. package/dist/chat/quote.d.ts +2 -0
  76. package/dist/chat/quote.js +25 -0
  77. package/dist/chat/recent-messages.d.ts +17 -0
  78. package/dist/chat/recent-messages.js +28 -0
  79. package/dist/chat/resume-compaction-dismissals.d.ts +3 -0
  80. package/dist/chat/resume-compaction-dismissals.js +14 -0
  81. package/dist/chat/runtime-modes.d.ts +4 -0
  82. package/dist/chat/runtime-modes.js +11 -0
  83. package/dist/chat/stash.d.ts +24 -0
  84. package/dist/chat/stash.js +88 -0
  85. package/dist/chat/subagent-conversation.d.ts +38 -0
  86. package/dist/chat/subagent-conversation.js +150 -0
  87. package/dist/chat/subagent-list.d.ts +16 -0
  88. package/dist/chat/subagent-list.js +76 -0
  89. package/dist/chat/subagent-support.d.ts +6 -0
  90. package/dist/chat/subagent-support.js +14 -0
  91. package/dist/chat/subagent-view.d.ts +33 -0
  92. package/dist/chat/subagent-view.js +65 -0
  93. package/dist/chat/thumbnails.d.ts +10 -0
  94. package/dist/chat/thumbnails.js +87 -0
  95. package/dist/chat/timeline-scroll.d.ts +16 -0
  96. package/dist/chat/timeline-scroll.js +100 -0
  97. package/dist/chat/ui/AccountPill.d.ts +3 -0
  98. package/dist/chat/ui/AccountPill.js +24 -0
  99. package/dist/chat/ui/BookmarkMarker.d.ts +8 -0
  100. package/dist/chat/ui/BookmarkMarker.js +59 -0
  101. package/dist/chat/ui/BookmarkSubmenu.d.ts +3 -0
  102. package/dist/chat/ui/BookmarkSubmenu.js +44 -0
  103. package/dist/chat/ui/ChatActivity.d.ts +3 -0
  104. package/dist/chat/ui/ChatActivity.js +87 -0
  105. package/dist/chat/ui/ChatReferenceChip.d.ts +4 -0
  106. package/dist/chat/ui/ChatReferenceChip.js +17 -0
  107. package/dist/chat/ui/CodeBlock.d.ts +4 -0
  108. package/dist/chat/ui/CodeBlock.js +130 -0
  109. package/dist/chat/ui/Composer.d.ts +14 -0
  110. package/dist/chat/ui/Composer.js +821 -0
  111. package/dist/chat/ui/ComposerInput.d.ts +28 -0
  112. package/dist/chat/ui/ComposerInput.js +118 -0
  113. package/dist/chat/ui/DiffPool.d.ts +5 -0
  114. package/dist/chat/ui/DiffPool.js +26 -0
  115. package/dist/chat/ui/EditDiff.d.ts +4 -0
  116. package/dist/chat/ui/EditDiff.js +21 -0
  117. package/dist/chat/ui/FadingWords.d.ts +3 -0
  118. package/dist/chat/ui/FadingWords.js +5 -0
  119. package/dist/chat/ui/ImageView.d.ts +6 -0
  120. package/dist/chat/ui/ImageView.js +85 -0
  121. package/dist/chat/ui/LimitState.d.ts +8 -0
  122. package/dist/chat/ui/LimitState.js +117 -0
  123. package/dist/chat/ui/Markdown.d.ts +18 -0
  124. package/dist/chat/ui/Markdown.js +158 -0
  125. package/dist/chat/ui/MessageActions.d.ts +9 -0
  126. package/dist/chat/ui/MessageActions.js +56 -0
  127. package/dist/chat/ui/Pickers.d.ts +15 -0
  128. package/dist/chat/ui/Pickers.js +110 -0
  129. package/dist/chat/ui/PromptComposer.d.ts +13 -0
  130. package/dist/chat/ui/PromptComposer.js +48 -0
  131. package/dist/chat/ui/QuoteButton.d.ts +7 -0
  132. package/dist/chat/ui/QuoteButton.js +76 -0
  133. package/dist/chat/ui/ResumeCompactionDock.d.ts +5 -0
  134. package/dist/chat/ui/ResumeCompactionDock.js +15 -0
  135. package/dist/chat/ui/RunSettings.d.ts +25 -0
  136. package/dist/chat/ui/RunSettings.js +148 -0
  137. package/dist/chat/ui/Scrubber.d.ts +17 -0
  138. package/dist/chat/ui/Scrubber.js +121 -0
  139. package/dist/chat/ui/SubagentControls.d.ts +11 -0
  140. package/dist/chat/ui/SubagentControls.js +37 -0
  141. package/dist/chat/ui/SubagentStopButton.d.ts +7 -0
  142. package/dist/chat/ui/SubagentStopButton.js +36 -0
  143. package/dist/chat/ui/SubagentTimeline.d.ts +4 -0
  144. package/dist/chat/ui/SubagentTimeline.js +92 -0
  145. package/dist/chat/ui/Timeline.d.ts +5 -0
  146. package/dist/chat/ui/Timeline.js +318 -0
  147. package/dist/chat/ui/TimelineMenu.d.ts +12 -0
  148. package/dist/chat/ui/TimelineMenu.js +61 -0
  149. package/dist/chat/ui/UnifiedDiff.d.ts +8 -0
  150. package/dist/chat/ui/UnifiedDiff.js +51 -0
  151. package/dist/chat/ui/UploadThumb.d.ts +4 -0
  152. package/dist/chat/ui/UploadThumb.js +11 -0
  153. package/dist/chat/ui/chips.d.ts +4 -0
  154. package/dist/chat/ui/chips.js +11 -0
  155. package/dist/chat/ui/code-lines.d.ts +18 -0
  156. package/dist/chat/ui/code-lines.js +46 -0
  157. package/dist/chat/ui/code-streaming.d.ts +1 -0
  158. package/dist/chat/ui/code-streaming.js +3 -0
  159. package/dist/chat/ui/code-theme.d.ts +3 -0
  160. package/dist/chat/ui/code-theme.js +10 -0
  161. package/dist/chat/ui/composer/chips.d.ts +6 -0
  162. package/dist/chat/ui/composer/chips.js +29 -0
  163. package/dist/chat/ui/composer/editor.d.ts +11 -0
  164. package/dist/chat/ui/composer/editor.js +94 -0
  165. package/dist/chat/ui/composer/keys.d.ts +30 -0
  166. package/dist/chat/ui/composer/keys.js +98 -0
  167. package/dist/chat/ui/diff-theme.d.ts +2 -0
  168. package/dist/chat/ui/diff-theme.js +20 -0
  169. package/dist/chat/ui/file-links.d.ts +5 -0
  170. package/dist/chat/ui/file-links.js +18 -0
  171. package/dist/chat/ui/find-reveal.d.ts +7 -0
  172. package/dist/chat/ui/find-reveal.js +13 -0
  173. package/dist/chat/ui/icons.d.ts +4 -0
  174. package/dist/chat/ui/icons.js +20 -0
  175. package/dist/chat/ui/markdown-blocks.d.ts +6 -0
  176. package/dist/chat/ui/markdown-blocks.js +101 -0
  177. package/dist/chat/ui/quote-selection.d.ts +3 -0
  178. package/dist/chat/ui/quote-selection.js +44 -0
  179. package/dist/chat/ui/rehype-chips.d.ts +23 -0
  180. package/dist/chat/ui/rehype-chips.js +28 -0
  181. package/dist/chat/ui/rehype-fade.d.ts +23 -0
  182. package/dist/chat/ui/rehype-fade.js +30 -0
  183. package/dist/chat/ui/remark-html-as-text.d.ts +8 -0
  184. package/dist/chat/ui/remark-html-as-text.js +24 -0
  185. package/dist/chat/ui/reveal.d.ts +13 -0
  186. package/dist/chat/ui/reveal.js +112 -0
  187. package/dist/chat/ui/rows/ForksRow.d.ts +4 -0
  188. package/dist/chat/ui/rows/ForksRow.js +40 -0
  189. package/dist/chat/ui/rows/MessageRows.d.ts +36 -0
  190. package/dist/chat/ui/rows/MessageRows.js +190 -0
  191. package/dist/chat/ui/rows/Rows.d.ts +12 -0
  192. package/dist/chat/ui/rows/Rows.js +49 -0
  193. package/dist/chat/ui/rows/SubagentRow.d.ts +15 -0
  194. package/dist/chat/ui/rows/SubagentRow.js +98 -0
  195. package/dist/chat/ui/rows/WorkRows.d.ts +46 -0
  196. package/dist/chat/ui/rows/WorkRows.js +143 -0
  197. package/dist/chat/ui/rows/WorkflowRow.d.ts +7 -0
  198. package/dist/chat/ui/rows/WorkflowRow.js +37 -0
  199. package/dist/chat/ui/rows/row-rhythm.d.ts +3 -0
  200. package/dist/chat/ui/rows/row-rhythm.js +14 -0
  201. package/dist/chat/ui/useToggleSet.d.ts +6 -0
  202. package/dist/chat/ui/useToggleSet.js +25 -0
  203. package/dist/host.d.ts +167 -0
  204. package/dist/host.js +41 -0
  205. package/dist/lazy.d.ts +5 -0
  206. package/dist/lazy.js +35 -0
  207. package/dist/locales/en/agent-chat.json +417 -0
  208. package/dist/locales/en/agent-prompts.json +42 -0
  209. package/dist/locales/en/agent-providers.json +131 -0
  210. package/dist/locales/en/agent-usage.json +131 -0
  211. package/dist/locales/nl/agent-chat.json +417 -0
  212. package/dist/locales/nl/agent-prompts.json +42 -0
  213. package/dist/locales/nl/agent-providers.json +131 -0
  214. package/dist/locales/nl/agent-usage.json +131 -0
  215. package/dist/locales.d.ts +5 -0
  216. package/dist/locales.js +18 -0
  217. package/dist/mounted-registry.d.ts +7 -0
  218. package/dist/mounted-registry.js +39 -0
  219. package/dist/port-transport.d.ts +6 -0
  220. package/dist/port-transport.js +120 -0
  221. package/dist/prompts/logic/focus.d.ts +1 -0
  222. package/dist/prompts/logic/focus.js +17 -0
  223. package/dist/prompts/logic/keys.d.ts +42 -0
  224. package/dist/prompts/logic/keys.js +84 -0
  225. package/dist/prompts/logic/prompts.d.ts +34 -0
  226. package/dist/prompts/logic/prompts.fixtures.d.ts +5 -0
  227. package/dist/prompts/logic/prompts.fixtures.js +73 -0
  228. package/dist/prompts/logic/prompts.js +22 -0
  229. package/dist/prompts/logic/subjects.d.ts +49 -0
  230. package/dist/prompts/logic/subjects.js +56 -0
  231. package/dist/prompts/logic/useFocusAfterAnswer.d.ts +6 -0
  232. package/dist/prompts/logic/useFocusAfterAnswer.js +28 -0
  233. package/dist/prompts/logic/usePromptSession.d.ts +24 -0
  234. package/dist/prompts/logic/usePromptSession.js +87 -0
  235. package/dist/prompts/ui/ApprovalBody.d.ts +13 -0
  236. package/dist/prompts/ui/ApprovalBody.js +35 -0
  237. package/dist/prompts/ui/PromptActions.d.ts +26 -0
  238. package/dist/prompts/ui/PromptActions.js +19 -0
  239. package/dist/prompts/ui/PromptCard.d.ts +16 -0
  240. package/dist/prompts/ui/PromptCard.js +50 -0
  241. package/dist/prompts/ui/PromptView.d.ts +2 -0
  242. package/dist/prompts/ui/PromptView.js +47 -0
  243. package/dist/prompts/ui/QuestionBody.d.ts +9 -0
  244. package/dist/prompts/ui/QuestionBody.js +87 -0
  245. package/dist/providers/AccountColors.d.ts +5 -0
  246. package/dist/providers/AccountColors.js +8 -0
  247. package/dist/providers/AccountDetail.d.ts +12 -0
  248. package/dist/providers/AccountDetail.js +76 -0
  249. package/dist/providers/AccountVariables.d.ts +8 -0
  250. package/dist/providers/AccountVariables.js +48 -0
  251. package/dist/providers/AddAccountForm.d.ts +10 -0
  252. package/dist/providers/AddAccountForm.js +58 -0
  253. package/dist/providers/CliDetail.d.ts +11 -0
  254. package/dist/providers/CliDetail.js +62 -0
  255. package/dist/providers/ProvidersPane.d.ts +8 -0
  256. package/dist/providers/ProvidersPane.js +90 -0
  257. package/dist/providers/account-actions.d.ts +7 -0
  258. package/dist/providers/account-actions.js +53 -0
  259. package/dist/providers/parts.d.ts +9 -0
  260. package/dist/providers/parts.js +18 -0
  261. package/dist/providers/provider-abilities.d.ts +2 -0
  262. package/dist/providers/provider-abilities.js +7 -0
  263. package/dist/providers/variables.d.ts +13 -0
  264. package/dist/providers/variables.js +53 -0
  265. package/dist/scope.d.ts +11 -0
  266. package/dist/scope.js +10 -0
  267. package/dist/settings/sections.d.ts +13 -0
  268. package/dist/settings/sections.js +30 -0
  269. package/dist/state/chats.d.ts +32 -0
  270. package/dist/state/chats.js +124 -0
  271. package/dist/state/provider-accounts.d.ts +17 -0
  272. package/dist/state/provider-accounts.js +63 -0
  273. package/dist/state/providers.d.ts +17 -0
  274. package/dist/state/providers.js +29 -0
  275. package/dist/state/usage.d.ts +41 -0
  276. package/dist/state/usage.js +100 -0
  277. package/dist/theme.css +186 -0
  278. package/dist/transport.d.ts +24 -0
  279. package/dist/transport.js +16 -0
  280. package/dist/ui/PlainTextarea.d.ts +2 -0
  281. package/dist/ui/PlainTextarea.js +5 -0
  282. package/dist/usage/LimitsList.d.ts +18 -0
  283. package/dist/usage/LimitsList.js +69 -0
  284. package/dist/usage/UsageBreakdown.d.ts +9 -0
  285. package/dist/usage/UsageBreakdown.js +69 -0
  286. package/dist/usage/UsageChart.d.ts +10 -0
  287. package/dist/usage/UsageChart.js +49 -0
  288. package/dist/usage/UsageDialog.d.ts +6 -0
  289. package/dist/usage/UsageDialog.js +15 -0
  290. package/dist/usage/UsageLimits.d.ts +1 -0
  291. package/dist/usage/UsageLimits.js +62 -0
  292. package/dist/usage/UsageLimitsCard.d.ts +4 -0
  293. package/dist/usage/UsageLimitsCard.js +27 -0
  294. package/dist/usage/UsagePage.d.ts +7 -0
  295. package/dist/usage/UsagePage.js +161 -0
  296. package/dist/usage/UsagePane.d.ts +3 -0
  297. package/dist/usage/UsagePane.js +24 -0
  298. package/dist/usage/UsageSummary.d.ts +12 -0
  299. package/dist/usage/UsageSummary.js +15 -0
  300. package/dist/usage/UsageTiles.d.ts +7 -0
  301. package/dist/usage/UsageTiles.js +19 -0
  302. package/dist/usage/format.d.ts +14 -0
  303. package/dist/usage/format.js +50 -0
  304. package/dist/usage/limit-groups.d.ts +22 -0
  305. package/dist/usage/limit-groups.js +78 -0
  306. package/dist/usage/limits.d.ts +4 -0
  307. package/dist/usage/limits.js +58 -0
  308. package/dist/usage/money.d.ts +1 -0
  309. package/dist/usage/money.js +13 -0
  310. package/dist/usage/summary.d.ts +34 -0
  311. package/dist/usage/summary.js +90 -0
  312. package/package.json +639 -2
package/dist/theme.css ADDED
@@ -0,0 +1,186 @@
1
+ /*
2
+ * The chat of @ruimte/agents-react, its providers and its usage: the tokens only they read and the
3
+ * rules they draw with. Imported right after `@ruimte/ui/theme.css`; the app scans the package with
4
+ * `@source` and loads `@tailwindcss/typography`, which the markdown of a thread builds on.
5
+ */
6
+ :root, [data-theme="light"] {
7
+ /* Skills read as a second kind of token next to a file mention, so they get their own hue. */
8
+ --skill: #9333ea;
9
+ --skill-soft: #f7eeff;
10
+ /* One color per agent CLI, so a bar, a dot and a legend row all name the same provider. Apple's
11
+ system orange and teal; Gemini and Copilot are reserved for the day their transcripts are read. */
12
+ --chart-claude: #ff9500;
13
+ --chart-codex: #30b0c7;
14
+ --chart-gemini: #3b82f6;
15
+ --chart-copilot: #64748b;
16
+ /* What a chat's context holds, one color per part; the rest is the system's and stays gray. */
17
+ --chart-context-tools: #4d7ce6;
18
+ --chart-context-files: #9a63e0;
19
+ --chart-context-conversation: #2f9e5a;
20
+ --chart-context-system: var(--text-faint);
21
+ }
22
+
23
+ [data-theme="dark"] {
24
+ --skill: #c084fc;
25
+ --skill-soft: #2f2350;
26
+ --chart-claude: #ff9f0a;
27
+ --chart-codex: #40c8e0;
28
+ --chart-gemini: #60a5fa;
29
+ --chart-copilot: #94a3b8;
30
+ --chart-context-tools: #8aa7f5;
31
+ --chart-context-files: #b98af3;
32
+ --chart-context-conversation: #6dd08b;
33
+ --chart-context-system: var(--text-faint);
34
+ }
35
+
36
+ @theme inline {
37
+ --color-skill: var(--skill);
38
+ --color-skill-soft: var(--skill-soft);
39
+ --color-chart-claude: var(--chart-claude);
40
+ --color-chart-codex: var(--chart-codex);
41
+ --color-chart-gemini: var(--chart-gemini);
42
+ --color-chart-copilot: var(--chart-copilot);
43
+ --color-chart-context-tools: var(--chart-context-tools);
44
+ --color-chart-context-files: var(--chart-context-files);
45
+ --color-chart-context-conversation: var(--chart-context-conversation);
46
+ --color-chart-context-system: var(--chart-context-system);
47
+ }
48
+
49
+ @layer components {
50
+ /* ---------------------------------------------------------------- Chat */
51
+
52
+ /* Text in a chat is there to be read and copied, so the thread selects even though the app does
53
+ not. Its chrome stays out of it: a drag on a fold header or a chip would select the row.
54
+ The pretty wrap from `body` would be recomputed on the growing paragraph every frame a reply
55
+ streams, so the thread wraps the plain way.
56
+ The gaps are the air a thread needs to read as turns: the step from a question to its answer,
57
+ and the wider step from the end of one turn to the next question. A view of its own gets more
58
+ of both, the way its type does. The block gap is the step inside a turn, where a run of tool
59
+ lines meets prose or a card. */
60
+ .chat-thread { user-select: text; text-wrap-style: auto; --chat-answer-gap: 16px; --chat-turn-gap: 24px; --chat-block-gap: 12px; }
61
+ .chat-column .chat-thread { --chat-answer-gap: 20px; --chat-turn-gap: 32px; --chat-block-gap: 16px; }
62
+ .chat-thread button, .chat-thread kbd { user-select: none; }
63
+
64
+ /*
65
+ * A chat that is a view of its own reads in a column. The scroller keeps the full width of the
66
+ * main column, so its scrollbar sits at that column's right edge and nothing jumps when the
67
+ * thread grows past a screenful; only the rows and the composer inside it are centered.
68
+ */
69
+ .chat-column .chat-column-content { margin-inline: auto; max-width: 768px; }
70
+ /* Wider by the input's inset (`.composer-input .cm-content` padding plus the card's border), so what is typed lines up with the thread. */
71
+ .chat-column .chat-composer-content { margin-inline: auto; max-width: calc(768px + 2 * 21px); }
72
+
73
+ /* A diff inside a chat tool block; the library paints its own colors inside a shadow root and
74
+ takes the face it draws them in through these variables. */
75
+ .chat-diff { font-size: var(--text-code); --diffs-font-size: var(--text-code); --diffs-font-family: var(--font-mono); }
76
+ .chat-diff + .chat-diff { border-top: 1px solid var(--border); }
77
+
78
+ /* Live tool rows shine once across; a paused animation costs nothing. */
79
+ .chat-live-text {
80
+ background: linear-gradient(90deg, var(--text-muted) 0%, var(--text) 50%, var(--text-muted) 100%);
81
+ background-size: 200% 100%;
82
+ -webkit-background-clip: text;
83
+ background-clip: text;
84
+ color: transparent;
85
+ animation: chat-shine 1.6s linear infinite;
86
+ }
87
+ @keyframes chat-shine { from { background-position: 200% 0; } to { background-position: -200% 0; } }
88
+
89
+ /* A word of a reply that just arrived. The duration is REVEAL_FADE_MS in chat/ui/reveal.ts, which
90
+ keeps the spans until it is over; ease-in-out, because ease-out jumps up and then trails. */
91
+ .chat-fade { animation: chat-fade 300ms ease-in-out both; }
92
+ /* A whole reply that was written out of sight. */
93
+ .chat-fade-whole { animation: chat-fade 220ms ease-out both; }
94
+ /* A block of a reply streamed a block at a time, faded in when it is inserted and never when a
95
+ finished thread opens, which carries no `data-arriving`. */
96
+ .chat-markdown[data-arriving] > * {
97
+ transition: opacity 300ms ease-in-out;
98
+ @starting-style { opacity: 0; }
99
+ }
100
+ @keyframes chat-fade { from { opacity: 0; } to { opacity: 1; } }
101
+ }
102
+
103
+ /*
104
+ * Markdown, outside every layer on purpose. The typography plugin registers `.prose` as a utility,
105
+ * and a later layer beats an earlier one whatever the specificity, so a rule that has to outrank
106
+ * prose cannot live in `components`. Unlayered styles win over every layer, which is the one
107
+ * place the app's tokens and scale are sure to land on top of the plugin's.
108
+ */
109
+ /*
110
+ * Markdown anywhere in the app: the chat timeline, a note node and the file preview all render
111
+ * the same `Markdown` component, which carries `prose prose-sm max-w-none text-sm` next to this
112
+ * class. Prose brings the rhythm; everything here is what the app decides instead of the plugin.
113
+ * The semantic tokens flip with the theme on their own, so the prose variables read from them
114
+ * and `prose-invert` is a second source of truth the app does not need. The size comes from the
115
+ * `text-sm` utility, which a node, a view and the file preview each move.
116
+ */
117
+ .prose.chat-markdown {
118
+ --tw-prose-body: var(--text);
119
+ --tw-prose-headings: var(--text);
120
+ --tw-prose-lead: var(--text-muted);
121
+ --tw-prose-links: var(--accent);
122
+ --tw-prose-bold: var(--text);
123
+ --tw-prose-counters: var(--text-faint);
124
+ --tw-prose-bullets: var(--text-faint);
125
+ --tw-prose-hr: var(--border);
126
+ --tw-prose-quotes: var(--text);
127
+ --tw-prose-quote-borders: var(--border);
128
+ --tw-prose-captions: var(--text-muted);
129
+ --tw-prose-kbd: var(--text);
130
+ --tw-prose-code: var(--text);
131
+ --tw-prose-pre-code: var(--text);
132
+ --tw-prose-pre-bg: var(--surface-sunken);
133
+ --tw-prose-th-borders: var(--border);
134
+ --tw-prose-td-borders: var(--border);
135
+ }
136
+ /* Headings stay on the app's scale, which is flatter than the one prose brings. */
137
+ .chat-markdown :is(h1, h2, h3, h4) { font-weight: 600; line-height: round(1.3em, 1px); }
138
+ .chat-markdown h1 { font-size: var(--text-lg); }
139
+ .chat-markdown h2 { font-size: var(--text-base); }
140
+ .chat-markdown :is(h3, h4) { font-size: var(--text-sm); }
141
+ .chat-markdown a { text-underline-offset: 2px; }
142
+ /* Code is the app's own: `--text-code` at every depth, and none of the quotes prose puts around
143
+ inline code, since the chip the component draws already says what it is. */
144
+ .chat-markdown code { font-size: var(--text-code); font-weight: 400; }
145
+ .chat-markdown code::before, .chat-markdown code::after { content: none; }
146
+ /* A checked box is the marker; prose would put a bullet next to it. */
147
+ .chat-markdown li:has(> input[type="checkbox"]) { list-style: none; }
148
+ .chat-markdown input[type="checkbox"] { margin-right: 6px; accent-color: var(--accent); }
149
+ /* A wide table scrolls in its own wrapper instead of stretching the column it sits in. The
150
+ wrapper is a scroll container, so the air around the table has to sit on it, not inside it. */
151
+ .chat-table { overflow-x: auto; margin-block: 2em; }
152
+ .chat-markdown table { margin: 0; border-collapse: collapse; font-size: var(--text-xs); }
153
+ .chat-markdown :is(th, td) { border: 1px solid var(--tw-prose-td-borders); padding: 4px 8px; text-align: left; }
154
+
155
+ /* A code block is shiki's own `<pre>`, which brings a background of its own to override. */
156
+ .chat-code { border-radius: 8px; border: 1px solid var(--border); background: var(--term-bg); overflow: auto; }
157
+ .chat-code pre { margin: 0; padding: round(0.625rem, 1px) round(0.75rem, 1px); background: transparent !important; font-family: var(--font-mono); font-size: var(--text-code); line-height: var(--text-code--line-height); }
158
+ .chat-code code { font-family: inherit; }
159
+
160
+ /* The chat composer is a CodeMirror editor, whose base theme lands in an unlayered style tag at
161
+ mount. These rules stay unlayered too, and carry one class more than the theme's selectors. */
162
+ .composer-input .cm-editor { max-height: 200px; }
163
+ .composer-input .cm-editor.cm-focused { outline: none; }
164
+ .composer-input .cm-editor .cm-scroller { overflow: auto; font-family: inherit; line-height: inherit; }
165
+ .composer-input .cm-editor .cm-scroller .cm-content { padding: 18px 14px 6px 20px; caret-color: var(--text); }
166
+ .composer-input .cm-scroller .cm-content .cm-line { padding: 0; }
167
+ .composer-input .cm-scroller .cm-content .cm-placeholder { color: var(--text-faint); }
168
+ .composer-hints, .composer-hints-extra { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 4px 8px; }
169
+ /* The joiner starts with its own comma, so it sits against the lead without the gap. */
170
+ .composer-hints-extra { margin-left: -8px; }
171
+ .composer-hints kbd { padding: 0 6px; border-radius: 5px; background: var(--surface-active); color: var(--text-muted); font-family: var(--font-mono); font-size: var(--text-code); line-height: var(--text-code--line-height); }
172
+ /* The widths of Tailwind's `@md` container variant, which the rest of the composer narrows at. */
173
+ @container composer (width < 28rem) {
174
+ .composer-input .cm-editor .cm-scroller .cm-content { padding: 14px 12px 4px 14px; }
175
+ .composer-hints-extra { display: none; }
176
+ }
177
+ /* Markdown stays source while you type: the markers stay put and dim, and what they wrap takes its look. */
178
+ .composer-input .cm-md-mark { color: var(--text-faint); }
179
+ .composer-input .cm-md-strong, .composer-input .cm-md-heading { font-weight: 600; }
180
+ .composer-input .cm-md-emphasis { font-style: italic; }
181
+ .composer-input .cm-md-strike { text-decoration: line-through; }
182
+ .composer-input .cm-md-link { color: var(--accent); text-decoration: underline; }
183
+ .composer-input .cm-md-code { padding: 1px 2px; border-radius: 4px; background: var(--surface-sunken); font-family: var(--font-mono); font-size: var(--text-code); }
184
+ .composer-input .cm-content .cm-line.cm-md-fence { padding: 0 8px; background: var(--surface-sunken); font-family: var(--font-mono); font-size: var(--text-code); }
185
+ .composer-input .cm-content .cm-line.cm-md-fence-start { padding-top: 4px; border-top-left-radius: 6px; border-top-right-radius: 6px; }
186
+ .composer-input .cm-content .cm-line.cm-md-fence-end { padding-bottom: 4px; border-bottom-left-radius: 6px; border-bottom-right-radius: 6px; }
@@ -0,0 +1,24 @@
1
+ import type { z } from 'zod';
2
+ import type { AGENT_EVENT_SCHEMAS, AGENT_REQUEST_SCHEMAS, AgentEventType, AgentRequestType } from '@ruimte/agent-contracts/protocol';
3
+ export type ChatRequestMap = {
4
+ [T in AgentRequestType]: {
5
+ payload: z.infer<(typeof AGENT_REQUEST_SCHEMAS)[T]['payload']>;
6
+ result: z.infer<(typeof AGENT_REQUEST_SCHEMAS)[T]['result']>;
7
+ };
8
+ };
9
+ export type ChatEventMap = {
10
+ [E in AgentEventType]: z.infer<(typeof AGENT_EVENT_SCHEMAS)[E]>;
11
+ };
12
+ export type ChatTransportStatus = 'connecting' | 'open' | 'closed';
13
+ export interface ChatTransport {
14
+ request<T extends AgentRequestType>(type: T, payload: ChatRequestMap[T]['payload']): Promise<ChatRequestMap[T]['result']>;
15
+ on<E extends AgentEventType>(event: E, handler: (payload: ChatEventMap[E]) => void): () => void;
16
+ readonly status: ChatTransportStatus;
17
+ subscribeStatus(handler: (status: ChatTransportStatus) => void): () => void;
18
+ }
19
+ export declare class ChatTransportError extends Error {
20
+ readonly code: string;
21
+ constructor(code: string, message: string);
22
+ }
23
+ export declare const errorCode: (error: unknown) => string | null;
24
+ export declare const isConnectionError: (error: unknown) => boolean;
@@ -0,0 +1,16 @@
1
+ /* A refused request always carries a code, so a caller branches on it instead of on the message. */
2
+ export class ChatTransportError extends Error {
3
+ code;
4
+ constructor(code, message) {
5
+ super(message);
6
+ this.name = 'ChatTransportError';
7
+ this.code = code;
8
+ }
9
+ }
10
+ /* The code of a refusal from any transport, this package's or the app's own: every one of them carries it the same way. */
11
+ export const errorCode = (error) => typeof error === 'object' && error !== null && 'code' in error && typeof error.code === 'string' ? error.code : null;
12
+ /* Whether a request failed because the link was down rather than because the host refused it; the reconnect asks again. */
13
+ export const isConnectionError = (error) => {
14
+ const code = errorCode(error);
15
+ return code === 'not-connected' || code === 'disconnected';
16
+ };
@@ -0,0 +1,2 @@
1
+ import type { TextareaProps } from '../host';
2
+ export declare function PlainTextarea({ buttonClassName: _buttonClassName, ...props }: TextareaProps): import("react").JSX.Element;
@@ -0,0 +1,5 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /* The field a written answer goes in when the app brings no dictation of its own. */
3
+ export function PlainTextarea({ buttonClassName: _buttonClassName, ...props }) {
4
+ return _jsx("textarea", { ...props });
5
+ }
@@ -0,0 +1,18 @@
1
+ import type { UsageLimitsSnapshot, UsageWindow } from '@ruimte/agent-contracts';
2
+ import { type LimitGroup } from './limit-groups';
3
+ export declare function WindowBar({ window, now, compact }: {
4
+ window: UsageWindow;
5
+ now: number;
6
+ compact: boolean;
7
+ }): import("react").JSX.Element;
8
+ interface LimitsListProps {
9
+ limits: UsageLimitsSnapshot;
10
+ now: number;
11
+ compact?: boolean;
12
+ }
13
+ export declare function LimitsList({ limits, now, compact }: LimitsListProps): import("react").JSX.Element;
14
+ export declare function AccountLimitsList({ groups, now }: {
15
+ groups: readonly LimitGroup[];
16
+ now: number;
17
+ }): import("react").JSX.Element;
18
+ export {};
@@ -0,0 +1,69 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Fragment } from 'react';
3
+ import clsx from 'clsx';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { limitsAccountId } from '../agents/account-limits';
6
+ import { AccountDot } from '../agents/AccountDot';
7
+ import { Tooltip } from '@ruimte/ui/Tooltip';
8
+ import { ProviderLogo } from '../agents/ProviderLogo';
9
+ import { formatClock, formatWeekdayClock, isSameDay } from '@ruimte/ui/format/datetime';
10
+ import { formatCountdown } from '@ruimte/ui/format/duration';
11
+ import { formatPercent } from '@ruimte/ui/format/number';
12
+ import { PROVIDER_COLORS, PROVIDER_LABELS } from './format';
13
+ import { accountNote, explain, isSignedOut, nextReset } from './limit-groups';
14
+ /* Red where a window is nearly spent, amber where it is worth knowing. A window with room to spare
15
+ is not news, so it takes the text color rather than a hue that competes with the two that are. */
16
+ const toneOf = (used) => (used >= 0.9 ? 'bg-status-error' : used >= 0.7 ? 'bg-status-needs-you' : 'bg-text');
17
+ /* A time of day when the reset is today, a weekday and a time when it is not. */
18
+ const resetAtLabel = (resetsAt, now) => (isSameDay(resetsAt, now) ? formatClock(resetsAt) : formatWeekdayClock(resetsAt));
19
+ /* How long the window still has, or nothing once it has run out. */
20
+ const resetInLabel = (resetsAt, now) => (resetsAt <= now ? null : formatCountdown(resetsAt - now));
21
+ /* How much of the window has run, or null when the provider named neither a reset nor a length. */
22
+ const elapsedShare = (window, now) => {
23
+ if (window.resetsAt === null || window.durationMs === null || window.durationMs <= 0) {
24
+ return null;
25
+ }
26
+ return Math.min(1, Math.max(0, 1 - (window.resetsAt - now) / window.durationMs));
27
+ };
28
+ export function WindowBar({ window, now, compact }) {
29
+ const { t } = useTranslation('agent-usage');
30
+ const percent = Math.round(window.used * 100);
31
+ const elapsed = elapsedShare(window, now);
32
+ // The bar fills as the quota is spent, so the mark beside it is how much of the window has run.
33
+ const mark = elapsed === null ? null : Math.round(elapsed * 100);
34
+ const resetsAt = window.resetsAt === null ? null : resetAtLabel(window.resetsAt, now);
35
+ const resetsIn = window.resetsAt === null ? null : resetInLabel(window.resetsAt, now);
36
+ const bar = (_jsxs("div", { role: "progressbar", "aria-label": t('limits.bar', { window: window.label, percent }), "aria-valuenow": percent, "aria-valuemin": 0, "aria-valuemax": 100, className: clsx('relative cursor-default', compact ? 'h-4' : 'h-6'), children: [_jsx("div", { className: clsx('absolute inset-x-0 rounded-full bg-surface-sunken', compact ? 'inset-y-1' : 'inset-y-1.5') }), _jsx("div", { className: clsx('absolute left-0 rounded-full', compact ? 'inset-y-1' : 'inset-y-1.5', toneOf(window.used)), style: { width: `${Math.min(100, percent)}%` } }), mark !== null && (_jsx("span", { "aria-hidden": true, className: clsx('absolute inset-y-0.5 w-px -translate-x-1/2', mark <= percent ? 'bg-surface' : 'bg-text-muted'), style: { left: `${mark}%` } }))] }));
37
+ return (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsxs("div", { className: "flex items-baseline gap-2 text-xs", children: [_jsx("span", { className: "text-text", children: window.label }), _jsx("span", { className: "ml-auto tabular-nums text-text-muted", children: t('limits.used', { percent }) })] }), compact ? (bar) : (_jsx(Tooltip, { label: _jsxs("span", { className: "flex flex-col gap-0.5", children: [_jsxs("span", { children: [t('limits.used', { percent }), mark !== null && ` · ${t('limits.windowPassed', { percent: mark })}`] }), mark !== null && _jsx("span", { className: "text-text-muted", children: mark <= percent ? t('limits.faster') : t('limits.slower') })] }), children: bar })), resetsAt !== null && (_jsxs("p", { className: "text-xs whitespace-nowrap text-text-faint", children: [t('limits.resets', { at: resetsAt }), resetsIn !== null && ` · ${resetsIn}`] }))] }));
38
+ }
39
+ /* Every provider with what its plan has left, shared by the usage page and the sidebar card. */
40
+ export function LimitsList({ limits, now, compact = false }) {
41
+ return (_jsx("div", { className: clsx('flex flex-col', compact ? 'gap-4' : 'gap-6'), children: limits.providers.map((provider) => {
42
+ const note = explain(provider);
43
+ return (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("p", { className: "flex items-center gap-2 text-xs font-medium", children: [_jsx("span", { style: { color: PROVIDER_COLORS[provider.kind] }, children: _jsx(ProviderLogo, { provider: provider.kind }) }), PROVIDER_LABELS[provider.kind], provider.plan !== null && _jsxs("span", { className: "text-text-muted", children: ["\u00B7 ", provider.plan] })] }), note === null ? (_jsx("div", { className: clsx('flex flex-col', compact ? 'gap-3' : 'gap-4'), children: provider.windows.map((window) => (_jsx(WindowBar, { window: window, now: now, compact: compact }, window.id))) })) : (_jsx("p", { className: "text-xs text-text-faint", children: note }))] }, limitsAccountId(provider)));
44
+ }) }));
45
+ }
46
+ /* One window on one line, for a card that holds several accounts: the label, a thin bar with the elapsed mark, the percent. */
47
+ function WindowLine({ window, now }) {
48
+ const { t } = useTranslation('agent-usage');
49
+ const percent = Math.round(window.used * 100);
50
+ const elapsed = elapsedShare(window, now);
51
+ const mark = elapsed === null ? null : Math.round(elapsed * 100);
52
+ return (_jsxs(_Fragment, { children: [_jsx("span", { className: "text-text-muted", children: window.label }), _jsxs("div", { role: "progressbar", "aria-label": t('limits.bar', { window: window.label, percent }), "aria-valuenow": percent, "aria-valuemin": 0, "aria-valuemax": 100, className: "relative h-1.5 rounded-full bg-surface-sunken", children: [_jsx("div", { className: clsx('absolute inset-y-0 left-0 rounded-full', toneOf(window.used)), style: { width: `${Math.min(100, percent)}%` } }), mark !== null && (_jsx("span", { "aria-hidden": true, className: clsx('absolute -inset-y-0.5 w-px -translate-x-1/2', mark <= percent ? 'bg-surface-raised' : 'bg-text-muted'), style: { left: `${mark}%` } }))] }), _jsx("span", { className: clsx('text-right tabular-nums', window.used >= 0.9 ? 'text-status-error' : 'text-text'), children: formatPercent(percent) })] }));
53
+ }
54
+ function AccountLines({ account, now }) {
55
+ const { t } = useTranslation('agent-usage');
56
+ const signedOut = isSignedOut(account);
57
+ const note = signedOut ? null : accountNote(account);
58
+ const windows = signedOut || note !== null ? [] : (account.entry?.windows ?? []);
59
+ const next = nextReset(windows, now);
60
+ const resetsIn = next === null ? null : resetInLabel(next.resetsAt, now);
61
+ return (_jsxs(_Fragment, { children: [_jsxs("p", { className: "col-span-full mt-2 flex min-w-0 items-center gap-2", children: [_jsx(AccountDot, { color: account.color }), _jsx("span", { className: "truncate", children: account.name }), signedOut ? (_jsxs("span", { className: "shrink-0 text-text-faint", children: ["\u00B7 ", t('limits.signedOut')] })) : (account.entry?.plan && _jsxs("span", { className: "shrink-0 text-text-muted", children: ["\u00B7 ", account.entry.plan] }))] }), note !== null && _jsx("p", { className: "col-span-full text-text-faint", children: note }), windows.map((window) => (_jsx(WindowLine, { window: window, now: now }, window.id))), next !== null && (_jsxs("p", { className: "col-span-full whitespace-nowrap text-text-faint", children: [t('limits.nextReset', { window: next.label, at: resetAtLabel(next.resetsAt, now) }), resetsIn !== null && ` · ${resetsIn}`] }))] }));
62
+ }
63
+ /*
64
+ * The hover card once a CLI has several accounts: a block per account, a line per window. One grid over
65
+ * the whole card, so the bars of every account start and end at the same place whatever their labels.
66
+ */
67
+ export function AccountLimitsList({ groups, now }) {
68
+ return (_jsx("div", { className: "grid grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-x-2 gap-y-1 text-xs", children: groups.map((group) => (_jsxs(Fragment, { children: [_jsxs("p", { className: "col-span-full mt-3 flex items-center gap-2 font-medium first:mt-0", children: [_jsx("span", { style: { color: PROVIDER_COLORS[group.kind] }, children: _jsx(ProviderLogo, { provider: group.kind }) }), PROVIDER_LABELS[group.kind]] }), group.accounts.map((account) => (_jsx(AccountLines, { account: account, now: now }, account.id)))] }, group.kind))) }));
69
+ }
@@ -0,0 +1,9 @@
1
+ import type { UsageProvider, UsageSummaryResult } from '@ruimte/agent-contracts';
2
+ import type { UsageMetric } from '../state/usage';
3
+ interface UsageBreakdownProps {
4
+ summary: UsageSummaryResult;
5
+ metric: UsageMetric;
6
+ providers: readonly UsageProvider[];
7
+ }
8
+ export declare function UsageBreakdown({ summary, metric, providers }: UsageBreakdownProps): import("react").JSX.Element;
9
+ export {};
@@ -0,0 +1,69 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import clsx from 'clsx';
4
+ import { CircleHelp, Folder, Info } from 'lucide-react';
5
+ import { useTranslation } from 'react-i18next';
6
+ import { Segmented } from '@ruimte/ui/controls';
7
+ import { useChatScope } from '../scope';
8
+ import { chatHost } from '../host';
9
+ import { SECTION_LABEL } from '@ruimte/ui/classes';
10
+ import { EmptyState } from '@ruimte/ui/EmptyState';
11
+ import { Tooltip } from '@ruimte/ui/Tooltip';
12
+ import { Icon } from '@ruimte/ui/Icon';
13
+ import { ProviderLogo } from '../agents/ProviderLogo';
14
+ import { USAGE_PROVIDERS, totalTokensOf } from '@ruimte/agent-contracts';
15
+ import { modelNameFromSlug } from '../agents/model-name';
16
+ import { formatCount, formatTokens, PROVIDER_COLORS, PROVIDER_LABELS, shortPath, slotLabel } from './format';
17
+ import { useMoney } from './money';
18
+ import { deriveDays } from './summary';
19
+ /* The row of every table, the same height and the same hover as a commit row in the git panel. */
20
+ const ROW = 'flex h-8 items-center gap-2 rounded-md px-2 text-xs hover:bg-surface-hover';
21
+ const HEAD = 'flex h-6 items-center gap-2 px-2 text-xs text-text-faint';
22
+ /* A row is padded so its hover has room around the text, and the table pulls that padding back off
23
+ again. The first column then starts on the same line as every other section of the page. */
24
+ const TABLE = '-mx-2 flex flex-col';
25
+ const TABS = ['models', 'projects', 'day'];
26
+ /* A share of the biggest row, never under one pixel. A row that did something has to be visible. */
27
+ const barWidth = (value, top) => (value <= 0 || top <= 0 ? 0 : Math.max(1, Math.round((value / top) * 100)));
28
+ function ProviderMark({ provider }) {
29
+ return (_jsx(Tooltip, { label: PROVIDER_LABELS[provider], children: _jsx("span", { style: { color: PROVIDER_COLORS[provider] }, children: _jsx(ProviderLogo, { provider: provider }) }) }));
30
+ }
31
+ function ModelRows({ models, metric, total }) {
32
+ const { t } = useTranslation('agent-usage');
33
+ const money = useMoney();
34
+ /* On the tokens metric the biggest row is the one that moved the most, not the dearest. */
35
+ const sorted = [...models].sort((a, b) => metric === 'tokens' ? totalTokensOf(b.totals) - totalTokensOf(a.totals) : (b.costUsd ?? -1) - (a.costUsd ?? -1));
36
+ return (_jsxs("div", { className: TABLE, children: [_jsxs("div", { className: HEAD, children: [_jsx("span", { className: "grow", children: t('breakdown.models.model') }), _jsx("span", { className: "w-16 text-right", children: t('breakdown.models.calls') }), _jsx("span", { className: "w-16 text-right", children: t('breakdown.models.input') }), _jsx("span", { className: "w-16 text-right", children: t('breakdown.models.output') }), _jsx("span", { className: "w-24 text-right", children: t('breakdown.models.cache') }), _jsx("span", { className: "w-20 text-right", children: t('breakdown.models.cost') }), _jsx("span", { className: "w-12 text-right", children: t('breakdown.models.share') })] }), sorted.map((model) => (_jsxs("div", { className: ROW, children: [_jsx(ProviderMark, { provider: model.provider }), _jsx("span", { className: "truncate", children: modelNameFromSlug(model.model) }), model.priceBasis === 'family' && model.pricedAs !== null && (_jsx(Tooltip, { label: t('breakdown.models.pricedAs', { model: modelNameFromSlug(model.pricedAs) }), children: _jsx("span", { className: "text-text-faint", children: _jsx(Icon, { icon: Info, size: 12 }) }) })), _jsx("span", { className: "ml-auto w-16 text-right tabular-nums text-text-muted", children: formatCount(model.totals.calls) }), _jsx("span", { className: "w-16 text-right tabular-nums text-text-muted", children: formatTokens(model.totals.input) }), _jsx("span", { className: "w-16 text-right tabular-nums text-text-muted", children: formatTokens(model.totals.output) }), _jsxs("span", { className: "w-24 text-right tabular-nums text-text-muted", children: [formatTokens(model.totals.cacheRead), " / ", formatTokens(model.totals.cacheWrite)] }), _jsx("span", { className: "w-20 text-right tabular-nums", children: model.costUsd === null ? (_jsx(Tooltip, { label: t('breakdown.models.noPrice'), children: _jsxs("span", { className: "inline-flex items-center gap-1 text-text-faint", children: [_jsx(Icon, { icon: CircleHelp, size: 12 }), "?"] }) })) : (money(model.costUsd)) }), _jsx("span", { className: "w-12 text-right tabular-nums text-text-muted", children: model.costUsd === null || total <= 0 ? '' : `${Math.round((model.costUsd / total) * 100)}%` })] }, `${model.provider} ${model.model}`)))] }));
37
+ }
38
+ /* One project's row, under the name and the mark its host gives it, or its folder's own under a folder. */
39
+ function ProjectRow({ project, top, money }) {
40
+ // The usage on screen is one host's, so the look of a project is that host's too.
41
+ const look = chatHost().useProjectLook(useChatScope().id, project.projectId);
42
+ return (_jsxs("div", { className: `${ROW} h-auto items-start py-1.5`, children: [_jsx("span", { className: "mt-px flex", children: look?.mark ?? (_jsx("span", { className: "text-text-faint", children: _jsx(Icon, { icon: Folder, size: 16 }) })) }), _jsxs("span", { className: "flex min-w-0 grow flex-col", children: [_jsx("span", { className: "truncate", children: look?.name ?? project.name }), _jsx("span", { className: "truncate text-text-faint", children: shortPath(project.folder) })] }), _jsx("span", { className: "mt-1 flex h-[9px] w-32 shrink-0 overflow-hidden rounded-full bg-surface-sunken", children: USAGE_PROVIDERS.map((provider) => {
43
+ const share = project.byProvider[provider];
44
+ const width = barWidth(share?.costUsd ?? 0, top);
45
+ return width === 0 ? null : _jsx("span", { style: { width: `${width}%`, background: PROVIDER_COLORS[provider] } }, provider);
46
+ }) }), _jsx("span", { className: "w-20 text-right tabular-nums", children: money(project.costUsd) }), _jsx("span", { className: "w-16 text-right tabular-nums text-text-muted", children: formatTokens(totalTokensOf(project.totals)) })] }));
47
+ }
48
+ function ProjectRows({ projects }) {
49
+ const money = useMoney();
50
+ const top = Math.max(...projects.map((project) => project.costUsd), 0);
51
+ return (_jsx("div", { className: clsx(TABLE, 'gap-1'), children: projects.map((project) => (_jsx(ProjectRow, { project: project, top: top, money: money }, project.folder))) }));
52
+ }
53
+ /* The chart written out per calendar day, a column per provider that did anything in the period. */
54
+ function DayRows({ summary, providers }) {
55
+ const { t } = useTranslation('agent-usage');
56
+ const money = useMoney();
57
+ const rows = deriveDays(summary);
58
+ if (rows.length === 0) {
59
+ return _jsx(EmptyState, { children: t('breakdown.days.empty') });
60
+ }
61
+ return (_jsxs("div", { className: TABLE, children: [_jsxs("div", { className: HEAD, children: [_jsx("span", { className: "grow", children: t('breakdown.days.day') }), providers.map((provider) => (_jsx("span", { className: "w-20 text-right", children: PROVIDER_LABELS[provider] }, provider))), _jsx("span", { className: "w-20 text-right", children: t('breakdown.days.total') }), _jsx("span", { className: "w-16 text-right", children: t('breakdown.days.tokens') })] }), rows.map((row) => (_jsxs("div", { className: ROW, children: [_jsx("span", { className: "grow truncate", children: slotLabel(row.slot) }), providers.map((provider) => (_jsx("span", { className: "w-20 text-right tabular-nums text-text-muted", children: row.costByProvider[provider] === undefined ? '' : money(row.costByProvider[provider]) }, provider))), _jsx("span", { className: "w-20 text-right tabular-nums", children: money(row.costUsd) }), _jsx("span", { className: "w-16 text-right tabular-nums text-text-muted", children: formatTokens(row.tokens) })] }, row.slot)))] }));
62
+ }
63
+ /* The same period cut three ways: which model, which checkout, and which day it went on. */
64
+ export function UsageBreakdown({ summary, metric, providers }) {
65
+ const { t } = useTranslation('agent-usage');
66
+ const [tab, setTab] = useState('models');
67
+ const total = summary.models.reduce((sum, model) => sum + (model.costUsd ?? 0), 0);
68
+ return (_jsxs("section", { className: "flex flex-col gap-3", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("h2", { className: SECTION_LABEL, children: t('breakdown.title') }), _jsx("div", { className: "ml-auto", children: _jsx(Segmented, { value: tab, options: TABS.map((id) => ({ id, label: t(`breakdown.tabs.${id}`) })), onChange: (id) => setTab(id), label: t('breakdown.title') }) })] }), tab === 'models' && _jsx(ModelRows, { models: summary.models, metric: metric, total: total }), tab === 'projects' && _jsx(ProjectRows, { projects: summary.projects }), tab === 'day' && _jsx(DayRows, { summary: summary, providers: providers })] }));
69
+ }
@@ -0,0 +1,10 @@
1
+ import type { UsageProvider } from '@ruimte/agent-contracts';
2
+ import { type ChartSlot } from './summary';
3
+ interface UsageChartProps {
4
+ slots: readonly ChartSlot[];
5
+ providers: readonly UsageProvider[];
6
+ format(value: number): string;
7
+ labelEvery: number;
8
+ }
9
+ export declare function UsageChart({ slots, providers, format, labelEvery }: UsageChartProps): import("react").JSX.Element;
10
+ export {};
@@ -0,0 +1,49 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import clsx from 'clsx';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { FLOAT } from '@ruimte/ui/classes';
6
+ import { useMeasuredWidth } from '@ruimte/ui/useMeasuredWidth';
7
+ import { PROVIDER_COLORS, PROVIDER_LABELS, slotAxisLabel, slotLabel } from './format';
8
+ import { niceScale } from './summary';
9
+ const HEIGHT = 224;
10
+ const TOOLTIP_WIDTH = 168;
11
+ /* A bar that is dimmed still has to read as its own color, so the rest keeps well over half of it. */
12
+ const DIMMED = 0.55;
13
+ /*
14
+ * One stacked bar per slot, a segment per provider. Stacked rather than layered areas, since the total
15
+ * of a day is the height of its bar, and the smaller provider never disappears under the larger one.
16
+ */
17
+ export function UsageChart({ slots, providers, format, labelEvery }) {
18
+ const { t } = useTranslation('agent-usage');
19
+ const [measure, width] = useMeasuredWidth();
20
+ const [hovered, setHovered] = useState(null);
21
+ const scale = niceScale(Math.max(...slots.map((slot) => slot.total), 0));
22
+ const band = slots.length === 0 ? 0 : width / slots.length;
23
+ const barWidth = Math.max(1, Math.round(band * 0.62));
24
+ const ticks = Array.from({ length: Math.round(scale.max / scale.step) + 1 }, (_, index) => index * scale.step);
25
+ const active = hovered === null ? null : slots[hovered];
26
+ const onPointerMove = (e) => {
27
+ const bounds = e.currentTarget.getBoundingClientRect();
28
+ const index = band === 0 ? -1 : Math.floor((e.clientX - bounds.left) / band);
29
+ setHovered(index >= 0 && index < slots.length ? index : null);
30
+ };
31
+ /* Near the right edge the card would run off the column, so it hangs from the other side. */
32
+ const tooltipLeft = hovered === null ? 0 : Math.round(Math.min(Math.max(0, hovered * band + band / 2 - TOOLTIP_WIDTH / 2), Math.max(0, width - TOOLTIP_WIDTH)));
33
+ return (_jsxs("div", { className: "flex flex-col", children: [_jsxs("div", { ref: measure, className: "relative", style: { height: HEIGHT }, onPointerMove: onPointerMove, onPointerLeave: () => setHovered(null), children: [_jsxs("svg", { width: width, height: HEIGHT, role: "presentation", children: [ticks.map((tick) => {
34
+ const y = Math.round(HEIGHT - (tick / scale.max) * HEIGHT) + 0.5;
35
+ return _jsx("line", { x1: 0, x2: width, y1: y, y2: y, stroke: "var(--border)", strokeWidth: 1 }, tick);
36
+ }), slots.map((slot, index) => {
37
+ const left = Math.round(index * band + (band - barWidth) / 2);
38
+ let bottom = HEIGHT;
39
+ return (_jsx("g", { opacity: hovered === null || hovered === index ? 1 : DIMMED, children: providers.map((provider) => {
40
+ const value = slot.byProvider[provider] ?? 0;
41
+ if (value <= 0) {
42
+ return null;
43
+ }
44
+ const height = Math.max(1, Math.round((value / scale.max) * HEIGHT));
45
+ bottom -= height;
46
+ return _jsx("rect", { x: left, y: bottom, width: barWidth, height: height, fill: PROVIDER_COLORS[provider] }, provider);
47
+ }) }, slot.slot));
48
+ }), hovered !== null && (_jsx("line", { x1: Math.round(hovered * band + band / 2) + 0.5, x2: Math.round(hovered * band + band / 2) + 0.5, y1: 0, y2: HEIGHT, stroke: "var(--border-strong)", strokeWidth: 1 }))] }), active && (_jsxs("div", { className: clsx(FLOAT, 'pointer-events-none absolute top-2 rounded-lg p-2 text-xs'), style: { left: tooltipLeft, width: TOOLTIP_WIDTH }, children: [_jsx("p", { className: "mb-1 font-medium", children: slotLabel(active.slot) }), providers.map((provider) => (_jsxs("p", { className: "flex items-center gap-1.5 text-text-muted", children: [_jsx("span", { className: "size-2 shrink-0 rounded-full", style: { background: PROVIDER_COLORS[provider] } }), PROVIDER_LABELS[provider], _jsx("span", { className: "ml-auto tabular-nums text-text", children: format(active.byProvider[provider] ?? 0) })] }, provider))), _jsxs("p", { className: "mt-1 flex items-center gap-1.5 border-t border-border pt-1 font-medium", children: [t('chart.total'), _jsx("span", { className: "ml-auto tabular-nums", children: format(active.total) })] })] }))] }), _jsx("div", { className: "relative mt-2 h-4", children: slots.map((slot, index) => index % labelEvery === 0 ? (_jsx("span", { className: "absolute -translate-x-1/2 text-xs whitespace-nowrap text-text-faint", style: { left: Math.round(index * band + band / 2) }, children: slotAxisLabel(slot.slot) }, slot.slot)) : null) })] }));
49
+ }
@@ -0,0 +1,6 @@
1
+ import type { ReactNode } from 'react';
2
+ export declare function UsageDialog({ open, onOpenChange, children }: {
3
+ open: boolean;
4
+ onOpenChange(open: boolean): void;
5
+ children: ReactNode;
6
+ }): import("react").JSX.Element;
@@ -0,0 +1,15 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import clsx from 'clsx';
3
+ import i18next from 'i18next';
4
+ import { Dialog } from '@base-ui-components/react/dialog';
5
+ import { useDialogLayer } from '@ruimte/ui/dialog-layer';
6
+ /*
7
+ * The frame of the usage page: larger than the settings, since the chart and the breakdown need the
8
+ * width. What goes in it (`UsagePage`, in an `ErrorBoundary` of the app's) mounts only while it is
9
+ * open, so nothing is scanned behind a closed dialog. It is a dialog and not a view, since none of it
10
+ * belongs to one project.
11
+ */
12
+ export function UsageDialog({ open, onOpenChange, children }) {
13
+ const stacked = useDialogLayer(open);
14
+ return (_jsx(Dialog.Root, { open: open, onOpenChange: onOpenChange, children: _jsxs(Dialog.Portal, { children: [_jsx(Dialog.Backdrop, { className: clsx('dialog-backdrop', stacked && 'dialog-backdrop-nested'), forceRender: stacked }), _jsxs(Dialog.Popup, { className: clsx('dialog-popup flex h-[820px] w-[1080px] flex-col', stacked && 'dialog-popup-nested'), children: [children, _jsx(Dialog.Description, { className: "sr-only", children: i18next.t('agent-usage:dialog.description') })] })] }) }));
15
+ }
@@ -0,0 +1 @@
1
+ export declare function UsageLimits(): import("react").JSX.Element | null;
@@ -0,0 +1,62 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { LogIn } from 'lucide-react';
5
+ import { AccountDot } from '../agents/AccountDot';
6
+ import { Segmented } from '@ruimte/ui/controls';
7
+ import { chatHost } from '../host';
8
+ import { PROVIDER_COLORS, PROVIDER_LABELS } from './format';
9
+ import { accountNote, checkedLabel, hasSeveralAccounts, isSignedOut } from './limit-groups';
10
+ import { WindowBar } from './LimitsList';
11
+ import { useLimitGroups, useUsageLimits } from './limits';
12
+ import { useProviderAccountsStore } from '../state/provider-accounts';
13
+ import { useChatScope } from '../scope';
14
+ import { Button } from '@ruimte/ui/Button';
15
+ import { SECTION_LABEL } from '@ruimte/ui/classes';
16
+ import { Icon } from '@ruimte/ui/Icon';
17
+ import { ProviderLogo } from '../agents/ProviderLogo';
18
+ import { Tooltip } from '@ruimte/ui/Tooltip';
19
+ import { useNow } from '@ruimte/ui/useNow';
20
+ const MINUTE_MS = 60_000;
21
+ function LoginButton({ account, login }) {
22
+ const { t } = useTranslation('agent-usage');
23
+ const button = (_jsxs(Button, { variant: "secondary", size: "sm", className: login.blocked === null ? 'self-start' : undefined, disabled: login.blocked !== null, onClick: () => login.open(account), children: [_jsx(Icon, { icon: LogIn, size: 14 }), t('limits.logIn')] }));
24
+ return login.blocked === null ? (button) : (_jsx(Tooltip, { label: login.blocked, children: _jsx("span", { className: "inline-flex self-start", children: button }) }));
25
+ }
26
+ /* One account: where its numbers come from, then a bar per window, or why there are none. */
27
+ function LimitCard({ account, named, now, login }) {
28
+ const { t } = useTranslation('agent-usage');
29
+ const signedOut = isSignedOut(account);
30
+ const note = signedOut ? null : accountNote(account);
31
+ const plan = signedOut ? null : account.entry?.plan;
32
+ const checked = signedOut || account.entry === null ? null : checkedLabel(account.entry, now);
33
+ return (_jsxs("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", children: [_jsxs("p", { className: "flex min-w-0 items-center gap-2 text-xs font-medium", children: [_jsx("span", { className: "shrink-0", style: { color: PROVIDER_COLORS[account.kind] }, children: _jsx(ProviderLogo, { provider: account.kind }) }), _jsx("span", { className: "shrink-0", children: PROVIDER_LABELS[account.kind] }), named && (_jsxs(_Fragment, { children: [_jsx("span", { className: "text-text-faint", children: "\u00B7" }), _jsx(AccountDot, { color: account.color }), _jsx("span", { className: "truncate", children: account.name })] })), plan && _jsxs("span", { className: "shrink-0 font-normal text-text-muted", children: ["\u00B7 ", plan] }), checked !== null && _jsx("span", { className: "ml-auto shrink-0 pl-2 font-normal text-text-faint", children: checked })] }), signedOut ? (_jsxs(_Fragment, { children: [_jsx("p", { className: "text-xs text-text-faint", children: t('limits.signedOutDetail') }), login !== null && _jsx(LoginButton, { account: account, login: login })] })) : note !== null ? (_jsx("p", { className: "text-xs text-text-faint", children: note })) : (_jsx("div", { className: "flex flex-col gap-4", children: account.entry?.windows.map((window) => (_jsx(WindowBar, { window: window, now: now, compact: false }, window.id))) }))] }));
34
+ }
35
+ /*
36
+ * What is left of each plan, asked of the CLIs themselves, a card per account. Nothing here reads a
37
+ * credential: every CLI holds its own login and answers the question when the daemon starts one and asks.
38
+ */
39
+ export function UsageLimits() {
40
+ const { t } = useTranslation('agent-usage');
41
+ const limits = useUsageLimits();
42
+ const groups = useLimitGroups(limits);
43
+ const now = useNow(MINUTE_MS);
44
+ const scopeId = useChatScope().id;
45
+ const loginCommands = useProviderAccountsStore((s) => s.byScope[scopeId]?.accounts?.loginCommands);
46
+ const { openLogin } = chatHost();
47
+ const blocked = chatHost().useLoginBlocked(scopeId);
48
+ const [filter, setFilter] = useState('all');
49
+ if (limits === null) {
50
+ return null;
51
+ }
52
+ const filterable = groups.length > 1 && hasSeveralAccounts(groups);
53
+ const current = filterable && groups.some((group) => group.kind === filter) ? filter : 'all';
54
+ const shown = current === 'all' ? groups : groups.filter((group) => group.kind === current);
55
+ const loginFor = (kind) => openLogin === null || loginCommands?.[kind] === undefined
56
+ ? null
57
+ : { blocked, open: (account) => void openLogin(scopeId, kind, account.id, account.name) };
58
+ return (_jsxs("section", { className: "flex flex-col gap-3", children: [_jsxs("div", { className: "flex min-h-8 items-center gap-3", children: [_jsx("h2", { className: `${SECTION_LABEL} grow`, children: t('limits.title') }), filterable && (_jsx(Segmented, { value: current, options: [
59
+ { id: 'all', label: t('dialog.accounts.all') },
60
+ ...groups.map((group) => ({ id: group.kind, label: PROVIDER_LABELS[group.kind] }))
61
+ ], onChange: setFilter, label: t('limits.filter') }))] }), _jsx("p", { className: "text-xs text-text-muted", children: t('limits.explain') }), _jsx("div", { className: "grid grid-cols-2 gap-3", children: shown.flatMap((group) => group.accounts.map((account) => (_jsx(LimitCard, { account: account, named: group.accounts.length > 1, now: now, login: loginFor(group.kind) }, account.id)))) })] }));
62
+ }
@@ -0,0 +1,4 @@
1
+ import { type ReactElement } from 'react';
2
+ export declare function UsageLimitsCard({ children }: {
3
+ children: ReactElement<Record<string, unknown>>;
4
+ }): import("react").JSX.Element;
@@ -0,0 +1,27 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import i18next from 'i18next';
3
+ import { useState } from 'react';
4
+ import { PreviewCard } from '@base-ui-components/react/preview-card';
5
+ import { hasSeveralAccounts } from './limit-groups';
6
+ import { AccountLimitsList, LimitsList } from './LimitsList';
7
+ import { useLimitGroups, useUsageLimits } from './limits';
8
+ import { useNow } from '@ruimte/ui/useNow';
9
+ const MINUTE_MS = 60_000;
10
+ /* Asking the CLIs starts a process each, so the question waits until the card is actually opened. */
11
+ function CardBody() {
12
+ const limits = useUsageLimits();
13
+ const groups = useLimitGroups(limits);
14
+ const now = useNow(MINUTE_MS);
15
+ if (limits === null) {
16
+ return _jsx("p", { className: "text-xs text-text-faint", children: i18next.t('agent-usage:limits.loading') });
17
+ }
18
+ return hasSeveralAccounts(groups) ? _jsx(AccountLimitsList, { groups: groups, now: now }) : _jsx(LimitsList, { limits: limits, now: now, compact: true });
19
+ }
20
+ /*
21
+ * What is left of each plan, without opening the usage dialog. A hover card rather than a tooltip:
22
+ * the bars take a moment to read, and it stays open while the pointer travels to it.
23
+ */
24
+ export function UsageLimitsCard({ children }) {
25
+ const [opened, setOpened] = useState(false);
26
+ return (_jsxs(PreviewCard.Root, { onOpenChange: (open) => open && setOpened(true), children: [_jsx(PreviewCard.Trigger, { render: children, delay: 500 }), _jsx(PreviewCard.Portal, { children: _jsx(PreviewCard.Positioner, { side: "top", align: "end", sideOffset: 8, className: "z-(--z-popup)", children: _jsx(PreviewCard.Popup, { className: "menu-popup w-64 p-3", children: opened && _jsx(CardBody, {}) }) }) })] }));
27
+ }