@songsid/agend 2.2.0-alpha.9 → 2.2.0-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 (377) hide show
  1. package/README.md +1 -1
  2. package/README.zh-TW.md +1 -1
  3. package/dist/agent-cli-instructions.md +16 -9
  4. package/dist/agent-cli.js +15 -7
  5. package/dist/agent-cli.js.map +1 -1
  6. package/dist/agent-endpoint.js +21 -9
  7. package/dist/agent-endpoint.js.map +1 -1
  8. package/dist/agent-whats-new.d.ts +47 -0
  9. package/dist/agent-whats-new.js +147 -0
  10. package/dist/agent-whats-new.js.map +1 -0
  11. package/dist/auth-api.d.ts +2 -2
  12. package/dist/auth-api.js +4 -2
  13. package/dist/auth-api.js.map +1 -1
  14. package/dist/backend/antigravity.d.ts +3 -0
  15. package/dist/backend/antigravity.js +9 -0
  16. package/dist/backend/antigravity.js.map +1 -1
  17. package/dist/backend/claude-carry.d.ts +22 -0
  18. package/dist/backend/claude-carry.js +170 -0
  19. package/dist/backend/claude-carry.js.map +1 -0
  20. package/dist/backend/claude-code.d.ts +65 -2
  21. package/dist/backend/claude-code.js +226 -33
  22. package/dist/backend/claude-code.js.map +1 -1
  23. package/dist/backend/codex-metadata.d.ts +33 -0
  24. package/dist/backend/codex-metadata.js +122 -18
  25. package/dist/backend/codex-metadata.js.map +1 -1
  26. package/dist/backend/codex.d.ts +3 -0
  27. package/dist/backend/codex.js +17 -1
  28. package/dist/backend/codex.js.map +1 -1
  29. package/dist/backend/credential-profile.d.ts +40 -3
  30. package/dist/backend/credential-profile.js +113 -5
  31. package/dist/backend/credential-profile.js.map +1 -1
  32. package/dist/backend/effort-metadata.d.ts +1 -1
  33. package/dist/backend/effort-metadata.js +3 -2
  34. package/dist/backend/effort-metadata.js.map +1 -1
  35. package/dist/backend/grok.d.ts +4 -0
  36. package/dist/backend/grok.js +14 -0
  37. package/dist/backend/grok.js.map +1 -1
  38. package/dist/backend/kiro-engine-ledger.d.ts +5 -0
  39. package/dist/backend/kiro-engine-ledger.js +20 -0
  40. package/dist/backend/kiro-engine-ledger.js.map +1 -1
  41. package/dist/backend/kiro-identity.d.ts +6 -0
  42. package/dist/backend/kiro-identity.js +19 -5
  43. package/dist/backend/kiro-identity.js.map +1 -1
  44. package/dist/backend/kiro.d.ts +39 -3
  45. package/dist/backend/kiro.js +143 -19
  46. package/dist/backend/kiro.js.map +1 -1
  47. package/dist/backend/launch-effort.d.ts +33 -0
  48. package/dist/backend/launch-effort.js +123 -0
  49. package/dist/backend/launch-effort.js.map +1 -0
  50. package/dist/backend/muse.d.ts +84 -0
  51. package/dist/backend/muse.js +449 -53
  52. package/dist/backend/muse.js.map +1 -1
  53. package/dist/backend/types.d.ts +33 -4
  54. package/dist/backend/types.js +2 -1
  55. package/dist/backend/types.js.map +1 -1
  56. package/dist/backend-command.d.ts +91 -0
  57. package/dist/backend-command.js +122 -0
  58. package/dist/backend-command.js.map +1 -0
  59. package/dist/background-work-ended.d.ts +18 -0
  60. package/dist/background-work-ended.js +30 -0
  61. package/dist/background-work-ended.js.map +1 -0
  62. package/dist/cache-service.d.ts +2 -0
  63. package/dist/cache-service.js +2 -1
  64. package/dist/cache-service.js.map +1 -1
  65. package/dist/channel/adapters/discord.d.ts +26 -1
  66. package/dist/channel/adapters/discord.js +98 -3
  67. package/dist/channel/adapters/discord.js.map +1 -1
  68. package/dist/channel/adapters/telegram.d.ts +26 -2
  69. package/dist/channel/adapters/telegram.js +123 -19
  70. package/dist/channel/adapters/telegram.js.map +1 -1
  71. package/dist/channel/mcp-server.js +9 -1
  72. package/dist/channel/mcp-server.js.map +1 -1
  73. package/dist/channel/mcp-tools.d.ts +11 -0
  74. package/dist/channel/mcp-tools.js +24 -13
  75. package/dist/channel/mcp-tools.js.map +1 -1
  76. package/dist/channel/message-edit-errors.d.ts +5 -0
  77. package/dist/channel/message-edit-errors.js +24 -0
  78. package/dist/channel/message-edit-errors.js.map +1 -0
  79. package/dist/channel/telegram-markdown.d.ts +6 -0
  80. package/dist/channel/telegram-markdown.js +104 -0
  81. package/dist/channel/telegram-markdown.js.map +1 -0
  82. package/dist/channel/tool-router.js +9 -3
  83. package/dist/channel/tool-router.js.map +1 -1
  84. package/dist/channel/types.d.ts +41 -3
  85. package/dist/cli.js +133 -28
  86. package/dist/cli.js.map +1 -1
  87. package/dist/command-table.d.ts +2 -2
  88. package/dist/command-table.js +6 -3
  89. package/dist/command-table.js.map +1 -1
  90. package/dist/config-validator.js +59 -0
  91. package/dist/config-validator.js.map +1 -1
  92. package/dist/cross-instance-envelope.d.ts +15 -2
  93. package/dist/cross-instance-envelope.js +29 -5
  94. package/dist/cross-instance-envelope.js.map +1 -1
  95. package/dist/daemon.d.ts +156 -1
  96. package/dist/daemon.js +764 -89
  97. package/dist/daemon.js.map +1 -1
  98. package/dist/delivery-outbox.d.ts +62 -7
  99. package/dist/delivery-outbox.js +309 -22
  100. package/dist/delivery-outbox.js.map +1 -1
  101. package/dist/delivery-reconciliation.d.ts +18 -1
  102. package/dist/delivery-reconciliation.js +67 -2
  103. package/dist/delivery-reconciliation.js.map +1 -1
  104. package/dist/fleet-context.d.ts +10 -1
  105. package/dist/fleet-manager.d.ts +352 -10
  106. package/dist/fleet-manager.js +1925 -175
  107. package/dist/fleet-manager.js.map +1 -1
  108. package/dist/general-knowledge/skills/backend-providers/SKILL.md +1 -1
  109. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +92 -21
  110. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +27 -3
  111. package/dist/general-knowledge/skills/delegation-playbook/SKILL.md +8 -8
  112. package/dist/general-knowledge/skills/fleet-config/SKILL.md +5 -5
  113. package/dist/general-knowledge/skills/fleet-health/SKILL.md +4 -4
  114. package/dist/general-knowledge/skills/fleet-restart/SKILL.md +13 -13
  115. package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +3 -2
  116. package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +6 -0
  117. package/dist/general-knowledge/skills/model-discovery/SKILL.md +9 -9
  118. package/dist/general-knowledge/skills/multi-channel/SKILL.md +8 -2
  119. package/dist/general-knowledge/skills/scheduling/SKILL.md +5 -3
  120. package/dist/general-knowledge/skills/session-management/SKILL.md +6 -5
  121. package/dist/general-knowledge/skills/tui-effort/SKILL.md +2 -2
  122. package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +8 -3
  123. package/dist/general-knowledge/steering/core-rules.md +8 -7
  124. package/dist/host-memory.d.ts +10 -0
  125. package/dist/host-memory.js +33 -1
  126. package/dist/host-memory.js.map +1 -1
  127. package/dist/human-delivery-receipts.d.ts +83 -0
  128. package/dist/human-delivery-receipts.js +194 -0
  129. package/dist/human-delivery-receipts.js.map +1 -0
  130. package/dist/instance-avatar.d.ts +36 -0
  131. package/dist/instance-avatar.js +57 -0
  132. package/dist/instance-avatar.js.map +1 -0
  133. package/dist/instance-config-impact.js +6 -0
  134. package/dist/instance-config-impact.js.map +1 -1
  135. package/dist/instance-lifecycle.d.ts +29 -6
  136. package/dist/instance-lifecycle.js +79 -28
  137. package/dist/instance-lifecycle.js.map +1 -1
  138. package/dist/instructions.d.ts +3 -1
  139. package/dist/instructions.js +11 -8
  140. package/dist/instructions.js.map +1 -1
  141. package/dist/kiro-log-gate.d.ts +42 -0
  142. package/dist/kiro-log-gate.js +39 -0
  143. package/dist/kiro-log-gate.js.map +1 -0
  144. package/dist/locale.js +204 -44
  145. package/dist/locale.js.map +1 -1
  146. package/dist/log-service.d.ts +99 -0
  147. package/dist/log-service.js +243 -0
  148. package/dist/log-service.js.map +1 -0
  149. package/dist/log-steps.d.ts +169 -0
  150. package/dist/log-steps.js +858 -0
  151. package/dist/log-steps.js.map +1 -0
  152. package/dist/login-controller.d.ts +20 -3
  153. package/dist/login-controller.js +71 -19
  154. package/dist/login-controller.js.map +1 -1
  155. package/dist/login-flows.d.ts +5 -2
  156. package/dist/login-flows.js +5 -4
  157. package/dist/login-flows.js.map +1 -1
  158. package/dist/memory-pressure.d.ts +13 -1
  159. package/dist/memory-pressure.js +50 -10
  160. package/dist/memory-pressure.js.map +1 -1
  161. package/dist/needs-you-hub.js +2 -0
  162. package/dist/needs-you-hub.js.map +1 -1
  163. package/dist/needs-you-live.d.ts +6 -0
  164. package/dist/needs-you-live.js +23 -3
  165. package/dist/needs-you-live.js.map +1 -1
  166. package/dist/next-steps.js +1 -1
  167. package/dist/next-steps.js.map +1 -1
  168. package/dist/outbound-handlers.d.ts +100 -0
  169. package/dist/outbound-handlers.js +263 -96
  170. package/dist/outbound-handlers.js.map +1 -1
  171. package/dist/outbound-schemas.d.ts +26 -15
  172. package/dist/outbound-schemas.js +24 -7
  173. package/dist/outbound-schemas.js.map +1 -1
  174. package/dist/public-link-progress.d.ts +1 -1
  175. package/dist/public-link-progress.js +8 -2
  176. package/dist/public-link-progress.js.map +1 -1
  177. package/dist/public-preview.d.ts +101 -0
  178. package/dist/public-preview.js +236 -0
  179. package/dist/public-preview.js.map +1 -0
  180. package/dist/public-web-gateway.js +11 -4
  181. package/dist/public-web-gateway.js.map +1 -1
  182. package/dist/public-web-link.d.ts +24 -1
  183. package/dist/public-web-link.js +63 -9
  184. package/dist/public-web-link.js.map +1 -1
  185. package/dist/reply-buttons-controller.d.ts +3 -0
  186. package/dist/reply-buttons-controller.js +3 -2
  187. package/dist/reply-buttons-controller.js.map +1 -1
  188. package/dist/restart-guard.d.ts +6 -3
  189. package/dist/restart-guard.js +6 -3
  190. package/dist/restart-guard.js.map +1 -1
  191. package/dist/restart-progress.d.ts +33 -0
  192. package/dist/restart-progress.js +30 -0
  193. package/dist/restart-progress.js.map +1 -1
  194. package/dist/service-activation.d.ts +15 -7
  195. package/dist/service-activation.js +46 -29
  196. package/dist/service-activation.js.map +1 -1
  197. package/dist/service-installer.d.ts +31 -0
  198. package/dist/service-installer.js +41 -2
  199. package/dist/service-installer.js.map +1 -1
  200. package/dist/settings-api.js +85 -25
  201. package/dist/settings-api.js.map +1 -1
  202. package/dist/settings-change.js +72 -1
  203. package/dist/settings-change.js.map +1 -1
  204. package/dist/settings-effect.js +28 -4
  205. package/dist/settings-effect.js.map +1 -1
  206. package/dist/settings-http-confirmation.js +7 -0
  207. package/dist/settings-http-confirmation.js.map +1 -1
  208. package/dist/setup-form.d.ts +1 -1
  209. package/dist/setup-form.js +1 -1
  210. package/dist/spawn-gate.js +5 -1
  211. package/dist/spawn-gate.js.map +1 -1
  212. package/dist/spawn-hold-reply.d.ts +12 -0
  213. package/dist/spawn-hold-reply.js +39 -0
  214. package/dist/spawn-hold-reply.js.map +1 -0
  215. package/dist/status-emojis.d.ts +10 -3
  216. package/dist/status-emojis.js +11 -2
  217. package/dist/status-emojis.js.map +1 -1
  218. package/dist/systemd-peer-bus.d.ts +95 -0
  219. package/dist/systemd-peer-bus.js +430 -0
  220. package/dist/systemd-peer-bus.js.map +1 -0
  221. package/dist/systemd-runtime.d.ts +27 -1
  222. package/dist/systemd-runtime.js +69 -7
  223. package/dist/systemd-runtime.js.map +1 -1
  224. package/dist/tips.js +4 -4
  225. package/dist/tips.js.map +1 -1
  226. package/dist/tool-progress.js +3 -2
  227. package/dist/tool-progress.js.map +1 -1
  228. package/dist/topic-commands.d.ts +42 -2
  229. package/dist/topic-commands.js +92 -7
  230. package/dist/topic-commands.js.map +1 -1
  231. package/dist/transcript-delta-reader.d.ts +3 -3
  232. package/dist/transcript-delta-reader.js +8 -8
  233. package/dist/transcript-delta-reader.js.map +1 -1
  234. package/dist/transcript-jsonl.d.ts +80 -3
  235. package/dist/transcript-jsonl.js +147 -10
  236. package/dist/transcript-jsonl.js.map +1 -1
  237. package/dist/transcript-monitor.d.ts +19 -0
  238. package/dist/transcript-monitor.js +87 -15
  239. package/dist/transcript-monitor.js.map +1 -1
  240. package/dist/transcript-sources.d.ts +78 -0
  241. package/dist/transcript-sources.js +375 -64
  242. package/dist/transcript-sources.js.map +1 -1
  243. package/dist/transcript-turns.d.ts +15 -2
  244. package/dist/transcript-turns.js +15 -2
  245. package/dist/transcript-turns.js.map +1 -1
  246. package/dist/tunnel/cloudflared.d.ts +7 -15
  247. package/dist/tunnel/cloudflared.js +36 -1
  248. package/dist/tunnel/cloudflared.js.map +1 -1
  249. package/dist/tunnel/purpose-lane.d.ts +5 -1
  250. package/dist/tunnel/purpose-lane.js +14 -7
  251. package/dist/tunnel/purpose-lane.js.map +1 -1
  252. package/dist/tunnel/types.d.ts +5 -1
  253. package/dist/tunnel/types.js.map +1 -1
  254. package/dist/turn-resume.d.ts +2 -0
  255. package/dist/turn-resume.js +1 -1
  256. package/dist/turn-resume.js.map +1 -1
  257. package/dist/types.d.ts +25 -0
  258. package/dist/ui/chat-commands.js +1 -0
  259. package/dist/ui/chat-emojis.js +33 -0
  260. package/dist/ui/chat-mentions.js +63 -0
  261. package/dist/ui/chat-render.js +42 -2
  262. package/dist/ui/chat-store.js +2 -2
  263. package/dist/ui/chat-strings.js +38 -20
  264. package/dist/ui/chat-thread.js +69 -12
  265. package/dist/ui/chat-time.js +80 -0
  266. package/dist/ui/panel-chat.js +39 -10
  267. package/dist/ui/panel-details.js +2 -0
  268. package/dist/ui/panel-fleet.js +199 -57
  269. package/dist/ui/panel-log.js +428 -0
  270. package/dist/ui/panel-needs.js +6 -2
  271. package/dist/ui/panel-settings.js +137 -32
  272. package/dist/ui/preview-panel.js +55 -15
  273. package/dist/ui/preview-status.js +28 -0
  274. package/dist/ui/preview.js +44 -3
  275. package/dist/ui/settings-dialogs.js +7 -2
  276. package/dist/ui/settings-model.js +2 -1
  277. package/dist/ui/settings-strings.js +46 -4
  278. package/dist/ui/settings-token.js +1 -1
  279. package/dist/ui/shared/app-i18n.js +43 -17
  280. package/dist/ui/shared/app-prefs.js +250 -0
  281. package/dist/ui/shared/app-route.js +7 -2
  282. package/dist/ui/shared/app-shell.js +165 -8
  283. package/dist/ui/shared/app-store.js +5 -1
  284. package/dist/ui/shared/app-stream.js +23 -5
  285. package/dist/ui/shared/app.css +159 -23
  286. package/dist/ui/shared/app.js +11 -1
  287. package/dist/ui/shared/fonts/README.md +1 -1
  288. package/dist/ui/shared/header-tools.js +55 -5
  289. package/dist/ui/shared/instance-nav.js +241 -45
  290. package/dist/ui/shared/instance-switch.js +4 -4
  291. package/dist/ui/shared/panel-view.js +6 -0
  292. package/dist/ui/shared/signin.js +7 -7
  293. package/dist/ui/shared/theme.js +39 -7
  294. package/dist/ui/shared/tokens.css +10 -1
  295. package/dist/ui/shared/ui-icons.js +1 -0
  296. package/dist/ui/shared/usage-dialog.js +23 -5
  297. package/dist/ui/shared/view-strings.js +32 -4
  298. package/dist/ui/signin.html +1 -1
  299. package/dist/unrecognised-prompt.d.ts +23 -0
  300. package/dist/unrecognised-prompt.js +74 -0
  301. package/dist/unrecognised-prompt.js.map +1 -0
  302. package/dist/update-install.d.ts +28 -2
  303. package/dist/update-install.js +22 -4
  304. package/dist/update-install.js.map +1 -1
  305. package/dist/update-marker.d.ts +51 -0
  306. package/dist/update-marker.js +61 -0
  307. package/dist/update-marker.js.map +1 -1
  308. package/dist/update-preflight.d.ts +100 -0
  309. package/dist/update-preflight.js +264 -0
  310. package/dist/update-preflight.js.map +1 -0
  311. package/dist/updater-caller.d.ts +31 -0
  312. package/dist/updater-caller.js +109 -0
  313. package/dist/updater-caller.js.map +1 -0
  314. package/dist/usage/i18n-keys.d.ts +1 -1
  315. package/dist/usage/i18n-keys.js +1 -1
  316. package/dist/usage/i18n-keys.js.map +1 -1
  317. package/dist/usage/providers.d.ts +34 -5
  318. package/dist/usage/providers.js +160 -27
  319. package/dist/usage/providers.js.map +1 -1
  320. package/dist/usage/statusline-usage.d.ts +6 -2
  321. package/dist/usage/statusline-usage.js +10 -4
  322. package/dist/usage/statusline-usage.js.map +1 -1
  323. package/dist/view-api.d.ts +1 -1
  324. package/dist/view-api.js +38 -8
  325. package/dist/view-api.js.map +1 -1
  326. package/dist/wake-coordinator.d.ts +11 -0
  327. package/dist/wake-coordinator.js +38 -4
  328. package/dist/wake-coordinator.js.map +1 -1
  329. package/dist/web-api.d.ts +49 -2
  330. package/dist/web-api.js +421 -31
  331. package/dist/web-api.js.map +1 -1
  332. package/dist/web-auth.d.ts +2 -2
  333. package/dist/web-auth.js +9 -4
  334. package/dist/web-auth.js.map +1 -1
  335. package/dist/web-commands.d.ts +27 -4
  336. package/dist/web-commands.js +19 -4
  337. package/dist/web-commands.js.map +1 -1
  338. package/dist/web-host-guard.d.ts +3 -11
  339. package/dist/web-host-guard.js +3 -12
  340. package/dist/web-host-guard.js.map +1 -1
  341. package/dist/web-images.d.ts +47 -0
  342. package/dist/web-images.js +169 -0
  343. package/dist/web-images.js.map +1 -0
  344. package/dist/web-mentions.d.ts +44 -0
  345. package/dist/web-mentions.js +75 -0
  346. package/dist/web-mentions.js.map +1 -0
  347. package/dist/web-prefs.d.ts +63 -0
  348. package/dist/web-prefs.js +265 -0
  349. package/dist/web-prefs.js.map +1 -0
  350. package/dist/web-preview.d.ts +14 -1
  351. package/dist/web-preview.js +35 -13
  352. package/dist/web-preview.js.map +1 -1
  353. package/dist/web-reply-files.d.ts +45 -0
  354. package/dist/web-reply-files.js +223 -0
  355. package/dist/web-reply-files.js.map +1 -0
  356. package/dist/web-schedules.d.ts +12 -0
  357. package/dist/web-schedules.js +34 -0
  358. package/dist/web-schedules.js.map +1 -0
  359. package/dist/web-server-drain.d.ts +10 -0
  360. package/dist/web-server-drain.js +50 -0
  361. package/dist/web-server-drain.js.map +1 -0
  362. package/dist/web-session.d.ts +36 -1
  363. package/dist/web-session.js +53 -6
  364. package/dist/web-session.js.map +1 -1
  365. package/dist/web-settings-patch.d.ts +23 -0
  366. package/dist/web-settings-patch.js +77 -0
  367. package/dist/web-settings-patch.js.map +1 -0
  368. package/dist/web-shell-routes.d.ts +5 -1
  369. package/dist/web-shell-routes.js +10 -1
  370. package/dist/web-shell-routes.js.map +1 -1
  371. package/dist/web-upload.d.ts +17 -4
  372. package/dist/web-upload.js +64 -13
  373. package/dist/web-upload.js.map +1 -1
  374. package/dist/workflow-templates/default.md +1 -1
  375. package/package.json +1 -1
  376. package/templates/launchd.plist.ejs +3 -1
  377. package/templates/systemd.service.ejs +2 -1
@@ -15,6 +15,7 @@ register("settings", {
15
15
  publicProtocol: "Tunnel protocol",
16
16
  webEcho: "Web echo",
17
17
  webEchoHint: "Copies web messages to the bound Telegram or Discord topic. ClassicBot rooms are excluded.",
18
+ webLogTab: "Log tab: each agent's steps", webLogTabHint: "Shows each instance's prompts, replies, tool calls and results, and fleet events in its Log tab, read from its own transcript only while the tab is open. Secret values are masked. Also over the public link. Turning it on waits for a fleet admin's confirmation.",
18
19
  general: "General Settings",
19
20
  advanced: "Advanced",
20
21
  new: "New",
@@ -75,7 +76,7 @@ register("settings", {
75
76
  crossInstanceVisibility: "Bot-to-bot messages in topics",
76
77
  crossInstanceVisibilityHint: "full: the whole message · summary: one line · hidden: nothing. Messages are always delivered; the Mirror Topic still shows them.",
77
78
  replyCompletionGuard: "Require a delivered reply",
78
- replyGuardSupported: "Available for Claude Code and Kiro legacy/TUI in MCP mode.",
79
+ replyGuardSupported: "Available for Claude Code, Codex, Muse and Kiro legacy/TUI in MCP mode.",
79
80
  replyGuardUnsupported: "Stored but inactive: {0} in {1} mode does not support reply-drop recovery.",
80
81
  logLevel: "Log level",
81
82
  concurrency: "Startup concurrency",
@@ -217,7 +218,9 @@ register("settings", {
217
218
  concurrencyWarning: "May exceed CPU capacity on smaller hosts",
218
219
  staggerRange: "Must be between 0 and 30000",
219
220
  staggerWarning: "Very low values can cause startup spikes",
220
- sec_device: "This device", deviceLead: "Choices for this browser only — other devices keep their own.",
221
+ sec_device: "Preferences", syncedTitle: "Synced across your devices", syncedLead: "Changes here apply on every device where you are signed in.",
222
+ syncedPublicLink: "On the public link these are read-only: a change here stays in this browser.",
223
+ deviceTitle: "This device only", deviceLead: "The tour, text size, view fit, the sidebar shown or hidden, panel widths and desktop notifications stay on each device.",
221
224
  sec_agents: "Agents",
222
225
  sec_bots: "Connections",
223
226
  sec_classic: "ClassicBot",
@@ -277,6 +280,24 @@ register("settings", {
277
280
  keyRollbackFailed: "Rollback failed",
278
281
  accessTitle: "Access",
279
282
  webChat: "Web chat",
283
+ sec_web: "Web",
284
+ webLead: "How the web dashboard behaves. A change that loosens access (longer sessions, the browser terminal back on or for longer, the public link) waits for a fleet admin to confirm it in General; one that tightens it applies at once.",
285
+ webPublicRefused: "Web settings can't be changed over a temporary public link. Open the dashboard at its own address to change them.",
286
+ webSessionTitle: "Sign-in sessions",
287
+ webSessionHint: "For sign-ins at the dashboard's own address (this computer, the LAN, a tailnet). A temporary public link always keeps 4 h / 30 min. Shortening ends the sessions it now outlives; lengthening applies to new sign-ins and to an existing one's idle time on its next use. Empty = the default.",
288
+ sessionAbsolute: "Session length, hours (1–720)",
289
+ sessionIdle: "Sign out after unused for, hours (1–720)",
290
+ sessionRange: "Must be a number of hours from 1 to 720",
291
+ sessionIdleOver: "Must not exceed the session length",
292
+ defaultHours: "{0} (default)",
293
+ webTerminalTitle: "Browser terminal",
294
+ webTerminalHint: "The token-gated terminal /login opens to sign a CLI in. Off: /login refuses. Applies to the next /login.",
295
+ webTerminalEnabled: "Allow the browser terminal",
296
+ webTerminalTtl: "Terminal session minutes (1–20)",
297
+ webTerminalTtlRange: "Must be a whole number from 1 to 20",
298
+ webReadOnlyTitle: "Set in fleet.yaml",
299
+ webReadOnlyLead: "Read-only here: these decide what the dashboard listens on and exposes. Edit fleet.yaml, then run agend reload (web.preview_port and web.preview_origin take a restart of AgEnD).",
300
+ notSet: "not set (default)",
280
301
  defaultsTitle: "Defaults",
281
302
  classicBackend: "ClassicBot default backend",
282
303
  whoUsesClassic: "Who can use ClassicBot",
@@ -385,6 +406,7 @@ register("settings", {
385
406
  publicProtocol: "Tunnel 協定",
386
407
  webEcho: "Web 同步",
387
408
  webEchoHint: "將網頁訊息複製到綁定的 Telegram 或 Discord 主題,不包含 ClassicBot 群組。",
409
+ webLogTab: "紀錄分頁:每個 agent 的步驟", webLogTabHint: "在每個 instance 的「紀錄」分頁顯示提示、回覆、工具呼叫與結果,以及 fleet 事件;只在分頁開著時從它自己的對話紀錄讀取。機密值會遮蔽。公開連結也看得到。開啟需 fleet admin 確認。",
388
410
  general: "一般設定",
389
411
  advanced: "進階",
390
412
  new: "新增",
@@ -420,7 +442,7 @@ register("settings", {
420
442
  crossInstanceVisibility: "Agent 之間的訊息在主題中",
421
443
  crossInstanceVisibilityHint: "full:完整訊息 · summary:一行摘要 · hidden:不顯示。訊息一律照常送達,Mirror Topic 也照常顯示。",
422
444
  replyCompletionGuard: "要求送出回覆",
423
- replyGuardSupported: "Claude Code 與 Kiro legacy/TUI 的 MCP 模式可用。",
445
+ replyGuardSupported: "Claude Code、Codex、Muse 與 Kiro legacy/TUI 的 MCP 模式可用。",
424
446
  replyGuardUnsupported: "設定會保留,但不會生效:{0} 的 {1} 模式不支援回覆遺失補救。",
425
447
  logLevel: "Log 等級",
426
448
  concurrency: "啟動併發數",
@@ -562,7 +584,9 @@ register("settings", {
562
584
  concurrencyWarning: "在較小的主機上可能超出 CPU 負荷",
563
585
  staggerRange: "必須介於 0 到 30000",
564
586
  staggerWarning: "數值太低可能造成啟動尖峰",
565
- sec_device: "這台裝置", deviceLead: "只套用在這個瀏覽器,其他裝置保留各自的設定。",
587
+ sec_device: "偏好設定", syncedTitle: "在你所有的裝置同步", syncedLead: "這裡的變更會套用到你登入的每一台裝置。",
588
+ syncedPublicLink: "公開連結只能讀取這些設定:在這裡變更只會留在這個瀏覽器。",
589
+ deviceTitle: "只限這台裝置", deviceLead: "導覽、文字大小、視圖寬度、側欄顯示或隱藏、面板寬度與桌面通知,每台裝置各自保留。",
566
590
  sec_agents: "Agent",
567
591
  sec_bots: "連線",
568
592
  sec_classic: "ClassicBot",
@@ -622,6 +646,24 @@ register("settings", {
622
646
  keyRollbackFailed: "還原失敗",
623
647
  accessTitle: "存取",
624
648
  webChat: "網頁聊天",
649
+ sec_web: "網頁",
650
+ webLead: "網頁儀表板的行為。放寬存取的變更(延長 session、重新開啟或延長瀏覽器終端機、公開連結)需要 fleet admin 在 General 確認;收緊的變更會立即套用。",
651
+ webPublicRefused: "無法透過臨時公開連結變更網頁設定。請從儀表板本身的網址開啟後再變更。",
652
+ webSessionTitle: "登入 session",
653
+ webSessionHint: "適用於從儀表板本身網址登入的 session(本機、區域網路、tailnet)。臨時公開連結固定為 4 小時 / 30 分鐘。縮短會讓已超過新限制的 session 結束;延長會套用到新的登入,以及既有 session 下次使用時的閒置時間。留空 = 預設值。",
654
+ sessionAbsolute: "Session 時長,小時(1–720)",
655
+ sessionIdle: "閒置多久後登出,小時(1–720)",
656
+ sessionRange: "必須是 1 到 720 之間的小時數",
657
+ sessionIdleOver: "不能超過 session 時長",
658
+ defaultHours: "{0}(預設)",
659
+ webTerminalTitle: "瀏覽器終端機",
660
+ webTerminalHint: "/login 用來登入 CLI 的 token 保護終端機。關閉時 /login 會拒絕。下一次 /login 起生效。",
661
+ webTerminalEnabled: "允許瀏覽器終端機",
662
+ webTerminalTtl: "終端機 session 分鐘數(1–20)",
663
+ webTerminalTtlRange: "必須是 1 到 20 之間的整數",
664
+ webReadOnlyTitle: "在 fleet.yaml 設定",
665
+ webReadOnlyLead: "這裡僅供檢視:這些設定決定儀表板監聽和公開的內容。請編輯 fleet.yaml 後執行 agend reload(web.preview_port 與 web.preview_origin 需要重新啟動 AgEnD)。",
666
+ notSet: "未設定(預設)",
625
667
  defaultsTitle: "預設值",
626
668
  classicBackend: "ClassicBot 預設 backend",
627
669
  whoUsesClassic: "誰可以使用 ClassicBot",
@@ -31,7 +31,7 @@ export function TokenField({ id, platform, value, onInput, identity, busy, onVer
31
31
  const [shown, setShown] = useState(false);
32
32
  return html`<div class="field token-field"><label for=${id}>${tn("wizardToken")}</label>
33
33
  <div class="token-row">
34
- <input id=${id} type=${shown ? "text" : "password"} autocomplete="off" autocapitalize="off" spellcheck="false" disabled=${!!busy}
34
+ <input id=${id} type=${shown ? "text" : "password"} autocomplete="off" autocapitalize="off" spellcheck=${false} disabled=${!!busy}
35
35
  placeholder=${platform === "discord" ? "MTA…" : "123456:ABC-DEF…"} value=${value} onInput=${(e) => onInput(e.target.value.trim())} />
36
36
  <button type="button" class="icon-btn" aria-pressed=${shown ? "true" : "false"} title=${shown ? tn("tokenHide") : tn("tokenShow")}
37
37
  aria-label=${shown ? tn("tokenHide") : tn("tokenShow")} onClick=${() => setShown(!shown)}><${Icon} name="eye" size=${16} /></button>
@@ -1,14 +1,33 @@
1
1
  // #1408 §4: one dictionary for the app, namespaced (app.*, chat.*, fleet.*, …). A panel registers its own namespace
2
2
  // when it loads; t("chat.send") resolves it in the chosen language and falls back to English, then to the key.
3
- // The language is this browser's choice (agend_lang, the key every panel has used), else the device's.
3
+ // The language is the person's choice — kept on the host since #1665 and cached in this browser as agend_lang, the key
4
+ // every panel has used — else the device's.
4
5
 
5
6
  const tables = { en: {}, "zh-TW": {} };
6
7
  const listeners = new Set();
7
8
 
8
- function stored() {
9
- try { const v = localStorage.getItem("agend_lang"); return v === "en" || v === "zh-TW" ? v : null; } catch { return null; }
9
+ /** The device's own language: what applies when none is chosen. */
10
+ export function deviceLanguage() { return typeof navigator !== "undefined" && /^zh/i.test(navigator.language || "") ? "zh-TW" : "en"; }
11
+ function cacheLang(v) { try { if (v) localStorage.setItem("agend_lang", v); else localStorage.removeItem("agend_lang"); } catch { /* this page only */ } }
12
+ /**
13
+ * #1665: the signed-in page carries the host's values (<html data-prefs>) — the same rule as theme.js: the host's
14
+ * language is this page's (the browser's copy is refreshed, best effort); none chosen on the host and this device in
15
+ * step with it: the device's language; not in step yet, or no data-prefs: this browser's copy, else the device's.
16
+ */
17
+ function initialLang() {
18
+ let d = null;
19
+ try { d = JSON.parse(document.documentElement.getAttribute("data-prefs") || "null"); } catch { d = null; }
20
+ if (d && typeof d === "object" && d.values && typeof d.values === "object") {
21
+ const host = d.values.lang;
22
+ if (host === "en" || host === "zh-TW") { cacheLang(host); return host; }
23
+ let inStep = false;
24
+ try { inStep = localStorage.getItem("agend_prefs_synced") === "1"; } catch { inStep = false; }
25
+ if (inStep) { cacheLang(null); return deviceLanguage(); }
26
+ }
27
+ try { const v = localStorage.getItem("agend_lang"); if (v === "en" || v === "zh-TW") return v; } catch { /* none */ }
28
+ return deviceLanguage();
10
29
  }
11
- let current = stored() || (typeof navigator !== "undefined" && /^zh/i.test(navigator.language || "") ? "zh-TW" : "en");
30
+ let current = initialLang();
12
31
 
13
32
  /** Add a namespace: `register("chat", { en: {...}, "zh-TW": {...} })`. */
14
33
  export function register(ns, byLang) {
@@ -19,11 +38,14 @@ export function register(ns, byLang) {
19
38
 
20
39
  export function lang() { return current; }
21
40
 
22
- /** Choose a language for this browser; every subscriber re-renders (a new navigation lease, §4). */
23
- export function setLang(next) {
41
+ /**
42
+ * Choose a language; every subscriber re-renders (a new navigation lease, §4). `{ store: false }`: this page only —
43
+ * the host's "none chosen" arriving from another device (app-prefs.js), which leaves no copy in this browser.
44
+ */
45
+ export function setLang(next, opts = {}) {
24
46
  if (next !== "en" && next !== "zh-TW") return;
25
47
  current = next;
26
- try { localStorage.setItem("agend_lang", next); } catch { /* this page only */ }
48
+ if (opts.store !== false) cacheLang(next);
27
49
  if (typeof document !== "undefined" && document.documentElement) document.documentElement.lang = next === "zh-TW" ? "zh-Hant" : "en";
28
50
  for (const fn of listeners) fn(next);
29
51
  }
@@ -38,13 +60,15 @@ export function t(key, ...values) {
38
60
 
39
61
  register("app", {
40
62
  en: {
41
- brand: "AgEnD", fleet: "Fleet", view: "View", settings: "Settings", instances: "Instances", newInstance: "New instance",
63
+ brand: "AgEnD", fleet: "Fleet", fleetOverview: "Fleet overview", view: "View", settings: "Settings", instances: "Instances", newInstance: "New instance",
42
64
  collapse: "Hide the sidebar", expand: "Show the sidebar", openMenu: "Open the sidebar", closeMenu: "Close the sidebar",
65
+ resizeSidebar: "Sidebar width", resizeSidebarHint: "Drag to resize · double-click to reset",
66
+ names: "Names", namesNickname: "Nickname first", namesInstance: "Real name first",
43
67
  theme: "Theme", themeSystem: "System", themeLight: "Light", themeDark: "Dark", language: "Language",
44
- previews: "Allow HTML previews on this device", tour: "Tour", tourTitle: "Show the web chat tour again",
45
- needsYou: "needs you", approxNote: "Read from the terminal, so approximate", chat: "Chat", details: "Details", instanceViews: "Views of {0}",
46
- textSize: "Text size: {0}", textSize_s: "S", textSize_m: "M", textSize_l: "L", textSize_fit: "Fit", usage: "Usage",
47
- statusRunning: "running", statusStopped: "stopped", statusCrashed: "crashed", statusWorking: "working", statusStuck: "looks stuck", statusIdle: "idle",
68
+ previews: "Allow HTML previews", tour: "Tour", tourTitle: "Show the web chat tour again",
69
+ needsYou: "needs you", approxNote: "Read from the terminal, so approximate", chat: "Chat", details: "Details", log: "Log", instanceViews: "Views of {0}",
70
+ textSize: "Text size: {0}", textSize_s: "S", textSize_m: "M", textSize_l: "L", textSize_fit: "Fit", usage: "Usage", usageHide: "Hide usage",
71
+ statusRunning: "running", statusStopped: "stopped", statusPaused: "paused", statusCrashed: "crashed", statusWorking: "working", statusStuck: "looks stuck", statusIdle: "idle",
48
72
  connReconnecting: "Can't reach AgEnD — it may be restarting. Reconnecting…", connReconnectingIn: "Can't reach AgEnD — it may be restarting. Trying again in {0} s.",
49
73
  hydrating: "Loading what is open now…", hydrateFailed: "Could not load what is open now (prompts, ticks).",
50
74
  skip: "Skip to the conversation", loadFailed: "This could not be loaded.", retry: "Try again", loading: "Loading…",
@@ -67,13 +91,15 @@ register("app", {
67
91
  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",
68
92
  },
69
93
  "zh-TW": {
70
- brand: "AgEnD", fleet: "Fleet", view: "檢視", settings: "設定", instances: "Instances", newInstance: "新增 instance",
94
+ brand: "AgEnD", fleet: "Fleet", fleetOverview: "Fleet 概覽", view: "檢視", settings: "設定", instances: "Instances", newInstance: "新增 instance",
71
95
  collapse: "隱藏側邊欄", expand: "顯示側邊欄", openMenu: "打開側邊欄", closeMenu: "關閉側邊欄",
96
+ resizeSidebar: "側邊欄寬度", resizeSidebarHint: "拖曳調整寬度 · 按兩下恢復預設",
97
+ names: "名稱", namesNickname: "暱稱優先", namesInstance: "實際名稱優先",
72
98
  theme: "主題", themeSystem: "跟隨系統", themeLight: "淺色", themeDark: "深色", language: "語言",
73
- previews: "允許這台裝置預覽 HTML", tour: "導覽", tourTitle: "再看一次網頁聊天導覽",
74
- needsYou: "等你回應", approxNote: "從終端畫面判讀,僅供參考", chat: "聊天", details: "詳細資訊", instanceViews: "{0} 的檢視",
75
- textSize: "字級:{0}", textSize_s: "小", textSize_m: "中", textSize_l: "大", textSize_fit: "剛好填滿", usage: "用量",
76
- statusRunning: "執行中", statusStopped: "已停止", statusCrashed: "已當掉", statusWorking: "工作中", statusStuck: "似乎卡住", statusIdle: "閒置",
99
+ previews: "允許預覽 HTML", tour: "導覽", tourTitle: "再看一次網頁聊天導覽",
100
+ needsYou: "等你回應", approxNote: "從終端畫面判讀,僅供參考", chat: "聊天", details: "詳細資訊", log: "紀錄", instanceViews: "{0} 的檢視",
101
+ textSize: "字級:{0}", textSize_s: "小", textSize_m: "中", textSize_l: "大", textSize_fit: "剛好填滿", usage: "用量", usageHide: "隱藏用量",
102
+ statusRunning: "執行中", statusStopped: "已停止", statusPaused: "已暫停", statusCrashed: "已當掉", statusWorking: "工作中", statusStuck: "似乎卡住", statusIdle: "閒置",
77
103
  connReconnecting: "連不上 AgEnD——可能正在重新啟動。正在重新連線…", connReconnectingIn: "連不上 AgEnD——可能正在重新啟動。{0} 秒後再試。",
78
104
  hydrating: "正在載入目前的狀態…", hydrateFailed: "無法載入目前的狀態(提示、送達標記)。",
79
105
  skip: "跳到對話", loadFailed: "無法載入。", retry: "再試一次", loading: "載入中…",
@@ -0,0 +1,250 @@
1
+ // #1665: the web preferences that follow the person between devices — kept on the host (/ui/prefs), with this browser's
2
+ // localStorage, under the keys every panel already reads, as the cache. Design: the comment on #1665.
3
+ //
4
+ // - Synced: theme, language, the sidebar (order, folds, filter, favorites — #1664) and the HTML-preview opt-in. The tour and the first-run
5
+ // card stay on each device, as every layout choice does.
6
+ // - The signed-in page is served with the host's values (<html data-prefs>). This module, imported before any panel
7
+ // reads its key, copies them into the cache: the host wins once it has a value — and, on a device already in step
8
+ // with it, a key the host does not have is the default (its stale copy here is cleared). theme.js and the
9
+ // dictionary did the same for the theme and language, before paint.
10
+ // - A change made here is cached at once and sent to the host (a short debounce gathers a burst, such as typing in the
11
+ // filter). Writes go one at a time; a key stays this page's own from the change until its write has ended, so no
12
+ // answer — a read or another write — that started before can put an older value back. Every answer carries the
13
+ // host's revision, and one older than what this page already has is ignored.
14
+ // - The public link, and the anonymous View reader, have no host write: theirs stays local.
15
+ // - The first load on a device (no agend_prefs_synced yet) uploads what it had locally for the keys the host does not
16
+ // have yet, each with onlyIfUnset — the first device to arrive wins — then reads the host's values back. Only once
17
+ // every upload got an answer and that read succeeded is the device marked: anything else tries again next load.
18
+ // - The status frame's prefs_rev: another device changed something; the host's values are read again and applied. A
19
+ // revision counts as seen only once its read succeeded (a failed one is tried again, at most every few seconds).
20
+ import { deviceLanguage, lang, setLang } from "./app-i18n.js";
21
+ import { appStore } from "./app-store.js";
22
+
23
+ const SYNCED_FLAG = "agend_prefs_synced";
24
+ const RETRY_MS = 5000;
25
+ const json = { toLocal: (v) => JSON.stringify(v), fromLocal: (s) => { try { return s == null ? undefined : JSON.parse(s); } catch { return undefined; } } };
26
+ /** Each synced preference: its cache key, and how a host value is cached (null: no entry) and read back. */
27
+ export const SYNCED = {
28
+ theme: { ls: "agend_theme", toLocal: (v) => (v === "light" || v === "dark" ? v : null), fromLocal: (s) => (s === "light" || s === "dark" ? s : undefined) },
29
+ lang: { ls: "agend_lang", toLocal: (v) => (v === "en" || v === "zh-TW" ? v : null), fromLocal: (s) => (s === "en" || s === "zh-TW" ? s : undefined) },
30
+ "nav.order": { ls: "agend_view_sidebar_order", ...json },
31
+ "nav.collapsed": { ls: "agend_instance_collapsed", ...json },
32
+ "nav.filter": { ls: "agend_instance_filter", ...json },
33
+ "nav.favorites": { ls: "agend_favorites", ...json }, // #1664
34
+ "preview.optIn": { ls: "agend_html_preview", toLocal: (v) => (v ? "on" : null), fromLocal: (s) => (s === "on" ? true : undefined) },
35
+ "names.primary": { ls: "agend_names_primary", toLocal: (v) => (v === "instance" ? v : null), fromLocal: (s) => (s === "instance" ? s : undefined) }, // #1722
36
+ };
37
+ const own = (o, k) => !!o && Object.prototype.hasOwnProperty.call(o, k);
38
+ const now = () => (typeof performance !== "undefined" ? performance.now() : Date.now());
39
+
40
+ function readLocal(key) { try { return localStorage.getItem(key); } catch { return null; } }
41
+ function writeLocal(key, s) { try { if (s == null) localStorage.removeItem(key); else localStorage.setItem(key, s); } catch { /* this page only */ } }
42
+ function cache(key, value) { const k = SYNCED[key]; writeLocal(k.ls, value == null ? null : k.toLocal(value)); }
43
+ const inStep = () => readLocal(SYNCED_FLAG) === "1";
44
+
45
+ function pageHost() {
46
+ try {
47
+ const d = JSON.parse(document.documentElement.getAttribute("data-prefs") || "null");
48
+ return d && typeof d === "object" && typeof d.rev === "number" && d.values && typeof d.values === "object" ? d : null;
49
+ } catch { return null; }
50
+ }
51
+
52
+ const initial = typeof document !== "undefined" ? pageHost() : null;
53
+ let rev = initial ? initial.rev : null;
54
+ let host = initial ? initial.values : null;
55
+ const pending = new Map(); // key → value chosen here, not sent yet
56
+ const mine = new Map(); // key → value chosen here whose write is not over (pending or out)
57
+ const inflight = new Map(); // key → writes sent and not answered yet
58
+ let flushTimer = null;
59
+ let writes = Promise.resolve(); // one write at a time, in the order they were made
60
+ const listeners = new Set();
61
+
62
+ // The host's values into the cache, now — before the sidebar, the tour or the first-run card read their keys. A key
63
+ // the host does not have: the default, on a device in step with the host; this device's own (to upload) otherwise.
64
+ if (host) {
65
+ const step = inStep();
66
+ for (const key of Object.keys(SYNCED)) {
67
+ if (own(host, key)) cache(key, host[key]);
68
+ else if (step) cache(key, null);
69
+ }
70
+ }
71
+
72
+ /** This page may change the host's preferences: the signed-in app, not over the public link. */
73
+ export function prefsWritable() {
74
+ return !!initial && !(typeof document !== "undefined" && document.body && document.body.dataset.publicLink === "1");
75
+ }
76
+ /**
77
+ * The value in force for a key, in memory — never only in this browser's storage (a full storage must not bring back
78
+ * an old value): this page's own choice while its write is not over; else the host's; else, on a device in step with
79
+ * the host, the default ({ value: undefined }). null: not known here (no host values on this page, or a device not yet
80
+ * in step) — the caller reads this browser's copy.
81
+ */
82
+ export function prefInForce(key) {
83
+ if (!own(SYNCED, key)) return null;
84
+ if (mine.has(key)) return { value: mine.get(key) };
85
+ if (host && own(host, key)) return { value: host[key] };
86
+ if (initial && inStep()) return { value: undefined };
87
+ return null;
88
+ }
89
+ /** The host's value for a key as this page last saw it (undefined: not set there, or not known). */
90
+ export function hostPref(key) { return host && own(host, key) ? host[key] : undefined; }
91
+ /** Called with the keys another device changed, after they are cached and applied here. Returns the unsubscribe. */
92
+ export function onPrefsChanged(fn) { listeners.add(fn); return () => listeners.delete(fn); }
93
+
94
+ /** A host answer ({ values, rev }): a real view of the host (not an unreadable store's defaults)? */
95
+ const isView = (body) => !!body && typeof body.rev === "number" && !!body.values && typeof body.values === "object" && !body.unreadable;
96
+ /** Take a view as this page's — unless it is older than the one it has. True: taken. */
97
+ function take(body) {
98
+ if (!isView(body) || (rev !== null && body.rev < rev)) return false;
99
+ rev = body.rev;
100
+ host = body.values;
101
+ return true;
102
+ }
103
+
104
+ /**
105
+ * A preference chosen on this page: cached now, and kept on the host (a burst within 400 ms is one write). Not writable
106
+ * (the public link, the anonymous reader): this browser only, as before.
107
+ */
108
+ export function savePref(key, value) {
109
+ if (!own(SYNCED, key)) return;
110
+ cache(key, value);
111
+ if (!prefsWritable()) return;
112
+ pending.set(key, value);
113
+ mine.set(key, value);
114
+ clearTimeout(flushTimer);
115
+ flushTimer = setTimeout(flush, 400);
116
+ }
117
+
118
+ /** One write. ok: the host took it (its answer is a view; an older one than this page has is not taken). */
119
+ async function post(set, onlyIfUnset) {
120
+ const res = await fetch("/ui/prefs", { method: "POST", credentials: "same-origin", headers: { "Content-Type": "application/json" },
121
+ body: JSON.stringify(onlyIfUnset ? { set, onlyIfUnset: true } : { set }) });
122
+ const body = await res.json().catch(() => null);
123
+ const ok = res.ok && isView(body);
124
+ if (ok) take(body);
125
+ return { ok, status: res.status };
126
+ }
127
+
128
+ function flush() {
129
+ flushTimer = null;
130
+ if (!pending.size) return writes;
131
+ const set = Object.fromEntries(pending);
132
+ const keys = [...pending.keys()];
133
+ pending.clear();
134
+ for (const k of keys) inflight.set(k, (inflight.get(k) || 0) + 1);
135
+ writes = writes.then(async () => {
136
+ let ok = false;
137
+ try { ok = (await post(set, false)).ok; } catch { ok = false; }
138
+ const released = new Set();
139
+ for (const k of keys) {
140
+ const n = (inflight.get(k) || 1) - 1;
141
+ if (n) inflight.set(k, n); else inflight.delete(k);
142
+ if (!n && !pending.has(k)) { mine.delete(k); released.add(k); }
143
+ }
144
+ // Each key whose last write this was, and not changed here again since, now shows the newest host view this page
145
+ // has — usually its own value; another device's newer one if that arrived while the write was out (held back then,
146
+ // and an older answer to this write cannot overrule it); refused or not sent, what the host has (read back first).
147
+ // Always applied: what the page shows came from this page's own choice, not from the cache, so the cache matching
148
+ // the host (a storage that refused the choice) says nothing about the screen.
149
+ if (!ok) await refresh();
150
+ align(released, { force: true });
151
+ });
152
+ return writes;
153
+ }
154
+
155
+ /**
156
+ * Read the host's values again and apply the ones that differ from this page's — not a key still being changed here
157
+ * (unsent, or its write not answered yet), and nothing from an answer older than the view this page has. True: the read
158
+ * succeeded (taken, or older than what this page already has — nothing to do).
159
+ */
160
+ export async function refresh() {
161
+ if (!initial) return false;
162
+ let body = null;
163
+ try {
164
+ const res = await fetch("/ui/prefs", { credentials: "same-origin" });
165
+ body = res.ok ? await res.json() : null;
166
+ } catch { body = null; }
167
+ if (!isView(body)) return false;
168
+ const before = new Map(Object.keys(SYNCED).map((key) => [key, own(host, key) ? SYNCED[key].toLocal(host[key]) : null]));
169
+ if (take(body)) align(Object.keys(SYNCED), { before });
170
+ return true;
171
+ }
172
+
173
+ /**
174
+ * Show the host view this page has for these keys — except a key still being changed here (unsent, or out). A key is
175
+ * applied when its host value moved in memory (`before`: the host view before this read), when this browser's copy
176
+ * differs, or always (`force`: this page's own write just ended). The copy alone never decides: a storage that refused
177
+ * a write keeps an old copy that may equal the host while the screen shows something else.
178
+ */
179
+ function align(keys, opts = {}) {
180
+ const changed = new Set();
181
+ for (const key of keys) {
182
+ if (pending.has(key) || inflight.has(key)) continue;
183
+ const k = SYNCED[key];
184
+ const want = own(host, key) ? k.toLocal(host[key]) : null;
185
+ const copyDiffers = readLocal(k.ls) !== want;
186
+ const moved = !!opts.before && opts.before.get(key) !== want;
187
+ if (!copyDiffers && !moved && !opts.force) continue;
188
+ if (copyDiffers) writeLocal(k.ls, want);
189
+ changed.add(key);
190
+ }
191
+ if (changed.size) apply(changed);
192
+ }
193
+
194
+ function apply(changed) {
195
+ // Only what differs from what the page shows (a key re-applied after its own write may already be in force).
196
+ const T = globalThis.AgendTheme;
197
+ if (changed.has("theme") && typeof T !== "undefined") { const v = hostPref("theme") || "system"; if (typeof T.get !== "function" || T.get() !== v) T.set(v); }
198
+ if (changed.has("lang")) {
199
+ const l = hostPref("lang");
200
+ // None chosen any more: the device's own language, on this page only (no copy is kept for it).
201
+ if (l) { if (l !== lang()) setLang(l); } else if (deviceLanguage() !== lang()) setLang(deviceLanguage(), { store: false });
202
+ }
203
+ const P = globalThis.AgendPreview;
204
+ if (changed.has("preview.optIn") && P && typeof P.setOptIn === "function") {
205
+ const on = hostPref("preview.optIn") === true;
206
+ if (typeof P.optedIn !== "function" || P.optedIn() !== on) P.setOptIn(on);
207
+ }
208
+ for (const fn of [...listeners]) { try { fn(changed); } catch { /* one listener's error stops no other */ } }
209
+ }
210
+
211
+ /**
212
+ * Once per device: upload what this browser had for the keys the host has not got yet (each on its own, onlyIfUnset — a
213
+ * value the host refuses as invalid is simply not taken), then adopt the host's values. Marked only when every upload
214
+ * was answered (taken, or refused as invalid) and the read back succeeded; a request with no answer, a server error, a
215
+ * refused session or an unreadable store leaves the device unmarked, so the next load tries again.
216
+ */
217
+ export async function migratePrefs() {
218
+ if (!prefsWritable() || inStep()) return false;
219
+ for (const key of Object.keys(SYNCED)) {
220
+ if (own(host, key)) continue;
221
+ const local = SYNCED[key].fromLocal(readLocal(SYNCED[key].ls));
222
+ if (local === undefined) continue;
223
+ let r;
224
+ try { r = await post({ [key]: local }, true); } catch { return false; }
225
+ if (!r.ok && r.status !== 400) return false;
226
+ }
227
+ if (!(await refresh())) return false;
228
+ writeLocal(SYNCED_FLAG, "1");
229
+ return true;
230
+ }
231
+
232
+ /** The app's boot ("full" mode): the first-load migration, then another device's changes at each status frame. */
233
+ export function bootPrefs() {
234
+ if (!initial) return;
235
+ // Each new revision the frames announce, read once (the store changes many times a second); one announced while a read
236
+ // is out is read as soon as it ends. Seen only once its read succeeded: after a failed read the next try waits for a
237
+ // later frame, at least RETRY_MS on.
238
+ let seen = rev, busy = false, failedAt = -Infinity;
239
+ const check = () => {
240
+ const r = appStore.get().prefsRev;
241
+ if (typeof r !== "number" || r === seen || busy) return;
242
+ if (r === rev) { seen = r; return; }
243
+ if (now() - failedAt < RETRY_MS) return;
244
+ busy = true;
245
+ refresh().then((ok) => { if (ok) seen = r; else failedAt = now(); }, () => { failedAt = now(); })
246
+ .finally(() => { busy = false; check(); });
247
+ };
248
+ appStore.subscribe(check);
249
+ migratePrefs();
250
+ }
@@ -2,7 +2,7 @@
2
2
  // a test runs both over one list of cases). Pure: no DOM, no history.
3
3
 
4
4
  export const FLEET_TABS = ["tasks", "schedules", "teams", "org", "cache", "config"];
5
- export const SETTINGS_SECTIONS = ["device", "agents", "bots", "classic", "general", "advanced"]; // #1604: device first
5
+ export const SETTINGS_SECTIONS = ["device", "agents", "bots", "classic", "general", "web", "advanced"]; // #1604: device first
6
6
  const MAX_INSTANCE_NAME = 128;
7
7
  const PATH_SEPARATOR_OR_CONTROL = /[/\\\u0000-\u001f\u007f]/;
8
8
 
@@ -13,7 +13,7 @@ export function isSafeInstanceName(name) {
13
13
  }
14
14
 
15
15
  /**
16
- * A path → the route it names: { panel: "chat" | "view" | "details", instance } | { panel: "fleet", tab } |
16
+ * A path → the route it names: { panel: "chat" | "view" | "details" | "log", instance } | { panel: "fleet", tab } |
17
17
  * { panel: "settings", section } | null (not a page of the app: a full load). A malformed name is null too; the
18
18
  * server answers it 400.
19
19
  */
@@ -28,6 +28,9 @@ export function parseRoute(pathname) {
28
28
  if (m) return FLEET_TABS.includes(m[1]) ? { panel: "fleet", tab: m[1] } : null;
29
29
  m = /^\/ui\/chat\/([^/]+)$/.exec(pathname);
30
30
  if (m) return named("chat", m[1]);
31
+ // #1218: one instance's Log.
32
+ m = /^\/ui\/log\/([^/]+)$/.exec(pathname);
33
+ if (m) return named("log", m[1]);
31
34
  if (pathname === "/view") return { panel: "view", instance: null };
32
35
  m = /^\/view\/([^/]+)$/.exec(pathname);
33
36
  if (m) return named("view", m[1]);
@@ -45,6 +48,7 @@ function named(panel, segment) {
45
48
  export function chatPath(instance) { return `/ui/chat/${encodeURIComponent(instance)}`; }
46
49
  export function viewPath(instance) { return instance ? `/view/${encodeURIComponent(instance)}` : "/view"; }
47
50
  export function detailsPath(instance) { return `/ui/fleet/agent/${encodeURIComponent(instance)}`; }
51
+ export function logPath(instance) { return `/ui/log/${encodeURIComponent(instance)}`; }
48
52
  export function fleetPath(tab) { return tab && tab !== "tasks" ? `/ui/fleet/${tab}` : "/ui/fleet"; }
49
53
  export const NEEDS_PATH = "/ui/needs";
50
54
  export function settingsPath(section) { return section && section !== "agents" ? `/settings/${section}` : "/settings"; }
@@ -55,6 +59,7 @@ export function routePath(route) {
55
59
  if (route.panel === "needs") return NEEDS_PATH;
56
60
  if (route.panel === "view") return viewPath(route.instance);
57
61
  if (route.panel === "details") return detailsPath(route.instance);
62
+ if (route.panel === "log") return logPath(route.instance);
58
63
  return route.instance ? chatPath(route.instance) : "/ui";
59
64
  }
60
65
  /** One string per route: a change of it is a new navigation (a new lease for the panel, §4). */