@songsid/agend 2.1.12-beta.9 โ 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.
- package/README.md +2 -2
- package/README.zh-TW.md +2 -2
- package/dist/auth/one-time-code.d.ts +19 -0
- package/dist/auth/one-time-code.js +49 -0
- package/dist/auth/one-time-code.js.map +1 -0
- package/dist/auth-api.d.ts +52 -0
- package/dist/auth-api.js +346 -0
- package/dist/auth-api.js.map +1 -0
- package/dist/backend/claude-code.d.ts +4 -0
- package/dist/backend/claude-code.js +97 -0
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/codex.js +59 -30
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/kiro-agent.d.ts +66 -0
- package/dist/backend/kiro-agent.js +302 -0
- package/dist/backend/kiro-agent.js.map +1 -0
- package/dist/backend/kiro-identity.d.ts +80 -0
- package/dist/backend/kiro-identity.js +537 -0
- package/dist/backend/kiro-identity.js.map +1 -0
- package/dist/backend/kiro-v2-store-worker.d.ts +1 -0
- package/dist/backend/kiro-v2-store-worker.js +87 -0
- package/dist/backend/kiro-v2-store-worker.js.map +1 -0
- package/dist/backend/kiro-v2-store.d.ts +54 -0
- package/dist/backend/kiro-v2-store.js +131 -0
- package/dist/backend/kiro-v2-store.js.map +1 -0
- package/dist/backend/kiro-v3-identity.d.ts +5 -0
- package/dist/backend/kiro-v3-identity.js +22 -0
- package/dist/backend/kiro-v3-identity.js.map +1 -1
- package/dist/backend/kiro.d.ts +83 -7
- package/dist/backend/kiro.js +418 -91
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/types.d.ts +45 -3
- package/dist/backend/types.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +1 -0
- package/dist/channel/adapters/discord.js +29 -10
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +1 -0
- package/dist/channel/adapters/telegram.js +13 -1
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/types.d.ts +9 -1
- package/dist/classic-channel-manager.d.ts +18 -10
- package/dist/classic-channel-manager.js +45 -18
- package/dist/classic-channel-manager.js.map +1 -1
- package/dist/cli.js +225 -127
- package/dist/cli.js.map +1 -1
- package/dist/command-table.d.ts +5 -3
- package/dist/command-table.js +29 -27
- package/dist/command-table.js.map +1 -1
- package/dist/config-validator.js +46 -0
- package/dist/config-validator.js.map +1 -1
- package/dist/config.js +12 -0
- package/dist/config.js.map +1 -1
- package/dist/connection-secrets.d.ts +6 -0
- package/dist/connection-secrets.js +4 -0
- package/dist/connection-secrets.js.map +1 -1
- package/dist/daemon.d.ts +89 -4
- package/dist/daemon.js +661 -77
- package/dist/daemon.js.map +1 -1
- package/dist/darwin-memory.d.ts +6 -1
- package/dist/darwin-memory.js +17 -6
- package/dist/darwin-memory.js.map +1 -1
- package/dist/delivery-consumed-watch.d.ts +85 -0
- package/dist/delivery-consumed-watch.js +244 -0
- package/dist/delivery-consumed-watch.js.map +1 -0
- package/dist/delivery-outbox.d.ts +61 -0
- package/dist/delivery-outbox.js +120 -2
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/delivery-reconciliation.d.ts +14 -5
- package/dist/delivery-reconciliation.js +27 -4
- package/dist/delivery-reconciliation.js.map +1 -1
- package/dist/delivery-reconciliation.test.js +35 -1
- package/dist/delivery-reconciliation.test.js.map +1 -1
- package/dist/event-loop-watch.d.ts +28 -0
- package/dist/event-loop-watch.js +75 -3
- package/dist/event-loop-watch.js.map +1 -1
- package/dist/fleet-context.d.ts +22 -2
- package/dist/fleet-level-config.d.ts +2 -2
- package/dist/fleet-level-config.js +6 -1
- package/dist/fleet-level-config.js.map +1 -1
- package/dist/fleet-manager.d.ts +293 -12
- package/dist/fleet-manager.js +2364 -449
- package/dist/fleet-manager.js.map +1 -1
- package/dist/host-memory.d.ts +4 -0
- package/dist/host-memory.js.map +1 -1
- package/dist/instance-config-impact.js +5 -0
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +10 -5
- package/dist/instance-lifecycle.js +162 -53
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/locale.js +150 -16
- package/dist/locale.js.map +1 -1
- package/dist/login-controller.d.ts +2 -0
- package/dist/login-controller.js +17 -5
- package/dist/login-controller.js.map +1 -1
- package/dist/memory-pressure.d.ts +7 -8
- package/dist/memory-pressure.js +76 -15
- package/dist/memory-pressure.js.map +1 -1
- package/dist/monotonic-budget.d.ts +2 -0
- package/dist/monotonic-budget.js +28 -0
- package/dist/monotonic-budget.js.map +1 -0
- package/dist/needs-you-hub.d.ts +114 -0
- package/dist/needs-you-hub.js +370 -0
- package/dist/needs-you-hub.js.map +1 -0
- package/dist/needs-you-live.d.ts +116 -0
- package/dist/needs-you-live.js +228 -0
- package/dist/needs-you-live.js.map +1 -0
- package/dist/needs-you.d.ts +132 -0
- package/dist/needs-you.js +164 -0
- package/dist/needs-you.js.map +1 -0
- package/dist/node-version-guard.d.ts +30 -0
- package/dist/node-version-guard.js +50 -0
- package/dist/node-version-guard.js.map +1 -0
- package/dist/operator-control-paths.d.ts +4 -0
- package/dist/operator-control-paths.js +24 -0
- package/dist/operator-control-paths.js.map +1 -0
- package/dist/outbound-handlers.d.ts +2 -0
- package/dist/outbound-handlers.js +9 -2
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +5 -5
- package/dist/profile-control.js +4 -20
- package/dist/profile-control.js.map +1 -1
- package/dist/public-web-gateway.d.ts +18 -0
- package/dist/public-web-gateway.js +141 -0
- package/dist/public-web-gateway.js.map +1 -0
- package/dist/public-web-link.d.ts +53 -0
- package/dist/public-web-link.js +175 -0
- package/dist/public-web-link.js.map +1 -0
- package/dist/quickstart-api.d.ts +8 -1
- package/dist/quickstart-api.js +86 -48
- package/dist/quickstart-api.js.map +1 -1
- package/dist/quickstart.d.ts +1 -0
- package/dist/quickstart.js +6 -2
- package/dist/quickstart.js.map +1 -1
- package/dist/scheduler/db.d.ts +7 -1
- package/dist/scheduler/db.js +38 -0
- package/dist/scheduler/db.js.map +1 -1
- package/dist/scheduler/index.d.ts +1 -1
- package/dist/scheduler/retry-label.d.ts +11 -0
- package/dist/scheduler/retry-label.js +24 -0
- package/dist/scheduler/retry-label.js.map +1 -0
- package/dist/scheduler/scheduler.d.ts +55 -2
- package/dist/scheduler/scheduler.js +220 -8
- package/dist/scheduler/scheduler.js.map +1 -1
- package/dist/scheduler/types.d.ts +22 -0
- package/dist/scheduler/types.js.map +1 -1
- package/dist/secret-store.d.ts +5 -0
- package/dist/secret-store.js +20 -0
- package/dist/secret-store.js.map +1 -1
- package/dist/service-activation.d.ts +110 -0
- package/dist/service-activation.js +302 -0
- package/dist/service-activation.js.map +1 -0
- package/dist/service-installer.d.ts +7 -0
- package/dist/service-installer.js +7 -4
- package/dist/service-installer.js.map +1 -1
- package/dist/service-restart-selection.d.ts +6 -5
- package/dist/service-restart-selection.js +6 -5
- package/dist/service-restart-selection.js.map +1 -1
- package/dist/settings-api.d.ts +10 -2
- package/dist/settings-api.js +175 -72
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-baseline.d.ts +20 -0
- package/dist/settings-baseline.js +104 -0
- package/dist/settings-baseline.js.map +1 -0
- package/dist/settings-change.d.ts +21 -0
- package/dist/settings-change.js +133 -0
- package/dist/settings-change.js.map +1 -0
- package/dist/settings-confirmation.d.ts +105 -0
- package/dist/settings-confirmation.js +232 -0
- package/dist/settings-confirmation.js.map +1 -0
- package/dist/settings-control.d.ts +41 -0
- package/dist/settings-control.js +294 -0
- package/dist/settings-control.js.map +1 -0
- package/dist/settings-effect.d.ts +24 -0
- package/dist/settings-effect.js +169 -0
- package/dist/settings-effect.js.map +1 -0
- package/dist/settings-http-confirmation.d.ts +29 -0
- package/dist/settings-http-confirmation.js +123 -0
- package/dist/settings-http-confirmation.js.map +1 -0
- package/dist/settings-instance-patch.d.ts +4 -0
- package/dist/settings-instance-patch.js +26 -0
- package/dist/settings-instance-patch.js.map +1 -0
- package/dist/settings-request-capability.d.ts +16 -0
- package/dist/settings-request-capability.js +32 -0
- package/dist/settings-request-capability.js.map +1 -0
- package/dist/settings-transaction.d.ts +65 -0
- package/dist/settings-transaction.js +223 -0
- package/dist/settings-transaction.js.map +1 -0
- package/dist/setup-auth.d.ts +10 -18
- package/dist/setup-auth.js +14 -38
- package/dist/setup-auth.js.map +1 -1
- package/dist/setup-form.d.ts +1 -1
- package/dist/setup-form.js +75 -18
- package/dist/setup-form.js.map +1 -1
- package/dist/setup-guide.d.ts +13 -0
- package/dist/setup-guide.js +25 -0
- package/dist/setup-guide.js.map +1 -0
- package/dist/setup-host.d.ts +16 -0
- package/dist/setup-host.js +197 -32
- package/dist/setup-host.js.map +1 -1
- package/dist/setup-wizard.js +3 -0
- package/dist/setup-wizard.js.map +1 -1
- package/dist/slash-authz.d.ts +17 -3
- package/dist/slash-authz.js +5 -0
- package/dist/slash-authz.js.map +1 -1
- package/dist/spawn-gate.js +3 -3
- package/dist/spawn-gate.js.map +1 -1
- package/dist/statusline-watcher.d.ts +5 -0
- package/dist/statusline-watcher.js +2 -0
- package/dist/statusline-watcher.js.map +1 -1
- package/dist/steer-capability.d.ts +6 -0
- package/dist/steer-capability.js +8 -0
- package/dist/steer-capability.js.map +1 -1
- package/dist/sync-work-attribution.d.ts +14 -0
- package/dist/sync-work-attribution.js +43 -8
- package/dist/sync-work-attribution.js.map +1 -1
- package/dist/tips.js +10 -5
- package/dist/tips.js.map +1 -1
- package/dist/tmux-control.d.ts +35 -5
- package/dist/tmux-control.js +291 -57
- package/dist/tmux-control.js.map +1 -1
- package/dist/tmux-manager.d.ts +20 -6
- package/dist/tmux-manager.js +90 -47
- package/dist/tmux-manager.js.map +1 -1
- package/dist/tmux-read.d.ts +59 -0
- package/dist/tmux-read.js +213 -0
- package/dist/tmux-read.js.map +1 -0
- package/dist/topic-commands.d.ts +21 -10
- package/dist/topic-commands.js +191 -58
- package/dist/topic-commands.js.map +1 -1
- package/dist/transcript-delta-reader.js +2 -1
- package/dist/transcript-delta-reader.js.map +1 -1
- package/dist/tunnel/cloudflared-install.d.ts +2 -0
- package/dist/tunnel/cloudflared-install.js +1 -1
- package/dist/tunnel/cloudflared-install.js.map +1 -1
- package/dist/tunnel/purpose-lane.d.ts +14 -0
- package/dist/tunnel/purpose-lane.js +43 -0
- package/dist/tunnel/purpose-lane.js.map +1 -0
- package/dist/turn-reply-guard.d.ts +2 -0
- package/dist/turn-reply-guard.js +10 -1
- package/dist/turn-reply-guard.js.map +1 -1
- package/dist/types.d.ts +33 -0
- package/dist/ui/app.html +18 -0
- package/dist/ui/chat-render.js +478 -0
- package/dist/ui/chat-store.js +289 -0
- package/dist/ui/chat-strings.js +69 -0
- package/dist/ui/chat-thread.js +209 -0
- package/dist/ui/chat-tour.js +116 -0
- package/dist/ui/fleet-org.js +142 -0
- package/dist/ui/panel-chat.js +418 -0
- package/dist/ui/panel-fleet.js +413 -0
- package/dist/ui/panel-needs.js +202 -0
- package/dist/ui/panel-settings.js +750 -0
- package/dist/ui/preview.js +266 -0
- package/dist/ui/settings-apply.js +239 -0
- package/dist/ui/settings-confirm.js +186 -0
- package/dist/ui/settings-dialogs.js +546 -0
- package/dist/ui/settings-model.js +277 -0
- package/dist/ui/settings-strings.js +652 -0
- package/dist/ui/settings-wizard.js +140 -0
- package/dist/ui/shared/agend-auth.js +93 -0
- package/dist/ui/shared/app-ctx.js +80 -0
- package/dist/ui/shared/app-html.js +9 -0
- package/dist/ui/shared/app-i18n.js +95 -0
- package/dist/ui/shared/app-nav.js +73 -0
- package/dist/ui/shared/app-needs.js +114 -0
- package/dist/ui/shared/app-route.js +73 -0
- package/dist/ui/shared/app-session.js +66 -0
- package/dist/ui/shared/app-shell.js +344 -0
- package/dist/ui/shared/app-store.js +51 -0
- package/dist/ui/shared/app-stream.js +201 -0
- package/dist/ui/shared/app.css +676 -0
- package/dist/ui/shared/app.js +88 -0
- package/dist/ui/shared/fonts/OFL.txt +92 -0
- package/dist/ui/shared/fonts/README.md +21 -0
- package/dist/ui/shared/fonts/inter.woff2 +0 -0
- package/dist/ui/shared/htm.module.js +1 -0
- package/dist/ui/shared/panel-view.js +655 -0
- package/dist/ui/shared/preact-hooks.module.js +2 -0
- package/dist/ui/shared/preact.module.js +2 -0
- package/dist/ui/shared/signin.js +130 -0
- package/dist/ui/shared/theme.js +29 -0
- package/dist/ui/shared/tokens.css +153 -0
- package/dist/ui/shared/ui-confirm.js +43 -0
- package/dist/ui/shared/ui-dialog.js +45 -0
- package/dist/ui/shared/ui-icons.js +61 -0
- package/dist/ui/shared/ui-menu.js +39 -0
- package/dist/ui/shared/ui-states.js +20 -0
- package/dist/ui/shared/ui-toast.js +18 -0
- package/dist/ui/shared/vendor/htm.LICENSE +202 -0
- package/dist/ui/shared/vendor/preact.LICENSE +21 -0
- package/dist/ui/shared/vendor/vendor.json +39 -0
- package/dist/ui/shared/view-strings.js +309 -0
- package/dist/ui/signin.html +51 -0
- package/dist/update-check.d.ts +6 -2
- package/dist/update-check.js +12 -3
- package/dist/update-check.js.map +1 -1
- package/dist/update-install.d.ts +71 -0
- package/dist/update-install.js +162 -0
- package/dist/update-install.js.map +1 -0
- package/dist/upgrade-notices.d.ts +17 -0
- package/dist/upgrade-notices.js +93 -0
- package/dist/upgrade-notices.js.map +1 -0
- package/dist/usage/statusline-usage.d.ts +2 -0
- package/dist/usage/statusline-usage.js +14 -10
- package/dist/usage/statusline-usage.js.map +1 -1
- package/dist/view-api.d.ts +6 -1
- package/dist/view-api.js +62 -51
- package/dist/view-api.js.map +1 -1
- package/dist/web-api.d.ts +71 -14
- package/dist/web-api.js +492 -77
- package/dist/web-api.js.map +1 -1
- package/dist/web-auth.d.ts +71 -27
- package/dist/web-auth.js +169 -69
- package/dist/web-auth.js.map +1 -1
- package/dist/web-body.d.ts +3 -0
- package/dist/web-body.js +55 -0
- package/dist/web-body.js.map +1 -0
- package/dist/web-channel-echo.d.ts +9 -0
- package/dist/web-channel-echo.js +55 -0
- package/dist/web-channel-echo.js.map +1 -0
- package/dist/web-chat-history.d.ts +125 -0
- package/dist/web-chat-history.js +184 -0
- package/dist/web-chat-history.js.map +1 -0
- package/dist/web-continuation.d.ts +7 -0
- package/dist/web-continuation.js +22 -0
- package/dist/web-continuation.js.map +1 -0
- package/dist/web-host-guard.d.ts +34 -0
- package/dist/web-host-guard.js +57 -1
- package/dist/web-host-guard.js.map +1 -1
- package/dist/web-login.d.ts +72 -0
- package/dist/web-login.js +132 -0
- package/dist/web-login.js.map +1 -0
- package/dist/web-org.d.ts +55 -0
- package/dist/web-org.js +67 -0
- package/dist/web-org.js.map +1 -0
- package/dist/web-preview.d.ts +77 -0
- package/dist/web-preview.js +240 -0
- package/dist/web-preview.js.map +1 -0
- package/dist/web-request-context.d.ts +10 -0
- package/dist/web-request-context.js +13 -0
- package/dist/web-request-context.js.map +1 -0
- package/dist/web-session.d.ts +167 -0
- package/dist/web-session.js +389 -0
- package/dist/web-session.js.map +1 -0
- package/dist/web-shell-routes.d.ts +59 -0
- package/dist/web-shell-routes.js +96 -0
- package/dist/web-shell-routes.js.map +1 -0
- package/dist/web-upload.d.ts +183 -0
- package/dist/web-upload.js +468 -0
- package/dist/web-upload.js.map +1 -0
- package/dist/website-tips-renderer.d.ts +2 -2
- package/dist/website-tips-renderer.js +3 -3
- package/dist/website-tips-renderer.js.map +1 -1
- package/package.json +8 -5
- package/scripts/preinstall-guard.cjs +36 -0
- package/dist/ui/dashboard.html +0 -774
- package/dist/ui/settings.html +0 -2165
- 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 => ({ "&":"&", "<":"<", ">":">" }[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>
|