dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
Theme Studio for the DeepSeek Harness Web UI: edit the design-system color tokens and fonts per light/dark scheme, in Settings or in a movable panel over the app.
Customize the Harness Web UI's colors and fonts from a Settings page, per light and dark scheme, with live preview, presets and a custom-CSS escape hatch.
The plugin does not invent a styling system. It edits the one the client already paints with:
--dsw-alias-* variables (surfaces, text, borders, accent,
states, buttons, code, diffs, menus, tooltips, scrollbars), the exact set the design system's
token sheets define.--dsw-specific-* surfaces a component declares for itself
(sidebar column and its rows, the message bubble, the composer, menu material, selectors).--dsw-static-* steps those aliases resolve to, for retinting the
whole product from one step (e.g. --dsw-static-deepseek-500).--dsw-font-family (interface), --ds-font-family-code (code, diffs,
editors), --dsw-font-family-brand (brand wordmark), plus the harness's own conversation
text size, written through theme.setFontSize().Overrides are handed to ctx.theme.overrideTokens(id, tokens), the theme service's public
extension point. The presenter folds the layer into the active snapshot and paints it on body,
so light/dark switching, first paint and teardown all stay the harness's business.
| Tab | What it edits |
|---|---|
| Colors Simple | The shortcut, and the tab the page opens on: a section list down the left (Text, App background, Conversation, Sidebar, Panels, Sidebar… menus, Accent, States, Code and diffs, Controls) and, for the chosen section, only its own relative colors — background, text, border, hover and so on. Each section carries a badge with how many of its colors you changed; no token is shared between two sections |
| Colors | The complete grid: searchable, grouped, one control per scheme (light / dark), a native swatch plus a free-form value (var(...), color-mix(...), transparent), per-token reset, "only changed" filter. Each row is just its label — the design system's description and the variable name are the row's tooltip |
| Fonts | The three font-family variables with a pick list per slot, a live sample and the conversation text size; families this machine does not have are marked |
| Presets | Five coherent starting points (Warm paper, Nord, Solarized, Rosé Pine, High contrast), JSON export/import, reset-all |
| Advanced | Free-form stylesheet with one-click snippets (interface scale, heading colors, softer corners, opaque menus, warm reading surface) |
The package declares dsh.bundle.patch and dsh.client, so the official installer mounts it and
adds it to the profile's bundle stack by itself:
dsh plugin --profile desktop add <absolute path to this directory>
That records a link: dependency, so the profile points at this directory instead of copying it:
editing src/ and rerunning npm run build is enough to update the plugin, and moving or deleting
this directory breaks the install. Use dsh plugin --profile desktop remove dsh-theme-studio to
uninstall, or reinstall from a git URL to freeze it.
Reload the Web GUI afterwards. The page appears in Settings → Theme Studio. Disable it without
uninstalling by overriding the row in the profile's cordis.patch.yml:
- id: theme-studio
disabled: true
<DSH_HOME>/dsh-theme-studio/theme.json — one document holding the master switch, the override
map and the custom CSS. It is written atomically and in order, so a color-picker drag cannot tear
the file. The client also mirrors the document into localStorage for an instant first paint and
for clients that cannot reach the host (a non-loopback page); the host copy always wins on
reconcile.
Host storage is served by a single exact Fetch route, POST /api/dsh-theme-studio, on the shared
/api channel. That channel's gateway applies the Host/Origin fence and browser authentication
before the request reaches the handler, and the handler re-validates every field before writing:
token names must be custom properties, values are length-capped, and unusable entries are dropped
rather than persisted.
lib/index.js host half: the storage route (hand-written ESM)
lib/client.js generated lazy-CJS bundle the browser loads
src/client.js engine + editor UI (edit this)
src/catalog.js essentials, labels, groups, presets, snippets (edit this)
data/token-defaults.json token directory extracted from the installed design system
scripts/build-client.mjs inlines the catalog + defaults into lib/client.js, validates them
scripts/check-client.mjs offline checks: activation, override layer, every tab, the host route
src/ is the source of truth for the client half; lib/client.js is generated and committed so
the plugin runs without a build step.
npm run build # regenerate lib/client.js (validates tokens, groups, presets and string keys)
npm run check # build --check + offline activation/render/route checks
The render checks need ReactDOM, which the shipped web client bundles rather than installing:
npm --prefix .devdeps install react@18.3.1 react-dom@18.3.1
Without it, npm run check skips the render assertions and reports the skip.
Yes, and this is the consumer chain read out of the installed build rather than assumed:
@deepseek-ai/dsh-web-frontend/dist/assets/index-*.css) opens with
body{font-family:var( --dsw-font-family, -apple-system, …) — the whole interface font comes
from that one variable, and everything without a font-family of its own inherits it.--dsw-font-family also feeds the conversation text ladder in ui-theme
(--dsw-font-markdown-base-font-family, --dsw-font-markdown-h1-font-family …, i.e. h1–h6 and
base text), the composer input, sidebar document previews and the code-block banner.--ds-font-family-code has nine consumers in the shell stylesheet alone, plus code blocks,
inline code, the JSON tree, web blocks, diffs and the editor.--dsw-font-family-brand backs the welcome screen and the brand wordmark.The override reaches all of them because theme.overrideTokens makes the presenter write
body.style.setProperty('--dsw-font-family', …): an inline declaration on body beats the
stylesheet rule and every descendant inherits it, while removing the override retracts it.
Two honest caveats: a family that is not installed on this machine falls through to the next entry
of the stack, and a few components hardcode a family in front of the variable (ui-conversation uses
Inter, var(--dsw-font-family) for counts and previews), so those prefer Inter when it exists.
Because of the first caveat, the editor probes each named pick with canvas metrics and marks the ones this machine does not have ("Roboto · not installed"), then warns under the field when the current value starts with a missing family. A fresh Windows install carries only Segoe UI, Consolas and Microsoft YaHei from the curated lists; picking Roboto there changes nothing visible, which is exactly what the marker now says up front.
The Settings dialog covers the window, so a colour changed there cannot be seen where it lands.
Open beside the interface in the editor toolbar closes the dialog and shows the same editor as a
movable panel over a corner of the app, so the conversation stays visible while a swatch is dragged.
The panel is the same component and the same store as the settings page, so the two never disagree:
close it with ✕, collapse it to its header with ▾, drag it by the header, and it returns where you
left it after a reload — position, collapsed state and open flag live in localStorage per browser,
never in the theme document.
It registers into shell.overlay, the layout's own list slot for floating surfaces: an absolutely
positioned layer above the frame whose occupants own their geometry and re-enable pointer events.
While the panel is closed it renders nothing at all, so that layer stays clean.
The right sidebar's tab API (ctx.sidebarRightTabs.register plus a keyed sidebar.right.pane.tab
view seat) would make this a docked tab instead of a floating panel. That path needs a live client
to verify: it involves a keyed session-scoped slot and a guide entry whose exact shape is only
documented by the shipped tab packages.
--dsw-alias-* tokens, so a renamed token degrades appearance instead of breaking rendering.ctx.effect-owned: unloading or reloading the plugin removes the
stylesheets, the override layer, the slot registration and the store subscription.essentials is explicit, every other token lands in the
first matching group, so a new token in a future harness version still appears somewhere.<select> plus a free-text field, never a <datalist>: the browser filters
datalist options by the field's current text, so after the first pick the list collapses to that
single entry and the control becomes a dead end.npm run check exercises the real artifacts offline: the bundle loads through the loader's lazy
factory and activates against a stand-in client context, the override layer is asserted per
scheme, every tab is rendered through React in three store states, the catalog is proven to
partition the token directory exactly once per token, and the host route is driven end to end
(envelope validation, save/load/reset, sanitizing, atomic write under a temporary DSH_HOME).
What it cannot establish: how the page looks with your theme applied. That needs the running GUI.