@songsid/agend 2.2.0-beta.8 → 2.2.0

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 (239) hide show
  1. package/README.md +40 -24
  2. package/README.zh-TW.md +40 -24
  3. package/dist/agent-endpoint.js +3 -0
  4. package/dist/agent-endpoint.js.map +1 -1
  5. package/dist/agent-whats-new.js +7 -1
  6. package/dist/agent-whats-new.js.map +1 -1
  7. package/dist/auth-api.d.ts +3 -1
  8. package/dist/auth-api.js +15 -1
  9. package/dist/auth-api.js.map +1 -1
  10. package/dist/backend/antigravity.d.ts +33 -0
  11. package/dist/backend/antigravity.js +126 -18
  12. package/dist/backend/antigravity.js.map +1 -1
  13. package/dist/backend/claude-code.d.ts +3 -0
  14. package/dist/backend/claude-code.js +9 -0
  15. package/dist/backend/claude-code.js.map +1 -1
  16. package/dist/backend/codex-metadata.d.ts +33 -0
  17. package/dist/backend/codex-metadata.js +122 -18
  18. package/dist/backend/codex-metadata.js.map +1 -1
  19. package/dist/backend/codex.d.ts +3 -0
  20. package/dist/backend/codex.js +17 -1
  21. package/dist/backend/codex.js.map +1 -1
  22. package/dist/backend/effort-metadata.d.ts +37 -1
  23. package/dist/backend/effort-metadata.js +86 -15
  24. package/dist/backend/effort-metadata.js.map +1 -1
  25. package/dist/backend/grok.d.ts +4 -0
  26. package/dist/backend/grok.js +14 -0
  27. package/dist/backend/grok.js.map +1 -1
  28. package/dist/backend/kiro.d.ts +18 -1
  29. package/dist/backend/kiro.js +35 -8
  30. package/dist/backend/kiro.js.map +1 -1
  31. package/dist/backend/launch-effort.d.ts +61 -0
  32. package/dist/backend/launch-effort.js +170 -0
  33. package/dist/backend/launch-effort.js.map +1 -0
  34. package/dist/backend/muse.d.ts +17 -0
  35. package/dist/backend/muse.js +86 -16
  36. package/dist/backend/muse.js.map +1 -1
  37. package/dist/backend/types.d.ts +40 -1
  38. package/dist/backend/types.js +12 -1
  39. package/dist/backend/types.js.map +1 -1
  40. package/dist/backend-command.d.ts +91 -0
  41. package/dist/backend-command.js +122 -0
  42. package/dist/backend-command.js.map +1 -0
  43. package/dist/channel/adapters/discord.d.ts +12 -0
  44. package/dist/channel/adapters/discord.js +49 -2
  45. package/dist/channel/adapters/discord.js.map +1 -1
  46. package/dist/channel/adapters/telegram.d.ts +18 -0
  47. package/dist/channel/adapters/telegram.js +24 -3
  48. package/dist/channel/adapters/telegram.js.map +1 -1
  49. package/dist/channel/mcp-tools.js +2 -0
  50. package/dist/channel/mcp-tools.js.map +1 -1
  51. package/dist/channel/tool-router.d.ts +2 -0
  52. package/dist/channel/tool-router.js +1 -1
  53. package/dist/channel/tool-router.js.map +1 -1
  54. package/dist/channel/types.d.ts +16 -0
  55. package/dist/cli.js +124 -24
  56. package/dist/cli.js.map +1 -1
  57. package/dist/command-table.d.ts +2 -2
  58. package/dist/command-table.js +5 -2
  59. package/dist/command-table.js.map +1 -1
  60. package/dist/compact-watch.d.ts +86 -0
  61. package/dist/compact-watch.js +288 -0
  62. package/dist/compact-watch.js.map +1 -0
  63. package/dist/config-validator.js +39 -0
  64. package/dist/config-validator.js.map +1 -1
  65. package/dist/config.d.ts +1 -0
  66. package/dist/config.js +2 -1
  67. package/dist/config.js.map +1 -1
  68. package/dist/daemon.d.ts +13 -1
  69. package/dist/daemon.js +160 -13
  70. package/dist/daemon.js.map +1 -1
  71. package/dist/delivery-outbox.d.ts +8 -1
  72. package/dist/delivery-outbox.js +6 -2
  73. package/dist/delivery-outbox.js.map +1 -1
  74. package/dist/doctor.js +7 -0
  75. package/dist/doctor.js.map +1 -1
  76. package/dist/fleet-context.d.ts +7 -0
  77. package/dist/fleet-manager.d.ts +160 -10
  78. package/dist/fleet-manager.js +837 -121
  79. package/dist/fleet-manager.js.map +1 -1
  80. package/dist/forged-envelope-scan.d.ts +4 -3
  81. package/dist/forged-envelope-scan.js +4 -3
  82. package/dist/forged-envelope-scan.js.map +1 -1
  83. package/dist/general-knowledge/skills/model-discovery/SKILL.md +4 -1
  84. package/dist/install-lock.d.ts +18 -1
  85. package/dist/install-lock.js +30 -3
  86. package/dist/install-lock.js.map +1 -1
  87. package/dist/instance-avatar.d.ts +5 -4
  88. package/dist/instance-avatar.js +12 -8
  89. package/dist/instance-avatar.js.map +1 -1
  90. package/dist/instance-lifecycle.js +3 -1
  91. package/dist/instance-lifecycle.js.map +1 -1
  92. package/dist/instructions.js +2 -2
  93. package/dist/instructions.js.map +1 -1
  94. package/dist/kiro-compact-pane.d.ts +38 -0
  95. package/dist/kiro-compact-pane.js +57 -0
  96. package/dist/kiro-compact-pane.js.map +1 -0
  97. package/dist/kiro-log-gate.d.ts +42 -0
  98. package/dist/kiro-log-gate.js +39 -0
  99. package/dist/kiro-log-gate.js.map +1 -0
  100. package/dist/launchd-handoff.d.ts +43 -0
  101. package/dist/launchd-handoff.js +56 -0
  102. package/dist/launchd-handoff.js.map +1 -0
  103. package/dist/locale.js +202 -30
  104. package/dist/locale.js.map +1 -1
  105. package/dist/log-service.js +10 -2
  106. package/dist/log-service.js.map +1 -1
  107. package/dist/log-steps.d.ts +26 -0
  108. package/dist/log-steps.js +215 -3
  109. package/dist/log-steps.js.map +1 -1
  110. package/dist/login-shell.d.ts +21 -0
  111. package/dist/login-shell.js +98 -0
  112. package/dist/login-shell.js.map +1 -0
  113. package/dist/outbound-handlers.d.ts +27 -0
  114. package/dist/outbound-handlers.js +120 -36
  115. package/dist/outbound-handlers.js.map +1 -1
  116. package/dist/outbound-schemas.d.ts +12 -0
  117. package/dist/outbound-schemas.js +15 -4
  118. package/dist/outbound-schemas.js.map +1 -1
  119. package/dist/provider-probe.d.ts +29 -0
  120. package/dist/provider-probe.js +198 -2
  121. package/dist/provider-probe.js.map +1 -1
  122. package/dist/public-web-gateway.d.ts +2 -0
  123. package/dist/public-web-gateway.js +11 -1
  124. package/dist/public-web-gateway.js.map +1 -1
  125. package/dist/quickstart-api.d.ts +1 -1
  126. package/dist/quickstart-api.js +8 -6
  127. package/dist/quickstart-api.js.map +1 -1
  128. package/dist/quickstart.d.ts +12 -0
  129. package/dist/quickstart.js +36 -6
  130. package/dist/quickstart.js.map +1 -1
  131. package/dist/reply-buttons-controller.d.ts +86 -5
  132. package/dist/reply-buttons-controller.js +271 -7
  133. package/dist/reply-buttons-controller.js.map +1 -1
  134. package/dist/reply-buttons.d.ts +48 -3
  135. package/dist/reply-buttons.js +105 -10
  136. package/dist/reply-buttons.js.map +1 -1
  137. package/dist/reply-polls.d.ts +85 -0
  138. package/dist/reply-polls.js +154 -0
  139. package/dist/reply-polls.js.map +1 -0
  140. package/dist/restart-guard.d.ts +6 -3
  141. package/dist/restart-guard.js +6 -3
  142. package/dist/restart-guard.js.map +1 -1
  143. package/dist/restart-progress.d.ts +0 -2
  144. package/dist/restart-progress.js +0 -2
  145. package/dist/restart-progress.js.map +1 -1
  146. package/dist/sd-notify.d.ts +22 -1
  147. package/dist/sd-notify.js +63 -10
  148. package/dist/sd-notify.js.map +1 -1
  149. package/dist/service-activation.d.ts +15 -7
  150. package/dist/service-activation.js +42 -26
  151. package/dist/service-activation.js.map +1 -1
  152. package/dist/service-installer.d.ts +24 -0
  153. package/dist/service-installer.js +49 -1
  154. package/dist/service-installer.js.map +1 -1
  155. package/dist/service-plan.d.ts +11 -0
  156. package/dist/service-plan.js +23 -0
  157. package/dist/service-plan.js.map +1 -1
  158. package/dist/systemd-peer-bus.d.ts +95 -0
  159. package/dist/systemd-peer-bus.js +430 -0
  160. package/dist/systemd-peer-bus.js.map +1 -0
  161. package/dist/systemd-runtime.d.ts +27 -1
  162. package/dist/systemd-runtime.js +69 -7
  163. package/dist/systemd-runtime.js.map +1 -1
  164. package/dist/tool-permissions.js +3 -3
  165. package/dist/tool-permissions.js.map +1 -1
  166. package/dist/topic-commands.d.ts +68 -2
  167. package/dist/topic-commands.js +141 -27
  168. package/dist/topic-commands.js.map +1 -1
  169. package/dist/transcript-jsonl.d.ts +7 -2
  170. package/dist/transcript-jsonl.js +13 -7
  171. package/dist/transcript-jsonl.js.map +1 -1
  172. package/dist/transcript-sources.js +6 -1
  173. package/dist/transcript-sources.js.map +1 -1
  174. package/dist/types.d.ts +8 -0
  175. package/dist/ui/chat-commands.js +1 -0
  176. package/dist/ui/chat-render.js +55 -10
  177. package/dist/ui/chat-store.js +69 -5
  178. package/dist/ui/chat-strings.js +28 -16
  179. package/dist/ui/chat-thread.js +132 -14
  180. package/dist/ui/chat-tour.js +17 -14
  181. package/dist/ui/panel-chat.js +16 -10
  182. package/dist/ui/panel-fleet.js +15 -2
  183. package/dist/ui/panel-log.js +55 -15
  184. package/dist/ui/panel-needs.js +10 -7
  185. package/dist/ui/panel-settings.js +58 -7
  186. package/dist/ui/settings-dialogs.js +11 -6
  187. package/dist/ui/settings-model.js +3 -1
  188. package/dist/ui/settings-strings.js +24 -10
  189. package/dist/ui/settings-token.js +7 -0
  190. package/dist/ui/settings-wizard.js +5 -3
  191. package/dist/ui/shared/app-i18n.js +1 -1
  192. package/dist/ui/shared/app-stream.js +4 -1
  193. package/dist/ui/shared/app.css +52 -10
  194. package/dist/ui/shared/header-tools.js +34 -14
  195. package/dist/ui/shared/instance-switch.js +5 -2
  196. package/dist/ui/shared/panel-view.js +15 -4
  197. package/dist/ui/shared/signin.js +15 -9
  198. package/dist/ui/shared/ui-icons.js +1 -0
  199. package/dist/ui/shared/view-strings.js +10 -2
  200. package/dist/ui/signin.html +8 -3
  201. package/dist/unrecognised-prompt.js +2 -2
  202. package/dist/unrecognised-prompt.js.map +1 -1
  203. package/dist/update-check.d.ts +31 -0
  204. package/dist/update-check.js +47 -0
  205. package/dist/update-check.js.map +1 -1
  206. package/dist/update-launch.d.ts +48 -1
  207. package/dist/update-launch.js +159 -5
  208. package/dist/update-launch.js.map +1 -1
  209. package/dist/usage/providers.d.ts +28 -8
  210. package/dist/usage/providers.js +96 -41
  211. package/dist/usage/providers.js.map +1 -1
  212. package/dist/view-api.js +26 -9
  213. package/dist/view-api.js.map +1 -1
  214. package/dist/web-api.d.ts +44 -2
  215. package/dist/web-api.js +265 -57
  216. package/dist/web-api.js.map +1 -1
  217. package/dist/web-chat-history.d.ts +26 -1
  218. package/dist/web-chat-history.js +47 -8
  219. package/dist/web-chat-history.js.map +1 -1
  220. package/dist/web-commands.d.ts +39 -3
  221. package/dist/web-commands.js +22 -5
  222. package/dist/web-commands.js.map +1 -1
  223. package/dist/web-login.d.ts +14 -0
  224. package/dist/web-login.js +4 -2
  225. package/dist/web-login.js.map +1 -1
  226. package/dist/web-prefs.d.ts +18 -0
  227. package/dist/web-prefs.js +36 -4
  228. package/dist/web-prefs.js.map +1 -1
  229. package/dist/web-profile.d.ts +32 -0
  230. package/dist/web-profile.js +132 -0
  231. package/dist/web-profile.js.map +1 -0
  232. package/dist/web-session.d.ts +14 -0
  233. package/dist/web-session.js +10 -0
  234. package/dist/web-session.js.map +1 -1
  235. package/dist/web-upload.d.ts +119 -4
  236. package/dist/web-upload.js +252 -39
  237. package/dist/web-upload.js.map +1 -1
  238. package/launcher/install-admission.cjs +61 -6
  239. package/package.json +3 -3
@@ -10,7 +10,7 @@ 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",
@@ -164,7 +164,9 @@ register("settings", {
164
164
  wizardWaitingStart: "Waiting for a message in the group…",
165
165
  wizardNoStartYet: "No group message yet — post one and press Detect again.",
166
166
  wizardGeneralChannel: "General channel id",
167
- 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.",
168
170
  wizardNeedVerify: "Verify the token before continuing.",
169
171
  wizardPlanLoading: "Building the plan…",
170
172
  wizardWillWrite: "This will be written:",
@@ -176,7 +178,7 @@ register("settings", {
176
178
  deleteBot: "Delete this bot",
177
179
  noAllowedUsers: "no allowed users",
178
180
  searchAll: "Search agents, bots, channels…",
179
- 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.",
180
182
  applyRestartNeeded: "Saved — restart AgEnD to apply",
181
183
  applyNoChange: "no change needed",
182
184
  applyRestartHint: "This change needs AgEnD itself restarted. You can restart it here, or run /restart in your chat channel.",
@@ -219,6 +221,11 @@ register("settings", {
219
221
  staggerRange: "Must be between 0 and 30000",
220
222
  staggerWarning: "Very low values can cause startup spikes",
221
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",
222
229
  syncedPublicLink: "On the public link these are read-only: a change here stays in this browser.",
223
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.",
224
231
  sec_agents: "Agents",
@@ -281,7 +288,7 @@ register("settings", {
281
288
  accessTitle: "Access",
282
289
  webChat: "Web chat",
283
290
  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.",
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.",
285
292
  webPublicRefused: "Web settings can't be changed over a temporary public link. Open the dashboard at its own address to change them.",
286
293
  webSessionTitle: "Sign-in sessions",
287
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.",
@@ -365,7 +372,7 @@ register("settings", {
365
372
  help1: "Agents: each agent's settings, Start/Stop, Pause/Wake, and New agent.",
366
373
  help2: "Connections: who may use each bot, its binding, its status emojis, a new token; and provider API keys.",
367
374
  help3: "ClassicBot: each room's backend, model and options.",
368
- 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.",
369
376
  help5: "Changes are staged, then applied together with Apply changes. Each shows what it costs: immediately, an agent restart, or an AgEnD restart.",
370
377
  help6: "A change to access, secrets or connections waits for a fleet admin to confirm it in chat before it applies.",
371
378
  },
@@ -401,7 +408,7 @@ register("settings", {
401
408
  classicChannels: "ClassicBot 頻道",
402
409
  noClassic: "沒有 ClassicBot 頻道 — 在聊天中用 /start 建立。",
403
410
  publicLink: "提供臨時公開網頁連結",
404
- publicLinkHint: "每次都需管理員明確啟用。Cloudflare 終止 TLS;登入者具有完整網頁管理權限,不公開 preview。",
411
+ publicLinkHint: "每次都需管理員明確啟用。Cloudflare 終止 TLS;登入者具有完整網頁管理權限。HTML 預覽會另開一條臨時通道,除非設定 web.preview_on_public_link: false。",
405
412
  publicTtl: "公開連結分鐘數(1–480)",
406
413
  publicProtocol: "Tunnel 協定",
407
414
  webEcho: "Web 同步",
@@ -530,7 +537,9 @@ register("settings", {
530
537
  wizardWaitingStart: "等待群組訊息中…",
531
538
  wizardNoStartYet: "還沒收到群組訊息 —— 發一則之後再按一次。",
532
539
  wizardGeneralChannel: "General 頻道 id",
533
- wizardAdminUser: "你的 user id(會成為第一個允許使用者)",
540
+ wizardAdminUser: "你的使用者 ID(必填;會成為第一個允許使用者)",
541
+ wizardNeedDiscordAdmin: "請輸入 Discord 使用者 ID:17–20 位數字,開頭不能是 0。",
542
+ wizardNeedTelegramAdmin: "請輸入 Telegram 使用者 ID:1–20 位正整數字,開頭不能是 0。",
534
543
  wizardNeedVerify: "請先驗證 token 再繼續。",
535
544
  wizardPlanLoading: "正在產生方案…",
536
545
  wizardWillWrite: "將寫入:",
@@ -542,7 +551,7 @@ register("settings", {
542
551
  deleteBot: "刪除這個機器人",
543
552
  noAllowedUsers: "沒有允許的使用者",
544
553
  searchAll: "搜尋 agent、機器人、頻道…",
545
- accessMovedHint: "誰可以使用 bot 是在連線本身設定 —— 請到「連線與機器人」開啟「{0}」的設定。",
554
+ accessMovedHint: "誰可以使用 bot 是在連線本身設定 —— 請到「連線」開啟「{0}」的設定。",
546
555
  applyRestartNeeded: "已儲存 —— 需要重新啟動 AgEnD 才會生效",
547
556
  applyNoChange: "無需變更",
548
557
  applyRestartHint: "此變更需要重新啟動 AgEnD 本身。可以在這裡重新啟動,或在聊天頻道執行 /restart。",
@@ -585,6 +594,11 @@ register("settings", {
585
594
  staggerRange: "必須介於 0 到 30000",
586
595
  staggerWarning: "數值太低可能造成啟動尖峰",
587
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: "無法儲存名字",
588
602
  syncedPublicLink: "公開連結只能讀取這些設定:在這裡變更只會留在這個瀏覽器。",
589
603
  deviceTitle: "只限這台裝置", deviceLead: "導覽、文字大小、視圖寬度、側欄顯示或隱藏、面板寬度與桌面通知,每台裝置各自保留。",
590
604
  sec_agents: "Agent",
@@ -647,7 +661,7 @@ register("settings", {
647
661
  accessTitle: "存取",
648
662
  webChat: "網頁聊天",
649
663
  sec_web: "網頁",
650
- webLead: "網頁儀表板的行為。放寬存取的變更(延長 session、重新開啟或延長瀏覽器終端機、公開連結)需要 fleet admin 在 General 確認;收緊的變更會立即套用。",
664
+ webLead: "網頁儀表板的行為。放寬存取的變更(延長 session、重新開啟或延長瀏覽器終端機、公開連結)需要 fleet admin 在聊天(General topic)或主機上確認;收緊的變更會立即套用。",
651
665
  webPublicRefused: "無法透過臨時公開連結變更網頁設定。請從儀表板本身的網址開啟後再變更。",
652
666
  webSessionTitle: "登入 session",
653
667
  webSessionHint: "適用於從儀表板本身網址登入的 session(本機、區域網路、tailnet)。臨時公開連結固定為 4 小時 / 30 分鐘。縮短會讓已超過新限制的 session 結束;延長會套用到新的登入,以及既有 session 下次使用時的閒置時間。留空 = 預設值。",
@@ -731,7 +745,7 @@ register("settings", {
731
745
  help1: "Agent:每個 agent 的設定、啟動/停止、暫停/喚醒,以及新增 agent。",
732
746
  help2: "連線:誰可以使用各個 bot、它的綁定、狀態表情、新的 token;以及供應商 API key。",
733
747
  help3: "ClassicBot:每個房間的 backend、model 與選項。",
734
- help4: "一般:所有 agent 的預設值、ClassicBot 預設值與網頁聊天。「檢查變更」會把它們暫存。",
748
+ help4: "一般:所有 agent 的預設值與 ClassicBot 預設值。網頁:儀表板的登入 session、瀏覽器終端機、網頁轉發、紀錄與公開連結。「檢查變更」會把它們暫存。",
735
749
  help5: "變更會先暫存,再用「套用變更」一起套用。每項都會標示代價:立即生效、重新啟動 agent,或重新啟動 AgEnD。",
736
750
  help6: "存取、密鑰或連線的變更,要等 fleet 管理員在聊天中確認後才會套用。",
737
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 }) })
@@ -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>
@@ -98,7 +98,7 @@ register("app", {
98
98
  theme: "主題", themeSystem: "跟隨系統", themeLight: "淺色", themeDark: "深色", language: "語言",
99
99
  previews: "允許預覽 HTML", tour: "導覽", tourTitle: "再看一次網頁聊天導覽",
100
100
  needsYou: "等你回應", approxNote: "從終端畫面判讀,僅供參考", chat: "聊天", details: "詳細資訊", log: "紀錄", instanceViews: "{0} 的檢視",
101
- textSize: "字級:{0}", textSize_s: "小", textSize_m: "中", textSize_l: "大", textSize_fit: "剛好填滿", usage: "用量", usageHide: "隱藏用量",
101
+ textSize: "字級:{0}", textSize_s: "小", textSize_m: "中", textSize_l: "大", textSize_fit: "填滿", usage: "用量", usageHide: "隱藏用量",
102
102
  statusRunning: "執行中", statusStopped: "已停止", statusPaused: "已暫停", statusCrashed: "已當掉", statusWorking: "工作中", statusStuck: "似乎卡住", statusIdle: "閒置",
103
103
  connReconnecting: "連不上 AgEnD——可能正在重新啟動。正在重新連線…", connReconnectingIn: "連不上 AgEnD——可能正在重新啟動。{0} 秒後再試。",
104
104
  hydrating: "正在載入目前的狀態…", hydrateFailed: "無法載入目前的狀態(提示、送達標記)。",
@@ -30,7 +30,7 @@
30
30
  // - suspend()/resume(): the page is leaving (pagehide) / came back from the back-forward cache.
31
31
 
32
32
  // #1218: steps_tick — an instance's transcript moved while a Log watches it (SSE only: a poll page asks the Log itself).
33
- const EVENTS = ["status", "message", "delivery", "deliveries", "prompt", "prompts", "prompt_resolved", "activity", "needs", "reply_buttons", "steps_tick"];
33
+ const EVENTS = ["status", "message", "delivery", "deliveries", "prompt", "prompts", "prompt_resolved", "activity", "needs", "reply_buttons", "steps_tick", "clock"];
34
34
  const MAX_HELD = 1000; // live events held during one catch-up read; more → read again afterwards
35
35
  const RETRY_DELAYS = [1000, 2000, 4000]; // after the first failed catch-up read; then it waits for Retry
36
36
  const POLL_LIMIT_MS = 10000; // a poll that has not answered by then counts as failed
@@ -114,6 +114,7 @@ export function createStream(opts) {
114
114
  if (closed || mine !== pollSeq || !r.ok) return false;
115
115
  const d = await r.json();
116
116
  if (closed || mine !== pollSeq) return false;
117
+ if (typeof d.now === "number") emit("clock", { now: d.now }); // #1823: read just now, handed over at once
117
118
  if (d.status) emit("status", d.status);
118
119
  for (const m of d.messages || []) emit("message", m);
119
120
  if (typeof d.cursor === "string") state.cursor = d.cursor;
@@ -209,6 +210,8 @@ export function createStream(opts) {
209
210
  try { data = JSON.parse(e.data); } catch { return; }
210
211
  if (!spoke) { spoke = true; alive(); if (again) catchUp(); }
211
212
  else alive();
213
+ // #1823: a clock reading is about now, not about order: it is never held behind a catch-up (it would be stale).
214
+ if (name === "clock") { emit(name, data); return; }
212
215
  live(name, data, name === "message" ? e.lastEventId : null);
213
216
  });
214
217
  }
@@ -69,10 +69,12 @@ html:has(.shell), html:has(.shell) body { overflow: hidden; }
69
69
  .sidebar {
70
70
  width: var(--sidebar-w); flex-shrink: 0; display: flex; flex-direction: column; background: var(--bg-sidebar);
71
71
  position: relative; /* #1722: the resize handle sits on its right edge (the width above: tokens.css, or this device's) */
72
- padding: env(safe-area-inset-top) 0 env(safe-area-inset-bottom) env(safe-area-inset-left);
72
+ padding: 0 0 env(safe-area-inset-bottom) env(safe-area-inset-left); /* the top inset is the head's, as the page header's is */
73
73
  }
74
74
  .sb-collapsed .sidebar { display: none; }
75
- .side-head { display: flex; align-items: center; gap: var(--s-1); padding: var(--s-3) var(--s-2) var(--s-2) var(--s-4); }
75
+ /* The brand row is the page header's row: the same height, padding and centre line (--header-h; .panel-head, .panel-title). */
76
+ .side-head { display: flex; align-items: center; gap: var(--s-1); box-sizing: content-box; min-height: calc(var(--header-h) - 2 * var(--s-1));
77
+ padding: max(var(--s-1), env(safe-area-inset-top)) var(--s-2) var(--s-1) var(--s-4); }
76
78
  .brand { flex: 1; font-size: var(--fs-lg); font-weight: var(--fw-semibold); letter-spacing: -0.01em; }
77
79
  .side-nav { display: flex; flex-direction: column; padding: var(--s-1) var(--s-2); gap: 1px; }
78
80
  .side-row {
@@ -150,6 +152,10 @@ html:has(.shell), html:has(.shell) body { overflow: hidden; }
150
152
  .s-device { display: flex; flex-direction: column; gap: var(--s-3); }
151
153
  .s-device .sub { margin: 0; color: var(--text-3); font-size: var(--fs-sm); }
152
154
  .s-device-row { display: flex; gap: var(--s-2); }
155
+ /* #1827: the person's web name — its field takes the card's width; a refusal reads in the danger colour. */
156
+ .s-profile .field { display: flex; flex-direction: column; gap: var(--s-1); }
157
+ .s-profile .field input { max-width: 24rem; }
158
+ .s-profile .err { color: var(--danger); }
153
159
  /* #1620: Settings → Web — the lead, and the read-only list of what is set in fleet.yaml. */
154
160
  .s-web .sub { margin: 0; color: var(--text-3); font-size: var(--fs-sm); }
155
161
  .s-web-fields:disabled select { opacity: 0.6; } /* over the public link: dimmed like the inputs (.field input:disabled) */
@@ -195,14 +201,17 @@ html:has(.shell), html:has(.shell) body { overflow: hidden; }
195
201
  }
196
202
  .panel-head .sb-open { display: none; }
197
203
  .sb-collapsed .panel-head .sb-open { display: inline-flex; }
198
- .panel-title { flex: 1; min-width: 0; display: flex; align-items: baseline; gap: var(--s-3); }
204
+ /* One row of --header-h on every page (a phone's instance tabs wrap below it): the title, its status and badges on one centre line. */
205
+ .panel-title { flex: 1; min-width: 0; display: flex; align-items: center; gap: var(--s-3); min-height: calc(var(--header-h) - 2 * var(--s-1)); }
199
206
  .panel-title h1 { font-size: var(--fs-lg); line-height: var(--lh-lg); font-weight: var(--fw-semibold); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; outline: none; }
200
207
  .panel-sub { color: var(--text-2); font-size: var(--fs-sm); white-space: nowrap; }
201
208
  .status { display: inline-flex; align-items: center; gap: 6px; }
202
209
  .panel-actions { display: flex; align-items: center; gap: var(--s-1); }
203
210
  /* #1523 N2: the instance view switch — beside the title on a wide screen; on a phone, a row of tabs under the header. */
204
211
  .inst-switch.seg { padding: 0; margin: 0; width: auto; max-width: none; flex-shrink: 0; }
205
- .inst-switch .seg-item { min-height: 30px; }
212
+ .inst-switch .seg-item { min-height: 30px; flex-direction: column; justify-content: center; gap: 0; }
213
+ /* The label again in the current tab's weight, taking width but no height and never seen: the bar's size does not change with the tab. */
214
+ .inst-switch .seg-item::after { content: attr(data-label); font-weight: var(--fw-semibold); height: 0; overflow: hidden; visibility: hidden; pointer-events: none; user-select: none; }
206
215
  /* #1523 N3 (Q2 = A): the text size, one custom property from the root's data-text-size (header-tools.js). The chat's
207
216
  messages, Details and the Fleet tabs scale the type tokens by it; View's terminal scales itself (VIEW_SCALE). */
208
217
  :root { --text-scale: 1; }
@@ -248,6 +257,10 @@ html:has(.shell), html:has(.shell) body { overflow: hidden; }
248
257
  .p-log .log-older, .p-log .log-start { text-align: center; margin: var(--s-2) 0; }
249
258
  .p-log .log-off { padding: var(--s-6) var(--s-4); }
250
259
  .p-details .d-edit { display: flex; align-items: center; justify-content: space-between; gap: var(--s-2); flex-wrap: wrap; margin-top: var(--s-2); }
260
+ /* The header's controls are one height, the icon buttons' (36 px): its small buttons, and the instance tabs when they share
261
+ its row (a phone puts them on a row of their own, under it). */
262
+ .panel-head .btn-sm { min-height: 36px; }
263
+ @media (min-width: 900px) { .panel-head .inst-switch .seg-item { min-height: 36px; } }
251
264
  @media (max-width: 899px) {
252
265
  .panel-head:has(.inst-switch) { flex-wrap: wrap; }
253
266
  .inst-switch.seg { order: 3; flex-basis: 100%; display: grid; grid-template-columns: repeat(4, 1fr); padding: 0 0 var(--s-1); }
@@ -264,8 +277,7 @@ html:has(.shell), html:has(.shell) body { overflow: hidden; }
264
277
  .empty-title { color: var(--text); font-size: var(--fs-lg); font-weight: var(--fw-semibold); }
265
278
  .empty-hint { font-size: var(--fs-sm); }
266
279
  .empty .btn { margin-top: var(--s-3); }
267
- /* #1519 P7: the first-run card (a fleet with no chat connection), above an empty chat or a Settings list. */
268
- .first-run-stack { width: 100%; max-width: var(--col); }
280
+ /* #1519 P7: the first-run card (a fleet with no chat connection), above a Settings list (#1854: no longer on Chat). */
269
281
  .first-run h3 { display: flex; align-items: center; gap: var(--s-2); }
270
282
  .first-run p { color: var(--text-2); font-size: var(--fs-sm); }
271
283
  .error-state { display: flex; flex-direction: column; align-items: center; gap: var(--s-3); padding: var(--s-8) var(--s-4); color: var(--danger); text-align: center; }
@@ -370,6 +382,13 @@ html:has(.shell), html:has(.shell) body { overflow: hidden; }
370
382
  such as 「昨天 下午03:58」, ticks) — and a block body would stretch to it; fit-content keeps a short message short, on
371
383
  the right under its meta, and a long one still wraps at the message's max-width. */
372
384
  .msg.user .body { background: var(--user-bubble); border-radius: var(--r-xl); padding: 10px var(--s-4); width: fit-content; max-width: 100%; margin-left: auto; }
385
+ /* A person's click on a reply button: the bubble shows 🔘 and the button's label (never the value the agent was given);
386
+ live, the label is a button that shows the reply its buttons were on. */
387
+ .click-chip { display: flex; }
388
+ .click-chip .click-link, .click-chip .click-text { display: inline-flex; align-items: baseline; gap: var(--s-1); padding: 0; border: 0; background: none; color: inherit; font: inherit; text-align: left; overflow-wrap: anywhere; }
389
+ .click-chip .click-link { cursor: pointer; }
390
+ .click-chip .click-link:hover, .click-chip .click-link:focus-visible { text-decoration: underline; }
391
+ .click-mark { flex: none; }
373
392
  .msg.agent { align-self: stretch; }
374
393
  /* #1672: a status line (another instance's message, a daemon notice): muted plain text, no bubble; a long one folds.
375
394
  Its class is `status-line`, never `status`: that is the header's inline-flex badge class, and on a message it turned
@@ -400,10 +419,13 @@ html:has(.shell), html:has(.shell) body { overflow: hidden; }
400
419
  .text-card { margin-top: var(--s-2); border: 1px solid var(--border); border-radius: var(--r-sm); padding: var(--s-2) var(--s-3); }
401
420
  .text-card .tc-head { display: flex; align-items: center; gap: var(--s-2); }
402
421
  .text-card .tc-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: var(--fs-xs); font-family: var(--font-mono); color: var(--text-2); }
403
- .text-card .tc-body { margin-top: var(--s-2); max-height: 480px; overflow: auto; }
422
+ /* #1812: the scroller spans the card's padding box and insets its own content — 12px on both sides, from the border on
423
+ the left and from the scrollbar (or the border, with overlay scrollbars) on the right. A stable gutter keeps a
424
+ classic scrollbar off the content, and the content from shifting when the scrollbar comes and goes. */
425
+ .text-card .tc-body { margin: var(--s-2) calc(-1 * var(--s-3)) 0; padding: 0 var(--s-3); max-height: 480px; overflow: auto; scrollbar-gutter: stable; }
404
426
  .text-card .tc-body[hidden] { display: none; }
405
427
  .text-card .tc-pre { margin: 0; white-space: pre-wrap; word-break: break-word; font-size: var(--fs-xs); }
406
- .text-card .tc-table-wrap { overflow-x: auto; }
428
+ .text-card .tc-table-wrap { overflow-x: auto; padding-bottom: var(--s-3); }
407
429
  .text-card .tc-table { border-collapse: collapse; font-size: var(--fs-xs); }
408
430
  .text-card .tc-table th, .text-card .tc-table td { border: 1px solid var(--border); padding: 2px 6px; text-align: left; vertical-align: top; }
409
431
  .text-card .tc-more { margin-top: var(--s-2); }
@@ -436,10 +458,17 @@ html.lb-open .scroller { overflow: hidden; }
436
458
  .msg .md ul { list-style: disc; } .msg .md ol { list-style: decimal; }
437
459
  .msg .md li > ul, .msg .md li > ol { margin: 2px 0; }
438
460
  .msg .md hr { border: 0; border-top: 1px solid var(--border); margin: var(--s-3) 0; }
439
- .msg .md .tbl { overflow-x: auto; margin: var(--s-2) 0; }
461
+ /* #1812: a table too wide for its message scrolls in this wrapper (see the cell rule below); the bottom padding keeps an
462
+ overlay scrollbar off the last row. */
463
+ .msg .md .tbl { overflow-x: auto; margin: var(--s-2) 0 0; padding-bottom: var(--s-3); }
440
464
  .msg .md table { border-collapse: collapse; font-size: var(--fs-sm); }
441
465
  .msg .md th, .msg .md td { border: 1px solid var(--border); padding: 5px 9px; text-align: left; vertical-align: top; }
442
466
  .msg .md th { background: var(--surface); font-weight: var(--fw-semibold); }
467
+ /* #1812: table cells keep their words whole. The message's `overflow-wrap: anywhere` also shrinks a cell's minimum
468
+ width, so auto table layout squeezed the columns and split words ("directo|r"); `break-word` breaks a word only when
469
+ it alone is wider than its line and leaves the minimum at whole words, so a wide table overflows into its scrolling
470
+ wrapper instead. A column keeps at least 5em: CJK text can break between any two characters. */
471
+ .msg .md th, .msg .md td, .text-card .tc-table th, .text-card .tc-table td { overflow-wrap: break-word; word-break: normal; min-width: 5em; }
443
472
  .msg .md .al-c { text-align: center; } .msg .md .al-r { text-align: right; }
444
473
  .msg .md h3, .msg .md h4, .msg .md h5, .msg .md h6 { margin: var(--s-3) 0 var(--s-2); font-size: var(--fs-md); font-weight: var(--fw-semibold); }
445
474
  .msg .md .tk-k { color: var(--tk-k); } .msg .md .tk-s { color: var(--tk-s); } .msg .md .tk-n { color: var(--tk-n); } .msg .md .tk-c { color: var(--tk-c); font-style: italic; }
@@ -516,6 +545,10 @@ html.lb-open .scroller { overflow: hidden; }
516
545
  .rb-btn { max-width: 100%; white-space: normal; text-align: left; }
517
546
  .rb-btn.chosen, .rb-btn.chosen:disabled { border-color: var(--ok); color: var(--text); background: color-mix(in srgb, var(--ok) 14%, transparent); font-weight: var(--fw-semibold); opacity: 1; }
518
547
  .rb-btn:disabled:not(.chosen) { opacity: 0.55; }
548
+ /* #1823: a poll — its closed buttons show the tally, so they stay readable; the time left keeps its width. */
549
+ .rb-row.poll .rb-btn:disabled { opacity: 0.85; }
550
+ .rb-row.poll .poll-left { font-variant-numeric: tabular-nums; }
551
+ .poll-voters { margin: var(--s-1) 0 0; padding-left: var(--s-4); color: var(--text-2); font-size: var(--fs-sm); }
519
552
  .rb-note { font-size: var(--fs-xs); color: var(--text-3); }
520
553
  .pv-menu { position: relative; }
521
554
  .pv-menu > summary { list-style: none; cursor: pointer; padding: 2px var(--s-2); border-radius: var(--r-sm); color: var(--text-3); }
@@ -704,7 +737,15 @@ body.side-resizing, body.side-resizing * { cursor: col-resize !important; user-s
704
737
  .nav-chip { display: flex; align-items: center; gap: var(--s-2); cursor: pointer; color: var(--text); }
705
738
  .nav-facet-reset { font-size: var(--fs-xs); }
706
739
 
707
- .p-view .panel-actions .btn { gap: var(--s-1); }
740
+ /* One gap on every page (View's, #1448): the header's controls are the same size on every tab. */
741
+ .panel-actions .btn { gap: var(--s-1); }
742
+ /* Aa's names share one cell, only the current one shown: the button is as wide as its widest name on every page. */
743
+ .hd-text-names { display: inline-grid; }
744
+ .hd-text-names > span { grid-area: 1 / 1; visibility: hidden; }
745
+ .hd-text-names > .on { visibility: visible; }
746
+ .hd-slot { visibility: hidden; pointer-events: none; }
747
+ /* #1844 Fleet overview → Docs: the arrow says "opens a new tab". */
748
+ .hd-docs .hd-ext { color: var(--text-3); }
708
749
  .v-term { flex: 1; min-height: 0; overflow: auto; scrollbar-gutter: stable; padding: 5px 6px; background: #000000; position: relative; }
709
750
  .v-pre { margin: 0; min-width: 100%; min-height: 100%; font-family: var(--font-mono); line-height: 1.2; white-space: pre; color: #d0d0d0; }
710
751
  .v-pre .ansi[data-b] { font-weight: var(--fw-bold); }
@@ -1041,6 +1082,7 @@ strong.org-name { font-size: var(--fs-sm); color: var(--text); }
1041
1082
  .cmd-head { display: flex; align-items: center; justify-content: space-between; gap: var(--s-2); }
1042
1083
  .cmd-name { font-weight: var(--fw-semibold); }
1043
1084
  .cmd-text { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; font-family: var(--font-ui); color: var(--text-2); }
1085
+ .cmd-phase { color: var(--text); font-weight: var(--fw-semibold); }
1044
1086
  .cmd-choices { display: flex; flex-wrap: wrap; gap: var(--s-1); max-height: 180px; overflow-y: auto; }
1045
1087
  .cmd-choices .btn.current { border-color: var(--accent); color: var(--accent); }
1046
1088
  .quick-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2); margin-bottom: var(--s-2); font-size: var(--fs-xs); color: var(--text-2); }
@@ -13,6 +13,8 @@ import { UsageBody, UsageDialog } from "./usage-dialog.js";
13
13
 
14
14
  export const TEXT_SIZE_KEY = "agend_text_size";
15
15
  export const VIEW_FIT_KEY = "agend_view_fit";
16
+ /** #1869: set once this device's View Fit was put back (a size chosen on another page used to turn it off). */
17
+ export const VIEW_FIT_RESTORED_KEY = "agend_view_fit_1869";
16
18
  const LEGACY_DENSITY_KEY = "agend_view_density";
17
19
  export const TEXT_SIZES = ["s", "m", "l"];
18
20
  /** View's terminal: its fitted size times this (S / M / L are View's old compact / – / comfortable). */
@@ -24,6 +26,7 @@ const write = (k, v) => { try { localStorage.setItem(k, v); } catch { /* this pa
24
26
  /**
25
27
  * This device's choice: `agend_text_size` (+ View's Fit), else — once — from View's `agend_view_density`:
26
28
  * comfortable → L, compact → S (Fit off); fit, or nothing → M with View on Fit (View's default, as before).
29
+ * #1869: until then any page's S / M / L also turned View's Fit off, so a Fit off from before is put back once.
27
30
  */
28
31
  export function loadTextSize() {
29
32
  let size = read(TEXT_SIZE_KEY), fit = read(VIEW_FIT_KEY);
@@ -31,7 +34,11 @@ export function loadTextSize() {
31
34
  const legacy = read(LEGACY_DENSITY_KEY);
32
35
  size = legacy === "comfortable" ? "l" : legacy === "compact" ? "s" : "m";
33
36
  fit = legacy === "comfortable" || legacy === "compact" ? "0" : "1";
34
- write(TEXT_SIZE_KEY, size); write(VIEW_FIT_KEY, fit);
37
+ write(TEXT_SIZE_KEY, size); write(VIEW_FIT_KEY, fit); write(VIEW_FIT_RESTORED_KEY, "1");
38
+ }
39
+ if (read(VIEW_FIT_RESTORED_KEY) !== "1") {
40
+ if (fit === "0") { fit = "1"; write(VIEW_FIT_KEY, fit); }
41
+ write(VIEW_FIT_RESTORED_KEY, "1");
35
42
  }
36
43
  return { size, viewFit: fit !== "0" };
37
44
  }
@@ -50,15 +57,22 @@ export function initTextSize() {
50
57
  window.addEventListener("storage", (e) => { if (!e || e.key === TEXT_SIZE_KEY || e.key === VIEW_FIT_KEY || e.key === null) textSizeStore.set(loadTextSize()); });
51
58
  }
52
59
  }
53
- /** A size chosen anywhere is the size everywhere (View's Fit included: the last choice wins). */
54
- export function setTextSize(size) {
60
+ /**
61
+ * A size chosen anywhere is the size everywhere. View's Fit is left only from View's own Aa (`view`): a size chosen on
62
+ * another page keeps View on Fit (#1869).
63
+ */
64
+ export function setTextSize(size, { view = false } = {}) {
55
65
  if (!TEXT_SIZES.includes(size)) return;
56
- write(TEXT_SIZE_KEY, size); write(VIEW_FIT_KEY, "0");
57
- textSizeStore.set({ size, viewFit: false });
66
+ write(TEXT_SIZE_KEY, size);
67
+ if (view) write(VIEW_FIT_KEY, "0");
68
+ textSizeStore.set((s) => ({ size, viewFit: view ? false : s.viewFit }));
58
69
  }
59
70
  export function setViewFit() { write(VIEW_FIT_KEY, "1"); textSizeStore.set((s) => ({ ...s, viewFit: true })); }
60
71
 
61
- /** Aa: S → M → L → S; on View, Fit → S → M → L → Fit. */
72
+ /**
73
+ * Aa: S → M → L → S; on View, Fit → S → M → L → Fit. Every page's button is as wide as its widest name (Fit's
74
+ * included): switching pages or sizes never moves the controls beside it.
75
+ */
62
76
  export function TextSizeButton({ view = false }) {
63
77
  const st = useStore(textSizeStore);
64
78
  const cur = view && st.viewFit ? "fit" : st.size;
@@ -66,8 +80,9 @@ export function TextSizeButton({ view = false }) {
66
80
  const next = order[(order.indexOf(cur) + 1) % order.length];
67
81
  const name = t(`app.textSize_${cur}`);
68
82
  const label = t("app.textSize", name);
69
- return html`<button type="button" class="btn btn-ghost btn-sm hd-text" onClick=${() => (next === "fit" ? setViewFit() : setTextSize(next))}
70
- title=${label} aria-label=${label}><${Icon} name="type" size=${16} /><span class="hide-narrow" aria-hidden="true">${name}</span></button>`;
83
+ return html`<button type="button" class="btn btn-ghost btn-sm hd-text" onClick=${() => (next === "fit" ? setViewFit() : setTextSize(next, { view }))}
84
+ title=${label} aria-label=${label}><${Icon} name="type" size=${16} /><span class="hide-narrow" aria-hidden="true"><span class="hd-text-names">${
85
+ ["fit", ...TEXT_SIZES].map(k => html`<span key=${k} class=${k === cur ? "on" : undefined}>${t(`app.textSize_${k}`)}</span>`)}</span></span></button>`;
71
86
  }
72
87
 
73
88
  /** Whether the fleet serves the usage panel: the page says so (data-usage-panel); "0" only when turned off. */
@@ -76,14 +91,15 @@ export function usageOffered() {
76
91
  }
77
92
 
78
93
  // #1673: the rail. Wide enough for it beside the sidebar and the page: RAIL_MEDIA (followed as it changes). Shown or
79
- // hidden is this device's choice (agend_usage_rail; "0" = hidden, else shown) — a layout choice, not synced (#1665).
94
+ // hidden is this device's choice (agend_usage_rail; "1" = shown, "0" = hidden) — a layout choice, not synced (#1665).
95
+ // #1854 (rank 7): a device that never chose starts with it hidden — a quiet first page; ◔ shows it (and remembers).
80
96
  export const RAIL_MEDIA = "(min-width: 1200px)";
81
97
  export const RAIL_KEY = "agend_usage_rail";
82
- /** The pages whose header has ◔ — the rail is theirs. */
83
- export const RAIL_PANELS = ["chat", "view", "details", "fleet"];
84
- export const usageRailStore = createStore({ open: true });
85
- /** A page load: this device's choice. */
86
- export function initUsageRail() { usageRailStore.set({ open: read(RAIL_KEY) !== "0" }); }
98
+ /** The pages whose header has ◔ — the rail is theirs (Log's too: one frame on every tab of an instance). */
99
+ export const RAIL_PANELS = ["chat", "view", "log", "details", "fleet"];
100
+ export const usageRailStore = createStore({ open: false });
101
+ /** A page load: this device's choice; none made yet = hidden. */
102
+ export function initUsageRail() { usageRailStore.set({ open: read(RAIL_KEY) === "1" }); }
87
103
  export function setUsageRail(open) { write(RAIL_KEY, open ? "1" : "0"); usageRailStore.set({ open: !!open }); }
88
104
  const railQuery = () => { try { return typeof matchMedia === "function" ? matchMedia(RAIL_MEDIA) : null; } catch { return null; } };
89
105
  /** Whether the screen is wide enough for the rail, now and as it changes. */
@@ -141,3 +157,7 @@ export function UsageButton({ navKey }) {
141
157
  export function HeaderTools({ view = false, navKey }) {
142
158
  return html`<${TextSizeButton} view=${view} /><${UsageButton} navKey=${navKey} />`;
143
159
  }
160
+ /** Where a page with no control of its own after the shared ones (Log) keeps the room Chat's ⋯ takes: the same box on every tab. */
161
+ export function HeaderSlot() {
162
+ return html`<span class="icon-btn hd-slot" aria-hidden="true"></span>`;
163
+ }
@@ -6,10 +6,13 @@ import { html } from "./app-html.js";
6
6
  import { t } from "./app-i18n.js";
7
7
  import { chatPath, detailsPath, logPath, viewPath } from "./app-route.js";
8
8
 
9
- /** current: "chat" | "view" | "log" | "details". #1218: Log sits between View and Details. */
9
+ /**
10
+ * current: "chat" | "view" | "log" | "details". #1218: Log sits between View and Details. `data-label`: each tab is as
11
+ * wide as its label in the current tab's weight (app.css), so the bar keeps its size whichever tab is current.
12
+ */
10
13
  export function InstanceSwitch({ name, current }) {
11
14
  const seg = (key, href, label) => html`<a key=${key} class=${`seg-item${current === key ? " active" : ""}`} href=${href}
12
- aria-current=${current === key ? "page" : undefined}>${label}</a>`;
15
+ aria-current=${current === key ? "page" : undefined} data-label=${label}>${label}</a>`;
13
16
  return html`<nav class="inst-switch seg" aria-label=${t("app.instanceViews", name)}>
14
17
  ${seg("chat", chatPath(name), t("app.chat"))}${seg("view", viewPath(name), t("app.view"))}${seg("log", logPath(name), t("app.log"))}${seg("details", detailsPath(name), t("app.details"))}
15
18
  </nav>`;
@@ -114,7 +114,9 @@ export { viewStore };
114
114
  // ── The panel ──
115
115
 
116
116
  // readStream: one stream of reads under a lease (read-stream.js; View's pane and roster use it).
117
- const MIN_PX = 12, MAX_PX = 22;
117
+ // S / M / L: the fitted size kept within 12–22 px, then scaled. Fit (#1869): never larger than what fits — only a 6 px
118
+ // floor for legibility (a grid too wide for the box even at 6 px, e.g. 200 columns on a phone, still scrolls).
119
+ const MIN_PX = 12, MAX_PX = 22, FIT_FLOOR_PX = 6;
118
120
 
119
121
  export function ViewPanel({ route, navKey }) {
120
122
  const lease = useLease(navKey);
@@ -219,10 +221,11 @@ function Terminal({ name, lease, density }) {
219
221
  const lh = parseFloat(pcs.lineHeight) / parseFloat(pcs.fontSize);
220
222
  const lineH = Number.isFinite(lh) && lh > 0 ? lh : 1.2;
221
223
  let size = Math.min(innerW / (g.cols * glyphRatio(p)), innerH / (g.rows * lineH)) * 0.995;
222
- size = Math.max(MIN_PX, Math.min(size, MAX_PX)) * (density === "fit" ? 1 : VIEW_SCALE[density] || 1);
223
- size = Math.max(8, Math.min(size, 32));
224
+ if (density === "fit") size = Math.max(FIT_FLOOR_PX, Math.min(size, MAX_PX));
225
+ else size = Math.max(8, Math.min(Math.max(MIN_PX, Math.min(size, MAX_PX)) * (VIEW_SCALE[density] || 1), 32));
224
226
  g.key = key;
225
- if (Math.abs(size - g.applied) < 0.75) return;
227
+ // A small change is skipped only when growing (the size applied still fits); a smaller box always shrinks it.
228
+ if (size >= g.applied && size - g.applied < 0.75) return;
226
229
  g.applied = size;
227
230
  p.style.fontSize = `${size.toFixed(2)}px`; // CSSOM, never a style attribute (#1300)
228
231
  };
@@ -314,6 +317,8 @@ function EditProfile({ it, onClose }) {
314
317
  const [f, setF] = useState({ display: it.display_name || "", role: it.role || "", desc: it.description || "" });
315
318
  const [busy, setBusy] = useState(false);
316
319
  const [msg, setMsg] = useState(null);
320
+ // #1630 follow-up: the uploaded picture is removed on Save (Cancel keeps it); a newly picked file wins over removing.
321
+ const [removePic, setRemovePic] = useState(false);
317
322
  const file = useRef(null);
318
323
  async function save() {
319
324
  if (busy) return;
@@ -323,6 +328,7 @@ function EditProfile({ it, onClose }) {
323
328
  const name = encodeURIComponent(it.instance_name);
324
329
  const draft = { display_name: f.display, role: f.role, description: f.desc };
325
330
  const picked = file.current && file.current.files && file.current.files[0];
331
+ const removing = removePic;
326
332
  try {
327
333
  const r = await fetch(`/api/profile/${name}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(draft) });
328
334
  if (!lease.current()) return;
@@ -331,6 +337,9 @@ function EditProfile({ it, onClose }) {
331
337
  if (picked) {
332
338
  const ar = await fetch(`/api/avatar/${name}`, { method: "POST", headers: { "Content-Type": picked.type }, body: picked });
333
339
  if (!ar.ok) { const e = await ar.json().catch(() => ({})); throw new Error(tn("avatarFailed", { error: e.error || ar.status })); }
340
+ } else if (removing) { // a newly picked file replaces it instead
341
+ const dr = await fetch(`/api/avatar/${name}`, { method: "DELETE" });
342
+ if (!dr.ok) { const e = await dr.json().catch(() => ({})); throw new Error(tn("avatarRemoveFailed", { error: e.error || dr.status })); }
334
343
  }
335
344
  if (!lease.current()) return;
336
345
  setBusy(false);
@@ -345,6 +354,8 @@ function EditProfile({ it, onClose }) {
345
354
  <label class="field"><span>${tn("role")}</span><input value=${f.role} onInput=${(e) => setF({ ...f, role: e.target.value })} /></label>
346
355
  <label class="field"><span>${tn("description")}</span><textarea rows="3" value=${f.desc} onInput=${(e) => setF({ ...f, desc: e.target.value })}></textarea></label>
347
356
  <label class="field"><span>${tn("avatar")}</span><input ref=${file} type="file" accept="image/png,image/jpeg,image/gif,image/webp" /></label>
357
+ ${it.has_avatar ? html`<div class="field-row"><button type="button" class="btn btn-sm" aria-pressed=${removePic ? "true" : "false"}
358
+ onClick=${() => setRemovePic(!removePic)}>${tn(removePic ? "keepPicture" : "removePicture")}</button>${removePic ? html`<span class="muted">${tn("removePictureNote")}</span>` : null}</div>` : null}
348
359
  </fieldset></form>
349
360
  ${msg ? html`<p class="err" role="alert">${msg}${msg === tn("signInToSave") ? html` <a class="link" href=${signInHref()}>${t("app.signIn")}</a>` : null}</p>` : null}
350
361
  </${Dialog}>`;
@@ -11,10 +11,12 @@
11
11
  const $ = (id) => document.getElementById(id);
12
12
  const T = {
13
13
  en: {
14
- checking: "Checking your session…", title: "Sign in to AgEnD", lead: "Enter the one-time code from your chat channel.",
14
+ checking: "Checking your session…", title: "Sign in to AgEnD", lead: "Enter your sign-in code.",
15
15
  label: "One-time code", submit: "Sign in", working: "Signing in…",
16
- hint: "Send /web to your AgEnD bot, or run “agend web --code” on the host. A code works once and expires in 5 minutes.",
17
- refused: "That code did not work — it may be wrong, expired or already used. Ask for a new one with /web.",
16
+ hint: "No code? Send /web in General, then press a sign-in button to get one.",
17
+ nobot: "No chat bot?", nobot_body: "Ask the operator to run agend web --code on the AgEnD computer.",
18
+ refused: "The code is invalid or no longer works. Send /web in General, then press a sign-in button for a new code.",
19
+ unconfirmed: "Sign-in could not be confirmed. Get a new code and try again; if it keeps failing, contact the operator.",
18
20
  paused: "Too many wrong codes — sign-in is paused for a few minutes.",
19
21
  network: "Could not reach AgEnD. Check the connection and try again.",
20
22
  resend: "Send me a new code", resent: "A new code was sent to you privately in chat. Enter it above.",
@@ -22,10 +24,12 @@
22
24
  resend_refused: "A new code could not be sent. Ask for one with /web.",
23
25
  },
24
26
  "zh-TW": {
25
- checking: "正在確認登入狀態…", title: "登入 AgEnD", lead: "請輸入聊天頻道給你的一次性登入碼。",
27
+ checking: "正在確認登入狀態…", title: "登入 AgEnD", lead: "輸入登入碼。",
26
28
  label: "一次性登入碼", submit: "登入", working: "登入中…",
27
- hint: "在 AgEnD bot 傳 /web,或在主機執行「agend web --code」。登入碼只能用一次,5 分鐘內有效。",
28
- refused: "登入碼無效:可能輸入錯誤、已過期或已使用。請用 /web 再要一個。",
29
+ hint: "沒收到?在 General 傳 /web,再按登入按鈕取碼。",
30
+ nobot: "沒有聊天 bot?", nobot_body: "請管理員在 AgEnD 電腦執行 agend web --code。",
31
+ refused: "登入碼無效或已失效。請在 General 傳 /web,再按登入按鈕取新碼。",
32
+ unconfirmed: "登入暫時無法確認。請重新取碼再試;若仍失敗,請聯絡管理員。",
29
33
  paused: "錯誤次數過多,登入暫停幾分鐘。",
30
34
  network: "連不上 AgEnD,請檢查連線後再試。",
31
35
  resend: "傳一組新的登入碼給我", resent: "新的登入碼已私訊給你,請在上方輸入。",
@@ -47,8 +51,8 @@
47
51
  history.replaceState(null, "", here.pathname + here.search + here.hash);
48
52
  }
49
53
  } catch { /* nothing to clean */ }
50
- // The command in the hint is a <code> that never breaks across lines (a break inside "--code" reads as two dashes
51
- // and a word, alpha.2 sweep); the text around it is text.
54
+ // The command in the "No chat bot?" help is a <code> that never breaks across lines (a break inside "--code" reads as
55
+ // two dashes and a word, alpha.2 sweep); the text around it is text.
52
56
  const CMD = "agend web --code";
53
57
  document.querySelectorAll("[data-i18n]").forEach((el) => {
54
58
  const text = t(el.getAttribute("data-i18n"));
@@ -163,7 +167,9 @@
163
167
  body: JSON.stringify({ code: $("code").value }),
164
168
  });
165
169
  if (r.ok) { clearBounce(); location.replace(nextTarget()); return; }
166
- msg.textContent = r.status === 429 ? t("paused") : t("refused");
170
+ // #1854: a 5xx is the server failing to confirm the sign-in (503: a public link's notice did not go out), not a
171
+ // wrong code — say so. The code is spent either way: the way on is a new one.
172
+ msg.textContent = r.status === 429 ? t("paused") : r.status >= 500 ? t("unconfirmed") : t("refused");
167
173
  } catch {
168
174
  msg.textContent = t("network");
169
175
  }
@@ -54,6 +54,7 @@ const P = {
54
54
  panel: () => html`<rect x="3" y="4" width="18" height="16" rx="2"/><path d="M14 4v16"/>`,
55
55
  back: () => html`<path d="M19 12H5M12 19l-7-7 7-7"/>`,
56
56
  forward: () => html`<path d="M5 12h14M12 5l7 7-7 7"/>`,
57
+ book: () => html`<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20V3H6.5A2.5 2.5 0 0 0 4 5.5z"/><path d="M4 19.5A2.5 2.5 0 0 0 6.5 22H20v-5"/>`,
57
58
  external: () => html`<path d="M14 4h6v6M20 4l-9 9"/><path d="M18 14v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h5"/>`,
58
59
  };
59
60