@songsid/agend 2.3.1 → 2.3.2-beta.10

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 (338) hide show
  1. package/THIRD_PARTY_NOTICES.md +40 -0
  2. package/dist/agent-cli.js +21 -2
  3. package/dist/agent-cli.js.map +1 -1
  4. package/dist/agent-endpoint.js +5 -0
  5. package/dist/agent-endpoint.js.map +1 -1
  6. package/dist/antigravity-usage-scan.d.ts +58 -0
  7. package/dist/antigravity-usage-scan.js +257 -0
  8. package/dist/antigravity-usage-scan.js.map +1 -0
  9. package/dist/antigravity-usage-worker.d.ts +1 -0
  10. package/dist/antigravity-usage-worker.js +10 -0
  11. package/dist/antigravity-usage-worker.js.map +1 -0
  12. package/dist/auth-api.js +4 -0
  13. package/dist/auth-api.js.map +1 -1
  14. package/dist/authority/authority-state.d.ts +4 -0
  15. package/dist/authority/authority-state.js +16 -10
  16. package/dist/authority/authority-state.js.map +1 -1
  17. package/dist/authority/session-authorizations.d.ts +7 -1
  18. package/dist/authority/session-authorizations.js +6 -4
  19. package/dist/authority/session-authorizations.js.map +1 -1
  20. package/dist/authority/web-authority.d.ts +19 -6
  21. package/dist/authority/web-authority.js +22 -8
  22. package/dist/authority/web-authority.js.map +1 -1
  23. package/dist/backend/kiro-identity.d.ts +64 -0
  24. package/dist/backend/kiro-identity.js +101 -0
  25. package/dist/backend/kiro-identity.js.map +1 -1
  26. package/dist/backend/kiro-retired-model.d.ts +62 -0
  27. package/dist/backend/kiro-retired-model.js +160 -0
  28. package/dist/backend/kiro-retired-model.js.map +1 -0
  29. package/dist/backend/kiro.d.ts +71 -2
  30. package/dist/backend/kiro.js +352 -11
  31. package/dist/backend/kiro.js.map +1 -1
  32. package/dist/backend/types.d.ts +59 -0
  33. package/dist/backend/types.js.map +1 -1
  34. package/dist/backend-signin-guide.d.ts +1 -0
  35. package/dist/backend-signin-guide.js +9 -0
  36. package/dist/backend-signin-guide.js.map +1 -0
  37. package/dist/backend-signin-pending.d.ts +10 -0
  38. package/dist/backend-signin-pending.js +44 -0
  39. package/dist/backend-signin-pending.js.map +1 -0
  40. package/dist/browser-backend-signins.d.ts +56 -0
  41. package/dist/browser-backend-signins.js +189 -0
  42. package/dist/browser-backend-signins.js.map +1 -0
  43. package/dist/cache-ledger.d.ts +38 -2
  44. package/dist/cache-ledger.js +133 -8
  45. package/dist/cache-ledger.js.map +1 -1
  46. package/dist/cache-prices.d.ts +2 -1
  47. package/dist/cache-prices.js +5 -2
  48. package/dist/cache-prices.js.map +1 -1
  49. package/dist/cache-service.d.ts +68 -0
  50. package/dist/cache-service.js +355 -13
  51. package/dist/cache-service.js.map +1 -1
  52. package/dist/channel/adapters/discord.d.ts +13 -0
  53. package/dist/channel/adapters/discord.js +103 -16
  54. package/dist/channel/adapters/discord.js.map +1 -1
  55. package/dist/channel/mcp-tools.js +34 -7
  56. package/dist/channel/mcp-tools.js.map +1 -1
  57. package/dist/channel/tool-router.js +2 -1
  58. package/dist/channel/tool-router.js.map +1 -1
  59. package/dist/channel/types.d.ts +11 -0
  60. package/dist/classic-channel-manager.d.ts +11 -0
  61. package/dist/classic-channel-manager.js +39 -1
  62. package/dist/classic-channel-manager.js.map +1 -1
  63. package/dist/cli.js +21 -2
  64. package/dist/cli.js.map +1 -1
  65. package/dist/config-validator.d.ts +7 -1
  66. package/dist/config-validator.js +92 -12
  67. package/dist/config-validator.js.map +1 -1
  68. package/dist/config.d.ts +19 -0
  69. package/dist/config.js +31 -1
  70. package/dist/config.js.map +1 -1
  71. package/dist/cpu-quota.d.ts +23 -0
  72. package/dist/cpu-quota.js +86 -0
  73. package/dist/cpu-quota.js.map +1 -0
  74. package/dist/daemon.d.ts +57 -3
  75. package/dist/daemon.js +330 -50
  76. package/dist/daemon.js.map +1 -1
  77. package/dist/dashboard-permissions.d.ts +20 -6
  78. package/dist/dashboard-permissions.js +58 -9
  79. package/dist/dashboard-permissions.js.map +1 -1
  80. package/dist/delivery-outbox.d.ts +12 -1
  81. package/dist/delivery-outbox.js +67 -15
  82. package/dist/delivery-outbox.js.map +1 -1
  83. package/dist/doctor.d.ts +3 -0
  84. package/dist/doctor.js +11 -1
  85. package/dist/doctor.js.map +1 -1
  86. package/dist/event-log.d.ts +5 -0
  87. package/dist/event-log.js +13 -0
  88. package/dist/event-log.js.map +1 -1
  89. package/dist/fleet-manager.d.ts +212 -0
  90. package/dist/fleet-manager.js +914 -111
  91. package/dist/fleet-manager.js.map +1 -1
  92. package/dist/fleet-process-handlers.d.ts +1 -0
  93. package/dist/fleet-process-handlers.js +9 -1
  94. package/dist/fleet-process-handlers.js.map +1 -1
  95. package/dist/forged-envelope-scan.d.ts +9 -0
  96. package/dist/forged-envelope-scan.js +30 -5
  97. package/dist/forged-envelope-scan.js.map +1 -1
  98. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +5 -2
  99. package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +14 -1
  100. package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +28 -5
  101. package/dist/general-knowledge/skills/web-appearance/SKILL.md +31 -8
  102. package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +138 -59
  103. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +145 -61
  104. package/dist/guest-seen-people.d.ts +38 -0
  105. package/dist/guest-seen-people.js +78 -0
  106. package/dist/guest-seen-people.js.map +1 -0
  107. package/dist/instance-config-impact.js +14 -6
  108. package/dist/instance-config-impact.js.map +1 -1
  109. package/dist/instance-lifecycle.d.ts +20 -0
  110. package/dist/instance-lifecycle.js +71 -13
  111. package/dist/instance-lifecycle.js.map +1 -1
  112. package/dist/kiro-db-reader.d.ts +5 -0
  113. package/dist/kiro-db-reader.js +18 -0
  114. package/dist/kiro-db-reader.js.map +1 -1
  115. package/dist/kiro-engine-status.d.ts +2 -0
  116. package/dist/kiro-engine-status.js +9 -0
  117. package/dist/kiro-engine-status.js.map +1 -1
  118. package/dist/kiro-migrate.d.ts +49 -0
  119. package/dist/kiro-migrate.js +105 -0
  120. package/dist/kiro-migrate.js.map +1 -0
  121. package/dist/kiro-transcript-lane.d.ts +2 -0
  122. package/dist/kiro-transcript-lane.js.map +1 -1
  123. package/dist/kiro-transcript-worker.js +1 -0
  124. package/dist/kiro-transcript-worker.js.map +1 -1
  125. package/dist/kiro-tui-forensic.d.ts +7 -0
  126. package/dist/kiro-tui-forensic.js +110 -0
  127. package/dist/kiro-tui-forensic.js.map +1 -0
  128. package/dist/kiro-usage-run.d.ts +7 -0
  129. package/dist/kiro-usage-run.js +39 -0
  130. package/dist/kiro-usage-run.js.map +1 -0
  131. package/dist/kiro-usage-scan.d.ts +49 -0
  132. package/dist/kiro-usage-scan.js +223 -0
  133. package/dist/kiro-usage-scan.js.map +1 -0
  134. package/dist/kiro-usage-worker.d.ts +1 -0
  135. package/dist/kiro-usage-worker.js +10 -0
  136. package/dist/kiro-usage-worker.js.map +1 -0
  137. package/dist/kiro-usage.d.ts +58 -0
  138. package/dist/kiro-usage.js +310 -0
  139. package/dist/kiro-usage.js.map +1 -0
  140. package/dist/locale.js +120 -12
  141. package/dist/locale.js.map +1 -1
  142. package/dist/log-service.js +1 -1
  143. package/dist/log-service.js.map +1 -1
  144. package/dist/login-url.d.ts +4 -0
  145. package/dist/login-url.js +22 -0
  146. package/dist/login-url.js.map +1 -0
  147. package/dist/mac-auto-restore.d.ts +117 -0
  148. package/dist/mac-auto-restore.js +294 -0
  149. package/dist/mac-auto-restore.js.map +1 -0
  150. package/dist/needs-you-hub.d.ts +2 -1
  151. package/dist/needs-you-hub.js +1 -1
  152. package/dist/needs-you-hub.js.map +1 -1
  153. package/dist/needs-you.d.ts +8 -2
  154. package/dist/needs-you.js +6 -0
  155. package/dist/needs-you.js.map +1 -1
  156. package/dist/notice-delivery.d.ts +16 -0
  157. package/dist/notice-delivery.js +35 -0
  158. package/dist/notice-delivery.js.map +1 -0
  159. package/dist/office-share-page.d.ts +16 -0
  160. package/dist/office-share-page.js +85 -0
  161. package/dist/office-share-page.js.map +1 -0
  162. package/dist/office-share-service.d.ts +91 -0
  163. package/dist/office-share-service.js +176 -0
  164. package/dist/office-share-service.js.map +1 -0
  165. package/dist/office-share.d.ts +258 -0
  166. package/dist/office-share.js +549 -0
  167. package/dist/office-share.js.map +1 -0
  168. package/dist/opencode-usage-scan.d.ts +28 -0
  169. package/dist/opencode-usage-scan.js +92 -0
  170. package/dist/opencode-usage-scan.js.map +1 -0
  171. package/dist/opencode-usage-worker.d.ts +1 -0
  172. package/dist/opencode-usage-worker.js +10 -0
  173. package/dist/opencode-usage-worker.js.map +1 -0
  174. package/dist/outbound-handlers.js +22 -2
  175. package/dist/outbound-handlers.js.map +1 -1
  176. package/dist/outbound-schemas.d.ts +28 -2
  177. package/dist/outbound-schemas.js +29 -2
  178. package/dist/outbound-schemas.js.map +1 -1
  179. package/dist/public-web-gateway.js +16 -2
  180. package/dist/public-web-gateway.js.map +1 -1
  181. package/dist/quickstart-api.js +13 -2
  182. package/dist/quickstart-api.js.map +1 -1
  183. package/dist/quickstart.js +2 -1
  184. package/dist/quickstart.js.map +1 -1
  185. package/dist/remote-card-answer.d.ts +83 -0
  186. package/dist/remote-card-answer.js +208 -0
  187. package/dist/remote-card-answer.js.map +1 -0
  188. package/dist/reply-triggers.d.ts +2 -0
  189. package/dist/reply-triggers.js +3 -0
  190. package/dist/reply-triggers.js.map +1 -0
  191. package/dist/settings-api.d.ts +28 -0
  192. package/dist/settings-api.js +148 -17
  193. package/dist/settings-api.js.map +1 -1
  194. package/dist/settings-change.js +54 -17
  195. package/dist/settings-change.js.map +1 -1
  196. package/dist/settings-effect.js +12 -3
  197. package/dist/settings-effect.js.map +1 -1
  198. package/dist/settings-instance-patch.js +4 -1
  199. package/dist/settings-instance-patch.js.map +1 -1
  200. package/dist/setup-form.d.ts +1 -1
  201. package/dist/setup-form.js +55 -6
  202. package/dist/setup-form.js.map +1 -1
  203. package/dist/setup-host.d.ts +9 -0
  204. package/dist/setup-host.js +127 -6
  205. package/dist/setup-host.js.map +1 -1
  206. package/dist/setup-signin-assets.d.ts +2 -0
  207. package/dist/setup-signin-assets.js +6 -0
  208. package/dist/setup-signin-assets.js.map +1 -0
  209. package/dist/setup-wizard.js +2 -0
  210. package/dist/setup-wizard.js.map +1 -1
  211. package/dist/start-attempt.d.ts +9 -5
  212. package/dist/start-attempt.js +27 -8
  213. package/dist/start-attempt.js.map +1 -1
  214. package/dist/start-failure-chat.d.ts +4 -0
  215. package/dist/start-failure-chat.js +23 -6
  216. package/dist/start-failure-chat.js.map +1 -1
  217. package/dist/start-failure-notice.d.ts +4 -0
  218. package/dist/start-failure-notice.js +8 -0
  219. package/dist/start-failure-notice.js.map +1 -1
  220. package/dist/start-failure.d.ts +25 -0
  221. package/dist/start-failure.js +75 -2
  222. package/dist/start-failure.js.map +1 -1
  223. package/dist/structured-signin-transport.d.ts +8 -0
  224. package/dist/structured-signin-transport.js +79 -0
  225. package/dist/structured-signin-transport.js.map +1 -0
  226. package/dist/structured-signin.d.ts +131 -0
  227. package/dist/structured-signin.js +388 -0
  228. package/dist/structured-signin.js.map +1 -0
  229. package/dist/tool-permissions.js +12 -0
  230. package/dist/tool-permissions.js.map +1 -1
  231. package/dist/topic-commands.js +6 -2
  232. package/dist/topic-commands.js.map +1 -1
  233. package/dist/transcript-sources.d.ts +26 -2
  234. package/dist/transcript-sources.js +103 -40
  235. package/dist/transcript-sources.js.map +1 -1
  236. package/dist/turn-reply-guard.d.ts +25 -0
  237. package/dist/turn-reply-guard.js +111 -12
  238. package/dist/turn-reply-guard.js.map +1 -1
  239. package/dist/types.d.ts +46 -9
  240. package/dist/ui/backend-signin.js +68 -12
  241. package/dist/ui/chat-ambient.js +1 -1
  242. package/dist/ui/chat-effects.js +4 -4
  243. package/dist/ui/chat-render.js +5 -2
  244. package/dist/ui/chat-strings.js +4 -4
  245. package/dist/ui/chat-thread.js +2 -2
  246. package/dist/ui/first-run.js +2 -2
  247. package/dist/ui/fleet-canvas-view.js +114 -0
  248. package/dist/ui/fleet-canvas.js +15 -77
  249. package/dist/ui/fleet-office-editor.js +94 -14
  250. package/dist/ui/fleet-office-layout.js +235 -0
  251. package/dist/ui/fleet-office-model.js +12 -6
  252. package/dist/ui/fleet-office-overview.js +167 -0
  253. package/dist/ui/fleet-office-pack.d.ts +44 -0
  254. package/dist/ui/fleet-office-pack.js +352 -37
  255. package/dist/ui/fleet-office-skin.js +220 -36
  256. package/dist/ui/fleet-office-store.js +112 -7
  257. package/dist/ui/fleet-office-view.js +155 -0
  258. package/dist/ui/fleet-office-zones.js +264 -42
  259. package/dist/ui/fleet-office.js +957 -160
  260. package/dist/ui/fleet-usage.js +26 -21
  261. package/dist/ui/office-share-dialog.js +145 -0
  262. package/dist/ui/office-share-page.js +164 -0
  263. package/dist/ui/panel-chat.js +9 -2
  264. package/dist/ui/panel-details.js +2 -2
  265. package/dist/ui/panel-fleet.js +11 -77
  266. package/dist/ui/panel-log.js +4 -4
  267. package/dist/ui/panel-needs.js +4 -4
  268. package/dist/ui/panel-settings.js +608 -167
  269. package/dist/ui/search-dialog.js +26 -7
  270. package/dist/ui/settings-appearance.js +82 -29
  271. package/dist/ui/settings-apply.js +42 -7
  272. package/dist/ui/settings-dialogs.js +76 -23
  273. package/dist/ui/settings-draft.js +51 -0
  274. package/dist/ui/settings-guests.js +255 -79
  275. package/dist/ui/settings-learn.js +2 -2
  276. package/dist/ui/settings-model.js +16 -6
  277. package/dist/ui/settings-registry.js +146 -0
  278. package/dist/ui/settings-strings.js +116 -44
  279. package/dist/ui/settings-web-authority.js +34 -23
  280. package/dist/ui/shared/app-i18n.js +18 -2
  281. package/dist/ui/shared/app-nav.js +10 -5
  282. package/dist/ui/shared/app-prefs.js +80 -2
  283. package/dist/ui/shared/app-route.js +39 -5
  284. package/dist/ui/shared/app-shell.js +73 -10
  285. package/dist/ui/shared/app-stream.js +3 -1
  286. package/dist/ui/shared/app.css +307 -17
  287. package/dist/ui/shared/app.js +3 -0
  288. package/dist/ui/shared/effect-stock-trust.js +1 -1
  289. package/dist/ui/shared/identity-color.js +4 -4
  290. package/dist/ui/shared/office/cozy.png +0 -0
  291. package/dist/ui/shared/side-panel.js +95 -0
  292. package/dist/ui/shared/signin.js +2 -2
  293. package/dist/ui/shared/ui-dialog.js +6 -1
  294. package/dist/ui/shared/ui-icons.js +2 -0
  295. package/dist/ui/shared/web-authorize.js +13 -12
  296. package/dist/ui/structured-signin.js +115 -0
  297. package/dist/usage-report.d.ts +20 -0
  298. package/dist/usage-report.js +54 -17
  299. package/dist/usage-report.js.map +1 -1
  300. package/dist/usage-rollup.d.ts +9 -1
  301. package/dist/usage-rollup.js +20 -4
  302. package/dist/usage-rollup.js.map +1 -1
  303. package/dist/web-api.d.ts +26 -2
  304. package/dist/web-api.js +361 -5
  305. package/dist/web-api.js.map +1 -1
  306. package/dist/web-auth.js +8 -2
  307. package/dist/web-auth.js.map +1 -1
  308. package/dist/web-backend-login.d.ts +1 -7
  309. package/dist/web-backend-login.js +2 -24
  310. package/dist/web-backend-login.js.map +1 -1
  311. package/dist/web-experimental.d.ts +1 -1
  312. package/dist/web-experimental.js +1 -1
  313. package/dist/web-experimental.js.map +1 -1
  314. package/dist/web-mod-agent.js +2 -2
  315. package/dist/web-mod-agent.js.map +1 -1
  316. package/dist/web-office-acts.d.ts +173 -0
  317. package/dist/web-office-acts.js +285 -0
  318. package/dist/web-office-acts.js.map +1 -0
  319. package/dist/web-office-skins.d.ts +109 -0
  320. package/dist/web-office-skins.js +299 -0
  321. package/dist/web-office-skins.js.map +1 -0
  322. package/dist/web-office-store.d.ts +8 -2
  323. package/dist/web-office-store.js +77 -14
  324. package/dist/web-office-store.js.map +1 -1
  325. package/dist/web-settings-patch.d.ts +8 -1
  326. package/dist/web-settings-patch.js +65 -5
  327. package/dist/web-settings-patch.js.map +1 -1
  328. package/dist/web-shell-routes.d.ts +4 -2
  329. package/dist/web-shell-routes.js +17 -7
  330. package/dist/web-shell-routes.js.map +1 -1
  331. package/dist/working-recovery.d.ts +104 -0
  332. package/dist/working-recovery.js +298 -0
  333. package/dist/working-recovery.js.map +1 -0
  334. package/dist/working-state-journal.d.ts +5 -1
  335. package/dist/working-state-journal.js +31 -3
  336. package/dist/working-state-journal.js.map +1 -1
  337. package/package.json +3 -2
  338. package/templates/windows-shortcuts.ps1 +155 -140
@@ -1,7 +1,7 @@
1
1
  // #1939 S2a: Settings → Web → Web authority — whether a signed-in admin's web messages carry their account's authority
2
2
  // in delivery receipts (docs/design/1939-verified-web-sessions.md §0, §0.1, §3, §10): the server switch (on by default),
3
3
  // "Only this computer can authorize", how long a session's authorization lasts, the sessions authorized now, and "Revoke
4
- // web authority". Changes go through Review changes → Apply like every Settings edit, and apply at once (§0.1: none of
4
+ // web authority". #2347: an edit is unsaved at once and saved by the Settings bar's Save like every Settings edit, and applies at once (§0.1: none of
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";
@@ -9,8 +9,9 @@ import { useTypedText } from "/assets/text-undo.js";
9
9
  import { toast } from "/assets/ui-toast.js";
10
10
  import { confirmDialog } from "/assets/ui-confirm.js";
11
11
  import { api } from "./settings-confirm.js";
12
- import { Feedback } from "./settings-dialogs.js";
13
- import { DEFAULT_SCHEMA, impactOf } from "./settings-model.js";
12
+ import { Feedback, InEffect } from "./settings-dialogs.js";
13
+ import { DEFAULT_SCHEMA, impactOf, sameValue } from "./settings-model.js";
14
+ import { editsOf, keepDraft, syncStage } from "./settings-draft.js";
14
15
  import { resetWebAuthorization } from "/assets/web-authorize.js";
15
16
  import "./settings-strings.js";
16
17
 
@@ -24,12 +25,14 @@ export function verifiedAuthorityBody(original, f) {
24
25
  if (f.hours !== original.hours) out.max_session_hours = f.hours === null ? null : f.hours;
25
26
  // #2453 (§0.2): off by default; turning it on is confirmed in a General or on the host.
26
27
  if (f.namedLink !== original.namedLink) out.named_link = f.namedLink ? true : null;
28
+ // §0.3: off by default (the user's decision); turning it on is confirmed in a General or on the host.
29
+ if (f.quickLink !== original.quickLink) out.quick_link = f.quickLink ? true : null;
27
30
  return Object.keys(out).length ? out : null;
28
31
  }
29
32
 
30
33
  export function WebAuthorityCard({ ctx, staged, disabled = false }) {
31
34
  const [overview, setOverview] = useState(null), [failed, setFailed] = useState(false);
32
- const [original, setOriginal] = useState(null), [f, setF] = useState(null), [msg, setMsg] = useState(null);
35
+ const [original, setOriginal] = useState(null), [f, setF] = useState(null);
33
36
  // #2342: the hours box keeps what was typed ("01", "1.50"; its undo history); the form gets the number.
34
37
  const [hoursText, onHoursText] = useTypedText(f ? f.hours : null, (v) => (v.trim() === "" ? null : Number(v)));
35
38
  const load = async () => {
@@ -42,12 +45,24 @@ export function WebAuthorityCard({ ctx, staged, disabled = false }) {
42
45
  if (!o || typeof o !== "object" || !o.state || !o.hours || !Array.isArray(o.lanHosts) || !Array.isArray(o.authorized)) { setFailed(true); return; }
43
46
  const c = o.configured || {};
44
47
  const orig = { enabled: c.enabled !== false, loopbackOnly: c.loopback_only === true, hours: typeof c.max_session_hours === "number" ? c.max_session_hours : null,
45
- namedLink: c.named_link === true };
48
+ namedLink: c.named_link === true, quickLink: c.quick_link === true };
46
49
  setOverview(o); setFailed(false);
47
50
  setOriginal((prev) => prev || orig);
48
- setF((prev) => prev || { ...orig });
51
+ // #2347: a draft kept from before this card was last shown (still unsaved) comes back over what is saved now — only
52
+ // its edited fields (#2367 review: a field never touched follows the configuration, another admin's change too).
53
+ setF((prev) => prev || { ...orig, ...((ctx.drafts && ctx.drafts.get("verified_authority")) || {}) });
49
54
  };
50
55
  useEffect(() => { load(); }, []);
56
+ // #2347: every edit is unsaved at once — kept as the draft and staged (an invalid hours box: Save waits for it).
57
+ useEffect(() => {
58
+ if (!f || !original || !overview || disabled) return;
59
+ keepDraft(ctx, "verified_authority", editsOf(f, original), f);
60
+ const bad = f.hours !== null && (!Number.isFinite(f.hours) || f.hours < overview.hours.min || f.hours > overview.hours.max);
61
+ if (bad) { syncStage(ctx, "verified_authority", { label: tn("vaTitle"), invalid: tn("vaHoursRange"), section: "access", target: "#vaHours" }); return; }
62
+ const body = verifiedAuthorityBody(original, f);
63
+ syncStage(ctx, "verified_authority", body ? { label: tn("vaTitle"), count: Object.keys(body).length,
64
+ impact: impactOf(ctx.schema || DEFAULT_SCHEMA, "web.verified_authority"), request: { method: "PUT", url: "/api/settings/fleet/web", body: { verified_authority: body } } } : null);
65
+ }, [f, original, overview]);
51
66
  const card = (body) => html`<section class="card s-web-authority"><h3>${tn("vaTitle")}</h3><p class="note">${tn("vaHint")}</p>${body}</section>`;
52
67
  if (failed) return card(html`<p class="note" role="status">${tn("vaLoadFailed")} <button type="button" class="btn btn-sm" onClick=${load}>${t("app.retry")}</button></p>`);
53
68
  if (!overview || !f) return card(html`<p class="note">${t("app.loading")}</p>`);
@@ -58,13 +73,7 @@ export function WebAuthorityCard({ ctx, staged, disabled = false }) {
58
73
  const warnHosts = !f.loopbackOnly && overview.lanHosts.length > 0;
59
74
  const when = (ms) => new Date(ms).toLocaleString();
60
75
 
61
- const review = () => {
62
- if (hoursError) return;
63
- const body = verifiedAuthorityBody(original, f);
64
- 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 } } });
65
- else if (staged.has("verified_authority")) ctx.unstage("verified_authority");
66
- setMsg(tn("readyApply"));
67
- };
76
+ const fc = (cls, key) => `${cls}${!sameValue(f[key], original[key]) ? " s-dirty" : ""}`;
68
77
  const revoke = async () => {
69
78
  const yes = await confirmDialog({ title: tn("vaRevokeTitle"), message: tn("vaRevokeConfirm"), confirmLabel: tn("vaRevoke"), danger: true });
70
79
  if (!yes) return;
@@ -76,24 +85,26 @@ export function WebAuthorityCard({ ctx, staged, disabled = false }) {
76
85
 
77
86
  return card(html`<fieldset class="s-va-fields" disabled=${disabled}>
78
87
  ${stuckOff ? html`<p class="note s-va-warn" role="status" id="vaStuckOff">${tn("vaStuckOff")}</p>` : null}
79
- <label class="check"><input id="vaEnabled" type="checkbox" checked=${f.enabled} onChange=${(e) => set({ enabled: e.target.checked })} /><span>${tn("vaEnabled")}</span></label>
88
+ <label class=${fc("check", "enabled")}><input id="vaEnabled" type="checkbox" checked=${f.enabled} onChange=${(e) => set({ enabled: e.target.checked })} /><span>${tn("vaEnabled")}</span></label>
80
89
  <p class="note">${tn("vaEnabledHint")}</p>
81
- <label class="check"><input id="vaLoopbackOnly" type="checkbox" checked=${f.loopbackOnly} onChange=${(e) => set({ loopbackOnly: e.target.checked })} /><span>${tn("vaLoopbackOnly")}</span></label>
90
+ <label class=${fc("check", "loopbackOnly")}><input id="vaLoopbackOnly" type="checkbox" checked=${f.loopbackOnly} onChange=${(e) => set({ loopbackOnly: e.target.checked })} /><span>${tn("vaLoopbackOnly")}</span></label>
82
91
  <p class="note">${tn("vaLoopbackOnlyHint")}</p>
83
92
  ${warnHosts ? html`<p class="note s-va-warn" role="status" id="vaLanWarning">${tn("vaLanWarning", overview.lanHosts.join(", "))}</p>` : null}
84
- ${overview.namedHost || original.namedLink ? html`<label class="check"><input id="vaNamedLink" type="checkbox" checked=${f.namedLink} onChange=${(e) => set({ namedLink: e.target.checked })} /><span>${tn("vaNamedLink", overview.namedHost || "—")}</span></label>
93
+ ${overview.namedHost || original.namedLink ? html`<label class=${fc("check", "namedLink")}><input id="vaNamedLink" type="checkbox" checked=${f.namedLink} onChange=${(e) => set({ namedLink: e.target.checked })} /><span>${tn("vaNamedLink", overview.namedHost || "—")}</span></label>
85
94
  <p class="note">${tn("vaNamedLinkHint")}</p>` : null}
86
- <div class="field"><label for="vaHours">${tn("vaHours", overview.hours.min, overview.hours.max)}</label>
87
- <input id="vaHours" type="number" min=${overview.hours.min} max=${overview.hours.max} step="any" placeholder=${tn("defaultHours", overview.hours.default)}
95
+ ${/* §0.3: always shown — any fleet can open the temporary public link with /web. */ ""}
96
+ <label class=${fc("check", "quickLink")}><input id="vaQuickLink" type="checkbox" checked=${f.quickLink} onChange=${(e) => set({ quickLink: e.target.checked })} /><span>${tn("vaQuickLink")}</span></label>
97
+ <p class="note">${tn("vaQuickLinkHint")}</p>
98
+ <div class=${fc("field", "hours")}><label for="vaHours">${tn("vaHours", overview.hours.min, overview.hours.max)}</label>
99
+ <input id="vaHours" type="number" min=${overview.hours.min} max=${overview.hours.max} step="any"
88
100
  class=${hoursError ? "invalid" : ""} value=${hoursText} onInput=${(e) => set({ hours: onHoursText(e.target.value) })} />
89
- <${Feedback} error=${hoursError} /></div>
101
+ <${Feedback} error=${hoursError} />
102
+ ${hoursText.trim() === "" ? html`<${InEffect} text=${tn("hoursValue", overview.hours.default)} />` : null}</div>
90
103
  <h4>${tn("vaSessionsTitle")}</h4>
91
104
  ${overview.authorized.length
92
105
  ? html`<ul class="s-va-sessions" id="vaSessions">${overview.authorized.map((a) => html`<li key=${a.handle} data-handle=${a.handle}>
93
- <strong>${a.label}</strong> <span class="note">${a.account}</span>${a.named ? html` <span class="note s-va-named">${tn("vaSessionNamed")}</span>` : null}<br /><span class="note">${tn("vaSessionUntil", when(a.authorizedAt), when(a.until))}</span></li>`)}</ul>`
106
+ <strong>${a.label}</strong> <span class="note">${a.account}</span>${a.named ? html` <span class="note s-va-named">${tn("vaSessionNamed")}</span>` : null}${a.quick ? html` <span class="note s-va-named s-va-quick">${tn("vaSessionQuick")}</span>` : null}<br /><span class="note">${tn("vaSessionUntil", when(a.authorizedAt), when(a.until))}</span></li>`)}</ul>`
94
107
  : html`<p class="note" id="vaSessionsNone">${tn("vaSessionsNone")}</p>`}
95
108
  </fieldset>
96
- <div class="save-row"><button type="button" class="btn btn-primary" id="vaReview" disabled=${disabled || !!hoursError} onClick=${review}>${tn("vaReview")}</button>
97
- <button type="button" class="btn btn-danger" id="vaRevoke" disabled=${disabled} onClick=${revoke}>${tn("vaRevoke")}</button>
98
- ${msg ? html`<span class="note" role="status">${msg}</span>` : null}</div>`);
109
+ <div class="save-row"><button type="button" class="btn btn-danger" id="vaRevoke" disabled=${disabled} onClick=${revoke}>${tn("vaRevoke")}</button></div>`);
99
110
  }
@@ -51,6 +51,12 @@ export function setLang(next, opts = {}) {
51
51
  }
52
52
  export function onLang(fn) { listeners.add(fn); return () => listeners.delete(fn); }
53
53
 
54
+ /** #2347: a string in a given language (settings search matches the labels of both), English when it has none. */
55
+ export function tIn(language, key) {
56
+ const table = tables[language] || tables.en;
57
+ return table[key] ?? tables.en[key] ?? key;
58
+ }
59
+
54
60
  /** "{0} is working…" with its values filled in. Text only: callers render it as text, never as markup. */
55
61
  export function t(key, ...values) {
56
62
  let s = tables[current][key] ?? tables.en[key] ?? key;
@@ -87,14 +93,19 @@ register("app", {
87
93
  opWriting: "Applying settings… {0}/{1}", opWaiting: "Settings: waiting for confirmation in chat", opApplying: "Applying settings…",
88
94
  opDone: "Settings applied", opRestart: "Settings saved — AgEnD needs a restart to use them", opPartial: "Some settings were not applied",
89
95
  opFailed: "Settings were not applied", openSettings: "Open Settings",
96
+ restartWaiting: "{0} saved change(s) take effect after AgEnD restarts", restartNow: "Restart now", restartLater: "Later",
97
+ restartConfirm: "Restart AgEnD now?\n\nEvery agent stops and starts again, work in progress is interrupted, and the panel is unreachable for up to a minute. A notice is posted to your chat channel.",
98
+ restartOnHost: "Run agend restart on the host to use them.",
99
+ restartRefused: "Restart refused: {0}",
90
100
  pendingSent: "Sent for confirmation — a fleet admin confirms it in chat.",
91
101
  pendingSelfSent: "Confirmed by your account (this browser is web-verified) — applying.",
92
102
  needsNav: "Needs you", needsTab: "Needs you", needsCount: "{0} waiting",
93
103
  // #2293: global search — chats, cross-instance messages and tasks (not the sidebar's filter of agents).
94
104
  search: "Search", searchTitle: "Search chats, messages between agents and tasks ({0})",
95
105
  confirmTitle: "Please confirm", confirmOk: "Continue", cancel: "Cancel",
106
+ sidePanelLeave: "Leave {0} without saving? Your changes there will be lost.", sidePanelDiscard: "Discard changes",
96
107
  // 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", needs_reason_task_review: "Waiting for your confirmation", needs_reason_task_blocked: "Blocked: asks you",
108
+ 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_backend_signin: "Backend 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
109
  },
99
110
  "zh-TW": {
100
111
  brand: "AgEnD", fleet: "Fleet", fleetOverview: "Fleet 概覽", view: "檢視", settings: "設定", instances: "Instances", newInstance: "新增 instance", newAgent: "新增 Agent",
@@ -124,11 +135,16 @@ register("app", {
124
135
  opWriting: "正在套用設定… {0}/{1}", opWaiting: "設定:等待在聊天中確認", opApplying: "正在套用設定…",
125
136
  opDone: "設定已套用", opRestart: "設定已儲存——需要重新啟動 AgEnD 才會生效", opPartial: "部分設定沒有套用",
126
137
  opFailed: "設定沒有套用", openSettings: "開啟設定",
138
+ restartWaiting: "{0} 項已儲存的變更要在 AgEnD 重新啟動後才會生效", restartNow: "立即重新啟動", restartLater: "稍後",
139
+ restartConfirm: "現在重新啟動 AgEnD?\n\n所有 agent 都會停止再啟動,進行中的工作會被中斷,面板會有最多約一分鐘無法連線。聊天頻道會收到一則通知。",
140
+ restartOnHost: "請在主機上執行 agend restart 讓它們生效。",
141
+ restartRefused: "重新啟動被拒絕:{0}",
127
142
  pendingSent: "已送出確認——由 fleet 管理員在聊天中確認。",
128
143
  pendingSelfSent: "已由你的帳號確認(這個瀏覽器已通過網頁驗證)——套用中。",
129
144
  needsNav: "等你處理", needsTab: "待處理", needsCount: "{0} 件待處理",
130
145
  search: "搜尋", searchTitle: "搜尋聊天、agent 之間的訊息與任務({0})",
131
146
  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: "傳送失敗", needs_reason_task_review: "等你確認", needs_reason_task_blocked: "卡住,在問你",
147
+ sidePanelLeave: "不儲存就離開「{0}」?你在那裡的修改會遺失。", sidePanelDiscard: "捨棄修改",
148
+ 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_backend_signin: "後端需要登入", needs_reason_crashed: "已崩潰", needs_reason_delivery_uncertain: "無法確認是否送達", needs_reason_delivery_failed: "傳送失敗", needs_reason_task_review: "等你確認", needs_reason_task_blocked: "卡住,在問你",
133
149
  },
134
150
  });
@@ -5,7 +5,7 @@
5
5
  // can no longer land.
6
6
  // A panel with work that leaving would lose (Settings' staged changes, §5) sets a leave guard: every navigation of
7
7
  // the app asks it first, Back/Forward included (the address bar is put back when it says no).
8
- import { parseRoute, legacyHashTarget } from "./app-route.js";
8
+ import { parseRoute, legacyHashTarget, legacySettingsTarget } from "./app-route.js";
9
9
  import { createStore } from "./app-store.js";
10
10
 
11
11
  export const navStore = createStore({ route: null, seq: 0 });
@@ -28,7 +28,9 @@ const mayLeave = (route, path) => !guard || guard(route, path) !== false;
28
28
  /** Start following the address bar. An old /ui#instance=<name> link becomes /ui/chat/<name> before the first render. */
29
29
  export function startRouter(w = window) {
30
30
  win = w;
31
- const legacy = legacyHashTarget(w.location.pathname, w.location.hash);
31
+ let legacy = legacyHashTarget(w.location.pathname, w.location.hash) || legacySettingsTarget(w.location.pathname);
32
+ // An old Settings address with a place of its own (/settings/web#guests) keeps it when its new one names none.
33
+ if (legacy && w.location.hash && !legacy.includes("#") && legacySettingsTarget(w.location.pathname)) legacy += w.location.hash;
32
34
  if (legacy) w.history.replaceState(null, "", legacy);
33
35
  here = w.location.pathname + w.location.search;
34
36
  navStore.set({ route: parseRoute(w.location.pathname), seq: 1 });
@@ -44,12 +46,15 @@ export function startRouter(w = window) {
44
46
 
45
47
  /** Go to `path`. A path that is not a route of the app is a full load. `force` skips the leave guard (it said yes). */
46
48
  export function navigate(path, opts = {}) {
47
- const url = new URL(path, win.location.href);
49
+ let url = new URL(path, win.location.href);
50
+ // #2347: an old Settings address goes to its section's new one.
51
+ const moved = url.origin === win.location.origin ? legacySettingsTarget(url.pathname) : null;
52
+ if (moved) { const hash = url.hash; url = new URL(moved, win.location.href); if (!url.hash && hash) url.hash = hash; }
48
53
  const route = parseRoute(url.pathname);
49
54
  if (!route || url.origin !== win.location.origin) { win.location.assign(url.href); return; }
50
55
  if (!opts.force && !mayLeave(route, url.pathname + url.search)) return;
51
- if (url.pathname + url.search !== win.location.pathname + win.location.search) {
52
- win.history[opts.replace ? "replaceState" : "pushState"](null, "", url.pathname + url.search);
56
+ if (url.pathname + url.search !== win.location.pathname + win.location.search || (url.hash && url.hash !== win.location.hash)) {
57
+ win.history[opts.replace ? "replaceState" : "pushState"](null, "", url.pathname + url.search + url.hash);
53
58
  }
54
59
  here = url.pathname + url.search;
55
60
  navStore.set(s => ({ route, seq: s.seq + 1 }));
@@ -81,6 +81,16 @@ const inflight = new Map(); // key → writes sent and not answered yet
81
81
  let flushTimer = null;
82
82
  let writes = Promise.resolve(); // one write at a time, in the order they were made
83
83
  const listeners = new Set();
84
+ // #2347: the "You" group's preferences (Settings → Preferences, Look) are saved by the Settings bar's Save like
85
+ // every Settings change. While Settings is open (holdPrefs), a choice there is shown on this page at once (cached, and
86
+ // this page's own in prefInForce) but not sent: `held` keeps it until savePrefsHeld sends them all, or discardHeldPrefs
87
+ // puts back what the host has (the same path as another device's change: apply and onPrefsChanged).
88
+ // Only keys Settings itself changes (a card's own size, card.sizeByInstance, is set from the card, outside Settings).
89
+ export const HELD_KEYS = Object.freeze(new Set(["theme", "lang", "names.primary", "preview.optIn", "fx.show", "color.show", "color.off", "card.size"]));
90
+ let holding = 0;
91
+ const held = new Map(); // key → value chosen in Settings, shown here, not saved yet
92
+ const heldFrom = new Map(); // key → its cached form before it was held (a choice back to it is no change)
93
+ const heldListeners = new Set();
84
94
  // #2193: whether the signed-in person has a personal effect slot chosen (their fx.* keys: in the page's data-prefs and
85
95
  // in every host read, never in this host cache). The effects runtime is loaded only when there is something to play.
86
96
  const ownerEffectsIn = (values) => !!values && OWNER_EFFECT_PREF_KEYS.some((k) => own(values, k) && values[k] != null && values[k] !== "");
@@ -141,6 +151,7 @@ function take(body) {
141
151
  */
142
152
  export function savePref(key, value) {
143
153
  if (!hostKey(key)) return;
154
+ if (holding && HELD_KEYS.has(key)) { hold(key, value); return; }
144
155
  cache(key, value);
145
156
  if (!prefsWritable()) return;
146
157
  pending.set(key, value);
@@ -149,6 +160,72 @@ export function savePref(key, value) {
149
160
  flushTimer = setTimeout(flush, 400);
150
161
  }
151
162
 
163
+ function hold(key, value) {
164
+ const k = SYNCED[key];
165
+ // What was in force before, as cached: never read back from this browser's copy, which the caller may have written
166
+ // already (theme.js stores the theme as it sets it).
167
+ if (!heldFrom.has(key)) { const f = prefInForce(key); heldFrom.set(key, f ? (f.value == null ? null : k.toLocal(f.value)) : readLocal(k.ls)); }
168
+ cache(key, value);
169
+ if ((value == null ? null : k.toLocal(value)) === heldFrom.get(key)) { // back to what it was: no change
170
+ held.delete(key); heldFrom.delete(key);
171
+ if (!pending.has(key) && !inflight.has(key)) mine.delete(key);
172
+ } else { held.set(key, value); mine.set(key, value); }
173
+ notifyHeld();
174
+ }
175
+ function notifyHeld() { for (const fn of [...heldListeners]) { try { fn(held.size); } catch { /* the rest still hear */ } } }
176
+
177
+ /**
178
+ * #2347: Settings is open — a "You" preference chosen now is held (shown, not saved) until savePrefsHeld or
179
+ * discardHeldPrefs. Returns the release; held choices left when the last hold goes (Settings closed without either) are
180
+ * put back.
181
+ */
182
+ export function holdPrefs() {
183
+ holding++;
184
+ let released = false;
185
+ return () => {
186
+ if (released) return;
187
+ released = true;
188
+ holding--;
189
+ // A panel shown again at once (a language switch remounts it) holds again before this runs.
190
+ setTimeout(() => { if (!holding && held.size) discardHeldPrefs(); }, 0);
191
+ };
192
+ }
193
+ /** How many "You" preferences are chosen and not saved yet. */
194
+ export function heldPrefsCount() { return held.size; }
195
+ /** Called with that count whenever it changes. Returns the unsubscribe. */
196
+ export function onHeldPrefs(fn) { heldListeners.add(fn); return () => heldListeners.delete(fn); }
197
+ /**
198
+ * Save every held choice (one write). Resolves when it has been answered; rejects when the host did not take it (what
199
+ * the host has is shown again). Not writable (the public link): this browser only, as before.
200
+ */
201
+ export async function savePrefsHeld() {
202
+ const keys = [...held.keys()];
203
+ for (const key of keys) { if (prefsWritable()) pending.set(key, held.get(key)); else mine.delete(key); }
204
+ held.clear(); heldFrom.clear();
205
+ notifyHeld();
206
+ if (!keys.length || !prefsWritable()) return;
207
+ clearTimeout(flushTimer);
208
+ if ((await flush()) === false) throw new Error("preferences not saved");
209
+ }
210
+ /** Put back what the host has for every held choice, on this page (theme, language, … and every onPrefsChanged reader). */
211
+ export function discardHeldPrefs() {
212
+ const keys = [...held.keys()];
213
+ held.clear(); heldFrom.clear();
214
+ for (const key of keys) if (!pending.has(key) && !inflight.has(key)) mine.delete(key);
215
+ notifyHeld();
216
+ if (keys.length) align(keys, { force: true });
217
+ }
218
+ /** Run `fn` with nothing held (a confirmed action, such as Restore defaults, saves at once); its keys are held no more. */
219
+ export function withoutHold(fn) {
220
+ const h = holding;
221
+ holding = 0;
222
+ try { return fn(); } finally {
223
+ holding = h;
224
+ for (const key of [...held.keys()]) if (pending.has(key)) { held.delete(key); heldFrom.delete(key); }
225
+ notifyHeld();
226
+ }
227
+ }
228
+
152
229
  /** One write. ok: the host took it (its answer is a view; an older one than this page has is not taken). */
153
230
  async function post(set, onlyIfUnset) {
154
231
  const res = await fetch("/ui/prefs", { method: "POST", credentials: "same-origin", headers: { "Content-Type": "application/json" },
@@ -173,7 +250,7 @@ function flush() {
173
250
  for (const k of keys) {
174
251
  const n = (inflight.get(k) || 1) - 1;
175
252
  if (n) inflight.set(k, n); else inflight.delete(k);
176
- if (!n && !pending.has(k)) { mine.delete(k); released.add(k); }
253
+ if (!n && !pending.has(k) && !held.has(k)) { mine.delete(k); released.add(k); }
177
254
  }
178
255
  // Each key whose last write this was, and not changed here again since, now shows the newest host view this page
179
256
  // has — usually its own value; another device's newer one if that arrived while the write was out (held back then,
@@ -182,6 +259,7 @@ function flush() {
182
259
  // the host (a storage that refused the choice) says nothing about the screen.
183
260
  if (!ok) await refresh();
184
261
  align(released, { force: true });
262
+ return ok;
185
263
  });
186
264
  return writes;
187
265
  }
@@ -214,7 +292,7 @@ function align(keys, opts = {}) {
214
292
  const changed = new Set();
215
293
  for (const key of keys) {
216
294
  if (!hostKey(key)) continue;
217
- if (pending.has(key) || inflight.has(key)) continue;
295
+ if (pending.has(key) || inflight.has(key) || held.has(key)) continue;
218
296
  const k = SYNCED[key];
219
297
  const want = own(host, key) ? k.toLocal(host[key]) : null;
220
298
  const copyDiffers = readLocal(k.ls) !== want;
@@ -1,8 +1,17 @@
1
1
  // #1408 §3: the app's routes on the client. The same table as the server's classifier (src/web-shell-routes.ts;
2
2
  // a test runs both over one list of cases). Pure: no DOM, no history.
3
3
 
4
- export const FLEET_TABS = ["tasks", "schedules", "teams", "org", "cache", "usage", "config"];
5
- export const SETTINGS_SECTIONS = ["device", "appearance", "agents", "bots", "classic", "general", "web", "advanced"]; // #1604: device first
4
+ // #2347 §10: Config is gone from Fleet — each of its settings has one home in Settings (/ui/fleet/config leads there).
5
+ export const FLEET_TABS = ["tasks", "schedules", "teams", "org", "cache", "usage"];
6
+ // #2347: two groups — "You" changes only you or this browser; "Fleet" changes AgEnD for everyone.
7
+ export const SETTINGS_GROUPS = Object.freeze([
8
+ Object.freeze({ id: "you", sections: Object.freeze(["preferences", "look"]) }),
9
+ Object.freeze({ id: "fleet", sections: Object.freeze(["agents", "agent-defaults", "chat", "web-dashboard", "access", "advanced"]) }),
10
+ ]);
11
+ export const SETTINGS_SECTIONS = Object.freeze(SETTINGS_GROUPS.flatMap((g) => g.sections));
12
+ /** #2347: the sections' names before the regrouping (links and bookmarks keep working) and where each one went. */
13
+ export const LEGACY_SETTINGS = Object.freeze({ device: "preferences", appearance: "look", general: "agent-defaults", bots: "chat", classic: "chat#classicbot", web: "access" });
14
+ const legacySection = (name) => (Object.prototype.hasOwnProperty.call(LEGACY_SETTINGS, name) ? LEGACY_SETTINGS[name] : null);
6
15
  const MAX_INSTANCE_NAME = 128;
7
16
  const PATH_SEPARATOR_OR_CONTROL = /[/\\\u0000-\u001f\u007f]/;
8
17
 
@@ -25,7 +34,7 @@ export function parseRoute(pathname) {
25
34
  let m = /^\/ui\/fleet\/agent\/([^/]+)$/.exec(pathname);
26
35
  if (m) return named("details", m[1]);
27
36
  m = /^\/ui\/fleet\/([^/]+)$/.exec(pathname);
28
- if (m) return FLEET_TABS.includes(m[1]) ? { panel: "fleet", tab: m[1] } : null;
37
+ if (m) return FLEET_TABS.includes(m[1]) ? { panel: "fleet", tab: m[1] } : m[1] === "config" ? { panel: "settings", section: "agent-defaults" } : null;
29
38
  m = /^\/ui\/chat\/([^/]+)$/.exec(pathname);
30
39
  if (m) return named("chat", m[1]);
31
40
  // #1218: one instance's Log.
@@ -36,7 +45,11 @@ export function parseRoute(pathname) {
36
45
  if (m) return named("view", m[1]);
37
46
  if (pathname === "/settings") return { panel: "settings", section: "agents" };
38
47
  m = /^\/settings\/([^/]+)$/.exec(pathname);
39
- if (m) return SETTINGS_SECTIONS.includes(m[1]) ? { panel: "settings", section: m[1] } : null;
48
+ if (m) {
49
+ if (SETTINGS_SECTIONS.includes(m[1])) return { panel: "settings", section: m[1] };
50
+ const moved = legacySection(m[1]);
51
+ return moved ? { panel: "settings", section: moved.split("#")[0] } : null;
52
+ }
40
53
  return null;
41
54
  }
42
55
  function named(panel, segment) {
@@ -69,7 +82,28 @@ export function rememberFleetTab(tab) {
69
82
  try { localStorage.setItem(LAST_FLEET_TAB_KEY, tab); } catch { /* this page only */ }
70
83
  }
71
84
  export const NEEDS_PATH = "/ui/needs";
72
- export function settingsPath(section) { return section && section !== "agents" ? `/settings/${section}` : "/settings"; }
85
+ /** A section's address; an old name gives its new one (and the place in it: `chat#classicbot`). */
86
+ export function settingsPath(name) {
87
+ const section = (name && legacySection(name)) || name;
88
+ return section && section !== "agents" ? `/settings/${section}` : "/settings";
89
+ }
90
+ /**
91
+ * #2347 §7: a section's own address, always with its name (`/settings/agents` too). On a phone `/settings` is the list of
92
+ * sections, so going *to a section* (the nav, search, a field to fix, "Edit in Settings") uses this; `settingsPath()` is
93
+ * Settings itself (the app's menu and tabs).
94
+ */
95
+ export function sectionPath(name) {
96
+ const path = settingsPath(name);
97
+ return path === "/settings" ? "/settings/agents" : path;
98
+ }
99
+ /** #2347: an old /settings/<name> address as its new one (with the place in it), or null. */
100
+ export function legacySettingsTarget(pathname) {
101
+ // #2347 §10: Fleet → Config's settings live in Settings now; its project folders are in Agent defaults.
102
+ if (pathname === "/ui/fleet/config") return "/settings/agent-defaults#project-roots";
103
+ const m = /^\/settings\/([^/]+)$/.exec(pathname || "");
104
+ const moved = m && legacySection(m[1]);
105
+ return moved ? settingsPath(moved) : null;
106
+ }
73
107
  export function routePath(route) {
74
108
  if (!route) return "/ui";
75
109
  if (route.panel === "fleet") return fleetPath(route.tab);
@@ -4,7 +4,7 @@
4
4
  import { html, useEffect, useLayoutEffect, useRef, useState } from "./app-html.js";
5
5
  import { Icon } from "./ui-icons.js";
6
6
  import { Toasts } from "./ui-toast.js";
7
- import { ConfirmHost } from "./ui-confirm.js";
7
+ import { ConfirmHost, confirmDialog } from "./ui-confirm.js";
8
8
  import { ColorDialogHost } from "./identity-color.js";
9
9
  import { t, lang, setLang, onLang } from "./app-i18n.js";
10
10
  import { appStore, createStore, guestBadge, guestPage, useStore } from "./app-store.js";
@@ -16,7 +16,8 @@ import { onPrefsChanged, savePref } from "./app-prefs.js";
16
16
  import { applyPageTheme, pageNow, resolvedMode, themesStore } from "./app-themes.js";
17
17
  import { ErrorState, Skeleton } from "./ui-states.js";
18
18
  import { InstanceNav, RosterNav, namesStore, setNamesPrimary } from "./instance-nav.js";
19
- import { UsageRail, usageRailStore } from "./header-tools.js";
19
+ import { usageRailStore } from "./header-tools.js";
20
+ import { RightRail, sidePanelStore } from "./side-panel.js";
20
21
  import { OwnerCssKeepBar } from "./owner-css.js";
21
22
  import { AgentWidget, fleetBinds, sidebarWidget, sidebarWidgetStore } from "./agent-widget.js";
22
23
 
@@ -89,14 +90,17 @@ export function openSearch(preset = {}) {
89
90
  }
90
91
  /**
91
92
  * Ctrl+K (⌘K) anywhere opens search; Ctrl+F (⌘F) in an agent's chat opens it narrowed to that agent — elsewhere the
92
- * browser's own find stays. Not while another dialog is open (search, open already, has its input focused).
93
+ * browser's own find stays. Not while another dialog is open (search, open already, has its input focused); a docked
94
+ * side panel (#2585) is not one.
93
95
  */
94
96
  export function handleSearchKey(e) {
95
97
  if (!searchOffered(true)) return;
96
98
  if (e.isComposing || e.altKey || e.shiftKey || !(isMacLike() ? e.metaKey : e.ctrlKey)) return;
97
99
  const key = (e.key || "").toLowerCase();
98
100
  if (key !== "k" && key !== "f") return;
99
- if (typeof document !== "undefined" && document.querySelector("dialog[open]")) return;
101
+ // #2585: a docked side panel is not modal — the page beside it is in use, so search is too (a result that leaves
102
+ // the page asks first when the panel holds changes: side-panel.js).
103
+ if (typeof document !== "undefined" && document.querySelector("dialog[open]:not(.side-panel)")) return;
100
104
  const route = navStore.get().route;
101
105
  if (key === "f" && !(route && route.panel === "chat" && route.instance)) return;
102
106
  e.preventDefault();
@@ -127,7 +131,7 @@ export const sidebarWidthStore = createStore({ width: null });
127
131
  function mainBudget() {
128
132
  const vw = viewportWidth();
129
133
  if (vw < 1200 || typeof document === "undefined") return vw;
130
- const rail = document.querySelector(".u-rail");
134
+ const rail = document.querySelector(".shell > .u-rail, .shell > .side-panel"); // #2585: a docked panel in its place
131
135
  if (!rail) return vw;
132
136
  const w = Math.round(rail.getBoundingClientRect ? rail.getBoundingClientRect().width : 0) || RAIL_W;
133
137
  return vw - w;
@@ -181,11 +185,15 @@ function SidebarResize() {
181
185
  // once the page has drawn its change.
182
186
  const again = () => afterPaint(applyKeptSidebarWidth);
183
187
  const offRail = usageRailStore.subscribe(again);
188
+ // #2585: a docked panel in the rail's place (it is published on every render of its editor: only its coming and
189
+ // going changes the room).
190
+ let docked = !!sidePanelStore.get().panel;
191
+ const offPanel = sidePanelStore.subscribe(() => { const now = !!sidePanelStore.get().panel; if (now !== docked) { docked = now; again(); } });
184
192
  const offNav = navStore.subscribe(again);
185
193
  const w = win();
186
194
  if (w && typeof w.addEventListener === "function") w.addEventListener("resize", applyKeptSidebarWidth);
187
195
  return () => {
188
- offRail(); offNav();
196
+ offRail(); offPanel(); offNav();
189
197
  if (w && typeof w.removeEventListener === "function") w.removeEventListener("resize", applyKeptSidebarWidth);
190
198
  if (activeDrag) activeDrag.end(false); // unmounted mid-drag: nothing kept, nothing left behind
191
199
  };
@@ -415,13 +423,13 @@ export function DevicePrefs({ labels = false } = {}) {
415
423
  useEffect(() => onLang(setL), []);
416
424
  return html`<div class=${`prefs${labels ? " labelled" : ""}`}>
417
425
  <label class="pref"><${Icon} name="sun" size=${16} /><span class=${labels ? "pref-label" : "sr-only"}>${t("app.theme")}</span>
418
- <select value=${theme} aria-label=${t("app.theme")} onChange=${(e) => { setTheme(e.target.value); if (typeof AgendTheme !== "undefined") AgendTheme.set(e.target.value); savePref("theme", e.target.value); }}>
426
+ <select id=${labels ? "pref-theme" : undefined} value=${theme} aria-label=${t("app.theme")} onChange=${(e) => { setTheme(e.target.value); if (typeof AgendTheme !== "undefined") AgendTheme.set(e.target.value); savePref("theme", e.target.value); }}>
419
427
  <option value="system">${t("app.themeSystem")}</option><option value="light">${t("app.themeLight")}</option><option value="dark">${t("app.themeDark")}</option></select></label>
420
428
  <label class="pref"><${Icon} name="globe" size=${16} /><span class=${labels ? "pref-label" : "sr-only"}>${t("app.language")}</span>
421
- <select value=${l} aria-label=${t("app.language")} onChange=${(e) => { setL(e.target.value); setLang(e.target.value); savePref("lang", e.target.value); }}>
429
+ <select id=${labels ? "pref-lang" : undefined} value=${l} aria-label=${t("app.language")} onChange=${(e) => { setL(e.target.value); setLang(e.target.value); savePref("lang", e.target.value); }}>
422
430
  <option value="en">English</option><option value="zh-TW">中文</option></select></label>
423
431
  <label class="pref"><${Icon} name="user" size=${16} /><span class=${labels ? "pref-label" : "sr-only"}>${t("app.names")}</span>
424
- <select value=${names.primary} aria-label=${t("app.names")} onChange=${(e) => setNamesPrimary(e.target.value)}>
432
+ <select id=${labels ? "pref-names" : undefined} value=${names.primary} aria-label=${t("app.names")} onChange=${(e) => setNamesPrimary(e.target.value)}>
425
433
  <option value="nickname">${t("app.namesNickname")}</option><option value="instance">${t("app.namesInstance")}</option></select></label>
426
434
  </div>`;
427
435
  }
@@ -586,6 +594,60 @@ function OperationLine({ route }) {
586
594
  <span>${text}</span> <a class="btn btn-sm" href=${settingsPath()}>${t("app.openSettings")}</a></div>`;
587
595
  }
588
596
 
597
+ /**
598
+ * #2347: saved changes that wait for an AgEnD restart, as the server says (GET /api/settings/restart-pending), into
599
+ * appStore.restartPending ({ keys, jobId } or null). A failed read keeps what was known.
600
+ */
601
+ export async function refreshRestartPending() {
602
+ let body;
603
+ try {
604
+ const r = await fetch("/api/settings/restart-pending");
605
+ if (!r.ok) return;
606
+ body = await r.json();
607
+ } catch { return; }
608
+ const keys = body && Array.isArray(body.keys) ? body.keys.filter((k) => typeof k === "string") : [];
609
+ appStore.set({ restartPending: body && body.pending === true
610
+ ? { keys, jobId: typeof body.job_id === "string" && body.job_id ? body.job_id : null } : null });
611
+ }
612
+
613
+ /**
614
+ * #2347: the app-wide line for saved changes that wait for an AgEnD restart — in every panel, until AgEnD restarts.
615
+ * Restart now asks first and is never automatic (the leader's default: a Save that restarted on its own once took 33
616
+ * agents down at once). Asked again whenever a Save or a restart settles. Left to the Save in hand while it runs or offers
617
+ * its own restart (Settings' card, or the line above in another panel). Over a temporary public link it says so and
618
+ * offers nothing; with no Settings job to carry the restart (a hand edit), it says how to restart on the host.
619
+ */
620
+ function RestartLine() {
621
+ const { restartPending: pending, settingsOp: op, restartRunner } = useStore(appStore);
622
+ const [later, setLater] = useState(null);
623
+ const [busy, setBusy] = useState(false);
624
+ const settled = !op || (!["writing", "posting", "watching"].includes(op.phase) && op.restart !== "busy");
625
+ useEffect(() => { if (settled) refreshRestartPending(); }, [op ? `${op.id}:${op.phase}:${op.restart}` : "none"]);
626
+ if (!pending || !settled) return null;
627
+ if (op && op.job && op.job.targets && op.job.targets.some((r) => r.target === "fleet" && r.status === "restart-required")) return null;
628
+ const seen = `${pending.jobId}|${pending.keys.join(",")}`;
629
+ if (later === seen) return null;
630
+ const canRestart = !!pending.jobId && typeof restartRunner === "function";
631
+ const restart = async () => {
632
+ if (!(await confirmDialog({ title: t("app.restartNow"), message: t("app.restartConfirm"), confirmLabel: t("app.restartNow"), danger: true }))) return;
633
+ setBusy(true);
634
+ let res;
635
+ try { res = await restartRunner(pending.jobId); } catch { res = { ok: false }; }
636
+ setBusy(false);
637
+ if (res && !res.ok && !res.stale && !res.busy) {
638
+ toast(t("app.restartRefused", (res.body && res.body.error) || t("app.failed")), false);
639
+ // #2378 review: a refusal may mean it is done already (restarted from another tab or the host): ask again.
640
+ refreshRestartPending();
641
+ }
642
+ };
643
+ return html`<div data-trust="status" class="conn restart-line lbar" role="status">
644
+ <span>${t("app.restartWaiting", Math.max(1, pending.keys.length))}</span>
645
+ ${pending.keys.length ? html`<span class="muted restart-keys">${pending.keys.join(", ")}</span>` : null}
646
+ ${!pending.jobId ? html`<span class="muted">${t("app.restartOnHost")}</span>` : null}
647
+ ${canRestart ? html`<button type="button" class="btn btn-sm btn-primary" disabled=${busy} onClick=${restart}>${t("app.restartNow")}</button>` : null}
648
+ <button type="button" class="btn btn-sm" onClick=${() => setLater(seen)}>${t("app.restartLater")}</button></div>`;
649
+ }
650
+
589
651
  /**
590
652
  * A module loader that can be tried again: a failed attempt is forgotten (the next call loads afresh) and `attempt`
591
653
  * counts, so a retry can ask for a new URL — a browser may keep a failed module import cached under the old one.
@@ -729,10 +791,11 @@ export function Shell({ panels, onNewInstance, viewOnly = false }) {
729
791
  <${ConnectionLine} />
730
792
  <${StartupLine} />
731
793
  ${viewOnly || guest ? null : html`<${OperationLine} route=${nav.route} />`}
794
+ ${viewOnly || guest ? null : html`<${RestartLine} />`}
732
795
  ${viewOnly || guest || !shell.banner ? null : html`<${shell.banner} />`}
733
796
  <${Outlet} route=${nav.route} seq=${nav.seq} panels=${panels} />
734
797
  </main>
735
- ${guest ? null : html`<${UsageRail} route=${nav.route} />`}
798
+ ${guest ? null : html`<${RightRail} route=${nav.route} />`}
736
799
  <${BottomTabs} route=${nav.route} viewOnly=${viewOnly} />
737
800
  ${shell.dialog ? html`<${shell.dialog.Component} ...${shell.dialog.props} onClose=${closeDialog} />` : null}
738
801
  ${viewOnly || guest || (nav.route && nav.route.panel === "settings") ? null : html`<${PendingChanges} />`}
@@ -35,7 +35,9 @@
35
35
  // #2233: backend_login — a dashboard sign-in started, moved or ended (no details: the page reads its own view).
36
36
  // tasks — the task board changed: the card numbers a write touched (SSE only: the board reads again when it is shown).
37
37
  // offices — the stored offices changed ({ rev }; SSE only: the Office view reads /ui/offices again).
38
- const EVENTS = ["status", "message", "agent_reply_live", "delivery", "deliveries", "prompt", "prompts", "prompt_resolved", "activity", "needs", "reply_buttons", "reactions", "steps_tick", "clock", "traffic", "delegation", "backend_login", "tasks", "offices"];
38
+ // office_skins — a skin pack was uploaded or deleted ({ rev }; SSE only: the Office view reads /ui/office-skins again).
39
+ // office_acts — the agents' live acts in the Office view changed ({ acts }, #2578; a poll page reads /ui/office-acts).
40
+ const EVENTS = ["status", "message", "agent_reply_live", "delivery", "deliveries", "prompt", "prompts", "prompt_resolved", "activity", "needs", "reply_buttons", "reactions", "steps_tick", "clock", "traffic", "delegation", "backend_login", "tasks", "offices", "office_skins", "office_acts"];
39
41
  const MAX_HELD = 1000; // live events held during one catch-up read; more → read again afterwards
40
42
  const RETRY_DELAYS = [1000, 2000, 4000]; // after the first failed catch-up read; then it waits for Retry
41
43
  const POLL_LIMIT_MS = 10000; // a poll that has not answered by then counts as failed