dsh-deepseek-style-theme 1.43.1 → 1.43.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/README.md CHANGED
@@ -112,13 +112,28 @@ dsh plugin --profile web add ./releases/dsh-deepseek-style-theme-1.43.1.tgz
112
112
 
113
113
  ### 从 npm
114
114
 
115
+ > ⚠️ **暂时不要用这一条。** npm 上的 `latest` 仍是 **1.43.1**,而那一版带着一个致命缺陷:客户端 bundle 的注册 id 还是旧 scope 的 `@dsh-external/dsh-deepseek-style-theme`,而宿主按包名 `dsh-deepseek-style-theme` 要求它自我注册,于是 **bundle 加载成功却什么都没注册**,主题的整个客户端半边失效。1.43.3 已修复。
116
+ >
117
+ > 在 1.43.3 发到 npm 之前,请用上面的 **tgz 别名地址** 或 `github:` 安装。
118
+
115
119
  ```bash
120
+ # 修复版发到 npm 之后可用;在那之前用 tgz 别名地址
116
121
  dsh plugin --profile web add dsh-deepseek-style-theme
117
122
  ```
118
123
 
119
- > 包名是**无 scope** 的 `dsh-deepseek-style-theme`。这是刻意的:`@dsh-external` 这个 scope 在 npm 上属于别人(`wulei1107`,下面挂着 `@dsh-external/dsh-vision-toolkit`),不是本项目的命名空间,也发不进去。仓库里 `cordis.patch.yml` 的 `name:` 与 `package.json` 的 `name` 必须始终一致——前者是 Loader 用来解析模块的说明符,写成解析不到的旧名会让插件装配失败。
124
+ > 包名是**无 scope** 的 `dsh-deepseek-style-theme`。这是刻意的:`@dsh-external` 这个 scope 在 npm 上属于别人(`wulei1107`,下面挂着 `@dsh-external/dsh-vision-toolkit`),不是本项目的命名空间,也发不进去。
125
+ >
126
+ > **改名的三个引用点必须同时对齐**,缺一个都会静默失效,且失效方式各不相同:
127
+ >
128
+ > | 位置 | 不对齐的后果 |
129
+ > |---|---|
130
+ > | `package.json` 的 `name` | 包装不进去 / 目录名不符 |
131
+ > | `cordis.patch.yml` 的 `name:` | Loader 解析不到模块,**插件装配失败** |
132
+ > | `lib/client.js` 里 `__ModuleLoader__.load` 的 `id` | bundle 加载成功却什么都没注册,**客户端半边失效**(1.43.1 的缺陷) |
133
+ >
134
+ > 自查:`grep '@dsh-external' lib/` 应当**一处都不命中**;命中即漏改(有意保留的兼容位置除外)。
120
135
  >
121
- > npm 是最省事的安装源:不走 `git clone`(绕开代理/证书问题),也不依赖 GitHub Release 资产(绕开 `releases/latest` 那类 URL 腐烂)。插件市场的目录条目一旦写上 `npm` 字段,市场就会优先用它而不是 tarball。
136
+ > npm 是最省事的安装源:不走 `git clone`(绕开代理/证书问题),也不依赖 GitHub Release 资产(绕开 `releases/latest` 那类 URL 腐烂)。
122
137
 
123
138
  ```bash
124
139
  # 或任意 git 仓库地址(若该仓库未提交构建产物,
package/cordis.patch.yml CHANGED
@@ -1,6 +1,6 @@
1
- # DeepSeek-Style-Theme bundle patch: inserts its public dsh.client entry into
2
- # the web plugin roster. The bundle is the official dsh plugin shape — install
3
- # it with `dsh plugin --profile <name> add <path-or-git-url>`.
4
- - insert:
5
- - id: ui-skin-deepseek-style
6
- name: 'dsh-deepseek-style-theme'
1
+ # DeepSeek-Style-Theme bundle patch: inserts its public dsh.client entry into
2
+ # the web plugin roster. The bundle is the official dsh plugin shape — install
3
+ # it with `dsh plugin --profile <name> add <path-or-git-url>`.
4
+ - insert:
5
+ - id: ui-skin-deepseek-style
6
+ name: 'dsh-deepseek-style-theme'
package/lib/client.js CHANGED
@@ -1,8 +1,22 @@
1
- window.__ModuleLoader__.load({
2
- id: "@dsh-external/dsh-deepseek-style-theme",
3
- factory: (require) => {
4
- var module = { exports: {} };
5
- var exports = module.exports;
1
+ // The loader requires this bundle to register itself under the id the host asked
2
+ // for, and that id is the package NAME of whichever copy is installed -- which is
3
+ // not one value during a rename. The package moved from
4
+ // @dsh-external/dsh-deepseek-style-theme to the unscoped dsh-deepseek-style-theme
5
+ // in 1.43.1, so a profile installed before the rename asks for the old id and one
6
+ // installed after asks for the new one. A single hard-coded literal therefore
7
+ // breaks whichever half it does not match, and BOTH directions were hit for real,
8
+ // one machine each:
9
+ //
10
+ // literal old, install new -> "loaded without registering \"dsh-deepseek-style-theme\""
11
+ // literal new, install old -> "loaded without registering \"@dsh-external/...\""
12
+ //
13
+ // So register under BOTH ids with the same factory: the loader finds the one it
14
+ // wants and the other entry is simply never asked for. The registrations are
15
+ // guarded individually because a loader might reject an id it did not request,
16
+ // and that must not take the working one down with it.
17
+ const dshDeepseekStyleThemeFactory = (require) => {
18
+ var module = { exports: {} };
19
+ var exports = module.exports;
6
20
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
21
  const React = require("react");
8
22
 
@@ -26,7 +40,7 @@ window.__ModuleLoader__.load({
26
40
  // page data — session titles, file paths, workspace names — must go
27
41
  // through `textContent`/`title`/`setAttribute`, never `innerHTML`. An
28
42
  // interpolated value there would be DOM XSS.
29
- const PLUGIN_ID = "@dsh-external/dsh-deepseek-style-theme";
43
+ const PLUGIN_ID = "dsh-deepseek-style-theme";
30
44
 
31
45
  // ─── fluid background + liquid-glass refraction ───────────────────────
32
46
  // Ported from dsh-theme-mineradio v2.3.8 (MIT, Copyright (c) 2026 John Wu).
@@ -632,7 +646,40 @@ void main() {
632
646
  var TALL_FILTER_ID = "dshome-glass-dispersion-tall";
633
647
  var TALL_TINT_ID = "dshome-glass-dispersion-tall-tint";
634
648
  var TALL_REFRACT_ID = "dshome-glass-dispersion-tall-refract";
635
- var ATTR = "data-dshome-dispersion";
649
+ // Two composer filters: `narrow` (an 8px band) and `wide` (16px).
650
+ //
651
+ // The band is sized by what the user can actually SEE, which is the top and
652
+ // bottom edges: the card's top abuts the 任务 bar and its bottom abuts the
653
+ // status line, so displacing the backdrop across those hard edges shows,
654
+ // while left and right sit over a continuous background where the same
655
+ // displacement changes nothing. The user accepted that asymmetry ("允许左右
656
+ // 无反射"), so these maps are tuned for the VERTICAL band and the horizontal
657
+ // one is left wherever it lands.
658
+ //
659
+ // Vertical band on screen = (edge + softness) / mapHeight * 1.56 * cardHeight
660
+ // -- the filter region is 156% of the surface's height (see filterMarkup) --
661
+ // so at the upstream 400x92 aspect and a 112px-tall card that is 1.90x the
662
+ // map inset: 1.7 + 2.5 gives 8px, 3.4 + 5 gives 16px.
663
+ var COMPOSER_FILTER_ID = "dshome-glass-dispersion-composer";
664
+ var COMPOSER_TINT_ID = "dshome-glass-dispersion-composer-tint";
665
+ var COMPOSER_REFRACT_ID = "dshome-glass-dispersion-composer-refract";
666
+ var WIDE_FILTER_ID = "dshome-glass-dispersion-composer-wide";
667
+ var WIDE_TINT_ID = "dshome-glass-dispersion-composer-wide-tint";
668
+ var WIDE_REFRACT_ID = "dshome-glass-dispersion-composer-wide-refract";
669
+ var COMPOSER_MAP_W = 400;
670
+ var COMPOSER_MAP_H = 92;
671
+ var COMPOSER_MAP_RADIUS = 50;
672
+ var COMPOSER_MAP_EDGE = 1.7;
673
+ var COMPOSER_MAP_SOFTNESS = 2.5;
674
+ var COMPOSER_WIDE_EDGE = 3.4;
675
+ var COMPOSER_WIDE_SOFTNESS = 5;
676
+ // Chromatic aberration, composer filters only. 0.18 splits the base
677
+ // displacement into three: red at 118%, blue at 82%, green in the middle,
678
+ // which is what makes a lens edge fringe instead of merely tinting. Kept to
679
+ // the two composer filters because it triples the displacement passes, and
680
+ // because the shared filters have to keep upstream's exact shape for the
681
+ // frosted recipe.
682
+ var COMPOSER_ABERRATION = 0.18;
636
683
  var DEFAULT_REF_SCALE = 60;
637
684
  // The liquid recipe drives the same feDisplacementMap harder: at 60 the
638
685
  // bend is there but only just legible once the blur sits on top of it, and
@@ -644,15 +691,20 @@ void main() {
644
691
  var TINT_SAT = 0.85;
645
692
  var TINT_LIGHT = 0.6;
646
693
  var DEFAULT_TINT_HUE = 44;
647
- function buildDisplacementMapDataUrl(width = 400, height = 92, radius = 50) {
694
+ // Upstream derives the inset from the map's SHORT side (7% of it, halved),
695
+ // which is only uniform if the map is square. The composer passes `edge`
696
+ // instead: one absolute inset for both axes, which comes out uniform as
697
+ // long as the map's aspect matches the filter region's (see COMPOSER_MAP_*).
698
+ // Defaults reproduce upstream's numbers exactly, so the frosted recipe's
699
+ // map stays byte for byte what it was.
700
+ function buildDisplacementMapDataUrl(width = 400, height = 92, radius = 50, edge = null, softness = 11) {
648
701
  const w = Math.max(240, Math.round(width));
649
702
  const h = Math.max(48, Math.round(height));
650
703
  const r = Math.max(12, Math.round(radius));
651
- const borderWidth = 0.07;
652
- const edge = Math.min(w, h) * (borderWidth * 0.5);
653
- const innerW = Math.max(1, w - edge * 2);
654
- const innerH = Math.max(1, h - edge * 2);
655
- const svg2 = `<svg viewBox="0 0 ${w} ${h}" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="glass-x" x1="0%" y1="0%" x2="100%" y2="0%"><stop offset="0%" stop-color="#000"/><stop offset="100%" stop-color="#f00"/></linearGradient><linearGradient id="glass-y" x1="0%" y1="0%" x2="0%" y2="100%"><stop offset="0%" stop-color="#000"/><stop offset="100%" stop-color="#00f"/></linearGradient></defs><rect x="0" y="0" width="${w}" height="${h}" fill="#000"/><rect x="0" y="0" width="${w}" height="${h}" rx="${r}" fill="url(#glass-x)"/><rect x="0" y="0" width="${w}" height="${h}" rx="${r}" fill="url(#glass-y)" style="mix-blend-mode:screen"/><rect x="${edge.toFixed(2)}" y="${edge.toFixed(2)}" width="${innerW.toFixed(2)}" height="${innerH.toFixed(2)}" rx="${r}" fill="hsl(0 0% 50% / 1)" style="filter:blur(11px)"/></svg>`;
704
+ const inset = edge === null ? Math.min(w, h) * 0.035 : Math.max(0, edge);
705
+ const innerW = Math.max(1, w - inset * 2);
706
+ const innerH = Math.max(1, h - inset * 2);
707
+ const svg2 = `<svg viewBox="0 0 ${w} ${h}" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="glass-x" x1="0%" y1="0%" x2="100%" y2="0%"><stop offset="0%" stop-color="#000"/><stop offset="100%" stop-color="#f00"/></linearGradient><linearGradient id="glass-y" x1="0%" y1="0%" x2="0%" y2="100%"><stop offset="0%" stop-color="#000"/><stop offset="100%" stop-color="#00f"/></linearGradient></defs><rect x="0" y="0" width="${w}" height="${h}" fill="#000"/><rect x="0" y="0" width="${w}" height="${h}" rx="${r}" fill="url(#glass-x)"/><rect x="0" y="0" width="${w}" height="${h}" rx="${r}" fill="url(#glass-y)" style="mix-blend-mode:screen"/><rect x="${inset.toFixed(2)}" y="${inset.toFixed(2)}" width="${innerW.toFixed(2)}" height="${innerH.toFixed(2)}" rx="${r}" fill="hsl(0 0% 50% / 1)" style="filter:blur(${softness})"/></svg>`;
656
708
  return `data:image/svg+xml,${encodeURIComponent(svg2)}`;
657
709
  }
658
710
  // Inlined from upstream `src/client/glass-dispersion.ts` (line 2409) —
@@ -693,8 +745,29 @@ void main() {
693
745
  }
694
746
  // Inlined from upstream `src/client/glass-dispersion.ts` (line 2443) —
695
747
  // builds the wide/tall SVG <filter> pair startGlassDispersion injects.
696
- function filterMarkup(id, refractId, tintId, map) {
697
- return `<filter id="${id}" color-interpolation-filters="sRGB" x="-12%" y="-28%" width="124%" height="156%"><feImage href="${map}" x="0" y="0" width="100%" height="100%" preserveAspectRatio="none" result="map"></feImage><feDisplacementMap id="${refractId}" in="SourceGraphic" in2="map" scale="${DEFAULT_REF_SCALE}" xChannelSelector="R" yChannelSelector="B" result="refracted"></feDisplacementMap><feOffset in="refracted" dx="${EDGE_DX}" dy="0" result="shifted"></feOffset><feBlend in="shifted" in2="refracted" mode="difference" result="edgeDiff"></feBlend><feColorMatrix id="${tintId}" in="edgeDiff" type="matrix" values="${tintMatrix(DEFAULT_TINT_HUE, TINT_OPACITY)}" result="edgeTint"></feColorMatrix><feBlend in="refracted" in2="edgeTint" mode="screen" result="output"></feBlend><feGaussianBlur in="output" stdDeviation="0.5"></feGaussianBlur></filter>`;
748
+ //
749
+ // `aberration` (0 = off, upstream's shape byte for byte) adds true
750
+ // chromatic aberration: the backdrop is displaced TWICE MORE at slightly
751
+ // different scales, and the result is assembled channel by channel -- R
752
+ // from the stronger displacement, B from the weaker, G from the base. That
753
+ // is what puts a red/blue fringe on a real lens edge; the tinted
754
+ // difference-blend below is only a colour wash and does not separate
755
+ // channels at all.
756
+ //
757
+ // The two extra maps carry a data-aberration factor so setRefraction() can
758
+ // rescale them in step with the base one instead of flattening all three
759
+ // to the same value.
760
+ function filterMarkup(id, refractId, tintId, map, aberration = 0) {
761
+ const chromatic = aberration <= 0 ? '' :
762
+ `<feDisplacementMap data-aberration="${(1 + aberration).toFixed(3)}" in="SourceGraphic" in2="map" scale="${DEFAULT_REF_SCALE}" xChannelSelector="R" yChannelSelector="B" result="refractR"></feDisplacementMap>` +
763
+ `<feDisplacementMap data-aberration="${(1 - aberration).toFixed(3)}" in="SourceGraphic" in2="map" scale="${DEFAULT_REF_SCALE}" xChannelSelector="R" yChannelSelector="B" result="refractB"></feDisplacementMap>` +
764
+ `<feColorMatrix in="refracted" type="matrix" values="0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 0" result="onlyG"></feColorMatrix>` +
765
+ `<feColorMatrix in="refractR" type="matrix" values="1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0" result="onlyR"></feColorMatrix>` +
766
+ `<feColorMatrix in="refractB" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0" result="onlyB"></feColorMatrix>` +
767
+ `<feBlend in="onlyG" in2="onlyR" mode="screen" result="rgMerge"></feBlend>` +
768
+ `<feBlend in="rgMerge" in2="onlyB" mode="screen" result="aberrated"></feBlend>`;
769
+ const merged = chromatic === '' ? 'refracted' : 'aberrated';
770
+ return `<filter id="${id}" color-interpolation-filters="sRGB" x="-12%" y="-28%" width="124%" height="156%"><feImage href="${map}" x="0" y="0" width="100%" height="100%" preserveAspectRatio="none" result="map"></feImage><feDisplacementMap id="${refractId}" in="SourceGraphic" in2="map" scale="${DEFAULT_REF_SCALE}" xChannelSelector="R" yChannelSelector="B" result="refracted"></feDisplacementMap>${chromatic}<feOffset in="${merged}" dx="${EDGE_DX}" dy="0" result="shifted"></feOffset><feBlend in="shifted" in2="${merged}" mode="difference" result="edgeDiff"></feBlend><feColorMatrix id="${tintId}" in="edgeDiff" type="matrix" values="${tintMatrix(DEFAULT_TINT_HUE, TINT_OPACITY)}" result="edgeTint"></feColorMatrix><feBlend in="${merged}" in2="edgeTint" mode="screen" result="output"></feBlend><feGaussianBlur in="output" stdDeviation="0.5"></feGaussianBlur></filter>`;
698
771
  }
699
772
  /**
700
773
  * Mount the two SVG backdrop filters and set `ATTR` on <html> so the
@@ -721,7 +794,9 @@ void main() {
721
794
  }
722
795
  const mapWide = buildDisplacementMapDataUrl(400, 92, 50);
723
796
  const mapTall = buildDisplacementMapDataUrl(92, 400, 50);
724
- const markup = `<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" width="0" height="0" style="position:absolute;overflow:hidden"><defs>` + filterMarkup(FILTER_ID, REFRACT_ID, TINT_ID, mapWide) + filterMarkup(TALL_FILTER_ID, TALL_REFRACT_ID, TALL_TINT_ID, mapTall) + `</defs></svg>`;
797
+ const mapComposer = buildDisplacementMapDataUrl(COMPOSER_MAP_W, COMPOSER_MAP_H, COMPOSER_MAP_RADIUS, COMPOSER_MAP_EDGE, COMPOSER_MAP_SOFTNESS);
798
+ const mapComposerWide = buildDisplacementMapDataUrl(COMPOSER_MAP_W, COMPOSER_MAP_H, COMPOSER_MAP_RADIUS, COMPOSER_WIDE_EDGE, COMPOSER_WIDE_SOFTNESS);
799
+ const markup = `<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" width="0" height="0" style="position:absolute;overflow:hidden"><defs>` + filterMarkup(FILTER_ID, REFRACT_ID, TINT_ID, mapWide) + filterMarkup(TALL_FILTER_ID, TALL_REFRACT_ID, TALL_TINT_ID, mapTall) + filterMarkup(COMPOSER_FILTER_ID, COMPOSER_REFRACT_ID, COMPOSER_TINT_ID, mapComposer, COMPOSER_ABERRATION) + filterMarkup(WIDE_FILTER_ID, WIDE_REFRACT_ID, WIDE_TINT_ID, mapComposerWide, COMPOSER_ABERRATION) + `</defs></svg>`;
725
800
  const container = document.createElement("div");
726
801
  container.setAttribute("aria-hidden", "true");
727
802
  container.style.cssText = "position:absolute;width:0;height:0;overflow:hidden;";
@@ -736,8 +811,15 @@ void main() {
736
811
  tint.forEach((node) => node.setAttribute("values", values));
737
812
  },
738
813
  setRefraction(scale) {
739
- const next = String(Math.max(0, Math.min(140, Number(scale) || 0)));
740
- refract.forEach((node) => node.setAttribute("scale", next));
814
+ // The aberration maps are the same displacement at a slightly
815
+ // different scale, so they carry a factor and are rescaled in step;
816
+ // flattening them all to one value would erase the fringing.
817
+ const base = Math.max(0, Math.min(140, Number(scale) || 0));
818
+ refract.forEach((node) => {
819
+ const factor = Number(node.getAttribute("data-aberration"));
820
+ const value = Number.isFinite(factor) && factor > 0 ? base * factor : base;
821
+ node.setAttribute("scale", String(Math.max(0, Math.min(140, value))));
822
+ });
741
823
  },
742
824
  dispose() {
743
825
  root.removeAttribute(ATTR);
@@ -1343,7 +1425,15 @@ html[data-dshome-dispersion] .VOzbGW_panel{
1343
1425
  // agent is already working is `"steering"`. Measured on the current build,
1344
1426
  // a transcript can contain ONLY steering bubbles — anchoring the selector
1345
1427
  // on "user" alone silently skipped every bubble on the page.
1346
- const LIQUID_BUBBLE = ':is([data-chat-flow-kind="user"],[data-chat-flow-kind="steering"]) [class*="bubble" i]';
1428
+ //
1429
+ // The second half covers the gap the user hit: a message typed while the
1430
+ // agent is still working is painted in the transcript BEFORE it is picked
1431
+ // up, and at that moment it sits outside any flow-kind container, so the
1432
+ // two anchored selectors miss it and it renders unglazed until the turn
1433
+ // consumes it. Rather than enumerate the container it lands in, this side
1434
+ // takes any bubble that is NOT part of an assistant step — which is the
1435
+ // only place a bubble must not be glassed.
1436
+ const LIQUID_BUBBLE = ':is([data-chat-flow-kind="user"],[data-chat-flow-kind="steering"]) [class*="bubble" i],[class*="bubble" i]:not([data-chat-flow-kind="assistant-step"] *)';
1347
1437
  const LIQUID_CSS = `
1348
1438
  @property --dshome-liquid-sweep{syntax:"<angle>";inherits:false;initial-value:0deg}
1349
1439
  @keyframes dshome-liquid-sweep{from{--dshome-liquid-sweep:0deg}to{--dshome-liquid-sweep:360deg}}
@@ -1362,6 +1452,13 @@ html[data-dshome-glass="liquid"]{
1362
1452
  than everything else on purpose: at the shared 11px the flow behind it stays
1363
1453
  legible and the pane never separates from the transcript. */
1364
1454
  --dshome-liquid-blur-sidebar:24px;
1455
+ /* The composer blurs less than the rest on purpose. Filters in a backdrop
1456
+ chain apply left to right, so url(refract) followed by blur(N) blurs the
1457
+ refraction AFTER it happens: a band narrower than about the blur radius is
1458
+ simply erased. At 11px the purpose-built ~4px band was invisible, which is
1459
+ exactly what was reported. 8px lets a ~10px band read while still frosting
1460
+ the backdrop. */
1461
+ --dshome-liquid-blur-composer:8px;
1365
1462
  /* One extra veil on top of the sidebar's glass. The pane sits over the fluid
1366
1463
  at roughly the same tone as the conversation it borders, so without this
1367
1464
  lift the two read as one surface. It is white in light mode. */
@@ -1424,6 +1521,25 @@ html[data-dshome-glass="liquid"] [data-composer-card]::after{
1424
1521
  background-image:none!important;
1425
1522
  box-shadow:none!important;
1426
1523
  }
1524
+ /* The frosted recipe gives the composer a brand-tinted conic ring, painted by
1525
+ the patch block at inset:-1px and spun by dsh-border-spin. Under liquid the
1526
+ user asked for it to go: it sits one pixel OUTSIDE the card's own 1.5px white
1527
+ border, so the pair reads as a coloured line with a white line outside it, and
1528
+ two competing edges is what liquid glass is not. content:none removes the
1529
+ pseudo-element outright rather than painting it transparent. */
1530
+ html[data-dshome-glass="liquid"] body [data-composer-card]::before{
1531
+ content:none!important;
1532
+ background:none!important;
1533
+ animation:none!important;
1534
+ }
1535
+ /* With the ring gone the card's own 1.5px white border is the only edge left,
1536
+ and on its own it reads as a flat white outline rather than as glass — which
1537
+ is what the user saw next. Liquid's edge is the inset hairline plus the
1538
+ refraction, so the border goes transparent. The WIDTH stays: removing it
1539
+ would resize the box and shift every control inside by 3px. */
1540
+ html[data-dshome-glass="liquid"] body [data-composer-card]{
1541
+ border-color:transparent!important;
1542
+ }
1427
1543
  /* The sidebar pane carries its own fill and its own shape: rounded only on the
1428
1544
  edge that faces the conversation, so it reads as a panel clipped to the
1429
1545
  window rather than as a full-bleed column. The hairline ring in the shared
@@ -1462,9 +1578,14 @@ html[data-dshome-glass="liquid"] body .hHd-Xa_root::after{
1462
1578
  }
1463
1579
  /* The travelling edge highlight. mask-composite:exclude turns the filled
1464
1580
  conic gradient into a 1px perimeter band; padding:1px on the pseudo-element
1465
- sets that band's width without touching the product's box model. */
1466
- html[data-dshome-glass="liquid"] .hHd-Xa_root::after,
1467
- html[data-dshome-glass="liquid"] ${LIQUID_BUBBLE}::after{
1581
+ sets that band's width without touching the product's box model.
1582
+ It is drawn on the SIDEBAR ONLY. The user asked for it off the composer and
1583
+ off the message bubbles under liquid: on a small pill the band reads as a
1584
+ bright white outline rather than as light travelling a glass edge, and on the
1585
+ composer it doubled up with the card's own border. The bubbles and the
1586
+ composer keep liquid's other three marks — the translucent fill, the sheen
1587
+ and the refraction. */
1588
+ html[data-dshome-glass="liquid"] .hHd-Xa_root::after{
1468
1589
  content:"";position:absolute;inset:0;z-index:2;pointer-events:none;border-radius:inherit;
1469
1590
  padding:1px;
1470
1591
  background:conic-gradient(from var(--dshome-liquid-sweep),
@@ -1477,12 +1598,31 @@ html[data-dshome-glass="liquid"] ${LIQUID_BUBBLE}::after{
1477
1598
  mask-composite:exclude;
1478
1599
  animation:dshome-liquid-sweep 6.5s linear infinite;
1479
1600
  }
1480
- html[data-dshome-dispersion][data-dshome-glass="liquid"] body [data-composer-card],
1601
+ /* The composer's edge refraction. Three states, selected by the durable
1602
+ composerRefraction setting (see html[data-dshome-composer]):
1603
+ narrow (default) -- an 8px band top and bottom
1604
+ wide -- a 16px band top and bottom
1605
+ off -- no dispersion at all
1606
+ Left and right are left as they fall: the band is there, but over a
1607
+ continuous background a horizontal displacement has nothing to show, which
1608
+ the user accepted. */
1481
1609
  html[data-dshome-dispersion][data-dshome-glass="liquid"] body .wSkVaW_header:hover::before,
1482
1610
  html[data-dshome-dispersion][data-dshome-glass="liquid"] body ${LIQUID_BUBBLE}{
1483
1611
  -webkit-backdrop-filter:url(#dshome-glass-dispersion) blur(var(--dshome-liquid-blur)) saturate(var(--dshome-liquid-saturate))!important;
1484
1612
  backdrop-filter:url(#dshome-glass-dispersion) blur(var(--dshome-liquid-blur)) saturate(var(--dshome-liquid-saturate))!important;
1485
1613
  }
1614
+ html[data-dshome-dispersion][data-dshome-glass="liquid"][data-dshome-composer="narrow"] body [data-composer-card]{
1615
+ -webkit-backdrop-filter:url(#dshome-glass-dispersion-composer) blur(var(--dshome-liquid-blur-composer)) saturate(var(--dshome-liquid-saturate))!important;
1616
+ backdrop-filter:url(#dshome-glass-dispersion-composer) blur(var(--dshome-liquid-blur-composer)) saturate(var(--dshome-liquid-saturate))!important;
1617
+ }
1618
+ html[data-dshome-dispersion][data-dshome-glass="liquid"][data-dshome-composer="wide"] body [data-composer-card]{
1619
+ -webkit-backdrop-filter:url(#dshome-glass-dispersion-composer-wide) blur(var(--dshome-liquid-blur-composer)) saturate(var(--dshome-liquid-saturate))!important;
1620
+ backdrop-filter:url(#dshome-glass-dispersion-composer-wide) blur(var(--dshome-liquid-blur-composer)) saturate(var(--dshome-liquid-saturate))!important;
1621
+ }
1622
+ html[data-dshome-dispersion][data-dshome-glass="liquid"][data-dshome-composer="off"] body [data-composer-card]{
1623
+ -webkit-backdrop-filter:blur(var(--dshome-liquid-blur-composer)) saturate(var(--dshome-liquid-saturate))!important;
1624
+ backdrop-filter:blur(var(--dshome-liquid-blur-composer)) saturate(var(--dshome-liquid-saturate))!important;
1625
+ }
1486
1626
  /* The workspace list is faded out at its bottom by a gradient whose end stop is
1487
1627
  the sidebar-fill token — an OPAQUE colour matched to the product's own sidebar
1488
1628
  background. Over a translucent glass pane that stop no longer matches what is
@@ -1503,9 +1643,20 @@ html[data-dshome-dispersion][data-dshome-glass="liquid"] body .hHd-Xa_root::befo
1503
1643
  -webkit-backdrop-filter:url(#dshome-glass-dispersion) blur(var(--dshome-liquid-blur-sidebar)) saturate(var(--dshome-liquid-saturate))!important;
1504
1644
  backdrop-filter:url(#dshome-glass-dispersion) blur(var(--dshome-liquid-blur-sidebar)) saturate(var(--dshome-liquid-saturate))!important;
1505
1645
  }
1646
+ /* Backdrop blur, switched off by the durable backdropBlur setting. Each glass
1647
+ recipe reads its radius from a variable, so turning the frosting off is one
1648
+ 0px per variable -- the translucency, the sheen and the edge refraction all
1649
+ stay exactly as they were. Written with a body step so it also out-ranks the
1650
+ html[data-dshome-glass] block the liquid radii are declared in. */
1651
+ html[data-dshome-blur="off"],
1652
+ html[data-dshome-blur="off"] body{
1653
+ --dshome-glass-blur:0px;
1654
+ --dshome-liquid-blur:0px;
1655
+ --dshome-liquid-blur-sidebar:0px;
1656
+ --dshome-liquid-blur-composer:0px;
1657
+ }
1506
1658
  @media (prefers-reduced-motion:reduce){
1507
- html[data-dshome-glass="liquid"] .hHd-Xa_root::after,
1508
- html[data-dshome-glass="liquid"] ${LIQUID_BUBBLE}::after{
1659
+ html[data-dshome-glass="liquid"] .hHd-Xa_root::after{
1509
1660
  animation:none;
1510
1661
  }
1511
1662
  }
@@ -1577,12 +1728,17 @@ html[data-dshome-dispersion][data-dshome-glass="liquid"] body .hHd-Xa_root::befo
1577
1728
  function createDarkSync(ctx, theme) {
1578
1729
  const apply = () => {
1579
1730
  let dark = false;
1580
- // The product's own attribute is authoritative when it is there: it
1581
- // is the same signal its `--dsw-alias-*` tokens are keyed on, and the
1582
- // page background is painted from this marker, so the two must not be
1583
- // allowed to drift apart.
1584
- if (document.body.hasAttribute('data-ds-dark-theme')) dark = true;
1585
- if (!dark && theme !== undefined && theme !== null && typeof theme.getTheme === 'function') {
1731
+ // Two signals, and BOTH of them lie in some state -- measured, not
1732
+ // assumed:
1733
+ // * the theme service reported `light` while the page was in dark
1734
+ // mode, so treating it as the only source painted a dark page light;
1735
+ // * `data-ds-dark-theme` was still on <body> after a switch back to
1736
+ // light, so treating IT as authoritative painted a light page dark.
1737
+ // This is the precedence that shipped through 1.43.1 and behaved in both
1738
+ // modes: ask the service first, and let the product's own attribute
1739
+ // override a "not dark" answer. Keep it, and sanity-check the signal
1740
+ // before touching it again.
1741
+ if (theme !== undefined && theme !== null && typeof theme.getTheme === 'function') {
1586
1742
  try {
1587
1743
  const snap = theme.getTheme();
1588
1744
  dark = snap !== undefined && snap !== null &&
@@ -1592,6 +1748,7 @@ html[data-dshome-dispersion][data-dshome-glass="liquid"] body .hHd-Xa_root::befo
1592
1748
  dark = false;
1593
1749
  }
1594
1750
  }
1751
+ if (!dark) dark = document.body.hasAttribute('data-ds-dark-theme');
1595
1752
  if (dark) document.body.setAttribute('data-dshome-dark', '');
1596
1753
  else document.body.removeAttribute('data-dshome-dark');
1597
1754
  };
@@ -1629,12 +1786,16 @@ html[data-dshome-dispersion][data-dshome-glass="liquid"] body .hHd-Xa_root::befo
1629
1786
  const root = document.documentElement;
1630
1787
  const apply = () => {
1631
1788
  root.setAttribute('data-dshome-glass', dsttGetGlass());
1789
+ root.setAttribute('data-dshome-composer', dsttGetComposer());
1790
+ root.setAttribute('data-dshome-blur', dsttGetBlur() ? 'on' : 'off');
1632
1791
  };
1633
1792
  apply();
1634
1793
  const unsubscribe = dsttSubscribe(apply);
1635
1794
  return () => {
1636
1795
  unsubscribe();
1637
1796
  root.removeAttribute('data-dshome-glass');
1797
+ root.removeAttribute('data-dshome-composer');
1798
+ root.removeAttribute('data-dshome-blur');
1638
1799
  };
1639
1800
  }
1640
1801
 
@@ -3258,6 +3419,18 @@ body[data-dshome-dark] .${CLS}{background:rgba(14,30,60,.94);box-shadow:0 18px 4
3258
3419
  liquid: { zh: "液态", en: "Liquid" },
3259
3420
  frosted: { zh: "白磨砂", en: "Frosted" }
3260
3421
  };
3422
+ // How wide the composer's edge refraction is under liquid, measured on the
3423
+ // top and bottom edges -- the only two where a displacement has anything to
3424
+ // show. The stylesheet gates the three states on <html data-dshome-composer>.
3425
+ // `origin` was this option's first id and is still accepted from a settings
3426
+ // file written before the rename; it reads back as `wide`.
3427
+ const DSTT_COMPOSER = ["narrow", "wide", "off"];
3428
+ const DSTT_COMPOSER_DEFAULT = "narrow";
3429
+ const DSTT_COMPOSER_LABELS = {
3430
+ narrow: { zh: "窄 8px", en: "Narrow 8px" },
3431
+ wide: { zh: "宽 16px", en: "Wide 16px" },
3432
+ off: { zh: "无", en: "Off" }
3433
+ };
3261
3434
 
3262
3435
  /**
3263
3436
  * Shared in-memory mirror of the durable DSTT mode. The settings domain's
@@ -3265,21 +3438,86 @@ body[data-dshome-dark] .${CLS}{background:rgba(14,30,60,.94);box-shadow:0 18px 4
3265
3438
  * plugin's own loopback channel instead; every reader (the color driver,
3266
3439
  * the settings tab) subscribes to this mirror.
3267
3440
  */
3268
- const dsttState = { mode: "peakvalley-redblue", fluidBrush: false, glassStyle: DSTT_GLASS_DEFAULT, listeners: new Set() };
3441
+ const dsttState = { mode: "peakvalley-redblue", fluidBrush: false, glassStyle: DSTT_GLASS_DEFAULT, composer: DSTT_COMPOSER_DEFAULT, blur: true, listeners: new Set() };
3442
+ // Fields the user has chosen in this session. A host half that predates a
3443
+ // field ignores it and answers without it -- or with its own default -- so
3444
+ // without this a fresh choice gets overwritten by the next sync and the
3445
+ // option appears to snap straight back to its default.
3446
+ const dsttTouched = new Set();
3269
3447
  const dsttGetMode = () => dsttState.mode;
3270
3448
  const dsttGetGlass = () => (DSTT_GLASS_STYLES.includes(dsttState.glassStyle) ? dsttState.glassStyle : DSTT_GLASS_DEFAULT);
3271
- /** Persist the glass recipe through the bridge; resolves with success. */
3449
+ const dsttGetComposer = () => {
3450
+ if (dsttState.composer === "origin") return "wide";
3451
+ return DSTT_COMPOSER.includes(dsttState.composer) ? dsttState.composer : DSTT_COMPOSER_DEFAULT;
3452
+ };
3453
+ const dsttGetBlur = () => dsttState.blur !== false;
3454
+ /**
3455
+ * Turn the glass frosting on or off. Same shape as the other CSS-only
3456
+ * preferences: apply locally first, then let the host persist it, and treat
3457
+ * its answer as confirmation only.
3458
+ */
3459
+ async function dsttSetBlur(ctx, next) {
3460
+ dsttState.blur = next === true;
3461
+ dsttTouched.add("blur");
3462
+ dsttNotify();
3463
+ try {
3464
+ const result = await bridgeCall(DSTT_SET, { mode: dsttState.mode, backdropBlur: dsttState.blur });
3465
+ if (result !== undefined && result !== null && result.ok === true) {
3466
+ return result.value.backdropBlur === dsttState.blur;
3467
+ }
3468
+ } catch (error) {
3469
+ // Fall through: the local choice stands, it just was not stored.
3470
+ }
3471
+ return false;
3472
+ }
3473
+ /**
3474
+ * Persist the composer's refraction width through the bridge.
3475
+ *
3476
+ * Applied locally FIRST, before the round trip. Both this and glassStyle
3477
+ * only flip a CSS attribute, so the choice is fully usable the moment it is
3478
+ * made — and it has to be, because a running dsh keeps the host half it
3479
+ * booted with: until `dsh web` is restarted, `dstt.mode.set` ignores fields
3480
+ * it does not know and answers without them, which used to make a brand new
3481
+ * setting look broken. The cost of applying first is that a choice made
3482
+ * against a stale host is not persisted and reverts on reload, so the
3483
+ * return value still reports the truth.
3484
+ */
3485
+ async function dsttSetComposer(ctx, next) {
3486
+ if (!DSTT_COMPOSER.includes(next)) return false;
3487
+ dsttState.composer = next;
3488
+ dsttTouched.add("composer");
3489
+ dsttNotify();
3490
+ try {
3491
+ const result = await bridgeCall(DSTT_SET, { mode: dsttState.mode, composerRefraction: next });
3492
+ // The response is used ONLY to confirm. It must never be written back
3493
+ // into the mirror: a host that accepts the call but does not store the
3494
+ // field answers with its own value (the default, `narrow`), and copying
3495
+ // that in is what made the option snap straight back the moment the
3496
+ // round trip landed.
3497
+ if (result !== undefined && result !== null && result.ok === true && DSTT_COMPOSER.includes(result.value.composerRefraction)) {
3498
+ return result.value.composerRefraction === next;
3499
+ }
3500
+ } catch (error) {
3501
+ // Fall through: the local choice stands, it just was not stored.
3502
+ }
3503
+ return false;
3504
+ }
3505
+ /** Persist the glass recipe through the bridge; resolves with success.
3506
+ * Applied locally first for the same reason as dsttSetComposer above. */
3272
3507
  async function dsttSetGlass(ctx, next) {
3273
3508
  if (!DSTT_GLASS_STYLES.includes(next)) return false;
3509
+ dsttState.glassStyle = next;
3510
+ dsttTouched.add("glassStyle");
3511
+ dsttNotify();
3274
3512
  try {
3275
3513
  const result = await bridgeCall(DSTT_SET, { mode: dsttState.mode, glassStyle: next });
3514
+ // Confirmation only -- see dsttSetComposer for why the host's echo must
3515
+ // not be copied back into the mirror.
3276
3516
  if (result !== undefined && result !== null && result.ok === true && DSTT_GLASS_STYLES.includes(result.value.glassStyle)) {
3277
- dsttState.glassStyle = result.value.glassStyle;
3278
- dsttNotify();
3279
- return true;
3517
+ return result.value.glassStyle === next;
3280
3518
  }
3281
3519
  } catch (error) {
3282
- // Fall through to the caller's failure handling.
3520
+ // Fall through: the local choice stands, it just was not stored.
3283
3521
  }
3284
3522
  return false;
3285
3523
  }
@@ -3327,6 +3565,9 @@ body[data-dshome-dark] .${CLS}{background:rgba(14,30,60,.94);box-shadow:0 18px 4
3327
3565
  return await response.json();
3328
3566
  }
3329
3567
 
3568
+ /** Fallback used when the host half is unavailable. */
3569
+ const dsttGetBlurFallback = () => dsttState.blur !== false;
3570
+
3330
3571
  /** Pull the durable mode from the host half once. */
3331
3572
  async function dsttSync(ctx) {
3332
3573
  try {
@@ -3335,8 +3576,13 @@ body[data-dshome-dark] .${CLS}{background:rgba(14,30,60,.94);box-shadow:0 18px 4
3335
3576
  dsttState.mode = result.value.mode;
3336
3577
  dsttState.fluidBrush = result.value.fluidBrush === true;
3337
3578
  // An older host half answers without a glass recipe: keep the
3338
- // default rather than clearing the attribute to nothing.
3339
- if (DSTT_GLASS_STYLES.includes(result.value.glassStyle)) dsttState.glassStyle = result.value.glassStyle;
3579
+ // default rather than clearing the attribute to nothing. And a
3580
+ // field the user has already chosen this session is never taken
3581
+ // back from the host -- a stale host answers with its own default,
3582
+ // which is what made a fresh choice look like it snapped back.
3583
+ if (!dsttTouched.has("glassStyle") && DSTT_GLASS_STYLES.includes(result.value.glassStyle)) dsttState.glassStyle = result.value.glassStyle;
3584
+ if (!dsttTouched.has("composer") && DSTT_COMPOSER.includes(result.value.composerRefraction)) dsttState.composer = result.value.composerRefraction;
3585
+ if (!dsttTouched.has("blur") && typeof result.value.backdropBlur === "boolean") dsttState.blur = result.value.backdropBlur;
3340
3586
  dsttNotify();
3341
3587
  }
3342
3588
  } catch (error) {}
@@ -3592,6 +3838,16 @@ body[data-dshome-dark] .${CLS}{background:rgba(14,30,60,.94);box-shadow:0 18px 4
3592
3838
  if (dstt === undefined || typeof dstt.setGlass !== 'function' || next === glassNow) return;
3593
3839
  dstt.setGlass(next);
3594
3840
  };
3841
+ const composerNow = dstt !== undefined && typeof dstt.composer === 'function' ? dstt.composer() : DSTT_COMPOSER_DEFAULT;
3842
+ const pickComposer = (next) => {
3843
+ if (dstt === undefined || typeof dstt.setComposer !== 'function' || next === composerNow) return;
3844
+ dstt.setComposer(next);
3845
+ };
3846
+ const blurOn = dstt !== undefined && typeof dstt.blur === 'function' ? dstt.blur() !== false : true;
3847
+ const toggleBlur = () => {
3848
+ if (dstt === undefined || typeof dstt.setBlur !== 'function') return;
3849
+ dstt.setBlur(!blurOn);
3850
+ };
3595
3851
 
3596
3852
  return React.createElement('div', { className: 'dshome-dstt' },
3597
3853
  React.createElement('h3', null,
@@ -3643,6 +3899,34 @@ body[data-dshome-dark] .${CLS}{background:rgba(14,30,60,.94);box-shadow:0 18px 4
3643
3899
  className: 'dshome-dstt-seg-btn' + (glassNow === style ? ' dshome-on' : ''),
3644
3900
  onClick: () => pickGlass(style)
3645
3901
  }, isZh ? DSTT_GLASS_LABELS[style].zh : DSTT_GLASS_LABELS[style].en)))),
3902
+ React.createElement('div', { className: 'dshome-dstt-row' },
3903
+ React.createElement('div', { className: 'dshome-dstt-row-body' },
3904
+ React.createElement('div', { className: 'dshome-dstt-row-title' }, isZh ? '对话框边缘倒影' : 'Composer edge reflection'),
3905
+ React.createElement('div', { className: 'dshome-dstt-row-hint' }, isZh
3906
+ ? '液态下对话框边缘的折射带宽(按上下量——左右背后是连续背景,同样的位移看不出变化)。窄 8px(默认)/ 宽 16px / 无,三者都是专为这张卡片做的位移图。'
3907
+ : 'Width of the lens band at the composer edge under liquid, measured top and bottom — left and right sit over a continuous backdrop where the same displacement shows nothing. Narrow 8px (default) / Wide 16px / Off; each is a map built for this card.')),
3908
+ React.createElement('div', { className: 'dshome-dstt-seg', role: 'radiogroup', 'aria-label': isZh ? '对话框边缘倒影' : 'Composer edge reflection' },
3909
+ DSTT_COMPOSER.map((choice) => React.createElement('button', {
3910
+ key: choice,
3911
+ type: 'button',
3912
+ role: 'radio',
3913
+ 'aria-checked': composerNow === choice,
3914
+ className: 'dshome-dstt-seg-btn' + (composerNow === choice ? ' dshome-on' : ''),
3915
+ onClick: () => pickComposer(choice)
3916
+ }, isZh ? DSTT_COMPOSER_LABELS[choice].zh : DSTT_COMPOSER_LABELS[choice].en)))),
3917
+ React.createElement('div', { className: 'dshome-dstt-row' },
3918
+ React.createElement('div', { className: 'dshome-dstt-row-body' },
3919
+ React.createElement('div', { className: 'dshome-dstt-row-title' }, isZh ? '背景模糊(磨砂)' : 'Backdrop blur'),
3920
+ React.createElement('div', { className: 'dshome-dstt-row-hint' }, isZh
3921
+ ? '玻璃面板后面那层虚化。关闭后玻璃变成纯半透明:通透度、顶部光泽、边缘折射都保留,只是不再把背后的东西磨掉。'
3922
+ : 'The frosting behind every glass pane. Off leaves the translucency, the sheen and the edge refraction intact and only stops blurring what is behind the glass.')),
3923
+ React.createElement('button', {
3924
+ type: 'button',
3925
+ role: 'switch',
3926
+ 'aria-checked': blurOn,
3927
+ className: 'dshome-dstt-seg-btn' + (blurOn ? ' dshome-on' : ''),
3928
+ onClick: toggleBlur
3929
+ }, blurOn ? (isZh ? '已开启' : 'On') : (isZh ? '已关闭' : 'Off'))),
3646
3930
  React.createElement('div', { className: 'dshome-dstt-status', 'data-peak': timeSliced && peakNow },
3647
3931
  '当前:' + colorName + ' · ' + status));
3648
3932
  }
@@ -3760,6 +4044,10 @@ body[data-dshome-dark] .${CLS}{background:rgba(14,30,60,.94);box-shadow:0 18px 4
3760
4044
  setBrush: (next) => dsttSetBrush(ctx, next),
3761
4045
  glass: dsttGetGlass,
3762
4046
  setGlass: (next) => dsttSetGlass(ctx, next),
4047
+ composer: dsttGetComposer,
4048
+ setComposer: (next) => dsttSetComposer(ctx, next),
4049
+ blur: dsttGetBlur,
4050
+ setBlur: (next) => dsttSetBlur(ctx, next),
3763
4051
  subscribe: dsttSubscribe
3764
4052
  },
3765
4053
  locale: ctx.get('locale')
@@ -3771,5 +4059,13 @@ body[data-dshome-dark] .${CLS}{background:rgba(14,30,60,.94);box-shadow:0 18px 4
3771
4059
  exports.apply = apply;
3772
4060
  exports.inject = ['theme', 'slots', 'locale', 'workspaces', 'connection'];
3773
4061
  return module.exports;
3774
- }
3775
- });
4062
+ };
4063
+
4064
+ // Both ids, same factory. See the note at the top of this file for why one
4065
+ // literal cannot work during the rename.
4066
+ try {
4067
+ window.__ModuleLoader__.load({ id: "dsh-deepseek-style-theme", factory: dshDeepseekStyleThemeFactory });
4068
+ } catch (error) {}
4069
+ try {
4070
+ window.__ModuleLoader__.load({ id: "@dsh-external/dsh-deepseek-style-theme", factory: dshDeepseekStyleThemeFactory });
4071
+ } catch (error) {}