@songsid/agend 2.3.0-beta.2 → 2.3.0-beta.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/dist/agent-whats-new.js +2 -0
- package/dist/agent-whats-new.js.map +1 -1
- package/dist/auth-api.d.ts +30 -1
- package/dist/auth-api.js +42 -12
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/claude-code.d.ts +2 -2
- package/dist/backend/claude-code.js +5 -4
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/types.d.ts +6 -0
- package/dist/backend/types.js +15 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/backend-availability.d.ts +120 -0
- package/dist/backend-availability.js +396 -0
- package/dist/backend-availability.js.map +1 -0
- package/dist/backend-command.d.ts +21 -3
- package/dist/backend-command.js +23 -4
- package/dist/backend-command.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +7 -0
- package/dist/channel/adapters/discord.js +35 -0
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/mcp-tools.js +8 -4
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/types.d.ts +6 -0
- package/dist/classic-channel-manager.d.ts +8 -3
- package/dist/classic-channel-manager.js +10 -3
- package/dist/classic-channel-manager.js.map +1 -1
- package/dist/config-validator.js +73 -2
- package/dist/config-validator.js.map +1 -1
- package/dist/delivery-outbox.js +5 -0
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/fleet-context.d.ts +10 -0
- package/dist/fleet-manager.d.ts +238 -5
- package/dist/fleet-manager.js +878 -71
- package/dist/fleet-manager.js.map +1 -1
- package/dist/fleet-traffic.d.ts +38 -0
- package/dist/fleet-traffic.js +96 -0
- package/dist/fleet-traffic.js.map +1 -0
- package/dist/general-knowledge/skills/credential-profiles/SKILL.md +13 -5
- package/dist/general-knowledge/skills/multi-channel/SKILL.md +1 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +56 -11
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +398 -0
- package/dist/human-delivery-receipts.js +2 -1
- package/dist/human-delivery-receipts.js.map +1 -1
- package/dist/locale.js +48 -8
- package/dist/locale.js.map +1 -1
- package/dist/memory-hogs.d.ts +27 -0
- package/dist/memory-hogs.js +124 -0
- package/dist/memory-hogs.js.map +1 -0
- package/dist/outbound-handlers.d.ts +2 -0
- package/dist/outbound-handlers.js +40 -1
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +92 -0
- package/dist/outbound-schemas.js +55 -1
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/public-web-gateway.js +1 -1
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/public-web-link.d.ts +9 -0
- package/dist/public-web-link.js +8 -0
- package/dist/public-web-link.js.map +1 -1
- package/dist/quickstart-api.d.ts +10 -1
- package/dist/quickstart-api.js +12 -4
- package/dist/quickstart-api.js.map +1 -1
- package/dist/reply-buttons-controller.d.ts +2 -0
- package/dist/reply-buttons-controller.js +1 -1
- package/dist/reply-buttons-controller.js.map +1 -1
- package/dist/tmux-manager.d.ts +5 -0
- package/dist/tmux-manager.js +16 -0
- package/dist/tmux-manager.js.map +1 -1
- package/dist/tool-permissions.js +4 -0
- package/dist/tool-permissions.js.map +1 -1
- package/dist/topic-commands.d.ts +5 -0
- package/dist/topic-commands.js +16 -0
- package/dist/topic-commands.js.map +1 -1
- package/dist/types.d.ts +25 -1
- package/dist/ui/app-effects.js +33 -9
- package/dist/ui/chat-effects.js +62 -4
- package/dist/ui/chat-strings.js +40 -4
- package/dist/ui/chat-thread.js +17 -6
- package/dist/ui/effect-trial-panel.js +19 -16
- package/dist/ui/fleet-canvas-layout.js +111 -0
- package/dist/ui/fleet-canvas.js +489 -0
- package/dist/ui/fleet-org.js +27 -12
- package/dist/ui/fleet-traffic.js +224 -0
- package/dist/ui/panel-chat.js +12 -5
- package/dist/ui/panel-details.js +2 -1
- package/dist/ui/panel-fleet.js +32 -11
- package/dist/ui/panel-settings.js +6 -4
- package/dist/ui/preview.js +3 -2
- package/dist/ui/settings-appearance.js +34 -18
- package/dist/ui/settings-catalog.js +89 -0
- package/dist/ui/settings-dialogs.js +40 -12
- package/dist/ui/settings-effects.js +4 -3
- package/dist/ui/settings-learn.js +25 -0
- package/dist/ui/settings-model.js +6 -2
- package/dist/ui/settings-mods.js +4 -3
- package/dist/ui/settings-owner-effects.js +8 -8
- package/dist/ui/settings-strings.js +40 -38
- package/dist/ui/shared/app-i18n.js +2 -2
- package/dist/ui/shared/app-prefs.js +1 -1
- package/dist/ui/shared/app-shell.js +23 -1
- package/dist/ui/shared/app-store.js +12 -1
- package/dist/ui/shared/app-stream.js +2 -1
- package/dist/ui/shared/app.css +186 -11
- package/dist/ui/shared/card-strings.js +4 -0
- package/dist/ui/shared/effect-app-runtime.js +1 -1
- package/dist/ui/shared/effect-frame.js +10 -5
- package/dist/ui/shared/effect-geometry.js +24 -9
- package/dist/ui/shared/effect-policy.js +2 -1
- package/dist/ui/shared/effect-registry.js +71 -17
- package/dist/ui/shared/effect-stock-trust.js +23 -2
- package/dist/ui/shared/effect-trial-shadow.js +62 -0
- package/dist/ui/shared/effect-trial-stage.js +15 -9
- package/dist/ui/shared/effect-trial.css +40 -0
- package/dist/ui/shared/identity-card.js +75 -4
- package/dist/ui/shared/identity-color.js +3 -3
- package/dist/ui/shared/instance-nav.js +20 -4
- package/dist/ui/shared/panel-view.js +24 -4
- package/dist/ui/shared/view-strings.js +22 -0
- package/dist/view-api.d.ts +32 -0
- package/dist/view-api.js +55 -4
- package/dist/view-api.js.map +1 -1
- package/dist/web-api.d.ts +19 -4
- package/dist/web-api.js +178 -20
- package/dist/web-api.js.map +1 -1
- package/dist/web-appearance-policy.d.ts +4 -6
- package/dist/web-appearance-policy.js +5 -12
- package/dist/web-appearance-policy.js.map +1 -1
- package/dist/web-appearance-tools.js +5 -1
- package/dist/web-appearance-tools.js.map +1 -1
- package/dist/web-appearance-view.d.ts +14 -1
- package/dist/web-appearance-view.js +98 -10
- package/dist/web-appearance-view.js.map +1 -1
- package/dist/web-appearance.d.ts +2 -2
- package/dist/web-appearance.js +5 -6
- package/dist/web-appearance.js.map +1 -1
- package/dist/web-auth.js +1 -1
- package/dist/web-auth.js.map +1 -1
- package/dist/web-card.d.ts +14 -0
- package/dist/web-card.js +40 -6
- package/dist/web-card.js.map +1 -1
- package/dist/web-channel-echo.d.ts +7 -1
- package/dist/web-channel-echo.js +6 -2
- package/dist/web-channel-echo.js.map +1 -1
- package/dist/web-chat-history.d.ts +40 -0
- package/dist/web-chat-history.js +66 -3
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-commands.d.ts +1 -0
- package/dist/web-effect-tools.js +2 -3
- package/dist/web-effect-tools.js.map +1 -1
- package/dist/web-effect-trials.d.ts +2 -2
- package/dist/web-effect-trials.js +3 -3
- package/dist/web-effect-trials.js.map +1 -1
- package/dist/web-guest-access.d.ts +125 -0
- package/dist/web-guest-access.js +232 -0
- package/dist/web-guest-access.js.map +1 -0
- package/dist/web-guest-rooms.d.ts +82 -0
- package/dist/web-guest-rooms.js +135 -0
- package/dist/web-guest-rooms.js.map +1 -0
- package/dist/web-identity-palette.d.ts +1 -1
- package/dist/web-identity-palette.js +2 -2
- package/dist/web-identity-palette.js.map +1 -1
- package/dist/web-login.d.ts +47 -0
- package/dist/web-login.js +115 -0
- package/dist/web-login.js.map +1 -1
- package/dist/web-mod-agent.d.ts +1 -1
- package/dist/web-mod-agent.js +2 -4
- package/dist/web-mod-agent.js.map +1 -1
- package/dist/web-mod-store.d.ts +5 -0
- package/dist/web-mod-store.js +6 -0
- package/dist/web-mod-store.js.map +1 -1
- package/dist/web-prefs.d.ts +8 -0
- package/dist/web-prefs.js +9 -0
- package/dist/web-prefs.js.map +1 -1
- package/dist/web-preview.d.ts +6 -2
- package/dist/web-preview.js +4 -2
- package/dist/web-preview.js.map +1 -1
- package/dist/web-returning-device.d.ts +39 -0
- package/dist/web-returning-device.js +46 -5
- package/dist/web-returning-device.js.map +1 -1
- package/dist/web-session.d.ts +58 -10
- package/dist/web-session.js +104 -22
- package/dist/web-session.js.map +1 -1
- package/dist/web-settings-tools.d.ts +81 -0
- package/dist/web-settings-tools.js +257 -0
- package/dist/web-settings-tools.js.map +1 -0
- package/dist/web-theme.d.ts +2 -0
- package/dist/web-theme.js +10 -1
- package/dist/web-theme.js.map +1 -1
- package/package.json +1 -1
|
@@ -15,7 +15,8 @@ import { AgentWidget, checkWidget, fleetBinds, setSidebarWidget, sidebarWidgetRa
|
|
|
15
15
|
import { confirmedWrite } from "./settings-confirm.js";
|
|
16
16
|
import { savePref } from "/assets/app-prefs.js";
|
|
17
17
|
import { confirmDialog } from "/assets/ui-confirm.js";
|
|
18
|
-
import { colorsOff, defaultPlaces, PLACEMENTS, setColorsOff, setDefaultPlaces, useIdentityColors } from "/assets/identity-color.js";
|
|
18
|
+
import { colorsOff, defaultPlaces, identityClasses, identityOf, PLACEMENTS, paintIdentity, setColorsOff, setDefaultPlaces, useIdentityColors } from "/assets/identity-color.js";
|
|
19
|
+
import { CardTitle } from "./settings-learn.js";
|
|
19
20
|
import "./settings-strings.js";
|
|
20
21
|
|
|
21
22
|
const tn = (k, ...v) => t(`settings.${k}`, ...v);
|
|
@@ -42,22 +43,38 @@ function refusal(body) {
|
|
|
42
43
|
export function AppearanceSection() {
|
|
43
44
|
const publicLink = typeof document !== "undefined" && document.body && document.body.dataset.publicLink === "1";
|
|
44
45
|
if (publicLink) return html`<section class="card s-appearance"><h3>${tn("cssTitle")}</h3><p class="sub">${tn("cssPublicLink")}</p></section>`;
|
|
45
|
-
|
|
46
|
+
const group = (key) => html`<h2 class="s-ap-group">${tn(key)}</h2>`;
|
|
47
|
+
return html`${group("groupLook")}<${ThemesCard} /><${IdentityColorCard} /><${CardSizeCard} />
|
|
48
|
+
${group("groupLayout")}<${SidebarWidgetCard} />
|
|
49
|
+
${group("groupCss")}<${OwnerCssCard} /><${DeviceCard} />
|
|
50
|
+
${group("groupMods")}<${ModsCard} /><${EffectsCard} />
|
|
51
|
+
${group("groupAdvanced")}<${CardLimitsCard} /><${RestoreDefaultsCard} />`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** #2056 B: a sample sidebar row in the host's default colour places, so the choice is seen rather than described. */
|
|
55
|
+
const SAMPLE = { color: { person: "preset:teal" } };
|
|
56
|
+
function ColorSample({ places, off }) {
|
|
57
|
+
const id = identityOf(SAMPLE, { hostDefault: places, off });
|
|
58
|
+
return html`<div class="idc-preview s-colors-sample" aria-hidden="true"><span class=${`inst v-inst ${identityClasses(id)}`} ref=${(el) => paintIdentity(el, id)}>
|
|
59
|
+
<span class="av"><span class="av-text">A</span></span><span class="inst-text"><span class="inst-name">${tn("colorSample")}</span></span></span></div>`;
|
|
46
60
|
}
|
|
47
61
|
|
|
48
62
|
/**
|
|
49
|
-
* #1958: identity colours on this host — where they show for an instance nobody chose for (
|
|
50
|
-
* changed), and a switch that turns them off everywhere while every pick is kept.
|
|
63
|
+
* #1958: identity colours on this host — where they show for an instance nobody chose for (#2056: name, bar and
|
|
64
|
+
* ring unless changed), and a switch that turns them off everywhere while every pick is kept.
|
|
51
65
|
*/
|
|
52
66
|
function IdentityColorCard() {
|
|
53
67
|
useIdentityColors();
|
|
54
68
|
const places = defaultPlaces(), off = colorsOff();
|
|
55
69
|
const toggle = (p) => setDefaultPlaces(places.includes(p) ? places.filter((x) => x !== p) : [...places, p]);
|
|
56
|
-
return html`<section class="card s-appearance s-synced s-colors"
|
|
70
|
+
return html`<section class="card s-appearance s-synced s-colors"><${CardTitle} title=${tn("colorTitle")} topic="colors" />
|
|
57
71
|
<p class="sub">${tn("colorLead")}</p>
|
|
58
|
-
<
|
|
59
|
-
|
|
60
|
-
|
|
72
|
+
<div class="s-colors-row">
|
|
73
|
+
<fieldset class="idc-places" disabled=${off}><legend>${tn("colorPlaces")}</legend>
|
|
74
|
+
${PLACEMENTS.map((p) => html`<label key=${p} class="check"><input type="checkbox" checked=${places.includes(p)} onChange=${() => toggle(p)} /> ${t(`color.place_${p}`)}</label>`)}
|
|
75
|
+
</fieldset>
|
|
76
|
+
<${ColorSample} places=${places} off=${off} />
|
|
77
|
+
</div>
|
|
61
78
|
<label class="check"><input type="checkbox" class="s-colors-off" checked=${off} onChange=${(e) => setColorsOff(e.target.checked)} /> <span>${tn("colorOff")}</span></label>
|
|
62
79
|
<p class="sub">${tn("colorOffLead")}</p>
|
|
63
80
|
</section>`;
|
|
@@ -88,7 +105,7 @@ function RestoreDefaultsCard() {
|
|
|
88
105
|
setBusy(false);
|
|
89
106
|
if (r.ok) toast(tn("restoreDone")); else toast(tn("restoreCssFailed", r.error), false);
|
|
90
107
|
}
|
|
91
|
-
return html`<section class="card s-appearance s-restore"
|
|
108
|
+
return html`<section class="card s-appearance s-restore"><${CardTitle} title=${tn("restoreTitle")} topic="restore" />
|
|
92
109
|
<p class="sub">${tn("restoreLead")}</p>
|
|
93
110
|
<button type="button" class="btn" disabled=${busy} onClick=${go}>${tn("restoreTitle")}</button>
|
|
94
111
|
</section>`;
|
|
@@ -98,7 +115,7 @@ function RestoreDefaultsCard() {
|
|
|
98
115
|
function CardSizeCard() {
|
|
99
116
|
useStore(cardSizeStore);
|
|
100
117
|
const size = cardSizeDefault();
|
|
101
|
-
return html`<section class="card s-appearance s-synced"
|
|
118
|
+
return html`<section class="card s-appearance s-synced"><${CardTitle} title=${tn("cardSizeTitle")} topic="cards" />
|
|
102
119
|
<div class="card-size" role="group" aria-label=${tn("cardSizeTitle")}>
|
|
103
120
|
${CARD_SIZES.map((s) => html`<button key=${s} type="button" class=${`seg-item${s === size ? " active" : ""}`} aria-pressed=${s === size ? "true" : "false"}
|
|
104
121
|
onClick=${() => setCardSizeDefault(s)}>${t(`card.size${s.toUpperCase()}Name`)}</button>`)}
|
|
@@ -131,7 +148,7 @@ function CardLimitsCard() {
|
|
|
131
148
|
};
|
|
132
149
|
useEffect(() => { load(); }, []);
|
|
133
150
|
if (!view) return null;
|
|
134
|
-
if (view.error) return html`<section class="card s-appearance"
|
|
151
|
+
if (view.error) return html`<section class="card s-appearance"><${CardTitle} title=${tn("cardLimitsTitle")} topic="limits" /><p class="sub">${tn("cardLimitsUnavailable")}</p></section>`;
|
|
135
152
|
const configured = (g, k) => (view.configured && view.configured[g] && typeof view.configured[g][k] === "number" ? view.configured[g][k] : null);
|
|
136
153
|
const save = async () => {
|
|
137
154
|
const body = {};
|
|
@@ -148,7 +165,7 @@ function CardLimitsCard() {
|
|
|
148
165
|
if (r.ok) { setMsg(tn("cardLimitsSaved")); load(); } else setMsg(tn("cardLimitsFailed", (r.body && r.body.error) || r.status));
|
|
149
166
|
};
|
|
150
167
|
const warnings = Array.isArray(view.warnings) ? view.warnings : [];
|
|
151
|
-
return html`<section class="card s-appearance"
|
|
168
|
+
return html`<section class="card s-appearance"><${CardTitle} title=${tn("cardLimitsTitle")} topic="limits" />
|
|
152
169
|
<p class="sub">${tn("cardLimitsLead")}</p>
|
|
153
170
|
<fieldset class="form" disabled=${busy}>
|
|
154
171
|
${["portrait", "banner", "instance_quota"].map((g) => html`<div key=${g} class="grid3">
|
|
@@ -170,7 +187,7 @@ function CardLimitsCard() {
|
|
|
170
187
|
/** #1629 r2 (d): whether this person's screens show agents' themes (and, as they come, effects) — synced. */
|
|
171
188
|
function ThemesCard() {
|
|
172
189
|
const s = useStore(themesStore);
|
|
173
|
-
return html`<section class="card s-appearance s-synced"
|
|
190
|
+
return html`<section class="card s-appearance s-synced"><${CardTitle} title=${tn("themesTitle")} topic="themes" />
|
|
174
191
|
<label class="check"><input type="checkbox" checked=${s.shown} disabled=${safeMode()} onChange=${(e) => setThemesShown(e.target.checked)} />
|
|
175
192
|
<span>${tn("themesShow")}</span></label>
|
|
176
193
|
<p class="sub">${safeMode() ? tn("themesSafeMode") : tn("themesShowLead")}</p>
|
|
@@ -209,7 +226,7 @@ function SidebarWidgetCard() {
|
|
|
209
226
|
toast(tn("widgetSaved"));
|
|
210
227
|
};
|
|
211
228
|
const remove = () => { setSidebarWidget(null); setText(""); setError(""); toast(tn("widgetRemoved")); };
|
|
212
|
-
return html`<section class="card s-appearance s-synced s-widget"
|
|
229
|
+
return html`<section class="card s-appearance s-synced s-widget"><${CardTitle} title=${tn("widgetTitle")} topic="widgets" />
|
|
213
230
|
<p class="sub">${tn("widgetLead")}</p>
|
|
214
231
|
<label class="field"><span class="sr-only">${tn("widgetTitle")}</span>
|
|
215
232
|
<textarea class="s-widget-text" rows="10" spellcheck=${false} autocapitalize="off" autocomplete="off" value=${text}
|
|
@@ -240,8 +257,8 @@ function OwnerCssCard() {
|
|
|
240
257
|
useEffect(() => { load(); }, []);
|
|
241
258
|
// A trial answered (kept or reverted) changes what is in force: read it again, keeping the editor's text.
|
|
242
259
|
useEffect(() => { if (state) load({ text: false }); }, [trial && trial.sha]);
|
|
243
|
-
if (!state) return html`<section class="card s-appearance"
|
|
244
|
-
if (state.failed) return html`<section class="card s-appearance"
|
|
260
|
+
if (!state) return html`<section class="card s-appearance"><${CardTitle} title=${tn("cssTitle")} topic="css" /><p class="sub">…</p></section>`;
|
|
261
|
+
if (state.failed) return html`<section class="card s-appearance"><${CardTitle} title=${tn("cssTitle")} topic="css" /><p class="err">${tn("cssLoadFailed")}</p></section>`;
|
|
245
262
|
|
|
246
263
|
const save = async () => {
|
|
247
264
|
setBusy(true); setError("");
|
|
@@ -285,7 +302,7 @@ function OwnerCssCard() {
|
|
|
285
302
|
? (state.pending ? tn("cssOnTrial", short(state.active)) : tn("cssInForce", short(state.active)))
|
|
286
303
|
: tn("cssNone");
|
|
287
304
|
const assetBytes = state.assets.reduce((n, a) => n + a.bytes, 0);
|
|
288
|
-
return html`<section class="card s-appearance s-owner-css"
|
|
305
|
+
return html`<section class="card s-appearance s-owner-css"><${CardTitle} title=${tn("cssTitle")} topic="css" />
|
|
289
306
|
<p class="sub">${tn("cssLead")}</p>
|
|
290
307
|
<p class="s-css-status" role="status">${inForce}</p>
|
|
291
308
|
<label class="field"><span class="sr-only">${tn("cssTitle")}</span>
|
|
@@ -319,7 +336,6 @@ function DeviceCard() {
|
|
|
319
336
|
return html`<section class="card s-appearance s-local"><h3>${tn("cssDeviceTitle")}</h3>
|
|
320
337
|
<label class="check"><input type="checkbox" checked=${off} onChange=${(e) => { setOwnerCssOffHere(e.target.checked); setOff(ownerCssOffHere()); }} />
|
|
321
338
|
<span>${tn("cssDeviceOff")}</span></label>
|
|
322
|
-
<p class="sub">${tn("cssDeviceOffLead")}</p>
|
|
323
339
|
<p class="sub">${tn("cssSafe")}</p>
|
|
324
340
|
</section>`;
|
|
325
341
|
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// #2072 part 3: what a backend or model field in Settings offers — the host's published availability (installed,
|
|
2
|
+
// signed in, new enough: /ui/backends) and the model catalog of that backend for that instance's account (/ui/models).
|
|
3
|
+
// Both answers are the server's cached ones; nothing here makes the server probe. A page asks once and reuses the answer
|
|
4
|
+
// for a short while, so opening several dialogs costs one request each.
|
|
5
|
+
import { useEffect, useState } from "/assets/app-html.js";
|
|
6
|
+
|
|
7
|
+
const TTL_MS = 30_000;
|
|
8
|
+
const cache = new Map(); // url → { at, value, pending }
|
|
9
|
+
|
|
10
|
+
async function getJson(url) {
|
|
11
|
+
const res = await fetch(url, { credentials: "same-origin" });
|
|
12
|
+
if (!res.ok) throw new Error(String(res.status));
|
|
13
|
+
return res.json();
|
|
14
|
+
}
|
|
15
|
+
/** The answer for `url`, shared by every field on the page while it is fresh; null when it cannot be read. */
|
|
16
|
+
function cached(url) {
|
|
17
|
+
const hit = cache.get(url), now = performance.now();
|
|
18
|
+
if (hit && (hit.pending || now - hit.at < TTL_MS)) return hit.pending ?? Promise.resolve(hit.value);
|
|
19
|
+
const pending = getJson(url).then((value) => { cache.set(url, { at: performance.now(), value, pending: null }); return value; })
|
|
20
|
+
.catch(() => { cache.delete(url); return null; });
|
|
21
|
+
cache.set(url, { at: now, value: null, pending });
|
|
22
|
+
return pending;
|
|
23
|
+
}
|
|
24
|
+
/** For tests and after a write that changes the answer. */
|
|
25
|
+
export function forgetCatalogs() { cache.clear(); }
|
|
26
|
+
|
|
27
|
+
function useAnswer(url) {
|
|
28
|
+
const [value, setValue] = useState(undefined); // undefined: loading; null: unavailable
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (!url) { setValue(null); return undefined; }
|
|
31
|
+
let live = true;
|
|
32
|
+
setValue(undefined);
|
|
33
|
+
cached(url).then((v) => { if (live) setValue(v); });
|
|
34
|
+
return () => { live = false; };
|
|
35
|
+
}, [url]);
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The host's backends with their state ({ name, state, minVersion, … }), or null while loading or unavailable. */
|
|
40
|
+
export function useBackendRows() {
|
|
41
|
+
const answer = useAnswer("/ui/backends");
|
|
42
|
+
return answer && Array.isArray(answer.backends) ? answer.backends : answer === undefined ? undefined : null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The model catalog of `backend` for `instance` (its account) — [] when none is cached; undefined while loading. */
|
|
46
|
+
export function useModelCatalog(backend, instance) {
|
|
47
|
+
const url = backend ? `/ui/models?backend=${encodeURIComponent(backend)}${instance ? `&instance=${encodeURIComponent(instance)}` : ""}` : null;
|
|
48
|
+
const answer = useAnswer(url);
|
|
49
|
+
if (answer === undefined) return undefined;
|
|
50
|
+
return answer && Array.isArray(answer.models) ? answer.models : [];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A state that keeps a backend from being chosen (shown, disabled, with why); "unknown" never does. */
|
|
54
|
+
export const BLOCKING_STATES = ["not_installed", "not_signed_in", "too_old"];
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The backend select's options: every backend the page knows, in its order, each with what the host said about it.
|
|
58
|
+
* The current value is always selectable — a probe never disables what an agent already runs — and one the page does
|
|
59
|
+
* not know (a removed backend) stays first, as before.
|
|
60
|
+
*/
|
|
61
|
+
export function backendChoices(known, rows, value, words) {
|
|
62
|
+
const byName = new Map((rows || []).map((r) => [r.name, r]));
|
|
63
|
+
const choice = (name) => {
|
|
64
|
+
const row = byName.get(name), state = row ? row.state ?? (row.installed ? "usable" : row.unknown ? "unknown" : "not_installed") : null;
|
|
65
|
+
const blocked = !!state && BLOCKING_STATES.includes(state) && name !== value;
|
|
66
|
+
const note = state === "not_installed" ? words.notInstalled : state === "not_signed_in" ? words.notSignedIn
|
|
67
|
+
: state === "too_old" ? words.tooOld(row.minVersion ?? "?") : state === "unknown" && name !== value ? words.unchecked : "";
|
|
68
|
+
return { value: name, label: note ? `${name} — ${note}` : name, disabled: blocked };
|
|
69
|
+
};
|
|
70
|
+
const list = known.map(choice);
|
|
71
|
+
return known.includes(value) || !value ? list : [{ value, label: words.removed(value), disabled: false }, ...list];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The id of the "type another model" choice (never a model name: it has a space). */
|
|
75
|
+
export const OTHER_MODEL = "\u0000other";
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The model select's options: the inherited choice (empty value), the current model first when the catalog does not
|
|
79
|
+
* list it (marked), the catalog, and "Other…" for a name the CLI does not list.
|
|
80
|
+
*/
|
|
81
|
+
export function modelChoices(catalog, value, words, inherit) {
|
|
82
|
+
const list = [];
|
|
83
|
+
if (inherit) list.push({ value: "", label: inherit });
|
|
84
|
+
const ids = new Set((catalog || []).map((m) => m.id));
|
|
85
|
+
if (value && !ids.has(value)) list.push({ value, label: `${value} — ${words.notListed}` });
|
|
86
|
+
for (const m of catalog || []) list.push({ value: m.id, label: m.description ? `${m.label || m.id} — ${m.description}` : (m.label || m.id) });
|
|
87
|
+
list.push({ value: OTHER_MODEL, label: words.other });
|
|
88
|
+
return list;
|
|
89
|
+
}
|
|
@@ -13,11 +13,13 @@ import { api, confirmedWrite, newKey } from "./settings-confirm.js";
|
|
|
13
13
|
import { adminUserIdError, adminUserIdPattern, botHandle, TokenEnvNote, TokenField, verifyBotToken } from "./settings-token.js";
|
|
14
14
|
import { DiscordServerPicker } from "./settings-discord.js";
|
|
15
15
|
import { startOperation } from "./settings-apply.js";
|
|
16
|
+
import "./settings-strings.js"; // its words, wherever a picker of this file is used (#2072: Fleet's create dialog too)
|
|
17
|
+
import { backendChoices, modelChoices, OTHER_MODEL, useBackendRows, useModelCatalog } from "./settings-catalog.js";
|
|
16
18
|
import {
|
|
17
19
|
ACCESS_MODES, AGENT_FIELDS, AGENT_MODES, BOT_FIELDS, CH_TYPES, CLASSIC_FIELDS, HANG_TIMEOUT_DEFAULT_MINUTES, LOG_LEVELS, TOOL_PROGRESS, TOOL_SETS,
|
|
18
20
|
VISIBILITY_MODES, accessConfirmations, agentForm, agentPatch, backendOptions, changedFields, channelId, channelsOf,
|
|
19
21
|
chLabel, hasOwn, impactOf, nonNegative, nonNegativeInteger, positive, replyGuardSupported, sameValue, visibilityDefault,
|
|
20
|
-
|
|
22
|
+
classicWebEchoEffective, worstImpact,
|
|
21
23
|
} from "./settings-model.js";
|
|
22
24
|
|
|
23
25
|
const tn = (k, ...v) => t(`settings.${k}`, ...v);
|
|
@@ -32,9 +34,36 @@ export function Select({ value, options, onChange, disabled, id, label }) {
|
|
|
32
34
|
return html`<select id=${id} value=${value} disabled=${disabled} aria-label=${label} onChange=${(e) => onChange(e.target.value)}>
|
|
33
35
|
${opts.map((o) => html`<option key=${o.value} value=${o.value}>${o.label}</option>`)}</select>`;
|
|
34
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* #2072: a backend field shows every backend with what this host says about it — one it cannot run is disabled with
|
|
39
|
+
* why (not installed, not signed in, too old); one not checked yet stays selectable; the current value always can be.
|
|
40
|
+
*/
|
|
35
41
|
export function BackendSelect({ value, onChange, disabled, id }) {
|
|
36
|
-
|
|
37
|
-
|
|
42
|
+
const rows = useBackendRows();
|
|
43
|
+
const known = backendOptions(value).filter((o) => !o.unavailable).map((o) => o.value);
|
|
44
|
+
const opts = backendChoices(known, rows || [], value, {
|
|
45
|
+
notInstalled: tn("backendNotInstalled"), notSignedIn: tn("backendNotSignedIn"), tooOld: (v) => tn("backendTooOld", v),
|
|
46
|
+
unchecked: tn("backendUnchecked"), removed: (v) => tn("backendUnavailable", v),
|
|
47
|
+
});
|
|
48
|
+
return html`<select id=${id} value=${value} disabled=${disabled} onChange=${(e) => onChange(e.target.value)}>
|
|
49
|
+
${opts.map((o) => html`<option key=${o.value} value=${o.value} disabled=${o.disabled}>${o.label}</option>`)}</select>`;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* #2072: a model field is a list — that backend's models for this account (cached; never a probe), the current one
|
|
53
|
+
* first and marked when the CLI does not list it, and "Other…" for a name it does not list (a text box opens with
|
|
54
|
+
* the current value in it: choosing "Other…" alone changes nothing — emptying the box is what inherits, #2098 review).
|
|
55
|
+
* `inherit`: the label of the empty choice (inherit / none), or omit it when the field has none.
|
|
56
|
+
*/
|
|
57
|
+
export function ModelSelect({ value, onChange, disabled, id, backend, instance, inherit }) {
|
|
58
|
+
const catalog = useModelCatalog(backend, instance);
|
|
59
|
+
const [typing, setTyping] = useState(false);
|
|
60
|
+
const opts = modelChoices(catalog || [], value, { notListed: tn("modelNotListed"), other: tn("modelOther") }, inherit);
|
|
61
|
+
return html`<div class="model-pick">
|
|
62
|
+
<select id=${id} value=${typing ? OTHER_MODEL : value} disabled=${disabled}
|
|
63
|
+
onChange=${(e) => { const v = e.target.value; if (v === OTHER_MODEL) setTyping(true); else { setTyping(false); onChange(v); } }}>
|
|
64
|
+
${opts.map((o) => html`<option key=${o.value} value=${o.value}>${o.label}</option>`)}</select>
|
|
65
|
+
${typing ? html`<input type="text" aria-label=${tn("modelOtherLabel")} placeholder=${tn("modelOtherPlaceholder")} disabled=${disabled}
|
|
66
|
+
value=${value} onInput=${(e) => onChange(e.target.value)} />` : null}</div>`;
|
|
38
67
|
}
|
|
39
68
|
export function Impact({ schema, field }) { return html`<span class="impact">${impactText(impactOf(schema, field))}</span>`; }
|
|
40
69
|
export function Label({ text, schema, field, htmlFor }) {
|
|
@@ -228,7 +257,8 @@ export function AgentDialog({ name, inst, ctx, onClose }) {
|
|
|
228
257
|
<input id="ag-dn" type="text" placeholder=${name} disabled=${f.inherit.display_name} value=${shown("display_name", f.display_name, defaults.display_name || "")} onInput=${(e) => set("display_name")(e.target.value)} /></${Override}></div>
|
|
229
258
|
<div class="field">${L(tn("description"), "instance.description", "ag-desc")}<input id="ag-desc" type="text" value=${f.description} onInput=${(e) => set("description")(e.target.value)} /></div>
|
|
230
259
|
<div class="field">${L(tn("backend"), "instance.backend", "ag-be")}<${BackendSelect} id="ag-be" value=${f.backend} onChange=${set("backend")} /></div>
|
|
231
|
-
<div class="field">${L(tn("model"), "instance.model", "ag-model")}
|
|
260
|
+
<div class="field">${L(tn("model"), "instance.model", "ag-model")}<${ModelSelect} id="ag-model" value=${f.model} onChange=${set("model")}
|
|
261
|
+
backend=${f.backend || defaults.backend || "claude-code"} instance=${name} inherit=${tn("inheritPlaceholder")} /></div>
|
|
232
262
|
</div>
|
|
233
263
|
<div class="field">${L(tn("workingDir"), "instance.working_directory", "ag-wd")}<input id="ag-wd" type="text" value=${f.working_directory} onInput=${(e) => set("working_directory")(e.target.value)} /></div>
|
|
234
264
|
<div class="field">${L(`${tn("autoPause")} (${tn("minutes")})`, "instance.auto_pause_after", "ag-auto")}
|
|
@@ -264,8 +294,8 @@ export function AgentDialog({ name, inst, ctx, onClose }) {
|
|
|
264
294
|
onChange=${(e) => set("lightweight")(e.target.checked)} /><span>${tn("lightweight")}</span></label></${Override}></div>
|
|
265
295
|
<div class="field">${L(tn("modelFailover"), "instance.model_failover", "ag-fo")}
|
|
266
296
|
<${Override} inherit=${f.inherit.model_failover} onInherit=${setInherit("model_failover")}>
|
|
267
|
-
|
|
268
|
-
value=${shown("model_failover", f.model_failover, (defaults.model_failover && defaults.model_failover[0]) || "")}
|
|
297
|
+
<${ModelSelect} id="ag-fo" disabled=${f.inherit.model_failover} backend=${f.backend || defaults.backend || "claude-code"} instance=${name} inherit=${tn("modelNone")}
|
|
298
|
+
value=${shown("model_failover", f.model_failover, (defaults.model_failover && defaults.model_failover[0]) || "")} onChange=${set("model_failover")} /></${Override}>
|
|
269
299
|
<${Feedback} warning=${failoverWarn} /></div>
|
|
270
300
|
</div>
|
|
271
301
|
<div class="field">${L(tn("systemPrompt"), "instance.systemPrompt", "ag-sys")}<input id="ag-sys" type="text" placeholder="e.g. file:prompts/dev.md" value=${f.systemPrompt} onInput=${(e) => set("systemPrompt")(e.target.value)} /></div>
|
|
@@ -557,7 +587,7 @@ export function ClassicDialog({ room, ctx, onClose }) {
|
|
|
557
587
|
backend: c.backend || inherited.backend, model: c.model || "",
|
|
558
588
|
auto_pause_after: String(c.auto_pause_after ?? inherited.auto_pause_after), tool_progress: c.tool_progress ?? inherited.tool_progress,
|
|
559
589
|
reply_completion_guard: c.reply_completion_guard ?? inherited.reply_completion_guard,
|
|
560
|
-
collab: !!c.collab, web_echo:
|
|
590
|
+
collab: !!c.collab, web_echo: classicWebEchoEffective(c, fleet), context_lines: String(c.context_lines ?? cd.context_lines ?? 5),
|
|
561
591
|
inherit: { auto_pause_after: !hasOwn(c, "auto_pause_after"), tool_progress: !hasOwn(c, "tool_progress"), reply_completion_guard: !hasOwn(c, "reply_completion_guard") },
|
|
562
592
|
}));
|
|
563
593
|
const set = (k) => (v) => setF((x) => ({ ...x, [k]: v }));
|
|
@@ -580,12 +610,9 @@ export function ClassicDialog({ room, ctx, onClose }) {
|
|
|
580
610
|
auto_pause_after: hasOwn(c, "auto_pause_after") ? c.auto_pause_after : null,
|
|
581
611
|
tool_progress: hasOwn(c, "tool_progress") ? c.tool_progress : null,
|
|
582
612
|
reply_completion_guard: hasOwn(c, "reply_completion_guard") ? c.reply_completion_guard : null,
|
|
583
|
-
collab: !!c.collab, context_lines: c.context_lines ?? cd.context_lines ?? 5, web_echo:
|
|
613
|
+
collab: !!c.collab, context_lines: c.context_lines ?? cd.context_lines ?? 5, web_echo: classicWebEchoEffective(c, fleet),
|
|
584
614
|
});
|
|
585
615
|
if (!Object.keys(patch).length) { onClose(); return; }
|
|
586
|
-
// #1320: turning web echo on for a group posts every web message where all its members can read it.
|
|
587
|
-
if (patch.web_echo === true && !c.web_echo && webEchoIsGroup(c.channelId)
|
|
588
|
-
&& !(await ask(tn("webEchoGroupConfirm"), { confirmLabel: tn("webEchoEnable") }))) return;
|
|
589
616
|
if (!lease.current()) return;
|
|
590
617
|
ctx.stageClassic(c, patch);
|
|
591
618
|
onClose();
|
|
@@ -594,7 +621,8 @@ export function ClassicDialog({ room, ctx, onClose }) {
|
|
|
594
621
|
return html`<${EditDialog} title=${c.name || c.instanceName} subtitle=${`ID: ${c.instanceName} · ${tn(`exec_${runtime.execution}`)}`} onClose=${onClose} onStage=${stage} ready=${ready} impacts=${CLASSIC_FIELDS} schema=${schema}>
|
|
595
622
|
<div class="grid2">
|
|
596
623
|
<div class="field">${L(tn("backend"), "classic.backend", "cl-be")}<${BackendSelect} id="cl-be" value=${f.backend} onChange=${set("backend")} /></div>
|
|
597
|
-
<div class="field">${L(tn("model"), "classic.model", "cl-model")}
|
|
624
|
+
<div class="field">${L(tn("model"), "classic.model", "cl-model")}<${ModelSelect} id="cl-model" value=${f.model} onChange=${set("model")}
|
|
625
|
+
backend=${f.backend} instance=${c.instanceName} inherit=${inherited.model ? tn("inheritValue", inherited.model) : tn("inheritPlaceholder")} /></div>
|
|
598
626
|
<div class="field">${L(`${tn("autoPause")} (${tn("minutes")})`, "classic.auto_pause_after", "cl-auto")}
|
|
599
627
|
<${Override} inherit=${f.inherit.auto_pause_after} onInherit=${setInherit("auto_pause_after")}>
|
|
600
628
|
<input id="cl-auto" type="number" min="0" step="1" class=${errors.auto ? "invalid" : ""} disabled=${f.inherit.auto_pause_after}
|
|
@@ -4,10 +4,11 @@ import { register, t } from "/assets/app-i18n.js";
|
|
|
4
4
|
import { appStore, useStore } from "/assets/app-store.js";
|
|
5
5
|
import { confirmDialog } from "/assets/ui-confirm.js";
|
|
6
6
|
import { OwnerEffects } from "./settings-owner-effects.js";
|
|
7
|
+
import { CardTitle } from "./settings-learn.js";
|
|
7
8
|
|
|
8
9
|
register("effectSettings", {
|
|
9
10
|
en: {
|
|
10
|
-
title: "Effects", lead: "
|
|
11
|
+
title: "Effects", lead: "Your effect files, and which one a chat uses.", folder: "Upload an effect folder", files: "effect.json + worker.js (and pictures), up to 1 MiB.",
|
|
11
12
|
inUse: "This effect is still in use and cannot be deleted.", quota: "Your library reached its 10-effect or 8 MiB limit. Delete unused copies first.", rateLimited: "Too many changes. Wait a moment, then try again.",
|
|
12
13
|
loading: "Loading effects…", retry: "Reload library", empty: "No effects in your library.", failed: "Couldn't load the library. Try again.", actionFailed: "Couldn't save this change. Try again.", savedRefresh: "Saved. Reload the library to see the result.",
|
|
13
14
|
uploadFailed: "Couldn't upload this folder. Check its files and size, then try again.", public: "Effect management isn't available on a temporary public link.", readOnly: "Read-only session: sign in as an admin to change effects.",
|
|
@@ -16,7 +17,7 @@ register("effectSettings", {
|
|
|
16
17
|
fps: "Host FPS p50 / p10", tasks: "Host long task p95", noTasks: "Not observed", standard: "Standard device", constrained: "Constrained device", params: "Parameters", badParams: "Check the parameter values.", more: "Show more", count: "{0} effects", one: "1 effect", profileFailed: "Couldn't read profile history. Try again.",
|
|
17
18
|
},
|
|
18
19
|
"zh-TW": {
|
|
19
|
-
title: "效果", lead: "
|
|
20
|
+
title: "效果", lead: "你的效果檔案,以及每個聊天使用哪一個。", folder: "上傳效果資料夾", files: "effect.json + worker.js(可加圖片),最多 1 MiB。",
|
|
20
21
|
inUse: "這個效果仍在使用中,無法刪除。", quota: "你的程式庫已達 10 個效果或 8 MiB 上限,請先刪除未使用的副本。", rateLimited: "變更太頻繁,請稍候再試。",
|
|
21
22
|
loading: "正在載入效果…", retry: "重新載入程式庫", empty: "程式庫中還沒有效果。", failed: "無法載入程式庫,請重試。", actionFailed: "無法儲存變更,請重試。", savedRefresh: "已儲存,請重新載入程式庫查看結果。",
|
|
22
23
|
uploadFailed: "無法上傳這個資料夾,請檢查檔案與大小後重試。", public: "臨時公開連結不提供效果管理。", readOnly: "目前為唯讀登入,請以管理員身分登入才能修改效果。",
|
|
@@ -180,7 +181,7 @@ export function EffectsCard() {
|
|
|
180
181
|
if (!current(op.seq)) return; finish(op.seq);
|
|
181
182
|
if (yes) await change(`/ui/effects/${id}`, undefined, "DELETE");
|
|
182
183
|
};
|
|
183
|
-
return html`<section class="card s-appearance s-effects"
|
|
184
|
+
return html`<section class="card s-appearance s-effects"><${CardTitle} title=${tn("title")} topic="effects" /><p class="sub">${tn("lead")}</p>
|
|
184
185
|
<div class="s-fx-actions"><button type="button" class="btn" disabled=${busy} onClick=${reload}>${tn("retry")}</button>
|
|
185
186
|
${view?.canWrite ? html`<button class="btn s-fx-choose" type="button" disabled=${busy} onClick=${() => uploadInput.current?.click()}>${tn("folder")}</button><input ref=${uploadInput} class="offscreen s-fx-upload" aria-label=${tn("folder")} tabindex="-1" type="file" webkitdirectory=${true} directory=${true} multiple disabled=${busy} onChange=${upload} />` : null}</div>
|
|
186
187
|
${busy ? html`<p class="sub" role="status">${tn("loading")}</p>` : null}${error ? html`<p class="err" role="status">${error}</p>` : null}${message ? html`<p class="sub" role="status">${message}</p>` : null}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// #2056 B: a card title with a small "?" that opens the card's section of the docs, so Settings → Web appearance
|
|
2
|
+
// keeps one short line per control and the long explanations live in docs/web-dashboard(.zh-TW).md.
|
|
3
|
+
import { html } from "/assets/app-html.js";
|
|
4
|
+
import { lang, t } from "/assets/app-i18n.js";
|
|
5
|
+
import "./settings-strings.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* #2056 B: where each card's long explanation lives — a heading of docs/web-dashboard(.zh-TW).md on GitHub, so the
|
|
9
|
+
* page keeps one short line per control and a small "?" for the rest.
|
|
10
|
+
*/
|
|
11
|
+
const DOCS = { en: "https://github.com/songsid/AgEnD/blob/main/docs/web-dashboard.md", "zh-TW": "https://github.com/songsid/AgEnD/blob/main/docs/web-dashboard.zh-TW.md" };
|
|
12
|
+
export const LEARN_ANCHORS = {
|
|
13
|
+
colors: ["identity-colours", "識別色"], css: ["custom-css-settings--web-appearance", "自訂-css設定--網頁外觀"], themes: ["agents-themes", "agent-的主題"],
|
|
14
|
+
cards: ["identity-cards", "身分卡"], limits: ["card-upload-limits", "身分卡上傳限制"], widgets: ["widgets", "小工具"], mods: ["mods", "mod"],
|
|
15
|
+
effects: ["effects", "效果"], restore: ["restore-defaults", "還原預設值"],
|
|
16
|
+
};
|
|
17
|
+
export function learnHref(topic, language = lang()) {
|
|
18
|
+
const zh = language === "zh-TW";
|
|
19
|
+
return `${DOCS[zh ? "zh-TW" : "en"]}#${encodeURIComponent(LEARN_ANCHORS[topic][zh ? 1 : 0])}`;
|
|
20
|
+
}
|
|
21
|
+
/** A card title with its "?" link to the docs (a new tab; the page itself never fetches anything). */
|
|
22
|
+
export function CardTitle({ title, topic }) {
|
|
23
|
+
return html`<h3 class="s-ap-title"><span>${title}</span>${topic ? html`<a class="s-learn" href=${learnHref(topic)} target="_blank" rel="noopener noreferrer"
|
|
24
|
+
aria-label=${t("settings.learnMore", title)} title=${t("settings.learnMore", title)}>?</a>` : null}</h3>`;
|
|
25
|
+
}
|
|
@@ -222,8 +222,12 @@ export function accessConfirmations(previousAccess, nextMode, nextUsers) {
|
|
|
222
222
|
return out;
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
-
/**
|
|
226
|
-
|
|
225
|
+
/**
|
|
226
|
+
* #2054: whether a ClassicBot room copies web chat, as the fleet decides it (ClassicChannelManager's load +
|
|
227
|
+
* classicWebEchoOn): an absent `web_echo` follows the fleet-wide `web.echo_to_channel` (default on); any other value
|
|
228
|
+
* echoes only when it is `true` (a `null`, `0` or string reads as off, as the manager reads it).
|
|
229
|
+
*/
|
|
230
|
+
export const classicWebEchoEffective = (room, fleet) => (room?.web_echo === undefined ? fleet?.web?.echo_to_channel !== false : room.web_echo === true);
|
|
227
231
|
|
|
228
232
|
// ── Developer YAML: the config subset's writer and reader, exactly as settings.html had them (round-trip verified) ──
|
|
229
233
|
export function toYaml(value, indent = 0) {
|
package/dist/ui/settings-mods.js
CHANGED
|
@@ -12,6 +12,7 @@ import { toast } from "/assets/ui-toast.js";
|
|
|
12
12
|
import { confirmDialog } from "/assets/ui-confirm.js";
|
|
13
13
|
import { startOwnerCssTrial } from "/assets/owner-css.js";
|
|
14
14
|
import { AgentWidget, checkWidget, fleetBinds, instanceBinds } from "/assets/agent-widget.js";
|
|
15
|
+
import { CardTitle } from "./settings-learn.js";
|
|
15
16
|
import "./settings-strings.js";
|
|
16
17
|
|
|
17
18
|
const tn = (k, ...v) => t(`settings.${k}`, ...v);
|
|
@@ -204,8 +205,8 @@ export function ModsCard() {
|
|
|
204
205
|
setState(b && Array.isArray(b.mods) ? { mods: b.mods.filter((m) => m && typeof m.id === "string"), staged, proposals } : { failed: true });
|
|
205
206
|
};
|
|
206
207
|
useEffect(() => { load(); }, []);
|
|
207
|
-
if (!state) return html`<section class="card s-appearance s-mods"
|
|
208
|
-
if (state.failed) return html`<section class="card s-appearance s-mods"
|
|
208
|
+
if (!state) return html`<section class="card s-appearance s-mods"><${CardTitle} title=${tn("modsTitle")} topic="mods" /><p class="sub">…</p></section>`;
|
|
209
|
+
if (state.failed) return html`<section class="card s-appearance s-mods"><${CardTitle} title=${tn("modsTitle")} topic="mods" /><p class="err">${tn("modsLoadFailed")}</p></section>`;
|
|
209
210
|
const stage = async () => {
|
|
210
211
|
const f = file.current && file.current.files && file.current.files[0];
|
|
211
212
|
if (!f) return;
|
|
@@ -238,7 +239,7 @@ export function ModsCard() {
|
|
|
238
239
|
if (!r || !r.ok) { toast(tn("modFailed", r ? r.status : "network"), false); return; }
|
|
239
240
|
download(await r.text(), `${m.name}-${m.version}.agendmod.json`);
|
|
240
241
|
};
|
|
241
|
-
return html`<section class="card s-appearance s-mods"
|
|
242
|
+
return html`<section class="card s-appearance s-mods"><${CardTitle} title=${tn("modsTitle")} topic="mods" />
|
|
242
243
|
<p class="sub">${tn("modsLead")}</p>
|
|
243
244
|
${state.staged ? html`<${Plan} key=${state.staged.stage} staged=${state.staged} onDone=${load} />` : html`<div class="s-mod-import">
|
|
244
245
|
<label class="btn"><${Icon} name="attach" size=${16} /><span>${tn("modChoose")}</span>
|
|
@@ -7,24 +7,24 @@ import { getAppEffects } from "./app-effects.js";
|
|
|
7
7
|
import { ownerEffectsStore, watchOwnerEffects, refreshOwnerEffects, saveOwnerEffects } from "/assets/owner-effect-prefs.js";
|
|
8
8
|
register("ownerEffects", {
|
|
9
9
|
en: {
|
|
10
|
-
title: "Your effect choices", operatorTitle: "Host-wide effect choices", person: "
|
|
11
|
-
operator: "
|
|
10
|
+
title: "Your effect choices", operatorTitle: "Host-wide effect choices", person: "Follows your sign-in; Discord and Telegram are separate.",
|
|
11
|
+
operator: "Shared by everyone signed in as the host.",
|
|
12
12
|
sidebar: "Sidebar", header: "Header", page: "Page", box: "Box", cursor: "Cursor", none: "None", savedChoice: "Saved choice",
|
|
13
13
|
unknown: "Choices are unavailable. Reload to try again.", changed: "This page can no longer access these choices. Reload and sign in again.",
|
|
14
14
|
unavailable: "No longer available", clear: "Clear", reload: "Reload choices", saved: "Saved.", failed: "Couldn't save. Reload choices and try again.",
|
|
15
15
|
savedRefresh: "Saved, but the choices couldn't be refreshed. Reload to check.", readOnly: "Sign in as an admin to change these choices.",
|
|
16
|
-
playback: "
|
|
17
|
-
pause: "Pause", resume: "Resume", running: "Running", cursorFailed: "The cursor could not be applied here. Clear or reload the choice to try again.", starting: "Starting", queued: "Waiting for a slot", paused: "Paused", off: "Playback is off or unavailable here.", budget: "Paused to
|
|
16
|
+
playback: "Plays once Effects and previews are allowed.",
|
|
17
|
+
pause: "Pause", resume: "Resume", running: "Running", cursorFailed: "The cursor could not be applied here. Clear or reload the choice to try again.", starting: "Starting", queued: "Waiting for a slot", paused: "Paused", off: "Playback is off or unavailable here.", budget: "Paused to keep the page responsive.", resumeFailed: "This effect cannot resume: the page or its safety history is not ready. Reload the page or choose another effect.",
|
|
18
18
|
},
|
|
19
19
|
"zh-TW": {
|
|
20
|
-
title: "你的效果選擇", operatorTitle: "主機共用效果選擇", person: "
|
|
21
|
-
operator: "
|
|
20
|
+
title: "你的效果選擇", operatorTitle: "主機共用效果選擇", person: "跟隨你的登入身分;Discord 與 Telegram 各自獨立。",
|
|
21
|
+
operator: "所有以主機身分登入的人共用。",
|
|
22
22
|
sidebar: "側邊欄", header: "頁首", page: "頁面", box: "區塊", cursor: "游標", none: "無", savedChoice: "已儲存的選擇",
|
|
23
23
|
unknown: "目前無法取得選擇,請重新載入再試。", changed: "這個頁面已無法存取選擇,請重新載入並登入。",
|
|
24
24
|
unavailable: "已無法使用", clear: "清除", reload: "重新載入選擇", saved: "已儲存。", failed: "無法儲存,請重新載入選擇再試。",
|
|
25
25
|
savedRefresh: "已儲存,但無法更新畫面;請重新載入確認。", readOnly: "請以管理員身分登入才能修改選擇。",
|
|
26
|
-
playback: "
|
|
27
|
-
pause: "暫停", resume: "恢復", running: "播放中", cursorFailed: "這裡無法套用游標。可清除或重新載入選擇後再試。", starting: "啟動中", queued: "等待空位", paused: "已暫停", off: "這裡已關閉播放,或無法播放效果。", budget: "
|
|
26
|
+
playback: "開啟效果並允許預覽後才會播放。",
|
|
27
|
+
pause: "暫停", resume: "恢復", running: "播放中", cursorFailed: "這裡無法套用游標。可清除或重新載入選擇後再試。", starting: "啟動中", queued: "等待空位", paused: "已暫停", off: "這裡已關閉播放,或無法播放效果。", budget: "為維持頁面順暢,已暫停。", resumeFailed: "頁面或安全紀錄尚未就緒,無法恢復此效果。請重新載入頁面,或選擇其他效果。",
|
|
28
28
|
},
|
|
29
29
|
});
|
|
30
30
|
const text = key => t(`ownerEffects.${key}`);
|