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
# from GitHub (first run asks for allowBuilds approval — follow the hint, retry)
dsh plugin --profile web add github:keyiadiannao/dsh-power-button
Any plugin you install runs third-party code with your own permissions — it can read your files, use your credentials, and reach the network, and tool approvals don’t sandbox it. GitHub-sourced plugins also run build scripts at install time — pnpm blocks those until you allow them, so an install can stop with ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED or ERR_PNPM_IGNORED_BUILDS; dsh prints the exact key to add under allowBuilds in your profile’s pnpm-workspace.yaml, and the install works on the next run. Allowing a build is a trust decision: only install sources you trust, and pin a commit (github:owner/repo#sha).
README
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.
Links
More in this category
yjh051108/dsh-routing-suite★ 6994
One repository, three parts: a runtime injector for DSH plugin packages (inject, hot-reload, unload, promote a dev staging tool to the front, route self-heal, plus a settings-page plugin manager that lists, unloads and drags folders in to internalize), a task-aware reasoning-mode router agent preset (router-standard / router-spec / router-react), and a graded two-level task protocol whose six tools (commit_star, lock_stage, revise_do, edit_plan, mark_task, redteam_verdict) pin task state to disk. The injector implementation ships in-tree, so the install carries its own behaviour rather than a dependency list.
strukto-ai/mirage#dsh★ 3762
Swaps the filesystem and bash providers for a mirage virtual workspace: file tools and shell commands run over mounted resources (RAM, S3, Redis, Slack, Gmail, Notion, Postgres) instead of the host disk, with per-mount read/write/exec modes, per-command sandbox routing (monty, pyodide, quickjs in process; docker, e2b, daytona remote), and installed CLIs (git, gh, slack, linear, ntn, gws, or one you register) as head words in the virtual terminal.
first-fluke/oh-my-agent#dsh★ 1357
Runs project OMA checks before and after tool execution and when a turn tries to finish. Installs its OMA CLI dependency and initializes unconfigured projects on first use. Requires Node.js 22+; initial setup needs network access and project write access.
MichengAI/dsh-skills-manager★ 444
Manages local DeepSeek Harness skills in Settings and shows shared Agent skills as read-only.
hust-open-atom-club/oh-dsh★ 324
Community distribution: TUI, desktop, and Web UI as one bundle with layered installation.
Fishquito7/dsh-skill-mcp-panel★ 248
Manages DSH skills and MCP servers from the web settings: skill cards with hot enable/disable, workspace scopes, groups, batch migration and drag-and-drop import, plus stdio/HTTP MCP CRUD with connection tests, secret redaction and the unified dsh-panel CLI.
Community comments
Comments are public GitHub Discussions. Loading them connects to GitHub and Giscus; a GitHub account is required to post.