@adecore/agents-react 0.39.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 (317) hide show
  1. package/dist/agents/AgentModelLabel.js +2 -1
  2. package/dist/agents/model-name.js +2 -1
  3. package/dist/agents/provider-paths.d.ts +2 -1
  4. package/dist/agents/provider-paths.js +7 -3
  5. package/dist/chat/account-choice.js +57 -28
  6. package/dist/chat/bookmarks.js +2 -1
  7. package/dist/chat/drafts.d.ts +0 -1
  8. package/dist/chat/drafts.js +9 -28
  9. package/dist/chat/logic/picker-entries.js +2 -8
  10. package/dist/chat/logic/resume-compaction.js +2 -5
  11. package/dist/chat/logic/timeline-copy.js +2 -8
  12. package/dist/chat/logic/timeline.js +9 -10
  13. package/dist/chat/logic/workflow.js +2 -4
  14. package/dist/chat/subagent-conversation.js +2 -1
  15. package/dist/chat/ui/GoalDock.js +2 -1
  16. package/dist/chat/ui/LimitState.js +4 -6
  17. package/dist/chat/ui/composer/use-continuable-turn.js +0 -1
  18. package/dist/chat/ui/composer/use-notice.js +2 -1
  19. package/dist/chat/ui/composer/use-prompt-history.js +1 -1
  20. package/dist/chat/ui/composer/use-prompt-menus.js +216 -253
  21. package/dist/chat/ui/composer/use-take-back.js +2 -2
  22. package/dist/chat/ui/diff/ApprovalReview.js +1 -1
  23. package/dist/chat/ui/highlighter.d.ts +0 -6
  24. package/dist/chat/ui/highlighter.js +0 -12
  25. package/dist/chat/ui/icons.d.ts +0 -1
  26. package/dist/chat/ui/icons.js +0 -3
  27. package/dist/chat/ui/rows/MessageRows.js +2 -5
  28. package/dist/chat/ui/rows/row-rhythm.js +2 -5
  29. package/dist/chat/ui/theme-watch.js +3 -7
  30. package/dist/prompts/logic/usePromptSession.js +2 -1
  31. package/dist/providers/AddAccountForm.js +2 -1
  32. package/dist/providers/CliDetail.js +104 -130
  33. package/dist/providers/account-actions.js +6 -9
  34. package/dist/state/chats.d.ts +1 -0
  35. package/dist/state/chats.js +6 -11
  36. package/dist/state/provider-accounts.d.ts +1 -1
  37. package/dist/state/provider-accounts.js +2 -8
  38. package/dist/transport.d.ts +2 -0
  39. package/dist/transport.js +14 -0
  40. package/dist/usage/ChartLogo.d.ts +5 -0
  41. package/dist/usage/ChartLogo.js +48 -0
  42. package/dist/usage/LimitsList.js +6 -16
  43. package/dist/usage/UsageBreakdown.js +17 -40
  44. package/dist/usage/UsageLimits.js +183 -211
  45. package/dist/usage/UsagePage.js +3 -17
  46. package/dist/usage/UsageSummary.js +4 -9
  47. package/dist/usage/limits.js +16 -31
  48. package/dist/usage/summary.d.ts +1 -1
  49. package/dist/usage/summary.js +3 -5
  50. package/package.json +26 -26
  51. package/src/agents/AccountDot.tsx +1 -0
  52. package/src/agents/AgentIcon.tsx +2 -0
  53. package/src/agents/AgentModelLabel.tsx +4 -6
  54. package/src/agents/ModelOptionControl.tsx +8 -0
  55. package/src/agents/account-limits.ts +3 -0
  56. package/src/agents/accounts.ts +12 -0
  57. package/src/agents/model-name.ts +13 -1
  58. package/src/agents/model-options.ts +2 -0
  59. package/src/agents/model-requirement.ts +4 -0
  60. package/src/agents/option-words.ts +2 -0
  61. package/src/agents/provider-paths.ts +8 -3
  62. package/src/agents/status-look.ts +1 -0
  63. package/src/chat/account-choice.ts +25 -19
  64. package/src/chat/actions.ts +5 -0
  65. package/src/chat/attachments.ts +8 -0
  66. package/src/chat/bookmarks.ts +13 -1
  67. package/src/chat/chat-client.ts +63 -0
  68. package/src/chat/chat-references.ts +3 -0
  69. package/src/chat/composer-effort.ts +5 -0
  70. package/src/chat/composer-send.ts +4 -0
  71. package/src/chat/drafts.ts +21 -17
  72. package/src/chat/forks.ts +4 -0
  73. package/src/chat/guards.ts +2 -0
  74. package/src/chat/index.ts +7 -0
  75. package/src/chat/logic/ansi.ts +47 -0
  76. package/src/chat/logic/bookmarks.ts +7 -0
  77. package/src/chat/logic/command-looks.ts +29 -0
  78. package/src/chat/logic/context-usage.ts +3 -0
  79. package/src/chat/logic/continue-after-stop.ts +5 -0
  80. package/src/chat/logic/effort.ts +4 -0
  81. package/src/chat/logic/embedded-code.ts +63 -0
  82. package/src/chat/logic/fork.ts +22 -0
  83. package/src/chat/logic/generated-image.ts +7 -0
  84. package/src/chat/logic/goal.ts +7 -0
  85. package/src/chat/logic/handback.ts +2 -0
  86. package/src/chat/logic/limit.ts +8 -0
  87. package/src/chat/logic/picker-entries.ts +16 -4
  88. package/src/chat/logic/resume-compaction.ts +22 -5
  89. package/src/chat/logic/scrubber.ts +38 -5
  90. package/src/chat/logic/thought.ts +5 -0
  91. package/src/chat/logic/thread-cards.ts +6 -0
  92. package/src/chat/logic/timeline-copy.ts +4 -9
  93. package/src/chat/logic/timeline-target.ts +15 -0
  94. package/src/chat/logic/timeline.ts +113 -10
  95. package/src/chat/logic/tools.ts +30 -0
  96. package/src/chat/logic/visual-height.ts +5 -0
  97. package/src/chat/logic/visual-viewport.ts +1 -0
  98. package/src/chat/logic/welcome.ts +3 -0
  99. package/src/chat/logic/workflow.ts +26 -7
  100. package/src/chat/mentions.ts +56 -0
  101. package/src/chat/pending-prompts.ts +16 -0
  102. package/src/chat/persisted-json.ts +15 -0
  103. package/src/chat/preferences.ts +16 -0
  104. package/src/chat/prompt-hold.ts +2 -0
  105. package/src/chat/quote.ts +7 -0
  106. package/src/chat/reading-width.ts +1 -0
  107. package/src/chat/recent-messages.ts +5 -0
  108. package/src/chat/resume-compaction-dismissals.ts +1 -0
  109. package/src/chat/stash.ts +12 -0
  110. package/src/chat/subagent-conversation.ts +24 -4
  111. package/src/chat/subagent-list.ts +14 -0
  112. package/src/chat/subagent-support.ts +1 -0
  113. package/src/chat/subagent-view.ts +9 -0
  114. package/src/chat/thumbnails.ts +14 -0
  115. package/src/chat/timeline-flash.ts +2 -0
  116. package/src/chat/timeline-scroll.ts +23 -0
  117. package/src/chat/ui/AccountPill.tsx +4 -0
  118. package/src/chat/ui/AnsiOutput.tsx +1 -0
  119. package/src/chat/ui/BookmarkMarker.tsx +5 -0
  120. package/src/chat/ui/BookmarkSubmenu.tsx +1 -0
  121. package/src/chat/ui/ChatActivity.tsx +13 -0
  122. package/src/chat/ui/CodeBlock.tsx +8 -0
  123. package/src/chat/ui/Composer.tsx +15 -0
  124. package/src/chat/ui/ComposerInput.tsx +22 -0
  125. package/src/chat/ui/DiffPool.tsx +3 -0
  126. package/src/chat/ui/EditDiff.tsx +5 -0
  127. package/src/chat/ui/GoalDock.tsx +13 -1
  128. package/src/chat/ui/ImageView.tsx +11 -0
  129. package/src/chat/ui/LimitState.tsx +23 -6
  130. package/src/chat/ui/Markdown.tsx +41 -0
  131. package/src/chat/ui/MermaidBlock.tsx +16 -0
  132. package/src/chat/ui/MessageActions.tsx +8 -0
  133. package/src/chat/ui/ModelMarks.tsx +1 -0
  134. package/src/chat/ui/Pickers.tsx +26 -0
  135. package/src/chat/ui/PromptComposer.tsx +2 -0
  136. package/src/chat/ui/PromptEditor.tsx +6 -0
  137. package/src/chat/ui/QuoteButton.tsx +12 -0
  138. package/src/chat/ui/ReferenceChip.tsx +3 -0
  139. package/src/chat/ui/ResumeCompactionDock.tsx +1 -0
  140. package/src/chat/ui/RowTime.tsx +1 -0
  141. package/src/chat/ui/RunSettings.tsx +38 -0
  142. package/src/chat/ui/Scrubber.tsx +14 -0
  143. package/src/chat/ui/SessionLine.tsx +1 -0
  144. package/src/chat/ui/ShellCommand.tsx +32 -0
  145. package/src/chat/ui/SubagentControls.tsx +7 -0
  146. package/src/chat/ui/SubagentInfo.tsx +1 -0
  147. package/src/chat/ui/SubagentStopButton.tsx +6 -0
  148. package/src/chat/ui/SubagentTimeline.tsx +10 -0
  149. package/src/chat/ui/Timeline.tsx +119 -0
  150. package/src/chat/ui/TimelineMenu.tsx +5 -0
  151. package/src/chat/ui/UnifiedDiff.tsx +6 -0
  152. package/src/chat/ui/UploadThumb.tsx +2 -0
  153. package/src/chat/ui/VisualDialogs.tsx +3 -0
  154. package/src/chat/ui/VisualFrame.tsx +15 -0
  155. package/src/chat/ui/Welcome.tsx +1 -0
  156. package/src/chat/ui/chip-clipboard.ts +20 -0
  157. package/src/chat/ui/code-lines.ts +14 -0
  158. package/src/chat/ui/code-theme.ts +1 -0
  159. package/src/chat/ui/composer/ComposerControls.tsx +5 -0
  160. package/src/chat/ui/composer/ComposerMenu.tsx +12 -0
  161. package/src/chat/ui/composer/DraftParts.tsx +2 -0
  162. package/src/chat/ui/composer/QueuedMessages.tsx +4 -0
  163. package/src/chat/ui/composer/chip-paste.ts +16 -0
  164. package/src/chat/ui/composer/chips.ts +6 -0
  165. package/src/chat/ui/composer/editor.ts +9 -0
  166. package/src/chat/ui/composer/insert.ts +2 -0
  167. package/src/chat/ui/composer/key-handler.ts +39 -0
  168. package/src/chat/ui/composer/keys.ts +23 -0
  169. package/src/chat/ui/composer/placeholder.ts +6 -0
  170. package/src/chat/ui/composer/prompt-picks.ts +1 -0
  171. package/src/chat/ui/composer/surface.ts +5 -0
  172. package/src/chat/ui/composer/use-chat-controls.ts +13 -0
  173. package/src/chat/ui/composer/use-compaction-offer.ts +3 -0
  174. package/src/chat/ui/composer/use-composer-menus.ts +2 -0
  175. package/src/chat/ui/composer/use-composer-submit.ts +27 -0
  176. package/src/chat/ui/composer/use-continuable-turn.ts +3 -5
  177. package/src/chat/ui/composer/use-draft-files.ts +24 -0
  178. package/src/chat/ui/composer/use-effort-stepper.ts +8 -0
  179. package/src/chat/ui/composer/use-notice.ts +4 -1
  180. package/src/chat/ui/composer/use-pending-requests.ts +3 -0
  181. package/src/chat/ui/composer/use-prompt-history.ts +20 -11
  182. package/src/chat/ui/composer/use-prompt-hold.ts +3 -0
  183. package/src/chat/ui/composer/use-prompt-menus.ts +34 -14
  184. package/src/chat/ui/composer/use-take-back.ts +20 -2
  185. package/src/chat/ui/diff/ApprovalReview.tsx +18 -15
  186. package/src/chat/ui/diff/DiffFallbackBoundary.tsx +1 -0
  187. package/src/chat/ui/diff/DiffOmitted.tsx +1 -0
  188. package/src/chat/ui/diff/DiffStack.tsx +2 -0
  189. package/src/chat/ui/diff/DiffUnavailable.tsx +1 -0
  190. package/src/chat/ui/diff/EditorEditDiff.tsx +13 -0
  191. package/src/chat/ui/diff/EditorPatchDiff.tsx +4 -0
  192. package/src/chat/ui/diff/LegacyDiffPool.tsx +2 -0
  193. package/src/chat/ui/diff/LegacyEditDiff.tsx +2 -0
  194. package/src/chat/ui/diff/LegacyUnifiedDiff.tsx +8 -0
  195. package/src/chat/ui/diff/RawDiff.tsx +1 -0
  196. package/src/chat/ui/diff/ReviewDiff.tsx +1 -0
  197. package/src/chat/ui/diff/diff-source-budget.ts +8 -0
  198. package/src/chat/ui/diff/editor-edit-model.ts +4 -0
  199. package/src/chat/ui/diff/review-plan.ts +9 -0
  200. package/src/chat/ui/diff/review-settle.ts +1 -0
  201. package/src/chat/ui/diff/syntax-runtime.ts +52 -0
  202. package/src/chat/ui/diff/use-review-stack.ts +9 -0
  203. package/src/chat/ui/diff-syntax.ts +12 -0
  204. package/src/chat/ui/diff-theme.ts +2 -0
  205. package/src/chat/ui/file-links.ts +2 -0
  206. package/src/chat/ui/find-reveal.ts +1 -0
  207. package/src/chat/ui/full-diff.ts +1 -0
  208. package/src/chat/ui/highlighter.ts +29 -15
  209. package/src/chat/ui/icons.tsx +2 -4
  210. package/src/chat/ui/index.ts +4 -0
  211. package/src/chat/ui/intelligent-ui/UiBlockFrame.tsx +25 -0
  212. package/src/chat/ui/intelligent-ui/UiFallbackPart.tsx +1 -0
  213. package/src/chat/ui/intelligent-ui/UiReply.tsx +63 -0
  214. package/src/chat/ui/intelligent-ui/block-local.ts +5 -0
  215. package/src/chat/ui/intelligent-ui/chart-data.ts +15 -0
  216. package/src/chat/ui/intelligent-ui/node-text.ts +17 -0
  217. package/src/chat/ui/intelligent-ui/renderers/binding.ts +1 -0
  218. package/src/chat/ui/intelligent-ui/renderers/chart.tsx +31 -0
  219. package/src/chat/ui/intelligent-ui/renderers/choices.tsx +4 -0
  220. package/src/chat/ui/intelligent-ui/renderers/content.tsx +18 -0
  221. package/src/chat/ui/intelligent-ui/renderers/data.tsx +11 -0
  222. package/src/chat/ui/intelligent-ui/renderers/inputs.tsx +13 -0
  223. package/src/chat/ui/intelligent-ui/renderers/links.tsx +9 -0
  224. package/src/chat/ui/intelligent-ui/renderers/map-slots.ts +15 -0
  225. package/src/chat/ui/intelligent-ui/renderers/map-style.ts +15 -0
  226. package/src/chat/ui/intelligent-ui/renderers/map-view.tsx +48 -0
  227. package/src/chat/ui/intelligent-ui/renderers/map.tsx +20 -0
  228. package/src/chat/ui/intelligent-ui/renderers/place-point.ts +3 -0
  229. package/src/chat/ui/intelligent-ui/renderers/structure.tsx +3 -0
  230. package/src/chat/ui/intelligent-ui/renderers/text.tsx +8 -0
  231. package/src/chat/ui/intelligent-ui/reply-parts.ts +5 -0
  232. package/src/chat/ui/intelligent-ui/table-data.ts +17 -0
  233. package/src/chat/ui/intelligent-ui/use-in-view.ts +4 -0
  234. package/src/chat/ui/intelligent-ui/use-later.ts +4 -0
  235. package/src/chat/ui/intelligent-ui/use-truncated.ts +5 -0
  236. package/src/chat/ui/intelligent-ui/use-ui-links.ts +29 -0
  237. package/src/chat/ui/intelligent-ui/use-ui-queries.ts +60 -0
  238. package/src/chat/ui/markdown-blocks.ts +64 -0
  239. package/src/chat/ui/mermaid.ts +4 -0
  240. package/src/chat/ui/quote-selection.ts +10 -0
  241. package/src/chat/ui/rehype-chips.ts +5 -0
  242. package/src/chat/ui/rehype-fade.ts +2 -0
  243. package/src/chat/ui/remark-html-as-text.ts +5 -0
  244. package/src/chat/ui/reveal.ts +18 -0
  245. package/src/chat/ui/rows/ForksRow.tsx +5 -0
  246. package/src/chat/ui/rows/GeneratedImageRow.tsx +10 -0
  247. package/src/chat/ui/rows/MessageRows.tsx +29 -5
  248. package/src/chat/ui/rows/RowContainer.tsx +1 -0
  249. package/src/chat/ui/rows/Rows.tsx +2 -0
  250. package/src/chat/ui/rows/SubagentRow.tsx +13 -0
  251. package/src/chat/ui/rows/TaskRow.tsx +7 -0
  252. package/src/chat/ui/rows/VisualRow.tsx +4 -0
  253. package/src/chat/ui/rows/WorkRows.tsx +46 -0
  254. package/src/chat/ui/rows/WorkflowRow.tsx +10 -0
  255. package/src/chat/ui/rows/row-rhythm.ts +7 -5
  256. package/src/chat/ui/theme-watch.ts +8 -7
  257. package/src/chat/ui/thread-references.ts +7 -0
  258. package/src/chat/ui/use-diff-source-tokens.ts +8 -0
  259. package/src/chat/ui/useToggleSet.ts +3 -0
  260. package/src/chat/ui/visual-theme.ts +30 -0
  261. package/src/chat/visual-bridge.ts +23 -0
  262. package/src/chat/visual-viewport.ts +12 -0
  263. package/src/host-hooks.ts +6 -4
  264. package/src/host.ts +2 -0
  265. package/src/index.ts +3 -0
  266. package/src/lazy.tsx +9 -0
  267. package/src/mounted-registry.ts +4 -0
  268. package/src/port-transport.ts +32 -0
  269. package/src/prompts/index.ts +2 -0
  270. package/src/prompts/logic/focus.ts +5 -0
  271. package/src/prompts/logic/keys.ts +12 -0
  272. package/src/prompts/logic/prompts.ts +8 -0
  273. package/src/prompts/logic/subjects.ts +8 -0
  274. package/src/prompts/logic/useFocusAfterAnswer.ts +2 -0
  275. package/src/prompts/logic/usePromptSession.ts +13 -1
  276. package/src/prompts/ui/ApprovalBody.tsx +2 -0
  277. package/src/prompts/ui/PromptActions.tsx +2 -0
  278. package/src/prompts/ui/PromptCard.tsx +6 -0
  279. package/src/prompts/ui/PromptView.tsx +6 -0
  280. package/src/prompts/ui/QuestionBody.tsx +11 -0
  281. package/src/providers/AccountColors.tsx +1 -0
  282. package/src/providers/AccountDetail.tsx +7 -0
  283. package/src/providers/AccountVariables.tsx +2 -0
  284. package/src/providers/AddAccountForm.tsx +5 -1
  285. package/src/providers/CliDetail.tsx +6 -5
  286. package/src/providers/InstallPage.tsx +11 -0
  287. package/src/providers/ProviderList.tsx +3 -0
  288. package/src/providers/ProvidersPane.tsx +18 -0
  289. package/src/providers/account-actions.ts +10 -8
  290. package/src/providers/install-steps.ts +3 -0
  291. package/src/providers/pane-target.ts +5 -0
  292. package/src/providers/provider-abilities.ts +1 -0
  293. package/src/providers/provider-groups.ts +6 -0
  294. package/src/providers/variables.ts +12 -0
  295. package/src/scope.ts +2 -0
  296. package/src/state/chats.ts +61 -14
  297. package/src/state/index.ts +2 -0
  298. package/src/state/provider-accounts.ts +7 -9
  299. package/src/state/providers.ts +1 -0
  300. package/src/state/usage.ts +6 -0
  301. package/src/storage.ts +18 -0
  302. package/src/testing/prompts.ts +5 -0
  303. package/src/transport.ts +18 -0
  304. package/src/usage/ChartLogo.tsx +12 -0
  305. package/src/usage/LimitsList.tsx +7 -8
  306. package/src/usage/UsageBreakdown.tsx +11 -4
  307. package/src/usage/UsageChart.tsx +5 -0
  308. package/src/usage/UsageLimits.tsx +10 -7
  309. package/src/usage/UsageLimitsCard.tsx +2 -0
  310. package/src/usage/UsagePage.tsx +16 -19
  311. package/src/usage/UsageSummary.tsx +4 -5
  312. package/src/usage/UsageTiles.tsx +2 -0
  313. package/src/usage/format.ts +7 -0
  314. package/src/usage/limit-groups.ts +14 -0
  315. package/src/usage/limits.ts +11 -13
  316. package/src/usage/money.ts +1 -0
  317. package/src/usage/summary.ts +15 -7
@@ -29,10 +29,13 @@ export function isImageAttachment(mime: string): boolean {
29
29
  /* The type on a file card: the extension, as long as it is short enough to read as one. */
30
30
  export function fileBadge(name: string): string | null {
31
31
  const dot = name.lastIndexOf('.');
32
+
32
33
  if (dot <= 0 || dot === name.length - 1) {
33
34
  return null;
34
35
  }
36
+
35
37
  const extension = name.slice(dot + 1);
38
+
36
39
  return extension.length <= 5 ? extension.toUpperCase() : null;
37
40
  }
38
41
 
@@ -46,6 +49,7 @@ export function checkAttachmentLimits<T extends IncomingFile>(current: number, i
46
49
  const accepted: T[] = [];
47
50
  const rejected: Array<{ name: string; reason: string }> = [];
48
51
  let bytes = currentBytes;
52
+
49
53
  for (const file of incoming) {
50
54
  if (file.bytes === 0) {
51
55
  rejected.push({ name: file.name, reason: i18next.t('agent-chat:attachments.rejected.empty') });
@@ -63,6 +67,7 @@ export function checkAttachmentLimits<T extends IncomingFile>(current: number, i
63
67
  bytes += file.bytes;
64
68
  }
65
69
  }
70
+
66
71
  return { accepted, rejected };
67
72
  }
68
73
 
@@ -75,10 +80,12 @@ function readAsBase64(file: File): Promise<string> {
75
80
  return new Promise((resolve, reject) => {
76
81
  const reader = new FileReader();
77
82
  reader.onerror = () => reject(reader.error ?? new Error(i18next.t('agent-chat:attachments.unreadable', { name: file.name })));
83
+
78
84
  reader.onload = () => {
79
85
  const url = String(reader.result);
80
86
  resolve(url.slice(url.indexOf(',') + 1));
81
87
  };
88
+
82
89
  reader.readAsDataURL(file);
83
90
  });
84
91
  }
@@ -104,6 +111,7 @@ export async function readStoredAttachments(
104
111
  stored: readonly ChatAttachment[]
105
112
  ): Promise<ChatAttachmentUpload[]> {
106
113
  const files = await Promise.all(stored.map(async (attachment) => new File([await read(attachment)], attachment.name, { type: attachment.mime })));
114
+
107
115
  return readAttachments(files);
108
116
  }
109
117
 
@@ -3,6 +3,7 @@ import { create } from 'zustand';
3
3
  import type { ChatBookmark } from '@adecore/agent-contracts';
4
4
  import type { ChatHost } from '../host';
5
5
  import type { ChatScope } from '../scope';
6
+ import { errorMessage } from '../transport';
6
7
  import { jumpToTimelineItem } from './timeline-scroll';
7
8
 
8
9
  /* The one message in the window whose bookmark has its name field open, by the scope's key of its chat. */
@@ -23,15 +24,17 @@ export const useBookmarkNaming = create<BookmarkNaming>((set) => ({
23
24
  type Notifier = Pick<ChatHost, 'notify'>;
24
25
 
25
26
  function failed(host: Notifier, title: string, e: unknown): void {
26
- host.notify({ title, description: e instanceof Error ? e.message : String(e), kind: 'error' });
27
+ host.notify({ title, description: errorMessage(e), kind: 'error' });
27
28
  }
28
29
 
29
30
  async function add(scope: ChatScope, host: Notifier, chatId: string, itemId: string, name?: string): Promise<boolean> {
30
31
  try {
31
32
  await scope.chats.addBookmark(chatId, itemId, name);
33
+
32
34
  return true;
33
35
  } catch (e) {
34
36
  failed(host, i18next.t('agent-chat:bookmarks.addFailed'), e);
37
+
35
38
  return false;
36
39
  }
37
40
  }
@@ -39,6 +42,7 @@ async function add(scope: ChatScope, host: Notifier, chatId: string, itemId: str
39
42
  /* Marks a message and opens the field for its name at once, before the host answered. */
40
43
  export async function placeBookmark(scope: ChatScope, host: Notifier, chatId: string, itemId: string): Promise<void> {
41
44
  useBookmarkNaming.getState().open(scope.keyOf(chatId), itemId);
45
+
42
46
  if (!(await add(scope, host, chatId, itemId))) {
43
47
  useBookmarkNaming.getState().close();
44
48
  }
@@ -57,13 +61,17 @@ export async function nameBookmark(
57
61
  previous: string | undefined
58
62
  ): Promise<void> {
59
63
  const next = name.trim();
64
+
60
65
  if (next === (previous ?? '')) {
61
66
  return;
62
67
  }
68
+
63
69
  if (next !== '') {
64
70
  await add(scope, host, chatId, itemId, next);
71
+
65
72
  return;
66
73
  }
74
+
67
75
  try {
68
76
  await scope.chats.renameBookmark(chatId, itemId, '');
69
77
  } catch (e) {
@@ -77,8 +85,10 @@ export async function removeBookmark(scope: ChatScope, host: Notifier, chatId: s
77
85
  await scope.chats.removeBookmark(chatId, bookmark.itemId);
78
86
  } catch (e) {
79
87
  failed(host, i18next.t('agent-chat:bookmarks.removeFailed'), e);
88
+
80
89
  return;
81
90
  }
91
+
82
92
  host.notify({
83
93
  title: i18next.t('agent-chat:bookmarks.removed'),
84
94
  kind: 'deleted',
@@ -89,8 +99,10 @@ export async function removeBookmark(scope: ChatScope, host: Notifier, chatId: s
89
99
  /* Scrolls the chat's thread to the message, and opens its name field for a rename. False when no thread of it is on screen yet. */
90
100
  export function goToBookmark(scope: ChatScope, chatId: string, itemId: string, rename = false): boolean {
91
101
  const key = scope.keyOf(chatId);
102
+
92
103
  if (rename) {
93
104
  useBookmarkNaming.getState().open(key, itemId);
94
105
  }
106
+
95
107
  return jumpToTimelineItem(key, itemId);
96
108
  }
@@ -78,10 +78,12 @@ export class ChatClient {
78
78
  this.transport = transport;
79
79
  this.sink = sink;
80
80
  this.providers = providers;
81
+
81
82
  this.unsubscribe.push(
82
83
  transport.on('chat.event', ({ chatId, event, seq }) => {
83
84
  const previous = this.sequence(chatId);
84
85
  this.sink.apply(chatId, event);
86
+
85
87
  if (previous !== undefined || (!this.sink.historyIdentity && this.mounted.get(chatId)?.attached)) {
86
88
  this.rememberSequence(chatId, seq);
87
89
  }
@@ -92,6 +94,7 @@ export class ChatClient {
92
94
  transport.on('chat.visuals', ({ chatId, visuals }) => this.sink.visuals(chatId, visuals)),
93
95
  transport.subscribeStatus((status) => this.onStatus(status))
94
96
  );
97
+
95
98
  if (transport.status === 'open') {
96
99
  void this.loadProviders();
97
100
  void this.loadStatuses();
@@ -100,39 +103,52 @@ export class ChatClient {
100
103
 
101
104
  inspect(chatId: string): Promise<ChatAttachResult> {
102
105
  const pending = this.inspections.get(chatId);
106
+
103
107
  if (pending) {
104
108
  return pending;
105
109
  }
110
+
106
111
  const read = this.transport.request('chat.attach', { chatId, historyLimit: 100 }).finally(async () => {
107
112
  this.inspections.delete(chatId);
113
+
108
114
  // A visible chat owns its attachment; an inspection must never release it.
109
115
  if (!this.mounted.get(chatId)) {
110
116
  await this.transport.request('chat.detach', { chatId }).catch(() => undefined);
111
117
  }
112
118
  });
119
+
113
120
  this.inspections.set(chatId, read);
121
+
114
122
  return read;
115
123
  }
116
124
 
117
125
  /* Shares a live attachment without creating a chat; a visible composer and several ancestors may hold it together. */
118
126
  observe(chatId: string): () => void {
119
127
  this.observers.set(chatId, (this.observers.get(chatId) ?? 0) + 1);
128
+
120
129
  if (!this.mounted.has(chatId)) {
121
130
  this.mounted.set(chatId, { attached: false, observerOnly: true });
122
131
  void this.attach(chatId).catch(() => undefined);
123
132
  }
133
+
124
134
  let released = false;
135
+
125
136
  return () => {
126
137
  if (released) {
127
138
  return;
128
139
  }
140
+
129
141
  released = true;
130
142
  const remaining = (this.observers.get(chatId) ?? 1) - 1;
143
+
131
144
  if (remaining > 0) {
132
145
  this.observers.set(chatId, remaining);
146
+
133
147
  return;
134
148
  }
149
+
135
150
  this.observers.delete(chatId);
151
+
136
152
  if (this.mounted.get(chatId)?.observerOnly) {
137
153
  void this.detach(chatId);
138
154
  }
@@ -142,13 +158,16 @@ export class ChatClient {
142
158
  /* Answers false when the transport is not connected; the chat opens once it is. */
143
159
  async open(chatId: string, options: ChatOpenOptions): Promise<boolean> {
144
160
  this.mounted.set(chatId, { ...options, attached: false });
161
+
145
162
  try {
146
163
  await this.attach(chatId);
164
+
147
165
  return true;
148
166
  } catch (e) {
149
167
  if (isConnectionError(e)) {
150
168
  return false;
151
169
  }
170
+
152
171
  throw e;
153
172
  }
154
173
  }
@@ -160,19 +179,23 @@ export class ChatClient {
160
179
  */
161
180
  async retarget(chatId: string, provider: AgentKind, selection: ModelSelection): Promise<void> {
162
181
  const entry = this.mounted.get(chatId);
182
+
163
183
  if (!entry) {
164
184
  return;
165
185
  }
186
+
166
187
  // The account belonged to the CLI the chat leaves, and the seq to its stream: the new chat's
167
188
  // stream starts over, so offering it the old seq would read as nothing missed and reset nothing.
168
189
  const { attached: _attached, account: _account, seq: _seq, observerOnly: _observerOnly, ...options } = entry;
169
190
  this.rememberSequence(chatId, undefined);
170
191
  this.mounted.delete(chatId);
171
192
  await this.transport.request('chat.kill', { chatId });
193
+
172
194
  try {
173
195
  await this.open(chatId, { ...options, provider, selection });
174
196
  } catch (e) {
175
197
  this.sink.forget(chatId);
198
+
176
199
  throw e;
177
200
  }
178
201
  }
@@ -188,14 +211,19 @@ export class ChatClient {
188
211
 
189
212
  async detach(chatId: string): Promise<void> {
190
213
  const entry = this.mounted.get(chatId);
214
+
191
215
  if (entry && this.observers.has(chatId)) {
192
216
  entry.observerOnly = true;
217
+
193
218
  return;
194
219
  }
220
+
195
221
  this.mounted.delete(chatId);
222
+
196
223
  if (!entry?.attached) {
197
224
  return;
198
225
  }
226
+
199
227
  try {
200
228
  await this.transport.request('chat.detach', { chatId });
201
229
  } catch {
@@ -221,29 +249,34 @@ export class ChatClient {
221
249
  /* Marks a message; the list every client of the chat holds comes back as `chat.bookmarks`. */
222
250
  async addBookmark(chatId: string, itemId: string, name?: string): Promise<ChatBookmark[]> {
223
251
  const { bookmarks } = await this.transport.request('chat.addBookmark', { chatId, itemId, ...(name === undefined ? {} : { name }) });
252
+
224
253
  return bookmarks;
225
254
  }
226
255
 
227
256
  /* An empty name takes the name away. */
228
257
  async renameBookmark(chatId: string, itemId: string, name: string): Promise<ChatBookmark[]> {
229
258
  const { bookmarks } = await this.transport.request('chat.renameBookmark', { chatId, itemId, name });
259
+
230
260
  return bookmarks;
231
261
  }
232
262
 
233
263
  async removeBookmark(chatId: string, itemId: string): Promise<ChatBookmark[]> {
234
264
  const { bookmarks } = await this.transport.request('chat.removeBookmark', { chatId, itemId });
265
+
235
266
  return bookmarks;
236
267
  }
237
268
 
238
269
  /* Takes a visual and its page out of the chat for good; the list every client of the chat holds comes back as `chat.visuals`. */
239
270
  async removeVisual(chatId: string, visualId: string): Promise<ChatVisual[]> {
240
271
  const { visuals } = await this.transport.request('chat.removeVisual', { chatId, visualId });
272
+
241
273
  return visuals;
242
274
  }
243
275
 
244
276
  /* What this chat's CLI would run as a skill, for the composer's `$` picker. */
245
277
  async listSkills(chatId: string): Promise<ChatSkill[]> {
246
278
  const { skills } = await this.transport.request('skills.list', { chatId });
279
+
247
280
  return skills;
248
281
  }
249
282
 
@@ -273,6 +306,7 @@ export class ChatClient {
273
306
  async loadStatuses(): Promise<void> {
274
307
  try {
275
308
  const { chats } = await this.transport.request('chat.list', {});
309
+
276
310
  for (const info of chats) {
277
311
  this.sink.status(info.chatId, info);
278
312
  }
@@ -285,6 +319,7 @@ export class ChatClient {
285
319
  if (!this.providers) {
286
320
  return;
287
321
  }
322
+
288
323
  try {
289
324
  const { providers } = await this.transport.request('provider.list', {});
290
325
  this.providers.setProviders(providers);
@@ -299,9 +334,11 @@ export class ChatClient {
299
334
  */
300
335
  loadEarlier(chatId: string, cursor: string): Promise<void> {
301
336
  const running = this.earlier.get(chatId);
337
+
302
338
  if (running) {
303
339
  return running;
304
340
  }
341
+
305
342
  const read = this.transport
306
343
  .request('chat.history', { chatId, cursor, limit: HISTORY_PAGE })
307
344
  .then((page) => {
@@ -311,14 +348,18 @@ export class ChatClient {
311
348
  })
312
349
  .catch(async (e: unknown) => {
313
350
  const entry = this.mounted.get(chatId);
351
+
314
352
  if (errorCode(e) !== 'history-expired' || !entry) {
315
353
  throw e;
316
354
  }
355
+
317
356
  this.rememberSequence(chatId, undefined);
318
357
  await this.attach(chatId);
319
358
  })
320
359
  .finally(() => this.earlier.delete(chatId));
360
+
321
361
  this.earlier.set(chatId, read);
362
+
322
363
  return read;
323
364
  }
324
365
 
@@ -331,9 +372,11 @@ export class ChatClient {
331
372
  for (const off of this.unsubscribe) {
332
373
  off();
333
374
  }
375
+
334
376
  this.unsubscribe.length = 0;
335
377
  this.observers.clear();
336
378
  this.checkpoints = new WeakMap();
379
+
337
380
  for (const chatId of [...this.mounted.keys()]) {
338
381
  void this.detach(chatId);
339
382
  }
@@ -346,9 +389,11 @@ export class ChatClient {
346
389
  */
347
390
  private async attach(chatId: string): Promise<void> {
348
391
  const entry = this.mounted.get(chatId);
392
+
349
393
  if (!entry) {
350
394
  return;
351
395
  }
396
+
352
397
  if (!entry.observerOnly) {
353
398
  await this.transport.request('chat.create', {
354
399
  chatId,
@@ -360,26 +405,34 @@ export class ChatClient {
360
405
  runtimeMode: entry.runtimeMode
361
406
  });
362
407
  }
408
+
363
409
  // Closed or opened again in the meantime: an opening that is still there attaches itself.
364
410
  if (this.mounted.get(chatId) !== entry) {
365
411
  return;
366
412
  }
413
+
367
414
  const since = this.sequence(chatId);
368
415
  const result = await this.transport.request('chat.attach', { chatId, historyLimit: HISTORY_PAGE, ...(since === undefined ? {} : { since }) });
369
416
  const current = this.mounted.get(chatId);
417
+
370
418
  if (current !== entry) {
371
419
  // Its detach found nothing attached yet, and the host would stream this chat here until the socket closes.
372
420
  if (!current) {
373
421
  await this.transport.request('chat.detach', { chatId }).catch(() => undefined);
374
422
  }
423
+
375
424
  return;
376
425
  }
426
+
377
427
  // A cache eviction while the request was in flight also invalidates its replay base.
378
428
  if (result.events && since !== undefined && this.sequence(chatId) === undefined) {
379
429
  await this.attach(chatId);
430
+
380
431
  return;
381
432
  }
433
+
382
434
  entry.attached = true;
435
+
383
436
  if (result.events && since !== undefined) {
384
437
  if (this.sink.replay) {
385
438
  this.sink.replay(chatId, result.events);
@@ -391,8 +444,10 @@ export class ChatClient {
391
444
  } else {
392
445
  const page =
393
446
  result.history === undefined ? undefined : { history: result.history, pending: result.pending ?? [], subagents: result.subagents ?? [] };
447
+
394
448
  this.sink.reset(chatId, result.info, result.items, page);
395
449
  }
450
+
396
451
  this.rememberSequence(chatId, result.seq);
397
452
  // A host without bookmarks or visuals sends none, and then this chat has none.
398
453
  this.sink.bookmarks(chatId, result.bookmarks ?? []);
@@ -403,16 +458,21 @@ export class ChatClient {
403
458
  if (!this.sink.historyIdentity) {
404
459
  return this.mounted.get(chatId)?.seq;
405
460
  }
461
+
406
462
  const identity = this.sink.historyIdentity(chatId);
463
+
407
464
  return identity === undefined ? undefined : this.checkpoints.get(identity);
408
465
  }
409
466
 
410
467
  private rememberSequence(chatId: string, seq: number | undefined): void {
411
468
  const entry = this.mounted.get(chatId);
469
+
412
470
  if (entry) {
413
471
  entry.seq = seq;
414
472
  }
473
+
415
474
  const identity = this.sink.historyIdentity?.(chatId);
475
+
416
476
  if (identity !== undefined) {
417
477
  if (seq === undefined) {
418
478
  this.checkpoints.delete(identity);
@@ -426,6 +486,7 @@ export class ChatClient {
426
486
  if (this.preferences === null) {
427
487
  return;
428
488
  }
489
+
429
490
  void this.transport.request('chat.setPreferences', this.preferences).catch(() => undefined);
430
491
  }
431
492
 
@@ -435,8 +496,10 @@ export class ChatClient {
435
496
  void this.loadProviders();
436
497
  void this.loadStatuses();
437
498
  void this.mounted.reattachAll((chatId) => this.attach(chatId));
499
+
438
500
  return;
439
501
  }
502
+
440
503
  this.mounted.detachAll();
441
504
  }
442
505
  }
@@ -38,6 +38,7 @@ export function referablesOfChats(chats: readonly ProjectChat[]): Referables {
38
38
  export function referenceTokens(ids: readonly string[], items: readonly Referable[], spelled: Readonly<Record<string, string>> = {}): ReferenceToken[] {
39
39
  return [...new Set(ids)].flatMap((id) => {
40
40
  const name = spelled[id] ?? items.find((item) => item.id === id)?.name;
41
+
41
42
  return name === undefined || name === '' ? [] : [{ id, name }];
42
43
  });
43
44
  }
@@ -45,8 +46,10 @@ export function referenceTokens(ids: readonly string[], items: readonly Referabl
45
46
  /* What a `#query` offers, the canvas first: never an id in `exclude`, and only chats where the machine takes no more. */
46
47
  export function referenceSuggestions(referables: Referables, query: string, exclude: readonly string[], limit: number): Referable[] {
47
48
  const needle = query.toLowerCase();
49
+
48
50
  const matches = referables.items.filter(
49
51
  (item) => item.name !== '' && (!referables.chatsOnly || item.kind === 'chat') && !exclude.includes(item.id) && item.name.toLowerCase().includes(needle)
50
52
  );
53
+
51
54
  return [...matches.filter((item) => item.group === 'canvas'), ...matches.filter((item) => item.group === 'views')].slice(0, limit);
52
55
  }
@@ -11,8 +11,10 @@ const steppers = new Map<string, Stepper[]>();
11
11
 
12
12
  export function registerEffortStepper(key: string, stepper: Stepper): () => void {
13
13
  steppers.set(key, [...(steppers.get(key) ?? []), stepper]);
14
+
14
15
  return () => {
15
16
  const left = (steppers.get(key) ?? []).filter((entry) => entry !== stepper);
17
+
16
18
  if (left.length > 0) {
17
19
  steppers.set(key, left);
18
20
  } else {
@@ -24,9 +26,12 @@ export function registerEffortStepper(key: string, stepper: Stepper): () => void
24
26
  /* False when that chat has no composer on screen. At either end of the model's efforts the composer says so and nothing changes. */
25
27
  export function stepEffortFromComposer(key: string, step: EffortStep): boolean {
26
28
  const stepper = steppers.get(key)?.at(-1);
29
+
27
30
  if (stepper === undefined) {
28
31
  return false;
29
32
  }
33
+
30
34
  stepper(step);
35
+
31
36
  return true;
32
37
  }
@@ -9,6 +9,7 @@ const senders = new Map<string, (how: ComposerSend) => void>();
9
9
 
10
10
  export function registerComposerSender(key: string, send: (how: ComposerSend) => void): () => void {
11
11
  senders.set(key, send);
12
+
12
13
  return () => {
13
14
  if (senders.get(key) === send) {
14
15
  senders.delete(key);
@@ -19,9 +20,12 @@ export function registerComposerSender(key: string, send: (how: ComposerSend) =>
19
20
  /* False when that chat has no composer on screen. An empty or unsendable draft sends nothing, as with the keys. */
20
21
  export function sendFromComposer(key: string, how: ComposerSend = 'default'): boolean {
21
22
  const send = senders.get(key);
23
+
22
24
  if (send === undefined) {
23
25
  return false;
24
26
  }
27
+
25
28
  send(how);
29
+
26
30
  return true;
27
31
  }
@@ -57,21 +57,20 @@ export const useDrafts = createChatStore<{ ids: string[] }>(
57
57
  (store) => store.setState({ ids: Object.keys(storage.read()) })
58
58
  );
59
59
 
60
- export function useHasDraft(chatId: string): boolean {
61
- return useDrafts((s) => s.ids.includes(chatId));
62
- }
63
-
64
60
  function trackDraft(chatId: string, held: boolean): void {
65
61
  const { ids } = useDrafts.getState();
62
+
66
63
  if (ids.includes(chatId) === held) {
67
64
  return;
68
65
  }
66
+
69
67
  useDrafts.setState({ ids: held ? [...ids, chatId] : ids.filter((id) => id !== chatId) });
70
68
  }
71
69
 
72
70
  /* An unsent prompt per chat node, kept across reloads so a half-written message is never lost. */
73
71
  export function readDraft(chatId: string): ChatDraft {
74
72
  const record = storage.read()[chatId];
73
+
75
74
  return record
76
75
  ? {
77
76
  text: record.text,
@@ -90,37 +89,37 @@ export function readDraft(chatId: string): ChatDraft {
90
89
  export function writeDraft(chatId: string, draft: ChatDraft): void {
91
90
  const drafts = storage.read();
92
91
  trackDraft(chatId, !isEmptyDraft(draft));
92
+
93
93
  if (isEmptyDraft(draft)) {
94
94
  delete drafts[chatId];
95
95
  storage.write(drafts);
96
+
96
97
  return;
97
98
  }
98
- drafts[chatId] = {
99
- text: draft.text,
100
- mentions: draft.mentions,
101
- skills: draft.skills,
102
- chats: draft.chats,
103
- attachments: draft.attachments,
104
- quote: draft.quote,
105
- ...(draft.quoteSource === null ? {} : { quoteSource: draft.quoteSource }),
106
- references: draft.references,
107
- ...namesOf(draft)
108
- };
99
+
100
+ drafts[chatId] = recordOf(draft, true);
101
+
109
102
  if (storage.write(drafts)) {
110
103
  return;
111
104
  }
105
+
112
106
  // A file can outgrow the storage quota; the text is the part worth keeping then.
113
- drafts[chatId] = {
107
+ drafts[chatId] = recordOf(draft, false);
108
+ storage.write(drafts);
109
+ }
110
+
111
+ function recordOf(draft: ChatDraft, withFiles: boolean): DraftRecord {
112
+ return {
114
113
  text: draft.text,
115
114
  mentions: draft.mentions,
116
115
  skills: draft.skills,
117
116
  chats: draft.chats,
117
+ ...(withFiles ? { attachments: draft.attachments } : {}),
118
118
  quote: draft.quote,
119
119
  ...(draft.quoteSource === null ? {} : { quoteSource: draft.quoteSource }),
120
120
  references: draft.references,
121
121
  ...namesOf(draft)
122
122
  };
123
- storage.write(drafts);
124
123
  }
125
124
 
126
125
  function namesOf(draft: ChatDraft): Pick<DraftRecord, 'referenceNames'> {
@@ -150,6 +149,7 @@ export function takeBackIntoDraft(
150
149
  taken.attachments.map((upload) => ({ name: upload.name, mime: upload.mime, bytes: uploadBytes(upload), upload })),
151
150
  current.attachments.reduce((bytes, upload) => bytes + uploadBytes(upload), 0)
152
151
  );
152
+
153
153
  return {
154
154
  draft: {
155
155
  text: current.text.trim() === '' ? taken.text : joinDraftText(taken.text, current.text),
@@ -173,6 +173,7 @@ const takers = new Map<string, DraftTaker>();
173
173
 
174
174
  export function takeDraftOffers(chatId: string, take: DraftTaker): () => void {
175
175
  takers.set(chatId, take);
176
+
176
177
  return () => {
177
178
  if (takers.get(chatId) === take) {
178
179
  takers.delete(chatId);
@@ -186,10 +187,13 @@ export function takeDraftOffers(chatId: string, take: DraftTaker): () => void {
186
187
  */
187
188
  export function offerDraft(chatId: string, text: string): void {
188
189
  const take = takers.get(chatId);
190
+
189
191
  if (take) {
190
192
  take(text);
193
+
191
194
  return;
192
195
  }
196
+
193
197
  const current = readDraft(chatId);
194
198
  writeDraft(chatId, { ...current, text: joinDraftText(current.text, text) });
195
199
  }
package/src/chat/forks.ts CHANGED
@@ -23,11 +23,13 @@ function joinedForks(
23
23
  ): (state: { statusByKey: ChatStatuses }) => string {
24
24
  let statuses: ChatStatuses | null = null;
25
25
  let joined = '';
26
+
26
27
  return (state) => {
27
28
  if (state.statusByKey !== statuses) {
28
29
  statuses = state.statusByKey;
29
30
  joined = [...collect(forkInfosOn(statuses, owns))].join('\n');
30
31
  }
32
+
31
33
  return joined;
32
34
  };
33
35
  }
@@ -41,6 +43,7 @@ export function useForkedTurns(chatId: string): ReadonlySet<string> {
41
43
  const { owns } = useChatScope();
42
44
  const select = joinedForks(owns, (infos) => forkedTurnIds(infos, chatId));
43
45
  const joined = useChats(select);
46
+
44
47
  return new Set(splitIds(joined));
45
48
  }
46
49
 
@@ -49,5 +52,6 @@ export function useForkIdsAfter(chatId: string, turnId: string): string[] {
49
52
  const { owns } = useChatScope();
50
53
  const select = joinedForks(owns, (infos) => forkIdsAfter(infos, chatId, turnId));
51
54
  const joined = useChats(select);
55
+
52
56
  return splitIds(joined);
53
57
  }
@@ -23,9 +23,11 @@ export function pasteBecomesAttachment(text: string): boolean {
23
23
  export function pastedTextName(taken: string[]): string {
24
24
  const names = new Set(taken);
25
25
  let index = 1;
26
+
26
27
  while (names.has(`paste-${index}.txt`)) {
27
28
  index += 1;
28
29
  }
30
+
29
31
  return `paste-${index}.txt`;
30
32
  }
31
33