packages/dsh-task-board
zhu1090093659/dsh-web侧边栏多列任务看板:卡片交给真实 DSH 智能体会话执行,支持 cron 定时(Host 侧到点执行,关浏览器也生效)。
在 DeepSeek Harness 右侧的可自定义部件栏:61 个部件覆盖会话统计、token 与缓存、上下文、后台任务、子代理、机器指标、OpenCode Go 与 Command Code 额度、会话成本、峰谷定价与 GitHub 待审队列;部件可在栏内市场安装,支持 1-4 列网格与 2x2 / 2x4 尺寸、逐实例配置、指针划过放大波峰与一键整栏收起。支持 DSH 0.1.x / 0.2.x,网页版与桌面端通用。
description: "A live widget rail on the right: session, machine, quota and cost at a glance."
DeepSeek-Harness Widgets
The blank strip beside the conversation, as a grid of live cards — and one click to put it away. 61 cards: session · context · background jobs · the machine · quota and spend · peak pricing Install each one, size it 2×2 or 2×4, tune it per instance, switch off what you do not watch; a macOS-Dock-style magnification wave follows the pointer.
What it looks like once installed (click the left still for the motion: rail-demo.gif, 3.8 MB) · and the market where every card is installed, sized and switched off.
Install (30 seconds) — paste these two lines to your DSH agent, or run them yourself:
# `web` is the profile name `dsh web` boots; yours is the directory name under $DSH_HOME/profiles/
dsh plugin --profile web add dsh-widgets
# then restart dsh web and hard-refresh the browser (Ctrl+Shift+R)No shell needed either way — say “install the dsh-widgets plugin, restart web, then tell me” to your DSH. The rail lives behind the Widgets icon in the session header; Settings → Widgets installs, reorders and resizes the cards.
Works on DeepSeek Harness 0.1.0-rc.6+ (0.1.x) and 0.2.0-rc.1+ (0.2.x).
What you get — 61 cards, yours to install, place and tune:
TL;DR: add one plugin, and the empty strip on the right of the conversation becomes a grid of live cards — tokens and cache, context, background jobs, your machine, your quota, your spend. Every card is an independent unit you can install, resize, tune or switch off; the whole rail disappears whenever you want the room back.
DeepSeek-Harness Widgets puts a widget rail on the right side of the DeepSeek Harness conversation page. Cards are independent units: pick them from the built-in market, place them in a 1–4 column grid, size a card 2×2 or 2×4, configure each instance on its own, and switch off anything you do not watch. The rail only ever claims the empty strip beside the transcript, and the whole thing is one click away from gone.
Why it is worth installing: the numbers it shows are the ones you otherwise open another tab or another panel for — how much context is left, what this session has cost, how many days the quota has left, how hot the machine is. They belong beside the conversation: one glance, no interruption.
If it earns its place: the plugin has no distribution channel beyond being found — a star on Physicolor/dsh-widgets is what moves it up the DSH plugin directories where other people are looking (GitHub search and recommendations both rank by stars). Arriving from a video? Saying which cards you installed in the comments tells me more about what to add than a star does; bug reports and card ideas live in issues.
A self-contained showcase site lives in website/ and is live at https://physicolor.github.io/dsh-widgets/ — what dsh-widgets is, why it exists, all 61 real widgets, the DSH Widget Design Grammar (the real unit/spacing/magnification formulas from src/client/index.ts, with an interactive rail running the actual magnification + right-anchored reflow), Widget Anatomy (a real card at ×2 with every padding, gap and inset measured from the DOM), the DSH Visual Audit (13 declared rules scored over all 61 widgets from live getBoundingClientRect measurements — rule-based, not a model), the widget-unit architecture, the production workflow, and a requirement-form → widget-spec generator. Plain HTML/CSS/JS, no build step, all paths relative for the project Pages base path. node website/verify.mjs self-verifies (static + SEO + Edge-headless browser checks, 88 checks); the widget table, the static gallery markup and the JSON-LD ItemList are generated from the manifests by node website/gen-site.mjs (--check fails on drift); node website/gen-og.mjs regenerates the social card. Deploy: see website/README.md.
Item · Detail Max columns · 1 / 2 / 3 / 4 (dropdown in settings, 2 by default). An UPPER BOUND: the deck steps down automatically when space runs short and never exceeds it when space is plentiful Max rows · 1–12 (5 by default). Multiplied by “max columns” it IS how many tiles the deck can seat, i.e. the cap on how many widgets may be PLACED: “max widgets” is read through columns × rows (the stored number is kept, so raising either brings it straight back), and the row shows a warning mark whose hover note says what clamped it Where the width comes from (why spare space does not always add a column) · The rail may only claim what the transcript's reading measure leaves: column − measure − 74px − 24px (74 = the shell scroller's own side padding plus the column-resize band, 24 = a safety margin). Of the two blank bands beside the conversation only the RIGHT one is claimable — the rail is right-anchored and claims by adding padding-right to the scroll body, which re-centres the prose; claiming more than column − measure would shrink the reading measure itself, and that line is not crossed. The left band is the prose's own centring margin. Measured at a 1578px window: column 1298, measure 748 → ~476px available, so 2 columns (372px) fit and 3 (546px) do not; drop “base card side” to 120px (or use a wider window) and the third column appears Base card size · The MINIMUM card side (150px by default). The rail is a fluid grid: within a column count the cards share the available width evenly, in 10px tiers, up to the size at which five card rows still fit the rail with the last row's bottom gap equal to the right gap (150 → 160 at a 1578×1000 window) — anything beyond that goes back to the transcript 2×4 tiles · Twice the width of a 2×2 plus a gap, same height; the same widget can be installed in both sizes at once Gap-free packing · Widgets pack by best-fit; gaps left by 2×4 tiles are backfilled by later 2×2s, so drag-reorder never leaves holes. In a 3-column deck a 2×4 that would strand itself moves one slot earlier and the 2×2 it displaces moves one slot later (rounding), so a wide tile is never left hanging Live width response · While the conversation width is dragged the available width is recomputed every frame and the cards change TIER the moment a boundary is crossed (the transcript inset is untweened for the drag's duration, so it tracks the conversation exactly); the column count steps at its thresholds. An all-2×4 deck skips the 3-column DEGRADATION step (2 ⇄ 4); an explicit 3-column preference is always honoured Transition feel · Both a size-tier change and a column change glide with a slight REBOUND (a ~4% overshoot spring) into their new size/cell, plus a small settle wave staggered in packing order (about 1.6% amplitude, 30ms per card) — never one whole-block pulse. The rail's own width and the transcript inset run on the SAME spring, so container, cards and conversation move together instead of the box jumping ahead of the cards Magnification · Works in multi-column grids too; magnified rows/columns yield by planar distance with constant spacing Wheel = row detents · The wheel steps whole ROWS (every offset is a multiple of the row pitch = card side + gap), so the top row is never cut in half and one notch pulls the next row up to where the first one was. The card the viewport would cut at the bottom is not drawn at all (see "Whole cards only" below), so the rail only ever shows whole cards. Scrolling always uses the browser's own smooth scrolling and behaves identically whether the pointer is on a card, in a gap, or on empty rail — never a per-notch hard jump
The rail's top edge sits on the session header's bottom edge (--dsx-rail-top; the first card lands exactly on that line — 44px at 1578×950, first card 50px). The anchor is the LOWER of "the 组件 capsule's own line" and "the header's bottom edge": the capsule is what the user reads the rail's position against (measured at 11px from the window top), but the header itself is opaque white at `z-index: 21` while the rail's composer seat is only 7 — so a card drawn above the header's bottom edge is PAINTED OVER, not clipped (measured: parking the rail at the capsule's 11px hid the first 33px of the first row). The default is therefore "as high as the capsule's line allows".
macOS-Dock-style hover magnification with two follow modes (toggle in Settings → Widgets → Settings → Hover Magnify → Realtime follow):
transition: none), so a card's right edge stays flush with the rail even mid-motion — no width/right desync while the pointer moves.The wave's progress is one spring, written every animation frame: displayed = 1 + (target − 1) · p, where target is the live pointer geometry and p springs between rest (0) and engaged (1) on an Apple-parameterised curve (response 200ms, bounce 0.1 — ≈250ms to settle, ~0.15% overshoot ≈0.4px). Both factors are continuous, so enter, follow and leave are one uninterrupted motion in both directions; a pointer that keeps moving only moves target, which can never retarget the curve (two earlier shapes — a plain CSS transition and a frozen target — both failed on exactly that, and both are gone). Releasing runs the same formula backwards, and prefers-reduced-motion writes p in a single frame.
In both modes the magnified deck is painted by a fixed overlay outside the rail's scroll-clip box, so leftward growth escapes clipping while the resting rail width (and the conversation column distance) never changes. Scaling preserves the square card shape and constant spacing; magnification is adjustable in settings (1.0–1.4).
Entering and leaving a card is one continuous move, not two decks cross-fading: at rest the overlay is pixel-identical to the real cards, so the hand-over happens when both agree exactly (invisible), and the following ~250ms writes every card's top/right/width/height together with its scale into the wave on that one spring — and back out on leave, returning to the resting geometry before the real deck takes over. The hovered card keeps its brand-blue outline inside the magnified layer.
Entering is a compositing cost, not a repaint one, and the fix is a permanent hint: the overlay's 15 slots (and the bottom Settings gear) carry will-change: transform at all times, so their layers exist before the pointer arrives — measured with the hint gated on the hover: the natural enter opened with a 63ms layer-promotion + raster frame, and with it permanent the same enter ran 16ms with zero dropped frames. An earlier "raster prewarm" (flipping the overlay visible for 64ms while the rail was idle) has been REMOVED: it almost never coincided with a real hover, and the stall came back on every enter without the permanent hint.
The rail and the magnify overlay are driven by ONE shared pointer surface (they are siblings by necessity — the overlay must escape the rail's scroll-clip box), and that surface is geometrically continuous: while the wave is live the layer itself is hit-capable and its hit box is widened leftwards by the maximum amount the cards overhang the rail (with the left padding compensated so the cards do not move). Cards, the gaps between them, and the strip a magnified card opens past the rail's left edge therefore all stay on the surface — otherwise a gap resolves to the conversation behind, which showed up as "hovering exactly in the gap cancels the wave" and "at the edge it keeps zooming in and out". A genuine leave still ends the wave (with a 6px tolerance so a pixel of jitter cannot flip it), and the wheel over a magnified card still scrolls the rail (intercepted as row-detent scrolling and stopped from reaching the conversation behind).
The overlay is portaled to `` (2026-10-01): it has to out-stack shell.overlay (z-index 20 — that is where harness-ui-harmonizer draws the wrapped center card's chrome), while the rail's own seat is only 7, and no z-index applied inside a stacking context can climb out of it. Portaled, the layer competes at the root context with z-index: 26: above the official right panel (10) and the shell overlay (20), below the settings panel (30). Three consequences, all handled: the pointer focus is driven by a window-level mousemove (the layer is no longer inside that wrapper subtree, so a pointer resting on a magnified card would otherwise freeze the wave), the wheel listener is attached to the layer as well (while it is live its own box covers the whole rail, so the wheel would land on it instead of the rail), and the reveal class for the 设置 tile rides the layer too (its mirrored button lives under `` now).
While an external source (OpenCode usage / Command Code / system monitor) has not answered, a card no longer shows an empty or zeroed body: it paints the silhouette of the body it is waiting for — the title stays the widget's real name, and the placeholder blocks are rounded fills with a 1.5s sweep (static under prefers-reduced-motion). The shape is per widget, so a loading rail is still readable as the cards it is about to show: three rings draw three rounded squares, a bar chart or sparkline draws one large rounded rectangle, a heatmap one wide block, a figure row its short columns and the quota card its three stacked bars; text-only cards keep the figure pill plus body rows. When the data lands it replaces the blocks in place, with no size or position jump.
Card corners are superellipses (corner-shape: squircle), not circular arcs: the curvature ramps in from the straight edges, which is what makes an iOS/Figma-style rounded rectangle read as soft rather than cut. It is native CSS, so the card's border and box-shadow follow the same outline (a clip-path squircle would slice both off); an engine without corner-shape degrades to a plain round corner with nothing else changed.
Both controls live in Settings → Widgets → Config: the switch (default ON, so the two shapes can be compared) and the corner gear — the radius as a share of the card's SHORT side (12 / 16 / 20 / 24 %, default 16%). Percent rather than px, because the reference keeps the corner-to-side ratio: a 150px card and its magnified 190px copy must not get the same corner (12% reproduces the old fixed 16px at side 150). The gear drives the rail's resting and magnified cards, the market / Config previews and the loading skeletons alike.
The general tab groups its rows by the surface each one configures instead of listing them in the order they were added:
Group · Contents Grid & Size · Max columns / Max rows / Max widgets / Base card size / Card gap / Whole cards only Card Look · Continuous corner curvature / Corner gear Hover Magnify · Continuous magnify / Hover magnify Open / Close Animation · Open shape / Open start scale / Position curve / Zoom curve Settings Panel · Panel width Conversation · Hide the stats line under the input box
Max columns, Max rows and Max widgets sit next to each other and are all numeric, because the third is literally the product of the first two (when it is clamped, the row grows a warning mark whose hover note names the number that clamped it). There is exactly one control-choice rule:
Value · Control numeric range · Stepper (the product's own numeric recipe) curated discrete set · Select (the Menu pattern — only the corner gear here) boolean · Switch
That rule had been broken: Max columns was a Select while Max rows, added later, was a Stepper — two widgets for the same kind of value. node scripts/verify-rail-rework.cjs audits it row by row now (both counts must be steppers, the page may hold only one Select, and the group order is asserted), so a new row has to pick its group first — there is no "append at the end".
The Whole-group zoom shape grows the rail out of a top-right anchor: it slides in (the shell's own 0.3s transition, same as the official right panel) *and* scales up from a start value. Those two halves are two elements on two curves: an outer translateX(travel) driven by the position curve, an inner scale(s0) driven by the zoom curve (one transform cannot carry two timing functions; both boxes are inset: 0 fixed, so the rail's coordinates are unchanged under either), each with transform-origin: top right — the viewport's own top-right corner, off the right edge of the screen. The parameters live in Settings → Widgets → Settings (the page's third tab) under Open / Close Animation:
60% / 40%), so a 14-cell deck lands on 9.23ms beats and the last card arrives exactly when the group would have: the cascade starts at the deck's bottom-left cell and hands over to the top-right one, and the collapse retracts in the mirror order — the same timeline played backwards, which is what lets a fast reversal (a close interrupting an open) resume from wherever the fold stands instead of restarting or freezing. Every cell gets its own delay (no cap). So the cards fly in from beyond the rail's right edge (the rail clips its own subtree, so a card is hidden until its turn brings it across), one beat after another, instead of the block arriving in one piece. Pure transform throughout, with no opacity change at all, and the bottom Settings tile is a cell of that same grid, so it folds with the rest. “Whole-group zoom” is the opposite trade: one group-wide slide + scale, everything together.settle × travel (≈ 18px at the default, 91px at 20%) past their seat and spring back. The number is the overshoot itself, not a control point: the position curve's ordinate is solved so the curve's peak clears 1 by exactly that fraction, which is what keeps the two shapes one map — the group's CSS transition and the per-card cascade evaluate the same cubic-bezier. It stays on POSITION on purpose: every curve's ordinate is clamped to [0, 1], because a SCALE past 100% would grow the rail past the column it lives in. The scale is therefore monotone and the settle is the position's. Real springs of this family (Android's DAMPING_RATIO_LOW_BOUNCY = 0.75, motion.dev's default bounce = 0.25 ≈ ζ 0.75) overshoot by 3–5% of their travel with no visible second bounce, which is where the default comes from; the peak lands at ~57% of the curve, where a ζ = 0.70 spring puts its own. Closing plays the same timeline backwards, so the settle leads the leave as a short wind-up instead of trailing it.Each curve *is* its cubic-bezier(): one line, two draggable control points (arrow keys nudge them) plus five presets (Linear / Standard / Ease out / Square root / Smooth). A preset is just a named value of those four numbers, so "Custom" is derived (shown when no preset matches) — there is no second state that could drift from the drawing. The dot under each field replays that curve live: constant speed horizontally (time), the curve vertically (progress).
Whole cards only (default on) fixes a different thing: row-detent scrolling guarantees the top is never cut, but the pane is not a whole number of rows tall, so the bottom card used to be sliced in half. With this on that card is not drawn and not hit-testable (its area is empty band — hovering it must not arm the wave). It is hidden per card slot rather than by clipping the pane: the pane's bottom edge sits only gap (24px by default) below the last whole c