Load Wallpaper Engine .mpkg files as the DSH web background: embedded video, time-of-day switching, unified frosted blur, local wallpaper library & rotation.
Install
# from npm (prebuilt)
dsh plugin --profile web add dsh-mpkg-wallpaper
# from GitHub (first run asks for allowBuilds approval — follow the hint, retry)
dsh plugin --profile web add github:XHR666/dsh-mpkg-wallpaper
Any plugin you install runs third-party code with your own permissions — it can read your files, use your credentials, and reach the network, and tool approvals don’t sandbox it. GitHub-sourced plugins also run build scripts at install time — pnpm blocks those until you allow them, so an install can stop with ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED or ERR_PNPM_IGNORED_BUILDS; dsh prints the exact key to add under allowBuilds in your profile’s pnpm-workspace.yaml, and the install works on the next run. Allowing a build is a trust decision: only install sources you trust, and pin a commit (github:owner/repo#sha).
README
Adds background wallpapers to the DeepSeek Harness Web UI (dsh web): Wallpaper Engine .mpkg parsing, Steam Workshop folders, video/web/image wallpapers, time-of-day switching for Time Variation wallpapers, a full-screen blur system, theme colours and glass surfaces, a local wallpaper library, scheduled rotation, a Now playing widget and one-click update. Almost every appearance detail is adjustable.
Version scope: this document describes the implementation shipped as
3.10.1inpackage.json. The publish surface is 15 files (lib/runtime files +package.json,icon.svg,cordis.patch.yml,README.md,README.en.md,THIRD-PARTY.md,LICENSE; measured withnpm pack --dry-run: 15 files / 2,088,240 B unpacked);lib/liquid-glass/**,lib/liquid-glass-bundle.js,dist/,tools/anddocs/never enter the npm package (package.json:8-21). Defaults that changed:npNowPlayingoff → on (since 3.8.0) andpowPauseHiddenoff → on (since 3.9.0, migrating only profiles that never set it; see What changed in this version).
Download · Install
The plugin is published on npm (dsh-mpkg-wallpaper). Four ways to load it — pick one from this table, then read its section:
| Option | Who it is for | How to update | Client UI |
|---|---|---|---|
1 dsh plugin add (recommended) |
the default choice; the market shows "installed" | dsh plugin --profile web update … |
full |
| 2 pnpm manual install | you manage the profile's dependency table yourself | same (through the dependency table) | full |
| 3 Git clone | developers / offline / you want to edit the code | git pull |
full |
| 4 single-file bundle | offline emergencies; reusing the routes from a non-DSH host | regenerate and replace that .mjs |
host half only |
Option 1: dsh plugin add (recommended, market-recognized)
dsh plugin --profile web add dsh-mpkg-wallpaper
# restart dsh web, then Ctrl+F5 in the browser
Option 2: pnpm manual install
pnpm --dir $DSH_HOME/profiles/<profile> add dsh-mpkg-wallpaper
# restart dsh web, then Ctrl+F5
Same source as option 1, just without the dsh plugin wrapper.
Option 3: Git clone (developers / offline)
git clone https://github.com/XHR666/dsh-mpkg-wallpaper.git $DSH_HOME/profiles/<profile>/node_modules/dsh-mpkg-wallpaper
# then register in the profile's cordis.patch.yml:
# - insert:
# - id: dsh-mpkg-wallpaper
# name: dsh-mpkg-wallpaper
# restart to take effect
Option 3 writes no dependency entry ⇒ the market does not show "installed" (display only, functionality unaffected).
Option 4: single-file bundle (offline / drop-in; host half only)
Inline the host half into a self-contained ESM and register that:
cd /path/to/dsh-mpkg-wallpaper
node tools/build-bundle.mjs # output: dist/dsh-mpkg-wallpaper.bundle.mjs (measured 449,671 B / 439.1KB; the gate output is authoritative)
node tools/build-bundle.mjs --check # cross-check against the source: export surface / route table / ping JSON shape (20 assertions)
node tools/bundle-equivalence-test.mjs # the fuller equivalence gate (38 assertions; gate step 11)
Copy dist/dsh-mpkg-wallpaper.bundle.mjs anywhere (e.g. ~/.dsh/plugins/), register it by absolute path in the profile's cordis.patch.yml, then restart dsh web:
# $DSH_HOME/profiles/<profile>/cordis.patch.yml
- insert:
- id: dsh-mpkg-wallpaper
name: /absolute/path/dsh-mpkg-wallpaper.bundle.mjs # ← points at the .mjs file itself
What this path loads and what it does not (code and gate facts):
| Item | Option 4 behaviour | Evidence |
|---|---|---|
| Host half (upload/Range streaming, scene extraction, audio lists, settings persistence, diag reporting — 41 routes) | Complete (four host modules inlined; node builtins only) | route table identical [41 entries] |
/api/mpkg-wallpaper/ping |
{ok, version, betterSidebar, betterSidebarVersion} key set identical to the source |
build-bundle.mjs --check |
| Client half (settings panel / wallpaper layer / frost / Now playing) | Not loaded — only the host export surface (apply / inject / __mpwTest) is present |
DSH finds the client half per package (Loader entries declaring dsh.client → exports["./client"]); a bare .mjs has no package.json |
GET /api/mpkg-wallpaper/lg/* (legacy WebGL hosting route, no client caller) |
404 unless a liquid-glass/ folder sits next to the bundle |
the route resolves liquid-glass/ from import.meta.url (lib/index.js:3453) |
ping.version |
null when the parent directory has no package.json (version display only) |
new URL('../package.json', import.meta.url) (lib/index.js:1622) |
| "Check for updates / one-click update" | No companion package.json ⇒ update-check 500s and update-apply writes next to the bundle: not recommended here |
lib/index.js:1792-1860 |
| Uninstall | Delete that .mjs and its line in cordis.patch.yml |
— |
Conclusion: option 4 is a degraded "host capability only" load (handy offline, as an emergency path, or when reusing the routes from a non-DSH host); use options 1/2/3 for the full UI. The artifact is not committed (
dist/is in.gitignore: it is a pure derivative oflib/*.js, two builds are byte-identical — section ② oftools/bundle-equivalence-test.mjs; generate it at release time and publish the hash).
Update
# options 1 / 2: through npm's latest tag
dsh plugin --profile web update dsh-mpkg-wallpaper
# option 3: inside the clone
git pull
# option 4: regenerate and replace that .mjs
node tools/build-bundle.mjs
Every route ends the same way: restart dsh web, then press Ctrl+F5.
Uninstall
Options 1/2/3: dsh plugin --profile web remove dsh-mpkg-wallpaper.
Option 4: delete the .mjs and its line in cordis.patch.yml.
Leftover data (optional cleanup): browser localStorage['dsh.mpkg-wallpaper.v2'] and the host directory ~/.dsh-mpkg-wallpaper/ (settings.json, web-store.json, media-audio.json, uploaded mpkg files, transcode cache, diag-*.json).
30-second quick start
The shortest path from installed to wallpaper on screen — three steps.
- Install and restart: pick any option in the previous section, restart
dsh web, pressCtrl+F5. - Open the panel: left sidebar → Settings → MPKG Wallpaper.
- Pick a wallpaper, any one of:
- drop in an
.mpkgfile (video wallpapers play directly; scene wallpapers use the static frame / layer composite) - choose a local image/video, or paste an image URL
- Custom local wallpaper folder: point it at any folder (the Steam
steamapps/workshop/content/431960root works — every subfolder counts as one wallpaper)
- drop in an
The defaults are already usable: master switch on, hybrid mode on, unified blur on (30px), Now playing mounted in the left sidebar. To fine-tune, three controls are enough to start: Wallpaper → Frosted blur (0–40), Surface unify → Full-screen blur degree (0–40), Wallpaper → Lens zoom (10–2000%).
If nothing happens, press one-click diagnostics report in the Other tab first (it downloads a JSON file when the host is unavailable), then take it to Bug reports.
Core features
This section groups features by what you can perceive (sources, time variation, blur, appearance, playback, library and rotation, safety, backup) rather than by code module.
📦 Wallpaper sources
- Wallpaper Engine
.mpkg: the container is parsed in the browser (nothing is uploaded to a third party); video wallpapers play their embedded mp4 / video textures, scene wallpapers have their assets extracted, and Time Variation wallpapers pick the material for the current time slot - Steam Workshop folders: Wallpaper Engine installs are discovered automatically (registry +
libraryfolders.vdf, non-default drives included) and listed asvideo / web / scene; the workshop root (steamapps/workshop/content/431960) can also be set as a custom folder — every subfolder is auto-detected as one wallpaper - Video:
.mp4/.webm/.mov/.m4vplay directly; Web: HTML loads in a sandboxed iframe (with a risk pre-scan); Image/animation/link: local images or URLs (includingdata:image) - Custom folder: any folder;
.mpkgfiles, workshop subfolders, images, videos andscene.pkgcan be mixed freely
⏰ Time Variation wallpapers
- Detects the WE time-variation properties (
morningtime / daytime / dusktime / nighttime / timevarying, default hours 5/8/17/20,lib/client.js:10419-10423) - Lazy loading: only the current slot is extracted (tens of MB per slot); other slots are read when you switch, so importing every slot at once cannot OOM
- Manual slot lock: slot buttons appear only for slots that actually exist inside the container (
lib/client.js:12797-12813), stored intimeOverride; "Auto" returns to time-based switching - No cross-talk: switching wallpapers clears the previous wallpaper's slot cache
🌊 Full-screen blur (frost) system
- Unified blur: one slider drives the wallpaper's blur plus the sidebar/title-bar frost thickness; whether the chat area and the new-chat button follow is controlled separately
- UI blur (independent switch + amount each): dialogs (generic centre windows + chat input), settings panel, download/confirm popups, popovers (menus/dropdowns/tooltips), mask (full-screen backdrop), left sidebar frost
- Show wallpaper: left sidebar / title bar / right sidebar dock are independent, and the title-bar frost radius can be set separately
🎨 Theme colours and glass surfaces (Aqua experiments default to off)
- Theme colour (
themeColor): colour picker + presets, driving the sidebar/title bar/new-chat button/settings dialog base colour; Accent (accent) drives the brand interaction colour (buttons/sliders/selection/links/send button) - Panel colours match wallpaper (
aquaTint): samples the wallpaper's dominant colour (video/GIF refresh every 2 s); plus unified fog (full-screen tinted mask), adaptive text colour + blue cleanup, dark-background text readability, task list frost - Liquid glass (CSS/SVG):
lgCss+ refraction amount on the composer / left sidebar / title bar; the WebGL runtime inlib/liquid-glass/is not part of the build (see File structure)
🧩 dsh-better-sidebar compatibility (shown once that plugin is detected)
- When installed, the Other tab gains a compatibility section: master switch
bsCompat(default on) plusbsFloat(floating two-layer fix: 14px rounded shell + transparent inner layer + zero margin + the resize strip moved inside the panel) /bsFont/bsReveal+bsRevealAlpha/bsAqua - The host
/pingreturns{ok, version, betterSidebar, betterSidebarVersion}; the client writesbody[data-mpw-bs-version]and version-specific rules are gated with[data-mpw-bs-version^="…"](lib/index.js:1626-1643,lib/client.js:255-269) - Details in
docs/BETTER-SIDEBAR-COMPAT.mdanddocs/BETTER-SIDEBAR-DOM-CONTRACT-0.19.1.md; regression:node tools/better-sidebar-compat-test.mjs
⏯️ Playback control and power saving
- Video/web wallpapers can be paused/played from the Wallpaper tab; the button reflects the real playback state, and changing unrelated settings does not restart playback (the
video.srcequality test was fixed) - Three power-saving switches: pause when the page is hidden/switched away, pause on window blur, pause on battery (
getBattery; silently skipped when unavailable). Any one of them pauses, all must recover to resume, and they share one gating model with manual pause
🚀 Large-file hybrid mode (hybrid, on by default)
- The mpkg is streamed to the DSH host → stored on disk → played back over HTTP Range (
lib/index.js:1688-1728,:1730-1790); >600MB works because the bytes never sit in memory. Turning it off falls back to browser-only mode (600MB ceiling)
🖼️ Local library and rotation
- Steam auto-discovery + custom folder (cross-platform folder picker); WE playlists (
general.playlistsinconfig.json) are imported as rotation lists - Previous/next switching and scheduled rotation (
rotate+rotateMin, 1–120 minutes); ticking items does not make the list jump to the top
🛡️ Safety and coexistence
- Conflict detection: other wallpaper/theme plugins disable this feature automatically (it can be forced back on, which writes
forceEnabled) .exe/applicationwallpapers are excluded outright (lib/web-wallpaper.js:101,:199); custom folders only read media files; host routes validate against path traversal; web wallpapers are isolated in a sandboxed iframe
💾 Backup, restore and settings persistence
- Backup & restore in the Other tab exports appearance settings as shareable JSON (
BACKUP_FIELDS,lib/client.js:11979-11993) and restores them on import - Besides browser
localStorage(keydsh.mpkg-wallpaper.v2,lib/client.js:48), settings are also stored on the host as~/.dsh-mpkg-wallpaper/settings.json, so they survive port changes and cleared browser data
Supported types and boundaries
This section answers "does my material work, and how well"; the boundary list under the table explains why some things are out of reach.
| Type | Status | What it can / cannot control |
|---|---|---|
| mpkg (video) | ✅ Full | Embedded mp4 / video textures play directly; mute, speed, pause, blur/zoom/brightness all adjustable |
| mpkg (scene) | 🟡 Compromise | Static frame / layer composite / embedded video slots; Live2D puppets, shaders and scripts are out of reach |
| Time Variation | ✅ Multi-slot | Automatic switching + manual lock; only the current slot is extracted |
| Video (mp4/webm/mov/m4v) | ✅ Full | Plays directly; fps/resolution caps require ffmpeg transcoding |
| Web (HTML) | 🟡 Experimental | Sandboxed iframe + WE API shim; Live2D-class settings are editable; external-SDK / heavily interactive ones are not adapted |
| scene.pkg loose folder | 🟡 Compromise | Same as mpkg scene wallpapers |
| preview.gif / image / animation | ✅ Full | Falls back to the author's preview animation when a scene has nothing better (lib/client.js:887, :12272) |
| Application (exe) | ❌ Excluded | Content detection yields unknown/excluded-application; never read, never executed (lib/web-wallpaper.js:199) |
| Custom folder (mixed) | ✅ Full | mpkg files and workshop subfolders mixed; bounded scan (depth ≤4, ≤4000 entries, lib/web-wallpaper.js:213-232) |
Hard boundaries (facts about the current implementation, not "later" items):
- Full dynamic reconstruction of scene wallpapers is impossible — the MDL puppet skeleton has no public format documentation and shaders/scripts have no Web runtime (see Scene adaptation)
- The pure-Scene renderer selector is not wired up:
sceneRender(webgl/static/elysia) in the Other tab has buttons and writes a value, but nothing in the repository reads it. The real path is decided by renderer availability (online →:8899iframe, offline → static-frame fallback,lib/client.js:3133-3138) - Web wallpapers cannot reproduce CSS
:hover/:active,isTrusted:true, in-framecontextmenuor pointer lock/fullscreen/download popups (inherent limits of synthetic events,docs/WEB-WALLPAPER.md§11.4) - Assets above 600MB work only in hybrid mode; browser-only mode additionally caps video textures at 250MB, images at 200MB and local image files at 100MB
Settings reference (7 tabs)
This is the authoritative table of every setting: each row = panel label + internal key + default + what it does + how to turn it off. The tab order matches the panel and is fixed: TAB_ORDER = ["source","wallpaper","appearance","unify","blur","other","liquid"] (lib/client.js:10730), labelled Background source / Wallpaper / Appearance / Surface unify / UI blur / Other / Liquid Glass (test).
1. Background source (source)
| Label | Key | Default | Purpose | Off / rollback |
|---|---|---|---|---|
| Enable mpkg background | enabled |
on | Master switch; off applies no background at all | off |
| Upload to dsh for streaming playback | hybrid |
on | Large-file hybrid mode (host upload → disk → Range playback, no 600MB ceiling) | off = browser-only mode |
| mpkg / image / video file / image URL | — | — | File pickers and URL input (http(s) or data:image) |
"Clear wallpaper" |
| Custom local wallpaper folder | customDirPath |
empty | Any folder; can point at the workshop root, every subfolder becomes one wallpaper | clear the field |
| Local wallpaper library (Steam scan) | — | — | Scans the WE install and Workshop, importing wallpapers and config.json playlists |
rescan replaces it |
| Rotation | rotate / rotateMin |
off / 5 min | Switches to the next wallpaper on a timer (1–120 minutes) | off |
| Time slot | timeOverride |
auto | Manually locks morning/day/dusk/night; buttons list only slots that really exist | "Auto" |
| Current wallpaper card | — | — | Preview, display name, container file name, type; pause/play, refresh, clear | — |
2. Wallpaper (wallpaper)
Contains the Wallpaper picture and Power saving sub-sections.
| Label | Key | Default | Purpose | Off / rollback |
|---|---|---|---|---|
| Mute (web wallpapers) | mute |
on | Web-wallpaper audio; off lets the wallpaper make sound | off |
| Now playing (above Settings in the sidebar) | npNowPlaying |
on | Left-sidebar player whose transport row controls wallpaper sound. Off = zero injection; yields to another plugin (data-mpw-np-yield) |
off / restore defaults |
| Play/pause also controls the wallpaper | npLinkWallpaper |
on | Now playing's play/pause and position also drive the wallpaper itself; off = only this plugin's player (when the wallpaper media is the only source the controls are honestly disabled) | off |
| Web frame mode (web wallpapers) | webFrameMode |
auto |
Three tiers for the web-wallpaper iframe: auto = sandboxed first, and when the browser blocks it / the WE API shim never reports it degrades inside the sandbox only (reloads without the shim marker, still opaque-origin) and never auto-upgrades to same-origin; sandbox = forced isolation, no automatic degrade (the reason is written to the status line); compat = same-origin compat tier (no shim marker, only reachable when the user picks it explicitly). One decision table only (lib/web-wallpaper.js); status in window.__mpwWebFrame and #mpw-bgWrap[data-mpw-webframe-mode|-reason|-attr|-fallback|-autocompat|-degraded], ledger in window.__mpwWebFrameGuard (see docs/WEB-WALLPAPER.md §3.0) |
pick auto; ?webframe=auto|sandbox|compat overrides for the current page |
| Auto-degrade to compat (high risk) | webFrameAutoCompat |
off | Risk rollback: when on, auto mode may switch to compat after the browser blocks the wallpaper — i.e. that third-party wallpaper runs same-origin with DSH (can read the host UI/local storage and call plugin routes with cookies). While off it only records the refusal and tells you to switch manually. Gate: tools/web-frame-origin-guard-test.mjs (see docs/SECURITY-ROUTES.md F5) |
off |
| Renderer upstream allowlist | sceneUrlWhitelist |
empty | Cross-origin rollback: sceneRendererUrl/sceneExtUrl only accept http(s) loopback (127.0.0.1/localhost/::1) and "same-host literal IPs" by default; a cross-origin upstream must have its origin listed here (comma/space separated) or it is refused and falls back to 127.0.0.1:8902 (recorded, never silent). ⚠ A listed upstream is also allowed to receive the scene token st (see docs/SECURITY-ROUTES.md F6) |
clear it = loopback only |
| Horizontal flip (mirror) | flipX |
off | Mirrors the wallpaper horizontally | off |
| Vertical flip (mirror) | flipY |
off | Mirrors the wallpaper vertically | off |
| Decode fps cap | fpsCap |
unlimited | Host ffmpeg frame extraction when the source exceeds the cap (24/30/48/60) | "unlimited" |
| Resolution cap | resMax |
source resolution | ffmpeg downscaling (720p/1080p/2K, aspect kept) | "source resolution" |
| Playback speed | playbackRate |
1x | 0.5–2x (steps 0.5/0.75/1/1.25/1.5/2) | 1x |
| ffmpeg status | — | — | Shows system/cached/env source; download when missing, uninstall the cached copy only (never touches the system one) | — |
| Adjustable options (collapsed) | propEdits |
empty | Editable and applied live: mpkg scene wallpapers use the P-203 cross-origin props channel (mpw-user-props, confirmed by a renderer receipt; see docs/WE-USER-PROPS.md §3); web wallpapers use the same-origin shim channel (resolution/language/volume, below) |
per-wallpaper reset |
| Frosted blur | blur |
12px | Wallpaper-layer blur (0–40) | 0 |
| Lens zoom | zoom |
100% | 10–2000% | 100% |
| Brightness | brightness |
100% | 50–150% filter | 100% |
| Lens position (pan) | lensX / lensY |
0 / 0 | Horizontal/vertical pan, ±2000 each | 0 |
| Power save · pause when hidden | powPauseHidden |
off | visibilitychange |
off |
| Power save · pause on blur | powPauseBlur |
off | blur/focus |
off |
| Power save · pause on battery | powPauseBattery |
off | getBattery; silently skipped when unavailable |
off |
3. Appearance (appearance)
Contains the Show wallpaper sub-section.
| Label | Key | Default | Purpose | Off / rollback |
|---|---|---|---|---|
| Floating cards | float |
off | Left sidebar/title bar become floating cards (radius + shadow + wallpaper showing through) | off |
| Theme colour | themeColor |
empty | Base tint of sidebar/title bar/new-chat/settings dialog (picker + presets) | empty = disabled |
| Panel colors match wallpaper | aquaTint |
off | Samples the wallpaper's dominant colour for panel bases (video/GIF refresh every 2 s) | off = use the picker |
| Accent | accent |
empty | Brand interaction colour (buttons/sliders/selection/links/send) | empty = DSH default |
| Mask custom colour | aquaColor |
empty | Custom colour for unified fog / panel tint | empty = grey |
| Custom grey text colour | fontColorGray |
off | Grey text uses a custom colour (fontColorGrayColor) |
off |
| Show wallpaper in left sidebar | sidebar |
on | off = solid opaque sidebar | off |
| Left sidebar frost | sidebarBlur |
off | backdrop-filter on the sidebar itself; removed automatically while popups are open |
off (requires sidebar reveal) |
| Left sidebar frost amount | sidebarBlurAmount |
14px | 0–40; taken over while unified blur is on | — |
| Show wallpaper behind the title bar | headerBg |
on | off = plain white title bar | off |
| Frost the title bar | headerBlur |
on | Taken over while unified blur is on | off |
| Title bar frost amount | headerBlurAmount |
0% | Frost thickness 0–100% (default 0 = transparent) | 0 |
| Set title bar frost separately | headerFrostOwn |
off | on = headerFrostAmount overrides the frost radius |
off |
| Title bar frost strength | headerFrostAmount |
30px | 0–60 | 0 |
| Right sidebar / dock blur | rightSidebarBlur |
on | The DSH right sidebar and bottom dock | off |
| Blur amount / surface opacity | rightSidebarBlurAmount / rightSidebarAlpha |
14px / 45% | 0–40 / 0–100% | — |
4. Surface unify (unify)
| Label | Key | Default | Purpose | Off / rollback |
|---|---|---|---|---|
| Unify blur | unifyTint |
on | One slider drives the whole-screen blur; takes over sidebar/title-bar/right-sidebar frost | off |
| Full-screen blur degree | unifyAmount |
30px | 0–40 (drives the wallpaper-layer blur) | — |
| Left sidebar / title-bar opacity | sidebarAlpha |
35% | Frost thickness 0–100% | — |
| Chat follows full-screen blur | chatFollow |
on | off = the chat area is driven by the Frosted blur slider | off |
| New-chat button follows panel opacity | sessionFollow |
on | off = back to the host's original colour | off |
| Unified fog (full-screen mask) | aquaMask |
off | All surfaces share one fog colour (moved here from the Aqua section) | off |
| Unified fog strength | aquaMaskAlpha |
82% | 0–100% | — |
5. UI blur (blur)
| Label | Key | Default | Amount key / default | Off |
|---|---|---|---|---|
| Blur dialogs | dialogBlur |
on | dialogAmount 14px |
off |
| Blur settings panel | settingsBlur |
on | settingsAmount 14px |
off |
| Blur download/confirm popups | confirmBlur |
on | confirmAmount 12px |
off |
| Blur popovers | popoverBlur |
on | popoverAmount 10px; plus popoverAlpha 94% surface opacity |
off |
| Blur mask (full-screen backdrop) | maskBlur |
on | maskAmount 8px |
off |
6. Other (other)
| Label | Key | Default | Purpose | Off / rollback |
|---|---|---|---|---|
| Light sharpening | sharp |
on | Improves low-quality GIFs; turn it off if animations stutter | off |
| Deep diving background box | thinkBg |
off | on = the thinking state gets a blurred background box | off |
| Task list frost | todoBlur |
off | Blurs todo-card backgrounds | off |
| Third-party UI radius compat | roundCompat |
off | Radius compatibility for third-party plugins | off |
| Adaptive text color + blue cleanup | aquaInk |
off | Text colour follows mask luminance + brand colours are unified (aquaInkColor) |
off |
| Dark-background text readability | aquaTextEnhance |
off | Two-colour text outline (an approximation) | off |
| Pure-Scene renderer selection | sceneRender |
— | ⚠ Has buttons and a write path, but no reader (not wired up) | — |
| Scene render reporting (troubleshooting) | sceneReport |
on | The wallpaper writes renderer state into reports/ every 10 s |
off |
| Scene first-frame watchdog | sceneWatchdog |
on | Falls back to the static frame when no frame arrives in time; sceneWatchdogSecs 8 s (3–30) |
off |
| Retry renderer / renderer debug params / extension hook URL | sceneDebugParams / sceneExtUrl |
empty | Allow-listed pass-through (ln/eye/audit/isolate/parallax/…); the extension hook is appended as extbase |
clear / clear all |
| One-click diagnostics report | — | — | Collects subsystem state → POST /diag; downloads a JSON file when the host is unavailable |
— |
| Diagnostics switch cheat sheet (renderer) | — | — | The 10 common renderer flags + "copy" for the URL fragment | — |
| better-sidebar compatibility | bsCompat / bsFloat / bsFont / bsReveal / bsRevealAlpha / bsAqua |
on / off / off / off / 62% / off | See above; shown only when better-sidebar is detected | master off = none apply |
| Backup & restore | — | — | Exports/imports the BACKUP_FIELDS allow-list (current wallpaper and scan folders excluded) |
— |
| Restore all defaults / feedback / check for updates | — | — | Resets appearance values; one-click update pulls the latest code from GitHub | — |
7. Liquid Glass (test) (liquid)
| Label | Key | Default | Purpose | Off / rollback |
|---|---|---|---|---|
| Test mode master switch | lgTest |
off | Keeps only wallpaper + floating + layout, disabling every appearance feature | off |
| Liquid glass (CSS) | lgCss |
off | Pure CSS/SVG refraction + specular edges (no WebGL context, can run alongside scene wallpapers) | off / URL ?lgcss=off |
| Refraction | lgCssAmount |
14px | 0–40 (0 = plain blur) | — |
| Composer / left sidebar / title bar liquid glass | lgComposer / lgSidebar / lgHeader |
off | Each selects its own target (JS applies a [data-mpw-lg-css] marker) |
off |
| Standalone demo page (port 3081) | — | — | The WebGL2 demo under tools/liquid-demo/ (a separate service; it does not affect the plugin) |
— |
Keys without a panel control
These keys exist and participate in the logic but have no widget on the settings page; they are reachable only through a backup import, by editing localStorage directly, or via URL parameters (lib/client.js:101-232 holds every default; tools/switch-wiring-test.mjs:41-64 is the "runtime only" allow-list with the reason for each entry).
| Key | Default | Notes |
|---|---|---|
clock / clock24h / clockSec / clockDate / clockPos / clockSize |
off / on / off / off / tr / 40 |
Runtime compatibility item: old configurations still render the clock, the settings page has no toggle |
bsAlpha |
off | better-sidebar panels follow the theme base colour (CSS reads it, no widget) |
bsBottomAvoid |
off | A settled deliberate no-op (alignment belongs to better-sidebar's ResizeObserver) |
newStyle |
off | Only changes settings-page widget looks; never enters buildCss |
forceEnabled |
off | Runtime priority flag for forcing the feature back on past conflict detection |
opacity |
82 | The "panel opacity" slider was removed (unified blur uses sidebarAlpha); the value is still read |
aquaTintStrength |
45 | How much of the wallpaper colour is mixed into panel tints; read at runtime |
glassColor / glassAlpha |
empty / 12 | Early WebGL liquid-glass leftovers: backup/restore only, no widget and no reader |
webInteraction |
pointer |
Web-wallpaper interaction mode (off/pointer/full); use ?mpwinteract=… or write the stored settings |
sceneRendererUrl |
http://127.0.0.1:8902/webloader/ |
Scene-renderer address, overridable (default = the one-stop :8902 bench serving /webloader/ itself; the old default was :8899, changed 2026-09-25) |
npVolume |
100 | The volume level in the Now playing card (0..100, applied to the real element); the default profile never writes element volume, only your own change does |
glassWindow |
— | Retired and deleted (2026-09-19): no widget, no reader; covered by settingsBlur + dialogBlur/popoverBlur |
Now playing and wallpaper sound
This section covers where the sidebar player mounts, what it shows and what it can control — its source is the wallpaper's own sound.
Location convention: this section refers to the implementation by symbol name (
resolveAnchor/shouldHide/occupantOf/PlayMark/markYieldinlib/now-playing.js,opsXand friends inlib/now-playing-math.js,npResolveMedia/npActiveVideo/npAudioScope/npApplyMute/applyNowPlayinginlib/client.js) — line numbers drift between versions; symbols are authoritative. The shape is "source + generated inline":lib/now-playing-math.js+lib/now-playing.jsare inlined byte-for-byte into theMPW-NP-GEN-START/ENDregion oflib/client.jsbytools/build-now-playing.mjs.
- Mount point: the host slot
sidebar.footer.action(thecreateSlotActionregistration inlib/client.js:id:"mpw-now-playing",order:60). When the slot is unavailable,resolveAnchor()falls back by mode:slot→settings-slot(before the host's settings cell) →settings-area(before[class*="settingsArea"]) →foot(first position in[class*="footArea"]); if none holds it creates no node at all and logs aconsole.warn. A late anchor still gets mounted: when no landing spot exists it watches the document and mounts itself as soon as the host's slot outlet renders (the old code only warned and returned, so after a wallpaper switch on a real machine the widget never came back). - Yielding (the counterpart of being on by default):
occupantOf(container, mode, selfNode)walks the container's children and lets three kinds through — our own nodes, host-owned nodes (the slot outlet / settings cell) and effectively empty nodes; the first remaining element counts as an occupant ⇒ we do not mount (before mounting) or we retract (after mounting, viaMutationObserverwithsubtree:true), writedata-mpw-np-yield="foreign-occupant"and log one readable warning; when the occupant leaves we come back. The criteria are two-way: neither our own nodes nor the host's cells may be misread as occupants. - Hidden when the sidebar is collapsed:
data-mpw-np-hidden+ CSSdisplay:none. The criterion is physical width first (shouldHide(width, hostCollapsed): a measured width ≥NP_COLLAPSE_MAX_W = 96forbids hiding); host signals (slotwide/data-sidebar-collapsed/ root classcollapsed) are a fallback only when the width cannot be measured, and there is a one-shot re-check after the anchor moves. When space is tight the whole widget scales by--mpw-np-fit = clamp(avail/260, 0.5, 1), whereavailmeasures our own container (not[class*="sidebarCol"]— that class name appears more than once on a real machine). - Shape: one pill that expands into a card — artwork (the current wallpaper's thumbnail), title/byline, progress rail + clock and a full-card hit target. Four keys when expanded: previous / play-pause / next / mute-unmute; three keys when collapsed — the mute key appears with the card, because the collapsed transport row is positioned as an 88px three-key row at
opsX(0) = 206and forcing a fourth key in would overflow the right padding. Expanding is a self-stopping 0→1 tween (no resident rAF); play/pause is not a swapped icon but the pair of eight-point quads, now shaped by a tween of the playback state itself (mark: 0 = paused, 1 = playing), while the morph progressponly drives size and position. - Data sources (
npResolveMedia: we only report what we actually know):
| Current wallpaper | What NP shows | What it can control |
|---|---|---|
Video wallpaper (the <video> that is really playing right now) |
Real playback state, duration and progress; when we know there is no audio track the byline says so | Play / pause / mute-unmute (no audio info ⇒ we do not guess, mute stays enabled); no track list ⇒ previous/next honestly disabled |
| Audio files that really exist in the wallpaper folder (custom folder / library / scene folder) | File name + list index; progress and duration from the media element | Our own <audio>: play / pause / previous · next step through the list in order and wrap around (with only one track both side keys are disabled) / mute |
| Web wallpaper (no separate audio file in the folder) | The byline says "web wallpaper sound" | Mute is the only channel (canPlay=false; we do not pretend we can pause in-frame WebAudio) |
| No source (static image / list not there yet / scene without separate audio) | Idle state (title = nothing playing) | No "does nothing when clicked" buttons; pressing play produces a panel note plus one console line — no fake actions |
- Two new controls (NP-4): ① a volume level — the volume bar in the card, key
npVolume(0..100), really applied to the element (the default profile never writes element volume, only your own change does); ② a draggable position (seek). Both are governed by the switch below. - "Play/pause also controls the wallpaper" (
npLinkWallpaper, default on): on = the transport and the progress bar drive the wallpaper's own playback/position too (default on = byte-identical to the previous behaviour); off = only this plugin's own player is driven and the wallpaper media is not touched at all; when the wallpaper media is the only sound source the controls are honestly disabled and the byline says why (np.note.linkOff). - What it cannot do (listed explicitly, no fake actions;
docs/NOW-PLAYING-DSH.md§7.7.7 and §7.8): there is no system media source (that is whatlib/media-session.jsis for, and it is not wired up yet); a web wallpaper's in-frame sound supports mute only — its own play/pause is out of reach; a video wallpaper has no previous/next; the mute key only appears in the expanded state (expand the card first to unmute — a geometry trade-off, not a broken key); the heart button is not rendered; no waveform, no keyboard shortcuts; audio inside a package (scene.pkg) can be listed but not played (np.note.pkgListOnly); the progress bar is draggable, but honestly not draggable when the link switch is off or nothing is seekable (np.note.noseek). The double-audio combination where "the wallpaper also plays the same track" has no on-device sample to verify against (the mitigation in place: the frame is force-muted while our player is playing). - Rollback: turn
npNowPlayingoff (or "Restore all defaults") ⇒ back to zero injection (no DOM, no observers, not a single NP rule in the output). Regression:node tools/now-playing-test.mjs(83 passed / 0 failed, including 7 mutations) +node tools/np-media-test.mjs(82 passed / 0 failed, including 12 mutation self-proofs;--no-mutationsleaves 70 main assertions) — both registered in step 2 oftools/check.sh. On-device probes (need:3080+ headless Firefox, not part of the standing gate):node tools/np-sidebar-live-probe.mjs(12 criteria) andnode tools/np-media-live-probe.mjs(16 PASS / 22 FAIL before the fix → 45 PASS / 0 FAIL after). - Attribution: the component is a line-by-line port of Bencho's "Now playing" (MIT) with the upstream comments kept verbatim; the sidebar mount controller, yield criteria, self-drawn icons, token mapping, data wiring and gates are written here. Registered in
THIRD-PARTY.md§6.
System media session (MPRIS / SMTC)
This section covers the host half of "show what the system is playing": the capability matrix, the honest unavailable path, and the current wiring status.
lib/media-session.js (1052 lines, MIT, written in this repository, no third-party code) is the host half of the "let Now playing show what the system is playing" chain. Contract: createMediaSession({run, platform, env, now, timeoutMs, log}) → {probe(), snapshot(), control(op,arg), stats(), lastProbe()} (lib/media-session.js:22-40).
Capability matrix
| Platform | Channel | Adapters (by priority) | Metadata / state / position / artwork | Control |
|---|---|---|---|---|
| Linux / FreeBSD / OpenBSD | MPRIS over D-Bus (org.mpris.MediaPlayer2.*) |
playerctl → dbus-send |
One call returns 7 fields: status / mpris:length / position / xesam:title / xesam:artist / xesam:album / mpris:artUrl |
play pause playpause next prev seek |
| Windows 10/11 | SMTC (GlobalSystemMediaTransportControlsSessionManager) |
smtc (powershell.exe -NoProfile -NonInteractive) |
Real WinRT calls for properties / timeline / playback controls / thumbnail (as a base64 data URL) | Same ops (op and position passed as separate argv elements) |
| macOS | — | — | ❌ Not implemented: unsupported-platform, 0 commands |
❌ |
| Anything else / no adapter | — | none |
❌ not-installed |
❌ |
- Shape: the
snapshotalways has its 21 keys; unreadable values are empty/neutral (duration/positionare milliseconds andnullwhen unknown — it never invents 0); whenavailable:true,titleis non-empty (lib/media-session.js:42-56,:220-244). - With no desktop session bus it reports unavailable honestly:
probe()really probes (dbus-send --session ListNames, falling back tobusctl --user list) and returnsavailable:false / reason:'no-session-bus'with adetailline such as "总线不可达(via dbus-send):…" (lib/media-session.js:684-706,:775-783). This is derived, not hard-coded — a machine with no desktop environment (container/Termux) really takes this path. - It never throws:
snapshot()/control()never reject; every failure is a return value whosereasonis one of 15 values (unsupported-platform / disabled-by-env / not-installed / no-session-bus / no-player / no-metadata / empty-output / unparsable / timeout / not-available / bad-op / bad-arg / bad-player / error). - Security: command names and arguments are passed separately (argv arrays, never through
sh -c); player names must match/^[A-Za-z0-9_.-]{1,64}$/or the result isbad-playerwith 0 commands;opis allow-listed andseekis bounded to0..24h; the timeout defaults to 800 ms (50–5000 ms); reads are single-flight with a global serial queue (at most one command at a time). - Zero dependencies: only
node:child_process;package.jsongained no dependency at all. - Environment variables:
MPW_MEDIA_ADAPTER(pin an adapter),MPW_MEDIA_PLAYER(pin a player name),MPW_MEDIA_TIMEOUT_MS.
⚠ Wiring status (honest): the module is not wired into the plugin yet — lib/index.js, lib/client.js and tools/build-bundle.mjs do not import it, and there is no /media-session or /media-control host route, so the Now playing widget still shows the wallpaper's own media, not a system player. Host routes and UI display are planned in docs/MEDIA-SESSION.md §9/§10 but marked as not done. Gate: node tools/media-session-test.mjs (95 assertions = 89 main + 6 mutation self-proofs), not part of tools/check.sh.
Diagnostics and troubleshooting
Start here when something is wrong: one click sends state back to the local host, and individual diagnostics switches are only for narrowing things down.
Plugin side (this plugin's own switches)
- One-click diagnostics report (Other tab): packs whether frost/sidebar/timeline were affected, the wallpaper type and path, whether the shim was injected, video decoding, surface tokens and scene health into one payload →
POST /api/mpkg-wallpaper/diag, written to~/.dsh/.dsh-mpkg-wallpaper/diag-<epochms>.json(lib/index.js:1605-1620; directory capped at 50 files / 32MB,lib/index.js:1523-1524; client-side single-payload cap 512KB). When the host is unavailable it automatically downloadsmpw-diag-<ISO>.jsoninstead, so state can be reported offline. Every field carriesprovenance; unreadable ones arevalue:null + degraded(docs/DIAGNOSTICS.md). - Automatic background reporting: the switch is
localStorage['mpwdiag']='1'(not a URL parameter — DSH's auth 303 strips the query,lib/client.js:4953-4957); after a reload it reports once 6 seconds in (≤6 per session), andwindow.onerroruses the same collector. - Plugin-side URL escape hatches (effective after a refresh, settings untouched):
| Parameter | Effect |
|---|---|
?hdrfrost=legacy|off |
Title-bar frost: old gate / fully off |
?hdrblur=pseudo|element |
Frost carrier: pseudo-element / element layer, for A/B |
?railink=off |
Disables the contrast compensation on the right-side timeline rail |
?sbfill=wide |
Restores the old global sidebar fill override |
?lgcss=off |
One-flag rollback of liquid glass |
?mpwtranscode=legacy|aggressive |
Video transcoding: old behaviour / probe even past the user's caps |
?mpwinteract=1|on (auto-arm) / off|0|pointer|full (mode only, no auto-arm) |
Web-wallpaper interaction |
?mpwstore=0|mem |
Disable the in-frame storage facade / keep it in memory without persisting |
?mpwpersist=legacy |
Settings persistence back to the old behaviour |
?bgwrapfix=legacy, ?hdrfrostwatch=off |
Wallpaper-layer visibility / frost watchdog |
?hiddengate=legacy |
Whole hidden-gate family back to the old behaviour (no more blocking our autoplay/retries while the page is hidden, frozen or background-reloaded; for A/B and emergencies. Default = never start playback while hidden, stop every sound surface we own when hidden, resume per policy on return; see docs/WALLPAPER-LIFECYCLE.md §6.4) |
Renderer-side diagnostics flags (pure Scene wallpapers)
Two different numbers — do not mix them up:
- The main table holds 158 flags, in the renderer repository,
we-scene-demo/docs/README-DIAGNOSTICS.md(generated bydiag-flag-check.mjs, cross-checked againstweb/diag-flags.json). - The plugin panel only ships the common / escape-hatch table (the 10 flags with
common:true), appended to the renderer URL, e.g.http://127.0.0.1:8902/webloader/?id=3719111841&audit=3:
| Flag | Purpose | Default |
|---|---|---|
att=legacy |
Attachment anchoring through the old self-computed path (A/B) | new path |
mcc=1 |
Force mesh bounding-box centre compensation | off |
piv=1 / piv=0 |
Sub-mesh pivot compensation all on / all off | eyes combo only |
align=0 |
Reproduce the old alignment (origin always the geometric centre) | official alignment |
parallax=legacy |
Mouse parallax back to the old approximation | official formula |
audio=1 |
Play the scene's sound layers (one stream each) | muted |
whitefallback=0 |
Missing-texture layers become transparent | white block (official) |
hier=0 |
Drop parent-chain composition, fall back to absolute positioning | parent-chain composition |
isolate=<layer> |
Keep only layers whose name matches (comma separated) visible | all visible |
audit=N |
Per-layer audit of the first N frames | 1 |
These are renderer URL parameters, not plugin settings; the plugin only appends them to the scene iframe (
lib/client.js:13981-14000,lib/client.js:3163). The panel copy and its offline mirror must match thecommonset ofdiag-flags.json, asserted bytools/panel-smoke.mjs(tools/panel-smoke.mjs:316-372).
Compatibility, limits and what it cannot do
This section gathers what environment and what sizes work, and what is out of reach. Per-type boundaries live in the previous section; the web-wallpaper and Now playing cannot-do lists live in their own sections.
Browser compatibility (measured reference)
| Browser | Rating | Notes |
|---|---|---|
| Chrome / Chromium (desktop) | ⭐⭐⭐ strong | Most complete: best backdrop-filter and color-mix, working iframe.muted, muted autoplay allowed |
| Edge (desktop) | ⭐⭐⭐ strong | Video wallpapers use a separate canvas path (dodging Edge's floating toolbar, lib/client.js:1464-1520); some versions show a static first frame only |
| Firefox | ⭐⭐ medium | Everything works (backdrop-filter 103+, transcode fallback for unsupported codecs); three deductions — see the note below |
| Android WebView / mobile | ⭐⭐ weak-medium | Autoplay depends on the host WebView config; getBattery may be missing (guarded); for extreme combinations prefer a static image/GIF or turn blur off |
Firefox's three deductions: ①
backdrop-filteris slower than Chromium (several blurs at once drop frames on low-end machines); ②iframe.mutedis unsupported ⇒ a web wallpaper with sound may have its first frame blocked by the autoplay policy; ③color-mixneeds 113+ (older versions fall back visually). The source degrades for each browser (rAF fallback whenrequestVideoFrameCallbackis missing, guards aroundResizeObserver/getBattery,.catchon everyplay(),backdrop-filterdetected withCSS.supportsand degraded to opaque).
Without a Wallpaper Engine install (missing / non-Windows)
A "WE install" means the Steam edition of Wallpaper Engine (appid 431960). The host's locateWallpaperEngine() (lib/index.js:311-399) searches in this order: Windows registry HKCU\Software\Valve\Steam\SteamPath → common Steam directories → non-Windows Steam directories (macOS ~/Library/Application Support/Steam, Linux/Android ~/.local/share/Steam, WSL /mnt/c/... plus a per-drive enumeration of /mnt/d…/mnt/z) → every library's steamapps/libraryfolders.vdf containing 431960 → and only accepts the one where <library>/steamapps/common/wallpaper_engine/wallpaper32.exe exists. If nothing is found it returns null and everything falls back:
| Scenario | Real behaviour (with code location) |
|---|---|
WE not installed (or wallpaper32.exe missing) |
steam-inventory returns 200 {ok:true, installDir:null, wallpapers:[]} — not an error, never a 500 (lib/index.js:3120-3180) |
| Clicking "Scan local library" | Empty list plus the "install not found" banner (lib/client.js:8781/:11555) and the empty-list hint. The scan itself does not fail |
| WE installed but no asset folders exist | Each root is checked individually ⇒ empty inventory, no banner; only the empty-list hint shows. Not implemented: no dedicated message |
| Non-Windows / mobile | Same as "WE not installed" (the registry branch returns null off win32); no side effects |
| WE playlists | Only when installDir exists and config.json parses; otherwise rotSeeded:true stops re-seeding (lib/client.js:8782-8793) |
| Still available without WE | Manual folders, direct import, URL/local files, scene extraction and diagnostics all work without |
…
Links
More in this category
zhu1090093659/dsh-web#packages/dsh-task-board★ 8262
Task board for the dsh web GUI: a sidebar multi-column kanban whose cards run in real DSH agent sessions and can also be scheduled with cron expressions, executed host-side even with the browser closed.
zhu1090093659/dsh-web#packages/dsh-web-all★ 8262
Plugin and skin collection for the DSH Web UI: task board, Git graph, right-side panel, remote mobile UI, pet, live token stats, and a skin center.
omdsh-dev/DSH-better-sidebar★ 3943
Full sidebar workbench with file rendering and editing, terminal, Git, and subagents; third-party plugins can register new tabs.
ccch1mneyyy/dsh-TUI★ 3913
Claude Code-style full-screen terminal UI: pixel-whale header, live status line, and streaming thought expansion.
MeteorNOX/DeepSeek-Balance-Whale-Widget★ 3793
A fixed-corner whale widget for the DSH web GUI — balance, today's usage and per-turn cost with peak/off-peak pricing, editable balance-alert and daily-budget bubbles, a module-based custom bubble queue with A/B weighted choices and random lines or images, 30+ vendor templates (OpenAI, OpenRouter, Kimi, SiliconFlow, Ark, Zhipu, MiniMax and more) with per-model balance and subscription quota, plus task-end sound, imported audio, custom roles and a resource manager. Local-only, no telemetry.
Devin-AXIS/deepseek-design#deepseek-idesign★ 1414
Visual design studio for websites, app prototypes, posters, cards, reports, and magazines, with templates, direct element editing, selection-aware AI draft handoff, and export.
Community comments
Comments are public GitHub Discussions. Loading them connects to GitHub and Giscus; a GitHub account is required to post.