dsh-status-indicator
A Windows system-tray indicator that monitors and controls a local DeepSeek Harness (DSH) web instance — the process started by dsh web
- Stars
- 0
- Language
- PowerShell
- Created
- Sep 12, 2026
- Updated
- Sep 12, 2026
Introduction
dsh-status-indicator
A Windows system-tray indicator that monitors and controls a local
DeepSeek Harness (DSH) web instance — the process started by dsh web
(node .../@deepseek-ai/dsh/lib/bin.js web).
The tray icon is the whole UI: green = running, yellow = starting/stopping, red = stopped/error. Right-click for start / restart / stop / open the Web UI.
中文文档见 README.zh-CN.md。
Features
| Live status | Tray icon colour + tooltip (PID, port, uptime, memory); balloon on state change |
| Lifecycle control | Start / Start as administrator (UAC) / Restart / Stop |
| Open the Web UI | Opens the browser at the authenticated URL (see below) |
| Self-elevation | "Restart indicator as administrator" hands the single-instance mutex over to an elevated copy |
| Three entry points | -Tray, -Once (one-shot status report), or an interactive console menu |
| Configurable logging | Level / directory / rotation via status-indicator.config.json |
| Autostart | Ready-to-use scheduled-task recipe (logon trigger, highest privileges) |
Requirements
- Windows (uses the WinForms tray, CIM and
Get-NetTCPConnection) - Windows PowerShell 5.1 or PowerShell 7+ — the script is UTF-8 with BOM
- Node.js on
PATH - DeepSeek Harness installed, i.e. a directory containing
node_modules\@deepseek-ai\dsh\lib\bin.js
Quick start
# one-shot status report
powershell -NoProfile -ExecutionPolicy Bypass -File .\status-indicator.ps1 -Once
# tray mode
powershell -NoProfile -ExecutionPolicy Bypass -File .\status-indicator.ps1 -Tray
# interactive console menu
powershell -NoProfile -ExecutionPolicy Bypass -File .\status-indicator.ps1
Parameters
| Parameter | Default | Meaning |
|---|---|---|
-Port | 3000 | Port the indicator uses when it starts DSH |
-DshDir | auto-detected | Directory containing node_modules\@deepseek-ai\dsh |
-Once | Print status once and exit | |
-Tray | Tray mode |
-DshDir is auto-detected in this order:
- the
-DshDirargument $env:DSH_INSTALL_DIR, then$env:DSH_DIR- the directory of the
dshcommand (e.g.<install>\dsh.cmd) and its parents %APPDATA%\npm\node_modules(global npm installs)
If none match, the indicator reports a clear error instead of guessing.
Port note.
-Portonly affects instances the indicator starts itself. If yourdsh webis configured to listen elsewhere, pass the matching-Port, otherwise the indicator will launch its own instance on3000.
How it works
UI (tray / console) ──request queue──▶ Worker (single long-lived engine) ──▶ Core (detection / process control)
▲ │
└──────────────────────── StateStore (snapshots) ◀─────────────────────────┘
- The UI thread never blocks. Tray clicks and the 1-second timer only enqueue
requests and read a snapshot. All CIM queries, TCP probes, process starts/stops
and
Start-Processcalls run on a background worker engine, so the context menu is always instantly responsive. - Lifecycle operations are serialized through one queue, each tagged with an
operationIdthat appears in the log. - Nothing is assumed to have succeeded. "Spawned a process" ≠ "started successfully": a start is only reported as successful after the process is observed and the port is listening. A stop is only successful once the PID is confirmed gone.
- Port ownership is decided by joining PID → command line → listening port. A port held by an unrelated program is never treated as "running"; it is only reported as a diagnostic, and only the port the indicator is itself binding can fail a start early.
- HTTP 401/403 does not mean "stopped". A successful TCP connect counts as "listening".
- Single instance via a named mutex (
Local\DshStatusIndicatorTray) — there is no PID file. When elevating, the new copy waits up to 30 s for the old one. - Bounded cleanup. Timer, NotifyIcon, menu, native HICONs, mutex and worker are all released on exit, and exiting never kills a running Harness.
Authenticated Web UI URL
dsh web mints a per-process random launch token that lives only in memory
and is printed exactly once at startup:
dsh web: http://127.0.0.1:3080/?token=<43-char-base64url>
Visiting that URL mints a signed HttpOnly cookie (30 days by default, bound to
the host:port authority). Any request without the token or cookie gets
401 dsh web authentication required.
Because the token is never written to disk, the indicator redirects the launched process's stdout to a capture file and parses the URL from it:
- capture files are named
.dsh-web-url-*.txtin the project root, at most 5 are kept, and they are deleted when the instance stops; - the token is held in memory only and is never written to the log;
.dsh-web-url-*.txtis gitignored — never commit it;- if no token URL could be captured, the indicator falls back to the plain URL (which still works if the browser already holds the cookie).
Configuration
status-indicator.config.json (created with defaults on first run; changes take
effect on the next start, there is no hot reload):
{
"logging": {
"enabled": true, // false = no I/O and no thread at all
"level": "INFO", // DEBUG / INFO / WARN / ERROR
"directory": "logs", // relative to the project root; absolute paths are rejected
"maxFileSizeMB": 5, // 1-100
"maxFiles": 5 // 1-50
}
}
Invalid values fall back to defaults and are recorded — a broken config never prevents startup. Logging is a low-overhead side channel: an in-memory queue plus a single writer thread, so the UI thread never touches the disk, with a bounded flush on exit.
Autostart (logon, elevated)
Run once from an elevated PowerShell:
schtasks /Create /F /TN "dsh-status-indicator" /SC ONLOGON /RL HIGHEST /DELAY 0000:05 `
/TR "powershell.exe -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File \"C:\path\to\status-indicator.ps1\" -Tray -Port 3000"
/RL HIGHEST runs the tray with administrator rights, so starting DSH does not
raise a UAC prompt. Remove it with schtasks /Delete /F /TN "dsh-status-indicator".
Testing
All suites are isolated: they never touch a real, already-running DSH instance.
powershell -NoProfile -ExecutionPolicy Bypass -File .\test\run-lifecycle-tests.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File .\test\run-logging-tests.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File .\test\run-token-url-tests.ps1
| Suite | Covers |
|---|---|
run-lifecycle-tests.ps1 | Start/verify, stop/verify, restart, start failure → ERROR, port-conflict diagnostics. Uses a fake Harness (test\dsh\...\bin.js) and a process filter restricted to that fake, so real processes are never matched. |
run-logging-tests.ps1 | Logging on/off, invalid-JSON fallback, bad level, absolute-path rejection, rotation, resume. |
run-token-url-tests.ps1 | Launches a real DSH on a spare port (-TestPort, default 3999), asserts the token URL is captured, and verifies bare URL → 401 / token URL → 303. Cleanup only kills PIDs on that test port. |
Expected results: 14/14, 12/12, 9/9.
Troubleshooting
| Symptom | Cause / fix |
|---|---|
dsh web authentication required in the browser | The plain URL was opened. Let the indicator start/restart the instance so it can capture the token, or open the URL printed by dsh web once to set the cookie. |
| DSH directory not found | Auto-detection failed. Pass -DshDir, or set DSH_INSTALL_DIR. |
| Tray icon does not appear | Another copy holds the single-instance mutex — check the log; a duplicate copy exits after 30 s. |
| Status stays red although DSH is running | DSH listens on a port outside the candidate set (-Port, 3000, 3080). Start the indicator with the matching -Port. |
| Chinese text renders as garbage | The script must stay UTF-8 with BOM; re-save with a BOM after editing. |