@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
@@ -21,6 +21,7 @@ import { Menu } from "/assets/ui-menu.js";
21
21
  import { Dialog } from "/assets/ui-dialog.js";
22
22
  import { Empty, Skeleton } from "/assets/ui-states.js";
23
23
  import { Icon } from "/assets/ui-icons.js";
24
+ import { recordEdit } from "/assets/text-undo.js";
24
25
  import { toast } from "/assets/ui-toast.js";
25
26
  import { confirmDialog } from "/assets/ui-confirm.js";
26
27
  import { savePref } from "/assets/app-prefs.js";
@@ -74,11 +75,14 @@ import { PreviewPanel, claimPanel, openPanel, panelClaims, panelStore, panelVers
74
75
  import { needsArgument, paletteFor, parseCommandLine } from "./chat-commands.js";
75
76
  import { installTour, refreshTourSpot } from "./chat-tour.js";
76
77
  import { LightboxHost, openLightbox } from "./image-lightbox.js";
77
- import { ChatEffectPosters } from "/ui/js/chat-effect-posters.js";
78
- import { ChatEffects, chatEffectsMenuAvailable } from "./chat-effects.js";
78
+ import { canvasWorkerMenuAvailable, experimentalComponent, useCanvasWorker } from "/assets/feature-flags.js";
79
+ const ChatEffectPosters = experimentalComponent(() => import("./chat-effect-posters.js"), m => m.ChatEffectPosters);
80
+ const ChatEffects = experimentalComponent(() => import("./chat-effects.js"), m => m.ChatEffects);
79
81
  import { setFollowsReducedMotion } from "/assets/fx-motion.js";
80
82
  import { AmbientMotionNote, ChatAmbient, ambientMenuItems } from "./chat-ambient.js";
81
- import { startEffectsWaker, getAppEffects, wakeEffects, effectsLoaded, onEffectsAwake, chatHasEffects } from "./effects-waker.js";
83
+ import { startEffectsWaker, getAppEffects, wakeEffects, effectsLoaded, onEffectsAwake, chatHasEffects } from "/assets/feature-flags.js";
84
+ /** The composer's textarea (#2342: text put back into it is a step Ctrl+Z goes back from). */
85
+ const composerField = () => (typeof document !== "undefined" ? document.getElementById("msgIn") : null);
82
86
 
83
87
  const R = () => globalThis.AgendChatRender;
84
88
  const P = () => globalThis.AgendPreview;
@@ -297,7 +301,8 @@ function ChatView({ name, inst, lease, navKey, exec, awaiting }) {
297
301
  else toast(t(verb === "start" ? "chat.started" : verb === "stop" ? "chat.stoppedInst" : "chat.restarted", name));
298
302
  };
299
303
  // #2193: this chat's Canvas Worker placements (its status row's effect_chat_v): none, no effects code runs for it.
300
- const fxHere = chatHasEffects(name);
304
+ const cwEnabled = useCanvasWorker();
305
+ const fxHere = cwEnabled && chatHasEffects(name);
301
306
  const items = [
302
307
  // #1523 N2: Details is a page now (the Fleet side of this instance), not a dialog.
303
308
  { key: "details", label: t("chat.details"), icon: "info", onSelect: () => navigate(detailsPath(name)) },
@@ -310,7 +315,7 @@ function ChatView({ name, inst, lease, navKey, exec, awaiting }) {
310
315
  ...themeMenuItems(name),
311
316
  // #2117: its ambient — why it is not showing here (with the way to change that), and a person's Clear.
312
317
  ...ambientMenuItems(name, { publicLink: document.body.dataset.publicLink === "1", clear: clearAmbient }),
313
- chatEffectsMenuAvailable() ? { key: "effects", label: t("chatFx.title"), onSelect: () => setDialog("effects") } : null,
318
+ cwEnabled && canvasWorkerMenuAvailable(window) ? { key: "effects", label: t("cwControls.title"), onSelect: () => setDialog("effects") } : null,
314
319
  { key: "delete", label: t("chat.delete"), icon: "trash", danger: true, onSelect: () => setDialog("delete") },
315
320
  ];
316
321
  const cls = statusClass(inst, exec, awaiting);
@@ -451,11 +456,16 @@ function follow(name, thread) {
451
456
  }));
452
457
  offs.push(P() ? P().onChange(() => thread.refreshCards()) : () => {});
453
458
  // #2193: the runtime may start later (an offer shown wakes it): its trial updates are followed from then.
454
- let trialOff = null;
455
- const watchTrial = () => { trialOff = getAppEffects()?.subscribeTrial(() => thread.render(store.state.msgs[name] || [])) ?? null; };
459
+ let trialOff = null, trialOwner = null;
460
+ const watchTrial = () => {
461
+ const next = getAppEffects();
462
+ if (next === trialOwner) return;
463
+ trialOff?.(); trialOff = null; trialOwner = next;
464
+ trialOff = next?.subscribeTrial(() => thread.render(store.state.msgs[name] || [])) ?? null;
465
+ };
456
466
  watchTrial();
457
- if (!trialOff) offs.push(onEffectsAwake(() => { watchTrial(); thread.render(store.state.msgs[name] || []); }));
458
- offs.push(() => trialOff?.());
467
+ offs.push(onEffectsAwake(() => { watchTrial(); thread.render(store.state.msgs[name] || []); }));
468
+ offs.push(() => { trialOff?.(); trialOff = null; trialOwner = null; });
459
469
  offs.push(panelStore.subscribe(() => thread.refreshCards())); // a card shows whether the panel has its block
460
470
  return () => { for (const off of offs) off(); };
461
471
  }
@@ -537,7 +547,7 @@ function Dock({ name, inst, lease, exec, awaiting, workingLabel }) {
537
547
  <${CommandCard} name=${name} lease=${lease} />
538
548
  <${QuickActions} name=${name} inst=${inst} lease=${lease} />
539
549
  ${s.failedSends[name] ? html`<div class="failed-send" role="alert"><span>${t("chat.notSent")}</span><span class="txt">${s.failedSends[name]}</span>
540
- <button type="button" class="btn btn-primary btn-sm" onClick=${() => store.putBack(name)}>${t("chat.putBack")}</button>
550
+ <button type="button" class="btn btn-primary btn-sm" onClick=${() => { recordEdit(composerField()); store.putBack(name); }}>${t("chat.putBack")}</button>
541
551
  <button type="button" class="btn btn-sm" onClick=${() => store.discardFailed(name)}>${t("chat.discard")}</button></div>` : null}
542
552
  ${s.waking[name] ? html`<p class="send-waking" role="status">${t("chat.waking", name)}</p>` : null}
543
553
  ${s.sendFailures[name] ? html`<div class="failed-send send-failure" role="alert"><span>${s.sendFailures[name].error}</span>
@@ -654,7 +664,8 @@ function Composer({ name, lease, busy }) {
654
664
  setValue("");
655
665
  store.runCommand(name, cmd.command, cmd.args, { alive: () => lease.current() });
656
666
  };
657
- const complete = (c) => setValue(`/${c.name}${c.arg ? " " : ""}`);
667
+ // #2342: a completion is a step Ctrl+Z goes back from (to what was typed); a command that runs consumes the line.
668
+ const complete = (c) => { recordEdit(input.current); setValue(`/${c.name}${c.arg ? " " : ""}`); };
658
669
  const submit = () => {
659
670
  if (pick) {
660
671
  if (value === `/${pick.name}` && !needsArgument(pick.name)) runLine({ command: pick.name, args: "" });
@@ -664,7 +675,7 @@ function Composer({ name, lease, busy }) {
664
675
  // #1920: a guest's "/…" would be refused as a command — said here, the draft kept to edit.
665
676
  if (guest && /^\s*\//.test(value)) { toast(t("chat.guestNoCommands"), false); return; }
666
677
  const cmd = files.length ? null : parseCommandLine(value);
667
- if (cmd && needsArgument(cmd.command) && !cmd.args) { setValue(`/${cmd.command} `); return; }
678
+ if (cmd && needsArgument(cmd.command) && !cmd.args) { recordEdit(input.current); setValue(`/${cmd.command} `); return; }
668
679
  if (cmd) { runLine(cmd); return; }
669
680
  store.send(name);
670
681
  };
@@ -728,7 +739,7 @@ function FileChip({ f, i, name }) {
728
739
  return html`<span class="file-chip">
729
740
  ${isImage ? html`<img ref=${img} alt="" />` : html`<span class="ic"><${Icon} name="file" size=${16} /></span>`}
730
741
  <span class="nm">${f.name}</span><span class="sz">${R().formatSize(f.size)}</span>
731
- ${store.isPasted(f) ? html`<button type="button" class="as-text" onClick=${() => store.fileBackAsText(name, i)}>${t("chat.asText")}</button>` : null}
742
+ ${store.isPasted(f) ? html`<button type="button" class="as-text" onClick=${() => { recordEdit(composerField()); store.fileBackAsText(name, i); }}>${t("chat.asText")}</button>` : null}
732
743
  <button type="button" class="chip-x" title=${t("chat.removeFile")} aria-label=${`${t("chat.removeFile")} ${f.name}`} onClick=${() => store.removeFile(name, i)}><${Icon} name="close" size=${14} /></button></span>`;
733
744
  }
734
745
 
@@ -25,6 +25,7 @@ import { Empty, ErrorState, Skeleton } from "/assets/ui-states.js";
25
25
  import { toast } from "/assets/ui-toast.js";
26
26
  import "./chat-strings.js";
27
27
  import { api } from "./settings-confirm.js";
28
+ import { InstanceBoardWork } from "./fleet-task-work.js";
28
29
  import { requestAgentSettings } from "./settings-request.js";
29
30
 
30
31
  export function DetailsPanel({ route, navKey }) {
@@ -150,6 +151,7 @@ function DetailsView({ name, inst, navKey, exec, awaiting }) {
150
151
  ${row(t("chat.dContext"), html`<span class="pct">${ctx != null ? `${Math.round(ctx)}%` : "--"}</span>${ctx != null ? bar(ctx, ctx > 80 ? "warn" : "") : null}`)}
151
152
  ${row(t("chat.dRate5"), html`<span class="pct">${Math.round(rl5)}%</span>${bar(rl5, rl5 > 90 ? "error" : rl5 > 70 ? "warn" : "")}`)}
152
153
  ${row(t("chat.dRate7"), html`<span class="pct">${Math.round(rl7)}%</span>${bar(rl7, rl7 > 90 ? "error" : rl7 > 70 ? "warn" : "")}`)}</section>
154
+ <${InstanceBoardWork} name=${name} onOpen=${(id) => navigate(`/ui/fleet?card=${encodeURIComponent(id)}`)} />
153
155
  <section class="card"><h3>${t("chat.dActivity")}</h3>${d.recent_activity?.length ? d.recent_activity.map((a, i) => html`<div key=${i} class="activity">
154
156
  <span class="mono muted">${a.timestamp ? new Date(`${a.timestamp.replace(" ", "T")}Z`).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : ""}</span>
155
157
  <span class="ev">${a.event}</span> ${a.summary || ""}</div>`) : html`<p class="note">${t("chat.dNoActivity")}</p>`}</section>`;
@@ -276,6 +276,10 @@ function Config({ lease }) {
276
276
  // Each save owns the form's busy state only while it is the latest: an older save that is decided later (it waited
277
277
  // for an admin's confirmation) never unlocks the form under a newer one still on its way (#1453 review).
278
278
  const saves = useRef(0);
279
+ // #2342: each project root's row keeps its own key — keyed by position, removing one shifted the texts below into
280
+ // the boxes above (each box's text rewritten, its undo history lost).
281
+ const rootKeys = useRef({ n: 0, list: [] });
282
+ const rootKey = (i) => { const k = rootKeys.current; while (k.list.length <= i) k.list.push(++k.n); return k.list[i]; };
279
283
  useEffect(() => {
280
284
  if (!state || !state.data) return;
281
285
  const c = state.data, ch = c.channel || {}, acc = ch.access || {};
@@ -314,9 +318,9 @@ function Config({ lease }) {
314
318
  <section class="card"><h3>${t("fleet.defaults")}</h3>
315
319
  <div class="field"><label for="cfg-be">${t("fleet.backend")}</label><${BackendSelect} id="cfg-be" value=${form.backend} onChange=${(v) => setForm({ ...form, backend: v })} /></div></section>
316
320
  <section class="card"><h3>${t("fleet.roots")}</h3>
317
- ${form.roots.map((r, i) => html`<div key=${i} class="field-row"><input value=${r} placeholder="/path/to/projects" aria-label=${t("fleet.roots")}
321
+ ${form.roots.map((r, i) => html`<div key=${rootKey(i)} class="field-row"><input value=${r} placeholder="/path/to/projects" aria-label=${t("fleet.roots")}
318
322
  onInput=${(e) => { const roots = [...form.roots]; roots[i] = e.target.value; setForm({ ...form, roots }); }} />
319
- <button type="button" class="icon-btn" aria-label=${t("fleet.removeRoot")} title=${t("fleet.removeRoot")} onClick=${() => setForm({ ...form, roots: form.roots.filter((_, j) => j !== i) })}><${Icon} name="close" size=${16} /></button></div>`)}
323
+ <button type="button" class="icon-btn" aria-label=${t("fleet.removeRoot")} title=${t("fleet.removeRoot")} onClick=${() => { rootKeys.current.list.splice(i, 1); setForm({ ...form, roots: form.roots.filter((_, j) => j !== i) }); }}><${Icon} name="close" size=${16} /></button></div>`)}
320
324
  <button type="button" class="btn btn-ghost" onClick=${() => setForm({ ...form, roots: [...form.roots, ""] })}><${Icon} name="plus" size=${16} />${t("fleet.addRoot")}</button></section>
321
325
  <div class="save-row"><button type="button" class="btn btn-primary" disabled=${saving} onClick=${save}>${t("fleet.save")}</button><span class="note">${t("fleet.saveNote")}</span></div>`}</${Loaded}>`;
322
326
  }
@@ -25,7 +25,8 @@ import { openSignIn, signInOffered } from "./backend-signin.js";
25
25
  register("needs", {
26
26
  en: {
27
27
  title: "Needs you", empty: "Nothing needs you right now", emptyHint: "When an agent waits for you, it shows here — and in your chat app's General.", emptyHintWeb: "When an agent waits for you, it shows here.",
28
- open: "Open chat", signIn: "Sign in", acknowledge: "Acknowledge", acknowledging: "Acknowledging…", answering: "Answering…",
28
+ open: "Open chat", openCard: "Open the card", noAssignee: "Task board (nobody assigned)", signIn: "Sign in",
29
+ gap: "not wrapped up: {0}", gapUnchecked: "{0} checklist item(s) unchecked", gapNoResult: "no result", gapJoin: ", ", acknowledge: "Acknowledge", acknowledging: "Acknowledging…", answering: "Answering…",
29
30
  ageNow: "just now", ageMin: "{0} min", ageHour: "{0} h", ageDay: "{0} d",
30
31
  notifyTitle: "Notify me on this device", notifyOn: "Notifications are on for this browser while a tab is open.",
31
32
  notifyOff: "Get a desktop notification when something new needs you while this tab is in the background.",
@@ -37,7 +38,8 @@ register("needs", {
37
38
  },
38
39
  "zh-TW": {
39
40
  title: "等你處理", empty: "目前沒有需要你處理的事", emptyHint: "agent 在等你時,會出現在這裡——也會出現在聊天 app 的 General。", emptyHintWeb: "agent 在等你時,會出現在這裡。",
40
- open: "開啟聊天", signIn: "登入", acknowledge: "確認", acknowledging: "確認中…", answering: "回覆中…",
41
+ open: "開啟聊天", openCard: "打開卡片", noAssignee: "任務看板(沒有負責人)", signIn: "登入",
42
+ gap: "還沒補齊:{0}", gapUnchecked: "還有 {0} 項沒勾", gapNoResult: "沒有產出總結", gapJoin: "、", acknowledge: "確認", acknowledging: "確認中…", answering: "回覆中…",
41
43
  ageNow: "剛剛", ageMin: "{0} 分鐘", ageHour: "{0} 小時", ageDay: "{0} 天",
42
44
  notifyTitle: "在這台裝置通知我", notifyOn: "這個瀏覽器的通知已開啟(需要開著分頁)。",
43
45
  notifyOff: "分頁在背景時,有新的事等你處理就跳出桌面通知。",
@@ -84,9 +86,21 @@ const setClaim = (kind, key, value) => claims.set((c) => {
84
86
  return { ...c, [kind]: next };
85
87
  });
86
88
 
87
- const ICON = { prompt: "chat", awaiting_input: "clock", instance: "alert", delivery: "send" };
89
+ const ICON = { prompt: "chat", awaiting_input: "clock", instance: "alert", delivery: "send", task: "tasks" };
88
90
  const BAD = new Set(["crashed", "delivery_failed", "hang", "exited"]);
89
91
 
92
+ /**
93
+ * An item's line. A task board card's is said here, in the page's language (#2246 step 3): its number and title, then
94
+ * its question or what it lacks; anything else is the server's own words.
95
+ */
96
+ function detailOf(item) {
97
+ const c = item.type === "task" ? item.card : null;
98
+ if (!c || !c.title) return item.detail;
99
+ const lacks = c.gap ? [c.gap.unchecked ? tn("gapUnchecked", c.gap.unchecked) : "", c.gap.noResult ? tn("gapNoResult") : ""].filter(Boolean).join(tn("gapJoin")) : "";
100
+ const tail = c.question || (lacks ? tn("gap", lacks) : "");
101
+ return `${c.code} ${c.title}${tail ? ` — ${tail}` : ""}`;
102
+ }
103
+
90
104
  /** "3 min", "2 h": the age shown beside an item (refreshed by a display timer, never by a read). */
91
105
  export function age(since, nowMs) {
92
106
  const minutes = Math.max(0, Math.floor((nowMs - since) / 60_000));
@@ -129,7 +143,7 @@ export function NeedsPanel({ navKey }) {
129
143
  <div data-trust="panel-content" class="panel-body"><div class="col">
130
144
  ${!items.length ? html`<${Empty} icon="check" title=${tn("empty")} hint=${tn(chatApp ? "emptyHint" : "emptyHintWeb")} />`
131
145
  : groups.map((g) => html`<section key=${g.instance} class="n-group" aria-label=${g.instance}>
132
- <h2 class="n-group-head"><a href=${chatPath(g.instance)} title=${g.instance}>${nameFor(g.instance)}</a></h2>
146
+ <h2 class="n-group-head">${g.instance ? html`<a href=${chatPath(g.instance)} title=${g.instance}>${nameFor(g.instance)}</a>` : tn("noAssignee")}</h2>
133
147
  ${g.items.map((item) => html`<${NeedsItem} key=${item.id} item=${item} nowMs=${nowMs} owner=${owner} />`)}
134
148
  </section>`)}
135
149
  <${NotifyToggle} />
@@ -180,14 +194,15 @@ function NeedsItem({ item, nowMs, owner }) {
180
194
  <span class="n-icon"><${Icon} name=${ICON[item.type] || "alert"} size=${18} /></span>
181
195
  <div class="n-body">
182
196
  <div class="n-title"><strong>${reasonText(item)}</strong><span class="n-age">${age(item.since, nowMs)}</span></div>
183
- ${item.detail ? html`<p class="n-detail">${item.detail}</p>` : null}
197
+ ${detailOf(item) ? html`<p class="n-detail">${detailOf(item)}</p>` : null}
184
198
  <div class="n-actions">
185
199
  ${item.type === "prompt" && Array.isArray(item.actions) ? item.actions.map((a) => html`<button key=${a.id} type="button" class="btn btn-sm"
186
200
  disabled=${promptBusy} onClick=${() => answer(a.id)}>${(p ? p.busy : fallback === "busy") ? tn("answering") : a.label}</button>`) : null}
187
201
  ${item.type === "delivery" ? html`<button type="button" class="btn btn-sm" disabled=${!!ack} onClick=${acknowledge}>
188
202
  <${Icon} name="check" size=${14} />${ack === "busy" ? tn("acknowledging") : tn("acknowledge")}</button>` : null}
189
203
  ${item.reason === "auth_paused" && item.signIn && signInOffered() ? html`<button type="button" class="btn btn-sm btn-primary" onClick=${() => openSignIn(item.signIn)}>${tn("signIn")}</button>` : null}
190
- <a class="btn btn-sm btn-ghost" href=${chatPath(item.instance)}>${tn("open")}</a>
204
+ ${item.type === "task" && item.card ? html`<a class="btn btn-sm" href=${`/ui/fleet?card=${encodeURIComponent(item.card.code)}`}>${tn("openCard")}</a>` : null}
205
+ ${item.instance ? html`<a class="btn btn-sm btn-ghost" href=${chatPath(item.instance)}>${tn("open")}</a>` : null}
191
206
  </div>
192
207
  </div>
193
208
  </article>`;
@@ -175,7 +175,7 @@ export function SettingsPanel({ route, navKey }) {
175
175
  const applied = (pendingChanges || []).filter((p) => p.state === "applied").map((p) => p.id).join(",");
176
176
  useEffect(() => { if (applied) reload(); }, [applied]);
177
177
 
178
- const stage = (key, change) => {
178
+ const stage = (key, change, quiet = false) => {
179
179
  setStaged((s) => {
180
180
  const m = new Map(s);
181
181
  const prev = m.get(key);
@@ -186,7 +186,7 @@ export function SettingsPanel({ route, navKey }) {
186
186
  m.set(key, { ...change, stageKey: key });
187
187
  return m;
188
188
  });
189
- toast(tn("queued", change.label));
189
+ if (!quiet) toast(tn("queued", change.label));
190
190
  };
191
191
  const unstage = (key) => setStaged((s) => { if (!s.has(key)) return s; const m = new Map(s); m.delete(key); return m; });
192
192
  const discard = () => { dropSecrets(staged); setStaged(new Map()); reload(); };
@@ -813,16 +813,26 @@ function WebSection({ ctx, staged }) {
813
813
  echo: web.echo_to_channel ?? true,
814
814
  log: web.log_tab === true, // #1218: off by default
815
815
  usagePub: web.usage_report_on_public_link === true, // #2122: off by default
816
- pub: { allow_public: (web.public_link && web.public_link.allow_public) ?? true, ttl_minutes: (web.public_link && web.public_link.ttl_minutes) ?? 120, protocol: (web.public_link && web.public_link.protocol) ?? "http2" },
816
+ pub: { allow_public: (web.public_link && web.public_link.allow_public) ?? true, ttl_minutes: (web.public_link && web.public_link.ttl_minutes) ?? 120, protocol: (web.public_link && web.public_link.protocol) ?? "http2", hostname: (web.public_link && web.public_link.hostname) ?? "", tunnel_token_env: (web.public_link && web.public_link.tunnel_token_env) ?? "", origin_port: (web.public_link && web.public_link.origin_port) ?? 19282 },
817
817
  session: { absolute_hours: local.absolute_hours ?? null, idle_hours: local.idle_hours ?? null },
818
818
  terminal: { enabled: term.enabled ?? null, ttl_minutes: term.ttl_minutes ?? null },
819
819
  }), []);
820
820
  const text = (v) => (v === null || v === undefined ? "" : String(v));
821
- const [f, setF] = useState(() => ({
822
- echo: original.echo, log: original.log, usagePub: original.usagePub, pub: original.pub.allow_public, ttl: String(original.pub.ttl_minutes), protocol: original.pub.protocol,
823
- abs: text(original.session.absolute_hours), idle: text(original.session.idle_hours),
824
- termOn: original.terminal.enabled !== false, termTtl: text(original.terminal.ttl_minutes),
825
- }));
821
+ const [f, setF] = useState(() => {
822
+ // Settings sections share one draft. Reopening Web must show it as well as preserve it in the Apply bar, so a
823
+ // subsequent field edit keeps earlier edits. The original disk values remain the diff/confirmation baseline.
824
+ const request = staged.get("web")?.request;
825
+ const pending = request?.method === "PUT" && request.url === "/api/settings/fleet/web" ? request.body || {} : {};
826
+ const pub = pending.public_link || {}, session = pending.session?.local || {}, terminal = pending.web_terminal || {};
827
+ const pick = (obj, key, fallback) => Object.prototype.hasOwnProperty.call(obj, key) ? obj[key] : fallback;
828
+ return {
829
+ echo: pick(pending, "echo_to_channel", original.echo), log: pick(pending, "log_tab", original.log), usagePub: pick(pending, "usage_report_on_public_link", original.usagePub),
830
+ pub: pick(pub, "allow_public", original.pub.allow_public), ttl: String(pick(pub, "ttl_minutes", original.pub.ttl_minutes)), protocol: pick(pub, "protocol", original.pub.protocol),
831
+ hostname: pick(pub, "hostname", original.pub.hostname), originPort: String(pick(pub, "origin_port", original.pub.origin_port)),
832
+ abs: text(pick(session, "absolute_hours", original.session.absolute_hours)), idle: text(pick(session, "idle_hours", original.session.idle_hours)),
833
+ termOn: pick(terminal, "enabled", original.terminal.enabled) !== false, termTtl: text(pick(terminal, "ttl_minutes", original.terminal.ttl_minutes)),
834
+ };
835
+ });
826
836
  const [msg, setMsg] = useState(null);
827
837
  const set = (k) => (v) => setF((x) => ({ ...x, [k]: v }));
828
838
  // Empty = the default (null in the write: the key is removed from fleet.yaml).
@@ -832,15 +842,22 @@ function WebSection({ ctx, staged }) {
832
842
  // the store and the validator apply it — so only an idle time that is typed can exceed the session length.
833
843
  const abs = value(f.abs) ?? 48, idle = value(f.idle) ?? Math.min(48, abs);
834
844
  const errors = {
845
+ hostname: f.hostname && !/^(?:https:\/\/)?[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)+\/?$/.test(f.hostname) ? "namedHostnameInvalid" : "",
846
+ originPort: !Number.isInteger(Number(f.originPort)) || Number(f.originPort) < 1024 || Number(f.originPort) > 65535 ? "namedPortInvalid" : "",
835
847
  ttl: !Number.isInteger(Number(f.ttl)) || Number(f.ttl) < 1 || Number(f.ttl) > 480 ? "publicTtl" : "",
836
848
  abs: hours(f.abs),
837
849
  idle: hours(f.idle) || (!hours(f.abs) && idle > abs ? "sessionIdleOver" : ""),
838
850
  termTtl: value(f.termTtl) !== null && (!Number.isInteger(value(f.termTtl)) || value(f.termTtl) < 1 || value(f.termTtl) > 20) ? "webTerminalTtlRange" : "",
839
851
  };
840
852
  const ready = !overPublicLink && !Object.values(errors).some(Boolean);
841
- const review = () => {
842
- if (!ready) return;
843
- const publicPatch = changedFields({ allow_public: f.pub, ttl_minutes: Number(f.ttl), protocol: f.protocol }, original.pub);
853
+ const review = (quiet = false) => {
854
+ // Invalid intermediate text must not leave an older valid draft available to Apply.
855
+ if (!ready) { if (staged.has("web")) ctx.unstage("web"); setMsg(null); return; }
856
+ // Keep an existing secret reference, including a custom one. Only a newly supplied hostname without any
857
+ // reference adopts the standard env key; mounting or editing an unrelated field never rewrites credentials.
858
+ const tokenEnv = f.hostname === original.pub.hostname ? original.pub.tunnel_token_env
859
+ : f.hostname ? original.pub.tunnel_token_env || "AGEND_CLOUDFLARE_TUNNEL_TOKEN" : "";
860
+ const publicPatch = changedFields({ allow_public: f.pub, ttl_minutes: Number(f.ttl), protocol: f.protocol, hostname: f.hostname, tunnel_token_env: tokenEnv, origin_port: Number(f.originPort) }, original.pub);
844
861
  const sessionPatch = changedFields({ absolute_hours: value(f.abs), idle_hours: value(f.idle) }, original.session);
845
862
  // The checkbox shows the default (on) for an unset key: only a real toggle is written; on again = back to the default.
846
863
  const terminalPatch = changedFields({ enabled: f.termOn === (original.terminal.enabled !== false) ? original.terminal.enabled : f.termOn ? null : false,
@@ -863,10 +880,16 @@ function WebSection({ ctx, staged }) {
863
880
  ];
864
881
  if (fields.length) {
865
882
  ctx.stage("web", { label: tn("sec_web"), impact: batchImpact(schema, fields, (k) => impactOf(schema, k)),
866
- request: { method: "PUT", url: "/api/settings/fleet/web", body } });
883
+ request: { method: "PUT", url: "/api/settings/fleet/web", body } }, quiet);
867
884
  } else if (staged.has("web")) ctx.unstage("web");
868
- setMsg(tn("readyApply"));
885
+ setMsg(fields.length ? tn("readyApply") : null);
869
886
  };
887
+ // This form already has validation and a single shared Web draft. Valid edits join that draft immediately;
888
+ // Apply, its confirmations and the leave guard still own all HTTP writes. Quiet staging avoids a toast per key.
889
+ // Returning from another Settings section remounts this form: its unchanged initial fields must not erase the
890
+ // parent's already-staged Web draft. Only an actual field edit replaces or removes that draft.
891
+ const formMounted = useRef(false);
892
+ useEffect(() => { if (!formMounted.current) { formMounted.current = true; return; } review(true); }, [f]);
870
893
  const shown = (path) => {
871
894
  const [top, key] = path.split(".");
872
895
  const v = (top === "web" ? web : term)[key];
@@ -902,18 +925,23 @@ function WebSection({ ctx, staged }) {
902
925
  <p class="note">${tn("webUsagePublicHint")}</p>
903
926
  <label class="check"><input id="publicWebLink" type="checkbox" checked=${f.pub} onChange=${(e) => set("pub")(e.target.checked)} /><span>${tn("publicLink")}</span><${Impact} schema=${schema} field="web.public_link.allow_public" /></label>
904
927
  <p class="note">${tn("publicLinkHint")}</p>
928
+ <div class="grid2">
929
+ <div class="field"><label for="publicWebHostname">${tn("namedHostname")}</label><input id="publicWebHostname" type="text" value=${f.hostname} placeholder="agend.example.com" class=${errors.hostname ? "invalid" : ""} onInput=${(e) => set("hostname")(e.target.value)} /><${Feedback} error=${errors.hostname} /></div>
930
+ <div class="field"><label for="publicWebOriginPort">${tn("namedOriginPort")}</label><input id="publicWebOriginPort" type="number" min="1024" max="65535" value=${f.originPort} class=${errors.originPort ? "invalid" : ""} onInput=${(e) => set("originPort")(e.target.value)} /><${Feedback} error=${errors.originPort} /></div>
931
+ </div>
932
+ <p class="note">${tn("namedTunnelHint")} <a href="https://github.com/songsid/AgEnD/blob/main/docs/named-tunnel.md" target="_blank" rel="noopener noreferrer">${tn("namedTunnelSetup")}</a></p>
905
933
  <div class="grid2">
906
934
  <div class="field"><label for="publicWebTtl">${tn("publicTtl")}</label><input id="publicWebTtl" type="number" min="1" max="480" class=${errors.ttl ? "invalid" : ""} value=${f.ttl} onInput=${(e) => set("ttl")(e.target.value)} /><${Feedback} error=${errors.ttl} /></div>
907
935
  <div class="field"><label for="publicWebProtocol">${tn("publicProtocol")}</label><${Select} id="publicWebProtocol" value=${f.protocol} onChange=${set("protocol")} options=${["http2", "quic", "auto"]} /></div>
908
936
  </div>
909
937
  </section>
910
938
  </fieldset>
911
- ${overPublicLink ? null : html`<${GuestsCard} ctx=${ctx} staged=${staged} /><${WebAuthorityCard} ctx=${ctx} staged=${staged} />`}
939
+ ${overPublicLink ? null : html`<${PublicTunnelToken} /><${GuestsCard} ctx=${ctx} staged=${staged} /><${WebAuthorityCard} ctx=${ctx} staged=${staged} />`}
912
940
  <section class="card s-web-readonly"><h3>${tn("webReadOnlyTitle")}</h3>
913
941
  <p class="note">${tn("webReadOnlyLead")}</p>
914
942
  <dl class="s-web-ro">${WEB_READ_ONLY.map((k) => html`<div key=${k}><dt><code>${k}</code></dt><dd>${shown(k)}</dd></div>`)}</dl>
915
943
  </section>
916
- <div class="save-row"><button type="button" class="btn btn-primary" disabled=${!ready} onClick=${review}>${tn("reviewChanges")}</button>
944
+ <div class="save-row"><button type="button" class="btn btn-primary" disabled=${!ready} onClick=${() => review()}>${tn("reviewChanges")}</button>
917
945
  ${msg ? html`<span class="note" role="status">${msg}</span>` : null}</div>
918
946
  </div>`;
919
947
  }
@@ -1033,3 +1061,50 @@ function HelpDialog({ onClose }) {
1033
1061
  }
1034
1062
 
1035
1063
  export { worstImpact, ACCESS_MODES };
1064
+
1065
+ function PublicTunnelToken() {
1066
+ const lease = useLease("public-tunnel-token");
1067
+ const [secret, setSecret] = useState("");
1068
+ const [present, setPresent] = useState(false);
1069
+ const [busy, setBusy] = useState(false);
1070
+ const inFlight = useRef(false);
1071
+ const [message, setMessage] = useState("");
1072
+ const base = "/api/settings/public-tunnel-token";
1073
+ const refresh = async () => {
1074
+ const result = await api(base);
1075
+ if (lease.current() && result.ok) setPresent(result.body?.[0]?.token_present === true);
1076
+ };
1077
+ useEffect(() => { refresh().catch(() => {}); }, []);
1078
+ const go = async (clear) => {
1079
+ if (inFlight.current || !clear && !secret) return;
1080
+ const value = secret;
1081
+ setSecret(""); inFlight.current = true; setBusy(true); setMessage("");
1082
+ const key = newKey("tunnel-token");
1083
+ try {
1084
+ const prepared = await api(`${base}/verify`, { method: "POST", headers: { "Idempotency-Key": key }, body: JSON.stringify(clear ? { clear: true } : { secret: value }) });
1085
+ if (!lease.current()) return;
1086
+ if (!prepared.ok && prepared.body?.error_code === "invalid_tunnel_token") { setMessage(tn("namedTokenInvalid")); return; }
1087
+ if (!prepared.ok) throw Error("refused");
1088
+ const applied = await confirmedWrite(`${base}/apply`, { method: "POST", key, label: tn(clear ? "namedTokenClear" : "namedTokenSet"), body: { verification_id: prepared.body?.verification_id, idempotency_key: key } });
1089
+ let body = applied.body || {};
1090
+ for (let n = 0; applied.ok && body.job_id && body.result === "applying" && n < 60; n++) {
1091
+ await new Promise(r => setTimeout(r, 250));
1092
+ const status = await api(`${base}/apply/${encodeURIComponent(body.job_id)}`);
1093
+ if (status.ok) body = status.body || body;
1094
+ }
1095
+ if (lease.current()) {
1096
+ setMessage(tn(applied.ok && body.result === "reloaded" ? "namedTokenSaved" : body.result === "pending_confirmation" ? "awaitingConfirmation" : "applyFailed"));
1097
+ await refresh();
1098
+ }
1099
+ } catch { if (lease.current()) setMessage(tn("applyFailed")); }
1100
+ finally { inFlight.current = false; if (lease.current()) setBusy(false); }
1101
+ };
1102
+ return html`<section class="card"><h3>${tn("namedToken")}</h3>
1103
+ <p class="note">${tn("namedTokenHint")}</p>
1104
+ <span class="tag">${present ? tn("keyConfigured") : tn("keyNotConfigured")}</span>
1105
+ <input id="publicTunnelToken" type="password" autocomplete="new-password" aria-label=${tn("namedToken")} value=${secret} onInput=${e => setSecret(e.target.value)} />
1106
+ <button type="button" class="btn btn-sm" disabled=${busy || !secret} onClick=${() => go(false)}>${tn("namedTokenSet")}</button>
1107
+ <button type="button" class="btn btn-sm" disabled=${busy || !present} onClick=${() => go(true)}>${tn("namedTokenClear")}</button>
1108
+ ${message ? html`<p class="note" role="status">${message}</p>` : null}
1109
+ </section>`;
1110
+ }
@@ -16,7 +16,8 @@ import { Icon } from "/assets/ui-icons.js";
16
16
  import "./chat-render.js";
17
17
  import "./preview.js";
18
18
  import { loadHtmlAttachment } from "./html-attachment.js";
19
- import { EffectTrialPanel } from "./effect-trial-panel.js";
19
+ import { experimentalComponent, useCanvasWorker } from "/assets/feature-flags.js";
20
+ const EffectTrialPanel = experimentalComponent(() => import("./effect-trial-panel.js"), m => m.EffectTrialPanel);
20
21
  import { chatTime, onDayChange } from "./chat-time.js";
21
22
 
22
23
  const R = () => globalThis.AgendChatRender;
@@ -5,6 +5,7 @@
5
5
  // agents, a task — opens here as the index's copy (GET /ui/search/context). Text only: no innerHTML, no style attribute.
6
6
  import { html, useEffect, useLayoutEffect, useMemo, useRef, useState } from "/assets/app-html.js";
7
7
  import { t, register } from "/assets/app-i18n.js";
8
+ import { recordEdit } from "/assets/text-undo.js";
8
9
  import { appStore, useStore } from "/assets/app-store.js";
9
10
  import { navigate } from "/assets/app-nav.js";
10
11
  import { chatPath, fleetPath } from "/assets/app-route.js";
@@ -286,7 +287,7 @@ export function SearchDialog({ preset = {}, onClose }) {
286
287
  <div class="search-body" ref=${list}>
287
288
  ${detail ? html`<${Detail} detail=${detail} q=${q} label=${label} onBack=${() => { setDetail(null); input.current && input.current.focus(); }} onClose=${onClose} />`
288
289
  : html`<${Results} q=${q} res=${res} groups=${groups} active=${active} setActive=${setActive} open=${open} label=${label} filtered=${activeFilters > 0}
289
- recent=${recent} pick=${(s) => { setQ(s); input.current && input.current.focus(); }} clearRecent=${() => { forgetRecent(); setRecent([]); }} />`}
290
+ recent=${recent} pick=${(s) => { recordEdit(input.current); setQ(s); input.current && input.current.focus(); }} clearRecent=${() => { forgetRecent(); setRecent([]); }} />`}
290
291
  </div>
291
292
  <div class="search-foot" aria-hidden="true">${t("search.keys")}</div>
292
293
  </div></dialog>`;