← Back to home@x102201

dsh-helper-plugin-notify-away

dsh-helper plugin: Cursor-style system notifications for DeepSeek Harness — silent while you watch the finishing session, toast when you have switched away. | 中文:dsh-helper 插件——为 DeepSeek Harness 提供离开才通知的系统提醒,正在看这场会话时保持安静。

Stars
0
Language
JavaScript
Created
Sep 20, 2026
Updated
Oct 1, 2026

Introduction

dsh-helper-plugin-notify-away

English | 中文

license: MIT dsh: 0.1.5-rc.2 tests: 93 passing

A system-notification plugin for DeepSeek Harness (dsh): raise an OS notification when a session finishes or blocks on you — by default wherever you are looking. Turn on the card's "Only when away" switch to stay silent while you watch that session.

Why it exists: a long dsh turn is easy to miss if you have gone to another window. The Web UI already shows a green "done" dot in the sidebar when you are looking at it. This plugin turns "finished / blocked on you" into a system toast — in dsh-helper by asking the host to raise it (chrome.webview.postMessage), in a system browser through the Notification API.

What it feels like: start a task and switch to another app, tab, or helper instance. When a root session goes idle, or stops to wait for you, a toast appears titled with that session. Clicking it focuses the Web UI and opens the session. By default it also toasts while you are watching that session; turn "Only when away" on to stop that.

How it feels: start a task, switch to another app, tab, or helper instance. When the root session goes idle — or stops to wait for you — a toast named after that session appears. Click it to focus the Web UI and open that session. If you were already watching that session on this instance, nothing is raised.

watching this session, this instance       silent
other instance / other window / other tab  system notification
                                           (finished *or* waiting on you)

Install (one line):

dsh plugin --profile web add link:/absolute/path/to/dsh-helper-plugin-notify-away
dsh plugin --profile web add github:x102201/dsh-helper-plugin-notify-away

What it does

PieceBehavior
Completion edgeFires when a listed session's running bit flips to idle. The first observation only records the bit, so a session already idle at load never toasts.
Wait edgeFires when uiSession.pendingInteractions newly carries a request for that session. A wait keeps running true, so the completion watcher would stay silent. Known kinds: approval, question, plan review. Any later kind still toasts (otherWait). Each kind can be turned off in config.
Away gateOff by default (toast wherever you look). Turn "Only when away" on to stay silent only while you are looking at that session in this instance: the panel is on screen, the helper is in front, and list.current matches. A hidden tab, another instance, an unfocused window, or a background session finishing / blocking still toasts.
Root sessionsCompletion edges ignore subagent rows (origin: 'subagent' or a parentId) by default, so parallel children do not flood the tray; turn "Include subagents" on to toast them too. Wait edges are not gated by that switch — a child agent blocked on you always toasts.
PermissionIn dsh-helper: none. In a system browser: asked on the first click or keystroke (Safari only grants gesture-bound requests).
DedupEach toast is tagged notify-away:<sessionId>, so a repeat replaces the previous instead of stacking.
ClickFocuses the window and calls ctx.sessions.open(sessionId). Helper click-back is the dsh-helper-notify-click event.

The plugin writes no session-event vocabulary of its own. It only reads the same sessions-list snapshot and pending-interaction map the sidebar reads. It does not answer approval or question waterfalls.

Install

Both installation styles are supported. The package ships plain ESM plus one ModuleLoader client factory (no build step, no runtime dependencies), so nothing has to be compiled or allowlisted by pnpm at install time.

Prerequisite: dsh plugin forwards its arguments to pnpm, so pnpm must be on PATH (corepack enable pnpm provides it). Without it the CLI prints pnpm not found on PATH and exits 127.

1. From a local checkout (link:)

dsh plugin --profile web add link:/absolute/path/to/dsh-helper-plugin-notify-away
  • The path must be absolute, or relative to the directory you run the command from (the CLI anchors relative link:/file: specs to your invoking directory, not to the profile).
  • link: symlinks the checkout, so edits to index.js/client.js/lib/*.js take effect on the next dsh web restart with no reinstall.

2. From a Git host (github:)

dsh plugin --profile web add github:<owner>/<repo>
  • The repository root must be this package (the directory holding package.json and cordis.patch.yml).
  • Pin a revision when you want reproducibility: github:<owner>/<repo>#<tag-or-commit>.
  • Because there is no prepare/postinstall script, pnpm never asks you to allowlist a build.

What happens underneath

dsh plugin ... add forwards to pnpm inside $DSH_HOME/profiles/web, then reconciles the profile manifest: because this package declares dsh.bundle.patch, its name is appended to dsh.profile.bundles automatically. On the next boot that bundle's patch layer adds one host row:

- insert:
    - id: notify-away
      name: ./index.js      # anchored to the patch file, so any install layout works

package.json's dsh.client declaration is what makes the Web UI scan this package and serve ./client as a ModuleLoader bundle. The host row exists so that scan has an active Loader entry, and so config is validated at boot.

Restart the profile to mount it:

dsh web            # alias of: dsh --profile web

In a system browser, grant notification permission the first time the page asks (your first click or keystroke). On macOS, also allow the browser in System Settings → Notifications. dsh-helper's embedded panel does not need this permission.

Try the host row without installing

examples/standalone.patch.yml anchors ../index.js to the patch file, so a bare checkout can mount the host row:

dsh --profile web --patch <checkout>/examples/standalone.patch.yml

The browser half still needs the package resolvable as dsh-helper-plugin-notify-away (a link: install), because dsh.client is discovered from the package manifest.

Verify the install

# the composed config tree should contain the notify-away row:
dsh --profile web --dump-config | grep -A2 notify-away

Then start a task in the Web UI, switch to another window, and wait for idle or a blocking prompt: a system notification titled with the session name should appear. Stay on that session with the window focused: nothing should appear.

Uninstall

dsh plugin --profile web remove dsh-helper-plugin-notify-away

Configuration

All keys are optional; an empty mapping uses the defaults (every notification kind on, wherever you are looking). The intended way to change them is the sidebar Plugins panel → the installed dsh-helper-plugin-notify-away card: its page carries the configuration between the description and 包含的组件 — seven switches that write on click, with no Save button.

The host row also validates the same keys at load (unknown keys and wrong types fail boot). A patch can pin deployment defaults; the card writes user overrides into the profile's cordis.patch.yml user layer and they take effect immediately, with no restart.

- id: notify-away
  config:
    onlyWhenAway: false
    includeSubagents: false
    body: Task finished.
    completion: true
    approval: true
    question: true
    planReview: true
    otherWait: true
KeyTypeDefaultMeaning
onlyWhenAwaybooleanfalseTurn it on to stay silent while you are looking at the session that just finished (with helper in front). Off by default: notify wherever you look.
includeSubagentsbooleanfalseAlso toast when a child agent finishes (running → idle). A child that waits on you always toasts, whatever this switch says.
titlestring(session display title)Optional static toast title.
bodystringTask finished.Toast body copy for a completion.
completionbooleantrueToast when a session goes running → idle. Cancelled turns also idle.
approvalbooleantrueToast on pending kind approval.
questionbooleantrueToast on pending kind question.
planReviewbooleantrueToast on pending kind plan-review.
otherWaitbooleantrueToast on any later pending kind the sidebar does not name yet.

Omit a kind (or leave it true) to keep it on. Set it to false to silence that kind only.

A patch replaces the targeted row's whole config mapping; omitted keys fall back to the defaults. Put overrides in the profile's cordis.patch.yml, or see examples/profile-patch.yml.

The live switches travel the 0.2 settings channel: the host declares only that this row ships its own page (ctx.settings.configure({ auto: false }, ctx.fiber)), the namespace is the Loader row id notify-away, described by the settings service straight from the exported Config schema; the browser half reads it through ctx.configForms.get('notify-away'), writes with .mutate(), and registers the page into the Plugins panel's plugins.bundle.config slot keyed by this package name — so it opens from OUR installed bundle card, not from the official plugin list (that seat is plugins.item). Saving writes the profile's user layer, not the cordis row. window.__dshHelperNotifyAway remains an emergency overlay on top of that.

Three things keep that page on screen. The host Config export has to be describable by the settings service (exporting it is enough, and only .volatile() fields reach the form). The page's store has to hand React a reference-stable snapshot: the renderer binds it through useSyncExternalStore, so a store that builds a new object per call re-renders forever until React throws — the slot's error boundary then hides the page and the console says slot entry crashed in 'plugins.bundle.config'. And the fiber that registers the page injects slots and configForms only, so every ambient service it touches must be read with the non-strict ctx.get(name) accessor: cordis throws cannot get property "locale" without inject on ctx.locale, and a throw before slots.register means no page at all (that error is the only trace).

How it works

sessions.list snapshot              ──►  running → idle edge
uiSession.pendingInteractions       ──►  new wait key (approval / question / …)
                                    │
                         shouldNotify(away || current !== sessionId)
                                    │
              chrome.webview.postMessage  ──►  helper OS toast
                         or Notification  ──►  browser OS banner
                                    │
                         click  ──►  window.focus + sessions.open
  • Away is a page fact. document.visibilityState === 'hidden' or !document.hasFocus(). There is no host-side focus signal.
  • dsh-helper does not need Notification permission. The client posts { kind: "notify-away", title, body, sessionId, tag } on the WebView2 page↔host channel already used by the panel. Helper shows the toast under its own AUMID and, on click, focuses the instance and dispatches dsh-helper-notify-click.
  • The client bundle is a factory, not an ESM graph. The Web UI loads client.js through window.__ModuleLoader__.load. Relative ./lib/ imports would not resolve, so client.js inlines the policy that lib/policy.js tests.
  • No install-time build. link: and github: therefore behave the same: the host imports only node: and relative paths, and there is no prepare/postinstall script for pnpm to allowlist.

Limits

  • Completions and waits use the same toast tag per session, so a later event replaces the previous one instead of stacking.
  • In a system browser the permission is browser-owned: if the prompt is denied, that origin stays silent. dsh-helper's panel uses postMessage instead and is not gated on this.
  • Cancelling a running turn also goes idle, so a cancelled task still triggers a "finished" toast unless completion is off.
  • Wait-toast copy is not configurable.

Compatibility

Written against dsh 0.1.5-rc.2. Seams used:

SeamUse
Host row name: ./index.js + dsh.bundle.patchlink: / github: install
package.json dsh.client (platform: web, inject: [@deepseek-ai/dsh-client-ui-plugin-manager])Web UI scans and serves ./client, after the Plugins panel declares plugins.bundle.config
Client inject: ['sessions']ctx.sessions.list snapshot + ctx.sessions.open; wait watcher attaches to ctx.uiSession.pendingInteractions
chrome.webview.postMessagedsh-helper panel → OS toast (no Notification permission)
Browser Notification APIfallback OS banner when the UI is not inside helper

Test

npm test              # node --test
npm run test:direct   # single process (sandboxes that block child processes)

Layout

index.js                     Cordis host plugin: config validation + ready log
client.js                    ModuleLoader factory: away gate + postMessage / Notification
cordis.patch.yml             bundle patch: insert the host row
lib/config.js                config schema and defaults
lib/schema.js                settings namespace schema (schemastery from the profile, or a fallback)
lib/policy.js                pure shouldNotify / running→idle / wait-key fold (tested)
examples/                    profile overlay and standalone host-row overlay
scripts/run-tests.mjs        single-process test runner
test/                        config, policy, package shape, host wiring, client factory
test-support/harness.mjs     fake Cordis ctx and sessions list
README.md / README.zh.md     this document, both languages

License

MIT.