@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
@@ -0,0 +1,87 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef } from 'react';
3
+ import clsx from 'clsx';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { Circle, CircleCheck, Square } from 'lucide-react';
6
+ import { answerFieldKey, choiceKey, stepIndex } from '../logic/keys';
7
+ import { pickPromptChoice } from '../logic/prompts';
8
+ import { Icon } from '@ruimte/ui/Icon';
9
+ import { chatHost } from '../../host';
10
+ /* One question of a request: its choices, "Something else…", or a written answer when it offers no choices. */
11
+ export function QuestionBody({ question, answer, onAnswer, onCommit, locked }) {
12
+ const { t } = useTranslation('agent-prompts');
13
+ const AnswerField = chatHost().dictation.Textarea;
14
+ // The choices, then the "Something else…" field form one list with a single tab stop.
15
+ const items = useRef([]);
16
+ const shownQuestion = useRef(question.id);
17
+ const count = question.choices.length + 1;
18
+ const chosen = question.choices.findIndex((choice) => !answer.custom && answer.choices.includes(choice.label));
19
+ const tabStop = chosen !== -1 ? chosen : answer.custom ? question.choices.length : 0;
20
+ // The focused choice of the question before is gone once the next one shows; the keyboard goes on in this one.
21
+ useEffect(() => {
22
+ if (shownQuestion.current === question.id) {
23
+ return;
24
+ }
25
+ shownQuestion.current = question.id;
26
+ if (document.activeElement === null || document.activeElement === document.body) {
27
+ (items.current[tabStop] ?? items.current[0])?.focus({ preventScroll: true });
28
+ }
29
+ }, [question.id, tabStop]);
30
+ const onChoiceKeyDown = (event, index, label) => {
31
+ const action = choiceKey(event.nativeEvent, {
32
+ multiSelect: question.multiSelect,
33
+ anyChosen: !answer.custom && answer.choices.length > 0
34
+ });
35
+ if (action === null) {
36
+ return;
37
+ }
38
+ event.preventDefault();
39
+ if (action.kind === 'move') {
40
+ items.current[stepIndex(index, count, action.to)]?.focus();
41
+ }
42
+ else if (action.kind === 'commit') {
43
+ onCommit(answer);
44
+ }
45
+ else {
46
+ const next = pickPromptChoice(answer, question, label);
47
+ onAnswer(next);
48
+ if (action.kind === 'pick-and-commit') {
49
+ onCommit(next);
50
+ }
51
+ }
52
+ };
53
+ const onFieldKeyDown = (event) => {
54
+ const field = event.currentTarget;
55
+ const action = answerFieldKey(event.nativeEvent, {
56
+ belowChoices: question.choices.length > 0,
57
+ caretAtStart: field.selectionStart === 0 && field.selectionEnd === 0,
58
+ hasText: field.value.trim() !== ''
59
+ });
60
+ if (action === null) {
61
+ return;
62
+ }
63
+ event.preventDefault();
64
+ if (action.kind === 'to-last-choice') {
65
+ items.current[question.choices.length - 1]?.focus();
66
+ }
67
+ else if (action.kind === 'commit') {
68
+ onCommit({ ...answer, custom: true });
69
+ }
70
+ };
71
+ return (_jsxs("div", { className: "flex flex-col gap-2", children: [question.multiSelect && _jsx("p", { className: "text-xs text-text-muted", children: t('question.multiSelect') }), question.choices.length > 0 && (_jsxs("div", { role: question.multiSelect ? 'group' : 'radiogroup', "aria-label": question.question, className: "flex flex-col gap-2", children: [question.choices.map((choice, index) => {
72
+ const selected = !answer.custom && answer.choices.includes(choice.label);
73
+ return (_jsxs("button", { ref: (element) => {
74
+ items.current[index] = element;
75
+ }, type: "button", role: question.multiSelect ? 'checkbox' : 'radio', "aria-checked": selected, tabIndex: index === tabStop ? 0 : -1, "data-prompt-entry": index === tabStop ? '' : undefined, disabled: locked, className: clsx('flex min-h-9 items-start gap-2 rounded-lg border px-3 py-2 text-left disabled:opacity-50', selected ? 'border-accent bg-accent-soft' : 'border-border bg-surface-hover hover:bg-surface-active'), onClick: () => onAnswer(pickPromptChoice(answer, question, choice.label)), onKeyDown: (event) => onChoiceKeyDown(event, index, choice.label), children: [_jsx(Icon, { icon: selected ? CircleCheck : question.multiSelect ? Square : Circle, size: 16, className: "mt-0.5 shrink-0 text-text-muted" }), _jsxs("span", { className: "flex min-w-0 flex-wrap items-baseline gap-x-2", children: [_jsx("span", { className: "text-xs text-text", children: choice.label }), choice.description && _jsx("span", { className: "text-xs text-text-muted", children: choice.description })] })] }, choice.label));
76
+ }), _jsxs("label", { className: clsx('flex min-h-9 items-start gap-2 rounded-lg border px-3 py-2 text-left text-xs text-text focus-within:outline-2 focus-within:outline-offset-1 focus-within:outline-accent', answer.custom ? 'border-accent bg-accent-soft' : 'border-border bg-surface-hover', locked && 'opacity-50'),
77
+ // A press picks the row; the keyboard only passing through on its way up or down does not.
78
+ onPointerDown: () => {
79
+ if (!locked && !answer.custom) {
80
+ onAnswer({ ...answer, custom: true });
81
+ }
82
+ }, children: [_jsx(Icon, { icon: answer.custom ? CircleCheck : Circle, size: 16, className: "mt-0.5 shrink-0" }), _jsx(AnswerField, { ref: (element) => {
83
+ items.current[question.choices.length] = element;
84
+ }, rows: 1, tabIndex: tabStop === question.choices.length ? 0 : -1, "data-prompt-entry": tabStop === question.choices.length ? '' : undefined, className: "max-h-40 min-w-0 flex-1 resize-none field-sizing-content bg-transparent outline-none placeholder:text-text-muted", buttonClassName: "-my-1 -mr-1.5", "aria-label": t('question.answerLabel'), placeholder: t('question.somethingElse'), value: answer.text, disabled: locked, onChange: (e) => onAnswer({ ...answer, custom: true, text: e.target.value }), onKeyDown: onFieldKeyDown })] })] })), question.choices.length === 0 && (_jsx(AnswerField, { ref: (element) => {
85
+ items.current[0] = element;
86
+ }, className: "field min-h-20 text-sm", "aria-label": t('question.answerLabel'), placeholder: t('question.answerPlaceholder'), "data-prompt-entry": "", value: answer.text, disabled: locked, onChange: (e) => onAnswer({ ...answer, text: e.target.value }), onKeyDown: onFieldKeyDown }))] }));
87
+ }
@@ -0,0 +1,5 @@
1
+ export declare function AccountColors({ value, label, onChange }: {
2
+ value: string | undefined;
3
+ label: string;
4
+ onChange(id: string): void;
5
+ }): import("react").JSX.Element;
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { AccentSwatches } from '@ruimte/ui/AccentSwatches';
3
+ import { chatHost } from '../host';
4
+ /* The colors an account may wear, the host's palette. */
5
+ export function AccountColors({ value, label, onChange }) {
6
+ const { all, featured, label: labelOf } = chatHost().accents;
7
+ return _jsx(AccentSwatches, { value: value, label: label, accents: all, featured: featured, labelOf: labelOf, onChange: onChange });
8
+ }
@@ -0,0 +1,12 @@
1
+ import type { ProviderInfo } from '@ruimte/agent-contracts';
2
+ import { type AccountEntry } from '../agents/accounts';
3
+ interface AccountDetailProps {
4
+ provider: ProviderInfo;
5
+ entry: AccountEntry;
6
+ canLogIn: boolean;
7
+ loginBlocked: string | null;
8
+ secretsAvailable: boolean;
9
+ onRemoved(): void;
10
+ }
11
+ export declare function AccountDetail({ provider, entry, canLogIn, loginBlocked, secretsAvailable, onRemoved }: AccountDetailProps): import("react").JSX.Element;
12
+ export {};
@@ -0,0 +1,76 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { Trans, useTranslation } from 'react-i18next';
4
+ import { LogIn } from 'lucide-react';
5
+ import { AccountDot } from '../agents/AccountDot';
6
+ import { accountName, FOLDER_VARIABLES } from '../agents/accounts';
7
+ import { ConfirmDialog } from '@ruimte/ui/settings/ConfirmDialog';
8
+ import { chatHost } from '../host';
9
+ import { useChatScope } from '../scope';
10
+ import { removeAccount, saveAccount } from './account-actions';
11
+ import { AccountVariables } from './AccountVariables';
12
+ import { AccountColors } from './AccountColors';
13
+ import { Toggle } from '@ruimte/ui/controls';
14
+ import { DetailHeader, REMOVE_BUTTON } from '@ruimte/ui/settings/DetailHeader';
15
+ import { CliMark } from './parts';
16
+ import { SettingsRow } from '@ruimte/ui/settings/SettingsRow';
17
+ import { SettingsSection } from '@ruimte/ui/settings/SettingsSection';
18
+ import { Button } from '@ruimte/ui/Button';
19
+ import { copyText } from '@ruimte/ui/clipboard';
20
+ import { Icon } from '@ruimte/ui/Icon';
21
+ import { Tooltip } from '@ruimte/ui/Tooltip';
22
+ // The states in which the machine's own sentence says more than the usual line under the login.
23
+ const TROUBLE = new Set(['folder-missing', 'unavailable', 'failed']);
24
+ /* The name a person types, saved when they leave the field; an empty one goes back to what was there. */
25
+ function NameField({ value, label, onSave }) {
26
+ const [draft, setDraft] = useState(value);
27
+ const commit = () => {
28
+ const name = draft.trim();
29
+ if (name === '' || name === value) {
30
+ setDraft(value);
31
+ return;
32
+ }
33
+ onSave(name);
34
+ };
35
+ return (_jsx("input", { className: "field w-55 max-w-full", value: draft, maxLength: 80, "aria-label": label, onChange: (event) => setDraft(event.target.value), onBlur: commit, onKeyDown: (event) => {
36
+ if (event.key === 'Enter') {
37
+ event.currentTarget.blur();
38
+ }
39
+ } }));
40
+ }
41
+ /* One account: how it looks in a picker, who is signed in, where its folder is, its variables. */
42
+ export function AccountDetail({ provider, entry, canLogIn, loginBlocked, secretsAvailable, onRemoved }) {
43
+ const { t } = useTranslation('agent-providers');
44
+ const scope = useChatScope();
45
+ const [confirming, setConfirming] = useState(false);
46
+ const { id, account, status, isDefault } = entry;
47
+ const name = accountName(entry, provider.name);
48
+ const folder = status?.home ?? '';
49
+ const variable = FOLDER_VARIABLES[provider.kind];
50
+ const signedIn = status?.state === 'ready';
51
+ const showFolder = () => {
52
+ copyText(folder);
53
+ chatHost().notify({ kind: 'success', title: t('account.folderCopied'), description: folder });
54
+ };
55
+ const loginDetail = () => {
56
+ if (status !== null && TROUBLE.has(status.state) && status.message) {
57
+ return status.message;
58
+ }
59
+ if (!signedIn) {
60
+ return t('account.login.signedOutDetail');
61
+ }
62
+ return status?.plan ? t('account.login.planDetail', { plan: status.plan }) : t('account.login.detail');
63
+ };
64
+ const loginButton = (_jsxs(Button, { variant: "secondary", disabled: loginBlocked !== null, onClick: () => void chatHost().openLogin?.(scope.id, provider.kind, id, name), children: [_jsx(Icon, { icon: LogIn, size: 14 }), signedIn ? t('account.login.logInAgain') : t('account.login.logIn')] }));
65
+ return (_jsxs(_Fragment, { children: [_jsx(DetailHeader, { mark: _jsx("span", { className: "grid h-6 shrink-0 place-items-center", children: _jsx(AccountDot, { color: account.color, className: "size-3" }) }), title: name, subtitle: _jsxs(_Fragment, { children: [_jsx(CliMark, { kind: provider.kind, size: 12, className: "text-text-muted" }), t('account.kind', { provider: provider.name })] }), actions: folder !== '' && (_jsx(Button, { variant: "secondary", onClick: showFolder, children: t('account.showFolder') })) }), _jsxs(SettingsSection, { children: [_jsx(SettingsRow, { label: t('account.enabled.label'), description: t('account.enabled.description'), control: _jsx(Toggle, { checked: account.enabled !== false, label: t('account.enabled.label'), onChange: (checked) => {
66
+ const { enabled: _enabled, ...rest } = account;
67
+ void saveAccount(scope, id, checked ? rest : { ...rest, enabled: false });
68
+ } }) }), _jsx(SettingsRow, { label: t('account.name'), control: _jsx(NameField, { value: name, label: t('account.name'), onSave: (label) => void saveAccount(scope, id, { ...account, label }) }, name) }), _jsx(SettingsRow, { label: t('account.color'), control: _jsx(AccountColors, { value: account.color, label: t('account.color'), onChange: (color) => void saveAccount(scope, id, { ...account, color }) }) })] }), canLogIn && (_jsxs(SettingsSection, { title: t('account.login.title'), children: [_jsx(SettingsRow, { label: signedIn
69
+ ? status?.email
70
+ ? t('account.login.signedInAs', { email: status.email })
71
+ : t('account.login.signedIn')
72
+ : t('account.login.signedOut'), description: loginBlocked === null ? loginDetail() : `${loginDetail()} ${loginBlocked}`, control: loginBlocked === null ? (loginButton) : (_jsx(Tooltip, { label: loginBlocked, children: _jsx("span", { className: "inline-flex", children: loginButton }) })) }), _jsxs(SettingsRow, { label: t('account.folder.label'), children: [_jsx("input", { className: "field h-8.5 font-mono text-code", value: folder, readOnly: true, "aria-label": t('account.folder.label') }), _jsx("p", { className: "-mt-1 text-xs text-text-muted", children: variable ? (_jsx(Trans, { t: t, i18nKey: "account.folder.detail", values: { variable }, components: { code: _jsx("code", { className: "font-mono text-code" }) } })) : (t('account.folder.detailPlain')) })] })] })), _jsx(AccountVariables, { id: id, account: account, secretsAvailable: secretsAvailable }, `${id}:${JSON.stringify(account.env ?? [])}`), _jsx(SettingsSection, { children: _jsx(SettingsRow, { label: t('account.remove.label'), description: isDefault ? t('account.remove.defaultNote') : t('account.remove.description'), control: _jsx("button", { type: "button", className: REMOVE_BUTTON, disabled: isDefault, onClick: () => setConfirming(true), children: t('account.remove.button') }) }) }), _jsx(ConfirmDialog, { open: confirming, onOpenChange: setConfirming, title: t('account.remove.confirmTitle', { account: name }), description: t('account.remove.confirmDescription', { folder }), confirmLabel: t('account.remove.confirm'), onConfirm: async () => {
73
+ await removeAccount(scope, id, provider.kind);
74
+ onRemoved();
75
+ } })] }));
76
+ }
@@ -0,0 +1,8 @@
1
+ import type { ProviderAccount } from '@ruimte/agent-contracts';
2
+ interface AccountVariablesProps {
3
+ id: string;
4
+ account: ProviderAccount;
5
+ secretsAvailable: boolean;
6
+ }
7
+ export declare function AccountVariables({ id, account, secretsAvailable }: AccountVariablesProps): import("react").JSX.Element;
8
+ export {};
@@ -0,0 +1,48 @@
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 { Lock, LockOpen, Plus, X } from 'lucide-react';
6
+ import { useChatScope } from '../scope';
7
+ import { saveAccount } from './account-actions';
8
+ import { draftsOf, emptyDraft, variablesChanged, variablesOf, variablesProblem } from './variables';
9
+ import { SettingsRow } from '@ruimte/ui/settings/SettingsRow';
10
+ import { SettingsSection } from '@ruimte/ui/settings/SettingsSection';
11
+ import { Button } from '@ruimte/ui/Button';
12
+ import { FORM_ERROR } from '@ruimte/ui/classes';
13
+ import { Icon } from '@ruimte/ui/Icon';
14
+ import { Tooltip } from '@ruimte/ui/Tooltip';
15
+ /*
16
+ * The environment of one account, edited as a whole and saved with one button: a half-typed name or a
17
+ * secret that still has to be retyped would otherwise reach the machine one keystroke at a time.
18
+ */
19
+ export function AccountVariables({ id, account, secretsAvailable }) {
20
+ const { t } = useTranslation('agent-providers');
21
+ const scope = useChatScope();
22
+ const [drafts, setDrafts] = useState(() => draftsOf(account.env));
23
+ const [saving, setSaving] = useState(false);
24
+ const changed = variablesChanged(drafts, account.env);
25
+ const problem = changed ? variablesProblem(drafts) : null;
26
+ const update = (key, patch) => {
27
+ setDrafts((current) => current.map((draft) => (draft.key === key ? { ...draft, ...patch } : draft)));
28
+ };
29
+ const save = async () => {
30
+ const env = variablesOf(drafts);
31
+ const { env: _env, ...rest } = account;
32
+ setSaving(true);
33
+ try {
34
+ await saveAccount(scope, id, env.length === 0 ? rest : { ...rest, env });
35
+ }
36
+ finally {
37
+ setSaving(false);
38
+ }
39
+ };
40
+ return (_jsxs(SettingsSection, { title: t('account.variables.title'), description: t('account.variables.description'), action: _jsxs(Button, { variant: "secondary", size: "sm", onClick: () => setDrafts((current) => [...current, emptyDraft(false)]), children: [_jsx(Icon, { icon: Plus, size: 14 }), t('account.variables.add')] }), footer: secretsAvailable ? undefined : t('account.variables.noKeychain'), children: [drafts.length === 0 && _jsx(SettingsRow, { muted: true, label: t('account.variables.none') }), drafts.map((draft, index) => {
41
+ const name = draft.name.trim() || String(index + 1);
42
+ const kept = draft.value === '' && draft.sensitive && draft.kept === draft.name.trim();
43
+ const lockLabel = draft.sensitive ? t('account.variables.unlock', { name }) : t('account.variables.lock', { name });
44
+ // Off a keychain a value can only be made plain, never secret.
45
+ const lockDisabled = !secretsAvailable && !draft.sensitive;
46
+ return (_jsxs("div", { className: "flex min-w-0 items-center gap-2 px-3.5 py-3 font-mono text-code", children: [_jsx("input", { className: "field h-7.5 w-47.5 shrink-0 font-mono text-code max-[640px]:w-32", value: draft.name, spellCheck: false, autoComplete: "off", "aria-label": t('account.variables.name', { index: index + 1 }), placeholder: "NAME", onChange: (event) => update(draft.key, { name: event.target.value }) }), _jsx("span", { className: "text-text-faint", children: "=" }), _jsx("input", { className: "field h-7.5 min-w-0 grow font-mono text-code", type: draft.sensitive ? 'password' : 'text', value: draft.value, spellCheck: false, autoComplete: "off", "aria-label": t('account.variables.value', { name }), placeholder: kept ? t('account.variables.kept') : undefined, onChange: (event) => update(draft.key, { value: event.target.value }) }), _jsx(Tooltip, { label: lockDisabled ? t('account.variables.noKeychain') : lockLabel, children: _jsx("span", { className: "inline-flex", children: _jsx("button", { type: "button", className: clsx('icon-btn icon-btn-sm', draft.sensitive && 'text-text'), "aria-label": lockLabel, "aria-pressed": draft.sensitive, disabled: lockDisabled, onClick: () => update(draft.key, { sensitive: !draft.sensitive }), children: _jsx(Icon, { icon: draft.sensitive ? Lock : LockOpen, size: 14 }) }) }) }), _jsx(Tooltip, { label: t('account.variables.remove', { name }), name: true, children: _jsx("button", { type: "button", className: "icon-btn icon-btn-sm", onClick: () => setDrafts((current) => current.filter((other) => other.key !== draft.key)), children: _jsx(Icon, { icon: X, size: 14 }) }) })] }, draft.key));
47
+ }), changed && (_jsxs("div", { className: "flex min-w-0 flex-wrap items-center justify-end gap-2 px-4.5 py-3", children: [problem !== null && (_jsx("span", { className: clsx(FORM_ERROR, 'mr-auto'), role: "alert", children: problem })), _jsx(Button, { onClick: () => setDrafts(draftsOf(account.env)), children: t('account.variables.discard') }), _jsx(Button, { variant: "primary", disabled: problem !== null || saving, onClick: () => void save(), children: t('common.save') })] }))] }));
48
+ }
@@ -0,0 +1,10 @@
1
+ import type { ProviderInfo } from '@ruimte/agent-contracts';
2
+ import { type AccountEntry } from '../agents/accounts';
3
+ interface AddAccountFormProps {
4
+ provider: ProviderInfo;
5
+ entries: AccountEntry[];
6
+ onCancel(): void;
7
+ onAdded(id: string, name: string, made: boolean): void;
8
+ }
9
+ export declare function AddAccountForm({ provider, entries, onCancel, onAdded }: AddAccountFormProps): import("react").JSX.Element;
10
+ export {};
@@ -0,0 +1,58 @@
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 { ChevronRight } from 'lucide-react';
6
+ import { freeAccountColor, mintAccountId } from '../agents/accounts';
7
+ import { chatHost } from '../host';
8
+ import { useChatScope } from '../scope';
9
+ import { createAccount, linkAccount } from './account-actions';
10
+ import { AccountColors } from './AccountColors';
11
+ import { DetailHeader } from '@ruimte/ui/settings/DetailHeader';
12
+ import { CliTile } from './parts';
13
+ import { SettingsRow } from '@ruimte/ui/settings/SettingsRow';
14
+ import { SettingsSection } from '@ruimte/ui/settings/SettingsSection';
15
+ import { providerAccountsOf } from '../state/provider-accounts';
16
+ import { Button } from '@ruimte/ui/Button';
17
+ import { FORM_ERROR } from '@ruimte/ui/classes';
18
+ import { Icon } from '@ruimte/ui/Icon';
19
+ /*
20
+ * A name and a color, asked before anything exists, so the account is made once with both. By default
21
+ * the machine makes the folder; under Advanced a person points at a folder of their own instead.
22
+ */
23
+ export function AddAccountForm({ provider, entries, onCancel, onAdded }) {
24
+ const { t } = useTranslation('agent-providers');
25
+ const scope = useChatScope();
26
+ const [name, setName] = useState('');
27
+ const [color, setColor] = useState(() => freeAccountColor(entries, chatHost().accents.current()));
28
+ const [advanced, setAdvanced] = useState(false);
29
+ const [folder, setFolder] = useState('');
30
+ const [busy, setBusy] = useState(false);
31
+ const [error, setError] = useState(null);
32
+ const label = name.trim();
33
+ const linking = advanced && folder.trim() !== '';
34
+ const submit = async (event) => {
35
+ event.preventDefault();
36
+ if (label === '' || busy) {
37
+ return;
38
+ }
39
+ setBusy(true);
40
+ setError(null);
41
+ try {
42
+ if (linking) {
43
+ const taken = new Set(Object.keys(providerAccountsOf(scope.id).accounts?.accounts ?? {}));
44
+ const id = mintAccountId(provider.kind, label, taken);
45
+ await linkAccount(scope, id, { kind: provider.kind, label, color, home: folder.trim() });
46
+ onAdded(id, label, false);
47
+ }
48
+ else {
49
+ onAdded(await createAccount(scope, provider.kind, label, color), label, true);
50
+ }
51
+ }
52
+ catch (e) {
53
+ setError(e instanceof Error ? e.message : String(e));
54
+ setBusy(false);
55
+ }
56
+ };
57
+ return (_jsxs("form", { className: "contents", onSubmit: (event) => void submit(event), children: [_jsx(DetailHeader, { mark: _jsx(CliTile, { kind: provider.kind }), title: t('add.title', { provider: provider.name }), subtitle: t('add.description') }), _jsxs(SettingsSection, { children: [_jsx(SettingsRow, { label: t('add.name'), control: _jsx("input", { className: "field w-55 max-w-full", value: name, maxLength: 80, autoFocus: true, placeholder: t('add.namePlaceholder'), "aria-label": t('add.name'), onChange: (event) => setName(event.target.value) }) }), _jsx(SettingsRow, { label: t('add.color'), control: _jsx(AccountColors, { value: color, label: t('add.color'), onChange: setColor }) })] }), _jsxs("div", { className: "flex min-w-0 flex-col gap-2.5", children: [_jsxs("button", { type: "button", "aria-expanded": advanced, className: "flex items-center gap-1.5 self-start text-sm font-medium text-text-muted hover:text-text", onClick: () => setAdvanced((open) => !open), children: [_jsx(Icon, { icon: ChevronRight, size: 14, className: clsx('transition-transform', advanced && 'rotate-90') }), t('add.advanced')] }), advanced && (_jsx(SettingsSection, { children: _jsx(SettingsRow, { label: t('add.link.label'), description: t('add.link.description'), children: _jsx("input", { className: "field font-mono text-code", value: folder, spellCheck: false, autoComplete: "off", placeholder: t('add.link.placeholder'), "aria-label": t('add.link.label'), onChange: (event) => setFolder(event.target.value) }) }) }))] }), _jsxs("div", { className: "flex min-w-0 flex-wrap items-center justify-end gap-2", children: [error !== null && (_jsx("span", { className: clsx(FORM_ERROR, 'mr-auto break-words'), role: "alert", children: error || t('add.failed') })), _jsx(Button, { onClick: onCancel, children: t('common.cancel') }), _jsx(Button, { type: "submit", variant: "primary", disabled: label === '' || busy, children: linking ? t('add.linkButton') : t('add.create') })] })] }));
58
+ }
@@ -0,0 +1,11 @@
1
+ import type { ProviderInfo } from '@ruimte/agent-contracts';
2
+ import { type AccountEntry } from '../agents/accounts';
3
+ interface CliDetailProps {
4
+ provider: ProviderInfo;
5
+ entries: AccountEntry[] | null;
6
+ canAddAccount: boolean;
7
+ onAddAccount(): void;
8
+ onPickAccount(id: string): void;
9
+ }
10
+ export declare function CliDetail({ provider, entries, canAddAccount, onAddAccount, onPickAccount }: CliDetailProps): import("react").JSX.Element;
11
+ export {};
@@ -0,0 +1,62 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useTranslation } from 'react-i18next';
3
+ import { ChevronRight, Plus } from 'lucide-react';
4
+ import { AccountDot } from '../agents/AccountDot';
5
+ import { ACCOUNT_TONE_CLASSES, accountName, accountStatusLine } from '../agents/accounts';
6
+ import { accountFor, forgetChatSelection, rememberChatAccount, rememberChatSelection, selectionFor, useChatPreferences } from '../chat/preferences';
7
+ import { Segmented, Toggle } from '@ruimte/ui/controls';
8
+ import { useChatScope } from '../scope';
9
+ import { providerAbilities } from './provider-abilities';
10
+ import { DetailHeader } from '@ruimte/ui/settings/DetailHeader';
11
+ import { CliTile } from './parts';
12
+ import { SettingsRow } from '@ruimte/ui/settings/SettingsRow';
13
+ import { SettingsSection } from '@ruimte/ui/settings/SettingsSection';
14
+ import { useProviderAccountsStore } from '../state/provider-accounts';
15
+ import { Button } from '@ruimte/ui/Button';
16
+ import { Icon } from '@ruimte/ui/Icon';
17
+ import { Select } from '@ruimte/ui/Select';
18
+ const PROVIDER_DEFAULT = '';
19
+ // Up to this many choices read at a glance side by side; more go into a menu.
20
+ const MAX_SEGMENTS = 3;
21
+ /* One row per knob the chosen model exposes; the composer's option picker shows the same descriptors. */
22
+ function ModelOptionRows({ provider, model, options }) {
23
+ const setOption = (id, value) => {
24
+ rememberChatSelection(provider, { model: model.slug, options: { ...options, [id]: value } });
25
+ };
26
+ return (_jsx(_Fragment, { children: model.options.map((option) => {
27
+ if (option.type !== 'select') {
28
+ return (_jsx(SettingsRow, { indent: true, muted: true, label: option.label, control: _jsx(Toggle, { checked: options[option.id] === true, label: option.label, onChange: (checked) => setOption(option.id, checked) }) }, option.id));
29
+ }
30
+ const value = String(options[option.id] ?? option.defaultChoice);
31
+ return (_jsx(SettingsRow, { indent: true, muted: true, label: option.label, control: option.choices.length <= MAX_SEGMENTS ? (_jsx(Segmented, { value: value, label: option.label, options: option.choices.map((choice) => ({ id: choice.id, label: choice.label })), onChange: (id) => setOption(option.id, id) })) : (_jsx(Select, { value: value, label: option.label, align: "end", items: option.choices.map((choice) => ({ value: choice.id, label: choice.label, description: choice.description })), onValueChange: (id) => setOption(option.id, id) })) }, option.id));
32
+ }) }));
33
+ }
34
+ /* One CLI: what it is, what a new chat of it starts with, and its accounts. */
35
+ export function CliDetail({ provider, entries, canAddAccount, onAddAccount, onPickAccount }) {
36
+ const { t } = useTranslation('agent-providers');
37
+ const { id: scopeId } = useChatScope();
38
+ const preferences = useChatPreferences();
39
+ const selection = selectionFor(preferences, provider.kind);
40
+ const model = provider.models.find((entry) => entry.slug === selection?.model);
41
+ const machineAccounts = useProviderAccountsStore((s) => (s.byScope[scopeId]?.loaded ? s.byScope[scopeId].accounts : undefined));
42
+ const picked = accountFor(preferences, scopeId, provider.kind, machineAccounts) ?? provider.kind;
43
+ // An account that is off is out of every picker, unless it is the one picked already.
44
+ const choosable = (entries ?? []).filter((entry) => entry.account.enabled !== false || entry.id === picked);
45
+ const canLogIn = canAddAccount;
46
+ return (_jsxs(_Fragment, { children: [_jsx(DetailHeader, { mark: _jsx(CliTile, { kind: provider.kind }), title: provider.name, subtitle: `${provider.version ? `${t('abilities.version', { version: provider.version })} ` : ''}${providerAbilities(provider)}`, actions: canAddAccount && (_jsxs(Button, { variant: "secondary", className: "whitespace-nowrap", onClick: onAddAccount, children: [_jsx(Icon, { icon: Plus, size: 14 }), t('cli.addAccount')] })) }), _jsxs(SettingsSection, { title: t('cli.defaults.title'), description: t('cli.defaults.description'), children: [choosable.length > 0 && (_jsx(SettingsRow, { searchId: "providers.defaults.account", label: t('cli.defaults.account'), control: _jsx(Select, { value: picked, label: t('cli.defaults.accountFor', { provider: provider.name }), align: "end", items: choosable.map((entry) => ({
47
+ value: entry.id,
48
+ label: accountName(entry, provider.name),
49
+ icon: _jsx(AccountDot, { color: entry.account.color, className: "size-2" })
50
+ })), onValueChange: (id) => rememberChatAccount(scopeId, provider.kind, id) }) })), provider.models.length > 0 && (_jsx(SettingsRow, { searchId: "providers.defaults.model", label: t('cli.defaults.model'), description: model ? t('cli.defaults.shared', { provider: provider.name }) : t('cli.defaults.usesCliDefault'), control: _jsx(Select, { value: model?.slug ?? PROVIDER_DEFAULT, label: t('cli.defaults.modelFor', { provider: provider.name }), align: "end", items: [
51
+ { value: PROVIDER_DEFAULT, label: t('cli.defaults.providerDefault') },
52
+ ...provider.models.map((entry) => ({
53
+ value: entry.slug,
54
+ label: entry.legacy ? t('cli.defaults.legacyModel', { name: entry.name }) : entry.name
55
+ }))
56
+ ], onValueChange: (value) => value === PROVIDER_DEFAULT
57
+ ? forgetChatSelection(provider.kind)
58
+ : rememberChatSelection(provider.kind, { model: value, options: {} }) }) })), model && _jsx(ModelOptionRows, { provider: provider.kind, model: model, options: selection?.options ?? {} }), choosable.length === 0 && provider.models.length === 0 && _jsx(SettingsRow, { muted: true, label: t('cli.defaults.usesCliDefault') })] }), entries === null ? (_jsx("p", { className: "text-xs text-text-faint", children: t('list.noAccounts') })) : (_jsx(SettingsSection, { title: t('cli.accounts'), footer: canAddAccount ? undefined : t('cli.onlyDefault', { provider: provider.name }), children: entries.map((entry) => {
59
+ const status = accountStatusLine(entry.status, canLogIn);
60
+ return (_jsxs("button", { type: "button", className: "flex w-full min-w-0 items-center gap-3 px-4.5 py-3 text-left first:rounded-t-xl last:rounded-b-xl hover:bg-surface-hover", onClick: () => onPickAccount(entry.id), children: [_jsx(AccountDot, { color: entry.account.color }), _jsx("span", { className: "min-w-0 grow truncate text-sm text-text", children: accountName(entry, provider.name) }), _jsx("span", { className: `shrink-0 text-xs ${ACCOUNT_TONE_CLASSES[status.tone]}`, children: status.text }), _jsx(Icon, { icon: ChevronRight, size: 14, className: "shrink-0 text-text-faint" })] }, entry.id));
61
+ }) }))] }));
62
+ }
@@ -0,0 +1,8 @@
1
+ import { type ReactNode } from 'react';
2
+ import type { ProviderInfo } from '@ruimte/agent-contracts';
3
+ interface ProvidersPaneProps {
4
+ target?: string | null;
5
+ detailOf?(provider: ProviderInfo): ReactNode | null;
6
+ }
7
+ export declare function ProvidersPane({ target, detailOf }: ProvidersPaneProps): import("react").JSX.Element;
8
+ export {};
@@ -0,0 +1,90 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMemo, useState } from 'react';
3
+ import clsx from 'clsx';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { Plus, Terminal } from 'lucide-react';
6
+ import { AccountDot } from '../agents/AccountDot';
7
+ import { ACCOUNT_TONE_CLASSES, accountName, accountsOfKind, accountStatusLine } from '../agents/accounts';
8
+ import { Skeleton } from '@ruimte/ui/controls';
9
+ import { MasterDetail } from '@ruimte/ui/settings/MasterDetail';
10
+ import { chatHost } from '../host';
11
+ import { useChatScope } from '../scope';
12
+ import { AccountDetail } from './AccountDetail';
13
+ import { AddAccountForm } from './AddAccountForm';
14
+ import { CliDetail } from './CliDetail';
15
+ import { CliMark } from './parts';
16
+ import { useProviderAccounts } from '../state/provider-accounts';
17
+ import { useProviders } from '../state/providers';
18
+ import { SECTION_LABEL } from '@ruimte/ui/classes';
19
+ import { ErrorBoundary } from '@ruimte/ui/ErrorBoundary';
20
+ import { Icon } from '@ruimte/ui/Icon';
21
+ import { Tooltip } from '@ruimte/ui/Tooltip';
22
+ /* A row of the list with a control of its own beside the part that picks it, so the two never nest. */
23
+ function ListRow({ selected, onSelect, children, trailing, className }) {
24
+ return (_jsxs("div", { className: clsx('flex min-w-0 shrink-0 items-center gap-2 rounded-lg pr-2', selected ? 'bg-text/5' : 'hover:bg-surface-hover', className), children: [_jsx("button", { type: "button", "aria-current": selected ? 'true' : undefined, className: "flex min-w-0 grow items-center gap-3 self-stretch rounded-lg py-2 pl-3 text-left focus-visible:-outline-offset-2", onClick: onSelect, children: children }), trailing] }));
25
+ }
26
+ /* The agent CLIs of the host in scope, each with its accounts, beside the detail of what is picked. */
27
+ export function ProvidersPane({ target = null, detailOf }) {
28
+ const { t } = useTranslation('agent-providers');
29
+ const { id: scopeId } = useChatScope();
30
+ const providers = useProviders((s) => s.providers);
31
+ const loaded = useProviders((s) => s.loaded);
32
+ const accounts = useProviderAccounts((s) => s.accounts);
33
+ const { openLogin } = chatHost();
34
+ const loginBlocked = chatHost().useLoginBlocked(scopeId);
35
+ const [picked, setPicked] = useState(() => (target === 'providers.apple' ? { kind: 'cli', cli: 'apple' } : null));
36
+ const installed = useMemo(() => providers.filter((provider) => provider.installed || provider.kind === 'apple'), [providers]);
37
+ const missing = useMemo(() => providers.filter((provider) => !provider.installed && provider.kind !== 'apple'), [providers]);
38
+ const entriesOf = (kind) => accountsOfKind(accounts, kind);
39
+ const canAddAccount = (kind) => accounts?.loginCommands?.[kind] !== undefined;
40
+ // What was picked may be gone since (an account removed elsewhere); the first CLI stands in for it.
41
+ const pickedEntry = picked?.kind === 'account' ? (Object.hasOwn(accounts?.accounts ?? {}, picked.id) ? picked.id : null) : null;
42
+ const current = picked !== null && (picked.kind !== 'account' || pickedEntry !== null) ? picked : installed[0] ? { kind: 'cli', cli: installed[0].kind } : null;
43
+ const currentAccount = current?.kind === 'account' ? accounts?.accounts[current.id] : undefined;
44
+ const currentCli = current === null ? null : current.kind === 'account' ? currentAccount?.kind : current.cli;
45
+ const provider = installed.find((entry) => entry.kind === currentCli) ?? null;
46
+ // A search result lands on a row of a CLI's detail, so a picked account makes way for its CLI.
47
+ const [seenTarget, setSeenTarget] = useState(target);
48
+ if (target !== seenTarget) {
49
+ setSeenTarget(target);
50
+ if (target === 'providers.apple') {
51
+ setPicked({ kind: 'cli', cli: 'apple' });
52
+ }
53
+ else if (target?.startsWith('providers.') && current?.kind !== 'cli' && provider !== null) {
54
+ setPicked({ kind: 'cli', cli: provider.kind });
55
+ }
56
+ }
57
+ const added = (id, name, made) => {
58
+ setPicked({ kind: 'account', id });
59
+ if (made && provider !== null && openLogin !== null && loginBlocked === null) {
60
+ void openLogin(scopeId, provider.kind, id, name);
61
+ }
62
+ };
63
+ const group = (cli) => {
64
+ const entries = cli.installed ? entriesOf(cli.kind) : [];
65
+ return (_jsxs("div", { className: "flex min-w-0 shrink-0 flex-col gap-0.5 pt-1", children: [_jsxs(ListRow, { selected: current?.kind === 'cli' && current.cli === cli.kind, onSelect: () => setPicked({ kind: 'cli', cli: cli.kind }), className: "pr-1", trailing: canAddAccount(cli.kind) && (_jsx(Tooltip, { label: t('list.addAccount', { provider: cli.name }), name: true, children: _jsx("button", { type: "button", className: "icon-btn icon-btn-sm", onClick: () => setPicked({ kind: 'add', cli: cli.kind }), children: _jsx(Icon, { icon: Plus, size: 14 }) }) })), children: [_jsx(CliMark, { kind: cli.kind }), _jsx("span", { className: "min-w-0 grow truncate text-xs font-medium text-text", children: cli.name }), cli.kind !== 'apple' && cli.version && _jsx("span", { className: "shrink-0 font-mono text-code text-text-faint", children: cli.version })] }), entries.map((entry) => {
66
+ const name = accountName(entry, cli.name);
67
+ const status = accountStatusLine(entry.status, canAddAccount(cli.kind));
68
+ return (_jsxs(ListRow, { selected: current?.kind === 'account' && current.id === entry.id, onSelect: () => setPicked({ kind: 'account', id: entry.id }), children: [_jsx("span", { className: "grid w-3.5 shrink-0 place-items-center", children: _jsx(AccountDot, { color: entry.account.color, className: "size-2.25" }) }), _jsxs("span", { className: "flex min-w-0 grow flex-col", children: [_jsxs("span", { className: "flex min-w-0 items-center gap-2 text-sm text-text", children: [_jsx("span", { className: "truncate", children: name }), entry.isDefault && (_jsx("span", { className: "shrink-0 rounded-md bg-surface-active px-1.5 text-xs text-text-muted", children: t('list.default') }))] }), _jsx("span", { className: clsx('truncate text-xs', ACCOUNT_TONE_CLASSES[status.tone]), children: status.text })] })] }, entry.id));
69
+ })] }, cli.kind));
70
+ };
71
+ const list = !loaded ? (_jsxs("div", { className: "flex flex-col gap-3 p-3", children: [_jsx(Skeleton, { className: "w-32" }), _jsx(Skeleton, { className: "w-40" })] })) : (_jsxs(_Fragment, { children: [installed.map(group), installed.length === 0 && _jsx("p", { className: "px-3 py-2 text-xs text-text-muted", children: t('list.none') }), missing.length > 0 && (_jsxs("div", { className: "mt-1.5 flex min-w-0 shrink-0 flex-col border-t border-border pt-2.5 pb-2", children: [_jsx("span", { className: clsx(SECTION_LABEL, 'px-3 pt-1 pb-1.5'), children: t('list.notInstalled') }), missing.map((cli) => (_jsxs("div", { className: "flex h-10 min-w-0 items-center gap-3 px-3 text-sm text-text-muted", children: [_jsx(Icon, { icon: Terminal, size: 14, className: "shrink-0" }), _jsx("span", { className: "min-w-0 grow truncate", children: cli.name }), _jsx("span", { className: "shrink-0 text-xs text-text-faint", children: t('list.missing') })] }, cli.kind))), _jsx("p", { className: "mt-1 px-3 text-xs text-text-faint", children: t('list.notInstalledNote') })] }))] }));
72
+ const detail = () => {
73
+ if (current === null || provider === null) {
74
+ return null;
75
+ }
76
+ const own = current.kind === 'cli' ? (detailOf?.(provider) ?? null) : null;
77
+ if (own !== null) {
78
+ return own;
79
+ }
80
+ if (current.kind === 'add') {
81
+ return (_jsx(AddAccountForm, { provider: provider, entries: entriesOf(provider.kind), onCancel: () => setPicked({ kind: 'cli', cli: provider.kind }), onAdded: added }, provider.kind));
82
+ }
83
+ if (current.kind === 'account') {
84
+ const entry = entriesOf(provider.kind).find((candidate) => candidate.id === current.id);
85
+ return (entry && (_jsx(AccountDetail, { provider: provider, entry: entry, canLogIn: openLogin !== null && canAddAccount(provider.kind), loginBlocked: loginBlocked, secretsAvailable: accounts?.secretsAvailable === true, onRemoved: () => setPicked({ kind: 'cli', cli: provider.kind }) }, entry.id)));
86
+ }
87
+ return (_jsx(CliDetail, { provider: provider, entries: accounts === null ? null : entriesOf(provider.kind), canAddAccount: canAddAccount(provider.kind), onAddAccount: () => setPicked({ kind: 'add', cli: provider.kind }), onPickAccount: (id) => setPicked({ kind: 'account', id }) }));
88
+ };
89
+ return (_jsx(MasterDetail, { listWidth: 340, listLabel: t('list.label'), list: list, detail: _jsx(ErrorBoundary, { label: t('failed'), resetKeys: [scopeId, current?.kind, currentCli], children: _jsx("div", { className: "flex min-w-0 flex-col gap-7", children: detail() }) }) }));
90
+ }
@@ -0,0 +1,7 @@
1
+ import type { AgentKind, ProviderAccount, ProviderAccountMap } from '@ruimte/agent-contracts';
2
+ import type { ChatScope } from '../scope';
3
+ export declare const saveAccounts: (scope: ChatScope, accounts: ProviderAccountMap) => Promise<boolean>;
4
+ export declare const saveAccount: (scope: ChatScope, id: string, account: ProviderAccount) => Promise<boolean>;
5
+ export declare const removeAccount: (scope: ChatScope, id: string, kind: AgentKind) => Promise<void>;
6
+ export declare const createAccount: (scope: ChatScope, kind: AgentKind, label: string, color: string) => Promise<string>;
7
+ export declare const linkAccount: (scope: ChatScope, id: string, account: ProviderAccount) => Promise<void>;
@@ -0,0 +1,53 @@
1
+ import i18next from 'i18next';
2
+ import { accountFor, readChatPreferences, rememberChatAccount } from '../chat/preferences';
3
+ import { chatHost } from '../host';
4
+ import { providerAccountsOf, useProviderAccountsStore } from '../state/provider-accounts';
5
+ const words = (key, values) => i18next.t(`agent-providers:${key}`, values);
6
+ const reason = (e) => (e instanceof Error ? e.message : String(e));
7
+ const linkTo = (scope) => {
8
+ if (scope.transport.status !== 'open') {
9
+ throw new Error(words('notAnswering'));
10
+ }
11
+ return scope.transport;
12
+ };
13
+ /* The whole map, as `accounts.save` takes it. Answers false when the host refused, which a toast then says. */
14
+ export const saveAccounts = async (scope, accounts) => {
15
+ try {
16
+ useProviderAccountsStore.getState().set(scope.id, await linkTo(scope).request('accounts.save', { accounts }));
17
+ return true;
18
+ }
19
+ catch (e) {
20
+ chatHost().notify({ id: `providers-save-${scope.id}`, kind: 'error', title: words('account.saveFailed'), description: reason(e) });
21
+ return false;
22
+ }
23
+ };
24
+ /* One account changed and every other one as the host last said. */
25
+ export const saveAccount = (scope, id, account) => {
26
+ const current = providerAccountsOf(scope.id).accounts?.accounts ?? {};
27
+ return saveAccounts(scope, { ...current, [id]: account });
28
+ };
29
+ /*
30
+ * Forgets an account; its folder stays where it is. A pick for new agents that named it goes back to
31
+ * the CLI's default account, since no chat started from it yet.
32
+ */
33
+ export const removeAccount = async (scope, id, kind) => {
34
+ const { [id]: _gone, ...rest } = providerAccountsOf(scope.id).accounts?.accounts ?? {};
35
+ if (!(await saveAccounts(scope, rest))) {
36
+ throw new Error(words('account.saveFailed'));
37
+ }
38
+ if (accountFor(readChatPreferences(), scope.id, kind) === id) {
39
+ rememberChatAccount(scope.id, kind, null);
40
+ }
41
+ };
42
+ /* An account in a folder the host makes for it; answers its id. Throws what the host said, for the form to show. */
43
+ export const createAccount = async (scope, kind, label, color) => {
44
+ const created = await linkTo(scope).request('accounts.create', { kind, label, color });
45
+ const { id, ...accounts } = created;
46
+ useProviderAccountsStore.getState().set(scope.id, accounts);
47
+ return id;
48
+ };
49
+ /* An account in a folder that is already on the host. Throws what the host said, for the form to show. */
50
+ export const linkAccount = async (scope, id, account) => {
51
+ const current = providerAccountsOf(scope.id).accounts?.accounts ?? {};
52
+ useProviderAccountsStore.getState().set(scope.id, await linkTo(scope).request('accounts.save', { accounts: { ...current, [id]: account } }));
53
+ };
@@ -0,0 +1,9 @@
1
+ import type { AgentKind } from '@ruimte/agent-contracts';
2
+ export declare function CliMark({ kind, size, className }: {
3
+ kind: AgentKind;
4
+ size?: number;
5
+ className?: string;
6
+ }): import("react").JSX.Element;
7
+ export declare function CliTile({ kind }: {
8
+ kind: AgentKind;
9
+ }): import("react").JSX.Element;
@@ -0,0 +1,18 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import clsx from 'clsx';
3
+ import { AgentIcon } from '../agents/AgentIcon';
4
+ // Each CLI in its color of the usage charts, so a mark here reads as the same CLI there.
5
+ const CLI_COLORS = {
6
+ apple: 'text-text-muted',
7
+ claude: 'text-chart-claude',
8
+ codex: 'text-chart-codex',
9
+ gemini: 'text-chart-gemini',
10
+ copilot: 'text-chart-copilot'
11
+ };
12
+ export function CliMark({ kind, size = 14, className }) {
13
+ return _jsx(AgentIcon, { kind: kind, size: size, className: clsx('shrink-0', CLI_COLORS[kind], className) });
14
+ }
15
+ /* As tall as the title's first line, so the mark sits beside the name rather than between the two lines. */
16
+ export function CliTile({ kind }) {
17
+ return (_jsx("span", { className: "grid h-6 shrink-0 place-items-center", children: _jsx(CliMark, { kind: kind, size: 24 }) }));
18
+ }
@@ -0,0 +1,2 @@
1
+ import type { ProviderInfo } from '@ruimte/agent-contracts';
2
+ export declare const providerAbilities: (provider: ProviderInfo) => string;