DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

marselarts /

marselarts/dsh-theme-studio

Verified

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.

★ 0 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@7234b617

dsh-theme-studio

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:

  • Semantic tokens — all 107 --dsw-alias-* variables (surfaces, text, borders, accent, states, buttons, code, diffs, menus, tooltips, scrollbars), the exact set the design system's token sheets define.
  • Component fills — the 11 --dsw-specific-* surfaces a component declares for itself (sidebar column and its rows, the message bubble, the composer, menu material, selectors).
  • Static palette — the 77 --dsw-static-* steps those aliases resolve to, for retinting the whole product from one step (e.g. --dsw-static-deepseek-500).
  • Typography — --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().
  • Custom CSS — a plugin-owned stylesheet for anything the tokens do not reach.

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)

Install

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

Where the settings live

<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.

Layout

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.

Do the fonts actually change?

Yes, and this is the consumer chain read out of the installed build rather than assumed:

  • The shell stylesheet (@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.

Editing beside the interface

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.

Design notes

  • No Harness Client package is imported as a module. Controls are written here and styled with the host's own recipes (switch, input, segmented control, settings row) using only --dsw-alias-* tokens, so a renamed token degrades appearance instead of breaking rendering.
  • Every string goes through the client locale service; English and Russian ship in the bundle.
  • All registrations are ctx.effect-owned: unloading or reloading the plugin removes the stylesheets, the override layer, the slot registration and the store subscription.
  • The catalog is partitioned at load: essentials is explicit, every other token lands in the first matching group, so a new token in a future harness version still appears somewhere.
  • The section is wrapped in an error boundary, so a future bug reports itself inline instead of blanking the settings entry. React's server renderers never consult boundaries, so this is the one behavior the offline checks cannot cover.
  • Font slots use a <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.

Verification status

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.

—/ 5

No ratings yet

Verified DSH bundle

Commit 7234b6177748

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout