dsh-background-by-model 0.5.2 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,98 @@
1
+ # Changelog
2
+
3
+ > The full release history lives here; the two most recent releases (v0.5.4 / v0.5.3) are also listed in [README.md](./README.md#recent-optimizations).
4
+
5
+ ## v0.5.4
6
+
7
+ - **A brand-filled control takes its label from the host's on-brand ink, not from `brand-text`** — Following the system theme painted the active layout chip, the **+ Add rule** button and the settings brand tile from the host's own `--dsw-alias-brand-primary`, which is a **contrast ink rather than a hue**: near-black (`#0f1115`) in the host's light scheme, near-white (`#f9fafb`) in its dark one. Their labels read `--dsw-alias-brand-text`, which the host defines as that very same color in both schemes and never uses in its own CSS. On a dark system that made them filled boxes with invisible labels — white primary button, white active chip, white icon tile (measured at exactly `#f9fafb`) — and they were only ever readable while the plugin owned the palette and generated its own brand pair. All four now read `--dsw-alias-label-primary-inverted`, the host's own ink for a brand/contrast fill: `#fff` on the light scheme's black fill, `#353638` on the dark scheme's white one.
8
+ - **The toggle's on-state knob was the same bug one size smaller** — a hard-coded `#fff` on a `--dsw-alias-brand-primary` track, i.e. a white knob on the white track the dark scheme paints there.
9
+ - **The plugin's own dark palette had the mirror of it** — that branch deliberately keeps its brand fill at 50%+ lightness, while `--dsw-alias-label-primary-foreground` — the token the **host's** own primary buttons label themselves with over `--dsw-alias-button-primary-fill` — was hard-coded white. It now flips with the fill (one shared verdict with `brand-text`), and the light branch re-emits `label-primary-inverted` so the plugin's palette owns the pair instead of inheriting it from whichever scheme the host happens to be in.
10
+
11
+ ## v0.5.3
12
+
13
+ - **One palette at a time: clearing the theme color no longer leaves the previous skin behind** — The three redirects inside the settings dialog (`--dsw-alias-bg-layer-*` → plugin-owned variables) have **no host fallback**, and neither do the trajectory view's scope or the AppFrame columns' inline backgrounds; all of them were only ever cleaned up at teardown. Switching a rule back to **System theme** therefore left a dark interface with a light dialog and white-on-white text. Every surface is now driven by one palette source: the rule's own tokens while it has a color, and the host's own tokens read back out of the cascade and re-emitted with your alpha when it has none, handed back on the way out. The no-fallback redirects are gated on `html[data-dab-themed]`, so they are only live while the variables they read are actually written.
14
+ - **Following the system theme keeps the wallpaper and the Interface sliders** — Clearing the color used to drop the whole token set, and the host's opaque `--dsw-alias-bg-base` then buried the wallpaper while every opacity/blur slider (main background, sidebar, cards, input, settings panel, trajectory view, right panel) silently stopped working. The surfaces are now the host's own colors times your opacity, so the wallpaper keeps showing through.
15
+ - **Following the system theme hands the host's dark flag back** — While it owns a color the plugin forces `body[data-ds-dark-theme]` (removing it outright for a light palette), and the host only rewrites that flag when it projects a **theme snapshot** — it does not watch the attribute. A flag dropped by a light skin therefore sticks: the readback captures the host's light palette the moment the color is cleared, and the interface only turned dark once an unrelated slider drag re-ran an apply. The switch now hands the flag back synchronously in the host's own boolean form, using the scheme the host resolved (`ctx.theme`'s `active.colorScheme`, falling back to `html[data-ds-theme-source]` plus `prefers-color-scheme`); the one-second watchdog re-reads the host palette while a rule has no color, and the handback side no longer sits behind the 60 ms skin debounce (which made it read the plugin's own skin on its way out).
16
+ - **The no-color button no longer does the opposite of its label** — It read **System theme** while its action was to install a hard-coded dark blue `#1D3463` (the same label means "clear → follow the system theme" one state over). It is now **Custom color**, and it runs the *Extract from this image* pass first, falling back to the seed swatch only when the image has nothing vivid.
17
+ - **The token fingerprint is only recorded after the stylesheet write succeeds**, so a failed write is retried instead of pinning the interface to the old palette until another key happens to change.
18
+ - **Fixed the mojibake dash in the `package.json` description**, which is what the npm page shows.
19
+
20
+ ## v0.5.0
21
+
22
+ - **The host self-check is gone.** Removed together with everything that fed it: the settings page's **Host check** tab, the contract table (`src/host-contracts.ts`), the browser probe (`src/client/judge.ts`), the installed-file scan (`src/host-scan.ts`), the `hostCheck` RPC, the `pnpm scan` script and its test suite. The plugin keeps only what it does: rules, wallpapers and interface opacity/blur.
23
+ - **`0.5.0` is implemented identically in `0.5.1` and `0.5.2`.** Those two are publisher-side noise: the first `npm publish` returned HTTP 202 and the version took a few minutes to appear on the registry, which was mistaken for a failure and re-published twice before the async publish landed. All three are live, byte-identical (12 files, same code), and `latest` pointed at `0.5.2` at that point. Install any of them — pin `0.5.2` if you want that tag's target by name.
24
+
25
+ ## v0.4.5
26
+
27
+ - **`optionalWhen` finally means what its name says** — The field is documented as "the check runs while this surface is on screen", but the probe was evaluating the exact opposite: an anchor that was *not* in the DOM produced *n/a*, and one that *was* produced the failure. Two of the three dock contracts and both conversation-view contracts were therefore testing the wrong state — which is how a healthy dock came back as `停靠空位 失效` with "no element matches `[data-dockkit-empty]`". The helper now reads the anchor as a positive predicate, and every anchor was renamed to say what it means (`dockHasTab`, `dockHasNoTab`, `chatHasMessages`, `paneFloated`, …) so an anchor named for an absence can no longer invert a check silently. Four tests pin the resulting state table in both directions.
28
+ - **A contract about the message column stops firing on a session that has no messages** — `[data-chat-flow]` is rendered only when the session *has* messages, so its absence is the normal state of a brand-new session (the host's `data-content-phase="hero"` screen) and of the trajectory tab. The anchor was "the conversation scrollport exists", which is true in all three states, so the check reported a rename on the hero screen and on **轨迹** — the second failure the panel showed. It is now keyed on `[data-chat-turn]`, the host's own mark on every message row, which is the one thing that is present exactly when a column should exist. Measured: hero → 0 flow / 0 turn rows; after one message → 1 flow / 4 turn rows; trajectory tab → 0 flow / 0 turn rows.
29
+ - **`visible` — presence is not the same question as "on screen"** — A selector check marked `visible: true` now also requires the element to be on screen, so an element the host parks with `visibility:hidden` cannot satisfy a contract about a surface the user is looking at. The dock's empty seat is deliberately *not* marked: the host keeps it rendered and hidden as tabs open and close, so asking whether it is visible would be asking about a state the host uses both ways.
30
+ - **One more anchor rule, written down** — An anchor must name the state in which its attribute is **expected**, never a neighbour of it that happens to appear at the same time. All four false-red bugs in this feature have been the same mistake: a mounted surface that correctly renders nothing for the attribute to sit on.
31
+
32
+ ## v0.4.4
33
+
34
+ - **The dock contracts stop asking a stylesheet question about JS attributes** — `dockkit.content` and `dockkit.float` were `rule` checks ("does a host stylesheet mention this literal?"). The host sets both attributes from JavaScript — `data-dockkit-content` on a tab host that has a tab, `data-dockkit-float` on a pane the user has floated — and never writes a CSS rule for either, so that question could only ever be answered "no". Both are now **presence** checks, guarded by the host's own tab marker (`data-dockkit-tab`) and by float mode respectively, so a dock with no tab open reads as *n/a* rather than as a rename. `dockkit.empty` — the empty seat the host renders in place of a populated tab host — is a presence check too. Verified against a live host: with no tab open `data-dockkit-tab`/`data-dockkit-content` are absent; clicking the dock's `+` makes both appear (tab 0→1, content 0→1, empty 1→0), and the checks then answer instead of skipping.
35
+ - **Anchors no longer depend on combinators** — The populated-tab anchor started out as `[data-dockkit-strip-tabs] > *`, which a DOM stand-in cannot answer and which let the fixture and the browser disagree. It is now a plain attribute selector.
36
+
37
+ ## v0.4.3
38
+
39
+ - **The Host check tab stops crying wolf** — Its first real run on a healthy 0.1.7 install reported **9 failures out of 28 contracts** (7 tokens "no longer declared", 2 attributes gone). Every one was the *probe reading the wrong place*, not the host: the tokens are published as `body{--dsw-…}` while the probe asked `getComputedStyle(document.documentElement)`, and `data-plugin` — which the host's own CSS modules set on every `<style>` they inject — was being used to mean "this plugin's sheet", so the check skipped the entire host theme and then called the tokens renamed. Both are fixed, and both now have tests that fail if they come back: the token root is a named part of `ProbeEnv`, and reading the plugin's own sheets goes through a marker the plugin writes itself (`data-dab-side`), via `getAttribute` rather than `dataset` — `dataset` only exposes the camel-cased key, so a probe indexing it by the dashed name works in a fixture and silently returns nothing in a browser.
40
+ - **`rule` checks now say which side they are about** — A token contract asserts two different things: that the host still *declares* the name, and that it *resolves to a value*. The first can only be answered from the host's rules, the second only from the live cascade, and the one entry whose other half is this plugin's own stylesheet (`.dab-card`) is now marked `side: 'own'` explicitly instead of being indistinguishable from a host rule. Every re-emitted token carries both checks, so a rename is reported as a renamed token rather than as an empty value.
41
+ - **A readable-but-empty host stylesheet is a failure, not a blind spot** — The old code only asked whether *some* sheet was readable, so a host sheet that was readable yet mentioned nothing made every token check pass. Readability is now tracked per side.
42
+
43
+ ## v0.4.2
44
+
45
+ - **New "Host check" tab — the plugin now reports what broke, instead of just breaking** — A plugin that styles someone else's interface depends on values it does not own: the host's session and model services, the elements it hangs backdrop filters on, and the design tokens it re-emits. Every one of those can be renamed between dsh releases, and all three 0.1.7 breakages were *silent* — nothing threw, the interface simply stopped following the settings. Settings → **Host check** now evaluates each of those touch points against the running build — the live DOM, the host's own stylesheets and the Cordis services — and shows, per point, whether it holds, what its loss looks like on screen, the literal it depends on, the host file that should define it and the plugin file that reads it. **Copy diagnostics** turns both halves into one markdown block for an issue. A surface that is merely off screen (the conversation view while the settings dialog is open, the file-preview panel before a file has been opened) is reported as *n/a*, never as a failure.
46
+ - **The same table is checked offline, with no browser** — `pnpm scan` (`scripts/host-contract-scan.mjs`) walks the installed dsh packages and fails non-zero the moment a contract target is gone, so a host rename turns into a red build right after `npm i -g @deepseek-ai/dsh@latest`, before anyone looks at the interface. It also prints the host version it found and whether it satisfies this plugin's floor.
47
+ - **One contract table, three consumers** — `src/host-contracts.ts` declares every touch point exactly once; the browser probe, the node scan and the tests all read it, so a newly added selector cannot be forgotten by the check that is supposed to notice it changing. `pnpm test` covers the table, the probe (against a fixture written in the shape of a host DOM), the UI stylesheet and the version floor.
48
+ - **A broken contract is no longer invisible in a log** — Two and a half seconds after boot, if any contract fails, the browser console gets one line per failure naming the literal, the plugin file that reads it and the host file expected to define it.
49
+
50
+ ## v0.4.1
51
+
52
+ - **The right panel stops painting a permanent frosted plate** — dsh 0.1.7 no longer paints the file-preview panel itself: the container became the slide / fullscreen viewport, stays mounted at its full width for the whole session, and collapsing it only slides the dock out (`transform` + `visibility`, never `display`). A background plus a backdrop-filter on that container therefore covered the empty right half of the frame whenever the panel was closed — the frosted plate 0.1.7 showed. The card now paints the elements the host actually paints (the dock's tab hosts: `[data-dockkit-content]`, `[data-dockkit-empty]`) and rides the host's own open/close and slide transitions; the old panel selector survives behind a `:has()` guard for older hosts, and the backdrop-filter is gated on `[data-sidebar-right-open]` so a collapsed panel is never filtered.
53
+ - **Switching models switches the background again** — 0.1.7's `sessions.list` snapshot no longer publishes `current`, so the plugin could not tell which session was on screen: no session id, no `modelSelection` projection, empty model text, and every model fell through to rule 1. The session id now comes from the host's own `uiSession.current.key` binding, with the legacy `list.current` and a `retainedBy.mainView` row as fallbacks — all optional, so the lookup also works while `uiSession` mounts late. Model switches remain instant, because they ride the per-session `modelSelection` projection subscription rather than a poll.
54
+ - **Menu group headings stop reading as white bands** — 0.1.7 split the menu surface in two: the shared `MenuSurface` material paints the new `--dsw-menu-surface-fill`, and every other overlay paints `--dsw-specific-menu`, which the host aliases to it. The plugin re-emitted only the latter, so a model-selector panel kept the host glass while its sticky provider headings took the plugin's near-opaque palette colour. Both tokens are now generated from one expression and read one plugin-owned variable, so the card slider fades them together and the alias can no longer break.
55
+ - **The card slider's label says what it owns** — It covers menus as well as cards, so the Chinese label changed from 「对话框中选项面板」 to 「卡片与菜单面板」 and the English one from `Cards & panels` to `Cards & menus`.
56
+ - **Declared host raised to `0.1.7-rc.2`** — Every `@deepseek-ai/dsh-*` peer and `engines.dsh` now require 0.1.7-rc.2 or later. The host's compatibility check matches those peers against its own runtime version (`includePrerelease`), so an older host reports the plugin as incompatible by name instead of running it silently. The code's own version fallbacks are unchanged.
57
+
58
+ ## v0.4.0
59
+
60
+ - **Color extraction now measures brightness the way a display does** — The auto light/dark band was decided from the average HSV *value* (the max channel), which reads every saturated pixel as bright: pure blue is `v = 1.0` while its Rec.709 luma is `0.072`. A dark, saturated wallpaper was therefore pushed into the light band and got a light palette built on top of a dark picture. The extractor now averages Rec.709 luma — the same measure the wallpaper verdict always used.
61
+ - **Choosing an image now themes the rule** — Picking or replacing a rule's image extracts its theme color automatically (global toggle, on by default). It only ever fills a rule that has *no* color, so a color you picked — or deliberately cleared — is never overwritten; because the decode is asynchronous the rule is re-checked on arrival, so a color chosen while it ran wins.
62
+ - **Match test card replaces the "Active now" readout** — The status card answered "what is active" and went stale between switches; the tester answers the better question one step earlier. Type any model name (the field follows the detected model until you type) and it reports which rule would match, or that the fallback would. The diagnostics it could not express — the value being *host default* rather than this session's model, and a sessions service that is not up yet — moved into the card as hints, so a broken lookup still cannot masquerade as a correct read.
63
+ - **Config** — The one new persisted field (`autoExtract`) landed in the shared schema, so both halves sanitize it identically.
64
+
65
+ ## v0.3.3
66
+
67
+ - **The config shape is declared once, for both halves** — The node half (which sanitizes every write to disk) and the browser half (the UI's own view of the same file) used to spell the shape out separately: the key lists appeared 6 times across the two halves plus two sets of defaults, so a field added to one side only was silently dropped by the other side's sanitizer — the slider stayed live in memory, `writeConfig` wrote a config without it, and the next load fell back to the default. That is exactly how `blurs.rightbar` / `rightbarOpacity` were lost in 0.3.2. The types, the key lists, the defaults and the pure normalizers now live in one shared module (`src/schema.ts`) that both halves import, and both run the *same* sanitizer, so the UI and the disk copy can no longer disagree.
68
+ - **Unknown config fields are now reported** — While sanitizing a write, the node half logs one warning per key the shared shape does not declare (`ignoring unknown config field "blurs.produced" (declared in one half only?)`), so this class of drift shows up in the host log instead of quietly discarding a setting. Legacy pre-0.3 top-level keys are exempt (the migration consumes them).
69
+ - **No behaviour change** — Defaults, clamps, migration and the persisted file stay byte-for-byte what they were; this release only removes the duplication.
70
+
71
+ ## v0.3.2
72
+
73
+ - **New "File preview panel" card** — The column that slides in from the right when you open a file now has opacity and blur sliders of its own. Its only surface was `--dsw-alias-bg-base` — the very token the Main background slider rewrites — so it used to be an unnamed second copy of the main background with no control of its own. The new card's opacity **follows Main background by default** and only takes over on the first drag, so nothing changes on upgrade; its blur stacks on top of the main-background blur. While the panel is closed it is merely slid off-screen, so the card costs nothing.
74
+ - **Host-side config keeps the new fields** — `theme-config.json` gained `rightbarOpacity` and `blurs.rightbar`. A missing field (any config written before this version, including an imported one) means "follow the main background".
75
+
76
+ ## v0.3.1
77
+
78
+ - **Docs and screenshots refreshed** — The screenshots now show the current three-tab UI: one config under DeepSeek and under Kimi, plus a single rule's editor. The seven old captures (still showing the removed Color tab and the old Background page) are gone, and the image folder dropped from 3.6 MB to 220 kB.
79
+ - **Screenshots ship inside the npm package** — `example_img/` is part of the published tarball now, so the README rendered on npm has no broken images.
80
+ - **Art credit** — The example wallpaper art comes from [ZipZipPipe](https://space.bilibili.com/4168597) on bilibili.
81
+
82
+ ## v0.3.0
83
+
84
+ - **Model rules replaced the single global wallpaper** — The background is now driven by an ordered list of rules, each selected by a match string against the current session's model name (first match wins, rule 1 falls back). The match string is compared against the provider, model id and display name together.
85
+ - **Appearance moved from global to per rule** — Wallpaper, theme color, layout mode, framing, background opacity and background blur are now properties of each rule, so different models can look completely different.
86
+ - **New tab layout** — Settings now has **Interface** (global), **Model Background** (the rule list) and **Config** (import/export). The **Color** tab was removed with the global theme color, and the old **Background** tab became **Model Background**.
87
+ - **Live match readout** — The Model Background tab reports the current model and the rule it resolved to (for example `Current model deepseek-flash → matched · Rule 1`), or a hint when the model can't be detected.
88
+ - **Export format v3** — `dsh-background-by-model-theme.json` now carries the full rule set and every rule's image (base64 inlined).
89
+ - **Breaking removals** — Video wallpapers, generated dynamic backgrounds (mesh gradient / Shader / geometric patterns) and the global theme color are gone.
90
+ - **Automatic legacy migration** — A pre-0.3.0 config becomes rule 1 with an empty match string, inheriting the old appearance, and stale video files are cleaned up.
91
+ - **Smoother switching, with a cross-fade** — Wallpapers are painted from an object URL, so one image is decoded once instead of being re-decoded on every switch, and a rule change now cross-fades (320 ms: the new image fades in over the old one, which stays fully opaque so the backdrop never brightens mid-transition). The drag-time low-resolution wallpaper swap was removed: it re-decoded and re-scaled the whole photo on every change, and its low-res frame was overwritten by the next repaint anyway.
92
+ - **Fixed: switching the model did nothing** — three things stacked up: (1) the old code read `ctx.modelDirectories`, which is not visible from a plugin context (it is registered in ui-model-selection's own scope), so it silently fell back to the host-wide default model; (2) this plugin's client bundle loads **before** `api-session-controller`, so `ctx.get('sessions')` is normally still undefined during `apply()` — and the old code gave up at that point and never retried, which is why nothing happened at all; (3) the readout passed a global default off as this session's model. It now reads **this session's own durable model selection** (`ctx.sessions.binding(id).session.projections.faceOf('modelSelection')` — the same value the model picker renders), waits for the `sessions` service at 400 ms intervals (up to 2 minutes), keeps a 1.5 s safety poll and rebinds on session switches; it labels the value **host default** when only the host-wide default was available, and the readout names the failing hop (not mounted / no current session / no projection / projection empty).
93
+
94
+ ## v0.2.4
95
+
96
+ - **dsh 0.1.5 persistence fixed** — The theme/wallpaper RPC channel is now registered directly in the plugin's own `webServer` scope as a prefix route (keeping the same Host/Origin auth fence), instead of through `connection.rpc.handle`, whose effect binds to the connection service's context and never mounted on some 0.1.5 hosts — requests that previously dropped to the SPA fallback with 405 and never reached the disk now persist again. Verified working on both 0.1.2 and 0.1.5.
97
+ - **Host compatibility declared** — Added `engines.dsh: ">=0.1.2-rc.1"` to declare which DeepSeek Harness host versions the plugin supports.
98
+ - **Dark badge tokens fixed (issue #9)** — In the dark preset, the `*-tertiary` badge surfaces (trajectory tool/context badges, connection pill, plan chip) were tinted nearly the same as their background, making light label text unreadable. They now use the native dark 800/900 steps, so bright text sits on a properly dark badge.
@@ -0,0 +1,98 @@
1
+ # 更新日志
2
+
3
+ > 这里保留完整的历史记录;最近两个版本(v0.5.4 / v0.5.3)同时列在 [README.zh.md](./README.zh.md#近期优化) 里。
4
+
5
+ ## v0.5.4
6
+
7
+ - **填充上的文字改读宿主的「色块墨色」,不再用 `brand-text`** — 跟随系统主题时,布局模式的选中胶囊、「+ 新增规则」按钮和设置页的品牌方块,底色取自宿主自己的 `--dsw-alias-brand-primary`,而它**不是品牌色、是对比墨色**:宿主浅色档近黑(`#0f1115`),深色档近白(`#f9fafb`)。它们的文字读的是 `--dsw-alias-brand-text`,而宿主在两档里都把它定义成与底色**完全相同**的值、自己的 CSS 从不使用。于是在深色系统上就成了有底色却没有文字的方块 —— 白底主按钮、白底选中胶囊、白底图标块(实测正好 `#f9fafb`),只有插件自己持有一套调色板时才看得见。四处现在统一读 `--dsw-alias-label-primary-inverted`,也就是宿主自己画在品牌/对比色块上的墨色:浅色档 `#fff`,深色档 `#353638`。
8
+ - **开关打开态的圆点是同一个毛病的小号版本** — 硬编码 `#fff` 画在 `--dsw-alias-brand-primary` 轨道上,而深色档那条轨道本身就是白的。
9
+ - **插件自建的深色调色板是同一问题的镜像** — 那一档的品牌填充故意保持 50% 以上亮度,而**宿主自己的主按钮**用来写标签的 `--dsw-alias-label-primary-foreground`(画在 `--dsw-alias-button-primary-fill` 上)被写死成白色。现在它随填充亮度翻转(与 `brand-text` 共用一条判定),浅色分支也补上了 `label-primary-inverted`,让插件的调色板自己成对,而不是继承宿主当前恰好所处的档位。
10
+
11
+ ## v0.5.3
12
+
13
+ - **一次只用一个调色板:清空主题色不再留下上一次的皮肤** — 设置弹窗里那三段重定向(`--dsw-alias-bg-layer-*` → 插件私有变量)**没有宿主回退**,轨迹页与三栏的内联背景同理,而它们只在 teardown 时才清理。于是把主题色切回「系统主题」后会留下「深色界面 + 浅色弹窗、白底白字」。现在这些表面由一个调色板来源统一驱动:有主题色时用规则自己的令牌,没有时**读取宿主自己的令牌**再套上你的透明度,切换时一并交还;无回退的重定向用 `html[data-dab-themed]` 门控,只有变量确实写了才生效。
14
+ - **无主题色也保住壁纸与「界面」页的滑块** — 以前清空主题色会撤掉整套令牌,宿主的实心 `--dsw-alias-bg-base` 就把壁纸整块盖住,主背景 / 左侧栏 / 卡片 / 输入框 / 设置面板 / 轨迹页 / 右侧栏的透明度与模糊也全部失效。现在表面是宿主的颜色乘你的透明度,壁纸继续从背后透出来。
15
+ - **跟随系统主题会把宿主的深色标记交还回去** — 插件持有主题色时会强改 `body[data-ds-dark-theme]`(浅色皮肤直接删掉),而宿主只在**主题快照变化时**才重写它、并不监听这个属性。被删掉的标记因此会一直生效:切回系统主题时读到的是宿主的浅色调色板,界面要等你碰一下滑块才变深。现在切换时按宿主自己解析出的方案同步交还标记(`ctx.theme` 的 `active.colorScheme`,取不到则回退 `html[data-ds-theme-source]` + `prefers-color-scheme`),并且每秒的守护定时器在无主题色状态下也会重读一次宿主调色板;「交还」这一侧不再走 60ms 防抖,避免读到插件自己那套即将下线的皮肤。
16
+ - **无颜色状态下的按钮不再做相反的事** — 那个按钮的文案是「系统主题」,动作却是塞一个硬编码的深蓝 `#1D3463`(同一条文案在有颜色时表示「清空 → 跟随系统主题」)。现在它叫 **「自定义颜色」**,并且先跑一次「从本图提取」,取不到鲜明颜色才退回种子色。
17
+ - **令牌指纹只在样式表写入成功后记账** — 写入抛错时不再把指纹提前记下,避免界面卡在旧调色板上直到另一个 key 变化。
18
+ - **`package.json` 描述里的乱码破折号修正**(它直接显示在 npm 页面上)。
19
+
20
+ ## v0.5.0
21
+
22
+ - **宿主自检整块删掉了。** 连同它的一切:设置页的 **「宿主自检」** 选项卡、约定表(`src/host-contracts.ts`)、浏览器探测(`src/client/judge.ts`)、安装目录扫描(`src/host-scan.ts`)、`hostCheck` RPC、`pnpm scan` 脚本以及它的测试。插件只留下它真正做的事:规则、壁纸、界面透明度与模糊。
23
+ - **`0.5.1` 与 `0.5.2` 的内容与 `0.5.0` 完全相同。** 这两个号是发布端的噪音:第一次 `npm publish` 返回的是 HTTP 202,版本要几分钟后才出现在 registry 上,被误当成失败又发了两遍。三个版本都在线上、代码逐字节一致(12 个文件),当时 `latest` 指向 `0.5.2`。装哪个都行;想按标签点名就固定 `0.5.2`。
24
+
25
+ ## v0.4.5
26
+
27
+ - **`optionalWhen` 终于按它名字的意思工作了** — 这个字段的文档写着「只有当这块界面在屏幕上时才检查」,但探测实现的是**反过来的**:锚点**不在** DOM 里时反而报 *未观察*,锚点**在**时才会去报失效。于是三条停靠约定里的两条、以及两条对话视图约定,全都在检查错误的状态 —— 这就是健康的停靠栏会报出「停靠空位 失效 / no element matches `[data-dockkit-empty]`」的原因。现在这个辅助函数把锚点当作**正向断言**来读,并且把所有锚点改名成它们真正的含义(`dockHasTab`、`dockHasNoTab`、`chatHasMessages`、`paneFloated` …),这样「用缺席来命名」的锚点再也不可能悄悄把检查反过来。四个测试把由此得到的状态表两个方向都钉住了。
28
+ - **关于消息列的约定不再对着「还没有任何消息」的会话开火** — `[data-chat-flow]` 只在会话**有消息**时才渲染,所以它不在,恰恰是全新会话(宿主的 `data-content-phase="hero"` 首屏)和轨迹页的正常状态。原来的锚点是「对话滚动区存在」,而这三个状态下它都存在,于是检查在首屏和**轨迹页**都报了「属性改名」—— 也就是面板上你看到的第二个失效。现在锚点改用 `[data-chat-turn]`,宿主给每条消息行打的标记,这是唯一「恰好只在消息列该存在时存在」的东西。实测:首屏 → 消息列 0 / 消息行 0;发一条消息后 → 1 / 4;轨迹页 → 0 / 0。
29
+ - **`visible` —— 「存在」和「在屏幕上」不是同一个问题** — 标记了 `visible: true` 的选择器检查现在要求元素真的在屏幕上,于是宿主用 `visibility:hidden` 停放在一边的元素,无法满足一条关于「用户正在看的界面」的约定。停靠空位**故意不标记**:宿主在开合 tab 时一直把它渲染着并隐藏,去问它可不可见,就是在问一个宿主两个方向都在用的状态。
30
+ - **再写死一条锚点规则** — 锚点必须命名「这个属性**预期出现**的状态」,绝不能是恰好同时出现的邻居。这个功能里四次「误报失效」全都是同一个错误:界面明明挂着、宿主行为也对,只是**没有任何东西**能让那个属性落上去。
31
+
32
+ ## v0.4.4
33
+
34
+ - **停靠相关的约定不再拿「样式表问题」去问 JS 属性** — `dockkit.content` 与 `dockkit.float` 原本是 `rule` 检查(「宿主样式表里有没有提到这个字面量」)。但这两个属性都是宿主从 **JavaScript** 打上去的:`data-dockkit-content` 只在**有 tab** 的停靠页宿主上,`data-dockkit-float` 只在用户真的把面板浮动起来时;宿主从来没为它们写过任何 CSS 规则。所以那个问题无论宿主健不健康都只会回答「没有」。现在两条都改成**存在性**检查,分别用宿主自己的 tab 标记(`data-dockkit-tab`)和浮动态做守卫,于是「没开 tab 的停靠」会报成 *未观察*,而不是「属性改名」。`dockkit.empty`(宿主在「有 tab」时用来替换空位的那个座位)同样改成存在性检查。已在真实宿主上验证:没开 tab 时 `data-dockkit-tab`/`data-dockkit-content` 都不存在;点一下停靠栏的 `+`,两者同时出现(tab 0→1、content 0→1、empty 1→0),此时检查会给出结论而不是跳过。
35
+ - **锚点不再依赖组合符** — 「有 tab」的锚点最初写成 `[data-dockkit-strip-tabs] > *`,这种写法 DOM 替身回答不了,也让夹具和浏览器悄悄产生分歧。现在改成一个普通属性选择器。
36
+
37
+ ## v0.4.3
38
+
39
+ - **「宿主自检」不再虚报故障** — 它第一次在健康的 0.1.7 上实跑,28 条约定报了 **9 条失效**(7 个令牌「不再声明」、2 个属性「消失」)。**全部是探测读错了地方,宿主一条都没坏**:设计令牌发布在 `body{--dsw-…}` 上,探测却去问 `getComputedStyle(document.documentElement)`;而 `data-plugin` —— 宿主自己的 CSS 模块在**每一个** `<style>` 上都写它 —— 被我当成了「这是插件自己的样式表」,于是检查跳过了整个宿主主题,再把令牌报成改名。两处都已修正,并且各配了会让它复发的测试:令牌读取的宿主元素成了 `ProbeEnv` 的显式成员;识别自己的样式表改用一个只由本插件写入的标记(`data-dab-side`),且用 `getAttribute` 而不是 `dataset` —— `dataset` 只暴露驼峰键,按带连字符的名字去索引它,在夹具里能通过、在浏览器里静默返回空。
40
+ - **`rule` 检查现在会说明自己查的是哪一侧** — 一条令牌约定其实断言了两件不同的事:宿主是否仍**声明**这个名字,以及它是否仍**解析出值**。前者只能从宿主的规则回答,后者只能从实时级联回答;而唯一一条「另一半是本插件自己样式表」的(`.dab-card`)现在显式标为 `side: 'own'`,不再和宿主规则混为一谈。所有被重新发出的令牌都同时带上这两项检查,于是「改名」会被报成改名,而不是「值为空」。
41
+ - **可读但没提到目标的宿主样式表算失效,不算盲区** — 旧逻辑只问「有没有任意一张表可读」,于是一张能读、却什么也没提到的宿主表会让所有令牌检查都通过。现在可读性按侧分别跟踪。
42
+
43
+ ## v0.4.2
44
+
45
+ - **新增「宿主自检」页 —— 插件现在能说清哪里坏了,而不只是坏掉** — 一个给别人界面做样式的插件,依赖的全是自己不拥有的东西:宿主的会话与模型服务、挂毛玻璃的那些元素、以及被重新发出的设计令牌。这些在 dsh 版本之间随时可能改名,而 0.1.7 那三次故障**全都是静默的** —— 什么都没抛错,界面只是不再跟随设置。现在 设置 → **宿主自检** 会逐条对当前构建做检查(实时 DOM、宿主自己的样式表、Cordis 服务),并给出:是否成立、失效时界面上是什么样子、依赖的字面量、本该由哪个宿主文件定义、以及本插件哪个文件在读取它。**复制诊断报告** 会把两侧结果合并成一份 markdown,直接贴进 issue。仅仅是「当前不在屏幕上」的情况(设置弹窗打开时的对话页、还没开过文件时的右侧栏)会报成 *未观察*,绝不算故障。
46
+ - **同一张表还能离线查,不需要浏览器** — `pnpm scan`(`scripts/host-contract-scan.mjs`)直接扫已安装的 dsh 包,某个约定目标一消失就以非零码退出;所以 `npm i -g @deepseek-ai/dsh@latest` 之后、还没打开界面之前,就能先红一次。它同时打印找到的宿主版本以及是否满足本插件的下限。
47
+ - **一张约定表,三处消费** — `src/host-contracts.ts` 把每个接触点只声明一次,浏览器探测、离线扫描与测试都读它,所以新加的选择器不可能被「本该发现它变化」的检查漏掉。`pnpm test` 覆盖这张表、探测逻辑(对着一个按宿主 DOM 形状写的夹具)、界面样式表与版本下限。
48
+ - **约定断裂不再只留在日志里没人看** — 启动 2.5 秒后,只要有约定失效,浏览器控制台会逐条打印:字面量、本插件读取它的文件、本应由哪个宿主文件定义。
49
+
50
+ ## v0.4.1
51
+
52
+ - **右侧面板不再糊出一块常驻磨砂板** — dsh 0.1.7 不再自己绘制文件预览面板:容器变成了滑动/全屏视口,整场会话都以全宽挂载,折叠只是把 dock 用 `transform` + `visibility` 滑出去(从不 `display:none`)。于是这个容器上的背景加 backdrop-filter 在面板折叠时照样盖住右半屏——就是那块磨砂板。现在卡片只画宿主真正在画的元素(dock 的 tab host:`[data-dockkit-content]`、`[data-dockkit-empty]`),跟着宿主自己的开合与滑动过渡走;旧的容器选择器留在 `:has()` 守卫后面以兼容老宿主,backdrop-filter 只在 `[data-sidebar-right-open]` 时生效,折叠态绝不走滤镜。
53
+ - **切换模型重新能切背景** — 0.1.7 的 `sessions.list` 快照不再发布 `current`,插件无法得知当前在屏的是哪个会话:拿不到会话 id → 没有 `modelSelection` 投影 → 模型文本为空 → 所有模型都退到规则 1。现在会话 id 先取宿主自己的 `uiSession.current.key`,老字段 `list.current` 与带 `retainedBy.mainView` 的行作为兜底,三跳都是可选的(`uiSession` 挂载晚也能解析)。换模型仍是即时的:它走的是该会话 `modelSelection` 投影的订阅,而不是轮询。
54
+ - **菜单分组标题不再是白道** — 0.1.7 把菜单面拆成了两个 token:共享的 `MenuSurface` 材质画新的 `--dsw-menu-surface-fill`,其他浮层画 `--dsw-specific-menu`,而宿主把后者别名到前者。插件只重新发射了后者,于是模型选择菜单的面板还是宿主玻璃,吸顶的分组标题却变成了插件近乎不透明的调色板色。现在两个 token 由同一个表达式生成、读同一个插件变量,卡片滑块让它们一起变化,别名也不会再被打断。
55
+ - **卡片滑块的标签改名** — 它同时管菜单与卡片,中文标签从「对话框中选项面板」改为「卡片与菜单面板」,英文从 `Cards & panels` 改为 `Cards & menus`。
56
+ - **声明宿主抬到 `0.1.7-rc.2`** — 所有 `@deepseek-ai/dsh-*` peer 与 `engines.dsh` 现在都要求 0.1.7-rc.2 及以后。宿主的兼容性校验会用自身运行时版本比对(带 `includePrerelease`),因此更旧的宿主会按名字报出插件不兼容,而不是静默运行。代码里的版本兜底逻辑未改动。
57
+
58
+ ## v0.4.0
59
+
60
+ - **取色改用「显示器看到的亮度」** — 亮暗带此前按 HSV 明度(max 通道)取平均,它把每个饱和像素都算成亮:纯蓝 `v = 1.0`,而 Rec.709 luma 只有 `0.072`。于是深色但饱和的壁纸被判进亮色带,在暗画面上配出一套浅色界面。现在改为平均 Rec.709 luma——与壁纸判定的同一套度量。
61
+ - **选图即上色** — 选图或换图后自动为规则取主题色(全局开关,默认开)。它**只补空**:你手动选过的颜色(或刻意清空的)绝不会被覆盖;由于解码是异步的,回来时会再确认一次,期间你手选的色优先。
62
+ - **匹配试跑卡取代「当前生效」卡片** — 原状态卡回答的是「现在生效什么」,且会在切换之间留旧值;试跑卡把同一个问题提前一步回答。输入任意模型名(未输入时自动跟随检测到的模型),即时显示会命中哪条规则,或提示走兜底。状态卡里那几句它表达不了的诊断——比如读到的只是**宿主默认**而非本会话模型、会话服务还没就绪——作为提示保留在卡片里,所以坏掉的链路依旧不会伪装成一次正常读取。
63
+ - **配置** — 新增的持久化字段只有一个(`autoExtract`),落在共享 schema 一处,两半边的净化行为因此完全一致。
64
+
65
+ ## v0.3.3
66
+
67
+ - **配置结构只声明一份,两半边共用** — node 半边(落盘前净化)与浏览器半边(UI 自己读同一份文件)此前各写一遍结构:键表在两侧合计 6 处、默认值两套,于是只在一边声明的字段会被另一边的净化静默丢掉——滑块当次有效、`writeConfig` 写下的配置里没有它、刷新即回默认。0.3.2 的 `blurs.rightbar` / `rightbarOpacity` 正是这么丢的。现在类型、键表、默认值与纯净化函数都收在 `src/schema.ts` 一处、两半边共同 import,两侧跑的是**同一份**净化代码,界面与磁盘的取值不可能再不一致。
68
+ - **未知字段会被点名** — node 半边净化写入时,对任何「共用结构里未声明」的字段打一次警告(`ignoring unknown config field "blurs.produced" (declared in one half only?)`),这类不同步会出现在宿主日志里,而不是安静地吞掉一个设置项。0.3 之前的旧顶层字段除外(迁移会消费它们)。
69
+ - **行为不变** — 默认值、夹取范围、迁移结果与落盘文件与此前逐字节一致,本次只消除重复声明。
70
+
71
+ ## v0.3.2
72
+
73
+ - **「文件预览侧栏」卡片** — 点开文件后从右侧滑出的那一栏现在有自己的透明度与模糊度滑块。它此前的唯一表面就是 `--dsw-alias-bg-base`,也就是「主背景」滑块改写的那个令牌,所以它一直是主背景的第二份拷贝、无法单独调节。新卡片的透明度**默认跟随「主背景」**(首次拖动后才由自己接管),因此升级后外观零变化;模糊则叠加在主背景模糊之上。面板关闭时只是滑出屏幕,这张卡片不会带来任何副作用。
74
+ - **宿主侧配置保留新字段** — `theme-config.json` 新增 `rightbarOpacity` 与 `blurs.rightbar`;缺字段(旧配置)一律按「跟随主背景」处理,导入旧配置文件同样如此。
75
+
76
+ ## v0.3.1
77
+
78
+ - **文档与截图更新** — 截图换成当前的三选项卡界面:同一份配置在 DeepSeek 与 Kimi 下的两种外观,外加单条规则的编辑器。旧的七张图(还带着已删除的「色彩」选项卡与旧「背景」页)已删除,图片体积从 3.6 MB 降到 220 KB。
79
+ - **npm 包内附截图** — `example_img/` 现在包含在发布包里,npm 页面上的 README 不再有裂图。
80
+ - **素材致谢** — 示例壁纸素材来源于 bilibili [ZipZipPipe](https://space.bilibili.com/4168597)。
81
+
82
+ ## v0.3.0
83
+
84
+ - **模型规则取代单张全局壁纸** — 背景改由有序规则列表驱动,每条规则用匹配串比对当前会话的模型名来选中(先命中者胜,规则 1 兜底);匹配串会同时比对 provider、模型 id 与显示名三者。
85
+ - **外观从全局下放到每条规则** — 壁纸、主题色、布局模式、取景、背景透明度与背景模糊都成为规则自身的属性,不同模型可以有完全不同的外观。
86
+ - **设置选项卡重构** — 现在是 **「界面」**(全局)、**「模型背景」**(规则列表)与 **「配置」**(导入/导出);**「色彩」** 选项卡随全局主题色一并删除,原 **「背景」** 选项卡改造成 **「模型背景」**。
87
+ - **实时状态提示** — 「模型背景」页会显示当前模型及其命中的规则(例如「当前模型 deepseek-flash → 命中 · 规则 1」),检测不到时给出提示。
88
+ - **导出格式 v3** — `dsh-background-by-model-theme.json` 现在包含全部规则配置**以及**每条规则的图片(base64 内联)。
89
+ - **破坏性移除** — 视频背景、生成式背景(网格渐变 / Shader / 几何图案)与全局主题色均已移除。
90
+ - **旧配置自动迁移** — 0.3.0 之前的配置会变成匹配串留空的规则 1 并继承旧外观,旧版遗留的视频文件会被清理。
91
+ - **切换更顺,并带淡入淡出** — 壁纸改用 object URL 绘制,同一张图只解码一次,不再每次切换重新解码;规则切换现在是 320 ms 交叉淡入(新图在旧图上方淡入,底层始终保持不透明,中途不会发亮)。同时移除了拖动滑块时的低清壁纸降级:它会在每次壁纸变化时对整张大图额外解码并缩放,而且它换上的低清图下一帧就被重新覆盖、实际并未生效。
92
+ - **修复“切换模型没反应”** — 三处叠加:① 旧实现读取的 `ctx.modelDirectories` 在插件上下文里不可见(它注册在 ui-model-selection 自己的作用域),于是悄悄退回了宿主全局默认模型;② 本插件的客户端 bundle **早于** `api-session-controller` 加载,`ctx.get('sessions')` 在 `apply()` 时通常还不存在,旧代码在这一步直接放弃、永不重试(这是“完全没反应”的直接原因);③ 状态行拿一个全局默认值冒充了本会话模型。现在改读**本会话自己的持久化模型选择**(`ctx.sessions.binding(id).session.projections.faceOf('modelSelection')`,即模型选择器读的同一份值),以 400 ms 间隔等待 `sessions` 服务出现(最多 2 分钟),带 1.5 秒兜底轮询与会话切换重绑定;只拿到宿主默认值时标注 **宿主默认**,并且状态行会指出是哪一环失败(未挂载 / 无当前会话 / 无投影投影为空)。
93
+
94
+ ## v0.2.4
95
+
96
+ - **修复 dsh 0.1.5 持久化失效** — 主题/背景图的 RPC 通道改为在插件自身 `webServer` 作用域内直接注册为 prefix 路由(保留 Host/Origin 鉴权围墙),不再经由 `connection.rpc.handle`(其 effect 绑定到 connection 服务上下文,导致部分 0.1.5 宿主挂载失败)。此前请求落到 SPA 兜底并返回 405、从未写入磁盘的设置,现可正常持久化(0.1.2 与 0.1.5 均已验证)。
97
+ - **声明宿主版本兼容** — 新增 `engines.dsh: ">=0.1.2-rc.1"`,声明插件所支持的 DeepSeek Harness 宿主版本范围。
98
+ - **修复深色徽章令牌(issue #9)** — 深色预设下 `*-tertiary` 徽章表面(轨迹工具/上下文徽章、连接胶囊、计划 chip)与背景色过于接近,浅色标签文字不可读;现已对齐原生深色 800/900 阶令牌,亮色文字落在正确深色徽章上。
package/README.md CHANGED
@@ -11,7 +11,7 @@ English | [中文](README.zh.md)
11
11
 
12
12
  A **DeepSeek Harness** appearance plugin built around an **ordered list of model rules**. Each rule carries its own wallpaper, theme color, layout mode, framing, opacity and blur — switch the model, and the background switches with it.
13
13
 
14
- > **v0.3.0 is a breaking release.** Video wallpapers, generated dynamic backgrounds and the single global theme color were removed. See [Upgrading from 0.2.x](#upgrading-from-02x).
14
+ > **v0.3.0 is a breaking release.** Video wallpapers, generated dynamic backgrounds and the single global theme color were removed. See the [v0.3.0 entry in CHANGELOG.md](./CHANGELOG.md#v030).
15
15
 
16
16
  ---
17
17
 
@@ -79,14 +79,14 @@ One interface, one config — only the current model differs:
79
79
  - **Per-rule Appearance** — Every rule owns its wallpaper, theme color, layout mode, framing, opacity and blur. Nothing is shared globally except the Interface tab.
80
80
  - **Import / Export** — Export every rule **including its wallpaper** to a `dsh-background-by-model-theme.json` (format version 3, images inlined as base64) and restore it anywhere.
81
81
  - **File-based Persistence** — All settings and images are stored on the filesystem under `~/.dsh/.dsh-background-by-model-data/`, not `localStorage`.
82
- - **Automatic Migration** — Old single-wallpaper configs are upgraded in place on first read. See [Upgrading from 0.2.x](#upgrading-from-02x).
82
+ - **Automatic Migration** — Old single-wallpaper configs are upgraded in place on first read. See the [v0.3.0 entry in CHANGELOG.md](./CHANGELOG.md#v030).
83
83
  - **Bilingual** — Full Chinese / English UI with automatic locale detection.
84
84
  - **Theme Watchdog** — Re-asserts the custom theme if the host resets it.
85
85
 
86
86
  ### Per-rule settings
87
87
 
88
88
  - **Wallpaper** — Upload any image as this rule's wallpaper; each rule keeps its own file.
89
- - **Theme Color** — HSL wheel plus numeric input and an inspiration palette, with **Extract from this image** and an **eyedropper**. When a rule sets no theme color, the system theme is used. Generates the full CSS design-token set in real time.
89
+ - **Theme Color** — HSL wheel plus numeric input and an inspiration palette, with **Extract from this image** and an **eyedropper**. When a rule sets no theme color, the system theme is used. Generates the full CSS design-token set in real time. Following the system theme hands the palette back to the host while the wallpaper and the Interface-tab opacity/blur sliders keep working — the surface colors are read back from the host's own tokens and re-emitted with your alpha, so the wallpaper is never buried under an opaque plate.
90
90
  - **Layout Mode** — Fit / Fill / Stretch / Tile / Center.
91
91
  - **Framing** — Drag to pan and scroll to zoom inside a viewport-proportional editor; **only editable in Fit mode**, and the committed framing stays consistent across window resizes and cross-monitor moves.
92
92
  - **Background Opacity** — `0–100%` for the rule's wallpaper layer.
@@ -94,7 +94,7 @@ One interface, one config — only the current model differs:
94
94
 
95
95
  ### Global settings (Interface tab)
96
96
 
97
- - **Per-part Interface Opacity** — Independent sliders for the main background, left panel, right panel, cards & panels (including the dropdowns and menus around the dialog), the input & controls (composer box, Cordis panel), plus the settings panel and the conversation text box.
97
+ - **Per-part Interface Opacity** — Independent sliders for the main background, left panel, right panel, cards & panels (including the dropdowns and menus around the dialog), the input & controls (composer box, Cordis panel), plus the settings panel and the conversation text box. Independent of the theme color: a rule with no color keeps every one of these sliders live.
98
98
  - **Per-part Interface Blur** — Frosted-glass `backdrop-filter` blur (`0–60 px`) for each interface part, including a real backdrop on the composer and Cordis panel via stable host selectors.
99
99
  - **Conversation & Trajectory** — The message list is wrapped in a translucent card automatically, and the trajectory page gets whole-page opacity & blur controls, letting the wallpaper shine through the content.
100
100
  - **Right Panel** — The column that slides in from the right when you open a file now has a card of its own: its opacity follows **Main background** until you drag it (then this card owns it), and its blur stacks on top of the main-background blur. While closed it takes no space and costs nothing.
@@ -111,7 +111,7 @@ Settings → **Theme** now has three tabs:
111
111
 
112
112
  The old **Color** tab is gone — the theme color is now a property of each rule. The old **Background** tab became **Model Background**.
113
113
 
114
- ## Storage & Migration
114
+ ## Storage
115
115
 
116
116
  Data directory: `~/.dsh/.dsh-background-by-model-data/` (Windows: `C:\Users\<you>\.dsh\.dsh-background-by-model-data\`)
117
117
 
@@ -120,16 +120,6 @@ Data directory: `~/.dsh/.dsh-background-by-model-data/` (Windows: `C:\Users\<you
120
120
  | `theme-config.json` | The rule list plus the global Interface settings |
121
121
  | `modelbg-<slot>` | The image of each rule, stored as raw bytes without a file extension |
122
122
 
123
- ### Upgrading from 0.2.x
124
-
125
- v0.3.0 **removed** three things:
126
-
127
- - **Video wallpapers** — no longer supported.
128
- - **Generated dynamic backgrounds** — mesh gradients, Shader and geometric patterns are gone.
129
- - **The global theme color** — it moved into each rule.
130
-
131
- Upgrading is otherwise automatic and non-destructive: on first read, a legacy config (one without a `rules` array) is migrated so that the old single `wallpaper.jpg` becomes **rule 1** with an **empty match string** (a pure fallback), inheriting the old theme color, layout mode, opacity, blur and framing. Leftover video files from the old version are cleaned up as well. Existing users keep the appearance they had — you only need to add more rules if you want a different background per model.
132
-
133
123
  ## Installation
134
124
 
135
125
  ### Method 1: npm install (Recommended)
@@ -182,12 +172,6 @@ After changing anything under `src/`, run `pnpm run bundle` again — the mounte
182
172
 
183
173
  `pnpm test` builds and typechecks (`tsdown && tsc -p tsconfig.json`).
184
174
 
185
- ## Compatibility
186
-
187
- - **dsh `>=0.1.7-rc.2`** — Declared in `peerDependencies` (every `@deepseek-ai/dsh-*` entry) and mirrored in `engines.dsh`. The host evaluates those peers against its own runtime version, so an older host reports the plugin as incompatible instead of running it silently.
188
- - **[`dsh web`](https://github.com/deepseek-ai/deepseek-harness)** — Full support on both the npm release and the new source build. The plugin auto-detects which client-module table the host ships (the new `@deepseek-ai/dsh-client-store` or the legacy `@deepseek-ai/dsh-client-runtime`) and resolves `defineStore` accordingly at runtime.
189
- - **[deepseek-harness-desktop](https://github.com/anywhere-labs/deepseek-harness-desktop)** — Supported
190
-
191
175
  ## FAQ
192
176
 
193
177
  **Every model looks the same — why?**
@@ -205,86 +189,33 @@ Yes — export it from the **Config** tab; the JSON inlines every rule's image.
205
189
  **Are video or generated backgrounds supported?**
206
190
  No. Both were removed in 0.3.0; each rule uses a static image.
207
191
 
208
- ## Recent Optimizations
209
-
210
- ### v0.5.0
211
-
212
- - **The host self-check is gone.** Removed together with everything that fed it: the settings page's **Host check** tab, the contract table (`src/host-contracts.ts`), the browser probe (`src/client/judge.ts`), the installed-file scan (`src/host-scan.ts`), the `hostCheck` RPC, the `pnpm scan` script and its test suite. The plugin keeps only what it does: rules, wallpapers and interface opacity/blur. What it cost to remove is in the diff — 28 contracts, three consumers and a report format, none of which ever agreed with reality for long enough to be worth the red lines it printed on a healthy host.
213
-
214
- ### v0.4.5
215
-
216
- - **`optionalWhen` finally means what its name says** — The field is documented as "the check runs while this surface is on screen", but the probe was evaluating the exact opposite: an anchor that was *not* in the DOM produced *n/a*, and one that *was* produced the failure. Two of the three dock contracts and both conversation-view contracts were therefore testing the wrong state — which is how a healthy dock came back as `停靠空位 失效` with "no element matches `[data-dockkit-empty]`". The helper now reads the anchor as a positive predicate, and every anchor was renamed to say what it means (`dockHasTab`, `dockHasNoTab`, `chatHasMessages`, `paneFloated`, …) so an anchor named for an absence can no longer invert a check silently. Four tests pin the resulting state table in both directions.
217
- - **A contract about the message column stops firing on a session that has no messages** — `[data-chat-flow]` is rendered only when the session *has* messages, so its absence is the normal state of a brand-new session (the host's `data-content-phase="hero"` screen) and of the trajectory tab. The anchor was "the conversation scrollport exists", which is true in all three states, so the check reported a rename on the hero screen and on **轨迹** — the second failure the panel showed. It is now keyed on `[data-chat-turn]`, the host's own mark on every message row, which is the one thing that is present exactly when a column should exist. Measured: hero → 0 flow / 0 turn rows; after one message → 1 flow / 4 turn rows; trajectory tab → 0 flow / 0 turn rows.
218
- - **`visible` — presence is not the same question as "on screen"** — A selector check marked `visible: true` now also requires the element to be on screen, so an element the host parks with `visibility:hidden` cannot satisfy a contract about a surface the user is looking at. The dock's empty seat is deliberately *not* marked: the host keeps it rendered and hidden as tabs open and close, so asking whether it is visible would be asking about a state the host uses both ways.
219
- - **One more anchor rule, written down** — An anchor must name the state in which its attribute is **expected**, never a neighbour of it that happens to appear at the same time. All four false-red bugs in this feature have been the same mistake: a mounted surface that correctly renders nothing for the attribute to sit on.
220
-
221
- ### v0.4.4
192
+ **After switching a rule back to "System theme", why is the settings dialog a different palette from the rest of the interface?**
193
+ That was a bug, and it is fixed: clearing the color only dropped the `body` token set, while the three **no-host-fallback** redirects inside the dialog (`--dsw-alias-bg-layer-*` → plugin-owned variables), the trajectory view and the AppFrame columns' inline backgrounds all kept the previous color — a dark interface with a light dialog and white-on-white text. Every surface is now driven by a single palette source: the rule's own tokens while it has a color, and the host's own tokens (read back and re-emitted with your alpha) while it follows the system theme, handed back to the host on the way out.
222
194
 
223
- - **The dock contracts stop asking a stylesheet question about JS attributes** — `dockkit.content` and `dockkit.float` were `rule` checks ("does a host stylesheet mention this literal?"). The host sets both attributes from JavaScript — `data-dockkit-content` on a tab host that has a tab, `data-dockkit-float` on a pane the user has floated — and never writes a CSS rule for either, so that question could only ever be answered "no". Both are now **presence** checks, guarded by the host's own tab marker (`data-dockkit-tab`) and by float mode respectively, so a dock with no tab open reads as *n/a* rather than as a rename. `dockkit.empty` — the empty seat the host renders in place of a populated tab host — is a presence check too. Verified against a live host: with no tab open `data-dockkit-tab`/`data-dockkit-content` are absent; clicking the dock's `+` makes both appear (tab 0→1, content 0→1, empty 1→0), and the checks then answer instead of skipping.
224
- - **Anchors no longer depend on combinators** — The populated-tab anchor started out as `[data-dockkit-strip-tabs] > *`, which a DOM stand-in cannot answer and which let the fixture and the browser disagree. It is now a plain attribute selector.
195
+ **Why does "Custom color" produce a color the moment I press it?**
196
+ Pressing it means leaving the system-theme state, so it has to hand out a starting color. It runs the same **Extract from this image** pass first and only falls back to a default swatch when the image has nothing vivid, so a press no longer jumps from a light interface straight to a dark blue that has nothing to do with the wallpaper.
225
197
 
226
- ### v0.4.3
198
+ **After switching back to "System theme" the interface stays light until I drag a slider — why?**
199
+ The other half of the same hole: while it owns a color the plugin **forces** `body[data-ds-dark-theme]` (setting its marker for a dark palette, removing the attribute for a light one), and the host only rewrites that flag when it projects a **theme snapshot** — it does not watch the attribute. A flag dropped by a light skin therefore sticks, so the readback captured the host's light palette at the moment the color was cleared and nothing ever re-read it until an unrelated apply ran. Now the switch hands the flag back synchronously in the host's own boolean form, the scheme the host resolved (`ctx.theme`'s `active.colorScheme`, falling back to `html[data-ds-theme-source]` plus `prefers-color-scheme`) is pushed into the render layer, and the one-second watchdog re-reads the host palette while a rule has no color, repainting when it moved.
227
200
 
228
- - **The Host check tab stops crying wolf** — Its first real run on a healthy 0.1.7 install reported **9 failures out of 28 contracts** (7 tokens "no longer declared", 2 attributes gone). Every one was the *probe reading the wrong place*, not the host: the tokens are published as `body{--dsw-…}` while the probe asked `getComputedStyle(document.documentElement)`, and `data-plugin` — which the host's own CSS modules set on every `<style>` they inject — was being used to mean "this plugin's sheet", so the check skipped the entire host theme and then called the tokens renamed. Both are fixed, and both now have tests that fail if they come back: the token root is a named part of `ProbeEnv`, and reading the plugin's own sheets goes through a marker the plugin writes itself (`data-dab-side`), via `getAttribute` rather than `dataset` — `dataset` only exposes the camel-cased key, so a probe indexing it by the dashed name works in a fixture and silently returns nothing in a browser.
229
- - **`rule` checks now say which side they are about** — A token contract asserts two different things: that the host still *declares* the name, and that it *resolves to a value*. The first can only be answered from the host's rules, the second only from the live cascade, and the one entry whose other half is this plugin's own stylesheet (`.dab-card`) is now marked `side: 'own'` explicitly instead of being indistinguishable from a host rule. Every re-emitted token carries both checks, so a rename is reported as a renamed token rather than as an empty value.
230
- - **A readable-but-empty host stylesheet is a failure, not a blind spot** — The old code only asked whether *some* sheet was readable, so a host sheet that was readable yet mentioned nothing made every token check pass. Readability is now tracked per side.
231
-
232
- ### v0.4.2
233
-
234
- - **New "Host check" tab — the plugin now reports what broke, instead of just breaking** — A plugin that styles someone else's interface depends on values it does not own: the host's session and model services, the elements it hangs backdrop filters on, and the design tokens it re-emits. Every one of those can be renamed between dsh releases, and all three 0.1.7 breakages were *silent* — nothing threw, the interface simply stopped following the settings. Settings → **Host check** now evaluates each of those touch points against the running build — the live DOM, the host's own stylesheets and the Cordis services — and shows, per point, whether it holds, what its loss looks like on screen, the literal it depends on, the host file that should define it and the plugin file that reads it. **Copy diagnostics** turns both halves into one markdown block for an issue. A surface that is merely off screen (the conversation view while the settings dialog is open, the file-preview panel before a file has been opened) is reported as *n/a*, never as a failure.
235
- - **The same table is checked offline, with no browser** — `pnpm scan` (`scripts/host-contract-scan.mjs`) walks the installed dsh packages and fails non-zero the moment a contract target is gone, so a host rename turns into a red build right after `npm i -g @deepseek-ai/dsh@latest`, before anyone looks at the interface. It also prints the host version it found and whether it satisfies this plugin's floor.
236
- - **One contract table, three consumers** — `src/host-contracts.ts` declares every touch point exactly once; the browser probe, the node scan and the tests all read it, so a newly added selector cannot be forgotten by the check that is supposed to notice it changing. `pnpm test` covers the table, the probe (against a fixture written in the shape of a host DOM), the UI stylesheet and the version floor.
237
- - **A broken contract is no longer invisible in a log** — Two and a half seconds after boot, if any contract fails, the browser console gets one line per failure naming the literal, the plugin file that reads it and the host file expected to define it.
238
-
239
- ### v0.4.1
240
-
241
- - **The right panel stops painting a permanent frosted plate** — dsh 0.1.7 no longer paints the file-preview panel itself: the container became the slide / fullscreen viewport, stays mounted at its full width for the whole session, and collapsing it only slides the dock out (`transform` + `visibility`, never `display`). A background plus a backdrop-filter on that container therefore covered the empty right half of the frame whenever the panel was closed — the frosted plate 0.1.7 showed. The card now paints the elements the host actually paints (the dock's tab hosts: `[data-dockkit-content]`, `[data-dockkit-empty]`) and rides the host's own open/close and slide transitions; the old panel selector survives behind a `:has()` guard for older hosts, and the backdrop-filter is gated on `[data-sidebar-right-open]` so a collapsed panel is never filtered.
242
- - **Switching models switches the background again** — 0.1.7's `sessions.list` snapshot no longer publishes `current`, so the plugin could not tell which session was on screen: no session id, no `modelSelection` projection, empty model text, and every model fell through to rule 1. The session id now comes from the host's own `uiSession.current.key` binding, with the legacy `list.current` and a `retainedBy.mainView` row as fallbacks — all optional, so the lookup also works while `uiSession` mounts late. Model switches remain instant, because they ride the per-session `modelSelection` projection subscription rather than a poll.
243
- - **Menu group headings stop reading as white bands** — 0.1.7 split the menu surface in two: the shared `MenuSurface` material paints the new `--dsw-menu-surface-fill`, and every other overlay paints `--dsw-specific-menu`, which the host aliases to it. The plugin re-emitted only the latter, so a model-selector panel kept the host glass while its sticky provider headings took the plugin's near-opaque palette colour. Both tokens are now generated from one expression and read one plugin-owned variable, so the card slider fades them together and the alias can no longer break.
244
- - **The card slider's label says what it owns** — It covers menus as well as cards, so the Chinese label changed from 「对话框中选项面板」 to 「卡片与菜单面板」 and the English one from `Cards & panels` to `Cards & menus`.
245
- - **Declared host raised to `0.1.7-rc.2`** — Every `@deepseek-ai/dsh-*` peer and `engines.dsh` now require 0.1.7-rc.2 or later. The host's compatibility check matches those peers against its own runtime version (`includePrerelease`), so an older host reports the plugin as incompatible by name instead of running it silently. The code's own version fallbacks are unchanged.
246
-
247
- ### v0.4.0
248
-
249
- - **Color extraction now measures brightness the way a display does** — The auto light/dark band was decided from the average HSV *value* (the max channel), which reads every saturated pixel as bright: pure blue is `v = 1.0` while its Rec.709 luma is `0.072`. A dark, saturated wallpaper was therefore pushed into the light band and got a light palette built on top of a dark picture. The extractor now averages Rec.709 luma — the same measure the wallpaper verdict always used.
250
- - **Choosing an image now themes the rule** — Picking or replacing a rule's image extracts its theme color automatically (global toggle, on by default). It only ever fills a rule that has *no* color, so a color you picked — or deliberately cleared — is never overwritten; because the decode is asynchronous the rule is re-checked on arrival, so a color chosen while it ran wins.
251
- - **Match test card replaces the "Active now" readout** — The status card answered "what is active" and went stale between switches; the tester answers the better question one step earlier. Type any model name (the field follows the detected model until you type) and it reports which rule would match, or that the fallback would. The diagnostics it could not express — the value being *host default* rather than this session's model, and a sessions service that is not up yet — moved into the card as hints, so a broken lookup still cannot masquerade as a correct read.
252
- - **Config** — The one new persisted field (`autoExtract`) landed in the shared schema, so both halves sanitize it identically.
253
-
254
- ### v0.3.3
255
-
256
- - **The config shape is declared once, for both halves** — The node half (which sanitizes every write to disk) and the browser half (the UI's own view of the same file) used to spell the shape out separately: the key lists appeared 6 times across the two halves plus two sets of defaults, so a field added to one side only was silently dropped by the other side's sanitizer — the slider stayed live in memory, `writeConfig` wrote a config without it, and the next load fell back to the default. That is exactly how `blurs.rightbar` / `rightbarOpacity` were lost in 0.3.2. The types, the key lists, the defaults and the pure normalizers now live in one shared module (`src/schema.ts`) that both halves import, and both run the *same* sanitizer, so the UI and the disk copy can no longer disagree.
257
- - **Unknown config fields are now reported** — While sanitizing a write, the node half logs one warning per key the shared shape does not declare (`ignoring unknown config field "blurs.produced" (declared in one half only?)`), so this class of drift shows up in the host log instead of quietly discarding a setting. Legacy pre-0.3 top-level keys are exempt (the migration consumes them).
258
- - **No behaviour change** — Defaults, clamps, migration and the persisted file stay byte-for-byte what they were; this release only removes the duplication.
259
-
260
- ### v0.3.2
261
-
262
- - **New "File preview panel" card** — The column that slides in from the right when you open a file now has opacity and blur sliders of its own. Its only surface was `--dsw-alias-bg-base` — the very token the Main background slider rewrites — so it used to be an unnamed second copy of the main background with no control of its own. The new card's opacity **follows Main background by default** and only takes over on the first drag, so nothing changes on upgrade; its blur stacks on top of the main-background blur. While the panel is closed it is merely slid off-screen, so the card costs nothing.
263
- - **Host-side config keeps the new fields** — `theme-config.json` gained `rightbarOpacity` and `blurs.rightbar`. A missing field (any config written before this version, including an imported one) means "follow the main background".
264
-
265
- ### v0.3.1
201
+ ## Recent Optimizations
266
202
 
267
- - **Docs and screenshots refreshed** — The screenshots now show the current three-tab UI: one config under DeepSeek and under Kimi, plus a single rule's editor. The seven old captures (still showing the removed Color tab and the old Background page) are gone, and the image folder dropped from 3.6 MB to 220 kB.
268
- - **Screenshots ship inside the npm package** — `example_img/` is part of the published tarball now, so the README rendered on npm has no broken images.
269
- - **Art credit** — The example wallpaper art comes from [ZipZipPipe](https://space.bilibili.com/4168597) on bilibili.
203
+ > Only the two most recent releases are listed here; older ones (v0.5.0 and earlier) live in [CHANGELOG.md](./CHANGELOG.md).
270
204
 
271
- ### v0.3.0
205
+ ### v0.5.4
272
206
 
273
- - **Model rules replaced the single global wallpaper** — The background is now driven by an ordered list of rules, each selected by a match string against the current session's model name (first match wins, rule 1 falls back). The match string is compared against the provider, model id and display name together.
274
- - **Appearance moved from global to per rule** — Wallpaper, theme color, layout mode, framing, background opacity and background blur are now properties of each rule, so different models can look completely different.
275
- - **New tab layout** — Settings now has **Interface** (global), **Model Background** (the rule list) and **Config** (import/export). The **Color** tab was removed with the global theme color, and the old **Background** tab became **Model Background**.
276
- - **Live match readout** — The Model Background tab reports the current model and the rule it resolved to (for example `Current model deepseek-flash → matched · Rule 1`), or a hint when the model can't be detected.
277
- - **Export format v3** — `dsh-background-by-model-theme.json` now carries the full rule set and every rule's image (base64 inlined).
278
- - **Breaking removals** — Video wallpapers, generated dynamic backgrounds (mesh gradient / Shader / geometric patterns) and the global theme color are gone.
279
- - **Automatic legacy migration** — A pre-0.3.0 config becomes rule 1 with an empty match string, inheriting the old appearance, and stale video files are cleaned up.
280
- - **Smoother switching, with a cross-fade** — Wallpapers are painted from an object URL, so one image is decoded once instead of being re-decoded on every switch, and a rule change now cross-fades (320 ms: the new image fades in over the old one, which stays fully opaque so the backdrop never brightens mid-transition). The drag-time low-resolution wallpaper swap was removed: it re-decoded and re-scaled the whole photo on every change, and its low-res frame was overwritten by the next repaint anyway.
281
- - **Fixed: switching the model did nothing** — three things stacked up: (1) the old code read `ctx.modelDirectories`, which is not visible from a plugin context (it is registered in ui-model-selection's own scope), so it silently fell back to the host-wide default model; (2) this plugin's client bundle loads **before** `api-session-controller`, so `ctx.get('sessions')` is normally still undefined during `apply()` — and the old code gave up at that point and never retried, which is why nothing happened at all; (3) the readout passed a global default off as this session's model. It now reads **this session's own durable model selection** (`ctx.sessions.binding(id).session.projections.faceOf('modelSelection')` — the same value the model picker renders), waits for the `sessions` service at 400 ms intervals (up to 2 minutes), keeps a 1.5 s safety poll and rebinds on session switches; it labels the value **host default** when only the host-wide default was available, and the readout names the failing hop (not mounted / no current session / no projection / projection empty).
207
+ - **A brand-filled control takes its label from the host's on-brand ink, not from `brand-text`** — Following the system theme painted the active layout chip, the **+ Add rule** button and the settings brand tile from the host's own `--dsw-alias-brand-primary`, which is a **contrast ink rather than a hue**: near-black (`#0f1115`) in the host's light scheme, near-white (`#f9fafb`) in its dark one. Their labels read `--dsw-alias-brand-text`, which the host defines as that very same color in both schemes and never uses in its own CSS. On a dark system that made them filled boxes with invisible labels — white primary button, white active chip, white icon tile (measured at exactly `#f9fafb`) — and they were only ever readable while the plugin owned the palette and generated its own brand pair. All four now read `--dsw-alias-label-primary-inverted`, the host's own ink for a brand/contrast fill: `#fff` on the light scheme's black fill, `#353638` on the dark scheme's white one.
208
+ - **The toggle's on-state knob was the same bug one size smaller** — a hard-coded `#fff` on a `--dsw-alias-brand-primary` track, i.e. a white knob on the white track the dark scheme paints there.
209
+ - **The plugin's own dark palette had the mirror of it** — that branch deliberately keeps its brand fill at 50%+ lightness, while `--dsw-alias-label-primary-foreground` — the token the **host's** own primary buttons label themselves with over `--dsw-alias-button-primary-fill` — was hard-coded white. It now flips with the fill (one shared verdict with `brand-text`), and the light branch re-emits `label-primary-inverted` so the plugin's palette owns the pair instead of inheriting it from whichever scheme the host happens to be in.
282
210
 
283
- ### v0.2.4
211
+ ### v0.5.3
284
212
 
285
- - **dsh 0.1.5 persistence fixed** — The theme/wallpaper RPC channel is now registered directly in the plugin's own `webServer` scope as a prefix route (keeping the same Host/Origin auth fence), instead of through `connection.rpc.handle`, whose effect binds to the connection service's context and never mounted on some 0.1.5 hosts — requests that previously dropped to the SPA fallback with 405 and never reached the disk now persist again. Verified working on both 0.1.2 and 0.1.5.
286
- - **Host compatibility declared** — Added `engines.dsh: ">=0.1.2-rc.1"` to declare which DeepSeek Harness host versions the plugin supports.
287
- - **Dark badge tokens fixed (issue #9)** — In the dark preset, the `*-tertiary` badge surfaces (trajectory tool/context badges, connection pill, plan chip) were tinted nearly the same as their background, making light label text unreadable. They now use the native dark 800/900 steps, so bright text sits on a properly dark badge.
213
+ - **One palette at a time: clearing the theme color no longer leaves the previous skin behind** — The three redirects inside the settings dialog (`--dsw-alias-bg-layer-*` → plugin-owned variables) have **no host fallback**, and neither do the trajectory view's scope or the AppFrame columns' inline backgrounds; all of them were only ever cleaned up at teardown. Switching a rule back to **System theme** therefore left a dark interface with a light dialog and white-on-white text. Every surface is now driven by one palette source: the rule's own tokens while it has a color, and the host's own tokens read back out of the cascade and re-emitted with your alpha when it has none, handed back on the way out. The no-fallback redirects are gated on `html[data-dab-themed]`, so they are only live while the variables they read are actually written.
214
+ - **Following the system theme keeps the wallpaper and the Interface sliders** — Clearing the color used to drop the whole token set, and the host's opaque `--dsw-alias-bg-base` then buried the wallpaper while every opacity/blur slider (main background, sidebar, cards, input, settings panel, trajectory view, right panel) silently stopped working. The surfaces are now the host's own colors times your opacity, so the wallpaper keeps showing through.
215
+ - **Following the system theme hands the host's dark flag back** — While it owns a color the plugin forces `body[data-ds-dark-theme]` (removing it outright for a light palette), and the host only rewrites that flag when it projects a **theme snapshot** — it does not watch the attribute. A flag dropped by a light skin therefore sticks: the readback captures the host's light palette the moment the color is cleared, and the interface only turned dark once an unrelated slider drag re-ran an apply. The switch now hands the flag back synchronously in the host's own boolean form, using the scheme the host resolved (`ctx.theme`'s `active.colorScheme`, falling back to `html[data-ds-theme-source]` plus `prefers-color-scheme`); the one-second watchdog re-reads the host palette while a rule has no color, and the handback side no longer sits behind the 60 ms skin debounce (which made it read the plugin's own skin on its way out).
216
+ - **The no-color button no longer does the opposite of its label** — It read **System theme** while its action was to install a hard-coded dark blue `#1D3463` (the same label means "clear → follow the system theme" one state over). It is now **Custom color**, and it runs the *Extract from this image* pass first, falling back to the seed swatch only when the image has nothing vivid.
217
+ - **The token fingerprint is only recorded after the stylesheet write succeeds**, so a failed write is retried instead of pinning the interface to the old palette until another key happens to change.
218
+ - **Fixed the mojibake dash in the `package.json` description**, which is what the npm page shows.
288
219
 
289
220
  ## Star History
290
221