DeepSeek Harness packaged as an Electron desktop application: the host composition boots in-process in the Electron main process, the built frontend loads from the local file system, and the /api fetch plus event downlinks cross an Electron IPC bridge (no browser, no port, no local HTTP server).
An unofficial desktop application for DeepSeek Harness: the host composition boots in-process inside Electron, the built Web frontend loads from the local filesystem, and every /api request plus event downlink crosses an Electron IPC bridge — no browser, no listening port, no local HTTP server.
⚠️ Unofficial. This project is not affiliated with, endorsed by, or published by DeepSeek. It packages the open-source @deepseek-ai/dsh distribution as-is. The whale mark is the official DeepSeek Harness favicon used for visual consistency only, and remains a DeepSeek trademark.
Most community wrappers spawn the dsh web CLI and point a browser window at 127.0.0.1:3080. This one follows the integration seam the upstream design documents explicitly: "Electron loads dist over file:// and carries fetch over an IPC bridge" (dsh-host-webserver). The whole shipped composition runs inside the Electron main process, so sessions, goals, background jobs, and plugins are first-class local state — closing the window (to the tray) keeps them running.
Features
🧬 In-process host
Boots the shipped web profile with dsh-app-boot; no child process, no port, no HTTP server
🔌 IPC transport
window.fetch / WebSocket shims in preload carry all RPC over Electron IPC; the 37 shipped client plugins run unmodified
🪟 Tray residency
Close-to-tray keeps sessions running; tray menu shows/hides the window, checks updates, quits
🔔 Native notifications
dsh-desktop · DSH Hub
Driven by the host's own event streams: approvals, questions, agent errors, dynamic-plugin run requests — always; reply-completion — only while the window is in the background (subagent sessions excluded)
🏠 First-run home wizard
Choose a private data directory or reuse ~/.dsh; a live dsh web instance on the shared home triggers a conflict warning
🛡️ Crash recovery
Renderer crash rebuilds the window and reloads the site; the host state survives (3 strikes per minute → give up)
📡 Chunked IPC responses
Headers first, body as chunk frames with a ready handshake (no frame loss); unary responses stay on the fast inline path
📦 Session export
Runs in-process with taskbar progress, native save dialog, and a completion notification
⬆️ Auto-update
electron-updater against GitHub Releases (publish.url → /releases/latest/download): background check 15 s after boot + tray gesture; latest.yml ships with every Release so installed builds update themselves
🐋 Official whale icon
The DeepSeek Harness favicon (black whale) rasterized to PNG via scripts/rasterize.mjs
🧰 Log repair tool
scripts/repair-log.mjs fixes the upstream interruption-flush seq-reorder corruption (see below)
Download
Artifact
Notes
DeepSeek-Harness-Desktop-Setup-.exe
NSIS installer (choose install dir); used by the auto-updater
DeepSeek-Harness-Desktop-Portable-.exe
Portable
latest.yml
Auto-update metadata, published beside the installers on every Release
Get the latest builds from Releases — built by GitHub Actions on every v* tag push. Not code-signed — SmartScreen will ask; signing is wired via CSC_LINK / CSC_KEY_PASSWORD.
Quick start
Launch the app. On first run, pick a data directory (private by default, or share the CLI's ~/.dsh).
Open Settings → Models, enter your DeepSeek API key (or a custom OpenAI-compatible endpoint). Applies live, no restart.
Click Select workspace (native OS dialog) and start a session.
This mirrors the official Quickstart flow; the desktop app boots the same web profile composition the CLI does, so model configuration, workspace handling, approval flows, and plugin development behave identically.
One deliberate deviation: the CLI uses the invoking directory as the default workspace; the desktop app boots from the user home (override with DSH_DESKTOP_CWD). Workspace selection happens in the UI.
Environment
Variable
Effect
DSH_DESKTOP_HOME
Harness home (highest precedence; falls back to an inherited DSH_HOME, then /dsh-home). Kept separate from ~/.dsh by default so the two installs never fight over profiles/node_modules fallback links.
DSH_DESKTOP_CWD
Boot working directory (default: user home).
DSH_DESKTOP_SCHEME=file
Debug-only file:// loading. The default app://localhost privileged scheme keeps connection.isLoopback true (host-scoped settings, open-file affordances); file:// reports a null origin and degrades those.
Sharing the CLI home (~/.dsh): pick it in the wizard, or set DSH_DESKTOP_HOME=/.dsh. Never run the desktop app and dsh web against the same home at the same time — concurrent writers corrupt session logs. The app detects a live web instance and warns before boot.
Development
npm install
npm start # dev run (unpackaged)
npm run dist:dir # unpacked build into release/win-unpacked
npm run dist # portable + NSIS installers (also writes latest.yml)
Node ≥ 22 and a Windows box for the packaged targets.
Releasing
npm version patch # or minor / major — bumps package.json, commits, tags
git push --follow-tags origin main
The release workflow (.github/workflows/release.yml) runs on every v* tag: it verifies the tag matches package.json's version, builds on windows-latest, and attaches the NSIS installer, portable build, blockmap, and latest.yml to a GitHub Release. Installed copies then auto-update from /releases/latest/download — no separate update server. Unreleased local builds stay quiet (the updater is inert unless packaged).
Packaging notes (real bugs fixed here)
npmRebuild: false — Electron ABI rebuilds try to compile node-pty from source (VS Spectre libs); the web profile never uses terminal rows. koffi / sharp / node-addon-require-builtin ship N-API prebuilds and load fine.
asarUnpack: node_modules/** — the healed profiles/node_modules junctions must point at real directories; ESM resolution cannot cross a junction into an asar archive. boot.mjs / site.mjs rewrite app.asar\ paths to app.asar.unpacked\.
Never set ELECTRON_RUN_AS_NODE globally — Chromium utility children inherit it and crash-loop. The native directory picker is replaced by Electron's dialog.showOpenDialog (the shipped koffi child-process backend cannot run under Electron), provided in the boot prepare hook because the API gateway injects directoryPicker.
net.fetch(file://) inside a protocol handler crashes the network service; the app:// handler reads files directly and returns Responses.
Session log repair (upstream bug workaround)
Upstream 0.1.0-rc.6 can write step/end + turn/end out of order relative to a buffered chunk run when a turn is interrupted, double-claiming seqs. Strict readers then refuse the session with corrupt session log: seq gap in committed region. scripts/repair-log.mjs simulates the reader's committed-chain walk and renumbers the disordered tail (seq / seq0 / sourceEventSeqs; timestamps untouched).
# Run ONLY against a cold log (the owning host must have exited).
node scripts/repair-log.mjs "/sessions///session.jsonl.zstd" --in-place
scripts/inspect-log.mjs [line] audits seq continuity. The bug should also be reported upstream — this repo ships the workaround, not the fix.
Architecture
Electron main process
└─ dsh web profile composition (booted in-process via dsh-app-boot)
├─ webserver row disabled → in-process webServer stub with identical
│ route/fallback/index-tap semantics, zero sockets
├─ every shipped host row (connection, modules, api-gateway, …) mounts
│ unchanged against the stub; their routes are captured and dispatched
└─ apiProxy event streams (mux/host) pumped by the main process
Renderer (app://localhost serving a materialized dist copy)
├─ preload replaces window.fetch / window.WebSocket with IPC shims
└─ shipped client code (37 plugin bundles) runs unmodified
IPC channels
dsh:fetch / dsh:fetch-abort / dsh:fetch-stream-ready / dsh:fetch-chunk /
dsh:fetch-end upstream RPC (unary + chunked bodies)
dsh:ws-open / dsh:ws-frame / dsh:ws-close downstream event streams
The transport seam is upstream's own design: toFetchHandler(apiProxy) wraps the API gateway into a transport-agnostic Request → Response function, and the trust fence treats the renderer as the loopback caller it logically is (Host: 127.0.0.1 on every mock request, sender-verified IPC).
Known limitations / roadmap
Response streaming is chunked but unbuffered for long-lived bodies only; unary responses buffer whole (fine for the current API).
No code-signing certificate (SmartScreen warns; CSC_LINK / CSC_KEY_PASSWORD in CI enable signing).
Windows-only packaging for now (main-process code is cross-platform; macOS/Linux configs are the next step).