# Development guide > ## ⚠️ Mandatory upkeep > > **Every change to this project must update this file and [`ADAPTATION.md`](ADAPTATION.md) in the same change.** > > There is no exception. A commit that adds, removes, or alters behaviour, files, > dependencies, build steps, or DSH interfaces without updating both documents is > incomplete. The release process in this file treats an out-of-date document as a > release blocker, and `npm test` fails when the upkeep block or the bilingual > pairing record is missing. > > When you touch this file, update [`DEVELOPMENT.zh.md`](DEVELOPMENT.zh.md) too and > re-record both hashes in [`README.i18n.yaml`](README.i18n.yaml) — the same rule > the two READMEs follow. ## What changed, where it goes | Kind of change | Document | |---|---| | New feature, changed behaviour, new/removed file, dependency bump, build or release step | **this file** | | New DSH release, interface change, compatibility range, adaptation checklist | **[ADAPTATION.md](ADAPTATION.md)** | | Anything a user sees (behaviour, option, limitation) | **[README.md](README.md)** + [`README.zh.md`](README.zh.md) | | Every release | [`CHANGELOG.md`](CHANGELOG.md) | ## Architecture One package, two halves — the DSH dual-face plugin shape: ```text package.json dsh.bundle ─┘ └─ dsh.client │ │ cordis.patch.yml │ (the scan serves ./client) │ │ lib/index.js ─── HTTP ───▶ lib/client.js ──▶ React slots (Host half) (Browser half) ``` **Host half — `lib/index.js`.** Injects the `webServer` and `timer` services and owns the window process, the HTTP surface, the `
` injection, and the persisted preference. Every registration is disposed on unload: routes through the `disposers` array, the injection listener through `ctx.on` (effect-scoped), the auto-open delay through `ctx.timeout` (also an effect), plus the child process tree. **Browser half — `src/client.js` → `lib/client.js`.** Claims two slots: | Slot | Entry | Purpose | |---|---|---| | `conversation.session.header.actions` | `desktop-window`, order 30 | the open/close button, state polled from `/desktop-window/status` | | `settings.general.item` | `desktop-auto`, order 30 | the auto-open switch | **Pure decisions — `lib/window-spec.js`.** Icon table, manifest body, favicon links, route→icon mapping, browser candidates, launch flags. No I/O, no Cordis, so it is directly unit-testable. Anything the tests need to reason about belongs here rather than inline in `lib/index.js`. ## Layout ```text package.json dual-face declaration (dsh.bundle + dsh.client) cordis.patch.yml composition layer: one plugin row lib/index.js host half: window process, HTTP routes, injection, state lib/window-spec.js pure decisions — unit-tested without DSH lib/client.js browser bundle (build artifact, committed) src/client.js client source (React) scripts/build-client.js build script (esbuild + registration shell) scripts/install.ps1 one-shot installer scripts/create-shortcut.ps1 one-shot desktop shortcut DSH-Desktop.cmd double-click launcher assets/ official DeepSeek whale icon (SVG / multi-size PNG / ICO) test/ logic tests (node:test) — no DSH install required ``` ## Environment ```sh npm install # esbuild only; react is a peer, provided by the DSH shell npm run build # regenerate lib/client.js from src/client.js npm test # four suites, no DSH install and no browser needed ``` Node.js `>= 22` is required by `package.json#engines`. `dsh` itself is **not** needed for development: the tests exercise the host half through an in-process Cordis/WebServer stand-in. ## The two hard rules for the client bundle `lib/client.js` is committed so installers never build. Both rules below were established by real failures and are enforced by `test/client-bundle.test.js` — do not "simplify" them: 1. **The `__ModuleLoader__.load` registration shell is mandatory.** The DSH client module system verifies that a bundle registered its factory; bare CJS output is not recognised. The registered `id` must equal the package name, because the module graph keys entries by package name. 2. **`react` must stay external.** Official bundles `require("react")` against the module table's seed word — the shared React the shell kernel registers. Bundling a second copy breaks Hooks. ## Platform abstraction Platform-dependent behaviour is a **pure decision plus a thin executor**, and the decision always lives in `lib/window-spec.js`: | Decision | Function | Executor | |---|---|---| | Which browser executables to try | `browserCandidates(platform, env, joinPath)` | `resolveBrowser()` — checks existence in order | | How to end the window process tree | `windowTermination(pid, platform, graceMs)` | `terminateWindow()` — runs the returned plan | The platform, the environment, and the path joiner are all parameters. That is what makes a macOS or Linux branch verifiable from a Windows machine; `test/platform.test.js` drives every platform's branch there. `test/platform.test.js` also asserts that `lib/index.js` contains none of these decisions — no `taskkill` call, no install-location literals, no signal names, no `process.platform ===` branch — because a decision inlined there escapes the tests that cover the other platforms. Why the branches exist at all: - Browser discovery differs by platform: Windows splits machine-wide (`Program Files`) from per-user (`LOCALAPPDATA`) installs, macOS keeps the executable inside `