dsh-deepseek-style-theme 2.0.82 → 2.0.85

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.
Files changed (3) hide show
  1. package/README.md +4 -3
  2. package/lib/client.js +341 -103
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # DeepSeek Style Theme【在抢修】
1
+ # DeepSeek Style Theme
2
2
 
3
3
  为 DeepSeek Harness Web GUI 复刻 DeepSeek 官网视觉的主题皮肤插件。
4
4
 
@@ -24,7 +24,7 @@
24
24
  - **标题栏**:默认透明,悬停变毛玻璃;栏内按钮统一为真圆角(覆盖产品全局 corner-shape:superellipse 造成的方角观感),会话标题小胶囊化
25
25
  - **悬停动画**:中/E、「打开方式」、「对话 / 轨迹」标签悬停时上浮 + 品牌色辉光;中/E 短标签「中/E」平滑展开为「中文/EN」(max-width 过渡,非 display 硬切);「打开方式」展开菜单时箭头翻转,切换标签时选中项弹出
26
26
  - **DeepSeek 品牌链接**:点击侧边栏 DeepSeek 标识跳转 `https://www.deepseek.com/`
27
- - **轨迹视图**:「对话 / 轨迹」标签常驻,可随时切回对话
27
+ - **轨迹视图**:「对话 / 轨迹」标签在标题栏悬停(`.dshome-swap`)或轨迹视图存在时显示,**不是常驻**——会话视图下实测 `[class*="wSkVaW_tabs"]` 默认 `display:none`(2.0.84 按实机报告 B4 勘误)
28
28
  - **运行中子代理面板**:有子代理启动时,右下角浮出玻璃面板,实时列出正在运行的子代理——名称、已运行时长、token 用量与流动进度条;点击条目直接跳转到该子代理会话,可折叠收起
29
29
  - **跨平台**:打开工作区目录时按系统调用文件管理器(Windows Explorer / macOS Finder / Linux 默认文件管理器)
30
30
  - **模型目录同步(1.40.0+)**:每次插件启动向 DeepSeek 端点询问一次模型列表,与 `llm-deepseek` 目录比对后按需对齐——「选择器里有哪些模型」由接口说了算,但**绝不臆造能力位、绝不清空你的条目**,移除项会逐个记日志;可用 `catalogSync: auto | add | off` 控制(详见下文)
@@ -318,7 +318,8 @@ dsh plugin --profile web remove dsh-deepseek-style-theme
318
318
  | `symptom-probe-selftest.mjs` | 上面那份探针的自测(19 项):在一个合成页面上跑(我们自己的 canvas + 带超宽子元素的消息列 + 每 60 ms 改一次 transform 的卡片 + CDP 派发真实鼠标移动),断言环境报告的字段形状、`run()` 的采样与逐字段 diff、五个开关的实际效果(含 `clip` 在 `overflow-y:auto` 旁会计算成 `hidden` 这条 CSS Overflow 3 规则) | `chrome --headless=new --remote-debugging-port=9333 --user-data-dir=%TEMP%\dsh-probe-chrome about:blank`,然后 `node tools/symptom-probe-selftest.mjs` |
319
319
  | `bridge-smoke.mjs` | 宿主端私有 RPC 通道:协议、错误路径,以及完整围栏(非回环对端 / 缺 `Host` / DNS rebinding / 跨站来源 / `Origin` 不匹配 / 非 JSON 类型 / UNC 路径 / `file.*` 端点识别与校验,24 项检查;加 `--open` 为 25 项,会真的用系统默认应用打开一个临时文件,**会在桌面弹出窗口**,默认不启用) | `node tools/bridge-smoke.mjs [已安装的 lib/index.js] [--open]` |
320
320
  | `dstt-schema-smoke.mjs` | DSTT 设置 schema 与写入路径的一致性:**每个能写进 `settings.yaml` 的值都必须过 `register()` 的校验**(1.43.2 那次 schema 缺 `wide` 的发布阻塞就靠它守)。含背景方式四档的往返、未知 id 拒写、自定义背景的归一化(控制字符→空格、去首尾、长度上限)、动态背景开关的读写与布尔回读,以及「客户端枚举 = 宿主枚举」的跨文件检查(29 项) | `node tools/dstt-schema-smoke.mjs [lib/index.js]` |
321
- | `bg-recipes-smoke.mjs` | 客户端源码层的数据与契约断言(2.0.73+,随每版增补):① 必须含纯白 + 近白、② 只有两个颜色参与、`bold` 浅色无 `#FFFFFF`、③ 的基底/让位规则与选择器守卫、壁纸引擎标记与「提示而非自动切换」、桌面壁纸端点与 `WallpaperStyle` 映射、`darkSync` 作用域、两条反引号守卫(整表扫描)、发送按钮 hover 不再位移、流场 16F 探测与回退、**悬停倾斜的抬升 1.01 与 `TILT_CONTROLS` 不含文字录入、进入控件时冻结而非释放、释放只在离开卡片矩形、两套玻璃配方都生效(2.0.82)**、横向裁切覆盖消息列/对话框/转写区(64 项) | `node tools/bg-recipes-smoke.mjs [lib/client.js]` |
321
+ | `bg-recipes-smoke.mjs` | 客户端源码层的数据与契约断言(2.0.73+,随每版增补):① 必须含纯白 + 近白、② 只有两个颜色参与、`bold` 浅色无 `#FFFFFF`、③ 的基底/让位规则与选择器守卫、壁纸引擎标记与「提示而非自动切换」、桌面壁纸端点与 `WallpaperStyle` 映射、`darkSync` 作用域、两条反引号守卫(整表扫描)、发送按钮 hover 不再位移、流场 16F 探测与回退、悬停倾斜的抬升 1.01 与 `TILT_CONTROLS` 不含文字录入、进入控件时冻结而非释放、释放只在离开卡片矩形、两套玻璃配方都生效(2.0.82)、横向裁切覆盖消息列/对话框/转写区、**8 个 CSS 契约名必须已声明 + 模板插值不得早于 const 声明(2.0.84:`ATTR` 被误删、`USER_BUBBLE` 提前插值都属于这一类,`node --check` 看不见)**(73 项) | `node tools/bg-recipes-smoke.mjs [lib/client.js]` |
322
+ | `parse-smoke.mjs` | **每个文件都必须能解析**(2.0.84+):`lib/` 与 `tools/` 下每个 `.js/.mjs` 过一遍 `node --check`。看着多余,其实不是——已经有三次"注释里的反引号把模板字符串提前结束"的事故(`lib/client.js` 两次、`tools/gui-probe.mjs` 一次,后者 Node 的报错还不带文件名)。纯解析门,不做 lint | `node tools/parse-smoke.mjs` |
322
323
  | `fluid-precision-test.mjs` | **流场存储精度对照实验**(2.0.81+):把 `lib/client.js` 里的真实 `VERTEX_SHADER` / `FLOW_SHADER` 抠出来,在无头 Chrome 里对同一场景各跑 240 步,分别存 RGBA8 与 RGBA16F,读回统计"取值分布 / 落在 1/255 网格上的层级 / max / mean",并判定 16F 是否真的摆脱了量化(5 项)。**需要 Chromium 系浏览器**,且 Chrome 在受限进程里起不来,请从普通终端跑 | `node tools/fluid-precision-test.mjs [--chrome <路径>] [--keep]` |
323
324
  | `catalog-sync-smoke.mjs` | 模型目录同步的全部分支:一致 / 可描述漂移 / 聚合网关目录 / 只追加 / `off` / baseURL 解析顺序 / 无密钥 / 端点故障 / 命名空间未就绪 / 版本冲突(27 项检查,全用替身,无需凭据与网络) | `node tools/catalog-sync-smoke.mjs [已安装的 lib/index.js]` |
324
325
  | `catalog-sync-live.mjs` | 用**真实端点 + 真实密钥**跑一遍同步:A 场景(目录已一致)应 0 写入,B 场景(人为制造漂移)应恰好 1 次写入并复原条目;写入被拦下,不落盘 | `DEEPSEEK_API_KEY=... node tools/catalog-sync-live.mjs [--profile web] [--drift-id deepseek-v4-pro]` |
package/lib/client.js CHANGED
@@ -499,6 +499,15 @@ void main() {
499
499
  let previous = 0;
500
500
  const step = 1e3 / 30;
501
501
  const frame = (now) => {
502
+ // Self-cleanup: a canvas that has left the DOM must not keep
503
+ // simulating. This is the backstop for a mount that threw midway --
504
+ // B1 in the 2026-09-16 report leaked one full-resolution simulator per
505
+ // 动态背景 flip because the disposer never came back.
506
+ if (!canvas.isConnected) {
507
+ running = false;
508
+ cancelAnimationFrame(raf);
509
+ return;
510
+ }
502
511
  raf = requestAnimationFrame(frame);
503
512
  if (now - previous < step) return;
504
513
  previous = now - (now - previous) % step;
@@ -690,6 +699,21 @@ void main() {
690
699
  var FILTER_ID = "dshome-glass-dispersion";
691
700
  var TINT_ID = "dshome-glass-dispersion-tint";
692
701
  var REFRACT_ID = "dshome-glass-dispersion-refract";
702
+ /**
703
+ * The opt-in marker on <html> that the stylesheets chain `url(#…)` behind.
704
+ *
705
+ * This declaration was DELETED in d862487 by accident and stayed missing
706
+ * from 1.43.6 through 2.0.82, which is the whole of B1 in the 2026-09-16 bug
707
+ * report: `startGlassDispersion()` threw a ReferenceError on its first line
708
+ * (`root.hasAttribute(ATTR)`), so the refraction never mounted on any
709
+ * version -- and because `startAmbient()` calls into this function, the
710
+ * ambient mount died with it, taking the disposer with it (the 动态背景
711
+ * toggle stopped unmounting, and each flip leaked a full-resolution fluid
712
+ * simulator). Two lessons are encoded here: the constant is declared next to
713
+ * the ids it belongs to, and the ambient mount no longer swallows a throw
714
+ * silently (see its try/catch and the caller's console.error).
715
+ */
716
+ var ATTR = "data-dshome-dispersion";
693
717
  var TALL_FILTER_ID = "dshome-glass-dispersion-tall";
694
718
  var TALL_TINT_ID = "dshome-glass-dispersion-tall-tint";
695
719
  var TALL_REFRACT_ID = "dshome-glass-dispersion-tall-refract";
@@ -1222,10 +1246,47 @@ body[data-dshome-dark] .dshome-sa-item.dshome-sa-stopping .dshome-sa-bar{backgro
1222
1246
  @media (max-width:760px){.dshome-sa{right:10px;bottom:10px;width:min(286px,calc(100vw - 20px))}}
1223
1247
  `;
1224
1248
 
1249
+ // ── USER_BUBBLE: the one selector both recipes glass user bubbles with ────
1250
+ // Declared HERE, above PATCH_BLOCKS and GLASS_CSS, because both interpolate
1251
+ // it while they are being built: as a `const` declared further down it threw
1252
+ // `Cannot access 'USER_BUBBLE' before initialization` the moment the factory
1253
+ // ran, which took the entire theme down (no style tags at all). Same failure
1254
+ // class as B1 -- an identifier that resolves to nothing at activation time --
1255
+ // and the reason tools/factory-smoke.mjs exists.
1256
+ //
1257
+ // Two properties are load-bearing, and both were learned the hard way
1258
+ // (B5 / B2 of the 2026-09-16 bug report):
1259
+ //
1260
+ // 1. It is ONE compound selector — no top-level comma. Used as a prefix in
1261
+ // `html[data-dshome-glass="liquid"] :is(USER_BUBBLE){…}` rules, a comma
1262
+ // list is a trap: the prefix binds only to the branch it touches and every
1263
+ // later branch escapes the gate entirely. That is exactly what happened.
1264
+ // A second branch, meant to cover a just-typed bubble, also matched the
1265
+ // product's TOOLTIP (`_bubble_<hash>_<n>`, `position:fixed`), and the
1266
+ // theme's `position:relative` turned that floating tooltip into an
1267
+ // in-flow element — 78px of width inside the composer, 48px inside a
1268
+ // message action bar — so hovering the send or copy button shoved the
1269
+ // button out from under the cursor, the tooltip unmounted, the button
1270
+ // came back, and the loop fed itself at up to 60Hz. That WAS the user's
1271
+ // 「按钮跳动」. Every usage is wrapped in `:is(…)` now, so a future second
1272
+ // branch cannot escape the gate again.
1273
+ // 2. The tooltip must be excluded by SHAPE. Message bubbles are
1274
+ // `<hash>_bubble` (`Sixlwa_bubble`, `oRe1gG_bubble`); the tooltip is the
1275
+ // leading-underscore form `_bubble_…`, which is what the first `:not()`
1276
+ // keys on. `:not([role="tooltip"])` is belt-and-braces for the day the
1277
+ // product starts labelling it.
1278
+ //
1279
+ // The gap the deleted branch covered — a message typed while the agent is
1280
+ // still working sits outside any flow-kind container until the turn picks it
1281
+ // up — is knowingly left unglazed for that moment: a transient unglazed
1282
+ // bubble is a cosmetic wait, a teleporting button is not.
1283
+ const USER_BUBBLE = ':is([data-chat-flow-kind="user"],[data-chat-flow-kind="steering"]) [class*="bubble" i]:not([class*="_bubble_"]):not([role="tooltip"])';
1284
+
1225
1285
  // PATCH_BLOCKS: every rule group that targets product build-time class
1226
1286
  // names. `anchor` is the class that must exist in the DOM before the
1227
1287
  // block is injected; each block degrades independently when its surface
1228
- // renames in a product rebuild.
1288
+ // renames in a product rebuild, and `anchor` may be a list of
1289
+ // alternatives when a surface has already been renamed once.
1229
1290
  const PATCH_BLOCKS = [
1230
1291
  // Frame / sidebar / details columns.
1231
1292
  { anchor: ".pI_x6G_frame", css: `
@@ -1314,7 +1375,7 @@ body[data-dshome-dark] .wSkVaW_tabActive{box-shadow:0 3px 12px color-mix(in srgb
1314
1375
  /* Header chips: the product's global corner-shape:superellipse(1.5) squares off
1315
1376
  even pill radii, so every control in the bar is restored to a true round end
1316
1377
  (and the session-title crumb, 12px on a 28px box, becomes a real pill). */
1317
- .wSkVaW_header .SVAs4q_label,.wSkVaW_header .cubgiG_seat,.wSkVaW_header .CAgGvG_split,.wSkVaW_header .dshome-locale,.wSkVaW_header [data-sidebar-right-expand],.wSkVaW_header .nL4_yW_sessionLogButton,.wSkVaW_header .wSkVaW_crumb{corner-shape:round}
1378
+ .wSkVaW_header .SVAs4q_label,.wSkVaW_header .cubgiG_seat,.wSkVaW_header .CAgGvG_split,.wSkVaW_header .dshome-locale,.wSkVaW_header [data-sidebar-right-expand],.wSkVaW_header [class*="_moreButton"],.wSkVaW_header .wSkVaW_crumb{corner-shape:round}
1318
1379
  .wSkVaW_header .wSkVaW_crumb{border-radius:999px}
1319
1380
  /* Open-in-app split button: hover lift with a brand tint, and the chevron turns
1320
1381
  over while its menu is open. */
@@ -1328,13 +1389,15 @@ body[data-dshome-dark] .wSkVaW_tabActive{box-shadow:0 3px 12px color-mix(in srgb
1328
1389
  body[data-dshome-dark] .wSkVaW_header .CAgGvG_split:hover{border-color:color-mix(in srgb,var(--ds-brand) 52%,transparent);background:color-mix(in srgb,var(--ds-brand) 16%,transparent);box-shadow:0 4px 16px color-mix(in srgb,var(--ds-brand) 30%,transparent)}
1329
1390
  body[data-dshome-dark] .wSkVaW_header .CAgGvG_split:hover .CAgGvG_main,body[data-dshome-dark] .wSkVaW_header .CAgGvG_split:hover .CAgGvG_chevron{background:color-mix(in srgb,var(--ds-brand) 24%,transparent);color:#fff}
1330
1391
  ` },
1331
- // Session-log button (hover expand).
1332
- { anchor: ".nL4_yW_sessionLogButton", css: `
1333
- .nL4_yW_sessionLogButton{min-width:0!important;width:28px;height:28px;padding:0!important;border-radius:50%!important;border:none!important;background:color-mix(in srgb,var(--ds-brand) 8%,transparent)!important;color:var(--ds-brand)!important;overflow:hidden;transition:width .2s ease,padding .2s ease,border-radius .2s ease}
1334
- .nL4_yW_sessionLogButton span{max-width:0;opacity:0;overflow:hidden;transition:max-width .2s ease,opacity .2s ease}
1335
- .wSkVaW_header:hover .nL4_yW_sessionLogButton{width:auto;padding:0 12px!important;border-radius:18px!important}
1336
- .wSkVaW_header:hover .nL4_yW_sessionLogButton span{max-width:90px;opacity:1}
1337
- body[data-dshome-dark] .nL4_yW_sessionLogButton{color:var(--ds-brand-light)!important}
1392
+ // Session-log button (hover expand). The product renamed it to
1393
+ // `nL4_yW_moreButton` on the current build (B2, 2026-09-16), so anchor and
1394
+ // rules both key on the suffix, the way `[class*="_fade" i]` already does.
1395
+ { anchor: '[class*="_moreButton"]', css: `
1396
+ :is([class*="_moreButton"]){min-width:0!important;width:28px;height:28px;padding:0!important;border-radius:50%!important;border:none!important;background:color-mix(in srgb,var(--ds-brand) 8%,transparent)!important;color:var(--ds-brand)!important;overflow:hidden;transition:width .2s ease,padding .2s ease,border-radius .2s ease}
1397
+ :is([class*="_moreButton"]) span{max-width:0;opacity:0;overflow:hidden;transition:max-width .2s ease,opacity .2s ease}
1398
+ .wSkVaW_header:hover :is([class*="_moreButton"]){width:auto;padding:0 12px!important;border-radius:18px!important}
1399
+ .wSkVaW_header:hover :is([class*="_moreButton"]) span{max-width:90px;opacity:1}
1400
+ body[data-dshome-dark] :is([class*="_moreButton"]){color:var(--ds-brand-light)!important}
1338
1401
  ` },
1339
1402
  // Glass cards with spinning gradient border — upgraded to liquid glass:
1340
1403
  // specular top edge, saturated blur, brighter inner rim.
@@ -1386,17 +1449,21 @@ body[data-dshome-dark] .uV2eYG_card::before{background:conic-gradient(from var(-
1386
1449
  { anchor: ".wSkVaW_scrollBody", css: `
1387
1450
  [class*="scrollBody"],[class*="composerSeat"],[class*="viewArea"]{overflow-x:clip}
1388
1451
  ` },
1389
- // Empty-state headline + preview badge.
1390
- { anchor: ".pXSMma_headlineText", css: `
1391
- .pXSMma_headlineText{font-size:34px;line-height:42px;font-weight:700;letter-spacing:-.01em;background:linear-gradient(120deg,#152443 0%,var(--ds-brand-deep) 55%,var(--ds-brand) 100%);-webkit-background-clip:text;background-clip:text;color:transparent}
1392
- body[data-dshome-dark] .pXSMma_headlineText{background:linear-gradient(120deg,#fff 0%,var(--ds-brand-light) 60%,var(--ds-brand) 100%);-webkit-background-clip:text;background-clip:text}
1393
- .pXSMma_previewBadge{border-radius:999px!important;background:color-mix(in srgb,var(--ds-brand) 10%,transparent)!important;border:1px solid color-mix(in srgb,var(--ds-brand) 25%,transparent)!important;color:var(--ds-brand)!important}
1394
- .pXSMma_fish{color:var(--ds-brand)!important}
1452
+ // Empty-state headline + preview badge. The product renamed
1453
+ // `pXSMma_headlineText` to `pXSMma_headline` (B2, 2026-09-16), which
1454
+ // silently killed this block and left the headline at 26px/500 with no
1455
+ // gradient. Keyed on the suffix now, along with the two brand marks that
1456
+ // live in the same family.
1457
+ { anchor: '[class*="_headline"]', css: `
1458
+ :is([class*="_headline"]){font-size:34px;line-height:42px;font-weight:700;letter-spacing:-.01em;background:linear-gradient(120deg,#152443 0%,var(--ds-brand-deep) 55%,var(--ds-brand) 100%);-webkit-background-clip:text;background-clip:text;color:transparent}
1459
+ body[data-dshome-dark] :is([class*="_headline"]){background:linear-gradient(120deg,#fff 0%,var(--ds-brand-light) 60%,var(--ds-brand) 100%);-webkit-background-clip:text;background-clip:text}
1460
+ :is([class*="_previewBadge"]){border-radius:999px!important;background:color-mix(in srgb,var(--ds-brand) 10%,transparent)!important;border:1px solid color-mix(in srgb,var(--ds-brand) 25%,transparent)!important;color:var(--ds-brand)!important}
1461
+ :is([class*="_fish"]){color:var(--ds-brand)!important}
1395
1462
  ` },
1396
1463
  // Chat bubbles — liquid glass panes.
1397
- { anchor: ".gdEzaW_bubble", css: `
1398
- .gdEzaW_bubble{background:rgba(255,255,255,.6)!important;-webkit-backdrop-filter:blur(16px);backdrop-filter:blur(16px);border:1px solid rgba(0,0,0,.05);box-shadow:0 4px 16px rgba(13,30,60,.06)}
1399
- body[data-dshome-dark] .gdEzaW_bubble{background:rgba(21,42,86,.65)!important;border:1px solid rgba(255,255,255,.1);box-shadow:0 4px 16px rgba(0,0,0,.25)}
1464
+ { anchor: ".wSkVaW_scrollBody", css: `
1465
+ :is(${USER_BUBBLE}){background:rgba(255,255,255,.6)!important;-webkit-backdrop-filter:blur(16px);backdrop-filter:blur(16px);border:1px solid rgba(0,0,0,.05);box-shadow:0 4px 16px rgba(13,30,60,.06)}
1466
+ body[data-dshome-dark] :is(${USER_BUBBLE}){background:rgba(21,42,86,.65)!important;border:1px solid rgba(255,255,255,.1);box-shadow:0 4px 16px rgba(0,0,0,.25)}
1400
1467
  ` },
1401
1468
  // Trajectory view bar.
1402
1469
  { anchor: ".fV0t5q_root", css: `
@@ -1470,7 +1537,7 @@ body[data-dshome-dark]{
1470
1537
  }
1471
1538
  html .hHd-Xa_root::before,
1472
1539
  html .uV2eYG_card::after,
1473
- html .gdEzaW_bubble,
1540
+ html :is(${USER_BUBBLE}),
1474
1541
  html .fV0t5q_root,
1475
1542
  html [data-composer-card],
1476
1543
  html .VOzbGW_panel,
@@ -1489,27 +1556,27 @@ html .dshome-file-menu{
1489
1556
  --dshome-spec-x/y (written per element by startSpecularParallax). */
1490
1557
  html [data-dshome-spot]:hover{--dshome-glass-spec:1;}
1491
1558
  html [data-composer-card]:hover{--dshome-glass-spec:1;--dshome-glass-sheen:.12;}
1492
- html .gdEzaW_bubble:hover,html .dshome-sa:hover,html .dshome-file-menu:hover{
1559
+ html :is(${USER_BUBBLE}):hover,html .dshome-sa:hover,html .dshome-file-menu:hover{
1493
1560
  background-color:rgb(255 255 255 / calc(var(--dshome-glass-sheen) + .16))!important;
1494
1561
  }
1495
- body[data-dshome-dark] .gdEzaW_bubble:hover,
1562
+ body[data-dshome-dark] :is(${USER_BUBBLE}):hover,
1496
1563
  body[data-dshome-dark] .dshome-sa:hover,
1497
1564
  body[data-dshome-dark] .dshome-file-menu:hover{
1498
1565
  background-color:rgb(255 255 255 / calc(var(--dshome-glass-sheen) + .06))!important;
1499
1566
  }
1500
1567
  html[data-dshome-dispersion] .hHd-Xa_root::before,
1501
- html[data-dshome-dispersion] .gdEzaW_bubble,
1568
+ html[data-dshome-dispersion] :is(${USER_BUBBLE}),
1502
1569
  html[data-dshome-dispersion] .VOzbGW_panel{
1503
1570
  -webkit-backdrop-filter:url(#dshome-glass-dispersion) blur(var(--dshome-glass-blur)) saturate(var(--dshome-glass-saturate))!important;
1504
1571
  backdrop-filter:url(#dshome-glass-dispersion) blur(var(--dshome-glass-blur)) saturate(var(--dshome-glass-saturate))!important;
1505
1572
  }
1506
1573
  @supports not ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){
1507
- html .hHd-Xa_root::before,html .uV2eYG_card::after,html .gdEzaW_bubble,html [data-composer-card],html .VOzbGW_panel,html .dshome-sa,html .dshome-file-menu{
1574
+ html .hHd-Xa_root::before,html .uV2eYG_card::after,html :is(${USER_BUBBLE}),html [data-composer-card],html .VOzbGW_panel,html .dshome-sa,html .dshome-file-menu{
1508
1575
  background-color:rgba(255,255,255,.86)!important;
1509
1576
  }
1510
1577
  body[data-dshome-dark] .hHd-Xa_root::before,
1511
1578
  body[data-dshome-dark] .uV2eYG_card::after,
1512
- body[data-dshome-dark] .gdEzaW_bubble,
1579
+ body[data-dshome-dark] :is(${USER_BUBBLE}),
1513
1580
  body[data-dshome-dark] [data-composer-card],
1514
1581
  body[data-dshome-dark] .VOzbGW_panel,
1515
1582
  body[data-dshome-dark] .dshome-sa,
@@ -1560,14 +1627,9 @@ html[data-dshome-dispersion] .VOzbGW_panel{
1560
1627
  // a transcript can contain ONLY steering bubbles — anchoring the selector
1561
1628
  // on "user" alone silently skipped every bubble on the page.
1562
1629
  //
1563
- // The second half covers the gap the user hit: a message typed while the
1564
- // agent is still working is painted in the transcript BEFORE it is picked
1565
- // up, and at that moment it sits outside any flow-kind container, so the
1566
- // two anchored selectors miss it and it renders unglazed until the turn
1567
- // consumes it. Rather than enumerate the container it lands in, this side
1568
- // takes any bubble that is NOT part of an assistant step — which is the
1569
- // only place a bubble must not be glassed.
1570
- 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"] *)';
1630
+ // (USER_BUBBLE itself is declared above PATCH_BLOCKS: this sheet and the
1631
+ // patch blocks both interpolate it, so it has to exist before either is
1632
+ // built.)
1571
1633
  const LIQUID_CSS = `
1572
1634
  @property --dshome-liquid-sweep{syntax:"<angle>";inherits:false;initial-value:0deg}
1573
1635
  @keyframes dshome-liquid-sweep{from{--dshome-liquid-sweep:0deg}to{--dshome-liquid-sweep:360deg}}
@@ -1635,7 +1697,7 @@ body[data-dshome-dark]{
1635
1697
  html[data-dshome-glass="liquid"] body .hHd-Xa_root::before,
1636
1698
  html[data-dshome-glass="liquid"] body [data-composer-card],
1637
1699
  html[data-dshome-glass="liquid"] body .wSkVaW_header:hover::before,
1638
- html[data-dshome-glass="liquid"] body ${LIQUID_BUBBLE}{
1700
+ html[data-dshome-glass="liquid"] body :is(${USER_BUBBLE}){
1639
1701
  background-color:var(--dshome-liquid-fill)!important;
1640
1702
  background-image:
1641
1703
  radial-gradient(360px circle at calc(50% + var(--dshome-spec-x,0) * 50%) calc(50% + var(--dshome-spec-y,0) * 50%), rgb(255 255 255 / calc(var(--dshome-glass-spec) * .24)), transparent 60%),
@@ -1688,8 +1750,11 @@ html[data-dshome-glass="liquid"] body .hHd-Xa_root::before{
1688
1750
  radial-gradient(360px circle at calc(50% + var(--dshome-spec-x,0) * 50%) calc(50% + var(--dshome-spec-y,0) * 50%), rgb(255 255 255 / calc(var(--dshome-glass-spec) * .24)), transparent 60%),
1689
1751
  linear-gradient(180deg, rgba(255,255,255,var(--dshome-liquid-sheen)), rgba(255,255,255,.015) 34%, rgba(255,255,255,0))!important;
1690
1752
  }
1691
- /* The message bubble is the one surface that is not already positioned. */
1692
- html[data-dshome-glass="liquid"] ${LIQUID_BUBBLE}{
1753
+ /* The message bubble is the one surface that is not already positioned. The
1754
+ tooltip is not in this list any more -- that was B5: making a
1755
+ position:fixed tooltip relative turned it into an in-flow 78px block that
1756
+ shoved the send button out from under the cursor. See USER_BUBBLE. */
1757
+ html[data-dshome-glass="liquid"] :is(${USER_BUBBLE}){
1693
1758
  position:relative;
1694
1759
  transition:background-color .18s ease,box-shadow .18s ease;
1695
1760
  }
@@ -1701,10 +1766,12 @@ html[data-dshome-glass="liquid"] [data-composer-card]{
1701
1766
  transition:transform .24s cubic-bezier(.2,.8,.2,1),background-color .18s ease,box-shadow .18s ease!important;
1702
1767
  will-change:transform;
1703
1768
  }
1704
- html[data-dshome-glass="liquid"] ${LIQUID_BUBBLE}:hover{
1769
+ html[data-dshome-glass="liquid"] :is(${USER_BUBBLE}):hover{
1705
1770
  background-color:rgb(255 255 255 / .18)!important;
1706
1771
  }
1707
- body[data-dshome-dark] ${LIQUID_BUBBLE}:hover{
1772
+ /* Liquid's dark hover used to sit OUTSIDE the gate, so it also repainted the
1773
+ frosted recipe's bubbles with liquid's values. Gated like its siblings. */
1774
+ html[data-dshome-glass="liquid"] body[data-dshome-dark] :is(${USER_BUBBLE}):hover{
1708
1775
  background-color:rgb(255 255 255 / .10)!important;
1709
1776
  }
1710
1777
  html[data-dshome-glass="liquid"] body .hHd-Xa_root::after{
@@ -1741,7 +1808,7 @@ html[data-dshome-glass="liquid"] .hHd-Xa_root::after{
1741
1808
  continuous background a horizontal displacement has nothing to show, which
1742
1809
  the user accepted. */
1743
1810
  html[data-dshome-dispersion][data-dshome-glass="liquid"] body .wSkVaW_header:hover::before,
1744
- html[data-dshome-dispersion][data-dshome-glass="liquid"] body ${LIQUID_BUBBLE}{
1811
+ html[data-dshome-dispersion][data-dshome-glass="liquid"] body :is(${USER_BUBBLE}){
1745
1812
  -webkit-backdrop-filter:url(#dshome-glass-dispersion) blur(var(--dshome-liquid-blur)) saturate(var(--dshome-liquid-saturate))!important;
1746
1813
  backdrop-filter:url(#dshome-glass-dispersion) blur(var(--dshome-liquid-blur)) saturate(var(--dshome-liquid-saturate))!important;
1747
1814
  }
@@ -1798,56 +1865,90 @@ html[data-dshome-blur="off"] body{
1798
1865
  html[data-dshome-glass="liquid"] body .hHd-Xa_root::before,
1799
1866
  html[data-dshome-glass="liquid"] body [data-composer-card],
1800
1867
  html[data-dshome-glass="liquid"] body .wSkVaW_header:hover::before,
1801
- html[data-dshome-glass="liquid"] body ${LIQUID_BUBBLE}{
1868
+ html[data-dshome-glass="liquid"] body :is(${USER_BUBBLE}){
1802
1869
  background-color:rgba(255,255,255,.86)!important;
1803
1870
  }
1804
1871
  body[data-dshome-dark] .hHd-Xa_root::before,
1805
1872
  body[data-dshome-dark] [data-composer-card],
1806
1873
  body[data-dshome-dark] .wSkVaW_header:hover::before,
1807
- body[data-dshome-dark] ${LIQUID_BUBBLE}{
1874
+ html[data-dshome-glass="liquid"] body[data-dshome-dark] :is(${USER_BUBBLE}){
1808
1875
  background-color:rgba(13,30,60,.92)!important;
1809
1876
  }
1810
1877
  }
1811
1878
  `;
1812
1879
 
1813
1880
  /**
1814
- * Inject `css` only while `anchor` matches the DOM. If the anchor is
1815
- * absent at call time, wait up to 30s for it (covers shells that render
1816
- * after the plugin activates); if it never appears, skip the block and log
1817
- * one diagnostic line instead of shipping dead CSS against a renamed
1818
- * product surface. Returns a disposer for the whole install.
1881
+ * Inject `css` while `anchor` (a selector, or a list of alternatives --
1882
+ * several product surfaces have been renamed at least once) matches the DOM.
1883
+ *
1884
+ * The wait is NOT time-boxed. It used to give up after 30s, which quietly
1885
+ * broke every surface the user opens late: B3 in the 2026-09-16 report
1886
+ * measured the same code injecting the session-row patch when the sidebar was
1887
+ * expanded at ~4s, and never injecting it when the sidebar was expanded after
1888
+ * 30s (the default is collapsed, so for most sessions the patch simply never
1889
+ * existed). Panels behind a click -- settings, the subagent panel, the
1890
+ * trajectory view -- were all in that set.
1891
+ *
1892
+ * The observer therefore lives as long as the plugin does, and the cost is
1893
+ * bounded instead of unbounded: re-checks are throttled to one per animation
1894
+ * frame, a batch that finds nothing schedules nothing, and the observer
1895
+ * disconnects the moment the block lands. A block that never lands costs one
1896
+ * `querySelector` per frame at worst, and logs once, with the anchors it tried.
1819
1897
  */
1820
1898
  function installGuarded(anchor, css) {
1899
+ const selectors = Array.isArray(anchor) ? anchor : [anchor];
1900
+ const label = selectors.join('|');
1821
1901
  let dispose = null;
1822
1902
  const applyNow = () => {
1823
1903
  if (dispose !== null) return true;
1824
- if (document.querySelector(anchor) === null) return false;
1825
- dispose = injectStyle(css, 'patch' + anchor);
1826
- return true;
1904
+ for (const selector of selectors) {
1905
+ if (document.querySelector(selector) === null) continue;
1906
+ dispose = injectStyle(css, 'patch' + selectors[0]);
1907
+ return true;
1908
+ }
1909
+ return false;
1827
1910
  };
1828
1911
  if (applyNow()) {
1829
1912
  return () => { if (dispose !== null) dispose(); };
1830
1913
  }
1831
- let timer = 0;
1832
- let mo = new MutationObserver(() => {
1833
- if (!applyNow()) return;
1834
- mo.disconnect();
1835
- clearTimeout(timer);
1836
- });
1914
+ let queued = false;
1915
+ let missed = false;
1916
+ let mo = null;
1917
+ const schedule = () => {
1918
+ if (queued || dispose !== null) return;
1919
+ queued = true;
1920
+ try {
1921
+ requestAnimationFrame(() => {
1922
+ queued = false;
1923
+ if (applyNow() && mo !== null) {
1924
+ mo.disconnect();
1925
+ mo = null;
1926
+ }
1927
+ });
1928
+ } catch (error) {
1929
+ queued = false;
1930
+ }
1931
+ };
1932
+ mo = new MutationObserver(schedule);
1837
1933
  try {
1838
1934
  mo.observe(document.body, { childList: true, subtree: true });
1839
1935
  } catch (error) {
1840
1936
  mo = null;
1841
1937
  }
1842
- timer = setTimeout(() => {
1843
- if (mo !== null) mo.disconnect();
1844
- if (dispose === null) {
1845
- console.info('[' + PLUGIN_ID + '] patch skipped: ' + anchor + ' not found in DOM (product surface renamed?)');
1846
- }
1847
- }, 30000);
1938
+ // One diagnostic line per block, on first sight of a shell that has
1939
+ // rendered and still does not carry any of the alternatives.
1940
+ const announce = () => {
1941
+ if (missed || dispose !== null) return;
1942
+ missed = true;
1943
+ console.info('[' + PLUGIN_ID + '] patch waiting: none of ' + label + ' in the DOM yet (renamed product surface? the guard keeps watching)');
1944
+ };
1945
+ const timer = setTimeout(() => {
1946
+ if (missed || dispose !== null) return;
1947
+ announce();
1948
+ }, 15000);
1848
1949
  return () => {
1950
+ if (timer !== 0) clearTimeout(timer);
1849
1951
  if (mo !== null) mo.disconnect();
1850
- clearTimeout(timer);
1851
1952
  if (dispose !== null) dispose();
1852
1953
  };
1853
1954
  }
@@ -2225,7 +2326,7 @@ html[data-dshome-blur="off"] body{
2225
2326
  * `.gdEzaW_bubble` this list used to carry matches nothing on the current
2226
2327
  * build, which is how the old entry went dead without anyone noticing.
2227
2328
  */
2228
- const SPOT_SURFACES = `${LIQUID_BUBBLE},.uV2eYG_card,.dshome-sa,.dshome-file-menu,.hHd-Xa_root,[data-composer-card]`;
2329
+ const SPOT_SURFACES = `${USER_BUBBLE},.uV2eYG_card,.dshome-sa,.dshome-file-menu,.hHd-Xa_root,[data-composer-card]`;
2229
2330
  function startSpecularSpotter() {
2230
2331
  const onOver = (event) => {
2231
2332
  const target = event.target;
@@ -2277,6 +2378,19 @@ html[data-dshome-blur="off"] body{
2277
2378
  // recipe. Both recipes tilt now, and because the liquid sheet used to be
2278
2379
  // the only place carrying `transition: transform`, the motion carries
2279
2380
  // its own transition (TILT_TRANSITION) so a recipe cannot lose it.
2381
+ // * 2.0.85 makes ENGAGEMENT GEOMETRIC and the gates honest. Engagement used
2382
+ // to need a `pointerover` whose target was inside the card; that is a
2383
+ // chain of assumptions about product markup (a wrapper with role=button,
2384
+ // a swapped card node, a portal, an overlay) which a theme cannot verify,
2385
+ // and failing it looks exactly like a broken effect -- reported as
2386
+ // "把我的对话框随着鼠标位置而变化的效果补回来". `spotAt()` decides from the
2387
+ // card's own rectangle on every pointermove, so a missed event costs one
2388
+ // frame instead of the whole effect; `controlAt()` remains the single hit
2389
+ // test, and only to answer "is a control under the pointer". The two
2390
+ // SILENT gates are reported now: `__dshomeTilt()` includes `reducedMotion`
2391
+ // (an OS setting that disables the effect and looks like a bug) and
2392
+ // `spots` (0 means the composer no longer matches TILT_SELECTOR), and the
2393
+ // reduced-motion case logs one info line instead of vanishing.
2280
2394
  const TILT_SELECTOR = '[data-composer-card],.uV2eYG_card';
2281
2395
  const TILT_MAX = 0.0175;
2282
2396
  const TILT_PERSPECTIVE = 800;
@@ -2306,13 +2420,28 @@ html[data-dshome-blur="off"] body{
2306
2420
  const tilted = new WeakSet();
2307
2421
  const settle = new Map();
2308
2422
  const reduced = typeof window.matchMedia === 'function' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
2309
- if (reduced) return () => {};
2310
- // Readable from the page: the effect is guarded twice, so "it is missing"
2311
- // is otherwise a guess about which guard ate it. `__dshomeTilt()` answers
2312
- // that in one call (and reports the recipe it is running under).
2423
+ // Readable from the page: the effect is guarded several times over, so "it
2424
+ // is missing" is otherwise a guess about which guard ate it.
2425
+ // `__dshomeTilt()` answers that in one call -- including the two SILENT
2426
+ // gates, which is why `reduced` and `spots` are in here at all: an OS-level
2427
+ // "reduce motion" or a composer whose markup no longer matches
2428
+ // TILT_SELECTOR both look exactly like a broken effect.
2313
2429
  try {
2314
- window.__dshomeTilt = () => ({ recipe: dsttGetGlass(), leaning: current !== null, frozen, engagements, sinceRelease: releasedAt === -Infinity ? null : Math.round(performance.now() - releasedAt) });
2430
+ window.__dshomeTilt = () => ({
2431
+ recipe: dsttGetGlass(),
2432
+ reducedMotion: reduced,
2433
+ spots: document.querySelectorAll(TILT_SELECTOR).length,
2434
+ leaning: current !== null,
2435
+ frozen,
2436
+ engagements,
2437
+ sinceRelease: releasedAt === -Infinity ? null : Math.round(performance.now() - releasedAt)
2438
+ });
2315
2439
  } catch (error) {}
2440
+ if (reduced) {
2441
+ // Never silent: this is the one gate a user cannot see in any setting.
2442
+ console.info('[dsh-deepseek-style-theme] composer tilt disabled: the system asks for reduced motion (prefers-reduced-motion: reduce)');
2443
+ return () => {};
2444
+ }
2316
2445
  const release = (spot) => {
2317
2446
  const pending = settle.get(spot);
2318
2447
  if (pending !== undefined) {
@@ -2351,21 +2480,48 @@ html[data-dshome-blur="off"] body{
2351
2480
  tilted.add(spot);
2352
2481
  });
2353
2482
  };
2483
+ /**
2484
+ * Which tilt spot contains a point, by RECTANGLE -- deliberately not by
2485
+ * walking `event.target` up to a selector. Engagement used to depend on a
2486
+ * `pointerover` whose target was inside the card, and that is a chain of
2487
+ * assumptions about the product's markup (a wrapper with role=button, a
2488
+ * swapped-in card node, a portal, an overlay) that a theme cannot verify.
2489
+ * The reported symptom of getting it wrong is indistinguishable from a
2490
+ * broken effect: the box just never leans. Geometry is the one thing the
2491
+ * theme can measure for itself.
2492
+ */
2493
+ const spotAt = (clientX, clientY) => {
2494
+ for (const spot of document.querySelectorAll(TILT_SELECTOR)) {
2495
+ const rect = spot.getBoundingClientRect();
2496
+ if (rect.width <= 0 || rect.height <= 0) continue;
2497
+ if (clientX < rect.left || clientX > rect.right || clientY < rect.top || clientY > rect.bottom) continue;
2498
+ return { spot, rect };
2499
+ }
2500
+ return null;
2501
+ };
2502
+ /** The topmost element at a point, used only to decide "is this a control". */
2503
+ const controlAt = (clientX, clientY) => {
2504
+ let node = null;
2505
+ try { node = document.elementFromPoint(clientX, clientY); } catch (error) { node = null; }
2506
+ if (node === null || typeof node.closest !== 'function') return false;
2507
+ return node.closest(TILT_CONTROLS) !== null;
2508
+ };
2354
2509
  const onOver = (event) => {
2510
+ // Kept for the one thing it does better than a move: it fires the
2511
+ // moment the pointer arrives, before any movement. Everything it
2512
+ // decides is re-derived from geometry, so a missed event costs one
2513
+ // frame, not the effect.
2355
2514
  const target = event.target;
2356
2515
  if (target === null || typeof target.closest !== 'function') return;
2357
2516
  const spot = target.closest(TILT_SELECTOR);
2358
2517
  if (spot === null) return;
2359
- // A control is under the pointer: hold the lean exactly where it is.
2360
- // Releasing here is what used to feed the ~8 Hz hover loop.
2361
- if (target.closest(TILT_CONTROLS) !== null) {
2518
+ const rect = spot.getBoundingClientRect();
2519
+ if (event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom) return;
2520
+ if (controlAt(event.clientX, event.clientY)) {
2362
2521
  if (current === spot) frozen = true;
2363
2522
  return;
2364
2523
  }
2365
- // The cooldown gates a fresh engagement only, never an ongoing lean.
2366
2524
  if (!tilted.has(spot) && performance.now() - releasedAt < TILT_COOLDOWN_MS) return;
2367
- const rect = spot.getBoundingClientRect();
2368
- if (event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom) return;
2369
2525
  const pending = settle.get(spot);
2370
2526
  if (pending !== undefined) {
2371
2527
  clearTimeout(pending);
@@ -2376,22 +2532,38 @@ html[data-dshome-blur="off"] body{
2376
2532
  paint(spot, rect, event.clientX, event.clientY);
2377
2533
  };
2378
2534
  const onMove = (event) => {
2379
- if (current === null) return;
2380
- const target = event.target;
2381
- if (target !== null && typeof target.closest === 'function' && target.closest(TILT_CONTROLS) !== null) {
2382
- frozen = true;
2535
+ const hit = spotAt(event.clientX, event.clientY);
2536
+ if (hit === null) {
2537
+ // Off the card entirely: release only the spot that was leaning.
2538
+ if (current !== null) {
2539
+ release(current);
2540
+ current = null;
2541
+ }
2542
+ return;
2543
+ }
2544
+ const { spot, rect } = hit;
2545
+ if (controlAt(event.clientX, event.clientY)) {
2546
+ // A control is under the pointer: hold the lean exactly where it is
2547
+ // (releasing here is what used to feed the ~8 Hz hover loop).
2548
+ if (current === spot) frozen = true;
2383
2549
  return;
2384
2550
  }
2551
+ // The cooldown gates a fresh engagement only, never an ongoing lean.
2552
+ if (!tilted.has(spot) && current !== spot && performance.now() - releasedAt < TILT_COOLDOWN_MS) return;
2553
+ if (current !== spot) {
2554
+ if (current !== null) release(current);
2555
+ const pending = settle.get(spot);
2556
+ if (pending !== undefined) {
2557
+ clearTimeout(pending);
2558
+ settle.delete(spot);
2559
+ }
2560
+ current = spot;
2561
+ }
2385
2562
  frozen = false;
2386
- paint(current, current.getBoundingClientRect(), event.clientX, event.clientY);
2563
+ paint(spot, rect, event.clientX, event.clientY);
2387
2564
  };
2388
2565
  const onOut = (event) => {
2389
2566
  if (current === null) return;
2390
- const target = event.target;
2391
- if (target !== null && typeof target.closest === 'function') {
2392
- const spot = target.closest(TILT_SELECTOR);
2393
- if (spot !== null && spot !== current) return;
2394
- }
2395
2567
  const rect = current.getBoundingClientRect();
2396
2568
  if (event.clientX >= rect.left && event.clientX <= rect.right && event.clientY >= rect.top && event.clientY <= rect.bottom) return;
2397
2569
  release(current);
@@ -2430,7 +2602,7 @@ html[data-dshome-blur="off"] body{
2430
2602
  if (!supportsWebgl2()) return startParticles();
2431
2603
  // Which build is running, readable from the page (see README). One line in
2432
2604
  // the console answers "is the bundle I just installed actually live?".
2433
- try { window.__dshomeBuild = '2.0.82 hover-lift'; } catch (error) {}
2605
+ try { window.__dshomeBuild = '2.0.85 glass-extras'; } catch (error) {}
2434
2606
  const canvas = document.createElement('canvas');
2435
2607
  canvas.setAttribute('data-dsh-deepseek-canvas', '');
2436
2608
  const style = canvas.style;
@@ -2472,17 +2644,6 @@ html[data-dshome-blur="off"] body{
2472
2644
  // `attachFluidInteractions` is therefore NOT wired — it is kept in the
2473
2645
  // file, unmodified, because re-enabling button ripples is one call, but
2474
2646
  // nothing in this theme uses it today.
2475
- const stopSpotter = startSpecularSpotter();
2476
- const stopParallax = startSpecularParallax();
2477
- const dispersion = startGlassDispersion({ hue: FLUID_HUES[dshomeColor()] });
2478
- // Both recipes share one filter pair, so the recipe selects the scale:
2479
- // 104 for liquid (visible edge refraction) and the untouched 60 for
2480
- // frosted, which is what the pane has always used.
2481
- const applyGlass = () => {
2482
- dispersion.setRefraction(dsttGetGlass() === 'liquid' ? LIQUID_REF_SCALE : DEFAULT_REF_SCALE);
2483
- };
2484
- applyGlass();
2485
- const unsubscribeGlass = dsttSubscribe(applyGlass);
2486
2647
  // ── palette reconciliation ────────────────────────────────────────────
2487
2648
  // One reader for BOTH inputs the palette depends on: the colour token on
2488
2649
  // <body> and the dark marker. Reported symptom: "切主题模式背景不变 /
@@ -2507,7 +2668,6 @@ html[data-dshome-blur="off"] body{
2507
2668
  };
2508
2669
  const unsubscribeColor = subscribeColor((color) => {
2509
2670
  syncFluid();
2510
- try { dispersion.setTint(FLUID_HUES[color] ?? FLUID_HUES.green); } catch (error) {}
2511
2671
  });
2512
2672
  const unsubscribeDark = darkSync === null ? null : darkSync.subscribe(syncFluid);
2513
2673
  // The colour driver writes the token as an attribute; watching it as well
@@ -2523,19 +2683,65 @@ html[data-dshome-blur="off"] body{
2523
2683
  return () => {
2524
2684
  unsubscribeColor();
2525
2685
  if (unsubscribeDark !== null) unsubscribeDark();
2686
+ if (tokenMirror !== null) tokenMirror.disconnect();
2687
+ clearInterval(fluidSync);
2688
+ fluid.dispose();
2689
+ canvas.remove();
2690
+ };
2691
+ }
2692
+
2693
+ /**
2694
+ * The cursor-following half of the glass: the specular spotter (which marks
2695
+ * the surfaces the highlight may land on), the parallax that writes
2696
+ * `--dshome-spec-x/y` for the hovered surface, and the SVG edge refraction.
2697
+ *
2698
+ * These used to be mounted INSIDE {@link startAmbient}, which tied them to
2699
+ * the fluid layer: turning 「动态背景」 off -- which is exactly what the user
2700
+ * did to stop the iGPU squares -- or choosing ③ 自选背景, or landing on an
2701
+ * engine without WebGL2 (the particle fallback returns before them), silently
2702
+ * took the dialog's cursor-following highlight and its edge refraction with
2703
+ * it. Reported as "把我的对话框随着鼠标位置而变化的效果补回来". They belong to
2704
+ * the glass, not to the flow field, so they mount on their own and survive
2705
+ * every background recipe.
2706
+ */
2707
+ function startGlassExtras() {
2708
+ const stopSpotter = startSpecularSpotter();
2709
+ const stopParallax = startSpecularParallax();
2710
+ const dispersion = startGlassDispersion({ hue: FLUID_HUES[dshomeColor()] });
2711
+ // Both recipes share one filter pair, so the recipe selects the scale:
2712
+ // 104 for liquid (visible edge refraction) and the untouched 60 for
2713
+ // frosted, which is what the pane has always used.
2714
+ const applyGlass = () => {
2715
+ dispersion.setRefraction(dsttGetGlass() === 'liquid' ? LIQUID_REF_SCALE : DEFAULT_REF_SCALE);
2716
+ };
2717
+ applyGlass();
2718
+ const unsubscribeGlass = dsttSubscribe(applyGlass);
2719
+ // The tint follows the DSTT colour token. Same belt-and-braces as the
2720
+ // fluid palette: subscribe AND watch the attribute the driver writes,
2721
+ // because the colour driver writes it before this mount exists.
2722
+ const syncTint = () => {
2723
+ try { dispersion.setTint(FLUID_HUES[dshomeColor()] ?? FLUID_HUES.green); } catch (error) {}
2724
+ };
2725
+ const unsubscribeColor = subscribeColor(syncTint);
2726
+ let tokenMirror = null;
2727
+ try {
2728
+ tokenMirror = new MutationObserver(syncTint);
2729
+ tokenMirror.observe(document.body, { attributes: true, attributeFilter: ['data-dshome-color'] });
2730
+ } catch (error) {
2731
+ tokenMirror = null;
2732
+ }
2733
+ return () => {
2734
+ unsubscribeColor();
2526
2735
  unsubscribeGlass();
2527
2736
  if (tokenMirror !== null) tokenMirror.disconnect();
2528
2737
  dispersion.dispose();
2529
2738
  stopParallax();
2530
2739
  stopSpotter();
2531
- clearInterval(fluidSync);
2532
- fluid.dispose();
2533
- canvas.remove();
2534
2740
  };
2535
2741
  }
2536
2742
 
2537
2743
  function startParticles() {
2538
- try { window.__dshomeBuild = '2.0.82 hover-lift'; } catch (error) {}
2744
+ try { window.__dshomeBuild = '2.0.85 glass-extras'; } catch (error) {}
2539
2745
  const canvas = document.createElement('canvas');
2540
2746
  canvas.setAttribute('data-dsh-deepseek-canvas', '');
2541
2747
  const style = canvas.style;
@@ -2607,6 +2813,11 @@ html[data-dshome-blur="off"] body{
2607
2813
 
2608
2814
  function step() {
2609
2815
  if (!running) return;
2816
+ // Same self-cleanup as the fluid loop: no canvas in the DOM, no work.
2817
+ if (!canvas.isConnected) {
2818
+ running = false;
2819
+ return;
2820
+ }
2610
2821
  g.clearRect(0, 0, W, H);
2611
2822
  const base = linkBase();
2612
2823
  for (let i = 0; i < parts.length; i++) {
@@ -3937,7 +4148,10 @@ body[data-dshome-dark] .${CLS}{background:rgba(14,30,60,.94);box-shadow:0 18px 4
3937
4148
  // set they asked to keep. Ids match the host schema's `backgroundMode`
3938
4149
  // enum; the free-text value for 方式3 is `customBackground`.
3939
4150
  const DSTT_BACKGROUNDS = ["classic", "white", "custom", "bold"];
3940
- const DSTT_BACKGROUND_DEFAULT = "classic";
4151
+ // `bold` by default: ① classic averages (soft lead + white + near-white) into a
4152
+ // wash the eye reads as blue-white whatever the colour token says, so it is a
4153
+ // poor first impression -- it stays selectable for anyone who wants it.
4154
+ const DSTT_BACKGROUND_DEFAULT = "bold";
3941
4155
  const DSTT_BACKGROUND_LABELS = {
3942
4156
  classic: { zh: "① 主色+纯白+近白", en: "1 · Lead + white + near-white" },
3943
4157
  white: { zh: "② 主色+纯白", en: "2 · Lead + white" },
@@ -4578,7 +4792,7 @@ body[data-dshome-dark] .${CLS}{background:rgba(14,30,60,.94);box-shadow:0 18px 4
4578
4792
  React.createElement('div', { className: 'dshome-dstt-row-title' }, isZh ? '背景方式' : 'Background recipe'),
4579
4793
  React.createElement('div', { className: 'dshome-dstt-row-hint' }, isZh
4580
4794
  ? '① 主色+纯白+近白(默认,旧版那套同色系柔和观感);② 主色+纯白(只有两个色参与,最干净);③ 自选背景:我们的流体与主题底色全部让位(没别的在画时是纯白 / 纯黑);装了 dsh-plugin-wallpaper-engine 且它正在渲染时,连纯白也不画,背景交给它。颜色只驱动按钮之类。输入框可填图片 URL / 任意 CSS 值,或点「桌面壁纸」用本机壁纸;浓三色是 1.43.12 那套,先留着。'
4581
- : '1 Lead + white + near-white (default: the soft single-family look this theme shipped before). 2 Lead + white (only two colours participate, the cleanest). 3 Custom background: our fluid and our themed background both stand down (flat white / black when nothing else paints); while dsh-plugin-wallpaper-engine is rendering we do not even paint that, and the background is its layer. The colour drives the buttons and such only. Type an image URL / any CSS value, or press Desktop for the Windows wallpaper. Bold is the 1.43.12 set, kept around.'),
4795
+ : 'Bold (default: the lead tone carries real chroma, so all six backgrounds read at a glance). 1 Lead + white + near-white (the soft single-family look this theme shipped before). 2 Lead + white (only two colours participate, the cleanest). 3 Custom background: our fluid and our themed background both stand down (flat white / black when nothing else paints); while dsh-plugin-wallpaper-engine is rendering we do not even paint that, and the background is its layer. The colour drives the buttons and such only. Type an image URL / any CSS value, or press Desktop for the Windows wallpaper.'),
4582
4796
  React.createElement('div', { className: 'dshome-dstt-seg dshome-dstt-seg-row', role: 'radiogroup', 'aria-label': isZh ? '背景方式' : 'Background recipe' },
4583
4797
  DSTT_BACKGROUNDS.map((id) => React.createElement('button', {
4584
4798
  key: id,
@@ -4811,7 +5025,19 @@ body[data-dshome-dark] .${CLS}{background:rgba(14,30,60,.94);box-shadow:0 18px 4
4811
5025
  try { stop(); } catch (error) {}
4812
5026
  return;
4813
5027
  }
4814
- try { dispose = startAmbient(); } catch (error) { dispose = () => {}; }
5028
+ try {
5029
+ dispose = startAmbient();
5030
+ } catch (error) {
5031
+ // NEVER silent. A throw inside the mount used to be swallowed right
5032
+ // here, which is how one undeclared identifier (ATTR, deleted in
5033
+ // d862487) hid for nineteen versions: no refraction anywhere, a
5034
+ // 动态背景 toggle that stopped unmounting, and one leaked
5035
+ // simulator per flip. Sweep the orphan canvas too -- the fluid
5036
+ // loop also self-cancels when its canvas leaves the DOM.
5037
+ console.error('[dsh-deepseek-style-theme] ambient background failed to mount:', error);
5038
+ for (const stray of document.querySelectorAll('canvas[data-dsh-deepseek-canvas]')) stray.remove();
5039
+ dispose = () => {};
5040
+ }
4815
5041
  };
4816
5042
  syncAmbient();
4817
5043
  const unsubscribe = dsttSubscribe(syncAmbient);
@@ -4822,6 +5048,18 @@ body[data-dshome-dark] .${CLS}{background:rgba(14,30,60,.94);box-shadow:0 18px 4
4822
5048
  if (stop !== null) { try { stop(); } catch (error) {} }
4823
5049
  };
4824
5050
  }, 'deepseek-style-theme: fluid background');
5051
+ // The cursor-following half of the glass mounts ON ITS OWN, with no
5052
+ // dependence on the fluid layer: see startGlassExtras for why (turning
5053
+ // 「动态背景」 off, or ③ 自选背景, or a machine without WebGL2 used to take
5054
+ // the dialog's cursor-following highlight and the edge refraction with it).
5055
+ ctx.effect(() => {
5056
+ try {
5057
+ return startGlassExtras();
5058
+ } catch (error) {
5059
+ console.error('[dsh-deepseek-style-theme] glass extras failed to mount:', error);
5060
+ return () => {};
5061
+ }
5062
+ }, 'deepseek-style-theme: glass extras (specular + dispersion)');
4825
5063
  // The composer's hover tilt is independent of the background: it must
4826
5064
  // survive a machine without WebGL2, where startAmbient() falls back to
4827
5065
  // the 2D particle field.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-deepseek-style-theme",
3
3
  "description": "DeepSeek official-home style theme: WebGL fluid-flow background, liquid glass with SVG edge refraction, glass sidebar and composer, frosted header, running-subagent progress panel, and DeepSeek brand link for the dsh web GUI",
4
- "version": "2.0.82",
4
+ "version": "2.0.85",
5
5
  "type": "module",
6
6
  "keywords": [
7
7
  "deepseek",