@songsid/agend 2.2.0-alpha.1 → 2.2.0-alpha.3

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 (317) hide show
  1. package/README.md +6 -4
  2. package/README.zh-TW.md +6 -4
  3. package/dist/agent-cli.js +6 -1
  4. package/dist/agent-cli.js.map +1 -1
  5. package/dist/agent-endpoint.d.ts +4 -1
  6. package/dist/agent-endpoint.js +82 -14
  7. package/dist/agent-endpoint.js.map +1 -1
  8. package/dist/agent-token-header.d.ts +13 -0
  9. package/dist/agent-token-header.js +16 -0
  10. package/dist/agent-token-header.js.map +1 -0
  11. package/dist/atomic-write.d.ts +21 -0
  12. package/dist/atomic-write.js +65 -0
  13. package/dist/atomic-write.js.map +1 -0
  14. package/dist/auth-api.js +2 -0
  15. package/dist/auth-api.js.map +1 -1
  16. package/dist/backend/antigravity.js +4 -3
  17. package/dist/backend/antigravity.js.map +1 -1
  18. package/dist/backend/codex.d.ts +7 -0
  19. package/dist/backend/codex.js +7 -0
  20. package/dist/backend/codex.js.map +1 -1
  21. package/dist/backend/grok.js +1 -1
  22. package/dist/backend/grok.js.map +1 -1
  23. package/dist/backend/kiro.d.ts +7 -0
  24. package/dist/backend/kiro.js +82 -33
  25. package/dist/backend/kiro.js.map +1 -1
  26. package/dist/backend/muse.d.ts +7 -0
  27. package/dist/backend/muse.js +8 -1
  28. package/dist/backend/muse.js.map +1 -1
  29. package/dist/backend/types.d.ts +12 -0
  30. package/dist/backend/types.js.map +1 -1
  31. package/dist/binary-probe.d.ts +80 -0
  32. package/dist/binary-probe.js +182 -0
  33. package/dist/binary-probe.js.map +1 -0
  34. package/dist/cache-analysis.d.ts +92 -0
  35. package/dist/cache-analysis.js +147 -0
  36. package/dist/cache-analysis.js.map +1 -0
  37. package/dist/cache-ledger.d.ts +108 -0
  38. package/dist/cache-ledger.js +230 -0
  39. package/dist/cache-ledger.js.map +1 -0
  40. package/dist/cache-prices.d.ts +32 -0
  41. package/dist/cache-prices.js +73 -0
  42. package/dist/cache-prices.js.map +1 -0
  43. package/dist/cache-scan.d.ts +39 -0
  44. package/dist/cache-scan.js +115 -0
  45. package/dist/cache-scan.js.map +1 -0
  46. package/dist/cache-service.d.ts +131 -0
  47. package/dist/cache-service.js +452 -0
  48. package/dist/cache-service.js.map +1 -0
  49. package/dist/channel/adapters/discord.d.ts +5 -1
  50. package/dist/channel/adapters/discord.js +56 -11
  51. package/dist/channel/adapters/discord.js.map +1 -1
  52. package/dist/channel/adapters/telegram.d.ts +38 -1
  53. package/dist/channel/adapters/telegram.js +82 -0
  54. package/dist/channel/adapters/telegram.js.map +1 -1
  55. package/dist/channel/mcp-tools.js +1 -1
  56. package/dist/channel/mcp-tools.js.map +1 -1
  57. package/dist/channel/tool-router.d.ts +8 -1
  58. package/dist/channel/tool-router.js +4 -2
  59. package/dist/channel/tool-router.js.map +1 -1
  60. package/dist/channel/types.d.ts +21 -0
  61. package/dist/cli-entry.d.ts +19 -0
  62. package/dist/cli-entry.js +36 -0
  63. package/dist/cli-entry.js.map +1 -0
  64. package/dist/cli.js +376 -54
  65. package/dist/cli.js.map +1 -1
  66. package/dist/connection-secrets.d.ts +5 -0
  67. package/dist/connection-secrets.js.map +1 -1
  68. package/dist/cpu-profile.js +50 -0
  69. package/dist/cpu-profile.js.map +1 -1
  70. package/dist/daemon.d.ts +18 -0
  71. package/dist/daemon.js +256 -63
  72. package/dist/daemon.js.map +1 -1
  73. package/dist/delivery-outbox.d.ts +1 -0
  74. package/dist/delivery-outbox.js +12 -0
  75. package/dist/delivery-outbox.js.map +1 -1
  76. package/dist/discord-permissions.d.ts +33 -0
  77. package/dist/discord-permissions.js +42 -0
  78. package/dist/discord-permissions.js.map +1 -0
  79. package/dist/env-file.d.ts +11 -0
  80. package/dist/env-file.js +27 -0
  81. package/dist/env-file.js.map +1 -0
  82. package/dist/event-log.d.ts +4 -1
  83. package/dist/event-log.js +6 -3
  84. package/dist/event-log.js.map +1 -1
  85. package/dist/fleet-context.d.ts +5 -0
  86. package/dist/fleet-context.js.map +1 -1
  87. package/dist/fleet-control-audit.d.ts +5 -0
  88. package/dist/fleet-control-audit.js +7 -0
  89. package/dist/fleet-control-audit.js.map +1 -1
  90. package/dist/fleet-level-config.d.ts +8 -1
  91. package/dist/fleet-level-config.js +53 -0
  92. package/dist/fleet-level-config.js.map +1 -1
  93. package/dist/fleet-manager.d.ts +122 -7
  94. package/dist/fleet-manager.js +871 -152
  95. package/dist/fleet-manager.js.map +1 -1
  96. package/dist/fleet-stop-budget.d.ts +22 -0
  97. package/dist/fleet-stop-budget.js +95 -0
  98. package/dist/fleet-stop-budget.js.map +1 -0
  99. package/dist/full-restart.js +2 -3
  100. package/dist/full-restart.js.map +1 -1
  101. package/dist/general-knowledge/skills/persona-emoji/SKILL.md +3 -1
  102. package/dist/install-lock.d.ts +49 -0
  103. package/dist/install-lock.js +200 -0
  104. package/dist/install-lock.js.map +1 -0
  105. package/dist/instance-config-impact.js +3 -2
  106. package/dist/instance-config-impact.js.map +1 -1
  107. package/dist/kiro-transcript-lane.d.ts +8 -4
  108. package/dist/kiro-transcript-lane.js +66 -44
  109. package/dist/kiro-transcript-lane.js.map +1 -1
  110. package/dist/locale.js +96 -0
  111. package/dist/locale.js.map +1 -1
  112. package/dist/next-steps.d.ts +5 -0
  113. package/dist/next-steps.js +26 -0
  114. package/dist/next-steps.js.map +1 -0
  115. package/dist/outbound-handlers.js +8 -0
  116. package/dist/outbound-handlers.js.map +1 -1
  117. package/dist/outbound-schemas.d.ts +4 -0
  118. package/dist/outbound-schemas.js +7 -2
  119. package/dist/outbound-schemas.js.map +1 -1
  120. package/dist/outbox-open-error.d.ts +7 -0
  121. package/dist/outbox-open-error.js +15 -0
  122. package/dist/outbox-open-error.js.map +1 -0
  123. package/dist/package-preimage.d.ts +26 -0
  124. package/dist/package-preimage.js +96 -0
  125. package/dist/package-preimage.js.map +1 -0
  126. package/dist/pane-context-cache.d.ts +25 -0
  127. package/dist/pane-context-cache.js +73 -0
  128. package/dist/pane-context-cache.js.map +1 -0
  129. package/dist/paths.d.ts +0 -7
  130. package/dist/paths.js +8 -2
  131. package/dist/paths.js.map +1 -1
  132. package/dist/provider-probe.d.ts +15 -0
  133. package/dist/provider-probe.js +20 -0
  134. package/dist/provider-probe.js.map +1 -1
  135. package/dist/public-link-progress.d.ts +73 -0
  136. package/dist/public-link-progress.js +190 -0
  137. package/dist/public-link-progress.js.map +1 -0
  138. package/dist/public-web-gateway.js +2 -2
  139. package/dist/public-web-gateway.js.map +1 -1
  140. package/dist/public-web-link.d.ts +6 -1
  141. package/dist/public-web-link.js +87 -15
  142. package/dist/public-web-link.js.map +1 -1
  143. package/dist/quickstart-api.d.ts +93 -20
  144. package/dist/quickstart-api.js +194 -62
  145. package/dist/quickstart-api.js.map +1 -1
  146. package/dist/quickstart.d.ts +15 -0
  147. package/dist/quickstart.js +110 -33
  148. package/dist/quickstart.js.map +1 -1
  149. package/dist/reply-buttons-controller.d.ts +98 -0
  150. package/dist/reply-buttons-controller.js +174 -0
  151. package/dist/reply-buttons-controller.js.map +1 -0
  152. package/dist/reply-buttons.d.ts +113 -0
  153. package/dist/reply-buttons.js +208 -0
  154. package/dist/reply-buttons.js.map +1 -0
  155. package/dist/reply-dedup.js +3 -1
  156. package/dist/reply-dedup.js.map +1 -1
  157. package/dist/restart-guard.d.ts +53 -0
  158. package/dist/restart-guard.js +114 -0
  159. package/dist/restart-guard.js.map +1 -0
  160. package/dist/scheduler/db.js +6 -2
  161. package/dist/scheduler/db.js.map +1 -1
  162. package/dist/service-activation.d.ts +62 -7
  163. package/dist/service-activation.js +237 -55
  164. package/dist/service-activation.js.map +1 -1
  165. package/dist/service-installer.d.ts +56 -1
  166. package/dist/service-installer.js +152 -12
  167. package/dist/service-installer.js.map +1 -1
  168. package/dist/service-plan.d.ts +65 -0
  169. package/dist/service-plan.js +108 -0
  170. package/dist/service-plan.js.map +1 -0
  171. package/dist/settings-api.js +92 -10
  172. package/dist/settings-api.js.map +1 -1
  173. package/dist/settings-authority.d.ts +11 -0
  174. package/dist/settings-authority.js +128 -0
  175. package/dist/settings-authority.js.map +1 -0
  176. package/dist/settings-change.d.ts +5 -0
  177. package/dist/settings-change.js +37 -8
  178. package/dist/settings-change.js.map +1 -1
  179. package/dist/settings-confirmation.d.ts +3 -0
  180. package/dist/settings-confirmation.js +7 -1
  181. package/dist/settings-confirmation.js.map +1 -1
  182. package/dist/settings-effect.js +19 -7
  183. package/dist/settings-effect.js.map +1 -1
  184. package/dist/settings-http-confirmation.js +1 -1
  185. package/dist/settings-http-confirmation.js.map +1 -1
  186. package/dist/settings-redaction.d.ts +18 -0
  187. package/dist/settings-redaction.js +58 -0
  188. package/dist/settings-redaction.js.map +1 -0
  189. package/dist/setup-form.d.ts +9 -1
  190. package/dist/setup-form.js +25 -1
  191. package/dist/setup-form.js.map +1 -1
  192. package/dist/setup-host.js +15 -6
  193. package/dist/setup-host.js.map +1 -1
  194. package/dist/setup-wizard.js +27 -19
  195. package/dist/setup-wizard.js.map +1 -1
  196. package/dist/sqlite-open-errors.d.ts +12 -0
  197. package/dist/sqlite-open-errors.js +13 -0
  198. package/dist/sqlite-open-errors.js.map +1 -0
  199. package/dist/systemd-runtime.d.ts +15 -0
  200. package/dist/systemd-runtime.js +41 -0
  201. package/dist/systemd-runtime.js.map +1 -0
  202. package/dist/tmux-control.d.ts +44 -0
  203. package/dist/tmux-control.js +170 -21
  204. package/dist/tmux-control.js.map +1 -1
  205. package/dist/token-env-name.d.ts +37 -0
  206. package/dist/token-env-name.js +93 -0
  207. package/dist/token-env-name.js.map +1 -0
  208. package/dist/tool-permissions.js +5 -3
  209. package/dist/tool-permissions.js.map +1 -1
  210. package/dist/topic-commands.d.ts +7 -16
  211. package/dist/topic-commands.js +56 -85
  212. package/dist/topic-commands.js.map +1 -1
  213. package/dist/transcript-monitor.js +3 -0
  214. package/dist/transcript-monitor.js.map +1 -1
  215. package/dist/transcript-sources.d.ts +68 -0
  216. package/dist/transcript-sources.js +336 -29
  217. package/dist/transcript-sources.js.map +1 -1
  218. package/dist/transcript-turns.d.ts +39 -0
  219. package/dist/transcript-turns.js +58 -0
  220. package/dist/transcript-turns.js.map +1 -0
  221. package/dist/tunnel/cloudflared-install.d.ts +18 -0
  222. package/dist/tunnel/cloudflared-install.js +15 -1
  223. package/dist/tunnel/cloudflared-install.js.map +1 -1
  224. package/dist/turn-reply-guard.d.ts +3 -0
  225. package/dist/turn-reply-guard.js.map +1 -1
  226. package/dist/ui/chat-commands.js +43 -0
  227. package/dist/ui/chat-render.js +34 -8
  228. package/dist/ui/chat-store.js +51 -2
  229. package/dist/ui/chat-strings.js +30 -2
  230. package/dist/ui/chat-thread.js +141 -47
  231. package/dist/ui/first-run.js +64 -0
  232. package/dist/ui/fleet-cache.js +168 -0
  233. package/dist/ui/html-attachment.js +26 -0
  234. package/dist/ui/panel-chat.js +117 -23
  235. package/dist/ui/panel-fleet.js +31 -17
  236. package/dist/ui/panel-settings.js +29 -15
  237. package/dist/ui/preview-panel.js +237 -0
  238. package/dist/ui/preview.js +7 -3
  239. package/dist/ui/settings-apply.js +3 -1
  240. package/dist/ui/settings-dialogs.js +95 -25
  241. package/dist/ui/settings-discord.js +130 -0
  242. package/dist/ui/settings-model.js +21 -0
  243. package/dist/ui/settings-strings.js +50 -8
  244. package/dist/ui/settings-token.js +54 -0
  245. package/dist/ui/settings-wizard.js +63 -29
  246. package/dist/ui/shared/app-route.js +1 -1
  247. package/dist/ui/shared/app-shell.js +25 -37
  248. package/dist/ui/shared/app-store.js +5 -3
  249. package/dist/ui/shared/app-stream.js +2 -1
  250. package/dist/ui/shared/app.css +181 -5
  251. package/dist/ui/shared/app.js +2 -1
  252. package/dist/ui/shared/instance-nav.js +310 -0
  253. package/dist/ui/shared/panel-view.js +12 -190
  254. package/dist/ui/shared/signin.js +13 -1
  255. package/dist/ui/shared/ui-icons.js +2 -0
  256. package/dist/ui/shared/view-roster-store.js +5 -0
  257. package/dist/ui/shared/view-strings.js +12 -0
  258. package/dist/ui/signin.html +2 -1
  259. package/dist/update-dispatch.d.ts +28 -0
  260. package/dist/update-dispatch.js +73 -0
  261. package/dist/update-dispatch.js.map +1 -0
  262. package/dist/update-install.d.ts +78 -8
  263. package/dist/update-install.js +158 -30
  264. package/dist/update-install.js.map +1 -1
  265. package/dist/update-launch.d.ts +22 -0
  266. package/dist/update-launch.js +106 -0
  267. package/dist/update-launch.js.map +1 -0
  268. package/dist/update-marker.js +3 -8
  269. package/dist/update-marker.js.map +1 -1
  270. package/dist/usage/format-rich.js +7 -1
  271. package/dist/usage/format-rich.js.map +1 -1
  272. package/dist/usage/i18n-keys.d.ts +1 -1
  273. package/dist/usage/i18n-keys.js +1 -1
  274. package/dist/usage/i18n-keys.js.map +1 -1
  275. package/dist/usage/providers.d.ts +4 -0
  276. package/dist/usage/providers.js +2 -1
  277. package/dist/usage/providers.js.map +1 -1
  278. package/dist/usage/usage-api.js +48 -2
  279. package/dist/usage/usage-api.js.map +1 -1
  280. package/dist/view-api.d.ts +40 -0
  281. package/dist/view-api.js +28 -10
  282. package/dist/view-api.js.map +1 -1
  283. package/dist/web-api.d.ts +22 -0
  284. package/dist/web-api.js +115 -16
  285. package/dist/web-api.js.map +1 -1
  286. package/dist/web-auth.d.ts +8 -0
  287. package/dist/web-auth.js +34 -8
  288. package/dist/web-auth.js.map +1 -1
  289. package/dist/web-chat-history.d.ts +21 -0
  290. package/dist/web-chat-history.js +44 -0
  291. package/dist/web-chat-history.js.map +1 -1
  292. package/dist/web-commands.d.ts +77 -0
  293. package/dist/web-commands.js +81 -0
  294. package/dist/web-commands.js.map +1 -0
  295. package/dist/web-host-guard.d.ts +18 -2
  296. package/dist/web-host-guard.js +30 -5
  297. package/dist/web-host-guard.js.map +1 -1
  298. package/dist/web-session.d.ts +7 -1
  299. package/dist/web-session.js +23 -2
  300. package/dist/web-session.js.map +1 -1
  301. package/dist/web-shell-routes.d.ts +1 -1
  302. package/dist/web-shell-routes.js +1 -1
  303. package/dist/web-shell-routes.js.map +1 -1
  304. package/launcher/agend +110 -0
  305. package/launcher/agend-agent +110 -0
  306. package/launcher/agend-agent.cjs +4 -0
  307. package/launcher/agend.cjs +4 -0
  308. package/launcher/install-admission.cjs +105 -0
  309. package/launcher/launch.cjs +64 -0
  310. package/launcher/postinstall.cjs +118 -0
  311. package/launcher/run-in-process.cjs +13 -0
  312. package/launcher/runtime-platform.cjs +86 -0
  313. package/launcher/runtime-select.cjs +217 -0
  314. package/package.json +12 -4
  315. package/scripts/preinstall-guard.cjs +40 -21
  316. package/templates/launchd.plist.ejs +2 -2
  317. package/templates/systemd.service.ejs +4 -4
@@ -62,7 +62,7 @@ register("settings", {
62
62
  se_primaryServer: "primary",
63
63
  se_externalEmojiHint: "Another server's emoji needs the bot's Use External Emojis permission in the channel it reacts in.",
64
64
  se_unavailable: "unavailable",
65
- se_hint: "Empty keeps the default. The preview is resolved by AgEnD exactly as the bot will react.",
65
+ se_hint: "Empty keeps the default. The preview is resolved by AgEnD exactly as the bot will react. Progress prefix is not a reaction: it starts the progress message.",
66
66
  detection: "Detection",
67
67
  startup: "Startup",
68
68
  backendSection: "Backend",
@@ -100,7 +100,18 @@ register("settings", {
100
100
  tokenEnv: "Bot token env var",
101
101
  groupId: "Group / guild id",
102
102
  nameField: "Name",
103
- envHint: "Put the actual token in ~/.agend/.env under this env var, then Reload Fleet.",
103
+ tokenShow: "Show the token", tokenHide: "Hide the token", tokenIsBot: "This is {0}.", tokenStatus: "Bot token",
104
+ tokenSet: "Token set", tokenSetAs: "Token set · {0}", tokenMissing: "Token missing", tokenReplace: "Replace",
105
+ tokenStoredAs: "The token is stored on this computer in ~/.agend/.env under this name — generated, and only needed if you manage .env yourself.",
106
+ tokenSavedRestart: "Token saved. This connection is not running, so it starts with the new token when AgEnD restarts (on the host: agend restart).",
107
+ newBotTokenHint: "Paste the bot's token. It is checked with the platform, then stored on this computer — never in the browser.",
108
+ connectionIdOptional: "Connection id (optional)",
109
+ connConnectedAs: "Connected · {0}", connReconnecting: "Reconnecting…", connStarting: "Starting…", connFailed: "Could not connect",
110
+ connNotRunning: "Not running", connTokenMissing: "Token missing", connRejected: "Token rejected — replace it",
111
+ discordInvite: "Invite {0} to your server", discordTheBot: "the bot", discordWaitingJoin: "Waiting for the bot to join a server…",
112
+ discordCheckAgain: "Check again", discordNoServerYet: "Invite the bot first, or type the server id",
113
+ discordIntentNote: "Turn on Message Content Intent for this bot in the Discord developer portal (it cannot be set from here):", discordPortalBot: "Developer portal → Bot",
114
+ discordMissingIntent: "Turn on Message Content Intent in the Discord developer portal → Bot, then restart AgEnD.",
104
115
  noAgents: "No agents yet — click + New.",
105
116
  noBots: "No bots configured.",
106
117
  currentVersion: "Current version",
@@ -123,7 +134,7 @@ register("settings", {
123
134
  wizardButton: "Setup wizard",
124
135
  wizardTitle: "Set up AgEnD",
125
136
  wizardStep: "Step {0} of {1}",
126
- wizardRerun: "A fleet is already configured — finishing replaces the connection that uses the same token variable.",
137
+ wizardRerun: "A fleet is already configured — finishing adds a new connection; nothing already there is replaced.",
127
138
  wizardBack: "Back",
128
139
  wizardNext: "Next",
129
140
  wizardFinish: "Create and start",
@@ -131,6 +142,14 @@ register("settings", {
131
142
  wizardBackendFound: "Found on this host: {0}",
132
143
  wizardBackendNone: "No backend CLI found on this host — install one first.",
133
144
  wizardAgentName: "First agent name",
145
+ wizardAgentWhich: "Which agent answers on this bot",
146
+ wizardAgentNew: "A new agent",
147
+ wizardAgentExisting: "An agent you already have",
148
+ wizardAgentPick: "Agent",
149
+ wizardAgentExistingNote: "It keeps its directory, backend and settings; only its chat connection becomes this bot (if it has one now, it moves here).",
150
+ wizardAgentNewName: "New agent name",
151
+ wizardTopicNotCarried: "Its topic ({0}) is in the chat of its old connection, so it is not carried over: in the new chat it has no topic until one is bound.",
152
+ wizardTopicGeneral: "It is the General agent: in the new chat it takes that chat's General topic.",
134
153
  wizardNeedDir: "Enter an absolute working directory.",
135
154
  wizardTelegramHint: "Create a bot with @BotFather, then add it to a group.",
136
155
  wizardDiscordHint: "Create an application in the Discord developer portal and invite the bot to your server.",
@@ -165,7 +184,7 @@ register("settings", {
165
184
  restartFleetConfirm: "Restart AgEnD now?\n\nEvery agent stops and starts again, work in progress is interrupted, and the panel is unreachable for up to a minute. A notice is posted to your chat channel.",
166
185
  restartFleetStarted: "Restarting AgEnD — this page keeps checking.",
167
186
  restartFleetBusy: "Restarting…",
168
- restartFleetTitle: "Restarting AgEnD…",
187
+ restartFleetTitle: "Restarting AgEnD…", connStartingNow: "Starting the new connection — no restart needed…",
169
188
  restartFleetWaiting: "AgEnD is restarting. This page keeps checking and will say when it is back — there is nothing to press.",
170
189
  working: "Working…",
171
190
  restartFleetRefused: "Restart refused: {0}",
@@ -217,6 +236,7 @@ register("settings", {
217
236
  countAgents: "{0} agents",
218
237
  countBots: "{0} connections",
219
238
  countRooms: "{0} rooms",
239
+ countAgent: "1 agent", countBot: "1 connection", countRoom: "1 room",
220
240
  newAgent: "New agent",
221
241
  newBot: "New connection",
222
242
  effortTag: "effort: {0}",
@@ -309,6 +329,7 @@ register("settings", {
309
329
  editMode: "Edit",
310
330
  viewMode: "View",
311
331
  copy: "Copy",
332
+ devDownload: "Download fleet.yaml",
312
333
  devEditor: "fleet.yaml editor",
313
334
  devApply: "Apply & save",
314
335
  devApplyHint: "Writes these settings and applies them, like Apply changes.",
@@ -351,7 +372,7 @@ register("settings", {
351
372
  se_primaryServer: "主伺服器",
352
373
  se_externalEmojiHint: "使用其他伺服器的 emoji,bot 在要加反應的頻道需有「使用外部表情符號」權限。",
353
374
  se_unavailable: "無法使用",
354
- se_hint: "留空即使用預設值。預覽由 AgEnD 依 bot 實際 react 的方式解析。",
375
+ se_hint: "留空即使用預設值。預覽由 AgEnD 依 bot 實際 react 的方式解析。進度前綴不是反應,而是進度訊息開頭的 emoji。",
355
376
  title: "設定",
356
377
  myAgents: "我的 Agent",
357
378
  bots: "機器人",
@@ -423,7 +444,18 @@ register("settings", {
423
444
  tokenEnv: "Bot token 環境變數",
424
445
  groupId: "群組 / guild id",
425
446
  nameField: "名稱",
426
- envHint: "把實際 token 放到 ~/.agend/.env 的這個環境變數下,然後點重新載入。",
447
+ tokenShow: "顯示 token", tokenHide: "隱藏 token", tokenIsBot: "這是 {0}。", tokenStatus: "Bot token",
448
+ tokenSet: "已設定 token", tokenSetAs: "已設定 token · {0}", tokenMissing: "缺少 token", tokenReplace: "更換",
449
+ tokenStoredAs: "token 存在這台電腦的 ~/.agend/.env,用這個名稱——自動產生,只有自行管理 .env 時才需要。",
450
+ tokenSavedRestart: "已儲存 token。這個連線目前沒有在執行,AgEnD 重新啟動時會用新的 token 啟動(在主機上:agend restart)。",
451
+ newBotTokenHint: "貼上 bot 的 token。會先向平台確認,再存到這台電腦——絕不存在瀏覽器。",
452
+ connectionIdOptional: "連線 id(選填)",
453
+ connConnectedAs: "已連線 · {0}", connReconnecting: "重新連線中…", connStarting: "啟動中…", connFailed: "無法連線",
454
+ connNotRunning: "未執行", connTokenMissing: "缺少 token", connRejected: "token 被拒——請更換",
455
+ discordInvite: "邀請 {0} 加入你的伺服器", discordTheBot: "bot", discordWaitingJoin: "等待 bot 加入伺服器…",
456
+ discordCheckAgain: "再檢查一次", discordNoServerYet: "請先邀請 bot,或輸入伺服器 id",
457
+ discordIntentNote: "請在 Discord 開發者入口網站為這個 bot 開啟 Message Content Intent(這裡無法代為設定):", discordPortalBot: "開發者入口網站 → Bot",
458
+ discordMissingIntent: "請到 Discord 開發者入口網站 → Bot 開啟 Message Content Intent,然後重新啟動 AgEnD。",
427
459
  noAgents: "還沒有 agent — 點 + 新增。",
428
460
  noBots: "尚未設定機器人。",
429
461
  currentVersion: "目前版本",
@@ -446,7 +478,7 @@ register("settings", {
446
478
  wizardButton: "設定精靈",
447
479
  wizardTitle: "設定 AgEnD",
448
480
  wizardStep: "第 {0} 步,共 {1} 步",
449
- wizardRerun: "已經有設定好的 fleet —— 完成後會取代使用相同 token 變數的那個連線。",
481
+ wizardRerun: "已經有設定好的 fleet —— 完成後會新增一個連線,不會取代任何既有的設定。",
450
482
  wizardBack: "上一步",
451
483
  wizardNext: "下一步",
452
484
  wizardFinish: "建立並啟動",
@@ -454,6 +486,14 @@ register("settings", {
454
486
  wizardBackendFound: "在這台主機上找到:{0}",
455
487
  wizardBackendNone: "這台主機上找不到任何後端 CLI —— 請先安裝一個。",
456
488
  wizardAgentName: "第一個 agent 名稱",
489
+ wizardAgentWhich: "由哪個 agent 在這個機器人上回覆",
490
+ wizardAgentNew: "新的 agent",
491
+ wizardAgentExisting: "既有的 agent",
492
+ wizardAgentPick: "Agent",
493
+ wizardAgentExistingNote: "它的目錄、backend 和設定都不變;只有聊天連線改成這個機器人(如果它目前已有連線,會移到這裡)。",
494
+ wizardAgentNewName: "新 agent 名稱",
495
+ wizardTopicNotCarried: "它的 topic({0})屬於舊連線的聊天室,不會帶過去:在新的聊天室裡,綁定新的 topic 之前它沒有 topic。",
496
+ wizardTopicGeneral: "它是 General agent:在新的聊天室裡會改用那裡的 General topic。",
457
497
  wizardNeedDir: "請輸入絕對路徑的工作目錄。",
458
498
  wizardTelegramHint: "用 @BotFather 建立 bot,然後把它加進一個群組。",
459
499
  wizardDiscordHint: "在 Discord 開發者後台建立應用程式,並把 bot 邀請到你的伺服器。",
@@ -488,7 +528,7 @@ register("settings", {
488
528
  restartFleetConfirm: "現在重新啟動 AgEnD?\n\n所有 agent 都會停止再啟動,進行中的工作會被中斷,面板會有最多約一分鐘無法連線。聊天頻道會收到一則通知。",
489
529
  restartFleetStarted: "AgEnD 重新啟動中 —— 此頁面會持續確認。",
490
530
  restartFleetBusy: "重啟中…",
491
- restartFleetTitle: "AgEnD 重新啟動中…",
531
+ restartFleetTitle: "AgEnD 重新啟動中…", connStartingNow: "正在啟動新連線——不需要重新啟動…",
492
532
  restartFleetWaiting: "AgEnD 正在重新啟動。此頁面會持續確認,恢復後會告訴你 —— 不需要再按任何按鈕。",
493
533
  working: "執行中…",
494
534
  restartFleetRefused: "重新啟動被拒絕:{0}",
@@ -540,6 +580,7 @@ register("settings", {
540
580
  countAgents: "{0} 個 agent",
541
581
  countBots: "{0} 個連線",
542
582
  countRooms: "{0} 個房間",
583
+ countAgent: "1 個 agent", countBot: "1 個連線", countRoom: "1 個房間",
543
584
  newAgent: "新增 agent",
544
585
  newBot: "新增連線",
545
586
  effortTag: "effort:{0}",
@@ -632,6 +673,7 @@ register("settings", {
632
673
  editMode: "編輯",
633
674
  viewMode: "檢視",
634
675
  copy: "複製",
676
+ devDownload: "下載 fleet.yaml",
635
677
  devEditor: "fleet.yaml 編輯器",
636
678
  devApply: "套用並儲存",
637
679
  devApplyHint: "寫入這些設定並套用,和「套用變更」相同。",
@@ -0,0 +1,54 @@
1
+ // #1519 P1 (docs/design/ux-onboarding-walkthrough.md §5.2): the one bot-token field — the setup wizard, "New
2
+ // connection" and a connection's "Replace token" use it. A password field (show/hide), Verify asks the platform who the
3
+ // token belongs to (Telegram getMe, Discord /users/@me) and shows the bot's name before anything is written. The token
4
+ // lives in this field's state only: never in storage, a URL, a label or the page's other state, and the server never
5
+ // hands it back — afterwards a connection shows "Token set · @bot".
6
+ import { html, useState } from "/assets/app-html.js";
7
+ import { t } from "/assets/app-i18n.js";
8
+ import { Icon } from "/assets/ui-icons.js";
9
+ import { api } from "./settings-confirm.js";
10
+
11
+ const tn = (k, ...v) => t(`settings.${k}`, ...v);
12
+
13
+ /** Who this token is: { valid, username?, reason? } — one outbound check, the token sent once and not kept. */
14
+ export async function verifyBotToken(platform, token) {
15
+ const res = await api("/api/settings/quickstart/probe", { method: "POST", body: JSON.stringify({ action: "verify", platform, token }) })
16
+ .catch(() => ({ ok: false, body: {} }));
17
+ // A Discord bot's invite and portal page come with it (#1519 P3).
18
+ if (res.ok && res.body && res.body.identity) return { ...res.body.identity, invite: res.body.invite_url || null, portal: res.body.portal_url || null };
19
+ return { valid: false, reason: (res.body && res.body.error) || tn("verifyFailed") };
20
+ }
21
+
22
+ /** A bot's name as it is addressed: @name, once. */
23
+ export const botHandle = (username) => (username ? `@${String(username).replace(/^@/, "")}` : "");
24
+
25
+ /**
26
+ * The field. props: id, platform, value, onInput(token) (the parent drops its verified identity on a change and moves
27
+ * its credential revision, so a Verify still on its way for the old value cannot land — #1529 review),
28
+ * identity ({ valid, username, reason } | null), busy (the field is locked meanwhile), onVerify(), hint.
29
+ */
30
+ export function TokenField({ id, platform, value, onInput, identity, busy, onVerify, hint }) {
31
+ const [shown, setShown] = useState(false);
32
+ return html`<div class="field token-field"><label for=${id}>${tn("wizardToken")}</label>
33
+ <div class="token-row">
34
+ <input id=${id} type=${shown ? "text" : "password"} autocomplete="off" autocapitalize="off" spellcheck="false" disabled=${!!busy}
35
+ placeholder=${platform === "discord" ? "MTA…" : "123456:ABC-DEF…"} value=${value} onInput=${(e) => onInput(e.target.value.trim())} />
36
+ <button type="button" class="icon-btn" aria-pressed=${shown ? "true" : "false"} title=${shown ? tn("tokenHide") : tn("tokenShow")}
37
+ aria-label=${shown ? tn("tokenHide") : tn("tokenShow")} onClick=${() => setShown(!shown)}><${Icon} name="eye" size=${16} /></button>
38
+ </div>
39
+ <p class="note">${hint || (platform === "telegram" ? tn("wizardTokenTelegram") : tn("wizardTokenDiscord"))}</p>
40
+ <div class="dlg-inline-actions"><button type="button" class="btn btn-sm" disabled=${busy || !value} onClick=${onVerify}>${tn("wizardVerify")}</button>
41
+ ${identity ? html`<span class=${`feedback${identity.valid ? " ok" : " error"}`} role="status">${identity.valid
42
+ ? tn("tokenIsBot", botHandle(identity.username) || tn("verified")) : (identity.reason || tn("verifyFailed"))}</span>` : null}</div>
43
+ </div>`;
44
+ }
45
+
46
+ /** The generated env var a token is stored under, read-only with a copy button (the "Advanced" view). */
47
+ export function TokenEnvNote({ name }) {
48
+ const [copied, setCopied] = useState(false);
49
+ if (!name) return null;
50
+ const copy = async () => { try { await navigator.clipboard.writeText(name); setCopied(true); } catch { /* the name is on screen */ } };
51
+ return html`<div class="field"><span>${tn("tokenEnv")}</span>
52
+ <div class="dlg-inline-actions"><code>${name}</code><button type="button" class="btn btn-sm btn-ghost" onClick=${copy}>${copied ? tn("copied") : tn("copy")}</button></div>
53
+ <p class="note">${tn("tokenStoredAs")}</p></div>`;
54
+ }
@@ -2,14 +2,16 @@
2
2
  // configuration and, when it finishes, writes the files (a write an admin may have to confirm, #1423) and hands over
3
3
  // to the ordinary Apply: the app's runner (settings-apply.js) posts the apply and watches its job, with the same
4
4
  // progress, deadline and restart recovery as every other change.
5
- import { html, useEffect, useState } from "/assets/app-html.js";
5
+ import { html, useEffect, useRef, useState } from "/assets/app-html.js";
6
6
  import { t } from "/assets/app-i18n.js";
7
7
  import { useLease } from "/assets/app-ctx.js";
8
8
  import { Dialog } from "/assets/ui-dialog.js";
9
9
  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
- import { Select } from "./settings-dialogs.js";
12
+ import { Drawer, Select } from "./settings-dialogs.js";
13
+ import { botHandle, TokenEnvNote, TokenField, verifyBotToken } from "./settings-token.js";
14
+ import { DiscordServerPicker } from "./settings-discord.js";
13
15
 
14
16
  const tn = (k, ...v) => t(`settings.${k}`, ...v);
15
17
  const STEPS = 4;
@@ -18,6 +20,10 @@ const post = (path, body) => api(path, { method: "POST", body: JSON.stringify(bo
18
20
  export function SetupWizard({ ctx, onClose }) {
19
21
  const lease = useLease("wizard");
20
22
  const [w, setW] = useState(null);
23
+ // The credential's revision (#1529 review): a token or platform change moves it, and a Verify, Detect or plan that
24
+ // began before is dropped when it lands — an answer about an old token never names, or plans for, a new one.
25
+ const rev = useRef(0);
26
+ const bump = () => { rev.current++; };
21
27
  const [busy, setBusy] = useState("");
22
28
  const [err, setErr] = useState("");
23
29
  useEffect(() => {
@@ -25,37 +31,51 @@ export function SetupWizard({ ctx, onClose }) {
25
31
  const env = await api("/api/settings/quickstart/environment").catch(() => null);
26
32
  if (!lease.current()) return;
27
33
  const e = (env && env.body) || { backends: [], channels: [], has_fleet: false };
28
- setW({ step: 1, env: e, backend: (e.backends || [])[0] || "claude-code", working_directory: "", instance_name: "agent-1",
29
- platform: "telegram", token: "", token_env: "AGEND_BOT_TOKEN", group_id: "", guild_id: "", general_channel_id: "", admin_user_id: "",
34
+ // #1519 P7: with agents already there, the bot is for one of them by default; a new agent gets a free name.
35
+ const agents = Array.isArray(e.agents) ? e.agents : [];
36
+ const names = new Set(agents.map((a) => a.name));
37
+ let n = 1; while (names.has(`agent-${n}`)) n++;
38
+ setW({ step: 1, env: e, backend: (e.backends || [])[0] || "claude-code", working_directory: "", instance_name: `agent-${n}`,
39
+ agent: agents.length ? "existing" : "new", existing_name: agents.length ? agents[0].name : "",
40
+ platform: "telegram", token: "", group_id: "", guild_id: "", general_channel_id: "", admin_user_id: "",
30
41
  identity: null, guilds: [], plan: null, offset: 0 });
31
42
  })();
32
43
  }, [lease]);
33
44
  if (!w) return html`<${Dialog} title=${tn("wizardTitle")} onClose=${onClose}><p class="note">…</p></${Dialog}>`;
34
- const set = (k) => (v) => setW((x) => ({ ...x, [k]: v }));
45
+ // Every field the plan or a probe is about moves the revision (#1529 review r2): a plan or Detect that began before an
46
+ // edit — target, admin, backend, directory, name — is dropped when it lands, never committed or written over the edit.
47
+ const set = (k) => (v) => { bump(); setW((x) => ({ ...x, [k]: v })); };
48
+ // No token env: the plan generates one (#1519 P1: AGEND_<PLATFORM>_<ID>_TOKEN, unique) and the commit carries the plan's.
49
+ // An existing agent (#1519 P7) is named and nothing else: the server keeps its directory and backend.
35
50
  const input = () => ({
36
- platform: w.platform, token_env: w.token_env, backend: w.backend, working_directory: w.working_directory, instance_name: w.instance_name,
51
+ platform: w.platform,
52
+ ...(w.agent === "existing" ? { instance_name: w.existing_name, existing_agent: true }
53
+ : { backend: w.backend, working_directory: w.working_directory, instance_name: w.instance_name }),
37
54
  group_id: w.group_id || undefined, guild_id: w.guild_id || undefined, general_channel_id: w.general_channel_id || undefined,
38
55
  admin_user_id: w.admin_user_id || undefined,
39
56
  });
40
57
 
41
58
  const verify = async () => {
59
+ const at = rev.current, token = w.token, platform = w.platform;
60
+ const live = () => lease.current() && rev.current === at;
42
61
  setBusy(tn("wizardVerifying")); setErr("");
43
- const res = await post("/api/settings/quickstart/probe", { action: "verify", platform: w.platform, token: w.token });
44
- if (!lease.current()) return;
45
- const identity = (res.body && res.body.identity) || { valid: false };
62
+ const identity = await verifyBotToken(platform, token);
63
+ if (!live()) { if (lease.current()) setBusy(""); return; }
46
64
  let guilds = [];
47
- if (identity.valid && w.platform === "discord") {
48
- const g = await post("/api/settings/quickstart/probe", { action: "guilds", token: w.token });
49
- if (!lease.current()) return;
65
+ if (identity.valid && platform === "discord") {
66
+ const g = await post("/api/settings/quickstart/probe", { action: "guilds", token });
67
+ if (!live()) { if (lease.current()) setBusy(""); return; }
50
68
  guilds = (g.body && g.body.guilds) || [];
51
69
  }
52
70
  setBusy("");
53
- setW((x) => ({ ...x, identity, guilds }));
71
+ setW((x) => (x.token === token && x.platform === platform ? { ...x, identity, guilds } : x));
54
72
  };
55
73
  const detect = async () => {
74
+ const at = rev.current;
56
75
  setBusy(tn("wizardWaitingStart")); setErr("");
57
76
  const res = await post("/api/settings/quickstart/probe", { action: "await-telegram-start", token: w.token, offset: w.offset || 0 });
58
77
  if (!lease.current()) return;
78
+ if (rev.current !== at) { setBusy(""); return; }
59
79
  setBusy("");
60
80
  if (!res.ok) { setErr((res.body && res.body.error) || tn("applyFailed")); return; }
61
81
  if (res.body.found) setW((x) => ({ ...x, offset: res.body.offset, group_id: String(res.body.found.groupId), admin_user_id: String(res.body.found.userId) }));
@@ -63,14 +83,19 @@ export function SetupWizard({ ctx, onClose }) {
63
83
  };
64
84
  const next = async () => {
65
85
  setErr("");
66
- if (w.step === 1) { if (!w.working_directory.startsWith("/")) { setErr(tn("wizardNeedDir")); return; } setW({ ...w, step: 2 }); return; }
86
+ if (w.step === 1) {
87
+ if (w.agent !== "existing" && !w.working_directory.startsWith("/")) { setErr(tn("wizardNeedDir")); return; }
88
+ setW({ ...w, step: 2 }); return;
89
+ }
67
90
  if (w.step === 2) { setW({ ...w, step: 3 }); return; }
68
91
  if (w.step === 3) {
69
92
  if (!w.identity || !w.identity.valid) { setErr(tn("wizardNeedVerify")); return; }
93
+ const at = rev.current;
70
94
  setBusy(tn("wizardPlanLoading"));
71
95
  const res = await post("/api/settings/quickstart/plan", input());
72
96
  if (!lease.current()) return;
73
97
  setBusy("");
98
+ if (rev.current !== at) return;
74
99
  if (!res.ok) { setErr((res.body && res.body.error) || tn("applyFailed")); return; }
75
100
  setW((x) => ({ ...x, plan: res.body, step: 4 }));
76
101
  return;
@@ -84,7 +109,9 @@ export function SetupWizard({ ctx, onClose }) {
84
109
  * commit's body until that write completes, then drops it.
85
110
  */
86
111
  const finish = () => {
87
- const body = { ...input(), token: w.token };
112
+ // The plan's target, all of it (#1529 review): its connection id and its generated token env — the commit refuses
113
+ // either if it was taken since, instead of quietly picking another.
114
+ const body = { ...input(), channel_id: w.plan.channel_id, token_env: w.plan.token_env, token_env_generated: true, token: w.token };
88
115
  // It writes the connection and the first agent: it costs what a connection change does (the server's schema).
89
116
  const handed = startOperation([{ label: tn("wizardTitle"), impact: impactOf((ctx && ctx.schema) || DEFAULT_SCHEMA, "fleet.channels"),
90
117
  request: { method: "POST", url: "/api/settings/quickstart/commit", body, sensitive: true } }]);
@@ -94,28 +121,31 @@ export function SetupWizard({ ctx, onClose }) {
94
121
  };
95
122
 
96
123
  let body;
124
+ const agents = Array.isArray(w.env.agents) ? w.env.agents : [];
97
125
  if (w.step === 1) {
98
- body = html`<div class="field"><label for="wz-be">${tn("wizardBackend")}</label>
126
+ const which = agents.length ? html`<div class="seg-inline" role="group" aria-label=${tn("wizardAgentWhich")}>
127
+ ${["existing", "new"].map((k) => html`<button key=${k} type="button" class=${`btn${w.agent === k ? " btn-primary" : ""}`} aria-pressed=${w.agent === k ? "true" : "false"}
128
+ onClick=${() => set("agent")(k)}>${k === "existing" ? tn("wizardAgentExisting") : tn("wizardAgentNew")}</button>`)}</div>` : null;
129
+ body = w.agent === "existing" ? html`${which}
130
+ <div class="field"><label for="wz-agent">${tn("wizardAgentPick")}</label>
131
+ <${Select} id="wz-agent" value=${w.existing_name} onChange=${set("existing_name")} options=${agents.map((a) => a.name)} />
132
+ <p class="note">${tn("wizardAgentExistingNote")}</p></div>` : html`${which}<div class="field"><label for="wz-be">${tn("wizardBackend")}</label>
99
133
  <${Select} id="wz-be" value=${w.backend} onChange=${set("backend")} options=${w.env.backends && w.env.backends.length ? w.env.backends : BACKENDS} />
100
134
  <p class="note">${w.env.backends && w.env.backends.length ? tn("wizardBackendFound", w.env.backends.join(", ")) : tn("wizardBackendNone")}</p></div>
101
135
  <div class="field"><label for="wz-wd">${tn("workingDir")}</label><input id="wz-wd" type="text" placeholder="/home/you/projects/app" value=${w.working_directory} onInput=${(e) => set("working_directory")(e.target.value.trim())} /></div>
102
- <div class="field"><label for="wz-name">${tn("wizardAgentName")}</label><input id="wz-name" type="text" value=${w.instance_name} onInput=${(e) => set("instance_name")(e.target.value.trim())} /></div>`;
136
+ <div class="field"><label for="wz-name">${agents.length ? tn("wizardAgentNewName") : tn("wizardAgentName")}</label><input id="wz-name" type="text" value=${w.instance_name} onInput=${(e) => set("instance_name")(e.target.value.trim())} /></div>`;
103
137
  } else if (w.step === 2) {
104
138
  body = html`<div class="seg-inline" role="group" aria-label=${tn("wizardPlatform")}>
105
139
  ${["telegram", "discord"].map((p) => html`<button key=${p} type="button" class=${`btn${w.platform === p ? " btn-primary" : ""}`} aria-pressed=${w.platform === p ? "true" : "false"}
106
- onClick=${() => setW({ ...w, platform: p, identity: null })}>${p === "telegram" ? "Telegram" : "Discord"}</button>`)}</div>
140
+ onClick=${() => { bump(); setW((x) => ({ ...x, platform: p, identity: null })); }}>${p === "telegram" ? "Telegram" : "Discord"}</button>`)}</div>
107
141
  <p class="note">${w.platform === "telegram" ? tn("wizardTelegramHint") : tn("wizardDiscordHint")}</p>`;
108
142
  } else if (w.step === 3) {
109
- body = html`<div class="field"><label for="wz-token">${tn("wizardToken")}</label>
110
- <input id="wz-token" type="password" autocomplete="new-password" placeholder="123456:ABC-DEF…" value=${w.token} onInput=${(e) => setW({ ...w, token: e.target.value.trim(), identity: null })} />
111
- <p class="note">${w.platform === "telegram" ? tn("wizardTokenTelegram") : tn("wizardTokenDiscord")}</p></div>
112
- <div class="field"><label for="wz-env">${tn("tokenEnv")}</label><input id="wz-env" type="text" value=${w.token_env} onInput=${(e) => set("token_env")(e.target.value.trim())} /></div>
113
- <div class="dlg-inline-actions"><button type="button" class="btn btn-sm" disabled=${!!busy} onClick=${verify}>${tn("wizardVerify")}</button>
114
- ${w.identity ? html`<span class=${`feedback${w.identity.valid ? "" : " error"}`} role="status">${w.identity.valid ? (w.identity.username || tn("verified")) : (w.identity.reason || tn("verifyFailed"))}</span>` : null}</div>
143
+ body = html`<${TokenField} id="wz-token" platform=${w.platform} value=${w.token} identity=${w.identity} busy=${!!busy}
144
+ onInput=${(v) => { bump(); setW((x) => ({ ...x, token: v, identity: null })); }} onVerify=${verify} />
115
145
  ${w.platform === "discord" ? html`
116
- <div class="field"><label for="wz-guild">${tn("guildIdField")}</label><${Select} id="wz-guild" value=${w.guild_id} onChange=${set("guild_id")}
117
- options=${["", ...w.guilds.map((g) => ({ value: g.id, label: `${g.name} (${g.id})` }))]} /></div>
118
- <div class="field"><label for="wz-gen">${tn("wizardGeneralChannel")}</label><input id="wz-gen" type="text" value=${w.general_channel_id} onInput=${(e) => set("general_channel_id")(e.target.value.trim())} /></div>
146
+ <${DiscordServerPicker} idPrefix="wz" token=${w.identity && w.identity.valid ? w.token : ""} bot=${w.identity && botHandle(w.identity.username)}
147
+ invite=${w.identity && w.identity.invite} portal=${w.identity && w.identity.portal} guilds=${w.guilds} onGuilds=${(g) => setW((x) => ({ ...x, guilds: g }))}
148
+ guild=${w.guild_id} onGuild=${set("guild_id")} channel=${w.general_channel_id} onChannel=${set("general_channel_id")} />
119
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>`
120
150
  : html`
121
151
  <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())} />
@@ -125,8 +155,12 @@ export function SetupWizard({ ctx, onClose }) {
125
155
  } else {
126
156
  body = !w.plan ? html`<p class="note">${tn("wizardPlanLoading")}</p>` : html`
127
157
  <p><strong>${tn("wizardWillWrite")}</strong></p>
128
- <pre class="s-yaml">${toYaml({ channels: [w.plan.channel], instances: { [w.plan.instance.name]: { working_directory: w.plan.instance.working_directory, backend: w.plan.instance.backend } } })}</pre>
129
- <p class="note">${tn("wizardEnvKeys", (w.plan.env_keys || []).join(", "))}</p>
158
+ <pre class="s-yaml">${toYaml({ channels: [w.plan.channel], instances: { [w.plan.instance.name]: { working_directory: w.plan.instance.working_directory, backend: w.plan.instance.backend, channel_id: w.plan.instance.channel_id,
159
+ ...(w.plan.instance.topic && w.plan.instance.topic.to != null ? { topic_id: w.plan.instance.topic.to } : {}) } } })}</pre>
160
+ ${w.plan.instance && w.plan.instance.existing ? html`<p class="note">${tn("wizardAgentExistingNote")}</p>` : null}
161
+ ${w.plan.instance && w.plan.instance.topic ? html`<p class="feedback warning">${w.plan.instance.topic.to == null
162
+ ? tn("wizardTopicNotCarried", w.plan.instance.topic.from) : tn("wizardTopicGeneral")}</p>` : null}
163
+ <${Drawer} title=${tn("advancedSection")}><${TokenEnvNote} name=${w.plan.token_env} /></${Drawer}>
130
164
  ${(w.plan.warnings || []).map((x, i) => html`<p key=${i} class="feedback warning">${x}</p>`)}`;
131
165
  }
132
166
  return html`<${Dialog} title=${tn("wizardTitle")} onClose=${onClose} wide=${true} busy=${busy === tn("wizardCommitting")}
@@ -1,7 +1,7 @@
1
1
  // #1408 §3: the app's routes on the client. The same table as the server's classifier (src/web-shell-routes.ts;
2
2
  // a test runs both over one list of cases). Pure: no DOM, no history.
3
3
 
4
- export const FLEET_TABS = ["tasks", "schedules", "teams", "org", "config"];
4
+ export const FLEET_TABS = ["tasks", "schedules", "teams", "org", "cache", "config"];
5
5
  export const SETTINGS_SECTIONS = ["agents", "bots", "classic", "general", "advanced"];
6
6
  const MAX_INSTANCE_NAME = 128;
7
7
  const PATH_SEPARATOR_OR_CONTROL = /[/\\\u0000-\u001f\u007f]/;
@@ -1,7 +1,7 @@
1
1
  // #1408 §2/§7: the app's frame — the sidebar (a drawer on a phone), the main area with its panel, the bottom tabs on a
2
2
  // phone, the connection line, toasts. Panels render their own header through <PanelHeader>, so every panel's title,
3
3
  // status and ⋯ actions sit in the same place.
4
- import { html, useEffect, useState } from "./app-html.js";
4
+ import { html, useEffect, useLayoutEffect, useRef, useState } from "./app-html.js";
5
5
  import { Icon } from "./ui-icons.js";
6
6
  import { Toasts } from "./ui-toast.js";
7
7
  import { ConfirmHost } from "./ui-confirm.js";
@@ -11,6 +11,7 @@ import { navStore } from "./app-nav.js";
11
11
  import { chatPath, viewPath, settingsPath, routeKey, NEEDS_PATH } from "./app-route.js";
12
12
  import { SessionMenu } from "./app-session.js";
13
13
  import { ErrorState, Skeleton } from "./ui-states.js";
14
+ import { InstanceNav, RosterNav } from "./instance-nav.js";
14
15
 
15
16
  /** Shell state shared with panels: the phone drawer, the collapsed sidebar, extensions a panel module adds. */
16
17
  export const shellStore = createStore({ drawer: false, collapsed: readCollapsed(), footer: [], keyboard: false, dialog: null, side: null });
@@ -25,6 +26,14 @@ export function setSideSection(Component) {
25
26
  return () => { if (shellStore.get().side === entry) shellStore.set({ side: null }); };
26
27
  }
27
28
 
29
+ // The sidebar's instance list (alpha.2, N1): one component on every page. keepActiveInView lives with it.
30
+ export { keepActiveInView } from "./instance-nav.js";
31
+
32
+ /** #1519 P7: "New agent" from anywhere (the sidebar's +, an empty chat): the app registers what opens it. */
33
+ let newInstanceOpener = null;
34
+ export function setNewInstanceOpener(fn) { newInstanceOpener = fn; }
35
+ export function requestNewInstance() { if (newInstanceOpener) newInstanceOpener(); }
36
+
28
37
  /** An app-level dialog (New instance): rendered by the shell until closed, whatever panel is showing. */
29
38
  export function showDialog(Component, props = {}) { shellStore.set({ dialog: { Component, props } }); }
30
39
  export function closeDialog() { shellStore.set({ dialog: null }); }
@@ -90,29 +99,6 @@ export function statusClass(i, exec, awaiting) {
90
99
  }
91
100
  export { statusLabel };
92
101
 
93
- function tooltip(i) {
94
- const src = (s) => (s === "instance" || s === "classic" ? "configured" : s === "fleet-default" ? "fleet default" : s);
95
- const model = i.model ? (i.model_source === "live" || i.model_source === "cli-default" || i.model_source === "unresolved" ? i.model : `${i.model} (${src(i.model_source)})`) : null;
96
- const effort = i.effort ? `effort:${i.effort}${i.effort_source ? ` (${src(i.effort_source)})` : ""}` : null;
97
- const ctx = i.context_pct != null ? `ctx:${Math.round(i.context_pct)}%` : null;
98
- const cost = i.cost > 0 ? `$${i.cost.toFixed(2)}` : null;
99
- return [i.name, i.backend, model, effort, ctx, cost, i.status].filter(Boolean).join(" · ");
100
- }
101
-
102
- function InstanceRow({ i, active, exec, awaiting }) {
103
- const alias = typeof i.display_name === "string" && i.display_name.trim() && i.display_name.trim() !== i.name ? i.display_name.trim() : "";
104
- const cls = statusClass(i, exec, awaiting);
105
- // Two lines: the name (never shortened to make room for a badge, #1408 rough edge 6), then the alias and/or
106
- // "needs you". Everything else is in the tooltip.
107
- return html`<li><a class=${`inst${active ? " active" : ""}`} href=${chatPath(i.name)} title=${tooltip(i)} aria-current=${active ? "page" : undefined}
108
- onClick=${closeDrawer}>
109
- <span class=${`dot ${cls}`} aria-hidden="true"></span>
110
- <span class="inst-text"><span class="inst-name">${i.name}</span>
111
- ${alias || awaiting != null ? html`<span class="inst-sub">${alias ? html`<span class="inst-alias">${alias}</span>` : null}
112
- ${awaiting != null ? html`<span class="badge-await" title=${awaiting || t("app.approxNote")}>${t("app.needsYou")}</span>` : null}</span>` : null}
113
- <span class="sr-only">${statusLabel(i, exec, awaiting)}</span></span></a></li>`;
114
- }
115
-
116
102
  /** Sign in, and come back here afterwards (the sign-in page accepts only the app's own pages). */
117
103
  export function signInHref() {
118
104
  const here = typeof location !== "undefined" ? location.pathname + location.search : "/view";
@@ -122,7 +108,6 @@ export function signInHref() {
122
108
  function Sidebar({ route, onNewInstance, viewOnly }) {
123
109
  const app = useStore(appStore);
124
110
  const shell = useStore(shellStore);
125
- const current = route && route.panel === "chat" ? route.instance : null;
126
111
  const on = (panel) => !!route && route.panel === panel;
127
112
  // A count badge is hidden at 0; its number is also in the link's accessible name.
128
113
  const navLink = (panel, href, icon, label, count = 0) => html`<a class=${`side-row${on(panel) ? " active" : ""}`} href=${href}
@@ -137,7 +122,7 @@ function Sidebar({ route, onNewInstance, viewOnly }) {
137
122
  <button type="button" class="icon-btn side-collapse" onClick=${toggleSidebar} aria-label=${narrow() ? t("app.closeMenu") : t("app.collapse")} title=${narrow() ? t("app.closeMenu") : t("app.collapse")} aria-controls="sidebar"><${Icon} name="sidebar" /></button>
138
123
  </div>
139
124
  <nav class="side-nav" aria-label=${t("app.menu")}>${navLink("view", "/view", "view", t("app.view"))}</nav>
140
- ${shell.side ? html`<${shell.side.Component} />` : html`<div class="side-section"></div>`}
125
+ ${shell.side ? html`<${shell.side.Component} />` : html`<${ViewOnlyNav} route=${route} />`}
141
126
  <div class="side-foot">
142
127
  <${Prefs} />
143
128
  <a class="side-row" href=${signInHref()}><${Icon} name="user" /><span>${t("app.signIn")}</span></a>
@@ -148,19 +133,15 @@ function Sidebar({ route, onNewInstance, viewOnly }) {
148
133
  <div class="side-head">
149
134
  <a class="brand" href="/ui" onClick=${closeDrawer}>${t("app.brand")}</a>
150
135
  <button type="button" class="icon-btn side-collapse" onClick=${toggleSidebar} aria-label=${narrow() ? t("app.closeMenu") : t("app.collapse")} title=${narrow() ? t("app.closeMenu") : t("app.collapse")} aria-controls="sidebar"><${Icon} name="sidebar" /></button>
151
- <button type="button" class="icon-btn" onClick=${onNewInstance} aria-label=${t("app.newInstance")} title=${t("app.newInstance")}><${Icon} name="edit" /></button>
136
+ <button type="button" class="icon-btn" onClick=${onNewInstance} aria-label=${t("app.newInstance")} title=${t("app.newInstance")}><${Icon} name="plus" /></button>
152
137
  </div>
153
138
  <nav class="side-nav" aria-label=${t("app.menu")}>
154
139
  ${navLink("needs", NEEDS_PATH, "inbox", t("app.needsNav"), needsCount)}
155
140
  ${navLink("fleet", "/ui/fleet", "fleet", t("app.fleet"))}
156
141
  ${navLink("view", "/view", "view", t("app.view"))}
157
142
  </nav>
158
- ${shell.side ? html`<${shell.side.Component} />` : html`<div class="side-section" id="instanceList">
159
- <h2 class="side-label">${t("app.instances")}</h2>
160
- ${app.instances.length ? html`<ul class="inst-list">${app.instances.map(i => html`<${InstanceRow} key=${i.name} i=${i} active=${i.name === current}
161
- exec=${app.exec[i.name]} awaiting=${Object.prototype.hasOwnProperty.call(app.awaiting, i.name) ? app.awaiting[i.name] : null} />`)}</ul>`
162
- : html`<p class="side-empty">${t("app.noInstances")}</p>`}
163
- </div>`}
143
+ ${shell.side ? html`<${shell.side.Component} />` : html`<${InstanceNav} route=${route} items=${app.instances} loaded=${app.ready !== false}
144
+ exec=${app.exec} awaiting=${app.awaiting} onPick=${closeDrawer} />`}
164
145
  <div class="side-foot">
165
146
  ${navLink("settings", settingsPath(), "settings", t("app.settings"))}
166
147
  ${shell.footer.map(({ key, Component }) => html`<${Component} key=${key} />`)}
@@ -170,6 +151,11 @@ function Sidebar({ route, onNewInstance, viewOnly }) {
170
151
  </aside>`;
171
152
  }
172
153
 
154
+ /** The anonymous View reader's list: View's roster read (no status frames without a session), the same component. */
155
+ function ViewOnlyNav({ route }) {
156
+ return html`<${RosterNav} route=${route} viewOnly=${true} onPick=${closeDrawer} />`;
157
+ }
158
+
173
159
  function Prefs() {
174
160
  const [theme, setTheme] = useState(typeof AgendTheme !== "undefined" ? AgendTheme.get() : "system");
175
161
  const [l, setL] = useState(lang());
@@ -245,14 +231,16 @@ const mmss = (ms) => { const s = Math.max(0, Math.ceil(ms / 1000)); return `${Ma
245
231
 
246
232
  /**
247
233
  * #1423: Settings writes an admin has to confirm, from whichever panel made them (settings-confirm.js publishes them to
248
- * appStore.pendingChanges). What it asks, how to confirm it, the countdown, Withdraw — in any panel.
234
+ * appStore.pendingChanges). What it asks, how to confirm it, the countdown, Withdraw — in any panel: a card in the
235
+ * corner, except in Settings itself, which shows the same cards in its column, right under the operation (`inline`),
236
+ * so they never cover it (alpha.2 sweep).
249
237
  */
250
- function PendingChanges() {
238
+ export function PendingChanges({ inline = false } = {}) {
251
239
  const { pendingChanges } = useStore(appStore);
252
240
  const list = pendingChanges || [];
253
241
  useTick(list.some(p => p.state === "pending"));
254
242
  if (!list.length) return null;
255
- return html`<div class="pending-stack" role="region" aria-label=${t("app.pendingTitle")}>${list.map(p => html`<section key=${p.id} class=${`pending-card ${p.state}`} role="status">
243
+ return html`<div class=${`pending-stack${inline ? " inline" : ""}`} role="region" aria-label=${t("app.pendingTitle")}>${list.map(p => html`<section key=${p.id} class=${`pending-card ${p.state}`} role="status">
256
244
  <div class="pending-head"><${Icon} name=${p.state === "applied" ? "check" : p.state === "pending" || p.state === "applying" ? "clock" : "alert"} size=${16} />
257
245
  <strong>${t(`app.pending_${p.state}`)}</strong>
258
246
  ${p.state === "pending" ? html`<span class="pending-left">${t("app.pendingLeft", mmss(p.deadline - NOW()))}</span>` : null}
@@ -336,7 +324,7 @@ export function Shell({ panels, onNewInstance, viewOnly = false }) {
336
324
  </main>
337
325
  <${BottomTabs} route=${nav.route} viewOnly=${viewOnly} />
338
326
  ${shell.dialog ? html`<${shell.dialog.Component} ...${shell.dialog.props} onClose=${closeDialog} />` : null}
339
- ${viewOnly ? null : html`<${PendingChanges} />`}
327
+ ${viewOnly || (nav.route && nav.route.panel === "settings") ? null : html`<${PendingChanges} />`}
340
328
  <${ConfirmHost} />
341
329
  <${Toasts} />
342
330
  </div>`;
@@ -21,8 +21,9 @@ export function createStore(initial) {
21
21
  /** The app's store. `ready`: a status frame has arrived (before it, an unknown instance may just not be listed yet).
22
22
  * `exec`: each instance's raw execution state (working / idle / stuck / null); `awaiting`: what it
23
23
  * waits on at its terminal (text, "" when unknown), absent when it waits on nothing. `hydration`: a late catch-up
24
- * (app-stream.js catchUp) — "catching" / "retrying" / "failed" / "ok"; `retryHydration` asks for it again. */
25
- export const appStore = createStore({ ready: false, instances: [], uptime: 0, exec: {}, awaiting: {}, connection: "connecting", needs: [],
24
+ * (app-stream.js catchUp) — "catching" / "retrying" / "failed" / "ok"; `retryHydration` asks for it again.
25
+ * `connections`: how many chat connections the fleet has (#1519 P7: 0 is web-only — the first-run card); null until known. */
26
+ export const appStore = createStore({ ready: false, instances: [], uptime: 0, exec: {}, awaiting: {}, connection: "connecting", needs: [], connections: null,
26
27
  hydration: "none", retryHydration: null });
27
28
 
28
29
  /** Fold one status frame (stream or poll) into the store. */
@@ -34,7 +35,8 @@ export function applyStatus(d) {
34
35
  exec[i.name] = (i.execution_state !== undefined ? i.execution_state : i.state) ?? null;
35
36
  if (i.state === "awaiting_input") awaiting[i.name] = typeof i.interaction_summary === "string" ? i.interaction_summary : "";
36
37
  }
37
- appStore.set({ ready: true, instances: d.instances, uptime: d.uptime ?? 0, exec, awaiting });
38
+ appStore.set({ ready: true, instances: d.instances, uptime: d.uptime ?? 0, exec, awaiting,
39
+ connections: typeof d.connections === "number" ? d.connections : null });
38
40
  }
39
41
 
40
42
  /** One "activity" event: an instance started or stopped working. */
@@ -15,7 +15,7 @@
15
15
  // older snapshot. A failed read is retried a few times with a growing delay, and then waits for the person (Retry);
16
16
  // the state is told as "hydration" events. Polls never overlap: a poll asked for while one runs follows it.
17
17
 
18
- const EVENTS = ["status", "message", "delivery", "deliveries", "prompt", "prompts", "prompt_resolved", "activity", "needs"];
18
+ const EVENTS = ["status", "message", "delivery", "deliveries", "prompt", "prompts", "prompt_resolved", "activity", "needs", "reply_buttons"];
19
19
  const MAX_HELD = 1000; // live events held during one catch-up read; more → read again afterwards
20
20
  const RETRY_DELAYS = [1000, 2000, 4000]; // after the first failed catch-up read; then it waits for Retry
21
21
  const POLL_LIMIT_MS = 10000; // a poll that has not answered by then counts as failed
@@ -83,6 +83,7 @@ export function createStream(opts) {
83
83
  if (typeof d.cursor === "string") state.cursor = d.cursor;
84
84
  if (Array.isArray(d.deliveries)) emit("deliveries", d.deliveries);
85
85
  if (Array.isArray(d.prompts)) emit("prompts", d.prompts);
86
+ if (Array.isArray(d.reply_buttons)) for (const u of d.reply_buttons) emit("reply_buttons", u); // #1266
86
87
  if (Array.isArray(d.needs)) emit("needs", { items: d.needs });
87
88
  return true;
88
89
  } catch { return false; /* the next tick, or the catch-up's retry, tries again */ }