@adecore/agents-react 0.38.0 → 0.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (351) hide show
  1. package/dist/agents/AgentModelLabel.d.ts +10 -0
  2. package/dist/agents/AgentModelLabel.js +98 -0
  3. package/dist/agents/model-name.js +2 -1
  4. package/dist/agents/provider-paths.d.ts +2 -1
  5. package/dist/agents/provider-paths.js +7 -3
  6. package/dist/chat/account-choice.js +57 -28
  7. package/dist/chat/bookmarks.js +2 -1
  8. package/dist/chat/drafts.d.ts +0 -1
  9. package/dist/chat/drafts.js +9 -28
  10. package/dist/chat/logic/picker-entries.js +2 -8
  11. package/dist/chat/logic/resume-compaction.js +2 -5
  12. package/dist/chat/logic/timeline-copy.js +2 -8
  13. package/dist/chat/logic/timeline.js +9 -10
  14. package/dist/chat/logic/workflow.js +2 -4
  15. package/dist/chat/subagent-conversation.js +2 -1
  16. package/dist/chat/ui/Composer.js +385 -360
  17. package/dist/chat/ui/GoalDock.js +2 -1
  18. package/dist/chat/ui/LimitState.js +4 -6
  19. package/dist/chat/ui/Pickers.d.ts +2 -1
  20. package/dist/chat/ui/Pickers.js +126 -123
  21. package/dist/chat/ui/PromptEditor.d.ts +19 -0
  22. package/dist/chat/ui/PromptEditor.js +115 -0
  23. package/dist/chat/ui/Timeline.d.ts +2 -1
  24. package/dist/chat/ui/Timeline.js +13 -1
  25. package/dist/chat/ui/composer/ComposerControls.d.ts +3 -0
  26. package/dist/chat/ui/composer/ComposerControls.js +235 -38
  27. package/dist/chat/ui/composer/key-handler.d.ts +1 -1
  28. package/dist/chat/ui/composer/key-handler.js +3 -11
  29. package/dist/chat/ui/composer/prompt-picks.d.ts +4 -0
  30. package/dist/chat/ui/composer/prompt-picks.js +14 -0
  31. package/dist/chat/ui/composer/use-composer-menus.d.ts +4 -49
  32. package/dist/chat/ui/composer/use-composer-menus.js +65 -519
  33. package/dist/chat/ui/composer/use-continuable-turn.js +0 -1
  34. package/dist/chat/ui/composer/use-notice.js +2 -1
  35. package/dist/chat/ui/composer/use-prompt-history.js +1 -1
  36. package/dist/chat/ui/composer/use-prompt-menus.d.ts +73 -0
  37. package/dist/chat/ui/composer/use-prompt-menus.js +499 -0
  38. package/dist/chat/ui/composer/use-take-back.js +2 -2
  39. package/dist/chat/ui/diff/ApprovalReview.js +1 -1
  40. package/dist/chat/ui/highlighter.d.ts +0 -6
  41. package/dist/chat/ui/highlighter.js +0 -12
  42. package/dist/chat/ui/icons.d.ts +0 -1
  43. package/dist/chat/ui/icons.js +0 -3
  44. package/dist/chat/ui/index.d.ts +3 -0
  45. package/dist/chat/ui/index.js +2 -0
  46. package/dist/chat/ui/rows/MessageRows.js +2 -5
  47. package/dist/chat/ui/rows/row-rhythm.js +2 -5
  48. package/dist/chat/ui/theme-watch.js +3 -7
  49. package/dist/host-hooks.d.ts +2 -1
  50. package/dist/host-hooks.js +6 -0
  51. package/dist/host.d.ts +17 -1
  52. package/dist/index.d.ts +1 -1
  53. package/dist/locales/en/agent-chat.d.ts +1 -0
  54. package/dist/locales/en/agent-chat.js +1 -0
  55. package/dist/locales/nl/agent-chat.d.ts +1 -0
  56. package/dist/locales/nl/agent-chat.js +1 -0
  57. package/dist/prompts/logic/usePromptSession.js +2 -1
  58. package/dist/providers/AddAccountForm.js +2 -1
  59. package/dist/providers/CliDetail.js +104 -130
  60. package/dist/providers/account-actions.js +6 -9
  61. package/dist/providers/index.d.ts +1 -0
  62. package/dist/providers/index.js +1 -0
  63. package/dist/state/chats.d.ts +1 -0
  64. package/dist/state/chats.js +6 -11
  65. package/dist/state/provider-accounts.d.ts +1 -1
  66. package/dist/state/provider-accounts.js +2 -8
  67. package/dist/theme.css +3 -0
  68. package/dist/transport.d.ts +2 -0
  69. package/dist/transport.js +14 -0
  70. package/dist/usage/ChartLogo.d.ts +5 -0
  71. package/dist/usage/ChartLogo.js +48 -0
  72. package/dist/usage/LimitsList.js +6 -16
  73. package/dist/usage/UsageBreakdown.js +17 -40
  74. package/dist/usage/UsageLimits.js +183 -211
  75. package/dist/usage/UsagePage.js +3 -17
  76. package/dist/usage/UsageSummary.js +4 -9
  77. package/dist/usage/limits.js +16 -31
  78. package/dist/usage/summary.d.ts +1 -1
  79. package/dist/usage/summary.js +3 -5
  80. package/package.json +26 -26
  81. package/src/agents/AccountDot.tsx +1 -0
  82. package/src/agents/AgentIcon.tsx +2 -0
  83. package/src/agents/AgentModelLabel.tsx +38 -0
  84. package/src/agents/ModelOptionControl.tsx +8 -0
  85. package/src/agents/account-limits.ts +3 -0
  86. package/src/agents/accounts.ts +12 -0
  87. package/src/agents/model-name.ts +13 -1
  88. package/src/agents/model-options.ts +2 -0
  89. package/src/agents/model-requirement.ts +4 -0
  90. package/src/agents/option-words.ts +2 -0
  91. package/src/agents/provider-paths.ts +8 -3
  92. package/src/agents/status-look.ts +1 -0
  93. package/src/chat/account-choice.ts +25 -19
  94. package/src/chat/actions.ts +5 -0
  95. package/src/chat/attachments.ts +8 -0
  96. package/src/chat/bookmarks.ts +13 -1
  97. package/src/chat/chat-client.ts +63 -0
  98. package/src/chat/chat-references.ts +3 -0
  99. package/src/chat/composer-effort.ts +5 -0
  100. package/src/chat/composer-send.ts +4 -0
  101. package/src/chat/drafts.ts +21 -17
  102. package/src/chat/forks.ts +4 -0
  103. package/src/chat/guards.ts +2 -0
  104. package/src/chat/index.ts +7 -0
  105. package/src/chat/logic/ansi.ts +47 -0
  106. package/src/chat/logic/bookmarks.ts +7 -0
  107. package/src/chat/logic/command-looks.ts +29 -0
  108. package/src/chat/logic/context-usage.ts +3 -0
  109. package/src/chat/logic/continue-after-stop.ts +5 -0
  110. package/src/chat/logic/effort.ts +4 -0
  111. package/src/chat/logic/embedded-code.ts +63 -0
  112. package/src/chat/logic/fork.ts +22 -0
  113. package/src/chat/logic/generated-image.ts +7 -0
  114. package/src/chat/logic/goal.ts +7 -0
  115. package/src/chat/logic/handback.ts +2 -0
  116. package/src/chat/logic/limit.ts +8 -0
  117. package/src/chat/logic/picker-entries.ts +16 -4
  118. package/src/chat/logic/resume-compaction.ts +22 -5
  119. package/src/chat/logic/scrubber.ts +38 -5
  120. package/src/chat/logic/thought.ts +5 -0
  121. package/src/chat/logic/thread-cards.ts +6 -0
  122. package/src/chat/logic/timeline-copy.ts +4 -9
  123. package/src/chat/logic/timeline-target.ts +15 -0
  124. package/src/chat/logic/timeline.ts +113 -10
  125. package/src/chat/logic/tools.ts +30 -0
  126. package/src/chat/logic/visual-height.ts +5 -0
  127. package/src/chat/logic/visual-viewport.ts +1 -0
  128. package/src/chat/logic/welcome.ts +3 -0
  129. package/src/chat/logic/workflow.ts +26 -7
  130. package/src/chat/mentions.ts +56 -0
  131. package/src/chat/pending-prompts.ts +16 -0
  132. package/src/chat/persisted-json.ts +15 -0
  133. package/src/chat/preferences.ts +16 -0
  134. package/src/chat/prompt-hold.ts +2 -0
  135. package/src/chat/quote.ts +7 -0
  136. package/src/chat/reading-width.ts +1 -0
  137. package/src/chat/recent-messages.ts +5 -0
  138. package/src/chat/resume-compaction-dismissals.ts +1 -0
  139. package/src/chat/stash.ts +12 -0
  140. package/src/chat/subagent-conversation.ts +24 -4
  141. package/src/chat/subagent-list.ts +14 -0
  142. package/src/chat/subagent-support.ts +1 -0
  143. package/src/chat/subagent-view.ts +9 -0
  144. package/src/chat/thumbnails.ts +14 -0
  145. package/src/chat/timeline-flash.ts +2 -0
  146. package/src/chat/timeline-scroll.ts +23 -0
  147. package/src/chat/ui/AccountPill.tsx +4 -0
  148. package/src/chat/ui/AnsiOutput.tsx +1 -0
  149. package/src/chat/ui/BookmarkMarker.tsx +5 -0
  150. package/src/chat/ui/BookmarkSubmenu.tsx +1 -0
  151. package/src/chat/ui/ChatActivity.tsx +13 -0
  152. package/src/chat/ui/CodeBlock.tsx +8 -0
  153. package/src/chat/ui/Composer.tsx +27 -3
  154. package/src/chat/ui/ComposerInput.tsx +22 -0
  155. package/src/chat/ui/DiffPool.tsx +3 -0
  156. package/src/chat/ui/EditDiff.tsx +5 -0
  157. package/src/chat/ui/GoalDock.tsx +13 -1
  158. package/src/chat/ui/ImageView.tsx +11 -0
  159. package/src/chat/ui/LimitState.tsx +23 -6
  160. package/src/chat/ui/Markdown.tsx +41 -0
  161. package/src/chat/ui/MermaidBlock.tsx +16 -0
  162. package/src/chat/ui/MessageActions.tsx +8 -0
  163. package/src/chat/ui/ModelMarks.tsx +1 -0
  164. package/src/chat/ui/Pickers.tsx +30 -1
  165. package/src/chat/ui/PromptComposer.tsx +2 -0
  166. package/src/chat/ui/PromptEditor.tsx +136 -0
  167. package/src/chat/ui/QuoteButton.tsx +12 -0
  168. package/src/chat/ui/ReferenceChip.tsx +3 -0
  169. package/src/chat/ui/ResumeCompactionDock.tsx +1 -0
  170. package/src/chat/ui/RowTime.tsx +1 -0
  171. package/src/chat/ui/RunSettings.tsx +38 -0
  172. package/src/chat/ui/Scrubber.tsx +14 -0
  173. package/src/chat/ui/SessionLine.tsx +1 -0
  174. package/src/chat/ui/ShellCommand.tsx +32 -0
  175. package/src/chat/ui/SubagentControls.tsx +7 -0
  176. package/src/chat/ui/SubagentInfo.tsx +1 -0
  177. package/src/chat/ui/SubagentStopButton.tsx +6 -0
  178. package/src/chat/ui/SubagentTimeline.tsx +10 -0
  179. package/src/chat/ui/Timeline.tsx +141 -1
  180. package/src/chat/ui/TimelineMenu.tsx +5 -0
  181. package/src/chat/ui/UnifiedDiff.tsx +6 -0
  182. package/src/chat/ui/UploadThumb.tsx +2 -0
  183. package/src/chat/ui/VisualDialogs.tsx +3 -0
  184. package/src/chat/ui/VisualFrame.tsx +15 -0
  185. package/src/chat/ui/Welcome.tsx +1 -0
  186. package/src/chat/ui/chip-clipboard.ts +20 -0
  187. package/src/chat/ui/code-lines.ts +14 -0
  188. package/src/chat/ui/code-theme.ts +1 -0
  189. package/src/chat/ui/composer/ComposerControls.tsx +70 -15
  190. package/src/chat/ui/composer/ComposerMenu.tsx +12 -0
  191. package/src/chat/ui/composer/DraftParts.tsx +2 -0
  192. package/src/chat/ui/composer/QueuedMessages.tsx +4 -0
  193. package/src/chat/ui/composer/chip-paste.ts +16 -0
  194. package/src/chat/ui/composer/chips.ts +6 -0
  195. package/src/chat/ui/composer/editor.ts +9 -0
  196. package/src/chat/ui/composer/insert.ts +2 -0
  197. package/src/chat/ui/composer/key-handler.ts +42 -9
  198. package/src/chat/ui/composer/keys.ts +23 -0
  199. package/src/chat/ui/composer/placeholder.ts +6 -0
  200. package/src/chat/ui/composer/prompt-picks.ts +18 -0
  201. package/src/chat/ui/composer/surface.ts +5 -0
  202. package/src/chat/ui/composer/use-chat-controls.ts +13 -0
  203. package/src/chat/ui/composer/use-compaction-offer.ts +3 -0
  204. package/src/chat/ui/composer/use-composer-menus.ts +20 -275
  205. package/src/chat/ui/composer/use-composer-submit.ts +27 -0
  206. package/src/chat/ui/composer/use-continuable-turn.ts +3 -5
  207. package/src/chat/ui/composer/use-draft-files.ts +24 -0
  208. package/src/chat/ui/composer/use-effort-stepper.ts +8 -0
  209. package/src/chat/ui/composer/use-notice.ts +4 -1
  210. package/src/chat/ui/composer/use-pending-requests.ts +3 -0
  211. package/src/chat/ui/composer/use-prompt-history.ts +20 -11
  212. package/src/chat/ui/composer/use-prompt-hold.ts +3 -0
  213. package/src/chat/ui/composer/use-prompt-menus.ts +361 -0
  214. package/src/chat/ui/composer/use-take-back.ts +20 -2
  215. package/src/chat/ui/diff/ApprovalReview.tsx +18 -15
  216. package/src/chat/ui/diff/DiffFallbackBoundary.tsx +1 -0
  217. package/src/chat/ui/diff/DiffOmitted.tsx +1 -0
  218. package/src/chat/ui/diff/DiffStack.tsx +2 -0
  219. package/src/chat/ui/diff/DiffUnavailable.tsx +1 -0
  220. package/src/chat/ui/diff/EditorEditDiff.tsx +13 -0
  221. package/src/chat/ui/diff/EditorPatchDiff.tsx +4 -0
  222. package/src/chat/ui/diff/LegacyDiffPool.tsx +2 -0
  223. package/src/chat/ui/diff/LegacyEditDiff.tsx +2 -0
  224. package/src/chat/ui/diff/LegacyUnifiedDiff.tsx +8 -0
  225. package/src/chat/ui/diff/RawDiff.tsx +1 -0
  226. package/src/chat/ui/diff/ReviewDiff.tsx +1 -0
  227. package/src/chat/ui/diff/diff-source-budget.ts +8 -0
  228. package/src/chat/ui/diff/editor-edit-model.ts +4 -0
  229. package/src/chat/ui/diff/review-plan.ts +9 -0
  230. package/src/chat/ui/diff/review-settle.ts +1 -0
  231. package/src/chat/ui/diff/syntax-runtime.ts +52 -0
  232. package/src/chat/ui/diff/use-review-stack.ts +9 -0
  233. package/src/chat/ui/diff-syntax.ts +12 -0
  234. package/src/chat/ui/diff-theme.ts +2 -0
  235. package/src/chat/ui/file-links.ts +2 -0
  236. package/src/chat/ui/find-reveal.ts +1 -0
  237. package/src/chat/ui/full-diff.ts +1 -0
  238. package/src/chat/ui/highlighter.ts +29 -15
  239. package/src/chat/ui/icons.tsx +2 -4
  240. package/src/chat/ui/index.ts +7 -0
  241. package/src/chat/ui/intelligent-ui/UiBlockFrame.tsx +25 -0
  242. package/src/chat/ui/intelligent-ui/UiFallbackPart.tsx +1 -0
  243. package/src/chat/ui/intelligent-ui/UiReply.tsx +63 -0
  244. package/src/chat/ui/intelligent-ui/block-local.ts +5 -0
  245. package/src/chat/ui/intelligent-ui/chart-data.ts +15 -0
  246. package/src/chat/ui/intelligent-ui/node-text.ts +17 -0
  247. package/src/chat/ui/intelligent-ui/renderers/binding.ts +1 -0
  248. package/src/chat/ui/intelligent-ui/renderers/chart.tsx +31 -0
  249. package/src/chat/ui/intelligent-ui/renderers/choices.tsx +4 -0
  250. package/src/chat/ui/intelligent-ui/renderers/content.tsx +18 -0
  251. package/src/chat/ui/intelligent-ui/renderers/data.tsx +11 -0
  252. package/src/chat/ui/intelligent-ui/renderers/inputs.tsx +13 -0
  253. package/src/chat/ui/intelligent-ui/renderers/links.tsx +9 -0
  254. package/src/chat/ui/intelligent-ui/renderers/map-slots.ts +15 -0
  255. package/src/chat/ui/intelligent-ui/renderers/map-style.ts +15 -0
  256. package/src/chat/ui/intelligent-ui/renderers/map-view.tsx +48 -0
  257. package/src/chat/ui/intelligent-ui/renderers/map.tsx +20 -0
  258. package/src/chat/ui/intelligent-ui/renderers/place-point.ts +3 -0
  259. package/src/chat/ui/intelligent-ui/renderers/structure.tsx +3 -0
  260. package/src/chat/ui/intelligent-ui/renderers/text.tsx +8 -0
  261. package/src/chat/ui/intelligent-ui/reply-parts.ts +5 -0
  262. package/src/chat/ui/intelligent-ui/table-data.ts +17 -0
  263. package/src/chat/ui/intelligent-ui/use-in-view.ts +4 -0
  264. package/src/chat/ui/intelligent-ui/use-later.ts +4 -0
  265. package/src/chat/ui/intelligent-ui/use-truncated.ts +5 -0
  266. package/src/chat/ui/intelligent-ui/use-ui-links.ts +29 -0
  267. package/src/chat/ui/intelligent-ui/use-ui-queries.ts +60 -0
  268. package/src/chat/ui/markdown-blocks.ts +64 -0
  269. package/src/chat/ui/mermaid.ts +4 -0
  270. package/src/chat/ui/quote-selection.ts +10 -0
  271. package/src/chat/ui/rehype-chips.ts +5 -0
  272. package/src/chat/ui/rehype-fade.ts +2 -0
  273. package/src/chat/ui/remark-html-as-text.ts +5 -0
  274. package/src/chat/ui/reveal.ts +18 -0
  275. package/src/chat/ui/rows/ForksRow.tsx +5 -0
  276. package/src/chat/ui/rows/GeneratedImageRow.tsx +10 -0
  277. package/src/chat/ui/rows/MessageRows.tsx +29 -5
  278. package/src/chat/ui/rows/RowContainer.tsx +1 -0
  279. package/src/chat/ui/rows/Rows.tsx +2 -0
  280. package/src/chat/ui/rows/SubagentRow.tsx +13 -0
  281. package/src/chat/ui/rows/TaskRow.tsx +7 -0
  282. package/src/chat/ui/rows/VisualRow.tsx +4 -0
  283. package/src/chat/ui/rows/WorkRows.tsx +46 -0
  284. package/src/chat/ui/rows/WorkflowRow.tsx +10 -0
  285. package/src/chat/ui/rows/row-rhythm.ts +7 -5
  286. package/src/chat/ui/theme-watch.ts +8 -7
  287. package/src/chat/ui/thread-references.ts +7 -0
  288. package/src/chat/ui/use-diff-source-tokens.ts +8 -0
  289. package/src/chat/ui/useToggleSet.ts +3 -0
  290. package/src/chat/ui/visual-theme.ts +30 -0
  291. package/src/chat/visual-bridge.ts +23 -0
  292. package/src/chat/visual-viewport.ts +12 -0
  293. package/src/host-hooks.ts +15 -5
  294. package/src/host.ts +29 -1
  295. package/src/index.ts +5 -0
  296. package/src/lazy.tsx +9 -0
  297. package/src/locales/en/agent-chat.ts +1 -0
  298. package/src/locales/nl/agent-chat.ts +1 -0
  299. package/src/mounted-registry.ts +4 -0
  300. package/src/port-transport.ts +32 -0
  301. package/src/prompts/index.ts +2 -0
  302. package/src/prompts/logic/focus.ts +5 -0
  303. package/src/prompts/logic/keys.ts +12 -0
  304. package/src/prompts/logic/prompts.ts +8 -0
  305. package/src/prompts/logic/subjects.ts +8 -0
  306. package/src/prompts/logic/useFocusAfterAnswer.ts +2 -0
  307. package/src/prompts/logic/usePromptSession.ts +13 -1
  308. package/src/prompts/ui/ApprovalBody.tsx +2 -0
  309. package/src/prompts/ui/PromptActions.tsx +2 -0
  310. package/src/prompts/ui/PromptCard.tsx +6 -0
  311. package/src/prompts/ui/PromptView.tsx +6 -0
  312. package/src/prompts/ui/QuestionBody.tsx +11 -0
  313. package/src/providers/AccountColors.tsx +1 -0
  314. package/src/providers/AccountDetail.tsx +7 -0
  315. package/src/providers/AccountVariables.tsx +2 -0
  316. package/src/providers/AddAccountForm.tsx +5 -1
  317. package/src/providers/CliDetail.tsx +6 -5
  318. package/src/providers/InstallPage.tsx +11 -0
  319. package/src/providers/ProviderList.tsx +3 -0
  320. package/src/providers/ProvidersPane.tsx +18 -0
  321. package/src/providers/account-actions.ts +10 -8
  322. package/src/providers/index.ts +1 -0
  323. package/src/providers/install-steps.ts +3 -0
  324. package/src/providers/pane-target.ts +5 -0
  325. package/src/providers/provider-abilities.ts +1 -0
  326. package/src/providers/provider-groups.ts +6 -0
  327. package/src/providers/variables.ts +12 -0
  328. package/src/scope.ts +2 -0
  329. package/src/state/chats.ts +61 -14
  330. package/src/state/index.ts +2 -0
  331. package/src/state/provider-accounts.ts +7 -9
  332. package/src/state/providers.ts +1 -0
  333. package/src/state/usage.ts +6 -0
  334. package/src/storage.ts +18 -0
  335. package/src/testing/prompts.ts +5 -0
  336. package/src/theme.css +3 -0
  337. package/src/transport.ts +18 -0
  338. package/src/usage/ChartLogo.tsx +12 -0
  339. package/src/usage/LimitsList.tsx +7 -8
  340. package/src/usage/UsageBreakdown.tsx +11 -4
  341. package/src/usage/UsageChart.tsx +5 -0
  342. package/src/usage/UsageLimits.tsx +10 -7
  343. package/src/usage/UsageLimitsCard.tsx +2 -0
  344. package/src/usage/UsagePage.tsx +16 -19
  345. package/src/usage/UsageSummary.tsx +4 -5
  346. package/src/usage/UsageTiles.tsx +2 -0
  347. package/src/usage/format.ts +7 -0
  348. package/src/usage/limit-groups.ts +14 -0
  349. package/src/usage/limits.ts +11 -13
  350. package/src/usage/money.ts +1 -0
  351. package/src/usage/summary.ts +15 -7
@@ -0,0 +1,136 @@
1
+ import { useLayoutEffect, useRef, type SetStateAction } from 'react';
2
+ import clsx from 'clsx';
3
+ import { Surface } from '@adecore/ui';
4
+ import type { ChatSkill } from '@adecore/agent-contracts';
5
+ import type { Referables } from '../chat-references';
6
+ import { ComposerInput, type ComposerInputHandle } from './ComposerInput';
7
+ import { chipDecorations } from './composer/chips';
8
+ import { ComposerMenu } from './composer/ComposerMenu';
9
+ import { pickInMenu } from './composer/key-handler';
10
+ import { usePromptMenus, type PromptDraft } from './composer/use-prompt-menus';
11
+
12
+ interface PromptEditorProps {
13
+ value: PromptDraft;
14
+ onChange(next: PromptDraft): void;
15
+ /* The editor's accessible name; a label element cannot point at the editable element. */
16
+ label: string;
17
+ placeholder: string;
18
+ skills: readonly ChatSkill[];
19
+ /* What the CLI answers after `/`, besides `/goal` where `goals` is set. */
20
+ slashCommands?: readonly string[];
21
+ goals?: boolean;
22
+ /* Where `@` searches for files; null offers none. */
23
+ cwd: string | null;
24
+ referables: Referables;
25
+ providerName?: string;
26
+ disabled?: boolean;
27
+ className?: string;
28
+ }
29
+
30
+ const NO_COMMANDS: readonly string[] = [];
31
+
32
+ /*
33
+ * A prompt written now and sent later, as a form field: the composer's editor with its pickers and chips,
34
+ * without a chat behind it. The host hands in what the CLI it goes to has, and keeps what was picked.
35
+ */
36
+ export function PromptEditor({
37
+ value,
38
+ onChange,
39
+ label,
40
+ placeholder,
41
+ skills,
42
+ slashCommands = NO_COMMANDS,
43
+ goals = false,
44
+ cwd,
45
+ referables,
46
+ providerName,
47
+ disabled = false,
48
+ className
49
+ }: PromptEditorProps) {
50
+ const inputRef = useRef<ComposerInputHandle>(null);
51
+ // A pick writes twice in one event (the text, then what it picked), and the second must build on the first.
52
+ const latest = useRef(value);
53
+
54
+ useLayoutEffect(() => {
55
+ latest.current = value;
56
+ }, [value]);
57
+
58
+ const setDraft = (action: SetStateAction<PromptDraft>): void => {
59
+ const next = typeof action === 'function' ? action(latest.current) : action;
60
+ latest.current = next;
61
+ onChange(next);
62
+ };
63
+
64
+ const setText = (next: string, mentions = value.mentions, chosen = value.skills): void => {
65
+ setDraft((current) => ({ ...current, text: next, mentions, skills: chosen }));
66
+ };
67
+
68
+ const menus = usePromptMenus({
69
+ draft: { draft: value, setDraft, setText },
70
+ inputRef,
71
+ cwd,
72
+ skills,
73
+ slashCommands,
74
+ goals,
75
+ providerName,
76
+ referables,
77
+ label
78
+ });
79
+
80
+ const extensions = chipDecorations({ mentions: value.mentions, skills: value.skills, references: menus.referenceChips });
81
+
82
+ const onKeyDown = (e: KeyboardEvent): boolean => {
83
+ if (e.isComposing) {
84
+ return false;
85
+ }
86
+
87
+ // Escape closes the picker, and only that: the dialog around the field stays.
88
+ if (e.key === 'Escape' && menus.menuOpen) {
89
+ menus.close();
90
+ e.stopPropagation();
91
+
92
+ return true;
93
+ }
94
+
95
+ // Enter adds a line here rather than sending, so on a command it completes like Tab.
96
+ if (e.key === 'Enter' && menus.commandMenuOpen) {
97
+ menus.completeCommand(menus.commands[menus.activeIndex]!);
98
+
99
+ return true;
100
+ }
101
+
102
+ return pickInMenu(e, menus);
103
+ };
104
+
105
+ return (
106
+ <div className={clsx('relative', className)}>
107
+ {/* Over the form under the field, not inside it, so what a person types never moves while the picker opens and closes. */}
108
+ {menus.menuOpen && (
109
+ <Surface className="absolute inset-x-0 top-full z-50 mt-1 overflow-hidden rounded-lg bg-clip-padding *:border-b-0">
110
+ <ComposerMenu menus={menus} />
111
+ </Surface>
112
+ )}
113
+ <div className="field h-auto overflow-hidden p-0 focus-ring-within">
114
+ <ComposerInput
115
+ ref={inputRef}
116
+ dictationToolbar={null}
117
+ className="composer-input prompt-editor select-text text-sm leading-normal text-text"
118
+ value={value.text}
119
+ placeholder={placeholder}
120
+ disabled={disabled}
121
+ tabbable
122
+ extensions={extensions}
123
+ aria={menus.inputAria}
124
+ onChange={(text, selection, state) => {
125
+ setText(text);
126
+ menus.setMenuIndex(0);
127
+ menus.trackTriggers(text, selection, state);
128
+ }}
129
+ onSelectionChange={menus.trackTriggers}
130
+ onBlur={menus.close}
131
+ onKeyDown={onKeyDown}
132
+ />
133
+ </div>
134
+ </div>
135
+ );
136
+ }
@@ -18,6 +18,7 @@ interface Offer {
18
18
  /* The last line the range covers. A triple-click ends on an empty rect at the start of the next row. */
19
19
  function lastLineOf(range: Range): DOMRect | null {
20
20
  const rects = [...range.getClientRects()].filter((rect) => rect.width > 0);
21
+
21
22
  return rects.at(-1) ?? null;
22
23
  }
23
24
 
@@ -39,15 +40,19 @@ export function QuoteButton({
39
40
 
40
41
  useEffect(() => {
41
42
  let pressed = false;
43
+
42
44
  const read = (): void => {
43
45
  const selection = document.getSelection();
44
46
  const quote = taker.current === null ? null : selectedAnswerQuote(selection, thread.current);
45
47
  const box = frame.current;
46
48
  const line = quote === null || selection === null ? null : lastLineOf(selection.getRangeAt(0));
49
+
47
50
  if (quote === null || box === null || line === null) {
48
51
  setOffer(null);
52
+
49
53
  return;
50
54
  }
55
+
51
56
  const bounds = box.getBoundingClientRect();
52
57
  // The canvas may zoom the cell; the button is placed in the frame's own pixels.
53
58
  const zoom = box.offsetWidth > 0 ? bounds.width / box.offsetWidth : 1;
@@ -55,23 +60,30 @@ export function QuoteButton({
55
60
  const y = (line.bottom - bounds.top) / zoom + GAP_PX;
56
61
  setOffer({ quote: quote.text, itemId: quote.itemId, x: Math.round(x), y: Math.round(y) });
57
62
  };
63
+
58
64
  const press = (): void => {
59
65
  pressed = true;
60
66
  };
67
+
61
68
  const release = (): void => {
62
69
  pressed = false;
63
70
  read();
64
71
  };
72
+
65
73
  const change = (): void => {
66
74
  if (pressed) {
67
75
  setOffer(null);
76
+
68
77
  return;
69
78
  }
79
+
70
80
  read();
71
81
  };
82
+
72
83
  document.addEventListener('pointerdown', press, true);
73
84
  document.addEventListener('pointerup', release, true);
74
85
  document.addEventListener('selectionchange', change);
86
+
75
87
  return () => {
76
88
  document.removeEventListener('pointerdown', press, true);
77
89
  document.removeEventListener('pointerup', release, true);
@@ -31,6 +31,7 @@ export function TokenChip({ kind, value, sigil, icon, label, tone, onOpen, class
31
31
  onOpen();
32
32
  }
33
33
  };
34
+
34
35
  return (
35
36
  <span
36
37
  {...rest}
@@ -55,11 +56,13 @@ export function ReferenceChip({ id }: { id: string }) {
55
56
  const { t } = useTranslation('agent-chat');
56
57
  const { items, open } = useContext(ReferenceLookupContext);
57
58
  const item = items.find((candidate) => candidate.id === id);
59
+
58
60
  if (item === undefined) {
59
61
  return (
60
62
  <TokenChip kind="gone" icon={<Icon icon={CircleOff} size={14} className={TOKEN_ICON} />} label={t('references.gone')} tone={REFERENCE_GONE_TONE} />
61
63
  );
62
64
  }
65
+
63
66
  return (
64
67
  <TokenChip
65
68
  kind="reference"
@@ -20,6 +20,7 @@ interface ResumeCompactionDockProps {
20
20
  export function ResumeCompactionDock({ tokens, reason = 'idle', onCompact, onDismiss }: ResumeCompactionDockProps) {
21
21
  const { t } = useTranslation('agent-chat');
22
22
  const cold = reason === 'cache-expired';
23
+
23
24
  return (
24
25
  <div className="flex items-center gap-2 border-b border-border px-3.5 py-2.5">
25
26
  <Icon icon={Minimize2} size={16} className="shrink-0 text-text-faint" />
@@ -8,6 +8,7 @@ import { TIMESTAMP_FORMAT } from './timestamp-format';
8
8
  */
9
9
  export function RowTime({ at }: { at: number }) {
10
10
  useFormatLocale();
11
+
11
12
  return (
12
13
  <Tooltip label={formatDateTime(at, TIMESTAMP_FORMAT)}>
13
14
  <time
@@ -92,9 +92,11 @@ interface AccountPick {
92
92
 
93
93
  function optionSummary(option: ModelOptionDescriptor, selection: ModelSelection): string | null {
94
94
  const value = selection.options[option.id];
95
+
95
96
  if (option.type === 'select') {
96
97
  return option.choices.find((choice) => choice.id === (value ?? option.defaultChoice))?.label ?? null;
97
98
  }
99
+
98
100
  return (value ?? option.defaultValue) === true ? option.label : null;
99
101
  }
100
102
 
@@ -148,16 +150,21 @@ export function RunSettings({
148
150
  const current = modelName(selection.model, owner?.models);
149
151
  const short = shortModelName(current, owner?.models);
150
152
  const options = orderOptions((model?.options ?? []).map(localizedOption));
153
+
151
154
  const summary = options.flatMap((option) => {
152
155
  const label = optionSummary(option, selection);
156
+
153
157
  return label === null ? [] : [{ id: option.id, label, ring: option.id === CONTEXT_OPTION }];
154
158
  });
159
+
155
160
  // A CLI without a context choice still fills one, so the ring stands on its own next to the size.
156
161
  const hasContextOption = options.some((option) => option.id === CONTEXT_OPTION);
157
162
  const standaloneContext = !hasContextOption && Boolean(usage.contextWindow);
163
+
158
164
  if (standaloneContext) {
159
165
  summary.push({ id: CONTEXT_OPTION, label: formatTokens(usage.contextWindow ?? 0), ring: true });
160
166
  }
167
+
161
168
  const grouped = providers.length > 1;
162
169
  const live = modelGroupsOf(providers, false);
163
170
  const legacy = modelGroupsOf(providers, true);
@@ -176,7 +183,9 @@ export function RunSettings({
176
183
 
177
184
  const searchable = wantsModelSearch(providers);
178
185
  const favorites = useChatPreferences((preferences) => preferences.favoriteModels);
186
+
179
187
  const favoritesOf = (kind: AgentKind): ReadonlySet<string> => favoriteModelsOf(favorites, kind);
188
+
180
189
  const found = query.trim() === '' ? null : searchModels(inPickerOrder(providers), query, favoritesOf);
181
190
 
182
191
  // The menu moves the focus to its popup as it opens, so the search field takes it a frame later.
@@ -184,7 +193,9 @@ export function RunSettings({
184
193
  if (!open || !searchable) {
185
194
  return;
186
195
  }
196
+
187
197
  const frame = requestAnimationFrame(() => searchRef.current?.focus());
198
+
188
199
  return () => cancelAnimationFrame(frame);
189
200
  }, [open, searchable]);
190
201
 
@@ -192,6 +203,7 @@ export function RunSettings({
192
203
  if (!next) {
193
204
  setQuery('');
194
205
  }
206
+
195
207
  onOpenChange(next);
196
208
  };
197
209
 
@@ -200,10 +212,13 @@ export function RunSettings({
200
212
  if (!searchable || event.target === searchRef.current || event.metaKey || event.ctrlKey || event.altKey) {
201
213
  return;
202
214
  }
215
+
203
216
  const typed = event.key.length === 1 && event.key !== ' ';
217
+
204
218
  if (!typed && event.key !== 'Backspace') {
205
219
  return;
206
220
  }
221
+
207
222
  event.preventDefault();
208
223
  event.stopPropagation();
209
224
  setQuery((now) => (typed ? now + event.key : now.slice(0, -1)));
@@ -216,17 +231,22 @@ export function RunSettings({
216
231
  event.preventDefault();
217
232
  event.stopPropagation();
218
233
  setQuery('');
234
+
219
235
  return;
220
236
  }
237
+
221
238
  if (event.key === 'Enter') {
222
239
  event.preventDefault();
223
240
  const first = found?.[0];
241
+
224
242
  if (first !== undefined) {
225
243
  onModel(first.entry.kind, first.models[0]!.slug);
226
244
  changeOpen(false);
227
245
  }
246
+
228
247
  return;
229
248
  }
249
+
230
250
  if (event.key !== 'Escape' && event.key !== 'ArrowDown' && event.key !== 'ArrowUp' && event.key !== 'Tab') {
231
251
  event.stopPropagation();
232
252
  }
@@ -235,6 +255,7 @@ export function RunSettings({
235
255
  // The name wraps rather than truncates, so a long name a provider gives is read whole.
236
256
  const modelRow = (entry: ProviderInfo, row: ModelInfo) => {
237
257
  const starred = favoritesOf(entry.kind).has(row.slug);
258
+
238
259
  return (
239
260
  <Menu.Row key={row.slug} aria-label={row.name}>
240
261
  <Menu.RadioItem value={modelValue(entry.kind, row.slug)} closeOnClick>
@@ -256,10 +277,13 @@ export function RunSettings({
256
277
  // Every model of one provider, the starred ones first and the rest under a label per maker where the provider names them.
257
278
  const byMaker = (entry: ProviderInfo, models: ModelInfo[]) => {
258
279
  const starred = favoritesOf(entry.kind);
280
+
259
281
  return favoritesFirst(models, starred).map((row, i, sorted) => {
260
282
  const previous = sorted[i - 1];
283
+
261
284
  const labeled =
262
285
  row.vendor !== undefined && !starred.has(row.slug) && (previous === undefined || starred.has(previous.slug) || previous.vendor !== row.vendor);
286
+
263
287
  return (
264
288
  <Fragment key={row.slug}>
265
289
  {labeled && <Menu.GroupLabel>{row.vendor}</Menu.GroupLabel>}
@@ -273,9 +297,11 @@ export function RunSettings({
273
297
  const providerModels = (entry: ProviderInfo, models: ModelInfo[]) => {
274
298
  const starred = favoritesOf(entry.kind);
275
299
  const shortlist = shortlistOf(entry.kind, models, { provider, model: selection.model }, starred);
300
+
276
301
  if (shortlist === null) {
277
302
  return [...models.filter((row) => starred.has(row.slug)), ...models.filter((row) => !starred.has(row.slug))].map((row) => modelRow(entry, row));
278
303
  }
304
+
279
305
  return (
280
306
  <>
281
307
  {shortlist.map((row) => modelRow(entry, row))}
@@ -468,6 +494,7 @@ function AccountSubmenu({ provider, account }: { provider: AgentKind; account: A
468
494
  const host = useChatHost();
469
495
  const { t } = useTranslation('agent-chat');
470
496
  const { choice, started, onPick } = account;
497
+
471
498
  return (
472
499
  <Menu.SubmenuRoot>
473
500
  <Menu.SubmenuTrigger className="text-text-muted">
@@ -502,9 +529,11 @@ function AccountRows({ choice, locked }: { choice: AccountChoice; locked(id: str
502
529
  const { t } = useTranslation('agent-chat');
503
530
  const limits = useUsageLimits();
504
531
  const now = useNow(MINUTE_MS);
532
+
505
533
  return choice.offered.map((entry) => {
506
534
  const session = sessionWindow(limitsOfAccount(limits, entry.id));
507
535
  const disabled = locked(entry.id);
536
+
508
537
  const row = (
509
538
  <Menu.RadioItem key={entry.id} value={entry.id} disabled={disabled} closeOnClick className="items-start data-disabled:opacity-50">
510
539
  <span className="flex min-w-0 grow flex-col">
@@ -516,6 +545,7 @@ function AccountRows({ choice, locked }: { choice: AccountChoice; locked(id: str
516
545
  </span>
517
546
  </Menu.RadioItem>
518
547
  );
548
+
519
549
  return disabled ? (
520
550
  <Tooltip key={entry.id} label={t('pickers.account.forkToSwitch', { account: choice.nameOf(entry) })} side="right" sideOffset={12}>
521
551
  {row}
@@ -529,6 +559,7 @@ function AccountRows({ choice, locked }: { choice: AccountChoice; locked(id: str
529
559
  /* How full the context is, drawn small enough to sit in the pill beside the window's size. */
530
560
  function ContextRing({ usage }: { usage: ChatUsage }) {
531
561
  const fraction = contextFraction(usage);
562
+
532
563
  return (
533
564
  <svg width="14" height="14" viewBox="0 0 14 14" className={clsx('shrink-0', contextTone(fraction))} aria-hidden="true">
534
565
  <circle cx="7" cy="7" r={RING_RADIUS} fill="none" strokeWidth="2" className="stroke-border-strong" />
@@ -554,6 +585,7 @@ function ContextUsage({ usage, disabled, onCompact }: { usage: ChatUsage; disabl
554
585
  const percent = Math.round(fraction * 100);
555
586
  const segments = contextSegments(usage);
556
587
  const used = formatTokens(usage.contextTokens);
588
+
557
589
  return (
558
590
  <Menu.Group className="flex flex-col gap-2 px-2.5 py-1.5">
559
591
  <div className="flex items-center gap-2">
@@ -623,14 +655,17 @@ function ContextUsage({ usage, disabled, onCompact }: { usage: ChatUsage; disabl
623
655
  */
624
656
  function OptionRow({ option, selection, onChange }: { option: ModelOptionDescriptor; selection: ModelSelection; onChange(value: string | boolean): void }) {
625
657
  const icon = <Icon icon={optionIcon(option)} size={14} className="shrink-0 text-text-faint" />;
658
+
626
659
  if (option.type === 'boolean') {
627
660
  const checked = (selection.options[option.id] ?? option.defaultValue) === true;
661
+
628
662
  const item = (
629
663
  <Menu.CheckboxItem indicator="end" className="text-text-muted" checked={checked} onCheckedChange={onChange} closeOnClick={false}>
630
664
  {icon}
631
665
  <span className="min-w-0 grow truncate">{option.label}</span>
632
666
  </Menu.CheckboxItem>
633
667
  );
668
+
634
669
  return option.description ? (
635
670
  <Tooltip label={option.description} side="right" sideOffset={12}>
636
671
  {item}
@@ -639,7 +674,9 @@ function OptionRow({ option, selection, onChange }: { option: ModelOptionDescrip
639
674
  item
640
675
  );
641
676
  }
677
+
642
678
  const value = String(selection.options[option.id] ?? option.defaultChoice);
679
+
643
680
  if (option.choices.length > INLINE_CHOICES) {
644
681
  return (
645
682
  <Menu.SubmenuRoot>
@@ -663,6 +700,7 @@ function OptionRow({ option, selection, onChange }: { option: ModelOptionDescrip
663
700
  </Menu.SubmenuRoot>
664
701
  );
665
702
  }
703
+
666
704
  return (
667
705
  <div className="flex items-center gap-2.5 px-2.5 py-1.5 text-sm text-text-muted">
668
706
  {icon}
@@ -99,8 +99,10 @@ export const Scrubber = memo(function Scrubber({
99
99
  if (strip === null || typeof ResizeObserver === 'undefined') {
100
100
  return;
101
101
  }
102
+
102
103
  const observer = new ResizeObserver(() => setHeight(strip.offsetHeight));
103
104
  observer.observe(strip);
105
+
104
106
  return () => observer.disconnect();
105
107
  }, [strip]);
106
108
 
@@ -115,13 +117,16 @@ export const Scrubber = memo(function Scrubber({
115
117
 
116
118
  const anchor = useMemo(() => {
117
119
  const element = strip;
120
+
118
121
  if (anchorY === null || element === null) {
119
122
  return undefined;
120
123
  }
124
+
121
125
  return {
122
126
  contextElement: element,
123
127
  getBoundingClientRect: () => {
124
128
  const rect = element.getBoundingClientRect();
129
+
125
130
  return DOMRect.fromRect({ x: rect.left, y: rect.top + anchorY + TICK_HEIGHT_PX / 2, width: rect.width, height: 0 });
126
131
  }
127
132
  };
@@ -131,10 +136,13 @@ export const Scrubber = memo(function Scrubber({
131
136
 
132
137
  const actionsFor = (tick: ScrubberTick | null): CardAction[] =>
133
138
  tick === null ? [] : Object.values(CARD_ACTIONS).filter((action) => action.offers(tick, chat));
139
+
134
140
  const actions = actionsFor(hoveredTick);
141
+
135
142
  /* The message a bookmark goes on: the one it is on already, or the person's own. A wake is the machine's and holds none. */
136
143
  const markableOf = (tick: ScrubberTick | null): string | null =>
137
144
  tick === null || chat === null ? null : (tick.bookmark?.itemId ?? (tick.kind === 'person' ? tick.id : null));
145
+
138
146
  const menuTick = menuIndex === null ? null : (ticks[menuIndex] ?? null);
139
147
  const menuMarkable = markableOf(menuTick);
140
148
 
@@ -142,10 +150,13 @@ export const Scrubber = memo(function Scrubber({
142
150
  // The hover card is a child here as well, and a click in it is about no tick.
143
151
  const index = strip === null || cameThroughPortal(e) ? null : messageAt(layout, e.clientY - strip.getBoundingClientRect().top);
144
152
  const tick = index === null ? null : (ticks[index] ?? null);
153
+
145
154
  if (tick === null || (actionsFor(tick).length === 0 && markableOf(tick) === null)) {
146
155
  e.preventBaseUIHandler();
156
+
147
157
  return;
148
158
  }
159
+
149
160
  setMenuIndex(index);
150
161
  };
151
162
 
@@ -165,6 +176,7 @@ export const Scrubber = memo(function Scrubber({
165
176
  onPointerMove={(e) => {
166
177
  setPointing(true);
167
178
  const index = pointAt(e);
179
+
168
180
  if (index !== hovered) {
169
181
  setHovered(index);
170
182
  }
@@ -172,6 +184,7 @@ export const Scrubber = memo(function Scrubber({
172
184
  onPointerLeave={() => setPointing(false)}
173
185
  onClick={(e) => {
174
186
  const index = pointAt(e);
187
+
175
188
  if (index !== null) {
176
189
  onPick(index);
177
190
  }
@@ -183,6 +196,7 @@ export const Scrubber = memo(function Scrubber({
183
196
  const distance = pointing && hoveredSlot !== null ? Math.abs(index - hoveredSlot) : null;
184
197
  // A bookmark is a place to find again from anywhere, so it never dims with the rest.
185
198
  const bright = slot.marked || slotInView(slot, inView) || distance === 0;
199
+
186
200
  return (
187
201
  <Fragment key={slot.first}>
188
202
  <span
@@ -20,6 +20,7 @@ export function SessionLine({ window, now }: { window: UsageWindow; now: number
20
20
  const tone = sessionTone(window.used);
21
21
  const mark = evenPace(window, now)?.percent ?? null;
22
22
  const resetsAt = window.resetsAt;
23
+
23
24
  return (
24
25
  <span className="flex items-center gap-2 text-xs text-text-faint tabular-nums">
25
26
  <span aria-hidden className="relative h-1 w-14 shrink-0 rounded-full bg-surface-sunken">
@@ -32,6 +32,7 @@ const cache = new Map<string, Run[]>();
32
32
  function runsOf(highlighter: Highlighter, code: string, language: string, theme: string, offset: number): Run[] {
33
33
  const fg = highlighter.getTheme(theme).fg.toLowerCase();
34
34
  const { tokens } = highlighter.codeToTokens(code, { lang: language as BundledLanguage, theme });
35
+
35
36
  return tokens.flat().map((token) => ({
36
37
  start: offset + token.offset,
37
38
  end: offset + token.offset + token.content.length,
@@ -44,18 +45,23 @@ function runsOf(highlighter: Highlighter, code: string, language: string, theme:
44
45
  /* `base` with `[start, end)` cut out of it and `inner` in its place. */
45
46
  function overlay(base: readonly Run[], start: number, end: number, inner: readonly Run[]): Run[] {
46
47
  const kept: Run[] = [];
48
+
47
49
  for (const run of base) {
48
50
  if (run.end <= start || run.start >= end) {
49
51
  kept.push(run);
52
+
50
53
  continue;
51
54
  }
55
+
52
56
  if (run.start < start) {
53
57
  kept.push({ ...run, end: start, content: run.content.slice(0, start - run.start) });
54
58
  }
59
+
55
60
  if (run.end > end) {
56
61
  kept.push({ ...run, start: end, content: run.content.slice(end - run.start) });
57
62
  }
58
63
  }
64
+
59
65
  return [...kept, ...inner].sort((left, right) => left.start - right.start);
60
66
  }
61
67
 
@@ -74,25 +80,34 @@ function languagesOf(command: string, extra: readonly EmbeddedCode[]): Set<strin
74
80
  function highlight(command: string, extra: readonly EmbeddedCode[], theme: string, code: CodeHighlighter): Run[] | null {
75
81
  const key = `${theme}\n${extra.map(({ start, end, language }) => `${start}:${end}:${language}`).join(',')}\n${command}`;
76
82
  const known = cache.get(key);
83
+
77
84
  if (known !== undefined) {
78
85
  // Touched entries move to the end, so the oldest falls out first.
79
86
  cache.delete(key);
80
87
  cache.set(key, known);
88
+
81
89
  return known;
82
90
  }
91
+
83
92
  const embedded = codeIn(command, extra);
84
93
  const highlighter = code.ready([SHELL_LANGUAGE, ...embedded.map(({ language }) => language)], theme);
94
+
85
95
  if (highlighter === null) {
86
96
  return null;
87
97
  }
98
+
88
99
  let runs = runsOf(highlighter, command, code.language(SHELL_LANGUAGE), theme, 0);
100
+
89
101
  for (const part of embedded) {
90
102
  runs = overlay(runs, part.start, part.end, runsOf(highlighter, command.slice(part.start, part.end), code.language(part.language), theme, part.start));
91
103
  }
104
+
92
105
  cache.set(key, runs);
106
+
93
107
  if (cache.size > CACHE_LIMIT) {
94
108
  cache.delete(cache.keys().next().value!);
95
109
  }
110
+
96
111
  return runs;
97
112
  }
98
113
 
@@ -101,20 +116,26 @@ function draw(command: string, runs: readonly Run[] | null, from: number, to: nu
101
116
  if (runs === null) {
102
117
  return [command.slice(from, to)];
103
118
  }
119
+
104
120
  const parts: ReactNode[] = [];
105
121
  let at = from;
122
+
106
123
  for (const run of runs) {
107
124
  if (run.end <= from || run.start >= to) {
108
125
  continue;
109
126
  }
127
+
110
128
  const start = Math.max(run.start, from);
111
129
  const end = Math.min(run.end, to);
130
+
112
131
  // Shiki leaves the line breaks out of its tokens; they come from the command itself.
113
132
  if (start > at) {
114
133
  parts.push(command.slice(at, start));
115
134
  }
135
+
116
136
  const content = run.content.slice(start - run.start, end - run.start);
117
137
  const style = tokenStyle(run);
138
+
118
139
  parts.push(
119
140
  style === undefined ? (
120
141
  content
@@ -124,11 +145,14 @@ function draw(command: string, runs: readonly Run[] | null, from: number, to: nu
124
145
  </span>
125
146
  )
126
147
  );
148
+
127
149
  at = end;
128
150
  }
151
+
129
152
  if (at < to) {
130
153
  parts.push(command.slice(at, to));
131
154
  }
155
+
132
156
  return parts;
133
157
  }
134
158
 
@@ -148,14 +172,18 @@ function Name({ named }: { named: NamedWord }) {
148
172
  function drawNamed(command: string, runs: readonly Run[] | null, names: readonly NamedWord[]): ReactNode[] {
149
173
  const parts: ReactNode[] = [];
150
174
  let at = 0;
175
+
151
176
  for (const named of names) {
152
177
  if (named.start < at) {
153
178
  continue;
154
179
  }
180
+
155
181
  parts.push(...draw(command, runs, at, named.start), <Name key={`name-${named.start}`} named={named} />);
156
182
  at = named.end;
157
183
  }
184
+
158
185
  parts.push(...draw(command, runs, at, command.length));
186
+
159
187
  return parts;
160
188
  }
161
189
 
@@ -180,10 +208,13 @@ export function ShellCommand({ command, code = NO_CODE, names = NO_NAMES }: { co
180
208
  if (runs !== null || tooLong) {
181
209
  return;
182
210
  }
211
+
183
212
  let cancelled = false;
213
+
184
214
  Promise.all([highlighter.loadTheme(theme, custom), ...[...languagesOf(command, code)].map((lang) => highlighter.loadLanguage(lang))])
185
215
  .then(() => !cancelled && setLoads((count) => count + 1))
186
216
  .catch(() => undefined);
217
+
187
218
  return () => {
188
219
  cancelled = true;
189
220
  };
@@ -192,5 +223,6 @@ export function ShellCommand({ command, code = NO_CODE, names = NO_NAMES }: { co
192
223
  if (names.length > 0) {
193
224
  return <>{drawNamed(command, runs, names)}</>;
194
225
  }
226
+
195
227
  return <>{runs === null ? command : draw(command, runs, 0, command.length)}</>;
196
228
  }