@songsid/agend 2.2.0-beta.9 → 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 (226) 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 +3 -0
  26. package/dist/backend/grok.js +9 -0
  27. package/dist/backend/grok.js.map +1 -1
  28. package/dist/backend/kiro.d.ts +3 -1
  29. package/dist/backend/kiro.js +7 -3
  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/types.d.ts +24 -0
  35. package/dist/backend/types.js +12 -1
  36. package/dist/backend/types.js.map +1 -1
  37. package/dist/backend-command.d.ts +91 -0
  38. package/dist/backend-command.js +122 -0
  39. package/dist/backend-command.js.map +1 -0
  40. package/dist/channel/adapters/discord.d.ts +12 -0
  41. package/dist/channel/adapters/discord.js +49 -2
  42. package/dist/channel/adapters/discord.js.map +1 -1
  43. package/dist/channel/adapters/telegram.d.ts +18 -0
  44. package/dist/channel/adapters/telegram.js +24 -3
  45. package/dist/channel/adapters/telegram.js.map +1 -1
  46. package/dist/channel/mcp-tools.js +2 -0
  47. package/dist/channel/mcp-tools.js.map +1 -1
  48. package/dist/channel/tool-router.d.ts +2 -0
  49. package/dist/channel/tool-router.js +1 -1
  50. package/dist/channel/tool-router.js.map +1 -1
  51. package/dist/channel/types.d.ts +16 -0
  52. package/dist/cli.js +124 -24
  53. package/dist/cli.js.map +1 -1
  54. package/dist/command-table.d.ts +2 -2
  55. package/dist/command-table.js +5 -2
  56. package/dist/command-table.js.map +1 -1
  57. package/dist/compact-watch.d.ts +86 -0
  58. package/dist/compact-watch.js +288 -0
  59. package/dist/compact-watch.js.map +1 -0
  60. package/dist/config-validator.js +39 -0
  61. package/dist/config-validator.js.map +1 -1
  62. package/dist/config.d.ts +1 -0
  63. package/dist/config.js +2 -1
  64. package/dist/config.js.map +1 -1
  65. package/dist/daemon.d.ts +9 -0
  66. package/dist/daemon.js +150 -9
  67. package/dist/daemon.js.map +1 -1
  68. package/dist/delivery-outbox.d.ts +8 -1
  69. package/dist/delivery-outbox.js +6 -2
  70. package/dist/delivery-outbox.js.map +1 -1
  71. package/dist/doctor.js +7 -0
  72. package/dist/doctor.js.map +1 -1
  73. package/dist/fleet-context.d.ts +7 -0
  74. package/dist/fleet-manager.d.ts +158 -9
  75. package/dist/fleet-manager.js +827 -118
  76. package/dist/fleet-manager.js.map +1 -1
  77. package/dist/forged-envelope-scan.d.ts +4 -3
  78. package/dist/forged-envelope-scan.js +4 -3
  79. package/dist/forged-envelope-scan.js.map +1 -1
  80. package/dist/general-knowledge/skills/model-discovery/SKILL.md +4 -1
  81. package/dist/install-lock.d.ts +18 -1
  82. package/dist/install-lock.js +30 -3
  83. package/dist/install-lock.js.map +1 -1
  84. package/dist/instance-avatar.d.ts +5 -4
  85. package/dist/instance-avatar.js +12 -8
  86. package/dist/instance-avatar.js.map +1 -1
  87. package/dist/instructions.js +2 -2
  88. package/dist/instructions.js.map +1 -1
  89. package/dist/kiro-compact-pane.d.ts +38 -0
  90. package/dist/kiro-compact-pane.js +57 -0
  91. package/dist/kiro-compact-pane.js.map +1 -0
  92. package/dist/launchd-handoff.d.ts +43 -0
  93. package/dist/launchd-handoff.js +56 -0
  94. package/dist/launchd-handoff.js.map +1 -0
  95. package/dist/locale.js +202 -30
  96. package/dist/locale.js.map +1 -1
  97. package/dist/log-service.js +10 -2
  98. package/dist/log-service.js.map +1 -1
  99. package/dist/login-shell.d.ts +21 -0
  100. package/dist/login-shell.js +98 -0
  101. package/dist/login-shell.js.map +1 -0
  102. package/dist/outbound-handlers.d.ts +27 -0
  103. package/dist/outbound-handlers.js +120 -36
  104. package/dist/outbound-handlers.js.map +1 -1
  105. package/dist/outbound-schemas.d.ts +12 -0
  106. package/dist/outbound-schemas.js +15 -4
  107. package/dist/outbound-schemas.js.map +1 -1
  108. package/dist/provider-probe.d.ts +29 -0
  109. package/dist/provider-probe.js +198 -2
  110. package/dist/provider-probe.js.map +1 -1
  111. package/dist/public-web-gateway.d.ts +2 -0
  112. package/dist/public-web-gateway.js +11 -1
  113. package/dist/public-web-gateway.js.map +1 -1
  114. package/dist/quickstart-api.d.ts +1 -1
  115. package/dist/quickstart-api.js +8 -6
  116. package/dist/quickstart-api.js.map +1 -1
  117. package/dist/quickstart.d.ts +12 -0
  118. package/dist/quickstart.js +36 -6
  119. package/dist/quickstart.js.map +1 -1
  120. package/dist/reply-buttons-controller.d.ts +86 -5
  121. package/dist/reply-buttons-controller.js +271 -7
  122. package/dist/reply-buttons-controller.js.map +1 -1
  123. package/dist/reply-buttons.d.ts +48 -3
  124. package/dist/reply-buttons.js +105 -10
  125. package/dist/reply-buttons.js.map +1 -1
  126. package/dist/reply-polls.d.ts +85 -0
  127. package/dist/reply-polls.js +154 -0
  128. package/dist/reply-polls.js.map +1 -0
  129. package/dist/restart-guard.d.ts +6 -3
  130. package/dist/restart-guard.js +6 -3
  131. package/dist/restart-guard.js.map +1 -1
  132. package/dist/restart-progress.d.ts +0 -2
  133. package/dist/restart-progress.js +0 -2
  134. package/dist/restart-progress.js.map +1 -1
  135. package/dist/sd-notify.d.ts +22 -1
  136. package/dist/sd-notify.js +63 -10
  137. package/dist/sd-notify.js.map +1 -1
  138. package/dist/service-activation.d.ts +15 -7
  139. package/dist/service-activation.js +42 -26
  140. package/dist/service-activation.js.map +1 -1
  141. package/dist/service-installer.d.ts +24 -0
  142. package/dist/service-installer.js +49 -1
  143. package/dist/service-installer.js.map +1 -1
  144. package/dist/service-plan.d.ts +11 -0
  145. package/dist/service-plan.js +23 -0
  146. package/dist/service-plan.js.map +1 -1
  147. package/dist/systemd-peer-bus.d.ts +95 -0
  148. package/dist/systemd-peer-bus.js +430 -0
  149. package/dist/systemd-peer-bus.js.map +1 -0
  150. package/dist/systemd-runtime.d.ts +27 -1
  151. package/dist/systemd-runtime.js +69 -7
  152. package/dist/systemd-runtime.js.map +1 -1
  153. package/dist/tool-permissions.js +3 -3
  154. package/dist/tool-permissions.js.map +1 -1
  155. package/dist/topic-commands.d.ts +68 -2
  156. package/dist/topic-commands.js +141 -27
  157. package/dist/topic-commands.js.map +1 -1
  158. package/dist/transcript-jsonl.d.ts +7 -2
  159. package/dist/transcript-jsonl.js +13 -7
  160. package/dist/transcript-jsonl.js.map +1 -1
  161. package/dist/transcript-sources.js +6 -1
  162. package/dist/transcript-sources.js.map +1 -1
  163. package/dist/types.d.ts +8 -0
  164. package/dist/ui/chat-commands.js +1 -0
  165. package/dist/ui/chat-render.js +55 -10
  166. package/dist/ui/chat-store.js +69 -5
  167. package/dist/ui/chat-strings.js +28 -16
  168. package/dist/ui/chat-thread.js +132 -14
  169. package/dist/ui/chat-tour.js +17 -14
  170. package/dist/ui/panel-chat.js +16 -10
  171. package/dist/ui/panel-fleet.js +15 -2
  172. package/dist/ui/panel-log.js +4 -4
  173. package/dist/ui/panel-needs.js +10 -7
  174. package/dist/ui/panel-settings.js +58 -7
  175. package/dist/ui/settings-dialogs.js +11 -6
  176. package/dist/ui/settings-model.js +3 -1
  177. package/dist/ui/settings-strings.js +24 -10
  178. package/dist/ui/settings-token.js +7 -0
  179. package/dist/ui/settings-wizard.js +5 -3
  180. package/dist/ui/shared/app-i18n.js +1 -1
  181. package/dist/ui/shared/app-stream.js +4 -1
  182. package/dist/ui/shared/app.css +52 -10
  183. package/dist/ui/shared/header-tools.js +34 -14
  184. package/dist/ui/shared/instance-switch.js +5 -2
  185. package/dist/ui/shared/panel-view.js +15 -4
  186. package/dist/ui/shared/signin.js +15 -9
  187. package/dist/ui/shared/ui-icons.js +1 -0
  188. package/dist/ui/shared/view-strings.js +10 -2
  189. package/dist/ui/signin.html +8 -3
  190. package/dist/update-check.d.ts +31 -0
  191. package/dist/update-check.js +47 -0
  192. package/dist/update-check.js.map +1 -1
  193. package/dist/update-launch.d.ts +48 -1
  194. package/dist/update-launch.js +159 -5
  195. package/dist/update-launch.js.map +1 -1
  196. package/dist/usage/providers.d.ts +28 -8
  197. package/dist/usage/providers.js +96 -41
  198. package/dist/usage/providers.js.map +1 -1
  199. package/dist/view-api.js +26 -9
  200. package/dist/view-api.js.map +1 -1
  201. package/dist/web-api.d.ts +44 -2
  202. package/dist/web-api.js +265 -57
  203. package/dist/web-api.js.map +1 -1
  204. package/dist/web-chat-history.d.ts +26 -1
  205. package/dist/web-chat-history.js +47 -8
  206. package/dist/web-chat-history.js.map +1 -1
  207. package/dist/web-commands.d.ts +39 -3
  208. package/dist/web-commands.js +22 -5
  209. package/dist/web-commands.js.map +1 -1
  210. package/dist/web-login.d.ts +14 -0
  211. package/dist/web-login.js +4 -2
  212. package/dist/web-login.js.map +1 -1
  213. package/dist/web-prefs.d.ts +18 -0
  214. package/dist/web-prefs.js +36 -4
  215. package/dist/web-prefs.js.map +1 -1
  216. package/dist/web-profile.d.ts +32 -0
  217. package/dist/web-profile.js +132 -0
  218. package/dist/web-profile.js.map +1 -0
  219. package/dist/web-session.d.ts +14 -0
  220. package/dist/web-session.js +10 -0
  221. package/dist/web-session.js.map +1 -1
  222. package/dist/web-upload.d.ts +119 -4
  223. package/dist/web-upload.js +252 -39
  224. package/dist/web-upload.js.map +1 -1
  225. package/launcher/install-admission.cjs +61 -6
  226. package/package.json +3 -3
@@ -5,7 +5,7 @@
5
5
  // here re-reads anything). Every read is under the navigation's lease: a list that arrives after the person moved on
6
6
  // is dropped (#1408 §4).
7
7
  import { html, useEffect, useRef, useState } from "/assets/app-html.js";
8
- import { t, register } from "/assets/app-i18n.js";
8
+ import { t, register, lang } from "/assets/app-i18n.js";
9
9
  import { appStore, useStore } from "/assets/app-store.js";
10
10
  import { useLease } from "/assets/app-ctx.js";
11
11
  import { PanelHeader, setTitle } from "/assets/app-shell.js";
@@ -27,6 +27,7 @@ register("fleet", {
27
27
  hostMemory_elevated: "Host memory pressure: elevated — new agents start one at a time, at least 5 s apart",
28
28
  hostMemory_critical: "Host memory pressure: critical — new agents wait for memory to recover",
29
29
  title: "Fleet", tasks: "Tasks", schedules: "Schedules", teams: "Teams", org: "Org chart", cache: "Cache", config: "Config",
30
+ docs: "Docs", docsNewTab: "Docs (opens in a new tab)",
30
31
  newTask: "New task", newSchedule: "New schedule", newTeam: "New team",
31
32
  noTasks: "No tasks yet", noTasksHint: "Create one to coordinate fleet work.", noSchedules: "No schedules yet", noSchedulesHint: "Automate recurring work with cron.",
32
33
  noTeams: "No teams yet", noTeamsHint: "Group instances for coordinated work.", countTasks: "{0} tasks", countSchedules: "{0} schedules", countTeams: "{0} teams", countTask: "1 task", countSchedule: "1 schedule", countTeam: "1 team",
@@ -57,6 +58,7 @@ register("fleet", {
57
58
  hostMemory_elevated: "主機記憶體壓力偏高:新的 agent 一次啟動一個,至少間隔 5 秒",
58
59
  hostMemory_critical: "主機記憶體壓力嚴重:新的 agent 會等記憶體恢復再啟動",
59
60
  title: "Fleet", tasks: "Tasks", schedules: "排程", teams: "Teams", org: "組織圖", cache: "快取", config: "設定",
61
+ docs: "文件", docsNewTab: "文件(在新分頁開啟)",
60
62
  newTask: "新增 task", newSchedule: "新增排程", newTeam: "新增 team",
61
63
  noTasks: "還沒有 task", noTasksHint: "建立一個來協調 fleet 的工作。", noSchedules: "還沒有排程", noSchedulesHint: "用 cron 自動執行例行工作。",
62
64
  noTeams: "還沒有 team", noTeamsHint: "把 instance 分組一起工作。", countTasks: "{0} 個 task", countSchedules: "{0} 個排程", countTeams: "{0} 個 team", countTask: "1 個 task", countSchedule: "1 個排程", countTeam: "1 個 team",
@@ -84,6 +86,11 @@ register("fleet", {
84
86
  },
85
87
  });
86
88
 
89
+ // The project's docs site (website/, GitHub Pages), in the dashboard's language — its own pages, not the app's.
90
+ const DOCS_SITE = "https://songsid.github.io/AgEnD";
91
+ export function docsUrl(language = lang()) {
92
+ return `${DOCS_SITE}${language === "zh-TW" ? "/zh-tw" : ""}/docs/web-guide/`;
93
+ }
87
94
  const ICONS = { tasks: "tasks", schedules: "clock", teams: "team", org: "org", cache: "chart", config: "sliders" };
88
95
  const BACKENDS = ["claude-code", "codex", "opencode", "kiro-cli", "antigravity", "grok", "muse"];
89
96
 
@@ -128,13 +135,19 @@ export function FleetPanel({ route, navKey }) {
128
135
  const { hostMemory } = useStore(appStore);
129
136
  const sub = hostMemory ? html`<span class=${`status host-mem ${hostMemory}`} role="status"><span class=${`dot ${hostMemory === "critical" ? "bad" : "warn"}`} aria-hidden="true"></span>${t(`fleet.hostMemory_${hostMemory}`)}</span>` : null;
130
137
  return html`<div class="panel p-fleet">
131
- <${PanelHeader} title=${t("fleet.title")} sub=${sub}><${HeaderTools} navKey=${navKey} /></${PanelHeader}>
138
+ <${PanelHeader} title=${t("fleet.title")} sub=${sub}><${DocsLink} /><${HeaderTools} navKey=${navKey} /></${PanelHeader}>
132
139
  <nav class="seg" aria-label=${t("fleet.title")}>${FLEET_TABS.map(k => html`<a key=${k} href=${fleetPath(k)} class=${`seg-item${k === tab ? " active" : ""}`}
133
140
  aria-current=${k === tab ? "page" : undefined}><${Icon} name=${ICONS[k]} size=${16} /><span>${t(`fleet.${k}`)}</span></a>`)}</nav>
134
141
  <div class="panel-body"><div class=${`col${tab === "org" || tab === "cache" ? " col-wide" : ""}`}><${Body} lease=${lease} /></div></div>
135
142
  </div>`;
136
143
  }
137
144
 
145
+ /** #1844: the project's docs, from the overview — a link out (a new tab, no opener), not a Fleet tab; in front of the shared controls. */
146
+ function DocsLink() {
147
+ return html`<a class="btn btn-ghost btn-sm hd-docs" href=${docsUrl()} target="_blank" rel="noopener noreferrer" aria-label=${t("fleet.docsNewTab")} title=${t("fleet.docsNewTab")}>
148
+ <${Icon} name="book" size=${16} /><span class="hide-narrow" aria-hidden="true">${t("fleet.docs")}</span><${Icon} name="external" size=${12} cls="hd-ext" /></a>`;
149
+ }
150
+
138
151
  function ListHead({ count, label, action }) {
139
152
  return html`<div class="list-head"><span>${count}</span>${action ? html`<button type="button" class="btn btn-primary" onClick=${action.onClick}><${Icon} name="plus" size=${16} />${label}</button>` : null}</div>`;
140
153
  }
@@ -17,7 +17,7 @@ import { appStore, useStore } from "/assets/app-store.js";
17
17
  import { useLease } from "/assets/app-ctx.js";
18
18
  import { PanelHeader, setTitle, statusClass, statusLabel, openDrawer } from "/assets/app-shell.js";
19
19
  import { InstanceSwitch } from "/assets/instance-switch.js";
20
- import { HeaderTools } from "/assets/header-tools.js";
20
+ import { HeaderSlot, HeaderTools } from "/assets/header-tools.js";
21
21
  import { Empty, ErrorState, Skeleton } from "/assets/ui-states.js";
22
22
  import { Icon } from "/assets/ui-icons.js";
23
23
  import { toast } from "/assets/ui-toast.js";
@@ -29,7 +29,7 @@ register("log", {
29
29
  en: {
30
30
  offTitle: "The Log is off",
31
31
  offBody: "Turn it on to see each agent's work step by step: what it was asked, its replies, every tool call with its arguments and result, errors, and fleet events. It reads the agent's own transcript only while this tab is open, and masks secret values.",
32
- enable: "Turn on the Log", enabling: "Waiting for a fleet admin to confirm it in General…", enabled: "The Log is on.",
32
+ enable: "Turn on the Log", enabling: "Waiting for a fleet admin to confirm it in chat (General topic) or on the host…", enabled: "The Log is on.",
33
33
  enableFailed: "The Log was not turned on: {0}", offPublic: "A fleet admin can turn it on in Settings → Web.",
34
34
  unsupported: "{0} has no transcript AgEnD can read, so this Log shows fleet events only. View shows its screen.",
35
35
  kiroWhy: "kiro's own steps are shown only when AgEnD can prove which conversation it is in now. {0} Fleet events are shown meanwhile.",
@@ -67,7 +67,7 @@ register("log", {
67
67
  "zh-TW": {
68
68
  offTitle: "紀錄未開啟",
69
69
  offBody: "開啟後可逐步看到每個 agent 的工作:收到的要求、回覆、每次工具呼叫的參數與結果、錯誤,以及 fleet 事件。只在這個分頁開著時讀取 agent 自己的對話紀錄,機密值會遮蔽。",
70
- enable: "開啟紀錄", enabling: "等待 fleet admin 在 General 確認…", enabled: "紀錄已開啟。",
70
+ enable: "開啟紀錄", enabling: "等待 fleet admin 在聊天(General topic)或主機上確認…", enabled: "紀錄已開啟。",
71
71
  enableFailed: "紀錄沒有開啟:{0}", offPublic: "fleet admin 可以在「設定 → 網頁」開啟。",
72
72
  unsupported: "{0} 沒有 AgEnD 讀得到的對話紀錄,所以這裡只顯示 fleet 事件。「檢視」可以看它的畫面。",
73
73
  kiroWhy: "只有在 AgEnD 能證明 kiro 目前所在的對話時,才會顯示它自己的步驟。{0}目前先顯示 fleet 事件。",
@@ -376,7 +376,7 @@ function LogView({ name, inst, navKey, exec, awaiting }) {
376
376
  ${["all", "tools", "errors"].map((k) => html`<button key=${k} type="button" class=${`chip-btn${filter === k ? " on" : ""}`} aria-pressed=${filter === k ? "true" : "false"} onClick=${() => setFilter(k)}>${tl(k)}</button>`)}
377
377
  <span class="grow"></span><span class="log-live" title=${tl("live")}><span class="dot running" aria-hidden="true"></span>${tl("live")}</span></div>` : null;
378
378
  return html`<div class="panel p-log">
379
- <${PanelHeader} title=${name} sub=${sub} nav=${html`<${InstanceSwitch} name=${name} current="log" />`}><${HeaderTools} navKey=${navKey} /></${PanelHeader}>
379
+ <${PanelHeader} title=${name} sub=${sub} nav=${html`<${InstanceSwitch} name=${name} current="log" />`}><${HeaderTools} navKey=${navKey} /><${HeaderSlot} /></${PanelHeader}>
380
380
  ${tools}
381
381
  <div class="panel-body log-body" ref=${scroller} onScroll=${onScroll}><div class="col log-col">${content}</div></div>
382
382
  </div>`;
@@ -23,25 +23,25 @@ import { notifyStore, notifySupport, reasonText, refreshNotify, setNotify } from
23
23
 
24
24
  register("needs", {
25
25
  en: {
26
- title: "Needs you", empty: "Nothing needs you right now", emptyHint: "When an agent waits for you, it shows here — and in Discord.",
26
+ title: "Needs you", empty: "Nothing needs you right now", emptyHint: "When an agent waits for you, it shows here — and in your chat app's General.", emptyHintWeb: "When an agent waits for you, it shows here.",
27
27
  open: "Open chat", acknowledge: "Acknowledge", acknowledging: "Acknowledging…", answering: "Answering…",
28
28
  ageNow: "just now", ageMin: "{0} min", ageHour: "{0} h", ageDay: "{0} d",
29
29
  notifyTitle: "Notify me on this device", notifyOn: "Notifications are on for this browser while a tab is open.",
30
30
  notifyOff: "Get a desktop notification when something new needs you while this tab is in the background.",
31
31
  notifyDenied: "This browser blocked notifications for AgEnD. Allow them in the site settings to turn this on.",
32
- notifyMobile: "On a phone, rely on the Discord notification: a new item is a new post in General.",
32
+ notifyMobile: "On a phone, rely on your chat app's notification: a new item is a new post in General.", notifyMobileWeb: "On a phone, open this page to see what needs you.",
33
33
  notifyInsecure: "Notifications need a secure address (localhost or the HTTPS public link), not a plain-HTTP LAN address.",
34
34
  notifyUnsupported: "This browser cannot show notifications.", turnOn: "Turn on", turnOff: "Turn off",
35
35
  typePrompt: "Fleet prompt", typeAwaiting: "At its terminal", typeInstance: "Instance", typeDelivery: "Delivery",
36
36
  },
37
37
  "zh-TW": {
38
- title: "等你處理", empty: "目前沒有需要你處理的事", emptyHint: "agent 在等你時,會出現在這裡——也會出現在 Discord。",
38
+ title: "等你處理", empty: "目前沒有需要你處理的事", emptyHint: "agent 在等你時,會出現在這裡——也會出現在聊天 app 的 General。", emptyHintWeb: "agent 在等你時,會出現在這裡。",
39
39
  open: "開啟聊天", acknowledge: "確認", acknowledging: "確認中…", answering: "回覆中…",
40
40
  ageNow: "剛剛", ageMin: "{0} 分鐘", ageHour: "{0} 小時", ageDay: "{0} 天",
41
41
  notifyTitle: "在這台裝置通知我", notifyOn: "這個瀏覽器的通知已開啟(需要開著分頁)。",
42
42
  notifyOff: "分頁在背景時,有新的事等你處理就跳出桌面通知。",
43
43
  notifyDenied: "這個瀏覽器封鎖了 AgEnD 的通知。請在網站設定中允許後再開啟。",
44
- notifyMobile: "在手機上請以 Discord 通知為主:每個新項目都會在 General 發一則新訊息。",
44
+ notifyMobile: "在手機上請以聊天 app 的通知為主:每個新項目都會在 General 發一則新訊息。", notifyMobileWeb: "在手機上請打開這個頁面查看等你處理的事。",
45
45
  notifyInsecure: "通知需要安全的網址(localhost 或 HTTPS 公開連結),區網的一般 HTTP 網址不行。",
46
46
  notifyUnsupported: "這個瀏覽器無法顯示通知。", turnOn: "開啟", turnOff: "關閉",
47
47
  typePrompt: "Fleet 提示", typeAwaiting: "在終端機上", typeInstance: "Instance", typeDelivery: "傳送",
@@ -108,7 +108,9 @@ export function groupNeeds(items) {
108
108
 
109
109
  export function NeedsPanel({ navKey }) {
110
110
  const lease = useLease(navKey);
111
- const { needs, chatOwner: owner, instances } = useStore(appStore);
111
+ const { needs, chatOwner: owner, instances, connections } = useStore(appStore);
112
+ // #1854: a fleet with no chat connection (web only, or not known yet) has no General to point at.
113
+ const chatApp = connections > 0;
112
114
  const names = useStore(namesStore);
113
115
  // #1630/#1722: a group is headed by the instance's leading name (the "Names" preference), as the sidebar shows it.
114
116
  const nameFor = (instance) => { const it = (instances || []).find((i) => i && i.name === instance); return it ? namesOf(it, names.primary).primary : instance; };
@@ -124,7 +126,7 @@ export function NeedsPanel({ navKey }) {
124
126
  return html`<div class="panel p-needs">
125
127
  <${PanelHeader} title=${tn("title")} sub=${items.length ? t("app.needsCount", items.length) : null} />
126
128
  <div class="panel-body"><div class="col">
127
- ${!items.length ? html`<${Empty} icon="check" title=${tn("empty")} hint=${tn("emptyHint")} />`
129
+ ${!items.length ? html`<${Empty} icon="check" title=${tn("empty")} hint=${tn(chatApp ? "emptyHint" : "emptyHintWeb")} />`
128
130
  : groups.map((g) => html`<section key=${g.instance} class="n-group" aria-label=${g.instance}>
129
131
  <h2 class="n-group-head"><a href=${chatPath(g.instance)} title=${g.instance}>${nameFor(g.instance)}</a></h2>
130
132
  ${g.items.map((item) => html`<${NeedsItem} key=${item.id} item=${item} nowMs=${nowMs} owner=${owner} />`)}
@@ -196,7 +198,8 @@ function NotifyToggle() {
196
198
  useEffect(() => { refreshNotify(); }, []);
197
199
  const on = state === "on";
198
200
  const toggle = () => { setNotify(!on); };
199
- const note = support === "mobile" ? tn("notifyMobile") : support === "insecure" ? tn("notifyInsecure")
201
+ const { connections } = useStore(appStore);
202
+ const note = support === "mobile" ? tn(connections > 0 ? "notifyMobile" : "notifyMobileWeb") : support === "insecure" ? tn("notifyInsecure")
200
203
  : support !== "ok" ? tn("notifyUnsupported") : state === "denied" ? tn("notifyDenied") : on ? tn("notifyOn") : tn("notifyOff");
201
204
  return html`<section class="card n-notify"><h3>${tn("notifyTitle")}</h3>
202
205
  <p class="note">${note}</p>
@@ -24,7 +24,7 @@ import "./settings-strings.js";
24
24
  import { api, attach, confirmedWrite, newKey } from "./settings-confirm.js";
25
25
  import { dismissOperation, operationActive, restartFleet, startOperation, takeLeftover } from "./settings-apply.js";
26
26
  import {
27
- ACCESS_MODES, AGENT_MODES, DEFAULT_SCHEMA, LOG_LEVELS, TOOL_PROGRESS, TOOL_SETS, VISIBILITY_MODES, batchImpact,
27
+ ACCESS_MODES, AGENT_MODES, DEFAULT_SCHEMA, HANG_TIMEOUT_DEFAULT_MINUTES, LOG_LEVELS, TOOL_PROGRESS, TOOL_SETS, VISIBILITY_MODES, batchImpact,
28
28
  changedFields, channelId, channelsOf, chLabel, connectionState, effectiveSummary, fleetModel, fromYaml, fullModelRequests, groupAgents,
29
29
  hasOwn, impactOf, localeOptions, defaultsImpact, requestImpact, nonNegative, positive, replyGuardSupported, shortName, STATUS_ORDER, toYaml,
30
30
  visibilityDefault, worstImpact,
@@ -270,7 +270,11 @@ export function DeviceSection() {
270
270
  const replay = () => {
271
271
  let last = null;
272
272
  try { last = localStorage.getItem("agend_last_instance"); } catch { /* none */ }
273
- navigate(last ? chatPath(last) : "/ui");
273
+ // #1854: the tour is about an agent's chat — with no agent to open (none remembered, or it is gone), say so instead
274
+ // of opening an empty chat that waits ~3 s and shows nothing.
275
+ const { instances } = appStore.get();
276
+ if (!last || !(instances || []).some((i) => i && i.name === last)) { toast(t("chat.tourNoAgent"), false); return; }
277
+ navigate(chatPath(last));
274
278
  // This replay belongs to the navigation it just made — that one, by its sequence number, not by URL (A → B → A, or
275
279
  // the same path again, is a new navigation). It did not happen (a leave guard said no) or it is not the chat:
276
280
  // nothing. Left meanwhile (another page, another navigation): nothing (#1609 review).
@@ -285,7 +289,7 @@ export function DeviceSection() {
285
289
  let tries = 0;
286
290
  const go = () => {
287
291
  if (!current()) return;
288
- if (!document.getElementById("sendBtn")) { if (++tries <= 30) setTimeout(go, 100); return; }
292
+ if (!document.getElementById("sendBtn")) { if (++tries <= 30) setTimeout(go, 100); else toast(t("chat.tourNoAgent"), false); return; }
289
293
  const here = document.getElementById("msgIn") || document.getElementById("main");
290
294
  if (here && typeof here.focus === "function") here.focus();
291
295
  if (current()) startTour();
@@ -293,7 +297,8 @@ export function DeviceSection() {
293
297
  setTimeout(go, 0);
294
298
  };
295
299
  const publicLink = typeof document !== "undefined" && document.body && document.body.dataset.publicLink === "1";
296
- return html`<section class="card s-device s-synced"><h3>${tn("syncedTitle")}</h3>
300
+ return html`<${ProfileCard} />
301
+ <section class="card s-device s-synced"><h3>${tn("syncedTitle")}</h3>
297
302
  <p class="sub">${publicLink ? tn("syncedPublicLink") : tn("syncedLead")}</p>
298
303
  <${DevicePrefs} labels=${true} />
299
304
  ${P() ? html`<label class="check"><input type="checkbox" checked=${on} onChange=${(e) => { const want = e.target.checked; setPreviewOptIn(want).then(setOn); }} />
@@ -305,6 +310,52 @@ export function DeviceSection() {
305
310
  </section>`;
306
311
  }
307
312
 
313
+ /**
314
+ * #1827: who agents see when this person writes from the web. Signed in with a code sent to their Discord or Telegram
315
+ * account, they are that account — its user id, and its name unless they choose another (it follows them to every
316
+ * device, the public link included). Otherwise "web-user", as before. The host checks a name in full and says why it
317
+ * refuses one; this only shows what it says.
318
+ */
319
+ const view = (b) => ({ phase: "ready", name: b.name || "", shown: b.shown || "", id: b.id || "", bound: b.bound === true, platform: b.platform_name || "", error: "" });
320
+ export function ProfileCard() {
321
+ const [state, setState] = useState({ phase: "loading", name: "", shown: "", id: "", bound: false, platform: "", error: "" });
322
+ const [draft, setDraft] = useState("");
323
+ const lease = useLease();
324
+ useEffect(() => {
325
+ let live = true;
326
+ api("/ui/profile").then((r) => {
327
+ if (!live) return;
328
+ if (!r.ok || !r.body) { setState((s) => ({ ...s, phase: "unavailable" })); return; }
329
+ setState(view(r.body));
330
+ setDraft(r.body.name || "");
331
+ }).catch(() => { if (live) setState((s) => ({ ...s, phase: "unavailable" })); });
332
+ return () => { live = false; };
333
+ }, []);
334
+ if (state.phase === "unavailable") return null;
335
+ const save = (clear) => {
336
+ setState((s) => ({ ...s, phase: "saving", error: "" }));
337
+ api("/ui/profile", { method: "POST", body: JSON.stringify({ name: clear ? null : draft }) }).then((r) => {
338
+ if (lease && !lease.current()) return;
339
+ if (!r.ok) { setState((s) => ({ ...s, phase: "ready", error: (r.body && r.body.error) || tn("profileFailed") })); return; }
340
+ setState(view(r.body));
341
+ setDraft(r.body.name || "");
342
+ toast(tn("profileSaved", r.body.shown || "web-user"));
343
+ }).catch(() => setState((s) => ({ ...s, phase: "ready", error: tn("profileFailed") })));
344
+ };
345
+ const busy = state.phase !== "ready";
346
+ const changed = draft.trim() !== state.name;
347
+ return html`<section class="card s-device s-profile"><h3>${tn("profileTitle")}</h3>
348
+ <p class="sub">${state.bound ? tn("profileLeadBound") : tn("profileLeadUnbound")}</p>
349
+ <div class="field"><label for="profile-name">${tn("profileName")}</label>
350
+ <input id="profile-name" type="text" maxlength="200" autocomplete="nickname" value=${draft} disabled=${busy}
351
+ placeholder=${state.platform || "web-user"} aria-invalid=${state.error ? "true" : "false"} aria-describedby="profile-help"
352
+ onInput=${(e) => setDraft(e.target.value)} onKeyDown=${(e) => { if (e.key === "Enter" && changed && draft.trim()) save(false); }} />
353
+ <p class="sub" id="profile-help">${state.error ? html`<span class="err" role="alert">${state.error}</span>` : tn("profileHelp", state.shown || "web-user", state.id)}</p></div>
354
+ <div class="s-device-row"><button type="button" class="btn primary" disabled=${busy || !changed || !draft.trim()} onClick=${() => save(false)}>${tn("profileSave")}</button>
355
+ ${state.name ? html`<button type="button" class="btn" disabled=${busy} onClick=${() => save(true)}>${state.bound ? tn("profileUseAccount", state.platform || "web-user") : tn("profileClear")}</button>` : null}</div>
356
+ </section>`;
357
+ }
358
+
308
359
  /** The operation in hand still owns Apply: its writes and job, or a restart of AgEnD from the press to its end. */
309
360
  const opBusy = (op) => !!op && (["writing", "posting", "watching"].includes(op.phase) || op.restart === "busy");
310
361
  function dropSecrets(staged) { for (const c of staged.values()) if (c.connectionSecret) c.connectionSecret.secret = ""; }
@@ -588,7 +639,7 @@ function General({ ctx }) {
588
639
  backend: d.backend || "claude-code", model: d.model || "", locale: d.locale || "", auto: String(d.auto_pause_after ?? 0),
589
640
  agent_mode: d.agent_mode || "mcp", tool_set: d.tool_set || "full", tool_progress: d.tool_progress ?? "off",
590
641
  visibility: visibilityDefault(d), reply: d.reply_completion_guard ?? true, log_level: d.log_level || "info",
591
- hangOn: hang.enabled !== false, hang: String(hang.timeout_minutes ?? 15),
642
+ hangOn: hang.enabled !== false, hang: String(hang.timeout_minutes ?? HANG_TIMEOUT_DEFAULT_MINUTES),
592
643
  concurrency: String(startup.concurrency ?? 10), stagger: String(startup.stagger_delay_ms ?? 500),
593
644
  classicBackend: c.backend || d.backend || "claude-code",
594
645
  classicToolProgress: c.tool_progress ?? d.tool_progress ?? "off", classicToolProgressInherit: !hasOwn(c, "tool_progress"),
@@ -617,7 +668,7 @@ function General({ ctx }) {
617
668
  backend: d.backend || "claude-code", model: d.model || undefined, locale: d.locale || null, auto_pause_after: d.auto_pause_after ?? 0,
618
669
  agent_mode: d.agent_mode || "mcp", tool_set: d.tool_set || "full", tool_progress: d.tool_progress ?? "off",
619
670
  cross_instance_visibility: visibilityDefault(d), reply_completion_guard: d.reply_completion_guard ?? true, log_level: d.log_level || "info",
620
- hang_detector: { enabled: hang.enabled !== false, timeout_minutes: hang.timeout_minutes ?? 15 },
671
+ hang_detector: { enabled: hang.enabled !== false, timeout_minutes: hang.timeout_minutes ?? HANG_TIMEOUT_DEFAULT_MINUTES },
621
672
  startup: { concurrency: startup.concurrency ?? 10, stagger_delay_ms: startup.stagger_delay_ms ?? 500 },
622
673
  });
623
674
  const oc = original.c;
@@ -710,7 +761,7 @@ function General({ ctx }) {
710
761
  */
711
762
  export const WEB_READ_ONLY = [
712
763
  "web.allowed_hosts", "web.view_access", "web.notify_login", "web.preview", "web.preview_port", "web.preview_origin", "web.preview_on_public_link",
713
- "web.usage_panel", "web.provider_secrets", "web_terminal.bind", "web_terminal.tunnel",
764
+ "web.usage_panel", "web.provider_secrets", "web.upload_max_file_mb", "web.upload_max_total_mb", "web_terminal.bind", "web_terminal.tunnel",
714
765
  ];
715
766
  function WebSection({ ctx, staged }) {
716
767
  const { schema } = ctx;
@@ -10,11 +10,11 @@ import { Icon } from "/assets/ui-icons.js";
10
10
  import { toast } from "/assets/ui-toast.js";
11
11
  import { confirmDialog } from "/assets/ui-confirm.js";
12
12
  import { api, confirmedWrite, newKey } from "./settings-confirm.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
  import { startOperation } from "./settings-apply.js";
16
16
  import {
17
- ACCESS_MODES, AGENT_FIELDS, AGENT_MODES, BOT_FIELDS, CH_TYPES, CLASSIC_FIELDS, LOG_LEVELS, TOOL_PROGRESS, TOOL_SETS,
17
+ ACCESS_MODES, AGENT_FIELDS, AGENT_MODES, BOT_FIELDS, CH_TYPES, CLASSIC_FIELDS, HANG_TIMEOUT_DEFAULT_MINUTES, LOG_LEVELS, TOOL_PROGRESS, TOOL_SETS,
18
18
  VISIBILITY_MODES, accessConfirmations, agentForm, agentPatch, backendOptions, changedFields, channelId, channelsOf,
19
19
  chLabel, hasOwn, impactOf, nonNegative, nonNegativeInteger, positive, replyGuardSupported, sameValue, visibilityDefault,
20
20
  webEchoIsGroup, worstImpact,
@@ -242,7 +242,7 @@ export function AgentDialog({ name, inst, ctx, onClose }) {
242
242
  <div class="field">${L(`${tn("hangTimeout")} (${tn("minutes")})`, "instance.hang_detector", "ag-hang")}
243
243
  <${Override} inherit=${f.inherit.hang} onInherit=${setInherit("hang")}>
244
244
  <input id="ag-hang" type="number" min="0.1" step="0.5" class=${errors.hang ? "invalid" : ""} disabled=${f.inherit.hang}
245
- value=${shown("hang", f.hang, String((defaults.hang_detector && defaults.hang_detector.timeout_minutes) ?? 15))} onInput=${(e) => set("hang")(e.target.value)} /></${Override}>
245
+ value=${shown("hang", f.hang, String((defaults.hang_detector && defaults.hang_detector.timeout_minutes) ?? HANG_TIMEOUT_DEFAULT_MINUTES))} onInput=${(e) => set("hang")(e.target.value)} /></${Override}>
246
246
  <${Feedback} error=${errors.hang} /></div>
247
247
  ${[["agent_mode", "agentMode", AGENT_MODES, defaults.agent_mode || "mcp"], ["tool_set", "toolSet", TOOL_SETS, defaults.tool_set || "full"],
248
248
  ["tool_progress", "toolProgress", TOOL_PROGRESS, defaults.tool_progress ?? "off"],
@@ -466,17 +466,18 @@ export function BotDialog({ id, ctx, onClose }) {
466
466
  */
467
467
  export function NewBotDialog({ ctx, onClose }) {
468
468
  const lease = useLease("new-bot");
469
- const [f, setF] = useState({ type: "discord", id: "", token: "", identity: null, guilds: [], group: "", general: "" });
469
+ const [f, setF] = useState({ type: "discord", id: "", token: "", identity: null, guilds: [], group: "", general: "", admin: "" });
470
470
  const [plan, setPlan] = useState(null);
471
471
  const [busy, setBusy] = useState(false);
472
472
  const [err, setErr] = useState("");
473
473
  // The form's revision (#1529 review): any change to the token, platform, id or group moves it; a Verify or a plan
474
474
  // that began before is dropped when it lands — never restoring an old token, naming it, or planning for it.
475
475
  const rev = useRef(0);
476
- const body = (x) => ({ platform: x.type, connection_only: true, ...(x.id.trim() ? { channel_id: x.id.trim() } : {}),
476
+ const body = (x) => ({ platform: x.type, connection_only: true, admin_user_id: x.admin.trim(), ...(x.id.trim() ? { channel_id: x.id.trim() } : {}),
477
477
  ...(x.group.trim() ? (x.type === "discord" ? { guild_id: x.group.trim() } : { group_id: x.group.trim() }) : {}),
478
478
  ...(x.type === "discord" && x.general.trim() ? { general_channel_id: x.general.trim() } : {}) });
479
479
  const planFor = async (x) => {
480
+ if (adminUserIdError(x.type, x.admin.trim())) { setPlan(null); return null; }
480
481
  const at = rev.current;
481
482
  const res = await api("/api/settings/quickstart/plan", { method: "POST", body: JSON.stringify(body(x)) }).catch(() => ({ ok: false, body: {} }));
482
483
  if (!lease.current() || rev.current !== at) return null;
@@ -501,6 +502,8 @@ export function NewBotDialog({ ctx, onClose }) {
501
502
  };
502
503
  const save = async () => {
503
504
  if (!f.identity || !f.identity.valid) { setErr(tn("wizardNeedVerify")); return; }
505
+ const adminError = adminUserIdError(f.type, f.admin.trim());
506
+ if (adminError) { setErr(adminError); return; }
504
507
  const x = f;
505
508
  setBusy(true);
506
509
  const p = await planFor(x);
@@ -514,7 +517,7 @@ export function NewBotDialog({ ctx, onClose }) {
514
517
  };
515
518
  // A change of platform or id is another connection: what was verified and planned no longer applies. The server or
516
519
  // group does not change the id or the token's name, so the plan (and its name under Advanced) stays.
517
- const set = (k) => (v) => { rev.current++; setF((x) => ({ ...x, [k]: v, ...(k === "type" ? { identity: null, guilds: [], group: "", general: "" } : {}) })); if (k !== "group" && k !== "general") setPlan(null); };
520
+ const set = (k) => (v) => { rev.current++; setF((x) => ({ ...x, [k]: v, ...(k === "type" ? { identity: null, guilds: [], group: "", general: "", admin: "" } : {}) })); if (k !== "group" && k !== "general") setPlan(null); };
518
521
  return html`<${Dialog} title=${tn("newBot")} onClose=${onClose} busy=${busy}
519
522
  actions=${html`<button type="button" class="btn" disabled=${busy} onClick=${onClose}>${tn("cancel")}</button>
520
523
  <button type="button" class="btn btn-primary" disabled=${busy || !f.identity || !f.identity.valid} onClick=${save}>${tn("save")}</button>`}>
@@ -528,6 +531,8 @@ export function NewBotDialog({ ctx, onClose }) {
528
531
  guild=${f.group} onGuild=${set("group")} channel=${f.general} onChannel=${set("general")} />`
529
532
  : html`<div class="field"><label for="nb-group">${tn("groupIdField")}</label>
530
533
  <input id="nb-group" type="text" value=${f.group} onInput=${(e) => set("group")(e.target.value.trim())} /></div>`}
534
+ <div class="field"><label for="nb-user">${tn("wizardAdminUser")}</label>
535
+ <input id="nb-user" type="text" required=${true} inputmode="numeric" pattern=${adminUserIdPattern(f.type)} value=${f.admin} onInput=${(e) => set("admin")(e.target.value.trim())} /></div>
531
536
  <${Drawer} title=${tn("advancedSection")}>
532
537
  <div class="field"><label for="nb-id">${tn("connectionIdOptional")}</label><input id="nb-id" type="text" placeholder=${plan ? plan.channel_id : f.type}
533
538
  value=${f.id} onInput=${(e) => set("id")(e.target.value)} onBlur=${() => { if (f.identity && f.identity.valid) planFor(f); }} /></div>
@@ -2,6 +2,8 @@
2
2
  // Ported from settings.html with their reasons: every rule here was a fix (the review or issue named beside it).
3
3
  // No DOM, no fetch; the panel (panel-settings.js) and the tests call these directly.
4
4
 
5
+ /** #1854: the stuck timeout when none is set — the daemon's DEFAULT_STUCK_TIMEOUT_MS (src/daemon.ts), in minutes. */
6
+ export const HANG_TIMEOUT_DEFAULT_MINUTES = 10;
5
7
  export const BACKENDS = ["claude-code", "kiro-cli", "codex", "opencode", "antigravity", "grok", "muse"];
6
8
  export const CH_TYPES = ["discord", "telegram"];
7
9
  export const LOCALES = [["en", "English"], ["zh-TW", "繁體中文 (zh-TW)"]];
@@ -166,7 +168,7 @@ export function agentForm(inst, defaults) {
166
168
  description: inst.description || "", systemPrompt: inst.systemPrompt || "", tags: [...(inst.tags || [])],
167
169
  general_topic: !!inst.general_topic,
168
170
  auto_pause_after: String(inst.auto_pause_after ?? defaults.auto_pause_after ?? 0),
169
- hang: String(hang.timeout_minutes ?? (defaults.hang_detector && defaults.hang_detector.timeout_minutes) ?? 15),
171
+ hang: String(hang.timeout_minutes ?? (defaults.hang_detector && defaults.hang_detector.timeout_minutes) ?? HANG_TIMEOUT_DEFAULT_MINUTES),
170
172
  agent_mode: inst.agent_mode || defaults.agent_mode || "mcp",
171
173
  tool_set: inst.tool_set || defaults.tool_set || "full",
172
174
  tool_progress: inst.tool_progress ?? defaults.tool_progress ?? "off",
@@ -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
  }