dsh-notify-me 1.7.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -256,6 +256,19 @@
256
256
  // through the Service Worker half of this same file; where that cannot
257
257
  // run, the toast simply falls back to its plain form.
258
258
  quickActions: true,
259
+ // Where the alert is drawn. The OS toast always lands on the primary
260
+ // display — Windows offers no setting (and Chromium no API) to aim a
261
+ // banner at another monitor — so a user whose DSH window lives on a
262
+ // second screen never sees one. This option draws the same alert inside
263
+ // the page instead: it appears on whatever screen the window is on, needs
264
+ // no notification permission, and can carry the approval buttons even on
265
+ // the desktop shell (whose dsh-app:// page cannot run the Service Worker
266
+ // that OS action buttons need). Independent of `toast`; neither replaces
267
+ // the other, so "both" is a valid setup.
268
+ overlay: false,
269
+ // Which corner of this window the overlay cards stack in. The screen
270
+ // follows the window — the page cannot aim it anywhere else either.
271
+ overlayCorner: "bottom-right", // bottom-right | bottom-left | top-right | top-left
259
272
  // External forwarding: mirror every alert the plugin actually delivers to
260
273
  // webhook endpoints (Discord / Bark / Gotify / custom), so a phone or
261
274
  // another box can tell the user to come back. Off by default — nothing
@@ -287,6 +300,12 @@
287
300
  function setConfigLocal(patch) {
288
301
  config = writeConfig(patch);
289
302
  applyTitle();
303
+ // Overlay re-apply: switching it off takes the cards down at once and a
304
+ // new corner moves the layer without waiting for the next alert.
305
+ try {
306
+ if (!config.overlay) clearOverlays();
307
+ else if (overlayLayer) overlayLayer.className = "dnm-ov-layer dnm-ov-" + overlayCorner();
308
+ } catch (e) {}
290
309
  return config;
291
310
  }
292
311
 
@@ -341,9 +360,10 @@
341
360
  sep: ":",
342
361
  approve: "同意",
343
362
  reject: "拒绝",
363
+ dismiss: "关闭这条提醒",
344
364
  testAttention: "这是一条测试提醒:DSH 需要你的操作",
345
365
  testDone: "测试提醒 · 回复完成示例",
346
- testApproval: "这是一条测试审批:点通知上的「同意 / 拒绝」按钮试试快捷裁决",
366
+ testApproval: "这是一条测试审批:点「同意 / 拒绝」试试快捷裁决",
347
367
  testApproved: "测试提醒 · 已模拟「同意」(仅测试,未改动任何会话)",
348
368
  testRejected: "测试提醒 · 已模拟「拒绝」(仅测试,未改动任何会话)",
349
369
  testExternalTitle: "外部通知测试",
@@ -367,9 +387,10 @@
367
387
  sep: ": ",
368
388
  approve: "Approve",
369
389
  reject: "Reject",
390
+ dismiss: "Dismiss this alert",
370
391
  testAttention: "This is a test alert: DSH is waiting for your input",
371
392
  testDone: "Test alert · reply-finished sample",
372
- testApproval: "This is a test approval: try quick decisions with the Approve / Reject buttons on the toast",
393
+ testApproval: "This is a test approval: try the Approve / Reject buttons to settle it",
373
394
  testApproved: "Test alert · simulated \"Approve\" (test only, nothing changed)",
374
395
  testRejected: "Test alert · simulated \"Reject\" (test only, nothing changed)",
375
396
  testExternalTitle: "External notification test",
@@ -639,6 +660,7 @@
639
660
  if (!rec) return false; // already answered elsewhere, or a stale toast
640
661
  delete pendingActions[key];
641
662
  if (!rec.test) closeNotificationTag(rec.tag);
663
+ closeOverlayForKey(key);
642
664
  if (rec.test) {
643
665
  // Test toasts settle nothing real; the feedback toast IS the proof that
644
666
  // the button clicked all the way through the worker bridge.
@@ -660,7 +682,10 @@
660
682
  // they are the only kind whose decision is a plain binary, and the only one
661
683
  // whose object answers answer('allowed-once' | 'rejected').
662
684
  function quickFor(key, it, sessionId) {
663
- if (!config.quickActions || !config.toast) return null;
685
+ // The overlay can settle approvals without a worker, so it counts as a
686
+ // button channel on its own — the desktop shell, where OS action buttons
687
+ // cannot render, gets them this way.
688
+ if (!config.quickActions || !(config.toast || config.overlay)) return null;
664
689
  if (!it || it.kind !== "approval" || typeof it.answer !== "function") return null;
665
690
  var q = {
666
691
  key: key,
@@ -734,6 +759,191 @@
734
759
  setTimeout(function () { n.close(); }, 15000);
735
760
  } catch (e) {}
736
761
  }
762
+
763
+ // ─────────────────── page overlay reminders ──────────────────────
764
+ // Windows draws notification banners on the primary display and exposes no
765
+ // way to aim them at another monitor; a user who keeps DSH on a second
766
+ // screen therefore never sees a toast. This overlay draws the same alert
767
+ // inside the page instead, so it lands on whatever screen the DSH window
768
+ // occupies. It also needs no notification permission, and — unlike OS
769
+ // action buttons, which require the Service Worker the desktop shell cannot
770
+ // run — it can carry the 同意/拒绝 buttons everywhere. Off by default;
771
+ // independent of the toast switch.
772
+ var OVERLAY_STYLE_ID = "dsh-notify-me/overlay";
773
+ var OVERLAY_ID = "dsh-notify-me-overlay";
774
+ var OVERLAY_MAX_CARDS = 4;
775
+ // An approval card is actionable and stays until it is answered or closed;
776
+ // the other kinds fade on the toast's own schedule (a toast closes at 15s).
777
+ var OVERLAY_TTL = { attention: 20000, done: 8000 };
778
+ var OVERLAY_CORNERS = ["bottom-right", "bottom-left", "top-right", "top-left"];
779
+ var OVERLAY_CSS = "" +
780
+ ".dnm-ov-layer{position:fixed;z-index:2147483000;display:flex;flex-direction:column;gap:10px;pointer-events:none;max-width:calc(100vw - 24px)}" +
781
+ ".dnm-ov-bottom-right{right:16px;bottom:16px;align-items:flex-end}" +
782
+ ".dnm-ov-bottom-left{left:16px;bottom:16px;align-items:flex-start}" +
783
+ ".dnm-ov-top-right{right:16px;top:16px;align-items:flex-end}" +
784
+ ".dnm-ov-top-left{left:16px;top:16px;align-items:flex-start}" +
785
+ ".dnm-ov{pointer-events:auto;width:320px;max-width:calc(100vw - 32px);box-sizing:border-box;border-radius:12px;padding:10px 12px 11px;display:flex;flex-direction:column;gap:7px;font-size:13px;line-height:19px;color:var(--dsw-alias-label-primary,#f3f4f6);background:var(--dsw-alias-bg-layer-3,rgba(30,32,36,.97));border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.35));border-left-width:3px;box-shadow:0 12px 32px rgba(0,0,0,.32);cursor:pointer;animation:dnmOvIn .16s ease-out}" +
786
+ ".dnm-ov-attention{border-left-color:var(--dsw-alias-state-warning-primary,#f59e0b)}" +
787
+ ".dnm-ov-done{border-left-color:var(--dsw-alias-state-success-primary,#22c55e)}" +
788
+ ".dnm-ov-head{display:flex;align-items:center;gap:7px}" +
789
+ ".dnm-ov-dot{flex:none;width:7px;height:7px;border-radius:50%;background:currentColor;opacity:.55}" +
790
+ ".dnm-ov-attention .dnm-ov-dot{color:var(--dsw-alias-state-warning-primary,#f59e0b);opacity:1}" +
791
+ ".dnm-ov-done .dnm-ov-dot{color:var(--dsw-alias-state-success-primary,#22c55e);opacity:1}" +
792
+ ".dnm-ov-title{flex:1;min-width:0;font-weight:600;font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}" +
793
+ ".dnm-ov-x{flex:none;border:0;background:transparent;color:inherit;opacity:.6;font-size:15px;line-height:15px;padding:0 2px;cursor:pointer;font-family:inherit}" +
794
+ ".dnm-ov-x:hover{opacity:1}" +
795
+ ".dnm-ov-body{color:var(--dsw-alias-label-secondary,rgba(220,222,226,.86));font-size:12px;line-height:18px;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden;word-break:break-word}" +
796
+ ".dnm-ov-actions{display:flex;gap:8px;margin-top:2px}" +
797
+ ".dnm-ov-btn{flex:1;border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.4));background:var(--dsw-alias-bg-layer-1,rgba(255,255,255,.06));color:inherit;border-radius:8px;padding:5px 10px;font-size:12px;font-family:inherit;cursor:pointer}" +
798
+ ".dnm-ov-btn:hover{background:var(--dsw-alias-bg-layer-2,rgba(255,255,255,.12))}" +
799
+ ".dnm-ov-approve{border-color:transparent;background:var(--dsw-alias-state-business-primary,#3b82f6);color:#fff}" +
800
+ ".dnm-ov-approve:hover{background:var(--dsw-alias-state-business-primary,#3b82f6);filter:brightness(1.06)}" +
801
+ ".dnm-ov-reject{color:inherit}" +
802
+ "@keyframes dnmOvIn{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}";
803
+ var overlayLayer = null;
804
+ var overlayCards = Object.create(null); // id -> { card, key, kind, timer }
805
+ var overlaySeq = 0;
806
+ function overlayCorner() {
807
+ return OVERLAY_CORNERS.indexOf(config.overlayCorner) === -1 ? "bottom-right" : config.overlayCorner;
808
+ }
809
+ function injectOverlayStyle() {
810
+ try {
811
+ if (typeof document === "undefined" || typeof document.createElement !== "function") return;
812
+ if (!document.head && !document.documentElement) return;
813
+ var sel = "style[data-plugin-css=" + JSON.stringify(OVERLAY_STYLE_ID) + "]";
814
+ var seen = false;
815
+ try { seen = !!(document.querySelector && document.querySelector(sel)); } catch (e) {}
816
+ if (seen) return;
817
+ var s = document.createElement("style");
818
+ s.dataset.plugin = "dsh-notify-me";
819
+ s.dataset.pluginCss = OVERLAY_STYLE_ID;
820
+ s.textContent = OVERLAY_CSS;
821
+ (document.head || document.documentElement).appendChild(s);
822
+ } catch (e) {}
823
+ }
824
+ // The fixed layer lives directly under <body> so no ancestor transform or
825
+ // stacking context in the host app can drag the cards away from the corner.
826
+ function ensureOverlayLayer() {
827
+ try {
828
+ if (typeof document === "undefined" || typeof document.createElement !== "function" || !document.body) return null;
829
+ injectOverlayStyle();
830
+ if (!overlayLayer || !overlayLayer.parentNode) {
831
+ // A reload of this bundle can leave a previous layer behind; the id is
832
+ // the only handle an HMR-free page gives us.
833
+ var old = null;
834
+ try { old = document.getElementById && document.getElementById(OVERLAY_ID); } catch (e) {}
835
+ if (old && old.parentNode) old.parentNode.removeChild(old);
836
+ overlayLayer = document.createElement("div");
837
+ overlayLayer.id = OVERLAY_ID;
838
+ overlayLayer.setAttribute("role", "status");
839
+ overlayLayer.setAttribute("aria-live", "polite");
840
+ document.body.appendChild(overlayLayer);
841
+ }
842
+ overlayLayer.className = "dnm-ov-layer dnm-ov-" + overlayCorner();
843
+ return overlayLayer;
844
+ } catch (e) { return null; }
845
+ }
846
+ function dismissOverlay(id) {
847
+ var rec = overlayCards[id];
848
+ if (!rec) return;
849
+ delete overlayCards[id];
850
+ if (rec.timer) { try { clearTimeout(rec.timer); } catch (e) {} }
851
+ try { if (rec.card && rec.card.parentNode) rec.card.parentNode.removeChild(rec.card); } catch (e) {}
852
+ }
853
+ // An approval that gets answered elsewhere (the in-page card, another
854
+ // toast, a session switch) must not leave a stale overlay behind.
855
+ function closeOverlayForKey(key) {
856
+ if (!key) return;
857
+ var ids = Object.keys(overlayCards);
858
+ for (var i = 0; i < ids.length; i++) {
859
+ if (overlayCards[ids[i]].key === key) dismissOverlay(ids[i]);
860
+ }
861
+ }
862
+ function clearOverlays() {
863
+ var ids = Object.keys(overlayCards);
864
+ for (var i = 0; i < ids.length; i++) dismissOverlay(ids[i]);
865
+ if (overlayLayer) {
866
+ try { if (overlayLayer.parentNode) overlayLayer.parentNode.removeChild(overlayLayer); } catch (e) {}
867
+ overlayLayer = null;
868
+ }
869
+ }
870
+ function overlayCount() { return Object.keys(overlayCards).length; }
871
+ // Same inputs as showToast: `quick` is the approval record built by
872
+ // quickFor(), and it is what lets an overlay settle the request in place.
873
+ function showOverlay(kind, title, body, sessionId, quick) {
874
+ if (!config.overlay) return false;
875
+ var layer = ensureOverlayLayer();
876
+ if (!layer) return false;
877
+ var d = texts();
878
+ try {
879
+ var id = "ov" + (++overlaySeq);
880
+ var card = document.createElement("div");
881
+ card.className = "dnm-ov dnm-ov-" + (kind === "attention" ? "attention" : "done");
882
+ card.setAttribute("role", kind === "attention" ? "alert" : "status");
883
+ var head = document.createElement("div");
884
+ head.className = "dnm-ov-head";
885
+ var dot = document.createElement("span");
886
+ dot.className = "dnm-ov-dot";
887
+ var label = document.createElement("span");
888
+ label.className = "dnm-ov-title";
889
+ label.textContent = String(title == null ? "" : title);
890
+ var close = document.createElement("button");
891
+ close.type = "button";
892
+ close.className = "dnm-ov-x";
893
+ close.setAttribute("aria-label", d.dismiss);
894
+ close.textContent = "×";
895
+ close.onclick = function (ev) {
896
+ if (ev && ev.stopPropagation) ev.stopPropagation();
897
+ dismissOverlay(id);
898
+ };
899
+ head.appendChild(dot);
900
+ head.appendChild(label);
901
+ head.appendChild(close);
902
+ card.appendChild(head);
903
+ if (body) {
904
+ var bodyEl = document.createElement("div");
905
+ bodyEl.className = "dnm-ov-body";
906
+ bodyEl.textContent = String(body);
907
+ card.appendChild(bodyEl);
908
+ }
909
+ if (quick && typeof quick.key === "string") {
910
+ var actions = document.createElement("div");
911
+ actions.className = "dnm-ov-actions";
912
+ var actionBtn = function (cls, text, outcome) {
913
+ var b = document.createElement("button");
914
+ b.type = "button";
915
+ b.className = "dnm-ov-btn " + cls;
916
+ b.textContent = text;
917
+ b.onclick = function (ev) {
918
+ if (ev && ev.stopPropagation) ev.stopPropagation();
919
+ try { decidePending(quick.key, outcome); } catch (e) {}
920
+ dismissOverlay(id);
921
+ };
922
+ return b;
923
+ };
924
+ actions.appendChild(actionBtn("dnm-ov-approve", d.approve, "allowed-once"));
925
+ actions.appendChild(actionBtn("dnm-ov-reject", d.reject, "rejected"));
926
+ card.appendChild(actions);
927
+ }
928
+ // A click on the card body opens the conversation, exactly like a toast
929
+ // body click — and here it is an ordinary page gesture, so it reaches the
930
+ // desktop's dsh:// raise path even though a worker click would not.
931
+ card.onclick = function () {
932
+ if (config.autoFocus) focusSession(sessionId || null);
933
+ dismissOverlay(id);
934
+ };
935
+ layer.appendChild(card);
936
+ var rec = { card: card, key: (quick && quick.key) ? quick.key : null, kind: kind, timer: null };
937
+ overlayCards[id] = rec;
938
+ var ids = Object.keys(overlayCards);
939
+ for (var i = 0; i < ids.length - OVERLAY_MAX_CARDS; i++) dismissOverlay(ids[i]);
940
+ if (!(quick && typeof quick.key === "string")) {
941
+ var ttl = OVERLAY_TTL[kind === "attention" ? "attention" : "done"];
942
+ if (ttl) rec.timer = setTimeout(function () { dismissOverlay(id); }, ttl);
943
+ }
944
+ return true;
945
+ } catch (e) { return false; }
946
+ }
737
947
  // Sound: WebAudio beeps (no asset needed). The context only runs after a
738
948
  // user gesture; we unlock it on the first pointer/key event.
739
949
  var audioCtx = null;
@@ -1044,7 +1254,10 @@
1044
1254
  var rec = pendingActions[key];
1045
1255
  if (rec) {
1046
1256
  delete pendingActions[key];
1047
- if (!rec.test) closeNotificationTag(rec.tag);
1257
+ if (!rec.test) {
1258
+ closeNotificationTag(rec.tag);
1259
+ closeOverlayForKey(key);
1260
+ }
1048
1261
  }
1049
1262
  if (!attentionKeysMarked[key]) return;
1050
1263
  delete attentionKeysMarked[key];
@@ -1084,6 +1297,7 @@
1084
1297
  function deliver(kind, title, body, sessionId, quick) {
1085
1298
  if (!config.enabled) return;
1086
1299
  showToast(kind, title, body, sessionId, quick);
1300
+ showOverlay(kind, title, body, sessionId, quick);
1087
1301
  if (kind === "attention") {
1088
1302
  playAttentionSound();
1089
1303
  } else {
@@ -1723,7 +1937,7 @@
1723
1937
  });
1724
1938
  } catch (e) {}
1725
1939
  // keep in sync with package.json "version" on every release
1726
- api.version = "1.7.0";
1940
+ api.version = "1.9.0";
1727
1941
  api.setConfig = function (patch) { return setConfigLocal(patch); };
1728
1942
  api.resetConfig = function () {
1729
1943
  try {
@@ -1791,6 +2005,14 @@
1791
2005
  customSoundDone: !!readSound("done"),
1792
2006
  customSoundAttention: !!readSound("attention"),
1793
2007
  quickActions: config.quickActions,
2008
+ // in-page overlay: the alert channel that follows the DSH window to
2009
+ // whatever display it sits on, instead of the OS toast's fixed primary
2010
+ // display. cards is the live stack, corner the configured anchor.
2011
+ overlay: {
2012
+ enabled: !!config.overlay,
2013
+ corner: overlayCorner(),
2014
+ cards: overlayCount()
2015
+ },
1794
2016
  // external webhook forwarding: counts come from externalStat (tallied
1795
2017
  // on every send), targets is the raw vs validated count.
1796
2018
  external: {
@@ -1837,9 +2059,19 @@
1837
2059
  var NS = "dsh-notify-me";
1838
2060
  var UI_zh = {
1839
2061
  nav: "通知提醒",
1840
- blurb: "当模型停下来需要你操作(审批 / 方案待确认 / 提问),或回复在后台完成时,用系统通知、提示音和标签页标题提醒你。",
2062
+ blurb: "模型停下来需要你操作,或回复在后台完成时提醒你。",
2063
+ secWays: "提醒方式",
2064
+ secWhen: "什么时候提醒",
2065
+ secTest: "试一试",
2066
+ advanced: "高级设置",
2067
+ advancedHint: "音效来源 · 外部通知 · 运行环境",
2068
+ secDiag: "运行环境与诊断",
2069
+ expand: "展开",
2070
+ collapse: "收起",
2071
+ permHintInline: "尚未授权,允许一次即可。",
2072
+ resetHint: "把上面所有选项还原成默认值(含已选的自定义音效)。",
1841
2073
  enable: "启用提醒",
1842
- enableHint: "总开关:关闭后不再弹系统通知、不播放提示音,也不再改动标签页标题。",
2074
+ enableHint: "关闭后不弹通知、不响提示音,也不改标签页标题。",
1843
2075
  hostEnv: "运行环境",
1844
2076
  hostEnvHint: "默认自动识别:页面协议是 dsh-app:// 即桌面端(DSH Desktop),其余为 Web 端。识别不准时手动指定:桌面端点通知借 dsh:// 深链把窗口带到前台、快捷裁决按钮不可用;Web 端走 window.focus() 与 Service Worker 桥。",
1845
2077
  hostEnvAuto: "自动识别(推荐)",
@@ -1849,11 +2081,11 @@
1849
2081
  hostEnvMismatchTail: "」,手动选择会覆盖它)",
1850
2082
  hostEnvDesktopForcedNote: "(当前页面不是 dsh-app://:点通知会尝试唤起 dsh:// 协议,浏览器可能弹「打开应用」确认框)",
1851
2083
  toast: "系统通知",
1852
- toastHint: "经浏览器弹出系统 Toast(Windows 通知中心);需要已授予通知权限。",
2084
+ toastHint: "经浏览器弹系统通知(Windows 通知中心),需要通知权限。",
1853
2085
  sound: "提示音",
1854
- soundHint: "提示音总开关;具体播什么由下面的「音效来源」决定。",
2086
+ soundHint: "提示音总开关;播什么由「高级设置 → 音效来源」决定。",
1855
2087
  soundSource: "音效来源",
1856
- soundSourceHint: "两类提醒各自选择声音来源,互不影响。默认是插件自带的内置合成音。",
2088
+ soundSourceHint: "两类提醒各自选声音来源,互不影响;默认是插件内置合成音。",
1857
2089
  soundDoneLabel: "「回复完成」的声音",
1858
2090
  soundAttentionLabel: "「需要你」的声音",
1859
2091
  soundSynth: "内置合成音(默认)",
@@ -1876,7 +2108,7 @@
1876
2108
  soundOffHint: "「提示音」开关是关的:先打开它才能试听(关掉时提醒本身也确实不出声)。",
1877
2109
  soundPreviewSystem: "「系统通知音」是随通知由系统播放的,插件这边没有可播的音;已发一条测试提醒,声音就是它。",
1878
2110
  external: "外部通知",
1879
- externalHint: "把提醒同时推送到 Discord Webhook / Bark / Gotify / 自定义 Webhook,人不在电脑前也能收到「回来一下」。与本地提醒同一套触发规则(上面的可见性开关一并生效)。",
2111
+ externalHint: "把提醒同时推到 Discord / Bark / Gotify / 自定义 Webhook,人不在电脑前也能收到。与本地提醒同一套触发规则。",
1880
2112
  externalEnable: "转发外部通知",
1881
2113
  externalEnableHint: "总开关:关闭后不向任何外部端点发送。",
1882
2114
  externalAttentionLabel: "转发「需要你」",
@@ -1907,21 +2139,30 @@
1907
2139
  externalNoTargets: "先添加至少一个推送目标。",
1908
2140
  externalOffHint: "先打开「转发外部通知」。",
1909
2141
  attentionVisible: "页面打开时也提醒「需要你」",
1910
- attentionVisibleHint: "默认开启:模型在等你审批 / 确认方案 / 回答问题(含提问弹窗)时,即使页面在最前也会提醒。",
2142
+ attentionVisibleHint: "页面在最前也提醒。关掉后只有切到后台才提醒。",
1911
2143
  currentQuiet: "当前对话不弹通知",
1912
- currentQuietHint: "默认开启:等待就发生在你正看着的这个对话里时,只留标签页标记,不弹系统通知、不响提示音,免得挡住审批卡片;页面切到后台后照常提醒。",
2144
+ currentQuietHint: "等待就发生在你正看着的对话里时只留标签页标记,免得挡住审批卡片;切到后台后照常提醒。",
1913
2145
  doneVisible: "页面打开时也提醒「回复完成」",
1914
- doneVisibleHint: "默认关闭:「完成」只在页面隐藏或切到后台时才提醒,避免打扰。",
2146
+ doneVisibleHint: "页面在最前也提醒。默认只在切到后台时提醒,避免打扰。",
1915
2147
  subagentQuiet: "子代理会话不提醒",
1916
- subagentQuietHint: "默认开启:后台子代理(subagent)会话的「需要你」与「回复完成」都不提醒——它们是另一个对话的步骤,完成由所属主对话自己的提醒覆盖;你当前打开的那个子代理会话照常提醒。关掉后恢复逐会话提醒。",
2148
+ subagentQuietHint: "跳过后台子代理会话(结果由所属主对话的提醒覆盖);你正打开的那个照常提醒。",
1917
2149
  autoFocus: "点击通知回到对应对话",
1918
- autoFocusHint: "默认开启:点系统通知会把 DSH 带到前台,并直接切到这条提醒所属的对话;关掉后点击通知不做任何事。",
2150
+ autoFocusHint: "点通知会切到这条提醒所属的对话;关掉后点击不做任何事。",
1919
2151
  quickActions: "审批通知上直接裁决",
1920
- quickActionsHint: "默认开启:权限审批的通知上带「同意 / 拒绝」按钮,点一下即按审批卡片的「允许一次 / 拒绝」直接完成裁决,不用切回页面。需要支持 Service Worker 的浏览器(http://127.0.0.1 或 https);用不了时自动退回普通通知。",
2152
+ quickActionsHint: "审批通知上带「同意 / 拒绝」,点一下即完成裁决,不用切回页面。需要 Service Worker(http://127.0.0.1 或 https),用不了时退回普通通知。",
1921
2153
  quickApprovalNote: "「同意」等价于审批卡片上的「允许一次」,只放行这一次。",
1922
2154
  bridgeOff: "快捷裁决按钮暂不可用(",
1923
2155
  bridgeOffTail: "):审批通知会退回不带按钮的普通样式。",
1924
2156
  bridgeOffDesktop: "桌面端暂不支持快捷裁决按钮:dsh-app:// 页面无法注册 Service Worker,审批通知只能是不带按钮的普通样式,测试按钮同样只发普通通知。这是宿主限制,不是设置坏了。",
2157
+ overlay: "页面浮层提醒",
2158
+ overlayHint: "在页面角落画提醒卡片:窗口在哪块屏,它就在哪块屏。系统通知只会出现在主屏,主屏不看时用它。不需要通知权限,桌面端也能直接「同意 / 拒绝」,可与系统通知同开。",
2159
+ overlayCorner: "浮层位置",
2160
+ overlayCornerHint: "贴在 DSH 窗口的哪个角;窗口在哪块屏,浮层就出现在哪块屏。",
2161
+ overlayPosTR: "右上",
2162
+ overlayPosBR: "右下",
2163
+ overlayPosTL: "左上",
2164
+ overlayPosBL: "左下",
2165
+ overlayQuickNote: "浮层上的「同意」同样等价于审批卡片上的「允许一次」,与通知按钮一致。",
1925
2166
  testApprovalNoBridge: "已发普通测试通知(快捷裁决按钮需要 Service Worker,当前环境没有,按钮渲染不了)。",
1926
2167
  testNeedsQuick: "先打开上面的「审批通知上直接裁决」才能测按钮。",
1927
2168
  volume: "音量",
@@ -1932,7 +2173,7 @@
1932
2173
  langAutoNote: "(跟随界面)",
1933
2174
  current: "界面当前语言",
1934
2175
  testTitle: "快速测试",
1935
- testHint: "用当前设置各发一条测试提醒(不受上面「页面打开时也提醒」开关限制)。",
2176
+ testHint: "用当前设置各发一条,不受「什么时候提醒」里的开关限制。",
1936
2177
  testAttention: "测试「需要你」",
1937
2178
  testDone: "测试「回复完成」",
1938
2179
  testApprovalBtn: "测试「审批按钮」",
@@ -1945,9 +2186,19 @@
1945
2186
  };
1946
2187
  var UI_en = {
1947
2188
  nav: "Notify me",
1948
- blurb: "When the agent stops and needs your input (approval / plan review / question) or a reply finishes in the background, get a system notification, a sound, and a tab-title marker.",
2189
+ blurb: "Get told when the agent needs your input, or when a reply finishes in the background.",
2190
+ secWays: "How you get told",
2191
+ secWhen: "When to alert",
2192
+ secTest: "Try it",
2193
+ advanced: "Advanced",
2194
+ advancedHint: "Alert sounds · External forwarding · Runtime",
2195
+ secDiag: "Runtime & diagnostics",
2196
+ expand: "Show",
2197
+ collapse: "Hide",
2198
+ permHintInline: "Not granted yet — allow it once from the page.",
2199
+ resetHint: "Put every option above back to its default (picked sound files included).",
1949
2200
  enable: "Enable reminders",
1950
- enableHint: "Master switch: when off, no toasts, no sounds, and the tab title is never modified.",
2201
+ enableHint: "When off: no toasts, no sounds, and the tab title is left alone.",
1951
2202
  hostEnv: "Environment",
1952
2203
  hostEnvHint: "Auto-detect by default: a dsh-app:// page is the Desktop (DSH Desktop) shell, anything else is the Web. Override it if detection comes out wrong: on the Desktop a toast click raises the window through the dsh:// deep link and quick-decision buttons are unavailable; on the Web it is window.focus() plus the Service Worker bridge.",
1953
2204
  hostEnvAuto: "Auto-detect (recommended)",
@@ -1957,11 +2208,11 @@
1957
2208
  hostEnvMismatchTail: "\"; the manual choice overrides it)",
1958
2209
  hostEnvDesktopForcedNote: " (this page is not dsh-app://: a toast click will try to launch the dsh:// protocol and the browser may show an \"open app?\" prompt)",
1959
2210
  toast: "System notifications",
1960
- toastHint: "Native toasts via the browser (Windows notification center); requires notification permission.",
2211
+ toastHint: "Native toasts through the browser (Windows notification center); needs notification permission.",
1961
2212
  sound: "Sound",
1962
- soundHint: "Master cue switch; what it actually plays comes from \"Alert sound\" below.",
2213
+ soundHint: "Master cue switch; what it plays comes from Advanced → Alert sound.",
1963
2214
  soundSource: "Alert sound",
1964
- soundSourceHint: "Each alert kind picks its own sound source; the two never affect each other. The default is the plugin's built-in cue.",
2215
+ soundSourceHint: "Each alert kind picks its own source; the two never affect each other. Default is the built-in cue.",
1965
2216
  soundDoneLabel: "\"Reply finished\" sound",
1966
2217
  soundAttentionLabel: "\"Needs you\" sound",
1967
2218
  soundSynth: "Built-in cue (default)",
@@ -1984,7 +2235,7 @@
1984
2235
  soundOffHint: "The Sound switch is off: turn it on to preview (with it off the alerts themselves stay silent too).",
1985
2236
  soundPreviewSystem: "The system sound plays with a toast, not from the plugin — a test alert was sent instead; that sound is it.",
1986
2237
  external: "External notifications",
1987
- externalHint: "Mirror alerts to a Discord webhook / Bark / Gotify / custom webhook so \"come back\" reaches you away from the machine. Same trigger rules as the local alerts (the visibility toggles above apply too).",
2238
+ externalHint: "Also push alerts to Discord / Bark / Gotify / custom webhooks, so you hear about it away from the machine. Same trigger rules as local alerts.",
1988
2239
  externalEnable: "Forward to external targets",
1989
2240
  externalEnableHint: "Master switch: when off, nothing is sent to any external endpoint.",
1990
2241
  externalAttentionLabel: "Forward \"needs you\"",
@@ -2015,21 +2266,30 @@
2015
2266
  externalNoTargets: "Add at least one push target first.",
2016
2267
  externalOffHint: "Turn on \"Forward to external targets\" first.",
2017
2268
  attentionVisible: "Also alert \"needs you\" while the page is open",
2018
- attentionVisibleHint: "On by default: alert even in the foreground when the model waits for approval / plan review / a question.",
2269
+ attentionVisibleHint: "Alert even while the page is in front. Turn off to hear only from the background.",
2019
2270
  currentQuiet: "Stay quiet for the conversation on screen",
2020
- currentQuietHint: "On by default: a wait inside the conversation you are looking at keeps only the tab marker — no toast or sound landing on the approval card. Backgrounding the page puts the alert back.",
2271
+ currentQuietHint: "A wait belonging to the conversation on screen keeps only the tab marker, so the approval card stays readable; backgrounding restores alerts.",
2021
2272
  doneVisible: "Also alert \"reply finished\" while the page is open",
2022
- doneVisibleHint: "Off by default: \"done\" only alerts while the page is hidden or backgrounded, to avoid noise.",
2273
+ doneVisibleHint: "Alert even while the page is in front. By default only background finishes alert.",
2023
2274
  subagentQuiet: "Ignore subagent sessions",
2024
- subagentQuietHint: "On by default: background sessions spawned by a subagent raise neither \"needs you\" nor \"reply finished\" alerts — they are steps of another conversation, already covered by that conversation's own alert. The subagent session you have open alerts as usual. Turn off to alert for every session.",
2275
+ subagentQuietHint: "Skip sessions spawned by a subagent (the parent conversation covers them); the one you have open still alerts.",
2025
2276
  autoFocus: "Click a toast to open that conversation",
2026
- autoFocusHint: "On by default: clicking a toast brings DSH forward and switches to the conversation the alert came from. When off, clicking a toast does nothing.",
2277
+ autoFocusHint: "A click switches to the conversation the alert came from; off means a click does nothing.",
2027
2278
  quickActions: "Decide approvals from the toast",
2028
- quickActionsHint: "On by default: approval notifications carry Approve / Reject buttons, and one click settles the request exactly like the Allow once / Reject buttons on the approval card. Needs a Service Worker capable browser (http://127.0.0.1 or https); falls back to plain toasts where unavailable.",
2279
+ quickActionsHint: "Approval notifications carry Approve / Reject; one click settles the request without switching back. Needs a Service Worker browser (http://127.0.0.1 or https), else plain toasts.",
2029
2280
  quickApprovalNote: "\"Approve\" equals \"Allow once\" on the approval card — it allows that one call only.",
2030
2281
  bridgeOff: "Quick-decision buttons are unavailable right now (",
2031
2282
  bridgeOffTail: "); approval toasts fall back to their plain form.",
2032
2283
  bridgeOffDesktop: "Quick-decision buttons are unavailable on Desktop: dsh-app:// pages cannot register a Service Worker, so approval toasts fall back to their plain, button-less form and the test button only sends a plain notification. This is a host limitation, not a broken setting.",
2284
+ overlay: "In-page overlay",
2285
+ overlayHint: "Draw a card in the page corner, on whatever display the window sits on. System notifications only land on the primary display. No permission needed, and the Desktop shell can Approve / Reject right on the card. Can run with toasts on.",
2286
+ overlayCorner: "Overlay position",
2287
+ overlayCornerHint: "Which corner of the DSH window the cards stack in; they follow the window to whatever display it is on.",
2288
+ overlayPosTR: "Top right",
2289
+ overlayPosBR: "Bottom right",
2290
+ overlayPosTL: "Top left",
2291
+ overlayPosBL: "Bottom left",
2292
+ overlayQuickNote: "\"Approve\" here equals \"Allow once\" on the approval card — the same decision the toast buttons send.",
2033
2293
  testApprovalNoBridge: "Plain test notification sent (quick-decision buttons need a Service Worker, which this environment lacks, so they cannot render).",
2034
2294
  testNeedsQuick: "Turn on \"Decide approvals from the toast\" above to test the buttons.",
2035
2295
  volume: "Volume",
@@ -2040,7 +2300,7 @@
2040
2300
  langAutoNote: " (follows the interface)",
2041
2301
  current: "Current interface language",
2042
2302
  testTitle: "Quick test",
2043
- testHint: "Send one test alert with the current settings (not limited by the \"while the page is open\" toggles above).",
2303
+ testHint: "Send one of each with the current settings; the \"When to alert\" switches are bypassed.",
2044
2304
  testAttention: "Test \"needs you\"",
2045
2305
  testDone: "Test \"reply finished\"",
2046
2306
  testApprovalBtn: "Test approval buttons",
@@ -2082,7 +2342,27 @@
2082
2342
  ".dnm-targetRow+.dnm-targetRow{border-top:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.35))}" +
2083
2343
  ".dnm-input{flex:1;min-width:180px;border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.4));background:var(--dsw-alias-bg-layer-1,rgba(255,255,255,.03));color:var(--dsw-alias-label-primary,rgba(20,20,20,.9));border-radius:8px;padding:5px 8px;font-size:12px;font-family:inherit}" +
2084
2344
  ".dnm-hint{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary,rgba(120,120,120,.9))}" +
2085
- ".dnm-feedback{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary,rgba(120,120,120,.9));min-height:18px}";
2345
+ ".dnm-feedback{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary,rgba(120,120,120,.9));min-height:18px}" +
2346
+ // Page header: mark + title + one-line purpose. The section titles below
2347
+ // carry the grouping, so the header stays small.
2348
+ ".dnm-head{display:flex;align-items:flex-start;gap:12px}" +
2349
+ ".dnm-headIcon{flex:none;width:36px;height:36px;border-radius:10px;display:grid;place-items:center;color:var(--dsw-alias-state-business-primary,#3b82f6);background:rgba(59,130,246,.12);border:1px solid rgba(59,130,246,.24)}" +
2350
+ ".dnm-headText{min-width:0;display:flex;flex-direction:column;gap:3px;padding-top:1px}" +
2351
+ ".dnm-headTitle{font-size:15px;font-weight:600;line-height:21px;color:var(--dsw-alias-label-primary,rgba(20,20,20,.9))}" +
2352
+ ".dnm-headSub{margin:0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary,rgba(120,120,120,.9))}" +
2353
+ ".dnm-secTitle{margin:2px 0 -6px;padding:0 2px;font-size:12px;font-weight:600;line-height:18px;color:var(--dsw-alias-label-secondary,rgba(90,90,90,.9));letter-spacing:.02em}" +
2354
+ // Sub-rows hang under the row they belong to (corner of the overlay,
2355
+ // volume under Sound) instead of reading as siblings.
2356
+ ".dnm-rowSub{padding-left:26px;background:var(--dsw-alias-bg-layer-2,rgba(128,128,128,.04))}" +
2357
+ ".dnm-rowSub .dnm-rowLabel{font-weight:500}" +
2358
+ ".dnm-rowStack{flex-direction:column;align-items:stretch;gap:10px}" +
2359
+ ".dnm-rowStack .dnm-btnRow{justify-content:flex-start;flex-wrap:wrap}" +
2360
+ ".dnm-advToggle{width:100%;box-sizing:border-box;display:flex;align-items:center;justify-content:space-between;gap:10px;border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.35));background:transparent;color:var(--dsw-alias-label-primary,rgba(20,20,20,.9));border-radius:10px;padding:10px 14px;font-family:inherit;font-size:13px;font-weight:600;cursor:pointer;text-align:left}" +
2361
+ ".dnm-advToggle:hover{background:var(--dsw-alias-bg-layer-1,rgba(128,128,128,.08))}" +
2362
+ ".dnm-advToggle:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#3b82f6);outline-offset:2px}" +
2363
+ ".dnm-advRight{display:flex;align-items:center;gap:8px;flex:none;font-weight:400;color:var(--dsw-alias-label-tertiary,rgba(120,120,120,.9))}" +
2364
+ ".dnm-advChev{font-size:10px;translate:0 1px;transition:transform .15s ease}" +
2365
+ ".dnm-advChev.open{transform:rotate(180deg)}";
2086
2366
  (function injectStyle() {
2087
2367
  try {
2088
2368
  if (typeof document === "undefined" || typeof document.createElement !== "function") return;
@@ -2366,43 +2646,75 @@
2366
2646
  else if (cfg.hostEnv !== "auto" && isDesktopShell() !== detectedDesktop()) {
2367
2647
  envHint += t("hostEnvMismatch") + (detectedDesktop() ? t("hostEnvDesktop") : t("hostEnvWeb")) + t("hostEnvMismatchTail");
2368
2648
  }
2649
+ // Page header: the plugin's own mark instead of the host's generic
2650
+ // settings gear, so the page is recognisable at a glance while the
2651
+ // Settings nav row itself stays host-controlled.
2652
+ function headIcon() {
2653
+ return h("svg", { viewBox: "0 0 24 24", width: 22, height: 22, fill: "none", stroke: "currentColor", "stroke-width": 1.6, "stroke-linecap": "round", "stroke-linejoin": "round", "aria-hidden": "true" }, [
2654
+ h("path", { d: "M12 3.4a5.1 5.1 0 0 0-5.1 5.1v3.1c0 .9-.3 1.7-.9 2.4l-1.1 1.3c-.5.6-.1 1.5.7 1.5h12.8c.8 0 1.2-.9.7-1.5l-1.1-1.3a3.8 3.8 0 0 1-.9-2.4V8.5A5.1 5.1 0 0 0 12 3.4Z" }),
2655
+ h("path", { d: "M9.8 19.2a2.4 2.4 0 0 0 4.4 0" }),
2656
+ h("circle", { cx: 18.3, cy: 5.3, r: 2.35, fill: "currentColor", stroke: "none" })
2657
+ ]);
2658
+ }
2659
+ function secTitle(labelText) {
2660
+ return h("div", { className: "dnm-secTitle" }, labelText);
2661
+ }
2662
+ // The permission state belongs on the System-notifications row it affects
2663
+ // rather than in a footer note the user has to connect to it.
2664
+ function toastHintText() {
2665
+ return t("toastHint") + (permOK ? "" : " " + t("permHintInline"));
2666
+ }
2667
+ var advState = React.useState(false);
2668
+ var advancedOpen = advState[0];
2669
+ var setAdvancedOpen = advState[1];
2670
+ var testRow = h("div", { className: "dnm-row dnm-rowStack" }, [
2671
+ h("div", { className: "dnm-rowText" }, [
2672
+ h("div", { className: "dnm-rowLabel" }, t("testTitle")),
2673
+ h("div", { className: "dnm-rowHint" }, t("testHint"))
2674
+ ]),
2675
+ h("div", { className: "dnm-btnRow" }, [
2676
+ h("button", { type: "button", className: "dnm-btn", disabled: !cfg.enabled, onClick: function () { runTest("attention"); } }, t("testAttention")),
2677
+ h("button", { type: "button", className: "dnm-btn", disabled: !cfg.enabled, onClick: function () { runTest("done"); } }, t("testDone")),
2678
+ h("button", { type: "button", className: "dnm-btn", disabled: !cfg.enabled || !cfg.quickActions, onClick: function () { runTest("approval"); } }, t("testApprovalBtn"))
2679
+ ])
2680
+ ]);
2369
2681
  return h("div", { className: "dnm-wrap" }, [
2370
- h("p", { className: "dnm-blurb" }, t("blurb")),
2682
+ h("div", { className: "dnm-head" }, [
2683
+ h("span", { className: "dnm-headIcon" }, headIcon()),
2684
+ h("div", { className: "dnm-headText" }, [
2685
+ h("div", { className: "dnm-headTitle" }, t("nav")),
2686
+ h("p", { className: "dnm-headSub" }, t("blurb"))
2687
+ ])
2688
+ ]),
2689
+ // 1) How the alert reaches you. Each channel owns its own row, and the
2690
+ // row's dependent control (overlay corner, volume) hangs beneath it.
2691
+ secTitle(t("secWays")),
2371
2692
  card([
2372
2693
  rowSwitch("enabled", t("enable"), t("enableHint"), cfg.enabled, function (v) {
2373
2694
  patch({ enabled: v });
2374
2695
  if (!v && attentionActive > 0) { attentionActive = 0; applyTitle(); }
2375
2696
  }),
2376
- h("div", { className: "dnm-row" }, [
2697
+ rowSwitch("toast", t("toast"), toastHintText(), cfg.toast, function (v) { patch({ toast: v }); }),
2698
+ rowSwitch("overlay", t("overlay"), t("overlayHint"), cfg.overlay, function (v) { patch({ overlay: v }); }),
2699
+ cfg.overlay ? h("div", { className: "dnm-row dnm-rowSub" }, [
2377
2700
  h("div", { className: "dnm-rowText" }, [
2378
- h("div", { className: "dnm-rowLabel" }, t("hostEnv")),
2379
- h("div", { className: "dnm-rowHint" }, envHint)
2701
+ h("div", { className: "dnm-rowLabel" }, t("overlayCorner")),
2702
+ h("div", { className: "dnm-rowHint" }, t("overlayCornerHint"))
2380
2703
  ]),
2381
2704
  h("select", {
2382
2705
  className: "dnm-select",
2383
- value: cfg.hostEnv,
2384
- "aria-label": t("hostEnv"),
2385
- onChange: function (e) { patch({ hostEnv: e.currentTarget.value }); }
2706
+ value: overlayCorner(),
2707
+ "aria-label": t("overlayCorner"),
2708
+ onChange: function (e) { patch({ overlayCorner: e.currentTarget.value }); }
2386
2709
  }, [
2387
- h("option", { value: "auto" }, t("hostEnvAuto")),
2388
- h("option", { value: "desktop" }, t("hostEnvDesktop")),
2389
- h("option", { value: "web" }, t("hostEnvWeb"))
2710
+ h("option", { value: "top-right" }, t("overlayPosTR")),
2711
+ h("option", { value: "bottom-right" }, t("overlayPosBR")),
2712
+ h("option", { value: "top-left" }, t("overlayPosTL")),
2713
+ h("option", { value: "bottom-left" }, t("overlayPosBL"))
2390
2714
  ])
2391
- ])
2392
- ]),
2393
- card([
2394
- rowSwitch("toast", t("toast"), t("toastHint"), cfg.toast, function (v) { patch({ toast: v }); }),
2715
+ ]) : null,
2395
2716
  rowSwitch("sound", t("sound"), t("soundHint"), cfg.sound, function (v) { patch({ sound: v }); }),
2396
- rowSwitch("attentionVisible", t("attentionVisible"), t("attentionVisibleHint"), !cfg.attentionHiddenOnly, function (v) { patch({ attentionHiddenOnly: !v }); }),
2397
- rowSwitch("currentQuiet", t("currentQuiet"), t("currentQuietHint"), cfg.currentHiddenOnly, function (v) { patch({ currentHiddenOnly: v }); }),
2398
- rowSwitch("doneVisible", t("doneVisible"), t("doneVisibleHint"), !cfg.doneHiddenOnly, function (v) { patch({ doneHiddenOnly: !v }); }),
2399
- rowSwitch("subagentQuiet", t("subagentQuiet"), t("subagentQuietHint"), cfg.ignoreSubagent, function (v) { patch({ ignoreSubagent: v }); }),
2400
- rowSwitch("autoFocus", t("autoFocus"), t("autoFocusHint"), cfg.autoFocus, function (v) { patch({ autoFocus: v }); }),
2401
- rowSwitch("quickActions", t("quickActions"), t("quickActionsHint"), cfg.quickActions, function (v) {
2402
- patch({ quickActions: v });
2403
- if (v) ensureActionBridge();
2404
- }),
2405
- h("div", { className: "dnm-row" }, [
2717
+ cfg.sound ? h("div", { className: "dnm-row dnm-rowSub" }, [
2406
2718
  h("div", { className: "dnm-rowText" }, [
2407
2719
  h("div", { className: "dnm-rowLabel" }, t("volume")),
2408
2720
  h("div", { className: "dnm-rowHint" }, String(Math.round(cfg.volume * 100)) + "%")
@@ -2415,126 +2727,159 @@
2415
2727
  "aria-label": t("volume"),
2416
2728
  onChange: function (e) { patch({ volume: Number(e.currentTarget.value) / 100 }); }
2417
2729
  })
2418
- ])
2730
+ ]) : null,
2731
+ rowSwitch("quickActions", t("quickActions"), t("quickActionsHint"), cfg.quickActions, function (v) {
2732
+ patch({ quickActions: v });
2733
+ if (v) ensureActionBridge();
2734
+ })
2419
2735
  ]),
2420
- // Alert sound source: the built-in cue, a picked file, or the platform's
2421
- // own notification sound. Kept next to the Sound/volume card so the
2422
- // sound-related knobs read together, and re-read through soundBump
2423
- // because picked files live outside the config object.
2424
- card([].concat(
2425
- [h("div", { className: "dnm-row" }, [
2426
- h("div", { className: "dnm-rowText" }, [
2427
- h("div", { className: "dnm-rowLabel" }, t("soundSource")),
2428
- h("div", { className: "dnm-rowHint" }, t("soundSourceHint"))
2429
- ])
2430
- ])],
2431
- soundRows("done", t("soundDoneLabel")),
2432
- soundRows("attention", t("soundAttentionLabel"))
2433
- )),
2434
- // External forwarding: the same alerts, mirrored to the user's own
2435
- // webhook endpoints so "come back" reaches them away from the machine.
2436
- card([].concat(
2437
- [h("div", { className: "dnm-row" }, [
2438
- h("div", { className: "dnm-rowText" }, [
2439
- h("div", { className: "dnm-rowLabel" }, t("external")),
2440
- h("div", { className: "dnm-rowHint" }, t("externalHint"))
2441
- ])
2442
- ]),
2443
- rowSwitch("externalEnabled", t("externalEnable"), t("externalEnableHint"), cfg.externalEnabled, function (v) { patch({ externalEnabled: v }); }),
2444
- rowSwitch("externalAttention", t("externalAttentionLabel"), t("externalAttentionHint"), cfg.externalAttention, function (v) { patch({ externalAttention: v }); }),
2445
- rowSwitch("externalDone", t("externalDoneLabel"), t("externalDoneHint"), cfg.externalDone, function (v) { patch({ externalDone: v }); }),
2446
- h("div", { className: "dnm-row" }, [
2447
- h("div", { className: "dnm-rowText" }, [
2448
- h("div", { className: "dnm-rowLabel" }, t("externalTargetsLabel")),
2449
- h("div", { className: "dnm-rowHint" }, t("externalTargetsHint"))
2450
- ])
2451
- ])],
2452
- externalRows(),
2453
- [(Array.isArray(cfg.externalTargets) && cfg.externalTargets.length) ? h("div", { className: "dnm-row" }, [
2454
- h("div", { className: "dnm-rowText" }, [h("div", { className: "dnm-hint" }, t("externalPrivacy"))])
2455
- ]) : h("div", { className: "dnm-row" }, [
2456
- h("div", { className: "dnm-rowText" }, [h("div", { className: "dnm-hint" }, t("externalEmpty"))])
2457
- ]),
2458
- h("div", { className: "dnm-row" }, [
2459
- h("div", { className: "dnm-btnRow" }, [
2460
- h("button", {
2461
- type: "button",
2462
- className: "dnm-btn",
2463
- "aria-label": t("externalAdd"),
2464
- onClick: addTarget
2465
- }, t("externalAdd"))
2466
- ])
2467
- ]),
2468
- h("div", { className: "dnm-row" }, [
2469
- h("div", { className: "dnm-rowText" }, [
2470
- h("div", { className: "dnm-rowLabel" }, t("externalTestLabel")),
2471
- h("div", { className: "dnm-rowHint" }, t("externalTestHint"))
2472
- ]),
2473
- h("div", { className: "dnm-btnRow" }, [
2474
- h("button", {
2475
- type: "button",
2476
- className: "dnm-btn",
2477
- "aria-label": t("externalTestLabel"),
2478
- disabled: !cfg.enabled,
2479
- onClick: runExternalTest
2480
- }, t("externalTestBtn"))
2481
- ])
2482
- ])
2483
- ])),
2736
+ // 2) Which events are worth interrupting for.
2737
+ secTitle(t("secWhen")),
2484
2738
  card([
2485
- h("div", { className: "dnm-row" }, [
2486
- h("div", { className: "dnm-rowText" }, [
2487
- h("div", { className: "dnm-rowLabel" }, t("language")),
2488
- h("div", { className: "dnm-rowHint" }, resolvedLangName + (cfg.language === "auto" ? t("langAutoNote") : ""))
2489
- ]),
2490
- h("select", {
2491
- className: "dnm-select",
2492
- value: cfg.language,
2493
- "aria-label": t("language"),
2494
- onChange: function (e) { patch({ language: e.currentTarget.value }); }
2495
- }, [
2496
- h("option", { value: "auto" }, t("langAuto")),
2497
- h("option", { value: "zh" }, t("langZh")),
2498
- h("option", { value: "en" }, t("langEn"))
2499
- ])
2500
- ]),
2501
- h("div", { className: "dnm-row" }, [
2502
- h("div", { className: "dnm-rowText" }, [
2503
- h("div", { className: "dnm-rowLabel" }, t("testTitle")),
2504
- h("div", { className: "dnm-rowHint" }, t("testHint"))
2505
- ]),
2506
- h("div", { className: "dnm-btnRow" }, [
2507
- h("button", { type: "button", className: "dnm-btn", disabled: !cfg.enabled, onClick: function () { runTest("attention"); } }, t("testAttention")),
2508
- h("button", { type: "button", className: "dnm-btn", disabled: !cfg.enabled, onClick: function () { runTest("done"); } }, t("testDone")),
2509
- h("button", { type: "button", className: "dnm-btn", disabled: !cfg.enabled || !cfg.quickActions, onClick: function () { runTest("approval"); } }, t("testApprovalBtn"))
2510
- ])
2511
- ]),
2512
- cfg.quickActions && cfg.toast && !bridgeActive() ? h("div", { className: "dnm-row" }, [
2513
- h("div", { className: "dnm-rowText" }, [h("div", { className: "dnm-rowHint" }, isDesktopShell() ? t("bridgeOffDesktop") : t("bridgeOff") + swState + t("bridgeOffTail"))])
2739
+ rowSwitch("attentionVisible", t("attentionVisible"), t("attentionVisibleHint"), !cfg.attentionHiddenOnly, function (v) { patch({ attentionHiddenOnly: !v }); }),
2740
+ rowSwitch("currentQuiet", t("currentQuiet"), t("currentQuietHint"), cfg.currentHiddenOnly, function (v) { patch({ currentHiddenOnly: v }); }),
2741
+ rowSwitch("doneVisible", t("doneVisible"), t("doneVisibleHint"), !cfg.doneHiddenOnly, function (v) { patch({ doneHiddenOnly: !v }); }),
2742
+ rowSwitch("subagentQuiet", t("subagentQuiet"), t("subagentQuietHint"), cfg.ignoreSubagent, function (v) { patch({ ignoreSubagent: v }); }),
2743
+ rowSwitch("autoFocus", t("autoFocus"), t("autoFocusHint"), cfg.autoFocus, function (v) { patch({ autoFocus: v }); })
2744
+ ]),
2745
+ // 3) Prove the setup works without leaving the page.
2746
+ secTitle(t("secTest")),
2747
+ card([
2748
+ testRow,
2749
+ !cfg.quickActions ? h("div", { className: "dnm-row" }, [
2750
+ h("div", { className: "dnm-rowText" }, [h("div", { className: "dnm-rowHint" }, t("testNeedsQuick"))])
2514
2751
  ]) : h("div", { className: "dnm-row" }, [
2515
- h("div", { className: "dnm-rowText" }, [h("div", { className: "dnm-rowHint" }, cfg.quickActions ? t("quickApprovalNote") : t("testNeedsQuick"))])
2752
+ h("div", { className: "dnm-rowText" }, [h("div", { className: "dnm-rowHint" }, cfg.overlay ? t("overlayQuickNote") : (cfg.toast && !bridgeActive() ? (isDesktopShell() ? t("bridgeOffDesktop") : t("bridgeOff") + swState + t("bridgeOffTail")) : t("quickApprovalNote")))])
2516
2753
  ]),
2517
- h("div", { className: "dnm-row" }, [
2754
+ feedback ? h("div", { className: "dnm-row" }, [
2518
2755
  h("div", { className: "dnm-rowText" }, [h("div", { className: "dnm-feedback" }, feedback)])
2519
- ]),
2520
- h("div", { className: "dnm-row" }, [
2521
- h("div", { className: "dnm-rowText" }, [
2522
- h("div", { className: "dnm-rowLabel" }, t("reset")),
2523
- h("div", { className: "dnm-rowHint" }, " ")
2524
- ]),
2525
- h("button", { type: "button", className: "dnm-btn", onClick: function () {
2526
- // api.resetConfig() also drops the picked cue files, which are
2527
- // stored outside the config.
2528
- setCfg(api.resetConfig());
2529
- applyTitle();
2530
- setSoundBump(soundBump + 1);
2531
- setFeedback(t("resetDone"));
2532
- } }, t("reset"))
2533
- ]),
2534
- !permOK ? h("div", { className: "dnm-row" }, [
2535
- h("div", { className: "dnm-rowText" }, [h("div", { className: "dnm-rowHint" }, t("permHint"))])
2536
2756
  ]) : null
2537
2757
  ]),
2758
+ // 4) Everything most users never touch, collapsed by default. It all
2759
+ // stays mounted-on-demand (no hidden DOM work while closed).
2760
+ h("button", {
2761
+ type: "button",
2762
+ className: "dnm-advToggle",
2763
+ "aria-expanded": advancedOpen ? "true" : "false",
2764
+ "aria-label": t("advanced"),
2765
+ onClick: function () { setAdvancedOpen(!advancedOpen); }
2766
+ }, [
2767
+ h("span", null, t("advanced")),
2768
+ h("span", { className: "dnm-advRight" }, [
2769
+ h("span", null, advancedOpen ? t("collapse") : t("advancedHint")),
2770
+ h("span", { className: "dnm-advChev" + (advancedOpen ? " open" : "") }, "▼")
2771
+ ])
2772
+ ]),
2773
+ advancedOpen ? [
2774
+ secTitle(t("soundSource")),
2775
+ // Alert sound source: built-in cue, a picked file, or the platform's
2776
+ // own sound. Re-read through soundBump because picked files live
2777
+ // outside the config object.
2778
+ card([].concat(
2779
+ [h("div", { className: "dnm-row" }, [
2780
+ h("div", { className: "dnm-rowText" }, [h("div", { className: "dnm-hint" }, t("soundSourceHint"))])
2781
+ ])],
2782
+ soundRows("done", t("soundDoneLabel")),
2783
+ soundRows("attention", t("soundAttentionLabel"))
2784
+ )),
2785
+ secTitle(t("external")),
2786
+ // External forwarding: the same alerts mirrored to the user's own
2787
+ // webhook endpoints so "come back" reaches them away from the machine.
2788
+ card([].concat(
2789
+ [h("div", { className: "dnm-row" }, [
2790
+ h("div", { className: "dnm-rowText" }, [h("div", { className: "dnm-hint" }, t("externalHint"))])
2791
+ ])],
2792
+ [rowSwitch("externalEnabled", t("externalEnable"), t("externalEnableHint"), cfg.externalEnabled, function (v) { patch({ externalEnabled: v }); })],
2793
+ [rowSwitch("externalAttention", t("externalAttentionLabel"), t("externalAttentionHint"), cfg.externalAttention, function (v) { patch({ externalAttention: v }); })],
2794
+ [rowSwitch("externalDone", t("externalDoneLabel"), t("externalDoneHint"), cfg.externalDone, function (v) { patch({ externalDone: v }); })],
2795
+ [h("div", { className: "dnm-row" }, [
2796
+ h("div", { className: "dnm-rowText" }, [
2797
+ h("div", { className: "dnm-rowLabel" }, t("externalTargetsLabel")),
2798
+ h("div", { className: "dnm-rowHint" }, t("externalTargetsHint"))
2799
+ ])
2800
+ ])],
2801
+ externalRows(),
2802
+ [(Array.isArray(cfg.externalTargets) && cfg.externalTargets.length) ? h("div", { className: "dnm-row" }, [
2803
+ h("div", { className: "dnm-rowText" }, [h("div", { className: "dnm-hint" }, t("externalPrivacy"))])
2804
+ ]) : h("div", { className: "dnm-row" }, [
2805
+ h("div", { className: "dnm-rowText" }, [h("div", { className: "dnm-hint" }, t("externalEmpty"))])
2806
+ ])],
2807
+ [h("div", { className: "dnm-row" }, [
2808
+ h("div", { className: "dnm-btnRow" }, [
2809
+ h("button", {
2810
+ type: "button",
2811
+ className: "dnm-btn",
2812
+ "aria-label": t("externalAdd"),
2813
+ onClick: addTarget
2814
+ }, t("externalAdd"))
2815
+ ])
2816
+ ])],
2817
+ [h("div", { className: "dnm-row" }, [
2818
+ h("div", { className: "dnm-rowText" }, [
2819
+ h("div", { className: "dnm-rowLabel" }, t("externalTestLabel")),
2820
+ h("div", { className: "dnm-rowHint" }, t("externalTestHint"))
2821
+ ]),
2822
+ h("div", { className: "dnm-btnRow" }, [
2823
+ h("button", {
2824
+ type: "button",
2825
+ className: "dnm-btn",
2826
+ "aria-label": t("externalTestLabel"),
2827
+ disabled: !cfg.enabled,
2828
+ onClick: runExternalTest
2829
+ }, t("externalTestBtn"))
2830
+ ])
2831
+ ])]
2832
+ )),
2833
+ secTitle(t("secDiag")),
2834
+ card([
2835
+ h("div", { className: "dnm-row" }, [
2836
+ h("div", { className: "dnm-rowText" }, [
2837
+ h("div", { className: "dnm-rowLabel" }, t("hostEnv")),
2838
+ h("div", { className: "dnm-rowHint" }, envHint)
2839
+ ]),
2840
+ h("select", {
2841
+ className: "dnm-select",
2842
+ value: cfg.hostEnv,
2843
+ "aria-label": t("hostEnv"),
2844
+ onChange: function (e) { patch({ hostEnv: e.currentTarget.value }); }
2845
+ }, [
2846
+ h("option", { value: "auto" }, t("hostEnvAuto")),
2847
+ h("option", { value: "desktop" }, t("hostEnvDesktop")),
2848
+ h("option", { value: "web" }, t("hostEnvWeb"))
2849
+ ])
2850
+ ]),
2851
+ h("div", { className: "dnm-row" }, [
2852
+ h("div", { className: "dnm-rowText" }, [
2853
+ h("div", { className: "dnm-rowLabel" }, t("language")),
2854
+ h("div", { className: "dnm-rowHint" }, resolvedLangName + (cfg.language === "auto" ? t("langAutoNote") : ""))
2855
+ ]),
2856
+ h("select", {
2857
+ className: "dnm-select",
2858
+ value: cfg.language,
2859
+ "aria-label": t("language"),
2860
+ onChange: function (e) { patch({ language: e.currentTarget.value }); }
2861
+ }, [
2862
+ h("option", { value: "auto" }, t("langAuto")),
2863
+ h("option", { value: "zh" }, t("langZh")),
2864
+ h("option", { value: "en" }, t("langEn"))
2865
+ ])
2866
+ ]),
2867
+ h("div", { className: "dnm-row" }, [
2868
+ h("div", { className: "dnm-rowText" }, [
2869
+ h("div", { className: "dnm-rowLabel" }, t("reset")),
2870
+ h("div", { className: "dnm-rowHint" }, t("resetHint"))
2871
+ ]),
2872
+ h("button", { type: "button", className: "dnm-btn", onClick: function () {
2873
+ // api.resetConfig() also drops the picked cue files, which are
2874
+ // stored outside the config.
2875
+ setCfg(api.resetConfig());
2876
+ applyTitle();
2877
+ setSoundBump(soundBump + 1);
2878
+ setFeedback(t("resetDone"));
2879
+ } }, t("reset"))
2880
+ ])
2881
+ ])
2882
+ ] : null,
2538
2883
  // Always-visible version line at the page foot: bug reports need it,
2539
2884
  // and "which build am I on" should not require the console.
2540
2885
  h("p", { className: "dnm-hint" }, t("versionLine") + api.version)
@@ -2633,6 +2978,7 @@
2633
2978
  for (var i = 0; i < unsubs.length; i++) { try { unsubs[i](); } catch (e) {} }
2634
2979
  detachFace();
2635
2980
  detachUiSession();
2981
+ clearOverlays();
2636
2982
  };
2637
2983
  }, "dsh-notify-me: watchers");
2638
2984
  }