1263 lines
49 KiB
JavaScript
1263 lines
49 KiB
JavaScript
// Generated by scripts/build-client.mjs — do not edit this file.
|
|
// Source of truth: src/client/**. Run `pnpm run build` after changing it.
|
|
// Served by dsh-client-modules at /plugins/@dsh-plugin/session-notify/client.js.
|
|
window.__ModuleLoader__.load({
|
|
id: '@dsh-plugin/session-notify',
|
|
factory(require) {
|
|
const module = { exports: {} }
|
|
// Same object as `module.exports`; kept so the emitted body may assign
|
|
// through either binding.
|
|
const exports = module.exports
|
|
|
|
var __create = Object.create;
|
|
var __defProp = Object.defineProperty;
|
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
var __getProtoOf = Object.getPrototypeOf;
|
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
var __export = (target, all) => {
|
|
for (var name in all)
|
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
};
|
|
var __copyProps = (to, from, except, desc) => {
|
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
for (let key of __getOwnPropNames(from))
|
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
}
|
|
return to;
|
|
};
|
|
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
// If the importer is in node compatibility mode or this is not an ESM
|
|
// file that has been converted to a CommonJS file using a Babel-
|
|
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
mod
|
|
));
|
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
|
|
// src/client/index.js
|
|
var index_exports = {};
|
|
__export(index_exports, {
|
|
apply: () => apply,
|
|
inject: () => inject
|
|
});
|
|
module.exports = __toCommonJS(index_exports);
|
|
|
|
// src/client/constants.js
|
|
var NS = "session-notify";
|
|
var PACKAGE_NAME = "@dsh-plugin/session-notify";
|
|
var KINDS = ["completion", "approval", "question"];
|
|
var PENDING_KINDS = ["approval", "question", "plan-review"];
|
|
var THROTTLE_MS = 1500;
|
|
var SETTLE_MS = 400;
|
|
var TOAST_DURATION_MS = 6e3;
|
|
var TOAST_LIMIT = 3;
|
|
var RAISED_LIMIT = 8;
|
|
var REPLAY_LIMIT = 8;
|
|
var REPLAY_TTL_MS = 30 * 60 * 1e3;
|
|
var REPLAY_QUIET_MS = 20 * 1e3;
|
|
|
|
// src/client/i18n/zh.js
|
|
var zh = {
|
|
"notification.completion": "会话已完成",
|
|
"notification.approval": "需要授权",
|
|
"notification.question": "需要回答",
|
|
"body.completion": "{title} 已完成这一轮回答",
|
|
"body.approval": "{title}:工具 {tool} 正在等待你的授权",
|
|
"body.approvalPlain": "{title}:有工具正在等待你的授权",
|
|
"body.question": "{title}:正在等待你的回答",
|
|
"body.planReview": "{title}:计划正在等待你确认",
|
|
"body.untitled": "未命名会话",
|
|
"toast.view": "查看",
|
|
"toast.dismiss": "关闭",
|
|
"settings.title": "会话通知",
|
|
"settings.description": "窗口不在前台时用系统通知,窗口在前台时用应用内轻弹窗;正在看的那个会话完成后不打扰。",
|
|
"settings.permission.granted": "系统通知已开启",
|
|
"settings.permission.default": "尚未授予系统通知权限",
|
|
"settings.permission.denied": "系统通知已被系统设置关闭",
|
|
"settings.permission.unsupported": "当前环境不支持系统通知,仍会显示应用内轻弹窗",
|
|
"settings.permission.hint.plain": "请在系统通知设置中允许 DeepSeek Harness",
|
|
"settings.permission.hint.win": "Windows 设置 → 系统 → 通知 → DeepSeek Harness",
|
|
"settings.permission.hint.mac": "系统设置 → 通知 → DeepSeek Harness",
|
|
"settings.permission.hint.linux": "在桌面环境的通知设置(GNOME / KDE)中允许 DeepSeek Harness",
|
|
"settings.action.allow": "允许通知",
|
|
"settings.action.testSystem": "测试系统通知",
|
|
"settings.action.testAny": "测试提醒",
|
|
"settings.action.unsupported": "不可用",
|
|
"settings.permission.promptFailed": "无法请求通知权限:{message}",
|
|
"settings.health.watching": "正在监听会话状态",
|
|
"settings.health.noHooks": "未收到会话状态(通知不会触发)",
|
|
"settings.health.error": "监听会话状态出错:{message}",
|
|
"settings.health.starting": "正在等待会话列表",
|
|
"settings.kinds.label": "提醒内容",
|
|
"settings.kind.completion": "完成",
|
|
"settings.kind.approval": "授权",
|
|
"settings.kind.question": "提问",
|
|
"config.section.kinds": "提醒内容",
|
|
"config.section.permission": "系统通知权限",
|
|
"config.section.status": "运行状态",
|
|
"config.intro": "提醒都从当前页面发出:窗口不在前台时是系统通知,窗口在前台时是这里的轻弹窗。",
|
|
"config.saveNote": "开关保存在浏览器本地,重装插件不会丢失。",
|
|
"config.testHint": "这里的按钮直接走系统通知通道,用来验证系统通知本身是否可用(窗口在前台也照发)。",
|
|
"config.testAny": "测试提醒",
|
|
"config.testSystem": "测试系统通知",
|
|
"config.diag.window": "窗口状态",
|
|
"config.diag.inFront": "在前台(会走轻弹窗)",
|
|
"config.diag.away": "不在前台(会走系统通知)",
|
|
"config.diag.last": "最近一次投递",
|
|
"config.diag.none": "还没有投递过",
|
|
"config.diag.systemShown": "系统通知(系统已确认弹出)",
|
|
"config.diag.systemUnconfirmed": "系统通知(系统没有回报,回到窗口时补发轻弹窗)",
|
|
"config.diag.systemFailed": "系统通知没弹出来,回到窗口时补发轻弹窗",
|
|
"config.diag.replayDelivery": "应用内轻弹窗(回到窗口时补发)",
|
|
"config.diag.popup": "应用内轻弹窗",
|
|
"config.diag.refused": "系统通知被拒绝,改用了轻弹窗",
|
|
"config.diag.replay": "等你回到窗口时补发:{count} 条",
|
|
"config.diag.replayNone": "没有待补发的提醒"
|
|
};
|
|
|
|
// src/client/i18n/en.js
|
|
var en = {
|
|
"notification.completion": "Conversation finished",
|
|
"notification.approval": "Approval required",
|
|
"notification.question": "Answer needed",
|
|
"body.completion": "{title} finished this round",
|
|
"body.approval": "{title}: {tool} is waiting for your approval",
|
|
"body.approvalPlain": "{title}: a tool is waiting for your approval",
|
|
"body.question": "{title}: waiting for your answer",
|
|
"body.planReview": "{title}: a plan is waiting for your review",
|
|
"body.untitled": "Untitled conversation",
|
|
"toast.view": "View",
|
|
"toast.dismiss": "Dismiss",
|
|
"settings.title": "Session notifications",
|
|
"settings.description": "A system notification while the window is in the background, a light in-app popup while it is in the foreground; the conversation on screen is never interrupted.",
|
|
"settings.permission.granted": "System notifications are on",
|
|
"settings.permission.default": "System notification permission has not been granted",
|
|
"settings.permission.denied": "System notifications are switched off in system settings",
|
|
"settings.permission.unsupported": "This environment cannot show system notifications; the in-app popup still works",
|
|
"settings.permission.hint.plain": "Allow DeepSeek Harness in your system notification settings",
|
|
"settings.permission.hint.win": "Windows Settings → System → Notifications → DeepSeek Harness",
|
|
"settings.permission.hint.mac": "System Settings → Notifications → DeepSeek Harness",
|
|
"settings.permission.hint.linux": "Allow DeepSeek Harness in your desktop notification settings (GNOME / KDE)",
|
|
"settings.action.allow": "Allow notifications",
|
|
"settings.action.testSystem": "Test system notification",
|
|
"settings.action.testAny": "Test alert",
|
|
"settings.action.unsupported": "Unavailable",
|
|
"settings.permission.promptFailed": "Could not request notification permission: {message}",
|
|
"settings.health.watching": "Watching session status",
|
|
"settings.health.noHooks": "No session status received (notifications will not fire)",
|
|
"settings.health.error": "Watching session status failed: {message}",
|
|
"settings.health.starting": "Waiting for the session list",
|
|
"settings.kinds.label": "Notify about",
|
|
"settings.kind.completion": "Finished",
|
|
"settings.kind.approval": "Approval",
|
|
"settings.kind.question": "Questions",
|
|
"config.section.kinds": "What to notify about",
|
|
"config.section.permission": "System notification permission",
|
|
"config.section.status": "Status",
|
|
"config.intro": "Every alert comes from this page: a system notification while the window is in the background, the light popup here while it is in the foreground.",
|
|
"config.saveNote": "The switches are stored in this browser and survive reinstalling the plugin.",
|
|
"config.testHint": "The buttons here go straight to the system channel, so you can verify it on its own (they fire even while the window is in front).",
|
|
"config.testAny": "Test alert",
|
|
"config.testSystem": "Test system notification",
|
|
"config.diag.window": "Window",
|
|
"config.diag.inFront": "in front (popup is used)",
|
|
"config.diag.away": "not in front (system notification is used)",
|
|
"config.diag.last": "Last delivery",
|
|
"config.diag.none": "nothing delivered yet",
|
|
"config.diag.systemShown": "system notification (confirmed on screen)",
|
|
"config.diag.systemUnconfirmed": "system notification (no confirmation, replayed in-app on return)",
|
|
"config.diag.systemFailed": "system notification never appeared, replayed in-app on return",
|
|
"config.diag.replayDelivery": "in-app popup (replayed when you came back)",
|
|
"config.diag.popup": "in-app popup",
|
|
"config.diag.refused": "system channel refused, popup was used instead",
|
|
"config.diag.replay": "waiting for you to come back: {count}",
|
|
"config.diag.replayNone": "nothing waiting to be replayed"
|
|
};
|
|
|
|
// src/client/i18n/index.js
|
|
var dictionaries = { zh, en };
|
|
function registerDictionaries(locale) {
|
|
if (locale === void 0 || typeof locale.register !== "function") return () => {
|
|
};
|
|
try {
|
|
return locale.register(NS, dictionaries);
|
|
} catch {
|
|
const disposers = Object.entries(dictionaries).map(([id, dict]) => locale.register(NS, id, dict));
|
|
return () => {
|
|
for (const dispose of disposers) dispose?.();
|
|
};
|
|
}
|
|
}
|
|
function createTranslator(locale) {
|
|
return (key, params) => {
|
|
if (locale !== void 0 && typeof locale.bind === "function") {
|
|
try {
|
|
const bound = locale.bind(NS);
|
|
if (typeof bound === "function") {
|
|
const translated = bound(key, params);
|
|
if (typeof translated === "string" && translated !== key) return translated;
|
|
}
|
|
} catch {
|
|
}
|
|
}
|
|
let language = "";
|
|
try {
|
|
const snapshot = locale?.getSnapshot?.();
|
|
language = String(snapshot?.active ?? "");
|
|
} catch {
|
|
language = "";
|
|
}
|
|
const dictionary = /^zh/i.test(language) ? zh : en;
|
|
const template = dictionary[key] ?? zh[key] ?? en[key] ?? key;
|
|
if (params === void 0) return template;
|
|
return template.replace(/\{(\w+)\}/g, (match, name) => Object.prototype.hasOwnProperty.call(params, name) ? String(params[name]) : match);
|
|
};
|
|
}
|
|
|
|
// src/client/platform.js
|
|
function notificationApi() {
|
|
const Ctor = globalThis.Notification;
|
|
return typeof Ctor === "function" ? Ctor : void 0;
|
|
}
|
|
function notificationPermission() {
|
|
const Ctor = notificationApi();
|
|
if (Ctor === void 0) return "unsupported";
|
|
const permission = Ctor.permission;
|
|
if (permission === "granted" || permission === "denied" || permission === "default") return permission;
|
|
return "default";
|
|
}
|
|
function windowIsAway() {
|
|
try {
|
|
if (document.visibilityState === "hidden") return true;
|
|
} catch {
|
|
}
|
|
try {
|
|
if (typeof document.hasFocus === "function") return !document.hasFocus();
|
|
} catch {
|
|
}
|
|
return false;
|
|
}
|
|
function focusWindow() {
|
|
try {
|
|
globalThis.focus?.();
|
|
} catch {
|
|
}
|
|
}
|
|
var systemIcon = (() => {
|
|
try {
|
|
const url = new URL("./icon.svg", document.baseURI).href;
|
|
if (url !== "") return url;
|
|
} catch {
|
|
}
|
|
try {
|
|
const svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M8 2.35C5.7 2.35 3.83 4.22 3.83 6.52V9.02L3.06 11.1C2.99 11.29 3.13 11.48 3.33 11.48H12.67C12.87 11.48 13.01 11.29 12.94 11.1L12.17 9.02V6.52C12.17 4.22 10.3 2.35 8 2.35Z" fill="none" stroke="%23247bbf" stroke-width="1.2"/><path d="M6.3 12.75C6.52 13.66 7.2 14.28 8 14.28C8.8 14.28 9.48 13.66 9.7 12.75" fill="none" stroke="%23247bbf" stroke-width="1.2" stroke-linecap="round"/></svg>';
|
|
return `data:image/svg+xml,${svg}`;
|
|
} catch {
|
|
return void 0;
|
|
}
|
|
})();
|
|
function platformKey() {
|
|
try {
|
|
const agent = navigator.userAgentData;
|
|
const platform = String((agent?.platform ?? navigator.platform) || navigator.userAgent || "");
|
|
if (/win/i.test(platform)) return "win";
|
|
if (/mac|iphone|ipad/i.test(platform)) return "mac";
|
|
if (/linux|x11/i.test(platform)) return "linux";
|
|
} catch {
|
|
}
|
|
return "plain";
|
|
}
|
|
|
|
// src/client/core/log.js
|
|
function text(error) {
|
|
return error instanceof Error ? error.message : String(error);
|
|
}
|
|
function report(message) {
|
|
try {
|
|
console.error(`session-notify: ${message}`);
|
|
} catch {
|
|
}
|
|
}
|
|
|
|
// src/client/core/store.js
|
|
function createStore(initial) {
|
|
let value = initial;
|
|
const listeners = /* @__PURE__ */ new Set();
|
|
return {
|
|
getSnapshot: () => value,
|
|
subscribe: (listener) => {
|
|
listeners.add(listener);
|
|
return () => {
|
|
listeners.delete(listener);
|
|
};
|
|
},
|
|
set: (next) => {
|
|
if (next === value) return;
|
|
value = next;
|
|
for (const listener of [...listeners]) listener();
|
|
}
|
|
};
|
|
}
|
|
|
|
// src/client/core/storage.js
|
|
var STORAGE_KEY = "dsh-plugin/session-notify";
|
|
function readStoredKinds() {
|
|
const kinds = { completion: true, approval: true, question: true };
|
|
try {
|
|
const raw = globalThis.localStorage?.getItem(STORAGE_KEY);
|
|
if (typeof raw !== "string" || raw === "") return kinds;
|
|
const stored = JSON.parse(raw);
|
|
for (const kind of KINDS) {
|
|
if (typeof stored?.[kind] === "boolean") kinds[kind] = stored[kind];
|
|
}
|
|
} catch {
|
|
}
|
|
return kinds;
|
|
}
|
|
function writeStoredKinds(kinds) {
|
|
try {
|
|
globalThis.localStorage?.setItem(STORAGE_KEY, JSON.stringify(kinds));
|
|
} catch {
|
|
}
|
|
}
|
|
|
|
// src/client/core/copy.js
|
|
function createCopy(t) {
|
|
const copyFor = (candidate) => {
|
|
const title = candidate.title === "" ? t("body.untitled") : candidate.title;
|
|
if (candidate.kind === "completion") {
|
|
return { kind: "completion", title: t("notification.completion"), body: t("body.completion", { title }) };
|
|
}
|
|
if (candidate.kind === "question") {
|
|
const question = candidate.detail;
|
|
return {
|
|
kind: "question",
|
|
title: t("notification.question"),
|
|
body: question === "" ? t("body.question", { title }) : `${title} · ${question}`
|
|
};
|
|
}
|
|
if (candidate.pendingKind === "plan-review") {
|
|
return { kind: "question", title: t("notification.question"), body: t("body.planReview", { title }) };
|
|
}
|
|
const tool = candidate.detail;
|
|
return {
|
|
kind: "approval",
|
|
title: t("notification.approval"),
|
|
body: tool === "" ? t("body.approvalPlain", { title }) : t("body.approval", { title, tool })
|
|
};
|
|
};
|
|
const describeDelivery = (last, tr) => {
|
|
if (last === null || last === void 0) return tr("config.diag.none");
|
|
const outcome = last.outcome.startsWith("system-refused") ? tr("config.diag.refused") : last.outcome === "system" ? last.shown === true ? tr("config.diag.systemShown") : last.shown === false ? tr("config.diag.systemFailed") : tr("config.diag.systemUnconfirmed") : last.outcome === "replay" ? tr("config.diag.replayDelivery") : tr("config.diag.popup");
|
|
return `${tr("config.diag.last")}: ${last.at} · ${outcome} · ${last.title}`;
|
|
};
|
|
const describeReplay = (count, tr) => count === 0 ? tr("config.diag.replayNone") : tr("config.diag.replay", { count });
|
|
return { copyFor, describeDelivery, describeReplay };
|
|
}
|
|
|
|
// src/client/core/toasts.js
|
|
function createToastStack({ store, copy }) {
|
|
const timers = /* @__PURE__ */ new Map();
|
|
let seq = 0;
|
|
const closeToast = (id) => {
|
|
const timer = timers.get(id);
|
|
if (timer !== void 0) {
|
|
clearTimeout(timer);
|
|
timers.delete(id);
|
|
}
|
|
const snapshot = store.getSnapshot();
|
|
if (!snapshot.toasts.some((toast) => toast.id === id)) return;
|
|
store.set({ ...snapshot, toasts: snapshot.toasts.filter((toast) => toast.id !== id) });
|
|
};
|
|
const armToast = (id) => {
|
|
const timer = timers.get(id);
|
|
if (timer !== void 0) clearTimeout(timer);
|
|
timers.set(id, setTimeout(() => closeToast(id), TOAST_DURATION_MS));
|
|
};
|
|
const holdToast = (id) => {
|
|
const timer = timers.get(id);
|
|
if (timer === void 0) return;
|
|
clearTimeout(timer);
|
|
timers.delete(id);
|
|
};
|
|
const showToast = (candidate) => {
|
|
const notice = copy.copyFor(candidate);
|
|
seq += 1;
|
|
const toast = {
|
|
id: `dsn-${String(seq)}`,
|
|
kind: notice.kind,
|
|
title: notice.title,
|
|
body: notice.body,
|
|
sessionId: candidate.sessionId
|
|
};
|
|
const snapshot = store.getSnapshot();
|
|
const next = [toast, ...snapshot.toasts];
|
|
for (const dropped of next.slice(TOAST_LIMIT)) {
|
|
const timer = timers.get(dropped.id);
|
|
if (timer !== void 0) {
|
|
clearTimeout(timer);
|
|
timers.delete(dropped.id);
|
|
}
|
|
}
|
|
store.set({ ...snapshot, toasts: next.slice(0, TOAST_LIMIT) });
|
|
armToast(toast.id);
|
|
};
|
|
const dispose = () => {
|
|
for (const timer of timers.values()) clearTimeout(timer);
|
|
timers.clear();
|
|
};
|
|
return { showToast, closeToast, armToast, holdToast, dispose };
|
|
}
|
|
|
|
// src/client/core/replay.js
|
|
function createReplay({ store, publish, showToast, recordDelivery }) {
|
|
let entries = [];
|
|
const dropReplay = (candidate) => {
|
|
const next = entries.filter((entry) => entry.candidate.sessionId !== candidate.sessionId || entry.candidate.kind !== candidate.kind);
|
|
if (next.length === entries.length) return;
|
|
entries = next;
|
|
publish();
|
|
};
|
|
const holdForReplay = (candidate, monitor) => {
|
|
const now = Date.now();
|
|
entries = [
|
|
...entries.filter((entry) => entry.candidate.sessionId !== candidate.sessionId || entry.candidate.kind !== candidate.kind),
|
|
{ candidate, at: now, monitor }
|
|
].slice(-REPLAY_LIMIT);
|
|
publish();
|
|
};
|
|
const flushReplay = () => {
|
|
if (entries.length === 0) return;
|
|
const now = Date.now();
|
|
const waiting = entries;
|
|
entries = [];
|
|
const due = waiting.filter((entry) => {
|
|
if (now - entry.at > REPLAY_TTL_MS) return false;
|
|
return !(entry.monitor?.shown === true && now - entry.at <= REPLAY_QUIET_MS);
|
|
});
|
|
const shown = due.slice(-TOAST_LIMIT);
|
|
for (const entry of shown) showToast(entry.candidate);
|
|
if (shown.length > 0) recordDelivery("replay", shown[shown.length - 1].candidate);
|
|
else publish();
|
|
};
|
|
const size = () => entries.length;
|
|
const dispose = () => {
|
|
entries = [];
|
|
};
|
|
return { holdForReplay, dropReplay, flushReplay, size, dispose };
|
|
}
|
|
|
|
// src/client/core/system-channel.js
|
|
function createSystemChannel({ store, copy, notify, openSession, dropReplay }) {
|
|
const raised = /* @__PURE__ */ new Set();
|
|
const confirmDelivery = (monitor, shown) => {
|
|
if (monitor === null || monitor === void 0 || typeof monitor !== "object") return;
|
|
monitor.shown = shown;
|
|
const snapshot = store.getSnapshot();
|
|
const last = snapshot.lastDelivery;
|
|
if (last === null || last === void 0 || last.monitor !== monitor || last.shown === shown) return;
|
|
store.set({ ...snapshot, lastDelivery: { ...last, shown } });
|
|
};
|
|
const keepRaised = (notification) => {
|
|
raised.add(notification);
|
|
while (raised.size > RAISED_LIMIT) {
|
|
const oldest = raised.values().next().value;
|
|
if (oldest === void 0) break;
|
|
raised.delete(oldest);
|
|
}
|
|
};
|
|
const notifySystem = (candidate, force) => {
|
|
const Ctor = globalThis.Notification;
|
|
const permission = notificationPermission();
|
|
if (permission === "unsupported") return { outcome: "unsupported" };
|
|
if (!force && permission !== "granted") return { outcome: "permission" };
|
|
try {
|
|
const notice = copy.copyFor(candidate);
|
|
const options = {
|
|
body: notice.body,
|
|
tag: `dsh-session-${candidate.sessionId}`,
|
|
requireInteraction: false
|
|
};
|
|
if (systemIcon !== void 0) options.icon = systemIcon;
|
|
const notification = new Ctor(notice.title, options);
|
|
const record = { outcome: "raised", notice, shown: void 0 };
|
|
keepRaised(notification);
|
|
notification.onshow = () => {
|
|
confirmDelivery(record, true);
|
|
};
|
|
notification.onerror = (event) => {
|
|
confirmDelivery(record, false);
|
|
notify(`system notification was refused: ${String(event?.message ?? "unknown")}`);
|
|
};
|
|
notification.onclose = () => {
|
|
raised.delete(notification);
|
|
};
|
|
notification.onclick = () => {
|
|
dropReplay(candidate);
|
|
focusWindow();
|
|
openSession(candidate.sessionId);
|
|
try {
|
|
notification.close();
|
|
} catch {
|
|
}
|
|
};
|
|
return record;
|
|
} catch (error) {
|
|
notify(`system notification failed: ${text(error)}`);
|
|
return { outcome: "threw" };
|
|
}
|
|
};
|
|
const dispose = () => {
|
|
raised.clear();
|
|
};
|
|
return { notifySystem, confirmDelivery, dispose };
|
|
}
|
|
|
|
// src/client/core/session.js
|
|
function servedPendingKind(value) {
|
|
if (value === void 0 || value === null || typeof value !== "object") return void 0;
|
|
const kind = value.kind;
|
|
return typeof kind === "string" && PENDING_KINDS.includes(kind) ? kind : void 0;
|
|
}
|
|
function preview(value) {
|
|
if (typeof value !== "string") return "";
|
|
const flat = value.replace(/\s+/g, " ").trim();
|
|
return flat.length > 80 ? `${flat.slice(0, 79)}…` : flat;
|
|
}
|
|
function questionText(interaction) {
|
|
if (interaction === null || typeof interaction !== "object") return "";
|
|
const first = Array.isArray(interaction.questions) ? interaction.questions[0] : void 0;
|
|
if (typeof first?.question === "string") return preview(first.question);
|
|
if (typeof first?.text === "string") return preview(first.text);
|
|
if (typeof interaction.question === "string") return preview(interaction.question);
|
|
if (typeof interaction.prompt === "string") return preview(interaction.prompt);
|
|
return preview(interaction.displayReason?.text ?? interaction.reason?.text ?? "");
|
|
}
|
|
function titleOf(summary, sessionId) {
|
|
const title = typeof summary?.title === "string" ? summary.title.trim() : "";
|
|
return title === "" ? sessionId.slice(0, 8) : title;
|
|
}
|
|
function isOnScreen(sessionId, list) {
|
|
if (list === void 0) return false;
|
|
for (const id of Object.keys(list.byId ?? {})) {
|
|
const summary = list.byId[id];
|
|
if (summary !== void 0 && (summary.retainedBy?.mainView ?? 0) > 0) return id === sessionId;
|
|
}
|
|
return false;
|
|
}
|
|
function stillWorth(candidate, live) {
|
|
const list = live.list;
|
|
if (candidate.test === true || list === void 0) return true;
|
|
const summary = list.byId?.[candidate.sessionId];
|
|
if (summary === void 0) return true;
|
|
const status = live.status;
|
|
const running = status?.get?.(candidate.sessionId)?.running ?? summary.running;
|
|
return running !== true;
|
|
}
|
|
|
|
// src/client/core/delivery.js
|
|
function createDelivery({ store, copy, live, toasts, system, replay }) {
|
|
const pending = [];
|
|
let settleTimer = 0;
|
|
let lastDelivery = 0;
|
|
let deliverySeq = 0;
|
|
const recordDelivery = (outcome, candidate, monitor) => {
|
|
deliverySeq += 1;
|
|
const now = /* @__PURE__ */ new Date();
|
|
const pad = (value) => String(value).padStart(2, "0");
|
|
const record = {
|
|
seq: deliverySeq,
|
|
outcome,
|
|
at: `${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`,
|
|
title: copy.copyFor(candidate).title,
|
|
shown: monitor?.shown,
|
|
monitor
|
|
};
|
|
store.set({ ...store.getSnapshot(), lastDelivery: record });
|
|
return record;
|
|
};
|
|
const deliver = (candidate, ignoreThrottle) => {
|
|
if (ignoreThrottle !== true) {
|
|
const now = Date.now();
|
|
if (now - lastDelivery < THROTTLE_MS) return;
|
|
lastDelivery = now;
|
|
}
|
|
if (!windowIsAway()) {
|
|
if (isOnScreen(candidate.sessionId, live.list)) return;
|
|
toasts.showToast(candidate);
|
|
recordDelivery("popup", candidate);
|
|
return;
|
|
}
|
|
const result = system.notifySystem(candidate, candidate.test === true);
|
|
if (result.outcome === "raised") {
|
|
recordDelivery("system", candidate, result);
|
|
if (candidate.test !== true) replay.holdForReplay(candidate, result);
|
|
return;
|
|
}
|
|
toasts.showToast(candidate);
|
|
recordDelivery(`system-refused-${result.outcome}`, candidate);
|
|
};
|
|
const flushSettle = () => {
|
|
settleTimer = 0;
|
|
const candidate = pending.shift();
|
|
if (candidate !== void 0 && stillWorth(candidate, live)) deliver(candidate, false);
|
|
if (pending.length > 0) settleTimer = setTimeout(() => flushSettle(), SETTLE_MS);
|
|
};
|
|
const queue = (candidate) => {
|
|
pending.push(candidate);
|
|
if (settleTimer !== 0) return;
|
|
settleTimer = setTimeout(() => flushSettle(), SETTLE_MS);
|
|
};
|
|
const resetThrottle = () => {
|
|
lastDelivery = 0;
|
|
};
|
|
const dispose = () => {
|
|
if (settleTimer !== 0) {
|
|
clearTimeout(settleTimer);
|
|
settleTimer = 0;
|
|
}
|
|
};
|
|
return { deliver, recordDelivery, queue, flushSettle, resetThrottle, dispose };
|
|
}
|
|
|
|
// src/client/core/observe.js
|
|
function createObservation({ store, live, queue }) {
|
|
const runs = /* @__PURE__ */ new Map();
|
|
const completionNotice = /* @__PURE__ */ new Set();
|
|
const pendingNotice = /* @__PURE__ */ new Map();
|
|
const reportHealth = (next) => {
|
|
const snapshot = store.getSnapshot();
|
|
const current = snapshot.health;
|
|
if (current.state === next.state && current.message === next.message) return;
|
|
if (current.state !== "watching") report(`observer health: ${next.state} ${next.message}`);
|
|
store.set({ ...snapshot, health: next });
|
|
};
|
|
const observe = (list, status) => {
|
|
if (list === void 0 || status === void 0) return;
|
|
live.list = list;
|
|
live.status = status;
|
|
const kinds = store.getSnapshot().kinds;
|
|
for (const sessionId of Object.keys(list.byId ?? {})) {
|
|
const summary = list.byId[sessionId];
|
|
if (summary === void 0 || summary.origin === "subagent") continue;
|
|
const sessionStatus = status.get(sessionId);
|
|
const running = sessionStatus?.running ?? summary.running;
|
|
if (running === true) {
|
|
runs.set(sessionId, true);
|
|
completionNotice.delete(sessionId);
|
|
} else {
|
|
if (runs.get(sessionId) === true) {
|
|
runs.set(sessionId, false);
|
|
if (summary.blank !== true && !completionNotice.has(sessionId) && kinds.completion) {
|
|
completionNotice.add(sessionId);
|
|
queue({
|
|
kind: "completion",
|
|
sessionId,
|
|
title: titleOf(summary, sessionId),
|
|
detail: "",
|
|
pendingKind: ""
|
|
});
|
|
}
|
|
} else if (!runs.has(sessionId)) {
|
|
runs.set(sessionId, false);
|
|
}
|
|
}
|
|
const interaction = sessionStatus?.pendingInteraction;
|
|
const pendingKind = servedPendingKind(interaction);
|
|
if (pendingKind === void 0) {
|
|
pendingNotice.delete(sessionId);
|
|
continue;
|
|
}
|
|
if (pendingNotice.get(sessionId) === pendingKind) continue;
|
|
pendingNotice.set(sessionId, pendingKind);
|
|
const trigger = pendingKind === "approval" ? "approval" : "question";
|
|
if (!kinds[trigger]) continue;
|
|
queue({
|
|
kind: trigger,
|
|
sessionId,
|
|
title: titleOf(summary, sessionId),
|
|
detail: pendingKind === "approval" ? String(interaction?.toolName ?? "") : questionText(interaction),
|
|
pendingKind
|
|
});
|
|
}
|
|
for (const sessionId of [...runs.keys()]) {
|
|
if (list.byId?.[sessionId] !== void 0) continue;
|
|
runs.delete(sessionId);
|
|
completionNotice.delete(sessionId);
|
|
pendingNotice.delete(sessionId);
|
|
}
|
|
};
|
|
return { observe, reportHealth };
|
|
}
|
|
|
|
// src/client/core/actions.js
|
|
function createActions({ store, t, delivery, system, toasts, publish }) {
|
|
const testCandidate = (title) => ({
|
|
kind: "completion",
|
|
sessionId: "test",
|
|
title,
|
|
detail: "",
|
|
pendingKind: "",
|
|
test: true
|
|
});
|
|
const sendTest = () => {
|
|
delivery.deliver(testCandidate(t("config.testAny")), true);
|
|
};
|
|
const sendTestSystem = () => {
|
|
const candidate = testCandidate(t("config.testSystem"));
|
|
const result = system.notifySystem(candidate, true);
|
|
if (result.outcome === "raised") {
|
|
delivery.recordDelivery("system", candidate, result);
|
|
return;
|
|
}
|
|
toasts.showToast(candidate);
|
|
delivery.recordDelivery(`system-refused-${result.outcome}`, candidate);
|
|
};
|
|
const requestPermission = async () => {
|
|
const Ctor = notificationApi();
|
|
if (Ctor === void 0) {
|
|
publish();
|
|
return;
|
|
}
|
|
try {
|
|
let result = Ctor.requestPermission();
|
|
if (result === void 0) {
|
|
result = new Promise((resolve) => {
|
|
Ctor.requestPermission((value) => resolve(value));
|
|
});
|
|
}
|
|
await result;
|
|
} catch (error) {
|
|
const snapshot2 = store.getSnapshot();
|
|
store.set({ ...snapshot2, promptError: t("settings.permission.promptFailed", { message: text(error) }) });
|
|
return;
|
|
}
|
|
const snapshot = store.getSnapshot();
|
|
store.set({ ...snapshot, permission: notificationPermission(), promptError: "" });
|
|
if (notificationPermission() === "granted") sendTest();
|
|
};
|
|
const setKindEnabled = (kind, enabled) => {
|
|
const snapshot = store.getSnapshot();
|
|
const kinds = { ...snapshot.kinds, [kind]: enabled };
|
|
writeStoredKinds(kinds);
|
|
store.set({ ...snapshot, kinds });
|
|
};
|
|
return { sendTest, sendTestSystem, requestPermission, setKindEnabled };
|
|
}
|
|
|
|
// src/client/ui/ToastLayer.js
|
|
var React3 = __toESM(require("react"), 1);
|
|
|
|
// src/client/ui/icons.js
|
|
var React = __toESM(require("react"), 1);
|
|
var h = React.createElement;
|
|
function BellIcon({ size = 16 }) {
|
|
return h(
|
|
"svg",
|
|
{
|
|
width: size,
|
|
height: size,
|
|
viewBox: "0 0 16 16",
|
|
fill: "none",
|
|
xmlns: "http://www.w3.org/2000/svg",
|
|
"aria-hidden": true
|
|
},
|
|
h("path", {
|
|
d: "M8 2.35C5.7 2.35 3.83 4.22 3.83 6.52V9.02L3.06 11.1C2.99 11.29 3.13 11.48 3.33 11.48H12.67C12.87 11.48 13.01 11.29 12.94 11.1L12.17 9.02V6.52C12.17 4.22 10.3 2.35 8 2.35Z",
|
|
stroke: "currentColor",
|
|
strokeWidth: 1.2,
|
|
strokeLinejoin: "round"
|
|
}),
|
|
h("path", {
|
|
d: "M6.3 12.75C6.52 13.66 7.2 14.28 8 14.28C8.8 14.28 9.48 13.66 9.7 12.75",
|
|
stroke: "currentColor",
|
|
strokeWidth: 1.2,
|
|
strokeLinecap: "round"
|
|
})
|
|
);
|
|
}
|
|
function ShieldIcon({ size = 16 }) {
|
|
return h(
|
|
"svg",
|
|
{
|
|
width: size,
|
|
height: size,
|
|
viewBox: "0 0 16 16",
|
|
fill: "none",
|
|
xmlns: "http://www.w3.org/2000/svg",
|
|
"aria-hidden": true
|
|
},
|
|
h("path", {
|
|
d: "M8 1.9 13.1 3.7V7.9C13.1 11 11 13.3 8 14.2C5 13.3 2.9 11 2.9 7.9V3.7L8 1.9Z",
|
|
stroke: "currentColor",
|
|
strokeWidth: 1.2,
|
|
strokeLinejoin: "round"
|
|
}),
|
|
h("path", {
|
|
d: "M5.9 8.05L7.35 9.5L10.15 6.6",
|
|
stroke: "currentColor",
|
|
strokeWidth: 1.2,
|
|
strokeLinecap: "round",
|
|
strokeLinejoin: "round"
|
|
})
|
|
);
|
|
}
|
|
function QuestionIcon({ size = 16 }) {
|
|
return h(
|
|
"svg",
|
|
{
|
|
width: size,
|
|
height: size,
|
|
viewBox: "0 0 16 16",
|
|
fill: "none",
|
|
xmlns: "http://www.w3.org/2000/svg",
|
|
"aria-hidden": true
|
|
},
|
|
h("circle", { cx: 8, cy: 8, r: 6.15, stroke: "currentColor", strokeWidth: 1.2 }),
|
|
h("path", {
|
|
d: "M6.35 6.35C6.35 5.44 7.09 4.7 8 4.7C8.91 4.7 9.65 5.44 9.65 6.35C9.65 7.75 8 7.7 8 9.15",
|
|
stroke: "currentColor",
|
|
strokeWidth: 1.2,
|
|
strokeLinecap: "round"
|
|
}),
|
|
h("circle", { cx: 8, cy: 11.3, r: 0.85, fill: "currentColor" })
|
|
);
|
|
}
|
|
function CloseIcon({ size = 12 }) {
|
|
return h(
|
|
"svg",
|
|
{
|
|
width: size,
|
|
height: size,
|
|
viewBox: "0 0 16 16",
|
|
fill: "none",
|
|
xmlns: "http://www.w3.org/2000/svg",
|
|
"aria-hidden": true
|
|
},
|
|
h("path", { d: "M2.5 2.5L13.5 13.5", stroke: "currentColor", strokeWidth: 1.4 }),
|
|
h("path", { d: "M13.5 2.5L2.5 13.5", stroke: "currentColor", strokeWidth: 1.4 })
|
|
);
|
|
}
|
|
function KindIcon({ kind, size }) {
|
|
if (kind === "approval") return h(ShieldIcon, { size });
|
|
if (kind === "question") return h(QuestionIcon, { size });
|
|
return h(BellIcon, { size });
|
|
}
|
|
|
|
// src/client/ui/hooks.js
|
|
var React2 = __toESM(require("react"), 1);
|
|
function useOwnStore(source) {
|
|
return React2.useSyncExternalStore(source.subscribe, source.getSnapshot, source.getSnapshot);
|
|
}
|
|
function useWindowState() {
|
|
const subscribe = React2.useCallback((listener) => {
|
|
try {
|
|
window.addEventListener("focus", listener);
|
|
window.addEventListener("blur", listener);
|
|
document.addEventListener("visibilitychange", listener);
|
|
} catch {
|
|
return () => {
|
|
};
|
|
}
|
|
return () => {
|
|
window.removeEventListener("focus", listener);
|
|
window.removeEventListener("blur", listener);
|
|
document.removeEventListener("visibilitychange", listener);
|
|
};
|
|
}, []);
|
|
return React2.useSyncExternalStore(subscribe, () => !windowIsAway(), () => true);
|
|
}
|
|
function seatTranslator(props, fallback) {
|
|
return typeof props.t === "function" ? props.t : fallback;
|
|
}
|
|
|
|
// src/client/ui/styles.js
|
|
var CSS = `
|
|
.dsn-stack{position:fixed;top:40px;right:24px;z-index:1100;display:flex;flex-direction:column;align-items:flex-end;gap:8px;pointer-events:none}
|
|
@media (max-width:520px){.dsn-stack{right:12px}}
|
|
.dsn-toast{box-sizing:border-box;pointer-events:auto;display:flex;align-items:flex-start;gap:10px;width:max-content;max-width:min(420px,calc(100vw - 48px));padding:12px 12px 12px 16px;border-radius:var(--dsw-radius-lg);background:var(--dsw-alias-toast-bg);color:var(--dsw-alias-toast-label);font-size:14px;line-height:22px;box-shadow:var(--dsw-shadow-lv3);animation:dsn-toast-in 160ms ease-out}
|
|
@keyframes dsn-toast-in{from{opacity:0;transform:translateX(8px)}to{opacity:1;transform:translateX(0)}}
|
|
@media (prefers-reduced-motion: reduce){.dsn-toast{animation:none}}
|
|
.dsn-toast-icon{display:grid;place-items:center;flex:none;height:22px;color:var(--dsw-alias-state-warn-label)}
|
|
.dsn-toast-icon.is-completion{color:var(--dsw-alias-state-success-primary)}
|
|
.dsn-toast-icon.is-question{color:var(--dsw-alias-brand-primary)}
|
|
.dsn-toast-text{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
|
|
.dsn-toast-title{font-weight:500;font-size:14px;line-height:22px}
|
|
.dsn-toast-desc{color:var(--dsw-alias-toast-label);opacity:.8;font-size:13px;line-height:20px;overflow-wrap:break-word}
|
|
.dsn-toast-actions{display:flex;align-items:center;gap:2px;flex:none}
|
|
.dsn-toast-action{display:inline-flex;align-items:center;height:28px;padding:0 8px;border:0;border-radius:var(--dsw-radius-sm);background:none;color:var(--dsw-static-deepseek-400);font:inherit;font-size:13px;line-height:20px;cursor:pointer}
|
|
.dsn-toast-action:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
.dsn-toast-action:focus-visible{outline:var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:2px}
|
|
.dsn-toast-close{flex:none;display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border:0;border-radius:var(--dsw-radius-sm);background:transparent;color:var(--dsw-alias-toast-label);opacity:.8;cursor:pointer}
|
|
.dsn-toast-close:hover{background:var(--dsw-alias-interactive-bg-hover);opacity:1}
|
|
.dsn-toast-close:focus-visible{outline:var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:2px}
|
|
.dsn-sections{display:flex;flex-direction:column;gap:24px}
|
|
.dsn-section{display:flex;flex-direction:column;gap:10px}
|
|
.dsn-section-title{margin:0;color:var(--dsw-alias-label-primary);font-size:14px;font-weight:500;line-height:22px}
|
|
.dsn-section-desc{margin:0;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}
|
|
.dsn-row{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:10px 0;border-bottom:.5px solid var(--dsw-alias-border-l2)}
|
|
.dsn-row:last-child{border-bottom:0}
|
|
.dsn-row-text{display:flex;flex-direction:column;gap:2px;min-width:0}
|
|
.dsn-row-title{color:var(--dsw-alias-label-primary);font-size:14px;line-height:22px}
|
|
.dsn-row-desc{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}
|
|
.dsn-status{margin:0;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}
|
|
.dsn-status-error{color:var(--dsw-alias-state-error-primary)}
|
|
.dsn-switch{box-sizing:border-box;position:relative;flex:0 0 auto;width:36px;height:20px;padding:2px;border:0;border-radius:999px;background:var(--dsw-alias-border-l3);cursor:pointer}
|
|
.dsn-switch[aria-checked='true']{background:var(--dsw-alias-brand-primary)}
|
|
.dsn-switch:disabled{cursor:default;opacity:.5}
|
|
.dsn-switch:focus-visible{outline:var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:2px}
|
|
.dsn-thumb{display:block;width:16px;height:16px;border-radius:50%;background:var(--dsw-alias-label-primary-foreground);transition:transform 120ms ease}
|
|
.dsn-switch[aria-checked='false'] .dsn-thumb{background:var(--dsw-alias-switch-thumb)}
|
|
.dsn-switch[aria-checked='true'] .dsn-thumb{transform:translateX(16px)}
|
|
.dsn-button{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;gap:4px;height:36px;padding:0 14px;border:.5px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-md);cursor:pointer;font-size:14px;line-height:22px;color:var(--dsw-alias-label-primary);background:transparent;flex:none}
|
|
.dsn-button:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}
|
|
.dsn-button:disabled{cursor:not-allowed;opacity:.4}
|
|
.dsn-button:focus-visible{outline:var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:2px}
|
|
`;
|
|
|
|
// src/client/ui/ToastLayer.js
|
|
var h2 = React3.createElement;
|
|
function ToastLayer(props) {
|
|
const snapshot = useOwnStore(props.store);
|
|
const toasts = snapshot.toasts;
|
|
const tr = seatTranslator(props, props.fallbackT);
|
|
React3.useEffect(() => {
|
|
if (toasts.length === 0) return void 0;
|
|
const onKeyDown = (event) => {
|
|
if (event.key !== "Escape" || event.defaultPrevented) return;
|
|
const top = toasts[0];
|
|
if (top === void 0) return;
|
|
event.preventDefault();
|
|
props.dismiss(top.id);
|
|
};
|
|
document.addEventListener("keydown", onKeyDown);
|
|
return () => {
|
|
document.removeEventListener("keydown", onKeyDown);
|
|
};
|
|
}, [toasts, props]);
|
|
if (toasts.length === 0) return null;
|
|
return h2(
|
|
React3.Fragment,
|
|
null,
|
|
h2("style", null, CSS),
|
|
h2(
|
|
"div",
|
|
{ className: "dsn-stack", role: "region", "aria-live": "polite" },
|
|
toasts.map((toast) => h2(
|
|
"div",
|
|
{
|
|
key: toast.id,
|
|
className: "dsn-toast",
|
|
role: toast.kind === "completion" ? "status" : "alert",
|
|
onMouseEnter: () => props.hold(toast.id),
|
|
onMouseLeave: () => props.arm(toast.id)
|
|
},
|
|
h2("span", { className: `dsn-toast-icon is-${toast.kind}` }, h2(KindIcon, { kind: toast.kind, size: 16 })),
|
|
h2(
|
|
"div",
|
|
{ className: "dsn-toast-text" },
|
|
h2("div", { className: "dsn-toast-title" }, toast.title),
|
|
h2("div", { className: "dsn-toast-desc" }, toast.body)
|
|
),
|
|
h2(
|
|
"div",
|
|
{ className: "dsn-toast-actions" },
|
|
h2("button", {
|
|
type: "button",
|
|
className: "dsn-toast-action",
|
|
onClick: () => {
|
|
props.dismiss(toast.id);
|
|
props.open(toast.sessionId);
|
|
}
|
|
}, tr("toast.view")),
|
|
h2("button", {
|
|
type: "button",
|
|
className: "dsn-toast-close",
|
|
"aria-label": tr("toast.dismiss"),
|
|
onClick: () => props.dismiss(toast.id)
|
|
}, h2(CloseIcon, { size: 14 }))
|
|
)
|
|
))
|
|
)
|
|
);
|
|
}
|
|
|
|
// src/client/ui/ConfigSection.js
|
|
var React5 = __toESM(require("react"), 1);
|
|
|
|
// src/client/ui/KindRow.js
|
|
var React4 = __toESM(require("react"), 1);
|
|
var h3 = React4.createElement;
|
|
function KindRow(props) {
|
|
return h3(
|
|
"div",
|
|
{ className: "dsn-row" },
|
|
h3(
|
|
"div",
|
|
{ className: "dsn-row-text" },
|
|
h3("div", { className: "dsn-row-title" }, props.label)
|
|
),
|
|
h3("button", {
|
|
type: "button",
|
|
role: "switch",
|
|
className: "dsn-switch",
|
|
"aria-checked": props.checked,
|
|
"aria-label": props.label,
|
|
disabled: props.disabled,
|
|
onClick: props.onToggle
|
|
}, h3("span", { className: "dsn-thumb" }))
|
|
);
|
|
}
|
|
|
|
// src/client/ui/ConfigSection.js
|
|
var h4 = React5.createElement;
|
|
function ConfigSection(props) {
|
|
const snapshot = useOwnStore(props.store);
|
|
const tr = seatTranslator(props, props.fallbackT);
|
|
if (props.view === "summary") return h4(React5.Fragment, null, tr("settings.description"));
|
|
const permission = snapshot.permission;
|
|
const supported = permission !== "unsupported";
|
|
const failed = permission === "denied" || snapshot.promptError !== "";
|
|
const statusKey = permission === "granted" ? "settings.permission.granted" : permission === "denied" ? "settings.permission.denied" : supported ? "settings.permission.default" : "settings.permission.unsupported";
|
|
const permissionText = snapshot.promptError === "" ? `${tr(statusKey)}${permission === "denied" || !supported ? ` · ${tr(`settings.permission.hint.${props.platform}`)}` : ""}` : snapshot.promptError;
|
|
const health = snapshot.health;
|
|
const healthText = health.state === "watching" ? tr("settings.health.watching") : health.state === "noHooks" ? tr("settings.health.noHooks") : health.state === "error" ? tr("settings.health.error", { message: health.message }) : tr("settings.health.starting");
|
|
const healthFailed = health.state !== "watching" && health.state !== "starting";
|
|
return h4(
|
|
React5.Fragment,
|
|
null,
|
|
h4("style", null, CSS),
|
|
h4(
|
|
"div",
|
|
{ className: "dsn-sections" },
|
|
// The page header already shows this bundle's title and description, so
|
|
// this section opens with what the header cannot say: how alerts are
|
|
// carried, and what this form changes.
|
|
h4(
|
|
"section",
|
|
{ className: "dsn-section" },
|
|
h4("p", { className: "dsn-section-desc" }, tr("config.intro"))
|
|
),
|
|
h4(
|
|
"section",
|
|
{ className: "dsn-section" },
|
|
h4("h3", { className: "dsn-section-title" }, tr("config.section.kinds")),
|
|
KINDS.map((kind) => h4(KindRow, {
|
|
key: kind,
|
|
label: tr(`settings.kind.${kind}`),
|
|
checked: snapshot.kinds[kind] === true,
|
|
disabled: false,
|
|
onToggle: () => props.setKind(kind, snapshot.kinds[kind] !== true)
|
|
})),
|
|
h4("p", { className: "dsn-section-desc" }, tr("config.saveNote"))
|
|
),
|
|
h4(
|
|
"section",
|
|
{ className: "dsn-section" },
|
|
h4("h3", { className: "dsn-section-title" }, tr("config.section.permission")),
|
|
h4(
|
|
"div",
|
|
{ className: "dsn-row" },
|
|
h4(
|
|
"div",
|
|
{ className: "dsn-row-text" },
|
|
h4("p", {
|
|
className: `dsn-status${failed ? " dsn-status-error" : ""}`,
|
|
role: failed ? "alert" : "status"
|
|
}, permissionText)
|
|
),
|
|
h4("button", {
|
|
type: "button",
|
|
className: "dsn-button",
|
|
disabled: !supported,
|
|
onClick: permission === "granted" ? props.testSystem : props.ask
|
|
}, tr(!supported ? "settings.action.unsupported" : permission === "granted" ? "settings.action.testSystem" : "settings.action.allow"))
|
|
),
|
|
h4("p", { className: "dsn-section-desc" }, tr("config.testHint"))
|
|
),
|
|
h4(
|
|
"section",
|
|
{ className: "dsn-section" },
|
|
h4("h3", { className: "dsn-section-title" }, tr("config.section.status")),
|
|
h4(
|
|
"div",
|
|
{ className: "dsn-row" },
|
|
h4(
|
|
"div",
|
|
{ className: "dsn-row-text" },
|
|
h4("p", { className: "dsn-status" }, `${tr("config.diag.window")}: ${tr(windowIsAway() ? "config.diag.away" : "config.diag.inFront")}`),
|
|
h4("p", {
|
|
className: `dsn-status${healthFailed ? " dsn-status-error" : ""}`,
|
|
role: healthFailed ? "alert" : "status"
|
|
}, healthText),
|
|
h4("p", { className: "dsn-status" }, props.copy.describeDelivery(snapshot.lastDelivery, tr)),
|
|
h4("p", { className: "dsn-status" }, props.copy.describeReplay(props.replayCount(), tr))
|
|
),
|
|
h4("button", {
|
|
type: "button",
|
|
className: "dsn-button",
|
|
onClick: props.testAny
|
|
}, tr("settings.action.testAny"))
|
|
)
|
|
)
|
|
)
|
|
);
|
|
}
|
|
|
|
// src/client/ui/NotifyObserver.js
|
|
var React6 = __toESM(require("react"), 1);
|
|
function NotifyObserver(props) {
|
|
useWindowState();
|
|
const useSessions = props.useSessions;
|
|
const useSessionStatus = props.useSessionStatus;
|
|
const select = React6.useCallback((snapshot) => snapshot, []);
|
|
const list = typeof useSessions === "function" ? useSessions(select) : void 0;
|
|
const status = typeof useSessionStatus === "function" ? useSessionStatus(select) : void 0;
|
|
React6.useEffect(() => {
|
|
try {
|
|
if (typeof props.useSessions !== "function" || typeof props.useSessionStatus !== "function") {
|
|
props.observation.reportHealth({ state: "noHooks", message: "" });
|
|
return;
|
|
}
|
|
if (list === void 0 || status === void 0) {
|
|
props.observation.reportHealth({ state: "starting", message: "" });
|
|
return;
|
|
}
|
|
props.observation.observe(list, status);
|
|
props.observation.reportHealth({ state: "watching", message: "" });
|
|
} catch (error) {
|
|
props.observation.reportHealth({ state: "error", message: text(error) });
|
|
}
|
|
});
|
|
return null;
|
|
}
|
|
|
|
// src/client/plugin.js
|
|
var inject = ["slots", "locale"];
|
|
function apply(ctx) {
|
|
const locale = ctx.get("locale");
|
|
const t = createTranslator(locale);
|
|
const store = createStore({
|
|
permission: notificationPermission(),
|
|
kinds: readStoredKinds(),
|
|
toasts: [],
|
|
promptError: "",
|
|
health: { state: "starting", message: "" },
|
|
lastDelivery: null
|
|
});
|
|
const live = { list: void 0, status: void 0 };
|
|
const runtime = {};
|
|
let disposed = false;
|
|
const publish = () => {
|
|
if (!disposed) store.set({ ...store.getSnapshot() });
|
|
};
|
|
const openSession = (sessionId) => {
|
|
try {
|
|
ctx.get("uiWorkspace")?.openSession?.(sessionId);
|
|
} catch (error) {
|
|
report(`could not open "${sessionId}": ${text(error)}`);
|
|
}
|
|
};
|
|
const copy = createCopy(t);
|
|
const toasts = createToastStack({ store, copy });
|
|
const replay = createReplay({
|
|
store,
|
|
publish,
|
|
showToast: toasts.showToast,
|
|
recordDelivery: (outcome, candidate, monitor) => runtime.delivery?.recordDelivery(outcome, candidate, monitor)
|
|
});
|
|
const system = createSystemChannel({
|
|
store,
|
|
copy,
|
|
notify: report,
|
|
openSession,
|
|
dropReplay: replay.dropReplay
|
|
});
|
|
const delivery = createDelivery({ store, copy, live, toasts, system, replay });
|
|
runtime.delivery = delivery;
|
|
const observation = createObservation({ store, live, queue: delivery.queue });
|
|
const actions = createActions({ store, t, delivery, system, toasts, publish });
|
|
const installBridges = () => {
|
|
const onBlur = () => {
|
|
delivery.resetThrottle();
|
|
};
|
|
const onReturn = () => {
|
|
delivery.resetThrottle();
|
|
replay.flushReplay();
|
|
};
|
|
const onVisibilityChange = () => {
|
|
if (!windowIsAway()) onReturn();
|
|
};
|
|
try {
|
|
window.addEventListener("focus", onReturn);
|
|
window.addEventListener("blur", onBlur);
|
|
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
} catch {
|
|
}
|
|
return () => {
|
|
try {
|
|
window.removeEventListener("focus", onReturn);
|
|
window.removeEventListener("blur", onBlur);
|
|
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
} catch {
|
|
}
|
|
delivery.dispose();
|
|
toasts.dispose();
|
|
system.dispose();
|
|
replay.dispose();
|
|
};
|
|
};
|
|
ctx.effect(() => registerDictionaries(locale), "session-notify: dictionaries");
|
|
ctx.effect(() => installBridges(), "session-notify: window focus");
|
|
ctx.effect(() => () => {
|
|
disposed = true;
|
|
}, "session-notify: disposal");
|
|
ctx.slots.inject("shell.overlay", () => ctx.slots.register({
|
|
name: "shell.overlay",
|
|
id: `${NS}-toasts`,
|
|
order: 100,
|
|
locale: NS,
|
|
inject: () => ({
|
|
store,
|
|
fallbackT: t,
|
|
dismiss: toasts.closeToast,
|
|
arm: toasts.armToast,
|
|
hold: toasts.holdToast,
|
|
open: (sessionId) => {
|
|
focusWindow();
|
|
openSession(sessionId);
|
|
}
|
|
})
|
|
}, ToastLayer));
|
|
ctx.slots.inject("plugins.bundle.config", () => ctx.slots.register({
|
|
name: "plugins.bundle.config",
|
|
key: PACKAGE_NAME,
|
|
locale: NS,
|
|
inject: () => ({
|
|
store,
|
|
fallbackT: t,
|
|
copy,
|
|
platform: platformKey(),
|
|
replayCount: replay.size,
|
|
ask: actions.requestPermission,
|
|
testAny: actions.sendTest,
|
|
testSystem: actions.sendTestSystem,
|
|
setKind: actions.setKindEnabled
|
|
})
|
|
}, ConfigSection));
|
|
ctx.slots.inject("shell.overlay", () => ctx.slots.register({
|
|
name: "shell.overlay",
|
|
id: `${NS}-watch`,
|
|
order: 101,
|
|
inject: () => ({ observation })
|
|
}, NotifyObserver));
|
|
}
|
|
|
|
return module.exports
|
|
},
|
|
})
|
|
|