@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,56 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from 'react';
3
+ import clsx from 'clsx';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { Bookmark, Check, Copy, GitFork } from 'lucide-react';
6
+ import { placeBookmark } from '../bookmarks';
7
+ import { forkRefusal } from '../logic/fork';
8
+ import { markdownOf, messageTextOf } from '../logic/timeline-copy';
9
+ import { chatHost } from '../../host';
10
+ import { useChatScope } from '../../scope';
11
+ import { useChatRow, useChats } from '../../state/chats';
12
+ import { BTN_GROUP } from '@ruimte/ui/classes';
13
+ import { copyText } from '@ruimte/ui/clipboard';
14
+ import { Icon } from '@ruimte/ui/Icon';
15
+ import { Tooltip } from '@ruimte/ui/Tooltip';
16
+ const COPIED_MS = 1500;
17
+ /*
18
+ * Bookmark, fork and copy under a message, shown while the pointer or the focus is on a
19
+ * `group/message`. The row always takes its height, so the virtualizer never measures it again on a
20
+ * hover. Like the strip's card, it leaves out an action the message cannot take right now.
21
+ */
22
+ export function MessageActions({ chatId, row }) {
23
+ const { t } = useTranslation('agent-chat');
24
+ const scope = useChatScope();
25
+ const { fork } = chatHost();
26
+ const turnId = row.item.turnId;
27
+ const bookmark = useChatRow(chatId, (chat) => chat?.bookmarks?.find((candidate) => candidate.itemId === row.id) ?? null);
28
+ const canFork = useChatRow(chatId, (chat) => turnId !== null && forkRefusal(chat?.info ?? null, chat?.structure[turnId]) === null);
29
+ const streaming = useChatRow(chatId, (chat) => {
30
+ const item = chat?.items[row.id];
31
+ return item?.kind === 'assistant' && item.streaming;
32
+ });
33
+ const hasText = useChatRow(chatId, (chat) => {
34
+ const item = chat?.items[row.id];
35
+ return (item?.kind === 'user' || item?.kind === 'assistant') && item.text.trim() !== '';
36
+ });
37
+ const [copied, setCopied] = useState(false);
38
+ useEffect(() => {
39
+ if (!copied) {
40
+ return;
41
+ }
42
+ const timer = setTimeout(() => setCopied(false), COPIED_MS);
43
+ return () => clearTimeout(timer);
44
+ }, [copied]);
45
+ // The row was derived from the structure, which a delta leaves alone; the text to copy is the one held now.
46
+ const copy = (plain) => {
47
+ const item = useChats.getState().byKey[scope.keyOf(chatId)]?.items[row.id];
48
+ const current = item?.kind === row.kind ? { ...row, item } : row;
49
+ const text = (plain ? null : markdownOf(current)) ?? messageTextOf(current);
50
+ if (text !== null) {
51
+ copyText(text);
52
+ setCopied(true);
53
+ }
54
+ };
55
+ return (_jsx("div", { role: "toolbar", "aria-label": t('timeline.actions.label'), className: clsx('flex h-7 items-center opacity-0 transition-opacity group-has-focus-visible/message:opacity-100 group-hover/message:opacity-100', row.kind === 'user' ? '-mr-1.5 mt-1.5 justify-end' : '-ml-1.5'), children: !streaming && (_jsxs("span", { className: BTN_GROUP, children: [bookmark === null && (_jsx(Tooltip, { label: t('bookmarks.add'), name: true, children: _jsx("button", { type: "button", className: "icon-btn icon-btn-sm", onClick: () => void placeBookmark(scope, chatId, row.id), children: _jsx(Icon, { icon: Bookmark, size: 14 }) }) })), canFork && turnId !== null && fork !== null && (_jsx(Tooltip, { label: t('timeline.menu.forkFromHere'), name: true, children: _jsx("button", { type: "button", className: "icon-btn icon-btn-sm", onClick: () => fork(chatId, turnId), children: _jsx(Icon, { icon: GitFork, size: 14 }) }) })), hasText && (_jsx(Tooltip, { label: copied ? t('timeline.actions.copied') : t('timeline.menu.copyMessage'), name: true, children: _jsx("button", { type: "button", className: "icon-btn icon-btn-sm", onClick: (e) => copy(e.shiftKey), children: _jsx(Icon, { icon: copied ? Check : Copy, size: 14 }) }) }))] })) }));
56
+ }
@@ -0,0 +1,15 @@
1
+ import type { AgentKind, ModelSelection, ProviderInfo } from '@ruimte/agent-contracts';
2
+ import { type StashedPrompt } from '../stash';
3
+ interface ModelPickerProps {
4
+ providers: ProviderInfo[];
5
+ provider: AgentKind;
6
+ selection: ModelSelection;
7
+ open: boolean;
8
+ onOpenChange(open: boolean): void;
9
+ onChange(provider: AgentKind, model: string): void;
10
+ }
11
+ export declare function ModelPicker({ providers, provider, selection, open, onOpenChange, onChange }: ModelPickerProps): import("react").JSX.Element;
12
+ export declare function StashPicker({ onRestore }: {
13
+ onRestore(prompt: StashedPrompt): void;
14
+ }): import("react").JSX.Element | null;
15
+ export {};
@@ -0,0 +1,110 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useRef, useState } from 'react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { Popover } from '@base-ui-components/react/popover';
5
+ import { Bookmark, ChevronDown, ChevronRight, Search, Trash2 } from 'lucide-react';
6
+ import { AgentIcon } from '../../agents/AgentIcon';
7
+ import { modelName } from '../../agents/model-name';
8
+ import { forgetStashed, STASH_SHORTCUT, useStash } from '../stash';
9
+ import { MENU_LABEL } from '@ruimte/ui/classes';
10
+ import { Tooltip } from '@ruimte/ui/Tooltip';
11
+ import { Icon } from '@ruimte/ui/Icon';
12
+ import { MenuCheck } from '@ruimte/ui/MenuCheck';
13
+ const triggerClass = 'flex h-7 shrink-0 items-center gap-1 whitespace-nowrap rounded-md px-2 text-xs text-text-muted hover:bg-surface-hover hover:text-text data-[popup-open]:bg-surface-active data-[popup-open]:text-text';
14
+ const matches = (model, query) => model.name.toLowerCase().includes(query) || model.slug.toLowerCase().includes(query);
15
+ /*
16
+ * Which model answers: the provider's mark plus the model's short name
17
+ * as the trigger, a search field over the popup, one group per provider and the legacy models
18
+ * behind an expander. Before the first message the list carries every installed CLI, so picking
19
+ * another provider's model is also how a chat picks its provider. A chat bound to one CLI passes
20
+ * only that CLI, which drops the group headers and leaves its own catalog to choose from.
21
+ */
22
+ export function ModelPicker({ providers, provider, selection, open, onOpenChange, onChange }) {
23
+ const { t } = useTranslation('agent-chat');
24
+ const [query, setQuery] = useState('');
25
+ const [legacyOpen, setLegacyOpen] = useState(false);
26
+ const [index, setIndex] = useState(0);
27
+ const inputRef = useRef(null);
28
+ const listRef = useRef(null);
29
+ const owner = providers.find((entry) => entry.kind === provider);
30
+ const current = modelName(selection.model, owner?.models);
31
+ const grouped = providers.length > 1;
32
+ const trimmed = query.trim().toLowerCase();
33
+ const entries = useMemo(() => {
34
+ const rows = providers.flatMap((entry) => entry.models.map((model) => ({ kind: 'model', provider: entry, model })));
35
+ if (trimmed !== '') {
36
+ return rows.filter((row) => matches(row.model, trimmed));
37
+ }
38
+ const live = rows.filter((row) => !row.model.legacy);
39
+ const legacy = rows.filter((row) => row.model.legacy);
40
+ if (legacy.length === 0) {
41
+ return live;
42
+ }
43
+ return legacyOpen ? [...live, { kind: 'legacy', count: legacy.length }, ...legacy] : [...live, { kind: 'legacy', count: legacy.length }];
44
+ }, [providers, trimmed, legacyOpen]);
45
+ /* Closing forgets the search, so the next open (a click or `/model`) starts on the whole list. */
46
+ const setOpen = (next) => {
47
+ if (!next) {
48
+ setQuery('');
49
+ setLegacyOpen(false);
50
+ }
51
+ setIndex(0);
52
+ onOpenChange(next);
53
+ };
54
+ // The list scrolls, so the row the arrows moved to has to come along.
55
+ useEffect(() => {
56
+ listRef.current?.querySelector('[data-active="true"]')?.scrollIntoView({ block: 'nearest' });
57
+ }, [index]);
58
+ const choose = (entry) => {
59
+ if (!entry) {
60
+ return;
61
+ }
62
+ if (entry.kind === 'legacy') {
63
+ setLegacyOpen(true);
64
+ inputRef.current?.focus();
65
+ return;
66
+ }
67
+ onChange(entry.provider.kind, entry.model.slug);
68
+ setOpen(false);
69
+ };
70
+ const onKeyDown = (e) => {
71
+ if (e.key === 'ArrowDown') {
72
+ e.preventDefault();
73
+ setIndex((i) => (entries.length === 0 ? 0 : (i + 1) % entries.length));
74
+ }
75
+ else if (e.key === 'ArrowUp') {
76
+ e.preventDefault();
77
+ setIndex((i) => (entries.length === 0 ? 0 : (i - 1 + entries.length) % entries.length));
78
+ }
79
+ else if (e.key === 'Enter') {
80
+ e.preventDefault();
81
+ choose(entries[index]);
82
+ }
83
+ };
84
+ return (_jsxs(Popover.Root, { open: open, onOpenChange: setOpen, children: [_jsx(Tooltip, { label: owner ? `${owner.name} · ${current}` : t('pickers.model.choose'), kbd: "/model", children: _jsxs(Popover.Trigger, { className: triggerClass, children: [_jsx(AgentIcon, { kind: provider, size: 12 }), _jsx("span", { className: "max-w-40 truncate", children: current }), _jsx(Icon, { icon: ChevronDown, size: 12, className: "text-text-faint" })] }) }), _jsx(Popover.Portal, { children: _jsx(Popover.Positioner, { className: "z-(--z-popup)", side: "top", sideOffset: 8, align: "start", children: _jsxs(Popover.Popup, { className: "picker-popup", initialFocus: inputRef, children: [_jsxs("div", { className: "flex items-center gap-2 border-b border-border px-2.5", children: [_jsx(Icon, { icon: Search, size: 14, className: "shrink-0 text-text-faint" }), _jsx("input", { ref: inputRef, className: "h-8 w-full bg-transparent text-sm text-text outline-none placeholder:text-text-faint", placeholder: t('pickers.model.search'), spellCheck: false, value: query, onChange: (e) => {
85
+ setQuery(e.target.value);
86
+ setIndex(0);
87
+ }, onKeyDown: onKeyDown })] }), _jsxs("div", { ref: listRef, className: "max-h-72 overflow-auto p-1", role: "listbox", children: [entries.length === 0 && (_jsx("div", { className: "px-3 py-6 text-center text-xs text-text-faint", children: providers.length === 0 ? t('pickers.model.noProvider') : t('pickers.model.noMatch') })), entries.map((entry, i) => {
88
+ if (entry.kind === 'legacy') {
89
+ return (_jsxs("button", { "data-active": i === index, className: "cursor-row flex w-full items-center gap-2 rounded-md px-2.5 py-1.5 text-left text-xs text-text-faint", onMouseEnter: () => setIndex(i), onClick: () => choose(entry), children: [_jsx(Icon, { icon: legacyOpen ? ChevronDown : ChevronRight, size: 12 }), t('pickers.model.legacyGroup', { count: entry.count })] }, "legacy"));
90
+ }
91
+ const previous = entries[i - 1];
92
+ const first = grouped && (previous === undefined || previous.kind !== 'model' || previous.provider.kind !== entry.provider.kind);
93
+ const chosen = entry.provider.kind === provider && entry.model.slug === selection.model;
94
+ return (_jsxs("div", { children: [first && (_jsxs("div", { className: `${MENU_LABEL} flex items-center gap-1.5`, children: [_jsx(AgentIcon, { kind: entry.provider.kind, size: 12 }), entry.provider.name] })), _jsxs("button", { role: "option", "aria-selected": chosen, "data-active": i === index, className: "cursor-row flex w-full items-center gap-2 rounded-md px-2.5 py-1.5 text-left text-sm text-text-muted", onMouseEnter: () => setIndex(i), onClick: () => choose(entry), children: [_jsx(MenuCheck, { kind: "radio", checked: chosen }), _jsx("span", { className: "min-w-0 truncate", children: entry.model.name }), entry.model.badge && (_jsx("span", { className: "rounded bg-accent-soft px-1 text-xs font-medium text-accent", children: entry.model.badge })), _jsx("span", { className: "grow" }), entry.model.legacy && _jsx("span", { className: "text-xs text-text-faint", children: t('pickers.model.legacy') })] })] }, `${entry.provider.kind}/${entry.model.slug}`));
95
+ })] })] }) }) })] }));
96
+ }
97
+ /*
98
+ * Prompts put aside with Cmd+S. The shelf is the whole app's, not this chat's. On a canvas a
99
+ * stashed prompt usually moves to another node, which is the reason to put it away in the first
100
+ * place. Restoring drops the text, the mentions and the skills into this composer; the files a
101
+ * draft held are named on the row but not kept, because their bytes never go to storage.
102
+ */
103
+ export function StashPicker({ onRestore }) {
104
+ const { t } = useTranslation('agent-chat');
105
+ const prompts = useStash((s) => s.prompts);
106
+ if (prompts.length === 0) {
107
+ return null;
108
+ }
109
+ return (_jsxs(Popover.Root, { children: [_jsx(Tooltip, { label: t('pickers.stash.title'), kbd: STASH_SHORTCUT, children: _jsxs(Popover.Trigger, { className: triggerClass, children: [_jsx(Icon, { icon: Bookmark, size: 12 }), _jsx("span", { className: "tabular-nums", children: prompts.length })] }) }), _jsx(Popover.Portal, { children: _jsx(Popover.Positioner, { className: "z-(--z-popup)", side: "top", sideOffset: 8, align: "start", children: _jsx(Popover.Popup, { className: "picker-popup w-80", children: _jsx("div", { className: "max-h-72 overflow-auto p-1", children: prompts.map((prompt) => (_jsxs("div", { className: "group/stash flex items-start gap-1", children: [_jsxs(Popover.Close, { className: "flex min-w-0 grow flex-col gap-0.5 rounded-md px-2 py-1.5 text-left text-xs text-text-muted hover:bg-surface-hover hover:text-text", onClick: () => onRestore(prompt), children: [_jsx("span", { className: "line-clamp-2 whitespace-pre-wrap", children: prompt.text || t('pickers.stash.noText') }), prompt.attachments.length > 0 && (_jsx("span", { className: "text-text-faint", children: t('pickers.stash.files', { count: prompt.attachments.length }) }))] }), _jsx(Tooltip, { label: t('common.action.delete'), name: true, children: _jsx("button", { className: "icon-btn icon-btn-xs mt-1 opacity-0 group-hover/stash:opacity-100 focus-visible:opacity-100", onClick: () => forgetStashed(prompt.id), children: _jsx(Icon, { icon: Trash2, size: 12 }) }) })] }, prompt.id))) }) }) }) })] }));
110
+ }
@@ -0,0 +1,13 @@
1
+ import { type ReactNode } from 'react';
2
+ import { type PendingPrompt } from '../../prompts/logic/prompts';
3
+ export declare function PromptComposer({ chatId, pending, focused, onShowElsewhere, disabled, hasDraft, denyReason, onAllAnswered, children }: {
4
+ chatId: string;
5
+ pending: PendingPrompt[];
6
+ focused: boolean;
7
+ onShowElsewhere?: () => void;
8
+ disabled: boolean;
9
+ hasDraft: boolean;
10
+ denyReason: boolean;
11
+ onAllAnswered?: () => void;
12
+ children: ReactNode;
13
+ }): import("react").JSX.Element;
@@ -0,0 +1,48 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useRef } from 'react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { ArrowDown, Hand, MessageCircleQuestionMark } from 'lucide-react';
5
+ import { focusPromptStart } from '../../prompts/logic/focus';
6
+ import { orderPrompts } from '../../prompts/logic/prompts';
7
+ import { answerPrompt, isBlockingSubject, promptCreatedAt, promptIdOf } from '../../prompts/logic/subjects';
8
+ import { useFocusAfterAnswer } from '../../prompts/logic/useFocusAfterAnswer';
9
+ import { usePromptSession } from '../../prompts/logic/usePromptSession';
10
+ import { PromptView } from '../../prompts/ui/PromptView';
11
+ import { chatHost } from '../../host';
12
+ import { useChatScope } from '../../scope';
13
+ import { useChatActions } from '../actions';
14
+ import { Icon } from '@ruimte/ui/Icon';
15
+ /* The one being read stays in front; otherwise blocking before optional, oldest first, as everywhere else. */
16
+ const pickSubject = (waiting, activeId) => waiting.find((subject) => promptIdOf(subject) === activeId) ?? orderPrompts(waiting, isBlockingSubject, promptCreatedAt)[0] ?? null;
17
+ export function PromptComposer({ chatId, pending, focused, onShowElsewhere, disabled, hasDraft, denyReason, onAllAnswered, children }) {
18
+ const { t } = useTranslation('agent-chat');
19
+ const { id } = useChatScope();
20
+ const actions = useChatActions();
21
+ const elsewhere = onShowElsewhere !== undefined;
22
+ // A card the app raises itself waits in the chat beside the CLI's own requests.
23
+ const extra = chatHost().prompts.useExtra(id, chatId);
24
+ const subjects = useMemo(() => [
25
+ ...pending.map((item) => ({ kind: 'chat', nodeId: chatId, item })),
26
+ ...extra.map((prompt) => ({ kind: 'host', nodeId: chatId, prompt }))
27
+ ], [pending, extra, chatId]);
28
+ const session = usePromptSession({ prompts: subjects, idOf: promptIdOf, pick: pickSubject, disabled });
29
+ const ref = useRef(null);
30
+ const { active, activeId: activeKey } = session;
31
+ const expanded = !!active;
32
+ const refocus = useFocusAfterAnswer(activeKey, ref, () => onAllAnswered?.());
33
+ useEffect(() => {
34
+ if (focused && expanded && !elsewhere && !ref.current?.querySelector('.prompt-card')?.contains(document.activeElement)) {
35
+ focusPromptStart(ref.current);
36
+ }
37
+ }, [activeKey, expanded, focused, elsewhere]);
38
+ return (_jsxs("div", { ref: ref, onPointerDownCapture: session.onPointerDownCapture, onClickCapture: session.onClickCapture, children: [active && elsewhere && (_jsxs("button", { type: "button", className: "flex w-full items-center gap-2 px-3.5 py-3 text-left text-sm text-text-muted hover:text-text", onClick: onShowElsewhere, children: [_jsx(Icon, { icon: (active.kind === 'chat' ? active.item.kind === 'approval' : active.prompt.asks === 'approval') ? Hand : MessageCircleQuestionMark, size: 16, className: "shrink-0 text-status-needs-you" }), _jsx("span", { className: "grow", children: t('composer.waitingBelow') }), _jsx(Icon, { icon: ArrowDown, size: 16, className: "shrink-0" })] })), active && !elsewhere && (_jsx("div", { hidden: !expanded, children: _jsx(PromptView, { subject: active, draft: session.draftOf(promptIdOf(active)), onDraft: (draft) => session.setDraft(promptIdOf(active), draft), onAction: (action) => {
39
+ refocus.hold();
40
+ void session
41
+ .act(active, () => answerPrompt(active, action, actions))
42
+ .then((result) => {
43
+ if (result === 'failed') {
44
+ refocus.release();
45
+ }
46
+ });
47
+ }, more: session.waiting.length - 1, hasDraft: hasDraft, denyReason: denyReason, disabled: disabled, sending: session.sending, error: session.errorOf(promptIdOf(active)) }, activeKey) })), _jsx("div", { hidden: expanded, children: children })] }));
48
+ }
@@ -0,0 +1,7 @@
1
+ import { type RefObject } from 'react';
2
+ import { type QuoteTaker } from './quote-selection';
3
+ export declare function QuoteButton({ thread, frame, taker }: {
4
+ thread: RefObject<HTMLElement | null>;
5
+ frame: RefObject<HTMLElement | null>;
6
+ taker: RefObject<QuoteTaker | null>;
7
+ }): import("react").JSX.Element | null;
@@ -0,0 +1,76 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from 'react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { Quote } from 'lucide-react';
5
+ import { selectedAnswerQuote } from './quote-selection';
6
+ import { Button } from '@ruimte/ui/Button';
7
+ import { FLOAT } from '@ruimte/ui/classes';
8
+ import { Icon } from '@ruimte/ui/Icon';
9
+ const GAP_PX = 6;
10
+ // Keeps the button's middle far enough from the edges that it never sticks out of the thread.
11
+ const EDGE_PX = 40;
12
+ /* The last line the range covers. A triple-click ends on an empty rect at the start of the next row. */
13
+ const lastLineOf = (range) => {
14
+ const rects = [...range.getClientRects()].filter((rect) => rect.width > 0);
15
+ return rects.at(-1) ?? null;
16
+ };
17
+ /*
18
+ * A button under a selection in an answer that hands it to the composer. Quoting on the selection
19
+ * alone grew the composer while the person was still selecting, which moved the thread under the
20
+ * pointer. It only shows once the pointer is up, so a drag never has it jump along.
21
+ */
22
+ export function QuoteButton({ thread, frame, taker }) {
23
+ const { t } = useTranslation('agent-chat');
24
+ const [offer, setOffer] = useState(null);
25
+ useEffect(() => {
26
+ let pressed = false;
27
+ const read = () => {
28
+ const selection = document.getSelection();
29
+ const quote = taker.current === null ? null : selectedAnswerQuote(selection, thread.current);
30
+ const box = frame.current;
31
+ const line = quote === null || selection === null ? null : lastLineOf(selection.getRangeAt(0));
32
+ if (quote === null || box === null || line === null) {
33
+ setOffer(null);
34
+ return;
35
+ }
36
+ const bounds = box.getBoundingClientRect();
37
+ // The canvas may zoom the cell; the button is placed in the frame's own pixels.
38
+ const zoom = box.offsetWidth > 0 ? bounds.width / box.offsetWidth : 1;
39
+ const x = Math.min(Math.max((line.right - bounds.left) / zoom, EDGE_PX), box.offsetWidth - EDGE_PX);
40
+ const y = (line.bottom - bounds.top) / zoom + GAP_PX;
41
+ setOffer({ quote, x: Math.round(x), y: Math.round(y) });
42
+ };
43
+ const press = () => {
44
+ pressed = true;
45
+ };
46
+ const release = () => {
47
+ pressed = false;
48
+ read();
49
+ };
50
+ const change = () => {
51
+ if (pressed) {
52
+ setOffer(null);
53
+ return;
54
+ }
55
+ read();
56
+ };
57
+ document.addEventListener('pointerdown', press, true);
58
+ document.addEventListener('pointerup', release, true);
59
+ document.addEventListener('selectionchange', change);
60
+ return () => {
61
+ document.removeEventListener('pointerdown', press, true);
62
+ document.removeEventListener('pointerup', release, true);
63
+ document.removeEventListener('selectionchange', change);
64
+ };
65
+ }, [thread, frame, taker]);
66
+ if (offer === null) {
67
+ return null;
68
+ }
69
+ return (_jsxs(Button, { size: "sm", className: `${FLOAT} absolute z-10 -translate-x-1/2 select-none`, style: { left: offer.x, top: offer.y },
70
+ // Pressing a button moves the selection, and the selection is what it quotes.
71
+ onMouseDown: (e) => e.preventDefault(), onClick: () => {
72
+ document.getSelection()?.removeAllRanges();
73
+ taker.current?.(offer.quote);
74
+ setOffer(null);
75
+ }, children: [_jsx(Icon, { icon: Quote, size: 12 }), t('composer.quote.add')] }));
76
+ }
@@ -0,0 +1,5 @@
1
+ export declare function ResumeCompactionDock({ tokens, onCompact, onDismiss }: {
2
+ tokens: number;
3
+ onCompact(): void;
4
+ onDismiss(): void;
5
+ }): import("react").JSX.Element;
@@ -0,0 +1,15 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useTranslation } from 'react-i18next';
3
+ import { Minimize2 } from 'lucide-react';
4
+ import { formatTokens } from '@ruimte/ui/format/number';
5
+ import { Button } from '@ruimte/ui/Button';
6
+ import { Icon } from '@ruimte/ui/Icon';
7
+ /*
8
+ * The offer at the top of the composer, for a chat that lay still long enough that the next message
9
+ * pays for the whole thread again. It takes no keys and blocks nothing: typing and sending keeps the
10
+ * full history, which is what a person who ignores this does.
11
+ */
12
+ export function ResumeCompactionDock({ tokens, onCompact, onDismiss }) {
13
+ const { t } = useTranslation('agent-chat');
14
+ return (_jsxs("div", { className: "flex items-center gap-2 border-b border-border px-3.5 py-2.5", children: [_jsx(Icon, { icon: Minimize2, size: 16, className: "shrink-0 text-text-faint" }), _jsxs("span", { className: "flex min-w-0 grow flex-col", children: [_jsx("span", { className: "truncate text-sm text-text", children: t('resumeCompaction.title') }), _jsx("span", { className: "truncate text-xs tabular-nums text-text-faint", children: t('resumeCompaction.carried', { tokens: formatTokens(tokens) }) })] }), _jsx(Button, { size: "sm", onClick: onDismiss, children: t('resumeCompaction.keep') }), _jsx(Button, { size: "sm", variant: "primary", onClick: onCompact, children: t('resumeCompaction.compact') })] }));
15
+ }
@@ -0,0 +1,25 @@
1
+ import type { AgentKind, ChatUsage, ModelInfo, ModelSelection, ProviderInfo, RuntimeMode } from '@ruimte/agent-contracts';
2
+ import type { AccountChoice } from '../account-choice';
3
+ interface RunSettingsProps {
4
+ providers: ProviderInfo[];
5
+ provider: AgentKind;
6
+ selection: ModelSelection;
7
+ model: ModelInfo | undefined;
8
+ runtimeMode: RuntimeMode;
9
+ open: boolean;
10
+ onOpenChange(open: boolean): void;
11
+ onModel(provider: AgentKind, slug: string): void;
12
+ onOption(id: string, value: string | boolean): void;
13
+ onMode(mode: RuntimeMode): void;
14
+ usage: ChatUsage;
15
+ compactDisabled: boolean;
16
+ onCompact(): void;
17
+ account: AccountPick | null;
18
+ }
19
+ interface AccountPick {
20
+ choice: AccountChoice;
21
+ started: boolean;
22
+ onPick(id: string): void;
23
+ }
24
+ export declare function RunSettings({ providers, provider, selection, model, runtimeMode, open, onOpenChange, onModel, onOption, onMode, usage, compactDisabled, onCompact, account }: RunSettingsProps): import("react").JSX.Element;
25
+ export {};
@@ -0,0 +1,148 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useTranslation } from 'react-i18next';
3
+ import { Menu } from '@base-ui-components/react/menu';
4
+ import clsx from 'clsx';
5
+ import { Activity, ChevronDown, ChevronRight, ChevronUp, FilePen, Gauge, Hand, Settings, Shield, ShieldAlert, SlidersHorizontal } from 'lucide-react';
6
+ import { AccountDot } from '../../agents/AccountDot';
7
+ import { limitsOfAccount, sessionWindow } from '../../agents/account-limits';
8
+ import { canContinueOn } from '../../agents/accounts';
9
+ import { AgentIcon } from '../../agents/AgentIcon';
10
+ import { modelName, shortModelName } from '../../agents/model-name';
11
+ import { CONTEXT_OPTION, contextFraction, contextSegments, orderOptions } from '../logic/context-usage';
12
+ import { RUNTIME_MODES } from '../runtime-modes';
13
+ import { formatClock, formatWeekdayClock, isSameDay } from '@ruimte/ui/format/datetime';
14
+ import { formatPercent, formatTokens } from '@ruimte/ui/format/number';
15
+ import { chatHost } from '../../host';
16
+ import { useUsageLimits } from '../../usage/limits';
17
+ import { MENU_HINT, MENU_LABEL, MENU_SEPARATOR } from '@ruimte/ui/classes';
18
+ import { Icon } from '@ruimte/ui/Icon';
19
+ import { MenuCheck } from '@ruimte/ui/MenuCheck';
20
+ import { Tooltip } from '@ruimte/ui/Tooltip';
21
+ import { useNow } from '@ruimte/ui/useNow';
22
+ const MINUTE_MS = 60_000;
23
+ const MODE_ICONS = {
24
+ supervised: Hand,
25
+ 'auto-accept-edits': FilePen,
26
+ auto: Shield,
27
+ 'full-access': ShieldAlert
28
+ };
29
+ // More choices than this do not fit beside the label, so they go in a submenu of their own.
30
+ const INLINE_CHOICES = 3;
31
+ const PART_COLORS = {
32
+ toolOutput: 'bg-chart-context-tools',
33
+ filesRead: 'bg-chart-context-files',
34
+ conversation: 'bg-chart-context-conversation',
35
+ system: 'bg-chart-context-system'
36
+ };
37
+ const RING_RADIUS = 5;
38
+ const RING_CIRCUMFERENCE = 2 * Math.PI * RING_RADIUS;
39
+ const SUBMENU_POSITIONER = { sideOffset: 4, alignOffset: -4 };
40
+ const optionSummary = (option, selection) => {
41
+ const value = selection.options[option.id];
42
+ if (option.type === 'select') {
43
+ return option.choices.find((choice) => choice.id === (value ?? option.defaultChoice))?.label ?? null;
44
+ }
45
+ return (value ?? option.defaultValue) === true ? option.label : null;
46
+ };
47
+ const optionIcon = (option) => option.type === 'boolean' ? Gauge : option.id === CONTEXT_OPTION ? Activity : SlidersHorizontal;
48
+ const contextTone = (fraction) => (fraction >= 0.9 ? 'text-status-error' : fraction >= 0.7 ? 'text-status-needs-you' : 'text-text');
49
+ // A radio group holds one string, so a model is named by its provider and its slug together.
50
+ const modelValue = (provider, slug) => `${provider}/${slug}`;
51
+ /*
52
+ * Everything that decides how the next turn runs, behind one pill: the model, its own knobs and the
53
+ * permission mode. The pill reads back all of it, so nothing has to be opened to know what is set.
54
+ */
55
+ export function RunSettings({ providers, provider, selection, model, runtimeMode, open, onOpenChange, onModel, onOption, onMode, usage, compactDisabled, onCompact, account }) {
56
+ const { t } = useTranslation('agent-chat');
57
+ const owner = providers.find((entry) => entry.kind === provider);
58
+ const current = modelName(selection.model, owner?.models);
59
+ const short = shortModelName(current, owner?.models);
60
+ const options = orderOptions(model?.options ?? []);
61
+ const summary = options.flatMap((option) => {
62
+ const label = optionSummary(option, selection);
63
+ return label === null ? [] : [{ id: option.id, label, ring: option.id === CONTEXT_OPTION }];
64
+ });
65
+ // A CLI without a context choice still fills one, so the ring stands on its own next to the size.
66
+ const hasContextOption = options.some((option) => option.id === CONTEXT_OPTION);
67
+ const standaloneContext = !hasContextOption && Boolean(usage.contextWindow);
68
+ if (standaloneContext) {
69
+ summary.push({ id: CONTEXT_OPTION, label: formatTokens(usage.contextWindow ?? 0), ring: true });
70
+ }
71
+ const grouped = providers.length > 1;
72
+ const live = providers.map((entry) => ({ entry, models: entry.models.filter((row) => !row.legacy) })).filter((group) => group.models.length > 0);
73
+ const legacy = providers.map((entry) => ({ entry, models: entry.models.filter((row) => row.legacy) })).filter((group) => group.models.length > 0);
74
+ const chosenLegacy = owner?.models.find((row) => row.legacy && row.slug === selection.model);
75
+ const chosenModel = modelValue(provider, selection.model);
76
+ const fullAccess = runtimeMode === 'full-access';
77
+ const chooseModel = (value) => {
78
+ const slash = value.indexOf('/');
79
+ onModel(value.slice(0, slash), value.slice(slash + 1));
80
+ };
81
+ const modelGroups = (groups) => groups.map(({ entry, models }) => (_jsxs(Menu.Group, { children: [grouped && (_jsxs(Menu.GroupLabel, { className: `${MENU_LABEL} flex items-center gap-1.5`, children: [_jsx(AgentIcon, { kind: entry.kind, size: 12 }), entry.name] })), models.map((row) => (_jsxs(Menu.RadioItem, { value: modelValue(entry.kind, row.slug), closeOnClick: true, className: "menu-item", children: [_jsx(MenuCheck, { kind: "radio" }), _jsx(AgentIcon, { kind: entry.kind, size: 14, className: "shrink-0" }), _jsx("span", { className: "min-w-0 truncate", children: row.name }), row.badge && _jsx("span", { className: "rounded bg-accent-soft px-1 text-xs font-medium text-accent", children: row.badge })] }, row.slug)))] }, entry.kind)));
82
+ return (_jsxs(Menu.Root, { open: open, onOpenChange: onOpenChange, children: [_jsx(Tooltip, { label: t('pickers.settings.title'), kbd: "/model", children: _jsxs(Menu.Trigger, { className: clsx('flex h-8 min-w-0 items-center gap-2 rounded-full border pr-3 pl-2.5 text-xs whitespace-nowrap text-text-muted @max-xl/composer:gap-1.5 @max-sm/composer:pr-2.5', open ? 'border-accent bg-accent-soft' : 'border-border hover:bg-surface-hover'), children: [_jsx(AgentIcon, { kind: provider, size: 14, className: "shrink-0" }), _jsxs("span", { className: "flex min-w-0 items-center gap-2 overflow-hidden @max-xl/composer:gap-1.5", children: [_jsx("span", { className: "truncate font-medium text-text @max-xl/composer:hidden", children: current }), _jsx("span", { className: "hidden truncate font-medium text-text @max-xl/composer:inline", children: short }), account !== null && (_jsxs("span", { className: "flex min-w-0 items-center gap-2 @max-xl/composer:gap-1.5", children: [_jsx("span", { className: "shrink-0 text-text-faint", children: "\u00B7" }), _jsx(AccountDot, { color: account.choice.current?.account.color }), _jsx("span", { className: "truncate @max-xl/composer:hidden", children: account.choice.current === null ? account.choice.currentId : account.choice.nameOf(account.choice.current) })] })), summary.map((entry) => (_jsxs("span", { className: clsx('flex shrink-0 items-center gap-2 @max-xl/composer:gap-1.5', !entry.ring && '@max-md/composer:hidden'), children: [_jsx("span", { className: "text-text-faint", children: "\u00B7" }), entry.ring && _jsx(ContextRing, { usage: usage }), _jsx("span", { className: clsx(entry.ring && '@max-sm/composer:hidden'), children: entry.label })] }, entry.id))), _jsx("span", { className: "shrink-0 text-text-faint", children: "\u00B7" }), _jsxs("span", { className: clsx('flex shrink-0 items-center gap-1', fullAccess && 'text-status-needs-you'), children: [_jsx(Icon, { icon: MODE_ICONS[runtimeMode], size: 12, className: "shrink-0" }), _jsx("span", { className: "@max-xl/composer:hidden", children: t(`modes.${runtimeMode}.short`) })] })] }), _jsx(Icon, { icon: open ? ChevronUp : ChevronDown, size: 12, className: "shrink-0 @max-sm/composer:hidden" })] }) }), _jsx(Menu.Portal, { children: _jsx(Menu.Positioner, { className: "z-(--z-popup)", side: "top", sideOffset: 8, align: "start", children: _jsxs(Menu.Popup, { className: "menu-popup w-80", children: [providers.length === 0 && _jsx("div", { className: "px-2.5 py-3 text-xs text-text-faint", children: t('pickers.model.noProvider') }), _jsx(Menu.RadioGroup, { value: chosenModel, onValueChange: chooseModel, children: modelGroups(live) }), legacy.length > 0 && (_jsxs(Menu.SubmenuRoot, { children: [_jsxs(Menu.SubmenuTrigger, { className: "menu-item text-text-muted", children: [_jsx("span", { className: "min-w-0 grow truncate", children: t('pickers.model.legacyGroup', { count: legacy.reduce((count, group) => count + group.models.length, 0) }) }), chosenLegacy && _jsx("span", { className: `${MENU_HINT} truncate`, children: chosenLegacy.name }), _jsx(Icon, { icon: ChevronRight, size: 14, className: "shrink-0 text-text-faint" })] }), _jsx(Menu.Portal, { children: _jsx(Menu.Positioner, { className: "z-(--z-popup)", ...SUBMENU_POSITIONER, children: _jsx(Menu.Popup, { className: "menu-popup min-w-56", children: _jsx(Menu.RadioGroup, { value: chosenModel, onValueChange: chooseModel, children: modelGroups(legacy) }) }) }) })] })), _jsx(Menu.Separator, { className: MENU_SEPARATOR }), account !== null && _jsx(AccountSubmenu, { provider: provider, account: account }), _jsxs(Menu.SubmenuRoot, { children: [_jsxs(Menu.SubmenuTrigger, { className: "menu-item text-text-muted", children: [_jsx(Icon, { icon: MODE_ICONS[runtimeMode], size: 14, className: clsx('shrink-0', fullAccess ? 'text-status-needs-you' : 'text-text-faint') }), _jsx("span", { className: "min-w-0 grow truncate", children: t('pickers.mode.label') }), _jsx("span", { className: `${MENU_HINT} truncate`, children: t(`modes.${runtimeMode}.label`) }), _jsx(Icon, { icon: ChevronRight, size: 14, className: "shrink-0 text-text-faint" })] }), _jsx(Menu.Portal, { children: _jsx(Menu.Positioner, { className: "z-(--z-popup)", ...SUBMENU_POSITIONER, children: _jsx(Menu.Popup, { className: "menu-popup w-72", children: _jsx(Menu.RadioGroup, { value: runtimeMode, onValueChange: (mode) => onMode(mode), children: RUNTIME_MODES.map((mode) => (_jsxs(Menu.RadioItem, { value: mode, className: "menu-item items-start", children: [_jsx(MenuCheck, { kind: "radio" }), _jsx("span", { className: "grid h-lh w-4 shrink-0 place-items-center", children: _jsx(Icon, { icon: MODE_ICONS[mode], size: 14, className: mode === 'full-access' ? 'text-status-needs-you' : 'text-text-faint' }) }), _jsxs("span", { className: "flex min-w-0 grow flex-col", children: [t(`modes.${mode}.label`), _jsx("span", { className: "text-xs text-text-faint", children: t(`modes.${mode}.hint`) })] })] }, mode))) }) }) }) })] }), options.map((option) => (_jsx(OptionRow, { option: option, selection: selection, onChange: (value) => onOption(option.id, value) }, option.id))), (hasContextOption || standaloneContext) && (_jsxs(_Fragment, { children: [_jsx(Menu.Separator, { className: MENU_SEPARATOR }), _jsx(ContextUsage, { usage: usage, disabled: compactDisabled, onCompact: onCompact })] }))] }) }) })] }));
83
+ }
84
+ /* Red where a window is nearly spent, amber where it is worth knowing, as the usage bars have it. */
85
+ const sessionTone = (used) => used >= 0.9
86
+ ? { bar: 'bg-status-error', text: 'text-status-error' }
87
+ : used >= 0.7
88
+ ? { bar: 'bg-status-needs-you', text: 'text-status-needs-you' }
89
+ : { bar: 'bg-text-muted', text: 'text-text-muted' };
90
+ /*
91
+ * The account the next turn runs under, for the chat's own CLI. Before the first turn any of them;
92
+ * after it one that reads the conversation, since the others would start it over, and a fork is how
93
+ * the conversation goes along to those. A switch applies from the next turn.
94
+ */
95
+ function AccountSubmenu({ provider, account }) {
96
+ const { t } = useTranslation('agent-chat');
97
+ const { choice, started, onPick } = account;
98
+ return (_jsxs(Menu.SubmenuRoot, { children: [_jsxs(Menu.SubmenuTrigger, { className: "menu-item text-text-muted", children: [_jsx("span", { className: "grid w-3.5 shrink-0 place-items-center", children: _jsx(AccountDot, { color: choice.current?.account.color }) }), _jsx("span", { className: "min-w-0 grow truncate", children: t('pickers.account.label') }), _jsx("span", { className: `${MENU_HINT} truncate`, children: choice.current === null ? choice.currentId : choice.nameOf(choice.current) }), _jsx(Icon, { icon: ChevronRight, size: 14, className: "shrink-0 text-text-faint" })] }), _jsx(Menu.Portal, { children: _jsx(Menu.Positioner, { className: "z-(--z-popup)", ...SUBMENU_POSITIONER, children: _jsxs(Menu.Popup, { className: "menu-popup min-w-64", children: [_jsxs(Menu.Group, { children: [_jsxs(Menu.GroupLabel, { className: `${MENU_LABEL} flex items-center gap-1.5`, children: [_jsx(AgentIcon, { kind: provider, size: 12 }), choice.providerName] }), _jsx(Menu.RadioGroup, { value: choice.currentId, onValueChange: (id) => onPick(id), children: _jsx(AccountRows, { choice: choice, locked: (id) => started && !canContinueOn(choice.accounts, provider, choice.currentId, id) }) })] }), _jsx(Menu.Separator, { className: MENU_SEPARATOR }), _jsxs(Menu.Item, { className: "menu-item text-text-muted", onClick: () => chatHost().openSettings('providers'), children: [_jsx(Icon, { icon: Settings, size: 14, className: "shrink-0 text-text-faint" }), t('pickers.account.manage')] })] }) }) })] }));
99
+ }
100
+ /* Mounted only while the submenu is open, since asking for the plan windows starts a CLI per account. */
101
+ function AccountRows({ choice, locked }) {
102
+ const { t } = useTranslation('agent-chat');
103
+ const limits = useUsageLimits();
104
+ const now = useNow(MINUTE_MS);
105
+ return choice.offered.map((entry) => {
106
+ const session = sessionWindow(limitsOfAccount(limits, entry.id));
107
+ const disabled = locked(entry.id);
108
+ const row = (_jsxs(Menu.RadioItem, { value: entry.id, disabled: disabled, closeOnClick: true, className: "menu-item items-start data-disabled:opacity-50", children: [_jsx(MenuCheck, { kind: "radio" }), _jsxs("span", { className: "flex min-w-0 grow flex-col", children: [_jsxs("span", { className: "flex min-w-0 items-center gap-2", children: [_jsx(AccountDot, { color: entry.account.color }), _jsx("span", { className: "truncate", children: choice.nameOf(entry) })] }), session !== null && _jsx(SessionLine, { used: session.used, resetsAt: session.resetsAt, now: now })] })] }, entry.id));
109
+ return disabled ? (_jsx(Tooltip, { label: t('pickers.account.forkToSwitch', { account: choice.nameOf(entry) }), side: "right", sideOffset: 12, children: row }, entry.id)) : (row);
110
+ });
111
+ }
112
+ function SessionLine({ used, resetsAt, now }) {
113
+ const { t } = useTranslation('agent-chat');
114
+ const percent = Math.round(used * 100);
115
+ const tone = sessionTone(used);
116
+ return (_jsxs("span", { className: "flex items-center gap-2 text-xs text-text-faint tabular-nums", children: [_jsx("span", { "aria-hidden": true, className: "relative h-1 w-14 shrink-0 overflow-hidden rounded-full bg-surface-sunken", children: _jsx("span", { className: clsx('absolute inset-y-0 left-0 rounded-full', tone.bar), style: { width: `${Math.min(100, percent)}%` } }) }), _jsx("span", { className: tone.text, children: formatPercent(percent) }), resetsAt !== null && (_jsxs("span", { className: "whitespace-nowrap", children: ["\u00B7 ", t('pickers.account.resets', { time: isSameDay(resetsAt, now) ? formatClock(resetsAt) : formatWeekdayClock(resetsAt) })] }))] }));
117
+ }
118
+ /* How full the context is, drawn small enough to sit in the pill beside the window's size. */
119
+ function ContextRing({ usage }) {
120
+ const fraction = contextFraction(usage);
121
+ return (_jsxs("svg", { width: "14", height: "14", viewBox: "0 0 14 14", className: clsx('shrink-0', contextTone(fraction)), "aria-hidden": "true", children: [_jsx("circle", { cx: "7", cy: "7", r: RING_RADIUS, fill: "none", strokeWidth: "2", className: "stroke-border-strong" }), _jsx("circle", { cx: "7", cy: "7", r: RING_RADIUS, fill: "none", strokeWidth: "2", stroke: "currentColor", strokeDasharray: RING_CIRCUMFERENCE, strokeDashoffset: RING_CIRCUMFERENCE * (1 - fraction), transform: "rotate(-90 7 7)" })] }));
122
+ }
123
+ /* How full the context is and what fills it, and the one thing to do about a full context. */
124
+ function ContextUsage({ usage, disabled, onCompact }) {
125
+ const { t } = useTranslation('agent-chat');
126
+ const fraction = contextFraction(usage);
127
+ const percent = Math.round(fraction * 100);
128
+ const segments = contextSegments(usage);
129
+ const used = formatTokens(usage.contextTokens);
130
+ return (_jsxs(Menu.Group, { className: "flex flex-col gap-2 px-2.5 py-1.5", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Menu.GroupLabel, { className: "min-w-0 grow truncate text-sm font-medium text-text", children: t('contextMeter.title') }), _jsx("span", { className: clsx('shrink-0 text-xs tabular-nums', fraction >= 0.7 ? contextTone(fraction) : 'text-text-faint'), children: usage.contextWindow ? t('contextMeter.of', { used, window: formatTokens(usage.contextWindow) }) : t('contextMeter.used', { tokens: used }) })] }), usage.contextWindow ? (_jsx("div", { role: "meter", "aria-valuenow": percent, "aria-valuemin": 0, "aria-valuemax": 100, "aria-label": t('contextMeter.aria', { percent }), className: "flex h-1.5 overflow-hidden rounded-full bg-surface-sunken", children: segments ? (segments.map((segment) => (_jsx("div", { className: clsx('h-full shrink-0', PART_COLORS[segment.part]), style: { width: `${segment.fraction * 100}%` } }, segment.part)))) : (_jsx("div", { className: clsx('h-full rounded-full bg-current', contextTone(fraction)), style: { width: `${percent}%` } })) })) : null, segments && (_jsx("div", { className: "flex flex-col gap-0.5 text-xs", children: segments.map((segment) => (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { "aria-hidden": "true", className: clsx('size-2 shrink-0 rounded-xs', PART_COLORS[segment.part]) }), _jsx("span", { className: "min-w-0 grow truncate text-text-muted", children: t(`contextMeter.parts.${segment.part}`) }), _jsx("span", { className: "shrink-0 text-text-faint tabular-nums", children: t('contextMeter.estimate', { tokens: formatTokens(segment.tokens) }) })] }, segment.part))) })), _jsxs("div", { className: "flex items-center gap-2 text-xs text-text-faint tabular-nums", children: [_jsx("span", { className: "min-w-0 grow truncate", children: usage.contextWindow ? t('contextMeter.percentUsed', { percent: formatPercent(fraction * 100) }) : null }), _jsxs(Menu.Item, { className: "flex shrink-0 cursor-default items-center gap-0.5 rounded font-medium text-accent outline-none data-disabled:opacity-50 data-highlighted:underline", disabled: disabled || usage.contextTokens === 0, onClick: onCompact, children: [t('contextMeter.compactNow'), _jsx(Icon, { icon: ChevronRight, size: 12 })] })] })] }));
131
+ }
132
+ /*
133
+ * One of the model's own knobs: a few choices side by side, more of them in a submenu, a checkbox for
134
+ * a boolean. Every choice is a menu item, so the arrow keys reach it.
135
+ */
136
+ function OptionRow({ option, selection, onChange }) {
137
+ const icon = _jsx(Icon, { icon: optionIcon(option), size: 14, className: "shrink-0 text-text-faint" });
138
+ if (option.type === 'boolean') {
139
+ const checked = (selection.options[option.id] ?? option.defaultValue) === true;
140
+ const item = (_jsxs(Menu.CheckboxItem, { className: "menu-item text-text-muted", checked: checked, onCheckedChange: onChange, closeOnClick: false, children: [icon, _jsx("span", { className: "min-w-0 grow truncate", children: option.label }), _jsx(MenuCheck, { kind: "checkbox" })] }));
141
+ return option.description ? (_jsx(Tooltip, { label: option.description, side: "right", sideOffset: 12, children: item })) : (item);
142
+ }
143
+ const value = String(selection.options[option.id] ?? option.defaultChoice);
144
+ if (option.choices.length > INLINE_CHOICES) {
145
+ return (_jsxs(Menu.SubmenuRoot, { children: [_jsxs(Menu.SubmenuTrigger, { className: "menu-item text-text-muted", children: [icon, _jsx("span", { className: "min-w-0 grow truncate", children: option.label }), _jsx("span", { className: MENU_HINT, children: option.choices.find((choice) => choice.id === value)?.label }), _jsx(Icon, { icon: ChevronRight, size: 14, className: "shrink-0 text-text-faint" })] }), _jsx(Menu.Portal, { children: _jsx(Menu.Positioner, { className: "z-(--z-popup)", ...SUBMENU_POSITIONER, children: _jsx(Menu.Popup, { className: "menu-popup min-w-56", children: _jsx(Menu.RadioGroup, { value: value, onValueChange: (next) => onChange(next), children: option.choices.map((choice) => (_jsxs(Menu.RadioItem, { value: choice.id, className: clsx('menu-item', choice.description && 'items-start'), children: [_jsx(MenuCheck, { kind: "radio" }), _jsxs("span", { className: "flex min-w-0 grow flex-col", children: [choice.label, choice.description && _jsx("span", { className: "text-xs text-text-faint", children: choice.description })] })] }, choice.id))) }) }) }) })] }));
146
+ }
147
+ return (_jsxs("div", { className: "flex items-center gap-2.5 px-2.5 py-1.5 text-sm text-text-muted", children: [icon, _jsx("span", { className: "min-w-0 grow truncate", children: option.label }), _jsx(Menu.RadioGroup, { value: value, onValueChange: (next) => onChange(next), "aria-label": option.label, className: "flex gap-0.5 rounded-md bg-surface-sunken p-0.5", children: option.choices.map((choice) => (_jsx(Menu.RadioItem, { value: choice.id, className: clsx('flex h-6 cursor-default items-center rounded px-2 text-xs outline-none', choice.id === value ? 'bg-surface-active text-text' : 'text-text-muted data-highlighted:text-text'), children: choice.label }, choice.id))) })] }));
148
+ }
@@ -0,0 +1,17 @@
1
+ import { type ScrubberTick } from '../logic/scrubber';
2
+ export interface CardChat {
3
+ chatId: string;
4
+ canFork(turnId: string): boolean;
5
+ fork(turnId: string): void;
6
+ }
7
+ interface ScrubberProps {
8
+ ticks: readonly ScrubberTick[];
9
+ firstInView: number | null;
10
+ lastInView: number | null;
11
+ onPick(index: number): void;
12
+ chat?: CardChat | null;
13
+ found?: readonly boolean[];
14
+ foundCurrent?: number | null;
15
+ }
16
+ export declare const Scrubber: import("react").MemoExoticComponent<({ ticks, firstInView, lastInView, onPick, chat, found, foundCurrent }: ScrubberProps) => import("react").JSX.Element>;
17
+ export {};