@songsid/agend 2.3.0-beta.3 → 2.3.0-beta.5
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-file-read.d.ts +4 -1
- package/dist/agent-file-read.js +129 -16
- package/dist/agent-file-read.js.map +1 -1
- package/dist/auth-api.d.ts +30 -1
- package/dist/auth-api.js +32 -7
- package/dist/auth-api.js.map +1 -1
- package/dist/authority/web-authority.d.ts +100 -0
- package/dist/authority/web-authority.js +130 -0
- package/dist/authority/web-authority.js.map +1 -0
- package/dist/backend/types.d.ts +1 -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 +5 -5
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/types.d.ts +6 -0
- package/dist/config-validator.js +91 -2
- package/dist/config-validator.js.map +1 -1
- package/dist/daemon.js +5 -5
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.d.ts +2 -0
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/fleet-context.d.ts +10 -0
- package/dist/fleet-manager.d.ts +230 -6
- package/dist/fleet-manager.js +719 -42
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +45 -18
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +7 -3
- package/dist/human-delivery-receipts.d.ts +10 -1
- package/dist/human-delivery-receipts.js +26 -9
- package/dist/human-delivery-receipts.js.map +1 -1
- package/dist/locale.js +42 -4
- package/dist/locale.js.map +1 -1
- package/dist/outbound-schemas.d.ts +7 -3
- package/dist/outbound-schemas.js +5 -1
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/public-web-link.d.ts +9 -0
- package/dist/public-web-link.js +8 -0
- package/dist/public-web-link.js.map +1 -1
- package/dist/quickstart-api.d.ts +10 -1
- package/dist/quickstart-api.js +12 -4
- package/dist/quickstart-api.js.map +1 -1
- package/dist/reply-buttons-controller.d.ts +4 -0
- package/dist/reply-buttons-controller.js +2 -2
- package/dist/reply-buttons-controller.js.map +1 -1
- package/dist/settings-api.d.ts +9 -0
- package/dist/settings-api.js +76 -2
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +66 -0
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +8 -1
- package/dist/settings-effect.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 +36 -0
- package/dist/ui/chat-strings.js +4 -2
- package/dist/ui/chat-thread.js +4 -1
- package/dist/ui/panel-chat.js +19 -9
- package/dist/ui/panel-fleet.js +18 -6
- package/dist/ui/panel-settings.js +5 -2
- package/dist/ui/settings-catalog.js +92 -0
- package/dist/ui/settings-dialogs.js +37 -6
- package/dist/ui/settings-guests.js +117 -0
- package/dist/ui/settings-strings.js +28 -0
- package/dist/ui/shared/app-i18n.js +2 -0
- package/dist/ui/shared/app-prefs.js +3 -3
- package/dist/ui/shared/app-shell.js +36 -6
- package/dist/ui/shared/app-store.js +14 -0
- package/dist/ui/shared/app.css +54 -3
- package/dist/ui/shared/app.js +21 -14
- package/dist/ui/shared/card-strings.js +4 -0
- package/dist/ui/shared/effect-stock-trust.js +23 -2
- package/dist/ui/shared/identity-card.js +70 -2
- package/dist/ui/shared/instance-nav.js +8 -5
- package/dist/ui/shared/panel-view.js +22 -2
- package/dist/ui/shared/view-strings.js +6 -0
- package/dist/view-api.d.ts +11 -0
- package/dist/view-api.js +27 -4
- package/dist/view-api.js.map +1 -1
- package/dist/web-api.d.ts +32 -1
- package/dist/web-api.js +166 -28
- 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 +4 -1
- package/dist/web-appearance-view.js +21 -2
- 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-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-upload.js +1 -1
- package/dist/web-effect-upload.js.map +1 -1
- package/dist/web-guest-access.d.ts +65 -7
- package/dist/web-guest-access.js +124 -18
- package/dist/web-guest-access.js.map +1 -1
- package/dist/web-guest-rooms.d.ts +82 -0
- package/dist/web-guest-rooms.js +135 -0
- package/dist/web-guest-rooms.js.map +1 -0
- package/dist/web-identity-color.d.ts +1 -1
- package/dist/web-identity-color.js +2 -2
- package/dist/web-identity-color.js.map +1 -1
- package/dist/web-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-prefs.js +1 -1
- package/dist/web-prefs.js.map +1 -1
- package/dist/web-returning-device.d.ts +41 -0
- package/dist/web-returning-device.js +57 -5
- package/dist/web-returning-device.js.map +1 -1
- package/dist/web-session.d.ts +50 -3
- package/dist/web-session.js +96 -14
- package/dist/web-session.js.map +1 -1
- package/dist/web-settings-patch.d.ts +25 -2
- package/dist/web-settings-patch.js +96 -1
- package/dist/web-settings-patch.js.map +1 -1
- package/dist/web-settings-tools.d.ts +12 -0
- package/dist/web-settings-tools.js +10 -0
- package/dist/web-settings-tools.js.map +1 -1
- package/dist/web-theme.d.ts +2 -0
- package/dist/web-theme.js +10 -1
- package/dist/web-theme.js.map +1 -1
- package/package.json +1 -1
package/dist/ui/panel-fleet.js
CHANGED
|
@@ -21,6 +21,7 @@ import { confirmedWrite } from "./settings-confirm.js";
|
|
|
21
21
|
import { OrgCanvas } from "./fleet-canvas.js";
|
|
22
22
|
import { labelOf } from "/assets/instance-nav.js";
|
|
23
23
|
import { CacheReport } from "./fleet-cache.js";
|
|
24
|
+
import { BackendSelect, ModelSelect } from "./settings-dialogs.js";
|
|
24
25
|
|
|
25
26
|
/** #2015: one of the largest processes as text: "python3 18.1 GB (dev-a)". */
|
|
26
27
|
export function memoryHogText(p) {
|
|
@@ -61,7 +62,7 @@ register("fleet", {
|
|
|
61
62
|
newInstance: "New instance", directory: "Directory", directoryHint: "Optional — a workspace is created if empty", topic: "Name", topicHint: "From the directory name",
|
|
62
63
|
topicRequired: "Name is required when Directory is empty", description: "Description", descriptionHint: "What this instance does", model: "Model", modelHint: "e.g. sonnet, opus, gpt-5",
|
|
63
64
|
branch: "Branch (git worktree)", branchHint: "e.g. feature-x", tags: "Tags", tagsHint: "comma-separated, e.g. dev, review", systemPrompt: "System prompt", systemPromptHint: "Custom instructions for this instance",
|
|
64
|
-
fleetDefault: "Use the fleet default", notInstalled: "(not installed)", deprecated: "(deprecated)", instanceCreated: "Instance created",
|
|
65
|
+
fleetDefault: "Use the fleet default", notInstalled: "(not installed)", notSignedIn: "(not signed in — use /login)", tooOld: "(needs {0} or newer)", unchecked: "(not checked yet)", deprecated: "(deprecated)", instanceCreated: "Instance created",
|
|
65
66
|
},
|
|
66
67
|
"zh-TW": {
|
|
67
68
|
hostMemory_elevated: "主機記憶體壓力偏高:新的 agent 一次啟動一個,至少間隔 5 秒",
|
|
@@ -92,7 +93,7 @@ register("fleet", {
|
|
|
92
93
|
newInstance: "新增 instance", directory: "目錄", directoryHint: "可留空——留空會自動建立工作區", topic: "名稱", topicHint: "預設取自目錄名稱",
|
|
93
94
|
topicRequired: "目錄留空時必須填寫名稱", description: "說明", descriptionHint: "這個 instance 做什麼", model: "模型", modelHint: "例如 sonnet、opus、gpt-5",
|
|
94
95
|
branch: "分支(git worktree)", branchHint: "例如 feature-x", tags: "標籤", tagsHint: "以逗號分隔,例如 dev, review", systemPrompt: "System prompt", systemPromptHint: "這個 instance 的自訂指示",
|
|
95
|
-
fleetDefault: "使用 fleet 預設", notInstalled: "(未安裝)", deprecated: "(已淘汰)", instanceCreated: "Instance 已建立",
|
|
96
|
+
fleetDefault: "使用 fleet 預設", notInstalled: "(未安裝)", notSignedIn: "(未登入,請用 /login)", tooOld: "(需要 {0} 以上版本)", unchecked: "(尚未檢查)", deprecated: "(已淘汰)", instanceCreated: "Instance 已建立",
|
|
96
97
|
},
|
|
97
98
|
});
|
|
98
99
|
|
|
@@ -334,8 +335,7 @@ function Config({ lease }) {
|
|
|
334
335
|
<div class="field"><label for="cfg-users">${t("fleet.allowedUsers")}</label><${Secret} id="cfg-users" value=${form.users} onInput=${set("users")} label=${t("fleet.allowedUsers")} />
|
|
335
336
|
<p class="note">${t("fleet.allowedUsersNote")}</p></div></section>
|
|
336
337
|
<section class="card"><h3>${t("fleet.defaults")}</h3>
|
|
337
|
-
<
|
|
338
|
-
${[...(BACKENDS.includes(form.backend) ? [] : [form.backend]), ...BACKENDS].map(b => html`<option key=${b} value=${b}>${b}</option>`)}</select></label></section>
|
|
338
|
+
<div class="field"><label for="cfg-be">${t("fleet.backend")}</label><${BackendSelect} id="cfg-be" value=${form.backend} onChange=${(v) => setForm({ ...form, backend: v })} /></div></section>
|
|
339
339
|
<section class="card"><h3>${t("fleet.roots")}</h3>
|
|
340
340
|
${form.roots.map((r, i) => html`<div key=${i} class="field-row"><input value=${r} placeholder="/path/to/projects" aria-label=${t("fleet.roots")}
|
|
341
341
|
onInput=${(e) => { const roots = [...form.roots]; roots[i] = e.target.value; setForm({ ...form, roots }); }} />
|
|
@@ -522,6 +522,8 @@ function CreateTeam({ onClose, onDone, team = null }) {
|
|
|
522
522
|
export function CreateInstanceDialog({ onClose }) {
|
|
523
523
|
const lease = useLease("create-instance");
|
|
524
524
|
const [backends, setBackends] = useState(null);
|
|
525
|
+
// The backend "fleet default" means (#2098 review P3-3): its models are the ones the model list offers then.
|
|
526
|
+
const [defaultBackend, setDefaultBackend] = useState("claude-code");
|
|
525
527
|
const [f, setF] = useState({ dir: "", topic: "", desc: "", backend: "", model: "", branch: "", tags: "", prompt: "" });
|
|
526
528
|
useEffect(() => {
|
|
527
529
|
(async () => {
|
|
@@ -530,6 +532,7 @@ export function CreateInstanceDialog({ onClose }) {
|
|
|
530
532
|
if (!lease.current()) return;
|
|
531
533
|
const list = bd.backends || [];
|
|
532
534
|
const first = list.find(b => b.installed && !b.deprecated) || list.find(b => b.installed);
|
|
535
|
+
if (typeof bd.defaultBackend === "string" && bd.defaultBackend) setDefaultBackend(bd.defaultBackend);
|
|
533
536
|
setBackends(list);
|
|
534
537
|
setF(x => ({ ...x, backend: first ? first.name : "" }));
|
|
535
538
|
} catch {
|
|
@@ -559,14 +562,23 @@ export function CreateInstanceDialog({ onClose }) {
|
|
|
559
562
|
${field(`${t("fleet.topic")}${f.dir.trim() ? "" : " *"}`, html`<input value=${f.topic} onInput=${set("topic")} placeholder=${t("fleet.topicHint")} />`)}
|
|
560
563
|
${field(t("fleet.description"), html`<input value=${f.desc} onInput=${set("desc")} placeholder=${t("fleet.descriptionHint")} />`)}
|
|
561
564
|
${field(t("fleet.backend"), backends ? html`<select value=${f.backend} onChange=${set("backend")}><option value="">${t("fleet.fleetDefault")}</option>
|
|
562
|
-
${backends.map(b => html`<option key=${b.name} value=${b.name} disabled=${
|
|
563
|
-
${field(t("fleet.model"), html
|
|
565
|
+
${backends.map(b => html`<option key=${b.name} value=${b.name} disabled=${backendBlocked(b)}>${b.name}${b.deprecated ? ` ${t("fleet.deprecated")}` : ""}${backendNote(b)}</option>`)}</select>` : html`<${Skeleton} lines=${1} />`)}
|
|
566
|
+
${field(t("fleet.model"), html`<${ModelSelect} value=${f.model} onChange=${(v) => setF(x => ({ ...x, model: v }))} backend=${f.backend || defaultBackend} inherit=${t("fleet.fleetDefault")} />`)}
|
|
564
567
|
${field(t("fleet.branch"), html`<input value=${f.branch} onInput=${set("branch")} placeholder=${t("fleet.branchHint")} />`)}
|
|
565
568
|
${field(t("fleet.tags"), html`<input value=${f.tags} onInput=${set("tags")} placeholder=${t("fleet.tagsHint")} />`)}
|
|
566
569
|
${field(t("fleet.systemPrompt"), html`<textarea rows="3" value=${f.prompt} onInput=${set("prompt")} placeholder=${t("fleet.systemPromptHint")}></textarea>`)}
|
|
567
570
|
</${FormDialog}>`;
|
|
568
571
|
}
|
|
569
572
|
|
|
573
|
+
// #2072: a backend this host cannot run is shown, disabled, with why; one not checked yet stays selectable.
|
|
574
|
+
const backendState = (b) => b.state ?? (b.installed ? "usable" : b.unknown ? "unknown" : "not_installed");
|
|
575
|
+
function backendBlocked(b) { return ["not_installed", "not_signed_in", "too_old"].includes(backendState(b)); }
|
|
576
|
+
function backendNote(b) {
|
|
577
|
+
const s = backendState(b);
|
|
578
|
+
return s === "not_installed" ? ` ${t("fleet.notInstalled")}` : s === "not_signed_in" ? ` ${t("fleet.notSignedIn")}`
|
|
579
|
+
: s === "too_old" ? ` ${t("fleet.tooOld", b.minVersion ?? "?")}` : s === "unknown" ? ` ${t("fleet.unchecked")}` : "";
|
|
580
|
+
}
|
|
581
|
+
|
|
570
582
|
// ── Cron in words (#1724: in the page's language) ──
|
|
571
583
|
/**
|
|
572
584
|
* A cron expression as a person says it — "Weekdays 18:00", 「每週一到五 18:00」, "Every 15 minutes" — or the expression
|
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
visibilityDefault, worstImpact,
|
|
32
32
|
} from "./settings-model.js";
|
|
33
33
|
import {
|
|
34
|
-
AgentDialog, BackendSelect, BotDialog, ChipList, ClassicDialog, Drawer, Feedback, Impact, Label, NewBotDialog, Override,
|
|
34
|
+
AgentDialog, BackendSelect, BotDialog, ChipList, ClassicDialog, Drawer, Feedback, Impact, Label, ModelSelect, NewBotDialog, Override,
|
|
35
35
|
Select, ask, impactText,
|
|
36
36
|
} from "./settings-dialogs.js";
|
|
37
37
|
import { SetupWizard } from "./settings-wizard.js";
|
|
@@ -43,6 +43,7 @@ import { setPreviewOptIn } from "./panel-chat.js";
|
|
|
43
43
|
import { startTour } from "./chat-tour.js";
|
|
44
44
|
import "./chat-strings.js";
|
|
45
45
|
import { AppearanceSection } from "./settings-appearance.js";
|
|
46
|
+
import { GuestsCard } from "./settings-guests.js";
|
|
46
47
|
|
|
47
48
|
const tn = (k, ...v) => t(`settings.${k}`, ...v);
|
|
48
49
|
const ICONS = { device: "user", appearance: "palette", agents: "bot", bots: "plug", classic: "room", general: "sliders", web: "globe", advanced: "code" };
|
|
@@ -707,7 +708,8 @@ function General({ ctx }) {
|
|
|
707
708
|
<div class="field">${L(tn("language"), "defaults.locale", "g-locale")}<${Select} id="g-locale" value=${f.locale} onChange=${set("locale")}
|
|
708
709
|
options=${localeOptions(f.locale).map((o) => (o.auto ? { value: "", label: tn("localeAuto") } : o))} /></div>
|
|
709
710
|
<div class="field">${L(tn("defaultBackend"), "defaults.backend", "g-be")}<${BackendSelect} id="g-be" value=${f.backend} onChange=${set("backend")} /></div>
|
|
710
|
-
<div class="field">${L(tn("defaultModel"), "defaults.model", "g-model")}
|
|
711
|
+
<div class="field">${L(tn("defaultModel"), "defaults.model", "g-model")}<${ModelSelect} id="g-model" value=${f.model} onChange=${set("model")}
|
|
712
|
+
backend=${f.backend || "claude-code"} inherit=${tn("modelCliDefault")} /></div>
|
|
711
713
|
<div class="field">${L(`${tn("autoPause")} (${tn("minutes")})`, "defaults.auto_pause_after", "g-auto")}<input id="g-auto" type="number" min="0" step="1" class=${errors.auto ? "invalid" : ""} value=${f.auto} onInput=${(e) => set("auto")(e.target.value)} /><${Feedback} error=${errors.auto} /></div>
|
|
712
714
|
<div class="field">${L(tn("toolProgress"), "defaults.tool_progress", "g-tp")}<${Select} id="g-tp" value=${f.tool_progress} onChange=${set("tool_progress")} options=${TOOL_PROGRESS} /></div>
|
|
713
715
|
<div class="field">${L(tn("crossInstanceVisibility"), "defaults.cross_instance_visibility", "g-vis")}<${Select} id="g-vis" value=${f.visibility} onChange=${set("visibility")} options=${VISIBILITY_MODES} />
|
|
@@ -865,6 +867,7 @@ function WebSection({ ctx, staged }) {
|
|
|
865
867
|
</div>
|
|
866
868
|
</section>
|
|
867
869
|
</fieldset>
|
|
870
|
+
<${GuestsCard} ctx=${ctx} staged=${staged} disabled=${overPublicLink} />
|
|
868
871
|
<section class="card s-web-readonly"><h3>${tn("webReadOnlyTitle")}</h3>
|
|
869
872
|
<p class="note">${tn("webReadOnlyLead")}</p>
|
|
870
873
|
<dl class="s-web-ro">${WEB_READ_ONLY.map((k) => html`<div key=${k}><dt><code>${k}</code></dt><dd>${shown(k)}</dd></div>`)}</dl>
|
|
@@ -0,0 +1,92 @@
|
|
|
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
|
+
/**
|
|
25
|
+
* Drop every remembered answer. Only tests call it today: a page keeps an answer TTL_MS, so a sign-in or a newly
|
|
26
|
+
* cached catalog shows within that time, or at once on the next page load.
|
|
27
|
+
*/
|
|
28
|
+
export function forgetCatalogs() { cache.clear(); }
|
|
29
|
+
|
|
30
|
+
function useAnswer(url) {
|
|
31
|
+
const [value, setValue] = useState(undefined); // undefined: loading; null: unavailable
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (!url) { setValue(null); return undefined; }
|
|
34
|
+
let live = true;
|
|
35
|
+
setValue(undefined);
|
|
36
|
+
cached(url).then((v) => { if (live) setValue(v); });
|
|
37
|
+
return () => { live = false; };
|
|
38
|
+
}, [url]);
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The host's backends with their state ({ name, state, minVersion, … }), or null while loading or unavailable. */
|
|
43
|
+
export function useBackendRows() {
|
|
44
|
+
const answer = useAnswer("/ui/backends");
|
|
45
|
+
return answer && Array.isArray(answer.backends) ? answer.backends : answer === undefined ? undefined : null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The model catalog of `backend` for `instance` (its account) — [] when none is cached; undefined while loading. */
|
|
49
|
+
export function useModelCatalog(backend, instance) {
|
|
50
|
+
const url = backend ? `/ui/models?backend=${encodeURIComponent(backend)}${instance ? `&instance=${encodeURIComponent(instance)}` : ""}` : null;
|
|
51
|
+
const answer = useAnswer(url);
|
|
52
|
+
if (answer === undefined) return undefined;
|
|
53
|
+
return answer && Array.isArray(answer.models) ? answer.models : [];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** A state that keeps a backend from being chosen (shown, disabled, with why); "unknown" never does. */
|
|
57
|
+
export const BLOCKING_STATES = ["not_installed", "not_signed_in", "too_old"];
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The backend select's options: every backend the page knows, in its order, each with what the host said about it.
|
|
61
|
+
* The current value is always selectable — a probe never disables what an agent already runs — and one the page does
|
|
62
|
+
* not know (a removed backend) stays first, as before.
|
|
63
|
+
*/
|
|
64
|
+
export function backendChoices(known, rows, value, words) {
|
|
65
|
+
const byName = new Map((rows || []).map((r) => [r.name, r]));
|
|
66
|
+
const choice = (name) => {
|
|
67
|
+
const row = byName.get(name), state = row ? row.state ?? (row.installed ? "usable" : row.unknown ? "unknown" : "not_installed") : null;
|
|
68
|
+
const blocked = !!state && BLOCKING_STATES.includes(state) && name !== value;
|
|
69
|
+
const note = state === "not_installed" ? words.notInstalled : state === "not_signed_in" ? words.notSignedIn
|
|
70
|
+
: state === "too_old" ? words.tooOld(row.minVersion ?? "?") : state === "unknown" && name !== value ? words.unchecked : "";
|
|
71
|
+
return { value: name, label: note ? `${name} — ${note}` : name, disabled: blocked };
|
|
72
|
+
};
|
|
73
|
+
const list = known.map(choice);
|
|
74
|
+
return known.includes(value) || !value ? list : [{ value, label: words.removed(value), disabled: false }, ...list];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The id of the "type another model" choice (never a model name: it has a space). */
|
|
78
|
+
export const OTHER_MODEL = "\u0000other";
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The model select's options: the inherited choice (empty value), the current model first when the catalog does not
|
|
82
|
+
* list it (marked), the catalog, and "Other…" for a name the CLI does not list.
|
|
83
|
+
*/
|
|
84
|
+
export function modelChoices(catalog, value, words, inherit) {
|
|
85
|
+
const list = [];
|
|
86
|
+
if (inherit) list.push({ value: "", label: inherit });
|
|
87
|
+
const ids = new Set((catalog || []).map((m) => m.id));
|
|
88
|
+
if (value && !ids.has(value)) list.push({ value, label: `${value} — ${words.notListed}` });
|
|
89
|
+
for (const m of catalog || []) list.push({ value: m.id, label: m.description ? `${m.label || m.id} — ${m.description}` : (m.label || m.id) });
|
|
90
|
+
list.push({ value: OTHER_MODEL, label: words.other });
|
|
91
|
+
return list;
|
|
92
|
+
}
|
|
@@ -13,6 +13,8 @@ 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,
|
|
@@ -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>
|
|
@@ -591,7 +621,8 @@ export function ClassicDialog({ room, ctx, onClose }) {
|
|
|
591
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}>
|
|
592
622
|
<div class="grid2">
|
|
593
623
|
<div class="field">${L(tn("backend"), "classic.backend", "cl-be")}<${BackendSelect} id="cl-be" value=${f.backend} onChange=${set("backend")} /></div>
|
|
594
|
-
<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>
|
|
595
626
|
<div class="field">${L(`${tn("autoPause")} (${tn("minutes")})`, "classic.auto_pause_after", "cl-auto")}
|
|
596
627
|
<${Override} inherit=${f.inherit.auto_pause_after} onInherit=${setInherit("auto_pause_after")}>
|
|
597
628
|
<input id="cl-auto" type="number" min="0" step="1" class=${errors.auto ? "invalid" : ""} disabled=${f.inherit.auto_pause_after}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// #1920 phase 3 (P3b-2): Settings → Web → Guests — guest sign-in for people who are not admins. The switches (guest
|
|
2
|
+
// sign-in, guests may chat), the agents an open connection's guests may reach, and every guest account: who, on which
|
|
3
|
+
// platform, what it reaches now, its sessions and devices, with Sign out (at once) and Narrow (staged with the rest).
|
|
4
|
+
// Changes go through Review changes → Apply like every Settings edit: one that widens what guests reach waits for a
|
|
5
|
+
// fleet admin's confirmation (the server's settings-change.ts), one that narrows it applies at once.
|
|
6
|
+
import { html, useEffect, useMemo, useState } from "/assets/app-html.js";
|
|
7
|
+
import { t } from "/assets/app-i18n.js";
|
|
8
|
+
import { toast } from "/assets/ui-toast.js";
|
|
9
|
+
import { confirmDialog } from "/assets/ui-confirm.js";
|
|
10
|
+
import { api } from "./settings-confirm.js";
|
|
11
|
+
import "./settings-strings.js";
|
|
12
|
+
|
|
13
|
+
const tn = (k, ...v) => t(`settings.${k}`, ...v);
|
|
14
|
+
const PLATFORMS = { discord: "Discord", telegram: "Telegram" };
|
|
15
|
+
const own = (o, k) => !!o && Object.prototype.hasOwnProperty.call(o, k);
|
|
16
|
+
const copyLists = (m) => { const out = {}; for (const [k, v] of Object.entries(m || {})) if (Array.isArray(v)) out[k] = [...v]; return out; };
|
|
17
|
+
const sameList = (a, b) => (a || []).length === (b || []).length && (a || []).every((n) => (b || []).includes(n));
|
|
18
|
+
const accountKey = (g) => `${g.adapterId}:${g.userId}`;
|
|
19
|
+
|
|
20
|
+
/** What changed against the configuration, as the PUT /api/settings/fleet/web body's `guests` (null: nothing). */
|
|
21
|
+
export function guestsBody(original, f) {
|
|
22
|
+
const out = {};
|
|
23
|
+
if (f.enabled !== original.enabled) out.enabled = f.enabled ? true : null; // off is the default
|
|
24
|
+
if (f.chat !== original.chat) out.chat = f.chat ? null : false; // on is the default
|
|
25
|
+
for (const key of ["open_instances", "narrow"]) {
|
|
26
|
+
const was = original[key], now = f[key], diff = {};
|
|
27
|
+
for (const k of new Set([...Object.keys(was), ...Object.keys(now)])) {
|
|
28
|
+
const a = own(was, k) ? was[k] : undefined, b = own(now, k) ? now[k] : undefined;
|
|
29
|
+
if (a === undefined && b === undefined) continue;
|
|
30
|
+
if (a !== undefined && b !== undefined && sameList(a, b)) continue;
|
|
31
|
+
// open_instances: none named = the entry leaves. narrow: undefined = lifted; [] = narrowed to nothing.
|
|
32
|
+
diff[k] = b === undefined || (key === "open_instances" && !b.length) ? null : [...b];
|
|
33
|
+
}
|
|
34
|
+
if (Object.keys(diff).length) out[key] = diff;
|
|
35
|
+
}
|
|
36
|
+
return Object.keys(out).length ? out : null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** `away`: narrowed names it no longer reaches — listed (marked) only so they can be unticked; unticked, they leave. */
|
|
40
|
+
function Names({ id, all, value, onChange, away = new Set() }) {
|
|
41
|
+
const chosen = new Set(value || []);
|
|
42
|
+
return html`<div class="guest-names" id=${id} role="group">${all.map((n) => html`<label key=${n} class=${`check guest-name${away.has(n) ? " guest-away" : ""}`}>
|
|
43
|
+
<input type="checkbox" checked=${chosen.has(n)} onChange=${(e) => { const s = new Set(chosen); if (e.target.checked) s.add(n); else s.delete(n); onChange(all.filter((x) => s.has(x))); }} />
|
|
44
|
+
<span>${n}${away.has(n) ? html` <span class="note">${tn("guestNotReachable")}</span>` : null}</span></label>`)}</div>`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function GuestsCard({ ctx, staged, disabled = false }) {
|
|
48
|
+
const g = (ctx.fleet.web && ctx.fleet.web.guests) || {};
|
|
49
|
+
const original = useMemo(() => ({ enabled: g.enabled === true, chat: g.chat !== false, open_instances: copyLists(g.open_instances), narrow: copyLists(g.narrow) }), []);
|
|
50
|
+
const [f, setF] = useState(() => ({ ...original, open_instances: copyLists(original.open_instances), narrow: copyLists(original.narrow) }));
|
|
51
|
+
const [overview, setOverview] = useState(null), [failed, setFailed] = useState(false), [narrowing, setNarrowing] = useState(null), [msg, setMsg] = useState(null);
|
|
52
|
+
const load = async () => {
|
|
53
|
+
let r = null;
|
|
54
|
+
try { r = await api("/api/settings/guests"); } catch { r = null; }
|
|
55
|
+
if (r && r.ok && r.body) { setOverview(r.body); setFailed(false); } else setFailed(true);
|
|
56
|
+
};
|
|
57
|
+
useEffect(() => { load(); }, []);
|
|
58
|
+
const set = (patch) => setF((x) => ({ ...x, ...patch }));
|
|
59
|
+
const setList = (key, k, list) => setF((x) => { const m = { ...x[key] }; if (list === undefined) delete m[k]; else m[k] = list; return { ...x, [key]: m }; });
|
|
60
|
+
const review = () => {
|
|
61
|
+
const body = guestsBody(original, f);
|
|
62
|
+
if (body) ctx.stage("guests", { label: tn("guestsTitle"), impact: "now", request: { method: "PUT", url: "/api/settings/fleet/web", body: { guests: body } } });
|
|
63
|
+
else if (staged.has("guests")) ctx.unstage("guests");
|
|
64
|
+
setMsg(tn("readyApply"));
|
|
65
|
+
};
|
|
66
|
+
const signOut = async (guest) => {
|
|
67
|
+
const yes = await confirmDialog({ title: tn("guestSignOutTitle"), message: tn("guestSignOutConfirm", guest.name), confirmLabel: tn("guestSignOut"), danger: true });
|
|
68
|
+
if (!yes) return;
|
|
69
|
+
let r = null;
|
|
70
|
+
try { r = await api("/api/settings/guests/revoke", { method: "POST", body: JSON.stringify({ adapterId: guest.adapterId, userId: guest.userId }) }); } catch { r = null; }
|
|
71
|
+
if (r && r.ok) { toast(tn(r.body && r.body.durable === false ? "guestSignedOutNotDurable" : "guestSignedOut", guest.name)); load(); }
|
|
72
|
+
else toast(tn("guestSignOutFailed"), false);
|
|
73
|
+
};
|
|
74
|
+
const instances = (overview && overview.instances) || [];
|
|
75
|
+
const open = ((overview && overview.connections) || []).filter((c) => c.mode === "open");
|
|
76
|
+
const guests = (overview && overview.guests) || [];
|
|
77
|
+
const when = (ms) => (typeof ms === "number" ? tn("guestLastActive", new Date(ms).toLocaleString()) : tn("guestNotSignedIn"));
|
|
78
|
+
return html`<section class="card s-guests"><h3>${tn("guestsTitle")}</h3>
|
|
79
|
+
<p class="note">${tn("guestsHint")}</p>
|
|
80
|
+
<fieldset class="s-guest-fields" disabled=${disabled}>
|
|
81
|
+
<label class="check"><input id="guestsEnabled" type="checkbox" checked=${f.enabled} onChange=${(e) => set({ enabled: e.target.checked })} /><span>${tn("guestsEnabled")}</span></label>
|
|
82
|
+
<p class="note">${tn("guestsEnabledHint")}</p>
|
|
83
|
+
<label class="check"><input id="guestsChat" type="checkbox" checked=${f.chat} onChange=${(e) => set({ chat: e.target.checked })} /><span>${tn("guestsChat")}</span></label>
|
|
84
|
+
<p class="note">${tn("guestsChatHint")}</p>
|
|
85
|
+
${open.map((c) => html`<div key=${c.id} class="field guest-open"><span class="label">${tn("guestsOpenFor", c.id, PLATFORMS[c.type] || c.type)}</span>
|
|
86
|
+
<p class="note">${tn("guestsOpenHint")}</p>
|
|
87
|
+
<${Names} id=${`guestsOpen-${c.id}`} all=${instances} value=${own(f.open_instances, c.id) ? f.open_instances[c.id] : []} onChange=${(list) => setList("open_instances", c.id, list)} /></div>`)}
|
|
88
|
+
<h4>${tn("guestsList")}</h4>
|
|
89
|
+
${failed ? html`<p class="note" role="status">${tn("guestsLoadFailed")} <button type="button" class="btn btn-sm" onClick=${load}>${t("app.retry")}</button></p>`
|
|
90
|
+
: !overview ? html`<p class="note">${t("app.loading")}</p>`
|
|
91
|
+
: !guests.length ? html`<p class="note" id="guestsNone">${tn("guestsNone")}</p>`
|
|
92
|
+
: html`<ul class="guest-list">${guests.map((guest) => {
|
|
93
|
+
const k = accountKey(guest), narrowed = own(f.narrow, k) ? f.narrow[k] : undefined;
|
|
94
|
+
return html`<li key=${k} class="guest-row" data-account=${k}>
|
|
95
|
+
<div class="guest-who"><strong>${guest.name}</strong> <span class="note">${PLATFORMS[guest.platform] || guest.platform} · ${guest.userId}</span></div>
|
|
96
|
+
<div class="note guest-facts">${tn("guestFacts", guest.scope.length, guest.sessions, guest.devices)} · ${when(guest.lastSeen)}</div>
|
|
97
|
+
<div class="note guest-narrow">${narrowed === undefined ? tn("guestNotNarrowed") : tn("guestNarrowedTo", narrowed.length ? narrowed.join(", ") : tn("guestNothing"))}</div>
|
|
98
|
+
<div class="guest-actions">
|
|
99
|
+
<button type="button" class="btn btn-sm" aria-expanded=${narrowing === k ? "true" : "false"} onClick=${() => setNarrowing(narrowing === k ? null : k)}>${tn("guestNarrow")}</button>
|
|
100
|
+
${narrowed !== undefined ? html`<button type="button" class="btn btn-sm" onClick=${() => setList("narrow", k, undefined)}>${tn("guestNarrowLift")}</button>` : null}
|
|
101
|
+
${guest.sessions || guest.devices ? html`<button type="button" class="btn btn-sm btn-danger" onClick=${() => signOut(guest)}>${tn("guestSignOut")}</button>` : null}
|
|
102
|
+
</div>
|
|
103
|
+
${narrowing === k ? (() => {
|
|
104
|
+
// #2104 review: only what this account can reach (narrowing never widens); a narrowed name it no longer
|
|
105
|
+
// reaches is listed to be unticked, marked.
|
|
106
|
+
const reach = guest.reach || guest.scope, away = new Set((narrowed || []).filter((n) => !reach.includes(n)));
|
|
107
|
+
return html`<div class="guest-narrow-edit"><p class="note">${tn("guestNarrowHint")}</p>
|
|
108
|
+
${reach.length || away.size ? html`<${Names} id=${`guestNarrow-${k}`} all=${[...reach, ...away]} away=${away} value=${narrowed === undefined ? guest.scope : narrowed} onChange=${(list) => setList("narrow", k, list)} />`
|
|
109
|
+
: html`<p class="note">${tn("guestReachesNothing")}</p>`}</div>`;
|
|
110
|
+
})() : null}
|
|
111
|
+
</li>`;
|
|
112
|
+
})}</ul>`}
|
|
113
|
+
</fieldset>
|
|
114
|
+
<div class="save-row"><button type="button" class="btn btn-primary" id="guestsReview" disabled=${disabled} onClick=${review}>${tn("guestsReview")}</button>
|
|
115
|
+
${msg ? html`<span class="note" role="status">${msg}</span>` : null}</div>
|
|
116
|
+
</section>`;
|
|
117
|
+
}
|
|
@@ -203,6 +203,9 @@ register("settings", {
|
|
|
203
203
|
changesApplied: "Changes applied. AgEnD is reconciling affected Agents.",
|
|
204
204
|
reviewChanges: "Review changes",
|
|
205
205
|
backendUnavailable: "{0} (removed — choose another backend)",
|
|
206
|
+
backendNotInstalled: "not installed", backendNotSignedIn: "not signed in (use /login)", backendTooOld: "needs {0} or newer", backendUnchecked: "not checked yet",
|
|
207
|
+
modelNotListed: "current — not in this CLI's list", modelOther: "Other…", modelOtherLabel: "Model name", modelOtherPlaceholder: "a model name this CLI accepts",
|
|
208
|
+
modelNone: "— none —", inheritValue: "inherit ({0})", modelCliDefault: "the CLI's own default",
|
|
206
209
|
readyApply: "Ready to apply",
|
|
207
210
|
copied: "Copied",
|
|
208
211
|
expectedMapping: "expected a mapping",
|
|
@@ -425,6 +428,17 @@ register("settings", {
|
|
|
425
428
|
webTerminalTtl: "Terminal session minutes (1–20)",
|
|
426
429
|
webTerminalTtlRange: "Must be a whole number from 1 to 20",
|
|
427
430
|
webReadOnlyTitle: "Set in fleet.yaml",
|
|
431
|
+
guestsTitle: "Guests", guestsReview: "Review guest changes",
|
|
432
|
+
guestsHint: "People who are not admins can sign in to the web as guests: they send /web where they already chat and get a code privately. A guest sees only the agents they can already use in chat, and their messages are recognition only.",
|
|
433
|
+
guestsEnabled: "Guest sign-in", guestsEnabledHint: "Off: no one can sign in as a guest, and every guest session ends. Turning it on waits for a fleet admin's confirmation.",
|
|
434
|
+
guestsChat: "Guests may chat", guestsChatHint: "Off: guests can read their agents' chats but not send, attach, click a reply's buttons or press Cancel.",
|
|
435
|
+
guestsOpenFor: "Agents for guests of {0} ({1})", guestsOpenHint: "This connection is open to everyone, so it has no list of people: its guests reach only the agents you tick here (none by default). Adding agents waits for a fleet admin's confirmation.",
|
|
436
|
+
guestsList: "Guest accounts", guestsNone: "No guest has signed in yet.", guestsLoadFailed: "The guest list could not be loaded.",
|
|
437
|
+
guestFacts: "Agents: {0} · Sessions: {1} · Remembered browsers: {2}", guestLastActive: "Last active {0}", guestNotSignedIn: "Not signed in now",
|
|
438
|
+
guestNotNarrowed: "Reaches every agent they can use in chat.", guestNarrowedTo: "Narrowed to: {0}", guestNothing: "nothing",
|
|
439
|
+
guestNarrow: "Narrow", guestNotReachable: "(no longer reachable)", guestReachesNothing: "This account reaches no agent from chat right now, so there is nothing to narrow.", guestNarrowLift: "Remove narrowing", guestNarrowHint: "Tick what this guest may still reach. Narrowing only takes away (it applies at once); widening or removing it waits for a fleet admin's confirmation.",
|
|
440
|
+
guestSignOut: "Sign out", guestSignOutTitle: "Sign out this guest?", guestSignOutConfirm: "{0}'s guest sessions end now, their sign-in code stops working and their browsers are forgotten. They can sign in again with /web while guest sign-in is on.",
|
|
441
|
+
guestSignedOut: "{0} is signed out.", guestSignedOutNotDurable: "{0} is signed out for now, but it could not be saved: a restart may bring their sessions back.", guestSignOutFailed: "That did not work — try again.",
|
|
428
442
|
webReadOnlyLead: "Read-only here: these decide what the dashboard listens on and exposes. Edit fleet.yaml, then run agend reload (web.preview_port and web.preview_origin take a restart of AgEnD).",
|
|
429
443
|
notSet: "not set (default)",
|
|
430
444
|
defaultsTitle: "Defaults",
|
|
@@ -697,6 +711,9 @@ register("settings", {
|
|
|
697
711
|
changesApplied: "變更已套用,AgEnD 正在協調受影響的 Agent。",
|
|
698
712
|
reviewChanges: "檢查變更",
|
|
699
713
|
backendUnavailable: "{0}(已移除,請選擇其他 backend)",
|
|
714
|
+
backendNotInstalled: "未安裝", backendNotSignedIn: "未登入(請用 /login)", backendTooOld: "需要 {0} 以上版本", backendUnchecked: "尚未檢查",
|
|
715
|
+
modelNotListed: "目前使用中 — 不在這個 CLI 的清單裡", modelOther: "其他…", modelOtherLabel: "模型名稱", modelOtherPlaceholder: "這個 CLI 接受的模型名稱",
|
|
716
|
+
modelNone: "— 無 —", inheritValue: "沿用({0})", modelCliDefault: "CLI 自己的預設",
|
|
700
717
|
readyApply: "可套用變更",
|
|
701
718
|
copied: "已複製",
|
|
702
719
|
expectedMapping: "預期為 mapping",
|
|
@@ -918,6 +935,17 @@ register("settings", {
|
|
|
918
935
|
webTerminalTtl: "終端機 session 分鐘數(1–20)",
|
|
919
936
|
webTerminalTtlRange: "必須是 1 到 20 之間的整數",
|
|
920
937
|
webReadOnlyTitle: "在 fleet.yaml 設定",
|
|
938
|
+
guestsTitle: "訪客", guestsReview: "檢視訪客變更",
|
|
939
|
+
guestsHint: "不是管理者的人可以用訪客身分登入網頁:在他本來就能聊天的地方傳 /web,就會私下收到登入碼。訪客只會看到在聊天中本來就能用的 agent,他的訊息僅供辨識。",
|
|
940
|
+
guestsEnabled: "訪客登入", guestsEnabledHint: "關閉:沒有人能以訪客登入,所有訪客 session 都會結束。開啟需 fleet admin 確認。",
|
|
941
|
+
guestsChat: "訪客可以聊天", guestsChatHint: "關閉:訪客可以讀他的 agent 的聊天,但不能送訊息、附加檔案、按回覆的按鈕或按取消。",
|
|
942
|
+
guestsOpenFor: "{0}({1})訪客可用的 agent", guestsOpenHint: "這個連線對所有人開放,沒有人員名單:它的訪客只能用你在這裡勾選的 agent(預設沒有)。新增 agent 需 fleet admin 確認。",
|
|
943
|
+
guestsList: "訪客帳號", guestsNone: "還沒有訪客登入過。", guestsLoadFailed: "無法載入訪客清單。",
|
|
944
|
+
guestFacts: "agent:{0} · session:{1} · 記住的瀏覽器:{2}", guestLastActive: "最後使用 {0}", guestNotSignedIn: "目前未登入",
|
|
945
|
+
guestNotNarrowed: "可用他在聊天中能用的所有 agent。", guestNarrowedTo: "已縮小為:{0}", guestNothing: "無",
|
|
946
|
+
guestNarrow: "縮小範圍", guestNotReachable: "(已無法使用)", guestReachesNothing: "這個帳號目前在聊天中沒有能用的 agent,沒有可以縮小的範圍。", guestNarrowLift: "取消縮小", guestNarrowHint: "勾選這位訪客仍可使用的 agent。縮小只會拿走權限(立即生效);放寬或取消縮小需 fleet admin 確認。",
|
|
947
|
+
guestSignOut: "登出", guestSignOutTitle: "登出這位訪客?", guestSignOutConfirm: "{0} 的訪客 session 會立即結束,登入碼失效,記住的瀏覽器也會被忘記。訪客登入開啟期間,他可以再傳 /web 登入。",
|
|
948
|
+
guestSignedOut: "已登出 {0}。", guestSignedOutNotDurable: "已暫時登出 {0},但無法保存:重新啟動後他的 session 可能會恢復。", guestSignOutFailed: "沒有成功,請再試一次。",
|
|
921
949
|
webReadOnlyLead: "這裡僅供檢視:這些設定決定儀表板監聽和公開的內容。請編輯 fleet.yaml 後執行 agend reload(web.preview_port 與 web.preview_origin 需要重新啟動 AgEnD)。",
|
|
922
950
|
notSet: "未設定(預設)",
|
|
923
951
|
defaultsTitle: "預設值",
|
|
@@ -74,6 +74,7 @@ register("app", {
|
|
|
74
74
|
hydrating: "Loading what is open now…", hydrateFailed: "Could not load what is open now (prompts, ticks).",
|
|
75
75
|
skip: "Skip to the conversation", loadFailed: "This could not be loaded.", retry: "Try again", loading: "Loading…",
|
|
76
76
|
noInstances: "No instances yet", noInstancesHint: "Create one to start chatting.",
|
|
77
|
+
guestChip: "Guest", guestAs: "Guest · {0}", guestAsOn: "Guest · {0} ({1})", guestHint: "You see only the agents you can already use in chat.",
|
|
77
78
|
session: "Session", signIn: "Sign in", signedIn: "Signed in", signedOut: "Not signed in", thisDevice: "This device", devicePrefs: "Theme and language",
|
|
78
79
|
lastActive: "active {0}", justNow: "just now", minAgo: "{0} min ago", hrAgo: "{0} h ago", signOut: "Sign out",
|
|
79
80
|
signOutAll: "Sign out everywhere", devices: "Signed-in devices", expires: "Ends by {0} at the latest, or after {1} unused",
|
|
@@ -106,6 +107,7 @@ register("app", {
|
|
|
106
107
|
hydrating: "正在載入目前的狀態…", hydrateFailed: "無法載入目前的狀態(提示、送達標記)。",
|
|
107
108
|
skip: "跳到對話", loadFailed: "無法載入。", retry: "再試一次", loading: "載入中…",
|
|
108
109
|
noInstances: "還沒有 instance", noInstancesHint: "建立一個就能開始聊天。",
|
|
110
|
+
guestChip: "訪客", guestAs: "訪客 · {0}", guestAsOn: "訪客 · {0}({1})", guestHint: "你只會看到在聊天中本來就能用的 agent。",
|
|
109
111
|
session: "登入狀態", signIn: "登入", signedIn: "已登入", signedOut: "尚未登入", thisDevice: "這台裝置", devicePrefs: "主題與語言",
|
|
110
112
|
lastActive: "{0}活動", justNow: "剛剛", minAgo: "{0} 分鐘前", hrAgo: "{0} 小時前", signOut: "登出",
|
|
111
113
|
signOutAll: "全部登出", devices: "已登入的裝置", expires: "最晚 {0} 結束;閒置 {1} 也會結束",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
// - The status frame's prefs_rev: another device changed something; the host's values are read again and applied. A
|
|
19
19
|
// revision counts as seen only once its read succeeded (a failed one is tried again, at most every few seconds).
|
|
20
20
|
import { deviceLanguage, lang, setLang } from "./app-i18n.js";
|
|
21
|
-
import { appStore } from "./app-store.js";
|
|
21
|
+
import { appStore, guestPage } from "./app-store.js";
|
|
22
22
|
|
|
23
23
|
const SYNCED_FLAG = "agend_prefs_synced";
|
|
24
24
|
const RETRY_MS = 5000;
|
|
@@ -88,9 +88,9 @@ if (host) {
|
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
/** This page may change the host's preferences: the signed-in app, not over the public link. */
|
|
91
|
+
/** This page may change the host's preferences: the signed-in app, not over the public link, not a guest's (#1920). */
|
|
92
92
|
export function prefsWritable() {
|
|
93
|
-
return !!initial && !(typeof document !== "undefined" && document.body && document.body.dataset.publicLink === "1");
|
|
93
|
+
return !!initial && !guestPage() && !(typeof document !== "undefined" && document.body && document.body.dataset.publicLink === "1");
|
|
94
94
|
}
|
|
95
95
|
/**
|
|
96
96
|
* The value in force for a key, in memory — never only in this browser's storage (a full storage must not bring back
|
|
@@ -7,7 +7,7 @@ import { Toasts } from "./ui-toast.js";
|
|
|
7
7
|
import { ConfirmHost } from "./ui-confirm.js";
|
|
8
8
|
import { ColorDialogHost } from "./identity-color.js";
|
|
9
9
|
import { t, lang, setLang, onLang } from "./app-i18n.js";
|
|
10
|
-
import { appStore, createStore, useStore } from "./app-store.js";
|
|
10
|
+
import { appStore, createStore, guestBadge, guestPage, useStore } from "./app-store.js";
|
|
11
11
|
import { navStore } from "./app-nav.js";
|
|
12
12
|
import { chatPath, viewPath, settingsPath, routeKey, NEEDS_PATH, detailsPath, fleetPath } from "./app-route.js";
|
|
13
13
|
import { SessionMenu } from "./app-session.js";
|
|
@@ -291,6 +291,22 @@ function Sidebar({ route, onNewInstance, viewOnly }) {
|
|
|
291
291
|
<${SidebarResize} />
|
|
292
292
|
</aside>`;
|
|
293
293
|
}
|
|
294
|
+
// #1920 phase 3: a guest's sidebar — its own agents to pick from, who it is signed in as, and its session; no fleet,
|
|
295
|
+
// Needs you, View, Settings, new agent or widget (each is an admin's, and the server refuses a guest them anyway).
|
|
296
|
+
if (guestPage()) {
|
|
297
|
+
return html`<aside id="sidebar" class="sidebar" aria-label=${t("app.menu")}>
|
|
298
|
+
<div class="side-head" data-trust="navigation">
|
|
299
|
+
<a class="brand" href="/ui" onClick=${closeDrawer}>${t("app.brand")}</a>
|
|
300
|
+
<button type="button" class="icon-btn side-collapse" onClick=${toggleSidebar} aria-label=${narrow() ? t("app.closeMenu") : t("app.collapse")} title=${narrow() ? t("app.closeMenu") : t("app.collapse")} aria-controls="sidebar"><${Icon} name="sidebar" /></button>
|
|
301
|
+
</div>
|
|
302
|
+
<${InstanceNav} route=${route} items=${app.instances} loaded=${app.ready !== false} exec=${app.exec} awaiting=${app.awaiting} onPick=${closeDrawer} />
|
|
303
|
+
<div class="side-foot" data-trust="navigation">
|
|
304
|
+
<${GuestChip} />
|
|
305
|
+
<${SessionMenu} />
|
|
306
|
+
</div>
|
|
307
|
+
<${SidebarResize} />
|
|
308
|
+
</aside>`;
|
|
309
|
+
}
|
|
294
310
|
// #1675: the whole fleet, on its Org chart (#1389), from anywhere — top left, so at the top of a phone's drawer too. The
|
|
295
311
|
// "Fleet" row under it goes to the open instance's Details (#1523 N2); this one is the Fleet panel, any of its tabs.
|
|
296
312
|
const onFleet = !!route && route.panel === "fleet";
|
|
@@ -319,6 +335,17 @@ function Sidebar({ route, onNewInstance, viewOnly }) {
|
|
|
319
335
|
</aside>`;
|
|
320
336
|
}
|
|
321
337
|
|
|
338
|
+
/** #1920 phase 3: "Guest · <name> (<platform>)" — who this guest's page is signed in as; what a guest sees, in its title. */
|
|
339
|
+
const PLATFORM_NAMES = { discord: "Discord", telegram: "Telegram" };
|
|
340
|
+
function GuestChip() {
|
|
341
|
+
const { name, platform } = guestBadge();
|
|
342
|
+
const where = PLATFORM_NAMES[platform] || platform;
|
|
343
|
+
const label = name ? (where ? t("app.guestAsOn", name, where) : t("app.guestAs", name)) : t("app.guestChip");
|
|
344
|
+
// Inside .side-foot (a marked, stock-trusted element): no trust marker of its own — effect-stock-trust's closed inventory
|
|
345
|
+
// refuses any element it does not know, which would hide a guest's static effect posters (#2101 review).
|
|
346
|
+
return html`<p class="guest-chip" title=${t("app.guestHint")}><${Icon} name="user" size=${14} /><span class="grow">${label}</span></p>`;
|
|
347
|
+
}
|
|
348
|
+
|
|
322
349
|
/** #1629 r4-4: the person's own sidebar widget (their synced preference), with the fleet's counts. */
|
|
323
350
|
function SidebarWidget({ app }) {
|
|
324
351
|
useStore(sidebarWidgetStore);
|
|
@@ -406,6 +433,8 @@ function BottomTabs({ route, viewOnly }) {
|
|
|
406
433
|
${tab(signInHref(), "user", t("app.signIn"), false)}
|
|
407
434
|
</nav>`;
|
|
408
435
|
}
|
|
436
|
+
// #1920: a guest has only its chats (the drawer lists them): no tabs to switch between.
|
|
437
|
+
if (guestPage()) return null;
|
|
409
438
|
return html`<nav data-trust="navigation" class="tabs" aria-label=${t("app.menu")}>
|
|
410
439
|
${tab(chatHref, "chat", t("app.chat"), route && route.panel === "chat", anyAwaiting)}
|
|
411
440
|
${tab(NEEDS_PATH, "inbox", t("app.needsTab"), route && route.panel === "needs", (app.needs || []).length, t("app.needsCount", (app.needs || []).length))}
|
|
@@ -530,23 +559,24 @@ export function Shell({ panels, onNewInstance, viewOnly = false }) {
|
|
|
530
559
|
vv.addEventListener("resize", onResize);
|
|
531
560
|
return () => vv.removeEventListener("resize", onResize);
|
|
532
561
|
}, []);
|
|
533
|
-
const
|
|
562
|
+
const guest = guestPage(); // #1920: a guest's page — no operations, usage, pending changes or owner CSS
|
|
563
|
+
const cls = ["shell", shell.collapsed ? "sb-collapsed" : "", shell.drawer ? "sb-open" : "", shell.keyboard ? "kb-open" : "", guest ? "guest" : ""].filter(Boolean).join(" ");
|
|
534
564
|
return html`<div class=${cls}>
|
|
535
565
|
<a data-trust="navigation" class="skip" href="#main">${t("app.skip")}</a>
|
|
536
566
|
<${Sidebar} route=${nav.route} onNewInstance=${onNewInstance} viewOnly=${viewOnly} />
|
|
537
567
|
<div class="scrim" onClick=${closeDrawer} aria-hidden="true"></div>
|
|
538
568
|
<main id="main" class="main" tabindex="-1" inert=${shell.drawer && narrow() ? true : undefined}>
|
|
539
569
|
<${ConnectionLine} />
|
|
540
|
-
${viewOnly ? null : html`<${OperationLine} route=${nav.route} />`}
|
|
570
|
+
${viewOnly || guest ? null : html`<${OperationLine} route=${nav.route} />`}
|
|
541
571
|
<${Outlet} route=${nav.route} seq=${nav.seq} panels=${panels} />
|
|
542
572
|
</main>
|
|
543
|
-
|
|
573
|
+
${guest ? null : html`<${UsageRail} route=${nav.route} />`}
|
|
544
574
|
<${BottomTabs} route=${nav.route} viewOnly=${viewOnly} />
|
|
545
575
|
${shell.dialog ? html`<${shell.dialog.Component} ...${shell.dialog.props} onClose=${closeDialog} />` : null}
|
|
546
|
-
${viewOnly || (nav.route && nav.route.panel === "settings") ? null : html`<${PendingChanges} />`}
|
|
576
|
+
${viewOnly || guest || (nav.route && nav.route.panel === "settings") ? null : html`<${PendingChanges} />`}
|
|
547
577
|
<${ConfirmHost} />
|
|
548
578
|
<${ColorDialogHost} />
|
|
549
|
-
${viewOnly ? null : html`<${OwnerCssKeepBar} />`}
|
|
579
|
+
${viewOnly || guest ? null : html`<${OwnerCssKeepBar} />`}
|
|
550
580
|
<${Toasts} />
|
|
551
581
|
</div>`;
|
|
552
582
|
}
|