@songsid/agend 2.1.12 → 2.2.0-alpha.2

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 (436) hide show
  1. package/README.md +4 -2
  2. package/README.zh-TW.md +4 -2
  3. package/dist/agent-cli.js +6 -1
  4. package/dist/agent-cli.js.map +1 -1
  5. package/dist/agent-endpoint.js +68 -12
  6. package/dist/agent-endpoint.js.map +1 -1
  7. package/dist/agent-token-header.d.ts +13 -0
  8. package/dist/agent-token-header.js +16 -0
  9. package/dist/agent-token-header.js.map +1 -0
  10. package/dist/auth/one-time-code.d.ts +19 -0
  11. package/dist/auth/one-time-code.js +49 -0
  12. package/dist/auth/one-time-code.js.map +1 -0
  13. package/dist/auth-api.d.ts +52 -0
  14. package/dist/auth-api.js +346 -0
  15. package/dist/auth-api.js.map +1 -0
  16. package/dist/backend/antigravity.js +4 -3
  17. package/dist/backend/antigravity.js.map +1 -1
  18. package/dist/backend/claude-code.d.ts +4 -0
  19. package/dist/backend/claude-code.js +97 -0
  20. package/dist/backend/claude-code.js.map +1 -1
  21. package/dist/backend/codex.js +59 -30
  22. package/dist/backend/codex.js.map +1 -1
  23. package/dist/backend/grok.js +1 -1
  24. package/dist/backend/grok.js.map +1 -1
  25. package/dist/backend/kiro-agent.d.ts +66 -0
  26. package/dist/backend/kiro-agent.js +302 -0
  27. package/dist/backend/kiro-agent.js.map +1 -0
  28. package/dist/backend/kiro-identity.d.ts +80 -0
  29. package/dist/backend/kiro-identity.js +537 -0
  30. package/dist/backend/kiro-identity.js.map +1 -0
  31. package/dist/backend/kiro-v2-store-worker.d.ts +1 -0
  32. package/dist/backend/kiro-v2-store-worker.js +87 -0
  33. package/dist/backend/kiro-v2-store-worker.js.map +1 -0
  34. package/dist/backend/kiro-v2-store.d.ts +54 -0
  35. package/dist/backend/kiro-v2-store.js +131 -0
  36. package/dist/backend/kiro-v2-store.js.map +1 -0
  37. package/dist/backend/kiro-v3-identity.d.ts +5 -0
  38. package/dist/backend/kiro-v3-identity.js +22 -0
  39. package/dist/backend/kiro-v3-identity.js.map +1 -1
  40. package/dist/backend/kiro.d.ts +90 -7
  41. package/dist/backend/kiro.js +500 -124
  42. package/dist/backend/kiro.js.map +1 -1
  43. package/dist/backend/muse.js +1 -1
  44. package/dist/backend/muse.js.map +1 -1
  45. package/dist/backend/types.d.ts +51 -3
  46. package/dist/backend/types.js.map +1 -1
  47. package/dist/cache-analysis.d.ts +92 -0
  48. package/dist/cache-analysis.js +147 -0
  49. package/dist/cache-analysis.js.map +1 -0
  50. package/dist/cache-ledger.d.ts +108 -0
  51. package/dist/cache-ledger.js +230 -0
  52. package/dist/cache-ledger.js.map +1 -0
  53. package/dist/cache-prices.d.ts +32 -0
  54. package/dist/cache-prices.js +73 -0
  55. package/dist/cache-prices.js.map +1 -0
  56. package/dist/cache-scan.d.ts +39 -0
  57. package/dist/cache-scan.js +115 -0
  58. package/dist/cache-scan.js.map +1 -0
  59. package/dist/cache-service.d.ts +115 -0
  60. package/dist/cache-service.js +417 -0
  61. package/dist/cache-service.js.map +1 -0
  62. package/dist/channel/adapters/discord.d.ts +1 -0
  63. package/dist/channel/adapters/discord.js +29 -10
  64. package/dist/channel/adapters/discord.js.map +1 -1
  65. package/dist/channel/adapters/telegram.d.ts +1 -0
  66. package/dist/channel/adapters/telegram.js +13 -1
  67. package/dist/channel/adapters/telegram.js.map +1 -1
  68. package/dist/channel/types.d.ts +9 -1
  69. package/dist/classic-channel-manager.d.ts +18 -10
  70. package/dist/classic-channel-manager.js +45 -18
  71. package/dist/classic-channel-manager.js.map +1 -1
  72. package/dist/cli-entry.d.ts +19 -0
  73. package/dist/cli-entry.js +36 -0
  74. package/dist/cli-entry.js.map +1 -0
  75. package/dist/cli.js +471 -146
  76. package/dist/cli.js.map +1 -1
  77. package/dist/command-table.d.ts +5 -3
  78. package/dist/command-table.js +29 -27
  79. package/dist/command-table.js.map +1 -1
  80. package/dist/config-validator.js +46 -0
  81. package/dist/config-validator.js.map +1 -1
  82. package/dist/config.js +12 -0
  83. package/dist/config.js.map +1 -1
  84. package/dist/connection-secrets.d.ts +6 -0
  85. package/dist/connection-secrets.js +4 -0
  86. package/dist/connection-secrets.js.map +1 -1
  87. package/dist/daemon.d.ts +95 -4
  88. package/dist/daemon.js +748 -111
  89. package/dist/daemon.js.map +1 -1
  90. package/dist/darwin-memory.d.ts +6 -1
  91. package/dist/darwin-memory.js +17 -6
  92. package/dist/darwin-memory.js.map +1 -1
  93. package/dist/delivery-consumed-watch.d.ts +85 -0
  94. package/dist/delivery-consumed-watch.js +244 -0
  95. package/dist/delivery-consumed-watch.js.map +1 -0
  96. package/dist/delivery-outbox.d.ts +61 -0
  97. package/dist/delivery-outbox.js +120 -2
  98. package/dist/delivery-outbox.js.map +1 -1
  99. package/dist/delivery-reconciliation.d.ts +14 -5
  100. package/dist/delivery-reconciliation.js +27 -4
  101. package/dist/delivery-reconciliation.js.map +1 -1
  102. package/dist/delivery-reconciliation.test.js +35 -1
  103. package/dist/delivery-reconciliation.test.js.map +1 -1
  104. package/dist/event-log.d.ts +4 -1
  105. package/dist/event-log.js +6 -3
  106. package/dist/event-log.js.map +1 -1
  107. package/dist/event-loop-watch.d.ts +28 -0
  108. package/dist/event-loop-watch.js +75 -3
  109. package/dist/event-loop-watch.js.map +1 -1
  110. package/dist/fleet-context.d.ts +22 -2
  111. package/dist/fleet-control-audit.d.ts +5 -0
  112. package/dist/fleet-control-audit.js +7 -0
  113. package/dist/fleet-control-audit.js.map +1 -1
  114. package/dist/fleet-level-config.d.ts +2 -2
  115. package/dist/fleet-level-config.js +28 -1
  116. package/dist/fleet-level-config.js.map +1 -1
  117. package/dist/fleet-manager.d.ts +339 -12
  118. package/dist/fleet-manager.js +2623 -461
  119. package/dist/fleet-manager.js.map +1 -1
  120. package/dist/full-restart.js +2 -3
  121. package/dist/full-restart.js.map +1 -1
  122. package/dist/general-knowledge/skills/persona-emoji/SKILL.md +3 -1
  123. package/dist/host-memory.d.ts +4 -0
  124. package/dist/host-memory.js.map +1 -1
  125. package/dist/install-lock.d.ts +49 -0
  126. package/dist/install-lock.js +200 -0
  127. package/dist/install-lock.js.map +1 -0
  128. package/dist/instance-config-impact.js +8 -2
  129. package/dist/instance-config-impact.js.map +1 -1
  130. package/dist/instance-lifecycle.d.ts +10 -5
  131. package/dist/instance-lifecycle.js +162 -53
  132. package/dist/instance-lifecycle.js.map +1 -1
  133. package/dist/locale.js +164 -16
  134. package/dist/locale.js.map +1 -1
  135. package/dist/login-controller.d.ts +2 -0
  136. package/dist/login-controller.js +17 -5
  137. package/dist/login-controller.js.map +1 -1
  138. package/dist/memory-pressure.d.ts +7 -8
  139. package/dist/memory-pressure.js +76 -15
  140. package/dist/memory-pressure.js.map +1 -1
  141. package/dist/monotonic-budget.d.ts +2 -0
  142. package/dist/monotonic-budget.js +28 -0
  143. package/dist/monotonic-budget.js.map +1 -0
  144. package/dist/needs-you-hub.d.ts +114 -0
  145. package/dist/needs-you-hub.js +370 -0
  146. package/dist/needs-you-hub.js.map +1 -0
  147. package/dist/needs-you-live.d.ts +116 -0
  148. package/dist/needs-you-live.js +228 -0
  149. package/dist/needs-you-live.js.map +1 -0
  150. package/dist/needs-you.d.ts +132 -0
  151. package/dist/needs-you.js +164 -0
  152. package/dist/needs-you.js.map +1 -0
  153. package/dist/node-version-guard.d.ts +30 -0
  154. package/dist/node-version-guard.js +50 -0
  155. package/dist/node-version-guard.js.map +1 -0
  156. package/dist/operator-control-paths.d.ts +4 -0
  157. package/dist/operator-control-paths.js +24 -0
  158. package/dist/operator-control-paths.js.map +1 -0
  159. package/dist/outbound-handlers.d.ts +2 -0
  160. package/dist/outbound-handlers.js +9 -2
  161. package/dist/outbound-handlers.js.map +1 -1
  162. package/dist/outbound-schemas.d.ts +5 -5
  163. package/dist/outbound-schemas.js +1 -1
  164. package/dist/outbound-schemas.js.map +1 -1
  165. package/dist/package-preimage.d.ts +26 -0
  166. package/dist/package-preimage.js +96 -0
  167. package/dist/package-preimage.js.map +1 -0
  168. package/dist/profile-control.js +4 -20
  169. package/dist/profile-control.js.map +1 -1
  170. package/dist/public-web-gateway.d.ts +18 -0
  171. package/dist/public-web-gateway.js +141 -0
  172. package/dist/public-web-gateway.js.map +1 -0
  173. package/dist/public-web-link.d.ts +53 -0
  174. package/dist/public-web-link.js +175 -0
  175. package/dist/public-web-link.js.map +1 -0
  176. package/dist/quickstart-api.d.ts +8 -1
  177. package/dist/quickstart-api.js +86 -48
  178. package/dist/quickstart-api.js.map +1 -1
  179. package/dist/quickstart.d.ts +1 -0
  180. package/dist/quickstart.js +16 -6
  181. package/dist/quickstart.js.map +1 -1
  182. package/dist/restart-guard.d.ts +52 -0
  183. package/dist/restart-guard.js +109 -0
  184. package/dist/restart-guard.js.map +1 -0
  185. package/dist/scheduler/db.d.ts +7 -1
  186. package/dist/scheduler/db.js +44 -2
  187. package/dist/scheduler/db.js.map +1 -1
  188. package/dist/scheduler/index.d.ts +1 -1
  189. package/dist/scheduler/retry-label.d.ts +11 -0
  190. package/dist/scheduler/retry-label.js +24 -0
  191. package/dist/scheduler/retry-label.js.map +1 -0
  192. package/dist/scheduler/scheduler.d.ts +55 -2
  193. package/dist/scheduler/scheduler.js +220 -8
  194. package/dist/scheduler/scheduler.js.map +1 -1
  195. package/dist/scheduler/types.d.ts +22 -0
  196. package/dist/scheduler/types.js.map +1 -1
  197. package/dist/secret-store.d.ts +5 -0
  198. package/dist/secret-store.js +20 -0
  199. package/dist/secret-store.js.map +1 -1
  200. package/dist/service-activation.d.ts +152 -0
  201. package/dist/service-activation.js +407 -0
  202. package/dist/service-activation.js.map +1 -0
  203. package/dist/service-installer.d.ts +63 -1
  204. package/dist/service-installer.js +141 -16
  205. package/dist/service-installer.js.map +1 -1
  206. package/dist/service-plan.d.ts +65 -0
  207. package/dist/service-plan.js +108 -0
  208. package/dist/service-plan.js.map +1 -0
  209. package/dist/service-restart-selection.d.ts +6 -5
  210. package/dist/service-restart-selection.js +6 -5
  211. package/dist/service-restart-selection.js.map +1 -1
  212. package/dist/settings-api.d.ts +10 -2
  213. package/dist/settings-api.js +216 -72
  214. package/dist/settings-api.js.map +1 -1
  215. package/dist/settings-baseline.d.ts +20 -0
  216. package/dist/settings-baseline.js +104 -0
  217. package/dist/settings-baseline.js.map +1 -0
  218. package/dist/settings-change.d.ts +21 -0
  219. package/dist/settings-change.js +161 -0
  220. package/dist/settings-change.js.map +1 -0
  221. package/dist/settings-confirmation.d.ts +105 -0
  222. package/dist/settings-confirmation.js +232 -0
  223. package/dist/settings-confirmation.js.map +1 -0
  224. package/dist/settings-control.d.ts +41 -0
  225. package/dist/settings-control.js +294 -0
  226. package/dist/settings-control.js.map +1 -0
  227. package/dist/settings-effect.d.ts +24 -0
  228. package/dist/settings-effect.js +169 -0
  229. package/dist/settings-effect.js.map +1 -0
  230. package/dist/settings-http-confirmation.d.ts +29 -0
  231. package/dist/settings-http-confirmation.js +123 -0
  232. package/dist/settings-http-confirmation.js.map +1 -0
  233. package/dist/settings-instance-patch.d.ts +4 -0
  234. package/dist/settings-instance-patch.js +26 -0
  235. package/dist/settings-instance-patch.js.map +1 -0
  236. package/dist/settings-request-capability.d.ts +16 -0
  237. package/dist/settings-request-capability.js +32 -0
  238. package/dist/settings-request-capability.js.map +1 -0
  239. package/dist/settings-transaction.d.ts +65 -0
  240. package/dist/settings-transaction.js +223 -0
  241. package/dist/settings-transaction.js.map +1 -0
  242. package/dist/setup-auth.d.ts +10 -18
  243. package/dist/setup-auth.js +14 -38
  244. package/dist/setup-auth.js.map +1 -1
  245. package/dist/setup-form.d.ts +1 -1
  246. package/dist/setup-form.js +75 -18
  247. package/dist/setup-form.js.map +1 -1
  248. package/dist/setup-guide.d.ts +13 -0
  249. package/dist/setup-guide.js +25 -0
  250. package/dist/setup-guide.js.map +1 -0
  251. package/dist/setup-host.d.ts +16 -0
  252. package/dist/setup-host.js +199 -35
  253. package/dist/setup-host.js.map +1 -1
  254. package/dist/setup-wizard.js +5 -1
  255. package/dist/setup-wizard.js.map +1 -1
  256. package/dist/slash-authz.d.ts +17 -3
  257. package/dist/slash-authz.js +5 -0
  258. package/dist/slash-authz.js.map +1 -1
  259. package/dist/spawn-gate.js +3 -3
  260. package/dist/spawn-gate.js.map +1 -1
  261. package/dist/sqlite-open-errors.d.ts +12 -0
  262. package/dist/sqlite-open-errors.js +13 -0
  263. package/dist/sqlite-open-errors.js.map +1 -0
  264. package/dist/statusline-watcher.d.ts +5 -0
  265. package/dist/statusline-watcher.js +2 -0
  266. package/dist/statusline-watcher.js.map +1 -1
  267. package/dist/steer-capability.d.ts +6 -0
  268. package/dist/steer-capability.js +8 -0
  269. package/dist/steer-capability.js.map +1 -1
  270. package/dist/sync-work-attribution.d.ts +14 -0
  271. package/dist/sync-work-attribution.js +43 -8
  272. package/dist/sync-work-attribution.js.map +1 -1
  273. package/dist/tips.js +10 -5
  274. package/dist/tips.js.map +1 -1
  275. package/dist/tmux-control.d.ts +57 -5
  276. package/dist/tmux-control.js +379 -61
  277. package/dist/tmux-control.js.map +1 -1
  278. package/dist/tmux-manager.d.ts +20 -6
  279. package/dist/tmux-manager.js +90 -47
  280. package/dist/tmux-manager.js.map +1 -1
  281. package/dist/tmux-read.d.ts +59 -0
  282. package/dist/tmux-read.js +213 -0
  283. package/dist/tmux-read.js.map +1 -0
  284. package/dist/topic-commands.d.ts +21 -10
  285. package/dist/topic-commands.js +205 -63
  286. package/dist/topic-commands.js.map +1 -1
  287. package/dist/transcript-delta-reader.js +2 -1
  288. package/dist/transcript-delta-reader.js.map +1 -1
  289. package/dist/tunnel/cloudflared-install.d.ts +2 -0
  290. package/dist/tunnel/cloudflared-install.js +1 -1
  291. package/dist/tunnel/cloudflared-install.js.map +1 -1
  292. package/dist/tunnel/purpose-lane.d.ts +14 -0
  293. package/dist/tunnel/purpose-lane.js +43 -0
  294. package/dist/tunnel/purpose-lane.js.map +1 -0
  295. package/dist/turn-reply-guard.d.ts +2 -0
  296. package/dist/turn-reply-guard.js +10 -1
  297. package/dist/turn-reply-guard.js.map +1 -1
  298. package/dist/types.d.ts +33 -0
  299. package/dist/ui/app.html +18 -0
  300. package/dist/ui/chat-commands.js +43 -0
  301. package/dist/ui/chat-render.js +478 -0
  302. package/dist/ui/chat-store.js +318 -0
  303. package/dist/ui/chat-strings.js +89 -0
  304. package/dist/ui/chat-thread.js +246 -0
  305. package/dist/ui/chat-tour.js +116 -0
  306. package/dist/ui/fleet-cache.js +168 -0
  307. package/dist/ui/fleet-org.js +142 -0
  308. package/dist/ui/panel-chat.js +504 -0
  309. package/dist/ui/panel-fleet.js +427 -0
  310. package/dist/ui/panel-needs.js +202 -0
  311. package/dist/ui/panel-settings.js +754 -0
  312. package/dist/ui/preview-panel.js +202 -0
  313. package/dist/ui/preview.js +270 -0
  314. package/dist/ui/settings-apply.js +239 -0
  315. package/dist/ui/settings-confirm.js +186 -0
  316. package/dist/ui/settings-dialogs.js +546 -0
  317. package/dist/ui/settings-model.js +277 -0
  318. package/dist/ui/settings-strings.js +654 -0
  319. package/dist/ui/settings-wizard.js +140 -0
  320. package/dist/ui/shared/agend-auth.js +93 -0
  321. package/dist/ui/shared/app-ctx.js +80 -0
  322. package/dist/ui/shared/app-html.js +9 -0
  323. package/dist/ui/shared/app-i18n.js +95 -0
  324. package/dist/ui/shared/app-nav.js +73 -0
  325. package/dist/ui/shared/app-needs.js +114 -0
  326. package/dist/ui/shared/app-route.js +73 -0
  327. package/dist/ui/shared/app-session.js +66 -0
  328. package/dist/ui/shared/app-shell.js +346 -0
  329. package/dist/ui/shared/app-store.js +51 -0
  330. package/dist/ui/shared/app-stream.js +201 -0
  331. package/dist/ui/shared/app.css +820 -0
  332. package/dist/ui/shared/app.js +88 -0
  333. package/dist/ui/shared/fonts/OFL.txt +92 -0
  334. package/dist/ui/shared/fonts/README.md +21 -0
  335. package/dist/ui/shared/fonts/inter.woff2 +0 -0
  336. package/dist/ui/shared/htm.module.js +1 -0
  337. package/dist/ui/shared/panel-view.js +655 -0
  338. package/dist/ui/shared/preact-hooks.module.js +2 -0
  339. package/dist/ui/shared/preact.module.js +2 -0
  340. package/dist/ui/shared/signin.js +142 -0
  341. package/dist/ui/shared/theme.js +29 -0
  342. package/dist/ui/shared/tokens.css +153 -0
  343. package/dist/ui/shared/ui-confirm.js +43 -0
  344. package/dist/ui/shared/ui-dialog.js +45 -0
  345. package/dist/ui/shared/ui-icons.js +63 -0
  346. package/dist/ui/shared/ui-menu.js +39 -0
  347. package/dist/ui/shared/ui-states.js +20 -0
  348. package/dist/ui/shared/ui-toast.js +18 -0
  349. package/dist/ui/shared/vendor/htm.LICENSE +202 -0
  350. package/dist/ui/shared/vendor/preact.LICENSE +21 -0
  351. package/dist/ui/shared/vendor/vendor.json +39 -0
  352. package/dist/ui/shared/view-strings.js +309 -0
  353. package/dist/ui/signin.html +52 -0
  354. package/dist/update-check.d.ts +6 -2
  355. package/dist/update-check.js +12 -3
  356. package/dist/update-check.js.map +1 -1
  357. package/dist/update-dispatch.d.ts +28 -0
  358. package/dist/update-dispatch.js +73 -0
  359. package/dist/update-dispatch.js.map +1 -0
  360. package/dist/update-install.d.ts +129 -0
  361. package/dist/update-install.js +262 -0
  362. package/dist/update-install.js.map +1 -0
  363. package/dist/upgrade-notices.d.ts +17 -0
  364. package/dist/upgrade-notices.js +93 -0
  365. package/dist/upgrade-notices.js.map +1 -0
  366. package/dist/usage/statusline-usage.d.ts +2 -0
  367. package/dist/usage/statusline-usage.js +14 -10
  368. package/dist/usage/statusline-usage.js.map +1 -1
  369. package/dist/view-api.d.ts +6 -1
  370. package/dist/view-api.js +62 -51
  371. package/dist/view-api.js.map +1 -1
  372. package/dist/web-api.d.ts +85 -14
  373. package/dist/web-api.js +557 -77
  374. package/dist/web-api.js.map +1 -1
  375. package/dist/web-auth.d.ts +71 -27
  376. package/dist/web-auth.js +169 -69
  377. package/dist/web-auth.js.map +1 -1
  378. package/dist/web-body.d.ts +3 -0
  379. package/dist/web-body.js +55 -0
  380. package/dist/web-body.js.map +1 -0
  381. package/dist/web-channel-echo.d.ts +9 -0
  382. package/dist/web-channel-echo.js +55 -0
  383. package/dist/web-channel-echo.js.map +1 -0
  384. package/dist/web-chat-history.d.ts +125 -0
  385. package/dist/web-chat-history.js +184 -0
  386. package/dist/web-chat-history.js.map +1 -0
  387. package/dist/web-commands.d.ts +77 -0
  388. package/dist/web-commands.js +81 -0
  389. package/dist/web-commands.js.map +1 -0
  390. package/dist/web-continuation.d.ts +7 -0
  391. package/dist/web-continuation.js +22 -0
  392. package/dist/web-continuation.js.map +1 -0
  393. package/dist/web-host-guard.d.ts +34 -0
  394. package/dist/web-host-guard.js +57 -1
  395. package/dist/web-host-guard.js.map +1 -1
  396. package/dist/web-login.d.ts +72 -0
  397. package/dist/web-login.js +132 -0
  398. package/dist/web-login.js.map +1 -0
  399. package/dist/web-org.d.ts +55 -0
  400. package/dist/web-org.js +67 -0
  401. package/dist/web-org.js.map +1 -0
  402. package/dist/web-preview.d.ts +77 -0
  403. package/dist/web-preview.js +240 -0
  404. package/dist/web-preview.js.map +1 -0
  405. package/dist/web-request-context.d.ts +10 -0
  406. package/dist/web-request-context.js +13 -0
  407. package/dist/web-request-context.js.map +1 -0
  408. package/dist/web-session.d.ts +167 -0
  409. package/dist/web-session.js +389 -0
  410. package/dist/web-session.js.map +1 -0
  411. package/dist/web-shell-routes.d.ts +59 -0
  412. package/dist/web-shell-routes.js +96 -0
  413. package/dist/web-shell-routes.js.map +1 -0
  414. package/dist/web-upload.d.ts +183 -0
  415. package/dist/web-upload.js +468 -0
  416. package/dist/web-upload.js.map +1 -0
  417. package/dist/website-tips-renderer.d.ts +2 -2
  418. package/dist/website-tips-renderer.js +3 -3
  419. package/dist/website-tips-renderer.js.map +1 -1
  420. package/launcher/agend +110 -0
  421. package/launcher/agend-agent +110 -0
  422. package/launcher/agend-agent.cjs +4 -0
  423. package/launcher/agend.cjs +4 -0
  424. package/launcher/install-admission.cjs +105 -0
  425. package/launcher/launch.cjs +64 -0
  426. package/launcher/postinstall.cjs +114 -0
  427. package/launcher/run-in-process.cjs +13 -0
  428. package/launcher/runtime-platform.cjs +86 -0
  429. package/launcher/runtime-select.cjs +217 -0
  430. package/package.json +18 -7
  431. package/scripts/preinstall-guard.cjs +55 -0
  432. package/templates/launchd.plist.ejs +2 -2
  433. package/templates/systemd.service.ejs +1 -1
  434. package/dist/ui/dashboard.html +0 -774
  435. package/dist/ui/settings.html +0 -2165
  436. package/dist/ui/view.html +0 -1139
@@ -0,0 +1,655 @@
1
+ // #1408 step 2: the View panel (/view, /view/<name>) — every agent's live terminal, its profile card, the AI usage
2
+ // panel, in the app shell. Public (/assets/): under `web.view_access: open` an anonymous reader opens it in the
3
+ // View-only shell, so nothing here needs a session to show, and the one write (Edit profile) asks for one.
4
+ //
5
+ // Its roster is the sidebar's section while it is mounted: groups by tag, the #999 filter, this browser's order (drag).
6
+ // Everything recurring is a passive read (#1374) taken through the navigation's lease: the pane every 800 ms and the
7
+ // roster every 5 s while the panel is mounted and the tab is visible, usage every 60 s while its dialog is open. They
8
+ // stop when the panel goes.
9
+ import { html, useEffect, useLayoutEffect, useMemo, useRef, useState } from "./app-html.js";
10
+ import { t } from "./app-i18n.js";
11
+ import { appStore, createStore, useStore } from "./app-store.js";
12
+ import { useLease } from "./app-ctx.js";
13
+ import { PanelHeader, setTitle, setSideSection, closeDrawer, onPanelKey, signInHref } from "./app-shell.js";
14
+ import { navigate } from "./app-nav.js";
15
+ import { viewPath } from "./app-route.js";
16
+ import { Dialog } from "./ui-dialog.js";
17
+ import { Empty, ErrorState, Skeleton } from "./ui-states.js";
18
+ import { Icon } from "./ui-icons.js";
19
+ import "./view-strings.js";
20
+
21
+ /** view.* text with named values: tn("paneFailed", { name, status }). */
22
+ function tn(key, values = {}) {
23
+ let s = t(`view.${key}`);
24
+ for (const [k, v] of Object.entries(values)) s = s.split(`{${k}}`).join(String(v));
25
+ return s;
26
+ }
27
+ const stored = (k) => { try { return localStorage.getItem(k); } catch { return null; } };
28
+ const store = (k, v) => { try { localStorage.setItem(k, v); } catch { /* this page only */ } };
29
+
30
+ // ── ANSI (SGR) → markup ── A coloured run is <span class="ansi" data-fg data-bg data-b>, never a style attribute (#1300);
31
+ // paintAnsi() sets the colours through the style object once it is on the page. The colours are the palette's or rgb()
32
+ // of numbers — nothing from the pane's text.
33
+ const BASE = ["#000000", "#cd3131", "#0dbc79", "#e5e510", "#2472c8", "#bc3fbc", "#11a8cd", "#e5e5e5"];
34
+ const BRIGHT = ["#666666", "#f14c4c", "#23d18b", "#f5f543", "#3b8eea", "#d670d6", "#29b8db", "#ffffff"];
35
+ export const esc = (s) => String(s).replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));
36
+ function xterm256(n) {
37
+ if (n < 8) return BASE[n];
38
+ if (n < 16) return BRIGHT[n - 8];
39
+ if (n < 232) { n -= 16; const r = Math.floor(n / 36), g = Math.floor((n % 36) / 6), b = n % 6; const v = (x) => (x ? x * 40 + 55 : 0); return `rgb(${v(r)},${v(g)},${v(b)})`; }
40
+ const l = (n - 232) * 10 + 8; return `rgb(${l},${l},${l})`;
41
+ }
42
+ export function ansiToHtml(text) {
43
+ let out = "", fg = null, bg = null, bold = false, open = false;
44
+ const attrs = () => {
45
+ let f = fg;
46
+ if (bold && typeof f === "number" && f < 8) f = BRIGHT[f];
47
+ else if (typeof f === "number") f = BASE[f];
48
+ const b = bg == null ? null : typeof bg === "number" ? BASE[bg] : bg;
49
+ return (f ? ` data-fg="${esc(f)}"` : "") + (b ? ` data-bg="${esc(b)}"` : "") + (bold ? " data-b" : "");
50
+ };
51
+ const flush = () => { if (open) { out += "</span>"; open = false; } };
52
+ const openSpan = () => { const a = attrs(); if (a) { out += `<span class="ansi"${a}>`; open = true; } };
53
+ const re = /\x1b\[([0-9;]*)m/g;
54
+ let last = 0, m;
55
+ const emit = (chunk) => { if (!chunk) return; if (!open) openSpan(); out += esc(chunk); };
56
+ while ((m = re.exec(text)) !== null) {
57
+ emit(text.slice(last, m.index));
58
+ last = re.lastIndex;
59
+ flush();
60
+ const codes = m[1].split(";").filter((x) => x !== "").map(Number);
61
+ if (codes.length === 0) codes.push(0);
62
+ for (let i = 0; i < codes.length; i++) {
63
+ const c = codes[i];
64
+ if (c === 0) { fg = bg = null; bold = false; }
65
+ else if (c === 1) bold = true;
66
+ else if (c === 22) bold = false;
67
+ else if (c === 39) fg = null;
68
+ else if (c === 49) bg = null;
69
+ else if (c >= 30 && c <= 37) fg = c - 30;
70
+ else if (c >= 90 && c <= 97) fg = BRIGHT[c - 90];
71
+ else if (c >= 40 && c <= 47) bg = c - 40;
72
+ else if (c >= 100 && c <= 107) bg = BRIGHT[c - 100];
73
+ else if (c === 38 || c === 48) {
74
+ const target = c === 38 ? "fg" : "bg";
75
+ if (codes[i + 1] === 5) { const col = xterm256(codes[i + 2]); if (target === "fg") fg = col; else bg = col; i += 2; }
76
+ else if (codes[i + 1] === 2) { const col = `rgb(${codes[i + 2] || 0},${codes[i + 3] || 0},${codes[i + 4] || 0})`; if (target === "fg") fg = col; else bg = col; i += 4; }
77
+ }
78
+ }
79
+ }
80
+ emit(text.slice(last));
81
+ flush();
82
+ return out;
83
+ }
84
+ function paintAnsi(root) {
85
+ for (const el of root.querySelectorAll(".ansi[data-fg], .ansi[data-bg]")) {
86
+ if (el.dataset.fg) el.style.color = el.dataset.fg;
87
+ if (el.dataset.bg) el.style.background = el.dataset.bg;
88
+ }
89
+ }
90
+
91
+ // ── The roster: groups, the #999 filter, this browser's order ──
92
+ export function groupOf(it) {
93
+ const tags = it.tags || [];
94
+ if (tags.includes("classic")) return "Classic";
95
+ if (tags.length) return tags[0];
96
+ return "Other";
97
+ }
98
+ const BACKEND_LABELS = { "claude-code": "Claude Code", "kiro-cli": "Kiro CLI", codex: "Codex", grok: "Grok Build", antigravity: "Antigravity", opencode: "OpenCode" };
99
+ export function backendLabel(backend) { return BACKEND_LABELS[backend] || backend || "Unknown"; }
100
+ const KNOWN_BACKENDS = new Set(Object.keys(BACKEND_LABELS));
101
+ const STATUS_KEYS = { running: "statusRunning", paused: "statusPaused", stopped: "statusStopped", crashed: "statusCrashed" };
102
+
103
+ export function normalizeFilter(query) { return String(query == null ? "" : query).trim().toLowerCase(); }
104
+ /** Case-insensitive substring on name (incl. display name), model and backend. */
105
+ export function instanceMatchesFilter(it, needle, labelOf) {
106
+ if (!needle) return true;
107
+ const fields = [it.instance_name, it.display_name, it.model, it.backend, labelOf ? labelOf(it.backend) : null];
108
+ return fields.some((value) => typeof value === "string" && value.toLowerCase().includes(needle));
109
+ }
110
+ /** Visible groups in display order, and the "N / M" counts. */
111
+ export function filterSidebar(groupNames, instByGroup, rosterByName, query, labelOf) {
112
+ const needle = normalizeFilter(query);
113
+ const groups = [];
114
+ let shown = 0, total = 0;
115
+ for (const g of groupNames) {
116
+ const names = (instByGroup.get(g) || []).filter((name) => rosterByName.has(name));
117
+ total += names.length;
118
+ const visible = names.filter((name) => instanceMatchesFilter(rosterByName.get(name), needle, labelOf));
119
+ shown += visible.length;
120
+ if (visible.length) groups.push({ group: g, names: visible });
121
+ }
122
+ return { groups, shown, total, active: needle.length > 0 };
123
+ }
124
+ export function instanceTooltip(it) {
125
+ const context = it.context_pct != null && Number.isFinite(it.context_pct) ? `${Math.round(Math.min(100, Math.max(0, it.context_pct)))}%` : tn("tooltipUnavailable");
126
+ const status = STATUS_KEYS[it.status] ? tn(STATUS_KEYS[it.status]) : it.status || tn("statusUnknown");
127
+ const line = (key, value) => tn(key, { value });
128
+ const lines = [];
129
+ if (it.display_name && it.display_name !== it.instance_name) lines.push(it.display_name, `(${it.instance_name})`);
130
+ else lines.push(it.instance_name);
131
+ lines.push(line("tooltipBackend", backendLabel(it.backend)));
132
+ const src = (s) => (s === "instance" || s === "classic" ? " (configured)" : s === "fleet-default" ? " (fleet default)" : "");
133
+ lines.push(line("tooltipModel", (it.model || tn("tooltipUnavailable")) + src(it.model_source)));
134
+ lines.push(line("tooltipStatus", status));
135
+ lines.push(line("tooltipContext", context));
136
+ if (it.effort) lines.push(line("tooltipEffort", it.effort + src(it.effort_source)));
137
+ if (typeof it.description === "string" && it.description.trim()) lines.push("", it.description.trim());
138
+ return lines.join("\n");
139
+ }
140
+
141
+ const SIDEBAR_ORDER_KEY = "agend_view_sidebar_order";
142
+ function readOrder() {
143
+ const groups = new Map(), insts = new Map();
144
+ try {
145
+ const rows = JSON.parse(stored(SIDEBAR_ORDER_KEY) || "[]");
146
+ if (Array.isArray(rows)) for (const row of rows) {
147
+ if (row && row.item_type === "group" && typeof row.item_name === "string") groups.set(row.item_name, Number(row.sort_index) || 0);
148
+ else if (row && row.item_type === "instance" && typeof row.item_name === "string") insts.set(row.item_name, Number(row.sort_index) || 0);
149
+ }
150
+ } catch { /* ignore invalid browser-local state */ }
151
+ return { groups, insts };
152
+ }
153
+ /** Display order: this browser's saved order first, then new groups/instances by tag and name. */
154
+ export function computeOrder(roster, order) {
155
+ const groups = new Map();
156
+ for (const it of roster) { const g = groupOf(it); if (!groups.has(g)) groups.set(g, []); groups.get(g).push(it); }
157
+ const gidx = (g) => (order.groups.has(g) ? order.groups.get(g) : Infinity);
158
+ const groupNames = [...groups.keys()].sort((a, b) => {
159
+ const ia = gidx(a), ib = gidx(b);
160
+ if (ia !== ib) return ia - ib;
161
+ if (a === "Other") return 1;
162
+ if (b === "Other") return -1;
163
+ return a.localeCompare(b);
164
+ });
165
+ const instByGroup = new Map();
166
+ for (const [g, items] of groups) {
167
+ instByGroup.set(g, items.map((it) => it.instance_name).sort((a, b) => {
168
+ const ia = order.insts.has(a) ? order.insts.get(a) : Infinity, ib = order.insts.has(b) ? order.insts.get(b) : Infinity;
169
+ return ia - ib;
170
+ }));
171
+ }
172
+ return { groupNames, instByGroup };
173
+ }
174
+ function moveIn(arr, moving, target, before) {
175
+ if (!arr) return;
176
+ const from = arr.indexOf(moving);
177
+ if (from === -1) return;
178
+ arr.splice(from, 1);
179
+ let to = arr.indexOf(target);
180
+ if (to === -1) { arr.push(moving); return; }
181
+ if (!before) to += 1;
182
+ arr.splice(to, 0, moving);
183
+ }
184
+
185
+ /** The roster shared by the panel and its sidebar section, for as long as the page lives. */
186
+ export const viewStore = createStore({ loaded: false, error: null, roster: [], filter: "", collapsed: new Set(), current: null, order: readOrder() });
187
+ const byName = (roster) => new Map(roster.map((r) => [r.instance_name, r]));
188
+
189
+ function persistOrder(groupNames, instByGroup) {
190
+ const rows = [];
191
+ groupNames.forEach((g, i) => rows.push({ item_type: "group", item_name: g, sort_index: i, group_name: null }));
192
+ 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 }));
193
+ store(SIDEBAR_ORDER_KEY, JSON.stringify(rows));
194
+ viewStore.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])) } });
195
+ }
196
+
197
+ /** The sidebar section while View is mounted: the filter, then the groups and their instances (links to /view/<name>). */
198
+ function ViewRoster() {
199
+ const v = useStore(viewStore);
200
+ const filterRef = useRef(null);
201
+ const drag = useRef(null);
202
+ const rosterByName = byName(v.roster);
203
+ const { groupNames, instByGroup } = computeOrder(v.roster, v.order);
204
+ const shown = filterSidebar(groupNames, instByGroup, rosterByName, v.filter, backendLabel);
205
+ const setFilter = (value) => viewStore.set({ filter: value });
206
+ const toggle = (g) => { const c = new Set(v.collapsed); if (c.has(g)) c.delete(g); else c.add(g); viewStore.set({ collapsed: c }); };
207
+ // Drag to reorder: only the full list (a filtered subset would be ambiguous), groups among groups, instances within
208
+ // their group.
209
+ const dragProps = (item) => shown.active ? {} : {
210
+ draggable: "true",
211
+ 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 */ } },
212
+ onDragEnd: (e) => { e.currentTarget.classList.remove("dragging"); drag.current = null; },
213
+ onDragOver: (e) => {
214
+ const d = drag.current;
215
+ if (!d || d.type !== item.type || d.name === item.name || (item.type === "instance" && d.group !== item.group)) return;
216
+ e.preventDefault();
217
+ const r = e.currentTarget.getBoundingClientRect();
218
+ const before = e.clientY < r.top + r.height / 2;
219
+ e.currentTarget.classList.toggle("drop-before", before);
220
+ e.currentTarget.classList.toggle("drop-after", !before);
221
+ },
222
+ onDragLeave: (e) => e.currentTarget.classList.remove("drop-before", "drop-after"),
223
+ onDrop: (e) => {
224
+ e.preventDefault();
225
+ const before = e.currentTarget.classList.contains("drop-before");
226
+ e.currentTarget.classList.remove("drop-before", "drop-after");
227
+ const d = drag.current;
228
+ if (!d || d.type !== item.type) return;
229
+ const names = [...groupNames], insts = new Map([...instByGroup].map(([g, n]) => [g, [...n]]));
230
+ if (item.type === "group") moveIn(names, d.name, item.name, before);
231
+ else if (d.group === item.group) moveIn(insts.get(item.group), d.name, item.name, before);
232
+ else return;
233
+ persistOrder(names, insts);
234
+ },
235
+ };
236
+ // The list scrolls; the filter under it stays in view (#999), so it sits outside the scrolling section.
237
+ return html`<div class="view-roster">
238
+ <div class="side-section" id="instanceList">
239
+ <h2 class="side-label">${tn("instancesLabel")}</h2>
240
+ ${!v.loaded ? html`<${Skeleton} lines=${4} />` : !v.roster.length ? html`<p class="side-empty">${tn("noInstances")}</p>` : null}
241
+ ${shown.groups.map(({ group: g, names }) => {
242
+ const folded = v.collapsed.has(g);
243
+ return html`<div key=${g} class="v-group">
244
+ <button type="button" class="v-group-head" aria-expanded=${folded ? "false" : "true"} onClick=${() => toggle(g)}
245
+ title=${tn(folded ? "expandGroup" : "collapseGroup", { group: g })} ...${dragProps({ type: "group", name: g })}>
246
+ <${Icon} name="chevron" size=${14} cls=${folded ? "caret folded" : "caret"} /><span class="grow">${g}</span><span class="count">${names.length}</span></button>
247
+ ${folded ? null : html`<ul class="inst-list">${names.map((name) => {
248
+ const it = rosterByName.get(name);
249
+ const alias = typeof it.display_name === "string" && it.display_name.trim() && it.display_name.trim() !== name ? it.display_name.trim() : "";
250
+ const active = name === v.current;
251
+ return html`<li key=${name}><a class=${`inst v-inst${active ? " active" : ""}`} href=${viewPath(name)} title=${instanceTooltip(it)}
252
+ aria-current=${active ? "page" : undefined} onClick=${closeDrawer} ...${dragProps({ type: "instance", name, group: g })}>
253
+ <span class=${`dot ${it.status === "running" ? "ok" : it.status === "crashed" ? "bad" : "off"}`} aria-hidden="true"></span>
254
+ <span class="inst-text"><span class="inst-name">${name}</span>${alias ? html`<span class="inst-sub"><span class="inst-alias">${alias}</span></span>` : null}</span>
255
+ <span class="v-meta">${it.context_pct != null ? `${Math.round(it.context_pct)}%` : ""}</span>
256
+ <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>
257
+ </a></li>`;
258
+ })}</ul>`}
259
+ </div>`;
260
+ })}
261
+ ${v.loaded && shown.active && !shown.groups.length ? html`<p class="side-empty">${tn("filterNone")}</p>` : null}
262
+ </div>
263
+ <div class="v-filter">
264
+ <label class="v-filter-box"><${Icon} name="search" size=${14} /><span class="sr-only">${tn("filterPh")}</span>
265
+ <input ref=${filterRef} id="filterInput" type="text" autocomplete="off" spellcheck="false" placeholder=${tn("filterPh")} value=${v.filter}
266
+ onInput=${(e) => setFilter(e.target.value)}
267
+ onKeyDown=${(e) => { if (e.key === "Escape" && v.filter) { e.preventDefault(); e.stopPropagation(); setFilter(""); } }} />
268
+ ${v.filter ? html`<button type="button" class="icon-btn v-filter-clear" aria-label=${tn("filterClear")} title=${tn("filterClear")}
269
+ onClick=${() => { setFilter(""); filterRef.current && filterRef.current.focus(); }}><${Icon} name="close" size=${14} /></button>` : null}
270
+ </label>
271
+ <p class="note" aria-live="polite">${shown.shown} / ${shown.total}${shown.active ? ` ${tn("filterShown")}` : ""}</p>
272
+ </div>
273
+ </div>`;
274
+ }
275
+
276
+ // ── The panel ──
277
+
278
+ /**
279
+ * One stream of reads under a lease (the pane, the roster, usage): one at a time, and only the newest may land. A tick
280
+ * while a read is on its way is skipped; a forced read (Refresh), or one started after the last has hung STUCK_MS,
281
+ * supersedes it — and a superseded read's answer is dropped at every step: its response, its body, its commit
282
+ * (#1448 review: an interval alone starts overlapping reads, and lease.current() only tells navigations apart).
283
+ */
284
+ const STUCK_MS = 10_000;
285
+ const clock = () => (typeof performance !== "undefined" ? performance.now() : Date.now());
286
+ export function readStream(lease) {
287
+ let gen = 0, busy = false, since = 0;
288
+ return {
289
+ /** A token for a new read, or 0 when this one should not start. */
290
+ begin(force = false) {
291
+ const at = clock();
292
+ if (busy && !force && at - since < STUCK_MS) return 0;
293
+ busy = true; since = at;
294
+ return ++gen;
295
+ },
296
+ /** May this read still land? */
297
+ live: (token) => token === gen && lease.current(),
298
+ end(token) { if (token === gen) busy = false; },
299
+ };
300
+ }
301
+
302
+ const DENSITY = { fit: 1, comfortable: 1.25, compact: 0.8 };
303
+ const DENSITY_ORDER = ["fit", "comfortable", "compact"];
304
+ const MIN_PX = 12, MAX_PX = 22;
305
+
306
+ export function ViewPanel({ route, navKey }) {
307
+ const lease = useLease(navKey);
308
+ const v = useStore(viewStore);
309
+ const { viewOnly } = useStore(appStore);
310
+ // { kind: "edit" | "usage" | "help", key }: a dialog belongs to the navigation that opened it. The profile editor's
311
+ // draft names one agent; on another navigation it is gone, never retargeted (#1448 review).
312
+ const [dialog, setDialogState] = useState(null);
313
+ const setDialog = (kind) => setDialogState(kind ? { kind, key: navKey } : null);
314
+ const open = dialog && dialog.key === navKey ? dialog.kind : null;
315
+ const [usageAvailable, setUsageAvailable] = useState(false);
316
+ const [density, setDensity] = useState(DENSITY[stored("agend_view_density")] ? stored("agend_view_density") : "fit");
317
+ const name = route.instance;
318
+ const it = name ? v.roster.find((r) => r.instance_name === name) : null;
319
+
320
+ // The roster is the sidebar's section while View is mounted.
321
+ useEffect(() => lease.hold(setSideSection(ViewRoster)), [lease]);
322
+ useEffect(() => { viewStore.set({ current: name }); }, [name]);
323
+ useEffect(() => { setTitle(it ? (it.display_name || it.instance_name) : tn("title")); }, [name, it && it.display_name, navKey]);
324
+ if (name && it) store("agend_last_view", name);
325
+
326
+ // The roster: now, then every 5 s while this panel is mounted and the tab is visible (a passive read).
327
+ useEffect(() => {
328
+ const reads = readStream(lease);
329
+ const load = async () => {
330
+ if (typeof document !== "undefined" && document.hidden) return;
331
+ const token = reads.begin();
332
+ if (!token) return;
333
+ try {
334
+ const r = await lease.fetch("/api/profiles");
335
+ if (!reads.live(token)) return;
336
+ if (!r.ok) throw new Error(`HTTP ${r.status}`);
337
+ const roster = await r.json();
338
+ if (!reads.live(token)) return;
339
+ viewStore.set({ loaded: true, error: null, roster: Array.isArray(roster) ? roster : [] });
340
+ } catch (e) { if (reads.live(token)) viewStore.set({ loaded: true, error: e && e.message ? e.message : "error" }); }
341
+ finally { reads.end(token); }
342
+ };
343
+ load();
344
+ lease.interval(load, 5000);
345
+ lease.on(document, "visibilitychange", () => { if (!document.hidden) load(); });
346
+ }, [lease]);
347
+ // Usage: whether the fleet offers it (a 404 means off). The read also warms the server's 5-minute cache.
348
+ useEffect(() => {
349
+ (async () => {
350
+ try { const r = await lease.fetch("/api/ai-usage"); if (lease.current()) setUsageAvailable(r.status !== 404); }
351
+ catch { /* stays hidden */ }
352
+ })();
353
+ }, [lease]);
354
+ // /view on its own: the instance this browser looked at last, else the first one.
355
+ useEffect(() => {
356
+ if (name || !v.loaded || !v.roster.length) return;
357
+ const last = stored("agend_last_view");
358
+ const pick = last && v.roster.some((r) => r.instance_name === last) ? last : v.roster[0].instance_name;
359
+ navigate(viewPath(pick), { replace: true });
360
+ }, [name, v.loaded, v.roster.length]);
361
+ // "/" focuses the filter (never while typing elsewhere).
362
+ useEffect(() => lease.hold(onPanelKey((e) => {
363
+ if (e.key !== "/" || e.ctrlKey || e.metaKey || e.altKey) return;
364
+ const target = e.target;
365
+ if (target && (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable)) return;
366
+ const f = document.getElementById("filterInput");
367
+ if (f) { e.preventDefault(); f.focus(); }
368
+ })), [lease]);
369
+
370
+ const cycleDensity = () => {
371
+ const next = DENSITY_ORDER[(DENSITY_ORDER.indexOf(density) + 1) % DENSITY_ORDER.length];
372
+ store("agend_view_density", next);
373
+ setDensity(next);
374
+ };
375
+ const densityName = tn(`density${density.charAt(0).toUpperCase()}${density.slice(1)}`);
376
+ const actions = html`
377
+ <button type="button" class="btn btn-ghost btn-sm" onClick=${cycleDensity} title=${tn("textSize", { size: densityName })} aria-label=${tn("textSize", { size: densityName })}><${Icon} name="type" size=${16} /><span class="hide-narrow" aria-hidden="true">${densityName}</span></button>
378
+ ${usageAvailable ? html`<button type="button" class="btn btn-ghost btn-sm" onClick=${() => setDialog("usage")} aria-label=${tn("usageButton")} title=${tn("usageButton")}><${Icon} name="chart" size=${16} /><span class="hide-narrow" aria-hidden="true">${tn("usageButton")}</span></button>` : null}
379
+ <button type="button" class="icon-btn" onClick=${() => setDialog("help")} aria-label=${tn("helpButton")} title=${tn("helpButton")}><${Icon} name="info" /></button>`;
380
+
381
+ let body;
382
+ if (v.error && !v.roster.length) body = html`<div class="panel-body center"><${ErrorState} message=${tn("rosterFailedShort")} onRetry=${() => navigate(location.pathname, { replace: true })} /></div>`;
383
+ else if (!v.loaded) body = html`<div class="panel-body"><${Skeleton} lines=${6} /></div>`;
384
+ else if (!v.roster.length) body = html`<div class="panel-body center"><${Empty} icon="view" title=${tn("noInstances")} /></div>`;
385
+ else if (!name) body = html`<div class="panel-body"><${Skeleton} lines=${6} /></div>`;
386
+ else if (!it) body = html`<div class="panel-body center"><${Empty} icon="alert" title=${tn("notFound", { name })} hint=${tn("notFoundHint")} /></div>`;
387
+ else body = html`<${Terminal} key=${name} name=${name} lease=${lease} density=${density} />
388
+ <${Card} it=${it} viewOnly=${viewOnly} onEdit=${() => setDialog("edit")} />`;
389
+
390
+ return html`<div class="panel p-view">
391
+ <${PanelHeader} title=${it ? (it.display_name || it.instance_name) : tn("title")}
392
+ sub=${it ? html`<span class="status"><span class=${`dot ${it.status === "running" ? "ok" : it.status === "crashed" ? "bad" : "off"}`} aria-hidden="true"></span>${tn(STATUS_KEYS[it.status] || "statusUnknown")}</span>` : null}>${actions}</${PanelHeader}>
393
+ ${body}
394
+ ${open === "edit" && it ? html`<${EditProfile} key=${navKey} it=${it} onClose=${() => setDialog(null)} />` : null}
395
+ ${open === "usage" ? html`<${UsageDialog} onClose=${() => setDialog(null)} />` : null}
396
+ ${open === "help" ? html`<${HelpDialog} onClose=${() => setDialog(null)} />` : null}
397
+ </div>`;
398
+ }
399
+
400
+ /** The live terminal: the pane every 800 ms (passive), drawn as escaped markup, its font fitted to the pane's grid. */
401
+ function Terminal({ name, lease, density }) {
402
+ const term = useRef(null), pre = useRef(null);
403
+ const grid = useRef({ cols: 0, rows: 0, applied: 0, key: "" });
404
+ const [problem, setProblem] = useState(null);
405
+ const fit = () => {
406
+ const g = grid.current, box = term.current, p = pre.current;
407
+ if (!box || !p || g.cols < 1 || g.rows < 1) return;
408
+ const cs = getComputedStyle(box);
409
+ const innerW = Math.max(1, box.clientWidth - (parseFloat(cs.paddingLeft) + parseFloat(cs.paddingRight) || 0));
410
+ const innerH = Math.max(1, box.clientHeight - (parseFloat(cs.paddingTop) + parseFloat(cs.paddingBottom) || 0));
411
+ const key = `${g.cols}x${g.rows}@${Math.round(innerW)}x${Math.round(innerH)}/${density}`;
412
+ if (key === g.key) return;
413
+ const pcs = getComputedStyle(p);
414
+ const lh = parseFloat(pcs.lineHeight) / parseFloat(pcs.fontSize);
415
+ const lineH = Number.isFinite(lh) && lh > 0 ? lh : 1.2;
416
+ let size = Math.min(innerW / (g.cols * glyphRatio(p)), innerH / (g.rows * lineH)) * 0.995;
417
+ size = Math.max(MIN_PX, Math.min(size, MAX_PX)) * (DENSITY[density] || 1);
418
+ size = Math.max(8, Math.min(size, 32));
419
+ g.key = key;
420
+ if (Math.abs(size - g.applied) < 0.75) return;
421
+ g.applied = size;
422
+ p.style.fontSize = `${size.toFixed(2)}px`; // CSSOM, never a style attribute (#1300)
423
+ };
424
+ useEffect(() => { grid.current.key = ""; fit(); }, [density]);
425
+ useEffect(() => {
426
+ const reads = readStream(lease);
427
+ const refresh = async () => {
428
+ if (document.hidden) return;
429
+ // A selection inside the pane is being copied: keep this frame until it is released.
430
+ const sel = typeof window.getSelection === "function" ? window.getSelection() : null;
431
+ if (sel && sel.toString().length > 0 && sel.anchorNode && pre.current && pre.current.contains(sel.anchorNode)) return;
432
+ const token = reads.begin();
433
+ if (!token) return;
434
+ try {
435
+ const r = await lease.fetch(`/api/pane/${encodeURIComponent(name)}`);
436
+ if (!reads.live(token)) return;
437
+ if (!r.ok) { setProblem(r.status === 404 ? tn("paneUnavailable") : tn("paneFailed", { name, status: r.status })); return; }
438
+ const text = await r.text();
439
+ if (!reads.live(token) || !pre.current) return;
440
+ const c = Number(r.headers.get("X-Pane-Cols")), rw = Number(r.headers.get("X-Pane-Rows"));
441
+ if (c >= 1 && rw >= 1 && (c !== grid.current.cols || rw !== grid.current.rows)) { grid.current.cols = c; grid.current.rows = rw; grid.current.key = ""; }
442
+ setProblem(null);
443
+ pre.current.innerHTML = ansiToHtml(text); // escaped by construction: markup only from ansiToHtml's own spans
444
+ paintAnsi(pre.current);
445
+ fit();
446
+ } catch { /* a transient error keeps the last frame */ }
447
+ finally { reads.end(token); }
448
+ };
449
+ refresh();
450
+ lease.interval(refresh, 800);
451
+ let timer = null;
452
+ const schedule = () => { lease.clear(timer); timer = lease.timeout(() => { grid.current.key = ""; fit(); }, 80); };
453
+ lease.on(window, "resize", schedule);
454
+ if (typeof ResizeObserver === "function" && term.current) {
455
+ const ro = new ResizeObserver(schedule);
456
+ ro.observe(term.current);
457
+ lease.hold(() => ro.disconnect());
458
+ }
459
+ }, [lease, name]);
460
+ return html`<div class="v-term" ref=${term}>${problem ? html`<p class="v-term-note" role="status">${problem}</p>` : null}<pre ref=${pre} class="v-pre"></pre></div>`;
461
+ }
462
+
463
+ // The advance of one monospace cell as a fraction of font-size, measured once on this page.
464
+ let cellRatio = 0;
465
+ function glyphRatio(pre) {
466
+ if (cellRatio) return cellRatio;
467
+ const probe = document.createElement("span");
468
+ probe.className = "v-probe";
469
+ probe.style.fontFamily = getComputedStyle(pre).fontFamily;
470
+ probe.textContent = "0".repeat(100);
471
+ document.body.appendChild(probe);
472
+ const w = probe.getBoundingClientRect().width;
473
+ probe.remove();
474
+ if (w > 0) cellRatio = w / 100 / 100;
475
+ return cellRatio || 0.6;
476
+ }
477
+
478
+ /** The profile card under the terminal: one line by default, the whole profile when opened. */
479
+ function Card({ it, viewOnly, onEdit }) {
480
+ const [open, setOpen] = useState(stored("agend_view_card_expanded") === "1");
481
+ const avatar = useRef(null);
482
+ // The avatar is fetched once per instance (cache-busted then), not on every 5-second roster refresh.
483
+ useLayoutEffect(() => {
484
+ if (!avatar.current || !it.has_avatar) return;
485
+ avatar.current.src = `/api/avatar/${encodeURIComponent(it.instance_name)}?t=${Date.now()}`;
486
+ }, [it.instance_name, it.has_avatar]);
487
+ const initial = (it.display_name || it.instance_name || "?").trim().charAt(0) || "?";
488
+ const ctx = it.context_pct != null ? ` · ctx ${Math.round(it.context_pct)}%` : "";
489
+ const toggle = () => { const next = !open; setOpen(next); store("agend_view_card_expanded", next ? "1" : "0"); };
490
+ return html`<section class=${`v-card${open ? " open" : ""}`} aria-label=${it.display_name || it.instance_name}>
491
+ <div class="v-avatar">${it.has_avatar ? html`<img ref=${avatar} alt="" />` : html`<span class="v-initial" aria-hidden="true">${initial}</span>`}</div>
492
+ <div class="v-card-body">
493
+ <div class="v-card-line"><strong>${it.display_name || it.instance_name}</strong>${it.role ? html`<span class="v-role">${it.role}</span>` : null}</div>
494
+ <div class="v-card-meta">${it.instance_name} · ${backendLabel(it.backend)}${it.model ? ` · ${it.model}` : ""}${ctx}</div>
495
+ ${open && it.description ? html`<p class="v-card-desc">${it.description}</p>` : null}
496
+ </div>
497
+ <button type="button" class="icon-btn" onClick=${toggle} aria-expanded=${open ? "true" : "false"} aria-label=${tn(open ? "collapseProfile" : "expandProfile")} title=${tn(open ? "collapseProfile" : "expandProfile")}>
498
+ <${Icon} name="chevron" size=${16} cls=${open ? "caret" : "caret flipped"} /></button>
499
+ ${viewOnly
500
+ ? html`<a class="btn btn-sm" href=${signInHref()}><${Icon} name="edit" size=${14} />${tn("signInToEdit")}</a>`
501
+ : html`<button type="button" class="btn btn-sm" onClick=${onEdit}><${Icon} name="edit" size=${14} />${tn("editProfile")}</button>`}
502
+ </section>`;
503
+ }
504
+
505
+ /** Edit profile: display name, role, description, avatar. A write: it needs a session (CSRF via agend-auth.js). */
506
+ function EditProfile({ it, onClose }) {
507
+ const lease = useLease(`edit:${it.instance_name}`);
508
+ const [f, setF] = useState({ display: it.display_name || "", role: it.role || "", desc: it.description || "" });
509
+ const [busy, setBusy] = useState(false);
510
+ const [msg, setMsg] = useState(null);
511
+ const file = useRef(null);
512
+ async function save() {
513
+ if (busy) return;
514
+ setBusy(true); setMsg(null);
515
+ // The whole transaction is decided now, before the first await: its target, the draft and the picked file. Every
516
+ // later step checks this dialog's lease first — a navigation ends it, and its second write never goes out.
517
+ const name = encodeURIComponent(it.instance_name);
518
+ const draft = { display_name: f.display, role: f.role, description: f.desc };
519
+ const picked = file.current && file.current.files && file.current.files[0];
520
+ try {
521
+ const r = await fetch(`/api/profile/${name}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(draft) });
522
+ if (!lease.current()) return;
523
+ if (r.status === 401) throw new Error(tn("signInToSave"));
524
+ if (!r.ok) { const e = await r.json().catch(() => ({})); throw new Error(tn("saveFailed", { error: e.error || `HTTP ${r.status}` })); }
525
+ if (picked) {
526
+ const ar = await fetch(`/api/avatar/${name}`, { method: "POST", headers: { "Content-Type": picked.type }, body: picked });
527
+ if (!ar.ok) { const e = await ar.json().catch(() => ({})); throw new Error(tn("avatarFailed", { error: e.error || ar.status })); }
528
+ }
529
+ if (!lease.current()) return;
530
+ setBusy(false);
531
+ onClose();
532
+ } catch (e) { if (lease.current()) { setBusy(false); setMsg(e.message); } }
533
+ }
534
+ return html`<${Dialog} title=${tn("profileTitle", { name: it.instance_name })} onClose=${onClose} busy=${busy}
535
+ actions=${html`<button type="button" class="btn" onClick=${onClose} disabled=${busy}>${tn("cancel")}</button>
536
+ <button type="button" class="btn btn-primary" onClick=${save} disabled=${busy}>${tn("save")}</button>`}>
537
+ <form class="form" onSubmit=${(e) => { e.preventDefault(); save(); }}><fieldset class="form" disabled=${busy}>
538
+ <label class="field"><span>${tn("displayName")}</span><input value=${f.display} onInput=${(e) => setF({ ...f, display: e.target.value })} /></label>
539
+ <label class="field"><span>${tn("role")}</span><input value=${f.role} onInput=${(e) => setF({ ...f, role: e.target.value })} /></label>
540
+ <label class="field"><span>${tn("description")}</span><textarea rows="3" value=${f.desc} onInput=${(e) => setF({ ...f, desc: e.target.value })}></textarea></label>
541
+ <label class="field"><span>${tn("avatar")}</span><input ref=${file} type="file" accept="image/png,image/jpeg,image/gif,image/webp" /></label>
542
+ </fieldset></form>
543
+ ${msg ? html`<p class="err" role="alert">${msg}${msg === tn("signInToSave") ? html` <a class="link" href=${signInHref()}>${t("app.signIn")}</a>` : null}</p>` : null}
544
+ </${Dialog}>`;
545
+ }
546
+
547
+ function HelpDialog({ onClose }) {
548
+ return html`<${Dialog} title=${tn("helpButton")} onClose=${onClose}>
549
+ <ul class="help-list">${["help1", "help2", "help3", "help4", "help5", "help6"].map((k) => html`<li key=${k}>${tn(k)}</li>`)}</ul>
550
+ </${Dialog}>`;
551
+ }
552
+
553
+ // ── AI usage ── Every provider the fleet's logins report; refreshed every 60 s while this dialog is open (passive).
554
+ const USAGE_ORDER_KEY = "agend_view_usage_order";
555
+ function usageText(fallback, ref) {
556
+ if (!ref || typeof ref.key !== "string") return fallback == null ? "" : String(fallback);
557
+ return (Array.isArray(ref.args) ? ref.args : []).reduce((text, value, i) => text.split(`{${i}}`).join(String(value)), t(`view.${ref.key}`));
558
+ }
559
+ function usageDuration(ms) {
560
+ const total = Math.max(1, Math.ceil(ms / 60000));
561
+ const d = Math.floor(total / 1440), h = Math.floor((total % 1440) / 60), m = total % 60;
562
+ if (total >= 2880) return usageText("", { key: "usage.duration.days_hours", args: [d, h] });
563
+ if (total >= 60) return usageText("", { key: "usage.duration.hours_minutes", args: [Math.floor(total / 60), m] });
564
+ return usageText("", { key: "usage.duration.minutes", args: [total] });
565
+ }
566
+ function resetText(iso) {
567
+ if (!iso) return "";
568
+ const ms = new Date(iso).getTime() - Date.now();
569
+ if (Number.isNaN(ms)) return "";
570
+ if (ms <= 0) return tn("usage.reset.soon");
571
+ return usageText("", { key: "usage.reset.in", args: [usageDuration(ms)] });
572
+ }
573
+ function expiryText(iso) {
574
+ if (!iso) return "";
575
+ const at = new Date(iso), ms = at.getTime() - Date.now();
576
+ if (!(ms > 0)) return "";
577
+ return usageText("", { key: "usage.ticket_expiry", args: [`${at.getMonth() + 1}/${at.getDate()}`, usageDuration(ms)] });
578
+ }
579
+ export function UsageMetric({ m }) {
580
+ const bar = useRef(null);
581
+ const label = usageText(m.label, m.labelI18n);
582
+ const note = m.note ? usageText(m.note, m.noteI18n) : "";
583
+ const pct = m.type === "percent" ? Math.min(100, Math.max(0, m.used ?? 0)) : 0;
584
+ useLayoutEffect(() => { if (bar.current) bar.current.style.width = `${pct}%`; }, [pct]); // CSSOM (#1300)
585
+ if (m.type === "percent") {
586
+ const cls = pct >= 90 ? "crit" : pct >= 70 ? "warn" : "";
587
+ const word = pct >= 90 ? tn("usageNear") : pct >= 70 ? tn("usageHigh") : "";
588
+ const sub = [note, resetText(m.resetsAt)].filter(Boolean).join(" · ");
589
+ return html`<div class="u-metric"><div class="u-row"><span class="u-label">${label}</span>${word ? html`<span class=${`u-status ${cls}`}>${word}</span>` : null}
590
+ <span class=${`u-pct ${cls}`}>${pct.toFixed(0)}<span class="u-unit">%</span></span></div>
591
+ <div class="u-meter"><div ref=${bar} class=${`u-fill ${cls}`}></div></div>${sub ? html`<div class="u-sub">${sub}</div>` : null}</div>`;
592
+ }
593
+ if (m.type === "dollars") {
594
+ const val = m.limit ? `$${(m.used ?? 0).toFixed(2)} / $${m.limit.toFixed(2)}` : `$${(m.used ?? 0).toFixed(2)}`;
595
+ return html`<div class="u-metric"><div class="u-row"><span class="u-label">${label}</span><span class="u-val">${val}</span></div>${note ? html`<div class="u-sub">${note}</div>` : null}</div>`;
596
+ }
597
+ const val = `${usageText(m.value ?? "", m.valueI18n)} ${usageText(m.unit ?? "", m.unitI18n)}`.trim();
598
+ const sub = [note, expiryText(m.expiresAt)].filter(Boolean).join(" · ");
599
+ return html`<div class="u-metric"><div class="u-row"><span class="u-label">${label}</span><span class="u-val">${val}</span></div>${sub ? html`<div class="u-sub">${sub}</div>` : null}</div>`;
600
+ }
601
+ function UsageDialog({ onClose }) {
602
+ const lease = useLease("usage-dialog");
603
+ const [data, setData] = useState(undefined); // undefined: loading; null: failed
604
+ const [order, setOrder] = useState(() => { try { const a = JSON.parse(stored(USAGE_ORDER_KEY) || "[]"); return Array.isArray(a) ? a.filter((x) => typeof x === "string") : []; } catch { return []; } });
605
+ const reads = useMemo(() => readStream(lease), [lease]);
606
+ // Refresh (force) supersedes a read on its way; the minute's tick waits for it.
607
+ const load = async (force) => {
608
+ const token = reads.begin(force);
609
+ if (!token) return;
610
+ try {
611
+ const r = await lease.fetch(`/api/ai-usage${force ? "?force=1" : ""}`);
612
+ if (!reads.live(token)) return;
613
+ const d = await r.json();
614
+ if (reads.live(token)) setData(d);
615
+ } catch { if (reads.live(token)) setData(null); }
616
+ finally { reads.end(token); }
617
+ };
618
+ useEffect(() => { load(false); lease.interval(() => load(false), 60_000); }, [lease]);
619
+ const key = (p) => String(p.id || p.name || "");
620
+ const ordered = (providers) => {
621
+ const rank = new Map(order.map((id, i) => [id, i]));
622
+ return providers.map((p, i) => ({ p, i })).sort((a, b) => {
623
+ const ar = rank.has(key(a.p)) ? rank.get(key(a.p)) : Infinity, br = rank.has(key(b.p)) ? rank.get(key(b.p)) : Infinity;
624
+ return ar === br ? a.i - b.i : ar - br;
625
+ }).map((e) => e.p);
626
+ };
627
+ const move = (id, delta, providers) => {
628
+ const ids = ordered(providers).map(key);
629
+ const from = ids.indexOf(id), to = from + delta;
630
+ if (from < 0 || to < 0 || to >= ids.length) return;
631
+ [ids[from], ids[to]] = [ids[to], ids[from]];
632
+ setOrder(ids); store(USAGE_ORDER_KEY, JSON.stringify(ids));
633
+ };
634
+ let content;
635
+ if (data === undefined) content = html`<${Skeleton} lines=${4} />`;
636
+ else if (!data || !Array.isArray(data.providers)) content = html`<${ErrorState} message=${tn("usageFail")} onRetry=${() => load(true)} />`;
637
+ else {
638
+ const providers = ordered(data.providers);
639
+ content = html`${!providers.length ? html`<p class="note">${tn("usage.empty_active")}</p>` : null}
640
+ ${providers.map((p, i) => html`<section key=${key(p)} class="u-provider">
641
+ <div class="u-head"><strong>${p.name}</strong>${p.plan ? html`<span class="pill">${p.plan}</span>` : null}
642
+ <span class="u-order">
643
+ <button type="button" class="icon-btn" disabled=${i === 0} aria-label=${tn("usageMoveUp")} title=${tn("usageMoveUp")} onClick=${() => move(key(p), -1, data.providers)}><${Icon} name="up" size=${14} /></button>
644
+ <button type="button" class="icon-btn" disabled=${i === providers.length - 1} aria-label=${tn("usageMoveDown")} title=${tn("usageMoveDown")} onClick=${() => move(key(p), 1, data.providers)}><${Icon} name="down" size=${14} /></button>
645
+ </span></div>
646
+ ${p.status === "error" ? html`<p class="u-err">${usageText(p.error || tn("usage.error_fallback"), p.errorI18n)}</p>`
647
+ : p.status === "no-credentials" ? html`<p class="note">${tn("usage.not_logged_in")}</p>${p.hint ? html`<p class="note">${usageText(p.hint, p.hintI18n)}</p>` : null}`
648
+ : html`${(p.metrics || []).map((m, j) => html`<${UsageMetric} key=${j} m=${m} />`)}
649
+ ${p.hint ? html`<p class="note">${usageText(p.hint, p.hintI18n)}</p>` : !(p.metrics || []).length ? html`<p class="note">${tn("usage.no_data")}</p>` : null}`}
650
+ </section>`)}
651
+ <div class="u-foot"><span class="note">${tn("usageUpdated")} ${new Date(data.fetchedAt).toLocaleTimeString()}</span>
652
+ <button type="button" class="btn btn-sm" onClick=${() => load(true)}>${tn("usageRefresh")}</button></div>`;
653
+ }
654
+ return html`<${Dialog} title=${tn("usage.title")} onClose=${onClose} wide=${true}>${content}</${Dialog}>`;
655
+ }