dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
把标签页图标变成绿/琥珀状态灯,并在会话跑完或有交互待处理时发送浏览器通知,含 47 个可选音效。
中文 | English
In one line: a notification center for DeepSeek Harness — it turns the tab's whale icon into a status light and gets your attention while you are looking elsewhere.
| Package | dsh-notice-center |
| Plugin instance id | notice-center |
| Settings namespace | notice-center |
| Install shapes | npm package / GitHub repo / tarball / local link — all four supported |
Usage
Development
Inside Harness, the two signals that matter — "a session finished" and "something is waiting for your decision" — only show up on the sidebar dots and inside the session itself. The moment you switch to another tab or another app, they are invisible.
Long tasks make this worse: you go do something else, come back, and the session finished ages ago — or an approval request has been sitting there for ten minutes.
This plugin moves both signals somewhere you will actually see them:
| While you are away from the DSH page | What you get |
|---|---|
| A session finished | The tab whale turns green, plus a system notification (with the turn duration when it is a single notification) |
| Something needs you | The tab whale turns amber, plus a system notification (saying whether it is an approval / question / plan review) |
Three colours are encoded directly into the tab icon, driven by the same official signal as the sidebar dots, so they can never drift apart:
| Whale colour | Meaning | When it clears |
|---|---|---|
| A session finished while you were not watching it | Clears once you open that session; back to default once you have opened them all | |
| A session awaits you: question / approval / plan review | Clears once handled | |
| All clear (the stock icon) | Default state |
Two events raise a notification, and by default only while you are not looking:
| Event | Trigger | What the notification says |
|---|---|---|
| Finished | A session completes | Title = session name; body = Session finished, with the turn duration appended when it is a single notification (e.g. Session finished · turn took 3s) |
| Pending | A new pending interaction appears | Title = session name; body = the interaction type: Approval needed · <tool> / Choose an option (Choose options for multi-select, Type an answer when there are no options, N questions for a batch) / Plan review; unknown types fall back to Something awaits you |
| Finished (green) | Pending (amber) |
|---|---|
Notifications are raised by the browser (Chrome here), so the card shows the browser and the site
127.0.0.1:3080— that is expected. The screenshots show the English UI; notification copy is localised at runtime.
Other behaviour:
+N appended to the titleFinished and pending each have their own dropdown, 47 entries in total: 2 built-in synthesised chimes plus 45 mp3s taken from opencode's bundled sound library.
| Pack | Entries |
|---|---|
| Built-in (synthesised) | Chime Up (finished default, rising two-tone), Chime Down (pending default, falling two-tone) |
| Alert | Alert 01–10 |
| Bip-bop | Bip-bop 01–10 |
| Staplebops | Staplebops 01–07 |
| Nope | Nope 01–12 |
| Yup | Yup 01–06 |
/notice-center-sounds/<id>.mp3; if that route is unavailable it falls back to the built-in chime, never to silenceChime Up / Chime Down1.4.0 requires 0.1.7-rc.2+; 1.3.x supports 0.1.2-rc.1+
engines.dsh and the @deepseek-ai/dsh-* peers, guarded by test/compat-range.smoke.mjs. The two lines split by host version: the Market never offers 1.4.0 to an older host, and the DSH core refuses to load it — older hosts simply stay on 1.3.x with no update prompt.settings.register while the old declaration still passed), so never treat it as a compatibility guarantee on its own.http://127.0.0.1:3080 or localhost works; over a LAN IP the browser's Notification API is unavailable (a browser restriction, not a plugin issue)dsh plugin --profile web add dsh-notice-center
Package page: https://www.npmjs.com/package/dsh-notice-center (latest is stable; prereleases go to the next tag — install with dsh-notice-center@next)
dsh plugin --profile web add github:SCP-QQ/dsh-notice-center
# or over SSH (when a proxy interferes with HTTPS on your machine)
dsh plugin --profile web add git+ssh://git@github.com:SCP-QQ/dsh-notice-center.git
npm pack # produces dsh-notice-center-<version>.tgz
dsh plugin --profile web add ./dsh-notice-center-<version>.tgz
In the profile's package.json: add "dsh-notice-center": "link:<project dir>" to dependencies and "dsh-notice-center" to dsh.profile.bundles, then run pnpm install inside the profile directory.
All four options require a Harness restart to take effect. Installing from npm / GitHub needs no extra
@deepseek-ai/*packages from the profile — the host half's only runtime dependency is@deepseek-ai/schemastery(available on npm).
dsh plugin --profile web remove dsh-notice-center
Remove the dependency and the dsh.profile.bundles entry, then restart — nothing is left behind.
Open the settings page — Settings → Notification center (a sidebar entry)
Grant system notifications — the master switch is on by default: after the page loads, your first click or key press makes the plugin ask the browser for notification permission once (browsers refuse requests without a user gesture, so the page cannot pop it on its own — it borrows your first interaction). Choose Allow and you also get a "Notifications enabled" confirmation. Frequency rules:
Later, use the status chip right below the switch (the short state is always visible; hover it or focus it with the keyboard for the full explanation):
| State | Chip | What to do |
|---|---|---|
| Not granted (you dismissed the prompt) | "Not granted - Grant", clickable | Click it and the browser asks again |
| Blocked | warning icon + "Blocked" | Browsers do not ask twice (and a page may not open site settings): hover for the recovery path — click the icon at the left of the address bar → Notifications → Allow, or open chrome://settings/content/notifications |
| Unsupported | one small line | Usually a secure context problem (see Install → Requirements): use http://127.0.0.1:3080 |
| Granted | hidden (the row stays clean) | — |
The switch itself always works (you can turn notifications off even while unauthorised — and turning it off stops the automatic request); change the permission in site settings and the chip follows immediately.
Tune as you like — expand the two groups: change colours, pick sounds, set volume, toggle Notify in the foreground / Auto hide
After that, whenever you switch away or minimise, a finished session or a pending interaction notifies you — and clicking the notification jumps straight back to that session.
| Group | Setting | Key | Default | Notes |
|---|---|---|---|---|
| Whale status light | Master switch | colorsEnabled | on | When off, the stock icon is kept |
| Finished | green | #22C55E | Finished status-light colour (official sidebar colour) | |
| Pending | amber | #F59E0B | Pending status-light colour (official sidebar colour) | |
| Default colour | black | unset | Unset = keep the stock /favicon.svg | |
| System notifications | Master switch | notifyEnabled | on | On by default; the first interaction asks for browser permission, and a status chip appears below the switch while it is missing |
| Auto hide | notifyAutoHide | on | Off = the notification stays until you dismiss it | |
| Notify in the foreground | notifyForeground | off | On = also notify while you are looking at the page | |
| Sound | notifySound | on | When on, the system sound is muted and the plugin sound plays | |
| Volume | notifyVolume | 0.6 | 0–1 | |
| Finished sound | notifyDoneSound | builtin-up | one of 47 | |
| Pending sound | notifyPendingSound | builtin-down | one of 47 |
Settings are persisted by Harness' settings service into the notice-center: section of <DSH_HOME>/settings.yaml.
The settings page also shows a one-line footer at the very top with the plugin name and version (e.g. "Notification center v1.3.0") — clicking it opens the repository in a new tab.
| Symptom | Cause and fix |
|---|---|
| No "Notification center" entry in Settings | The plugin did not load. Make sure you restarted Harness after installing, then look for settings namespace "notice-center" registered in the Harness console |
| A switch does nothing / a new setting has no effect | After changing the host half (lib/index.mjs) you must restart Harness — settings writes go through the host schema |
| A permission prompt popped up on my very first click after installing | The master switch is on by default, so the plugin borrows your first interaction (click / key press) to ask once — browsers refuse permission requests without a user gesture. Choose Block if you do not want it, and use the status chip in the settings later |
| No notifications at all | Three conditions are required: ① the master switch is on; ② browser permission is Allow; ③ the page is not in the foreground when the event fires (use Notify in the foreground to relax this) |
| No notifications over a LAN IP | Non-secure context: the browser disables the Notification API, so system notifications are entirely unavailable (unrelated to Notify in the foreground). Use 127.0.0.1 / localhost; over a LAN only the tab status light works |
| Nothing after closing the tab | Known limitation, see below |
| The green light disappears after a refresh | Expected. That state is the official signal's in-memory state and is lost on refresh |
| The notification flashes by too fast | Turn Auto hide off and it stays until dismissed |
| No sound | Check in order: sound switch, volume not 0, whether the sound route is available (it falls back to the built-in chime, so it should never be fully silent) |
| Notifications swallowed by the OS | Windows Focus Assist / Do Not Disturb intercepts them; the plugin cannot detect it |
Everything below is for maintainers: architecture, local development, tests and releasing. If you only want to use the plugin, you can stop here.
The plugin has two halves running in different processes, exchanging configuration through Harness' settings service:
| Part | File | Runs in | Responsibility |
|---|---|---|---|
| Host half | lib/index.mjs | Harness main process (Node) | ① Declare the Config schema (settings form, write validation and the legacy settings.yaml import all derive from it); ② serve /notice-center-sounds/<id>.mp3 |
| Browser half | lib/client.cjs | Browser (plugin bundle) | favicon state machine + notification state machine + sound library + settings page |
| Bundle patch | cordis.patch.yml | Loader layer | Insert one row id: notice-center into the plugin tree (that id IS the settings namespace) |
The host half's two optional channels:
ctx.inject(["settings"], …) — declares "this plugin ships its own page": settings.configure({ auto: false }, ctx.fiber). The owner must be the plugin's own fiber (describe() looks the policy up by entry.fiber, and the inject child is a different fiber)ctx.inject(["webServer"], …) — mounts the sound route. When the service is absent the route is not mounted and the browser half falls back to the built-in chimeThe settings contract (since 0.1.7 — the easiest thing to break silently here):
describe() reads the entry fiber's runtime.Config, and the entry id is the namespace — the plugin no longer registers a namespace (the old settings.register(ns, schema) was removed in 0.1.7)..volatile(): volatileForm() only projects fields under the nearest volatile ancestor, so one unmarked field disappears from the settings page entirely (test/host-half.smoke.mjs asserts this field by field). .volatile() requires schemastery ≥ 3.18.4.<DSH_HOME>/settings.yaml, writes each section into the entry carrying the same id, then renames the file to settings.yaml.imported. Our section name equals the entry id (notice-center).Why the host half does not import @deepseek-ai/dsh-settings: it keeps the host half's only runtime dependency at @deepseek-ai/schemastery — and because profiles default to autoInstallPeers: false, resolving any @deepseek-ai peer would actually fail on a version mismatch and break loading. This is what makes a clean registry install possible.
Where the browser half gets its data (all official client services; the plugin holds no state of its own):
| Service | Provided by | Used for |
|---|---|---|
configForms | @deepseek-ai/dsh-client-ui-settings | configForms.get(entryId) reads and writes settings (the old settingsScope was removed in 0.1.7); the snapshot carries status / writable |
sessions | @deepseek-ai/dsh-api-session-controller/client | Session list snapshot: origin / running / displayTitle |
uiSession | @deepseek-ai/dsh-client-ui-session | sessionStatus: Map<sessionId, { running, pendingInteraction, completionUnread }> — 0.1.7 removed both the row fields completed / pendingInteraction and the pendingInteractions store; the official workspace session rows use the same mapping (completed ≡ completionUnread) |
The sound route is deliberately narrow: only ^[a-z0-9-]+\.mp3$ filenames are served (no path traversal), and because the audio is a package constant it is cached immutable.
package.json package metadata (scripts: test / test:host / test:client)
cordis.patch.yml bundle patch layer (insert id: notice-center)
lib/index.mjs host half: settings schema + sound static route
lib/client.cjs browser half: favicon machine + notification machine + sound library + settings page
assets/audio/*.mp3 45 opencode sounds (MIT; see assets/audio/README.md)
docs/images/ README screenshots, banners and the social-preview card
test/host-half.smoke.mjs host-half smoke test (164 lines)
test/client-half.smoke.mjs browser-half smoke test: state machine + real settings-page render (687 lines)
.github/workflows/test.yml CI: runs pnpm test on push / PR
.github/workflows/publish.yml release: tag-triggered, OIDC, no token
sessions.list changes
└─ sync()
├─ trackEdges() running true->false edge (compensation when the current session finishes)
├─ detectTransitions() official completed false->true; new sessions in pendingInteractions
│ └─ queueNotification(kind, sessionId, label, typeLabel)
│ ├─ master switch off -> dropped
│ ├─ this same completion already queued -> dropped (no double-send)
│ └─ enqueued + 300ms aggregation window
└─ targetOf() -> favicon becomes green / amber / default
300ms later, flushNotifications()
├─ permission not granted -> dropped
├─ page in foreground and foreground notify off -> dropped
├─ group by kind -> one Notification per group (title = session name, body = event type)
└─ play the selected sound
Notifications deliberately carry no tag: the same session reuses the same tag across runs, and Windows/Chrome treats the later one as an update to an existing notification — it replaces silently and never raises a banner again.
pnpm install # the only runtime dependency is @deepseek-ai/schemastery
npm test # host-half + browser-half smoke tests
The restart rule has two halves — this is the easiest thing to trip over:
| What you changed | Restart Harness? | Why |
|---|---|---|
lib/client.cjs (browser half) | No — a page refresh is cleanest | Client HMR stat-polls the bundle and tells the browser to reload that plugin |
lib/index.mjs (host half) | Yes, mandatory | Host-half hot reload relies on cordis-plugin-hmr (needs the loader to run with --expose-internals), which does not work in practice. Schema changes (adding/removing settings) also need a restart — otherwise the settings page write fails validation, which looks like "the switch does nothing" |
| First install, any rename | Yes, mandatory | The plugin tree and bundle list changed |
Debugging: browser-half errors land in the DevTools console; a successful host-half registration logs dsh-notice-center: settings namespace "notice-center" registered to the Harness console.
npm test # both (this is what CI runs)
npm run test:host # host half only
npm run test:client # browser half only
Both smoke tests are zero-dependency, pure Node — no browser needed:
apply(), asserts only the expected services are injected, checks schema defaults and validation, exercises the sound route (serves an mp3 / 404 for unknown / rejects path traversal / all 45 packaged files present) and verifies that an existing settings.yaml passes the schema with every field preserved verbatimwindow.__ModuleLoader__ / document / Notification, evaluates the bundle and takes its factory, assembles apply() with a fake ctx, then drives state transitions and asserts the Notification constructor arguments. The settings page is really rendered against a recording stub (React element tree), asserting switch order, that each control writes only its own field, collapse behaviour, the footer, and moreInvariants pinned by the tests — do not break these:
PLUGIN_VERSION / PLUGIN_NAME / PLUGIN_REPO in lib/client.cjs must match package.jsonSOUND_PACKS library must map one-to-one onto the files in assets/audio/; adding a sound means updating it in two places: lib/client.cjs and test/client-half.smoke.mjszh and en dictionaries must have identical key setsReleasing is driven by a tag-triggered publish.yml — ordinary commits and pushes never publish: