@songsid/agend 2.3.3-beta.4 → 2.3.3-beta.5

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 (234) hide show
  1. package/dist/agent-cli.js +13 -112
  2. package/dist/agent-cli.js.map +1 -1
  3. package/dist/agent-endpoint.d.ts +0 -1
  4. package/dist/agent-endpoint.js +1 -9
  5. package/dist/agent-endpoint.js.map +1 -1
  6. package/dist/agent-whats-new.d.ts +3 -6
  7. package/dist/agent-whats-new.js +7 -18
  8. package/dist/agent-whats-new.js.map +1 -1
  9. package/dist/auth-api.d.ts +0 -7
  10. package/dist/auth-api.js +1 -15
  11. package/dist/auth-api.js.map +1 -1
  12. package/dist/backend/codex.d.ts +1 -0
  13. package/dist/backend/codex.js +106 -15
  14. package/dist/backend/codex.js.map +1 -1
  15. package/dist/backend/types.d.ts +17 -0
  16. package/dist/backend/types.js.map +1 -1
  17. package/dist/bundled-skills.d.ts +4 -6
  18. package/dist/bundled-skills.js +10 -14
  19. package/dist/bundled-skills.js.map +1 -1
  20. package/dist/channel/mcp-server.js +5 -5
  21. package/dist/channel/mcp-server.js.map +1 -1
  22. package/dist/channel/mcp-tools.d.ts +1 -3
  23. package/dist/channel/mcp-tools.js +4 -58
  24. package/dist/channel/mcp-tools.js.map +1 -1
  25. package/dist/cli.js +3 -1
  26. package/dist/cli.js.map +1 -1
  27. package/dist/config-validator.js +6 -3
  28. package/dist/config-validator.js.map +1 -1
  29. package/dist/daemon.d.ts +11 -3
  30. package/dist/daemon.js +59 -107
  31. package/dist/daemon.js.map +1 -1
  32. package/dist/dashboard-live.d.ts +1 -1
  33. package/dist/dashboard-live.js +3 -3
  34. package/dist/dashboard-live.js.map +1 -1
  35. package/dist/dashboard-permissions.d.ts +1 -1
  36. package/dist/dashboard-permissions.js +1 -3
  37. package/dist/dashboard-permissions.js.map +1 -1
  38. package/dist/fleet-manager.d.ts +26 -44
  39. package/dist/fleet-manager.js +70 -226
  40. package/dist/fleet-manager.js.map +1 -1
  41. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +11 -1
  42. package/dist/general-knowledge/skills/session-management/SKILL.md +6 -0
  43. package/dist/general-knowledge/skills/web-appearance/SKILL.md +13 -204
  44. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +26 -63
  45. package/dist/instance-lifecycle.js +1 -1
  46. package/dist/instance-lifecycle.js.map +1 -1
  47. package/dist/locale.js +2 -0
  48. package/dist/locale.js.map +1 -1
  49. package/dist/outbound-handlers.js +19 -7
  50. package/dist/outbound-handlers.js.map +1 -1
  51. package/dist/outbound-schemas.d.ts +0 -62
  52. package/dist/outbound-schemas.js +2 -41
  53. package/dist/outbound-schemas.js.map +1 -1
  54. package/dist/public-web-gateway.js +0 -9
  55. package/dist/public-web-gateway.js.map +1 -1
  56. package/dist/public-web-link.d.ts +2 -0
  57. package/dist/public-web-link.js +5 -0
  58. package/dist/public-web-link.js.map +1 -1
  59. package/dist/search/search-docs.d.ts +1 -1
  60. package/dist/search/search-docs.js +2 -2
  61. package/dist/search/search-docs.js.map +1 -1
  62. package/dist/settings-api.d.ts +6 -0
  63. package/dist/settings-api.js +14 -2
  64. package/dist/settings-api.js.map +1 -1
  65. package/dist/settings-change.js +3 -1
  66. package/dist/settings-change.js.map +1 -1
  67. package/dist/tool-permissions.js +3 -10
  68. package/dist/tool-permissions.js.map +1 -1
  69. package/dist/topic-commands.d.ts +6 -2
  70. package/dist/topic-commands.js +2 -0
  71. package/dist/topic-commands.js.map +1 -1
  72. package/dist/types.d.ts +2 -1
  73. package/dist/ui/chat-ambient.js +4 -4
  74. package/dist/ui/chat-commands.js +3 -2
  75. package/dist/ui/chat-store.js +4 -1
  76. package/dist/ui/chat-strings.js +6 -42
  77. package/dist/ui/chat-thread.js +6 -44
  78. package/dist/ui/panel-chat.js +21 -50
  79. package/dist/ui/preview-panel.js +3 -15
  80. package/dist/ui/search-dialog.js +30 -14
  81. package/dist/ui/settings-appearance.js +1 -3
  82. package/dist/ui/settings-guests.js +13 -4
  83. package/dist/ui/settings-learn.js +1 -1
  84. package/dist/ui/settings-mods.js +5 -12
  85. package/dist/ui/settings-registry.js +0 -1
  86. package/dist/ui/settings-strings.js +6 -6
  87. package/dist/ui/shared/app-prefs.js +17 -17
  88. package/dist/ui/shared/app-shell.js +12 -8
  89. package/dist/ui/shared/app-store.js +6 -2
  90. package/dist/ui/shared/app.css +4 -36
  91. package/dist/ui/shared/app.js +5 -4
  92. package/dist/ui/shared/instance-nav.js +9 -9
  93. package/dist/ui/shared/instance-switch.js +8 -2
  94. package/dist/ui/shared/panel-view.js +22 -8
  95. package/dist/ui/shared/view-strings.js +6 -0
  96. package/dist/usage/usage-api.d.ts +3 -0
  97. package/dist/usage/usage-api.js +5 -1
  98. package/dist/usage/usage-api.js.map +1 -1
  99. package/dist/view-api.d.ts +2 -0
  100. package/dist/view-api.js +64 -0
  101. package/dist/view-api.js.map +1 -1
  102. package/dist/web-api.d.ts +9 -17
  103. package/dist/web-api.js +255 -371
  104. package/dist/web-api.js.map +1 -1
  105. package/dist/web-appearance-tools.d.ts +0 -2
  106. package/dist/web-appearance-tools.js +2 -5
  107. package/dist/web-appearance-tools.js.map +1 -1
  108. package/dist/web-appearance-view.d.ts +0 -6
  109. package/dist/web-appearance-view.js +0 -1
  110. package/dist/web-appearance-view.js.map +1 -1
  111. package/dist/web-appearance.d.ts +14 -7
  112. package/dist/web-appearance.js +5 -6
  113. package/dist/web-appearance.js.map +1 -1
  114. package/dist/web-asset-parts.d.ts +1 -17
  115. package/dist/web-asset-parts.js +1 -15
  116. package/dist/web-asset-parts.js.map +1 -1
  117. package/dist/web-auth.js +1 -7
  118. package/dist/web-auth.js.map +1 -1
  119. package/dist/web-chat-history.d.ts +3 -7
  120. package/dist/web-chat-history.js +10 -17
  121. package/dist/web-chat-history.js.map +1 -1
  122. package/dist/web-commands.d.ts +9 -1
  123. package/dist/web-commands.js +13 -0
  124. package/dist/web-commands.js.map +1 -1
  125. package/dist/web-experimental.d.ts +2 -11
  126. package/dist/web-experimental.js +8 -36
  127. package/dist/web-experimental.js.map +1 -1
  128. package/dist/web-guest-access.d.ts +40 -2
  129. package/dist/web-guest-access.js +50 -14
  130. package/dist/web-guest-access.js.map +1 -1
  131. package/dist/web-mod-bundle.d.ts +2 -45
  132. package/dist/web-mod-bundle.js +15 -63
  133. package/dist/web-mod-bundle.js.map +1 -1
  134. package/dist/web-mod-store.d.ts +3 -48
  135. package/dist/web-mod-store.js +30 -97
  136. package/dist/web-mod-store.js.map +1 -1
  137. package/dist/web-prefs.d.ts +13 -0
  138. package/dist/web-prefs.js +38 -0
  139. package/dist/web-prefs.js.map +1 -1
  140. package/dist/web-preview.d.ts +1 -11
  141. package/dist/web-preview.js +6 -20
  142. package/dist/web-preview.js.map +1 -1
  143. package/dist/web-session.d.ts +11 -3
  144. package/dist/web-session.js +22 -11
  145. package/dist/web-session.js.map +1 -1
  146. package/dist/web-settings-patch.d.ts +1 -0
  147. package/dist/web-settings-patch.js +6 -3
  148. package/dist/web-settings-patch.js.map +1 -1
  149. package/dist/web-settings-tools.d.ts +0 -13
  150. package/dist/web-settings-tools.js +12 -84
  151. package/dist/web-settings-tools.js.map +1 -1
  152. package/package.json +1 -1
  153. package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +0 -376
  154. package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +0 -672
  155. package/dist/ui/app-effects.js +0 -251
  156. package/dist/ui/chat-effect-posters.js +0 -42
  157. package/dist/ui/chat-effects.js +0 -167
  158. package/dist/ui/effect-trial-controller.js +0 -124
  159. package/dist/ui/effect-trial-offer.js +0 -2
  160. package/dist/ui/effect-trial-panel.js +0 -40
  161. package/dist/ui/effects-waker.js +0 -2
  162. package/dist/ui/settings-effects.js +0 -202
  163. package/dist/ui/settings-owner-effects.js +0 -82
  164. package/dist/ui/shared/effect-app-runtime.js +0 -299
  165. package/dist/ui/shared/effect-baseline.js +0 -64
  166. package/dist/ui/shared/effect-chat-runtime.js +0 -162
  167. package/dist/ui/shared/effect-cursor.js +0 -126
  168. package/dist/ui/shared/effect-display-metadata.js +0 -39
  169. package/dist/ui/shared/effect-frame.js +0 -146
  170. package/dist/ui/shared/effect-geometry.js +0 -137
  171. package/dist/ui/shared/effect-host-style.js +0 -10
  172. package/dist/ui/shared/effect-owner-regions.js +0 -74
  173. package/dist/ui/shared/effect-owner-source.js +0 -122
  174. package/dist/ui/shared/effect-policy.js +0 -151
  175. package/dist/ui/shared/effect-poster-reader.js +0 -98
  176. package/dist/ui/shared/effect-posters.js +0 -117
  177. package/dist/ui/shared/effect-profiler.js +0 -184
  178. package/dist/ui/shared/effect-profiles.js +0 -65
  179. package/dist/ui/shared/effect-registry.js +0 -545
  180. package/dist/ui/shared/effect-runtime-loader.js +0 -236
  181. package/dist/ui/shared/effect-safety.js +0 -153
  182. package/dist/ui/shared/effect-stock-snapshot.js +0 -43
  183. package/dist/ui/shared/effect-stock-trust.js +0 -217
  184. package/dist/ui/shared/effect-trial-shadow.js +0 -62
  185. package/dist/ui/shared/effect-trial-source.js +0 -103
  186. package/dist/ui/shared/effect-trial-stage.js +0 -170
  187. package/dist/ui/shared/effect-trial.css +0 -40
  188. package/dist/ui/shared/feature-flags.js +0 -127
  189. package/dist/ui/shared/owner-effect-prefs.js +0 -117
  190. package/dist/web-effect-bootstrap.d.ts +0 -12
  191. package/dist/web-effect-bootstrap.js +0 -49
  192. package/dist/web-effect-bootstrap.js.map +0 -1
  193. package/dist/web-effect-bundle.d.ts +0 -28
  194. package/dist/web-effect-bundle.js +0 -171
  195. package/dist/web-effect-bundle.js.map +0 -1
  196. package/dist/web-effect-canonical.d.ts +0 -8
  197. package/dist/web-effect-canonical.js +0 -27
  198. package/dist/web-effect-canonical.js.map +0 -1
  199. package/dist/web-effect-library.d.ts +0 -114
  200. package/dist/web-effect-library.js +0 -530
  201. package/dist/web-effect-library.js.map +0 -1
  202. package/dist/web-effect-playback.d.ts +0 -30
  203. package/dist/web-effect-playback.js +0 -71
  204. package/dist/web-effect-playback.js.map +0 -1
  205. package/dist/web-effect-poster.d.ts +0 -8
  206. package/dist/web-effect-poster.js +0 -52
  207. package/dist/web-effect-poster.js.map +0 -1
  208. package/dist/web-effect-profiles.d.ts +0 -118
  209. package/dist/web-effect-profiles.js +0 -181
  210. package/dist/web-effect-profiles.js.map +0 -1
  211. package/dist/web-effect-service.d.ts +0 -93
  212. package/dist/web-effect-service.js +0 -335
  213. package/dist/web-effect-service.js.map +0 -1
  214. package/dist/web-effect-tools.d.ts +0 -48
  215. package/dist/web-effect-tools.js +0 -129
  216. package/dist/web-effect-tools.js.map +0 -1
  217. package/dist/web-effect-trials.d.ts +0 -86
  218. package/dist/web-effect-trials.js +0 -204
  219. package/dist/web-effect-trials.js.map +0 -1
  220. package/dist/web-effect-upload.d.ts +0 -27
  221. package/dist/web-effect-upload.js +0 -185
  222. package/dist/web-effect-upload.js.map +0 -1
  223. package/dist/web-effect-worker.d.ts +0 -8
  224. package/dist/web-effect-worker.js +0 -170
  225. package/dist/web-effect-worker.js.map +0 -1
  226. package/dist/web-effect.d.ts +0 -103
  227. package/dist/web-effect.js +0 -153
  228. package/dist/web-effect.js.map +0 -1
  229. package/dist/web-experimental-http.d.ts +0 -2
  230. package/dist/web-experimental-http.js +0 -12
  231. package/dist/web-experimental-http.js.map +0 -1
  232. package/dist/web-mod-agent.d.ts +0 -50
  233. package/dist/web-mod-agent.js +0 -207
  234. package/dist/web-mod-agent.js.map +0 -1
@@ -1,40 +0,0 @@
1
- /** #1629: fixed host producer. Creates no frame or request; the admitted app controller owns its lease. */
2
- import { html, useLayoutEffect, useRef } from "/assets/app-html.js";
3
- import { register, t } from "/assets/app-i18n.js";
4
- import { createEffectTrialShadow } from "/assets/effect-trial-shadow.js";
5
- import { createEffectTrialStage } from "/assets/effect-trial-stage.js";
6
- register("effectTrial", {
7
- en: { label: "Trial", stop: "Stop trial", close: "Close", note: "A bounded sample. Nothing is applied or saved." },
8
- "zh-TW": { label: "試播", stop: "停止試播", close: "關閉", note: "僅在此區塊試播,不會套用或儲存。" },
9
- });
10
- /** The caller owns authenticated admission, lease/deadline and shared pool. No script/HTML is rendered here. */
11
- export function EffectTrialPanel({ window: host = globalThis.window, binding, title, current, onStage, onStop, onClose }) {
12
- const panel = useRef(null), life = useRef(null), latest = useRef(null);
13
- latest.current = { current, onStage, onStop, onClose };
14
- const stop = close => {
15
- const entry = life.current; if (!entry || (close ? entry.closed : entry.dead)) return;
16
- if (close) entry.closed = true;
17
- if (!entry.dead) { entry.dead = true; entry.api?.dispose(); }
18
- try { (close ? entry.onClose : entry.onStop)?.("stopped"); } catch { /* lease already ended */ }
19
- };
20
- useLayoutEffect(() => {
21
- const entry = { dead: false, closed: false, api: null, shadow: null, onStop, onClose }; life.current = entry;
22
- const ended = reason => { if (entry.dead) return; entry.dead = true; try { entry.onStop?.(reason); } catch { /* never revive */ } };
23
- try {
24
- entry.shadow = createEffectTrialShadow({ window: host, panel: panel.current, title,
25
- labels: { label: t("effectTrial.label"), close: t("effectTrial.close"), stop: t("effectTrial.stop"), note: t("effectTrial.note") },
26
- onStop: () => stop(false), onClose: () => stop(true), onError: ended,
27
- onReady(shell) {
28
- if (entry.dead || life.current !== entry) return;
29
- entry.api = createEffectTrialStage({ window: host, panel: panel.current, stage: shell.stage, binding,
30
- current: own => !entry.dead && life.current === entry && latest.current.current?.(own) === true, onStop: ended });
31
- if (!entry.api) ended("stage_unavailable");
32
- else { try { latest.current.onStage?.(entry.api); } catch { entry.api.dispose(); ended("stage_unavailable"); } }
33
- } });
34
- if (!entry.shadow) ended("stage_unavailable");
35
- } catch { entry.api?.dispose(); ended("stage_unavailable"); }
36
- return () => { entry.api?.dispose(); ended("stopped"); entry.shadow?.dispose(); entry.dead = true; if (life.current === entry) life.current = null; };
37
- }, [host, binding]);
38
- useLayoutEffect(() => { life.current?.shadow?.text(title, { label: t("effectTrial.label"), close: t("effectTrial.close"), stop: t("effectTrial.stop"), note: t("effectTrial.note") }); });
39
- return html`<aside ref=${panel} data-trust="preview" class="pv-panel effect-trial-panel" aria-label=${t("effectTrial.label")}></aside>`;
40
- }
@@ -1,2 +0,0 @@
1
- // Compatibility entry for opted-in effect consumers. Product boot uses the neutral feature policy.
2
- export { EMPTY_CHAT_EFFECTS, chatHasEffects, getAppEffects, effectsAwake, effectsLoaded, onEffectsAwake, wakeEffects, startEffectsWaker } from "/assets/feature-flags.js";
@@ -1,202 +0,0 @@
1
- // #1629 B2: library management only. This module never loads Worker code or creates a preview/frame.
2
- import { html, useEffect, useRef, useState } from "/assets/app-html.js";
3
- import { register, t } from "/assets/app-i18n.js";
4
- import { appStore, useStore } from "/assets/app-store.js";
5
- import { confirmDialog } from "/assets/ui-confirm.js";
6
- import { OwnerEffects } from "./settings-owner-effects.js";
7
- import { CardTitle } from "./settings-learn.js";
8
-
9
- register("effectSettings", {
10
- en: {
11
- title: "Effects", lead: "Your effect files, and which one a chat uses.", folder: "Upload an effect folder", files: "effect.json + worker.js (and pictures), up to 1 MiB.",
12
- inUse: "This effect is still in use and cannot be deleted.", quota: "Your library reached its 10-effect or 8 MiB limit. Delete unused copies first.", rateLimited: "Too many changes. Wait a moment, then try again.",
13
- loading: "Loading effects…", retry: "Reload library", empty: "No effects in your library.", failed: "Couldn't load the library. Try again.", actionFailed: "Couldn't save this change. Try again.", savedRefresh: "Saved. Reload the library to see the result.",
14
- uploadFailed: "Couldn't upload this folder. Check its files and size, then try again.", public: "Effect management isn't available on a temporary public link.", readOnly: "Read-only session: sign in as an admin to change effects.",
15
- yours: "Your copy", shared: "Shared with you", share: "Share your copy with agents and people on this fleet", delete: "Delete your copy", deleteConfirm: "Delete your unused copy of this effect? Other people's copies are kept.",
16
- chat: "Chat", chooseChat: "Choose a chat", apply: "Save for this chat", remove: "Remove from this chat", saved: "Saved.", history: "Profile history", unmeasured: "Not measured yet", unavailable: "Profile unavailable", reported: "Reported host observations; they don't prove an effect's cost.",
17
- fps: "Host FPS p50 / p10", tasks: "Host long task p95", noTasks: "Not observed", standard: "Standard device", constrained: "Constrained device", params: "Parameters", badParams: "Check the parameter values.", more: "Show more", count: "{0} effects", one: "1 effect", profileFailed: "Couldn't read profile history. Try again.",
18
- },
19
- "zh-TW": {
20
- title: "效果", lead: "你的效果檔案,以及每個聊天使用哪一個。", folder: "上傳效果資料夾", files: "effect.json + worker.js(可加圖片),最多 1 MiB。",
21
- inUse: "這個效果仍在使用中,無法刪除。", quota: "你的程式庫已達 10 個效果或 8 MiB 上限,請先刪除未使用的副本。", rateLimited: "變更太頻繁,請稍候再試。",
22
- loading: "正在載入效果…", retry: "重新載入程式庫", empty: "程式庫中還沒有效果。", failed: "無法載入程式庫,請重試。", actionFailed: "無法儲存變更,請重試。", savedRefresh: "已儲存,請重新載入程式庫查看結果。",
23
- uploadFailed: "無法上傳這個資料夾,請檢查檔案與大小後重試。", public: "臨時公開連結不提供效果管理。", readOnly: "目前為唯讀登入,請以管理員身分登入才能修改效果。",
24
- yours: "你持有的副本", shared: "他人與你共享", share: "將你的副本共享給這個 fleet 的 agent 與使用者", delete: "刪除你的副本", deleteConfirm: "刪除你未使用的效果副本?其他人的副本會保留。",
25
- chat: "聊天", chooseChat: "選擇聊天", apply: "儲存到這個聊天", remove: "從這個聊天移除", saved: "已儲存。", history: "效能回報紀錄", unmeasured: "尚未測量", unavailable: "無法取得效能回報", reported: "這是主機觀測回報,不能證明效果本身的資源用量。",
26
- fps: "主機 FPS p50/p10", tasks: "主機長任務 p95", noTasks: "未觀測到", standard: "一般裝置", constrained: "受限裝置", params: "參數", badParams: "請檢查參數值。", more: "顯示更多", count: "{0} 個效果", one: "1 個效果", profileFailed: "無法讀取效能回報紀錄,請重試。",
27
- },
28
- });
29
- const tn = (key, ...args) => t(`effectSettings.${key}`, ...args);
30
- const REFUSALS = Object.freeze({ effect_in_use: "inUse", effect_quota: "quota", rate_limited: "rateLimited" });
31
- const refusal = (body, fallback) => tn(typeof body?.error === "string" && Object.hasOwn(REFUSALS, body.error) ? REFUSALS[body.error] : fallback);
32
- const ID = /^[0-9a-f]{32}$/;
33
- const PATH = /^(?:effect\.json|worker\.js|poster\.png|cursor\.png|assets\/[a-z0-9][a-z0-9_-]{0,63}\.(?:png|jpe?g|webp|gif))$/;
34
- const RAW_MAX = 8 * 1024 * 1024, WIRE_MAX = 12 * 1024 * 1024;
35
- const plain = (v) => !!v && typeof v === "object" && Object.getPrototypeOf(v) === Object.prototype;
36
- const publicLink = () => typeof document !== "undefined" && document.body?.dataset.publicLink === "1";
37
-
38
- /** File metadata bounds are checked before reading. No directory path reaches the server. */
39
- export async function encodeEffectFolder(input, signal) {
40
- const files = Array.from(input || []);
41
- if (files.length < 2 || files.length > 20) throw Error("effect_files");
42
- const nested = files.some(f => !!f?.webkitRelativePath), paths = new Set(); let root = null, raw = 0, assets = 0;
43
- const rows = files.map(file => {
44
- if (!file || typeof file.name !== "string" || !Number.isSafeInteger(file.size) || file.size < 0 || (raw += file.size) > RAW_MAX || typeof file.arrayBuffer !== "function") throw Error("effect_files");
45
- let path = file.name;
46
- if (nested) {
47
- const relative = file.webkitRelativePath;
48
- if (typeof relative !== "string" || relative.startsWith("/") || relative.includes("\\")) throw Error("effect_files");
49
- const at = relative.indexOf("/"); if (at < 1) throw Error("effect_files");
50
- const prefix = relative.slice(0, at); if (root === null) root = prefix; if (root !== prefix) throw Error("effect_files");
51
- path = relative.slice(at + 1);
52
- }
53
- if (!PATH.test(path) || paths.has(path) || (path.startsWith("assets/") && ++assets > 16)) throw Error("effect_files");
54
- paths.add(path); return { file, path };
55
- });
56
- if (!paths.has("effect.json") || !paths.has("worker.js")) throw Error("effect_files");
57
- const encoded = [];
58
- for (const { file, path } of rows) {
59
- if (signal?.aborted) throw Error("cancelled");
60
- const buffer = await file.arrayBuffer(); if (signal?.aborted) throw Error("cancelled");
61
- if (!(buffer instanceof ArrayBuffer) || buffer.byteLength !== file.size) throw Error("effect_files");
62
- const bytes = new Uint8Array(buffer), chunks = [];
63
- for (let i = 0; i < bytes.length; i += 8192) chunks.push(String.fromCharCode(...bytes.subarray(i, i + 8192)));
64
- encoded.push({ path, data_b64: btoa(chunks.join("")) });
65
- }
66
- const body = JSON.stringify(encoded); if (new TextEncoder().encode(body).byteLength > WIRE_MAX) throw Error("effect_files");
67
- return body;
68
- }
69
- export function effectCatalog(body) {
70
- if (!plain(body) || typeof body.canWrite !== "boolean" || !Array.isArray(body.effects)) return null;
71
- const seen = new Set(), effects = [];
72
- for (const e of body.effects) {
73
- if (!plain(e) || typeof e.id !== "string" || !ID.test(e.id) || seen.has(e.id) || !plain(e.manifest) || typeof e.manifest.title !== "string" || e.manifest.title.length < 1 || e.manifest.title.length > 40 || !Number.isInteger(e.bytes) || e.bytes < 1 || e.bytes > 1048576 || typeof e.ownClaim !== "boolean" || typeof e.sharedByYou !== "boolean" || !e.ownClaim && e.sharedByYou) return null;
74
- if (effectParamOverrides(e.manifest) === null) return null;
75
- seen.add(e.id); effects.push(e);
76
- }
77
- return { canWrite: body.canWrite, effects };
78
- }
79
- /** UI draft conversion only; the host independently checks the manifest and every override on apply. */
80
- export function effectParamOverrides(manifest, draft = {}) {
81
- if (!plain(manifest) || !plain(draft) || manifest.params !== undefined && !plain(manifest.params)) return null;
82
- const defs = manifest.params || {}, out = Object.create(null);
83
- if (Object.keys(defs).length > 16 || Object.keys(draft).some(k => !Object.hasOwn(defs, k))) return null;
84
- for (const [key, d] of Object.entries(defs)) {
85
- if (!/^[a-z][a-z0-9_]{0,31}$/.test(key) || !plain(d)) return null;
86
- const supplied = Object.hasOwn(draft, key); let v = supplied ? draft[key] : d.default;
87
- if (d.type === "int" || d.type === "number") {
88
- if (supplied && typeof v === "string" && v.trim() !== "") v = Number(v);
89
- if (typeof v !== "number" || !Number.isFinite(v) || !Number.isFinite(d.min) || !Number.isFinite(d.max) || v < d.min || v > d.max || d.type === "int" && !Number.isSafeInteger(v)) return null;
90
- if (d.type === "number" && d.step !== undefined) {
91
- const n = (v - d.min) / d.step;
92
- if (!(d.step > 0) || !Number.isFinite(n) || Math.abs(n - Math.round(n)) > 1e-8) return null;
93
- }
94
- } else if (d.type === "bool") { if (typeof v !== "boolean") return null; }
95
- else if (d.type === "enum") { if (!Array.isArray(d.values) || d.values.length > 16 || typeof v !== "string" || !d.values.includes(v)) return null; }
96
- else if (d.type === "color") { if (typeof v !== "string" || !/^#[0-9a-fA-F]{6}$/.test(v)) return null; v = v.toLowerCase(); }
97
- else return null;
98
- if (v !== d.default) out[key] = v;
99
- }
100
- return out;
101
- }
102
- function Params({ manifest, draft, update, disabled }) {
103
- const defs = plain(manifest.params) ? Object.entries(manifest.params).slice(0, 16) : [];
104
- if (!defs.length) return null;
105
- return html`<fieldset class="s-fx-params" disabled=${disabled}><legend>${tn("params")}</legend>${defs.map(([key, d]) => {
106
- const value = Object.hasOwn(draft, key) ? draft[key] : d.default;
107
- if (d.type === "bool") return html`<label key=${key} class="check"><input data-param=${key} type="checkbox" checked=${value} onChange=${e => update(key, e.target.checked)} />${key}</label>`;
108
- if (d.type === "enum") return html`<label key=${key}>${key}<select data-param=${key} value=${value} onChange=${e => update(key, e.target.value)}>${d.values.map(v => html`<option key=${v} value=${v}>${v}</option>`)}</select></label>`;
109
- return html`<label key=${key}>${key}<input data-param=${key} type=${d.type === "color" ? "color" : "number"} min=${d.min} max=${d.max} step=${d.type === "int" ? 1 : d.step || "any"} value=${value} onInput=${e => update(key, e.target.value)} /></label>`;
110
- })}</fieldset>`;
111
- }
112
- async function call(path, init, signal) {
113
- try {
114
- const res = await fetch(path, { credentials: "same-origin", ...init, signal });
115
- let body = null; try { body = await res.json(); } catch { /* unknown response refuses */ }
116
- return { ok: res.ok && !body?.error, body };
117
- } catch { return { ok: false, body: null }; }
118
- }
119
-
120
- function Profile({ value }) {
121
- if (!value || value.error || !Array.isArray(value.records)) return html`<p class="sub">${tn("unavailable")}</p>`;
122
- if (value.status === "unmeasured" || !value.records.length) return html`<p class="sub">${tn("unmeasured")}</p>`;
123
- return html`<div class="s-fx-profile"><p class="sub">${tn("reported")}</p>${value.records.map((p, i) => {
124
- const valid = p && ["standard", "constrained"].includes(p.device) && [p.fps_p50, p.fps_p10].every(n => typeof n === "number" && Number.isFinite(n) && n >= 0 && n <= 1000);
125
- if (!valid) return html`<p key=${i} class="sub">${tn("unavailable")}</p>`;
126
- return html`<div key=${i}><strong>${tn(p.device)}</strong><p class="sub">${tn("fps")}: ${p.fps_p50} / ${p.fps_p10} · ${tn("tasks")}: ${typeof p.long_task_p95 === "number" && Number.isFinite(p.long_task_p95) ? `${p.long_task_p95} ms` : tn("noTasks")}</p></div>`;
127
- })}</div>`;
128
- }
129
-
130
- /** Admin changes remain authoritative on the host; canWrite is a display hint only. */
131
- export function EffectsCard() {
132
- const app = useStore(appStore), [view, setView] = useState(null), [error, setError] = useState(null), [busy, setBusy] = useState(false), [message, setMessage] = useState(null);
133
- const [target, setTarget] = useState(""), [limit, setLimit] = useState(50), [history, setHistory] = useState({}), [drafts, setDrafts] = useState({});
134
- const uploadInput = useRef(null);
135
- const life = useRef({ alive: false, seq: 0, controller: null, pending: false });
136
- const current = (seq) => life.current.alive && seq === life.current.seq;
137
- const begin = () => { if (!life.current.alive || life.current.pending || publicLink()) return null; life.current.pending = true; life.current.controller?.abort(); const controller = new AbortController(), seq = ++life.current.seq; life.current.controller = controller; return { seq, signal: controller.signal }; };
138
- const finish = seq => { if (current(seq)) { life.current.pending = false; setBusy(false); } };
139
- const load = async (op) => {
140
- const r = await call("/ui/effects", {}, op.signal); if (!current(op.seq)) return false;
141
- const data = r.ok ? effectCatalog(r.body) : null; setView(data); setHistory({}); setError(data ? null : tn("failed")); return !!data;
142
- };
143
- const reload = async () => { const op = begin(); if (!op) return; setBusy(true); setMessage(null); await load(op); finish(op.seq); };
144
- useEffect(() => {
145
- life.current.alive = true;
146
- if (!publicLink()) reload();
147
- return () => { life.current.alive = false; life.current.seq++; life.current.controller?.abort(); };
148
- }, []);
149
- if (publicLink()) return html`<section class="card s-appearance s-effects"><h3>${tn("title")}</h3><p class="sub">${tn("public")}</p></section>`;
150
- const names = app.ready ? [...new Set((app.instances || []).map(i => i?.name).filter(n => typeof n === "string" && n.length > 0 && n.length <= 128))] : [];
151
- const selected = names.includes(target) ? target : "";
152
- const change = async (path, body, method = "POST") => {
153
- if (busy || !view?.canWrite) return;
154
- const op = begin(); if (!op) return; setBusy(true); setMessage(null);
155
- const r = await call(path, { method, ...(body === undefined ? {} : { headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }) }, op.signal);
156
- if (!current(op.seq)) return;
157
- if (!r.ok) setMessage(refusal(r.body, "actionFailed")); else { const refreshed = await load(op); if (current(op.seq)) setMessage(tn(refreshed ? "saved" : "savedRefresh")); }
158
- finish(op.seq);
159
- };
160
- const upload = async (event) => {
161
- const input = event.currentTarget, files = Array.from(input.files || []); input.value = "";
162
- if (busy || !view?.canWrite || !files.length) return;
163
- const op = begin(); if (!op) return; setBusy(true); setMessage(null);
164
- try {
165
- const body = await encodeEffectFolder(files, op.signal); if (!current(op.seq)) return;
166
- const r = await call("/ui/effects/upload", { method: "POST", headers: { "Content-Type": "application/json" }, body }, op.signal);
167
- if (!current(op.seq)) return;
168
- if (!r.ok) setMessage(refusal(r.body, "uploadFailed")); else { const refreshed = await load(op); if (current(op.seq)) setMessage(tn(refreshed ? "saved" : "savedRefresh")); }
169
- } catch { if (current(op.seq)) setMessage(tn("uploadFailed")); }
170
- finish(op.seq);
171
- };
172
- const profile = async (id) => {
173
- if (busy) return; const op = begin(); if (!op) return; setBusy(true);
174
- const r = await call(`/ui/effects/profile/${id}`, {}, op.signal); if (!current(op.seq)) return;
175
- if (r.ok && r.body && Array.isArray(r.body.records) && r.body.records.length <= 40) setHistory(h => ({ ...h, [id]: r.body })); else setMessage(tn("profileFailed"));
176
- finish(op.seq);
177
- };
178
- const askDelete = async id => {
179
- if (!view?.canWrite) return; const op = begin(); if (!op) return; setBusy(true);
180
- const yes = await confirmDialog({ message: tn("deleteConfirm"), confirmLabel: tn("delete"), danger: true });
181
- if (!current(op.seq)) return; finish(op.seq);
182
- if (yes) await change(`/ui/effects/${id}`, undefined, "DELETE");
183
- };
184
- return html`<section class="card s-appearance s-effects"><${CardTitle} title=${tn("title")} topic="effects" /><p class="sub">${tn("lead")}</p>
185
- <div class="s-fx-actions"><button type="button" class="btn" disabled=${busy} onClick=${reload}>${tn("retry")}</button>
186
- ${view?.canWrite ? html`<button class="btn s-fx-choose" type="button" disabled=${busy} onClick=${() => uploadInput.current?.click()}>${tn("folder")}</button><input ref=${uploadInput} class="offscreen s-fx-upload" aria-label=${tn("folder")} tabindex="-1" type="file" webkitdirectory=${true} directory=${true} multiple disabled=${busy} onChange=${upload} />` : null}</div>
187
- ${busy ? html`<p class="sub" role="status">${tn("loading")}</p>` : null}${error ? html`<p class="err" role="status">${error}</p>` : null}${message ? html`<p class="sub" role="status">${message}</p>` : null}
188
- ${view ? html`<p class="sub">${tn(view.effects.length === 1 ? "one" : "count", view.effects.length)}${!view.canWrite ? ` · ${tn("readOnly")}` : ""}</p>` : null}
189
- ${view?.canWrite ? html`<p class="sub">${tn("files")}</p><label class="s-fx-chat">${tn("chat")}<select class="s-fx-target" value=${selected} disabled=${busy} onChange=${e => setTarget(e.target.value)}><option value="">${tn("chooseChat")}</option>${names.map(n => html`<option key=${n} value=${n}>${n}</option>`)}</select></label>` : null}
190
- ${view && !view.effects.length ? html`<p class="sub">${tn("empty")}</p>` : null}
191
- <${OwnerEffects} catalog=${view} busy=${busy} />
192
- <div class="s-fx-list">${(view?.effects || []).slice(0, limit).map(e => html`<article class="s-fx-item" key=${e.id} data-effect-id=${e.id}><h4>${e.manifest.title}</h4><p class="sub"><code>${e.id.slice(0, 8)}</code> · ${Math.ceil(e.bytes / 1024)} KiB · ${tn(e.ownClaim ? "yours" : "shared")}</p>
193
- ${e.manifest.poster === "poster.png" ? html`<img class="s-fx-poster" src=${`/ui/effects/image/${e.id}/poster.png`} alt="" loading="lazy" />` : null}
194
- <${Profile} value=${history[e.id] || e.profiles} />
195
- ${view.canWrite ? html`<${Params} manifest=${e.manifest} draft=${drafts[e.id] || {}} disabled=${busy} update=${(key, value) => setDrafts(s => ({ ...s, [e.id]: { ...(s[e.id] || {}), [key]: value } }))} />` : null}<div class="s-fx-actions"><button class="btn" type="button" disabled=${busy} onClick=${() => profile(e.id)}>${tn("history")}</button>
196
- ${view.canWrite ? html`<button class="btn s-fx-apply" type="button" disabled=${busy || !selected} onClick=${() => { const params = effectParamOverrides(e.manifest, drafts[e.id] || {}); if (params === null) setMessage(tn("badParams")); else change("/ui/effects/apply", { instance: selected, id: e.id, params }); }}>${tn("apply")}</button><button class="btn s-fx-remove" type="button" disabled=${busy || !selected} onClick=${() => change("/ui/effects/remove", { instance: selected, id: e.id })}>${tn("remove")}</button>` : null}
197
- ${view.canWrite && e.ownClaim ? html`<button class="btn s-fx-delete" type="button" disabled=${busy} onClick=${() => askDelete(e.id)}>${tn("delete")}</button>` : null}</div>
198
- ${view.canWrite && e.ownClaim ? html`<label class="check"><input class="s-fx-share" type="checkbox" checked=${e.sharedByYou} disabled=${busy} onChange=${event => change("/ui/effects/share", { id: e.id, shared: event.target.checked })} />${tn("share")}</label>` : null}
199
- </article>`)}</div>
200
- ${view && limit < view.effects.length ? html`<button class="btn" type="button" onClick=${() => setLimit(n => n + 50)}>${tn("more")}</button>` : null}
201
- </section>`;
202
- }
@@ -1,82 +0,0 @@
1
- // #1629: authoritative selectors and local slot controls; the app owns all playback and resource reads.
2
- import { html, useEffect, useRef, useState } from "/assets/app-html.js";
3
- import { register, t } from "/assets/app-i18n.js";
4
- import { useStore } from "/assets/app-store.js";
5
- import { OWNER_EFFECT_PREF_KEYS } from "/assets/app-prefs.js";
6
- import { getAppEffects, wakeEffects } from "./effects-waker.js";
7
- import { ownerEffectsStore, watchOwnerEffects, refreshOwnerEffects, saveOwnerEffects } from "/assets/owner-effect-prefs.js";
8
- register("ownerEffects", {
9
- en: {
10
- title: "Your effect choices", operatorTitle: "Host-wide effect choices", person: "Follows your sign-in; Discord and Telegram are separate.",
11
- operator: "Shared by everyone signed in as the host.",
12
- sidebar: "Sidebar", header: "Header", page: "Page", box: "Box", cursor: "Cursor", none: "None", savedChoice: "Saved choice",
13
- unknown: "Choices are unavailable. Reload to try again.", changed: "This page can no longer access these choices. Reload and sign in again.",
14
- unavailable: "No longer available", clear: "Clear", reload: "Reload choices", saved: "Saved.", failed: "Couldn't save. Reload choices and try again.",
15
- savedRefresh: "Saved, but the choices couldn't be refreshed. Reload to check.", readOnly: "Sign in as an admin to change these choices.",
16
- playback: "Effects don't require HTML-preview permission.",
17
- pause: "Pause", resume: "Resume", running: "Running", cursorFailed: "The cursor could not be applied here. Clear or reload the choice to try again.", starting: "Starting", queued: "Waiting for a slot", paused: "Paused", off: "Playback is off or unavailable here.", budget: "Paused to keep the page responsive.", resumeFailed: "This effect cannot resume: the page or its safety history is not ready. Reload the page or choose another effect.",
18
- },
19
- "zh-TW": {
20
- title: "你的效果選擇", operatorTitle: "主機共用效果選擇", person: "跟隨你的登入身分;Discord 與 Telegram 各自獨立。",
21
- operator: "所有以主機身分登入的人共用。",
22
- sidebar: "側邊欄", header: "頁首", page: "頁面", box: "區塊", cursor: "游標", none: "無", savedChoice: "已儲存的選擇",
23
- unknown: "目前無法取得選擇,請重新載入再試。", changed: "這個頁面已無法存取選擇,請重新載入並登入。",
24
- unavailable: "已無法使用", clear: "清除", reload: "重新載入選擇", saved: "已儲存。", failed: "無法儲存,請重新載入選擇再試。",
25
- savedRefresh: "已儲存,但無法更新畫面;請重新載入確認。", readOnly: "請以管理員身分登入才能修改選擇。",
26
- playback: "效果不需要 HTML 預覽權限。",
27
- pause: "暫停", resume: "恢復", running: "播放中", cursorFailed: "這裡無法套用游標。可清除或重新載入選擇後再試。", starting: "啟動中", queued: "等待空位", paused: "已暫停", off: "這裡已關閉播放,或無法播放效果。", budget: "為維持頁面順暢,已暫停。", resumeFailed: "頁面或安全紀錄尚未就緒,無法恢復此效果。請重新載入頁面,或選擇其他效果。",
28
- },
29
- });
30
- const text = key => t(`ownerEffects.${key}`);
31
- export function ownerEffectFits(manifest, key) {
32
- if (!manifest || typeof manifest !== "object" || !OWNER_EFFECT_PREF_KEYS.includes(key)) return false;
33
- if (key === "fx.cursor") return !!manifest.cursor;
34
- const region = key.slice(3);
35
- return Array.isArray(manifest.regions) && manifest.regions.includes(region) && (region !== "box" || !!manifest.box);
36
- }
37
- export function OwnerEffects({ catalog, busy = false }) {
38
- const state = useStore(ownerEffectsStore), [message, setMessage] = useState(null), life = useRef({ alive: false, seq: 0 }), [runtime, setRuntime] = useState(null);
39
- useEffect(() => { life.current.alive = true; const stop = watchOwnerEffects(); return () => { life.current.alive = false; life.current.seq++; stop(); }; }, []);
40
- useEffect(() => {
41
- // #2193: managing personal effects needs the runtime's view of them: it is woken here (it may still be asleep).
42
- let live = true, entry = getAppEffects(), off = null, end = null;
43
- const pull = () => { if (!live) return; const gates = entry?.gates(); setRuntime({ entry, snapshot: entry?.api?.ownerSnapshot?.(), cursor: entry?.cursorSnapshot?.(), available: !!entry && !entry.dead && gates.enabled && gates.show && gates.transportReady && !gates.safe && !gates.publicLink && !gates.reducedMotion }); };
44
- const bind = () => { off = entry?.subscribe(pull) ?? null; end = entry?.onEnd(pull) ?? null; pull(); };
45
- if (entry) bind(); else { pull(); void wakeEffects().then(() => { if (!live) return; entry = getAppEffects(); bind(); }); }
46
- return () => { live = false; off?.(); end?.(); };
47
- }, []);
48
- const reload = () => { getAppEffects()?.api?.changedOwner?.(); return refreshOwnerEffects(); };
49
- // Library reloads/own share changes can alter availability without changing the preference revision.
50
- useEffect(() => { if (catalog) reload(); }, [catalog]);
51
- if (state.scope === "none") return null;
52
- const disabled = busy || state.busy || state.status !== "known" || state.revoked || !catalog?.canWrite;
53
- const save = async (key, id) => {
54
- if (disabled || !life.current.alive) return;
55
- if (id !== null && !(catalog.effects || []).some(e => e.id === id && ownerEffectFits(e.manifest, key))) return;
56
- const seq = ++life.current.seq; setMessage(null);
57
- const result = await saveOwnerEffects({ [key]: id });
58
- if (!life.current.alive || seq !== life.current.seq) return;
59
- if (result.ok || result.saved) getAppEffects()?.api?.changedOwner?.(); // #2193: asleep, the saved slot wakes it (app-prefs)
60
- setMessage(text(result.ok ? "saved" : result.saved ? "savedRefresh" : "failed"));
61
- };
62
- return html`<section class="s-fx-owner" aria-label=${text(state.scope === "operator" ? "operatorTitle" : "title")}><h4>${text(state.scope === "operator" ? "operatorTitle" : "title")}</h4><p class="sub">${text(state.scope === "operator" ? "operator" : "person")}</p>
63
- <p class="sub">${text("playback")}</p>${catalog && !catalog.canWrite ? html`<p class="sub">${text("readOnly")}</p>` : null}
64
- <button class="btn" type="button" disabled=${busy || state.busy || state.revoked} onClick=${reload}>${text("reload")}</button>
65
- ${state.status !== "known" ? html`<p class="sub" role="status">${text(state.revoked ? "changed" : "unknown")}</p>` : OWNER_EFFECT_PREF_KEYS.map(key => {
66
- const row = state.slots[key], options = (catalog?.effects || []).filter(e => ownerEffectFits(e.manifest, key));
67
- const absent = !!row.id && !options.some(e => e.id === row.id);
68
- const play = runtime?.snapshot?.rows?.find(r => r.slot === key && r.id === row.id);
69
- const unavailable = row.status === "unavailable" || runtime?.snapshot?.slots?.[key]?.id === row.id && runtime.snapshot.slots[key].status === "unavailable";
70
- const phase = !runtime?.available ? "off" : play?.reason === "paused" || play?.reason === "host_budget" ? "paused" : play?.phase === "running" ? "running" : play?.phase ? "starting" : play?.verdict === "queued" ? "queued" : "off";
71
- return html`<div class="s-fx-owner-row" key=${key}><label>${text(key.slice(3))}<select class="s-fx-target" data-owner-slot=${key} disabled=${disabled} value=${row.id || ""} onChange=${e => save(key, e.target.value || null)}>
72
- <option value="">${text("none")}</option>${absent ? html`<option value=${row.id} disabled>${text(row.status === "unavailable" ? "unavailable" : "savedChoice")}</option>` : null}
73
- ${options.map(e => html`<option key=${e.id} value=${e.id}>${e.manifest.title}</option>`)}</select></label>
74
- ${unavailable ? html`<span class="sub" role="status">${text("unavailable")}</span>` : null}
75
- ${key === "fx.cursor" && row.status === "available" && row.id && !unavailable ? html`<span class="sub" role="status" data-owner-runtime=${key}>${text(!runtime?.available ? "off" : runtime.cursor?.id === row.id && runtime.cursor.status === "running" ? "running" : runtime.cursor?.id === row.id && runtime.cursor.status === "loading" ? "starting" : runtime.cursor?.id === row.id && runtime.cursor.status === "unavailable" ? "cursorFailed" : "off")}</span>` : null}
76
- ${key !== "fx.cursor" && row.status === "available" && row.id && !unavailable ? html`<span class="sub" role="status" data-owner-runtime=${key}>${text(phase)}</span>
77
- ${play?.reason === "host_budget" ? html`<span class="sub">${text("budget")}</span>` : null}
78
- <button class="btn" type="button" data-owner-pause=${key} disabled=${!runtime?.available || !play} onClick=${() => runtime?.entry?.api?.pauseOwner?.(key)}>${text("pause")}</button>
79
- <button class="btn" type="button" data-owner-resume=${key} disabled=${!runtime?.available || !play} onClick=${() => { if (!runtime?.entry?.api?.resumeOwner?.(key)) setMessage(text("resumeFailed")); }}>${text("resume")}</button>` : null}
80
- ${row.id ? html`<button class="btn" type="button" data-owner-clear=${key} disabled=${disabled} onClick=${() => save(key, null)}>${text("clear")}</button>` : null}</div>`;
81
- })}${message ? html`<p class="sub" role="status">${message}</p>` : null}</section>`;
82
- }