dsh-web-lifecycle
DSH web plugin: Restart and Shutdown buttons in the sidebar footer. Restart relaunches dsh web on the same host/port so the tab reconnects itself; Shutdown stops the server to free the terminal and closes the browser tab.
- Stars
- 0
- Language
- JavaScript
- Created
- Sep 10, 2026
- Updated
- Sep 10, 2026
Introduction
dsh-web-lifecycle
A DSH web plugin that puts two buttons in the sidebar footer:
sidebar footer → [ ⟳ Restart ] stop the server, bring an identical
instance back on the same host/port,
and reload this page
→ [ ⏻ Shutdown ] stop the server for good so the terminal
is free, then close this tab
Both ask for confirmation first. No more switching to a terminal several times a day to restart the harness — and nothing to clean up when you are done for the night.
- Same port, same session. A restart pins the resolved port, so the browser cookie's authority is unchanged — you stay signed in and your open session comes back after the reload.
- Two restart transports. Works standalone (detached relaunch), and gets
faster and terminal-friendly when launched through the bundled
dshwebwrapper. - No runtime dependencies. Only Node built-ins.
- Inert when unsupported. No
webServer/ noappExit→ nothing mounts, the host is never affected.
Install
# from GitHub
dsh plugin --profile web add github:DDA-DIGITAL/dsh-web-lifecycle
# from a local checkout
dsh plugin --profile web add /path/to/dsh-web-lifecycle
# from npm, once published
dsh plugin --profile web add dsh-web-lifecycle
Then restart dsh web once so the host half loads, and refresh the page for the
client half:
dsh --profile web --dump-config # the row `web-lifecycle` should be listed
Restart
| Launch mode | What happens |
|---|---|
dshweb (supervised) | Exits with code 75; the wrapper relaunches in the same terminal in about a second. |
plain dsh web (detached) | A coordinator in its own process session outlives the dying process, waits for the port to be released, spawns an identical dsh web, health-checks it, and logs the chain to $DSH_HOME/plugins-data/dsh-web-lifecycle/restart.log. |
Either way the page waits for the origin to drop, waits for it to answer, then reloads itself.
Shutdown
Shutdown exits with code 0 — deliberately not 75 — so the wrapper's relaunch condition is false and your prompt comes back:
dshweb: dsh web stopped (exit 0); supervisor exiting
No coordinator is spawned, so nothing can bring the server back. Under plain
dsh web the process simply exits (it was detached, so your terminal was already
free).
About closing the tab
The plugin does try to close the tab for you (window.close()), and that works
when the GUI runs as an installed PWA or app window. For an ordinary browser tab
it will not work: browsers only let a page close windows that a script
opened, and DSH hands the URL to your OS browser, so there is no opener.
So the tab is handled in two steps: try to close, then — if it is still there —
show a full-screen card that says "dsh web has stopped", retitles the tab to
dsh web stopped, and reminds you to press ⌘W / Ctrl+W. There is no
auto-reload on shutdown: there is nothing left to load.
The supervisor wrapper (optional, recommended)
Mode A keeps the server attached to your terminal and makes restarts ~1 second:
install -m 0755 scripts/dshweb ~/bin/dshweb # or /usr/local/bin
dshweb # same flags as `dsh web`
dshweb --port 3080
The wrapper runs dsh web in a loop and treats exit code 75 as "relaunch
me". Ctrl-C still works (SIGINT exits 130, which is not 75, so the loop stops),
and a Shutdown exits 0, which also stops the loop. Without the wrapper the plugin
still works — it just falls back to the detached relaunch.
Configuration
Defaults live in the plugin row (cordis.patch.yml) and are re-applied by the
plugin itself:
- id: web-lifecycle
config:
confirm: true # show the confirmation popover before either action
allowShutdown: true # false hides the Shutdown row entirely
healthTimeoutMs: 30000 # how long a detached restart waits for the new server
logToConsole: true # log the plugin's decisions into the dsh web log
Override the row by id in $DSH_HOME/profiles/web/cordis.patch.yml — that file is
watched live, so the change applies without a restart.
What an action costs
Both actions interrupt everything the process was running: active turns,
subagents, background jobs and scheduled work. Sessions themselves are durable and
reopen after a restart, but anything in flight is gone — which is why the
confirmation popovers are on by default. Turn them off with confirm: false if
you prefer a single click.
After a shutdown, the only way back is a terminal: dshweb or dsh web.
Troubleshooting
| Symptom | What to do |
|---|---|
| The buttons are missing | Check dsh --profile web --dump-config lists web-lifecycle, then restart dsh web and refresh the page. |
| Overlay says "did not come back" | Read $DSH_HOME/plugins-data/dsh-web-lifecycle/restart.log; it ends with the failing startup output. Start the server manually with dsh web. |
| Red status dot | The channel is not answering — the host half is not mounted, or the page is stale. Refresh; if it persists, restart once from the terminal. |
| Restart works but the tab asks you to authenticate | The port changed. Do not start the server with --port 0, and keep the wrapper's flags identical to the original invocation. |
| Shutdown card stays open | Expected on an ordinary tab — press ⌘W. It closes by itself in a PWA/app window. |
| A supervisor (launchd/systemd/pm2) keeps resurrecting the server | Unsupported: shutdown stops the process, the supervisor starts it again. |
How it works
- Host half (
lib/index.js) registers a prefix route onwebServerat/dsh-web-lifecyclewithPOST /status,POST /restartandPOST /shutdown. Requests pass through the host's own browser fence (connection.requestRejection: Host/Origin trust plus the signed cookie), falling back to a loopback check. - Browser half (
lib/client.js) is served by the client-modules host at/plugins/dsh-web-lifecycle/client.jsand registers one entry into thesidebar.footer.actionslot. That slot's container is a flex row, so a second entry would render beside Restart rather than under it; both rows therefore live inside one entry and are stacked by the plugin's own column container. - Exit codes are the protocol: 75 = relaunch me, 0 = stop. Unit tests pin both, because confusing them would turn Shutdown into a Restart.
Two host quirks are worked around deliberately, both found while building this:
connection.rpc.handleis unusable in@deepseek-ai/dsh0.1.5-rc.1 — it reacheswebServerthrough a context that never injected it, so every call throwscannot get property "webServer" without inject. Registering the route directly is exactly what the connection plugin does for its own/apiroute.ctx.get("connection")returnsundefinedon a context that never declared it, which silently downgrades the browser fence to loopback-only. The service is therefore named in theinjectlist and read as a property, which is the only access shape cordis permits.
Development
npm test # node --test: config, argv planning, waits, lifecycle policy, HTTP envelope, client bundle shape
npm run check
Layout:
lib/relaunch.js pure helpers shared by both halves (argv planning, waits, ports, exit codes)
lib/index.js host half: route, restart/shutdown policy, coordinator spawn
lib/restart-agent.mjs detached coordinator (detached restart only)
lib/client.js browser half: footer rows, popovers, status card, tab close
scripts/dshweb supervisor wrapper (exit 75 = relaunch, anything else = stop)
test/ unit tests
License
MIT — see LICENSE.
Repository: https://github.com/DDA-DIGITAL/dsh-web-lifecycle