DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

1020252969 /

1020252969/dsh-wallpaper

Verified

A DSH wallpaper plugin: bundled default background, live control panel, translucent shell tokens.

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

dsh-shell-wallpaper

One picture behind the whole DeepSeek Harness GUI, plus a self-contained control panel that takes effect while you drag.

A default picture ships inside the plugin (assets/default-bg.jpg), so a fresh install shows a wallpaper with zero configuration. The user can switch to any picture at any time (upload / URL / path) and come back to the bundled one with one click.

A small host half (serves exactly one file over HTTP, plus a /info descriptor) and a small client half (paints it behind the shell, polls that descriptor, and draws a floating 🎨 panel). No Wallpaper Engine, no video, no glass pipeline — the DSH built-in theme and the installed skin plugins have no background-image support at all, so this is the smallest thing that works.

Why the panel is drawn by us instead of by the settings framework: dsh-settings' own README says "autoGenerate … currently no shipped client does this" — the live settings service publishes the schema, but no released client renders a page from it. Registering a custom settings slot has also white-screened this profile before. So the panel is plain DOM injected by our already-proven client bundle, in its own try/catch: if it breaks, only the panel breaks, the wallpaper still paints and the GUI never white-screens.

Install

& 'D:\software\deepseek harness\resources\runtime\cli\bin\dsh.cmd' plugin --profile desktop add 'file:C:\Users\1\.dsh\plugins\dsh-wallpaper'

Or from npm / from the published repository:

& 'D:\software\deepseek harness\resources\runtime\cli\bin\dsh.cmd' plugin --profile desktop add 'dsh-shell-wallpaper'
& 'D:\software\deepseek harness\resources\runtime\cli\bin\dsh.cmd' plugin --profile desktop add 'github:1020252969/dsh-wallpaper'

No config is required — the bundled assets/default-bg.jpg is used automatically. If you want to point at your own file or set defaults, add the config to C:\Users\1\.dsh\profiles\desktop\cordis.patch.yml. Write the whole block — a patch row replaces the row's entire config, so a partial block silently resets the missing keys to their defaults:

- id: wallpaper
  config:
    enabled: true
    imagePath: ''            # empty = the bundled default picture
    opacity: 1
    blur: 0
    panelAlpha: 0.35
    backdropBlur: 0
    fill: cover
    scrimColor: '#08090e'
    scrimAlpha: 0.5

Reload the page once after installing the bundle (the client bundle is injected at page load). After that, everything is live — no more reloads.

The control panel

Click the 🎨 launcher in the bottom-right corner. The panel has:

  • a status badge (✓ 背景已启用 / ✗ 已关闭 / 未配置 / 文件缺失) and a live preview thumbnail,
  • an enable checkbox,
  • five sliders — opacity, wallpaper blur, panel alpha, frosted-glass blur, scrim strength — each showing its current value,
  • fill mode (铺满 / 完整), a scrim colour picker,
  • four presets (清晰 / 磨砂 / 鲜明 / 氛围),
  • upload a local picture (JPG/PNG/GIF/WebP/BMP/AVIF), or paste an http(s) URL and click 应用链接,
  • 内置默认图 — switch back to the picture bundled with the plugin (also re-enables the background),
  • 清除背景 (switch off) and 重置为配置 (drop all panel changes).

Every control writes through POST /wallpaper/api/config and the change shows up within about a second, because the painter polls /info.

Where changes are stored

Panel changes go to <wallpaper-dir>/settings.json (default C:\Users\1\.dsh\wallpaper\settings.json), not into cordis.patch.yml. The file is an override layered on top of the profile patch:

read() = normalizeConfig({ ...plainConfig(profile row), ...await store.load() })

Consequences: changes survive a restart, dragging a slider does not rewrite the patch (no config-reload storm), and reset to config simply deletes the override file. The dir can be changed with config.wallpaperDir or the DSH_WALLPAPER_DIR env var (the latter is what the tests use to isolate a scratch dir).

Config

Every field is volatile(), because the live settings service can only persist volatile fields — a non-volatile field makes the form read-only in a way that is very hard to notice. (Even though our own panel no longer depends on that form, the exported Config stays for compatibility.)

Field Meaning Default
enabled Switch the whole background off without deleting the config. true
imagePath Absolute path to the picture. Forward slashes. An empty value uses the bundled assets/default-bg.jpg. A wrong path fails quietly (console warning, stock theme). '' → bundled
opacity 0–1, the image layer's own opacity. 1
blur 0–64 px, blur on the image itself. Above ~24 it is a colour wash. 0
panelAlpha 0–1, how opaque the shell's surfaces stay. This is the readability knob. 1 hides the image completely. Two nested surfaces are painted (_frame and _centerCol), so the visible result is closer to 1-(1-a)². 0.35
backdropBlur 0–64 px, backdrop-filter on the big shell surfaces — frosted glass instead of a flat tint. 0 = off. 0
fill cover (铺满) or contain (完整). cover
scrimColor Scrim colour, #rrggbb / rgb() / rgba(). Sits between image and UI. #08090e
scrimAlpha 0–1, scrim strength. 0.35
scrim Legacy: any CSS colour, used only when scrimColor cannot be parsed. ''

How it works, and why it is not a one-liner

The shell paints opaque surfaces over everything. From @deepseek-ai/dsh-client-ui-layout's AppFrame.module.css:

.mP3x-q_frame     { background: var(--dsw-alias-bg-base) }
.mP3x-q_centerCol { background: var(--dsw-alias-bg-base) }
.mP3x-q_sidebarCol{ background: var(--dsw-specific-sidebar-fill) }
[data-windows-titlebar] .mP3x-q_frame { background: var(--dsw-specific-sidebar-fill) }

So a background-image on body is invisible. The plugin instead:

  1. puts a position: fixed; z-index: -1 layer on <body>,
  2. re-points --dsw-alias-bg-base and --dsw-specific-sidebar-fill at color-mix(in srgb, <theme base> <panelAlpha>%, transparent),
  3. repeats those overrides under body[data-ds-dark-theme][data-dsh-wallpaper] — this is the part that bites. The theme package appends its palette stylesheet to <head> after this plugin's, and body[data-ds-dark-theme] has the same specificity (0,1,1) but comes later, so it repaints the frame with an opaque dark colour and the image disappears. The dark selector raises specificity to (0,2,1).

The <theme base> is read from getComputedStyle(document.body).backgroundColor, not from the token — the token is usually another var() and would resolve to literal text.

Class names in the layout are CSS-module hashes (mP3x-q_), which change between releases, so nothing here matches on the hash. The --dsw-alias-* tokens are the stable contract. The frosted panels are found the same way the CSS cannot: by geometry — any direct-ish child of <body> covering at least 20% of the viewport with an opaque background gets data-dsh-wp-frost, and the stylesheet only applies backdrop-filter to that attribute.

Staying in sync (and self-healing)

GET /wallpaper/api/info carries a revision string that changes whenever a knob or the file does. The client polls it about once a second (and stops while the tab is hidden) and repaints only when it moves. That is what makes the panel sliders live, and it also means replacing the picture in place takes effect without a reload. Against a version-1/2 host, the same value is derived locally from the payload.

The host is a SPA: switching conversations/themes appends new stylesheets that can re-paint the two tokens opaque, or knock our <style> out of <head> — the wallpaper "goes black" mid-conversation. An unchanged revision used to skip repainting entirely, so it never recovered. Every tick now also runs selfHeal() (checks the stylesheet/layer/flag are present, moves our sheet back to the end of <head> if needed, repaints from lastGoodInfo). shellBaseColor() caches only fully opaque base colours so repeated repaints cannot darken the tint.

Endpoints

Route Behaviour
GET /wallpaper/api/info JSON. ok:false + reason (disabled / missing / not-a-file) + error when there is nothing to paint; cache-control: no-store. Ships the four presets and wallpaperDir for the panel.
GET|HEAD /wallpaper/api/image Streams the one configured file (or the bundled default). ETag + If-None-Match → 304. There is no path parameter, so it cannot read another file.
POST /wallpaper/api/config {...knobs} → whitelist → persist to settings.json. {reset:true} deletes the override. {useDefault:true} clears imagePath (→ bundled picture) and re-enables the background.
POST /wallpaper/api/clear Persist {enabled:false}.
POST /wallpaper/api/upload {dataBase64} → magic-byte sniff → save as wallpaper-<time>-<rand>.<ext> (client filename ignored) → point imagePath at it and enable. 12 MB cap.
POST /wallpaper/api/import-url {url} → download → same save path.

URL import and SSRF

importFromUrl is deliberately paranoid because the host runs on the user's own machine:

  • only http:/https:, no credentials in the URL, only ports 80/443;
  • dns.lookup(all) resolves the host before fetching, and every returned address must pass ipIsPrivate() — loopback, link-local, RFC1918, CGNAT, the documentation ranges, 169.254.169.254-style metadata, multicast/reserved and IPv6 ::1/fc00::/7/fe80::/10/::ffff: mapped addresses are all refused;
  • redirects are followed manually (redirect:'manual'), at most 4 hops, re-validating each hop (so a public URL cannot bounce into the LAN);
  • 15 s timeout via AbortController; content-length and the actual stream are both capped at 12 MB;
  • the downloaded bytes must still pass sniffImage() — the file is never trusted by its extension.

Uploads are equally strict: sniffImage() recognises real magic bytes for jpg/png/gif/webp/bmp/avif and rejects everything else (415), and malformed JSON / bad base64 is 400.

Limits

  • One image. No slideshow, no video, no per-workspace image. If you want those, install dsh-plugin-wallpaper-engine (1.4.0, ~4.5 MB client bundle + 8 host rows) instead.
  • Dark mode only in practice. The scrim and panelAlpha defaults are tuned for the dark palette with a bright picture. On a light theme you would want panelAlpha near 0.95 and a lighter scrim.
  • The frosted-panel heuristic is a heuristic. If a future layout nests the frame deeper than two levels, nothing gets frosted and the setting looks dead.
  • The panel is a floating button, not a settings page. It is intentionally independent of the settings framework (see the top of this file). It appears after one page reload from installation.

Development

Tests need @deepseek-ai/schemastery at runtime, which lives in the profile, so the source directory carries a node_modules junction to C:\Users\1\.dsh\profiles\desktop\node_modules:

cd C:\Users\1\.dsh\plugins\dsh-wallpaper
node test\host.spec.mjs     # 34 assertions
node test\client.spec.mjs   # 77 assertions

The client spec stubs setInterval — the real file polls, and an unstubbed interval keeps node alive forever. It also uses a thin DOM (no addEventListener) for the painter assertions so the panel degrades gracefully, and a rich DOM for the panel assertions. Host tests point DSH_WALLPAPER_DIR at a scratch dir so they never touch the real settings.json.

Uninstall

& 'D:\software\deepseek harness\resources\runtime\cli\bin\dsh.cmd' plugin --profile desktop remove 'dsh-shell-wallpaper'

Also delete the - id: wallpaper block from cordis.patch.yml. Without the plugin the block is inert (Cordis skips rows whose bundle is missing), but leaving it means a stale config waiting for a future reinstall.

—/ 5

No ratings yet

Verified DSH bundle

Commit c7ef9df0153e

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