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:
- Write the current
sessionIdinto localStorage (same origin, not lost on reload); location.reload();- 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:
- Browser
POST /api/dsh-restart/restart { sessionId, force }; - Host performs an active work check (only passes if
force), writespending-restore.json(the session to return to after restart); - Spawns a short-lived powershell, calls
Win32_Process.Create→WmiPrvSE.execreateshelper.ps1(usingWin32_ProcessStartup’sShowWindow = 0to make it born without a window); - Waits for helper to write the boot status before returning 200;
- Helper verifies that the ShellPid’s image path is still equal to the target exe (prevents PID reuse),
taskkill /Fonly kills the Electron shell (without/T), waits for Host to exit gracefully (max 8 seconds), thenStart-Processrestarts the exe (retry once on failure and verify the new process is alive), and writes the result torestart-state.json; - 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.Createis executed byWmiPrvSE.exe, and the helper’s parent process is the WMI service, so it remains unharmed when the shell dies; the boot line inhelper.logwill recordparent=…(WmiPrvSE). - Don’t use
detached: true. On Windows, startingpowershell.exewith Node usingdetachedcauses 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’sbootor a clear rejection from WMI, it returns 500 and writes acancel-restartmarker. The helper in the delay will see this and immediately give up. - Created process must be born hidden.
-WindowStyle Hiddenwon’t save it (that hides the window after it exists), you must constructWin32_ProcessStartupand pass it toCreate.
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:
- 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 checkhelper.log. - App exited but didn’t come back — helper died before killing, or
Start-Processwas blocked. Manually double-clickD:\dsh\DeepSeek Harness.exe;restart-state.jsonwill show which step it stopped at. - 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.
- 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-menumarker + 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.Createis blocked by security software, restart will explicitly reject (the app is unaffected), and the error and return code are written tolaunch-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_ProcessStartupconstruction fails on a certain machine, the helper will exit and start with a visible window (restart itself proceeds normally), andwindowHiddeninlaunch-result.jsonwill befalse.
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