@songsid/agend 2.2.0 → 2.3.0-beta.2

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 (298) 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 +35 -0
  7. package/dist/agent-file-read.js +144 -0
  8. package/dist/agent-file-read.js.map +1 -0
  9. package/dist/agent-whats-new.js +8 -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/claude-code.js +5 -1
  18. package/dist/backend/claude-code.js.map +1 -1
  19. package/dist/backend/launch-effort.js +1 -1
  20. package/dist/backend/launch-effort.js.map +1 -1
  21. package/dist/backend/muse.js +10 -2
  22. package/dist/backend/muse.js.map +1 -1
  23. package/dist/backend/types.d.ts +8 -0
  24. package/dist/backend/types.js +10 -0
  25. package/dist/backend/types.js.map +1 -1
  26. package/dist/cache-analysis.d.ts +47 -1
  27. package/dist/cache-analysis.js +68 -8
  28. package/dist/cache-analysis.js.map +1 -1
  29. package/dist/cache-report-tool.d.ts +17 -0
  30. package/dist/cache-report-tool.js +40 -0
  31. package/dist/cache-report-tool.js.map +1 -0
  32. package/dist/cache-service.d.ts +15 -0
  33. package/dist/cache-service.js +26 -1
  34. package/dist/cache-service.js.map +1 -1
  35. package/dist/channel/adapters/discord.d.ts +2 -0
  36. package/dist/channel/adapters/discord.js +17 -2
  37. package/dist/channel/adapters/discord.js.map +1 -1
  38. package/dist/channel/adapters/telegram.d.ts +8 -1
  39. package/dist/channel/adapters/telegram.js +74 -4
  40. package/dist/channel/adapters/telegram.js.map +1 -1
  41. package/dist/channel/mcp-tools.js +36 -0
  42. package/dist/channel/mcp-tools.js.map +1 -1
  43. package/dist/channel/types.d.ts +15 -0
  44. package/dist/cli.js +7 -3
  45. package/dist/cli.js.map +1 -1
  46. package/dist/config-validator.js +22 -2
  47. package/dist/config-validator.js.map +1 -1
  48. package/dist/daemon.js +244 -20
  49. package/dist/daemon.js.map +1 -1
  50. package/dist/fleet-context.d.ts +5 -0
  51. package/dist/fleet-manager.d.ts +92 -0
  52. package/dist/fleet-manager.js +410 -29
  53. package/dist/fleet-manager.js.map +1 -1
  54. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +7 -0
  55. package/dist/general-knowledge/skills/web-appearance/SKILL.md +367 -0
  56. package/dist/instance-avatar.js +3 -0
  57. package/dist/instance-avatar.js.map +1 -1
  58. package/dist/instance-config-impact.js +3 -0
  59. package/dist/instance-config-impact.js.map +1 -1
  60. package/dist/instance-lifecycle.js +9 -4
  61. package/dist/instance-lifecycle.js.map +1 -1
  62. package/dist/needs-you-hub.d.ts +4 -0
  63. package/dist/needs-you-hub.js +31 -6
  64. package/dist/needs-you-hub.js.map +1 -1
  65. package/dist/needs-you.d.ts +1 -13
  66. package/dist/needs-you.js +3 -37
  67. package/dist/needs-you.js.map +1 -1
  68. package/dist/outbound-handlers.d.ts +7 -0
  69. package/dist/outbound-handlers.js +65 -10
  70. package/dist/outbound-handlers.js.map +1 -1
  71. package/dist/outbound-schemas.d.ts +74 -0
  72. package/dist/outbound-schemas.js +56 -0
  73. package/dist/outbound-schemas.js.map +1 -1
  74. package/dist/public-web-gateway.js +14 -0
  75. package/dist/public-web-gateway.js.map +1 -1
  76. package/dist/repo-worktree-guard.d.ts +36 -0
  77. package/dist/repo-worktree-guard.js +143 -0
  78. package/dist/repo-worktree-guard.js.map +1 -0
  79. package/dist/scheduler/scheduler.d.ts +70 -1
  80. package/dist/scheduler/scheduler.js +256 -17
  81. package/dist/scheduler/scheduler.js.map +1 -1
  82. package/dist/service-installer.d.ts +2 -1
  83. package/dist/service-installer.js +18 -3
  84. package/dist/service-installer.js.map +1 -1
  85. package/dist/settings-api.js +74 -1
  86. package/dist/settings-api.js.map +1 -1
  87. package/dist/settings-change.js +8 -0
  88. package/dist/settings-change.js.map +1 -1
  89. package/dist/settings-effect.js +16 -2
  90. package/dist/settings-effect.js.map +1 -1
  91. package/dist/settings-http-confirmation.js +5 -0
  92. package/dist/settings-http-confirmation.js.map +1 -1
  93. package/dist/tool-permissions.d.ts +5 -0
  94. package/dist/tool-permissions.js +61 -2
  95. package/dist/tool-permissions.js.map +1 -1
  96. package/dist/topic-commands.d.ts +10 -6
  97. package/dist/topic-commands.js +21 -17
  98. package/dist/topic-commands.js.map +1 -1
  99. package/dist/topic-link.d.ts +128 -0
  100. package/dist/topic-link.js +269 -0
  101. package/dist/topic-link.js.map +1 -0
  102. package/dist/types.d.ts +13 -0
  103. package/dist/ui/app-effects.js +187 -0
  104. package/dist/ui/chat-effect-posters.js +42 -0
  105. package/dist/ui/chat-effects.js +72 -0
  106. package/dist/ui/chat-live-replies.js +58 -0
  107. package/dist/ui/chat-render.js +4 -4
  108. package/dist/ui/chat-store.js +9 -3
  109. package/dist/ui/chat-strings.js +15 -0
  110. package/dist/ui/chat-thread.js +47 -21
  111. package/dist/ui/chat-tour.js +1 -1
  112. package/dist/ui/effect-trial-controller.js +119 -0
  113. package/dist/ui/effect-trial-offer.js +31 -0
  114. package/dist/ui/effect-trial-panel.js +37 -0
  115. package/dist/ui/fleet-cache.js +23 -1
  116. package/dist/ui/image-lightbox.js +1 -1
  117. package/dist/ui/panel-chat.js +85 -17
  118. package/dist/ui/panel-details.js +18 -5
  119. package/dist/ui/panel-fleet.js +3 -3
  120. package/dist/ui/panel-log.js +6 -6
  121. package/dist/ui/panel-needs.js +3 -3
  122. package/dist/ui/panel-settings.js +8 -5
  123. package/dist/ui/preview-panel.js +25 -9
  124. package/dist/ui/settings-appearance.js +325 -0
  125. package/dist/ui/settings-effects.js +201 -0
  126. package/dist/ui/settings-mods.js +257 -0
  127. package/dist/ui/settings-owner-effects.js +80 -0
  128. package/dist/ui/settings-strings.js +245 -0
  129. package/dist/ui/shared/agend-auth.js +27 -3
  130. package/dist/ui/shared/agent-widget.js +248 -0
  131. package/dist/ui/shared/app-i18n.js +2 -0
  132. package/dist/ui/shared/app-prefs.js +30 -10
  133. package/dist/ui/shared/app-route.js +23 -1
  134. package/dist/ui/shared/app-session.js +1 -1
  135. package/dist/ui/shared/app-shell.js +29 -16
  136. package/dist/ui/shared/app-store.js +2 -0
  137. package/dist/ui/shared/app-stream.js +12 -7
  138. package/dist/ui/shared/app-themes.js +188 -0
  139. package/dist/ui/shared/app.css +225 -3
  140. package/dist/ui/shared/app.js +6 -0
  141. package/dist/ui/shared/card-strings.js +28 -0
  142. package/dist/ui/shared/effect-app-runtime.js +248 -0
  143. package/dist/ui/shared/effect-baseline.js +64 -0
  144. package/dist/ui/shared/effect-chat-runtime.js +154 -0
  145. package/dist/ui/shared/effect-cursor.js +125 -0
  146. package/dist/ui/shared/effect-frame.js +127 -0
  147. package/dist/ui/shared/effect-geometry.js +122 -0
  148. package/dist/ui/shared/effect-host-style.js +10 -0
  149. package/dist/ui/shared/effect-owner-regions.js +74 -0
  150. package/dist/ui/shared/effect-owner-source.js +115 -0
  151. package/dist/ui/shared/effect-policy.js +137 -0
  152. package/dist/ui/shared/effect-poster-reader.js +98 -0
  153. package/dist/ui/shared/effect-posters.js +117 -0
  154. package/dist/ui/shared/effect-profiler.js +162 -0
  155. package/dist/ui/shared/effect-profiles.js +65 -0
  156. package/dist/ui/shared/effect-registry.js +496 -0
  157. package/dist/ui/shared/effect-runtime-loader.js +234 -0
  158. package/dist/ui/shared/effect-safety.js +153 -0
  159. package/dist/ui/shared/effect-stock-snapshot.js +34 -0
  160. package/dist/ui/shared/effect-stock-trust.js +165 -0
  161. package/dist/ui/shared/effect-trial-source.js +103 -0
  162. package/dist/ui/shared/effect-trial-stage.js +147 -0
  163. package/dist/ui/shared/header-tools.js +1 -1
  164. package/dist/ui/shared/identity-card.js +202 -0
  165. package/dist/ui/shared/identity-color.js +261 -0
  166. package/dist/ui/shared/instance-nav.js +37 -19
  167. package/dist/ui/shared/owner-css.js +221 -0
  168. package/dist/ui/shared/owner-effect-prefs.js +117 -0
  169. package/dist/ui/shared/panel-view.js +81 -8
  170. package/dist/ui/shared/signin.js +2 -0
  171. package/dist/ui/shared/ui-dialog.js +1 -1
  172. package/dist/ui/shared/ui-icons.js +2 -0
  173. package/dist/ui/shared/ui-menu.js +1 -1
  174. package/dist/ui/shared/ui-toast.js +1 -1
  175. package/dist/ui/shared/usage-dialog.js +14 -2
  176. package/dist/ui/shared/view-strings.js +36 -2
  177. package/dist/ui/signin.html +1 -0
  178. package/dist/usage/providers.d.ts +3 -0
  179. package/dist/usage/providers.js +4 -3
  180. package/dist/usage/providers.js.map +1 -1
  181. package/dist/view-api.d.ts +29 -0
  182. package/dist/view-api.js +188 -8
  183. package/dist/view-api.js.map +1 -1
  184. package/dist/web-api.d.ts +42 -0
  185. package/dist/web-api.js +898 -36
  186. package/dist/web-api.js.map +1 -1
  187. package/dist/web-appearance-limits.d.ts +82 -0
  188. package/dist/web-appearance-limits.js +83 -0
  189. package/dist/web-appearance-limits.js.map +1 -0
  190. package/dist/web-appearance-policy.d.ts +66 -0
  191. package/dist/web-appearance-policy.js +94 -0
  192. package/dist/web-appearance-policy.js.map +1 -0
  193. package/dist/web-appearance-tools.d.ts +50 -0
  194. package/dist/web-appearance-tools.js +105 -0
  195. package/dist/web-appearance-tools.js.map +1 -0
  196. package/dist/web-appearance-view.d.ts +99 -0
  197. package/dist/web-appearance-view.js +163 -0
  198. package/dist/web-appearance-view.js.map +1 -0
  199. package/dist/web-appearance.d.ts +79 -0
  200. package/dist/web-appearance.js +298 -0
  201. package/dist/web-appearance.js.map +1 -0
  202. package/dist/web-asset-parts.d.ts +93 -0
  203. package/dist/web-asset-parts.js +133 -0
  204. package/dist/web-asset-parts.js.map +1 -0
  205. package/dist/web-auth.js +17 -1
  206. package/dist/web-auth.js.map +1 -1
  207. package/dist/web-card.d.ts +96 -0
  208. package/dist/web-card.js +232 -0
  209. package/dist/web-card.js.map +1 -0
  210. package/dist/web-chat-history.d.ts +13 -0
  211. package/dist/web-chat-history.js +23 -2
  212. package/dist/web-chat-history.js.map +1 -1
  213. package/dist/web-effect-bundle.d.ts +28 -0
  214. package/dist/web-effect-bundle.js +126 -0
  215. package/dist/web-effect-bundle.js.map +1 -0
  216. package/dist/web-effect-canonical.d.ts +8 -0
  217. package/dist/web-effect-canonical.js +27 -0
  218. package/dist/web-effect-canonical.js.map +1 -0
  219. package/dist/web-effect-library.d.ts +114 -0
  220. package/dist/web-effect-library.js +530 -0
  221. package/dist/web-effect-library.js.map +1 -0
  222. package/dist/web-effect-poster.d.ts +8 -0
  223. package/dist/web-effect-poster.js +52 -0
  224. package/dist/web-effect-poster.js.map +1 -0
  225. package/dist/web-effect-profiles.d.ts +118 -0
  226. package/dist/web-effect-profiles.js +180 -0
  227. package/dist/web-effect-profiles.js.map +1 -0
  228. package/dist/web-effect-service.d.ts +86 -0
  229. package/dist/web-effect-service.js +314 -0
  230. package/dist/web-effect-service.js.map +1 -0
  231. package/dist/web-effect-tools.d.ts +47 -0
  232. package/dist/web-effect-tools.js +129 -0
  233. package/dist/web-effect-tools.js.map +1 -0
  234. package/dist/web-effect-trials.d.ts +86 -0
  235. package/dist/web-effect-trials.js +204 -0
  236. package/dist/web-effect-trials.js.map +1 -0
  237. package/dist/web-effect-upload.d.ts +27 -0
  238. package/dist/web-effect-upload.js +185 -0
  239. package/dist/web-effect-upload.js.map +1 -0
  240. package/dist/web-effect-worker.d.ts +8 -0
  241. package/dist/web-effect-worker.js +170 -0
  242. package/dist/web-effect-worker.js.map +1 -0
  243. package/dist/web-effect.d.ts +103 -0
  244. package/dist/web-effect.js +153 -0
  245. package/dist/web-effect.js.map +1 -0
  246. package/dist/web-identity-color.d.ts +87 -0
  247. package/dist/web-identity-color.js +209 -0
  248. package/dist/web-identity-color.js.map +1 -0
  249. package/dist/web-identity-palette.d.ts +113 -0
  250. package/dist/web-identity-palette.js +84 -0
  251. package/dist/web-identity-palette.js.map +1 -0
  252. package/dist/web-image-meta.d.ts +25 -0
  253. package/dist/web-image-meta.js +200 -0
  254. package/dist/web-image-meta.js.map +1 -0
  255. package/dist/web-mod-agent.d.ts +50 -0
  256. package/dist/web-mod-agent.js +205 -0
  257. package/dist/web-mod-agent.js.map +1 -0
  258. package/dist/web-mod-bundle.d.ts +149 -0
  259. package/dist/web-mod-bundle.js +470 -0
  260. package/dist/web-mod-bundle.js.map +1 -0
  261. package/dist/web-mod-export.d.ts +59 -0
  262. package/dist/web-mod-export.js +79 -0
  263. package/dist/web-mod-export.js.map +1 -0
  264. package/dist/web-mod-store.d.ts +283 -0
  265. package/dist/web-mod-store.js +625 -0
  266. package/dist/web-mod-store.js.map +1 -0
  267. package/dist/web-org.d.ts +3 -3
  268. package/dist/web-org.js +11 -17
  269. package/dist/web-org.js.map +1 -1
  270. package/dist/web-owner-css-store.d.ts +180 -0
  271. package/dist/web-owner-css-store.js +384 -0
  272. package/dist/web-owner-css-store.js.map +1 -0
  273. package/dist/web-owner-css.d.ts +58 -0
  274. package/dist/web-owner-css.js +552 -0
  275. package/dist/web-owner-css.js.map +1 -0
  276. package/dist/web-prefs.d.ts +48 -0
  277. package/dist/web-prefs.js +209 -33
  278. package/dist/web-prefs.js.map +1 -1
  279. package/dist/web-preview.d.ts +9 -1
  280. package/dist/web-preview.js +24 -7
  281. package/dist/web-preview.js.map +1 -1
  282. package/dist/web-settings-patch.d.ts +18 -0
  283. package/dist/web-settings-patch.js +51 -0
  284. package/dist/web-settings-patch.js.map +1 -1
  285. package/dist/web-shell-routes.d.ts +1 -1
  286. package/dist/web-shell-routes.js +1 -1
  287. package/dist/web-shell-routes.js.map +1 -1
  288. package/dist/web-theme-store.d.ts +218 -0
  289. package/dist/web-theme-store.js +516 -0
  290. package/dist/web-theme-store.js.map +1 -0
  291. package/dist/web-theme.d.ts +121 -0
  292. package/dist/web-theme.js +407 -0
  293. package/dist/web-theme.js.map +1 -0
  294. package/dist/web-widget.d.ts +72 -0
  295. package/dist/web-widget.js +245 -0
  296. package/dist/web-widget.js.map +1 -0
  297. package/package.json +1 -1
  298. package/templates/systemd.service.ejs +8 -0
@@ -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 */
@@ -138,7 +157,9 @@ html:has(.shell), html:has(.shell) body { overflow: hidden; }
138
157
  }
139
158
  /* #1604: one row — Settings and Session (the anonymous View reader: Sign in and the theme/language popover). Their
140
159
  pop-ups open over the whole footer's width. */
141
- .side-foot { display: flex; align-items: center; gap: var(--s-1); padding: var(--s-2); position: relative; }
160
+ /* #1922: a divider above the footer, with the filter block's bottom padding and the footer's top padding trimmed to
161
+ --s-1: the gap reads as a section break, not an empty ~20 px band (the two --s-2 paddings used to stack). */
162
+ .side-foot { display: flex; align-items: center; gap: var(--s-1); padding: var(--s-1) var(--s-2) var(--s-2); border-top: 1px solid var(--border); position: relative; }
142
163
  .side-foot > .side-row, .side-foot > .session { flex: 1; min-width: 0; }
143
164
  .side-foot > .session { position: static; }
144
165
  .side-foot .side-row span:not(.dot):not(.meta) { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@@ -346,6 +367,8 @@ html:has(.shell), html:has(.shell) body { overflow: hidden; }
346
367
  .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
368
  .seg-item:hover { background: var(--surface); color: var(--text); }
348
369
  .seg-item.active { background: var(--surface-2); color: var(--text); font-weight: var(--fw-semibold); }
370
+ /* #1629: Settings has eight sections (Appearance added): they wrap to a second row rather than hide behind a scroll. */
371
+ .p-settings .seg { flex-wrap: wrap; row-gap: var(--s-1); overflow-x: visible; }
349
372
  .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
373
  .rows { display: flex; flex-direction: column; gap: var(--s-1); }
351
374
  .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 +524,20 @@ html.lb-open .scroller { overflow: hidden; }
501
524
  CSSOM), a full-screen sheet on a narrow one. The frame is the card's (preview.js), sized by the panel here. */
502
525
  .chat-split { flex: 1; min-height: 0; display: flex; }
503
526
  .chat-main { flex: 1; min-width: 0; min-height: 0; display: flex; flex-direction: column; }
527
+ /* #1629: an agent's theme on its own chat surface — custom properties set on .p-chat through the CSSOM, choices as
528
+ these tokens. The surface paints its own background (the page's body is not its). */
529
+ .p-chat.th { background: var(--bg); color: var(--text); }
530
+ .p-chat.th .msg.user .body { border-radius: var(--bubble-radius, var(--r-xl)); }
531
+ .p-chat.th-font-system .thread, .p-chat.th-font-system .composer textarea { font-family: system-ui, -apple-system, "Segoe UI", sans-serif; }
532
+ .p-chat.th-font-mono .thread, .p-chat.th-font-mono .composer textarea { font-family: var(--font-mono); }
533
+ .p-chat.th-bubble-thin .msg.user .body { box-shadow: inset 0 0 0 1px var(--border); }
534
+ .p-chat.th-bubble-accent .msg.user .body { box-shadow: inset 0 0 0 1px var(--accent); }
535
+ .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); }
536
+ .chat-main.has-th-bg { position: relative; }
537
+ .chat-main.has-th-bg > :not(.th-bg) { position: relative; z-index: 1; }
538
+ .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; }
539
+ .th-bg-cover { object-fit: cover; }
540
+ .th-bg-contain { object-fit: contain; }
504
541
  .pv-divider { flex: 0 0 8px; cursor: col-resize; background: var(--bg); border-left: 1px solid var(--border); touch-action: none; position: relative; }
505
542
  .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
543
  .pv-divider:hover::after, .pv-divider:focus-visible::after, .chat-split.resizing .pv-divider::after { background: var(--accent); }
@@ -567,6 +604,9 @@ html.lb-open .scroller { overflow: hidden; }
567
604
  .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
605
  .prompt.done { background: transparent; border-style: dashed; color: var(--text-2); }
569
606
  .prompt .txt { white-space: pre-wrap; overflow-wrap: anywhere; }
607
+ /* #1990: a name in a status line or a prompt that opens that instance in the app. */
608
+ .st-link { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; }
609
+ .st-link:hover { text-decoration-thickness: 2px; }
570
610
  .prompt .acts { display: flex; gap: var(--s-2); margin-top: var(--s-2); align-items: center; flex-wrap: wrap; }
571
611
  .prompt .wait { font-size: var(--fs-xs); color: var(--text-3); }
572
612
  .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 +650,58 @@ html.lb-open .scroller { overflow: hidden; }
610
650
  .drop-card { display: flex; flex-direction: column; align-items: center; gap: var(--s-2); color: var(--accent); font-weight: var(--fw-semibold); }
611
651
 
612
652
  /* ── Toasts ── */
653
+ /* #1629 r4 §3: Settings → Appearance (owner CSS). */
654
+ .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; }
655
+ .s-widget-preview { max-width: 320px; }
656
+ /* #1629 r4-5: Settings → Appearance → Mods — the plan is shown in full before anything is applied. */
657
+ .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); }
658
+ .s-mod-plan h4 { font-size: var(--fs-lg); font-weight: var(--fw-semibold); overflow-wrap: anywhere; }
659
+ .s-mod-plan h4 .muted { font-size: var(--fs-sm); font-weight: var(--fw-regular); }
660
+ .s-mod-items { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--s-3); }
661
+ .s-mod-item { display: flex; flex-direction: column; gap: var(--s-1); }
662
+ .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; }
663
+ .s-mod-widget { display: block; max-width: min(360px, 100%); }
664
+ .s-mod-theme { display: flex; flex-direction: column; gap: var(--s-1); min-width: 0; width: 100%; }
665
+ .s-mod-kv { display: flex; gap: var(--s-2); align-items: baseline; flex-wrap: wrap; min-width: 0; }
666
+ .s-mod-kv .k { color: var(--text-3); font-size: var(--fs-xs); min-width: 9em; }
667
+ .s-mod-kv .v { color: var(--text); min-width: 0; overflow-wrap: anywhere; }
668
+ .s-mod-sws { display: flex; flex-wrap: wrap; gap: var(--s-1) var(--s-3); }
669
+ .s-mod-sw { display: inline-flex; align-items: center; gap: var(--s-1); }
670
+ .s-mod-sw-chip { display: inline-block; width: 12px; height: 12px; border-radius: 3px; border: 1px solid var(--border-strong); background: var(--sw, transparent); }
671
+ .s-mod-pic { display: inline-flex; align-items: center; gap: var(--s-2); min-width: 0; }
672
+ .s-mod-pic img { width: 64px; height: 40px; object-fit: cover; border-radius: var(--r-sm); border: 1px solid var(--border); }
673
+ .s-mod-raw summary { cursor: pointer; font-size: var(--fs-xs); color: var(--text-2); }
674
+ .s-mod-widget .aw-chat_header { border-width: 1px; border-radius: var(--r-md); }
675
+ .s-mod-text { white-space: pre-wrap; overflow-wrap: anywhere; }
676
+ .s-mod-warn { color: var(--warn); font-size: var(--fs-sm); padding-left: 1.75rem; }
677
+ .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; }
678
+ .s-mod-err { white-space: pre-line; }
679
+ .s-mod-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--s-2); }
680
+ .s-mod-list li { display: flex; align-items: center; gap: var(--s-2); flex-wrap: wrap; min-width: 0; }
681
+ .s-mod-list .grow { flex: 1; min-width: 0; overflow-wrap: anywhere; }
682
+ .s-mod-proposals { display: flex; flex-direction: column; gap: var(--s-2); }
683
+ .s-mod-proposals h4 { font-size: var(--fs-sm); font-weight: var(--fw-semibold); color: var(--text-2); }
684
+ .s-mod-export summary { cursor: pointer; font-weight: var(--fw-medium); }
685
+ .s-mod-export[open] { display: flex; flex-direction: column; gap: var(--s-2); }
686
+ .s-mod-boxes { display: flex; flex-wrap: wrap; gap: var(--s-1) var(--s-4); }
687
+ .s-css-actions, .s-widget-actions { display: flex; gap: var(--s-2); flex-wrap: wrap; }
688
+ .s-css-status { margin: 0; font-size: var(--fs-sm); color: var(--text-2); }
689
+ .s-css-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--s-2); }
690
+ .s-css-list li { display: flex; align-items: center; gap: var(--s-2); flex-wrap: wrap; min-width: 0; }
691
+ .s-css-list code { overflow-wrap: anywhere; }
692
+ .s-owner-css h4 { margin: var(--s-2) 0 0; font-size: var(--fs-sm); }
693
+ /* #1629 r4 §3: keep-or-revert after saving custom CSS — on every page, above everything but dialogs' scrim. */
694
+ .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);
695
+ flex-wrap: wrap; max-width: calc(100vw - 32px); padding: var(--s-2) var(--s-3); border-radius: var(--r-md); background: var(--surface);
696
+ color: var(--text-1); border: 1px solid var(--border); box-shadow: var(--shadow-2); font-size: var(--fs-sm); }
697
+ .oc-keep-text { display: inline-flex; align-items: baseline; gap: var(--s-2); min-width: 0; }
698
+ .oc-keep-q { font-weight: var(--fw-medium); }
699
+ .oc-keep-left { color: var(--text-3); font-variant-numeric: tabular-nums; }
700
+ /* A phone: one row across the top — the question over the seconds left, then Keep and Revert. */
701
+ @media (max-width: 479px) {
702
+ .oc-keep { left: var(--s-2); right: var(--s-2); transform: none; max-width: none; flex-wrap: nowrap; }
703
+ .oc-keep-text { flex: 1; flex-direction: column; align-items: flex-start; gap: 0; }
704
+ }
613
705
  .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
706
  .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
707
  .toast.err { background: var(--danger-fill); color: var(--on-danger); }
@@ -691,6 +783,9 @@ html.lb-open .scroller { overflow: hidden; }
691
783
  .inst-star.on { color: var(--warn); }
692
784
  .inst-star.on svg, .fav-mark { fill: currentColor; }
693
785
  .fav-head { cursor: default; }
786
+ /* #1936: the open instance, pinned on top while a filter is on — its header reads "Current", a line under it. */
787
+ .pinned-head { cursor: default; }
788
+ .v-pinned { border-bottom: 1px solid var(--border); padding-bottom: var(--s-2); margin-bottom: var(--s-1); }
694
789
  .fav-mark { color: var(--warn); }
695
790
  .inst-nav:not(.fav-editing) .inst-star:not(.on) { visibility: hidden; }
696
791
  /* As specific as the rule that hides it (and later), so hover and focus win. */
@@ -710,7 +805,7 @@ body.side-resizing, body.side-resizing * { cursor: col-resize !important; user-s
710
805
  .dragging { opacity: 0.4; }
711
806
  .drop-before { box-shadow: inset 0 2px 0 var(--accent); }
712
807
  .drop-after { box-shadow: inset 0 -2px 0 var(--accent); }
713
- .v-filter { flex: none; padding: var(--s-2) var(--s-3) var(--s-2); border-top: 1px solid var(--border); }
808
+ .v-filter { flex: none; padding: var(--s-2) var(--s-3) var(--s-1); border-top: 1px solid var(--border); }
714
809
  .v-filter-box { display: flex; align-items: center; gap: var(--s-2); padding: 0 var(--s-1) 0 var(--s-3); border-radius: var(--r-sm); background: var(--surface); color: var(--text-3); }
715
810
  .v-filter-box:focus-within { box-shadow: 0 0 0 2px var(--focus); }
716
811
  .v-filter-box input { flex: 1; min-width: 0; height: 34px; border: 0; background: transparent; color: var(--text); font-size: var(--fs-sm); outline: none; }
@@ -773,6 +868,10 @@ body.side-resizing, body.side-resizing * { cursor: col-resize !important; user-s
773
868
  /* #1749: one line beside ↑↓ — the name gives way first, then the plan badge (each with an ellipsis); ↑↓ never wrap. */
774
869
  .u-head strong { min-width: 0; flex: 0 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
775
870
  .u-plan { min-width: 0; flex: none; max-width: 55%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
871
+ /* #2014: a credential profile's row — the provider name beside the plan badge, and the profile on its own full-width
872
+ muted line under the head (one line in the rail, whole in the dialog). */
873
+ .u-profile { margin-top: calc(-1 * var(--s-2) + 2px); color: var(--text-3); font-size: var(--fs-xs); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
874
+ .dlg .u-profile { white-space: normal; overflow-wrap: anywhere; }
776
875
  .u-order { margin-left: auto; display: flex; gap: 2px; flex: none; }
777
876
  .u-order .icon-btn { width: 28px; height: 28px; }
778
877
  .u-order .icon-btn:disabled { opacity: 0.3; }
@@ -1099,3 +1198,126 @@ strong.org-name { font-size: var(--fs-sm); color: var(--text); }
1099
1198
  /* #1682: a mention, as Discord draws one — accent text on a tint of it; the id (or the Telegram handle) on hover. */
1100
1199
  .md .mention { display: inline; padding: 0 0.2em; border-radius: var(--r-sm); color: var(--accent); background: var(--accent-dim);
1101
1200
  font-weight: var(--fw-medium); white-space: nowrap; }
1201
+
1202
+ /* #1629 r4 §1 / r4.1 Q3 (a): the identity card (identity-card.js). The size is a class token; its heights are here
1203
+ (S 160 / M 240 / L 360 px, on a phone 120 / 180 / 240); the banner scales with the portrait. Never a style attribute. */
1204
+ .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; }
1205
+ .id-card.card-s { --id-h: 160px; }
1206
+ .id-card.card-l { --id-h: 360px; }
1207
+ .id-card.card-c { --id-h: 96px; margin: 0; border: 0; border-radius: 0; background: none; }
1208
+ .id-banner { display: block; width: 100%; height: calc(var(--id-h) / 2); object-fit: cover; }
1209
+ .id-row { display: flex; align-items: flex-start; gap: var(--s-3); padding: var(--s-3) var(--s-4); }
1210
+ .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); }
1211
+ .id-text { flex: 1; min-width: 0; }
1212
+ .id-tagline { margin: 0 0 var(--s-1); font-weight: var(--fw-semibold); overflow-wrap: anywhere; }
1213
+ .id-bio { margin: 0; color: var(--text-2); white-space: pre-line; overflow-wrap: anywhere; }
1214
+ .card-size { display: flex; flex-shrink: 0; gap: var(--s-1); }
1215
+ .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; }
1216
+ .card-size .seg-item.active { border-color: var(--accent); background: var(--accent-dim); color: var(--text); }
1217
+ .chat-card { flex-shrink: 0; border-bottom: 1px solid var(--border); }
1218
+ /* #1629 r4-4: widgets — the page's own components, from a declarative spec (agent-widget.js). The host draws the 1px
1219
+ frame and the ✦ chip, outside anything the spec chooses; nothing in a widget takes a click or focus. Colours are the
1220
+ theme's tokens (an agent's chat header sits inside its themed chat), so a person's own CSS can restyle them too. */
1221
+ .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; }
1222
+ .aw-head { display: flex; align-items: center; gap: var(--s-2); min-width: 0; }
1223
+ .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; }
1224
+ .aw-by { font-size: var(--fs-xs); color: var(--text-3); white-space: nowrap; }
1225
+ .aw-items { display: flex; flex-direction: column; gap: var(--s-2); min-width: 0; }
1226
+ .aw-item { display: flex; align-items: center; gap: var(--s-2); min-width: 0; font-size: var(--fs-sm); }
1227
+ .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; }
1228
+ .aw-value { color: var(--text); font-weight: var(--fw-medium); overflow-wrap: anywhere; min-width: 0; }
1229
+ .aw-stat .aw-value { margin-left: auto; font-variant-numeric: tabular-nums; }
1230
+ .aw-unit { margin-left: 2px; color: var(--text-2); font-weight: var(--fw-regular); }
1231
+ .aw-text .aw-value { font-weight: var(--fw-regular); }
1232
+ .aw-bar { flex: 1; min-width: 48px; height: 6px; appearance: none; border: 0; border-radius: var(--r-full); background: var(--surface-2); overflow: hidden; }
1233
+ .aw-bar::-webkit-progress-bar { background: var(--surface-2); border-radius: var(--r-full); }
1234
+ .aw-bar::-webkit-progress-value { background: var(--accent); border-radius: var(--r-full); }
1235
+ .aw-bar::-moz-progress-bar { background: var(--accent); border-radius: var(--r-full); }
1236
+ .aw-progress .aw-value { margin-left: auto; font-size: var(--fs-xs); font-variant-numeric: tabular-nums; min-width: 3.5em; text-align: right; }
1237
+ .aw-list { flex-direction: column; align-items: stretch; gap: var(--s-1); }
1238
+ .aw-list ul { margin: 0; padding-left: 1.2em; display: flex; flex-direction: column; gap: 2px; overflow-wrap: anywhere; }
1239
+ .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); }
1240
+ .aw-tone-ok { background: var(--ok-bg); color: var(--ok); }
1241
+ .aw-tone-warn { background: var(--warn-bg); color: var(--warn); }
1242
+ .aw-tone-bad { background: var(--danger-bg); color: var(--danger); }
1243
+ .aw-tone-info { background: var(--accent-dim); color: var(--accent); }
1244
+ .aw-spark { flex: 1; min-width: 48px; height: 24px; }
1245
+ .aw-spark polyline { fill: none; stroke: var(--accent); stroke-width: 1.5; vector-effect: non-scaling-stroke; }
1246
+ .aw-spark-item .aw-value { font-size: var(--fs-xs); font-variant-numeric: tabular-nums; }
1247
+ /* The chat header strip: one compact row under the card, in that agent's chat only. When it is wider than the chat (a
1248
+ phone), the row scrolls sideways inside its own box — never wraps out of it, never cuts an item. */
1249
+ .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; }
1250
+ .aw-chat_header .aw-head { flex-shrink: 0; }
1251
+ .aw-chat_header .aw-items { flex-direction: row; flex: 1 0 auto; gap: var(--s-4); }
1252
+ .aw-chat_header .aw-item { flex: 0 0 auto; white-space: nowrap; }
1253
+ .aw-chat_header .aw-value { white-space: nowrap; }
1254
+ .aw-chat_header .aw-text .aw-value { display: inline-block; max-width: 24em; overflow: hidden; text-overflow: ellipsis; vertical-align: bottom; }
1255
+ .aw-chat_header .aw-stat .aw-value { margin-left: 0; }
1256
+ .aw-chat_header .aw-bar { flex: 0 0 auto; width: 96px; }
1257
+ /* 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). */
1258
+ @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); } }
1259
+ .aw-details { background: var(--surface); margin-bottom: var(--s-4); padding: var(--s-4) var(--s-5); }
1260
+ .side-widget { padding: 0 var(--s-3) var(--s-2); }
1261
+ .aw-sidebar { max-height: 240px; overflow: auto; }
1262
+
1263
+ .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; }
1264
+ .chat-card-peek { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-3); }
1265
+ @media (max-width: 479px) {
1266
+ .id-card.card-s { --id-h: 120px; }
1267
+ .id-card.card-m { --id-h: 180px; }
1268
+ .id-card.card-l { --id-h: 240px; }
1269
+ .id-row { flex-wrap: wrap; }
1270
+ .id-text { flex-basis: 100%; }
1271
+ }
1272
+ /* #1629 r4 §1: the card in View (above the profile row) and its part of Edit profile. */
1273
+ .v-id { flex-shrink: 0; padding: var(--s-2) var(--s-4) 0; background: var(--bg); }
1274
+ .v-id .id-card { margin: 0; }
1275
+ .form-sub { margin: var(--s-2) 0 0; font-size: var(--fs-sm); font-weight: var(--fw-semibold); }
1276
+ /* #1629 r4.1 Q3 (b): Settings → Appearance, the card's upload limits (three to a row; one on a phone). */
1277
+ .grid3 { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--s-3) var(--s-4); margin-bottom: var(--s-3); }
1278
+ .s-limit-warnings { margin: var(--s-2) 0; padding-left: var(--s-4); }
1279
+ @media (max-width: 479px) { .grid3 { grid-template-columns: minmax(0, 1fr); } }
1280
+
1281
+ /* #1629 B2: effect library controls. No runtime layer is created by this card. */
1282
+ .s-fx-actions { display: flex; flex-wrap: wrap; gap: var(--s-2); align-items: center; }
1283
+ .s-fx-item { margin-top: var(--s-3); padding-top: var(--s-3); border-top: 1px solid var(--border); }
1284
+ .s-fx-item h4 { margin: 0; }
1285
+ .s-fx-poster { display: block; max-width: min(240px, 100%); max-height: 160px; object-fit: contain; border-radius: var(--r-sm); }
1286
+
1287
+ .s-fx-chat, .s-fx-params label:not(.check) { display: flex; align-items: center; flex-wrap: wrap; gap: var(--s-2); }
1288
+ .s-fx-chat { margin: var(--s-2) 0; }
1289
+ .s-fx-params { display: flex; flex-direction: column; gap: var(--s-2); margin: var(--s-2) 0; max-width: 100%; }
1290
+ .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); }
1291
+
1292
+ /* #1629: stock paint bands. Structural ancestors remain neutral; only bounded content owns level 1.
1293
+ The viewport effect root is a direct body sibling: behind 0 / stock content 1 (header 2) / front 20.
1294
+ Nested menus cannot cross a content stacking context: the unmounted trust guard suspends fronts for overlays. */
1295
+ .app-root { z-index: auto; }
1296
+ .conn, .panel-head, .chat-card, .aw-chat_header, .chat-main > .scroller, .chat-main > .dock,
1297
+ .sidebar > .side-head, .sidebar > .side-nav, .sidebar > .side-section, .sidebar > .side-widget, .sidebar > .side-foot,
1298
+ .u-rail > .u-rail-head, .u-rail > .u-rail-body { position: relative; z-index: 1; }
1299
+ /* Header menus must stay above the later scroller stacking context. */
1300
+ .panel-head { z-index: 2; }
1301
+ @media (min-width: 900px) { .chat-split > .pv-divider, .chat-split > .pv-panel { position: relative; z-index: 1; } }
1302
+ /* #1629: owner choices share the existing control font floor. */
1303
+ .s-fx-owner{margin-top:var(--s-4);padding-top:var(--s-3);border-top:1px solid var(--border)}
1304
+ .s-fx-owner-row{display:flex;align-items:end;flex-wrap:wrap;gap:var(--s-2);margin-top:var(--s-2)}
1305
+ .s-fx-owner-row>label{display:flex;flex-direction:column;gap:var(--s-1);min-width:0;flex:1}
1306
+
1307
+ /* #1629: host-created static poster IMG; geometry and visibility belong to the trusted CSSOM binding. */
1308
+ .agend-effect-poster { object-fit: cover !important; object-position: center !important; user-select: none; }
1309
+
1310
+ /* #1629: full-app bounded stock surfaces. Panel roots stay neutral; effects never raise the whole app. */
1311
+ .p-fleet > .seg, .p-settings > .seg,
1312
+ .p-fleet > .panel-body, .p-needs > .panel-body, .p-settings > .panel-body,
1313
+ .p-log > .panel-body, .p-details > .panel-body, .p-view > .panel-body,
1314
+ .p-log > .log-bar, .p-view > .v-term, .p-view > .v-id, .p-view > .v-card { position: relative; z-index: 1; }
1315
+ /* Keep the existing Apply bar's sticky behavior, in its own bounded stock band. */
1316
+ .p-settings > .s-pending { z-index: 1; }
1317
+
1318
+ /* #1629: host-owned local Trial panel. Only its canvas frame lives inside the paint-contained stage. */
1319
+ .effect-trial-head, .effect-trial-controls, .effect-trial-note { position: relative; z-index: 1; background: var(--bg); padding: var(--s-3); margin: 0; }
1320
+ .effect-trial-head { display: flex; align-items: center; gap: var(--s-2); border-bottom: 1px solid var(--border); }
1321
+ .effect-trial-head > span { flex: 1; min-width: 0; overflow-wrap: anywhere; }
1322
+ .effect-trial-body { flex: 1; min-height: 0; display: flex; padding: var(--s-3); }
1323
+ .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
+ }