// 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.watch": "会话监听", "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": "待补发提醒", "config.diag.replayValue": "{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.watch": "Session watch", "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": "Replay", "config.diag.replayValue": "{count} waiting", "config.diag.replayNone": "None" }; // 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 = ''; 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.pendingKind === "plan-review") { return { kind: "question", title: t("notification.question"), body: t("body.planReview", { title }) }; } if (candidate.kind === "question") { const question = candidate.detail; return { kind: "question", title: t("notification.question"), body: question === "" ? t("body.question", { title }) : `${title} · ${question}` }; } const tool = candidate.detail; return { kind: "approval", title: t("notification.approval"), body: tool === "" ? t("body.approvalPlain", { title }) : t("body.approval", { title, tool }) }; }; const deliveryFacts = (last, tr) => { if (last === null || last === void 0) return null; 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 { at: last.at, outcome, title: last.title ?? "" }; }; const replayValue = (count, tr) => count === 0 ? tr("config.diag.replayNone") : tr("config.diag.replayValue", { count }); return { copyFor, deliveryFacts, replayValue }; } // 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 stillOwed(candidate, live) { if (candidate.test === true) return true; if (candidate.pendingKind !== "") { const sessionStatus = live.status?.get?.(candidate.sessionId); if (sessionStatus === void 0) return true; return servedPendingKind(sessionStatus.pendingInteraction) === candidate.pendingKind; } const list = live.list; if (list === void 0) return true; const summary = list.byId?.[candidate.sessionId]; if (summary === void 0) return true; const running = live.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 false; lastDelivery = now; } if (!windowIsAway()) { if (isOnScreen(candidate.sessionId, live.list)) return true; toasts.showToast(candidate); recordDelivery("popup", candidate); return true; } 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 true; } toasts.showToast(candidate); recordDelivery(`system-refused-${result.outcome}`, candidate); return true; }; const flushSettle = () => { settleTimer = 0; const candidate = pending.shift(); if (candidate !== void 0 && stillOwed(candidate, live)) { const delivered = deliver(candidate, false); if (delivered === false && candidate.pendingKind !== "") pending.push(candidate); } 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-bg-layer-2);--dsw-elevation-stroke-color:var(--dsw-alias-border-l1);box-shadow:var(--dsw-elevation-prominent);color:var(--dsw-alias-label-primary);font-size:14px;line-height:22px;animation:dsn-toast-in var(--ds-transition-duration,.2s) var(--ds-ease-in-out,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-state-business-primary)} .dsn-toast-text{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px} .dsn-toast-title{color:var(--dsw-alias-label-primary);font-weight:500;font-size:14px;line-height:22px} .dsn-toast-desc{color:var(--dsw-alias-label-secondary);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-alias-state-business-primary);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-label-tertiary);cursor:pointer} .dsn-toast-close:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)} .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-page{display:flex;flex-direction:column;gap:28px;max-width:560px} .dsn-lede{margin:0;color:var(--dsw-alias-label-secondary);font-size:13px;line-height:20px} .dsn-group{display:flex;flex-direction:column;gap:2px} .dsn-group-title{margin:0 0 8px;color:var(--dsw-alias-label-primary);font-size:13px;font-weight:600;line-height:20px} .dsn-group-note{margin:8px 0 0;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px} .dsn-list{display:flex;flex-direction:column} .dsn-row{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:10px 0} .dsn-row+.dsn-row{border-top:.5px solid var(--dsw-alias-border-l2)} .dsn-row.is-action{justify-content:flex-end;padding:12px 0 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-sub{margin:0;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px} .dsn-state{display:inline-flex;align-items:center;gap:8px;color:var(--dsw-alias-label-primary);font-size:14px;line-height:22px} .dsn-dot{flex:none;width:6px;height:6px;border-radius:50%;background:var(--dsw-alias-state-success-primary)} .dsn-dot.is-warn{background:var(--dsw-alias-state-warn-label)} .dsn-dot.is-error{background:var(--dsw-alias-state-error-primary)} .dsn-dot.is-muted{background:var(--dsw-alias-border-l3)} .dsn-facts{display:grid;grid-template-columns:auto minmax(0,1fr);gap:8px 14px;margin:0;min-width:0} .dsn-fact-key{margin:0;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;white-space:nowrap} .dsn-fact{display:flex;flex-direction:column;gap:2px;margin:0;min-width:0} .dsn-fact-value{margin:0;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;overflow-wrap:anywhere} .dsn-fact-value.is-error{color:var(--dsw-alias-state-error-primary)} .dsn-fact-title{margin:0;color:var(--dsw-alias-label-primary);font-size:12px;line-height:18px;overflow-wrap:anywhere} .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:32px;padding:0 12px;border:.5px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-md);cursor:pointer;font-size:13px;line-height:20px;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} .dsn-button.is-primary{border-color:transparent;background:var(--dsw-alias-brand-primary);color:var(--dsw-alias-label-primary-foreground)} .dsn-button.is-primary:hover:not(:disabled){background:var(--dsw-alias-brand-primary);opacity:.88} `; // 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 promptFailed = snapshot.promptError !== ""; const permissionText = promptFailed ? snapshot.promptError : tr(statusKey); const dotClass = permission === "granted" ? "" : failed ? " is-error" : supported ? " is-warn" : " is-muted"; const permissionNote = promptFailed ? "" : permission === "denied" || !supported ? tr(`settings.permission.hint.${props.platform}`) : permission === "granted" ? tr("config.testHint") : tr("settings.permission.hint.plain"); 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"; const delivery = props.copy.deliveryFacts(snapshot.lastDelivery, tr); return h4( React5.Fragment, null, h4("style", null, CSS), h4( "div", { className: "dsn-page" }, // The page header already shows this bundle's title and description, so this // lede says what the header cannot: how an alert travels, and that the // channel is chosen at delivery time. h4("p", { className: "dsn-lede" }, tr("config.intro")), h4( "section", { className: "dsn-group" }, h4("h3", { className: "dsn-group-title" }, tr("config.section.kinds")), h4( "div", { className: "dsn-list" }, 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-group-note" }, tr("config.saveNote")) ), h4( "section", { className: "dsn-group" }, h4("h3", { className: "dsn-group-title" }, tr("config.section.permission")), h4( "div", { className: "dsn-list" }, h4( "div", { className: "dsn-row" }, h4( "div", { className: "dsn-row-text" }, h4( "span", { className: "dsn-state", role: failed ? "alert" : "status" }, h4("span", { className: `dsn-dot${dotClass}` }), permissionText ), h4("p", { className: "dsn-row-sub" }, permissionNote) ), h4("button", { type: "button", className: `dsn-button${supported && permission !== "granted" ? " is-primary" : ""}`, disabled: !supported, onClick: permission === "granted" ? props.testSystem : props.ask }, tr(!supported ? "settings.action.unsupported" : permission === "granted" ? "settings.action.testSystem" : "settings.action.allow")) ) ) ), h4( "section", { className: "dsn-group" }, h4("h3", { className: "dsn-group-title" }, tr("config.section.status")), h4( "dl", { className: "dsn-facts" }, h4("dt", { className: "dsn-fact-key" }, tr("config.diag.window")), h4( "dd", { className: "dsn-fact" }, h4("p", { className: "dsn-fact-value" }, tr(windowIsAway() ? "config.diag.away" : "config.diag.inFront")) ), h4("dt", { className: "dsn-fact-key" }, tr("config.diag.watch")), h4( "dd", { className: "dsn-fact" }, h4("p", { className: `dsn-fact-value${healthFailed ? " is-error" : ""}`, role: healthFailed ? "alert" : "status" }, healthText) ), h4("dt", { className: "dsn-fact-key" }, tr("config.diag.last")), h4( "dd", { className: "dsn-fact" }, delivery === null ? h4("p", { className: "dsn-fact-value" }, tr("config.diag.none")) : [ h4("p", { key: "line", className: "dsn-fact-value" }, `${delivery.at} · ${delivery.outcome}`), delivery.title === "" ? null : h4("p", { key: "title", className: "dsn-fact-title" }, delivery.title) ] ), h4("dt", { className: "dsn-fact-key" }, tr("config.diag.replay")), h4( "dd", { className: "dsn-fact" }, h4("p", { className: "dsn-fact-value" }, props.copy.replayValue(props.replayCount(), tr)) ) ), h4( "div", { className: "dsn-list" }, h4( "div", { className: "dsn-row is-action" }, 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 }, })