Back to home@DDA-DIGITAL

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
GitHub repo

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 dshweb wrapper.
  • No runtime dependencies. Only Node built-ins.
  • Inert when unsupported. No webServer / no appExit → 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 modeWhat 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

SymptomWhat to do
The buttons are missingCheck 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 dotThe 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 authenticateThe port changed. Do not start the server with --port 0, and keep the wrapper's flags identical to the original invocation.
Shutdown card stays openExpected on an ordinary tab — press ⌘W. It closes by itself in a PWA/app window.
A supervisor (launchd/systemd/pm2) keeps resurrecting the serverUnsupported: shutdown stops the process, the supervisor starts it again.

How it works

  • Host half (lib/index.js) registers a prefix route on webServer at /dsh-web-lifecycle with POST /status, POST /restart and POST /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.js and registers one entry into the sidebar.footer.action slot. 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:

  1. connection.rpc.handle is unusable in @deepseek-ai/dsh 0.1.5-rc.1 — it reaches webServer through a context that never injected it, so every call throws cannot get property "webServer" without inject. Registering the route directly is exactly what the connection plugin does for its own /api route.
  2. ctx.get("connection") returns undefined on a context that never declared it, which silently downgrades the browser fence to loopback-only. The service is therefore named in the inject list 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