@songsid/agend 2.3.0-beta.22 → 2.3.0-beta.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (298) hide show
  1. package/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 +1 -0
  6. package/dist/agent-endpoint.js +4 -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 +54 -21
  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 +3 -1
  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 +11 -10
  31. package/dist/bug-report-cli.js.map +1 -1
  32. package/dist/bug-report.js +11 -10
  33. package/dist/bug-report.js.map +1 -1
  34. package/dist/bundled-skills.d.ts +7 -3
  35. package/dist/bundled-skills.js +16 -7
  36. package/dist/bundled-skills.js.map +1 -1
  37. package/dist/channel/adapters/discord.js +3 -3
  38. package/dist/channel/adapters/discord.js.map +1 -1
  39. package/dist/channel/mcp-server.js +12 -4
  40. package/dist/channel/mcp-server.js.map +1 -1
  41. package/dist/channel/mcp-tools.d.ts +3 -1
  42. package/dist/channel/mcp-tools.js +36 -0
  43. package/dist/channel/mcp-tools.js.map +1 -1
  44. package/dist/child-process.d.ts +21 -0
  45. package/dist/child-process.js +54 -0
  46. package/dist/child-process.js.map +1 -0
  47. package/dist/cli.js +188 -204
  48. package/dist/cli.js.map +1 -1
  49. package/dist/command-table.d.ts +1 -1
  50. package/dist/command-table.js +1 -1
  51. package/dist/command-table.js.map +1 -1
  52. package/dist/config-validator.js +20 -1
  53. package/dist/config-validator.js.map +1 -1
  54. package/dist/config.js +3 -1
  55. package/dist/config.js.map +1 -1
  56. package/dist/daemon.d.ts +8 -0
  57. package/dist/daemon.js +31 -8
  58. package/dist/daemon.js.map +1 -1
  59. package/dist/darwin-memory.js +1 -1
  60. package/dist/doctor.d.ts +5 -0
  61. package/dist/doctor.js +42 -0
  62. package/dist/doctor.js.map +1 -1
  63. package/dist/export-import.js +4 -3
  64. package/dist/export-import.js.map +1 -1
  65. package/dist/fleet-level-config.d.ts +1 -1
  66. package/dist/fleet-level-config.js +2 -0
  67. package/dist/fleet-level-config.js.map +1 -1
  68. package/dist/fleet-manager.d.ts +45 -0
  69. package/dist/fleet-manager.js +324 -36
  70. package/dist/fleet-manager.js.map +1 -1
  71. package/dist/full-restart.d.ts +5 -5
  72. package/dist/full-restart.js +5 -5
  73. package/dist/full-restart.js.map +1 -1
  74. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +48 -10
  75. package/dist/general-knowledge/skills/fleet-config/SKILL.md +1 -1
  76. package/dist/general-knowledge/skills/fleet-restart/SKILL.md +7 -8
  77. package/dist/general-knowledge/skills/multi-channel/SKILL.md +1 -1
  78. package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +319 -0
  79. package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +529 -0
  80. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +17 -4
  81. package/dist/general-knowledge/steering/core-rules.md +1 -1
  82. package/dist/instance-config-impact.js +10 -3
  83. package/dist/instance-config-impact.js.map +1 -1
  84. package/dist/instance-lifecycle.js +4 -4
  85. package/dist/locale.js +296 -4
  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-schemas.d.ts +3 -3
  101. package/dist/public-link-progress.d.ts +12 -1
  102. package/dist/public-link-progress.js +5 -10
  103. package/dist/public-link-progress.js.map +1 -1
  104. package/dist/public-tunnel-config.d.ts +5 -0
  105. package/dist/public-tunnel-config.js +45 -1
  106. package/dist/public-tunnel-config.js.map +1 -1
  107. package/dist/public-web-link.js +6 -5
  108. package/dist/public-web-link.js.map +1 -1
  109. package/dist/quickstart.js +15 -9
  110. package/dist/quickstart.js.map +1 -1
  111. package/dist/resource-report.js +1 -1
  112. package/dist/scheduler/task-board.d.ts +41 -1
  113. package/dist/scheduler/task-board.js +152 -5
  114. package/dist/scheduler/task-board.js.map +1 -1
  115. package/dist/sd-notify.js +2 -1
  116. package/dist/sd-notify.js.map +1 -1
  117. package/dist/service-installer.d.ts +69 -0
  118. package/dist/service-installer.js +182 -0
  119. package/dist/service-installer.js.map +1 -1
  120. package/dist/service-owner.d.ts +44 -0
  121. package/dist/service-owner.js +48 -0
  122. package/dist/service-owner.js.map +1 -0
  123. package/dist/service-plan.d.ts +8 -0
  124. package/dist/service-plan.js +13 -0
  125. package/dist/service-plan.js.map +1 -1
  126. package/dist/settings-api.d.ts +4 -0
  127. package/dist/settings-api.js +21 -3
  128. package/dist/settings-api.js.map +1 -1
  129. package/dist/settings-change.js +88 -4
  130. package/dist/settings-change.js.map +1 -1
  131. package/dist/settings-confirmation.d.ts +2 -0
  132. package/dist/settings-confirmation.js +14 -8
  133. package/dist/settings-confirmation.js.map +1 -1
  134. package/dist/settings-control.js +2 -1
  135. package/dist/settings-control.js.map +1 -1
  136. package/dist/settings-effect.js +2 -0
  137. package/dist/settings-effect.js.map +1 -1
  138. package/dist/settings-http-confirmation.d.ts +2 -0
  139. package/dist/settings-http-confirmation.js +3 -2
  140. package/dist/settings-http-confirmation.js.map +1 -1
  141. package/dist/settings-request-capability.d.ts +4 -1
  142. package/dist/settings-request-capability.js +11 -3
  143. package/dist/settings-request-capability.js.map +1 -1
  144. package/dist/setup-auth.d.ts +1 -0
  145. package/dist/setup-auth.js +2 -1
  146. package/dist/setup-auth.js.map +1 -1
  147. package/dist/setup-host.d.ts +1 -0
  148. package/dist/setup-host.js +7 -3
  149. package/dist/setup-host.js.map +1 -1
  150. package/dist/setup-wizard.js +1 -1
  151. package/dist/setup-wizard.js.map +1 -1
  152. package/dist/task-board-summary.d.ts +101 -0
  153. package/dist/task-board-summary.js +177 -0
  154. package/dist/task-board-summary.js.map +1 -0
  155. package/dist/task-board-waker.d.ts +8 -1
  156. package/dist/task-board-waker.js +15 -6
  157. package/dist/task-board-waker.js.map +1 -1
  158. package/dist/tmux-control.js +1 -1
  159. package/dist/tmux-control.js.map +1 -1
  160. package/dist/tmux-exec-helper.d.ts +13 -3
  161. package/dist/tmux-exec-helper.js +34 -29
  162. package/dist/tmux-exec-helper.js.map +1 -1
  163. package/dist/tmux-manager.js +1 -1
  164. package/dist/tmux-read.js +1 -1
  165. package/dist/tmux-read.js.map +1 -1
  166. package/dist/topic-commands.d.ts +3 -0
  167. package/dist/topic-commands.js +29 -18
  168. package/dist/topic-commands.js.map +1 -1
  169. package/dist/transcript-monitor.js +5 -0
  170. package/dist/transcript-monitor.js.map +1 -1
  171. package/dist/transcript-sources.d.ts +2 -0
  172. package/dist/transcript-sources.js +4 -0
  173. package/dist/transcript-sources.js.map +1 -1
  174. package/dist/tunnel/cloudflared-install.js +3 -1
  175. package/dist/tunnel/cloudflared-install.js.map +1 -1
  176. package/dist/tunnel/cloudflared.d.ts +1 -1
  177. package/dist/tunnel/cloudflared.js +51 -10
  178. package/dist/tunnel/cloudflared.js.map +1 -1
  179. package/dist/tunnel/named-diagnostics.d.ts +13 -0
  180. package/dist/tunnel/named-diagnostics.js +52 -0
  181. package/dist/tunnel/named-diagnostics.js.map +1 -0
  182. package/dist/tunnel/types.d.ts +2 -1
  183. package/dist/tunnel/types.js.map +1 -1
  184. package/dist/types.d.ts +9 -0
  185. package/dist/ui/app-effects.js +10 -4
  186. package/dist/ui/chat-strings.js +3 -0
  187. package/dist/ui/chat-thread.js +14 -7
  188. package/dist/ui/effect-trial-offer.js +2 -31
  189. package/dist/ui/effects-waker.js +2 -70
  190. package/dist/ui/fleet-task-card.js +5 -5
  191. package/dist/ui/fleet-task-common.js +43 -9
  192. package/dist/ui/fleet-task-work.js +107 -0
  193. package/dist/ui/fleet-tasks.js +46 -12
  194. package/dist/ui/image-lightbox.js +39 -1
  195. package/dist/ui/image-zoom.js +242 -0
  196. package/dist/ui/panel-chat.js +24 -13
  197. package/dist/ui/panel-details.js +2 -0
  198. package/dist/ui/panel-fleet.js +6 -2
  199. package/dist/ui/panel-needs.js +21 -6
  200. package/dist/ui/panel-settings.js +35 -13
  201. package/dist/ui/preview-panel.js +2 -1
  202. package/dist/ui/search-dialog.js +2 -1
  203. package/dist/ui/settings-appearance.js +44 -10
  204. package/dist/ui/settings-dialogs.js +33 -17
  205. package/dist/ui/settings-discord.js +6 -2
  206. package/dist/ui/settings-guests.js +2 -1
  207. package/dist/ui/settings-model.js +2 -11
  208. package/dist/ui/settings-mods.js +8 -3
  209. package/dist/ui/settings-strings.js +32 -0
  210. package/dist/ui/settings-token.js +3 -1
  211. package/dist/ui/settings-web-authority.js +6 -3
  212. package/dist/ui/settings-wizard.js +13 -7
  213. package/dist/ui/shared/app-i18n.js +2 -2
  214. package/dist/ui/shared/app-needs.js +2 -2
  215. package/dist/ui/shared/app-store.js +2 -2
  216. package/dist/ui/shared/app.css +93 -32
  217. package/dist/ui/shared/app.js +5 -1
  218. package/dist/ui/shared/effect-app-runtime.js +7 -5
  219. package/dist/ui/shared/effect-chat-runtime.js +2 -7
  220. package/dist/ui/shared/effect-cursor.js +3 -2
  221. package/dist/ui/shared/effect-display-metadata.js +39 -0
  222. package/dist/ui/shared/effect-registry.js +4 -2
  223. package/dist/ui/shared/effect-runtime-loader.js +3 -1
  224. package/dist/ui/shared/effect-stock-trust.js +5 -5
  225. package/dist/ui/shared/feature-flags.js +127 -0
  226. package/dist/ui/shared/identity-color.js +6 -3
  227. package/dist/ui/shared/instance-nav.js +3 -2
  228. package/dist/ui/shared/panel-view.js +14 -29
  229. package/dist/ui/shared/text-undo.js +210 -0
  230. package/dist/ui/shared/ui-dialog.js +4 -3
  231. package/dist/ui/shared/ui-icons.js +2 -0
  232. package/dist/ui/shared/usage-dialog.js +29 -8
  233. package/dist/ui/shared/view-strings.js +2 -4
  234. package/dist/ui/web-terminal/terminal.html +10 -10
  235. package/dist/ui/web-terminal/terminal.js +58 -12
  236. package/dist/update-dispatch.js +1 -1
  237. package/dist/update-launch.d.ts +1 -1
  238. package/dist/update-launch.js +1 -2
  239. package/dist/update-launch.js.map +1 -1
  240. package/dist/update-preflight.js +1 -1
  241. package/dist/usage/providers.js +1 -1
  242. package/dist/usage/providers.js.map +1 -1
  243. package/dist/view-api.js +1 -1
  244. package/dist/web-api.d.ts +6 -0
  245. package/dist/web-api.js +163 -21
  246. package/dist/web-api.js.map +1 -1
  247. package/dist/web-app-icon-worker.js +2 -1
  248. package/dist/web-app-icon-worker.js.map +1 -1
  249. package/dist/web-app-icon.d.ts +10 -8
  250. package/dist/web-app-icon.js +14 -13
  251. package/dist/web-app-icon.js.map +1 -1
  252. package/dist/web-appearance-tools.d.ts +1 -0
  253. package/dist/web-appearance-tools.js +7 -3
  254. package/dist/web-appearance-tools.js.map +1 -1
  255. package/dist/web-appearance.d.ts +4 -10
  256. package/dist/web-appearance.js +4 -3
  257. package/dist/web-appearance.js.map +1 -1
  258. package/dist/web-auth.d.ts +8 -4
  259. package/dist/web-auth.js +28 -17
  260. package/dist/web-auth.js.map +1 -1
  261. package/dist/web-chat-history.d.ts +14 -0
  262. package/dist/web-chat-history.js +26 -1
  263. package/dist/web-chat-history.js.map +1 -1
  264. package/dist/web-cookie-names.d.ts +5 -0
  265. package/dist/web-cookie-names.js +12 -0
  266. package/dist/web-cookie-names.js.map +1 -0
  267. package/dist/web-experimental-http.d.ts +2 -0
  268. package/dist/web-experimental-http.js +12 -0
  269. package/dist/web-experimental-http.js.map +1 -0
  270. package/dist/web-experimental.d.ts +11 -0
  271. package/dist/web-experimental.js +49 -0
  272. package/dist/web-experimental.js.map +1 -0
  273. package/dist/web-guest-access.d.ts +1 -0
  274. package/dist/web-guest-access.js +4 -2
  275. package/dist/web-guest-access.js.map +1 -1
  276. package/dist/web-mod-store.d.ts +2 -0
  277. package/dist/web-mod-store.js +10 -0
  278. package/dist/web-mod-store.js.map +1 -1
  279. package/dist/web-png.d.ts +4 -0
  280. package/dist/web-png.js +10 -2
  281. package/dist/web-png.js.map +1 -1
  282. package/dist/web-preview.d.ts +2 -0
  283. package/dist/web-preview.js +2 -0
  284. package/dist/web-preview.js.map +1 -1
  285. package/dist/web-returning-device.d.ts +4 -2
  286. package/dist/web-returning-device.js +12 -5
  287. package/dist/web-returning-device.js.map +1 -1
  288. package/dist/web-settings-patch.d.ts +2 -1
  289. package/dist/web-settings-patch.js +5 -1
  290. package/dist/web-settings-patch.js.map +1 -1
  291. package/dist/web-settings-tools.d.ts +1 -0
  292. package/dist/web-settings-tools.js +14 -10
  293. package/dist/web-settings-tools.js.map +1 -1
  294. package/dist/web-terminal.js +1 -1
  295. package/dist/web-theme-store.d.ts +2 -2
  296. package/dist/web-theme-store.js +6 -3
  297. package/dist/web-theme-store.js.map +1 -1
  298. package/package.json +11 -11
@@ -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=${tn("idLine", 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=${`${tn("idLine", 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
  };
@@ -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";
@@ -36,6 +37,8 @@ function download(text, name) {
36
37
  function why(body) {
37
38
  if (!body) return tn("modFailed", "?");
38
39
  if (Array.isArray(body.problems) && body.problems.length) return body.problems.slice(0, 5).map((p) => `${p.path || "file"}: ${p.problem}`).join("\n");
40
+ // #2390: a refusal the page has words for, in its language (the host's detail is English).
41
+ if (body.error === "canvas_worker_experimental") return tn("modCanvasWorkerOff");
39
42
  return tn("modFailed", body.detail ? `${body.error}: ${body.detail}` : body.error || "?");
40
43
  }
41
44
 
@@ -45,7 +48,7 @@ export function defaultChoice(plan) {
45
48
  for (const it of plan.items) {
46
49
  if (it.part === "theme" || it.part === "identity" || it.part === "ambient") c[it.part] = true;
47
50
  else if (it.part === "widget") c.widgets[it.slot] = true;
48
- else if (it.part === "effect") c.effects = true;
51
+ else if (it.part === "effect" && canvasWorkerEnabled()) c.effects = true;
49
52
  // ownerCss stays false: "read it before turning it on" (r4 §4).
50
53
  }
51
54
  return c;
@@ -80,7 +83,7 @@ export function ThemeDetail({ theme, pictures, effects }) {
80
83
  ${th.background ? pic(th.background.image, tn("modBackground")) : null}
81
84
  ${th.background ? row(tn("modBackgroundFit"), `${th.background.fit} · ${Math.round(th.background.opacity * 100)}%`) : null}
82
85
  ${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>`)}
86
+ ${(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
87
  <details class="s-mod-raw"><summary>${tn("modAllFields")}</summary><pre class="s-mod-css-text">${JSON.stringify(th, null, 2)}</pre></details>
85
88
  </div>`;
86
89
  }
@@ -114,6 +117,7 @@ function PlanItem({ it, choice, setChoice, binds, fleet }) {
114
117
  <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
118
  }
116
119
  if (it.part === "effect") {
120
+ if (!canvasWorkerEnabled()) return null;
117
121
  return html`<li class="s-mod-item">${tick(tn("modPartEffect", it.title), choice.effects, (v) => setChoice({ ...choice, effects: v }))}
118
122
  <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
123
  }
@@ -137,7 +141,7 @@ function Plan({ staged, onDone }) {
137
141
  const names = (app.instances || []).map((i) => i.name);
138
142
  const install = async () => {
139
143
  setBusy(true); setError("");
140
- const r = await postJson("/ui/mods/commit", { stage: staged.stage, plan_hash: staged.plan_hash, include: choice, ...(toInstance ? { target } : {}) });
144
+ const r = await postJson("/ui/mods/commit", { stage: staged.stage, plan_hash: staged.plan_hash, include: { ...choice, effects: canvasWorkerEnabled() && choice.effects }, ...(toInstance ? { target } : {}) });
141
145
  setBusy(false);
142
146
  // The stage this plan shows is gone or no longer this plan (replaced on another device, expired): read again —
143
147
  // the page then shows whatever is staged now, with its own, fresh choices (keyed by stage).
@@ -205,6 +209,7 @@ function ExportForm() {
205
209
  }
206
210
 
207
211
  export function ModsCard() {
212
+ useCanvasWorker();
208
213
  const [state, setState] = useState(null);
209
214
  const [busy, setBusy] = useState(false);
210
215
  const [error, setError] = useState("");
@@ -21,6 +21,7 @@ register("settings", {
21
21
  namedTunnelSetup: "How to set this up",
22
22
  namedToken: "Cloudflare connector token",
23
23
  namedTokenHint: "Stored privately, never shown again. Format checked locally; Cloudflare credentials and routing are checked when opening. Saving or clearing closes the current public link.",
24
+ namedTokenInvalid: "Paste only the long string that starts with eyJ.",
24
25
  namedTokenSet: "Set token",
25
26
  namedTokenClear: "Clear token",
26
27
  awaitingConfirmation: "Waiting for confirmation.",
@@ -375,6 +376,8 @@ register("settings", {
375
376
  modCancel: "Cancel",
376
377
  modInstalled: "Installed {0}.",
377
378
  modFailed: "Not done: {0}",
379
+ idLine: "ID: {0}",
380
+ modCanvasWorkerOff: "Not done: this mod has Canvas Worker effects, an experimental feature. Turn it on in Settings → Web appearance first.",
378
381
  modDownload: "Download",
379
382
  modUninstall: "Uninstall",
380
383
  modUninstallConfirm: "Uninstall {0}? Its effects leave your library unless something still uses them. What it applied stays.",
@@ -418,6 +421,19 @@ register("settings", {
418
421
  appIconError_app_icon_superseded: "The icon was reset meanwhile; choose the picture again.",
419
422
  appIconError_app_icon_not_reset: "The default could not be restored ({0}).",
420
423
  appIconError_other: "The app icon was not changed ({0}).",
424
+ appIconError_app_icon_not_kept: "This fleet does not keep an app icon.",
425
+ appIconError_app_icon_failed: "The app icon request failed; try again.",
426
+ appIconDetail_max_size: "at most {0} MiB",
427
+ appIconDetail_png_only: "a PNG image; the page converts JPG and WebP",
428
+ appIconDetail_busy: "another icon is being made",
429
+ appIconDetail_reset_meanwhile: "the icon was reset meanwhile",
430
+ appIconDetail_too_slow: "it took too long to read",
431
+ appIconDetail_unreadable: "the image could not be read",
432
+ appIconDetail_corrupt: "the PNG file could not be read ({0})",
433
+ appIconDetail_not_square: "it is not square ({0}×{1})",
434
+ appIconDetail_too_small: "it is smaller than {0}×{0}",
435
+ appIconDetail_too_big: "it is larger than {0}×{0}",
436
+ appIconDetail_interlaced: "interlaced PNGs are not supported",
421
437
  cssTitle: "Custom CSS (yours only)",
422
438
  cssLead: "Applies to your screens only.",
423
439
  cssLoadFailed: "Couldn't load your custom CSS. Reload the page to try again.",
@@ -621,6 +637,7 @@ register("settings", {
621
637
  namedTunnelSetup: "設定教學",
622
638
  namedToken: "Cloudflare 連接器 token",
623
639
  namedTokenHint: "私密儲存,不會讀回。儲存時只檢查格式,開啟時才確認憑證與路由。設定或清除會關閉目前的公開連結。",
640
+ namedTokenInvalid: "請只貼上以 eyJ 開頭的長字串。",
624
641
  namedTokenSet: "設定 token",
625
642
  namedTokenClear: "清除 token",
626
643
  awaitingConfirmation: "等待確認中。",
@@ -949,6 +966,8 @@ register("settings", {
949
966
  modCancel: "取消",
950
967
  modInstalled: "已安裝 {0}。",
951
968
  modFailed: "未完成:{0}",
969
+ idLine: "ID:{0}",
970
+ modCanvasWorkerOff: "未完成:這個 mod 含有 Canvas Worker 效果,這是實驗性功能。請先到「設定 → 網頁外觀」開啟。",
952
971
  modDownload: "下載",
953
972
  modUninstall: "解除安裝",
954
973
  modUninstallConfirm: "要解除安裝 {0} 嗎?它的效果會離開你的效果庫,除非還有東西在使用。它已套用的外觀會保留。",
@@ -992,6 +1011,19 @@ register("settings", {
992
1011
  appIconError_app_icon_superseded: "圖示剛被恢復成預設,請重新選擇圖片。",
993
1012
  appIconError_app_icon_not_reset: "無法恢復預設({0})。",
994
1013
  appIconError_other: "App 圖示沒有更換({0})。",
1014
+ appIconError_app_icon_not_kept: "這個 fleet 沒有保存 App 圖示。",
1015
+ appIconError_app_icon_failed: "App 圖示的請求失敗,請再試一次。",
1016
+ appIconDetail_max_size: "最多 {0} MiB",
1017
+ appIconDetail_png_only: "需要 PNG 圖片;頁面會自動轉換 JPG 與 WebP",
1018
+ appIconDetail_busy: "另一張圖示正在處理中",
1019
+ appIconDetail_reset_meanwhile: "圖示剛被恢復成預設",
1020
+ appIconDetail_too_slow: "讀取時間過長",
1021
+ appIconDetail_unreadable: "無法讀取這張圖片",
1022
+ appIconDetail_corrupt: "無法解讀這個 PNG 檔案({0})",
1023
+ appIconDetail_not_square: "不是正方形({0}×{1})",
1024
+ appIconDetail_too_small: "小於 {0}×{0}",
1025
+ appIconDetail_too_big: "大於 {0}×{0}",
1026
+ appIconDetail_interlaced: "不支援交錯式(interlaced)PNG",
995
1027
  cssTitle: "自訂 CSS(只套用在你自己)",
996
1028
  cssLead: "只套用在你的畫面。",
997
1029
  cssLoadFailed: "無法載入你的自訂 CSS。請重新整理頁面再試一次。",
@@ -5,6 +5,7 @@
5
5
  // hands it back — afterwards a connection shows "Token set · @bot".
6
6
  import { html, useState } from "/assets/app-html.js";
7
7
  import { t } from "/assets/app-i18n.js";
8
+ import { useTypedText } from "/assets/text-undo.js";
8
9
  import { Icon } from "/assets/ui-icons.js";
9
10
  import { api } from "./settings-confirm.js";
10
11
 
@@ -36,10 +37,11 @@ export const botHandle = (username) => (username ? `@${String(username).replace(
36
37
  */
37
38
  export function TokenField({ id, platform, value, onInput, identity, busy, onVerify, hint }) {
38
39
  const [shown, setShown] = useState(false);
40
+ const [text, onText] = useTypedText(value); // #2342: the box keeps what was typed; the parent gets it trimmed
39
41
  return html`<div class="field token-field"><label for=${id}>${tn("wizardToken")}</label>
40
42
  <div class="token-row">
41
43
  <input id=${id} type=${shown ? "text" : "password"} autocomplete="off" autocapitalize="off" spellcheck=${false} disabled=${!!busy}
42
- placeholder=${platform === "discord" ? "MTA…" : "123456:ABC-DEF…"} value=${value} onInput=${(e) => onInput(e.target.value.trim())} />
44
+ placeholder=${platform === "discord" ? "MTA…" : "123456:ABC-DEF…"} value=${text} onInput=${(e) => onInput(onText(e.target.value))} />
43
45
  <button type="button" class="icon-btn" aria-pressed=${shown ? "true" : "false"} title=${shown ? tn("tokenHide") : tn("tokenShow")}
44
46
  aria-label=${shown ? tn("tokenHide") : tn("tokenShow")} onClick=${() => setShown(!shown)}><${Icon} name="eye" size=${16} /></button>
45
47
  </div>
@@ -5,10 +5,12 @@
5
5
  // them grants anything by itself — a session still needs its account's Confirm in Discord or Telegram).
6
6
  import { html, useEffect, useState } from "/assets/app-html.js";
7
7
  import { t } from "/assets/app-i18n.js";
8
+ import { useTypedText } from "/assets/text-undo.js";
8
9
  import { toast } from "/assets/ui-toast.js";
9
10
  import { confirmDialog } from "/assets/ui-confirm.js";
10
11
  import { api } from "./settings-confirm.js";
11
12
  import { Feedback } from "./settings-dialogs.js";
13
+ import { DEFAULT_SCHEMA, impactOf } from "./settings-model.js";
12
14
  import { resetWebAuthorization } from "/assets/web-authorize.js";
13
15
  import "./settings-strings.js";
14
16
 
@@ -26,6 +28,8 @@ export function verifiedAuthorityBody(original, f) {
26
28
  export function WebAuthorityCard({ ctx, staged, disabled = false }) {
27
29
  const [overview, setOverview] = useState(null), [failed, setFailed] = useState(false);
28
30
  const [original, setOriginal] = useState(null), [f, setF] = useState(null), [msg, setMsg] = useState(null);
31
+ // #2342: the hours box keeps what was typed ("01", "1.50"; its undo history); the form gets the number.
32
+ const [hoursText, onHoursText] = useTypedText(f ? f.hours : null, (v) => (v.trim() === "" ? null : Number(v)));
29
33
  const load = async () => {
30
34
  // What changes here (a revoke, the switch) changes this browser's own line: the chat reads it again when it shows.
31
35
  resetWebAuthorization();
@@ -46,7 +50,6 @@ export function WebAuthorityCard({ ctx, staged, disabled = false }) {
46
50
  if (!overview || !f) return card(html`<p class="note">${t("app.loading")}</p>`);
47
51
 
48
52
  const set = (patch) => setF((x) => ({ ...x, ...patch }));
49
- const hoursText = f.hours === null ? "" : String(f.hours);
50
53
  const hoursError = f.hours !== null && (!Number.isFinite(f.hours) || f.hours < overview.hours.min || f.hours > overview.hours.max) ? "vaHoursRange" : "";
51
54
  const stuckOff = f.enabled && original.enabled && !overview.state.enabled;
52
55
  const warnHosts = !f.loopbackOnly && overview.lanHosts.length > 0;
@@ -55,7 +58,7 @@ export function WebAuthorityCard({ ctx, staged, disabled = false }) {
55
58
  const review = () => {
56
59
  if (hoursError) return;
57
60
  const body = verifiedAuthorityBody(original, f);
58
- if (body) ctx.stage("verified_authority", { label: tn("vaTitle"), impact: "now", request: { method: "PUT", url: "/api/settings/fleet/web", body: { verified_authority: body } } });
61
+ if (body) ctx.stage("verified_authority", { label: tn("vaTitle"), impact: impactOf(ctx.schema || DEFAULT_SCHEMA, "web.verified_authority"), request: { method: "PUT", url: "/api/settings/fleet/web", body: { verified_authority: body } } });
59
62
  else if (staged.has("verified_authority")) ctx.unstage("verified_authority");
60
63
  setMsg(tn("readyApply"));
61
64
  };
@@ -77,7 +80,7 @@ export function WebAuthorityCard({ ctx, staged, disabled = false }) {
77
80
  ${warnHosts ? html`<p class="note s-va-warn" role="status" id="vaLanWarning">${tn("vaLanWarning", overview.lanHosts.join(", "))}</p>` : null}
78
81
  <div class="field"><label for="vaHours">${tn("vaHours", overview.hours.min, overview.hours.max)}</label>
79
82
  <input id="vaHours" type="number" min=${overview.hours.min} max=${overview.hours.max} step="any" placeholder=${tn("defaultHours", overview.hours.default)}
80
- class=${hoursError ? "invalid" : ""} value=${hoursText} onInput=${(e) => set({ hours: e.target.value.trim() === "" ? null : Number(e.target.value) })} />
83
+ class=${hoursError ? "invalid" : ""} value=${hoursText} onInput=${(e) => set({ hours: onHoursText(e.target.value) })} />
81
84
  <${Feedback} error=${hoursError} /></div>
82
85
  <h4>${tn("vaSessionsTitle")}</h4>
83
86
  ${overview.authorized.length
@@ -4,11 +4,12 @@
4
4
  // progress, deadline and restart recovery as every other change.
5
5
  import { html, useEffect, useRef, useState } from "/assets/app-html.js";
6
6
  import { t } from "/assets/app-i18n.js";
7
+ import { useTypedText } 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 { api } from "./settings-confirm.js";
10
11
  import { startOperation } from "./settings-apply.js";
11
- import { BACKENDS, DEFAULT_SCHEMA, impactOf, toYaml } from "./settings-model.js";
12
+ import { BACKENDS, newConnectionImpact, toYaml } from "./settings-model.js";
12
13
  import { Drawer, Select } from "./settings-dialogs.js";
13
14
  import { adminUserIdError, adminUserIdPattern, botHandle, TokenEnvNote, TokenField, verifyBotToken } from "./settings-token.js";
14
15
  import { DiscordServerPicker } from "./settings-discord.js";
@@ -26,6 +27,11 @@ export function SetupWizard({ ctx, onClose }) {
26
27
  const bump = () => { rev.current++; };
27
28
  const [busy, setBusy] = useState("");
28
29
  const [err, setErr] = useState("");
30
+ // #2342: the typed fields keep what was typed (a space in a path, the undo history); the form gets them trimmed.
31
+ const [dirText, onDirText] = useTypedText(w ? w.working_directory : "");
32
+ const [nameText, onNameText] = useTypedText(w ? w.instance_name : "");
33
+ const [userText, onUserText] = useTypedText(w ? w.admin_user_id : "");
34
+ const [groupText, onGroupText] = useTypedText(w ? w.group_id : "");
29
35
  useEffect(() => {
30
36
  (async () => {
31
37
  const env = await api("/api/settings/quickstart/environment").catch(() => null);
@@ -115,7 +121,7 @@ export function SetupWizard({ ctx, onClose }) {
115
121
  // either if it was taken since, instead of quietly picking another.
116
122
  const body = { ...input(), channel_id: w.plan.channel_id, token_env: w.plan.token_env, token_env_generated: true, token: w.token };
117
123
  // It writes the connection and the first agent: it costs what a connection change does (the server's schema).
118
- const handed = startOperation([{ label: tn("wizardTitle"), impact: impactOf((ctx && ctx.schema) || DEFAULT_SCHEMA, "fleet.channels"),
124
+ const handed = startOperation([{ label: tn("wizardTitle"), impact: newConnectionImpact(ctx && ctx.schema),
119
125
  request: { method: "POST", url: "/api/settings/quickstart/commit", body, sensitive: true } }]);
120
126
  if (!handed) { setErr(tn("applyBusyLocal")); return; }
121
127
  setW((x) => ({ ...x, token: "" }));
@@ -134,8 +140,8 @@ export function SetupWizard({ ctx, onClose }) {
134
140
  <p class="note">${tn("wizardAgentExistingNote")}</p></div>` : html`${which}<div class="field"><label for="wz-be">${tn("wizardBackend")}</label>
135
141
  <${Select} id="wz-be" value=${w.backend} onChange=${set("backend")} options=${w.env.backends && w.env.backends.length ? w.env.backends : BACKENDS} />
136
142
  <p class="note">${w.env.backends && w.env.backends.length ? tn("wizardBackendFound", w.env.backends.join(", ")) : tn("wizardBackendNone")}</p></div>
137
- <div class="field"><label for="wz-wd">${tn("workingDir")}</label><input id="wz-wd" type="text" placeholder="/home/you/projects/app" value=${w.working_directory} onInput=${(e) => set("working_directory")(e.target.value.trim())} /></div>
138
- <div class="field"><label for="wz-name">${agents.length ? tn("wizardAgentNewName") : tn("wizardAgentName")}</label><input id="wz-name" type="text" value=${w.instance_name} onInput=${(e) => set("instance_name")(e.target.value.trim())} /></div>`;
143
+ <div class="field"><label for="wz-wd">${tn("workingDir")}</label><input id="wz-wd" type="text" placeholder="/home/you/projects/app" value=${dirText} onInput=${(e) => set("working_directory")(onDirText(e.target.value))} /></div>
144
+ <div class="field"><label for="wz-name">${agents.length ? tn("wizardAgentNewName") : tn("wizardAgentName")}</label><input id="wz-name" type="text" value=${nameText} onInput=${(e) => set("instance_name")(onNameText(e.target.value))} /></div>`;
139
145
  } else if (w.step === 2) {
140
146
  body = html`<div class="seg-inline" role="group" aria-label=${tn("wizardPlatform")}>
141
147
  ${["telegram", "discord"].map((p) => html`<button key=${p} type="button" class=${`btn${w.platform === p ? " btn-primary" : ""}`} aria-pressed=${w.platform === p ? "true" : "false"}
@@ -148,12 +154,12 @@ export function SetupWizard({ ctx, onClose }) {
148
154
  <${DiscordServerPicker} idPrefix="wz" token=${w.identity && w.identity.valid ? w.token : ""} bot=${w.identity && botHandle(w.identity.username)}
149
155
  invite=${w.identity && w.identity.invite} portal=${w.identity && w.identity.portal} guilds=${w.guilds} onGuilds=${(g) => setW((x) => ({ ...x, guilds: g }))}
150
156
  guild=${w.guild_id} onGuild=${set("guild_id")} channel=${w.general_channel_id} onChannel=${set("general_channel_id")} />
151
- <div class="field"><label for="wz-user">${tn("wizardAdminUser")}</label><input id="wz-user" type="text" required=${true} inputmode="numeric" pattern=${adminUserIdPattern(w.platform)} placeholder=${tn("discordUserPlaceholder")} value=${w.admin_user_id} onInput=${(e) => set("admin_user_id")(e.target.value.trim())} /></div>`
157
+ <div class="field"><label for="wz-user">${tn("wizardAdminUser")}</label><input id="wz-user" type="text" required=${true} inputmode="numeric" pattern=${adminUserIdPattern(w.platform)} placeholder=${tn("discordUserPlaceholder")} value=${userText} onInput=${(e) => set("admin_user_id")(onUserText(e.target.value))} /></div>`
152
158
  : html`
153
- <div class="field"><label for="wz-group">${tn("groupIdField")}</label><input id="wz-group" type="text" placeholder="-1001234567890" value=${w.group_id} onInput=${(e) => set("group_id")(e.target.value.trim())} />
159
+ <div class="field"><label for="wz-group">${tn("groupIdField")}</label><input id="wz-group" type="text" placeholder="-1001234567890" value=${groupText} onInput=${(e) => set("group_id")(onGroupText(e.target.value))} />
154
160
  <p class="note">${tn("wizardDetectHint")}</p></div>
155
161
  <div class="dlg-inline-actions"><button type="button" class="btn btn-sm" disabled=${!!busy} onClick=${detect}>${tn("wizardDetect")}</button></div>
156
- <div class="field"><label for="wz-user">${tn("wizardAdminUser")}</label><input id="wz-user" type="text" required=${true} inputmode="numeric" pattern=${adminUserIdPattern(w.platform)} placeholder=${tn("telegramUserPlaceholder")} value=${w.admin_user_id} onInput=${(e) => set("admin_user_id")(e.target.value.trim())} /></div>`}`;
162
+ <div class="field"><label for="wz-user">${tn("wizardAdminUser")}</label><input id="wz-user" type="text" required=${true} inputmode="numeric" pattern=${adminUserIdPattern(w.platform)} placeholder=${tn("telegramUserPlaceholder")} value=${userText} onInput=${(e) => set("admin_user_id")(onUserText(e.target.value))} /></div>`}`;
157
163
  } else {
158
164
  body = !w.plan ? html`<p class="note">${tn("wizardPlanLoading")}</p>` : html`
159
165
  <p><strong>${tn("wizardWillWrite")}</strong></p>
@@ -94,7 +94,7 @@ register("app", {
94
94
  search: "Search", searchTitle: "Search chats, messages between agents and tasks ({0})",
95
95
  confirmTitle: "Please confirm", confirmOk: "Continue", cancel: "Cancel",
96
96
  // The server's own wording (src/locale.ts needs.reason.*), so the web says what Discord says.
97
- needs_reason_hang: "Not responding", needs_reason_exited: "Exited", needs_reason_assist: "Waiting at its terminal", needs_reason_permission: "Permission needed", needs_reason_dangerous_command: "Dangerous command waiting", needs_reason_login: "Sign-in needed", needs_reason_dialog: "Waiting at a dialog", needs_reason_terminal_input: "May be waiting for input", needs_reason_auth_paused: "Paused: sign-in needed", needs_reason_crashed: "Crashed", needs_reason_delivery_uncertain: "Could not confirm delivery", needs_reason_delivery_failed: "Delivery failed",
97
+ needs_reason_hang: "Not responding", needs_reason_exited: "Exited", needs_reason_assist: "Waiting at its terminal", needs_reason_permission: "Permission needed", needs_reason_dangerous_command: "Dangerous command waiting", needs_reason_login: "Sign-in needed", needs_reason_dialog: "Waiting at a dialog", needs_reason_terminal_input: "May be waiting for input", needs_reason_auth_paused: "Paused: sign-in needed", needs_reason_crashed: "Crashed", needs_reason_delivery_uncertain: "Could not confirm delivery", needs_reason_delivery_failed: "Delivery failed", needs_reason_task_review: "Waiting for your confirmation", needs_reason_task_blocked: "Blocked: asks you",
98
98
  },
99
99
  "zh-TW": {
100
100
  brand: "AgEnD", fleet: "Fleet", fleetOverview: "Fleet 概覽", view: "檢視", settings: "設定", instances: "Instances", newInstance: "新增 instance", newAgent: "新增 Agent",
@@ -129,6 +129,6 @@ register("app", {
129
129
  needsNav: "等你處理", needsTab: "待處理", needsCount: "{0} 件待處理",
130
130
  search: "搜尋", searchTitle: "搜尋聊天、agent 之間的訊息與任務({0})",
131
131
  confirmTitle: "請確認", confirmOk: "繼續", cancel: "取消",
132
- needs_reason_hang: "沒有回應", needs_reason_exited: "已結束", needs_reason_assist: "在終端機上等待", needs_reason_permission: "需要權限", needs_reason_dangerous_command: "危險指令待確認", needs_reason_login: "需要登入", needs_reason_dialog: "停在對話框", needs_reason_terminal_input: "可能在等輸入", needs_reason_auth_paused: "已暫停:需要登入", needs_reason_crashed: "已崩潰", needs_reason_delivery_uncertain: "無法確認是否送達", needs_reason_delivery_failed: "傳送失敗",
132
+ needs_reason_hang: "沒有回應", needs_reason_exited: "已結束", needs_reason_assist: "在終端機上等待", needs_reason_permission: "需要權限", needs_reason_dangerous_command: "危險指令待確認", needs_reason_login: "需要登入", needs_reason_dialog: "停在對話框", needs_reason_terminal_input: "可能在等輸入", needs_reason_auth_paused: "已暫停:需要登入", needs_reason_crashed: "已崩潰", needs_reason_delivery_uncertain: "無法確認是否送達", needs_reason_delivery_failed: "傳送失敗", needs_reason_task_review: "等你確認", needs_reason_task_blocked: "卡住,在問你",
133
133
  },
134
134
  });
@@ -77,8 +77,8 @@ export function startNeedsNotifier({ open, env = globalThis } = {}) {
77
77
  seen.add(item.id);
78
78
  if (!notifyOn(env)) return;
79
79
  try {
80
- const n = new env.Notification(item.instance, { body: item.detail ? `${reasonText(item)}: ${item.detail}` : reasonText(item), tag: item.id });
81
- n.onclick = () => { try { env.focus && env.focus(); } catch { /* ignore */ } if (open) open(item.instance); n.close(); };
80
+ const n = new env.Notification(item.instance || reasonText(item), { body: item.detail ? `${reasonText(item)}: ${item.detail}` : reasonText(item), tag: item.id });
81
+ n.onclick = () => { try { env.focus && env.focus(); } catch { /* ignore */ } if (open && item.instance) open(item.instance); n.close(); };
82
82
  } catch { /* a browser that refuses at the last moment: the list still shows it */ }
83
83
  };
84
84
  const consider = (items) => {
@@ -24,7 +24,7 @@ export function createStore(initial) {
24
24
  * (app-stream.js catchUp) — "catching" / "retrying" / "failed" / "ok"; `retryHydration` asks for it again.
25
25
  * `connections`: how many chat connections the fleet has (#1519 P7: 0 is web-only — the first-run card); null until known. */
26
26
  export const appStore = createStore({ ready: false, instances: [], uptime: 0, exec: {}, awaiting: {}, connection: "connecting", needs: [], connections: null,
27
- hydration: "none", retryHydration: null });
27
+ hydration: "none", retryHydration: null, canvasWorker: typeof document !== "undefined" && document.body?.dataset.canvasWorker === "1" });
28
28
 
29
29
  /**
30
30
  * #1920 phase 3: this page is a guest's — the server says so on <body> (data-tier="guest"). A guest sees only its own
@@ -49,7 +49,7 @@ export function applyStatus(d) {
49
49
  exec[i.name] = (i.execution_state !== undefined ? i.execution_state : i.state) ?? null;
50
50
  if (i.state === "awaiting_input") awaiting[i.name] = typeof i.interaction_summary === "string" ? i.interaction_summary : "";
51
51
  }
52
- appStore.set({ ready: true, instances: d.instances, uptime: d.uptime ?? 0, exec, awaiting,
52
+ appStore.set({ canvasWorker: d.canvas_worker === true, ready: true, instances: d.instances, uptime: d.uptime ?? 0, exec, awaiting,
53
53
  connections: typeof d.connections === "number" ? d.connections : null,
54
54
  // #1665: the host preferences' revision (app-prefs.js re-reads them when it moves).
55
55
  prefsRev: typeof d.prefs_rev === "number" ? d.prefs_rev : null,