packages/dsh-task-board
zhu1090093659/dsh-web侧边栏多列任务看板:卡片交给真实 DSH 智能体会话执行,支持 cron 定时(Host 侧到点执行,关浏览器也生效)。
DeepSeek Harness UI 协调器:规范化官方未完善、自相矛盾的界面,协调已装插件间的样式冲突并统一视觉语言(全部遵循官方设计令牌)——支持实时自定义对话宽度、markdown 字号、工作区缩放、UI 字体栈,并在设置页提供圆角中栏卡片;纯客户端 dsh bundle,零模型开销,无 host 端,可无残留移除。
description: "One interface language for the Harness, plus per-plugin reconciliation and a read-only compatibility audit."
DSH UI Harmonizer
UI harmonizer for DeepSeek Harness. Normalizes the official UI · reconciles every plugin · settings auto-normalizer · UI customization (width, fonts, panel glide)
Part of DSH Design System — this repository is the runtime half. The spec it enforces lives at Physicolor/dsh-design-resources.
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.
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
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
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
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.
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// · 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:
--dsw-* semantic tokens and follow light/dark themes automatically;ctx.effect returns a disposer, so stopping the plugin leaves no residue — including DOM nodes it relocated and third-party strings it rewrote;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).
# 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-enhancerAfter installing, hard-refresh the browser (Ctrl+Shift+R); the "UI Customization" block appears under Settings → General.
pnpm install
pnpm run build # tsdown builds lib/
pnpm run check # typecheck + buildpeerDependencies: @deepseek-ai/dsh-client-ui-slots, dsh-client-runtime (provided by the DSH web profile);cordis.patch.yml inserts the ui-enhancer row; the browser half is declared by dsh.client;npx tsdown rebuild → sync into profiles/web/node_modules/dsh-ui-harmonizer/lib/ → hard-refresh the browser.0.1.0-rc.6 and compatible later 0.1.x;dsh-better-sidebar, dsh-widgets, dsh-context, dsh-notification, dshmarket;Refactor — every source file now belongs to exactly one layer
core/ (infrastructure true regardless of DSH or any plugin), harness/ (normalizations aimed at the DSH shell), plugins// (adaptations written for one community plugin), self/ (this plugin's own UI). src/client/ now holds nothing but index.ts and services.d.ts..module.css fragments (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, and scripts/tools/css-baseline.mjs now pins the compiled result.npx tsc --noEmit went from 52 errors to 0, and build runs tsc so lib/types/ really exists (it was advertised in files but never emitted).Fixed — dead and wrong selectors, found by an adversarial pass over the live DOM
[role='menu'] [class^='_list_'] is both a descendant and a prefix match, while the product puts role and 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)._brand _wide) silently stopped matching [class$='_brand']; the anchors now read "the class list CONTAINS a _brand item".svg { width:182px } stretched the whale 7.6×.menu-anchor must not treat the shell's settings button (aria-haspopup="dialog", aria-expanded true 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.deadInThisView was declared twice (a count and a row array), so its Markdown export printed [object Object]; a dead market-hash rule and a misattributed data-dsh-* coupling are gone.Fixed — the frame's column track actually animates on DSH 0.2
transition: grid-template-columns behind [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-animating cleared by the component's own 600ms fallback timer.[data-dragging] / [data-rightbar-instant] rules still win), and frame-track.ts owns the one exclusion the product implements in JS — a window resize.Fixed — lifecycle
setInterval is 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).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
shell.overlay cover, 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, and disposeDynamicStyle still clears enhc-center-card-on so a page running an older build of the plugin during a hot update cannot keep a card that nothing paints. The persisted card key is dropped when state is read, so a profile that had it on does not carry a field nothing owns.New — the right sidebar's open/close stops re-laying out the whole frame ("Smooth Panel Toggle", default on)
grid-template-columns on _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 own ResizeObserver wakes 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's padding-right eases over 300ms and _widthHandle's left/right ease over 300ms too. The transcript column is centered by scrollBody 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.ts takes the right-panel button's click in the capture phase: it writes data-enhc-instant on `, pins _frame and _scrollBody to 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 while transitionrun/transitionstart/transitionend all still fire. It is an attribute rather than a class because the theme observer watches 's class and rebuilding the material costs ~7ms per write (measurement in the handoff); transition-duration: none is wrong for the same reason 0s` is — a transition that never starts emits no events at all, and dsh-widgets' rail yield reads that beat.transform on the transcript column and left on the composer capsule. A cover must track the layout, never predict it: the keyframe's from is the displacement read in the same frame the cover is armed, and a ResizeObserver re-seats it after layout and before paint while it runs (a fixed keyframe overshoots by construction whenever its from comes 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.dsh-widgets keeps three position: fixed nodes (dsx-stats-drawer/-zoom/-rail) inside those ancestors, and a transform rewrites their containing block and drags the rail into the viewport (measured: rail 1247 → 412.4). left is a layout property that creates no containing block, so the rail's drift reads 0px in every sample.step bails as soon as it is stale. Before the fix, 9 clicks at 130ms left transform: translateX(-230px) and left: -230px behind 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.getAnimations() on the dock plus each cover's own busy()), capped at HOLD_MS + 400ms. Releasing on the click's clock cut the open slide at currentTime 254.7/300 with no animationend at all, snapping the dock from translateX(10.35px) to none — the residual difference between the release and the panel's own animationend is now ≈10ms.enhc-panel-glide on `, 9 toggles each): long-task blocking **285.2ms → 205.7ms (−28%)**, long tasks 58 → 43; the price is about +8ms of layout and +23 LayoutCount per toggle, with TaskDuration/ScriptDuration essentially 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 ; getComputedStyle inside the track's transitionrun`) are used as causal evidence.apply.ts owns the root class, disposeDynamicStyle also drops data-enhc-instant).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 misses enhc-window-resizing) plus scripts/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's right still at the anchor is the swallowed-rail race in dsh-widgets itself (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 claiming 0px — which would be our residue.New — the top bar glides with the panel, and its badges retire before the title is crushed
_headerUtilities and 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 no position: fixed descendant left the title truncating instantly). Measured: setting header.style.width back to its closed value while the panel is open reproduces the closed header exactly — 280,1427,50, title column 212px, strip 867.5,139,26, against the open state's 280,659,78 / 41.5px / 697,87,58.instant-track.ts grew a width mode next to left and transform, 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 no `transform` on any header node.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 with min-width: 0; overflow: hidden. Measured 139x26 and 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.ts spends the badges in the order asked for — 标准模式 → 智能体团队 → 子智能体, any badge it cannot name last — until _crumbs clears 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.aria-haspopup="tree" for the subagent catalog, data-team-action for the team action, a bare span for the preset label. The fit reads _crumbs.clientWidth after applying a candidate rung rather than computing from badge widths, because those widths are not constants: the product's own @container rules zero the 标准模式 label below a 540px row and the 智能体团队 label below 480px, and the background counter's width follows the job count.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 wit