@desplega.ai/agent-swarm 2.7.6 → 2.7.7

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 (244) hide show
  1. package/dist/{acp-adapter-bp91d37f.js → acp-adapter-7b4vk3h5.js} +5 -4
  2. package/dist/{actions-yj0eeaqh.js → actions-3tt153af.js} +18 -18
  3. package/dist/{amp-adapter-4pg9h34q.js → amp-adapter-njpts0yr.js} +1 -1
  4. package/dist/{anthropic-messages-x4q6rfq5.js → anthropic-messages-xey2skwx.js} +7 -7
  5. package/dist/{app-909e97cr.js → app-nky4378f.js} +3 -3
  6. package/dist/{assistant-qdyb5fyy.js → assistant-jnb669pa.js} +19 -19
  7. package/dist/{azure-openai-responses-s1w5n70z.js → azure-openai-responses-26t7j9ra.js} +5 -5
  8. package/dist/{boot-reembed-fk0s9z18.js → boot-reembed-9mgvaztt.js} +1 -1
  9. package/dist/{boot-reembed-gs1rzrnh.js → boot-reembed-qs2djdej.js} +1 -1
  10. package/dist/{boot-scrub-sweep-4qfd4kfx.js → boot-scrub-sweep-yjpdj7s1.js} +3 -3
  11. package/dist/{boot-seeds-yr00k13d.js → boot-seeds-5cm6ambd.js} +4 -4
  12. package/dist/{boxd-vdmfssd9.js → boxd-nv507va8.js} +1 -1
  13. package/dist/{claude-adapter-qta1f1xn.js → claude-adapter-fn287zmm.js} +5 -4
  14. package/dist/{claude-managed-setup-sc6j4dm0.js → claude-managed-setup-wtdz2mj2.js} +1 -1
  15. package/dist/{claude-sdk-session-zgrz6yvc.js → claude-sdk-session-x91kqyhm.js} +8288 -8224
  16. package/dist/{cli-wp9sp1qb.js → cli-08bmz9z9.js} +1 -1
  17. package/dist/{cli-yy3rhsdw.js → cli-1c6tzt8g.js} +7 -7
  18. package/dist/{cli-q0k1w5zd.js → cli-35rsp8ye.js} +1 -1
  19. package/dist/{cli-xnrwazav.js → cli-3fg0bdcv.js} +1 -1
  20. package/dist/{cli-89knwags.js → cli-5b456bpf.js} +2 -2
  21. package/dist/{cli-drr5v6av.js → cli-5jnhv72d.js} +1 -1
  22. package/dist/{cli-dxae2yg7.js → cli-5zx699j9.js} +2 -2
  23. package/dist/{cli-0at8w34w.js → cli-6g0hb4cp.js} +8 -8
  24. package/dist/{cli-e3r2neck.js → cli-6pfd54vx.js} +5 -5
  25. package/dist/{cli-bsn8nhrq.js → cli-6vcp76qk.js} +3 -3
  26. package/dist/{cli-ekwks2vs.js → cli-72v799xn.js} +1 -1
  27. package/dist/cli-82j36f2m.js +45 -0
  28. package/dist/{cli-yx4kfngt.js → cli-8305gbq0.js} +29 -16
  29. package/dist/{cli-4necwtsv.js → cli-8sckt74b.js} +8 -8
  30. package/dist/{cli-vvpx3gsx.js → cli-95jzxcp3.js} +1 -1
  31. package/dist/{cli-hr2e2fft.js → cli-adeegh7d.js} +7 -7
  32. package/dist/{cli-m48g6p95.js → cli-afkqkrtw.js} +3 -3
  33. package/dist/{cli-0qatpy8q.js → cli-ayayvfg4.js} +24 -4
  34. package/dist/{cli-bqt1mptt.js → cli-az48n0nr.js} +4 -4
  35. package/dist/{cli-zm72anhg.js → cli-bfv7vdg6.js} +9 -9
  36. package/dist/{cli-hgaazk69.js → cli-caj33vcq.js} +5 -5
  37. package/dist/{cli-3ypk9vk1.js → cli-cb2a8pna.js} +1 -1
  38. package/dist/{cli-waqbxg08.js → cli-cppkf404.js} +17 -17
  39. package/dist/{cli-yn4kxs06.js → cli-ddaanqkd.js} +4 -4
  40. package/dist/{cli-386tyxxg.js → cli-e0kmphd6.js} +2 -2
  41. package/dist/{cli-54t00gt8.js → cli-e46hg0xw.js} +1 -1
  42. package/dist/{cli-jm2vfpfd.js → cli-e5bzrn01.js} +1 -1
  43. package/dist/{cli-436532ck.js → cli-excnnjg2.js} +165 -153
  44. package/dist/{cli-71ptav7x.js → cli-ferkkrg9.js} +1 -1
  45. package/dist/{cli-vpf89f34.js → cli-fmjd0t1y.js} +3 -3
  46. package/dist/{cli-pd7vagab.js → cli-fyt2g4wd.js} +3 -3
  47. package/dist/{cli-gdhpcbhf.js → cli-g08zdaec.js} +1 -1
  48. package/dist/{cli-ehq736cn.js → cli-g7s47cc5.js} +4 -4
  49. package/dist/{cli-svj5090z.js → cli-gz6q4eve.js} +3 -3
  50. package/dist/{cli-8snvgs5r.js → cli-je518txm.js} +10 -10
  51. package/dist/{cli-rmmmrjkd.js → cli-jtjtabxe.js} +1 -1
  52. package/dist/{cli-611a1294.js → cli-k0wwfqbg.js} +18 -1
  53. package/dist/{cli-es6va8jk.js → cli-k1nje3v3.js} +38 -43
  54. package/dist/{cli-p5gp8pcx.js → cli-k8196sh2.js} +6 -6
  55. package/dist/{cli-nwj0ee2w.js → cli-k899ehvh.js} +2 -2
  56. package/dist/{cli-decfb6v8.js → cli-m0da8j35.js} +23 -9
  57. package/dist/{cli-he9wchme.js → cli-mkk9rnzz.js} +4 -4
  58. package/dist/{cli-nhrvpaj7.js → cli-n6bv12jn.js} +510 -4
  59. package/dist/{cli-8bc1w0tw.js → cli-nmzhp1b5.js} +14 -12
  60. package/dist/{cli-0yv5z2pg.js → cli-p82cnnjq.js} +1 -1
  61. package/dist/{cli-c60vetmm.js → cli-pc6n7fre.js} +1 -1
  62. package/dist/{cli-xds5f77d.js → cli-pjctyea3.js} +2 -2
  63. package/dist/{cli-xbbndvjy.js → cli-pq73h5mz.js} +3 -3
  64. package/dist/{cli-aa6yxc7m.js → cli-pz4rk021.js} +1 -1
  65. package/dist/{cli-xnqgpshy.js → cli-q3tkegby.js} +1 -1
  66. package/dist/cli-qghe0h66.js +67 -0
  67. package/dist/{cli-zbbwez0r.js → cli-qh4d4tvk.js} +4 -4
  68. package/dist/{cli-w6dt9ged.js → cli-rtt8g4v9.js} +1 -1
  69. package/dist/{cli-1h3wcpbw.js → cli-snpj5zbj.js} +2 -2
  70. package/dist/{cli-a7v854rf.js → cli-tgrv9cjx.js} +263 -12
  71. package/dist/{cli-faxe6thg.js → cli-vz22fyqv.js} +186 -54
  72. package/dist/{cli-k2gzwsj2.js → cli-wk5yrfwd.js} +160 -26
  73. package/dist/{cli-shpbx5ee.js → cli-ws19768n.js} +1641 -49
  74. package/dist/{cli-70r77n04.js → cli-xae4g51r.js} +1 -1
  75. package/dist/{cli-kqdg1bsb.js → cli-yndmfxb3.js} +2 -2
  76. package/dist/{cli-bjb3z51z.js → cli-yyj3j5m4.js} +2 -2
  77. package/dist/cli.js +21 -21
  78. package/dist/{cloudflare-workers-ai-system-one-bm60fjm0.js → cloudflare-workers-ai-system-one-ef2a980q.js} +2 -2
  79. package/dist/{codex-adapter-ayd1fb6h.js → codex-adapter-cf2yxe9m.js} +3 -2
  80. package/dist/{codex-hook-9efm2x1m.js → codex-hook-a0c6g6t6.js} +1 -1
  81. package/dist/{codex-session-runner-jesa32xc.js → codex-session-runner-6vfzdgbd.js} +3 -2
  82. package/dist/{commands-hanewgf2.js → commands-t3mfy7vn.js} +3 -3
  83. package/dist/{controller-dt1n3c1q.js → controller-q7xrbaz0.js} +3 -3
  84. package/dist/{controllers-connect-xx8x64n5.js → controllers-connect-c6nm70kg.js} +19 -19
  85. package/dist/{cursor-adapter-9s4dwwdw.js → cursor-adapter-ftd81ga8.js} +1 -1
  86. package/dist/{db-3vttcbv4.js → db-dk572x25.js} +3 -3
  87. package/dist/{db-9dv0g1ab.js → db-vemtpmcc.js} +4 -2
  88. package/dist/{dispatcher-tx9hd1sv.js → dispatcher-kh7ecxh9.js} +17 -17
  89. package/dist/{dsh-adapter-007y4ejs.js → dsh-adapter-v8628r89.js} +1 -1
  90. package/dist/{e2b-we6hs6q5.js → e2b-fr12yccx.js} +1 -1
  91. package/dist/{e2b-a19d28rz.js → e2b-x6ywzak3.js} +1 -1
  92. package/dist/{easter-egg-3d-rgbrqb8s.js → easter-egg-3d-nenw4x63.js} +1 -1
  93. package/dist/{embedding-space-ny1e2940.js → embedding-space-0jepb239.js} +6 -6
  94. package/dist/{embeddings-2t386t13.js → embeddings-xxt49yxg.js} +1 -1
  95. package/dist/{execute-pksh0ceh.js → execute-1mhw6e4x.js} +1 -1
  96. package/dist/{external-channel-phgmhmd7.js → external-channel-55v12e60.js} +2 -2
  97. package/dist/{fx-adapter-fy8rr921.js → fx-adapter-bs8wqrk5.js} +6 -5
  98. package/dist/{google-generative-ai-8y3m3k4b.js → google-generative-ai-v94hr7h0.js} +5 -5
  99. package/dist/{google-vertex-w3v98n7b.js → google-vertex-g7vr6cf8.js} +5 -5
  100. package/dist/{grok-adapter-q0abvpaj.js → grok-adapter-gpjqmkcq.js} +6 -5
  101. package/dist/handlers-zxmp1ta4.js +32 -0
  102. package/dist/{heartbeat-kadyek8p.js → heartbeat-k42svnr0.js} +20 -20
  103. package/dist/{hook-9k551xnj.js → hook-114m7hap.js} +4 -3
  104. package/dist/{hook-5zs3msf6.js → hook-6nphweh0.js} +24 -9
  105. package/dist/{http-mhz9vw1f.js → http-r8cc4wz7.js} +1025 -173
  106. package/dist/index-a8phxqnk.js +37 -0
  107. package/dist/{index-vdygbq10.js → index-e3s8wmxy.js} +1 -1
  108. package/dist/{index-q361cg2f.js → index-fpk4bcd9.js} +1 -1
  109. package/dist/index-k6h00t4c.js +30 -0
  110. package/dist/{index-rvr00sg7.js → index-nh3wyqdt.js} +1017 -574
  111. package/dist/index-xrz7te8h.js +33 -0
  112. package/dist/{keepalive-w56f32cg.js → keepalive-x060nt9p.js} +3 -3
  113. package/dist/lead-956sd2yc.js +71 -0
  114. package/dist/{maintenance-7mdgxcea.js → maintenance-2xxfexhf.js} +1 -1
  115. package/dist/{mistral-conversations-4pmwj1c0.js → mistral-conversations-3gjpdb4b.js} +4 -4
  116. package/dist/{oauth-refresh-sweep-29txy9vk.js → oauth-refresh-sweep-fckc4ar5.js} +2 -2
  117. package/dist/{onboard-kpsrkarj.js → onboard-zc48j237.js} +3 -3
  118. package/dist/{openai-codex-responses-44j573nm.js → openai-codex-responses-8pbxq9h5.js} +5 -5
  119. package/dist/{openai-completions-a4gee2a8.js → openai-completions-rm00s912.js} +4 -4
  120. package/dist/{openai-decisions-w4qkrcar.js → openai-decisions-8v957r2r.js} +1 -1
  121. package/dist/{openai-responses-rbp6h5kr.js → openai-responses-agxpf96k.js} +5 -5
  122. package/dist/{opencode-adapter-j25vm3rr.js → opencode-adapter-r690h1cr.js} +4 -3
  123. package/dist/{otel-impl-6kkct7a8.js → otel-impl-r5xbh81q.js} +4 -4
  124. package/dist/{pi-mono-adapter-exhkzhn2.js → pi-mono-adapter-c7v8sdec.js} +7 -6
  125. package/dist/{pricing-refresh-n2x7jh6k.js → pricing-refresh-3pmre11v.js} +3 -3
  126. package/dist/{rbac-roles-81ypx6es.js → rbac-roles-3vbshwsb.js} +1 -1
  127. package/dist/{rbac-roles-b5htkjby.js → rbac-roles-910mmkgz.js} +2 -2
  128. package/dist/{render-v2-qn6jmpk4.js → render-v2-pxykkbab.js} +3 -3
  129. package/dist/{resume-wd30wx23.js → resume-adda0bwy.js} +6 -6
  130. package/dist/{runtime-4hsg7h60.js → runtime-fnbm7stk.js} +1 -1
  131. package/dist/{session-access-5qvfevhk.js → session-access-2fvamrdc.js} +4 -4
  132. package/dist/{session-source-backfill-n1czavbr.js → session-source-backfill-08xvv0sm.js} +2 -2
  133. package/dist/{setup-dnb56qy5.js → setup-1aqmvmh1.js} +1 -1
  134. package/dist/{typesafe-system-one-5mavm259.js → typesafe-system-one-0bmqcxfg.js} +2 -2
  135. package/dist/{virtual-modules-76xxv9fs.js → virtual-modules-d2pjk9j1.js} +5 -5
  136. package/dist/{work-objects-4n21tfkm.js → work-objects-9s0yrm5j.js} +20 -20
  137. package/dist/{worker-follow-up-77hvzxww.js → worker-follow-up-3q78fqmr.js} +17 -17
  138. package/dist/worker-y15hy08h.js +71 -0
  139. package/openapi.json +197 -4
  140. package/package.json +5 -5
  141. package/src/apps/definition.ts +6 -5
  142. package/src/be/db/page-theme-tokens.ts +12 -0
  143. package/src/be/db/tasks/read.ts +9 -1
  144. package/src/be/db/tasks/write.ts +19 -0
  145. package/src/be/db-client.ts +107 -7
  146. package/src/be/db.ts +34 -2
  147. package/src/be/migrations/269_pages_design_system.sql +23 -0
  148. package/src/be/seed/schedules-seeder.ts +0 -10
  149. package/src/be/seed-scripts/catalog/catalog-report.inline.ts +2 -0
  150. package/src/be/seed-scripts/catalog/catalog-report.ts +2 -0
  151. package/src/be/seed-scripts/catalog/memory-eval.ts +2 -0
  152. package/src/be/seed-scripts/catalog/swarm-update-check.ts +150 -0
  153. package/src/be/seed-scripts/index.ts +9 -0
  154. package/src/be/seed-skills/bundled-files.generated.json +3 -3
  155. package/src/be/swarm-config-guard.ts +32 -0
  156. package/src/be/task-auto-title-listener.ts +127 -0
  157. package/src/commands/onboard/agent-fs-version.ts +1 -1
  158. package/src/commands/profile-sync.ts +151 -16
  159. package/src/commands/runner.ts +15 -7
  160. package/src/hooks/hook.ts +20 -9
  161. package/src/http/index.ts +3 -0
  162. package/src/http/pages-public.ts +93 -129
  163. package/src/http/pages.ts +94 -21
  164. package/src/pages/browser-sdk.ts +48 -21
  165. package/src/pages/color-literals.ts +115 -0
  166. package/src/pages/page-head.ts +766 -0
  167. package/src/pages/version.ts +3 -0
  168. package/src/providers/amp-adapter.ts +1 -1
  169. package/src/providers/codex-adapter.ts +1 -0
  170. package/src/providers/codex-app-server.ts +27 -1
  171. package/src/providers/pi-mono-extension.ts +30 -15
  172. package/src/tasks/follow-up-gate.ts +318 -0
  173. package/src/tasks/worker-follow-up.ts +21 -1
  174. package/src/tests/codex-app-server.test.ts +59 -0
  175. package/src/tests/codex-gateway-route.test.ts +50 -1
  176. package/src/tests/create-page-tool.test.ts +501 -0
  177. package/src/tests/db-client-busy-retry.test.ts +81 -0
  178. package/src/tests/db-task-read-contract.test.ts +1 -0
  179. package/src/tests/db-task-write-contract.test.ts +2 -0
  180. package/src/tests/db-tasks-read.test.ts +21 -0
  181. package/src/tests/fixtures/pages/head-v1.html +56 -0
  182. package/src/tests/follow-up-gate-shadow.test.ts +513 -0
  183. package/src/tests/identity-field-budget.test.ts +1 -1
  184. package/src/tests/internal-ai/credentials.test.ts +62 -0
  185. package/src/tests/internal-ai/task-title.test.ts +283 -0
  186. package/src/tests/merge-gate-detect-changes.test.ts +145 -0
  187. package/src/tests/page-proxy.test.ts +1 -1
  188. package/src/tests/page-session-dev-cookie.test.ts +188 -0
  189. package/src/tests/pages-frame-ancestors.test.ts +28 -1
  190. package/src/tests/pages-head-v2.test.ts +534 -0
  191. package/src/tests/pages-http.test.ts +330 -0
  192. package/src/tests/pages-public-html.test.ts +43 -0
  193. package/src/tests/pages-storage.test.ts +182 -0
  194. package/src/tests/pages-versioning.test.ts +72 -0
  195. package/src/tests/profile-sync-stale-writeback.test.ts +334 -0
  196. package/src/tests/profile-sync.test.ts +11 -15
  197. package/src/tests/scheduled-task-required-params-migration.test.ts +3 -1
  198. package/src/tests/script-connections.test.ts +2 -0
  199. package/src/tests/seed-registry.test.ts +28 -6
  200. package/src/tests/seed-schedules.test.ts +61 -8
  201. package/src/tests/seed-scripts.test.ts +142 -2
  202. package/src/tests/swarm-diff.test.ts +121 -2
  203. package/src/tests/task-auto-title-config-guard.test.ts +37 -0
  204. package/src/tests/task-auto-title-listener.test.ts +165 -0
  205. package/src/tests/theme-import.test.ts +758 -0
  206. package/src/tests/themes-page-theme.test.ts +155 -0
  207. package/src/tests/themes-presets.test.ts +172 -0
  208. package/src/tests/workflow-script-principal.test.ts +2 -1
  209. package/src/themes/classics.ts +949 -0
  210. package/src/themes/import.ts +420 -0
  211. package/src/themes/page-fields.ts +40 -0
  212. package/src/themes/page-theme.ts +102 -0
  213. package/src/themes/presets.ts +561 -0
  214. package/src/themes/tokens.ts +218 -0
  215. package/src/tools/app-upsert.ts +1 -1
  216. package/src/tools/create-page.ts +118 -12
  217. package/src/tools/utils.ts +33 -0
  218. package/src/types.ts +45 -3
  219. package/src/utils/constants.ts +19 -0
  220. package/src/utils/follow-up-gate-schedule-ids.ts +13 -0
  221. package/src/utils/internal-ai/credentials.ts +146 -93
  222. package/src/utils/internal-ai/index.ts +10 -0
  223. package/src/utils/internal-ai/models.ts +18 -0
  224. package/src/utils/internal-ai/task-title.ts +282 -0
  225. package/src/utils/page-session.ts +37 -0
  226. package/templates/schedules/daily-status-report/content.md +88 -2
  227. package/templates/schedules/daily-swarm-update-check/content.md +16 -40
  228. package/templates/skills/apps/SKILL.md +1 -1
  229. package/templates/skills/apps/content.md +1 -1
  230. package/templates/skills/heartbeat-runbook/content.md +1 -1
  231. package/templates/skills/pages/config.json +1 -1
  232. package/templates/skills/pages/content.md +539 -86
  233. package/templates/skills/pages/files/examples/annotated-pr.html +21 -12
  234. package/templates/skills/pages/files/examples/multiplayer-board.html +41 -41
  235. package/templates/skills/pages/files/examples/report-page.html +125 -124
  236. package/dist/cli-2pnswgh8.js +0 -189
  237. package/dist/handlers-by3sfxyx.js +0 -32
  238. package/dist/index-4f2pvxae.js +0 -37
  239. package/dist/index-98gz7tmp.js +0 -30
  240. package/dist/index-k08z2cak.js +0 -33
  241. package/dist/lead-enq35nqp.js +0 -70
  242. package/dist/worker-gg1576j3.js +0 -70
  243. package/templates/schedules/daily-workflow-health-audit/config.json +0 -19
  244. package/templates/schedules/daily-workflow-health-audit/content.md +0 -191
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Page theme token contract: the dashboard token names that HTML pages
3
+ * (design version 2) expose.
4
+ *
5
+ * A page's look comes only from these custom properties. Presets, per-page
6
+ * overrides and imported themes all resolve to this set, so one switch
7
+ * restyles the whole page. Sidebar, action, peer, type-scale and easing
8
+ * tokens are dashboard-only and stay out of the contract.
9
+ */
10
+
11
+ /** Color tokens: one value per mode. */
12
+ export const PAGE_THEME_COLOR_TOKENS = [
13
+ "--color-background",
14
+ "--color-foreground",
15
+ "--color-card",
16
+ "--color-card-foreground",
17
+ "--color-surface",
18
+ "--color-popover",
19
+ "--color-popover-foreground",
20
+ "--color-primary",
21
+ "--color-primary-foreground",
22
+ "--color-secondary",
23
+ "--color-secondary-foreground",
24
+ "--color-muted",
25
+ "--color-muted-foreground",
26
+ "--color-accent",
27
+ "--color-accent-foreground",
28
+ "--color-destructive",
29
+ "--color-destructive-foreground",
30
+ "--color-border",
31
+ "--color-border-subtle",
32
+ "--color-input",
33
+ "--color-ring",
34
+ "--color-chart-1",
35
+ "--color-chart-2",
36
+ "--color-chart-3",
37
+ "--color-chart-4",
38
+ "--color-chart-5",
39
+ "--color-status-success",
40
+ "--color-status-success-strong",
41
+ "--color-status-success-foreground",
42
+ "--color-status-active",
43
+ "--color-status-active-strong",
44
+ "--color-status-active-foreground",
45
+ "--color-status-error",
46
+ "--color-status-error-strong",
47
+ "--color-status-error-foreground",
48
+ "--color-status-info",
49
+ "--color-status-info-strong",
50
+ "--color-status-info-foreground",
51
+ "--color-status-pending",
52
+ "--color-status-pending-strong",
53
+ "--color-status-pending-foreground",
54
+ "--color-status-warning",
55
+ "--color-status-warning-strong",
56
+ "--color-status-warning-foreground",
57
+ "--color-status-paused",
58
+ "--color-status-paused-strong",
59
+ "--color-status-paused-foreground",
60
+ "--color-status-neutral",
61
+ "--color-status-neutral-strong",
62
+ "--color-status-neutral-foreground",
63
+ "--color-status-success-solid",
64
+ "--color-status-active-solid",
65
+ ] as const;
66
+
67
+ /** Font and radius tokens: one value for both modes. */
68
+ export const PAGE_THEME_SHARED_TOKENS = [
69
+ "--font-sans",
70
+ "--font-mono",
71
+ "--radius-sm",
72
+ "--radius-md",
73
+ "--radius-lg",
74
+ "--radius-xl",
75
+ ] as const;
76
+
77
+ export const PAGE_THEME_TOKENS = [...PAGE_THEME_COLOR_TOKENS, ...PAGE_THEME_SHARED_TOKENS] as const;
78
+
79
+ export type PageThemeToken = (typeof PAGE_THEME_TOKENS)[number];
80
+ export type PageThemeTokenValues = Partial<Record<PageThemeToken, string>>;
81
+ export type PageThemeTokens = { light: PageThemeTokenValues; dark: PageThemeTokenValues };
82
+
83
+ type PageThemeColorToken = (typeof PAGE_THEME_COLOR_TOKENS)[number];
84
+ type PageThemeSharedToken = (typeof PAGE_THEME_SHARED_TOKENS)[number];
85
+
86
+ /** Hive values from the light `@theme` block of `apps/ui/src/styles/globals.css`. */
87
+ const HIVE_SHARED_TOKENS: Record<PageThemeSharedToken, string> = {
88
+ "--font-sans": '"Space Grotesk", sans-serif',
89
+ "--font-mono": '"Space Mono", monospace',
90
+ "--radius-sm": "0.375rem",
91
+ "--radius-md": "0.5rem",
92
+ "--radius-lg": "0.625rem",
93
+ "--radius-xl": "0.625rem",
94
+ };
95
+
96
+ const HIVE_LIGHT_COLORS: Record<PageThemeColorToken, string> = {
97
+ "--color-background": "oklch(1 0 0)",
98
+ "--color-foreground": "oklch(0.141 0.005 285.823)",
99
+ "--color-card": "oklch(1 0 0)",
100
+ "--color-card-foreground": "oklch(0.141 0.005 285.823)",
101
+ "--color-surface": "oklch(0.985 0.0015 286)",
102
+ "--color-popover": "oklch(1 0 0)",
103
+ "--color-popover-foreground": "oklch(0.141 0.005 285.823)",
104
+ "--color-primary": "oklch(0.555 0.163 48.998)",
105
+ "--color-primary-foreground": "oklch(0.985 0 0)",
106
+ "--color-secondary": "oklch(0.967 0.001 286.375)",
107
+ "--color-secondary-foreground": "oklch(0.21 0.006 285.885)",
108
+ "--color-muted": "oklch(0.967 0.001 286.375)",
109
+ "--color-muted-foreground": "oklch(0.552 0.016 285.938)",
110
+ "--color-accent": "oklch(0.943 0.003 286.375)",
111
+ "--color-accent-foreground": "oklch(0.21 0.006 285.885)",
112
+ "--color-destructive": "oklch(0.577 0.245 27.325)",
113
+ "--color-destructive-foreground": "oklch(0.985 0 0)",
114
+ "--color-border": "oklch(0.92 0.004 286.32)",
115
+ "--color-border-subtle": "oklch(0.945 0.003 286)",
116
+ "--color-input": "oklch(0.92 0.004 286.32)",
117
+ "--color-ring": "oklch(0.555 0.163 48.998)",
118
+ "--color-chart-1": "oklch(0.646 0.222 41.116)",
119
+ "--color-chart-2": "oklch(0.6 0.118 184.704)",
120
+ "--color-chart-3": "oklch(0.398 0.07 227.392)",
121
+ "--color-chart-4": "oklch(0.828 0.189 84.429)",
122
+ "--color-chart-5": "oklch(0.769 0.188 70.08)",
123
+ "--color-status-success": "oklch(0.74 0.1 163)",
124
+ "--color-status-success-strong": "oklch(0.5 0.09 163)",
125
+ "--color-status-success-foreground": "oklch(0.21 0.006 285.885)",
126
+ "--color-status-active": "oklch(0.81 0.11 75)",
127
+ "--color-status-active-strong": "oklch(0.55 0.1 68)",
128
+ "--color-status-active-foreground": "oklch(0.21 0.006 285.885)",
129
+ "--color-status-error": "oklch(0.72 0.12 25)",
130
+ "--color-status-error-strong": "oklch(0.51 0.14 25)",
131
+ "--color-status-error-foreground": "oklch(0.21 0.006 285.885)",
132
+ "--color-status-info": "oklch(0.74 0.09 235)",
133
+ "--color-status-info-strong": "oklch(0.5 0.09 240)",
134
+ "--color-status-info-foreground": "oklch(0.21 0.006 285.885)",
135
+ "--color-status-pending": "oklch(0.84 0.1 95)",
136
+ "--color-status-pending-strong": "oklch(0.55 0.09 90)",
137
+ "--color-status-pending-foreground": "oklch(0.21 0.006 285.885)",
138
+ "--color-status-warning": "oklch(0.76 0.11 55)",
139
+ "--color-status-warning-strong": "oklch(0.53 0.11 50)",
140
+ "--color-status-warning-foreground": "oklch(0.21 0.006 285.885)",
141
+ "--color-status-paused": "oklch(0.71 0.1 262)",
142
+ "--color-status-paused-strong": "oklch(0.5 0.1 262)",
143
+ "--color-status-paused-foreground": "oklch(0.21 0.006 285.885)",
144
+ "--color-status-neutral": "oklch(0.62 0.014 286)",
145
+ "--color-status-neutral-strong": "oklch(0.48 0.014 286)",
146
+ "--color-status-neutral-foreground": "oklch(0.21 0.006 285.885)",
147
+ "--color-status-success-solid": "oklch(0.627 0.194 149.2)",
148
+ "--color-status-active-solid": "oklch(0.666 0.179 58.318)",
149
+ };
150
+
151
+ const HIVE_DARK_COLORS: Record<PageThemeColorToken, string> = {
152
+ "--color-background": "oklch(0.141 0.005 285.823)",
153
+ "--color-foreground": "oklch(0.985 0 0)",
154
+ "--color-card": "oklch(0.21 0.006 285.885)",
155
+ "--color-card-foreground": "oklch(0.985 0 0)",
156
+ "--color-surface": "oklch(0.185 0.006 286)",
157
+ "--color-popover": "oklch(0.21 0.006 285.885)",
158
+ "--color-popover-foreground": "oklch(0.985 0 0)",
159
+ "--color-primary": "oklch(0.769 0.188 70.08)",
160
+ "--color-primary-foreground": "oklch(0.21 0.006 285.885)",
161
+ "--color-secondary": "oklch(0.274 0.006 286.033)",
162
+ "--color-secondary-foreground": "oklch(0.985 0 0)",
163
+ "--color-muted": "oklch(0.274 0.006 286.033)",
164
+ "--color-muted-foreground": "oklch(0.705 0.015 286.067)",
165
+ "--color-accent": "oklch(0.274 0.006 286.033)",
166
+ "--color-accent-foreground": "oklch(0.985 0 0)",
167
+ "--color-destructive": "oklch(0.704 0.191 22.216)",
168
+ "--color-destructive-foreground": "oklch(0.985 0 0)",
169
+ "--color-border": "oklch(1 0 0 / 10%)",
170
+ "--color-border-subtle": "oklch(1 0 0 / 8%)",
171
+ "--color-input": "oklch(1 0 0 / 15%)",
172
+ "--color-ring": "oklch(0.769 0.188 70.08)",
173
+ "--color-chart-1": "oklch(0.488 0.243 264.376)",
174
+ "--color-chart-2": "oklch(0.696 0.17 162.48)",
175
+ "--color-chart-3": "oklch(0.769 0.188 70.08)",
176
+ "--color-chart-4": "oklch(0.627 0.265 303.9)",
177
+ "--color-chart-5": "oklch(0.645 0.246 16.439)",
178
+ "--color-status-success": "oklch(0.78 0.1 163)",
179
+ "--color-status-success-strong": "oklch(0.78 0.1 163)",
180
+ "--color-status-success-foreground": "oklch(0.21 0.006 285.885)",
181
+ "--color-status-active": "oklch(0.84 0.1 80)",
182
+ "--color-status-active-strong": "oklch(0.84 0.1 80)",
183
+ "--color-status-active-foreground": "oklch(0.21 0.006 285.885)",
184
+ "--color-status-error": "oklch(0.74 0.11 22)",
185
+ "--color-status-error-strong": "oklch(0.74 0.11 22)",
186
+ "--color-status-error-foreground": "oklch(0.21 0.006 285.885)",
187
+ "--color-status-info": "oklch(0.78 0.09 235)",
188
+ "--color-status-info-strong": "oklch(0.78 0.09 235)",
189
+ "--color-status-info-foreground": "oklch(0.21 0.006 285.885)",
190
+ "--color-status-pending": "oklch(0.86 0.09 95)",
191
+ "--color-status-pending-strong": "oklch(0.86 0.09 95)",
192
+ "--color-status-pending-foreground": "oklch(0.21 0.006 285.885)",
193
+ "--color-status-warning": "oklch(0.79 0.1 55)",
194
+ "--color-status-warning-strong": "oklch(0.79 0.1 55)",
195
+ "--color-status-warning-foreground": "oklch(0.21 0.006 285.885)",
196
+ "--color-status-paused": "oklch(0.74 0.09 260)",
197
+ "--color-status-paused-strong": "oklch(0.74 0.09 260)",
198
+ "--color-status-paused-foreground": "oklch(0.21 0.006 285.885)",
199
+ "--color-status-neutral": "oklch(0.72 0.012 286)",
200
+ "--color-status-neutral-strong": "oklch(0.72 0.012 286)",
201
+ "--color-status-neutral-foreground": "oklch(0.21 0.006 285.885)",
202
+ "--color-status-success-solid": "oklch(0.723 0.219 149.6)",
203
+ "--color-status-active-solid": "oklch(0.769 0.188 70.08)",
204
+ };
205
+
206
+ /**
207
+ * Full Hive values for every contract token, copied from the light `@theme`
208
+ * and `.dark` blocks of `apps/ui/src/styles/globals.css` (keep in sync;
209
+ * `themes-presets.test.ts` fails on drift). The shared font and radius
210
+ * tokens come from the light block and apply to both modes.
211
+ */
212
+ export const HIVE_PAGE_TOKENS: {
213
+ light: Record<PageThemeToken, string>;
214
+ dark: Record<PageThemeToken, string>;
215
+ } = {
216
+ light: { ...HIVE_LIGHT_COLORS, ...HIVE_SHARED_TOKENS },
217
+ dark: { ...HIVE_DARK_COLORS, ...HIVE_SHARED_TOKENS },
218
+ };
@@ -24,7 +24,7 @@ export const registerAppUpsertTool = (server: McpServer) => {
24
24
  {
25
25
  title: "Create or update an app",
26
26
  description:
27
- "Stores a schema-backed app definition with models, queries/actions, pages, reusable elements, optional userConfig fields, and an optional top-level theme (a dashboard preset slug for the app's canvas — hive (stock; omit to inherit the viewer's theme), meadow, iris, rose, cobalt, ember, carbon, plus the classic presets github, vscode, material, solarized, tokyo, monokai, gruvbox, catppuccin; viewers can override it per-user; unknown slugs degrade to the viewer's dashboard theme), then returns its dashboard URL. userConfig is versioned schema only; each user's values are stored separately, survive rollback, and schema changes are always compatible. Pages may bind a declared field read-only at exactly /user/<field>; pure and bound reusable elements must receive that value through a prop. Elements are private by default: pure elements read declared props, allow $item/$index inside repeats, may expose one leaf ElementSlot, and cannot invoke actions; bound elements may use the defining app's queries/actions, while exported bound elements cannot navigate. Prop kinds include enum with a required non-empty enum values array. Pages or elements reuse them with literal ElementRef targets, and cross-app refs require export: true. Zero-model pure-UI apps are valid. Pass appId to update; breaking a referenced export is blocked by the compatibility gate unless forceElementBreak explicitly names it.",
27
+ "Stores a schema-backed app definition with models, queries/actions, pages, reusable elements, optional userConfig fields, and an optional top-level theme (a dashboard preset slug for the app's canvas: hive (stock; omit to inherit the viewer's theme), meadow, iris, rose, cobalt, ember, carbon, paper (the warm editorial paper look), plus the classic presets github, vscode, material, solarized, tokyo, monokai, gruvbox, catppuccin; viewers can override it per-user; unknown slugs degrade to the viewer's dashboard theme), then returns its dashboard URL. userConfig is versioned schema only; each user's values are stored separately, survive rollback, and schema changes are always compatible. Pages may bind a declared field read-only at exactly /user/<field>; pure and bound reusable elements must receive that value through a prop. Elements are private by default: pure elements read declared props, allow $item/$index inside repeats, may expose one leaf ElementSlot, and cannot invoke actions; bound elements may use the defining app's queries/actions, while exported bound elements cannot navigate. Prop kinds include enum with a required non-empty enum values array. Pages or elements reuse them with literal ElementRef targets, and cross-app refs require export: true. Zero-model pure-UI apps are valid. Pass appId to update; breaking a referenced export is blocked by the compatibility gate unless forceElementBreak explicitly names it.",
28
28
  annotations: { destructiveHint: false },
29
29
  inputSchema: z.object({
30
30
  name: z.string().min(1).describe("Human-readable app name."),
@@ -12,6 +12,10 @@
12
12
  * the title slugifies to empty (e.g. all symbols), fall back to the
13
13
  * generated page id.
14
14
  *
15
+ * Design fields: `theme`, `themeTokens` and `designVersion` set how an HTML
16
+ * page is styled. A new page gets designVersion 2. On an upsert, an omitted
17
+ * field keeps the stored value, and `null` clears `theme` / `themeTokens`.
18
+ *
15
19
  * Architecture note: this tool runs on the API server (per
16
20
  * `src/server.ts:155`+ pattern) and accesses `src/be/db` directly. The
17
21
  * worker-side DB-boundary invariant doesn't apply because `src/tools/*`
@@ -23,9 +27,18 @@ import { authorizeAssetKeyWrite } from "@/be/asset-key-auth";
23
27
  import { resolveTaskAuditUserId } from "@/be/audit-user";
24
28
  import { createPage, getPage, getPageBySlug, updatePage } from "@/be/db";
25
29
  import { getPageEditCounter } from "@/be/db/page-versions";
30
+ import { classAttributeValues, countColorLiterals } from "@/pages/color-literals";
26
31
  import { snapshotPage } from "@/pages/version";
32
+ import { readThemeFields } from "@/themes/page-fields";
33
+ import { PAGE_THEME_COLOR_TOKENS } from "@/themes/tokens";
27
34
  import { createToolRegistrar, swarmToolOutputSchema, toolErr, toolOk } from "@/tools/utils";
28
- import { AssetKeySchema, PageAuthModeSchema, PageContentTypeSchema } from "@/types";
35
+ import {
36
+ AssetKeySchema,
37
+ type Page,
38
+ PageAuthModeSchema,
39
+ PageContentTypeSchema,
40
+ PageThemeTokensInputSchema,
41
+ } from "@/types";
29
42
  import { getAppUrl, getPublicMcpBaseUrl } from "@/utils/constants";
30
43
 
31
44
  /** Same slugifier used by the HTTP createPage handler. */
@@ -46,6 +59,26 @@ function getAppBaseUrl(): string {
46
59
  return getAppUrl();
47
60
  }
48
61
 
62
+ /** A `swarm-` primitive class inside one class attribute value. */
63
+ const SWARM_CLASS = /(?:^|\s)swarm-/;
64
+ const COLOR_TOKEN_REFERENCE = /var\(\s*--color-/i;
65
+ const PAGE_COLOR_TOKENS: ReadonlySet<string> = new Set<string>(PAGE_THEME_COLOR_TOKENS);
66
+
67
+ /** The body uses a `swarm-` class or a `var(--color-...)` token, which only design version 2 styles. */
68
+ function bodyUsesDesignSystem(html: string): boolean {
69
+ return (
70
+ COLOR_TOKEN_REFERENCE.test(html) ||
71
+ classAttributeValues(html).some((value) => SWARM_CLASS.test(value))
72
+ );
73
+ }
74
+
75
+ /** The stored overrides set light colors and no dark colors, so dark mode keeps the preset's colors. */
76
+ function hasLightOnlyColors(tokens: Page["themeTokens"]): boolean {
77
+ if (!tokens) return false;
78
+ const isColor = (name: string) => PAGE_COLOR_TOKENS.has(name);
79
+ return Object.keys(tokens.light).some(isColor) && !Object.keys(tokens.dark).some(isColor);
80
+ }
81
+
49
82
  export const registerCreatePageTool = (server: McpServer) => {
50
83
  createToolRegistrar(server)(
51
84
  "create_page",
@@ -56,7 +89,15 @@ export const registerCreatePageTool = (server: McpServer) => {
56
89
  "Calls are upsert-by-(agent, slug): if you previously created a page " +
57
90
  "with the same slug, its prior state is snapshotted and the row is " +
58
91
  "updated. Use this for static reports, dashboards, or JSON action " +
59
- "specs that don't need a long-lived process.",
92
+ "specs that don't need a long-lived process. " +
93
+ "HTML pages use the swarm design system (designVersion 2): theme " +
94
+ "tokens such as var(--color-primary), the swarm-* primitives, and " +
95
+ "Tailwind v4 utilities such as bg-card and text-muted-foreground. " +
96
+ "Set `theme` to a preset id or pass `themeTokens` (a token map, or a " +
97
+ 'CSS variable block with format "css") to restyle the whole page. ' +
98
+ "Do not hardcode colors: inside the dashboard the page follows the " +
99
+ "viewer's theme and dark mode. Pages created before designVersion 2 " +
100
+ "keep the old look; pass designVersion: 2 when you rewrite one.",
60
101
  annotations: { destructiveHint: false },
61
102
  inputSchema: z.object({
62
103
  key: AssetKeySchema.optional().describe(
@@ -99,6 +140,27 @@ export const registerCreatePageTool = (server: McpServer) => {
99
140
  .describe(
100
141
  "Declared credential needs for JSON pages (renderer ignores for v1 — reserved for follow-up).",
101
142
  ),
143
+ theme: z
144
+ .string()
145
+ .nullable()
146
+ .optional()
147
+ .describe(
148
+ "Theme preset id that restyles the whole page, for example paper, github or hive. An unknown id returns an error that lists every preset. Omit to follow the viewer's theme (on update, to keep the stored one). Pass null to clear it.",
149
+ ),
150
+ themeTokens: PageThemeTokensInputSchema.nullable()
151
+ .optional()
152
+ .describe(
153
+ 'Per-page token overrides, applied on top of the preset. Send { light, dark } maps of token name to CSS value (for example { light: { "--color-primary": "oklch(0.55 0.2 260)" } }), or { format: "css", css } with a CSS variable block (:root, .dark, @theme) as shadcn, tweakcn and Tailwind v4 export it. Stored normalized. Names outside the token contract, and values that reference a variable outside it, are ignored and reported in the result. Replaces the stored overrides. Omit to keep them on update. Pass null to clear them.',
154
+ ),
155
+ designVersion: z
156
+ .number()
157
+ .int()
158
+ .min(1)
159
+ .max(2)
160
+ .optional()
161
+ .describe(
162
+ "Design system version. 2 is the swarm design system and the default for new pages. 1 is the original look that older pages keep. Omit on update to keep the stored value.",
163
+ ),
102
164
  }),
103
165
  outputSchema: swarmToolOutputSchema({
104
166
  yourAgentId: z.string().optional(),
@@ -107,6 +169,11 @@ export const registerCreatePageTool = (server: McpServer) => {
107
169
  version: z.number().optional(),
108
170
  app_url: z.string().optional(),
109
171
  api_url: z.string().optional(),
172
+ designVersion: z.number().optional(),
173
+ ignoredTokens: z.array(z.string()).optional(),
174
+ colorLiteralCount: z.number().optional(),
175
+ usesDesignSystem: z.boolean().optional(),
176
+ lightOnlyColors: z.boolean().optional(),
110
177
  }),
111
178
  },
112
179
  async (input, requestInfo, _meta) => {
@@ -115,6 +182,9 @@ export const registerCreatePageTool = (server: McpServer) => {
115
182
  return toolErr(msg);
116
183
  }
117
184
 
185
+ const themeFields = readThemeFields(input);
186
+ if ("error" in themeFields) return toolErr(themeFields.error);
187
+
118
188
  const slug = input.slug ?? slugify(input.title);
119
189
  const finalSlug = slug || "page"; // Fallback if title slugifies to empty.
120
190
 
@@ -167,6 +237,10 @@ export const registerCreatePageTool = (server: McpServer) => {
167
237
  passwordHash: passwordHash ?? null,
168
238
  body: input.body,
169
239
  needsCredentials: needsCredentialsNames ?? null,
240
+ // Omitted design fields keep the stored values.
241
+ theme: themeFields.theme,
242
+ themeTokens: themeFields.themeTokens,
243
+ designVersion: input.designVersion,
170
244
  });
171
245
  if (!updated) {
172
246
  const msg = `Failed to update existing page ${existing.id}.`;
@@ -188,6 +262,9 @@ export const registerCreatePageTool = (server: McpServer) => {
188
262
  passwordHash,
189
263
  body: input.body,
190
264
  needsCredentials: needsCredentialsNames,
265
+ theme: themeFields.theme,
266
+ themeTokens: themeFields.themeTokens,
267
+ designVersion: input.designVersion,
191
268
  });
192
269
  id = created.id;
193
270
  } catch (err) {
@@ -209,17 +286,46 @@ export const registerCreatePageTool = (server: McpServer) => {
209
286
  const appUrl = `${getAppBaseUrl()}/pages/${id}`;
210
287
  const version = await getPageEditCounter(id);
211
288
 
212
- return toolOk(`Page "${input.title}" saved (slug=${finalSlug}, version=${version}).`, {
213
- details: `API: ${apiUrl}\n App: ${appUrl}`,
214
- data: {
215
- yourAgentId: requestInfo.agentId,
216
- id,
217
- key: fresh.key,
218
- version,
219
- app_url: appUrl,
220
- api_url: apiUrl,
289
+ // Judged on the saved page, so an upsert that keeps an older design
290
+ // version is judged by the version it keeps. The NUDGES entry for this
291
+ // tool reads these flags. Only HTML pages get the design system head.
292
+ const designFlags =
293
+ fresh.contentType === "text/html"
294
+ ? {
295
+ usesDesignSystem:
296
+ typeof input.theme === "string" ||
297
+ Boolean(input.themeTokens) ||
298
+ bodyUsesDesignSystem(fresh.body),
299
+ lightOnlyColors: hasLightOnlyColors(fresh.themeTokens),
300
+ ...(fresh.designVersion >= 2
301
+ ? { colorLiteralCount: countColorLiterals(fresh.body) }
302
+ : {}),
303
+ }
304
+ : {};
305
+ const { ignored } = themeFields;
306
+
307
+ return toolOk(
308
+ `Page "${input.title}" saved (slug=${finalSlug}, version=${version}, designVersion=${fresh.designVersion}).` +
309
+ (ignored.length > 0
310
+ ? " Some theme tokens were ignored (outside the page token contract or referencing a variable outside it)."
311
+ : ""),
312
+ {
313
+ details:
314
+ `API: ${apiUrl}\n App: ${appUrl}` +
315
+ (ignored.length > 0 ? `\n Ignored theme tokens: ${ignored.join(", ")}` : ""),
316
+ data: {
317
+ yourAgentId: requestInfo.agentId,
318
+ id,
319
+ key: fresh.key,
320
+ version,
321
+ app_url: appUrl,
322
+ api_url: apiUrl,
323
+ designVersion: fresh.designVersion,
324
+ ...(ignored.length > 0 ? { ignoredTokens: ignored } : {}),
325
+ ...designFlags,
326
+ },
221
327
  },
222
- });
328
+ );
223
329
  },
224
330
  );
225
331
  };
@@ -22,6 +22,7 @@ import { upsertKv } from "../be/db";
22
22
  import { MCP_OVERFLOW_NAMESPACE, mcpOverflowNamespace } from "../kv-overflow";
23
23
  import { joinKvPath, type KvShapeEntry, splitKvPath, summarizeKvShape } from "../kv-view";
24
24
  import { withSpan } from "../otel";
25
+ import { COLOR_LITERAL_THRESHOLD } from "../pages/color-literals";
25
26
  import type { PermissionVerb } from "../rbac/permissions";
26
27
  import { SCRIPT_LONG_TIMEOUT_HINT_MS } from "../scripts-runtime/executors/types";
27
28
  import { scrubObject, scrubSecrets } from "../utils/secret-scrubber";
@@ -314,6 +315,37 @@ const workflowLongScriptTimeoutNudge = (r: SwarmToolResult): string | undefined
314
315
  return hint ? WORKFLOW_LONG_SCRIPT_TIMEOUT_NUDGE : undefined;
315
316
  };
316
317
 
318
+ /**
319
+ * One call can fall into more than one create_page trap, and a result carries
320
+ * one nudge, so this entry joins one sentence per trap that applies: a
321
+ * design version 1 page that uses the design system, theme tokens that set
322
+ * light colors only, and hardcoded colors. The tool computes the flags.
323
+ */
324
+ const createPageNudge = (r: SwarmToolResult): string | undefined => {
325
+ if (!r.ok) return undefined;
326
+ const data = r.data as
327
+ | {
328
+ designVersion?: unknown;
329
+ usesDesignSystem?: unknown;
330
+ lightOnlyColors?: unknown;
331
+ colorLiteralCount?: unknown;
332
+ }
333
+ | undefined;
334
+ const count = data?.colorLiteralCount;
335
+ const sentences = [
336
+ data?.designVersion === 1 && data.usesDesignSystem === true
337
+ ? "This page is on designVersion 1 (the original look), so design system classes, tokens and themes have no effect. Pass designVersion: 2."
338
+ : undefined,
339
+ data?.lightOnlyColors === true
340
+ ? "Your themeTokens set colors for light mode only. Dark mode keeps the preset's dark colors; add a dark map (or a .dark block) to restyle it too."
341
+ : undefined,
342
+ typeof count === "number" && count >= COLOR_LITERAL_THRESHOLD
343
+ ? `This page hardcodes ${count} color values. Use theme tokens (var(--color-primary), bg-primary, text-muted-foreground) so theme and dark-mode switches restyle it. Until then the page stays light in a dark dashboard.`
344
+ : undefined,
345
+ ].filter((sentence) => sentence !== undefined);
346
+ return sentences.length > 0 ? sentences.join(" ") : undefined;
347
+ };
348
+
317
349
  /**
318
350
  * Central conditional nudges, keyed by tool name. Applied by the finalize
319
351
  * pipeline when the tool did not set an explicit nudge. Keep entries to a
@@ -333,6 +365,7 @@ export const NUDGES: Record<string, (result: SwarmToolResult) => string | undefi
333
365
  "patch-workflow-node": workflowLongScriptTimeoutNudge,
334
366
  "slack-download-file": slackDownloadFileNudge,
335
367
  "slack-read": slackReadNudge,
368
+ create_page: createPageNudge,
336
369
  "script-search": (r) => {
337
370
  if (!r.ok) return undefined;
338
371
  // proxyScriptsApi wraps the parsed HTTP body as data = { status, data },
package/src/types.ts CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  import { normalizeAssetKey } from "./assets/key";
6
6
  import { isSafeBundlePath } from "./extensions/bundle-path";
7
+ import { THEME_IMPORT_FORMATS } from "./themes/import";
7
8
  import { MAX_PROFILE_FILE_LENGTH } from "./utils/constants";
8
9
  import { z } from "./utils/zod-openapi";
9
10
 
@@ -1695,6 +1696,8 @@ export const AgentLogEventTypeSchema = z.enum([
1695
1696
  "task_deferred_wait_woke",
1696
1697
  // The lead follow-up was skipped because the woken waiter already carries the result
1697
1698
  "task_follow_up_suppressed",
1699
+ // Shadow follow-up gate: what a verifier said about a gated run (never skips the follow-up)
1700
+ "task_follow_up_gate_shadow",
1698
1701
  // store-progress refused a completion once because its citations were inaccurate
1699
1702
  "task_citation_check_refused",
1700
1703
  "channel_message",
@@ -2773,10 +2776,36 @@ export type PageContentType = z.infer<typeof PageContentTypeSchema>;
2773
2776
  export const PageAuthModeSchema = z.enum(["public", "authed", "password"]);
2774
2777
  export type PageAuthMode = z.infer<typeof PageAuthModeSchema>;
2775
2778
 
2779
+ // Per-page theme overrides as stored: page token names (`--color-primary`,
2780
+ // `--radius-lg`) mapped to CSS values, per mode. Writes go through
2781
+ // `importThemeTokens` (src/themes/import.ts), which maps outside names onto the
2782
+ // token contract and checks every value, so the stored form is always normalized.
2783
+ export const PageThemeTokensSchema = z
2784
+ .object({
2785
+ light: z.record(z.string(), z.string()),
2786
+ dark: z.record(z.string(), z.string()),
2787
+ })
2788
+ .openapi("PageThemeTokens");
2789
+
2790
+ // Write-side `themeTokens` input, mirroring `ThemeImportInput` (src/themes/import.ts)
2791
+ // as one flat object: the default `tokens` format sends `light` / `dark` maps,
2792
+ // the `css` format sends a CSS variable block. `importThemeTokens` checks which
2793
+ // fields go with which format, so a mismatch gets a precise error message. The
2794
+ // `format` enum is the importer's own list, so a new importer needs no change
2795
+ // here unless it takes a new payload field.
2796
+ export const PageThemeTokensInputSchema = z.strictObject({
2797
+ format: z.enum(THEME_IMPORT_FORMATS).optional(),
2798
+ light: z.record(z.string(), z.string()).optional(),
2799
+ dark: z.record(z.string(), z.string()).optional(),
2800
+ css: z.string().optional(),
2801
+ });
2802
+ export type PageThemeTokensInput = z.infer<typeof PageThemeTokensInputSchema>;
2803
+
2776
2804
  // PageSnapshot captures the mutable content fields frozen per-version in
2777
2805
  // page_versions.snapshot. Omits id / agentId / slug / timestamps (these are
2778
2806
  // invariant across versions for a given page id; the slug is a parent-only
2779
- // identifier).
2807
+ // identifier). The design fields are optional because snapshots taken before
2808
+ // design version 2 do not carry them (they read as design version 1).
2780
2809
  export const PageSnapshotSchema = z
2781
2810
  .object({
2782
2811
  title: z.string(),
@@ -2786,6 +2815,9 @@ export const PageSnapshotSchema = z
2786
2815
  passwordHash: z.string().optional(),
2787
2816
  body: z.string(),
2788
2817
  needsCredentials: z.array(z.string()).optional(),
2818
+ theme: z.string().nullable().optional(),
2819
+ themeTokens: PageThemeTokensSchema.nullable().optional(),
2820
+ designVersion: z.number().int().min(1).optional(),
2789
2821
  })
2790
2822
  .openapi("PageSnapshot");
2791
2823
  export type PageSnapshot = z.infer<typeof PageSnapshotSchema>;
@@ -2803,6 +2835,9 @@ export const PageSchema = z
2803
2835
  passwordHash: z.string().optional(),
2804
2836
  body: z.string(),
2805
2837
  needsCredentials: z.array(z.string()).optional(),
2838
+ theme: z.string().nullable(),
2839
+ themeTokens: PageThemeTokensSchema.nullable(),
2840
+ designVersion: z.number().int().min(1),
2806
2841
  viewCount: z.number().int().min(0).default(0),
2807
2842
  createdAt: z.string(),
2808
2843
  updatedAt: z.string(),
@@ -2826,8 +2861,15 @@ export type WorkflowSummary = Omit<
2826
2861
  "definition" | "triggers" | "cooldown" | "input" | "triggerSchema"
2827
2862
  > & { nodeCount: number };
2828
2863
 
2829
- /** `/api/pages` list item — drops the (potentially huge) `body` and `passwordHash`. */
2830
- export type PageSummary = Omit<Page, "body" | "passwordHash">;
2864
+ /**
2865
+ * `/api/pages` list item: drops the (potentially huge) `body` and `passwordHash`,
2866
+ * plus the design fields (`theme`, `themeTokens`, `designVersion`). List views do
2867
+ * not render a page, so those only come with `?fields=full` or the get-by-id route.
2868
+ */
2869
+ export type PageSummary = Omit<
2870
+ Page,
2871
+ "body" | "passwordHash" | "theme" | "themeTokens" | "designVersion"
2872
+ >;
2831
2873
 
2832
2874
  /** `/api/schedules` list item — swaps the full `taskTemplate` for a short preview. */
2833
2875
  export type ScheduledTaskSummary = Omit<ScheduledTask, "taskTemplate"> & {
@@ -3,6 +3,7 @@
3
3
  */
4
4
 
5
5
  import { isEnvFlagEnabled, parseEnvFlag } from "./env-flag";
6
+ import { isDevOrTestNodeEnv } from "./node-env";
6
7
 
7
8
  /** Maximum profile-file length in JavaScript characters (not UTF-8 bytes). */
8
9
  export const MAX_PROFILE_FILE_LENGTH = 256 * 1024;
@@ -45,6 +46,24 @@ export function getAppUrl(fallback = DEFAULT_APP_URL): string {
45
46
  return (getConfiguredAppUrls()[0] || fallback).replace(/\/+$/, "");
46
47
  }
47
48
 
49
+ /**
50
+ * Local dashboard origins (Vite ports and portless `*.localhost`) that may
51
+ * iframe pages without being listed in `APP_URL`. Returned only when NODE_ENV
52
+ * is explicitly development or test: deployed images leave NODE_ENV unset, and
53
+ * an unset value must not let a localhost origin frame production pages.
54
+ */
55
+ export function getLocalDevAppOrigins(): string[] {
56
+ if (!isDevOrTestNodeEnv()) return [];
57
+ return [
58
+ "http://localhost:5274",
59
+ "http://localhost:5175",
60
+ "http://127.0.0.1:5274",
61
+ "http://127.0.0.1:5175",
62
+ "https://*.localhost",
63
+ "http://*.localhost",
64
+ ];
65
+ }
66
+
48
67
  /**
49
68
  * Internal API/MCP base URL — how workers/agents and in-process callers reach
50
69
  * the API server. May be a private/cluster address (e.g. the Helm ClusterIP
@@ -0,0 +1,13 @@
1
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2
+
3
+ /** Split `FOLLOW_UP_GATE_SCHEDULE_IDS`, a comma-separated id list. Blank entries are dropped. */
4
+ export function parseFollowUpGateScheduleIds(raw: string | undefined): string[] {
5
+ return (raw ?? "")
6
+ .split(",")
7
+ .map((id) => id.trim())
8
+ .filter(Boolean);
9
+ }
10
+
11
+ export function isFollowUpGateScheduleId(id: string): boolean {
12
+ return UUID_RE.test(id);
13
+ }