@aphexcms/cms-core 9.7.0 → 9.8.1

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 (330) hide show
  1. package/dist/agent-chat-state.svelte.d.ts +38 -0
  2. package/dist/agent-chat-state.svelte.d.ts.map +1 -0
  3. package/dist/agent-chat-state.svelte.js +86 -0
  4. package/dist/ai/content-workspace-tools.d.ts +3 -0
  5. package/dist/ai/content-workspace-tools.d.ts.map +1 -0
  6. package/dist/ai/content-workspace-tools.js +50 -0
  7. package/dist/ai/default-system-prompt.d.ts +2 -0
  8. package/dist/ai/default-system-prompt.d.ts.map +1 -0
  9. package/dist/ai/default-system-prompt.js +15 -0
  10. package/dist/ai/index.d.ts +2 -0
  11. package/dist/ai/index.d.ts.map +1 -0
  12. package/dist/ai/index.js +4 -0
  13. package/dist/ai/interfaces/ai-provider.d.ts +67 -0
  14. package/dist/ai/interfaces/ai-provider.d.ts.map +1 -0
  15. package/dist/ai/interfaces/ai-provider.js +16 -0
  16. package/dist/ai/run-agent-turn.d.ts +29 -0
  17. package/dist/ai/run-agent-turn.d.ts.map +1 -0
  18. package/dist/ai/run-agent-turn.js +166 -0
  19. package/dist/api/agent-chat.d.ts +27 -0
  20. package/dist/api/agent-chat.d.ts.map +1 -0
  21. package/dist/api/agent-chat.js +64 -0
  22. package/dist/api/documents.d.ts +8 -3
  23. package/dist/api/documents.d.ts.map +1 -1
  24. package/dist/api/documents.js +6 -6
  25. package/dist/api/schemas/agent-chat.d.ts +45 -0
  26. package/dist/api/schemas/agent-chat.d.ts.map +1 -0
  27. package/dist/api/schemas/agent-chat.js +35 -0
  28. package/dist/api/schemas/agent-operations.d.ts +13 -0
  29. package/dist/api/schemas/agent-operations.d.ts.map +1 -0
  30. package/dist/api/schemas/agent-operations.js +15 -0
  31. package/dist/api/schemas/documents.d.ts +22 -0
  32. package/dist/api/schemas/documents.d.ts.map +1 -1
  33. package/dist/api/schemas/documents.js +20 -2
  34. package/dist/chunk-recovery.d.ts +33 -0
  35. package/dist/chunk-recovery.d.ts.map +1 -0
  36. package/dist/chunk-recovery.js +131 -0
  37. package/dist/client/index.d.ts +1 -0
  38. package/dist/client/index.d.ts.map +1 -1
  39. package/dist/client/index.js +1 -0
  40. package/dist/client/ui.d.ts +1 -0
  41. package/dist/client/ui.d.ts.map +1 -1
  42. package/dist/client/ui.js +1 -0
  43. package/dist/components/AdminApp.svelte +37 -3
  44. package/dist/components/AdminApp.svelte.d.ts.map +1 -1
  45. package/dist/components/admin/ActivityView.svelte +251 -42
  46. package/dist/components/admin/ActivityView.svelte.d.ts.map +1 -1
  47. package/dist/components/admin/AgentChat.svelte +977 -0
  48. package/dist/components/admin/AgentChat.svelte.d.ts +17 -0
  49. package/dist/components/admin/AgentChat.svelte.d.ts.map +1 -0
  50. package/dist/components/admin/DocumentEditor.svelte +273 -60
  51. package/dist/components/admin/DocumentEditor.svelte.d.ts +6 -0
  52. package/dist/components/admin/DocumentEditor.svelte.d.ts.map +1 -1
  53. package/dist/components/admin/ObjectModal.svelte +4 -2
  54. package/dist/components/admin/fields/ArrayField.svelte +4 -1
  55. package/dist/components/admin/fields/ReferenceField.svelte +12 -1
  56. package/dist/components/admin/fields/ReferenceField.svelte.d.ts.map +1 -1
  57. package/dist/components/index.d.ts +1 -0
  58. package/dist/components/index.d.ts.map +1 -1
  59. package/dist/components/index.js +1 -0
  60. package/dist/components/layout/Sidebar.svelte +26 -2
  61. package/dist/components/layout/Sidebar.svelte.d.ts +1 -0
  62. package/dist/components/layout/Sidebar.svelte.d.ts.map +1 -1
  63. package/dist/db/interfaces/agent-change-sets.d.ts +17 -0
  64. package/dist/db/interfaces/agent-change-sets.d.ts.map +1 -0
  65. package/dist/db/interfaces/agent-change-sets.js +1 -0
  66. package/dist/db/interfaces/document.d.ts +25 -3
  67. package/dist/db/interfaces/document.d.ts.map +1 -1
  68. package/dist/db/interfaces/document.js +19 -1
  69. package/dist/db/interfaces/index.d.ts +4 -1
  70. package/dist/db/interfaces/index.d.ts.map +1 -1
  71. package/dist/db/interfaces/index.js +1 -1
  72. package/dist/document-refresh.svelte.d.ts +2 -0
  73. package/dist/document-refresh.svelte.d.ts.map +1 -1
  74. package/dist/document-refresh.svelte.js +15 -0
  75. package/dist/document-workspace-registry.svelte.d.ts +15 -0
  76. package/dist/document-workspace-registry.svelte.d.ts.map +1 -0
  77. package/dist/document-workspace-registry.svelte.js +30 -0
  78. package/dist/field-validation/utils.d.ts.map +1 -1
  79. package/dist/field-validation/utils.js +211 -29
  80. package/dist/hooks.d.ts +2 -0
  81. package/dist/hooks.d.ts.map +1 -1
  82. package/dist/hooks.js +2 -0
  83. package/dist/lib/agent-chat-state.svelte.d.ts +38 -0
  84. package/dist/lib/agent-chat-state.svelte.d.ts.map +1 -0
  85. package/dist/lib/agent-chat-state.svelte.js +87 -0
  86. package/dist/lib/agent-chat-state.svelte.js.map +1 -0
  87. package/dist/lib/ai/content-workspace-tools.d.ts +3 -0
  88. package/dist/lib/ai/content-workspace-tools.d.ts.map +1 -0
  89. package/dist/lib/ai/content-workspace-tools.js +51 -0
  90. package/dist/lib/ai/content-workspace-tools.js.map +1 -0
  91. package/dist/lib/ai/default-system-prompt.d.ts +2 -0
  92. package/dist/lib/ai/default-system-prompt.d.ts.map +1 -0
  93. package/dist/lib/ai/default-system-prompt.js +16 -0
  94. package/dist/lib/ai/default-system-prompt.js.map +1 -0
  95. package/dist/lib/ai/index.d.ts +2 -0
  96. package/dist/lib/ai/index.d.ts.map +1 -0
  97. package/dist/lib/ai/index.js +5 -0
  98. package/dist/lib/ai/index.js.map +1 -0
  99. package/dist/lib/ai/interfaces/ai-provider.d.ts +67 -0
  100. package/dist/lib/ai/interfaces/ai-provider.d.ts.map +1 -0
  101. package/dist/lib/ai/interfaces/ai-provider.js +17 -0
  102. package/dist/lib/ai/interfaces/ai-provider.js.map +1 -0
  103. package/dist/lib/ai/run-agent-turn.d.ts +29 -0
  104. package/dist/lib/ai/run-agent-turn.d.ts.map +1 -0
  105. package/dist/lib/ai/run-agent-turn.js +167 -0
  106. package/dist/lib/ai/run-agent-turn.js.map +1 -0
  107. package/dist/lib/api/agent-chat.d.ts +27 -0
  108. package/dist/lib/api/agent-chat.d.ts.map +1 -0
  109. package/dist/lib/api/agent-chat.js +65 -0
  110. package/dist/lib/api/agent-chat.js.map +1 -0
  111. package/dist/lib/api/documents.d.ts +8 -3
  112. package/dist/lib/api/documents.d.ts.map +1 -1
  113. package/dist/lib/api/documents.js +6 -6
  114. package/dist/lib/api/documents.js.map +1 -1
  115. package/dist/lib/api/schemas/agent-chat.d.ts +45 -0
  116. package/dist/lib/api/schemas/agent-chat.d.ts.map +1 -0
  117. package/dist/lib/api/schemas/agent-chat.js +36 -0
  118. package/dist/lib/api/schemas/agent-chat.js.map +1 -0
  119. package/dist/lib/api/schemas/agent-operations.d.ts +13 -0
  120. package/dist/lib/api/schemas/agent-operations.d.ts.map +1 -0
  121. package/dist/lib/api/schemas/agent-operations.js +16 -0
  122. package/dist/lib/api/schemas/agent-operations.js.map +1 -0
  123. package/dist/lib/api/schemas/documents.d.ts +22 -0
  124. package/dist/lib/api/schemas/documents.d.ts.map +1 -1
  125. package/dist/lib/api/schemas/documents.js +20 -2
  126. package/dist/lib/api/schemas/documents.js.map +1 -1
  127. package/dist/lib/chunk-recovery.d.ts +33 -0
  128. package/dist/lib/chunk-recovery.d.ts.map +1 -0
  129. package/dist/lib/chunk-recovery.js +132 -0
  130. package/dist/lib/chunk-recovery.js.map +1 -0
  131. package/dist/lib/client/index.d.ts +1 -0
  132. package/dist/lib/client/index.d.ts.map +1 -1
  133. package/dist/lib/client/index.js +1 -0
  134. package/dist/lib/client/index.js.map +1 -1
  135. package/dist/lib/client/ui.d.ts +1 -0
  136. package/dist/lib/client/ui.d.ts.map +1 -1
  137. package/dist/lib/client/ui.js +1 -0
  138. package/dist/lib/client/ui.js.map +1 -1
  139. package/dist/lib/components/index.d.ts +1 -0
  140. package/dist/lib/components/index.d.ts.map +1 -1
  141. package/dist/lib/components/index.js +1 -0
  142. package/dist/lib/components/index.js.map +1 -1
  143. package/dist/lib/db/interfaces/agent-change-sets.d.ts +17 -0
  144. package/dist/lib/db/interfaces/agent-change-sets.d.ts.map +1 -0
  145. package/dist/lib/db/interfaces/agent-change-sets.js +2 -0
  146. package/dist/lib/db/interfaces/agent-change-sets.js.map +1 -0
  147. package/dist/lib/db/interfaces/document.d.ts +25 -3
  148. package/dist/lib/db/interfaces/document.d.ts.map +1 -1
  149. package/dist/lib/db/interfaces/document.js +19 -1
  150. package/dist/lib/db/interfaces/document.js.map +1 -1
  151. package/dist/lib/db/interfaces/index.d.ts +4 -1
  152. package/dist/lib/db/interfaces/index.d.ts.map +1 -1
  153. package/dist/lib/db/interfaces/index.js +1 -1
  154. package/dist/lib/db/interfaces/index.js.map +1 -1
  155. package/dist/lib/document-refresh.svelte.d.ts +2 -0
  156. package/dist/lib/document-refresh.svelte.d.ts.map +1 -1
  157. package/dist/lib/document-refresh.svelte.js +15 -0
  158. package/dist/lib/document-refresh.svelte.js.map +1 -1
  159. package/dist/lib/document-workspace-registry.svelte.d.ts +15 -0
  160. package/dist/lib/document-workspace-registry.svelte.d.ts.map +1 -0
  161. package/dist/lib/document-workspace-registry.svelte.js +31 -0
  162. package/dist/lib/document-workspace-registry.svelte.js.map +1 -0
  163. package/dist/lib/field-validation/utils.d.ts.map +1 -1
  164. package/dist/lib/field-validation/utils.js +211 -29
  165. package/dist/lib/field-validation/utils.js.map +1 -1
  166. package/dist/lib/hooks.d.ts +2 -0
  167. package/dist/lib/hooks.d.ts.map +1 -1
  168. package/dist/lib/hooks.js +2 -0
  169. package/dist/lib/hooks.js.map +1 -1
  170. package/dist/lib/local-api/collection-api.d.ts +7 -2
  171. package/dist/lib/local-api/collection-api.d.ts.map +1 -1
  172. package/dist/lib/local-api/collection-api.js +17 -10
  173. package/dist/lib/local-api/collection-api.js.map +1 -1
  174. package/dist/lib/mcp/tools.d.ts +39 -4
  175. package/dist/lib/mcp/tools.d.ts.map +1 -1
  176. package/dist/lib/mcp/tools.js +546 -362
  177. package/dist/lib/mcp/tools.js.map +1 -1
  178. package/dist/lib/plugins/resolver.d.ts +8 -1
  179. package/dist/lib/plugins/resolver.d.ts.map +1 -1
  180. package/dist/lib/plugins/resolver.js +14 -1
  181. package/dist/lib/plugins/resolver.js.map +1 -1
  182. package/dist/lib/plugins/types.d.ts +23 -2
  183. package/dist/lib/plugins/types.d.ts.map +1 -1
  184. package/dist/lib/plugins/types.js.map +1 -1
  185. package/dist/lib/preview/stega.d.ts +2 -2
  186. package/dist/lib/preview/stega.d.ts.map +1 -1
  187. package/dist/lib/preview/stega.js +52 -29
  188. package/dist/lib/preview/stega.js.map +1 -1
  189. package/dist/lib/schema-utils/validator.d.ts.map +1 -1
  190. package/dist/lib/schema-utils/validator.js +5 -2
  191. package/dist/lib/schema-utils/validator.js.map +1 -1
  192. package/dist/lib/server/api/index.d.ts.map +1 -1
  193. package/dist/lib/server/api/index.js +8 -0
  194. package/dist/lib/server/api/index.js.map +1 -1
  195. package/dist/lib/server/api/routes/agent-change-sets.d.ts +4 -0
  196. package/dist/lib/server/api/routes/agent-change-sets.d.ts.map +1 -0
  197. package/dist/lib/server/api/routes/agent-change-sets.js +132 -0
  198. package/dist/lib/server/api/routes/agent-change-sets.js.map +1 -0
  199. package/dist/lib/server/api/routes/agent-chat.d.ts +14 -0
  200. package/dist/lib/server/api/routes/agent-chat.d.ts.map +1 -0
  201. package/dist/lib/server/api/routes/agent-chat.js +224 -0
  202. package/dist/lib/server/api/routes/agent-chat.js.map +1 -0
  203. package/dist/lib/server/api/routes/document-versions.d.ts.map +1 -1
  204. package/dist/lib/server/api/routes/document-versions.js +16 -2
  205. package/dist/lib/server/api/routes/document-versions.js.map +1 -1
  206. package/dist/lib/server/api/routes/documents-by-id.d.ts.map +1 -1
  207. package/dist/lib/server/api/routes/documents-by-id.js +11 -1
  208. package/dist/lib/server/api/routes/documents-by-id.js.map +1 -1
  209. package/dist/lib/server/api/routes/documents-publish.d.ts.map +1 -1
  210. package/dist/lib/server/api/routes/documents-publish.js +36 -3
  211. package/dist/lib/server/api/routes/documents-publish.js.map +1 -1
  212. package/dist/lib/server/api/routes/jobs.d.ts.map +1 -1
  213. package/dist/lib/server/api/routes/jobs.js +4 -2
  214. package/dist/lib/server/api/routes/jobs.js.map +1 -1
  215. package/dist/lib/server/api/routes/resolve-created-by.d.ts +13 -0
  216. package/dist/lib/server/api/routes/resolve-created-by.d.ts.map +1 -0
  217. package/dist/lib/server/api/routes/resolve-created-by.js +31 -0
  218. package/dist/lib/server/api/routes/resolve-created-by.js.map +1 -0
  219. package/dist/lib/server/index.d.ts +1 -0
  220. package/dist/lib/server/index.d.ts.map +1 -1
  221. package/dist/lib/server/index.js +1 -0
  222. package/dist/lib/server/index.js.map +1 -1
  223. package/dist/lib/services/references-service.d.ts +1 -1
  224. package/dist/lib/services/references-service.d.ts.map +1 -1
  225. package/dist/lib/services/version-service.d.ts +4 -4
  226. package/dist/lib/services/version-service.d.ts.map +1 -1
  227. package/dist/lib/services/version-service.js +15 -11
  228. package/dist/lib/services/version-service.js.map +1 -1
  229. package/dist/lib/type-gen.d.ts.map +1 -1
  230. package/dist/lib/type-gen.js +11 -0
  231. package/dist/lib/type-gen.js.map +1 -1
  232. package/dist/lib/types/agent-change-sets.d.ts +70 -0
  233. package/dist/lib/types/agent-change-sets.d.ts.map +1 -0
  234. package/dist/lib/types/agent-change-sets.js +7 -0
  235. package/dist/lib/types/agent-change-sets.js.map +1 -0
  236. package/dist/lib/types/agent-stream.d.ts +60 -0
  237. package/dist/lib/types/agent-stream.d.ts.map +1 -0
  238. package/dist/lib/types/agent-stream.js +12 -0
  239. package/dist/lib/types/agent-stream.js.map +1 -0
  240. package/dist/lib/types/agent-tools.d.ts +65 -0
  241. package/dist/lib/types/agent-tools.d.ts.map +1 -0
  242. package/dist/lib/types/agent-tools.js +2 -0
  243. package/dist/lib/types/agent-tools.js.map +1 -0
  244. package/dist/lib/types/config.d.ts +19 -0
  245. package/dist/lib/types/config.d.ts.map +1 -1
  246. package/dist/lib/types/document-workspace.d.ts +53 -0
  247. package/dist/lib/types/document-workspace.d.ts.map +1 -0
  248. package/dist/lib/types/document-workspace.js +15 -0
  249. package/dist/lib/types/document-workspace.js.map +1 -0
  250. package/dist/lib/types/document.d.ts +5 -0
  251. package/dist/lib/types/document.d.ts.map +1 -1
  252. package/dist/lib/types/index.d.ts +2 -0
  253. package/dist/lib/types/index.d.ts.map +1 -1
  254. package/dist/lib/types/index.js +2 -0
  255. package/dist/lib/types/index.js.map +1 -1
  256. package/dist/lib/types/schemas.js +1 -1
  257. package/dist/lib/types/schemas.js.map +1 -1
  258. package/dist/lib/utils/fetch-remote-file.d.ts +11 -0
  259. package/dist/lib/utils/fetch-remote-file.d.ts.map +1 -0
  260. package/dist/lib/utils/fetch-remote-file.js +169 -0
  261. package/dist/lib/utils/fetch-remote-file.js.map +1 -0
  262. package/dist/local-api/collection-api.d.ts +7 -2
  263. package/dist/local-api/collection-api.d.ts.map +1 -1
  264. package/dist/local-api/collection-api.js +17 -10
  265. package/dist/mcp/tools.d.ts +39 -4
  266. package/dist/mcp/tools.d.ts.map +1 -1
  267. package/dist/mcp/tools.js +546 -362
  268. package/dist/plugins/resolver.d.ts +8 -1
  269. package/dist/plugins/resolver.d.ts.map +1 -1
  270. package/dist/plugins/resolver.js +14 -1
  271. package/dist/plugins/types.d.ts +23 -2
  272. package/dist/plugins/types.d.ts.map +1 -1
  273. package/dist/preview/stega.d.ts +2 -2
  274. package/dist/preview/stega.d.ts.map +1 -1
  275. package/dist/preview/stega.js +52 -29
  276. package/dist/schema-utils/validator.d.ts.map +1 -1
  277. package/dist/schema-utils/validator.js +5 -2
  278. package/dist/server/api/index.d.ts.map +1 -1
  279. package/dist/server/api/index.js +8 -0
  280. package/dist/server/api/routes/agent-change-sets.d.ts +4 -0
  281. package/dist/server/api/routes/agent-change-sets.d.ts.map +1 -0
  282. package/dist/server/api/routes/agent-change-sets.js +131 -0
  283. package/dist/server/api/routes/agent-chat.d.ts +14 -0
  284. package/dist/server/api/routes/agent-chat.d.ts.map +1 -0
  285. package/dist/server/api/routes/agent-chat.js +223 -0
  286. package/dist/server/api/routes/document-versions.d.ts.map +1 -1
  287. package/dist/server/api/routes/document-versions.js +16 -2
  288. package/dist/server/api/routes/documents-by-id.d.ts.map +1 -1
  289. package/dist/server/api/routes/documents-by-id.js +11 -1
  290. package/dist/server/api/routes/documents-publish.d.ts.map +1 -1
  291. package/dist/server/api/routes/documents-publish.js +36 -3
  292. package/dist/server/api/routes/jobs.d.ts.map +1 -1
  293. package/dist/server/api/routes/jobs.js +4 -2
  294. package/dist/server/api/routes/resolve-created-by.d.ts +13 -0
  295. package/dist/server/api/routes/resolve-created-by.d.ts.map +1 -0
  296. package/dist/server/api/routes/resolve-created-by.js +30 -0
  297. package/dist/server/index.d.ts +1 -0
  298. package/dist/server/index.d.ts.map +1 -1
  299. package/dist/server/index.js +1 -0
  300. package/dist/services/references-service.d.ts +1 -1
  301. package/dist/services/references-service.d.ts.map +1 -1
  302. package/dist/services/version-service.d.ts +4 -4
  303. package/dist/services/version-service.d.ts.map +1 -1
  304. package/dist/services/version-service.js +15 -11
  305. package/dist/type-gen.d.ts.map +1 -1
  306. package/dist/type-gen.js +11 -0
  307. package/dist/types/agent-change-sets.d.ts +70 -0
  308. package/dist/types/agent-change-sets.d.ts.map +1 -0
  309. package/dist/types/agent-change-sets.js +6 -0
  310. package/dist/types/agent-stream.d.ts +60 -0
  311. package/dist/types/agent-stream.d.ts.map +1 -0
  312. package/dist/types/agent-stream.js +11 -0
  313. package/dist/types/agent-tools.d.ts +65 -0
  314. package/dist/types/agent-tools.d.ts.map +1 -0
  315. package/dist/types/agent-tools.js +1 -0
  316. package/dist/types/config.d.ts +19 -0
  317. package/dist/types/config.d.ts.map +1 -1
  318. package/dist/types/document-workspace.d.ts +53 -0
  319. package/dist/types/document-workspace.d.ts.map +1 -0
  320. package/dist/types/document-workspace.js +14 -0
  321. package/dist/types/document.d.ts +5 -0
  322. package/dist/types/document.d.ts.map +1 -1
  323. package/dist/types/index.d.ts +2 -0
  324. package/dist/types/index.d.ts.map +1 -1
  325. package/dist/types/index.js +2 -0
  326. package/dist/types/schemas.js +1 -1
  327. package/dist/utils/fetch-remote-file.d.ts +11 -0
  328. package/dist/utils/fetch-remote-file.d.ts.map +1 -0
  329. package/dist/utils/fetch-remote-file.js +168 -0
  330. package/package.json +5 -2
@@ -0,0 +1,977 @@
1
+ <script lang="ts">
2
+ import { Button } from '@aphexcms/ui/shadcn/button';
3
+ import { Textarea } from '@aphexcms/ui/shadcn/textarea';
4
+ import {
5
+ MessageScroller,
6
+ MessageScrollerButton,
7
+ MessageScrollerContent,
8
+ MessageScrollerItem,
9
+ MessageScrollerProvider,
10
+ MessageScrollerViewport
11
+ } from '@aphexcms/ui/shadcn/message-scroller';
12
+ import { Message, MessageContent, MessageFooter } from '@aphexcms/ui/shadcn/message';
13
+ import { Bubble, BubbleContent } from '@aphexcms/ui/shadcn/bubble';
14
+ import { Marker, MarkerContent, MarkerIcon } from '@aphexcms/ui/shadcn/marker';
15
+ import {
16
+ Bot,
17
+ Check,
18
+ ChevronDown,
19
+ CircleAlert,
20
+ Copy,
21
+ Database,
22
+ FileCode,
23
+ FilePlus,
24
+ FileText,
25
+ Image,
26
+ ImagePlus,
27
+ Info,
28
+ Layers,
29
+ LoaderCircle,
30
+ Pencil,
31
+ RefreshCw,
32
+ Search,
33
+ Send,
34
+ Settings,
35
+ ShieldCheck,
36
+ Sparkles,
37
+ Square,
38
+ Trash2,
39
+ UploadCloud,
40
+ Wrench
41
+ } from '@lucide/svelte';
42
+ import { page } from '$app/state';
43
+ import type { Component } from 'svelte';
44
+ import { marked } from 'marked';
45
+ import DOMPurify from 'dompurify';
46
+ import {
47
+ agentChatState,
48
+ type HistoryMessage,
49
+ type Turn,
50
+ type ToolCall
51
+ } from '../../agent-chat-state.svelte';
52
+ import { documentWorkspaceRegistry } from '../../document-workspace-registry.svelte';
53
+ import type { DocumentWorkspace } from '../../types/document-workspace';
54
+ import type { AgentStreamEvent } from '../../types/agent-stream';
55
+ import { streamAgentChat, recordWorkspaceOperation } from '../../api/agent-chat';
56
+ import { resolvePreviewTitle } from '../../utils/preview';
57
+ import { notifyCollectionChanged } from '../../document-refresh.svelte';
58
+
59
+ /** A suggestion can be a bare string, or `{ text, icon }` for a leading icon — the plain
60
+ * string form stays the common case so existing callers don't need to change anything. */
61
+ type Suggestion = string | { text: string; icon?: Component };
62
+
63
+ const DEFAULT_SUGGESTIONS: Suggestion[] = [
64
+ 'List the collections in this CMS',
65
+ 'Summarize the latest published documents',
66
+ 'Describe the schema for my pages'
67
+ ];
68
+
69
+ let {
70
+ embedded = false,
71
+ title = 'Aphex Assistant',
72
+ subtitle = 'CMS-aware answers and content tools',
73
+ suggestions = DEFAULT_SUGGESTIONS
74
+ }: {
75
+ embedded?: boolean;
76
+ title?: string;
77
+ subtitle?: string;
78
+ suggestions?: Suggestion[];
79
+ } = $props();
80
+
81
+ /** Result shape a workspace-tool handler (below) hands back — the same idea as
82
+ * `AgentToolResult`, kept local so this file doesn't need a server-only type import. */
83
+ type WorkspaceToolHandlerResult = { success: boolean; data?: unknown; error?: string };
84
+
85
+ type WorkspaceToolHandler = (
86
+ args: Record<string, unknown>,
87
+ ws: DocumentWorkspace
88
+ ) => Promise<WorkspaceToolHandlerResult>;
89
+
90
+ const patchFieldsHandler: WorkspaceToolHandler = async (args, ws) => {
91
+ const fields = (args.fields as Record<string, unknown>) ?? {};
92
+ ws.apply({ type: 'patchFields', fields });
93
+ const validation = await ws.validate();
94
+ return { success: true, data: { applied: Object.keys(fields), validation } };
95
+ };
96
+
97
+ const saveDraftHandler: WorkspaceToolHandler = async (_args, ws) => {
98
+ const snapshot = ws.getSnapshot();
99
+ const result = await ws.flushSave(snapshot.revision ?? undefined);
100
+ if (!result.success) {
101
+ return {
102
+ success: false,
103
+ error: result.conflict
104
+ ? 'Revision conflict — reload before continuing.'
105
+ : (result.error ?? 'Save failed')
106
+ };
107
+ }
108
+ return { success: true, data: { revision: result.revision } };
109
+ };
110
+
111
+ /** Resolves `content_patch_fields`/`content_save_draft` against a live `DocumentWorkspace` —
112
+ * see types/document-workspace.ts and ai/content-workspace-tools.ts for why these two tools
113
+ * can't be executed server-side at all. */
114
+ const WORKSPACE_TOOL_HANDLERS: Record<string, WorkspaceToolHandler> = {
115
+ content_patch_fields: patchFieldsHandler,
116
+ content_save_draft: saveDraftHandler
117
+ };
118
+
119
+ /** Only a flush's outcome is worth an audit row — `content_patch_fields` just buffers an
120
+ * in-memory change with nothing durable to attribute yet (see the plan doc's atomicity
121
+ * design: one save = one operation, not one per patch). */
122
+ const WORKSPACE_TOOLS_RECORDED = new Set(['content_save_draft']);
123
+
124
+ /** `currentDocKey` is always built as `${docType}:${docId}` with both non-empty (see
125
+ * below), but that invariant isn't visible to `.split(':')`'s return type — parse it back
126
+ * out explicitly instead of asserting the shape with a cast. */
127
+ function parseDocKey(key: string): { collection: string; id: string } | null {
128
+ const separator = key.indexOf(':');
129
+ if (separator === -1) return null;
130
+ return { collection: key.slice(0, separator), id: key.slice(separator + 1) };
131
+ }
132
+
133
+ let input = $state('');
134
+ let streaming = $state(false);
135
+ let controller: AbortController | null = null;
136
+
137
+ // The admin desk is a single `/admin` route driven by `docType`/`docId` query params
138
+ // (see AdminApp.svelte), not per-document URLs — this is how the assistant knows what's
139
+ // open without any data being pushed to it. Only a breadcrumb, never the document's
140
+ // content itself: the model calls `get_document` (already a registered tool) when it
141
+ // actually needs field values, so this stays cheap on every request and never goes stale.
142
+ const currentDocKey = $derived.by(() => {
143
+ const docType = page.url.searchParams.get('docType');
144
+ const docId = page.url.searchParams.get('docId');
145
+ return docType && docId ? `${docType}:${docId}` : null;
146
+ });
147
+
148
+ // A URL param says *which* document is open; it's not a handle to mutate. Cross-check
149
+ // against document-workspace-registry.svelte.ts — the live editor tab's own registration —
150
+ // so a stale/mismatched registration (e.g. from a just-closed tab) never gets bridged into.
151
+ // Only when both agree does this chat gain the content_patch_fields/content_save_draft
152
+ // tools; everywhere else (no document open, or a URL that doesn't match what's registered)
153
+ // it behaves exactly as a general chat always has.
154
+ const matchedWorkspace = $derived.by(() => {
155
+ if (!currentDocKey) return null;
156
+ const [docType, docId] = currentDocKey.split(':');
157
+ const registered = documentWorkspaceRegistry.current;
158
+ if (registered && registered.documentId === docId && registered.collection === docType) {
159
+ return registered.workspace;
160
+ }
161
+ return null;
162
+ });
163
+
164
+ /** No schema context is threaded down to this global panel (schema context is scoped to
165
+ * DocumentEditor.svelte's own subtree) — resolving via conventional field names only
166
+ * (`resolvePreviewTitle`'s no-schema fallback path) rather than a schema's own
167
+ * `preview.prepare`/`select` config. Good enough for a breadcrumb chip; not worth wiring
168
+ * schema context app-wide just for this. */
169
+ function capitalize(s: string): string {
170
+ return s.length > 0 ? s.charAt(0).toUpperCase() + s.slice(1) : s;
171
+ }
172
+
173
+ // Cursor-style persistent context chip: shows what's currently attached, live, distinct
174
+ // from the conversation transcript. Three states — no doc open (null, nothing rendered),
175
+ // doc open but not bridged (editing: false, read-only awareness only), doc open and
176
+ // bridged (editing: true, content_patch_fields/content_save_draft are live).
177
+ const documentLabel = $derived.by(() => {
178
+ if (!currentDocKey) return null;
179
+ const parsed = parseDocKey(currentDocKey);
180
+ if (!parsed) return null;
181
+ const typeLabel = capitalize(parsed.collection);
182
+ const workspace = matchedWorkspace;
183
+ if (!workspace) return { editing: false, text: typeLabel };
184
+ const title = resolvePreviewTitle(workspace.getSnapshot().data, null, typeLabel);
185
+ return { editing: true, text: `${typeLabel} — ${title}` };
186
+ });
187
+
188
+ // Soft nudge, not a hard limit — this endpoint resends the full conversation every call,
189
+ // so `lastPromptTokens` only grows. A conservative threshold since the actual context
190
+ // window varies by provider/model and isn't known client-side.
191
+ const LONG_CONVERSATION_TOKENS = 50_000;
192
+ const isGettingLong = $derived(agentChatState.lastPromptTokens > LONG_CONVERSATION_TOKENS);
193
+
194
+ async function send(message = input.trim()) {
195
+ if (!message || streaming) return;
196
+
197
+ input = '';
198
+ if (currentDocKey && currentDocKey !== agentChatState.contextSentFor) {
199
+ const [docType, docId] = currentDocKey.split(':');
200
+ agentChatState.history.push({
201
+ role: 'system',
202
+ content: matchedWorkspace
203
+ ? `The user is currently viewing document ${docId} in collection ${docType} in the editor. Use content_patch_fields/content_save_draft for edits to this document — do not use update_document for it, since only the workspace tools keep the open editor in sync.`
204
+ : `The user is currently viewing document ${docId} in collection ${docType}.`
205
+ });
206
+ agentChatState.contextSentFor = currentDocKey;
207
+ }
208
+ const userTurn: Turn = {
209
+ id: crypto.randomUUID(),
210
+ role: 'user',
211
+ text: message,
212
+ status: 'complete',
213
+ toolCalls: [],
214
+ historyIndexBeforeTurn: agentChatState.history.length
215
+ };
216
+ const assistantTurn: Turn = {
217
+ id: crypto.randomUUID(),
218
+ role: 'assistant',
219
+ text: '',
220
+ status: 'streaming',
221
+ toolCalls: []
222
+ };
223
+ agentChatState.turns.push(userTurn, assistantTurn);
224
+ agentChatState.history.push({ role: 'user', content: message });
225
+ const assistantIndex = agentChatState.turns.length - 1;
226
+ streaming = true;
227
+ controller = new AbortController();
228
+
229
+ // Captured once, at the start of this turn — even if the registered document changes
230
+ // mid-turn (navigation), this turn keeps bridging into the document it started with,
231
+ // not whatever happens to be open when a later leg resumes.
232
+ const workspace = matchedWorkspace;
233
+ const documentContext = workspace && currentDocKey ? parseDocKey(currentDocKey) : null;
234
+
235
+ let changeSetId: string | null = null;
236
+ let cumulativeUsage = { promptTokens: 0, completionTokens: 0 };
237
+ // Set by content_patch_fields, cleared by content_save_draft — if a turn ends with
238
+ // buffered-but-unflushed patches (the model forgot to call content_save_draft), the
239
+ // safety net below flushes them once rather than leaving them stranded unsaved.
240
+ let dirtySinceFlush = false;
241
+
242
+ if (workspace) workspace.beginBatch('agent');
243
+
244
+ try {
245
+ let messages: HistoryMessage[] = agentChatState.history;
246
+ let turnDone = false;
247
+
248
+ while (!turnDone) {
249
+ let pendingWorkspaceCalls:
250
+ | { toolCallId: string; name: string; arguments: Record<string, unknown> }[]
251
+ | null = null;
252
+
253
+ for await (const event of streamAgentChat(
254
+ {
255
+ messages,
256
+ documentContext: documentContext ?? undefined,
257
+ changeSetId: changeSetId ?? undefined,
258
+ priorUsage: changeSetId ? cumulativeUsage : undefined
259
+ },
260
+ controller.signal
261
+ )) {
262
+ if (event.type === 'usage') {
263
+ cumulativeUsage = {
264
+ promptTokens: event.promptTokens,
265
+ completionTokens: event.completionTokens
266
+ };
267
+ }
268
+ if (event.type === 'done') {
269
+ changeSetId = event.changeSetId ?? changeSetId;
270
+ messages = event.messages;
271
+ agentChatState.history = messages;
272
+ if (event.finishReason === 'awaiting_workspace_tool') {
273
+ pendingWorkspaceCalls = event.pendingWorkspaceCalls ?? [];
274
+ } else {
275
+ turnDone = true;
276
+ const turn = agentChatState.turns[assistantIndex];
277
+ if (turn?.status === 'streaming') {
278
+ turn.status = event.finishReason === 'error' ? 'error' : 'complete';
279
+ }
280
+ }
281
+ } else {
282
+ handleStreamEvent(assistantIndex, event);
283
+ }
284
+ }
285
+
286
+ if (pendingWorkspaceCalls && pendingWorkspaceCalls.length > 0) {
287
+ for (const call of pendingWorkspaceCalls) {
288
+ const handler = workspace ? WORKSPACE_TOOL_HANDLERS[call.name] : undefined;
289
+ const result: WorkspaceToolHandlerResult = handler
290
+ ? await handler(call.arguments, workspace!)
291
+ : {
292
+ success: false,
293
+ error: workspace
294
+ ? `Unknown workspace tool: ${call.name}`
295
+ : 'No document is open to apply this change to.'
296
+ };
297
+
298
+ if (call.name === 'content_patch_fields') dirtySinceFlush = result.success;
299
+ if (call.name === 'content_save_draft') {
300
+ dirtySinceFlush = false;
301
+ // Resolved entirely client-side, so handleStreamEvent never sees it —
302
+ // invalidate the list here too, same as the server-mode tools above.
303
+ if (result.success && documentContext) {
304
+ notifyCollectionChanged(documentContext.collection);
305
+ }
306
+ }
307
+
308
+ resolveWorkspaceToolCall(assistantIndex, call.toolCallId, result);
309
+ messages = [
310
+ ...messages,
311
+ {
312
+ role: 'tool',
313
+ toolCallId: call.toolCallId,
314
+ content: JSON.stringify(
315
+ result.success ? (result.data ?? null) : { error: result.error }
316
+ )
317
+ }
318
+ ];
319
+
320
+ if (changeSetId && documentContext && WORKSPACE_TOOLS_RECORDED.has(call.name)) {
321
+ await safeRecordWorkspaceOperation({
322
+ changeSetId,
323
+ toolName: call.name,
324
+ collection: documentContext.collection,
325
+ id: documentContext.id,
326
+ success: result.success,
327
+ error: result.error,
328
+ arguments: call.arguments,
329
+ data: result.data
330
+ });
331
+ }
332
+ }
333
+ agentChatState.history = messages;
334
+ } else if (!turnDone) {
335
+ // Defensive: the stream ended (e.g. aborted) without a 'done' event at all.
336
+ turnDone = true;
337
+ }
338
+ }
339
+
340
+ if (dirtySinceFlush && workspace) {
341
+ const result = await saveDraftHandler({}, workspace);
342
+ if (result.success && documentContext) notifyCollectionChanged(documentContext.collection);
343
+ if (changeSetId && documentContext) {
344
+ await safeRecordWorkspaceOperation({
345
+ changeSetId,
346
+ toolName: 'content_save_draft',
347
+ collection: documentContext.collection,
348
+ id: documentContext.id,
349
+ success: result.success,
350
+ error: result.error,
351
+ arguments: {},
352
+ data: result.data
353
+ });
354
+ }
355
+ }
356
+ } catch (error) {
357
+ const turn = agentChatState.turns[assistantIndex];
358
+ if (!turn) return;
359
+ if (error instanceof DOMException && error.name === 'AbortError') {
360
+ turn.status = 'stopped';
361
+ } else {
362
+ turn.status = 'error';
363
+ turn.error = error instanceof Error ? error.message : String(error);
364
+ }
365
+ } finally {
366
+ if (workspace) workspace.endBatch();
367
+ streaming = false;
368
+ controller = null;
369
+ }
370
+ }
371
+
372
+ // Server-mode tools that create/update/publish a document by `collection` — a collection
373
+ // list open elsewhere in the session (AdminApp) has no way to know about these on its own,
374
+ // same class of staleness as the single-document editor case. All three take `collection`
375
+ // as an argument, so the tool call's own arguments are enough to know what to invalidate —
376
+ // no need to inspect the (differently-shaped) result data of each.
377
+ const LIST_INVALIDATING_TOOLS = new Set([
378
+ 'create_document',
379
+ 'update_document',
380
+ 'publish_document'
381
+ ]);
382
+
383
+ function handleStreamEvent(assistantIndex: number, event: AgentStreamEvent) {
384
+ const turn = agentChatState.turns[assistantIndex];
385
+ if (!turn) return;
386
+ if (event.type === 'text') {
387
+ turn.text += event.delta;
388
+ } else if (event.type === 'toolCall') {
389
+ turn.toolCalls.push({
390
+ id: event.toolCallId,
391
+ name: event.name,
392
+ arguments: event.arguments,
393
+ status: 'running'
394
+ });
395
+ } else if (event.type === 'toolResult') {
396
+ const tool = turn.toolCalls.find((call) => call.id === event.toolCallId);
397
+ if (tool) {
398
+ tool.status = event.success ? 'complete' : 'error';
399
+ tool.result = event.data;
400
+ tool.error = event.error;
401
+ if (event.success && LIST_INVALIDATING_TOOLS.has(tool.name)) {
402
+ const collection = tool.arguments?.collection;
403
+ if (typeof collection === 'string') notifyCollectionChanged(collection);
404
+ }
405
+ }
406
+ } else if (event.type === 'error') {
407
+ turn.status = 'error';
408
+ turn.error = event.message;
409
+ } else if (event.type === 'usage') {
410
+ // The endpoint resends the full conversation on every call, so the latest
411
+ // promptTokens is effectively "how big the conversation currently is" — used to
412
+ // warn before it gets unwieldy. Overwrite, don't sum: a turn can report usage
413
+ // multiple times across tool-calling round trips, and each one already reflects
414
+ // the whole conversation up to that point, not an incremental slice.
415
+ agentChatState.lastPromptTokens = event.promptTokens;
416
+ }
417
+ // 'done' is handled by send()'s own loop, not here — unlike every other event, it has
418
+ // to *decide* something (pause-and-resume vs. actually finish) that only the loop
419
+ // driving the fetch calls can act on; this function only ever does passive display
420
+ // updates for events streamed within one leg.
421
+ }
422
+
423
+ /** Reflects a workspace tool's locally-resolved result onto its (already-rendered, still
424
+ * "running") entry in the turn's tool-call list — the SSE stream already emitted the
425
+ * `toolCall` event for it (before the server paused), just not a `toolResult`, since only
426
+ * the browser can produce that result. Same shape update as the `toolResult` branch above. */
427
+ function resolveWorkspaceToolCall(
428
+ assistantIndex: number,
429
+ toolCallId: string,
430
+ result: WorkspaceToolHandlerResult
431
+ ) {
432
+ const turn = agentChatState.turns[assistantIndex];
433
+ const tool = turn?.toolCalls.find((call) => call.id === toolCallId);
434
+ if (tool) {
435
+ tool.status = result.success ? 'complete' : 'error';
436
+ tool.result = result.data;
437
+ tool.error = result.error;
438
+ }
439
+ }
440
+
441
+ /** Best-effort audit record for a workspace tool's result — `recordWorkspaceOperation`
442
+ * (api/agent-chat.ts) throws on any HTTP/network failure (it goes through `apiClient`); this
443
+ * call must never break the chat turn just because the audit write failed. */
444
+ async function safeRecordWorkspaceOperation(
445
+ body: Parameters<typeof recordWorkspaceOperation>[0]
446
+ ) {
447
+ try {
448
+ await recordWorkspaceOperation(body);
449
+ } catch {
450
+ // best-effort — see doc comment above
451
+ }
452
+ }
453
+
454
+ function stop() {
455
+ controller?.abort();
456
+ }
457
+
458
+ function clearConversation() {
459
+ if (streaming) return;
460
+ // This wipes the persisted copy too (agent-chat-state.svelte.ts mirrors every change to
461
+ // localStorage), so confirm — unlike before that persistence existed, this is no longer
462
+ // trivially recoverable by just reopening the panel.
463
+ if (!confirm('Clear this conversation? This cannot be undone.')) return;
464
+ agentChatState.turns = [];
465
+ agentChatState.history = [];
466
+ agentChatState.contextSentFor = null;
467
+ agentChatState.lastPromptTokens = 0;
468
+ input = '';
469
+ }
470
+
471
+ function retry(turnIndex: number) {
472
+ if (streaming || turnIndex < 1) return;
473
+ const prompt = agentChatState.turns[turnIndex - 1];
474
+ if (!prompt || prompt.role !== 'user') return;
475
+ agentChatState.turns = agentChatState.turns.slice(0, turnIndex - 1);
476
+ // A turn's server-reported `messages` can add more than the naive "2 entries" once tool
477
+ // calls are involved, so roll back to the exact length recorded before this turn rather
478
+ // than assuming an offset.
479
+ agentChatState.history = agentChatState.history.slice(
480
+ 0,
481
+ prompt.historyIndexBeforeTurn ?? agentChatState.history.length
482
+ );
483
+ send(prompt.text);
484
+ }
485
+
486
+ async function copyText(text: string) {
487
+ await navigator.clipboard.writeText(text);
488
+ }
489
+
490
+ marked.setOptions({ breaks: true, gfm: true });
491
+
492
+ // Assistant text is model-generated markdown, not trusted HTML — parse then sanitize before
493
+ // ever using `{@html}`. `marked.parse` is sync here (no async extensions registered).
494
+ function renderMarkdown(text: string): string {
495
+ return DOMPurify.sanitize(marked.parse(text, { async: false }));
496
+ }
497
+
498
+ function formatToolName(name: string) {
499
+ return name.replaceAll('_', ' ');
500
+ }
501
+
502
+ // Past-tense phrase per tool, for both the one-line turn summary ("Listed collections,
503
+ // updated a document") and each entry's title in the expanded detail.
504
+ const TOOL_VERBS: Record<string, string> = {
505
+ describe_cms: 'inspected the CMS',
506
+ list_collections: 'listed collections',
507
+ get_schema: 'read a schema',
508
+ validate_schema: 'validated a schema',
509
+ validate_document: 'validated a document',
510
+ query_documents: 'searched documents',
511
+ get_document: 'read a document',
512
+ create_document: 'created a document',
513
+ update_document: 'updated a document',
514
+ publish_document: 'published a document',
515
+ get_singleton: 'read settings',
516
+ update_singleton: 'updated settings',
517
+ list_assets: 'browsed assets',
518
+ upload_asset: 'uploaded a file'
519
+ };
520
+
521
+ const TOOL_ICONS: Record<string, Component> = {
522
+ describe_cms: Info,
523
+ list_collections: Layers,
524
+ get_schema: FileCode,
525
+ validate_schema: ShieldCheck,
526
+ validate_document: ShieldCheck,
527
+ query_documents: Search,
528
+ get_document: FileText,
529
+ create_document: FilePlus,
530
+ update_document: Pencil,
531
+ publish_document: UploadCloud,
532
+ get_singleton: Settings,
533
+ update_singleton: Settings,
534
+ list_assets: Image,
535
+ upload_asset: ImagePlus
536
+ };
537
+
538
+ function toolVerb(name: string) {
539
+ return TOOL_VERBS[name] ?? formatToolName(name);
540
+ }
541
+
542
+ function toolIcon(name: string): Component {
543
+ return TOOL_ICONS[name] ?? Wrench;
544
+ }
545
+
546
+ function toolTitle(tool: ToolCall) {
547
+ const verb = toolVerb(tool.name);
548
+ return verb.charAt(0).toUpperCase() + verb.slice(1);
549
+ }
550
+
551
+ /** One line of context under a tool entry — where it looked/wrote, and a result count if
552
+ * the tool's own result shape has one. Kept generic across tools rather than special-cased
553
+ * per name, since new tools shouldn't need a matching branch here to show *something*. */
554
+ function plural(n: number, word: string) {
555
+ return `${n} ${word}${n === 1 ? '' : 's'}`;
556
+ }
557
+
558
+ /** A result note per tool, named to its actual result shape — not a generic "find the
559
+ * first array field" guess, which produced nonsense (e.g. counting validate_document's
560
+ * `errors` array as "results", or grabbing an arbitrary field off describe_cms). Tools
561
+ * whose result has nothing meaningful to summarize just get no note (the location line
562
+ * — collection/id — still shows). */
563
+ function toolResultNote(tool: ToolCall): string | null {
564
+ const r = tool.result;
565
+ if (!r || typeof r !== 'object') return null;
566
+ const record = r as Record<string, unknown>;
567
+
568
+ switch (tool.name) {
569
+ case 'list_collections': {
570
+ const n = (record.collections as unknown[] | undefined)?.length;
571
+ return n === undefined ? null : plural(n, 'collection');
572
+ }
573
+ case 'list_assets': {
574
+ const n = (record.assets as unknown[] | undefined)?.length;
575
+ return n === undefined ? null : plural(n, 'asset');
576
+ }
577
+ case 'query_documents': {
578
+ const total = record.totalDocs;
579
+ return typeof total === 'number' ? plural(total, 'document') : null;
580
+ }
581
+ case 'validate_document':
582
+ case 'validate_schema': {
583
+ if (record.isValid === true) return 'valid';
584
+ const n = (record.errors as unknown[] | undefined)?.length ?? 0;
585
+ return plural(n, 'error');
586
+ }
587
+ default:
588
+ return null;
589
+ }
590
+ }
591
+
592
+ function toolDetail(tool: ToolCall): string | null {
593
+ const parts: string[] = [];
594
+ const collection = tool.arguments.collection;
595
+ const id = tool.arguments.id;
596
+ if (typeof collection === 'string') parts.push(collection);
597
+ if (typeof id === 'string') parts.push(id);
598
+ const location = parts.join(' · ');
599
+
600
+ return [location, toolResultNote(tool)].filter(Boolean).join(' — ') || null;
601
+ }
602
+
603
+ // The turn's collapsed summary line — unique tool verbs in first-use order, e.g.
604
+ // "Listed collections, updated a document".
605
+ function toolsSummary(turn: Turn): string {
606
+ const seen = new Set<string>();
607
+ const verbs: string[] = [];
608
+ for (const tool of turn.toolCalls) {
609
+ if (seen.has(tool.name)) continue;
610
+ seen.add(tool.name);
611
+ verbs.push(toolVerb(tool.name));
612
+ }
613
+ const sentence = verbs.join(', ');
614
+ return sentence.charAt(0).toUpperCase() + sentence.slice(1);
615
+ }
616
+
617
+ function streamingLabel(turn: Turn) {
618
+ if (turn.toolCalls.some((tool) => tool.status === 'running')) return 'Using tools...';
619
+ if (turn.text) return 'Generating response...';
620
+ return 'Thinking...';
621
+ }
622
+
623
+ function handleComposerKeydown(event: KeyboardEvent) {
624
+ if (event.key === 'Enter' && !event.shiftKey && !event.isComposing) {
625
+ event.preventDefault();
626
+ send();
627
+ }
628
+ }
629
+ </script>
630
+
631
+ <div class="bg-muted/20 flex h-full min-h-0 flex-col">
632
+ <header
633
+ class="bg-background/90 relative flex h-14 shrink-0 items-center justify-between border-b px-4 backdrop-blur sm:px-6 {embedded
634
+ ? 'pr-20 sm:pr-20'
635
+ : ''}"
636
+ >
637
+ <div class="flex min-w-0 items-center gap-3">
638
+ <div
639
+ class="bg-primary/10 text-primary flex size-8 shrink-0 items-center justify-center rounded-lg"
640
+ >
641
+ <Sparkles class="size-4" />
642
+ </div>
643
+ <div class="min-w-0">
644
+ <h1 class="truncate text-sm font-semibold">{title}</h1>
645
+ <p class="text-muted-foreground truncate text-xs">{subtitle}</p>
646
+ </div>
647
+ </div>
648
+ {#if agentChatState.turns.length > 0}
649
+ <Button
650
+ variant="ghost"
651
+ size="icon-sm"
652
+ onclick={clearConversation}
653
+ disabled={streaming}
654
+ aria-label="Clear conversation"
655
+ class={embedded ? 'absolute top-2 right-14' : ''}
656
+ >
657
+ <Trash2 />
658
+ </Button>
659
+ {/if}
660
+ </header>
661
+
662
+ <MessageScrollerProvider
663
+ autoScroll
664
+ defaultScrollPosition="last-anchor"
665
+ scrollPreviousItemPeek={48}
666
+ >
667
+ <MessageScroller class="min-h-0">
668
+ <MessageScrollerViewport>
669
+ <MessageScrollerContent
670
+ class="mx-auto w-full max-w-3xl px-4 py-8 sm:px-6"
671
+ aria-busy={streaming}
672
+ >
673
+ {#if agentChatState.turns.length === 0}
674
+ <div class="m-auto flex max-w-xl flex-col items-center px-4 py-12 text-center">
675
+ <div
676
+ class="border-primary/20 bg-primary/10 text-primary mb-5 flex size-14 items-center justify-center rounded-2xl border shadow-sm"
677
+ >
678
+ <Bot class="size-7" />
679
+ </div>
680
+ <h2 class="text-xl font-semibold tracking-tight">What are we working on?</h2>
681
+ <p class="text-muted-foreground mt-2 max-w-md text-sm leading-relaxed">
682
+ Ask about your schemas, inspect documents, or use the CMS tools available to your
683
+ role.
684
+ </p>
685
+ <div class="mt-6 grid w-full gap-2 sm:grid-cols-3">
686
+ {#each suggestions as suggestion}
687
+ {@const text = typeof suggestion === 'string' ? suggestion : suggestion.text}
688
+ {@const Icon = typeof suggestion === 'string' ? null : suggestion.icon}
689
+ <button
690
+ type="button"
691
+ class="bg-background hover:bg-accent flex cursor-pointer items-start gap-2 rounded-xl border p-3 text-left text-xs leading-relaxed shadow-xs transition-colors"
692
+ onclick={() => send(text)}
693
+ >
694
+ {#if Icon}
695
+ <Icon class="text-muted-foreground mt-0.5 size-3.5 shrink-0" />
696
+ {/if}
697
+ <span>{text}</span>
698
+ </button>
699
+ {/each}
700
+ </div>
701
+ </div>
702
+ {:else}
703
+ {#each agentChatState.turns as turn, index (turn.id)}
704
+ <MessageScrollerItem messageId={turn.id} scrollAnchor={turn.role === 'user'}>
705
+ <Message align={turn.role === 'user' ? 'end' : 'start'}>
706
+ <MessageContent>
707
+ {#if turn.role === 'user'}
708
+ <Bubble variant="secondary" align="end"
709
+ ><BubbleContent class="rounded-xl">{turn.text}</BubbleContent></Bubble
710
+ >
711
+ {:else}
712
+ {#if turn.toolCalls.length > 0}
713
+ <details class="group/tools mb-2 w-full">
714
+ <summary
715
+ class="text-muted-foreground hover:text-foreground flex cursor-pointer list-none items-center gap-1.5 text-xs transition-colors"
716
+ >
717
+ <span
718
+ class={turn.toolCalls.some((t) => t.status === 'running')
719
+ ? 'shimmer'
720
+ : ''}>{toolsSummary(turn)}</span
721
+ >
722
+ <ChevronDown
723
+ class="size-3 transition-transform group-open/tools:rotate-180"
724
+ />
725
+ </summary>
726
+ <div
727
+ class="bg-muted/40 mt-1.5 flex flex-col gap-2.5 rounded-lg px-2.5 py-2"
728
+ >
729
+ {#each turn.toolCalls as tool (tool.id)}
730
+ {@const Icon = toolIcon(tool.name)}
731
+ {@const detail = toolDetail(tool)}
732
+ <div class="flex flex-col gap-0.5">
733
+ <div class="text-muted-foreground flex items-center gap-2 text-xs">
734
+ {#if tool.status === 'running'}
735
+ <LoaderCircle class="size-3 shrink-0 animate-spin" />
736
+ {:else if tool.status === 'error'}
737
+ <CircleAlert class="text-destructive size-3 shrink-0" />
738
+ {:else}
739
+ <Icon class="size-3 shrink-0" />
740
+ {/if}
741
+ <span>{toolTitle(tool)}</span>
742
+ </div>
743
+ {#if tool.error}
744
+ <p class="text-destructive pl-5 text-xs">{tool.error}</p>
745
+ {:else if detail}
746
+ <p class="text-muted-foreground/80 pl-5 text-xs">{detail}</p>
747
+ {/if}
748
+ </div>
749
+ {/each}
750
+ <div class="text-muted-foreground flex items-center gap-2 text-xs">
751
+ {#if turn.toolCalls.some((t) => t.status === 'running')}
752
+ <LoaderCircle class="size-3 animate-spin" /> Working…
753
+ {:else if turn.toolCalls.some((t) => t.status === 'error')}
754
+ <CircleAlert class="text-destructive size-3" /> Done with errors
755
+ {:else}
756
+ <Check class="size-3" /> Done
757
+ {/if}
758
+ </div>
759
+ </div>
760
+ </details>
761
+ {/if}
762
+
763
+ {#if turn.text}
764
+ <Bubble variant="ghost"
765
+ ><BubbleContent class="markdown-body px-0"
766
+ >{@html renderMarkdown(turn.text)}</BubbleContent
767
+ ></Bubble
768
+ >
769
+ {/if}
770
+ {#if turn.status === 'streaming'}
771
+ <Marker
772
+ role="status"
773
+ class="bg-muted/60 mt-1 w-fit justify-start rounded-full px-3 py-1.5"
774
+ >
775
+ <MarkerIcon><LoaderCircle class="animate-spin" /></MarkerIcon>
776
+ <MarkerContent class="shimmer font-medium"
777
+ >{streamingLabel(turn)}</MarkerContent
778
+ >
779
+ </Marker>
780
+ {/if}
781
+
782
+ {#if turn.error}
783
+ <Marker variant="border" class="text-destructive border-destructive/30"
784
+ ><MarkerIcon><CircleAlert /></MarkerIcon><MarkerContent
785
+ >{turn.error}</MarkerContent
786
+ ></Marker
787
+ >
788
+ {/if}
789
+ <MessageFooter>
790
+ {#if turn.status === 'stopped'}<span>Stopped</span>{/if}
791
+ {#if turn.text && turn.status !== 'streaming'}
792
+ <Button
793
+ variant="ghost"
794
+ size="icon-sm"
795
+ class="size-7"
796
+ onclick={() => copyText(turn.text)}
797
+ aria-label="Copy response"><Copy /></Button
798
+ >
799
+ <Button
800
+ variant="ghost"
801
+ size="icon-sm"
802
+ class="size-7"
803
+ onclick={() => retry(index)}
804
+ disabled={streaming}
805
+ aria-label="Retry response"><RefreshCw /></Button
806
+ >
807
+ {/if}
808
+ </MessageFooter>
809
+ {/if}
810
+ </MessageContent>
811
+ </Message>
812
+ </MessageScrollerItem>
813
+ {/each}
814
+ {/if}
815
+ </MessageScrollerContent>
816
+ </MessageScrollerViewport>
817
+ <MessageScrollerButton />
818
+ </MessageScroller>
819
+ </MessageScrollerProvider>
820
+
821
+ <div
822
+ class="from-background via-background bg-linear-to-t to-transparent px-3 pt-2 pb-3 sm:px-6 sm:pb-5"
823
+ >
824
+ {#if documentLabel}
825
+ <div
826
+ class="mx-auto mb-2 flex max-w-3xl items-center gap-1.5 text-xs {documentLabel.editing
827
+ ? 'text-primary'
828
+ : 'text-muted-foreground'}"
829
+ >
830
+ {#if documentLabel.editing}
831
+ <Pencil class="size-3.5 shrink-0" />
832
+ <span class="truncate font-medium">Editing: {documentLabel.text}</span>
833
+ {:else}
834
+ <FileText class="size-3.5 shrink-0" />
835
+ <span class="truncate">Viewing: {documentLabel.text}</span>
836
+ {/if}
837
+ </div>
838
+ {/if}
839
+ {#if isGettingLong}
840
+ <div
841
+ class="border-rule bg-muted/60 text-muted-foreground mx-auto mb-2 flex max-w-3xl items-center gap-2 rounded-lg border px-3 py-2 text-xs"
842
+ >
843
+ <CircleAlert class="size-3.5 shrink-0" />
844
+ <span class="flex-1"
845
+ >This conversation is getting long (~{Math.round(agentChatState.lastPromptTokens / 1000)}K
846
+ tokens sent each turn) — consider clearing it for better results.</span
847
+ >
848
+ <Button variant="ghost" size="sm" class="h-6 shrink-0 px-2" onclick={clearConversation}
849
+ >Clear</Button
850
+ >
851
+ </div>
852
+ {/if}
853
+ <form
854
+ class="bg-background focus-within:border-ring focus-within:ring-ring/20 mx-auto max-w-3xl rounded-2xl border p-2 shadow-lg transition-shadow focus-within:ring-3"
855
+ onsubmit={(event) => {
856
+ event.preventDefault();
857
+ send();
858
+ }}
859
+ >
860
+ <Textarea
861
+ bind:value={input}
862
+ onkeydown={handleComposerKeydown}
863
+ rows={1}
864
+ placeholder="Ask about your content..."
865
+ class="max-h-40 min-h-11 resize-none border-0 bg-transparent px-2 shadow-none focus-visible:ring-0"
866
+ disabled={streaming}
867
+ />
868
+ <div class="flex items-center justify-between px-1 pt-1">
869
+ <div class="text-muted-foreground flex items-center gap-1.5 text-[11px]">
870
+ <Database class="size-3" /><span>Uses tools allowed by your role</span>
871
+ </div>
872
+ {#if streaming}
873
+ <Button
874
+ type="button"
875
+ size="icon-sm"
876
+ variant="outline"
877
+ onclick={stop}
878
+ aria-label="Stop response"><Square class="fill-current" /></Button
879
+ >
880
+ {:else}
881
+ <Button type="submit" size="icon-sm" disabled={!input.trim()} aria-label="Send message"
882
+ ><Send /></Button
883
+ >
884
+ {/if}
885
+ </div>
886
+ </form>
887
+ <p class="text-muted-foreground mx-auto mt-2 max-w-3xl text-center text-[10px]">
888
+ AI can make mistakes. Review content changes before publishing.
889
+ </p>
890
+ </div>
891
+ </div>
892
+
893
+ <style>
894
+ /* `{@html}`-injected markdown bypasses Svelte's scoped-style attribute, so these rules
895
+ have to be :global — they only ever target `.markdown-body`, scoped by that class alone. */
896
+ :global(.markdown-body > :first-child) {
897
+ margin-top: 0;
898
+ }
899
+ :global(.markdown-body > :last-child) {
900
+ margin-bottom: 0;
901
+ }
902
+ :global(.markdown-body p) {
903
+ margin: 0.5em 0;
904
+ }
905
+ :global(.markdown-body ul),
906
+ :global(.markdown-body ol) {
907
+ margin: 0.5em 0;
908
+ padding-left: 1.25em;
909
+ }
910
+ :global(.markdown-body ul) {
911
+ list-style: disc;
912
+ }
913
+ :global(.markdown-body ol) {
914
+ list-style: decimal;
915
+ }
916
+ :global(.markdown-body li) {
917
+ margin: 0.15em 0;
918
+ }
919
+ :global(.markdown-body li > p) {
920
+ margin: 0;
921
+ }
922
+ :global(.markdown-body h1),
923
+ :global(.markdown-body h2),
924
+ :global(.markdown-body h3) {
925
+ font-weight: 600;
926
+ margin: 0.75em 0 0.35em;
927
+ }
928
+ :global(.markdown-body h1) {
929
+ font-size: 1.15em;
930
+ }
931
+ :global(.markdown-body h2) {
932
+ font-size: 1.08em;
933
+ }
934
+ :global(.markdown-body h3) {
935
+ font-size: 1.02em;
936
+ }
937
+ :global(.markdown-body a) {
938
+ color: var(--primary);
939
+ text-decoration: underline;
940
+ text-underline-offset: 2px;
941
+ }
942
+ :global(.markdown-body code) {
943
+ background: var(--muted);
944
+ padding: 0.1em 0.35em;
945
+ border-radius: 0.3em;
946
+ font-size: 0.85em;
947
+ }
948
+ :global(.markdown-body pre) {
949
+ background: var(--muted);
950
+ padding: 0.6em 0.75em;
951
+ border-radius: 0.5em;
952
+ overflow-x: auto;
953
+ margin: 0.5em 0;
954
+ }
955
+ :global(.markdown-body pre code) {
956
+ background: none;
957
+ padding: 0;
958
+ }
959
+ :global(.markdown-body blockquote) {
960
+ border-left: 2px solid var(--border);
961
+ padding-left: 0.75em;
962
+ margin: 0.5em 0;
963
+ color: var(--muted-foreground);
964
+ }
965
+ :global(.markdown-body table) {
966
+ border-collapse: collapse;
967
+ margin: 0.5em 0;
968
+ }
969
+ :global(.markdown-body th),
970
+ :global(.markdown-body td) {
971
+ border: 1px solid var(--border);
972
+ padding: 0.25em 0.5em;
973
+ }
974
+ :global(.markdown-body strong) {
975
+ font-weight: 600;
976
+ }
977
+ </style>