Skip to content
dsh.fish
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

![Color theme](asset/dsh-color-theme.jpeg)

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

  • This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.
Source