Bundle
dsh-theme-colorizer
DSH plugin: adds customizable color themes to the DeepSeek Web UI, selectable in the Settings page
- Source
- RealHacker
- stars
- 2 stars
- License
- MIT
- Updated
- Updated 4 days ago
Readme
# dsh-theme-colorizer

A DeepSeek Harness (DSH) plugin that adds customizable color themes to the DeepSeek Web UI. The user selects a color theme on the Settings page, and the chosen theme's `--dsw-alias-*` CSS token overrides are applied on top of the active light/dark base palette.
## Features
- **5 color themes**: Ocean Blue, Forest Green, Sunset Orange, Royal Purple, Monochrome
- **Settings page integration**: a "Color Theme" row appears in the General section of the Settings panel
- **Persisted preference**: the user's choice is kept in browser `localStorage` and survives page refreshes
- **Composes with the built-in Appearance row**: the color theme is independent of the light/dark/system preference — both layers compose through the theme service's override stacking
## How it works
### The DSH plugin loading model
A DSH plugin is a Cordis plugin module. It exports an `apply(ctx)` function that the Cordis loader calls with a **context** (`ctx`). Every registration the plugin makes — theme overrides, slot registrations — is an effect on that context, so unloading the plugin automatically tears down its contributions.
This plugin has **two halves** because it contributes to both the Host (Node.js server) and the Client (browser) sides of the DSH web application:
### Host side (`src/index.ts`)
The Host half is a loadable but inert entry: the row in the composition must resolve a package main, and the browser plugin graph is served from the package's `./client` export. There is **no host-side work** because persistence is client-side: the Host API gateway only exposes settings namespaces on a curated allowlist (`WEB_SETTINGS_NAMESPACES` in the apiproxy), and plugin-owned exposure is deferred harness work — a write to an unlisted namespace is refused with `settings-not-exposed`. Keeping the preference in `localStorage` makes the plugin fully self-contained (no harness source changes, no host rebuild).
**When loaded**: at server startup, when the Loader reads the composition and mounts this plugin entry. The host half's `apply` runs and returns immediately.
### Client side (`src/client/index.ts`)
The Client half runs in the browser. It:
1. **Reads the stored color theme** — synchronously from `localStorage` under the key `dsh-theme-colorizer:theme` (falling back to the default), so there is no first-paint race after a refresh.
2. **Applies token overrides** — calls `ctx.theme.overrideTokens('dsh-theme-colorizer', tokens)` to overlay the selected theme's `--dsw-alias-*` CSS custom properties on top of the active base palette. The theme service composes override layers in stacking order; later layers win per-token. The plugin's layer is identified by the source string `'dsh-theme-colorizer'`, so re-calling with the same source replaces the layer atomically.
3. **Persists the selection** — writes the chosen id back to `localStorage` on every change; a `storage` event listener keeps other open tabs in sync.
4. **Registers a settings row** — injects into the `settings.general.item` slot and registers a `ThemeColorRow` React component that renders the five color-theme swatches. The component receives the current selection and a `setColorTheme` callback through its inject face.
**When loaded**: when the browser's module loader constructs the `window.__DSH_BOOT__` plugin graph. The `dsh.client` manifest in `package.json` declares this is a web plugin with `immediately: false`, so it loads during the normal plugin graph initialization (not in the stage-one prefetch tier).
### Settings row rendering
The `ThemeColorRow` component renders inside the General section of the Settings panel. It uses the DSH slot system:
- The `ui-settings` package declares the `settings.general.item` slot type
- The `ui-settings-general` package renders the General section and its item slots
- This plugin registers into that slot with `id: 'theme-colorizer'` and `order: 20` (placing it after the built-in Appearance row at order 10)
### Theme token override flow
```
User picks "Forest Green" in Settings
→ ThemeColorRow calls setColorTheme('forest-green')
→ Client plugin persists it to localStorage (dsh-theme-colorizer:theme)
→ Client plugin calls ctx.theme.overrideTokens('dsh-theme-colorizer', forestTokens)
→ ThemeRuntime publishes theme/change event
→ ui-layout's ThemePresenter reads the new snapshot
→ CSS variables on <body> update
→ All components re-render with new colors
→ On the next page load, apply() reads localStorage and re-applies the theme
```
## Installation
### Quick Installation
Execute this command:
```
dsh plugin --profile web add dist\dsh-theme-colorizer-1.0.1.tgz
```
### Prerequisites
- A working DeepSeek Harness installation (the `deepseek-harness` repository checkout)
- The DSH web app is built and running (`pnpm run dev:web` or `dsh web`)
### Method A (recommended): install as a bundle with `dsh plugin add`
The plugin is packaged as an installable **bundle**. Two manifest entries make that work:
- `package.json` declares `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }` — the bundle layer
- `cordis.patch.yml` at the package root inserts the plugin row into the composition
`dsh plugin` installs bundles into a profile with a single command, exactly like the built-in `dshmarket`/`dsh-better-sidebar` plugins in the `web` profile.
**1. Build the plugin** (the build config lives in the harness checkout):
```powershell
cd E:\Deepseek\deepseek-harness
pnpm run build:lib:client
```
This emits `packages\client\dsh-theme-colorizer\lib\` (node half `index.js`/`invariant.js` + browser half `client.js`).
**2. Sync the built output into the workspace copy** (the installable artifact):
```powershell
Copy-Item -Recurse -Force packages\client\dsh-theme-colorizer\lib E:\Deepseek\Workspace\plugins\dsh-theme-colorizer\
```
**3. Pack a tarball and install it into the `web` profile:**
```powershell
cd E:\Deepseek\Workspace\plugins\dsh-theme-colorizer
pnpm pack --pack-destination E:\Deepseek\Workspace\plugins\dist
dsh plugin --profile web add E:\Deepseek\Workspace\plugins\dist\dsh-theme-colorizer-1.0.0.tgz
```
That initializes the profile if needed, installs the package, and appends `dsh-theme-colorizer` to the profile's `dsh.profile.bundles`. Verify without booting:
```powershell
dsh --profile web --dump-config # shows a "# == dsh-theme-colorizer" layer with the theme-colorizer row
```
**4. Restart `dsh web`.**
Why a tarball and not a plain `add <directory>` or `file:` spec?
- A plain directory `add` uses pnpm's `link:` — a junction to your source dir. Node resolves the module's **realpath**, so the plugin's `@deepseek-ai/*` imports start walking from `E:\Deepseek\Workspace\...` and never reach the in-box fallback `$DSH_HOME/profiles/node_modules` (the healed module fallback that supplies every `@deepseek-ai/*` package at runtime).
- `file:` specs mangle Windows drive-letter paths in pnpm (`ENOENT: scandir '<profile>\E:\...'`).
- A tarball extracts the package inside the profile's own `.pnpm` store, so its realpath stays under `$DSH_HOME/profiles/` and every runtime import resolves through the fallback. That is why the installable manifest declares **no runtime dependencies**: all `@deepseek-ai/*` imports are in-box packages served by the fallback, and `clsx` is inlined into the browser bundle. (`workspace:^` ranges would also break pnpm installs outside the harness workspace — `ERR_PNPM_WORKSPACE_PKG_NOT_FOUND`.)
To update the plugin later: rebuild, re-sync `lib/`, re-pack, then `dsh plugin --profile web add <tarball>` again (or bump the version first). To remove it: `dsh plugin --profile web remove dsh-theme-colorizer`.
### Method B: build-integrated (manual, for development inside the checkout)
Use this only when developing against the harness source tree directly. **Do not combine with Method A** — both would insert the same `theme-colorizer` row id into the composition.
**Step 1: Place the plugin in the packages directory**
Copy the plugin into `packages/client/` of the deepseek-harness repository:
```powershell
Copy-Item -Recurse E:\Deepseek\Workspace\plugins\dsh-theme-colorizer packages\client\dsh-theme-colorizer
```
Or use a junction for live edits:
```powershell
New-Item -ItemType Junction -Path packages\client\dsh-theme-colorizer -Target E:\Deepseek\Workspace\plugins\dsh-theme-colorizer
```
**Step 2: Register the plugin as a TypeScript project**
The DSH build is two-phase: `tsc -b` compiles every package and emits `lib/types/*.js`, then `tsdown` bundles each package **from** those files. `tsc -b` only compiles packages listed as project references, so add the package to `tsconfig.client.json`:
```jsonc
{ "path": "./packages/client/dsh-theme-colorizer" },
```
**Step 3: Add the plugin to the web-app composition**
In `packages/bundle/web-app/cordis.patch.yml`, add the entry under the `dsh.client` rows section:
```yaml
- id: theme-colorizer
name: 'dsh-theme-colorizer'
```
**Step 4: Add the package as a dependency**
In `packages/bundle/web-app/package.json`, add `"dsh-theme-colorizer": "workspace:^"` to `dependencies` and `devDependencies`.
**Step 5: Install and build**
```powershell
cd deepseek-harness
pnpm install
pnpm run build:lib:client
```
Do **not** use `pnpm run build --filter dsh-theme-colorizer` — the package has no standalone `build` script, and tsdown alone cannot build it (its entry points are the `lib/types/*.js` files that only the `tsc -b` phase produces). `pnpm run build:lib:client` runs `tsc -b tsconfig.client.json` then `tsdown --env.DSH_BUILD_FACE client`; the workspace tsdown pass auto-discovers the package through the `packages/*/*` glob.
### Restart the web server (both methods)
If the web server is already running, restart it so the Loader re-reads the composition and the modules scan re-serves the plugin bundle:
```powershell
# Using dsh CLI:
dsh web
# Or during development:
pnpm run dev:web
```
### Verification
1. Open the Web UI at `http://127.0.0.1:3080`
2. Click the Settings icon (gear) in the sidebar
3. In the General section, you should see the **"Color Theme"** row with five colored swatches
4. Click a swatch — the UI colors should update immediately
5. The selection persists across page reloads (it is stored in the DSH user-settings document)
## Troubleshooting
### `[UNRESOLVED_ENTRY] Cannot resolve entry module lib/types/index.js`
This means tsdown ran before TypeScript emitted `lib/types/index.js` — i.e. `tsc -b` did not compile this package. The DSH build compiles first (`tsc -b` emits `lib/types/*.js`) and bundles second (tsdown reads those files). The two usual causes:
1. **The package is not a project reference.** Add `{ "path": "./packages/client/dsh-theme-colorizer" }` to the `references` array of `tsconfig.client.json` (Method B, Step 2), then run `pnpm run build:lib:client`.
2. **tsdown ran directly.** Run the full pipeline — `pnpm run build:lib:client` — instead of `tsdown` / `pnpm run bundle` alone. After a successful build, `packages/client/dsh-theme-colorizer/lib/types/` should contain `index.js`, `invariant.js`, and `client/index.js`.
### `cannot get property "theme" without inject`
The client half accesses `ctx.theme` but the plugin's Cordis `inject` array omitted `'theme'`. Cordis refuses to hand out a provided service that was not injected. The plugin declares `inject = ['slots', 'locale', 'connection', 'remote', 'settingsScope', 'theme']` — keep `'theme'` in that list.
### `ERR_PNPM_WORKSPACE_PKG_NOT_FOUND` when installing
The installable manifest must not carry `workspace:^` ranges — those only resolve inside the harness workspace. The bundle's `package.json` deliberately declares no runtime dependencies: every `@deepseek-ai/*` import is an in-box package served by the `$DSH_HOME/profiles/node_modules` fallback at runtime, and `clsx` is inlined into the browser bundle.
### `ENOENT: scandir '<profile>\E:\...'` when using a `file:` spec
pnpm on Windows mangles drive-letter paths in `file:` specs. Install from a packed tarball instead (Method A, step 3).
### Nothing changes after picking a theme
- Check the browser console for the "cannot get property" error above (stale bundle: rebuild and re-sync `lib/`).
- Confirm the settings row appears: the composition must contain the `theme-colorizer` row (`dsh --profile web --dump-config`). If you installed with Method A, do **not** also keep a `theme-colorizer` row in `packages/bundle/web-app/cordis.patch.yml` — a duplicate row id fails the load.
## File structure
```
dsh-theme-colorizer/
package.json # Bundle + client manifest (dsh.bundle / dsh.client), no runtime deps
cordis.patch.yml # Bundle patch: inserts the theme-colorizer row
tsconfig.json # TypeScript config (extends tsconfig.base.client.json, used for the harness build)
tsdown.config.ts # Build config using the shared clientBundle preset (harness build only)
lib/ # Built output (copied from the harness build before packing)
index.js # Host-side entry (bundled)
invariant.js # Companion entry (bundled)
client.js # Browser bundle
types/ # Emitted declarations
src/
index.ts # Host-side entry: inert apply (persistence is client-side)
invariant.ts # Companion plugin for invariant checks
theme-color-settings.ts # Shared constants, types, and the localStorage key
css-modules.d.ts # Type declarations for CSS Module imports
client/
index.ts # Client-side entry: reads/persists theme, applies overrides, registers row
color-themes.ts # Theme definitions (token overrides for each color theme)
ThemeColorRow.tsx # React component for the settings row
ThemeColorRow.module.css # Styles for the settings row
locales.ts # i18n strings (zh/en)
README.md # This file
```
## Known Limitations and Deferred Work
- **Color theme does not affect the scrollbar or code-highlighting colors** — those are styled by separate stylesheets (`scrollbar.css`, `shiki.css`) that use their own token sets. A future version could override those tokens as well.
- **Persistence is per-browser** — the choice lives in `localStorage`, so it does not follow the user across machines or browsers. The DSH settings document would be the natural home, but the Host API gateway does not expose plugin-owned settings namespaces yet (`settings-not-exposed` is a deliberate allowlist boundary; see `WEB_SETTINGS_NAMESPACES` in the apiproxy). If the harness adds plugin-owned exposure, migrate the persistence to the settings seam.
- **No custom theme authoring** — the five themes are built in. A future version could allow users to define custom themes.
- **The active theme is not reflected in the initial HTML bootstrap** — the first paint before the client plugin tree activates uses the default theme. The built-in theme bootstrap (`boot-theme.ts`) only handles the light/dark/system preference. A future version could also embed the color-theme tokens in the bootstrap script.Install
dsh plugin --profile web add github:RealHacker/dsh-theme-colorizer
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-theme-colorizer from the hub
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.