dsh-custom-mode 1.9.19 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ # 双语配对一致性记录:两侧在「上次确认一致」时的 git blob 哈希。
2
+ # 两份文档权威相同——改完任意一侧,请把另一侧也改掉,然后重新记录:
3
+ # node tools/verify-translation-pairing.mjs --write
4
+ README.md: 4e45a434fa7cfd4a939cda95c8d79d81395df7fe
5
+ README.zh.md: 22d9b905b579dab2c35faa095e71ffb211ab46f2
package/README.md CHANGED
@@ -1,79 +1,446 @@
1
1
  # dsh-custom-mode
2
2
 
3
+ English | [中文](README.zh.md)
4
+
5
+ **DeepSeek Harness (dsh) custom mode plugin** — edit a mode's system prompt, base mode and plugin switches in
6
+ the settings page, and keep several assistants side by side (multi-mode / multi-persona, coding, chat or
7
+ role-play). · **中文**:DeepSeek Harness(dsh)自定义模式插件 —— 在设置页编辑系统提示词、选择基础模式、逐行
8
+ 开关插件,可并存多个助手(多模式 / 多角色扮演 RP)。
9
+
3
10
  ![dsh-custom-mode — a settings page for dsh agent modes](https://raw.githubusercontent.com/BOWLUNA/dsh-custom-mode/main/docs/images/header.png)
4
11
 
5
- [![npm](https://img.shields.io/npm/v/dsh-custom-mode?label=npm&style=flat-square&logo=npm&logoColor=white&labelColor=1f2430)](https://www.npmjs.com/package/dsh-custom-mode) [![CI](https://img.shields.io/github/actions/workflow/status/BOWLUNA/dsh-custom-mode/test.yml?label=CI&style=flat-square&logo=githubactions&logoColor=white&labelColor=1f2430)](https://github.com/BOWLUNA/dsh-custom-mode/actions/workflows/test.yml) [![license](https://img.shields.io/badge/license-MIT-97ca00?style=flat-square&logo=opensourceinitiative&logoColor=white&labelColor=1f2430)](LICENSE) [![dsh](https://img.shields.io/badge/dsh-%E2%89%A50.1.5--rc.2-4d6bfe?style=flat-square&logo=deepseek&logoColor=white&labelColor=1f2430)](https://github.com/BOWLUNA/dsh-custom-mode#readme)
12
+ <!-- community badge row: required in every new repository (see the storefront template in the operator SOP/DSH Plugins folder) -->
13
+
14
+ [![Star this repo](https://img.shields.io/badge/Star-this%20repo-1f2430?style=flat-square&logo=github&logoColor=white&labelColor=1f2430)](https://github.com/BOWLUNA/dsh-custom-mode/stargazers) [![npm](https://img.shields.io/npm/v/dsh-custom-mode?label=npm&style=flat-square&logo=npm&logoColor=white&labelColor=1f2430)](https://www.npmjs.com/package/dsh-custom-mode) [![CI](https://img.shields.io/github/actions/workflow/status/BOWLUNA/dsh-custom-mode/test.yml?label=CI&style=flat-square&logo=githubactions&logoColor=white&labelColor=1f2430)](https://github.com/BOWLUNA/dsh-custom-mode/actions/workflows/test.yml) [![license](https://img.shields.io/badge/license-MIT-97ca00?style=flat-square&logo=opensourceinitiative&logoColor=white&labelColor=1f2430)](LICENSE) [![dsh](https://img.shields.io/badge/dsh-%E2%89%A50.1.5--rc.2-4d6bfe?style=flat-square&logo=deepseek&logoColor=white&labelColor=1f2430)](https://github.com/BOWLUNA/dsh-custom-mode#readme)
6
15
 
7
16
  <!-- badge rows: same two-part structure (dark label + coloured value) in both rows; every new repo copies this block -->
8
17
  [![bilibili](https://img.shields.io/badge/bilibili-videos-%2300A1D6?style=flat-square&logo=bilibili&logoColor=white&labelColor=1f2430)](https://b23.tv/qJ4Ev0W) [![Douyin](https://img.shields.io/badge/Douyin-shorts-%23FE2C55?style=flat-square&logo=tiktok&logoColor=white&labelColor=1f2430)](https://v.douyin.com/VWh0M03Fa4Y/) [![RedNote](https://img.shields.io/badge/RedNote-notes-%23FF2442?style=flat-square&logo=xiaohongshu&logoColor=white&labelColor=1f2430)](https://xhslink.cn/o/A7QtXmePBBF) [![Discord](https://img.shields.io/badge/Discord-chat-%235865F2?style=flat-square&logo=discord&logoColor=white&labelColor=1f2430)](https://discord.gg/pz97SfAfSy) [![GitHub](https://img.shields.io/github/discussions/BOWLUNA/dsh-custom-mode?label=GitHub&style=flat-square&logo=github&logoColor=white&labelColor=1f2430)](https://github.com/BOWLUNA/dsh-custom-mode/discussions)
9
18
 
10
- <!-- community badge row: required in every new repository (see the storefront template in the operator SOP/DSH Plugins folder) -->
19
+ A custom mode for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (dsh). Its
20
+ system prompt is a plain file you can edit on the Web settings page, and an edit takes effect on the
21
+ **next model step** — no restart, no new session.
11
22
 
12
- The **settings-page half** of [dsh-custom-mode](https://github.com/BOWLUNA/dsh-custom-mode) — a Web UI for
13
- [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (dsh) agent modes: choose a mode's base
14
- composition, toggle the plugin rows it mounts, and edit its **system prompt**, which is a plain file the
15
- agent loop re-reads before every model step — **a save applies on the next step, with no restart and no new
16
- session**. Several modes ("assistants") live side by side, each with its own prompt.
23
+ In one line: **a settings page for dsh agent modes** — choose a mode's base composition, toggle the plugin
24
+ rows it mounts, and edit its system prompt, which the agent loop re-reads before every model step. Several
25
+ modes ("assistants") can live side by side, each with its own prompt.
17
26
 
18
- Searched for as: custom mode · custom prompt · system-prompt editor · multi-mode / several assistants ·
19
- multi-agent · roleplay (RP) / chat personas ·
20
- 自定义模式 · 自定义提示词 · 多助手/多模式 · 角色扮演/聊天人格(RP).
27
+ Also searched for as: custom mode · custom prompt · system-prompt editor · multi-mode / several assistants ·
28
+ multi-agent · roleplay (RP) / chat personas.
21
29
 
22
- ![Assistant manager](https://raw.githubusercontent.com/BOWLUNA/dsh-custom-mode/main/docs/images/05-assistant-manager.png)
30
+ **Two dsh lines are supported: the newest line and the previous stable** — both checked on every push,
31
+ neither merely claimed. npm's `latest` and `next` have both pointed at `0.2.0-rc.2` since 2026-09-29, so
32
+ "latest stable" and "latest preview" are one release now; the second leg stays because most installs in
33
+ the field are still on `0.1.7-rc.2`:
34
+
35
+ | dsh | role | status |
36
+ | --- | --- | --- |
37
+ | `0.2.0-rc.2` | **newest line** — npm's `latest` *and* `next`, and the line the official desktop app ships (it is version-locked to dsh) | ✅ CI (ubuntu node 20/24 + **windows**) + a real instance: install, composition and the seeded preset verified on this line (the full render gate runs here) |
38
+ | `0.1.7-rc.2` | previous stable — still inside the declared range, and what most existing installs run | ✅ CI + a real instance: verified rendered on this line since 1.9.15 |
23
39
 
24
- ![Mode name and base mode](https://raw.githubusercontent.com/BOWLUNA/dsh-custom-mode/main/docs/images/01-mode-switch.png)
40
+ Older builds (`0.1.5-rc.3`, `0.1.6-alpha.*`) use the same mechanisms and remain inside the declared peer
41
+ range, but they no longer get a CI leg of their own.
25
42
 
26
- ![Plugin switches](https://raw.githubusercontent.com/BOWLUNA/dsh-custom-mode/main/docs/images/02-plugin-switches.png)
43
+ The four official modes (`standard` / `ptc` / `minimal` / `cordis`) are untouched.
27
44
 
28
- ![System prompt](https://raw.githubusercontent.com/BOWLUNA/dsh-custom-mode/main/docs/images/03-system-prompt.png)
45
+ | | |
46
+ | --- | --- |
47
+ | ![Mode name and base mode](https://raw.githubusercontent.com/BOWLUNA/dsh-custom-mode/main/docs/images/01-mode-switch.png) | ![Plugin switches](https://raw.githubusercontent.com/BOWLUNA/dsh-custom-mode/main/docs/images/02-plugin-switches.png) |
48
+ | ![System prompt](https://raw.githubusercontent.com/BOWLUNA/dsh-custom-mode/main/docs/images/03-system-prompt.png) | ![Mode picker](https://raw.githubusercontent.com/BOWLUNA/dsh-custom-mode/main/docs/images/04-preset-picker.png) |
29
49
 
30
- ![Mode picker](https://raw.githubusercontent.com/BOWLUNA/dsh-custom-mode/main/docs/images/04-preset-picker.png)
50
+ The assistant manager, cropped to its own section. All five images are English, **all exactly 800x800**, and
51
+ shot on a throwaway `DSH_HOME` with no sessions by `tools/screenshots/shoot-fresh.mjs` (the rendered page
52
+ is what `tools/browser-verify.mjs` checks):
53
+
54
+ ![Assistant manager](https://raw.githubusercontent.com/BOWLUNA/dsh-custom-mode/main/docs/images/05-assistant-manager.png)
31
55
 
32
56
  ## Install
33
57
 
58
+ One command installs everything — the settings-page plugin, and the preset it seeds on first
59
+ activation:
60
+
61
+ ```sh
62
+ dsh plugin --profile web add dsh-custom-mode@1.11.0 # pin the version to get this one for sure
63
+ # A bare `add dsh-custom-mode` is subject to pnpm's release cooldown (`minimumReleaseAge`, 1 day by
64
+ # default): for hours after a release it can silently install an OLDER version — measured: a bare
65
+ # install 38 minutes after 1.3.0 shipped landed on 1.0.3. Check what you got with `npm ls
66
+ # dsh-custom-mode` inside the profile, or pin the version as above.
67
+ ```
68
+
69
+ Restart dsh afterwards, then choose「自定义模式」for a new session. The preset is written to
70
+ `$DSH_HOME/.agent-presets/custom/`; anything already there is left alone, so a `prompt.md` you wrote
71
+ yourself is never overwritten.
72
+
73
+ **Platforms**: the suite runs on every push under Ubuntu (Node 20 and 24) **and Windows (Node 24)** — the
74
+ Windows job exists because that platform has its own failure modes (MSYS paths in `install.sh`, `rename`
75
+ locking under concurrent saves, platform expressions evaluating the other way). Both shell scripts work in
76
+ any POSIX shell, Git Bash included.
77
+
78
+ ### Installing from the interface (no terminal)
79
+
80
+ From dsh `0.1.6-alpha.2` there is a Plugins page: **sidebar → Plugins → Add plugin**. It takes three
81
+ kinds of input, and all three work here:
82
+
83
+ | Input | What to paste |
84
+ | --- | --- |
85
+ | **Package name** | `dsh-custom-mode` |
86
+ | **GitHub repository URL** | `https://github.com/BOWLUNA/dsh-custom-mode` (the repository root) |
87
+ | **Local plugin directory** | `<your clone>` — the repository root, which *is* the package |
88
+
89
+ There is also an **in-app market** for browsing the whole ecosystem: install `dshmarket`
90
+ (`dsh plugin --profile web add dshmarket`), open **Settings → Plugin Market** and search
91
+ `dsh-custom-mode` — its cards read the `engines.dsh` range this plugin declares, and the curated shots
92
+ listed in the repository's `screenshots.json` (five images; they are the same `docs/images/*.png`
93
+ files the README shows, so each picture exists once).
94
+
95
+ All three work because the repository's **root `package.json` is the published package**: `dsh.bundle`,
96
+ `main` and `exports["./client"]` are declared there, so an npm install and a GitHub-URL install fetch
97
+ the same files. Before 1.10.0 this repository carried two manifests — a `private: true` wrapper at the
98
+ root plus `editor/package.json` — and that is what made third-party catalogues render the plugin as
99
+ `dsh-custom-mode#editor` and what made dshfind-derived cards read the wrapper's `private: true` and
100
+ report "not published to npm". One manifest at the root, and `test/manifests.test.mjs` keeps it that way.
101
+
102
+ ### On the desktop app
103
+
104
+ The desktop application is part of dsh itself (`apps/desktop`), version-locked to it — Electron and
105
+ `@deepseek-ai/dsh` always carry the same exact version — and it is **the complete Web application in an
106
+ Electron shell**, so this plugin's page renders there unchanged. One install path, and it is the one above:
107
+
108
+ - **Install from inside the app**: sidebar → **Plugins** → add plugin → search `dsh-custom-mode`.
109
+ The app runs the shared plugin manager with its own bundled pnpm.
110
+ - **Do not try to use the CLI for it — dsh itself refuses** (measured on 0.1.7-rc.2):
111
+ ```
112
+ $ dsh plugin --profile desktop add .
113
+ error: profile "desktop" is managed exclusively by the Electron application
114
+ ```
115
+ The desktop owns `$DSH_HOME/profiles/desktop`: package operations there hold a profile transaction lock
116
+ and startup recovery renames `cordis.patch.yml`. `install.sh` therefore **refuses** `--profile desktop`
117
+ too and points you at the app, so the two agree instead of one silently working around the other.
118
+ (A lab simulation of the profile shape can override it with `DSH_ALLOW_DESKTOP_PROFILE=1`.)
119
+ - **Nothing else to do on the preset side**: `$DSH_HOME/.agent-presets/` is product data shared by the
120
+ desktop app and the CLI, and the preset is seeded by the plugin on first activation.
121
+ - If a third-party bundle ever keeps the app from starting, the native recovery dialog offers
122
+ **Disable third-party plugins**; installed packages and the plugin's own data stay on disk.
123
+
124
+ **Compatibility is declared, not assumed.** dsh checks every plugin's
125
+ `peerDependencies["@deepseek-ai/dsh"]` against the running runtime (prereleases included), and since
126
+ 0.2.0 that check is an **installation gate**, not a warning: a range that does not cover the runtime
127
+ means `dsh plugin add` refuses the package outright
128
+ (`installation rejected: Plugin … is incompatible with dsh 0.2.0-rc.2`). The declared range therefore
129
+ *is* the support statement, which is why it moves only after the new line has been installed and tested
130
+ — a range that "probably" works is exactly the kind of claim that turns into a support ticket.
131
+
132
+ To keep the sources around as well — or to install without npm — clone and run the script, which does
133
+ the same two things explicitly:
134
+
34
135
  ```sh
35
- dsh plugin --profile web add dsh-custom-mode
136
+ git clone https://github.com/BOWLUNA/dsh-custom-mode
137
+ cd dsh-custom-mode
138
+ ./install.sh # copies preset/ into $DSH_HOME/.agent-presets/custom/ and installs the plugin
139
+ ./uninstall.sh # removes the plugin; keeps your prompt unless you pass --purge
36
140
  ```
37
141
 
38
- **That one command is the whole install.** The package carries the agent preset, and the host half writes it
39
- into `$DSH_HOME/.agent-presets/` on first activation — filling in only what is missing, so a `prompt.md` you
40
- wrote yourself is never overwritten.
142
+ The mode needs a profile that ships `agent-presets` — the `web` profile does, `tui` and `headless` do
143
+ not.
144
+
145
+ ### Trying it without installing anything
146
+
147
+ Every release carries a **`dsh-custom-mode.dshpreset`** beside the source
148
+ ([latest](https://github.com/BOWLUNA/dsh-custom-mode/releases/latest/download/dsh-custom-mode.dshpreset)).
149
+ It is a zip holding one ready-to-use assistant: `manifest.json` plus `preset/` — the composition, a
150
+ `prompt.md`, the reader that re-reads it before every model step, and a model-facing tool that reads and
151
+ rewrites that prompt. Import it in the desktop app and you have a working「自定义模式」**without
152
+ installing a package**:
153
+
154
+ | | |
155
+ | --- | --- |
156
+ | **What you get** | one assistant whose system prompt is a plain file. Edit it in any editor — the agent loop re-reads it before every model step, so the change applies on the next step, with no restart. Or just ask the model to rewrite it: the preset ships exactly that tool. |
157
+ | **What you don't get** | the settings page. That is what the plugin adds — the page itself, plus managing **several** assistants side by side and picking one from the new-session menu. |
158
+
159
+ So the preset is the 30-second way to see what this is, and the plugin is the way to live with it.
160
+ Importing the preset does not conflict with installing the plugin afterwards: the plugin seeds its own
161
+ preset under `$DSH_HOME/.agent-presets/` and never overwrites a `prompt.md` you wrote.
162
+
163
+ ## Usage
164
+
165
+ The settings page (Settings → "Custom mode") is an **assistant manager**: the top of the page lists
166
+ every custom mode you have — create, switch, delete — and the four blocks below (name, base mode,
167
+ plugin switches, system prompt) edit **whichever one is selected**.
168
+
169
+ - **Ordering** — "Move up / Move down" writes the order into each assistant's `preset.yml` (`order`,
170
+ the roster's own sort key), so it survives a restart and the new-session picker follows it.
171
+ - **Import / export a prompt** — "Export prompt" saves the current text as a `.md`; "Import prompt"
172
+ reads a file into the **editor** (nothing is written until you save), so an import goes through the
173
+ same `{{…}}` validation as anything typed.
174
+ - **Asking the agent to change its own prompt requires your approval.** The in-session `custom_prompt`
175
+ tool goes through the platform's approval seam (`tools/pre-execute` returning `ask`), so the request waits
176
+ for an explicit「允许一次」and shows what would be written and where. Measured: with the `ask` approval
177
+ policy the panel appears and approving really writes; with `never` (full access) nothing prompts and the
178
+ call is **denied**. The worst case is therefore "the change does not happen", never "it happened quietly".
179
+ - **配置了却不生效会被点名** — the page warns when a setting cannot take effect: the「身份(系统提示词)」
180
+ row is off while `prompt.md` still has content (so your prompt is silently ignored), the `custom_prompt`
181
+ tool row is off, the assistant has no name or no description (the picker shows the bare id /「暂无描述」).
182
+ - **The agent can change its own prompt, with your approval** — the in-session `custom_prompt` tool reads the
183
+ prompt, replaces it, or **appends** to it. Appending matters because it never has to *reproduce* the whole
184
+ prompt: an agent that wants to remember one rule cannot lose existing content on the way (measured: it may
185
+ still read first to see what it is adding to). Both writing actions go through the platform's approval panel
186
+ first.
187
+ - **Change history** — every save, *and* any change made outside this page (the in-session
188
+ `custom_prompt` tool, a hand edit of `prompt.md`), leaves a version in the history list under the
189
+ prompt box, labelled with when and where it came from. Loading one only edits the draft: nothing is
190
+ written until you save, so browsing old versions cannot destroy the current one. Before this, a prompt
191
+ changed from inside a session was invisible — the page only ever showed "the current text".
192
+ - **Reset to the factory prompt** — one click puts the **shipped template** (the text a new assistant
193
+ starts from) back into the editor. It is draft-only like every other edit: nothing is written until
194
+ you save, and Reload discards it. Before this existed, a prompt you had edited into a corner could
195
+ only be recovered by deleting the assistant and creating it again.
196
+ - **New assistant** — type a name and click "New assistant". It is seeded from the packaged template:
197
+ the full Standard row set plus a starter prompt, selectable in a new session as soon as you save it —
198
+ **in an already-open page the picker's list is a load-time snapshot, so refresh (F5) once to see a new
199
+ assistant there** (measured; the roster itself is up to date).
200
+ - **Duplicate** — copies the selected assistant's prompt, base mode and row switches into a new one;
201
+ the two are independent afterwards.
202
+ - **Each assistant is independent** — its prompt, base mode and row switches are its own; changing one
203
+ leaves the others alone.
204
+ - **Base mode is the row set, not the prompt** — it decides which rows exist and which tools the mode has;
205
+ this plugin always replaces the base's `persona` row with its own reader (`complete: false`), so the
206
+ base's *prompt* semantics are **not** inherited. Minimal is the visible case: you get minimal's tool set,
207
+ not minimal's prompt.
208
+ - **A fifth base mode: Custom — the union of every shipped row.** The first four are dsh's own; the fifth is
209
+ **synthesised by this plugin**. Its row set is the union of those four, and a row declared by more than one
210
+ mode keeps the text and shipped default of whichever mode comes first in `standard → ptc → minimal →
211
+ cordis` order — so standard's shipped state wins where there is a conflict.
212
+ Why it has to exist: a switch can only rewrite a row the *base text already has*, so on Standard the rows
213
+ only PTC declares (`tool-presentation`), only Minimal declares (the persistent-terminal group) and only
214
+ Cordis declares (`tool-cordis`) were **unreachable** — not hidden in the UI, just absent from the composer.
215
+ Measured on `0.2.0-rc.2`: standard 32 rows, PTC 33, Cordis 33, Minimal 7, and the union **40**. Groups
216
+ travel whole with their `isolate` realms and `!!js` conditions intact, so the union cannot invent a row
217
+ that `dsh-agent-presets` would refuse to mount.
218
+ - **The two shells cannot both be on, and the plugin moves the switch for you.** The union contains one
219
+ mutually exclusive pair: Minimal's "persistent terminal" set and standard's `tool-bash` / `tool-pwsh` are
220
+ two implementations of the same thing — both register a tool called `bash`. Enabling both makes
221
+ `dsh-agent-presets` mark the whole mode **broken**, and a broken mode is silently dropped from **every**
222
+ picker while the settings page stays green. So in Custom mode, turning one side on turns the other off,
223
+ and the save message says which row moved. A conflicting combination written outside the page (hand edit,
224
+ another tool) is named by a warning instead of failing silently.
225
+ - **Switching assistants never discards drafts** — each keeps its own unsaved edits, marked
226
+ "Unsaved" in the list; the only path that throws edits away is the reload button, which renames
227
+ itself to say so.
228
+ - **Delete** — a confirmation, then the plugin does the removal itself. Which confirmation depends on the
229
+ line: when the shell hands a third-party client plugin its `RiskConfirmation` atom you get the shell's own
230
+ dialog with a tick-box; on `0.1.7-rc.2` the client seed table only exposes `Button/Input/Switch/Tag/Pill`,
231
+ so it degrades to a native `confirm` (same guardrail, one prompt instead of a dialog). Removal works on both
232
+ lines: `0.1.6` and older have `agentPresets.remove()`, while `0.1.7`+ has no such call at all — there the
233
+ plugin disposes its own registration and removes the directory. It only removes the mode directory from disk:
234
+ **sessions already using it keep running** (their composition was read when they started), and new sessions
235
+ no longer offer it.
236
+ - **Ask the agent** — every assistant ships a `custom_prompt` tool, so a session can read or rewrite
237
+ **its own** prompt.
238
+ - **Edit the file** — `$DSH_HOME/.agent-presets/<assistant id>/prompt.md` is that assistant's single
239
+ source of truth.
240
+
241
+ Only `{{model}}`, `{{cwd}}` and `{{provider}}` are interpolated. An unknown `{{…}}` is rejected when
242
+ saved: the renderer throws on it, which would fail every request in that mode.
243
+
244
+ Every control (buttons, inputs, switches, tags, the confirmation dialog, icons) comes from the
245
+ shell's own `@deepseek-ai/dsh-client-ui-primitives`, so theme, light/dark and future restyling reach
246
+ this page automatically; a shell that does not provide those atoms falls back to built-in plain
247
+ controls with the same behaviour.
248
+
249
+ An "assistant" is **one directory** under the user preset root (default `$DSH_HOME/.agent-presets/`),
250
+ and its directory name is its internal id. The page manages only **presets this tool created** — the
251
+ test is that the directory carries `prompt.md` and that its composition injects identity through
252
+ `prompt-reader.mjs`. Any other hand-authored preset is neither listed nor touched: the page
253
+ regenerates a composition from a base mode, and doing that to a hand-written one would destroy it.
254
+
255
+ ### How this differs from the built-in Plugins page
41
256
 
42
- To keep the source around as well, clone the repository and run `./install.sh` instead; it does the same two
43
- things explicitly, and `./uninstall.sh` undoes them (keeping your prompt unless you pass `--purge`).
257
+ From dsh `0.1.6-alpha.2` the harness ships a Plugins page that can enable and disable plugins live.
258
+ It and this mode's per-row switches act at **different levels**:
44
259
 
45
- The mode is **web-profile only**: `agent-presets`, the service that mounts presets at all, ships with dsh's
46
- `web` profile. In `tui` / `headless` this package activates but registers nothing.
260
+ | | Built-in Plugins page | This mode's per-row switches |
261
+ | --- | --- | --- |
262
+ | Scope | **The whole profile** — what this machine has installed | **One agent mode** — which rows its composition mounts |
263
+ | Typical use | Turn a plugin off globally | Keep Standard fully loaded and trim this mode to what it needs |
47
264
 
48
- ## Requirements
265
+ They coexist: the harness decides what the machine has, this mode decides which of it the mode uses.
49
266
 
50
- - **dsh `>=0.1.5-rc.2 <0.2.0-0 || >=0.1.6-alpha.1 <0.2.0-0 || >=0.1.7-alpha.1 <0.2.0-0 || >=0.2.0-0 <0.3.0-0`**(两条线:最新正式版 `0.1.7-rc.2` 与最新预览版 `0.2.0-rc.1`;更早的 `0.1.5-rc.3`/`0.1.6-alpha.*` 与它们同一套机制,仍在范围内但不单独测)** — declared in `engines.dsh` and as an *optional* peer range. This package never
51
- imports `@deepseek-ai/dsh`; it only reads the host services dsh injects.
52
- - **No build step, no dependencies**: the source is published as-is and uses only Node's standard library.
53
- - The package version is its own line (`1.0.0`, `1.0.1`, …). Which dsh it supports is declared in
54
- `engines.dsh`; see the repository README's "Versioning" section.
267
+ The boundary is **drawn by upstream**: the Plugins page documents that it manages "the profile's bundles
268
+ and their uniquely addressable rows", and states plainly that **agent-preset rows remain read-only**.
269
+ The preset layer is therefore out of its reach — and that is exactly the layer this mode covers.
270
+
271
+ A demonstrable example: `tool-plugin-manager` (the agent-facing install/toggle tool) ships **off in
272
+ Standard and PTC** — only Creator enables it. The official modes give you no way to change that; here
273
+ you flip one switch.
274
+
275
+ ## How it works
276
+
277
+ dsh normally takes the system prompt from a preset's YAML, and `@deepseek-ai/dsh-persona` resolves its
278
+ `prefix` once at mount. This preset registers the same `deployment:persona-prefix` section but makes
279
+ its `text` a **function**, which the agent loop calls before every model step.
280
+
281
+ Two different things therefore decide when a change lands:
282
+
283
+ - **Prompt text** is re-read per step, so an edit applies to the **running** session immediately.
284
+ - **Switches and base mode** rewrite the composition file. `agent-presets` remounts a preset when that
285
+ file's `mtimeMs` and `size` change, so a **new session** picks them up; a running session keeps the
286
+ configuration it started with, which is deliberate — swapping a tool set mid-conversation would be wrong.
287
+
288
+ Row switches are tri-state. A row you never touched stays byte-identical to the shipped one, including
289
+ its `!!js` platform condition and its shipped `disabled` state; an explicit on/off replaces that
290
+ condition with a boolean. Platform expressions are evaluated on the host, so the page shows the state
291
+ actually in force on this machine rather than whether a key exists.
292
+
293
+ Several assistants need no new mechanism: `dsh-agent-presets` already scans **every** directory under
294
+ the user preset root, and re-reads those roots on each roster call, so a directory created just now is
295
+ selectable the next time a session is started. Each assistant's `prompt-reader.mjs` / `prompt-tool.mjs`
296
+ resolves `prompt.md` relative to **its own module location**, so N copies are N independent prompts.
297
+ Creation seeds the packaged template; deletion goes through the platform's `agentPresets.remove()` where it
298
+ exists and through the plugin's own dispose-and-remove path where it does not (0.1.7+),
299
+ which refuses a shipped preset and re-checks that the directory really lives under the writable root.
300
+
301
+ ## Versioning
302
+
303
+ The package version is its **own line** — `1.0.0`, then `1.0.1`, … It does not mirror the DSH release.
304
+ What this plugin supports is declared in `engines.dsh` and the `@deepseek-ai/dsh` peer range in
305
+ `package.json`, and `tools/verify-version-consistency.mjs` (run in CI) asserts that the DSH
306
+ version CI installs and tests falls inside those ranges.
307
+
308
+ **Two lines are supported: the newest line (`0.2.0-rc.2`, which npm's `latest` and `next` both point at
309
+ since 2026-09-29, and the line the desktop app ships) and the previous stable (`0.1.7-rc.2` — still inside
310
+ the declared range, and what most existing installs run)** — declared as
311
+ `>=0.1.5-rc.2 <0.2.0-0 || >=0.1.6-alpha.1 <0.2.0-0 || >=0.1.7-alpha.1 <0.2.0-0 || >=0.2.0-0 <0.3.0-0` (the extra clauses exist because
312
+ semver only matches a pre-release inside a range that names its own `major.minor.patch`), and CI installs both
313
+ and runs the whole suite against each — the rendered gate runs on the **newest** line, because that is
314
+ where the shell's own UI changes land first.
315
+ **The official desktop app (DeepSeek Harness Desktop) is covered too**: it is version-locked to dsh and now
316
+ ships `0.2.0-rc.2`, i.e. the same combination this matrix pins. Install it from inside the app (sidebar →
317
+ Plugins); the CLI is refused for that profile by dsh itself. On `0.2.0-rc.2` measured (2026-09-30, clean
318
+ throwaway `DSH_HOME`): `dsh plugin --profile web add dsh-custom-mode` resolves the package in 640 ms, the
319
+ composition tree carries the row, boot seeds all five preset files, the declarative registry syncs the
320
+ assistant, and the `agentPresets` capability set is unchanged (`list, register, inventory, select,
321
+ document`).
322
+
323
+ Two reasons for the split. A bare `x.y.z` is what directories and markets require before they will
324
+ auto-install a package — several resolve npm `latest` and reject anything carrying a prerelease tag.
325
+ And a version string was never a checkable claim anyway: the declared range is, and it is the thing
326
+ that goes stale when upstream moves. What decides compatibility in practice is still whether the APIs
327
+ below exist, which is what the ranges are for.
328
+
329
+ <details>
330
+ <summary>Coupling points (check these when upgrading dsh)</summary>
331
+
332
+ | Dependency | Failure if it changes |
333
+ | --- | --- |
334
+ | `ctx.systemPrompt.section()` with a **function** `text` | the prompt stops hot-reloading — the point of the project |
335
+ | `agentPresets` remounts on composition `mtimeMs`+`size` | switches need a process restart to apply |
336
+ | `ctx.tools.register()` | loses the `custom_prompt` tool |
337
+ | `ctx.connection.fetch.register({ path, methods, requestBody, fetch })` | settings page 404s — nothing is registered |
338
+ | `kind: 'prefix'` matching both `path` and `path/…` | only the list opens; `/state`, `/create`, `/delete` all 404 |
339
+ | `agentPresets.list()` rows carrying `id` / `trust` / `path`, with `preset.yml` supplying `name` / `description` | the assistant list is empty or unrecognisable |
340
+ | `agentPresets.remove(id)` (legacy only), refusing `trust: 'system'` | 0.1.7+ deletion no longer needs it — the declarative backend disposes the registration and deletes the directory instead |
341
+ | the `/api` channel's fence (Host/Origin + browser auth) | the page cannot authenticate at all; do **not** "fix" it by moving the route to the raw `webServer` table |
342
+ | `ctx.inject(deps, cb)` (scoped wait) | the row parks in `pending` in profiles without a web server |
343
+ | `dsh.client` + `exports["./client"]`, client bundle id == package name | the browser half is not discovered |
344
+ | `settings.section` slot (`id` / `order` / `label`) | page placement and label |
345
+ | **`settings.section` no longer takes `locale:`** (since 0.1.6-alpha.2) | the shell does not hand over a `t` bound to this namespace; the page carries its own dictionaries as a floor — see ARCHITECTURE §15 |
346
+ | `preset.yml`'s `order` participating in the roster sort | move up/down stops working |
347
+ | `ctx.locale.register/bind` | falls back to Chinese |
348
+ | **where the base composition comes from** — `agentPresets.readDocument(<mode>).content` on 0.1.7+, the `@deepseek-ai/dsh-agent-presets` files before that | the base mode and the plugin switches become read-only (the prompt still saves); see `base-composition.mjs` |
349
+ | shipped layout `<presets>/<id>/agent.cordis.yml` and row text shape | base-mode switching breaks |
350
+ | `!!js` platform expressions | platform rows display the wrong state |
351
+
352
+ </details>
55
353
 
56
354
  ## Documentation
57
355
 
58
- The full documentation is bilingual in the repository:
356
+ - [`docs/TROUBLESHOOTING.md`](docs/TROUBLESHOOTING.md) — failures reproduced on a real machine, with symptoms, cause and a way out.
357
+ - [`docs/MEASUREMENTS.md`](docs/MEASUREMENTS.md) — the commands and raw output behind each claim.
358
+ - [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) — why this is two artifacts, and which host APIs it depends on.
359
+ - [`docs/PUBLISHING.md`](docs/PUBLISHING.md) — how the npm package is published.
360
+ - [`AGENTS.md`](AGENTS.md) — agent-facing notes, including the full recipe for verifying the UI in a
361
+ real browser on the lab (`tools/browser-verify.mjs`).
362
+ - [`CHANGELOG.md`](CHANGELOG.md) · [`SECURITY.md`](SECURITY.md) · [`CONTRIBUTING.md`](CONTRIBUTING.md)
363
+
364
+ ## Development
365
+
366
+ ```sh
367
+ node test/run.mjs # 15 suites; resolves the shipped presets itself (0.1.7+ derives them from the host)
368
+ ```
59
369
 
60
- - [README](https://github.com/BOWLUNA/dsh-custom-mode#readme) (English) ·
61
- [中文说明](https://github.com/BOWLUNA/dsh-custom-mode/blob/main/README.zh.md)
62
- - [Troubleshooting](https://github.com/BOWLUNA/dsh-custom-mode/blob/main/docs/TROUBLESHOOTING.md) —
63
- every failure that was actually reproduced, with symptoms, cause and a way out
64
- - [Measurements](https://github.com/BOWLUNA/dsh-custom-mode/blob/main/docs/MEASUREMENTS.md) — the commands
65
- and raw output behind every claim
66
- - [Architecture](https://github.com/BOWLUNA/dsh-custom-mode/blob/main/docs/ARCHITECTURE.md) — why this had
67
- to be two artifacts, and which host APIs it depends on
370
+ Edits to `client.js` are hot-swapped by `@deepseek-ai/dsh-client-hmr` about a second later; the
371
+ host half (`index.mjs`, `composition.mjs`, `meta.mjs`, `paths.mjs`) needs a restart. See
372
+ [`test/README.md`](test/README.md) for what each suite protects, and [`CONTRIBUTING.md`](CONTRIBUTING.md)
373
+ before changing behaviour.
68
374
 
69
- ## Security note
375
+ ## If it helps
70
376
 
71
- The route this package serves runs the platform's own browser-trust check
72
- (`ctx.connection.requestRejection`) before anything else and **fails closed** when that service is
73
- unavailable. Before that check existed, the route could be read and written unauthenticated — see
74
- [SECURITY.md](https://github.com/BOWLUNA/dsh-custom-mode/blob/main/SECURITY.md) for the affected range and
75
- the mitigation.
377
+ A ⭐ on GitHub is what makes a plugin findable in a catalogue of thousands — it costs you nothing and it is
378
+ the whole reason this page keeps getting maintenance. If it is **not** working on your dsh line, open an issue
379
+ with the output of `dsh --version`: that is the fastest path to a fix, and the compatibility table above is
380
+ what came out of the last one.
76
381
 
77
382
  ## License
78
383
 
79
384
  MIT
385
+
386
+ ---
387
+
388
+ ## Built with
389
+
390
+ | | |
391
+ | --- | --- |
392
+ | Model | DeepSeek V4.1 Flash (`deepseek-v4-flash`, provider `deepseek-official`) |
393
+ | Runtime | DeepSeek Harness **0.2.0-rc.2** (newest line) / **0.1.7-rc.2** (previous stable) — the two supported lines; the token figures below are from an earlier `0.1.6-alpha.2` session |
394
+ | Uncached input | 224,058 tok |
395
+ | Cache reads | 125,638,016 tok |
396
+ | Output | 425,539 tok |
397
+
398
+ The whole project, research and dead ends included, cost the DSH client **126,287,613 tokens** at a
399
+ **99.8%** cache hit rate (cache reads ÷ all input, 125,862,074).
400
+
401
+ ### What the review-driven iterations cost
402
+
403
+ Four external reviews turned into releases `1.4.0` … `1.9.0` (the stable-line picker fix, the Windows write
404
+ races, the i18n leaks, the portability fixes, the two UI passes). Measured in the same client:
405
+
406
+ | | |
407
+ | --- | --- |
408
+ | Uncached input | 3,973,904 tok |
409
+ | Cache reads | 711,962,624 tok |
410
+ | Output | 1,559,791 tok |
411
+
412
+ That is **717,496,319 tokens** (3,973,904 + 711,962,624 + 1,559,791) at a **99.4%** cache hit rate
413
+ (711,962,624 ÷ 715,936,528). Counting the first pass as well, the project stands at **843,783,932 tokens**,
414
+ **99.5%** cached (837,600,640 ÷ 841,798,602).
415
+
416
+ ## Updating
417
+
418
+ The plugin lives in the profile's `node_modules` and the platform owns that installation, so updating means
419
+ installing again — your data is not touched:
420
+
421
+ ```sh
422
+ # the pinned form: what you ask for is what you get
423
+ dsh plugin --profile web add dsh-custom-mode@1.11.0
424
+ # then restart the DSH process that serves the web profile
425
+ ```
426
+
427
+ **Do not install by bare name if you want this release.** pnpm applies a release cooldown (`minimumReleaseAge`,
428
+ 24 hours by default) and a bare `dsh plugin add dsh-custom-mode` resolves to *the newest version older than 24
429
+ hours*. Measured on a clean machine while 1.9.0 was latest: the bare command installed **1.0.1**. Pinning the
430
+ version bypasses the cooldown, which is why the command above carries `@`.
431
+
432
+ **How to know what you are running**: the settings page shows the installed version at the bottom (`插件版本`).
433
+ Compare it with `npm view dsh-custom-mode version`.
434
+
435
+ **What an update does not touch**: `$DSH_HOME/.agent-presets/<your assistants>/` — `prompt.md`, `preset.yml` and
436
+ your row switches are yours. Seeding only fills in *missing* files, so a prompt you wrote is never overwritten.
437
+
438
+ **If the base mode and the plugin switches are greyed out with a one-line warning**: this dsh line exposes no
439
+ shipped composition to this plugin (the resolver tried the host's `readDocument()`, the legacy presets package
440
+ and the packaged `dsh-web-app` patch — the host log names each attempt). The system prompt still saves on its
441
+ own; nothing else about the mode is touched. On 0.1.7 that was a hard 500 until 1.9.13.
442
+
443
+ **If a mode stops appearing in the picker after an update**: an assistant created by an older version keeps its
444
+ old composition file, and if that file enables a plugin row this dsh line does not ship, the platform marks the
445
+ whole preset broken and silently drops it while the settings page keeps working. Open the settings page: it says
446
+ so and offers **「Fix for this line」**, which turns exactly those rows off and leaves everything else alone.