@kesike/reminder-dsh 0.2.0 → 0.2.2
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.md +37 -32
- package/README.zh-CN.md +32 -106
- package/example.patch.yml +1 -0
- package/lib/client.js +193 -57
- package/package.json +2 -2
- package/src/index.js +5 -1
- package/src/modules/popup/ToastActivator.cs +33 -5
- package/src/modules/popup/ToastIdentity.cs +71 -0
- package/src/modules/popup/approval.js +60 -0
- package/src/modules/popup/index.js +53 -8
- package/src/modules/popup/open.js +37 -0
- package/src/modules/popup/reply.js +49 -18
- package/src/modules/popup/summary.js +16 -3
- package/src/modules/popup/toast.js +37 -13
- package/src/modules/popup/toast.ps1 +77 -18
- package/src/modules/taskbar/badge.ps1 +237 -0
- package/src/modules/taskbar/index.js +2 -2
- package/src/modules/taskbar/windows.js +16 -13
- package/src/reminder.js +13 -1
- package/src/modules/taskbar/flash.ps1 +0 -107
package/README.md
CHANGED
|
@@ -1,63 +1,68 @@
|
|
|
1
1
|
# reminder-dsh
|
|
2
2
|
|
|
3
|
-
Windows reminders for DeepSeek Harness,
|
|
3
|
+
Native Windows reminders for DeepSeek Harness (DSH). Completion and approval toasts use the session title, and you can reply or handle an approval from the notification. An optional taskbar badge counts completions while the DSH window is in the background.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
- **Windows system notifications:** show an AI task summary in a native notification banner and Notification Center. The reply field sends a message to the original DSH session.
|
|
5
|
+
DeepSeek Harness 的 Windows 提醒插件。完成和审批通知以会话标题为标题,支持可选 AI 总结、通知内回复和审批,也可以在任务栏显示未查看的完成数。打开 **设置 → 通知** 即可配置。
|
|
7
6
|
|
|
8
|
-
[
|
|
7
|
+
[简体中文安装与使用说明](https://github.com/KeS1Ke/reminder-dsh/blob/main/README.zh-CN.md) · [npm package](https://www.npmjs.com/package/@kesike/reminder-dsh)
|
|
9
8
|
|
|
10
|
-
## Install
|
|
9
|
+
## Install or update / 安装或更新
|
|
11
10
|
|
|
12
|
-
|
|
11
|
+
Requirements are Windows 10 or 11 with an interactive desktop session, Node.js 24.9.0 or later, and DSH `>=0.2.2-rc.1 <0.3.0`. Toasts use the built-in Windows PowerShell. The taskbar badge needs PowerShell 7 (`pwsh.exe` on PATH). Both run on the machine hosting the DSH backend.
|
|
13
12
|
|
|
14
13
|
```powershell
|
|
15
|
-
dsh plugin --profile
|
|
16
|
-
dsh --profile web
|
|
14
|
+
dsh plugin --profile desktop add @kesike/reminder-dsh
|
|
17
15
|
```
|
|
18
16
|
|
|
19
|
-
|
|
17
|
+
Restart DSH Desktop after installation or an update. For DSH Web, use the profile that runs your Windows backend, for example `dsh plugin --profile web add @kesike/reminder-dsh`, then restart that backend. For a GitHub install, use `github:KeS1Ke/reminder-dsh` in place of the npm package name.
|
|
20
18
|
|
|
21
|
-
|
|
19
|
+
桌面版执行上面的命令后重启 DSH。Web 版请把 `desktop` 换成实际运行 Windows 后端的 profile。系统通知使用 Windows 自带的 PowerShell,任务栏角标需要 PATH 中有 PowerShell 7。
|
|
22
20
|
|
|
23
|
-
|
|
21
|
+
## Choose what you see
|
|
24
22
|
|
|
25
|
-
|
|
23
|
+
Open **Settings → Notifications** in DSH.
|
|
26
24
|
|
|
27
|
-
|
|
25
|
+
| Setting | Default | What it controls |
|
|
26
|
+
| --- | --- | --- |
|
|
27
|
+
| Windows system notifications | On | Native completion and approval toasts |
|
|
28
|
+
| Show task summary in notifications | On | A short AI summary of the final answer in completion toasts |
|
|
29
|
+
| Notification interval | 10 seconds | Minimum time between completion toast submissions, from 1 to 300 seconds |
|
|
30
|
+
| Taskbar completion badge | Off | Unread completion count on the DSH taskbar icon |
|
|
31
|
+
| Target window title contains | Blank | Automatic window matching, or a unique title substring you specify |
|
|
28
32
|
|
|
29
|
-
|
|
33
|
+
Apply switch changes together with **Save** at the bottom of the page. **Cancel** discards pending switch changes. The title and interval fields have separate save buttons. Saved settings apply without restarting the plugin.
|
|
30
34
|
|
|
31
|
-
|
|
35
|
+
## Notifications and taskbar badge
|
|
32
36
|
|
|
33
|
-
|
|
37
|
+
- **Completion.** A toast follows a successful main-session turn once the session becomes idle. Concurrent completions are queued in order. Subagents, cancelled or failed turns, and restored history are excluded. The session title identifies the conversation, with a configured fallback when no title is available.
|
|
38
|
+
- **AI summary.** The plugin makes an auxiliary request through the session's configured model and asks for a short Chinese summary of its final text reply. It can add model usage. A failed summary falls back to a labeled excerpt. Disable summaries to skip this request.
|
|
39
|
+
- **Quick reply.** Send one reply per completion toast back to its original session within 24 hours while the original session and backend are available.
|
|
40
|
+
- **Approval.** The toast shows the requesting tool and reason. **Allow** grants the current request once and **Reject** declines it. The toast and the in-session card share the same pending approval.
|
|
41
|
+
- **Taskbar badge.** Completed turns increase the count while the target window is in the background. Bringing it to the foreground clears the count. Counts above 99 appear as `99+`. For DSH Web, leave the DSH tab selected in its browser window.
|
|
34
42
|
|
|
35
|
-
|
|
43
|
+
## Opening a session from a toast
|
|
36
44
|
|
|
37
|
-
|
|
45
|
+
Click the body of a completion or approval toast to request its original session. The backend keeps the latest click until the focused DSH interface acknowledges it. Version 0.2.2 fixes an inherited Electron environment variable that could prevent `dsh://open` from starting Desktop in GUI mode.
|
|
38
46
|
|
|
39
|
-
|
|
47
|
+
Windows click delivery and foreground activation can still be intermittent. If DSH stays in the background or the session does not switch, open DSH and select the session named in the notification. Test with a fresh toast after upgrading or restarting. Existing toasts can outlive the backend and their local routes.
|
|
40
48
|
|
|
41
|
-
-
|
|
42
|
-
- `src/modules/popup/`: summaries, submission queue and native Windows toast delivery.
|
|
43
|
-
- `src/reminder.js`: shared main-session completion detection.
|
|
44
|
-
- `lib/client.js`: settings for both modules, using DSH ConfigForms.
|
|
49
|
+
Windows manages banners and Notification Center. The plugin uses a per-user notification identity and, when it finds DSH Desktop, a per-user Start menu shortcut. Interaction routes run through the plugin's local activator and named pipe.
|
|
45
50
|
|
|
46
|
-
|
|
51
|
+
## Profile configuration and removal
|
|
47
52
|
|
|
48
|
-
|
|
53
|
+
Profile overrides use the `dsh-reminder` entry in `cordis.patch.yml` with separate `popup` and `taskbar` settings. See [example.patch.yml](https://github.com/KeS1Ke/reminder-dsh/blob/main/example.patch.yml). If you are migrating from `@kesike/dsh-taskbar-reminder`, remove that package and rename its `dsh-taskbar-reminder` entry while keeping its configuration. Remove a separate `dsh-winreminder` install if present to prevent duplicate notifications.
|
|
49
54
|
|
|
50
|
-
|
|
55
|
+
To remove this plugin, run `dsh plugin --profile desktop remove @kesike/reminder-dsh` with your actual profile name, remove any manual `dsh-reminder` override, and restart DSH.
|
|
56
|
+
|
|
57
|
+
## Development
|
|
58
|
+
|
|
59
|
+
```powershell
|
|
51
60
|
npm ci --ignore-scripts
|
|
52
61
|
npm test
|
|
53
62
|
npm run test:settings-ui
|
|
54
63
|
npm pack --dry-run
|
|
55
64
|
```
|
|
56
65
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
Automated checks include Cordis Loader hot updates, independent module selection, queue scheduling, model-failure handling and subprocess control. Windows-only checks cover the native toast payload and Win32 helper. Notifications are delivered on the Windows backend machine.
|
|
60
|
-
|
|
61
|
-
## License
|
|
66
|
+
Use `npm run test:popup` for a sample Windows toast and `npm run test:taskbar -- "window title"` for a native badge check. Set `CHROMIUM_PATH` if the settings test needs a specific Chromium executable.
|
|
62
67
|
|
|
63
|
-
|
|
68
|
+
Source and icon licensing are available in the [repository](https://github.com/KeS1Ke/reminder-dsh). The plugin is distributed under [MIT](https://github.com/KeS1Ke/reminder-dsh/blob/main/LICENSE).
|
package/README.zh-CN.md
CHANGED
|
@@ -1,138 +1,64 @@
|
|
|
1
1
|
# reminder-dsh
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
在 Windows 上接收 DeepSeek Harness(DSH)的完成提醒和审批请求。通知带有会话标题,可以直接回复原会话或处理当前审批。需要安静一点时,可以单独使用任务栏角标,查看还有多少完成提醒未读。
|
|
4
4
|
|
|
5
|
-
[English](README.md)
|
|
5
|
+
[English](https://github.com/KeS1Ke/reminder-dsh/blob/main/README.md) · [npm](https://www.npmjs.com/package/@kesike/reminder-dsh)
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## 安装或更新
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
- 系统通知使用 Windows 自带的 `powershell.exe`。任务栏闪烁需要 PowerShell 7,`pwsh.exe` 须在 PATH 中。
|
|
11
|
-
- Node.js **24.9.0+**,DSH **`>=0.2.2-rc.1 <0.3.0`**。
|
|
12
|
-
|
|
13
|
-
原仓库骨架提到的 DSH `0.1.1-rc.2` 不支持本版采用的动态配置接口,需要升级。实现依据官方源码 [`fcc0ad4`](https://github.com/deepseek-ai/deepseek-harness/tree/fcc0ad462172522aaf5c93a7b2e01f6b9f0b0159)。
|
|
9
|
+
需要 Windows 10/11 本机交互式桌面会话、Node.js 24.9.0+ 和 DSH `>=0.2.2-rc.1 <0.3.0`。系统通知使用 Windows 自带的 `powershell.exe`,任务栏角标需要 PATH 中有 PowerShell 7(`pwsh.exe`)。提醒在 DSH 后端所在的 Windows 机器上出现。
|
|
14
10
|
|
|
15
11
|
```powershell
|
|
16
|
-
|
|
17
|
-
pwsh --version
|
|
18
|
-
|
|
19
|
-
dsh plugin --profile web add github:KeS1Ke/reminder-dsh
|
|
20
|
-
dsh --profile web
|
|
12
|
+
dsh plugin --profile desktop add @kesike/reminder-dsh
|
|
21
13
|
```
|
|
22
14
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
包名 `@kesike/reminder-dsh`,配置条目 ID `dsh-reminder`,与 GitHub 仓库名一致。在当前 profile 安装一次即可,不要在每个 agent 里重复安装。
|
|
26
|
-
|
|
27
|
-
### 从旧版本升级
|
|
15
|
+
安装或更新后完全退出并重新打开 DSH Desktop。Web 版请使用实际运行 Windows 后端的 profile,例如 `dsh plugin --profile web add @kesike/reminder-dsh`,然后重启该后端。也可把命令里的包名换成 `github:KeS1Ke/reminder-dsh` 从 GitHub 安装。
|
|
28
16
|
|
|
29
|
-
|
|
17
|
+
## 设置提醒方式
|
|
30
18
|
|
|
31
|
-
|
|
19
|
+
打开 DSH 的 **设置 → 通知**。
|
|
32
20
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
| 模块 / 字段 | 默认值 | 行为 |
|
|
21
|
+
| 设置 | 默认值 | 用途 |
|
|
36
22
|
| --- | --- | --- |
|
|
37
|
-
| Windows
|
|
38
|
-
|
|
|
39
|
-
|
|
|
40
|
-
|
|
|
41
|
-
|
|
42
|
-
设置使用 DSH 原生 ConfigForm 和 `.volatile()` 动态引用,保存到当前 profile 的配置覆盖层,不重新挂载插件。只读配置、上层覆盖或网络错误导致保存被拒绝时,会显示失败反馈。
|
|
43
|
-
|
|
44
|
-
两个模块独立运行。关闭任务栏闪烁会停止闪烁,关闭系统通知会取消尚未发送的提醒。重新开启后仅处理新的任务完成事件。
|
|
45
|
-
|
|
46
|
-
## 模块一:任务栏闪烁
|
|
23
|
+
| Windows 系统通知 | 开启 | 接收主会话完成和人工审批的原生通知 |
|
|
24
|
+
| 在通知中显示任务总结 | 开启 | 在完成通知正文中加入简短 AI 总结 |
|
|
25
|
+
| 通知发送间隔 | 10 秒 | 两条完成通知提交给 Windows 的最短间隔,可设 1–300 秒 |
|
|
26
|
+
| Windows 任务栏完成角标 | 关闭 | 在目标窗口的任务栏图标上显示未查看完成数 |
|
|
27
|
+
| 目标窗口标题包含 | 空 | 自动识别 DSH 窗口,或填写能唯一匹配的标题文字 |
|
|
47
28
|
|
|
48
|
-
|
|
29
|
+
修改开关后,点击页底的 **保存** 一次应用,点击 **取消** 丢弃未保存修改。窗口标题和发送间隔各有单独的保存按钮。设置保存到当前 profile 后立即生效。
|
|
49
30
|
|
|
50
|
-
|
|
31
|
+
## 完成、回复和审批
|
|
51
32
|
|
|
52
|
-
|
|
33
|
+
- **完成通知**使用会话标题,取不到标题时用配置的备用标题。主会话正常结束并进入空闲才触发。子会话、失败或取消的轮次以及恢复历史会话均不触发完成提醒。多个完成事件按顺序提交。
|
|
34
|
+
- **AI 任务总结**使用会话当前模型,对最后一轮的最终文字回复生成约 3 行的中文总结,可能产生额外模型用量。总结失败时显示带提示的摘录。关闭总结后跳过这次模型调用。
|
|
35
|
+
- **快速回复**把消息送回发出该通知的会话。每条完成通知可回复一次,入口保留 24 小时,要求原会话及后端仍可用。
|
|
36
|
+
- **人工审批**在通知正文中显示申请工具和原因。「允许」仅授予本次请求一次,「拒绝」驳回本次请求。通知按钮与会话内审批卡片处理的是同一个待决请求。
|
|
37
|
+
- **任务栏角标**在目标窗口位于后台时累加未查看完成数。切回目标窗口后清零,超过 99 显示 `99+`。使用 Web 版时,请让 DSH 标签页保持为该浏览器窗口的当前标签页。
|
|
53
38
|
|
|
54
|
-
|
|
39
|
+
## 点击通知返回会话
|
|
55
40
|
|
|
56
|
-
|
|
41
|
+
点击完成或审批通知正文会请求打开原会话。后端保留最近一次点击,等前台 DSH 界面确认接收。0.2.2 修复了通知激活器继承 Electron Node 模式变量、导致 `dsh://open` 无法正常唤起桌面版的问题。
|
|
57
42
|
|
|
58
|
-
|
|
43
|
+
实际点击仍可能遇到 Windows 回调未送达或窗口未置前。如果没有跳转,请打开 DSH,按通知显示的会话标题找到原会话。更新或重启后请使用新生成的通知,旧通知的跳转和交互入口可能随原后端退出而失效。
|
|
59
44
|
|
|
60
|
-
|
|
45
|
+
横幅和通知中心由 Windows 管理。插件使用当前用户的通知身份,检测到 DSH Desktop 时会创建当前用户的开始菜单快捷方式。通知交互通过本机激活器和命名管道交给插件后端。
|
|
61
46
|
|
|
62
|
-
|
|
47
|
+
## 配置文件与卸载
|
|
63
48
|
|
|
64
|
-
|
|
49
|
+
在当前 profile 的 `cordis.patch.yml` 中使用 `dsh-reminder` 条目,分开配置 `popup` 和 `taskbar`。参见 [配置示例](https://github.com/KeS1Ke/reminder-dsh/blob/main/example.patch.yml)。从 `@kesike/dsh-taskbar-reminder` 迁移时,先卸载旧包,将 `dsh-taskbar-reminder` 条目改为 `dsh-reminder` 并保留原配置。如还安装了独立的 `dsh-winreminder`,请移除旧插件以避免重复提醒。
|
|
65
50
|
|
|
66
|
-
|
|
51
|
+
卸载时运行 `dsh plugin --profile desktop remove @kesike/reminder-dsh`,将 `desktop` 替换为实际 profile,移除手工添加的 `dsh-reminder` 配置覆盖,然后重启 DSH。
|
|
67
52
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
默认等待最多 15 秒、总结最多 180 字符;超长回复只取首尾合计 12000 字符。超时或失败时明确显示“AI 总结暂不可用”并附最终回复摘录;没有最终文字回复时说明原因并使用备用文案。模型摘要不能替代对实际完成情况的检查。
|
|
71
|
-
|
|
72
|
-
## 提醒触发条件
|
|
73
|
-
|
|
74
|
-
两个模块共用主会话完成检测:观察到 `running`,最后一个 `turn/end` 为 `completed`,再进入 `idle`。子代理(`origin: subagent` 或 `delegationDepth > 0`)、取消、报错、阻止执行、输出长度达到上限、历史恢复和重复空闲事件不提醒。普通会话分叉仍算主会话。
|
|
75
|
-
|
|
76
|
-
这里只表示本轮正常结束并空闲,不保证任务业务目标全部完成。后台工作随后唤醒主会话并再次正常完成时,会产生新的完成事件。
|
|
77
|
-
|
|
78
|
-
## 文件配置
|
|
79
|
-
|
|
80
|
-
在当前 profile 的 `cordis.patch.yml` 添加覆盖项:
|
|
81
|
-
|
|
82
|
-
```yaml
|
|
83
|
-
- id: dsh-reminder
|
|
84
|
-
config:
|
|
85
|
-
taskbar:
|
|
86
|
-
enabled: false
|
|
87
|
-
windowTitle: ''
|
|
88
|
-
popup:
|
|
89
|
-
enabled: true
|
|
90
|
-
durationSeconds: 10
|
|
91
|
-
title: DSH 任务完成
|
|
92
|
-
message: 主会话已完成本轮任务。
|
|
93
|
-
summaryMaxChars: 180
|
|
94
|
-
summaryTimeoutSeconds: 15
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
`taskbar.enabled`、`taskbar.windowTitle`、`popup.enabled`、`popup.durationSeconds` 支持热更新。`popup.durationSeconds` 现在表示连续通知的发送间隔。其余为普通配置,修改时遵循插件重载。`summaryMaxChars` 为 60–1000,`summaryTimeoutSeconds` 为 1–60。
|
|
98
|
-
|
|
99
|
-
## 模块目录
|
|
100
|
-
|
|
101
|
-
```text
|
|
102
|
-
src/index.js 组合两个模块、注册热更新
|
|
103
|
-
src/reminder.js 共用主会话完成检测
|
|
104
|
-
src/modules/taskbar/ 原生任务栏闪烁、窗口定位、子进程管理
|
|
105
|
-
src/modules/popup/ AI 总结、发送队列、Windows 原生通知
|
|
106
|
-
lib/index.js 兼容原包入口
|
|
107
|
-
lib/client.js DSH 设置界面的两个模块配置
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
## 验证与开发
|
|
53
|
+
## 开发验证
|
|
111
54
|
|
|
112
55
|
```powershell
|
|
113
|
-
git clone https://github.com/KeS1Ke/reminder-dsh.git
|
|
114
|
-
cd reminder-dsh
|
|
115
56
|
npm ci --ignore-scripts
|
|
116
57
|
npm test
|
|
117
58
|
npm run test:settings-ui
|
|
118
|
-
npm run
|
|
119
|
-
npm run test:taskbar
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
`test:popup` 发送一条示例系统通知,并将测试回复打印到终端。`test:taskbar` 需要有可匹配的 DSH 窗口,可通过 `npm run test:taskbar -- "具体窗口标题"` 指定匹配文字。
|
|
123
|
-
|
|
124
|
-
自动测试覆盖真实 Cordis Loader 动态更新、模块独立开关、排队和间隔重算、摘要回退及子进程行为。Chromium 测试通过测试夹具验证「通知」注册和设置保存,可用 `CHROMIUM_PATH` 指定浏览器。Windows 专用测试验证原生通知载荷和 Win32 辅助代码。
|
|
125
|
-
|
|
126
|
-
提醒发生在 **DSH 后端所在 Windows 机器**。WSL/Linux 后端、远程 Windows 浏览器连接到别处后端、Windows 服务或无人登录会话不保证有可见提醒。
|
|
127
|
-
|
|
128
|
-
## 卸载
|
|
129
|
-
|
|
130
|
-
```powershell
|
|
131
|
-
dsh plugin --profile web remove @kesike/reminder-dsh
|
|
59
|
+
npm pack --dry-run
|
|
132
60
|
```
|
|
133
61
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
## 许可证
|
|
62
|
+
在 Windows 上用 `npm run test:popup` 发送测试通知,或用 `npm run test:taskbar -- "窗口标题"` 检查角标。设置页浏览器测试可通过 `CHROMIUM_PATH` 指定 Chromium。
|
|
137
63
|
|
|
138
|
-
插件代码采用 [MIT](LICENSE)
|
|
64
|
+
插件代码采用 [MIT](https://github.com/KeS1Ke/reminder-dsh/blob/main/LICENSE) 许可证,图标来源及许可见 [资源说明](https://github.com/KeS1Ke/reminder-dsh/blob/main/assets/README.md)。
|
package/example.patch.yml
CHANGED
package/lib/client.js
CHANGED
|
@@ -9,13 +9,17 @@ window.__ModuleLoader__.load({
|
|
|
9
9
|
zh: {
|
|
10
10
|
nav: '通知',
|
|
11
11
|
title: 'Windows 系统通知',
|
|
12
|
-
taskbar: 'Windows
|
|
13
|
-
taskbarDescription: '
|
|
12
|
+
taskbar: 'Windows 任务栏完成角标',
|
|
13
|
+
taskbarDescription: '主会话完成时在 DSH 任务栏图标的角上显示未查看的完成数量(深色圆底白色数字,同 ChatGPT 角标)。切回目标窗口后角标清除。此开关独立于 Windows 系统通知。',
|
|
14
|
+
summary: '在通知中显示任务总结',
|
|
15
|
+
summaryDescription: '开启后通知正文最多显示 3 行任务总结;关闭后通知只显示会话标题,并省去一次 AI 总结调用。',
|
|
14
16
|
windowTitle: '目标窗口标题包含(可选)', saveTitle: '保存目标窗口',
|
|
15
17
|
windowHelp: '留空自动识别 DeepSeek Harness / DSH 窗口。浏览器需显示 DSH 标签页;匹配多个窗口时,请填写更具体的标题文本。',
|
|
16
|
-
description: '
|
|
18
|
+
description: '通过 Windows 通知显示任务总结和待处理审批。可直接回复原会话或处理本次审批。',
|
|
17
19
|
interval: '通知发送间隔(秒)', intervalHelp: '默认 10 秒,可设为 1–300 秒。横幅显示时间由 Windows 管理。', save: '保存间隔',
|
|
18
|
-
hot: '
|
|
20
|
+
hot: '开关更改需点击本页底部的保存才生效;关闭 Windows 系统通知会取消尚未发送的提醒。',
|
|
21
|
+
saveChanges: '保存', cancelChanges: '取消',
|
|
22
|
+
switchUnsaved: '开关更改尚未保存,保存后立即生效。',
|
|
19
23
|
saved: '已保存,立即生效。',
|
|
20
24
|
error: '设置未保存,请检查连接或配置覆盖后重试。',
|
|
21
25
|
unavailable: '此配置暂不可编辑,请确认插件已加载且当前配置可写。',
|
|
@@ -23,24 +27,110 @@ window.__ModuleLoader__.load({
|
|
|
23
27
|
en: {
|
|
24
28
|
nav: 'Notifications',
|
|
25
29
|
title: 'Windows system notifications',
|
|
26
|
-
taskbar: 'Windows taskbar
|
|
27
|
-
taskbarDescription: '
|
|
30
|
+
taskbar: 'Windows taskbar completion badge',
|
|
31
|
+
taskbarDescription: 'Show the number of unread completed sessions on the DSH taskbar icon (dark chip with white digits, like the ChatGPT badge) when a main session finishes. Focusing the target window clears the badge. This switch is independent of Windows notifications.',
|
|
32
|
+
summary: 'Show task summary in notifications',
|
|
33
|
+
summaryDescription: 'When on, the notification body shows the task summary in up to three lines. When off, only the session title is shown and no summary model call is made.',
|
|
28
34
|
windowTitle: 'Target window title contains (optional)', saveTitle: 'Save target window',
|
|
29
35
|
windowHelp: 'Leave blank to detect DeepSeek Harness / DSH windows. A browser must show its DSH tab. If multiple windows match, enter a more specific title.',
|
|
30
|
-
description: 'Show
|
|
36
|
+
description: 'Show task summaries and pending approvals in Windows notifications. Reply to the original session or decide the current approval from the notification.',
|
|
31
37
|
interval: 'Notification interval (seconds)', intervalHelp: 'Default: 10 seconds. Range: 1–300. Windows controls how long banners stay visible.', save: 'Save interval',
|
|
32
|
-
hot: '
|
|
38
|
+
hot: 'Switch changes apply only after you save them at the bottom of this page. Turning notifications off cancels pending sends.',
|
|
39
|
+
saveChanges: 'Save', cancelChanges: 'Cancel',
|
|
40
|
+
switchUnsaved: 'Switch changes are not saved yet. Saving applies them immediately.',
|
|
33
41
|
saved: 'Saved and applied immediately.',
|
|
34
42
|
error: 'Could not save. Check the connection or configuration overrides and retry.',
|
|
35
43
|
unavailable: 'Settings are unavailable. Check that the plugin is loaded and its configuration is writable.',
|
|
36
44
|
},
|
|
37
45
|
}
|
|
38
46
|
|
|
39
|
-
|
|
47
|
+
/** 16 px outline bell matching the DSH medium icon weight (1.3 px stroke, currentColor). */
|
|
48
|
+
function BellIcon({ size = 16 }) {
|
|
49
|
+
return h('svg', {
|
|
50
|
+
width: size, height: size, viewBox: '0 0 16 16', fill: 'none', stroke: 'currentColor',
|
|
51
|
+
strokeWidth: 1.3, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': 'true', focusable: 'false',
|
|
52
|
+
},
|
|
53
|
+
h('path', { d: 'M12 5.33A4 4 0 0 0 4 5.33c0 4.67-2 6-2 6h12s-2-1.33-2-6' }),
|
|
54
|
+
h('path', { d: 'M9.15 14a1.33 1.33 0 0 1-2.3 0' }))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Resolve one of the plugin's host routes under the page base (prefix-safe for the desktop shell). */
|
|
58
|
+
function hostRoute(path) {
|
|
59
|
+
try {
|
|
60
|
+
const base = typeof document !== 'undefined' && typeof document.baseURI === 'string' ? document.baseURI : ''
|
|
61
|
+
if (!base) return undefined
|
|
62
|
+
const url = new URL(base)
|
|
63
|
+
url.search = ''
|
|
64
|
+
url.hash = ''
|
|
65
|
+
if (!url.pathname.endsWith('/')) url.pathname += '/'
|
|
66
|
+
return new URL(path.replace(/^\/+/, ''), url.href).href
|
|
67
|
+
} catch { return undefined }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** A background page cannot consume a click intended for the DSH window brought to the front. */
|
|
71
|
+
function followSessionOpens(workspace, endpoint) {
|
|
72
|
+
const lifetime = new AbortController()
|
|
73
|
+
const foreground = () => document.visibilityState === 'visible' && document.hasFocus()
|
|
74
|
+
let poll, acknowledgedId
|
|
75
|
+
|
|
76
|
+
async function watch(signal) {
|
|
77
|
+
const url = new URL(endpoint)
|
|
78
|
+
let failures = 0
|
|
79
|
+
while (!signal.aborted) {
|
|
80
|
+
try {
|
|
81
|
+
if (acknowledgedId) url.searchParams.set('ack', acknowledgedId)
|
|
82
|
+
const response = await fetch(url.href, { signal, credentials: 'same-origin', cache: 'no-store' })
|
|
83
|
+
if (!response.ok) throw new Error(`session-open route ${response.status}`)
|
|
84
|
+
if (response.status === 200) {
|
|
85
|
+
const value = await response.json()
|
|
86
|
+
if (signal.aborted || !foreground()) return
|
|
87
|
+
if (typeof value?.id !== 'string' || typeof value.sessionId !== 'string' || !value.sessionId) {
|
|
88
|
+
throw new Error('Invalid session-open response')
|
|
89
|
+
}
|
|
90
|
+
// A notification outlives its session: opening an id DSH no longer knows must not stop the poll.
|
|
91
|
+
try { workspace.openSession(value.sessionId) } catch {}
|
|
92
|
+
acknowledgedId = value.id
|
|
93
|
+
}
|
|
94
|
+
failures = 0
|
|
95
|
+
} catch {
|
|
96
|
+
if (signal.aborted) return
|
|
97
|
+
const delay = Math.min(10_000, 500 * 2 ** Math.min(++failures, 5))
|
|
98
|
+
await new Promise(resolve => {
|
|
99
|
+
const done = () => { clearTimeout(timer); signal.removeEventListener('abort', done); resolve() }
|
|
100
|
+
const timer = setTimeout(done, delay)
|
|
101
|
+
signal.addEventListener('abort', done, { once: true })
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function update() {
|
|
108
|
+
if (!foreground()) {
|
|
109
|
+
poll?.abort()
|
|
110
|
+
poll = undefined
|
|
111
|
+
} else if (!poll) {
|
|
112
|
+
const current = new AbortController()
|
|
113
|
+
poll = current
|
|
114
|
+
void watch(current.signal).finally(() => { if (poll === current) poll = undefined })
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
window.addEventListener('focus', update, { signal: lifetime.signal })
|
|
118
|
+
window.addEventListener('blur', update, { signal: lifetime.signal })
|
|
119
|
+
document.addEventListener('visibilitychange', update, { signal: lifetime.signal })
|
|
120
|
+
update()
|
|
121
|
+
return () => { lifetime.abort(); poll?.abort() }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const buttonStyle = { padding: '6px 12px', borderRadius: 6, border: '1px solid currentColor', background: 'transparent', color: 'inherit', cursor: 'pointer' }
|
|
125
|
+
|
|
126
|
+
function NotificationSettings({ usePreferences, saveOptions, setDurationSeconds, setWindowTitle, t }) {
|
|
40
127
|
const snapshot = usePreferences(value => value)
|
|
41
128
|
const [busy, setBusy] = React.useState(false)
|
|
42
|
-
const [
|
|
129
|
+
const [switchFeedback, setSwitchFeedback] = React.useState('')
|
|
130
|
+
const [windowFeedback, setWindowFeedback] = React.useState('')
|
|
131
|
+
const [intervalFeedback, setIntervalFeedback] = React.useState('')
|
|
43
132
|
const [interval, setInterval] = React.useState(String(snapshot.value?.popup?.durationSeconds ?? 10))
|
|
133
|
+
const [drafts, setDrafts] = React.useState({})
|
|
44
134
|
const intervalId = React.useId()
|
|
45
135
|
const titleId = React.useId()
|
|
46
136
|
const [windowTitle, changeWindowTitle] = React.useState(snapshot.value?.taskbar?.windowTitle ?? '')
|
|
@@ -49,67 +139,99 @@ window.__ModuleLoader__.load({
|
|
|
49
139
|
const seconds = Number(interval)
|
|
50
140
|
const validInterval = interval.trim() !== '' && Number.isInteger(seconds) && seconds >= 1 && seconds <= 300
|
|
51
141
|
const available = snapshot.status === 'ready' && snapshot.writable
|
|
52
|
-
|
|
142
|
+
// Switches only edit drafts; the bar at the bottom writes every pending switch as one mutation.
|
|
143
|
+
const stored = {
|
|
144
|
+
taskbarEnabled: snapshot.value?.taskbar?.enabled === true,
|
|
145
|
+
popupEnabled: snapshot.value?.popup?.enabled === true,
|
|
146
|
+
showSummary: snapshot.value?.popup?.showSummary !== false,
|
|
147
|
+
}
|
|
148
|
+
const shown = name => drafts[name] ?? stored[name]
|
|
149
|
+
const changes = {}
|
|
150
|
+
for (const name of Object.keys(stored)) if (shown(name) !== stored[name]) changes[name] = shown(name)
|
|
151
|
+
const dirty = Object.keys(changes).length > 0
|
|
152
|
+
const stage = (name, value) => {
|
|
153
|
+
if (busy || !available) return
|
|
154
|
+
setSwitchFeedback('')
|
|
155
|
+
setDrafts(current => ({ ...current, [name]: value }))
|
|
156
|
+
}
|
|
157
|
+
const saveSwitches = async () => {
|
|
158
|
+
if (busy || !available || !dirty) return
|
|
159
|
+
setBusy(true)
|
|
160
|
+
setSwitchFeedback('')
|
|
161
|
+
try {
|
|
162
|
+
if (await saveOptions(changes)) { setDrafts({}); setSwitchFeedback('saved') }
|
|
163
|
+
else setSwitchFeedback('error')
|
|
164
|
+
} catch { setSwitchFeedback('error') } finally { setBusy(false) }
|
|
165
|
+
}
|
|
166
|
+
const cancelSwitches = () => {
|
|
167
|
+
if (busy) return
|
|
168
|
+
setDrafts({})
|
|
169
|
+
setSwitchFeedback('')
|
|
170
|
+
}
|
|
171
|
+
// A successful switch save is a transient confirmation; the bar itself disappears with the changes.
|
|
172
|
+
React.useEffect(() => {
|
|
173
|
+
if (switchFeedback !== 'saved') return undefined
|
|
174
|
+
const timer = setTimeout(() => setSwitchFeedback(''), 5000)
|
|
175
|
+
return () => clearTimeout(timer)
|
|
176
|
+
}, [switchFeedback])
|
|
53
177
|
return h('div', { style: { padding: '12px 0' }, 'data-reminder-dsh-settings': '' },
|
|
54
178
|
h('section', { 'data-reminder-module': 'taskbar', style: { marginBottom: 24, borderBottom: '1px solid #8885', paddingBottom: 20 } },
|
|
55
179
|
h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 24 } },
|
|
56
180
|
h('h3', { style: { fontSize: 14, margin: 0 } }, t('taskbar')),
|
|
57
|
-
h(Switch, { checked:
|
|
58
|
-
onChange:
|
|
59
|
-
setBusy(true); setFeedback('')
|
|
60
|
-
try { setFeedback(await setTaskbarEnabled(value) ? 'saved' : 'error') }
|
|
61
|
-
catch { setFeedback('error') }
|
|
62
|
-
finally { setBusy(false) }
|
|
63
|
-
},
|
|
64
|
-
})),
|
|
181
|
+
h(Switch, { checked: shown('taskbarEnabled'), disabled: busy || !available, label: t('taskbar'),
|
|
182
|
+
onChange: value => stage('taskbarEnabled', value) })),
|
|
65
183
|
h('p', { style: { opacity: 0.75, fontSize: 13 } }, t('taskbarDescription')),
|
|
66
184
|
h('form', { onSubmit: async event => {
|
|
67
185
|
event.preventDefault()
|
|
68
186
|
if (busy || !available || windowTitle.length > 200) return
|
|
69
|
-
setBusy(true);
|
|
70
|
-
try {
|
|
71
|
-
catch {
|
|
187
|
+
setBusy(true); setWindowFeedback('')
|
|
188
|
+
try { setWindowFeedback(await setWindowTitle(windowTitle.trim()) ? 'saved' : 'error') }
|
|
189
|
+
catch { setWindowFeedback('error') }
|
|
72
190
|
finally { setBusy(false) }
|
|
73
191
|
} },
|
|
74
192
|
h('label', { htmlFor: titleId }, t('windowTitle')),
|
|
75
193
|
h('div', { style: { display: 'flex', gap: 12, margin: '8px 0' } },
|
|
76
194
|
h('input', { id: titleId, type: 'text', maxLength: 200, value: windowTitle, disabled: busy || !available,
|
|
77
|
-
onChange: event => changeWindowTitle(event.target.value),
|
|
195
|
+
onChange: event => { changeWindowTitle(event.target.value); setWindowFeedback('') },
|
|
78
196
|
style: { flex: 1, minWidth: 100, padding: '6px 8px', border: '1px solid currentColor', borderRadius: 6, background: 'transparent', color: 'inherit' } }),
|
|
79
197
|
h('button', { type: 'submit', disabled: busy || !available, style: { padding: '6px 12px' } }, t('saveTitle'))),
|
|
80
|
-
h('p', { style: { opacity: 0.75, fontSize: 12 } }, t('windowHelp'))
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
h(
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
},
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
198
|
+
h('p', { style: { opacity: 0.75, fontSize: 12 } }, t('windowHelp')),
|
|
199
|
+
windowFeedback && h('p', { role: windowFeedback === 'error' ? 'alert' : 'status' }, t(windowFeedback)))),
|
|
200
|
+
h('section', { 'data-reminder-module': 'popup' },
|
|
201
|
+
h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 24 } },
|
|
202
|
+
h('div', { style: { fontWeight: 600 } }, t('title')),
|
|
203
|
+
h(Switch, { checked: shown('popupEnabled'), disabled: busy || !available, label: t('title'),
|
|
204
|
+
onChange: value => stage('popupEnabled', value) })),
|
|
205
|
+
h('p', { style: { opacity: 0.75, fontSize: 13, margin: '8px 0' } }, t('description')),
|
|
206
|
+
h('p', { style: { opacity: 0.75, fontSize: 12, margin: '8px 0' } }, t('hot')),
|
|
207
|
+
h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 24, marginTop: 12 } },
|
|
208
|
+
h('div', { style: { fontWeight: 600 } }, t('summary')),
|
|
209
|
+
h(Switch, { checked: shown('showSummary'), disabled: busy || !available, label: t('summary'),
|
|
210
|
+
onChange: value => stage('showSummary', value) })),
|
|
211
|
+
h('p', { style: { opacity: 0.75, fontSize: 13, margin: '8px 0' } }, t('summaryDescription')),
|
|
212
|
+
h('form', { onSubmit: async event => {
|
|
213
|
+
event.preventDefault()
|
|
214
|
+
if (!validInterval || busy || !available) return
|
|
215
|
+
setBusy(true)
|
|
216
|
+
setIntervalFeedback('')
|
|
217
|
+
try { setIntervalFeedback(await setDurationSeconds(seconds) ? 'saved' : 'error') }
|
|
218
|
+
catch { setIntervalFeedback('error') }
|
|
219
|
+
finally { setBusy(false) }
|
|
220
|
+
} },
|
|
221
|
+
h('label', { htmlFor: intervalId }, t('interval')),
|
|
222
|
+
h('div', { style: { display: 'flex', gap: 12, alignItems: 'center', margin: '8px 0' } },
|
|
223
|
+
h('input', { id: intervalId, type: 'number', min: 1, max: 300, step: 1, value: interval,
|
|
224
|
+
disabled: busy || !available, onChange: event => { setInterval(event.target.value); setIntervalFeedback('') },
|
|
225
|
+
style: { width: 90, padding: '6px 8px', border: '1px solid currentColor', borderRadius: 6, background: 'transparent', color: 'inherit' } }),
|
|
226
|
+
h('button', { type: 'submit', disabled: busy || !available || !validInterval, style: buttonStyle }, t('save'))),
|
|
227
|
+
h('p', { style: { opacity: 0.75, fontSize: 12 } }, t('intervalHelp')),
|
|
228
|
+
intervalFeedback && h('p', { role: intervalFeedback === 'error' ? 'alert' : 'status' }, t(intervalFeedback)))),
|
|
111
229
|
!available && h('p', { role: 'status' }, t('unavailable')),
|
|
112
|
-
|
|
230
|
+
(dirty || switchFeedback) && h('section', { 'data-reminder-switch-changes': '', style: { marginTop: 24, paddingTop: 20, borderTop: '1px solid #8885', display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' } },
|
|
231
|
+
dirty && h('button', { type: 'button', disabled: busy || !available, onClick: saveSwitches, style: buttonStyle }, t('saveChanges')),
|
|
232
|
+
dirty && h('button', { type: 'button', disabled: busy || !available, onClick: cancelSwitches, style: buttonStyle }, t('cancelChanges')),
|
|
233
|
+
h('p', { role: switchFeedback === 'error' ? 'alert' : 'status', style: { margin: 0, opacity: 0.75, fontSize: 12 } },
|
|
234
|
+
switchFeedback === 'error' ? t('error') : dirty ? t('switchUnsaved') : t('saved'))))
|
|
113
235
|
}
|
|
114
236
|
|
|
115
237
|
return {
|
|
@@ -119,13 +241,27 @@ window.__ModuleLoader__.load({
|
|
|
119
241
|
ctx.effect(() => ctx.locale.register('reminder-dsh', dictionaries))
|
|
120
242
|
const t = ctx.locale.bind('reminder-dsh')
|
|
121
243
|
const form = ctx.configForms.get('dsh-reminder')
|
|
244
|
+
ctx.inject(['uiWorkspace'], scope => {
|
|
245
|
+
const workspace = scope.get('uiWorkspace')
|
|
246
|
+
if (!workspace || typeof workspace.openSession !== 'function') return
|
|
247
|
+
const endpoint = hostRoute('api/reminder-dsh/session-open')
|
|
248
|
+
if (!endpoint) return
|
|
249
|
+
return followSessionOpens(workspace, endpoint)
|
|
250
|
+
})
|
|
122
251
|
const inject = () => ({
|
|
123
252
|
hooks: { preferences: form },
|
|
124
|
-
|
|
253
|
+
// One atomic write for every pending switch so the bar cannot leave half-applied settings.
|
|
254
|
+
saveOptions: changes => form.mutate([
|
|
255
|
+
...changes.taskbarEnabled === undefined ? [] : [{ op: 'set', path: ['taskbar', 'enabled'], value: changes.taskbarEnabled }],
|
|
256
|
+
...changes.popupEnabled === undefined ? [] : [{ op: 'set', path: ['popup', 'enabled'], value: changes.popupEnabled }],
|
|
257
|
+
...changes.showSummary === undefined ? [] : [{ op: 'set', path: ['popup', 'showSummary'], value: changes.showSummary }],
|
|
258
|
+
]),
|
|
125
259
|
setDurationSeconds: value => form.mutate([{ op: 'set', path: ['popup', 'durationSeconds'], value }]),
|
|
126
|
-
setTaskbarEnabled: value => form.mutate([{ op: 'set', path: ['taskbar', 'enabled'], value }]),
|
|
127
260
|
setWindowTitle: value => form.mutate([{ op: 'set', path: ['taskbar', 'windowTitle'], value }]),
|
|
128
261
|
})
|
|
262
|
+
ctx.slots.inject('settings.section.icon', () => ctx.slots.register({
|
|
263
|
+
name: 'settings.section.icon', id: 'dsh-reminder', order: 50, locale: 'reminder-dsh',
|
|
264
|
+
}, BellIcon))
|
|
129
265
|
ctx.slots.inject('settings.section', () => ctx.slots.register({
|
|
130
266
|
name: 'settings.section', id: 'dsh-reminder', order: 50, label: () => t('nav'), locale: 'reminder-dsh', inject,
|
|
131
267
|
}, NotificationSettings))
|