dsh-power-button
A DeepSeek Harness Web plugin: one draggable power pill in the GUI, with a live status dot
and restart / stop actions for the running dsh web service.
Host-only, zero dependencies, no build step.
Why
dsh web keeps running until something stops it. A deployment that wraps it in a watchdog
usually waits out an idle window before tearing the server down, so the port stays occupied for
a while after the last browser tab closes, and a relaunch in that window re-opens a page whose
server is about to disappear.
This plugin puts both actions where the user already is — one pill in the Web GUI, with a dot that says whether the host is answering.
Use
A small translucent pill sits in the configured corner (bottom-right by default).
- The dot — green when the host answered the last liveness poll, red when it did not.
Polling is a tiny same-origin
GETevery 5 seconds and costs no model tokens. - Press the pill — a small menu opens with two actions:
- 重启服务 (Restart) — the host relaunches itself in place and a new page opens automatically once the port answers again.
- 结束服务 (Stop) — the process ends immediately and the port is released.
- Pressing the pill again, or clicking anywhere else, closes the menu.
- Set
restart: falseand a press stops outright, with no menu at all.
- Drag the pill — move it anywhere, with mouse or touch. The position is stored in
localStorageand restored on the next load. - Dragging never fires an action: pointer travel past 4px marks a drag, and the click the browser emits afterwards is swallowed.
- The pill is clamped inside the viewport — and re-clamped when the window shrinks — so it is always reachable.
- It lives outside the application's React root, so a re-render cannot remove it.
How it works
The plugin is host-only. No client bundle is built and no @deepseek-ai/* package is
imported: a plugin installed with link: is resolved by Node to its real path, where the
harness packages are not reachable. It uses two extension points the Web carrier already
exposes:
| Extension point | Used for |
|---|---|
webServer.tapIndex() |
injecting the pill's markup, CSS, and script into index.html |
webServer.register() |
serving /dsh-power-button/status, /restart, and /stop |
Stopping calls the launcher's ctx.appExit — the same bounded shutdown path Ctrl+C uses:
dispose the application tree, then let the process exit. That path only ends the process if the
event loop actually drains, so the plugin also arms an unreferenced 2-second watchdog: it
never delays a clean exit, and it guarantees the process is gone even when another plugin is
still holding a handle.
Restarting cannot be done by the host, because the host is the thing going away. The restart route spawns a detached helper — the same Node binary running an inline module — which
- waits for this process to disappear,
- waits for the port to stop answering,
- starts the recorded boot invocation again with the same environment and working directory,
- and, when asked, opens the page as soon as that port answers.
The helper outlives the host (detached + stdio: "ignore" + unref()), so the relaunch
survives the shutdown that triggered it.
Measured on Windows 11 / Node 24: a stop releases the port ~0.2–0.5 s after the POST returns 200; a restart brings the port back in a few seconds, and the failure mode is safe — if the helper cannot be spawned, the route answers 500 and the service is left running.
Security
All three routes require every one of:
- the custom request header
x-dsh-power-button: stop— a cross-origin page cannot send it without a CORS preflight this server never approves, so no random website can restart or stop your server; - a loopback
Host(127.0.0.1/localhost/[::1]), which closes the DNS-rebinding variant; - an
Origin, when the request carries one, matching that Host.
Anything else is 403. status answers only GET/HEAD; stop and restart answer
only POST — a non-matching method is 405.
Nothing outside the profile is touched: the restart helper runs the same boot invocation this
process was started with, or exactly the restartCommand you configured.
Config
Every key is optional. Set them on the plugin row:
`@yaml
- insert:
- id: power-button name: dsh-power-button config: label: 结束服务 # pill text and tooltip position: bottom-right # starting corner, or bottom-left draggable: true # false pins it to that corner status: true # false removes the dot and stops polling statusPollMs: 5000 # liveness poll interval, minimum 1000 restart: true # false removes the restart action and the menu restartCommand: [] # argv; empty = resume this same boot invocation restartOpenBrowser: true # false when the command opens the page itself restartWaitMs: 30000 # how long the helper waits for the port before giving up enabled: true # false injects nothing
`@
Restarting through a launcher
By default the helper resumes this invocation — the same Node binary, script, and arguments. That is the only thing the plugin can know for certain, and it means a relaunched host has no wrapper watching it: it stays up until you stop it.
If your deployment is started by a launcher that must come back too — for instance one with its
own idle watchdog — point restartCommand at it and let it own the browser:
`@yaml
- insert:
- id: power-button name: dsh-power-button config: restartCommand: - 'C:\path\to\YourLauncher.exe' restartOpenBrowser: false
`@
Install
@sh dsh plugin --profile web add dsh-power-button @
Restart dsh web afterwards: a plugin change never reaches an already-running process.
Test
@sh node --test test/plugin.test.js @
No test dependencies. The injected page script is executed for real against a hand-built DOM —
press versus drag, the movement threshold, clamping, persistence, the menu, the restart action,
and the status poll — and the routes are driven through fake node:http request/response
pairs, including the compression path that silently drops res.end callbacks. The detached
helper's source is syntax-checked as ESM so a broken relaunch cannot ship.
License
MIT
No comments yet. Be the first to write one.