@songsid/agend 2.2.0-beta.2 → 2.2.0-beta.20

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 (389) hide show
  1. package/README.md +40 -24
  2. package/README.zh-TW.md +40 -24
  3. package/dist/agent-cli-instructions.md +6 -2
  4. package/dist/agent-cli.js +15 -7
  5. package/dist/agent-cli.js.map +1 -1
  6. package/dist/agent-endpoint.js +3 -0
  7. package/dist/agent-endpoint.js.map +1 -1
  8. package/dist/agent-whats-new.js +16 -4
  9. package/dist/agent-whats-new.js.map +1 -1
  10. package/dist/auth-api.d.ts +3 -1
  11. package/dist/auth-api.js +17 -1
  12. package/dist/auth-api.js.map +1 -1
  13. package/dist/backend/antigravity.d.ts +33 -0
  14. package/dist/backend/antigravity.js +126 -18
  15. package/dist/backend/antigravity.js.map +1 -1
  16. package/dist/backend/claude-carry.d.ts +22 -0
  17. package/dist/backend/claude-carry.js +170 -0
  18. package/dist/backend/claude-carry.js.map +1 -0
  19. package/dist/backend/claude-code.d.ts +65 -2
  20. package/dist/backend/claude-code.js +226 -33
  21. package/dist/backend/claude-code.js.map +1 -1
  22. package/dist/backend/codex-metadata.d.ts +33 -0
  23. package/dist/backend/codex-metadata.js +122 -18
  24. package/dist/backend/codex-metadata.js.map +1 -1
  25. package/dist/backend/codex.d.ts +3 -0
  26. package/dist/backend/codex.js +17 -1
  27. package/dist/backend/codex.js.map +1 -1
  28. package/dist/backend/credential-profile.d.ts +40 -3
  29. package/dist/backend/credential-profile.js +113 -5
  30. package/dist/backend/credential-profile.js.map +1 -1
  31. package/dist/backend/effort-metadata.d.ts +37 -1
  32. package/dist/backend/effort-metadata.js +86 -15
  33. package/dist/backend/effort-metadata.js.map +1 -1
  34. package/dist/backend/grok.d.ts +4 -0
  35. package/dist/backend/grok.js +14 -0
  36. package/dist/backend/grok.js.map +1 -1
  37. package/dist/backend/kiro.d.ts +27 -1
  38. package/dist/backend/kiro.js +63 -10
  39. package/dist/backend/kiro.js.map +1 -1
  40. package/dist/backend/launch-effort.d.ts +61 -0
  41. package/dist/backend/launch-effort.js +170 -0
  42. package/dist/backend/launch-effort.js.map +1 -0
  43. package/dist/backend/muse.d.ts +84 -0
  44. package/dist/backend/muse.js +449 -53
  45. package/dist/backend/muse.js.map +1 -1
  46. package/dist/backend/types.d.ts +48 -2
  47. package/dist/backend/types.js +12 -1
  48. package/dist/backend/types.js.map +1 -1
  49. package/dist/backend-command.d.ts +91 -0
  50. package/dist/backend-command.js +122 -0
  51. package/dist/backend-command.js.map +1 -0
  52. package/dist/background-work-ended.d.ts +18 -0
  53. package/dist/background-work-ended.js +30 -0
  54. package/dist/background-work-ended.js.map +1 -0
  55. package/dist/cache-service.d.ts +2 -0
  56. package/dist/cache-service.js +2 -1
  57. package/dist/cache-service.js.map +1 -1
  58. package/dist/channel/adapters/discord.d.ts +26 -1
  59. package/dist/channel/adapters/discord.js +103 -3
  60. package/dist/channel/adapters/discord.js.map +1 -1
  61. package/dist/channel/adapters/telegram.d.ts +35 -3
  62. package/dist/channel/adapters/telegram.js +122 -23
  63. package/dist/channel/adapters/telegram.js.map +1 -1
  64. package/dist/channel/mcp-tools.js +5 -3
  65. package/dist/channel/mcp-tools.js.map +1 -1
  66. package/dist/channel/message-edit-errors.d.ts +5 -0
  67. package/dist/channel/message-edit-errors.js +24 -0
  68. package/dist/channel/message-edit-errors.js.map +1 -0
  69. package/dist/channel/telegram-markdown.d.ts +6 -0
  70. package/dist/channel/telegram-markdown.js +104 -0
  71. package/dist/channel/telegram-markdown.js.map +1 -0
  72. package/dist/channel/tool-router.d.ts +2 -0
  73. package/dist/channel/tool-router.js +6 -5
  74. package/dist/channel/tool-router.js.map +1 -1
  75. package/dist/channel/types.d.ts +46 -4
  76. package/dist/cli.js +184 -34
  77. package/dist/cli.js.map +1 -1
  78. package/dist/command-table.d.ts +2 -2
  79. package/dist/command-table.js +5 -2
  80. package/dist/command-table.js.map +1 -1
  81. package/dist/compact-watch.d.ts +86 -0
  82. package/dist/compact-watch.js +288 -0
  83. package/dist/compact-watch.js.map +1 -0
  84. package/dist/config-validator.js +55 -5
  85. package/dist/config-validator.js.map +1 -1
  86. package/dist/config.d.ts +1 -0
  87. package/dist/config.js +2 -1
  88. package/dist/config.js.map +1 -1
  89. package/dist/cross-instance-envelope.js +2 -0
  90. package/dist/cross-instance-envelope.js.map +1 -1
  91. package/dist/daemon.d.ts +129 -1
  92. package/dist/daemon.js +754 -94
  93. package/dist/daemon.js.map +1 -1
  94. package/dist/delivery-outbox.d.ts +64 -8
  95. package/dist/delivery-outbox.js +315 -24
  96. package/dist/delivery-outbox.js.map +1 -1
  97. package/dist/delivery-reconciliation.d.ts +18 -1
  98. package/dist/delivery-reconciliation.js +67 -2
  99. package/dist/delivery-reconciliation.js.map +1 -1
  100. package/dist/doctor.js +7 -0
  101. package/dist/doctor.js.map +1 -1
  102. package/dist/fleet-context.d.ts +12 -1
  103. package/dist/fleet-manager.d.ts +392 -19
  104. package/dist/fleet-manager.js +2030 -275
  105. package/dist/fleet-manager.js.map +1 -1
  106. package/dist/forged-envelope-scan.d.ts +4 -3
  107. package/dist/forged-envelope-scan.js +4 -3
  108. package/dist/forged-envelope-scan.js.map +1 -1
  109. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +84 -16
  110. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +16 -1
  111. package/dist/general-knowledge/skills/model-discovery/SKILL.md +4 -1
  112. package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +4 -0
  113. package/dist/host-memory.d.ts +10 -0
  114. package/dist/host-memory.js +33 -1
  115. package/dist/host-memory.js.map +1 -1
  116. package/dist/human-delivery-receipts.d.ts +83 -0
  117. package/dist/human-delivery-receipts.js +194 -0
  118. package/dist/human-delivery-receipts.js.map +1 -0
  119. package/dist/install-lock.d.ts +18 -1
  120. package/dist/install-lock.js +30 -3
  121. package/dist/install-lock.js.map +1 -1
  122. package/dist/instance-avatar.d.ts +5 -4
  123. package/dist/instance-avatar.js +12 -8
  124. package/dist/instance-avatar.js.map +1 -1
  125. package/dist/instance-config-impact.js +1 -0
  126. package/dist/instance-config-impact.js.map +1 -1
  127. package/dist/instance-lifecycle.d.ts +25 -6
  128. package/dist/instance-lifecycle.js +76 -27
  129. package/dist/instance-lifecycle.js.map +1 -1
  130. package/dist/instructions.d.ts +1 -1
  131. package/dist/instructions.js +3 -3
  132. package/dist/instructions.js.map +1 -1
  133. package/dist/kiro-compact-pane.d.ts +38 -0
  134. package/dist/kiro-compact-pane.js +57 -0
  135. package/dist/kiro-compact-pane.js.map +1 -0
  136. package/dist/kiro-log-gate.d.ts +42 -0
  137. package/dist/kiro-log-gate.js +39 -0
  138. package/dist/kiro-log-gate.js.map +1 -0
  139. package/dist/launchd-handoff.d.ts +43 -0
  140. package/dist/launchd-handoff.js +56 -0
  141. package/dist/launchd-handoff.js.map +1 -0
  142. package/dist/locale.js +268 -42
  143. package/dist/locale.js.map +1 -1
  144. package/dist/log-service.d.ts +99 -0
  145. package/dist/log-service.js +243 -0
  146. package/dist/log-service.js.map +1 -0
  147. package/dist/log-steps.d.ts +169 -0
  148. package/dist/log-steps.js +858 -0
  149. package/dist/log-steps.js.map +1 -0
  150. package/dist/login-controller.d.ts +20 -3
  151. package/dist/login-controller.js +71 -19
  152. package/dist/login-controller.js.map +1 -1
  153. package/dist/login-flows.d.ts +5 -2
  154. package/dist/login-flows.js +5 -4
  155. package/dist/login-flows.js.map +1 -1
  156. package/dist/login-shell.d.ts +21 -0
  157. package/dist/login-shell.js +98 -0
  158. package/dist/login-shell.js.map +1 -0
  159. package/dist/memory-pressure.d.ts +13 -1
  160. package/dist/memory-pressure.js +50 -10
  161. package/dist/memory-pressure.js.map +1 -1
  162. package/dist/needs-you-hub.js +2 -0
  163. package/dist/needs-you-hub.js.map +1 -1
  164. package/dist/needs-you-live.d.ts +6 -0
  165. package/dist/needs-you-live.js +23 -3
  166. package/dist/needs-you-live.js.map +1 -1
  167. package/dist/outbound-handlers.d.ts +100 -0
  168. package/dist/outbound-handlers.js +277 -93
  169. package/dist/outbound-handlers.js.map +1 -1
  170. package/dist/outbound-schemas.d.ts +30 -15
  171. package/dist/outbound-schemas.js +23 -5
  172. package/dist/outbound-schemas.js.map +1 -1
  173. package/dist/provider-probe.d.ts +29 -0
  174. package/dist/provider-probe.js +198 -2
  175. package/dist/provider-probe.js.map +1 -1
  176. package/dist/public-link-progress.d.ts +1 -1
  177. package/dist/public-link-progress.js +8 -2
  178. package/dist/public-link-progress.js.map +1 -1
  179. package/dist/public-preview.d.ts +101 -0
  180. package/dist/public-preview.js +236 -0
  181. package/dist/public-preview.js.map +1 -0
  182. package/dist/public-web-gateway.d.ts +2 -0
  183. package/dist/public-web-gateway.js +20 -6
  184. package/dist/public-web-gateway.js.map +1 -1
  185. package/dist/public-web-link.d.ts +24 -1
  186. package/dist/public-web-link.js +63 -9
  187. package/dist/public-web-link.js.map +1 -1
  188. package/dist/quickstart-api.d.ts +1 -1
  189. package/dist/quickstart-api.js +8 -6
  190. package/dist/quickstart-api.js.map +1 -1
  191. package/dist/quickstart.d.ts +12 -0
  192. package/dist/quickstart.js +36 -6
  193. package/dist/quickstart.js.map +1 -1
  194. package/dist/reply-buttons-controller.d.ts +89 -5
  195. package/dist/reply-buttons-controller.js +273 -8
  196. package/dist/reply-buttons-controller.js.map +1 -1
  197. package/dist/reply-buttons.d.ts +48 -3
  198. package/dist/reply-buttons.js +105 -10
  199. package/dist/reply-buttons.js.map +1 -1
  200. package/dist/reply-polls.d.ts +85 -0
  201. package/dist/reply-polls.js +154 -0
  202. package/dist/reply-polls.js.map +1 -0
  203. package/dist/restart-guard.d.ts +6 -3
  204. package/dist/restart-guard.js +6 -3
  205. package/dist/restart-guard.js.map +1 -1
  206. package/dist/restart-progress.d.ts +33 -2
  207. package/dist/restart-progress.js +30 -2
  208. package/dist/restart-progress.js.map +1 -1
  209. package/dist/sd-notify.d.ts +22 -1
  210. package/dist/sd-notify.js +63 -10
  211. package/dist/sd-notify.js.map +1 -1
  212. package/dist/service-activation.d.ts +15 -7
  213. package/dist/service-activation.js +42 -26
  214. package/dist/service-activation.js.map +1 -1
  215. package/dist/service-installer.d.ts +55 -0
  216. package/dist/service-installer.js +90 -3
  217. package/dist/service-installer.js.map +1 -1
  218. package/dist/service-plan.d.ts +11 -0
  219. package/dist/service-plan.js +23 -0
  220. package/dist/service-plan.js.map +1 -1
  221. package/dist/settings-api.js +14 -4
  222. package/dist/settings-api.js.map +1 -1
  223. package/dist/settings-change.js +4 -2
  224. package/dist/settings-change.js.map +1 -1
  225. package/dist/settings-effect.js +2 -0
  226. package/dist/settings-effect.js.map +1 -1
  227. package/dist/spawn-gate.js +5 -1
  228. package/dist/spawn-gate.js.map +1 -1
  229. package/dist/spawn-hold-reply.d.ts +12 -0
  230. package/dist/spawn-hold-reply.js +39 -0
  231. package/dist/spawn-hold-reply.js.map +1 -0
  232. package/dist/systemd-peer-bus.d.ts +95 -0
  233. package/dist/systemd-peer-bus.js +430 -0
  234. package/dist/systemd-peer-bus.js.map +1 -0
  235. package/dist/systemd-runtime.d.ts +27 -1
  236. package/dist/systemd-runtime.js +69 -7
  237. package/dist/systemd-runtime.js.map +1 -1
  238. package/dist/tips.js +1 -1
  239. package/dist/tips.js.map +1 -1
  240. package/dist/tool-permissions.js +3 -3
  241. package/dist/tool-permissions.js.map +1 -1
  242. package/dist/tool-progress.js +3 -2
  243. package/dist/tool-progress.js.map +1 -1
  244. package/dist/topic-commands.d.ts +71 -4
  245. package/dist/topic-commands.js +167 -34
  246. package/dist/topic-commands.js.map +1 -1
  247. package/dist/transcript-delta-reader.d.ts +3 -3
  248. package/dist/transcript-delta-reader.js +8 -8
  249. package/dist/transcript-delta-reader.js.map +1 -1
  250. package/dist/transcript-jsonl.d.ts +85 -3
  251. package/dist/transcript-jsonl.js +156 -13
  252. package/dist/transcript-jsonl.js.map +1 -1
  253. package/dist/transcript-monitor.d.ts +11 -0
  254. package/dist/transcript-monitor.js +61 -13
  255. package/dist/transcript-monitor.js.map +1 -1
  256. package/dist/transcript-sources.d.ts +24 -0
  257. package/dist/transcript-sources.js +211 -56
  258. package/dist/transcript-sources.js.map +1 -1
  259. package/dist/tunnel/cloudflared.d.ts +7 -15
  260. package/dist/tunnel/cloudflared.js +36 -1
  261. package/dist/tunnel/cloudflared.js.map +1 -1
  262. package/dist/tunnel/purpose-lane.d.ts +5 -1
  263. package/dist/tunnel/purpose-lane.js +14 -7
  264. package/dist/tunnel/purpose-lane.js.map +1 -1
  265. package/dist/tunnel/types.d.ts +5 -1
  266. package/dist/tunnel/types.js.map +1 -1
  267. package/dist/turn-resume.d.ts +2 -0
  268. package/dist/turn-resume.js +1 -1
  269. package/dist/turn-resume.js.map +1 -1
  270. package/dist/types.d.ts +22 -0
  271. package/dist/ui/chat-commands.js +1 -0
  272. package/dist/ui/chat-mentions.js +63 -0
  273. package/dist/ui/chat-render.js +80 -10
  274. package/dist/ui/chat-store.js +70 -6
  275. package/dist/ui/chat-strings.js +62 -32
  276. package/dist/ui/chat-thread.js +199 -24
  277. package/dist/ui/chat-time.js +80 -0
  278. package/dist/ui/chat-tour.js +17 -14
  279. package/dist/ui/panel-chat.js +45 -21
  280. package/dist/ui/panel-details.js +2 -0
  281. package/dist/ui/panel-fleet.js +213 -58
  282. package/dist/ui/panel-log.js +428 -0
  283. package/dist/ui/panel-needs.js +14 -10
  284. package/dist/ui/panel-settings.js +82 -17
  285. package/dist/ui/preview-panel.js +55 -15
  286. package/dist/ui/preview-status.js +28 -0
  287. package/dist/ui/preview.js +44 -3
  288. package/dist/ui/settings-dialogs.js +11 -6
  289. package/dist/ui/settings-model.js +3 -1
  290. package/dist/ui/settings-strings.js +32 -12
  291. package/dist/ui/settings-token.js +8 -1
  292. package/dist/ui/settings-wizard.js +5 -3
  293. package/dist/ui/shared/app-i18n.js +41 -15
  294. package/dist/ui/shared/app-prefs.js +250 -0
  295. package/dist/ui/shared/app-route.js +6 -1
  296. package/dist/ui/shared/app-shell.js +165 -8
  297. package/dist/ui/shared/app-store.js +5 -1
  298. package/dist/ui/shared/app-stream.js +26 -5
  299. package/dist/ui/shared/app.css +170 -30
  300. package/dist/ui/shared/app.js +11 -1
  301. package/dist/ui/shared/fonts/README.md +1 -1
  302. package/dist/ui/shared/header-tools.js +83 -13
  303. package/dist/ui/shared/instance-nav.js +178 -51
  304. package/dist/ui/shared/instance-switch.js +8 -5
  305. package/dist/ui/shared/panel-view.js +21 -4
  306. package/dist/ui/shared/signin.js +15 -9
  307. package/dist/ui/shared/theme.js +39 -7
  308. package/dist/ui/shared/tokens.css +1 -1
  309. package/dist/ui/shared/ui-icons.js +2 -0
  310. package/dist/ui/shared/usage-dialog.js +23 -5
  311. package/dist/ui/shared/view-strings.js +40 -4
  312. package/dist/ui/signin.html +8 -3
  313. package/dist/unrecognised-prompt.d.ts +23 -0
  314. package/dist/unrecognised-prompt.js +74 -0
  315. package/dist/unrecognised-prompt.js.map +1 -0
  316. package/dist/update-check.d.ts +31 -0
  317. package/dist/update-check.js +47 -0
  318. package/dist/update-check.js.map +1 -1
  319. package/dist/update-install.d.ts +13 -1
  320. package/dist/update-install.js +16 -4
  321. package/dist/update-install.js.map +1 -1
  322. package/dist/update-launch.d.ts +48 -1
  323. package/dist/update-launch.js +159 -5
  324. package/dist/update-launch.js.map +1 -1
  325. package/dist/update-marker.d.ts +51 -0
  326. package/dist/update-marker.js +61 -0
  327. package/dist/update-marker.js.map +1 -1
  328. package/dist/usage/i18n-keys.d.ts +1 -1
  329. package/dist/usage/i18n-keys.js +1 -1
  330. package/dist/usage/i18n-keys.js.map +1 -1
  331. package/dist/usage/providers.d.ts +62 -13
  332. package/dist/usage/providers.js +256 -68
  333. package/dist/usage/providers.js.map +1 -1
  334. package/dist/usage/statusline-usage.d.ts +6 -2
  335. package/dist/usage/statusline-usage.js +10 -4
  336. package/dist/usage/statusline-usage.js.map +1 -1
  337. package/dist/view-api.js +31 -9
  338. package/dist/view-api.js.map +1 -1
  339. package/dist/web-api.d.ts +82 -4
  340. package/dist/web-api.js +632 -88
  341. package/dist/web-api.js.map +1 -1
  342. package/dist/web-auth.js +5 -2
  343. package/dist/web-auth.js.map +1 -1
  344. package/dist/web-chat-history.d.ts +26 -1
  345. package/dist/web-chat-history.js +47 -8
  346. package/dist/web-chat-history.js.map +1 -1
  347. package/dist/web-commands.d.ts +41 -5
  348. package/dist/web-commands.js +25 -7
  349. package/dist/web-commands.js.map +1 -1
  350. package/dist/web-login.d.ts +14 -0
  351. package/dist/web-login.js +4 -2
  352. package/dist/web-login.js.map +1 -1
  353. package/dist/web-mentions.d.ts +44 -0
  354. package/dist/web-mentions.js +75 -0
  355. package/dist/web-mentions.js.map +1 -0
  356. package/dist/web-prefs.d.ts +81 -0
  357. package/dist/web-prefs.js +297 -0
  358. package/dist/web-prefs.js.map +1 -0
  359. package/dist/web-preview.d.ts +14 -1
  360. package/dist/web-preview.js +35 -13
  361. package/dist/web-preview.js.map +1 -1
  362. package/dist/web-profile.d.ts +32 -0
  363. package/dist/web-profile.js +132 -0
  364. package/dist/web-profile.js.map +1 -0
  365. package/dist/web-reply-files.d.ts +45 -0
  366. package/dist/web-reply-files.js +223 -0
  367. package/dist/web-reply-files.js.map +1 -0
  368. package/dist/web-schedules.d.ts +12 -0
  369. package/dist/web-schedules.js +34 -0
  370. package/dist/web-schedules.js.map +1 -0
  371. package/dist/web-server-drain.d.ts +10 -0
  372. package/dist/web-server-drain.js +50 -0
  373. package/dist/web-server-drain.js.map +1 -0
  374. package/dist/web-session.d.ts +14 -0
  375. package/dist/web-session.js +11 -1
  376. package/dist/web-session.js.map +1 -1
  377. package/dist/web-settings-patch.d.ts +3 -1
  378. package/dist/web-settings-patch.js +4 -1
  379. package/dist/web-settings-patch.js.map +1 -1
  380. package/dist/web-shell-routes.d.ts +4 -0
  381. package/dist/web-shell-routes.js +9 -0
  382. package/dist/web-shell-routes.js.map +1 -1
  383. package/dist/web-upload.d.ts +135 -7
  384. package/dist/web-upload.js +315 -51
  385. package/dist/web-upload.js.map +1 -1
  386. package/launcher/install-admission.cjs +61 -6
  387. package/package.json +3 -3
  388. package/templates/launchd.plist.ejs +3 -1
  389. package/templates/systemd.service.ejs +2 -1
@@ -10,11 +10,12 @@ register("settings", {
10
10
  classicChannels: "ClassicBot Channels",
11
11
  noClassic: "No ClassicBot channels — start one with /start in a chat.",
12
12
  publicLink: "Offer temporary public web links",
13
- publicLinkHint: "Explicit admin consent on every request. Cloudflare handles TLS; link users have full web-admin access. No preview exposure.",
13
+ publicLinkHint: "Explicit admin consent on every request. Cloudflare handles TLS; link users have full web-admin access. HTML previews get a second temporary tunnel unless web.preview_on_public_link is false.",
14
14
  publicTtl: "Public link minutes (1–480)",
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",
@@ -163,7 +164,9 @@ register("settings", {
163
164
  wizardWaitingStart: "Waiting for a message in the group…",
164
165
  wizardNoStartYet: "No group message yet — post one and press Detect again.",
165
166
  wizardGeneralChannel: "General channel id",
166
- wizardAdminUser: "Your user id (becomes the first allowed user)",
167
+ wizardAdminUser: "Your user ID (required; becomes the first allowed user)",
168
+ wizardNeedDiscordAdmin: "Enter your Discord user ID: 17–20 digits, without a leading zero.",
169
+ wizardNeedTelegramAdmin: "Enter your Telegram user ID: 1–20 positive digits, without a leading zero.",
167
170
  wizardNeedVerify: "Verify the token before continuing.",
168
171
  wizardPlanLoading: "Building the plan…",
169
172
  wizardWillWrite: "This will be written:",
@@ -175,7 +178,7 @@ register("settings", {
175
178
  deleteBot: "Delete this bot",
176
179
  noAllowedUsers: "no allowed users",
177
180
  searchAll: "Search agents, bots, channels…",
178
- accessMovedHint: "Who may use the bot is set on the connection itself — open Settings on \"{0}\" under Connections & Bots.",
181
+ accessMovedHint: "Who may use the bot is set on the connection itself — open Settings on \"{0}\" under Connections.",
179
182
  applyRestartNeeded: "Saved — restart AgEnD to apply",
180
183
  applyNoChange: "no change needed",
181
184
  applyRestartHint: "This change needs AgEnD itself restarted. You can restart it here, or run /restart in your chat channel.",
@@ -217,7 +220,14 @@ register("settings", {
217
220
  concurrencyWarning: "May exceed CPU capacity on smaller hosts",
218
221
  staggerRange: "Must be between 0 and 30000",
219
222
  staggerWarning: "Very low values can cause startup spikes",
220
- sec_device: "This device", deviceLead: "Choices for this browser only — other devices keep their own.",
223
+ sec_device: "Preferences", syncedTitle: "Synced across your devices", syncedLead: "Changes here apply on every device where you are signed in.",
224
+ profileTitle: "Your name",
225
+ profileLeadBound: "Agents see your web messages as the Discord or Telegram account your sign-in code was sent to: its id, and its name unless you choose another here. It follows you to every device you sign in on. It is for recognition only: it grants nothing.",
226
+ profileLeadUnbound: "This browser was not signed in with a code sent to your Discord or Telegram account, so agents see your web messages as \"web-user\" (or the name you set here). Sign in with a code from /web to be recognised as your account.",
227
+ profileName: "Name", profileHelp: "Agents see: {0} · id {1}", profileSave: "Save", profileClear: "Use web-user", profileUseAccount: "Use {0}",
228
+ profileSaved: "Agents now see you as {0}", profileFailed: "Could not save the name",
229
+ syncedPublicLink: "On the public link these are read-only: a change here stays in this browser.",
230
+ 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
231
  sec_agents: "Agents",
222
232
  sec_bots: "Connections",
223
233
  sec_classic: "ClassicBot",
@@ -278,7 +288,7 @@ register("settings", {
278
288
  accessTitle: "Access",
279
289
  webChat: "Web chat",
280
290
  sec_web: "Web",
281
- 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.",
291
+ 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 chat (General topic) or on the host; one that tightens it applies at once.",
282
292
  webPublicRefused: "Web settings can't be changed over a temporary public link. Open the dashboard at its own address to change them.",
283
293
  webSessionTitle: "Sign-in sessions",
284
294
  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.",
@@ -362,7 +372,7 @@ register("settings", {
362
372
  help1: "Agents: each agent's settings, Start/Stop, Pause/Wake, and New agent.",
363
373
  help2: "Connections: who may use each bot, its binding, its status emojis, a new token; and provider API keys.",
364
374
  help3: "ClassicBot: each room's backend, model and options.",
365
- help4: "General: defaults for every agent, ClassicBot defaults and web chat. Review changes stages them.",
375
+ help4: "General: defaults for every agent and ClassicBot defaults. Web: the dashboard's sign-in sessions, browser terminal, web echo, Log and public link. Review changes stages them.",
366
376
  help5: "Changes are staged, then applied together with Apply changes. Each shows what it costs: immediately, an agent restart, or an AgEnD restart.",
367
377
  help6: "A change to access, secrets or connections waits for a fleet admin to confirm it in chat before it applies.",
368
378
  },
@@ -398,11 +408,12 @@ register("settings", {
398
408
  classicChannels: "ClassicBot 頻道",
399
409
  noClassic: "沒有 ClassicBot 頻道 — 在聊天中用 /start 建立。",
400
410
  publicLink: "提供臨時公開網頁連結",
401
- publicLinkHint: "每次都需管理員明確啟用。Cloudflare 終止 TLS;登入者具有完整網頁管理權限,不公開 preview。",
411
+ publicLinkHint: "每次都需管理員明確啟用。Cloudflare 終止 TLS;登入者具有完整網頁管理權限。HTML 預覽會另開一條臨時通道,除非設定 web.preview_on_public_link: false。",
402
412
  publicTtl: "公開連結分鐘數(1–480)",
403
413
  publicProtocol: "Tunnel 協定",
404
414
  webEcho: "Web 同步",
405
415
  webEchoHint: "將網頁訊息複製到綁定的 Telegram 或 Discord 主題,不包含 ClassicBot 群組。",
416
+ webLogTab: "紀錄分頁:每個 agent 的步驟", webLogTabHint: "在每個 instance 的「紀錄」分頁顯示提示、回覆、工具呼叫與結果,以及 fleet 事件;只在分頁開著時從它自己的對話紀錄讀取。機密值會遮蔽。公開連結也看得到。開啟需 fleet admin 確認。",
406
417
  general: "一般設定",
407
418
  advanced: "進階",
408
419
  new: "新增",
@@ -526,7 +537,9 @@ register("settings", {
526
537
  wizardWaitingStart: "等待群組訊息中…",
527
538
  wizardNoStartYet: "還沒收到群組訊息 —— 發一則之後再按一次。",
528
539
  wizardGeneralChannel: "General 頻道 id",
529
- wizardAdminUser: "你的 user id(會成為第一個允許使用者)",
540
+ wizardAdminUser: "你的使用者 ID(必填;會成為第一個允許使用者)",
541
+ wizardNeedDiscordAdmin: "請輸入 Discord 使用者 ID:17–20 位數字,開頭不能是 0。",
542
+ wizardNeedTelegramAdmin: "請輸入 Telegram 使用者 ID:1–20 位正整數字,開頭不能是 0。",
530
543
  wizardNeedVerify: "請先驗證 token 再繼續。",
531
544
  wizardPlanLoading: "正在產生方案…",
532
545
  wizardWillWrite: "將寫入:",
@@ -538,7 +551,7 @@ register("settings", {
538
551
  deleteBot: "刪除這個機器人",
539
552
  noAllowedUsers: "沒有允許的使用者",
540
553
  searchAll: "搜尋 agent、機器人、頻道…",
541
- accessMovedHint: "誰可以使用 bot 是在連線本身設定 —— 請到「連線與機器人」開啟「{0}」的設定。",
554
+ accessMovedHint: "誰可以使用 bot 是在連線本身設定 —— 請到「連線」開啟「{0}」的設定。",
542
555
  applyRestartNeeded: "已儲存 —— 需要重新啟動 AgEnD 才會生效",
543
556
  applyNoChange: "無需變更",
544
557
  applyRestartHint: "此變更需要重新啟動 AgEnD 本身。可以在這裡重新啟動,或在聊天頻道執行 /restart。",
@@ -580,7 +593,14 @@ register("settings", {
580
593
  concurrencyWarning: "在較小的主機上可能超出 CPU 負荷",
581
594
  staggerRange: "必須介於 0 到 30000",
582
595
  staggerWarning: "數值太低可能造成啟動尖峰",
583
- sec_device: "這台裝置", deviceLead: "只套用在這個瀏覽器,其他裝置保留各自的設定。",
596
+ sec_device: "偏好設定", syncedTitle: "在你所有的裝置同步", syncedLead: "這裡的變更會套用到你登入的每一台裝置。",
597
+ profileTitle: "你的名字",
598
+ profileLeadBound: "agent 會把你從網頁送出的訊息,認成收到登入驗證碼的那個 Discord 或 Telegram 帳號:用它的 id,名字也用它的,除非你在這裡另外取名。它會跟著你到每一台登入的裝置。這只用來辨認,不代表任何權限。",
599
+ profileLeadUnbound: "這個瀏覽器不是用私訊到你 Discord 或 Telegram 帳號的驗證碼登入的,所以 agent 會把你的網頁訊息看成「web-user」(或你在這裡設定的名字)。用 /web 的驗證碼登入,就能被認成你的帳號。",
600
+ profileName: "名字", profileHelp: "agent 看到:{0} · id {1}", profileSave: "儲存", profileClear: "改回 web-user", profileUseAccount: "改用 {0}",
601
+ profileSaved: "agent 現在會看到你是 {0}", profileFailed: "無法儲存名字",
602
+ syncedPublicLink: "公開連結只能讀取這些設定:在這裡變更只會留在這個瀏覽器。",
603
+ deviceTitle: "只限這台裝置", deviceLead: "導覽、文字大小、視圖寬度、側欄顯示或隱藏、面板寬度與桌面通知,每台裝置各自保留。",
584
604
  sec_agents: "Agent",
585
605
  sec_bots: "連線",
586
606
  sec_classic: "ClassicBot",
@@ -641,7 +661,7 @@ register("settings", {
641
661
  accessTitle: "存取",
642
662
  webChat: "網頁聊天",
643
663
  sec_web: "網頁",
644
- webLead: "網頁儀表板的行為。放寬存取的變更(延長 session、重新開啟或延長瀏覽器終端機、公開連結)需要 fleet admin 在 General 確認;收緊的變更會立即套用。",
664
+ webLead: "網頁儀表板的行為。放寬存取的變更(延長 session、重新開啟或延長瀏覽器終端機、公開連結)需要 fleet admin 在聊天(General topic)或主機上確認;收緊的變更會立即套用。",
645
665
  webPublicRefused: "無法透過臨時公開連結變更網頁設定。請從儀表板本身的網址開啟後再變更。",
646
666
  webSessionTitle: "登入 session",
647
667
  webSessionHint: "適用於從儀表板本身網址登入的 session(本機、區域網路、tailnet)。臨時公開連結固定為 4 小時 / 30 分鐘。縮短會讓已超過新限制的 session 結束;延長會套用到新的登入,以及既有 session 下次使用時的閒置時間。留空 = 預設值。",
@@ -725,7 +745,7 @@ register("settings", {
725
745
  help1: "Agent:每個 agent 的設定、啟動/停止、暫停/喚醒,以及新增 agent。",
726
746
  help2: "連線:誰可以使用各個 bot、它的綁定、狀態表情、新的 token;以及供應商 API key。",
727
747
  help3: "ClassicBot:每個房間的 backend、model 與選項。",
728
- help4: "一般:所有 agent 的預設值、ClassicBot 預設值與網頁聊天。「檢查變更」會把它們暫存。",
748
+ help4: "一般:所有 agent 的預設值與 ClassicBot 預設值。網頁:儀表板的登入 session、瀏覽器終端機、網頁轉發、紀錄與公開連結。「檢查變更」會把它們暫存。",
729
749
  help5: "變更會先暫存,再用「套用變更」一起套用。每項都會標示代價:立即生效、重新啟動 agent,或重新啟動 AgEnD。",
730
750
  help6: "存取、密鑰或連線的變更,要等 fleet 管理員在聊天中確認後才會套用。",
731
751
  },
@@ -10,6 +10,13 @@ import { api } from "./settings-confirm.js";
10
10
 
11
11
  const tn = (k, ...v) => t(`settings.${k}`, ...v);
12
12
 
13
+ // UI feedback only: the plan and commit independently enforce this on the server.
14
+ export const adminUserIdPattern = (platform) => platform === "discord" ? "[1-9][0-9]{16,19}" : "[1-9][0-9]{0,19}";
15
+ export function adminUserIdError(platform, value) {
16
+ return typeof value === "string" && new RegExp(`^${adminUserIdPattern(platform)}$`).test(value)
17
+ ? null : tn(platform === "discord" ? "wizardNeedDiscordAdmin" : "wizardNeedTelegramAdmin");
18
+ }
19
+
13
20
  /** Who this token is: { valid, username?, reason? } — one outbound check, the token sent once and not kept. */
14
21
  export async function verifyBotToken(platform, token) {
15
22
  const res = await api("/api/settings/quickstart/probe", { method: "POST", body: JSON.stringify({ action: "verify", platform, token }) })
@@ -31,7 +38,7 @@ export function TokenField({ id, platform, value, onInput, identity, busy, onVer
31
38
  const [shown, setShown] = useState(false);
32
39
  return html`<div class="field token-field"><label for=${id}>${tn("wizardToken")}</label>
33
40
  <div class="token-row">
34
- <input id=${id} type=${shown ? "text" : "password"} autocomplete="off" autocapitalize="off" spellcheck="false" disabled=${!!busy}
41
+ <input id=${id} type=${shown ? "text" : "password"} autocomplete="off" autocapitalize="off" spellcheck=${false} disabled=${!!busy}
35
42
  placeholder=${platform === "discord" ? "MTA…" : "123456:ABC-DEF…"} value=${value} onInput=${(e) => onInput(e.target.value.trim())} />
36
43
  <button type="button" class="icon-btn" aria-pressed=${shown ? "true" : "false"} title=${shown ? tn("tokenHide") : tn("tokenShow")}
37
44
  aria-label=${shown ? tn("tokenHide") : tn("tokenShow")} onClick=${() => setShown(!shown)}><${Icon} name="eye" size=${16} /></button>
@@ -10,7 +10,7 @@ import { api } from "./settings-confirm.js";
10
10
  import { startOperation } from "./settings-apply.js";
11
11
  import { BACKENDS, DEFAULT_SCHEMA, impactOf, toYaml } from "./settings-model.js";
12
12
  import { Drawer, Select } from "./settings-dialogs.js";
13
- import { botHandle, TokenEnvNote, TokenField, verifyBotToken } from "./settings-token.js";
13
+ import { adminUserIdError, adminUserIdPattern, botHandle, TokenEnvNote, TokenField, verifyBotToken } from "./settings-token.js";
14
14
  import { DiscordServerPicker } from "./settings-discord.js";
15
15
 
16
16
  const tn = (k, ...v) => t(`settings.${k}`, ...v);
@@ -90,6 +90,8 @@ export function SetupWizard({ ctx, onClose }) {
90
90
  if (w.step === 2) { setW({ ...w, step: 3 }); return; }
91
91
  if (w.step === 3) {
92
92
  if (!w.identity || !w.identity.valid) { setErr(tn("wizardNeedVerify")); return; }
93
+ const adminError = adminUserIdError(w.platform, w.admin_user_id);
94
+ if (adminError) { setErr(adminError); return; }
93
95
  const at = rev.current;
94
96
  setBusy(tn("wizardPlanLoading"));
95
97
  const res = await post("/api/settings/quickstart/plan", input());
@@ -146,12 +148,12 @@ export function SetupWizard({ ctx, onClose }) {
146
148
  <${DiscordServerPicker} idPrefix="wz" token=${w.identity && w.identity.valid ? w.token : ""} bot=${w.identity && botHandle(w.identity.username)}
147
149
  invite=${w.identity && w.identity.invite} portal=${w.identity && w.identity.portal} guilds=${w.guilds} onGuilds=${(g) => setW((x) => ({ ...x, guilds: g }))}
148
150
  guild=${w.guild_id} onGuild=${set("guild_id")} channel=${w.general_channel_id} onChannel=${set("general_channel_id")} />
149
- <div class="field"><label for="wz-user">${tn("wizardAdminUser")}</label><input id="wz-user" type="text" placeholder=${tn("discordUserPlaceholder")} value=${w.admin_user_id} onInput=${(e) => set("admin_user_id")(e.target.value.trim())} /></div>`
151
+ <div class="field"><label for="wz-user">${tn("wizardAdminUser")}</label><input id="wz-user" type="text" required=${true} inputmode="numeric" pattern=${adminUserIdPattern(w.platform)} placeholder=${tn("discordUserPlaceholder")} value=${w.admin_user_id} onInput=${(e) => set("admin_user_id")(e.target.value.trim())} /></div>`
150
152
  : html`
151
153
  <div class="field"><label for="wz-group">${tn("groupIdField")}</label><input id="wz-group" type="text" placeholder="-1001234567890" value=${w.group_id} onInput=${(e) => set("group_id")(e.target.value.trim())} />
152
154
  <p class="note">${tn("wizardDetectHint")}</p></div>
153
155
  <div class="dlg-inline-actions"><button type="button" class="btn btn-sm" disabled=${!!busy} onClick=${detect}>${tn("wizardDetect")}</button></div>
154
- <div class="field"><label for="wz-user">${tn("wizardAdminUser")}</label><input id="wz-user" type="text" placeholder=${tn("telegramUserPlaceholder")} value=${w.admin_user_id} onInput=${(e) => set("admin_user_id")(e.target.value.trim())} /></div>`}`;
156
+ <div class="field"><label for="wz-user">${tn("wizardAdminUser")}</label><input id="wz-user" type="text" required=${true} inputmode="numeric" pattern=${adminUserIdPattern(w.platform)} placeholder=${tn("telegramUserPlaceholder")} value=${w.admin_user_id} onInput=${(e) => set("admin_user_id")(e.target.value.trim())} /></div>`}`;
155
157
  } else {
156
158
  body = !w.plan ? html`<p class="note">${tn("wizardPlanLoading")}</p>` : html`
157
159
  <p><strong>${tn("wizardWillWrite")}</strong></p>
@@ -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,12 +60,14 @@ 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",
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",
47
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).",
@@ -67,12 +91,14 @@ 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: "用量",
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: "隱藏用量",
76
102
  statusRunning: "執行中", statusStopped: "已停止", statusPaused: "已暫停", statusCrashed: "已當掉", statusWorking: "工作中", statusStuck: "似乎卡住", statusIdle: "閒置",
77
103
  connReconnecting: "連不上 AgEnD——可能正在重新啟動。正在重新連線…", connReconnectingIn: "連不上 AgEnD——可能正在重新啟動。{0} 秒後再試。",
78
104
  hydrating: "正在載入目前的狀態…", hydrateFailed: "無法載入目前的狀態(提示、送達標記)。",
@@ -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
+ }
@@ -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). */