@songsid/agend 2.3.0-beta.1 → 2.3.0-beta.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-cli-instructions.md +5 -2
- package/dist/agent-cli.js +43 -7
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.js +4 -0
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-file-read.d.ts +14 -6
- package/dist/agent-file-read.js +207 -19
- package/dist/agent-file-read.js.map +1 -1
- package/dist/agent-whats-new.d.ts +11 -0
- package/dist/agent-whats-new.js +29 -15
- package/dist/agent-whats-new.js.map +1 -1
- package/dist/auth-api.d.ts +30 -1
- package/dist/auth-api.js +45 -13
- package/dist/auth-api.js.map +1 -1
- package/dist/authority/authority-state.d.ts +38 -0
- package/dist/authority/authority-state.js +114 -0
- package/dist/authority/authority-state.js.map +1 -0
- package/dist/authority/session-authorizations.d.ts +43 -0
- package/dist/authority/session-authorizations.js +102 -0
- package/dist/authority/session-authorizations.js.map +1 -0
- package/dist/authority/web-authority.d.ts +120 -0
- package/dist/authority/web-authority.js +157 -0
- package/dist/authority/web-authority.js.map +1 -0
- package/dist/backend/claude-code.d.ts +2 -2
- package/dist/backend/claude-code.js +10 -5
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/types.d.ts +14 -0
- package/dist/backend/types.js +25 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/backend-availability.d.ts +120 -0
- package/dist/backend-availability.js +396 -0
- package/dist/backend-availability.js.map +1 -0
- package/dist/backend-command.d.ts +21 -3
- package/dist/backend-command.js +23 -4
- package/dist/backend-command.js.map +1 -1
- package/dist/bundled-skills.d.ts +14 -0
- package/dist/bundled-skills.js +29 -0
- package/dist/bundled-skills.js.map +1 -0
- package/dist/cache-analysis.d.ts +47 -1
- package/dist/cache-analysis.js +68 -8
- package/dist/cache-analysis.js.map +1 -1
- package/dist/cache-ledger.d.ts +16 -3
- package/dist/cache-ledger.js +33 -13
- package/dist/cache-ledger.js.map +1 -1
- package/dist/cache-report-tool.d.ts +17 -0
- package/dist/cache-report-tool.js +40 -0
- package/dist/cache-report-tool.js.map +1 -0
- package/dist/cache-service.d.ts +70 -3
- package/dist/cache-service.js +178 -17
- package/dist/cache-service.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +7 -0
- package/dist/channel/adapters/discord.js +35 -0
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/mcp-tools.js +22 -10
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/types.d.ts +6 -0
- package/dist/classic-channel-manager.d.ts +10 -5
- package/dist/classic-channel-manager.js +12 -9
- package/dist/classic-channel-manager.js.map +1 -1
- package/dist/cli.js +130 -31
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.js +103 -4
- package/dist/config-validator.js.map +1 -1
- package/dist/daemon.d.ts +7 -0
- package/dist/daemon.js +63 -16
- package/dist/daemon.js.map +1 -1
- package/dist/decision-list.d.ts +25 -0
- package/dist/decision-list.js +30 -0
- package/dist/decision-list.js.map +1 -0
- package/dist/delivery-outbox.d.ts +10 -0
- package/dist/delivery-outbox.js +12 -1
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/fleet-context.d.ts +10 -0
- package/dist/fleet-manager.d.ts +417 -11
- package/dist/fleet-manager.js +1387 -109
- package/dist/fleet-manager.js.map +1 -1
- package/dist/fleet-traffic.d.ts +74 -0
- package/dist/fleet-traffic.js +196 -0
- package/dist/fleet-traffic.js.map +1 -0
- package/dist/general-knowledge/skills/credential-profiles/SKILL.md +20 -5
- package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +6 -1
- package/dist/general-knowledge/skills/multi-channel/SKILL.md +3 -2
- package/dist/general-knowledge/skills/persona-emoji/SKILL.md +1 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +257 -23
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +478 -0
- package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +1 -1
- package/dist/human-delivery-receipts.d.ts +24 -1
- package/dist/human-delivery-receipts.js +64 -11
- package/dist/human-delivery-receipts.js.map +1 -1
- package/dist/instance-config-impact.js +3 -1
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-names.d.ts +4 -0
- package/dist/instance-names.js +12 -0
- package/dist/instance-names.js.map +1 -0
- package/dist/instructions.d.ts +12 -1
- package/dist/instructions.js +13 -2
- package/dist/instructions.js.map +1 -1
- package/dist/locale.js +100 -8
- package/dist/locale.js.map +1 -1
- package/dist/ls-attach-resolve.d.ts +95 -0
- package/dist/ls-attach-resolve.js +174 -0
- package/dist/ls-attach-resolve.js.map +1 -0
- package/dist/memory-hogs.d.ts +27 -0
- package/dist/memory-hogs.js +124 -0
- package/dist/memory-hogs.js.map +1 -0
- package/dist/outbound-handlers.d.ts +10 -0
- package/dist/outbound-handlers.js +122 -2
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +164 -4
- package/dist/outbound-schemas.js +99 -3
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/public-web-gateway.js +8 -2
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/public-web-link.d.ts +9 -0
- package/dist/public-web-link.js +8 -0
- package/dist/public-web-link.js.map +1 -1
- package/dist/quickstart-api.d.ts +10 -1
- package/dist/quickstart-api.js +12 -4
- package/dist/quickstart-api.js.map +1 -1
- package/dist/reply-buttons-controller.d.ts +4 -0
- package/dist/reply-buttons-controller.js +2 -2
- package/dist/reply-buttons-controller.js.map +1 -1
- package/dist/restart-progress.d.ts +65 -1
- package/dist/restart-progress.js +236 -10
- package/dist/restart-progress.js.map +1 -1
- package/dist/rollout-index.d.ts +5 -0
- package/dist/rollout-index.js +26 -5
- package/dist/rollout-index.js.map +1 -1
- package/dist/scheduler/scheduler.d.ts +12 -0
- package/dist/scheduler/scheduler.js +83 -10
- package/dist/scheduler/scheduler.js.map +1 -1
- package/dist/service-installer.d.ts +2 -1
- package/dist/service-installer.js +18 -3
- package/dist/service-installer.js.map +1 -1
- package/dist/settings-api.d.ts +19 -0
- package/dist/settings-api.js +137 -2
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +104 -0
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +17 -1
- package/dist/settings-effect.js.map +1 -1
- package/dist/tmux-manager.d.ts +5 -0
- package/dist/tmux-manager.js +16 -0
- package/dist/tmux-manager.js.map +1 -1
- package/dist/tool-permissions.d.ts +4 -6
- package/dist/tool-permissions.js +53 -5
- package/dist/tool-permissions.js.map +1 -1
- package/dist/topic-commands.d.ts +7 -1
- package/dist/topic-commands.js +18 -4
- package/dist/topic-commands.js.map +1 -1
- package/dist/types.d.ts +48 -1
- package/dist/ui/app-effects.js +77 -31
- package/dist/ui/chat-ambient.js +307 -0
- package/dist/ui/chat-effect-posters.js +8 -8
- package/dist/ui/chat-effects.js +101 -13
- package/dist/ui/chat-render.js +21 -3
- package/dist/ui/chat-strings.js +59 -6
- package/dist/ui/chat-thread.js +159 -21
- package/dist/ui/effect-trial-controller.js +13 -8
- package/dist/ui/effect-trial-panel.js +19 -16
- package/dist/ui/fleet-cache.js +23 -1
- package/dist/ui/fleet-canvas-layout.js +111 -0
- package/dist/ui/fleet-canvas.js +525 -0
- package/dist/ui/fleet-org.js +27 -12
- package/dist/ui/fleet-traffic.js +343 -0
- package/dist/ui/fleet-usage.js +401 -0
- package/dist/ui/panel-chat.js +154 -49
- package/dist/ui/panel-details.js +2 -1
- package/dist/ui/panel-fleet.js +43 -17
- package/dist/ui/panel-settings.js +26 -8
- package/dist/ui/preview.js +3 -2
- package/dist/ui/settings-appearance.js +42 -18
- package/dist/ui/settings-catalog.js +92 -0
- package/dist/ui/settings-dialogs.js +40 -12
- package/dist/ui/settings-effects.js +4 -3
- package/dist/ui/settings-guests.js +117 -0
- package/dist/ui/settings-learn.js +25 -0
- package/dist/ui/settings-model.js +15 -6
- package/dist/ui/settings-mods.js +23 -9
- package/dist/ui/settings-owner-effects.js +9 -9
- package/dist/ui/settings-strings.js +120 -42
- package/dist/ui/settings-web-authority.js +91 -0
- package/dist/ui/shared/agend-auth.js +1 -1
- package/dist/ui/shared/app-i18n.js +4 -2
- package/dist/ui/shared/app-prefs.js +5 -4
- package/dist/ui/shared/app-route.js +19 -1
- package/dist/ui/shared/app-session.js +2 -0
- package/dist/ui/shared/app-shell.js +92 -27
- package/dist/ui/shared/app-store.js +26 -1
- package/dist/ui/shared/app-stream.js +3 -1
- package/dist/ui/shared/app-themes.js +24 -6
- package/dist/ui/shared/app.css +365 -17
- package/dist/ui/shared/app.js +21 -14
- package/dist/ui/shared/card-strings.js +4 -0
- package/dist/ui/shared/effect-app-runtime.js +68 -19
- package/dist/ui/shared/effect-chat-runtime.js +16 -3
- package/dist/ui/shared/effect-frame.js +26 -7
- package/dist/ui/shared/effect-geometry.js +24 -9
- package/dist/ui/shared/effect-policy.js +21 -7
- package/dist/ui/shared/effect-profiler.js +31 -9
- package/dist/ui/shared/effect-registry.js +87 -48
- package/dist/ui/shared/effect-stock-snapshot.js +9 -0
- package/dist/ui/shared/effect-stock-trust.js +62 -10
- package/dist/ui/shared/effect-trial-shadow.js +62 -0
- package/dist/ui/shared/effect-trial-source.js +1 -1
- package/dist/ui/shared/effect-trial-stage.js +54 -31
- package/dist/ui/shared/effect-trial.css +40 -0
- package/dist/ui/shared/fx-motion.js +67 -0
- package/dist/ui/shared/identity-card.js +75 -4
- package/dist/ui/shared/identity-color.js +3 -3
- package/dist/ui/shared/instance-nav.js +194 -28
- package/dist/ui/shared/panel-view.js +24 -4
- package/dist/ui/shared/tokens.css +9 -3
- package/dist/ui/shared/ui-icons.js +1 -0
- package/dist/ui/shared/usage-dialog.js +22 -2
- package/dist/ui/shared/view-strings.js +68 -2
- package/dist/ui/shared/web-authorize.js +151 -0
- package/dist/ui/signin.html +1 -1
- package/dist/ui/web-terminal/terminal.css +4 -4
- package/dist/usage/providers.d.ts +28 -0
- package/dist/usage/providers.js +126 -36
- package/dist/usage/providers.js.map +1 -1
- package/dist/usage-report.d.ts +158 -0
- package/dist/usage-report.js +226 -0
- package/dist/usage-report.js.map +1 -0
- package/dist/usage-rollup.d.ts +51 -0
- package/dist/usage-rollup.js +168 -0
- package/dist/usage-rollup.js.map +1 -0
- package/dist/view-api.d.ts +39 -0
- package/dist/view-api.js +65 -4
- package/dist/view-api.js.map +1 -1
- package/dist/web-ambient.d.ts +72 -0
- package/dist/web-ambient.js +161 -0
- package/dist/web-ambient.js.map +1 -0
- package/dist/web-api.d.ts +69 -5
- package/dist/web-api.js +362 -45
- package/dist/web-api.js.map +1 -1
- package/dist/web-appearance-policy.d.ts +4 -6
- package/dist/web-appearance-policy.js +5 -12
- package/dist/web-appearance-policy.js.map +1 -1
- package/dist/web-appearance-tools.d.ts +3 -0
- package/dist/web-appearance-tools.js +22 -3
- package/dist/web-appearance-tools.js.map +1 -1
- package/dist/web-appearance-view.d.ts +27 -2
- package/dist/web-appearance-view.js +129 -14
- package/dist/web-appearance-view.js.map +1 -1
- package/dist/web-appearance.d.ts +17 -3
- package/dist/web-appearance.js +82 -7
- package/dist/web-appearance.js.map +1 -1
- package/dist/web-asset-parts.d.ts +5 -10
- package/dist/web-asset-parts.js +3 -3
- package/dist/web-asset-parts.js.map +1 -1
- package/dist/web-auth.js +3 -1
- package/dist/web-auth.js.map +1 -1
- package/dist/web-card.d.ts +14 -0
- package/dist/web-card.js +40 -6
- package/dist/web-card.js.map +1 -1
- package/dist/web-channel-echo.d.ts +7 -1
- package/dist/web-channel-echo.js +6 -2
- package/dist/web-channel-echo.js.map +1 -1
- package/dist/web-chat-history.d.ts +50 -0
- package/dist/web-chat-history.js +87 -5
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-commands.d.ts +1 -0
- package/dist/web-effect-bootstrap.d.ts +12 -0
- package/dist/web-effect-bootstrap.js +49 -0
- package/dist/web-effect-bootstrap.js.map +1 -0
- package/dist/web-effect-bundle.js +46 -1
- package/dist/web-effect-bundle.js.map +1 -1
- package/dist/web-effect-playback.d.ts +30 -0
- package/dist/web-effect-playback.js +71 -0
- package/dist/web-effect-playback.js.map +1 -0
- package/dist/web-effect-profiles.d.ts +4 -4
- package/dist/web-effect-service.d.ts +11 -4
- package/dist/web-effect-service.js +28 -5
- package/dist/web-effect-service.js.map +1 -1
- package/dist/web-effect-tools.d.ts +1 -0
- package/dist/web-effect-tools.js +8 -5
- package/dist/web-effect-tools.js.map +1 -1
- package/dist/web-effect-trials.d.ts +4 -4
- package/dist/web-effect-trials.js +6 -6
- package/dist/web-effect-trials.js.map +1 -1
- package/dist/web-effect-upload.d.ts +2 -4
- package/dist/web-effect-upload.js +3 -8
- package/dist/web-effect-upload.js.map +1 -1
- package/dist/web-effect-worker.d.ts +1 -1
- package/dist/web-effect-worker.js +2 -2
- package/dist/web-effect-worker.js.map +1 -1
- package/dist/web-effect.d.ts +14 -0
- package/dist/web-effect.js +21 -0
- package/dist/web-effect.js.map +1 -1
- package/dist/web-guest-access.d.ts +125 -0
- package/dist/web-guest-access.js +234 -0
- package/dist/web-guest-access.js.map +1 -0
- package/dist/web-guest-rooms.d.ts +82 -0
- package/dist/web-guest-rooms.js +135 -0
- package/dist/web-guest-rooms.js.map +1 -0
- package/dist/web-identity-color.d.ts +1 -1
- package/dist/web-identity-color.js +2 -2
- package/dist/web-identity-color.js.map +1 -1
- package/dist/web-identity-palette.d.ts +1 -1
- package/dist/web-identity-palette.js +2 -2
- package/dist/web-identity-palette.js.map +1 -1
- package/dist/web-image-meta.d.ts +13 -0
- package/dist/web-image-meta.js +62 -0
- package/dist/web-image-meta.js.map +1 -1
- package/dist/web-login.d.ts +47 -0
- package/dist/web-login.js +115 -0
- package/dist/web-login.js.map +1 -1
- package/dist/web-mod-agent.d.ts +1 -1
- package/dist/web-mod-agent.js +12 -8
- package/dist/web-mod-agent.js.map +1 -1
- package/dist/web-mod-bundle.d.ts +14 -1
- package/dist/web-mod-bundle.js +68 -3
- package/dist/web-mod-bundle.js.map +1 -1
- package/dist/web-mod-export.d.ts +9 -0
- package/dist/web-mod-export.js +21 -0
- package/dist/web-mod-export.js.map +1 -1
- package/dist/web-mod-store.d.ts +7 -1
- package/dist/web-mod-store.js +21 -7
- package/dist/web-mod-store.js.map +1 -1
- package/dist/web-prefs.d.ts +14 -0
- package/dist/web-prefs.js +24 -4
- package/dist/web-prefs.js.map +1 -1
- package/dist/web-preview.d.ts +6 -2
- package/dist/web-preview.js +4 -2
- package/dist/web-preview.js.map +1 -1
- package/dist/web-returning-device.d.ts +41 -0
- package/dist/web-returning-device.js +57 -5
- package/dist/web-returning-device.js.map +1 -1
- package/dist/web-session-authorization.d.ts +132 -0
- package/dist/web-session-authorization.js +193 -0
- package/dist/web-session-authorization.js.map +1 -0
- package/dist/web-session.d.ts +70 -10
- package/dist/web-session.js +137 -22
- package/dist/web-session.js.map +1 -1
- package/dist/web-settings-patch.d.ts +49 -2
- package/dist/web-settings-patch.js +145 -1
- package/dist/web-settings-patch.js.map +1 -1
- package/dist/web-settings-tools.d.ts +89 -0
- package/dist/web-settings-tools.js +270 -0
- package/dist/web-settings-tools.js.map +1 -0
- package/dist/web-shell-routes.d.ts +1 -1
- package/dist/web-shell-routes.js +1 -1
- package/dist/web-shell-routes.js.map +1 -1
- package/dist/web-theme-store.d.ts +39 -0
- package/dist/web-theme-store.js +122 -11
- package/dist/web-theme-store.js.map +1 -1
- package/dist/web-theme.d.ts +2 -0
- package/dist/web-theme.js +10 -1
- package/dist/web-theme.js.map +1 -1
- package/dist/web-upload.d.ts +13 -0
- package/dist/web-upload.js +39 -3
- package/dist/web-upload.js.map +1 -1
- package/dist/workflow-templates/default.md +1 -1
- package/package.json +1 -1
- package/templates/systemd.service.ejs +8 -0
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// Public (/assets/): an anonymous View reader gets the same list, fed by View's roster read instead of status frames.
|
|
10
10
|
import { html, useLayoutEffect, useRef, useState } from "./app-html.js";
|
|
11
11
|
import { t } from "./app-i18n.js";
|
|
12
|
-
import { appStore, createStore, useStore } from "./app-store.js";
|
|
12
|
+
import { appStore, createStore, guestPage, useStore } from "./app-store.js";
|
|
13
13
|
import { chatPath, detailsPath, logPath, viewPath } from "./app-route.js";
|
|
14
14
|
import { Icon } from "./ui-icons.js";
|
|
15
15
|
import { Skeleton } from "./ui-states.js";
|
|
@@ -30,12 +30,52 @@ const stored = (k) => { try { return localStorage.getItem(k); } catch { return n
|
|
|
30
30
|
const nameOf = (it) => (it && (it.name != null ? it.name : it.instance_name)) || "";
|
|
31
31
|
|
|
32
32
|
// ── Groups, labels, the filter ──
|
|
33
|
+
/** A team's group key: no tag can be it (a group name is never a control character), so a tag named like a team stays apart. */
|
|
34
|
+
export const TEAM_GROUP = "\u0001team:";
|
|
35
|
+
/**
|
|
36
|
+
* #2172: an agent in a team (fleet.yaml `teams.<name>.members`; `teams` on a status or roster row, in fleet.yaml's
|
|
37
|
+
* order) sits under its first team, whatever its tags say; one in no team under its first tag; a ClassicBot room under
|
|
38
|
+
* "Classic"; the rest under "Other".
|
|
39
|
+
*/
|
|
33
40
|
export function groupOf(it) {
|
|
34
41
|
const tags = Array.isArray(it.tags) ? it.tags : [];
|
|
35
42
|
if (tags.includes("classic")) return "Classic";
|
|
43
|
+
const teams = Array.isArray(it.teams) ? it.teams.filter((x) => typeof x === "string" && x) : [];
|
|
44
|
+
if (teams.length) return TEAM_GROUP + teams[0];
|
|
36
45
|
if (tags.length) return tags[0];
|
|
37
46
|
return "Other";
|
|
38
47
|
}
|
|
48
|
+
/** "team" for a team's group, "tag" for a tag's; null for Classic, Other and Favorites. */
|
|
49
|
+
export function groupKind(g) {
|
|
50
|
+
if (typeof g !== "string") return null;
|
|
51
|
+
if (g.startsWith(TEAM_GROUP)) return "team";
|
|
52
|
+
return g === "Other" || g === "Classic" || g.charCodeAt(0) < 32 ? null : "tag";
|
|
53
|
+
}
|
|
54
|
+
/** A group's heading: a team's name; the fallback group ("Other") in the page's language; a tag as it is. */
|
|
55
|
+
export function groupLabel(g) { return g === "Other" ? tn("groupOther") : groupKind(g) === "team" ? g.slice(TEAM_GROUP.length) : g; }
|
|
56
|
+
/** #2172: what a group is, said on its heading — a team's icon, or "#" before a tag — so the grouping can be read. */
|
|
57
|
+
export function GroupMark({ group }) {
|
|
58
|
+
const kind = groupKind(group);
|
|
59
|
+
if (!kind) return null;
|
|
60
|
+
const what = tn(kind === "team" ? "groupTeam" : "groupTag");
|
|
61
|
+
return kind === "team"
|
|
62
|
+
? html`<span class="v-group-kind team" title=${what}><${Icon} name="team" size=${13} /><span class="sr-only">${what}: </span></span>`
|
|
63
|
+
: html`<span class="v-group-kind tag" title=${what} aria-hidden="true">#</span>`;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* #2050: the model as a person reads it. The server's resolver writes English for a model it has not resolved yet
|
|
67
|
+
* ("default (detected when it starts)") and for a CLI's own default ("x (default)"); those are shown in the page's language.
|
|
68
|
+
*/
|
|
69
|
+
export function modelText(it, raw = it && it.model) {
|
|
70
|
+
if (!raw || typeof raw !== "string") return raw;
|
|
71
|
+
if (it.model_source === "unresolved") {
|
|
72
|
+
const why = /^default \((.*)\)$/.exec(raw)?.[1];
|
|
73
|
+
const reason = why === "detected when it starts" ? tn("modelWhyStart") : why === "this CLI does not report a default" ? tn("modelWhyCli") : why;
|
|
74
|
+
return reason ? tn("modelDefaultWhy", { reason }) : raw === "default" ? tn("modelDefault") : raw;
|
|
75
|
+
}
|
|
76
|
+
if (it.model_source === "cli-default") { const m = /^(.*) \(default\)$/.exec(raw); if (m) return tn("modelCliDefault", { model: m[1] }); }
|
|
77
|
+
return raw;
|
|
78
|
+
}
|
|
39
79
|
const BACKEND_LABELS = { "claude-code": "Claude Code", "kiro-cli": "Kiro CLI", codex: "Codex", grok: "Grok Build", antigravity: "Antigravity", opencode: "OpenCode" };
|
|
40
80
|
export function backendLabel(backend) { return BACKEND_LABELS[backend] || backend || "Unknown"; }
|
|
41
81
|
export const KNOWN_BACKENDS = new Set(Object.keys(BACKEND_LABELS));
|
|
@@ -93,8 +133,8 @@ export function instanceTooltip(it) {
|
|
|
93
133
|
if (it.display_name && it.display_name !== name) lines.push(it.display_name, `(${name})`);
|
|
94
134
|
else lines.push(name);
|
|
95
135
|
lines.push(line("tooltipBackend", backendLabel(it.backend)));
|
|
96
|
-
const src = (s) => (s === "instance" || s === "classic" ? "
|
|
97
|
-
lines.push(line("tooltipModel", (it
|
|
136
|
+
const src = (s) => (s === "instance" || s === "classic" ? tn("srcConfigured") : s === "fleet-default" ? tn("srcFleetDefault") : "");
|
|
137
|
+
lines.push(line("tooltipModel", (modelText(it) || tn("tooltipUnavailable")) + src(it.model_source)));
|
|
98
138
|
lines.push(line("tooltipStatus", status));
|
|
99
139
|
lines.push(line("tooltipContext", context));
|
|
100
140
|
if (it.effort) lines.push(line("tooltipEffort", it.effort + src(it.effort_source)));
|
|
@@ -108,6 +148,7 @@ const SIDEBAR_ORDER_KEY = "agend_view_sidebar_order"; // View's key since #999
|
|
|
108
148
|
const COLLAPSED_KEY = "agend_instance_collapsed";
|
|
109
149
|
const FILTER_KEY = "agend_instance_filter";
|
|
110
150
|
const FAVORITES_KEY = "agend_favorites"; // #1664
|
|
151
|
+
const SORT_KEY = "agend_nav_sort"; // #2179
|
|
111
152
|
/**
|
|
112
153
|
* A synced sidebar preference, as the JSON its reader parses: the value in force in memory (app-prefs.js — this
|
|
113
154
|
* page's own change, else the host's; a full or blocked storage never brings an old one back), else this browser's copy.
|
|
@@ -138,6 +179,13 @@ function readFilter() {
|
|
|
138
179
|
return { q: f && typeof f.q === "string" ? f.q : "", status: strings(f && f.status).filter((s) => STATUS_FACETS.includes(s)), cli: strings(f && f.cli) };
|
|
139
180
|
} catch { return { q: "", status: [], cli: [] }; }
|
|
140
181
|
}
|
|
182
|
+
/** #2179: how the list sorts — { mode: "custom" (the person's order; the default) | "recent", favs: "fixed" | "recent" }. */
|
|
183
|
+
function readSort() {
|
|
184
|
+
try {
|
|
185
|
+
const s = JSON.parse(inForce("nav.sort", SORT_KEY) || "{}");
|
|
186
|
+
return { mode: s && s.mode === "recent" ? "recent" : "custom", favs: s && s.favs === "recent" ? "recent" : "fixed" };
|
|
187
|
+
} catch { return { mode: "custom", favs: "fixed" }; }
|
|
188
|
+
}
|
|
141
189
|
function readFavorites() {
|
|
142
190
|
try { const a = JSON.parse(inForce("nav.favorites", FAVORITES_KEY) || "[]"); return Array.isArray(a) ? [...new Set(a.filter((x) => typeof x === "string"))] : []; } catch { return []; }
|
|
143
191
|
}
|
|
@@ -151,7 +199,9 @@ export function computeOrder(roster, order) {
|
|
|
151
199
|
if (ia !== ib) return ia - ib;
|
|
152
200
|
if (a === "Other") return 1;
|
|
153
201
|
if (b === "Other") return -1;
|
|
154
|
-
|
|
202
|
+
const ta = groupKind(a) === "team", tb = groupKind(b) === "team";
|
|
203
|
+
if (ta !== tb) return ta ? -1 : 1; // #2172: teams first, then tags
|
|
204
|
+
return groupLabel(a).localeCompare(groupLabel(b));
|
|
155
205
|
});
|
|
156
206
|
const instByGroup = new Map();
|
|
157
207
|
for (const [g, items] of groups) {
|
|
@@ -174,12 +224,18 @@ function moveIn(arr, moving, target, before) {
|
|
|
174
224
|
}
|
|
175
225
|
|
|
176
226
|
/** The list's state for the page's life: this browser's order, folded groups and filter (each also kept in storage). */
|
|
177
|
-
export const navStore = createStore({ order: readOrder(), collapsed: readCollapsed(), filter: readFilter(), favorites: readFavorites() });
|
|
227
|
+
export const navStore = createStore({ order: readOrder(), collapsed: readCollapsed(), filter: readFilter(), favorites: readFavorites(), sort: readSort() });
|
|
178
228
|
/** Read them again from this browser's storage — what a page load does (the store above is made that way). */
|
|
179
229
|
export function loadNavPrefs() {
|
|
180
|
-
navStore.set({ order: readOrder(), collapsed: readCollapsed(), filter: readFilter(), favorites: readFavorites() });
|
|
230
|
+
navStore.set({ order: readOrder(), collapsed: readCollapsed(), filter: readFilter(), favorites: readFavorites(), sort: readSort() });
|
|
181
231
|
namesStore.set({ primary: readNamesPrimary() });
|
|
182
232
|
}
|
|
233
|
+
/** #2179: change how the list sorts ({ mode?, favs? }): the same on every page and device. */
|
|
234
|
+
export function setNavSort(patch) {
|
|
235
|
+
const sort = { ...navStore.get().sort, ...patch };
|
|
236
|
+
navStore.set({ sort });
|
|
237
|
+
savePref("nav.sort", { mode: sort.mode, favs: sort.favs });
|
|
238
|
+
}
|
|
183
239
|
/** Change the filter ({ q?, status?, cli? }): the same on every page, and kept by this browser. */
|
|
184
240
|
export function setNavFilter(patch) {
|
|
185
241
|
const filter = { ...navStore.get().filter, ...patch };
|
|
@@ -188,7 +244,7 @@ export function setNavFilter(patch) {
|
|
|
188
244
|
}
|
|
189
245
|
function setCollapsed(collapsed) { navStore.set({ collapsed }); savePref("nav.collapsed", [...collapsed]); }
|
|
190
246
|
// Another device rearranged, folded or filtered: this page's list follows (its cache is already updated).
|
|
191
|
-
onPrefsChanged((keys) => { if (keys.has("nav.order") || keys.has("nav.collapsed") || keys.has("nav.filter") || keys.has("nav.favorites")) loadNavPrefs(); });
|
|
247
|
+
onPrefsChanged((keys) => { if (keys.has("nav.order") || keys.has("nav.collapsed") || keys.has("nav.filter") || keys.has("nav.favorites") || keys.has("nav.sort")) loadNavPrefs(); });
|
|
192
248
|
|
|
193
249
|
// ── #1664: favorites — starred instances, pinned at the top in the order they were starred (or moved) ──
|
|
194
250
|
export const MAX_FAVORITES = 50;
|
|
@@ -224,6 +280,8 @@ function writeFacetsOpen(open) { try { localStorage.setItem(FACETS_KEY, JSON.str
|
|
|
224
280
|
|
|
225
281
|
/** The Favorites group's key among the tag groups: no tag can be it (a group name is never a control character). */
|
|
226
282
|
const FAV_GROUP = "\u0000favorites";
|
|
283
|
+
/** #1936: the open instance, pinned on top while a filter is on. */
|
|
284
|
+
const PINNED_GROUP = "\u0000pinned";
|
|
227
285
|
function persistOrder(groupNames, instByGroup) {
|
|
228
286
|
const rows = [];
|
|
229
287
|
groupNames.forEach((g, i) => rows.push({ item_type: "group", item_name: g, sort_index: i, group_name: null }));
|
|
@@ -337,6 +395,71 @@ export function InstanceAvatar({ it, state, cli = false }) {
|
|
|
337
395
|
const statusText = (facet) => (facet === "needs" ? t("app.needsYou") : facet === "working" ? t("app.statusWorking") : facet === "idle" ? t("app.statusIdle")
|
|
338
396
|
: facet === "crashed" ? t("app.statusCrashed") : t("app.statusStopped"));
|
|
339
397
|
|
|
398
|
+
// ── #2179: when each instance last replied to a person (the status frame's last_reply) ──
|
|
399
|
+
/** A reply this recent (minutes) is highlighted; the highlight fades to nothing over this time. */
|
|
400
|
+
export const REPLY_FRESH_MIN = 10;
|
|
401
|
+
/** Minutes since the instance last replied to a person, or null (none kept, or no time on this row). */
|
|
402
|
+
export function replyAge(it, now = Date.now()) {
|
|
403
|
+
const at = it && typeof it.last_reply === "string" ? Date.parse(it.last_reply) : NaN;
|
|
404
|
+
return Number.isFinite(at) ? Math.max(0, (now - at) / 60000) : null;
|
|
405
|
+
}
|
|
406
|
+
/** "just now", "2 min ago", "3 h ago", "yesterday", "4 d ago" — in the page's language. */
|
|
407
|
+
export function replyText(mins) {
|
|
408
|
+
if (mins < 1) return tn("replyJustNow");
|
|
409
|
+
if (mins < 60) return tn("replyMinutes", { n: Math.floor(mins) });
|
|
410
|
+
if (mins < 1440) return tn("replyHours", { n: Math.floor(mins / 60) });
|
|
411
|
+
if (mins < 2880) return tn("replyYesterday");
|
|
412
|
+
return tn("replyDays", { n: Math.floor(mins / 1440) });
|
|
413
|
+
}
|
|
414
|
+
/** How strong the highlight is: 1 just after a reply, down to 0 at REPLY_FRESH_MIN minutes. */
|
|
415
|
+
export function replyFade(mins) { return mins == null || mins >= REPLY_FRESH_MIN ? 0 : 1 - mins / REPLY_FRESH_MIN; }
|
|
416
|
+
/** The highlight's strength on the row, through the CSSOM (style-src has no 'unsafe-inline'). */
|
|
417
|
+
function paintReply(el, fade) {
|
|
418
|
+
if (!el) return;
|
|
419
|
+
if (fade > 0) el.style.setProperty("--reply-a", fade.toFixed(3)); else el.style.removeProperty("--reply-a");
|
|
420
|
+
}
|
|
421
|
+
/** Most recent reply first; rows with none keep their order after them (a stable sort). */
|
|
422
|
+
function byRecentReply(age) {
|
|
423
|
+
return (a, b) => {
|
|
424
|
+
const x = age(a), y = age(b);
|
|
425
|
+
if (x == null || y == null) return x == null ? (y == null ? 0 : 1) : -1;
|
|
426
|
+
return x - y;
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
/** The group that holds every non-favorite row in "recent" mode: no tag or team can be it, and it is never saved. */
|
|
430
|
+
const RECENT_GROUP = "\u0000recent";
|
|
431
|
+
|
|
432
|
+
/** #2179: the sort button and its menu — how the list sorts, said on the button. */
|
|
433
|
+
function SortMenu({ sort }) {
|
|
434
|
+
const [open, setOpen] = useState(false);
|
|
435
|
+
const box = useRef(null);
|
|
436
|
+
useLayoutEffect(() => {
|
|
437
|
+
if (!open) return undefined;
|
|
438
|
+
const doc = box.current && box.current.ownerDocument;
|
|
439
|
+
if (!doc) return undefined;
|
|
440
|
+
const away = (e) => { if (box.current && !box.current.contains(e.target)) setOpen(false); };
|
|
441
|
+
const esc = (e) => { if (e.key === "Escape") { e.preventDefault(); setOpen(false); } };
|
|
442
|
+
doc.addEventListener("pointerdown", away, true);
|
|
443
|
+
doc.addEventListener("keydown", esc, true);
|
|
444
|
+
return () => { doc.removeEventListener("pointerdown", away, true); doc.removeEventListener("keydown", esc, true); };
|
|
445
|
+
}, [open]);
|
|
446
|
+
const recent = sort.mode === "recent";
|
|
447
|
+
const radio = (group, value, label, hint) => html`<label class="nav-sort-opt"><input type="radio" name=${`nav-sort-${group}`} value=${value}
|
|
448
|
+
checked=${sort[group] === value} onChange=${() => setNavSort({ [group]: value })} /><span>${label}${hint ? html`<span class="nav-sort-hint">${hint}</span>` : null}</span></label>`;
|
|
449
|
+
return html`<div class="nav-sort" ref=${box}>
|
|
450
|
+
<button type="button" class=${`chip-btn nav-sort-btn${recent ? " on" : ""}`} aria-haspopup="dialog" aria-expanded=${open ? "true" : "false"}
|
|
451
|
+
title=${tn("sortTitle")} onClick=${() => setOpen(!open)}><${Icon} name="sort" size=${12} /><span>${tn(recent ? "sortRecent" : "sortCustom")}</span></button>
|
|
452
|
+
${open ? html`<div class="nav-sort-menu" role="dialog" aria-label=${tn("sortTitle")}>
|
|
453
|
+
<fieldset><legend>${tn("sortTitle")}</legend>
|
|
454
|
+
${radio("mode", "custom", tn("sortCustom"), tn("sortCustomHint"))}
|
|
455
|
+
${radio("mode", "recent", tn("sortRecent"), tn("sortRecentHint"))}</fieldset>
|
|
456
|
+
<fieldset><legend>${tn("sortFavs")}</legend>
|
|
457
|
+
${radio("favs", "fixed", tn("sortFavsFixed"), "")}
|
|
458
|
+
${radio("favs", "recent", tn("sortFavsRecent"), "")}</fieldset>
|
|
459
|
+
</div>` : null}
|
|
460
|
+
</div>`;
|
|
461
|
+
}
|
|
462
|
+
|
|
340
463
|
/**
|
|
341
464
|
* The sidebar's instance list. `route`: the page's (which instance is open, and which view a row opens). `items`: the
|
|
342
465
|
* rows (status frames' instances, or View's roster for an anonymous reader); `loaded` false shows a skeleton.
|
|
@@ -350,19 +473,21 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
|
|
|
350
473
|
const filterRef = useRef(null);
|
|
351
474
|
const list = useRef(null);
|
|
352
475
|
const drag = useRef(null);
|
|
476
|
+
// #2179: the reply times and their highlight move on by themselves — the list is drawn again every 30 s.
|
|
477
|
+
const [, setTick] = useState(0);
|
|
478
|
+
useLayoutEffect(() => { const id = setInterval(() => setTick((n) => n + 1), 30000); return () => clearInterval(id); }, []);
|
|
353
479
|
// #1700: which facet groups are open — this device's (screen layout, not a synced preference), read when the list mounts.
|
|
354
480
|
const [facetOpen, setFacetOpen] = useState(readFacetsOpen);
|
|
355
481
|
const toggleFacet = (key) => setFacetOpen((cur) => { const next = { ...cur, [key]: !cur[key] }; writeFacetsOpen(next); return next; });
|
|
356
482
|
// #1664: "☆ Edit" — the stars on every row (on a phone the only way to them) and ↑ ↓ on the favorites — only on the
|
|
357
483
|
// full list, as drag: in a filtered one the row above may not be the one above in the list (#1677 review).
|
|
358
484
|
const [editing, setEditing] = useState(false);
|
|
359
|
-
// Where a row goes: the view the page is in (#1523 §3.1). View → View; Details,
|
|
360
|
-
//
|
|
485
|
+
// Where a row goes: the view the page is in (#1523 §3.1). View → View; Details → Details; everywhere else, a Fleet tab
|
|
486
|
+
// included (#1917: the user's call, replacing Q4 = A), → Chat. The anonymous reader only ever has View.
|
|
361
487
|
const rowPath = viewOnly || (route && route.panel === "view") ? viewPath
|
|
362
488
|
: route && route.panel === "log" ? logPath // #1218: Log → Log
|
|
363
|
-
: route &&
|
|
489
|
+
: route && route.panel === "details" ? detailsPath : chatPath;
|
|
364
490
|
const active = route && (route.panel === "chat" || route.panel === "view" || route.panel === "log" || route.panel === "details") ? route.instance : null;
|
|
365
|
-
useLayoutEffect(() => { keepActiveInView(list.current); }, [active, items.length, loaded]);
|
|
366
491
|
const own = (map, name) => Object.prototype.hasOwnProperty.call(map, name);
|
|
367
492
|
const byName = new Map(items.map((it) => [nameOf(it), it]));
|
|
368
493
|
const facetOf = (name) => statusFacet(byName.get(name), own(exec, name) ? exec[name] : null, own(awaiting, name) ? awaiting[name] : null);
|
|
@@ -374,18 +499,44 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
|
|
|
374
499
|
const favs = nav.favorites.filter((n) => known.has(n));
|
|
375
500
|
const favSet = new Set(favs);
|
|
376
501
|
const canStar = !viewOnly && prefsWritable();
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
502
|
+
// #1920: a guest's list is to pick from — no reordering and no filters (one this device kept would hide rows with no
|
|
503
|
+
// way to clear it).
|
|
504
|
+
const guest = guestPage();
|
|
505
|
+
// #2179: when each row last replied (a guest's list has no times). "recent" mode (never a guest's): the favorites keep
|
|
506
|
+
// their order or are sorted too, and every other row is one list, the last reply first. "custom" (the default) is the
|
|
507
|
+
// person's own order — a reply never moves a row.
|
|
508
|
+
const now = Date.now();
|
|
509
|
+
// The anonymous View reader (viewOnly) has no reply times (/api/profiles carries none) and no way to change the sort:
|
|
510
|
+
// its list is always Custom, whatever this browser kept from a signed-in page (#2181 review).
|
|
511
|
+
const ageOf = (n) => (guest || viewOnly ? null : replyAge(byName.get(n), now));
|
|
512
|
+
const recent = !guest && !viewOnly && nav.sort.mode === "recent";
|
|
513
|
+
const favsSorted = recent && nav.sort.favs === "recent";
|
|
514
|
+
let shownGroups, shownInsts;
|
|
515
|
+
if (recent) {
|
|
516
|
+
const sortRecent = byRecentReply(ageOf);
|
|
517
|
+
const rest = groupNames.flatMap((g) => instByGroup.get(g) || []).filter((n) => !favSet.has(n)).sort(sortRecent);
|
|
518
|
+
shownGroups = [...(favs.length ? [FAV_GROUP] : []), ...(rest.length ? [RECENT_GROUP] : [])];
|
|
519
|
+
shownInsts = new Map([[FAV_GROUP, favsSorted ? [...favs].sort(sortRecent) : favs], [RECENT_GROUP, rest]]);
|
|
520
|
+
} else {
|
|
521
|
+
shownGroups = favs.length ? [FAV_GROUP, ...groupNames] : groupNames;
|
|
522
|
+
shownInsts = new Map([...instByGroup].map(([g, names]) => [g, names.filter((n) => !favSet.has(n))]));
|
|
523
|
+
if (favs.length) shownInsts.set(FAV_GROUP, favs);
|
|
524
|
+
}
|
|
525
|
+
const f = guest ? { q: "", status: [], cli: [] } : nav.filter;
|
|
381
526
|
const shown = filterSidebar(shownGroups, shownInsts, byName, f.q, backendLabel, { status: f.status, cli: f.cli, statusOf: facetOf });
|
|
527
|
+
// #1936: while a filter is on, the instance you have open stays on top, pinned and marked "Current", whether or not
|
|
528
|
+
// it matches; a match is not repeated below. "N / M" still counts the filter's matches (the pinned row counts only
|
|
529
|
+
// when it is one). Clearing the filter puts it back in its place, and the list scrolls to it again.
|
|
530
|
+
const pinned = shown.active && active && byName.has(active) ? active : null;
|
|
531
|
+
const listed = pinned ? shown.groups.map((x) => ({ ...x, names: x.names.filter((n) => n !== pinned) })).filter((x) => x.names.length) : shown.groups;
|
|
532
|
+
useLayoutEffect(() => { keepActiveInView(list.current); }, [active, items.length, loaded, shown.active]);
|
|
382
533
|
const backends = [...new Set(items.map((it) => it.backend).filter((b) => typeof b === "string" && b))].sort((a, b) => backendLabel(a).localeCompare(backendLabel(b)));
|
|
383
534
|
const toggle = (g) => { const c = new Set(navStore.get().collapsed); if (c.has(g)) c.delete(g); else c.add(g); setCollapsed(c); };
|
|
384
535
|
// From the store, not this render's copy: two clicks before the next render must both count.
|
|
385
536
|
const flip = (key, value) => { const cur = new Set(navStore.get().filter[key]); if (cur.has(value)) cur.delete(value); else cur.add(value); setNavFilter({ [key]: [...cur] }); };
|
|
386
537
|
// Drag to reorder: only the full list (a filtered subset would be ambiguous), groups among groups, instances within
|
|
387
538
|
// their group.
|
|
388
|
-
const dragProps = (item) => shown.active ? {} : {
|
|
539
|
+
const dragProps = (item) => shown.active || guest || recent ? {} : { // #2179: not while sorted by reply
|
|
389
540
|
draggable: true,
|
|
390
541
|
onDragStart: (e) => { drag.current = item; e.currentTarget.classList.add("dragging"); try { e.dataTransfer.effectAllowed = "move"; e.dataTransfer.setData("text/plain", item.name); } catch { /* Firefox needs data */ } },
|
|
391
542
|
onDragEnd: (e) => { e.currentTarget.classList.remove("dragging"); drag.current = null; },
|
|
@@ -440,18 +591,24 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
|
|
|
440
591
|
// #1958: its identity colour — the ring, a left bar and/or the name, as resolved; painted through the CSSOM.
|
|
441
592
|
const id = identityOf(it);
|
|
442
593
|
const full = !starred && favs.length >= MAX_FAVORITES;
|
|
443
|
-
|
|
594
|
+
// #2179 (B5, the user's pick): the % (and the ★ beside the row) on the name's line; the time of the last reply at the
|
|
595
|
+
// end of the second line, highlighted for REPLY_FRESH_MIN minutes, fading by itself (a reply under a minute pulses once).
|
|
596
|
+
const mins = ageOf(name), fade = replyFade(mins);
|
|
597
|
+
const two = !!secondary || mins != null;
|
|
598
|
+
return html`<li key=${name} class=${`v-li${two ? " two-line" : ""}`}><a class=${`inst v-inst${on ? " active" : ""}${fade > 0 ? " replied" : ""}${mins != null && mins < 1 ? " just" : ""} ${identityClasses(id)}`}
|
|
599
|
+
ref=${(el) => { paintIdentity(el, id); paintReply(el, fade); }} href=${rowPath(name)} title=${instanceTooltip(it)}
|
|
444
600
|
aria-current=${on ? "page" : undefined} onClick=${onPick} ...${fav ? {} : dragProps({ type: "instance", name, group: g })}>
|
|
445
601
|
<${InstanceAvatar} it=${it} state=${dotState(it, own(exec, name) ? exec[name] : null, waiting)} cli=${true} />
|
|
446
|
-
<span class="inst-text"><span class="inst-name" title=${label}>${label}</span>
|
|
447
|
-
|
|
602
|
+
<span class="inst-text"><span class="inst-line"><span class="inst-name" title=${label}>${label}</span>
|
|
603
|
+
<span class="v-meta">${it.context_pct != null ? `${Math.round(it.context_pct)}%` : ""}</span></span>
|
|
604
|
+
${two ? html`<span class="inst-line inst-line2"><span class="inst-alt" title=${secondary || undefined}>${secondary || ""}</span>
|
|
605
|
+
${mins != null ? html`<span class=${`inst-when${fade > 0 ? " fresh" : ""}`} title=${tn("replyTitle", { time: new Date(Date.parse(it.last_reply)).toLocaleString() })}>${replyText(mins)}</span>` : null}</span>` : null}
|
|
448
606
|
${facet === "needs" ? html`<span class="inst-sub"><span class="badge-await" title=${waiting || t("app.approxNote")}>${t("app.needsYou")}</span></span>` : null}
|
|
449
607
|
<span class="sr-only">${tn("tooltipBackend", { value: backendLabel(it.backend) })}</span></span>
|
|
450
|
-
<span class="v-meta">${it.context_pct != null ? `${Math.round(it.context_pct)}%` : ""}</span>
|
|
451
608
|
</a>${fav && editing && canStar ? html`<button type="button" class="icon-btn inst-move" aria-label=${tn("favUp", { name: label })}
|
|
452
|
-
title=${shown.active ? tn("favFiltered") : tn("favUp", { name: label })} disabled=${shown.active || i === 0} onClick=${() => { if (!shown.active) moveFavorite(name, -1, known); }}><${Icon} name="up" size=${14} /></button><button type="button" class="icon-btn inst-move"
|
|
453
|
-
aria-label=${tn("favDown", { name: label })} title=${shown.active ? tn("favFiltered") : tn("favDown", { name: label })} disabled=${shown.active || i === count - 1}
|
|
454
|
-
onClick=${() => { if (!shown.active) moveFavorite(name, 1, known); }}><${Icon} name="down" size=${14} /></button>` : null}${canStar
|
|
609
|
+
title=${shown.active ? tn("favFiltered") : favsSorted ? tn("favSorted") : tn("favUp", { name: label })} disabled=${shown.active || favsSorted || i === 0} onClick=${() => { if (!shown.active && !favsSorted) moveFavorite(name, -1, known); }}><${Icon} name="up" size=${14} /></button><button type="button" class="icon-btn inst-move"
|
|
610
|
+
aria-label=${tn("favDown", { name: label })} title=${shown.active ? tn("favFiltered") : favsSorted ? tn("favSorted") : tn("favDown", { name: label })} disabled=${shown.active || favsSorted || i === count - 1}
|
|
611
|
+
onClick=${() => { if (!shown.active && !favsSorted) moveFavorite(name, 1, known); }}><${Icon} name="down" size=${14} /></button>` : null}${canStar
|
|
455
612
|
? html`<button type="button" class=${`icon-btn inst-star${starred ? " on" : ""}`} aria-pressed=${starred ? "true" : "false"} disabled=${full}
|
|
456
613
|
aria-label=${tn(starred ? "favRemove" : "favAdd", { name: label })} title=${full ? tn("favFull", { max: MAX_FAVORITES }) : tn(starred ? "favRemove" : "favAdd", { name: label })}
|
|
457
614
|
onClick=${() => toggleFavorite(name, known)}><${Icon} name="star" size=${14} /></button>` : null}</li>`;
|
|
@@ -460,10 +617,19 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
|
|
|
460
617
|
return html`<div class=${`view-roster inst-nav${editing && canStar ? " fav-editing" : ""}`}>
|
|
461
618
|
<div data-trust="navigation" class="side-section" id="instanceList" ref=${list}>
|
|
462
619
|
<div class="inst-head"><h2 class="side-label">${tn("instancesLabel")}</h2>
|
|
620
|
+
${canStar && items.length && !guest ? html`<${SortMenu} sort=${nav.sort} />`
|
|
621
|
+
: recent ? html`<span class="chip-btn nav-sort-btn on nav-sort-static"><${Icon} name="sort" size=${12} /><span>${tn("sortRecent")}</span></span>` : null}
|
|
463
622
|
${canStar && items.length ? html`<button type="button" class="chip-btn fav-edit" aria-pressed=${editing ? "true" : "false"} onClick=${() => setEditing(!editing)}>
|
|
464
623
|
${editing ? tn("favDone") : html`<${Icon} name="star" size=${12} />${tn("favEdit")}`}</button>` : null}</div>
|
|
465
624
|
${!loaded ? html`<${Skeleton} lines=${4} />` : !items.length ? html`<p class="side-empty">${t("app.noInstances")}</p>` : null}
|
|
466
|
-
${
|
|
625
|
+
${pinned ? html`<div key=${PINNED_GROUP} class="v-group v-pinned" role="group" aria-label=${tn("pinnedLabel")}>
|
|
626
|
+
<div class="v-group-head pinned-head"><span class="grow">${tn("pinnedCurrent")}</span></div>
|
|
627
|
+
<ul class="inst-list">${row(pinned, PINNED_GROUP, 0, 1)}</ul></div>` : null}
|
|
628
|
+
${listed.map(({ group: g, names }) => {
|
|
629
|
+
// #2179: in "recent" mode every other row is one list — open, not dragged, not saved as a group.
|
|
630
|
+
if (g === RECENT_GROUP) return html`<div key=${g} class="v-group v-recent" role="group" aria-label=${tn("recentOthers")}>
|
|
631
|
+
<div class="v-group-head recent-head"><${Icon} name="clock" size=${14} /><span class="grow">${tn("recentOthers")}</span><span class="count">${names.length}</span></div>
|
|
632
|
+
<ul class="inst-list">${names.map((name, i) => row(name, g, i, names.length))}</ul></div>`;
|
|
467
633
|
// #1664: Favorites — always open, not dragged (↑ ↓ in Edit), named in the page's language.
|
|
468
634
|
if (g === FAV_GROUP) return html`<div key=${g} class="v-group v-fav" role="group" aria-label=${tn("favorites")}>
|
|
469
635
|
<div class="v-group-head fav-head"><${Icon} name="star" size=${14} cls="fav-mark" /><span class="grow">${tn("favorites")}</span><span class="count">${names.length}</span></div>
|
|
@@ -471,14 +637,14 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
|
|
|
471
637
|
const folded = nav.collapsed.has(g);
|
|
472
638
|
return html`<div key=${g} class="v-group">
|
|
473
639
|
<button type="button" class="v-group-head" aria-expanded=${folded ? "false" : "true"} onClick=${() => toggle(g)}
|
|
474
|
-
title=${tn(folded ? "expandGroup" : "collapseGroup", { group: g })} ...${dragProps({ type: "group", name: g })}>
|
|
475
|
-
<${Icon} name="chevron" size=${14} cls=${folded ? "caret folded" : "caret"} /><span class="grow">${g}</span><span class="count">${names.length}</span></button>
|
|
640
|
+
title=${tn(folded ? "expandGroup" : "collapseGroup", { group: groupLabel(g) })} ...${dragProps({ type: "group", name: g })}>
|
|
641
|
+
<${Icon} name="chevron" size=${14} cls=${folded ? "caret folded" : "caret"} /><${GroupMark} group=${g} /><span class="grow">${groupLabel(g)}</span><span class="count">${names.length}</span></button>
|
|
476
642
|
${folded ? null : html`<ul class="inst-list">${names.map((name, i) => row(name, g, i, names.length))}</ul>`}
|
|
477
643
|
</div>`;
|
|
478
644
|
})}
|
|
479
645
|
${loaded && shown.active && !shown.groups.length ? html`<p class="side-empty">${tn("filterNone")}</p>` : null}
|
|
480
646
|
</div>
|
|
481
|
-
|
|
647
|
+
${guest ? null : html`<div class="v-filter">
|
|
482
648
|
<label class="v-filter-box"><${Icon} name="search" size=${14} /><span class="sr-only">${tn("filterPh")}</span>
|
|
483
649
|
<input ref=${filterRef} id="filterInput" type="text" autocomplete="off" spellcheck=${false} placeholder=${tn("filterPh")} value=${f.q}
|
|
484
650
|
onInput=${(e) => setNavFilter({ q: e.target.value })}
|
|
@@ -494,7 +660,7 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
|
|
|
494
660
|
</div>
|
|
495
661
|
<p class="note nav-shown" aria-live="polite">${shown.shown} / ${shown.total}${shown.active ? ` ${tn("filterShown")}` : ""}</p>
|
|
496
662
|
</div>
|
|
497
|
-
</div
|
|
663
|
+
</div>`}
|
|
498
664
|
</div>`;
|
|
499
665
|
}
|
|
500
666
|
|
|
@@ -16,7 +16,7 @@ import { HeaderTools, VIEW_SCALE, textSizeStore } from "./header-tools.js";
|
|
|
16
16
|
import { readStream } from "./read-stream.js";
|
|
17
17
|
// The usage dialog's metric row lives with the dialog now (usage-dialog.js); re-exported for what imported it here.
|
|
18
18
|
export { UsageMetric } from "./usage-dialog.js";
|
|
19
|
-
import { backendLabel, STATUS_KEYS } from "./instance-nav.js";
|
|
19
|
+
import { backendLabel, modelText, STATUS_KEYS } from "./instance-nav.js";
|
|
20
20
|
import { viewStore } from "./view-roster-store.js";
|
|
21
21
|
|
|
22
22
|
/** #1580: the app's stream says the fleet is not answering (a restart): the View's polls skip their turn until it is. */
|
|
@@ -27,7 +27,7 @@ import { Dialog } from "./ui-dialog.js";
|
|
|
27
27
|
import { Empty, ErrorState, Skeleton } from "./ui-states.js";
|
|
28
28
|
import { Icon } from "./ui-icons.js";
|
|
29
29
|
import "./view-strings.js";
|
|
30
|
-
import { checkCard, fitPicture, IdentityCard, useCard } from "./identity-card.js";
|
|
30
|
+
import { BannerFocusPreview, checkCard, fitPicture, IdentityCard, useBannerPreview, useCard } from "./identity-card.js";
|
|
31
31
|
import { confirmDialog } from "./ui-confirm.js";
|
|
32
32
|
import { openColorDialog } from "./identity-color.js";
|
|
33
33
|
|
|
@@ -305,7 +305,7 @@ function Card({ it, viewOnly, onEdit }) {
|
|
|
305
305
|
<div class="v-avatar">${it.has_avatar ? html`<img ref=${avatar} alt="" />` : html`<span class="v-initial" aria-hidden="true">${initial}</span>`}</div>
|
|
306
306
|
<div class="v-card-body">
|
|
307
307
|
<div class="v-card-line"><strong>${it.display_name || it.instance_name}</strong>${it.role ? html`<span class="v-role">${it.role}</span>` : null}</div>
|
|
308
|
-
<div class="v-card-meta">${it.instance_name} · ${backendLabel(it.backend)}${it.model ? ` · ${it
|
|
308
|
+
<div class="v-card-meta">${it.instance_name} · ${backendLabel(it.backend)}${it.model ? ` · ${modelText(it)}` : ""}${ctx}</div>
|
|
309
309
|
${open && it.description ? html`<p class="v-card-desc">${it.description}</p>` : null}
|
|
310
310
|
</div>
|
|
311
311
|
<button type="button" class="icon-btn" onClick=${toggle} aria-expanded=${open ? "true" : "false"} aria-label=${tn(open ? "collapseProfile" : "expandProfile")} title=${tn(open ? "collapseProfile" : "expandProfile")}>
|
|
@@ -332,6 +332,11 @@ function EditProfile({ it, onClose }) {
|
|
|
332
332
|
const [removeCard, setRemoveCard] = useState({ portrait: false, banner: false });
|
|
333
333
|
const [clearCard, setClearCard] = useState(false);
|
|
334
334
|
const pics = { portrait: useRef(null), banner: useRef(null) };
|
|
335
|
+
// #2086: the banner's focus — this person's (null: follow the agent's, else the centre), and the picture to preview:
|
|
336
|
+
// a newly chosen file, else the banner drawn now.
|
|
337
|
+
// `agent`: the agent's own focus (what "Follow the agent's" shows; else the centre).
|
|
338
|
+
const [focus, setFocus] = useState({ value: 50, mine: null, start: null, agent: null });
|
|
339
|
+
const [bannerPreview, preview] = useBannerPreview();
|
|
335
340
|
useEffect(() => {
|
|
336
341
|
let live = true;
|
|
337
342
|
// The card's fields stay disabled until this read is in (#1889 review: a draft typed meanwhile was overwritten);
|
|
@@ -343,6 +348,11 @@ function EditProfile({ it, onClose }) {
|
|
|
343
348
|
const resolved = checkCard(it.instance_name, body);
|
|
344
349
|
const mine = (k) => (resolved && resolved[k] && resolved[k].by === "human" ? resolved[k] : null);
|
|
345
350
|
setCard({ mine: { tagline: mine("tagline"), bio: mine("bio"), portrait: mine("portrait"), banner: mine("banner") }, limits: body.limits || null });
|
|
351
|
+
const bf = resolved && resolved.banner_focus;
|
|
352
|
+
const agent = Number.isInteger(body.banner_focus_agent) && body.banner_focus_agent >= 0 && body.banner_focus_agent <= 100 ? body.banner_focus_agent : null;
|
|
353
|
+
const mineFocus = bf && bf.by === "human" ? bf.value : null;
|
|
354
|
+
setFocus({ value: mineFocus ?? agent ?? 50, mine: mineFocus, start: mineFocus, agent });
|
|
355
|
+
if (resolved && resolved.banner) preview.fromSrc(resolved.banner.src);
|
|
346
356
|
setCardText({ tagline: mine("tagline") ? mine("tagline").text : "", bio: mine("bio") ? mine("bio").text : "" });
|
|
347
357
|
}).catch(() => { if (live && lease.current()) setCard(none); });
|
|
348
358
|
return () => { live = false; };
|
|
@@ -360,6 +370,7 @@ function EditProfile({ it, onClose }) {
|
|
|
360
370
|
const clearing = clearCard;
|
|
361
371
|
const before = card && card.mine ? { tagline: card.mine.tagline ? card.mine.tagline.text : "", bio: card.mine.bio ? card.mine.bio.text : "" } : { tagline: "", bio: "" };
|
|
362
372
|
if (!clearing) for (const k of ["tagline", "bio"]) if (cardText[k] !== before[k]) draft[`card_${k}`] = cardText[k];
|
|
373
|
+
if (!clearing && focus.mine !== focus.start) draft.card_banner_focus = focus.mine; // #2086
|
|
363
374
|
const cardPics = {};
|
|
364
375
|
try {
|
|
365
376
|
// A picture too large is fitted first (after asking) — before anything is written, so declining writes nothing.
|
|
@@ -422,10 +433,19 @@ function EditProfile({ it, onClose }) {
|
|
|
422
433
|
<label class="field"><span>${tn("cardTagline")}</span><input value=${cardText.tagline} onInput=${(e) => setCardText({ ...cardText, tagline: e.target.value })} /></label>
|
|
423
434
|
<label class="field"><span>${tn("cardBio")}</span><textarea rows="4" value=${cardText.bio} onInput=${(e) => setCardText({ ...cardText, bio: e.target.value })}></textarea></label>
|
|
424
435
|
${["portrait", "banner"].map((slot) => html`<div key=${slot} class="form">
|
|
425
|
-
<label class="field"><span>${tn(slot === "portrait" ? "cardPortrait" : "cardBanner")}</span><input ref=${pics[slot]} type="file" accept="image/png,image/jpeg,image/webp"
|
|
436
|
+
<label class="field"><span>${tn(slot === "portrait" ? "cardPortrait" : "cardBanner")}</span><input ref=${pics[slot]} type="file" accept="image/png,image/jpeg,image/webp"
|
|
437
|
+
onChange=${slot === "banner" ? (e) => { const fl = e.currentTarget.files && e.currentTarget.files[0]; if (fl) preview.fromFile(fl); } : undefined} /></label>
|
|
426
438
|
${card && card.mine && card.mine[slot] ? html`<div class="field-row"><button type="button" class="btn btn-sm" aria-pressed=${removeCard[slot] ? "true" : "false"}
|
|
427
439
|
onClick=${() => setRemoveCard({ ...removeCard, [slot]: !removeCard[slot] })}>${tn(removeCard[slot] ? "cardKeep" : "cardRemove")}</button></div>` : null}
|
|
428
440
|
</div>`)}
|
|
441
|
+
${bannerPreview ? html`<div class="field bf-field">
|
|
442
|
+
<span>${tn("cardBannerFocus")}</span>
|
|
443
|
+
<${BannerFocusPreview} src=${bannerPreview.src} focus=${focus.value} />
|
|
444
|
+
<input type="range" min="0" max="100" step="1" value=${focus.value} aria-label=${tn("cardBannerFocus")}
|
|
445
|
+
onInput=${(e) => { const v = Number(e.currentTarget.value); setFocus({ ...focus, value: v, mine: v }); }} />
|
|
446
|
+
<span class="note">${tn("cardBannerFocusNote")}</span>
|
|
447
|
+
${focus.mine !== null ? html`<div class="field-row"><button type="button" class="btn btn-sm" onClick=${() => setFocus({ ...focus, value: focus.agent ?? 50, mine: null })}>${tn("cardFocusFollow")}</button></div>` : null}
|
|
448
|
+
</div>` : null}
|
|
429
449
|
</fieldset>
|
|
430
450
|
<div class="field-row"><button type="button" class="btn btn-sm" aria-pressed=${clearCard ? "true" : "false"} onClick=${() => setClearCard(!clearCard)}>${tn(clearCard ? "keepCard" : "clearCard")}</button>
|
|
431
451
|
${clearCard ? html`<span class="muted">${tn("clearCardNote")}</span>` : null}</div>
|
|
@@ -9,7 +9,12 @@
|
|
|
9
9
|
* surfaces it is used on, and focus rings and strong borders meet 3:1 (tests/web-tokens-1408.test.ts).
|
|
10
10
|
*
|
|
11
11
|
* Inter is bundled (/assets/inter.woff2, SIL OFL 1.1 — see fonts/README.md): the CSP allows fonts from 'self' only,
|
|
12
|
-
* and nothing is loaded from another site. Chinese falls back to the system's CJK fonts, in the order below.
|
|
12
|
+
* and nothing is loaded from another site. Chinese falls back to the system's CJK fonts, in the order below.
|
|
13
|
+
*
|
|
14
|
+
* Emoji (#1918 follow-up, the user's tofu boxes): every stack ends with the platforms' colour emoji fonts and the
|
|
15
|
+
* `emoji` generic (--font-emoji), so an emoji the text fonts lack is drawn by Windows' Segoe UI Emoji, macOS's Apple
|
|
16
|
+
* Color Emoji or Linux's Noto Color Emoji rather than as a box — for the stock stacks and each theme font
|
|
17
|
+
* (app.css: .th-font-system, .th-font-mono). A browser tries the families in order for each character. */
|
|
13
18
|
|
|
14
19
|
@font-face {
|
|
15
20
|
font-family: "Inter";
|
|
@@ -22,8 +27,9 @@
|
|
|
22
27
|
|
|
23
28
|
:root {
|
|
24
29
|
/* ── Type ── */
|
|
25
|
-
--font-
|
|
26
|
-
--font-
|
|
30
|
+
--font-emoji: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji", emoji;
|
|
31
|
+
--font-ui: Inter, "PingFang TC", "PingFang SC", "Noto Sans TC", "Noto Sans SC", "Noto Sans CJK TC", "Noto Sans CJK SC", "Microsoft JhengHei", "Microsoft YaHei", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif, var(--font-emoji);
|
|
32
|
+
--font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace, var(--font-emoji);
|
|
27
33
|
--fs-xs: 12px; --lh-xs: 16px;
|
|
28
34
|
--fs-sm: 13px; --lh-sm: 18px;
|
|
29
35
|
--fs-md: 15px; --lh-md: 24px; /* message text */
|
|
@@ -37,6 +37,7 @@ const P = {
|
|
|
37
37
|
type: () => html`<path d="M4 7V5h16v2M9 19h6M12 5v14"/>`,
|
|
38
38
|
chart: () => html`<path d="M3 20h18M6 16v-4M11 16V8M16 16v-6"/>`,
|
|
39
39
|
up: () => html`<path d="M12 19V5M5 12l7-7 7 7"/>`,
|
|
40
|
+
sort: () => html`<path d="M7 4v16M3 16l4 4 4-4M17 20V4M13 8l4-4 4 4"/>`, // #2179: the sidebar's sort
|
|
40
41
|
star: () => html`<path d="M12 2.5l2.94 5.96 6.56.95-4.75 4.63 1.12 6.54L12 17.5l-5.87 3.08 1.12-6.54L2.5 9.41l6.56-.95L12 2.5z"/>`,
|
|
41
42
|
eye: () => html`<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/>`,
|
|
42
43
|
check: () => html`<path d="M20 6L9 17l-5-5"/>`,
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { html, useEffect, useLayoutEffect, useMemo, useRef, useState } from "./app-html.js";
|
|
5
5
|
import { t } from "./app-i18n.js";
|
|
6
6
|
import { useLease } from "./app-ctx.js";
|
|
7
|
+
import { guestPage } from "./app-store.js";
|
|
7
8
|
import { readStream } from "./read-stream.js";
|
|
8
9
|
import { Dialog } from "./ui-dialog.js";
|
|
9
10
|
import { ErrorState, Skeleton } from "./ui-states.js";
|
|
@@ -66,7 +67,17 @@ export function UsageMetric({ m }) {
|
|
|
66
67
|
const sub = [note, expiryText(m.expiresAt)].filter(Boolean).join(" · ");
|
|
67
68
|
return html`<div class="u-metric"><div class="u-row"><span class="u-label">${label}</span><span class="u-val">${val}</span></div>${sub ? html`<div class="u-sub">${sub}</div>` : null}</div>`;
|
|
68
69
|
}
|
|
69
|
-
/**
|
|
70
|
+
/**
|
|
71
|
+
* #2014: a row's title. A credential profile's row (`profile`; its API `name` is "Claude (<profile>)") shows the
|
|
72
|
+
* provider name on the title line beside the plan badge, and the profile on its own full-width muted line under it;
|
|
73
|
+
* the full name is the tooltip, and the dialog shows the profile unclipped. Any other row: its name.
|
|
74
|
+
*/
|
|
75
|
+
export function providerTitle(p) {
|
|
76
|
+
const name = String((p && p.name) || ""), profile = p && typeof p.profile === "string" && p.profile ? p.profile : "";
|
|
77
|
+
const suffix = ` (${profile})`;
|
|
78
|
+
return profile && name.endsWith(suffix) ? { base: name.slice(0, -suffix.length), profile } : { base: name, profile };
|
|
79
|
+
}
|
|
80
|
+
|
|
70
81
|
/**
|
|
71
82
|
* #1749: a plan as its badge reads — one short line. "Name [Tier] (N×)" (Claude's "Team Premium (6.25x)", "Max (20x)")
|
|
72
83
|
* becomes "Name ×N"; anything else (Pro, Codex's and muse's labels) as it is. The full name stays on the badge's
|
|
@@ -76,6 +87,7 @@ export function planBadge(plan) {
|
|
|
76
87
|
const m = /^(\S+)(?: [^()]+)? \((\d+(?:\.\d+)?)x\)$/i.exec(String(plan || "").trim());
|
|
77
88
|
return m ? `${m[1]} ×${m[2]}` : String(plan || "");
|
|
78
89
|
}
|
|
90
|
+
/** "Updated 3 min ago" (#1585): how old the numbers are — the panel may show the last snapshot while a fresh one comes. */
|
|
79
91
|
export function updatedAgo(fetchedAt, now = Date.now()) {
|
|
80
92
|
const ms = now - new Date(fetchedAt).getTime();
|
|
81
93
|
if (!Number.isFinite(ms) || ms < 60_000) return tn("usageUpdatedJustNow");
|
|
@@ -134,11 +146,12 @@ export function UsageBody({ owner }) {
|
|
|
134
146
|
const providers = ordered(data.providers);
|
|
135
147
|
content = html`${!providers.length ? html`<p class="note">${tn("usage.empty_active")}</p>` : null}
|
|
136
148
|
${providers.map((p, i) => html`<section key=${key(p)} class="u-provider">
|
|
137
|
-
<div class="u-head"><strong title=${p.name}>${p.
|
|
149
|
+
<div class="u-head"><strong title=${p.name}>${providerTitle(p).base}</strong>${p.plan ? html`<span class="pill u-plan" title=${p.plan}><span aria-hidden="true">${planBadge(p.plan)}</span><span class="sr-only">${p.plan}</span></span>` : null}
|
|
138
150
|
<span class="u-order">
|
|
139
151
|
<button type="button" class="icon-btn" disabled=${i === 0} aria-label=${tn("usageMoveUp")} title=${tn("usageMoveUp")} onClick=${() => move(key(p), -1, data.providers)}><${Icon} name="up" size=${14} /></button>
|
|
140
152
|
<button type="button" class="icon-btn" disabled=${i === providers.length - 1} aria-label=${tn("usageMoveDown")} title=${tn("usageMoveDown")} onClick=${() => move(key(p), 1, data.providers)}><${Icon} name="down" size=${14} /></button>
|
|
141
153
|
</span></div>
|
|
154
|
+
${providerTitle(p).profile ? html`<div class="u-profile" title=${p.name}>${providerTitle(p).profile}</div>` : null}
|
|
142
155
|
${p.stale ? html`<p class="note">${usageText(p.hint || "", p.hintI18n)}</p>` : null}
|
|
143
156
|
${p.status === "error" && !p.transient ? html`<p class="u-err">${usageText(p.error || tn("usage.error_fallback"), p.errorI18n)}</p>`
|
|
144
157
|
: p.status === "error" && p.transient ? html`<p class="note">${usageText(p.error || tn("usage.error_fallback"), p.errorI18n)}</p>`
|
|
@@ -147,11 +160,18 @@ export function UsageBody({ owner }) {
|
|
|
147
160
|
${!p.stale && p.hint ? html`<p class="note">${usageText(p.hint, p.hintI18n)}</p>` : !(p.metrics || []).length ? html`<p class="note">${tn("usage.no_data")}</p>` : null}`}
|
|
148
161
|
</section>`)}
|
|
149
162
|
<div class="u-foot"><span class="note" title=${new Date(data.fetchedAt).toLocaleString()}>${updatedAgo(data.fetchedAt)}${data.refreshing ? html` · <span class="u-refreshing">${tn("usageRefreshing")}</span>` : null}</span>
|
|
163
|
+
${byAgent() ? html`<a class="btn btn-ghost btn-sm u-by-agent" href="/ui/fleet/usage" title=${tn("usageByAgentTitle")}>${tn("usageByAgent")}</a>` : null}
|
|
150
164
|
<button type="button" class="btn btn-sm" onClick=${() => load(true)}>${tn("usageRefresh")}</button></div>`;
|
|
151
165
|
}
|
|
152
166
|
return content;
|
|
153
167
|
}
|
|
154
168
|
|
|
169
|
+
/**
|
|
170
|
+
* #2122: the plans above are per login; each agent's own tokens and API-equivalent are Fleet → Usage — linked from the
|
|
171
|
+
* app (not View's anonymous reader, not a guest, who has no Fleet).
|
|
172
|
+
*/
|
|
173
|
+
const byAgent = () => !guestPage() && typeof location !== "undefined" && location.pathname.startsWith("/ui");
|
|
174
|
+
|
|
155
175
|
/** `owner`: the navigation that opened it — its reads and the minute's timer live and end with it. */
|
|
156
176
|
export function UsageDialog({ onClose, owner = "" }) {
|
|
157
177
|
return html`<${Dialog} title=${tn("usage.title")} onClose=${onClose} wide=${true}><${UsageBody} owner=${`usage-dialog:${owner}`} /></${Dialog}>`;
|