dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
🫧 Frutiger Aero skin for DeepSeek Harness (dsh): glass over a living sky-and-water wallpaper, aqua gloss, light + dark. Rebuilds the phone layout too — drawer, dock, edge swipes, keyboard-aware composer. 玻璃质感皮肤,电脑端与手机端都适配。
English | 中文
Frutiger Aero for the DeepSeek Harness browser UI — glass, water, sky and bubbles, on desktop and on a phone that finally fits a thumb.
A portable dsh profile bundle. It reskins the whole Web surface through the product's own token
system, and it rebuilds the narrow-viewport layout so a phone stops being a squeezed desktop and
becomes an app.
requires nothing at all. Install is a file copy;
there is no build step and no network access at runtime.
One line. No package manager, no registry account, no build step.
macOS / Linux:
curl -fsSL https://raw.githubusercontent.com/Hotsteel2901/dsh-frutiger-aero/main/install.sh | sh
Windows (PowerShell):
irm https://raw.githubusercontent.com/Hotsteel2901/dsh-frutiger-aero/main/install.ps1 | iex
Both download a snapshot of this repository, place it in the target profile's node_modules, and
add it to that profile's bundle list. They need nothing but Node 24 or newer — no package
manager, no registry, no build. Pass a profile name to install somewhere other than frutiger:
curl -fsSL .../install.sh | sh -s -- --profile aero
Why Node 24, and not the 20 this used to ask for. The Harness CLI dispatches through
if (import.meta.main), and that property is unimplemented before Node 24. On 20 and 22 the guard is simply falsy:dshprints nothing and exits0. That is indistinguishable from a broken install, and it is the single most common reason someone concluded this one was. The installers now refuse to run on an older Node and say so in one sentence instead of leaving you with a CLI that silently does nothing.
The installers default to the default branch, not to releases/latest. An earlier version did
the opposite, and it was the source of a second, subtler problem: a release tag is cut at a moment
in time while package.json keeps reporting the branch's version, so tag 1.0.3 and main both
announced version: 1.1.0 while holding different code. Reinstalling therefore downloaded the same
snapshot forever, and there was no way to tell. Pin a specific ref when you want one:
curl -fsSL .../install.sh | sh -s -- --ref main
Both installers fetch that ref fresh on every run, so re-running the one-liner is how you move to the newest version — nothing is cached, and there is no version number to bump by hand. Every install prints the version and build fingerprint it wrote, which is the answer to "am I on the latest?".
git clone https://github.com/Hotsteel2901/dsh-frutiger-aero
cd dsh-frutiger-aero
node install.mjs # installs into `--profile frutiger`
node install.mjs --link # …or link the checkout, so a rebuild is live
Then:
dsh --profile frutiger --port 3099 --no-open
…and open the URL that command prints — it carries a one-time token, so the browser gets an authenticated session cookie.
| flag | effect |
|---|---|
--profile <name> | profile to create or update (default frutiger) |
--home <dir> | Harness home to operate on (default $DSH_HOME, then ~/.dsh) |
--link | symlink the checkout instead of copying — for hacking on the plugin |
--print | resolve everything, write nothing |
--uninstall | remove the package and its bundle entry, keeping the profile and its sessions |
--doctor | inspect an existing install and print every problem with a fix for each — read-only |
--repair | re-copy the payload over an existing install, keeping the profile and its sessions |
--json | machine-readable result |
The two installers accept the same settings through the environment, which is the only option
available under irm … | iex — Invoke-Expression has no -Args, so there is nowhere to put a
flag on that command line. install.sh --help prints the values a run would actually use.
| variable | effect |
|---|---|
DSH_FRUTIGER_REPO | owner/repo to fetch from (default Hotsteel2901/dsh-frutiger-aero) |
DSH_FRUTIGER_REF | tag or commit to pin (default: the default branch, resolved at install time) |
DSH_FRUTIGER_PROFILE | profile to create or update (default frutiger) |
DSH_FRUTIGER_HOME | Harness home to operate on (default $DSH_HOME, then ~/.dsh) |
$env:DSH_FRUTIGER_PROFILE = 'aero'
irm https://raw.githubusercontent.com/Hotsteel2901/dsh-frutiger-aero/main/install.ps1 | iex
The landing page picks the right one of these commands for your system, so there is normally no reason to type it by hand.
Do not delete the profile and start over — that is both destructive and, when the real cause was an unstartable Node, guaranteed not to help. Ask instead:
node install.mjs --profile frutiger --doctor
It reports what is actually wrong, why, and the one command that fixes it, and it exits non-zero only when there is something to act on. Every install prints the build fingerprint it wrote, so "am I on the latest?" has an answer that does not depend on trusting the version string:
dsh-frutiger-aero: installed profile "frutiger" (copy)
version 1.1.0
build 20bfbdb4cf57
The fingerprint is a content hash of the sources the artifact was built from, and the build is
reproducible, so it identifies a build exactly — two builds that differ in one byte of CSS have
different fingerprints. That is what makes --doctor able to tell you your copy is stale rather
than guessing from a version number that two different snapshots happen to share.
Per browser, without touching the install. The plugin publishes a small control surface:
window.__FRUTIGER__.setEffects('lite') // 'full' | 'lite' | 'off'
window.__FRUTIGER__.setScene(false) // wallpaper only
window.__FRUTIGER__.tier() // what is active right now
window.__FRUTIGER__.bubbles() // { step, steps, count }
window.__FRUTIGER__.bubbles('calm') // 'calm' | 'normal' | 'lively'
Wallpaper density. The tier picks a sensible default number of bubbles for your device, and
bubbles() lets you disagree with it without turning the wallpaper off:
| step | effect | on a full tier | on a lite tier |
|---|---|---|---|
calm | about half as many | 10 | 5 |
normal | the tier default | 22 | 10 |
lively | nearly twice as many | 40 | 18 |
The multipliers apply to the tier's baseline rather than replacing it, so a lite device stays
lite at every step. The count is clamped to 3–48 either way. Changing it rebuilds the scene but
not the composition — bubble placement comes from a fixed seed, so the wallpaper you chose is
the one you keep. The preference is stored per browser, survives a reload, and a sticky-hands
click on normal clears the stored value rather than pinning today's default.
…or straight from the URL:
http://127.0.0.1:3099/?frutiger=off # no decoration at all
http://127.0.0.1:3099/?frutiger=lite # palette and layout, no blur, 10 bubbles
http://127.0.0.1:3099/?frutiger=full # force the top tier and skip the governor
http://127.0.0.1:3099/?bubbles=calm # a quieter wallpaper, stored setting untouched
The URL form is read on every load after the token exchange, so bookmark the clean URL rather
than the one dsh prints. A query parameter always beats the stored preference, which makes
?bubbles= the right tool for a screenshot or a bug report that needs an exact count.
Disable the row, keep the package. In <profile>/cordis.patch.yml:
- id: frutiger-aero
disabled: true
The profile reloads its patch file live, so this takes effect without a restart.
Remove it. node install.mjs --uninstall, or run install.sh --uninstall on a copy of the
one-liner. The profile and its sessions are left alone.
The stock narrow layout squeezes: below 1024px the sidebar keeps its grid track, so a 390px phone gives 56px to an icon rail and 334px to the thing being read — and once the drawer opens, the transcript is 108px wide. This plugin changes the layout itself, not just the paint.
| ≥ 1024px | 641–1023px | ≤ 640px | |
|---|---|---|---|
| layout | stock three columns | one column, sidebar overlays | one column, rail off-canvas |
| navigation | sidebar | 56px rail, drawer over content | floating bottom dock |
| reading measure | product default | full column, capped at 680px | full column |
On top of that:
visualViewport publishes the occluded height as a custom
property, so the composer lifts by exactly that much on iOS, where the layout viewport never
resizes.touch-action: manipulation, momentum scrolling, contained overscroll, no tap highlight,
and light haptics on press.viewport-fit=cover plus env(safe-area-inset-*) on the drawer, the dock and
the composer, and 100dvh instead of 100vh.Two product-level defects are fixed rather than worked around, because a reskin that leaves them is a reskin of a broken screen:
clamp(680px, …) — a 680px floor,
wider than a phone, which clips every turn into a column of left-edge slivers.A full-screen right panel (a file preview, a diff) keeps the dock — session navigation is still useful while reading — but the panel stops above it, so the last lines of a file are never stranded behind a floating bar.
Everything. The skin is additive:
interact.mjs asserts this on every run — see Verification.
The bundle patch inserts a single row whose host half is an empty apply(). The row exists for
what its manifest declares: dsh.client, which makes @deepseek-ai/dsh-client-modules compose
lib/client.js into window.__DSH_BOOT__ as a browser plugin. Nothing is added to the host — no
service, no tool, no config, no session state.
The browser half paints, and only paints. Every hook it installs is a stylesheet, a node it owns,
a passive listener, or an attribute on a node the product owns, and each one is released through
ctx.effect. Setting disabled: true on the row returns the page to stock.
The product's colour system is two tiers — --dsw-static-* raw ramps and --dsw-alias-*
semantic roles — and every surface, border and label resolves through the semantic tier.
Overriding that tier retints the entire application without a single component-level rule, and
because those alias values are what ui-layout writes onto <body>, it is also the only layer
that composes with the built-in light/dark switch instead of fighting it.
The palette is written twice on purpose:
!important stylesheet keyed on body[data-ds-dark-theme] — the only thing in the
cascade that outranks an inline custom property, so the skin can never lose a specificity race
to a load-order accident;theme.overrideTokens(), so the runtime snapshot, the theme-color metadata and the
Appearance preview cubes stay coherent with what is painted.src/client/palette.js is the single source of truth for both. The raw ramp is shared; only the
semantic aliases differ per scheme, because Aero is a light material (glass over a bright sky)
and its night counterpart is a dark one (bioluminescence over deep water).
CSS-module class names are hashed per build (.pI_x6G_sidebarCol), so they are worthless as
selectors across an upgrade. What the product does keep stable is the semantic data-*
vocabulary its own styles and tests depend on. The skin is written against that —
[data-rightbar-col], [data-shell-overlay], [data-conversation-scroll],
[data-composer-card], [data-chat-flow-kind], [data-files-row] — plus ARIA roles and element
semantics.
A small rAF-coalesced tagger adds the rest: it finds the frame as the parent of
[data-rightbar-col], labels each column data-fa-col, and finds the conversation canvas as the
first ancestor of the transcript that actually paints a background. It watches childList and
three presentation attributes, never the message list, and never its own writes.
Nine layers in one fixed element with contain: strict, so it cannot participate in layout,
cannot be scrolled, and never appears in a hit test. Two rules keep it cheap:
transform and opacity are animated — the two properties a compositor can animate
without waking the main thread;backdrop-filter. The scene is built from large feathered gradients
— already the look a blur would produce — so no filter is ever recomputed as the bubbles move.There is no rAF loop anywhere in the plugin, and no will-change hint: 22 promoted layers cost
more than the wallpaper is worth, and an animated transform is promoted by the engine anyway.
Adding animation to an application you do not own is only responsible if the cost is bounded, so the plugin runs a tier governor.
| tier | chosen when | wallpaper | blur | animations |
|---|---|---|---|---|
full | fine pointer, ≥ 3 cores, ≥ 4 GB | 22 bubbles, caustics, light shafts, grain | yes | all |
lite | coarse pointer or small screen, ≤ 4 cores, ≤ 3 GB, save-data, reduced motion | 10 bubbles | no | entrances only |
off | explicit ?frutiger=off | none | no | none |
Classification reads only signals that are free (pointer, hardwareConcurrency,
deviceMemory, saveData, prefers-reduced-motion), and the plugin then samples frame pacing
for about a second after boot and downgrades once if the samples disagree — the governor only
ever moves down, so a device that recovers cannot oscillate between rich and plain. An explicit
request disables the governor entirely: a user who asked for full asked for full.
The one genuinely expensive property in the design is backdrop-filter, and it is gated twice:
off below full, and applied only to panes smaller than the viewport. That second gate is
load-bearing — an early build applied it to the right column, which is a zero-width track on
desktop but spans the whole screen once the frame collapses to a single column, so on a phone it
blurred and desaturated the entire application.
prefers-reduced-motion is honoured as no movement, not no theme: the wallpaper stays and
freezes. A hidden tab pauses every animation.
dsh --profile web, or any profile built on
@deepseek-ai/dsh-base + @deepseek-ai/dsh-web-app). Desktop clients that embed the dsh Web
UI get the same skin, because the plugin targets the product's tokens and hooks rather than one
client's DOM.backdrop-filter, :has() and dvh
all degrade rather than break — the lite tier exists precisely so a device that cannot afford
the glass still gets the palette and the layout.--dsw-* tokens is retinted along with the rest of the
app. A plugin that hardcodes its own colours, or paints a full-viewport backdrop-filter, can
still clash — that is a real limitation, not a hypothetical one.The host half is an empty apply() — no service, no tool, no session state. The browser half
paints and nothing else. Every side effect is released through ctx.effect, so disabling the row
returns the page to stock.
node install.mjs --uninstall, or - id: frutiger-aero + disabled: true in the profile's
cordis.patch.yml. Both leave the profile, its sessions and its settings in place.
Because the stock narrow layout is broken in a way that colour cannot fix: 108px of transcript on a 390px phone. A skin that leaves that alone is a skin of a broken screen.
Read the tier table above. The short version: at full you get a blurred composer, a blurred
dialog and a 22-bubble wallpaper, all of it compositor-only; at lite you get the palette and
the layout with no filters at all. Nothing animates off screen or in a background tab.
Not yet through a settings UI. src/client/palette.js is the single source of truth and
node build.mjs regenerates both the stylesheet and the theme-service override from it. A
settings row is a reasonable next step — see CHANGELOG.md.
.devtools/ drives the real page with Playwright. It is not part of the deliverable and not
needed to install anything, but it is how every claim in this README was checked.
| script | what it answers |
|---|---|
interact.mjs | real input — CDP touch swipes, taps, typing, wheel, drag, selection: 29 assertions per run, in both shipped locales |
docktest.mjs | the phone dock in Chinese and English: 24 assertions per run |
trajcheck.mjs | the Trajectory view on a phone and on the desktop: 33 assertions |
installcheck.mjs | the install path without a browser — Node floor, idempotency, --doctor, --repair, reproducibility: 15 checks |
settingscheck.mjs | the settings dialog from the dock, both locales, phone and desktop: 23 assertions |
aligndiff.mjs | off-centre controls introduced by the skin, measured against a stock profile |
final.mjs | every viewport: layout, drawer, dock, computed styles, console errors, screenshots |
tiers.mjs | the same page at full / lite / off, per viewport |
lum.mjs | rendered-contrast measurement, for finding things that only exist in pixels |
landing.mjs | the GitHub Pages landing page: 19 checks across desktop, mobile and reduced motion |
hit.mjs | what a tap at each point of the viewport actually reaches |
The suite runs against desktop, laptop, tablet, portrait phone, small phone and landscape phone, in both colour schemes, at every tier, and in both languages the client ships. Current state: 29/29 interaction checks (×2 locales), 24/24 dock checks, 11/11 Trajectory checks and 19/19 landing checks, with a clean console throughout.