Skip to content

Releases: Physicolor/dsh-widgets

v1.6.1

Choose a tag to compare

@Physicolor Physicolor released this 19 Sep 09:15

dsh-widgets v1.6.1 — the rail scrolls by whole rows, stops at its own content, and releases the hover every time

A fix release. Three rail defects that survived v1.6.0 are gone. Every claim below was measured against the live GUI at 1578×1000 (15 widgets, 2 columns, pitch 184px) and is re-checked by three probe scripts that ship in scripts/.

Fixed — the wheel scrolls in whole rows

  • The last rows could never reach the top. The rail's scrollable range was the deck's own height minus the viewport, which is shorter than the last row's offset (measured: range 750px with 8 rows × pitch 184), so the browser clamped the final detents and the deck stopped moving while the wheel kept turning. The scrollable content is now derived from the geometry — contentH = max(rows · pitch, stackHeight) + railViewportHeight — with the difference reserved by a non-interactive tail inside the rail.
  • Half-scrolled rests. The wheel's own native scroll lands on the compositor before the event reaches the main thread (measured: scrollTop was already 120 when the handler ran), so an interrupted animation could leave the deck a third of a row up. The rail now runs its own 240ms tween that always ends on a detent, plus a row guard that snaps any off-grid rest position back onto 2 + row · pitch.
  • The dependency array that cancelled the tween. The wheel effect listed railElRef — a plain { current } object rebuilt on every parent render — so every parent re-render tore the effect down and its cleanup called stopTween() mid-flight.
  • Line- and page-mode wheels did nothing or jumped nine rows. LINE/PAGE deltas are discrete: exactly one event is one detent, whatever its magnitude.

Added — the scroll stops when the components end

  • A threshold instead of an endless tail. A row may top out only while its own cards still reach into the viewport: lastRow = ceil((contentBottom + viewportHeight − rowSeat) / pitch) − 1. Measured: 8 rows, deepest card bottom 1450px, viewport 936px → the deck stops at the 7th detent (1290px) and further notches change nothing (1290 → 1290).

Fixed — the magnification wave always ends when the pointer leaves

  • The safety net was cancelling itself. A window-level mousemove guard had been added to end the wave when the pointer is no longer over a tile — but its effect had no dependency array, so while the wave was live (a re-render per frame) every cleanup cleared the pending release timer. It now mounts once and reads its state from refs.
  • "On the widgets" now means ON A TILE, not "inside the rail". The rail's box is 372 × 936 and mostly empty — its padding, the 24px gaps between cards and every blank run below the last row are all inside it — so the old box test kept the wave alive over gaps, above the first row and right of the last card. Tiles are tested individually with a 7px halo.
  • Verified on six exit paths (scripts/verify-rail-interaction.cjs), each one checked with document.elementFromPoint so an endpoint that is genuinely over a card is never mistaken for a defect.

Also in this release

  • The trajectory widget (官方「轨迹」三色泳道的卡片版) that had been sitting in the tree since the previous version.
  • Three verification scripts: verify-rail-interaction.cjs, verify-rail-scroll.cjs, verify-rail-wheel-matrix.cjs (8 wheel input styles), plus verify-release-smoke.cjs.

中文摘要

修复版本。v1.6.0 之后仍存在的三个组件栏缺陷全部消除,每条都在真实 GUI 上实测(1578×1000,15 个组件,2 列,行距 184px)。

  • 滚轮按整行滚动:一档恰好一行(2 + row · pitch),走组件栏自己的 240ms 补间,并有行守卫把偏离网格的静止位置吸附回网格;LINE/PAGE 模式按离散量处理(一个事件 = 一行),不再「完全不动」或「一次跳九行」;最后几行此前永远顶不到顶(范围被 clamp)。
  • 滚到组件结束就停lastRow 按「最深卡片是否还能进入视口」计算——8 行时 deck 停在第 7 个挡位(1290px),继续滚动完全不动。
  • 悬停离开必定复位:window 级指针守卫此前会在每次重渲染时清掉自己的释放定时器(永远没机会执行);判据也从「在组件栏里」改为「在卡片上」(逐张卡片 + 7px 光晕)。六条退出路径全部复验。
  • 顺带发布此前留在工作树里的 trajectory 组件。

Full entry, with every measurement behind each change: CHANGELOG.md · 中文版 CHANGELOG.zh-CN.md

v1.6.0 — rail geometry from the transcript measure, panel swallow, hover-jank fix

Choose a tag to compare

@Physicolor Physicolor released this 18 Sep 11:48

dsh-widgets v1.6.0 — the rail obeys the transcript measure, and the right panel truly swallows it

Full entry, with every measurement behind each change: CHANGELOG.md · 中文版 CHANGELOG.zh-CN.md

Added — geometry derived from the product's own transcript measure

  • Budget = official conversation column width − official transcript measure − 74px box inset (reads --dsh-conversation-column-width / --dsh-chat-content-width, with a live getBoundingClientRect fallback), plus a degradation ladder: fewer columns → narrower single column → yield entirely (--dsx-rail-w: 0, nothing drawn, the user's preferences untouched, and the rail returns by itself when the window grows).
  • The transcript keeps the product's own measure at every viewport: 748px at 1578/1400/1280 and 664px at 1120, where the rail takes 372/198/154/0. The inset rule lost its !important — the rail can now only collapse, never squeeze the reading column below the product's own floor.
  • The header capsule shows data-space="tight" (dimmed, aria-disabled, not-allowed) when there is no room, instead of opening an empty rail.

Changed — the right panel swallows the rail instead of the rail fading beside it

  • The rail's host moved from shell.overlay to conversation.input.overlay: probed with elementFromPoint, a fixed element inside shell.overlay paints above the official panel (z-20 vs z-10), while inside the conversation subtree it paints below it. The panel's edge now sweeps across the rail and uncovers it again on the way back.
  • One variable (--dsx-rail-right) drives the rail, the magnify overlay and the settings drawer; while a panel exists the rail pins to the viewport's right edge, and [data-yielded] / [data-no-room] stop the invisible surfaces from capturing pointer events.
  • Track and occupant share one curve: the transcript inset uses the official 0.3s duration/easing tokens instead of 0.2s ease, and the overlay composer seat follows the same rule.

Changed — native follow via CSS anchor positioning

  • anchor-name: --dsx-center on the conversation host; measured per frame |column right − rail right| = 0.00px through a full open/close.
  • The ResizeObserver that had been observing zero elements (it was constructed during apply(), before the shell mounted its frame) is re-bound lazily and self-heals; the rail predicts the track's final width from the AppFrame's transitionrun, so it no longer trails the panel by ~100ms.
  • Boot no longer plays the enter animation when the preference says "open" but the session is not known yet — that first-paint slide painted the rail at the left edge for ~2.6s before jumping.

Fixed

  • Hovering the rail no longer re-renders the whole rail on every pointer frame (RailWave extraction, transform: scale() about a top-right origin, class-based deck fade): p50 frame 12.5ms → 4.2ms, p95 41.8ms → 8.3ms, frames > 26ms 15 → 1, JS during 2.6s of hovering 598ms → 193ms.
  • A new conversation could leave the rail painted over the hero (5/5 reproductions, still intercepting pointer events after 6s): the bridge now runs on useSyncExternalStore and body.dsx-stats-no-session hides the drawer instantly — 5/5 non-interactive within ~300ms afterwards.
  • The turn navigator lost its hover and click to the product's invisible 40px column-width drag band (z-index: 8 vs 7): the hit-test order is fixed ([data-conversation-scroll] :has(> nav[aria-label]) { z-index: 9 }), not the geometry; hover now resolves to the navigator and a click no longer starts a width drag.
  • heatmap-bars first/last date labels wrapped in their ~14px column (the text is 17px wide at 9px type) and pushed the bars out of the 150px card — they carry white-space: nowrap now. The rail's root typography is pinned (16px / line-height: normal) so the transcript's 14px/24px can no longer leak into the cards.
  • context-water 2×2 no longer bursts its 150px slot: the legend rows carry an explicit 1.2 line-height and the segmented bar's bottom gap drops 10 → 6px (card back to 150px, chart 76.17px of 79px available).
  • The Components capsule no longer changes place after a bundle reload (order: 10 tied with dsh-better-sidebar's bottom-panel toggle; it registers at 5 now).

Website

The showcase is now a design-system site: design principles bound to real widgets, the DSH Widget Design Grammar (real constants plus an interactive rail running the plugin's own magnification curve), Widget Anatomy annotated from measured DOM rectangles, a 13-rule DSH Visual Audit scored over all 33 widgets, and real SEO (canonical, Open Graph, Twitter card, JSON-LD ItemList, sitemap.xml) generated from the manifests by website/gen-site.mjs. Verify: 80/80 checks.

Known cost

With the rail open at ≤1600px the yield pushes the transcript scroller below the product's 900px container query, so DSH hides its own turn navigator. Keeping both (a 2-column rail and an always-visible navigator) would require narrowing only the message content and not the scroll box — deliberately not done in this release.

中文摘要

  • 组件栏宽度不再固定 372px:改为由官方「对话列宽 − 正文 measure − 74px」推导,并按阶梯降级(少一列 → 单列收窄 → 整个让位)。四档视口下正文始终保住官方 measure(748 / 748 / 748 / 664),组件栏分别占 372 / 198 / 154 / 0;原先硬占 372px 会把正文压到 554 / 394。
  • 右侧栏打开时真正「吞掉」组件栏:宿主从 shell.overlay 迁入对话子树(elementFromPoint 实测层级在面板之下),面板推进时逐段覆盖、退走时逐段露出;轨道与占用方共用同一条曲线(官方 0.3s token,不再是 0.2s ease)。
  • 跟随改为原生 CSS anchor positioning:逐帧 |列右缘 − rail 右缘| = 0.00px;此前那个「观察了 0 个元素」的 ResizeObserver 改为惰性重绑并自愈,同时用 AppFrame 的 transitionrun 预测本帧目标宽度,不再晚一拍;开机恢复也不再播放入场动画(曾把组件栏画在屏幕左缘约 2.6s)。
  • 悬浮不再整栏重渲染:p50 12.5ms → 4.2ms、p95 41.8ms → 8.3ms、超 26ms 帧 15 → 1、2.6s 悬浮内 JS 598ms → 193ms。
  • 修复:新建对话后组件栏残留在页面上(5/5 复现 → 修复后 5/5 在 ~300ms 内不可交互);轮次导航被 40px 拖动带抢走 hover 与点击(改的是命中优先级,不是几何);热度图柱状图首尾日期标签在 ~14px 列里折行把柱子顶出 150px 卡片(nowrap + 钉死组件栏根排版,防止对话正文 14px/24px 渗入);context-water 2×2 超出 slot 6px(legend 行高 1.2、分段条下间距 10 → 6px);组件胶囊因 order 与 better-sidebar 撞车而在重载后移位(改为 5)。
  • 官网:从组件画廊升级为设计系统站点——设计原则绑定真实组件、Widget Design Grammar(真实常量 + 跑插件自身放大曲线的交互轨道)、Widget Anatomy(从实测 DOM 矩形标注)、33 个组件 × 13 条规则的视觉审计,以及由 manifest 生成的真实 SEO(canonical / Open Graph / JSON-LD ItemList / sitemap)。验证 80/80。
  • 已知代价:≤1600px 且组件栏展开时,正文滚动盒会低于官方 900px 容器查询阈值,DSH 会隐藏自己的轮次导航(要两者兼得需只收窄消息内容、不收窄滚动盒,属结构性改造,本版未做)。

v1.5.0 — Quota Manager, live day data, text-level alerts

Choose a tag to compare

@Physicolor Physicolor released this 15 Sep 04:40

Highlights

New — 额度管理 / Quota Manager (2×2, Coding Plan group). A monthly-window card: the projected month-end percent sits top-right, the billing period rides the second line, and two figures anchor the bottom — today's usage vs today's recommended budget.

  • Projection = used% + recent pace × days left, the pace taken over the last 3 day-equivalents (two whole days plus today, prorated; today only counts after 6 h). The projection and the budget are therefore the same statement: projected > 100% ⇔ pace > budget — the card can no longer say "today is under budget" and "the month lands over 100%" at once.
  • Past 100% the figure itself turns red and breathes (1.6 s); the card-wide red glow is gone.
  • The balance is converted at the period's realised local tokens ÷ credits rate, so every figure shares one caliber. Missing inputs render 数据不足 instead of an invented quota.

Fixed — daily usage now moves with every turn. The day map comes from usage-center's log fold, which rescans on its own ~30 s cadence, so a finished turn kept showing the previous figure (measured: ~0.4M behind an independent log fold). The client keeps its per-step counter running and takes max(index, local) for today only — history stays authoritative — and the host route accepts ?refresh=1 to rescan usage-center on demand.

Fixed — Command Code monthly window. used / (used + remaining) summed two unrelated snapshots (17.26 + 59.01 = 76.27 on a $70 plan → 22.6% vs the account page's ~16%). Now (allowance − remaining) / allowance with the published plan table.

Changed — one text-level alert. The card glow mechanism was removed entirely; peak pricing's EXPENSIVE and Quota Manager's over-100% now turn the value red and breathe (static under prefers-reduced-motion).

Layout — the card head row was restructured. Two independent, top-aligned slots: a 20px value no longer stretches the line box and pushes the 13px title ~5px lower than other cards; the right slot cancels its own extra line height so the caption hugs the title. New figures chart kind (label/value pairs sharing the head row's gutters).

Showcase. The gallery is complete at 33 widgets (it had stalled at 24, missing the whole 8-widget Command Code family), with new Command Code / Device filters, and the widget table is now generated from the manifests by website/sync-data.mjs. Headless site verification: 44/44.

Compatibility. DeepSeek Harness 0.1.5: useChat/useSession snapshot split, official right-bar (--dsx-rightbar-w) adaptation, and the retired @deepseek-ai/dsh-client-runtime dependency dropped.

中文摘要

  • 新增「额度管理」组件(2×2,Coding Plan 分组):右上角月末用量预测百分比 + 账期日期 + 底部「今日用量 / 今日推荐」;预测与推荐数学同调(预测 > 100% ⇔ 近期速率 > 推荐速率),超 100% 时数值变红呼吸。
  • 修复每日用量不更新:客户端逐 step 本地计数与索引对「今天」取 max,每轮对话结束即刷新;host 路由支持 ?refresh=1 立即重扫用量中心。
  • 修复 Command Code 月窗口口径:改用 (额度 − 剩余) / 额度,与官网一致。
  • 告警统一为文字红闪:删除整套卡片红色内晕(alert / .dsx-peak-alert)。
  • 标题行重构:左右两个独立 slot 顶对齐,账期行紧贴标题;新增 figures 图表类型。
  • 展示页补齐 33 个组件(此前 24,缺整族 Command Code)并新增两个筛选。

Verification

docs/probe-quota-manage.mjs (live math + independent recompute), docs/verify-quota-manage-render.cjs (real render layer through react-dom/server), docs/verify-quota-manage-layout.cjs (headless layout measurements), docs/verify-usage-freshness.mjs (merge contract + host value vs an independent log fold), website/verify.mjs (44/44).

Full diff: v1.4.1...v1.5.0

v1.4.1

Choose a tag to compare

@Physicolor Physicolor released this 04 Sep 15:45

System monitor widgets family - local hardware dashboards (device group):

  • sys-cpu / sys-gpu (2x2 big-figure cards, click/dropdown big-metric switch)
  • sys-rings (CPU/GPU twin rings), sys-board (2x4 all-rings dashboard)
  • sys-gpu-line (Windows-task-manager style utilization sparkline,
    sample window 10-30pts, default 20) + /api/sysinfo host route
  • P1 crash guard: one malformed payload can no longer hide the rail
  • Rail drawer animation + usage decimal fix (folded from prior working tree)
  • Publishing: npm Trusted Publishing (OIDC) workflow - tag v* auto-publishes

v1.2.4

Choose a tag to compare

@Physicolor Physicolor released this 28 Aug 07:55

v1.2.4

Fix — rail open/close glide sync (main-thread right transition):

  • v1.2.3 moved rail glide to compositor transform (zero drops) but compositor and main-thread animations split on busy frames ("rail glides first, conversation lags"). The fix returns rail to the same transition: right path as the conversation column: same-path animations cannot split by definition.
  • Measured: rail↔conversation right-edge std=0 (perfect lockstep); dropped frames 0%; left-edge drift 0; single-frame steps 220px→71px.

README English-only cleanup:

  • All Chinese component names replaced with English equivalents throughout changelog (peak-pricing, usage-bars, usage-rings, Components, etc.).

v1.2.3

Choose a tag to compare

@Physicolor Physicolor released this 28 Aug 05:51

v1.2.3

核心 — 右面板开合动画掉帧修复(与 dsh-better-sidebar / dsh-ui-harmonizer 协同):

  • rail 与放大 overlay 侧移改为合成器 transform 平移(right:0 + translateX(---dsh-sidebar-width)),rail 子树零逐帧 reflow
  • 移除卡片 slot 常驻 will-change(两套 deck × N 卡片的合成层常驻清空)
  • 放大 overlay 卡片体仅真正放大时渲染,静息 DOM 减半

掉帧数值对比(playwright + 本机 Edge 实测,右面板开合动画窗口):

场景 修复前 修复后
重会话动画掉帧率(>26ms/帧占比) 20–31% ≈ 0–1.4%
主线程长任务 单次 60–210ms、每动画数个 0
组件栏开启 vs 关闭 明显(打开即卡) 无差异(零额外成本)
滑动路径 right/margin 逐帧 layout(全树 reflow) transform 合成平移

→ 组件常开下开关右面板与关闭组件同样丝滑(典型会话全程 60fps);重会话残余 ~10% 来自对话列宽度过渡(UI 协调层,Roadmap)。

新增 — 多 Key 用量联动(配合 dsh-multikey-pool): /api/opencode-usage-multi 逐 Key 拉取用量、总 Key 共同用量、单击整卡循环切换视图、切 Key 联动多 Key 池主力。
新增 — 全面中英文适配: 接入官方 locale 服务全量字典驱动(设置/组件栏/市场/表单等),语言即时切换无需刷新,locale 缺席自动回退内置字典。

dsh-widgets v1.2.2

Choose a tag to compare

@Physicolor Physicolor released this 27 Aug 03:00

dsh-widgets v1.2.2

A batched release combining several widget improvements: two new widgets, a data-viz rework of the usage bars, preview state toggling and dark-mode fixes.

New — 峰谷定价 (peak-pricing) widget

  • 2×2-only market widget showing whether right now is inside a DeepSeek V4 peak-pricing window (Beijing time, Mon–Fri 09:00–12:00 & 14:00–18:00; custom schedules on the roadmap).
  • Bottom-left big label mirrors the cache/tokens card: red EXPENSIVE in a peak window, CHEAP otherwise.
  • During a peak window the whole card glows with a gentle breathing red inner glow (2.2s, modest swing; prefers-reduced-motion gets a static glow). The live window row under the title lights up brand-blue.
  • A 30s always-on tick keeps the peak/off-peak state fresh even with no turn running.

New — OpenCode usage rings widget (用量环图)

  • One donut per window (rolling / weekly / monthly) side by side — same data as the usage bars, circle form. Clean ring centres let the strokes run thick (5px); percents sit under each ring, exact values on hover. The existing bars widget coexists unchanged.

Changed — usage bars proportioning (用量对比)

  • Elastic equal columns with a 4px gutter; each bar fills ~60% of its column (~24px on a 2×2) instead of fat full-width blocks. Fully rounded corners; no value labels — hover tooltip for the exact percent plus faint dashed 25/50/75% reference lines.

Improved — preview state toggling + dark-mode arrow fix

  • Click the preview card to flip a stateful widget's state (峰谷定价 peak/off-peak) without waiting for the real window; declared via the new simToggle descriptor.
  • Fixed .dsx-select chevron not rendering in dark mode: fill='currentColor' in a background-image data-URI SVG draws nothing; explicit light/dark fills now (near-white under body[data-ds-dark-theme]).

Fixed

  • Filled action buttons readable in dark mode again (business/error fill tokens instead of near-white brand token).
  • Add-panel height no longer collapses when dsh-better-sidebar's right panel is open.
  • 2×4 tiles correctly masked in a 1-column layout (strike-through + yellow "1列不可用" capsule + disabled add in the market).

中文摘要

本批次合并发布多项改动:

  • 新增「峰谷定价」组件(2×2):实时判断 DeepSeek V4 高峰时段(北京时间工作日 09:00–12:00 / 14:00–18:00),高峰时整卡呼吸式红色内晕 + 红色 EXPENSIVE 大标签,低峰 CHEAP;当前时段行品牌蓝高亮;30s 常驻心跳保证窗口边界准时翻转。
  • 新增「用量环图」组件:滚动/周/月三窗口环形图,环条加粗、百分比置于环下、悬停显示精确值;与「用量对比」柱状图并存。
  • 「用量对比」柱状图比例重构:弹性等分列 + 4px 间距,柱宽约列 60% 并全圆角;不贴数值,悬停出精确值 + 25/50/75% 淡参考线。
  • 预览可点击切换状态:峰谷定价预览点击卡片即可切换高峰/低峰查看两种形态(simToggle 机制)。
  • 修复.dsx-select 下拉箭头深色模式不可见(currentColor 在背景 SVG 中不渲染);深色模式实心按钮文字不可见;Better Sidebar 开启时添加面板高度塌缩;1 列布局下 2×4 组件屏蔽提示。

完整说明见 README(英文/简体中文)。

v1.2.1

Choose a tag to compare

@Physicolor Physicolor released this 24 Aug 12:45

Fixed — the last edit is now flushed to the host store on page close, so widget state survives ANY desktop shell.

  • Root cause: the 400ms-debounced host PUT had no unload flush; closing the window within that window (or while the PUT was in flight) lost the write with the page. Desktop builds that spawn a fresh random loopback origin per launch make localStorage a new realm every boot — that single missed PUT meant changes do not save on desktop.
  • Fix: pagehide now flushes pending state via navigator.sendBeacon (keepalive-fetch fallback); the host route already accepts PUT/POST. The debounced PUT also gained keepalive: true.
  • Verified end-to-end against the real host store: edit + immediate pagehide (~80ms, inside the debounce) → real beacon persisted, host savedAt matches localStorage, no duplicate debounce fetch, user state restored after test.

修复 — 页面关闭时把最后一次修改同步冲入 host 存储,组件状态在任何桌面壳/浏览器/设备下都不再丢失。

v1.2.0

Choose a tag to compare

@Physicolor Physicolor released this 23 Aug 03:15

v1.2.0 — the big UI overhaul + heatmap/bars fixes (single unified release, supersedes 1.1.6/1.2.1)

Includes every change since v1.1.5 — the market rework, hover engine, personalization, and the token-usage fixes.

  • 🧰 Add-only market: pick a widget (2×2 and 2×4 are independent entries), size via arrows/dropdown, one-click 添加; no install/uninstall zone; groups 系统 / OpenCode Go / Coding Plan 用量 / 其它.

  • 🎨 Preview parity: heatmap previews via the same grid path as the live card (correct 7×13 orientation), 2×4 previews scale to fit, config preview fills free space, quote previews with sample text.

  • 🔄 Dock-style hover engine rework: card-anchored trigger (armed on actual card hits, kept across gaps, disarmed on leaving the rail), gap-gliding wave, phase-based transitions (instant realtime follow = aligned edges + exact gaps; tweened enter/exit), right edge never drifts.

  • 🏠 Personalization: fresh installs pre-load only the stats-line family; new 隐藏输入框下方文字条 switch (hides labels, keeps space); tasks never vanish (暂无任务); market card layout; limit warning floats.

  • 💾 Host-persisted state (from v1.1.5 lineage): widget config survives restarts/origins; upgrade-fidelity regression suite.

  • 🗓️ Heatmap: timezone-aware day attribution (记账时区, default Beijing UTC+8), no more cross-session over-credit, one-shot cleanup.

  • 📊 Usage bars: 7-day window normalization (tallest bar of the week always full) + weekly alignment option.

  • 🔧 Capsule button styling restored (CSS BOM fix); 4-column add-button overlap fixed (and it stays in its gap slot while hovering).

  • 🧰 市场改为只添加(组件按尺寸独立条目、一键添加),无安装/卸载分区;分组 系统/OpenCode Go/Coding Plan 用量/其它

  • 🔄 悬浮引擎重构:以卡片为锚、空隙滑行、进出平滑,右缘恒对齐

  • 🏠 默认只预装文字条组件;新增隐藏文字条开关;任务常驻

  • 🗓️ 热度图按时区记账(默认北京)+ 修复跨会话误记

  • 📊 柱状图窗口内归一化 + 每周对齐

  • 📐 修复 4 列添加按钮重叠/跳动;胶囊样式 BOM 修复

v1.1.5

Choose a tag to compare

@Physicolor Physicolor released this 22 Aug 09:19

v1.1.5 — widget state now survives restarts (bug-fix patch)

Fixed: configuration was only stored in browser localStorage (per-origin cache) — it reset on origin switches (localhost vs 127.0.0.1), private mode, site-data clearing, or silently dropped writes, and never appeared on other devices.

  • Host-persisted state: new GET/PUT /api/widgets-state route stores the rail configuration atomically (tmp + rename) at profiles/web/dsh-widgets-state.json under the profile data dir — one authoritative copy per DSH service, shared by every browser/address that reaches it.

  • Boot + live sync: the client syncs on boot (savedAt newest wins between localStorage and the host file) and writes every change to both channels (localStorage immediately, host via 400 ms debounced PUT).

  • Cross-tab / multi-origin live updates: a storage event re-reads the configuration in sibling tabs; switching back to a tab re-pulls the host store, so multi-window sessions (including localhost vs 127.0.0.1) converge live.

  • Compatibility: existing harness-widgets.* localStorage keys untouched; unknown/legacy stored shapes normalize identically for both channels; devices stay independent by design (per-machine state file, no cloud sync).

  • 修复:组件配置此前只存浏览器 localStorage,重启/换 origin/清数据/静默写入失败都会丢,其它设备也读不到

  • Host 新增 /api/widgets-state 原子落盘,设备级持久保存;启动与实时双通道同步

  • 跨标签页/跨 origin 实时收敛;heatmap 账本仍本地;设备间独立保存(默认无云同步)