โ† Back to home@Hotsteel2901

dsh-frutiger-aero

๐Ÿซง 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. ็Žป็’ƒ่ดจๆ„Ÿ็šฎ่‚ค๏ผŒ็”ต่„‘็ซฏไธŽๆ‰‹ๆœบ็ซฏ้ƒฝ้€‚้…ใ€‚

Stars
1
Language
JavaScript
Created
Sep 19, 2026
Updated
Sep 19, 2026

Introduction


description: "Frutiger Aero profile bundle for the DeepSeek Harness browser surface: a glass/aqua reskin built on the product's own token graph, plus a mobile layout that replaces the squeezed desktop with a drawer, a thumb-reachable dock and a keyboard-aware composer." kind: "plugin"

dsh-frutiger-aero

English | ไธญๆ–‡

npm version npm downloads license dsh-plugin DeepSeek Harness landing page

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.

  • ๐Ÿ–ฅ๏ธ Desktop โ€” the three-column layout, drag-to-resize handles, dialogs, menus, text selection, wheel scrolling and keyboard input all still work. The skin adds material and changes no behaviour.
  • ๐Ÿ“ฑ Mobile โ€” an overlay drawer instead of a crushed conversation, a floating glass dock under the thumb, edge-swipe gestures, a keyboard-aware composer, 44px touch targets and safe-area insets throughout.
  • ๐ŸŽจ Frutiger Aero โ€” frosted panes over a living wallpaper, aqua gloss on everything you can press, a bright rim on every edge, and a dark scheme that is deep water rather than grey.
  • ๐ŸŒ— Light and dark โ€” one palette per scheme, composing with the built-in appearance switch instead of fighting it.
  • ๐Ÿ“ฆ Zero dependencies โ€” the browser half requires nothing at all. Install is a file copy; there is no build step and no network access at runtime.

The dsh desktop layout with the Frutiger Aero skin

The dsh chat on a phone: frosted glass composer, glass to-do bar and a floating glass dock over a sky-and-water wallpaper The same phone view in the dark scheme: deep water, bioluminescent bubbles and a cyan-rimmed composer The navigation drawer open over the conversation with a dimming scrim behind it


Contents


Install

Three ways in. Pick whichever matches what you already have.

1 ยท npm one-liner (recommended)

dsh plugin --profile web add dsh-frutiger-aero

Installs the latest published version and registers it as a profile bundle layer. Reload the web UI and it is on. Works on every platform, and dsh plugin --profile web remove dsh-frutiger-aero takes it back off.

2 ยท GitHub installer (no npm account, no git)

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 the latest release, place it in the target profile's node_modules, and add it to that profile's bundle list. They need nothing but Node โ€” 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

3 ยท From a clone (for development)

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.

Installer options

flageffect
--profile <name>profile to create or update (default frutiger)
--home <dir>Harness home to operate on (default $DSH_HOME, then ~/.dsh)
--linksymlink the checkout instead of copying โ€” for hacking on the plugin
--printresolve everything, write nothing
--uninstallremove the package and its bundle entry, keeping the profile and its sessions
--jsonmachine-readable result

Turning it off, or down

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

โ€ฆ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

The URL form is read on every load after the token exchange, so bookmark the clean URL rather than the one dsh prints.

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 dsh plugin --profile web remove dsh-frutiger-aero. The profile and its sessions are left alone.


What changes on a phone

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.

โ‰ฅ 1024px641โ€“1023pxโ‰ค 640px
layoutstock three columnsone column, sidebar overlaysone column, rail off-canvas
navigationsidebar56px rail, drawer over contentfloating bottom dock
reading measureproduct defaultfull column, capped at 680pxfull column

On top of that:

  • A drawer that slides over the content, with a scrim that dismisses it and takes the tap that would otherwise have scrolled the page behind it.
  • Edge-swipe gestures โ€” swipe from the left edge to open, swipe left to close. Vertical intent hands the gesture straight back to the scroller.
  • A bottom dock whose buttons click the product's own controls by accessible name rather than reimplementing anything. A button whose control is absent simply does not render.
  • A keyboard-aware composer โ€” 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 ergonomics โ€” 44px targets on coarse pointers (with a 36px floor for dense toolbars, where forcing 44px would reflow the strip), a 16px input floor that stops iOS's sticky focus zoom, touch-action: manipulation, momentum scrolling, contained overscroll, no tap highlight, and light haptics on press.
  • Safe areas โ€” 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:

  1. the 56px rail that crushes the transcript; and
  2. a conversation that declares its reading measure as 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.


What stays the same on a desktop

Everything. The skin is additive:

  • three-column layout, column drag handles, sidebar collapse/expand;
  • dialogs, menus, popovers, tooltips and toasts (which get the glass treatment, not new behaviour);
  • text selection, wheel scrolling, keyboard navigation and focus rings;
  • the product's own light/dark switch, its font-size setting and every other preference.

interact.mjs asserts this on every run โ€” see Verification.

The dsh desktop layout in the dark scheme

Settings dialog and the mobile file preview

The settings dialog as a large frosted pane over a dimmed, blurred app A full-screen file preview on a phone, stopping above the floating dock


How it works

It is a plugin, and it behaves like one

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.

Colour: the token graph, not the components

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:

  • as an !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;
  • through 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).

Structure: the product's own vocabulary

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.

The wallpaper

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:

  1. only transform and opacity are animated โ€” the two properties a compositor can animate without waking the main thread;
  2. nothing animates behind a 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.


Performance

Adding animation to an application you do not own is only responsible if the cost is bounded, so the plugin runs a tier governor.

tierchosen whenwallpaperbluranimations
fullfine pointer, โ‰ฅ 3 cores, โ‰ฅ 4 GB22 bubbles, caustics, light shafts, grainyesall
litecoarse pointer or small screen, โ‰ค 4 cores, โ‰ค 3 GB, save-data, reduced motion10 bubblesnoentrances only
offexplicit ?frutiger=offnonenonone

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.


Compatibility

  • Surface: the dsh Web UI (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.
  • Node: whatever the dsh installation itself requires. The plugin adds no engine requirement.
  • Browsers: any current Chromium, Firefox or WebKit. 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.
  • Other plugins: anything built on the --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.

FAQ

Does it break the app, or leak outside the theme?

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.

How do I get the stock UI back exactly?

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.

Why does it touch the layout on mobile instead of only the colours?

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.

Will it slow my machine down?

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.

Can I change the palette without forking?

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.


Verification

.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.

scriptwhat it answers
interact.mjsreal input โ€” CDP touch swipes, taps, typing, wheel, drag, selection: 29 assertions per run, in both shipped locales
docktest.mjsthe phone dock in Chinese and English: 24 assertions per run
trajcheck.mjsthe Trajectory view on a phone and on the desktop: 11 assertions
settingscheck.mjsthe settings dialog from the dock, both locales, phone and desktop: 23 assertions
aligndiff.mjsoff-centre controls introduced by the skin, measured against a stock profile
final.mjsevery viewport: layout, drawer, dock, computed styles, console errors, screenshots
tiers.mjsthe same page at full / lite / off, per viewport
lum.mjsrendered-contrast measurement, for finding things that only exist in pixels
landing.mjsthe GitHub Pages landing page: 19 checks across desktop, mobile and reduced motion
hit.mjswhat 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.


Repository layout

install.mjs                  portable profile installer / uninstaller
install.sh, install.ps1      GitHub one-liners (no npm account needed)
packages/frutiger-aero/      the plugin โ€” this is what gets published to npm
  package.json               dsh.bundle + dsh.client declarations
  cordis.patch.yml           the one row this bundle inserts
  build.mjs                  inlines src/css/*.css into lib/client.js
  src/host.js                node half (deliberately empty)
  src/client/palette.js      the whole colour system, one source of truth
  src/client/scenery.js      the wallpaper's DOM, seeded and deterministic
  src/client/runtime.js      tiering, tagging, mobile layer, dock, control surface
  src/css/*.css              base, scenery, material, mobile, effects
  lib/                       built artifacts (committed โ€” installing needs no build)
docs/                        the GitHub Pages landing page
submission/                  ready-to-PR registry entry
.devtools/                   the verification harness

Rebuild the browser half after editing anything under src/:

node packages/frutiger-aero/build.mjs

Contributing

Issues and pull requests are welcome โ€” see CONTRIBUTING.md. The short version: read How it works first, keep the two rules (paint only, and release every side effect through ctx.effect), and run interact.mjs before opening a PR that touches the mobile layer.

License

MIT โ€” see LICENSE.

An unofficial community plugin. Not affiliated with or endorsed by DeepSeek.