@songsid/agend 2.2.0-beta.9 → 2.3.0-beta.1

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 (422) hide show
  1. package/README.md +40 -24
  2. package/README.zh-TW.md +40 -24
  3. package/dist/agent-cli.js +248 -2
  4. package/dist/agent-cli.js.map +1 -1
  5. package/dist/agent-endpoint.d.ts +2 -0
  6. package/dist/agent-endpoint.js +28 -0
  7. package/dist/agent-endpoint.js.map +1 -1
  8. package/dist/agent-file-read.d.ts +30 -0
  9. package/dist/agent-file-read.js +129 -0
  10. package/dist/agent-file-read.js.map +1 -0
  11. package/dist/agent-whats-new.js +20 -1
  12. package/dist/agent-whats-new.js.map +1 -1
  13. package/dist/atomic-write.js +9 -1
  14. package/dist/atomic-write.js.map +1 -1
  15. package/dist/auth-api.d.ts +3 -1
  16. package/dist/auth-api.js +32 -1
  17. package/dist/auth-api.js.map +1 -1
  18. package/dist/backend/antigravity.d.ts +33 -0
  19. package/dist/backend/antigravity.js +129 -21
  20. package/dist/backend/antigravity.js.map +1 -1
  21. package/dist/backend/claude-code.d.ts +3 -0
  22. package/dist/backend/claude-code.js +9 -0
  23. package/dist/backend/claude-code.js.map +1 -1
  24. package/dist/backend/codex-metadata.d.ts +33 -0
  25. package/dist/backend/codex-metadata.js +122 -18
  26. package/dist/backend/codex-metadata.js.map +1 -1
  27. package/dist/backend/codex.d.ts +3 -0
  28. package/dist/backend/codex.js +17 -1
  29. package/dist/backend/codex.js.map +1 -1
  30. package/dist/backend/effort-metadata.d.ts +37 -1
  31. package/dist/backend/effort-metadata.js +86 -15
  32. package/dist/backend/effort-metadata.js.map +1 -1
  33. package/dist/backend/grok.d.ts +3 -0
  34. package/dist/backend/grok.js +9 -0
  35. package/dist/backend/grok.js.map +1 -1
  36. package/dist/backend/kiro.d.ts +3 -1
  37. package/dist/backend/kiro.js +7 -3
  38. package/dist/backend/kiro.js.map +1 -1
  39. package/dist/backend/launch-effort.d.ts +61 -0
  40. package/dist/backend/launch-effort.js +170 -0
  41. package/dist/backend/launch-effort.js.map +1 -0
  42. package/dist/backend/muse.js +10 -2
  43. package/dist/backend/muse.js.map +1 -1
  44. package/dist/backend/types.d.ts +24 -0
  45. package/dist/backend/types.js +12 -1
  46. package/dist/backend/types.js.map +1 -1
  47. package/dist/backend-command.d.ts +91 -0
  48. package/dist/backend-command.js +122 -0
  49. package/dist/backend-command.js.map +1 -0
  50. package/dist/channel/adapters/discord.d.ts +14 -0
  51. package/dist/channel/adapters/discord.js +66 -4
  52. package/dist/channel/adapters/discord.js.map +1 -1
  53. package/dist/channel/adapters/telegram.d.ts +26 -1
  54. package/dist/channel/adapters/telegram.js +98 -7
  55. package/dist/channel/adapters/telegram.js.map +1 -1
  56. package/dist/channel/mcp-tools.js +36 -0
  57. package/dist/channel/mcp-tools.js.map +1 -1
  58. package/dist/channel/tool-router.d.ts +2 -0
  59. package/dist/channel/tool-router.js +1 -1
  60. package/dist/channel/tool-router.js.map +1 -1
  61. package/dist/channel/types.d.ts +31 -0
  62. package/dist/cli.js +124 -24
  63. package/dist/cli.js.map +1 -1
  64. package/dist/command-table.d.ts +2 -2
  65. package/dist/command-table.js +5 -2
  66. package/dist/command-table.js.map +1 -1
  67. package/dist/compact-watch.d.ts +86 -0
  68. package/dist/compact-watch.js +288 -0
  69. package/dist/compact-watch.js.map +1 -0
  70. package/dist/config-validator.js +55 -0
  71. package/dist/config-validator.js.map +1 -1
  72. package/dist/config.d.ts +1 -0
  73. package/dist/config.js +2 -1
  74. package/dist/config.js.map +1 -1
  75. package/dist/daemon.d.ts +9 -0
  76. package/dist/daemon.js +394 -29
  77. package/dist/daemon.js.map +1 -1
  78. package/dist/delivery-outbox.d.ts +8 -1
  79. package/dist/delivery-outbox.js +6 -2
  80. package/dist/delivery-outbox.js.map +1 -1
  81. package/dist/doctor.js +7 -0
  82. package/dist/doctor.js.map +1 -1
  83. package/dist/fleet-context.d.ts +12 -0
  84. package/dist/fleet-manager.d.ts +247 -9
  85. package/dist/fleet-manager.js +1227 -142
  86. package/dist/fleet-manager.js.map +1 -1
  87. package/dist/forged-envelope-scan.d.ts +4 -3
  88. package/dist/forged-envelope-scan.js +4 -3
  89. package/dist/forged-envelope-scan.js.map +1 -1
  90. package/dist/general-knowledge/skills/model-discovery/SKILL.md +4 -1
  91. package/dist/general-knowledge/skills/web-appearance/SKILL.md +258 -0
  92. package/dist/install-lock.d.ts +18 -1
  93. package/dist/install-lock.js +30 -3
  94. package/dist/install-lock.js.map +1 -1
  95. package/dist/instance-avatar.d.ts +5 -4
  96. package/dist/instance-avatar.js +15 -8
  97. package/dist/instance-avatar.js.map +1 -1
  98. package/dist/instance-config-impact.js +3 -0
  99. package/dist/instance-config-impact.js.map +1 -1
  100. package/dist/instance-lifecycle.js +9 -4
  101. package/dist/instance-lifecycle.js.map +1 -1
  102. package/dist/instructions.js +2 -2
  103. package/dist/instructions.js.map +1 -1
  104. package/dist/kiro-compact-pane.d.ts +38 -0
  105. package/dist/kiro-compact-pane.js +57 -0
  106. package/dist/kiro-compact-pane.js.map +1 -0
  107. package/dist/launchd-handoff.d.ts +43 -0
  108. package/dist/launchd-handoff.js +56 -0
  109. package/dist/launchd-handoff.js.map +1 -0
  110. package/dist/locale.js +202 -30
  111. package/dist/locale.js.map +1 -1
  112. package/dist/log-service.js +10 -2
  113. package/dist/log-service.js.map +1 -1
  114. package/dist/login-shell.d.ts +21 -0
  115. package/dist/login-shell.js +98 -0
  116. package/dist/login-shell.js.map +1 -0
  117. package/dist/needs-you-hub.d.ts +4 -0
  118. package/dist/needs-you-hub.js +31 -6
  119. package/dist/needs-you-hub.js.map +1 -1
  120. package/dist/needs-you.d.ts +1 -13
  121. package/dist/needs-you.js +3 -37
  122. package/dist/needs-you.js.map +1 -1
  123. package/dist/outbound-handlers.d.ts +32 -0
  124. package/dist/outbound-handlers.js +150 -45
  125. package/dist/outbound-handlers.js.map +1 -1
  126. package/dist/outbound-schemas.d.ts +76 -0
  127. package/dist/outbound-schemas.js +65 -4
  128. package/dist/outbound-schemas.js.map +1 -1
  129. package/dist/provider-probe.d.ts +29 -0
  130. package/dist/provider-probe.js +198 -2
  131. package/dist/provider-probe.js.map +1 -1
  132. package/dist/public-web-gateway.d.ts +2 -0
  133. package/dist/public-web-gateway.js +25 -1
  134. package/dist/public-web-gateway.js.map +1 -1
  135. package/dist/quickstart-api.d.ts +1 -1
  136. package/dist/quickstart-api.js +8 -6
  137. package/dist/quickstart-api.js.map +1 -1
  138. package/dist/quickstart.d.ts +12 -0
  139. package/dist/quickstart.js +36 -6
  140. package/dist/quickstart.js.map +1 -1
  141. package/dist/reply-buttons-controller.d.ts +86 -5
  142. package/dist/reply-buttons-controller.js +271 -7
  143. package/dist/reply-buttons-controller.js.map +1 -1
  144. package/dist/reply-buttons.d.ts +48 -3
  145. package/dist/reply-buttons.js +105 -10
  146. package/dist/reply-buttons.js.map +1 -1
  147. package/dist/reply-polls.d.ts +85 -0
  148. package/dist/reply-polls.js +154 -0
  149. package/dist/reply-polls.js.map +1 -0
  150. package/dist/repo-worktree-guard.d.ts +36 -0
  151. package/dist/repo-worktree-guard.js +143 -0
  152. package/dist/repo-worktree-guard.js.map +1 -0
  153. package/dist/restart-guard.d.ts +6 -3
  154. package/dist/restart-guard.js +6 -3
  155. package/dist/restart-guard.js.map +1 -1
  156. package/dist/restart-progress.d.ts +0 -2
  157. package/dist/restart-progress.js +0 -2
  158. package/dist/restart-progress.js.map +1 -1
  159. package/dist/scheduler/scheduler.d.ts +58 -1
  160. package/dist/scheduler/scheduler.js +176 -10
  161. package/dist/scheduler/scheduler.js.map +1 -1
  162. package/dist/sd-notify.d.ts +22 -1
  163. package/dist/sd-notify.js +63 -10
  164. package/dist/sd-notify.js.map +1 -1
  165. package/dist/service-activation.d.ts +15 -7
  166. package/dist/service-activation.js +42 -26
  167. package/dist/service-activation.js.map +1 -1
  168. package/dist/service-installer.d.ts +24 -0
  169. package/dist/service-installer.js +49 -1
  170. package/dist/service-installer.js.map +1 -1
  171. package/dist/service-plan.d.ts +11 -0
  172. package/dist/service-plan.js +23 -0
  173. package/dist/service-plan.js.map +1 -1
  174. package/dist/settings-api.js +74 -1
  175. package/dist/settings-api.js.map +1 -1
  176. package/dist/settings-change.js +8 -0
  177. package/dist/settings-change.js.map +1 -1
  178. package/dist/settings-effect.js +16 -2
  179. package/dist/settings-effect.js.map +1 -1
  180. package/dist/settings-http-confirmation.js +5 -0
  181. package/dist/settings-http-confirmation.js.map +1 -1
  182. package/dist/systemd-peer-bus.d.ts +95 -0
  183. package/dist/systemd-peer-bus.js +430 -0
  184. package/dist/systemd-peer-bus.js.map +1 -0
  185. package/dist/systemd-runtime.d.ts +27 -1
  186. package/dist/systemd-runtime.js +69 -7
  187. package/dist/systemd-runtime.js.map +1 -1
  188. package/dist/tool-permissions.js +53 -3
  189. package/dist/tool-permissions.js.map +1 -1
  190. package/dist/topic-commands.d.ts +72 -2
  191. package/dist/topic-commands.js +156 -38
  192. package/dist/topic-commands.js.map +1 -1
  193. package/dist/topic-link.d.ts +128 -0
  194. package/dist/topic-link.js +269 -0
  195. package/dist/topic-link.js.map +1 -0
  196. package/dist/transcript-jsonl.d.ts +7 -2
  197. package/dist/transcript-jsonl.js +13 -7
  198. package/dist/transcript-jsonl.js.map +1 -1
  199. package/dist/transcript-sources.js +6 -1
  200. package/dist/transcript-sources.js.map +1 -1
  201. package/dist/types.d.ts +21 -0
  202. package/dist/ui/app-effects.js +187 -0
  203. package/dist/ui/chat-commands.js +1 -0
  204. package/dist/ui/chat-effect-posters.js +42 -0
  205. package/dist/ui/chat-effects.js +72 -0
  206. package/dist/ui/chat-live-replies.js +58 -0
  207. package/dist/ui/chat-render.js +59 -14
  208. package/dist/ui/chat-store.js +78 -8
  209. package/dist/ui/chat-strings.js +43 -16
  210. package/dist/ui/chat-thread.js +175 -31
  211. package/dist/ui/chat-tour.js +18 -15
  212. package/dist/ui/effect-trial-controller.js +119 -0
  213. package/dist/ui/effect-trial-offer.js +31 -0
  214. package/dist/ui/effect-trial-panel.js +37 -0
  215. package/dist/ui/image-lightbox.js +1 -1
  216. package/dist/ui/panel-chat.js +101 -27
  217. package/dist/ui/panel-details.js +18 -5
  218. package/dist/ui/panel-fleet.js +18 -5
  219. package/dist/ui/panel-log.js +10 -10
  220. package/dist/ui/panel-needs.js +13 -10
  221. package/dist/ui/panel-settings.js +66 -12
  222. package/dist/ui/preview-panel.js +25 -9
  223. package/dist/ui/settings-appearance.js +325 -0
  224. package/dist/ui/settings-dialogs.js +11 -6
  225. package/dist/ui/settings-effects.js +201 -0
  226. package/dist/ui/settings-model.js +3 -1
  227. package/dist/ui/settings-mods.js +257 -0
  228. package/dist/ui/settings-owner-effects.js +80 -0
  229. package/dist/ui/settings-strings.js +269 -10
  230. package/dist/ui/settings-token.js +7 -0
  231. package/dist/ui/settings-wizard.js +5 -3
  232. package/dist/ui/shared/agend-auth.js +27 -3
  233. package/dist/ui/shared/agent-widget.js +248 -0
  234. package/dist/ui/shared/app-i18n.js +3 -1
  235. package/dist/ui/shared/app-prefs.js +30 -10
  236. package/dist/ui/shared/app-route.js +23 -1
  237. package/dist/ui/shared/app-session.js +1 -1
  238. package/dist/ui/shared/app-shell.js +29 -16
  239. package/dist/ui/shared/app-store.js +2 -0
  240. package/dist/ui/shared/app-stream.js +15 -7
  241. package/dist/ui/shared/app-themes.js +188 -0
  242. package/dist/ui/shared/app.css +266 -11
  243. package/dist/ui/shared/app.js +6 -0
  244. package/dist/ui/shared/card-strings.js +28 -0
  245. package/dist/ui/shared/effect-app-runtime.js +248 -0
  246. package/dist/ui/shared/effect-baseline.js +64 -0
  247. package/dist/ui/shared/effect-chat-runtime.js +154 -0
  248. package/dist/ui/shared/effect-cursor.js +125 -0
  249. package/dist/ui/shared/effect-frame.js +127 -0
  250. package/dist/ui/shared/effect-geometry.js +122 -0
  251. package/dist/ui/shared/effect-host-style.js +10 -0
  252. package/dist/ui/shared/effect-owner-regions.js +74 -0
  253. package/dist/ui/shared/effect-owner-source.js +115 -0
  254. package/dist/ui/shared/effect-policy.js +137 -0
  255. package/dist/ui/shared/effect-poster-reader.js +98 -0
  256. package/dist/ui/shared/effect-posters.js +117 -0
  257. package/dist/ui/shared/effect-profiler.js +162 -0
  258. package/dist/ui/shared/effect-profiles.js +65 -0
  259. package/dist/ui/shared/effect-registry.js +496 -0
  260. package/dist/ui/shared/effect-runtime-loader.js +234 -0
  261. package/dist/ui/shared/effect-safety.js +153 -0
  262. package/dist/ui/shared/effect-stock-snapshot.js +34 -0
  263. package/dist/ui/shared/effect-stock-trust.js +165 -0
  264. package/dist/ui/shared/effect-trial-source.js +103 -0
  265. package/dist/ui/shared/effect-trial-stage.js +147 -0
  266. package/dist/ui/shared/header-tools.js +35 -15
  267. package/dist/ui/shared/identity-card.js +202 -0
  268. package/dist/ui/shared/identity-color.js +261 -0
  269. package/dist/ui/shared/instance-nav.js +22 -14
  270. package/dist/ui/shared/instance-switch.js +5 -2
  271. package/dist/ui/shared/owner-css.js +221 -0
  272. package/dist/ui/shared/owner-effect-prefs.js +117 -0
  273. package/dist/ui/shared/panel-view.js +96 -12
  274. package/dist/ui/shared/signin.js +17 -9
  275. package/dist/ui/shared/ui-dialog.js +1 -1
  276. package/dist/ui/shared/ui-icons.js +3 -0
  277. package/dist/ui/shared/ui-menu.js +1 -1
  278. package/dist/ui/shared/ui-toast.js +1 -1
  279. package/dist/ui/shared/view-strings.js +40 -2
  280. package/dist/ui/signin.html +9 -3
  281. package/dist/update-check.d.ts +31 -0
  282. package/dist/update-check.js +47 -0
  283. package/dist/update-check.js.map +1 -1
  284. package/dist/update-launch.d.ts +48 -1
  285. package/dist/update-launch.js +159 -5
  286. package/dist/update-launch.js.map +1 -1
  287. package/dist/usage/providers.d.ts +28 -8
  288. package/dist/usage/providers.js +96 -41
  289. package/dist/usage/providers.js.map +1 -1
  290. package/dist/view-api.d.ts +29 -0
  291. package/dist/view-api.js +214 -17
  292. package/dist/view-api.js.map +1 -1
  293. package/dist/web-api.d.ts +86 -2
  294. package/dist/web-api.js +1160 -92
  295. package/dist/web-api.js.map +1 -1
  296. package/dist/web-appearance-limits.d.ts +82 -0
  297. package/dist/web-appearance-limits.js +83 -0
  298. package/dist/web-appearance-limits.js.map +1 -0
  299. package/dist/web-appearance-policy.d.ts +66 -0
  300. package/dist/web-appearance-policy.js +94 -0
  301. package/dist/web-appearance-policy.js.map +1 -0
  302. package/dist/web-appearance-tools.d.ts +50 -0
  303. package/dist/web-appearance-tools.js +105 -0
  304. package/dist/web-appearance-tools.js.map +1 -0
  305. package/dist/web-appearance-view.d.ts +99 -0
  306. package/dist/web-appearance-view.js +163 -0
  307. package/dist/web-appearance-view.js.map +1 -0
  308. package/dist/web-appearance.d.ts +79 -0
  309. package/dist/web-appearance.js +298 -0
  310. package/dist/web-appearance.js.map +1 -0
  311. package/dist/web-asset-parts.d.ts +98 -0
  312. package/dist/web-asset-parts.js +133 -0
  313. package/dist/web-asset-parts.js.map +1 -0
  314. package/dist/web-auth.js +17 -1
  315. package/dist/web-auth.js.map +1 -1
  316. package/dist/web-card.d.ts +96 -0
  317. package/dist/web-card.js +232 -0
  318. package/dist/web-card.js.map +1 -0
  319. package/dist/web-chat-history.d.ts +39 -1
  320. package/dist/web-chat-history.js +69 -9
  321. package/dist/web-chat-history.js.map +1 -1
  322. package/dist/web-commands.d.ts +39 -3
  323. package/dist/web-commands.js +22 -5
  324. package/dist/web-commands.js.map +1 -1
  325. package/dist/web-effect-bundle.d.ts +28 -0
  326. package/dist/web-effect-bundle.js +126 -0
  327. package/dist/web-effect-bundle.js.map +1 -0
  328. package/dist/web-effect-canonical.d.ts +8 -0
  329. package/dist/web-effect-canonical.js +27 -0
  330. package/dist/web-effect-canonical.js.map +1 -0
  331. package/dist/web-effect-library.d.ts +114 -0
  332. package/dist/web-effect-library.js +530 -0
  333. package/dist/web-effect-library.js.map +1 -0
  334. package/dist/web-effect-poster.d.ts +8 -0
  335. package/dist/web-effect-poster.js +52 -0
  336. package/dist/web-effect-poster.js.map +1 -0
  337. package/dist/web-effect-profiles.d.ts +118 -0
  338. package/dist/web-effect-profiles.js +180 -0
  339. package/dist/web-effect-profiles.js.map +1 -0
  340. package/dist/web-effect-service.d.ts +86 -0
  341. package/dist/web-effect-service.js +312 -0
  342. package/dist/web-effect-service.js.map +1 -0
  343. package/dist/web-effect-tools.d.ts +47 -0
  344. package/dist/web-effect-tools.js +126 -0
  345. package/dist/web-effect-tools.js.map +1 -0
  346. package/dist/web-effect-trials.d.ts +86 -0
  347. package/dist/web-effect-trials.js +204 -0
  348. package/dist/web-effect-trials.js.map +1 -0
  349. package/dist/web-effect-upload.d.ts +29 -0
  350. package/dist/web-effect-upload.js +190 -0
  351. package/dist/web-effect-upload.js.map +1 -0
  352. package/dist/web-effect-worker.d.ts +8 -0
  353. package/dist/web-effect-worker.js +170 -0
  354. package/dist/web-effect-worker.js.map +1 -0
  355. package/dist/web-effect.d.ts +89 -0
  356. package/dist/web-effect.js +132 -0
  357. package/dist/web-effect.js.map +1 -0
  358. package/dist/web-identity-color.d.ts +87 -0
  359. package/dist/web-identity-color.js +209 -0
  360. package/dist/web-identity-color.js.map +1 -0
  361. package/dist/web-identity-palette.d.ts +113 -0
  362. package/dist/web-identity-palette.js +84 -0
  363. package/dist/web-identity-palette.js.map +1 -0
  364. package/dist/web-image-meta.d.ts +25 -0
  365. package/dist/web-image-meta.js +200 -0
  366. package/dist/web-image-meta.js.map +1 -0
  367. package/dist/web-login.d.ts +14 -0
  368. package/dist/web-login.js +4 -2
  369. package/dist/web-login.js.map +1 -1
  370. package/dist/web-mod-agent.d.ts +50 -0
  371. package/dist/web-mod-agent.js +203 -0
  372. package/dist/web-mod-agent.js.map +1 -0
  373. package/dist/web-mod-bundle.d.ts +144 -0
  374. package/dist/web-mod-bundle.js +464 -0
  375. package/dist/web-mod-bundle.js.map +1 -0
  376. package/dist/web-mod-export.d.ts +59 -0
  377. package/dist/web-mod-export.js +79 -0
  378. package/dist/web-mod-export.js.map +1 -0
  379. package/dist/web-mod-store.d.ts +283 -0
  380. package/dist/web-mod-store.js +624 -0
  381. package/dist/web-mod-store.js.map +1 -0
  382. package/dist/web-org.d.ts +3 -3
  383. package/dist/web-org.js +11 -17
  384. package/dist/web-org.js.map +1 -1
  385. package/dist/web-owner-css-store.d.ts +180 -0
  386. package/dist/web-owner-css-store.js +384 -0
  387. package/dist/web-owner-css-store.js.map +1 -0
  388. package/dist/web-owner-css.d.ts +58 -0
  389. package/dist/web-owner-css.js +552 -0
  390. package/dist/web-owner-css.js.map +1 -0
  391. package/dist/web-prefs.d.ts +66 -0
  392. package/dist/web-prefs.js +243 -35
  393. package/dist/web-prefs.js.map +1 -1
  394. package/dist/web-preview.d.ts +9 -1
  395. package/dist/web-preview.js +24 -7
  396. package/dist/web-preview.js.map +1 -1
  397. package/dist/web-profile.d.ts +32 -0
  398. package/dist/web-profile.js +132 -0
  399. package/dist/web-profile.js.map +1 -0
  400. package/dist/web-session.d.ts +14 -0
  401. package/dist/web-session.js +10 -0
  402. package/dist/web-session.js.map +1 -1
  403. package/dist/web-settings-patch.d.ts +18 -0
  404. package/dist/web-settings-patch.js +51 -0
  405. package/dist/web-settings-patch.js.map +1 -1
  406. package/dist/web-shell-routes.d.ts +1 -1
  407. package/dist/web-shell-routes.js +1 -1
  408. package/dist/web-shell-routes.js.map +1 -1
  409. package/dist/web-theme-store.d.ts +218 -0
  410. package/dist/web-theme-store.js +516 -0
  411. package/dist/web-theme-store.js.map +1 -0
  412. package/dist/web-theme.d.ts +121 -0
  413. package/dist/web-theme.js +407 -0
  414. package/dist/web-theme.js.map +1 -0
  415. package/dist/web-upload.d.ts +119 -4
  416. package/dist/web-upload.js +252 -39
  417. package/dist/web-upload.js.map +1 -1
  418. package/dist/web-widget.d.ts +72 -0
  419. package/dist/web-widget.js +245 -0
  420. package/dist/web-widget.js.map +1 -0
  421. package/launcher/install-admission.cjs +61 -6
  422. package/package.json +3 -3
@@ -0,0 +1,117 @@
1
+ // #1629: host-authoritative personal choices. No local cache, migration, frame, code fetch or automatic resend.
2
+ import { createStore, appStore } from "./app-store.js";
3
+ import { OWNER_EFFECT_PREF_KEYS } from "./app-prefs.js";
4
+ import { checkEffectOwnerSlots } from "./effect-runtime-loader.js";
5
+ const KEYS = OWNER_EFFECT_PREF_KEYS, ID = /^[a-f0-9]{32}$/, EPOCH = /^[a-f0-9]{16}$/;
6
+ const plain = x => !!x && typeof x === "object" && Object.getPrototypeOf(x) === Object.prototype;
7
+ const own = (x, k) => Object.hasOwn(x, k);
8
+ const unknownSlots = () => Object.fromEntries(KEYS.map(k => [k, { status: "unknown", id: null }]));
9
+ const publicPage = () => typeof document === "undefined" || document.body?.dataset.publicLink === "1" || document.body?.dataset.mode === "view-only";
10
+ function bootScope() {
11
+ try {
12
+ const x = JSON.parse(document.documentElement.getAttribute("data-prefs") || "null");
13
+ return plain(x) && ["person", "operator"].includes(x.ownerScope) && typeof x.ownerEpoch === "string" && EPOCH.test(x.ownerEpoch)
14
+ ? Object.freeze({ scope: x.ownerScope, epoch: x.ownerEpoch }) : null;
15
+ } catch { return null; }
16
+ }
17
+ const owner = bootScope();
18
+ export const ownerEffectsStore = createStore({ scope: owner?.scope ?? "none", status: "unknown", revoked: false, busy: false, slots: unknownSlots(), error: null });
19
+ let watchers = 0, generation = 0, active = null, queuedRead = false, unsubscribe = null, rev = -1, seen = null, failedAt = -Infinity;
20
+ const clock = () => { try { const n = performance.now(); return Number.isFinite(n) && n >= 0 ? n : null; } catch { return null; } };
21
+ const eligible = () => !!owner && watchers > 0 && !publicPage() && !ownerEffectsStore.get().revoked;
22
+ const current = op => eligible() && op.generation === generation && active === op;
23
+ function revoke() {
24
+ queuedRead = false;
25
+ ownerEffectsStore.set({ status: "unknown", revoked: true, slots: unknownSlots(), error: "sign_in_changed" });
26
+ }
27
+ function unknown(error) { const n = clock(); failedAt = n === null ? Infinity : n; ownerEffectsStore.set({ status: "unknown", slots: unknownSlots(), error }); }
28
+ function claim() {
29
+ if (!eligible() || active) return null;
30
+ const controller = new AbortController(), op = { controller, generation };
31
+ active = op; ownerEffectsStore.set({ busy: true, error: null }); return op;
32
+ }
33
+ async function call(op, path, init = {}) {
34
+ const started = clock();
35
+ if (started === null || op.controller.signal.aborted) return { ok: false, status: 0, body: null };
36
+ const timer = setTimeout(() => op.controller.abort(), 10000);
37
+ try {
38
+ const res = await fetch(path, { credentials: "same-origin", ...init, signal: op.controller.signal });
39
+ let body = null; try { body = await res.json(); } catch { /* unknown */ }
40
+ const ended = clock();
41
+ if (op.controller.signal.aborted || ended === null || ended < started || ended - started >= 10000) return { ok: false, status: 0, body: null };
42
+ return { ok: res.ok, status: res.status, body };
43
+ } catch { return { ok: false, status: 0, body: null }; }
44
+ finally { clearTimeout(timer); }
45
+ }
46
+ function finish(op) {
47
+ if (active !== op) return;
48
+ active = null; ownerEffectsStore.set({ busy: false });
49
+ const again = queuedRead; queuedRead = false;
50
+ const n = clock();
51
+ if (again && eligible() && n !== null && n - failedAt >= 5000) refreshOwnerEffects();
52
+ }
53
+ /** An owner response is closed and bounded. Unavailable means a stored id, not an empty slot or private manifest. */
54
+ export function checkOwnerSlots(body) { return checkEffectOwnerSlots(body); }
55
+
56
+ async function read(op) {
57
+ const r = await call(op, `/ui/effects/owner?ownerEpoch=${owner.epoch}`);
58
+ if (!current(op)) return false;
59
+ if ([401, 403, 409].includes(r.status)) { revoke(); return false; }
60
+ const data = r.ok ? checkOwnerSlots(r.body) : null;
61
+ if (!data) { unknown("unreadable"); return false; }
62
+ if (data.rev < rev) { unknown("stale"); return false; }
63
+ rev = data.rev; failedAt = -Infinity;
64
+ ownerEffectsStore.set({ status: "known", slots: data.slots, error: null }); return true;
65
+ }
66
+ export async function refreshOwnerEffects() {
67
+ if (!eligible()) return false;
68
+ if (active) { queuedRead = true; return false; }
69
+ const op = claim(); if (!op) return false;
70
+ try { return await read(op); } finally { finish(op); }
71
+ }
72
+ /** One physical operation survives until settlement, including after abort. At most one coalesced read follows. */
73
+ export function watchOwnerEffects() {
74
+ watchers++;
75
+ if (watchers === 1 && eligible()) {
76
+ const check = () => {
77
+ const next = appStore.get().prefsRev, n = clock();
78
+ if (next !== seen || ownerEffectsStore.get().status === "unknown" && n !== null && n - failedAt >= 5000) { seen = next; refreshOwnerEffects(); }
79
+ };
80
+ unsubscribe = appStore.subscribe(check); check();
81
+ }
82
+ let ended = false;
83
+ return () => {
84
+ if (ended) return; ended = true;
85
+ if (--watchers > 0) return;
86
+ generation++; queuedRead = false; unsubscribe?.(); unsubscribe = null; active?.controller.abort();
87
+ ownerEffectsStore.set({ status: "unknown", slots: unknownSlots(), error: null });
88
+ };
89
+ }
90
+ export async function saveOwnerEffects(set) {
91
+ if (!plain(set) || !Object.keys(set).length || Object.keys(set).some(k => !KEYS.includes(k) || set[k] !== null && (typeof set[k] !== "string" || !ID.test(set[k])))) return { ok: false, error: "invalid" };
92
+ // Snapshot both data and immutable boot context before the first wait. No optimistic runtime selection.
93
+ const values = { ...set }, op = claim(); if (!op) return { ok: false, error: "busy_or_unavailable" };
94
+ let saved = false;
95
+ try {
96
+ const r = await call(op, "/ui/prefs", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ set: values, ownerEpoch: owner.epoch }) });
97
+ if (!current(op)) return { ok: false, error: "superseded" };
98
+ if ([401, 403, 409].includes(r.status)) { revoke(); return { ok: false, error: "sign_in_changed" }; }
99
+ if (r.ok && plain(r.body) && (r.body.ownerEpoch !== owner.epoch || r.body.ownerScope !== owner.scope)) { revoke(); return { ok: false, error: "sign_in_changed" }; }
100
+ if (!r.ok || !plain(r.body) || !plain(r.body.values) || !Number.isSafeInteger(r.body.rev) || r.body.rev < 0 || r.body.rev < rev || r.body.unreadable) { unknown("not_saved"); return { ok: false, error: "not_saved" }; }
101
+ if (!Object.entries(values).every(([k, v]) => v === null ? !own(r.body.values, k) : r.body.values[k] === v)) { unknown("not_saved"); return { ok: false, error: "not_saved" }; }
102
+ saved = true;
103
+ const refreshed = await read(op); return { ok: refreshed, saved, error: refreshed ? null : "refresh_failed" };
104
+ } finally { finish(op); }
105
+ }
106
+ /** Runtime adapter asks only for the exact, currently known selection; a cursor can never request Worker source. */
107
+ export function ownerEffectUrl(key, id, kind = "resource") {
108
+ const row = ownerEffectsStore.get().slots[key];
109
+ if (!eligible() || ownerEffectsStore.get().status !== "known" || !KEYS.includes(key) || !row || row.status !== "available" || row.id !== id || !["resource", "poster", "cursor"].includes(kind) || kind === "resource" && key === "fx.cursor") return null;
110
+ const path = kind === "resource" ? `/ui/effects/resource/${id}` : `/ui/effects/image/${id}/${kind}.png`;
111
+ return `${path}?slot=${encodeURIComponent(key)}&ownerEpoch=${owner.epoch}`;
112
+ }
113
+ export function markOwnerEffectUnavailable(key, id) {
114
+ const s = ownerEffectsStore.get(), row = s.slots[key];
115
+ if (!eligible() || s.status !== "known" || !KEYS.includes(key) || row?.id !== id) return;
116
+ ownerEffectsStore.set({ slots: { ...s.slots, [key]: { status: "unavailable", id } } });
117
+ }
@@ -27,6 +27,9 @@ import { Dialog } from "./ui-dialog.js";
27
27
  import { Empty, ErrorState, Skeleton } from "./ui-states.js";
28
28
  import { Icon } from "./ui-icons.js";
29
29
  import "./view-strings.js";
30
+ import { checkCard, fitPicture, IdentityCard, useCard } from "./identity-card.js";
31
+ import { confirmDialog } from "./ui-confirm.js";
32
+ import { openColorDialog } from "./identity-color.js";
30
33
 
31
34
  /** view.* text with named values: tn("paneFailed", { name, status }). */
32
35
  function tn(key, values = {}) {
@@ -114,7 +117,9 @@ export { viewStore };
114
117
  // ── The panel ──
115
118
 
116
119
  // readStream: one stream of reads under a lease (read-stream.js; View's pane and roster use it).
117
- const MIN_PX = 12, MAX_PX = 22;
120
+ // S / M / L: the fitted size kept within 12–22 px, then scaled. Fit (#1869): never larger than what fits — only a 6 px
121
+ // floor for legibility (a grid too wide for the box even at 6 px, e.g. 200 columns on a phone, still scrolls).
122
+ const MIN_PX = 12, MAX_PX = 22, FIT_FLOOR_PX = 6;
118
123
 
119
124
  export function ViewPanel({ route, navKey }) {
120
125
  const lease = useLease(navKey);
@@ -178,15 +183,15 @@ export function ViewPanel({ route, navKey }) {
178
183
  <button type="button" class="icon-btn" onClick=${() => setDialog("help")} aria-label=${tn("helpButton")} title=${tn("helpButton")}><${Icon} name="info" /></button>`;
179
184
 
180
185
  let body;
181
- if (v.error && !v.roster.length) body = html`<div class="panel-body center"><${ErrorState} message=${tn("rosterFailedShort")} onRetry=${() => navigate(location.pathname, { replace: true })} /></div>`;
182
- else if (!v.loaded) body = html`<div class="panel-body"><${Skeleton} lines=${6} /></div>`;
183
- else if (!v.roster.length) body = html`<div class="panel-body center"><${Empty} icon="view" title=${tn("noInstances")} /></div>`;
184
- else if (!name) body = html`<div class="panel-body"><${Skeleton} lines=${6} /></div>`;
185
- else if (!it) body = html`<div class="panel-body center"><${Empty} icon="alert" title=${tn("notFound", { name })} hint=${tn("notFoundHint")} /></div>`;
186
+ if (v.error && !v.roster.length) body = html`<div data-trust="panel-content" class="panel-body center"><${ErrorState} message=${tn("rosterFailedShort")} onRetry=${() => navigate(location.pathname, { replace: true })} /></div>`;
187
+ else if (!v.loaded) body = html`<div data-trust="panel-content" class="panel-body"><${Skeleton} lines=${6} /></div>`;
188
+ else if (!v.roster.length) body = html`<div data-trust="panel-content" class="panel-body center"><${Empty} icon="view" title=${tn("noInstances")} /></div>`;
189
+ else if (!name) body = html`<div data-trust="panel-content" class="panel-body"><${Skeleton} lines=${6} /></div>`;
190
+ else if (!it) body = html`<div data-trust="panel-content" class="panel-body center"><${Empty} icon="alert" title=${tn("notFound", { name })} hint=${tn("notFoundHint")} /></div>`;
186
191
  else body = html`<${Terminal} key=${name} name=${name} lease=${lease} density=${density} />
187
192
  <${Card} it=${it} viewOnly=${viewOnly} onEdit=${() => setDialog("edit")} />`;
188
193
 
189
- return html`<div class="panel p-view">
194
+ return html`<div data-effect-panel="view" class="panel p-view">
190
195
  <${PanelHeader} title=${it ? (it.display_name || it.instance_name) : tn("title")}
191
196
  sub=${it ? html`<span class="status"><span class=${`dot ${it.status === "running" ? "ok" : it.status === "crashed" ? "bad" : "off"}`} aria-hidden="true"></span>${tn(STATUS_KEYS[it.status] || "statusUnknown")}</span>` : null}
192
197
  nav=${it && !viewOnly ? html`<${InstanceSwitch} name=${name} current="view" />` : null}>${actions}</${PanelHeader}>
@@ -219,10 +224,11 @@ function Terminal({ name, lease, density }) {
219
224
  const lh = parseFloat(pcs.lineHeight) / parseFloat(pcs.fontSize);
220
225
  const lineH = Number.isFinite(lh) && lh > 0 ? lh : 1.2;
221
226
  let size = Math.min(innerW / (g.cols * glyphRatio(p)), innerH / (g.rows * lineH)) * 0.995;
222
- size = Math.max(MIN_PX, Math.min(size, MAX_PX)) * (density === "fit" ? 1 : VIEW_SCALE[density] || 1);
223
- size = Math.max(8, Math.min(size, 32));
227
+ if (density === "fit") size = Math.max(FIT_FLOOR_PX, Math.min(size, MAX_PX));
228
+ else size = Math.max(8, Math.min(Math.max(MIN_PX, Math.min(size, MAX_PX)) * (VIEW_SCALE[density] || 1), 32));
224
229
  g.key = key;
225
- if (Math.abs(size - g.applied) < 0.75) return;
230
+ // A small change is skipped only when growing (the size applied still fits); a smaller box always shrinks it.
231
+ if (size >= g.applied && size - g.applied < 0.75) return;
226
232
  g.applied = size;
227
233
  p.style.fontSize = `${size.toFixed(2)}px`; // CSSOM, never a style attribute (#1300)
228
234
  };
@@ -263,7 +269,7 @@ function Terminal({ name, lease, density }) {
263
269
  lease.hold(() => ro.disconnect());
264
270
  }
265
271
  }, [lease, name]);
266
- return html`<div class="v-term" ref=${term}>${problem ? html`<p class="v-term-note" role="status">${problem}</p>` : null}<pre ref=${pre} class="v-pre"></pre></div>`;
272
+ return html`<div data-trust="panel-content" class="v-term" ref=${term}>${problem ? html`<p class="v-term-note" role="status">${problem}</p>` : null}<pre ref=${pre} class="v-pre"></pre></div>`;
267
273
  }
268
274
 
269
275
  // The advance of one monospace cell as a fraction of font-size, measured once on this page.
@@ -293,7 +299,9 @@ function Card({ it, viewOnly, onEdit }) {
293
299
  const initial = (it.display_name || it.instance_name || "?").trim().charAt(0) || "?";
294
300
  const ctx = it.context_pct != null ? ` · ctx ${Math.round(it.context_pct)}%` : "";
295
301
  const toggle = () => { const next = !open; setOpen(next); store("agend_view_card_expanded", next ? "1" : "0"); };
296
- return html`<section class=${`v-card${open ? " open" : ""}`} aria-label=${it.display_name || it.instance_name}>
302
+ // #1629 r4 §1: the identity card, above the profile row while it is expanded.
303
+ const card = useCard(it.instance_name, it.card_v);
304
+ return html`${open && card ? html`<div data-trust="panel-content" class="v-id"><${IdentityCard} name=${it.instance_name} card=${card} /></div>` : null}<section data-trust="panel-content" class=${`v-card${open ? " open" : ""}`} aria-label=${it.display_name || it.instance_name}>
297
305
  <div class="v-avatar">${it.has_avatar ? html`<img ref=${avatar} alt="" />` : html`<span class="v-initial" aria-hidden="true">${initial}</span>`}</div>
298
306
  <div class="v-card-body">
299
307
  <div class="v-card-line"><strong>${it.display_name || it.instance_name}</strong>${it.role ? html`<span class="v-role">${it.role}</span>` : null}</div>
@@ -314,7 +322,31 @@ function EditProfile({ it, onClose }) {
314
322
  const [f, setF] = useState({ display: it.display_name || "", role: it.role || "", desc: it.description || "" });
315
323
  const [busy, setBusy] = useState(false);
316
324
  const [msg, setMsg] = useState(null);
325
+ // #1630 follow-up: the uploaded picture is removed on Save (Cancel keeps it); a newly picked file wins over removing.
326
+ const [removePic, setRemovePic] = useState(false);
317
327
  const file = useRef(null);
328
+ // #1629 r4 §1 / r4.1: the identity card's own fields — this person's (theirs win over the agent's, field by field).
329
+ // Read when the dialog opens: what is drawn now, whose each field is, and the upload limits in force.
330
+ const [card, setCard] = useState(null);
331
+ const [cardText, setCardText] = useState({ tagline: "", bio: "" });
332
+ const [removeCard, setRemoveCard] = useState({ portrait: false, banner: false });
333
+ const [clearCard, setClearCard] = useState(false);
334
+ const pics = { portrait: useRef(null), banner: useRef(null) };
335
+ useEffect(() => {
336
+ let live = true;
337
+ // The card's fields stay disabled until this read is in (#1889 review: a draft typed meanwhile was overwritten);
338
+ // a failed read lets them be edited, starting empty.
339
+ const none = { mine: { tagline: null, bio: null, portrait: null, banner: null }, limits: null };
340
+ lease.fetch(`/api/card/${encodeURIComponent(it.instance_name)}`).then((r) => (r.ok ? r.json() : null)).then((body) => {
341
+ if (!live || !lease.current()) return;
342
+ if (!body) { setCard(none); return; }
343
+ const resolved = checkCard(it.instance_name, body);
344
+ const mine = (k) => (resolved && resolved[k] && resolved[k].by === "human" ? resolved[k] : null);
345
+ setCard({ mine: { tagline: mine("tagline"), bio: mine("bio"), portrait: mine("portrait"), banner: mine("banner") }, limits: body.limits || null });
346
+ setCardText({ tagline: mine("tagline") ? mine("tagline").text : "", bio: mine("bio") ? mine("bio").text : "" });
347
+ }).catch(() => { if (live && lease.current()) setCard(none); });
348
+ return () => { live = false; };
349
+ }, []);
318
350
  async function save() {
319
351
  if (busy) return;
320
352
  setBusy(true); setMsg(null);
@@ -323,7 +355,28 @@ function EditProfile({ it, onClose }) {
323
355
  const name = encodeURIComponent(it.instance_name);
324
356
  const draft = { display_name: f.display, role: f.role, description: f.desc };
325
357
  const picked = file.current && file.current.files && file.current.files[0];
358
+ const removing = removePic;
359
+ // The card: Clear card wins over everything else in it; otherwise only the fields that changed are sent.
360
+ const clearing = clearCard;
361
+ const before = card && card.mine ? { tagline: card.mine.tagline ? card.mine.tagline.text : "", bio: card.mine.bio ? card.mine.bio.text : "" } : { tagline: "", bio: "" };
362
+ if (!clearing) for (const k of ["tagline", "bio"]) if (cardText[k] !== before[k]) draft[`card_${k}`] = cardText[k];
363
+ const cardPics = {};
326
364
  try {
365
+ // A picture too large is fitted first (after asking) — before anything is written, so declining writes nothing.
366
+ if (!clearing) for (const slot of ["portrait", "banner"]) {
367
+ const chosen = pics[slot].current && pics[slot].current.files && pics[slot].current.files[0];
368
+ if (chosen) {
369
+ const limit = card && card.limits && card.limits[slot];
370
+ let blob = chosen;
371
+ if (limit) {
372
+ try { blob = await fitPicture(chosen, limit, (w, h, nw, nh) => confirmDialog({ message: tn("resizeAsk", { w, h, nw, nh }), confirmLabel: tn("resizeGo") })); }
373
+ catch { throw new Error(tn("pictureTooLarge")); }
374
+ }
375
+ if (!lease.current()) return;
376
+ if (!blob) { setBusy(false); return; }
377
+ cardPics[slot] = blob;
378
+ } else if (removeCard[slot]) cardPics[slot] = "remove";
379
+ }
327
380
  const r = await fetch(`/api/profile/${name}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(draft) });
328
381
  if (!lease.current()) return;
329
382
  if (r.status === 401) throw new Error(tn("signInToSave"));
@@ -331,6 +384,21 @@ function EditProfile({ it, onClose }) {
331
384
  if (picked) {
332
385
  const ar = await fetch(`/api/avatar/${name}`, { method: "POST", headers: { "Content-Type": picked.type }, body: picked });
333
386
  if (!ar.ok) { const e = await ar.json().catch(() => ({})); throw new Error(tn("avatarFailed", { error: e.error || ar.status })); }
387
+ } else if (removing) { // a newly picked file replaces it instead
388
+ const dr = await fetch(`/api/avatar/${name}`, { method: "DELETE" });
389
+ if (!dr.ok) { const e = await dr.json().catch(() => ({})); throw new Error(tn("avatarRemoveFailed", { error: e.error || dr.status })); }
390
+ }
391
+ if (!lease.current()) return;
392
+ if (clearing) {
393
+ const cr = await fetch(`/api/card/${name}`, { method: "DELETE" });
394
+ if (!cr.ok) { const e = await cr.json().catch(() => ({})); throw new Error(tn("cardFailed", { error: e.error || cr.status })); }
395
+ }
396
+ for (const [slot, what] of Object.entries(cardPics)) {
397
+ if (!lease.current()) return;
398
+ const pr = what === "remove"
399
+ ? await fetch(`/api/card/${name}/${slot}`, { method: "DELETE" })
400
+ : await fetch(`/api/card/${name}/${slot}`, { method: "POST", headers: { "Content-Type": what.type || "application/octet-stream" }, body: what });
401
+ if (!pr.ok) { const e = await pr.json().catch(() => ({})); throw new Error(tn("cardFailed", { error: e.detail || e.error || pr.status })); }
334
402
  }
335
403
  if (!lease.current()) return;
336
404
  setBusy(false);
@@ -345,6 +413,22 @@ function EditProfile({ it, onClose }) {
345
413
  <label class="field"><span>${tn("role")}</span><input value=${f.role} onInput=${(e) => setF({ ...f, role: e.target.value })} /></label>
346
414
  <label class="field"><span>${tn("description")}</span><textarea rows="3" value=${f.desc} onInput=${(e) => setF({ ...f, desc: e.target.value })}></textarea></label>
347
415
  <label class="field"><span>${tn("avatar")}</span><input ref=${file} type="file" accept="image/png,image/jpeg,image/gif,image/webp" /></label>
416
+ ${it.has_avatar ? html`<div class="field-row"><button type="button" class="btn btn-sm" aria-pressed=${removePic ? "true" : "false"}
417
+ onClick=${() => setRemovePic(!removePic)}>${tn(removePic ? "keepPicture" : "removePicture")}</button>${removePic ? html`<span class="muted">${tn("removePictureNote")}</span>` : null}</div>` : null}
418
+ <div class="field-row"><button type="button" class="btn btn-sm" onClick=${() => openColorDialog(it.instance_name, (appStore.get().instances || []).find((i) => i.name === it.instance_name))}>${t("color.menu")}</button></div>
419
+ <h3 class="form-sub">${tn("cardSection")}</h3>
420
+ <p class="note">${tn("cardLead")}</p>
421
+ <fieldset class="form" disabled=${clearCard || !card} aria-busy=${card ? "false" : "true"}>
422
+ <label class="field"><span>${tn("cardTagline")}</span><input value=${cardText.tagline} onInput=${(e) => setCardText({ ...cardText, tagline: e.target.value })} /></label>
423
+ <label class="field"><span>${tn("cardBio")}</span><textarea rows="4" value=${cardText.bio} onInput=${(e) => setCardText({ ...cardText, bio: e.target.value })}></textarea></label>
424
+ ${["portrait", "banner"].map((slot) => html`<div key=${slot} class="form">
425
+ <label class="field"><span>${tn(slot === "portrait" ? "cardPortrait" : "cardBanner")}</span><input ref=${pics[slot]} type="file" accept="image/png,image/jpeg,image/webp" /></label>
426
+ ${card && card.mine && card.mine[slot] ? html`<div class="field-row"><button type="button" class="btn btn-sm" aria-pressed=${removeCard[slot] ? "true" : "false"}
427
+ onClick=${() => setRemoveCard({ ...removeCard, [slot]: !removeCard[slot] })}>${tn(removeCard[slot] ? "cardKeep" : "cardRemove")}</button></div>` : null}
428
+ </div>`)}
429
+ </fieldset>
430
+ <div class="field-row"><button type="button" class="btn btn-sm" aria-pressed=${clearCard ? "true" : "false"} onClick=${() => setClearCard(!clearCard)}>${tn(clearCard ? "keepCard" : "clearCard")}</button>
431
+ ${clearCard ? html`<span class="muted">${tn("clearCardNote")}</span>` : null}</div>
348
432
  </fieldset></form>
349
433
  ${msg ? html`<p class="err" role="alert">${msg}${msg === tn("signInToSave") ? html` <a class="link" href=${signInHref()}>${t("app.signIn")}</a>` : null}</p>` : null}
350
434
  </${Dialog}>`;
@@ -11,10 +11,13 @@
11
11
  const $ = (id) => document.getElementById(id);
12
12
  const T = {
13
13
  en: {
14
- checking: "Checking your session…", title: "Sign in to AgEnD", lead: "Enter the one-time code from your chat channel.",
14
+ checking: "Checking your session…", title: "Sign in to AgEnD", lead: "Enter your sign-in code.",
15
15
  label: "One-time code", submit: "Sign in", working: "Signing in…",
16
- hint: "Send /web to your AgEnD bot, or run “agend web --code” on the host. A code works once and expires in 5 minutes.",
17
- refused: "That code did not work — it may be wrong, expired or already used. Ask for a new one with /web.",
16
+ hint: "No code? Send /web in General, then press a sign-in button to get one.",
17
+ nobot: "No chat bot?", nobot_body: "Ask the operator to run agend web --code on the AgEnD computer.",
18
+ safe: "Page looks wrong after signing in?", safe_body: "Your custom CSS may be hiding something. Add ?safe=1 to the end of the address to load the page without it, then fix it or turn it off in Settings → Web appearance.",
19
+ refused: "The code is invalid or no longer works. Send /web in General, then press a sign-in button for a new code.",
20
+ unconfirmed: "Sign-in could not be confirmed. Get a new code and try again; if it keeps failing, contact the operator.",
18
21
  paused: "Too many wrong codes — sign-in is paused for a few minutes.",
19
22
  network: "Could not reach AgEnD. Check the connection and try again.",
20
23
  resend: "Send me a new code", resent: "A new code was sent to you privately in chat. Enter it above.",
@@ -22,10 +25,13 @@
22
25
  resend_refused: "A new code could not be sent. Ask for one with /web.",
23
26
  },
24
27
  "zh-TW": {
25
- checking: "正在確認登入狀態…", title: "登入 AgEnD", lead: "請輸入聊天頻道給你的一次性登入碼。",
28
+ checking: "正在確認登入狀態…", title: "登入 AgEnD", lead: "輸入登入碼。",
26
29
  label: "一次性登入碼", submit: "登入", working: "登入中…",
27
- hint: "在 AgEnD bot 傳 /web,或在主機執行「agend web --code」。登入碼只能用一次,5 分鐘內有效。",
28
- refused: "登入碼無效:可能輸入錯誤、已過期或已使用。請用 /web 再要一個。",
30
+ hint: "沒收到?在 General 傳 /web,再按登入按鈕取碼。",
31
+ nobot: "沒有聊天 bot?", nobot_body: "請管理員在 AgEnD 電腦執行 agend web --code。",
32
+ safe: "登入後畫面不對勁?", safe_body: "可能是你的自訂 CSS 把東西藏起來了。在網址最後加上 ?safe=1,就會不套用它地載入;再到「設定 → 網頁外觀」修正或關閉。",
33
+ refused: "登入碼無效或已失效。請在 General 傳 /web,再按登入按鈕取新碼。",
34
+ unconfirmed: "登入暫時無法確認。請重新取碼再試;若仍失敗,請聯絡管理員。",
29
35
  paused: "錯誤次數過多,登入暫停幾分鐘。",
30
36
  network: "連不上 AgEnD,請檢查連線後再試。",
31
37
  resend: "傳一組新的登入碼給我", resent: "新的登入碼已私訊給你,請在上方輸入。",
@@ -47,8 +53,8 @@
47
53
  history.replaceState(null, "", here.pathname + here.search + here.hash);
48
54
  }
49
55
  } catch { /* nothing to clean */ }
50
- // The command in the hint is a <code> that never breaks across lines (a break inside "--code" reads as two dashes
51
- // and a word, alpha.2 sweep); the text around it is text.
56
+ // The command in the "No chat bot?" help is a <code> that never breaks across lines (a break inside "--code" reads as
57
+ // two dashes and a word, alpha.2 sweep); the text around it is text.
52
58
  const CMD = "agend web --code";
53
59
  document.querySelectorAll("[data-i18n]").forEach((el) => {
54
60
  const text = t(el.getAttribute("data-i18n"));
@@ -163,7 +169,9 @@
163
169
  body: JSON.stringify({ code: $("code").value }),
164
170
  });
165
171
  if (r.ok) { clearBounce(); location.replace(nextTarget()); return; }
166
- msg.textContent = r.status === 429 ? t("paused") : t("refused");
172
+ // #1854: a 5xx is the server failing to confirm the sign-in (503: a public link's notice did not go out), not a
173
+ // wrong code — say so. The code is spent either way: the way on is a new one.
174
+ msg.textContent = r.status === 429 ? t("paused") : r.status >= 500 ? t("unconfirmed") : t("refused");
167
175
  } catch {
168
176
  msg.textContent = t("network");
169
177
  }
@@ -35,7 +35,7 @@ export function Dialog({ title, onClose, actions, children, busy = false, wide =
35
35
  d.addEventListener("click", onClick);
36
36
  return () => { d.removeEventListener("cancel", onCancel); d.removeEventListener("click", onClick); };
37
37
  }, [onClose]);
38
- return html`<dialog ref=${ref} class=${`dlg${wide ? " dlg-wide" : ""}`} aria-label=${label || title}>
38
+ return html`<dialog data-trust="dialog" ref=${ref} class=${`dlg${wide ? " dlg-wide" : ""}`} aria-label=${label || title}>
39
39
  <div class="dlg-box">
40
40
  <div class="dlg-head"><h2 class="dlg-title">${title}</h2>
41
41
  <button type="button" class="icon-btn dlg-x" aria-label=${t("app.close")} title=${t("app.close")} onClick=${() => { if (!busyRef.current) onClose(); }}><${Icon} name="close" /></button></div>
@@ -54,6 +54,9 @@ const P = {
54
54
  panel: () => html`<rect x="3" y="4" width="18" height="16" rx="2"/><path d="M14 4v16"/>`,
55
55
  back: () => html`<path d="M19 12H5M12 19l-7-7 7-7"/>`,
56
56
  forward: () => html`<path d="M5 12h14M12 5l7 7-7 7"/>`,
57
+ // #1629: Settings → Appearance.
58
+ palette: () => html`<path d="M12 3a9 9 0 1 0 0 18c1.1 0 1.7-.8 1.7-1.7 0-.5-.2-.9-.5-1.2-.3-.3-.5-.7-.5-1.2 0-.9.8-1.7 1.7-1.7H16a5 5 0 0 0 5-5c0-4-4-7.2-9-7.2z"/><circle cx="7.5" cy="10.5" r="1"/><circle cx="10.5" cy="7" r="1"/><circle cx="15" cy="7.5" r="1"/>`,
59
+ book: () => html`<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20V3H6.5A2.5 2.5 0 0 0 4 5.5z"/><path d="M4 19.5A2.5 2.5 0 0 0 6.5 22H20v-5"/>`,
57
60
  external: () => html`<path d="M14 4h6v6M20 4l-9 9"/><path d="M18 14v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h5"/>`,
58
61
  };
59
62
 
@@ -31,7 +31,7 @@ export function Menu({ items, label, icon = "more", cls = "" }) {
31
31
  return html`<div class=${`menu ${cls}`}>
32
32
  <button ref=${btn} type="button" class="icon-btn" aria-haspopup="menu" aria-expanded=${open ? "true" : "false"}
33
33
  aria-label=${label || t("app.more")} title=${label || t("app.more")} onClick=${() => setOpen(!open)}><${Icon} name=${icon} /></button>
34
- ${open ? html`<div ref=${list} class="menu-list" role="menu" aria-label=${label || t("app.more")} onKeyDown=${onKey}>
34
+ ${open ? html`<div data-trust="menu" ref=${list} class="menu-list" role="menu" aria-label=${label || t("app.more")} onKeyDown=${onKey}>
35
35
  ${shown.map(item => html`<button key=${item.key} type="button" role="menuitem" class=${`menu-item${item.danger ? " danger" : ""}`}
36
36
  disabled=${!!item.disabled} onClick=${() => choose(item)}>${item.icon ? html`<${Icon} name=${item.icon} size=${16} />` : null}<span>${item.label}</span></button>`)}
37
37
  </div>` : null}
@@ -14,5 +14,5 @@ export function toast(text, ok = true) {
14
14
  export function Toasts() {
15
15
  const [list, setList] = useState(toasts.get());
16
16
  useEffect(() => toasts.subscribe(setList), []);
17
- return html`<div class="toasts" role="status" aria-live="polite">${list.map(x => html`<div key=${x.id} class=${`toast ${x.ok ? "ok" : "err"}`}>${x.text}</div>`)}</div>`;
17
+ return html`<div data-trust="toast" class="toasts" role="status" aria-live="polite">${list.map(x => html`<div key=${x.id} class=${`toast ${x.ok ? "ok" : "err"}`}>${x.text}</div>`)}</div>`;
18
18
  }
@@ -152,6 +152,25 @@ register("view", {
152
152
  "paneFailed": "⚠ Could not load pane for {name} (HTTP {status}).",
153
153
  "signInToSave": "Sign in to save — your session may have ended.",
154
154
  "avatarFailed": "Avatar upload failed: {error}",
155
+ "avatarRemoveFailed": "Could not remove the picture: {error}",
156
+ "removePicture": "Remove picture",
157
+ "keepPicture": "Keep picture",
158
+ "removePictureNote": "Removed when you save. The bot's avatar, the persona emoji or the first letter shows instead.",
159
+ "cardSection": "Identity card",
160
+ "cardLead": "Yours wins over the agent's, field by field. Leave a field empty to show the agent's.",
161
+ "cardTagline": "Tagline (one line, up to 60 characters)",
162
+ "cardBio": "Bio (up to 280 characters, 3 line breaks)",
163
+ "cardPortrait": "Portrait",
164
+ "cardBanner": "Banner",
165
+ "cardRemove": "Remove yours",
166
+ "cardKeep": "Keep yours",
167
+ "clearCard": "Clear card",
168
+ "keepCard": "Keep card",
169
+ "clearCardNote": "Removed when you save: your tagline, bio, portrait and banner. The agent's own card shows instead.",
170
+ "resizeAsk": "This picture is {w}×{h}; it will be resized to {nw}×{nh}.",
171
+ "resizeGo": "Resize",
172
+ "cardFailed": "Card not saved: {error}",
173
+ "pictureTooLarge": "This picture can't be made small enough to upload.",
155
174
  "saved": "Saved",
156
175
  "saveFailed": "Save failed: {error}",
157
176
  "signInToEdit": "Sign in to edit",
@@ -202,8 +221,8 @@ register("view", {
202
221
  "help1": "在清單裡選一個 instance:它的即時終端畫面會顯示在這裡。",
203
222
  "help2": "點群組名稱可以收合或展開。",
204
223
  "help3": "「編輯個人資料」可修改顯示名稱、角色、描述和頭像。",
205
- "help5": "「字級」會在剛好填滿 → 小 → 中 → 大之間切換。小、中、大是所有頁面共用的字級;剛好填滿讓終端畫面符合框的大小。",
206
- "densityFit": "剛好填滿",
224
+ "help5": "「字級」會在填滿 → 小 → 中 → 大之間切換。小、中、大是所有頁面共用的字級;填滿讓終端畫面符合框的大小。",
225
+ "densityFit": "填滿",
207
226
  "densityComfortable": "舒適",
208
227
  "densityCompact": "精簡",
209
228
  "densityLabel": "字級密度",
@@ -328,6 +347,25 @@ register("view", {
328
347
  "paneFailed": "⚠ 無法載入 {name} 的 pane(HTTP {status})。",
329
348
  "signInToSave": "請先登入才能儲存:你的 session 可能已結束。",
330
349
  "avatarFailed": "上傳頭像失敗:{error}",
350
+ "avatarRemoveFailed": "無法移除圖片:{error}",
351
+ "removePicture": "移除圖片",
352
+ "keepPicture": "保留圖片",
353
+ "removePictureNote": "儲存後移除,改顯示 Bot 的頭像、persona emoji 或名稱第一個字。",
354
+ "cardSection": "身分卡",
355
+ "cardLead": "你設定的欄位優先於 agent 的,逐欄比較。欄位留空就顯示 agent 的。",
356
+ "cardTagline": "標語(一行,最多 60 個字)",
357
+ "cardBio": "簡介(最多 280 個字、3 次換行)",
358
+ "cardPortrait": "肖像",
359
+ "cardBanner": "橫幅",
360
+ "cardRemove": "移除你的",
361
+ "cardKeep": "保留你的",
362
+ "clearCard": "清除卡片",
363
+ "keepCard": "保留卡片",
364
+ "clearCardNote": "儲存後移除你的標語、簡介、肖像與橫幅,改顯示 agent 自己的卡片。",
365
+ "resizeAsk": "這張圖片是 {w}×{h},會縮小為 {nw}×{nh}。",
366
+ "resizeGo": "縮小",
367
+ "cardFailed": "卡片未儲存:{error}",
368
+ "pictureTooLarge": "這張圖片無法縮到可上傳的大小。",
331
369
  "saved": "已儲存",
332
370
  "saveFailed": "儲存失敗:{error}",
333
371
  "signInToEdit": "登入後才能編輯",
@@ -32,7 +32,11 @@
32
32
  #msg { min-height: 1.5em; margin-top: 14px; font-size: var(--fs-sm); color: var(--danger); }
33
33
  #msg.ok { color: var(--text-2); }
34
34
  .hint { margin-top: 18px; font-size: var(--fs-xs); color: var(--text-3); }
35
- .hint code { white-space: nowrap; font-family: var(--font-mono); }
35
+ .hint code, .help code { white-space: nowrap; font-family: var(--font-mono); }
36
+ /* #1854: the operator's way in (no chat bot) — closed until asked for. */
37
+ .help { margin-top: 8px; font-size: var(--fs-xs); color: var(--text-3); }
38
+ .help summary { cursor: pointer; width: fit-content; }
39
+ .help p { margin: 6px 0 0; color: var(--text-3); }
36
40
  [hidden] { display: none !important; }
37
41
  </style>
38
42
  </head>
@@ -42,14 +46,16 @@
42
46
  <div id="checking" data-i18n="checking">Checking your session…</div>
43
47
  <form id="form" hidden autocomplete="off">
44
48
  <h1 data-i18n="title">Sign in to AgEnD</h1>
45
- <p data-i18n="lead">Enter the one-time code from your chat channel.</p>
49
+ <p data-i18n="lead">Enter your sign-in code.</p>
46
50
  <label for="code" data-i18n="label">One-time code</label>
47
51
  <input id="code" name="code" inputmode="text" autocomplete="one-time-code" autocapitalize="characters"
48
52
  autocorrect="off" spellcheck="false" maxlength="12" placeholder="XXXX-XXXX" required>
49
53
  <button id="go" type="submit" data-i18n="submit">Sign in</button>
50
54
  <button id="resend" type="button" class="secondary" hidden data-i18n="resend">Send me a new code</button>
51
55
  <div id="msg" role="alert" aria-live="polite"></div>
52
- <div class="hint" data-i18n="hint">Send /web to your AgEnD bot, or run “<code>agend web --code</code>” on the host. A code works once and expires in 5 minutes.</div>
56
+ <div class="hint" data-i18n="hint">No code? Send /web in General, then press a sign-in button to get one.</div>
57
+ <details class="help"><summary data-i18n="nobot">No chat bot?</summary><p data-i18n="nobot_body">Ask the operator to run <code>agend web --code</code> on the AgEnD computer.</p></details>
58
+ <details class="help"><summary data-i18n="safe">Page looks wrong after signing in?</summary><p data-i18n="safe_body">Your custom CSS may be hiding something. Add ?safe=1 to the end of the address to load the page without it, then fix it or turn it off in Settings → Appearance.</p></details>
53
59
  </form>
54
60
  <noscript>This page needs JavaScript to sign you in.</noscript>
55
61
  </main>
@@ -80,6 +80,37 @@ export declare function runningFleetPredatesInstall(opts: {
80
80
  isFleetProcess: (pid: number) => boolean;
81
81
  marginMs?: number;
82
82
  }): boolean;
83
+ /**
84
+ * #1829 review: what fleet.pid says, as evidence — "missing" only when the file is not there (ENOENT), "valid" for a
85
+ * whole positive pid, "unknown" for anything else (unreadable, a directory, empty, malformed). Unknown is never read
86
+ * as a stopped fleet.
87
+ */
88
+ export declare function readFleetPidEvidence(path: string, read?: (path: string) => string): {
89
+ kind: "missing";
90
+ } | {
91
+ kind: "valid";
92
+ pid: number;
93
+ } | {
94
+ kind: "unknown";
95
+ };
96
+ /** The largest pid `process.kill` accepts (a signed 32-bit integer). */
97
+ export declare const MAX_PID = 2147483647;
98
+ /**
99
+ * Whether `pid` may be alive: only ESRCH ("no such process") proves it gone. EPERM is a live process of another
100
+ * user, and any other failure (an argument the call refused, for one) proves nothing — so: alive.
101
+ */
102
+ export declare function pidAlive(pid: number, kill?: (pid: number, signal: 0) => unknown): boolean;
103
+ /**
104
+ * #1829 ⑥: whether the fleet is PROVEN not running — no fleet.pid (a fleet removes it when it stops), a pid that no
105
+ * longer exists, or a live pid whose readable command line is not a fleet's (a recycled pid). A command line that
106
+ * cannot be read proves nothing either way: not stopped.
107
+ */
108
+ export declare function fleetProvenStopped(opts: {
109
+ pid: number | null;
110
+ alive: (pid: number) => boolean;
111
+ commandLine: (pid: number) => string;
112
+ isFleetProcess: (commandLine: string) => boolean;
113
+ }): boolean;
83
114
  /** Start time of a live process, via `ps -o lstart=` (Linux and macOS); null if gone. */
84
115
  export declare function processStartMs(pid: number, run?: (args: string[]) => string): number | null;
85
116
  interface UpdateOutput {
@@ -1,4 +1,5 @@
1
1
  import { execFileSync } from "node:child_process";
2
+ import { readFileSync } from "node:fs";
2
3
  import { SYSTEMD_RESTART_INDETERMINATE_EXIT_CODE } from "./service-restart-selection.js";
3
4
  /**
4
5
  * What a chat `/update` (Discord slash or Telegram) runs: no channel flag, ever.
@@ -160,6 +161,52 @@ export function runningFleetPredatesInstall(opts) {
160
161
  return false;
161
162
  return started < opts.installedAtMs - (opts.marginMs ?? 2_000);
162
163
  }
164
+ /**
165
+ * #1829 review: what fleet.pid says, as evidence — "missing" only when the file is not there (ENOENT), "valid" for a
166
+ * whole positive pid, "unknown" for anything else (unreadable, a directory, empty, malformed). Unknown is never read
167
+ * as a stopped fleet.
168
+ */
169
+ export function readFleetPidEvidence(path, read = p => readFileSync(p, "utf-8")) {
170
+ let raw;
171
+ try {
172
+ raw = read(path);
173
+ }
174
+ catch (err) {
175
+ return err?.code === "ENOENT" ? { kind: "missing" } : { kind: "unknown" };
176
+ }
177
+ const text = raw.trim();
178
+ const pid = /^[1-9][0-9]{0,9}$/.test(text) ? Number(text) : Number.NaN;
179
+ // process.kill takes a signed 32-bit pid (review): anything above is not a pid it can ask about.
180
+ return pid <= MAX_PID ? { kind: "valid", pid } : { kind: "unknown" };
181
+ }
182
+ /** The largest pid `process.kill` accepts (a signed 32-bit integer). */
183
+ export const MAX_PID = 2_147_483_647;
184
+ /**
185
+ * Whether `pid` may be alive: only ESRCH ("no such process") proves it gone. EPERM is a live process of another
186
+ * user, and any other failure (an argument the call refused, for one) proves nothing — so: alive.
187
+ */
188
+ export function pidAlive(pid, kill = (p, s) => process.kill(p, s)) {
189
+ try {
190
+ kill(pid, 0);
191
+ return true;
192
+ }
193
+ catch (err) {
194
+ return err?.code !== "ESRCH";
195
+ }
196
+ }
197
+ /**
198
+ * #1829 ⑥: whether the fleet is PROVEN not running — no fleet.pid (a fleet removes it when it stops), a pid that no
199
+ * longer exists, or a live pid whose readable command line is not a fleet's (a recycled pid). A command line that
200
+ * cannot be read proves nothing either way: not stopped.
201
+ */
202
+ export function fleetProvenStopped(opts) {
203
+ if (opts.pid === null)
204
+ return true;
205
+ if (!opts.alive(opts.pid))
206
+ return true;
207
+ const line = opts.commandLine(opts.pid);
208
+ return line !== "" && !opts.isFleetProcess(line);
209
+ }
163
210
  /** Start time of a live process, via `ps -o lstart=` (Linux and macOS); null if gone. */
164
211
  export function processStartMs(pid, run = args => execFileSync("ps", args, { encoding: "utf8", env: { ...process.env, LC_ALL: "C", TZ: process.env.TZ }, stdio: ["ignore", "pipe", "ignore"], timeout: 5000 })) {
165
212
  try {