@songsid/agend 2.3.0-beta.21 → 2.3.0-beta.23

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 (300) hide show
  1. package/README.md +2 -2
  2. package/README.zh-TW.md +2 -2
  3. package/dist/agent-cli.js +4 -2
  4. package/dist/agent-cli.js.map +1 -1
  5. package/dist/agent-endpoint.d.ts +9 -0
  6. package/dist/agent-endpoint.js +15 -0
  7. package/dist/agent-endpoint.js.map +1 -1
  8. package/dist/agent-whats-new.d.ts +5 -0
  9. package/dist/agent-whats-new.js +16 -1
  10. package/dist/agent-whats-new.js.map +1 -1
  11. package/dist/auth-api.d.ts +7 -0
  12. package/dist/auth-api.js +10 -3
  13. package/dist/auth-api.js.map +1 -1
  14. package/dist/backend/codex-package.d.ts +30 -0
  15. package/dist/backend/codex-package.js +133 -0
  16. package/dist/backend/codex-package.js.map +1 -0
  17. package/dist/backend/codex.d.ts +2 -0
  18. package/dist/backend/codex.js +9 -2
  19. package/dist/backend/codex.js.map +1 -1
  20. package/dist/backend/grok.js +56 -3
  21. package/dist/backend/grok.js.map +1 -1
  22. package/dist/backend/kiro.d.ts +9 -1
  23. package/dist/backend/kiro.js +102 -1
  24. package/dist/backend/kiro.js.map +1 -1
  25. package/dist/backend/opencode-permission.js +1 -1
  26. package/dist/backend/opencode-permission.js.map +1 -1
  27. package/dist/backend/opencode.js +2 -1
  28. package/dist/backend/opencode.js.map +1 -1
  29. package/dist/binary-probe.js +1 -1
  30. package/dist/bug-report-cli.js +1 -1
  31. package/dist/bundled-skills.d.ts +7 -3
  32. package/dist/bundled-skills.js +16 -7
  33. package/dist/bundled-skills.js.map +1 -1
  34. package/dist/channel/adapters/discord.js +3 -3
  35. package/dist/channel/adapters/discord.js.map +1 -1
  36. package/dist/channel/mcp-server.js +12 -4
  37. package/dist/channel/mcp-server.js.map +1 -1
  38. package/dist/channel/mcp-tools.d.ts +3 -1
  39. package/dist/channel/mcp-tools.js +38 -2
  40. package/dist/channel/mcp-tools.js.map +1 -1
  41. package/dist/child-process.d.ts +21 -0
  42. package/dist/child-process.js +54 -0
  43. package/dist/child-process.js.map +1 -0
  44. package/dist/cli.js +167 -182
  45. package/dist/cli.js.map +1 -1
  46. package/dist/command-table.d.ts +1 -1
  47. package/dist/command-table.js +1 -1
  48. package/dist/command-table.js.map +1 -1
  49. package/dist/config-validator.js +28 -2
  50. package/dist/config-validator.js.map +1 -1
  51. package/dist/config.d.ts +2 -2
  52. package/dist/config.js +16 -2
  53. package/dist/config.js.map +1 -1
  54. package/dist/daemon.d.ts +14 -0
  55. package/dist/daemon.js +45 -8
  56. package/dist/daemon.js.map +1 -1
  57. package/dist/darwin-memory.js +1 -1
  58. package/dist/doctor.d.ts +5 -0
  59. package/dist/doctor.js +42 -0
  60. package/dist/doctor.js.map +1 -1
  61. package/dist/export-import.js +1 -1
  62. package/dist/export-import.js.map +1 -1
  63. package/dist/fleet-level-config.d.ts +1 -1
  64. package/dist/fleet-level-config.js +2 -0
  65. package/dist/fleet-level-config.js.map +1 -1
  66. package/dist/fleet-manager.d.ts +73 -2
  67. package/dist/fleet-manager.js +400 -33
  68. package/dist/fleet-manager.js.map +1 -1
  69. package/dist/full-restart.d.ts +5 -5
  70. package/dist/full-restart.js +5 -5
  71. package/dist/full-restart.js.map +1 -1
  72. package/dist/general-knowledge/skills/fleet-config/SKILL.md +1 -1
  73. package/dist/general-knowledge/skills/fleet-restart/SKILL.md +8 -9
  74. package/dist/general-knowledge/skills/multi-channel/SKILL.md +1 -1
  75. package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +319 -0
  76. package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +529 -0
  77. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +19 -4
  78. package/dist/general-knowledge/steering/core-rules.md +1 -1
  79. package/dist/instance-config-impact.js +13 -3
  80. package/dist/instance-config-impact.js.map +1 -1
  81. package/dist/instance-lifecycle.js +6 -6
  82. package/dist/instance-lifecycle.js.map +1 -1
  83. package/dist/instructions.js +2 -1
  84. package/dist/instructions.js.map +1 -1
  85. package/dist/locale.js +137 -10
  86. package/dist/locale.js.map +1 -1
  87. package/dist/login-flows.js +1 -1
  88. package/dist/login-shell.js +3 -1
  89. package/dist/login-shell.js.map +1 -1
  90. package/dist/ls-rows.js +1 -1
  91. package/dist/needs-you-hub.d.ts +98 -1
  92. package/dist/needs-you-hub.js +313 -44
  93. package/dist/needs-you-hub.js.map +1 -1
  94. package/dist/needs-you-live.d.ts +5 -2
  95. package/dist/needs-you-live.js +6 -3
  96. package/dist/needs-you-live.js.map +1 -1
  97. package/dist/needs-you.d.ts +58 -3
  98. package/dist/needs-you.js +36 -3
  99. package/dist/needs-you.js.map +1 -1
  100. package/dist/outbound-handlers.js +25 -10
  101. package/dist/outbound-handlers.js.map +1 -1
  102. package/dist/outbound-schemas.d.ts +3 -3
  103. package/dist/provider-secret-registry.d.ts +5 -3
  104. package/dist/provider-secret-registry.js +8 -2
  105. package/dist/provider-secret-registry.js.map +1 -1
  106. package/dist/public-link-progress.d.ts +12 -1
  107. package/dist/public-link-progress.js +5 -10
  108. package/dist/public-link-progress.js.map +1 -1
  109. package/dist/public-tunnel-config.d.ts +11 -0
  110. package/dist/public-tunnel-config.js +68 -0
  111. package/dist/public-tunnel-config.js.map +1 -0
  112. package/dist/public-web-gateway.d.ts +2 -0
  113. package/dist/public-web-gateway.js +3 -2
  114. package/dist/public-web-gateway.js.map +1 -1
  115. package/dist/public-web-link.d.ts +13 -2
  116. package/dist/public-web-link.js +37 -10
  117. package/dist/public-web-link.js.map +1 -1
  118. package/dist/quickstart.js +14 -8
  119. package/dist/quickstart.js.map +1 -1
  120. package/dist/resource-report.js +1 -1
  121. package/dist/scheduler/task-board.d.ts +41 -1
  122. package/dist/scheduler/task-board.js +150 -4
  123. package/dist/scheduler/task-board.js.map +1 -1
  124. package/dist/sd-notify.js +2 -1
  125. package/dist/sd-notify.js.map +1 -1
  126. package/dist/secret-store.d.ts +2 -0
  127. package/dist/secret-store.js +21 -0
  128. package/dist/secret-store.js.map +1 -1
  129. package/dist/service-installer.d.ts +69 -0
  130. package/dist/service-installer.js +181 -0
  131. package/dist/service-installer.js.map +1 -1
  132. package/dist/service-owner.d.ts +44 -0
  133. package/dist/service-owner.js +48 -0
  134. package/dist/service-owner.js.map +1 -0
  135. package/dist/settings-api.d.ts +5 -0
  136. package/dist/settings-api.js +32 -10
  137. package/dist/settings-api.js.map +1 -1
  138. package/dist/settings-change.js +86 -3
  139. package/dist/settings-change.js.map +1 -1
  140. package/dist/settings-effect.js +6 -1
  141. package/dist/settings-effect.js.map +1 -1
  142. package/dist/settings-http-confirmation.d.ts +2 -0
  143. package/dist/settings-http-confirmation.js +4 -3
  144. package/dist/settings-http-confirmation.js.map +1 -1
  145. package/dist/settings-request-capability.d.ts +4 -1
  146. package/dist/settings-request-capability.js +11 -3
  147. package/dist/settings-request-capability.js.map +1 -1
  148. package/dist/setup-host.js +1 -1
  149. package/dist/setup-wizard.js +1 -1
  150. package/dist/setup-wizard.js.map +1 -1
  151. package/dist/task-board-summary.d.ts +101 -0
  152. package/dist/task-board-summary.js +177 -0
  153. package/dist/task-board-summary.js.map +1 -0
  154. package/dist/task-board-waker.d.ts +8 -1
  155. package/dist/task-board-waker.js +15 -6
  156. package/dist/task-board-waker.js.map +1 -1
  157. package/dist/tmux-control.js +1 -1
  158. package/dist/tmux-control.js.map +1 -1
  159. package/dist/tmux-exec-helper.d.ts +5 -1
  160. package/dist/tmux-exec-helper.js +13 -21
  161. package/dist/tmux-exec-helper.js.map +1 -1
  162. package/dist/tmux-manager.js +1 -1
  163. package/dist/tmux-read.js +1 -1
  164. package/dist/tmux-read.js.map +1 -1
  165. package/dist/topic-commands.d.ts +3 -0
  166. package/dist/topic-commands.js +29 -18
  167. package/dist/topic-commands.js.map +1 -1
  168. package/dist/transcript-monitor.js +5 -0
  169. package/dist/transcript-monitor.js.map +1 -1
  170. package/dist/transcript-sources.d.ts +2 -0
  171. package/dist/transcript-sources.js +4 -0
  172. package/dist/transcript-sources.js.map +1 -1
  173. package/dist/tunnel/cloudflared-install.js +3 -1
  174. package/dist/tunnel/cloudflared-install.js.map +1 -1
  175. package/dist/tunnel/cloudflared.d.ts +7 -16
  176. package/dist/tunnel/cloudflared.js +65 -13
  177. package/dist/tunnel/cloudflared.js.map +1 -1
  178. package/dist/tunnel/named-diagnostics.d.ts +13 -0
  179. package/dist/tunnel/named-diagnostics.js +52 -0
  180. package/dist/tunnel/named-diagnostics.js.map +1 -0
  181. package/dist/tunnel/types.d.ts +2 -1
  182. package/dist/tunnel/types.js.map +1 -1
  183. package/dist/types.d.ts +12 -0
  184. package/dist/ui/app-effects.js +10 -4
  185. package/dist/ui/app.html +2 -0
  186. package/dist/ui/chat-render.js +39 -0
  187. package/dist/ui/chat-store.js +8 -0
  188. package/dist/ui/chat-strings.js +5 -2
  189. package/dist/ui/chat-thread.js +15 -8
  190. package/dist/ui/effect-trial-offer.js +2 -31
  191. package/dist/ui/effects-waker.js +2 -70
  192. package/dist/ui/fleet-canvas.js +14 -6
  193. package/dist/ui/fleet-route-scene.js +86 -21
  194. package/dist/ui/fleet-task-card.js +5 -5
  195. package/dist/ui/fleet-task-common.js +41 -7
  196. package/dist/ui/fleet-task-work.js +107 -0
  197. package/dist/ui/fleet-tasks.js +46 -12
  198. package/dist/ui/fleet-traffic.js +57 -10
  199. package/dist/ui/icons/icon-192.png +0 -0
  200. package/dist/ui/icons/icon-512.png +0 -0
  201. package/dist/ui/image-lightbox.js +39 -1
  202. package/dist/ui/image-zoom.js +242 -0
  203. package/dist/ui/panel-chat.js +24 -13
  204. package/dist/ui/panel-details.js +2 -0
  205. package/dist/ui/panel-fleet.js +6 -2
  206. package/dist/ui/panel-needs.js +21 -6
  207. package/dist/ui/panel-settings.js +90 -15
  208. package/dist/ui/preview-panel.js +2 -1
  209. package/dist/ui/search-dialog.js +2 -1
  210. package/dist/ui/settings-appearance.js +159 -9
  211. package/dist/ui/settings-dialogs.js +33 -17
  212. package/dist/ui/settings-discord.js +6 -2
  213. package/dist/ui/settings-guests.js +2 -1
  214. package/dist/ui/settings-learn.js +1 -0
  215. package/dist/ui/settings-model.js +2 -11
  216. package/dist/ui/settings-mods.js +6 -3
  217. package/dist/ui/settings-strings.js +80 -0
  218. package/dist/ui/settings-token.js +3 -1
  219. package/dist/ui/settings-web-authority.js +6 -3
  220. package/dist/ui/settings-wizard.js +13 -7
  221. package/dist/ui/shared/app-i18n.js +2 -2
  222. package/dist/ui/shared/app-needs.js +2 -2
  223. package/dist/ui/shared/app-store.js +2 -2
  224. package/dist/ui/shared/app-stream.js +2 -1
  225. package/dist/ui/shared/app.css +125 -28
  226. package/dist/ui/shared/app.js +5 -1
  227. package/dist/ui/shared/effect-app-runtime.js +7 -5
  228. package/dist/ui/shared/effect-chat-runtime.js +2 -7
  229. package/dist/ui/shared/effect-cursor.js +3 -2
  230. package/dist/ui/shared/effect-display-metadata.js +39 -0
  231. package/dist/ui/shared/effect-registry.js +4 -2
  232. package/dist/ui/shared/effect-runtime-loader.js +3 -1
  233. package/dist/ui/shared/effect-stock-trust.js +1 -1
  234. package/dist/ui/shared/feature-flags.js +127 -0
  235. package/dist/ui/shared/identity-color.js +6 -3
  236. package/dist/ui/shared/instance-nav.js +3 -2
  237. package/dist/ui/shared/text-undo.js +210 -0
  238. package/dist/ui/shared/ui-dialog.js +4 -3
  239. package/dist/ui/shared/ui-icons.js +2 -0
  240. package/dist/ui/signin.html +2 -0
  241. package/dist/update-dispatch.js +1 -1
  242. package/dist/update-launch.d.ts +1 -1
  243. package/dist/update-launch.js +1 -2
  244. package/dist/update-launch.js.map +1 -1
  245. package/dist/usage/providers.js +1 -1
  246. package/dist/usage/providers.js.map +1 -1
  247. package/dist/view-api.js +1 -1
  248. package/dist/web-api.d.ts +12 -2
  249. package/dist/web-api.js +266 -18
  250. package/dist/web-api.js.map +1 -1
  251. package/dist/web-app-icon-worker.d.ts +1 -0
  252. package/dist/web-app-icon-worker.js +15 -0
  253. package/dist/web-app-icon-worker.js.map +1 -0
  254. package/dist/web-app-icon.d.ts +107 -0
  255. package/dist/web-app-icon.js +256 -0
  256. package/dist/web-app-icon.js.map +1 -0
  257. package/dist/web-appearance-tools.d.ts +1 -0
  258. package/dist/web-appearance-tools.js +7 -3
  259. package/dist/web-appearance-tools.js.map +1 -1
  260. package/dist/web-appearance.d.ts +4 -10
  261. package/dist/web-appearance.js +4 -3
  262. package/dist/web-appearance.js.map +1 -1
  263. package/dist/web-chat-history.d.ts +37 -0
  264. package/dist/web-chat-history.js +57 -2
  265. package/dist/web-chat-history.js.map +1 -1
  266. package/dist/web-experimental-http.d.ts +2 -0
  267. package/dist/web-experimental-http.js +12 -0
  268. package/dist/web-experimental-http.js.map +1 -0
  269. package/dist/web-experimental.d.ts +11 -0
  270. package/dist/web-experimental.js +49 -0
  271. package/dist/web-experimental.js.map +1 -0
  272. package/dist/web-guest-access.d.ts +1 -0
  273. package/dist/web-guest-access.js +11 -3
  274. package/dist/web-guest-access.js.map +1 -1
  275. package/dist/web-icons.d.ts +33 -1
  276. package/dist/web-icons.js +83 -11
  277. package/dist/web-icons.js.map +1 -1
  278. package/dist/web-mod-store.d.ts +2 -0
  279. package/dist/web-mod-store.js +10 -0
  280. package/dist/web-mod-store.js.map +1 -1
  281. package/dist/web-png.d.ts +29 -0
  282. package/dist/web-png.js +321 -0
  283. package/dist/web-png.js.map +1 -0
  284. package/dist/web-preview.d.ts +2 -0
  285. package/dist/web-preview.js +2 -0
  286. package/dist/web-preview.js.map +1 -1
  287. package/dist/web-reactions.d.ts +34 -0
  288. package/dist/web-reactions.js +47 -0
  289. package/dist/web-reactions.js.map +1 -0
  290. package/dist/web-settings-patch.d.ts +2 -1
  291. package/dist/web-settings-patch.js +5 -1
  292. package/dist/web-settings-patch.js.map +1 -1
  293. package/dist/web-settings-tools.d.ts +1 -0
  294. package/dist/web-settings-tools.js +14 -10
  295. package/dist/web-settings-tools.js.map +1 -1
  296. package/dist/web-terminal.js +1 -1
  297. package/dist/web-theme-store.d.ts +2 -2
  298. package/dist/web-theme-store.js +6 -3
  299. package/dist/web-theme-store.js.map +1 -1
  300. package/package.json +1 -1
@@ -2,10 +2,12 @@
2
2
  // versions, the pictures and fonts it may name, and this device's off switch. Settings itself is always drawn stock
3
3
  // (owner-css.js removes the link here), so a stylesheet that hides things can always be fixed from this page.
4
4
  import { html, useEffect, useRef, useState } from "/assets/app-html.js";
5
- import { t } from "/assets/app-i18n.js";
5
+ import { register, t } from "/assets/app-i18n.js";
6
+ import { recordEdit } from "/assets/text-undo.js";
6
7
  import { appStore, useStore } from "/assets/app-store.js";
7
8
  import { ModsCard } from "./settings-mods.js";
8
- import { EffectsCard } from "./settings-effects.js";
9
+ import { experimentalComponent, useCanvasWorker } from "/assets/feature-flags.js";
10
+ const EffectsCard = experimentalComponent(() => import("./settings-effects.js"), m => m.EffectsCard);
9
11
  import { Icon } from "/assets/ui-icons.js";
10
12
  import { toast } from "/assets/ui-toast.js";
11
13
  import { ownerCssOffHere, ownerCssTrial, safeMode, setOwnerCssActive, setOwnerCssOffHere, startOwnerCssTrial } from "/assets/owner-css.js";
@@ -45,11 +47,30 @@ export function AppearanceSection() {
45
47
  const publicLink = typeof document !== "undefined" && document.body && document.body.dataset.publicLink === "1";
46
48
  if (publicLink) return html`<section class="card s-appearance"><h3>${tn("cssTitle")}</h3><p class="sub">${tn("cssPublicLink")}</p></section>`;
47
49
  const group = (key) => html`<h2 class="s-ap-group">${tn(key)}</h2>`;
48
- return html`${group("groupLook")}<${ThemesCard} /><${IdentityColorCard} /><${CardSizeCard} />
50
+ return html`${group("groupLook")}<${ThemesCard} /><${IdentityColorCard} /><${CardSizeCard} /><${AppIconCard} />
49
51
  ${group("groupLayout")}<${SidebarWidgetCard} />
50
52
  ${group("groupCss")}<${OwnerCssCard} /><${DeviceCard} />
51
53
  ${group("groupMods")}<${ModsCard} /><${EffectsCard} />
52
- ${group("groupAdvanced")}<${CardLimitsCard} /><${RestoreDefaultsCard} />`;
54
+ ${group("groupAdvanced")}<${ExperimentalEffectsCard} /><${CardLimitsCard} /><${RestoreDefaultsCard} />`;
55
+ }
56
+
57
+ register("experimentalEffects", {
58
+ en: { title: "Experimental: Canvas Worker effects", help: "Enable uploaded Canvas Worker effects. Ambient effects are always available. Saved effects stay intact when this is off.", failed: "Could not change this setting. Try again." },
59
+ "zh-TW": { title: "實驗功能:Canvas Worker 特效", help: "啟用上傳的 Canvas Worker 特效。氛圍特效不受影響;關閉時保留已儲存的特效。", failed: "無法變更此設定,請重試。" },
60
+ });
61
+ function ExperimentalEffectsCard() {
62
+ const enabled = useCanvasWorker(), [writable, setWritable] = useState(false), [busy, setBusy] = useState(false), [failed, setFailed] = useState(false);
63
+ useEffect(() => { let live = true; Promise.resolve(globalThis.AgendAuth?.trialSession?.()).then(value => { if (live) setWritable(value?.localAdmin === true); }, () => {}); return () => { live = false; }; }, []);
64
+ if (!writable) return null;
65
+ const change = async value => {
66
+ setBusy(true); setFailed(false);
67
+ try {
68
+ const result = await confirmedWrite("/api/settings/fleet/web", { method: "PUT", body: { experimental: { canvas_worker: value } }, label: t("experimentalEffects.title") });
69
+ if (result.ok) appStore.set({ canvasWorker: value }); else setFailed(true);
70
+ } catch { setFailed(true); }
71
+ finally { setBusy(false); }
72
+ };
73
+ return html`<section class="card s-appearance s-experimental-effects"><label class="check"><input type="checkbox" checked=${enabled} disabled=${busy} onChange=${event => change(event.target.checked)} /><span>${t("experimentalEffects.title")}</span></label><p class="sub">${t("experimentalEffects.help")}</p>${failed ? html`<p role="alert">${t("experimentalEffects.failed")}</p>` : null}</section>`;
53
74
  }
54
75
 
55
76
  /** #2056 B: a sample sidebar row in the host's default colour places, so the choice is seen rather than described. */
@@ -127,6 +148,131 @@ function CardSizeCard() {
127
148
  </section>`;
128
149
  }
129
150
 
151
+ /**
152
+ * #2308: the image as one square PNG the host makes the icon sizes from — centre-cropped, drawn at 512 px if smaller
153
+ * (marked low-resolution), as it is up to 1024 px, else 1024. The browser decodes whatever it can show (PNG, JPG, WebP);
154
+ * the host takes only that PNG.
155
+ */
156
+ export async function appIconPng(file) {
157
+ const bmp = await createImageBitmap(file);
158
+ try {
159
+ const side = Math.min(bmp.width, bmp.height);
160
+ const out = Math.min(1024, Math.max(512, side));
161
+ const canvas = document.createElement("canvas");
162
+ canvas.width = canvas.height = out;
163
+ const g = canvas.getContext("2d");
164
+ g.imageSmoothingQuality = "high";
165
+ g.drawImage(bmp, (bmp.width - side) / 2, (bmp.height - side) / 2, side, side, 0, 0, out, out);
166
+ const blob = await new Promise((resolve) => canvas.toBlob(resolve, "image/png"));
167
+ if (!blob) throw new Error("no PNG");
168
+ return { blob, low: side < 512 };
169
+ } finally {
170
+ if (bmp.close) bmp.close();
171
+ }
172
+ }
173
+
174
+ /** #2308: this page's icon links follow a change at once (the next pages carry the version from the host). */
175
+ export function refreshAppIconLinks(version, custom) {
176
+ if (typeof document === "undefined") return;
177
+ const v = encodeURIComponent(version);
178
+ const head = document.head;
179
+ const links = [...head.querySelectorAll("link")];
180
+ const byPath = (path) => links.find((l) => (l.getAttribute("href") || "").split("?")[0] === path);
181
+ for (const [path, rel] of [["/favicon.ico", "icon"], ["/apple-touch-icon.png", "apple-touch-icon"], ["/manifest.webmanifest", "manifest"]]) {
182
+ const el = byPath(path);
183
+ if (el && el.getAttribute("rel") === rel) el.setAttribute("href", `${path}?v=${v}`);
184
+ }
185
+ const svg = byPath("/favicon.svg");
186
+ if (custom && svg) svg.remove();
187
+ else if (!custom && !svg) {
188
+ const link = document.createElement("link");
189
+ for (const [k, val] of [["rel", "icon"], ["href", `/favicon.svg?v=${v}`], ["type", "image/svg+xml"], ["sizes", "any"]]) link.setAttribute(k, val);
190
+ head.appendChild(link);
191
+ }
192
+ }
193
+
194
+ /**
195
+ * #2308: the app icon — what a browser tab and a home screen show. One picture for every page (sign-in and the public
196
+ * link too); an admin changes it, everyone else sees it.
197
+ */
198
+ function AppIconCard() {
199
+ const [state, setState] = useState(null);
200
+ const [canWrite, setCanWrite] = useState(false);
201
+ const [picked, setPicked] = useState(null); // { blob, url, low }
202
+ const [busy, setBusy] = useState(false);
203
+ const [error, setError] = useState("");
204
+ const file = useRef(null);
205
+ const load = async () => {
206
+ const r = await call("/ui/app-icon");
207
+ setState(r.ok && r.body && typeof r.body.version === "string" ? r.body : { failed: true });
208
+ };
209
+ useEffect(() => {
210
+ load();
211
+ const auth = typeof window !== "undefined" ? window.AgendAuth : null;
212
+ if (auth && auth.canWrite) Promise.resolve(auth.canWrite()).then((v) => setCanWrite(v === true), () => {});
213
+ }, []);
214
+ useEffect(() => () => { if (picked) URL.revokeObjectURL(picked.url); }, [picked]);
215
+ const title = html`<${CardTitle} title=${tn("appIconTitle")} topic="app-icon" />`;
216
+ if (!state) return html`<section class="card s-appearance s-app-icon">${title}<p class="sub">…</p></section>`;
217
+ if (state.failed) return html`<section class="card s-appearance s-app-icon">${title}<p class="err">${tn("appIconLoadFailed")}</p></section>`;
218
+
219
+ const choose = async () => {
220
+ const f = file.current && file.current.files && file.current.files[0];
221
+ if (file.current) file.current.value = "";
222
+ if (!f) return;
223
+ setError("");
224
+ try {
225
+ const made = await appIconPng(f);
226
+ setPicked({ ...made, url: URL.createObjectURL(made.blob) });
227
+ } catch {
228
+ setError(tn("appIconNotAnImage"));
229
+ }
230
+ };
231
+ const answered = (r, done) => {
232
+ setBusy(false);
233
+ if (!r.ok) {
234
+ const known = ["app_icon_too_large", "app_icon_not_png", "app_icon_unreadable", "app_icon_admin_only", "app_icon_needs_a_signed_in_person", "app_icon_busy", "app_icon_superseded", "app_icon_not_reset"];
235
+ setError(known.includes(r.body && r.body.error) ? tn(`appIconError_${r.body.error}`, (r.body && r.body.detail) || "") : tn("appIconError_other", (r.body && r.body.error) || String(r.status)));
236
+ return;
237
+ }
238
+ setState(r.body);
239
+ setPicked(null);
240
+ refreshAppIconLinks(r.body.version, r.body.custom);
241
+ toast(done);
242
+ };
243
+ const use = async () => {
244
+ setBusy(true); setError("");
245
+ answered(await call("/ui/app-icon", { method: "PUT", headers: { "Content-Type": "image/png" }, body: picked.blob }), tn("appIconSaved"));
246
+ };
247
+ const reset = async () => {
248
+ setBusy(true); setError("");
249
+ answered(await call("/ui/app-icon", { method: "DELETE" }), tn("appIconResetDone"));
250
+ };
251
+ const v = encodeURIComponent(state.version);
252
+ return html`<section class="card s-appearance s-app-icon">${title}
253
+ <p class="sub">${tn("appIconLead")}</p>
254
+ <div class="s-app-icon-row">
255
+ <figure class="s-app-icon-now"><img class="s-app-icon-big" src=${`/apple-touch-icon.png?v=${v}`} width="96" height="96" alt=${tn("appIconCurrent")} />
256
+ <img class="s-app-icon-small" src=${`/icon-192.png?v=${v}`} width="32" height="32" alt="" />
257
+ <figcaption>${state.custom ? tn("appIconCustom") : tn("appIconDefault")}</figcaption></figure>
258
+ ${picked ? html`<figure class="s-app-icon-new"><img class="s-app-icon-big" src=${picked.url} width="96" height="96" alt=${tn("appIconPreview")} />
259
+ <figcaption>${tn("appIconPreview")}</figcaption></figure>` : null}
260
+ </div>
261
+ ${picked && picked.low ? html`<p class="sub s-app-icon-low">${tn("appIconLow")}</p>` : null}
262
+ ${error ? html`<p class="err" role="alert">${error}</p>` : null}
263
+ ${canWrite ? html`<div class="s-css-actions">
264
+ <input ref=${file} type="file" class="sr-only" id="s-app-icon-file" accept="image/png,image/jpeg,image/webp" onChange=${choose} />
265
+ ${picked ? html`
266
+ <button type="button" class="btn btn-primary" disabled=${busy} onClick=${use}><${Icon} name="check" size=${16} />${tn("appIconUse")}</button>
267
+ <button type="button" class="btn" disabled=${busy} onClick=${() => setPicked(null)}>${tn("appIconCancel")}</button>`
268
+ : html`<button type="button" class="btn" disabled=${busy} onClick=${() => file.current && file.current.click()}>${tn("appIconChoose")}</button>`}
269
+ ${state.custom && !picked ? html`<button type="button" class="btn" disabled=${busy} onClick=${reset}>${tn("appIconReset")}</button>` : null}
270
+ </div>` : html`<p class="sub">${tn("appIconReadOnly")}</p>`}
271
+ <p class="sub">${tn("appIconWho")}</p>
272
+ <p class="sub">${tn("appIconIos")}</p>
273
+ </section>`;
274
+ }
275
+
130
276
  const LIMIT_FIELDS = [["portrait", "max_width"], ["portrait", "max_height"], ["portrait", "max_kib"], ["banner", "max_width"], ["banner", "max_height"], ["banner", "max_kib"],
131
277
  ["instance_quota", "files"], ["instance_quota", "mib"]];
132
278
  /**
@@ -222,6 +368,7 @@ function SidebarWidgetCard() {
222
368
  const app = useStore(appStore);
223
369
  const saved = sidebarWidgetRaw();
224
370
  const [text, setText] = useState(() => (saved ? JSON.stringify(saved, null, 2) : ""));
371
+ const area = useRef(null);
225
372
  const [error, setError] = useState("");
226
373
  let parsed = null, problem = "";
227
374
  if (text.trim()) {
@@ -234,17 +381,17 @@ function SidebarWidgetCard() {
234
381
  setSidebarWidget(parsed);
235
382
  toast(tn("widgetSaved"));
236
383
  };
237
- const remove = () => { setSidebarWidget(null); setText(""); setError(""); toast(tn("widgetRemoved")); };
384
+ const remove = () => { setSidebarWidget(null); recordEdit(area.current); setText(""); setError(""); toast(tn("widgetRemoved")); };
238
385
  return html`<section class="card s-appearance s-synced s-widget"><${CardTitle} title=${tn("widgetTitle")} topic="widgets" />
239
386
  <p class="sub">${tn("widgetLead")}</p>
240
387
  <label class="field"><span class="sr-only">${tn("widgetTitle")}</span>
241
- <textarea class="s-widget-text" rows="10" spellcheck=${false} autocapitalize="off" autocomplete="off" value=${text}
388
+ <textarea ref=${area} class="s-widget-text" rows="10" spellcheck=${false} autocapitalize="off" autocomplete="off" value=${text}
242
389
  onInput=${(e) => { setText(e.target.value); setError(""); }}></textarea></label>
243
390
  ${error || problem ? html`<p class="err" role="alert">${error || problem}</p>` : null}
244
391
  ${preview ? html`<div class="s-widget-preview"><${AgentWidget} slot="sidebar" widget=${preview} binds=${fleetBinds(app.instances, app.exec, app.awaiting)} /></div>` : null}
245
392
  <div class="s-widget-actions">
246
393
  <button type="button" class="btn btn-primary" disabled=${!parsed || !!problem} onClick=${save}><${Icon} name="check" size=${16} />${tn("widgetSave")}</button>
247
- ${!text.trim() ? html`<button type="button" class="btn" onClick=${() => setText(JSON.stringify(SIDEBAR_WIDGET_EXAMPLE, null, 2))}>${tn("widgetExample")}</button>` : null}
394
+ ${!text.trim() ? html`<button type="button" class="btn" onClick=${() => { recordEdit(area.current); setText(JSON.stringify(SIDEBAR_WIDGET_EXAMPLE, null, 2)); }}>${tn("widgetExample")}</button>` : null}
248
395
  ${saved ? html`<button type="button" class="btn" onClick=${remove}>${tn("widgetRemove")}</button>` : null}
249
396
  </div>
250
397
  </section>`;
@@ -253,6 +400,7 @@ function SidebarWidgetCard() {
253
400
  function OwnerCssCard() {
254
401
  const [state, setState] = useState(null);
255
402
  const [text, setText] = useState("");
403
+ const area = useRef(null);
256
404
  const [busy, setBusy] = useState(false);
257
405
  const [error, setError] = useState("");
258
406
  const trial = useStore(ownerCssTrial);
@@ -261,6 +409,8 @@ function OwnerCssCard() {
261
409
  const r = await call("/ui/owner-css/state");
262
410
  if (!r.ok || !r.body) { setState({ failed: true }); return; }
263
411
  setState(r.body);
412
+ // #2342: a restored version replaces the editor's text as a step Ctrl+Z goes back from.
413
+ if (opts.undoable) recordEdit(area.current);
264
414
  if (opts.text !== false) setText(r.body.text || "");
265
415
  };
266
416
  useEffect(() => { load(); }, []);
@@ -284,7 +434,7 @@ function OwnerCssCard() {
284
434
  setBusy(false);
285
435
  if (!r.ok) { setError(refusal(r.body)); return; }
286
436
  setOwnerCssActive(r.body.active);
287
- load();
437
+ load({ undoable: true });
288
438
  };
289
439
  const addAsset = async () => {
290
440
  const f = file.current && file.current.files && file.current.files[0];
@@ -315,7 +465,7 @@ function OwnerCssCard() {
315
465
  <p class="sub">${tn("cssLead")}</p>
316
466
  <p class="s-css-status" role="status">${inForce}</p>
317
467
  <label class="field"><span class="sr-only">${tn("cssTitle")}</span>
318
- <textarea class="s-css-text" rows="14" spellcheck=${false} autocapitalize="off" autocomplete="off" value=${text} disabled=${busy}
468
+ <textarea ref=${area} class="s-css-text" rows="14" spellcheck=${false} autocapitalize="off" autocomplete="off" value=${text} disabled=${busy}
319
469
  onInput=${(e) => setText(e.target.value)}></textarea></label>
320
470
  ${error ? html`<p class="err" role="alert">${error}</p>` : null}
321
471
  <div class="s-css-actions">
@@ -4,6 +4,7 @@
4
4
  // provider key): each goes through confirmedWrite, so a change an admin must confirm waits for it (#1423).
5
5
  import { html, useEffect, useMemo, useRef, useState } from "/assets/app-html.js";
6
6
  import { t } from "/assets/app-i18n.js";
7
+ import { recordEdit } from "/assets/text-undo.js";
7
8
  import { useLease } from "/assets/app-ctx.js";
8
9
  import { Dialog } from "/assets/ui-dialog.js";
9
10
  import { Icon } from "/assets/ui-icons.js";
@@ -16,11 +17,13 @@ import { startOperation } from "./settings-apply.js";
16
17
  import "./settings-strings.js"; // its words, wherever a picker of this file is used (#2072: Fleet's create dialog too)
17
18
  import { backendChoices, modelChoices, OTHER_MODEL, useBackendRows, useModelCatalog } from "./settings-catalog.js";
18
19
  import {
19
- ACCESS_MODES, AGENT_FIELDS, AGENT_MODES, BOT_FIELDS, CH_TYPES, CLASSIC_FIELDS, HANG_TIMEOUT_DEFAULT_MINUTES, LOG_LEVELS, TOOL_PROGRESS, TOOL_SETS,
20
+ ACCESS_MODES, AGENT_MODES, CH_TYPES, HANG_TIMEOUT_DEFAULT_MINUTES, LOG_LEVELS, TOOL_PROGRESS, TOOL_SETS,
20
21
  VISIBILITY_MODES, accessConfirmations, agentForm, agentPatch, backendOptions, changedFields, channelId, channelsOf,
21
- chLabel, hasOwn, impactOf, nonNegative, nonNegativeInteger, positive, replyGuardSupported, sameValue, visibilityDefault,
22
+ chLabel, hasOwn, impactOf, newConnectionImpact, nonNegative, nonNegativeInteger, positive, replyGuardSupported, sameValue, visibilityDefault,
22
23
  classicWebEchoEffective, worstImpact,
23
24
  } from "./settings-model.js";
25
+ /** #2342: a field by id, for recordEdit before a pick or a reset writes it. */
26
+ const byId = (id) => (typeof document !== "undefined" ? document.getElementById(id) : null);
24
27
 
25
28
  const tn = (k, ...v) => t(`settings.${k}`, ...v);
26
29
  export const impactText = (kind) => tn(kind === "now" ? "impactNow" : kind === "fleet" ? "impactFleet" : "impactAgent");
@@ -103,7 +106,7 @@ export function ChipList({ items, onChange, placeholder, label }) {
103
106
 
104
107
  const replyHint = (backend, mode, kiroUi) => (replyGuardSupported(backend, mode, kiroUi) ? tn("replyGuardSupported") : tn("replyGuardUnsupported", backend, mode));
105
108
 
106
- /** The dialog every editor uses: Cancel / Stage change; the footer says what the whole dialog will cost. */
109
+ /** The dialog every editor uses: Cancel / Stage change; the footer says what the changes made so far will cost (#2346: only the fields that changed). */
107
110
  function EditDialog({ title, subtitle, onClose, onStage, impacts, schema, children, ready = true, wide = true }) {
108
111
  const kinds = [...new Set((impacts || []).map((f) => impactOf(schema, f)))];
109
112
  return html`<${Dialog} title=${title} label=${subtitle ? `${title} — ${subtitle}` : title} onClose=${onClose} wide=${wide}
@@ -184,9 +187,9 @@ export function StatusEmojiEditor({ own, platform, channel, previewBody, onChang
184
187
  <label for=${`se-${key}`}>${tn(`se_${key}`)}</label>
185
188
  <input id=${`se-${key}`} type="text" autocomplete="off" value=${values[key] || ""} placeholder=${builtins[key] || ""} onInput=${(e) => set(key, e.target.value)} />
186
189
  <button type="button" class="btn btn-sm" onClick=${() => setPicker(picker === key ? null : key)} aria-expanded=${picker === key ? "true" : "false"}>${tn("se_pick")}</button>
187
- <button type="button" class="icon-btn" aria-label=${tn("se_useDefault")} title=${tn("se_useDefault")} onClick=${() => set(key, "")}><${Icon} name="close" size=${14} /></button>
190
+ <button type="button" class="icon-btn" aria-label=${tn("se_useDefault")} title=${tn("se_useDefault")} onClick=${() => { recordEdit(byId(`se-${key}`)); set(key, ""); }}><${Icon} name="close" size=${14} /></button>
188
191
  ${picker === key ? html`<${EmojiPicker} cat=${cat} keyName=${key} platform=${platform} channel=${channel}
189
- onPick=${(v) => { set(key, v); setPicker(null); }} onClose=${() => setPicker(null)} />` : null}
192
+ onPick=${(v) => { recordEdit(byId(`se-${key}`)); set(key, v); setPicker(null); }} onClose=${() => setPicker(null)} />` : null}
190
193
  </div>`)}</div>
191
194
  <p class="note">${tn("se_hint")}</p>
192
195
  </div>`;
@@ -241,16 +244,17 @@ export function AgentDialog({ name, inst, ctx, onClose }) {
241
244
  };
242
245
  const failoverWarn = !f.inherit.model_failover && !f.model_failover.trim() ? "failoverEmpty" : "";
243
246
  const ready = !errors.auto && !errors.hang;
247
+ const pending = () => agentPatch({ ...f, status_emojis: emojis.value }, inst, defaults, visibilityBaseline, emojis.baseline);
244
248
  const stage = () => {
245
249
  if (!ready) return;
246
- const patch = agentPatch({ ...f, status_emojis: emojis.value }, inst, defaults, visibilityBaseline, emojis.baseline);
250
+ const patch = pending();
247
251
  if (Object.keys(patch).length) ctx.stageAgent(name, inst, patch);
248
252
  onClose();
249
253
  };
250
254
  // What an override shows while it inherits: the default.
251
255
  const shown = (k, own, dflt) => (f.inherit[k] ? dflt : own);
252
256
  const L = (text, field, id) => html`<${Label} text=${text} schema=${schema} field=${field} htmlFor=${id} />`;
253
- return html`<${EditDialog} title=${ctx.shortName(name, inst)} subtitle=${`ID: ${name}`} onClose=${onClose} onStage=${stage} ready=${ready} impacts=${AGENT_FIELDS} schema=${schema}>
257
+ return html`<${EditDialog} title=${ctx.shortName(name, inst)} subtitle=${`ID: ${name}`} onClose=${onClose} onStage=${stage} ready=${ready} impacts=${Object.keys(pending()).map((k) => `instance.${k}`)} schema=${schema}>
254
258
  <div class="grid2">
255
259
  <div class="field">${L(tn("displayName"), "instance.display_name", "ag-dn")}
256
260
  <${Override} inherit=${f.inherit.display_name} onInherit=${setInherit("display_name")}>
@@ -406,8 +410,8 @@ export function BotDialog({ id, ctx, onClose }) {
406
410
  setToken(""); setTokenId(null); setReplacing(false);
407
411
  setTokenNote({ text: tn("tokenQueued") });
408
412
  };
409
- const stage = () => {
410
- if (!ch) return;
413
+ /** The connections with this dialog's edits, and which parts changed. */
414
+ const pending = () => {
411
415
  const original = structuredClone(chs);
412
416
  const next = structuredClone(original);
413
417
  next[index] = { ...next[index], access: { ...(next[index].access || {}), mode, allowed_users: [...users] } };
@@ -418,6 +422,16 @@ export function BotDialog({ id, ctx, onClose }) {
418
422
  if (emojis.value) options.status_emojis = emojis.value; else delete options.status_emojis;
419
423
  next[index] = { ...next[index], options };
420
424
  }
425
+ return { next, accessChanged, emojisChanged };
426
+ };
427
+ const pendingImpacts = () => {
428
+ if (!ch) return [];
429
+ const p = pending();
430
+ return [...(p.accessChanged ? ["fleet.channels"] : []), ...(p.emojisChanged ? ["fleet.channel.options.status_emojis"] : [])];
431
+ };
432
+ const stage = () => {
433
+ if (!ch) return;
434
+ const { next, accessChanged, emojisChanged } = pending();
421
435
  if (accessChanged || emojisChanged) {
422
436
  ctx.stageChannels(`connections:${ch.id || chLabel(index)}`, next, {
423
437
  label: tn(accessChanged && emojisChanged ? "updateAccessEmojis" : accessChanged ? "updateAccess" : "updateEmojis", ch.id || chLabel(index)),
@@ -445,7 +459,7 @@ export function BotDialog({ id, ctx, onClose }) {
445
459
  };
446
460
  const L = (text, field, htmlFor) => html`<${Label} text=${text} schema=${schema} field=${field} htmlFor=${htmlFor} />`;
447
461
  if (!ch) return null;
448
- return html`<${EditDialog} title=${type === "telegram" ? "Telegram" : "Discord"} subtitle=${id} onClose=${onClose} onStage=${stage} impacts=${BOT_FIELDS} schema=${schema}>
462
+ return html`<${EditDialog} title=${type === "telegram" ? "Telegram" : "Discord"} subtitle=${id} onClose=${onClose} onStage=${stage} impacts=${pendingImpacts()} schema=${schema}>
449
463
  <div class="grid2">
450
464
  <div class="field">${L(tn("accessMode"), "fleet.channel.access.mode", "bot-mode")}<${Select} id="bot-mode" value=${mode} onChange=${setMode} options=${ACCESS_MODES} />
451
465
  <${Feedback} warning=${accessWarn} /></div>
@@ -503,6 +517,7 @@ export function NewBotDialog({ ctx, onClose }) {
503
517
  // The form's revision (#1529 review): any change to the token, platform, id or group moves it; a Verify or a plan
504
518
  // that began before is dropped when it lands — never restoring an old token, naming it, or planning for it.
505
519
  const rev = useRef(0);
520
+ // The fields keep what was typed (#2342: trimming the box would rewrite it, losing its undo); trimmed here.
506
521
  const body = (x) => ({ platform: x.type, connection_only: true, admin_user_id: x.admin.trim(), ...(x.id.trim() ? { channel_id: x.id.trim() } : {}),
507
522
  ...(x.group.trim() ? (x.type === "discord" ? { guild_id: x.group.trim() } : { group_id: x.group.trim() }) : {}),
508
523
  ...(x.type === "discord" && x.general.trim() ? { general_channel_id: x.general.trim() } : {}) });
@@ -538,7 +553,7 @@ export function NewBotDialog({ ctx, onClose }) {
538
553
  setBusy(true);
539
554
  const p = await planFor(x);
540
555
  if (!p) { if (lease.current()) setBusy(false); return; }
541
- const handed = startOperation([{ label: tn("addBotLabel", p.channel_id), impact: impactOf(ctx.schema, "fleet.channels"),
556
+ const handed = startOperation([{ label: tn("addBotLabel", p.channel_id), impact: newConnectionImpact(ctx.schema),
542
557
  request: { method: "POST", url: "/api/settings/quickstart/commit",
543
558
  body: { ...body(x), channel_id: p.channel_id, token_env: p.token_env, token_env_generated: true, token: x.token }, sensitive: true } }]);
544
559
  if (!handed) { setErr(tn("applyBusyLocal")); setBusy(false); return; }
@@ -560,9 +575,9 @@ export function NewBotDialog({ ctx, onClose }) {
560
575
  invite=${f.identity && f.identity.invite} portal=${f.identity && f.identity.portal} guilds=${f.guilds} onGuilds=${(g) => setF((x) => ({ ...x, guilds: g }))}
561
576
  guild=${f.group} onGuild=${set("group")} channel=${f.general} onChannel=${set("general")} />`
562
577
  : html`<div class="field"><label for="nb-group">${tn("groupIdField")}</label>
563
- <input id="nb-group" type="text" value=${f.group} onInput=${(e) => set("group")(e.target.value.trim())} /></div>`}
578
+ <input id="nb-group" type="text" value=${f.group} onInput=${(e) => set("group")(e.target.value)} /></div>`}
564
579
  <div class="field"><label for="nb-user">${tn("wizardAdminUser")}</label>
565
- <input id="nb-user" type="text" required=${true} inputmode="numeric" pattern=${adminUserIdPattern(f.type)} value=${f.admin} onInput=${(e) => set("admin")(e.target.value.trim())} /></div>
580
+ <input id="nb-user" type="text" required=${true} inputmode="numeric" pattern=${adminUserIdPattern(f.type)} value=${f.admin} onInput=${(e) => set("admin")(e.target.value)} /></div>
566
581
  <${Drawer} title=${tn("advancedSection")}>
567
582
  <div class="field"><label for="nb-id">${tn("connectionIdOptional")}</label><input id="nb-id" type="text" placeholder=${plan ? plan.channel_id : f.type}
568
583
  value=${f.id} onInput=${(e) => set("id")(e.target.value)} onBlur=${() => { if (f.identity && f.identity.valid) planFor(f); }} /></div>
@@ -597,9 +612,7 @@ export function ClassicDialog({ room, ctx, onClose }) {
597
612
  const runtime = ctx.classicRuntime(c.instanceName);
598
613
  const classicMode = fd.agent_mode ?? "mcp";
599
614
  const lease = useLease("classic-dialog");
600
- const stage = async () => {
601
- if (!ready) return;
602
- const patch = changedFields({
615
+ const pending = () => changedFields({
603
616
  backend: f.backend, model: f.model.trim() || null,
604
617
  auto_pause_after: f.inherit.auto_pause_after ? null : Number(f.auto_pause_after),
605
618
  tool_progress: f.inherit.tool_progress ? null : f.tool_progress,
@@ -612,13 +625,16 @@ export function ClassicDialog({ room, ctx, onClose }) {
612
625
  reply_completion_guard: hasOwn(c, "reply_completion_guard") ? c.reply_completion_guard : null,
613
626
  collab: !!c.collab, context_lines: c.context_lines ?? cd.context_lines ?? 5, web_echo: classicWebEchoEffective(c, fleet),
614
627
  });
628
+ const stage = async () => {
629
+ if (!ready) return;
630
+ const patch = pending();
615
631
  if (!Object.keys(patch).length) { onClose(); return; }
616
632
  if (!lease.current()) return;
617
633
  ctx.stageClassic(c, patch);
618
634
  onClose();
619
635
  };
620
636
  const L = (text, field, id) => html`<${Label} text=${text} schema=${schema} field=${field} htmlFor=${id} />`;
621
- return html`<${EditDialog} title=${c.name || c.instanceName} subtitle=${`ID: ${c.instanceName} · ${tn(`exec_${runtime.execution}`)}`} onClose=${onClose} onStage=${stage} ready=${ready} impacts=${CLASSIC_FIELDS} schema=${schema}>
637
+ return html`<${EditDialog} title=${c.name || c.instanceName} subtitle=${`ID: ${c.instanceName} · ${tn(`exec_${runtime.execution}`)}`} onClose=${onClose} onStage=${stage} ready=${ready} impacts=${Object.keys(pending()).map((k) => `classic.${k}`)} schema=${schema}>
622
638
  <div class="grid2">
623
639
  <div class="field">${L(tn("backend"), "classic.backend", "cl-be")}<${BackendSelect} id="cl-be" value=${f.backend} onChange=${set("backend")} /></div>
624
640
  <div class="field">${L(tn("model"), "classic.model", "cl-model")}<${ModelSelect} id="cl-model" value=${f.model} onChange=${set("model")}
@@ -9,6 +9,7 @@
9
9
  // B, nor after A again (an older A), nor once the page is gone.
10
10
  import { html, useEffect, useRef, useState } from "/assets/app-html.js";
11
11
  import { t } from "/assets/app-i18n.js";
12
+ import { useTypedText } from "/assets/text-undo.js";
12
13
  import { api } from "./settings-confirm.js";
13
14
  import { Select } from "./settings-dialogs.js";
14
15
 
@@ -23,6 +24,9 @@ const probe = (body) => api("/api/settings/quickstart/probe", { method: "POST",
23
24
  */
24
25
  export function DiscordServerPicker({ idPrefix, token, bot, invite, portal, guilds, onGuilds, guild, onGuild, channel, onChannel }) {
25
26
  const [polling, setPolling] = useState(null); // { timedOut } while waiting for the bot to join
27
+ // #2342: the typed IDs keep what was typed (a space, the undo history); the picks get them trimmed.
28
+ const [guildText, onGuildText] = useTypedText(guild);
29
+ const [channelText, onChannelText] = useTypedText(channel);
26
30
  const [channels, setChannels] = useState(null); // null: not asked; { list } | { error }
27
31
  const live = useRef(true);
28
32
  const now = useRef({ token, guild, channel });
@@ -120,11 +124,11 @@ export function DiscordServerPicker({ idPrefix, token, bot, invite, portal, guil
120
124
  <div class="field"><label for=${`${idPrefix}-guild`}>${tn("guildIdField")}</label>
121
125
  ${guilds && guilds.length
122
126
  ? html`<${Select} id=${`${idPrefix}-guild`} value=${guild} onChange=${(v) => pickGuild(v, true)} options=${["", ...guilds.map((g) => ({ value: g.id, label: `${g.name} (${g.id})` }))]} />`
123
- : html`<input id=${`${idPrefix}-guild`} type="text" value=${guild} placeholder=${tn("discordNoServerYet")} onInput=${(e) => pickGuild(e.target.value.trim(), true)} />`}</div>
127
+ : html`<input id=${`${idPrefix}-guild`} type="text" value=${guildText} placeholder=${tn("discordNoServerYet")} onInput=${(e) => pickGuild(onGuildText(e.target.value), true)} />`}</div>
124
128
  <div class="field"><label for=${`${idPrefix}-gen`}>${tn("wizardGeneralChannel")}</label>
125
129
  ${channels && channels.list && channels.list.length
126
130
  ? html`<${Select} id=${`${idPrefix}-gen`} value=${channel} onChange=${pickChannel} options=${["", ...channels.list.map((c) => ({ value: c.id, label: `#${c.name}` }))]} />`
127
- : html`<input id=${`${idPrefix}-gen`} type="text" value=${channel} onInput=${(e) => pickChannel(e.target.value.trim())} />`}
131
+ : html`<input id=${`${idPrefix}-gen`} type="text" value=${channelText} onInput=${(e) => pickChannel(onChannelText(e.target.value))} />`}
128
132
  ${channels && channels.error ? html`<p class="feedback warning">${channels.error}</p>` : null}</div>
129
133
  </div>`;
130
134
  }
@@ -8,6 +8,7 @@ import { t } from "/assets/app-i18n.js";
8
8
  import { toast } from "/assets/ui-toast.js";
9
9
  import { confirmDialog } from "/assets/ui-confirm.js";
10
10
  import { api } from "./settings-confirm.js";
11
+ import { DEFAULT_SCHEMA, impactOf } from "./settings-model.js";
11
12
  import "./settings-strings.js";
12
13
 
13
14
  const tn = (k, ...v) => t(`settings.${k}`, ...v);
@@ -59,7 +60,7 @@ export function GuestsCard({ ctx, staged, disabled = false }) {
59
60
  const setList = (key, k, list) => setF((x) => { const m = { ...x[key] }; if (list === undefined) delete m[k]; else m[k] = list; return { ...x, [key]: m }; });
60
61
  const review = () => {
61
62
  const body = guestsBody(original, f);
62
- if (body) ctx.stage("guests", { label: tn("guestsTitle"), impact: "now", request: { method: "PUT", url: "/api/settings/fleet/web", body: { guests: body } } });
63
+ if (body) ctx.stage("guests", { label: tn("guestsTitle"), impact: impactOf(ctx.schema || DEFAULT_SCHEMA, "web.guests"), request: { method: "PUT", url: "/api/settings/fleet/web", body: { guests: body } } });
63
64
  else if (staged.has("guests")) ctx.unstage("guests");
64
65
  setMsg(tn("readyApply"));
65
66
  };
@@ -13,6 +13,7 @@ export const LEARN_ANCHORS = {
13
13
  colors: ["identity-colours", "識別色"], css: ["custom-css-settings--web-appearance", "自訂-css設定--網頁外觀"], themes: ["agents-themes", "agent-的主題"],
14
14
  cards: ["identity-cards", "身分卡"], limits: ["card-upload-limits", "身分卡上傳限制"], widgets: ["widgets", "小工具"], mods: ["mods", "mod"],
15
15
  effects: ["effects", "效果"], restore: ["restore-defaults", "還原預設值"],
16
+ "app-icon": ["app-icon", "app-圖示"],
16
17
  };
17
18
  export function learnHref(topic, language = lang()) {
18
19
  const zh = language === "zh-TW";
@@ -56,6 +56,8 @@ export const chLabel = (i) => (i === 0 ? "primary" : "persona");
56
56
  // ── Impacts: the server's (/api/settings/schema, derived from HOT_INSTANCE_CONFIG_KEYS); this page never decides one ──
57
57
  export const DEFAULT_SCHEMA = { impacts: {}, order: ["now", "instance", "fleet"] };
58
58
  export const impactOf = (schema, field) => (schema.impacts && schema.impacts[field]) || "instance";
59
+ /** #2346: a new connection starts at once while another one runs (the schema says so); the first one needs AgEnD restarted. */
60
+ export const newConnectionImpact = (schema) => (schema && schema.connections_start_live === true ? "now" : impactOf(schema || DEFAULT_SCHEMA, "fleet.channels"));
59
61
  /** A batch costs as much as its worst member, in the order the server ships. */
60
62
  export function batchImpact(schema, keys, impactFor) {
61
63
  const order = schema.order;
@@ -197,17 +199,6 @@ export function agentForm(inst, defaults) {
197
199
  };
198
200
  }
199
201
 
200
- /** Every field the agent dialog shows, for its footer's impact summary. */
201
- export const AGENT_FIELDS = [
202
- "instance.display_name", "instance.description", "instance.backend", "instance.model", "instance.working_directory",
203
- "instance.auto_pause_after", "instance.topic_id", "instance.hang_detector", "instance.agent_mode", "instance.tool_set",
204
- "instance.tool_progress", "instance.cross_instance_visibility", "instance.reply_completion_guard", "instance.log_level",
205
- "instance.lightweight", "instance.model_failover", "instance.systemPrompt", "instance.tags", "instance.general_topic",
206
- "instance.status_emojis",
207
- ];
208
- export const CLASSIC_FIELDS = ["classic.backend", "classic.model", "classic.auto_pause_after", "classic.tool_progress",
209
- "classic.reply_completion_guard", "classic.collab", "classic.context_lines", "classic.web_echo"];
210
- export const BOT_FIELDS = ["fleet.channel.access.mode", "fleet.channel.access.allowed_users", "fleet.channels", "fleet.channel.options.status_emojis"];
211
202
 
212
203
  /** Validation messages (keys of the settings namespace), or "" when the value is fine. */
213
204
  export const nonNegative = (v) => (!Number.isFinite(Number(v)) || String(v).trim() === "" || Number(v) < 0 ? "mustNonNegative" : "");
@@ -4,6 +4,7 @@
4
4
  // parts the person ticks are applied, all or nothing. Custom CSS from a file is never ticked by default. Installed
5
5
  // mods can be downloaded again or uninstalled (what they applied stays: Reset and the CSS controls work on it).
6
6
  // Export builds a file from what is in force now.
7
+ import { canvasWorkerEnabled, useCanvasWorker } from "/assets/feature-flags.js";
7
8
  import { html, useEffect, useLayoutEffect, useRef, useState } from "/assets/app-html.js";
8
9
  import { t } from "/assets/app-i18n.js";
9
10
  import { appStore, useStore } from "/assets/app-store.js";
@@ -45,7 +46,7 @@ export function defaultChoice(plan) {
45
46
  for (const it of plan.items) {
46
47
  if (it.part === "theme" || it.part === "identity" || it.part === "ambient") c[it.part] = true;
47
48
  else if (it.part === "widget") c.widgets[it.slot] = true;
48
- else if (it.part === "effect") c.effects = true;
49
+ else if (it.part === "effect" && canvasWorkerEnabled()) c.effects = true;
49
50
  // ownerCss stays false: "read it before turning it on" (r4 §4).
50
51
  }
51
52
  return c;
@@ -80,7 +81,7 @@ export function ThemeDetail({ theme, pictures, effects }) {
80
81
  ${th.background ? pic(th.background.image, tn("modBackground")) : null}
81
82
  ${th.background ? row(tn("modBackgroundFit"), `${th.background.fit} · ${Math.round(th.background.opacity * 100)}%`) : null}
82
83
  ${th.avatar ? pic(th.avatar, tn("modAvatar")) : null}
83
- ${(effects || []).map((e, i) => html`<div key=${i}>${row(tn("modPlacesEffect"), html`<span><code>${e.effect}</code>${e.id ? html` <span class="muted">${e.id.slice(0, 8)}</span>` : null}${Object.keys(e.params || {}).length ? html` · <code>${JSON.stringify(e.params)}</code>` : null}</span>`)}</div>`)}
84
+ ${(canvasWorkerEnabled() ? effects || [] : []).map((e, i) => html`<div key=${i}>${row(tn("modPlacesEffect"), html`<span><code>${e.effect}</code>${e.id ? html` <span class="muted">${e.id.slice(0, 8)}</span>` : null}${Object.keys(e.params || {}).length ? html` · <code>${JSON.stringify(e.params)}</code>` : null}</span>`)}</div>`)}
84
85
  <details class="s-mod-raw"><summary>${tn("modAllFields")}</summary><pre class="s-mod-css-text">${JSON.stringify(th, null, 2)}</pre></details>
85
86
  </div>`;
86
87
  }
@@ -114,6 +115,7 @@ function PlanItem({ it, choice, setChoice, binds, fleet }) {
114
115
  <details class="s-mod-raw"><summary>${tn("modAllFields")}</summary><pre class="s-mod-css-text">${JSON.stringify({ loop: it.loop, click: it.click }, null, 2)}</pre></details></div></li>`;
115
116
  }
116
117
  if (it.part === "effect") {
118
+ if (!canvasWorkerEnabled()) return null;
117
119
  return html`<li class="s-mod-item">${tick(tn("modPartEffect", it.title), choice.effects, (v) => setChoice({ ...choice, effects: v }))}
118
120
  <div class="s-mod-detail"><span>${kib(it.bytes)} · ${tn("modRegions", it.regions.join(", "))}</span>${it.wants.length ? html`<span>${tn("modWants", it.wants.join(", "))}</span>` : null}</div></li>`;
119
121
  }
@@ -137,7 +139,7 @@ function Plan({ staged, onDone }) {
137
139
  const names = (app.instances || []).map((i) => i.name);
138
140
  const install = async () => {
139
141
  setBusy(true); setError("");
140
- const r = await postJson("/ui/mods/commit", { stage: staged.stage, plan_hash: staged.plan_hash, include: choice, ...(toInstance ? { target } : {}) });
142
+ const r = await postJson("/ui/mods/commit", { stage: staged.stage, plan_hash: staged.plan_hash, include: { ...choice, effects: canvasWorkerEnabled() && choice.effects }, ...(toInstance ? { target } : {}) });
141
143
  setBusy(false);
142
144
  // The stage this plan shows is gone or no longer this plan (replaced on another device, expired): read again —
143
145
  // the page then shows whatever is staged now, with its own, fresh choices (keyed by stage).
@@ -205,6 +207,7 @@ function ExportForm() {
205
207
  }
206
208
 
207
209
  export function ModsCard() {
210
+ useCanvasWorker();
208
211
  const [state, setState] = useState(null);
209
212
  const [busy, setBusy] = useState(false);
210
213
  const [error, setError] = useState("");