Introduction¶
DSH’s desktop client is an Electron shell. After installing or stopping plugins, or when wanting to adjust the window and right sidebar sizes, the conventional approach is to manually quit the process and reopen. The official interface does not have a built-in “Restart” entry; desktop-side Cookie entries dsh-auth-* keep adding up without being overwritten, eventually causing the request header limit to be exceeded. The server returns 431, and the client only displays a single “Authentication Failed” message without showing the cause. The default window size is too small, and the right sidebar is too wide, with no exposed settings.
dsh-restart is an add-on plugin addressing these three issues, maintained by Abin1020, categorized under client-side. It does not perform ASAR patching or modify the official installer, so nightly updates will not overwrite it.
What is this¶
One-sentence positioning: A client-side plugin attached to the DSH profile, providing three capabilities: an in-page restart button, Cookie cleaning, and custom window and right sidebar sizes.
License is MIT, version 0.1.1 in package.json, engines.node requires >=18. The entry points are split: lib/index.js is the Host side main, client.js is the client bundle entry, injected via cordis.patch.yml.
The core problem it solves is: The desktop lacks a safe “Restart and Clean Up” path, nor a window size entry without modifying app.asar.
Core Features¶
Feature 1: Cookie Cleaning¶
The reason is that dsh-client-connection includes the Host’s random port when generating cookie names, but the cookie scope only recognizes the host, not the port:
// node_modules/@deepseek-ai/dsh-client-connection/lib/index.js
const COOKIE_PREFIX = "dsh-auth-";
function cookieName(authority) {
return COOKIE_PREFIX + encodeBase64Url(createHash("sha256").update(authority).digest());
}
Thus, every startup gets a new port → new cookie name → adds a new entry, never overwriting. The actual value given in the README is that accumulating to 71 entries is about 16.1 KB, exceeding Node’s default 16384 byte limit. There are three upstream reports: #10802, #10828, #11095.
Runtime cleanup is impossible: While Electron is running, Network/Cookies is exclusively locked by Chromium. The README records the actual results of four attempted paths:
| Method | Result |
|---|---|
fs.openSync(p, 'r') |
EBUSY |
fs.openSync(p, 'r+') |
EPERM |
new DatabaseSync(p, { readOnly: true }) |
unable to open database file (errcode 14) |
file:...?immutable=1 |
Same failure |
Therefore, cleanup happens after the app exits, executed by a detached helper. The SQL is just one line:
DELETE FROM cookies WHERE name LIKE 'dsh-auth-%'
A few constraints worth remembering:
- Back up to
Network/dsh-tools-backup/Cookies.<timestamp>.bakbefore deleting, rolling to keep the latest 3. - If backup fails, do not delete; better to leave it than to delete it bare.
- After deleting,
VACUUMand re-read for verification, and write the actual number of deleted entries intolast-run.json. - Only match the
dsh-auth-prefix; do not touch cookies from other sites.
Feature 2: In-page Restart Button¶
There is a loop arrow in the top right of the title bar, to the left of the minimize button; clicking it triggers a restart. All three features are together in “Desktop Maintenance” on the settings page.
The restart chain state flow is as follows:
Page button → POST /dsh-restart/restart → Host returns 202
→ detached helper starts
→ Wait 800ms (let response return to page first)
→ taskkill /PID <Electron> /F
→ Clear cookies
→ Wait 1200ms
→ Relaunch DeepSeek Harness.exe
A few key trade-offs:
- Do not use
process.exit()directly in the Host. If the Host exits itself, the main process will detect it as a crash and pop up a native crash dialog (DesktopHostProcess.fail→reportFatal). - Kill only the Electron main process, no
/T. The helper is a child process of the Host, which is a child process of Electron./Twould kill the helper as well. Killing only the main process is safe: once it exits, the Host’s IPC disconnects, andprocess.once("disconnect", stop)handles the cleanup itself. - Verify identity before acting.
tasklist /FI "PID eq <pid>"checks that the image name is notdeepseek harness.exe; if so, abort to prevent accidentally killing unrelated processes when a PID is reused.
The restart button is not available immediately after the first installation because the button itself is loaded at that moment, so the first effective restart must be a manual exit and reopen.
Feature 3: Window Size and Right Sidebar Width¶
Neither requires modifying app.asar.
The right sidebar uses the real layout:
ctx.slots.entries("root") finds the entry registered for layout,
use entry.store.create().actions.setRightbar(px) to modify layout directly
The upper limit is min(viewport × 0.7, viewport − 280 − 400), the lower limit is 300, and exceeding it is truncated by clampWidth. Why not use ctx.layout: LayoutController only exposes openRightbar / closeRightbar / toggleSidebar, there is no setRightbar, and the width is written into an inline gridTemplateColumns, which CSS cannot override. Why not slots.inject("root"): root is a single slot, and the catalog explicitly states that double registration will shadow the entire AppFrame, so only entries("root") can read existing entries. The left sidebar remains unchanged, keeping the default 280.
The real work for the window side is done by lib/winagent.exe. The window.resizeTo / moveTo in the render layer is just a fallback. The reason is that the client bundle executes about 10.4 seconds after navigation starts, at which point the window is already show(); in Electron, document.visibilityState reports visible for hidden windows as well, so using it to judge “not yet displayed” is invalid. The Host’s apply() definitely completes before navigateMain(), so launching an independent process from the Host side to manipulate the window has about ten seconds of buffer.
main.js:12889 createWindow(appPreload, false, true) ← show = false
main.js:12937 enterWorkspace(): await navigateMain(url) 之后才 window.show()
main.js:13128 mainWindow = createMainWindow()
└── 里面 new DesktopHostProcess(...) ← Host 先起来,页面后加载
Key implementation points of winagent.exe (source code is in the same directory winagent-src.cs):
- Find window by
--pid:EnumWindows+GetWindowThreadProcessId. Only recognize the class name prefixChrome_WidgetWin_1. Pick the one with the largest area (there is a hiddenChrome_WidgetWin_0in the same process). - Poll once every 40 ms, max 30 seconds; if the parent process disappears first, write
parent-goneand exit directly, leaving no orphans. - Border calculation is not hardcoded: First
DwmGetWindowAttribute(DWMWA_EXTENDED_FRAME_BOUNDS)to get the visible bounds, useGetWindowRect.Width - visible.Widthto calculate the invisible border of the window at that moment, and add it. So--width 1690results in a visible width of 1690. - Max 2 rounds of
SetWindowPoscorrection,|actual - target| ≤ 2is considered accurate.
It is a one-off detached + unref process, sets up and exits, and does not persist.
The README records two actual measurements: In the real restart log, waitedMs is 3–5 ms; on the Host side, 20 samples of spawn itself were: min 12.2 ms, p50 17.4 ms, max 27.9 ms, avg 17.8 ms. Compared to the main window taking about 10.4 seconds to go from creation to show(), the overhead accounts for about 0.17%.
The “Startup Recovery” line on the interface prioritizes the agent record: visibleAtApply === false means it completed before the window was displayed, so no visible jitter; true means it arrived after the window was already displayed. If there is no agent record, it falls back to the render layer’s boot.log diagnostics. If neither exists, it prompts “No startup record yet”.
Installation and Usage¶
It is recommended to go through the plugin page. Open Settings → Plugins, click Add Plugin, and fill in Package name or address:
https://github.com/Abin1020/dsh-restart
Click Install, and after installation, completely exit DSH and reopen.
During installation, DSH will automatically register this plugin into the profile’s dependencies and dsh.profile.bundles, so there is no need to manually edit any files. The plugin takes effect after restarting, but the first restart must be a manual exit and reopen—the restart button is loaded at that point.
For offline packages or when modifying code, use manual installation:
# 1. Put it in the plugin directory
git clone https://github.com/Abin1020/dsh-restart.git ~/.dsh/plugins/dsh-restart
# 2. Register in profile's package.json
# ~/.dsh/profiles/desktop/package.json
# - dependencies add "dsh-restart": "file:../../plugins/dsh-restart"
# - dsh.profile.bundles add "dsh-restart" at the end
# 3. Create link (profile uses pnpm's nodeLinker: hoisted, it won't create this link for you)
# Windows PowerShell:
# New-Item -ItemType Junction `
# -Path "$env:USERPROFILE\.dsh\profiles\desktop\node_modules\dsh-restart" `
# -Target "$env:USERPROFILE\.dsh\plugins\dsh-restart"
You also need to restart DSH once.
Confirm installation: Open http://127.0.0.1:<current_port>/dsh-restart/status in a browser or PowerShell; returning 200 means loading was successful. The port is in the startup log or in the address bar.
Uninstall via the plugin page Uninstall. DSH will clear the registration in dependencies and dsh.profile.bundles, and you need to restart once again to fully uninstall. For manually installed versions, you need to clean up yourself:
# 1. Remove from profile (replace ~ with your user directory)
# ~/.dsh/profiles/desktop/package.json
# Delete "dsh-restart" from dependencies
# Delete "dsh-restart" from dsh.profile.bundles
# 2. Delete link
rm ~/.dsh/profiles/desktop/node_modules/dsh-restart
# 3. Delete plugin and data (optional)
rm -rf ~/.dsh/plugins/dsh-restart ~/.dsh/dsh-restart
The backup cookie database is at %APPDATA%\@deepseek-ai\dsh-desktop\Network\dsh-tools-backup\, delete it manually if needed.
Typical Usage¶
Adjust the window and right sidebar, and save the layout. The layout is written to $DSH_HOME/dsh-restart/layout.json:
{ "window": { "width": 1600, "height": 1000, "x": 40, "y": 30 }, "rightbar": { "width": 520 } }
If either item is null or missing, it means do not override and use the application default.
Configuration items are in the profile’s cordis.patch.yml:
- insert:
- id: dsh-restart
name: 'dsh-restart'
config:
cleanOnEveryRestart: true
keepBackups: 3
restartLaunchDelayMs: 1200
killDelayMs: 800
dryRun: false
The working mode of the three features can be summarized as one sentence: Restart is the trigger point, cleanup is the side effect.
- Every time you click restart, cookies are cleared by default, ensuring there is only ever 1 entry in the database.
- No timers are used. During runtime, the cookie database is locked by Chromium, so it cannot be cleaned; but “must clear on every restart” already guarantees that no second entry will accumulate.
- Cleanup definitely happens between “process exited” and “new process started”, which is the only time it is safe to open SQLite.
- Layout preferences also follow the same chain: adjust → save as default → click restart → when the new process starts, the client reads
layout.json, and the agent applies it beforeshow().
Applicable Scenarios and Notes¶
Suitable for these situations:
- Encountered “Authentication Failed” but couldn’t find the clue of 431.
- Need to frequently install, stop plugins, or change configs, making manual exit and reopen very frequent.
- Feel the default window is too small and the right sidebar too wide, and don’t want to modify
app.asar.
A few notes:
- The plugin runs with the current DSH process permissions and can read/write your user-data directory. It is recommended to read the source code and the MIT license before installing.
- It depends on
lib/winagent.exe, which is a build artifact;scripts/build-winagent.mjsis the build script. When verifying versions, rely on the source code andwinagent-src.cs. - Restarting will kill the Electron main process, so please save unsaved content in dialog boxes first.
- This plugin belongs to the community directory and has no official affiliation with DeepSeek / Huansuan; the DSH ecosystem advocates “Everything is a plugin”, and this directory is an independent site, not the official app store.
Conclusion¶
Its value lies not in looking at a single feature, but in turning “Restart” into a reusable chain: Restart naturally comes with one cleanup and the application of a new layout, and it does not touch app.asar at all, so nightly updates will not wipe it out.
Directory page and source code:
- https://www.skillhub.cn/plugins/Abin1020/dsh-restart
- https://github.com/Abin1020/dsh-restart