@songsid/agend 2.3.0-beta.4 → 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/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/channel/mcp-tools.js +1 -1
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/config-validator.js +19 -1
- 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-manager.d.ts +56 -0
- package/dist/fleet-manager.js +86 -10
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +28 -11
- 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 +24 -8
- package/dist/human-delivery-receipts.js.map +1 -1
- package/dist/outbound-schemas.d.ts +3 -3
- package/dist/reply-buttons-controller.d.ts +2 -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/types.d.ts +12 -0
- package/dist/ui/chat-strings.js +4 -2
- package/dist/ui/panel-chat.js +15 -6
- package/dist/ui/panel-fleet.js +4 -1
- package/dist/ui/panel-settings.js +2 -0
- package/dist/ui/settings-catalog.js +4 -1
- package/dist/ui/settings-guests.js +117 -0
- package/dist/ui/settings-strings.js +22 -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 +18 -0
- package/dist/ui/shared/app.js +21 -14
- package/dist/ui/shared/instance-nav.js +8 -5
- package/dist/web-api.d.ts +16 -1
- package/dist/web-api.js +37 -10
- package/dist/web-api.js.map +1 -1
- package/dist/web-effect-upload.js +1 -1
- package/dist/web-effect-upload.js.map +1 -1
- 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-prefs.js +1 -1
- package/dist/web-prefs.js.map +1 -1
- package/dist/web-returning-device.d.ts +2 -0
- package/dist/web-returning-device.js +11 -0
- package/dist/web-returning-device.js.map +1 -1
- package/dist/web-session.d.ts +7 -0
- package/dist/web-session.js +17 -0
- 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/package.json +1 -1
package/dist/ui/chat-strings.js
CHANGED
|
@@ -41,7 +41,8 @@ register("chat", {
|
|
|
41
41
|
a11yWorking: "{0} started working", a11yIdle: "{0} finished", a11yReplied: "{0} replied", conversationWith: "Conversation with {0}",
|
|
42
42
|
stopping: "Stopping {0}…", awaiting: "{0} is waiting for your input", approxNote: "Read from the terminal, so approximate", elapsedTitle: "Time since this page saw it start (approximate)",
|
|
43
43
|
dropHere: "Drop files to attach them", asText: "As text", pasteAttached: "A long paste ({0} characters) was attached as a text file", pasteKeptAsText: "No room for another attachment: the long paste stays as text",
|
|
44
|
-
send: "Send", composerPlaceholder: "Message {0}…", composerHint: "Enter to send · Shift+Enter for a new line", composerMore: "/ for commands · Esc stops the reply",
|
|
44
|
+
send: "Send", composerPlaceholder: "Message {0}…", composerHint: "Enter to send · Shift+Enter for a new line", composerMore: "/ for commands · Esc stops the reply", guestMore: "Esc stops the reply",
|
|
45
|
+
guestNote: "You're a guest: the agent sees your messages as a guest's (recognition only).", guestNoCommands: "Guests can't run commands — send it without the leading /.", attachTitle: "Attach files (any type — or paste / drop them)",
|
|
45
46
|
pvPreview: "Preview", pvStop: "Stop", pvDownload: "Download", pvMenu: "Preview options", pvHtml: "HTML",
|
|
46
47
|
pvAllowButton: "Allow previews", pvAllow: "Allow HTML previews — they may be able to send data out", pvDisallow: "Stop allowing HTML previews",
|
|
47
48
|
pvConfirm: "Allow HTML previews?\n\nThis applies on every device where you are signed in. A preview runs the agent's HTML in an isolated frame. It cannot use your login, but it may be able to send data out, and it can slow or freeze this tab. You still click Preview each time.",
|
|
@@ -157,7 +158,8 @@ register("chat", {
|
|
|
157
158
|
a11yWorking: "{0} 開始工作", a11yIdle: "{0} 已完成", a11yReplied: "{0} 已回覆", conversationWith: "與 {0} 的對話",
|
|
158
159
|
stopping: "正在中止 {0}…", awaiting: "{0} 正在等你回應", approxNote: "從終端畫面判讀,僅供參考", elapsedTitle: "從這個頁面看到它開始工作起算(約略)",
|
|
159
160
|
dropHere: "放開即可附加檔案", asText: "改為文字", pasteAttached: "貼上的長文字({0} 個字元)已改為文字檔附加", pasteKeptAsText: "附件已滿,貼上的長文字保留為一般文字",
|
|
160
|
-
send: "送出", composerPlaceholder: "傳訊息給 {0}…", composerHint: "Enter 送出 · Shift+Enter 換行", composerMore: "/ 叫出指令 · Esc 中止回覆",
|
|
161
|
+
send: "送出", composerPlaceholder: "傳訊息給 {0}…", composerHint: "Enter 送出 · Shift+Enter 換行", composerMore: "/ 叫出指令 · Esc 中止回覆", guestMore: "Esc 中止回覆",
|
|
162
|
+
guestNote: "你是訪客:agent 會把你的訊息視為訪客的訊息(僅供辨識)。", guestNoCommands: "訪客不能執行指令,請去掉開頭的 / 再送出。", attachTitle: "附加檔案(任何類型,也可以貼上或拖曳)",
|
|
161
163
|
pvPreview: "預覽", pvStop: "停止", pvDownload: "下載", pvMenu: "預覽選項", pvHtml: "HTML",
|
|
162
164
|
pvAllowButton: "允許預覽", pvAllow: "允許預覽 HTML(預覽可能把資料傳出去)", pvDisallow: "不再允許預覽 HTML",
|
|
163
165
|
pvConfirm: "要允許預覽 HTML 嗎?\n\n這會套用到你登入的每一台裝置。預覽會在隔離的框架裡執行 agent 的 HTML。它無法使用你的登入,但可能把資料傳出去,也可能讓這個分頁變慢或卡住。每次仍需要你按「預覽」。",
|
package/dist/ui/panel-chat.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// that apply to its state are offered (#1408 rough edges 4 and 7).
|
|
9
9
|
import { html, useEffect, useLayoutEffect, useRef, useState } from "/assets/app-html.js";
|
|
10
10
|
import { t } from "/assets/app-i18n.js";
|
|
11
|
-
import { appStore, useStore } from "/assets/app-store.js";
|
|
11
|
+
import { appStore, guestPage, useStore } from "/assets/app-store.js";
|
|
12
12
|
import { useLease } from "/assets/app-ctx.js";
|
|
13
13
|
import { modelText, namesOf, namesStore } from "/assets/instance-nav.js";
|
|
14
14
|
import { PanelHeader, onPanelKey, setTitle, statusClass, statusLabel, openDrawer, requestNewInstance } from "/assets/app-shell.js";
|
|
@@ -116,7 +116,8 @@ export function boot({ stream, boot: bootData, deps = {} }) {
|
|
|
116
116
|
}
|
|
117
117
|
// #1604: the preview opt-in and Tour live in Settings → This device now (and on each preview card's menu).
|
|
118
118
|
installTour();
|
|
119
|
-
|
|
119
|
+
// #1920 phase 3: effects are an admin's to place (a guest's page reads none: each read would be refused).
|
|
120
|
+
if (!guestPage()) startAppEffects({ store });
|
|
120
121
|
return store;
|
|
121
122
|
}
|
|
122
123
|
|
|
@@ -278,13 +279,16 @@ function ChatView({ name, inst, lease, navKey, exec, awaiting }) {
|
|
|
278
279
|
{ key: "delete", label: t("chat.delete"), icon: "trash", danger: true, onSelect: () => setDialog("delete") },
|
|
279
280
|
];
|
|
280
281
|
const cls = statusClass(inst, exec, awaiting);
|
|
282
|
+
// #1920 phase 3: a guest has this chat only — no Details, View or Log to switch to, no lifecycle, colour, theme,
|
|
283
|
+
// effects or delete (a guest's status frame carries no model or effort either, so those chips do not show).
|
|
284
|
+
const guest = guestPage();
|
|
281
285
|
// #1269 quick actions: the model and effort it runs, each a way to /model or /effort (the same pick list).
|
|
282
286
|
const pick = (command) => store.runCommand(name, command, "", { alive: () => lease.current() });
|
|
283
287
|
const sub = html`<span class="status"><span class=${`dot ${cls}`} aria-hidden="true"></span>${statusLabel(inst, exec, awaiting)}</span>
|
|
284
288
|
${inst.model ? html`<button type="button" class="hd-chip" title=${t("chat.chipModel", modelText(inst))} aria-label=${t("chat.chipModel", modelText(inst))} onClick=${() => pick("model")}>${modelText(inst)}</button>` : null}
|
|
285
289
|
${inst.effort ? html`<button type="button" class="hd-chip" title=${t("chat.chipEffort", inst.effort)} aria-label=${t("chat.chipEffort", inst.effort)} onClick=${() => pick("effort")}>${inst.effort}</button>` : null}`;
|
|
286
290
|
return html`<div class=${`panel p-chat${wrap ? " wrap-code" : ""}`} ref=${view}>
|
|
287
|
-
<${PanelHeader} title=${headerName(namesOf(inst || { name }, names.primary))} sub=${sub} nav=${html`<${InstanceSwitch} name=${name} current="chat" />`}><${ChatEffectPosters} name=${name} lease=${lease} element=${effectSurface}
|
|
291
|
+
<${PanelHeader} title=${headerName(namesOf(inst || { name }, names.primary))} sub=${sub} nav=${guest ? null : html`<${InstanceSwitch} name=${name} current="chat" />`}><${ChatEffectPosters} name=${name} lease=${lease} element=${effectSurface} />${guest ? null : html`<${ChatEffects} name=${name} lease=${lease} element=${effectSurface} store=${store} open=${dialog === "effects"} onClose=${() => setDialog(null)} />`}<${HeaderTools} navKey=${navKey} />${guest ? null : html`<${Menu} items=${items} label=${t("app.more")} />`}</${PanelHeader}>
|
|
288
292
|
<${ChatCard} key=${name} name=${name} card=${card} />
|
|
289
293
|
<${AgentWidget} slot="chat_header" widget=${widgetFor(name, "chat_header")} binds=${instanceBinds(inst, exec, awaiting)} />
|
|
290
294
|
<div class="chat-split" ref=${split}>
|
|
@@ -379,7 +383,8 @@ function Dock({ name, inst, lease, exec, awaiting, workingLabel }) {
|
|
|
379
383
|
<button type="button" class="btn btn-primary btn-sm" onClick=${() => store.putBack(name)}>${t("chat.putBack")}</button>
|
|
380
384
|
<button type="button" class="btn btn-sm" onClick=${() => store.discardFailed(name)}>${t("chat.discard")}</button></div>` : null}
|
|
381
385
|
<${Composer} name=${name} lease=${lease} busy=${R().isBusy(exec)} />
|
|
382
|
-
<p class="hint" title=${t("chat.composerMore")}>${t("chat.composerHint")}</p>
|
|
386
|
+
<p class="hint" title=${t(guestPage() ? "chat.guestMore" : "chat.composerMore")}>${t("chat.composerHint")}</p>
|
|
387
|
+
${guestPage() ? html`<p class="guest-note">${t("chat.guestNote")}</p>` : null}
|
|
383
388
|
</div></div>`;
|
|
384
389
|
}
|
|
385
390
|
|
|
@@ -456,8 +461,10 @@ function Composer({ name, lease, busy }) {
|
|
|
456
461
|
const value = s.drafts[name] || "";
|
|
457
462
|
const files = s.pendingFiles[name] || [];
|
|
458
463
|
const content = !!value.trim() || files.length > 0;
|
|
459
|
-
// A file waiting: what goes is a message with its file, never a command (no palette, no command line).
|
|
460
|
-
|
|
464
|
+
// A file waiting: what goes is a message with its file, never a command (no palette, no command line). A guest never
|
|
465
|
+
// runs one (#1920: the server refuses its slash text), so it has no palette either.
|
|
466
|
+
const guest = guestPage();
|
|
467
|
+
const palette = guest || files.length || closedFor === value ? null : paletteFor(value);
|
|
461
468
|
const pick = palette && palette.length ? palette[Math.min(sel, palette.length - 1)] : null;
|
|
462
469
|
// Grow with the text, up to a limit: set through the CSSOM, never a style attribute (#1300).
|
|
463
470
|
useLayoutEffect(() => { const el = input.current; if (!el) return; el.style.height = "auto"; el.style.height = `${Math.min(el.scrollHeight, 240)}px`; }, [value]);
|
|
@@ -478,6 +485,8 @@ function Composer({ name, lease, busy }) {
|
|
|
478
485
|
else complete(pick);
|
|
479
486
|
return;
|
|
480
487
|
}
|
|
488
|
+
// #1920: a guest's "/…" would be refused as a command — said here, the draft kept to edit.
|
|
489
|
+
if (guest && /^\s*\//.test(value)) { toast(t("chat.guestNoCommands"), false); return; }
|
|
481
490
|
const cmd = files.length ? null : parseCommandLine(value);
|
|
482
491
|
if (cmd && needsArgument(cmd.command) && !cmd.args) { setValue(`/${cmd.command} `); return; }
|
|
483
492
|
if (cmd) { runLine(cmd); return; }
|
package/dist/ui/panel-fleet.js
CHANGED
|
@@ -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 {
|
|
@@ -560,7 +563,7 @@ export function CreateInstanceDialog({ onClose }) {
|
|
|
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
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} />`)}
|
|
563
|
-
${field(t("fleet.model"), html`<${ModelSelect} value=${f.model} onChange=${(v) => setF(x => ({ ...x, model: v }))} backend=${f.backend ||
|
|
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>`)}
|
|
@@ -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" };
|
|
@@ -866,6 +867,7 @@ function WebSection({ ctx, staged }) {
|
|
|
866
867
|
</div>
|
|
867
868
|
</section>
|
|
868
869
|
</fieldset>
|
|
870
|
+
<${GuestsCard} ctx=${ctx} staged=${staged} disabled=${overPublicLink} />
|
|
869
871
|
<section class="card s-web-readonly"><h3>${tn("webReadOnlyTitle")}</h3>
|
|
870
872
|
<p class="note">${tn("webReadOnlyLead")}</p>
|
|
871
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>
|
|
@@ -21,7 +21,10 @@ function cached(url) {
|
|
|
21
21
|
cache.set(url, { at: now, value: null, pending });
|
|
22
22
|
return pending;
|
|
23
23
|
}
|
|
24
|
-
/**
|
|
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
|
+
*/
|
|
25
28
|
export function forgetCatalogs() { cache.clear(); }
|
|
26
29
|
|
|
27
30
|
function useAnswer(url) {
|
|
@@ -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
|
+
}
|
|
@@ -428,6 +428,17 @@ register("settings", {
|
|
|
428
428
|
webTerminalTtl: "Terminal session minutes (1–20)",
|
|
429
429
|
webTerminalTtlRange: "Must be a whole number from 1 to 20",
|
|
430
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.",
|
|
431
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).",
|
|
432
443
|
notSet: "not set (default)",
|
|
433
444
|
defaultsTitle: "Defaults",
|
|
@@ -924,6 +935,17 @@ register("settings", {
|
|
|
924
935
|
webTerminalTtl: "終端機 session 分鐘數(1–20)",
|
|
925
936
|
webTerminalTtlRange: "必須是 1 到 20 之間的整數",
|
|
926
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: "沒有成功,請再試一次。",
|
|
927
949
|
webReadOnlyLead: "這裡僅供檢視:這些設定決定儀表板監聽和公開的內容。請編輯 fleet.yaml 後執行 agend reload(web.preview_port 與 web.preview_origin 需要重新啟動 AgEnD)。",
|
|
928
950
|
notSet: "未設定(預設)",
|
|
929
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
|
}
|
|
@@ -26,6 +26,20 @@ export function createStore(initial) {
|
|
|
26
26
|
export const appStore = createStore({ ready: false, instances: [], uptime: 0, exec: {}, awaiting: {}, connection: "connecting", needs: [], connections: null,
|
|
27
27
|
hydration: "none", retryHydration: null });
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* #1920 phase 3: this page is a guest's — the server says so on <body> (data-tier="guest"). A guest sees only its own
|
|
31
|
+
* agents' chats: no fleet, Needs you, View, Settings, filters, commands or new agent, and nothing it may not read is asked
|
|
32
|
+
* for (each would be refused). The server checks every request regardless; this only keeps the page to what works.
|
|
33
|
+
*/
|
|
34
|
+
export function guestPage() {
|
|
35
|
+
return typeof document !== "undefined" && !!document.body && document.body.dataset.tier === "guest";
|
|
36
|
+
}
|
|
37
|
+
/** Who a guest's page is signed in as: its web name and its platform (the server's, on <body>). */
|
|
38
|
+
export function guestBadge() {
|
|
39
|
+
const d = typeof document !== "undefined" && document.body ? document.body.dataset : {};
|
|
40
|
+
return { name: d.guestName || "", platform: d.guestPlatform || "" };
|
|
41
|
+
}
|
|
42
|
+
|
|
29
43
|
/** Fold one status frame (stream or poll) into the store. */
|
|
30
44
|
export function applyStatus(d) {
|
|
31
45
|
if (!d || !Array.isArray(d.instances)) return;
|
package/dist/ui/shared/app.css
CHANGED
|
@@ -181,6 +181,13 @@ html:has(.shell), html:has(.shell) body { overflow: hidden; }
|
|
|
181
181
|
.side-foot > .session { position: static; }
|
|
182
182
|
.side-foot .side-row span:not(.dot):not(.meta) { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
183
183
|
.side-foot .session .meta { display: none; }
|
|
184
|
+
/* #1920 phase 3: a guest's page says who it is signed in as, beside its session. */
|
|
185
|
+
.guest-chip { flex: 1; min-width: 0; display: flex; align-items: center; gap: var(--s-1); margin: 0; padding: 6px var(--s-2); border-radius: var(--r-sm);
|
|
186
|
+
background: var(--surface-2); color: var(--text-2); font-size: var(--fs-sm); }
|
|
187
|
+
.guest-chip .grow { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
188
|
+
/* Its own row: a name and a platform do not fit beside the session at sidebar widths. */
|
|
189
|
+
.guest .side-foot { flex-wrap: wrap; }
|
|
190
|
+
.guest .side-foot > .guest-chip { flex-basis: 100%; }
|
|
184
191
|
.prefs-pop > summary { list-style: none; cursor: pointer; }
|
|
185
192
|
.prefs-pop > summary::-webkit-details-marker { display: none; }
|
|
186
193
|
.prefs.labelled { flex-direction: column; gap: var(--s-2); padding: 0; }
|
|
@@ -661,6 +668,17 @@ html.lb-open .scroller { overflow: hidden; }
|
|
|
661
668
|
@media (max-width: 479px) { .btn-stop .stop-label { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; } }
|
|
662
669
|
.btn-stop { background: var(--bg); }
|
|
663
670
|
.hint { font-size: var(--fs-xs); color: var(--text-3); text-align: center; }
|
|
671
|
+
/* #1920 phase 3: Settings → Web → Guests. */
|
|
672
|
+
.s-guests .guest-names { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 2px var(--s-3); max-height: 220px; overflow: auto;
|
|
673
|
+
padding: var(--s-1) var(--s-2); border: 1px solid var(--border); border-radius: var(--r-sm); }
|
|
674
|
+
.s-guests .guest-name span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
675
|
+
.s-guests .guest-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--s-2); }
|
|
676
|
+
.s-guests .guest-row { padding: var(--s-2) var(--s-3); border: 1px solid var(--border); border-radius: var(--r-sm); }
|
|
677
|
+
.s-guests .guest-actions { display: flex; flex-wrap: wrap; gap: var(--s-1); margin-top: var(--s-1); }
|
|
678
|
+
.s-guests .guest-narrow-edit { margin-top: var(--s-2); }
|
|
679
|
+
.s-guests .guest-away { color: var(--text-3); }
|
|
680
|
+
/* #1920: a guest's note under the composer — on a phone too, where the key hints are hidden. */
|
|
681
|
+
.guest-note { margin: 2px 0 0; font-size: var(--fs-xs); color: var(--text-3); text-align: center; }
|
|
664
682
|
.pending-files { display: flex; flex-wrap: wrap; gap: var(--s-2); padding: var(--s-2) var(--s-2) 0; }
|
|
665
683
|
.file-chip { display: flex; align-items: center; gap: 6px; font-size: var(--fs-xs); padding: 4px var(--s-2); border-radius: var(--r-md); background: var(--bg); max-width: 320px; }
|
|
666
684
|
.file-chip img { width: 28px; height: 28px; object-fit: cover; border-radius: 4px; }
|
package/dist/ui/shared/app.js
CHANGED
|
@@ -12,7 +12,7 @@ import { Shell, handleKey, showDialog, closeDrawer, openDrawer, shellStore, retr
|
|
|
12
12
|
import { installSwipe } from "./app-swipe.js";
|
|
13
13
|
import { startRouter, navStore, navigate } from "./app-nav.js";
|
|
14
14
|
import { createStream } from "./app-stream.js";
|
|
15
|
-
import { appStore, applyStatus, applyActivity } from "./app-store.js";
|
|
15
|
+
import { appStore, applyStatus, applyActivity, guestPage } from "./app-store.js";
|
|
16
16
|
import { chatPath } from "./app-route.js";
|
|
17
17
|
import { startNeedsNotifier } from "./app-needs.js";
|
|
18
18
|
import { toast } from "./ui-toast.js";
|
|
@@ -29,6 +29,9 @@ initUsageRail();
|
|
|
29
29
|
// session-only panel, nothing under /ui/ is ever imported or read (#1408 §3).
|
|
30
30
|
const mode = boot.mode === "view-only" ? "view-only" : "full";
|
|
31
31
|
appStore.set({ viewOnly: mode === "view-only" });
|
|
32
|
+
// #1920 phase 3: a guest's page has its chats and nothing else — no panel, notifier, theme list or owner CSS a guest
|
|
33
|
+
// would be refused.
|
|
34
|
+
const guest = guestPage();
|
|
32
35
|
|
|
33
36
|
// A failed load can be retried (the panel's Retry): each retry asks for a fresh URL, since a browser may keep a failed
|
|
34
37
|
// module import cached under the first one. boot() runs once, whichever attempt succeeds.
|
|
@@ -80,19 +83,22 @@ if (mode === "full") {
|
|
|
80
83
|
// a boot import that failed before the Outlet first asked would be retried at once, depending on timing.)
|
|
81
84
|
let chatLoads = 0;
|
|
82
85
|
panels.set("chat", { load: () => (chatLoads++ === 0 ? chatBoot : loadChat()).then((m) => m.ChatPanel) });
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
86
|
+
if (!guest) {
|
|
87
|
+
panels.set("fleet", { load: () => loadFleet().then((m) => m.FleetPanel) });
|
|
88
|
+
panels.set("settings", { load: () => loadSettings().then((m) => m.SettingsPanel) });
|
|
89
|
+
panels.set("needs", { load: () => loadNeeds().then((m) => m.NeedsPanel) });
|
|
90
|
+
// #1523 N2: one instance's Details (/ui/fleet/agent/<name>).
|
|
91
|
+
panels.set("details", { load: () => loadDetails().then((m) => m.DetailsPanel) });
|
|
92
|
+
// #1218: one instance's Log (/ui/log/<name>).
|
|
93
|
+
panels.set("log", { load: () => loadLog().then((m) => m.LogPanel) });
|
|
94
|
+
// #1386 §6.3: desktop notifications for new "Needs you" items, in any panel, while this signed-in page is open.
|
|
95
|
+
startNeedsNotifier({ open: (instance) => navigate(chatPath(instance)) });
|
|
96
|
+
}
|
|
92
97
|
// #1665: this device's first upload of its own choices (once), then another device's changes at each status frame.
|
|
98
|
+
// A guest's page only reads them (prefsWritable is false there).
|
|
93
99
|
bootPrefs();
|
|
94
|
-
// #1629: agents' themes, read when the status frame's themes_rev moves.
|
|
95
|
-
startThemes();
|
|
100
|
+
// #1629: agents' themes, read when the status frame's themes_rev moves (a guest's frames never carry it).
|
|
101
|
+
if (!guest) startThemes();
|
|
96
102
|
onNewInstance = async () => {
|
|
97
103
|
closeDrawer(); // on a phone the dialog opens from the drawer: the drawer goes first
|
|
98
104
|
let m;
|
|
@@ -103,8 +109,9 @@ if (mode === "full") {
|
|
|
103
109
|
} else appStore.set({ connection: "none" });
|
|
104
110
|
|
|
105
111
|
startRouter(window);
|
|
106
|
-
// #1629 r4 §3: the person's owner CSS (full app only; never on Settings, in safe mode or where this device turned it off
|
|
107
|
-
|
|
112
|
+
// #1629 r4 §3: the person's owner CSS (full app only; never on Settings, in safe mode or where this device turned it off;
|
|
113
|
+
// never a guest's — owner CSS is an admin's).
|
|
114
|
+
if (!guest) installOwnerCss();
|
|
108
115
|
// /ui on its own opens the chat this browser had open last (an unknown name shows the panel's "not found").
|
|
109
116
|
{
|
|
110
117
|
const r = navStore.get().route;
|