@songsid/agend 2.3.0-beta.2 → 2.3.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-whats-new.js +2 -0
- package/dist/agent-whats-new.js.map +1 -1
- package/dist/auth-api.d.ts +30 -1
- package/dist/auth-api.js +42 -12
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/claude-code.d.ts +2 -2
- package/dist/backend/claude-code.js +5 -4
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/types.d.ts +6 -0
- package/dist/backend/types.js +15 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/backend-availability.d.ts +120 -0
- package/dist/backend-availability.js +396 -0
- package/dist/backend-availability.js.map +1 -0
- package/dist/backend-command.d.ts +21 -3
- package/dist/backend-command.js +23 -4
- package/dist/backend-command.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +7 -0
- package/dist/channel/adapters/discord.js +35 -0
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/mcp-tools.js +8 -4
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/types.d.ts +6 -0
- package/dist/classic-channel-manager.d.ts +8 -3
- package/dist/classic-channel-manager.js +10 -3
- package/dist/classic-channel-manager.js.map +1 -1
- package/dist/config-validator.js +73 -2
- package/dist/config-validator.js.map +1 -1
- package/dist/delivery-outbox.js +5 -0
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/fleet-context.d.ts +10 -0
- package/dist/fleet-manager.d.ts +238 -5
- package/dist/fleet-manager.js +878 -71
- package/dist/fleet-manager.js.map +1 -1
- package/dist/fleet-traffic.d.ts +38 -0
- package/dist/fleet-traffic.js +96 -0
- package/dist/fleet-traffic.js.map +1 -0
- package/dist/general-knowledge/skills/credential-profiles/SKILL.md +13 -5
- package/dist/general-knowledge/skills/multi-channel/SKILL.md +1 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +56 -11
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +398 -0
- package/dist/human-delivery-receipts.js +2 -1
- package/dist/human-delivery-receipts.js.map +1 -1
- package/dist/locale.js +48 -8
- package/dist/locale.js.map +1 -1
- package/dist/memory-hogs.d.ts +27 -0
- package/dist/memory-hogs.js +124 -0
- package/dist/memory-hogs.js.map +1 -0
- package/dist/outbound-handlers.d.ts +2 -0
- package/dist/outbound-handlers.js +40 -1
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +92 -0
- package/dist/outbound-schemas.js +55 -1
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/public-web-gateway.js +1 -1
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/public-web-link.d.ts +9 -0
- package/dist/public-web-link.js +8 -0
- package/dist/public-web-link.js.map +1 -1
- package/dist/quickstart-api.d.ts +10 -1
- package/dist/quickstart-api.js +12 -4
- package/dist/quickstart-api.js.map +1 -1
- package/dist/reply-buttons-controller.d.ts +2 -0
- package/dist/reply-buttons-controller.js +1 -1
- package/dist/reply-buttons-controller.js.map +1 -1
- package/dist/tmux-manager.d.ts +5 -0
- package/dist/tmux-manager.js +16 -0
- package/dist/tmux-manager.js.map +1 -1
- package/dist/tool-permissions.js +4 -0
- package/dist/tool-permissions.js.map +1 -1
- package/dist/topic-commands.d.ts +5 -0
- package/dist/topic-commands.js +16 -0
- package/dist/topic-commands.js.map +1 -1
- package/dist/types.d.ts +25 -1
- package/dist/ui/app-effects.js +33 -9
- package/dist/ui/chat-effects.js +62 -4
- package/dist/ui/chat-strings.js +40 -4
- package/dist/ui/chat-thread.js +17 -6
- package/dist/ui/effect-trial-panel.js +19 -16
- package/dist/ui/fleet-canvas-layout.js +111 -0
- package/dist/ui/fleet-canvas.js +489 -0
- package/dist/ui/fleet-org.js +27 -12
- package/dist/ui/fleet-traffic.js +224 -0
- package/dist/ui/panel-chat.js +12 -5
- package/dist/ui/panel-details.js +2 -1
- package/dist/ui/panel-fleet.js +32 -11
- package/dist/ui/panel-settings.js +6 -4
- package/dist/ui/preview.js +3 -2
- package/dist/ui/settings-appearance.js +34 -18
- package/dist/ui/settings-catalog.js +89 -0
- package/dist/ui/settings-dialogs.js +40 -12
- package/dist/ui/settings-effects.js +4 -3
- package/dist/ui/settings-learn.js +25 -0
- package/dist/ui/settings-model.js +6 -2
- package/dist/ui/settings-mods.js +4 -3
- package/dist/ui/settings-owner-effects.js +8 -8
- package/dist/ui/settings-strings.js +40 -38
- package/dist/ui/shared/app-i18n.js +2 -2
- package/dist/ui/shared/app-prefs.js +1 -1
- package/dist/ui/shared/app-shell.js +23 -1
- package/dist/ui/shared/app-store.js +12 -1
- package/dist/ui/shared/app-stream.js +2 -1
- package/dist/ui/shared/app.css +186 -11
- package/dist/ui/shared/card-strings.js +4 -0
- package/dist/ui/shared/effect-app-runtime.js +1 -1
- package/dist/ui/shared/effect-frame.js +10 -5
- package/dist/ui/shared/effect-geometry.js +24 -9
- package/dist/ui/shared/effect-policy.js +2 -1
- package/dist/ui/shared/effect-registry.js +71 -17
- package/dist/ui/shared/effect-stock-trust.js +23 -2
- package/dist/ui/shared/effect-trial-shadow.js +62 -0
- package/dist/ui/shared/effect-trial-stage.js +15 -9
- package/dist/ui/shared/effect-trial.css +40 -0
- package/dist/ui/shared/identity-card.js +75 -4
- package/dist/ui/shared/identity-color.js +3 -3
- package/dist/ui/shared/instance-nav.js +20 -4
- package/dist/ui/shared/panel-view.js +24 -4
- package/dist/ui/shared/view-strings.js +22 -0
- package/dist/view-api.d.ts +32 -0
- package/dist/view-api.js +55 -4
- package/dist/view-api.js.map +1 -1
- package/dist/web-api.d.ts +19 -4
- package/dist/web-api.js +178 -20
- package/dist/web-api.js.map +1 -1
- package/dist/web-appearance-policy.d.ts +4 -6
- package/dist/web-appearance-policy.js +5 -12
- package/dist/web-appearance-policy.js.map +1 -1
- package/dist/web-appearance-tools.js +5 -1
- package/dist/web-appearance-tools.js.map +1 -1
- package/dist/web-appearance-view.d.ts +14 -1
- package/dist/web-appearance-view.js +98 -10
- package/dist/web-appearance-view.js.map +1 -1
- package/dist/web-appearance.d.ts +2 -2
- package/dist/web-appearance.js +5 -6
- package/dist/web-appearance.js.map +1 -1
- package/dist/web-auth.js +1 -1
- package/dist/web-auth.js.map +1 -1
- package/dist/web-card.d.ts +14 -0
- package/dist/web-card.js +40 -6
- package/dist/web-card.js.map +1 -1
- package/dist/web-channel-echo.d.ts +7 -1
- package/dist/web-channel-echo.js +6 -2
- package/dist/web-channel-echo.js.map +1 -1
- package/dist/web-chat-history.d.ts +40 -0
- package/dist/web-chat-history.js +66 -3
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-commands.d.ts +1 -0
- package/dist/web-effect-tools.js +2 -3
- package/dist/web-effect-tools.js.map +1 -1
- package/dist/web-effect-trials.d.ts +2 -2
- package/dist/web-effect-trials.js +3 -3
- package/dist/web-effect-trials.js.map +1 -1
- package/dist/web-guest-access.d.ts +125 -0
- package/dist/web-guest-access.js +232 -0
- package/dist/web-guest-access.js.map +1 -0
- package/dist/web-guest-rooms.d.ts +82 -0
- package/dist/web-guest-rooms.js +135 -0
- package/dist/web-guest-rooms.js.map +1 -0
- package/dist/web-identity-palette.d.ts +1 -1
- package/dist/web-identity-palette.js +2 -2
- package/dist/web-identity-palette.js.map +1 -1
- package/dist/web-login.d.ts +47 -0
- package/dist/web-login.js +115 -0
- package/dist/web-login.js.map +1 -1
- package/dist/web-mod-agent.d.ts +1 -1
- package/dist/web-mod-agent.js +2 -4
- package/dist/web-mod-agent.js.map +1 -1
- package/dist/web-mod-store.d.ts +5 -0
- package/dist/web-mod-store.js +6 -0
- package/dist/web-mod-store.js.map +1 -1
- package/dist/web-prefs.d.ts +8 -0
- package/dist/web-prefs.js +9 -0
- package/dist/web-prefs.js.map +1 -1
- package/dist/web-preview.d.ts +6 -2
- package/dist/web-preview.js +4 -2
- package/dist/web-preview.js.map +1 -1
- package/dist/web-returning-device.d.ts +39 -0
- package/dist/web-returning-device.js +46 -5
- package/dist/web-returning-device.js.map +1 -1
- package/dist/web-session.d.ts +58 -10
- package/dist/web-session.js +104 -22
- package/dist/web-session.js.map +1 -1
- package/dist/web-settings-tools.d.ts +81 -0
- package/dist/web-settings-tools.js +257 -0
- package/dist/web-settings-tools.js.map +1 -0
- package/dist/web-theme.d.ts +2 -0
- package/dist/web-theme.js +10 -1
- package/dist/web-theme.js.map +1 -1
- package/package.json +1 -1
|
@@ -95,14 +95,35 @@ function outset(style) {
|
|
|
95
95
|
}
|
|
96
96
|
return pad <= 256 ? Math.ceil(pad) : null;
|
|
97
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* #2086: the identity card's banner fill (a blurred copy beside a centred banner) — the one filter stock admits. Only
|
|
100
|
+
* this exact value, on exactly `.id-banner-box > img.id-banner-fill`, absolutely positioned in that box (so the box is
|
|
101
|
+
* its containing block — #2089 review: a fixed fill is the viewport's, and no box clips it), and the box positioned and
|
|
102
|
+
* clipping on both axes; its paint is then that box's rectangle (the blur and its scale never leave it). The box must
|
|
103
|
+
* also be a real block box that the browser reports as the fill's offset parent (#2089 review r3: `display: contents`
|
|
104
|
+
* keeps the computed position and overflow but generates no box, so an outer ancestor would place the fill, and an inline
|
|
105
|
+
* box clips differently). Anything else with a filter refuses.
|
|
106
|
+
*/
|
|
107
|
+
export const STOCK_BANNER_FILL_FILTER = "blur(24px) saturate(0.75) brightness(0.7)";
|
|
108
|
+
function stockBannerFill(host, el) {
|
|
109
|
+
const box = el.parentElement;
|
|
110
|
+
if (typeof el.matches !== "function" || !el.matches(".id-banner-box > img.id-banner-fill") || !box) return null;
|
|
111
|
+
const s = host.getComputedStyle(el), b = host.getComputedStyle(box);
|
|
112
|
+
if (s.filter !== STOCK_BANNER_FILL_FILTER || !none(s.backdropFilter) || !none(b.filter) || !none(b.backdropFilter)) return null;
|
|
113
|
+
if (!["hidden", "clip"].includes(b.overflowX) || !["hidden", "clip"].includes(b.overflowY)) return null;
|
|
114
|
+
if (s.position !== "absolute" || !["relative", "absolute"].includes(b.position)) return null;
|
|
115
|
+
if (b.display !== "block" || el.offsetParent !== box) return null;
|
|
116
|
+
return rect(box);
|
|
117
|
+
}
|
|
98
118
|
function protectedPaint(host, root, budget) {
|
|
99
119
|
const nodes = [root, ...root.querySelectorAll("*")]; budget.n += nodes.length;
|
|
100
120
|
if (budget.n > 4096) return null;
|
|
101
121
|
let union = null;
|
|
102
122
|
for (const el of nodes) {
|
|
103
123
|
if (!visible(host, el)) continue;
|
|
104
|
-
const style = host.getComputedStyle(el),
|
|
105
|
-
|
|
124
|
+
const style = host.getComputedStyle(el), pad = outset(style);
|
|
125
|
+
const r = none(style.filter) ? rect(el) : stockBannerFill(host, el);
|
|
126
|
+
if (!r || pad === null || !none(style.backdropFilter)) return null;
|
|
106
127
|
const a = { x: r.x - pad, y: r.y - pad, width: r.width + pad * 2, height: r.height + pad * 2 };
|
|
107
128
|
if (!union) union = a;
|
|
108
129
|
else { const x = Math.min(union.x, a.x), y = Math.min(union.y, a.y); union = { x, y, width: Math.max(union.x + union.width, a.x + a.width) - x, height: Math.max(union.y + union.height, a.y + a.height) - y }; }
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/** #2039: fixed host-only closed shadow shell. Owner CSS has no exported parts or slots into it. */
|
|
2
|
+
const roots = new WeakMap();
|
|
3
|
+
const clear = root => { while (root.firstChild) root.removeChild(root.firstChild); };
|
|
4
|
+
function sheetOf(host, record) {
|
|
5
|
+
try {
|
|
6
|
+
const link = record.link, sheet = link?.sheet;
|
|
7
|
+
return record.ready && !record.dead && roots.get(record.panel)?.current === record && record.root.mode === "closed" && record.root.host === record.panel
|
|
8
|
+
&& link.getRootNode() === record.root && link.isConnected && link.disabled === false && sheet?.ownerNode === link && sheet.disabled === false
|
|
9
|
+
&& sheet.href === new URL("/assets/effect-trial.css", host.location.origin).href && typeof sheet.insertRule === "function" && typeof sheet.deleteRule === "function" && sheet.cssRules ? sheet : null;
|
|
10
|
+
} catch { return null; }
|
|
11
|
+
}
|
|
12
|
+
/** Reflect only a shell issued here, after its fixed same-origin stylesheet loaded. Never a serialized capability. */
|
|
13
|
+
export function effectTrialShadow(host, panel) {
|
|
14
|
+
const record = roots.get(panel)?.current;
|
|
15
|
+
return record && sheetOf(host, record) === record.proof?.sheet ? record.proof : null;
|
|
16
|
+
}
|
|
17
|
+
export function createEffectTrialShadow({ window: host, panel, title, labels, onStop, onClose, onReady, onError } = {}) {
|
|
18
|
+
if (!host?.document || panel?.ownerDocument !== host.document || typeof panel.attachShadow !== "function" || typeof onReady !== "function" || typeof onError !== "function") return null;
|
|
19
|
+
let slot = roots.get(panel), cleanup = null;
|
|
20
|
+
const clock = () => (host.performance ?? globalThis.performance).now(), later = host.setTimeout?.bind(host) ?? globalThis.setTimeout, cancel = host.clearTimeout?.bind(host) ?? globalThis.clearTimeout;
|
|
21
|
+
try {
|
|
22
|
+
if (slot?.current && !slot.current.dead) return null;
|
|
23
|
+
if (!slot) { slot = { root: panel.attachShadow({ mode: "closed" }), current: null }; roots.set(panel, slot); }
|
|
24
|
+
const root = slot.root; clear(root);
|
|
25
|
+
const doc = host.document, element = (tag, cls) => { const el = doc.createElement(tag); el.className = cls; return el; };
|
|
26
|
+
const link = element("link", "effect-trial-style"); link.setAttribute("rel", "stylesheet"); link.setAttribute("href", "/assets/effect-trial.css");
|
|
27
|
+
const head = element("header", "effect-trial-head"), label = element("strong", "effect-trial-label"), name = element("span", "effect-trial-title"), close = element("button", "effect-trial-close"), controls = element("div", "effect-trial-controls"), stop = element("button", "effect-trial-stop"), body = element("div", "effect-trial-body"), stage = element("div", "effect-trial-stage"), note = element("p", "effect-trial-note");
|
|
28
|
+
close.setAttribute("type", "button"); stop.setAttribute("type", "button"); stage.setAttribute("aria-hidden", "true");
|
|
29
|
+
head.append(label, name, close); controls.append(stop); body.append(stage);
|
|
30
|
+
const nodes = [head, controls, body, note]; for (const el of nodes) el.setAttribute("hidden", "");
|
|
31
|
+
const began = clock(); if (!Number.isFinite(began) || began < 0) return null;
|
|
32
|
+
let timer = null;
|
|
33
|
+
const record = { panel, root, link, stage, head, controls, body, note, close, stop, ready: false, dead: false, proof: null }; slot.current = record;
|
|
34
|
+
const text = (nextTitle, nextLabels) => {
|
|
35
|
+
if (record.dead || !nextLabels) return;
|
|
36
|
+
label.textContent = nextLabels.label; name.textContent = typeof nextTitle === "string" ? nextTitle : "";
|
|
37
|
+
close.textContent = nextLabels.close; stop.textContent = nextLabels.stop; note.textContent = nextLabels.note;
|
|
38
|
+
panel.setAttribute("data-effect-trial-theme", doc.documentElement.dataset.theme === "dark" ? "dark" : "light");
|
|
39
|
+
};
|
|
40
|
+
const failed = () => { if (!record.dead) { dispose(); try { onError("stage_unavailable"); } catch { /* cleanup already owns failure */ } } };
|
|
41
|
+
const loaded = () => {
|
|
42
|
+
if (record.dead || record.ready || slot.current !== record) return;
|
|
43
|
+
const at = clock(); if (!Number.isFinite(at) || at < began || at - began >= 10000) { failed(); return; }
|
|
44
|
+
record.ready = true; const sheet = sheetOf(host, record);
|
|
45
|
+
if (!sheet) { failed(); return; }
|
|
46
|
+
cancel(timer); timer = null;
|
|
47
|
+
record.proof = Object.freeze({ panel, root, link, sheet, stage, head, controls, body, note, close, stop });
|
|
48
|
+
for (const el of nodes) el.removeAttribute("hidden");
|
|
49
|
+
try { onReady(record.proof); } catch { failed(); }
|
|
50
|
+
};
|
|
51
|
+
const stopClick = () => { if (!record.dead) onStop?.(); }, closeClick = () => { if (!record.dead) onClose?.(); };
|
|
52
|
+
function dispose() {
|
|
53
|
+
if (record.dead) return; record.dead = true; cancel(timer); timer = null;
|
|
54
|
+
link.removeEventListener("load", loaded); link.removeEventListener("error", failed); stop.removeEventListener("click", stopClick); close.removeEventListener("click", closeClick);
|
|
55
|
+
if (slot.current === record) { slot.current = null; clear(root); }
|
|
56
|
+
}
|
|
57
|
+
cleanup = dispose;
|
|
58
|
+
link.addEventListener("load", loaded); link.addEventListener("error", failed); stop.addEventListener("click", stopClick); close.addEventListener("click", closeClick);
|
|
59
|
+
text(title, labels); timer = later(failed, 10000); root.append(link, ...nodes);
|
|
60
|
+
return Object.freeze({ dispose, text });
|
|
61
|
+
} catch { cleanup?.(); return null; }
|
|
62
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** #1629: host-issued LOCAL trial paint receipt. No authority, loader, frame factory or pool is created here. */
|
|
2
|
+
import { effectTrialShadow } from "./effect-trial-shadow.js";
|
|
2
3
|
import { effectHostStyle } from "./effect-host-style.js";
|
|
3
4
|
import { effectStockSnapshot } from "./effect-stock-snapshot.js";
|
|
4
5
|
import { STOCK_EFFECT_BARRIERS } from "./effect-stock-trust.js";
|
|
@@ -58,10 +59,11 @@ export function effectTrialStage(receipt, expected, stock) {
|
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
/** Only the fixed host TrialPanel supplies these elements and a current server-run lease. Not an agent manifest. */
|
|
61
|
-
export function createEffectTrialStage({ window: host, panel, stage, binding: raw, current,
|
|
62
|
+
export function createEffectTrialStage({ window: host, panel, stage, binding: raw, current, onStop = () => {} } = {}) {
|
|
62
63
|
const own = binding(raw);
|
|
63
|
-
if (!own || typeof current !== "function" || typeof
|
|
64
|
-
|
|
64
|
+
if (!own || typeof current !== "function" || typeof onStop !== "function" || !host?.document || !panel || !stage || owners.has(stage)) return null;
|
|
65
|
+
const shell = effectTrialShadow(host, panel), hostSheet = effectHostStyle(host);
|
|
66
|
+
let sheet; try { sheet = shell?.sheet; if (!shell || shell.stage !== stage || !hostSheet || !sheet || host.CSS?.supports("contain", "paint") !== true || host.CSS.supports("overflow", "clip") !== true) return null; } catch { return null; }
|
|
65
67
|
const receipt = Object.freeze(Object.create(null)), frames = new Set(), listeners = new Set(); let dead = false, constraint = null;
|
|
66
68
|
const identity = Object.freeze({ key: own.key, id: own.id, instance: own.instance, generation: own.generation, serial: ++serial });
|
|
67
69
|
const invalidate = reason => {
|
|
@@ -76,23 +78,27 @@ export function createEffectTrialStage({ window: host, panel, stage, binding: ra
|
|
|
76
78
|
function check(stock) {
|
|
77
79
|
try {
|
|
78
80
|
// This callback may reenter disposal or replace the owner. Recheck identity after it returns.
|
|
79
|
-
if (dead || current(own) !== true || constraint !== null && constraint() !== true || dead || owners.get(stage) !== receipt ||
|
|
81
|
+
if (dead || current(own) !== true || constraint !== null && constraint() !== true || dead || owners.get(stage) !== receipt || dead) return null;
|
|
80
82
|
const proof = effectStockSnapshot(stock), doc = host.document;
|
|
81
|
-
if (effectHostStyle(host) !==
|
|
83
|
+
if (effectHostStyle(host) !== hostSheet || effectTrialShadow(host, panel) !== shell) return null;
|
|
82
84
|
// The own panel MUST still suppress ordinary foreground effects. Never synthesize front:true.
|
|
83
85
|
if (!proof || proof.front !== false || doc.visibilityState !== "visible" || doc.hasFocus?.() !== true || doc.fullscreenElement || doc.pictureInPictureElement) return null;
|
|
84
86
|
const split = doc.querySelector(".main > .p-chat > .chat-split");
|
|
85
87
|
if (!split || doc.querySelectorAll(".main > .p-chat").length !== 1 || doc.querySelectorAll(".chat-split > .pv-panel").length !== 1
|
|
86
88
|
|| panel.parentElement !== split || panel.ownerDocument !== doc || panel.isConnected !== true || panel.className !== "pv-panel effect-trial-panel" || panel.getAttribute("data-trust") !== "preview") return null;
|
|
87
|
-
const children = [...
|
|
88
|
-
if (children.length !== names.length || children.some((el, i) => el.className !== names[i]) || children[
|
|
89
|
+
const children = [...shell.root.children], names = ["effect-trial-style", "effect-trial-head", "effect-trial-controls", "effect-trial-body", "effect-trial-note"];
|
|
90
|
+
if (panel.children.length !== 0 || children.length !== names.length || children.some((el, i) => el.className !== names[i]) || children[3].children.length !== 1 || children[3].firstElementChild !== stage
|
|
89
91
|
|| stage.className !== "effect-trial-stage" || stage.ownerDocument !== doc || stage.isConnected !== true || stage.hasAttribute("tabindex") || stage.hasAttribute("contenteditable")) return null;
|
|
92
|
+
for (const control of [shell.head, shell.controls, shell.close, shell.stop, shell.note]) {
|
|
93
|
+
const style = host.getComputedStyle(control);
|
|
94
|
+
if (style.display === "none" || style.visibility !== "visible" || style.opacity !== "1" || !rect(control)) return null;
|
|
95
|
+
}
|
|
90
96
|
if ([...doc.querySelectorAll(STOCK_EFFECT_BARRIERS)].some(el => el !== panel && visible(host, el))
|
|
91
97
|
|| [...doc.querySelectorAll(".pv-panel")].some(el => el !== panel && visible(host, el))) return null;
|
|
92
98
|
const active = doc.activeElement;
|
|
93
99
|
if (active && active !== doc.body && active !== doc.documentElement && (active.matches("input,select,textarea,[contenteditable]") || active.isContentEditable)) return null;
|
|
94
100
|
let el = stage, depth = 0;
|
|
95
|
-
while (el) { const s = host.getComputedStyle(el); if (++depth > 128 || el.ownerDocument !== doc || !neutral(s) || s.opacity !== "1" || s.visibility !== "visible" || s.display === "none" || s.contain !== (el === stage ? "paint" : "none")) return null; if (el === doc.documentElement) break; el = el.parentElement; }
|
|
101
|
+
while (el) { const s = host.getComputedStyle(el); if (++depth > 128 || el.ownerDocument !== doc || !neutral(s) || s.opacity !== "1" || s.visibility !== "visible" || s.display === "none" || s.contain !== (el === stage ? "paint" : "none")) return null; if (el === doc.documentElement) break; el = el.parentElement ?? (el.getRootNode?.() === shell.root ? panel : null); }
|
|
96
102
|
if (el !== doc.documentElement) return null;
|
|
97
103
|
const s = host.getComputedStyle(stage), r = rect(stage), p = rect(panel);
|
|
98
104
|
if (!r || !p || s.position !== "relative" || s.zIndex !== "0" || s.overflowX !== "clip" || s.overflowY !== "clip"
|
|
@@ -103,7 +109,7 @@ export function createEffectTrialStage({ window: host, panel, stage, binding: ra
|
|
|
103
109
|
if ([...stage.children].some(child => ![...frames].some(f => f.element === child))) return null;
|
|
104
110
|
const width = Math.floor(r.width), height = Math.floor(r.height);
|
|
105
111
|
if (width < 1 || height < 1 || width > 4096 || height > 4096 || width * height > 8388608 || dead || owners.get(stage) !== receipt) return null;
|
|
106
|
-
if (current(own) !== true || constraint !== null && constraint() !== true || dead || owners.get(stage) !== receipt ||
|
|
112
|
+
if (current(own) !== true || constraint !== null && constraint() !== true || dead || owners.get(stage) !== receipt || dead || stage.isConnected !== true || panel.parentElement !== split) return null;
|
|
107
113
|
return { x: r.x, y: r.y, width, height };
|
|
108
114
|
} catch { return null; }
|
|
109
115
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/* #2039: fixed closed-shadow Trial chrome; no inherited owner-CSS typography, parts or slots. */
|
|
2
|
+
:host {
|
|
3
|
+
all: initial !important;
|
|
4
|
+
box-sizing: border-box !important;
|
|
5
|
+
display: flex !important;
|
|
6
|
+
flex: 0 0 45% !important;
|
|
7
|
+
flex-direction: column !important;
|
|
8
|
+
align-self: stretch !important;
|
|
9
|
+
width: 45% !important;
|
|
10
|
+
min-width: 320px !important;
|
|
11
|
+
max-width: calc(100% - 368px) !important;
|
|
12
|
+
min-height: 0 !important;
|
|
13
|
+
height: auto !important;
|
|
14
|
+
position: relative !important;
|
|
15
|
+
z-index: 1 !important;
|
|
16
|
+
overflow: clip !important;
|
|
17
|
+
background: #ffffff !important;
|
|
18
|
+
color: #1b1b1f !important;
|
|
19
|
+
font: 400 16px/1.4 system-ui, sans-serif !important;
|
|
20
|
+
direction: ltr !important;
|
|
21
|
+
unicode-bidi: isolate !important;
|
|
22
|
+
text-align: start !important;
|
|
23
|
+
visibility: visible !important;
|
|
24
|
+
opacity: 1 !important;
|
|
25
|
+
}
|
|
26
|
+
:host([data-effect-trial-theme="dark"]) { background: #202024 !important; color: #f0f0f4 !important; }
|
|
27
|
+
:host::before, :host::after { all: initial !important; content: none !important; display: none !important; }
|
|
28
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
29
|
+
[hidden] { display: none !important; }
|
|
30
|
+
.effect-trial-head, .effect-trial-controls, .effect-trial-note { position: relative; z-index: 1; padding: 12px; margin: 0; }
|
|
31
|
+
.effect-trial-head { display: flex; align-items: center; gap: 8px; border-bottom: 1px solid currentColor; }
|
|
32
|
+
.effect-trial-title { flex: 1; min-width: 0; overflow-wrap: anywhere; }
|
|
33
|
+
button { appearance: auto; display: inline-block; font: inherit; color: inherit; background: transparent; border: 1px solid currentColor; border-radius: 6px; padding: 8px 12px; cursor: pointer; }
|
|
34
|
+
button:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }
|
|
35
|
+
.effect-trial-body { flex: 1; min-height: 0; display: flex; padding: 12px; }
|
|
36
|
+
.effect-trial-stage { position: relative; z-index: 0; flex: 1; min-width: 0; min-height: 0; contain: paint; overflow: clip; padding: 0; border: 0; outline: none; box-shadow: none; }
|
|
37
|
+
.effect-trial-note { font-size: 14px; }
|
|
38
|
+
@media (max-width: 899px) {
|
|
39
|
+
:host { position: fixed !important; inset: 0 !important; z-index: 37 !important; width: auto !important; min-width: 0 !important; max-width: none !important; padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left) !important; }
|
|
40
|
+
}
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
//
|
|
5
5
|
// Nothing the agent wrote is markup: the tagline and bio are text nodes (never linkified), the pictures are same-origin
|
|
6
6
|
// <img> elements whose URLs are checked here again, and sizes are class tokens only (card-s | card-m | card-l, heights in
|
|
7
|
-
// app.css) — no style attribute, no CSSOM size (#1300).
|
|
7
|
+
// app.css) — no style attribute, no CSSOM size (#1300). #2086: the banner's focus is a position, not a size: one custom
|
|
8
|
+
// property set through the CSSOM (`--id-focus`), and the banner is never drawn wider than BANNER_MAX_ASPECT : 1. The size is a viewer's choice: every card's default
|
|
8
9
|
// (`card.size`) and the cards set apart from it (`card.sizeByInstance`, at most 200), synced like the other preferences.
|
|
9
10
|
import { html, useEffect, useState } from "./app-html.js";
|
|
10
11
|
import { t } from "./app-i18n.js";
|
|
@@ -13,6 +14,28 @@ import { onPrefsChanged, prefInForce, savePref } from "./app-prefs.js";
|
|
|
13
14
|
import "./card-strings.js";
|
|
14
15
|
|
|
15
16
|
export const CARD_SIZES = ["s", "m", "l"];
|
|
17
|
+
/**
|
|
18
|
+
* #2086: the widest a banner is ever drawn (width : height) — app.css caps its width at this many times its size's
|
|
19
|
+
* tallest height. So the band of the picture shown everywhere is (picture's aspect ÷ this) of its height, at the focus.
|
|
20
|
+
*/
|
|
21
|
+
export const BANNER_MAX_ASPECT = 5;
|
|
22
|
+
/**
|
|
23
|
+
* #2086 (#2089 review): the narrowest a card draws its banner — 9 : 4 (app.css caps a banner's height at its box's width ÷ 2.25, on
|
|
24
|
+
* every layout) — which crops a wide picture's sides. Every card is between this and BANNER_MAX_ASPECT, so the area shown on every screen is
|
|
25
|
+
* the narrowest box's width (centred) × the widest box's height (at the focus).
|
|
26
|
+
*/
|
|
27
|
+
export const BANNER_MIN_ASPECT = 9 / 4;
|
|
28
|
+
/** The share of a picture's height a box of `boxAspect` shows (1: all of it; a narrower box crops the sides instead). */
|
|
29
|
+
export const bannerShownHeight = (pictureAspect, boxAspect) => Math.min(1, pictureAspect / boxAspect);
|
|
30
|
+
/** The share of a picture's width a box of `boxAspect` shows, centred (1: all of it). */
|
|
31
|
+
export const bannerShownWidth = (pictureAspect, boxAspect) => Math.min(1, boxAspect / pictureAspect);
|
|
32
|
+
/** The area every screen shows, as shares of the picture: { left, top, width, height } at focus `f`. */
|
|
33
|
+
export function bannerSafeArea(pictureAspect, f) {
|
|
34
|
+
const width = bannerShownWidth(pictureAspect, BANNER_MIN_ASPECT), height = bannerShownHeight(pictureAspect, BANNER_MAX_ASPECT);
|
|
35
|
+
return { left: (1 - width) / 2, top: bannerBandTop(height, f), width, height };
|
|
36
|
+
}
|
|
37
|
+
/** That band's top, as a share of the picture's height, at focus `f` (0–100) — object-position's own rule. */
|
|
38
|
+
export const bannerBandTop = (shown, f) => (1 - shown) * (f / 100);
|
|
16
39
|
export const MAX_CARD_SIZES = 200;
|
|
17
40
|
const own = (o, k) => !!o && Object.prototype.hasOwnProperty.call(o, k);
|
|
18
41
|
const readLocal = (k) => { try { return localStorage.getItem(k); } catch { return null; } };
|
|
@@ -82,6 +105,9 @@ export function checkCard(name, body) {
|
|
|
82
105
|
const v = body[k];
|
|
83
106
|
if (v && typeof v === "object" && typeof v.text === "string" && v.text.length && v.text.length <= MAX[k] && (v.by === "human" || v.by === "agent")) out[k] = { text: v.text, by: v.by };
|
|
84
107
|
}
|
|
108
|
+
// #2086: the banner's focus, a whole number 0 (top) … 100 (bottom).
|
|
109
|
+
const f = body.banner_focus;
|
|
110
|
+
if (f && typeof f === "object" && Number.isInteger(f.value) && f.value >= 0 && f.value <= 100 && (f.by === "human" || f.by === "agent")) out.banner_focus = { value: f.value, by: f.by };
|
|
85
111
|
return Object.keys(out).length ? out : null;
|
|
86
112
|
}
|
|
87
113
|
// #1889 review: per instance, an epoch that moves whenever the version this page wants changes (a clear included).
|
|
@@ -135,7 +161,8 @@ function SizeControl({ name, size }) {
|
|
|
135
161
|
}
|
|
136
162
|
|
|
137
163
|
/**
|
|
138
|
-
* The card. `compact`: the chat header's own small size (no size control
|
|
164
|
+
* The card. `compact`: the chat header's own small size (no size control; the banner still follows the size choice,
|
|
165
|
+
* #2038). Nothing when the card is empty. The
|
|
139
166
|
* tagline and bio say whose they are (title), since a person's field wins over the agent's.
|
|
140
167
|
*/
|
|
141
168
|
export function IdentityCard({ name, card, compact = false, sizeControl = !compact }) {
|
|
@@ -143,11 +170,17 @@ export function IdentityCard({ name, card, compact = false, sizeControl = !compa
|
|
|
143
170
|
const [broken, setBroken] = useState({});
|
|
144
171
|
if (!card) return null;
|
|
145
172
|
const size = compact ? "c" : cardSizeFor(name);
|
|
173
|
+
// #2038: the chat header's card is its own small size, but its banner follows the person's size choice too.
|
|
174
|
+
const bannerSize = compact ? ` bs-${cardSizeFor(name)}` : "";
|
|
146
175
|
const whose = (f) => t(f.by === "human" ? "card.byHuman" : "card.byAgent");
|
|
147
176
|
const pic = (slot, cls, alt) => card[slot] && broken[slot] !== card[slot].src
|
|
148
177
|
? html`<img class=${cls} src=${card[slot].src} alt=${alt} title=${whose(card[slot])} decoding="async" onError=${() => setBroken({ ...broken, [slot]: card[slot].src })} />` : null;
|
|
149
|
-
|
|
150
|
-
|
|
178
|
+
// #2086: the banner at its focus; on a screen wider than BANNER_MAX_ASPECT it is centred over a blurred copy of itself.
|
|
179
|
+
const focus = card.banner_focus ? card.banner_focus.value : 50;
|
|
180
|
+
const atFocus = (el) => { if (el && el.style) el.style.setProperty("--id-focus", `${focus}%`); };
|
|
181
|
+
const banner = pic("banner", "id-banner", "");
|
|
182
|
+
return html`<section class=${`id-card card-${size}${bannerSize}`} aria-label=${t("card.label", name)}>
|
|
183
|
+
${banner ? html`<div class="id-banner-box" ref=${atFocus}><img class="id-banner-fill" src=${card.banner.src} alt="" aria-hidden="true" decoding="async" />${banner}</div>` : null}
|
|
151
184
|
<div class="id-row">
|
|
152
185
|
${pic("portrait", "id-portrait", t("card.portraitAlt", name))}
|
|
153
186
|
<div class="id-text">
|
|
@@ -159,6 +192,44 @@ export function IdentityCard({ name, card, compact = false, sizeControl = !compa
|
|
|
159
192
|
</section>`;
|
|
160
193
|
}
|
|
161
194
|
|
|
195
|
+
// ── #2086: the banner's focus, set in View's Edit profile — the whole picture, what each screen shows of it ──
|
|
196
|
+
/**
|
|
197
|
+
* #2089 review: the picture the focus preview shows — a file just chosen (a blob URL this owns: released when another is
|
|
198
|
+
* chosen and when the dialog closes) or the banner drawn now (a server URL, never released).
|
|
199
|
+
*/
|
|
200
|
+
export function useBannerPreview() {
|
|
201
|
+
const [preview, setPreview] = useState(null);
|
|
202
|
+
useEffect(() => () => { if (preview && preview.blob) { try { URL.revokeObjectURL(preview.blob); } catch { /* already gone */ } } }, [preview]);
|
|
203
|
+
return [preview, {
|
|
204
|
+
fromFile(file) { let url = null; try { url = URL.createObjectURL(file); } catch { return; } setPreview({ src: url, blob: url }); },
|
|
205
|
+
fromSrc(src) { setPreview({ src }); },
|
|
206
|
+
}];
|
|
207
|
+
}
|
|
208
|
+
/** A phone draws an M card's banner at about 3 : 1 (app.css, ≤479 px) — a guide for that size, not every phone card. */
|
|
209
|
+
export const BANNER_PHONE_ASPECT = 3;
|
|
210
|
+
/**
|
|
211
|
+
* The banner whole, with the band every screen shows at this focus (dashed; outside it shaded) and the band a phone
|
|
212
|
+
* shows (solid) — measured from the picture itself once it has loaded. Positions go through the CSSOM (#1300).
|
|
213
|
+
*/
|
|
214
|
+
export function BannerFocusPreview({ src, focus }) {
|
|
215
|
+
const [size, setSize] = useState(null);
|
|
216
|
+
if (!src) return null;
|
|
217
|
+
const ar = size ? size.w / size.h : null;
|
|
218
|
+
const put = (el, a) => { for (const k of ["left", "top", "width", "height"]) el.style.setProperty(k, `${(a[k] * 100).toFixed(2)}%`); };
|
|
219
|
+
// Every screen: the safe area (both crops). The phone guide: an M card on a phone, as drawn there (both crops too).
|
|
220
|
+
const safe = (el) => { if (el && el.style && ar) put(el, bannerSafeArea(ar, focus)); };
|
|
221
|
+
const phone = (el) => {
|
|
222
|
+
if (!el || !el.style || !ar) return;
|
|
223
|
+
const width = bannerShownWidth(ar, BANNER_PHONE_ASPECT), height = bannerShownHeight(ar, BANNER_PHONE_ASPECT);
|
|
224
|
+
put(el, { left: (1 - width) / 2, top: bannerBandTop(height, focus), width, height });
|
|
225
|
+
};
|
|
226
|
+
return html`<div class="bf-wrap">
|
|
227
|
+
<img class="bf-pic" src=${src} alt="" onLoad=${(e) => { const i = e.currentTarget; if (i.naturalWidth && i.naturalHeight) setSize({ w: i.naturalWidth, h: i.naturalHeight }); }} />
|
|
228
|
+
${ar ? html`<div class="bf-band bf-phone" ref=${phone}><span>${t("card.bannerPhone")}</span></div>
|
|
229
|
+
<div class="bf-band bf-safe" ref=${safe}><span>${t("card.bannerSafe")}</span></div>` : null}
|
|
230
|
+
</div>`;
|
|
231
|
+
}
|
|
232
|
+
|
|
162
233
|
// ── A person's picture for the card (View's Edit profile), fitted before it is sent (r4.1 Q3 (b)) ──
|
|
163
234
|
/**
|
|
164
235
|
* `file` fitted to `limit` ({ max_width, max_height, max_bytes }): as it is when it already fits; otherwise, once the
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// (`color`, `color_show`); this module resolves them as src/web-identity-color.ts does (a test pins the two equal),
|
|
4
4
|
// paints the sidebar row through the CSSOM (no style attribute, #1300), and holds the Color dialog.
|
|
5
5
|
// colour: a person's pick > the agent's (themes shown) > its own bot's role colour > automatic (a few soft colours)
|
|
6
|
-
// places: a person's choice > the agent's (themes shown) > this host's default (color.show) >
|
|
6
|
+
// places: a person's choice > the agent's (themes shown) > this host's default (color.show) > all three (#2056)
|
|
7
7
|
// Settings → Web appearance can turn colours off for the host (color.off): nothing is painted, every pick is kept.
|
|
8
8
|
import { html, useEffect, useState } from "./app-html.js";
|
|
9
9
|
import { createStore, useStore } from "./app-store.js";
|
|
@@ -30,7 +30,7 @@ export const AUTO_PALETTE = [
|
|
|
30
30
|
{ id: "moss", light: "#5b7340", dark: "#a9c48d" }, { id: "slate", light: "#5f6b78", dark: "#aeb8c4" },
|
|
31
31
|
];
|
|
32
32
|
export const PLACEMENTS = ["name", "bar", "ring"];
|
|
33
|
-
export const DEFAULT_PLACEMENTS = ["bar", "ring"];
|
|
33
|
+
export const DEFAULT_PLACEMENTS = ["name", "bar", "ring"]; // #2056: all three where nobody chose (was ring + bar)
|
|
34
34
|
const HEX = /^#[0-9a-f]{6}$/;
|
|
35
35
|
const presetOf = (c) => (typeof c === "string" && c.startsWith("preset:") ? IDENTITY_PALETTE.find((p) => p.id === c.slice(7)) : undefined);
|
|
36
36
|
const autoOf = (c) => (typeof c === "string" && c.startsWith("auto:") ? AUTO_PALETTE.find((p) => p.id === c.slice(5)) : undefined);
|
|
@@ -115,7 +115,7 @@ export function colorsOff() {
|
|
|
115
115
|
const f = prefInForce("color.off");
|
|
116
116
|
return f ? f.value === true : readLocal("agend_color_off") === "on";
|
|
117
117
|
}
|
|
118
|
-
/** The default places (null: back to
|
|
118
|
+
/** The default places (null: back to all three, #2056). */
|
|
119
119
|
export function setDefaultPlaces(list) {
|
|
120
120
|
const v = list === null ? null : placesOk(list);
|
|
121
121
|
const same = v && v.length === DEFAULT_PLACEMENTS.length && v.every((p, i) => p === DEFAULT_PLACEMENTS[i]);
|
|
@@ -36,6 +36,22 @@ export function groupOf(it) {
|
|
|
36
36
|
if (tags.length) return tags[0];
|
|
37
37
|
return "Other";
|
|
38
38
|
}
|
|
39
|
+
/** A group's heading: the fallback group ("Other") is a key, shown in the page's language; a tag is shown as it is. */
|
|
40
|
+
export function groupLabel(g) { return g === "Other" ? tn("groupOther") : g; }
|
|
41
|
+
/**
|
|
42
|
+
* #2050: the model as a person reads it. The server's resolver writes English for a model it has not resolved yet
|
|
43
|
+
* ("default (detected when it starts)") and for a CLI's own default ("x (default)"); those are shown in the page's language.
|
|
44
|
+
*/
|
|
45
|
+
export function modelText(it, raw = it && it.model) {
|
|
46
|
+
if (!raw || typeof raw !== "string") return raw;
|
|
47
|
+
if (it.model_source === "unresolved") {
|
|
48
|
+
const why = /^default \((.*)\)$/.exec(raw)?.[1];
|
|
49
|
+
const reason = why === "detected when it starts" ? tn("modelWhyStart") : why === "this CLI does not report a default" ? tn("modelWhyCli") : why;
|
|
50
|
+
return reason ? tn("modelDefaultWhy", { reason }) : raw === "default" ? tn("modelDefault") : raw;
|
|
51
|
+
}
|
|
52
|
+
if (it.model_source === "cli-default") { const m = /^(.*) \(default\)$/.exec(raw); if (m) return tn("modelCliDefault", { model: m[1] }); }
|
|
53
|
+
return raw;
|
|
54
|
+
}
|
|
39
55
|
const BACKEND_LABELS = { "claude-code": "Claude Code", "kiro-cli": "Kiro CLI", codex: "Codex", grok: "Grok Build", antigravity: "Antigravity", opencode: "OpenCode" };
|
|
40
56
|
export function backendLabel(backend) { return BACKEND_LABELS[backend] || backend || "Unknown"; }
|
|
41
57
|
export const KNOWN_BACKENDS = new Set(Object.keys(BACKEND_LABELS));
|
|
@@ -93,8 +109,8 @@ export function instanceTooltip(it) {
|
|
|
93
109
|
if (it.display_name && it.display_name !== name) lines.push(it.display_name, `(${name})`);
|
|
94
110
|
else lines.push(name);
|
|
95
111
|
lines.push(line("tooltipBackend", backendLabel(it.backend)));
|
|
96
|
-
const src = (s) => (s === "instance" || s === "classic" ? "
|
|
97
|
-
lines.push(line("tooltipModel", (it
|
|
112
|
+
const src = (s) => (s === "instance" || s === "classic" ? tn("srcConfigured") : s === "fleet-default" ? tn("srcFleetDefault") : "");
|
|
113
|
+
lines.push(line("tooltipModel", (modelText(it) || tn("tooltipUnavailable")) + src(it.model_source)));
|
|
98
114
|
lines.push(line("tooltipStatus", status));
|
|
99
115
|
lines.push(line("tooltipContext", context));
|
|
100
116
|
if (it.effort) lines.push(line("tooltipEffort", it.effort + src(it.effort_source)));
|
|
@@ -481,8 +497,8 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
|
|
|
481
497
|
const folded = nav.collapsed.has(g);
|
|
482
498
|
return html`<div key=${g} class="v-group">
|
|
483
499
|
<button type="button" class="v-group-head" aria-expanded=${folded ? "false" : "true"} onClick=${() => toggle(g)}
|
|
484
|
-
title=${tn(folded ? "expandGroup" : "collapseGroup", { group: g })} ...${dragProps({ type: "group", name: g })}>
|
|
485
|
-
<${Icon} name="chevron" size=${14} cls=${folded ? "caret folded" : "caret"} /><span class="grow">${g}</span><span class="count">${names.length}</span></button>
|
|
500
|
+
title=${tn(folded ? "expandGroup" : "collapseGroup", { group: groupLabel(g) })} ...${dragProps({ type: "group", name: g })}>
|
|
501
|
+
<${Icon} name="chevron" size=${14} cls=${folded ? "caret folded" : "caret"} /><span class="grow">${groupLabel(g)}</span><span class="count">${names.length}</span></button>
|
|
486
502
|
${folded ? null : html`<ul class="inst-list">${names.map((name, i) => row(name, g, i, names.length))}</ul>`}
|
|
487
503
|
</div>`;
|
|
488
504
|
})}
|
|
@@ -16,7 +16,7 @@ import { HeaderTools, VIEW_SCALE, textSizeStore } from "./header-tools.js";
|
|
|
16
16
|
import { readStream } from "./read-stream.js";
|
|
17
17
|
// The usage dialog's metric row lives with the dialog now (usage-dialog.js); re-exported for what imported it here.
|
|
18
18
|
export { UsageMetric } from "./usage-dialog.js";
|
|
19
|
-
import { backendLabel, STATUS_KEYS } from "./instance-nav.js";
|
|
19
|
+
import { backendLabel, modelText, STATUS_KEYS } from "./instance-nav.js";
|
|
20
20
|
import { viewStore } from "./view-roster-store.js";
|
|
21
21
|
|
|
22
22
|
/** #1580: the app's stream says the fleet is not answering (a restart): the View's polls skip their turn until it is. */
|
|
@@ -27,7 +27,7 @@ import { Dialog } from "./ui-dialog.js";
|
|
|
27
27
|
import { Empty, ErrorState, Skeleton } from "./ui-states.js";
|
|
28
28
|
import { Icon } from "./ui-icons.js";
|
|
29
29
|
import "./view-strings.js";
|
|
30
|
-
import { checkCard, fitPicture, IdentityCard, useCard } from "./identity-card.js";
|
|
30
|
+
import { BannerFocusPreview, checkCard, fitPicture, IdentityCard, useBannerPreview, useCard } from "./identity-card.js";
|
|
31
31
|
import { confirmDialog } from "./ui-confirm.js";
|
|
32
32
|
import { openColorDialog } from "./identity-color.js";
|
|
33
33
|
|
|
@@ -305,7 +305,7 @@ function Card({ it, viewOnly, onEdit }) {
|
|
|
305
305
|
<div class="v-avatar">${it.has_avatar ? html`<img ref=${avatar} alt="" />` : html`<span class="v-initial" aria-hidden="true">${initial}</span>`}</div>
|
|
306
306
|
<div class="v-card-body">
|
|
307
307
|
<div class="v-card-line"><strong>${it.display_name || it.instance_name}</strong>${it.role ? html`<span class="v-role">${it.role}</span>` : null}</div>
|
|
308
|
-
<div class="v-card-meta">${it.instance_name} · ${backendLabel(it.backend)}${it.model ? ` · ${it
|
|
308
|
+
<div class="v-card-meta">${it.instance_name} · ${backendLabel(it.backend)}${it.model ? ` · ${modelText(it)}` : ""}${ctx}</div>
|
|
309
309
|
${open && it.description ? html`<p class="v-card-desc">${it.description}</p>` : null}
|
|
310
310
|
</div>
|
|
311
311
|
<button type="button" class="icon-btn" onClick=${toggle} aria-expanded=${open ? "true" : "false"} aria-label=${tn(open ? "collapseProfile" : "expandProfile")} title=${tn(open ? "collapseProfile" : "expandProfile")}>
|
|
@@ -332,6 +332,11 @@ function EditProfile({ it, onClose }) {
|
|
|
332
332
|
const [removeCard, setRemoveCard] = useState({ portrait: false, banner: false });
|
|
333
333
|
const [clearCard, setClearCard] = useState(false);
|
|
334
334
|
const pics = { portrait: useRef(null), banner: useRef(null) };
|
|
335
|
+
// #2086: the banner's focus — this person's (null: follow the agent's, else the centre), and the picture to preview:
|
|
336
|
+
// a newly chosen file, else the banner drawn now.
|
|
337
|
+
// `agent`: the agent's own focus (what "Follow the agent's" shows; else the centre).
|
|
338
|
+
const [focus, setFocus] = useState({ value: 50, mine: null, start: null, agent: null });
|
|
339
|
+
const [bannerPreview, preview] = useBannerPreview();
|
|
335
340
|
useEffect(() => {
|
|
336
341
|
let live = true;
|
|
337
342
|
// The card's fields stay disabled until this read is in (#1889 review: a draft typed meanwhile was overwritten);
|
|
@@ -343,6 +348,11 @@ function EditProfile({ it, onClose }) {
|
|
|
343
348
|
const resolved = checkCard(it.instance_name, body);
|
|
344
349
|
const mine = (k) => (resolved && resolved[k] && resolved[k].by === "human" ? resolved[k] : null);
|
|
345
350
|
setCard({ mine: { tagline: mine("tagline"), bio: mine("bio"), portrait: mine("portrait"), banner: mine("banner") }, limits: body.limits || null });
|
|
351
|
+
const bf = resolved && resolved.banner_focus;
|
|
352
|
+
const agent = Number.isInteger(body.banner_focus_agent) && body.banner_focus_agent >= 0 && body.banner_focus_agent <= 100 ? body.banner_focus_agent : null;
|
|
353
|
+
const mineFocus = bf && bf.by === "human" ? bf.value : null;
|
|
354
|
+
setFocus({ value: mineFocus ?? agent ?? 50, mine: mineFocus, start: mineFocus, agent });
|
|
355
|
+
if (resolved && resolved.banner) preview.fromSrc(resolved.banner.src);
|
|
346
356
|
setCardText({ tagline: mine("tagline") ? mine("tagline").text : "", bio: mine("bio") ? mine("bio").text : "" });
|
|
347
357
|
}).catch(() => { if (live && lease.current()) setCard(none); });
|
|
348
358
|
return () => { live = false; };
|
|
@@ -360,6 +370,7 @@ function EditProfile({ it, onClose }) {
|
|
|
360
370
|
const clearing = clearCard;
|
|
361
371
|
const before = card && card.mine ? { tagline: card.mine.tagline ? card.mine.tagline.text : "", bio: card.mine.bio ? card.mine.bio.text : "" } : { tagline: "", bio: "" };
|
|
362
372
|
if (!clearing) for (const k of ["tagline", "bio"]) if (cardText[k] !== before[k]) draft[`card_${k}`] = cardText[k];
|
|
373
|
+
if (!clearing && focus.mine !== focus.start) draft.card_banner_focus = focus.mine; // #2086
|
|
363
374
|
const cardPics = {};
|
|
364
375
|
try {
|
|
365
376
|
// A picture too large is fitted first (after asking) — before anything is written, so declining writes nothing.
|
|
@@ -422,10 +433,19 @@ function EditProfile({ it, onClose }) {
|
|
|
422
433
|
<label class="field"><span>${tn("cardTagline")}</span><input value=${cardText.tagline} onInput=${(e) => setCardText({ ...cardText, tagline: e.target.value })} /></label>
|
|
423
434
|
<label class="field"><span>${tn("cardBio")}</span><textarea rows="4" value=${cardText.bio} onInput=${(e) => setCardText({ ...cardText, bio: e.target.value })}></textarea></label>
|
|
424
435
|
${["portrait", "banner"].map((slot) => html`<div key=${slot} class="form">
|
|
425
|
-
<label class="field"><span>${tn(slot === "portrait" ? "cardPortrait" : "cardBanner")}</span><input ref=${pics[slot]} type="file" accept="image/png,image/jpeg,image/webp"
|
|
436
|
+
<label class="field"><span>${tn(slot === "portrait" ? "cardPortrait" : "cardBanner")}</span><input ref=${pics[slot]} type="file" accept="image/png,image/jpeg,image/webp"
|
|
437
|
+
onChange=${slot === "banner" ? (e) => { const fl = e.currentTarget.files && e.currentTarget.files[0]; if (fl) preview.fromFile(fl); } : undefined} /></label>
|
|
426
438
|
${card && card.mine && card.mine[slot] ? html`<div class="field-row"><button type="button" class="btn btn-sm" aria-pressed=${removeCard[slot] ? "true" : "false"}
|
|
427
439
|
onClick=${() => setRemoveCard({ ...removeCard, [slot]: !removeCard[slot] })}>${tn(removeCard[slot] ? "cardKeep" : "cardRemove")}</button></div>` : null}
|
|
428
440
|
</div>`)}
|
|
441
|
+
${bannerPreview ? html`<div class="field bf-field">
|
|
442
|
+
<span>${tn("cardBannerFocus")}</span>
|
|
443
|
+
<${BannerFocusPreview} src=${bannerPreview.src} focus=${focus.value} />
|
|
444
|
+
<input type="range" min="0" max="100" step="1" value=${focus.value} aria-label=${tn("cardBannerFocus")}
|
|
445
|
+
onInput=${(e) => { const v = Number(e.currentTarget.value); setFocus({ ...focus, value: v, mine: v }); }} />
|
|
446
|
+
<span class="note">${tn("cardBannerFocusNote")}</span>
|
|
447
|
+
${focus.mine !== null ? html`<div class="field-row"><button type="button" class="btn btn-sm" onClick=${() => setFocus({ ...focus, value: focus.agent ?? 50, mine: null })}>${tn("cardFocusFollow")}</button></div>` : null}
|
|
448
|
+
</div>` : null}
|
|
429
449
|
</fieldset>
|
|
430
450
|
<div class="field-row"><button type="button" class="btn btn-sm" aria-pressed=${clearCard ? "true" : "false"} onClick=${() => setClearCard(!clearCard)}>${tn(clearCard ? "keepCard" : "clearCard")}</button>
|
|
431
451
|
${clearCard ? html`<span class="muted">${tn("clearCardNote")}</span>` : null}</div>
|
|
@@ -51,6 +51,14 @@ register("view", {
|
|
|
51
51
|
"tooltipEffort": "Effort: {value}",
|
|
52
52
|
"tooltipCost": "Cost: {value}",
|
|
53
53
|
"tooltipUnavailable": "Unavailable",
|
|
54
|
+
"srcConfigured": " (configured)",
|
|
55
|
+
"srcFleetDefault": " (fleet default)",
|
|
56
|
+
"modelDefault": "default",
|
|
57
|
+
"modelDefaultWhy": "default ({reason})",
|
|
58
|
+
"modelCliDefault": "{model} (default)",
|
|
59
|
+
"modelWhyStart": "detected when it starts",
|
|
60
|
+
"modelWhyCli": "this CLI does not report a default",
|
|
61
|
+
"groupOther": "Other",
|
|
54
62
|
"statusRunning": "Running",
|
|
55
63
|
"statusPaused": "Paused",
|
|
56
64
|
"statusStopped": "Stopped",
|
|
@@ -162,6 +170,9 @@ register("view", {
|
|
|
162
170
|
"cardBio": "Bio (up to 280 characters, 3 line breaks)",
|
|
163
171
|
"cardPortrait": "Portrait",
|
|
164
172
|
"cardBanner": "Banner",
|
|
173
|
+
"cardBannerFocus": "Banner focus",
|
|
174
|
+
"cardBannerFocusNote": "What stays in view when the banner is cropped: the dashed area shows on every screen and card size; the yellow one is a phone's medium card.",
|
|
175
|
+
"cardFocusFollow": "Follow the agent's",
|
|
165
176
|
"cardRemove": "Remove yours",
|
|
166
177
|
"cardKeep": "Keep yours",
|
|
167
178
|
"clearCard": "Clear card",
|
|
@@ -248,6 +259,14 @@ register("view", {
|
|
|
248
259
|
"tooltipEffort": "推理強度:{value}",
|
|
249
260
|
"tooltipCost": "費用:{value}",
|
|
250
261
|
"tooltipUnavailable": "無資料",
|
|
262
|
+
"srcConfigured": "(設定值)",
|
|
263
|
+
"srcFleetDefault": "(fleet 預設)",
|
|
264
|
+
"modelDefault": "預設",
|
|
265
|
+
"modelDefaultWhy": "預設({reason})",
|
|
266
|
+
"modelCliDefault": "{model}(預設)",
|
|
267
|
+
"modelWhyStart": "啟動時偵測",
|
|
268
|
+
"modelWhyCli": "這個 CLI 不提供預設值",
|
|
269
|
+
"groupOther": "其他",
|
|
251
270
|
"statusRunning": "運行中",
|
|
252
271
|
"statusPaused": "已暫停",
|
|
253
272
|
"statusStopped": "已停止",
|
|
@@ -359,6 +378,9 @@ register("view", {
|
|
|
359
378
|
"cardBio": "簡介(最多 280 個字、3 次換行)",
|
|
360
379
|
"cardPortrait": "肖像",
|
|
361
380
|
"cardBanner": "橫幅",
|
|
381
|
+
"cardBannerFocus": "橫幅焦點",
|
|
382
|
+
"cardBannerFocusNote": "橫幅被裁切時保留在畫面中的部分:虛線框在所有螢幕與卡片大小都看得到;黃色框是手機上中卡片的範圍。",
|
|
383
|
+
"cardFocusFollow": "跟隨 agent 的設定",
|
|
362
384
|
"cardRemove": "移除你的",
|
|
363
385
|
"cardKeep": "保留你的",
|
|
364
386
|
"clearCard": "清除卡片",
|
package/dist/view-api.d.ts
CHANGED
|
@@ -84,6 +84,38 @@ export declare function resolveInstanceIdentity(input: {
|
|
|
84
84
|
}): InstanceIdentity;
|
|
85
85
|
/** Profiles by instance, for a status read: only when profiles.db already exists — a status read never creates it. */
|
|
86
86
|
export declare function profileIdentities(dataDir: string): Map<string, Pick<ProfileRow, "display_name" | "description" | "role" | "avatar_path">>;
|
|
87
|
+
/** #2056: one instance's person-picked colour and places, as the Colour dialog (GET /api/profile) reads them. */
|
|
88
|
+
export declare function personIdentityColour(dataDir: string, name: string): {
|
|
89
|
+
color: string | null;
|
|
90
|
+
show: string[] | null;
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* #2056: a person's colour for one instance — exactly POST /api/profile's colour fields: the store's own check (in set(),
|
|
94
|
+
* before anything is written), the table made first, the write in a profiles.db transaction. The caller announces
|
|
95
|
+
* (identityChanged).
|
|
96
|
+
*/
|
|
97
|
+
export declare function setPersonIdentityColour(dataDir: string, name: string, identity: {
|
|
98
|
+
color?: unknown;
|
|
99
|
+
show?: unknown;
|
|
100
|
+
}): {
|
|
101
|
+
ok: true;
|
|
102
|
+
} | {
|
|
103
|
+
ok: false;
|
|
104
|
+
status: number;
|
|
105
|
+
error: string;
|
|
106
|
+
detail?: string;
|
|
107
|
+
};
|
|
108
|
+
/** #2086: the banner focus a person set for one instance (null: none; the agent's, else the centre, is drawn). */
|
|
109
|
+
export declare function personBannerFocus(dataDir: string, name: string): number | null;
|
|
110
|
+
/** #2086: a person's banner focus — exactly POST /api/profile's card_banner_focus (the store's check; its own write). */
|
|
111
|
+
export declare function setPersonBannerFocus(dataDir: string, name: string, focus: unknown): {
|
|
112
|
+
ok: true;
|
|
113
|
+
} | {
|
|
114
|
+
ok: false;
|
|
115
|
+
status: number;
|
|
116
|
+
error: string;
|
|
117
|
+
detail?: string;
|
|
118
|
+
};
|
|
87
119
|
/** Every person-picked colour, for a status read (never creates profiles.db or its table). */
|
|
88
120
|
export declare function identityColors(dataDir: string): ReturnType<typeof IdentityColorStore.all>;
|
|
89
121
|
/** The card picture drawn for `slot`: the person's, else the agent's (the same rule as the card's JSON). */
|