Skip to content
dsh.fish
Bundle

dsh-quick-reload-lyjs

Quick Reload — a one-click page refresh in the DSH sidebar foot, so hot-injected client plugins take effect without quitting the app.

Source
lyjsyyds
License
MIT
Updated
Updated 9 days ago

Readme

# dsh-quick-reload

One-click **刷新页面** button for the DSH Web UI: a small translucent ⟳ pinned to
the frame's bottom-right corner, available in every view.

## Why

Injecting or editing a client plugin updates the Host's cordis tree live, but the
browser half that is already loaded keeps running the old code — so the only
reliable way to see the change used to be quitting and reopening the whole app.
This button reboots the Web page against the freshly published module graph, so
a hot-injected plugin appears immediately.

## What it registers

| Half | Entry point | Work |
| --- | --- | --- |
| Host | `lib/index.js` | nothing (keeps the package a valid bundle row) |
| Client | `lib/client.js` | `shell.overlay` / id `quick-reload` / order 100 / label from locale |

`shell.overlay` is root scope and kind **list** with `replaceRisk: none`: the
button lives in the frame-wide floating layer
(`.BynINW_overlayLayer{z-index:20;pointer-events:none;position:absolute;inset:0}`),
so it is global (mounted with the layout frame, not with a Session), additive
(a fresh `id` can never replace or displace anyone), and it shares a **row** with
nobody — every occupant positions itself.

The layer is click-through, so this entry opts back in with `pointer-events:auto`.

## Why not the other "global-looking" seats

| Seat | Why not |
| --- | --- |
| `sidebar.footer.action` | Renders into `._2H3hWW_footerActions{display:flex}` — one flex **row**. `dsh-context`'s `context-overview` entry there is `width:calc(100% + 4px)` + `margin:0 -2px`, i.e. it wants the whole row; a second entry overlaps it (this was the original bug). Forcing my item to `flex:0 0 36px` would stop the overlap but squeeze the other plugin's label instead. |
| `conversation.session.header.actions` | Session-scoped: it does not exist without an open Session. Also a shared, width-constrained action row. |
| `sidebar.panellist` | Its ids address a main panel — clicking switches the main column. Wrong semantics for an action. |
| `conversation.header.corner`, `shell.leading`, `sidebar.*` singletons | Single slots: registering shadows the shipped UI (`replaceRisk: shadows-shipped-ui`). |

## Placement

Bottom-right, inset 18px — the frame's floating-pill convention. That corner is
free of persistent plugin UI: the only foreign `shell.overlay` entry is
`dsh-market-toast` (a transient install toast), toasts otherwise anchor to the
**top** through the primitives' `overlayTopMargin()` helper
(`max(min, --dsh-frame-top-clearance + 20)`), and dshmarket's own bottom-right
pill (`.nUhMVa_top`) is `position:absolute` **inside** the market panel
(`.nUhMVa_root{position:relative}`), not in this frame layer.

The button is 32px and translucent (`.68` opacity) until hover/focus, so even
when something transient passes underneath, nothing is really hidden.

## What the ⟳ click actually does

A page reload alone can keep serving stale JavaScript: the Host advertises every
bundle under `/plugins/**` with `Cache-Control: public, max-age=31536000,
immutable`. That is fine while a bundle's `rev` moves with it — but the rev is
derived from the artifact's **file metadata** (`mtimeMs` / `ctime` / size), not
from its bytes, so two different builds can share one rev and the year-long cache
then wins. So the click purges the *rebuildable* caches before reloading:

1. **Cache Storage** — every bucket is deleted (including the remote-access shell
   cache `dsh-remote-shell-v1`, which that plugin re-prefetches on its next visit).
2. **Service workers** — every registration gets `update()`, so whatever the SW
   serves comes from the Host again.
3. **Bundle bytes** — the module graph is read from the Host's own HMR stream
   (`GET /plugins/events`, the SSE frame the Host already broadcasts), and every
   `/plugins/**` URL it advertises — plus every one this page already loaded,
   taken from the performance timeline — is re-fetched with `cache:'reload'`.
   Bounded: at most 160 URLs, a 1.2s timeout per request, a 2.2s overall budget,
   so the reload never waits longer than that.

### What it deliberately keeps

`localStorage`, `sessionStorage`, cookies, the profile on disk, your Sessions and
the open conversation are **not** touched. That is where real state lives: per-
Session drafts, pane widths, and the remote-access device credential
(`dsh-remote-device`) — wiping those would lose work or break remote pairing.

### Why it cannot interrupt anything

Nothing in this path reaches the Host process or the agent runtime: no app
restart, no `/dsh-market/restart`, no profile write. A running turn, a background
job or a terminal session keeps running while the page reloads, and the page
reconnects to it afterwards. For a bare reload with no cache work, **Shift-click**
the button.

## Install

The npm package name is **`dsh-quick-reload-lyjs`** (the plain `dsh-quick-reload`
is taken on npm); the repository and the plugin id keep the short name. From the
release tarball — prebuilt, nothing to compile (with the desktop app fully quit):

```powershell
dsh plugin --profile desktop add https://github.com/lyjsyyds/dsh-quick-reload/releases/download/v0.1.1/dsh-quick-reload-lyjs-0.1.1.tgz
```

Or through the GUI — **设置 → 插件 → 安装** — with that same URL as the spec.

From a local checkout (the development form; the CLI writes both `dependencies`
and `dsh.profile.bundles` itself):

```powershell
dsh plugin --profile desktop add link:<absolute path to this directory>
```

## Remove

Uninstall the bundle in **设置 → 插件** (or `dsh plugin --profile desktop remove
dsh-quick-reload-lyjs`) and drop it from `dsh.profile.bundles`; the button disappears
on the next start.

## Notes

- A reload is enough for **client** changes. A change that rewrites already
  loaded Host modules still needs an app restart.
- When the Host re-publishes a file, the new `rev` alone already defeats the
  immutable cache; the revalidation pass exists for the rev-collision case (same
  metadata, different bytes).
- It only reloads the page and clears browser-side caches; it never writes to the
  profile, so it cannot break the plugin set.

## Tests

```powershell
npm test            # or: node tests/quick-reload.smoke.mjs
```

`tests/quick-reload.smoke.mjs` loads the bundle the way the Host's module loader
does (`window.__ModuleLoader__.load`), applies the plugin to a stub context,
renders the overlay entry with a small React stand-in that really tracks state,
and drives the whole click path against stubbed browser globals — Cache Storage,
service workers, the `/plugins/events` SSE frame, the performance timeline and
`fetch`. 19 checks, no dependencies, no network: it also pins the bounds (at most
160 URLs, one revalidation per URL) and proves a wedged origin cannot hold the
reload. A few checks are timing assertions, so the file takes about 3 seconds.

## License

MIT © 2026 LYJS. Source: <https://github.com/lyjsyyds/dsh-quick-reload>

Install

dsh plugin --profile web add github:lyjsyyds/dsh-quick-reload

Profile: web

  • This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.
Source