dsh-plugin-desktop-notify 0.5.6 → 0.5.7

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/README.en.md CHANGED
@@ -1,214 +1,77 @@
1
1
  # dsh-plugin-desktop-notify
2
2
 
3
- Codex-style **macOS notifications and a Dock unread badge** for the
4
- [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) desktop app.
5
-
6
- - A native banner, attributed to **DeepSeek Harness itself**, when a turn finishes
7
- **even while the window is in the background**, when a tool approval / question /
8
- plan review is waiting, or when a turn fails.
9
- - **Click the banner to jump back to that session** and raise the window.
10
- - The Dock icon keeps a number for every session that needs you until you actually
11
- look at it.
12
-
13
- [中文](README.md) · MIT
3
+ Desktop notifications and an app-icon badge for DSH: a finished turn, a pending approval or question, and a failure, with click-through back to that session.
14
4
 
15
5
  ## What it does
16
6
 
17
- | Event | Banner | Dock badge |
18
- |---|---|---|
19
- | A turn finishes while you are not watching it | “Task completed” + “「name」Completed in 3m 12s” | +1 |
20
- | A tool approval / question / plan review is waiting | “Approval needed” / “Your reply is needed” / “Plan review needed” | +1 |
21
- | A turn fails | “Task failed” + the error summary | +1 |
22
- | You open (and look at) that session | — | cleared |
23
-
24
- Rules:
25
-
26
- - **A notification always implies a badge**: both read the same attention facts.
27
- - **“Watching” means** the session is in the main view **and** the window is focused
28
- **and** visible. The harness’s own `completionUnread` only covers the first case;
29
- a backgrounded window is invisible to the host, so the plugin adds the
30
- focus/visibility half itself with `document.hasFocus()` / `visibilityState`.
31
- - **The badge is sticky** until you actually look at the session; the harness
32
- clearing `completionUnread` does not silently drop it.
33
- - A failed turn posts the error banner only, never an extra “completed” one.
34
- - Sessions of subagents / Agent Teams members are ignored.
35
-
36
- ## Requirements
37
-
38
- - Tested on **DSH Desktop 0.2.1-rc.3** (Electron **44.7.0**) on **macOS**.
39
- - The desktop shell must register `dsh-app://` as a secure context (current builds do),
40
- otherwise `navigator.setAppBadge` and `Notification` are unavailable
41
- (`capabilities()` in DevTools reports `badge: false`).
42
- - System Settings → Notifications → **DeepSeek Harness** must be allowed, otherwise
43
- you get the badge without banners.
44
- - **Windows / Linux: untested, but expected to work.** Both surfaces are plain web
45
- APIs: Electron has supported the Badging API on Windows since v29 (taskbar overlay,
46
- see [the v29.1.5 Windows badge fix](https://releases.electronjs.org/release/v29.1.5)),
47
- and `Notification` maps to Windows toasts. The real caveat is that Windows toasts
48
- need an AppUserModelID plus a Start Menu shortcut, and the DSH main process never
49
- calls `app.setAppUserModelId`, so whether banners appear depends on the shortcut
50
- the installer writes.
51
- - The only genuinely macOS-only code is the **optional host fallback**
52
- (`osascript` / `afplay` / `lsappinfo`), disabled by default.
53
- - To check a platform: run `capabilities()` (it reports `platform`) and then
54
- `simulateComplete()`, and open an issue with the result.
55
-
56
- ### Banner click does nothing?
57
-
58
- Read `__DSH_DESKTOP_NOTIFY__.state().lastClick`:
59
-
60
- - `null` → the click never reached the renderer. Rare on macOS, **common on Windows**:
61
- WinRT toast activation needs a registered COM activator
62
- ([electron/electron#24524](https://github.com/electron/electron/issues/24524)), and without
63
- one the click may simply dismiss the toast. A plugin cannot fix that layer.
64
- - `opened.ok: false` → the click arrived but the session switch failed; `opened.attempts`
65
- says why (`uiWorkspace` unreachable, `retain` failed, …). Since v0.5.2 the plugin declares
66
- `uiWorkspace` as a dependency (the same way the official `ui-chat` plugin does), which is
67
- what made the macOS click fail to switch sessions before.
68
- - `opened.ok: true` but the view did not change → the window was hidden; the click only
69
- activates the app, so bring it forward from the Dock/taskbar.
70
-
71
- The click path can be exercised directly with `__DSH_DESKTOP_NOTIFY__.openSession("<session id>")`.
72
-
73
- ### Windows: the click switches the session, but the window may stay behind
74
-
75
- Not something a plugin can fix. The click reaches the renderer (since v0.5.2
76
- `lastClick.opened.ok === true` and the session really switches), but raising the main
77
- window needs the main process (`BrowserWindow.show()` / `focus()`) or a registered
78
- AppUserModelID. The renderer only has `webContents.focus()`, and Windows' foreground
79
- lock ignores requests from non-foreground processes.
80
-
81
- Two upstream fixes, either works:
82
-
83
- 1. Call `app.setAppUserModelId("com.deepseek.dsh")` in the desktop main process and ship a
84
- Start Menu shortcut with the same AUMID — Windows then activates the app on toast
85
- clicks, so the window comes forward.
86
- 2. Expose a "focus window" IPC channel the renderer can call.
87
-
88
- Until then: if the window does not surface, click the taskbar/tray icon — **the session
89
- is already selected**.
90
-
91
- ### Windows: badge appears, no banner
92
-
93
- **Fixed in v0.4.4**: older builds passed a long `tag`
94
- (`dsh-plugin-desktop-notify:complete:<sessionId>`), and Windows 11 silently drops web
95
- notifications whose tag is longer than ~34 characters
96
- ([electron/electron#40433](https://github.com/electron/electron/issues/40433)). The
97
- plugin no longer sets `tag` / `silent`; dedupe relies on status edges, which is why a
98
- bare `new Notification("t",{body:"b"})` showed up while the plugin's banner did not.
99
-
100
- If it still fails after upgrading, use the checks below:
101
-
102
-
103
- The badge is a taskbar overlay and needs no AppUserModelID. **Windows toasts do need an
104
- AppUserModelID plus a Start Menu shortcut**, and the DSH main process never calls
105
- `app.setAppUserModelId` (checked `lib/main.js`), so Windows can drop the toast silently —
106
- it is not the plugin's logic. Confirm with:
107
-
108
- ```js
109
- new Notification("test", { body: "windows toast check" }) // nothing here means app-level
110
- __DSH_DESKTOP_NOTIFY__.state().lastNotify // the last delivery attempt
111
- ```
7
+ - **Turn finished** — including while the window is backgrounded or another app is in front
8
+ - **You are needed** — a tool approval, a question, or a plan waiting for confirmation
9
+ - **Turn failed** — the error is summarised in the body
10
+ - **Badge** — the app icon carries the number of sessions that need you, cleared once you have looked
11
+ - **Click a notification** — it switches to that session instead of only raising the app
112
12
 
113
- Two ways out: DSH sets `app.setAppUserModelId` upstream (then the current code shows toasts
114
- with click-through), or the host half falls back to a PowerShell toast (visible, but
115
- attributed to Windows PowerShell and without click-through).
13
+ Subagent sessions are skipped by default.
116
14
 
117
15
  ## Install
118
16
 
119
- Inside the DSH desktop app: **Plugins → Install**, then paste the package name:
17
+ DSH desktop → **Plugins** → Install, then paste the package name:
120
18
 
121
19
  ```
122
20
  dsh-plugin-desktop-notify
123
21
  ```
124
22
 
125
- Press **⌘R** afterwards so the renderer reloads the bundle (restart the app if it
126
- says `restart-required`). To update, uninstall and install it again.
23
+ Press **⌘R** afterwards. The GitHub address
24
+ (`https://github.com/chendaleiQ/dsh-plugin-desktop-notify`) and an absolute local
25
+ directory work the same way.
127
26
 
128
- Without access to npmjs.com (or offline), the repository address
129
- (`https://github.com/chendaleiQ/dsh-plugin-desktop-notify`) and a local checkout
130
- directory both work the same way.
131
- For development, symlink it into the profile:
27
+ ## Preferences
132
28
 
133
- ```bash
134
- git clone https://github.com/chendaleiQ/dsh-plugin-desktop-notify.git
135
- cd dsh-plugin-desktop-notify
136
- ln -s "$PWD" ~/.dsh/profiles/desktop/node_modules/dsh-plugin-desktop-notify
137
- # then add "dsh-plugin-desktop-notify" to dsh.profile.bundles in
138
- # ~/.dsh/profiles/desktop/package.json
139
- ```
29
+ Stored in localStorage under `dsh-plugin-desktop-notify.preferences`:
140
30
 
141
- ## Self-check
31
+ | Field | Default | Meaning |
32
+ | --- | --- | --- |
33
+ | `notifications` | `true` | Master switch |
34
+ | `notifyComplete` | `true` | Notify when a turn finishes |
35
+ | `notifyPending` | `true` | Notify for an approval / question / plan |
36
+ | `notifyError` | `true` | Notify when a turn fails |
37
+ | `notifyViewed` | `false` | Also notify for the session you are looking at |
38
+ | `badge` | `true` | Show the badge |
39
+ | `excludeSubagents` | `true` | Ignore subagent sessions |
40
+ | `titleComplete` / `titlePending` / `titleError` | empty | Custom titles; empty keeps the built-in copy |
41
+ | `maxBodyChars` | `140` | Body length limit |
142
42
 
143
- Open DevTools (`⌘⌥I`) in the app window:
43
+ ## Language
144
44
 
145
- ```js
146
- __DSH_DESKTOP_NOTIFY__.capabilities() // {notification:"granted", badge:true, ...}
147
- __DSH_DESKTOP_NOTIFY__.state() // live attention + badge number
148
- __DSH_DESKTOP_NOTIFY__.simulateComplete() // fire a banner and set the badge to 1
149
- __DSH_DESKTOP_NOTIFY__.reset() // undo the simulation
150
- __DSH_DESKTOP_NOTIFY__.state().lastClick // what the last banner click did
151
- __DSH_DESKTOP_NOTIFY__.openSession("<session id>") // run the click path manually
152
- ```
45
+ The copy follows DSH's language setting (Settings → General → Language): Chinese
46
+ by default, English once the app is switched.
153
47
 
154
- ## Preferences
48
+ ## Self-check
155
49
 
156
- No settings UI; preferences live in the renderer’s `localStorage`
157
- (desktop and web profiles are independent), applied after ⌘R:
50
+ From the renderer console:
158
51
 
159
52
  ```js
160
- localStorage.setItem("dsh-plugin-desktop-notify:preferences", JSON.stringify({
161
- notifications: true, // banners master switch
162
- notifyComplete: true,
163
- notifyPending: true, // approvals / questions / plan reviews
164
- notifyError: true,
165
- notifyViewed: false, // true = also banner the session you are watching
166
- badge: true, // Dock badge master switch
167
- excludeSubagents: true,
168
- // empty = built-in copy, following the UI language (zh/en)
169
- titleComplete: "",
170
- titlePending: "",
171
- titleError: "",
172
- maxBodyChars: 140
173
- }))
53
+ __DSH_DESKTOP_NOTIFY__.capabilities()
54
+ __DSH_DESKTOP_NOTIFY__.state()
55
+ __DSH_DESKTOP_NOTIFY__.report()
174
56
  ```
175
57
 
176
- ## How it works
177
-
178
- Both surfaces only exist in the desktop **renderer**, not in the Host process:
179
-
180
- | Capability | API | Lands at |
181
- |---|---|---|
182
- | Native banner + click callback | `new Notification(...)` / `onclick` | Electron → macOS Notification Center, attributed to DeepSeek Harness |
183
- | Dock badge | `navigator.setAppBadge(n)` / `clearAppBadge()` | Chromium → Electron `BadgeManager` → `NSDockTile.badgeLabel` |
58
+ ## Known limits
184
59
 
185
- Facts come from the harness’s own `uiSession.sessionStatus`
186
- (`completionUnread`, `pendingInteraction`, the same data the sidebar dots read) plus
187
- `api-session/error` for failures, so “read” means exactly what the official UI means.
188
-
189
- The Host half (`index.js`) is an **osascript fallback, disabled by default**: flip
190
- `enabled` in `cordis.patch.yml` if a renderer has no `Notification` (enabling both
191
- double-notifies).
60
+ - **Windows**: clicking a notification switches the session, but the window does
61
+ not always come to the front — the AppUserModelID and a main-process focus
62
+ channel are the desktop app's to provide
63
+ - **Attribution**: the notification centre shows DeepSeek Harness, because the
64
+ banner is a renderer-side native notification
65
+ - **Host half** (`index.js`, an osascript fallback) is off by default; switch it on
66
+ only when a renderer cannot post notifications
192
67
 
193
68
  ## Development
194
69
 
195
- Zero dependencies, hand-written ES modules, no build step.
196
-
197
70
  ```bash
198
- node test/client.test.mjs # renderer half: node:vm + stubbed globals
199
- node test/host.test.mjs /tmp/host-test.log # host fallback
200
- npm test
71
+ node test/client.test.mjs
72
+ node test/host.test.mjs
201
73
  ```
202
74
 
203
- ## Limitations
204
-
205
- - macOS is verified; Windows / Linux are untested (see Requirements).
206
- - “Stuck” covers waiting-for-approval / question / plan review / failure; a
207
- “still running but silent for N minutes” watchdog would need a host timer.
208
- - Badge facts live in host memory and reset when the app restarts (harness semantics).
209
- - Conflicts with `@caohaoyangcn/dsh-dock-badge` (badge-only, with a settings page):
210
- install only one, they fight over the same Dock badge.
211
-
212
75
  ## License
213
76
 
214
- MIT — use it however you like.
77
+ MIT · https://github.com/chendaleiQ/dsh-plugin-desktop-notify
package/README.md CHANGED
@@ -1,238 +1,71 @@
1
1
  # dsh-plugin-desktop-notify
2
2
 
3
- 给 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 桌面版补上 **Codex 风格的系统通知 + 程序坞小红点**:
3
+ 给 DSH 加**桌面通知 + 应用图标小红点**:任务完成、等待你操作、任务失败时提醒你,点通知直接回到那个会话。
4
4
 
5
- - 任务跑完(**包括窗口在后台/最小化时**)弹一条 **DeepSeek Harness 自己发的** macOS 横幅;
6
- - 有工具等你审批、有提问等你回答、有计划等你确认时弹「需要你确认」;
7
- - 任务出错时弹「任务出错」,正文带错误摘要;
8
- - **点横幅直接回到那个会话**并抬起窗口;
9
- - 需要你处理的会话在 Dock 图标上留一个数字,直到你真的看过它才清零。
5
+ ## 它做什么
10
6
 
11
- [English](README.en.md) · MIT
7
+ - **任务完成**——包括窗口切到后台、你在别的应用里的时候
8
+ - **等待你操作**——工具审批、提问、执行计划确认
9
+ - **任务失败**——这一轮出错时提示
10
+ - **小红点**——应用图标上显示「需要你的会话数」,你看过之后才清零
11
+ - **点击通知**——切到对应会话,而不是只把应用叫到前面
12
12
 
13
- ## 和 Codex 桌面版对齐
14
-
15
- | Codex 桌面版 | 这里 |
16
- |---|---|
17
- | Settings → Notifications → **Task completion** | 标题「任务已完成」,正文「对话名」已完成,用时 3 分 12 秒 |
18
- | Settings → Notifications → **Approval required** | 标题「等待你的批准」,正文「对话名」请求使用 Bash,等待你的批准 |
19
- | 等你回复(提问 / 计划确认) | 标题「等待你的回答」/「等待你的确认」,正文带会话名 |
20
- | 任务失败 | 标题「任务执行失败」,正文带会话名与错误摘要 |
21
- | Dock 徽标 | 数字 = **当前需要你的会话数**;打开该会话即清 |
22
-
23
- 规则:
24
-
25
- - **有通知就有红点**:两者读同一份 attention 事实,弹了横幅就一定进徽标。
26
- - **「在看」才算看过**:`主视图是它` **且** `窗口有焦点` **且** `窗口可见`,三者同时成立才不打扰。
27
- 宿主的 `completionUnread` 只覆盖第一种(切到别的会话);窗口被切到后台时它看不见,
28
- 这部分由插件用 `document.hasFocus()` / `visibilityState` 自己补——否则最常见的那种
29
- 「切到别的 App 等它跑完」永远不会提醒。
30
- - **红点粘住**:弹过横幅的会话,红点保留到你真的切回来并看着它;宿主把 `completionUnread`
31
- 清掉并不会让红点消失。
32
- - **失败不重复播**:一轮以错误结束时只发「任务出错」,不再补一条「任务完成」。
33
- - 子会话(subagent / Agent Teams 成员)不弹不计;同一会话同类横幅用 `tag` 覆盖,不堆一屏。
34
-
35
- ## 兼容性
36
-
37
- - **已在 macOS 实测**:DSH Desktop 0.2.1-rc.3(Electron 44.7.0)。
38
- - 需要桌面端把 `dsh-app://` 注册为 secure context(现版本就是这么做的),否则渲染层拿不到
39
- `navigator.setAppBadge` 与 `Notification`;`capabilities()` 自检里会显示 `badge: false`。
40
- - 系统设置 → 通知 → **DeepSeek Harness** 必须是允许状态,否则只有红点没有横幅。
41
- - **Windows / Linux:未实测,但理论上可用**。渲染层用的是标准 Web API:
42
- Electron 从 29 起支持 Windows 上的 Badging API(任务栏 overlay,修过 [Windows 下徽标文字不显示](https://releases.electronjs.org/release/v29.1.5)),
43
- `Notification` 走 Windows toast。真正的风险在 Windows 的 toast 需要 AppUserModelID +
44
- 开始菜单快捷方式,而 DSH 主进程没有显式 `setAppUserModelId`(我查过 `lib/main.js`),
45
- 所以能不能出横幅取决于安装包写的快捷方式。
46
- - **唯一确定只在 macOS 的是宿主兜底那半边**(`osascript` / `afplay` / `lsappinfo`),
47
- 它默认关闭,只在你没有 `Notification` 时才需要。
48
- - 想验证 Windows:跑一次 `capabilities()`(会带上 `platform`),再 `simulateComplete()`,
49
- 看 toast 与任务栏数字是否出现,把结果开个 issue 就行。
50
-
51
- ### 点击通知没反应?
52
-
53
- 先看 `__DSH_DESKTOP_NOTIFY__.state().lastClick`:
54
-
55
- - `null` → 点击事件根本没送到渲染层。macOS 上基本不会;**Windows 上很常见**:Electron 的 WinRT toast 点击激活需要 COM activator 注册([electron/electron#24524](https://github.com/electron/electron/issues/24524)),没注册时点横幅可能只是把系统通知收起。这一层插件修不了。
56
- - `opened.ok: false` → 点击到了,但切会话失败,`opened.attempts` 里写着原因(`uiWorkspace` 服务不可达 / `retain` 失败等)。
57
- v0.5.2 起插件把 `uiWorkspace` 声明成了依赖(和官方 `ui-chat` 插件一致),此前它在这个插件里是取不到的——这就是 macOS 上「点击只激活应用、不切会话」的原因。
58
- - `opened.ok: true` 但界面没变 → 窗口被藏起来了(点横幅只激活应用,不一定会显示窗口),手动点 Dock/任务栏图标把窗口调出来即可。
59
-
60
- 也可以直接验证切会话这条链路:`__DSH_DESKTOP_NOTIFY__.openSession("<会话 id>")`。
61
-
62
- ### Windows:点通知切会话成功,但窗口不一定到最前
63
-
64
- 这不是插件能修的:点击事件已经送到渲染层(v0.5.2 起 `lastClick.opened.ok === true`,会话也确实切了),
65
- 但把**主窗口提到前台**需要主进程的 `BrowserWindow.show()` / `focus()`,或者系统层面的
66
- AppUserModelID 注册;渲染层只有 `webContents.focus()`,而 Windows 的前台锁定会忽略来自
67
- 非前台进程的请求。
68
-
69
- 两种上游修法(任一即可):
70
-
71
- 1. 桌面端主进程调用 `app.setAppUserModelId("com.deepseek.dsh")`,并让安装包写出带同一 AUMID
72
- 的开始菜单快捷方式——Windows 就会在点击 toast 时激活应用,窗口自然到前台。
73
- 2. 主进程暴露一个「focus window」的 IPC 通道,让插件请求抬窗。
74
-
75
- 在此之前:点通知后若窗口没上来,点任务栏/托盘图标即可,**会话已经切好了**。
76
-
77
- ### Windows 上「有红点、没横幅」
78
-
79
- **v0.4.4 已修**:旧版本给每条通知带了一个很长的 `tag`(`dsh-plugin-desktop-notify:complete:<sessionId>`),
80
- 而 Windows 11 会**静默丢弃 tag 长度超过约 34 字符的 Web Notification**([electron/electron#40433](https://github.com/electron/electron/issues/40433))。
81
- 现在不再设置 `tag` / `silent`,去重改由状态边沿判断完成——所以裸 `new Notification("t",{body:"b"})` 能弹、
82
- 插件的不弹,正是这个原因。
83
-
84
- 如果升级后仍不弹,再按下面排查:
85
-
86
-
87
- 红点走的是任务栏 overlay,不需要 AppUserModelID;**Windows toast 需要 AppUserModelID +
88
- 开始菜单快捷方式**才会显示。DSH 主进程没有调用 `app.setAppUserModelId`(`lib/main.js` 里没有
89
- 这行),所以 toast 很可能被 Windows 静默丢弃——这不是插件逻辑问题。先确认:
90
-
91
- ```js
92
- new Notification("test", { body: "windows toast check" }) // 连这条都不弹 = 应用层问题
93
- __DSH_DESKTOP_NOTIFY__.state().lastNotify // 插件最后一次投递的结果
94
- ```
95
-
96
- 两条路:等 DSH 主进程补上 `app.setAppUserModelId`(那时现有代码就能正常出 toast 并支持点击);
97
- 或改用宿主侧 PowerShell toast 兜底(能提示,但归属会显示成 Windows PowerShell,点击无法跳回会话)。
13
+ 子智能体的会话默认不提醒(可在偏好里打开)。
98
14
 
99
15
  ## 安装
100
16
 
101
- ### 方式一:npm(推荐)
102
-
103
- DSH 桌面版左侧栏 **插件** → **安装**,填包名:
17
+ DSH 桌面版 → 左侧栏「插件」→ 安装,填包名:
104
18
 
105
19
  ```
106
20
  dsh-plugin-desktop-notify
107
21
  ```
108
22
 
109
- 装完按 **⌘R** 刷新窗口(渲染层要重新加载)。提示 `restart-required` 就重启一次应用。
110
- 更新:插件页里卸载再装一次即为最新版。
111
-
112
- > 国内网络可以给 npm 配镜像(`npm config set registry https://registry.npmmirror.com`),
113
- > 或者直接用下面的 GitHub / 本地方式。
114
-
115
- ### 方式二:从 GitHub 安装
116
-
117
- 插件页 → 安装 → 填仓库地址:
118
-
119
- ```
120
- https://github.com/chendaleiQ/dsh-plugin-desktop-notify
121
- ```
23
+ 装完按 **⌘R** 刷新窗口。也可以填 GitHub 地址
24
+ (`https://github.com/chendaleiQ/dsh-plugin-desktop-notify`)或本地目录的绝对路径。
122
25
 
123
- ### 方式三:本地目录
124
-
125
- ```bash
126
- git clone https://github.com/chendaleiQ/dsh-plugin-desktop-notify.git
127
- ```
26
+ ## 偏好
128
27
 
129
- 插件页 → 安装 → 填这个目录的**绝对路径**(目录,不是文件):
28
+ 存在浏览器 localStorage 的 `dsh-plugin-desktop-notify.preferences`:
130
29
 
131
- ```
132
- /path/to/dsh-plugin-desktop-notify
133
- ```
30
+ | 字段 | 默认 | 说明 |
31
+ | --- | --- | --- |
32
+ | `notifications` | `true` | 总开关 |
33
+ | `notifyComplete` | `true` | 任务完成时通知 |
34
+ | `notifyPending` | `true` | 等待审批 / 回答 / 计划确认时通知 |
35
+ | `notifyError` | `true` | 任务失败时通知 |
36
+ | `notifyViewed` | `false` | 正在看的会话也通知(默认跳过) |
37
+ | `badge` | `true` | 显示小红点 |
38
+ | `excludeSubagents` | `true` | 忽略子智能体会话 |
39
+ | `titleComplete` / `titlePending` / `titleError` | 空 | 自定义标题,空 = 用内置文案 |
40
+ | `maxBodyChars` | `140` | 正文长度上限 |
134
41
 
135
- ### 方式四:开发调试(直接 link 进 profile)
42
+ ## 语言
136
43
 
137
- ```bash
138
- git clone https://github.com/chendaleiQ/dsh-plugin-desktop-notify.git
139
- cd dsh-plugin-desktop-notify
140
- ln -s "$PWD" ~/.dsh/profiles/desktop/node_modules/dsh-plugin-desktop-notify
141
- # 再把 dsh-plugin-desktop-notify 加进 ~/.dsh/profiles/desktop/package.json 的 dsh.profile.bundles
142
- ```
143
-
144
- 改完代码按 ⌘R 重载渲染层即可(Host 那半边改动需要重启应用)。
44
+ 跟随 DSH 的语言设置(设置 → 通用 → 语言):默认中文,切成英文后通知文案同步变成英文。
145
45
 
146
46
  ## 自检
147
47
 
148
- 窗口里按 `⌘⌥I` 打开 DevTools,Console:
48
+ 渲染层控制台:
149
49
 
150
50
  ```js
151
- __DSH_DESKTOP_NOTIFY__.capabilities() // {notification:"granted", badge:true, ...}
152
- __DSH_DESKTOP_NOTIFY__.state() // 当前 attention 与徽标数字
153
- __DSH_DESKTOP_NOTIFY__.simulateComplete() // 立刻弹一条横幅并把 Dock 置 1
154
- __DSH_DESKTOP_NOTIFY__.reset() // 清掉自检状态
155
- __DSH_DESKTOP_NOTIFY__.state().lastClick // 上一次点横幅发生了什么
156
- __DSH_DESKTOP_NOTIFY__.openSession("<会话 id>") // 直接按点击逻辑切会话,验证服务可达
51
+ __DSH_DESKTOP_NOTIFY__.capabilities() // 平台、通知权限、小红点、服务可达性
52
+ __DSH_DESKTOP_NOTIFY__.state() // 小红点、待处理会话、最近一次通知/点击
53
+ __DSH_DESKTOP_NOTIFY__.report() // 上面全部 + 窗口焦点,便于排查
157
54
  ```
158
55
 
159
- 手工验收:让一个会话跑十几秒,**切到别的 App**(失焦即可,不必切会话),跑完应看到横幅 + Dock `1`;
160
- 点横幅回来,数字清零。
161
-
162
- ## 偏好
163
-
164
- 没有设置界面,偏好放在渲染进程本地(desktop / web profile 各自独立),控制台执行后 ⌘R:
165
-
166
- ```js
167
- localStorage.setItem("dsh-plugin-desktop-notify:preferences", JSON.stringify({
168
- notifications: true, // 总开关:横幅
169
- notifyComplete: true, // 任务完成
170
- notifyPending: true, // 等审批 / 提问 / 计划确认
171
- notifyError: true, // 任务失败
172
- notifyViewed: false, // true = 盯着看它也弹(但不进红点)
173
- badge: true, // 总开关:Dock 红点
174
- excludeSubagents: true, // 子会话不弹不计(建议保持)
175
- // 留空 = 用内置文案(跟随界面语言 zh/en)
176
- titleComplete: "",
177
- titlePending: "",
178
- titleError: "",
179
- maxBodyChars: 140
180
- }))
181
- // 恢复默认
182
- localStorage.removeItem("dsh-plugin-desktop-notify:preferences")
183
- ```
184
-
185
- ## 为什么是渲染层插件
186
-
187
- 这两件事只能在桌面**渲染进程**做,Host 子进程碰不到:
188
-
189
- | 能力 | 接口 | 去处 |
190
- |---|---|---|
191
- | 原生横幅 + 点击回调 | `new Notification(...)` / `onclick` | Electron → macOS 通知中心,**归属 DeepSeek Harness** |
192
- | Dock 小红点 | `navigator.setAppBadge(n)` / `clearAppBadge()` | Chromium → Electron `BadgeManager` → `NSDockTile.badgeLabel` |
193
-
194
- 宿主半边(`index.js`)保留成**默认关闭的 osascript 兜底**:万一某个渲染环境没有 `Notification`,
195
- 把 `cordis.patch.yml` 里的 `enabled` 改成 `true` 再重启——两者同时开会重复提醒。
196
-
197
- 数据源用的是 Harness 自己的 `uiSession.sessionStatus`(`completionUnread` / `pendingInteraction`,
198
- 和侧边栏状态点同源)+ `api-session/error`(失败),插件不自己记账,所以「读过」的语义和官方 UI 一致。
199
-
200
- ## 目录
201
-
202
- ```
203
- dsh-plugin-desktop-notify/
204
- ├── package.json # dsh.bundle.patch + dsh.client(渲染层入口与 inject 顺序)
205
- ├── cordis.patch.yml # desktop-notify 一行(宿主兜底默认 enabled: false)
206
- ├── lib/client.js # 渲染层:横幅 + 点击跳转 + Dock 徽标
207
- ├── index.js # Host 半边:osascript 兜底,默认关闭
208
- ├── test/client.test.mjs # 渲染层测试(node:vm + 假浏览器全局)
209
- ├── test/host.test.mjs # 宿主兜底测试
210
- ├── icon.svg / locale/{en,zh}.json / README(.en).md / LICENSE
211
- ```
56
+ ## 已知限制
212
57
 
213
- ## 开发与测试
58
+ - **Windows**:点通知会切到对应会话,但窗口不一定到前台——`AppUserModelID` 和主进程 focus 通道属于桌面端,渲染层插件做不到
59
+ - **通知归属**:系统通知中心里显示为 DeepSeek Harness(用的是渲染层原生通知)
60
+ - **宿主半边**(`index.js`,osascript 兜底)默认关闭;只有渲染层拿不到 `Notification` 时才需要打开
214
61
 
215
- 零依赖,手写 ES 模块,无构建步骤。
62
+ ## 开发
216
63
 
217
64
  ```bash
218
- node test/client.test.mjs # 渲染层:node:vm + 假 window/navigator/Notification
219
- node test/host.test.mjs /tmp/host-test.log # 宿主兜底:假 ctx + 合成 session/event
220
- npm test # 两个都跑
65
+ node test/client.test.mjs # 渲染层:文案、小红点、点击链路
66
+ node test/host.test.mjs # 宿主兜底:dryRun 下的事件处理
221
67
  ```
222
68
 
223
- 渲染层测试覆盖 40+ 项断言:边沿只弹一次、后台窗口算「没在看」、红点粘住到真看过、
224
- 完成/审批/失败各自增减、失败不重复播、子会话排除、各偏好开关。
225
-
226
- ## 已知限制
227
-
228
- - macOS 已实测;Windows / Linux 未实测(见上面「兼容性」)。
229
- - 「卡住」目前覆盖**等待审批 / 提问 / 计划确认 / 失败**;没有做「还在 running 但长时间无输出」
230
- 的超时判定(需要宿主侧定时器)。
231
- - `completionUnread` 与失败事实在宿主内存里,重启应用从零开始(官方语义)。
232
- - 点击横幅若对应会话已归档,只会抬窗。
233
- - 与 [`@caohaoyangcn/dsh-dock-badge`](https://www.npmjs.com/package/@caohaoyangcn/dsh-dock-badge)
234
- 功能重叠(那个只做徽标、带设置页),**两个都装会抢写同一个 Dock 徽标,只留一个**。
235
-
236
- ## License
69
+ ## 协议
237
70
 
238
- MIT,随便用、随便改、随便发。
71
+ MIT · 仓库 https://github.com/chendaleiQ/dsh-plugin-desktop-notify
package/lib/client.js CHANGED
@@ -91,7 +91,7 @@ window.__ModuleLoader__.load({
91
91
  return name === "" ? text : `「${name}」${text}`
92
92
  }
93
93
 
94
- /** The UI language drives the copy; the app always sets `<html lang>`. */
94
+ /** The UI language before the app's locale service answers: `<html lang>`, then the browser. */
95
95
  function activeLanguage() {
96
96
  try {
97
97
  const html = typeof document === "undefined" ? "" : document.documentElement?.lang ?? ""
@@ -104,14 +104,14 @@ window.__ModuleLoader__.load({
104
104
  return "zh"
105
105
  }
106
106
 
107
- /** The copy table for the current UI language. */
108
- function copy() {
109
- return COPY[activeLanguage()] ?? COPY.zh
107
+ /** The copy table for one UI language. */
108
+ function copy(language) {
109
+ return COPY[language] ?? COPY.zh
110
110
  }
111
111
 
112
112
  /** Compact elapsed time: "42 秒" / "3 分 12 秒" / "1 小时 5 分". */
113
- function durationText(milliseconds) {
114
- const table = copy()
113
+ function durationText(milliseconds, language) {
114
+ const table = copy(language)
115
115
  const total = Math.max(1, Math.round(milliseconds / 1000))
116
116
  if (total < 60) return table.seconds(total)
117
117
  const minutes = Math.floor(total / 60)
@@ -243,12 +243,28 @@ window.__ModuleLoader__.load({
243
243
  }
244
244
  }
245
245
 
246
- const inject = ["sessions", "uiSession", "uiWorkspace"]
246
+ const inject = ["sessions", "uiSession", "uiWorkspace", "locale"]
247
247
 
248
248
  /**
249
249
  * @param ctx - Client root context.
250
250
  */
251
251
  function apply(ctx) {
252
+ /**
253
+ * The app's own language, in the order DSH resolves it: the locale service
254
+ * (Settings → General), then `<html lang>`, then the browser. Chinese is
255
+ * the default; English is the only other language this plugin ships.
256
+ */
257
+ const languageOf = () => {
258
+ try {
259
+ const active = String(ctx.locale?.getSnapshot?.().active ?? "").toLowerCase()
260
+ if (active.startsWith("zh")) return "zh"
261
+ if (active.startsWith("en")) return "en"
262
+ } catch {
263
+ /* an older host without the locale service falls back below */
264
+ }
265
+ return activeLanguage()
266
+ }
267
+
252
268
  const preferences = readPreferences()
253
269
  askPermission()
254
270
 
@@ -439,13 +455,13 @@ window.__ModuleLoader__.load({
439
455
  if (!turnStartedAt.has(sessionId)) turnStartedAt.set(sessionId, Date.now())
440
456
  }
441
457
 
442
- const table = copy()
458
+ const table = copy(languageOf())
443
459
  const name = sessionName(summary, preferences.maxBodyChars)
444
460
 
445
461
  if (completionEdge) {
446
462
  const started = turnStartedAt.get(sessionId)
447
463
  turnStartedAt.delete(sessionId)
448
- const duration = started === undefined ? "" : durationText(Date.now() - started)
464
+ const duration = started === undefined ? "" : durationText(Date.now() - started, languageOf())
449
465
  const title = preferences.titleComplete || table.completeTitle
450
466
  const body = bodyText(table.complete(name, duration), preferences.maxBodyChars)
451
467
  if (looking) {
@@ -498,7 +514,7 @@ window.__ModuleLoader__.load({
498
514
  const summary = summaries[sessionId]
499
515
  if (preferences.excludeSubagents && isSubagent(summary)) return
500
516
  if (isLooking(summary)) return
501
- const table = copy()
517
+ const table = copy(languageOf())
502
518
  const raw = typeof message === "string" ? message.trim() : ""
503
519
  const text = bodyText(raw, preferences.maxBodyChars)
504
520
  const before = attention.get(sessionId) ?? {}
@@ -622,9 +638,9 @@ window.__ModuleLoader__.load({
622
638
  }
623
639
  },
624
640
  simulateComplete(sessionId = "desktop-notify-simulation") {
625
- const table = copy()
641
+ const table = copy(languageOf())
626
642
  setAttention(sessionId, { completed: true })
627
- notify("complete", sessionId, preferences.titleComplete || table.completeTitle, table.complete("", durationText(12000)))
643
+ notify("complete", sessionId, preferences.titleComplete || table.completeTitle, table.complete("", durationText(12000, languageOf())))
628
644
  applyBadge()
629
645
  return this.state()
630
646
  },
package/locale/en.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "meta": {
3
- "title": "Desktop Notifications & Dock Badge",
4
- "description": "Codex-style alerts: a macOS banner for a finished turn, a pending approval or question, and a failure, plus a Dock badge counting the sessions that need you. Click the banner to open that session."
3
+ "title": "Desktop Notifications & Badge",
4
+ "description": "A system notification when a turn finishes, an approval or question is waiting, or a task fails, plus a badge on the app icon counting the sessions that need you. Click the notification to open that session."
5
5
  }
6
6
  }
package/locale/zh.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "meta": {
3
3
  "title": "桌面通知与小红点",
4
- "description": "对齐 Codex:任务完成、等待审批/回答、任务失败时弹 macOS 横幅,程序坞图标显示待处理会话数,点横幅回到该会话。"
4
+ "description": "任务完成、等待审批或回答、任务失败时弹系统通知,应用图标显示待处理数量;点通知直接回到那个会话。"
5
5
  }
6
6
  }
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "dsh-plugin-desktop-notify",
3
- "version": "0.5.6",
3
+ "version": "0.5.7",
4
4
  "type": "module",
5
- "description": "Codex-style macOS notifications and Dock badge for DeepSeek Harness: completion (including backgrounded windows), pending approval/question/plan, and failures, with click-through back to the session",
5
+ "description": "Desktop notifications and a Dock/taskbar badge for DeepSeek Harness: a finished turn, a pending approval or question, and a failure, with click-through back to the session",
6
6
  "license": "MIT",
7
7
  "main": "index.js",
8
8
  "exports": {