@songsid/agend 2.3.0-beta.1 → 2.3.0-beta.11

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 (357) hide show
  1. package/dist/agent-cli-instructions.md +5 -2
  2. package/dist/agent-cli.js +43 -7
  3. package/dist/agent-cli.js.map +1 -1
  4. package/dist/agent-endpoint.js +4 -0
  5. package/dist/agent-endpoint.js.map +1 -1
  6. package/dist/agent-file-read.d.ts +14 -6
  7. package/dist/agent-file-read.js +207 -19
  8. package/dist/agent-file-read.js.map +1 -1
  9. package/dist/agent-whats-new.d.ts +11 -0
  10. package/dist/agent-whats-new.js +29 -15
  11. package/dist/agent-whats-new.js.map +1 -1
  12. package/dist/auth-api.d.ts +30 -1
  13. package/dist/auth-api.js +45 -13
  14. package/dist/auth-api.js.map +1 -1
  15. package/dist/authority/authority-state.d.ts +38 -0
  16. package/dist/authority/authority-state.js +114 -0
  17. package/dist/authority/authority-state.js.map +1 -0
  18. package/dist/authority/session-authorizations.d.ts +43 -0
  19. package/dist/authority/session-authorizations.js +102 -0
  20. package/dist/authority/session-authorizations.js.map +1 -0
  21. package/dist/authority/web-authority.d.ts +120 -0
  22. package/dist/authority/web-authority.js +157 -0
  23. package/dist/authority/web-authority.js.map +1 -0
  24. package/dist/backend/claude-code.d.ts +2 -2
  25. package/dist/backend/claude-code.js +10 -5
  26. package/dist/backend/claude-code.js.map +1 -1
  27. package/dist/backend/types.d.ts +14 -0
  28. package/dist/backend/types.js +25 -0
  29. package/dist/backend/types.js.map +1 -1
  30. package/dist/backend-availability.d.ts +120 -0
  31. package/dist/backend-availability.js +396 -0
  32. package/dist/backend-availability.js.map +1 -0
  33. package/dist/backend-command.d.ts +21 -3
  34. package/dist/backend-command.js +23 -4
  35. package/dist/backend-command.js.map +1 -1
  36. package/dist/bundled-skills.d.ts +14 -0
  37. package/dist/bundled-skills.js +29 -0
  38. package/dist/bundled-skills.js.map +1 -0
  39. package/dist/cache-analysis.d.ts +47 -1
  40. package/dist/cache-analysis.js +68 -8
  41. package/dist/cache-analysis.js.map +1 -1
  42. package/dist/cache-ledger.d.ts +16 -3
  43. package/dist/cache-ledger.js +33 -13
  44. package/dist/cache-ledger.js.map +1 -1
  45. package/dist/cache-report-tool.d.ts +17 -0
  46. package/dist/cache-report-tool.js +40 -0
  47. package/dist/cache-report-tool.js.map +1 -0
  48. package/dist/cache-service.d.ts +70 -3
  49. package/dist/cache-service.js +178 -17
  50. package/dist/cache-service.js.map +1 -1
  51. package/dist/channel/adapters/discord.d.ts +7 -0
  52. package/dist/channel/adapters/discord.js +35 -0
  53. package/dist/channel/adapters/discord.js.map +1 -1
  54. package/dist/channel/mcp-tools.js +22 -10
  55. package/dist/channel/mcp-tools.js.map +1 -1
  56. package/dist/channel/types.d.ts +6 -0
  57. package/dist/classic-channel-manager.d.ts +10 -5
  58. package/dist/classic-channel-manager.js +12 -9
  59. package/dist/classic-channel-manager.js.map +1 -1
  60. package/dist/cli.js +130 -31
  61. package/dist/cli.js.map +1 -1
  62. package/dist/config-validator.js +103 -4
  63. package/dist/config-validator.js.map +1 -1
  64. package/dist/daemon.d.ts +7 -0
  65. package/dist/daemon.js +63 -16
  66. package/dist/daemon.js.map +1 -1
  67. package/dist/decision-list.d.ts +25 -0
  68. package/dist/decision-list.js +30 -0
  69. package/dist/decision-list.js.map +1 -0
  70. package/dist/delivery-outbox.d.ts +10 -0
  71. package/dist/delivery-outbox.js +12 -1
  72. package/dist/delivery-outbox.js.map +1 -1
  73. package/dist/fleet-context.d.ts +10 -0
  74. package/dist/fleet-manager.d.ts +417 -11
  75. package/dist/fleet-manager.js +1387 -109
  76. package/dist/fleet-manager.js.map +1 -1
  77. package/dist/fleet-traffic.d.ts +74 -0
  78. package/dist/fleet-traffic.js +196 -0
  79. package/dist/fleet-traffic.js.map +1 -0
  80. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +20 -5
  81. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +6 -1
  82. package/dist/general-knowledge/skills/multi-channel/SKILL.md +3 -2
  83. package/dist/general-knowledge/skills/persona-emoji/SKILL.md +1 -1
  84. package/dist/general-knowledge/skills/web-appearance/SKILL.md +257 -23
  85. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +478 -0
  86. package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +1 -1
  87. package/dist/human-delivery-receipts.d.ts +24 -1
  88. package/dist/human-delivery-receipts.js +64 -11
  89. package/dist/human-delivery-receipts.js.map +1 -1
  90. package/dist/instance-config-impact.js +3 -1
  91. package/dist/instance-config-impact.js.map +1 -1
  92. package/dist/instance-names.d.ts +4 -0
  93. package/dist/instance-names.js +12 -0
  94. package/dist/instance-names.js.map +1 -0
  95. package/dist/instructions.d.ts +12 -1
  96. package/dist/instructions.js +13 -2
  97. package/dist/instructions.js.map +1 -1
  98. package/dist/locale.js +100 -8
  99. package/dist/locale.js.map +1 -1
  100. package/dist/ls-attach-resolve.d.ts +95 -0
  101. package/dist/ls-attach-resolve.js +174 -0
  102. package/dist/ls-attach-resolve.js.map +1 -0
  103. package/dist/memory-hogs.d.ts +27 -0
  104. package/dist/memory-hogs.js +124 -0
  105. package/dist/memory-hogs.js.map +1 -0
  106. package/dist/outbound-handlers.d.ts +10 -0
  107. package/dist/outbound-handlers.js +122 -2
  108. package/dist/outbound-handlers.js.map +1 -1
  109. package/dist/outbound-schemas.d.ts +164 -4
  110. package/dist/outbound-schemas.js +99 -3
  111. package/dist/outbound-schemas.js.map +1 -1
  112. package/dist/public-web-gateway.js +8 -2
  113. package/dist/public-web-gateway.js.map +1 -1
  114. package/dist/public-web-link.d.ts +9 -0
  115. package/dist/public-web-link.js +8 -0
  116. package/dist/public-web-link.js.map +1 -1
  117. package/dist/quickstart-api.d.ts +10 -1
  118. package/dist/quickstart-api.js +12 -4
  119. package/dist/quickstart-api.js.map +1 -1
  120. package/dist/reply-buttons-controller.d.ts +4 -0
  121. package/dist/reply-buttons-controller.js +2 -2
  122. package/dist/reply-buttons-controller.js.map +1 -1
  123. package/dist/restart-progress.d.ts +65 -1
  124. package/dist/restart-progress.js +236 -10
  125. package/dist/restart-progress.js.map +1 -1
  126. package/dist/rollout-index.d.ts +5 -0
  127. package/dist/rollout-index.js +26 -5
  128. package/dist/rollout-index.js.map +1 -1
  129. package/dist/scheduler/scheduler.d.ts +12 -0
  130. package/dist/scheduler/scheduler.js +83 -10
  131. package/dist/scheduler/scheduler.js.map +1 -1
  132. package/dist/service-installer.d.ts +2 -1
  133. package/dist/service-installer.js +18 -3
  134. package/dist/service-installer.js.map +1 -1
  135. package/dist/settings-api.d.ts +19 -0
  136. package/dist/settings-api.js +137 -2
  137. package/dist/settings-api.js.map +1 -1
  138. package/dist/settings-change.js +104 -0
  139. package/dist/settings-change.js.map +1 -1
  140. package/dist/settings-effect.js +17 -1
  141. package/dist/settings-effect.js.map +1 -1
  142. package/dist/tmux-manager.d.ts +5 -0
  143. package/dist/tmux-manager.js +16 -0
  144. package/dist/tmux-manager.js.map +1 -1
  145. package/dist/tool-permissions.d.ts +4 -6
  146. package/dist/tool-permissions.js +53 -5
  147. package/dist/tool-permissions.js.map +1 -1
  148. package/dist/topic-commands.d.ts +7 -1
  149. package/dist/topic-commands.js +18 -4
  150. package/dist/topic-commands.js.map +1 -1
  151. package/dist/types.d.ts +48 -1
  152. package/dist/ui/app-effects.js +77 -31
  153. package/dist/ui/chat-ambient.js +307 -0
  154. package/dist/ui/chat-effect-posters.js +8 -8
  155. package/dist/ui/chat-effects.js +101 -13
  156. package/dist/ui/chat-render.js +21 -3
  157. package/dist/ui/chat-strings.js +59 -6
  158. package/dist/ui/chat-thread.js +159 -21
  159. package/dist/ui/effect-trial-controller.js +13 -8
  160. package/dist/ui/effect-trial-panel.js +19 -16
  161. package/dist/ui/fleet-cache.js +23 -1
  162. package/dist/ui/fleet-canvas-layout.js +111 -0
  163. package/dist/ui/fleet-canvas.js +525 -0
  164. package/dist/ui/fleet-org.js +27 -12
  165. package/dist/ui/fleet-traffic.js +343 -0
  166. package/dist/ui/fleet-usage.js +401 -0
  167. package/dist/ui/panel-chat.js +154 -49
  168. package/dist/ui/panel-details.js +2 -1
  169. package/dist/ui/panel-fleet.js +43 -17
  170. package/dist/ui/panel-settings.js +26 -8
  171. package/dist/ui/preview.js +3 -2
  172. package/dist/ui/settings-appearance.js +42 -18
  173. package/dist/ui/settings-catalog.js +92 -0
  174. package/dist/ui/settings-dialogs.js +40 -12
  175. package/dist/ui/settings-effects.js +4 -3
  176. package/dist/ui/settings-guests.js +117 -0
  177. package/dist/ui/settings-learn.js +25 -0
  178. package/dist/ui/settings-model.js +15 -6
  179. package/dist/ui/settings-mods.js +23 -9
  180. package/dist/ui/settings-owner-effects.js +9 -9
  181. package/dist/ui/settings-strings.js +120 -42
  182. package/dist/ui/settings-web-authority.js +91 -0
  183. package/dist/ui/shared/agend-auth.js +1 -1
  184. package/dist/ui/shared/app-i18n.js +4 -2
  185. package/dist/ui/shared/app-prefs.js +5 -4
  186. package/dist/ui/shared/app-route.js +19 -1
  187. package/dist/ui/shared/app-session.js +2 -0
  188. package/dist/ui/shared/app-shell.js +92 -27
  189. package/dist/ui/shared/app-store.js +26 -1
  190. package/dist/ui/shared/app-stream.js +3 -1
  191. package/dist/ui/shared/app-themes.js +24 -6
  192. package/dist/ui/shared/app.css +365 -17
  193. package/dist/ui/shared/app.js +21 -14
  194. package/dist/ui/shared/card-strings.js +4 -0
  195. package/dist/ui/shared/effect-app-runtime.js +68 -19
  196. package/dist/ui/shared/effect-chat-runtime.js +16 -3
  197. package/dist/ui/shared/effect-frame.js +26 -7
  198. package/dist/ui/shared/effect-geometry.js +24 -9
  199. package/dist/ui/shared/effect-policy.js +21 -7
  200. package/dist/ui/shared/effect-profiler.js +31 -9
  201. package/dist/ui/shared/effect-registry.js +87 -48
  202. package/dist/ui/shared/effect-stock-snapshot.js +9 -0
  203. package/dist/ui/shared/effect-stock-trust.js +62 -10
  204. package/dist/ui/shared/effect-trial-shadow.js +62 -0
  205. package/dist/ui/shared/effect-trial-source.js +1 -1
  206. package/dist/ui/shared/effect-trial-stage.js +54 -31
  207. package/dist/ui/shared/effect-trial.css +40 -0
  208. package/dist/ui/shared/fx-motion.js +67 -0
  209. package/dist/ui/shared/identity-card.js +75 -4
  210. package/dist/ui/shared/identity-color.js +3 -3
  211. package/dist/ui/shared/instance-nav.js +194 -28
  212. package/dist/ui/shared/panel-view.js +24 -4
  213. package/dist/ui/shared/tokens.css +9 -3
  214. package/dist/ui/shared/ui-icons.js +1 -0
  215. package/dist/ui/shared/usage-dialog.js +22 -2
  216. package/dist/ui/shared/view-strings.js +68 -2
  217. package/dist/ui/shared/web-authorize.js +151 -0
  218. package/dist/ui/signin.html +1 -1
  219. package/dist/ui/web-terminal/terminal.css +4 -4
  220. package/dist/usage/providers.d.ts +28 -0
  221. package/dist/usage/providers.js +126 -36
  222. package/dist/usage/providers.js.map +1 -1
  223. package/dist/usage-report.d.ts +158 -0
  224. package/dist/usage-report.js +226 -0
  225. package/dist/usage-report.js.map +1 -0
  226. package/dist/usage-rollup.d.ts +51 -0
  227. package/dist/usage-rollup.js +168 -0
  228. package/dist/usage-rollup.js.map +1 -0
  229. package/dist/view-api.d.ts +39 -0
  230. package/dist/view-api.js +65 -4
  231. package/dist/view-api.js.map +1 -1
  232. package/dist/web-ambient.d.ts +72 -0
  233. package/dist/web-ambient.js +161 -0
  234. package/dist/web-ambient.js.map +1 -0
  235. package/dist/web-api.d.ts +69 -5
  236. package/dist/web-api.js +362 -45
  237. package/dist/web-api.js.map +1 -1
  238. package/dist/web-appearance-policy.d.ts +4 -6
  239. package/dist/web-appearance-policy.js +5 -12
  240. package/dist/web-appearance-policy.js.map +1 -1
  241. package/dist/web-appearance-tools.d.ts +3 -0
  242. package/dist/web-appearance-tools.js +22 -3
  243. package/dist/web-appearance-tools.js.map +1 -1
  244. package/dist/web-appearance-view.d.ts +27 -2
  245. package/dist/web-appearance-view.js +129 -14
  246. package/dist/web-appearance-view.js.map +1 -1
  247. package/dist/web-appearance.d.ts +17 -3
  248. package/dist/web-appearance.js +82 -7
  249. package/dist/web-appearance.js.map +1 -1
  250. package/dist/web-asset-parts.d.ts +5 -10
  251. package/dist/web-asset-parts.js +3 -3
  252. package/dist/web-asset-parts.js.map +1 -1
  253. package/dist/web-auth.js +3 -1
  254. package/dist/web-auth.js.map +1 -1
  255. package/dist/web-card.d.ts +14 -0
  256. package/dist/web-card.js +40 -6
  257. package/dist/web-card.js.map +1 -1
  258. package/dist/web-channel-echo.d.ts +7 -1
  259. package/dist/web-channel-echo.js +6 -2
  260. package/dist/web-channel-echo.js.map +1 -1
  261. package/dist/web-chat-history.d.ts +50 -0
  262. package/dist/web-chat-history.js +87 -5
  263. package/dist/web-chat-history.js.map +1 -1
  264. package/dist/web-commands.d.ts +1 -0
  265. package/dist/web-effect-bootstrap.d.ts +12 -0
  266. package/dist/web-effect-bootstrap.js +49 -0
  267. package/dist/web-effect-bootstrap.js.map +1 -0
  268. package/dist/web-effect-bundle.js +46 -1
  269. package/dist/web-effect-bundle.js.map +1 -1
  270. package/dist/web-effect-playback.d.ts +30 -0
  271. package/dist/web-effect-playback.js +71 -0
  272. package/dist/web-effect-playback.js.map +1 -0
  273. package/dist/web-effect-profiles.d.ts +4 -4
  274. package/dist/web-effect-service.d.ts +11 -4
  275. package/dist/web-effect-service.js +28 -5
  276. package/dist/web-effect-service.js.map +1 -1
  277. package/dist/web-effect-tools.d.ts +1 -0
  278. package/dist/web-effect-tools.js +8 -5
  279. package/dist/web-effect-tools.js.map +1 -1
  280. package/dist/web-effect-trials.d.ts +4 -4
  281. package/dist/web-effect-trials.js +6 -6
  282. package/dist/web-effect-trials.js.map +1 -1
  283. package/dist/web-effect-upload.d.ts +2 -4
  284. package/dist/web-effect-upload.js +3 -8
  285. package/dist/web-effect-upload.js.map +1 -1
  286. package/dist/web-effect-worker.d.ts +1 -1
  287. package/dist/web-effect-worker.js +2 -2
  288. package/dist/web-effect-worker.js.map +1 -1
  289. package/dist/web-effect.d.ts +14 -0
  290. package/dist/web-effect.js +21 -0
  291. package/dist/web-effect.js.map +1 -1
  292. package/dist/web-guest-access.d.ts +125 -0
  293. package/dist/web-guest-access.js +234 -0
  294. package/dist/web-guest-access.js.map +1 -0
  295. package/dist/web-guest-rooms.d.ts +82 -0
  296. package/dist/web-guest-rooms.js +135 -0
  297. package/dist/web-guest-rooms.js.map +1 -0
  298. package/dist/web-identity-color.d.ts +1 -1
  299. package/dist/web-identity-color.js +2 -2
  300. package/dist/web-identity-color.js.map +1 -1
  301. package/dist/web-identity-palette.d.ts +1 -1
  302. package/dist/web-identity-palette.js +2 -2
  303. package/dist/web-identity-palette.js.map +1 -1
  304. package/dist/web-image-meta.d.ts +13 -0
  305. package/dist/web-image-meta.js +62 -0
  306. package/dist/web-image-meta.js.map +1 -1
  307. package/dist/web-login.d.ts +47 -0
  308. package/dist/web-login.js +115 -0
  309. package/dist/web-login.js.map +1 -1
  310. package/dist/web-mod-agent.d.ts +1 -1
  311. package/dist/web-mod-agent.js +12 -8
  312. package/dist/web-mod-agent.js.map +1 -1
  313. package/dist/web-mod-bundle.d.ts +14 -1
  314. package/dist/web-mod-bundle.js +68 -3
  315. package/dist/web-mod-bundle.js.map +1 -1
  316. package/dist/web-mod-export.d.ts +9 -0
  317. package/dist/web-mod-export.js +21 -0
  318. package/dist/web-mod-export.js.map +1 -1
  319. package/dist/web-mod-store.d.ts +7 -1
  320. package/dist/web-mod-store.js +21 -7
  321. package/dist/web-mod-store.js.map +1 -1
  322. package/dist/web-prefs.d.ts +14 -0
  323. package/dist/web-prefs.js +24 -4
  324. package/dist/web-prefs.js.map +1 -1
  325. package/dist/web-preview.d.ts +6 -2
  326. package/dist/web-preview.js +4 -2
  327. package/dist/web-preview.js.map +1 -1
  328. package/dist/web-returning-device.d.ts +41 -0
  329. package/dist/web-returning-device.js +57 -5
  330. package/dist/web-returning-device.js.map +1 -1
  331. package/dist/web-session-authorization.d.ts +132 -0
  332. package/dist/web-session-authorization.js +193 -0
  333. package/dist/web-session-authorization.js.map +1 -0
  334. package/dist/web-session.d.ts +70 -10
  335. package/dist/web-session.js +137 -22
  336. package/dist/web-session.js.map +1 -1
  337. package/dist/web-settings-patch.d.ts +49 -2
  338. package/dist/web-settings-patch.js +145 -1
  339. package/dist/web-settings-patch.js.map +1 -1
  340. package/dist/web-settings-tools.d.ts +89 -0
  341. package/dist/web-settings-tools.js +270 -0
  342. package/dist/web-settings-tools.js.map +1 -0
  343. package/dist/web-shell-routes.d.ts +1 -1
  344. package/dist/web-shell-routes.js +1 -1
  345. package/dist/web-shell-routes.js.map +1 -1
  346. package/dist/web-theme-store.d.ts +39 -0
  347. package/dist/web-theme-store.js +122 -11
  348. package/dist/web-theme-store.js.map +1 -1
  349. package/dist/web-theme.d.ts +2 -0
  350. package/dist/web-theme.js +10 -1
  351. package/dist/web-theme.js.map +1 -1
  352. package/dist/web-upload.d.ts +13 -0
  353. package/dist/web-upload.js +39 -3
  354. package/dist/web-upload.js.map +1 -1
  355. package/dist/workflow-templates/default.md +1 -1
  356. package/package.json +1 -1
  357. package/templates/systemd.service.ejs +8 -0
@@ -6,11 +6,12 @@
6
6
  // right, the agent's at full width, and one rounded composer pinned below with attach, Stop reply and Send inside it.
7
7
  // The instance's own actions (details, start, stop, restart, delete) are in the header's ⋯ menu, and only the ones
8
8
  // that apply to its state are offered (#1408 rough edges 4 and 7).
9
- import { html, useEffect, useLayoutEffect, useRef, useState } from "/assets/app-html.js";
10
- import { t } from "/assets/app-i18n.js";
11
- import { appStore, useStore } from "/assets/app-store.js";
9
+ import { html, useCallback, useEffect, useLayoutEffect, useRef, useState } from "/assets/app-html.js";
10
+ import { lang, t } from "/assets/app-i18n.js";
11
+ import { appStore, guestPage, useStore } from "/assets/app-store.js";
12
+ import { AuthorizeChip } from "/assets/web-authorize.js";
12
13
  import { useLease } from "/assets/app-ctx.js";
13
- import { namesOf, namesStore } from "/assets/instance-nav.js";
14
+ import { modelText, namesOf, namesStore, navStore as instanceNavStore } from "/assets/instance-nav.js";
14
15
  import { PanelHeader, onPanelKey, setTitle, statusClass, statusLabel, openDrawer, requestNewInstance } from "/assets/app-shell.js";
15
16
  import { detailsPath, textLinkSegments } from "/assets/app-route.js";
16
17
  import { InstanceSwitch } from "/assets/instance-switch.js";
@@ -23,7 +24,7 @@ import { Icon } from "/assets/ui-icons.js";
23
24
  import { toast } from "/assets/ui-toast.js";
24
25
  import { confirmDialog } from "/assets/ui-confirm.js";
25
26
  import { savePref } from "/assets/app-prefs.js";
26
- import { applyTheme, resolvedMode, themeFor, themeImageUrl, themeMenuItems, themesStore } from "/assets/app-themes.js";
27
+ import { applyTheme, clearAmbient, resolvedMode, themeFor, themeImageUrl, themeMenuItems, themesStore } from "/assets/app-themes.js";
27
28
  import { colorMenuItem } from "/assets/identity-color.js";
28
29
  import { IdentityCard, useCard } from "/assets/identity-card.js";
29
30
  import { AgentWidget, instanceBinds, widgetFor } from "/assets/agent-widget.js";
@@ -56,6 +57,8 @@ import { installTour, refreshTourSpot } from "./chat-tour.js";
56
57
  import { LightboxHost, openLightbox } from "./image-lightbox.js";
57
58
  import { ChatEffectPosters } from "/ui/js/chat-effect-posters.js";
58
59
  import { ChatEffects, chatEffectsMenuAvailable } from "./chat-effects.js";
60
+ import { setFollowsReducedMotion } from "/assets/fx-motion.js";
61
+ import { AmbientMotionNote, ChatAmbient, ambientMenuItems } from "./chat-ambient.js";
59
62
  import { startAppEffects, getAppEffects } from "./app-effects.js";
60
63
 
61
64
  const R = () => globalThis.AgendChatRender;
@@ -116,6 +119,7 @@ export function boot({ stream, boot: bootData, deps = {} }) {
116
119
  }
117
120
  // #1604: the preview opt-in and Tour live in Settings → This device now (and on each preview card's menu).
118
121
  installTour();
122
+ // #2118: viewing current in-scope Chat effects needs no management or personal-slot authority.
119
123
  startAppEffects({ store });
120
124
  return store;
121
125
  }
@@ -222,6 +226,9 @@ function ChatView({ name, inst, lease, navKey, exec, awaiting }) {
222
226
  const themeKey = JSON.stringify(theme);
223
227
  const card = useCard(name, inst.card_v);
224
228
  const [dialog, setDialog] = useState(null); // "delete" | null
229
+ const [canvasPlayback, setCanvasPlayback] = useState(null);
230
+ const publishCanvasPlayback = useCallback(value => setCanvasPlayback(previous =>
231
+ !previous && !value.playing || previous?.name === value.name && previous.playing === value.playing ? previous : value), []);
225
232
  const [wrap, setWrap] = useState(codeWrap);
226
233
  const view = useRef(null), split = useRef(null), thread = useRef(null), bg = useRef(null), effectSurface = useRef(null);
227
234
  useLayoutEffect(() => { applyTheme(view.current, theme, mode); }, [themeKey, mode]);
@@ -274,24 +281,32 @@ function ChatView({ name, inst, lease, navKey, exec, awaiting }) {
274
281
  colorMenuItem(name, inst),
275
282
  // #1629: a person's control over this agent's theme — not over the public link (it only views themes).
276
283
  ...themeMenuItems(name),
284
+ // #2117: its ambient — why it is not showing here (with the way to change that), and a person's Clear.
285
+ ...ambientMenuItems(name, { publicLink: document.body.dataset.publicLink === "1", clear: clearAmbient }),
277
286
  chatEffectsMenuAvailable() ? { key: "effects", label: t("chatFx.title"), onSelect: () => setDialog("effects") } : null,
278
287
  { key: "delete", label: t("chat.delete"), icon: "trash", danger: true, onSelect: () => setDialog("delete") },
279
288
  ];
280
289
  const cls = statusClass(inst, exec, awaiting);
290
+ // #1920 phase 3: a guest has this chat only — no Details, View or Log to switch to, no lifecycle, colour, theme,
291
+ // effect management or delete (a guest's status frame carries no model or effort either, so those chips do not show).
292
+ const guest = guestPage();
281
293
  // #1269 quick actions: the model and effort it runs, each a way to /model or /effort (the same pick list).
282
294
  const pick = (command) => store.runCommand(name, command, "", { alive: () => lease.current() });
283
295
  const sub = html`<span class="status"><span class=${`dot ${cls}`} aria-hidden="true"></span>${statusLabel(inst, exec, awaiting)}</span>
284
- ${inst.model ? html`<button type="button" class="hd-chip" title=${t("chat.chipModel", inst.model)} aria-label=${t("chat.chipModel", inst.model)} onClick=${() => pick("model")}>${inst.model}</button>` : null}
296
+ ${inst.model ? html`<button type="button" class="hd-chip" title=${t("chat.chipModel", modelText(inst))} aria-label=${t("chat.chipModel", modelText(inst))} onClick=${() => pick("model")}>${modelText(inst)}</button>` : null}
285
297
  ${inst.effort ? html`<button type="button" class="hd-chip" title=${t("chat.chipEffort", inst.effort)} aria-label=${t("chat.chipEffort", inst.effort)} onClick=${() => pick("effort")}>${inst.effort}</button>` : null}`;
286
298
  return html`<div class=${`panel p-chat${wrap ? " wrap-code" : ""}`} ref=${view}>
287
- <${PanelHeader} title=${headerName(namesOf(inst || { name }, names.primary))} sub=${sub} nav=${html`<${InstanceSwitch} name=${name} current="chat" />`}><${ChatEffectPosters} name=${name} lease=${lease} element=${effectSurface} /><${ChatEffects} name=${name} lease=${lease} element=${effectSurface} store=${store} open=${dialog === "effects"} onClose=${() => setDialog(null)} /><${HeaderTools} navKey=${navKey} /><${Menu} items=${items} label=${t("app.more")} /></${PanelHeader}>
299
+ <${PanelHeader} title=${headerName(namesOf(inst || { name }, names.primary))} sub=${sub} nav=${guest ? null : html`<${InstanceSwitch} name=${name} current="chat" />`} notice=${html`<${ChatEffects} name=${name} lease=${lease} element=${effectSurface} store=${store} open=${dialog === "effects"} onPlayback=${publishCanvasPlayback} onClose=${() => setDialog(null)} />`}><${ChatEffectPosters} name=${name} lease=${lease} element=${effectSurface} /><${HeaderTools} navKey=${navKey} />${guest ? null : html`<${Menu} items=${items} label=${t("app.more")} />`}</${PanelHeader}>
288
300
  <${ChatCard} key=${name} name=${name} card=${card} />
289
301
  <${AgentWidget} slot="chat_header" widget=${widgetFor(name, "chat_header")} binds=${instanceBinds(inst, exec, awaiting)} />
290
302
  <div class="chat-split" ref=${split}>
291
303
  <div ref=${effectSurface} class=${`chat-main${theme && theme.background ? " has-th-bg" : ""}`}>
292
304
  ${theme && theme.background ? html`<img ref=${bg} class=${`th-bg th-bg-${theme.background.fit}`} src=${themeImageUrl(name, theme.background.image)} alt="" aria-hidden="true" decoding="async" />` : null}
305
+ <${ChatAmbient} name=${name} surface=${effectSurface} part="layer" />
306
+ <${AmbientMotionNote} name=${name} canvasPlaying=${canvasPlayback?.name === name && canvasPlayback.playing} />
293
307
  <${Thread} name=${name} th=${thread} />
294
308
  <${Dock} name=${name} inst=${inst} lease=${lease} exec=${exec} awaiting=${awaiting} workingLabel=${theme ? theme.workingLabel : undefined} />
309
+ <${ChatAmbient} name=${name} surface=${effectSurface} part="front" />
295
310
  </div>
296
311
  <${PreviewPanel} name=${name} split=${split} msgs=${() => store.state.msgs[name] || []} subscribe=${(fn) => store.subscribe(fn)}
297
312
  keyOf=${msgKey} reveal=${(k) => thread.current && thread.current.reveal(k)} download=${downloadHtml} allow=${(commit) => setPreviewOptIn(true, commit)} />
@@ -302,61 +317,145 @@ function ChatView({ name, inst, lease, navKey, exec, awaiting }) {
302
317
  </div>`;
303
318
  }
304
319
 
305
- /** The thread: Preact owns the elements, the keyed renderer owns what is inside the list (chat-thread.js). */
320
+ /**
321
+ * #2053: the last THREAD_KEEP chats' threads are kept when their view leaves (the chat view is keyed by instance, so
322
+ * each switch built the thread again: a revisit was no faster than a first visit). A kept thread is its list element
323
+ * and its renderer, and nothing more: it leaves as a chat always did (previews stop), with no subscription or timer
324
+ * left; shown again, one render draws only what changed meanwhile, where the reader was. A new language, another
325
+ * document, or more than the budget lets one go.
326
+ *
327
+ * #2184: the budget (raised for people who switch among ~10 favourites): up to THREAD_KEEP threads on a wide screen and
328
+ * THREAD_KEEP_NARROW on a narrow one (a phone), and THREAD_KEEP_NODES drawn messages in all, so memory stays bounded
329
+ * however long the chats are. Over it, the least recently shown goes first — one that is not a favourite before a
330
+ * favourite; the chat on screen never goes.
331
+ */
332
+ export const THREAD_KEEP = 12;
333
+ export const THREAD_KEEP_NARROW = 6;
334
+ export const THREAD_KEEP_NODES = 2400;
335
+ /**
336
+ * The kept threads to let go, in order. `kept`: [name, drawn messages], least recently shown first; `current`: the
337
+ * one on screen (never let go).
338
+ */
339
+ export function threadsToLetGo(kept, current, { keep, maxNodes, favorites }) {
340
+ let count = kept.length, nodes = kept.reduce((n, [, d]) => n + d, 0);
341
+ const out = [];
342
+ const order = [...kept.filter(([n]) => n !== current && !favorites.has(n)), ...kept.filter(([n]) => n !== current && favorites.has(n))];
343
+ for (const [n, d] of order) {
344
+ if (count <= keep && nodes <= maxNodes) break;
345
+ out.push(n); count--; nodes -= d;
346
+ }
347
+ return out;
348
+ }
349
+ const narrowScreen = () => typeof matchMedia === "function" && matchMedia("(max-width: 899px)").matches;
350
+ /** Let go what the budget does not hold (after `current` was shown, or drew more). */
351
+ function trimKept(current) {
352
+ const kept = [...keptThreads].map(([n, e]) => [n, e.thread.nodes.size]);
353
+ const favorites = new Set(instanceNavStore.get().favorites);
354
+ for (const n of threadsToLetGo(kept, current, { keep: narrowScreen() ? THREAD_KEEP_NARROW : THREAD_KEEP, maxNodes: THREAD_KEEP_NODES, favorites })) {
355
+ letGo(keptThreads.get(n), true); keptThreads.delete(n);
356
+ }
357
+ }
358
+ const keptThreads = new Map(); // name → { thread, list, sink, lang }
359
+ /** Tests only: let every kept thread go. */
360
+ export function forgetKeptThreads() { for (const e of keptThreads.values()) letGo(e, true); keptThreads.clear(); }
361
+ function letGo(e, own) { e.thread.dispose(own); e.list.remove(); }
362
+
363
+ /** A chat's thread: its own list element and renderer, and a sink for the view that shows it. */
364
+ function threadEntry(name, doc) {
365
+ const list = doc.createElement("div");
366
+ list.className = "thread";
367
+ list.setAttribute("role", "log");
368
+ list.setAttribute("aria-live", "off");
369
+ list.setAttribute("aria-label", t("chat.conversationWith", name));
370
+ const e = { list, sink: null, lang: lang(), thread: null };
371
+ const thread = createThread(list, doc.createElement("div"), {
372
+ t, tf: t, isUser: (x) => store.isUser(x, name), onJump: (j) => e.sink?.setJump(j), onEmpty: (v) => e.sink?.setEmpty(v),
373
+ setPreviewOptIn, copyText, download: downloadHtml, toggleWrap,
374
+ effectTrialStatus: x => getAppEffects()?.trialStatus(x) ?? "localAdmin",
375
+ effectTrialFailure: x => getAppEffects()?.trialFailure(x),
376
+ fixEffectTrial: (x, reason) => {
377
+ if (reason === "reducedMotion") setFollowsReducedMotion(false);
378
+ else if (reason === "busy") getAppEffects()?.stopTrial();
379
+ else if (reason === "transport") location.reload();
380
+ else if (reason === "localAdmin") location.href = "/signin?next=" + encodeURIComponent(location.pathname + location.search);
381
+ else navigate("/settings/appearance");
382
+ },
383
+ canTryEffect: x => getAppEffects()?.canTry(x) === true,
384
+ startEffectTrial: x => { const seq = navStore.get().seq; void getAppEffects()?.startTrial(x).then(ok => { if (!ok && e.sink && navStore.get().seq === seq && document.hasFocus?.() === true && document.visibilityState === "visible") toast(t("chat.effectTrialUnavailable")); }); },
385
+ clickReplyButton: (id, index) => store.clickReplyButton(name, id, index), // #1266
386
+ replyButtonBusy: (id) => store.state.rbBusy.has(id),
387
+ pollMine: (id) => store.state.pollMine[id], // #1823: this device's own vote
388
+ serverNow: () => Date.now() + store.state.clockSkew, // #1823: the deadline is the server's
389
+ panel: { shown: shownKey, open: (spec, o) => openPanel(spec, o), version: panelVersion, claim: claimPanel, claims: panelClaims }, // #1481, #1704
390
+ openImage: openLightbox, // an image, full size, in the page
391
+ emojis: (id) => !!emojiIndex && emojiIndex.known(id), // #1630: the fleet's server emoji
392
+ mentions: (kind, id) => (mentionIndex ? mentionIndex.name(kind, id) : null), // #1682: mention names
393
+ });
394
+ e.thread = thread;
395
+ return e;
396
+ }
397
+ /** While a thread is shown: what it follows. Returns what lets it all go. */
398
+ function follow(name, thread) {
399
+ // #1630: the server-emoji list is read once a message shows a tag; when it arrives, those messages are drawn again.
400
+ // #1682: the same for mention names — asked for the ids the messages carry; when names arrive, drawn again.
401
+ const wantEmojis = (msgs) => { if (emojiIndex && emojiIndex.wants(msgs)) emojiIndex.ensure(); if (mentionIndex) mentionIndex.ensure(msgs); };
402
+ const offs = [];
403
+ if (emojiIndex) offs.push(emojiIndex.subscribe(() => thread.render(store.state.msgs[name] || [])));
404
+ if (mentionIndex) offs.push(mentionIndex.subscribe(() => thread.render(store.state.msgs[name] || [])));
405
+ wantEmojis(store.state.msgs[name]);
406
+ offs.push(store.subscribe((instance, kind) => {
407
+ if (instance !== name) return;
408
+ if (kind === "msgs") { thread.render(store.state.msgs[name] || []); wantEmojis(store.state.msgs[name]); }
409
+ else if (kind === "sent") thread.jumpLatest();
410
+ }));
411
+ offs.push(P() ? P().onChange(() => thread.refreshCards()) : () => {});
412
+ offs.push(getAppEffects()?.subscribeTrial(() => thread.render(store.state.msgs[name] || [])) ?? (() => {}));
413
+ offs.push(panelStore.subscribe(() => thread.refreshCards())); // a card shows whether the panel has its block
414
+ return () => { for (const off of offs) off(); };
415
+ }
416
+
417
+ /** The thread's view: Preact owns the scroller and the jump pill; the kept list (chat-thread.js) is put inside it. */
306
418
  function Thread({ name, th }) {
307
- const scroller = useRef(null), list = useRef(null);
419
+ const scroller = useRef(null), host = useRef(null);
308
420
  const [jump, setJump] = useState({ show: false, unseen: 0 });
309
421
  const [empty, setEmpty] = useState(false);
310
422
  useLayoutEffect(() => {
311
- let live = true;
312
- const thread = createThread(list.current, scroller.current, {
313
- t, tf: t, isUser: (x) => store.isUser(x, name), onJump: setJump, onEmpty: setEmpty,
314
- setPreviewOptIn, copyText, download: downloadHtml, toggleWrap,
315
- canTryEffect: x => getAppEffects()?.canTry(x) === true,
316
- startEffectTrial: x => { const seq = navStore.get().seq; void getAppEffects()?.startTrial(x).then(ok => { if (!ok && live && navStore.get().seq === seq && document.hasFocus?.() === true && document.visibilityState === "visible") toast(t("chat.effectTrialUnavailable")); }); },
317
- clickReplyButton: (id, index) => store.clickReplyButton(name, id, index), // #1266
318
- replyButtonBusy: (id) => store.state.rbBusy.has(id),
319
- pollMine: (id) => store.state.pollMine[id], // #1823: this device's own vote
320
- serverNow: () => Date.now() + store.state.clockSkew, // #1823: the deadline is the server's
321
- panel: { shown: shownKey, open: (spec, o) => openPanel(spec, o), version: panelVersion, claim: claimPanel, claims: panelClaims }, // #1481, #1704
322
- openImage: openLightbox, // an image, full size, in the page
323
- emojis: (id) => !!emojiIndex && emojiIndex.known(id), // #1630: the fleet's server emoji
324
- mentions: (kind, id) => (mentionIndex ? mentionIndex.name(kind, id) : null), // #1682: mention names
325
- });
423
+ const doc = host.current.ownerDocument;
424
+ let e = keptThreads.get(name);
425
+ // Kept only while it can be shown as it is: the same language, and this page's own document.
426
+ if (e && (e.lang !== lang() || e.list.ownerDocument !== doc)) { letGo(e, true); keptThreads.delete(name); e = undefined; }
427
+ const kept = !!e;
428
+ if (e) keptThreads.delete(name);
429
+ else e = threadEntry(name, doc);
430
+ keptThreads.set(name, e); // the most recent last
431
+ trimKept(name);
432
+ const thread = e.thread, sc = scroller.current, ls = e.list;
433
+ e.sink = { setJump, setEmpty };
434
+ host.current.append(ls);
435
+ thread.attach(sc);
326
436
  th.current = thread;
327
- // #1630: the server-emoji list is read once a message shows a tag; when it arrives, those messages are drawn again.
328
- // #1682: the same for mention names — asked for the ids the messages carry; when names arrive, drawn again.
329
- const wantEmojis = (msgs) => { if (emojiIndex && emojiIndex.wants(msgs)) emojiIndex.ensure(); if (mentionIndex) mentionIndex.ensure(msgs); };
330
- const offEmojis = emojiIndex ? emojiIndex.subscribe(() => thread.render(store.state.msgs[name] || [])) : () => {};
331
- const offMentions = mentionIndex ? mentionIndex.subscribe(() => thread.render(store.state.msgs[name] || [])) : () => {};
437
+ // Up to date, at the reader's place (the bottom unless they had scrolled up); a kept one's cards say what they can do.
332
438
  thread.render(store.state.msgs[name] || [], { restore: store.state.scrollMemo[name] ?? null });
333
- wantEmojis(store.state.msgs[name]);
334
- const off = store.subscribe((instance, kind) => {
335
- if (instance !== name) return;
336
- if (kind === "msgs") { thread.render(store.state.msgs[name] || []); wantEmojis(store.state.msgs[name]); }
337
- else if (kind === "sent") thread.jumpLatest();
338
- });
339
- const offPv = P() ? P().onChange(() => thread.refreshCards()) : () => {};
340
- const offTrials = getAppEffects()?.subscribeTrial(() => thread.render(store.state.msgs[name] || [])) ?? (() => {});
341
- const offPanel = panelStore.subscribe(() => thread.refreshCards()); // a card shows whether the panel has its block
439
+ if (kept) thread.refreshCards();
440
+ const unfollow = follow(name, thread);
342
441
  const onScroll = () => { store.state.scrollMemo[name] = thread.onScroll(); };
343
- const sc = scroller.current, ls = list.current;
344
442
  sc.addEventListener("scroll", onScroll, { passive: true });
345
443
  ls.addEventListener("click", thread.onClick);
346
444
  return () => {
347
- live = false;
348
- off(); offPv(); offPanel(); offTrials(); offEmojis(); offMentions();
445
+ unfollow();
349
446
  sc.removeEventListener("scroll", onScroll);
350
447
  ls.removeEventListener("click", thread.onClick);
351
- thread.dispose(); // the whole thread goes: every preview in it stops first
448
+ e.sink = null;
352
449
  th.current = null;
450
+ if (keptThreads.get(name) === e) { thread.park(); ls.remove(); } // kept for a later visit: the chat left
451
+ else letGo(e, false); // let go meanwhile (a new language)
353
452
  };
354
453
  }, [name]);
355
454
  return html`<div class="scroller" ref=${scroller}>
356
- <div class="thread" ref=${list} role="log" aria-live="off" aria-label=${t("chat.conversationWith", name)}></div>
455
+ <div class="thread-host" ref=${host}></div>
357
456
  ${empty ? html`<${Empty} icon="chat" title=${t("chat.noMessages")} />` : null}
358
- ${jump.show ? html`<button data-trust="thread-control" type="button" class="jump-latest" onClick=${() => th.current && th.current.jumpLatest()}>
359
- <${Icon} name="down" size=${16} />${jump.unseen > 0 ? t("chat.newBelow", jump.unseen) : t("chat.jumpLatest")}</button>` : null}
457
+ <div class="jump-dock">${jump.show ? html`<button data-trust="thread-control" type="button" class="jump-latest" onClick=${() => th.current && th.current.jumpLatest()}>
458
+ <${Icon} name="down" size=${16} />${jump.unseen > 0 ? t("chat.newBelow", jump.unseen) : t("chat.jumpLatest")}</button>` : null}</div>
360
459
  </div>`;
361
460
  }
362
461
 
@@ -373,7 +472,8 @@ function Dock({ name, inst, lease, exec, awaiting, workingLabel }) {
373
472
  <button type="button" class="btn btn-primary btn-sm" onClick=${() => store.putBack(name)}>${t("chat.putBack")}</button>
374
473
  <button type="button" class="btn btn-sm" onClick=${() => store.discardFailed(name)}>${t("chat.discard")}</button></div>` : null}
375
474
  <${Composer} name=${name} lease=${lease} busy=${R().isBusy(exec)} />
376
- <p class="hint" title=${t("chat.composerMore")}>${t("chat.composerHint")}</p>
475
+ <p class="hint" title=${t(guestPage() ? "chat.guestMore" : "chat.composerMore")}>${t("chat.composerHint")}</p>
476
+ ${guestPage() ? html`<p class="guest-note">${t("chat.guestNote")}</p>` : html`<${AuthorizeChip} />`}
377
477
  </div></div>`;
378
478
  }
379
479
 
@@ -393,7 +493,8 @@ function CommandCard({ name, lease }) {
393
493
  : c.compact === "lost" ? t("chat.compactLost") : c.compact === "not_compacted" ? t("chat.compactNotCompacted") : t("chat.compactUnknown")}</p>` : null}
394
494
  ${c.choices && c.choices.prompt ? html`<pre class="cmd-text">${c.choices.prompt}</pre>` : null}
395
495
  ${c.choices ? html`<div class="cmd-choices" role="group" aria-label=${t("chat.cmdChoose")}>${c.choices.options.map((o) => html`<button key=${o.id} type="button"
396
- class=${`btn btn-sm${o.id === c.choices.current ? " current" : ""}`} aria-current=${o.id === c.choices.current ? "true" : undefined} onClick=${() => run(o.id)}>${o.label}</button>`)}</div>` : null}
496
+ class=${`btn btn-sm${o.id === c.choices.current ? " current" : ""}`} aria-current=${o.id === c.choices.current ? "true" : undefined}
497
+ disabled=${!!o.disabled} title=${o.disabled || undefined} onClick=${() => { if (!o.disabled) run(o.id); }}>${o.label}</button>`)}</div>` : null}
397
498
  </div>`;
398
499
  }
399
500
 
@@ -449,8 +550,10 @@ function Composer({ name, lease, busy }) {
449
550
  const value = s.drafts[name] || "";
450
551
  const files = s.pendingFiles[name] || [];
451
552
  const content = !!value.trim() || files.length > 0;
452
- // A file waiting: what goes is a message with its file, never a command (no palette, no command line).
453
- const palette = files.length || closedFor === value ? null : paletteFor(value);
553
+ // A file waiting: what goes is a message with its file, never a command (no palette, no command line). A guest never
554
+ // runs one (#1920: the server refuses its slash text), so it has no palette either.
555
+ const guest = guestPage();
556
+ const palette = guest || files.length || closedFor === value ? null : paletteFor(value);
454
557
  const pick = palette && palette.length ? palette[Math.min(sel, palette.length - 1)] : null;
455
558
  // Grow with the text, up to a limit: set through the CSSOM, never a style attribute (#1300).
456
559
  useLayoutEffect(() => { const el = input.current; if (!el) return; el.style.height = "auto"; el.style.height = `${Math.min(el.scrollHeight, 240)}px`; }, [value]);
@@ -471,6 +574,8 @@ function Composer({ name, lease, busy }) {
471
574
  else complete(pick);
472
575
  return;
473
576
  }
577
+ // #1920: a guest's "/…" would be refused as a command — said here, the draft kept to edit.
578
+ if (guest && /^\s*\//.test(value)) { toast(t("chat.guestNoCommands"), false); return; }
474
579
  const cmd = files.length ? null : parseCommandLine(value);
475
580
  if (cmd && needsArgument(cmd.command) && !cmd.args) { setValue(`/${cmd.command} `); return; }
476
581
  if (cmd) { runLine(cmd); return; }
@@ -14,6 +14,7 @@ import { PanelHeader, setTitle, statusClass, statusLabel, openDrawer } from "/as
14
14
  import { navigate } from "/assets/app-nav.js";
15
15
  import { settingsPath } from "/assets/app-route.js";
16
16
  import { InstanceSwitch } from "/assets/instance-switch.js";
17
+ import { modelText } from "/assets/instance-nav.js";
17
18
  import { HeaderTools } from "/assets/header-tools.js";
18
19
  import { Menu } from "/assets/ui-menu.js";
19
20
  import { themeMenuItems, themesStore } from "/assets/app-themes.js";
@@ -143,7 +144,7 @@ function DetailsView({ name, inst, navKey, exec, awaiting }) {
143
144
  <button type="button" class="btn btn-sm d-edit-settings" onClick=${editInSettings}>${t("chat.dEditInSettings")}</button></div></section>
144
145
  <section class="card"><h3>${t("chat.dRuntime")}</h3>
145
146
  ${row(t("chat.dBackend"), d.backend || "--")}
146
- ${row(t("chat.dModel"), `${d.model || sl.model?.display_name || "--"}${d.model_source === "live" ? "" : src(d.model_source)}`)}
147
+ ${row(t("chat.dModel"), `${(d.model && modelText(d)) || sl.model?.display_name || "--"}${d.model_source === "live" ? "" : src(d.model_source)}`)}
147
148
  ${d.effort ? row(t("chat.dEffort"), `${d.effort}${src(d.effort_source)}`) : null}
148
149
  ${row(t("chat.dCost"), `$${cost.toFixed(2)} ${t("chat.dCostNote")}`)}
149
150
  ${row(t("chat.dContext"), html`<span class="pct">${ctx != null ? `${Math.round(ctx)}%` : "--"}</span>${ctx != null ? bar(ctx, ctx > 80 ? "warn" : "") : null}`)}
@@ -11,22 +11,33 @@ import { useLease } from "/assets/app-ctx.js";
11
11
  import { PanelHeader, setTitle } from "/assets/app-shell.js";
12
12
  import { HeaderTools } from "/assets/header-tools.js";
13
13
  import { renavigate } from "/assets/app-nav.js";
14
- import { chatPath, fleetPath, FLEET_TABS } from "/assets/app-route.js";
14
+ import { chatPath, fleetPath, FLEET_TABS, rememberFleetTab } from "/assets/app-route.js";
15
15
  import { Dialog } from "/assets/ui-dialog.js";
16
16
  import { Empty, ErrorState, Skeleton } from "/assets/ui-states.js";
17
17
  import { Icon } from "/assets/ui-icons.js";
18
18
  import { toast } from "/assets/ui-toast.js";
19
19
  import { confirmDialog } from "/assets/ui-confirm.js";
20
20
  import { confirmedWrite } from "./settings-confirm.js";
21
- import { OrgChart } from "./fleet-org.js";
21
+ import { OrgCanvas } from "./fleet-canvas.js";
22
22
  import { labelOf } from "/assets/instance-nav.js";
23
23
  import { CacheReport } from "./fleet-cache.js";
24
+ import { UsageTab } from "./fleet-usage.js";
25
+ import { BackendSelect, ModelSelect } from "./settings-dialogs.js";
26
+
27
+ /** #2015: one of the largest processes as text: "python3 18.1 GB (dev-a)". */
28
+ export function memoryHogText(p) {
29
+ const size = p.rssBytes >= 1024 ** 3 ? `${(p.rssBytes / 1024 ** 3).toFixed(1)} GB` : `${Math.round(p.rssBytes / 1024 ** 2)} MB`;
30
+ const owner = p.owner === "instance" && p.instance ? p.instance
31
+ : t(p.owner === "fleet" ? "fleet.hostMemory_ownerFleet" : p.owner === "tmux" ? "fleet.hostMemory_ownerTmux" : "fleet.hostMemory_ownerOther");
32
+ return `${p.name} ${size} (${owner})`;
33
+ }
24
34
 
25
35
  register("fleet", {
26
36
  en: {
27
37
  hostMemory_elevated: "Host memory pressure: elevated — new agents start one at a time, at least 5 s apart",
28
38
  hostMemory_critical: "Host memory pressure: critical — new agents wait for memory to recover",
29
- title: "Fleet", tasks: "Tasks", schedules: "Schedules", teams: "Teams", org: "Org chart", cache: "Cache", config: "Config",
39
+ hostMemory_top: "Largest: {0}", hostMemory_ownerFleet: "the fleet", hostMemory_ownerTmux: "tmux", hostMemory_ownerOther: "not an instance",
40
+ title: "Fleet", tasks: "Tasks", schedules: "Schedules", teams: "Teams", org: "Org chart", cache: "Cache", usage: "Usage", config: "Config",
30
41
  docs: "Docs", docsNewTab: "Docs (opens in a new tab)",
31
42
  newTask: "New task", newSchedule: "New schedule", newTeam: "New team",
32
43
  noTasks: "No tasks yet", noTasksHint: "Create one to coordinate fleet work.", noSchedules: "No schedules yet", noSchedulesHint: "Automate recurring work with cron.",
@@ -52,12 +63,13 @@ register("fleet", {
52
63
  newInstance: "New instance", directory: "Directory", directoryHint: "Optional — a workspace is created if empty", topic: "Name", topicHint: "From the directory name",
53
64
  topicRequired: "Name is required when Directory is empty", description: "Description", descriptionHint: "What this instance does", model: "Model", modelHint: "e.g. sonnet, opus, gpt-5",
54
65
  branch: "Branch (git worktree)", branchHint: "e.g. feature-x", tags: "Tags", tagsHint: "comma-separated, e.g. dev, review", systemPrompt: "System prompt", systemPromptHint: "Custom instructions for this instance",
55
- fleetDefault: "Use the fleet default", notInstalled: "(not installed)", deprecated: "(deprecated)", instanceCreated: "Instance created",
66
+ fleetDefault: "Use the fleet default", notInstalled: "(not installed)", notSignedIn: "(not signed in — use /login)", tooOld: "(needs {0} or newer)", unchecked: "(not checked yet)", deprecated: "(deprecated)", instanceCreated: "Instance created",
56
67
  },
57
68
  "zh-TW": {
58
69
  hostMemory_elevated: "主機記憶體壓力偏高:新的 agent 一次啟動一個,至少間隔 5 秒",
59
70
  hostMemory_critical: "主機記憶體壓力嚴重:新的 agent 會等記憶體恢復再啟動",
60
- title: "Fleet", tasks: "Tasks", schedules: "排程", teams: "Teams", org: "組織圖", cache: "快取", config: "設定",
71
+ hostMemory_top: "最大:{0}", hostMemory_ownerFleet: "fleet 本身", hostMemory_ownerTmux: "tmux", hostMemory_ownerOther: "不屬於任何 instance",
72
+ title: "Fleet", tasks: "Tasks", schedules: "排程", teams: "Teams", org: "組織圖", cache: "快取", usage: "用量", config: "設定",
61
73
  docs: "文件", docsNewTab: "文件(在新分頁開啟)",
62
74
  newTask: "新增 task", newSchedule: "新增排程", newTeam: "新增 team",
63
75
  noTasks: "還沒有 task", noTasksHint: "建立一個來協調 fleet 的工作。", noSchedules: "還沒有排程", noSchedulesHint: "用 cron 自動執行例行工作。",
@@ -82,7 +94,7 @@ register("fleet", {
82
94
  newInstance: "新增 instance", directory: "目錄", directoryHint: "可留空——留空會自動建立工作區", topic: "名稱", topicHint: "預設取自目錄名稱",
83
95
  topicRequired: "目錄留空時必須填寫名稱", description: "說明", descriptionHint: "這個 instance 做什麼", model: "模型", modelHint: "例如 sonnet、opus、gpt-5",
84
96
  branch: "分支(git worktree)", branchHint: "例如 feature-x", tags: "標籤", tagsHint: "以逗號分隔,例如 dev, review", systemPrompt: "System prompt", systemPromptHint: "這個 instance 的自訂指示",
85
- fleetDefault: "使用 fleet 預設", notInstalled: "(未安裝)", deprecated: "(已淘汰)", instanceCreated: "Instance 已建立",
97
+ fleetDefault: "使用 fleet 預設", notInstalled: "(未安裝)", notSignedIn: "(未登入,請用 /login)", tooOld: "(需要 {0} 以上版本)", unchecked: "(尚未檢查)", deprecated: "(已淘汰)", instanceCreated: "Instance 已建立",
86
98
  },
87
99
  });
88
100
 
@@ -91,7 +103,7 @@ const DOCS_SITE = "https://songsid.github.io/AgEnD";
91
103
  export function docsUrl(language = lang()) {
92
104
  return `${DOCS_SITE}${language === "zh-TW" ? "/zh-tw" : ""}/docs/web-guide/`;
93
105
  }
94
- const ICONS = { tasks: "tasks", schedules: "clock", teams: "team", org: "org", cache: "chart", config: "sliders" };
106
+ const ICONS = { tasks: "tasks", schedules: "clock", teams: "team", org: "org", cache: "chart", usage: "chart", config: "sliders" };
95
107
  const BACKENDS = ["claude-code", "codex", "opencode", "kiro-cli", "antigravity", "grok", "muse"];
96
108
 
97
109
  async function api(method, path, body, lease) {
@@ -130,15 +142,18 @@ export function FleetPanel({ route, navKey }) {
130
142
  const lease = useLease(navKey);
131
143
  const tab = route.tab;
132
144
  useEffect(() => { setTitle(`${t("fleet.title")} · ${t(`fleet.${tab}`)}`); }, [tab, navKey]);
133
- const Body = { tasks: Tasks, schedules: Schedules, teams: Teams, org: Org, cache: Cache, config: Config }[tab] || Tasks;
145
+ useEffect(() => { rememberFleetTab(tab); }, [tab]); // #2183: the sidebar's and the phone's Fleet reopen it
146
+ const Body = { tasks: Tasks, schedules: Schedules, teams: Teams, org: Org, cache: Cache, usage: UsageTab, config: Config }[tab] || Tasks;
134
147
  // #1685: host memory pressure slowing or holding new agents — shown here, where macOS's routine "elevated" is not a chat notice.
135
- const { hostMemory } = useStore(appStore);
136
- const sub = hostMemory ? html`<span class=${`status host-mem ${hostMemory}`} role="status"><span class=${`dot ${hostMemory === "critical" ? "bad" : "warn"}`} aria-hidden="true"></span>${t(`fleet.hostMemory_${hostMemory}`)}</span>` : null;
148
+ const { hostMemory, hostMemoryTop } = useStore(appStore);
149
+ // #2015: and which processes hold the most, and whose — text only (never chat, #1712).
150
+ const top = hostMemory && hostMemoryTop?.length ? t("fleet.hostMemory_top", hostMemoryTop.map(memoryHogText).join(" · ")) : "";
151
+ const sub = hostMemory ? html`<span class=${`status host-mem ${hostMemory}`} role="status"><span class=${`dot ${hostMemory === "critical" ? "bad" : "warn"}`} aria-hidden="true"></span>${t(`fleet.hostMemory_${hostMemory}`)}${top ? html`<span class="host-mem-top">${top}</span>` : null}</span>` : null;
137
152
  return html`<div data-effect-panel="fleet" class="panel p-fleet">
138
153
  <${PanelHeader} title=${t("fleet.title")} sub=${sub}><${DocsLink} /><${HeaderTools} navKey=${navKey} /></${PanelHeader}>
139
154
  <nav data-trust="panel-navigation" class="seg" aria-label=${t("fleet.title")}>${FLEET_TABS.map(k => html`<a key=${k} href=${fleetPath(k)} class=${`seg-item${k === tab ? " active" : ""}`}
140
155
  aria-current=${k === tab ? "page" : undefined}><${Icon} name=${ICONS[k]} size=${16} /><span>${t(`fleet.${k}`)}</span></a>`)}</nav>
141
- <div data-trust="panel-content" class="panel-body"><div class=${`col${tab === "org" || tab === "cache" ? " col-wide" : ""}`}><${Body} lease=${lease} /></div></div>
156
+ <div data-trust="panel-content" class="panel-body"><div class=${`col${tab === "org" || tab === "cache" || tab === "usage" ? " col-wide" : ""}`}><${Body} lease=${lease} /></div></div>
142
157
  </div>`;
143
158
  }
144
159
 
@@ -255,11 +270,11 @@ function Teams({ lease }) {
255
270
  ${editing ? html`<${CreateTeam} team=${editing} onClose=${() => setEditing(null)} onDone=${() => setVersion(v => v + 1)} />` : null}`;
256
271
  }
257
272
 
258
- /** #1389: the org chart — its structure read once per opening, its live state from the app store (fleet-org.js). */
273
+ /** #1389: the org chart — its structure read once per opening, its live state from the app store. #1918: a canvas, or the list (fleet-canvas.js). */
259
274
  function Org({ lease }) {
260
275
  const state = useLoad(lease, "/ui/org", 0);
261
276
  const app = useStore(appStore);
262
- return html`<${Loaded} state=${state}>${(org) => html`<${OrgChart} org=${org} app=${app} />`}</${Loaded}>`;
277
+ return html`<${Loaded} state=${state}>${(org) => html`<${OrgCanvas} org=${org} app=${app} lease=${lease} />`}</${Loaded}>`;
263
278
  }
264
279
 
265
280
  /** #1468: the prompt-cache analysis — one read per opening and per window chosen; Refresh reads again (fleet-cache.js). */
@@ -322,8 +337,7 @@ function Config({ lease }) {
322
337
  <div class="field"><label for="cfg-users">${t("fleet.allowedUsers")}</label><${Secret} id="cfg-users" value=${form.users} onInput=${set("users")} label=${t("fleet.allowedUsers")} />
323
338
  <p class="note">${t("fleet.allowedUsersNote")}</p></div></section>
324
339
  <section class="card"><h3>${t("fleet.defaults")}</h3>
325
- <label class="field"><span>${t("fleet.backend")}</span><select value=${form.backend} onChange=${set("backend")}>
326
- ${[...(BACKENDS.includes(form.backend) ? [] : [form.backend]), ...BACKENDS].map(b => html`<option key=${b} value=${b}>${b}</option>`)}</select></label></section>
340
+ <div class="field"><label for="cfg-be">${t("fleet.backend")}</label><${BackendSelect} id="cfg-be" value=${form.backend} onChange=${(v) => setForm({ ...form, backend: v })} /></div></section>
327
341
  <section class="card"><h3>${t("fleet.roots")}</h3>
328
342
  ${form.roots.map((r, i) => html`<div key=${i} class="field-row"><input value=${r} placeholder="/path/to/projects" aria-label=${t("fleet.roots")}
329
343
  onInput=${(e) => { const roots = [...form.roots]; roots[i] = e.target.value; setForm({ ...form, roots }); }} />
@@ -510,6 +524,8 @@ function CreateTeam({ onClose, onDone, team = null }) {
510
524
  export function CreateInstanceDialog({ onClose }) {
511
525
  const lease = useLease("create-instance");
512
526
  const [backends, setBackends] = useState(null);
527
+ // The backend "fleet default" means (#2098 review P3-3): its models are the ones the model list offers then.
528
+ const [defaultBackend, setDefaultBackend] = useState("claude-code");
513
529
  const [f, setF] = useState({ dir: "", topic: "", desc: "", backend: "", model: "", branch: "", tags: "", prompt: "" });
514
530
  useEffect(() => {
515
531
  (async () => {
@@ -518,6 +534,7 @@ export function CreateInstanceDialog({ onClose }) {
518
534
  if (!lease.current()) return;
519
535
  const list = bd.backends || [];
520
536
  const first = list.find(b => b.installed && !b.deprecated) || list.find(b => b.installed);
537
+ if (typeof bd.defaultBackend === "string" && bd.defaultBackend) setDefaultBackend(bd.defaultBackend);
521
538
  setBackends(list);
522
539
  setF(x => ({ ...x, backend: first ? first.name : "" }));
523
540
  } catch {
@@ -547,14 +564,23 @@ export function CreateInstanceDialog({ onClose }) {
547
564
  ${field(`${t("fleet.topic")}${f.dir.trim() ? "" : " *"}`, html`<input value=${f.topic} onInput=${set("topic")} placeholder=${t("fleet.topicHint")} />`)}
548
565
  ${field(t("fleet.description"), html`<input value=${f.desc} onInput=${set("desc")} placeholder=${t("fleet.descriptionHint")} />`)}
549
566
  ${field(t("fleet.backend"), backends ? html`<select value=${f.backend} onChange=${set("backend")}><option value="">${t("fleet.fleetDefault")}</option>
550
- ${backends.map(b => html`<option key=${b.name} value=${b.name} disabled=${!b.installed}>${b.name}${b.deprecated ? ` ${t("fleet.deprecated")}` : ""}${b.installed ? "" : ` ${t("fleet.notInstalled")}`}</option>`)}</select>` : html`<${Skeleton} lines=${1} />`)}
551
- ${field(t("fleet.model"), html`<input value=${f.model} onInput=${set("model")} placeholder=${t("fleet.modelHint")} />`)}
567
+ ${backends.map(b => html`<option key=${b.name} value=${b.name} disabled=${backendBlocked(b)}>${b.name}${b.deprecated ? ` ${t("fleet.deprecated")}` : ""}${backendNote(b)}</option>`)}</select>` : html`<${Skeleton} lines=${1} />`)}
568
+ ${field(t("fleet.model"), html`<${ModelSelect} value=${f.model} onChange=${(v) => setF(x => ({ ...x, model: v }))} backend=${f.backend || defaultBackend} inherit=${t("fleet.fleetDefault")} />`)}
552
569
  ${field(t("fleet.branch"), html`<input value=${f.branch} onInput=${set("branch")} placeholder=${t("fleet.branchHint")} />`)}
553
570
  ${field(t("fleet.tags"), html`<input value=${f.tags} onInput=${set("tags")} placeholder=${t("fleet.tagsHint")} />`)}
554
571
  ${field(t("fleet.systemPrompt"), html`<textarea rows="3" value=${f.prompt} onInput=${set("prompt")} placeholder=${t("fleet.systemPromptHint")}></textarea>`)}
555
572
  </${FormDialog}>`;
556
573
  }
557
574
 
575
+ // #2072: a backend this host cannot run is shown, disabled, with why; one not checked yet stays selectable.
576
+ const backendState = (b) => b.state ?? (b.installed ? "usable" : b.unknown ? "unknown" : "not_installed");
577
+ function backendBlocked(b) { return ["not_installed", "not_signed_in", "too_old"].includes(backendState(b)); }
578
+ function backendNote(b) {
579
+ const s = backendState(b);
580
+ return s === "not_installed" ? ` ${t("fleet.notInstalled")}` : s === "not_signed_in" ? ` ${t("fleet.notSignedIn")}`
581
+ : s === "too_old" ? ` ${t("fleet.tooOld", b.minVersion ?? "?")}` : s === "unknown" ? ` ${t("fleet.unchecked")}` : "";
582
+ }
583
+
558
584
  // ── Cron in words (#1724: in the page's language) ──
559
585
  /**
560
586
  * A cron expression as a person says it — "Weekdays 18:00", 「每週一到五 18:00」, "Every 15 minutes" — or the expression