UI harmonizer for DeepSeek Harness: normalizes unfinished or self-contradictory official UI, reconciles style conflicts between installed plugins, and unifies the visual language via official design tokens — with live-customizable chat width, markdown font size, workspace scale, UI font stack and a rounded center-column card from Settings → General; client-only dsh bundle, zero model cost, no host side, removable without residue.
Install
# from npm (prebuilt)
dsh plugin --profile web add dsh-ui-harmonizer
# from GitHub (first run asks for allowBuilds approval — follow the hint, retry)
dsh plugin --profile web add github:Physicolor/dsh-ui-harmonizer
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
description: "One interface language for the Harness, plus per-plugin reconciliation and a read-only compatibility audit."
TL;DR: You installed a bunch of DSH plugins but the UI looks inconsistent? DSH UI Harmonizer uses CSS overrides + runtime DOM coordination to bring them back to the official design language — non-destructive, fully reversible, zero model cost.
DSH UI Harmonizer is a client-only DSH bundle plugin. It adds no model tools and modifies no session logs — it adjusts the UI purely through official slots (settings.section / settings.general.item) and the --dsw-* semantic token system.
Features
🎨 Official UI Normalization
| Capability | Detail |
|---|---|
| Single-line header | Moves the conversation / trajectory selector into the title row; the header collapses to one line |
| Button capsule family | Session log, widgets, and toggle buttons unified into 32px capsules |
| Right-rail flush rounded rect | better-sidebar panel overlay layout; the header stays put |
| Unified settings header | Title 18/600 + description 13px + hairline divider |
| Native-title tooltips | Raw title attributes render as the official dark tooltip bubble instead of the OS-native popup |
♻️ Plugin Visual Reconciliation
| Target | Approach |
|---|---|
dsh-better-sidebar |
Capsule-ize toggle buttons, unify panel backgrounds, coordinate layout, smooth transitions |
dsh-widgets |
Matching stat capsule family, header utilities alignment |
dsh-context |
Keep the 组件 rail alive under its 上下文 dashboard: the composer seat dsh-context hides is the seat the rail is mounted in, so the seat is restored clipped and zero-height while the rail is on. Its 上下文洞察 dashboard (shell.overlay) gets the frame back: the outlet's z-20 stacking context is raised above the z-21 header while the backdrop is up (the mask used to leave the header, and the widgets' body-portaled hover magnifier, painting over the modal), the polychrome ContextIcon resolves to currentColor in both chrome seats, and the active pills stop painting the dark theme's near-white primary fill inside the dashboard. The dashboard then wears the Settings window itself: --dsw-radius-panel corners, bg-layer-2 fill, --dsw-elevation-prominent shadow and no border, the dialog title row (22px top / 24px side inset, 16px/24px 500 title, 28px round close seat), the content column starting 54px down at a 24px inset, and the official card recipe (20px radius, 12px 14px padding, 10px card-list gap) on its tiles — see docs/dialog-window-style.md |
@omdsh-dev/dsh-genui |
render_ui panels & tool cards: width follows the conversation content width (--enhancer-content-width, e.g. 840px) instead of inflating across the whole seat; fold bar long-title shrink fix with unified 11px/16px padding; 16px side-padding standard for full-width blocks (banner/steps, no box expansion); width guardrails for svg/pre/canvas/img/mermaid |
| Third-party settings pages | Auto-fill headings, drop redundant icons, normalize spacing |
🧹 Settings Auto-Normalizer ⭐
When any third-party plugin adds a page to settings.section that doesn't follow the official spec, this plugin auto-corrects it:
| Auto-check | Fix |
|---|---|
| Missing page title | Injects an 18/600 title (from the nav label or a known mapping) |
| Redundant icon beside the title | Removes the title-row logo, keeps plain text |
| Title/description too tight | Unifies to 4px spacing + hairline divider |
| Inconsistent type/size | Title 18/600, description 13/20 + border-bottom |
🎛️ UI Customization
The "UI Customization" block under Settings → General: chat width, markdown font size, workspace scale, UI font stack and smooth panel toggle all adjust live. "Smooth panel toggle" (default on) makes the right sidebar's open/close stop re-laying out the whole frame every frame: the center track snaps to its final value and the motion is re-added as covers on the transcript column and the composer capsule, which do not force layout. Turning it off restores the original eased track.
Architecture
The plugin is organised by target, not by file type. It exists to harmonize two different things at once — (a) the DSH shell itself and (b) whichever community plugins happen to be installed next to it — so every source file belongs to exactly one of four layers:
| Layer | Answers | Examples |
|---|---|---|
core/ |
infrastructure true regardless of DSH or any plugin | state model + persistence, the Harmony Contract, i18n, DOM/React helpers |
harness/ |
normalizations and repairs aimed at the DSH shell | official control-recipe mirrors, the settings-header reconciler, the chat-width channel, the frame's column-track transition, menu width, the stylesheet keeper, native-title tooltips, the rounded center card |
plugins/<package>/ |
adaptations written for one community plugin | commandcode-provider text normalisation, dsh-widgets rail squeeze + handle re-anchor, dsh-better-sidebar panels, dsh-genui width hygiene |
self/ |
the plugin's own UI | Settings → General rows, the font selector, the Harmony Doctor page |
Stylesheets follow the same split: 29 .module.css fragments with a single entry
(src/client/styles/index.ts) whose import order is the cascade order.
The rest of the contract is unchanged:
- Zero model cost: the host (node) half is a no-op; all changes happen in the browser half;
- Official design tokens: all styles use the
--dsw-*semantic tokens and follow light/dark themes automatically; - Two injection channels: static rules (CSS Modules) + dynamic
<style data-plugin>tags; - Reversible cleanup: every
ctx.effectreturns a disposer, so stopping the plugin leaves no residue — including DOM nodes it relocated and third-party strings it rewrote; - Slot integration:
settings.general.item/settings.section/shell.overlay.
Guards live in scripts/: tools/css-baseline.mjs (byte-invariant of the compiled
stylesheet), verify-frame-track.cjs (column-track animation + panel glide),
verify-rapid-toggle.cjs (the glide's re-entrancy: overlapping toggles must leave
no inline style, no running cover and no rail drift), verify/settings-page.cjs
(Settings → General signals), probes/harness/* (selector and network triage).
Installation
# via npm (plugin market)
dsh plugin --profile web add dsh-ui-harmonizer
# local development (link)
dsh plugin --profile web add link:D:/dsh-home/plugins/harness-ui-enhancer
After installing, hard-refresh the browser (Ctrl+Shift+R); the "UI Customization" block appears under Settings → General.
Development
pnpm install
pnpm run build # tsdown builds lib/
pnpm run check # typecheck + build
peerDependencies:@deepseek-ai/dsh-client-ui-slots,dsh-client-runtime(provided by the DSH web profile);- A pure client plugin:
cordis.patch.ymlinserts theui-enhancerrow; the browser half is declared bydsh.client; - Must sync after changes:
npx tsdownrebuild → sync intoprofiles/web/node_modules/dsh-ui-harmonizer/lib/→ hard-refresh the browser.
Compatibility
- DeepSeek Harness
0.1.0-rc.6and compatible later0.1.x; - Integrates via official slots, coexisting with better-sidebar, dsh-widgets, dshmarket, etc. by slot order;
- Known reconciliation targets:
dsh-better-sidebar,dsh-widgets,dsh-context,dsh-notification,dshmarket; - The page fully restores to defaults after uninstall/disable — no residue.
Roadmap
- Phase 1 · Official UI Normalization (in progress): keep fixing unfinished parts of the official UI;
- Phase 2 · Plugin compatibility coordinator (in progress): detect and fix layout/style conflicts between plugins;
- Phase 3 · Unified visual style (in progress): an optional style layer — the "rounded card" is live (wrapped-header model), plus title-tooltip unification; next: spacing density, more radius/animation unification;
- Liquid Glass exploration: normalization's end goal is lowering cognitive cost — unified headings and tooltips remove the micro-fatigue of switching between styles; a material layer goes further, using one consistent physical metaphor to signal elevation and interactivity so the whole page reads as a single mental model. Scope: pure CSS/token experiments on top of semantic aliases — an opt-in switch, at most a couple of large backdrop surfaces (GPU budget), honoring reduced-transparency/reduced-motion, falling back to today's solid fills where unsupported, never touching plugin sources, and only shipping if readability measurably survives it;
- Phase 4 · Ecosystem: crystallize into an extensible rule-registration mechanism.
Changelog
v0.9.0 — one interface language for the Harness, and a layout per target
Refactor — every source file now belongs to exactly one layer
- The plugin is organised by TARGET:
core/(infrastructure true regardless of DSH or any plugin),harness/(normalizations aimed at the DSH shell),plugins/<package>/(adaptations written for one community plugin),self/(this plugin's own UI).src/client/now holds nothing butindex.tsandservices.d.ts. - The 1253-line stylesheet is split into
.module.cssfragments (29 today — the plugin-reconciliation sheets for dsh-widgets / dsh-context, including its Settings-window shell, were appended after the split) behind ONE entry (src/client/styles/index.ts) whose import order is the cascade order; the split itself was proven byte-identical to the original, andscripts/tools/css-baseline.mjsnow pins the compiled result. npx tsc --noEmitwent from 52 errors to 0, andbuildrunstscsolib/types/really exists (it was advertised infilesbut never emitted).
Fixed — dead and wrong selectors, found by an adversarial pass over the live DOM
- The dropdown-menu recipe had never applied:
[role='menu'] [class^='_list_']is both a descendant and a prefix match, while the product putsroleand the list class on the SAME element with the list class not first (_surface… _list_4ub78_7 …). Measured with a menu open — descendant 0, prefix 0, contains 1. Fixed to[role='menu'][class*='_list_']; list / item / icon / label rules now land (padding 4px, min-width 218px, item min-height 40px). - Sidebar rows that carry a modifier class (
_brand _wide) silently stopped matching[class$='_brand']; the anchors now read "the class list CONTAINS a_branditem". - The brand wordmark is no longer resized at all — 0.2 renders it as TWO svgs (whale + HARNESS badge), so one
svg { width:182px }stretched the whale 7.6×. menu-anchormust not treat the shell's settings button (aria-haspopup="dialog",aria-expandedtrue for as long as the dialog is open) as a menu trigger — it was force-sizing the product's own popups to a sidebar row.- The Doctor's
deadInThisViewwas declared twice (a count and a row array), so its Markdown export printed[object Object]; a deadmarket-hashrule and a misattributeddata-dsh-*coupling are gone.
Fixed — the frame's column track actually animates on DSH 0.2
- 0.2 moved the AppFrame's
transition: grid-template-columnsbehind[data-animating], which React sets in a layout effect on the same commit that writes the new track — the before-change style therefore carries no transition, so none ever ran and the conversation column SNAPPED. Measured: 1640px → 776px in one frame, no transition events,data-animatingcleared by the component's own 600ms fallback timer. - The stylesheet now keeps the transition on the frame permanently (the product's own
[data-dragging]/[data-rightbar-instant]rules still win), andframe-track.tsowns the one exclusion the product implements in JS — a window resize.
Fixed — lifecycle
- The two DOM relocations (session tabs, bottom-workbench toggle) put their nodes back on dispose; the center card's unconditional 1Hz
setIntervalis gone (ResizeObserver + resize + transition events already cover it); the settings-header fingerprint hashes TEXT instead of its length (equal-length rewrites used to freeze the title).
v0.9.1 — smooth panel toggle, header badge fit, one settings-header skeleton; the rounded card removed
Published as a patch at the owner's call: it carries new behaviour, which semver would normally file as a minor. It also removes an optional visual layer that 0.9.0 shipped, so the changelog entry for it is written as a removal rather than as a silent disappearance.
Removed — the center-column rounded card
- 🗑️ The "Rounded Card" switch (Settings → General → UI Customization) is gone, with its
shell.overlaycover, its stylesheet and its state field. Reason: it was a visual opinion rather than a normalization — it added a corner radius and a shadow the product does not have, and it owned a second surface inside the center column that every other co-tenant had to route around. What remains in that block is the part that removes a defect instead of adding a look: chat width, font stack and panel glide. The removal is residue-free by construction — the root class is no longer written, anddisposeDynamicStylestill clearsenhc-center-card-onso a page running an older build of the plugin during a hot update cannot keep a card that nothing paints. The persistedcardkey is dropped when state is read, so a profile that had it on does not carry a field nothing owns. - 🧭 The Doctor's surface inventory consequently lists one declared surface instead of two. Historical changelog entries below still mention the card; they describe the releases they belong to and are left as written.
New — the right sidebar's open/close stops re-laying out the whole frame ("Smooth Panel Toggle", default on)
- 🔍 Measured first: the product eases
grid-template-columnson_frame, and that is a layout property — the center track travels 1427px → 659px over 300ms, re-laying out the entire frame on every frame of it (the product's ownResizeObserverwakes 27 times, against 10 for a left-panel toggle) while the compositor and GPU threads idle. Per-frame capture named more than one offender: besides the track,_scrollBody'spadding-righteases over 300ms and_widthHandle'sleft/rightease over 300ms too. The transcript column is centered byscrollBody width − padding-right, so when the track has arrived and the padding has not, the column transiently flies out — measured +261px of overshoot on close, and +54px still left when only the track is pinned. - ✅
src/client/harness/chrome/instant-track.tstakes the right-panel button's click in the capture phase: it writesdata-enhc-instanton<html>, pins_frameand_scrollBodyto a 1ms transition, then finishes every transition the gate created from the mutation microtask of the product's own track write — so the track lands in one frame whiletransitionrun/transitionstart/transitionendall still fire. It is an attribute rather than a class because the theme observer watches<html>'sclassand rebuilding the material costs ~7ms per write (measurement in the handoff);transition-duration: noneis wrong for the same reason0sis — a transition that never starts emits no events at all, and dsh-widgets' rail yield reads that beat. - 🧷 The motion is re-added as covers that do not force layout —
transformon the transcript column andlefton the composer capsule. A cover must track the layout, never predict it: the keyframe'sfromis the displacement read in the same frame the cover is armed, and aResizeObserverre-seats it after layout and before paint while it runs (a fixed keyframe overshoots by construction whenever itsfromcomes from a transient — the close direction's first readable seed is +358px against a 304px corridor). Measured on four arms, no frame paints outside the[312, 616]corridor and neither endpoint is ever returned to, which is the "flash then translate" report. - 🚫 Nothing transforms the composer or any of its ancestors:
dsh-widgetskeeps threeposition: fixednodes (dsx-stats-drawer/-zoom/-rail) inside those ancestors, and atransformrewrites their containing block and drags the rail into the viewport (measured: rail 1247 → 412.4).leftis a layout property that creates no containing block, so the rail's drift reads 0px in every sample. - 🔁 Re-entrancy is conserved: the driver gives every click a generation token and an older
stepbails as soon as it is stale. Before the fix, 9 clicks at 130ms lefttransform: translateX(-230px)andleft: -230pxbehind permanently (the orphan cover adopted the newer click's inline style as the "original" and then "restored" it); after the fix three storms (9×130ms / 15×60ms / 5×400ms) pass every check with no residue. - 🔒 The gate is released only when nothing is still moving (
getAnimations()on the dock plus each cover's ownbusy()), capped atHOLD_MS + 400ms. Releasing on the click's clock cut the open slide atcurrentTime 254.7/300with noanimationendat all, snapping the dock fromtranslateX(10.35px)tonone— the residual difference between the release and the panel's ownanimationendis now ≈10ms. - 📊 Cost and gain (one built artifact, the only variable being
enhc-panel-glideon<html>, 9 toggles each): long-task blocking 285.2ms → 205.7ms (−28%), long tasks 58 → 43; the price is about +8ms of layout and +23LayoutCountper toggle, withTaskDuration/ScriptDurationessentially unchanged. Treat these as directional only: the later 15-toggle pair on one build disagreed between its own two arms, so only the isolated mechanism measurements (a class write vs an attribute write on<html>;getComputedStyleinside the track'stransitionrun) are used as causal evidence. - 🎛️ The Setting → General → UI Customization row "Smooth Panel Toggle" applies live, defaults on, and leaves no residue (
apply.tsowns the root class,disposeDynamicStylealso dropsdata-enhc-instant). - 🧪 Guards:
scripts/verify-frame-track.cjs(12 checks: snap on open and close, the three transition events, a cover on the transcript column, zero rail drift, and a viewport change that neither eases nor missesenhc-window-resizing) plusscripts/verify-rapid-toggle.cjs(28 checks: the re-entrancy contract above, plus both gates must have been released). The rapid-toggle guard also tells whose rail drift it is: a drift with the rail'srightstill at the anchor is the swallowed-rail race indsh-widgetsitself (2/30 storms with this plugin inert, 1/60 with it live), so the check passes only on that signature and fails on a drift with the rail already claiming0px— which would be our residue.
New — the top bar glides with the panel, and its badges retire before the title is crushed
- 🔍 Measured first: the transcript column glided while the header snapped in the click's own commit. Per-frame sampling of a toggle showed
_headerUtilitiesand the tab strip never moving at all after the snap, against the transcript's 509.1 → 312 ease. The header cannot be covered the way the column is — its left edge is pinned to the left column and its right edge is the panel's, so its geometry is a width, not an offset, and a relative offset cannot stretch the title column (covering the hosts that have noposition: fixeddescendant left the title truncating instantly). Measured: settingheader.style.widthback to its closed value while the panel is open reproduces the closed header exactly —280,1427,50, title column 212px, strip867.5,139,26, against the open state's280,659,78/ 41.5px /697,87,58. - ✅
instant-track.tsgrew awidthmode next toleftandtransform, sharing one driver, reseat and release path, and arms it on the header from a width read in the same capture-phase click. Measured on the three-badge reference session (1707×1067): 12 distinct painted widths, 1427 → 659 on open (18 in the first run) and 22 on close, first frame at the closed width. Everything inside follows for free — title, badges, tab strip, right-hand buttons — so no per-host covers and notransformon any header node. - 🐛 The other half of the report was wrapping, and the badges were not the cause: the strip is relocated into the 30px title row and arrives with the product's
flex: 0 1 auto+min-width: auto, so the cluster's shrink squeezed the strip until its capsule labels wrapped — strip 26 → 58px and the header 50 → 78px. One declaration,flex: none, holds the strip at its content width and moves the whole deficit onto_crumbs, which the product already ships withmin-width: 0; overflow: hidden. Measured139x26and a 50px header in all ten arms (1707/1440/1280/1152/1024 × open/closed) and on every frame of two live toggles (57 and 69 sampled frames). - ✅
harness/header-fit.tsspends the badges in the order asked for — 标准模式 → 智能体团队 → 子智能体, any badge it cannot name last — until_crumbsclears a 160px legibility floor, with 24px of hysteresis so a threshold cannot flip-flop. Measured on the reference session with the panel open: title 0px → 165px with two of three badges retired; the rung is monotone through the open (0 of 56 frame pairs went backwards) and closing restores all three plus the title's full 212px. - 🧷 Badge identity is attribute-based, never text: the visible strings are runtime locale lookups (∅ occurrences in the app archive, escaped or not) and the module classes are build-hashed —
aria-haspopup="tree"for the subagent catalog,data-team-actionfor the team action, a barespanfor the preset label. The fit reads_crumbs.clientWidthafter applying a candidate rung rather than computing from badge widths, because those widths are not constants: the product's own@containerrules zero the 标准模式 label below a 540px row and the 智能体团队 label below 480px, and the background counter's width follows the job count. - 🧪 Guard:
scripts/verify-header-fit.cjs(14 checks — the closed baseline and the ladder's badge set, the glide's distinct widths and its starting value, zero wrapping frames through a live toggle, a monotone rung, the settled prefix + floor, full restore on close, size at four widths in both panel states, no residue attribute, no page errors).
Refactor — the settings page header is ONE skeleton with ONE geometry, on every page
- 🔍 Measured first (
scripts/probes/settings/probe-header-geometry.mjs, all eight pages that have a header): the title's viewport position was already identical everywhere (titleTopFromDialog = 54), but the title→description distance was not — 4px on this plugin's own pages, 12px on the official ones, 0px on Command Code (its container is block flow, so no gap at all and the description touched the title) and 16px on 侧边卡片. The official value, read with this plugin's stylesheets temporarily disabled: 12px on models / agent presets / bundled plugins — the section container's owngap. The "official 4px" the older docs claimed was this plugin's own compression, and it is retracted. - ✅
src/client/settings-page.tsnow produces exactly the skeleton a page needs and nothing more:h2.enhc-page-title+p.enhc-page-introas siblings. This plugin's own pages render it throughSettingsPageHeader, foreign pages get the pair stamped on their real nodes, and the head wrapper is gone — so a DevTools inspection shows the same two-node header shape on every page, official or third-party. - 📐 The spacing is no longer a stylesheet constant: the reconciler measures the container that actually lays the header out (walking up through
display: contentsslot outlets) and writes the correction onto the description (margin-top = 12px − container gap), plus a compensating negativemargin-topon the title when a container contributes its ownpadding-top/border-top. After the fix every page reads title y=134, intro y=172, gap=12px while the containers still differ (4 / none / 12 / 16 / 2px). - 🔒 Nothing is moved or re-parented (a foreign React tree never sees a wrapper it did not render), a page that already conforms is left exactly as authored, and the reconciler's diff releases a node — and its inline spacing — the moment a page re-renders around it.
Fixed — the sidebar account popup was narrower than the row it hangs from
- 🐛 The account menu (设置 / 意见反馈 / 退出登录) painted at the product Menu's content width (218–360px) under a sidebar row that is the full 256px — a visible seam between the popup and its trigger.
- ✅
src/client/menu-anchor.tspins a portalled menu opened from the settings-launcher seat to its trigger row's width (box-sizing: border-box), scoped to that seat: the composer's model/permission pickers and every other menu keep their own recipe.
Fixed — a third-party settings page rendered completely unstyled (loader claim-pass bug)
- 🔍 Root cause found in the product's client-module loader, not in the plugins:
claimStyles(id)runs after every module factory materializes and adopts every untagged<style>in the document for that plugin (style:not([data-plugin])), whileremoveOwnedStyles(id)deletes everystyle[data-plugin=id]when that adopter reloads, unloads or is pruned. A plugin that hand-injects its stylesheet once inapply()(dsh-notification:#dsh-notification-style) therefore loses it permanently for the session — measured live: the 通知 page rendered as a raw text dump (no cards, no badges, default buttons) and#dsh-notification-stylewas simply absent. - ✅
src/client/style-keeper.tssnapshots hand-injected stylesheets (data-plugin-cssis the discriminator: bundler-emitted tags are the loader's own and stay untouched) and restores one that disappears, after a grace period that lets a legitimately hot-reloaded owner re-inject first. A restored sheet keeps its authored attributes — the thief'sdata-pluginclaim is dropped. - 📊 Probe (live page, loader semantics reproduced): baseline 38 rules + card
1px/12px/flex→ after the theft no sheet and0px/0px/block→ after the grace period 38 rules and the card styled again; 9/9 checks pass.
Cleanup
- 🧽 Repaired 48 mojibake characters in
enhancer.module.csscomments (an old PowerShellGet-Content -Raw/Set-Contentround trip had re-encoded the file as GBK:—→鈥?,×→脳, one Chinese phrase destroyed). Comment-only damage — the build was never affected — rewritten as UTF-8 by a Node script, never through a shell.
v0.9.0 — repositioned: from CSS patch to UI contract layer + compatibility auditor
New — Harmony Contract (cross-plugin UI contract)
- 🧭 Publishes negotiation variables on
<html>that any plugin or theme can read without depending on this one:--enhc-contract(revision),--enhc-surface-solid,--enhc-glass-aware,--enhc-solid-fill,--enhc-content-width,--enhc-sidebar-scale. - 🔌 Provides
ctx.get('uiHarmony')using the product's ownctx.reflect.provideidiom (the same one behindctx.sidebarRight): neighbours declare what they occupy withregisterSurface({ id, role, occupies, widthVariable, transition, tokens, opaque })instead of being measured by guesswork. This plugin only arbitrates and reports; it declares its own surface too (the Settings → General rows and the UI Compatibility page). - 🪟 Material awareness derived from the semantic tokens themselves (alpha of
--dsw-alias-bg-base/-layer-1/--dsw-specific-sidebar-fill), never from plugin identity. Under a glass theme--enhc-solid-fillbecomestransparent, so the header/panels stop painting an unblurred opaque rectangle over the glass. Measured by simulation (no third-party theme installed): overriding--dsw-alias-bg-basetorgba(255,255,255,0.45)flips--enhc-glass-aware0 to 1,--enhc-surface-solid1 to 0,--enhc-solid-filltotransparent, and the session headerbackground-colorfromrgb(255,255,255)torgba(0, 0, 0, 0); removing the override restores all four.
New — Harmony Doctor (local read-only compatibility audit)
- 🩺 Settings → UI Compatibility: one click runs four checks, zero network, zero model calls, exports a Markdown report.
- 🕳️ Dead-rule ledger: every one of the plugin's own selectors is match-counted, and a selector counts as dead only when it matched nothing in every view observed — the ledger persists per view, so the verdict sharpens as the user moves around. Measured over six probe views (hero, settings, session, doctor, glass on, glass off): of 149 selectors, 64 match in at least one view and 85 match in none of the observed views.
- 🔗 Foreign coupling health: selectors that still read another plugin's private hashed class or undocumented
data-*host, with live match counts (39 couplings reported automatically, a batch of them now matching 0 because better-sidebar 0.19.1 removed the classes). - ⚖️ Conflict / redundancy verdicts: inline self-check (what we wrote must read back) plus a redundancy test (temporarily disable all our sheets and re-read the computed value; unchanged means the rule achieves nothing).
- 🗂️ Surface inventory: every
data-slot, cross-plugindata-*, published contract variable and declared surface, with consumers and counts.
Fixed — the font setting never took effect
- 🔍 Root cause: the 12
--dsw-font-markdown-*tokens were written into a<style>tag at head index 4, while the official theme'sgradient-shadow-text.css(declaring the same tokens onbody{}) sits at index 24. Identical selector and specificity, so the later official rule wins — all four font presets read back byte-identical token values, and CDP reportedSegoe UI + Microsoft YaHeifor every one of them. The picker was never broken and the fonts were never missing (HarmonyOS Sans SC, Microsoft YaHei, Noto Sans SC, Georgia, SimSun, Consolas and Courier New all resolve on this machine). - ✅ Fix: the family is applied as inline custom properties on
<body>/<html>(order-independent, removed exactly on dispose), and the token sizes now reference the product's own--dsh-content-font-size/--dsh-content-font-deltainstead of hardcoded px. - 🎯 Measured after the fix:
serif→ tokenGeorgia..., CDP rendersGeorgia(10 glyphs) +SimSun(4);yaheiwith scope Whole UI → UI elements renderMicrosoft YaHei;default→ no override left at all. - 🧩 Coverage closed: the plugin now writes all 12
--dsw-font-markdown-*tokens DSH 0.1.5 actually consumes, including the three the old code missed —-table-head,-code-block-small,-code-font-family. The other 90 declarations are never consumed; leaving them alone is free. Code blocks follow a monospace pick and stay on the product's code stack under a prose pick. - ➕ New font scope (chat prose / whole UI) and a missing-font hint (two-baseline canvas width-diff;
document.fonts.checkreturns true for unknown families and is unusable here).
Removed / simplified (absorbed by DSH 0.1.5 or already dead)
- 🗑️ The
html.enhc-panel-openseat mechanism and the whole better-sidebar toggle-cluster seat are gone. Measured: better-sidebar 0.19.1's 192-key CSS-module map has notoggleCluster/panelHidden/panelResize, and it no longer publishes--dsh-sidebar-width(it publishes--dsh-title-bar-strip/--dsh-sidebar-height). Keeping the code was worse than dead:panelOpenwas permanentlytrue, so the "compact floating seat" became the default shape — the design running backwards, silently. - 🗑️ Dropped the
[data-input-scroll]and user-bubble font-size overrides: the product already sizes both from--dsh-content-font-size, and our overrides froze the official row on those two surfaces (measured line-height21pxto24pxafter removal). - 🗑️ The content-size row itself is retired in favour of the product's
FontSizeRow(12-17px,ui-themenamespace): this plugin now consumes that channel instead of racing it for the same tokens. Kept because the product has nothing equivalent: the chat-width row, and the font family + scope (the product's--dsw-font-familyis a fixed:rootvalue with no user setting). - 🧹 The rounded center-column card is now material-variable driven (
--enhc-solid-fill) so glass themes keep their surface.
Reproduce
- Rendered-level probe:
node scripts/probes/plugin-eco/probe-harmony.mjs http://127.0.0.1:19387 out.json(scripts/lib/auth.mjsmints a browser-session cookie from the persisted secret, so it passes the UI gate without the one-time launch token and without disturbing the running session). - Every number, method and caveat:
docs/MEASUREMENTS.md.
v0.8.4 (unreleased, pending acceptance)
Compatibility — DSH 0.1.5 client-structure alignment:
- 🧭 Center-column lookup rewritten (the one hard break): 0.1.5 moved the conversation from the root child slot
conversationto a keyedmainentry declaringmain.conversation, and both anchors render withdisplay:contents, so the oldslot.parentElementreturned an unmeasurable wrapper.findCenterColumn()now accepts either anchor and resolves the real column withclosest('[class$="_centerCol"]'); the parent walk stays as a fallback. The rounded-card CSS:has()guard accepts both spellings. - 🔌 Types and packaging follow the official layout:
@deepseek-ai/dsh-client-runtimewas retired in 0.1.5, so the client context type comes from@deepseek-ai/cordisand the package was dropped from peer/dev dependencies and the tsdown platform table. - 🔍 Audit conclusion (everything else survived, no code change):
conversation.session.header > header,_titleCluster/_headerActions/_tabs/_viewArea/_composerSeat, the sidebar family, the settings quartet and the menu primitives all still exist;_flowItem/_bubblemoved to the newdsh-client-ui-chatpackage with new hashes, which suffix selectors ignore. - ✅ Measured: in a 0.1.5-rc.2 isolated instance (own DSH_HOME, own profile, port 3081) the rounded-card overlay geometry is correct under
main.conversation(left=56px / width=694px), with no uncaught exception and no slot error. - 🎛️ Top bar (2026-09-13): the conversation/trajectory tabs go back to the official underline style (the custom capsule overlay is deleted, and the
margin-top:10pxthat made the tabs sit low inside the title row is zeroed); the header's right-side reserved band for a floating better-sidebar cluster is removed, because 0.19 registers its controls into the product's own header seat and no longer publishes that variable. - 📌 Known follow-ups:
_flowItemhash changes are cosmetic only; the.nArs4W_*rules (better-sidebar 0.14 class names) were already dead under 0.19.1 and have since been removed in 0.9.0.
v0.8.3 (unreleased, pending acceptance)
Perf — the sidebar squeeze keeps its smooth progressive glide at full frame rate (companion to dsh-widgets v1.2.3):
- Root cause of the panel-toggle jank: the three squeezed surfaces (conversation
viewArea,composerSeat, header) animatemargin-right: var(--dsh-sidebar-width)over 0.3s — a per-frame reflow of the WHOLE conversation DOM — which on long sessions (thousands of nodes) dropped to 20–31% dropped frames and visibly desynced from the compositor-driven widget rail and panel slide. - Fix: keep the progressive margin animation exactly as-is (left edge pinned, right edge gliding, text reflowing progressively — no "jump to final width, then slide" compromise) and make each per-frame reflow cheap instead: every conversation turn/step (
*_flowItem) now getscontent-visibility: auto+contain-intrinsic-size: auto 120px, so off-screen items skip layout entirely and each animation frame reflows only the handful of visible items.autolets the browser remember each item's last rendered height, so scrollbar height stays stable; browsers without support simply ignore the rule. - Measured (playwright + local Edge, widget rail open, heavy sessions, panel open/close window): dropped frames 20–31% → 11.5% (rail fix) → 0%; viewArea LEFT edge drift during the animation: 0 px (always aligned); rail↔conversation right-edge offset constant (std 0.01 px — perfect lockstep); scrollHeight after a jump-to-bottom: 0% shift (intrinsic sizes converge); once warm, the largest single-frame step is ~89 px — a mid-curve frame under headless software rendering, smaller on real GPUs. Known one-off: the FIRST panel open after a page load still has one large step (better-sidebar's first panel render long-task, unrelated to this change).
- Self-contained verification:
scripts/archive/dead-probes/verify-glide.cjs(npm i -D playwright-core && node scripts/archive/dead-probes/verify-glide.cjs [session]).
v0.8.2 — released
Feature (i18n — Chinese/English locale adaptation):
- All hardcoded Chinese UI strings in Settings → General now adapt to the browser locale: page header title/description, five setting row titles/descriptions, and font preset labels all render in English when the locale is not
zh-*. - New
src/client/i18n.tsmodule centralizes every user-facing string; language detection is responsive — re-evaluated on every render call, so switching Settings → Language takes effect immediately without a page reload. - Detection priority:
localStorage('dsh-language')(written by the official Settings panel) →<html lang="...">attribute →navigator.languagefallback. - The
KNOWN_TITLESfallback table (used by the settings-section title-inject logic for third-party pages without a heading) is now locale-aware, matching both Chinese and English intro prefixes, and re-evaluated on each call so language switches take effect for injected titles too. - No visual or behavioral change in Chinese locales; English locales now see fully translated labels instead of a mix of Chinese and English.
v0.8.1 — released (2026-08-27)
Fix (cross-plugin width hygiene — @omdsh-dev/dsh-genui render_ui panels & tool cards):
- Root cause — the fold bar's
.panelToggletitle span (long nowrap text such as "opencode-go Multi-key Mini Pool — Final Architecture") is a flex child missingmin-width:0; flex defaultmin-width:autorefuses to shrink, so the title's max-content width inflates the fold bar and the whole panel beyond the conversation column. Harmonizer adds a hash-agnosticflex: 1 1 0%; min-width: 0shrink baseline (so ellipsis engages) and caps the panel/fold bar width; the same family is covered for.toolFallbackMeta(tool fallback long meta) and.tlTime(timeline long timestamps). Pure CSS overrides, zero modification of dsh-genui source; the rules survive upstream CSS-module rebuilds (class-substring matching). - Final baseline (user acceptance criteria, settled) — panel/tool width follows the conversation content width, i.e. the harmonizer chat column max-width slider's
--enhancer-content-width(currently 840px), matching the officialMd3f7G_columnconversation-content column — not the input box and not the outer composer seat. Final rule:[data-genui-panel]{ display:block; width:100% !important; max-width: var(--enhancer-content-width, 748px) !important; margin:10px auto 2px !important; contain:inline-size; box-sizing:border-box }— adapts live to the chat-width slider and centers horizontally in the conversation column. The earlier "measured input-box width via--enhc-message-maxw" approach was dropped (baseline drift). Lesson:width:auto+margin:autotriggers shrink-to-fit on a flex cross axis and collapses the panel into a vertical sliver (regression); always pair an explicitwidth:100%with a max-width instead. - Horizontal spacing for full-width blocks —
bannerreuses the fold bar's width format: content width with 16px side insets (text starts at the same x as the panelToggle title), never a negative-margin box expansion (which pushed padding past the card edge), in every container (panel body / inline / tool card);stepsgets 16px side padding inside padding-less inline/tool-card containers; svg/pre/canvas/img/mermaid are all width-guarded; block components (callout/card/list) keep their own shape untouched.
v0.8.0 — released
Feature (native-title tooltip harmonizer):
- Any element that only carries the raw HTML
titleattribute (the model selector trigger, assorted product controls) used to pop the OS-native tooltip and break the visual language kept by every surface routed through the official Tooltip primitive. Hover/focus is now intercepted: the title is lifted for the interaction and re-rendered as the official bubble —--dsw-alias-tooltip-bgchip, padding 3px 7px, radius 8px, 13px/20px type, 50vw width cap, 500ms hover delay, immediate on keyboard focus, placed 8px below the anchor (flips above when clipped), clamped to a 12px viewport margin, z-index 100 popup band. - Rollback safety: an ancestor carrying
data-enhc-no-tooltipopts a subtree out; the lifted attribute is restored verbatim on leave/blur/plugin stop (if the app rewrote the title mid-hover, its newer value wins); the fade-in respects reduced motion.
Fix (toggle cluster seat):
- The floating better-sidebar toggle cluster got an opaque seat in
bg-base. Default (panel closed): a whole-height block spanning the session header band (top 0 → 56px), so buttons/seat/header read as one flush right edge and widgets-rail cards can no longer show through. While a better-sidebar right panel is open (html.enhc-panel-open, kept in sync by the client half) the seat collapses back to a compact floating chip, because the panel's top edge deliberately sits below the page top and a tall seat would jut into it.
Fix (rounded card wraps the session header):
- The AppFrame's shell.overlay outlet is itself a z-20 stacking context, so the card chrome painted inside it can never out-draw the z-21 session header. Zero-pixel split-paint model (no white slab anywhere): the HEADER draws the card's top edge as an INSET box-shadow hairline (a real border-top grew the header by 1px and misaligned it against out-of-flow controls) plus the 18px top-left radius, while the overlay box demotes to a pure shadow caster spanning header + content with a custom left/top-emphasized recipe — official lv3 offsets down-right and its tight contact halo painted a stray edge line along the window. Routes without a session header fall back to the classic self-drawn card. Hover/active fills ride above the seat; the seat carries a continuation of the header's top line so the edge reads unbroken across the full card width.
v0.7.1 (folded into v0.8.0)
Fix (better-sidebar Files tab strip):
- 📏 Tabs now FILL the 44px tab bar: the previous fixed
height: 36pxbroke better-sidebar's nativealign-items: stretchchain, leaving ~8px of dead space at the strip's bottom. Reverted toheight: auto+ explicitalign-self: stretch— the 14px label and icons stay vertically centered inside the taller strip. - ↔️ The open right panel's tab strip now reserves 90px on its right end (was 72px): the toggle cluster got bigger (two 32px capsules + 6px gap at
right: 12px= 82px total), so the old seat let the rightmost tab / + button slide under the capsules. 90px = 82px cluster + 8px breathing room. The bottom panel's 40px seat is untouched. - 🧭 The session header's shared right margin bumped 82px → 90px to match the wider cluster (covers both the collapsed corner seat and the open-panel
max()path).
Fix (dark-mode active-state glyphs):
- ⚪ The active "Chat" tab now renders WHITE glyphs on the DeepSeek brand-blue fill in dark mode. It had used
--dsw-alias-label-primary-inverted, which resolves to a near-black bluish-800 on dark themes — black text on the blue fill. - ⚪ The activated "Components" capsule keeps its own shipped pair (
state-business-primary+#fff): an earlier override in this plugin had replaced that white with the same near-black token (dark-mode only). That override is dropped, so both buttons read as blue fill + white glyphs in light AND dark themes, matching the official nav-cell pattern.
v0.7.0
Meta — renamed package to dsh-ui-harmonizer:
- 📦 npm package renamed
harness-ui-enhancer→dsh-ui-harmonizer(dsh- prefix + "harmonizer" naming matches the ecosystem norm and search; old package deprecated, redirects here). - 🎯 Positioning: "UI harmonizer for DeepSeek Harness" — normalize/reconcile/unify the UI into the official design language (not just polish).
- 🔀 GitHub repo renamed
Physicolor/harness-ui-enhancer→Physicolor/dsh-ui-harmonizer(old URL auto-redirects; stars/issues preserved). - ♻️ Install:
dsh plugin --profile web add dsh-ui-harmonizer. No data impact (client-only plugin, no persisted keys).
v0.6.3
Meta:
- 🏷️ Added npm
keywords(deepseek-harness / dsh / cordis / plugin / web-ui / ui-enhancement) so the package shows up in npm search; no code change. - 🪧 GitHub repo topics expanded (deepseek-harness, cordis, cordis-plugin, browser-extension, web-ui, ui-enhancement, plugins).
v0.6.2
Fix:
- 🧱 The session header now has an opaque card surface (
--dsw-alias-bg-base) and sits one step above the shell's overlay layer (z-index: 21, still below better-sidebar's panels at 40 and modals): the dsh-widgets rail and its magnify overlay slide UNDER the header's white rectangle instead of visually stacking onto the header buttons. Header rules stay in the enhancer (the widgets plugin no longer touches official elements).
v0.6.1
Fix:
- 🧩 The big empty gap between the chat/input and the right sidebar when it opens: fixed the double-squeeze on the conversation's
margin-right. The#rootneutralization previously cleared onlymargin-right, leaving better-sidebar'swidth: calc(100% - var(--dsh-sidebar-width))active — the width squeeze first narrowed the column to the panel's left edge, then the viewArea/composerSeat margin squeezed a second time, pushing the conversation one full panel-width short of the panel. Addedwidth: 100%to fully neutralize#root, so the inner margin is now the single, correct squeeze (the chat's right edge meets the panel's left edge, minus the 8px scrollbar gutter).
v0.6.0
Removed:
- 🗑️ Removed MCP server management and task automation — these weren't "UI polish" concerns, so they were removed wholesale (the host-half API routes are gone; the plugin is a pure client with zero host logic). The MCP / automation buttons no longer appear at the bottom-left.
New:
- 🃏 Rounded card: the conversation area renders as a card with a rounded top-left corner and a drop shadow (Settings → General → UI Customization → rounded card). No source changes: a transparent
shell.overlaycover (top border + top-left radius +--dsw-shadow-lv3shadow); the shadow bleeds left into the sidebar (card thickness), the top moves down 1px for the shadow gap; right/bottom are natural window edges with no drawn border; the left edge borrows the sidebar's ownborder-right; aResizeObservertracks the center column so it follows sidebar drags / collapse / details-column changes; the content's top-left is masked round by the center column's ownborder-radius; pure CSS gating (html.enhc-center-card-on), togglable and residue-free.
Fix:
- 🎚️ Live feedback for the customization toggles: the rounded-card switch uses a local mirrored state
…
Links
More in this category
zhu1090093659/dsh-web#packages/dsh-task-board★ 8597
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★ 8597
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.
MeteorNOX/DeepSeek-Balance-Whale-Widget★ 4424
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.
ccch1mneyyy/dsh-TUI★ 4268
Claude Code-style full-screen terminal UI: pixel-whale header, live status line, and streaming thought expansion.
omdsh-dev/DSH-better-sidebar★ 4098
Full sidebar workbench with file rendering and editing, terminal, Git, and subagents; third-party plugins can register new tabs.
Devin-AXIS/deepseek-design#deepseek-idesign★ 1445
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.