# Changelog
English | [中文](CHANGELOG.zh.md)
The version number is this package's **own stable line** (`1.0.0`, `1.0.1`, …) and every publish gets
a new one. Which DSH it supports is declared in `engines.dsh` + the `@deepseek-ai/dsh` peer range, and
CI asserts the DSH version it actually installs and tests falls inside them — see the "Versioning"
section of the README. Entries from `0.1.6-alpha.*` and earlier follow the old convention (the version
mirrored the DSH release) and are kept as history.
## [2.3.1] — 2026-11-04
This patch covers editing and draft-safety fixes and adaptation to DSH `0.2.2-rc.1`. See the [version-specific validation and limits](https://github.com/BOWLUNA/dsh-custom-mode/blob/main/docs/CURRENT_COMPATIBILITY.md#231-validation).
- Reject stale saves against prompt, metadata and composition bytes, including missing-file state. Failed audit writes cannot bypass conflicts; save/repair/reorder return only their own exact submitted generations while preserving legacy tokens.
- Keep drafts in the current browser document across settings navigation, without silently advancing dirty baselines. Warn before reload/close and require an explicit draft-discarding read after a conflict; this is not a disk backup.
- Confirm before restoring the starting prompt, importing a file or loading history over unsaved prompt text. Cancellation preserves the draft; replacement still needs saving.
- Confirm that duplication uses the saved configuration when pending edits exist, retaining the original draft and unrelated creation input.
- Add the page-header New assistant entry, focus its form, and prevent composition/IME-confirm Enter from creating an assistant while ordinary Enter remains available. Keep native UI controls.
- Stop cleanup/purge on failed or unverifiable host removal. Bound purge paths, preserve newline-bearing names as complete targets, avoid traversing links, retain shared installations and respect desktop ownership.
- Adapt current exact hosts, including `0.2.2-rc.1`: decode YAML-quoted disable predicates, preserve supported conditions for native loader context, isolate the editor's boolean capability snapshot, keep unknown context conditions disabled and avoid quote-only false overrides.
- Align security, onboarding and release records with actual validation and current host CI targets. Earlier source checks cover 20 suites / 1315 checks, three-host packed/browser/draft/lifecycle results and two-host native API scenarios; they are not new-version publication or remote CI evidence. [Detailed boundaries](https://github.com/BOWLUNA/dsh-custom-mode/blob/main/docs/CURRENT_COMPATIBILITY.md#231-validation) retain the untested Electron, real-model/GPU, OS IME and strict same-document frontend HMR limits.
## [2.3.0] — 2026-10-28
Published on [GitHub Releases](https://github.com/BOWLUNA/dsh-custom-mode/releases/tag/v2.3.0) and [npm](https://www.npmjs.com/package/dsh-custom-mode/v/2.3.0). The completed release gates and downloaded asset checksums are recorded in [current compatibility](https://github.com/BOWLUNA/dsh-custom-mode/blob/main/docs/CURRENT_COMPATIBILITY.md).
- Add a page-header running-version display and a user-initiated update check through the host's native plugin manager. On capable alpha hosts, recommended updates open official Plugins details for confirmation and installation; older hosts receive a details/guide fallback. Keep original file/link/tarball sources, separate query/update permissions and guard all pending drafts before navigation.
- Release owned assistant registrations when the plugin scope stops, fence queued and late synchronization after shutdown, and retain failed cleanup handles for diagnosis/retry. Show bounded, typed synchronization status and per-assistant warnings without treating another assistant's failure as local damage or offering an ineffective composition repair. Document the one-host-restart recovery path for native updates/restores from 2.2.6; see [current compatibility](https://github.com/BOWLUNA/dsh-custom-mode/blob/main/docs/CURRENT_COMPATIBILITY.md) for the local/native results and limits.
- Put installation and the first successful request before screenshots, add direct maintenance/release navigation, and document isolated exact-host preparation. Refresh issue/PR forms and provide a public, no-details request for a private security reporting channel without claiming private reporting is enabled.
- Record 2.3.0 verification and publication separately from 2.2.6's historical evidence. No new updater backend, shell installation mechanism or credential handling is introduced.
## [2.2.6] — 2026-10-27
- Adapt the prompt reader to the scoped working-directory service while preserving old hosts and custom cwd templates. Retire only the exact legacy directory sentence at runtime; user prompt/suffix files are not rewritten.
- Guard asynchronous assistant edits, reloads, history and imports against late responses. Expose separate activation and refresh retries without replaying a completed mutation.
- Recover interrupted multi-file saves before assistant discovery. Preserve ambiguous old backups and report recovery-required state instead of seeding over user data.
- Serialize disk changes with preset synchronization and expose a synchronization-only retry after saved-but-not-applied failures.
- Preserve clone-install metadata, prepare offline archives of the actual old package, and verify package/code rollback. Publish missing user files exclusively after complete staging; retain recovery copies and preserve concurrent user files on failures.
- Keep native UI primitives, replace disabled creation-mode buttons with a Standard-start explanation, and label the alpha host's program/tool executor row without changing shipped composition defaults.
- Fix session-trace double counting with newer Node's concatenated Zstandard decoder, reject embedded frames in truncated skippable payloads, and choose the latest session by home/file type rather than directory-name guesses. Keep exact rc.2/rc.4/alpha.2 gates and real marketplace checks; test-tool failures remain release blockers.
- Clarify first-request setup, editing versus session selection, current recovery and the existing gated publisher flow. Keep historical incidents separate from current instructions. Exact completed checks and platform limits are recorded in CURRENT_COMPATIBILITY.md.
## [2.2.5] — 2026-10-17
- Verify the exact official DSH `0.2.1-rc.2` npm host and upstream tag. Its preset registry, authenticated routes, native settings interface and four base declarations match alpha.5; the existing compatibility range and user configuration remain valid.
- Add `0.2.1-rc.2` to Linux/Windows unit and boot CI and the required packed-install/browser release matrix, retaining `0.2.0-rc.2` and `0.2.1-alpha.5`.
- Correct the marketplace probe to test the inherited row on both alpha.5 and `0.2.1-rc.2`, including native on/save/off/save. Only `0.2.0-rc.2` lacks that row.
- Check 15 suites / 935 checks, 74 rendered browser checks, the mode picker and 22 lifecycle checks on isolated Linux Web profiles. No model or GPU call is made.
Install `2.2.5` and restart the process serving the Web profile. Existing prompts and presets need no migration. Windows/Electron desktop and real model/tool/GPU execution remain unverified. Release still requires the compatibility and browser gates, with plugin archive, preset-only archive and SHA-256 checksums.
## [2.2.4] — 2026-10-17
- Label the official `tool-plugin-market` composition row in English and Chinese on DSH `0.2.1-alpha.5`. Its shipped default remains off; composition text and platform conditions are preserved.
- Update both current-channel CI pins and browser navigation for the alpha.5 More/Account → Settings menu. Reuse one browser page and clear completed protocol timers.
- Verify isolated Linux x64 Web profiles on exact official DSH `0.2.0-rc.2` and `0.2.1-alpha.5`: 15 suites, 935 checks per host; 74 rendered browser checks; five base modes, save/restart, uninstall/reinstall, and user-data preservation. The alpha.5 row is tested through its real native switch and save path; rc.2 does not acquire that row.
- Attach the clean npm plugin archive, preset-only import archive, and SHA-256 checksums to GitHub Releases. Existing compatibility and browser release gates remain required.
DSH rc.2 and alpha.5 are prerelease hosts. Windows/Electron desktop, real model/tool execution, and real GPU execution are unverified. No model was called during these checks.
Install `2.2.4` and restart the process serving the Web profile. Existing prompts and presets need no migration. The `.dshpreset` asset contains only the preset; install the `.tgz` or npm package for the settings page.
## [2.2.3]
- Preserve flow-style, scalar, and alias `agent-presets` settings when choosing a default assistant. Recognize quoted section and default keys, so an existing choice is never overwritten and valid YAML is not corrupted.
- Keep nested `!!js` configuration values in the host loader's expression format. Creator's skill directories now resolve instead of failing schema validation; disk composition text stays intact.
- Use native Input and Button atoms for filtering and disclosure controls. Remove menu, selector, disabled-state, and danger-button repaints; document textareas follow the native InlineEditor surface and font while preserving manual save and ordinary newlines.
- Test the current default host `0.2.0-rc.2` and the new preview `0.2.1-alpha.2` against their own shipped declarations. Tests no longer assume removed Codex/Claude subagent rows exist.
- Add reproducible packed-install and runtime checks, covering every base mode, rendered settings, repeated operations, uninstall/reinstall, and user-data preservation. Release now waits for both compatibility and browser workflows; the browser matrix covers both channels and redacts startup tokens.
- Add read-only discovery adapters and checks for dshmarket, DSH Plugin Hub, and DSH Plugin Store. Stale, unreachable, and unverified directory entries remain explicit limitations; no external listing is submitted.
- Verify candidate installation, activation and removal through the real dshmarket and Plugin Hub UI paths. Store still hides this package under its upstream install-tested-only filter and offers no custom-source input; no Store approval is claimed.
- Make npm publish failures propagate through the release log pipeline with `pipefail`; E403, OTP, successful publication and already-published conflict outcomes were verified without registry writes.
This release targets the Web profile, tested on both current channels. VMware/Electron desktop coverage remains unverified and is not claimed by this release. Store listing approval remains an external distribution limitation.
Install `2.2.3` and restart the profile's DSH process. No prompt or preset migration is required.
## [2.2.2]
### Logic tree
- The settings page is two branches. This assistant keeps all five base modes selectable. New assistant keeps Coding, Writing, and Chat selectable, and shows the official modes with only Standard lit. The other four are dim and cannot be chosen there.
- A new assistant still starts on the official Standard mode. The dim row is the same rule, shown on the page.
### What you need to do
Install `2.2.2` and restart dsh.
## [2.2.1]
### Storefront
- README shows a centered 2×2 of four 800×400 screenshots: assistants, system prompt, base mode, plugin switches. No letterbox.
- npm keywords cover the English search terms and 自定义模式 / 系统提示词 / 提示词编辑.
- Header banner states the same sentence in English and Chinese.
### What you need to do
Nothing, unless you want the pictures. Restart is not required for a README-only install, but installing `2.2.1` still needs a restart of dsh.
## [2.2.0]
### The prompt you save is what a new session starts from
- When `settings.yaml` has no `agent-presets.default`, the plugin writes `custom`. A value you already set, including `standard`, stays. Standard, PTC, Minimal, and Creator remain in the menu.
- Save sits directly under the system prompt. Plugin switches stay behind "Show plugin switches".
- Session logs named `session.v4.jsonl.zstd` are read by `tools/session-trace.mjs`, as well as `session.v3.jsonl.zstd`.
- The save message says the next model step rereads `prompt.md`. It no longer says the current session keeps the old prompt.
- Package description, keywords, and this README no longer claim a role-play preset.
### What you need to do
Restart dsh after installing `2.2.0`. If a new session should stay on Standard, set `agent-presets.default: standard` in `settings.yaml` before the first launch of this version, or change it back afterward.
## [2.1.0]
### A stale settings save no longer discards a prompt written outside the page
- The page sends the newest history number as `baseN`. If `prompt.md` changed after that revision, the save returns `conflict` and does not write. The other text stays on disk and in history. Reload, then decide whether to overwrite.
- A client that omits `baseN` still overwrites. The replaced text is kept in history. A numeric string is not a `baseN`. The prompt-only fallback uses the same rule.
- Plugin rows use one height (`min-height: 74px`). The browser gate reads its labels from the dictionary, and delete confirmation clicks the dialog that is outside `.cpfe`.
## [2.0.1]
### The page fits one screen more closely
- Plugin rows no longer repeat On/Off next to the switch, and a row with no note does not keep an empty second line. Base-mode pills stay on one line.
- Sections are separated by the same hairline the settings pages use. New assistant sits with the assistant block, not after the switches.
- Upgrade notes: restart dsh after install. Existing assistants stay. `prompt.starter.md` is written only when you create an assistant.
## [2.0.0]
### The settings page is the assistant, then its prompt
- Each assistant still has its own system prompt, one of the four shipped base modes, and plugin switches. Choosing nothing, or Coding, creates the same assistant as before.
- Writing and Chat are starters for a new assistant only. They do not change the assistant already on screen. Each stores the prompt it was created with, and Restore reads that file back.
- The page order is the current assistant first: prompt, name, save, base mode, plugin switches. New assistant is its own section at the end.
## [1.13.0]
### Settings page reads as one settings page
- The tools disclosure is a single heading, Tools, with a chevron. It no longer says "Hide capabilities · Capabilities".
- Save sits in normal flow under the prompt. It no longer sticks over the editor. The filesystem path is no longer printed on the page.
- Section introductions are one line. Repeated explanations under the assistant list are gone. Fields use the same surface token as native inputs.
## [1.12.2]
### Storefront
- Header redrawn at 1280×640. It names the product and the install command, and it does not carry a plugin version.
- The five example images were reshot on a throwaway dsh `0.2.0-rc.2` home. Each file is 800×800. The README shows four of them as a centered 2×2, and the assistant view on its own.
## [1.12.1]
### Storefront and host alignment
- Colors and the code font use the host tokens (`--dsw-*`, `--ds-font-family-code`), so dark mode, light mode and third-party themes that retint those variables apply to this page.
- The page follows the shell language. The shell ships Chinese and English. A language pack that selects another language, with no dictionary here, shows English rather than the whole page in Chinese.
- The official one-click installer matches `peerDependencies` and includes prereleases. The current stable line `0.2.0-rc.2` and the preview line `0.2.1-alpha.1` are both inside the range.
- The README no longer carries internal notes or token accounting.
## [1.12.0]
### The settings page leads with the prompt, and writes no longer disagree with what the page shows
- The system prompt and the save bar come before the plugin switches. Capabilities (base mode and row switches) start collapsed, and rows can be filtered by name. The fifth base mode is named "All shipped rows".
- When the host marks a mode broken, its reason is shown under the warning. A prompt-only save says the next model step picks it up.
- Unknown `{{…}}` variables, repeated row ids, and platform conditions that fail to evaluate are named on the page.
- Save, repair, delete, create, and reorder share one write lock. A create that fails after the directory exists removes that directory. Declarative delete moves the directory aside before unregistering.
- The in-session approval dialog uses the current assistant's name. `npm publish` runs only on a version tag.
## [1.11.6]
### Re-measuring the native metrics: two assertions were false, and three UI defects were fixed
The trigger was a one-line request: **the fonts, the UI style and the formatting all have to match the
native UI**. The method was not to read code but to boot **a clean instance on each supported line** and put
the official settings page and ours in the same browser, reading `getComputedStyle` +
`getBoundingClientRect` element by element.
**Two assertions that were already guarded were wrong:**
- **Section title 14px/22px w500 -> the official value is w400.** `AGENTS.md` #13 and
`docs/MEASUREMENTS.md` §29/§35 all recorded w500; measured once on each line, the official `_title` and
`groupTitle` are **both 400**. The only 14px w500 in the shell is the plugin **card** title (line-height
20px, a different shape); page titles are 16px/24px w500.
- **Row padding 12px -> the official settings row is 16px.** 1.11.2 deliberately squeezed it to 12px to take
the row from 76px to 66px, but the comparison target was a **card** (`cardContent`, padding 12px 14px)
while our rows are **settings rows** (1px divider, title left, control right). More to the point, the
cause of that round's "rows ranged 66–84px" was `.cpfe-row-note` missing `white-space:nowrap`, fixed
separately in the same release — lowering the padding was treating a disease that no longer existed.
Back at 16px, the row is **77px**, the same height as the official settings row.
**Three real defects:**
- **The warning block had no CSS at all.** `.cpfe-warns` / `.cpfe-warn-row` / `.cpfe-warn` had **zero rules**
in any stylesheet, so every warning was a run of un-styled `
`. Now 12px/18px, `gap 6px`, and the
shell's warning icon at 14×14 aligned to the first text line.
- **The dropdown was not the native shape.** It used the shell Button's `ghost` + `size:"sm"` (28px /
transparent / 12px text / radius 8px) with the chevron **before** the text. The official control is
36px / radius-md / grey fill / 14px text with the chevron **after** it. Now drawn to those metrics
(`.cpfe-selector`) — each official settings page **defines its own** `_selector`, and the Button atom has
no such variant.
- **Input radii disagreed with each other** (input / desc / editor = 8 / 10 / 10px) while the official
control uses `--dsw-radius-md` (12px) throughout. Unified.
**Terminology and copy:**
- The `cordis` mode is presented as **Creator mode** (Chinese 创造模式) in the official UI. This repository
labelled the pill 「Cordis 模式」 while `base.all.note` in the same dictionary said 创造模式 — one mode,
two names on one page. The four English labels were also all missing the official " mode" suffix.
- Three **dead entries** removed (`assistant.meta`, `aria.expandHint`, `aria.collapseHint`): they existed
only in the dictionaries and were never referenced by any call site.
- The five `SectionHint` call sites each passed four props the component **does not read**
(`id` / `expanded` / `onToggleExpand` / `t`).
- One **inaccurate claim** removed: the English `rows.hint` said the row list was laid out "like the
official plugin list", but the official plugin page is a **2-column card grid** whose descriptions
**wrap to two lines** — not the same thing as our single-column, divider-separated list.
The render gate went **70 -> 74** (the official dropdown shape, the chevron after the text, **the warning
block having rules in the stylesheet**, and the 14px/22px row title); `0.2.0-rc.2` and `0.1.7-rc.2` each
report **74 passed / 0 failed**. Tests are unchanged at 15 suites / **876 checks**.
### A full read-through: ten entries that looked alive but never reached a pixel
Before releasing, I went through the page once more and checked the **reference surface** rather than
impressions. A small tool classified every dictionary entry by its call sites and found ten entries (ten in
each language) that **can never be rendered**:
- **Five `*.short` entries** (`assistant` / `name` / `mode` / `rows` / `prompt`) — the short half of the
collapsible introduction. Since the introduction became a **single always-visible line**, `SectionHint`
renders only `detail`, and every call site passes `detail` ⇒ the `hint` argument is **never used**.
**Visible to grep, invisible on screen** — the hardest kind to notice. Fixed by reducing `SectionHint` to
a single `text` prop and deleting the five short entries.
- **Three `msg.loading` / `msg.notLoaded` / `msg.reread`** — no call site at all.
- **Two `warn.approvalGateMissing.label` / `.hint`** — masked by the `t("warn." + code)` **dynamic family**:
that spelling only ever produces `warn.`, never `warn..label`.
The dictionary went 216 -> **206** entries. The auditing tool itself had a false positive worth recording:
it did not recognise `apiText(x, cond ? "a" : "b")`, so it reported two live entries as dead.
**Terminology and wording:**
- **「底子」→「基础模式」** (three places): one thing carrying two names on one page, while the pill itself is
labelled 基础模式.
- Second person and casual phrasing removed: 「你写的提示词现在不起作用」→「所写的提示词不会生效」;
「手动拨动后以你的为准」→「手动设置后以手动值为准」; 「两份从此各改各的」→「两份配置彼此独立」.
- `tag.essential` in English: `core` -> `Core`, matching the Chinese 基础能力.
**One documentation untruth corrected along the way**: `docs/PUBLISHING.md` still showed the `.dshpreset`
example as `"name": "自定义模式 / Custom mode"`, but 1.11.5 **deliberately dropped the bilingual name**
(`release.yml` changed with it). The document said one thing and the code did another; both now agree.
The render gate reports **74 passed / 0 failed** on both lines; tests are **15 suites / 876 checks**
(unchanged — removing dictionary entries does not move the check count); the real-boot guard passes
A/B/C/D on both. Dictionary parity is pinned by `test/locales.test.mjs`, and this round it earned its keep:
it caught `client.js` and `locales.mjs` disagreeing on their key sets the moment they did.
## [1.11.5]
### The workflow files themselves need validating — their mistakes only surface at tag time
While changing the `.dshpreset` manifest (this release also drops its bilingual name), I indented the new
lines by **4 spaces** inside a `run: |` block whose content sits at 10. A YAML block scalar **ends** at the
first line shallower than its own first line, so everything after it was parsed as YAML — and `release.yml`
became syntactically invalid.
The cost is what makes this worth a release of its own: 876 unit checks, the 70-check render gate and every
ledger guard **all pass**, because none of them reads the workflows. It only blows up at the **next tag** —
at the moment you are already publishing.
- **New `tools/verify-workflows.py`**: validates `.github/workflows/*.yml` with a **real** YAML parser, and
`compile()`s the body of every `<<'TAG'` heredoc after de-indenting it by the tag (valid YAML does not
imply valid code inside it). Wired into `test.yml`, so CI runs it every time.
- **A hand-rolled guard was tried and deleted.** I first wrote a pure-string check in the unit suite, and
mutation testing falsified it immediately: **it did not catch the bug it was written for**, because "block
ended early" and "block ended normally" look identical at the string level — the 4-space line is
structurally plausible as an ordinary terminator, and only a parser can tell. By this repository's own
rule — *a guard that has not been turned red by a mutation is not a guard* — that one was removed in
favour of the real parser above.
- **The `.dshpreset` import name is no longer Chinese/English.** It used to be
`自定义模式 / Custom mode` plus an English description. The reason is the same as for
`preset/preset.yml`: that is **product data**, and the shell localises its own shipped presets but cannot
localise ours. The name is now one language and the description is empty; after importing, the user can
write their own.
15 suites / **876 checks** (unchanged — the deleted unsound guard balances the new one). Two new mutations
verified to go red: a shallow block indent, and broken Python inside a heredoc.
## [1.11.4]
### The "Composition problem" badge, and three pieces of copy around it that either misled or lied
Four changes, all from one screenshot of a real install.
- **Literal asterisks were rendering on the page (a real bug).** Three notes contained markdown such as
`**静默丢弃**`, but the page puts the result of `t(key)` straight into a text node and **does not parse
markdown** — so the user saw the asterisks. All four strings are cleaned, and a general guard now rejects
literal markdown in any locale value (**bold / inline code / links / headings**), on both languages, in
`test/locales.test.mjs`.
- **The copy named the wrong place.** "点**右侧**的「按本线修复」" — the button renders **below**, not to the
right. It now says 下方. The English string never named a position, so it was already correct.
- **The badge went from an implementation detail to a consequence you can act on.** "组成文件有问题" talks
about a file the user has never heard of; it now says what it means for this machine:
`{count} row(s) unusable here` — the count comes from the rows the host's own `broken` names, parsed
server-side and sent with the list — falling back to "Will not start here" when no count can be parsed.
Hovering (the Tag's `title`) gives the full explanation and the next step. It also used to say the same
thing as the banner below it, twice, in two different colours (red tag vs ⚠).
- **One note was simply untrue.** `warn.presetBroken` said "the host's own reason is in Details", while
`state.broken` — the platform's own sentence, like `tool-workflow (…): waiting for workflowEngine` —
**was never rendered anywhere on the page**. Following the instruction led nowhere. It is now printed on
its own line under the warning (monospace, with a title), and the sentence points at it. A platform
diagnostic is not our prose, so it gets its own line instead of being folded into the paragraph.
15 suites / **876 checks** (was 873). Two new mutations verified to go red: putting literal markdown back
into a locale value, and dropping `brokenRows` from `readList`.
## [1.11.3]
### The 1.11.2 description migration never ran — and it could only ever have run for the wrong people
1.11.2 added a migration for the description (clear the bilingual string we shipped years ago, verbatim).
Installed on a real machine, the description did not change by a single character.
The cause was where it was called from: the migration lived inside `seedPresetWithLog()`, but
`seedOnActivation()` **returns early** when `isSeeded(root)` is true — so the one case that actually needs a
migration, "this plugin is already installed", never reached it. A fresh install would have reached it, and a
fresh install is exactly the case that does not need it.
The fix moves the migration into the loop in `seedOnActivation()` that runs on **every** activation, placed
**before** the early return. `dropShippedDescription()` itself is unchanged: it only matches strings we
shipped, verbatim, and never touches text the user wrote.
★ One rule for the list: **next to an "already exists, skip" branch, anything that only happens on the
create path has to be asked "and what about everyone who already has data?"** That is the third time this
sentence has earned its place in this repository.
Also: `tools/ZZ-measure.mjs` — a temporary copy of the render gate used to measure type and spacing on a real
machine in the previous round — had been committed by mistake. It is gone; throwaway scaffolding does not
belong in the repository.
15 suites / **873 checks** (was 869; four new ones: the migration must fire on an already-seeded install, and
user text must be untouched). Mutation M15 (moving the migration back into `seedPresetWithLog`) is verified to
turn the new assertion red.
## [1.11.2]
### Settings-page styling pass: two type sizes, one row height, tighter copy, no more mixed-language description
Four pieces of feedback, and each one started by **measuring the native** UI in the official desktop app
rather than guessing.
- **Four type sizes collapsed into two.** The same role (the note under a block) had two spellings:
`.cpfe-note` was **11px + the secondary colour**, `.cpfe-hint-line` was **12px + tertiary**; with 13px
inputs and 14px titles that is four sizes on one page. Now it matches the native scale: **14/22 titles,
12/18 body** (inputs stay 13px — that is the shell's own input size). Every 11px is gone.
- **"The row spacing is inconsistent" had two causes, both fixed.** First, `.cpfe-note` is a `
`, so the
browser gave it a 1em vertical margin — at 11px that is 11px, which is why two otherwise identical blocks
sat **35px and 24px** apart (measured). It now has an explicit `margin:0` plus a uniform 8px. Second,
**the note wrapped**, so a row with a note was taller than a row without one. Now it matches the native
plugin page: **`white-space:nowrap` + ellipsis**, with a `title` so the text is still readable.
- **Row height 76px -> 66px, and now identical on every row.** The native plugin card is 83px but carries
**two** lines of description; ours was 76px carrying one — half the content, the same height, which is
exactly what "too tall" meant. Vertical padding 16px -> 12px, title line-height 22 -> 20. Then the badge
line was pinned to 18px: `.cpfe-tag`'s 0.5px border made a badged row 1px taller (measured 66 vs 67).
**Every row is 66px now, and an assertion holds it there.**
- **Copy tightened.** Eight notes went from conversational to written ("pick one" -> "select", "This page
manages only the assistants this tool created" -> "Manages only the assistants this plugin created").
Both languages updated together.
- **The description is no longer a Chinese/English run-on.** The `完整编码能力… / Full coding ability…` line
was the "short bilingual" compromise introduced in 1.7.0, and it existed for a real reason: the
description is **product data**, and the shell localises its own shipped presets but cannot localise ours.
One language is foreign in the other UI; both languages is the mixing that got flagged. So the template
**no longer seeds a description** — an absent description is a state the shell already supports, and the
settings page's Description field is always there. Installs carrying **one of our own shipped strings
verbatim** get it cleared; anything the user typed is left alone. The "no description" warning is gone
too: defaulting to empty while permanently showing a warning was self-contradictory.
- **All five screenshots re-shot** (still 800x800).
15 suites / **869 checks** (was 861); the render gate 67 -> **70** (new: identical row heights, non-wrapping
notes, ellipsis carries a title). See `docs/MEASUREMENTS.md` section 35.
## [1.11.1]
### The desktop app: two things that only show up when you really run it
The official desktop app was finally run for real (Electron 152, dsh `0.2.0-rc.2`, profile `desktop`,
`DSH_HOME=~/.dsh`). It works - and it exposed two things the web line cannot show.
- **After the first repair the preset was still broken, and the page said everything was fine.**
Turning off the rows this line cannot *resolve* (`@deepseek-ai/dsh-workflow-worker-thread` is not installed
in the desktop profile) leaves the rows that *wait on it* (`tool-workflow`, `tool-ralph` - "waiting for
workflowEngine") unable to start. At that moment `unresolvable` was empty, `warnings` was empty, and the
mode **still did not appear** in the picker. The plugin now also reads the **host's own `broken`**
diagnostic, extracts the rows it names (`brokenRowIds`), warns on the page when that reason is one our own
check cannot explain (`presetBroken`), and `POST /repair` turns those rows off as well. One click now
converges instead of stalling halfway.
- **The repair button vanished exactly when it was still needed.** It used to render only for
`unresolvableRows`, so after the first repair the warning text told the user to click a button that was no
longer on screen. It now renders for `presetBroken` too.
- **Verified end to end on the real desktop app**: install into the `desktop` profile through the app's own
bundled CLI, relaunch, open the account menu -> Settings -> Custom mode, click "Fix for this line" once,
restart - and **Custom mode appears in the mode picker** (five modes, plus the second assistant's
"Writing helper"). The picker only re-reads the registry at boot, so a restart is what makes a repaired
preset visible.
- **All screenshots re-shot**, each exactly 800x800 (`01` now shows the five base-mode pills, `04` lists
Custom mode). `04` no longer uses the shared fixed crop: the picker hangs off the **vertically centred**
composer, so a fixed box left about 320 of its 800 rows as plain background; the box is now aimed at the
popup's own centre (still 800x800, so the README table stays flush).
- 15 suites / **861 checks** (was 850). Three more mutations verified to go red. See `docs/MEASUREMENTS.md`
section 34.
## [1.11.0]
### A fifth base mode — and the union that would have shipped as a broken preset
- **「自定义模式」 is now a base mode, not only a preset name.** The picker offers five: the four dsh ships
(`standard` / `ptc` / `minimal` / `cordis`) plus `all`, whose row set is the **union** of those four.
Until now the row list was whatever the single shipped mode you picked happened to contain, and a switch
can only rewrite a row the base text already has — so on standard, `tool-presentation` (PTC only), the
whole `persistent-shell` group (Minimal only) and `tool-cordis` (Cordis only) were unreachable. Not hidden
by the UI: absent from the composer. Measured on `0.2.0-rc.2`: 32 / 33 / 33 / 7 rows, union **40**. A row
declared by several modes keeps the text and shipped default of whichever mode comes first
(`standard → ptc → minimal → cordis`), and a group travels whole, `isolate` realms, comments and `!!js`
conditions included — inventing a realm for a carried service row is what would make the preset unusable.
- **Two groups in that union cannot be on at the same time, and the first version did not know.** Minimal's
`persistent-shell` set and standard's `tool-bash` / `tool-pwsh` are two implementations of the same thing —
both register a tool called `bash`. Enabling both makes `dsh-agent-presets` mark the whole preset
**broken**, and a broken preset is silently dropped from every picker *while the settings page stays
green*. Measured by writing the composition to disk, really starting the harness and reading the
instance's own `/api/custom-mode`. `EXCLUSIVE_ROW_SETS` now records the conflict, the union ships with the
non-preferred side off, and `saveState` moves the switches for you — `code: savedWithExclusiveRows` names
what it turned off, and the page re-reads what the server actually wrote. A conflicting composition
written **outside** the page (hand edit, another tool, a direct API call) is named on the page through the
existing `exclusiveRowsActive` warning.
- **The shipped default could not be "whatever the source mode shipped".** `persistent-shell` is enabled in
minimal by definition — that is what minimal is — so copying each row's shipped state produced an invalid
default: saving `mode: all` with `overrides: {}` answered `saved`, and the roster was already `broken`.
The union now disables the exclusive side at synthesis time.
- **Verified end to end on both supported lines** (`0.2.0-rc.2` and `0.1.7-rc.2`): same script, two
harnesses, four steps each — switch to `all`, flip the exclusive side, flip back, read the page state;
`broken` is absent at every step, `warnings` empty, stderr 0 bytes.
`tools/boot-check.mjs` passes A/B/C/D on both.
- 15 suites / **850 checks** (was 805). Eleven mutations were run against copies of the tree and each went
red on the assertion meant to catch it — including two that caught defects in the new code and in the new
tests themselves (a `ReferenceError` where a typed failure was intended, and a vacuous assertion that
stayed green when its configuration was emptied). See `docs/MEASUREMENTS.md` §33.
## [1.10.0]
### The repository root is the package now — and dsh 0.2.0-rc.2 is the line CI pins
Three things, one release.
- **dsh 0.2.0-rc.2 supported, verified end to end.** npm's `latest` `and` `next` both moved to it on
2026-09-29, so "latest stable" and "latest preview" are one release now; `0.1.7-rc.2` becomes the previous
stable and stays as the second CI leg, because most installs in the field are still on it. The declared
range already covered `0.2.x`, so this was verification rather than code: on a clean throwaway
`DSH_HOME`, `dsh plugin --profile web add dsh-custom-mode` resolves the package in 640 ms, the composition
tree carries the row, boot seeds all five preset files, the declarative registry syncs the assistant, and
the `agentPresets` capability set is unchanged (`list, register, inventory, select, document`). CI's main
axis moves to `0.2.0-rc.2` (ubuntu node 20/24 + Windows), the stable leg stays `0.1.7-rc.2`, and the
render gate runs on the newest line.
- **One manifest, at the repository root.** Until now the root `package.json` was a `private: true` wrapper
(what a GitHub-URL install read) while `editor/package.json` was the published package. That split had two
visible costs. Third-party catalogues rendered the plugin as `dsh-custom-mode#editor` — a monorepo
subpackage name for a repository that is not a monorepo. And detectors that read the repository's *root*
manifest, dshfind being one, saw `private: true` and reported **"the author has not published to npm"**
for a package that has been on npm since 2026-09-17. The package now lives at the repository root,
`editor/` is gone, and so is the duplicated `editor/preset/` copy: the packaged preset **is** `preset/`.
`npm publish`, `dsh plugin add .`, the GitHub-URL install and the package-name install all read one file.
`test/manifests.test.mjs` is the guard — root manifest present and not `private`, no second manifest,
and the `files` whitelist still covers the import graph.
- **One copy of every screenshot.** `screenshots.json` used to list a duplicated
`editor/assets/storefront-0{1..4}-*.png` set, and its first file was mislabelled: the entry named
"assistant manager" actually held the mode-switch capture, while the real assistant-manager image was not
in the set at all. `screenshots.json` now lists the five canonical `docs/images/*.png` files the README
shows, so a catalogue card and the README cannot disagree, and four duplicate images left the repository.
**The installed package is unchanged.** Instead of assuming that, this release measures it: `npm pack
--dry-run` on the flattened tree lists the same runtime files 1.9.19 shipped, plus the `README*` files npm
auto-includes (the repository README is the npm landing page now). This release moves files in git, not in
the package a user installs.
## [1.9.19]
### dsh 0.2.0-rc.1: supported, verified on the rendered page, and it enforces compatibility now
`@deepseek-ai/dsh@0.2.0-rc.1` shipped under npm's `next` tag (and tag `dsh-v0.2.0-rc.1` in the repository).
Adapting to it was mostly *not* code:
- **0.2.0 refuses to install an incompatible plugin** — the compatibility check that used to warn is now a
gate: `dsh: installation rejected: Plugin dsh-custom-mode@1.9.18 is incompatible with dsh 0.2.0-rc.1:`
`peerDependencies {"@deepseek-ai/dsh":">=0.1.5-rc.2 <0.2.0-0 || ..."} ... Exact-version exemption: not
active.` So the declared range *is* the feature switch, and it now carries the 0.2 line
(`|| >=0.2.0-0 <0.3.0-0`), after the verification below.
- **Everything else already held.** On 0.2.0: the plugin installs and boots with no errors; the
`agentPresets` capability set is the same surface as 0.1.7 (`list, register, inventory, select,
document`; no `remove`/`copy`/`read`); the unit suite is **801/801** (the seeded composition derives from
the host's declaration and every row resolves); the mode appears in the rendered picker; and the whole
**65-check render gate passes** — including the official-metrics assertions (14px/22px weight-500 section
titles, 12px/18px tertiary intros, flat rows with 1px dividers, the shell's `[role=switch]`), so 0.2.0's
Web UI did not move the ground the page stands on.
- **Desktop is covered by the same work**: the app is version-locked to dsh and `apps/desktop` at the
0.2.0-rc.1 tag is `0.2.0-rc.1`, still owning `$DSH_HOME/profiles/desktop`; install from the app's Plugins
page, exactly as in 1.9.18.
- **CI follows the policy** (two latests): the main axis is now `0.2.0-rc.1` (ubuntu node 20/24 + Windows),
the stable leg is `0.1.7-rc.2` (npm `latest`), the old `0.1.5-rc.3` leg is retired, and the render gate
runs on the preview line — where the shell's own UI changes land first.
One operational note worth keeping: **a release is not finished when `npm view` shows the version.** For about
20 minutes after `@deepseek-ai/dsh@0.2.0-rc.1` appeared, installing it failed with
`ETARGET ... No matching version found for @deepseek-ai/dsh-client-ui-settings-account@0.2.0-rc.1` — one of its
79 direct dependencies (and at least one transitive one) was still uploading. The same install succeeded on the
fourth attempt. If a brand-new dsh line fails to install, retry before suspecting the plugin.
## [1.9.18]
### The desktop app is supported now — and 0.2.0 is not out, so nothing was adapted "early"
Asked whether dsh 0.2.0 (and with it the desktop app) had already landed unannounced, since plugins were
said to be adaptable ahead of time. Measured (2026-09-27): **no.** `npm view @deepseek-ai/dsh dist-tags` is
still `latest: 0.1.7-rc.2`, the newest of its 27 versions is `0.1.7-rc.2`, the repository's `master` says
`0.1.7-rc.2`, and the newest tag is `dsh-v0.1.7-rc.2`. There is nothing to adapt to yet.
What *is* already there is the **desktop application** (`apps/desktop`, `apps/desktop-host`), at the same
0.1.7-rc.2, as private packages — so it ships as signed Electron installers, version-locked to dsh, and it is
the complete dsh Web application in an Electron shell. That is a target this plugin can support today, and
1.9.18 makes the support explicit:
- **Install from inside the app**: sidebar → **Plugins** → add plugin → `dsh-custom-mode`. The app runs the
shared plugin manager with its own bundled pnpm.
- **The CLI is refused, by dsh itself** — not just by us: `dsh plugin --profile desktop add ./editor`
answers `error: profile "desktop" is managed exclusively by the Electron application` (measured).
`install.sh` and `uninstall.sh` now refuse `--profile desktop` too, with the same advice, so the script and
the runtime point the same way instead of one working around the other. `DSH_ALLOW_DESKTOP_PROFILE=1` is
there only for local simulations of the profile shape.
- **The preset half needs nothing**: `.agent-presets/` is product data shared by the desktop app and the CLI.
If a third-party bundle ever blocks startup, the app's native recovery dialog offers Disable third-party
plugins.
- **Compatibility is declared, not assumed**: dsh checks every plugin's `peerDependencies["@deepseek-ai/dsh"]`
against the running runtime (prereleases included) and warns when the range does not cover it. Our range
deliberately stops below `0.2.0-0`, so a 0.2.x runtime gets that warning until the line is adapted and
re-tested. The four places a new line changes — CI matrix, the release gate's installs, `engines.dsh` and
the peer range — are now written down in `AGENTS.md` (10 and 18) so that adaptation is mechanical.
Evidence (the raw commands and output) is in `docs/MEASUREMENTS.md` §30; `test/manifests.test.mjs` asserts
that both scripts keep refusing the desktop profile.
## [1.9.17]
### The storefront images were stale (and two of them were the same picture)
Documentation and packaging only — no runtime change. What was actually wrong:
- The four images the plugin directory shows (`editor/assets/storefront-0*.png`) were still the 1440x900
set from before the UI refactor, and two of them were **byte-identical** (the scroll helper in the
capture script silently did nothing, so "plugin switches" and "system prompt" came out as the same view).
They are now **800x800** — one canvas, same as the five README images — English, and produced from the
same run as the README set by `tools/screenshots/shoot-fresh.mjs`.
- That script is new and documented: it pins a throwaway `DSH_HOME` with no sessions, a fixed viewport
(1440x900, device scale 1) and a fixed crop, and it renames the seeded assistant and creates a second one
so the images match the README. Two measured traps are written into it: the settings dialog must be opened
with a **real pointer** (this shell ignores synthesised `click()` for some controls), and the first-run
**"Add an API key" modal is also a `[role=dialog]`** — treating "a dialog exists" as "settings is open"
makes every page shot the same picture.
- Both package manifests now carry **Chinese keywords** as well (npm search indexes them), and the
repository description is bilingual, because GitHub topics cannot contain Chinese.
## [1.9.16]
### The settings page looks like the rest of dsh now — it never used the shell's components
Three versions of "this page feels cheap" had one root cause, and it was one line:
- **The atom-library probe was wrong, so every control fell back to hand-painted stand-ins.** The check was
`typeof atoms.Button === "function"`, but the shell's components are `forwardRef(...)` / `memo(...)`
**objects**. So the probe failed and the page rendered its own plain controls: `input[type=checkbox]`
instead of the shell's switch, hand-styled buttons, a native `window.confirm` instead of the shell's risk
dialog. Measured on `0.1.7-rc.2`: `require("@deepseek-ai/dsh-client-ui-primitives")` returns **279
exports** (`Button`, `RiskConfirmation`, `Modal`, `Menu`, `SegmentedControl`, the icon set) — the module
was in the platform seed table the whole time. "Usable" now means "React can render it" (`$$typeof`).
- **The icons never rendered either**: the page asked for `IconPlusOutline16`, the shell exports
`IconPlusOutlineRegular`. A small alias layer maps the short names.
- **The page's own CSS used a different scale from the official settings pages.** Measured (same line, the
General page): section titles are **14px/22px weight 500**, intros are **12px/18px** in the tertiary label
colour, and a settings row is `padding: 16px 0` with **one 1px divider** — no card, no radius, no fill.
Ours had 15px/700 titles, a click-to-expand "dot" next to every heading, and each plugin row wrapped in a
rounded, bordered, tinted card. All of that is gone and `tools/browser-verify.mjs` now asserts the official
numbers (including "the switch is the shell's `[role=switch]`, and there is no hand-painted checkbox"),
so the drift cannot come back quietly.
- **Dropdowns are the shell's `Menu`** (a ghost button + chevron + floating panel), used for assistant
switching and for the prompt history — the history selector used to be a browser `