@k2b/cloud 0.6.0-rc.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 (540) hide show
  1. package/README.md +98 -0
  2. package/package.json +128 -0
  3. package/public/logo.svg +1 -0
  4. package/scripts/app-favicon.test.ts +49 -0
  5. package/scripts/app-favicon.ts +51 -0
  6. package/scripts/build.ts +205 -0
  7. package/scripts/preload.ts +164 -0
  8. package/scripts/sync-recovery-smoke.ts +200 -0
  9. package/src/_internal/app-favicon.ts +2 -0
  10. package/src/_internal/app-presentation.ts +61 -0
  11. package/src/_internal/app-workspace-state.ts +24 -0
  12. package/src/_internal/bounded-json.ts +47 -0
  13. package/src/_internal/build-metadata.ts +9 -0
  14. package/src/_internal/capabilities.ts +1118 -0
  15. package/src/_internal/define-app.ts +801 -0
  16. package/src/_internal/heartbeat.ts +167 -0
  17. package/src/_internal/help-catalog.ts +241 -0
  18. package/src/_internal/help.ts +95 -0
  19. package/src/_internal/nats-connection.ts +34 -0
  20. package/src/_internal/page-responses.ts +40 -0
  21. package/src/_internal/process-sync.ts +117 -0
  22. package/src/_internal/readiness.ts +3 -0
  23. package/src/_internal/registry-validation.ts +136 -0
  24. package/src/_internal/registry-watch.ts +52 -0
  25. package/src/_internal/registry.ts +338 -0
  26. package/src/_internal/runtime-compatibility.ts +64 -0
  27. package/src/_internal/runtime-context.ts +84 -0
  28. package/src/_internal/runtime-watcher.ts +78 -0
  29. package/src/_internal/static-assets.ts +86 -0
  30. package/src/_internal/status-preserving-ssr.ts +49 -0
  31. package/src/access/PermissionEditor.tsx +486 -0
  32. package/src/access/ResourceApiKeys.tsx +271 -0
  33. package/src/access/messages.ts +121 -0
  34. package/src/access/ui.ts +16 -0
  35. package/src/account/Avatar.tsx +28 -0
  36. package/src/account/EntitySearch.tsx +363 -0
  37. package/src/account/avatar-upload-dialog.tsx +197 -0
  38. package/src/account/avatar-upload.ts +149 -0
  39. package/src/account/ui.ts +3 -0
  40. package/src/ai/admin.ts +47 -0
  41. package/src/ai/approval-routes.ts +130 -0
  42. package/src/ai/approvals.ts +139 -0
  43. package/src/ai/assistant-models.ts +61 -0
  44. package/src/ai/attachments.ts +40 -0
  45. package/src/ai/browser.ts +92 -0
  46. package/src/ai/capabilities.ts +714 -0
  47. package/src/ai/capability-execution.ts +151 -0
  48. package/src/ai/chat/blocks.render.test.tsx +1108 -0
  49. package/src/ai/chat/blocks.tsx +730 -0
  50. package/src/ai/chat/builtin-tools.tsx +428 -0
  51. package/src/ai/chat/capability-table.tsx +87 -0
  52. package/src/ai/chat/composer-adapter.ts +169 -0
  53. package/src/ai/chat/file-tools.tsx +81 -0
  54. package/src/ai/chat/message-actions.tsx +370 -0
  55. package/src/ai/chat/message-utils.ts +479 -0
  56. package/src/ai/chat/messages.ts +43 -0
  57. package/src/ai/chat/presentation.tsx +395 -0
  58. package/src/ai/chat/primitives.tsx +3 -0
  59. package/src/ai/chat/tool-disclosure.tsx +41 -0
  60. package/src/ai/chat/turn-navigator-utils.ts +28 -0
  61. package/src/ai/chat/turn-navigator.tsx +164 -0
  62. package/src/ai/chat/user-message.tsx +149 -0
  63. package/src/ai/chat/visual-tools.tsx +435 -0
  64. package/src/ai/chat/web-tools.tsx +202 -0
  65. package/src/ai/chat-task-contracts.ts +113 -0
  66. package/src/ai/chat-tasks.ts +952 -0
  67. package/src/ai/client/controller.ts +1211 -0
  68. package/src/ai/client/file-source.ts +95 -0
  69. package/src/ai/client/live-connection.ts +140 -0
  70. package/src/ai/client/projection.ts +178 -0
  71. package/src/ai/client/transport.ts +111 -0
  72. package/src/ai/compaction.ts +182 -0
  73. package/src/ai/credentials.ts +124 -0
  74. package/src/ai/default-tool-contracts.ts +108 -0
  75. package/src/ai/default-tools.ts +127 -0
  76. package/src/ai/enrich.ts +280 -0
  77. package/src/ai/executor.ts +1624 -0
  78. package/src/ai/fetch-file-tool.ts +313 -0
  79. package/src/ai/file-context.ts +92 -0
  80. package/src/ai/file-media-type.ts +28 -0
  81. package/src/ai/file-mount.ts +16 -0
  82. package/src/ai/file-tools.ts +566 -0
  83. package/src/ai/files-store.ts +390 -0
  84. package/src/ai/firecrawl-tools.ts +305 -0
  85. package/src/ai/grids-skill.ts +126 -0
  86. package/src/ai/http.ts +265 -0
  87. package/src/ai/index.ts +337 -0
  88. package/src/ai/kit-skill.ts +22 -0
  89. package/src/ai/limits.ts +5 -0
  90. package/src/ai/live-events.ts +289 -0
  91. package/src/ai/live-messages.ts +45 -0
  92. package/src/ai/live-outbox.ts +102 -0
  93. package/src/ai/live-routes.ts +412 -0
  94. package/src/ai/live.ts +2 -0
  95. package/src/ai/maintenance.ts +302 -0
  96. package/src/ai/markdown-pdf-tool.ts +95 -0
  97. package/src/ai/memories.ts +605 -0
  98. package/src/ai/memory-learning-runs.ts +222 -0
  99. package/src/ai/memory-learning.ts +619 -0
  100. package/src/ai/memory-tool.ts +120 -0
  101. package/src/ai/memory-workflow-evidence.ts +158 -0
  102. package/src/ai/migrate.ts +2082 -0
  103. package/src/ai/model-access-migrate.ts +30 -0
  104. package/src/ai/model-access.ts +191 -0
  105. package/src/ai/personal-agent.ts +6 -0
  106. package/src/ai/prefs.ts +72 -0
  107. package/src/ai/project-tool.ts +94 -0
  108. package/src/ai/projects-routes.ts +277 -0
  109. package/src/ai/projects.ts +902 -0
  110. package/src/ai/protocol.ts +279 -0
  111. package/src/ai/provider.ts +73 -0
  112. package/src/ai/public-projection.ts +25 -0
  113. package/src/ai/resource-markers.ts +31 -0
  114. package/src/ai/resource-refs.ts +70 -0
  115. package/src/ai/routes.ts +1131 -0
  116. package/src/ai/runtime-tools.ts +39 -0
  117. package/src/ai/runtime.ts +544 -0
  118. package/src/ai/settings.ts +529 -0
  119. package/src/ai/short-id.ts +45 -0
  120. package/src/ai/skill-catalog.ts +73 -0
  121. package/src/ai/skill-format.ts +145 -0
  122. package/src/ai/skill-seeds.ts +430 -0
  123. package/src/ai/skill-tool.ts +81 -0
  124. package/src/ai/skills-routes.ts +143 -0
  125. package/src/ai/skills.ts +934 -0
  126. package/src/ai/solid.ts +27 -0
  127. package/src/ai/store.ts +3113 -0
  128. package/src/ai/stream.ts +308 -0
  129. package/src/ai/structured-runs.ts +66 -0
  130. package/src/ai/structured.ts +186 -0
  131. package/src/ai/system-prompt.ts +170 -0
  132. package/src/ai/task-prompt.ts +22 -0
  133. package/src/ai/timeline.ts +148 -0
  134. package/src/ai/tool-audit.ts +177 -0
  135. package/src/ai/tool-result-budget.ts +16 -0
  136. package/src/ai/tools.ts +155 -0
  137. package/src/ai/types.ts +967 -0
  138. package/src/ai/ui.tsx +39 -0
  139. package/src/ai/usage-migrate.ts +81 -0
  140. package/src/ai/usage.ts +315 -0
  141. package/src/ai/validate.ts +54 -0
  142. package/src/ai/vision-tool.ts +79 -0
  143. package/src/api/accounts-entities.ts +174 -0
  144. package/src/api/admin-ai-projects.ts +74 -0
  145. package/src/api/admin-ai-skills.ts +76 -0
  146. package/src/api/admin-ai-usage.ts +42 -0
  147. package/src/api/admin-core-settings.ts +508 -0
  148. package/src/api/admin-identity.ts +148 -0
  149. package/src/api/admin-lifecycle.ts +206 -0
  150. package/src/api/admin-linux-identities.ts +66 -0
  151. package/src/api/announcements.ts +131 -0
  152. package/src/api/app-approval.ts +129 -0
  153. package/src/api/apps.ts +51 -0
  154. package/src/api/auth/schemas.ts +54 -0
  155. package/src/api/auth.ts +370 -0
  156. package/src/api/capabilities.ts +658 -0
  157. package/src/api/help-messages.ts +19 -0
  158. package/src/api/help.ts +78 -0
  159. package/src/api/index.ts +108 -0
  160. package/src/api/mcp.ts +744 -0
  161. package/src/api/me-rail.ts +45 -0
  162. package/src/api/me.ts +753 -0
  163. package/src/api/search/schemas.ts +79 -0
  164. package/src/api/search.bench.ts +185 -0
  165. package/src/api/search.ts +407 -0
  166. package/src/api/sync-ops.ts +79 -0
  167. package/src/api/widgets.ts +143 -0
  168. package/src/browser/CloudResourceSearch.behavior.test.tsx +74 -0
  169. package/src/browser/CloudResourceSearch.tsx +791 -0
  170. package/src/browser/app-approval-vault.ts +305 -0
  171. package/src/browser/app-approval.ts +234 -0
  172. package/src/browser/live-websocket.ts +268 -0
  173. package/src/browser/locale-preference.ts +25 -0
  174. package/src/browser/notification-target.ts +27 -0
  175. package/src/browser/notification-timeout.ts +14 -0
  176. package/src/browser/notifications.ts +181 -0
  177. package/src/browser/resource-clipboard.ts +70 -0
  178. package/src/browser/resource-picker.tsx +39 -0
  179. package/src/browser/resource-search-messages.ts +55 -0
  180. package/src/browser/resource-search.ts +27 -0
  181. package/src/capabilities/client.ts +149 -0
  182. package/src/capabilities/response.ts +39 -0
  183. package/src/capabilities/server.ts +215 -0
  184. package/src/capabilities/signals.ts +4 -0
  185. package/src/capabilities/testing.ts +12 -0
  186. package/src/capabilities/types.ts +32 -0
  187. package/src/cli/access.ts +499 -0
  188. package/src/cli/account.ts +382 -0
  189. package/src/cli/admin/account-administration.ts +45 -0
  190. package/src/cli/admin/account-categories.ts +49 -0
  191. package/src/cli/admin/ai-usage.ts +118 -0
  192. package/src/cli/admin/app-sign-in.ts +49 -0
  193. package/src/cli/admin/data.ts +343 -0
  194. package/src/cli/admin/documentation.ts +34 -0
  195. package/src/cli/admin/gateway.ts +155 -0
  196. package/src/cli/admin/index.ts +61 -0
  197. package/src/cli/admin/instance.ts +250 -0
  198. package/src/cli/admin/jobs.ts +223 -0
  199. package/src/cli/admin/legal.ts +122 -0
  200. package/src/cli/admin/linux.ts +53 -0
  201. package/src/cli/admin/logs.ts +324 -0
  202. package/src/cli/admin/metrics.ts +142 -0
  203. package/src/cli/admin/nats.ts +167 -0
  204. package/src/cli/admin/notifications.ts +516 -0
  205. package/src/cli/admin/shared.ts +113 -0
  206. package/src/cli/admin/sync.ts +188 -0
  207. package/src/cli/admin/telemetry.ts +314 -0
  208. package/src/cli/admin/webhooks.ts +222 -0
  209. package/src/cli/admin/workflows.ts +367 -0
  210. package/src/cli/apps.ts +40 -0
  211. package/src/cli/capabilities.ts +239 -0
  212. package/src/cli/commands.ts +604 -0
  213. package/src/cli/index.ts +61 -0
  214. package/src/cli/locale.ts +31 -0
  215. package/src/cli/output.ts +58 -0
  216. package/src/clients/core.ts +20 -0
  217. package/src/config/env.ts +54 -0
  218. package/src/config/index.ts +6 -0
  219. package/src/contracts/account-categories.ts +62 -0
  220. package/src/contracts/announcements.ts +121 -0
  221. package/src/contracts/app-approval.ts +197 -0
  222. package/src/contracts/app.ts +162 -0
  223. package/src/contracts/capabilities.ts +484 -0
  224. package/src/contracts/index.ts +14 -0
  225. package/src/contracts/notification-live.ts +66 -0
  226. package/src/contracts/notification-types.ts +235 -0
  227. package/src/contracts/posix.ts +69 -0
  228. package/src/contracts/profile.ts +85 -0
  229. package/src/contracts/rail-preferences.ts +53 -0
  230. package/src/contracts/registry.ts +110 -0
  231. package/src/contracts/resource-clipboard.ts +33 -0
  232. package/src/contracts/settings-types.ts +98 -0
  233. package/src/contracts/shared.ts +408 -0
  234. package/src/contracts/user-notifications.ts +101 -0
  235. package/src/contracts/widgets.ts +183 -0
  236. package/src/desktop/index.ts +704 -0
  237. package/src/desktop/solid.tsx +938 -0
  238. package/src/index.ts +58 -0
  239. package/src/server/actor.ts +41 -0
  240. package/src/server/api/index.ts +2 -0
  241. package/src/server/api/respond.ts +96 -0
  242. package/src/server/api-client.ts +54 -0
  243. package/src/server/app-context.ts +39 -0
  244. package/src/server/help.ts +187 -0
  245. package/src/server/index.ts +89 -0
  246. package/src/server/locale.ts +55 -0
  247. package/src/server/middleware/auth.ts +386 -0
  248. package/src/server/middleware/index.ts +28 -0
  249. package/src/server/middleware/invocation.ts +55 -0
  250. package/src/server/middleware/middleware.ts +65 -0
  251. package/src/server/middleware/openapi.ts +126 -0
  252. package/src/server/middleware/rate-limit.ts +125 -0
  253. package/src/server/middleware/request-logger.ts +41 -0
  254. package/src/server/middleware/route-template.ts +46 -0
  255. package/src/server/middleware/runtime.ts +22 -0
  256. package/src/server/middleware/settings.ts +55 -0
  257. package/src/server/middleware/validator.ts +49 -0
  258. package/src/server/middleware/workload.ts +22 -0
  259. package/src/server/ratelimit.ts +151 -0
  260. package/src/server/request-cookies.ts +18 -0
  261. package/src/server/services/access.ts +644 -0
  262. package/src/server/services/freeipa/client.ts +137 -0
  263. package/src/server/services/freeipa/index.ts +9 -0
  264. package/src/server/services/freeipa/session.ts +123 -0
  265. package/src/server/services/freeipa/test-certificates.ts +44 -0
  266. package/src/server/services/freeipa/tls.ts +44 -0
  267. package/src/server/services/freeipa/transport.ts +116 -0
  268. package/src/server/services/freeipa/util.ts +59 -0
  269. package/src/server/services/geo.ts +198 -0
  270. package/src/server/services/index.ts +48 -0
  271. package/src/server/services/pagination.ts +22 -0
  272. package/src/server/services/services.ts +13 -0
  273. package/src/server/time.ts +33 -0
  274. package/src/services/account-category-policy.ts +51 -0
  275. package/src/services/account-lifecycle/audit.ts +41 -0
  276. package/src/services/account-lifecycle/index.ts +940 -0
  277. package/src/services/account-lifecycle/ipa-backfill.ts +152 -0
  278. package/src/services/account-lifecycle/notification-sender.ts +13 -0
  279. package/src/services/account-lifecycle/scheduler.ts +506 -0
  280. package/src/services/account-model.ts +22 -0
  281. package/src/services/accounts/app.ts +1671 -0
  282. package/src/services/accounts/authz.ts +44 -0
  283. package/src/services/accounts/avatar.ts +120 -0
  284. package/src/services/accounts/base-group.ts +11 -0
  285. package/src/services/accounts/base-user.ts +46 -0
  286. package/src/services/accounts/duplicate-emails.ts +82 -0
  287. package/src/services/accounts/entities.ts +622 -0
  288. package/src/services/accounts/group-sql.ts +106 -0
  289. package/src/services/accounts/groups.ts +272 -0
  290. package/src/services/accounts/index.ts +14 -0
  291. package/src/services/accounts/ipa-data.ts +66 -0
  292. package/src/services/accounts/lifecycle.ts +2 -0
  293. package/src/services/accounts/local-groups.ts +499 -0
  294. package/src/services/accounts/model.ts +129 -0
  295. package/src/services/accounts/notification-sender.ts +31 -0
  296. package/src/services/accounts/posix.ts +386 -0
  297. package/src/services/accounts/request-policy.ts +11 -0
  298. package/src/services/accounts/switching.ts +126 -0
  299. package/src/services/accounts/users.ts +745 -0
  300. package/src/services/announcements/index.ts +224 -0
  301. package/src/services/app-approval.ts +498 -0
  302. package/src/services/audit/index.ts +438 -0
  303. package/src/services/auth-flows/index.ts +15 -0
  304. package/src/services/auth-flows/ipa.ts +197 -0
  305. package/src/services/auth-flows/magic-link.ts +185 -0
  306. package/src/services/auth-flows/notification-sender.ts +10 -0
  307. package/src/services/auth-flows/password-reset.ts +268 -0
  308. package/src/services/auth-flows/proxy-return.ts +49 -0
  309. package/src/services/document-extraction.ts +148 -0
  310. package/src/services/freeipa-config.ts +179 -0
  311. package/src/services/gateway.ts +186 -0
  312. package/src/services/identity/constants.ts +18 -0
  313. package/src/services/identity/index.ts +52 -0
  314. package/src/services/identity/invocation-actor.ts +120 -0
  315. package/src/services/identity/invocation-authority.ts +53 -0
  316. package/src/services/identity/invocation-operations.ts +15 -0
  317. package/src/services/identity/invocation-token.ts +276 -0
  318. package/src/services/identity/key-config.ts +41 -0
  319. package/src/services/identity/key-ring.ts +530 -0
  320. package/src/services/identity/metrics.ts +29 -0
  321. package/src/services/identity/routes.ts +18 -0
  322. package/src/services/identity/runtime-config.ts +88 -0
  323. package/src/services/identity/session-token.ts +152 -0
  324. package/src/services/identity/signer-refresh.ts +44 -0
  325. package/src/services/identity/workload-auth.ts +64 -0
  326. package/src/services/index.ts +187 -0
  327. package/src/services/ipa/auth.ts +216 -0
  328. package/src/services/ipa/connection.ts +57 -0
  329. package/src/services/ipa/effective-groups.ts +70 -0
  330. package/src/services/ipa/groups.ts +722 -0
  331. package/src/services/ipa/guard.ts +17 -0
  332. package/src/services/ipa/index.ts +17 -0
  333. package/src/services/ipa/posix.ts +19 -0
  334. package/src/services/ipa/profile.ts +132 -0
  335. package/src/services/ipa/runtime.ts +28 -0
  336. package/src/services/ipa/search.ts +154 -0
  337. package/src/services/ipa/service-account.ts +15 -0
  338. package/src/services/ipa/sync-planning.ts +100 -0
  339. package/src/services/ipa/sync.ts +862 -0
  340. package/src/services/ipa/users.ts +806 -0
  341. package/src/services/legal-consent.ts +76 -0
  342. package/src/services/logging/index.ts +477 -0
  343. package/src/services/logging/redaction.ts +20 -0
  344. package/src/services/logging/trace.ts +1231 -0
  345. package/src/services/mandates/index.ts +855 -0
  346. package/src/services/mandates/policy.ts +85 -0
  347. package/src/services/network-security.ts +31 -0
  348. package/src/services/notifications/batches.ts +722 -0
  349. package/src/services/notifications/browser.ts +252 -0
  350. package/src/services/notifications/catalog.ts +113 -0
  351. package/src/services/notifications/channels.ts +104 -0
  352. package/src/services/notifications/dispatcher.ts +139 -0
  353. package/src/services/notifications/email.ts +118 -0
  354. package/src/services/notifications/index.ts +633 -0
  355. package/src/services/notifications/live.ts +50 -0
  356. package/src/services/notifications/observability.ts +407 -0
  357. package/src/services/notifications/platform.ts +421 -0
  358. package/src/services/notifications/runtime.ts +120 -0
  359. package/src/services/notifications/user.ts +248 -0
  360. package/src/services/notifications/web-push-transport.ts +135 -0
  361. package/src/services/oauth-tokens.ts +209 -0
  362. package/src/services/outbox.ts +187 -0
  363. package/src/services/pdf/gotenberg.ts +290 -0
  364. package/src/services/pdf/index.ts +43 -0
  365. package/src/services/pdf/markdown.ts +209 -0
  366. package/src/services/pdf/template-preview.ts +122 -0
  367. package/src/services/postgres.ts +76 -0
  368. package/src/services/providers/index.ts +27 -0
  369. package/src/services/providers/local/auth.ts +51 -0
  370. package/src/services/providers/local/index.ts +4 -0
  371. package/src/services/providers/local/users.ts +263 -0
  372. package/src/services/rail-preferences.ts +27 -0
  373. package/src/services/runtime-lifecycle.ts +191 -0
  374. package/src/services/secrets.ts +10 -0
  375. package/src/services/service-account-credentials.ts +891 -0
  376. package/src/services/service-accounts.ts +184 -0
  377. package/src/services/session/index.ts +236 -0
  378. package/src/services/session/test-fixture.ts +18 -0
  379. package/src/services/session/user.ts +144 -0
  380. package/src/services/settings/api.ts +61 -0
  381. package/src/services/settings/app.ts +86 -0
  382. package/src/services/settings/core-settings.ts +796 -0
  383. package/src/services/settings/crypto.ts +69 -0
  384. package/src/services/settings/defaults.ts +374 -0
  385. package/src/services/settings/index.ts +215 -0
  386. package/src/services/settings/namespace.ts +9 -0
  387. package/src/services/settings/snapshot.ts +49 -0
  388. package/src/services/settings/store.ts +241 -0
  389. package/src/services/settings/templates.ts +4 -0
  390. package/src/services/sync-ops.ts +403 -0
  391. package/src/services/topic-cursor.ts +22 -0
  392. package/src/services/weather/forecast.ts +323 -0
  393. package/src/services/weather/geo.ts +119 -0
  394. package/src/services/weather/index.ts +67 -0
  395. package/src/services/weather/location.ts +24 -0
  396. package/src/services/weather/locations.ts +133 -0
  397. package/src/services/weather/migrate.ts +45 -0
  398. package/src/services/weather/settings.ts +66 -0
  399. package/src/services/weather/short-id.ts +57 -0
  400. package/src/services/weather/types.ts +61 -0
  401. package/src/services/weather/ui.ts +50 -0
  402. package/src/services/webauthn.ts +385 -0
  403. package/src/shared/account-action-notice.ts +70 -0
  404. package/src/shared/account-display.ts +15 -0
  405. package/src/shared/account-session.ts +15 -0
  406. package/src/shared/ai-platform-prompt.ts +134 -0
  407. package/src/shared/ai-usage.ts +49 -0
  408. package/src/shared/app-presentation.ts +48 -0
  409. package/src/shared/app-url.ts +12 -0
  410. package/src/shared/branding.ts +14 -0
  411. package/src/shared/capability-messages.ts +70 -0
  412. package/src/shared/email-html.ts +54 -0
  413. package/src/shared/format.ts +91 -0
  414. package/src/shared/help.ts +68 -0
  415. package/src/shared/icons.ts +400 -0
  416. package/src/shared/index.ts +54 -0
  417. package/src/shared/locale.ts +48 -0
  418. package/src/shared/login-method.ts +44 -0
  419. package/src/shared/markdown/client.ts +130 -0
  420. package/src/shared/markdown/extensions/code.ts +105 -0
  421. package/src/shared/markdown/extensions/guided-help.ts +100 -0
  422. package/src/shared/markdown/extensions/images.ts +79 -0
  423. package/src/shared/markdown/extensions/info-blocks.ts +97 -0
  424. package/src/shared/markdown/extensions/katex.ts +120 -0
  425. package/src/shared/markdown/extensions/links.ts +43 -0
  426. package/src/shared/markdown/extensions/mark.ts +48 -0
  427. package/src/shared/markdown/extensions/sub-sup.ts +60 -0
  428. package/src/shared/markdown/extensions/tables.ts +109 -0
  429. package/src/shared/markdown/extensions/task-list.ts +53 -0
  430. package/src/shared/markdown/formula.ts +1187 -0
  431. package/src/shared/markdown/index.ts +193 -0
  432. package/src/shared/markdown/shared.ts +36 -0
  433. package/src/shared/mock-cover.ts +130 -0
  434. package/src/shared/network-address.ts +106 -0
  435. package/src/shared/redirect.ts +56 -0
  436. package/src/shared/template-rendering.ts +140 -0
  437. package/src/shared/theme.ts +68 -0
  438. package/src/shared/time.ts +13 -0
  439. package/src/ssr/AdminLayout.tsx +53 -0
  440. package/src/ssr/AdminSidebar.tsx +50 -0
  441. package/src/ssr/AppLaunchpad.island.tsx +236 -0
  442. package/src/ssr/AppWorkspaceController.island.tsx +21 -0
  443. package/src/ssr/BrowserNotifications.island.tsx +135 -0
  444. package/src/ssr/GlobalAnnouncements.island.tsx +142 -0
  445. package/src/ssr/GlobalSearchDialog.behavior.test.tsx +96 -0
  446. package/src/ssr/GlobalSearchDialog.tsx +35 -0
  447. package/src/ssr/GlobalSearchHelpDialog.tsx +104 -0
  448. package/src/ssr/GlobalSearchTrigger.island.tsx +55 -0
  449. package/src/ssr/HotkeysHelpRail.island.tsx +55 -0
  450. package/src/ssr/Layout.render.test.tsx +148 -0
  451. package/src/ssr/Layout.tsx +292 -0
  452. package/src/ssr/LayoutBreadcrumbs.island.tsx +44 -0
  453. package/src/ssr/LayoutFooter.tsx +22 -0
  454. package/src/ssr/LayoutHeader.tsx +81 -0
  455. package/src/ssr/LayoutHelp.tsx +869 -0
  456. package/src/ssr/LayoutPreferences.island.tsx +36 -0
  457. package/src/ssr/LayoutRail.tsx +58 -0
  458. package/src/ssr/MinimalLayout.tsx +46 -0
  459. package/src/ssr/PageError.tsx +29 -0
  460. package/src/ssr/ProfilePreferences.island.tsx +92 -0
  461. package/src/ssr/RailApps.island.tsx +61 -0
  462. package/src/ssr/RailEditor.tsx +293 -0
  463. package/src/ssr/RegisteredHelpDocuments.island.tsx +3 -0
  464. package/src/ssr/TimezoneCookie.island.tsx +23 -0
  465. package/src/ssr/admin-active-link.ts +47 -0
  466. package/src/ssr/admin-navigation.ts +78 -0
  467. package/src/ssr/app-appearance.ts +23 -0
  468. package/src/ssr/app-navigation.ts +41 -0
  469. package/src/ssr/help-messages.ts +104 -0
  470. package/src/ssr/index.ts +14 -0
  471. package/src/ssr/islands/SearchBar.island.tsx +80 -0
  472. package/src/ssr/islands/index.ts +16 -0
  473. package/src/ssr/layout-context.ts +17 -0
  474. package/src/ssr/layout-help-markdown.ts +9 -0
  475. package/src/ssr/layout-help-navigation.ts +24 -0
  476. package/src/ssr/layout-help-url.ts +5 -0
  477. package/src/ssr/layout-runtime.ts +22 -0
  478. package/src/ssr/page-error-messages.ts +25 -0
  479. package/src/ssr/platform-messages.ts +135 -0
  480. package/src/ssr/preference-controller.ts +44 -0
  481. package/src/ssr/profile-preferences-messages.ts +29 -0
  482. package/src/ssr/rail-context.ts +21 -0
  483. package/src/ssr/rail-messages.ts +66 -0
  484. package/src/ssr/rail-navigation.ts +60 -0
  485. package/src/ssr/runtime.ts +31 -0
  486. package/src/ssr/url-filter.ts +127 -0
  487. package/src/styles/base-popover.css +36 -0
  488. package/src/styles/effects.css +851 -0
  489. package/src/styles/global.css +268 -0
  490. package/src/styles/input.css +58 -0
  491. package/src/styles/tokens.css +230 -0
  492. package/src/styles/utilities-buttons.css +405 -0
  493. package/src/styles/utilities-data.css +33 -0
  494. package/src/styles/utilities-feedback.css +232 -0
  495. package/src/styles/utilities-layout.css +603 -0
  496. package/src/styles/utilities-markdown-editor.css +525 -0
  497. package/src/styles/utilities-markdown-table.css +234 -0
  498. package/src/styles/utilities-navigation.css +1054 -0
  499. package/src/types/ambient.d.ts +24 -0
  500. package/src/workflows/ai/index.ts +17 -0
  501. package/src/workflows/ai/runtime.ts +286 -0
  502. package/src/workflows/ai/store.ts +314 -0
  503. package/src/workflows/ai/types.ts +111 -0
  504. package/src/workflows/ai-actions.ts +269 -0
  505. package/src/workflows/authoring.ts +88 -0
  506. package/src/workflows/builtins.ts +193 -0
  507. package/src/workflows/contracts.ts +280 -0
  508. package/src/workflows/definition.ts +334 -0
  509. package/src/workflows/editor.ts +113 -0
  510. package/src/workflows/index.ts +71 -0
  511. package/src/workflows/language/binder.ts +45 -0
  512. package/src/workflows/language/canonical.ts +29 -0
  513. package/src/workflows/language/compiler.ts +396 -0
  514. package/src/workflows/language/expressions.ts +34 -0
  515. package/src/workflows/language/index.ts +20 -0
  516. package/src/workflows/language/references.ts +55 -0
  517. package/src/workflows/language/schema.ts +134 -0
  518. package/src/workflows/language/strict-yaml.ts +131 -0
  519. package/src/workflows/manifest.ts +24 -0
  520. package/src/workflows/module.ts +12 -0
  521. package/src/workflows/runtime/coordinator.ts +115 -0
  522. package/src/workflows/runtime/executor.ts +1003 -0
  523. package/src/workflows/runtime/index.ts +60 -0
  524. package/src/workflows/runtime/instant.ts +8 -0
  525. package/src/workflows/runtime/ports.ts +213 -0
  526. package/src/workflows/runtime/schedule.ts +203 -0
  527. package/src/workflows/runtime/trigger.ts +37 -0
  528. package/src/workflows/runtime/wake.ts +13 -0
  529. package/src/workflows/store/actions.ts +438 -0
  530. package/src/workflows/store/budget.ts +145 -0
  531. package/src/workflows/store/definitions.ts +345 -0
  532. package/src/workflows/store/events.ts +383 -0
  533. package/src/workflows/store/index.ts +107 -0
  534. package/src/workflows/store/observability.ts +594 -0
  535. package/src/workflows/store/runs.ts +975 -0
  536. package/src/workflows/store/transaction.ts +23 -0
  537. package/src/workflows/store/worker.ts +439 -0
  538. package/src/workflows/testing/conformance.ts +72 -0
  539. package/src/workflows/testing/index.ts +11 -0
  540. package/src/workflows/testing/process-fixtures.ts +275 -0
@@ -0,0 +1,1108 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { mkdtempSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { resolve } from "node:path";
5
+ import { createConfig } from "@k2b/ssr";
6
+ import { type Accessor, createComponent, createRoot, createSignal } from "solid-js";
7
+ import { renderToString } from "solid-js/web";
8
+ import type { AiActiveTurn } from "../client/projection";
9
+ import type { AiTurnBlock } from "../protocol";
10
+ import type { AiAssistantTimelineItem } from "../timeline";
11
+ import type { AiStoredMessage } from "../types";
12
+
13
+ const root = mkdtempSync(resolve(tmpdir(), "cloud-capability-block-tests-"));
14
+ const { plugin } = createConfig({ dev: true, rootDir: root });
15
+ Bun.plugin(plugin());
16
+ process.once("exit", () => rmSync(root, { recursive: true, force: true }));
17
+
18
+ const { AiTurnBlockList, AiTurnBlockView } = await import("./blocks");
19
+ const { AiChatActionsProvider } = await import("./message-actions");
20
+ const { AiAssistantContent, createAiChatTimeline, partitionCompletedAssistantBlocks } = await import("./presentation");
21
+ const { createAiToolDisclosureState } = await import("./tool-disclosure");
22
+ const { CloudSurveyBlock, CloudTextEditorBlock } = await import("./visual-tools");
23
+
24
+ const hasOpenDetails = (html: string): boolean => /<details\b[^>]*\sopen(?:=""|(?=[\s>]))/.test(html);
25
+ const hasOpenDetailsContaining = (html: string, text: string): boolean => {
26
+ const textIndex = html.indexOf(text);
27
+ const detailsIndex = html.lastIndexOf("<details", textIndex);
28
+ const tagEnd = html.indexOf(">", detailsIndex);
29
+ return detailsIndex >= 0 && tagEnd > detailsIndex && /\sopen(?:=""|(?=[\s>]))/.test(html.slice(detailsIndex, tagEnd + 1));
30
+ };
31
+
32
+ const block = (status: "running" | "awaiting_approval" | "completed" | "failed"): AiTurnBlock => ({
33
+ id: "tool-call-1",
34
+ kind: "tool",
35
+ callId: "call-1",
36
+ name: "contacts__query__list",
37
+ args: { limit: 10 },
38
+ status,
39
+ result: status === "running" || status === "awaiting_approval" ? undefined : { data: [] },
40
+ isError: status === "failed",
41
+ approval:
42
+ status === "awaiting_approval"
43
+ ? { message: "Contacts: List contacts\nReview the validated arguments below before running this Action.", allowAlways: false }
44
+ : undefined,
45
+ presentation: {
46
+ kind: "capability",
47
+ appId: "contacts",
48
+ appName: "Contacts",
49
+ appIcon: "ti ti-address-book",
50
+ appAccent: "#0f766e",
51
+ title: "List contacts",
52
+ capabilityKind: "query",
53
+ },
54
+ });
55
+
56
+ describe("capability tool presentation", () => {
57
+ test("table preview handles empty, capped, invalid and failed results honestly", () => {
58
+ const render = (result: unknown, failed = false) => {
59
+ const completed = block(failed ? "failed" : "completed");
60
+ if (completed.kind !== "tool") throw new Error("tool expected");
61
+ completed.result = result;
62
+ return renderToString(() => createComponent(AiTurnBlockView, { block: completed, turnId: "turn-1" }));
63
+ };
64
+ const presentation = { kind: "table", rowsPath: [], columns: [{ path: ["name"], label: "Name" }] };
65
+ expect(render({ data: [], presentation })).toContain("No matching rows");
66
+ const many = Array.from({ length: 101 }, (_, index) => ({ name: `item-${index}` }));
67
+ expect(render({ data: many, presentation })).toContain("first 100 returned rows");
68
+ expect(render({ data: [], presentation: { ...presentation, kind: "future" } })).not.toContain("<table");
69
+ expect(render({ data: {}, presentation })).not.toContain("<table");
70
+ expect(render({ data: [], presentation }, true)).not.toContain("<table");
71
+ });
72
+ test("renders metadata from actual values, including exact decimals, safe links and partial results", () => {
73
+ const completed = block("completed");
74
+ if (completed.kind !== "tool") throw new Error("tool expected");
75
+ completed.result = {
76
+ data: { rows: [{ values: { amount: "12345678901234567890.123400" }, links: [{ rel: "open", href: "/app/grids/Base01" }] }] },
77
+ presentation: {
78
+ kind: "table",
79
+ rowsPath: ["rows"],
80
+ columns: [{ path: ["values", "amount"], label: "Amount", format: "number" }],
81
+ rowLinksPath: ["links"],
82
+ },
83
+ page: { hasMore: true },
84
+ summary: "Query preview",
85
+ links: [{ rel: "open", href: "/app/grids/Base01/query?q=test", title: "Open query editor" }],
86
+ };
87
+ const html = renderToString(() => createComponent(AiTurnBlockView, { block: completed, turnId: "turn-1" }));
88
+ expect(html).toContain("12345678901234567890.123400");
89
+ expect(html).toContain("<table");
90
+ expect(html).toContain("/app/grids/Base01");
91
+ expect(html).toContain("More rows");
92
+ expect(html).toMatch(/<a(?=[^>]*href="\/app\/grids\/Base01")(?=[^>]*target="_blank")[^>]*>/);
93
+ expect(html).toMatch(/<a(?=[^>]*href="\/app\/grids\/Base01\/query\?q=test")(?=[^>]*target="_blank")[^>]*>/);
94
+ expect(html).toContain('rel="noopener noreferrer"');
95
+ });
96
+ test("renders compact capability rows with only the capability title", () => {
97
+ for (const status of ["running", "completed", "failed"] as const) {
98
+ const html = renderToString(() => createComponent(AiTurnBlockView, { block: block(status), turnId: "turn-1" }));
99
+ expect(html).toContain("List contacts");
100
+ expect(html).not.toContain("Contacts: List contacts");
101
+ expect(html).toContain("ti-address-book");
102
+ if (status !== "failed") expect(html).not.toContain("<small>");
103
+ expect(html).toContain("--k2b-chat-activity-accent:#0f766e");
104
+ }
105
+ });
106
+
107
+ test("renders structured tool input and responses as full-width data previews", () => {
108
+ const completed = block("completed");
109
+ if (completed.kind !== "tool") throw new Error("tool block missing");
110
+ completed.args = {
111
+ mailboxId: "5guDsC",
112
+ read: true,
113
+ target: { conversationId: "nTf34n", sourceFolderId: "dScSu4" },
114
+ fourth: 4,
115
+ fifth: 5,
116
+ sixth: 6,
117
+ seventh: 7,
118
+ eighth: 8,
119
+ ninth: 9,
120
+ };
121
+ completed.result = {
122
+ data: { conversationId: "nTf34n", commands: [{ state: "queued" }] },
123
+ refs: [],
124
+ links: [],
125
+ };
126
+
127
+ const html = renderToString(() => createComponent(AiTurnBlockView, { block: completed, turnId: "turn-1" }));
128
+
129
+ expect(html.match(/class="k2b-content-structured-data w-full"/g)?.length).toBe(2);
130
+ expect(html).toContain('data-body-inset="false"');
131
+ expect(html).toContain('class="flex w-full min-w-0 flex-col gap-2"');
132
+ expect(html).not.toContain("ml-6");
133
+ expect(html).not.toContain("max-w-xl");
134
+ expect(html).toContain('<p class="mb-1 text-[10px] font-medium uppercase tracking-wide text-dimmed">Input</p>');
135
+ expect(html).toContain('<p class="mb-1 text-[10px] font-medium uppercase tracking-wide text-dimmed">Response</p>');
136
+ expect(html).not.toContain("k2b-content-structured-data__title");
137
+ expect(html).toContain("1 more row hidden");
138
+ expect(html).toContain("View raw");
139
+ expect(html).toContain("mailboxId");
140
+ expect(html).toContain("commands");
141
+ });
142
+
143
+ test.each([
144
+ {
145
+ name: "web_search",
146
+ args: { query: "Cloud platform" },
147
+ result: [{ title: "Cloud", url: "https://cloud.example/docs" }],
148
+ content: "cloud.example",
149
+ bodyClass: "max-h-56 w-full min-w-0",
150
+ },
151
+ {
152
+ name: "web_extract",
153
+ args: { url: "https://cloud.example/docs" },
154
+ result: { title: "Cloud docs", url: "https://cloud.example/docs", description: "Application platform." },
155
+ content: "Application platform.",
156
+ bodyClass: "flex w-full min-w-0",
157
+ },
158
+ ])("renders completed $name results full-width without an activity inset", ({ name, args, result, content, bodyClass }) => {
159
+ const completed: AiTurnBlock = {
160
+ id: `${name}-call`,
161
+ kind: "tool",
162
+ callId: `${name}-1`,
163
+ name,
164
+ args,
165
+ status: "completed",
166
+ result,
167
+ };
168
+
169
+ const html = renderToString(() => createComponent(AiTurnBlockView, { block: completed, turnId: "turn-1" }));
170
+
171
+ expect(html).toContain('data-body-inset="false"');
172
+ expect(html).toContain(bodyClass);
173
+ expect(html).toContain(content);
174
+ expect(html).not.toContain("max-w-xl");
175
+ expect(hasOpenDetails(html)).toBe(true);
176
+ });
177
+
178
+ test("renders internal discovery results as readable tool lists instead of JSON", () => {
179
+ const completed: AiTurnBlock = {
180
+ id: "search-tools-call",
181
+ kind: "tool",
182
+ callId: "search-tools-1",
183
+ name: "search_tools",
184
+ args: { query: "tag mail" },
185
+ status: "completed",
186
+ result: {
187
+ tools: [
188
+ {
189
+ name: "mail__action__conversation_dot_tag_dot_change",
190
+ title: "Change conversation tags",
191
+ description: "Add or remove mailbox tags.",
192
+ kind: "action",
193
+ appId: "mail",
194
+ },
195
+ ],
196
+ },
197
+ };
198
+
199
+ const html = renderToString(() => createComponent(AiTurnBlockView, { block: completed, turnId: "turn-1" }));
200
+
201
+ expect(html).toContain("Search tools: tag mail");
202
+ expect(html).toContain("Change conversation tags");
203
+ expect(html).toContain("Add or remove mailbox tags.");
204
+ expect(html).toContain(">mail</span>");
205
+ expect(html).toContain(">·</span>");
206
+ expect(html).toContain("min-w-0 flex-1 truncate text-dimmed");
207
+ expect(html).not.toContain("bg-zinc-100/70");
208
+ expect(html).not.toContain(">Input</p>");
209
+ expect(html).not.toContain(">Response</p>");
210
+ expect(html).not.toContain("k2b-content-structured-data");
211
+ expect(hasOpenDetails(html)).toBe(false);
212
+ });
213
+
214
+ test("renders a loaded Skill with only its name and description", () => {
215
+ const completed: AiTurnBlock = {
216
+ id: "load-skill-call",
217
+ kind: "tool",
218
+ callId: "load-skill-1",
219
+ name: "load_skill",
220
+ args: { name: "skill-creator" },
221
+ status: "completed",
222
+ result: {
223
+ name: "skill-creator",
224
+ description: "Create or update a reusable Skill.",
225
+ revision: 4,
226
+ instructions: "Secret renderer noise",
227
+ mount: "/skills/skill-creator",
228
+ files: ["/skills/skill-creator/SKILL.md"],
229
+ },
230
+ };
231
+
232
+ const html = renderToString(() => createComponent(AiTurnBlockView, { block: completed, turnId: "turn-1" }));
233
+
234
+ expect(html).toContain("Loaded skill skill-creator");
235
+ expect(html).toContain("Create or update a reusable Skill.");
236
+ expect(html).toContain("ti-sparkles");
237
+ expect(html).not.toContain("Secret renderer noise");
238
+ expect(html).not.toContain("/skills/skill-creator");
239
+ expect(html).not.toContain(">Input</p>");
240
+ expect(html).not.toContain(">Response</p>");
241
+ expect(html).not.toContain("k2b-content-structured-data");
242
+ expect(hasOpenDetails(html)).toBe(false);
243
+ });
244
+
245
+ test("uses provider-authored capability summaries and semantic links", () => {
246
+ const completed = block("completed");
247
+ if (completed.kind !== "tool") throw new Error("tool block missing");
248
+ completed.result = {
249
+ data: { conversationId: "nTf34n", tags: ["#foo"] },
250
+ summary: "Tagged mail with #foo",
251
+ refs: [{ type: "mail.conversation", id: "nTf34n" }],
252
+ links: [{ rel: "open", href: "/app/mail/5guDsC?conversation=nTf34n" }],
253
+ };
254
+
255
+ const html = renderToString(() => createComponent(AiTurnBlockView, { block: completed, turnId: "turn-1" }));
256
+
257
+ expect(html).toContain("Tagged mail with #foo");
258
+ expect(html).not.toContain(">List contacts</strong>");
259
+ expect(html).not.toContain("mail.conversation:nTf34n");
260
+ expect(html).toContain('href="/app/mail/5guDsC?conversation=nTf34n"');
261
+ expect(html).toMatch(/class="k2b-button ai-chat-result-link\s*"/);
262
+ expect(html).not.toContain(">Input</p>");
263
+ expect(html).not.toContain(">Response</p>");
264
+ expect(html).not.toContain("k2b-content-structured-data");
265
+ expect(hasOpenDetails(html)).toBe(false);
266
+ });
267
+
268
+ test("shows only the canonical capability error in a compact row", () => {
269
+ const failed = block("failed");
270
+ if (failed.kind !== "tool") throw new Error("tool block missing");
271
+ failed.result = "VALIDATION_FAILED: Capability input did not match the registered schema Input issues: mailboxId: Must be a stable ID";
272
+
273
+ const html = renderToString(() => createComponent(AiTurnBlockView, { block: failed, turnId: "turn-1" }));
274
+
275
+ expect(html).toContain("List contacts failed");
276
+ expect(html).toContain("mailboxId: Must be a stable ID");
277
+ expect(html).toContain('data-tone="danger"');
278
+ expect(html).not.toContain(">Input</p>");
279
+ expect(html).not.toContain(">Response</p>");
280
+ expect(hasOpenDetails(html)).toBe(false);
281
+ });
282
+
283
+ test("does not turn malformed historical capability links into navigation", () => {
284
+ const completed = block("completed");
285
+ if (completed.kind !== "tool") throw new Error("tool block missing");
286
+ completed.result = {
287
+ data: { conversationId: "nTf34n" },
288
+ summary: "Loaded conversation",
289
+ links: [{ rel: "open", href: "//example.test/escape", title: "Open" }],
290
+ };
291
+
292
+ const html = renderToString(() => createComponent(AiTurnBlockView, { block: completed, turnId: "turn-1" }));
293
+
294
+ expect(html).toContain("Loaded conversation");
295
+ expect(html).not.toContain("example.test");
296
+ });
297
+
298
+ test.each([
299
+ ["list_apps", {}, { apps: { contacts: "People and address books" } }, "People and address books"],
300
+ [
301
+ "load_tools",
302
+ { names: ["mail.conversation.activity.list"] },
303
+ {
304
+ loaded: ["mail.conversation.activity.list"],
305
+ alreadyLoaded: [],
306
+ missing: [],
307
+ evicted: [],
308
+ titles: { "mail.conversation.activity.list": "List mail activity" },
309
+ },
310
+ "List mail activity",
311
+ ],
312
+ [
313
+ "search_help",
314
+ { query: "mail tags" },
315
+ { documents: [{ appId: "mail", appName: "Mail", documentId: "tags", title: "Organize with tags" }] },
316
+ "Organize with tags",
317
+ ],
318
+ [
319
+ "read_help",
320
+ { appId: "mail", documentId: "tags" },
321
+ { document: { appId: "mail", appName: "Mail", documentId: "tags", title: "Organize with tags", markdown: "# Tags" } },
322
+ "Read help: Organize with tags",
323
+ ],
324
+ [
325
+ "search_project",
326
+ { query: "invoice" },
327
+ { items: [{ kind: "knowledge", id: "abc123", title: "Invoice policy" }], truncated: false },
328
+ "Invoice policy",
329
+ ],
330
+ [
331
+ "read_project_knowledge",
332
+ { id: "abc123" },
333
+ { title: "Invoice policy", content: "Policy text" },
334
+ "Read Project knowledge: Invoice policy",
335
+ ],
336
+ [
337
+ "list_files",
338
+ { path: "/" },
339
+ { files: [{ path: "/report.pdf", size: 2048, mediaType: "application/pdf", origin: "assistant" }], truncated: false },
340
+ "report.pdf",
341
+ ],
342
+ [
343
+ "read_file",
344
+ { path: "/notes.md", offset: 0, length: 100 },
345
+ {
346
+ path: "/notes.md",
347
+ mediaType: "text/markdown",
348
+ representation: "text",
349
+ content: "hidden",
350
+ offset: 0,
351
+ nextOffset: 42,
352
+ eof: true,
353
+ truncated: false,
354
+ },
355
+ "Bytes 0–42 · complete",
356
+ ],
357
+ [
358
+ "write_file",
359
+ { path: "/notes.md", content: "hidden" },
360
+ { path: "/notes.md", size: 42, mediaType: "text/markdown" },
361
+ "Wrote file: notes.md",
362
+ ],
363
+ ["calculate", { kind: "math", expression: "21 * 2" }, { result: "42" }, "21 * 2"],
364
+ [
365
+ "view_image",
366
+ { path: "/chart.png" },
367
+ { path: "/chart.png", mediaType: "image/png", description: "A rising line chart." },
368
+ "A rising line chart.",
369
+ ],
370
+ [
371
+ "markdown_to_pdf",
372
+ { path: "/report.md" },
373
+ { sourcePath: "/report.md", path: "/report.pdf", size: 4096, mediaType: "application/pdf" },
374
+ "Created PDF: report.pdf",
375
+ ],
376
+ [
377
+ "read_cloud_resource",
378
+ { type: "contacts.contact", id: "abc123" },
379
+ { data: { id: "abc123" }, summary: "Loaded contact Ada" },
380
+ "Loaded contact Ada",
381
+ ],
382
+ ] as const)("renders %s with its readable built-in presentation", (name, args, result, expected) => {
383
+ const completed: AiTurnBlock = {
384
+ id: `${name}-call`,
385
+ kind: "tool",
386
+ callId: `${name}-1`,
387
+ name,
388
+ args,
389
+ status: "completed",
390
+ result,
391
+ };
392
+
393
+ const html = renderToString(() => createComponent(AiTurnBlockView, { block: completed, turnId: "turn-1" }));
394
+
395
+ expect(html).toContain(expected);
396
+ expect(html).not.toContain(">Input</p>");
397
+ expect(html).not.toContain(">Response</p>");
398
+ expect(html).not.toContain("k2b-content-structured-data");
399
+ expect(hasOpenDetails(html)).toBe(name === "view_image");
400
+ if (name === "load_tools") expect(html).not.toContain("mail.conversation.activity.list");
401
+ });
402
+
403
+ test("renders persisted local Bash calls without execution controls", () => {
404
+ const completed: AiTurnBlock = {
405
+ id: "bash-call-1",
406
+ kind: "tool",
407
+ callId: "bash-1",
408
+ name: "local_bash",
409
+ args: { command: "git status --short" },
410
+ status: "completed",
411
+ result: { status: "completed", exitCode: 0, stdout: "", stderr: "", truncated: false },
412
+ isError: false,
413
+ };
414
+ const pending: AiTurnBlock = { ...completed, status: "awaiting_client", result: undefined };
415
+
416
+ const completedHtml = renderToString(() => createComponent(AiTurnBlockView, { block: completed, turnId: "turn-1" }));
417
+ const pendingHtml = renderToString(() => createComponent(AiTurnBlockView, { block: pending, turnId: "turn-1" }));
418
+
419
+ expect(completedHtml).toContain("Local Bash");
420
+ expect(completedHtml).toContain("ti-terminal-2");
421
+ expect(completedHtml).toContain("git status --short");
422
+ expect(completedHtml).not.toContain("<small>");
423
+ expect(hasOpenDetails(completedHtml)).toBe(false);
424
+ expect(pendingHtml).toContain("Local Bash");
425
+ expect(pendingHtml).toContain("ti-terminal-2");
426
+ expect(pendingHtml).not.toContain("Approve");
427
+ expect(pendingHtml).not.toContain("Run");
428
+ });
429
+
430
+ test("uses semantic built-in icons while a tool runs and after it completes", () => {
431
+ const running: AiTurnBlock = {
432
+ id: "project-call-1",
433
+ kind: "tool",
434
+ callId: "project-1",
435
+ name: "search_project",
436
+ args: { action: "list" },
437
+ status: "running",
438
+ };
439
+ const completed: AiTurnBlock = { ...running, status: "completed", result: { ok: true, message: "Found 1 Project item." } };
440
+
441
+ const runningHtml = renderToString(() => createComponent(AiTurnBlockView, { block: running, turnId: "turn-1" }));
442
+ const completedHtml = renderToString(() => createComponent(AiTurnBlockView, { block: completed, turnId: "turn-1" }));
443
+
444
+ expect(runningHtml).toContain("ti-folder-search");
445
+ expect(completedHtml).toContain("ti-folder-search");
446
+ });
447
+
448
+ test("keeps Present busy until a completed result is available", () => {
449
+ const running: AiTurnBlock = {
450
+ id: "present-call",
451
+ kind: "tool",
452
+ callId: "present-1",
453
+ name: "present",
454
+ args: { path: "/report.pdf", title: "Quarterly report" },
455
+ status: "running",
456
+ };
457
+
458
+ const html = renderToString(() => createComponent(AiTurnBlockView, { block: running, turnId: "turn-1" }));
459
+
460
+ expect(html).toContain("Preparing file");
461
+ expect(html).toContain('data-busy="true"');
462
+ expect(html).not.toContain("Quarterly report");
463
+ expect(html).not.toContain("Download");
464
+ });
465
+
466
+ test("shows one presented-file label and underlines only the Download text on hover", () => {
467
+ const completed: AiTurnBlock = {
468
+ id: "present-call",
469
+ kind: "tool",
470
+ callId: "present-1",
471
+ name: "present",
472
+ args: { path: "/testdatei.md", title: "Testdatei.md" },
473
+ status: "completed",
474
+ result: { path: "/testdatei.md", size: 120, mediaType: "text/markdown" },
475
+ };
476
+
477
+ const html = renderToString(() =>
478
+ createComponent(AiChatActionsProvider, {
479
+ actions: { onOpenFile: () => undefined, fileUrl: () => "/download/testdatei.md" },
480
+ get children() {
481
+ return createComponent(AiTurnBlockView, { block: completed, turnId: "turn-1" });
482
+ },
483
+ }),
484
+ );
485
+
486
+ expect(html.match(/Testdatei\.md/g)).toHaveLength(1);
487
+ expect(html).toContain('download="testdatei.md"');
488
+ expect(html).toContain('<span class="underline-offset-2 group-hover:underline">Download</span>');
489
+ expect(html).not.toMatch(/<a[^>]*hover:underline/);
490
+ });
491
+
492
+ test("collapses intermediate text and tools after completion while keeping rich results and the final text visible", () => {
493
+ const blocks: AiTurnBlock[] = [
494
+ { id: "text-before", kind: "text", text: "Before tool" },
495
+ {
496
+ id: "tool-middle",
497
+ kind: "tool",
498
+ callId: "tool-1",
499
+ name: "unknown_tool",
500
+ status: "completed",
501
+ result: { ok: true },
502
+ },
503
+ {
504
+ id: "card-middle",
505
+ kind: "tool",
506
+ callId: "card-1",
507
+ name: "card",
508
+ args: { title: "Useful card", value: "42" },
509
+ status: "completed",
510
+ result: { ok: true },
511
+ },
512
+ {
513
+ id: "present-middle",
514
+ kind: "tool",
515
+ callId: "present-1",
516
+ name: "present",
517
+ args: { path: "/report.pdf", title: "Useful report" },
518
+ status: "completed",
519
+ result: { path: "/report.pdf", size: 12, mediaType: "application/pdf" },
520
+ },
521
+ { id: "text-after", kind: "text", text: "After tool" },
522
+ ];
523
+ const item: AiAssistantTimelineItem = {
524
+ type: "assistant",
525
+ id: "stored-1",
526
+ loopId: "turn-1",
527
+ entries: [],
528
+ blocks,
529
+ actionEntry: null,
530
+ workedMs: 1_000,
531
+ };
532
+ const partitioned = partitionCompletedAssistantBlocks(blocks);
533
+ expect(partitioned.worked.map((candidate) => candidate.id)).toEqual(["text-before", "tool-middle"]);
534
+ expect(partitioned.visible.map((candidate) => candidate.id)).toEqual(["card-middle", "present-middle", "text-after"]);
535
+
536
+ const storedHtml = renderToString(() => createComponent(AiAssistantContent, { item }));
537
+ const liveHtml = renderToString(() => createComponent(AiTurnBlockList, { blocks, turnId: "turn-1" }));
538
+
539
+ expect(storedHtml).toContain("Worked for 1s");
540
+ expect(hasOpenDetails(storedHtml)).toBe(false);
541
+ expect(storedHtml.indexOf("Worked for 1s")).toBeLessThan(storedHtml.indexOf("Before tool"));
542
+ expect(storedHtml.indexOf("Before tool")).toBeLessThan(storedHtml.indexOf("Unknown tool"));
543
+ expect(storedHtml.indexOf("Unknown tool")).toBeLessThan(storedHtml.indexOf("Useful card"));
544
+ expect(storedHtml.indexOf("Useful card")).toBeLessThan(storedHtml.indexOf("Useful report"));
545
+ expect(storedHtml.indexOf("Useful report")).toBeLessThan(storedHtml.indexOf("After tool"));
546
+ expect(storedHtml).not.toContain("unknown_tool");
547
+
548
+ expect(liveHtml.indexOf("Before tool")).toBeLessThan(liveHtml.indexOf("Unknown tool"));
549
+ expect(liveHtml.indexOf("Unknown tool")).toBeLessThan(liveHtml.indexOf("Useful card"));
550
+ expect(liveHtml.indexOf("Useful card")).toBeLessThan(liveHtml.indexOf("Useful report"));
551
+ expect(liveHtml.indexOf("Useful report")).toBeLessThan(liveHtml.indexOf("After tool"));
552
+ expect(liveHtml).not.toContain("Worked for");
553
+ });
554
+
555
+ test("does not leave an empty block-list item in the chat layout", () => {
556
+ const html = renderToString(() => createComponent(AiTurnBlockList, { blocks: [], turnId: "turn-empty" }));
557
+
558
+ expect(html).not.toContain("flex flex-col");
559
+ });
560
+
561
+ test("opens failed completed work immediately and preserves an explicit Worked disclosure choice", () => {
562
+ const item: AiAssistantTimelineItem = {
563
+ type: "assistant",
564
+ id: "stored-failure",
565
+ loopId: "turn-failure",
566
+ entries: [],
567
+ blocks: [
568
+ {
569
+ id: "failed-tool",
570
+ kind: "tool",
571
+ callId: "failed-1",
572
+ name: "unknown_tool",
573
+ status: "failed",
574
+ result: { code: "VALIDATION_FAILED", message: "Mailbox is required" },
575
+ isError: true,
576
+ },
577
+ { id: "final-text", kind: "text", text: "I could not finish that." },
578
+ ],
579
+ actionEntry: null,
580
+ workedMs: 2_000,
581
+ };
582
+
583
+ const failedHtml = renderToString(() => createComponent(AiAssistantContent, { item }));
584
+ expect(hasOpenDetailsContaining(failedHtml, "Worked for 2s")).toBe(true);
585
+ expect(failedHtml).toContain('data-tone="danger"');
586
+ expect(failedHtml).toContain("Mailbox is required");
587
+
588
+ const disclosureState = createAiToolDisclosureState();
589
+ disclosureState.set("worked:turn-failure", false);
590
+ const collapsedHtml = renderToString(() => createComponent(AiAssistantContent, { item: { ...item }, disclosureState }));
591
+ expect(hasOpenDetailsContaining(collapsedHtml, "Worked for 2s")).toBe(false);
592
+ });
593
+
594
+ test("shows categorized fetch_file failures as a clear danger activity", () => {
595
+ const failed: AiTurnBlock = {
596
+ id: "fetch-file-failed",
597
+ kind: "tool",
598
+ callId: "fetch-call-1",
599
+ name: "fetch_file",
600
+ args: { url: "https://files.example/missing.pdf" },
601
+ status: "failed",
602
+ result: "File not found — The linked file does not exist or is no longer available.",
603
+ isError: true,
604
+ };
605
+
606
+ const html = renderToString(() => createComponent(AiTurnBlockView, { block: failed, turnId: "turn-fetch" }));
607
+
608
+ expect(html).toContain('data-tone="danger"');
609
+ expect(html).toContain("File not found");
610
+ expect(html).toContain("The linked file does not exist or is no longer available.");
611
+ expect(html).not.toContain("Fetch file failed");
612
+ expect(hasOpenDetailsContaining(html, "File not found")).toBe(true);
613
+ expect(html).toContain("https://files.example/missing.pdf");
614
+ });
615
+
616
+ test("shows fetched file identity, metadata, and destination with a first-party favicon", () => {
617
+ const completed: AiTurnBlock = {
618
+ id: "fetch-file-completed",
619
+ kind: "tool",
620
+ callId: "fetch-call-2",
621
+ name: "fetch_file",
622
+ args: { url: "https://github.com/example/repo/raw/main/guide.pdf" },
623
+ status: "completed",
624
+ result: {
625
+ path: "/imports/guide.pdf",
626
+ size: 2048,
627
+ mediaType: "application/pdf",
628
+ url: "https://raw.githubusercontent.com/example/repo/main/guide.pdf",
629
+ },
630
+ isError: false,
631
+ };
632
+
633
+ const html = renderToString(() => createComponent(AiTurnBlockView, { block: completed, turnId: "turn-fetch" }));
634
+
635
+ expect(html).toContain("Fetched guide.pdf");
636
+ expect(html).toContain("github.com · 2.0 KB · application/pdf");
637
+ expect(html).toContain('src="https://github.com/favicon.ico"');
638
+ expect(html).toContain("https://raw.githubusercontent.com/example/repo/main/guide.pdf");
639
+ expect(html).toContain("/imports/guide.pdf");
640
+ expect(html).not.toContain(">Input</p>");
641
+ expect(html).not.toContain(">Response</p>");
642
+ });
643
+
644
+ test("keeps the app identity on approval prompts", () => {
645
+ const renderApproval = (approvalBlock: AiTurnBlock) =>
646
+ renderToString(() =>
647
+ createComponent(AiChatActionsProvider, {
648
+ actions: { onApproval: async () => undefined },
649
+ get children() {
650
+ return createComponent(AiTurnBlockView, { block: approvalBlock, turnId: "turn-1" });
651
+ },
652
+ }),
653
+ );
654
+ const html = renderApproval(block("awaiting_approval"));
655
+ expect(html).toContain(">Contacts · List contacts</h3>");
656
+ expect(html).toContain("List contacts");
657
+ expect(html).toContain('data-variant="ai"');
658
+ expect(html).toContain('<span class="k2b-button__label">List contacts</span>');
659
+ expect(html).not.toContain("Contacts: List contacts");
660
+ expect(html).not.toContain("Approve Contacts: List contacts");
661
+ expect(html).not.toContain("Review the validated arguments");
662
+ expect(html).not.toContain("Contacts · Approval required");
663
+ expect(html).toContain("ti-address-book");
664
+ expect(html).toContain("--app-accent:#0f766e");
665
+ expect(html).toContain("app-accent-scope");
666
+ expect(html).toContain("border-[var(--k2b-border)]");
667
+ expect(html).toContain("bg-[var(--k2b-surface)]");
668
+ expect(html).toContain("data-ai-approval-footer");
669
+ expect(html).toContain('aria-expanded="false"');
670
+ expect(html).toContain("k2b-split-button");
671
+ expect(html).toContain("Details");
672
+ expect(html).toContain("ti-eye");
673
+ expect(html).toContain("More options for List contacts");
674
+ expect(html).not.toContain("Always approve");
675
+ expect(html).not.toContain("k2b-content-structured-data");
676
+
677
+ const rememberable = block("awaiting_approval");
678
+ if (rememberable.kind !== "tool" || !rememberable.approval) throw new Error("approval block missing");
679
+ rememberable.approval.allowAlways = true;
680
+ const rememberableHtml = renderApproval(rememberable);
681
+ expect(rememberableHtml).toContain("k2b-split-button");
682
+ expect(rememberableHtml).toContain("Always approve");
683
+ expect(rememberableHtml).toContain("More options for List contacts");
684
+ expect(rememberableHtml).not.toContain("Always allow");
685
+
686
+ const customReview = block("awaiting_approval");
687
+ if (customReview.kind !== "tool" || !customReview.approval) throw new Error("approval block missing");
688
+ customReview.approval.message = "The draft includes an external recipient.";
689
+ customReview.approval.review = {
690
+ message: "The draft includes an external recipient.",
691
+ details: [
692
+ { label: "Subject", value: "Release follow-up" },
693
+ { label: "Recipients", value: "Ada", display: "inline" },
694
+ { label: "Send on", value: "2026-08-20", format: "date" },
695
+ { label: "Send at", value: "2026-08-20T09:00:00+02:00", format: "date-time" },
696
+ { label: "Proposed body", value: "Hello **Ada**\n<script>alert('plain text')</script>", display: "block" },
697
+ ],
698
+ links: [
699
+ { rel: "open", href: "/app/contacts" },
700
+ { rel: "edit", href: "/app/mail/MbA123/drafts/DrG789", title: "Edit draft" },
701
+ ],
702
+ approvalScope: "book:default",
703
+ };
704
+ const customHtml = renderApproval(customReview);
705
+ expect(customHtml).toContain("The draft includes an external recipient.");
706
+ expect(customHtml.indexOf("The draft includes an external recipient.")).toBeLessThan(customHtml.indexOf("data-ai-approval-footer"));
707
+ expect(customHtml).toContain('<dt class="font-semibold text-primary">Subject</dt>');
708
+ expect(customHtml).toContain("border-t border-[var(--k2b-border)] pt-3");
709
+ expect(customHtml).not.toContain("border-y border-[var(--k2b-border)]");
710
+ expect(customHtml).toContain('<dd class="min-w-0 whitespace-pre-wrap break-words">Release follow-up</dd>');
711
+ expect(customHtml).toContain('<dt class="font-semibold text-primary">Recipients</dt>');
712
+ expect(customHtml).toContain('<time datetime="2026-08-20">');
713
+ expect(customHtml).toContain('<time datetime="2026-08-20T09:00:00+02:00">');
714
+ expect(customHtml).toContain('aria-label="Proposed body"');
715
+ expect(customHtml).toContain('aria-label="Proposed body content"');
716
+ expect(customHtml).toContain('tabindex="0"');
717
+ expect(customHtml).toContain("Hello **Ada**");
718
+ expect(customHtml).toContain("&lt;script>alert('plain text')&lt;/script>");
719
+ expect(customHtml).not.toContain("<strong>Ada</strong>");
720
+ expect(customHtml).toContain('href="/app/mail/MbA123/drafts/DrG789"');
721
+ expect(customHtml).toContain(">Open in Contacts</span></a>");
722
+ expect(customHtml).toContain(">Edit draft</span></a>");
723
+ expect(customHtml.indexOf(">Open in Contacts</span></a>")).toBeGreaterThan(customHtml.indexOf("data-ai-approval-footer"));
724
+ expect(customHtml.indexOf(">Open in Contacts</span></a>")).toBeLessThan(customHtml.indexOf(">Reject</span>"));
725
+ expect(customHtml).toContain('data-variant="ghost"');
726
+ expect(customHtml).not.toContain("book:default");
727
+ });
728
+
729
+ test("renders a rejected approval as one compact result row", () => {
730
+ const rejected = block("awaiting_approval");
731
+ if (rejected.kind !== "tool") throw new Error("tool block missing");
732
+ rejected.status = "rejected";
733
+ rejected.approval = undefined;
734
+ rejected.result = "Capability Action was rejected by the user.";
735
+ rejected.isError = true;
736
+
737
+ const html = renderToString(() => createComponent(AiTurnBlockView, { block: rejected, turnId: "turn-1" }));
738
+
739
+ expect(html).toContain("List contacts was rejected");
740
+ expect(html).toContain("ti-address-book");
741
+ expect(html).toContain("--k2b-chat-activity-accent:#0f766e");
742
+ expect(html).not.toContain("Capability Action was rejected by the user.");
743
+ expect(html).not.toContain("data-ai-approval-footer");
744
+ expect(html).not.toContain(">Input</p>");
745
+ expect(html).not.toContain(">Response</p>");
746
+ expect(html).not.toContain("k2b-content-structured-data");
747
+ expect(hasOpenDetails(html)).toBe(false);
748
+ });
749
+ });
750
+
751
+ describe("live tool disclosure stability", () => {
752
+ test("keeps a user disclosure override across a remounted loop block", () => {
753
+ const disclosureState = createAiToolDisclosureState();
754
+ const completed: AiTurnBlock = {
755
+ id: "tool-stable",
756
+ kind: "tool",
757
+ callId: "call-stable",
758
+ name: "unknown_tool",
759
+ status: "completed",
760
+ result: { step: 1 },
761
+ };
762
+
763
+ const initialHtml = renderToString(() =>
764
+ createComponent(AiTurnBlockList, { blocks: [completed], turnId: "turn-stable", disclosureState }),
765
+ );
766
+ expect(hasOpenDetails(initialHtml)).toBe(false);
767
+
768
+ disclosureState.set(completed.id, true);
769
+ const nextHtml = renderToString(() =>
770
+ createComponent(AiTurnBlockList, {
771
+ blocks: [{ ...completed, result: { step: 2 } }],
772
+ turnId: "turn-stable",
773
+ disclosureState,
774
+ }),
775
+ );
776
+ expect(hasOpenDetails(nextHtml)).toBe(true);
777
+ });
778
+
779
+ test("does not recreate stored timeline items for active-turn updates", () => {
780
+ const storedMessage: AiStoredMessage = {
781
+ id: "message-1",
782
+ shortId: "msg1",
783
+ conversationId: "conversation-1",
784
+ seq: 1,
785
+ kind: "message",
786
+ message: { role: "user", content: [{ type: "text", text: "Hello" }] },
787
+ loopId: null,
788
+ modelProfileId: null,
789
+ providerModel: null,
790
+ usage: null,
791
+ stopReason: null,
792
+ loopAggregate: null,
793
+ loopDoneReason: null,
794
+ compactedAt: null,
795
+ meta: null,
796
+ createdAt: "2026-08-20T12:00:00.000Z",
797
+ };
798
+ const activeTurn: AiActiveTurn = {
799
+ turnId: "turn-1",
800
+ attempt: 1,
801
+ seq: 2,
802
+ status: "running",
803
+ blocks: [{ id: "text-1", kind: "text", text: "Working" }],
804
+ modelProfileId: null,
805
+ };
806
+
807
+ createRoot((dispose) => {
808
+ const [messages] = createSignal<readonly AiStoredMessage[]>([storedMessage]);
809
+ const [active, setActive] = createSignal<AiActiveTurn | null>(activeTurn);
810
+ let timeline: Accessor<readonly import("@k2b/ui").ChatTimelineItem[]> | undefined;
811
+ createComponent(AiChatActionsProvider, {
812
+ actions: {},
813
+ get children() {
814
+ timeline = createAiChatTimeline({ messages, activeTurn: active });
815
+ return undefined;
816
+ },
817
+ });
818
+
819
+ const storedBefore = timeline?.()[0];
820
+ setActive({ ...activeTurn, seq: 3, blocks: [...activeTurn.blocks, { id: "text-2", kind: "text", text: "More" }] });
821
+ expect(timeline?.()[0]).toBe(storedBefore);
822
+ dispose();
823
+ });
824
+ });
825
+ });
826
+
827
+ describe("survey presentation", () => {
828
+ test("uses a neutral action sheet with vertical choices and a bottom action footer", () => {
829
+ const html = renderToString(() =>
830
+ createComponent(CloudSurveyBlock, {
831
+ args: {
832
+ title: "Choose a follow-up time",
833
+ description: "When should I remind you?",
834
+ questions: [
835
+ {
836
+ id: "timing",
837
+ type: "single",
838
+ label: "Reminder",
839
+ required: true,
840
+ options: [
841
+ { label: "Tomorrow morning", value: "tomorrow" },
842
+ { label: "Friday afternoon", value: "friday" },
843
+ ],
844
+ },
845
+ {
846
+ id: "confidence",
847
+ type: "rating",
848
+ label: "Confidence",
849
+ min: 1,
850
+ max: 5,
851
+ },
852
+ ],
853
+ },
854
+ onSubmit: async () => undefined,
855
+ }),
856
+ );
857
+
858
+ expect(html).toContain("Choose a follow-up time");
859
+ expect(html).toContain('class="w-full min-w-0 overflow-hidden');
860
+ expect(html).toContain('class="mt-2 grid gap-1.5"');
861
+ expect(html).toContain('type="radio"');
862
+ expect(html).toContain('class="k2b-slider"');
863
+ expect(html).toContain("var(--k2b-surface-muted)");
864
+ expect(html).toContain("Tomorrow morning");
865
+ expect(html).toContain("Friday afternoon");
866
+ expect(html).toContain("border-t border-[var(--k2b-border)]");
867
+ expect(html).toContain("k2b-button ml-auto");
868
+ expect(html).toContain("Submit");
869
+ });
870
+
871
+ test("renders accepted survey answers in the shared user bubble", () => {
872
+ const completed: AiTurnBlock = {
873
+ id: "survey-call",
874
+ kind: "tool",
875
+ callId: "call-1",
876
+ name: "survey",
877
+ args: {
878
+ title: "Invoice details",
879
+ questions: [{ id: "amount", type: "text", label: "Amount" }],
880
+ },
881
+ status: "completed",
882
+ frontendMode: "client_interaction",
883
+ result: { submitted: true, answers: { amount: "150 EUR" } },
884
+ };
885
+
886
+ const activeHtml = renderToString(() => createComponent(AiTurnBlockView, { block: completed, turnId: "turn-1", active: true }));
887
+ const historicalHtml = renderToString(() => createComponent(AiTurnBlockView, { block: completed, turnId: "turn-1" }));
888
+
889
+ expect(activeHtml).toContain('data-role="user"');
890
+ expect(activeHtml).toContain("Amount");
891
+ expect(activeHtml).toContain("150 EUR");
892
+ expect(activeHtml).not.toContain("<details");
893
+ expect(activeHtml).not.toContain("Submit</span>");
894
+ expect(historicalHtml).toBe(activeHtml);
895
+ });
896
+ });
897
+
898
+ describe("survey answer timeline", () => {
899
+ const survey = {
900
+ id: "tool:survey-1",
901
+ kind: "tool",
902
+ callId: "survey-1",
903
+ name: "survey",
904
+ status: "completed",
905
+ frontendMode: "client_interaction",
906
+ args: {
907
+ title: "Preferences",
908
+ questions: [
909
+ {
910
+ id: "choice",
911
+ type: "multiple",
912
+ label: "Which days?",
913
+ options: [
914
+ { value: "mo", label: "Monday" },
915
+ { value: "tu", label: "Tuesday" },
916
+ ],
917
+ },
918
+ { id: "note", type: "text", label: "Anything else?" },
919
+ ],
920
+ },
921
+ result: { submitted: true, answers: { choice: ["mo", "tu"], note: "<script>hello</script>\nSecond line" } },
922
+ } satisfies Extract<AiTurnBlock, { kind: "tool" }>;
923
+
924
+ const turn = (blocks: AiTurnBlock[]): AiActiveTurn => ({
925
+ turnId: "survey-turn",
926
+ attempt: 1,
927
+ seq: 1,
928
+ status: "running",
929
+ blocks,
930
+ modelProfileId: null,
931
+ });
932
+ const snapshot = (messages: AiStoredMessage[], activeTurn: AiActiveTurn | null) =>
933
+ createRoot((dispose) => {
934
+ const items = createAiChatTimeline({ messages: () => messages, activeTurn: () => activeTurn })();
935
+ const result = items.map((item) => ({
936
+ ...item,
937
+ html: renderToString(() => item.content),
938
+ }));
939
+ dispose();
940
+ return result;
941
+ });
942
+ const stored = (message: AiStoredMessage["message"], seq: number): AiStoredMessage => ({
943
+ id: `stored-${seq}`,
944
+ shortId: `stored-${seq}`,
945
+ conversationId: "conversation-1",
946
+ seq,
947
+ kind: "message",
948
+ message,
949
+ loopId: "survey-turn",
950
+ modelProfileId: null,
951
+ providerModel: null,
952
+ usage: null,
953
+ stopReason: null,
954
+ loopAggregate: null,
955
+ loopDoneReason: null,
956
+ compactedAt: null,
957
+ meta: null,
958
+ createdAt: "2026-09-08T12:00:00Z",
959
+ });
960
+
961
+ test("keeps accepted answers between assistant outputs in live and reloaded history", () => {
962
+ const active = snapshot(
963
+ [],
964
+ turn([{ id: "before", kind: "text", text: "Before answer" }, survey, { id: "after", kind: "text", text: "After answer" }]),
965
+ );
966
+ const history = snapshot(
967
+ [
968
+ stored(
969
+ {
970
+ role: "assistant",
971
+ content: [
972
+ { type: "text", text: "Before answer" },
973
+ { type: "tool_call", id: survey.callId, name: survey.name, args: survey.args },
974
+ ],
975
+ },
976
+ 1,
977
+ ),
978
+ stored({ role: "tool_result", callId: survey.callId, name: survey.name, result: survey.result }, 2),
979
+ stored({ role: "assistant", content: [{ type: "text", text: "After answer" }], stopReason: "stop" }, 3),
980
+ ],
981
+ null,
982
+ );
983
+ for (const items of [active, history]) {
984
+ expect(items.map((item) => item.kind === "message" && item.role)).toEqual(["assistant", "user", "assistant"]);
985
+ expect(items[0]!.html).toContain("Before answer");
986
+ expect(items[2]!.html).toContain("After answer");
987
+ expect(items[1]!.html).toContain("Which days?");
988
+ expect(items[1]!.html).toContain("Monday, Tuesday");
989
+ expect(items[1]!.html).toContain("&lt;script>hello&lt;/script>");
990
+ expect(items[1]!.html).not.toContain("<details");
991
+ expect(items[0]!.html + items[2]!.html).not.toContain("Which days?");
992
+ expect(items[1]!.kind === "message" && items[1]!.actions).toBeUndefined();
993
+ }
994
+ expect(active[1]!.id).toBe(history[1]!.id);
995
+ expect(history.filter((item) => item.kind === "message" && item.actions?.length)).toHaveLength(1);
996
+ });
997
+
998
+ test("replaces only accepted results and keeps assistant progress after the answer", () => {
999
+ for (const pending of [
1000
+ { ...survey, status: "awaiting_client" as const, result: undefined },
1001
+ { ...survey, status: "failed" as const, isError: true },
1002
+ { ...survey, result: { submitted: false } },
1003
+ ]) {
1004
+ const items = snapshot([], turn([pending]));
1005
+ expect(items.map((item) => item.kind === "message" && item.role)).toEqual(["assistant"]);
1006
+ }
1007
+ const accepted = snapshot([], turn([survey]));
1008
+ expect(accepted.map((item) => item.kind === "message" && item.role)).toEqual(["user", "assistant"]);
1009
+ expect(accepted[1]).toMatchObject({ status: "streaming" });
1010
+ expect(accepted[1]!.html).toBe("");
1011
+ });
1012
+
1013
+ test("keeps empty answers and zero ratings readable and IDs scoped to their turn", () => {
1014
+ const result = { submitted: true, answers: { choice: [], score: 0 } };
1015
+ const first = snapshot([], turn([{ ...survey, result }]));
1016
+ const second = snapshot([], { ...turn([{ ...survey, result }]), turnId: "another-turn" });
1017
+ expect(first[0]!.html).toContain("No answer");
1018
+ expect(first[0]!.html).toContain(">0</dd>");
1019
+ expect(first[0]!.id).not.toBe(second[0]!.id);
1020
+ });
1021
+
1022
+ test("preserves consecutive surveys and steering in their original order", () => {
1023
+ const items = snapshot(
1024
+ [],
1025
+ turn([
1026
+ survey,
1027
+ { id: "steer-1", kind: "steer_message", steerId: "steer-1", text: "One more thing", status: "pending" },
1028
+ { ...survey, id: "tool:survey-2", callId: "survey-2", name: "cloud_survey" },
1029
+ ]),
1030
+ );
1031
+ expect(items.map((item) => item.kind === "message" && item.role)).toEqual(["user", "user", "user", "assistant"]);
1032
+ expect(new Set(items.map((item) => item.id)).size).toBe(items.length);
1033
+ expect(items[1]!.html).toContain("One more thing");
1034
+ });
1035
+ });
1036
+
1037
+ describe("text editor presentation", () => {
1038
+ test.each(["plain", "markdown"] as const)("renders %s source in the direct Markdown editor", (format) => {
1039
+ const html = renderToString(() =>
1040
+ createComponent(CloudTextEditorBlock, {
1041
+ args: {
1042
+ title: "Review mail",
1043
+ description: "Adjust this draft before I continue.",
1044
+ content: "Hello **Ada**",
1045
+ format,
1046
+ submitLabel: "Use draft",
1047
+ },
1048
+ onSubmit: async () => undefined,
1049
+ }),
1050
+ );
1051
+
1052
+ expect(html).toContain("Review mail");
1053
+ expect(html).toContain('aria-label="Review mail"');
1054
+ expect(html).toContain("k2b-markdown-editor");
1055
+ expect(html).not.toContain("k2b-autocomplete");
1056
+ expect(html).not.toContain('data-fill="true"');
1057
+ expect(html).not.toContain("Adjust this draft before I continue.");
1058
+ expect(html).toContain("Suggest changes");
1059
+ expect(html).toContain("13 / 20,000");
1060
+ expect(html).toContain("Use draft");
1061
+ expect(html).toContain("20,000");
1062
+ });
1063
+
1064
+ test("collapses accepted text immediately and keeps the exact source in details", () => {
1065
+ const completed: AiTurnBlock = {
1066
+ id: "text-editor-call",
1067
+ kind: "tool",
1068
+ callId: "call-2",
1069
+ name: "text_editor",
1070
+ args: { title: "Review mail", content: "Initial", format: "markdown" },
1071
+ status: "completed",
1072
+ frontendMode: "client_interaction",
1073
+ result: { submitted: true, content: "Hello **Ada**", format: "markdown" },
1074
+ };
1075
+ const activeHtml = renderToString(() => createComponent(AiTurnBlockView, { block: completed, turnId: "turn-1", active: true }));
1076
+ const historicalHtml = renderToString(() => createComponent(AiTurnBlockView, { block: completed, turnId: "turn-1" }));
1077
+
1078
+ expect(activeHtml).toContain("Review mail · waiting");
1079
+ expect(activeHtml).toContain('class="group w-full min-w-0 text-xs"');
1080
+ expect(activeHtml).toContain('class="mt-1 w-full min-w-0 rounded-md bg-zinc-100/70');
1081
+ expect(activeHtml).toContain("Hello **Ada**");
1082
+ expect(activeHtml).not.toContain("<strong>Ada</strong>");
1083
+ expect(activeHtml).not.toContain("Waiting for the assistant to continue");
1084
+ expect(historicalHtml).toContain("Review mail · submitted");
1085
+ });
1086
+
1087
+ test("keeps revision feedback distinct from accepted source", () => {
1088
+ const completed: AiTurnBlock = {
1089
+ id: "text-editor-feedback-call",
1090
+ kind: "tool",
1091
+ callId: "call-3",
1092
+ name: "text_editor",
1093
+ args: { title: "Review mail", content: "Initial", format: "markdown" },
1094
+ status: "completed",
1095
+ frontendMode: "client_interaction",
1096
+ result: { submitted: false, feedback: "Make the opening more direct." },
1097
+ };
1098
+
1099
+ const activeHtml = renderToString(() => createComponent(AiTurnBlockView, { block: completed, turnId: "turn-1", active: true }));
1100
+ const historicalHtml = renderToString(() => createComponent(AiTurnBlockView, { block: completed, turnId: "turn-1" }));
1101
+
1102
+ expect(activeHtml).toContain("Review mail · revising");
1103
+ expect(activeHtml).toContain("Make the opening more direct.");
1104
+ expect(activeHtml).toContain("Feedback");
1105
+ expect(historicalHtml).toContain("Review mail · changes requested");
1106
+ expect(historicalHtml).not.toContain("Initial");
1107
+ });
1108
+ });