@songsid/agend 2.1.12 โ†’ 2.2.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (358) hide show
  1. package/README.md +2 -2
  2. package/README.zh-TW.md +2 -2
  3. package/dist/auth/one-time-code.d.ts +19 -0
  4. package/dist/auth/one-time-code.js +49 -0
  5. package/dist/auth/one-time-code.js.map +1 -0
  6. package/dist/auth-api.d.ts +52 -0
  7. package/dist/auth-api.js +346 -0
  8. package/dist/auth-api.js.map +1 -0
  9. package/dist/backend/claude-code.d.ts +4 -0
  10. package/dist/backend/claude-code.js +97 -0
  11. package/dist/backend/claude-code.js.map +1 -1
  12. package/dist/backend/codex.js +59 -30
  13. package/dist/backend/codex.js.map +1 -1
  14. package/dist/backend/kiro-agent.d.ts +66 -0
  15. package/dist/backend/kiro-agent.js +302 -0
  16. package/dist/backend/kiro-agent.js.map +1 -0
  17. package/dist/backend/kiro-identity.d.ts +80 -0
  18. package/dist/backend/kiro-identity.js +537 -0
  19. package/dist/backend/kiro-identity.js.map +1 -0
  20. package/dist/backend/kiro-v2-store-worker.d.ts +1 -0
  21. package/dist/backend/kiro-v2-store-worker.js +87 -0
  22. package/dist/backend/kiro-v2-store-worker.js.map +1 -0
  23. package/dist/backend/kiro-v2-store.d.ts +54 -0
  24. package/dist/backend/kiro-v2-store.js +131 -0
  25. package/dist/backend/kiro-v2-store.js.map +1 -0
  26. package/dist/backend/kiro-v3-identity.d.ts +5 -0
  27. package/dist/backend/kiro-v3-identity.js +22 -0
  28. package/dist/backend/kiro-v3-identity.js.map +1 -1
  29. package/dist/backend/kiro.d.ts +83 -7
  30. package/dist/backend/kiro.js +418 -91
  31. package/dist/backend/kiro.js.map +1 -1
  32. package/dist/backend/types.d.ts +45 -3
  33. package/dist/backend/types.js.map +1 -1
  34. package/dist/channel/adapters/discord.d.ts +1 -0
  35. package/dist/channel/adapters/discord.js +29 -10
  36. package/dist/channel/adapters/discord.js.map +1 -1
  37. package/dist/channel/adapters/telegram.d.ts +1 -0
  38. package/dist/channel/adapters/telegram.js +13 -1
  39. package/dist/channel/adapters/telegram.js.map +1 -1
  40. package/dist/channel/types.d.ts +9 -1
  41. package/dist/classic-channel-manager.d.ts +18 -10
  42. package/dist/classic-channel-manager.js +45 -18
  43. package/dist/classic-channel-manager.js.map +1 -1
  44. package/dist/cli.js +225 -127
  45. package/dist/cli.js.map +1 -1
  46. package/dist/command-table.d.ts +5 -3
  47. package/dist/command-table.js +29 -27
  48. package/dist/command-table.js.map +1 -1
  49. package/dist/config-validator.js +46 -0
  50. package/dist/config-validator.js.map +1 -1
  51. package/dist/config.js +12 -0
  52. package/dist/config.js.map +1 -1
  53. package/dist/connection-secrets.d.ts +6 -0
  54. package/dist/connection-secrets.js +4 -0
  55. package/dist/connection-secrets.js.map +1 -1
  56. package/dist/daemon.d.ts +89 -4
  57. package/dist/daemon.js +661 -77
  58. package/dist/daemon.js.map +1 -1
  59. package/dist/darwin-memory.d.ts +6 -1
  60. package/dist/darwin-memory.js +17 -6
  61. package/dist/darwin-memory.js.map +1 -1
  62. package/dist/delivery-consumed-watch.d.ts +85 -0
  63. package/dist/delivery-consumed-watch.js +244 -0
  64. package/dist/delivery-consumed-watch.js.map +1 -0
  65. package/dist/delivery-outbox.d.ts +61 -0
  66. package/dist/delivery-outbox.js +120 -2
  67. package/dist/delivery-outbox.js.map +1 -1
  68. package/dist/delivery-reconciliation.d.ts +14 -5
  69. package/dist/delivery-reconciliation.js +27 -4
  70. package/dist/delivery-reconciliation.js.map +1 -1
  71. package/dist/delivery-reconciliation.test.js +35 -1
  72. package/dist/delivery-reconciliation.test.js.map +1 -1
  73. package/dist/event-loop-watch.d.ts +28 -0
  74. package/dist/event-loop-watch.js +75 -3
  75. package/dist/event-loop-watch.js.map +1 -1
  76. package/dist/fleet-context.d.ts +22 -2
  77. package/dist/fleet-level-config.d.ts +2 -2
  78. package/dist/fleet-level-config.js +6 -1
  79. package/dist/fleet-level-config.js.map +1 -1
  80. package/dist/fleet-manager.d.ts +293 -12
  81. package/dist/fleet-manager.js +2364 -449
  82. package/dist/fleet-manager.js.map +1 -1
  83. package/dist/host-memory.d.ts +4 -0
  84. package/dist/host-memory.js.map +1 -1
  85. package/dist/instance-config-impact.js +5 -0
  86. package/dist/instance-config-impact.js.map +1 -1
  87. package/dist/instance-lifecycle.d.ts +10 -5
  88. package/dist/instance-lifecycle.js +162 -53
  89. package/dist/instance-lifecycle.js.map +1 -1
  90. package/dist/locale.js +150 -16
  91. package/dist/locale.js.map +1 -1
  92. package/dist/login-controller.d.ts +2 -0
  93. package/dist/login-controller.js +17 -5
  94. package/dist/login-controller.js.map +1 -1
  95. package/dist/memory-pressure.d.ts +7 -8
  96. package/dist/memory-pressure.js +76 -15
  97. package/dist/memory-pressure.js.map +1 -1
  98. package/dist/monotonic-budget.d.ts +2 -0
  99. package/dist/monotonic-budget.js +28 -0
  100. package/dist/monotonic-budget.js.map +1 -0
  101. package/dist/needs-you-hub.d.ts +114 -0
  102. package/dist/needs-you-hub.js +370 -0
  103. package/dist/needs-you-hub.js.map +1 -0
  104. package/dist/needs-you-live.d.ts +116 -0
  105. package/dist/needs-you-live.js +228 -0
  106. package/dist/needs-you-live.js.map +1 -0
  107. package/dist/needs-you.d.ts +132 -0
  108. package/dist/needs-you.js +164 -0
  109. package/dist/needs-you.js.map +1 -0
  110. package/dist/node-version-guard.d.ts +30 -0
  111. package/dist/node-version-guard.js +50 -0
  112. package/dist/node-version-guard.js.map +1 -0
  113. package/dist/operator-control-paths.d.ts +4 -0
  114. package/dist/operator-control-paths.js +24 -0
  115. package/dist/operator-control-paths.js.map +1 -0
  116. package/dist/outbound-handlers.d.ts +2 -0
  117. package/dist/outbound-handlers.js +9 -2
  118. package/dist/outbound-handlers.js.map +1 -1
  119. package/dist/outbound-schemas.d.ts +5 -5
  120. package/dist/profile-control.js +4 -20
  121. package/dist/profile-control.js.map +1 -1
  122. package/dist/public-web-gateway.d.ts +18 -0
  123. package/dist/public-web-gateway.js +141 -0
  124. package/dist/public-web-gateway.js.map +1 -0
  125. package/dist/public-web-link.d.ts +53 -0
  126. package/dist/public-web-link.js +175 -0
  127. package/dist/public-web-link.js.map +1 -0
  128. package/dist/quickstart-api.d.ts +8 -1
  129. package/dist/quickstart-api.js +86 -48
  130. package/dist/quickstart-api.js.map +1 -1
  131. package/dist/quickstart.d.ts +1 -0
  132. package/dist/quickstart.js +6 -2
  133. package/dist/quickstart.js.map +1 -1
  134. package/dist/scheduler/db.d.ts +7 -1
  135. package/dist/scheduler/db.js +38 -0
  136. package/dist/scheduler/db.js.map +1 -1
  137. package/dist/scheduler/index.d.ts +1 -1
  138. package/dist/scheduler/retry-label.d.ts +11 -0
  139. package/dist/scheduler/retry-label.js +24 -0
  140. package/dist/scheduler/retry-label.js.map +1 -0
  141. package/dist/scheduler/scheduler.d.ts +55 -2
  142. package/dist/scheduler/scheduler.js +220 -8
  143. package/dist/scheduler/scheduler.js.map +1 -1
  144. package/dist/scheduler/types.d.ts +22 -0
  145. package/dist/scheduler/types.js.map +1 -1
  146. package/dist/secret-store.d.ts +5 -0
  147. package/dist/secret-store.js +20 -0
  148. package/dist/secret-store.js.map +1 -1
  149. package/dist/service-activation.d.ts +110 -0
  150. package/dist/service-activation.js +302 -0
  151. package/dist/service-activation.js.map +1 -0
  152. package/dist/service-installer.d.ts +7 -0
  153. package/dist/service-installer.js +7 -4
  154. package/dist/service-installer.js.map +1 -1
  155. package/dist/service-restart-selection.d.ts +6 -5
  156. package/dist/service-restart-selection.js +6 -5
  157. package/dist/service-restart-selection.js.map +1 -1
  158. package/dist/settings-api.d.ts +10 -2
  159. package/dist/settings-api.js +175 -72
  160. package/dist/settings-api.js.map +1 -1
  161. package/dist/settings-baseline.d.ts +20 -0
  162. package/dist/settings-baseline.js +104 -0
  163. package/dist/settings-baseline.js.map +1 -0
  164. package/dist/settings-change.d.ts +21 -0
  165. package/dist/settings-change.js +133 -0
  166. package/dist/settings-change.js.map +1 -0
  167. package/dist/settings-confirmation.d.ts +105 -0
  168. package/dist/settings-confirmation.js +232 -0
  169. package/dist/settings-confirmation.js.map +1 -0
  170. package/dist/settings-control.d.ts +41 -0
  171. package/dist/settings-control.js +294 -0
  172. package/dist/settings-control.js.map +1 -0
  173. package/dist/settings-effect.d.ts +24 -0
  174. package/dist/settings-effect.js +169 -0
  175. package/dist/settings-effect.js.map +1 -0
  176. package/dist/settings-http-confirmation.d.ts +29 -0
  177. package/dist/settings-http-confirmation.js +123 -0
  178. package/dist/settings-http-confirmation.js.map +1 -0
  179. package/dist/settings-instance-patch.d.ts +4 -0
  180. package/dist/settings-instance-patch.js +26 -0
  181. package/dist/settings-instance-patch.js.map +1 -0
  182. package/dist/settings-request-capability.d.ts +16 -0
  183. package/dist/settings-request-capability.js +32 -0
  184. package/dist/settings-request-capability.js.map +1 -0
  185. package/dist/settings-transaction.d.ts +65 -0
  186. package/dist/settings-transaction.js +223 -0
  187. package/dist/settings-transaction.js.map +1 -0
  188. package/dist/setup-auth.d.ts +10 -18
  189. package/dist/setup-auth.js +14 -38
  190. package/dist/setup-auth.js.map +1 -1
  191. package/dist/setup-form.d.ts +1 -1
  192. package/dist/setup-form.js +75 -18
  193. package/dist/setup-form.js.map +1 -1
  194. package/dist/setup-guide.d.ts +13 -0
  195. package/dist/setup-guide.js +25 -0
  196. package/dist/setup-guide.js.map +1 -0
  197. package/dist/setup-host.d.ts +16 -0
  198. package/dist/setup-host.js +197 -32
  199. package/dist/setup-host.js.map +1 -1
  200. package/dist/setup-wizard.js +3 -0
  201. package/dist/setup-wizard.js.map +1 -1
  202. package/dist/slash-authz.d.ts +17 -3
  203. package/dist/slash-authz.js +5 -0
  204. package/dist/slash-authz.js.map +1 -1
  205. package/dist/spawn-gate.js +3 -3
  206. package/dist/spawn-gate.js.map +1 -1
  207. package/dist/statusline-watcher.d.ts +5 -0
  208. package/dist/statusline-watcher.js +2 -0
  209. package/dist/statusline-watcher.js.map +1 -1
  210. package/dist/steer-capability.d.ts +6 -0
  211. package/dist/steer-capability.js +8 -0
  212. package/dist/steer-capability.js.map +1 -1
  213. package/dist/sync-work-attribution.d.ts +14 -0
  214. package/dist/sync-work-attribution.js +43 -8
  215. package/dist/sync-work-attribution.js.map +1 -1
  216. package/dist/tips.js +10 -5
  217. package/dist/tips.js.map +1 -1
  218. package/dist/tmux-control.d.ts +35 -5
  219. package/dist/tmux-control.js +291 -57
  220. package/dist/tmux-control.js.map +1 -1
  221. package/dist/tmux-manager.d.ts +20 -6
  222. package/dist/tmux-manager.js +90 -47
  223. package/dist/tmux-manager.js.map +1 -1
  224. package/dist/tmux-read.d.ts +59 -0
  225. package/dist/tmux-read.js +213 -0
  226. package/dist/tmux-read.js.map +1 -0
  227. package/dist/topic-commands.d.ts +21 -10
  228. package/dist/topic-commands.js +191 -58
  229. package/dist/topic-commands.js.map +1 -1
  230. package/dist/transcript-delta-reader.js +2 -1
  231. package/dist/transcript-delta-reader.js.map +1 -1
  232. package/dist/tunnel/cloudflared-install.d.ts +2 -0
  233. package/dist/tunnel/cloudflared-install.js +1 -1
  234. package/dist/tunnel/cloudflared-install.js.map +1 -1
  235. package/dist/tunnel/purpose-lane.d.ts +14 -0
  236. package/dist/tunnel/purpose-lane.js +43 -0
  237. package/dist/tunnel/purpose-lane.js.map +1 -0
  238. package/dist/turn-reply-guard.d.ts +2 -0
  239. package/dist/turn-reply-guard.js +10 -1
  240. package/dist/turn-reply-guard.js.map +1 -1
  241. package/dist/types.d.ts +33 -0
  242. package/dist/ui/app.html +18 -0
  243. package/dist/ui/chat-render.js +478 -0
  244. package/dist/ui/chat-store.js +289 -0
  245. package/dist/ui/chat-strings.js +69 -0
  246. package/dist/ui/chat-thread.js +209 -0
  247. package/dist/ui/chat-tour.js +116 -0
  248. package/dist/ui/fleet-org.js +142 -0
  249. package/dist/ui/panel-chat.js +418 -0
  250. package/dist/ui/panel-fleet.js +413 -0
  251. package/dist/ui/panel-needs.js +202 -0
  252. package/dist/ui/panel-settings.js +750 -0
  253. package/dist/ui/preview.js +266 -0
  254. package/dist/ui/settings-apply.js +239 -0
  255. package/dist/ui/settings-confirm.js +186 -0
  256. package/dist/ui/settings-dialogs.js +546 -0
  257. package/dist/ui/settings-model.js +277 -0
  258. package/dist/ui/settings-strings.js +652 -0
  259. package/dist/ui/settings-wizard.js +140 -0
  260. package/dist/ui/shared/agend-auth.js +93 -0
  261. package/dist/ui/shared/app-ctx.js +80 -0
  262. package/dist/ui/shared/app-html.js +9 -0
  263. package/dist/ui/shared/app-i18n.js +95 -0
  264. package/dist/ui/shared/app-nav.js +73 -0
  265. package/dist/ui/shared/app-needs.js +114 -0
  266. package/dist/ui/shared/app-route.js +73 -0
  267. package/dist/ui/shared/app-session.js +66 -0
  268. package/dist/ui/shared/app-shell.js +344 -0
  269. package/dist/ui/shared/app-store.js +51 -0
  270. package/dist/ui/shared/app-stream.js +201 -0
  271. package/dist/ui/shared/app.css +676 -0
  272. package/dist/ui/shared/app.js +88 -0
  273. package/dist/ui/shared/fonts/OFL.txt +92 -0
  274. package/dist/ui/shared/fonts/README.md +21 -0
  275. package/dist/ui/shared/fonts/inter.woff2 +0 -0
  276. package/dist/ui/shared/htm.module.js +1 -0
  277. package/dist/ui/shared/panel-view.js +655 -0
  278. package/dist/ui/shared/preact-hooks.module.js +2 -0
  279. package/dist/ui/shared/preact.module.js +2 -0
  280. package/dist/ui/shared/signin.js +130 -0
  281. package/dist/ui/shared/theme.js +29 -0
  282. package/dist/ui/shared/tokens.css +153 -0
  283. package/dist/ui/shared/ui-confirm.js +43 -0
  284. package/dist/ui/shared/ui-dialog.js +45 -0
  285. package/dist/ui/shared/ui-icons.js +61 -0
  286. package/dist/ui/shared/ui-menu.js +39 -0
  287. package/dist/ui/shared/ui-states.js +20 -0
  288. package/dist/ui/shared/ui-toast.js +18 -0
  289. package/dist/ui/shared/vendor/htm.LICENSE +202 -0
  290. package/dist/ui/shared/vendor/preact.LICENSE +21 -0
  291. package/dist/ui/shared/vendor/vendor.json +39 -0
  292. package/dist/ui/shared/view-strings.js +309 -0
  293. package/dist/ui/signin.html +51 -0
  294. package/dist/update-check.d.ts +6 -2
  295. package/dist/update-check.js +12 -3
  296. package/dist/update-check.js.map +1 -1
  297. package/dist/update-install.d.ts +71 -0
  298. package/dist/update-install.js +162 -0
  299. package/dist/update-install.js.map +1 -0
  300. package/dist/upgrade-notices.d.ts +17 -0
  301. package/dist/upgrade-notices.js +93 -0
  302. package/dist/upgrade-notices.js.map +1 -0
  303. package/dist/usage/statusline-usage.d.ts +2 -0
  304. package/dist/usage/statusline-usage.js +14 -10
  305. package/dist/usage/statusline-usage.js.map +1 -1
  306. package/dist/view-api.d.ts +6 -1
  307. package/dist/view-api.js +62 -51
  308. package/dist/view-api.js.map +1 -1
  309. package/dist/web-api.d.ts +71 -14
  310. package/dist/web-api.js +492 -77
  311. package/dist/web-api.js.map +1 -1
  312. package/dist/web-auth.d.ts +71 -27
  313. package/dist/web-auth.js +169 -69
  314. package/dist/web-auth.js.map +1 -1
  315. package/dist/web-body.d.ts +3 -0
  316. package/dist/web-body.js +55 -0
  317. package/dist/web-body.js.map +1 -0
  318. package/dist/web-channel-echo.d.ts +9 -0
  319. package/dist/web-channel-echo.js +55 -0
  320. package/dist/web-channel-echo.js.map +1 -0
  321. package/dist/web-chat-history.d.ts +125 -0
  322. package/dist/web-chat-history.js +184 -0
  323. package/dist/web-chat-history.js.map +1 -0
  324. package/dist/web-continuation.d.ts +7 -0
  325. package/dist/web-continuation.js +22 -0
  326. package/dist/web-continuation.js.map +1 -0
  327. package/dist/web-host-guard.d.ts +34 -0
  328. package/dist/web-host-guard.js +57 -1
  329. package/dist/web-host-guard.js.map +1 -1
  330. package/dist/web-login.d.ts +72 -0
  331. package/dist/web-login.js +132 -0
  332. package/dist/web-login.js.map +1 -0
  333. package/dist/web-org.d.ts +55 -0
  334. package/dist/web-org.js +67 -0
  335. package/dist/web-org.js.map +1 -0
  336. package/dist/web-preview.d.ts +77 -0
  337. package/dist/web-preview.js +240 -0
  338. package/dist/web-preview.js.map +1 -0
  339. package/dist/web-request-context.d.ts +10 -0
  340. package/dist/web-request-context.js +13 -0
  341. package/dist/web-request-context.js.map +1 -0
  342. package/dist/web-session.d.ts +167 -0
  343. package/dist/web-session.js +389 -0
  344. package/dist/web-session.js.map +1 -0
  345. package/dist/web-shell-routes.d.ts +59 -0
  346. package/dist/web-shell-routes.js +96 -0
  347. package/dist/web-shell-routes.js.map +1 -0
  348. package/dist/web-upload.d.ts +183 -0
  349. package/dist/web-upload.js +468 -0
  350. package/dist/web-upload.js.map +1 -0
  351. package/dist/website-tips-renderer.d.ts +2 -2
  352. package/dist/website-tips-renderer.js +3 -3
  353. package/dist/website-tips-renderer.js.map +1 -1
  354. package/package.json +8 -5
  355. package/scripts/preinstall-guard.cjs +36 -0
  356. package/dist/ui/dashboard.html +0 -774
  357. package/dist/ui/settings.html +0 -2165
  358. package/dist/ui/view.html +0 -1139
package/dist/ui/view.html DELETED
@@ -1,1139 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>AgEnD View</title>
7
- <style>
8
- * { margin: 0; padding: 0; box-sizing: border-box; }
9
- :root {
10
- --bg:#0d1117; --panel:#151b23; --panel-2:#1c2330; --border:#262c36; --fg:#d1d7e0; --dim:#8b949e;
11
- --accent:#58a6ff; --accent-soft:rgba(88,166,255,.14); --ok:#3fb950; --warn:#d29922; --crit:#f85149;
12
- --radius:8px;
13
- }
14
- html, body { width: 100%; height: 100%; }
15
- body { background: var(--bg); color: var(--fg); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; display: flex; width: 100vw; min-width: 0; height: 100vh; overflow: hidden; }
16
-
17
- /* Sidebar */
18
- #sidebar { width: 240px; flex: 0 0 240px; background: var(--panel); border-right: 1px solid var(--border); display: flex; flex-direction: column; overflow: hidden; }
19
- /* The list scrolls; the filter stays pinned to the bottom of the sidebar. */
20
- #list { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
21
- /* The UA's default scrollbar is a light grey bar with arrows on this dark panel.
22
- Tinted from the sidebar's own --dim; scoped to #list so the terminal's own
23
- scrollbar (#term) is left alone. WebKit/Blink get the pseudo-elements (thin,
24
- rounded, no arrows, a hover state); Chromium 121+ would ignore them if
25
- scrollbar-color were also set, so the standard properties are Firefox-only. */
26
- #list::-webkit-scrollbar { width: 10px; }
27
- #list::-webkit-scrollbar-track { background: transparent; }
28
- #list::-webkit-scrollbar-button { display: none; }
29
- #list::-webkit-scrollbar-thumb { background: color-mix(in srgb, var(--dim) 35%, transparent); background-clip: padding-box; border: 2px solid transparent; border-radius: 5px; }
30
- #list::-webkit-scrollbar-thumb:hover { background: color-mix(in srgb, var(--dim) 70%, transparent); background-clip: padding-box; }
31
- @supports (-moz-appearance: none) {
32
- #list { scrollbar-width: thin; scrollbar-color: color-mix(in srgb, var(--dim) 35%, transparent) transparent; }
33
- #list:hover { scrollbar-color: color-mix(in srgb, var(--dim) 70%, transparent) transparent; }
34
- }
35
- #filterBar { flex: 0 0 auto; border-top: 1px solid var(--border); padding: 8px 10px 9px; background: var(--panel); }
36
- .filter-wrap { position: relative; display: flex; align-items: center; }
37
- .filter-wrap .filter-icon { position: absolute; left: 8px; width: 13px; height: 13px; color: var(--dim); pointer-events: none; }
38
- #filterInput { width: 100%; background: var(--bg); color: var(--fg); border: 1px solid var(--border); border-radius: 7px; padding: 6px 26px 6px 27px; font-size: 12px; font-family: inherit; transition: border-color .15s, box-shadow .15s; }
39
- #filterInput::placeholder { color: #6e7681; }
40
- #filterInput:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px rgba(88,166,255,.16); }
41
- #filterClear { position: absolute; right: 4px; width: 20px; height: 20px; border: none; border-radius: 50%; background: none; color: var(--dim); cursor: pointer; font-size: 15px; line-height: 1; display: none; align-items: center; justify-content: center; padding: 0; }
42
- #filterClear.show { display: inline-flex; }
43
- #filterClear:hover { color: var(--fg); background: rgba(255,255,255,.08); }
44
- #filterCount { margin-top: 5px; font-size: 11px; color: var(--dim); font-variant-numeric: tabular-nums; display: flex; justify-content: space-between; }
45
- #filterCount .filtering { color: var(--accent); }
46
- .filter-empty { padding: 18px 14px; color: var(--dim); font-size: 12px; text-align: center; }
47
- #sidebar h1 { display: flex; align-items: center; gap: 8px; font-size: 14px; color: var(--fg); padding: 14px 16px; border-bottom: 1px solid var(--border); font-weight: 600; letter-spacing: .3px; }
48
- #sidebar h1::before { content: ""; width: 10px; height: 10px; flex: 0 0 10px; border-radius: 3px; background: linear-gradient(135deg, var(--accent), #a371f7); box-shadow: 0 0 10px rgba(88,166,255,.45); }
49
- .group-header { display: flex; align-items: center; gap: 6px; padding: 10px 12px 6px; cursor: pointer; font-size: 11px; font-weight: 600; color: var(--dim); text-transform: uppercase; letter-spacing: .6px; user-select: none; }
50
- .group-header:hover { color: var(--fg); }
51
- .group-header .caret { font-size: 9px; opacity: .8; }
52
- .group-header .caret { width: 10px; flex: 0 0 10px; }
53
- .group-header .gcount { margin-left: auto; font-weight: 500; font-size: 10px; letter-spacing: 0; padding: 1px 6px; border-radius: 9px; background: var(--panel-2); color: var(--dim); }
54
- .inst { display: flex; align-items: center; gap: 8px; margin: 1px 6px; padding: 8px 10px 8px 14px; cursor: pointer; border-radius: 6px; border-left: 3px solid transparent; font-size: 13px; transition: background .12s; }
55
- .dragging { opacity: .4; }
56
- .drop-before { border-top: 2px solid var(--accent) !important; }
57
- .drop-after { border-bottom: 2px solid var(--accent) !important; }
58
- [draggable="true"] { cursor: grab; }
59
- .inst:hover { background: rgba(255,255,255,.045); }
60
- .inst.active { background: var(--accent-soft); border-left-color: var(--accent); }
61
- .inst.active .nm { color: #f0f6fc; font-weight: 600; }
62
- .dot { width: 8px; height: 8px; border-radius: 50%; flex: 0 0 8px; }
63
- .dot.running { background: var(--ok); box-shadow: 0 0 0 3px rgba(63,185,80,.15); } .dot.stopped { background: #6e7681; } .dot.crashed { background: var(--crit); box-shadow: 0 0 0 3px rgba(248,81,73,.18); }
64
- .inst .identity { flex: 1; min-width: 0; }
65
- .inst .nm { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
66
- .inst .alias { display: block; margin-top: 2px; color: var(--dim); font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
67
- .inst .pct { color: var(--dim); font-size: 11px; font-variant-numeric: tabular-nums; }
68
- .cli-icon { width: 10px; height: 10px; flex: 0 0 10px; border-radius: 50%; border: 1px solid rgba(255,255,255,.24); box-shadow: 0 0 0 1px rgba(0,0,0,.18); }
69
- .cli-claude-code { background: #c678dd; }
70
- .cli-kiro-cli { background: #58a6ff; }
71
- .cli-codex { background: #3fb950; }
72
- .cli-grok { background: #0d1117; border-color: #f0f6fc; }
73
- .cli-antigravity { background: #f85149; }
74
- .cli-opencode { background: #f0f6fc; }
75
- .cli-other { background: #6e7681; }
76
-
77
- /* Main */
78
- #main { flex: 1; display: flex; flex-direction: column; min-width: 0; }
79
- /* Padding kept at 5/6px: enough that glyphs don't touch the edge, small
80
- enough that the terminal keeps essentially the whole main area. */
81
- #term { flex: 1; background: #000; overflow: auto; scrollbar-gutter: stable; padding: 5px 6px; }
82
- #term pre { min-width: 100%; min-height: 100%; font-family: "SFMono-Regular", Menlo, Consolas, "Liberation Mono", monospace; line-height: 1.2; white-space: pre; color: #d0d0d0; }
83
-
84
- /* Card โ€” a single compact bar by default so the terminal keeps the height.
85
- `.expanded` restores the tall layout with avatar + description. */
86
- #card { background: var(--panel); border-top: 1px solid var(--border); padding: 5px 10px; display: flex; gap: 10px; align-items: center; }
87
- #avatarBox { width: 24px; height: 24px; flex: 0 0 24px; }
88
- .avatar-img { width: 100%; height: 100%; border-radius: 50%; object-fit: cover; display: block; }
89
- .avatar-ph { width: 100%; height: 100%; border-radius: 50%; background: #30363d; color: var(--dim); display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 600; text-transform: uppercase; user-select: none; }
90
- #cardBody { flex: 1; min-width: 0; display: flex; align-items: baseline; gap: 10px; overflow: hidden; }
91
- #cardBody .row1 { display: flex; align-items: baseline; gap: 10px; flex: 0 0 auto; }
92
- #cardBody .dname { font-size: 13px; font-weight: 600; white-space: nowrap; }
93
- #cardBody .role { color: var(--accent); font-size: 12px; white-space: nowrap; }
94
- #cardBody .meta { color: var(--dim); font-size: 11px; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
95
- #cardBody .desc { display: none; }
96
- #cardToggle { background: none; border: none; color: var(--dim); cursor: pointer; font-size: 12px; padding: 2px 4px; flex: 0 0 auto; }
97
- #cardToggle:hover { color: var(--fg); }
98
-
99
- #card.expanded { align-items: flex-start; padding: 12px 14px; }
100
- #card.expanded #avatarBox { width: 48px; height: 48px; flex: 0 0 48px; }
101
- #card.expanded .avatar-ph { font-size: 20px; }
102
- #card.expanded #cardBody { display: block; }
103
- #card.expanded #cardBody .dname { font-size: 16px; }
104
- #card.expanded #cardBody .role { font-size: 13px; }
105
- #card.expanded #cardBody .meta { font-size: 12px; margin-top: 3px; white-space: normal; overflow: visible; }
106
- #card.expanded #cardBody .desc { display: block; font-size: 13px; margin-top: 6px; color: #adbac7; white-space: pre-wrap; }
107
- #editBtn { background: var(--panel-2); color: var(--fg); border: 1px solid var(--border); border-radius: 6px; padding: 3px 10px; cursor: pointer; font-size: 12px; flex: 0 0 auto; transition: border-color .15s, background .15s; }
108
- #editBtn:hover { border-color: var(--accent); background: var(--accent-soft); }
109
-
110
- /* Edit form */
111
- #edit { display: none; flex-direction: column; gap: 8px; flex: 1; }
112
- #edit input, #edit textarea { background: var(--bg); color: var(--fg); border: 1px solid var(--border); border-radius: 6px; padding: 6px 8px; font-size: 13px; font-family: inherit; width: 100%; }
113
- #edit textarea { resize: vertical; min-height: 48px; }
114
- #edit .actions { display: flex; gap: 8px; align-items: center; }
115
- #edit button { border: none; border-radius: 6px; padding: 6px 14px; cursor: pointer; font-size: 13px; }
116
- #saveBtn { background: #238636; color: #fff; } #cancelBtn { background: #21262d; color: var(--fg); border: 1px solid var(--border); }
117
- #edit .hint { color: var(--dim); font-size: 11px; }
118
- #msg { font-size: 12px; }
119
- .err { color: #f85149; } .ok { color: #3fb950; }
120
- .help-btn { position: fixed; top: 10px; right: 14px; width: 34px; height: 34px; border-radius: 10px; border: 1px solid var(--border); background: rgba(21,27,35,.88); color: var(--dim); cursor: pointer; font-size: 16px; z-index: 60; display: inline-flex; align-items: center; justify-content: center; padding: 0; backdrop-filter: blur(6px); transition: color .15s, border-color .15s, background .15s; }
121
- .help-btn svg { width: 17px; height: 17px; }
122
- .help-btn:hover { color: #f0f6fc; background: var(--accent-soft); border-color: var(--accent); }
123
- .help-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,.5); display: none; z-index: 70; }
124
- .help-backdrop.open { display: block; }
125
- .help-panel { position: fixed; top: 0; right: 0; height: 100%; width: 360px; max-width: 90vw; background: var(--panel); border-left: 1px solid var(--border); box-shadow: -8px 0 24px rgba(0,0,0,.4); transform: translateX(100%); transition: transform .2s ease; z-index: 80; overflow-y: auto; padding: 20px 22px; }
126
- .help-panel.open { transform: translateX(0); }
127
- .help-panel h3 { font-size: 17px; margin-bottom: 10px; display: flex; align-items: center; gap: 8px; }
128
- .help-panel h3 svg { width: 18px; height: 18px; color: var(--accent); }
129
- .help-panel ul { margin: 8px 0 0 18px; }
130
- .help-panel li { font-size: 13px; margin: 6px 0; color: var(--fg); }
131
- .help-panel .close { position: absolute; top: 14px; right: 16px; cursor: pointer; color: var(--dim); font-size: 22px; line-height: 1; background: none; border: none; }
132
-
133
- /* AI usage panel (shares .help-panel chrome) */
134
- .u-provider { margin: 14px 0 4px; padding: 14px 14px 10px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--panel-2); }
135
- .u-provider:first-of-type { margin-top: 6px; }
136
- .u-head { display: flex; align-items: center; gap: 8px; }
137
- .u-head .u-name { font-size: 15px; font-weight: 700; }
138
- .u-head .u-plan { color: var(--dim); font-size: 11px; padding: 1px 7px; border: 1px solid var(--border); border-radius: 9px; }
139
- .u-order { display: flex; gap: 3px; margin-left: auto; }
140
- .u-move { width: 24px; height: 22px; border: 1px solid var(--border); border-radius: 5px; background: none; color: var(--dim); cursor: pointer; line-height: 1; }
141
- .u-move:hover:not(:disabled) { color: var(--fg); border-color: var(--accent); }
142
- .u-move:disabled { opacity: .25; cursor: default; }
143
- /* Readability: the percentage is the number people come here for, so it is
144
- large and bold, and the bar is thick enough to read at a glance. */
145
- .u-metric { margin: 14px 0 6px; }
146
- .u-row { display: flex; align-items: baseline; gap: 8px; font-size: 13px; }
147
- .u-row .u-label { color: #adbac7; flex: 1; min-width: 0; }
148
- .u-row .u-val { font-variant-numeric: tabular-nums; font-size: 15px; font-weight: 600; color: #f0f6fc; }
149
- .u-row .u-val.u-pct { font-size: 24px; font-weight: 700; line-height: 1; letter-spacing: -.5px; }
150
- .u-row .u-val.u-pct .u-unit { font-size: 14px; font-weight: 600; margin-left: 1px; opacity: .75; }
151
- .u-row .u-val.warn { color: var(--warn); } .u-row .u-val.crit { color: var(--crit); }
152
- .u-row .u-status { font-size: 11px; font-weight: 700; padding: 2px 7px; border-radius: 9px; align-self: center; }
153
- .u-status.warn { color: var(--warn); background: rgba(210,153,34,.14); } .u-status.crit { color: var(--crit); background: rgba(248,81,73,.14); }
154
- .u-meter { height: 10px; background: rgba(255,255,255,.07); border-radius: 5px; overflow: hidden; margin-top: 7px; }
155
- .u-fill { height: 100%; border-radius: 5px; background: var(--accent); min-width: 3px; transition: width .3s ease; }
156
- .u-fill.warn { background: var(--warn); } .u-fill.crit { background: var(--crit); }
157
- .u-sub { color: var(--dim); font-size: 12px; margin-top: 5px; font-variant-numeric: tabular-nums; }
158
- .u-err { color: var(--warn); font-size: 13px; margin: 8px 0; }
159
- /* Neutral note (subscription plan, or "sign-in needed") โ€” deliberately not the
160
- amber error colour, since neither state is a failure. */
161
- .u-note { color: var(--dim); font-size: 13px; margin: 8px 0; }
162
- .u-foot { color: var(--dim); font-size: 11px; margin-top: 14px; display: flex; gap: 8px; align-items: baseline; }
163
- .u-foot button { background: none; border: 1px solid var(--border); border-radius: 5px; color: var(--dim); cursor: pointer; font-size: 11px; padding: 2px 8px; }
164
- .u-foot button:hover { color: var(--fg); border-color: var(--accent); }
165
-
166
- #densityBtn { font-size: 14px; font-weight: 600; letter-spacing: -.5px; }
167
- @media (max-width: 700px) {
168
- #sidebar { width: 180px; flex-basis: 180px; }
169
- /* The compact bar must stay one line; only the expanded form wraps. */
170
- #card.expanded { flex-wrap: wrap; padding: 10px 12px; }
171
- #edit { flex-basis: 100%; }
172
- }
173
- </style>
174
- </head>
175
- <body>
176
- <button class="help-btn" id="usageBtn" data-i18n-title="usageButton" title="AI usage" style="right:140px; display:none"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" aria-hidden="true"><path d="M2.5 13.5h11"/><path d="M4.5 11V8"/><path d="M8 11V4.5"/><path d="M11.5 11V6.5"/></svg></button>
177
- <button class="help-btn" id="densityBtn" data-i18n-title="densityLabel" title="Font density" style="right:98px">โคข</button>
178
- <button class="help-btn" id="langBtn" data-i18n-title="languageButton" title="Language" style="right:56px"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><circle cx="8" cy="8" r="5.8"/><path d="M2.3 8h11.4M8 2.2c1.7 1.6 2.6 3.6 2.6 5.8S9.7 12.2 8 13.8M8 2.2C6.3 3.8 5.4 5.8 5.4 8s.9 4.2 2.6 5.8"/></svg></button>
179
- <button class="help-btn" id="helpBtn" data-i18n-title="helpButton" title="Help"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" aria-hidden="true"><circle cx="8" cy="8" r="5.8"/><path d="M8 7.3v3.6"/><circle cx="8" cy="5.1" r=".6" fill="currentColor" stroke="none"/></svg></button>
180
- <div class="help-backdrop" id="helpBackdrop"></div>
181
- <aside class="help-panel" id="helpPanel">
182
- <button class="close" id="helpClose" data-i18n-title="close" title="Close">ร—</button>
183
- <div id="helpContent"></div>
184
- </aside>
185
- <aside class="help-panel" id="usagePanel">
186
- <button class="close" id="usageClose" data-i18n-title="close" title="Close">ร—</button>
187
- <div id="usageContent"></div>
188
- </aside>
189
- <div id="sidebar"><h1 data-i18n="viewTitle">AgEnD ยท View</h1><div id="list"></div>
190
- <div id="filterBar">
191
- <div class="filter-wrap">
192
- <svg class="filter-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><circle cx="7" cy="7" r="4.5"/><path d="m10.5 10.5 3.5 3.5" stroke-linecap="round"/></svg>
193
- <input id="filterInput" type="text" autocomplete="off" spellcheck="false" data-i18n-ph="filterPh" placeholder="Filter: name, model, backend" aria-label="Filter instances">
194
- <button id="filterClear" type="button" data-i18n-title="filterClear" title="Clear filter" aria-label="Clear filter">ร—</button>
195
- </div>
196
- <div id="filterCount" aria-live="polite"></div>
197
- </div>
198
- </div>
199
- <div id="main">
200
- <div id="term"><pre id="pre"></pre></div>
201
- <div id="card">
202
- <div id="avatarBox"></div>
203
- <div id="cardBody">
204
- <div class="row1"><span class="dname" id="dname">โ€”</span><span class="role" id="role"></span></div>
205
- <div class="meta" id="meta"></div>
206
- <div class="desc" id="desc"></div>
207
- </div>
208
- <button id="cardToggle" data-i18n-title="expandProfile" title="Expand profile" aria-expanded="false">โ–ฒ</button>
209
- <button id="editBtn" data-i18n="editProfile">โœ๏ธ Edit</button>
210
- <div id="edit">
211
- <input id="fDisplay" data-i18n-ph="displayName" placeholder="Display name">
212
- <input id="fRole" data-i18n-ph="role" placeholder="Role">
213
- <textarea id="fDesc" data-i18n-ph="description" placeholder="Description"></textarea>
214
- <input id="fAvatar" type="file" accept="image/png,image/jpeg,image/gif,image/webp">
215
- <input id="fToken" type="password" data-i18n-ph="tokenPh" placeholder="web.token (required to save)">
216
- <div class="actions">
217
- <button id="saveBtn" data-i18n="save">Save</button>
218
- <button id="cancelBtn" data-i18n="cancel">Cancel</button>
219
- <span id="msg"></span>
220
- </div>
221
- <div class="hint" data-i18n="saveHint">Viewing is open (read-only). Saving requires the web.token (~/.agend/web.token).</div>
222
- </div>
223
- </div>
224
- </div>
225
-
226
- <script>
227
- (() => {
228
- // GET routes are open (no token). Profile/avatar writes require web.token;
229
- // visual ordering is browser-local and never leaves localStorage.
230
- const urlToken = new URLSearchParams(location.search).get("token") || "";
231
- const q = (id) => document.getElementById(id);
232
-
233
- // โ”€โ”€ i18n โ”€โ”€
234
- const I18N = {
235
- en: { viewTitle: "AgEnD ยท View", editProfile: "โœ๏ธ Edit", save: "Save", cancel: "Cancel",
236
- filterPh: "Filter: name, model, backend", filterClear: "Clear filter", filterNone: "No instance matches", filterShown: "shown",
237
- displayName: "Display name", role: "Role", description: "Description", tokenPh: "web.token (required to save)",
238
- saveHint: "Viewing is open (read-only). Saving requires the web.token (~/.agend/web.token).", helpTitle: "๐Ÿ“– Help",
239
- help1: "Pick an instance on the left โ†’ its live terminal shows on the right",
240
- help2: "Click a group header โ–ผ/โ–ถ to collapse/expand", help3: "โœ๏ธ Edit changes display name, role, description",
241
- help4: "Drag groups or instances to reorder them in this browser",
242
- help5: "Click โคข to cycle font density: Fit โ†’ Comfortable โ†’ Compact",
243
- densityFit: "Fit", densityComfortable: "Comfortable", densityCompact: "Compact", densityLabel: "Font density",
244
- usageTitle: "๐Ÿ“Š AI Usage", usageEmpty: "No AI CLI logins found on this machine.",
245
- usageUpdated: "updated", usageRefresh: "Refresh", usageHigh: "high", usageNear: "near limit",
246
- usageMoveUp: "Move up", usageMoveDown: "Move down",
247
- tooltipBackend: "Backend: {value}", tooltipStatus: "Status: {value}", tooltipContext: "Context: {value}",
248
- tooltipModel: "Model: {value}", tooltipEffort: "Effort: {value}",
249
- tooltipUnavailable: "Unavailable", statusRunning: "Running", statusPaused: "Paused",
250
- statusStopped: "Stopped", statusCrashed: "Crashed", statusUnknown: "Unknown",
251
- usageResets: "resets in {t}", usageSoon: "resets soon", usageFail: "Could not load usage data.",
252
- "usage.title": "AI Subscription Usage", "usage.title_plain": "AI subscription usage", "usage.empty_active": "No active backends with usage tracking",
253
- "usage.not_logged_in": "not logged in", "usage.error_fallback": "error", "usage.no_data": "no data",
254
- "usage.reset.in": "resets in {0}", "usage.reset.soon": "resets soon", "usage.ticket_expiry": "๐ŸŽซ Nearest expiry: {0} (in {1})",
255
- "usage.duration.days_hours": "{0}d {1}h", "usage.duration.hours_minutes": "{0}h {1}m", "usage.duration.minutes": "{0}m",
256
- "usage.metric.session": "Session", "usage.metric.weekly": "Weekly", "usage.metric.monthly": "Monthly",
257
- "usage.metric.days_window": "{0}-day window", "usage.metric.named": "{0}",
258
- "usage.metric.named_session": "{0} (session)", "usage.metric.named_weekly": "{0} (weekly)",
259
- "usage.metric.named_monthly": "{0} (monthly)", "usage.metric.named_days": "{0} ({1}d)",
260
- "usage.metric.extra_usage": "Extra usage", "usage.metric.rate_limit_resets": "Rate limit resets",
261
- "usage.metric.credits": "Credits", "usage.metric.weekly_limit": "Weekly limit",
262
- "usage.metric.pay_as_you_go": "Pay as you go", "usage.metric.bonus_credits": "Bonus credits",
263
- "usage.metric.overage_charges": "Overage charges", "usage.metric.pay_per_use": "Pay-per-use",
264
- "usage.metric.usage_monthly": "Usage (monthly)",
265
- "usage.unit.available": "available", "usage.unit.credits": "credits",
266
- "usage.value.disabled": "Disabled", "usage.value.cap": "{0} cap", "usage.value.unlimited": "โ™พ๏ธ Unlimited",
267
- "usage.note.binding": "binding", "usage.note.binding_severity": "binding ยท {0}",
268
- "usage.note.busiest_model": "busiest of {0} model", "usage.note.busiest_models": "busiest of {0} models",
269
- "usage.note.used_limit_unit": "{0} / {1} {2}", "usage.note.bonus_codes": "{0} / {1} {2} ยท {3} codes",
270
- "usage.metric.agy_claude_others": "Claude & others", "usage.metric.agy_claude_others_session": "Claude & others (session)",
271
- "usage.metric.agy_claude_others_weekly": "Claude & others (weekly)",
272
- "usage.hint.statusline_now": "from {0}'s statusline, just now โ€” no API call; plan and per-model limits need a CLI login",
273
- "usage.hint.statusline_minutes": "from {0}'s statusline, {1}m ago โ€” no API call; plan and per-model limits need a CLI login",
274
- "usage.hint.claude_api_key": "API-key login has no subscription usage. Use `claude /login` or set CLAUDE_CODE_OAUTH_TOKEN (from `claude setup-token`).",
275
- "usage.hint.login_claude": "Log in with the Claude Code CLI (`claude`).",
276
- "usage.hint.login_agy": "Log in with the Antigravity CLI (`agy`).", "usage.hint.no_quota": "No quota information reported for this account.",
277
- "usage.hint.login_codex": "Log in with the Codex CLI (`codex`).", "usage.hint.codex_no_oauth": "auth.json has no OAuth login (API-key-only auth cannot read usage).",
278
- "usage.hint.login_grok": "Log in with the Grok CLI (`grok login`).", "usage.hint.grok_no_login": "auth.json has no usable login. Run `grok login`.",
279
- "usage.hint.login_kiro": "Log in with the Kiro CLI (`kiro-cli`).", "usage.hint.kiro_signed_out": "Signed out โ€” run `kiro-cli` to log in.",
280
- "usage.hint.token_refreshing": "Token refreshing โ€” try again in a moment.", "usage.hint.subscription_no_credit": "Subscription โ€” no credit usage to report.",
281
- "usage.hint.no_quota_account": "No quota information available for this account type.",
282
- "usage.error.unreachable": "Could not reach {0}.", "usage.error.token_rejected": "Token rejected. Run `{0}` once to refresh the login.",
283
- "usage.error.rate_limited": "Rate limited by {0} โ€” try again later.", "usage.error.http": "Usage request failed (HTTP {0}).",
284
- "usage.error.billing_http": "Billing request failed (HTTP {0}).", "usage.error.invalid_response": "Invalid response from usage endpoint.",
285
- "usage.error.invalid_billing_response": "Invalid response from billing endpoint.", "usage.error.invalid_kiro_response": "Invalid response from GetUsageLimits.",
286
- "usage.error.claude_token_expired": "Access token expired โ€” it refreshes the next time any Claude instance runs.",
287
- "usage.error.codex_token_expired": "Access token expired. Run `codex` once to refresh it.",
288
- "usage.error.grok_expired": "Grok session expired. Run `grok login` again.", "usage.error.grok_response_changed": "Grok billing response changed.",
289
- "usage.error.agy_cap": "Individual cap reached โ€” CLI is rate limited (resets in ~{0}). The per-model pool quotas are separate and may still read 0% used.",
290
- "usage.error.agy_no_refresh": "Access token expired and no refresh token is stored. Run `agy` once.",
291
- "usage.error.agy_token_expired": "Access token expired. Run `agy` once to refresh it (or set AGY_OAUTH_CLIENT_ID/SECRET to refresh automatically).",
292
- "usage.error.agy_login_expired": "Google sign-in expired. Run `agy` once to log in again.", "usage.error.agy_refresh_empty": "Google token refresh returned no token.",
293
- "usage.error.grok_auth_readonly": "Token expired and auth.json is read-only. Run `grok` once to refresh it.", "usage.error.grok_no_refresh": "Token expired and no refresh token was found. Run `grok login`.",
294
- "usage.error.grok_refresh_unreachable": "Could not reach auth.x.ai to refresh the token.", "usage.error.grok_invalid_refresh": "Invalid refresh response from auth.x.ai.",
295
- "usage.error.grok_persist": "Refreshed but could not write auth.json. Run `grok` once instead.",
296
- usageButton: "AI usage", languageButton: "Language", helpButton: "Help", close: "Close", expandProfile: "Expand profile",
297
- rosterFailed: "Failed to load roster: {error}\n(Is the fleet running?)", paneFailed: "โš  Could not load pane for {name} (HTTP {status}).",
298
- tokenRequired: "web.token required", avatarFailed: "Avatar upload failed: {error}", saved: "Saved", saveFailed: "Save failed: {error}" },
299
- "zh-TW": { viewTitle: "AgEnD ยท ๆชข่ฆ–", editProfile: "โœ๏ธ ็ทจ่ผฏ", save: "ๅ„ฒๅญ˜", cancel: "ๅ–ๆถˆ",
300
- filterPh: "็ฏฉ้ธ๏ผšๅ็จฑใ€ๆจกๅž‹ใ€ๅพŒ็ซฏ", filterClear: "ๆธ…้™ค็ฏฉ้ธ", filterNone: "ๆฒ’ๆœ‰็ฌฆๅˆ็š„ instance", filterShown: "้กฏ็คบไธญ",
301
- displayName: "้กฏ็คบๅ็จฑ", role: "่ง’่‰ฒ", description: "ๆ่ฟฐ", tokenPh: "web.token๏ผˆๅ„ฒๅญ˜ๆ™‚้œ€่ฆ๏ผ‰",
302
- saveHint: "ๆชข่ฆ–็‚บๅ…ฌ้–‹๏ผˆๅ”ฏ่ฎ€๏ผ‰ใ€‚ๅ„ฒๅญ˜้œ€่ฆ web.token๏ผˆ~/.agend/web.token๏ผ‰ใ€‚", helpTitle: "๐Ÿ“– ไฝฟ็”จ่ชชๆ˜Ž",
303
- help1: "ๅทฆๅด้ธ instance โ†’ ๅณๅด้กฏ็คบๅณๆ™‚ terminal ็•ซ้ข",
304
- help2: "้ปž group header โ–ผ/โ–ถ ๆ”ถๅˆ/ๅฑ•้–‹", help3: "โœ๏ธ Edit ๅฏไฟฎๆ”น้กฏ็คบๅ็จฑใ€่ง’่‰ฒใ€ๆ่ฟฐ",
305
- help4: "ๆ‹–ๆ›ณ group ๆˆ– instance ๅฏ่ชฟๆ•ดๆญค็€่ฆฝๅ™จ็š„้กฏ็คบ้ †ๅบ",
306
- help5: "้ปž โคข ๅพช็’ฐๅˆ‡ๆ›ๅญ—็ดšๅฏ†ๅบฆ๏ผšๅ‰›ๅฅฝๅกซๆปฟ โ†’ ่ˆ’้ฉ โ†’ ็ฒพ็ฐก",
307
- densityFit: "ๅ‰›ๅฅฝๅกซๆปฟ", densityComfortable: "่ˆ’้ฉ", densityCompact: "็ฒพ็ฐก", densityLabel: "ๅญ—็ดšๅฏ†ๅบฆ",
308
- usageTitle: "๐Ÿ“Š AI ่จ‚้–ฑ็”จ้‡", usageEmpty: "ๆœฌๆฉŸๆฒ’ๆœ‰ๅตๆธฌๅˆฐๅทฒ็™ปๅ…ฅ็š„ AI CLIใ€‚",
309
- usageUpdated: "ๆ›ดๆ–ฐๆ–ผ", usageRefresh: "้‡ๆ–ฐๆ•ด็†", usageHigh: "ๅ้ซ˜", usageNear: "ๆŽฅ่ฟ‘ไธŠ้™",
310
- usageMoveUp: "ไธŠ็งป", usageMoveDown: "ไธ‹็งป",
311
- tooltipBackend: "ๅพŒ็ซฏ๏ผš{value}", tooltipStatus: "็‹€ๆ…‹๏ผš{value}", tooltipContext: "ไธŠไธ‹ๆ–‡๏ผš{value}",
312
- tooltipModel: "ๆจกๅž‹๏ผš{value}", tooltipEffort: "ๆŽจ็†ๅผทๅบฆ๏ผš{value}",
313
- tooltipUnavailable: "็„ก่ณ‡ๆ–™", statusRunning: "้‹่กŒไธญ", statusPaused: "ๅทฒๆšซๅœ",
314
- statusStopped: "ๅทฒๅœๆญข", statusCrashed: "ๅทฒๅดฉๆฝฐ", statusUnknown: "ๆœช็Ÿฅ",
315
- usageResets: "{t}ๅพŒ้‡็ฝฎ", usageSoon: "ๅณๅฐ‡้‡็ฝฎ", usageFail: "็„กๆณ•่ผ‰ๅ…ฅ็”จ้‡่ณ‡ๆ–™ใ€‚",
316
- "usage.title": "AI ่จ‚้–ฑ็”จ้‡", "usage.title_plain": "AI ่จ‚้–ฑ็”จ้‡", "usage.empty_active": "ๆฒ’ๆœ‰ไฝฟ็”จไธญไธ”ๆ”ฏๆด็”จ้‡่ฟฝ่นค็š„ Backend",
317
- "usage.not_logged_in": "ๆœช็™ปๅ…ฅ", "usage.error_fallback": "้Œฏ่ชค", "usage.no_data": "็„ก่ณ‡ๆ–™",
318
- "usage.reset.in": "{0} ๅพŒ้‡็ฝฎ", "usage.reset.soon": "ๅณๅฐ‡้‡็ฝฎ", "usage.ticket_expiry": "๐ŸŽซ ๆœ€่ฟ‘้ŽๆœŸ๏ผš{0}๏ผˆ{1} ๅพŒ๏ผ‰",
319
- "usage.duration.days_hours": "{0}d{1}h", "usage.duration.hours_minutes": "{0}h{1}m", "usage.duration.minutes": "{0}m",
320
- "usage.metric.session": "Session", "usage.metric.weekly": "ๆฏ้€ฑ", "usage.metric.monthly": "ๆฏๆœˆ",
321
- "usage.metric.days_window": "{0} ๅคฉ่ฆ–็ช—", "usage.metric.named": "{0}",
322
- "usage.metric.named_session": "{0}๏ผˆSession๏ผ‰", "usage.metric.named_weekly": "{0}๏ผˆ้€ฑ๏ผ‰",
323
- "usage.metric.named_monthly": "{0}๏ผˆๆฏๆœˆ๏ผ‰", "usage.metric.named_days": "{0}๏ผˆ{1} ๅคฉ๏ผ‰",
324
- "usage.metric.extra_usage": "้กๅค–็”จ้‡", "usage.metric.rate_limit_resets": "้กๅบฆ้‡็ฝฎๅˆธ",
325
- "usage.metric.credits": "้กๅบฆ", "usage.metric.weekly_limit": "ๆฏ้€ฑไธŠ้™",
326
- "usage.metric.pay_as_you_go": "้šจ็”จ้šจไป˜", "usage.metric.bonus_credits": "่ดˆ้€้กๅบฆ",
327
- "usage.metric.overage_charges": "่ถ…้ก่ฒป็”จ", "usage.metric.pay_per_use": "้šจ็”จ้šจไป˜",
328
- "usage.metric.usage_monthly": "็”จ้‡๏ผˆๆฏๆœˆ๏ผ‰",
329
- "usage.unit.available": "ๅฏ็”จ", "usage.unit.credits": "้กๅบฆ",
330
- "usage.value.disabled": "ๅทฒๅœ็”จ", "usage.value.cap": "ไธŠ้™ {0}", "usage.value.unlimited": "โ™พ๏ธ ็„ก้™",
331
- "usage.note.binding": "็”Ÿๆ•ˆ", "usage.note.binding_severity": "็”Ÿๆ•ˆ ยท {0}",
332
- "usage.note.busiest_model": "{0} ๅ€‹ๆจกๅž‹ไธญ็”จ้‡ๆœ€้ซ˜่€…", "usage.note.busiest_models": "{0} ๅ€‹ๆจกๅž‹ไธญ็”จ้‡ๆœ€้ซ˜่€…",
333
- "usage.note.used_limit_unit": "{0} / {1} {2}", "usage.note.bonus_codes": "{0} / {1} {2} ยท {3} ็ต„ไปฃ็ขผ",
334
- "usage.metric.agy_claude_others": "Claude ่ˆ‡ๅ…ถไป–ๆจกๅž‹", "usage.metric.agy_claude_others_session": "Claude ่ˆ‡ๅ…ถไป–ๆจกๅž‹๏ผˆSession๏ผ‰",
335
- "usage.metric.agy_claude_others_weekly": "Claude ่ˆ‡ๅ…ถไป–ๆจกๅž‹๏ผˆๆฏ้€ฑ๏ผ‰",
336
- "usage.hint.statusline_now": "ไพ†่‡ช {0} ็š„ statusline๏ผˆๅ‰›ๅ‰›๏ผ‰๏ผ›ๆœชๅ‘ผๅซ API๏ผŒๆ–นๆกˆ่ˆ‡ๅ„ๆจกๅž‹ไธŠ้™้œ€่ฆ CLI ็™ปๅ…ฅ่ณ‡ๆ–™",
337
- "usage.hint.statusline_minutes": "ไพ†่‡ช {0} ็š„ statusline๏ผˆ{1} ๅˆ†้˜ๅ‰๏ผ‰๏ผ›ๆœชๅ‘ผๅซ API๏ผŒๆ–นๆกˆ่ˆ‡ๅ„ๆจกๅž‹ไธŠ้™้œ€่ฆ CLI ็™ปๅ…ฅ่ณ‡ๆ–™",
338
- "usage.hint.claude_api_key": "API key ็™ปๅ…ฅๆฒ’ๆœ‰่จ‚้–ฑ็”จ้‡่ณ‡ๆ–™ใ€‚่ซ‹ไฝฟ็”จ `claude /login`๏ผŒๆˆ–่จญๅฎš CLAUDE_CODE_OAUTH_TOKEN๏ผˆ็”ฑ `claude setup-token` ๅ–ๅพ—๏ผ‰ใ€‚",
339
- "usage.hint.login_claude": "่ซ‹ไฝฟ็”จ Claude Code CLI๏ผˆ`claude`๏ผ‰็™ปๅ…ฅใ€‚",
340
- "usage.hint.login_agy": "่ซ‹ไฝฟ็”จ Antigravity CLI๏ผˆ`agy`๏ผ‰็™ปๅ…ฅใ€‚", "usage.hint.no_quota": "ๆญคๅธณ่™Ÿๆœชๅ›žๅ ฑ้กๅบฆ่ณ‡่จŠใ€‚",
341
- "usage.hint.login_codex": "่ซ‹ไฝฟ็”จ Codex CLI๏ผˆ`codex`๏ผ‰็™ปๅ…ฅใ€‚", "usage.hint.codex_no_oauth": "auth.json ๆฒ’ๆœ‰ OAuth ็™ปๅ…ฅ่ณ‡ๆ–™๏ผ›ๅชๆœ‰ API key ๆ™‚็„กๆณ•่ฎ€ๅ–็”จ้‡ใ€‚",
342
- "usage.hint.login_grok": "่ซ‹ไฝฟ็”จ Grok CLI๏ผˆ`grok login`๏ผ‰็™ปๅ…ฅใ€‚", "usage.hint.grok_no_login": "auth.json ๆฒ’ๆœ‰ๅฏ็”จ็š„็™ปๅ…ฅ่ณ‡ๆ–™ใ€‚่ซ‹ๅŸท่กŒ `grok login`ใ€‚",
343
- "usage.hint.login_kiro": "่ซ‹ไฝฟ็”จ Kiro CLI๏ผˆ`kiro-cli`๏ผ‰็™ปๅ…ฅใ€‚", "usage.hint.kiro_signed_out": "็›ฎๅ‰ๅทฒ็™ปๅ‡บ๏ผ›่ซ‹ๅŸท่กŒ `kiro-cli` ็™ปๅ…ฅใ€‚",
344
- "usage.hint.token_refreshing": "Token ๆ›ดๆ–ฐไธญ โ€” ่ซ‹็จๅ€™ๅ†่ฉฆใ€‚", "usage.hint.subscription_no_credit": "่จ‚้–ฑๆ–นๆกˆๆฒ’ๆœ‰ๅฏๅ›žๅ ฑ็š„้กๅบฆ็”จ้‡ใ€‚",
345
- "usage.hint.no_quota_account": "ๆญคๅธณ่™Ÿ้กžๅž‹ๆฒ’ๆœ‰ๅฏ็”จ็š„้กๅบฆ่ณ‡่จŠใ€‚",
346
- "usage.error.unreachable": "็„กๆณ•้€ฃ็ทš่‡ณ {0}ใ€‚", "usage.error.token_rejected": "Token ๅทฒ่ขซๆ‹’็ต•ใ€‚่ซ‹ๅŸท่กŒไธ€ๆฌก `{0}` ไปฅๆ›ดๆ–ฐ็™ปๅ…ฅใ€‚",
347
- "usage.error.rate_limited": "{0} ๆšซๆ™‚้™ๅˆถ่ซ‹ๆฑ‚้€Ÿ็އ๏ผŒ่ซ‹็จๅพŒๅ†่ฉฆใ€‚", "usage.error.http": "็”จ้‡่ซ‹ๆฑ‚ๅคฑๆ•—๏ผˆHTTP {0}๏ผ‰ใ€‚",
348
- "usage.error.billing_http": "่จˆ่ฒป่ซ‹ๆฑ‚ๅคฑๆ•—๏ผˆHTTP {0}๏ผ‰ใ€‚", "usage.error.invalid_response": "็”จ้‡็ซฏ้ปžๅ›žๅ‚ณไบ†็„กๆ•ˆ่ณ‡ๆ–™ใ€‚",
349
- "usage.error.invalid_billing_response": "่จˆ่ฒป็ซฏ้ปžๅ›žๅ‚ณไบ†็„กๆ•ˆ่ณ‡ๆ–™ใ€‚", "usage.error.invalid_kiro_response": "GetUsageLimits ๅ›žๅ‚ณไบ†็„กๆ•ˆ่ณ‡ๆ–™ใ€‚",
350
- "usage.error.claude_token_expired": "Access token ๅทฒ้ŽๆœŸ๏ผ›ไธ‹ๆฌกๅ•Ÿๅ‹•ไปปไธ€ Claude instance ๆ™‚ๆœƒ้‡ๆ–ฐๆ•ด็†ใ€‚",
351
- "usage.error.codex_token_expired": "Access token ๅทฒ้ŽๆœŸใ€‚่ซ‹ๅŸท่กŒไธ€ๆฌก `codex` ไปฅ้‡ๆ–ฐๆ•ด็†ใ€‚",
352
- "usage.error.grok_expired": "Grok session ๅทฒ้ŽๆœŸใ€‚่ซ‹ๅ†ๆฌกๅŸท่กŒ `grok login`ใ€‚", "usage.error.grok_response_changed": "Grok ่จˆ่ฒปๅ›žๆ‡‰ๆ ผๅผๅทฒ่ฎŠๆ›ดใ€‚",
353
- "usage.error.agy_cap": "ๅ€‹ไบบไธŠ้™ๅทฒ็”จๅฎŒ๏ผŒCLI ๆญฃๅ—ๅˆฐ้€Ÿ็އ้™ๅˆถ๏ผˆ็ด„ {0} ๅพŒ้‡็ฝฎ๏ผ‰ใ€‚ๅ„ๆจกๅž‹็š„้กๅบฆๆฑ ๆ˜ฏๅฆไธ€็ต„่จˆๆ•ธ๏ผŒๅ› ๆญคไปๅฏ่ƒฝ้กฏ็คบ 0% ๅทฒไฝฟ็”จใ€‚",
354
- "usage.error.agy_no_refresh": "Access token ๅทฒ้ŽๆœŸ๏ผŒไธ”ๆœชๅ„ฒๅญ˜ refresh tokenใ€‚่ซ‹ๅŸท่กŒไธ€ๆฌก `agy`ใ€‚",
355
- "usage.error.agy_token_expired": "Access token ๅทฒ้ŽๆœŸใ€‚่ซ‹ๅŸท่กŒไธ€ๆฌก `agy` ้‡ๆ–ฐๆ•ด็†๏ผŒๆˆ–่จญๅฎš AGY_OAUTH_CLIENT_ID/SECRET ไปฅ่‡ชๅ‹•ๆ›ดๆ–ฐใ€‚",
356
- "usage.error.agy_login_expired": "Google ็™ปๅ…ฅๅทฒ้ŽๆœŸใ€‚่ซ‹ๅŸท่กŒไธ€ๆฌก `agy` ้‡ๆ–ฐ็™ปๅ…ฅใ€‚", "usage.error.agy_refresh_empty": "Google token ๆ›ดๆ–ฐๅ›žๆ‡‰ๆฒ’ๆœ‰ access tokenใ€‚",
357
- "usage.error.grok_auth_readonly": "Token ๅทฒ้ŽๆœŸไธ” auth.json ็‚บๅ”ฏ่ฎ€ใ€‚่ซ‹ๅŸท่กŒไธ€ๆฌก `grok` ้‡ๆ–ฐๆ•ด็†ใ€‚", "usage.error.grok_no_refresh": "Token ๅทฒ้ŽๆœŸไธ”ๆ‰พไธๅˆฐ refresh tokenใ€‚่ซ‹ๅŸท่กŒ `grok login`ใ€‚",
358
- "usage.error.grok_refresh_unreachable": "็„กๆณ•้€ฃ็ทš่‡ณ auth.x.ai ไปฅๆ›ดๆ–ฐ tokenใ€‚", "usage.error.grok_invalid_refresh": "auth.x.ai ๅ›žๅ‚ณไบ†็„กๆ•ˆ็š„ token ๆ›ดๆ–ฐ่ณ‡ๆ–™ใ€‚",
359
- "usage.error.grok_persist": "Token ๅทฒๆ›ดๆ–ฐไฝ†็„กๆณ•ๅฏซๅ…ฅ auth.jsonใ€‚่ซ‹ๆ”น็‚บๅŸท่กŒไธ€ๆฌก `grok`ใ€‚",
360
- usageButton: "AI ่จ‚้–ฑ็”จ้‡", languageButton: "่ชž่จ€", helpButton: "ไฝฟ็”จ่ชชๆ˜Ž", close: "้—œ้–‰", expandProfile: "ๅฑ•้–‹ๅ€‹ไบบ่ณ‡ๆ–™",
361
- rosterFailed: "่ผ‰ๅ…ฅ instance ๆธ…ๅ–ฎๅคฑๆ•—๏ผš{error}\n๏ผˆFleet ๆ˜ฏๅฆๆญฃๅœจๅŸท่กŒ๏ผŸ๏ผ‰", paneFailed: "โš  ็„กๆณ•่ผ‰ๅ…ฅ {name} ็š„ pane๏ผˆHTTP {status}๏ผ‰ใ€‚",
362
- tokenRequired: "้œ€่ฆ web.token", avatarFailed: "ไธŠๅ‚ณ้ ญๅƒๅคฑๆ•—๏ผš{error}", saved: "ๅทฒๅ„ฒๅญ˜", saveFailed: "ๅ„ฒๅญ˜ๅคฑๆ•—๏ผš{error}" },
363
- };
364
- let lang = localStorage.getItem("agend_lang") || (navigator.language && navigator.language.toLowerCase().startsWith("zh") ? "zh-TW" : "en");
365
- if (!I18N[lang]) lang = "en";
366
- const T = (k) => (I18N[lang] && I18N[lang][k]) || I18N.en[k] || k;
367
- const TF = (k, values = {}) => Object.entries(values).reduce((s, [name, value]) => s.replaceAll(`{${name}}`, String(value)), T(k));
368
- function applyI18n() {
369
- document.querySelectorAll("[data-i18n]").forEach(e => { e.textContent = T(e.getAttribute("data-i18n")); });
370
- document.querySelectorAll("[data-i18n-ph]").forEach(e => { e.setAttribute("placeholder", T(e.getAttribute("data-i18n-ph"))); });
371
- document.querySelectorAll("[data-i18n-title]").forEach(e => { e.setAttribute("title", T(e.getAttribute("data-i18n-title"))); });
372
- const hc = q("helpContent");
373
- if (hc) { hc.innerHTML = ""; const h3 = document.createElement("h3"); h3.textContent = T("helpTitle"); hc.append(h3);
374
- const ul = document.createElement("ul"); for (const k of ["help1","help2","help3","help4","help5"]) { const li = document.createElement("li"); li.textContent = T(k); ul.append(li); } hc.append(ul); }
375
- renderDensityBtn();
376
- }
377
-
378
- let roster = [];
379
- let rosterByName = new Map();
380
- let current = null;
381
- // #999: the sidebar filter. Only the input and its clear button change it, so
382
- // the 5-second roster refresh re-renders through it instead of resetting it.
383
- let filterQuery = "";
384
- let poll = null;
385
- // Pane grid in cells, reported by /api/pane's X-Pane-Cols / X-Pane-Rows.
386
- // This โ€” not the captured text โ€” is the font-size input.
387
- let paneCols = 0, paneRows = 0;
388
- let lastFitKey = ""; // skip redundant recalcs
389
- let appliedSize = 0; // last font-size actually written to #pre
390
- let resizeTimer = null;
391
- // Font density โ€” the manual escape hatch. Once the tmux grid is large
392
- // (120x32), the auto-fit size is necessarily small, so the user needs a way
393
- // to trade "everything visible" against "readable glyphs".
394
- const DENSITY = { fit: 1, comfortable: 1.25, compact: 0.8 };
395
- const DENSITY_GLYPH = { fit: "โคข", comfortable: "A+", compact: "Aโˆ’" };
396
- const DENSITY_ORDER = ["fit", "comfortable", "compact"];
397
- let density = localStorage.getItem("agend_view_density");
398
- if (!DENSITY[density]) density = "fit";
399
- const collapsed = new Set(); // group names the user has collapsed
400
- // Drag-sort state
401
- let sortGroups = new Map(); // groupName -> sort_index (persisted override)
402
- let sortInsts = new Map(); // instanceName -> sort_index (within its group)
403
- let groupNames = []; // group display order (computed)
404
- let instByGroup = new Map(); // group -> [instance_name, โ€ฆ] display order (computed)
405
- let dragItem = null; // { type:'group'|'instance', name, group }
406
- const SIDEBAR_ORDER_KEY = "agend_view_sidebar_order";
407
- const USAGE_ORDER_KEY = "agend_view_usage_order";
408
-
409
- const api = (path) => urlToken ? `${path}${path.includes("?") ? "&" : "?"}token=${encodeURIComponent(urlToken)}` : path;
410
-
411
- // โ”€โ”€ ANSI (SGR) โ†’ HTML โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
412
- const BASE = ["#000000","#cd3131","#0dbc79","#e5e510","#2472c8","#bc3fbc","#11a8cd","#e5e5e5"];
413
- const BRIGHT = ["#666666","#f14c4c","#23d18b","#f5f543","#3b8eea","#d670d6","#29b8db","#ffffff"];
414
- const esc = (s) => s.replace(/[&<>]/g, c => ({ "&":"&amp;", "<":"&lt;", ">":"&gt;" }[c]));
415
-
416
- function ansiToHtml(text) {
417
- let out = "", fg = null, bg = null, bold = false, open = false;
418
- const style = () => {
419
- const p = [];
420
- let f = fg;
421
- if (bold && typeof f === "number" && f < 8) f = BRIGHT[f];
422
- else if (typeof f === "number") f = BASE[f];
423
- if (f) p.push("color:" + f);
424
- if (bg) p.push("background:" + (typeof bg === "number" ? BASE[bg] : bg));
425
- if (bold) p.push("font-weight:bold");
426
- return p.join(";");
427
- };
428
- const flush = () => { if (open) { out += "</span>"; open = false; } };
429
- const openSpan = () => { const s = style(); if (s) { out += `<span style="${s}">`; open = true; } };
430
-
431
- const re = /\x1b\[([0-9;]*)m/g;
432
- let last = 0, m;
433
- const emit = (chunk) => { if (!chunk) return; if (!open) openSpan(); out += esc(chunk); };
434
- while ((m = re.exec(text)) !== null) {
435
- emit(text.slice(last, m.index));
436
- last = re.lastIndex;
437
- flush();
438
- const codes = m[1].split(";").filter(x => x !== "").map(Number);
439
- if (codes.length === 0) codes.push(0);
440
- for (let i = 0; i < codes.length; i++) {
441
- const c = codes[i];
442
- if (c === 0) { fg = bg = null; bold = false; }
443
- else if (c === 1) bold = true;
444
- else if (c === 22) bold = false;
445
- else if (c === 39) fg = null;
446
- else if (c === 49) bg = null;
447
- else if (c >= 30 && c <= 37) fg = c - 30;
448
- else if (c >= 90 && c <= 97) fg = BRIGHT[c - 90];
449
- else if (c >= 40 && c <= 47) bg = c - 40;
450
- else if (c >= 100 && c <= 107) bg = BRIGHT[c - 100];
451
- else if (c === 38 || c === 48) {
452
- const target = c === 38 ? "fg" : "bg";
453
- if (codes[i + 1] === 5) { const n = codes[i + 2]; const col = xterm256(n); if (target === "fg") fg = col; else bg = col; i += 2; }
454
- 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; }
455
- }
456
- }
457
- }
458
- emit(text.slice(last));
459
- flush();
460
- return out;
461
- }
462
- function xterm256(n) {
463
- if (n < 8) return BASE[n];
464
- if (n < 16) return BRIGHT[n - 8];
465
- 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)})`; }
466
- const l = (n - 232) * 10 + 8; return `rgb(${l},${l},${l})`;
467
- }
468
-
469
- // โ”€โ”€ Sidebar (grouped by tag) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
470
- // Group name: "Classic" if tagged classic, else the first tag, else "Other".
471
- function groupOf(it) {
472
- const tags = it.tags || [];
473
- if (tags.includes("classic")) return "Classic";
474
- if (tags.length) return tags[0];
475
- return "Other";
476
- }
477
- function sidebarAlias(it) {
478
- const alias = typeof it.display_name === "string" ? it.display_name.trim() : "";
479
- return alias && alias !== it.instance_name ? alias : "";
480
- }
481
-
482
- const BACKEND_LABELS = {
483
- "claude-code": "Claude Code",
484
- "kiro-cli": "Kiro CLI",
485
- codex: "Codex",
486
- grok: "Grok Build",
487
- antigravity: "Antigravity",
488
- opencode: "OpenCode",
489
- };
490
- const STATUS_LABEL_KEYS = {
491
- running: "statusRunning",
492
- paused: "statusPaused",
493
- stopped: "statusStopped",
494
- crashed: "statusCrashed",
495
- };
496
-
497
- function backendLabel(backend) {
498
- return BACKEND_LABELS[backend] || backend || "Unknown";
499
- }
500
-
501
- function backendIconHtml(backend) {
502
- const known = new Set(["claude-code", "kiro-cli", "codex", "grok", "antigravity", "opencode"]);
503
- const id = known.has(backend) ? backend : "other";
504
- const label = backendLabel(backend);
505
- return `<span class="cli-icon cli-${id}" title="${esc(label)}" aria-label="Backend: ${esc(label)}"></span>`;
506
- }
507
-
508
- function instanceTooltip(it) {
509
- const context = it.context_pct != null && Number.isFinite(it.context_pct)
510
- ? `${Math.round(Math.min(100, Math.max(0, it.context_pct)))}%`
511
- : T("tooltipUnavailable");
512
- const statusKey = STATUS_LABEL_KEYS[it.status];
513
- const status = statusKey ? T(statusKey) : it.status || T("statusUnknown");
514
- const translated = (key, value) => T(key).replace("{value}", value);
515
- // B3: display_name as first line, then raw instance_name if they differ
516
- const lines = [];
517
- if (it.display_name && it.display_name !== it.instance_name) {
518
- lines.push(it.display_name);
519
- lines.push(`(${it.instance_name})`);
520
- } else {
521
- lines.push(it.instance_name);
522
- }
523
- lines.push(translated("tooltipBackend", backendLabel(it.backend)));
524
- // Model with source labeling: instance/classic โ†’ configured, fleet-default โ†’ fleet default
525
- // live/cli-default/unresolved โ†’ no suffix (resolved.display already explains)
526
- const modelDisplay = it.model || T("tooltipUnavailable");
527
- const modelSuffix = it.model_source === "instance" || it.model_source === "classic" ? " (configured)" : it.model_source === "fleet-default" ? " (fleet default)" : "";
528
- lines.push(translated("tooltipModel", modelDisplay + modelSuffix));
529
- lines.push(translated("tooltipStatus", status));
530
- lines.push(translated("tooltipContext", context));
531
- // Effort: only show if present (backend capability already filtered by getUiStatus)
532
- if (it.effort) {
533
- const effortSuffix = it.effort_source === "instance" || it.effort_source === "classic" ? " (configured)" : it.effort_source === "fleet-default" ? " (fleet default)" : "";
534
- lines.push(translated("tooltipEffort", it.effort + effortSuffix));
535
- }
536
- if (typeof it.description === "string" && it.description.trim()) {
537
- lines.push("", it.description.trim());
538
- }
539
- return lines.join("\n");
540
- }
541
-
542
- // Compute display order: persisted sort-order first (by index), then any
543
- // new/unsorted groups & instances by the tag+alpha fallback. Stored in
544
- // groupNames / instByGroup so drag handlers can mutate + persist them.
545
- function computeOrder() {
546
- const groups = new Map();
547
- for (const it of roster) {
548
- const g = groupOf(it);
549
- if (!groups.has(g)) groups.set(g, []);
550
- groups.get(g).push(it);
551
- }
552
- const gidx = (g) => (sortGroups.has(g) ? sortGroups.get(g) : Infinity);
553
- groupNames = [...groups.keys()].sort((a, b) => {
554
- const ia = gidx(a), ib = gidx(b);
555
- if (ia !== ib) return ia - ib;
556
- if (a === "Other") return 1;
557
- if (b === "Other") return -1;
558
- return a.localeCompare(b);
559
- });
560
- instByGroup = new Map();
561
- for (const [g, items] of groups) {
562
- // stable sort keeps roster order for equal (unsorted) indices
563
- const names = items.map(it => it.instance_name).sort((a, b) => {
564
- const ia = sortInsts.has(a) ? sortInsts.get(a) : Infinity;
565
- const ib = sortInsts.has(b) ? sortInsts.get(b) : Infinity;
566
- return ia - ib;
567
- });
568
- instByGroup.set(g, names);
569
- }
570
- }
571
-
572
- // โ”€โ”€ sidebar filter (#999) โ€” pure; tests execute this block โ”€โ”€
573
- function normalizeFilter(query) {
574
- return String(query == null ? "" : query).trim().toLowerCase();
575
- }
576
- /** Case-insensitive substring on name (incl. display name), model and backend. */
577
- function instanceMatchesFilter(it, needle, labelOf) {
578
- if (!needle) return true;
579
- const fields = [it.instance_name, it.display_name, it.model, it.backend, labelOf ? labelOf(it.backend) : null];
580
- return fields.some(value => typeof value === "string" && value.toLowerCase().includes(needle));
581
- }
582
- /** Visible groups in display order, and the "N / M" counts. */
583
- function filterSidebar(groupNames, instByGroup, rosterByName, query, labelOf) {
584
- const needle = normalizeFilter(query);
585
- const groups = [];
586
- let shown = 0, total = 0;
587
- for (const g of groupNames) {
588
- const names = (instByGroup.get(g) || []).filter(name => rosterByName.has(name));
589
- total += names.length;
590
- const visible = names.filter(name => instanceMatchesFilter(rosterByName.get(name), needle, labelOf));
591
- shown += visible.length;
592
- if (visible.length) groups.push({ group: g, names: visible });
593
- }
594
- return { groups, shown, total, active: needle.length > 0 };
595
- }
596
- // โ”€โ”€ end sidebar filter โ”€โ”€
597
-
598
- function renderFilterStatus(view) {
599
- const count = q("filterCount");
600
- count.innerHTML = view.active
601
- ? `<span class="filtering">${view.shown} / ${view.total}</span><span>${esc(T("filterShown"))}</span>`
602
- : `<span>${view.shown} / ${view.total}</span>`;
603
- q("filterClear").classList.toggle("show", filterQuery.length > 0);
604
- }
605
-
606
- function renderList() {
607
- const list = q("list");
608
- list.innerHTML = "";
609
- const view = filterSidebar(groupNames, instByGroup, rosterByName, filterQuery, backendLabel);
610
- for (const { group: g, names } of view.groups) {
611
- const isCollapsed = collapsed.has(g);
612
- const header = document.createElement("div");
613
- header.className = "group-header";
614
- // Reordering a filtered subset would be ambiguous; drag only the full list.
615
- header.draggable = !view.active;
616
- header.innerHTML = `<span class="caret">${isCollapsed ? "โ–ถ" : "โ–ผ"}</span><span>${esc(g)}</span><span class="gcount">${names.length}</span>`;
617
- header.onclick = () => { if (collapsed.has(g)) collapsed.delete(g); else collapsed.add(g); renderList(); };
618
- wireDrag(header, { type: "group", name: g });
619
- list.appendChild(header);
620
- if (isCollapsed) continue;
621
-
622
- for (const name of names) {
623
- const it = rosterByName.get(name);
624
- const el = document.createElement("div");
625
- el.className = "inst" + (name === current ? " active" : "");
626
- el.draggable = !view.active;
627
- // M2: context_pct != null shows value including 0%; null means unavailable (empty)
628
- const alias = sidebarAlias(it);
629
- el.innerHTML = `<span class="dot ${it.status}"></span><span class="identity"><span class="nm">${esc(it.instance_name)}</span>${alias ? `<span class="alias">${esc(alias)}</span>` : ""}</span><span class="pct">${it.context_pct != null ? Math.round(it.context_pct)+"%" : ""}</span>${backendIconHtml(it.backend)}`;
630
- el.title = instanceTooltip(it);
631
- el.onclick = () => select(name);
632
- wireDrag(el, { type: "instance", name, group: g });
633
- list.appendChild(el);
634
- }
635
- }
636
- if (view.active && !view.groups.length) list.innerHTML = `<div class="filter-empty">${esc(T("filterNone"))}</div>`;
637
- renderFilterStatus(view);
638
- }
639
-
640
- // โ”€โ”€ Drag & drop โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
641
- function wireDrag(el, item) {
642
- el.addEventListener("dragstart", (e) => {
643
- dragItem = item;
644
- el.classList.add("dragging");
645
- e.dataTransfer.effectAllowed = "move";
646
- try { e.dataTransfer.setData("text/plain", item.name); } catch { /* Firefox needs data set */ }
647
- });
648
- el.addEventListener("dragend", () => {
649
- el.classList.remove("dragging");
650
- document.querySelectorAll(".drop-before,.drop-after").forEach(n => n.classList.remove("drop-before", "drop-after"));
651
- dragItem = null;
652
- });
653
- el.addEventListener("dragover", (e) => {
654
- if (!dragItem || dragItem.type !== item.type) return; // only same kind
655
- if (item.type === "instance" && dragItem.group !== item.group) return; // no cross-group
656
- if (dragItem.name === item.name) return;
657
- e.preventDefault();
658
- const before = e.clientY < el.getBoundingClientRect().top + el.offsetHeight / 2;
659
- el.classList.toggle("drop-before", before);
660
- el.classList.toggle("drop-after", !before);
661
- });
662
- el.addEventListener("dragleave", () => el.classList.remove("drop-before", "drop-after"));
663
- el.addEventListener("drop", (e) => {
664
- e.preventDefault();
665
- const before = el.classList.contains("drop-before");
666
- el.classList.remove("drop-before", "drop-after");
667
- if (!dragItem || dragItem.type !== item.type) return;
668
- if (item.type === "group") reorder(groupNames, dragItem.name, item.name, before);
669
- else if (dragItem.group === item.group) reorder(instByGroup.get(item.group), dragItem.name, item.name, before);
670
- else return;
671
- renderList();
672
- persistOrder();
673
- });
674
- }
675
-
676
- function reorder(arr, moving, target, before) {
677
- if (!arr) return;
678
- const from = arr.indexOf(moving);
679
- if (from === -1) return;
680
- arr.splice(from, 1);
681
- let to = arr.indexOf(target);
682
- if (to === -1) { arr.push(moving); return; }
683
- if (!before) to += 1;
684
- arr.splice(to, 0, moving);
685
- }
686
-
687
- function orderRows() {
688
- const rows = [];
689
- groupNames.forEach((g, i) => rows.push({ item_type: "group", item_name: g, sort_index: i, group_name: null }));
690
- for (const g of groupNames) {
691
- (instByGroup.get(g) || []).forEach((n, j) => rows.push({ item_type: "instance", item_name: n, sort_index: j, group_name: g }));
692
- }
693
- return rows;
694
- }
695
-
696
- function persistOrder() {
697
- // Mirror into the local maps so a mid-flight refresh doesn't revert.
698
- sortGroups = new Map(groupNames.map((g, i) => [g, i]));
699
- sortInsts = new Map();
700
- for (const g of groupNames) (instByGroup.get(g) || []).forEach((n, j) => sortInsts.set(n, j));
701
- try { localStorage.setItem(SIDEBAR_ORDER_KEY, JSON.stringify(orderRows())); }
702
- catch { /* ordering still applies for this page even if storage is unavailable */ }
703
- }
704
-
705
- function restoreSidebarOrder() {
706
- sortGroups = new Map(); sortInsts = new Map();
707
- try {
708
- const rows = JSON.parse(localStorage.getItem(SIDEBAR_ORDER_KEY) || "[]");
709
- if (!Array.isArray(rows)) return;
710
- for (const row of rows) {
711
- if (row.item_type === "group" && typeof row.item_name === "string") sortGroups.set(row.item_name, Number(row.sort_index) || 0);
712
- else if (row.item_type === "instance" && typeof row.item_name === "string") sortInsts.set(row.item_name, Number(row.sort_index) || 0);
713
- }
714
- } catch { /* ignore invalid browser-local state */ }
715
- }
716
-
717
- function renderCard() {
718
- const it = roster.find(r => r.instance_name === current);
719
- if (!it) return;
720
- q("dname").textContent = it.display_name || it.instance_name;
721
- q("role").textContent = it.role || "";
722
- // R2: context_pct != null shows value including 0%; null means unavailable (hide)
723
- const ctxPart = it.context_pct != null ? ` ยท ctx ${Math.round(it.context_pct)}%` : "";
724
- q("meta").textContent = `${it.instance_name} ยท ${it.backend}${it.model ? " ยท " + it.model : ""}${ctxPart} ยท ${it.status}`;
725
- q("desc").textContent = it.description || "";
726
- // Only issue an <img> request when an avatar actually exists โ€” otherwise a
727
- // 404 shows a broken-image icon. No avatar โ†’ CSS placeholder (initial).
728
- const box = q("avatarBox");
729
- if (it.has_avatar) {
730
- const base = api(`/api/avatar/${encodeURIComponent(it.instance_name)}`);
731
- // Only rebuild (and cache-bust) when the avatar's instance changed.
732
- // Rebuilding every renderCard re-issued the <img> request each 5s
733
- // loadRoster tick, which made the avatar flicker. Compare the raw src
734
- // attribute (relative, as set) rather than img.src (resolved absolute).
735
- const existingImg = box.querySelector("img");
736
- const cur = existingImg ? (existingImg.getAttribute("src") || "") : "";
737
- // The delimiter (base+"?" / base+"&") prevents a prefix collision between
738
- // e.g. ".../foo" and ".../foobar".
739
- const sameAvatar = existingImg && (cur.startsWith(base + "?") || cur.startsWith(base + "&"));
740
- if (!sameAvatar) {
741
- const src = base + (base.includes("?") ? "&" : "?") + "t=" + Date.now(); // cache-bust, correct query separator
742
- box.innerHTML = `<img class="avatar-img" alt="" src="${src}">`;
743
- }
744
- } else {
745
- const initial = (it.display_name || it.instance_name || "?").trim().charAt(0) || "?";
746
- const ph = box.querySelector(".avatar-ph");
747
- if (!ph || ph.textContent !== initial) {
748
- box.innerHTML = `<div class="avatar-ph">${esc(initial)}</div>`;
749
- }
750
- }
751
- }
752
-
753
- async function loadRoster() {
754
- try {
755
- const pr = await fetch(api("/api/profiles"));
756
- if (!pr.ok) throw new Error("HTTP " + pr.status);
757
- roster = await pr.json();
758
- rosterByName = new Map(roster.map(r => [r.instance_name, r]));
759
- if (!current && roster.length) current = roster[0].instance_name;
760
- computeOrder();
761
- renderList(); renderCard();
762
- } catch (e) { q("pre").textContent = TF("rosterFailed", { error: e.message }); }
763
- }
764
-
765
- // โ”€โ”€ Terminal โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
766
- async function refreshPane() {
767
- if (!current) return;
768
- // If the user is selecting text inside the pane, skip this refresh so the
769
- // 800ms poll doesn't wipe the selection mid-copy. Resumes on the next tick
770
- // once the selection is released/collapsed.
771
- const sel = window.getSelection();
772
- if (sel && sel.toString().length > 0 && sel.anchorNode && q("pre").contains(sel.anchorNode)) return;
773
- const target = current; // guard against a switch mid-fetch
774
- try {
775
- const r = await fetch(api(`/api/pane/${encodeURIComponent(target)}`));
776
- if (target !== current) return; // user switched while awaiting โ€” drop stale
777
- if (!r.ok) {
778
- // Surface the failure instead of leaving the previous instance's output
779
- // on screen (which reads as "switching did nothing").
780
- q("pre").textContent = TF("paneFailed", { name: target, status: r.status });
781
- return;
782
- }
783
- const text = await r.text();
784
- if (target !== current) return;
785
- // Pane grid comes from tmux, so a sparse frame can't distort the fit.
786
- const c = Number(r.headers.get("X-Pane-Cols")), rw = Number(r.headers.get("X-Pane-Rows"));
787
- if (c >= 1 && rw >= 1 && (c !== paneCols || rw !== paneRows)) { paneCols = c; paneRows = rw; lastFitKey = ""; }
788
- q("pre").innerHTML = ansiToHtml(text);
789
- fitFont();
790
- } catch { /* transient network error โ€” keep last frame */ }
791
- }
792
-
793
- // โ”€โ”€ Font sizing โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
794
- // Measured advance width of one monospace cell as a fraction of font-size.
795
- // The old hardcoded .6em was only an estimate and drifts per resolved font.
796
- // The ratio is font-size independent, so measuring once is enough.
797
- let cellRatio = 0;
798
- function glyphRatio() {
799
- if (cellRatio) return cellRatio;
800
- const probe = document.createElement("span");
801
- probe.style.cssText = "position:absolute;visibility:hidden;white-space:pre;left:-9999px;top:0;font-size:100px";
802
- probe.style.fontFamily = getComputedStyle(q("pre")).fontFamily;
803
- probe.textContent = "0".repeat(100);
804
- document.body.appendChild(probe);
805
- const w = probe.getBoundingClientRect().width;
806
- probe.remove();
807
- if (w > 0) cellRatio = w / 100 / 100; // รท100 chars, รท100px font-size
808
- return cellRatio || 0.6; // headless / odd env โ†’ old estimate
809
- }
810
-
811
- // The removed 48px ceiling was what made an 80x24 pane render at ~33px on a
812
- // 1080p screen โ€” the "font is huge" complaint. A single desktop range is
813
- // enough; /view is not targeting phones.
814
- const MIN_PX = 12, MAX_PX = 22;
815
-
816
- // Order: fit โ†’ clamp โ†’ user density โ†’ absolute readability bounds. When the
817
- // clamped size can't show all `cols`, #term scrolls rather than shrinking
818
- // below the floor.
819
- function fitFont() {
820
- if (paneCols < 1 || paneRows < 1) return; // no trustworthy size yet
821
- const term = q("term"), pre = q("pre");
822
- const cs = getComputedStyle(term);
823
- const padX = parseFloat(cs.paddingLeft) + parseFloat(cs.paddingRight);
824
- const padY = parseFloat(cs.paddingTop) + parseFloat(cs.paddingBottom);
825
- const innerW = Math.max(1, term.clientWidth - (padX || 0));
826
- const innerH = Math.max(1, term.clientHeight - (padY || 0));
827
- const key = `${paneCols}x${paneRows}@${Math.round(innerW)}x${Math.round(innerH)}/${density}`;
828
- if (key === lastFitKey) return;
829
- const pcs = getComputedStyle(pre);
830
- const lhRatio = parseFloat(pcs.lineHeight) / parseFloat(pcs.fontSize);
831
- const lineH = Number.isFinite(lhRatio) && lhRatio > 0 ? lhRatio : 1.2;
832
- const widthFit = innerW / (paneCols * glyphRatio());
833
- const heightFit = innerH / (paneRows * lineH);
834
- // 0.5% slack keeps an exact fit from rounding into a scrollbar (paired with
835
- // scrollbar-gutter:stable, that's what stops resizeโ†’scrollbarโ†’resize loops).
836
- let size = Math.min(widthFit, heightFit) * 0.995;
837
- size = Math.max(MIN_PX, Math.min(size, MAX_PX));
838
- size *= DENSITY[density] || 1;
839
- size = Math.max(8, Math.min(size, 32));
840
- lastFitKey = key;
841
- // Sub-pixel churn (a scrollbar appearing shrinks the box, which would shrink
842
- // the font, which would hide the scrollbar again) resolves to a no-op here.
843
- if (Math.abs(size - appliedSize) < 0.75) return;
844
- appliedSize = size;
845
- pre.style.fontSize = size.toFixed(2) + "px";
846
- }
847
-
848
- function scheduleFit() {
849
- clearTimeout(resizeTimer);
850
- resizeTimer = setTimeout(() => { lastFitKey = ""; fitFont(); }, 80);
851
- }
852
-
853
- function renderDensityBtn() {
854
- const b = q("densityBtn");
855
- if (!b) return;
856
- b.textContent = DENSITY_GLYPH[density];
857
- b.title = `${T("densityLabel")}: ${T("density" + density.charAt(0).toUpperCase() + density.slice(1))}`;
858
- }
859
-
860
- function cycleDensity() {
861
- density = DENSITY_ORDER[(DENSITY_ORDER.indexOf(density) + 1) % DENSITY_ORDER.length];
862
- localStorage.setItem("agend_view_density", density);
863
- renderDensityBtn();
864
- lastFitKey = "";
865
- fitFont();
866
- }
867
-
868
- function select(name) {
869
- if (name === current) return;
870
- current = name;
871
- lastFitKey = ""; // force a refit once the new pane's size lands
872
- exitEdit();
873
- q("pre").textContent = ""; // blank the old instance's output immediately
874
- renderList(); renderCard(); refreshPane();
875
- }
876
-
877
- // โ”€โ”€ Edit โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
878
- // The card is a one-line bar by default; expanding it (or editing) is what
879
- // borrows height back from the terminal.
880
- let cardExpanded = localStorage.getItem("agend_view_card_expanded") === "1";
881
- function applyCardExpanded(on) {
882
- q("card").classList.toggle("expanded", on);
883
- const t = q("cardToggle");
884
- t.textContent = on ? "โ–ผ" : "โ–ฒ";
885
- t.setAttribute("aria-expanded", on ? "true" : "false");
886
- scheduleFit(); // the terminal just gained or lost height
887
- }
888
- function setCardExpanded(on, persist) {
889
- if (persist) { cardExpanded = on; localStorage.setItem("agend_view_card_expanded", on ? "1" : "0"); }
890
- applyCardExpanded(on);
891
- }
892
-
893
- function enterEdit() {
894
- const it = roster.find(r => r.instance_name === current);
895
- if (!it) return;
896
- q("fDisplay").value = it.display_name || "";
897
- q("fRole").value = it.role || "";
898
- q("fDesc").value = it.description || "";
899
- q("fToken").value = localStorage.getItem("agend_web_token") || "";
900
- q("msg").textContent = "";
901
- applyCardExpanded(true); // form needs the room; don't persist it
902
- q("cardBody").style.display = "none";
903
- q("cardToggle").style.display = "none";
904
- q("editBtn").style.display = "none";
905
- q("edit").style.display = "flex";
906
- }
907
- function exitEdit() {
908
- q("edit").style.display = "none";
909
- q("cardBody").style.display = "";
910
- q("cardToggle").style.display = "";
911
- q("editBtn").style.display = "";
912
- applyCardExpanded(cardExpanded); // back to the user's own preference
913
- }
914
-
915
- async function save() {
916
- const it = roster.find(r => r.instance_name === current);
917
- if (!it) return;
918
- const wt = q("fToken").value.trim();
919
- if (!wt) { q("msg").className = "err"; q("msg").textContent = T("tokenRequired"); return; }
920
- localStorage.setItem("agend_web_token", wt);
921
- const name = encodeURIComponent(current);
922
- try {
923
- const r = await fetch(`/api/profile/${name}`, {
924
- method: "POST",
925
- headers: { "Content-Type": "application/json", "X-Agend-Token": wt },
926
- body: JSON.stringify({ display_name: q("fDisplay").value, role: q("fRole").value, description: q("fDesc").value }),
927
- });
928
- if (!r.ok) { const e = await r.json().catch(() => ({})); throw new Error(e.error || ("HTTP " + r.status)); }
929
- const file = q("fAvatar").files[0];
930
- if (file) {
931
- const ar = await fetch(`/api/avatar/${name}`, { method: "POST", headers: { "Content-Type": file.type, "X-Agend-Token": wt }, body: file });
932
- if (!ar.ok) { const e = await ar.json().catch(() => ({})); throw new Error(TF("avatarFailed", { error: e.error || ar.status })); }
933
- }
934
- q("msg").className = "ok"; q("msg").textContent = T("saved");
935
- await loadRoster();
936
- setTimeout(exitEdit, 500);
937
- } catch (e) { q("msg").className = "err"; q("msg").textContent = e.message.startsWith(T("avatarFailed").split("{")[0]) ? e.message : TF("saveFailed", { error: e.message }); }
938
- }
939
-
940
- q("editBtn").onclick = enterEdit;
941
- q("cancelBtn").onclick = exitEdit;
942
- q("saveBtn").onclick = save;
943
- q("cardToggle").onclick = () => setCardExpanded(!q("card").classList.contains("expanded"), true);
944
-
945
- const setHelp = (open) => { q("helpPanel").classList.toggle("open", open); q("helpBackdrop").classList.toggle("open", open); };
946
- q("helpBtn").onclick = () => setHelp(!q("helpPanel").classList.contains("open"));
947
- q("helpClose").onclick = () => setHelp(false);
948
- q("helpBackdrop").onclick = () => setHelp(false);
949
- q("langBtn").onclick = () => { lang = lang === "en" ? "zh-TW" : "en"; localStorage.setItem("agend_lang", lang); applyI18n(); renderList(); };
950
-
951
- // โ”€โ”€ Filter input โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
952
- const setFilter = (value) => { filterQuery = value; renderList(); };
953
- q("filterInput").addEventListener("input", (e) => setFilter(e.target.value));
954
- q("filterInput").addEventListener("keydown", (e) => {
955
- if (e.key === "Escape" && filterQuery) { e.stopPropagation(); e.target.value = ""; setFilter(""); }
956
- });
957
- q("filterClear").onclick = () => { q("filterInput").value = ""; setFilter(""); q("filterInput").focus(); };
958
- // "/" focuses the filter, as in most list UIs โ€” never while typing elsewhere.
959
- window.addEventListener("keydown", (e) => {
960
- if (e.key !== "/" || e.ctrlKey || e.metaKey || e.altKey) return;
961
- const t = e.target;
962
- if (t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.isContentEditable)) return;
963
- e.preventDefault(); q("filterInput").focus();
964
- });
965
- q("densityBtn").onclick = cycleDensity;
966
- window.addEventListener("keydown", (e) => { if (e.key === "Escape") setHelp(false); });
967
-
968
- // โ”€โ”€ AI usage panel โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
969
- let usageData = null;
970
- let usageTimer = null;
971
- let usageOrder = [];
972
- try {
973
- const saved = JSON.parse(localStorage.getItem(USAGE_ORDER_KEY) || "[]");
974
- if (Array.isArray(saved)) usageOrder = saved.filter(v => typeof v === "string");
975
- } catch { /* ignore invalid browser-local state */ }
976
-
977
- function providerKey(provider) { return String(provider.id || provider.name || ""); }
978
- function orderedUsageProviders(providers) {
979
- const rank = new Map(usageOrder.map((id, i) => [id, i]));
980
- return providers.map((provider, index) => ({ provider, index })).sort((a, b) => {
981
- const ar = rank.has(providerKey(a.provider)) ? rank.get(providerKey(a.provider)) : Infinity;
982
- const br = rank.has(providerKey(b.provider)) ? rank.get(providerKey(b.provider)) : Infinity;
983
- return ar === br ? a.index - b.index : ar - br;
984
- }).map(entry => entry.provider);
985
- }
986
-
987
- function moveUsageProvider(id, delta) {
988
- if (!usageData || !Array.isArray(usageData.providers)) return;
989
- const ids = orderedUsageProviders(usageData.providers).map(providerKey);
990
- const from = ids.indexOf(id), to = from + delta;
991
- if (from < 0 || to < 0 || to >= ids.length) return;
992
- [ids[from], ids[to]] = [ids[to], ids[from]];
993
- usageOrder = ids;
994
- try { localStorage.setItem(USAGE_ORDER_KEY, JSON.stringify(usageOrder)); }
995
- catch { /* ordering still applies for this page even if storage is unavailable */ }
996
- renderUsage();
997
- }
998
-
999
- function usageResetText(iso) {
1000
- if (!iso) return "";
1001
- const ms = new Date(iso).getTime() - Date.now();
1002
- if (isNaN(ms)) return "";
1003
- if (ms <= 0) return T("usage.reset.soon");
1004
- return usageLocalized("", { key: "usage.reset.in", args: [usageDuration(ms)] });
1005
- }
1006
-
1007
- // #1244: "๐ŸŽซ Nearest expiry: 10/22 (in 15d 4h)" โ€” when the soonest reset ticket expires. An expiry, not a reset.
1008
- function usageExpiryText(iso) {
1009
- if (!iso) return "";
1010
- const at = new Date(iso), ms = at.getTime() - Date.now();
1011
- if (!(ms > 0)) return "";
1012
- return usageLocalized("", { key: "usage.ticket_expiry", args: [`${at.getMonth() + 1}/${at.getDate()}`, usageDuration(ms)] });
1013
- }
1014
-
1015
- function usageDuration(ms) {
1016
- const totalMinutes = Math.max(1, Math.ceil(ms / 60000));
1017
- const d = Math.floor(totalMinutes / 1440), h = Math.floor((totalMinutes % 1440) / 60), m = totalMinutes % 60;
1018
- const duration = totalMinutes >= 2880
1019
- ? usageLocalized("", { key: "usage.duration.days_hours", args: [d, h] })
1020
- : totalMinutes >= 60
1021
- ? usageLocalized("", { key: "usage.duration.hours_minutes", args: [Math.floor(totalMinutes / 60), m] })
1022
- : usageLocalized("", { key: "usage.duration.minutes", args: [totalMinutes] });
1023
- return duration;
1024
- }
1025
-
1026
- function usageLocalized(fallback, ref) {
1027
- if (!ref || typeof ref.key !== "string") return fallback == null ? "" : String(fallback);
1028
- const args = Array.isArray(ref.args) ? ref.args : [];
1029
- return args.reduce((text, value, index) => text.replaceAll(`{${index}}`, String(value)), T(ref.key));
1030
- }
1031
-
1032
- function usageMetricHtml(m) {
1033
- const label = usageLocalized(m.label, m.labelI18n);
1034
- const note = m.note ? usageLocalized(m.note, m.noteI18n) : "";
1035
- if (m.type === "percent") {
1036
- const pct = Math.min(100, Math.max(0, m.used ?? 0));
1037
- const cls = pct >= 90 ? "crit" : pct >= 70 ? "warn" : "";
1038
- const word = pct >= 90 ? T("usageNear") : pct >= 70 ? T("usageHigh") : "";
1039
- const sub = [note, usageResetText(m.resetsAt)].filter(Boolean).join(" ยท ");
1040
- return `<div class="u-metric"><div class="u-row"><span class="u-label">${esc(label)}</span>` +
1041
- (word ? `<span class="u-status ${cls}">${esc(word)}</span>` : "") +
1042
- `<span class="u-val u-pct ${cls}">${pct.toFixed(0)}<span class="u-unit">%</span></span></div>` +
1043
- `<div class="u-meter"><div class="u-fill ${cls}" style="width:${pct}%"></div></div>` +
1044
- (sub ? `<div class="u-sub">${esc(sub)}</div>` : "") + `</div>`;
1045
- }
1046
- if (m.type === "dollars") {
1047
- const val = m.limit ? `$${(m.used ?? 0).toFixed(2)} / $${m.limit.toFixed(2)}` : `$${(m.used ?? 0).toFixed(2)}`;
1048
- return `<div class="u-metric"><div class="u-row"><span class="u-label">${esc(label)}</span><span class="u-val">${esc(val)}</span></div>` +
1049
- (note ? `<div class="u-sub">${esc(note)}</div>` : "") + `</div>`;
1050
- }
1051
- const value = usageLocalized(m.value ?? "", m.valueI18n);
1052
- const unit = usageLocalized(m.unit ?? "", m.unitI18n);
1053
- const val = `${value} ${unit}`.trim();
1054
- const sub = [note, usageExpiryText(m.expiresAt)].filter(Boolean).join(" ยท ");
1055
- return `<div class="u-metric"><div class="u-row"><span class="u-label">${esc(label)}</span><span class="u-val">${esc(val)}</span></div>` +
1056
- (sub ? `<div class="u-sub">${esc(sub)}</div>` : "") + `</div>`;
1057
- }
1058
-
1059
- const USAGE_ICON = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" aria-hidden="true"><path d="M2.5 13.5h11"/><path d="M4.5 11V8"/><path d="M8 11V4.5"/><path d="M11.5 11V6.5"/></svg>';
1060
- function renderUsage() {
1061
- const box = q("usageContent");
1062
- if (!usageData || !Array.isArray(usageData.providers)) { box.innerHTML = `<h3>${USAGE_ICON}${T("usage.title")}</h3><div class="u-err">${T("usageFail")}</div>`; return; }
1063
- let html = `<h3>${USAGE_ICON}${T("usage.title")}</h3>`;
1064
- const providers = orderedUsageProviders(usageData.providers);
1065
- if (!providers.length) html += `<div class="u-err">${T("usage.empty_active")}</div>`;
1066
- for (let index = 0; index < providers.length; index++) {
1067
- const p = providers[index];
1068
- const key = providerKey(p);
1069
- html += `<div class="u-provider"><div class="u-head"><span class="u-name">${esc(p.name)}</span>` +
1070
- (p.plan ? `<span class="u-plan">${esc(p.plan)}</span>` : "") +
1071
- `<span class="u-order"><button class="u-move" data-provider="${esc(key)}" data-delta="-1" title="${T("usageMoveUp")}" aria-label="${T("usageMoveUp")}"${index === 0 ? " disabled" : ""}>โ†‘</button>` +
1072
- `<button class="u-move" data-provider="${esc(key)}" data-delta="1" title="${T("usageMoveDown")}" aria-label="${T("usageMoveDown")}"${index === providers.length - 1 ? " disabled" : ""}>โ†“</button></span></div>`;
1073
- if (p.status === "error") html += `<div class="u-err">${esc(usageLocalized(p.error || T("usage.error_fallback"), p.errorI18n))}</div>`;
1074
- else if (p.status === "no-credentials") {
1075
- html += `<div class="u-note">${esc(T("usage.not_logged_in"))}</div>`;
1076
- if (p.hint) html += `<div class="u-note">${esc(usageLocalized(p.hint, p.hintI18n))}</div>`;
1077
- }
1078
- else {
1079
- html += (p.metrics || []).map(usageMetricHtml).join("");
1080
- // A provider with nothing to meter (subscription plan) or one awaiting
1081
- // sign-in carries a hint instead of metrics. Without this the panel
1082
- // rendered an empty box, so those states looked broken.
1083
- if (p.hint) html += `<div class="u-note">${esc(usageLocalized(p.hint, p.hintI18n))}</div>`;
1084
- else if (!(p.metrics || []).length) html += `<div class="u-note">${esc(T("usage.no_data"))}</div>`;
1085
- }
1086
- html += `</div>`;
1087
- }
1088
- const at = new Date(usageData.fetchedAt).toLocaleTimeString();
1089
- html += `<div class="u-foot"><span>${T("usageUpdated")} ${esc(at)}</span><button id="usageRefresh">${T("usageRefresh")}</button></div>`;
1090
- box.innerHTML = html;
1091
- box.querySelectorAll(".u-move").forEach(button => {
1092
- button.onclick = () => moveUsageProvider(button.dataset.provider, Number(button.dataset.delta));
1093
- });
1094
- const rb = q("usageRefresh");
1095
- if (rb) rb.onclick = () => loadUsage(true);
1096
- }
1097
-
1098
- async function loadUsage(force) {
1099
- try {
1100
- const r = await fetch(`/api/ai-usage${force ? "?force=1" : ""}`);
1101
- if (r.status === 404) { q("usageBtn").style.display = "none"; return false; }
1102
- usageData = await r.json();
1103
- } catch { usageData = null; }
1104
- renderUsage();
1105
- return true;
1106
- }
1107
-
1108
- const setUsage = (open) => {
1109
- q("usagePanel").classList.toggle("open", open);
1110
- q("helpBackdrop").classList.toggle("open", open);
1111
- if (open) { setHelp(false); loadUsage(false); if (!usageTimer) usageTimer = setInterval(() => loadUsage(false), 60_000); }
1112
- else if (usageTimer) { clearInterval(usageTimer); usageTimer = null; }
1113
- };
1114
- q("usageBtn").onclick = () => setUsage(!q("usagePanel").classList.contains("open"));
1115
- q("usageClose").onclick = () => setUsage(false);
1116
- q("helpBackdrop").addEventListener("click", () => setUsage(false));
1117
- q("helpBtn").addEventListener("click", () => setUsage(false));
1118
- window.addEventListener("keydown", (e) => { if (e.key === "Escape") setUsage(false); });
1119
- q("langBtn").addEventListener("click", () => { if (usageData) renderUsage(); });
1120
- // Probe once at boot: shows the usage button only when the endpoint is enabled,
1121
- // and warms the server-side 5-minute cache so the first open is instant.
1122
- loadUsage(false).then(ok => { if (ok !== false) q("usageBtn").style.display = ""; });
1123
-
1124
- window.addEventListener("resize", scheduleFit);
1125
- // Catches container changes the window resize event doesn't see โ€” chiefly the
1126
- // profile card expanding or collapsing.
1127
- if (window.ResizeObserver) new ResizeObserver(scheduleFit).observe(q("term"));
1128
- applyI18n();
1129
- applyCardExpanded(cardExpanded);
1130
- restoreSidebarOrder();
1131
-
1132
- // โ”€โ”€ Boot โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1133
- loadRoster().then(() => { refreshPane(); });
1134
- poll = setInterval(refreshPane, 800);
1135
- setInterval(loadRoster, 5000); // refresh status dots / ctx%
1136
- })();
1137
- </script>
1138
- </body>
1139
- </html>