dsh-power
Manifest validWindows-only power card injected into the DSH page: the artwork carries two hit zones (left shuts down, right restarts), the first click only turns the card into a yes/no question and the second click
dsh-power
Power actions for DSH (DeepSeek Harness) desktop profile: restart and shutdown via a draggable power card on the page.
What it does
- A single draggable card on the DSH UI: left half = shut down, right half = restart; middle seam is safe.
- First click asks ("Shut down?" / "Restart?"). Second click on any spot executes that action (clicking elsewhere won't re-target).
- Escape / click outside / 3s timeout cancels with a brief "Cancelled" toast.
Shift+click= dry-run only (checks endpoints without firing).- Drag to move (saved in localStorage); double-click the middle seam resets position.
Behind the scenes:
- The new instance is launched outside the host's Job Object (WMI → explorer/scheduler fallback) and is only reported as up once it answers on
/api/dsh-power/statuswith a pid that started after the kill — "something is listening" is not enough, because a second instance answers just as well. - After it says OK the helper keeps watching for a while, outside the lock: if the instance dies, is torn down, or the serving pid changes without this helper launching anything, the recorded result is corrected instead of staying a false success.
- Retries are cut short when they cannot win: if an old instance still holds the single-instance lock the failure says
lock-held-by-otherinstead of burning the whole budget, and a launch path that reported no pid at all is declared dead after ~6s and retried. - If you have multiple DSH instances running, this version kills only your current session tree (parent + children whose path matches), avoiding killing another instance by accident.
- On restart, the new window is brought to the foreground so you can see that it finished.
Requirements
- Windows 10/11 (PowerShell 5.1 for WMI and Win32 calls).
- DSH desktop profile (injected by the host process, no browser side needed).
- Node.js 20+ (whatever DSH ships is fine).
Install
From GitHub:
dsh plugin --profile desktop add github:nanshang328/dsh-power
Or from a local folder / ZIP download:
dsh plugin --profile desktop add ./dsh-power
Then restart DSH. The package declares dsh.bundle, so it mounts as a normal profile layer.
Config
Plugin config (override the same id in your profile's cordis.patch.yml; a later patch
REPLACES the whole config object, so restate every key you still want):
- id: dsh-power
name: 'dsh-power'
config:
port: 0 # 0 = discover from webServer/--port
processName: DeepSeek Harness
exePath: '' # '' = use process.execPath
showRestart: true
showShutdown: true
confirmPower: true # both actions need a second click (confirmShutdown is the old name, still read)
confirmMs: 3000 # how long the pending question stays armed
foreground: true # bring the new window to the front after restart
logToTemp: true # write %TEMP%\dsh-power.log
Security notes
- The power endpoints require
isLoopbackAND a customx-dsh-power: 1header. A random web page cannot send that header cross-origin without a CORS preflight, which this route does not answer — so drive-by "shut down the user's app" from an ad iframe is blocked. (Ano-corsfetch with only simple headers still reaches the server, which is exactly why the header check exists.) - Multi-instance safety: with several DSH instances running, only the calling instance's
process tree (its Electron shell + same-named descendants, resolved from the host pid)
is stopped. Other instances are left alone. If the tree cannot be resolved the script
aborts instead of falling back to "kill all named".
-KillAllNamedrestores the old behavior explicitly;-Explainprints both sets without killing anything.
Known limitations
- No unsaved changes are preserved by any OS-level hook (that's expected).
- After a successful restart, the widget may momentarily say "request failed" — that's just the old instance dying mid-flight. Check logs or
/power-statusfor confirmation. - Some fully-focused games / video apps reject
SetForegroundWindow; in that case logs will sayforeground=refused. - Position persistence is browser-scoped (localStorage); switching browsers changes where the card appears.
- Hand-run mode (
-Port 0, when you run the script yourself) has no status route to probe: readiness there is "the pid we launched outlived its first seconds", and it is watched by pid liveness rather than by the port.
Development
- Source:
lib/index.js(host half),client/widget.js(the card),scripts/restart-dsh.ps1(the restart engine).client/niang-sleep.webpis inlined into the widget bytools/embed-icon.cjs(runpython tools/build-icon.pyfirst if you swap the art). - The PowerShell script must stay ASCII-only: Windows PowerShell 5.1 reads a BOM-less
.ps1as ANSI, and Chinese comments can swallow newlines and break the parser. - Checks:
npm run check(node --check on both JS files);pwsh -File test/tree-test.ps1(unit test for the multi-instance tree resolution against a fake process table);pwsh -File testbed\run-test.ps1 -Scenario A|B|Candpwsh -File testbed\noport-test.ps1(end-to-end runs of the engine against a fake app on ports 19501-19505 — they never touch the real DSH);pwsh -File scripts/restart-dsh.ps1 -Explain -HostPid <pid>(dry look at what a restart would kill on YOUR machine, kills nothing). - Run the testbed in a normal, unrestricted shell. Inside a restricted sandbox WMI is denied
(
CimException: 拒绝访问), the launch legitimately falls back to explorer, and a healthy run then looks like a failure. - Hot deploy while developing: copy the tree into
~/.dsh/profiles/desktop/plugins/dsh-powerand register it viadsh plugin --profile desktop add <path>. Widget changes need only a page refresh (the route re-reads the file per request);lib/index.jschanges need a DSH restart; the.ps1is spawned fresh per action, so it is always live.
History of design fixes (each one maps to a real failure)
- Kill shell + host together — the Electron shell auto-restarts a killed host and races the helper for the web port (EADDRINUSE crash loop).
- Launch via WMI (explorer/scheduler fallbacks) — a helper launched inside the host's Job Object dies with it, so the relaunch never happens.
- Readiness = a real
/api/dsh-power/statusreply from a NEW pid, not "a pid exists". - CSRF header on the power endpoints (a plain cross-origin POST used to reach them).
- Multi-instance safety: stop only our own process tree, resolved from the host pid.
- Foreground push after restart, so a fullscreen game doesn't hide the result.
- schtasks one-shot task deleted right after use (it used to linger forever).
- Settle window after the kill: an Electron shell respawns its host, so a shell killed a moment too late left a young boot behind to race the new instance for the port. Same-named processes younger than the kill are now stopped too, then the port is waited out.
lock-held-by-other/launch-failedverdicts: retrying against a lock another instance owns can never succeed, and the explorer/schtasks paths can report no pid at all — both used to consume the entire start budget (30s per try) before saying anything.- Watch after "OK": readiness was a single observation, so an instance that died or was
restarted a second later still left
ready/okin the status file. The helper now watches for a while (outside the lock) and rewrites the record asdied/teardown/replaced. - Hand-run mode (
-Port 0) no longer polls a budget it cannot satisfy and no longer reports a healthy relaunch asteardownwith exit code 3 — it watches the pid it launched. - WMI calls carry a 20s operation timeout: this helper runs after the old instance is already dead, so a hung CIM call must fall through to the explorer fallback instead of leaving the app stopped with nobody left to relaunch it.
- The status record carries
servingPid(the host pid the port actually named), so "up" names the instance it verified instead of only the pid a launch path reported.
License
MIT (copy of the pattern used by companion plugins).
Comments
Loading…
Similar plugins
by SanYe-SanJiu
Windows-only process control for the harness: a sidebar button that shuts DSH down through the host's own graceful exit path, an app-window / normal-tab switch on the Plugins card that rewrites the de
★ 0
MIT
JavaScript
Sep 24, 2026
dsh plugin --profile web add dsh-power-switchA small self-contained power control for DSH: sidebar power button with an upward restart/shutdown menu and a Windows-shutdown-style overlay. Own restart & shutdown engine (no dependency on other plug
★ 0
dsh plugin --profile web add dsh-power-buttonby mostkia
Power controls (shutdown / restart) for the DSH sidebar plus a Windows tray launcher with start-at-logon.
★ 0
MIT
PowerShell
Oct 5, 2026
dsh plugin --profile web add @mostkia/dsh-launcherby Neptune810
A draggable one-press stop button for the DeepSeek Harness Web GUI. / DSH 一键结束服务按钮。
★ 0
MIT
JavaScript
Sep 23, 2026
dsh plugin --profile web add dsh-power-buttonby keyiadiannao
Self-contained power control for DeepSeek Harness: sidebar power button with upward restart/shutdown menu, Windows-shutdown-style overlay, own restart & shutdown engine (no dependency on other plugins
★ 9
↓ 44/wk
MIT
TypeScript
Oct 7, 2026
dsh plugin --profile web add dsh-power-buttonby Mandarin715
Windows boot autostart and one-click restart for the DSH service, from inside DSH: a windowless HKCU-Run bootstrap entry, a restart button in the settings card, the current tokenised access URL, and a
★ 1
↓ 7/wk
MIT
JavaScript
Oct 1, 2026
dsh plugin --profile web add dsh-autostart