- Home
- Plugins
- Dev & Plugin Tools
- dsh-power-button
dsh-power-button
keyiadiannao/dsh-power-button
A 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 plugins), dynamic-port aware.
Install
dsh plugin --profile web add github:keyiadiannao/dsh-power-buttonREADME
dsh-power-button
A self-contained power & lifecycle controller for DeepSeek Harness: a sidebar power button with a Restart / Shutdown menu and a full-screen transition overlay. The restart engine is built in — no third-party dependencies.
Developed with DeepSeek AI assistance; reviewed before release.
Features
- Sidebar power button in the footer action slot, theme-aware and styled to match the adjacent Settings trigger.
- Restart / Shutdown menu with a Windows-style full-screen overlay; the page reloads by itself after a confirmed restart.
- Self-contained restart engine: a detached
.cjshelper takes over relaunching DSH through an ARM → COMMIT → ACK handshake, waits for the old process, the port and the session logs, then relaunches the same invocation in the same cwd. A--port 0command line is pinned to the port the old process actually bound. In the Desktop app the restart is handed to Electron instead, when that app advertises a restart protocol. No PowerShell, notaskkill. /restartand/shutdowncommands, arestart_harnessmodel tool (registration is skipped when another plugin already owns the name), and a read-onlyrestart_statustool for checking whether a restart happened.- Localized UI and host notices (zh / en), following the profile's
locale.preference. - Startup housekeeping: helper logs and scripts, per-restart handshake records, and undelivered notices older than 7 days are pruned from the runtime directory.
Screenshots
① Sidebar power button — theme-aware footer entry.

② Restart / Shutdown menu — two actions, one click away.

③ Shutdown confirm dialog — the default focus sits on Cancel; only an explicit confirm stops the process.

④ Shutdown progress overlay — shows the current stage while the process winds down.

⑤ Restart completed toast — after the auto reload, a notice confirms DSH is back.

Install
dsh plugin --profile web add "github:keyiadiannao/dsh-power-button#master"
Restart DSH; a power button appears in the sidebar footer. Requires Node ≥ 22.19.
Configuration
Configured through the profile's cordis layer (cordis.patch.yml or the settings UI):
| Key | Default | Meaning |
|---|---|---|
enableModelTool | true | Register the restart_harness model tool. Set false to keep restart on the GUI button and /restart only. |
maxDelayMs | 5000 | Upper bound (ms) for the model tool's delayMs. The effective floor is 1000 ms. |
restartWakeMode | quiet | What the session that asked for a restart is told afterwards. quiet stages the notice for its next step without waking anything; notify also wakes that session once it is live, so the model reports the restart without a user message. Neither mode restarts anything on its own. |
- id: dsh-power-button
config:
enableModelTool: true
How it works
Restart is an explicit ARM → COMMIT → ACK handshake. The old process does not exit until a live helper owns the relaunch; every failure before the ACK keeps the old process running. After spawning the successor, the helper waits up to 180 seconds for readiness. The browser allows a five-minute polling budget for the full restart, including shutdown and preparation. A refused request remains an error even if later health probes fail.
click power → Restart
[host] POST /api/dsh-power-button/restart
write the per-restart helper .cjs (0600: it embeds the relaunch argv)
spawn `node <helper>` detached
[helper] ARM → status { stage: 'armed' }
[host] wait for ARMED (5 s), then write the COMMIT file for this restartId
[helper] ACK → status { stage: 'committed' }
[host] only now: unref the helper, flush live sessions (≤5 s),
request exit after delayMs with a 15 s watchdog behind it
[helper] wait for the old PID to exit, the port to free, the session logs to settle
write the v2 marker, then spawn DSH with the same invocation and cwd
poll /health until instanceId differs and restart.restartId matches
[client] reload
Five invariants hold this together:
- The helper lives outside the process tree (
detached, a real.cjsfile — a multi-linenode -eis mangled by WindowsCreateProcess), so terminating DSH cannot kill it mid-flight. - Success is confirmed by a per-process
instanceIdthat must change, so a brief outage never fakes it. - The launcher's exit channel is read at
ctx.get('appExit'), never as a property: it is an optional host service this plugin does not inject, and a property read resolves toundefined— which would skip graceful disposal and hard-kill instead. - Exit is bounded end to end, and the bounds are derived rather than duplicated: graceful exit gets a 15 s watchdog, the pre-exit flush cap is 5 s, and the helper's patience for the old PID is computed from both, so they cannot drift into the helper giving up first.
- The successor's environment is re-read from the registry, not inherited: a process environment is a snapshot from its launcher, so a variable changed with
setxor System Properties would otherwise never reach the relaunched instance. The block is composed in the order Windows uses — the inherited block as the base layer, machine values over it, then user values shadowing those — and aREG_EXPAND_SZvalue is resolved against the block as it stands at its own stage, so a machine value cannot see a user-only name. That isolation is between registry layers, not a byte-exact replay of how Windows builds the block: names the launcher already carried stay visible at every stage, which is deliberate because some of them —SystemRooton a real machine — exist only in the process environment.PATHkeeps both halves (machine first) and each part is expanded according to its own type before duplicates are dropped. It is read throughreg export(UTF-16LE), so non-ASCII values survive on any console code page; the export is a transient file under the harness home that is deleted in the same call, and it inherits that directory's ACL exactly as.credentials.yamldoes — the file mode is not a privacy guarantee on Windows. A variable deleted from the registry still cannot be seen, and a directory deleted from a registryPATHsurvives while the launcher's block still carries it — it cannot be told apart from one a package manager injected at launch, and dropping those would break children that resolve tools by name. Non-Windows platforms keep plain inheritance.
Shutdown posts /api/dsh-power-button/shutdown and stops without relaunching. It is irreversible, so the button and /shutdown both open a confirm dialog; the model is never given shutdown.
Desktop
The Desktop application runs the Host as a child of its Electron shell, so exiting that child invokes the app's crash recovery and replaying its command line yields a Host without a window or its original IPC channel. Two cases, decided by what the host advertises:
- Host advertises a restart protocol (
DSH_DESKTOP_RESTART_PROTOCOLin the Host environment, IPC channel connected): the plugin writes the restart marker, flushes live sessions, and asks the shell to restart; Electron owns the relaunch. If the shell does not take over within 20 seconds, the request fails, the transition is released, and the button works again. - Host advertises nothing (every released DeepSeek Harness so far): restart and shutdown are refused with
503andreason: "desktop-unsupported"; no helper or exit is scheduled. Use the application's Quit menu or tray action, then reopen it — closing the main window can leave the app running in the tray.
Making the first case available means changing DeepSeek Harness itself, which is outside this plugin's boundary; the steps are in docs/desktop-restart-protocol.md.
Safety
- Destructive POSTs go through the deployment's own browser-trust fence and authentication via
ctx.connection.requestRejection— the call the host's/apiroute uses./api/dsh-power-button/*is longer than the official/apiprefix, so longest-prefix matching never routes these requests through that fence; delegating keeps them under the operator's real policy, including LAN authorities derived from an all-interface bind and anytrustedHosts/--trusted-hostdeclaration. - On a host with no connection service (a profile without the web bundle) the plugin falls back to its own rules:
Hostmust be a loopback authority or one the host declared, plus the cross-site and full-authorityOriginchecks. That fallback is narrower than the official fence and can only refuse; it does not provide browser-session authentication. - A refusal answers with a stable
reasoncode and logs the observedHost/Origin/sec-fetch-site/ remote address.host-fenceandnot-authenticatedmean the deployment decided, not this plugin. - An at-most-once latch rejects duplicate transitions, so a concurrent second POST gets
409. - The model tool's
delayMsis floored at 1000 ms — the model cannot kill the process before its own turn settles. - The restart marker is consumed on boot, so a later ordinary launch never misreports a restart. Command lines are redacted in logs; helper and marker files are written
0600, the runtime directory0700.
Restart awareness
Three audiences, deliberately not one:
- The UI shows a localized "Restarted" toast. The restart identity stays on
/healthfor the process's whole life; acknowledging the toast clears only the pending flag. - The session that asked gets a notice shaped by
restartWakeMode—Agent.inject()whenquiet,Agent.followup()whennotify. Only a restart with a recorded causal session gets one: a GUI click records none and is never attributed to whichever conversation happens to be open. At most one notice per session is queued and it is deleted once delivered. - Every other session can call
restart_status, which reports whether a restart happened, its id, stage, the instances involved, timings, who asked, and whether the running process is the instance that restart produced.
The plugin never appends a model-visible event and never forges a turn: awareness is handed to the Agent API, whose inject() durably stages an agent/inbox/spliced record for the next legitimate step. Delivery is once per notice in normal operation; a crash between the inbox write and the deletion can repeat it, and the notice text is written so a repeat is harmless.
/health also reports appExit: "available" | "missing" — whether the launcher-provided exit channel actually resolves here, which turns a delayed restart into a one-request diagnosis. Both exit paths log the pre-exit flush tally (live / fulfilled / rejected / timedOut), so a refused or partial durable write is visible afterwards instead of looking like a clean exit.
Development
pnpm build # tsdown: host + client bundle (committed to lib/)
pnpm typecheck # tsc --noEmit over src
pnpm typecheck:tests # the root tsconfig excludes tests/
pnpm test # vitest: unit + process-level acceptance
pnpm check # all of the above, in order
CI runs both typechecks, tests and build on Windows, Ubuntu and macOS, gates that the committed lib/ matches a fresh build byte for byte, and checks the pnpm pack tarball; a separate Linux job repeats everything on Node 22.19, the floor engines.node declares. tests/restart-runtime.spec.ts runs the exact helper the host generates as a real process against real TCP ports - the relaunch chain, no relaunch without COMMIT, bounded spawn retries, a stale instance refused, and the failure diagnoses - and the suite isolates DSH_HOME, so it never touches your real ~/.dsh.
License & Attribution
MIT. The "detached helper relaunch" idea follows anweat/dsh-restart (MIT); the implementation is independently written (real .cjs file, no PowerShell, dynamic port), no code copied.
Thanks to @qq1131909103: PR #8 established that the Desktop Host belongs to the Electron shell, which is why this plugin refuses power actions there instead of spawning a competing Host, and what docs/desktop-restart-protocol.md is built on.
Related plugins
deepseek-harness
deepseek-ai/deepseek-harness
dsh-market
dsh-market/dsh-market
dsh-web (dsh-plugin-manager)
zhu1090093659/dsh-web
dsh-web
zhu1090093659/dsh-web