@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
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// Public (/assets/): an anonymous View reader gets the same list, fed by View's roster read instead of status frames.
|
|
10
10
|
import { html, useLayoutEffect, useRef, useState } from "./app-html.js";
|
|
11
11
|
import { t } from "./app-i18n.js";
|
|
12
|
-
import { appStore, createStore, useStore } from "./app-store.js";
|
|
12
|
+
import { appStore, createStore, guestPage, useStore } from "./app-store.js";
|
|
13
13
|
import { chatPath, detailsPath, logPath, viewPath } from "./app-route.js";
|
|
14
14
|
import { Icon } from "./ui-icons.js";
|
|
15
15
|
import { Skeleton } from "./ui-states.js";
|
|
@@ -391,10 +391,13 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
|
|
|
391
391
|
const favs = nav.favorites.filter((n) => known.has(n));
|
|
392
392
|
const favSet = new Set(favs);
|
|
393
393
|
const canStar = !viewOnly && prefsWritable();
|
|
394
|
+
// #1920: a guest's list is to pick from — no reordering and no filters (one this device kept would hide rows with no
|
|
395
|
+
// way to clear it).
|
|
396
|
+
const guest = guestPage();
|
|
394
397
|
const shownGroups = favs.length ? [FAV_GROUP, ...groupNames] : groupNames;
|
|
395
398
|
const shownInsts = new Map([...instByGroup].map(([g, names]) => [g, names.filter((n) => !favSet.has(n))]));
|
|
396
399
|
if (favs.length) shownInsts.set(FAV_GROUP, favs);
|
|
397
|
-
const f = nav.filter;
|
|
400
|
+
const f = guest ? { q: "", status: [], cli: [] } : nav.filter;
|
|
398
401
|
const shown = filterSidebar(shownGroups, shownInsts, byName, f.q, backendLabel, { status: f.status, cli: f.cli, statusOf: facetOf });
|
|
399
402
|
// #1936: while a filter is on, the instance you have open stays on top, pinned and marked "Current", whether or not
|
|
400
403
|
// it matches; a match is not repeated below. "N / M" still counts the filter's matches (the pinned row counts only
|
|
@@ -408,7 +411,7 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
|
|
|
408
411
|
const flip = (key, value) => { const cur = new Set(navStore.get().filter[key]); if (cur.has(value)) cur.delete(value); else cur.add(value); setNavFilter({ [key]: [...cur] }); };
|
|
409
412
|
// Drag to reorder: only the full list (a filtered subset would be ambiguous), groups among groups, instances within
|
|
410
413
|
// their group.
|
|
411
|
-
const dragProps = (item) => shown.active ? {} : {
|
|
414
|
+
const dragProps = (item) => shown.active || guest ? {} : {
|
|
412
415
|
draggable: true,
|
|
413
416
|
onDragStart: (e) => { drag.current = item; e.currentTarget.classList.add("dragging"); try { e.dataTransfer.effectAllowed = "move"; e.dataTransfer.setData("text/plain", item.name); } catch { /* Firefox needs data */ } },
|
|
414
417
|
onDragEnd: (e) => { e.currentTarget.classList.remove("dragging"); drag.current = null; },
|
|
@@ -504,7 +507,7 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
|
|
|
504
507
|
})}
|
|
505
508
|
${loaded && shown.active && !shown.groups.length ? html`<p class="side-empty">${tn("filterNone")}</p>` : null}
|
|
506
509
|
</div>
|
|
507
|
-
|
|
510
|
+
${guest ? null : html`<div class="v-filter">
|
|
508
511
|
<label class="v-filter-box"><${Icon} name="search" size=${14} /><span class="sr-only">${tn("filterPh")}</span>
|
|
509
512
|
<input ref=${filterRef} id="filterInput" type="text" autocomplete="off" spellcheck=${false} placeholder=${tn("filterPh")} value=${f.q}
|
|
510
513
|
onInput=${(e) => setNavFilter({ q: e.target.value })}
|
|
@@ -520,7 +523,7 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
|
|
|
520
523
|
</div>
|
|
521
524
|
<p class="note nav-shown" aria-live="polite">${shown.shown} / ${shown.total}${shown.active ? ` ${tn("filterShown")}` : ""}</p>
|
|
522
525
|
</div>
|
|
523
|
-
</div
|
|
526
|
+
</div>`}
|
|
524
527
|
</div>`;
|
|
525
528
|
}
|
|
526
529
|
|
package/dist/web-api.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type HumanOrigin } from "./human-delivery-receipts.js";
|
|
2
|
+
import { type HumanAuthority, type WebAuthorityResult, type WebPeer } from "./authority/web-authority.js";
|
|
2
3
|
import type { CachedImage } from "./web-images.js";
|
|
3
4
|
import type { SettingsExecution } from "./settings-transaction.js";
|
|
4
5
|
import type { PaneContextSource } from "./pane-context-cache.js";
|
|
@@ -13,7 +14,7 @@ import type { LifecycleCreateArgs } from "./instance-lifecycle.js";
|
|
|
13
14
|
import { type PreviewAvailability } from "./web-preview.js";
|
|
14
15
|
import { type WebChatHistory } from "./web-chat-history.js";
|
|
15
16
|
import { type WebFileLedger } from "./web-upload.js";
|
|
16
|
-
import type { WebSessionStore } from "./web-session.js";
|
|
17
|
+
import type { WebSessionStore, SessionRecord } from "./web-session.js";
|
|
17
18
|
import { type WebPrefsStore } from "./web-prefs.js";
|
|
18
19
|
import { type WebAccount } from "./web-profile.js";
|
|
19
20
|
import { type MentionNames, type MentionQuery } from "./web-mentions.js";
|
|
@@ -204,10 +205,18 @@ export interface WebApiContext {
|
|
|
204
205
|
}, opts: {
|
|
205
206
|
publicLink: boolean;
|
|
206
207
|
person?: WebPerson;
|
|
208
|
+
authority?: HumanAuthority;
|
|
207
209
|
}): Promise<{
|
|
208
210
|
status: number;
|
|
209
211
|
body: unknown;
|
|
210
212
|
}>;
|
|
213
|
+
/** #1939 S1: the design's §3 for one request (`instance` once the body names it); absent: web carries no authority. */
|
|
214
|
+
webAuthority?(input: {
|
|
215
|
+
session: SessionRecord | null;
|
|
216
|
+
gateway: boolean;
|
|
217
|
+
peer: WebPeer;
|
|
218
|
+
instance?: string;
|
|
219
|
+
}): WebAuthorityResult;
|
|
211
220
|
/**
|
|
212
221
|
* #1827: a web person — for the account a session is bound to (none: the operator) — as agents and chats see them.
|
|
213
222
|
* Absent: everyone is "web-user", as before.
|
|
@@ -238,6 +247,7 @@ export interface WebApiContext {
|
|
|
238
247
|
userId: string;
|
|
239
248
|
name: string;
|
|
240
249
|
guest?: true;
|
|
250
|
+
authority?: HumanAuthority;
|
|
241
251
|
}): Promise<{
|
|
242
252
|
status: number;
|
|
243
253
|
error?: string;
|
|
@@ -359,6 +369,11 @@ export interface AppShellContext {
|
|
|
359
369
|
readonly ownerCss?: OwnerCssStore;
|
|
360
370
|
readonly webToken?: string | null;
|
|
361
371
|
readonly webSessions?: WebApiContext["webSessions"];
|
|
372
|
+
/** #1920 phase 3: a guest's page says who it is signed in as (its web name and platform), for its "Guest" chip. */
|
|
373
|
+
guestBadge?(owner: WebAccount): {
|
|
374
|
+
name: string;
|
|
375
|
+
platform: string;
|
|
376
|
+
};
|
|
362
377
|
}
|
|
363
378
|
export type AppShellMode = "full" | "view-only";
|
|
364
379
|
/** The app shell page for one entry, under the panels' CSP; it may frame exactly <preview origin>/frame, and only
|
package/dist/web-api.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { webHumanOrigin } from "./human-delivery-receipts.js";
|
|
2
|
+
import { classifyWebPeer, confirmAfterBody, noAuthority } from "./authority/web-authority.js";
|
|
2
3
|
import { guestChatRefusal, guestCommandText, guestDeliveries, guestFrameData, guestHistory, guestMessages, guestPollBody, guestPrefs, guestRequestOf, guestSeesMessage, guestStatus } from "./web-guest-access.js";
|
|
3
4
|
import { withSpawnHoldReply } from "./spawn-hold-reply.js";
|
|
4
5
|
import { binaryProbe } from "./binary-probe.js";
|
|
@@ -902,6 +903,17 @@ function webPersonOf(req, url, ctx) {
|
|
|
902
903
|
const who = webAccountOf(req, url, ctx);
|
|
903
904
|
return who ? ctx.webPerson(who.owner) : null;
|
|
904
905
|
}
|
|
906
|
+
/**
|
|
907
|
+
* #1939 S1: whether this request carries `web-session` authority, asked as it stands now — on arrival without an instance,
|
|
908
|
+
* after the body with the one it names (then confirmAfterBody joins the two). Never touches the session.
|
|
909
|
+
*/
|
|
910
|
+
function webAuthorityOf(req, url, ctx, instance) {
|
|
911
|
+
if (!ctx.webAuthority)
|
|
912
|
+
return { authority: noAuthority(0), refusal: "not_session" };
|
|
913
|
+
const v = evaluateWebRequest(req, url, ctx.webToken ?? null, ctx.webSessions, { touch: false });
|
|
914
|
+
return ctx.webAuthority({ session: v.kind === "allow" && v.via === "session" ? v.session : null, gateway: !!gatewayRequestContext(req),
|
|
915
|
+
peer: classifyWebPeer(req), ...(instance !== undefined ? { instance } : {}) });
|
|
916
|
+
}
|
|
905
917
|
/** #1827: the person, or the request is answered 401 here (it is no longer authorized) and null is returned. */
|
|
906
918
|
function webPersonOr401(req, res, url, ctx) {
|
|
907
919
|
const p = webPersonOf(req, url, ctx);
|
|
@@ -989,6 +1001,8 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
989
1001
|
return true;
|
|
990
1002
|
}
|
|
991
1003
|
if (method === "GET" && path === "/ui/backends") {
|
|
1004
|
+
// #2072 (#2098 review P3-3): the backend an agent created with "fleet default" runs, so its model list can be shown.
|
|
1005
|
+
const defaultBackend = ctx.fleetConfig?.defaults?.backend ?? "claude-code";
|
|
992
1006
|
const BACKENDS = [
|
|
993
1007
|
{ name: "claude-code", binary: "claude" },
|
|
994
1008
|
{ name: "codex", binary: "codex" },
|
|
@@ -1005,7 +1019,7 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
1005
1019
|
name: r.backend, binary: binaryOf.get(r.backend).binary, installed: r.state !== "not_installed" && r.path !== null, path: r.path ?? "",
|
|
1006
1020
|
deprecated: binaryOf.get(r.backend).deprecated ?? false, state: r.state, minVersion: r.minVersion, ...(r.state === "unknown" ? { unknown: true } : {}),
|
|
1007
1021
|
}));
|
|
1008
|
-
json(res, 200, { backends });
|
|
1022
|
+
json(res, 200, { backends, defaultBackend });
|
|
1009
1023
|
return true;
|
|
1010
1024
|
}
|
|
1011
1025
|
// #1490: seven sequential 2 s `execFileSync("which")` calls used to block the event loop here. The shared probe
|
|
@@ -1017,7 +1031,7 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
1017
1031
|
return { name: b.name, binary: b.binary, installed: path !== null, path: path ?? "", deprecated: b.deprecated ?? false,
|
|
1018
1032
|
...(found.known ? {} : { unknown: true }) };
|
|
1019
1033
|
})).then((backends) => { if (!res.headersSent)
|
|
1020
|
-
json(res, 200, { backends }); }, () => { if (!res.headersSent)
|
|
1034
|
+
json(res, 200, { backends, defaultBackend }); }, () => { if (!res.headersSent)
|
|
1021
1035
|
json(res, 500, { error: "backend probe failed" }); });
|
|
1022
1036
|
return true;
|
|
1023
1037
|
}
|
|
@@ -1540,6 +1554,7 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
1540
1554
|
return true;
|
|
1541
1555
|
}
|
|
1542
1556
|
const click = ctx.clickWebReplyButton.bind(ctx);
|
|
1557
|
+
const arrival = webAuthorityOf(req, url, ctx); // #1939 §3: on arrival, and again after the body
|
|
1543
1558
|
(async () => {
|
|
1544
1559
|
let body;
|
|
1545
1560
|
try {
|
|
@@ -1577,7 +1592,8 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
1577
1592
|
if (!person)
|
|
1578
1593
|
return;
|
|
1579
1594
|
// #1920 review: a guest's choice reaches the agent marked as a guest's, as its typed messages do.
|
|
1580
|
-
const
|
|
1595
|
+
const { authority } = confirmAfterBody(arrival, webAuthorityOf(req, url, ctx, instance));
|
|
1596
|
+
const r = await click(instance, id, index, { key: person.voterKey, userId: person.userId, name: person.name, ...(guest ? { guest: true } : {}), authority });
|
|
1581
1597
|
json(res, r.status, r.status === 200 ? { answered: true, ...(r.options ? { options: r.options } : {}) } : { error: r.error ?? "Refused", ...(r.status === 409 ? { gone: true } : {}) });
|
|
1582
1598
|
})().catch(err => {
|
|
1583
1599
|
ctx.logger.error({ err: err.message }, "Web reply-button click failed");
|
|
@@ -1761,6 +1777,7 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
1761
1777
|
return true;
|
|
1762
1778
|
}
|
|
1763
1779
|
const run = ctx.webCommand.bind(ctx);
|
|
1780
|
+
const arrival = webAuthorityOf(req, url, ctx); // #1939 §3: on arrival, and again after the body
|
|
1764
1781
|
(async () => {
|
|
1765
1782
|
let body;
|
|
1766
1783
|
try {
|
|
@@ -1789,7 +1806,8 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
1789
1806
|
const person = webPersonOr401(req, res, url, ctx); // #1827: asked again, now the body is in
|
|
1790
1807
|
if (!person)
|
|
1791
1808
|
return;
|
|
1792
|
-
const
|
|
1809
|
+
const { authority } = confirmAfterBody(arrival, webAuthorityOf(req, url, ctx, instance));
|
|
1810
|
+
const r = await run({ instance, command, args: str(body.args, MAX_COMMAND_ARGS), confirm: body.confirm }, { publicLink: !!gatewayRequestContext(req), person, authority });
|
|
1793
1811
|
json(res, r.status, r.body);
|
|
1794
1812
|
})().catch(err => {
|
|
1795
1813
|
ctx.logger.error({ err: err.message }, "Web command failed");
|
|
@@ -2445,6 +2463,7 @@ function webEchoPreview(message, names) {
|
|
|
2445
2463
|
}
|
|
2446
2464
|
/** Handle POST /ui/send — extracted for readability. */
|
|
2447
2465
|
function handleSendMessage(req, res, ctx) {
|
|
2466
|
+
const arrival = webAuthorityOf(req, new URL(req.url ?? "/", "http://localhost"), ctx); // #1939 §3: again after the body
|
|
2448
2467
|
let body = "";
|
|
2449
2468
|
let size = 0;
|
|
2450
2469
|
req.on("data", (chunk) => {
|
|
@@ -2512,6 +2531,8 @@ function handleSendMessage(req, res, ctx) {
|
|
|
2512
2531
|
json(res, 401, { error: WEB_TOKEN_INVALID_MESSAGE });
|
|
2513
2532
|
return;
|
|
2514
2533
|
}
|
|
2534
|
+
// #1939 S1: what the receipt records about who authorized it (observe only: agents still read web as recognition).
|
|
2535
|
+
const { authority } = confirmAfterBody(arrival, webAuthorityOf(req, new URL(req.url ?? "/", "http://localhost"), ctx, instance));
|
|
2515
2536
|
const ts = new Date().toISOString();
|
|
2516
2537
|
// The id the agent is given, and the one its delivery reports come back under: the page's ticks.
|
|
2517
2538
|
const messageId = newWebMessageId();
|
|
@@ -2547,7 +2568,7 @@ function handleSendMessage(req, res, ctx) {
|
|
|
2547
2568
|
let delivered;
|
|
2548
2569
|
try {
|
|
2549
2570
|
const deliver = (name, payload) => ctx.deliverHumanToInstance
|
|
2550
|
-
? ctx.deliverHumanToInstance(name, payload, webHumanOrigin(), typed)
|
|
2571
|
+
? ctx.deliverHumanToInstance(name, payload, webHumanOrigin(false, authority), typed)
|
|
2551
2572
|
: ctx.deliverToInstance(name, payload);
|
|
2552
2573
|
delivered = await deliver(instance, {
|
|
2553
2574
|
type: "fleet_inbound",
|
|
@@ -2825,8 +2846,8 @@ function prefsViewer(req, url, ctx) {
|
|
|
2825
2846
|
return null;
|
|
2826
2847
|
// #1920: a guest is a guest on every listener. The public link's page and reads too — before, a guest signed in through
|
|
2827
2848
|
// it (#2094) read the host's preferences unfiltered: the sidebar's order and favourites, naming instances out of its
|
|
2828
|
-
// scope.
|
|
2829
|
-
const guest = v.via === "session" && v.session?.tier === "guest" ? { guest: true } : {};
|
|
2849
|
+
// scope. Its account (phase 3) names it on its page.
|
|
2850
|
+
const guest = v.via === "session" && v.session?.tier === "guest" ? { guest: true, ...(v.session.owner ? { owner: v.session.owner } : {}) } : {};
|
|
2830
2851
|
if (!gatewayRequestContext(req) && v.via === "session" && v.session) {
|
|
2831
2852
|
const person = webPersonKey(v.session.owner);
|
|
2832
2853
|
return { person, epoch: v.session.handle, scope: person === OPERATOR ? "operator" : "person", canWrite: v.session.tier === "admin", via: "session", ...guest };
|
|
@@ -2854,6 +2875,10 @@ function shellHtmlTag(ctx, mode, viewer) {
|
|
|
2854
2875
|
function shellOwnerCss(ctx, mode, viewer) {
|
|
2855
2876
|
return mode === "full" && viewer?.person !== null && viewer?.person !== undefined && ctx.ownerCss ? ctx.ownerCss.active(viewer.person) : null;
|
|
2856
2877
|
}
|
|
2878
|
+
function guestBodyAttrs(ctx, owner, attr) {
|
|
2879
|
+
const badge = ctx.guestBadge?.(owner);
|
|
2880
|
+
return ` data-tier="guest"${badge ? ` data-guest-name="${attr(badge.name)}" data-guest-platform="${attr(badge.platform)}"` : ""}`;
|
|
2881
|
+
}
|
|
2857
2882
|
function shellBodyTag(req, ctx, mode, viewer) {
|
|
2858
2883
|
const p = shellPreview(req, ctx, mode);
|
|
2859
2884
|
const ownerCss = shellOwnerCss(ctx, mode, viewer);
|
|
@@ -2870,8 +2895,8 @@ function shellBodyTag(req, ctx, mode, viewer) {
|
|
|
2870
2895
|
+ ` data-preview-reason-code="${attr(p ? p.code ?? "" : gatewayRequestContext(req) ? "publicLink" : "notOffered")}"`
|
|
2871
2896
|
// #1723: why the public link's preview tunnel failed (a fixed reason the page words), when it did.
|
|
2872
2897
|
+ `${p?.detail ? ` data-preview-detail="${attr(p.detail)}"` : ""}`
|
|
2873
|
-
// #1523 N3: whether the fleet serves /api/ai-usage (web.usage_panel), for every page's
|
|
2874
|
-
+ ` data-usage-panel="${ctx.fleetConfig?.web?.usage_panel === false ? "0" : "1"}"`
|
|
2898
|
+
// #1523 N3: whether the fleet serves /api/ai-usage (web.usage_panel), for every page's ◔ — never to a guest (#1920).
|
|
2899
|
+
+ ` data-usage-panel="${ctx.fleetConfig?.web?.usage_panel === false || viewer?.guest ? "0" : "1"}"`
|
|
2875
2900
|
// #1218: whether the Log tab is on (web.log_tab; off by default): the tab says so and offers to turn it on.
|
|
2876
2901
|
+ ` data-log-tab="${ctx.fleetConfig?.web?.log_tab === true ? "1" : "0"}"`
|
|
2877
2902
|
// #1937: the upload limits in force for this page (the config's; the public link's own cap on it) — what the chat
|
|
@@ -2882,7 +2907,9 @@ function shellBodyTag(req, ctx, mode, viewer) {
|
|
|
2882
2907
|
// #1629 r4 §3: the person's owner CSS in force (a sha), linked by the page unless this device or this load says not.
|
|
2883
2908
|
+ ` data-owner-css-state="${ownerCssState}"${ownerCss ? ` data-owner-css="${attr(ownerCss)}"` : ""}`
|
|
2884
2909
|
// #1589: the public link shows attachments as downloads only (no inline text or media): its scope is unchanged.
|
|
2885
|
-
+ `${gatewayRequestContext(req) ? ' data-public-link="1"' : ""}
|
|
2910
|
+
+ `${gatewayRequestContext(req) ? ' data-public-link="1"' : ""}`
|
|
2911
|
+
// #1920 phase 3: a guest's page — the shell shows only what a guest may use, and who it is signed in as.
|
|
2912
|
+
+ `${mode === "full" && viewer?.guest && viewer.owner ? guestBodyAttrs(ctx, viewer.owner, attr) : ""}>`;
|
|
2886
2913
|
}
|
|
2887
2914
|
/** The app shell page for one entry, under the panels' CSP; it may frame exactly <preview origin>/frame, and only
|
|
2888
2915
|
* when this load chose one. */
|