dsh-emacs-bridge
This is a two-way bridge between Emacs and a Deepseek Harness session. The bridge moves text from Emacs to DeepSeek Harness (DSH), and vice versa, over loopback HTTP. This lets you type in Emacs and read DSH's replies without copy-pasting, while also avoiding streaming voluminous LLM outputs through Emacs.
It consists of two components:
dsh-plugin/— a DeepSeek Harness plugin (dsh-emacs-bridge).emacs/dsh-bridge.el— an Emacs package to interact with the harness.
Installation
Requirements
dsh, the DeepSeek Harness.- Node.js and
pnpmto build the plugin. - Emacs 29 or later.
- (Recommended) The
markdown-modeEmacs package.
Emacs package
To build an Emacs package that also bundles the DSH plugin, run this in the repository's root directory:
make package
Then, in Emacs:
M-x package-install-file RET /path/to/dsh-bridge-<version>.tar RET- (optional) If you run DSH from a source checkout, customize the
variable
dsh-bridge-dsh-command(e.g.,M-x customize-variable RET dsh-bridge-dsh-command RET) with the DSH command (see below). Skip this ifdshis on the executable path or run vianpx. M-x dsh-bridge-install-plugin— install the bundled plugin into DSH.- Start or restart
dsh web.
To remove the plugin later, run M-x dsh-bridge-uninstall-plugin.
Here is an example of dsh-bridge-dsh-command for a source checkout:
(setq dsh-bridge-dsh-command "pnpm -C /path/to/deepseek-harness dsh")
Note that ~ is not expanded, so specify the full path. Don't add an
additional web argument to the end.
Manual compilation and installation
Instead of an all-in-one Emacs package, you can build and install the DSH plugin and Emacs library manually.
Build and install the DeepSeek Harness plugin
From this repository's root directory:
make build # emits dsh-plugin/lib/index.js + lib/client.js
If you have dsh installed on the executable path, run the following
commands:
# global install, from this repo root:
dsh plugin --profile web add link:./dsh-plugin
dsh web
If you have a source checkout of DSH and run it as a pnpm script
(pnpm dsh web), run the following from the deepseek-harness
directory instead, replacing the link: path with the appropriate
path into this repo:
# source checkout, from deepseek-harness root:
pnpm dsh plugin --profile web add link:/absolute/path/to/dsh-emacs-bridge/dsh-plugin
pnpm dsh web
Install the Emacs library
Put this in your Emacs init file (~/.emacs.d/init.el or ~/.emacs),
replacing the path with the actual path to dsh-bridge.el.
(load "/path/to/dsh-emacs-bridge/emacs/dsh-bridge.el")
Alternatively, copy dsh-bridge.el into your Emacs load-path and do
(require 'dsh-bridge).
Usage
From Emacs, the main entry-points are these two commands:
M-x dsh-bridge— open a transient menu for DSH commands.M-x dsh-bridge-list-sessions— show a list of DSH sessions.
Consider giving either of these a global keybinding, e.g.,
(keymap-global-set "C-c d" #'dsh-bridge)
Transient menu
The M-x dsh-bridge command opens a transient menu that prompts for
the next command. The top line shows the session your next command
will act on: the buffer's own session when the dispatcher is invoked
from a DSH-View or DSH-Prompt buffer, otherwise the default target
session if one is set, otherwise the last-active session.
The following commands are available from the transient menu:
q— exit the transient menu.r— open a DSH-Prompt buffer for the session.s— send the region or buffer as a prompt.d— send the region or buffer as a draft (can still edit in DSH composer before submitting).f— fetch the session's latest reply into the DSH-View buffer.D— show the session's read-only report (see Session report buffer).t— set the default target session.u— clear the default target session.l— open the DSH-Sessions buffer.
Sessions menu
The M-x dsh-bridge-list-sessions command opens a buffer with a list
of DSH sessions. The default target session (if any) is marked by a
* in the leftmost column, and the S (state) column shows each
session's live status — a filled circle that is green when idle and
amber when running, ? when unknown — obeying
dsh-bridge-status-indicator.
The following commands are available from the DSH-Sessions buffer:
q— quit the window and bury the buffer.f— fetch the last output for the session at point into a DSH-View buffer.rorRET— open a DSH-Prompt buffer for the session at point.t— set the session at point as the default target.u— clear the default target.v— toggle whether archived sessions are shown (hidden by default).R— rename the session at point.d— archive the session at point.+— create a new session, optionally in a new workspace.W— rename the workspace of the session at point.D— describe the session.g— refresh the DSH-Sessions buffer.
For a full list, see the menu bar. Other tabulated-list-mode keys
are also available.
DSH-View buffer
This read-only buffer contains the model output for a DSH session.
Each buffer holds one agent turn (i.e., all replies from a user prompt
to an idle). It is fetched by f from the transient menu or the
DSH-Sessions buffer, C-c C-f from the DSH-Prompt buffer, or pushed
from the web UI's "Send to Emacs" button (see below). Also, from the
DSH-Prompt buffer (see below), doing C-c C-c to send a prompt will
subsequently pop to a DSH-View buffer to view the replies.
The following commands are available in a DSH-View buffer:
g— re-fetch the current session's newest turn.r— open a DSH-Prompt buffer for the current session.w— copy the reply (region, else the whole shown turn's raw Markdown without the divider lines) to the kill ring.B— branch the shown turn into a new session (see Branching a turn).i— receive the latest "Send to Emacs" message (see below).D— show the session's read-only report (see Session report buffer).M-p/M-n— cycle the current session's turns (older / newer).l— open the DSH-Sessions buffer.q— quit the window and bury the buffer.
When created, a DSH-View buffer usually follows the latest turn, so
that the buffer is automatically updated as more replies arrive.
Walking back through older turns with M-p suspends following;
cycling back to the newest turn with M-n resumes it automatically.
To customize this behavior, change dsh-bridge-view-follow-at-newest.
If markdown-mode is installed, and dsh-bridge-view-gfm is non-nil,
the reply is font-locked as GitHub-Flavored Markdown (the dividers use
GFM horizontal-rule syntax, so they render cleanly).
Branching a turn
B in a DSH-View buffer branches the shown turn into a new session:
the host forks the session's prefix through the end of that turn into a
child session, and Emacs then opens the child's DSH-View (following its
inherited turns) together with a DSH-Prompt buffer for it. This is the
same operation as the web UI's per-message "Fork" action, and the new
session appears in both session lists.
Two things differ from the source, by design: the child inherits the agent preset but starts on the default model (the fork does not copy the source's current model selection), and it is not auto-titled, so it shows up by its id tail until you rename it or its first prompt is titled. Emacs says both in the confirmation message.
A turn that is still running has no fork boundary and cannot be
branched; cycle to a completed turn first (M-p). The fork route
reads a cold source session without resuming it, so branching does not
spawn the source's agent as a side effect.
DSH-Prompt buffer
This buffer is used to compose a prompt, or reply, for a DSH session.
It is opened by r/RET from the DSH-Sessions buffer, and r from
the transient menu or DSH-View buffer. The session affected is
determined by how this buffer was invoked; for instance, r from a
DSH-View buffer opens a prompt for the same session. If a renamed
DSH-Prompt buffer is already bound to that session, it is reused.
The following commands are available from the DSH-Prompt buffer:
C-c C-c— send the buffer as a prompt. On success, bury the buffer and pop to the DSH-View for that session.C-c C-d— push the buffer to the DSH composer as a draft.C-c C-m— set the model and reasoning effort.C-c C-s— rebind the buffer to another session.C-c C-k— erase the buffer.C-c C-f— open the DSH-View buffer for this session.C-c C-l— open the DSH-Sessions buffer.M-p/M-n— walk the session's prompt history.
While walking the prompt history with M-p/M-n, you may edit
earlier prompts. This blocks further history navigation; to resume,
you must send the prompt first, or revert with M-x revert-buffer.
When markdown-mode is installed, this buffer derives from it, so
most markdown editing commands are also available.
Answering ask-user questions
When the model requests additional user inputs via the
ask_user_question tool, the query is surfaced in the DSH-View
buffer. Typing a here (or in the DSH-Sessions buffer with point on
the session) opens a buffer for handling the query.
In this buffer, mark the option(s) you choose with RET. You can
also navigate to a question block and type your desired option's
number key, or c to write a freeform answer via the minibuffer.
To submit the answers, type C-c C-c. Alternatively, type C-c C-k
to decline the query, canceling the tool call.
Sending text from DSH to Emacs
The DSH plugin adds a "Send to Emacs" button that lets you push
specific assistant messages to Emacs. This automatically pops to the
DSH-View buffer in Emacs. You can use i in the DSH-View buffer (or
run M-x dsh-bridge-receive) to pull the last message pushed.
Development testing
Running make test launches the standard unit test suite (Vitest for
plugin, ERT for elisp). Running make integration-test performs a
suite of integration tests that boots the plugin against a live
DeepSeek Harness host with a mock LLM; see integration/README.md.
It is not part of make test (which stays fast); run it before
committing a host-plugin change and before a release.
Permissions, authentication, and failure bounds
The DSH plugin registers its routes on the DSH web server's loopback
listener, and the browser plugin calls only same-origin
/dsh-bridge/* routes. No third-party service is contacted.
Every /dsh-bridge route requires a shared bearer token stored at
~/.dsh/dsh-bridge-token, generated on first use in mode 0600. Emacs
reads this file directly, while the browser plugin fetches it from the
loopback-only GET /dsh-bridge/token route (peer- and origin-fenced).
HTTP request bodies are capped at 1 MiB; larger bodies get a 413
error. DSH-to-Emacs messages are held in a bounded outbox (100
unacknowledged entries); overflow evicts the oldest entries and is
reported to the collector. Naming a cold (persisted-only) session
from Emacs resumes it on demand, matching the web UI; an id neither
live nor persisted is 404, a subagent-owned session is 409, and a
draft push fails with 409 when no browser client is subscribed. The
read-only session report and the POST /dsh-bridge/fork source read are
the exceptions: each observes a cold session's persisted log without
resuming it.
License
This software is released under the terms of the GNU General Public License version 3, or later. See COPYING.
No comments yet. Be the first to write one.