@songsid/agend 2.2.0-beta.1 → 2.2.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 (401) hide show
  1. package/README.md +1 -1
  2. package/README.zh-TW.md +1 -1
  3. package/dist/agent-cli-instructions.md +16 -9
  4. package/dist/agent-cli.js +15 -7
  5. package/dist/agent-cli.js.map +1 -1
  6. package/dist/agent-endpoint.js +24 -9
  7. package/dist/agent-endpoint.js.map +1 -1
  8. package/dist/agent-whats-new.d.ts +47 -0
  9. package/dist/agent-whats-new.js +147 -0
  10. package/dist/agent-whats-new.js.map +1 -0
  11. package/dist/auth-api.d.ts +5 -3
  12. package/dist/auth-api.js +19 -3
  13. package/dist/auth-api.js.map +1 -1
  14. package/dist/backend/antigravity.d.ts +8 -0
  15. package/dist/backend/antigravity.js +32 -9
  16. package/dist/backend/antigravity.js.map +1 -1
  17. package/dist/backend/claude-carry.d.ts +22 -0
  18. package/dist/backend/claude-carry.js +170 -0
  19. package/dist/backend/claude-carry.js.map +1 -0
  20. package/dist/backend/claude-code.d.ts +65 -2
  21. package/dist/backend/claude-code.js +226 -33
  22. package/dist/backend/claude-code.js.map +1 -1
  23. package/dist/backend/codex-metadata.d.ts +33 -0
  24. package/dist/backend/codex-metadata.js +122 -18
  25. package/dist/backend/codex-metadata.js.map +1 -1
  26. package/dist/backend/codex.d.ts +3 -0
  27. package/dist/backend/codex.js +17 -1
  28. package/dist/backend/codex.js.map +1 -1
  29. package/dist/backend/credential-profile.d.ts +40 -3
  30. package/dist/backend/credential-profile.js +113 -5
  31. package/dist/backend/credential-profile.js.map +1 -1
  32. package/dist/backend/effort-metadata.d.ts +37 -1
  33. package/dist/backend/effort-metadata.js +86 -15
  34. package/dist/backend/effort-metadata.js.map +1 -1
  35. package/dist/backend/grok.d.ts +4 -0
  36. package/dist/backend/grok.js +14 -0
  37. package/dist/backend/grok.js.map +1 -1
  38. package/dist/backend/kiro-engine-ledger.d.ts +5 -0
  39. package/dist/backend/kiro-engine-ledger.js +20 -0
  40. package/dist/backend/kiro-engine-ledger.js.map +1 -1
  41. package/dist/backend/kiro-identity.d.ts +6 -0
  42. package/dist/backend/kiro-identity.js +19 -5
  43. package/dist/backend/kiro-identity.js.map +1 -1
  44. package/dist/backend/kiro.d.ts +39 -3
  45. package/dist/backend/kiro.js +143 -19
  46. package/dist/backend/kiro.js.map +1 -1
  47. package/dist/backend/launch-effort.d.ts +61 -0
  48. package/dist/backend/launch-effort.js +170 -0
  49. package/dist/backend/launch-effort.js.map +1 -0
  50. package/dist/backend/muse.d.ts +84 -0
  51. package/dist/backend/muse.js +449 -53
  52. package/dist/backend/muse.js.map +1 -1
  53. package/dist/backend/types.d.ts +47 -4
  54. package/dist/backend/types.js +12 -1
  55. package/dist/backend/types.js.map +1 -1
  56. package/dist/backend-command.d.ts +91 -0
  57. package/dist/backend-command.js +122 -0
  58. package/dist/backend-command.js.map +1 -0
  59. package/dist/background-work-ended.d.ts +18 -0
  60. package/dist/background-work-ended.js +30 -0
  61. package/dist/background-work-ended.js.map +1 -0
  62. package/dist/cache-service.d.ts +2 -0
  63. package/dist/cache-service.js +2 -1
  64. package/dist/cache-service.js.map +1 -1
  65. package/dist/channel/adapters/discord.d.ts +36 -1
  66. package/dist/channel/adapters/discord.js +126 -4
  67. package/dist/channel/adapters/discord.js.map +1 -1
  68. package/dist/channel/adapters/telegram.d.ts +42 -2
  69. package/dist/channel/adapters/telegram.js +142 -23
  70. package/dist/channel/adapters/telegram.js.map +1 -1
  71. package/dist/channel/mcp-server.js +9 -1
  72. package/dist/channel/mcp-server.js.map +1 -1
  73. package/dist/channel/mcp-tools.d.ts +11 -0
  74. package/dist/channel/mcp-tools.js +26 -13
  75. package/dist/channel/mcp-tools.js.map +1 -1
  76. package/dist/channel/message-edit-errors.d.ts +5 -0
  77. package/dist/channel/message-edit-errors.js +24 -0
  78. package/dist/channel/message-edit-errors.js.map +1 -0
  79. package/dist/channel/telegram-markdown.d.ts +6 -0
  80. package/dist/channel/telegram-markdown.js +104 -0
  81. package/dist/channel/telegram-markdown.js.map +1 -0
  82. package/dist/channel/tool-router.d.ts +2 -0
  83. package/dist/channel/tool-router.js +10 -4
  84. package/dist/channel/tool-router.js.map +1 -1
  85. package/dist/channel/types.d.ts +55 -3
  86. package/dist/cli.js +167 -39
  87. package/dist/cli.js.map +1 -1
  88. package/dist/command-table.d.ts +2 -2
  89. package/dist/command-table.js +6 -3
  90. package/dist/command-table.js.map +1 -1
  91. package/dist/config-validator.js +59 -0
  92. package/dist/config-validator.js.map +1 -1
  93. package/dist/cross-instance-envelope.d.ts +15 -2
  94. package/dist/cross-instance-envelope.js +29 -5
  95. package/dist/cross-instance-envelope.js.map +1 -1
  96. package/dist/daemon.d.ts +156 -1
  97. package/dist/daemon.js +773 -89
  98. package/dist/daemon.js.map +1 -1
  99. package/dist/delivery-outbox.d.ts +70 -8
  100. package/dist/delivery-outbox.js +315 -24
  101. package/dist/delivery-outbox.js.map +1 -1
  102. package/dist/delivery-reconciliation.d.ts +18 -1
  103. package/dist/delivery-reconciliation.js +67 -2
  104. package/dist/delivery-reconciliation.js.map +1 -1
  105. package/dist/fleet-context.d.ts +10 -1
  106. package/dist/fleet-manager.d.ts +404 -13
  107. package/dist/fleet-manager.js +2039 -210
  108. package/dist/fleet-manager.js.map +1 -1
  109. package/dist/general-knowledge/skills/backend-providers/SKILL.md +1 -1
  110. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +92 -21
  111. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +27 -3
  112. package/dist/general-knowledge/skills/delegation-playbook/SKILL.md +8 -8
  113. package/dist/general-knowledge/skills/fleet-config/SKILL.md +5 -5
  114. package/dist/general-knowledge/skills/fleet-health/SKILL.md +4 -4
  115. package/dist/general-knowledge/skills/fleet-restart/SKILL.md +13 -13
  116. package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +3 -2
  117. package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +6 -0
  118. package/dist/general-knowledge/skills/model-discovery/SKILL.md +12 -9
  119. package/dist/general-knowledge/skills/multi-channel/SKILL.md +8 -2
  120. package/dist/general-knowledge/skills/scheduling/SKILL.md +5 -3
  121. package/dist/general-knowledge/skills/session-management/SKILL.md +6 -5
  122. package/dist/general-knowledge/skills/tui-effort/SKILL.md +2 -2
  123. package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +8 -3
  124. package/dist/general-knowledge/steering/core-rules.md +8 -7
  125. package/dist/host-memory.d.ts +10 -0
  126. package/dist/host-memory.js +33 -1
  127. package/dist/host-memory.js.map +1 -1
  128. package/dist/human-delivery-receipts.d.ts +83 -0
  129. package/dist/human-delivery-receipts.js +194 -0
  130. package/dist/human-delivery-receipts.js.map +1 -0
  131. package/dist/instance-avatar.d.ts +36 -0
  132. package/dist/instance-avatar.js +57 -0
  133. package/dist/instance-avatar.js.map +1 -0
  134. package/dist/instance-config-impact.js +6 -0
  135. package/dist/instance-config-impact.js.map +1 -1
  136. package/dist/instance-lifecycle.d.ts +29 -6
  137. package/dist/instance-lifecycle.js +79 -28
  138. package/dist/instance-lifecycle.js.map +1 -1
  139. package/dist/instructions.d.ts +3 -1
  140. package/dist/instructions.js +11 -8
  141. package/dist/instructions.js.map +1 -1
  142. package/dist/kiro-log-gate.d.ts +42 -0
  143. package/dist/kiro-log-gate.js +39 -0
  144. package/dist/kiro-log-gate.js.map +1 -0
  145. package/dist/locale.js +226 -44
  146. package/dist/locale.js.map +1 -1
  147. package/dist/log-service.d.ts +99 -0
  148. package/dist/log-service.js +243 -0
  149. package/dist/log-service.js.map +1 -0
  150. package/dist/log-steps.d.ts +169 -0
  151. package/dist/log-steps.js +858 -0
  152. package/dist/log-steps.js.map +1 -0
  153. package/dist/login-controller.d.ts +20 -3
  154. package/dist/login-controller.js +71 -19
  155. package/dist/login-controller.js.map +1 -1
  156. package/dist/login-flows.d.ts +5 -2
  157. package/dist/login-flows.js +5 -4
  158. package/dist/login-flows.js.map +1 -1
  159. package/dist/memory-pressure.d.ts +13 -1
  160. package/dist/memory-pressure.js +50 -10
  161. package/dist/memory-pressure.js.map +1 -1
  162. package/dist/needs-you-hub.js +2 -0
  163. package/dist/needs-you-hub.js.map +1 -1
  164. package/dist/needs-you-live.d.ts +6 -0
  165. package/dist/needs-you-live.js +23 -3
  166. package/dist/needs-you-live.js.map +1 -1
  167. package/dist/next-steps.js +1 -1
  168. package/dist/next-steps.js.map +1 -1
  169. package/dist/outbound-handlers.d.ts +100 -0
  170. package/dist/outbound-handlers.js +284 -97
  171. package/dist/outbound-handlers.js.map +1 -1
  172. package/dist/outbound-schemas.d.ts +38 -15
  173. package/dist/outbound-schemas.js +36 -8
  174. package/dist/outbound-schemas.js.map +1 -1
  175. package/dist/public-link-progress.d.ts +1 -1
  176. package/dist/public-link-progress.js +8 -2
  177. package/dist/public-link-progress.js.map +1 -1
  178. package/dist/public-preview.d.ts +101 -0
  179. package/dist/public-preview.js +236 -0
  180. package/dist/public-preview.js.map +1 -0
  181. package/dist/public-web-gateway.js +14 -4
  182. package/dist/public-web-gateway.js.map +1 -1
  183. package/dist/public-web-link.d.ts +24 -1
  184. package/dist/public-web-link.js +63 -9
  185. package/dist/public-web-link.js.map +1 -1
  186. package/dist/reply-buttons-controller.d.ts +89 -5
  187. package/dist/reply-buttons-controller.js +273 -8
  188. package/dist/reply-buttons-controller.js.map +1 -1
  189. package/dist/reply-buttons.d.ts +48 -3
  190. package/dist/reply-buttons.js +105 -10
  191. package/dist/reply-buttons.js.map +1 -1
  192. package/dist/reply-polls.d.ts +85 -0
  193. package/dist/reply-polls.js +154 -0
  194. package/dist/reply-polls.js.map +1 -0
  195. package/dist/restart-guard.d.ts +6 -3
  196. package/dist/restart-guard.js +6 -3
  197. package/dist/restart-guard.js.map +1 -1
  198. package/dist/restart-progress.d.ts +33 -0
  199. package/dist/restart-progress.js +30 -0
  200. package/dist/restart-progress.js.map +1 -1
  201. package/dist/service-activation.d.ts +15 -7
  202. package/dist/service-activation.js +46 -29
  203. package/dist/service-activation.js.map +1 -1
  204. package/dist/service-installer.d.ts +31 -0
  205. package/dist/service-installer.js +41 -2
  206. package/dist/service-installer.js.map +1 -1
  207. package/dist/settings-api.js +85 -25
  208. package/dist/settings-api.js.map +1 -1
  209. package/dist/settings-change.js +72 -1
  210. package/dist/settings-change.js.map +1 -1
  211. package/dist/settings-effect.js +28 -4
  212. package/dist/settings-effect.js.map +1 -1
  213. package/dist/settings-http-confirmation.js +7 -0
  214. package/dist/settings-http-confirmation.js.map +1 -1
  215. package/dist/setup-form.d.ts +1 -1
  216. package/dist/setup-form.js +1 -1
  217. package/dist/spawn-gate.js +5 -1
  218. package/dist/spawn-gate.js.map +1 -1
  219. package/dist/spawn-hold-reply.d.ts +12 -0
  220. package/dist/spawn-hold-reply.js +39 -0
  221. package/dist/spawn-hold-reply.js.map +1 -0
  222. package/dist/status-emojis.d.ts +10 -3
  223. package/dist/status-emojis.js +11 -2
  224. package/dist/status-emojis.js.map +1 -1
  225. package/dist/systemd-peer-bus.d.ts +95 -0
  226. package/dist/systemd-peer-bus.js +430 -0
  227. package/dist/systemd-peer-bus.js.map +1 -0
  228. package/dist/systemd-runtime.d.ts +27 -1
  229. package/dist/systemd-runtime.js +69 -7
  230. package/dist/systemd-runtime.js.map +1 -1
  231. package/dist/tips.js +4 -4
  232. package/dist/tips.js.map +1 -1
  233. package/dist/tool-permissions.js +3 -3
  234. package/dist/tool-permissions.js.map +1 -1
  235. package/dist/tool-progress.js +3 -2
  236. package/dist/tool-progress.js.map +1 -1
  237. package/dist/topic-commands.d.ts +42 -2
  238. package/dist/topic-commands.js +101 -11
  239. package/dist/topic-commands.js.map +1 -1
  240. package/dist/transcript-delta-reader.d.ts +3 -3
  241. package/dist/transcript-delta-reader.js +8 -8
  242. package/dist/transcript-delta-reader.js.map +1 -1
  243. package/dist/transcript-jsonl.d.ts +80 -3
  244. package/dist/transcript-jsonl.js +147 -10
  245. package/dist/transcript-jsonl.js.map +1 -1
  246. package/dist/transcript-monitor.d.ts +19 -0
  247. package/dist/transcript-monitor.js +87 -15
  248. package/dist/transcript-monitor.js.map +1 -1
  249. package/dist/transcript-sources.d.ts +78 -0
  250. package/dist/transcript-sources.js +375 -64
  251. package/dist/transcript-sources.js.map +1 -1
  252. package/dist/transcript-turns.d.ts +15 -2
  253. package/dist/transcript-turns.js +15 -2
  254. package/dist/transcript-turns.js.map +1 -1
  255. package/dist/tunnel/cloudflared.d.ts +7 -15
  256. package/dist/tunnel/cloudflared.js +36 -1
  257. package/dist/tunnel/cloudflared.js.map +1 -1
  258. package/dist/tunnel/purpose-lane.d.ts +5 -1
  259. package/dist/tunnel/purpose-lane.js +14 -7
  260. package/dist/tunnel/purpose-lane.js.map +1 -1
  261. package/dist/tunnel/types.d.ts +5 -1
  262. package/dist/tunnel/types.js.map +1 -1
  263. package/dist/turn-resume.d.ts +2 -0
  264. package/dist/turn-resume.js +1 -1
  265. package/dist/turn-resume.js.map +1 -1
  266. package/dist/types.d.ts +25 -0
  267. package/dist/ui/chat-commands.js +1 -0
  268. package/dist/ui/chat-emojis.js +33 -0
  269. package/dist/ui/chat-mentions.js +63 -0
  270. package/dist/ui/chat-render.js +61 -4
  271. package/dist/ui/chat-store.js +37 -3
  272. package/dist/ui/chat-strings.js +40 -20
  273. package/dist/ui/chat-thread.js +111 -12
  274. package/dist/ui/chat-time.js +80 -0
  275. package/dist/ui/panel-chat.js +41 -10
  276. package/dist/ui/panel-details.js +2 -0
  277. package/dist/ui/panel-fleet.js +199 -57
  278. package/dist/ui/panel-log.js +428 -0
  279. package/dist/ui/panel-needs.js +6 -2
  280. package/dist/ui/panel-settings.js +184 -32
  281. package/dist/ui/preview-panel.js +55 -15
  282. package/dist/ui/preview-status.js +28 -0
  283. package/dist/ui/preview.js +44 -3
  284. package/dist/ui/settings-dialogs.js +7 -2
  285. package/dist/ui/settings-model.js +2 -1
  286. package/dist/ui/settings-strings.js +56 -4
  287. package/dist/ui/settings-token.js +1 -1
  288. package/dist/ui/shared/app-i18n.js +43 -17
  289. package/dist/ui/shared/app-prefs.js +250 -0
  290. package/dist/ui/shared/app-route.js +7 -2
  291. package/dist/ui/shared/app-shell.js +165 -8
  292. package/dist/ui/shared/app-store.js +5 -1
  293. package/dist/ui/shared/app-stream.js +26 -5
  294. package/dist/ui/shared/app.css +163 -22
  295. package/dist/ui/shared/app.js +11 -1
  296. package/dist/ui/shared/fonts/README.md +1 -1
  297. package/dist/ui/shared/header-tools.js +55 -5
  298. package/dist/ui/shared/instance-nav.js +241 -45
  299. package/dist/ui/shared/instance-switch.js +4 -4
  300. package/dist/ui/shared/panel-view.js +6 -0
  301. package/dist/ui/shared/signin.js +7 -7
  302. package/dist/ui/shared/theme.js +39 -7
  303. package/dist/ui/shared/tokens.css +1 -1
  304. package/dist/ui/shared/ui-icons.js +1 -0
  305. package/dist/ui/shared/usage-dialog.js +23 -5
  306. package/dist/ui/shared/view-strings.js +32 -4
  307. package/dist/ui/signin.html +1 -1
  308. package/dist/unrecognised-prompt.d.ts +23 -0
  309. package/dist/unrecognised-prompt.js +74 -0
  310. package/dist/unrecognised-prompt.js.map +1 -0
  311. package/dist/update-check.d.ts +31 -0
  312. package/dist/update-check.js +47 -0
  313. package/dist/update-check.js.map +1 -1
  314. package/dist/update-install.d.ts +28 -2
  315. package/dist/update-install.js +22 -4
  316. package/dist/update-install.js.map +1 -1
  317. package/dist/update-launch.d.ts +46 -1
  318. package/dist/update-launch.js +146 -3
  319. package/dist/update-launch.js.map +1 -1
  320. package/dist/update-marker.d.ts +51 -0
  321. package/dist/update-marker.js +61 -0
  322. package/dist/update-marker.js.map +1 -1
  323. package/dist/update-preflight.d.ts +100 -0
  324. package/dist/update-preflight.js +264 -0
  325. package/dist/update-preflight.js.map +1 -0
  326. package/dist/updater-caller.d.ts +31 -0
  327. package/dist/updater-caller.js +109 -0
  328. package/dist/updater-caller.js.map +1 -0
  329. package/dist/usage/i18n-keys.d.ts +1 -1
  330. package/dist/usage/i18n-keys.js +1 -1
  331. package/dist/usage/i18n-keys.js.map +1 -1
  332. package/dist/usage/providers.d.ts +34 -5
  333. package/dist/usage/providers.js +160 -27
  334. package/dist/usage/providers.js.map +1 -1
  335. package/dist/usage/statusline-usage.d.ts +6 -2
  336. package/dist/usage/statusline-usage.js +10 -4
  337. package/dist/usage/statusline-usage.js.map +1 -1
  338. package/dist/view-api.d.ts +1 -1
  339. package/dist/view-api.js +38 -8
  340. package/dist/view-api.js.map +1 -1
  341. package/dist/wake-coordinator.d.ts +11 -0
  342. package/dist/wake-coordinator.js +38 -4
  343. package/dist/wake-coordinator.js.map +1 -1
  344. package/dist/web-api.d.ts +89 -4
  345. package/dist/web-api.js +551 -45
  346. package/dist/web-api.js.map +1 -1
  347. package/dist/web-auth.d.ts +2 -2
  348. package/dist/web-auth.js +9 -4
  349. package/dist/web-auth.js.map +1 -1
  350. package/dist/web-chat-history.d.ts +12 -1
  351. package/dist/web-chat-history.js +29 -7
  352. package/dist/web-chat-history.js.map +1 -1
  353. package/dist/web-commands.d.ts +27 -4
  354. package/dist/web-commands.js +19 -4
  355. package/dist/web-commands.js.map +1 -1
  356. package/dist/web-host-guard.d.ts +3 -11
  357. package/dist/web-host-guard.js +3 -12
  358. package/dist/web-host-guard.js.map +1 -1
  359. package/dist/web-images.d.ts +47 -0
  360. package/dist/web-images.js +169 -0
  361. package/dist/web-images.js.map +1 -0
  362. package/dist/web-login.d.ts +14 -0
  363. package/dist/web-login.js +4 -2
  364. package/dist/web-login.js.map +1 -1
  365. package/dist/web-mentions.d.ts +44 -0
  366. package/dist/web-mentions.js +75 -0
  367. package/dist/web-mentions.js.map +1 -0
  368. package/dist/web-prefs.d.ts +81 -0
  369. package/dist/web-prefs.js +297 -0
  370. package/dist/web-prefs.js.map +1 -0
  371. package/dist/web-preview.d.ts +14 -1
  372. package/dist/web-preview.js +35 -13
  373. package/dist/web-preview.js.map +1 -1
  374. package/dist/web-profile.d.ts +32 -0
  375. package/dist/web-profile.js +132 -0
  376. package/dist/web-profile.js.map +1 -0
  377. package/dist/web-reply-files.d.ts +45 -0
  378. package/dist/web-reply-files.js +223 -0
  379. package/dist/web-reply-files.js.map +1 -0
  380. package/dist/web-schedules.d.ts +12 -0
  381. package/dist/web-schedules.js +34 -0
  382. package/dist/web-schedules.js.map +1 -0
  383. package/dist/web-server-drain.d.ts +10 -0
  384. package/dist/web-server-drain.js +50 -0
  385. package/dist/web-server-drain.js.map +1 -0
  386. package/dist/web-session.d.ts +50 -1
  387. package/dist/web-session.js +63 -6
  388. package/dist/web-session.js.map +1 -1
  389. package/dist/web-settings-patch.d.ts +23 -0
  390. package/dist/web-settings-patch.js +77 -0
  391. package/dist/web-settings-patch.js.map +1 -0
  392. package/dist/web-shell-routes.d.ts +5 -1
  393. package/dist/web-shell-routes.js +10 -1
  394. package/dist/web-shell-routes.js.map +1 -1
  395. package/dist/web-upload.d.ts +17 -4
  396. package/dist/web-upload.js +64 -13
  397. package/dist/web-upload.js.map +1 -1
  398. package/dist/workflow-templates/default.md +1 -1
  399. package/package.json +1 -1
  400. package/templates/launchd.plist.ejs +3 -1
  401. package/templates/systemd.service.ejs +2 -1
@@ -7,13 +7,14 @@
7
7
  // - a row opens the view you are in (View → View, everywhere else → Chat);
8
8
  // - the list is never remounted on navigation, and only its own scrollTop follows the active row (#1515).
9
9
  // Public (/assets/): an anonymous View reader gets the same list, fed by View's roster read instead of status frames.
10
- import { html, useLayoutEffect, useRef } from "./app-html.js";
10
+ import { html, useLayoutEffect, useRef, useState } from "./app-html.js";
11
11
  import { t } from "./app-i18n.js";
12
12
  import { appStore, createStore, useStore } from "./app-store.js";
13
- import { chatPath, detailsPath, viewPath } from "./app-route.js";
13
+ import { chatPath, detailsPath, logPath, viewPath } from "./app-route.js";
14
14
  import { Icon } from "./ui-icons.js";
15
15
  import { Skeleton } from "./ui-states.js";
16
16
  import { viewStore } from "./view-roster-store.js";
17
+ import { onPrefsChanged, prefInForce, prefsWritable, savePref } from "./app-prefs.js";
17
18
  import "./view-strings.js";
18
19
 
19
20
  /** view.* text with named values. */
@@ -23,7 +24,6 @@ function tn(key, values = {}) {
23
24
  return s;
24
25
  }
25
26
  const stored = (k) => { try { return localStorage.getItem(k); } catch { return null; } };
26
- const store = (k, v) => { try { localStorage.setItem(k, v); } catch { /* this page only */ } };
27
27
  /** A row's instance name: `name` (status frames) or `instance_name` (View's roster). */
28
28
  const nameOf = (it) => (it && (it.name != null ? it.name : it.instance_name)) || "";
29
29
 
@@ -101,14 +101,24 @@ export function instanceTooltip(it) {
101
101
  return lines.join("\n");
102
102
  }
103
103
 
104
- // ── This browser's order (drag), folded groups and filter ──
104
+ // ── The order (drag), folded groups and filter: host preferences since #1665 (nav.*), cached in this browser ──
105
105
  const SIDEBAR_ORDER_KEY = "agend_view_sidebar_order"; // View's key since #999: everyone's arrangement carries over
106
106
  const COLLAPSED_KEY = "agend_instance_collapsed";
107
107
  const FILTER_KEY = "agend_instance_filter";
108
+ const FAVORITES_KEY = "agend_favorites"; // #1664
109
+ /**
110
+ * A synced sidebar preference, as the JSON its reader parses: the value in force in memory (app-prefs.js — this
111
+ * page's own change, else the host's; a full or blocked storage never brings an old one back), else this browser's copy.
112
+ */
113
+ function inForce(prefKey, lsKey) {
114
+ const p = prefInForce(prefKey);
115
+ if (!p) return stored(lsKey);
116
+ return p.value === undefined ? null : JSON.stringify(p.value);
117
+ }
108
118
  function readOrder() {
109
119
  const groups = new Map(), insts = new Map();
110
120
  try {
111
- const rows = JSON.parse(stored(SIDEBAR_ORDER_KEY) || "[]");
121
+ const rows = JSON.parse(inForce("nav.order", SIDEBAR_ORDER_KEY) || "[]");
112
122
  if (Array.isArray(rows)) for (const row of rows) {
113
123
  if (row && row.item_type === "group" && typeof row.item_name === "string") groups.set(row.item_name, Number(row.sort_index) || 0);
114
124
  else if (row && row.item_type === "instance" && typeof row.item_name === "string") insts.set(row.item_name, Number(row.sort_index) || 0);
@@ -117,15 +127,18 @@ function readOrder() {
117
127
  return { groups, insts };
118
128
  }
119
129
  function readCollapsed() {
120
- try { const a = JSON.parse(stored(COLLAPSED_KEY) || "[]"); return new Set(Array.isArray(a) ? a.filter((x) => typeof x === "string") : []); } catch { return new Set(); }
130
+ try { const a = JSON.parse(inForce("nav.collapsed", COLLAPSED_KEY) || "[]"); return new Set(Array.isArray(a) ? a.filter((x) => typeof x === "string") : []); } catch { return new Set(); }
121
131
  }
122
132
  const strings = (a) => (Array.isArray(a) ? a.filter((x) => typeof x === "string") : []);
123
133
  function readFilter() {
124
134
  try {
125
- const f = JSON.parse(stored(FILTER_KEY) || "{}");
135
+ const f = JSON.parse(inForce("nav.filter", FILTER_KEY) || "{}");
126
136
  return { q: f && typeof f.q === "string" ? f.q : "", status: strings(f && f.status).filter((s) => STATUS_FACETS.includes(s)), cli: strings(f && f.cli) };
127
137
  } catch { return { q: "", status: [], cli: [] }; }
128
138
  }
139
+ function readFavorites() {
140
+ try { const a = JSON.parse(inForce("nav.favorites", FAVORITES_KEY) || "[]"); return Array.isArray(a) ? [...new Set(a.filter((x) => typeof x === "string"))] : []; } catch { return []; }
141
+ }
129
142
  /** Display order: this browser's saved order first, then new groups/instances by tag and name. */
130
143
  export function computeOrder(roster, order) {
131
144
  const groups = new Map();
@@ -159,29 +172,165 @@ function moveIn(arr, moving, target, before) {
159
172
  }
160
173
 
161
174
  /** The list's state for the page's life: this browser's order, folded groups and filter (each also kept in storage). */
162
- export const navStore = createStore({ order: readOrder(), collapsed: readCollapsed(), filter: readFilter() });
175
+ export const navStore = createStore({ order: readOrder(), collapsed: readCollapsed(), filter: readFilter(), favorites: readFavorites() });
163
176
  /** Read them again from this browser's storage — what a page load does (the store above is made that way). */
164
- export function loadNavPrefs() { navStore.set({ order: readOrder(), collapsed: readCollapsed(), filter: readFilter() }); }
177
+ export function loadNavPrefs() {
178
+ navStore.set({ order: readOrder(), collapsed: readCollapsed(), filter: readFilter(), favorites: readFavorites() });
179
+ namesStore.set({ primary: readNamesPrimary() });
180
+ }
165
181
  /** Change the filter ({ q?, status?, cli? }): the same on every page, and kept by this browser. */
166
182
  export function setNavFilter(patch) {
167
183
  const filter = { ...navStore.get().filter, ...patch };
168
184
  navStore.set({ filter });
169
- store(FILTER_KEY, JSON.stringify(filter));
185
+ savePref("nav.filter", { q: filter.q, status: filter.status, cli: filter.cli });
186
+ }
187
+ function setCollapsed(collapsed) { navStore.set({ collapsed }); savePref("nav.collapsed", [...collapsed]); }
188
+ // Another device rearranged, folded or filtered: this page's list follows (its cache is already updated).
189
+ onPrefsChanged((keys) => { if (keys.has("nav.order") || keys.has("nav.collapsed") || keys.has("nav.filter") || keys.has("nav.favorites")) loadNavPrefs(); });
190
+
191
+ // ── #1664: favorites — starred instances, pinned at the top in the order they were starred (or moved) ──
192
+ export const MAX_FAVORITES = 50;
193
+ /** The favorites that are instances now (`known`: the names listed): what is shown, and the base of every change — so
194
+ * a deleted instance is dropped, never sent back to the host (which would refuse it). */
195
+ function liveFavorites(known) { return navStore.get().favorites.filter((n) => known.has(n)); }
196
+ function setFavorites(next) { navStore.set({ favorites: next }); savePref("nav.favorites", next); }
197
+ /** Star or unstar `name`. False: not done (the list is full). */
198
+ export function toggleFavorite(name, known) {
199
+ const cur = liveFavorites(known);
200
+ if (cur.includes(name)) { setFavorites(cur.filter((n) => n !== name)); return true; }
201
+ if (cur.length >= MAX_FAVORITES || !known.has(name)) return false;
202
+ setFavorites([...cur, name]);
203
+ return true;
204
+ }
205
+ /** Move a favorite one place up (-1) or down (+1). */
206
+ export function moveFavorite(name, delta, known) {
207
+ const cur = liveFavorites(known);
208
+ const from = cur.indexOf(name), to = from + delta;
209
+ if (from < 0 || to < 0 || to >= cur.length) return;
210
+ cur.splice(to, 0, cur.splice(from, 1)[0]);
211
+ setFavorites(cur);
170
212
  }
171
- function setCollapsed(collapsed) { navStore.set({ collapsed }); store(COLLAPSED_KEY, JSON.stringify([...collapsed])); }
213
+ // ── #1700: the filter's facet groups, open or folded — per device (localStorage; blocked or full storage: this page only) ──
214
+ const FACETS_KEY = "agend_nav_facets";
215
+ function readFacetsOpen() {
216
+ try {
217
+ const v = JSON.parse(stored(FACETS_KEY) || "{}");
218
+ return { status: !!(v && v.status === true), cli: !!(v && v.cli === true) };
219
+ } catch { return { status: false, cli: false }; }
220
+ }
221
+ function writeFacetsOpen(open) { try { localStorage.setItem(FACETS_KEY, JSON.stringify({ status: !!open.status, cli: !!open.cli })); } catch { /* this page only */ } }
222
+
223
+ /** The Favorites group's key among the tag groups: no tag can be it (a group name is never a control character). */
224
+ const FAV_GROUP = "\u0000favorites";
172
225
  function persistOrder(groupNames, instByGroup) {
173
226
  const rows = [];
174
227
  groupNames.forEach((g, i) => rows.push({ item_type: "group", item_name: g, sort_index: i, group_name: null }));
175
228
  for (const g of groupNames) (instByGroup.get(g) || []).forEach((n, j) => rows.push({ item_type: "instance", item_name: n, sort_index: j, group_name: g }));
176
- store(SIDEBAR_ORDER_KEY, JSON.stringify(rows));
229
+ savePref("nav.order", rows);
177
230
  navStore.set({ order: { groups: new Map(groupNames.map((g, i) => [g, i])), insts: new Map(rows.filter((r) => r.item_type === "instance").map((r) => [r.item_name, r.sort_index])) } });
178
231
  }
179
232
 
233
+ // ── #1630: what stands for an instance (design: the comment on #1630) ──
234
+ // The server decides (the status frame's `avatar` / `accent`); the page only draws it. The anonymous View roster has
235
+ // neither: its rows get the first character. A picture is only ever a same-origin /ui/img/ path — never a platform's
236
+ // CDN (CSP img-src stays 'self'); one that fails to load falls back to the first character, never a broken image.
237
+
238
+ const GRAPHEMES = typeof Intl !== "undefined" && typeof Intl.Segmenter === "function" ? new Intl.Segmenter(undefined, { granularity: "grapheme" }) : null;
239
+ /** The first user-perceived character, upper-cased where that means anything. */
240
+ export function firstCharacter(text) {
241
+ const s = String(text == null ? "" : text).trim();
242
+ if (!s) return "?";
243
+ if (GRAPHEMES) for (const { segment } of GRAPHEMES.segment(s)) return segment.toLocaleUpperCase();
244
+ return Array.from(s)[0].toLocaleUpperCase();
245
+ }
246
+ /** What a row is called: the display name when it has one, else the instance name (shown on hover and in Details). */
247
+ export function labelOf(it) {
248
+ const d = it && typeof it.display_name === "string" ? it.display_name.trim() : "";
249
+ return d || nameOf(it);
250
+ }
251
+
252
+ // ── #1722: both names — which one leads is a synced preference (#1665 "names.primary", default the nickname) ──
253
+ const NAMES_KEY = "agend_names_primary";
254
+ function readNamesPrimary() {
255
+ const p = prefInForce("names.primary");
256
+ const v = p ? p.value : stored(NAMES_KEY);
257
+ return v === "instance" ? "instance" : "nickname";
258
+ }
259
+ /** { primary: "nickname" | "instance" } — every surface that names an instance (sidebar, chat header, Needs you) reads it. */
260
+ export const namesStore = createStore({ primary: readNamesPrimary() });
261
+ export function setNamesPrimary(value) {
262
+ const primary = value === "instance" ? "instance" : "nickname";
263
+ namesStore.set({ primary });
264
+ savePref("names.primary", primary);
265
+ }
266
+ onPrefsChanged((keys) => { if (keys.has("names.primary")) namesStore.set({ primary: readNamesPrimary() }); });
267
+ /**
268
+ * An instance's two names as the web shows them: the one leading (`primary`) and the other (`secondary`), or "" when
269
+ * there is no other — no nickname, or a nickname that is the instance name itself (one line).
270
+ */
271
+ export function namesOf(it, primary = namesStore.get().primary) {
272
+ const real = nameOf(it);
273
+ const nick = it && typeof it.display_name === "string" ? it.display_name.trim() : "";
274
+ if (!nick || nick === real) return { primary: real, secondary: "" };
275
+ return primary === "instance" ? { primary: real, secondary: nick } : { primary: nick, secondary: real };
276
+ }
277
+
278
+ /** #1722: the CLI on the avatar's corner — a letter, so it reads without colour (its name is in the row's text too). */
279
+ const CLI_MONOGRAM = { "claude-code": "C", codex: "X", "kiro-cli": "K", grok: "G", antigravity: "A", opencode: "O", muse: "M" };
280
+ // One path segment under /ui/img/<kind>/ — no further slash or backslash, and not a dot segment ("." / ".." or their
281
+ // %2e spellings, which a browser resolves too): the picture can only ever be one of those routes (#1642 review).
282
+ const sameOrigin = (src) => typeof src === "string"
283
+ && /^\/ui\/img\/(bot|avatar|emoji)\/(?!(?:\.|%2[eE]){1,2}(?:\?|$))[^/\\?#\s]+(\?v=[0-9A-Za-z%._-]*)?$/.test(src);
284
+ /** The avatar to draw: the server's, if it is one the page may load; else the first character. */
285
+ export function avatarOf(it) {
286
+ const a = it && it.avatar;
287
+ if (a && a.kind === "image" && sameOrigin(a.src)) return { kind: "image", src: a.src };
288
+ if (a && a.kind === "emoji" && sameOrigin(a.src)) return { kind: "emoji", src: a.src, alt: typeof a.alt === "string" ? a.alt : "" };
289
+ if (a && a.kind === "emoji" && typeof a.text === "string" && a.text.trim()) return { kind: "emoji", text: a.text.trim() };
290
+ // #1722: the initial is the leading name's ("Names"), not the server's (which is always the nickname's).
291
+ return { kind: "initial", text: firstCharacter(namesOf(it).primary) || (a && a.kind === "initial" && typeof a.text === "string" ? a.text : "") };
292
+ }
293
+ const ACCENT = /^#[0-9a-f]{6}$/i;
294
+ /** The status dot on the avatar: needs you, crashed, paused, stopped, looks stuck, working, idle — colour AND a label. */
295
+ export function dotState(it, exec, awaiting) {
296
+ if (awaiting != null) return "needs";
297
+ if (!it || it.status === "crashed") return "crashed";
298
+ if (it.status === "paused") return "paused";
299
+ if (it.status !== "running") return "stopped";
300
+ return exec === "stuck" ? "stuck" : exec === "working" ? "working" : "idle";
301
+ }
302
+ const DOT = {
303
+ needs: ["warn", () => t("app.needsYou")], crashed: ["bad", () => t("app.statusCrashed")], paused: ["off hollow", () => t("app.statusPaused")],
304
+ stopped: ["off", () => t("app.statusStopped")], stuck: ["warn", () => t("app.statusStuck")], working: ["busy", () => t("app.statusWorking")],
305
+ idle: ["ok", () => t("app.statusIdle")],
306
+ };
307
+ export function dotLabel(state) { return (DOT[state] || DOT.stopped)[1](); }
308
+
309
+ /** An instance's avatar with its status dot. The ring (its bot's colour) is a CSS custom property set through the CSSOM. */
310
+ export function InstanceAvatar({ it, state, cli = false }) {
311
+ const a = avatarOf(it);
312
+ const [broken, setBroken] = useState("");
313
+ const el = useRef(null);
314
+ const accent = typeof it.accent === "string" && ACCENT.test(it.accent) && a.kind === "image" ? it.accent : "";
315
+ useLayoutEffect(() => {
316
+ const node = el.current;
317
+ if (!node || !node.style) return;
318
+ if (accent) node.style.setProperty("--ring", accent); else node.style.removeProperty("--ring");
319
+ }, [accent]);
320
+ const src = a.kind !== "initial" && a.src && broken !== a.src ? a.src : "";
321
+ const text = a.kind === "emoji" && a.text ? a.text : a.kind === "initial" ? a.text : firstCharacter(namesOf(it).primary);
322
+ const [cls] = DOT[state] || DOT.stopped;
323
+ const label = dotLabel(state);
324
+ return html`<span ref=${el} class=${`av${accent ? " ring" : ""}${a.kind === "emoji" ? " av-emoji" : ""}`}>
325
+ ${src ? html`<img src=${src} alt="" decoding="async" onError=${() => setBroken(src)} />` : html`<span class="av-text" aria-hidden="true">${text}</span>`}
326
+ <span class=${`av-dot dot ${cls}`} role="img" aria-label=${label} title=${label}></span>${cli
327
+ ? html`<span class="av-cli" aria-hidden="true" title=${backendLabel(it.backend)}>${CLI_MONOGRAM[it.backend] || "·"}</span>` : null}</span>`;
328
+ }
329
+
180
330
  // ── The list ──
181
331
 
182
332
  const statusText = (facet) => (facet === "needs" ? t("app.needsYou") : facet === "working" ? t("app.statusWorking") : facet === "idle" ? t("app.statusIdle")
183
333
  : facet === "crashed" ? t("app.statusCrashed") : t("app.statusStopped"));
184
- const dotClass = (facet, exec) => (facet === "needs" ? "warn" : facet === "crashed" ? "bad" : facet === "stopped" ? "off" : exec === "stuck" ? "warn" : facet === "working" ? "busy" : "ok");
185
334
 
186
335
  /**
187
336
  * The sidebar's instance list. `route`: the page's (which instance is open, and which view a row opens). `items`: the
@@ -191,21 +340,39 @@ const dotClass = (facet, exec) => (facet === "needs" ? "warn" : facet === "crash
191
340
  */
192
341
  export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting = {}, viewOnly = false, onPick }) {
193
342
  const nav = useStore(navStore);
343
+ const names = useStore(namesStore); // #1722: which name leads
194
344
  const filterRef = useRef(null);
195
345
  const list = useRef(null);
196
346
  const drag = useRef(null);
347
+ // #1700: which facet groups are open — this device's (screen layout, not a synced preference), read when the list mounts.
348
+ const [facetOpen, setFacetOpen] = useState(readFacetsOpen);
349
+ const toggleFacet = (key) => setFacetOpen((cur) => { const next = { ...cur, [key]: !cur[key] }; writeFacetsOpen(next); return next; });
350
+ // #1664: "☆ Edit" — the stars on every row (on a phone the only way to them) and ↑ ↓ on the favorites — only on the
351
+ // full list, as drag: in a filtered one the row above may not be the one above in the list (#1677 review).
352
+ const [editing, setEditing] = useState(false);
197
353
  // Where a row goes: the view the page is in (#1523 §3.1). View → View; Details, or a Fleet tab (Q4 = A), → Details;
198
354
  // everywhere else → Chat. The anonymous reader only ever has View.
199
355
  const rowPath = viewOnly || (route && route.panel === "view") ? viewPath
356
+ : route && route.panel === "log" ? logPath // #1218: Log → Log
200
357
  : route && (route.panel === "details" || route.panel === "fleet") ? detailsPath : chatPath;
201
- const active = route && (route.panel === "chat" || route.panel === "view" || route.panel === "details") ? route.instance : null;
358
+ const active = route && (route.panel === "chat" || route.panel === "view" || route.panel === "log" || route.panel === "details") ? route.instance : null;
202
359
  useLayoutEffect(() => { keepActiveInView(list.current); }, [active, items.length, loaded]);
203
360
  const own = (map, name) => Object.prototype.hasOwnProperty.call(map, name);
204
361
  const byName = new Map(items.map((it) => [nameOf(it), it]));
205
362
  const facetOf = (name) => statusFacet(byName.get(name), own(exec, name) ? exec[name] : null, own(awaiting, name) ? awaiting[name] : null);
206
363
  const { groupNames, instByGroup } = computeOrder(items, nav.order);
364
+ // #1664: the favorites that are instances now, as a group of their own on top; the other groups keep the rest, in
365
+ // their order. Changing them: a signed-in page that may write preferences (not the public link, not the anonymous
366
+ // reader — they still see the list).
367
+ const known = new Set(byName.keys());
368
+ const favs = nav.favorites.filter((n) => known.has(n));
369
+ const favSet = new Set(favs);
370
+ const canStar = !viewOnly && prefsWritable();
371
+ const shownGroups = favs.length ? [FAV_GROUP, ...groupNames] : groupNames;
372
+ const shownInsts = new Map([...instByGroup].map(([g, names]) => [g, names.filter((n) => !favSet.has(n))]));
373
+ if (favs.length) shownInsts.set(FAV_GROUP, favs);
207
374
  const f = nav.filter;
208
- const shown = filterSidebar(groupNames, instByGroup, byName, f.q, backendLabel, { status: f.status, cli: f.cli, statusOf: facetOf });
375
+ const shown = filterSidebar(shownGroups, shownInsts, byName, f.q, backendLabel, { status: f.status, cli: f.cli, statusOf: facetOf });
209
376
  const backends = [...new Set(items.map((it) => it.backend).filter((b) => typeof b === "string" && b))].sort((a, b) => backendLabel(a).localeCompare(backendLabel(b)));
210
377
  const toggle = (g) => { const c = new Set(navStore.get().collapsed); if (c.has(g)) c.delete(g); else c.add(g); setCollapsed(c); };
211
378
  // From the store, not this render's copy: two clicks before the next render must both count.
@@ -213,7 +380,7 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
213
380
  // Drag to reorder: only the full list (a filtered subset would be ambiguous), groups among groups, instances within
214
381
  // their group.
215
382
  const dragProps = (item) => shown.active ? {} : {
216
- draggable: "true",
383
+ draggable: true,
217
384
  onDragStart: (e) => { drag.current = item; e.currentTarget.classList.add("dragging"); try { e.dataTransfer.effectAllowed = "move"; e.dataTransfer.setData("text/plain", item.name); } catch { /* Firefox needs data */ } },
218
385
  onDragEnd: (e) => { e.currentTarget.classList.remove("dragging"); drag.current = null; },
219
386
  onDragOver: (e) => {
@@ -239,57 +406,86 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
239
406
  persistOrder(names, insts);
240
407
  },
241
408
  };
242
- const chips = (key, values, label, text) => html`<details class="nav-facet">
243
- <summary>${label}${f[key].length ? html` <span class="nav-facet-n">${f[key].length}</span>` : null}</summary>
244
- <div class="nav-facet-list" role="group" aria-label=${label}>${values.map((value) => html`<label key=${value} class="nav-chip">
245
- <input type="checkbox" checked=${f[key].includes(value)} onChange=${() => flip(key, value)} /><span>${text(value)}</span></label>`)}</div>
246
- </details>`;
409
+ // #1700: a facet group (Status, CLI) folds under a real header button. Folded with a filter set, the header says so:
410
+ // one or two choices by name, more as "N selected" — a filter is never out of sight.
411
+ const chips = (key, values, label, text) => {
412
+ const open = !!facetOpen[key];
413
+ const sel = f[key];
414
+ const summary = !open && sel.length ? (sel.length <= 2 ? sel.map(text).join(", ") : tn("facetSelected", { n: sel.length })) : "";
415
+ const hint = tn(open ? "facetHide" : "facetShow", { group: label });
416
+ return html`<div class=${`nav-facet${open ? " open" : ""}`}>
417
+ <button type="button" class="nav-facet-head" aria-expanded=${open ? "true" : "false"} aria-controls=${`nav-facet-${key}`}
418
+ title=${summary ? `${hint} (${label} · ${summary})` : hint} onClick=${() => toggleFacet(key)}>
419
+ <${Icon} name="chevron" size=${12} cls=${open ? "caret" : "caret folded"} /><span class="nav-facet-label">${label}</span>
420
+ ${summary ? html`<span class="nav-facet-sum">· ${summary}</span>` : sel.length ? html`<span class="nav-facet-n">${sel.length}</span>` : null}
421
+ </button>
422
+ ${open ? html`<div id=${`nav-facet-${key}`} class="nav-facet-list" role="group" aria-label=${label}>${values.map((value) => html`<label key=${value} class="nav-chip">
423
+ <input type="checkbox" checked=${sel.includes(value)} onChange=${() => flip(key, value)} /><span>${text(value)}</span></label>`)}</div>` : null}
424
+ </div>`;
425
+ };
426
+ const row = (name, g, i, count) => {
427
+ const it = byName.get(name);
428
+ // #1722: both names, the preferred one first; one line when there is no other.
429
+ const { primary: label, secondary } = namesOf(it, names.primary);
430
+ const facet = facetOf(name);
431
+ const waiting = own(awaiting, name) ? awaiting[name] : null;
432
+ const on = name === active;
433
+ const fav = g === FAV_GROUP, starred = favSet.has(name);
434
+ const full = !starred && favs.length >= MAX_FAVORITES;
435
+ return html`<li key=${name} class="v-li"><a class=${`inst v-inst${on ? " active" : ""}`} href=${rowPath(name)} title=${instanceTooltip(it)}
436
+ aria-current=${on ? "page" : undefined} onClick=${onPick} ...${fav ? {} : dragProps({ type: "instance", name, group: g })}>
437
+ <${InstanceAvatar} it=${it} state=${dotState(it, own(exec, name) ? exec[name] : null, waiting)} cli=${true} />
438
+ <span class="inst-text"><span class="inst-name" title=${label}>${label}</span>
439
+ ${secondary ? html`<span class="inst-alt" title=${secondary}>${secondary}</span>` : null}
440
+ ${facet === "needs" ? html`<span class="inst-sub"><span class="badge-await" title=${waiting || t("app.approxNote")}>${t("app.needsYou")}</span></span>` : null}
441
+ <span class="sr-only">${tn("tooltipBackend", { value: backendLabel(it.backend) })}</span></span>
442
+ <span class="v-meta">${it.context_pct != null ? `${Math.round(it.context_pct)}%` : ""}</span>
443
+ </a>${fav && editing && canStar ? html`<button type="button" class="icon-btn inst-move" aria-label=${tn("favUp", { name: label })}
444
+ title=${shown.active ? tn("favFiltered") : tn("favUp", { name: label })} disabled=${shown.active || i === 0} onClick=${() => { if (!shown.active) moveFavorite(name, -1, known); }}><${Icon} name="up" size=${14} /></button><button type="button" class="icon-btn inst-move"
445
+ aria-label=${tn("favDown", { name: label })} title=${shown.active ? tn("favFiltered") : tn("favDown", { name: label })} disabled=${shown.active || i === count - 1}
446
+ onClick=${() => { if (!shown.active) moveFavorite(name, 1, known); }}><${Icon} name="down" size=${14} /></button>` : null}${canStar
447
+ ? html`<button type="button" class=${`icon-btn inst-star${starred ? " on" : ""}`} aria-pressed=${starred ? "true" : "false"} disabled=${full}
448
+ aria-label=${tn(starred ? "favRemove" : "favAdd", { name: label })} title=${full ? tn("favFull", { max: MAX_FAVORITES }) : tn(starred ? "favRemove" : "favAdd", { name: label })}
449
+ onClick=${() => toggleFavorite(name, known)}><${Icon} name="star" size=${14} /></button>` : null}</li>`;
450
+ };
247
451
  // The list scrolls; the filter under it stays in view (#999), so it sits outside the scrolling section.
248
- return html`<div class="view-roster inst-nav">
452
+ return html`<div class=${`view-roster inst-nav${editing && canStar ? " fav-editing" : ""}`}>
249
453
  <div class="side-section" id="instanceList" ref=${list}>
250
- <h2 class="side-label">${tn("instancesLabel")}</h2>
454
+ <div class="inst-head"><h2 class="side-label">${tn("instancesLabel")}</h2>
455
+ ${canStar && items.length ? html`<button type="button" class="chip-btn fav-edit" aria-pressed=${editing ? "true" : "false"} onClick=${() => setEditing(!editing)}>
456
+ ${editing ? tn("favDone") : html`<${Icon} name="star" size=${12} />${tn("favEdit")}`}</button>` : null}</div>
251
457
  ${!loaded ? html`<${Skeleton} lines=${4} />` : !items.length ? html`<p class="side-empty">${t("app.noInstances")}</p>` : null}
252
458
  ${shown.groups.map(({ group: g, names }) => {
459
+ // #1664: Favorites — always open, not dragged (↑ ↓ in Edit), named in the page's language.
460
+ if (g === FAV_GROUP) return html`<div key=${g} class="v-group v-fav" role="group" aria-label=${tn("favorites")}>
461
+ <div class="v-group-head fav-head"><${Icon} name="star" size=${14} cls="fav-mark" /><span class="grow">${tn("favorites")}</span><span class="count">${names.length}</span></div>
462
+ <ul class="inst-list">${names.map((name, i) => row(name, g, i, names.length))}</ul></div>`;
253
463
  const folded = nav.collapsed.has(g);
254
464
  return html`<div key=${g} class="v-group">
255
465
  <button type="button" class="v-group-head" aria-expanded=${folded ? "false" : "true"} onClick=${() => toggle(g)}
256
466
  title=${tn(folded ? "expandGroup" : "collapseGroup", { group: g })} ...${dragProps({ type: "group", name: g })}>
257
467
  <${Icon} name="chevron" size=${14} cls=${folded ? "caret folded" : "caret"} /><span class="grow">${g}</span><span class="count">${names.length}</span></button>
258
- ${folded ? null : html`<ul class="inst-list">${names.map((name) => {
259
- const it = byName.get(name);
260
- const alias = typeof it.display_name === "string" && it.display_name.trim() && it.display_name.trim() !== name ? it.display_name.trim() : "";
261
- const facet = facetOf(name);
262
- const waiting = own(awaiting, name) ? awaiting[name] : null;
263
- const on = name === active;
264
- return html`<li key=${name}><a class=${`inst v-inst${on ? " active" : ""}`} href=${rowPath(name)} title=${instanceTooltip(it)}
265
- aria-current=${on ? "page" : undefined} onClick=${onPick} ...${dragProps({ type: "instance", name, group: g })}>
266
- <span class=${`dot ${dotClass(facet, own(exec, name) ? exec[name] : null)}`} aria-hidden="true"></span>
267
- <span class="inst-text"><span class="inst-name">${name}</span>
268
- ${alias || facet === "needs" ? html`<span class="inst-sub">${alias ? html`<span class="inst-alias">${alias}</span>` : null}
269
- ${facet === "needs" ? html`<span class="badge-await" title=${waiting || t("app.approxNote")}>${t("app.needsYou")}</span>` : null}</span>` : null}
270
- <span class="sr-only">${statusText(facet)}</span></span>
271
- <span class="v-meta">${it.context_pct != null ? `${Math.round(it.context_pct)}%` : ""}</span>
272
- <span class=${`cli-icon cli-${KNOWN_BACKENDS.has(it.backend) ? it.backend : "other"}`} title=${backendLabel(it.backend)} aria-label=${tn("tooltipBackend", { value: backendLabel(it.backend) })}></span>
273
- </a></li>`;
274
- })}</ul>`}
468
+ ${folded ? null : html`<ul class="inst-list">${names.map((name, i) => row(name, g, i, names.length))}</ul>`}
275
469
  </div>`;
276
470
  })}
277
471
  ${loaded && shown.active && !shown.groups.length ? html`<p class="side-empty">${tn("filterNone")}</p>` : null}
278
472
  </div>
279
473
  <div class="v-filter">
280
474
  <label class="v-filter-box"><${Icon} name="search" size=${14} /><span class="sr-only">${tn("filterPh")}</span>
281
- <input ref=${filterRef} id="filterInput" type="text" autocomplete="off" spellcheck="false" placeholder=${tn("filterPh")} value=${f.q}
475
+ <input ref=${filterRef} id="filterInput" type="text" autocomplete="off" spellcheck=${false} placeholder=${tn("filterPh")} value=${f.q}
282
476
  onInput=${(e) => setNavFilter({ q: e.target.value })}
283
477
  onKeyDown=${(e) => { if (e.key === "Escape" && f.q) { e.preventDefault(); e.stopPropagation(); setNavFilter({ q: "" }); } }} />
284
478
  ${f.q ? html`<button type="button" class="icon-btn v-filter-clear" aria-label=${tn("filterClear")} title=${tn("filterClear")}
285
479
  onClick=${() => { setNavFilter({ q: "" }); filterRef.current && filterRef.current.focus(); }}><${Icon} name="close" size=${14} /></button>` : null}
286
480
  </label>
287
481
  <div class="nav-facets">
288
- ${viewOnly ? null : chips("status", STATUS_FACETS, tn("filterStatus"), statusText)}
289
- ${backends.length > 1 || f.cli.length ? chips("cli", backends, tn("filterCli"), backendLabel) : null}
290
- ${f.status.length || f.cli.length ? html`<button type="button" class="chip-btn nav-facet-reset" onClick=${() => setNavFilter({ status: [], cli: [] })}>${tn("filterFacetsReset")}</button>` : null}
482
+ <div class="nav-facet-groups">
483
+ ${viewOnly ? null : chips("status", STATUS_FACETS, tn("filterStatus"), statusText)}
484
+ ${backends.length > 1 || f.cli.length ? chips("cli", backends, tn("filterCli"), backendLabel) : null}
485
+ ${f.status.length || f.cli.length ? html`<button type="button" class="chip-btn nav-facet-reset" onClick=${() => setNavFilter({ status: [], cli: [] })}>${tn("filterFacetsReset")}</button>` : null}
486
+ </div>
487
+ <p class="note nav-shown" aria-live="polite">${shown.shown} / ${shown.total}${shown.active ? ` ${tn("filterShown")}` : ""}</p>
291
488
  </div>
292
- <p class="note" aria-live="polite">${shown.shown} / ${shown.total}${shown.active ? ` ${tn("filterShown")}` : ""}</p>
293
489
  </div>
294
490
  </div>`;
295
491
  }
@@ -1,16 +1,16 @@
1
1
  // #1523 N2 (§3.2): the instance view switch — Chat | View | Details of ONE instance. Each segment is a link to that
2
2
  // view's URL for the same instance, so switching keeps the instance (and the sidebar, which is never remounted).
3
- // Desktop: links in the header; a phone: the same three as top tabs under it (app.css). The anonymous View reader
3
+ // Desktop: links in the header; a phone: the same four as top tabs under it (app.css). The anonymous View reader
4
4
  // gets none (Chat and Details need a session): the caller does not render it there.
5
5
  import { html } from "./app-html.js";
6
6
  import { t } from "./app-i18n.js";
7
- import { chatPath, detailsPath, viewPath } from "./app-route.js";
7
+ import { chatPath, detailsPath, logPath, viewPath } from "./app-route.js";
8
8
 
9
- /** current: "chat" | "view" | "details". */
9
+ /** current: "chat" | "view" | "log" | "details". #1218: Log sits between View and Details. */
10
10
  export function InstanceSwitch({ name, current }) {
11
11
  const seg = (key, href, label) => html`<a key=${key} class=${`seg-item${current === key ? " active" : ""}`} href=${href}
12
12
  aria-current=${current === key ? "page" : undefined}>${label}</a>`;
13
13
  return html`<nav class="inst-switch seg" aria-label=${t("app.instanceViews", name)}>
14
- ${seg("chat", chatPath(name), t("app.chat"))}${seg("view", viewPath(name), t("app.view"))}${seg("details", detailsPath(name), t("app.details"))}
14
+ ${seg("chat", chatPath(name), t("app.chat"))}${seg("view", viewPath(name), t("app.view"))}${seg("log", logPath(name), t("app.log"))}${seg("details", detailsPath(name), t("app.details"))}
15
15
  </nav>`;
16
16
  }
@@ -201,7 +201,13 @@ function Terminal({ name, lease, density }) {
201
201
  const term = useRef(null), pre = useRef(null);
202
202
  const grid = useRef({ cols: 0, rows: 0, applied: 0, key: "" });
203
203
  const [problem, setProblem] = useState(null);
204
+ // #1674: the text size as of this render. The pane refresh and the resize handlers below are set up once per mount
205
+ // and keep the fit() of that render; reading the size here, not from that render's props, keeps them on the current
206
+ // choice (they used to put the mount-time size back within one 800 ms refresh).
207
+ const dens = useRef(density);
208
+ dens.current = density;
204
209
  const fit = () => {
210
+ const density = dens.current;
205
211
  const g = grid.current, box = term.current, p = pre.current;
206
212
  if (!box || !p || g.cols < 1 || g.rows < 1) return;
207
213
  const cs = getComputedStyle(box);
@@ -4,7 +4,7 @@
4
4
  // (a SameSite=Strict cookie is not sent on a navigation that came from a chat app), probe the session
5
5
  // from *here* — a same-site request does carry the cookie — and carry on to the page that was asked
6
6
  // for; (2) otherwise take the one-time code and exchange it. A browser that signed in before with a code from
7
- // /dashboard (a returning device, #1570) is also offered "Send me a new code": the server sends it privately to the
7
+ // /web (a returning device, #1570) is also offered "Send me a new code": the server sends it privately to the
8
8
  // person who received that earlier code — the page never learns who that is.
9
9
  (() => {
10
10
  "use strict";
@@ -13,24 +13,24 @@
13
13
  en: {
14
14
  checking: "Checking your session…", title: "Sign in to AgEnD", lead: "Enter the one-time code from your chat channel.",
15
15
  label: "One-time code", submit: "Sign in", working: "Signing in…",
16
- hint: "Send /dashboard 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 /dashboard.",
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.",
18
18
  paused: "Too many wrong codes — sign-in is paused for a few minutes.",
19
19
  network: "Could not reach AgEnD. Check the connection and try again.",
20
20
  resend: "Send me a new code", resent: "A new code was sent to you privately in chat. Enter it above.",
21
21
  resend_wait: "A code was asked for a moment ago — wait a little and try again.",
22
- resend_refused: "A new code could not be sent. Ask for one with /dashboard.",
22
+ resend_refused: "A new code could not be sent. Ask for one with /web.",
23
23
  },
24
24
  "zh-TW": {
25
25
  checking: "正在確認登入狀態…", title: "登入 AgEnD", lead: "請輸入聊天頻道給你的一次性登入碼。",
26
26
  label: "一次性登入碼", submit: "登入", working: "登入中…",
27
- hint: "在 AgEnD bot 傳 /dashboard,或在主機執行「agend web --code」。登入碼只能用一次,5 分鐘內有效。",
28
- refused: "登入碼無效:可能輸入錯誤、已過期或已使用。請用 /dashboard 再要一個。",
27
+ hint: "在 AgEnD bot 傳 /web,或在主機執行「agend web --code」。登入碼只能用一次,5 分鐘內有效。",
28
+ refused: "登入碼無效:可能輸入錯誤、已過期或已使用。請用 /web 再要一個。",
29
29
  paused: "錯誤次數過多,登入暫停幾分鐘。",
30
30
  network: "連不上 AgEnD,請檢查連線後再試。",
31
31
  resend: "傳一組新的登入碼給我", resent: "新的登入碼已私訊給你,請在上方輸入。",
32
32
  resend_wait: "剛剛才要過登入碼,請稍候再試。",
33
- resend_refused: "無法傳送新的登入碼。請用 /dashboard 再要一個。",
33
+ resend_refused: "無法傳送新的登入碼。請用 /web 再要一個。",
34
34
  },
35
35
  };
36
36
  const lang = /^zh/i.test(navigator.language || "") ? "zh-TW" : "en";
@@ -1,29 +1,61 @@
1
1
  // Light / dark for a panel (#1307). Loaded from /assets/theme.js in <head>, before the page paints, so a chosen theme
2
2
  // never flashes the other one first. "system" (the default) follows the device's prefers-color-scheme through CSS;
3
- // "light" or "dark" is this browser's own choice and lives in localStorage, like agend_lang.
3
+ // "light" or "dark" lives in localStorage, like agend_lang.
4
+ //
5
+ // #1665: the choice is kept on the host and follows the person between devices. The signed-in app's page carries the
6
+ // host's values in <html data-prefs>: the host's theme is what this page paints and holds, whatever this browser's
7
+ // storage does (the copy kept there is best effort — a full or blocked storage changes nothing on screen). Nothing
8
+ // chosen on the host: a device already in step with it (app-prefs.js's migration done) takes the default; one not yet
9
+ // in step keeps its own until it has uploaded it. Without data-prefs (the sign-in page, the anonymous View reader) this
10
+ // browser's copy decides, as before.
4
11
  //
5
12
  // A page's tokens follow the pattern: dark under :root, light under @media (prefers-color-scheme: light) for
6
13
  // :root:not([data-theme="dark"]), and again under :root[data-theme="light"].
7
14
  (function () {
8
15
  "use strict";
9
16
  var KEY = "agend_theme";
17
+ var SYNCED_FLAG = "agend_prefs_synced";
10
18
  function valid(v) { return v === "light" || v === "dark" ? v : "system"; }
11
19
  function read() {
12
20
  try { return valid(localStorage.getItem(KEY)); } catch (e) { return "system"; } // storage blocked: follow the device
13
21
  }
22
+ function cache(v) {
23
+ try { if (v === "system") localStorage.removeItem(KEY); else localStorage.setItem(KEY, v); } catch (e) { /* this page only */ }
24
+ }
14
25
  function apply(v) {
15
26
  var root = document.documentElement;
16
27
  if (v === "system") root.removeAttribute("data-theme");
17
28
  else root.setAttribute("data-theme", v);
18
29
  }
19
- apply(read());
30
+ /** The host's choice for this page: a theme, or null — none known here (no data-prefs, or not in step yet). */
31
+ function hostChoice() {
32
+ var d = null;
33
+ try { d = JSON.parse(document.documentElement.getAttribute("data-prefs") || "null"); } catch (e) { d = null; }
34
+ if (!d || typeof d !== "object" || !d.values || typeof d.values !== "object") return null;
35
+ var v = d.values.theme;
36
+ if (v === "light" || v === "dark" || v === "system") return v;
37
+ var inStep = false;
38
+ try { inStep = localStorage.getItem(SYNCED_FLAG) === "1"; } catch (e) { inStep = false; }
39
+ return inStep ? "system" : null;
40
+ }
41
+ var current = hostChoice();
42
+ if (current !== null) cache(current); else current = read();
43
+ apply(current);
44
+ var listeners = [];
20
45
  window.AgendTheme = {
21
- get: read,
22
- /** Choose "system", "light" or "dark" for this browser; applied now even when it cannot be stored. */
46
+ /** The theme this page shows. */
47
+ get: function () { return current; },
48
+ /** Choose "system", "light" or "dark" (this browser's copy; app-prefs.js keeps the host's); applied now even when it cannot be stored. */
23
49
  set: function (v) {
24
- v = valid(v);
25
- try { if (v === "system") localStorage.removeItem(KEY); else localStorage.setItem(KEY, v); } catch (e) { /* this page only */ }
26
- apply(v);
50
+ current = valid(v);
51
+ cache(current);
52
+ apply(current);
53
+ for (var i = 0; i < listeners.length; i++) { try { listeners[i](current); } catch (e) { /* one listener's error stops no other */ } }
54
+ },
55
+ /** Call `fn` with the theme whenever it changes on this page (an open Preferences shows it); returns the unsubscribe. */
56
+ onChange: function (fn) {
57
+ listeners.push(fn);
58
+ return function () { var i = listeners.indexOf(fn); if (i >= 0) listeners.splice(i, 1); };
27
59
  },
28
60
  };
29
61
  })();
@@ -22,7 +22,7 @@
22
22
 
23
23
  :root {
24
24
  /* ── Type ── */
25
- --font-ui: Inter, "PingFang TC", "Noto Sans TC", "Microsoft JhengHei", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
25
+ --font-ui: Inter, "PingFang TC", "PingFang SC", "Noto Sans TC", "Noto Sans SC", "Noto Sans CJK TC", "Noto Sans CJK SC", "Microsoft JhengHei", "Microsoft YaHei", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
26
26
  --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
27
27
  --fs-xs: 12px; --lh-xs: 16px;
28
28
  --fs-sm: 13px; --lh-sm: 18px;
@@ -37,6 +37,7 @@ const P = {
37
37
  type: () => html`<path d="M4 7V5h16v2M9 19h6M12 5v14"/>`,
38
38
  chart: () => html`<path d="M3 20h18M6 16v-4M11 16V8M16 16v-6"/>`,
39
39
  up: () => html`<path d="M12 19V5M5 12l7-7 7 7"/>`,
40
+ star: () => html`<path d="M12 2.5l2.94 5.96 6.56.95-4.75 4.63 1.12 6.54L12 17.5l-5.87 3.08 1.12-6.54L2.5 9.41l6.56-.95L12 2.5z"/>`,
40
41
  eye: () => html`<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/>`,
41
42
  check: () => html`<path d="M20 6L9 17l-5-5"/>`,
42
43
  inbox: () => html`<path d="M22 12h-6l-2 3h-4l-2-3H2"/><path d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/>`,