DeepSeek Harness Plugin

TelosmaYLX/dsh-session-notify

Stars ★ 2 Downloads (30d) 1,846 Category Notifications & Integrations Added 2026-08-29 npm @telosmaylx/dsh-session-notify

Notifies you with a browser notification and toast whenever a session turn ends, with 5-language templates and custom presets.

Install

# from npm (prebuilt)

dsh plugin --profile web add @telosmaylx/dsh-session-notify

# from GitHub (first run asks for allowBuilds approval — follow the hint, retry)

dsh plugin --profile web add github:TelosmaYLX/dsh-session-notify

Any plugin you install runs third-party code with your own permissions — it can read your files, use your credentials, and reach the network, and tool approvals don’t sandbox it. GitHub-sourced plugins also run build scripts at install time — pnpm blocks those until you allow them, so an install can stop with ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED or ERR_PNPM_IGNORED_BUILDS; dsh prints the exact key to add under allowBuilds in your profile’s pnpm-workspace.yaml, and the install works on the next run. Allowing a build is a trust decision: only install sources you trust, and pin a commit (github:owner/repo#sha).

README

dsh-session-notify

简体中文 · English · 繁體中文 · 日本語 · 한국어

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.

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 (channels are configurable separately for the unfocused and the focused window, each with an "Off" option); when the AI asks you a question or requests approval, it also alerts you immediately — no need to watch the session page. Built-in 5 languages, 4 style presets (kaomoji / Felyne / Neko girl / DeepSeek-chan), 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.


Table of Contents


Features

Three-Channel Notifications, None Missed

Channel Form Description
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.
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.
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.

[!NOTE] The latter two channels are routed by the window focus state: the settings panel has one dropdown for "Unfocused" and one for "Focused", each offering System + in-page / System only / In-page only / Off. The window counts as unfocused when document.visibilityState === 'hidden' or !document.hasFocus() — that is, when the tab is switched away, the window is minimized, or you click elsewhere, the "Unfocused" route applies.

Full Coverage of Background Sessions

  • 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.
  • 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).

Instant Question Alerts

  • When the AI calls ask_user_question to ask you something, the host immediately writes the "question title + body" into a dedicated projection unit (key = session-complete-notify-question), and the client polls it in real time and pops an alert — even if you are looking at another page, you won't miss the question.
  • The question copy is fully customizable: the title follows the "per-reason title → global title → default title" chain, the body supports the {question} placeholder (injected with the AI's actual question), and the {image} / {icon} / {audio} media switches work as well.

Instant Approval Alerts

  • Alerts immediately when a session requests a permission approval (approval/asked) and clears on approval/decided — you won't miss it while looking at another tab.
  • Three fallback signals: the harness-native pendingInteractions (most accurate when the host provides it) → the host approval projection (key = session-complete-notify-approval, title and body rendered by the host in the current language) → the session list snapshot's pendingInteraction === 'approval'.
  • The copy carries only the tool name and an optional reason (e.g. "The session is waiting for approval of Bash."), never command arguments or other sensitive content; push channel and media settings apply as well.

Customizable Down to Every Sentence

  • 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).
  • 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).
  • Preset system: a built-in "Default" baseline plus 4 one-click style presets (kaomoji / Felyne / Neko girl / DeepSeek-chan — a full set of stylized copy for the title, all 5 end reasons and the question); 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.
  • Push title template: when left empty, each reason uses a default title (completed = task completed / errored = task errored / … / question = AI is asking you a question); {title} references the session title.

Same Source as the Official Metrics

  • Cache hit rate comes from the official tokenUsage projection: cache reads / (uncached input + cache reads + cache writes).
  • Generation speed comes from the official sessionStats projection: output tokens ÷ decode time.
  • 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.

[!NOTE] 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 does NOT include duration and usage (insert the placeholders in a custom template to show them).

Engineering Quality

  • Responds only to live events: resume and replay do not replay old notifications; loading a session does not spam the screen.
  • Self-immune to loops: the message type the plugin appends (user/message) is disjoint from its own listen target (turn/*).
  • 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.
  • 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.
  • 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.
  • Zero-dependency self-test: npm run selftest runs scripts/selftest.mjs — host schema and sanitize, client pure logic and wiring, settings-panel render smoke, alert-sound playback chain, and README × 5 consistency, all as executable assertions; must be run before releasing.

Requirements

Dependency Requirement
DSH (DeepSeek Harness) 0.1.x and 0.2.0-rc.2+ (verified on 0.2.0-rc.2). Web profile deployment; the official base bundle ships @deepseek-ai/dsh-settings and session projections, and the plugin adapts to both settings contracts automatically
cordis >=4.0.0-rc <5 (peer dependency, provided by the host)
Node.js >=22 (host side)
Browser System notifications when Web Notification is supported; toast falls back when unsupported, permission denied or silently suppressed

The two host generations use different settings contracts; the plugin adapts automatically, with no configuration:

Environment Settings namespace Settings panel entry
dsh 0.1.x Chosen by the plugin: session-complete-notify (ctx.settings.register) Host settingsScope → settings.plugin.item slot
dsh 0.2.0-rc.2+ Profile entry id: dsh-session-notify Host configForms → plugins.bundle.config / plugins.row.config / settings.plugins.tab slots

The 0.1.x code path is unchanged, but the latest round of changes was only regression-tested on 0.2.0-rc.2. On older hosts with schemastery < 3.18.3 the plugin skips volatile() (0.1.x does not need it).


Installation

[!WARNING] 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.

Method 1: dsh plugin add (recommended)

Installing the package also automatically applies cordis.patch.yml, mounting the plugin into the profile assembly (host event subscription + client boot graph injection).

dsh plugin --profile web add @telosmaylx/dsh-session-notify

Method 2: Install from the GitHub repository

dsh plugin add github:TelosmaYLX/dsh-session-notify

You can also run it inside a DSH Web GUI session:

dev_install_package github=TelosmaYLX/dsh-session-notify

Method 3: Hot-mount a local directory (for development)

Replace the path with your clone directory and run it inside a DSH Web GUI session:

dev_install_package dir=/你的/克隆目录/dsh-session-notify

Method 4: Manual install of the npm package

First pack it:

npm pack @telosmaylx/dsh-session-notify

After extracting, install from the specified directory (run inside a DSH Web GUI session):

dev_install_package dir=/解压/目录/package

Method 5: Manual cordis patch (no installer required)

Append to ~/.dsh/profiles/web/cordis.patch.yml:

- insert:
    - id: dsh-session-notify
      name: '@telosmaylx/dsh-session-notify'
      config: {}

[!IMPORTANT] 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.

Uninstall

A single command removes the plugin and its mounting (automatically removes the insert entry from cordis.patch.yml):

dsh plugin --profile web remove @telosmaylx/dsh-session-notify

[!NOTE] 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.

What Is Automatically Cleaned Up on Uninstall

The plugin implements complete lifecycle teardown (Cordis effect discipline); on uninstall/disable/HMR hot reload:

Plane Automatically released resources
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
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

Data Retained After Uninstall

  • Settings configuration (language, copy templates) remains in the settings document and is automatically restored after reinstalling;
  • Custom presets are stored in the browser localStorage (dsh-scn-custom-presets), still present after reinstalling;
  • 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).

Quick Start

  1. Install by any of the methods above and refresh the page.
  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.
  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.
  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.

Right after installation, a collapsible hint line like this appears in the session log:

会话「重构登录模块」已完成(用时 1 分 12 秒,消耗 1,240 输入 / 3,560 输出)。

The default copy embeds the session-title token ({title}) after "Session"; when the session has no title, it automatically falls back to "Session completed".


Notification Behavior

Trigger Conditions

When each conversation turn ends (turn/end), the end reason is checked; hitting the whitelist triggers a notification:

End reason Meaning Default
completed Session completed normally Notify
aborted Session aborted Notify
blocked Session blocked Notify
error Session errored (with error details, truncated when too long) Notify
max-tokens Reached the output token cap Notify
interrupted Interrupted (an orphan-turn close marker back-filled by the persistence backend after crash recovery) No notification (configurable)

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.

Question alerts are a separate channel and do NOT go through the whitelist above: when the AI calls ask_user_question and waits for your answer (a tool/call event), the alert fires immediately, and it becomes invalid once the tool/result returns. Questions are not written to the session log — they only pop a notification.

Approvals are another independent channel: the alert fires immediately when a session requests a permission approval (approval/asked) and clears on approval/decided. It writes no session-log entry either — just a notification; the title and body carry only the tool name and an optional reason, never command arguments.

Where Does the Push Body Come From

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):

  1. Host projection (key = session-complete-notify) — available for every session; background sessions get the full text too;
  2. The notice node in the session event window (kind=context + form=notice) — for the session being viewed, available immediately after persistence;
  3. Fallback — "See the in-session system message for details" + workspace info (last segment of cwd).

Question alerts take their body from the host projection as well (key = session-complete-notify-question, with the title and body already rendered by the host); on older hosts without that projection, the client assembles the title and {question} text itself as a fallback.

Approval alerts resolve three signal sources in order of availability: the harness-native pendingInteractions → the host approval projection (key = session-complete-notify-approval) → the session list snapshot's pendingInteraction field; whichever arrives first alerts, and each approval pushes only once.

Notification Examples

All of the following are actually generated by buildNotice in lib/core.js. The default copy is uniform — "Session "{title}" . Click to view." (wording varies per end reason; duration and usage are not included):

English default copy (session title in double quotes):

Session "重构登录模块" completed. Click to view.   ← completed
Session "重构登录模块" aborted. Click to view.     ← aborted
Session "重构登录模块" blocked. Click to view.     ← blocked
Session "重构登录模块" hit the cap. Click to view. ← hit the cap
Session "重构登录模块" failed. Click to view.      ← errored

When the session has no title (titleValue empty), it falls back to "Session completed. Click to view."; duration / usage / cache hit / speed only appear in custom templates via the {duration} {usage} {cache} {tps} placeholders.

Custom template (edited in the settings panel; this example uses all info slots):

{title} 干完了!用时 {duration},消耗 {usage},缓存命中 {cache},速度 {tps}

Rendered result:

重构登录模块 干完了!用时 3 分 25 秒,消耗 103,600 输入 / 35,600 输出,缓存命中 96.5%,速度 92 tok/s

The same event in the five languages:

会话「重构登录模块」已完成(用时 3 分 25 秒,消耗 1,240 输入 / 3,560 输出)。
會話「重構登入模組」已完成(用時 3 分 25 秒,消耗 1,240 輸入 / 3,560 輸出)。
Session "重构登录模块" completed (took 3m25s, used 1,240 in / 3,560 out).
セッション「重构登录模块」完了(所要 3 分 25 秒、消費 1,240 入力 / 3,560 出力)。
세션「重构登录模块」 완료(소요 3분 25초, 소모 1,240 입력 / 3,560 출력)。

Notification Permissions

Permission state Behavior
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)
granted Sends system notifications over the channel chosen for "Unfocused" / "Focused" (independent tags, never overwriting each other; "Off" leaves that route silent)
denied (blocked by the browser) Toast only; the settings panel shows address-bar instructions (permission icon → site settings → notifications → allow)
undefined (non-secure context / unsupported) Toast only; recommends switching to "In-page only"

Configuration

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 the "Trigger reason whitelist" is configured in the config of the host cordis.patch.yml (skipping subagents is controlled by a checkbox in the panel).

Settings Panel

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):

Area Content
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"
Language Radio selection among 5 languages; switching instantly re-renders the whole panel
Unfocused Choose one of four: Off (fully silent in that state) / dual channel (system notification + in-page toast, default) / system notification only / in-page only — applies while the window is unfocused (tab switched away, minimized, focus lost)
Focused Choose one of four: same options (dual channel by default) — applies while the window is focused. The two routes are independent and can be combined freely, e.g. "system notification when unfocused + no interruption when focused"
Audio A collapsible section (expanded by default, click the ▾ in the header to collapse): the alert sound's volume and duration. When expanded it holds the "Volume" label and the slider on the same line (the "Volume" label sits in front of the slider, and to the right of the slider come the percentage and the "Test / Stop" button: 0–100%, stored internally as 0–1, default 60%; the preview plays at the current volume — drag the slider and click once to hear the difference; while playing the button turns into "Stop" and clicking it again stops playback; the button is disabled when no alert sound is configured) and the "Duration" row (same row order: label → slider → number box → unit; the slider spans 0–15 s with 0.1 s steps and dragging changes it directly, while the number box still accepts any number of seconds; 0 = no limit by default, and once it is set to N seconds, if the audio is still playing when the limit is reached → 0.5 s volume fade-out, then stop, while audio shorter than the limit just plays to its end; it only affects playback — uploaded audio is stored as-is and never trimmed). Both are written into the settings document as volume / maxDuration. A third row is the global alert sound (an "Add audio" button while unset; once set, a type/size summary such as WAV · 5 KB plus Replace / Test / Clear), written as globalAudio — it plays when the content specifies no audio for that state (priority: that state's own audio > global alert sound; nothing plays when neither is set)
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
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, question = AI is asking you a question) plus per-reason titles (6 inputs, one per reason, each with a "+" insert button for info tokens — including "Question", no images/icons; inserted at the caret; takes precedence over the global title; empty = use the global or the language default)
Content A collapsible section (collapsed by default, click to expand). When expanded, each reason (completed, errored, aborted, blocked, output cap, question) 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), {audio} chips click the play icon to preview the sound — while playing it becomes a pause icon and clicking it again stops it — and only the "×" removes the tag (prevents accidental removal too; deleting the tag only clears that reason's audio data, the body text stays intact); other chips are removed by clicking; after editing, clearing shows the "leave empty to use the default text" placeholder (non-selectable/non-deletable); the question row's default text is "AI is asking: {question}", where {question} is replaced by the AI's actual question when sent (the insert menu offers a "Question" token too, with the same interaction as the other tokens)
Skip subagent sessions Checkbox (written into the settings document on save)
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
Per-reason titles Collapsible area (collapsed by default): one title input per end reason; empty = use the global template or the language default title
Save Writes to the host settings document (language / templates / titleTemplate / titleTemplates / pushModeBlur / pushModeFocus / skipSubagents / audios / globalAudio / volume / maxDuration); only the fields that actually changed are written (everything else is left untouched), and the buttons are disabled while saving; shows a "Click to Refresh" link after saving
Reset One-click restore of default values (the current language is kept, titles/templates/unfocused & focused channels/alert sounds/duration restore to defaults) and saves immediately

[!NOTE] The trade-off of "Unfocused" / "Focused": 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). The two routes are independent, so you can set e.g. system notification when unfocused and complete silence when focused. Existing configurations are unaffected — if the settings document has no pushModeBlur / pushModeFocus, both routes fall back to the legacy single pushMode value. 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 — the test notification prefers the "Focused" route, falls back to the "Unfocused" route when that is "Off", and falls back to dual when both are off, so the preview always gives feedback.

[!NOTE] 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.

[!NOTE] 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.

Template tokens and placeholders

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:

Placeholder Meaning Example value
{title} Session title (also usable in the push title template) 重构登录模块
{duration} Duration of this turn (timed from turn/start to turn/end) 3 分 25 秒 / 3m25s
{usage} Token usage (input = uncached + cache reads + cache writes) 1,240 输入 / 3,560 输出
{error} Error message (shows none when there is no error; single-lined, truncated at 80 characters) connection timeout
{cache} Cache hit rate (per the official projection; empty when no data) 96.5%
{tps} Generation speed (per the official projection; empty when no data) 92 tok/s
{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 —
{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 —
{audio} Custom alert-sound switch: insert via "+ Insert info → Audio" and pick a local audio file (mp3/wav/ogg… read as-is, no compression and no trimming; asks for confirmation above 512 KB), independent per reason; empty by default (no sound unless you set one), with the volume and the duration controlled globally from the panel's "Audio" fold (the volume slider / the "Duration" row — slider 0–15 s, step 0.1 plus a number box, 0 = no limit by default, 0.5 s fade when exceeded), and previews play at the current volume; click the play icon on the tag to preview the sound — while playing it becomes a pause icon and clicking it again stops playback; only the "×" on the tag removes it (deleting it only clears that reason's audio data and the tag, the body text is unaffected); stripped from the rendered body, never written into the session log; only one {audio} per reason (the insert button is disabled once it is present, and duplicate tags are stripped on save). Whether it plays is decided by the saved template (takes effect after Save + refresh, like every other panel setting) — the sound plays only if the tag is present and a file is chosen for that state; otherwise the global alert sound from the Audio section plays (nothing plays when neither is set) —
{question} Question-row-only placeholder: replaced with the AI's actual question text when sent; integrated with the "+ Insert info" menu (pick the "Question" token, or just type it — both render as a chip); only available in the question channel, inserted in other reason rows it is replaced with an empty string (no literal leak) Should I continue generating the report?
{label} Deprecated — automatically stripped at render time; old templates remain compatible (the option has been removed from the insert menu) —

An empty template uses the built-in default copy (the uniform "Session "{title}" . Click to view." phrasing; duration and usage are not included). 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.

Preset System

  • Built-in preset: only "Default", serving as the baseline.
  • Custom presets: stored in localStorage (key = dsh-scn-custom-presets):
    • After naming, "Add" saves it as a custom preset; afterwards it can be auto-synced with "Modify" or removed with "Delete";
    • Auto-numbered unnamed presets: saving directly from "Default / blank" automatically generates Untitled, Untitled 2, Untitled 3… (the number takes the current max + 1);
    • The form shows a "From: xxx · Modified" origin indicator (when loaded from a preset but the content has been changed).
  • 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.

Host Configuration Options

- insert:
    - id: dsh-session-notify
      name: '@telosmaylx/dsh-session-notify'
      config:
        reasons: [completed, aborted, blocked, error, max-tokens]
        skipSubagents: true
Field Type Default Description
reasons string[] [completed, aborted, blocked, error, max-tokens] Whitelist of turn/end reasons that trigger notifications
skipSubagents boolean true Skip subagent sessions (origin=subagent or delegationDepth>0)

How It Works

The plugin is split into a host plane (Node) and a client plane (browser), bridged by the session log (JSONL) and official session projections:

┌─────────────────── 宿主平面(lib/index.js,Node)──────────────────┐
│                                                                     │
│  session/event 火线                                                 │
│   ├─ turn/start        → tracker 起表(key: sessionId:turn)        │
│   ├─ assistant/message → 累加该轮 token 用量                        │
│   ├─ tool/call         → ask_user_question?写提问投影(标题+正文) │
│   └─ turn/end          → reason.kind ∈ reasons ?                   │
│                            ├─ 子代理会话?跳过                       │
│                            ├─ 读官方投影:cache / tps / title        │
│                            ├─ 按语言+模板构建通知(summary ≤120 字) │
│                            └─ queueMicrotask 追加系统消息            │
│                                 (避开 append 重入窗口)             │
│                                                                     │
│  settings.register   → 官方「设置 → 插件」命名空间(失败退避重试)   │
│  sessionProjections  → 注册投影单元(key=session-complete-notify)  │
│                        + 提问投影(key=session-complete-notify-     │
│                          question,等待回答期间持续推送)            │
└──────────────────────────────┬──────────────────────────────────────┘
                               │ user/message (source: plugin, form: notice)
                               ▼  JSONL 持久化 + 投影推送
┌─────────────────── 客户端平面(lib/client.js,浏览器)──────────────┐
│                                                                     │
│  会话列表订阅:running true → false 边沿 → pushCompletion            │
│   ├─ 取正文:投影 → 事件窗口 notice → 降级(轮询 ≤6s)               │
│   ├─ Web Notification(独立 tag,点击聚焦)                          │
│   └─ 页内 toast(永远展示,≤3 条,10s 自动消失)                     │
│  提问投影轮询(key=session-complete-notify-question):              │
│   有值 → 立即弹提醒(标题+正文),无值清空                            │
│                                                                     │
│  slots.inject('settings.plugin.item') → 设置卡片(预设/语言/模板)   │
└─────────────────────────────────────────────────────────────────────┘

Key Design Decisions

  • No replay: only live events are processed; resume and replay do not back-fill historical notifications.
  • No self-loop: the plugin appends user/message and only listens to turn/*; the event types are disjoint.
  • 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).
  • 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).
  • 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).
  • 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).
  • 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.

Project Structure

dsh-session-notify/
├── lib/
│   ├── index.js      # 宿主平面(Node):session/event 订阅 → 系统消息落盘;
│   │                 #   settings 命名空间注册(schemastery schema,退避重试);
│   │                 #   sessionProjections 投影单元(后台会话推送正文)
│   ├── core.js       # 纯逻辑层(零依赖,可独立测试):轮次计时与用量聚合、
│   │                 #   5 语言文案表、时长/用量/缓存/速度格式化、
│   │                 #   模板渲染({title}{duration}{usage}{error}{cache}{tps})、
│   │                 #   提问正文构建(buildQuestionBody,{question} + 媒体剥除)
│   └── client.js     # 浏览器平面:完成推送(系统通知 + toast)、
│                     #   设置卡片(Chip 模板编辑器 + 预设系统 + 实时预览)
├── scripts/
│   ├── build.sh                # 零构建:仅 node --check 语法校验
│   ├── selftest.mjs            # 零依赖自检(发布前必跑):schema / 纯逻辑 / 渲染冒烟 / 播放链路 / README 一致性
│   ├── verify-notice.mjs       # 校验会话日志落盘证据(zstd 多帧逐帧解压)
│   ├── probe-client.mjs        # 探针:客户端装配
│   ├── probe-client-e2e.mjs    # 探针:客户端端到端
│   ├── probe-card-render.mjs   # 探针:设置卡片渲染
│   ├── probe-settings-card.mjs # 探针:设置面板卡片
│   ├── probe-settings-check.mjs# 探针:设置面板检查
│   └── probe-diag-settings.mjs # 探针:settings 诊断
├── cordis.patch.yml  # dsh.bundle manifest —— dsh plugin add 自动挂载的凭证
├── package.json      # dsh.bundle(patch)+ dsh.client(web 注入)双 manifest;
│                     #   exports: "." / "./client" / "./core"
├── LICENSE           # MIT
└── README.md         # 本文档

Development & Debugging

Syntax check (zero-build, the same check as prepublishOnly):

npm run build

Self-test (must be run before releasing): host schema / client pure logic and wiring / settings-panel render smoke / alert-sound playback chain / README × 5 consistency:

npm run selftest

Publishing (runs the prepublishOnly syntax check automatically before publishing):

npm publish --registry=https://registry.npmjs.org --access public

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):

node scripts/verify-notice.mjs <session.jsonl.zstd>

Debugging Entry Points

Entry point Content
~/.dsh/session-complete-notify.log Host diagnostic log: settings registration, retries and failures, projection registration, append-failure stacks
Browser console [dsh-session-notify-client] Client log: permission state, notification display, settings save
window.__dsch_notify_debug.readNotice(id) Manually read the latest notification body of a given session
window.__dsch_notify_debug.snapshotDebug(id) Session tail node types + notice count + most recent body (first 200 chars)
window.__dsch_notify_debug.alertAudio(action, src, volume, maxSeconds) Alert-sound probe: play / stop / toggle / state, returns the currently playing source ('' = not playing)

FAQ

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.

Yes. When the AI calls ask_user_question and waits for your answer, the host immediately writes the "question title + body" into a dedicated projection (key = session-complete-notify-question), and the client pops an alert as soon as it polls the value — even if you are looking at another page, you won't miss it. The question copy is as customizable as the completion notifications: the "Title / Content" sections of the settings panel each have a "Question" row, the body supports the {question} placeholder (injected with the AI's actual question), and the {image} / {icon} / {audio} media switches work too. Once you answer (tool/result), the alert is invalidated and does not linger.

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.

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.

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.

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.

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.

The icon is customizable: in the settings panel's "Notification image" section you can upload a hero image and an icon (global), or insert the {icon} tag in a per-reason template to assign a per-reason icon (takes precedence over the global one). The sound is customizable as well: in a state's "Content" row, click "+ Insert info → Audio", insert the {audio} tag and pick a local audio file (one per state, empty by default — no sound unless you set one); the volume and the duration are set globally in the panel's "Audio" fold (the "Volume" label sits in front of the slider — drag the slider and click "Test" to hear the effect right away, since the preview follows the current volume; the "Duration" row is a slider (0–15 s, step 0.1) plus a number box, 0 = no limit by default, fading out over 0.5 s when exceeded; uploaded audio is stored as-is and never trimmed), and the sound is played once at that volume when a notification or an in-page toast is sent; the play icon on an audio tag previews the sound and toggles playback (while playing it becomes a pause icon, and clicking it again stops it); the toast itself is a fixed dark card. For other needs, feel free to open an Issue or PR.

Both are browser behaviors; the plugin cannot force anything:

  • 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.
  • 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. The "Unfocused" and "Focused" dropdowns behave identically here:
    • System + in-page → browser built-in popup + plugin toast, two in-page prompts;
    • System only → ineffective (QQ Browser always renders it as an in-page popup);
    • In-page only → the browser built-in popup doesn't appear; only the plugin's small toast remains in-page (recommended);
    • Off → that state is fully silent. The "Send" test button for each reason in the settings panel is likewise rendered by this rule.
  • 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.
  • Also note: when accessed via http://IP (non-secure context), Notification

…

Content from the project README on GitHub ↗

Links

More in this category

View the whole category →

Community comments

Comments are public GitHub Discussions. Loading them connects to GitHub and Giscus; a GitHub account is required to post.