Bundle
dsh-process-control
DeepSeek Harness web plugin: one process-control button beside the sidebar Settings row — restart the host, reload the page, or quit the host process.
- Source
- baicaibucai1
- stars
- 1 stars
- License
- MIT
- Updated
- Updated 17 hours ago
Readme
# dsh-process-control
A DeepSeek Harness **web plugin**: one process-control button beside the sidebar
Settings row.
Since DSH is normally started detached (`Start-Process … -WindowStyle Hidden`
under Windows, a background service under Linux), the host keeps running with no
window and no obvious way to stop it. This plugin puts the three operations you
actually need one click from the UI:
| Menu item | What it does |
| --- | --- |
| **重启 DSH** | Relaunches the host from its exact boot invocation, detached, after the listening socket is released. The page reconnects once it is back. |
| **刷新页面** | Reloads the current page. |
| **结束进程** | Terminates the host process (SIGTERM, with a hard exit fallback). |
The menu header reports the live host facts: `pid`, serving port, whether the
boot invocation could be determined, and the plugin version.
Restart and quit ask for a second click before acting.
## Install
```sh
# from npm — the channel the plugin market's discovery index reads
dsh plugin --profile <profile> add dsh-process-control
# straight from git
dsh plugin --profile <profile> add git+https://github.com/baicaibucai1/dsh-process-control.git
# or from a local checkout
dsh plugin --profile <profile> add /path/to/dsh-process-control
```
That command reconciles `dsh.profile.bundles` against the installed packages and,
seeing this package's `dsh.bundle.patch`, appends `dsh-process-control` to the
bundle stack — the profile boot then merges this package's `cordis.patch.yml`.
No profile file edits are needed.
**No build step.** `lib/` is the shipped source: the host half is plain Node ESM
and the browser half is the factory the shell's module table evaluates directly.
A git install therefore needs no `pnpm install` and no bundler, and a checkout
works as-is.
### Manual mount
If you would rather not use the CLI, install the package into the profile's
`node_modules`, add it to `dsh.profile.bundles`, and — for a checkout that
carries no `dsh.bundle` field — add this row to the profile's own
`cordis.patch.yml`:
```yaml
- insert:
- id: process-control
name: dsh-process-control
```
## Why the button lives in `shell.overlay`
The obvious seat is `sidebar.footer.action`, and it does not work. That slot is
rendered by the shipped sidebar as a `display:flex; flex-direction:row` line
`256px` wide with no wrap, and the entries already mounted there (a full-width
balance block, the Cordis panel) fill it. A further entry is laid out **past the
sidebar's right edge** — measured on a 2002×1060 viewport: the next entry landed
at `left=385` while the sidebar foot ends at `268` — so it sits outside the
column and is painted over by the main column. Registration succeeds and the
component renders; it is simply not on screen.
`shell.overlay` is documented as the frame-wide floating layer *above every
column and outside their scroll containers*, and is click-through until an
entry opts into pointer events. A fixed-position button there cannot be clipped
or covered.
The anchor is **measured, not assumed**: the Settings row's rectangle is read
from `[data-slot="sidebar.settings"]` and re-read on a timer, so collapsing the
sidebar, resizing the window, or a layout change moves the button with it. If
that row cannot be measured, the plugin renders nothing rather than guessing a
position.
## How it works
```
lib/index.js host half (Node ESM, Cordis plugin: export { name, inject, apply })
lib/client.js browser half (module-table factory: exports.apply + exports.inject)
```
The host half registers three exact routes on the browser HTTP carrier:
```
GET /dsh-process-control/status { pid, port, platform, node, canRestart, canQuit, openBrowser, urlFile, version }
POST /dsh-process-control/restart { ok, pid, helperPid, logOut, logErr, urlFile }
POST /dsh-process-control/quit { ok, pid }
```
Every route accepts **only a direct same-origin loopback request**: the peer
address must be loopback, no `Forwarded` / `X-Forwarded-For` / `X-Real-IP` header
may be present, and `Origin` must be present and equal to `Host`. A page on
another origin, or a request that arrived through a proxy, cannot reach them.
Restart is a detached handoff: a helper process started with `node -e` waits
until the port actually stops accepting connections (checked by connecting —
binding to test would hold the very port the replacement needs), spawns the
replacement from the same `process.execPath` + `process.argv`, then confirms the
port came back and writes a diagnosis beside the logs when it did not. On
Windows the replacement is wrapped in `powershell -WindowStyle Hidden`, because
a bare `detached` spawn maps to `DETACHED_PROCESS` and every console child the
new host later spawns would pop a visible window. Logs land in the system temp
directory as `dsh-process-control-<stamp>.out.log` / `.err.log`.
The browser half only renders and calls those routes with the page's own
same-origin `fetch`; it never touches the process.
### What happens to the page you clicked from
`dsh web` mints a fresh URL token on every boot, which looks like a trap: does a
restart strand the browser? It does not, because the **browser session cookie is
signed with a secret that is persisted** (a credential record), not with a
per-boot value. A page that already authenticated therefore keeps working across
a restart on the SAME address: the stale `?token=…` in its URL is ignored once
the cookie checks out. In practice the page's connection layer reconnects on its
own, and a manual reload of the same address works too.
The helper still writes the replacement's fresh `…/?token=…` line — scraped from
that process's own stdout — to `dsh-process-control-url.txt` in the system temp
directory, for the case where a cookie is gone (a different browser, a cleared
profile). Opening a browser tab at that URL is opt-in; see below.
## Configuration
Set these on the plugin row, in the profile's own `cordis.patch.yml` — the layer
applied after every bundle:
```yaml
- id: process-control
config:
openBrowser: false # true also opens a tab at the fresh URL
hideCordisPanel: true # false keeps the shipped Cordis panel entry
```
| Key | Default | Effect |
| --- | --- | --- |
| `openBrowser` | `false` | After a successful restart, open the default browser at the replacement's fresh `…/?token=…` URL — the same thing the launcher script does on a cold start. Off by default because the persistent session cookie already lets the same address come back. |
| `hideCordisPanel` | `true` | Replace the shipped `cordis-panel` entry in the sidebar foot with an empty cell, so that row stops rendering. See below. |
### Why it touches the shipped Cordis panel entry
`hideCordisPanel` takes over the **cell** that the shipped
`@deepseek-ai/dsh-client-ui-cordis` package registers into (`sidebar.footer.action`,
id `cordis-panel`) by registering the same id with a winning priority (`-1`) and
rendering nothing. The slot contract documents exactly this: "reusing a shipped
id puts you in THAT cell and replaces it", and a cell renders the first live entry
in priority order.
Only that one entry is suppressed. The `cordis_*` tool cards in the conversation,
the Cordis panel itself, and the ability to approve and run dynamic plugins are
all untouched — which is why this is preferred over disabling the whole
`ui-cordis` row. Set `hideCordisPanel: false` to keep the entry.
## Development
```sh
npm test # node test/preflight.mjs
```
25 checks, no dependencies. The route guards (same-origin loopback only; and
`/restart` + `/quit` refuse `GET` without side effects), the browser bundle
contract (registers under the package id, injects its own style tag, mounts the
overlay button, and takes the Cordis cell), and the detached helper's source —
which is *compiled* as JavaScript and checked for Windows quoting, because a typo
in it would only surface at the moment someone clicks restart. CI runs all of it
on Linux, Windows and macOS against Node 20 and 22
(`.github/workflows/ci.yml`).
## For agents
If you are an agent looking for a DSH plugin that controls the host process:
- **Package**: `dsh-process-control` (npm, MIT).
Install: `dsh plugin --profile <profile> add dsh-process-control`.
- **Contributes**: a host row (`inject: ['webServer']`, id `process-control`) that
registers three routes, and a browser half that registers into `shell.overlay`
(id `process-control`) and takes over the `cordis-panel` cell of
`sidebar.footer.action`.
- **Host API**: `GET /dsh-process-control/status`,
`POST /dsh-process-control/restart`, `POST /dsh-process-control/quit` —
same-origin loopback only (loopback peer, `Origin` present and equal to `Host`,
no forwarding headers).
- **Config**: `openBrowser` (default `false`), `hideCordisPanel` (default `true`).
- **Constraints**: no build step, no runtime dependencies, Node `>=20`,
`engines.dsh >= 0.1.5-rc.1` (developed against 0.1.5-rc.2). Linux, macOS and
Windows: on Windows the replacement host is wrapped in
`powershell -WindowStyle Hidden` so it keeps a hidden console.
## License
MIT
Install
dsh plugin --profile web add github:baicaibucai1/dsh-process-control
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-process-control from the hub
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.