dsh-settings-order
DSH Web 设置左列自由排序插件:拖动 / Alt+↑↓ / ↑↓ 按钮,宿主持久,跨浏览器生效(Free ordering for the DeepSeek Harness Web Settings navigation)
- Stars
- 1
- Language
- JavaScript
- Created
- Sep 23, 2026
- Updated
- Oct 3, 2026
Introduction
dsh-settings-order
Free ordering for the Settings navigation in the DeepSeek Harness Web GUI.
Drag a page, press Alt+↑/Alt+↓, or use the footer's ↑/↓ controls — the
order is stored on the host, so it survives restarts and follows you to every
browser the transport reaches.
设置
↑ ↓ 恢复默认
归档会话 ← dragged up from the bottom
通用设置
模型
插件
插件市场
…

Why this exists
The Settings dialog's left column is rendered from the shell's
settings.section list slot, and SlotCore keeps list entries sorted by
priority then by the registering plugin's own order. That is a build-time
decision by each plugin — the shell exposes no drag, no sort control, no
preference, and there is no supported way to change it from user space.
This plugin leaves the slot registry alone (no re-registration, no order
override, no order patching of other packages). It reorders the rows that are
already rendered, and remembers the result.
What you get
| Drag | grab any row and drop it where you want (an insertion mark shows the landing slot) |
| Keyboard | focus a row and press Alt+↑ / Alt+↓ |
| Footer controls | ↑ / ↓ move the page you are currently viewing one place — the touch-friendly path, since a phone browser has no mouse drag and no Alt key |
| Reset | once your order differs from the built-in one, a 恢复默认 / Reset action returns it |
| Host-persisted | DSH 0.1.7-rc.1 (and compatible stable builds) stores the list in the active profile's cordis.patch.yml, under the settings-order entry's config.order; every browser that reaches that host shares it |
| Browser-local fallback | a browser the transport cannot reach (some remote setups) keeps its own localStorage copy and says so in the footer |
| Fail-soft | if a future DSH build changes the markup, the plugin changes nothing and shows 无法识别设置项 in the footer instead of failing silently |
| Non-destructive | third-party pages (archived-sessions, market, cost-meter, …) reorder exactly like built-ins; a page added later keeps the position the shell gives it; ids that no longer exist are ignored |
Nothing else is touched: no session or workspace ordering, no other plugin's DOM, no slot registrations, no model-visible input, no network.
Install
Requirements: a DSH install with the Web GUI and a profile to install into
(web in the commands below). The schema-derived volatile Config /
SettingsForms API is checked against 0.1.7-rc.1 and against the harness the
DSH Desktop 0.2.0-rc.2 build bundles; hosts that still expose the legacy
settings.register() API are also supported. Nothing is built at install time —
the client bundle ships ready to serve, so a plain dsh plugin add is enough.
# from npm (a released version; the registry is the quickest path)
dsh plugin --profile web add dsh-settings-order
# pin a released version
dsh plugin --profile web add dsh-settings-order@0.2.6
# from GitHub (tracks `main`)
dsh plugin --profile web add github:jackovibe/dsh-settings-order
# pin a GitHub release instead
dsh plugin --profile web add github:jackovibe/dsh-settings-order#v0.2.6
# or from a local checkout / tarball
npm pack
dsh plugin --profile web add .\dsh-settings-order-0.2.6.tgz
dsh plugin add records the dependency and appends it to
dsh.profile.bundles, which is what mounts it — the package carries its own
bundle patch, so never add a second insert for it in the profile's
cordis.patch.yml (a duplicate loader id would break startup).
Then restart dsh web: the host half exposes its volatile Config through the
schema-derived settings service, and the profile's client bundles are served
from a boot-time snapshot, so a page refresh alone is not enough. Open 设置 / Settings — the navigation column now
gains a footer with ↑ / ↓, the hint line and (once you reorder) 恢复默认.
DSH Desktop
The Desktop build carries its own harness (0.2.0-rc.2 bundles the whole
@deepseek-ai/dsh-* set at that version) and its own desktop profile, so
install into that profile with the CLI the app ships — the npm-installed
dsh boots a different harness and cannot even dump the Desktop profile
(error: profile "desktop" is managed exclusively by the Electron application):
& "D:\DSH Desktop\resources\runtime\cli\bin\dsh.cmd" `
plugin --profile desktop add dsh-settings-order@0.2.6
Adjust the path to wherever the Desktop build is installed. The command installs
under the app's own pnpm, which matters because the Desktop profile's lockfile
is the app's to write. Then restart the app: the host half registers at boot
and the client bundles are served from a boot-time snapshot, so the footer
appears only after the restart. An ordering change then lands in
~/.dsh/profiles/desktop/cordis.patch.yml under settings-order.config.order,
exactly as in the Web profile.
Update
dsh plugin --profile web up dsh-settings-order # re-resolve the dependency
Restart dsh web when the new release changed the client half
(lib/client.js); a docs-only release does not need it.
Usage
- Open 设置 / Settings.
- Reorder however you like: drag a row, or select a page and press
↑/↓in the footer (orAlt+↑/Alt+↓on a focused row). - The order is saved immediately; the footer's
恢复默认/Resetappears once your order differs from the built-in one. The one-line hint retires after your first reorder.
Storage
For DSH 0.1.7-rc.1's schema-derived settings API, the order is saved in the
active profile's cordis.patch.yml as the loader entry's config:
- id: settings-order
config:
order:
- general
- archived-sessions
- plugins
The exact file is the active profile patch (available as settings.documentPath),
not the removed ~/.dsh/settings.yaml. The browser half reaches that document
through the settings domain's configForms service
(ctx.configForms.get('settings-order')), which DSH 0.1.7 introduced; on older
hosts it falls back to the legacy settingsScope namespace settings-order,
whose order field is stored by that host's settings provider.
Browser-local fallback (localStorage): dsh.settings-order.nav (the ordered
ids), dsh.settings-order.hint-seen (the hint flag).
How it works
- Rows are found by CSS-module suffix —
[class*="_navList"],[class*="_navCell"],[class*="_navLabel"]. The hashed prefix (VOzbGW_…) changes between builds; the suffixes do not. - Row identity is the React key. The shell keys each row button with its
settings.sectionentry id, so the fiber'skeyis the id the host stores. An unreadable fiber falls back to the row's label text. - Reorder = move the existing nodes inside their own parent
(
navList.appendChildin the target order). Nothing is re-created, so React keeps ownership; aMutationObserverre-applies the saved order whenever the shell re-renders the list. - The built-in order (for
Reset) is read live fromctx.slots.entries('settings.section'), which SlotCore keeps sorted byprioritythenorder. - Writes are optimistic: the local order is applied immediately and kept until the host echoes it back, so a slow round-trip never flickers.
npm test pins all of the above — selectors, the row key, the slot ordering —
against the installed DSH, so an upgrade that breaks them fails the test suite
rather than the user's Settings dialog.
Verification
npm run check # the served bundle is the built source
npm test # static invariants + the installed host contract
npm run e2e:dom # inject the browser half into a live GUI and exercise it
npm run e2e # against the installed plugin: host persistence, reset, drag, reload
e2e/preinstall-dom-check.mjs works before the plugin is installed: it injects
the built browser half into the running GUI, runs apply() twice — once with no
settings scope (the browser-local/remote path) and once against a stubbed host
scope — and asserts row identities, Alt+↓, a native HTML5 drag, persistence,
the footer's ↑/↓ controls and reset, and that a full page reload re-applies
the stored order. e2e/settings-order-e2e.mjs runs against the installed
plugin, snapshots the host order first and restores it afterwards.
Both harnesses take the GUI from DSH_E2E_URL, or else from the newest token URL
in ~/.dsh/dsh-web.log; the settings/workspace documents come from
DSH_E2E_HOME, or else ~/.dsh. They need playwright-core and a reachable
dsh web.
Verifying against an isolated home
Prefer a scratch instance with its own profile patch: settings edits are written
to the active profile's cordis.patch.yml. Isolating the home/profile also keeps
your real configuration out of any test. This repository's e2e harnesses target
an installed plugin and a live GUI; do not run them against a user's profile
without an isolated environment.
$home2 = Join-Path (Get-Location) '.scratch-home' # inside the repo, git-ignored
New-Item -ItemType Directory $home2 -Force | Out-Null
New-Item -ItemType Junction "$home2\profiles" "$env:USERPROFILE\.dsh\profiles" # reuse the installed profile
$env:DSH_HOME = $home2
dsh web --no-open --port 3099 # prints its own token URL
# then, in another shell:
$env:DSH_E2E_URL = 'http://127.0.0.1:3099/?token=…'
$env:DSH_E2E_HOME = $home2
node e2e/settings-order-e2e.mjs 3099
A fresh home shows the first-run overlays (beta notice, sidebar tip); the
harnesses dismiss them by pressing only the "keep things as they are" choices.
shots/ is git-ignored — those captures are full-window and contain real
session titles; only the dialog-side crop in docs/settings-order.png ships.
Remove
dsh plugin --profile web remove dsh-settings-order
# and drop "dsh-settings-order" from dsh.profile.bundles, then restart dsh web
scripts/rollback.ps1 does both steps and restarts the server through the
watchdog.
Compatibility
Verified on both harnesses the plugin targets:
| Harness | How it was exercised | Result |
|---|---|---|
| DSH Desktop 0.2.0-rc.2 | the plugin's own profile, GUI | footer renders; a reorder is persisted into ~/.dsh/profiles/desktop/cordis.patch.yml under settings-order.config.order |
| DSH Web 0.2.0-rc.2 | the web profile upgraded from 0.1.7-rc.1 | plugin present in the boot payload, client bundle served, no incompatible / failed to apply / register is not a function at startup |
| DSH 0.1.7-rc.1 | the API read from the installed source and types | SettingsForms + volatile Config on the host side, configForms.get(entryId) with set/unset and loading | ready | unavailable on the browser side |
The Settings-shell markup and slot contract are also covered by the
installed-host contract test. The browser-DOM layer's own
npm run e2e:dom harness is not run in this repository's CI (it needs
playwright-core and a matching chromium). npm test skips the host contract
block when no DSH install is found; point DSH_CORE_ROOT at the
@deepseek-ai scope directory to check a specific build.
On peer ranges: DSH's install audit (
dsh-app-boot'sevaluatePluginCompatibility) inspects only@deepseek-ai/dsh*peers and compares withincludePrerelease: true. Use that same rule when judging whether a version installs, rather than inferring it from default semver.
Development
node scripts/build-client.mjs # src/client-src.js → lib/client.js
node scripts/build-client.mjs --check # fail when the bundle is stale
node --test # contract + invariants
Layout: lib/index.js (host half: the settings namespace), src/client-src.js
(browser half, plain script), lib/client.js (served bundle), cordis.patch.yml
(the bundle patch), e2e/, test/, scripts/, docs/.
Privacy
shots/ is git-ignored on purpose: those screenshots are full-window captures
of a live GUI and contain real session titles. The screenshot in this README is
a dialog-only crop.