dsh-notify-me 1.4.0 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/README.en.md +6 -6
- package/README.md +6 -6
- package/lib/client.js +223 -22
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to **dsh-notify-me** are documented here.
|
|
4
4
|
|
|
5
|
+
## [1.5.1] — 2026-10-04
|
|
6
|
+
|
|
7
|
+
### 修复
|
|
8
|
+
- **「点击通知回到对应对话」在带按钮的审批通知上补齐窗口调度**:这类通知由 Service Worker 弹出,此前 worker 只把点击转告页面,窗口能不能到前台全押在页面消息回调里的 `window.focus()` 上——那段代码不在用户点击的手势上下文里,Windows 前台抢占规则下窗口可能根本抬不起来。现在 worker 在 `notificationclick` 里按规范用 `client.focus()` 调起窗口。
|
|
9
|
+
- **多标签页不再互相抢焦点、一起乱切对话**:worker 的点击转信此前广播给所有同源 DSH 标签页,每个开着的标签页都会聚焦自己并各自切对话。现在一次点击只挑一个窗口(优先当前可见的)调起并切对话,其余窗口只收「不导航」的副本、留在原地;「同意 / 拒绝」按钮照旧只裁决、不调窗口。
|
|
10
|
+
- 「点击通知回到对应对话」关掉后的承诺在持久通知上也成立:show 时把该设置记进通知 `data`,worker 据此不调任何窗口;旧版本弹出的遗留通知没有该字段,维持此前行为。
|
|
11
|
+
|
|
12
|
+
### 变更
|
|
13
|
+
- 版本号 1.5.0 → 1.5.1;`window.__dshNotifyMe.version` 同步。
|
|
14
|
+
- README(中英)同步:「点击通知回到对应对话」补多窗口口径,「工作原理」补 worker 侧的窗口调度说明。
|
|
15
|
+
|
|
16
|
+
### 测试
|
|
17
|
+
- `smoke/smoke-test.cjs` 快捷裁决桥用例补两个 window client 的点击路由断言:点正文只调起可见窗口并只让它切对话(其余客户端收 `navigate: false` 不动)、点正文不裁决;点按钮不调任何窗口也不切对话;`autoFocus` 关掉时通知 `data.focus` 记 `false`、点击既不调窗口也不切对话。
|
|
18
|
+
|
|
19
|
+
## [1.5.0] — 2026-10-03
|
|
20
|
+
|
|
21
|
+
### 新增
|
|
22
|
+
- **「完成未读」标题标记**:后台会话的回复跑完、你还没打开看的期间,标签页标题前出现 `✅ 回复完成 · …` / `✅ Reply finished · …`,打开该会话(宿主此时清掉未读)或它再次开跑即消失——离开屏幕再回来,一眼看到哪些跑完了。与「需要你」标记可同时出现(「需要你」在前),宿主改写标题后标记基于新标题重建。未读事实取自宿主:`≥ 0.1.6` 用 `sessionStatus` 行的 `completionUnread`(后台会话完成即点亮,打开 / 再次开跑 / 会话消失即清零),旧宿主用列表行的 `completed` 提醒位(选中会话即清零)。静音中的后台子代理会话照旧不标记;「页面打开时也提醒『回复完成』」开关只管通知,不影响标记。
|
|
23
|
+
- **「回复完成」通道接上宿主的完成未读标志**:此前只靠 `running` 快照边沿,页面没看见的完成(插件热重载中途、快照把 true→false 合并成一跳、完成发生在绑定之前)就永远漏掉。现在 `sessionStatus` 的 `completionUnread` 级别标志(0.1.6+)负责补漏——每条未读实例只报一次,绑定时已存在的未读也补报一声;边沿与级别标志是同一次完成的两个视角、同刻到达时按短时间窗合并成一声。边沿路径保持不变:当前会话的正文仍带回复摘要,旧宿主照旧。
|
|
24
|
+
- 「等你操作」不算完成:一轮带着 plan-review / 提问结束时只走「需要你」提醒,答完也不补「回复完成」马后炮;宿主仍标未读时,标题标记照常挂到你打开会话为止。后台子代理会话的未读照旧静音,解除静音后补报一次。
|
|
25
|
+
- `window.__dshNotifyMe.debug()` 新增 `doneUnreadMarked`(当前挂着「完成未读」标记的会话列表)。
|
|
26
|
+
|
|
27
|
+
### 变更
|
|
28
|
+
- 版本号 1.4.0 → 1.5.0;`window.__dshNotifyMe.version` 同步。
|
|
29
|
+
- README(中英)同步:「提醒时机」表、「提醒方式」的标题标记说明、「工作原理」的完成未读级别标志两代取源与合并口径。
|
|
30
|
+
|
|
31
|
+
### 测试
|
|
32
|
+
- `smoke/smoke-test.cjs` 新增 completionUnread 用例段:漏报补发、同刻双视角只响一声、绑定前已有的未读只报一次、等输入的停顿不算完成(答完不补马后炮、标记照旧)、当前会话不走未读通道、后台子代理静音与解除静音、页面可见时只留标记不弹通知、双标记叠加与宿主改标题后的重建;legacy 用例补 `completed` 提醒位的标记出现与「选中会话即撤」。
|
|
33
|
+
- `smoke/cordis-host-test.mjs` 在真 cordis `sessionStatus` 宿主上补一条走真 store 的完成未读用例:报告一次、标记、不重复、打开即撤。
|
|
34
|
+
|
|
5
35
|
## [1.4.0] — 2026-10-03
|
|
6
36
|
|
|
7
37
|
### 新增
|
package/README.en.md
CHANGED
|
@@ -43,7 +43,7 @@ dsh-notify-me decouples **supervision** from **sitting in front of the screen**:
|
|
|
43
43
|
| When | What you get | Default |
|
|
44
44
|
| --- | --- | --- |
|
|
45
45
|
| 🔔 **The agent needs your input** — sandbox approval / plan review / a question (`ask_user_question`) | Toast + sound + `🔔 Action needed ·` title marker; approval toasts also carry Approve / Reject buttons | Alerts even while the page is visible (the conversation on screen is the exception: tab marker only by default) |
|
|
46
|
-
| ✅ **A reply finishes** — a turn completes; background sessions finishing are also reported | Toast + sound | Alerts only while the page is hidden / backgrounded |
|
|
46
|
+
| ✅ **A reply finishes** — a turn completes; background sessions finishing are also reported | Toast + sound; while a background session is **finished but unread** the tab title carries a `✅ Reply finished ·` marker, gone once you open that session | Alerts only while the page is hidden / backgrounded (the title marker is not gated by this) |
|
|
47
47
|
|
|
48
48
|
> **Background** subagent child sessions are **not** alerted about by default: DSH lists each of them as its own session, but they are steps inside the parent conversation's turn, so an alert for them is only noise. The subagent session you have open is not part of this and alerts as usual. Settings → "Ignore subagent sessions" turns the mute off.
|
|
49
49
|
|
|
@@ -56,7 +56,7 @@ Open **Settings → Notify me** (refresh the page once after installing):
|
|
|
56
56
|
- **"Needs you" alerts while the page is open** (default on) and **"Reply finished" alerts while the page is open** (default off);
|
|
57
57
|
- **Stay quiet for the conversation on screen** (default on): 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 that alert back at once, unless you already handled it. Waits in other background sessions are unaffected;
|
|
58
58
|
- **Ignore subagent sessions** (default on): background subagent child sessions raise neither "needs you" nor "reply finished" — those are steps of the parent conversation's turn, and the parent's own alert already covers them. The subagent session you have open alerts as usual. Turn it off to alert for every session;
|
|
59
|
-
- **Click a toast to open that conversation** (default on): clicking brings DSH to the front and switches to the conversation the alert came from; when off, clicking a toast does nothing;
|
|
59
|
+
- **Click a toast to open that conversation** (default on): clicking brings DSH to the front and switches to the conversation the alert came from. With several DSH tabs open, one click raises exactly one window (a visible one first) and switches only that one — the others stay put; when off, clicking a toast does nothing;
|
|
60
60
|
- **Decide approvals from the toast** (default on): approval notifications carry Approve / Reject buttons, and one click settles the request exactly like the Allow once / Reject buttons on the approval card. Clicking the toast body still takes you back to the conversation;
|
|
61
61
|
- **Notification language** — follow the interface / 简体中文 / English: controls the language of the alert text and the `🔔 …` title marker;
|
|
62
62
|
- **Test buttons** — send one "needs you", "reply finished" or "approval buttons" test alert with the current settings (**not** limited by the "while the page is open" toggles; the "needs you" test's title marker clears itself after ~6s).
|
|
@@ -67,7 +67,7 @@ Open **Settings → Notify me** (refresh the page once after installing):
|
|
|
67
67
|
|
|
68
68
|
- **System notification** — a toast through the browser into the Windows action center / macOS Notification Center (clicking it opens the conversation it came from; approval toasts can be decided straight from the buttons)
|
|
69
69
|
- **Sound** — WebAudio beeps (distinct patterns for "needs you" vs "done")
|
|
70
|
-
- **Tab title marker** — while something is waiting on you, the tab title is prefixed with `🔔 Action needed · …` / `🔔 需要你 · …`
|
|
70
|
+
- **Tab title marker** — while something is waiting on you, the tab title is prefixed with `🔔 Action needed · …` / `🔔 需要你 · …`; while a background session is **finished but unread**, with `✅ Reply finished · …` / `✅ 回复完成 · …`, gone once you open that session (or it starts running again). Both kinds can be up together, "needs you" first; a host title change is rebased under the markers
|
|
71
71
|
|
|
72
72
|
This is a **browser-layer** plugin: the DSH page must stay open (minimized or backgrounded is fine — that's exactly the "away" state it watches for).
|
|
73
73
|
|
|
@@ -88,7 +88,7 @@ Restart `dsh web`, then hard-refresh the page (Ctrl+Shift+R).
|
|
|
88
88
|
| `0.1.2-alpha.2 .. 0.1.6-alpha.1` | `uiSession.pendingInteractions` | snapshot `current` | `sessions.open()` |
|
|
89
89
|
| `>= 0.1.6-alpha.2` (incl. `0.2.0-rc.2`) | `uiSession.sessionStatus` | `retainedBy.mainView` | `uiWorkspace.openSession()` |
|
|
90
90
|
|
|
91
|
-
The ranges are illustrative: the plugin picks its source by store shape (whenever `sessionStatus` exists it takes the new path); the version numbers only mark where each shape appeared.
|
|
91
|
+
The ranges are illustrative: the plugin picks its source by store shape (whenever `sessionStatus` exists it takes the new path); the version numbers only mark where each shape appeared. The `peerDependencies` range in `package.json` likewise only shapes npm's install warning — compatibility is declared per release in `dshReleases` after real testing, and untested new versions fall back to shape-driven behaviour.
|
|
92
92
|
|
|
93
93
|
Verified to actually activate on `0.1.2-rc.1`, `0.1.5-rc.2` and `0.2.0-rc.2` (each booted in its own profile: the Settings → Notifications section registers, i.e. the plugin's `apply` really runs), and previously on `0.1.1-rc.2`. Two traps are worth remembering: a package listed in `dsh.client.inject` that a newer runtime no longer ships parks the client entry at `pending (waiting for services: …)` forever — which is what broke 1.1.3 on `0.1.2-rc.1` and later; and when the host renames a store or a field nothing throws, alerts just go **silent** — `0.2.0-rc.2` moved `pendingInteractions`→`sessionStatus`, `list.current`→`retainedBy.mainView` and `sessions.open()`→`uiWorkspace.openSession()`. So every adaptation must confirm `window.__dshNotifyMe.debug()` reports `bound` and which source answered.
|
|
94
94
|
|
|
@@ -136,14 +136,14 @@ The reminder core picks its interaction source per host generation (see the tabl
|
|
|
136
136
|
|
|
137
137
|
- the **selected session's** `SessionSnapshot`: a `running: true → false` edge means a reply finished;
|
|
138
138
|
- **pending interactions**: a new key in `uiSession.sessionStatus` (`>= 0.1.6`, per-session `pendingInteraction`) or in `pendingInteractions` (`0.1.2 .. 0.1.5`) means the agent is waiting on you — approval requests carry the tool name and reason, questions their text; older hosts fall back to the controller snapshot's `pending[]`;
|
|
139
|
-
- **every other listed session's** summary: a `running: true → false` edge (or the legacy `completed` flag) alerts you about background work
|
|
139
|
+
- **every other listed session's** summary: a `running: true → false` edge (or the legacy `completed` flag) alerts you about background work. The host's completion-unread level flag backs that edge up and owns the title marker: `sessionStatus` rows' `completionUnread` on `>= 0.1.6` (lit when a background session finishes, cleared when it is opened / re-runs / disappears), the list row's `completed` reminder bit on older hosts (cleared on select). It re-reports a completion whose edge this page never saw (a plugin remount mid-turn, coalesced snapshots) exactly once, and when both views of one completion arrive together a short window collapses them into a single alert.
|
|
140
140
|
- **subagent sessions**: **background** rows carrying `origin: 'subagent'` (and a `parentId`) stay muted by default. The subagent session you have open is exempt — its alerts follow the normal current-conversation rules (tab marker only while the page is visible, delivered on background), its outstanding waits are picked up when you switch into it, and switching away mutes them again. A fork has a `parentId` but no `origin`, is your own conversation, and keeps alerting throughout.
|
|
141
141
|
|
|
142
142
|
The reminder core stays dependency-free and self-contained: notification copy resolves at alert time from the chosen language (follow-interface / Simplified Chinese / English). The Settings page is an **optional** React surface — it only registers into Settings when the web profile provides the `slots` / `locale` services and `react`; without them the plugin degrades gracefully to alerts-only (no Settings page).
|
|
143
143
|
|
|
144
144
|
### Quick decisions, and how they reach the agent
|
|
145
145
|
|
|
146
|
-
Action buttons (`actions`) belong to persistent notifications shown through `ServiceWorkerRegistration.showNotification()`; passing `actions` to the `new Notification()` constructor throws a `TypeError`. That is a browser rule no plugin can route around, so the bundle holds two jobs in the same bytes: the page half registers the reminder factory as it always did, and the very same file is registered as a Service Worker whose only task is relaying which button was clicked back to the page. The decision itself happens in the page, through the same `PendingApproval.answer('allowed-once' | 'rejected')` the approval card uses — which is why "Approve" is exactly "Allow once" and nothing broader.
|
|
146
|
+
Action buttons (`actions`) belong to persistent notifications shown through `ServiceWorkerRegistration.showNotification()`; passing `actions` to the `new Notification()` constructor throws a `TypeError`. That is a browser rule no plugin can route around, so the bundle holds two jobs in the same bytes: the page half registers the reminder factory as it always did, and the very same file is registered as a Service Worker whose only task is relaying which button was clicked back to the page. The decision itself happens in the page, through the same `PendingApproval.answer('allowed-once' | 'rejected')` the approval card uses — which is why "Approve" is exactly "Allow once" and nothing broader. The "raise a window and switch its conversation" part of a body click is scheduled on the worker half too: `client.focus()` raises exactly one window (a visible one first) and routes the conversation switch to that window alone, so several open DSH tabs never fight over focus or all switch at once.
|
|
147
147
|
|
|
148
148
|
Every button toast is booked against its interaction key, so it can only decide the request it was raised for. Once the page answers that request or a newer one replaces it, clicking the leftover buttons decides nothing and just closes the notification. Button toasts also skip the auto-dismiss timer: an undecided approval stays in the notification center until you settle it.
|
|
149
149
|
|
package/README.md
CHANGED
|
@@ -43,7 +43,7 @@ dsh-notify-me 把「监督」和「守在屏幕前」解耦:只在真的出现
|
|
|
43
43
|
| 时机 | 提醒内容 | 默认 |
|
|
44
44
|
| --- | --- | --- |
|
|
45
45
|
| 🔔 **模型需要你操作** — 审批请求 / 方案待确认(plan review)/ 提问(`ask_user_question`) | 通知 + 提示音 + `🔔 需要你 ·` 标题标记;审批的通知上直接带「同意 / 拒绝」按钮 | 页面可见也提醒(当前对话除外:默认只留标题标记) |
|
|
46
|
-
| ✅ **回复完成** — 一轮回复跑完;后台会话完成也会报 | 通知 +
|
|
46
|
+
| ✅ **回复完成** — 一轮回复跑完;后台会话完成也会报 | 通知 + 提示音;后台会话**完成但还没看**的期间标题带 `✅ 回复完成 ·` 标记,打开该会话即撤 | 仅页面隐藏/后台时提醒(标题标记不受此限) |
|
|
47
47
|
|
|
48
48
|
> **后台**子代理(subagent)子会话的等待与完成默认**不提醒**:DSH 把每个子代理子会话列成独立会话,但它们是所属主对话那一轮里的步骤,提醒只会是噪音。你当前打开的那个子代理会话不算在内,照常提醒。设置页「子代理会话不提醒」可关掉这个静音。
|
|
49
49
|
|
|
@@ -56,7 +56,7 @@ dsh-notify-me 把「监督」和「守在屏幕前」解耦:只在真的出现
|
|
|
56
56
|
- **页面打开时也提醒「需要你」**(默认开)与 **页面打开时也提醒「回复完成」**(默认关);
|
|
57
57
|
- **当前对话不弹通知**(默认开):等待就发生在你正看着的对话里时只留标题标记,不弹通知也不响音(免得盖住审批卡片);页面切到后台立刻补发,若你已经处理掉就作废。其它后台会话不受影响;
|
|
58
58
|
- **子代理会话不提醒**(默认开):后台子代理(subagent)子会话的「需要你」与「回复完成」都不提醒——它们是主对话那一轮的步骤,完成由主对话自己的提醒覆盖;你当前打开的那个子代理会话照常提醒;关掉后恢复逐会话提醒;
|
|
59
|
-
- **点击通知回到对应对话**(默认开):点通知把 DSH
|
|
59
|
+
- **点击通知回到对应对话**(默认开):点通知把 DSH 带到前台并切到提醒所属的对话;多个 DSH 标签页同时开着时,一次点击只调起一个窗口(优先当前可见的那个)并只切它的对话,其余标签页不动;关掉后点通知不做任何事;
|
|
60
60
|
- **审批通知上直接裁决**(默认开):权限审批的通知上带「同意 / 拒绝」按钮,点一下就按审批卡片的「允许一次 / 拒绝」直接裁决,不用切回页面;点通知正文仍是原来的「切回对应对话」;
|
|
61
61
|
- **通知语言**:跟随界面 / 简体中文 / English——决定通知文字与 `🔔 …` 标题标记使用的语言;
|
|
62
62
|
- **测试按钮**:用当前设置立即发一条「需要你」「回复完成」或「审批按钮」测试提醒(**不受**上面「页面打开时也提醒」开关限制;「需要你」测试的标题标记约 6 秒后自动消失)。
|
|
@@ -67,7 +67,7 @@ dsh-notify-me 把「监督」和「守在屏幕前」解耦:只在真的出现
|
|
|
67
67
|
|
|
68
68
|
- **系统通知**:经浏览器弹出的系统通知,进 Windows 通知中心或 macOS 通知中心(点击回到提醒所属的对话;审批的通知点按钮直接裁决)
|
|
69
69
|
- **提示音**:WebAudio 合成音(「需要你」与「完成」使用不同音型)
|
|
70
|
-
- **标签页标题标记**:有待处理事项时,标题前出现 `🔔 需要你 · …` / `🔔 Action needed · …`
|
|
70
|
+
- **标签页标题标记**:有待处理事项时,标题前出现 `🔔 需要你 · …` / `🔔 Action needed · …`;后台会话**完成但还没看**时出现 `✅ 回复完成 · …` / `✅ Reply finished · …`,打开对应会话(或它再次开跑)即消失。两种标记可同时出现,「需要你」在前;宿主改写标题后标记基于新标题重建
|
|
71
71
|
|
|
72
72
|
本插件是**浏览器层**实现:DSH 页面需保持打开(最小化/后台即可——那正是它监听的「离开」状态)。
|
|
73
73
|
|
|
@@ -88,7 +88,7 @@ dsh plugin --profile web add dsh-notify-me
|
|
|
88
88
|
| `0.1.2-alpha.2 .. 0.1.6-alpha.1` | `uiSession.pendingInteractions` | 快照 `current` | `sessions.open()` |
|
|
89
89
|
| `≥ 0.1.6-alpha.2`(含 `0.2.0-rc.2`) | `uiSession.sessionStatus` | `retainedBy.mainView` | `uiWorkspace.openSession()` |
|
|
90
90
|
|
|
91
|
-
区间仅作示意:插件按 store 形状自动选源(`sessionStatus`
|
|
91
|
+
区间仅作示意:插件按 store 形状自动选源(`sessionStatus` 存在即走新路),版本号只是各形状的出现边界。`package.json` 的 `peerDependencies` 范围同理只影响 npm install 的提示文案——兼容与否以 `dshReleases` 的逐版实测声明为准,未实测的新版本由形状驱动兜底。
|
|
92
92
|
|
|
93
93
|
实测**真正激活**(各自独立 profile 启动:设置里出现「通知提醒」分区,即插件的 `apply` 确实执行)于 `0.1.2-rc.1`、`0.1.5-rc.2` 与 `0.2.0-rc.2`;更早的 `0.1.1-rc.2` 亦验证过。两个坑值得记住:一是 `dsh.client.inject` 里列了新版运行时已不再提供的包,客户端条目会停在 `pending (waiting for services: …)` 而永不执行——1.1.3 在 `0.1.2-rc.1` 及以后正是这样失效的;二是宿主换存储/字段名时**不会报错**,提醒只会静默失效——`0.2.0-rc.2` 上 `pendingInteractions`→`sessionStatus`、`list.current`→`retainedBy.mainView`、`sessions.open()`→`uiWorkspace.openSession()` 都是这一类,所以每次适配都要用 `window.__dshNotifyMe.debug()` 确认真的是 `bound`、以及用的是哪个来源。
|
|
94
94
|
|
|
@@ -132,14 +132,14 @@ window.__dshNotifyMe.decide("approval:3", "allowed-once") // 程序化裁决:
|
|
|
132
132
|
|
|
133
133
|
- **当前会话** `SessionSnapshot`:`running` true→false = 回复完成;
|
|
134
134
|
- **待办交互**:`uiSession` 的 `sessionStatus`(`≥ 0.1.6`,逐会话 `pendingInteraction`)或 `pendingInteractions`(`0.1.2 .. 0.1.5`)出现新 key = 模型在等你,容器里带上审批工具名 / 理由、提问文本;旧宿主回落到控制器快照的 `pending[]`;
|
|
135
|
-
- **其它已列会话**摘要:`running` true→false(或旧宿主的 `completed` 边沿)=
|
|
135
|
+
- **其它已列会话**摘要:`running` true→false(或旧宿主的 `completed` 边沿)= 后台工作提醒。宿主的「完成未读」级别标志负责补漏与标题标记:`≥ 0.1.6` 用 `sessionStatus` 行的 `completionUnread`(后台会话完成即点亮、打开该会话/再次开跑/会话消失即清零),旧宿主用列表行的 `completed` 提醒位(选中会话即清零);页面没看见的完成边沿(插件热重载中途、快照合并跳变)由此补报一次,边沿与级别标志同来时按短时间窗合并,只响一声。
|
|
136
136
|
- **子代理会话**:列表行带 `origin: 'subagent'`(并有 `parentId`)的**后台**会话默认静音;你正打开着的那个子代理会话不算静音对象,它的提醒走当前对话的常规规则(页面可见时默认只留标题标记、转后台补发),切进来时积压的等待同样补上,切走后重新静音。只有 `parentId`、没有 `origin` 的分支(fork)会话属于你自己的对话,始终照常提醒。
|
|
137
137
|
|
|
138
138
|
提醒核心仍然零第三方运行时依赖、完全自包含:通知文字按所选语言(跟随界面 / 中文 / English)即时解析。设置页是**可选**的 React 呈现层——当 DSH web profile 提供 `slots` / `locale` / `react` 时才注册进「设置 → 通知提醒」,缺任一能力时插件自动降级为纯提醒(无设置页),不影响功能。
|
|
139
139
|
|
|
140
140
|
### 快捷裁决怎么走通的
|
|
141
141
|
|
|
142
|
-
通知按钮(`actions`)只属于 Service Worker 弹的持久通知,往 `new Notification()` 里传 `actions` 会直接抛 `TypeError`——这是绕不开的浏览器限制。所以 bundle 一份文件干两件事:页面里照常注册提醒工厂,同一份字节再注册成一个只干转信的 Service Worker,把「点了哪个按钮」(同意 / 拒绝 / 点正文)带回页面。真正的裁决发生在页面里,调的是审批卡片自己用的 `PendingApproval.answer('allowed-once' | 'rejected')
|
|
142
|
+
通知按钮(`actions`)只属于 Service Worker 弹的持久通知,往 `new Notification()` 里传 `actions` 会直接抛 `TypeError`——这是绕不开的浏览器限制。所以 bundle 一份文件干两件事:页面里照常注册提醒工厂,同一份字节再注册成一个只干转信的 Service Worker,把「点了哪个按钮」(同意 / 拒绝 / 点正文)带回页面。真正的裁决发生在页面里,调的是审批卡片自己用的 `PendingApproval.answer('allowed-once' | 'rejected')`,所以「同意」就是「允许一次」,不多放行任何东西。点正文的「调窗口 + 切对话」同样归 worker 这半侧调度:它用 `client.focus()` 只调起一个窗口(优先当前可见的),并只让那个窗口切对话——多个 DSH 标签页不会互相抢焦点或一起乱切。
|
|
143
143
|
|
|
144
144
|
每条按钮通知按交互 key 记账,一次只认领一条等待;请求被页面里答掉、或被新请求顶替之后,残留通知上的按钮再点也不会误裁,只是安静地关掉。带按钮的通知不设自动消失——审批没裁决就一直留在通知中心里等你。
|
|
145
145
|
|
package/lib/client.js
CHANGED
|
@@ -28,6 +28,13 @@
|
|
|
28
28
|
// * "sessions" -> the selected session's ConversationSnapshot (running)
|
|
29
29
|
// and the session-list summaries of every other session (running /
|
|
30
30
|
// completed), which carry the "reply finished" edge on every version.
|
|
31
|
+
// The host's completion-unread level flag backs that edge up and owns
|
|
32
|
+
// the "完成未读" title marker: `sessionStatus.completionUnread` on
|
|
33
|
+
// 0.1.6+ (armed when a background session finishes, cleared when it is
|
|
34
|
+
// opened / re-runs / disappears), the list row's `completed` reminder
|
|
35
|
+
// bit on older hosts. The edge path stays the reporter for the on-screen
|
|
36
|
+
// session (its snippet comes from the face snapshot); the two views of
|
|
37
|
+
// one completion are collapsed by reportDone's short dedupe window.
|
|
31
38
|
// The selected ("current") session is read from the list snapshot's
|
|
32
39
|
// `current` field where it exists (<= 0.1.5-rc.3) and otherwise derived
|
|
33
40
|
// from the mainView retention count (0.1.6+, the way the shipped
|
|
@@ -48,7 +55,8 @@
|
|
|
48
55
|
// card uses. Action buttons only exist on persistent notifications, so this
|
|
49
56
|
// file is dual-context: registered as a Service Worker as well, whose
|
|
50
57
|
// notificationclick relays the chosen button back to this page (broadcast +
|
|
51
|
-
// client postMessage)
|
|
58
|
+
// client postMessage) and, on a body click, raises exactly one window
|
|
59
|
+
// (client.focus()) and routes the conversation switch to that window alone. Where a worker cannot run (insecure context, no
|
|
52
60
|
// bundle URL) the plugin degrades to today's plain toast;
|
|
53
61
|
// - registers a "通知提醒 / Notify me" page into DSH Settings
|
|
54
62
|
// (settings.section slot) exposing an on/off master switch, per-channel
|
|
@@ -96,22 +104,92 @@
|
|
|
96
104
|
}
|
|
97
105
|
} catch (e) {}
|
|
98
106
|
}
|
|
107
|
+
// One relayed click; `navigate` says whether the receiving page may
|
|
108
|
+
// steer its window (raise it + switch to the alerted conversation).
|
|
109
|
+
function clickMsg(base, navigate) {
|
|
110
|
+
return {
|
|
111
|
+
source: base.source,
|
|
112
|
+
type: base.type,
|
|
113
|
+
action: base.action,
|
|
114
|
+
key: base.key,
|
|
115
|
+
sessionId: base.sessionId,
|
|
116
|
+
navigate: navigate
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
// A body click means "take me there", and both halves of that request are
|
|
120
|
+
// this handler's job. The window raise belongs here because only a
|
|
121
|
+
// notificationclick carries the click's user activation — the page-side
|
|
122
|
+
// window.focus() a plain relay triggers afterwards can lose the OS
|
|
123
|
+
// foreground race and leave DSH behind other windows. So exactly one
|
|
124
|
+
// window client is picked (a visible one first), that client alone gets
|
|
125
|
+
// `navigate: true` plus the focus() call, and every other recipient gets
|
|
126
|
+
// `navigate: false` and stays put: several open DSH tabs must not fight
|
|
127
|
+
// over focus or all switch their own conversation at once. Buttons stay
|
|
128
|
+
// hands-off — deciding from the toast must not drag a window up. The
|
|
129
|
+
// `data.focus` flag (the autoFocus setting at show time) keeps the
|
|
130
|
+
// "off = clicking does nothing" promise even though a worker cannot read
|
|
131
|
+
// the page's config; toasts from older versions carry no flag and keep
|
|
132
|
+
// the previous best-effort behavior.
|
|
133
|
+
function deliverClick(base, wantsFocus) {
|
|
134
|
+
var send = function (list) {
|
|
135
|
+
list = list || [];
|
|
136
|
+
var best = null;
|
|
137
|
+
if (wantsFocus) {
|
|
138
|
+
for (var i = 0; i < list.length; i++) {
|
|
139
|
+
var c = list[i];
|
|
140
|
+
if (!c || typeof c.postMessage !== "function") continue;
|
|
141
|
+
if (c.visibilityState === "visible") { best = c; break; }
|
|
142
|
+
if (!best) best = c;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
for (var j = 0; j < list.length; j++) {
|
|
146
|
+
try { list[j].postMessage(clickMsg(base, wantsFocus && list[j] === best)); } catch (e) {}
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
if (typeof g.BroadcastChannel === "function") {
|
|
150
|
+
var ch = new g.BroadcastChannel(BRIDGE_CHANNEL);
|
|
151
|
+
ch.postMessage(clickMsg(base, false));
|
|
152
|
+
try { ch.close(); } catch (e) {}
|
|
153
|
+
}
|
|
154
|
+
} catch (e) {}
|
|
155
|
+
if (best && typeof best.focus === "function") {
|
|
156
|
+
try {
|
|
157
|
+
var fp = best.focus();
|
|
158
|
+
if (fp && typeof fp.then === "function") fp.then(null, function () {});
|
|
159
|
+
} catch (e) {}
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
try {
|
|
163
|
+
if (g.clients && typeof g.clients.matchAll === "function") {
|
|
164
|
+
var p = g.clients.matchAll({ type: "window", includeUncontrolled: true });
|
|
165
|
+
if (p && typeof p.then === "function") {
|
|
166
|
+
p.then(send, function () { relay(clickMsg(base, wantsFocus)); });
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
} catch (e) {}
|
|
171
|
+
relay(clickMsg(base, wantsFocus));
|
|
172
|
+
}
|
|
99
173
|
g.addEventListener("notificationclick", function (event) {
|
|
100
174
|
try {
|
|
101
175
|
var n = event.notification;
|
|
102
176
|
var data = (n && n.data) || {};
|
|
103
|
-
var
|
|
177
|
+
var action = event.action || "";
|
|
178
|
+
var base = {
|
|
104
179
|
source: "dsh-notify-me",
|
|
105
180
|
type: "notification",
|
|
106
|
-
action:
|
|
181
|
+
action: action,
|
|
107
182
|
key: data.key || null,
|
|
108
183
|
sessionId: data.sessionId || null
|
|
109
184
|
};
|
|
110
185
|
try { if (n && typeof n.close === "function") n.close(); } catch (e) {}
|
|
186
|
+
var run = action
|
|
187
|
+
? function () { relay(base); }
|
|
188
|
+
: function () { deliverClick(base, data.focus !== false); };
|
|
111
189
|
if (typeof event.waitUntil === "function") {
|
|
112
|
-
event.waitUntil(Promise.resolve().then(
|
|
190
|
+
event.waitUntil(Promise.resolve().then(run));
|
|
113
191
|
} else {
|
|
114
|
-
|
|
192
|
+
run();
|
|
115
193
|
}
|
|
116
194
|
} catch (e) {}
|
|
117
195
|
});
|
|
@@ -156,7 +234,7 @@
|
|
|
156
234
|
toast: true,
|
|
157
235
|
sound: true,
|
|
158
236
|
volume: 0.5,
|
|
159
|
-
autoFocus: true, // clicking the toast
|
|
237
|
+
autoFocus: true, // clicking the toast raises DSH and switches to the alerted conversation
|
|
160
238
|
// Approval toasts carry 同意/拒绝 buttons and settle the request on one
|
|
161
239
|
// click (the same answer('allowed-once' | 'rejected') the in-page
|
|
162
240
|
// approval card sends). Buttons need persistent notifications, served
|
|
@@ -203,6 +281,7 @@
|
|
|
203
281
|
doneBody: "回复已完成",
|
|
204
282
|
background: "后台会话",
|
|
205
283
|
marker: "🔔 需要你 · ",
|
|
284
|
+
doneMarker: "✅ 回复完成 · ",
|
|
206
285
|
sep: ":",
|
|
207
286
|
approve: "同意",
|
|
208
287
|
reject: "拒绝",
|
|
@@ -226,6 +305,7 @@
|
|
|
226
305
|
doneBody: "Reply finished",
|
|
227
306
|
background: "Background session",
|
|
228
307
|
marker: "🔔 Action needed · ",
|
|
308
|
+
doneMarker: "✅ Reply finished · ",
|
|
229
309
|
sep: ": ",
|
|
230
310
|
approve: "Approve",
|
|
231
311
|
reject: "Reject",
|
|
@@ -406,6 +486,11 @@
|
|
|
406
486
|
if (!msg || msg.source !== "dsh-notify-me" || msg.type !== "notification") return;
|
|
407
487
|
if (msg.action === "approve") { decidePending(msg.key, "allowed-once"); return; }
|
|
408
488
|
if (msg.action === "reject") { decidePending(msg.key, "rejected"); return; }
|
|
489
|
+
// The worker picks the one window a click should land on (see the bridge):
|
|
490
|
+
// its `navigate: false` copies keep every other DSH tab from stealing
|
|
491
|
+
// focus or switching its own conversation. A message from before that
|
|
492
|
+
// flag existed carries none and falls back to the old best-effort route.
|
|
493
|
+
if (msg.navigate === false) return;
|
|
409
494
|
if (config.autoFocus) focusSession(msg.sessionId || null);
|
|
410
495
|
}
|
|
411
496
|
// The live interaction wins over the reference captured at alert time (the
|
|
@@ -506,7 +591,7 @@
|
|
|
506
591
|
// The plugin owns the audio (see showToast's silent note below).
|
|
507
592
|
silent: true,
|
|
508
593
|
icon: (location.origin || "") + "/favicon.svg",
|
|
509
|
-
data: { key: quick.key, sessionId: sessionId || null, kind: kind, test: !!quick.test },
|
|
594
|
+
data: { key: quick.key, sessionId: sessionId || null, kind: kind, test: !!quick.test, focus: config.autoFocus !== false },
|
|
510
595
|
actions: [
|
|
511
596
|
{ action: "approve", title: d.approve },
|
|
512
597
|
{ action: "reject", title: d.reject }
|
|
@@ -599,16 +684,25 @@
|
|
|
599
684
|
// title over (or an earlier load leaving a marker behind) can never make us
|
|
600
685
|
// mistake our own marker for the base title.
|
|
601
686
|
var attentionActive = 0;
|
|
687
|
+
var doneActive = 0; // "完成未读" marks, one per finished-but-unread session
|
|
602
688
|
var hostBase = null; // document.title as the host last left it
|
|
603
689
|
var writtenTitle = null; // exact string this plugin last put into document.title
|
|
604
|
-
// Our own marker
|
|
605
|
-
//
|
|
606
|
-
//
|
|
690
|
+
// Our own marker prefixes never belong to the base title — strip leftovers
|
|
691
|
+
// (an earlier load that was closed while a mark was up, or a title the host
|
|
692
|
+
// tracked while a marker was up) before storing the base. Both marker kinds
|
|
693
|
+
// can be up at once and either may lead, so strip repeatedly.
|
|
607
694
|
function cleanBase(t) {
|
|
608
695
|
var d = texts();
|
|
609
|
-
var prefixes = [d.marker, TXT.zh.marker, TXT.en.marker];
|
|
610
|
-
|
|
611
|
-
|
|
696
|
+
var prefixes = [d.marker, TXT.zh.marker, TXT.en.marker, d.doneMarker, TXT.zh.doneMarker, TXT.en.doneMarker];
|
|
697
|
+
var stripped = true;
|
|
698
|
+
while (stripped) {
|
|
699
|
+
stripped = false;
|
|
700
|
+
for (var i = 0; i < prefixes.length; i++) {
|
|
701
|
+
if (prefixes[i] && t.indexOf(prefixes[i]) === 0) {
|
|
702
|
+
t = t.slice(prefixes[i].length);
|
|
703
|
+
stripped = true;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
612
706
|
}
|
|
613
707
|
return t;
|
|
614
708
|
}
|
|
@@ -616,7 +710,15 @@
|
|
|
616
710
|
try {
|
|
617
711
|
var current = String(document.title || "");
|
|
618
712
|
if (current !== writtenTitle) hostBase = cleanBase(current);
|
|
619
|
-
var
|
|
713
|
+
var d = texts();
|
|
714
|
+
var prefix = "";
|
|
715
|
+
if (config.enabled) {
|
|
716
|
+
// "needs you" leads when both kinds are up: it is the one that wants
|
|
717
|
+
// action, the unread mark only says something finished.
|
|
718
|
+
if (attentionActive > 0) prefix += d.marker;
|
|
719
|
+
if (doneActive > 0) prefix += d.doneMarker;
|
|
720
|
+
}
|
|
721
|
+
var next = prefix + hostBase;
|
|
620
722
|
writtenTitle = next;
|
|
621
723
|
if (current !== next) document.title = next;
|
|
622
724
|
} catch (e) {}
|
|
@@ -629,6 +731,24 @@
|
|
|
629
731
|
if (attentionActive > 0) attentionActive -= 1;
|
|
630
732
|
applyTitle();
|
|
631
733
|
}
|
|
734
|
+
// Unread-reply marker: one per session id, held exactly while the host
|
|
735
|
+
// still calls the completion unread (sessionStatus.completionUnread on
|
|
736
|
+
// 0.1.6+, the list row's `completed` reminder bit on older hosts) and
|
|
737
|
+
// dropped the moment the session is opened — the tab then reads "something
|
|
738
|
+
// finished, waiting for you" until it is read.
|
|
739
|
+
var doneUnreadMarks = Object.create(null); // sid -> true
|
|
740
|
+
function markDoneUnread(sid) {
|
|
741
|
+
if (!sid || doneUnreadMarks[sid]) return;
|
|
742
|
+
doneUnreadMarks[sid] = true;
|
|
743
|
+
doneActive += 1;
|
|
744
|
+
applyTitle();
|
|
745
|
+
}
|
|
746
|
+
function unmarkDoneUnread(sid) {
|
|
747
|
+
if (!sid || !doneUnreadMarks[sid]) return;
|
|
748
|
+
delete doneUnreadMarks[sid];
|
|
749
|
+
if (doneActive > 0) doneActive -= 1;
|
|
750
|
+
applyTitle();
|
|
751
|
+
}
|
|
632
752
|
// Title marker ownership: on hosts that publish pending interactions
|
|
633
753
|
// (uiSession) that source owns the marker, but whichever source saw the wait
|
|
634
754
|
// first must still mark it (the other one may never report that key at all),
|
|
@@ -664,6 +784,14 @@
|
|
|
664
784
|
// release path deletes it) and is re-delivered once the page is backgrounded.
|
|
665
785
|
var quietedAttention = Object.create(null);
|
|
666
786
|
var lastAttentionAt = 0;
|
|
787
|
+
// One completion can reach this plugin twice: the running edge from the
|
|
788
|
+
// controller snapshots and the completionUnread level flag from
|
|
789
|
+
// sessionStatus are two views of the same host event and land in the same
|
|
790
|
+
// tick (the host publishes both stores from one manager update). A short
|
|
791
|
+
// per-session window collapses them; two genuine completions of one session
|
|
792
|
+
// are never that close — each needs a whole turn in between.
|
|
793
|
+
var doneFiredAt = Object.create(null); // sid -> last done report timestamp
|
|
794
|
+
var DONE_DEDUPE_MS = 250;
|
|
667
795
|
|
|
668
796
|
function hidden() { return document.hidden || document.visibilityState === "hidden"; }
|
|
669
797
|
|
|
@@ -804,6 +932,18 @@
|
|
|
804
932
|
if (snippet) body = body + "\n" + snippet;
|
|
805
933
|
fire("done", d.doneTitle, body, false, sessionId);
|
|
806
934
|
}
|
|
935
|
+
// Every "reply finished" report funnels through here, so the edge path and
|
|
936
|
+
// the completionUnread level flag cannot double-alert one completion. The
|
|
937
|
+
// timestamp is recorded even when the visibility rules suppress the toast:
|
|
938
|
+
// the report was handled (the unread marker is what persists), not queued.
|
|
939
|
+
function reportDone(sessionLabel, snippet, sessionId) {
|
|
940
|
+
if (sessionId) {
|
|
941
|
+
var now = Date.now();
|
|
942
|
+
if (doneFiredAt[sessionId] && now - doneFiredAt[sessionId] < DONE_DEDUPE_MS) return;
|
|
943
|
+
doneFiredAt[sessionId] = now;
|
|
944
|
+
}
|
|
945
|
+
fireDone(sessionLabel, snippet, sessionId);
|
|
946
|
+
}
|
|
807
947
|
// Test alerts deliberately ignore the visibility rules (attentionHiddenOnly
|
|
808
948
|
// / doneHiddenOnly): the whole point of a test button is to prove the
|
|
809
949
|
// channel works while you are looking at the page. Only the master switch
|
|
@@ -960,7 +1100,7 @@
|
|
|
960
1100
|
// 2) a non-selected session finished while we were away (sidebar "done")
|
|
961
1101
|
var notified = false;
|
|
962
1102
|
if (!muted && !row.face && row.completed && prev && !prev.completed && prev.seeded) {
|
|
963
|
-
|
|
1103
|
+
reportDone(sessionLabelText(row), "", row.id);
|
|
964
1104
|
notified = true;
|
|
965
1105
|
}
|
|
966
1106
|
|
|
@@ -970,7 +1110,7 @@
|
|
|
970
1110
|
if (Date.now() - lastAttentionAt < 300) {
|
|
971
1111
|
/* answered by an attention alert above */
|
|
972
1112
|
} else {
|
|
973
|
-
|
|
1113
|
+
reportDone(sessionLabelText(row), row.face ? snippetOfLastAssistant(row.faceSnap) : "", row.id);
|
|
974
1114
|
}
|
|
975
1115
|
}
|
|
976
1116
|
|
|
@@ -986,6 +1126,16 @@
|
|
|
986
1126
|
if (!uiPending && prev && prevPendingKeys.length > 0 && pendingNow.length === 0) {
|
|
987
1127
|
for (var r = 0; r < prevPendingKeys.length; r++) unmarkAttention(prevPendingKeys[r]);
|
|
988
1128
|
}
|
|
1129
|
+
|
|
1130
|
+
// Unread-reply marker on hosts whose reminder bit lives in the list row
|
|
1131
|
+
// (`completed`: armed when a non-selected session finishes, cleared on
|
|
1132
|
+
// select — the manager's completedNotifications). Hosts exposing
|
|
1133
|
+
// sessionStatus get their marker from syncDoneFromStatus instead; the
|
|
1134
|
+
// two never share a row, so the mark counter stays balanced.
|
|
1135
|
+
if (!statusDoneSource()) {
|
|
1136
|
+
if (row.completed && !muted) markDoneUnread(row.id);
|
|
1137
|
+
else unmarkDoneUnread(row.id);
|
|
1138
|
+
}
|
|
989
1139
|
}
|
|
990
1140
|
|
|
991
1141
|
// face -> row adapter (selected session, rich data)
|
|
@@ -1054,12 +1204,18 @@
|
|
|
1054
1204
|
bindCurrentFace();
|
|
1055
1205
|
bindUiSession();
|
|
1056
1206
|
// The subagent mute keys on "not the session on screen", and switching
|
|
1057
|
-
// sessions only emits on the list store — re-run the
|
|
1207
|
+
// sessions only emits on the list store — re-run the status pass so
|
|
1058
1208
|
// the decision tracks the switch: opening a muted subagent session
|
|
1059
1209
|
// announces its outstanding wait (under the current-conversation rules),
|
|
1060
1210
|
// leaving it puts the next edge back in the muted bucket.
|
|
1061
|
-
|
|
1211
|
+
evalUiStatus();
|
|
1062
1212
|
var byId = (st && st.byId) || {};
|
|
1213
|
+
// A vanished row drops its unread-reply mark with it: the host forgets
|
|
1214
|
+
// the reminder bit together with the row, and a gone row reports
|
|
1215
|
+
// nothing. (Hosts with sessionStatus sweep this in syncDoneFromStatus.)
|
|
1216
|
+
if (!statusDoneSource()) {
|
|
1217
|
+
for (var mk in doneUnreadMarks) if (!byId[mk]) unmarkDoneUnread(mk);
|
|
1218
|
+
}
|
|
1063
1219
|
for (var sid in byId) {
|
|
1064
1220
|
if (sid === cur) continue; // current handled by the face watcher
|
|
1065
1221
|
var sum = byId[sid];
|
|
@@ -1162,16 +1318,22 @@
|
|
|
1162
1318
|
uiStore = store;
|
|
1163
1319
|
uiSource = found.source;
|
|
1164
1320
|
uiPending = true; // this watcher now owns the title marker
|
|
1165
|
-
uiSub = store.subscribe(function () { try {
|
|
1321
|
+
uiSub = store.subscribe(function () { try { evalUiStatus(); } catch (e) {} });
|
|
1166
1322
|
noteUiBind("bound");
|
|
1167
|
-
|
|
1323
|
+
evalUiStatus(); // seed: a wait that predates this plugin still alerts once
|
|
1324
|
+
}
|
|
1325
|
+
// True once the sessionStatus store is the live uiSession source (0.1.6+):
|
|
1326
|
+
// its rows then also own the completion-unread level flag and its marker.
|
|
1327
|
+
function statusDoneSource() {
|
|
1328
|
+
return !!(uiPending && uiSource === "sessionStatus");
|
|
1168
1329
|
}
|
|
1169
|
-
function
|
|
1330
|
+
function evalUiStatus() {
|
|
1170
1331
|
var svc = lookupService("uiSession");
|
|
1171
1332
|
var found = interactionStore(svc);
|
|
1172
1333
|
if (!found || typeof found.store.getSnapshot !== "function") return;
|
|
1173
1334
|
var snap = found.store.getSnapshot(); // Map<sessionId, interaction|status>
|
|
1174
1335
|
if (!snap || typeof snap.forEach !== "function") return;
|
|
1336
|
+
if (found.source === "sessionStatus") syncDoneFromStatus(snap);
|
|
1175
1337
|
var seen = Object.create(null);
|
|
1176
1338
|
snap.forEach(function (value, sid) {
|
|
1177
1339
|
var it = pendingOf(value, found.source);
|
|
@@ -1210,6 +1372,42 @@
|
|
|
1210
1372
|
delete uiBaseline[sid2];
|
|
1211
1373
|
}
|
|
1212
1374
|
}
|
|
1375
|
+
// The sessionStatus rows also carry the reply-finished channel's level
|
|
1376
|
+
// flag (0.1.6-alpha.2+): `completionUnread` lights when a session that is
|
|
1377
|
+
// not on screen finishes and clears when the session is opened, starts
|
|
1378
|
+
// running again, or disappears. Two jobs live here:
|
|
1379
|
+
// * the "完成未读" title marker, held exactly while a background session
|
|
1380
|
+
// is finished-but-unread — open the session and the tab goes quiet;
|
|
1381
|
+
// * one done alert for completions whose running edge this page never saw
|
|
1382
|
+
// (coalesced list snapshots, a plugin remount mid-turn, a completion
|
|
1383
|
+
// that predates this bind). The latch reports each unread instance once;
|
|
1384
|
+
// reportDone's window collapses the double view when the edge did fire.
|
|
1385
|
+
// The edge path stays the reporter for the on-screen session (its snippet
|
|
1386
|
+
// comes from the face snapshot) and for hosts without this store.
|
|
1387
|
+
var uiDoneBaseline = Object.create(null); // sid -> true once this unread reported
|
|
1388
|
+
function syncDoneFromStatus(snap) {
|
|
1389
|
+
var want = Object.create(null);
|
|
1390
|
+
try {
|
|
1391
|
+
snap.forEach(function (value, sid) {
|
|
1392
|
+
if (!value) return;
|
|
1393
|
+
// A stop that awaits input (plan-review / question end the turn with
|
|
1394
|
+
// a pending interaction) is a wait, not a completion — the same rule
|
|
1395
|
+
// as the edge path's !hasPendingNow guard. The on-screen session is
|
|
1396
|
+
// never "unread" (the host clears the flag for it; its completion is
|
|
1397
|
+
// the face edge's job), and a background subagent step is muted.
|
|
1398
|
+
if (!value.completionUnread || value.pendingInteraction || isSubagentSession(sid) || currentSessionId() === sid) return;
|
|
1399
|
+
want[sid] = true;
|
|
1400
|
+
markDoneUnread(sid);
|
|
1401
|
+
if (uiDoneBaseline[sid]) return; // this unread instance already reported
|
|
1402
|
+
uiDoneBaseline[sid] = true;
|
|
1403
|
+
if (Date.now() - lastAttentionAt < 300) return; // same-tick attention owns the moment
|
|
1404
|
+
reportDone(labelForSessionId(sid), "", sid);
|
|
1405
|
+
});
|
|
1406
|
+
} catch (e) {}
|
|
1407
|
+
// answered / re-run / gone / muted: latch and mark go with the flag
|
|
1408
|
+
for (var s in uiDoneBaseline) if (!want[s]) delete uiDoneBaseline[s];
|
|
1409
|
+
for (var m in doneUnreadMarks) if (!want[m]) unmarkDoneUnread(m);
|
|
1410
|
+
}
|
|
1213
1411
|
function detachUiSession() {
|
|
1214
1412
|
if (uiSub) { try { uiSub(); } catch (e) {} uiSub = null; }
|
|
1215
1413
|
uiStore = null;
|
|
@@ -1218,6 +1416,8 @@
|
|
|
1218
1416
|
// go back to the controller marker path
|
|
1219
1417
|
for (var s in uiBaseline) unmarkAttention(uiBaseline[s].key);
|
|
1220
1418
|
uiBaseline = Object.create(null);
|
|
1419
|
+
for (var dm in doneUnreadMarks) unmarkDoneUnread(dm);
|
|
1420
|
+
uiDoneBaseline = Object.create(null);
|
|
1221
1421
|
uiPending = false;
|
|
1222
1422
|
}
|
|
1223
1423
|
|
|
@@ -1235,7 +1435,7 @@
|
|
|
1235
1435
|
});
|
|
1236
1436
|
} catch (e) {}
|
|
1237
1437
|
// keep in sync with package.json "version" on every release
|
|
1238
|
-
api.version = "1.
|
|
1438
|
+
api.version = "1.5.1";
|
|
1239
1439
|
api.setConfig = function (patch) { return setConfigLocal(patch); };
|
|
1240
1440
|
api.resetConfig = function () {
|
|
1241
1441
|
try { localStorage.removeItem(LS_CONFIG); } catch (e) {}
|
|
@@ -1286,6 +1486,7 @@
|
|
|
1286
1486
|
pendingBaselines: Object.keys(uiBaseline).length,
|
|
1287
1487
|
firedKeys: Object.keys(attentionKeysFired),
|
|
1288
1488
|
markedKeys: Object.keys(attentionKeysMarked),
|
|
1489
|
+
doneUnreadMarked: Object.keys(doneUnreadMarks),
|
|
1289
1490
|
quietedKeys: Object.keys(quietedAttention),
|
|
1290
1491
|
lastAttentionAt: lastAttentionAt ? new Date(lastAttentionAt).toISOString() : null
|
|
1291
1492
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-notify-me",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"description": "DSH 桌面消息提醒插件:模型需要你操作(审批/方案确认/提问的可操作提醒)或回复在后台完成时,系统通知+提示音+标签页标题提醒你;权限审批的通知上可直接点「同意/拒绝」完成裁决;可在设置页开关并切换通知语言。Desktop notification & message alerts for DeepSeek Harness web — toasts, sounds and a tab-title marker when the agent needs your input (approval / plan review / question) or a reply finishes in the background, Approve/Reject buttons on approval toasts to decide right from the notification; toggleable from Settings with zh/en notification language.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|