@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
@@ -139,14 +139,31 @@ import { LogService } from "./log-service.js";
139
139
  import { WebChatHistory, WEB_CHAT_TEXT_MAX, isWebMessageId, newWebMessageId } from "./web-chat-history.js";
140
140
  import { WebChatDiskStore } from "./web-chat-store.js";
141
141
  import { SeenUsers, resolveMentions } from "./web-mentions.js";
142
- import { WebPrefsStore } from "./web-prefs.js";
142
+ import { OPERATOR, WebPrefsStore } from "./web-prefs.js";
143
+ import { agendVersion } from "./agent-whats-new.js";
144
+ import { OwnerCssStore } from "./web-owner-css-store.js";
145
+ import { ThemeStore } from "./web-theme-store.js";
146
+ import { WebAppearance } from "./web-appearance.js";
147
+ import { WebEffectLibrary } from "./web-effect-library.js";
148
+ import { WebEffectTrials } from "./web-effect-trials.js";
149
+ import { WebEffectService } from "./web-effect-service.js";
150
+ import { WebEffectProfiles } from "./web-effect-profiles.js";
151
+ import { WebEffectTools, WEB_EFFECT_TOOLS } from "./web-effect-tools.js";
152
+ import { resolveEffectParams } from "./web-effect.js";
153
+ import { cw1EffectChecker } from "./web-mod-bundle.js";
154
+ import { effectiveAppearanceLimits } from "./web-appearance-limits.js";
143
155
  import { WEB_NAME_FALLBACK, safeWebName, validateWebName, webNameInUse, webPersonKey } from "./web-profile.js";
144
156
  import { ReplyButtonStore, parseReplyButtons, replyButtonClickText, replyButtonsFallbackText, REPLY_BUTTON_PREFIX } from "./reply-buttons.js";
145
157
  import { ReplyButtonsController } from "./reply-buttons-controller.js";
146
158
  import { parsePoll } from "./reply-polls.js";
147
159
  import { isUploadRequest, limitRequestReceive, publicAttachment, sweepOrphanedUploads, UPLOAD_REQUEST_TIMEOUT_MS, uploadLimits, WebFileLedger } from "./web-upload.js";
148
160
  import { REPLY_FILES_MAX_BYTES, snapshotReplyFile, sweepReplySnapshots, webFilesDir } from "./web-reply-files.js";
149
- import { handleViewRequest, isViewPath, profileIdentities, resolveInstanceIdentity } from "./view-api.js";
161
+ import { cardPicture, handleViewRequest, humanCards, identityColors, isViewPath, profileIdentities, resolveInstanceIdentity } from "./view-api.js";
162
+ import { autoIdentityColor } from "./web-identity-color.js";
163
+ import { AppearanceTools } from "./web-appearance-tools.js";
164
+ import { cardVersion, resolveCard } from "./web-card.js";
165
+ import { ModStore } from "./web-mod-store.js";
166
+ import { AgentMods } from "./web-mod-agent.js";
150
167
  import { downloadStickerImage } from "./channel/sticker-download.js";
151
168
  import { WebImageCache, imageMimeForFile, WEB_IMAGE_MAX_AGE_MS, WEB_IMAGE_MAX_BYTES, WEB_IMAGE_RETRY_MS } from "./web-images.js";
152
169
  import { accentColor, avatarFor, ownBotConnections } from "./instance-avatar.js";
@@ -192,7 +209,8 @@ import { NeedsYouHub } from "./needs-you-hub.js";
192
209
  import { WEB_CHAT_NOTICE, claimNotice, hasWebChat, releaseNotice, upgradeNoticesPath } from "./upgrade-notices.js";
193
210
  import { GENERAL_PAUSE_ERROR, isGeneralInstance } from "./general-instance.js";
194
211
  import { buildOrgChart } from "./web-org.js";
195
- import { CacheService, WINDOWS } from "./cache-service.js";
212
+ import { GENERAL_POINTING_REFUSALS, SNOWFLAKE, hasPlaceRefs, placeRef, webRoute, webTextLinks, placeSuffix, renderPlaceRefs, stripPlaceRefs, topicUrl, worldPlaceOf } from "./topic-link.js";
213
+ import { CacheService, cacheWindow } from "./cache-service.js";
196
214
  import { runWebCommand } from "./web-commands.js";
197
215
  import { CompactWatch, launchIdentity } from "./compact-watch.js";
198
216
  import { agyLegacyAgentsFile, danglingSymlink, migrateAgyAgentsFile } from "./backend/antigravity.js";
@@ -884,6 +902,183 @@ export class FleetManager {
884
902
  return true;
885
903
  return false;
886
904
  }
905
+ themeStoreInstance;
906
+ webAppearanceService;
907
+ /** #1629: every instance's theme, placements, assets and lock (web-themes.json). */
908
+ get themeStore() {
909
+ // r4.1 Q3 (b): the card's limits are read from fleet.yaml at each upload (a change applies without a restart).
910
+ return (this.themeStoreInstance ??= new ThemeStore(this.dataDir, { limits: () => effectiveAppearanceLimits(this.fleetConfig?.web?.appearance).limits, effectsDropped: ids => this.effectLibrary.collectUnreferenced(ids) }));
911
+ }
912
+ /** #1629: the theme tools, a person's reset and lock, and the one appearance rate limiter (shared with effects). */
913
+ get webAppearance() {
914
+ return (this.webAppearanceService ??= new WebAppearance(this.themeStore, () => this.fleetConfig?.web ?? null, () => { try {
915
+ this.emitSseEvent("status", this.getUiStatus());
916
+ }
917
+ catch { /* the next frame carries the rev */ } }));
918
+ }
919
+ effectLibraryInstance;
920
+ effectTrialsInstance;
921
+ /** Lazy, single-owner storage. Register every host reference source before any operation can collect. */
922
+ get effectLibrary() {
923
+ if (!this.effectLibraryInstance) {
924
+ const library = new WebEffectLibrary(this.dataDir);
925
+ // Providers are lazy; publish only after every registration succeeds.
926
+ library.registerReferenceSource("themes", () => {
927
+ if (!this.themeStore.readable())
928
+ throw new Error("themes_unreadable");
929
+ return this.themeStore.placedEffects().flatMap(row => row.ids.map(id => ({ id, holder: `instance:${row.name}` })));
930
+ });
931
+ library.registerReferenceSource("prefs", () => this.webPrefs.ownerEffectReferences());
932
+ library.registerReferenceSource("mods", () => {
933
+ const mods = this.mods.list();
934
+ if (mods.unreadable)
935
+ throw new Error("mods_unreadable");
936
+ return mods.mods.flatMap(mod => mod.effects.map(effect => ({ id: effect.id, holder: `mod:${mod.id}` })));
937
+ });
938
+ this.effectTrialsInstance = new WebEffectTrials({ library, known: name => this.isKnownInstance(name) });
939
+ this.effectLibraryInstance = library;
940
+ }
941
+ return this.effectLibraryInstance;
942
+ }
943
+ /** Volatile Try offers and session-bound runs share this library. */
944
+ get effectTrials() { if (!this.effectTrialsInstance)
945
+ void this.effectLibrary; return this.effectTrialsInstance; }
946
+ effectProfilesInstance;
947
+ get effectProfiles() { return (this.effectProfilesInstance ??= new WebEffectProfiles(this.dataDir)); }
948
+ effectServiceInstance;
949
+ get effectService() {
950
+ return (this.effectServiceInstance ??= new WebEffectService({ library: this.effectLibrary, themes: this.themeStore, profiles: this.effectProfiles, prefs: this.webPrefs, trials: this.effectTrials,
951
+ known: name => this.isKnownInstance(name), changed: () => { try {
952
+ this.emitSseEvent("status", this.getUiStatus());
953
+ }
954
+ catch { /* next status carries revision */ } } }));
955
+ }
956
+ effectToolsService;
957
+ /** Caller identity is supplied only by the owned IPC connection / verified agent endpoint token. */
958
+ get effectTools() {
959
+ return (this.effectToolsService ??= new WebEffectTools({
960
+ library: this.effectLibrary, limiter: this.webAppearance.limiter,
961
+ authorize: (name, buckets) => !this.isKnownInstance(name) ? { error: "unknown_instance" } : this.webAppearance.authorizeEffect(name, buckets),
962
+ placed: name => this.themeStore.get(name).placements.effects,
963
+ place: (name, effects) => {
964
+ const before = this.themeStore.get(name).placements.effects;
965
+ const explicit = new Set(effects.filter(e => typeof e !== "string").map(e => e.id));
966
+ const w = this.themeStore.setPlacements(name, { effects }, "agent", what => {
967
+ // An unchanged human/private placement may be retained, never exposed or newly acquired by an agent.
968
+ const r = explicit.has(what.id) ? this.effectLibrary.read(what.id, { kind: "agent", id: name })
969
+ : this.effectLibrary.readReferenced(what.id, `instance:${name}`);
970
+ if (!r.ok)
971
+ return r.error;
972
+ const params = explicit.has(what.id) ? what.params : before.find(e => e.id === what.id)?.params;
973
+ return resolveEffectParams(r.entry.manifest, params ?? {}).ok ? null : "effect_params";
974
+ });
975
+ return w.ok ? { ok: true, rev: w.rev } : { ok: false, error: w.error };
976
+ },
977
+ changed: () => { try {
978
+ this.emitSseEvent("status", this.getUiStatus());
979
+ }
980
+ catch { /* later status carries placements */ } },
981
+ // A nonmutating own/shared offer is not an appearance write. Only an explicit local-admin click may start it.
982
+ preview: async (instance, id) => {
983
+ const result = this.effectTrials.offer(instance, id);
984
+ if (!result.ok)
985
+ return { error: "not_found" };
986
+ const present = this.webChatHistory.list(instance).some(row => row.effectTrial?.offer === result.offer.offer);
987
+ if (!present && !this.publishEffectTrial(instance, result.offer))
988
+ return { error: "trial_unavailable" };
989
+ return { id, status: "offered", expiresAt: result.offer.expiresAt, message: "A trial card is in your web chat. A signed-in local admin can start it; nothing was applied." };
990
+ },
991
+ profiles: id => this.effectProfiles.read(id),
992
+ profileSummary: id => this.effectService.profileSummary(id),
993
+ profileSummaries: ids => this.effectService.profileSummaries(ids),
994
+ }));
995
+ }
996
+ appearanceToolsService;
997
+ /** #1629 r4 §1: get_web_appearance / preview_web_appearance — the caller's own look, resolved as the page resolves it. */
998
+ get appearanceTools() {
999
+ return (this.appearanceToolsService ??= new AppearanceTools({
1000
+ frame: (name) => (this.getUiStatus().instances.find(i => i.name === name) ?? { name }),
1001
+ theme: (name) => { const r = this.themeStore.get(name); return { theme: r.theme, locked: r.locked, widgets: r.placements.widgets }; },
1002
+ card: (name) => resolveCard(name, humanCards(this.dataDir).get(name), this.themeStore.get(name).theme.card),
1003
+ // #1958: the host's identity-colour settings, as the page reads them (an unreadable prefs file: the defaults).
1004
+ colorPrefs: () => { try {
1005
+ const v = this.webPrefs.read(OPERATOR).values;
1006
+ return { show: v["color.show"], off: v["color.off"] === true };
1007
+ }
1008
+ catch {
1009
+ return {};
1010
+ } },
1011
+ cardPicture: (name, slot) => cardPicture(this, name, slot),
1012
+ themeAsset: (name, id) => this.themeStore.asset(name, id),
1013
+ profileAvatar: (name) => this.webImage("avatar", name),
1014
+ botAvatar: (_name, key) => this.webImage("bot", key),
1015
+ themeAnswer: (name) => this.webAppearance.getTheme(name),
1016
+ }));
1017
+ }
1018
+ /** #1958: a profile, card or colour write (view-api.ts) landed: every open page gets a fresh status frame. */
1019
+ identityChanged() {
1020
+ try {
1021
+ this.emitSseEvent("status", this.getUiStatus());
1022
+ }
1023
+ catch { /* the next frame carries it */ }
1024
+ }
1025
+ /** A fleet.yaml instance (own key) or a ClassicBot room, now (#1629: a person's reset/lock names one). */
1026
+ isKnownInstance(name) {
1027
+ return Object.hasOwn(this.fleetConfig?.instances ?? {}, name) || (this.classicChannels?.getAll() ?? []).some((ch) => ch.instanceName === name);
1028
+ }
1029
+ modStore;
1030
+ /**
1031
+ * #1629 r4-5 / B2: installed mods use the same validated library and scoped holder claims as direct uploads.
1032
+ */
1033
+ get mods() {
1034
+ return (this.modStore ??= new ModStore(this.dataDir, {
1035
+ themes: this.themeStore,
1036
+ prefs: this.webPrefs,
1037
+ ownerCss: this.ownerCss,
1038
+ effects: this.effectLibrary,
1039
+ context: () => ({ agendVersion: agendVersion(), limits: this.themeStore.limits(), effects: cw1EffectChecker() }),
1040
+ isKnownInstance: (name) => this.isKnownInstance(name),
1041
+ prefsIdentity: OPERATOR,
1042
+ }));
1043
+ }
1044
+ agentModsService;
1045
+ /** #1629 r4-5b: export_web_mod / propose_web_mod — the caller's own look as a file, and a proposal for a person. */
1046
+ get agentMods() {
1047
+ return (this.agentModsService ??= new AgentMods({
1048
+ themes: this.themeStore, mods: this.mods, limiter: this.webAppearance.limiter,
1049
+ webConfig: () => this.fleetConfig?.web ?? null,
1050
+ // The mod store's own: a proposal is checked exactly as the person's import of it will be.
1051
+ context: () => this.mods.checkContext(),
1052
+ readOwnEffect: (instance, id) => { const r = this.effectLibrary.read(id, { kind: "agent", id: instance }); return r.ok ? { name: r.entry.manifest.name, files: r.entry.files } : null; },
1053
+ notice: (instance, text) => { try {
1054
+ this.emitSseEvent("message", { instance, sender: instance, role: "status", text, ts: new Date().toISOString() });
1055
+ }
1056
+ catch { /* the page reads it from Settings too */ } },
1057
+ changed: () => { try {
1058
+ this.emitSseEvent("status", this.getUiStatus());
1059
+ }
1060
+ catch { /* the next frame */ } },
1061
+ }));
1062
+ }
1063
+ /**
1064
+ * #1629 r4-5c: an effect as this person may carry it away in an export — their own claim, or one a person shared —
1065
+ * read with their principal (Sol's ACL). Being able to view or run a placed effect never grants packaging someone
1066
+ * else's private claim: the library's reference read is runtime access, not an export path. Null: not theirs.
1067
+ */
1068
+ modPlacedEffect(person, id) {
1069
+ const r = this.effectLibrary.read(id, { kind: "human", id: person });
1070
+ return r.ok ? { name: r.entry.manifest.name, files: r.entry.files } : null;
1071
+ }
1072
+ /** #1629 r4-5: an instance's card text as every viewer sees it (a person's fields over the agent's). */
1073
+ modCardText(name) {
1074
+ const card = resolveCard(name, humanCards(this.dataDir).get(name), this.themeStore.get(name).theme.card);
1075
+ return { ...(card.tagline ? { tagline: card.tagline.text } : {}), ...(card.bio ? { bio: card.bio.text } : {}) };
1076
+ }
1077
+ ownerCssStore;
1078
+ /** #1629 r4 §3: each person's own CSS; which version is in force is their `css.active` in the web preferences. */
1079
+ get ownerCss() {
1080
+ return (this.ownerCssStore ??= new OwnerCssStore(this.dataDir, this.webPrefs));
1081
+ }
887
1082
  get webPrefs() {
888
1083
  // #1664: a favorite must be an instance of this fleet now — fleet.yaml's (own keys) or a ClassicBot room's.
889
1084
  return (this.webPrefsStore ??= new WebPrefsStore(this.dataDir, {
@@ -3704,7 +3899,11 @@ export class FleetManager {
3704
3899
  // A command off for want of fleet admins names where they go (`{0}` in its disabled reply).
3705
3900
  if (decision.reply === spec.disabled)
3706
3901
  args.push(this.adminListKey(adapterId));
3707
- await data.respond(t(key, ...args)).catch(() => { });
3902
+ // #1990: a refusal that sends the person to General links to it — not in a ClassicBot room, whose readers need
3903
+ // not be in the fleet's guild.
3904
+ const toGeneral = GENERAL_POINTING_REFUSALS.has(key) && scope !== "classic" && scope !== "none";
3905
+ const reply = toGeneral ? this.renderPlaces(adapterId, t(key, ...args) + placeSuffix({ kind: "general" }, "General")).text : t(key, ...args);
3906
+ await data.respond(reply).catch(() => { });
3708
3907
  return;
3709
3908
  }
3710
3909
  if (data.command === "start") {
@@ -5302,7 +5501,8 @@ export class FleetManager {
5302
5501
  const owner = id ? this.dashboardOwner(msg.userId, id, msg.chatId, msg.threadId, msg.username) : null;
5303
5502
  if (!owner) {
5304
5503
  const adapter = id ? this.adapters.get(id) : undefined;
5305
- await adapter?.sendText(msg.chatId, t("dashboard.public_general"), { threadId: msg.threadId });
5504
+ const out = this.renderPlaces(id, t("dashboard.public_general") + placeSuffix({ kind: "general" }, "General"), { threadId: msg.threadId });
5505
+ await adapter?.sendText(msg.chatId, out.text, out.opts);
5306
5506
  return;
5307
5507
  }
5308
5508
  if (!await this.showDashboardMenu(owner)) {
@@ -6273,7 +6473,7 @@ export class FleetManager {
6273
6473
  ...DEFAULT_SCHEDULER_CONFIG,
6274
6474
  ...this.fleetConfig?.defaults.scheduler,
6275
6475
  };
6276
- this.scheduler = new Scheduler(join(this.dataDir, "scheduler.db"), (schedule, runId, retry) => this.handleScheduleTrigger(schedule, runId, retry), schedulerConfig, (name) => this.fleetConfig?.instances?.[name] != null || !!this.classicChannels?.getAll().some(ch => ch.instanceName === name), (schedule, retry, reason) => this.scheduleRetryDropped(schedule, retry, reason));
6476
+ this.scheduler = new Scheduler(join(this.dataDir, "scheduler.db"), (schedule, runId, retry) => this.handleScheduleTrigger(schedule, runId, retry), schedulerConfig, (name) => this.fleetConfig?.instances?.[name] != null || !!this.classicChannels?.getAll().some(ch => ch.instanceName === name), (schedule, retry, reason) => this.scheduleRetryDropped(schedule, retry, reason), this.logger);
6277
6477
  this.scheduler.init();
6278
6478
  this.logger.info("Scheduler initialized");
6279
6479
  // Tips share the daily-report clock but remain an independent internal
@@ -7505,6 +7705,48 @@ export class FleetManager {
7505
7705
  this.adapterRestarting.delete(id);
7506
7706
  }
7507
7707
  }
7708
+ /**
7709
+ * #1990: how a reader in world `worldId`'s group reaches a place token's target — undefined (plain text) for another
7710
+ * world's instance, an instance without a fleet topic (a ClassicBot room may be anywhere), or an id no link is built
7711
+ * from. Discord: the channel mention; Telegram: the topic URL (General's topic included).
7712
+ */
7713
+ placeLink(worldId, target) {
7714
+ const world = this.worlds.get(worldId ?? "");
7715
+ if (!world)
7716
+ return undefined;
7717
+ let topic;
7718
+ if (target.kind === "general") {
7719
+ const notice = this.fleetNoticeTarget(world.id);
7720
+ if (!notice || notice.chatId !== world.groupId || notice.opts.threadId === undefined)
7721
+ return undefined;
7722
+ topic = String(notice.opts.threadId);
7723
+ }
7724
+ else {
7725
+ if (this.getInstanceAdapterId(target.name) !== world.id)
7726
+ return undefined;
7727
+ const topicId = this.fleetConfig?.instances[target.name]?.topic_id;
7728
+ if (topicId == null)
7729
+ return undefined;
7730
+ topic = String(topicId);
7731
+ }
7732
+ if (world.type === "discord")
7733
+ return SNOWFLAKE.test(topic) && SNOWFLAKE.test(world.groupId) ? { mention: `<#${topic}>` } : undefined;
7734
+ const url = topicUrl(worldPlaceOf(world), topic, { general: target.kind === "general" });
7735
+ return url ? { url } : undefined;
7736
+ }
7737
+ /** #1990: a message for world `worldId`'s group with its place tokens rendered (links as text or Telegram entities). */
7738
+ renderPlaces(worldId, text, opts) {
7739
+ if (!hasPlaceRefs(text))
7740
+ return { text, opts };
7741
+ const type = this.worlds.get(worldId ?? "")?.type;
7742
+ const surface = type === "discord" ? "discord" : type === "telegram" ? "telegram" : "plain";
7743
+ const out = renderPlaceRefs(text, surface, target => this.placeLink(worldId, target));
7744
+ return { text: out.text, opts: out.textLinks.length ? { ...opts, textLinks: out.textLinks } : opts };
7745
+ }
7746
+ /** #1990: renderPlaces for the world that owns `instanceName`'s topic. */
7747
+ renderPlacesFor(instanceName, text, opts) {
7748
+ return this.renderPlaces(this.getInstanceAdapterId(instanceName), text, opts);
7749
+ }
7508
7750
  /** Handle inbound message — transcribe voice if present, then route */
7509
7751
  findGeneralInstance(adapterId) {
7510
7752
  if (!this.fleetConfig)
@@ -9177,6 +9419,19 @@ export class FleetManager {
9177
9419
  }
9178
9420
  dispatchTypedIpc(name, msg) {
9179
9421
  const type = String(msg.type);
9422
+ if (type.startsWith("fleet_") && WEB_EFFECT_TOOLS.has(type.slice(6))) {
9423
+ const ipc = this.instanceIpcClients.get(name), fleetRequestId = msg.fleetRequestId;
9424
+ if (!ipc || typeof fleetRequestId !== "string")
9425
+ return;
9426
+ void this.handleWebThemeHttp(name, type.slice(6), (msg.payload ?? {})).then(result => {
9427
+ if (this.instanceIpcClients.get(name) === ipc)
9428
+ ipc.send({ type: "fleet_web_theme_response", fleetRequestId, result });
9429
+ }, err => {
9430
+ if (this.instanceIpcClients.get(name) === ipc)
9431
+ ipc.send({ type: "fleet_web_theme_response", fleetRequestId, error: err instanceof Error ? err.message : "Effect operation failed" });
9432
+ }).catch(err => this.logger.debug({ err }, "Effect tool response could not be written"));
9433
+ return;
9434
+ }
9180
9435
  if (type.startsWith("fleet_schedule_")) {
9181
9436
  this.handleScheduleCrud(name, msg);
9182
9437
  return;
@@ -9202,6 +9457,36 @@ export class FleetManager {
9202
9457
  this.handlePersonaEmoji(name, msg);
9203
9458
  return;
9204
9459
  }
9460
+ if (type === "fleet_get_web_theme" || type === "fleet_set_web_theme" || type === "fleet_set_web_theme_asset" || type === "fleet_reset_web_theme"
9461
+ || type === "fleet_get_web_appearance" || type === "fleet_preview_web_appearance"
9462
+ || type === "fleet_set_web_widget" || type === "fleet_remove_web_widget"
9463
+ || type === "fleet_export_web_mod" || type === "fleet_propose_web_mod") {
9464
+ this.handleWebTheme(name, msg);
9465
+ return;
9466
+ }
9467
+ }
9468
+ /**
9469
+ * #1629: the caller's own web theme over IPC (the agent endpoint calls the same service). `name` is the instance this
9470
+ * IPC connection belongs to — never anything the message says. A refusal is a result the agent reads (its `error`
9471
+ * and what to do), not a transport error.
9472
+ */
9473
+ handleWebTheme(instanceName, msg) {
9474
+ const fleetRequestId = msg.fleetRequestId;
9475
+ const ipc = this.instanceIpcClients.get(instanceName);
9476
+ if (!ipc)
9477
+ return;
9478
+ const tool = String(msg.type).replace(/^fleet_/, "");
9479
+ try {
9480
+ const args = (msg.payload ?? {});
9481
+ const result = tool === "get_web_appearance" || tool === "preview_web_appearance"
9482
+ ? this.appearanceTools.handle(instanceName, tool, args)
9483
+ : tool === "export_web_mod" || tool === "propose_web_mod" ? this.agentMods.handle(instanceName, tool, args)
9484
+ : this.webAppearance.handleTool(instanceName, tool, args);
9485
+ ipc.send({ type: "fleet_web_theme_response", fleetRequestId, result });
9486
+ }
9487
+ catch (err) {
9488
+ ipc.send({ type: "fleet_web_theme_response", fleetRequestId, error: err.message });
9489
+ }
9205
9490
  }
9206
9491
  /** `list_emojis` / `set_persona_emoji` / `preview_emojis` / `list_stickers` / `preview_stickers` over IPC; the agent endpoint calls the same methods. */
9207
9492
  handlePersonaEmoji(instanceName, msg) {
@@ -9606,6 +9891,19 @@ export class FleetManager {
9606
9891
  async handleSetPersonaEmojiHttp(instance, args) {
9607
9892
  return this.setPersonaEmoji(instance, args);
9608
9893
  }
9894
+ /** #1629: `agend-agent theme …` — the same service as the tools; `instance` is the token's. */
9895
+ async handleWebThemeHttp(instance, tool, args) {
9896
+ if (WEB_EFFECT_TOOLS.has(tool)) {
9897
+ if (!this.isKnownInstance(instance))
9898
+ return { error: "unknown_instance" };
9899
+ return this.effectTools.handle(instance, tool, args);
9900
+ }
9901
+ if (tool === "get_web_appearance" || tool === "preview_web_appearance")
9902
+ return this.appearanceTools.handle(instance, tool, args);
9903
+ if (tool === "export_web_mod" || tool === "propose_web_mod")
9904
+ return this.agentMods.handle(instance, tool, args);
9905
+ return this.webAppearance.handleTool(instance, tool, args);
9906
+ }
9609
9907
  async handlePreviewEmojisHttp(instance, args) {
9610
9908
  return this.previewEmojis(instance, args);
9611
9909
  }
@@ -10308,6 +10606,17 @@ export class FleetManager {
10308
10606
  catch (err) {
10309
10607
  this.logger.warn({ err: err.message, name }, "Dropping a removed instance from web favorites failed");
10310
10608
  }
10609
+ try {
10610
+ this.themeStore.forget(name);
10611
+ this.webAppearance.forget(name);
10612
+ this.appearanceTools.forget(name);
10613
+ this.effectToolsService?.forget(name);
10614
+ this.mods.forgetInstance(name);
10615
+ this.agentMods.forget(name);
10616
+ }
10617
+ catch (err) {
10618
+ this.logger.warn({ err: err.message, name }, "Dropping a removed instance's web theme failed");
10619
+ }
10311
10620
  // Clean up statusline watcher + instance directory
10312
10621
  this.statuslineWatcher.unwatch(name);
10313
10622
  // instanceDirToClean and effectiveBackend are captured before lifecycle.remove
@@ -10596,10 +10905,25 @@ export class FleetManager {
10596
10905
  const avatar = avatarFor({ name, displayName, profileVersion, bot: bot && connection ? { connection, version: bot.version } : null,
10597
10906
  personaEmoji: cfg?.status_emojis?.delivered });
10598
10907
  // The ring belongs to an own bot's face: only when that is what is shown (a shared bot's would ring everyone alike).
10599
- const accent = !profileVersion && bot && ownConnection && owner
10908
+ // #1958: the own bot's colour is also the identity colour's role tier — whatever face is shown (never a shared bot's).
10909
+ const roleColor = ownConnection && owner
10600
10910
  ? this.webAccent(ownConnection, owner, cfg ? (cfg.topic_id == null ? undefined : String(cfg.topic_id)) : this.classicChannels?.getChannelIdByInstance(name))
10601
10911
  : null;
10602
- return accent ? { avatar, accent } : { avatar };
10912
+ const accent = !profileVersion && bot && ownConnection && owner ? roleColor : null;
10913
+ // #1629 (first design §5, r2 (d)): the agent's theme may name its own picture and ring colour. Both come below a
10914
+ // person's upload (never when there is one) and ride beside the theme-free face: a viewer who turned agents'
10915
+ // themes off (fx.show), or a safe-mode load, shows `avatar` and `accent` as before.
10916
+ let theme = { v: 1 };
10917
+ try {
10918
+ theme = this.themeStore.get(name).theme;
10919
+ }
10920
+ catch { /* unreadable themes: none */ }
10921
+ const themeAvatar = !profileVersion && theme.avatar ? { kind: "image", src: `/ui/img/theme/${encodeURIComponent(name)}/${theme.avatar}` } : undefined;
10922
+ const themeAccent = !profileVersion && theme.accent ? theme.accent : undefined;
10923
+ return { avatar, ...(accent ? { accent } : {}), ...(themeAvatar ? { themeAvatar } : {}), ...(themeAccent ? { themeAccent } : {}),
10924
+ // #1958: the agent's own identity colour (theme.identity.color, else its accent) and where it would show it.
10925
+ ...(roleColor ? { roleColor } : {}), ...(theme.identity?.color ?? theme.accent ? { agentColor: theme.identity?.color ?? theme.accent } : {}),
10926
+ ...(theme.identity?.show ? { agentShow: theme.identity.show } : {}) };
10603
10927
  }
10604
10928
  /**
10605
10929
  * GET /ui/img/bot|avatar/<key>: what is cached, never a fetch. Only a connection some instance or room speaks
@@ -11686,7 +12010,8 @@ export class FleetManager {
11686
12010
  // Discord, so every such fallback failed inside the catch handler.
11687
12011
  const target = this.fleetNoticeTarget();
11688
12012
  if (this.adapter && target) {
11689
- this.adapter.sendText(target.chatId, body, target.opts)
12013
+ const out = this.renderPlaces(this.getPrimaryAdapterId(), body, target.opts);
12014
+ this.adapter.sendText(target.chatId, out.text, out.opts)
11690
12015
  .catch(err => this.logger.warn({ err }, "Failed to send fleet error notification"));
11691
12016
  dispatched = true;
11692
12017
  }
@@ -11698,7 +12023,7 @@ export class FleetManager {
11698
12023
  // also suppressed the next ten minutes of identical ones. Leaving the key
11699
12024
  // unspent lets the next caller, once an adapter is up, actually get
11700
12025
  // through. The counter is not reset either: nothing was shown.
11701
- this.logger.warn({ text: body }, "Fleet error could not be delivered (no adapter or no target yet) — not consuming the throttle window");
12026
+ this.logger.warn({ text: stripPlaceRefs(body) }, "Fleet error could not be delivered (no adapter or no target yet) — not consuming the throttle window");
11702
12027
  return false;
11703
12028
  }
11704
12029
  if (!throttled)
@@ -11736,24 +12061,28 @@ export class FleetManager {
11736
12061
  const groupId = channelCfg?.group_id;
11737
12062
  // Fleet topic instance
11738
12063
  const threadId = this.fleetConfig?.instances[instanceName]?.topic_id;
12064
+ const worldId = this.getInstanceAdapterId(instanceName);
11739
12065
  if (threadId != null && groupId) {
11740
- adapter.sendText(String(groupId), text, { threadId: String(threadId), ...extraOpts })
12066
+ const out = this.renderPlaces(worldId, text, { threadId: String(threadId), ...extraOpts });
12067
+ adapter.sendText(String(groupId), out.text, out.opts)
11741
12068
  .catch(e => this.logger.warn({ err: e, instanceName }, "Failed to send instance topic notification"));
11742
12069
  return true;
11743
12070
  }
11744
12071
  // Classic instance: find its channelId from the classic manager
11745
12072
  const classicChatId = this.classicChannels?.getChannelIdByInstance(instanceName);
11746
12073
  if (classicChatId) {
11747
- adapter.sendText(classicChatId, text, extraOpts)
12074
+ // #1990: a ClassicBot room's readers need not be in the fleet's group, so its notices name places as plain text.
12075
+ adapter.sendText(classicChatId, stripPlaceRefs(text), extraOpts)
11748
12076
  .catch(e => this.logger.warn({ err: e, instanceName }, "Failed to send classic notification"));
11749
12077
  return true;
11750
12078
  }
11751
12079
  // Fallback: the instance has neither a topic nor a classic channel, so post
11752
12080
  // where fleet-wide notices go. Not the bare group id: on Discord that is a
11753
12081
  // guild, and the send fails inside the catch handler.
11754
- const target = this.fleetNoticeTarget(this.getInstanceAdapterId(instanceName));
12082
+ const target = this.fleetNoticeTarget(worldId);
11755
12083
  if (target) {
11756
- adapter.sendText(target.chatId, text, { ...target.opts, ...extraOpts })
12084
+ const out = this.renderPlaces(worldId, text, { ...target.opts, ...extraOpts });
12085
+ adapter.sendText(target.chatId, out.text, out.opts)
11757
12086
  .catch(e => this.logger.warn({ err: e, instanceName }, "Failed to send notification (no topic)"));
11758
12087
  return true;
11759
12088
  }
@@ -11824,12 +12153,14 @@ export class FleetManager {
11824
12153
  this.pendingNonceButtons.set(nonce, entry);
11825
12154
  try {
11826
12155
  const choices = opts.choices.map(c => ({ id: `${opts.prefix}${nonce}:${c.action}`, label: c.label }));
12156
+ // #1990: place names link for the group the prompt is posted in; the dashboard's copy is plain (for now).
12157
+ const posted = this.renderPlaces(opts.adapterId, opts.message, opts.threadId ? { threadId: opts.threadId } : undefined);
11827
12158
  const sent = opts.deliver ? await opts.deliver(choices) : await opts.adapter.notifyAlert(opts.chatId, {
11828
12159
  type: opts.alertType,
11829
12160
  instanceName: opts.instanceName,
11830
- message: opts.message,
12161
+ message: posted.text,
11831
12162
  choices,
11832
- }, opts.threadId ? { threadId: opts.threadId } : undefined);
12163
+ }, posted.opts);
11833
12164
  // Bind the nonce to the provider's canonical delivery address, not the
11834
12165
  // logical routing input. Telegram, for example, represents General as
11835
12166
  // AgEnD topic "1" on input but omits message_thread_id on the wire and in
@@ -11847,8 +12178,15 @@ export class FleetManager {
11847
12178
  // Offered on the dashboard only once it is live on the platform (a failed post is disarmed above),
11848
12179
  // and only if nothing claimed or expired it meanwhile.
11849
12180
  if (WEB_MIRRORED_PROMPT_PREFIXES.has(opts.prefix) && this.pendingNonceButtons.get(nonce) === entry) {
12181
+ // #1990: on the dashboard an instance's name opens its terminal View (where the prompt is answered by hand).
12182
+ const web = renderPlaceRefs(opts.message, "web", target => {
12183
+ const route = target.kind === "instance" && Object.hasOwn(this.fleetConfig?.instances ?? {}, target.name) ? webRoute("view", target.name) : undefined;
12184
+ return route ? { url: route } : undefined;
12185
+ });
12186
+ const links = webTextLinks(web.textLinks, web.text);
11850
12187
  entry.web = {
11851
- text: opts.message,
12188
+ text: web.text,
12189
+ ...(links ? { links } : {}),
11852
12190
  actions: opts.choices.map(c => ({ id: c.action, label: c.label })),
11853
12191
  expiresAt: Date.now() + (opts.timeoutMs ?? NONCE_BUTTON_TIMEOUT_MS),
11854
12192
  };
@@ -12100,7 +12438,7 @@ export class FleetManager {
12100
12438
  isClassic: name => classic.has(name),
12101
12439
  place: name => {
12102
12440
  const world = this.worlds.get(this.getInstanceAdapterId(name) ?? "");
12103
- return world ? { type: world.type, ...(world.groupId ? { groupId: world.groupId } : {}) } : undefined;
12441
+ return world ? worldPlaceOf(world) : undefined;
12104
12442
  },
12105
12443
  // fleet.yaml's topic only. A ClassicBot room has none here, so no link: its channel may sit in any allowed
12106
12444
  // guild, not necessarily its world's group.
@@ -12159,8 +12497,14 @@ export class FleetManager {
12159
12497
  * (cache-service.ts) — never a vendor call, never a synchronous transcript read.
12160
12498
  */
12161
12499
  cacheReport(window) {
12162
- const w = Object.prototype.hasOwnProperty.call(WINDOWS, window) ? window : "7d";
12163
- this.cacheService ??= new CacheService({
12500
+ return this.cacheServiceFor().report(cacheWindow(window));
12501
+ }
12502
+ /** #2001: get_cache_report's — the same report, as last worked out (reused for a minute, until the next pass). */
12503
+ cacheReportPublished(window) {
12504
+ return this.cacheServiceFor().published(cacheWindow(window));
12505
+ }
12506
+ cacheServiceFor() {
12507
+ return this.cacheService ??= new CacheService({
12164
12508
  instances: () => this.cacheInstances(),
12165
12509
  claudeProjectsDir: () => join(process.env.CLAUDE_CONFIG_DIR?.trim() || join(homedir(), ".claude"), "projects"),
12166
12510
  claudeKey: claudeProjectKey,
@@ -12169,7 +12513,6 @@ export class FleetManager {
12169
12513
  metaPath: join(this.dataDir, "cache-codex-rollouts.json"),
12170
12514
  log: (level, msg, extra) => this.logger[level](extra ?? {}, msg),
12171
12515
  });
12172
- return this.cacheService.report(w);
12173
12516
  }
12174
12517
  /** The web's Acknowledge (any item; a signed-in session is fleet-admin level). */
12175
12518
  acknowledgeNeedsItem(id, principal) {
@@ -12265,9 +12608,7 @@ export class FleetManager {
12265
12608
  finishedAt: Date.parse(d.finishedAt) || 0,
12266
12609
  })),
12267
12610
  ownerOf,
12268
- worlds: () => [...this.worlds.values()].map((w) => ({
12269
- id: w.id, adapter: w.adapter, place: { type: w.type, ...(w.groupId ? { groupId: w.groupId } : {}) },
12270
- })),
12611
+ worlds: () => [...this.worlds.values()].map((w) => ({ id: w.id, adapter: w.adapter, place: worldPlaceOf(w) })),
12271
12612
  noticeTarget: world => {
12272
12613
  const target = this.fleetNoticeTarget(world);
12273
12614
  return target ? { chatId: target.chatId, ...(target.opts.threadId !== undefined ? { threadId: String(target.opts.threadId) } : {}) } : null;
@@ -12919,12 +13260,12 @@ export class FleetManager {
12919
13260
  * the operator's. Keep the instance notice passive and put the action in the
12920
13261
  * same-world General topic where an administrator can make the choice. */
12921
13262
  async notifyNormalExit(instanceName) {
12922
- this.notifyInstanceTopic(instanceName, t("exit.instance_notice", instanceName));
13263
+ this.notifyInstanceTopic(instanceName, t("exit.instance_notice", instanceName) + placeSuffix({ kind: "general" }, "General"));
12923
13264
  const web = this.webOnlyPromptPlace();
12924
13265
  if (web) {
12925
13266
  await this.postNonceButtonPrompt({
12926
13267
  prefix: EXIT_RESTART_CALLBACK_PREFIX, alertType: "exit_restart", instanceName, ...web,
12927
- message: t("exit.general_notice", instanceName),
13268
+ message: t("exit.general_notice", placeRef({ kind: "instance", name: instanceName }, instanceName)),
12928
13269
  choices: [{ action: "restart", label: t("exit.restart") }, { action: "ignore", label: t("exit.ignore") }],
12929
13270
  expiredText: t("exit.expired", instanceName),
12930
13271
  });
@@ -12953,7 +13294,7 @@ export class FleetManager {
12953
13294
  adapterId,
12954
13295
  chatId,
12955
13296
  threadId,
12956
- message: t("exit.general_notice", instanceName),
13297
+ message: t("exit.general_notice", placeRef({ kind: "instance", name: instanceName }, instanceName)),
12957
13298
  choices: [
12958
13299
  { action: "restart", label: t("exit.restart") },
12959
13300
  { action: "ignore", label: t("exit.ignore") },
@@ -13000,7 +13341,7 @@ export class FleetManager {
13000
13341
  * contain usernames or secret-adjacent text and stays in daemon.log.
13001
13342
  */
13002
13343
  async notifyInteractivePrompt(instanceName, kind) {
13003
- this.notifyInstanceTopic(instanceName, t("interactive.instance_notice", instanceName));
13344
+ this.notifyInstanceTopic(instanceName, t("interactive.instance_notice", instanceName) + placeSuffix({ kind: "general" }, "General"));
13004
13345
  // Multi-world fleets can have one General per bot/platform. Route the assist
13005
13346
  // request to the General bound to the same adapter as the blocked instance.
13006
13347
  const generalName = this.findGeneralInstance(this.getInstanceAdapterId(instanceName));
@@ -13011,7 +13352,7 @@ export class FleetManager {
13011
13352
  const label = this.interactivePromptLabel(kind);
13012
13353
  const prompt = {
13013
13354
  prefix: INTERACTIVE_ASSIST_CALLBACK_PREFIX, alertType: "interactive_prompt", instanceName,
13014
- message: t("interactive.general_notice", instanceName, label),
13355
+ message: t("interactive.general_notice", placeRef({ kind: "instance", name: instanceName }, instanceName), label),
13015
13356
  choices: [{ action: "confirm", label: t("interactive.confirm") }, { action: "cancel", label: t("interactive.cancel") }],
13016
13357
  expiredText: t("interactive.expired", instanceName),
13017
13358
  extra: { generalName, promptKind: kind, assistFor: this.interactionWaitKey(instanceName) },
@@ -14610,6 +14951,14 @@ export class FleetManager {
14610
14951
  const room = this.classicChannels?.getAll().find(c => c.instanceName === name);
14611
14952
  return room ? `room:${room.channelId}:${room.adapterId ?? ""}` : null;
14612
14953
  }
14954
+ /** The only Try-card publisher: no arbitrary SSE sender/role can manufacture this metadata. */
14955
+ publishEffectTrial(instance, offer) {
14956
+ const row = this.webChatHistory.recordEffectTrial(instance, offer, new Date().toISOString());
14957
+ if (!row)
14958
+ return false;
14959
+ broadcastSseEvent(this.sseClients, "message", row, err => this.logger.debug({ err }, "SSE client write failed; evicting"), this.webChatHistory.cursorOf(row));
14960
+ return true;
14961
+ }
14613
14962
  /** Push an SSE event to all connected Web UI clients. */
14614
14963
  emitSseEvent(event, data) {
14615
14964
  // #1386: a prompt opened or closed, or an instance's state moved — "Needs you" may have changed.
@@ -14626,8 +14975,12 @@ export class FleetManager {
14626
14975
  role: typeof m.role === "string" ? m.role : undefined,
14627
14976
  buttons: m.buttons,
14628
14977
  click: m.click,
14978
+ links: m.links,
14629
14979
  });
14630
14980
  broadcastSseEvent(this.sseClients, event, recorded, onError, this.webChatHistory.cursorOf(recorded));
14981
+ // #1629: live-only receipt after the recorded agent reply. Never persist/replay it or give it an SSE cursor.
14982
+ if (recorded.role === "agent")
14983
+ broadcastSseEvent(this.sseClients, "agent_reply_live", { v: 1, boot: recorded.boot, id: recorded.id, instance: recorded.instance }, onError);
14631
14984
  return;
14632
14985
  }
14633
14986
  // #1266: a reply's buttons ended — the history shows it too (a later load, the public link's poll).
@@ -17295,6 +17648,14 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
17295
17648
  catch (err) {
17296
17649
  this.logger.warn({ err: err.message, instanceName: ch.instanceName }, "Dropping a removed room from web favorites failed");
17297
17650
  }
17651
+ try {
17652
+ this.themeStore.forget(ch.instanceName);
17653
+ this.webAppearance.limiter.forget(ch.instanceName);
17654
+ this.effectToolsService?.forget(ch.instanceName);
17655
+ }
17656
+ catch (err) {
17657
+ this.logger.warn({ err: err.message, instanceName: ch.instanceName }, "Dropping a removed room's web theme failed");
17658
+ }
17298
17659
  }
17299
17660
  this.instanceWorldBinding.delete(ch.instanceName);
17300
17661
  // #1716: say so in the reply when background work was ended with the stop.
@@ -20024,6 +20385,9 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
20024
20385
  // /api/profiles (resolveInstanceIdentity) — the sidebar groups, searches and labels with it on every page.
20025
20386
  const classicRooms = new Map((this.classicChannels?.getAll() ?? []).map(ch => [ch.instanceName, ch]));
20026
20387
  const profiles = profileIdentities(this.dataDir);
20388
+ // #1629 r4 §1: the identity card's version per instance (the page reads the card when it moves).
20389
+ const cards = humanCards(this.dataDir);
20390
+ const colors = identityColors(this.dataDir);
20027
20391
  // #1630: decorative — whatever goes wrong deciding an avatar never costs the frame (every instance gets its first character).
20028
20392
  let bots = { own: new Map(), speaking: new Map() };
20029
20393
  try {
@@ -20083,6 +20447,21 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
20083
20447
  display_name: identity.display_name || undefined,
20084
20448
  avatar: image.avatar,
20085
20449
  ...(image.accent ? { accent: image.accent } : {}),
20450
+ // #1629: the agent's theme's picture and ring colour, for viewers who show agents' themes.
20451
+ ...(image.themeAvatar ? { theme_avatar: image.themeAvatar } : {}),
20452
+ ...(image.themeAccent ? { theme_accent: image.themeAccent } : {}),
20453
+ ...(() => { try {
20454
+ const v = cardVersion(resolveCard(name, cards.get(name), this.themeStore.get(name).theme.card));
20455
+ return v ? { card_v: v } : {};
20456
+ }
20457
+ catch {
20458
+ return {};
20459
+ } })(),
20460
+ // #1958: the identity colour's tiers (the page resolves them: the agent's only while it shows agents' themes).
20461
+ color: { ...(colors.get(name)?.color ? { person: colors.get(name).color } : {}), ...(image.agentColor ? { agent: image.agentColor } : {}),
20462
+ ...(image.roleColor ? { role: image.roleColor } : {}), auto: autoIdentityColor(name) },
20463
+ // #1958 decision 2: where it shows — a person's choice for this instance, the agent's; else the viewer's host default.
20464
+ color_show: { ...(colors.get(name)?.show ? { person: colors.get(name).show } : {}), ...(image.agentShow ? { agent: image.agentShow } : {}) },
20086
20465
  description: identity.description || undefined,
20087
20466
  role: identity.role || undefined,
20088
20467
  tags: identity.tags,
@@ -20109,6 +20488,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
20109
20488
  connections: (this.fleetConfig?.channels ?? (this.fleetConfig?.channel ? [this.fleetConfig.channel] : [])).length,
20110
20489
  // #1665: the host preferences' revision — a page that has another re-reads /ui/prefs (no values in this frame).
20111
20490
  prefs_rev: this.webPrefs.rev(),
20491
+ // #1629: the themes' revision — the page re-reads GET /ui/themes when it moves.
20492
+ themes_rev: this.themeStore.rev(),
20112
20493
  // #1685: host memory pressure while it slows or holds new agents (the dashboard's indicator; macOS elevated is
20113
20494
  // shown here rather than in chat). null when normal, unknown or advisory.
20114
20495
  host_memory: this.hostMemoryIndicator(),