@nonamelego/dsh-catppuccin 0.6.4 → 0.6.6-beta.0

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/README.en.md CHANGED
@@ -56,7 +56,7 @@ glass skin on Web / Desktop, and the four official theme palettes auto-synced to
56
56
 
57
57
  It ships all four Catppuccin flavours — **Latte**, **Frappé**, **Macchiato**, **Mocha** —
58
58
  remapping the whole interface to the matching palette, with a **settings section of its own**
59
- (**Settings → Catppuccin**: flavour, code highlighting, token overrides, glass, update check).
59
+ (**Settings → Catppuccin**: flavour, code highlighting, token overrides, glass, update check, feedback).
60
60
  Your choice is persisted and restored after restarts.
61
61
 
62
62
  It also includes an optional **glassmorphism** skin: the top bar, sidebar, composer,
@@ -69,7 +69,7 @@ Catppuccin theme automatically.
69
69
  - 🎨 Four themes: Latte (light), Frappé / Macchiato / Mocha (dark)
70
70
  - 🧩 Registered into the official theme system, on a par with the built-in light / dark / system themes
71
71
  - 🎯 Full-UI colour coverage — not just one or two accent colours
72
- - ⚙️ One-click switch in Settings, persisted and restored across restarts
72
+ - ⚙️ One-click switch on its own Settings section, persisted and restored across restarts
73
73
  - 🔧 **Custom token overrides**: override individual colour tokens with `--dsw-* var: value` pairs (e.g. turn comments blue); persisted alongside the selected flavour
74
74
  - 🖍️ **Code-block highlight style**: default / italic-comments shiki themes
75
75
  - 🌐 Seven UI languages (Chinese / English / Japanese / Korean / Spanish / French / German, follows the system language)
@@ -80,7 +80,7 @@ Catppuccin theme automatically.
80
80
  (interaction reference: [DSH-Transparent-UI-Plugin](https://github.com/WYH66666666/DSH-Transparent-UI-Plugin))
81
81
  - 🌫️ **Glass details**: gradient blur bands at the top/bottom page edges, a floating glass
82
82
  rail when the sidebar is collapsed, an ambient theme-following ground (a same-family
83
- wash + two soft glows) — content softens as it scrolls under the panes
83
+ wash; the two soft glows are an **opt-in switch, off by default**) — content softens as it scrolls under the panes
84
84
  - 🎨 Glass colours follow the current Catppuccin theme
85
85
  - 🔄 **Update check**: one-click "Check for updates" in Settings compares the latest npm
86
86
  version and gives you a copyable upgrade command; **auto-check is on by default** (once at
@@ -260,6 +260,14 @@ The **Glass** section lives on the **Settings → Catppuccin** page:
260
260
  - **Backdrop brightness**: dark mode darkens 0–50, light mode brightens 50–100 (50 = as-is),
261
261
  mixed straight into the ground; at the extremes the ambience fades out and the ground
262
262
  collapses back to pure white / pure black.
263
+ - **Ground glows**: adds two screen-pinned soft glows on top of the ground (the `0.6.3` look),
264
+ **off by default** — they do not follow the panes, so each pane transmits a different slice
265
+ of them and the top-left corner brightens visibly when on.
266
+ - **In-place previews**: a **ground sample strip** in the glass section (drag brightness or
267
+ toggle the glows and it changes on the spot — it is painted by the page ground's own rule);
268
+ a **code sample** under the highlight picker (reads `--shiki-token-*`, so switching to
269
+ italic-comments recolours it immediately); and **Show the UI** next to the mode picker,
270
+ which closes Settings so you can look at the real layout.
263
271
 
264
272
  Glass colours follow the active theme live; all settings persist across restarts.
265
273
 
@@ -296,8 +304,9 @@ What this plugin does:
296
304
  bubbles and the new-session button; in Mica mode they become rounded floating cards and
297
305
  chat content scrolls *under* the glass, blurred; the collapsed sidebar becomes a
298
306
  floating rail at the edge of the chat area (the "performance & usage" stats line is
299
- *not* one of them — it stays stock, outside the glass card; see the `0.6.2`
300
- "fused band removed" entry in the changelog);
307
+ *not* a glass card and stays outside it, but since `0.6.4` it sits on a **frosted
308
+ ground-coloured strip** so the scrolling transcript cannot read through the readouts —
309
+ see the `0.6.2` "fused band removed" entry and issue #29 in `0.6.4`);
301
310
  - **Page-edge gradient blur**: a blur band at the top and bottom of the viewport so
302
311
  messages are softened as they melt past the edges (borrowed from
303
312
  [DSH-Transparent-UI-Plugin](https://github.com/WYH66666666/DSH-Transparent-UI-Plugin)'s
@@ -371,10 +380,15 @@ theme/glass actually reached the sidebar in that run), so no conclusion is drawn
371
380
  | DSH | `>=0.1.7-rc.1` (the settings page consumes the official `dsh-client-ui-primitives` `SegmentedControl`, which only exists from `0.1.7-alpha.1`; the legacy `settingsScope` code path is kept, but 0.1.5 / 0.1.6 hosts are no longer declared supported) |
372
381
  | Node.js | `>=20` |
373
382
  | Profile | `web` (the Web GUI and both desktop shells run the web UI and share this plugin); desktop profiles are named `desktop` |
374
- | Verified exact version | `0.1.7-rc.1`: installed, started, persisted a setting to disk and restored it across a restart in a real profile ([evidence](docs/issue-15-settings-seam-0.1.7.md)); `0.1.7-rc.2`: boot-level e2e and live-page sampling of the glass layer (issues #16 / #17); `0.1.5-rc.3`, `0.1.7-alpha.1` and `0.1.7-alpha.2` are declared as the same seam |
383
+ | Verified exact version | `0.2.1-rc.2`: real-device acceptance on the official desktop shell — plugin boots, tokens apply, glass recipe intact, **settings writes really reach `cordis.patch.yml`** (they did not before the fix) and are adopted back after a restart ([audit](docs/desktop-0.2.1-adaptation-audit.md)); `0.2.0-rc.2`: boot-level e2e on the official shell's bundled runtime (19/19), real desktop-window pixels and the title-bar colour chain, `configForms` persistence ([audit](docs/desktop-0.2.0-adaptation-audit.md)); `0.1.7-rc.1`: installed, started, persisted a setting to disk and restored it across a restart in a real profile ([evidence](docs/issue-15-settings-seam-0.1.7.md)); `0.1.7-rc.2`: boot-level e2e and live-page sampling of the glass layer (issues #16 / #17); `0.1.5-rc.1`: boot-level e2e rerun under the CI recipe (**historical**, below the current floor) |
375
384
 
376
- The machine-readable form of the above is `dsh.compatibility` (`dsh` / `dshReleases` /
377
- `dshOperations`) in `package.json`.
385
+ `dsh.compatibility` (`dsh` / `dshReleases` / `dshOperations`) in `package.json` **declares only the
386
+ three currently supported lines**: `0.1.7-rc.1` / `0.2.0-rc.2` / `0.2.1-rc.2` (floor `>=0.1.7-rc.1`).
387
+ Earlier `0.1.5-rc.*` / `0.1.7-alpha.*` runs are records of what was verified **at the time** and are no
388
+ longer declared.
389
+
390
+ Colour coverage is based on `dsh-v0.2.0-rc.2`'s `design-platform.css`: all 190 `--dsw-*` tokens per
391
+ scheme (static 77 / alias 101 / specific 11 / non-family 1), including the 17 aliases added in 0.2.0.
378
392
 
379
393
  ### Permissions and external access
380
394
 
package/README.md CHANGED
@@ -53,7 +53,7 @@ Web / 桌面端做全界面换色与玻璃质感,TUI 端自动同步四套官
53
53
 
54
54
  它内置 Catppuccin 的四个主题——**Latte**、**Frappé**、**Macchiato**、**Mocha**——
55
55
  把整个界面的配色都换成对应的 Catppuccin 色板;并在设置里拥有**自己的独立分区**
56
- (**设置 → Catppuccin**:风味、代码高亮、自定义覆盖、玻璃质感、更新检查都在这一页),
56
+ (**设置 → Catppuccin**:风味、代码高亮、自定义覆盖、玻璃质感、更新检查、反馈与支持都在这一页),
57
57
  选择会自动保存、重启自动恢复。
58
58
 
59
59
  同时内置一套可开关的**玻璃质感**(Glassmorphism)皮肤:顶栏、侧边栏、
@@ -66,7 +66,7 @@ Catppuccin 主题。
66
66
  - 🎨 四个主题:Latte(浅色)、Frappé / Macchiato / Mocha(深色)
67
67
  - 🧩 接入官方主题系统,与内置浅色 / 深色 / 跟随系统主题平级
68
68
  - 🎯 全界面配色覆盖,不只是一两个强调色
69
- - ⚙️ 设置页一行切换,选择自动保存、重启自动恢复
69
+ - ⚙️ 设置页独立分区一键切换,选择自动保存、重启自动恢复
70
70
  - 🔧 **自定义 token 覆盖**:按「`--dsw-* 变量: 值`」逐条覆盖单个配色 token(例如把注释色换成蓝色),与所选风味一起持久保存
71
71
  - 🖍️ **代码块高亮风格**:默认 / 注释斜体(italic-comments)两套 shiki 风格可选
72
72
  - 🌐 中 / 英 / 日 / 韩 / 西 / 法 / 德七语文案(跟随系统语言)
@@ -75,7 +75,7 @@ Catppuccin 主题。
75
75
  只给输入框卡片与浮层上玻璃),模糊度、磨砂度、
76
76
  背景亮度自由调节(交互参考 [DSH-Transparent-UI-Plugin](https://github.com/WYH66666666/DSH-Transparent-UI-Plugin))
77
77
  - 🌫️ **玻璃拟态细节**:页面上下边缘渐变模糊、折叠侧边栏悬浮玻璃、
78
- 随主题的氛围地面(同色系渐变 + 两枚柔光斑)——内容滚入视口边缘时柔化穿过,
78
+ 随主题的氛围地面(同色系渐变;两枚柔光斑是**可选开关、默认关**)——内容滚入视口边缘时柔化穿过,
79
79
  玻璃透着地面的明暗,层次更立体
80
80
  - 🎨 玻璃配色自动跟随当前 Catppuccin 主题
81
81
  - 🔄 **检查 Catppuccin 插件更新**:设置页一键检测本插件(dsh-catppuccin)在 npm 上的最新版本,发现新版直接给出可复制的升级命令;**默认开启自动检查**(启动后一次 + 每 6 小时),更新渠道可选稳定版 / Beta
@@ -229,6 +229,11 @@ dsh plugin --profile dsh-tui add https://github.com/NoNameLeGo/dsh-catppuccin-th
229
229
  不透明度。
230
230
  - **背景亮度**:深色模式 0–50 压暗、浅色模式 50–100 提亮(50 为原样),
231
231
  直接调和进地面底色;拉到两端时氛围层淡出,回到纯白 / 纯黑。
232
+ - **地面光斑**:在地面渐变上叠两枚**固定于屏幕**的柔光斑(`0.6.3` 的观感),**默认关**——
233
+ 光斑不跟面板走,每块玻璃透出的是它的不同部位,打开后左上角会明显偏亮。
234
+ - **就地预览**:玻璃区里有一条**地面样本条**(拖亮度或切光斑,它当场变化——取的就是页面地面那套色);
235
+ 代码高亮下方有**代码样本**(直接读 `--shiki-token-*`,切「柔和注释」即刻变色);
236
+ 模式选择旁的 **看一眼主界面** 会关掉设置页,让你直接看真实布局。
232
237
 
233
238
  玻璃配色自动跟随当前主题,切换 Latte / Frappé / Macchiato / Mocha 时即时
234
239
  变色;所有设置跨重启自动恢复。
@@ -263,8 +268,8 @@ dsh plugin --profile dsh-tui add https://github.com/NoNameLeGo/dsh-catppuccin-th
263
268
  - **六个区域玻璃化**:顶栏、侧边栏、输入框、轨迹视图、聊天气泡
264
269
  和新会话按钮;云母模式下成为带圆角的悬浮卡片,聊天内容滚动时从玻璃
265
270
  下方穿过、被模糊;折叠侧边栏时导航条同样悬浮在聊天区边缘
266
- (「性能与用量」统计行不在此列——它保持上游原样,留在玻璃卡之外;见
267
- CHANGELOG 的 `0.6.2`「拆除融合带」);
271
+ (「性能与用量」统计行不套玻璃卡、留在卡外,但自 `0.6.4` 起它下面有一条**同色磨砂背板**,
272
+ 防止滚动正文从状态栏读数底下透出——见 CHANGELOG 的 `0.6.2`「拆除融合带」与 `0.6.4` 的 issue #29);
268
273
  - **页面边缘渐变模糊**:视口上下各有一条渐变模糊带,消息滚到边缘时被
269
274
  柔化穿过——内容在边界「融化」(借鉴
270
275
  [DSH-Transparent-UI-Plugin](https://github.com/WYH66666666/DSH-Transparent-UI-Plugin)
@@ -328,9 +333,12 @@ console.table({ backdropFilter: cs.backdropFilter, background: cs.backgroundColo
328
333
  | DSH | `>=0.1.7-rc.1`(设置页消费官方 `dsh-client-ui-primitives` 的 `SegmentedControl`,该导出自 `0.1.7-alpha.1` 起才有;旧的 `settingsScope` 通道代码保留,但 0.1.5 / 0.1.6 宿主不再声明支持) |
329
334
  | Node.js | `>=20` |
330
335
  | Profile | `web`(Web GUI 与两个桌面壳都启动 web 界面,共用本插件);桌面端默认 profile 名为 `desktop`(已声明) |
331
- | 已验证的具体版本 | `0.2.1-rc.2`:官方桌面壳真机验收——插件正常启动、token 逐项生效、玻璃配方完整,**改设置真写回 `cordis.patch.yml`(修复前不写)**、重启后从文档采纳回来([审计](docs/desktop-0.2.1-adaptation-audit.md));`0.2.0-rc.2`:官方桌面壳自带运行时的启动级 e2e(19/19)、真机桌面窗口像素与标题栏取色链路、`configForms` 落盘([审计](docs/desktop-0.2.0-adaptation-audit.md));`0.1.7-rc.1`:真实 profile 上完成安装、启动、改设置落盘与重启恢复([证据](docs/issue-15-settings-seam-0.1.7.md));`0.1.7-rc.2`:启动级 e2e 与玻璃层的真页采样(issue #16 / #17);`0.1.5-rc.1`:按 CI 口径复跑的启动级 e2e;`0.1.5-rc.3`、`0.1.7-alpha.1`、`0.1.7-alpha.2` 为同一 seam 的声明 |
336
+ | 已验证的具体版本 | `0.2.1-rc.2`:官方桌面壳真机验收——插件正常启动、token 逐项生效、玻璃配方完整,**改设置真写回 `cordis.patch.yml`(修复前不写)**、重启后从文档采纳回来([审计](docs/desktop-0.2.1-adaptation-audit.md));`0.2.0-rc.2`:官方桌面壳自带运行时的启动级 e2e(19/19)、真机桌面窗口像素与标题栏取色链路、`configForms` 落盘([审计](docs/desktop-0.2.0-adaptation-audit.md));`0.1.7-rc.1`:真实 profile 上完成安装、启动、改设置落盘与重启恢复([证据](docs/issue-15-settings-seam-0.1.7.md));`0.1.7-rc.2`:启动级 e2e 与玻璃层的真页采样(issue #16 / #17);`0.1.5-rc.1`:按 CI 口径复跑的启动级 e2e(**历史记录**,已低于当前兼容下限) |
337
+
338
+ `package.json` 的 `dsh.compatibility`(`dsh` / `dshReleases` / `dshOperations`)**只声明当前支持的三条**:
339
+ `0.1.7-rc.1` / `0.2.0-rc.2` / `0.2.1-rc.2`(兼容下限 `>=0.1.7-rc.1`);更早的 `0.1.5-rc.*`
340
+ 与 `0.1.7-alpha.*` 是**当时**的验证记录,不在声明内。
332
341
 
333
- 以上也是 `package.json` 里 `dsh.compatibility`(`dsh` / `dshReleases` / `dshOperations`)的机器可读版本。
334
342
  色彩覆盖以 `dsh-v0.2.0-rc.2` 的 `design-platform.css` 为基线:每方案 190 个 `--dsw-*` token(static 77 /
335
343
  alias 101 / specific 11 / 非三族 1)全覆盖,含 0.2.0 新增的 17 个 alias。
336
344
 
package/lib/client.js CHANGED
@@ -683,10 +683,88 @@ window.__ModuleLoader__.load({
683
683
  function stateFromSettingsSection(section) {
684
684
  return sanitizeState(section);
685
685
  }
686
+ /** Field-for-field equality of two override maps — the same rule
687
+ * {@link settingsSectionsEqual} applies to them (both directions, so a key
688
+ * that only one side holds makes them differ). */
689
+ function sameOverrides(a, b) {
690
+ const keys = Object.keys(a);
691
+ return keys.length === Object.keys(b).length && keys.every((key) => b[key] === a[key]);
692
+ }
686
693
  /** Whether two settings sections are field-for-field equal (used to skip
687
694
  * redundant mirror writes and to recognize the Host echoing our own write). */
688
695
  function settingsSectionsEqual(a, b) {
689
- return a.flavor === b.flavor && a.glass.enabled === b.glass.enabled && a.glass.mode === b.glass.mode && a.glass.blur === b.glass.blur && a.glass.frost === b.glass.frost && a.glass.brightness === b.glass.brightness && a.glass.glow === b.glass.glow && a.autoCheck === b.autoCheck && a.updateChannel === b.updateChannel && a.shikiStyle === b.shikiStyle && Object.keys(a.overrides).every((key) => b.overrides[key] === a.overrides[key]) && Object.keys(b.overrides).every((key) => a.overrides[key] === b.overrides[key]);
696
+ return a.flavor === b.flavor && a.glass.enabled === b.glass.enabled && a.glass.mode === b.glass.mode && a.glass.blur === b.glass.blur && a.glass.frost === b.glass.frost && a.glass.brightness === b.glass.brightness && a.glass.glow === b.glass.glow && a.autoCheck === b.autoCheck && a.updateChannel === b.updateChannel && a.shikiStyle === b.shikiStyle && sameOverrides(a.overrides, b.overrides);
697
+ }
698
+ /**
699
+ * Replay the local changes a window made BEFORE the durable channel became
700
+ * usable onto the document state — the issue #30 fix.
701
+ *
702
+ * The R2 mechanism (`pendingLocalPush` in `src/client/index.ts`) exists so a
703
+ * change made during the boot window is not rolled back by the first snapshot
704
+ * the settings scope resolves. It used to replay the WHOLE local state, and on
705
+ * a fresh origin (empty localStorage: a new port, a cleared site, a webview
706
+ * partition) "the whole local state" is nothing but the shipped defaults — so
707
+ * the first snapshot rewrote the document to `flavor: off`, `glass.enabled:
708
+ * false`, `mode: mica`. The trigger is not even a user action: the glass layer
709
+ * republishes on every `theme/change`, and `ui-theme` adopts its preference on
710
+ * every boot.
711
+ *
712
+ * This replays only the fields whose value moved away from `baseline` — the
713
+ * local section as of the last moment the window knew it was in sync with the
714
+ * document (boot, or the last adoption / successful write). Everything else
715
+ * keeps the document's value, and `changed === false` tells the caller to take
716
+ * the ordinary "document wins" adoption path instead of writing anything.
717
+ *
718
+ * ⚠️ Every field of {@link CatppuccinSettingsSection} must be carried here.
719
+ * A missing one is SILENT in both directions — a boot-window change to it is
720
+ * dropped, and (because the caller's whole-state fallback is gone) nothing
721
+ * else would notice. Same rule as `stateToMutateOps` (issue #26), and
722
+ * `tests/state.spec.ts` holds a drift guard that fails when a field is added
723
+ * without a mutant here.
724
+ *
725
+ * Known gap (accepted): a delta cannot tell "untouched" from "explicitly set
726
+ * to the current default", so setting a field BACK to its default value inside
727
+ * the boot window yields to the document. The window is sub-second and the
728
+ * direction is "document wins", i.e. no durable data is lost.
729
+ *
730
+ * @param document - the section the settings document resolved (wins).
731
+ * @param baseline - the local section at the last sync point.
732
+ * @param local - the local section right now.
733
+ * @returns the merged state plus whether anything actually differed.
734
+ */
735
+ function replayLocalChanges(document, baseline, local) {
736
+ const merged = settingsSectionFromState(document);
737
+ let changed = false;
738
+ const carry = (key) => {
739
+ if (local[key] !== baseline[key]) {
740
+ merged[key] = local[key];
741
+ changed = true;
742
+ }
743
+ };
744
+ const carryGlass = (key) => {
745
+ if (local.glass[key] !== baseline.glass[key]) {
746
+ merged.glass[key] = local.glass[key];
747
+ changed = true;
748
+ }
749
+ };
750
+ carry("flavor");
751
+ carryGlass("enabled");
752
+ carryGlass("mode");
753
+ carryGlass("blur");
754
+ carryGlass("frost");
755
+ carryGlass("brightness");
756
+ carryGlass("glow");
757
+ carry("autoCheck");
758
+ carry("updateChannel");
759
+ carry("shikiStyle");
760
+ if (!sameOverrides(local.overrides, baseline.overrides)) {
761
+ merged.overrides = { ...local.overrides };
762
+ changed = true;
763
+ }
764
+ return {
765
+ state: sanitizeState(merged),
766
+ changed
767
+ };
690
768
  }
691
769
  //#endregion
692
770
  //#region src/client/glass/glass-layer.ts
@@ -3371,6 +3449,46 @@ window.__ModuleLoader__.load({
3371
3449
  }
3372
3450
  ];
3373
3451
  //#endregion
3452
+ //#region src/boot-palette.ts
3453
+ /** Attribute marking the injected `<style>` so the Client can find it. */
3454
+ const BOOT_PALETTE_ATTR = "data-dsh-catppuccin-boot";
3455
+ //#endregion
3456
+ //#region src/client/boot-palette.ts
3457
+ /**
3458
+ * Client half of the pre-paint palette (issue #30 补充, 2026-10-27).
3459
+ *
3460
+ * The Host injects a `<style data-dsh-catppuccin-boot>` into the index HTML so
3461
+ * the first frame is already the persisted flavour (see `src/boot-palette.ts`).
3462
+ * That sheet is only a stand-in: its selector is `html body` (0,0,2), which
3463
+ * outranks the official `body` token block, so leaving it in place would pin
3464
+ * every token and defeat later theme switches (the user picks Light or turns the
3465
+ * flavour off and nothing changes).
3466
+ *
3467
+ * So it is removed the moment the flavour theme actually takes over — which is
3468
+ * NOT the same instant as "setTheme was called":
3469
+ * - a reload where the runtime preference already equals the flavour id never
3470
+ * calls setTheme (see `applyDesired`'s early return), and
3471
+ * - a built-in pick (Light / Dark / System) must remove it too, because the
3472
+ * sheet belongs to a flavour that is no longer wanted.
3473
+ * The restore effect therefore clears it from every one of those points plus a
3474
+ * fallback timer and the effect teardown.
3475
+ */
3476
+ /**
3477
+ * Remove every injected pre-paint sheet from this document.
3478
+ *
3479
+ * Idempotent and safe when nothing was injected (hosts without the index
3480
+ * injection seam, `flavor: off`, a document that already cleared it).
3481
+ * @returns whether an element was actually removed (diagnostics and tests).
3482
+ */
3483
+ function clearBootPalette() {
3484
+ let removed = false;
3485
+ for (const element of document.querySelectorAll(`[${BOOT_PALETTE_ATTR}]`)) {
3486
+ element.remove();
3487
+ removed = true;
3488
+ }
3489
+ return removed;
3490
+ }
3491
+ //#endregion
3374
3492
  //#region src/client/shiki-tokens.ts
3375
3493
  /**
3376
3494
  * Canonical Catppuccin syntax colours per flavour.
@@ -4167,9 +4285,10 @@ window.__ModuleLoader__.load({
4167
4285
  let lastWrittenSection;
4168
4286
  let handleStaleConflict;
4169
4287
  let pendingLocalPush = false;
4288
+ let syncedSection = settingsSectionFromState(buildLocalState());
4170
4289
  const baseRevisionTracker = createBaseRevisionTracker();
4171
4290
  const queuePersist = () => {
4172
- if (!isScopeUsable(scope.getSnapshot())) pendingLocalPush = true;
4291
+ if (!isScopeUsable(scope.getSnapshot()) && !settingsSectionsEqual(settingsSectionFromState(buildLocalState()), syncedSection)) pendingLocalPush = true;
4173
4292
  baseRevisionTracker.capture(() => scope.getSnapshot().revision);
4174
4293
  scheduleDurablePersist(persistLocal);
4175
4294
  };
@@ -4181,8 +4300,10 @@ window.__ModuleLoader__.load({
4181
4300
  baseRevision,
4182
4301
  lastWrittenSection
4183
4302
  }).then((outcome) => {
4184
- if (outcome === "written") lastWrittenSection = settingsSectionFromState(state);
4185
- else if (outcome === "stale") {
4303
+ if (outcome === "written") {
4304
+ lastWrittenSection = settingsSectionFromState(state);
4305
+ syncedSection = lastWrittenSection;
4306
+ } else if (outcome === "stale") {
4186
4307
  emitConflict();
4187
4308
  handleStaleConflict?.();
4188
4309
  }
@@ -4190,14 +4311,22 @@ window.__ModuleLoader__.load({
4190
4311
  };
4191
4312
  const AUTO_CHECK_BOOT_DELAY_MS = 3e3;
4192
4313
  const AUTO_CHECK_INTERVAL_MS = 216e5;
4314
+ /** How long the Host's pre-paint palette may survive before it is removed
4315
+ * unconditionally (issue #30 补充). Generous on purpose: removing it EARLY
4316
+ * re-exposes the official palette for one frame, which is the bug. */
4317
+ const BOOT_PALETTE_FALLBACK_MS = 1e4;
4193
4318
  let lastAutoResult = null;
4194
4319
  let armAutoCheck;
4195
4320
  ctx.effect(() => {
4196
4321
  let liveBuiltinPick = null;
4322
+ const bootPaletteFallback = window.setTimeout(() => {
4323
+ clearBootPalette();
4324
+ }, BOOT_PALETTE_FALLBACK_MS);
4197
4325
  const originalSetTheme = theme.setTheme;
4198
4326
  const restoreGuardSetTheme = (id) => {
4199
4327
  liveBuiltinPick = isBuiltinPreference(id) ? id : null;
4200
4328
  originalSetTheme.call(theme, id);
4329
+ clearBootPalette();
4201
4330
  };
4202
4331
  theme.setTheme = restoreGuardSetTheme;
4203
4332
  let restorePending = false;
@@ -4207,7 +4336,10 @@ window.__ModuleLoader__.load({
4207
4336
  queueMicrotask(() => {
4208
4337
  restorePending = false;
4209
4338
  const preference = theme.getTheme().preference;
4210
- if (preference === desired) return;
4339
+ if (preference === desired) {
4340
+ clearBootPalette();
4341
+ return;
4342
+ }
4211
4343
  if (builtinPickWins(preference, liveBuiltinPick)) return;
4212
4344
  ensureThemeRegistered(desired);
4213
4345
  try {
@@ -4220,11 +4352,17 @@ window.__ModuleLoader__.load({
4220
4352
  const desired = readFlavor();
4221
4353
  if (desired === "off") {
4222
4354
  const preference = theme.getTheme().preference;
4223
- if (readExplicitFlavorOff() && flavorFromThemeId(preference) !== "off") scheduleRestore(readRestoredPreference());
4355
+ if (readExplicitFlavorOff()) {
4356
+ clearBootPalette();
4357
+ if (flavorFromThemeId(preference) !== "off") scheduleRestore(readRestoredPreference());
4358
+ }
4224
4359
  return;
4225
4360
  }
4226
4361
  const preference = theme.getTheme().preference;
4227
- if (preference === desired) return;
4362
+ if (preference === desired) {
4363
+ clearBootPalette();
4364
+ return;
4365
+ }
4228
4366
  if (builtinPickWins(preference, liveBuiltinPick)) return;
4229
4367
  scheduleRestore(desired);
4230
4368
  };
@@ -4256,8 +4394,21 @@ window.__ModuleLoader__.load({
4256
4394
  const local = buildLocalState();
4257
4395
  if (pendingLocalPush) {
4258
4396
  pendingLocalPush = false;
4259
- queuePersist();
4260
- return;
4397
+ const replayed = replayLocalChanges(state, syncedSection, settingsSectionFromState(local));
4398
+ if (replayed.changed) {
4399
+ writeFlavor(replayed.state.flavor);
4400
+ glass.applyRemote(replayed.state.glass);
4401
+ writeAutoCheck(replayed.state.autoCheck);
4402
+ writeUpdateChannel(replayed.state.updateChannel);
4403
+ writeOverrides(replayed.state.overrides);
4404
+ writeShikiStyle(replayed.state.shikiStyle);
4405
+ emitPrefs();
4406
+ reapplyThemePrefs();
4407
+ applyDesired();
4408
+ syncedSection = settingsSectionFromState(replayed.state);
4409
+ queuePersist();
4410
+ return;
4411
+ }
4261
4412
  }
4262
4413
  if (!scope.hasUserLayer(snapshot)) {
4263
4414
  if (!isDefaultState(local)) queuePersist();
@@ -4274,6 +4425,7 @@ window.__ModuleLoader__.load({
4274
4425
  emitPrefs();
4275
4426
  reapplyThemePrefs();
4276
4427
  applyDesired();
4428
+ syncedSection = settingsSectionFromState(state);
4277
4429
  };
4278
4430
  applyScopeSnapshot();
4279
4431
  const offScope = scope.subscribe(applyScopeSnapshot);
@@ -4307,6 +4459,8 @@ window.__ModuleLoader__.load({
4307
4459
  offGlass();
4308
4460
  disposer();
4309
4461
  window.removeEventListener("storage", onStorage);
4462
+ window.clearTimeout(bootPaletteFallback);
4463
+ clearBootPalette();
4310
4464
  window.clearTimeout(bootTimer);
4311
4465
  window.clearInterval(intervalTimer);
4312
4466
  cancelDurablePersist();