@songsid/agend 2.2.0 → 2.3.0-beta.1
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-cli.js +248 -2
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.d.ts +2 -0
- package/dist/agent-endpoint.js +25 -0
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-file-read.d.ts +30 -0
- package/dist/agent-file-read.js +129 -0
- package/dist/agent-file-read.js.map +1 -0
- package/dist/agent-whats-new.js +13 -0
- package/dist/agent-whats-new.js.map +1 -1
- package/dist/atomic-write.js +9 -1
- package/dist/atomic-write.js.map +1 -1
- package/dist/auth-api.js +17 -0
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/antigravity.js +3 -3
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/launch-effort.js +1 -1
- package/dist/backend/launch-effort.js.map +1 -1
- package/dist/backend/muse.js +10 -2
- package/dist/backend/muse.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +2 -0
- package/dist/channel/adapters/discord.js +17 -2
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +8 -1
- package/dist/channel/adapters/telegram.js +74 -4
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/mcp-tools.js +34 -0
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/types.d.ts +15 -0
- package/dist/config-validator.js +17 -1
- package/dist/config-validator.js.map +1 -1
- package/dist/daemon.js +244 -20
- package/dist/daemon.js.map +1 -1
- package/dist/fleet-context.d.ts +5 -0
- package/dist/fleet-manager.d.ts +89 -0
- package/dist/fleet-manager.js +401 -25
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +258 -0
- package/dist/instance-avatar.js +3 -0
- package/dist/instance-avatar.js.map +1 -1
- package/dist/instance-config-impact.js +3 -0
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.js +9 -4
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/needs-you-hub.d.ts +4 -0
- package/dist/needs-you-hub.js +31 -6
- package/dist/needs-you-hub.js.map +1 -1
- package/dist/needs-you.d.ts +1 -13
- package/dist/needs-you.js +3 -37
- package/dist/needs-you.js.map +1 -1
- package/dist/outbound-handlers.d.ts +5 -0
- package/dist/outbound-handlers.js +30 -9
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +64 -0
- package/dist/outbound-schemas.js +50 -0
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/public-web-gateway.js +14 -0
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/repo-worktree-guard.d.ts +36 -0
- package/dist/repo-worktree-guard.js +143 -0
- package/dist/repo-worktree-guard.js.map +1 -0
- package/dist/scheduler/scheduler.d.ts +58 -1
- package/dist/scheduler/scheduler.js +176 -10
- package/dist/scheduler/scheduler.js.map +1 -1
- package/dist/settings-api.js +74 -1
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +8 -0
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +16 -2
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-http-confirmation.js +5 -0
- package/dist/settings-http-confirmation.js.map +1 -1
- package/dist/tool-permissions.js +50 -0
- package/dist/tool-permissions.js.map +1 -1
- package/dist/topic-commands.d.ts +10 -6
- package/dist/topic-commands.js +21 -17
- package/dist/topic-commands.js.map +1 -1
- package/dist/topic-link.d.ts +128 -0
- package/dist/topic-link.js +269 -0
- package/dist/topic-link.js.map +1 -0
- package/dist/types.d.ts +13 -0
- package/dist/ui/app-effects.js +187 -0
- package/dist/ui/chat-effect-posters.js +42 -0
- package/dist/ui/chat-effects.js +72 -0
- package/dist/ui/chat-live-replies.js +58 -0
- package/dist/ui/chat-render.js +4 -4
- package/dist/ui/chat-store.js +9 -3
- package/dist/ui/chat-strings.js +15 -0
- package/dist/ui/chat-thread.js +47 -21
- package/dist/ui/chat-tour.js +1 -1
- package/dist/ui/effect-trial-controller.js +119 -0
- package/dist/ui/effect-trial-offer.js +31 -0
- package/dist/ui/effect-trial-panel.js +37 -0
- package/dist/ui/image-lightbox.js +1 -1
- package/dist/ui/panel-chat.js +85 -17
- package/dist/ui/panel-details.js +18 -5
- package/dist/ui/panel-fleet.js +3 -3
- package/dist/ui/panel-log.js +6 -6
- package/dist/ui/panel-needs.js +3 -3
- package/dist/ui/panel-settings.js +8 -5
- package/dist/ui/preview-panel.js +25 -9
- package/dist/ui/settings-appearance.js +325 -0
- package/dist/ui/settings-effects.js +201 -0
- package/dist/ui/settings-mods.js +257 -0
- package/dist/ui/settings-owner-effects.js +80 -0
- package/dist/ui/settings-strings.js +245 -0
- package/dist/ui/shared/agend-auth.js +27 -3
- package/dist/ui/shared/agent-widget.js +248 -0
- package/dist/ui/shared/app-i18n.js +2 -0
- package/dist/ui/shared/app-prefs.js +30 -10
- package/dist/ui/shared/app-route.js +23 -1
- package/dist/ui/shared/app-session.js +1 -1
- package/dist/ui/shared/app-shell.js +29 -16
- package/dist/ui/shared/app-store.js +2 -0
- package/dist/ui/shared/app-stream.js +12 -7
- package/dist/ui/shared/app-themes.js +188 -0
- package/dist/ui/shared/app.css +214 -1
- package/dist/ui/shared/app.js +6 -0
- package/dist/ui/shared/card-strings.js +28 -0
- package/dist/ui/shared/effect-app-runtime.js +248 -0
- package/dist/ui/shared/effect-baseline.js +64 -0
- package/dist/ui/shared/effect-chat-runtime.js +154 -0
- package/dist/ui/shared/effect-cursor.js +125 -0
- package/dist/ui/shared/effect-frame.js +127 -0
- package/dist/ui/shared/effect-geometry.js +122 -0
- package/dist/ui/shared/effect-host-style.js +10 -0
- package/dist/ui/shared/effect-owner-regions.js +74 -0
- package/dist/ui/shared/effect-owner-source.js +115 -0
- package/dist/ui/shared/effect-policy.js +137 -0
- package/dist/ui/shared/effect-poster-reader.js +98 -0
- package/dist/ui/shared/effect-posters.js +117 -0
- package/dist/ui/shared/effect-profiler.js +162 -0
- package/dist/ui/shared/effect-profiles.js +65 -0
- package/dist/ui/shared/effect-registry.js +496 -0
- package/dist/ui/shared/effect-runtime-loader.js +234 -0
- package/dist/ui/shared/effect-safety.js +153 -0
- package/dist/ui/shared/effect-stock-snapshot.js +34 -0
- package/dist/ui/shared/effect-stock-trust.js +165 -0
- package/dist/ui/shared/effect-trial-source.js +103 -0
- package/dist/ui/shared/effect-trial-stage.js +147 -0
- package/dist/ui/shared/header-tools.js +1 -1
- package/dist/ui/shared/identity-card.js +202 -0
- package/dist/ui/shared/identity-color.js +261 -0
- package/dist/ui/shared/instance-nav.js +22 -14
- package/dist/ui/shared/owner-css.js +221 -0
- package/dist/ui/shared/owner-effect-prefs.js +117 -0
- package/dist/ui/shared/panel-view.js +81 -8
- package/dist/ui/shared/signin.js +2 -0
- package/dist/ui/shared/ui-dialog.js +1 -1
- package/dist/ui/shared/ui-icons.js +2 -0
- package/dist/ui/shared/ui-menu.js +1 -1
- package/dist/ui/shared/ui-toast.js +1 -1
- package/dist/ui/shared/view-strings.js +30 -0
- package/dist/ui/signin.html +1 -0
- package/dist/view-api.d.ts +29 -0
- package/dist/view-api.js +188 -8
- package/dist/view-api.js.map +1 -1
- package/dist/web-api.d.ts +42 -0
- package/dist/web-api.js +896 -36
- package/dist/web-api.js.map +1 -1
- package/dist/web-appearance-limits.d.ts +82 -0
- package/dist/web-appearance-limits.js +83 -0
- package/dist/web-appearance-limits.js.map +1 -0
- package/dist/web-appearance-policy.d.ts +66 -0
- package/dist/web-appearance-policy.js +94 -0
- package/dist/web-appearance-policy.js.map +1 -0
- package/dist/web-appearance-tools.d.ts +50 -0
- package/dist/web-appearance-tools.js +105 -0
- package/dist/web-appearance-tools.js.map +1 -0
- package/dist/web-appearance-view.d.ts +99 -0
- package/dist/web-appearance-view.js +163 -0
- package/dist/web-appearance-view.js.map +1 -0
- package/dist/web-appearance.d.ts +79 -0
- package/dist/web-appearance.js +298 -0
- package/dist/web-appearance.js.map +1 -0
- package/dist/web-asset-parts.d.ts +98 -0
- package/dist/web-asset-parts.js +133 -0
- package/dist/web-asset-parts.js.map +1 -0
- package/dist/web-auth.js +17 -1
- package/dist/web-auth.js.map +1 -1
- package/dist/web-card.d.ts +96 -0
- package/dist/web-card.js +232 -0
- package/dist/web-card.js.map +1 -0
- package/dist/web-chat-history.d.ts +13 -0
- package/dist/web-chat-history.js +23 -2
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-effect-bundle.d.ts +28 -0
- package/dist/web-effect-bundle.js +126 -0
- package/dist/web-effect-bundle.js.map +1 -0
- package/dist/web-effect-canonical.d.ts +8 -0
- package/dist/web-effect-canonical.js +27 -0
- package/dist/web-effect-canonical.js.map +1 -0
- package/dist/web-effect-library.d.ts +114 -0
- package/dist/web-effect-library.js +530 -0
- package/dist/web-effect-library.js.map +1 -0
- package/dist/web-effect-poster.d.ts +8 -0
- package/dist/web-effect-poster.js +52 -0
- package/dist/web-effect-poster.js.map +1 -0
- package/dist/web-effect-profiles.d.ts +118 -0
- package/dist/web-effect-profiles.js +180 -0
- package/dist/web-effect-profiles.js.map +1 -0
- package/dist/web-effect-service.d.ts +86 -0
- package/dist/web-effect-service.js +312 -0
- package/dist/web-effect-service.js.map +1 -0
- package/dist/web-effect-tools.d.ts +47 -0
- package/dist/web-effect-tools.js +126 -0
- package/dist/web-effect-tools.js.map +1 -0
- package/dist/web-effect-trials.d.ts +86 -0
- package/dist/web-effect-trials.js +204 -0
- package/dist/web-effect-trials.js.map +1 -0
- package/dist/web-effect-upload.d.ts +29 -0
- package/dist/web-effect-upload.js +190 -0
- package/dist/web-effect-upload.js.map +1 -0
- package/dist/web-effect-worker.d.ts +8 -0
- package/dist/web-effect-worker.js +170 -0
- package/dist/web-effect-worker.js.map +1 -0
- package/dist/web-effect.d.ts +89 -0
- package/dist/web-effect.js +132 -0
- package/dist/web-effect.js.map +1 -0
- package/dist/web-identity-color.d.ts +87 -0
- package/dist/web-identity-color.js +209 -0
- package/dist/web-identity-color.js.map +1 -0
- package/dist/web-identity-palette.d.ts +113 -0
- package/dist/web-identity-palette.js +84 -0
- package/dist/web-identity-palette.js.map +1 -0
- package/dist/web-image-meta.d.ts +25 -0
- package/dist/web-image-meta.js +200 -0
- package/dist/web-image-meta.js.map +1 -0
- package/dist/web-mod-agent.d.ts +50 -0
- package/dist/web-mod-agent.js +203 -0
- package/dist/web-mod-agent.js.map +1 -0
- package/dist/web-mod-bundle.d.ts +144 -0
- package/dist/web-mod-bundle.js +464 -0
- package/dist/web-mod-bundle.js.map +1 -0
- package/dist/web-mod-export.d.ts +59 -0
- package/dist/web-mod-export.js +79 -0
- package/dist/web-mod-export.js.map +1 -0
- package/dist/web-mod-store.d.ts +283 -0
- package/dist/web-mod-store.js +624 -0
- package/dist/web-mod-store.js.map +1 -0
- package/dist/web-org.d.ts +3 -3
- package/dist/web-org.js +11 -17
- package/dist/web-org.js.map +1 -1
- package/dist/web-owner-css-store.d.ts +180 -0
- package/dist/web-owner-css-store.js +384 -0
- package/dist/web-owner-css-store.js.map +1 -0
- package/dist/web-owner-css.d.ts +58 -0
- package/dist/web-owner-css.js +552 -0
- package/dist/web-owner-css.js.map +1 -0
- package/dist/web-prefs.d.ts +48 -0
- package/dist/web-prefs.js +209 -33
- package/dist/web-prefs.js.map +1 -1
- package/dist/web-preview.d.ts +9 -1
- package/dist/web-preview.js +24 -7
- package/dist/web-preview.js.map +1 -1
- package/dist/web-settings-patch.d.ts +18 -0
- package/dist/web-settings-patch.js +51 -0
- package/dist/web-settings-patch.js.map +1 -1
- package/dist/web-shell-routes.d.ts +1 -1
- package/dist/web-shell-routes.js +1 -1
- package/dist/web-shell-routes.js.map +1 -1
- package/dist/web-theme-store.d.ts +218 -0
- package/dist/web-theme-store.js +516 -0
- package/dist/web-theme-store.js.map +1 -0
- package/dist/web-theme.d.ts +121 -0
- package/dist/web-theme.js +407 -0
- package/dist/web-theme.js.map +1 -0
- package/dist/web-widget.d.ts +72 -0
- package/dist/web-widget.js +245 -0
- package/dist/web-widget.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** #1629: static Chat fallback. It requires no preview origin, device consent, Worker or personal identity. */
|
|
2
|
+
import { useLayoutEffect } from "/assets/app-html.js";
|
|
3
|
+
import { appStore } from "/assets/app-store.js";
|
|
4
|
+
import { onPrefsChanged, prefInForce } from "/assets/app-prefs.js";
|
|
5
|
+
import { themesShown, themesStore } from "/assets/app-themes.js";
|
|
6
|
+
import { ownerCssLifecycle } from "/assets/owner-css.js";
|
|
7
|
+
import { chatEffectIdentity } from "/assets/effect-chat-runtime.js";
|
|
8
|
+
import { createChatPosters } from "/assets/effect-posters.js";
|
|
9
|
+
|
|
10
|
+
export function ChatEffectPosters({ name, lease, element }) {
|
|
11
|
+
useLayoutEffect(() => {
|
|
12
|
+
let live = true, api = null;
|
|
13
|
+
const releases = [], host = window;
|
|
14
|
+
const current = () => live && lease.current();
|
|
15
|
+
const cleanup = () => { if (!live) return; live = false; try { api?.dispose?.(); } catch { /* optional */ } for (const off of releases) { try { off?.(); } catch { /* release all */ } } };
|
|
16
|
+
lease.hold(cleanup);
|
|
17
|
+
try {
|
|
18
|
+
const preview = host.AgendPreview, motion = host.matchMedia?.("(prefers-reduced-motion: reduce)") ?? { matches: true };
|
|
19
|
+
const observe = () => {
|
|
20
|
+
let previewReady = false; try { previewReady = preview?.availability().ok === true; } catch { /* fallback */ }
|
|
21
|
+
// This is a surface fallback, not a replacement for an explicit Pause, queued effect or failed live read.
|
|
22
|
+
const playback = chatEffectIdentity(host) && typeof host.Worker === "function" && typeof host.OffscreenCanvas === "function" && previewReady &&
|
|
23
|
+
prefInForce("preview.optIn")?.value === true && preview?.optedIn() === true && preview?.never() !== true && !motion.matches && host.navigator?.connection?.saveData !== true;
|
|
24
|
+
return { shown: themesShown(), fallback: !playback, ownerCss: ownerCssLifecycle.get().state };
|
|
25
|
+
};
|
|
26
|
+
api = createChatPosters({ window: host, name, element: element.current, current, observe });
|
|
27
|
+
if (!api.ok) return cleanup;
|
|
28
|
+
const sync = () => { if (current()) api.sync(); };
|
|
29
|
+
let revision = appStore.get().themesRev;
|
|
30
|
+
releases.push(appStore.subscribe(s => {
|
|
31
|
+
if (!current()) return;
|
|
32
|
+
if (s.connection === "ended") { api.dispose(); return; }
|
|
33
|
+
if (s.themesRev !== revision) { revision = s.themesRev; api.changed(); } else sync();
|
|
34
|
+
}));
|
|
35
|
+
releases.push(ownerCssLifecycle.subscribe(sync)); releases.push(themesStore.subscribe(sync)); releases.push(onPrefsChanged(sync));
|
|
36
|
+
if (typeof preview?.onChange === "function") releases.push(preview.onChange(sync));
|
|
37
|
+
for (const target of [motion, host.navigator?.connection]) if (target?.addEventListener) { target.addEventListener("change", sync); releases.push(() => target.removeEventListener("change", sync)); }
|
|
38
|
+
sync(); return cleanup;
|
|
39
|
+
} catch { cleanup(); return cleanup; }
|
|
40
|
+
}, [name, lease]);
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// #1629: Chat contributes a committed lease to the one app owner; no private pool or server polling.
|
|
2
|
+
import { html, useLayoutEffect, useRef, useState } from "/assets/app-html.js";
|
|
3
|
+
import { register, t } from "/assets/app-i18n.js";
|
|
4
|
+
import { appStore, useStore } from "/assets/app-store.js";
|
|
5
|
+
import { ownerCssLifecycle } from "/assets/owner-css.js";
|
|
6
|
+
import { chatEffectState, chatEffectIdentity } from "/assets/effect-chat-runtime.js";
|
|
7
|
+
import { Dialog } from "/assets/ui-dialog.js";
|
|
8
|
+
import { navigate } from "/assets/app-nav.js";
|
|
9
|
+
import { toast } from "/assets/ui-toast.js";
|
|
10
|
+
import { getAppEffects } from "./app-effects.js";
|
|
11
|
+
|
|
12
|
+
register("chatFx", {
|
|
13
|
+
en: { title: "Effects", loading: "Checking effects…", empty: "No effects in this chat.", failed: "Effects stopped because their resources could not be verified. Reload the page to try again.", reload: "Reload page", pause: "Pause", resume: "Resume", remove: "Remove from chat", removeFailed: "Could not remove this effect. Reload the page to check its state.", css: "Agent foreground effects are paused while your custom CSS is on or cannot be verified.", setting: "Open Appearance settings", running: "Running", starting: "Starting", off: "Stopped", resumeFailed: "This effect cannot resume: the page or its safety history is not ready. Reload the page or choose another effect.", queued: "Waiting for a slot", unknown: "Playback is off or unavailable here.", budget: "Paused to protect page responsiveness. Resume keeps the three-pauses safety limit.", paused: "Paused", low: "Reduced quality" },
|
|
14
|
+
"zh-TW": { title: "效果", loading: "正在確認效果…", empty: "這個聊天沒有放置效果。", failed: "無法確認效果資源,已停止播放。重新載入頁面後再試。", reload: "重新載入頁面", pause: "暫停", resume: "恢復", remove: "從聊天移除", removeFailed: "無法移除效果。請重新載入頁面確認狀態。", css: "自訂 CSS 開啟或無法確認時,代理的前景效果會暫停。", setting: "開啟外觀設定", running: "播放中", starting: "啟動中", off: "已停止", resumeFailed: "頁面或安全紀錄尚未就緒,無法恢復此效果。請重新載入頁面,或選擇其他效果。", queued: "等待空位", unknown: "這裡已關閉播放,或無法播放效果。", budget: "為維持頁面順暢,已暫停效果。恢復仍保留三次暫停的安全上限。", paused: "已暫停", low: "降低畫質" },
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
/** Local read/admin sessions keep controls; public, View, poll and header-only pages have no runtime identity. */
|
|
18
|
+
export function chatEffectsMenuAvailable() { return chatEffectIdentity(window) !== null; }
|
|
19
|
+
|
|
20
|
+
export function ChatEffects({ name, lease, element, store, open, onClose }) {
|
|
21
|
+
const cssLife = useStore(ownerCssLifecycle);
|
|
22
|
+
const runtime = useRef(null), [state, setState] = useState(null), [canWrite, setCanWrite] = useState(false), [busy, setBusy] = useState(false);
|
|
23
|
+
useLayoutEffect(() => {
|
|
24
|
+
let live = true, api = null;
|
|
25
|
+
const releases = [];
|
|
26
|
+
// Effects are optional: a failed setup must release its partial ownership without breaking Chat.
|
|
27
|
+
const cleanup = () => {
|
|
28
|
+
if (!live) return;
|
|
29
|
+
live = false;
|
|
30
|
+
try { api?.dispose?.(); } catch { /* continue releasing partial setup */ }
|
|
31
|
+
for (const off of releases) { try { if (typeof off === "function") off(); } catch { /* release the rest */ } }
|
|
32
|
+
if (runtime.current === api) runtime.current = null;
|
|
33
|
+
};
|
|
34
|
+
lease.hold(cleanup);
|
|
35
|
+
try {
|
|
36
|
+
setBusy(false); onClose(); setCanWrite(false); setState(null);
|
|
37
|
+
const host = window, owner = getAppEffects(host);
|
|
38
|
+
const current = () => live && lease.current();
|
|
39
|
+
if (!owner) { setState({ error: null, rows: [], effects: [], unavailable: true }); return cleanup; }
|
|
40
|
+
releases.push(owner.onEnd(() => { if (current()) setState({ error: "resource_unavailable", rows: [], effects: [], unavailable: true }); }));
|
|
41
|
+
const changed = value => {
|
|
42
|
+
if (!current()) return;
|
|
43
|
+
const g = owner.gates();
|
|
44
|
+
setState({ ...value, unavailable: !g.enabled || !g.show || !g.previewReady || !g.optIn || g.safe || g.publicLink || g.reducedMotion || g.saveData });
|
|
45
|
+
};
|
|
46
|
+
api = owner.attachChat({ name, element: element.current, current,
|
|
47
|
+
observe: () => ({ state: chatEffectState(appStore.get(), name) }), onChange: changed });
|
|
48
|
+
runtime.current = api;
|
|
49
|
+
if (!api?.ok) { setState({ error: null, rows: [], effects: [], unavailable: true }); return cleanup; }
|
|
50
|
+
host.AgendAuth?.canWrite?.().then(value => { if (current()) setCanWrite(value === true); }).catch(() => {});
|
|
51
|
+
api.sync();
|
|
52
|
+
return cleanup;
|
|
53
|
+
} catch {
|
|
54
|
+
cleanup();
|
|
55
|
+
if (lease.current()) setState({ error: "runtime_unavailable", rows: [], effects: [] });
|
|
56
|
+
return cleanup;
|
|
57
|
+
}
|
|
58
|
+
}, [name, lease]);
|
|
59
|
+
const rows = state?.rows ?? [], effects = state?.effects ?? [];
|
|
60
|
+
const css = cssLife.state !== "off" && effects.some(p => p.by !== "human" && p.manifest.layer === "front");
|
|
61
|
+
const remove = async id => { if (busy) return; setBusy(true); const api = runtime.current; const ok = await api?.remove?.(id); if (!lease.current() || runtime.current !== api) return; setBusy(false); if (!ok) toast(t("chatFx.removeFailed"), false); };
|
|
62
|
+
return html`${open ? html`<${Dialog} title=${t("chatFx.title")} onClose=${onClose} busy=${busy}>
|
|
63
|
+
${state?.error ? html`<p>${t("chatFx.failed")}</p><button type="button" class="btn" onClick=${() => location.reload()}>${t("chatFx.reload")}</button>` : null}
|
|
64
|
+
${css ? html`<p>${t("chatFx.css")}</p><button type="button" class="btn" onClick=${() => { onClose(); navigate("/settings/appearance"); }}>${t("chatFx.setting")}</button>` : null}
|
|
65
|
+
${!effects.length && !state?.error ? html`<p>${t(state?.unavailable ? "chatFx.unknown" : state?.loading || !state ? "chatFx.loading" : "chatFx.empty")}</p>` : null}
|
|
66
|
+
${effects.map(p => { const row = rows.find(r => r.id === p.id), phase = row?.phase; return html`<section key=${p.id}><h3>${p.manifest.title}</h3><p>${t(row?.reason === "paused" || row?.reason === "host_budget" ? "chatFx.paused" : phase === "running" ? "chatFx.running" : phase ? "chatFx.starting" : row?.verdict === "queued" ? "chatFx.queued" : "chatFx.off")}${row?.quality === "low" ? ` · ${t("chatFx.low")}` : ""}</p>
|
|
67
|
+
${row?.reason === "host_budget" ? html`<p>${t("chatFx.budget")}</p>` : null}
|
|
68
|
+
<button type="button" class="btn" disabled=${busy} onClick=${() => runtime.current?.pause?.(p.id)}>${t("chatFx.pause")}</button>
|
|
69
|
+
<button type="button" class="btn" disabled=${busy} onClick=${() => { if (!runtime.current?.resume?.(p.id)) toast(t("chatFx.resumeFailed"), false); }}>${t("chatFx.resume")}</button>
|
|
70
|
+
${canWrite ? html`<button type="button" class="btn" disabled=${busy} onClick=${() => remove(p.id)}>${t("chatFx.remove")}</button>` : null}</section>`; })}
|
|
71
|
+
</${Dialog}>` : null}`;
|
|
72
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// #1629: transient host notifications, never message content or replay/history authority.
|
|
2
|
+
const own = (o, k) => Object.prototype.hasOwnProperty.call(o, k);
|
|
3
|
+
function fields(value, keys, closed = false) {
|
|
4
|
+
try {
|
|
5
|
+
if (!value || typeof value !== "object" || ![Object.prototype, null].includes(Object.getPrototypeOf(value))) return null;
|
|
6
|
+
if (closed && Reflect.ownKeys(value).length !== keys.length) return null;
|
|
7
|
+
const out = {};
|
|
8
|
+
for (const k of keys) { const d = Object.getOwnPropertyDescriptor(value, k); if (!d || !own(d, "value")) return null; out[k] = d.value; }
|
|
9
|
+
return out;
|
|
10
|
+
} catch { return null; }
|
|
11
|
+
}
|
|
12
|
+
const identity = x => !!x && typeof x.boot === "string" && /^[0-9a-f]{1,32}$/.test(x.boot)
|
|
13
|
+
&& Number.isSafeInteger(x.id) && x.id > 0 && typeof x.instance === "string" && x.instance.length >= 1 && x.instance.length <= 128;
|
|
14
|
+
const MAX_AGE = 5000, MAX_BOOTS = 64, MAX_PENDING = 64;
|
|
15
|
+
export function createLiveAgentReplies(now) {
|
|
16
|
+
const seen = new Map(), pending = new Map(), listeners = new Set();
|
|
17
|
+
let last = null, revoked = false;
|
|
18
|
+
function time() {
|
|
19
|
+
if (revoked) return null;
|
|
20
|
+
let t; try { t = now(); } catch { t = null; }
|
|
21
|
+
if (typeof t !== "number" || !Number.isFinite(t) || t < 0 || last !== null && t < last) { revoked = true; pending.clear(); return null; }
|
|
22
|
+
last = t;
|
|
23
|
+
for (const [key, item] of pending) if (t - item.at > MAX_AGE) pending.delete(key);
|
|
24
|
+
return t;
|
|
25
|
+
}
|
|
26
|
+
function ingress(extra, t) {
|
|
27
|
+
const e = fields(extra, ["source", "receivedAt"], true);
|
|
28
|
+
return e && e.source === "sse" && typeof e.receivedAt === "number" && Number.isFinite(e.receivedAt) && e.receivedAt >= 0 && e.receivedAt <= t && t - e.receivedAt <= MAX_AGE ? e.receivedAt : null;
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
// Record every accepted ingress, including history/poll, before its message-array merge/eviction.
|
|
32
|
+
message(value, extra) {
|
|
33
|
+
const m = fields(value, ["boot", "id", "instance"]); if (!identity(m)) return;
|
|
34
|
+
const role = fields(value, ["role"])?.role;
|
|
35
|
+
const t = time(); if (t === null) return;
|
|
36
|
+
const key = `${m.boot}:${m.id}`, previous = seen.get(m.boot) || 0;
|
|
37
|
+
pending.delete(key);
|
|
38
|
+
if (!seen.has(m.boot) && seen.size >= MAX_BOOTS) { revoked = true; pending.clear(); return; }
|
|
39
|
+
seen.set(m.boot, Math.max(previous, m.id));
|
|
40
|
+
const at = ingress(extra, t);
|
|
41
|
+
if (m.id <= previous || role !== "agent" || at === null) return;
|
|
42
|
+
// Reconnect replay has no receipts: discard old candidates, never the durable-in-page watermark.
|
|
43
|
+
if (pending.size >= MAX_PENDING) pending.delete(pending.keys().next().value);
|
|
44
|
+
pending.set(key, { instance: m.instance, at });
|
|
45
|
+
},
|
|
46
|
+
receipt(value, extra) {
|
|
47
|
+
const r = fields(value, ["v", "boot", "id", "instance"], true); if (!identity(r) || r.v !== 1) return false;
|
|
48
|
+
const t = time(); if (t === null) return false;
|
|
49
|
+
const at = ingress(extra, t); if (at === null) return false;
|
|
50
|
+
const key = `${r.boot}:${r.id}`, candidate = pending.get(key); pending.delete(key);
|
|
51
|
+
if (!candidate || candidate.instance !== r.instance || at < candidate.at || t - candidate.at > MAX_AGE) return false;
|
|
52
|
+
const event = Object.freeze({ v: 1, boot: r.boot, id: r.id, instance: r.instance });
|
|
53
|
+
for (const fn of [...listeners]) { try { fn(event); } catch { /* one host observer cannot suppress another */ } }
|
|
54
|
+
return true;
|
|
55
|
+
},
|
|
56
|
+
subscribe(fn) { if (typeof fn !== "function") return () => {}; listeners.add(fn); return () => listeners.delete(fn); },
|
|
57
|
+
};
|
|
58
|
+
}
|
package/dist/ui/chat-render.js
CHANGED
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
function link(href, label) {
|
|
39
|
-
return '<a href="' + href + '" target="_blank" rel="noopener noreferrer">' + label + "</a>";
|
|
39
|
+
return '<a href="' + href + '" target="_blank" rel="noopener noreferrer" data-trust="link">' + label + "</a>";
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
// Placeholders survive the later passes untouched (they contain no Markdown characters).
|
|
@@ -594,8 +594,8 @@
|
|
|
594
594
|
if (cards && isHtmlAttachment(a)) after.push('<div class="html-card" data-att="' + a.id + '"></div>');
|
|
595
595
|
var url = "/ui/file/" + a.id;
|
|
596
596
|
var kind = inline ? attachmentPreviewType(a) : null;
|
|
597
|
-
if (kind === "video") after.push('<div class="att-media"><video class="att-video" controls preload="metadata" src="' + url + '"></video></div>');
|
|
598
|
-
else if (kind === "audio") after.push('<div class="att-media"><audio class="att-audio" controls preload="metadata" src="' + url + '"></audio></div>');
|
|
597
|
+
if (kind === "video") after.push('<div data-trust="media" class="att-media"><video class="att-video" controls preload="metadata" src="' + url + '"></video></div>');
|
|
598
|
+
else if (kind === "audio") after.push('<div data-trust="media" class="att-media"><audio class="att-audio" controls preload="metadata" src="' + url + '"></audio></div>');
|
|
599
599
|
else if (kind) after.push('<div class="text-card" data-att="' + a.id + '" data-type="' + kind + '"></div>');
|
|
600
600
|
var name = escapeHtml(a.name || "file");
|
|
601
601
|
if (a.kind === "photo") {
|
|
@@ -604,7 +604,7 @@
|
|
|
604
604
|
out.push('<a class="att-file" href="' + url + '" download="' + name + '">📎 ' + name + ' <span class="att-size">' + escapeHtml(formatSize(a.size)) + "</span></a>");
|
|
605
605
|
}
|
|
606
606
|
});
|
|
607
|
-
return (out.length ? '<div class="atts">' + out.join("") + "</div>" : "") + after.join("");
|
|
607
|
+
return (out.length ? '<div data-trust="attachment" class="atts">' + out.join("") + "</div>" : "") + after.join("");
|
|
608
608
|
}
|
|
609
609
|
|
|
610
610
|
return {
|
package/dist/ui/chat-store.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// Requests and #1374: the history of an instance is read once per page, when a person first opens its chat (a
|
|
9
9
|
// navigation — it counts as use, as the old click did). Coming back to it reads nothing; the stream keeps it current.
|
|
10
10
|
import "./chat-render.js";
|
|
11
|
+
import { createLiveAgentReplies } from "./chat-live-replies.js";
|
|
11
12
|
|
|
12
13
|
const R = () => globalThis.AgendChatRender;
|
|
13
14
|
|
|
@@ -56,6 +57,7 @@ export function createChatStore(deps) {
|
|
|
56
57
|
const pastedTexts = new WeakMap();
|
|
57
58
|
const now = deps.now || (() => (typeof performance !== "undefined" ? performance.now() : Date.now()));
|
|
58
59
|
const t = deps.t, tf = deps.t;
|
|
60
|
+
const liveReplies = createLiveAgentReplies(now);
|
|
59
61
|
|
|
60
62
|
/** Tell subscribers what changed: `kind` "msgs" (the thread redraws) or "state" (everything else). */
|
|
61
63
|
function changed(instance, kind = "state") { for (const fn of [...subs]) fn(instance, kind); }
|
|
@@ -99,8 +101,9 @@ export function createChatStore(deps) {
|
|
|
99
101
|
function noteServerClock(c) {
|
|
100
102
|
if (c && typeof c.now === "number" && Number.isFinite(c.now)) s.clockSkew = c.now - Date.now();
|
|
101
103
|
}
|
|
102
|
-
function ingest(m) {
|
|
104
|
+
function ingest(m, extra) {
|
|
103
105
|
if (!m || typeof m.instance !== "string") return;
|
|
106
|
+
liveReplies.message(m, extra);
|
|
104
107
|
const before = s.msgs[m.instance];
|
|
105
108
|
s.msgs[m.instance] = R().mergeMessages(before, [m], 500);
|
|
106
109
|
if (m.instance === s.current && s.msgs[m.instance] !== before && m.role !== "status" && !isUser(m, m.instance) && (s.msgs[m.instance] || []).length > (before || []).length) deps.announce(tf("chat.a11yReplied", m.sender));
|
|
@@ -147,6 +150,7 @@ export function createChatStore(deps) {
|
|
|
147
150
|
const r = await api("GET", `/ui/history?instance=${encodeURIComponent(name)}&limit=200`);
|
|
148
151
|
if (!r || !Array.isArray(r.messages)) { s.historyRead.delete(name); return true; }
|
|
149
152
|
// Kept even when the person has moved on: the store is page-lived, and the answer belongs to `name`.
|
|
153
|
+
for (const m of r.messages) liveReplies.message(m);
|
|
150
154
|
s.msgs[name] = R().mergeMessages(s.msgs[name], r.messages, 500);
|
|
151
155
|
changed(name, "msgs");
|
|
152
156
|
} catch { s.historyRead.delete(name); /* the live stream still works; the next visit tries again */ }
|
|
@@ -162,7 +166,7 @@ export function createChatStore(deps) {
|
|
|
162
166
|
// A prompt already held (being answered, or claimed from Needs you) keeps its busy/answered state: a repeat of it
|
|
163
167
|
// only refreshes its text and buttons (#1463 review).
|
|
164
168
|
if (!had) s.prompts[p.nonce] = { ...p, resolved: false, busy: false };
|
|
165
|
-
else if (!had.resolved) Object.assign(had, { text: p.text, actions: p.actions, expiresAt: p.expiresAt });
|
|
169
|
+
else if (!had.resolved) Object.assign(had, { text: p.text, links: p.links, actions: p.actions, expiresAt: p.expiresAt });
|
|
166
170
|
if (p.instance === s.current) changed(p.instance);
|
|
167
171
|
else if (fresh) deps.toast(tf("chat.promptFor", p.instance, p.text), false);
|
|
168
172
|
}
|
|
@@ -174,7 +178,7 @@ export function createChatStore(deps) {
|
|
|
174
178
|
for (const p of list) {
|
|
175
179
|
const had = s.prompts[p.nonce];
|
|
176
180
|
if (!had) s.prompts[p.nonce] = { ...p, resolved: false, busy: false };
|
|
177
|
-
else if (!had.resolved) Object.assign(had, { text: p.text, actions: p.actions, expiresAt: p.expiresAt });
|
|
181
|
+
else if (!had.resolved) Object.assign(had, { text: p.text, links: p.links, actions: p.actions, expiresAt: p.expiresAt });
|
|
178
182
|
}
|
|
179
183
|
for (const n of Object.keys(s.prompts)) if (!s.prompts[n].resolved && !open.has(n)) s.prompts[n].resolved = true;
|
|
180
184
|
changed(null);
|
|
@@ -380,6 +384,7 @@ export function createChatStore(deps) {
|
|
|
380
384
|
stream.on("status", applyStatus);
|
|
381
385
|
stream.on("activity", applyActivity);
|
|
382
386
|
stream.on("message", ingest);
|
|
387
|
+
stream.on("agent_reply_live", liveReplies.receipt);
|
|
383
388
|
stream.on("delivery", (d) => applyDeliveries([d]));
|
|
384
389
|
stream.on("deliveries", applyDeliveries);
|
|
385
390
|
stream.on("prompt", onPrompt);
|
|
@@ -393,6 +398,7 @@ export function createChatStore(deps) {
|
|
|
393
398
|
state: s,
|
|
394
399
|
subscribe(fn) { subs.add(fn); return () => subs.delete(fn); },
|
|
395
400
|
setCurrent(name) { s.current = name; },
|
|
401
|
+
onAgentReply: liveReplies.subscribe,
|
|
396
402
|
attach, applyStatus, applyActivity, ingest, applyDeliveries, openHistory,
|
|
397
403
|
onPrompt, applyPrompts, resolvePrompt, answerPrompt, answerByNonce, holdPrompt, releasePrompt, promptsFor,
|
|
398
404
|
applyReplyButtons, clickReplyButton,
|
package/dist/ui/chat-strings.js
CHANGED
|
@@ -3,6 +3,8 @@ import { register } from "/assets/app-i18n.js";
|
|
|
3
3
|
|
|
4
4
|
register("chat", {
|
|
5
5
|
en: {
|
|
6
|
+
effectTrial: "Effect trial", effectTrialLocal: "Try requires a local admin, enabled previews and custom CSS off.",
|
|
7
|
+
effectTrialStart: "Try", effectTrialExpired: "This trial offer has expired.", effectTrialUnavailable: "Trial unavailable: access or playback settings may have changed.",
|
|
6
8
|
cmdConfirm: "Clear", cmdConfirmBackend: "Switch and restart", cmdRunning: "Running /{0}…", cmdNotCommand: "Not a command here — Enter sends it as a message.", cmdDismiss: "Dismiss", cmdCommands: "Commands",
|
|
7
9
|
compactRunning: "Compacting…", compactDone: "✅ Conversation compacted", compactUnknown: "This backend does not report when compaction ends.",
|
|
8
10
|
compactTimeout: "No sign that compaction finished within {0} minutes — check the agent's own chat.",
|
|
@@ -90,9 +92,17 @@ register("chat", {
|
|
|
90
92
|
dBindingNone: "not bound (web only)", dPrimary: "the first connection", dTags: "Tags", dReadOnly: "Read only — change it in Settings.",
|
|
91
93
|
dEditInSettings: "Edit in Settings", dPause: "Pause", dWake: "Wake", dPaused: "{0} paused", dWoken: "{0} is waking up", dActionFailed: "That did not work — try again.",
|
|
92
94
|
dNoActivity: "No activity in the last hour.", dWorking: "Working…",
|
|
95
|
+
// #1629: a person's control over an agent's own web theme.
|
|
96
|
+
themeLock: "Lock theme", themeUnlock: "Unlock theme", themeReset: "Reset theme…", themeResetGo: "Reset",
|
|
97
|
+
themeResetConfirm: "Reset {0}'s theme to the default? Its colours, picture and working label go; a lock stays.",
|
|
98
|
+
themeResetDone: "{0}'s theme was reset.", themeLocked: "{0}'s theme is locked: the agent can't change it.", themeUnlocked: "{0}'s theme is unlocked.",
|
|
99
|
+
themeFailed: "The theme was not changed ({0}).",
|
|
100
|
+
widgetRemoveHeader: "Remove the chat header widget", widgetRemoveDetails: "Remove the Details widget", widgetRemoved: "{0}'s widget was removed.",
|
|
93
101
|
configured: "(configured)", fleetDefault: "(fleet default)",
|
|
94
102
|
},
|
|
95
103
|
"zh-TW": {
|
|
104
|
+
effectTrial: "效果試播", effectTrialLocal: "需由本機管理員啟用預覽並關閉自訂 CSS,才能試播。",
|
|
105
|
+
effectTrialStart: "試播", effectTrialExpired: "試播邀請已過期。", effectTrialUnavailable: "無法試播:存取權或播放設定可能已變更。",
|
|
96
106
|
cmdConfirm: "清空", cmdConfirmBackend: "切換並重新啟動", cmdRunning: "執行 /{0} 中…", cmdNotCommand: "這裡沒有這個指令——按 Enter 會當成一般訊息送出。", cmdDismiss: "關閉", cmdCommands: "指令",
|
|
97
107
|
compactRunning: "壓縮中…", compactDone: "✅ 對話已壓縮", compactUnknown: "這個後端不會回報壓縮何時完成。",
|
|
98
108
|
compactTimeout: "{0} 分鐘內沒有看到壓縮完成的紀錄,請到代理自己的對話確認。",
|
|
@@ -178,6 +188,11 @@ register("chat", {
|
|
|
178
188
|
dBindingNone: "未綁定(只用網頁)", dPrimary: "第一個連線", dTags: "標籤", dReadOnly: "這裡只能檢視——請到設定修改。",
|
|
179
189
|
dEditInSettings: "到設定編輯", dPause: "暫停", dWake: "喚醒", dPaused: "{0} 已暫停", dWoken: "{0} 正在喚醒", dActionFailed: "沒有成功,請再試一次。",
|
|
180
190
|
dNoActivity: "最近一小時沒有活動。", dWorking: "處理中…",
|
|
191
|
+
themeLock: "鎖定主題", themeUnlock: "解除主題鎖定", themeReset: "重設主題…", themeResetGo: "重設",
|
|
192
|
+
themeResetConfirm: "要把 {0} 的主題重設為預設嗎?顏色、圖片和處理中文字都會移除;鎖定會保留。",
|
|
193
|
+
themeResetDone: "已重設 {0} 的主題。", themeLocked: "已鎖定 {0} 的主題:agent 無法變更。", themeUnlocked: "已解除 {0} 的主題鎖定。",
|
|
194
|
+
themeFailed: "主題沒有變更({0})。",
|
|
195
|
+
widgetRemoveHeader: "移除聊天標頭的小工具", widgetRemoveDetails: "移除詳細資料頁的小工具", widgetRemoved: "已移除 {0} 的小工具。",
|
|
181
196
|
configured: "(設定值)", fleetDefault: "(fleet 預設)",
|
|
182
197
|
},
|
|
183
198
|
});
|
package/dist/ui/chat-thread.js
CHANGED
|
@@ -8,8 +8,10 @@
|
|
|
8
8
|
// Unrelated updates (a new message, another message's tick) stop nothing (#1306 §6.2, #1408 §4).
|
|
9
9
|
import "./chat-render.js";
|
|
10
10
|
import "./preview.js";
|
|
11
|
+
import { effectTrialMessage } from "./effect-trial-offer.js";
|
|
11
12
|
import { loadHtmlAttachment } from "./html-attachment.js";
|
|
12
13
|
import { chatTime, chatTimeFull, onDayChange } from "./chat-time.js";
|
|
14
|
+
import { textLinkSegments } from "/assets/app-route.js";
|
|
13
15
|
|
|
14
16
|
const R = () => globalThis.AgendChatRender;
|
|
15
17
|
/** #1589: the public link shows attachments as downloads only (its page says so: <body data-public-link="1">). */
|
|
@@ -68,7 +70,7 @@ export function createThread(list, scroller, opts) {
|
|
|
68
70
|
if (el.textContent !== v) el.textContent = v;
|
|
69
71
|
}
|
|
70
72
|
// #1770: "today" ends at the fleet's midnight; the labels are rewritten then (and on every render), while there are any.
|
|
71
|
-
let offDay = null;
|
|
73
|
+
let offDay = null, trialTimer = null;
|
|
72
74
|
function armDay() {
|
|
73
75
|
if (nodes.size && !offDay) offDay = onDayChange(() => { for (const v of nodes.values()) timeLabel(v.node); });
|
|
74
76
|
else if (!nodes.size && offDay) { offDay(); offDay = null; }
|
|
@@ -80,11 +82,13 @@ export function createThread(list, scroller, opts) {
|
|
|
80
82
|
// #1770: the label ("17:38", "Yesterday 17:38", "10/11 17:38") depends on today, so it is not part of the html a node
|
|
81
83
|
// is kept by: it is written into the node (timeLabels) — crossing midnight relabels in place and reloads no preview.
|
|
82
84
|
const time = x.ts ? `<span class="time" data-ts="${escAttr(x.ts)}" title="${escAttr(chatTimeFull(x.ts))}"></span>` : `<span class="time"></span>`;
|
|
85
|
+
const trial = effectTrialMessage(x);
|
|
86
|
+
if (trial) return trialHtml(trial, time, x);
|
|
83
87
|
if (x.role === "status") return statusHtml(x, time);
|
|
84
88
|
const u = opts.isUser(x);
|
|
85
89
|
// renderMarkdown escapes the whole text before it adds any tag of its own (chat-render.js).
|
|
86
90
|
const ticks = u && x.delivery ? R().deliveryHtml(x.delivery, TICKS()) : "";
|
|
87
|
-
const tools = u ? "" : `<div class="msg-tools"><button type="button" class="chip-btn icon-only" data-act="copyMsg" data-arg="${escAttr(msgKey(x))}" title="${escAttr(tr("chat.copyMessage"))}" aria-label="${escAttr(tr("chat.copyMessage"))}">${COPY}</button></div>`;
|
|
91
|
+
const tools = u ? "" : `<div data-trust="thread-control" class="msg-tools"><button type="button" class="chip-btn icon-only" data-act="copyMsg" data-arg="${escAttr(msgKey(x))}" title="${escAttr(tr("chat.copyMessage"))}" aria-label="${escAttr(tr("chat.copyMessage"))}">${COPY}</button></div>`;
|
|
88
92
|
const buttons = x.role === "agent" ? replyButtonsHtml(x.buttons) : "";
|
|
89
93
|
// A person's click on a reply button shows as the button's label, never the line the agent was given (its value).
|
|
90
94
|
const clicked = x.role === "user" && x.click && typeof x.click.label === "string";
|
|
@@ -97,10 +101,21 @@ export function createThread(list, scroller, opts) {
|
|
|
97
101
|
function clickHtml(click) {
|
|
98
102
|
const inner = `<span class="click-mark" aria-hidden="true">🔘</span><span class="sr-only">${escAttr(tr("chat.clicked"))} </span>${escAttr(click.label)}`;
|
|
99
103
|
const of = typeof click.of === "string" && /^[0-9a-f]{32}$/.test(click.of) ? click.of : null;
|
|
100
|
-
return `<div class="click-chip">${of
|
|
104
|
+
return `<div class="click-chip" data-trust="thread-control">${of
|
|
101
105
|
? `<button type="button" class="click-link" data-act="showButtons" data-arg="${escAttr(of)}" title="${escAttr(tr("chat.clickShow"))}">${inner}</button>`
|
|
102
106
|
: `<span class="click-text">${inner}</span>`}</div>`;
|
|
103
107
|
}
|
|
108
|
+
function trialNow() { try { const t = opts.serverNow ? opts.serverNow() : Date.now(); return Number.isFinite(t) ? t : Infinity; } catch { return Infinity; } }
|
|
109
|
+
function trialHtml(offer, time, message) {
|
|
110
|
+
const expired = trialNow() >= offer.expiresAt, available = !PUBLIC_LINK() && !expired && opts.canTryEffect?.(message) === true;
|
|
111
|
+
const button = available ? `<button type="button" class="btn btn-sm effect-trial-start" data-act="effectTrial" data-arg="${escAttr(msgKey(message))}">${escAttr(tr("chat.effectTrialStart"))}</button>` : "";
|
|
112
|
+
return `<div class="msg status-line"><div class="meta">${time}</div><div class="body"><div class="effect-trial-offer" data-trust="thread-control"><strong>${escAttr(tr("chat.effectTrial"))}: ${escAttr(offer.title)}</strong><div>${escAttr(tr(expired ? "chat.effectTrialExpired" : "chat.effectTrialLocal"))}</div>${button}</div></div></div>`;
|
|
113
|
+
}
|
|
114
|
+
function armTrials() {
|
|
115
|
+
if (trialTimer !== null) clearTimeout(trialTimer); trialTimer = null;
|
|
116
|
+
const now = trialNow(), next = msgs.map(x => effectTrialMessage(x)).filter(x => x && x.expiresAt > now).reduce((n, x) => Math.min(n, x.expiresAt), Infinity);
|
|
117
|
+
if (Number.isFinite(next)) trialTimer = setTimeout(() => { trialTimer = null; render(msgs); }, Math.max(1, Math.min(2147483647, next - now)));
|
|
118
|
+
}
|
|
104
119
|
/**
|
|
105
120
|
* #1672: a status line (another instance's message, a daemon notice) is muted, read-only plain text — never Markdown,
|
|
106
121
|
* a preview card, a button or a person's bubble. Checked before isUser, which reads the sender: a fleet name need not
|
|
@@ -111,10 +126,12 @@ export function createThread(list, scroller, opts) {
|
|
|
111
126
|
const lines = R().lineCount(text);
|
|
112
127
|
const manyLines = lines > STATUS_FOLD_LINES;
|
|
113
128
|
const more = manyLines || text.length > STATUS_FOLD_CHARS
|
|
114
|
-
? `<button type="button" class="st-more" data-act="toggleFold"${manyLines ? ` data-lines="${escAttr(lines)}"` : ""}>${escAttr(manyLines ? trf("chat.showAll", lines) : tr("chat.showMore"))}</button>`
|
|
129
|
+
? `<button data-trust="thread-control" type="button" class="st-more" data-act="toggleFold"${manyLines ? ` data-lines="${escAttr(lines)}"` : ""}>${escAttr(manyLines ? trf("chat.showAll", lines) : tr("chat.showMore"))}</button>`
|
|
115
130
|
: "";
|
|
116
|
-
const tools = `<div class="msg-tools"><button type="button" class="chip-btn icon-only" data-act="copyMsg" data-arg="${escAttr(msgKey(x))}" title="${escAttr(tr("chat.copyMessage"))}" aria-label="${escAttr(tr("chat.copyMessage"))}">${COPY}</button></div>`;
|
|
117
|
-
|
|
131
|
+
const tools = `<div data-trust="thread-control" class="msg-tools"><button type="button" class="chip-btn icon-only" data-act="copyMsg" data-arg="${escAttr(msgKey(x))}" title="${escAttr(tr("chat.copyMessage"))}" aria-label="${escAttr(tr("chat.copyMessage"))}">${COPY}</button></div>`;
|
|
132
|
+
// #1990: the other instance's name opens its chat (an in-app link the router takes: no target).
|
|
133
|
+
const body = textLinkSegments(text, x.links).map(sg => sg.href ? `<a data-trust="link" class="st-link" href="${escAttr(sg.href)}">${escAttr(sg.text)}</a>` : escAttr(sg.text)).join("");
|
|
134
|
+
return `<div class="msg status-line${more ? " folded" : ""}"><div class="meta">${time}</div><div class="body"><div class="st-text">${body}</div>${more}</div>${tools}</div>`;
|
|
118
135
|
}
|
|
119
136
|
/**
|
|
120
137
|
* #1266: an agent reply's buttons. Labels are text (escaped, never Markdown); the click names the set and the index,
|
|
@@ -130,7 +147,7 @@ export function createThread(list, scroller, opts) {
|
|
|
130
147
|
return `<button type="button" class="btn btn-sm rb-btn${chosen ? " chosen" : ""}" data-act="replyButton" data-arg="${escAttr(`${b.id}:${i}`)}"${off ? " disabled" : ""}>${escAttr(chosen ? `✓ ${label}` : label)}</button>`;
|
|
131
148
|
}).join("");
|
|
132
149
|
const note = b.state === "chosen" ? trf("chat.rbChosen", b.by || "") : b.state === "expired" ? tr("chat.rbExpired") : "";
|
|
133
|
-
return `<div class="rb-row" role="group" aria-label="${escAttr(tr("chat.rbGroup"))}">${items}${note ? `<span class="rb-note">${escAttr(note)}</span>` : ""}</div>`;
|
|
150
|
+
return `<div data-trust="reply" class="rb-row" role="group" aria-label="${escAttr(tr("chat.rbGroup"))}">${items}${note ? `<span class="rb-note">${escAttr(note)}</span>` : ""}</div>`;
|
|
134
151
|
}
|
|
135
152
|
/**
|
|
136
153
|
* #1823: a poll. Anyone here may click while it is open; the page marks its own vote (opts.pollMine), which no one else
|
|
@@ -151,9 +168,9 @@ export function createThread(list, scroller, opts) {
|
|
|
151
168
|
? escAttr(trf("chat.pollClosed", p.answered))
|
|
152
169
|
: `${escAttr(trf("chat.pollAnswered", p.answered))} · ⏳ <span class="poll-left" data-deadline="${escAttr(p.deadline)}" title="${escAttr(tr("chat.pollLeft"))}"></span>`;
|
|
153
170
|
const voters = closed && Array.isArray(p.voters)
|
|
154
|
-
? `<ul class="poll-voters" aria-label="${escAttr(tr("chat.pollVoters"))}">${b.labels.map((label, i) => `<li>${escAttr(`${label}: ${(p.voters[i] || []).join(", ") || "—"}`)}</li>`).join("")}</ul>`
|
|
171
|
+
? `<ul data-trust="reply" class="poll-voters" aria-label="${escAttr(tr("chat.pollVoters"))}">${b.labels.map((label, i) => `<li>${escAttr(`${label}: ${(p.voters[i] || []).join(", ") || "—"}`)}</li>`).join("")}</ul>`
|
|
155
172
|
: "";
|
|
156
|
-
return `<div class="rb-row poll" role="group" aria-label="${escAttr(tr("chat.pollGroup"))}">${items}<span class="rb-note">${status}</span></div>${voters}`;
|
|
173
|
+
return `<div data-trust="reply" class="rb-row poll" role="group" aria-label="${escAttr(tr("chat.pollGroup"))}">${items}<span class="rb-note">${status}</span></div>${voters}`;
|
|
157
174
|
}
|
|
158
175
|
/** #1823: the time left on this node's open polls, now. */
|
|
159
176
|
function pollLeft(node) {
|
|
@@ -187,7 +204,7 @@ export function createThread(list, scroller, opts) {
|
|
|
187
204
|
const doc = list.ownerDocument;
|
|
188
205
|
for (const pre of root.querySelectorAll(".md pre")) {
|
|
189
206
|
const box = doc.createElement("div"); box.className = "codeblock";
|
|
190
|
-
const head = doc.createElement("div"); head.className = "cb-head";
|
|
207
|
+
const head = doc.createElement("div"); head.className = "cb-head"; head.setAttribute("data-trust", "thread-control");
|
|
191
208
|
const lang = doc.createElement("span"); lang.className = "cb-lang"; lang.textContent = pre.dataset.lang || "text";
|
|
192
209
|
const wrap = doc.createElement("button"); wrap.type = "button"; wrap.className = "chip-btn"; wrap.dataset.act = "toggleWrap"; wrap.textContent = tr("chat.wrap");
|
|
193
210
|
const copy = doc.createElement("button"); copy.type = "button"; copy.className = "chip-btn"; copy.dataset.act = "copyCode"; copy.textContent = tr("chat.copy");
|
|
@@ -196,7 +213,7 @@ export function createThread(list, scroller, opts) {
|
|
|
196
213
|
const lines = R().lineCount(pre.textContent);
|
|
197
214
|
if (lines > R().CODE_FOLD_LINES) {
|
|
198
215
|
box.classList.add("folded");
|
|
199
|
-
const more = doc.createElement("button"); more.type = "button"; more.className = "cb-more"; more.dataset.act = "toggleFold";
|
|
216
|
+
const more = doc.createElement("button"); more.type = "button"; more.className = "cb-more"; more.setAttribute("data-trust", "thread-control"); more.dataset.act = "toggleFold";
|
|
200
217
|
more.dataset.lines = String(lines); more.textContent = trf("chat.showAll", lines);
|
|
201
218
|
box.append(more);
|
|
202
219
|
}
|
|
@@ -214,7 +231,7 @@ export function createThread(list, scroller, opts) {
|
|
|
214
231
|
if (!fence) continue;
|
|
215
232
|
ph.textContent = "";
|
|
216
233
|
if (ph.classList.contains("truncated") || !fence.terminated) {
|
|
217
|
-
const note = doc.createElement("div"); note.className = "pv-note"; note.textContent = tr("chat.pvTruncated"); ph.append(note);
|
|
234
|
+
const note = doc.createElement("div"); note.className = "pv-note"; note.setAttribute("data-trust", "thread-control"); note.textContent = tr("chat.pvTruncated"); ph.append(note);
|
|
218
235
|
continue;
|
|
219
236
|
}
|
|
220
237
|
buildCard(ph, x, `${msgKey(x)}:f${n}`, { n, code: fence.code }, stoppedKeys);
|
|
@@ -244,12 +261,12 @@ export function createThread(list, scroller, opts) {
|
|
|
244
261
|
const doc = list.ownerDocument;
|
|
245
262
|
const el = (tag, cls, text) => { const e = doc.createElement(tag); if (cls) e.className = cls; if (text != null) e.textContent = text; return e; };
|
|
246
263
|
ph.textContent = "";
|
|
247
|
-
const head = el("div", "tc-head");
|
|
264
|
+
const head = el("div", "tc-head"); head.setAttribute("data-trust", "thread-control");
|
|
248
265
|
const label = el("span", "tc-label", att.name);
|
|
249
266
|
const show = el("button", "btn btn-sm tc-show", tr("chat.tcShow")); show.type = "button";
|
|
250
267
|
const dl = el("a", "btn btn-sm tc-dl", tr("chat.tcDownload")); dl.href = `/ui/file/${att.id}`; dl.setAttribute("download", att.name);
|
|
251
268
|
head.append(label, show, dl);
|
|
252
|
-
const note = el("div", "pv-note");
|
|
269
|
+
const note = el("div", "pv-note"); note.setAttribute("data-trust", "thread-control");
|
|
253
270
|
const body = el("div", "tc-body"); body.hidden = true;
|
|
254
271
|
ph.append(head, note, body);
|
|
255
272
|
let text = null, reading = false, all = false;
|
|
@@ -262,7 +279,8 @@ export function createThread(list, scroller, opts) {
|
|
|
262
279
|
// Formatting is built only within TEXT_PREVIEW_NODES: the markup is measured as a string first, never as DOM.
|
|
263
280
|
const plain = () => {
|
|
264
281
|
const pre = el("pre", "tc-pre"), code = el("code", null, src);
|
|
265
|
-
|
|
282
|
+
const note = el("div", "pv-note", tr("chat.tcPlain")); note.setAttribute("data-trust", "thread-control");
|
|
283
|
+
pre.append(code); body.append(note, pre);
|
|
266
284
|
};
|
|
267
285
|
if (type === "md") {
|
|
268
286
|
const html = R().renderMarkdown(src);
|
|
@@ -282,7 +300,7 @@ export function createThread(list, scroller, opts) {
|
|
|
282
300
|
}
|
|
283
301
|
}
|
|
284
302
|
if (cut) {
|
|
285
|
-
const more = el("button", "btn btn-sm tc-more", trf("chat.tcShowAll", lines.length)); more.type = "button";
|
|
303
|
+
const more = el("button", "btn btn-sm tc-more", trf("chat.tcShowAll", lines.length)); more.setAttribute("data-trust", "thread-control"); more.type = "button";
|
|
286
304
|
more.onclick = () => { all = true; draw(); };
|
|
287
305
|
body.append(more);
|
|
288
306
|
}
|
|
@@ -343,7 +361,7 @@ export function createThread(list, scroller, opts) {
|
|
|
343
361
|
const el = (tag, cls, text) => { const e = doc.createElement(tag); if (cls) e.className = cls; if (text != null) e.textContent = text; return e; };
|
|
344
362
|
const att = source.att || null;
|
|
345
363
|
let code = att ? null : source.code;
|
|
346
|
-
const head = el("div", "pv-head");
|
|
364
|
+
const head = el("div", "pv-head"); head.setAttribute("data-trust", "thread-control");
|
|
347
365
|
const label = el("span", "pv-label", att ? `${tr("chat.pvHtml")} · ${att.name}` : tr("chat.pvHtml"));
|
|
348
366
|
const run = el("button", "btn btn-sm pv-run", tr("chat.pvPreview")); run.type = "button";
|
|
349
367
|
const stopB = el("button", "btn btn-sm pv-stop", tr("chat.pvStop")); stopB.type = "button"; stopB.hidden = true;
|
|
@@ -353,15 +371,15 @@ export function createThread(list, scroller, opts) {
|
|
|
353
371
|
const panel = opts.panel || null;
|
|
354
372
|
const toPanel = panel ? el("button", "btn btn-sm btn-ghost pv-open", tr("chat.pvOpenPanel")) : null;
|
|
355
373
|
if (toPanel) toPanel.type = "button";
|
|
356
|
-
const menu = el("details", "pv-menu");
|
|
374
|
+
const menu = el("details", "pv-menu"); menu.setAttribute("data-trust", "preview-menu");
|
|
357
375
|
const sum = el("summary", null, "⋯"); sum.title = tr("chat.pvMenu"); sum.setAttribute("aria-label", tr("chat.pvMenu"));
|
|
358
376
|
const optB = el("button", "chip-btn pv-opt"); optB.type = "button";
|
|
359
377
|
const neverB = el("button", "chip-btn pv-never"); neverB.type = "button";
|
|
360
378
|
menu.append(sum, optB, neverB);
|
|
361
379
|
head.append(label, run, stopB, ...(toPanel ? [toPanel] : []), ...(dl ? [dl] : []), menu);
|
|
362
|
-
const note = el("div", "pv-note");
|
|
380
|
+
const note = el("div", "pv-note"); note.setAttribute("data-trust", "thread-control");
|
|
363
381
|
// #1568: the banner, with its "I understand" (hidden until the next sign-in; the frame's border stays).
|
|
364
|
-
const banner = el("div", "pv-banner"); banner.hidden = true;
|
|
382
|
+
const banner = el("div", "pv-banner"); banner.setAttribute("data-trust", "thread-control"); banner.hidden = true;
|
|
365
383
|
const ack = el("button", "btn btn-sm btn-ghost pv-ack", P().bannerAckLabel()); ack.type = "button"; ack.title = P().bannerAckTitle();
|
|
366
384
|
banner.append(el("span", "pv-banner-text", P().banner()), ack);
|
|
367
385
|
const holder = el("div", "pv-holder");
|
|
@@ -493,6 +511,7 @@ export function createThread(list, scroller, opts) {
|
|
|
493
511
|
function render(next, renderOpts = {}) {
|
|
494
512
|
const stick = pinned || nearBottom();
|
|
495
513
|
msgs = next || [];
|
|
514
|
+
armTrials();
|
|
496
515
|
if (!msgs.length) {
|
|
497
516
|
for (const v of nodes.values()) stopIn(v.node);
|
|
498
517
|
nodes.clear();
|
|
@@ -531,7 +550,7 @@ export function createThread(list, scroller, opts) {
|
|
|
531
550
|
const stopped = have ? stopIn(have.node) : [];
|
|
532
551
|
const node = msgNode(html, x, stopped);
|
|
533
552
|
if (have) have.node.replaceWith(node); else added++;
|
|
534
|
-
have = { html, node, x }; nodes.set(key, have);
|
|
553
|
+
have = { html, node, x, trialButton: node.querySelector(".effect-trial-offer > button.effect-trial-start") }; nodes.set(key, have);
|
|
535
554
|
} else { have.x = x; timeLabel(have.node); }
|
|
536
555
|
const want = prev ? prev.nextSibling : list.firstChild;
|
|
537
556
|
// A node already in the list is being moved: its preview stops first. A new node (still in its template) has none.
|
|
@@ -605,12 +624,19 @@ export function createThread(list, scroller, opts) {
|
|
|
605
624
|
else if (act === "replyButton") {
|
|
606
625
|
const m = /^([0-9a-f]{32}):(\d{1,2})$/.exec(el.dataset.arg || "");
|
|
607
626
|
if (m && opts.clickReplyButton) opts.clickReplyButton(m[1], Number(m[2]));
|
|
627
|
+
} else if (act === "effectTrial") {
|
|
628
|
+
// Only the current host-produced keyed card and a real local click can request admission. No token is read from DOM.
|
|
629
|
+
const key = el.dataset.arg, row = msgs.find(x => msgKey(x) === key), owner = nodes.get(key), node = owner?.node, offer = effectTrialMessage(row);
|
|
630
|
+
if (PUBLIC_LINK() || e.defaultPrevented || e.isTrusted !== true || owner?.trialButton !== el || el.className !== "btn btn-sm effect-trial-start" || !node?.contains(el) || el.closest(".effect-trial-offer")?.getAttribute("data-trust") !== "thread-control"
|
|
631
|
+
|| !offer || trialNow() >= offer.expiresAt || opts.canTryEffect?.(row) !== true) return;
|
|
632
|
+
opts.startEffectTrial?.(row);
|
|
608
633
|
} else if (act === "showButtons") { const x = msgs.find(y => y.buttons && y.buttons.id === el.dataset.arg); if (x) reveal(msgKey(x)); }
|
|
609
634
|
}
|
|
610
635
|
|
|
611
636
|
/** The thread goes (the chat left, or another instance opened): every preview in it stops first. */
|
|
612
637
|
function dispose() {
|
|
613
638
|
disposed = true;
|
|
639
|
+
if (trialTimer !== null) clearTimeout(trialTimer); trialTimer = null;
|
|
614
640
|
if (ro) ro.disconnect();
|
|
615
641
|
if (P()) P().stopAll("leave");
|
|
616
642
|
nodes.clear();
|
package/dist/ui/chat-tour.js
CHANGED
|
@@ -58,7 +58,7 @@ function show(i) {
|
|
|
58
58
|
if (spot) spot.classList.add("tour-spot");
|
|
59
59
|
let card = byId("tour");
|
|
60
60
|
if (!card) {
|
|
61
|
-
card = document.createElement("section"); card.id = "tour"; card.className = "tour";
|
|
61
|
+
card = document.createElement("section"); card.id = "tour"; card.className = "tour"; card.setAttribute("data-trust", "tour");
|
|
62
62
|
card.setAttribute("role", "dialog"); card.setAttribute("aria-labelledby", "tourTitle");
|
|
63
63
|
card.addEventListener("click", onCardClick);
|
|
64
64
|
document.body.appendChild(card);
|