dsh-session-hub — Session Status Hub for DeepSeek Harness
English · 简体中文
A missing "how are all my Sessions doing right now?" surface for DeepSeek Harness:
- A rail in the top title bar listing the Sessions that need you, ordered by
red (failed) > yellow (waiting for your answer) > green (unread) > blue (running). One click jumps straight to that Session. - Windows notifications when a Session changes state (finished / failed / needs an answer), where clicking the toast reopens the Session and brings the DSH window forward.
- Taskbar status dot on the DSH taskbar button (red / yellow / green, priority
red > yellow > green), viaITaskbarList3.SetOverlayIcon— visible while the window is minimized or buried behind other apps, including when the conversation you are looking at finishes in the background (the stock UI marks nothing for the main view; this plugin fills that gap).
Full, behaviour-level documentation (state machine, notification edge engine, the measured Windows toast
taglimit, taskbar worker internals, every known boundary) is in the Chinese doc: README.zh-CN.md.
States and priority
| State | Determined by | Rail chip | Taskbar |
|---|---|---|---|
failed error |
hubTurnOutcome.outcome ∈ error / interrupted, not yet acknowledged |
🔴 + 报错 | red (highest) |
waiting answer |
pendingInteraction.kind ∈ approval / plan-review / question |
🟡 + 等待回答 | yellow |
unread unread |
completionUnread === true, or the plugin's own "finished in background" record |
🟢 + 待查看 | green |
running running |
sessionStatus.running, or the Session row's running |
🔵 + 运行中 | no dot |
- Chip order is the colour order (red > yellow > green > blue); within a state, most recently updated first. The 3 chips shown by default are therefore always the 3 that most need you.
- Classification order (which bucket a Session lands in) is independent of display order: waiting > failed > running > unread.
- Running never lights the taskbar. Running is the normal case; lighting it would leave the dot permanently green and mask real red/yellow signal. It still shows as a blue pulsing chip.
- Blank "new session" rows and subagent Sessions (
origin: subagent) are ignored. - A "failed" chip clears when you click it or when you are looking at that Session with the window focused; a new failure re-lights it.
Why the plugin records unread state itself
The stock ui-session only sets completionUnread when a turn ends in a Session that is
not the main view — so "I was reading this conversation, switched to another app, the turn
finished" produces nothing: no dot, no taskbar mark, no rail chip. The plugin observes the
same edge and, when the window is not in the foreground, records the completion itself
(localStorage dsh-session-hub/local-unread-v1) so the green dot, the green taskbar mark
and the 待查看 chip all appear. It clears when you come back and look at that Session, or when
you click its notification/chip.
Notification categories
| Notification | Trigger | Default |
|---|---|---|
| Needs your response | a pendingInteraction appears / changes key |
✅ |
| Turn failed | new turn outcome error / interrupted (incl. account signed-out hook) |
✅ |
| Turn completed | new turn outcome completed |
✅ |
| Output limit reached | new turn outcome max-tokens |
✅ |
| Turn stopped | aborted (you pressed stop) |
❌ |
No notifications for "thinking… / running a tool…" streaming activity — that is where most notification plugins become noise. Titles carry a colour emoji (🟢 done, 🟡 needs you, 🔴 failed, 🟠 max-tokens) from the same palette as the rail and the taskbar dot; Windows toasts always use the app icon, so text is the only place colour can live.
Requirements
- DeepSeek Harness with the
sessionProjections,sessionsanduiSessionservices (any recent desktop or web build). - Node.js ≥ 20 for the host half (DSH ships its own; nothing to install — the plugin has zero npm dependencies).
- Windows 10/11 for window raising and the taskbar dot (PowerShell 5.1 + Win32). The rail, notifications and navigation also work in the web UI.
Install
Naming: the npm package is
dsh-session-status-hub(dsh-session-hubwas already taken on npm by another plugin); the repository name and the plugin's internal id staydsh-session-hub.
Option 1 — npm
npm install dsh-session-status-hub
Option 2 — from source. Clone (or download) the repo anywhere:
git clone https://github.com/railgun20001/dsh-session-hub.git
Then, either:
- DSH → Plugins page → add local plugin, and pick the cloned
dsh-session-hubdirectory; or - plugin manager (if your session exposes a
plugin_managertool) →install_bundlewith that directory as the target.
After installing: the host half (index.js) hot-loads with the profile — no restart. The
client half (lib/client.js) needs a page reload (Ctrl+R). Uninstalling removes the
taskbar dot automatically.
Configuration
Config lives in localStorage under dsh-session-hub/config-v1; defaults are already
"don't disturb me":
| Key | Default | Meaning |
|---|---|---|
enabled |
true |
Master switch (watermarks keep advancing; re-enabling never replays history) |
onComplete / onFailure / onAnswer |
true |
completed / failed / needs your response |
onMaxTokens |
true |
output-limit hits |
onStopped |
false |
manual stop / interrupt |
includeSubagents |
false |
also notify and list subagent Sessions |
silenceCurrentWhenFocused |
true |
stay silent for the Session you are currently reading |
silenceAllWhenFocused |
false |
never notify while the window is focused |
requireVisibleForFocus |
true |
minimized/hidden counts as unfocused (notify anyway) |
freshnessWindowMs |
120000 |
edges older than this only advance watermarks (0 disables) |
sound |
true |
system notification sound |
colorMarkers |
true |
colour emoji in notification titles |
colorMarkers |
true |
colour emoji in notification titles |
raiseWindow |
true |
bring the desktop window forward on notification click |
maxRememberedSessions |
300 |
cap on the notification watermark table (newest by updatedAt kept) |
Alongside config-v1, the plugin keeps a few state keys under the same prefix — worth knowing
when troubleshooting, migrating or wiping data:
| Key | Contents |
|---|---|
watermarks-v1 |
notification watermarks: how far each Session has been consumed |
error-acks-v1 |
acknowledged failures { sessionId: seq } (what turns the red chip off) |
local-unread-v1 |
plugin-recorded "finished in background, unseen" { sessionId: seq } |
recent-v1 |
cross-tab dedupe window (4 s) |
notify-log-v1 |
last 20 notification landing results |
From the DSH DevTools console:
__DSH_SESSION_HUB__.getConfig()
__DSH_SESSION_HUB__.setConfig({ silenceAllWhenFocused: true })
enabled / onComplete only mute notifications — the unread dot and the taskbar mark are
state facts ("this turn finished and you haven't looked"), not interruptions.
Diagnostics
npm run check # 1) lib/client.js matches src/ 2) 135 test cases
__DSH_SESSION_HUB__.inspect() // versions, services, focused, hostHalf, rows, taskbar
__DSH_SESSION_HUB__.getRows() // current rail rows
__DSH_SESSION_HUB__.getTaskbarState() // red / yellow / green / none
__DSH_SESSION_HUB__.getLocalUnread() // plugin-side "finished in background, unseen" records
__DSH_SESSION_HUB__.testNotification() // fire a test toast; clicking it should jump back
__DSH_SESSION_HUB__.getNotifyLog() // last 20 notification landing results
__DSH_SESSION_HUB__.pingHost() // 'present' / 'absent' / 'unknown'
Host half, read-only:
# The origin follows your DSH instance: whatever origin serves the page also serves these
# routes (the desktop shell forwards same-origin paths to the host). Example: the web UI at
# http://127.0.0.1:8779/.
$base = 'http://127.0.0.1:8779'
$h = @{ 'x-dsh-session-hub' = '1' } # internal routes require the signature header (403 without it)
Invoke-RestMethod -Uri "$base/dsh-session-hub/ping" -Method POST -Headers $h
Invoke-RestMethod -Uri "$base/dsh-session-hub/debug" -Method POST -Headers $h
Architecture
dsh-session-hub/
├── index.js host half: hubTurnOutcome projection + routes
├── lib/raise-window.js host half: window raise + taskbar dot (resident PowerShell worker)
├── lib/client.js client bundle (generated by build/bundle.mjs — do not edit)
├── src/hub-identity.js plugin id / projection key / route prefix / signature header / storage keys (shared)
├── src/hub-strings.js UI strings zh/en (single source)
├── src/hub-model.js rail + taskbar logic (pure, unit-testable)
├── src/notify-core.js notification edge engine (pure, unit-testable)
├── src/hub-scroll.js sidebar row reveal/scroll + reveal controller (unit-testable)
├── src/hub-json-store.js localStorage JSON read/write (fallback on read, warn on write)
├── src/hub-stores.js config / watermarks / error acks / background-completion records
├── src/hub-notify.js notification landing: short tag, cross-tab dedupe, landing log
├── src/hub-host.js host-half routes: raise window / taskbar state / liveness probe
├── src/hub-ui.js React rail + row hint components
├── src/hub-slots.js slot registration
├── src/client-main.js wiring: services → model → notifications / rail / taskbar
├── build/bundle.mjs inlines src/ in order into one classic-script bundle
├── test/ 135 node:test cases, zero dependencies
├── locale/{zh,en}.json plugin manager title and description
├── icon.svg plugin icon
├── cordis.patch.yml bundle patch inserting the session-hub row
├── .github/workflows/ci.yml CI: bundle-in-sync check + full test run
└── LICENSE MIT
Why the plugin computes edges itself: DSH hands the client current state, not an event
stream (running is a boolean, pendingInteraction the current value, the projection the
last turn's outcome). Notifications therefore have to be derived from state edges, in exactly
one implementation (planNotifications), which also reports "this turn really ended" so the
client can record unread state the stock UI never sets for the main view.
Why failure detection needs the host half: the client cannot see why the previous turn
ended. turn/end carries reason.kind, but only for retained Sessions. A session projection
is the supported answer: the framework folds all Sessions (cold ones included), and the
value reaches the client through three supported channels. The plugin therefore retains no
Session at all.
Known boundaries
- The taskbar dot and window raising are Windows-only; in a plain browser
(
desktopShell === false) nothing is reported. - Window raising depends on the Windows foreground lock; fullscreen apps, elevated (UAC) windows and RDP switches can still refuse. In-app navigation always happens.
- The first notification click may be slow (the worker compiles C# once, ~6 s on a slow machine); the worker is pre-warmed as soon as a notification is shown, and clicks queue.
- Notification
tags are capped at 15 characters on purpose: a longtagmakes Chromium drop the whole toast silently — the measured table is in the Chinese doc. - Sidebar scrolling relies on the official
data-row-keyconvention; if upstream changes it, scrolling degrades silently (navigation is unaffected).revealSessionRow(id)returnsnullwhen that happens. - Failed/unread marks do not expire on their own: failures need an explicit ack, unread needs you to open the Session.
- The "failed" state cannot be painted on the official sidebar row dot (the upstream row
state machine has no error state), so the truth is surfaced in the row hover card instead
(
本轮失败/本轮中断).
Security and privacy
- Naming: the npm package is
dsh-session-status-hub(the plain name was taken);cordis.patch.yml'snameand thewindow.__ModuleLoader__registration id both use the package name, whilePLUGIN_ID(route prefix,localStoragekeys, log tags) deliberately staysdsh-session-hub— renaming it would orphan every stored setting for no benefit. - No outbound network calls. The plugin only talks to the local host half
(
dsh-session-hub/*same-origin routes) — no telemetry, no analytics, no third parties. - Everything stays on this machine: config, watermarks, error acks, unread records and
the notification landing log live in
localStorageunder thedsh-session-hub/prefix. The plugin never reads conversation content — only the last turn's outcome and a few state booleans. - Internal routes require a signature header: every client request carries
x-dsh-session-hub: 1and the host half only answers when it is present (403 otherwise). This is not authentication — it blocks cross-site triggering: a custom header turns a browser request into a preflighted cross-site request, and the host half returns no CORS headers, so a random web page that can POST to the local port never reaches the handler./raise(bring the window forward) and/warmup(spawn the PowerShell worker) therefore cannot be triggered by arbitrary pages. The endpoints still only affect the window and the taskbar icon and return no user data (/debugreturns projection keys, versions and process state). Even so, do not expose the DSH web port to an untrusted network. - The signature header is new in this version: host half and client half must be the same version or newer. An older client half (no header) is rejected by a newer host half, and the console says so explicitly ("missing signature header, reload the page to align").
- The host half spawns one PowerShell process (
-NoProfile -EncodedCommand, no script file written, no execution-policy change). The worker script is inlib/raise-window.js; setraiseWindow: falseto skip window raising.
Contributing
npm run check must pass (bundle in sync with src/, all tests green). lib/client.js is
generated: edit src/ and run npm run build, never edit the bundle by hand. Commits follow
Conventional Commits with a Chinese subject and a body describing the change and its impact.
License
MIT © 2026 railgun20001
No comments yet. Be the first to write one.