A lightweight macOS app wrapper for DeepSeek Harness.
DS Harness turns dsh web into a normal macOS application with a native window, Dock icon, and persistent background service — without Electron or an external browser window.
curl -fsSL https://raw.githubusercontent.com/beforewave/dsh-desktop/main/install.sh | bashDS Harness is installed to:
~/Applications/DS Harness.app
The app is currently unsigned. On first launch, right-click DS Harness.app in Finder, choose Open, then confirm.
You can always run:
dsh webDS Harness does not replace DeepSeek Harness. It provides the desktop lifecycle around it.
dsh web |
DS Harness |
|---|---|
| Start from Terminal | Launch from Finder, Spotlight, or Dock |
| Open in a browser | Native macOS window |
| Backend tied to manual process management | Backend managed by launchd |
| Restart manually after a crash | launchd keeps the backend alive |
| Manage duplicate instances yourself | Reuses an existing DSH server |
| Shell environment may differ under background execution | Resolves Node and runtime PATH explicitly |
| No desktop lifecycle | Normal macOS app lifecycle |
| Manual upgrade awareness | Checks for newer DSH versions |
The goal is to keep the wrapper small and let upstream DeepSeek Harness remain the actual application.
DS Harness.app
│
├── AppKit
│ └── WKWebView
│ └── http://127.0.0.1:3080
│
└── launcher
└── launchd
└── dsh web
The desktop window is a thin native AppKit host.
WKWebView renders the existing DSH Web UI directly inside the app, so Chrome and Electron are not required at runtime.
The dsh web backend runs independently under launchd.
The desktop UI and DSH backend intentionally have separate lifecycles.
Open DS Harness
→ start or reuse dsh web
→ open the native window
Close the window
→ backend keeps running
Click the Dock icon
→ window opens again
Cmd+Q
→ desktop app exits
→ backend keeps running
dsh web crashes
→ launchd restarts it
Open DS Harness again
→ existing backend is reused
Logout / reboot
→ backend stops
→ it does not automatically start at next login
This gives DSH a persistent backend during the current login session without turning DS Harness into a login item.
Before starting a backend, DS Harness checks:
http://127.0.0.1:3080
If a working DSH Web server is already available, it is reused.
DS Harness does not kill an arbitrary process simply because it owns port 3080.
DS Harness prefers an existing working dsh installation:
dsh webIf no working dsh command is available, it can fall back to a pinned npm version:
npx -y @deepseek-ai/dsh@<version> webNode.js is required.
DS Harness explicitly resolves the Node executable and runtime PATH before handing the backend to launchd, avoiding common background-process issues such as:
env: node: No such file or directory
DS Harness uses the normal DeepSeek Harness configuration directory:
~/.dsh
including:
~/.dsh/settings.yaml
The launcher explicitly sets:
DSH_HOME=~/.dsh
Your DSH configuration is not copied, replaced, or removed by DS Harness.
DS Harness desktop-side customization is read directly from:
~/.config/dsh-desktop/
├── env.sh
├── healthcheck.sh
├── pre-start.d/
├── post-ready.d/
└── commands.d/
All files are optional. DS Harness does not create, copy, overwrite, or mutate this directory. There are no built-in command files in the app bundle; this directory is the single source of truth for desktop customization.
If present, env.sh is sourced before DS Harness resolves or starts the backend, and is also sourced when a menu command runs.
Example:
# ~/.config/dsh-desktop/env.sh
export HTTPS_PROXY="http://127.0.0.1:7890"Scripts in pre-start.d/*.sh run before a DS Harness-managed backend is started. If any pre-start hook fails, startup is aborted.
Scripts in post-ready.d/*.sh run after the backend becomes healthy. Post-ready hook failures are logged but do not prevent the desktop UI from opening.
Hook output is written to:
~/Library/Logs/DS Harness/extensions.log
If healthcheck.sh exists, DS Harness uses it instead of the default HTTP readiness check against http://127.0.0.1:3080. A zero exit status means the backend is healthy.
Each commands.d/*.sh file containing # @menu metadata becomes a native menu action. The same configured actions are shown in both the DS Harness application menu and the right-side macOS status menu.
Example:
#!/bin/bash
# @menu Restart Backend
# @shortcut cmd+shift+r
# @order 10
# @separator before
launchctl kickstart -k \
"gui/$(id -u)/${DSH_DESKTOP_SERVICE_LABEL}"Supported metadata:
@menu required menu title
@shortcut e.g. cmd+r, cmd+shift+r, cmd+option+l
@order numeric sort order; default 100
@separator before or after
@enabled true/false
The command list is re-scanned whenever DS Harness becomes active, so changing commands.d does not require rebuilding the app. Reload UI and the standard macOS items such as About, Hide, Edit, and Quit remain built into the host.
Extension scripts receive these environment variables:
DSH_DESKTOP_CONFIG_DIR
DSH_DESKTOP_SUPPORT_DIR
DSH_DESKTOP_LOG_DIR
DSH_DESKTOP_SERVICE_LABEL
DSH_DESKTOP_URL
DSH_HOME
A useful optional command is commands.d/check-for-updates.sh: it compares the current DSH version with @deepseek-ai/dsh@latest. If an update is available, the menu presents a single Update action; the script handles the underlying runtime automatically. The currently running backend is never interrupted.
Menu command stdout/stderr is written to:
~/Library/Logs/DS Harness/commands.log
The managed launchd service is:
com.beforewave.ds-harness.web
Runtime state is stored under:
~/Library/Application Support/DS Harness
Logs are stored under:
~/Library/Logs/DS Harness
The launchd definition is created at runtime rather than installed into ~/Library/LaunchAgents.
As a result, the backend can remain alive after the desktop app exits without automatically starting after a reboot or new login.
DS Harness can check whether a newer @deepseek-ai/dsh version is available.
Updates are never silently applied to a running backend.
For an installed dsh, DS Harness can provide the npm update command.
For the npx fallback, it can update the pinned version used the next time the backend starts.
DS Harness also migrates runtime state created by earlier launcher versions.
For example, the retired service:
com.beforewave.dsh.web
is migrated to:
com.beforewave.ds-harness.web
Migration happens before checking port 3080, preventing an obsolete DS Harness-managed backend from being silently reused.
Only launcher-owned runtime files are cleaned up.
The user's:
~/.dsh
directory is deliberately left untouched.
- macOS 12 or later
- Node.js
- DeepSeek Harness installed globally, or npm/npx available
Chrome is not required.
Check whether DSH is responding:
curl http://127.0.0.1:3080Check which process owns the port:
lsof -nP -iTCP:3080 -sTCP:LISTENCheck the managed service:
launchctl print gui/$(id -u)/com.beforewave.ds-harness.webCheck logs:
tail -200 "$HOME/Library/Logs/DS Harness/web-error.log"
tail -200 "$HOME/Library/Logs/DS Harness/web.log"Stop the managed backend:
launchctl bootout \
gui/$(id -u)/com.beforewave.ds-harness.web \
2>/dev/null || trueRemove DS Harness:
rm -rf "$HOME/Applications/DS Harness.app"
rm -rf "$HOME/Library/Application Support/DS Harness"
rm -rf "$HOME/Library/Logs/DS Harness"Your DeepSeek Harness configuration in ~/.dsh is intentionally preserved.
Developed with assistance from ChatGPT.