A native desktop shell for DeepSeek Harness.
Rust + Tauri 2. It supervises the local dsh service, reclaims every process it
spawns, and never forks the upstream project to do it.
Under 4 MB per installer · all artifacts and checksums · 简体中文
One click from a registry listing to a layer in the harness's profile — read the manifest, install through the harness's own plugin command, switch it off again without uninstalling it.
| ⟳ Supervised, not just launched Backoff restart when it exits, and a real HTTP probe every 10 seconds to catch the harness that is alive but wedged. A restart lands on a new port and the window follows it. |
⛨ Nothing outlives the window Every child joins a Windows job object or a POSIX process group, so the kernel reclaims the whole tree — including the grandchildren a plain kill would orphan, and even if the shell is killed outright. |
| ⬗ A plugin marketplace in the window Search the npm registry, see what a package declares before you commit to it, install into the hosted profile through the harness's own command. Disable a plugin without uninstalling it. |
▣ Your phone, without putting the agent on the networkdsh stays on loopback, and that is not configurable. What opens is a separate gateway on one LAN address, paired by a QR code good for one device and two minutes. |
dsh is a local web service. Running it from a terminal works, but it leaves you
managing a process by hand: finding a free port, noticing when it dies, and
cleaning up the tool subprocesses it leaves behind when it does.
DSH Studio makes that a window. The design goal is that the shell should be boring — it starts the service, keeps it alive, and stays out of the way of the harness UI.
Supervises, rather than just launches. The service runs under a supervisor that restarts it with backoff when it exits. A restart lands on a new port and the window follows it — no stale bookmarks, no manual re-launch.
Notices a service that is alive but wedged. Watching the process is only half the job: a server that has stopped answering still has a live PID, and a TCP connect still succeeds because the kernel completes the handshake from the listen backlog. So the supervisor sends a real HTTP request every 10 seconds. Three consecutive misses and the harness is recycled.
Reclaims the whole process tree.
The harness spawns tools, which spawn their own children. On Windows the service
is launched into a Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, so the
kernel tears the tree down even if the shell is killed outright. On Unix it gets
its own process group and is signalled as a group. Closing the window leaves
nothing behind.
Picks its own port.
--port 0 asks the OS for an unused one and the supervisor reads back the port
the service actually bound. There is no configured port to collide with, and no
scan-for-a-free-port race between the check and the bind.
Brings its own Node, if the machine has none.
Being sent to nodejs.org and told to come back is where most people stop. So the
row that says Node is missing is also a button: it reads the current LTS from the
official release index, downloads that build, checks it against the published
SHA-256 before unpacking it, and only calls it installed once the unpacked binary
answers --version. It lands inside the app's own data directory — nothing is
added to PATH, nothing is written to the registry, and deleting the directory
undoes it. There is a second mirror for places where nodejs.org is slow, serving
the same bytes.
Installs the harness for you.
If @deepseek-ai/dsh is not on the machine, the row that says so is a button.
It runs npm install against a private prefix inside the app's data directory —
invoking npm-cli.js through the detected Node binary directly, never through a
shell — and streams the output into the window while it works.
Hosts the harness instead of replacing it. The harness UI is loaded in a frame under the shell's own title bar, so the window stays movable and closable, and switching to the control panel does not throw away a running session. Nothing about the upstream project is patched or vendored.
Extends it through its own plugin system. There is a marketplace in the window: search the npm registry, read what a package declares before you commit to it, and install into the harness's hosted profile. Installation goes through the harness's own plugin command rather than around it — no private side channel into somebody else's config. A package whose manifest declares a profile patch becomes a layer the harness loads; one that does not is labelled the plain library it is, instead of appearing as a plugin that mysteriously did nothing. An installed plugin can also be switched off without being uninstalled, so "is this the one breaking it?" costs a click rather than a download.
Reaches your phone without putting the agent on the network.
Remote access is off until you open it, and opening it does not move the
service — dsh stays bound to loopback, which is not configurable. What opens is
a separate gateway, bound to one LAN address. Pairing is a QR code, and the code
inside it is good for one device and two minutes: scan it and that phone walks
away with a credential of its own, which is what every request after it carries.
Everything from there is spliced straight through to the harness. Paired devices
are listed on the pane, and forgetting one revokes its credential and drops the
connection it already had. Close the door and all of them die with it.
Open the door, scan once, and the code is gone — the phone that redeemed it keeps a key of its own, listed on the pane and revocable on its own.
One process owns everything. The Rust side supervises the service and the WebView renders the shell; the harness itself is loaded from its own origin, so what you see is the real upstream UI rather than a re-implementation of it.
flowchart LR
phone["phone on the same network"]
subgraph app["DSH Studio — one process"]
ui["WebView<br/>React shell UI"]
sup["Rust supervisor<br/>backoff · readiness · health"]
gw["remote gateway<br/>one LAN address · per-device keys"]
ui <-->|Tauri IPC| sup
ui <-->|Tauri IPC| gw
end
sup ==>|"spawn: node dsh web --port 0"| dsh
subgraph guard["proc-guard — Job Object (Windows) / process group (Unix)"]
dsh["dsh service<br/>127.0.0.1:ephemeral"]
t1["tool subprocess"]
t2["tool subprocess"]
dsh --> t1
dsh --> t2
end
ui -.->|iframe loads the origin| dsh
sup -.->|"HTTP probe every 10s"| dsh
phone -.->|"paired by QR, then cookie"| gw
gw ==>|"spliced, once the credential checks out"| dsh
The startup sequence is worth spelling out, because every step exists to remove a failure the terminal version has:
- Detect. Every Node on the machine is probed with
--version— including the ones a version manager installed but never put onPATH. The newest one that meets the minimum wins. - Install, if needed.
@deepseek-ai/dshgoes into a private prefix under the app's data directory. Nothing is written to your global npm root. - Launch. The service is spawned into a Job Object (Windows) or its own
process group (Unix), with
--port 0so the kernel assigns the port. - Read back. The supervisor parses the readiness line the service prints and learns the port it actually bound. No guessing, no scanning.
- Host. The window loads that origin in a frame, and keeps probing it over HTTP. Three consecutive misses and step 3 runs again.
The control panel after all five steps: what was detected, what the kernel
handed out, and the service's own output. Every image on this page is captured
from the shipped UI by the deterministic script in media/,
against a stand-in backend — which is why no real home directory, LAN address or
pairing key appears in any of them.
Grab an installer from Releases. Every tagged version is built by CI for four targets:
| Platform | Artifact |
|---|---|
| Windows x64 | .exe (NSIS, per-user install — no admin prompt) and .msi |
| macOS Apple Silicon | .dmg |
| macOS Intel | .dmg |
| Linux x64 | .AppImage, .deb, .rpm |
Or through a package manager. The manifests all live in packaging/
and are generated from a real release, so the version and the SHA-256 in them are
never hand-typed:
scoop bucket add dsh https://github.com/Moresyl/dsh-studio
scoop install dsh-studiowinget, Homebrew Cask and AUR manifests are written and validated but not yet
submitted to their registries — packaging/README.md says
exactly what each one is still waiting on.
Signing. The release pipeline signs macOS builds with an Apple Developer ID, notarizes and staples them, and signs the Windows installers through Azure Artifact Signing. Both are conditional on the credentials being configured, so a fork's build comes out unsigned rather than broken. Releases up to and including v0.4.0 were cut before this existed — on macOS the first launch is blocked by Gatekeeper, so approve the app in System Settings → Privacy & Security.
Downloading from a mirror rather than from GitHub? Releases carry a
SHA256SUMS.txt; packaging/MIRRORS.md covers checking a
download against it, and why the checksum has to come from GitHub even when the
bytes did not.
No Node.js on the machine is fine — the app installs one for you. What changed between versions is in the changelog.
Early. The Windows path is built and verified end to end; the rest is honest about being unfinished.
| Environment detection, one-click install | ✅ |
| Supervisor, backoff restart, health probing | ✅ |
| Process-tree reclamation (Windows / Unix) | ✅ |
| Harness hosting, log console, English + 中文 | ✅ |
| Plugin marketplace — install, switch, remove | ✅ |
| Remote access, single-use QR, revocable keys | ✅ |
| Signed in-app update, checked on a schedule | ✅ |
| Verified on Windows 11 | ✅ |
| macOS / Linux rendering | ⏳ not yet run |
| Node runtime fetched and verified on demand | ✅ no system Node needed |
Code signing, notarization, SHA256SUMS.txt |
✅ in the pipeline, from the next release on |
| Download page, five packaging channels | ✅ Scoop live; four written, not yet submitted |
| Tray icon, close-to-tray while serving | ✅ |
| Native context menus, saved window bounds | ✅ |
| Light and dark, following the system or not | ✅ |
| Silent self-update | ⏳ planned |
| Packaged releases | ✅ automated for Windows, Linux, and macOS (Intel + Apple Silicon) |
Three decisions shape everything else here, and each one gives something up.
The upstream service is hosted, not forked. Vendoring the harness into this repository would buy direct control of its UI, at the price of merging every upstream release forward forever. Hosting it unmodified gives up that control — the plan for extending the UI is to go through the harness's own plugin system rather than around it — and takes upstream updates for free. A plugin installed from the shell's own marketplace is the supported way to change what the harness does.
Shutdown is the kernel's job, not a signal's. Killing the process you spawned does not kill the tools it spawned, and on Windows there is no process group to fall back on — so a shell that crashes can strand a compiler, a test runner, or a language server nobody can now see. A Job Object makes the kernel responsible for the whole tree, which is why closing this window is enough even when the closing was not graceful.
The service stays on loopback; reach is a separate, authenticated door. Binding an agent that can run shell commands to a LAN interface is not something to do by default, and not something to do without a credential. Remote access is off until you turn it on, and when you do, a gateway holding one credential per paired device proxies to a service that never stopped being loopback-only. Any one of those credentials can be taken back on its own, mid-connection.
- Nothing you have to install first. DSH Studio needs Node.js 20 or newer to
run the harness, and it finds one if you have it — including the ones a version
manager installed but never put on
PATH. If you do not, it downloads and verifies one into its own data directory. The harness itself is installed for you either way. - Windows 10/11 with WebView2 (present on Windows 11 by default).
pnpm install
pnpm tauri dev # run it
pnpm tauri build # produce installers for the current platformChecks:
pnpm lint # ESLint, zero warnings
pnpm exec tsc --noEmit # strict TypeScript
pnpm test # store and i18n behaviour
cargo test --manifest-path src-tauri/Cargo.toml --workspacesrc/ React 19 + Tailwind 4 shell UI
src-tauri/src/harness/ supervisor, readiness parsing, health probe, install
src-tauri/src/remote/ LAN gateway, pairing codes, QR, address selection
src-tauri/src/plugins/ registry search, profile inspection, install/switch/remove
src-tauri/crates/
node-runtime/ find a usable Node on this machine
proc-guard/ kill a process tree and mean it
node-runtime and proc-guard are deliberately free of Tauri and of anything
specific to this app — they are two small crates that answer two questions any
desktop app wrapping a Node service has to answer.
Does this replace the harness UI? No. The harness is loaded from its own service, unmodified. What the shell adds is the window around it, and everything needed to keep the service alive inside that window.
Do I need to install dsh myself?
No. If it is missing, the row that says so is a button. It installs into a
private prefix inside the app's data directory rather than your global npm root,
so nothing on the rest of your machine changes.
Do I need Node.js installed?
No. If you have one the shell uses it — including the ones nvm, fnm or Volta
installed but never added to PATH. If you do not, the row that says so is a
button: it fetches the current LTS, checks it against the published SHA-256, and
keeps it inside the app's own data directory. Your PATH is not touched, so this
cannot disturb a Node you rely on for something else.
Which port does it use?
Whichever one the kernel hands out. --port 0 means there is no configured port
to collide with, and the supervisor reads the real port back from the service's
own readiness line. This is also why a restart can land somewhere else and the
window simply follows.
I closed the window and the harness kept running. That is deliberate, while a service is up. The window hides to the tray and the service keeps working; the close button says so on hover. Quit from the tray menu to stop everything.
Does closing the app leave processes behind?
It should not, including if the shell is killed outright rather than closed.
That is what proc-guard is for. If you ever find an orphan, that is a bug
worth reporting.
How do I use it from my phone? Open the Remote pane, press Open access, and scan the code with the phone's camera. Both devices have to be on the same network — there is no relay and no account, so nothing about the pairing leaves the room. The code is good once and for two minutes; the phone that redeems it gets a key of its own, and the pane lists it afterwards, so that key can be taken back without disturbing anything else that paired.
Can I install any npm package as a plugin? You can install any package, but only one that declares a profile patch in its manifest becomes an active layer — the marketplace says which is which before you install. Plugins land in the harness's own profile through its own plugin command, so what the shell installs is exactly what the harness would have.
Is my data sent anywhere? The shell makes exactly one request you did not ask for: a GET to this repository's public release feed, shortly after launch and every six hours after, to find out whether there is a newer version. No account, no identifier, nothing about your machine. That is the whole list.
Everything else stays where it is. The service is bound to loopback and that is not a setting — an agent that can run shell commands has no business being reachable by default. Remote access does not change it: the service stays on loopback, and what listens on the network is a gateway that will not forward a byte without a credential it minted itself. It is off until you switch it on, and it goes off again the moment the harness stops. What the harness itself does with your API keys is upstream's business, not this project's.
| Where | For |
|---|---|
| Report a bug | The form asks for the platform, the Node and the log up front — the three things triage needs. |
| Ask for a feature | Including "the harness can do this from a terminal and the window cannot". |
| Report something privately | Anything about the gateway, the pairing keys, or the supervisor. See SECURITY.md. |
| The harness itself | The agent, its UI, its models. This repository is only the window around it. |
Issues in Chinese are welcome and get answered in Chinese — the app, the README, the changelog and the contributing guide are all bilingual, and so is triage.
Issues and pull requests are welcome — see CONTRIBUTING.md (中文) for how to set up, what the checks are, and how commits are worded here.
The one house rule is in the code style: comments explain why a thing is the way it is, not what the line below does.
MIT.
DSH Studio is an independent project. It is not affiliated with or endorsed by DeepSeek.