@telosmaylx/dsh-session-notify 0.1.10 → 0.1.12

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,590 +1,595 @@
1
- <div align="center">
2
-
3
- # dsh-session-notify
4
-
5
- [简体中文](README.md) · **English** · [繁體中文](README.zh-TW.md) · [日本語](README.ja.md) · [한국어](README.ko.md)
6
-
7
- **DSH (DeepSeek Harness) session-completion notification plugin — at the end of every turn, the completion status comes to you, instead of you staring at the screen waiting.**
8
-
9
- [![npm version](https://img.shields.io/npm/v/@telosmaylx/dsh-session-notify)](https://www.npmjs.com/package/@telosmaylx/dsh-session-notify)
10
- [![npm downloads](https://img.shields.io/npm/dm/@telosmaylx/dsh-session-notify)](https://www.npmjs.com/package/@telosmaylx/dsh-session-notify)
11
- [![license](https://img.shields.io/npm/l/@telosmaylx/dsh-session-notify)](./LICENSE)
12
- [![node](https://img.shields.io/node/v/@telosmaylx/dsh-session-notify)](https://www.npmjs.com/package/@telosmaylx/dsh-session-notify)
13
- [![DSH](https://img.shields.io/badge/DSH-Web%20Profile-4D6BFE)](https://www.npmjs.com/package/@telosmaylx/dsh-session-notify)
14
- [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/TelosmaYLX/dsh-session-notify/pulls)
15
-
16
- At the end of every conversation turn, writes "completed / errored / blocked / hit the cap" along with duration and token usage into the session log, and pushes a browser system notification and an in-page toast. Built-in 5 languages, a visual copy-template editor, and a custom preset library; cache hit rate and generation speed come from the official projections, consistent with the status bar.
17
-
18
- </div>
19
-
20
- ---
21
-
22
- ## Table of Contents
23
-
24
- - [Features](#features)
25
- - [Requirements](#requirements)
26
- - [Installation](#installation)
27
- - [Uninstall](#uninstall)
28
- - [Quick Start](#quick-start)
29
- - [Notification Behavior](#notification-behavior)
30
- - [Trigger Conditions](#trigger-conditions)
31
- - [Where Does the Push Body Come From](#where-does-the-push-body-come-from)
32
- - [Notification Examples](#notification-examples)
33
- - [Notification Permissions](#notification-permissions)
34
- - [Configuration](#configuration)
35
- - [Settings Panel](#settings-panel)
36
- - [Template tokens and placeholders](#template-tokens-and-placeholders)
37
- - [Preset System](#preset-system)
38
- - [Host Configuration Options](#host-configuration-options)
39
- - [How It Works](#how-it-works)
40
- - [Project Structure](#project-structure)
41
- - [Development & Debugging](#development--debugging)
42
- - [FAQ](#faq)
43
- - [Changelog](#changelog)
44
- - [Contributing](#contributing)
45
- - [Related Links](#related-links)
46
- - [License](#license)
47
-
48
- ---
49
-
50
- ## Features
51
-
52
- ### Three-Channel Notifications, None Missed
53
-
54
- | Channel | Form | Description |
55
- | --- | --- | --- |
56
- | In-session system message | Collapsible hint line | When each turn ends, appends the end reason, duration and usage as a plugin-sourced system message into the session log, persisted with the JSONL, and still visible after restoring or replaying the session. |
57
- | Browser system notification | Web Notification | Native popup. Each completion event uses an independent `tag` (`dsh-session-notify:<timestamp>`), so it neither replaces the previous one nor collapses into a single grouped entry; clicking the notification focuses back to the window. |
58
- | In-page toast | Floating popup at the bottom-right | The always-shown fallback channel: still gives visible feedback when system notifications are silently suppressed by the platform, permission is denied, or the environment does not support them. At most 3 on screen at once (oldest removed when exceeded), auto-dismisses after 10 seconds, click to close. |
59
-
60
- ### Full Coverage of Background Sessions
61
-
62
- - The host maintains a "most recent notification body" session projection unit (key = `session-complete-notify`) for all sessions, including background ones whose windows aren't open, so the pushed body is consistent across sessions and does not depend on you happening to have that window open.
63
- - The client observes the `running` flag of all sessions from the session list snapshot; a `true → false` edge triggers a push, following the same strategy as the official sidebar reminders (the first observation only records a baseline; sessions already idle are not back-filled).
64
-
65
- ### Customizable Down to Every Sentence
66
-
67
- - **5 languages**: Simplified Chinese, Traditional Chinese, English, Japanese, Korean — the notification copy, the duration and usage wording, and the settings panel UI all switch with the language (instant re-render on switch).
68
- - **Visual template editor** (Chip editor): dynamic information renders as inline chips (placeholder code never leaks); "+ Insert Info" inserts at the cursor (can be inserted mid-text); clicking a chip removes it; each field has a live preview (information flows into the body with sample values).
69
- - **Preset system**: the built-in "Default" preset serves as the baseline; the current configuration can be saved as a custom preset (persisted in `localStorage`), supports auto-numbered unnamed presets (`Untitled`, `Untitled 2`…), a "From: xxx · Modified" origin indicator, and deleting presets.
70
- - **Push title template**: when left empty, each reason uses a default title (completed = task completed / errored = task errored / …); `{title}` references the session title.
71
-
72
- ### Same Source as the Official Metrics
73
-
74
- - **Cache hit rate** comes from the official `tokenUsage` projection: cache reads / (uncached input + cache reads + cache writes).
75
- - **Generation speed** comes from the official `sessionStats` projection: output tokens ÷ decode time.
76
- - Both are fully consistent with the dsh-web-ui status bar, excluding queuing, preparation and tool time; when the projection is unavailable or the data is not ready, it automatically falls back to a local usage-aggregation estimate.
77
-
78
- > [!NOTE]
79
- > The cache hit rate and speed are only shown when inserted via the `{cache}` and `{tps}` placeholders in a custom template. With the built-in default copy, the body contains only duration and usage.
80
-
81
- ### Engineering Quality
82
-
83
- - **Responds only to live events**: resume and replay do not replay old notifications; loading a session does not spam the screen.
84
- - **Self-immune to loops**: the message type the plugin appends (`user/message`) is disjoint from its own listen target (`turn/*`).
85
- - **Zero external dependencies**: the host plane has zero bare imports; UserMessage is constructed manually per the `dsh-llm` `createUserMessage` contract; the pure logic layer (`lib/core.js`) has zero dependencies and can be tested independently.
86
- - **Cordis effect discipline**: the retry timer is wrapped in `ctx.effect()` and returns a `clearTimeout` disposer; the registration is automatically revoked when the fiber unloads, making HMR hot reload safe.
87
- - **Installs and mounts in one step**: declares the official `dsh.bundle` manifest; a single `dsh plugin add` command installs it and it works, no hand-written patch needed.
88
-
89
- ---
90
-
91
- ## Requirements
92
-
93
- | Dependency | Requirement |
94
- | --- | --- |
95
- | DSH (DeepSeek Harness) | Web profile deployment. The official base bundle includes `@deepseek-ai/dsh-settings` (settings namespace) and session projections by default; no extra configuration needed |
96
- | cordis | `>=4.0.0-rc <5` (peer dependency, provided by the host) |
97
- | Node.js | `>=22` (host side) |
98
- | Browser | System notifications when Web Notification is supported; toast falls back when unsupported, permission denied or silently suppressed |
99
-
100
- ---
101
-
102
- ## Installation
103
-
104
- > [!WARNING]
105
- > A bare `npm install` only puts the package into the dependency tree; it does **not register the plugin** — this is DSH's official design (`npm install only adds the dependency; it does not register the plugin`). The only official way to auto-mount is `dsh plugin add`: it reads the `dsh.bundle` manifest inside the package (declared by this plugin since 0.1.3, pointing to the `cordis.patch.yml` at the repository root) and applies it automatically.
106
-
107
- ### Method 1: dsh plugin add (recommended)
108
-
109
- Installing the package also automatically applies `cordis.patch.yml`, mounting the plugin into the profile assembly (host event subscription + client boot graph injection).
110
-
111
- ```bash
112
- dsh plugin --profile web add @telosmaylx/dsh-session-notify
113
- ```
114
-
115
- ### Method 2: Install from the GitHub repository
116
-
117
- ```bash
118
- dsh plugin add github:TelosmaYLX/dsh-session-notify
119
- ```
120
-
121
- You can also run it inside a DSH Web GUI session:
122
-
123
- ```bash
124
- dev_install_package github=TelosmaYLX/dsh-session-notify
125
- ```
126
-
127
- ### Method 3: Hot-mount a local directory (for development)
128
-
129
- Replace the path with your clone directory and run it inside a DSH Web GUI session:
130
-
131
- ```bash
132
- dev_install_package dir=/你的/克隆目录/dsh-session-notify
133
- ```
134
-
135
- ### Method 4: Manual install of the npm package
136
-
137
- First pack it:
138
-
139
- ```bash
140
- npm pack @telosmaylx/dsh-session-notify
141
- ```
142
-
143
- After extracting, install from the specified directory (run inside a DSH Web GUI session):
144
-
145
- ```bash
146
- dev_install_package dir=/解压/目录/package
147
- ```
148
-
149
- ### Method 5: Manual cordis patch (no installer required)
150
-
151
- Append to `~/.dsh/profiles/web/cordis.patch.yml`:
152
-
153
- ```yaml
154
- - insert:
155
- - id: dsh-session-notify
156
- name: '@telosmaylx/dsh-session-notify'
157
- config: {}
158
- ```
159
-
160
- > [!IMPORTANT]
161
- > Regardless of the method, you need to **refresh the browser page once** after installing — the client bundle is injected via the `__DSH_BOOT__` boot graph.
162
-
163
- ## Uninstall
164
-
165
- A single command removes the plugin and its mounting (automatically removes the insert entry from `cordis.patch.yml`):
166
-
167
- ```bash
168
- dsh plugin --profile web remove @telosmaylx/dsh-session-notify
169
- ```
170
-
171
- > [!NOTE]
172
- > Users who installed manually (Methods 4/5) need to also remove the corresponding insert entry from `~/.dsh/profiles/web/cordis.patch.yml`, then refresh the page.
173
-
174
- ### What Is Automatically Cleaned Up on Uninstall
175
-
176
- The plugin implements complete lifecycle teardown (Cordis effect discipline); on uninstall/disable/HMR hot reload:
177
-
178
- | Plane | Automatically released resources |
179
- | --- | --- |
180
- | host | `session/event` event subscription, settings namespace, session projection units, settings-registration retry timer (wrapped in `ctx.effect`); sets an uninstalled flag to suppress already-scheduled microtask appends |
181
- | client | Session list subscription, poll timer for completion push bodies, `window.__dsch_notify_debug` debug hook (removed by reference to prevent closure leaks), in-page toast container DOM |
182
-
183
- ### Data Retained After Uninstall
184
-
185
- - **Settings configuration** (language, copy templates) remains in the settings document and is automatically restored after reinstalling;
186
- - **Custom presets** are stored in the browser `localStorage` (`dsh-scn-custom-presets`), still present after reinstalling;
187
- - System messages already appended to historical sessions and JSONL logs are **not** rolled back (they are part of the session data, with the same semantics as the official sidebar hints).
188
-
189
- ---
190
-
191
- ## Quick Start
192
-
193
- 1. Install by any of the methods above and refresh the page.
194
- 2. Start any conversation turn and wait for it to end — a toast pops up in the bottom-right, a system notification appears in the browser, and a collapsible system hint line appears in the session log.
195
- 3. The first time a completion event is received, the browser requests notification permission (asked only once per page); after granting, subsequent completions all get system notifications.
196
- 4. Open **Settings → Plugins → Session Completion Notify**, switch languages, edit copy templates, and save presets. After saving, click "Click to Refresh" so both the host and client sides re-read, and the new configuration takes effect.
197
-
198
- Right after installation, a collapsible hint line like this appears in the session log:
199
-
200
- ```text
201
- 会话「重构登录模块」已完成(用时 1 分 12 秒,消耗 1,240 输入 / 3,560 输出)。
202
- ```
203
-
204
- > The default copy embeds the session-title token (`{title}`) after "Session"; when the session has no title, it automatically falls back to "Session completed".
205
-
206
- ---
207
-
208
- ## Notification Behavior
209
-
210
- ### Trigger Conditions
211
-
212
- When each conversation turn ends (`turn/end`), the end reason is checked; hitting the whitelist triggers a notification:
213
-
214
- | End reason | Meaning | Default |
215
- | --- | --- | --- |
216
- | `completed` | Session completed normally | Notify |
217
- | `aborted` | Session aborted | Notify |
218
- | `blocked` | Session blocked | Notify |
219
- | `error` | Session errored (with error details, truncated when too long) | Notify |
220
- | `max-tokens` | Reached the output token cap | Notify |
221
- | `interrupted` | Interrupted (an orphan-turn close marker back-filled by the persistence backend after crash recovery) | No notification (configurable) |
222
-
223
- **Subagent sessions are skipped by default** (`header.origin === 'subagent'` or `delegationDepth > 0`) — subagents are orchestrated by their parent session, so per-turn notifications are noise; the skip can be disabled in the host configuration.
224
-
225
- ### Where Does the Push Body Come From
226
-
227
- When the client observes a `running: true → false` edge in the session list, it pushes; the body is fetched with the following priority (polls for up to 6 seconds at 400ms intervals):
228
-
229
- 1. **Host projection** (key = `session-complete-notify`) — available for every session; background sessions get the full text too;
230
- 2. **The notice node in the session event window** (`kind=context` + `form=notice`) — for the session being viewed, available immediately after persistence;
231
- 3. **Fallback** — "See the in-session system message for details" + workspace info (last segment of `cwd`).
232
-
233
- ### Notification Examples
234
-
235
- All of the following are actually generated by `buildNotice` in `lib/core.js`. The default copy uses **differentiated phrasing** per end reason (not uniform sentence patterns):
236
-
237
- Simplified Chinese default copy:
238
-
239
- ```text
240
- 会话「重构登录模块」已完成(用时 3 分 25 秒,消耗 12,400 输入 / 35,600 输出)。 ← 完成:括号紧凑式 + 内嵌会话标题
241
- 会话「重构登录模块」已中止。用时 3 分 25 秒,消耗 12,400 输入 / 35,600 输出。 ← 中止:句号拆句
242
- 会话「重构登录模块」被阻塞。用时 3 分 25 秒,消耗 12,400 输入 / 35,600 输出。 ← 阻塞:句号拆句
243
- 会话「重构登录模块」达到输出上限。用时 3 分 25 秒,消耗 12,400 输入 / 35,600 输出,建议拆分任务后重试。 ← 上限:附建议
244
- ```
245
-
246
- > When the session has no title (`titleValue` empty), it automatically falls back to a phrasing without the title, e.g. "Session completed (took …)".
247
-
248
- On error, the error details come first (single-lined, truncated past 40 characters):
249
-
250
- ```text
251
- 会话「重构登录模块」出错:connection timeout(用时 12 秒)。
252
- ```
253
-
254
- English default copy (session title in double quotes):
255
-
256
- ```text
257
- Session "重构登录模块" completed (took 3m25s, used 12,400 in / 35,600 out).
258
- Session "重构登录模块" hit the output-token cap. Took 3m25s, used 12,400 in / 35,600 out — consider splitting the task.
259
- ```
260
-
261
- Custom template (edited in the settings panel; this example uses all info slots):
262
-
263
- ```text
264
- {title} 干完了!用时 {duration},消耗 {usage},缓存命中 {cache},速度 {tps}
265
- ```
266
-
267
- Rendered result:
268
-
269
- ```text
270
- 重构登录模块 干完了!用时 3 分 25 秒,消耗 103,600 输入 / 35,600 输出,缓存命中 96.5%,速度 92 tok/s
271
- ```
272
-
273
- The same event in the five languages:
274
-
275
- ```text
276
- 会话「重构登录模块」已完成(用时 3 分 25 秒,消耗 1,240 输入 / 3,560 输出)。
277
- 會話「重構登入模組」已完成(用時 3 分 25 秒,消耗 1,240 輸入 / 3,560 輸出)。
278
- Session "重构登录模块" completed (took 3m25s, used 1,240 in / 3,560 out).
279
- セッション「重构登录模块」完了(所要 3 分 25 秒、消費 1,240 入力 / 3,560 出力)。
280
- 세션「重构登录模块」 완료(소요 3분 25초, 소모 1,240 입력 / 3,560 출력)。
281
- ```
282
-
283
- ### Notification Permissions
284
-
285
- | Permission state | Behavior |
286
- | --- | --- |
287
- | `default` (undecided) | Completion events only send a toast; the "Notification Permissions" area of the settings panel provides a "Request Authorization" button (**requested within a user gesture** — Chromium ignores non-gesture automatic requests, so the plugin no longer requests automatically) |
288
- | `granted` | Sends system notifications per "Push Mode" (independent tags, never overwriting each other) |
289
- | `denied` (blocked by the browser) | Toast only; the settings panel shows address-bar instructions (permission icon → site settings → notifications → allow) |
290
- | `undefined` (non-secure context / unsupported) | Toast only; recommends switching to "In-page only" |
291
-
292
- ---
293
-
294
- ## Configuration
295
-
296
- Most configuration is done in the **DSH Web UI → Settings → Plugins → Session Completion Notify** panel (takes effect after saving and clicking "Click to Refresh"). Only "Trigger reason whitelist" and "Skip subagents" are configured in the `config` of the host `cordis.patch.yml`.
297
-
298
- ### Settings Panel
299
-
300
- The panel is registered in the official "Settings → Plugins" panel (`settings.plugin.item` keyed slot, key = `session-complete-notify`), replicating the native plugin card style value by value (12px radius, expand/collapse, rotating chevron, footer status bits + discarded ghost + primary-color save button):
301
-
302
- | Area | Content |
303
- | --- | --- |
304
- | Presets | Dropdown to select a built-in or custom preset; "Add" saves the current configuration as a custom preset; the current preset can be "Deleted" |
305
- | Language | Radio selection among 5 languages; switching instantly re-renders the whole panel |
306
- | Push mode | Choose one of three: dual channel (system notification + in-page toast, default) / system notification only / in-page only |
307
- | Push title | The title template shared by all reasons (a plain input with native placeholder behavior: disappears when typing, restores when cleared); when left empty, each reason uses a default title (completed = task completed, errored = task errored, aborted = task aborted, blocked = task blocked, cap = task hit the output cap); `{title}` references the session title (click "+ Session Title" to insert at the cursor) |
308
- | Templates × 5 | Each end reason (completed, errored, aborted, blocked, output cap) has its own Chip editor: text + inline info chips, insert at the cursor, click to remove, live preview |
309
- | Skip subagent sessions | Checkbox (written into the settings document on save) |
310
- | Notification permissions | Status shown in real time: granted (green) / not yet granted (with a "Request Authorization" button) / blocked by the browser (with address-bar instructions) / environment unsupported |
311
- | Per-reason titles | Collapsible area (collapsed by default): one title input per end reason; empty = use the global template or the language default title |
312
- | Save | Writes to the host settings document (`language` / `templates` / `titleTemplate` / `titleTemplates` / `pushMode`); shows a "Click to Refresh" link after saving |
313
- | Reset | One-click restore of default values (**the current language is kept**, titles/templates/push mode restore to defaults) and saves immediately |
314
-
315
- > [!NOTE]
316
- > The trade-off of "Push mode": `dual` (default) fires both a Windows system notification and an in-page toast; the toast is the fallback channel, guarding against system notifications being silently suppressed by the platform (focus assist, notification banner off). However, **Chromium-shell browsers such as QQ Browser render `Notification` as a "browser built-in in-page push popup"** (a banner at the top/corner of the page, not going through the Windows notification center) — in that case `dual` causes two in-page prompts (the browser built-in popup + the plugin toast). For such browsers, choose "In-page only" (the `Notification` API is no longer called, the browser built-in popup won't appear, and only the plugin's own small toast remains in-page); "System notification only" is ineffective in QQ Browser (it always renders as an in-page popup). The "Send" test button for each reason in the settings panel is likewise affected.
317
-
318
- > [!NOTE]
319
- > Whether the system notification (`Notification` API) can pop up is determined jointly by the **browser and how the site is accessed**: Edge/Chrome **auto-block notifications** for "unfamiliar" sites (a "notifications blocked" appears in the address bar) — click the permission icon on the left of the address bar → site settings → notifications → allow to restore; when accessed as a non-secure context like `http://IP`, `Notification` simply does not exist, so switch to "In-page only". The "Notification Permissions" area of the settings panel shows the current state in real time and gives the corresponding action guide (one-click authorization request). In Firefox, when the window is focused, notifications show as in-page banners and only go to the system notification center when unfocused.
320
-
321
- > [!NOTE]
322
- > The "Skip subagent sessions" checkbox in the panel saves a boolean into the settings document; `config.skipSubagents` in the host `cordis.patch.yml` is its startup default; if either is true, subagents are skipped.
323
-
324
- ### Template tokens and placeholders
325
-
326
- Each end reason has its own template input; **the token is the switch** — the data is shown only when you insert the corresponding info token into the template:
327
-
328
- | Placeholder | Meaning | Example value |
329
- | --- | --- | --- |
330
- | `{title}` | Session title (also usable in the push title template) | `重构登录模块` |
331
- | `{duration}` | Duration of this turn (timed from `turn/start` to `turn/end`) | `3 分 25 秒` / `3m25s` |
332
- | `{usage}` | Token usage (input = uncached + cache reads + cache writes) | `1,240 输入 / 3,560 输出` |
333
- | `{error}` | Error message (shows `none` when there is no error; single-lined, truncated at 80 characters) | `connection timeout` |
334
- | `{cache}` | Cache hit rate (per the official projection; empty when no data) | `96.5%` |
335
- | `{tps}` | Generation speed (per the official projection; empty when no data) | `92 tok/s` |
336
- | `{label}` | Deprecated automatically stripped at render time; old templates remain compatible (the option has been removed from the insert menu) | |
337
-
338
- An empty template uses the built-in default copy (automatically includes duration and usage). The collapsible row's `summary` shares the same source as the body (the rendered result is truncated to 120 characters) users who only look at the collapsible row still see the real title, duration and usage.
339
-
340
- ### Preset System
341
-
342
- - **Built-in preset**: only "Default", serving as the baseline.
343
- - **Custom presets**: stored in `localStorage` (key = `dsh-scn-custom-presets`):
344
- - After naming, "Add" saves it as a custom preset; afterwards it can be auto-synced with "Modify" or removed with "Delete";
345
- - **Auto-numbered unnamed presets**: saving directly from "Default / blank" automatically generates `Untitled`, `Untitled 2`, `Untitled 3`… (the number takes the current max + 1);
346
- - The form shows a "From: xxx · Modified" origin indicator (when loaded from a preset but the content has been changed).
347
- - **Save syncs**: on save, if the form originates from a custom preset, that preset is updated; otherwise a new one is created or the unnamed-preset numbering continues.
348
-
349
- ### Host Configuration Options
350
-
351
- ```yaml
352
- - insert:
353
- - id: dsh-session-notify
354
- name: '@telosmaylx/dsh-session-notify'
355
- config:
356
- reasons: [completed, aborted, blocked, error, max-tokens]
357
- skipSubagents: true
358
- ```
359
-
360
- | Field | Type | Default | Description |
361
- | --- | --- | --- | --- |
362
- | `reasons` | `string[]` | `[completed, aborted, blocked, error, max-tokens]` | Whitelist of `turn/end` reasons that trigger notifications |
363
- | `skipSubagents` | `boolean` | `true` | Skip subagent sessions (`origin=subagent` or `delegationDepth>0`) |
364
-
365
- ---
366
-
367
- ## How It Works
368
-
369
- The plugin is split into a **host plane** (Node) and a **client plane** (browser), bridged by the session log (JSONL) and official session projections:
370
-
371
- ```text
372
- ┌─────────────────── 宿主平面(lib/index.js,Node)──────────────────┐
373
- │ │
374
- │ session/event 火线 │
375
- │ ├─ turn/start → tracker 起表(key: sessionId:turn) │
376
- ├─ assistant/message → 累加该轮 token 用量
377
- └─ turn/end → reason.kind ∈ reasons ?
378
- ├─ 子代理会话?跳过
379
- ├─ 读官方投影:cache / tps / title
380
- ├─ 按语言+模板构建通知(summary ≤120 字)
381
- └─ queueMicrotask 追加系统消息
382
- (避开 append 重入窗口)
383
-
384
- settings.register → 官方「设置 → 插件」命名空间(失败退避重试)
385
- sessionProjections → 注册投影单元(key=session-complete-notify)
386
- └──────────────────────────────┬──────────────────────────────────────┘
387
- user/message (source: plugin, form: notice)
388
- JSONL 持久化 + 投影推送
389
- ┌─────────────────── 客户端平面(lib/client.js,浏览器)──────────────┐
390
-
391
- 会话列表订阅:running true false 边沿 → pushCompletion │
392
- │ ├─ 取正文:投影 → 事件窗口 notice → 降级(轮询 ≤6s) │
393
- ├─ Web Notification(独立 tag,点击聚焦)
394
- └─ 页内 toast(永远展示,≤3 条,10s 自动消失)
395
-
396
- slots.inject('settings.plugin.item') 设置卡片(预设/语言/模板)
397
- └─────────────────────────────────────────────────────────────────────┘
398
- ```
399
-
400
- ### Key Design Decisions
401
-
402
- - **No replay**: only live events are processed; resume and replay do not back-fill historical notifications.
403
- - **No self-loop**: the plugin appends `user/message` and only listens to `turn/*`; the event types are disjoint.
404
- - **Zero external imports**: the plugin is loaded from the repository directory by realpath, and `@deepseek-ai/*` cannot be bare-resolved — the host plane uses `createRequire` to anchor the profile's shared dependency hub (`.dsh/profiles/node_modules`) for `schemastery` (settings schema) and `zod` (projection schema); UserMessage is constructed manually per the `dsh-llm` contract (`id = crypto.randomUUID()`, deep-freeze done by the adopt-snapshot stage of `session.append`).
405
- - **Append reentrancy avoidance**: the `session/event` observer callback runs within the publish boundary of the append fired on `turn/end` (dsh-session sets `entry.appending` before dispatch and resets it in `finally`), so a synchronous append would be rejected — therefore it is deferred to `queueMicrotask` (the microtask runs only after this synchronous stack, including the `finally` reset).
406
- - **Effect discipline**: the settings-registration backoff retry timer is wrapped in `ctx.effect()` and returns a `clearTimeout` disposer — if the plugin is uninstalled or hot-reloaded within the retry window, the timer is torn down with the fiber and never registers against a released ctx (in very old environments without the `ctx.effect` API, it degrades to a bare timer plus a ctx-torn-down guard catch).
407
- - **HMR safe**: the `core.js` import carries `?v=1` cache busting (HMR reload is keyed by URL); if settings registration hits a hot-reload race (duplicate), it automatically backs off and retries (up to 8 times, interval `400ms × attempts`).
408
- - **Dual-track projection registration**: prefers `ctx.root.get('sessionProjections')` (the instance closest to the host root), falling back to the injection instance when unavailable; if registered only into the injection instance, the client may not read the projection unit and the push body takes the fallback path best-effort, does not affect in-session system messages.
409
-
410
- ---
411
-
412
- ## Project Structure
413
-
414
- ```text
415
- dsh-session-notify/
416
- ├── lib/
417
- │ ├── index.js # 宿主平面(Node):session/event 订阅 → 系统消息落盘;
418
- │ │ # settings 命名空间注册(schemastery schema,退避重试);
419
- │ │ # sessionProjections 投影单元(后台会话推送正文)
420
- │ ├── core.js # 纯逻辑层(零依赖,可独立测试):轮次计时与用量聚合、
421
- │ │ # 5 语言文案表、时长/用量/缓存/速度格式化、
422
- │ │ # 模板渲染({title}{duration}{usage}{error}{cache}{tps})
423
- └── client.js # 浏览器平面:完成推送(系统通知 + toast)、
424
- # 设置卡片(Chip 模板编辑器 + 预设系统 + 实时预览)
425
- ├── scripts/
426
- ├── build.sh # 零构建:仅 node --check 语法校验
427
- ├── verify-notice.mjs # 校验会话日志落盘证据(zstd 多帧逐帧解压)
428
- ├── probe-client.mjs # 探针:客户端装配
429
- │ ├── probe-client-e2e.mjs # 探针:客户端端到端
430
- │ ├── probe-card-render.mjs # 探针:设置卡片渲染
431
- │ ├── probe-settings-card.mjs # 探针:设置面板卡片
432
- │ ├── probe-settings-check.mjs# 探针:设置面板检查
433
- └── probe-diag-settings.mjs # 探针:settings 诊断
434
- ├── cordis.patch.yml # dsh.bundle manifest —— dsh plugin add 自动挂载的凭证
435
- ├── package.json # dsh.bundle(patch)+ dsh.client(web 注入)双 manifest;
436
- # exports: "." / "./client" / "./core"
437
- ├── LICENSE # MIT
438
- └── README.md # 本文档
439
- ```
440
-
441
- ---
442
-
443
- ## Development & Debugging
444
-
445
- Syntax check (zero-build, the same check as `prepublishOnly`):
446
-
447
- ```bash
448
- npm run build
449
- ```
450
-
451
- Publishing (runs the `prepublishOnly` syntax check automatically before publishing):
452
-
453
- ```bash
454
- npm publish --registry=https://registry.npmjs.org --access public
455
- ```
456
-
457
- Offline verification: extracts all plugin-source events and the `turn/end` tail sequence from a session log (when no path is given, automatically selects the newest session under `~/.dsh/sessions`):
458
-
459
- ```bash
460
- node scripts/verify-notice.mjs <session.jsonl.zstd>
461
- ```
462
-
463
- ### Debugging Entry Points
464
-
465
- | Entry point | Content |
466
- | --- | --- |
467
- | `~/.dsh/session-complete-notify.log` | Host diagnostic log: settings registration, retries and failures, projection registration, append-failure stacks |
468
- | Browser console `[dsh-session-notify-client]` | Client log: permission state, notification display, settings save |
469
- | `window.__dsch_notify_debug.readNotice(id)` | Manually read the latest notification body of a given session |
470
- | `window.__dsch_notify_debug.snapshotDebug(id)` | Session tail node types + notice count + most recent body (first 200 chars) |
471
-
472
- ---
473
-
474
- ## FAQ
475
-
476
- <details>
477
- <summary><b>Why isn't it auto-mounted after npm install?</b></summary>
478
-
479
- This is DSH's official design: `npm install` only puts the package into the dependency tree, it does not register the plugin. The only way to auto-mount is `dsh plugin add` — it reads the `dsh.bundle` manifest inside the package (declared by this plugin since 0.1.3) and automatically applies `cordis.patch.yml`. See [Installation](#installation).
480
-
481
- </details>
482
-
483
- <details>
484
- <summary><b>Why is "interrupted" not notified?</b></summary>
485
-
486
- `interrupted` is an orphan-turn close marker back-filled by the persistence backend after crash recovery; from the user's perspective it is not a "completion" (otherwise restoring a session would flood the screen with false reports). If you really need it, you can add it to `reasons` in the host configuration.
487
-
488
- </details>
489
-
490
- <details>
491
- <summary><b>Do background sessions (with no window open) also get pushed?</b></summary>
492
-
493
- Yes. The client observes the `running` edges of all sessions from the session list snapshot; the body prefers the host projection — the host maintains a projection unit for all sessions (including background ones), so the pushed body is consistent across sessions. When the projection is unavailable, it falls back to the event window or workspace info.
494
-
495
- </details>
496
-
497
- <details>
498
- <summary><b>Why does it ask me to refresh the page after saving settings?</b></summary>
499
-
500
- The host reads the settings once when registering the namespace, and the client bundle is assembled at page load. After saving, click "Click to Refresh" so both sides re-read, and the new language and templates take effect.
501
-
502
- </details>
503
-
504
- <details>
505
- <summary><b>Where do the cache hit rate and speed data come from? Why are they sometimes empty?</b></summary>
506
-
507
- They come from the official `sessionProjections` (`tokenUsage`, `sessionStats`), consistent with the dsh-web-ui status bar. When the host fails to read the projection snapshot or the data is not ready, it falls back to a local usage-aggregation estimate; if there is still no data, the field is left empty (the token shows nothing even if inserted). Additionally, these two appear only when inserted via the `{cache}` and `{tps}` placeholders in a custom template; the default copy does not include them.
508
-
509
- </details>
510
-
511
- <details>
512
- <summary><b>What if the error message in the notification body is too long or has line breaks?</b></summary>
513
-
514
- Both the summary line (collapsible row) and error details are single-lined and truncated: summary 120 chars, template `{error}` 80 chars, default-copy error details 40 chars; overlong text ends with an ellipsis.
515
-
516
- </details>
517
-
518
- <details>
519
- <summary><b>Can I customize the icon or sound of system notifications?</b></summary>
520
-
521
- The current version uses the browser's default notification style and does not inject a custom icon or sound; the toast is a fixed dark card. If you need these capabilities, feel free to open an Issue or PR.
522
-
523
- </details>
524
-
525
- <details>
526
- <summary><b>Why can't Edge push system notifications? Why does QQ Browser only show an in-page banner (built-in push popup)?</b></summary>
527
-
528
- Both are browser behaviors; the plugin cannot force anything:
529
-
530
- - **Edge / Chrome**: **auto-block notifications** for "unfamiliar" sites (a "notifications blocked" appears in the address bar). Click the permission icon on the left of the address bar → site settings → notifications → allow to restore, after which the Windows notification center works normally. You can also turn off "auto-block" in the browser's notification settings.
531
- - **Chromium shells like QQ Browser**: always render `Notification` as a **browser built-in in-page push popup** (a banner at the top/corner of the page, not going through the Windows notification center), and there is no system-notification option. How the three push modes actually behave:
532
- - `Dual channel` → browser built-in popup + plugin toast, two in-page prompts;
533
- - `System notification only` ineffective (QQ Browser always renders it as an in-page popup);
534
- - `In-page only` the browser built-in popup doesn't appear; only the plugin's small toast remains in-page (recommended).
535
- The "Send" test button for each reason in the settings panel is likewise rendered by this rule.
536
- - **Firefox**: when the window is focused, notifications show as in-page banners; they go to the system notification center only when unfocused/minimized; permission must be manually allowed in the address bar.
537
- - Also note: when accessed via `http://IP` (non-secure context), `Notification` does not exist, so no browser can pop a system notification.
538
-
539
- The "Notification Permissions" area of the settings panel shows the current state and the corresponding action guide in real time.
540
-
541
- </details>
542
-
543
- ---
544
-
545
- ## Changelog
546
-
547
- | Version | Date | Changes |
548
- | --- | --- | --- |
549
- | **0.1.10** | 2026-08-29 | "Push title" changed to a native input (native placeholder behavior: not copyable, disappears when typing, restores when cleared; "+ Session Title" inserts `{title}` at the cursor); docs add the QQ Browser built-in push popup explanation (actual behavior of the three push modes + the send button tests follow the same rule) |
550
- | **0.1.9** | 2026-08-29 | Push titles now support **per-reason customization** (collapsible UI, collapsed by default to stay lean; when empty, each reason uses a differentiated default title: task completed / task errored / task aborted / task blocked / task hit the output cap, in 5 languages); the projection upgraded to an object (kind/text/title) carrying the host-rendered title; the reset button **keeps the current language**; the default copy embeds the "session title" token (Session "{title}" completed, auto-falls back when there's no title); settings-panel template previews sync; "+ Insert Info" no longer auto-collapses after inserting a token; deleting a custom preset currently in use automatically switches back to the default; template preview fixed (doesn't hide on click, hides only when typing, restores when cleared); each reason gains a "Send" button (one-click test notification rendered with the current template) |
551
- | **0.1.8** | 2026-08-29 | Default push title changed to "Task completed" (`{title}` can still reference the session title); default copy is differentiated per end reason (completed = compact parentheses / aborted·blocked = separate sentences / errored = error first / cap = with a suggestion, in 5 languages); the settings panel gains a "Reset" button for one-click restore of defaults |
552
- | **0.1.7** | 2026-08-29 | Fix the 0.1.6 settings-card crash: `notificationPermissionRow`/`requestPermissionNow` referenced Card-component-internal state (out of scope), causing a render ReferenceError and the whole settings card to disappear; changed to self-contained + callback params |
553
- | **0.1.6** | 2026-08-29 | The settings panel gains a "Notification permissions" status area (grant state in real time + one-click request-authorization button + address-bar guide when blocked); authorization changed to **requesting within a user gesture** (Chromium ignores non-gesture automatic requests; the typical Edge auto-block-for-unfamiliar-sites scenario is solved); FAQ adds browser-difference explanations |
554
- | **0.1.5** | 2026-08-29 | New "Push mode" setting (dual channel / system notification only / in-page only): solves the double-prompt caused by Chromium shells like QQ Browser rendering `Notification` as an in-page banner; `pushMode` added to the settings schema and settings panel |
555
- | **0.1.4** | 2026-08-28 | Complete uninstall support added: `dispose` lifecycle teardown (host sets an uninstalled flag to suppress pending microtask appends; client cleans up the body-polling timer, the `__dsch_notify_debug` hook, and the toast container); uninstall docs and FAQ updated accordingly |
556
- | **0.1.3** | 2026-08-28 | Declare the official `dsh.bundle` manifest (`dsh plugin add` auto-mounts with a single command); settings retry timer wrapped in `ctx.effect()` (Cordis effect discipline); installation docs rearranged |
557
- | 0.1.2 | 2026-08-27 | Package renamed to the `@telosmaylx` scope (npm username scope) |
558
- | 0.1.1 | 2026-08-27 | Documented the GitHub and npm installation methods |
559
- | 0.1.0 | 2026-08-26 | Initial version: in-session system messages + browser push + official settings panel |
560
-
561
- ---
562
-
563
- ## Contributing
564
-
565
- Issues and PRs are welcome:
566
-
567
- 1. Fork the repository and create a new branch (`feat/xxx`)
568
- 2. After changes, run `npm run build` for the syntax check
569
- 3. Submit a PR, describing the motivation and how you verified it
570
-
571
- Please follow the [Cordis development tutorial](https://deepseek-harness.github.io/deepseek-harness/develop/cordis-tutorial) discipline before submitting:
572
-
573
- - Resources outside Cordis (timers, subscriptions, watchers) must be wrapped in `ctx.effect()` and return a disposer;
574
- - Configuration items must have an explicit `id` to prevent edit drift;
575
- - A plugin must declare the `dsh.bundle` manifest to be recognized and installed by `dsh plugin add`.
576
-
577
- ---
578
-
579
- ## Related Links
580
-
581
- - [awesome-dsh-plugin](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin) — A curated list of DSH plugins (submission rule: `dsh.bundle` is the only credential for installation)
582
- - [Cordis development tutorial](https://deepseek-harness.github.io/deepseek-harness/develop/cordis-tutorial) — The full plugin-development process (chapters 01–07)
583
- - [npm package homepage](https://www.npmjs.com/package/@telosmaylx/dsh-session-notify)
584
- - [GitHub repository](https://github.com/TelosmaYLX/dsh-session-notify)
585
-
586
- ---
587
-
588
- ## License
589
-
590
- [MIT](./LICENSE) © dsh-session-notify contributors
1
+ <div align="center">
2
+
3
+ # dsh-session-notify
4
+
5
+ [简体中文](README.md) · **English** · [繁體中文](README.zh-TW.md) · [日本語](README.ja.md) · [한국어](README.ko.md)
6
+
7
+ **DSH (DeepSeek Harness) session-completion notification plugin — at the end of every turn, the completion status comes to you, instead of you staring at the screen waiting.**
8
+
9
+ [![npm version](https://img.shields.io/npm/v/@telosmaylx/dsh-session-notify)](https://www.npmjs.com/package/@telosmaylx/dsh-session-notify)
10
+ [![npm downloads](https://img.shields.io/npm/dm/@telosmaylx/dsh-session-notify)](https://www.npmjs.com/package/@telosmaylx/dsh-session-notify)
11
+ [![license](https://img.shields.io/npm/l/@telosmaylx/dsh-session-notify)](./LICENSE)
12
+ [![node](https://img.shields.io/node/v/@telosmaylx/dsh-session-notify)](https://www.npmjs.com/package/@telosmaylx/dsh-session-notify)
13
+ [![DSH](https://img.shields.io/badge/DSH-Web%20Profile-4D6BFE)](https://www.npmjs.com/package/@telosmaylx/dsh-session-notify)
14
+ [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/TelosmaYLX/dsh-session-notify/pulls)
15
+
16
+ At the end of every conversation turn, writes "completed / errored / blocked / hit the cap" along with duration and token usage into the session log, and pushes a browser system notification and an in-page toast. Built-in 5 languages, a visual copy-template editor, and a custom preset library; cache hit rate and generation speed come from the official projections, consistent with the status bar.
17
+
18
+ </div>
19
+
20
+ ---
21
+
22
+ ## Table of Contents
23
+
24
+ - [Features](#features)
25
+ - [Requirements](#requirements)
26
+ - [Installation](#installation)
27
+ - [Uninstall](#uninstall)
28
+ - [Quick Start](#quick-start)
29
+ - [Notification Behavior](#notification-behavior)
30
+ - [Trigger Conditions](#trigger-conditions)
31
+ - [Where Does the Push Body Come From](#where-does-the-push-body-come-from)
32
+ - [Notification Examples](#notification-examples)
33
+ - [Notification Permissions](#notification-permissions)
34
+ - [Configuration](#configuration)
35
+ - [Settings Panel](#settings-panel)
36
+ - [Template tokens and placeholders](#template-tokens-and-placeholders)
37
+ - [Preset System](#preset-system)
38
+ - [Host Configuration Options](#host-configuration-options)
39
+ - [How It Works](#how-it-works)
40
+ - [Project Structure](#project-structure)
41
+ - [Development & Debugging](#development--debugging)
42
+ - [FAQ](#faq)
43
+ - [Changelog](#changelog)
44
+ - [Contributing](#contributing)
45
+ - [Related Links](#related-links)
46
+ - [License](#license)
47
+
48
+ ---
49
+
50
+ ## Features
51
+
52
+ ### Three-Channel Notifications, None Missed
53
+
54
+ | Channel | Form | Description |
55
+ | --- | --- | --- |
56
+ | In-session system message | Collapsible hint line | When each turn ends, appends the end reason, duration and usage as a plugin-sourced system message into the session log, persisted with the JSONL, and still visible after restoring or replaying the session. |
57
+ | Browser system notification | Web Notification | Native popup. Each completion event uses an independent `tag` (`dsh-session-notify:<timestamp>`), so it neither replaces the previous one nor collapses into a single grouped entry; clicking the notification focuses back to the window. |
58
+ | In-page toast | Floating popup at the bottom-right | The always-shown fallback channel: still gives visible feedback when system notifications are silently suppressed by the platform, permission is denied, or the environment does not support them. At most 3 on screen at once (oldest removed when exceeded), auto-dismisses after 10 seconds, click to close. |
59
+
60
+ ### Full Coverage of Background Sessions
61
+
62
+ - The host maintains a "most recent notification body" session projection unit (key = `session-complete-notify`) for all sessions, including background ones whose windows aren't open, so the pushed body is consistent across sessions and does not depend on you happening to have that window open.
63
+ - The client observes the `running` flag of all sessions from the session list snapshot; a `true → false` edge triggers a push, following the same strategy as the official sidebar reminders (the first observation only records a baseline; sessions already idle are not back-filled).
64
+
65
+ ### Customizable Down to Every Sentence
66
+
67
+ - **5 languages**: Simplified Chinese, Traditional Chinese, English, Japanese, Korean — the notification copy, the duration and usage wording, and the settings panel UI all switch with the language (instant re-render on switch).
68
+ - **Visual template editor** (Chip editor): dynamic information renders as inline chips (placeholder code never leaks); "+ Insert Info" inserts at the cursor (can be inserted mid-text); clicking a chip removes it; each field has a live preview (information flows into the body with sample values).
69
+ - **Preset system**: the built-in "Default" preset serves as the baseline; the current configuration can be saved as a custom preset (persisted in `localStorage`), supports auto-numbered unnamed presets (`Untitled`, `Untitled 2`…), a "From: xxx · Modified" origin indicator, and deleting presets.
70
+ - **Push title template**: when left empty, each reason uses a default title (completed = task completed / errored = task errored / …); `{title}` references the session title.
71
+
72
+ ### Same Source as the Official Metrics
73
+
74
+ - **Cache hit rate** comes from the official `tokenUsage` projection: cache reads / (uncached input + cache reads + cache writes).
75
+ - **Generation speed** comes from the official `sessionStats` projection: output tokens ÷ decode time.
76
+ - Both are fully consistent with the dsh-web-ui status bar, excluding queuing, preparation and tool time; when the projection is unavailable or the data is not ready, it automatically falls back to a local usage-aggregation estimate.
77
+
78
+ > [!NOTE]
79
+ > The cache hit rate and speed are only shown when inserted via the `{cache}` and `{tps}` placeholders in a custom template. With the built-in default copy, the body contains only duration and usage.
80
+
81
+ ### Engineering Quality
82
+
83
+ - **Responds only to live events**: resume and replay do not replay old notifications; loading a session does not spam the screen.
84
+ - **Self-immune to loops**: the message type the plugin appends (`user/message`) is disjoint from its own listen target (`turn/*`).
85
+ - **Zero external dependencies**: the host plane has zero bare imports; UserMessage is constructed manually per the `dsh-llm` `createUserMessage` contract; the pure logic layer (`lib/core.js`) has zero dependencies and can be tested independently.
86
+ - **Cordis effect discipline**: the retry timer is wrapped in `ctx.effect()` and returns a `clearTimeout` disposer; the registration is automatically revoked when the fiber unloads, making HMR hot reload safe.
87
+ - **Installs and mounts in one step**: declares the official `dsh.bundle` manifest; a single `dsh plugin add` command installs it and it works, no hand-written patch needed.
88
+
89
+ ---
90
+
91
+ ## Requirements
92
+
93
+ | Dependency | Requirement |
94
+ | --- | --- |
95
+ | DSH (DeepSeek Harness) | Web profile deployment. The official base bundle includes `@deepseek-ai/dsh-settings` (settings namespace) and session projections by default; no extra configuration needed |
96
+ | cordis | `>=4.0.0-rc <5` (peer dependency, provided by the host) |
97
+ | Node.js | `>=22` (host side) |
98
+ | Browser | System notifications when Web Notification is supported; toast falls back when unsupported, permission denied or silently suppressed |
99
+
100
+ ---
101
+
102
+ ## Installation
103
+
104
+ > [!WARNING]
105
+ > A bare `npm install` only puts the package into the dependency tree; it does **not register the plugin** — this is DSH's official design (`npm install only adds the dependency; it does not register the plugin`). The only official way to auto-mount is `dsh plugin add`: it reads the `dsh.bundle` manifest inside the package (declared by this plugin since 0.1.3, pointing to the `cordis.patch.yml` at the repository root) and applies it automatically.
106
+
107
+ ### Method 1: dsh plugin add (recommended)
108
+
109
+ Installing the package also automatically applies `cordis.patch.yml`, mounting the plugin into the profile assembly (host event subscription + client boot graph injection).
110
+
111
+ ```bash
112
+ dsh plugin --profile web add @telosmaylx/dsh-session-notify
113
+ ```
114
+
115
+ ### Method 2: Install from the GitHub repository
116
+
117
+ ```bash
118
+ dsh plugin add github:TelosmaYLX/dsh-session-notify
119
+ ```
120
+
121
+ You can also run it inside a DSH Web GUI session:
122
+
123
+ ```bash
124
+ dev_install_package github=TelosmaYLX/dsh-session-notify
125
+ ```
126
+
127
+ ### Method 3: Hot-mount a local directory (for development)
128
+
129
+ Replace the path with your clone directory and run it inside a DSH Web GUI session:
130
+
131
+ ```bash
132
+ dev_install_package dir=/你的/克隆目录/dsh-session-notify
133
+ ```
134
+
135
+ ### Method 4: Manual install of the npm package
136
+
137
+ First pack it:
138
+
139
+ ```bash
140
+ npm pack @telosmaylx/dsh-session-notify
141
+ ```
142
+
143
+ After extracting, install from the specified directory (run inside a DSH Web GUI session):
144
+
145
+ ```bash
146
+ dev_install_package dir=/解压/目录/package
147
+ ```
148
+
149
+ ### Method 5: Manual cordis patch (no installer required)
150
+
151
+ Append to `~/.dsh/profiles/web/cordis.patch.yml`:
152
+
153
+ ```yaml
154
+ - insert:
155
+ - id: dsh-session-notify
156
+ name: '@telosmaylx/dsh-session-notify'
157
+ config: {}
158
+ ```
159
+
160
+ > [!IMPORTANT]
161
+ > Regardless of the method, you need to **refresh the browser page once** after installing — the client bundle is injected via the `__DSH_BOOT__` boot graph.
162
+
163
+ ## Uninstall
164
+
165
+ A single command removes the plugin and its mounting (automatically removes the insert entry from `cordis.patch.yml`):
166
+
167
+ ```bash
168
+ dsh plugin --profile web remove @telosmaylx/dsh-session-notify
169
+ ```
170
+
171
+ > [!NOTE]
172
+ > Users who installed manually (Methods 4/5) need to also remove the corresponding insert entry from `~/.dsh/profiles/web/cordis.patch.yml`, then refresh the page.
173
+
174
+ ### What Is Automatically Cleaned Up on Uninstall
175
+
176
+ The plugin implements complete lifecycle teardown (Cordis effect discipline); on uninstall/disable/HMR hot reload:
177
+
178
+ | Plane | Automatically released resources |
179
+ | --- | --- |
180
+ | host | `session/event` event subscription, settings namespace, session projection units, settings-registration retry timer (wrapped in `ctx.effect`); sets an uninstalled flag to suppress already-scheduled microtask appends |
181
+ | client | Session list subscription, poll timer for completion push bodies, `window.__dsch_notify_debug` debug hook (removed by reference to prevent closure leaks), in-page toast container DOM |
182
+
183
+ ### Data Retained After Uninstall
184
+
185
+ - **Settings configuration** (language, copy templates) remains in the settings document and is automatically restored after reinstalling;
186
+ - **Custom presets** are stored in the browser `localStorage` (`dsh-scn-custom-presets`), still present after reinstalling;
187
+ - System messages already appended to historical sessions and JSONL logs are **not** rolled back (they are part of the session data, with the same semantics as the official sidebar hints).
188
+
189
+ ---
190
+
191
+ ## Quick Start
192
+
193
+ 1. Install by any of the methods above and refresh the page.
194
+ 2. Start any conversation turn and wait for it to end — a toast pops up in the bottom-right, a system notification appears in the browser, and a collapsible system hint line appears in the session log.
195
+ 3. The first time a completion event is received, the browser requests notification permission (asked only once per page); after granting, subsequent completions all get system notifications.
196
+ 4. Open **Settings → Plugins → Session Completion Notify**, switch languages, edit copy templates, and save presets. After saving, click "Click to Refresh" so both the host and client sides re-read, and the new configuration takes effect.
197
+
198
+ Right after installation, a collapsible hint line like this appears in the session log:
199
+
200
+ ```text
201
+ 会话「重构登录模块」已完成(用时 1 分 12 秒,消耗 1,240 输入 / 3,560 输出)。
202
+ ```
203
+
204
+ > The default copy embeds the session-title token (`{title}`) after "Session"; when the session has no title, it automatically falls back to "Session completed".
205
+
206
+ ---
207
+
208
+ ## Notification Behavior
209
+
210
+ ### Trigger Conditions
211
+
212
+ When each conversation turn ends (`turn/end`), the end reason is checked; hitting the whitelist triggers a notification:
213
+
214
+ | End reason | Meaning | Default |
215
+ | --- | --- | --- |
216
+ | `completed` | Session completed normally | Notify |
217
+ | `aborted` | Session aborted | Notify |
218
+ | `blocked` | Session blocked | Notify |
219
+ | `error` | Session errored (with error details, truncated when too long) | Notify |
220
+ | `max-tokens` | Reached the output token cap | Notify |
221
+ | `interrupted` | Interrupted (an orphan-turn close marker back-filled by the persistence backend after crash recovery) | No notification (configurable) |
222
+
223
+ **Subagent sessions are skipped by default** (`header.origin === 'subagent'` or `delegationDepth > 0`) — subagents are orchestrated by their parent session, so per-turn notifications are noise; the skip can be disabled in the host configuration.
224
+
225
+ ### Where Does the Push Body Come From
226
+
227
+ When the client observes a `running: true → false` edge in the session list, it pushes; the body is fetched with the following priority (polls for up to 6 seconds at 400ms intervals):
228
+
229
+ 1. **Host projection** (key = `session-complete-notify`) — available for every session; background sessions get the full text too;
230
+ 2. **The notice node in the session event window** (`kind=context` + `form=notice`) — for the session being viewed, available immediately after persistence;
231
+ 3. **Fallback** — "See the in-session system message for details" + workspace info (last segment of `cwd`).
232
+
233
+ ### Notification Examples
234
+
235
+ All of the following are actually generated by `buildNotice` in `lib/core.js`. The default copy uses **differentiated phrasing** per end reason (not uniform sentence patterns):
236
+
237
+ Simplified Chinese default copy:
238
+
239
+ ```text
240
+ 会话「重构登录模块」已完成(用时 3 分 25 秒,消耗 12,400 输入 / 35,600 输出)。 ← 完成:括号紧凑式 + 内嵌会话标题
241
+ 会话「重构登录模块」已中止。用时 3 分 25 秒,消耗 12,400 输入 / 35,600 输出。 ← 中止:句号拆句
242
+ 会话「重构登录模块」被阻塞。用时 3 分 25 秒,消耗 12,400 输入 / 35,600 输出。 ← 阻塞:句号拆句
243
+ 会话「重构登录模块」达到输出上限。用时 3 分 25 秒,消耗 12,400 输入 / 35,600 输出,建议拆分任务后重试。 ← 上限:附建议
244
+ ```
245
+
246
+ > When the session has no title (`titleValue` empty), it automatically falls back to a phrasing without the title, e.g. "Session completed (took …)".
247
+
248
+ On error, the error details come first (single-lined, truncated past 40 characters):
249
+
250
+ ```text
251
+ 会话「重构登录模块」出错:connection timeout(用时 12 秒)。
252
+ ```
253
+
254
+ English default copy (session title in double quotes):
255
+
256
+ ```text
257
+ Session "重构登录模块" completed (took 3m25s, used 12,400 in / 35,600 out).
258
+ Session "重构登录模块" hit the output-token cap. Took 3m25s, used 12,400 in / 35,600 out — consider splitting the task.
259
+ ```
260
+
261
+ Custom template (edited in the settings panel; this example uses all info slots):
262
+
263
+ ```text
264
+ {title} 干完了!用时 {duration},消耗 {usage},缓存命中 {cache},速度 {tps}
265
+ ```
266
+
267
+ Rendered result:
268
+
269
+ ```text
270
+ 重构登录模块 干完了!用时 3 分 25 秒,消耗 103,600 输入 / 35,600 输出,缓存命中 96.5%,速度 92 tok/s
271
+ ```
272
+
273
+ The same event in the five languages:
274
+
275
+ ```text
276
+ 会话「重构登录模块」已完成(用时 3 分 25 秒,消耗 1,240 输入 / 3,560 输出)。
277
+ 會話「重構登入模組」已完成(用時 3 分 25 秒,消耗 1,240 輸入 / 3,560 輸出)。
278
+ Session "重构登录模块" completed (took 3m25s, used 1,240 in / 3,560 out).
279
+ セッション「重构登录模块」完了(所要 3 分 25 秒、消費 1,240 入力 / 3,560 出力)。
280
+ 세션「重构登录模块」 완료(소요 3분 25초, 소모 1,240 입력 / 3,560 출력)。
281
+ ```
282
+
283
+ ### Notification Permissions
284
+
285
+ | Permission state | Behavior |
286
+ | --- | --- |
287
+ | `default` (undecided) | Completion events only send a toast; the "Notification Permissions" area of the settings panel provides a "Request Authorization" button (**requested within a user gesture** — Chromium ignores non-gesture automatic requests, so the plugin no longer requests automatically) |
288
+ | `granted` | Sends system notifications per "Push Mode" (independent tags, never overwriting each other) |
289
+ | `denied` (blocked by the browser) | Toast only; the settings panel shows address-bar instructions (permission icon → site settings → notifications → allow) |
290
+ | `undefined` (non-secure context / unsupported) | Toast only; recommends switching to "In-page only" |
291
+
292
+ ---
293
+
294
+ ## Configuration
295
+
296
+ Most configuration is done in the **DSH Web UI → Settings → Plugins → Session Completion Notify** panel (takes effect after saving and clicking "Click to Refresh"). Only "Trigger reason whitelist" and "Skip subagents" are configured in the `config` of the host `cordis.patch.yml`.
297
+
298
+ ### Settings Panel
299
+
300
+ The panel is registered in the official "Settings → Plugins" panel (`settings.plugin.item` keyed slot, key = `session-complete-notify`), replicating the native plugin card style value by value (12px radius, expand/collapse, rotating chevron, footer status bits + discarded ghost + primary-color save button):
301
+
302
+ | Area | Content |
303
+ | --- | --- |
304
+ | Presets | Dropdown to select a built-in or custom preset; "Add" saves the current configuration as a custom preset; the current preset can be "Deleted" |
305
+ | Language | Radio selection among 5 languages; switching instantly re-renders the whole panel |
306
+ | Push mode | Choose one of three: dual channel (system notification + in-page toast, default) / system notification only / in-page only |
307
+ | Notification media | Two sources for the large image: **per-reason upload** — insert the `{image}` token via "+ Insert info → Image" in a template and pick a local file (shown as a chip with a thumbnail in the editor, auto-compressed to 512px wide with a 16:9 center crop matching the notification display ratio, saved per reason); **global image/icon** two upload cards side by side in one row (**icon first**; empty = a rounded "+" tile; click to upload; **image 512×288 with a 16:9 center crop, icon 128×128 with a 1:1 square center crop**; once uploaded the card shows the thumbnail — **click it for a fullscreen preview of the full original image (aspect-ratio-preserving, uncropped)**, the "×" at the top-right removes it). Icon left empty = site default icon, or insert the `{icon}` token in a template for a **per-reason icon** (takes precedence over the global one). Effective on the system-notification channel only (the in-page toast is a text card); the "Send" test buttons apply them too |
308
+ | Title | A collapsible section (**collapsed by default**, click to expand): the **global push title** (shared by all reasons; a Chip editor — info inserted via "+ Insert info" shows as **chip tags**, click a chip to remove; **when the notification is sent, info tokens in the title (duration / usage / error / cache / speed) are replaced with the real values, never shown as raw codes**; when left empty, each reason uses a default title — completed = task completed, errored = task errored, aborted = task aborted, blocked = task blocked, cap = task hit the output cap) plus **per-reason titles** (5 inputs, one per reason; empty = use the global or the language default) |
309
+ | Content | A collapsible section (**collapsed by default**, click to expand). When expanded, each end reason (completed, errored, aborted, blocked, output cap) is a **single-row layout** (reason label + Chip editor + a "+" insert button that turns into "−" while the menu is open + a **paper-plane send button**; the buttons are rectangular and vertically centered): **with an empty template (default preset) the editor shows the default text**; text + inline info chips, insert at the cursor; for `{image}`/`{icon}` chips **click the thumbnail to preview the full image, only the "×" removes it** (prevents accidental removal); other chips are removed by clicking; **after editing, clearing shows the "leave empty to use the default text" placeholder (non-selectable/non-deletable)** |
310
+ | Skip subagent sessions | Checkbox (written into the settings document on save) |
311
+ | Notification permissions | Status shown in real time: granted (green) / not yet granted (with a "Request Authorization" button) / blocked by the browser (with address-bar instructions) / environment unsupported |
312
+ | Per-reason titles | Collapsible area (collapsed by default): one title input per end reason; empty = use the global template or the language default title |
313
+ | Save | Writes to the host settings document (`language` / `templates` / `titleTemplate` / `titleTemplates` / `pushMode`); shows a "Click to Refresh" link after saving |
314
+ | Reset | One-click restore of default values (**the current language is kept**, titles/templates/push mode restore to defaults) and saves immediately |
315
+
316
+ > [!NOTE]
317
+ > The trade-off of "Push mode": `dual` (default) fires both a Windows system notification and an in-page toast; the toast is the fallback channel, guarding against system notifications being silently suppressed by the platform (focus assist, notification banner off). However, **Chromium-shell browsers such as QQ Browser render `Notification` as a "browser built-in in-page push popup"** (a banner at the top/corner of the page, not going through the Windows notification center) — in that case `dual` causes two in-page prompts (the browser built-in popup + the plugin toast). For such browsers, choose "In-page only" (the `Notification` API is no longer called, the browser built-in popup won't appear, and only the plugin's own small toast remains in-page); "System notification only" is ineffective in QQ Browser (it always renders as an in-page popup). The "Send" test button for each reason in the settings panel is likewise affected.
318
+
319
+ > [!NOTE]
320
+ > Whether the system notification (`Notification` API) can pop up is determined jointly by the **browser and how the site is accessed**: Edge/Chrome **auto-block notifications** for "unfamiliar" sites (a "notifications blocked" appears in the address bar) — click the permission icon on the left of the address bar → site settings → notifications → allow to restore; when accessed as a non-secure context like `http://IP`, `Notification` simply does not exist, so switch to "In-page only". The "Notification Permissions" area of the settings panel shows the current state in real time and gives the corresponding action guide (one-click authorization request). In Firefox, when the window is focused, notifications show as in-page banners and only go to the system notification center when unfocused.
321
+
322
+ > [!NOTE]
323
+ > The "Skip subagent sessions" checkbox in the panel saves a boolean into the settings document; `config.skipSubagents` in the host `cordis.patch.yml` is its startup default; if either is true, subagents are skipped.
324
+
325
+ ### Template tokens and placeholders
326
+
327
+ Each end reason has its own template input; **the token is the switch** — the data is shown only when you insert the corresponding info token into the template:
328
+
329
+ | Placeholder | Meaning | Example value |
330
+ | --- | --- | --- |
331
+ | `{title}` | Session title (also usable in the push title template) | `重构登录模块` |
332
+ | `{duration}` | Duration of this turn (timed from `turn/start` to `turn/end`) | `3 25 秒` / `3m25s` |
333
+ | `{usage}` | Token usage (input = uncached + cache reads + cache writes) | `1,240 输入 / 3,560 输出` |
334
+ | `{error}` | Error message (shows `none` when there is no error; single-lined, truncated at 80 characters) | `connection timeout` |
335
+ | `{cache}` | Cache hit rate (per the official projection; empty when no data) | `96.5%` |
336
+ | `{tps}` | Generation speed (per the official projection; empty when no data) | `92 tok/s` |
337
+ | `{image}` | Custom notification-image switch: insert via "+ Insert info → Image" and pick a local file (auto-compressed to 512px), independent per reason; stripped from the rendered body, never written into the session log; removing the token also clears that reason's image data | — |
338
+ | `{icon}` | Custom notification-icon switch: insert via "+ Insert info → Icon" and pick a local file (auto-compressed to 128×128 square), independent per reason; stripped from the rendered body, never written into the session log; takes precedence over the global "Notification icon"; removing the token also clears that reason's icon data | |
339
+ | `{label}` | Deprecated — automatically stripped at render time; old templates remain compatible (the option has been removed from the insert menu) | — |
340
+
341
+ An empty template uses the built-in default copy (automatically includes duration and usage). The collapsible row's `summary` shares the same source as the body (the rendered result is truncated to 120 characters) — users who only look at the collapsible row still see the real title, duration and usage.
342
+
343
+ ### Preset System
344
+
345
+ - **Built-in preset**: only "Default", serving as the baseline.
346
+ - **Custom presets**: stored in `localStorage` (key = `dsh-scn-custom-presets`):
347
+ - After naming, "Add" saves it as a custom preset; afterwards it can be auto-synced with "Modify" or removed with "Delete";
348
+ - **Auto-numbered unnamed presets**: saving directly from "Default / blank" automatically generates `Untitled`, `Untitled 2`, `Untitled 3`… (the number takes the current max + 1);
349
+ - The form shows a "From: xxx · Modified" origin indicator (when loaded from a preset but the content has been changed).
350
+ - **Save syncs**: on save, if the form originates from a custom preset, that preset is updated; otherwise a new one is created or the unnamed-preset numbering continues.
351
+
352
+ ### Host Configuration Options
353
+
354
+ ```yaml
355
+ - insert:
356
+ - id: dsh-session-notify
357
+ name: '@telosmaylx/dsh-session-notify'
358
+ config:
359
+ reasons: [completed, aborted, blocked, error, max-tokens]
360
+ skipSubagents: true
361
+ ```
362
+
363
+ | Field | Type | Default | Description |
364
+ | --- | --- | --- | --- |
365
+ | `reasons` | `string[]` | `[completed, aborted, blocked, error, max-tokens]` | Whitelist of `turn/end` reasons that trigger notifications |
366
+ | `skipSubagents` | `boolean` | `true` | Skip subagent sessions (`origin=subagent` or `delegationDepth>0`) |
367
+
368
+ ---
369
+
370
+ ## How It Works
371
+
372
+ The plugin is split into a **host plane** (Node) and a **client plane** (browser), bridged by the session log (JSONL) and official session projections:
373
+
374
+ ```text
375
+ ┌─────────────────── 宿主平面(lib/index.js,Node)──────────────────┐
376
+
377
+ session/event 火线
378
+ ├─ turn/start → tracker 起表(key: sessionId:turn)
379
+ ├─ assistant/message 累加该轮 token 用量
380
+ └─ turn/end → reason.kind reasons ?
381
+ ├─ 子代理会话?跳过
382
+ ├─ 读官方投影:cache / tps / title
383
+ ├─ 按语言+模板构建通知(summary ≤120 字)
384
+ └─ queueMicrotask 追加系统消息
385
+ (避开 append 重入窗口)
386
+ │ │
387
+ settings.register → 官方「设置 插件」命名空间(失败退避重试) │
388
+ sessionProjections → 注册投影单元(key=session-complete-notify) │
389
+ └──────────────────────────────┬──────────────────────────────────────┘
390
+ user/message (source: plugin, form: notice)
391
+ JSONL 持久化 + 投影推送
392
+ ┌─────────────────── 客户端平面(lib/client.js,浏览器)──────────────┐
393
+
394
+ 会话列表订阅:running true false 边沿 → pushCompletion
395
+ ├─ 取正文:投影 → 事件窗口 notice → 降级(轮询 ≤6s)
396
+ ├─ Web Notification(独立 tag,点击聚焦)
397
+ │ └─ 页内 toast(永远展示,≤3 条,10s 自动消失) │
398
+ │ │
399
+ │ slots.inject('settings.plugin.item') → 设置卡片(预设/语言/模板) │
400
+ └─────────────────────────────────────────────────────────────────────┘
401
+ ```
402
+
403
+ ### Key Design Decisions
404
+
405
+ - **No replay**: only live events are processed; resume and replay do not back-fill historical notifications.
406
+ - **No self-loop**: the plugin appends `user/message` and only listens to `turn/*`; the event types are disjoint.
407
+ - **Zero external imports**: the plugin is loaded from the repository directory by realpath, and `@deepseek-ai/*` cannot be bare-resolved the host plane uses `createRequire` to anchor the profile's shared dependency hub (`.dsh/profiles/node_modules`) for `schemastery` (settings schema) and `zod` (projection schema); UserMessage is constructed manually per the `dsh-llm` contract (`id = crypto.randomUUID()`, deep-freeze done by the adopt-snapshot stage of `session.append`).
408
+ - **Append reentrancy avoidance**: the `session/event` observer callback runs within the publish boundary of the append fired on `turn/end` (dsh-session sets `entry.appending` before dispatch and resets it in `finally`), so a synchronous append would be rejected therefore it is deferred to `queueMicrotask` (the microtask runs only after this synchronous stack, including the `finally` reset).
409
+ - **Effect discipline**: the settings-registration backoff retry timer is wrapped in `ctx.effect()` and returns a `clearTimeout` disposer — if the plugin is uninstalled or hot-reloaded within the retry window, the timer is torn down with the fiber and never registers against a released ctx (in very old environments without the `ctx.effect` API, it degrades to a bare timer plus a ctx-torn-down guard catch).
410
+ - **HMR safe**: the `core.js` import carries `?v=1` cache busting (HMR reload is keyed by URL); if settings registration hits a hot-reload race (duplicate), it automatically backs off and retries (up to 8 times, interval `400ms × attempts`).
411
+ - **Dual-track projection registration**: prefers `ctx.root.get('sessionProjections')` (the instance closest to the host root), falling back to the injection instance when unavailable; if registered only into the injection instance, the client may not read the projection unit and the push body takes the fallback path — best-effort, does not affect in-session system messages.
412
+
413
+ ---
414
+
415
+ ## Project Structure
416
+
417
+ ```text
418
+ dsh-session-notify/
419
+ ├── lib/
420
+ │ ├── index.js # 宿主平面(Node):session/event 订阅 → 系统消息落盘;
421
+ │ │ # settings 命名空间注册(schemastery schema,退避重试);
422
+ │ │ # sessionProjections 投影单元(后台会话推送正文)
423
+ ├── core.js # 纯逻辑层(零依赖,可独立测试):轮次计时与用量聚合、
424
+ # 5 语言文案表、时长/用量/缓存/速度格式化、
425
+ │ │ # 模板渲染({title}{duration}{usage}{error}{cache}{tps})
426
+ └── client.js # 浏览器平面:完成推送(系统通知 + toast)、
427
+ # 设置卡片(Chip 模板编辑器 + 预设系统 + 实时预览)
428
+ ├── scripts/
429
+ │ ├── build.sh # 零构建:仅 node --check 语法校验
430
+ │ ├── verify-notice.mjs # 校验会话日志落盘证据(zstd 多帧逐帧解压)
431
+ │ ├── probe-client.mjs # 探针:客户端装配
432
+ │ ├── probe-client-e2e.mjs # 探针:客户端端到端
433
+ ├── probe-card-render.mjs # 探针:设置卡片渲染
434
+ ├── probe-settings-card.mjs # 探针:设置面板卡片
435
+ ├── probe-settings-check.mjs# 探针:设置面板检查
436
+ └── probe-diag-settings.mjs # 探针:settings 诊断
437
+ ├── cordis.patch.yml # dsh.bundle manifest —— dsh plugin add 自动挂载的凭证
438
+ ├── package.json # dsh.bundle(patch)+ dsh.client(web 注入)双 manifest;
439
+ │ # exports: "." / "./client" / "./core"
440
+ ├── LICENSE # MIT
441
+ └── README.md # 本文档
442
+ ```
443
+
444
+ ---
445
+
446
+ ## Development & Debugging
447
+
448
+ Syntax check (zero-build, the same check as `prepublishOnly`):
449
+
450
+ ```bash
451
+ npm run build
452
+ ```
453
+
454
+ Publishing (runs the `prepublishOnly` syntax check automatically before publishing):
455
+
456
+ ```bash
457
+ npm publish --registry=https://registry.npmjs.org --access public
458
+ ```
459
+
460
+ Offline verification: extracts all plugin-source events and the `turn/end` tail sequence from a session log (when no path is given, automatically selects the newest session under `~/.dsh/sessions`):
461
+
462
+ ```bash
463
+ node scripts/verify-notice.mjs <session.jsonl.zstd>
464
+ ```
465
+
466
+ ### Debugging Entry Points
467
+
468
+ | Entry point | Content |
469
+ | --- | --- |
470
+ | `~/.dsh/session-complete-notify.log` | Host diagnostic log: settings registration, retries and failures, projection registration, append-failure stacks |
471
+ | Browser console `[dsh-session-notify-client]` | Client log: permission state, notification display, settings save |
472
+ | `window.__dsch_notify_debug.readNotice(id)` | Manually read the latest notification body of a given session |
473
+ | `window.__dsch_notify_debug.snapshotDebug(id)` | Session tail node types + notice count + most recent body (first 200 chars) |
474
+
475
+ ---
476
+
477
+ ## FAQ
478
+
479
+ <details>
480
+ <summary><b>Why isn't it auto-mounted after npm install?</b></summary>
481
+
482
+ This is DSH's official design: `npm install` only puts the package into the dependency tree, it does not register the plugin. The only way to auto-mount is `dsh plugin add` — it reads the `dsh.bundle` manifest inside the package (declared by this plugin since 0.1.3) and automatically applies `cordis.patch.yml`. See [Installation](#installation).
483
+
484
+ </details>
485
+
486
+ <details>
487
+ <summary><b>Why is "interrupted" not notified?</b></summary>
488
+
489
+ `interrupted` is an orphan-turn close marker back-filled by the persistence backend after crash recovery; from the user's perspective it is not a "completion" (otherwise restoring a session would flood the screen with false reports). If you really need it, you can add it to `reasons` in the host configuration.
490
+
491
+ </details>
492
+
493
+ <details>
494
+ <summary><b>Do background sessions (with no window open) also get pushed?</b></summary>
495
+
496
+ Yes. The client observes the `running` edges of all sessions from the session list snapshot; the body prefers the host projection — the host maintains a projection unit for all sessions (including background ones), so the pushed body is consistent across sessions. When the projection is unavailable, it falls back to the event window or workspace info.
497
+
498
+ </details>
499
+
500
+ <details>
501
+ <summary><b>Why does it ask me to refresh the page after saving settings?</b></summary>
502
+
503
+ The host reads the settings once when registering the namespace, and the client bundle is assembled at page load. After saving, click "Click to Refresh" so both sides re-read, and the new language and templates take effect.
504
+
505
+ </details>
506
+
507
+ <details>
508
+ <summary><b>Where do the cache hit rate and speed data come from? Why are they sometimes empty?</b></summary>
509
+
510
+ They come from the official `sessionProjections` (`tokenUsage`, `sessionStats`), consistent with the dsh-web-ui status bar. When the host fails to read the projection snapshot or the data is not ready, it falls back to a local usage-aggregation estimate; if there is still no data, the field is left empty (the token shows nothing even if inserted). Additionally, these two appear only when inserted via the `{cache}` and `{tps}` placeholders in a custom template; the default copy does not include them.
511
+
512
+ </details>
513
+
514
+ <details>
515
+ <summary><b>What if the error message in the notification body is too long or has line breaks?</b></summary>
516
+
517
+ Both the summary line (collapsible row) and error details are single-lined and truncated: summary 120 chars, template `{error}` 80 chars, default-copy error details 40 chars; overlong text ends with an ellipsis.
518
+
519
+ </details>
520
+
521
+ <details>
522
+ <summary><b>Can I customize the icon or sound of system notifications?</b></summary>
523
+
524
+ The current version uses the browser's default notification style and does not inject a custom icon or sound; the toast is a fixed dark card. If you need these capabilities, feel free to open an Issue or PR.
525
+
526
+ </details>
527
+
528
+ <details>
529
+ <summary><b>Why can't Edge push system notifications? Why does QQ Browser only show an in-page banner (built-in push popup)?</b></summary>
530
+
531
+ Both are browser behaviors; the plugin cannot force anything:
532
+
533
+ - **Edge / Chrome**: **auto-block notifications** for "unfamiliar" sites (a "notifications blocked" appears in the address bar). Click the permission icon on the left of the address bar → site settings → notifications → allow to restore, after which the Windows notification center works normally. You can also turn off "auto-block" in the browser's notification settings.
534
+ - **Chromium shells like QQ Browser**: always render `Notification` as a **browser built-in in-page push popup** (a banner at the top/corner of the page, not going through the Windows notification center), and there is no system-notification option. How the three push modes actually behave:
535
+ - `Dual channel` browser built-in popup + plugin toast, two in-page prompts;
536
+ - `System notification only` ineffective (QQ Browser always renders it as an in-page popup);
537
+ - `In-page only` the browser built-in popup doesn't appear; only the plugin's small toast remains in-page (recommended).
538
+ The "Send" test button for each reason in the settings panel is likewise rendered by this rule.
539
+ - **Firefox**: when the window is focused, notifications show as in-page banners; they go to the system notification center only when unfocused/minimized; permission must be manually allowed in the address bar.
540
+ - Also note: when accessed via `http://IP` (non-secure context), `Notification` does not exist, so no browser can pop a system notification.
541
+
542
+ The "Notification Permissions" area of the settings panel shows the current state and the corresponding action guide in real time.
543
+
544
+ </details>
545
+
546
+ ---
547
+
548
+ ## Changelog
549
+
550
+ | Version | Date | Changes |
551
+ | --- | --- | --- |
552
+ | **0.1.12** | 2026-08-29 | packaging cleanup: removed the temporary audit script that had been packed into scripts |
553
+ | **0.1.11** | 2026-08-29 | New **custom notification media**: ① the template "+ Insert info" menu gains an **Image** token — insert `{image}` and pick a local file (**auto-compressed to 512px wide with a 16:9 center crop matching the notification display ratio**, shown as a thumbnail chip in the editor), uploaded per reason and stored in the settings document (stripped from the rendered body, never written into the session log); ② **notification image/icon are now upload cards** (empty = a rounded "+" tile, click to upload; **image 512×288 with a 16:9 center crop, icon 128×128 with a 1:1 square center crop**; once uploaded the card shows the thumbnail with a "×" at the top-right to remove it) — the crop guarantees the uploaded image appears complete in the notification card instead of being hard-cropped by the system's display area; ③ the **push-title "+ Insert info"** inserts any info token (session title / duration / usage / cache hit / speed), not just the title; the "Notification media" area in the settings panel now shows a **processing guide** (crop ratios) and a **thumbnail preview** after upload; ④ **per-reason notification icons** (insert the `{icon}` token in a template + local upload, 128×128 square, takes precedence over the global icon); ⑤ **removing an `{image}`/`{icon}` token clears that reason's image/icon data**; ⑥ **layout polish: "Body templates × 5" is now a collapsible section (collapsed by default to keep the panel compact, with a customized-count hint in the header, click to expand)**; ⑦ **layout polish ②: the notification image/icon upload cards sit side by side in one row; the push title and per-reason titles merge into a "Title" collapsible section (collapsed by default); collapse indicators now use triangle icons (the "Expand/Collapse" texts removed, reducing i18n burden)**; ⑧ **interaction polish: the push title is now a Chip editor (inserted info shows as chip tags instead of raw `{title}` codes); the image/icon cards are reordered (icon first); "Body templates × 5" renamed to "Content"; image/icon thumbnails open a fullscreen lightbox preview on click, and only the "×" removes a tag (no accidental removal)**; ⑨ **previews unified to the full original: upload now stores both the cropped version (for the notification) and an aspect-preserving full version (1024px, used by the lightbox), so card and chip previews both show the uncropped image; the collapse triangle icons are enlarged**; ⑩ **title info-token fix: `{duration}` `{usage}` `{error}` `{cache}` `{tps}` in the notification title are replaced with the session's real values, never leaking raw codes (chips in the editor, real data when sent); "Content" rows are now single-line (reason label + editor + a "+/−" insert button + a paper-plane send button)**; ⑪ **details: the live preview under each Content row is removed (more compact); the Content editors use a tighter padding (compact); the "+/−" and paper-plane buttons now have rectangular borders and are vertically centered**; ⑫ **deletion-experience fix: clearing a Content editor no longer restores the default text (it shows the placeholder hint instead), and the caret returns to the end after clearing — holding Backspace now deletes continuously; "+/−" now use SVG stroke icons (exactly centered in the button); the paper-plane icon is rotated 30° counterclockwise**; ⑬ **placeholder & default text: with an empty template (default preset) the editor directly shows the default text (what you see is what gets pushed); after editing, clearing shows the "leave empty to use the default text" placeholder — implemented with a CSS pseudo-element (behaves like a native input placeholder: non-selectable, non-deletable), replacing the previously copyable placeholder text**; ⑭ **details ②: the push-title placeholder now reads "Generic push title; leave empty to use the default title (per-reason titles below take precedence)"; clicking an empty editor moves the caret to the very start (typed text begins at the leftmost position, like a native placeholder); the paper-plane icon is rotated another 30° counterclockwise (60° total); the Content reason labels are now auto-width (editor hugs the label text); editor heights are unified (box-sizing border-box + min-height 38px, matching the plain inputs)**; ⑮ **details ③: the reason labels are back to a fixed 70px width (Content and per-reason titles aligned); Backspace/Delete on a chip is now handled manually so the caret stays at the deletion point (no more jumping to the start); the send button icon is now a bell (push/notification semantics); custom presets now also save the notification image/icon (including the full preview versions), the default preset stays empty**; ⑯ **details ④: the image/icon upload cards are now equal height (64px; the 16:9 image is 114px wide); the reason labels shrink from 70px to 60px; the send button icon changed from a bell to a rightward push arrow; the "Title" and "Content" collapsible headers are bold**; ⑰ **default text & labels: max-tokens is now uniformly shown as "Cap" (label / default title / placeholder, synced in all 5 languages); the per-reason default body is now "Session "{title}" completed. Click to view." (5 languages, editor hint and host rendering in sync); the per-reason title rows each gain a "+" insert button (like the push title: info tokens only, no image/icon, inserted at the caret)**; ⑱ **chips & preset management: the per-reason titles are now Chip editors (inserted info shows as chip tags instead of raw {title} codes; {image}{icon} are blocked — typed literally they stay as text and are stripped when sent); custom presets gain a "Rename" action (appears when a custom preset is selected; the dropdown and the active preset sync after renaming)**; ⑲ **details ⑤: Chip editor text is now vertically centered (line-height and chips unified to 20px, 9px vertical padding — single-line text sits centered in the 38px box); the "Title" and "Content" fold bars now have a filled background for contrast (rounded tinted bar as header, expanded body indented with a left border line); full audit of all 86 i18n keys across the 5 languages (all translated — tweaked zh-TW "Rename" and ko "Discard")**; the "Send" test buttons apply them too; effective on the system-notification channel only (the in-page toast is a text card); **details ⑥: the filled background of the "Title" / "Content" fold headers is removed; the placeholder text color is now rgba(127,127,127,0.5) (lighter, no longer affected by the theme variable)**; the "Send" test buttons apply them too; effective on the system-notification channel only (the in-page toast is a text card); ㉑ **fix: the divider lines between the per-reason rows under Content are removed; when Content is expanded, the line under the header is now present and spans the full width, matching Title; Backspace/Delete on a chip no longer jumps the caret to the start (inside the element container the chip is located by child-node index, covering the caret-right-after-chip and editor-end cases); renaming an unnamed preset upgrades it to a named preset (new id) so the dropdown shows the new name immediately**; the "Send" test buttons apply them too; effective on the system-notification channel only (the in-page toast is a text card); ㉒ **fix: the placeholder hint no longer lingers after typing (the placeholder CSS selector is now aligned with the state attribute — it disappears as you type and text starts at the left); uploading/removing an image or icon keeps the caret in place instead of jumping to the start (the caret offset is saved before the DOM rebuild and restored after); the preset dropdown no longer shows a white focus/selected outline**; the "Send" test buttons apply them too; effective on the system-notification channel only (the in-page toast is a text card); ㉓ **fix: deleting a chip (including image/icon) no longer moves or hides the caret, so backspace-deleting is continuous — the rebuild triggered by image-data changes is now on-demand (skipped when the DOM no longer contains that media chip and the template has no such tag, keeping the caret at the deletion point; only uploads/replacements that need a thumbnail refresh trigger a rebuild), and the caret-offset math was fixed (a chip counts as exactly one position, never walking into its inner text)**; the "Send" test buttons apply them too; effective on the system-notification channel only (the in-page toast is a text card) |
554
+ | **0.1.10** | 2026-08-29 | "Push title" changed to a native input (native placeholder behavior: not copyable, disappears when typing, restores when cleared; "+ Session Title" inserts `{title}` at the cursor); docs add the QQ Browser built-in push popup explanation (actual behavior of the three push modes + the send button tests follow the same rule) |
555
+ | **0.1.9** | 2026-08-29 | Push titles now support **per-reason customization** (collapsible UI, collapsed by default to stay lean; when empty, each reason uses a differentiated default title: task completed / task errored / task aborted / task blocked / task hit the output cap, in 5 languages); the projection upgraded to an object (kind/text/title) carrying the host-rendered title; the reset button **keeps the current language**; the default copy embeds the "session title" token (Session "{title}" completed, auto-falls back when there's no title); settings-panel template previews sync; "+ Insert Info" no longer auto-collapses after inserting a token; deleting a custom preset currently in use automatically switches back to the default; template preview fixed (doesn't hide on click, hides only when typing, restores when cleared); each reason gains a "Send" button (one-click test notification rendered with the current template) |
556
+ | **0.1.8** | 2026-08-29 | Default push title changed to "Task completed" (`{title}` can still reference the session title); default copy is differentiated per end reason (completed = compact parentheses / aborted·blocked = separate sentences / errored = error first / cap = with a suggestion, in 5 languages); the settings panel gains a "Reset" button for one-click restore of defaults |
557
+ | **0.1.7** | 2026-08-29 | Fix the 0.1.6 settings-card crash: `notificationPermissionRow`/`requestPermissionNow` referenced Card-component-internal state (out of scope), causing a render ReferenceError and the whole settings card to disappear; changed to self-contained + callback params |
558
+ | **0.1.6** | 2026-08-29 | The settings panel gains a "Notification permissions" status area (grant state in real time + one-click request-authorization button + address-bar guide when blocked); authorization changed to **requesting within a user gesture** (Chromium ignores non-gesture automatic requests; the typical Edge auto-block-for-unfamiliar-sites scenario is solved); FAQ adds browser-difference explanations |
559
+ | **0.1.5** | 2026-08-29 | New "Push mode" setting (dual channel / system notification only / in-page only): solves the double-prompt caused by Chromium shells like QQ Browser rendering `Notification` as an in-page banner; `pushMode` added to the settings schema and settings panel |
560
+ | **0.1.4** | 2026-08-28 | Complete uninstall support added: `dispose` lifecycle teardown (host sets an uninstalled flag to suppress pending microtask appends; client cleans up the body-polling timer, the `__dsch_notify_debug` hook, and the toast container); uninstall docs and FAQ updated accordingly |
561
+ | **0.1.3** | 2026-08-28 | Declare the official `dsh.bundle` manifest (`dsh plugin add` auto-mounts with a single command); settings retry timer wrapped in `ctx.effect()` (Cordis effect discipline); installation docs rearranged |
562
+ | 0.1.2 | 2026-08-27 | Package renamed to the `@telosmaylx` scope (npm username scope) |
563
+ | 0.1.1 | 2026-08-27 | Documented the GitHub and npm installation methods |
564
+ | 0.1.0 | 2026-08-26 | Initial version: in-session system messages + browser push + official settings panel |
565
+
566
+ ---
567
+
568
+ ## Contributing
569
+
570
+ Issues and PRs are welcome:
571
+
572
+ 1. Fork the repository and create a new branch (`feat/xxx`)
573
+ 2. After changes, run `npm run build` for the syntax check
574
+ 3. Submit a PR, describing the motivation and how you verified it
575
+
576
+ Please follow the [Cordis development tutorial](https://deepseek-harness.github.io/deepseek-harness/develop/cordis-tutorial) discipline before submitting:
577
+
578
+ - Resources outside Cordis (timers, subscriptions, watchers) must be wrapped in `ctx.effect()` and return a disposer;
579
+ - Configuration items must have an explicit `id` to prevent edit drift;
580
+ - A plugin must declare the `dsh.bundle` manifest to be recognized and installed by `dsh plugin add`.
581
+
582
+ ---
583
+
584
+ ## Related Links
585
+
586
+ - [awesome-dsh-plugin](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin) — A curated list of DSH plugins (submission rule: `dsh.bundle` is the only credential for installation)
587
+ - [Cordis development tutorial](https://deepseek-harness.github.io/deepseek-harness/develop/cordis-tutorial) — The full plugin-development process (chapters 01–07)
588
+ - [npm package homepage](https://www.npmjs.com/package/@telosmaylx/dsh-session-notify)
589
+ - [GitHub repository](https://github.com/TelosmaYLX/dsh-session-notify)
590
+
591
+ ---
592
+
593
+ ## License
594
+
595
+ [MIT](./LICENSE) © dsh-session-notify contributors