@songsid/agend 2.2.0 → 2.3.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (271) hide show
  1. package/dist/agent-cli.js +248 -2
  2. package/dist/agent-cli.js.map +1 -1
  3. package/dist/agent-endpoint.d.ts +2 -0
  4. package/dist/agent-endpoint.js +25 -0
  5. package/dist/agent-endpoint.js.map +1 -1
  6. package/dist/agent-file-read.d.ts +30 -0
  7. package/dist/agent-file-read.js +129 -0
  8. package/dist/agent-file-read.js.map +1 -0
  9. package/dist/agent-whats-new.js +13 -0
  10. package/dist/agent-whats-new.js.map +1 -1
  11. package/dist/atomic-write.js +9 -1
  12. package/dist/atomic-write.js.map +1 -1
  13. package/dist/auth-api.js +17 -0
  14. package/dist/auth-api.js.map +1 -1
  15. package/dist/backend/antigravity.js +3 -3
  16. package/dist/backend/antigravity.js.map +1 -1
  17. package/dist/backend/launch-effort.js +1 -1
  18. package/dist/backend/launch-effort.js.map +1 -1
  19. package/dist/backend/muse.js +10 -2
  20. package/dist/backend/muse.js.map +1 -1
  21. package/dist/channel/adapters/discord.d.ts +2 -0
  22. package/dist/channel/adapters/discord.js +17 -2
  23. package/dist/channel/adapters/discord.js.map +1 -1
  24. package/dist/channel/adapters/telegram.d.ts +8 -1
  25. package/dist/channel/adapters/telegram.js +74 -4
  26. package/dist/channel/adapters/telegram.js.map +1 -1
  27. package/dist/channel/mcp-tools.js +34 -0
  28. package/dist/channel/mcp-tools.js.map +1 -1
  29. package/dist/channel/types.d.ts +15 -0
  30. package/dist/config-validator.js +17 -1
  31. package/dist/config-validator.js.map +1 -1
  32. package/dist/daemon.js +244 -20
  33. package/dist/daemon.js.map +1 -1
  34. package/dist/fleet-context.d.ts +5 -0
  35. package/dist/fleet-manager.d.ts +89 -0
  36. package/dist/fleet-manager.js +401 -25
  37. package/dist/fleet-manager.js.map +1 -1
  38. package/dist/general-knowledge/skills/web-appearance/SKILL.md +258 -0
  39. package/dist/instance-avatar.js +3 -0
  40. package/dist/instance-avatar.js.map +1 -1
  41. package/dist/instance-config-impact.js +3 -0
  42. package/dist/instance-config-impact.js.map +1 -1
  43. package/dist/instance-lifecycle.js +9 -4
  44. package/dist/instance-lifecycle.js.map +1 -1
  45. package/dist/needs-you-hub.d.ts +4 -0
  46. package/dist/needs-you-hub.js +31 -6
  47. package/dist/needs-you-hub.js.map +1 -1
  48. package/dist/needs-you.d.ts +1 -13
  49. package/dist/needs-you.js +3 -37
  50. package/dist/needs-you.js.map +1 -1
  51. package/dist/outbound-handlers.d.ts +5 -0
  52. package/dist/outbound-handlers.js +30 -9
  53. package/dist/outbound-handlers.js.map +1 -1
  54. package/dist/outbound-schemas.d.ts +64 -0
  55. package/dist/outbound-schemas.js +50 -0
  56. package/dist/outbound-schemas.js.map +1 -1
  57. package/dist/public-web-gateway.js +14 -0
  58. package/dist/public-web-gateway.js.map +1 -1
  59. package/dist/repo-worktree-guard.d.ts +36 -0
  60. package/dist/repo-worktree-guard.js +143 -0
  61. package/dist/repo-worktree-guard.js.map +1 -0
  62. package/dist/scheduler/scheduler.d.ts +58 -1
  63. package/dist/scheduler/scheduler.js +176 -10
  64. package/dist/scheduler/scheduler.js.map +1 -1
  65. package/dist/settings-api.js +74 -1
  66. package/dist/settings-api.js.map +1 -1
  67. package/dist/settings-change.js +8 -0
  68. package/dist/settings-change.js.map +1 -1
  69. package/dist/settings-effect.js +16 -2
  70. package/dist/settings-effect.js.map +1 -1
  71. package/dist/settings-http-confirmation.js +5 -0
  72. package/dist/settings-http-confirmation.js.map +1 -1
  73. package/dist/tool-permissions.js +50 -0
  74. package/dist/tool-permissions.js.map +1 -1
  75. package/dist/topic-commands.d.ts +10 -6
  76. package/dist/topic-commands.js +21 -17
  77. package/dist/topic-commands.js.map +1 -1
  78. package/dist/topic-link.d.ts +128 -0
  79. package/dist/topic-link.js +269 -0
  80. package/dist/topic-link.js.map +1 -0
  81. package/dist/types.d.ts +13 -0
  82. package/dist/ui/app-effects.js +187 -0
  83. package/dist/ui/chat-effect-posters.js +42 -0
  84. package/dist/ui/chat-effects.js +72 -0
  85. package/dist/ui/chat-live-replies.js +58 -0
  86. package/dist/ui/chat-render.js +4 -4
  87. package/dist/ui/chat-store.js +9 -3
  88. package/dist/ui/chat-strings.js +15 -0
  89. package/dist/ui/chat-thread.js +47 -21
  90. package/dist/ui/chat-tour.js +1 -1
  91. package/dist/ui/effect-trial-controller.js +119 -0
  92. package/dist/ui/effect-trial-offer.js +31 -0
  93. package/dist/ui/effect-trial-panel.js +37 -0
  94. package/dist/ui/image-lightbox.js +1 -1
  95. package/dist/ui/panel-chat.js +85 -17
  96. package/dist/ui/panel-details.js +18 -5
  97. package/dist/ui/panel-fleet.js +3 -3
  98. package/dist/ui/panel-log.js +6 -6
  99. package/dist/ui/panel-needs.js +3 -3
  100. package/dist/ui/panel-settings.js +8 -5
  101. package/dist/ui/preview-panel.js +25 -9
  102. package/dist/ui/settings-appearance.js +325 -0
  103. package/dist/ui/settings-effects.js +201 -0
  104. package/dist/ui/settings-mods.js +257 -0
  105. package/dist/ui/settings-owner-effects.js +80 -0
  106. package/dist/ui/settings-strings.js +245 -0
  107. package/dist/ui/shared/agend-auth.js +27 -3
  108. package/dist/ui/shared/agent-widget.js +248 -0
  109. package/dist/ui/shared/app-i18n.js +2 -0
  110. package/dist/ui/shared/app-prefs.js +30 -10
  111. package/dist/ui/shared/app-route.js +23 -1
  112. package/dist/ui/shared/app-session.js +1 -1
  113. package/dist/ui/shared/app-shell.js +29 -16
  114. package/dist/ui/shared/app-store.js +2 -0
  115. package/dist/ui/shared/app-stream.js +12 -7
  116. package/dist/ui/shared/app-themes.js +188 -0
  117. package/dist/ui/shared/app.css +214 -1
  118. package/dist/ui/shared/app.js +6 -0
  119. package/dist/ui/shared/card-strings.js +28 -0
  120. package/dist/ui/shared/effect-app-runtime.js +248 -0
  121. package/dist/ui/shared/effect-baseline.js +64 -0
  122. package/dist/ui/shared/effect-chat-runtime.js +154 -0
  123. package/dist/ui/shared/effect-cursor.js +125 -0
  124. package/dist/ui/shared/effect-frame.js +127 -0
  125. package/dist/ui/shared/effect-geometry.js +122 -0
  126. package/dist/ui/shared/effect-host-style.js +10 -0
  127. package/dist/ui/shared/effect-owner-regions.js +74 -0
  128. package/dist/ui/shared/effect-owner-source.js +115 -0
  129. package/dist/ui/shared/effect-policy.js +137 -0
  130. package/dist/ui/shared/effect-poster-reader.js +98 -0
  131. package/dist/ui/shared/effect-posters.js +117 -0
  132. package/dist/ui/shared/effect-profiler.js +162 -0
  133. package/dist/ui/shared/effect-profiles.js +65 -0
  134. package/dist/ui/shared/effect-registry.js +496 -0
  135. package/dist/ui/shared/effect-runtime-loader.js +234 -0
  136. package/dist/ui/shared/effect-safety.js +153 -0
  137. package/dist/ui/shared/effect-stock-snapshot.js +34 -0
  138. package/dist/ui/shared/effect-stock-trust.js +165 -0
  139. package/dist/ui/shared/effect-trial-source.js +103 -0
  140. package/dist/ui/shared/effect-trial-stage.js +147 -0
  141. package/dist/ui/shared/header-tools.js +1 -1
  142. package/dist/ui/shared/identity-card.js +202 -0
  143. package/dist/ui/shared/identity-color.js +261 -0
  144. package/dist/ui/shared/instance-nav.js +22 -14
  145. package/dist/ui/shared/owner-css.js +221 -0
  146. package/dist/ui/shared/owner-effect-prefs.js +117 -0
  147. package/dist/ui/shared/panel-view.js +81 -8
  148. package/dist/ui/shared/signin.js +2 -0
  149. package/dist/ui/shared/ui-dialog.js +1 -1
  150. package/dist/ui/shared/ui-icons.js +2 -0
  151. package/dist/ui/shared/ui-menu.js +1 -1
  152. package/dist/ui/shared/ui-toast.js +1 -1
  153. package/dist/ui/shared/view-strings.js +30 -0
  154. package/dist/ui/signin.html +1 -0
  155. package/dist/view-api.d.ts +29 -0
  156. package/dist/view-api.js +188 -8
  157. package/dist/view-api.js.map +1 -1
  158. package/dist/web-api.d.ts +42 -0
  159. package/dist/web-api.js +896 -36
  160. package/dist/web-api.js.map +1 -1
  161. package/dist/web-appearance-limits.d.ts +82 -0
  162. package/dist/web-appearance-limits.js +83 -0
  163. package/dist/web-appearance-limits.js.map +1 -0
  164. package/dist/web-appearance-policy.d.ts +66 -0
  165. package/dist/web-appearance-policy.js +94 -0
  166. package/dist/web-appearance-policy.js.map +1 -0
  167. package/dist/web-appearance-tools.d.ts +50 -0
  168. package/dist/web-appearance-tools.js +105 -0
  169. package/dist/web-appearance-tools.js.map +1 -0
  170. package/dist/web-appearance-view.d.ts +99 -0
  171. package/dist/web-appearance-view.js +163 -0
  172. package/dist/web-appearance-view.js.map +1 -0
  173. package/dist/web-appearance.d.ts +79 -0
  174. package/dist/web-appearance.js +298 -0
  175. package/dist/web-appearance.js.map +1 -0
  176. package/dist/web-asset-parts.d.ts +98 -0
  177. package/dist/web-asset-parts.js +133 -0
  178. package/dist/web-asset-parts.js.map +1 -0
  179. package/dist/web-auth.js +17 -1
  180. package/dist/web-auth.js.map +1 -1
  181. package/dist/web-card.d.ts +96 -0
  182. package/dist/web-card.js +232 -0
  183. package/dist/web-card.js.map +1 -0
  184. package/dist/web-chat-history.d.ts +13 -0
  185. package/dist/web-chat-history.js +23 -2
  186. package/dist/web-chat-history.js.map +1 -1
  187. package/dist/web-effect-bundle.d.ts +28 -0
  188. package/dist/web-effect-bundle.js +126 -0
  189. package/dist/web-effect-bundle.js.map +1 -0
  190. package/dist/web-effect-canonical.d.ts +8 -0
  191. package/dist/web-effect-canonical.js +27 -0
  192. package/dist/web-effect-canonical.js.map +1 -0
  193. package/dist/web-effect-library.d.ts +114 -0
  194. package/dist/web-effect-library.js +530 -0
  195. package/dist/web-effect-library.js.map +1 -0
  196. package/dist/web-effect-poster.d.ts +8 -0
  197. package/dist/web-effect-poster.js +52 -0
  198. package/dist/web-effect-poster.js.map +1 -0
  199. package/dist/web-effect-profiles.d.ts +118 -0
  200. package/dist/web-effect-profiles.js +180 -0
  201. package/dist/web-effect-profiles.js.map +1 -0
  202. package/dist/web-effect-service.d.ts +86 -0
  203. package/dist/web-effect-service.js +312 -0
  204. package/dist/web-effect-service.js.map +1 -0
  205. package/dist/web-effect-tools.d.ts +47 -0
  206. package/dist/web-effect-tools.js +126 -0
  207. package/dist/web-effect-tools.js.map +1 -0
  208. package/dist/web-effect-trials.d.ts +86 -0
  209. package/dist/web-effect-trials.js +204 -0
  210. package/dist/web-effect-trials.js.map +1 -0
  211. package/dist/web-effect-upload.d.ts +29 -0
  212. package/dist/web-effect-upload.js +190 -0
  213. package/dist/web-effect-upload.js.map +1 -0
  214. package/dist/web-effect-worker.d.ts +8 -0
  215. package/dist/web-effect-worker.js +170 -0
  216. package/dist/web-effect-worker.js.map +1 -0
  217. package/dist/web-effect.d.ts +89 -0
  218. package/dist/web-effect.js +132 -0
  219. package/dist/web-effect.js.map +1 -0
  220. package/dist/web-identity-color.d.ts +87 -0
  221. package/dist/web-identity-color.js +209 -0
  222. package/dist/web-identity-color.js.map +1 -0
  223. package/dist/web-identity-palette.d.ts +113 -0
  224. package/dist/web-identity-palette.js +84 -0
  225. package/dist/web-identity-palette.js.map +1 -0
  226. package/dist/web-image-meta.d.ts +25 -0
  227. package/dist/web-image-meta.js +200 -0
  228. package/dist/web-image-meta.js.map +1 -0
  229. package/dist/web-mod-agent.d.ts +50 -0
  230. package/dist/web-mod-agent.js +203 -0
  231. package/dist/web-mod-agent.js.map +1 -0
  232. package/dist/web-mod-bundle.d.ts +144 -0
  233. package/dist/web-mod-bundle.js +464 -0
  234. package/dist/web-mod-bundle.js.map +1 -0
  235. package/dist/web-mod-export.d.ts +59 -0
  236. package/dist/web-mod-export.js +79 -0
  237. package/dist/web-mod-export.js.map +1 -0
  238. package/dist/web-mod-store.d.ts +283 -0
  239. package/dist/web-mod-store.js +624 -0
  240. package/dist/web-mod-store.js.map +1 -0
  241. package/dist/web-org.d.ts +3 -3
  242. package/dist/web-org.js +11 -17
  243. package/dist/web-org.js.map +1 -1
  244. package/dist/web-owner-css-store.d.ts +180 -0
  245. package/dist/web-owner-css-store.js +384 -0
  246. package/dist/web-owner-css-store.js.map +1 -0
  247. package/dist/web-owner-css.d.ts +58 -0
  248. package/dist/web-owner-css.js +552 -0
  249. package/dist/web-owner-css.js.map +1 -0
  250. package/dist/web-prefs.d.ts +48 -0
  251. package/dist/web-prefs.js +209 -33
  252. package/dist/web-prefs.js.map +1 -1
  253. package/dist/web-preview.d.ts +9 -1
  254. package/dist/web-preview.js +24 -7
  255. package/dist/web-preview.js.map +1 -1
  256. package/dist/web-settings-patch.d.ts +18 -0
  257. package/dist/web-settings-patch.js +51 -0
  258. package/dist/web-settings-patch.js.map +1 -1
  259. package/dist/web-shell-routes.d.ts +1 -1
  260. package/dist/web-shell-routes.js +1 -1
  261. package/dist/web-shell-routes.js.map +1 -1
  262. package/dist/web-theme-store.d.ts +218 -0
  263. package/dist/web-theme-store.js +516 -0
  264. package/dist/web-theme-store.js.map +1 -0
  265. package/dist/web-theme.d.ts +121 -0
  266. package/dist/web-theme.js +407 -0
  267. package/dist/web-theme.js.map +1 -0
  268. package/dist/web-widget.d.ts +72 -0
  269. package/dist/web-widget.js +245 -0
  270. package/dist/web-widget.js.map +1 -0
  271. package/package.json +1 -1
@@ -112,7 +112,26 @@ html:has(.shell), html:has(.shell) body { overflow: hidden; }
112
112
  .av img { width: 100%; height: 100%; border-radius: 50%; object-fit: cover; }
113
113
  .av.av-emoji { background: transparent; font-size: 18px; font-weight: normal; }
114
114
  .av.av-emoji img { width: 22px; height: 22px; border-radius: 0; object-fit: contain; }
115
- .av.ring { box-shadow: 0 0 0 2px var(--ring, transparent); }
115
+ /* #1958: an instance's identity colour — both modes' values set on the row through the CSSOM (identity-color.js), the
116
+ page's mode picks one (the same selectors as tokens.css). Places: .id-ring (the avatar's ring, every face), .id-bar (a
117
+ bar at the row's left edge), .id-name (the name, in a shade readable on the page and the sidebar). */
118
+ .idc { --id: var(--id-dark); --id-name: var(--id-name-dark); }
119
+ @media (prefers-color-scheme: light) { :root:not([data-theme="dark"]) .idc { --id: var(--id-light); --id-name: var(--id-name-light); } }
120
+ :root[data-theme="light"] .idc { --id: var(--id-light); --id-name: var(--id-name-light); }
121
+ .idc.id-ring .av { box-shadow: 0 0 0 2px var(--id); }
122
+ .idc.id-bar { box-shadow: inset 3px 0 0 var(--id); }
123
+ .idc.id-name .inst-name { color: var(--id-name); }
124
+ .idc-swatches, .idc-places { border: 0; padding: 0; margin: 0 0 var(--s-3); display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
125
+ .idc-swatches legend, .idc-places legend { font-weight: var(--fw-semibold); margin-bottom: 6px; width: 100%; }
126
+ .idc-swatch { --sw: var(--sw-dark); width: 28px; height: 28px; border-radius: 50%; border: 2px solid transparent; background: var(--sw); cursor: pointer; padding: 0; }
127
+ @media (prefers-color-scheme: light) { :root:not([data-theme="dark"]) .idc-swatch { --sw: var(--sw-light); } }
128
+ :root[data-theme="light"] .idc-swatch { --sw: var(--sw-light); }
129
+ .idc-swatch.on { border-color: var(--text); box-shadow: 0 0 0 2px var(--bg); }
130
+ .idc-custom { display: inline-flex; gap: 6px; align-items: center; }
131
+ .idc-custom input[type="color"] { width: 32px; height: 28px; padding: 0; border: 0; background: none; }
132
+ .idc-hex { width: 9ch; font-family: var(--font-mono, monospace); }
133
+ .dlg-actions .dlg-spacer { flex: 1 1 auto; }
134
+ .idc-preview { padding: 6px; border-radius: 10px; background: var(--bg-sidebar); max-width: 260px; }
116
135
  .av-text { overflow: hidden; }
117
136
  .av .av-dot { position: absolute; right: -2px; bottom: -2px; margin: 0; box-shadow: 0 0 0 2px var(--bg-sidebar); }
118
137
  /* #1722: the CLI as a letter on the avatar's top-left corner — monochrome (the #1630 ring keeps the colour), cut out like the dot */
@@ -346,6 +365,8 @@ html:has(.shell), html:has(.shell) body { overflow: hidden; }
346
365
  .seg-item { display: inline-flex; align-items: center; gap: var(--s-2); min-height: 34px; padding: 0 var(--s-3); border-radius: var(--r-full); color: var(--text-2); font-size: var(--fs-sm); white-space: nowrap; }
347
366
  .seg-item:hover { background: var(--surface); color: var(--text); }
348
367
  .seg-item.active { background: var(--surface-2); color: var(--text); font-weight: var(--fw-semibold); }
368
+ /* #1629: Settings has eight sections (Appearance added): they wrap to a second row rather than hide behind a scroll. */
369
+ .p-settings .seg { flex-wrap: wrap; row-gap: var(--s-1); overflow-x: visible; }
349
370
  .list-head { display: flex; justify-content: space-between; align-items: center; gap: var(--s-3); margin: var(--s-2) 0 var(--s-3); color: var(--text-2); font-size: var(--fs-sm); }
350
371
  .rows { display: flex; flex-direction: column; gap: var(--s-1); }
351
372
  .row-item { display: flex; align-items: center; gap: var(--s-3); flex-wrap: wrap; padding: var(--s-3) var(--s-4); border-radius: var(--r-md); background: var(--surface); font-size: var(--fs-sm); }
@@ -501,6 +522,20 @@ html.lb-open .scroller { overflow: hidden; }
501
522
  CSSOM), a full-screen sheet on a narrow one. The frame is the card's (preview.js), sized by the panel here. */
502
523
  .chat-split { flex: 1; min-height: 0; display: flex; }
503
524
  .chat-main { flex: 1; min-width: 0; min-height: 0; display: flex; flex-direction: column; }
525
+ /* #1629: an agent's theme on its own chat surface — custom properties set on .p-chat through the CSSOM, choices as
526
+ these tokens. The surface paints its own background (the page's body is not its). */
527
+ .p-chat.th { background: var(--bg); color: var(--text); }
528
+ .p-chat.th .msg.user .body { border-radius: var(--bubble-radius, var(--r-xl)); }
529
+ .p-chat.th-font-system .thread, .p-chat.th-font-system .composer textarea { font-family: system-ui, -apple-system, "Segoe UI", sans-serif; }
530
+ .p-chat.th-font-mono .thread, .p-chat.th-font-mono .composer textarea { font-family: var(--font-mono); }
531
+ .p-chat.th-bubble-thin .msg.user .body { box-shadow: inset 0 0 0 1px var(--border); }
532
+ .p-chat.th-bubble-accent .msg.user .body { box-shadow: inset 0 0 0 1px var(--accent); }
533
+ .p-chat.th-agent-bubble .msg.agent .body { background: var(--agent-bubble); border-radius: var(--bubble-radius, var(--r-xl)); padding: 10px var(--s-4); }
534
+ .chat-main.has-th-bg { position: relative; }
535
+ .chat-main.has-th-bg > :not(.th-bg) { position: relative; z-index: 1; }
536
+ .th-bg { position: absolute; inset: 0; width: 100%; height: 100%; opacity: var(--th-bg-opacity, 1); pointer-events: none; z-index: 0; user-select: none; }
537
+ .th-bg-cover { object-fit: cover; }
538
+ .th-bg-contain { object-fit: contain; }
504
539
  .pv-divider { flex: 0 0 8px; cursor: col-resize; background: var(--bg); border-left: 1px solid var(--border); touch-action: none; position: relative; }
505
540
  .pv-divider::after { content: ""; position: absolute; top: 50%; left: 2px; width: 3px; height: 32px; margin-top: -16px; border-radius: 2px; background: var(--border-strong); }
506
541
  .pv-divider:hover::after, .pv-divider:focus-visible::after, .chat-split.resizing .pv-divider::after { background: var(--accent); }
@@ -567,6 +602,9 @@ html.lb-open .scroller { overflow: hidden; }
567
602
  .prompt { padding: var(--s-3) var(--s-4); border-radius: var(--r-md); font-size: var(--fs-sm); background: var(--warn-bg); border: 1px solid var(--warn-line); }
568
603
  .prompt.done { background: transparent; border-style: dashed; color: var(--text-2); }
569
604
  .prompt .txt { white-space: pre-wrap; overflow-wrap: anywhere; }
605
+ /* #1990: a name in a status line or a prompt that opens that instance in the app. */
606
+ .st-link { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; }
607
+ .st-link:hover { text-decoration-thickness: 2px; }
570
608
  .prompt .acts { display: flex; gap: var(--s-2); margin-top: var(--s-2); align-items: center; flex-wrap: wrap; }
571
609
  .prompt .wait { font-size: var(--fs-xs); color: var(--text-3); }
572
610
  .work-bar { display: flex; gap: var(--s-2); align-items: center; padding: 0 var(--s-2); font-size: var(--fs-sm); color: var(--text-2); }
@@ -610,6 +648,58 @@ html.lb-open .scroller { overflow: hidden; }
610
648
  .drop-card { display: flex; flex-direction: column; align-items: center; gap: var(--s-2); color: var(--accent); font-weight: var(--fw-semibold); }
611
649
 
612
650
  /* ── Toasts ── */
651
+ /* #1629 r4 §3: Settings → Appearance (owner CSS). */
652
+ .s-css-text, .s-widget-text { font-family: var(--font-mono); font-size: var(--fs-sm); width: 100%; min-height: 12rem; resize: vertical; tab-size: 2; }
653
+ .s-widget-preview { max-width: 320px; }
654
+ /* #1629 r4-5: Settings → Appearance → Mods — the plan is shown in full before anything is applied. */
655
+ .s-mod-plan { display: flex; flex-direction: column; gap: var(--s-2); border: 1px solid var(--border); border-radius: var(--r-md); padding: var(--s-3); }
656
+ .s-mod-plan h4 { font-size: var(--fs-lg); font-weight: var(--fw-semibold); overflow-wrap: anywhere; }
657
+ .s-mod-plan h4 .muted { font-size: var(--fs-sm); font-weight: var(--fw-regular); }
658
+ .s-mod-items { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--s-3); }
659
+ .s-mod-item { display: flex; flex-direction: column; gap: var(--s-1); }
660
+ .s-mod-detail { display: flex; flex-wrap: wrap; gap: var(--s-1) var(--s-3); padding-left: 1.75rem; color: var(--text-2); font-size: var(--fs-sm); min-width: 0; }
661
+ .s-mod-widget { display: block; max-width: min(360px, 100%); }
662
+ .s-mod-theme { display: flex; flex-direction: column; gap: var(--s-1); min-width: 0; width: 100%; }
663
+ .s-mod-kv { display: flex; gap: var(--s-2); align-items: baseline; flex-wrap: wrap; min-width: 0; }
664
+ .s-mod-kv .k { color: var(--text-3); font-size: var(--fs-xs); min-width: 9em; }
665
+ .s-mod-kv .v { color: var(--text); min-width: 0; overflow-wrap: anywhere; }
666
+ .s-mod-sws { display: flex; flex-wrap: wrap; gap: var(--s-1) var(--s-3); }
667
+ .s-mod-sw { display: inline-flex; align-items: center; gap: var(--s-1); }
668
+ .s-mod-sw-chip { display: inline-block; width: 12px; height: 12px; border-radius: 3px; border: 1px solid var(--border-strong); background: var(--sw, transparent); }
669
+ .s-mod-pic { display: inline-flex; align-items: center; gap: var(--s-2); min-width: 0; }
670
+ .s-mod-pic img { width: 64px; height: 40px; object-fit: cover; border-radius: var(--r-sm); border: 1px solid var(--border); }
671
+ .s-mod-raw summary { cursor: pointer; font-size: var(--fs-xs); color: var(--text-2); }
672
+ .s-mod-widget .aw-chat_header { border-width: 1px; border-radius: var(--r-md); }
673
+ .s-mod-text { white-space: pre-wrap; overflow-wrap: anywhere; }
674
+ .s-mod-warn { color: var(--warn); font-size: var(--fs-sm); padding-left: 1.75rem; }
675
+ .s-mod-css-text { margin: 0 0 0 1.75rem; max-height: 240px; overflow: auto; padding: var(--s-2); border-radius: var(--r-sm); background: var(--code-bg); font-family: var(--font-mono); font-size: var(--fs-xs); white-space: pre-wrap; overflow-wrap: anywhere; }
676
+ .s-mod-err { white-space: pre-line; }
677
+ .s-mod-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--s-2); }
678
+ .s-mod-list li { display: flex; align-items: center; gap: var(--s-2); flex-wrap: wrap; min-width: 0; }
679
+ .s-mod-list .grow { flex: 1; min-width: 0; overflow-wrap: anywhere; }
680
+ .s-mod-proposals { display: flex; flex-direction: column; gap: var(--s-2); }
681
+ .s-mod-proposals h4 { font-size: var(--fs-sm); font-weight: var(--fw-semibold); color: var(--text-2); }
682
+ .s-mod-export summary { cursor: pointer; font-weight: var(--fw-medium); }
683
+ .s-mod-export[open] { display: flex; flex-direction: column; gap: var(--s-2); }
684
+ .s-mod-boxes { display: flex; flex-wrap: wrap; gap: var(--s-1) var(--s-4); }
685
+ .s-css-actions, .s-widget-actions { display: flex; gap: var(--s-2); flex-wrap: wrap; }
686
+ .s-css-status { margin: 0; font-size: var(--fs-sm); color: var(--text-2); }
687
+ .s-css-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--s-2); }
688
+ .s-css-list li { display: flex; align-items: center; gap: var(--s-2); flex-wrap: wrap; min-width: 0; }
689
+ .s-css-list code { overflow-wrap: anywhere; }
690
+ .s-owner-css h4 { margin: var(--s-2) 0 0; font-size: var(--fs-sm); }
691
+ /* #1629 r4 §3: keep-or-revert after saving custom CSS — on every page, above everything but dialogs' scrim. */
692
+ .oc-keep { position: fixed; top: var(--s-3); left: 50%; transform: translateX(-50%); z-index: 120; display: flex; align-items: center; gap: var(--s-2);
693
+ flex-wrap: wrap; max-width: calc(100vw - 32px); padding: var(--s-2) var(--s-3); border-radius: var(--r-md); background: var(--surface);
694
+ color: var(--text-1); border: 1px solid var(--border); box-shadow: var(--shadow-2); font-size: var(--fs-sm); }
695
+ .oc-keep-text { display: inline-flex; align-items: baseline; gap: var(--s-2); min-width: 0; }
696
+ .oc-keep-q { font-weight: var(--fw-medium); }
697
+ .oc-keep-left { color: var(--text-3); font-variant-numeric: tabular-nums; }
698
+ /* A phone: one row across the top — the question over the seconds left, then Keep and Revert. */
699
+ @media (max-width: 479px) {
700
+ .oc-keep { left: var(--s-2); right: var(--s-2); transform: none; max-width: none; flex-wrap: nowrap; }
701
+ .oc-keep-text { flex: 1; flex-direction: column; align-items: flex-start; gap: 0; }
702
+ }
613
703
  .toasts { position: fixed; bottom: var(--s-6); right: var(--s-6); z-index: 100; display: flex; flex-direction: column; align-items: flex-end; gap: var(--s-2); pointer-events: none; }
614
704
  .toast { padding: 10px var(--s-4); max-width: min(420px, calc(100vw - 32px)); border-radius: var(--r-md); font-size: var(--fs-sm); font-weight: var(--fw-medium); box-shadow: var(--shadow-2); background: var(--accent-fill); color: var(--on-accent); animation: toast-in var(--t-med) var(--ease); }
615
705
  .toast.err { background: var(--danger-fill); color: var(--on-danger); }
@@ -1099,3 +1189,126 @@ strong.org-name { font-size: var(--fs-sm); color: var(--text); }
1099
1189
  /* #1682: a mention, as Discord draws one — accent text on a tint of it; the id (or the Telegram handle) on hover. */
1100
1190
  .md .mention { display: inline; padding: 0 0.2em; border-radius: var(--r-sm); color: var(--accent); background: var(--accent-dim);
1101
1191
  font-weight: var(--fw-medium); white-space: nowrap; }
1192
+
1193
+ /* #1629 r4 §1 / r4.1 Q3 (a): the identity card (identity-card.js). The size is a class token; its heights are here
1194
+ (S 160 / M 240 / L 360 px, on a phone 120 / 180 / 240); the banner scales with the portrait. Never a style attribute. */
1195
+ .id-card { --id-h: 240px; margin: 0 0 var(--s-4); border: 1px solid var(--border); border-radius: var(--r-md); background: var(--surface); overflow: hidden; }
1196
+ .id-card.card-s { --id-h: 160px; }
1197
+ .id-card.card-l { --id-h: 360px; }
1198
+ .id-card.card-c { --id-h: 96px; margin: 0; border: 0; border-radius: 0; background: none; }
1199
+ .id-banner { display: block; width: 100%; height: calc(var(--id-h) / 2); object-fit: cover; }
1200
+ .id-row { display: flex; align-items: flex-start; gap: var(--s-3); padding: var(--s-3) var(--s-4); }
1201
+ .id-portrait { display: block; flex-shrink: 0; height: var(--id-h); width: auto; max-width: min(calc(var(--id-h) * 0.75), 100%); object-fit: cover; border-radius: var(--r-sm); }
1202
+ .id-text { flex: 1; min-width: 0; }
1203
+ .id-tagline { margin: 0 0 var(--s-1); font-weight: var(--fw-semibold); overflow-wrap: anywhere; }
1204
+ .id-bio { margin: 0; color: var(--text-2); white-space: pre-line; overflow-wrap: anywhere; }
1205
+ .card-size { display: flex; flex-shrink: 0; gap: var(--s-1); }
1206
+ .card-size .seg-item { min-height: 28px; padding: 0 var(--s-2); border: 1px solid transparent; background: none; font: inherit; font-size: var(--fs-xs); cursor: pointer; }
1207
+ .card-size .seg-item.active { border-color: var(--accent); background: var(--accent-dim); color: var(--text); }
1208
+ .chat-card { flex-shrink: 0; border-bottom: 1px solid var(--border); }
1209
+ /* #1629 r4-4: widgets — the page's own components, from a declarative spec (agent-widget.js). The host draws the 1px
1210
+ frame and the ✦ chip, outside anything the spec chooses; nothing in a widget takes a click or focus. Colours are the
1211
+ theme's tokens (an agent's chat header sits inside its themed chat), so a person's own CSS can restyle them too. */
1212
+ .aw { border: 1px dashed var(--border-strong); border-radius: var(--r-md); padding: var(--s-2) var(--s-3); display: flex; flex-direction: column; gap: var(--s-2); min-width: 0; overflow: clip; user-select: text; }
1213
+ .aw-head { display: flex; align-items: center; gap: var(--s-2); min-width: 0; }
1214
+ .aw-chip { font-size: var(--fs-xs); font-weight: var(--fw-semibold); color: var(--accent); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
1215
+ .aw-by { font-size: var(--fs-xs); color: var(--text-3); white-space: nowrap; }
1216
+ .aw-items { display: flex; flex-direction: column; gap: var(--s-2); min-width: 0; }
1217
+ .aw-item { display: flex; align-items: center; gap: var(--s-2); min-width: 0; font-size: var(--fs-sm); }
1218
+ .aw-label { color: var(--text-2); font-size: var(--fs-xs); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex-shrink: 1; min-width: 0; }
1219
+ .aw-value { color: var(--text); font-weight: var(--fw-medium); overflow-wrap: anywhere; min-width: 0; }
1220
+ .aw-stat .aw-value { margin-left: auto; font-variant-numeric: tabular-nums; }
1221
+ .aw-unit { margin-left: 2px; color: var(--text-2); font-weight: var(--fw-regular); }
1222
+ .aw-text .aw-value { font-weight: var(--fw-regular); }
1223
+ .aw-bar { flex: 1; min-width: 48px; height: 6px; appearance: none; border: 0; border-radius: var(--r-full); background: var(--surface-2); overflow: hidden; }
1224
+ .aw-bar::-webkit-progress-bar { background: var(--surface-2); border-radius: var(--r-full); }
1225
+ .aw-bar::-webkit-progress-value { background: var(--accent); border-radius: var(--r-full); }
1226
+ .aw-bar::-moz-progress-bar { background: var(--accent); border-radius: var(--r-full); }
1227
+ .aw-progress .aw-value { margin-left: auto; font-size: var(--fs-xs); font-variant-numeric: tabular-nums; min-width: 3.5em; text-align: right; }
1228
+ .aw-list { flex-direction: column; align-items: stretch; gap: var(--s-1); }
1229
+ .aw-list ul { margin: 0; padding-left: 1.2em; display: flex; flex-direction: column; gap: 2px; overflow-wrap: anywhere; }
1230
+ .aw-badge { display: inline-block; padding: 1px var(--s-2); border-radius: var(--r-full); font-size: var(--fs-xs); font-weight: var(--fw-medium); white-space: nowrap; background: var(--surface-2); color: var(--text-2); }
1231
+ .aw-tone-ok { background: var(--ok-bg); color: var(--ok); }
1232
+ .aw-tone-warn { background: var(--warn-bg); color: var(--warn); }
1233
+ .aw-tone-bad { background: var(--danger-bg); color: var(--danger); }
1234
+ .aw-tone-info { background: var(--accent-dim); color: var(--accent); }
1235
+ .aw-spark { flex: 1; min-width: 48px; height: 24px; }
1236
+ .aw-spark polyline { fill: none; stroke: var(--accent); stroke-width: 1.5; vector-effect: non-scaling-stroke; }
1237
+ .aw-spark-item .aw-value { font-size: var(--fs-xs); font-variant-numeric: tabular-nums; }
1238
+ /* The chat header strip: one compact row under the card, in that agent's chat only. When it is wider than the chat (a
1239
+ phone), the row scrolls sideways inside its own box — never wraps out of it, never cuts an item. */
1240
+ .aw-chat_header { flex-shrink: 0; flex-direction: row; flex-wrap: nowrap; align-items: center; gap: var(--s-4); border-width: 0 0 1px; border-radius: 0; padding: var(--s-1) var(--s-4); min-height: 32px; max-height: 72px; overflow-x: auto; overflow-y: hidden; overscroll-behavior-x: contain; }
1241
+ .aw-chat_header .aw-head { flex-shrink: 0; }
1242
+ .aw-chat_header .aw-items { flex-direction: row; flex: 1 0 auto; gap: var(--s-4); }
1243
+ .aw-chat_header .aw-item { flex: 0 0 auto; white-space: nowrap; }
1244
+ .aw-chat_header .aw-value { white-space: nowrap; }
1245
+ .aw-chat_header .aw-text .aw-value { display: inline-block; max-width: 24em; overflow: hidden; text-overflow: ellipsis; vertical-align: bottom; }
1246
+ .aw-chat_header .aw-stat .aw-value { margin-left: 0; }
1247
+ .aw-chat_header .aw-bar { flex: 0 0 auto; width: 96px; }
1248
+ /* On a phone the strip may run past the edge: a fade says there is more to scroll to (the last item scrolls clear of it). */
1249
+ @media (max-width: 899px) { .aw-chat_header { padding-right: var(--s-7); -webkit-mask-image: linear-gradient(to right, #000 calc(100% - var(--s-7)), transparent); mask-image: linear-gradient(to right, #000 calc(100% - var(--s-7)), transparent); } }
1250
+ .aw-details { background: var(--surface); margin-bottom: var(--s-4); padding: var(--s-4) var(--s-5); }
1251
+ .side-widget { padding: 0 var(--s-3) var(--s-2); }
1252
+ .aw-sidebar { max-height: 240px; overflow: auto; }
1253
+
1254
+ .chat-card-toggle { display: flex; align-items: center; gap: var(--s-2); width: 100%; min-height: 32px; padding: 0 var(--s-4); border: 0; background: none; color: var(--text-2); font: inherit; font-size: var(--fs-xs); text-align: left; cursor: pointer; }
1255
+ .chat-card-peek { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-3); }
1256
+ @media (max-width: 479px) {
1257
+ .id-card.card-s { --id-h: 120px; }
1258
+ .id-card.card-m { --id-h: 180px; }
1259
+ .id-card.card-l { --id-h: 240px; }
1260
+ .id-row { flex-wrap: wrap; }
1261
+ .id-text { flex-basis: 100%; }
1262
+ }
1263
+ /* #1629 r4 §1: the card in View (above the profile row) and its part of Edit profile. */
1264
+ .v-id { flex-shrink: 0; padding: var(--s-2) var(--s-4) 0; background: var(--bg); }
1265
+ .v-id .id-card { margin: 0; }
1266
+ .form-sub { margin: var(--s-2) 0 0; font-size: var(--fs-sm); font-weight: var(--fw-semibold); }
1267
+ /* #1629 r4.1 Q3 (b): Settings → Appearance, the card's upload limits (three to a row; one on a phone). */
1268
+ .grid3 { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--s-3) var(--s-4); margin-bottom: var(--s-3); }
1269
+ .s-limit-warnings { margin: var(--s-2) 0; padding-left: var(--s-4); }
1270
+ @media (max-width: 479px) { .grid3 { grid-template-columns: minmax(0, 1fr); } }
1271
+
1272
+ /* #1629 B2: effect library controls. No runtime layer is created by this card. */
1273
+ .s-fx-actions { display: flex; flex-wrap: wrap; gap: var(--s-2); align-items: center; }
1274
+ .s-fx-item { margin-top: var(--s-3); padding-top: var(--s-3); border-top: 1px solid var(--border); }
1275
+ .s-fx-item h4 { margin: 0; }
1276
+ .s-fx-poster { display: block; max-width: min(240px, 100%); max-height: 160px; object-fit: contain; border-radius: var(--r-sm); }
1277
+
1278
+ .s-fx-chat, .s-fx-params label:not(.check) { display: flex; align-items: center; flex-wrap: wrap; gap: var(--s-2); }
1279
+ .s-fx-chat { margin: var(--s-2) 0; }
1280
+ .s-fx-params { display: flex; flex-direction: column; gap: var(--s-2); margin: var(--s-2) 0; max-width: 100%; }
1281
+ .s-fx-target, .s-fx-params input:not([type=checkbox]), .s-fx-params select { min-height: 36px; min-width: 0; max-width: 100%; padding: 0 var(--s-2); border: 1px solid var(--border); border-radius: var(--r-sm); background: var(--bg); color: var(--text); }
1282
+
1283
+ /* #1629: stock paint bands. Structural ancestors remain neutral; only bounded content owns level 1.
1284
+ The viewport effect root is a direct body sibling: behind 0 / stock content 1 (header 2) / front 20.
1285
+ Nested menus cannot cross a content stacking context: the unmounted trust guard suspends fronts for overlays. */
1286
+ .app-root { z-index: auto; }
1287
+ .conn, .panel-head, .chat-card, .aw-chat_header, .chat-main > .scroller, .chat-main > .dock,
1288
+ .sidebar > .side-head, .sidebar > .side-nav, .sidebar > .side-section, .sidebar > .side-widget, .sidebar > .side-foot,
1289
+ .u-rail > .u-rail-head, .u-rail > .u-rail-body { position: relative; z-index: 1; }
1290
+ /* Header menus must stay above the later scroller stacking context. */
1291
+ .panel-head { z-index: 2; }
1292
+ @media (min-width: 900px) { .chat-split > .pv-divider, .chat-split > .pv-panel { position: relative; z-index: 1; } }
1293
+ /* #1629: owner choices share the existing control font floor. */
1294
+ .s-fx-owner{margin-top:var(--s-4);padding-top:var(--s-3);border-top:1px solid var(--border)}
1295
+ .s-fx-owner-row{display:flex;align-items:end;flex-wrap:wrap;gap:var(--s-2);margin-top:var(--s-2)}
1296
+ .s-fx-owner-row>label{display:flex;flex-direction:column;gap:var(--s-1);min-width:0;flex:1}
1297
+
1298
+ /* #1629: host-created static poster IMG; geometry and visibility belong to the trusted CSSOM binding. */
1299
+ .agend-effect-poster { object-fit: cover !important; object-position: center !important; user-select: none; }
1300
+
1301
+ /* #1629: full-app bounded stock surfaces. Panel roots stay neutral; effects never raise the whole app. */
1302
+ .p-fleet > .seg, .p-settings > .seg,
1303
+ .p-fleet > .panel-body, .p-needs > .panel-body, .p-settings > .panel-body,
1304
+ .p-log > .panel-body, .p-details > .panel-body, .p-view > .panel-body,
1305
+ .p-log > .log-bar, .p-view > .v-term, .p-view > .v-id, .p-view > .v-card { position: relative; z-index: 1; }
1306
+ /* Keep the existing Apply bar's sticky behavior, in its own bounded stock band. */
1307
+ .p-settings > .s-pending { z-index: 1; }
1308
+
1309
+ /* #1629: host-owned local Trial panel. Only its canvas frame lives inside the paint-contained stage. */
1310
+ .effect-trial-head, .effect-trial-controls, .effect-trial-note { position: relative; z-index: 1; background: var(--bg); padding: var(--s-3); margin: 0; }
1311
+ .effect-trial-head { display: flex; align-items: center; gap: var(--s-2); border-bottom: 1px solid var(--border); }
1312
+ .effect-trial-head > span { flex: 1; min-width: 0; overflow-wrap: anywhere; }
1313
+ .effect-trial-body { flex: 1; min-height: 0; display: flex; padding: var(--s-3); }
1314
+ .effect-trial-stage { position: relative; z-index: 0; flex: 1; min-width: 0; min-height: 0; contain: paint; overflow: clip; padding: 0; border: 0; outline: none; box-shadow: none; }
@@ -18,6 +18,8 @@ import { startNeedsNotifier } from "./app-needs.js";
18
18
  import { toast } from "./ui-toast.js";
19
19
  import { t } from "./app-i18n.js";
20
20
  import { initTextSize, initUsageRail } from "./header-tools.js";
21
+ import { installOwnerCss } from "./owner-css.js";
22
+ import { startThemes } from "./app-themes.js";
21
23
 
22
24
  const boot = document.body.dataset;
23
25
  // #1523 N3: this device's text size, on the root before the first paint of any panel.
@@ -89,6 +91,8 @@ if (mode === "full") {
89
91
  startNeedsNotifier({ open: (instance) => navigate(chatPath(instance)) });
90
92
  // #1665: this device's first upload of its own choices (once), then another device's changes at each status frame.
91
93
  bootPrefs();
94
+ // #1629: agents' themes, read when the status frame's themes_rev moves.
95
+ startThemes();
92
96
  onNewInstance = async () => {
93
97
  closeDrawer(); // on a phone the dialog opens from the drawer: the drawer goes first
94
98
  let m;
@@ -99,6 +103,8 @@ if (mode === "full") {
99
103
  } else appStore.set({ connection: "none" });
100
104
 
101
105
  startRouter(window);
106
+ // #1629 r4 §3: the person's owner CSS (full app only; never on Settings, in safe mode or where this device turned it off).
107
+ installOwnerCss();
102
108
  // /ui on its own opens the chat this browser had open last (an unknown name shows the panel's "not found").
103
109
  {
104
110
  const r = navStore.get().route;
@@ -0,0 +1,28 @@
1
+ // #1629 r4 §1 / r4.1: the identity card's words (card.*), in English and zh-TW. Public (/assets/): View's anonymous
2
+ // reader sees a card too.
3
+ import { register } from "./app-i18n.js";
4
+
5
+ register("card", {
6
+ en: {
7
+ label: "Identity card of {0}",
8
+ portraitAlt: "{0}'s portrait",
9
+ size: "Card size",
10
+ sizeS: "S", sizeM: "M", sizeL: "L",
11
+ sizeSName: "Small", sizeMName: "Medium", sizeLName: "Large",
12
+ byHuman: "set by a person",
13
+ byAgent: "set by the agent",
14
+ show: "Show card",
15
+ hide: "Hide card",
16
+ },
17
+ "zh-TW": {
18
+ label: "{0} 的身分卡",
19
+ portraitAlt: "{0} 的肖像",
20
+ size: "卡片大小",
21
+ sizeS: "小", sizeM: "中", sizeL: "大",
22
+ sizeSName: "小", sizeMName: "中", sizeLName: "大",
23
+ byHuman: "由人設定",
24
+ byAgent: "由 agent 設定",
25
+ show: "顯示卡片",
26
+ hide: "隱藏卡片",
27
+ },
28
+ });
@@ -0,0 +1,248 @@
1
+ /** #1629: app-owned pool; Chat contributes a lease, never another registry/loader. */
2
+ import { createEffectTrialSource } from "./effect-trial-source.js";
3
+ import { createOwnerEffectSource } from "./effect-owner-source.js";
4
+ import { createEffectRuntimeLoader } from "./effect-runtime-loader.js";
5
+ import { createEffectRegistry } from "./effect-registry.js";
6
+ import { createEffectSafety } from "./effect-safety.js";
7
+ import { stockEffectTrust } from "./effect-stock-trust.js";
8
+ import { createEffectProfileCollector } from "./effect-profiles.js";
9
+ import { chatEffectIdentity, createEffectWriter } from "./effect-chat-runtime.js";
10
+
11
+ const episodes = new WeakSet();
12
+
13
+ /** `current` belongs to the page/session episode. Route cleanup only releases its contribution. */
14
+ export function createAppEffectRuntime({ window: host, current, observe, onChange = () => {}, onDispose = () => {}, resolveOwnerTarget = null, onOwnerUnavailable = () => {}, factories = {} }) {
15
+ let dead = false, error = null, loader = null, registry = null, writer = null, source = null, personal = null, trial = null, pending = null, lastMetadata = null, serial = 0, revision = 0, stamp = null, syncing = false, queued = false;
16
+ const bindings = new Map(), profiles = new Map();
17
+ const identity = chatEffectIdentity(host), auth = host?.AgendAuth;
18
+ const safe = fn => { try { return fn(); } catch { return null; } };
19
+ const alive = () => { if (dead) return false; const value = safe(() => current() === true && !auth.sessionEnded()); return !dead && value === true; };
20
+ const sourceAlive = s => {
21
+ if (!s || !alive() || s !== source || s.dead) return false;
22
+ const value = safe(() => s.current() === true && s.element.isConnected === true);
23
+ return !dead && s === source && !s.dead && value === true;
24
+ };
25
+ const placementIdentity = p => JSON.stringify([p.by, p.manifest.layer, p.params]);
26
+ const clock = () => host.performance.now();
27
+ let offAuth = null;
28
+ const snapshot = s => ({ error: error ?? s?.error ?? null, rows: registry?.snapshot().filter(row => s?.bindings.get(row.id)?.key === row.key) ?? [], effects: s?.metadata?.effects ?? [], loading: !!s && !s.dead && s.metadata === null && !s.error });
29
+ function tell() {
30
+ const s = source, value = snapshot(s); if (dead) return;
31
+ if (sourceAlive(s)) safe(() => s.onChange(value));
32
+ if (!dead) safe(() => onChange(snapshot(source)));
33
+ }
34
+ function discard(s) {
35
+ if (!s || s.dead) return;
36
+ s.dead = true; s.generation++; s.metadata = null;
37
+ if (source === s) source = null;
38
+ safe(() => trial?.stopFor(s));
39
+ for (const b of s.bindings.values()) bindings.delete(b.key); s.bindings.clear();
40
+ if (pending?.source === s) safe(() => pending.controller.abort());
41
+ revision++; stamp = null;
42
+ }
43
+ function dispose() {
44
+ if (dead) return; dead = true; discard(source);
45
+ safe(() => trial?.dispose()); safe(() => pending?.controller.abort()); safe(() => registry?.dispose?.()); safe(() => loader?.dispose?.()); safe(() => personal?.dispose()); safe(() => writer?.dispose()); safe(() => offAuth?.());
46
+ safe(() => host?.removeEventListener?.("pagehide", dispose));
47
+ for (const row of profiles.values()) safe(() => row.collector.dispose()); profiles.clear(); bindings.clear(); safe(() => onDispose({ error }));
48
+ }
49
+ function fail(reason) { if (dead) return; error = reason; dispose(); safe(() => onChange({ error, rows: [], effects: [], loading: false })); }
50
+ const unavailable = { ok: false, sync() {}, changedChat() {}, reply() {}, detachChat() {}, attachChat() { return null; }, attachTrial() { return null; }, stopTrial() {}, trialSnapshot: () => ({ rows: [] }), snapshot: () => snapshot(null), dispose };
51
+ if (!host || typeof host !== "object" || episodes.has(host) || !identity || typeof current !== "function" || typeof observe !== "function" || typeof auth?.onSessionEnd !== "function" || typeof auth?.sessionEnded !== "function") return unavailable;
52
+ episodes.add(host); // Claim before invoking current/auth callbacks: one attempt per Window episode.
53
+ if (!alive()) { dispose(); return unavailable; }
54
+ try {
55
+ offAuth = auth.onSessionEnd(dispose);
56
+ if (!alive()) { dispose(); safe(() => offAuth?.()); return unavailable; }
57
+ host.addEventListener?.("pagehide", dispose);
58
+ } catch { fail("runtime_unavailable"); return unavailable; }
59
+ const make = (key, fn) => factories[key] ?? fn;
60
+ function bindingAlive(b) { if (b?.kind === "trial") return trial?.current(b) === true; if (b?.kind === "owner") return personal?.current(b) === true; return !!b && sourceAlive(b.source) && b.source.generation === b.generation && bindings.get(b.key) === b && b.source.bindings.get(b.id) === b; }
61
+ const bindingFor = (key, id) => { const b = bindings.get(key) ?? trial?.binding(key, id) ?? personal?.binding(key, id); return b?.id === id && bindingAlive(b) ? b : null; };
62
+ try {
63
+ writer = createEffectWriter(host, alive);
64
+ loader = make("loader", createEffectRuntimeLoader)({ window: host, ownerEpoch: identity.epoch, current: alive, onRevoke: () => fail("runtime_unavailable") });
65
+ if (!loader.ok || typeof loader.placementsSettled !== "function" || typeof loader.resourceSettled !== "function") throw Error("runtime_unavailable");
66
+ registry = make("registry", createEffectRegistry)({ window: host, origin: host.document.body.dataset.previewOrigin, boot: host.document.body.dataset.previewBoot,
67
+ load: async (descriptor, signal) => {
68
+ const b = bindingFor(descriptor.key, descriptor.id);
69
+ if (!bindingAlive(b) || descriptor.id !== b.id || signal.aborted) return null;
70
+ if (b.kind === "trial") return trial.load(b, signal);
71
+ if (b.kind === "owner") return personal.load(b, signal);
72
+ const value = await loader.resourceSettled({ id: b.id, instance: b.source.name }, signal);
73
+ if (!bindingAlive(b) || signal.aborted) return null;
74
+ if (!value) { refuseSource(b.source); return null; }
75
+ return value;
76
+ },
77
+ safety: make("safety", createEffectSafety)({ storage: host.localStorage, crypto: host.crypto }),
78
+ stockSnapshot: ({ frameRoot }) => make("stock", stockEffectTrust)(host, { frameRoot, ownerCss: observe().ownerCss }),
79
+ onChange() { try { tell(); emit(); } catch { fail("runtime_unavailable"); } },
80
+ onMount(owner) {
81
+ const b = bindingFor(owner.key, owner.id); if (!bindingAlive(b) || b.id !== owner.id || !owner.current()) return;
82
+ const collector = make("collector", createEffectProfileCollector)({ key: owner.key, id: owner.id, device: owner.device, clock });
83
+ const began = safe(clock);
84
+ if (collector.ok && Number.isFinite(began) && began >= 0 && bindingAlive(b) && owner.current()) profiles.set(owner, { binding: b, collector, began, attempted: null }); else collector.dispose?.();
85
+ },
86
+ onRetire(owner) { profiles.get(owner)?.collector.dispose(); profiles.delete(owner); },
87
+ onProfile(raw, owner) {
88
+ const row = profiles.get(owner); if (!row || !bindingAlive(row.binding) || !owner.current()) return;
89
+ row.collector.add(raw); if (row.binding.kind === "trial") return; // Diagnostic samples stay transient; shared safety still runs in registry.
90
+ const now = clock();
91
+ if (!Number.isFinite(now) || now < row.began || now - row.began < 10000 && raw.verdict !== "paused" || row.attempted !== null && now - row.attempted < 10000) return;
92
+ const report = row.collector.report(); if (!report || !bindingAlive(row.binding) || !owner.current()) return;
93
+ row.attempted = now;
94
+ const query = row.binding.kind === "owner" ? "" : `instance=${encodeURIComponent(row.binding.source.name)}&`;
95
+ void writer.post(`/ui/effects/profile/${owner.id}?${query}ownerEpoch=${identity.epoch}`, report.profile);
96
+ }, clock,
97
+ });
98
+ if (!registry.ok) throw Error("runtime_unavailable");
99
+ trial = make("trialSource", createEffectTrialSource)({ window: host, current: () => {
100
+ if (!alive()) return false; const v = observe(), c = v.context;
101
+ return alive() && v.ownerCss === "off" && c.enabled === true && c.show === true && c.previewReady === true && c.optIn === true
102
+ && c.safe === false && c.publicLink === false && c.reducedMotion === false && c.saveData === false && c.hidden === false;
103
+ }, ownerCurrent: (s, name) => sourceAlive(s) && s.name === name,
104
+ stock: () => registry.frameRoot ? make("stock", stockEffectTrust)(host, { frameRoot: registry.frameRoot, ownerCss: observe().ownerCss }) : null,
105
+ readResource: (spec, signal) => typeof loader.trialResourceSettled === "function" ? loader.trialResourceSettled(spec, signal) : Promise.resolve(null),
106
+ onChange() { revision++; stamp = null; sync(); tell(); },
107
+ });
108
+ if (typeof resolveOwnerTarget === "function") {
109
+ if (typeof loader.ownerSettled !== "function") throw Error("runtime_unavailable");
110
+ personal = createOwnerEffectSource({ current: alive,
111
+ readMetadata: signal => loader.ownerSettled(signal), readResource: (spec, signal) => loader.resourceSettled(spec, signal), resolveTarget: resolveOwnerTarget,
112
+ stock: () => registry.frameRoot ? make("stock", stockEffectTrust)(host, { frameRoot: registry.frameRoot, ownerCss: observe().ownerCss }) : null,
113
+ onChange() { revision++; stamp = null; sync(); tell(); }, onUnavailable: (slot, id) => safe(() => onOwnerUnavailable(slot, id)),
114
+ });
115
+ if (!personal.ok) throw Error("runtime_unavailable");
116
+ }
117
+ } catch { fail("runtime_unavailable"); return unavailable; }
118
+ function descriptors(s) {
119
+ if (!sourceAlive(s) || s.error) return [];
120
+ return (s.metadata?.effects ?? []).filter(p => p.manifest.regions.includes("chat")).map((p, order) => {
121
+ let b = s.bindings.get(p.id); const identity = placementIdentity(p);
122
+ if (!b || b.identity !== identity) {
123
+ if (b) bindings.delete(b.key);
124
+ b = Object.freeze({ key: `chat:${s.serial}:${p.id}`, id: p.id, source: s, generation: s.generation, identity });
125
+ s.bindings.set(p.id, b); bindings.set(b.key, b);
126
+ }
127
+ return { key: b.key, id: p.id, by: p.by, region: "chat", layer: p.manifest.layer, order, paused: s.paused.get(p.id) === identity, visible: true, instance: s.name, params: p.params, element: s.element };
128
+ });
129
+ }
130
+ function observation() {
131
+ const value = observe();
132
+ return { ...value, context: { ...value.context, openInstance: sourceAlive(source) ? source.name : null } };
133
+ }
134
+ function emit() {
135
+ if (!alive() || !registry?.ok) return;
136
+ const s = source, value = observation();
137
+ if (sourceAlive(s)) { const state = safe(() => s.observe().state); if (sourceAlive(s) && ["idle", "working", "needs_you", "stopped"].includes(state)) registry.publish("state", { state }, s.name); }
138
+ if (alive() && ["light", "dark"].includes(value.mode)) registry.publish("theme", { mode: value.mode });
139
+ }
140
+ function refuseSource(s) {
141
+ if (!sourceAlive(s)) return;
142
+ s.error = "resource_unavailable"; s.metadata = null; s.generation++;
143
+ for (const b of s.bindings.values()) bindings.delete(b.key); s.bindings.clear(); revision++; stamp = null; sync(); tell();
144
+ }
145
+ function queueSync() {
146
+ if (queued || dead) return; queued = true;
147
+ Promise.resolve().then(() => { queued = false; if (alive()) sync(); else dispose(); });
148
+ }
149
+ function sync() {
150
+ if (!alive()) { dispose(); return; } if (syncing) { queueSync(); return; }
151
+ syncing = true;
152
+ try {
153
+ const candidate = source;
154
+ if (candidate && !sourceAlive(candidate)) discard(candidate);
155
+ const before = revision, value = observation(), s = source;
156
+ if (!alive() || revision !== before || s !== source) { if (alive()) queueSync(); return; }
157
+ const entries = [...(personal?.descriptors() ?? []), ...(trial?.descriptors() ?? []), ...descriptors(s)];
158
+ if (!alive() || revision !== before || s !== source) { stamp = null; if (alive()) queueSync(); return; }
159
+ const next = JSON.stringify([revision, value.context, entries.map(d => [d.key, d.paused])]);
160
+ if (stamp !== next) {
161
+ if (!alive() || revision !== before || s !== source) { stamp = null; if (alive()) queueSync(); return; }
162
+ stamp = next; registry.update(entries, value.context);
163
+ }
164
+ if (!alive() || revision !== before || s !== source) { stamp = null; if (alive()) queueSync(); return; }
165
+ emit();
166
+ const c = value.context;
167
+ if (!pending && c.enabled && c.show && c.previewReady && c.optIn && !c.safe && !c.publicLink && !c.reducedMotion && !c.saveData && !c.hidden) {
168
+ const ownerNeeds = personal?.needsMetadata() === true, chatNeeds = sourceAlive(s) && !s.error && !s.metadata;
169
+ if (!alive() || revision !== before || s !== source) { if (alive()) queueSync(); return; }
170
+ if (ownerNeeds && (!chatNeeds || lastMetadata !== "owner")) void readPersonal();
171
+ else if (chatNeeds) void read(s);
172
+ }
173
+ } catch { fail("runtime_unavailable"); }
174
+ finally { syncing = false; }
175
+ }
176
+ async function readPersonal() {
177
+ if (!personal || pending || !personal.needsMetadata() || !alive()) return;
178
+ const op = { source: personal, controller: new host.AbortController() }; pending = op; lastMetadata = "owner";
179
+ try { await personal.read(op.controller.signal); }
180
+ catch { fail("runtime_unavailable"); }
181
+ finally { if (pending === op) { pending = null; if (alive()) sync(); } }
182
+ }
183
+ function changedOwner() {
184
+ if (!personal || !alive()) return;
185
+ if (pending?.source === personal) safe(() => pending.controller.abort());
186
+ personal.invalidate();
187
+ }
188
+ function ownerSnapshot() {
189
+ return { ...(personal?.snapshot() ?? { error: null, rev: -1, loading: false, slots: {} }),
190
+ rows: registry?.snapshot().flatMap(row => { const b = personal?.binding(row.key, row.id); return b ? [{ ...row, slot: b.slot }] : []; }) ?? [] };
191
+ }
192
+ async function cursorImageSettled(selection, signal) {
193
+ const own = personal?.cursorSelection();
194
+ if (!alive() || !own || !personal.cursorCurrent(selection) || signal?.aborted) return null;
195
+ let value; try { value = await loader.cursorImageSettled(own.id, signal); } catch { value = null; }
196
+ if (!alive() || !personal.cursorCurrent(own) || !personal.cursorCurrent(selection) || signal?.aborted) return null;
197
+ if (!value) { personal.refuseCursor(own); return null; }
198
+ return value;
199
+ }
200
+ async function read(s) {
201
+ if (!sourceAlive(s) || pending || s.error) return;
202
+ const op = { source: s, generation: s.generation, controller: new host.AbortController() }; pending = op; lastMetadata = "chat";
203
+ try {
204
+ const answer = await loader.placementsSettled(s.name, op.controller.signal);
205
+ if (!sourceAlive(s) || op.generation !== s.generation || pending !== op || op.controller.signal.aborted) return;
206
+ if (!answer) { refuseSource(s); return; }
207
+ s.metadata = answer;
208
+ for (const [id, identity] of s.paused) if (!answer.effects.some(p => p.id === id && placementIdentity(p) === identity)) s.paused.delete(id);
209
+ revision++; stamp = null; sync(); tell();
210
+ } catch { if (sourceAlive(s) && op.generation === s.generation) refuseSource(s); }
211
+ finally { if (pending === op) { pending = null; if (alive()) sync(); } }
212
+ }
213
+ function changedSource(s) {
214
+ if (!sourceAlive(s) || s.error) return;
215
+ s.generation++; s.metadata = null;
216
+ for (const b of s.bindings.values()) bindings.delete(b.key); s.bindings.clear();
217
+ if (pending?.source === s) safe(() => pending.controller.abort()); revision++; stamp = null; sync(); tell();
218
+ }
219
+ function publishReply(s, event) { if (sourceAlive(s) && !s.error && event.instance === s.name) registry.publish("reply", {}, s.name); }
220
+ function attachChat(options) {
221
+ discard(source); const expected = revision; sync();
222
+ if (!alive() || revision !== expected || source !== null) return null;
223
+ const { name, element, current: ownCurrent, observe: ownObserve, onChange: ownChange = () => {} } = options ?? {};
224
+ if (!alive() || typeof name !== "string" || name.length < 1 || name.length > 128 || !element || element.isConnected !== true || typeof ownCurrent !== "function" || typeof ownObserve !== "function" || safe(() => ownCurrent() === true) !== true || revision !== expected || source !== null) return null;
225
+ const s = { name, element, current: ownCurrent, observe: ownObserve, onChange: ownChange, serial: ++serial, generation: 0, dead: false, error: null, metadata: null, bindings: new Map(), paused: new Map() };
226
+ source = s; revision++; stamp = null;
227
+ const placement = id => sourceAlive(s) && !s.error ? s.metadata?.effects.find(p => p.id === id) : null;
228
+ const changed = () => changedSource(s);
229
+ const api = { ok: true, sync,
230
+ changed,
231
+ reply(event) { publishReply(s, event); },
232
+ pause(id) { const p = placement(id); if (!p) return false; s.paused.set(id, placementIdentity(p)); revision++; sync(); return true; },
233
+ resume(id) { const p = placement(id); if (!p) return false; s.paused.delete(id); revision++; sync(); const b = s.bindings.get(id); return bindingAlive(b) && registry.resume(b.key); },
234
+ async remove(id) { const p = placement(id); if (!p) return false; const generation = s.generation; const ok = await writer.post("/ui/effects/remove", { instance: s.name, id }); if (!sourceAlive(s) || generation !== s.generation) return false; if (ok) changed(); return ok; },
235
+ snapshot: () => sourceAlive(s) ? snapshot(s) : { error: null, rows: [], effects: [], loading: false },
236
+ dispose() { if (source !== s) return; discard(s); sync(); },
237
+ };
238
+ sync(); tell(); return sourceAlive(s) ? api : null;
239
+ }
240
+ return { ok: true, sync, attachChat, changedOwner, ownerSnapshot,
241
+ attachTrial(options) { const s = source; return sourceAlive(s) ? trial.attach(options, s) : null; },
242
+ stopTrial() { trial?.stop(); },
243
+ trialSnapshot() { return { rows: registry?.snapshot().filter(row => trial?.binding(row.key, row.id)) ?? [] }; },
244
+ cursorSelection: () => personal?.cursorSelection() ?? null, cursorImageSettled,
245
+ pauseOwner(slot) { return personal?.pause(slot) === true; },
246
+ resumeOwner(slot) { if (!personal?.unpause(slot)) return false; const b = personal.slotBinding(slot); return bindingAlive(b) && registry.resume(b.key); },
247
+ changedChat() { changedSource(source); }, reply(event) { publishReply(source, event); }, detachChat() { discard(source); sync(); }, snapshot: () => snapshot(source), dispose };
248
+ }
@@ -0,0 +1,64 @@
1
+ /** #1629: host cadence before ANY effect frame; never learns a baseline from an active effect. */
2
+ const finite = n => typeof n === "number" && Number.isFinite(n);
3
+ export function captureEffectBaseline({ window: host, empty, clock = () => host.performance.now() } = {}) {
4
+ let finish, settled = false, raf = null, timer = null, last = null, first = null, count = 0;
5
+ const promise = new Promise(resolve => { finish = resolve; });
6
+ const end = value => {
7
+ if (settled) return;
8
+ settled = true;
9
+ if (raf !== null) host.cancelAnimationFrame(raf);
10
+ if (timer !== null) host.clearTimeout(timer);
11
+ host.document.removeEventListener("visibilitychange", visibility);
12
+ finish(value);
13
+ };
14
+ const fail = reason => end({ ok: false, reason });
15
+ const gates = () => {
16
+ try { return host.document.visibilityState === "visible" && empty() === true; } catch { return false; }
17
+ };
18
+ const time = () => {
19
+ let now; try { now = clock(); } catch { return null; }
20
+ if (!finite(now) || now < 0 || last !== null && now < last) return null;
21
+ last = now; return now;
22
+ };
23
+ const visibility = () => { if (!gates()) fail("baseline_unavailable"); };
24
+ const step = () => {
25
+ if (settled) return;
26
+ raf = null;
27
+ if (!gates()) { fail("baseline_unavailable"); return; }
28
+ const now = time();
29
+ if (now === null) { fail("clock_unavailable"); return; }
30
+ if (now - began >= 10000) { fail("baseline_timeout"); return; }
31
+ if (first === null) { first = now; count = 1; }
32
+ else if (now > sample) count++;
33
+ sample = now;
34
+ if (now - first >= 2000) {
35
+ const fps = (count - 1) * 1000 / (now - first);
36
+ if (!finite(fps) || fps <= 0 || fps > 1000 || !gates()) fail("baseline_unavailable");
37
+ else end({ ok: true, baselineFps: fps });
38
+ return;
39
+ }
40
+ raf = host.requestAnimationFrame(step);
41
+ };
42
+ let began = null, sample = null;
43
+ if (!host || !host.document || typeof empty !== "function" || typeof clock !== "function" || ["requestAnimationFrame", "cancelAnimationFrame", "setTimeout", "clearTimeout"].some(k => typeof host[k] !== "function") || typeof host.document.addEventListener !== "function" || typeof host.document.removeEventListener !== "function") {
44
+ settled = true; finish({ ok: false, reason: "baseline_unsupported" });
45
+ return { promise, cancel() {} };
46
+ }
47
+ began = time();
48
+ if (began === null) fail("clock_unavailable");
49
+ else if (!gates()) fail("baseline_unavailable");
50
+ else {
51
+ host.document.addEventListener("visibilitychange", visibility);
52
+ const deadline = () => {
53
+ if (settled) return;
54
+ if (!gates()) { fail("baseline_unavailable"); return; }
55
+ const now = time();
56
+ if (now === null) fail("clock_unavailable");
57
+ else if (now - began >= 10000) fail("baseline_timeout");
58
+ else timer = host.setTimeout(deadline, Math.max(1, 10000 - (now - began)));
59
+ };
60
+ timer = host.setTimeout(deadline, 10000);
61
+ raf = host.requestAnimationFrame(step);
62
+ }
63
+ return { promise, cancel() { fail("baseline_cancelled"); } };
64
+ }