dsh-plugin-session-notifier 0.1.1 → 0.1.3

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
@@ -5,7 +5,7 @@
5
5
  [![npm](https://img.shields.io/npm/v/dsh-plugin-session-notifier)](https://www.npmjs.com/package/dsh-plugin-session-notifier)
6
6
  [![license](https://img.shields.io/github/license/slvayf/dsh-plugin-session-notifier)](LICENSE)
7
7
 
8
- A DeepSeek Harness bundle. It raises a desktop notification and a short chime when a session finishes,
8
+ A DeepSeek Harness session notification bundle. It raises a desktop notification and a short chime when a session finishes,
9
9
  fails, or stops to wait for your answer (a question or a tool approval). Nothing is missed while you
10
10
  are in another window, watching a video, or playing a game.
11
11
 
@@ -21,7 +21,7 @@ The interface follows DSH's Chinese/English setting.
21
21
  | The session fails | A low two-note chime and an error notification |
22
22
  | The session you are looking at ends | Reminded after about 1 second, effectively immediate |
23
23
  | Any other session ends | Waits out the 10/20/30 second merge window to avoid a burst |
24
- | It continues within 10 seconds | Merged into one reminder |
24
+ | Any other session resumes inside the merge window | Merged into one reminder |
25
25
  | You do not come back | Repeats every 30 seconds, at most 3 times, and stops as soon as the page is focused |
26
26
  | Subagent sessions | Silent, so one task's internal fan-out never rings |
27
27
 
@@ -43,22 +43,33 @@ The panel says so as well.
43
43
 
44
44
  ## Install
45
45
 
46
- **1. By package name (recommended).** Ask the agent to call:
46
+ **1. From the DSH interface**
47
+
48
+ 1. Open the **Plugins** page in the sidebar
49
+ 2. Choose **+ Add plugin** at the top right
50
+ 3. Enter `dsh-plugin-session-notifier`, or name a version, for example `dsh-plugin-session-notifier@0.1.2`
51
+ 4. Pick the China mainland registry mirror if you are there, it is much faster
52
+ 5. Choose **Install**
53
+
54
+ **2. Ask the agent**
47
55
 
48
56
  ```
49
- plugin_manager({ action: "install_bundle", target: "dsh-plugin-session-notifier@0.1.0" })
57
+ plugin_manager({ action: "install_bundle", target: "dsh-plugin-session-notifier" })
50
58
  ```
51
59
 
52
- You can also type `dsh-plugin-session-notifier` into **Plugins → Add plugin** yourself. Pick the
53
- China mainland registry mirror if you are there, it is much faster.
60
+ Omitting the version installs the newest release that has also cleared pnpm's release-age window.
61
+
62
+ **3. Local development**
54
63
 
55
- **2. Local development:** `plugin_manager({ action: "install_bundle", target: "/absolute/path/dsh-plugin-session-notifier" })`.
64
+ ```
65
+ plugin_manager({ action: "install_bundle", target: "/absolute/path/dsh-plugin-session-notifier" })
66
+ ```
56
67
 
57
68
  After installation the left sidebar shows the bell entry, and the `main` panel key is `session-notifier`.
58
69
  Compatible DSH versions are declared by `engines.dsh` in `package.json`, currently `>=0.2.1-rc.3`.
59
70
 
60
- Upgrading means uninstalling the old version and installing the new one. The plugin manager does not
61
- upgrade in place.
71
+ Upgrading a plugin in DeepSeek Harness does not happen in place: uninstall the old version, then install the new one. If a new version is
72
+ reported as still inside pnpm's release-age window (24 hours by default), wait it out or name the version.
62
73
 
63
74
  ## Limits
64
75
 
@@ -93,6 +104,7 @@ upgrade in place.
93
104
  | `client.js` | Browser half: the reminder engine and the settings panel |
94
105
  | `cordis.patch.yml` | Inserts the plugin row |
95
106
  | `locale/*.json` | Title and description the plugin manager displays |
107
+ | `scripts/sync-version.mjs` | Copies the release version into the panel footer |
96
108
  | `test/*.mjs` | Engine scenarios plus static style and copy checks |
97
109
  | `.github/workflows/` | `ci` runs the checks, `publish` uploads to npm |
98
110
 
package/README.md CHANGED
@@ -1,12 +1,12 @@
1
- # 会话结束提醒 · Session Notifier
1
+ # 会话提醒 · Session Notifier
2
2
 
3
3
  [English](README.en.md) | 中文
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/dsh-plugin-session-notifier)](https://www.npmjs.com/package/dsh-plugin-session-notifier)
6
6
  [![license](https://img.shields.io/github/license/slvayf/dsh-plugin-session-notifier)](LICENSE)
7
7
 
8
- DeepSeek Harness 插件。会话跑完、出错,或者停下来等你回答(提问 / 工具授权)时,
9
- 用桌面通知加提示音提醒你,切到别的窗口、全屏看视频或打游戏时也不会错过。
8
+ DeepSeek Harness 会话通知插件。会话跑完、出错,或者停下来等你回答(提问 / 工具授权)时,
9
+ 用桌面通知和提示音提醒你。切到别的窗口、全屏看视频或打游戏时也不会错过。
10
10
 
11
11
  左侧栏有铃铛入口,点开是设置页,所有开关即时生效。界面跟随 DSH 的中英文设置切换。
12
12
 
@@ -19,7 +19,7 @@ DeepSeek Harness 插件。会话跑完、出错,或者停下来等你回答(
19
19
  | 会话出错 | 低沉的两声提示音 + 错误通知 |
20
20
  | 结束的是你正看着的会话 | 约 1 秒后提醒,几乎即时 |
21
21
  | 结束的是其他会话 | 等 10 / 20 / 30 秒的合并窗口再提醒,避免连环响 |
22
- | 10 秒内又继续跑 | 合并成一次提醒 |
22
+ | 结束的是其他会话,在合并窗口内又继续跑 | 合并成一次提醒 |
23
23
  | 你没回来看 | 每 30 秒再提醒一次,最多 3 次,页面获得焦点后立即停止 |
24
24
  | 子代理 | 不提醒,避免一个任务内部并行时连环响 |
25
25
 
@@ -40,21 +40,33 @@ DeepSeek Harness 插件。会话跑完、出错,或者停下来等你回答(
40
40
 
41
41
  ## 安装
42
42
 
43
- **一、按包名安装(推荐)**。让智能体调用:
43
+ **一、在 DSH 界面里安装**
44
+
45
+ 1. 侧边栏打开「插件」页
46
+ 2. 点右上角「+ 添加插件」
47
+ 3. 输入框填 `dsh-plugin-session-notifier`,想装指定版本就在包名后加版本,例如 `dsh-plugin-session-notifier@0.1.2`
48
+ 4. 安装源可选「中国大陆镜像源」,国内会快很多
49
+ 5. 点「安装」
50
+
51
+ **二、让智能体安装**
44
52
 
45
53
  ```
46
- plugin_manager({ action: "install_bundle", target: "dsh-plugin-session-notifier@0.1.0" })
54
+ plugin_manager({ action: "install_bundle", target: "dsh-plugin-session-notifier" })
47
55
  ```
48
56
 
49
- 也可以自己在「插件 → 添加插件」里输入 `dsh-plugin-session-notifier`。
50
- 安装源可选「中国大陆镜像源」,国内会快很多。
57
+ 不写版本号时,装的是最新的、并且已经过 pnpm 发布观察期的版本。
58
+
59
+ **三、本地开发**
51
60
 
52
- **二、本地开发**:`plugin_manager({ action: "install_bundle", target: "/绝对路径/dsh-plugin-session-notifier" })`。
61
+ ```
62
+ plugin_manager({ action: "install_bundle", target: "/绝对路径/dsh-plugin-session-notifier" })
63
+ ```
53
64
 
54
65
  安装后左侧栏出现铃铛入口,`main` 面板 key 为 `session-notifier`。
55
66
  兼容的 DSH 版本由 `package.json` 的 `engines.dsh` 声明,当前为 `>=0.2.1-rc.3`。
56
67
 
57
- 升级需要先卸载旧版本再安装新版本,插件管理器不支持原地升级。
68
+ 目前 DeepSeek Harness 的插件升级不能原地进行,需要先卸载旧版本,再安装新版本。如果提示某个新版本还处在 pnpm 的发布观察期内
69
+ (默认 24 小时),要么等它过去,要么直接指定版本号安装。
58
70
 
59
71
  ## 能力边界
60
72
 
@@ -84,6 +96,7 @@ plugin_manager({ action: "install_bundle", target: "dsh-plugin-session-notifier@
84
96
  | `client.js` | 浏览器半侧,提醒引擎与设置面板 |
85
97
  | `cordis.patch.yml` | 插入插件行 |
86
98
  | `locale/*.json` | 插件管理器展示用的标题与描述 |
99
+ | `scripts/sync-version.mjs` | 发布时把版本号同步到面板底部 |
87
100
  | `test/*.mjs` | 引擎场景、样式、文案的静态检查 |
88
101
  | `.github/workflows/` | `ci` 跑检查,`publish` 发 npm |
89
102
 
package/client.js CHANGED
@@ -28,13 +28,17 @@ window.__ModuleLoader__.load({
28
28
  /** Monotonic per-page counter: two reminders in one millisecond still need distinct tags. */
29
29
  let notificationSeq = 0;
30
30
  const RECENT_LIMIT = 12;
31
+ /** Shown at the foot of the panel; scripts/sync-version.mjs keeps it in step with package.json. */
32
+ const PLUGIN_VERSION = '0.1.3';
31
33
  /** Wait before reminding about the session the user is looking at. */
32
34
  const VIEWING_MERGE_MS = 1000;
35
+ /** Two reminders landing together share one tone instead of stacking two. */
36
+ const TONE_GAP_MS = 250;
33
37
 
34
38
  //#region copy
35
39
  const zh = {
36
40
  'panel.label': '会话提醒',
37
- 'panel.title': '会话结束提醒',
41
+ 'panel.title': '会话提醒',
38
42
  'panel.desc':
39
43
  '会话跑完或停下来等你回答时,用桌面通知和提示音提醒你。切到别的窗口、全屏看视频或打游戏时也不会错过。',
40
44
  'group.master': '总开关',
@@ -101,11 +105,12 @@ window.__ModuleLoader__.load({
101
105
  'notify.test.title': '这是一条测试提醒',
102
106
  'notify.test.body': '看到这条通知、听到提示音,就说明提醒已经生效。',
103
107
  'session.unknown': '新会话',
108
+ 'footer.version': '版本 {version}',
104
109
  };
105
110
 
106
111
  const en = {
107
- 'panel.label': 'Session alerts',
108
- 'panel.title': 'Session end notifier',
112
+ 'panel.label': 'Session Notifier',
113
+ 'panel.title': 'Session Notifier',
109
114
  'panel.desc':
110
115
  'A desktop notification and a short chime when a session finishes or stops to wait for you — so a task that ends while you are on another window is not missed.',
111
116
  'group.master': 'Master switch',
@@ -172,16 +177,17 @@ window.__ModuleLoader__.load({
172
177
  'notify.test.title': 'This is a test reminder',
173
178
  'notify.test.body': 'If you can see this notification and hear the chime, reminders work.',
174
179
  'session.unknown': 'New session',
180
+ 'footer.version': 'Version {version}',
175
181
  };
176
182
  //#endregion
177
183
 
178
184
  //#region settings
179
185
  /**
180
- * Stored-document version. Version 1 shipped with `whenFocused: false` and a 5 s
181
- * merge window; those two defaults changed, so a version-1 document is migrated
182
- * instead of silently keeping the older values.
186
+ * Stored-document version, written with every save. Nothing reads it yet: the key
187
+ * was introduced with the current defaults, so no older document can exist. It is
188
+ * here so a future default change can migrate instead of guessing.
183
189
  */
184
- const SETTINGS_VERSION = 2;
190
+ const SETTINGS_VERSION = 1;
185
191
 
186
192
  /** Merge-window choices, in milliseconds. */
187
193
  const COMBINE_CHOICES = [10000, 20000, 30000];
@@ -223,12 +229,6 @@ window.__ModuleLoader__.load({
223
229
  for (const key of Object.keys(DEFAULTS)) {
224
230
  if (typeof parsed[key] === typeof DEFAULTS[key]) next[key] = parsed[key];
225
231
  }
226
- const version = typeof parsed.v === 'number' ? parsed.v : 1;
227
- if (version < SETTINGS_VERSION) {
228
- // The two defaults that changed; other stored choices stay as the user left them.
229
- next.whenFocused = DEFAULTS.whenFocused;
230
- next.combineMs = DEFAULTS.combineMs;
231
- }
232
232
  } catch (_storageUnavailable) {
233
233
  // A blocked or corrupt store simply falls back to the defaults.
234
234
  }
@@ -287,8 +287,33 @@ window.__ModuleLoader__.load({
287
287
  /** Newest-first reminder log for the panel. */
288
288
  recent: [],
289
289
  recentListeners: new Set(),
290
+ /** Timers owned by one notification or tone, cleared with the plugin. */
291
+ timers: new Set(),
292
+ /** Wall-clock time of the last tone, so two reminders do not overlap. */
293
+ lastToneAt: 0,
290
294
  };
291
295
 
296
+ /**
297
+ * Run a callback later, keeping its timer id in the owned set.
298
+ * @param callback - work to run once.
299
+ * @param delay - delay in milliseconds.
300
+ * @returns the timer id.
301
+ */
302
+ function startTimer(callback, delay) {
303
+ const id = window.setTimeout(() => {
304
+ state.timers.delete(id);
305
+ callback();
306
+ }, delay);
307
+ state.timers.add(id);
308
+ return id;
309
+ }
310
+
311
+ /** Drop every timer this plugin started. */
312
+ function clearTimers() {
313
+ for (const id of state.timers) window.clearTimeout(id);
314
+ state.timers.clear();
315
+ }
316
+
292
317
  /** Faces assigned when they become available; every consumer tolerates absence. */
293
318
  const services = {
294
319
  sessions: null,
@@ -397,8 +422,13 @@ window.__ModuleLoader__.load({
397
422
  * Play the reminder tone for one kind of event.
398
423
  * @param kind - `done`, `awaiting`, `error`, or `test`.
399
424
  */
400
- function chime(kind) {
425
+ function chime(kind, force) {
401
426
  if (!settings.sound) return;
427
+ const at = Date.now();
428
+ if (force !== true) {
429
+ if (at - state.lastToneAt < TONE_GAP_MS) return;
430
+ state.lastToneAt = at;
431
+ }
402
432
  const context = audioContext();
403
433
  if (context === null) return;
404
434
  if (context.state === 'suspended' && typeof context.resume === 'function') {
@@ -439,7 +469,7 @@ window.__ModuleLoader__.load({
439
469
  oscillator.start(start + offset);
440
470
  oscillator.stop(start + offset + duration + 0.03);
441
471
  }
442
- window.setTimeout(() => {
472
+ startTimer(() => {
443
473
  try {
444
474
  master.disconnect();
445
475
  } catch (_disconnected) {
@@ -525,8 +555,8 @@ window.__ModuleLoader__.load({
525
555
  // Already gone.
526
556
  }
527
557
  };
528
- window.setTimeout(() => settle('unconfirmed'), 2500);
529
- window.setTimeout(() => {
558
+ startTimer(() => settle('unconfirmed'), 2500);
559
+ startTimer(() => {
530
560
  try {
531
561
  notification.close();
532
562
  } catch (_closed) {
@@ -657,7 +687,7 @@ window.__ModuleLoader__.load({
657
687
  */
658
688
  function runTestReminder() {
659
689
  primeAudio();
660
- chime('test');
690
+ chime('test', true);
661
691
  publishProbe({ status: 'pending' });
662
692
  const delivery = notifyDesktop(
663
693
  'test',
@@ -688,6 +718,9 @@ window.__ModuleLoader__.load({
688
718
  });
689
719
  if (!settings.repeat || attempt >= settings.repeatMax) return;
690
720
  if (!pageIsAway()) return;
721
+ // A chain already armed for this session would keep its own timer, and its
722
+ // callback would delete this newer record.
723
+ cancelRepeat(sessionId);
691
724
  const timer = window.setTimeout(() => {
692
725
  state.repeats.delete(sessionId);
693
726
  if (!pageIsAway()) return;
@@ -719,13 +752,17 @@ window.__ModuleLoader__.load({
719
752
  */
720
753
  function scheduleEnd(sessionId, kind) {
721
754
  const previous = state.pendingEnd.get(sessionId);
755
+ // A failed turn reports its error and then its idle edge, so an armed error
756
+ // outranks the finish that follows it. Otherwise every failure would be
757
+ // announced as an ordinary completion.
758
+ const effective = previous !== undefined && previous.kind === 'error' ? 'error' : kind;
722
759
  if (previous !== undefined) window.clearTimeout(previous.timer);
723
760
  const timer = window.setTimeout(() => {
724
761
  state.pendingEnd.delete(sessionId);
725
762
  if (!eligible(sessionId)) return;
726
- deliver(kind, sessionId, 1);
763
+ deliver(effective, sessionId, 1);
727
764
  }, mergeWindowFor(sessionId));
728
- state.pendingEnd.set(sessionId, { timer, kind });
765
+ state.pendingEnd.set(sessionId, { timer, kind: effective });
729
766
  }
730
767
 
731
768
  /**
@@ -1198,6 +1235,8 @@ window.__ModuleLoader__.load({
1198
1235
  ),
1199
1236
  ),
1200
1237
  ),
1238
+
1239
+ h('p', { className: 'sen-version' }, t('footer.version', { version: PLUGIN_VERSION })),
1201
1240
  ),
1202
1241
  ),
1203
1242
  );
@@ -1288,6 +1327,7 @@ window.__ModuleLoader__.load({
1288
1327
  '.sen-btn-primary:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover)}',
1289
1328
  '.sen-actions{display:flex;align-items:center;gap:8px;padding:16px 0;border-bottom:.5px solid var(--dsw-alias-border-l2)}',
1290
1329
  '.sen-empty{margin:16px 0;font-size:13px;line-height:20px;color:var(--dsw-alias-label-tertiary)}',
1330
+ '.sen-version{margin:8px 0 0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}',
1291
1331
  '.sen-recent{list-style:none;margin:0;padding:0}',
1292
1332
  '.sen-recent-row{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 0;border-bottom:.5px solid var(--dsw-alias-border-l2);font-size:13px;line-height:20px}',
1293
1333
  '.sen-recent-name{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-primary)}',
@@ -1328,6 +1368,7 @@ window.__ModuleLoader__.load({
1328
1368
  ctx.effect(
1329
1369
  () => () => {
1330
1370
  cancelAll();
1371
+ clearTimers();
1331
1372
  },
1332
1373
  'session-notifier: timers',
1333
1374
  );
package/locale/en.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "meta": {
3
- "title": "Session End Notifier",
4
- "description": "Raises a desktop notification and a short chime when a session finishes or waits for your answer, so you notice while you are away from the page."
3
+ "title": "Session Notifier",
4
+ "description": "A desktop notification and a short chime when a session finishes or stops to wait for you — so a task that ends while you are on another window is not missed."
5
5
  }
6
6
  }
package/locale/zh.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "meta": {
3
- "title": "会话结束提醒",
4
- "description": "会话跑完或停下来等你回答时,用桌面通知和提示音提醒你,切到别处也不会错过。"
3
+ "title": "会话提醒",
4
+ "description": "会话跑完或停下来等你回答时,用桌面通知和提示音提醒你。切到别的窗口、全屏看视频或打游戏时也不会错过。"
5
5
  }
6
6
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-session-notifier",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Desktop notification and a short chime when a DeepSeek Harness session finishes or stops to wait for your answer.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -49,7 +49,8 @@
49
49
  },
50
50
  "scripts": {
51
51
  "verify": "node test/verify-engine.mjs && node test/check-styles.mjs && node test/check-copy.mjs",
52
- "prepublishOnly": "npm run verify"
52
+ "prepublishOnly": "npm run verify",
53
+ "version": "node scripts/sync-version.mjs && git add client.js"
53
54
  },
54
55
  "dsh": {
55
56
  "bundle": {