@songsid/agend 2.2.0-beta.1 → 2.2.0-beta.3
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 +1 -1
- package/README.zh-TW.md +1 -1
- package/dist/agent-cli-instructions.md +10 -7
- package/dist/agent-endpoint.js +21 -9
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-whats-new.d.ts +47 -0
- package/dist/agent-whats-new.js +141 -0
- package/dist/agent-whats-new.js.map +1 -0
- package/dist/auth-api.d.ts +2 -2
- package/dist/auth-api.js +4 -2
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/kiro-engine-ledger.d.ts +5 -0
- package/dist/backend/kiro-engine-ledger.js +20 -0
- package/dist/backend/kiro-engine-ledger.js.map +1 -1
- package/dist/backend/kiro-identity.d.ts +6 -0
- package/dist/backend/kiro-identity.js +19 -5
- package/dist/backend/kiro-identity.js.map +1 -1
- package/dist/backend/kiro.d.ts +14 -2
- package/dist/backend/kiro.js +84 -9
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/types.d.ts +4 -2
- package/dist/backend/types.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +11 -1
- package/dist/channel/adapters/discord.js +23 -1
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +19 -1
- package/dist/channel/adapters/telegram.js +60 -9
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/mcp-server.js +9 -1
- package/dist/channel/mcp-server.js.map +1 -1
- package/dist/channel/mcp-tools.d.ts +11 -0
- package/dist/channel/mcp-tools.js +23 -12
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/telegram-markdown.d.ts +6 -0
- package/dist/channel/telegram-markdown.js +104 -0
- package/dist/channel/telegram-markdown.js.map +1 -0
- package/dist/channel/tool-router.js +9 -3
- package/dist/channel/tool-router.js.map +1 -1
- package/dist/channel/types.d.ts +14 -2
- package/dist/cli.js +55 -16
- package/dist/cli.js.map +1 -1
- package/dist/command-table.js +1 -1
- package/dist/command-table.js.map +1 -1
- package/dist/config-validator.js +49 -0
- package/dist/config-validator.js.map +1 -1
- package/dist/cross-instance-envelope.d.ts +15 -2
- package/dist/cross-instance-envelope.js +27 -5
- package/dist/cross-instance-envelope.js.map +1 -1
- package/dist/daemon.d.ts +48 -0
- package/dist/daemon.js +250 -22
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.d.ts +17 -2
- package/dist/delivery-outbox.js +13 -2
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/delivery-reconciliation.d.ts +18 -1
- package/dist/delivery-reconciliation.js +67 -2
- package/dist/delivery-reconciliation.js.map +1 -1
- package/dist/fleet-manager.d.ts +63 -1
- package/dist/fleet-manager.js +314 -20
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/backend-providers/SKILL.md +1 -1
- package/dist/general-knowledge/skills/credential-profiles/SKILL.md +9 -6
- package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +11 -2
- package/dist/general-knowledge/skills/delegation-playbook/SKILL.md +8 -8
- package/dist/general-knowledge/skills/fleet-config/SKILL.md +5 -5
- package/dist/general-knowledge/skills/fleet-health/SKILL.md +4 -4
- package/dist/general-knowledge/skills/fleet-restart/SKILL.md +13 -13
- package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +3 -2
- package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +6 -0
- package/dist/general-knowledge/skills/model-discovery/SKILL.md +9 -9
- package/dist/general-knowledge/skills/multi-channel/SKILL.md +8 -2
- package/dist/general-knowledge/skills/scheduling/SKILL.md +5 -3
- package/dist/general-knowledge/skills/session-management/SKILL.md +6 -5
- package/dist/general-knowledge/skills/tui-effort/SKILL.md +2 -2
- package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +4 -3
- package/dist/general-knowledge/steering/core-rules.md +8 -7
- package/dist/instance-avatar.d.ts +36 -0
- package/dist/instance-avatar.js +57 -0
- package/dist/instance-avatar.js.map +1 -0
- package/dist/instance-config-impact.js +5 -0
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +4 -0
- package/dist/instance-lifecycle.js +3 -1
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instructions.d.ts +2 -0
- package/dist/instructions.js +10 -7
- package/dist/instructions.js.map +1 -1
- package/dist/locale.js +36 -26
- package/dist/locale.js.map +1 -1
- package/dist/next-steps.js +1 -1
- package/dist/next-steps.js.map +1 -1
- package/dist/outbound-handlers.d.ts +2 -0
- package/dist/outbound-handlers.js +28 -15
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +10 -2
- package/dist/outbound-schemas.js +15 -5
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/public-web-gateway.js +7 -2
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/service-activation.js +4 -3
- package/dist/service-activation.js.map +1 -1
- package/dist/settings-api.js +71 -21
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +70 -1
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +26 -4
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-http-confirmation.js +7 -0
- package/dist/settings-http-confirmation.js.map +1 -1
- package/dist/setup-form.d.ts +1 -1
- package/dist/setup-form.js +1 -1
- package/dist/status-emojis.d.ts +10 -3
- package/dist/status-emojis.js +11 -2
- package/dist/status-emojis.js.map +1 -1
- package/dist/tips.js +4 -4
- package/dist/tips.js.map +1 -1
- package/dist/transcript-delta-reader.d.ts +3 -3
- package/dist/transcript-delta-reader.js +8 -8
- package/dist/transcript-delta-reader.js.map +1 -1
- package/dist/transcript-monitor.d.ts +8 -0
- package/dist/transcript-monitor.js +26 -2
- package/dist/transcript-monitor.js.map +1 -1
- package/dist/transcript-sources.d.ts +54 -0
- package/dist/transcript-sources.js +188 -27
- package/dist/transcript-sources.js.map +1 -1
- package/dist/transcript-turns.d.ts +15 -2
- package/dist/transcript-turns.js +15 -2
- package/dist/transcript-turns.js.map +1 -1
- package/dist/types.d.ts +11 -0
- package/dist/ui/chat-emojis.js +33 -0
- package/dist/ui/chat-render.js +12 -0
- package/dist/ui/chat-store.js +1 -1
- package/dist/ui/chat-strings.js +12 -12
- package/dist/ui/chat-thread.js +23 -4
- package/dist/ui/panel-chat.js +16 -3
- package/dist/ui/panel-needs.js +5 -2
- package/dist/ui/panel-settings.js +129 -31
- package/dist/ui/settings-dialogs.js +7 -2
- package/dist/ui/settings-model.js +2 -1
- package/dist/ui/settings-strings.js +44 -4
- package/dist/ui/shared/app-i18n.js +37 -15
- package/dist/ui/shared/app-prefs.js +249 -0
- package/dist/ui/shared/app-route.js +1 -1
- package/dist/ui/shared/app-shell.js +17 -4
- package/dist/ui/shared/app-store.js +3 -1
- package/dist/ui/shared/app.css +55 -0
- package/dist/ui/shared/app.js +6 -1
- package/dist/ui/shared/header-tools.js +55 -5
- package/dist/ui/shared/instance-nav.js +169 -32
- package/dist/ui/shared/panel-view.js +6 -0
- package/dist/ui/shared/signin.js +7 -7
- package/dist/ui/shared/theme.js +39 -7
- package/dist/ui/shared/ui-icons.js +1 -0
- package/dist/ui/shared/usage-dialog.js +13 -4
- package/dist/ui/shared/view-strings.js +22 -4
- package/dist/ui/signin.html +1 -1
- package/dist/update-install.d.ts +15 -1
- package/dist/update-install.js +6 -0
- package/dist/update-install.js.map +1 -1
- package/dist/update-preflight.d.ts +100 -0
- package/dist/update-preflight.js +264 -0
- package/dist/update-preflight.js.map +1 -0
- package/dist/updater-caller.d.ts +31 -0
- package/dist/updater-caller.js +109 -0
- package/dist/updater-caller.js.map +1 -0
- package/dist/view-api.d.ts +1 -1
- package/dist/view-api.js +38 -8
- package/dist/view-api.js.map +1 -1
- package/dist/wake-coordinator.d.ts +11 -0
- package/dist/wake-coordinator.js +38 -4
- package/dist/wake-coordinator.js.map +1 -1
- package/dist/web-api.d.ts +21 -1
- package/dist/web-api.js +157 -1
- package/dist/web-api.js.map +1 -1
- package/dist/web-auth.d.ts +2 -2
- package/dist/web-auth.js +7 -4
- package/dist/web-auth.js.map +1 -1
- package/dist/web-host-guard.d.ts +3 -11
- package/dist/web-host-guard.js +3 -12
- package/dist/web-host-guard.js.map +1 -1
- package/dist/web-images.d.ts +47 -0
- package/dist/web-images.js +169 -0
- package/dist/web-images.js.map +1 -0
- package/dist/web-prefs.d.ts +63 -0
- package/dist/web-prefs.js +263 -0
- package/dist/web-prefs.js.map +1 -0
- package/dist/web-session.d.ts +36 -1
- package/dist/web-session.js +53 -6
- package/dist/web-session.js.map +1 -1
- package/dist/web-settings-patch.d.ts +21 -0
- package/dist/web-settings-patch.js +74 -0
- package/dist/web-settings-patch.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/workflow-templates/default.md +1 -1
- package/package.json +1 -1
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
// #1523 N3 (§3.4, Q2 = A): the controls every page's header has, in one order on Chat, View, Details and Fleet —
|
|
2
2
|
// Aa text size, shared per device: S / M / L (View also keeps Fit, its own extra choice);
|
|
3
3
|
// ◔ AI usage, unless the fleet turned it off (web.usage_panel: false — said by the page itself, no probe read).
|
|
4
|
+
// #1673: on a wide screen it is a right rail beside the page (shown or hidden per device); ◔ toggles it. On a
|
|
5
|
+
// narrower one, ◔ opens the dialog.
|
|
4
6
|
// The size is one attribute on the root (data-text-size); CSS turns it into one custom property the chat, Details and
|
|
5
7
|
// Fleet read (#1300: no style attribute). View's terminal multiplies its fitted size by VIEW_SCALE, as its density did.
|
|
6
|
-
import { html, useState } from "./app-html.js";
|
|
8
|
+
import { html, useEffect, useState } from "./app-html.js";
|
|
7
9
|
import { t } from "./app-i18n.js";
|
|
8
10
|
import { createStore, useStore } from "./app-store.js";
|
|
9
11
|
import { Icon } from "./ui-icons.js";
|
|
10
|
-
import { UsageDialog } from "./usage-dialog.js";
|
|
12
|
+
import { UsageBody, UsageDialog } from "./usage-dialog.js";
|
|
11
13
|
|
|
12
14
|
export const TEXT_SIZE_KEY = "agend_text_size";
|
|
13
15
|
export const VIEW_FIT_KEY = "agend_view_fit";
|
|
@@ -73,16 +75,64 @@ export function usageOffered() {
|
|
|
73
75
|
try { return document.body && document.body.dataset ? document.body.dataset.usagePanel !== "0" : true; } catch { return true; }
|
|
74
76
|
}
|
|
75
77
|
|
|
78
|
+
// #1673: the rail. Wide enough for it beside the sidebar and the page: RAIL_MEDIA (followed as it changes). Shown or
|
|
79
|
+
// hidden is this device's choice (agend_usage_rail; "0" = hidden, else shown) — a layout choice, not synced (#1665).
|
|
80
|
+
export const RAIL_MEDIA = "(min-width: 1200px)";
|
|
81
|
+
export const RAIL_KEY = "agend_usage_rail";
|
|
82
|
+
/** The pages whose header has ◔ — the rail is theirs. */
|
|
83
|
+
export const RAIL_PANELS = ["chat", "view", "details", "fleet"];
|
|
84
|
+
export const usageRailStore = createStore({ open: true });
|
|
85
|
+
/** A page load: this device's choice. */
|
|
86
|
+
export function initUsageRail() { usageRailStore.set({ open: read(RAIL_KEY) !== "0" }); }
|
|
87
|
+
export function setUsageRail(open) { write(RAIL_KEY, open ? "1" : "0"); usageRailStore.set({ open: !!open }); }
|
|
88
|
+
const railQuery = () => { try { return typeof matchMedia === "function" ? matchMedia(RAIL_MEDIA) : null; } catch { return null; } };
|
|
89
|
+
/** Whether the screen is wide enough for the rail, now and as it changes. */
|
|
90
|
+
export function useRailWidth() {
|
|
91
|
+
const [wide, setWide] = useState(() => !!railQuery()?.matches);
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
const q = railQuery();
|
|
94
|
+
if (!q) return undefined;
|
|
95
|
+
const on = () => setWide(!!q.matches);
|
|
96
|
+
on();
|
|
97
|
+
if (typeof q.addEventListener === "function") { q.addEventListener("change", on); return () => q.removeEventListener("change", on); }
|
|
98
|
+
if (typeof q.addListener === "function") { q.addListener(on); return () => q.removeListener(on); }
|
|
99
|
+
return undefined;
|
|
100
|
+
}, []);
|
|
101
|
+
return wide;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The rail: beside the page on a wide screen, on the pages with ◔, while this device shows it. Its reads are its own
|
|
106
|
+
* lease: they go on across navigation and end when it is hidden, the screen narrows, or it goes.
|
|
107
|
+
*/
|
|
108
|
+
export function UsageRail({ route }) {
|
|
109
|
+
const wide = useRailWidth();
|
|
110
|
+
const { open } = useStore(usageRailStore);
|
|
111
|
+
if (!wide || !open || !route || !RAIL_PANELS.includes(route.panel) || !usageOffered()) return null;
|
|
112
|
+
return html`<aside class="u-rail" aria-label=${t("view.usage.title_plain")}>
|
|
113
|
+
<div class="u-rail-head"><h2>${t("view.usage.title")}</h2>
|
|
114
|
+
<button type="button" class="icon-btn" onClick=${() => setUsageRail(false)} aria-label=${t("app.usageHide")} title=${t("app.usageHide")}><${Icon} name="close" size=${16} /></button></div>
|
|
115
|
+
<div class="u-rail-body"><${UsageBody} owner="usage-rail" /></div>
|
|
116
|
+
</aside>`;
|
|
117
|
+
}
|
|
118
|
+
|
|
76
119
|
/**
|
|
77
|
-
* ◔: the usage dialog (usage-dialog.js). It belongs to the navigation that opened it (`navKey`, #1408 §4; #1563
|
|
120
|
+
* ◔: on a wide screen, shows or hides the rail (#1673); otherwise the usage dialog (usage-dialog.js). It belongs to the navigation that opened it (`navKey`, #1408 §4; #1563
|
|
78
121
|
* review): another route, the same route again, another tab of the panel or a language switch closes it, and its
|
|
79
122
|
* reads and timer end with it — the dialog's lease is that navigation's.
|
|
80
123
|
*/
|
|
81
124
|
export function UsageButton({ navKey }) {
|
|
82
125
|
const [openFor, setOpenFor] = useState(null);
|
|
126
|
+
const wide = useRailWidth();
|
|
127
|
+
const rail = useStore(usageRailStore);
|
|
128
|
+
// Widening ends a dialog opened on the narrower screen — closed for good, not hidden while wide: narrowing again
|
|
129
|
+
// (after the rail was hidden, or with this device's rail hidden) never brings it back (#1679 review).
|
|
130
|
+
useEffect(() => { if (wide) setOpenFor(null); }, [wide]);
|
|
83
131
|
if (!usageOffered()) return null;
|
|
84
|
-
const open = openFor !== null && openFor === navKey;
|
|
85
|
-
|
|
132
|
+
const open = !wide && openFor !== null && openFor === navKey;
|
|
133
|
+
const click = () => (wide ? setUsageRail(!rail.open) : setOpenFor(navKey));
|
|
134
|
+
return html`<button type="button" class="btn btn-ghost btn-sm hd-usage" onClick=${click} aria-label=${t("app.usage")} title=${t("app.usage")}
|
|
135
|
+
aria-pressed=${wide ? (rail.open ? "true" : "false") : undefined}>
|
|
86
136
|
<${Icon} name="chart" size=${16} /><span class="hide-narrow" aria-hidden="true">${t("app.usage")}</span></button>
|
|
87
137
|
${open ? html`<${UsageDialog} key=${navKey} owner=${navKey} onClose=${() => setOpenFor(null)} />` : null}`;
|
|
88
138
|
}
|
|
@@ -7,13 +7,14 @@
|
|
|
7
7
|
// - a row opens the view you are in (View → View, everywhere else → Chat);
|
|
8
8
|
// - the list is never remounted on navigation, and only its own scrollTop follows the active row (#1515).
|
|
9
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";
|
|
10
|
+
import { html, useLayoutEffect, useRef, useState } from "./app-html.js";
|
|
11
11
|
import { t } from "./app-i18n.js";
|
|
12
12
|
import { appStore, createStore, useStore } from "./app-store.js";
|
|
13
13
|
import { chatPath, detailsPath, viewPath } from "./app-route.js";
|
|
14
14
|
import { Icon } from "./ui-icons.js";
|
|
15
15
|
import { Skeleton } from "./ui-states.js";
|
|
16
16
|
import { viewStore } from "./view-roster-store.js";
|
|
17
|
+
import { onPrefsChanged, prefInForce, prefsWritable, savePref } from "./app-prefs.js";
|
|
17
18
|
import "./view-strings.js";
|
|
18
19
|
|
|
19
20
|
/** view.* text with named values. */
|
|
@@ -23,7 +24,6 @@ function tn(key, values = {}) {
|
|
|
23
24
|
return s;
|
|
24
25
|
}
|
|
25
26
|
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
27
|
/** A row's instance name: `name` (status frames) or `instance_name` (View's roster). */
|
|
28
28
|
const nameOf = (it) => (it && (it.name != null ? it.name : it.instance_name)) || "";
|
|
29
29
|
|
|
@@ -101,14 +101,24 @@ export function instanceTooltip(it) {
|
|
|
101
101
|
return lines.join("\n");
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
// ──
|
|
104
|
+
// ── The order (drag), folded groups and filter: host preferences since #1665 (nav.*), cached in this browser ──
|
|
105
105
|
const SIDEBAR_ORDER_KEY = "agend_view_sidebar_order"; // View's key since #999: everyone's arrangement carries over
|
|
106
106
|
const COLLAPSED_KEY = "agend_instance_collapsed";
|
|
107
107
|
const FILTER_KEY = "agend_instance_filter";
|
|
108
|
+
const FAVORITES_KEY = "agend_favorites"; // #1664
|
|
109
|
+
/**
|
|
110
|
+
* A synced sidebar preference, as the JSON its reader parses: the value in force in memory (app-prefs.js — this
|
|
111
|
+
* page's own change, else the host's; a full or blocked storage never brings an old one back), else this browser's copy.
|
|
112
|
+
*/
|
|
113
|
+
function inForce(prefKey, lsKey) {
|
|
114
|
+
const p = prefInForce(prefKey);
|
|
115
|
+
if (!p) return stored(lsKey);
|
|
116
|
+
return p.value === undefined ? null : JSON.stringify(p.value);
|
|
117
|
+
}
|
|
108
118
|
function readOrder() {
|
|
109
119
|
const groups = new Map(), insts = new Map();
|
|
110
120
|
try {
|
|
111
|
-
const rows = JSON.parse(
|
|
121
|
+
const rows = JSON.parse(inForce("nav.order", SIDEBAR_ORDER_KEY) || "[]");
|
|
112
122
|
if (Array.isArray(rows)) for (const row of rows) {
|
|
113
123
|
if (row && row.item_type === "group" && typeof row.item_name === "string") groups.set(row.item_name, Number(row.sort_index) || 0);
|
|
114
124
|
else if (row && row.item_type === "instance" && typeof row.item_name === "string") insts.set(row.item_name, Number(row.sort_index) || 0);
|
|
@@ -117,15 +127,18 @@ function readOrder() {
|
|
|
117
127
|
return { groups, insts };
|
|
118
128
|
}
|
|
119
129
|
function readCollapsed() {
|
|
120
|
-
try { const a = JSON.parse(
|
|
130
|
+
try { const a = JSON.parse(inForce("nav.collapsed", COLLAPSED_KEY) || "[]"); return new Set(Array.isArray(a) ? a.filter((x) => typeof x === "string") : []); } catch { return new Set(); }
|
|
121
131
|
}
|
|
122
132
|
const strings = (a) => (Array.isArray(a) ? a.filter((x) => typeof x === "string") : []);
|
|
123
133
|
function readFilter() {
|
|
124
134
|
try {
|
|
125
|
-
const f = JSON.parse(
|
|
135
|
+
const f = JSON.parse(inForce("nav.filter", FILTER_KEY) || "{}");
|
|
126
136
|
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
137
|
} catch { return { q: "", status: [], cli: [] }; }
|
|
128
138
|
}
|
|
139
|
+
function readFavorites() {
|
|
140
|
+
try { const a = JSON.parse(inForce("nav.favorites", FAVORITES_KEY) || "[]"); return Array.isArray(a) ? [...new Set(a.filter((x) => typeof x === "string"))] : []; } catch { return []; }
|
|
141
|
+
}
|
|
129
142
|
/** Display order: this browser's saved order first, then new groups/instances by tag and name. */
|
|
130
143
|
export function computeOrder(roster, order) {
|
|
131
144
|
const groups = new Map();
|
|
@@ -159,29 +172,121 @@ function moveIn(arr, moving, target, before) {
|
|
|
159
172
|
}
|
|
160
173
|
|
|
161
174
|
/** 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() });
|
|
175
|
+
export const navStore = createStore({ order: readOrder(), collapsed: readCollapsed(), filter: readFilter(), favorites: readFavorites() });
|
|
163
176
|
/** 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() }); }
|
|
177
|
+
export function loadNavPrefs() { navStore.set({ order: readOrder(), collapsed: readCollapsed(), filter: readFilter(), favorites: readFavorites() }); }
|
|
165
178
|
/** Change the filter ({ q?, status?, cli? }): the same on every page, and kept by this browser. */
|
|
166
179
|
export function setNavFilter(patch) {
|
|
167
180
|
const filter = { ...navStore.get().filter, ...patch };
|
|
168
181
|
navStore.set({ filter });
|
|
169
|
-
|
|
182
|
+
savePref("nav.filter", { q: filter.q, status: filter.status, cli: filter.cli });
|
|
183
|
+
}
|
|
184
|
+
function setCollapsed(collapsed) { navStore.set({ collapsed }); savePref("nav.collapsed", [...collapsed]); }
|
|
185
|
+
// Another device rearranged, folded or filtered: this page's list follows (its cache is already updated).
|
|
186
|
+
onPrefsChanged((keys) => { if (keys.has("nav.order") || keys.has("nav.collapsed") || keys.has("nav.filter") || keys.has("nav.favorites")) loadNavPrefs(); });
|
|
187
|
+
|
|
188
|
+
// ── #1664: favorites — starred instances, pinned at the top in the order they were starred (or moved) ──
|
|
189
|
+
export const MAX_FAVORITES = 50;
|
|
190
|
+
/** The favorites that are instances now (`known`: the names listed): what is shown, and the base of every change — so
|
|
191
|
+
* a deleted instance is dropped, never sent back to the host (which would refuse it). */
|
|
192
|
+
function liveFavorites(known) { return navStore.get().favorites.filter((n) => known.has(n)); }
|
|
193
|
+
function setFavorites(next) { navStore.set({ favorites: next }); savePref("nav.favorites", next); }
|
|
194
|
+
/** Star or unstar `name`. False: not done (the list is full). */
|
|
195
|
+
export function toggleFavorite(name, known) {
|
|
196
|
+
const cur = liveFavorites(known);
|
|
197
|
+
if (cur.includes(name)) { setFavorites(cur.filter((n) => n !== name)); return true; }
|
|
198
|
+
if (cur.length >= MAX_FAVORITES || !known.has(name)) return false;
|
|
199
|
+
setFavorites([...cur, name]);
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
/** Move a favorite one place up (-1) or down (+1). */
|
|
203
|
+
export function moveFavorite(name, delta, known) {
|
|
204
|
+
const cur = liveFavorites(known);
|
|
205
|
+
const from = cur.indexOf(name), to = from + delta;
|
|
206
|
+
if (from < 0 || to < 0 || to >= cur.length) return;
|
|
207
|
+
cur.splice(to, 0, cur.splice(from, 1)[0]);
|
|
208
|
+
setFavorites(cur);
|
|
170
209
|
}
|
|
171
|
-
|
|
210
|
+
/** The Favorites group's key among the tag groups: no tag can be it (a group name is never a control character). */
|
|
211
|
+
const FAV_GROUP = "\u0000favorites";
|
|
172
212
|
function persistOrder(groupNames, instByGroup) {
|
|
173
213
|
const rows = [];
|
|
174
214
|
groupNames.forEach((g, i) => rows.push({ item_type: "group", item_name: g, sort_index: i, group_name: null }));
|
|
175
215
|
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
|
-
|
|
216
|
+
savePref("nav.order", rows);
|
|
177
217
|
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
218
|
}
|
|
179
219
|
|
|
220
|
+
// ── #1630: what stands for an instance (design: the comment on #1630) ──
|
|
221
|
+
// The server decides (the status frame's `avatar` / `accent`); the page only draws it. The anonymous View roster has
|
|
222
|
+
// neither: its rows get the first character. A picture is only ever a same-origin /ui/img/ path — never a platform's
|
|
223
|
+
// CDN (CSP img-src stays 'self'); one that fails to load falls back to the first character, never a broken image.
|
|
224
|
+
|
|
225
|
+
const GRAPHEMES = typeof Intl !== "undefined" && typeof Intl.Segmenter === "function" ? new Intl.Segmenter(undefined, { granularity: "grapheme" }) : null;
|
|
226
|
+
/** The first user-perceived character, upper-cased where that means anything. */
|
|
227
|
+
export function firstCharacter(text) {
|
|
228
|
+
const s = String(text == null ? "" : text).trim();
|
|
229
|
+
if (!s) return "?";
|
|
230
|
+
if (GRAPHEMES) for (const { segment } of GRAPHEMES.segment(s)) return segment.toLocaleUpperCase();
|
|
231
|
+
return Array.from(s)[0].toLocaleUpperCase();
|
|
232
|
+
}
|
|
233
|
+
/** What a row is called: the display name when it has one, else the instance name (shown on hover and in Details). */
|
|
234
|
+
export function labelOf(it) {
|
|
235
|
+
const d = it && typeof it.display_name === "string" ? it.display_name.trim() : "";
|
|
236
|
+
return d || nameOf(it);
|
|
237
|
+
}
|
|
238
|
+
// One path segment under /ui/img/<kind>/ — no further slash or backslash, and not a dot segment ("." / ".." or their
|
|
239
|
+
// %2e spellings, which a browser resolves too): the picture can only ever be one of those routes (#1642 review).
|
|
240
|
+
const sameOrigin = (src) => typeof src === "string"
|
|
241
|
+
&& /^\/ui\/img\/(bot|avatar|emoji)\/(?!(?:\.|%2[eE]){1,2}(?:\?|$))[^/\\?#\s]+(\?v=[0-9A-Za-z%._-]*)?$/.test(src);
|
|
242
|
+
/** The avatar to draw: the server's, if it is one the page may load; else the first character. */
|
|
243
|
+
export function avatarOf(it) {
|
|
244
|
+
const a = it && it.avatar;
|
|
245
|
+
if (a && a.kind === "image" && sameOrigin(a.src)) return { kind: "image", src: a.src };
|
|
246
|
+
if (a && a.kind === "emoji" && sameOrigin(a.src)) return { kind: "emoji", src: a.src, alt: typeof a.alt === "string" ? a.alt : "" };
|
|
247
|
+
if (a && a.kind === "emoji" && typeof a.text === "string" && a.text.trim()) return { kind: "emoji", text: a.text.trim() };
|
|
248
|
+
return { kind: "initial", text: a && a.kind === "initial" && typeof a.text === "string" && a.text ? a.text : firstCharacter(labelOf(it)) };
|
|
249
|
+
}
|
|
250
|
+
const ACCENT = /^#[0-9a-f]{6}$/i;
|
|
251
|
+
/** The status dot on the avatar: needs you, crashed, paused, stopped, looks stuck, working, idle — colour AND a label. */
|
|
252
|
+
export function dotState(it, exec, awaiting) {
|
|
253
|
+
if (awaiting != null) return "needs";
|
|
254
|
+
if (!it || it.status === "crashed") return "crashed";
|
|
255
|
+
if (it.status === "paused") return "paused";
|
|
256
|
+
if (it.status !== "running") return "stopped";
|
|
257
|
+
return exec === "stuck" ? "stuck" : exec === "working" ? "working" : "idle";
|
|
258
|
+
}
|
|
259
|
+
const DOT = {
|
|
260
|
+
needs: ["warn", () => t("app.needsYou")], crashed: ["bad", () => t("app.statusCrashed")], paused: ["off hollow", () => t("app.statusPaused")],
|
|
261
|
+
stopped: ["off", () => t("app.statusStopped")], stuck: ["warn", () => t("app.statusStuck")], working: ["busy", () => t("app.statusWorking")],
|
|
262
|
+
idle: ["ok", () => t("app.statusIdle")],
|
|
263
|
+
};
|
|
264
|
+
export function dotLabel(state) { return (DOT[state] || DOT.stopped)[1](); }
|
|
265
|
+
|
|
266
|
+
/** An instance's avatar with its status dot. The ring (its bot's colour) is a CSS custom property set through the CSSOM. */
|
|
267
|
+
export function InstanceAvatar({ it, state }) {
|
|
268
|
+
const a = avatarOf(it);
|
|
269
|
+
const [broken, setBroken] = useState("");
|
|
270
|
+
const el = useRef(null);
|
|
271
|
+
const accent = typeof it.accent === "string" && ACCENT.test(it.accent) && a.kind === "image" ? it.accent : "";
|
|
272
|
+
useLayoutEffect(() => {
|
|
273
|
+
const node = el.current;
|
|
274
|
+
if (!node || !node.style) return;
|
|
275
|
+
if (accent) node.style.setProperty("--ring", accent); else node.style.removeProperty("--ring");
|
|
276
|
+
}, [accent]);
|
|
277
|
+
const src = a.kind !== "initial" && a.src && broken !== a.src ? a.src : "";
|
|
278
|
+
const text = a.kind === "emoji" && a.text ? a.text : a.kind === "initial" ? a.text : firstCharacter(labelOf(it));
|
|
279
|
+
const [cls] = DOT[state] || DOT.stopped;
|
|
280
|
+
const label = dotLabel(state);
|
|
281
|
+
return html`<span ref=${el} class=${`av${accent ? " ring" : ""}${a.kind === "emoji" ? " av-emoji" : ""}`}>
|
|
282
|
+
${src ? html`<img src=${src} alt="" decoding="async" onError=${() => setBroken(src)} />` : html`<span class="av-text" aria-hidden="true">${text}</span>`}
|
|
283
|
+
<span class=${`av-dot dot ${cls}`} role="img" aria-label=${label} title=${label}></span></span>`;
|
|
284
|
+
}
|
|
285
|
+
|
|
180
286
|
// ── The list ──
|
|
181
287
|
|
|
182
288
|
const statusText = (facet) => (facet === "needs" ? t("app.needsYou") : facet === "working" ? t("app.statusWorking") : facet === "idle" ? t("app.statusIdle")
|
|
183
289
|
: 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
290
|
|
|
186
291
|
/**
|
|
187
292
|
* The sidebar's instance list. `route`: the page's (which instance is open, and which view a row opens). `items`: the
|
|
@@ -194,6 +299,9 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
|
|
|
194
299
|
const filterRef = useRef(null);
|
|
195
300
|
const list = useRef(null);
|
|
196
301
|
const drag = useRef(null);
|
|
302
|
+
// #1664: "☆ Edit" — the stars on every row (on a phone the only way to them) and ↑ ↓ on the favorites — only on the
|
|
303
|
+
// full list, as drag: in a filtered one the row above may not be the one above in the list (#1677 review).
|
|
304
|
+
const [editing, setEditing] = useState(false);
|
|
197
305
|
// Where a row goes: the view the page is in (#1523 §3.1). View → View; Details, or a Fleet tab (Q4 = A), → Details;
|
|
198
306
|
// everywhere else → Chat. The anonymous reader only ever has View.
|
|
199
307
|
const rowPath = viewOnly || (route && route.panel === "view") ? viewPath
|
|
@@ -204,9 +312,22 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
|
|
|
204
312
|
const byName = new Map(items.map((it) => [nameOf(it), it]));
|
|
205
313
|
const facetOf = (name) => statusFacet(byName.get(name), own(exec, name) ? exec[name] : null, own(awaiting, name) ? awaiting[name] : null);
|
|
206
314
|
const { groupNames, instByGroup } = computeOrder(items, nav.order);
|
|
315
|
+
// #1664: the favorites that are instances now, as a group of their own on top; the other groups keep the rest, in
|
|
316
|
+
// their order. Changing them: a signed-in page that may write preferences (not the public link, not the anonymous
|
|
317
|
+
// reader — they still see the list).
|
|
318
|
+
const known = new Set(byName.keys());
|
|
319
|
+
const favs = nav.favorites.filter((n) => known.has(n));
|
|
320
|
+
const favSet = new Set(favs);
|
|
321
|
+
const canStar = !viewOnly && prefsWritable();
|
|
322
|
+
const shownGroups = favs.length ? [FAV_GROUP, ...groupNames] : groupNames;
|
|
323
|
+
const shownInsts = new Map([...instByGroup].map(([g, names]) => [g, names.filter((n) => !favSet.has(n))]));
|
|
324
|
+
if (favs.length) shownInsts.set(FAV_GROUP, favs);
|
|
207
325
|
const f = nav.filter;
|
|
208
|
-
const shown = filterSidebar(
|
|
326
|
+
const shown = filterSidebar(shownGroups, shownInsts, byName, f.q, backendLabel, { status: f.status, cli: f.cli, statusOf: facetOf });
|
|
209
327
|
const backends = [...new Set(items.map((it) => it.backend).filter((b) => typeof b === "string" && b))].sort((a, b) => backendLabel(a).localeCompare(backendLabel(b)));
|
|
328
|
+
// #1630: a display name two rows share would tell them apart from nothing — only then does the row keep its name too.
|
|
329
|
+
const labelCount = new Map();
|
|
330
|
+
for (const it of items) { const l = labelOf(it); labelCount.set(l, (labelCount.get(l) || 0) + 1); }
|
|
210
331
|
const toggle = (g) => { const c = new Set(navStore.get().collapsed); if (c.has(g)) c.delete(g); else c.add(g); setCollapsed(c); };
|
|
211
332
|
// From the store, not this render's copy: two clicks before the next render must both count.
|
|
212
333
|
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] }); };
|
|
@@ -244,34 +365,50 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
|
|
|
244
365
|
<div class="nav-facet-list" role="group" aria-label=${label}>${values.map((value) => html`<label key=${value} class="nav-chip">
|
|
245
366
|
<input type="checkbox" checked=${f[key].includes(value)} onChange=${() => flip(key, value)} /><span>${text(value)}</span></label>`)}</div>
|
|
246
367
|
</details>`;
|
|
368
|
+
const row = (name, g, i, count) => {
|
|
369
|
+
const it = byName.get(name);
|
|
370
|
+
const label = labelOf(it);
|
|
371
|
+
// The instance name under a display name only when another row has the same display name.
|
|
372
|
+
const twin = label !== name && (labelCount.get(label) || 0) > 1 ? name : "";
|
|
373
|
+
const facet = facetOf(name);
|
|
374
|
+
const waiting = own(awaiting, name) ? awaiting[name] : null;
|
|
375
|
+
const on = name === active;
|
|
376
|
+
const fav = g === FAV_GROUP, starred = favSet.has(name);
|
|
377
|
+
const full = !starred && favs.length >= MAX_FAVORITES;
|
|
378
|
+
return html`<li key=${name} class="v-li"><a class=${`inst v-inst${on ? " active" : ""}`} href=${rowPath(name)} title=${instanceTooltip(it)}
|
|
379
|
+
aria-current=${on ? "page" : undefined} onClick=${onPick} ...${fav ? {} : dragProps({ type: "instance", name, group: g })}>
|
|
380
|
+
<${InstanceAvatar} it=${it} state=${dotState(it, own(exec, name) ? exec[name] : null, waiting)} />
|
|
381
|
+
<span class="inst-text"><span class="inst-name">${label}</span>
|
|
382
|
+
${twin || facet === "needs" ? html`<span class="inst-sub">${twin ? html`<span class="inst-alias inst-id">${twin}</span>` : null}
|
|
383
|
+
${facet === "needs" ? html`<span class="badge-await" title=${waiting || t("app.approxNote")}>${t("app.needsYou")}</span>` : null}</span>` : null}</span>
|
|
384
|
+
<span class="v-meta">${it.context_pct != null ? `${Math.round(it.context_pct)}%` : ""}</span>
|
|
385
|
+
<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>
|
|
386
|
+
</a>${fav && editing && canStar ? html`<button type="button" class="icon-btn inst-move" aria-label=${tn("favUp", { name: label })}
|
|
387
|
+
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"
|
|
388
|
+
aria-label=${tn("favDown", { name: label })} title=${shown.active ? tn("favFiltered") : tn("favDown", { name: label })} disabled=${shown.active || i === count - 1}
|
|
389
|
+
onClick=${() => { if (!shown.active) moveFavorite(name, 1, known); }}><${Icon} name="down" size=${14} /></button>` : null}${canStar
|
|
390
|
+
? html`<button type="button" class=${`icon-btn inst-star${starred ? " on" : ""}`} aria-pressed=${starred ? "true" : "false"} disabled=${full}
|
|
391
|
+
aria-label=${tn(starred ? "favRemove" : "favAdd", { name: label })} title=${full ? tn("favFull", { max: MAX_FAVORITES }) : tn(starred ? "favRemove" : "favAdd", { name: label })}
|
|
392
|
+
onClick=${() => toggleFavorite(name, known)}><${Icon} name="star" size=${14} /></button>` : null}</li>`;
|
|
393
|
+
};
|
|
247
394
|
// The list scrolls; the filter under it stays in view (#999), so it sits outside the scrolling section.
|
|
248
|
-
return html`<div class
|
|
395
|
+
return html`<div class=${`view-roster inst-nav${editing && canStar ? " fav-editing" : ""}`}>
|
|
249
396
|
<div class="side-section" id="instanceList" ref=${list}>
|
|
250
|
-
<h2 class="side-label">${tn("instancesLabel")}</h2>
|
|
397
|
+
<div class="inst-head"><h2 class="side-label">${tn("instancesLabel")}</h2>
|
|
398
|
+
${canStar && items.length ? html`<button type="button" class="chip-btn fav-edit" aria-pressed=${editing ? "true" : "false"} onClick=${() => setEditing(!editing)}>
|
|
399
|
+
${editing ? tn("favDone") : html`<${Icon} name="star" size=${12} />${tn("favEdit")}`}</button>` : null}</div>
|
|
251
400
|
${!loaded ? html`<${Skeleton} lines=${4} />` : !items.length ? html`<p class="side-empty">${t("app.noInstances")}</p>` : null}
|
|
252
401
|
${shown.groups.map(({ group: g, names }) => {
|
|
402
|
+
// #1664: Favorites — always open, not dragged (↑ ↓ in Edit), named in the page's language.
|
|
403
|
+
if (g === FAV_GROUP) return html`<div key=${g} class="v-group v-fav" role="group" aria-label=${tn("favorites")}>
|
|
404
|
+
<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>
|
|
405
|
+
<ul class="inst-list">${names.map((name, i) => row(name, g, i, names.length))}</ul></div>`;
|
|
253
406
|
const folded = nav.collapsed.has(g);
|
|
254
407
|
return html`<div key=${g} class="v-group">
|
|
255
408
|
<button type="button" class="v-group-head" aria-expanded=${folded ? "false" : "true"} onClick=${() => toggle(g)}
|
|
256
409
|
title=${tn(folded ? "expandGroup" : "collapseGroup", { group: g })} ...${dragProps({ type: "group", name: g })}>
|
|
257
410
|
<${Icon} name="chevron" size=${14} cls=${folded ? "caret folded" : "caret"} /><span class="grow">${g}</span><span class="count">${names.length}</span></button>
|
|
258
|
-
${folded ? null : html`<ul class="inst-list">${names.map((name) =>
|
|
259
|
-
const it = byName.get(name);
|
|
260
|
-
const alias = typeof it.display_name === "string" && it.display_name.trim() && it.display_name.trim() !== name ? it.display_name.trim() : "";
|
|
261
|
-
const facet = facetOf(name);
|
|
262
|
-
const waiting = own(awaiting, name) ? awaiting[name] : null;
|
|
263
|
-
const on = name === active;
|
|
264
|
-
return html`<li key=${name}><a class=${`inst v-inst${on ? " active" : ""}`} href=${rowPath(name)} title=${instanceTooltip(it)}
|
|
265
|
-
aria-current=${on ? "page" : undefined} onClick=${onPick} ...${dragProps({ type: "instance", name, group: g })}>
|
|
266
|
-
<span class=${`dot ${dotClass(facet, own(exec, name) ? exec[name] : null)}`} aria-hidden="true"></span>
|
|
267
|
-
<span class="inst-text"><span class="inst-name">${name}</span>
|
|
268
|
-
${alias || facet === "needs" ? html`<span class="inst-sub">${alias ? html`<span class="inst-alias">${alias}</span>` : null}
|
|
269
|
-
${facet === "needs" ? html`<span class="badge-await" title=${waiting || t("app.approxNote")}>${t("app.needsYou")}</span>` : null}</span>` : null}
|
|
270
|
-
<span class="sr-only">${statusText(facet)}</span></span>
|
|
271
|
-
<span class="v-meta">${it.context_pct != null ? `${Math.round(it.context_pct)}%` : ""}</span>
|
|
272
|
-
<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>
|
|
273
|
-
</a></li>`;
|
|
274
|
-
})}</ul>`}
|
|
411
|
+
${folded ? null : html`<ul class="inst-list">${names.map((name, i) => row(name, g, i, names.length))}</ul>`}
|
|
275
412
|
</div>`;
|
|
276
413
|
})}
|
|
277
414
|
${loaded && shown.active && !shown.groups.length ? html`<p class="side-empty">${tn("filterNone")}</p>` : null}
|
|
@@ -201,7 +201,13 @@ function Terminal({ name, lease, density }) {
|
|
|
201
201
|
const term = useRef(null), pre = useRef(null);
|
|
202
202
|
const grid = useRef({ cols: 0, rows: 0, applied: 0, key: "" });
|
|
203
203
|
const [problem, setProblem] = useState(null);
|
|
204
|
+
// #1674: the text size as of this render. The pane refresh and the resize handlers below are set up once per mount
|
|
205
|
+
// and keep the fit() of that render; reading the size here, not from that render's props, keeps them on the current
|
|
206
|
+
// choice (they used to put the mount-time size back within one 800 ms refresh).
|
|
207
|
+
const dens = useRef(density);
|
|
208
|
+
dens.current = density;
|
|
204
209
|
const fit = () => {
|
|
210
|
+
const density = dens.current;
|
|
205
211
|
const g = grid.current, box = term.current, p = pre.current;
|
|
206
212
|
if (!box || !p || g.cols < 1 || g.rows < 1) return;
|
|
207
213
|
const cs = getComputedStyle(box);
|
package/dist/ui/shared/signin.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// (a SameSite=Strict cookie is not sent on a navigation that came from a chat app), probe the session
|
|
5
5
|
// from *here* — a same-site request does carry the cookie — and carry on to the page that was asked
|
|
6
6
|
// for; (2) otherwise take the one-time code and exchange it. A browser that signed in before with a code from
|
|
7
|
-
// /
|
|
7
|
+
// /web (a returning device, #1570) is also offered "Send me a new code": the server sends it privately to the
|
|
8
8
|
// person who received that earlier code — the page never learns who that is.
|
|
9
9
|
(() => {
|
|
10
10
|
"use strict";
|
|
@@ -13,24 +13,24 @@
|
|
|
13
13
|
en: {
|
|
14
14
|
checking: "Checking your session…", title: "Sign in to AgEnD", lead: "Enter the one-time code from your chat channel.",
|
|
15
15
|
label: "One-time code", submit: "Sign in", working: "Signing in…",
|
|
16
|
-
hint: "Send /
|
|
17
|
-
refused: "That code did not work — it may be wrong, expired or already used. Ask for a new one with /
|
|
16
|
+
hint: "Send /web to your AgEnD bot, or run “agend web --code” on the host. A code works once and expires in 5 minutes.",
|
|
17
|
+
refused: "That code did not work — it may be wrong, expired or already used. Ask for a new one with /web.",
|
|
18
18
|
paused: "Too many wrong codes — sign-in is paused for a few minutes.",
|
|
19
19
|
network: "Could not reach AgEnD. Check the connection and try again.",
|
|
20
20
|
resend: "Send me a new code", resent: "A new code was sent to you privately in chat. Enter it above.",
|
|
21
21
|
resend_wait: "A code was asked for a moment ago — wait a little and try again.",
|
|
22
|
-
resend_refused: "A new code could not be sent. Ask for one with /
|
|
22
|
+
resend_refused: "A new code could not be sent. Ask for one with /web.",
|
|
23
23
|
},
|
|
24
24
|
"zh-TW": {
|
|
25
25
|
checking: "正在確認登入狀態…", title: "登入 AgEnD", lead: "請輸入聊天頻道給你的一次性登入碼。",
|
|
26
26
|
label: "一次性登入碼", submit: "登入", working: "登入中…",
|
|
27
|
-
hint: "在 AgEnD bot 傳 /
|
|
28
|
-
refused: "登入碼無效:可能輸入錯誤、已過期或已使用。請用 /
|
|
27
|
+
hint: "在 AgEnD bot 傳 /web,或在主機執行「agend web --code」。登入碼只能用一次,5 分鐘內有效。",
|
|
28
|
+
refused: "登入碼無效:可能輸入錯誤、已過期或已使用。請用 /web 再要一個。",
|
|
29
29
|
paused: "錯誤次數過多,登入暫停幾分鐘。",
|
|
30
30
|
network: "連不上 AgEnD,請檢查連線後再試。",
|
|
31
31
|
resend: "傳一組新的登入碼給我", resent: "新的登入碼已私訊給你,請在上方輸入。",
|
|
32
32
|
resend_wait: "剛剛才要過登入碼,請稍候再試。",
|
|
33
|
-
resend_refused: "無法傳送新的登入碼。請用 /
|
|
33
|
+
resend_refused: "無法傳送新的登入碼。請用 /web 再要一個。",
|
|
34
34
|
},
|
|
35
35
|
};
|
|
36
36
|
const lang = /^zh/i.test(navigator.language || "") ? "zh-TW" : "en";
|
package/dist/ui/shared/theme.js
CHANGED
|
@@ -1,29 +1,61 @@
|
|
|
1
1
|
// Light / dark for a panel (#1307). Loaded from /assets/theme.js in <head>, before the page paints, so a chosen theme
|
|
2
2
|
// never flashes the other one first. "system" (the default) follows the device's prefers-color-scheme through CSS;
|
|
3
|
-
// "light" or "dark"
|
|
3
|
+
// "light" or "dark" lives in localStorage, like agend_lang.
|
|
4
|
+
//
|
|
5
|
+
// #1665: the choice is kept on the host and follows the person between devices. The signed-in app's page carries the
|
|
6
|
+
// host's values in <html data-prefs>: the host's theme is what this page paints and holds, whatever this browser's
|
|
7
|
+
// storage does (the copy kept there is best effort — a full or blocked storage changes nothing on screen). Nothing
|
|
8
|
+
// chosen on the host: a device already in step with it (app-prefs.js's migration done) takes the default; one not yet
|
|
9
|
+
// in step keeps its own until it has uploaded it. Without data-prefs (the sign-in page, the anonymous View reader) this
|
|
10
|
+
// browser's copy decides, as before.
|
|
4
11
|
//
|
|
5
12
|
// A page's tokens follow the pattern: dark under :root, light under @media (prefers-color-scheme: light) for
|
|
6
13
|
// :root:not([data-theme="dark"]), and again under :root[data-theme="light"].
|
|
7
14
|
(function () {
|
|
8
15
|
"use strict";
|
|
9
16
|
var KEY = "agend_theme";
|
|
17
|
+
var SYNCED_FLAG = "agend_prefs_synced";
|
|
10
18
|
function valid(v) { return v === "light" || v === "dark" ? v : "system"; }
|
|
11
19
|
function read() {
|
|
12
20
|
try { return valid(localStorage.getItem(KEY)); } catch (e) { return "system"; } // storage blocked: follow the device
|
|
13
21
|
}
|
|
22
|
+
function cache(v) {
|
|
23
|
+
try { if (v === "system") localStorage.removeItem(KEY); else localStorage.setItem(KEY, v); } catch (e) { /* this page only */ }
|
|
24
|
+
}
|
|
14
25
|
function apply(v) {
|
|
15
26
|
var root = document.documentElement;
|
|
16
27
|
if (v === "system") root.removeAttribute("data-theme");
|
|
17
28
|
else root.setAttribute("data-theme", v);
|
|
18
29
|
}
|
|
19
|
-
|
|
30
|
+
/** The host's choice for this page: a theme, or null — none known here (no data-prefs, or not in step yet). */
|
|
31
|
+
function hostChoice() {
|
|
32
|
+
var d = null;
|
|
33
|
+
try { d = JSON.parse(document.documentElement.getAttribute("data-prefs") || "null"); } catch (e) { d = null; }
|
|
34
|
+
if (!d || typeof d !== "object" || !d.values || typeof d.values !== "object") return null;
|
|
35
|
+
var v = d.values.theme;
|
|
36
|
+
if (v === "light" || v === "dark" || v === "system") return v;
|
|
37
|
+
var inStep = false;
|
|
38
|
+
try { inStep = localStorage.getItem(SYNCED_FLAG) === "1"; } catch (e) { inStep = false; }
|
|
39
|
+
return inStep ? "system" : null;
|
|
40
|
+
}
|
|
41
|
+
var current = hostChoice();
|
|
42
|
+
if (current !== null) cache(current); else current = read();
|
|
43
|
+
apply(current);
|
|
44
|
+
var listeners = [];
|
|
20
45
|
window.AgendTheme = {
|
|
21
|
-
|
|
22
|
-
|
|
46
|
+
/** The theme this page shows. */
|
|
47
|
+
get: function () { return current; },
|
|
48
|
+
/** Choose "system", "light" or "dark" (this browser's copy; app-prefs.js keeps the host's); applied now even when it cannot be stored. */
|
|
23
49
|
set: function (v) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
apply(
|
|
50
|
+
current = valid(v);
|
|
51
|
+
cache(current);
|
|
52
|
+
apply(current);
|
|
53
|
+
for (var i = 0; i < listeners.length; i++) { try { listeners[i](current); } catch (e) { /* one listener's error stops no other */ } }
|
|
54
|
+
},
|
|
55
|
+
/** Call `fn` with the theme whenever it changes on this page (an open Preferences shows it); returns the unsubscribe. */
|
|
56
|
+
onChange: function (fn) {
|
|
57
|
+
listeners.push(fn);
|
|
58
|
+
return function () { var i = listeners.indexOf(fn); if (i >= 0) listeners.splice(i, 1); };
|
|
27
59
|
},
|
|
28
60
|
};
|
|
29
61
|
})();
|
|
@@ -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
|
+
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"/>`,
|
|
40
41
|
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"/>`,
|
|
41
42
|
check: () => html`<path d="M20 6L9 17l-5-5"/>`,
|
|
42
43
|
inbox: () => html`<path d="M22 12h-6l-2 3h-4l-2-3H2"/><path d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/>`,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// #1523 N3 (§3.4): the AI usage dialog, opened from every page's header (◔). Moved from panel-view.js. Public
|
|
2
2
|
// (/assets/): View's anonymous reader opens it too. It reads /api/ai-usage every 60 s while open (passive, #1374).
|
|
3
|
+
// #1673: its body (UsageBody) is also the wide screen's right rail (header-tools.js UsageRail) — one read, one view.
|
|
3
4
|
import { html, useEffect, useLayoutEffect, useMemo, useRef, useState } from "./app-html.js";
|
|
4
5
|
import { t } from "./app-i18n.js";
|
|
5
6
|
import { useLease } from "./app-ctx.js";
|
|
@@ -73,9 +74,12 @@ export function updatedAgo(fetchedAt, now = Date.now()) {
|
|
|
73
74
|
return tn("usageUpdatedAgo", { age: min < 60 ? tn("usageMinutes", { n: min }) : tn("usageHours", { n: Math.floor(min / 60) }) });
|
|
74
75
|
}
|
|
75
76
|
|
|
76
|
-
/**
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
/**
|
|
78
|
+
* The usage read and its rows: the dialog's body and the rail's (#1673). `owner`: its lease's key — the reads and the
|
|
79
|
+
* minute's timer live and end with it.
|
|
80
|
+
*/
|
|
81
|
+
export function UsageBody({ owner }) {
|
|
82
|
+
const lease = useLease(owner);
|
|
79
83
|
const [data, setData] = useState(undefined); // undefined: loading; null: failed
|
|
80
84
|
const [order, setOrder] = useState(() => { try { const a = JSON.parse(stored(USAGE_ORDER_KEY) || "[]"); return Array.isArray(a) ? a.filter((x) => typeof x === "string") : []; } catch { return []; } });
|
|
81
85
|
const reads = useMemo(() => readStream(lease), [lease]);
|
|
@@ -136,5 +140,10 @@ export function UsageDialog({ onClose, owner = "" }) {
|
|
|
136
140
|
<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>
|
|
137
141
|
<button type="button" class="btn btn-sm" onClick=${() => load(true)}>${tn("usageRefresh")}</button></div>`;
|
|
138
142
|
}
|
|
139
|
-
return
|
|
143
|
+
return content;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** `owner`: the navigation that opened it — its reads and the minute's timer live and end with it. */
|
|
147
|
+
export function UsageDialog({ onClose, owner = "" }) {
|
|
148
|
+
return html`<${Dialog} title=${tn("usage.title")} onClose=${onClose} wide=${true}><${UsageBody} owner=${`usage-dialog:${owner}`} /></${Dialog}>`;
|
|
140
149
|
}
|
|
@@ -18,7 +18,7 @@ register("view", {
|
|
|
18
18
|
"displayName": "Display name",
|
|
19
19
|
"role": "Role",
|
|
20
20
|
"description": "Description",
|
|
21
|
-
"saveHint": "Saving needs a signed-in session. Send /
|
|
21
|
+
"saveHint": "Saving needs a signed-in session. Send /web to your AgEnD bot for a sign-in code.",
|
|
22
22
|
"helpTitle": "📖 Help",
|
|
23
23
|
"help1": "Pick an instance in the list: its live terminal shows here.",
|
|
24
24
|
"help2": "Click a group's name to fold it or open it again.",
|
|
@@ -164,7 +164,16 @@ register("view", {
|
|
|
164
164
|
"profileTitle": "Edit {name}'s profile",
|
|
165
165
|
"avatar": "Avatar",
|
|
166
166
|
"collapseProfile": "Hide the profile",
|
|
167
|
-
"instancesLabel": "Instances"
|
|
167
|
+
"instancesLabel": "Instances",
|
|
168
|
+
"favorites": "Favorites",
|
|
169
|
+
"favAdd": "Add {name} to favorites",
|
|
170
|
+
"favRemove": "Remove {name} from favorites",
|
|
171
|
+
"favEdit": "Edit",
|
|
172
|
+
"favDone": "Done",
|
|
173
|
+
"favUp": "Move {name} up",
|
|
174
|
+
"favDown": "Move {name} down",
|
|
175
|
+
"favFull": "Favorites are full ({max})",
|
|
176
|
+
"favFiltered": "Clear the filter to reorder favorites"
|
|
168
177
|
},
|
|
169
178
|
"zh-TW": {
|
|
170
179
|
"editProfile": "編輯個人資料",
|
|
@@ -180,7 +189,7 @@ register("view", {
|
|
|
180
189
|
"displayName": "顯示名稱",
|
|
181
190
|
"role": "角色",
|
|
182
191
|
"description": "描述",
|
|
183
|
-
"saveHint": "儲存需要已登入的 session。請在 AgEnD bot 傳 /
|
|
192
|
+
"saveHint": "儲存需要已登入的 session。請在 AgEnD bot 傳 /web 取得登入碼。",
|
|
184
193
|
"helpTitle": "📖 使用說明",
|
|
185
194
|
"help1": "在清單裡選一個 instance:它的即時終端畫面會顯示在這裡。",
|
|
186
195
|
"help2": "點群組名稱可以收合或展開。",
|
|
@@ -326,6 +335,15 @@ register("view", {
|
|
|
326
335
|
"profileTitle": "編輯 {name} 的個人資料",
|
|
327
336
|
"avatar": "頭像",
|
|
328
337
|
"collapseProfile": "隱藏個人資料",
|
|
329
|
-
"instancesLabel": "Instances"
|
|
338
|
+
"instancesLabel": "Instances",
|
|
339
|
+
"favorites": "最愛",
|
|
340
|
+
"favAdd": "把 {name} 加入最愛",
|
|
341
|
+
"favRemove": "把 {name} 從最愛移除",
|
|
342
|
+
"favEdit": "編輯",
|
|
343
|
+
"favDone": "完成",
|
|
344
|
+
"favUp": "把 {name} 上移",
|
|
345
|
+
"favDown": "把 {name} 下移",
|
|
346
|
+
"favFull": "最愛已滿({max} 個)",
|
|
347
|
+
"favFiltered": "清除篩選後才能調整最愛順序"
|
|
330
348
|
}
|
|
331
349
|
});
|