A small native Linux desktop window for DeepSeek Harness.
It boots a Harness profile (dsh web) as a child process, reads the authenticated
loopback URL the host prints on stdout, and loads that URL in a WebKitGTK window.
┌─ dsh-desktop (Go, ~2.8 MB) ──────────────────────────────┐
│ spawns: dsh web --port 0 --no-open │
│ reads: "dsh web: http://127.0.0.1:37907/?token=..." │
│ shows: that URL in a native WebKitGTK window │
│ quits: kills the whole host process group │
└──────────────────────────────────────────────────────────┘
| dsh-desktop | Electron shell | |
|---|---|---|
| Runtime you install | WebKitGTK + GTK 3 (already on any GTK desktop) | bundled Chromium (~150 MB) + Node |
| Binary size | 2.8 MB | 120–330 MB |
| Extra Node runtime | none — reuses your installed dsh |
bundles a second Node |
| Memory | one WebKit render process | Chromium multi-process |
| Source | ~600 lines of Go | thousands, plus a build toolchain |
Because it does not fork or patch upstream, it shares ~/.dsh with your CLI:
the same sessions, settings, and agent presets.
- Linux x86_64 (any distro with GTK 3 and WebKitGTK 4.1)
- Go 1.21+ to build
dshon yourPATH(or pass--dsh)
Runtime packages, Debian/Ubuntu:
sudo apt install libgtk-3-0 libwebkit2gtk-4.1-0Build packages:
sudo apt install golang libgtk-3-dev libwebkit2gtk-4.1-dev pkg-configFedora: sudo dnf install golang gtk3-devel webkit2gtk4.1-devel
Arch: sudo pacman -S go gtk3 webkit2gtk-4.1
./scripts/install.shInstalls per user, no sudo:
| Path | What |
|---|---|
~/.local/bin/dsh-desktop |
the binary |
~/.local/share/applications/dsh-desktop.desktop |
application-menu entry |
~/.local/share/icons/hicolor/*/apps/dsh-desktop.png |
icons, 16→512 px |
Then launch DeepSeek Harness from your application menu, or run dsh-desktop.
Uninstall with ./scripts/install.sh --uninstall. Your ~/.dsh data is never touched.
dsh-desktop [options]
-profile string Harness profile to boot (default "web")
-port int loopback port for the host (default 21777)
-width, -height initial window size (default 1280x840)
-dsh string path to the dsh executable
-no-dmabuf disable WebKit's DMA-BUF renderer (default true)
-safe-render additionally disable WebKit compositing mode
-opaque-menus make the client's translucent menus opaque (default true)
-debug enable the WebKit inspector
-verbose echo the host's output to stderr
-check-update print the newest release and whether it is newer, then exit
-update download the newest release, replace this binary, and exit
-update-check check for a newer release after boot and show a banner (default true)
-selftest 25s load, inspect the rendered page, print JSON, exit
-version print the version
The host listens on 21777 by default — above 20000, below the Linux ephemeral range (32768–60999), so the OS is unlikely to hand the same port to something else while the window is open. A stable port means the URL survives restarts.
dsh itself does not fall back when its port is taken; it aborts with
EADDRINUSE. So the app picks the port before spawning:
- 21777 free → uses 21777.
- 21777 taken → walks upward and uses the next free port (21778, 21779, …), up to 64 candidates. Every candidate stays above 20000.
-port 0→ hands the decision to the OS, which may pick any port.
A probe cannot reserve the port for the child that will actually bind it, so a lost race is retried from the next free port rather than shown as an error.
Your agent presets — including montir — live in a profile, not in the app.
The default web profile is the one this machine already uses, so the desktop
window sees exactly the same preset roster and the same default as the CLI.
If you keep presets in another profile, pass it:
dsh-desktop -profile workThe client styles menu surfaces as --dsw-menu-surface-fill: #43454a73 — 45%
opaque — and leans on backdrop-filter: blur(40px) for contrast. That works on
Chromium, where the blur composites the page behind the menu. On this render path
the blur does not composite, so the menu draws as a see-through panel and its
labels are hard to read against the content underneath.
-opaque-menus (on by default) replaces the fill with the same colour made
opaque, which is what the menu looks like once the blur has done its work:
#43454a at 45% over the #151517 base -> #2a2b2e
The blur is switched off with it, since nothing depends on it any more. Pass
-opaque-menus=false to keep the upstream translucency.
Some drivers (notably NVIDIA under Wayland) cannot give WebKit a DMA-BUF buffer
and the engine aborts with Could not create GBM EGL display: EGL_SUCCESS.
-no-dmabuf is therefore on by default. If the window still renders blank or
garbled, add -safe-render. To opt out entirely:
dsh-desktop -no-dmabuf=falseBoth flags only set environment variables, and never override a value you set
yourself — so WEBKIT_DISABLE_DMABUF_RENDERER=0 dsh-desktop wins over the default.
The shell updates itself from this repository's GitHub releases:
dsh-desktop -check-update # dsh-desktop 0.1.3 (latest 0.1.4)
# Update available: 0.1.4 — install it with: dsh-desktop -update
dsh-desktop -update # download, verify the archive, replace the binaryBoth are ordinary commands: no window, no profile, no single-instance lock, so an
update runs while a window is open. The replacement is a rename of the new
binary over the old path — the only mutation — so a failed download or a
truncated archive leaves the working install untouched. The binary it replaced
is kept beside it as dsh-desktop.backup; restore it by hand if a release
misbehaves.
On launch the app also checks in the background (4 s timeout, once) and, when a
newer release exists, shows a dismissible banner in the window and a desktop
notification. It never installs anything by itself — a self-replacing binary
is not something to do behind the user's back. Disable even the check with
-update-check=false or DSH_DESKTOP_NO_UPDATE_CHECK=1.
This updates the shell. The dsh CLI and the plugins in your profile are a
separate stack with its own release cadence; the dsh-autoupdate plugin reports
and applies those.
make build # -> dist/dsh-desktop
make run # build and launch
make test # unit tests + full smoke test
make unit # unit tests only (no display, no dsh needed)
make vet # gofmt + go vet
make icons # re-render assets/icons from assets/icon.svg
make release # package dist/dsh-desktop-<version>-linux-x86_64.tar.gzmake unit covers port selection, the URL pattern, and the default port range.
It needs no display and no dsh install, so it runs anywhere:
ok github.com/folkwayse/dsh-desktop-linux
make test adds the end-to-end check. It boots an actual host, loads the actual
UI in an actual WebKit window, then reaches into the rendered DOM and reports
what it finds:
1. boot, load, inspect
PASS process exited 0
PASS window loaded a page
PASS app root element rendered
PASS client UI is interactive
PASS stylesheet bundle applied
PASS no page-level errors
PASS montir preset on the roster
2. clean shutdown
PASS no orphan processes left behind
This matters because "the process started and a window exists" is compatible
with a completely blank page. Only inspecting the live document distinguishes
the two. The orphan check compares PID sets before and after, not command-line
patterns — pgrep -f matches the test script's own arguments and reports
phantom orphans.
| Path | Purpose |
|---|---|
main.go |
everything: flags, host supervision, WebKit window, self-test |
update.go |
the self-update engine: release feed, version compare, atomic install, banner |
main_test.go |
unit tests for port selection and the URL pattern |
update_test.go |
unit tests for version compare, asset pick, and a real download-and-replace |
pkgconfig/webkit2gtk-4.0.pc |
forwards the legacy 4.0 pkg-config name to 4.1 |
scripts/install.sh |
build + per-user install (and --uninstall) |
scripts/smoke.sh |
the end-to-end test |
scripts/render-icons.py |
rasterizes assets/icon.svg via librsvg |
assets/icon.svg |
icon source — one spine (the harness), three bound plugins |
The C library vendored inside webview_go still asks pkg-config for
webkit2gtk-4.0. Ubuntu 26.04 ships only webkit2gtk-4.1, which is
API-compatible for everything webview.h touches. Rather than patch the vendored
dependency, pkgconfig/webkit2gtk-4.0.pc forwards the old name to the new
library. make and install.sh put that directory on PKG_CONFIG_PATH.
- Linux only. The Go code is portable but the window layer (WebKitGTK) is not
wired for macOS/Windows here; those need the
webview_goCocoa/WebView2 paths. - No tray icon. Closing the window quits the app and stops the host. The Electron shells keep a tray instead.
- Updates only what it ships. The shell replaces its own binary; the
dshCLI and the profile's plugins are updated by thedsh-autoupdateplugin. dshmust resolve. If the CLI is not onPATH, pass--dsh <path>or setDSH_BIN.- Single instance per profile. A second launch refuses to start while one window is open for the same profile.
MIT — see LICENSE. Third-party notices, including the MIT text for the
bundled webview_go and the C webview implementation it vendors, are in
THIRD_PARTY_NOTICES.md.