@songsid/agend 2.2.0-alpha.2 → 2.2.0-alpha.4
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 +4 -4
- package/README.zh-TW.md +4 -4
- package/dist/agent-endpoint.d.ts +4 -1
- package/dist/agent-endpoint.js +14 -2
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/atomic-write.d.ts +21 -0
- package/dist/atomic-write.js +65 -0
- package/dist/atomic-write.js.map +1 -0
- package/dist/auth-api.js +2 -0
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/codex.d.ts +7 -0
- package/dist/backend/codex.js +7 -0
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/muse.d.ts +7 -0
- package/dist/backend/muse.js +7 -0
- package/dist/backend/muse.js.map +1 -1
- package/dist/backend/types.d.ts +6 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/binary-probe.d.ts +80 -0
- package/dist/binary-probe.js +182 -0
- package/dist/binary-probe.js.map +1 -0
- package/dist/cache-service.d.ts +17 -1
- package/dist/cache-service.js +39 -4
- package/dist/cache-service.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +5 -1
- package/dist/channel/adapters/discord.js +56 -11
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +38 -1
- package/dist/channel/adapters/telegram.js +82 -0
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/mcp-tools.js +1 -1
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/tool-router.d.ts +8 -1
- package/dist/channel/tool-router.js +4 -2
- package/dist/channel/tool-router.js.map +1 -1
- package/dist/channel/types.d.ts +21 -0
- package/dist/cli.js +133 -38
- package/dist/cli.js.map +1 -1
- package/dist/connection-secrets.d.ts +5 -0
- package/dist/connection-secrets.js.map +1 -1
- package/dist/cpu-profile.js +50 -0
- package/dist/cpu-profile.js.map +1 -1
- package/dist/daemon.d.ts +12 -0
- package/dist/daemon.js +166 -26
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.d.ts +1 -0
- package/dist/delivery-outbox.js +12 -0
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/discord-general.d.ts +15 -0
- package/dist/discord-general.js +24 -0
- package/dist/discord-general.js.map +1 -0
- package/dist/discord-permissions.d.ts +33 -0
- package/dist/discord-permissions.js +42 -0
- package/dist/discord-permissions.js.map +1 -0
- package/dist/env-file.d.ts +11 -0
- package/dist/env-file.js +27 -0
- package/dist/env-file.js.map +1 -0
- package/dist/fleet-context.d.ts +5 -0
- package/dist/fleet-context.js.map +1 -1
- package/dist/fleet-level-config.d.ts +8 -1
- package/dist/fleet-level-config.js +31 -0
- package/dist/fleet-level-config.js.map +1 -1
- package/dist/fleet-manager.d.ts +76 -7
- package/dist/fleet-manager.js +610 -142
- package/dist/fleet-manager.js.map +1 -1
- package/dist/fleet-stop-budget.d.ts +22 -0
- package/dist/fleet-stop-budget.js +95 -0
- package/dist/fleet-stop-budget.js.map +1 -0
- package/dist/kiro-transcript-lane.d.ts +8 -4
- package/dist/kiro-transcript-lane.js +66 -44
- package/dist/kiro-transcript-lane.js.map +1 -1
- package/dist/locale.js +88 -0
- package/dist/locale.js.map +1 -1
- package/dist/next-steps.d.ts +5 -0
- package/dist/next-steps.js +26 -0
- package/dist/next-steps.js.map +1 -0
- package/dist/outbound-handlers.js +8 -0
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +4 -0
- package/dist/outbound-schemas.js +6 -1
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/outbox-open-error.d.ts +7 -0
- package/dist/outbox-open-error.js +15 -0
- package/dist/outbox-open-error.js.map +1 -0
- package/dist/pane-context-cache.d.ts +25 -0
- package/dist/pane-context-cache.js +73 -0
- package/dist/pane-context-cache.js.map +1 -0
- package/dist/paths.d.ts +0 -7
- package/dist/paths.js +8 -2
- package/dist/paths.js.map +1 -1
- package/dist/provider-probe.d.ts +15 -0
- package/dist/provider-probe.js +20 -0
- package/dist/provider-probe.js.map +1 -1
- package/dist/public-link-progress.d.ts +75 -0
- package/dist/public-link-progress.js +204 -0
- package/dist/public-link-progress.js.map +1 -0
- package/dist/public-web-gateway.js +4 -1
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/public-web-link.d.ts +9 -2
- package/dist/public-web-link.js +89 -15
- package/dist/public-web-link.js.map +1 -1
- package/dist/quickstart-api.d.ts +93 -20
- package/dist/quickstart-api.js +198 -63
- package/dist/quickstart-api.js.map +1 -1
- package/dist/quickstart.d.ts +15 -0
- package/dist/quickstart.js +100 -29
- package/dist/quickstart.js.map +1 -1
- package/dist/reply-buttons-controller.d.ts +98 -0
- package/dist/reply-buttons-controller.js +174 -0
- package/dist/reply-buttons-controller.js.map +1 -0
- package/dist/reply-buttons.d.ts +113 -0
- package/dist/reply-buttons.js +208 -0
- package/dist/reply-buttons.js.map +1 -0
- package/dist/reply-dedup.js +3 -1
- package/dist/reply-dedup.js.map +1 -1
- package/dist/restart-guard.d.ts +1 -0
- package/dist/restart-guard.js +8 -3
- package/dist/restart-guard.js.map +1 -1
- package/dist/service-activation.d.ts +15 -2
- package/dist/service-activation.js +81 -4
- package/dist/service-activation.js.map +1 -1
- package/dist/service-installer.js +19 -1
- package/dist/service-installer.js.map +1 -1
- package/dist/settings-api.js +53 -12
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-authority.d.ts +11 -0
- package/dist/settings-authority.js +128 -0
- package/dist/settings-authority.js.map +1 -0
- package/dist/settings-change.d.ts +5 -0
- package/dist/settings-change.js +4 -3
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-confirmation.d.ts +3 -0
- package/dist/settings-confirmation.js +7 -1
- package/dist/settings-confirmation.js.map +1 -1
- package/dist/settings-effect.js +19 -7
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-http-confirmation.js +1 -1
- package/dist/settings-http-confirmation.js.map +1 -1
- package/dist/settings-redaction.d.ts +18 -0
- package/dist/settings-redaction.js +58 -0
- package/dist/settings-redaction.js.map +1 -0
- package/dist/setup-form.d.ts +9 -1
- package/dist/setup-form.js +25 -1
- package/dist/setup-form.js.map +1 -1
- package/dist/setup-host.js +13 -3
- package/dist/setup-host.js.map +1 -1
- package/dist/setup-wizard.js +26 -19
- package/dist/setup-wizard.js.map +1 -1
- package/dist/systemd-runtime.d.ts +15 -0
- package/dist/systemd-runtime.js +41 -0
- package/dist/systemd-runtime.js.map +1 -0
- package/dist/tmux-control.d.ts +22 -0
- package/dist/tmux-control.js +82 -17
- package/dist/tmux-control.js.map +1 -1
- package/dist/token-env-name.d.ts +37 -0
- package/dist/token-env-name.js +93 -0
- package/dist/token-env-name.js.map +1 -0
- package/dist/tool-permissions.js +5 -3
- package/dist/tool-permissions.js.map +1 -1
- package/dist/topic-commands.d.ts +7 -16
- package/dist/topic-commands.js +48 -84
- package/dist/topic-commands.js.map +1 -1
- package/dist/transcript-monitor.js +3 -0
- package/dist/transcript-monitor.js.map +1 -1
- package/dist/transcript-sources.d.ts +68 -0
- package/dist/transcript-sources.js +336 -29
- package/dist/transcript-sources.js.map +1 -1
- package/dist/transcript-turns.d.ts +39 -0
- package/dist/transcript-turns.js +58 -0
- package/dist/transcript-turns.js.map +1 -0
- package/dist/tunnel/cloudflared-install.d.ts +42 -0
- package/dist/tunnel/cloudflared-install.js +194 -9
- package/dist/tunnel/cloudflared-install.js.map +1 -1
- package/dist/tunnel/deb-extract.d.ts +9 -0
- package/dist/tunnel/deb-extract.js +94 -0
- package/dist/tunnel/deb-extract.js.map +1 -0
- package/dist/turn-reply-guard.d.ts +3 -0
- package/dist/turn-reply-guard.js.map +1 -1
- package/dist/ui/app.html +3 -0
- package/dist/ui/chat-render.js +32 -6
- package/dist/ui/chat-store.js +21 -1
- package/dist/ui/chat-strings.js +45 -0
- package/dist/ui/chat-thread.js +109 -52
- package/dist/ui/first-run.js +64 -0
- package/dist/ui/html-attachment.js +26 -0
- package/dist/ui/icons/apple-touch-icon.png +0 -0
- package/dist/ui/icons/favicon.ico +0 -0
- package/dist/ui/icons/favicon.svg +8 -0
- package/dist/ui/panel-chat.js +16 -7
- package/dist/ui/panel-fleet.js +5 -5
- package/dist/ui/panel-settings.js +18 -8
- package/dist/ui/preview-panel.js +47 -7
- package/dist/ui/preview.js +36 -13
- package/dist/ui/settings-apply.js +3 -1
- package/dist/ui/settings-dialogs.js +95 -25
- package/dist/ui/settings-discord.js +130 -0
- package/dist/ui/settings-model.js +21 -0
- package/dist/ui/settings-strings.js +46 -6
- package/dist/ui/settings-token.js +54 -0
- package/dist/ui/settings-wizard.js +63 -29
- package/dist/ui/shared/app-shell.js +19 -33
- package/dist/ui/shared/app-store.js +5 -3
- package/dist/ui/shared/app-stream.js +2 -1
- package/dist/ui/shared/app.css +34 -2
- package/dist/ui/shared/app.js +2 -1
- package/dist/ui/shared/instance-nav.js +310 -0
- package/dist/ui/shared/panel-view.js +12 -190
- package/dist/ui/shared/view-roster-store.js +5 -0
- package/dist/ui/shared/view-strings.js +12 -0
- package/dist/ui/signin.html +3 -0
- package/dist/update-dispatch.d.ts +1 -1
- package/dist/update-dispatch.js +1 -1
- package/dist/update-install.d.ts +15 -3
- package/dist/update-install.js +41 -13
- package/dist/update-install.js.map +1 -1
- package/dist/update-launch.d.ts +22 -0
- package/dist/update-launch.js +106 -0
- package/dist/update-launch.js.map +1 -0
- package/dist/update-marker.js +3 -8
- package/dist/update-marker.js.map +1 -1
- package/dist/usage/format-rich.js +7 -1
- package/dist/usage/format-rich.js.map +1 -1
- package/dist/usage/i18n-keys.d.ts +1 -1
- package/dist/usage/i18n-keys.js +1 -1
- package/dist/usage/i18n-keys.js.map +1 -1
- package/dist/usage/providers.d.ts +4 -0
- package/dist/usage/providers.js +2 -1
- package/dist/usage/providers.js.map +1 -1
- package/dist/usage/usage-api.js +48 -2
- package/dist/usage/usage-api.js.map +1 -1
- package/dist/view-api.d.ts +40 -0
- package/dist/view-api.js +28 -10
- package/dist/view-api.js.map +1 -1
- package/dist/web-api.d.ts +8 -0
- package/dist/web-api.js +53 -17
- package/dist/web-api.js.map +1 -1
- package/dist/web-auth.d.ts +8 -0
- package/dist/web-auth.js +34 -8
- package/dist/web-auth.js.map +1 -1
- package/dist/web-chat-history.d.ts +21 -0
- package/dist/web-chat-history.js +44 -0
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-host-guard.d.ts +18 -2
- package/dist/web-host-guard.js +30 -5
- package/dist/web-host-guard.js.map +1 -1
- package/dist/web-icons.d.ts +21 -0
- package/dist/web-icons.js +38 -0
- package/dist/web-icons.js.map +1 -0
- package/dist/web-preview.d.ts +9 -0
- package/dist/web-preview.js +15 -9
- package/dist/web-preview.js.map +1 -1
- package/dist/web-session.d.ts +7 -1
- package/dist/web-session.js +23 -2
- package/dist/web-session.js.map +1 -1
- package/launcher/postinstall.cjs +8 -4
- package/package.json +1 -1
- package/templates/systemd.service.ejs +3 -3
package/dist/ui/shared/app.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// reached only by the dynamic imports below, in "full" mode. A test walks the static imports over real HTTP and asserts
|
|
7
7
|
// none of them names /ui/.
|
|
8
8
|
import { html, render } from "./app-html.js";
|
|
9
|
-
import { Shell, handleKey, showDialog, closeDrawer, retryable } from "./app-shell.js";
|
|
9
|
+
import { Shell, handleKey, showDialog, closeDrawer, retryable, setNewInstanceOpener } from "./app-shell.js";
|
|
10
10
|
import { startRouter, navStore, navigate } from "./app-nav.js";
|
|
11
11
|
import { createStream } from "./app-stream.js";
|
|
12
12
|
import { appStore, applyStatus, applyActivity } from "./app-store.js";
|
|
@@ -72,6 +72,7 @@ if (mode === "full") {
|
|
|
72
72
|
try { m = await loadFleet(); } catch { toast(t("app.loadFailed"), false); return; }
|
|
73
73
|
showDialog(m.CreateInstanceDialog);
|
|
74
74
|
};
|
|
75
|
+
setNewInstanceOpener(onNewInstance);
|
|
75
76
|
} else appStore.set({ connection: "none" });
|
|
76
77
|
|
|
77
78
|
startRouter(window);
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
// alpha.2 (docs/design/web-unified-instance-nav.md, N1): the one instance list of the sidebar, on every page — Chat,
|
|
2
|
+
// View, Fleet, Settings, Needs — and for View's anonymous reader. It was two lists: the shell's (live dots, server
|
|
3
|
+
// order, no filter, every row to Chat) and View's roster (tag groups, drag order, a filter, rows to View). Now one:
|
|
4
|
+
// - live state (the status frames the app already receives; no new poll): dots, "needs you", context %, backend;
|
|
5
|
+
// - View's tag groups, folding and drag order, kept by this browser;
|
|
6
|
+
// - the filter — text plus status and CLI chips — kept by this browser and the same on every page;
|
|
7
|
+
// - a row opens the view you are in (View → View, everywhere else → Chat);
|
|
8
|
+
// - the list is never remounted on navigation, and only its own scrollTop follows the active row (#1515).
|
|
9
|
+
// Public (/assets/): an anonymous View reader gets the same list, fed by View's roster read instead of status frames.
|
|
10
|
+
import { html, useLayoutEffect, useRef } from "./app-html.js";
|
|
11
|
+
import { t } from "./app-i18n.js";
|
|
12
|
+
import { appStore, createStore, useStore } from "./app-store.js";
|
|
13
|
+
import { chatPath, viewPath } from "./app-route.js";
|
|
14
|
+
import { Icon } from "./ui-icons.js";
|
|
15
|
+
import { Skeleton } from "./ui-states.js";
|
|
16
|
+
import { viewStore } from "./view-roster-store.js";
|
|
17
|
+
import "./view-strings.js";
|
|
18
|
+
|
|
19
|
+
/** view.* text with named values. */
|
|
20
|
+
function tn(key, values = {}) {
|
|
21
|
+
let s = t(`view.${key}`);
|
|
22
|
+
for (const [k, v] of Object.entries(values)) s = s.split(`{${k}}`).join(String(v));
|
|
23
|
+
return s;
|
|
24
|
+
}
|
|
25
|
+
const stored = (k) => { try { return localStorage.getItem(k); } catch { return null; } };
|
|
26
|
+
const store = (k, v) => { try { localStorage.setItem(k, v); } catch { /* this page only */ } };
|
|
27
|
+
/** A row's instance name: `name` (status frames) or `instance_name` (View's roster). */
|
|
28
|
+
const nameOf = (it) => (it && (it.name != null ? it.name : it.instance_name)) || "";
|
|
29
|
+
|
|
30
|
+
// ── Groups, labels, the filter ──
|
|
31
|
+
export function groupOf(it) {
|
|
32
|
+
const tags = Array.isArray(it.tags) ? it.tags : [];
|
|
33
|
+
if (tags.includes("classic")) return "Classic";
|
|
34
|
+
if (tags.length) return tags[0];
|
|
35
|
+
return "Other";
|
|
36
|
+
}
|
|
37
|
+
const BACKEND_LABELS = { "claude-code": "Claude Code", "kiro-cli": "Kiro CLI", codex: "Codex", grok: "Grok Build", antigravity: "Antigravity", opencode: "OpenCode" };
|
|
38
|
+
export function backendLabel(backend) { return BACKEND_LABELS[backend] || backend || "Unknown"; }
|
|
39
|
+
export const KNOWN_BACKENDS = new Set(Object.keys(BACKEND_LABELS));
|
|
40
|
+
export const STATUS_KEYS = { running: "statusRunning", paused: "statusPaused", stopped: "statusStopped", crashed: "statusCrashed" };
|
|
41
|
+
|
|
42
|
+
/** The status chips, in order (design Q5): what the row's dot says, in five words. */
|
|
43
|
+
export const STATUS_FACETS = ["working", "needs", "idle", "stopped", "crashed"];
|
|
44
|
+
/** Which status chip a row falls under: needs you first, then crashed, not running, working (or stuck), idle. */
|
|
45
|
+
export function statusFacet(it, exec, awaiting) {
|
|
46
|
+
if (awaiting != null) return "needs";
|
|
47
|
+
if (!it || it.status === "crashed") return "crashed";
|
|
48
|
+
if (it.status !== "running") return "stopped";
|
|
49
|
+
return exec === "working" || exec === "stuck" ? "working" : "idle";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function normalizeFilter(query) { return String(query == null ? "" : query).trim().toLowerCase(); }
|
|
53
|
+
/** Case-insensitive substring on name (incl. display name), model and backend. */
|
|
54
|
+
export function instanceMatchesFilter(it, needle, labelOf) {
|
|
55
|
+
if (!needle) return true;
|
|
56
|
+
const fields = [nameOf(it), it.display_name, it.model, it.backend, labelOf ? labelOf(it.backend) : null];
|
|
57
|
+
return fields.some((value) => typeof value === "string" && value.toLowerCase().includes(needle));
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Visible groups in display order, and the "N / M" counts. `facets` (optional): { status: [...], cli: [...],
|
|
61
|
+
* statusOf(name) } — an empty list does not filter; a non-empty one keeps the rows whose status / backend is in it.
|
|
62
|
+
*/
|
|
63
|
+
export function filterSidebar(groupNames, instByGroup, rosterByName, query, labelOf, facets) {
|
|
64
|
+
const needle = normalizeFilter(query);
|
|
65
|
+
const status = new Set(facets && Array.isArray(facets.status) ? facets.status : []);
|
|
66
|
+
const cli = new Set(facets && Array.isArray(facets.cli) ? facets.cli : []);
|
|
67
|
+
const keep = (name) => {
|
|
68
|
+
const it = rosterByName.get(name);
|
|
69
|
+
if (!instanceMatchesFilter(it, needle, labelOf)) return false;
|
|
70
|
+
if (status.size && !status.has(facets.statusOf ? facets.statusOf(name) : statusFacet(it))) return false;
|
|
71
|
+
if (cli.size && !cli.has(it.backend || "")) return false;
|
|
72
|
+
return true;
|
|
73
|
+
};
|
|
74
|
+
const groups = [];
|
|
75
|
+
let shown = 0, total = 0;
|
|
76
|
+
for (const g of groupNames) {
|
|
77
|
+
const names = (instByGroup.get(g) || []).filter((name) => rosterByName.has(name));
|
|
78
|
+
total += names.length;
|
|
79
|
+
const visible = names.filter(keep);
|
|
80
|
+
shown += visible.length;
|
|
81
|
+
if (visible.length) groups.push({ group: g, names: visible });
|
|
82
|
+
}
|
|
83
|
+
return { groups, shown, total, active: needle.length > 0 || status.size > 0 || cli.size > 0 };
|
|
84
|
+
}
|
|
85
|
+
export function instanceTooltip(it) {
|
|
86
|
+
const context = it.context_pct != null && Number.isFinite(it.context_pct) ? `${Math.round(Math.min(100, Math.max(0, it.context_pct)))}%` : tn("tooltipUnavailable");
|
|
87
|
+
const status = STATUS_KEYS[it.status] ? tn(STATUS_KEYS[it.status]) : it.status || tn("statusUnknown");
|
|
88
|
+
const line = (key, value) => tn(key, { value });
|
|
89
|
+
const lines = [];
|
|
90
|
+
const name = nameOf(it);
|
|
91
|
+
if (it.display_name && it.display_name !== name) lines.push(it.display_name, `(${name})`);
|
|
92
|
+
else lines.push(name);
|
|
93
|
+
lines.push(line("tooltipBackend", backendLabel(it.backend)));
|
|
94
|
+
const src = (s) => (s === "instance" || s === "classic" ? " (configured)" : s === "fleet-default" ? " (fleet default)" : "");
|
|
95
|
+
lines.push(line("tooltipModel", (it.model || tn("tooltipUnavailable")) + src(it.model_source)));
|
|
96
|
+
lines.push(line("tooltipStatus", status));
|
|
97
|
+
lines.push(line("tooltipContext", context));
|
|
98
|
+
if (it.effort) lines.push(line("tooltipEffort", it.effort + src(it.effort_source)));
|
|
99
|
+
if (typeof it.cost === "number" && it.cost > 0) lines.push(line("tooltipCost", `$${it.cost.toFixed(2)}`));
|
|
100
|
+
if (typeof it.description === "string" && it.description.trim()) lines.push("", it.description.trim());
|
|
101
|
+
return lines.join("\n");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ── This browser's order (drag), folded groups and filter ──
|
|
105
|
+
const SIDEBAR_ORDER_KEY = "agend_view_sidebar_order"; // View's key since #999: everyone's arrangement carries over
|
|
106
|
+
const COLLAPSED_KEY = "agend_instance_collapsed";
|
|
107
|
+
const FILTER_KEY = "agend_instance_filter";
|
|
108
|
+
function readOrder() {
|
|
109
|
+
const groups = new Map(), insts = new Map();
|
|
110
|
+
try {
|
|
111
|
+
const rows = JSON.parse(stored(SIDEBAR_ORDER_KEY) || "[]");
|
|
112
|
+
if (Array.isArray(rows)) for (const row of rows) {
|
|
113
|
+
if (row && row.item_type === "group" && typeof row.item_name === "string") groups.set(row.item_name, Number(row.sort_index) || 0);
|
|
114
|
+
else if (row && row.item_type === "instance" && typeof row.item_name === "string") insts.set(row.item_name, Number(row.sort_index) || 0);
|
|
115
|
+
}
|
|
116
|
+
} catch { /* ignore invalid browser-local state */ }
|
|
117
|
+
return { groups, insts };
|
|
118
|
+
}
|
|
119
|
+
function readCollapsed() {
|
|
120
|
+
try { const a = JSON.parse(stored(COLLAPSED_KEY) || "[]"); return new Set(Array.isArray(a) ? a.filter((x) => typeof x === "string") : []); } catch { return new Set(); }
|
|
121
|
+
}
|
|
122
|
+
const strings = (a) => (Array.isArray(a) ? a.filter((x) => typeof x === "string") : []);
|
|
123
|
+
function readFilter() {
|
|
124
|
+
try {
|
|
125
|
+
const f = JSON.parse(stored(FILTER_KEY) || "{}");
|
|
126
|
+
return { q: f && typeof f.q === "string" ? f.q : "", status: strings(f && f.status).filter((s) => STATUS_FACETS.includes(s)), cli: strings(f && f.cli) };
|
|
127
|
+
} catch { return { q: "", status: [], cli: [] }; }
|
|
128
|
+
}
|
|
129
|
+
/** Display order: this browser's saved order first, then new groups/instances by tag and name. */
|
|
130
|
+
export function computeOrder(roster, order) {
|
|
131
|
+
const groups = new Map();
|
|
132
|
+
for (const it of roster) { const g = groupOf(it); if (!groups.has(g)) groups.set(g, []); groups.get(g).push(it); }
|
|
133
|
+
const gidx = (g) => (order.groups.has(g) ? order.groups.get(g) : Infinity);
|
|
134
|
+
const groupNames = [...groups.keys()].sort((a, b) => {
|
|
135
|
+
const ia = gidx(a), ib = gidx(b);
|
|
136
|
+
if (ia !== ib) return ia - ib;
|
|
137
|
+
if (a === "Other") return 1;
|
|
138
|
+
if (b === "Other") return -1;
|
|
139
|
+
return a.localeCompare(b);
|
|
140
|
+
});
|
|
141
|
+
const instByGroup = new Map();
|
|
142
|
+
for (const [g, items] of groups) {
|
|
143
|
+
instByGroup.set(g, items.map(nameOf).sort((a, b) => {
|
|
144
|
+
const ia = order.insts.has(a) ? order.insts.get(a) : Infinity, ib = order.insts.has(b) ? order.insts.get(b) : Infinity;
|
|
145
|
+
return ia - ib;
|
|
146
|
+
}));
|
|
147
|
+
}
|
|
148
|
+
return { groupNames, instByGroup };
|
|
149
|
+
}
|
|
150
|
+
function moveIn(arr, moving, target, before) {
|
|
151
|
+
if (!arr) return;
|
|
152
|
+
const from = arr.indexOf(moving);
|
|
153
|
+
if (from === -1) return;
|
|
154
|
+
arr.splice(from, 1);
|
|
155
|
+
let to = arr.indexOf(target);
|
|
156
|
+
if (to === -1) { arr.push(moving); return; }
|
|
157
|
+
if (!before) to += 1;
|
|
158
|
+
arr.splice(to, 0, moving);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** The list's state for the page's life: this browser's order, folded groups and filter (each also kept in storage). */
|
|
162
|
+
export const navStore = createStore({ order: readOrder(), collapsed: readCollapsed(), filter: readFilter() });
|
|
163
|
+
/** Read them again from this browser's storage — what a page load does (the store above is made that way). */
|
|
164
|
+
export function loadNavPrefs() { navStore.set({ order: readOrder(), collapsed: readCollapsed(), filter: readFilter() }); }
|
|
165
|
+
/** Change the filter ({ q?, status?, cli? }): the same on every page, and kept by this browser. */
|
|
166
|
+
export function setNavFilter(patch) {
|
|
167
|
+
const filter = { ...navStore.get().filter, ...patch };
|
|
168
|
+
navStore.set({ filter });
|
|
169
|
+
store(FILTER_KEY, JSON.stringify(filter));
|
|
170
|
+
}
|
|
171
|
+
function setCollapsed(collapsed) { navStore.set({ collapsed }); store(COLLAPSED_KEY, JSON.stringify([...collapsed])); }
|
|
172
|
+
function persistOrder(groupNames, instByGroup) {
|
|
173
|
+
const rows = [];
|
|
174
|
+
groupNames.forEach((g, i) => rows.push({ item_type: "group", item_name: g, sort_index: i, group_name: null }));
|
|
175
|
+
for (const g of groupNames) (instByGroup.get(g) || []).forEach((n, j) => rows.push({ item_type: "instance", item_name: n, sort_index: j, group_name: g }));
|
|
176
|
+
store(SIDEBAR_ORDER_KEY, JSON.stringify(rows));
|
|
177
|
+
navStore.set({ order: { groups: new Map(groupNames.map((g, i) => [g, i])), insts: new Map(rows.filter((r) => r.item_type === "instance").map((r) => [r.item_name, r.sort_index])) } });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ── The list ──
|
|
181
|
+
|
|
182
|
+
const statusText = (facet) => (facet === "needs" ? t("app.needsYou") : facet === "working" ? t("app.statusWorking") : facet === "idle" ? t("app.statusIdle")
|
|
183
|
+
: facet === "crashed" ? t("app.statusCrashed") : t("app.statusStopped"));
|
|
184
|
+
const dotClass = (facet, exec) => (facet === "needs" ? "warn" : facet === "crashed" ? "bad" : facet === "stopped" ? "off" : exec === "stuck" ? "warn" : facet === "working" ? "busy" : "ok");
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* The sidebar's instance list. `route`: the page's (which instance is open, and which view a row opens). `items`: the
|
|
188
|
+
* rows (status frames' instances, or View's roster for an anonymous reader); `loaded` false shows a skeleton.
|
|
189
|
+
* `exec` / `awaiting`: live state by name (empty for the anonymous reader). `onPick`: a row was clicked (the drawer
|
|
190
|
+
* closes).
|
|
191
|
+
*/
|
|
192
|
+
export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting = {}, viewOnly = false, onPick }) {
|
|
193
|
+
const nav = useStore(navStore);
|
|
194
|
+
const filterRef = useRef(null);
|
|
195
|
+
const list = useRef(null);
|
|
196
|
+
const drag = useRef(null);
|
|
197
|
+
const toView = viewOnly || (route && route.panel === "view");
|
|
198
|
+
const active = route && (route.panel === "chat" || route.panel === "view") ? route.instance : null;
|
|
199
|
+
useLayoutEffect(() => { keepActiveInView(list.current); }, [active, items.length, loaded]);
|
|
200
|
+
const own = (map, name) => Object.prototype.hasOwnProperty.call(map, name);
|
|
201
|
+
const byName = new Map(items.map((it) => [nameOf(it), it]));
|
|
202
|
+
const facetOf = (name) => statusFacet(byName.get(name), own(exec, name) ? exec[name] : null, own(awaiting, name) ? awaiting[name] : null);
|
|
203
|
+
const { groupNames, instByGroup } = computeOrder(items, nav.order);
|
|
204
|
+
const f = nav.filter;
|
|
205
|
+
const shown = filterSidebar(groupNames, instByGroup, byName, f.q, backendLabel, { status: f.status, cli: f.cli, statusOf: facetOf });
|
|
206
|
+
const backends = [...new Set(items.map((it) => it.backend).filter((b) => typeof b === "string" && b))].sort((a, b) => backendLabel(a).localeCompare(backendLabel(b)));
|
|
207
|
+
const toggle = (g) => { const c = new Set(navStore.get().collapsed); if (c.has(g)) c.delete(g); else c.add(g); setCollapsed(c); };
|
|
208
|
+
// From the store, not this render's copy: two clicks before the next render must both count.
|
|
209
|
+
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] }); };
|
|
210
|
+
// Drag to reorder: only the full list (a filtered subset would be ambiguous), groups among groups, instances within
|
|
211
|
+
// their group.
|
|
212
|
+
const dragProps = (item) => shown.active ? {} : {
|
|
213
|
+
draggable: "true",
|
|
214
|
+
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 */ } },
|
|
215
|
+
onDragEnd: (e) => { e.currentTarget.classList.remove("dragging"); drag.current = null; },
|
|
216
|
+
onDragOver: (e) => {
|
|
217
|
+
const d = drag.current;
|
|
218
|
+
if (!d || d.type !== item.type || d.name === item.name || (item.type === "instance" && d.group !== item.group)) return;
|
|
219
|
+
e.preventDefault();
|
|
220
|
+
const r = e.currentTarget.getBoundingClientRect();
|
|
221
|
+
const before = e.clientY < r.top + r.height / 2;
|
|
222
|
+
e.currentTarget.classList.toggle("drop-before", before);
|
|
223
|
+
e.currentTarget.classList.toggle("drop-after", !before);
|
|
224
|
+
},
|
|
225
|
+
onDragLeave: (e) => e.currentTarget.classList.remove("drop-before", "drop-after"),
|
|
226
|
+
onDrop: (e) => {
|
|
227
|
+
e.preventDefault();
|
|
228
|
+
const before = e.currentTarget.classList.contains("drop-before");
|
|
229
|
+
e.currentTarget.classList.remove("drop-before", "drop-after");
|
|
230
|
+
const d = drag.current;
|
|
231
|
+
if (!d || d.type !== item.type) return;
|
|
232
|
+
const names = [...groupNames], insts = new Map([...instByGroup].map(([g, n]) => [g, [...n]]));
|
|
233
|
+
if (item.type === "group") moveIn(names, d.name, item.name, before);
|
|
234
|
+
else if (d.group === item.group) moveIn(insts.get(item.group), d.name, item.name, before);
|
|
235
|
+
else return;
|
|
236
|
+
persistOrder(names, insts);
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
const chips = (key, values, label, text) => html`<details class="nav-facet">
|
|
240
|
+
<summary>${label}${f[key].length ? html` <span class="nav-facet-n">${f[key].length}</span>` : null}</summary>
|
|
241
|
+
<div class="nav-facet-list" role="group" aria-label=${label}>${values.map((value) => html`<label key=${value} class="nav-chip">
|
|
242
|
+
<input type="checkbox" checked=${f[key].includes(value)} onChange=${() => flip(key, value)} /><span>${text(value)}</span></label>`)}</div>
|
|
243
|
+
</details>`;
|
|
244
|
+
// The list scrolls; the filter under it stays in view (#999), so it sits outside the scrolling section.
|
|
245
|
+
return html`<div class="view-roster inst-nav">
|
|
246
|
+
<div class="side-section" id="instanceList" ref=${list}>
|
|
247
|
+
<h2 class="side-label">${tn("instancesLabel")}</h2>
|
|
248
|
+
${!loaded ? html`<${Skeleton} lines=${4} />` : !items.length ? html`<p class="side-empty">${t("app.noInstances")}</p>` : null}
|
|
249
|
+
${shown.groups.map(({ group: g, names }) => {
|
|
250
|
+
const folded = nav.collapsed.has(g);
|
|
251
|
+
return html`<div key=${g} class="v-group">
|
|
252
|
+
<button type="button" class="v-group-head" aria-expanded=${folded ? "false" : "true"} onClick=${() => toggle(g)}
|
|
253
|
+
title=${tn(folded ? "expandGroup" : "collapseGroup", { group: g })} ...${dragProps({ type: "group", name: g })}>
|
|
254
|
+
<${Icon} name="chevron" size=${14} cls=${folded ? "caret folded" : "caret"} /><span class="grow">${g}</span><span class="count">${names.length}</span></button>
|
|
255
|
+
${folded ? null : html`<ul class="inst-list">${names.map((name) => {
|
|
256
|
+
const it = byName.get(name);
|
|
257
|
+
const alias = typeof it.display_name === "string" && it.display_name.trim() && it.display_name.trim() !== name ? it.display_name.trim() : "";
|
|
258
|
+
const facet = facetOf(name);
|
|
259
|
+
const waiting = own(awaiting, name) ? awaiting[name] : null;
|
|
260
|
+
const on = name === active;
|
|
261
|
+
return html`<li key=${name}><a class=${`inst v-inst${on ? " active" : ""}`} href=${toView ? viewPath(name) : chatPath(name)} title=${instanceTooltip(it)}
|
|
262
|
+
aria-current=${on ? "page" : undefined} onClick=${onPick} ...${dragProps({ type: "instance", name, group: g })}>
|
|
263
|
+
<span class=${`dot ${dotClass(facet, own(exec, name) ? exec[name] : null)}`} aria-hidden="true"></span>
|
|
264
|
+
<span class="inst-text"><span class="inst-name">${name}</span>
|
|
265
|
+
${alias || facet === "needs" ? html`<span class="inst-sub">${alias ? html`<span class="inst-alias">${alias}</span>` : null}
|
|
266
|
+
${facet === "needs" ? html`<span class="badge-await" title=${waiting || t("app.approxNote")}>${t("app.needsYou")}</span>` : null}</span>` : null}
|
|
267
|
+
<span class="sr-only">${statusText(facet)}</span></span>
|
|
268
|
+
<span class="v-meta">${it.context_pct != null ? `${Math.round(it.context_pct)}%` : ""}</span>
|
|
269
|
+
<span class=${`cli-icon cli-${KNOWN_BACKENDS.has(it.backend) ? it.backend : "other"}`} title=${backendLabel(it.backend)} aria-label=${tn("tooltipBackend", { value: backendLabel(it.backend) })}></span>
|
|
270
|
+
</a></li>`;
|
|
271
|
+
})}</ul>`}
|
|
272
|
+
</div>`;
|
|
273
|
+
})}
|
|
274
|
+
${loaded && shown.active && !shown.groups.length ? html`<p class="side-empty">${tn("filterNone")}</p>` : null}
|
|
275
|
+
</div>
|
|
276
|
+
<div class="v-filter">
|
|
277
|
+
<label class="v-filter-box"><${Icon} name="search" size=${14} /><span class="sr-only">${tn("filterPh")}</span>
|
|
278
|
+
<input ref=${filterRef} id="filterInput" type="text" autocomplete="off" spellcheck="false" placeholder=${tn("filterPh")} value=${f.q}
|
|
279
|
+
onInput=${(e) => setNavFilter({ q: e.target.value })}
|
|
280
|
+
onKeyDown=${(e) => { if (e.key === "Escape" && f.q) { e.preventDefault(); e.stopPropagation(); setNavFilter({ q: "" }); } }} />
|
|
281
|
+
${f.q ? html`<button type="button" class="icon-btn v-filter-clear" aria-label=${tn("filterClear")} title=${tn("filterClear")}
|
|
282
|
+
onClick=${() => { setNavFilter({ q: "" }); filterRef.current && filterRef.current.focus(); }}><${Icon} name="close" size=${14} /></button>` : null}
|
|
283
|
+
</label>
|
|
284
|
+
<div class="nav-facets">
|
|
285
|
+
${viewOnly ? null : chips("status", STATUS_FACETS, tn("filterStatus"), statusText)}
|
|
286
|
+
${backends.length > 1 || f.cli.length ? chips("cli", backends, tn("filterCli"), backendLabel) : null}
|
|
287
|
+
${f.status.length || f.cli.length ? html`<button type="button" class="chip-btn nav-facet-reset" onClick=${() => setNavFilter({ status: [], cli: [] })}>${tn("filterFacetsReset")}</button>` : null}
|
|
288
|
+
</div>
|
|
289
|
+
<p class="note" aria-live="polite">${shown.shown} / ${shown.total}${shown.active ? ` ${tn("filterShown")}` : ""}</p>
|
|
290
|
+
</div>
|
|
291
|
+
</div>`;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Keep a sidebar list's current item in view by scrolling that list only (alpha.2): never scrollIntoView, which would
|
|
296
|
+
* also scroll every scrollable ancestor, the page included. A list the reader scrolled stays where it is otherwise.
|
|
297
|
+
*/
|
|
298
|
+
export function keepActiveInView(container) {
|
|
299
|
+
const a = container && container.querySelector('[aria-current="page"]');
|
|
300
|
+
if (!a) return;
|
|
301
|
+
const c = container.getBoundingClientRect(), r = a.getBoundingClientRect();
|
|
302
|
+
if (r.top < c.top) container.scrollTop -= c.top - r.top;
|
|
303
|
+
else if (r.bottom > c.bottom) container.scrollTop += r.bottom - c.bottom;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** The list fed by View's roster read (/api/profiles): the anonymous View reader's, who has no status frames. */
|
|
307
|
+
export function RosterNav({ route, viewOnly = false, onPick }) {
|
|
308
|
+
const v = useStore(viewStore);
|
|
309
|
+
return html`<${InstanceNav} route=${route || { panel: "view", instance: v.current }} items=${v.roster} loaded=${v.loaded} viewOnly=${viewOnly} onPick=${onPick} />`;
|
|
310
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// panel, in the app shell. Public (/assets/): under `web.view_access: open` an anonymous reader opens it in the
|
|
3
3
|
// View-only shell, so nothing here needs a session to show, and the one write (Edit profile) asks for one.
|
|
4
4
|
//
|
|
5
|
-
// Its roster
|
|
5
|
+
// Its roster read also feeds the sidebar's list for an anonymous reader (instance-nav.js is the list on every page).
|
|
6
6
|
// Everything recurring is a passive read (#1374) taken through the navigation's lease: the pane every 800 ms and the
|
|
7
7
|
// roster every 5 s while the panel is mounted and the tab is visible, usage every 60 s while its dialog is open. They
|
|
8
8
|
// stop when the panel goes.
|
|
@@ -10,7 +10,9 @@ import { html, useEffect, useLayoutEffect, useMemo, useRef, useState } from "./a
|
|
|
10
10
|
import { t } from "./app-i18n.js";
|
|
11
11
|
import { appStore, createStore, useStore } from "./app-store.js";
|
|
12
12
|
import { useLease } from "./app-ctx.js";
|
|
13
|
-
import { PanelHeader, setTitle,
|
|
13
|
+
import { PanelHeader, setTitle, onPanelKey, signInHref } from "./app-shell.js";
|
|
14
|
+
import { backendLabel, STATUS_KEYS } from "./instance-nav.js";
|
|
15
|
+
import { viewStore } from "./view-roster-store.js";
|
|
14
16
|
import { navigate } from "./app-nav.js";
|
|
15
17
|
import { viewPath } from "./app-route.js";
|
|
16
18
|
import { Dialog } from "./ui-dialog.js";
|
|
@@ -88,190 +90,10 @@ function paintAnsi(root) {
|
|
|
88
90
|
}
|
|
89
91
|
}
|
|
90
92
|
|
|
91
|
-
// ── The roster
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if (tags.length) return tags[0];
|
|
96
|
-
return "Other";
|
|
97
|
-
}
|
|
98
|
-
const BACKEND_LABELS = { "claude-code": "Claude Code", "kiro-cli": "Kiro CLI", codex: "Codex", grok: "Grok Build", antigravity: "Antigravity", opencode: "OpenCode" };
|
|
99
|
-
export function backendLabel(backend) { return BACKEND_LABELS[backend] || backend || "Unknown"; }
|
|
100
|
-
const KNOWN_BACKENDS = new Set(Object.keys(BACKEND_LABELS));
|
|
101
|
-
const STATUS_KEYS = { running: "statusRunning", paused: "statusPaused", stopped: "statusStopped", crashed: "statusCrashed" };
|
|
102
|
-
|
|
103
|
-
export function normalizeFilter(query) { return String(query == null ? "" : query).trim().toLowerCase(); }
|
|
104
|
-
/** Case-insensitive substring on name (incl. display name), model and backend. */
|
|
105
|
-
export function instanceMatchesFilter(it, needle, labelOf) {
|
|
106
|
-
if (!needle) return true;
|
|
107
|
-
const fields = [it.instance_name, it.display_name, it.model, it.backend, labelOf ? labelOf(it.backend) : null];
|
|
108
|
-
return fields.some((value) => typeof value === "string" && value.toLowerCase().includes(needle));
|
|
109
|
-
}
|
|
110
|
-
/** Visible groups in display order, and the "N / M" counts. */
|
|
111
|
-
export function filterSidebar(groupNames, instByGroup, rosterByName, query, labelOf) {
|
|
112
|
-
const needle = normalizeFilter(query);
|
|
113
|
-
const groups = [];
|
|
114
|
-
let shown = 0, total = 0;
|
|
115
|
-
for (const g of groupNames) {
|
|
116
|
-
const names = (instByGroup.get(g) || []).filter((name) => rosterByName.has(name));
|
|
117
|
-
total += names.length;
|
|
118
|
-
const visible = names.filter((name) => instanceMatchesFilter(rosterByName.get(name), needle, labelOf));
|
|
119
|
-
shown += visible.length;
|
|
120
|
-
if (visible.length) groups.push({ group: g, names: visible });
|
|
121
|
-
}
|
|
122
|
-
return { groups, shown, total, active: needle.length > 0 };
|
|
123
|
-
}
|
|
124
|
-
export function instanceTooltip(it) {
|
|
125
|
-
const context = it.context_pct != null && Number.isFinite(it.context_pct) ? `${Math.round(Math.min(100, Math.max(0, it.context_pct)))}%` : tn("tooltipUnavailable");
|
|
126
|
-
const status = STATUS_KEYS[it.status] ? tn(STATUS_KEYS[it.status]) : it.status || tn("statusUnknown");
|
|
127
|
-
const line = (key, value) => tn(key, { value });
|
|
128
|
-
const lines = [];
|
|
129
|
-
if (it.display_name && it.display_name !== it.instance_name) lines.push(it.display_name, `(${it.instance_name})`);
|
|
130
|
-
else lines.push(it.instance_name);
|
|
131
|
-
lines.push(line("tooltipBackend", backendLabel(it.backend)));
|
|
132
|
-
const src = (s) => (s === "instance" || s === "classic" ? " (configured)" : s === "fleet-default" ? " (fleet default)" : "");
|
|
133
|
-
lines.push(line("tooltipModel", (it.model || tn("tooltipUnavailable")) + src(it.model_source)));
|
|
134
|
-
lines.push(line("tooltipStatus", status));
|
|
135
|
-
lines.push(line("tooltipContext", context));
|
|
136
|
-
if (it.effort) lines.push(line("tooltipEffort", it.effort + src(it.effort_source)));
|
|
137
|
-
if (typeof it.description === "string" && it.description.trim()) lines.push("", it.description.trim());
|
|
138
|
-
return lines.join("\n");
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const SIDEBAR_ORDER_KEY = "agend_view_sidebar_order";
|
|
142
|
-
function readOrder() {
|
|
143
|
-
const groups = new Map(), insts = new Map();
|
|
144
|
-
try {
|
|
145
|
-
const rows = JSON.parse(stored(SIDEBAR_ORDER_KEY) || "[]");
|
|
146
|
-
if (Array.isArray(rows)) for (const row of rows) {
|
|
147
|
-
if (row && row.item_type === "group" && typeof row.item_name === "string") groups.set(row.item_name, Number(row.sort_index) || 0);
|
|
148
|
-
else if (row && row.item_type === "instance" && typeof row.item_name === "string") insts.set(row.item_name, Number(row.sort_index) || 0);
|
|
149
|
-
}
|
|
150
|
-
} catch { /* ignore invalid browser-local state */ }
|
|
151
|
-
return { groups, insts };
|
|
152
|
-
}
|
|
153
|
-
/** Display order: this browser's saved order first, then new groups/instances by tag and name. */
|
|
154
|
-
export function computeOrder(roster, order) {
|
|
155
|
-
const groups = new Map();
|
|
156
|
-
for (const it of roster) { const g = groupOf(it); if (!groups.has(g)) groups.set(g, []); groups.get(g).push(it); }
|
|
157
|
-
const gidx = (g) => (order.groups.has(g) ? order.groups.get(g) : Infinity);
|
|
158
|
-
const groupNames = [...groups.keys()].sort((a, b) => {
|
|
159
|
-
const ia = gidx(a), ib = gidx(b);
|
|
160
|
-
if (ia !== ib) return ia - ib;
|
|
161
|
-
if (a === "Other") return 1;
|
|
162
|
-
if (b === "Other") return -1;
|
|
163
|
-
return a.localeCompare(b);
|
|
164
|
-
});
|
|
165
|
-
const instByGroup = new Map();
|
|
166
|
-
for (const [g, items] of groups) {
|
|
167
|
-
instByGroup.set(g, items.map((it) => it.instance_name).sort((a, b) => {
|
|
168
|
-
const ia = order.insts.has(a) ? order.insts.get(a) : Infinity, ib = order.insts.has(b) ? order.insts.get(b) : Infinity;
|
|
169
|
-
return ia - ib;
|
|
170
|
-
}));
|
|
171
|
-
}
|
|
172
|
-
return { groupNames, instByGroup };
|
|
173
|
-
}
|
|
174
|
-
function moveIn(arr, moving, target, before) {
|
|
175
|
-
if (!arr) return;
|
|
176
|
-
const from = arr.indexOf(moving);
|
|
177
|
-
if (from === -1) return;
|
|
178
|
-
arr.splice(from, 1);
|
|
179
|
-
let to = arr.indexOf(target);
|
|
180
|
-
if (to === -1) { arr.push(moving); return; }
|
|
181
|
-
if (!before) to += 1;
|
|
182
|
-
arr.splice(to, 0, moving);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/** The roster shared by the panel and its sidebar section, for as long as the page lives. */
|
|
186
|
-
export const viewStore = createStore({ loaded: false, error: null, roster: [], filter: "", collapsed: new Set(), current: null, order: readOrder() });
|
|
187
|
-
const byName = (roster) => new Map(roster.map((r) => [r.instance_name, r]));
|
|
188
|
-
|
|
189
|
-
function persistOrder(groupNames, instByGroup) {
|
|
190
|
-
const rows = [];
|
|
191
|
-
groupNames.forEach((g, i) => rows.push({ item_type: "group", item_name: g, sort_index: i, group_name: null }));
|
|
192
|
-
for (const g of groupNames) (instByGroup.get(g) || []).forEach((n, j) => rows.push({ item_type: "instance", item_name: n, sort_index: j, group_name: g }));
|
|
193
|
-
store(SIDEBAR_ORDER_KEY, JSON.stringify(rows));
|
|
194
|
-
viewStore.set({ order: { groups: new Map(groupNames.map((g, i) => [g, i])), insts: new Map(rows.filter((r) => r.item_type === "instance").map((r) => [r.item_name, r.sort_index])) } });
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/** The sidebar section while View is mounted: the filter, then the groups and their instances (links to /view/<name>). */
|
|
198
|
-
function ViewRoster() {
|
|
199
|
-
const v = useStore(viewStore);
|
|
200
|
-
const filterRef = useRef(null);
|
|
201
|
-
const drag = useRef(null);
|
|
202
|
-
const rosterByName = byName(v.roster);
|
|
203
|
-
const { groupNames, instByGroup } = computeOrder(v.roster, v.order);
|
|
204
|
-
const shown = filterSidebar(groupNames, instByGroup, rosterByName, v.filter, backendLabel);
|
|
205
|
-
const setFilter = (value) => viewStore.set({ filter: value });
|
|
206
|
-
const toggle = (g) => { const c = new Set(v.collapsed); if (c.has(g)) c.delete(g); else c.add(g); viewStore.set({ collapsed: c }); };
|
|
207
|
-
// Drag to reorder: only the full list (a filtered subset would be ambiguous), groups among groups, instances within
|
|
208
|
-
// their group.
|
|
209
|
-
const dragProps = (item) => shown.active ? {} : {
|
|
210
|
-
draggable: "true",
|
|
211
|
-
onDragStart: (e) => { drag.current = item; e.currentTarget.classList.add("dragging"); try { e.dataTransfer.effectAllowed = "move"; e.dataTransfer.setData("text/plain", item.name); } catch { /* Firefox needs data */ } },
|
|
212
|
-
onDragEnd: (e) => { e.currentTarget.classList.remove("dragging"); drag.current = null; },
|
|
213
|
-
onDragOver: (e) => {
|
|
214
|
-
const d = drag.current;
|
|
215
|
-
if (!d || d.type !== item.type || d.name === item.name || (item.type === "instance" && d.group !== item.group)) return;
|
|
216
|
-
e.preventDefault();
|
|
217
|
-
const r = e.currentTarget.getBoundingClientRect();
|
|
218
|
-
const before = e.clientY < r.top + r.height / 2;
|
|
219
|
-
e.currentTarget.classList.toggle("drop-before", before);
|
|
220
|
-
e.currentTarget.classList.toggle("drop-after", !before);
|
|
221
|
-
},
|
|
222
|
-
onDragLeave: (e) => e.currentTarget.classList.remove("drop-before", "drop-after"),
|
|
223
|
-
onDrop: (e) => {
|
|
224
|
-
e.preventDefault();
|
|
225
|
-
const before = e.currentTarget.classList.contains("drop-before");
|
|
226
|
-
e.currentTarget.classList.remove("drop-before", "drop-after");
|
|
227
|
-
const d = drag.current;
|
|
228
|
-
if (!d || d.type !== item.type) return;
|
|
229
|
-
const names = [...groupNames], insts = new Map([...instByGroup].map(([g, n]) => [g, [...n]]));
|
|
230
|
-
if (item.type === "group") moveIn(names, d.name, item.name, before);
|
|
231
|
-
else if (d.group === item.group) moveIn(insts.get(item.group), d.name, item.name, before);
|
|
232
|
-
else return;
|
|
233
|
-
persistOrder(names, insts);
|
|
234
|
-
},
|
|
235
|
-
};
|
|
236
|
-
// The list scrolls; the filter under it stays in view (#999), so it sits outside the scrolling section.
|
|
237
|
-
return html`<div class="view-roster">
|
|
238
|
-
<div class="side-section" id="instanceList">
|
|
239
|
-
<h2 class="side-label">${tn("instancesLabel")}</h2>
|
|
240
|
-
${!v.loaded ? html`<${Skeleton} lines=${4} />` : !v.roster.length ? html`<p class="side-empty">${tn("noInstances")}</p>` : null}
|
|
241
|
-
${shown.groups.map(({ group: g, names }) => {
|
|
242
|
-
const folded = v.collapsed.has(g);
|
|
243
|
-
return html`<div key=${g} class="v-group">
|
|
244
|
-
<button type="button" class="v-group-head" aria-expanded=${folded ? "false" : "true"} onClick=${() => toggle(g)}
|
|
245
|
-
title=${tn(folded ? "expandGroup" : "collapseGroup", { group: g })} ...${dragProps({ type: "group", name: g })}>
|
|
246
|
-
<${Icon} name="chevron" size=${14} cls=${folded ? "caret folded" : "caret"} /><span class="grow">${g}</span><span class="count">${names.length}</span></button>
|
|
247
|
-
${folded ? null : html`<ul class="inst-list">${names.map((name) => {
|
|
248
|
-
const it = rosterByName.get(name);
|
|
249
|
-
const alias = typeof it.display_name === "string" && it.display_name.trim() && it.display_name.trim() !== name ? it.display_name.trim() : "";
|
|
250
|
-
const active = name === v.current;
|
|
251
|
-
return html`<li key=${name}><a class=${`inst v-inst${active ? " active" : ""}`} href=${viewPath(name)} title=${instanceTooltip(it)}
|
|
252
|
-
aria-current=${active ? "page" : undefined} onClick=${closeDrawer} ...${dragProps({ type: "instance", name, group: g })}>
|
|
253
|
-
<span class=${`dot ${it.status === "running" ? "ok" : it.status === "crashed" ? "bad" : "off"}`} aria-hidden="true"></span>
|
|
254
|
-
<span class="inst-text"><span class="inst-name">${name}</span>${alias ? html`<span class="inst-sub"><span class="inst-alias">${alias}</span></span>` : null}</span>
|
|
255
|
-
<span class="v-meta">${it.context_pct != null ? `${Math.round(it.context_pct)}%` : ""}</span>
|
|
256
|
-
<span class=${`cli-icon cli-${KNOWN_BACKENDS.has(it.backend) ? it.backend : "other"}`} title=${backendLabel(it.backend)} aria-label=${tn("tooltipBackend", { value: backendLabel(it.backend) })}></span>
|
|
257
|
-
</a></li>`;
|
|
258
|
-
})}</ul>`}
|
|
259
|
-
</div>`;
|
|
260
|
-
})}
|
|
261
|
-
${v.loaded && shown.active && !shown.groups.length ? html`<p class="side-empty">${tn("filterNone")}</p>` : null}
|
|
262
|
-
</div>
|
|
263
|
-
<div class="v-filter">
|
|
264
|
-
<label class="v-filter-box"><${Icon} name="search" size=${14} /><span class="sr-only">${tn("filterPh")}</span>
|
|
265
|
-
<input ref=${filterRef} id="filterInput" type="text" autocomplete="off" spellcheck="false" placeholder=${tn("filterPh")} value=${v.filter}
|
|
266
|
-
onInput=${(e) => setFilter(e.target.value)}
|
|
267
|
-
onKeyDown=${(e) => { if (e.key === "Escape" && v.filter) { e.preventDefault(); e.stopPropagation(); setFilter(""); } }} />
|
|
268
|
-
${v.filter ? html`<button type="button" class="icon-btn v-filter-clear" aria-label=${tn("filterClear")} title=${tn("filterClear")}
|
|
269
|
-
onClick=${() => { setFilter(""); filterRef.current && filterRef.current.focus(); }}><${Icon} name="close" size=${14} /></button>` : null}
|
|
270
|
-
</label>
|
|
271
|
-
<p class="note" aria-live="polite">${shown.shown} / ${shown.total}${shown.active ? ` ${tn("filterShown")}` : ""}</p>
|
|
272
|
-
</div>
|
|
273
|
-
</div>`;
|
|
274
|
-
}
|
|
93
|
+
// ── The roster ── The sidebar's list is instance-nav.js on every page (alpha.2, N1); its helpers are re-exported here
|
|
94
|
+
// for the View code and tests that used them from this module.
|
|
95
|
+
export { groupOf, backendLabel, normalizeFilter, instanceMatchesFilter, filterSidebar, instanceTooltip, computeOrder } from "./instance-nav.js";
|
|
96
|
+
export { viewStore };
|
|
275
97
|
|
|
276
98
|
// ── The panel ──
|
|
277
99
|
|
|
@@ -317,8 +139,6 @@ export function ViewPanel({ route, navKey }) {
|
|
|
317
139
|
const name = route.instance;
|
|
318
140
|
const it = name ? v.roster.find((r) => r.instance_name === name) : null;
|
|
319
141
|
|
|
320
|
-
// The roster is the sidebar's section while View is mounted.
|
|
321
|
-
useEffect(() => lease.hold(setSideSection(ViewRoster)), [lease]);
|
|
322
142
|
useEffect(() => { viewStore.set({ current: name }); }, [name]);
|
|
323
143
|
useEffect(() => { setTitle(it ? (it.display_name || it.instance_name) : tn("title")); }, [name, it && it.display_name, navKey]);
|
|
324
144
|
if (name && it) store("agend_last_view", name);
|
|
@@ -643,10 +463,12 @@ function UsageDialog({ onClose }) {
|
|
|
643
463
|
<button type="button" class="icon-btn" disabled=${i === 0} aria-label=${tn("usageMoveUp")} title=${tn("usageMoveUp")} onClick=${() => move(key(p), -1, data.providers)}><${Icon} name="up" size=${14} /></button>
|
|
644
464
|
<button type="button" class="icon-btn" disabled=${i === providers.length - 1} aria-label=${tn("usageMoveDown")} title=${tn("usageMoveDown")} onClick=${() => move(key(p), 1, data.providers)}><${Icon} name="down" size=${14} /></button>
|
|
645
465
|
</span></div>
|
|
646
|
-
${p.
|
|
466
|
+
${p.stale ? html`<p class="note">${usageText(p.hint || "", p.hintI18n)}</p>` : null}
|
|
467
|
+
${p.status === "error" && !p.transient ? html`<p class="u-err">${usageText(p.error || tn("usage.error_fallback"), p.errorI18n)}</p>`
|
|
468
|
+
: p.status === "error" && p.transient ? html`<p class="note">${usageText(p.error || tn("usage.error_fallback"), p.errorI18n)}</p>`
|
|
647
469
|
: p.status === "no-credentials" ? html`<p class="note">${tn("usage.not_logged_in")}</p>${p.hint ? html`<p class="note">${usageText(p.hint, p.hintI18n)}</p>` : null}`
|
|
648
470
|
: html`${(p.metrics || []).map((m, j) => html`<${UsageMetric} key=${j} m=${m} />`)}
|
|
649
|
-
${p.hint ? html`<p class="note">${usageText(p.hint, p.hintI18n)}</p>` : !(p.metrics || []).length ? html`<p class="note">${tn("usage.no_data")}</p>` : null}`}
|
|
471
|
+
${!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}`}
|
|
650
472
|
</section>`)}
|
|
651
473
|
<div class="u-foot"><span class="note">${tn("usageUpdated")} ${new Date(data.fetchedAt).toLocaleTimeString()}</span>
|
|
652
474
|
<button type="button" class="btn btn-sm" onClick=${() => load(true)}>${tn("usageRefresh")}</button></div>`;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// View's roster read (/api/profiles), shared by the View panel (its cards) and the anonymous reader's sidebar list
|
|
2
|
+
// (instance-nav.js), for as long as the page lives. The View panel fills it while mounted; nothing else reads the server.
|
|
3
|
+
import { createStore } from "./app-store.js";
|
|
4
|
+
|
|
5
|
+
export const viewStore = createStore({ loaded: false, error: null, roster: [], current: null });
|
|
@@ -12,6 +12,9 @@ register("view", {
|
|
|
12
12
|
"filterClear": "Clear filter",
|
|
13
13
|
"filterNone": "No instance matches",
|
|
14
14
|
"filterShown": "shown",
|
|
15
|
+
"filterStatus": "Status",
|
|
16
|
+
"filterCli": "CLI",
|
|
17
|
+
"filterFacetsReset": "Clear",
|
|
15
18
|
"displayName": "Display name",
|
|
16
19
|
"role": "Role",
|
|
17
20
|
"description": "Description",
|
|
@@ -38,6 +41,7 @@ register("view", {
|
|
|
38
41
|
"tooltipContext": "Context: {value}",
|
|
39
42
|
"tooltipModel": "Model: {value}",
|
|
40
43
|
"tooltipEffort": "Effort: {value}",
|
|
44
|
+
"tooltipCost": "Cost: {value}",
|
|
41
45
|
"tooltipUnavailable": "Unavailable",
|
|
42
46
|
"statusRunning": "Running",
|
|
43
47
|
"statusPaused": "Paused",
|
|
@@ -109,6 +113,8 @@ register("view", {
|
|
|
109
113
|
"usage.error.unreachable": "Could not reach {0}.",
|
|
110
114
|
"usage.error.token_rejected": "Token rejected. Run `{0}` once to refresh the login.",
|
|
111
115
|
"usage.error.rate_limited": "Rate limited by {0} — try again later.",
|
|
116
|
+
"usage.error.rate_limited_transient": "{0}: can't fetch usage right now (rate limited). This does not affect your agents.",
|
|
117
|
+
"usage.stale_rate_limited": "data from {0} min ago — can't refresh right now",
|
|
112
118
|
"usage.error.http": "Usage request failed (HTTP {0}).",
|
|
113
119
|
"usage.error.billing_http": "Billing request failed (HTTP {0}).",
|
|
114
120
|
"usage.error.invalid_response": "Invalid response from usage endpoint.",
|
|
@@ -163,6 +169,9 @@ register("view", {
|
|
|
163
169
|
"filterClear": "清除篩選",
|
|
164
170
|
"filterNone": "沒有符合的 instance",
|
|
165
171
|
"filterShown": "顯示中",
|
|
172
|
+
"filterStatus": "狀態",
|
|
173
|
+
"filterCli": "CLI",
|
|
174
|
+
"filterFacetsReset": "清除",
|
|
166
175
|
"displayName": "顯示名稱",
|
|
167
176
|
"role": "角色",
|
|
168
177
|
"description": "描述",
|
|
@@ -189,6 +198,7 @@ register("view", {
|
|
|
189
198
|
"tooltipContext": "上下文:{value}",
|
|
190
199
|
"tooltipModel": "模型:{value}",
|
|
191
200
|
"tooltipEffort": "推理強度:{value}",
|
|
201
|
+
"tooltipCost": "費用:{value}",
|
|
192
202
|
"tooltipUnavailable": "無資料",
|
|
193
203
|
"statusRunning": "運行中",
|
|
194
204
|
"statusPaused": "已暫停",
|
|
@@ -260,6 +270,8 @@ register("view", {
|
|
|
260
270
|
"usage.error.unreachable": "無法連線至 {0}。",
|
|
261
271
|
"usage.error.token_rejected": "Token 已被拒絕。請執行一次 `{0}` 以更新登入。",
|
|
262
272
|
"usage.error.rate_limited": "{0} 暫時限制請求速率,請稍後再試。",
|
|
273
|
+
"usage.error.rate_limited_transient": "暫時查不到 {0} 用量({0} 限制查詢頻率),不影響你的 agent。",
|
|
274
|
+
"usage.stale_rate_limited": "{0} 分鐘前的資料,暫時無法更新",
|
|
263
275
|
"usage.error.http": "用量請求失敗(HTTP {0})。",
|
|
264
276
|
"usage.error.billing_http": "計費請求失敗(HTTP {0})。",
|
|
265
277
|
"usage.error.invalid_response": "用量端點回傳了無效資料。",
|
package/dist/ui/signin.html
CHANGED
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
<meta name="referrer" content="no-referrer">
|
|
7
7
|
<meta name="robots" content="noindex">
|
|
8
8
|
<title>AgEnD — Sign in</title>
|
|
9
|
+
<link rel="icon" href="/favicon.ico" sizes="16x16 32x32 64x64">
|
|
10
|
+
<link rel="icon" href="/favicon.svg" type="image/svg+xml" sizes="any">
|
|
11
|
+
<link rel="apple-touch-icon" href="/apple-touch-icon.png" sizes="180x180">
|
|
9
12
|
<script src="/assets/theme.js"></script>
|
|
10
13
|
<link rel="stylesheet" href="/assets/tokens.css">
|
|
11
14
|
<style>
|