@ray1270/dsh-restart: Adds Refresh and One-Click Restart to DSH Desktop

Introduction

The port of the DSH desktop version is randomly assigned on every startup (--port 0), so every restart is equivalent to changing origins: the old page loses connection, the shell automatically opens a new page, and you get thrown back to the homepage. To “restart and see the effect,” you originally had to close the window, find the icon, wait for startup, and manually click back to the session. Changes to client plugins (slot registration, UI code) normally only need to be reloaded into the page to take effect, but pressing F5 causes the current session to drop back to the homepage—because DSH’s current session only lives in the page, not on the URL.

The client plugin introduced below, @ray1270/dsh-restart, solves both of these issues.

What is this

Released by maintainer Ray1270 (author in package.json is Ray_lv) on npm as a client plugin (dsh.client.platform = web), current version 0.3.7, MIT license. It adds two persistent buttons to the top bar of the DeepSeek Harness Desktop window:

App  Edit  Refresh  Restart

What do the two buttons do

Refresh: Reloads the current page. The Host remains unchanged, session history remains unchanged, and running agents remain unchanged. It simply reinstalls the Web client. After loading, it still stops at the previous session (the session marker is valid for 2 minutes). The cost is that unsent drafts in the input box are lost; sent conversations, currently generating replies, and background jobs are not affected. Since it doesn’t cross origins, there are no network requests involved in this refresh chain.

Restart: Restarts the entire application. The first point is a pre-check: the button displays “Confirm Restart · N items”, where N is the number of agents / jobs / terminals that will be interrupted at this moment; it automatically reverts after 4 seconds if not clicked. The second point is execution: after 1-3 seconds, the application exits and automatically restarts, returning to the same session (valid for 30 minutes). Once confirmed, it is a true interruption: session history is appended, and what is lost is the current round of dialogue and job output.

The reason for “refresh once, restart twice”: refresh doesn’t kill anything on the server side, the biggest cost is just drafts; restart actually interrupts active tasks, so one must be made clearly aware of the numbers before deciding.

When clicked and failed (Restart Failed / Refresh Failed), the button resets after 6 seconds, with the reason in the tooltip.

Installation and Activation

Published to npm, one command:

dsh plugin --profile desktop add @ray1270/dsh-restart

After installation, you must restart the application once for it to load (this time you can only restart manually, the classic chicken-and-egg problem), and after that, you rely on these two buttons.

dsh plugin add goes through pnpm add into the profile and appends the package name to dsh.profile.bundles, so you don’t need to manually edit any files. The “Plugins → Add Plugin” in the app follows the same path, just with an extra input field for the path.

If not using npm, download the latest ray1270-dsh-restart-<version>.tgz from Releases and fill in its absolute path in “Plugins → Add Plugin”. Both paths install the same file—the attachment of the release is byte-by-byte identical to the tgz on npm. To verify yourself:

Get-FileHash .\ray1270-dsh-restart-<version>.tgz -Algorithm SHA256   # Compare with the same-name .sha256 file

Install from source (for development):

dsh plugin --profile desktop add "<absolute path to this directory>"

After installation, you still need to restart the application once.

Configuration

Written in the config: section of the profile loading line (you can also directly edit cordis.patch.yml):

Key Default Meaning
enabled true Global switch for restart; when false, restart reports ‘unavailable’ and rejects all restart requests (Refresh is unaffected)
delayMs 1200 Delay before action, used to ensure HTTP receipts arrive at the browser first
hostGraceMs 8000 Timeout for Host to exit gracefully; forced kill only if it times out
killTimeoutMs 15000 Hard limit to wait for the old process to disappear completely
pendingTtlMinutes 30 Validity period of the ‘which session to return to after restart’ marker

Refresh has no configuration items: it doesn’t touch the Host, so there are no configurable dangerous actions.

How Refresh Works

Purely client-side, Host does not participate:

  1. Write the current sessionId into localStorage (same origin, not lost on reload);
  2. location.reload();
  3. When the page reloads, the client plugin re-fetches and remounts, reads the marker and calls uiWorkspace.openSession(sessionId), then deletes the marker.

Refresh doesn’t cross origins, so storing the marker in browser storage is enough; there is no need to go through the Host file.

How Restart Works

Scheduled by the Host side, the helper is started by the WMI service, bypassing the application’s own process tree. The overall flow:

  1. Browser POST /api/dsh-restart/restart { sessionId, force };
  2. Host performs an active work check (only passes if force), writes pending-restore.json (the session to return to after restart);
  3. Spawns a short-lived powershell, calls Win32_Process.Create → WmiPrvSE.exe creates helper.ps1 (using Win32_ProcessStartup’s ShowWindow = 0 to make it born without a window);
  4. Waits for helper to write the boot status before returning 200;
  5. Helper verifies that the ShellPid’s image path is still equal to the target exe (prevents PID reuse), taskkill /F only kills the Electron shell (without /T), waits for Host to exit gracefully (max 8 seconds), then Start-Process restarts the exe (retry once on failure and verify the new process is alive), and writes the result to restart-state.json;
  6. Once the shell dies, Host’s IPC pipe disconnects, and Host runs its own application.shutdown.shutdown(0) to exit gracefully (5 second limit).

Several key points:

  • Only kill the shell, not the tree. Host’s child processes register process.once("disconnect", () => stop()), so when the shell dies, they automatically complete the full dispose flow. Thus, the “hard kill” is only hard on the shell itself.
  • Helper must stay alive past the moment the shell dies. This application’s child process tree does not survive past the shell; ordinary child processes cannot do this (manifesting as “only closing, no opening”). Win32_Process.Create is executed by WmiPrvSE.exe, and the helper’s parent process is the WMI service, so it remains unharmed when the shell dies; the boot line in helper.log will record parent=…(WmiPrvSE).
  • Don’t use detached: true. On Windows, starting powershell.exe with Node using detached causes it to exit immediately with code 0, executing not a single line. What truly keeps the helper alive is “who created it”.
  • Give up on restart if WMI startup fails. The short-lived launcher writes the result into launch-result.json; if Host cannot receive the helper’s boot or a clear rejection from WMI, it returns 500 and writes a cancel-restart marker. The helper in the delay will see this and immediately give up.
  • Created process must be born hidden. -WindowStyle Hidden won’t save it (that hides the window after it exists), you must construct Win32_ProcessStartup and pass it to Create.

The built-in safe restart in the ecosystem is ctx.appExit(0), but on the desktop version, it effectively commits Host suicide, the shell judges it as abnormal termination and pops up a “Application stopped unexpectedly” recovery box, requiring one more click and leaving a fake crash in the logs. This plugin chooses “kill shell + relaunch”, so it can be done in one click.

Troubleshoot by likelihood:

  1. No response when clicked, app still running — helper didn’t start (WMI disabled/blocked, script deleted). The app is completely unaffected. First check ~/.dsh/dsh-restart/launch-result.json, then check helper.log.
  2. App exited but didn’t come back — helper died before killing, or Start-Process was blocked. Manually double-click D:\dsh\DeepSeek Harness.exe; restart-state.json will show which step it stopped at.
  3. Returned to homepage after clicking “Refresh” — the session marker failed to write (incognito mode, storage disabled by policy) or the session was deleted; refresh chain failures will silently return to the homepage.
  4. Want to bypass all this — right-click tray exit, then double-click the icon.

State files are all in ~/.dsh/dsh-restart/: restart-state.json (last restart stage), helper.log (process log with timestamps), launch-result.json (WMI creation result), pending-restore.json (pending session marker, deleted after consumption), cancel-restart (briefly appears only when Host gives up on startup).

Applicable Scenarios and Notes

Suitable for debugging client plugins in the DeepSeek Harness Desktop version or for those who often need to restart the app to see effects. Refresh is usable in any environment; Restart only supports the Desktop version (CLI / headless Host has no shell to restart, button will explicitly report “Unsupported”).

A few notes:

  • The plugin runs with the permissions of the current dsh process; it is recommended to review the source code and license (MIT) before installing.
  • The top bar entry relies on the desktop shell’s own menu bar (preload uses data-windows-menu marker + open shadow root); the plugin can only append sibling buttons behind it; if not detected, it silently fails to mount and falls back to the far right of the session title bar. Both entries share the same logic, with the top bar version taking priority.
  • Only one entry is visible on screen at a time: when the top bar mounts successfully, the pair in the title bar renders as empty but still hangs (it is the only place that can get sessionId, used for filling in the session after refresh/restart).
  • Restart relies on the WMI service. If WMI is disabled by policy, or Win32_Process.Create is blocked by security software, restart will explicitly reject (the app is unaffected), and the error and return code are written to launch-result.json.
  • Antivirus might be curious: “PowerShell with hidden window killing the app and relaunching” is a classic heuristic for EDRs. The helper is plaintext, fully commented, fixed path, and logs are verifiable; no encoding obfuscation was done.
  • In extreme cases, a console window might still appear: if Win32_ProcessStartup construction fails on a certain machine, the helper will exit and start with a visible window (restart itself proceeds normally), and windowHidden in launch-result.json will be false.

Summary

Refresh solves “client changes only require reloading the page without losing the current session,” and restart solves “being thrown back to the homepage after restart.” Both are session-centric, with cost prompts and failure reasons written on the UI and in the logs.

Directory: https://www.skillhub.cn/plugins/Ray1270/dsh-restart
Repo: https://github.com/Ray1270/dsh-restart
npm: https://www.npmjs.com/package/@ray1270/dsh-restart

羽毛球分组比赛记分
小程序二维码

欢迎使用《羽毛球分组比赛记分》微信小程序

Xiaoye