dsh-deepseek-style-theme 2.0.81 → 2.0.84

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 +23 -11
  2. package/lib/client.js +267 -102
  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` 控制(详见下文)
@@ -50,7 +50,7 @@
50
50
  - **流体跟随笔刷**(`fluidBrush`,默认关):开启后光标才把速度写进流场、拖出尾迹;**改完刷新页面生效**。
51
51
  - **玻璃风格**(`glassStyle`,默认 `液态`):
52
52
  - **液态**(1.43.0+):Apple Liquid Glass 方向——填充降到 `rgb(255 255 255 / .14)`、顶部光泽降到 `.07`,背板链改为靠 `saturate(1.75)` 让身后颜色发亮,共用的 `feDisplacementMap` 走更大的 scale(104,白磨砂仍是 60),边框上再跑一圈自走的 `conic-gradient` 高光(`@property` 注册的 `<angle>`,与鼠标无关)。上玻璃的只有四处:**侧边栏外壳 / 对话框 / 标题栏(悬停)/ 用户消息气泡**;
53
- - **白磨砂**:1.42.x 那套白磨砂 + 蓝变光边框,**逐像素不变**(新配方整块挂在 `html[data-dshome-glass="liquid"]` 下,已用 1991 个元素的计算样式签名验证过:切到白磨砂时新样式表贡献为零)。
53
+ - **白磨砂**:1.42.x 那套白磨砂 + 蓝变光边框,**样式逐像素不变**(新配方整块挂在 `html[data-dshome-glass="liquid"]` 下,已用 1991 个元素的计算样式签名验证过:切到白磨砂时新样式表贡献为零)。**唯一的例外是悬停倾斜**:它自 2.0.82 起两套配方都生效(原先只在液态下挂),运动与配方无关——见「已知产品侧缺陷」一节;
54
54
 
55
55
  ### 高峰判定(1.37.0+)
56
56
 
@@ -88,7 +88,7 @@
88
88
 
89
89
  - **探测**:只读 DOM 标记 `[data-dsh-better-sidebar]`(它的面板宿主)与 `[data-dsh-center-col]`(它给中心列打的标记);宿主侧无需任何配合。
90
90
  - **布局**:它用 `#root [data-dsh-center-col]{margin-bottom:var(--dsh-sidebar-height)}` 把会话列顶起来;本主题**不写中心列的尺寸、不动它的 tab 栏 / 面板内部**,只对产品自己的表面(侧栏外壳、对话框、标题栏)做玻璃与配色。
91
- - **已知交互(重要)**:它的面板宿主几何同步会在检测到**页面级 transform** 时降级(它自己会打 `data-dsh-panel-host-degraded`)。本主题的「对话框悬停倾斜」会给对话框卡片写 `transform: perspective(800px) … scale(1.01)` —— 这是**卡片级**而非页面级,正常情况无碍;但若你在同装后看到悬停抖动 / 底部滑块闪动,先把 **DSTT → 动态背景** 关掉、再试 `玻璃风格 → 白磨砂`(白磨砂下倾斜不生效),即可二分定位是谁在动。
91
+ - **已知交互(重要)**:它的面板宿主几何同步会在检测到**页面级 transform** 时降级(它自己会打 `data-dsh-panel-host-degraded`)。本主题的「对话框悬停倾斜」会给对话框卡片写 `transform: perspective(800px) … scale(1.01)` —— 这是**卡片级**而非页面级,正常情况无碍;但若你在同装后看到悬停抖动 / 底部滑块闪动,先按上一节的顺序二分:`__dshomeTilt()` 看倾斜此刻是否在动、`tilt(false)` 一行冻结它、再 `ambient(false)` 关掉动态背景画布(2.0.82 起两套玻璃配方都会倾斜,所以"切白磨砂"不再是有效的二分手段)。
92
92
  - **右侧栏配色**:它面板内部的配色由它自己管;本主题的深色规则只覆盖产品原生的右栏表面(`.VOzbGW_panel` 等),不改它的 class 命名空间。
93
93
 
94
94
  ### 适配清单(写给以后接手的人)
@@ -117,7 +117,7 @@
117
117
 
118
118
  **主题的处置**(2.0.78):给会话滚动区加 `overflow-x: clip`(守护块,锚点 `.wSkVaW_scrollBody`,规则按 `[class*="scrollBody"]` 子串匹配,散列类改名也不会静默失效)。横向不再产生滚动条,那 8px 就不存在,闭环断开;每个代码块自己的横向滚动不受影响。代价:超出列宽的部分会被裁掉而不是可滚动 —— 在聊天列里这是更小的恶。**溢出的根因在产品侧,值得上报。**
119
119
 
120
- **第二半:我们自己的倾斜**(2.0.79 已修)。滚动条链条断掉后,真机逐帧采样显示剩下的抖动来自悬停倾斜本身:
120
+ **第二半:我们自己的倾斜**(2.0.79 想修,2.0.82 才算修对)。滚动条链条断掉后,真机逐帧采样显示剩下的抖动来自悬停倾斜本身:
121
121
 
122
122
  ```
123
123
  card=700/757 … under=div.uV2eYG_row hovCard=1
@@ -126,12 +126,19 @@ card=699/761 … under=div.wSkVaW_widthHandle hovCard=0 ← 边缘越过了
126
126
  card=700/757 … under=div.wSkVaW_widthHandle hovCard=0 ← 倾斜解除、卡片缩回
127
127
  ```
128
128
 
129
- 指针**静止**时,1% 放大把卡片边缘推出 8px,于是指针底下的元素在"对话框控件"与"列的宽度把手"之间翻转,而每次翻转都会重新武装或解除倾斜 → 因为过渡只有 0.1–0.24s,比被滚动条中介的那条**更快**。修法两条:
129
+ 指针**静止**时,1% 放大把卡片边缘推出 8px,于是指针底下的元素在"对话框控件"与"列的宽度把手"之间翻转,而每次翻转都会重新武装或解除倾斜 → 因为过渡只有 0.1–0.24s,比被滚动条中介的那条**更快**。
130
130
 
131
- 1. `TILT_SCALE = 1.01 → 1`:保留倾斜(旋转只动约 1px),去掉会推动边缘 8px 的抬升;
132
- 2. 指针在**交互控件或拖拽把手**上时不倾斜(`TILT_INTERACTIVE`,`pointerover` 与 `pointermove` 双入口判定)—— 倾斜只作用在对话框的空白处。
131
+ **2.0.79 修错了**:它把抬升去掉(`TILT_SCALE: 1.01 → 1`),并把 `input / textarea / [contenteditable]` 也算成"不能压在动面上的控件"。两条都是矫枉过正,而且同因:**输入框几乎盖满整张卡片**,把它排除等于让倾斜在"鼠标放到对话框上"这个它唯一存在的姿势里永不触发,抬升也跟着一起没了。用户报回来的是「鼠标放在对话框,放大、倾斜没了」——修法本身成了新的缺陷。
133
132
 
134
- > 排查方法记在这里,方便复现:`ResizeObserver` 盯住卡片到 body 的祖先链(谁改高度谁自己报),再对滚动区打 `scrollWidth/clientWidth` 与"右边缘越界元素";最后用 rAF 逐帧记录"指针底下是谁 + 候选元素的矩形",翻转对就能一眼看出。
133
+ **2.0.82 的修法(保留效果,切断回路)**:
134
+
135
+ 1. **抬起回来**:`TILT_SCALE` 恢复 `1.01`;
136
+ 2. **输入框上可以倾斜**:`TILT_CONTROLS` 只留 `button / [role=button] / a[href] / [class*="andle" i]`,文字录入不再挡。倾斜是**带着输入框一起动**的,指针不会因此掉出输入框;
137
+ 3. **滞回取代"越界即释放"**(关键):指针进入控件时**冻结当前角度**——不重画、不回弹、不重新武装;回到卡片空白处再续;只有指针**离开卡片自己的矩形**才释放,释放后还有 `TILT_COOLDOWN_MS = 200` 的冷却期才允许下一次进入。那条自激回路需要"释放"来喂它,而现在指针静止时倾角是**死的**,没有任何反馈通路。
138
+ 4. **两套玻璃配方都倾斜**:原先 `paint` / `onOver` 各有一道"不是液态就早退"的闸门(理由是白磨砂要保持逐像素不变),结果是**用白磨砂的人永远看不到这个效果**——用户点名的第二条要求就是「白磨砂玻璃也要有」。现在闸门拆掉,而且运动**自带过渡**(`TILT_TRANSITION`,值与原液态样式表里那条相同),这样任何配方都不会因为少了一条 `transition: transform` 而变成硬跳;
139
+ 5. 状态可从页面直接读:`window.__dshomeTilt()` → `{ recipe, leaning, frozen, engagements, sinceRelease }`。"效果不见了"因此不再需要猜是哪道闸门吃掉的。
140
+
141
+ > 排查方法记在这里,方便复现:`ResizeObserver` 盯住卡片到 body 的祖先链(谁改高度谁自己报),再对滚动区打 `scrollWidth/clientWidth` 与"右边缘越界元素";最后用 rAF 逐帧记录"指针底下是谁 + 候选元素的矩形",翻转对就能一眼看出。跨机器复现用 `tools/symptom-probe.js`(贴进控制台,`run(秒)` 同时采样 GPU 状态与光标下的逐字段 diff,`tilt(false)` 一行就能把倾斜冻结做对照)。
135
142
 
136
143
  ## 健壮性设计(1.33.1+)
137
144
 
@@ -303,13 +310,16 @@ dsh plugin --profile web remove dsh-deepseek-style-theme
303
310
 
304
311
  ## 自测工具(开发用,不随包发布)
305
312
 
306
- `tools/` 里的脚本都在本机直接跑,**不进发布产物**(`package.json` 的 `files` 只含 `lib/`、`cordis.patch.yml`、`README.md`、`LICENSE`)。除 `gui-probe.mjs` 与 `--open` 的 `bridge-smoke.mjs` 外,都不需要浏览器、不需要网络。
313
+ `tools/` 里的脚本都在本机直接跑,**不进发布产物**(`package.json` 的 `files` 只含 `lib/`、`cordis.patch.yml`、`README.md`、`LICENSE`)。除 `gui-probe.mjs`、`symptom-probe-selftest.mjs` 与 `--open` 的 `bridge-smoke.mjs` 外,都不需要浏览器、不需要网络。
307
314
 
308
315
  | 工具 | 作用 | 运行 |
309
316
  |---|---|---|
317
+ | `symptom-probe.js` | **贴进浏览器控制台的现场探针**(2.0.81+):专门给「症状机器不是开发机」的情况用。先打印一份环境 JSON——每个补丁块是否落到 DOM 上(`style[data-plugin-css]` 清单 + 13 个锚点是否存在:锚点按产品构建哈希命名,换构建就会静默跳过,2.0.78 的 `overflow-x: clip` 就是靠它守住的)、`data-dshome-*` 标记、每块 canvas 的 z-index/opacity/所在 GPU/是否 context lost、消息列的 `scrollWidth - clientWidth` 与超宽/自溢出的子元素、发送按钮等控件的盒模型与 transition;再给 `run(秒)`:**同时**采样 GPU 状态(fps、context lost、`gl.getError()`、canvas 尺寸抖动)与光标下的逐帧变化(命中元素链、盒、transform、padding、margin 的逐字段 diff,带 240 条截断与变化率),最后是 `tilt(false)` / `clip(true)` / `ambient(false)` / `wallpaper(false)` / `glass(false)` 五个开关,用来把「关掉 X 之后症状还在不在」变成一句话答案。不写任何设置、不弹权限框 | 控制台粘贴整份文件 |
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` |
310
319
  | `bridge-smoke.mjs` | 宿主端私有 RPC 通道:协议、错误路径,以及完整围栏(非回环对端 / 缺 `Host` / DNS rebinding / 跨站来源 / `Origin` 不匹配 / 非 JSON 类型 / UNC 路径 / `file.*` 端点识别与校验,24 项检查;加 `--open` 为 25 项,会真的用系统默认应用打开一个临时文件,**会在桌面弹出窗口**,默认不启用) | `node tools/bridge-smoke.mjs [已安装的 lib/index.js] [--open]` |
311
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]` |
312
- | `bg-recipes-smoke.mjs` | 客户端源码层的数据与契约断言(2.0.73+,随每版增补):① 必须含纯白 + 近白、② 只有两个颜色参与、`bold` 浅色无 `#FFFFFF`、③ 的基底/让位规则与选择器守卫、壁纸引擎标记与「提示而非自动切换」、桌面壁纸端点与 `WallpaperStyle` 映射、`darkSync` 作用域、两条反引号守卫(整表扫描)、`TILT_SCALE = 1` 与「控件上不倾斜」、发送按钮 hover 不再位移、流场 16F 探测与回退(61 项) | `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` |
313
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]` |
314
324
  | `catalog-sync-smoke.mjs` | 模型目录同步的全部分支:一致 / 可描述漂移 / 聚合网关目录 / 只追加 / `off` / baseURL 解析顺序 / 无密钥 / 端点故障 / 命名空间未就绪 / 版本冲突(27 项检查,全用替身,无需凭据与网络) | `node tools/catalog-sync-smoke.mjs [已安装的 lib/index.js]` |
315
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]` |
@@ -337,6 +347,8 @@ node tools/gui-probe.mjs --models --file-card
337
347
  │ ├── bridge-smoke.mjs # 宿主端私有 RPC 通道冒烟测试
338
348
  │ ├── catalog-sync-smoke.mjs # 模型目录同步冒烟测试(六种分支)
339
349
  │ ├── catalog-sync-live.mjs # 同步的真端点/真密钥校验(写入不落盘)
350
+ │ ├── symptom-probe.js # 贴进控制台的现场探针(环境报告 + 采样 + 逐帧 diff + 开关)
351
+ │ ├── symptom-probe-selftest.mjs # 上面那份探针的自测(合成页面 + CDP 真实鼠标)
340
352
  │ └── gui-probe.mjs # 实时页面的无头浏览器探测与像素扫描
341
353
  └── lib/
342
354
  ├── index.js # host 端:打开工作区 / 交付文件 RPC、DSTT 设置、模型目录同步
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.
@@ -1374,20 +1437,33 @@ body[data-dshome-dark] .uV2eYG_card::before{background:conic-gradient(from var(-
1374
1437
  // whatever sticks out past the column edge is clipped instead of
1375
1438
  // scrollable, which in a chat column is the lesser evil -- and this is
1376
1439
  // the guarded-block pattern, so it disappears with the anchor.
1440
+ //
1441
+ // 2.0.82 widens the net to the column's other scrollers. The mechanism
1442
+ // needs only ONE `overflow-y: auto` container (the other axis computes
1443
+ // to `auto`, so it can grow a horizontal scrollbar) whose child grows on
1444
+ // hover; the first report named the message list, but the same 8px step
1445
+ // under the send button can come from the composer's own seat or the
1446
+ // transcript's view area, which that first fix did not cover. Popups
1447
+ // portalled to <body> are unaffected; anything rendered inline inside
1448
+ // these containers is clipped, exactly as it already was for the list.
1377
1449
  { anchor: ".wSkVaW_scrollBody", css: `
1378
- [class*="scrollBody"]{overflow-x:clip}
1450
+ [class*="scrollBody"],[class*="composerSeat"],[class*="viewArea"]{overflow-x:clip}
1379
1451
  ` },
1380
- // Empty-state headline + preview badge.
1381
- { anchor: ".pXSMma_headlineText", css: `
1382
- .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}
1383
- 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}
1384
- .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}
1385
- .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}
1386
1462
  ` },
1387
1463
  // Chat bubbles — liquid glass panes.
1388
- { anchor: ".gdEzaW_bubble", css: `
1389
- .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)}
1390
- 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)}
1391
1467
  ` },
1392
1468
  // Trajectory view bar.
1393
1469
  { anchor: ".fV0t5q_root", css: `
@@ -1461,7 +1537,7 @@ body[data-dshome-dark]{
1461
1537
  }
1462
1538
  html .hHd-Xa_root::before,
1463
1539
  html .uV2eYG_card::after,
1464
- html .gdEzaW_bubble,
1540
+ html :is(${USER_BUBBLE}),
1465
1541
  html .fV0t5q_root,
1466
1542
  html [data-composer-card],
1467
1543
  html .VOzbGW_panel,
@@ -1480,27 +1556,27 @@ html .dshome-file-menu{
1480
1556
  --dshome-spec-x/y (written per element by startSpecularParallax). */
1481
1557
  html [data-dshome-spot]:hover{--dshome-glass-spec:1;}
1482
1558
  html [data-composer-card]:hover{--dshome-glass-spec:1;--dshome-glass-sheen:.12;}
1483
- 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{
1484
1560
  background-color:rgb(255 255 255 / calc(var(--dshome-glass-sheen) + .16))!important;
1485
1561
  }
1486
- body[data-dshome-dark] .gdEzaW_bubble:hover,
1562
+ body[data-dshome-dark] :is(${USER_BUBBLE}):hover,
1487
1563
  body[data-dshome-dark] .dshome-sa:hover,
1488
1564
  body[data-dshome-dark] .dshome-file-menu:hover{
1489
1565
  background-color:rgb(255 255 255 / calc(var(--dshome-glass-sheen) + .06))!important;
1490
1566
  }
1491
1567
  html[data-dshome-dispersion] .hHd-Xa_root::before,
1492
- html[data-dshome-dispersion] .gdEzaW_bubble,
1568
+ html[data-dshome-dispersion] :is(${USER_BUBBLE}),
1493
1569
  html[data-dshome-dispersion] .VOzbGW_panel{
1494
1570
  -webkit-backdrop-filter:url(#dshome-glass-dispersion) blur(var(--dshome-glass-blur)) saturate(var(--dshome-glass-saturate))!important;
1495
1571
  backdrop-filter:url(#dshome-glass-dispersion) blur(var(--dshome-glass-blur)) saturate(var(--dshome-glass-saturate))!important;
1496
1572
  }
1497
1573
  @supports not ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){
1498
- 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{
1499
1575
  background-color:rgba(255,255,255,.86)!important;
1500
1576
  }
1501
1577
  body[data-dshome-dark] .hHd-Xa_root::before,
1502
1578
  body[data-dshome-dark] .uV2eYG_card::after,
1503
- body[data-dshome-dark] .gdEzaW_bubble,
1579
+ body[data-dshome-dark] :is(${USER_BUBBLE}),
1504
1580
  body[data-dshome-dark] [data-composer-card],
1505
1581
  body[data-dshome-dark] .VOzbGW_panel,
1506
1582
  body[data-dshome-dark] .dshome-sa,
@@ -1551,14 +1627,9 @@ html[data-dshome-dispersion] .VOzbGW_panel{
1551
1627
  // a transcript can contain ONLY steering bubbles — anchoring the selector
1552
1628
  // on "user" alone silently skipped every bubble on the page.
1553
1629
  //
1554
- // The second half covers the gap the user hit: a message typed while the
1555
- // agent is still working is painted in the transcript BEFORE it is picked
1556
- // up, and at that moment it sits outside any flow-kind container, so the
1557
- // two anchored selectors miss it and it renders unglazed until the turn
1558
- // consumes it. Rather than enumerate the container it lands in, this side
1559
- // takes any bubble that is NOT part of an assistant step — which is the
1560
- // only place a bubble must not be glassed.
1561
- 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.)
1562
1633
  const LIQUID_CSS = `
1563
1634
  @property --dshome-liquid-sweep{syntax:"<angle>";inherits:false;initial-value:0deg}
1564
1635
  @keyframes dshome-liquid-sweep{from{--dshome-liquid-sweep:0deg}to{--dshome-liquid-sweep:360deg}}
@@ -1626,7 +1697,7 @@ body[data-dshome-dark]{
1626
1697
  html[data-dshome-glass="liquid"] body .hHd-Xa_root::before,
1627
1698
  html[data-dshome-glass="liquid"] body [data-composer-card],
1628
1699
  html[data-dshome-glass="liquid"] body .wSkVaW_header:hover::before,
1629
- html[data-dshome-glass="liquid"] body ${LIQUID_BUBBLE}{
1700
+ html[data-dshome-glass="liquid"] body :is(${USER_BUBBLE}){
1630
1701
  background-color:var(--dshome-liquid-fill)!important;
1631
1702
  background-image:
1632
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%),
@@ -1679,8 +1750,11 @@ html[data-dshome-glass="liquid"] body .hHd-Xa_root::before{
1679
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%),
1680
1751
  linear-gradient(180deg, rgba(255,255,255,var(--dshome-liquid-sheen)), rgba(255,255,255,.015) 34%, rgba(255,255,255,0))!important;
1681
1752
  }
1682
- /* The message bubble is the one surface that is not already positioned. */
1683
- 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}){
1684
1758
  position:relative;
1685
1759
  transition:background-color .18s ease,box-shadow .18s ease;
1686
1760
  }
@@ -1692,10 +1766,12 @@ html[data-dshome-glass="liquid"] [data-composer-card]{
1692
1766
  transition:transform .24s cubic-bezier(.2,.8,.2,1),background-color .18s ease,box-shadow .18s ease!important;
1693
1767
  will-change:transform;
1694
1768
  }
1695
- html[data-dshome-glass="liquid"] ${LIQUID_BUBBLE}:hover{
1769
+ html[data-dshome-glass="liquid"] :is(${USER_BUBBLE}):hover{
1696
1770
  background-color:rgb(255 255 255 / .18)!important;
1697
1771
  }
1698
- 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{
1699
1775
  background-color:rgb(255 255 255 / .10)!important;
1700
1776
  }
1701
1777
  html[data-dshome-glass="liquid"] body .hHd-Xa_root::after{
@@ -1732,7 +1808,7 @@ html[data-dshome-glass="liquid"] .hHd-Xa_root::after{
1732
1808
  continuous background a horizontal displacement has nothing to show, which
1733
1809
  the user accepted. */
1734
1810
  html[data-dshome-dispersion][data-dshome-glass="liquid"] body .wSkVaW_header:hover::before,
1735
- html[data-dshome-dispersion][data-dshome-glass="liquid"] body ${LIQUID_BUBBLE}{
1811
+ html[data-dshome-dispersion][data-dshome-glass="liquid"] body :is(${USER_BUBBLE}){
1736
1812
  -webkit-backdrop-filter:url(#dshome-glass-dispersion) blur(var(--dshome-liquid-blur)) saturate(var(--dshome-liquid-saturate))!important;
1737
1813
  backdrop-filter:url(#dshome-glass-dispersion) blur(var(--dshome-liquid-blur)) saturate(var(--dshome-liquid-saturate))!important;
1738
1814
  }
@@ -1789,56 +1865,90 @@ html[data-dshome-blur="off"] body{
1789
1865
  html[data-dshome-glass="liquid"] body .hHd-Xa_root::before,
1790
1866
  html[data-dshome-glass="liquid"] body [data-composer-card],
1791
1867
  html[data-dshome-glass="liquid"] body .wSkVaW_header:hover::before,
1792
- html[data-dshome-glass="liquid"] body ${LIQUID_BUBBLE}{
1868
+ html[data-dshome-glass="liquid"] body :is(${USER_BUBBLE}){
1793
1869
  background-color:rgba(255,255,255,.86)!important;
1794
1870
  }
1795
1871
  body[data-dshome-dark] .hHd-Xa_root::before,
1796
1872
  body[data-dshome-dark] [data-composer-card],
1797
1873
  body[data-dshome-dark] .wSkVaW_header:hover::before,
1798
- body[data-dshome-dark] ${LIQUID_BUBBLE}{
1874
+ html[data-dshome-glass="liquid"] body[data-dshome-dark] :is(${USER_BUBBLE}){
1799
1875
  background-color:rgba(13,30,60,.92)!important;
1800
1876
  }
1801
1877
  }
1802
1878
  `;
1803
1879
 
1804
1880
  /**
1805
- * Inject `css` only while `anchor` matches the DOM. If the anchor is
1806
- * absent at call time, wait up to 30s for it (covers shells that render
1807
- * after the plugin activates); if it never appears, skip the block and log
1808
- * one diagnostic line instead of shipping dead CSS against a renamed
1809
- * 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.
1810
1897
  */
1811
1898
  function installGuarded(anchor, css) {
1899
+ const selectors = Array.isArray(anchor) ? anchor : [anchor];
1900
+ const label = selectors.join('|');
1812
1901
  let dispose = null;
1813
1902
  const applyNow = () => {
1814
1903
  if (dispose !== null) return true;
1815
- if (document.querySelector(anchor) === null) return false;
1816
- dispose = injectStyle(css, 'patch' + anchor);
1817
- 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;
1818
1910
  };
1819
1911
  if (applyNow()) {
1820
1912
  return () => { if (dispose !== null) dispose(); };
1821
1913
  }
1822
- let timer = 0;
1823
- let mo = new MutationObserver(() => {
1824
- if (!applyNow()) return;
1825
- mo.disconnect();
1826
- clearTimeout(timer);
1827
- });
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);
1828
1933
  try {
1829
1934
  mo.observe(document.body, { childList: true, subtree: true });
1830
1935
  } catch (error) {
1831
1936
  mo = null;
1832
1937
  }
1833
- timer = setTimeout(() => {
1834
- if (mo !== null) mo.disconnect();
1835
- if (dispose === null) {
1836
- console.info('[' + PLUGIN_ID + '] patch skipped: ' + anchor + ' not found in DOM (product surface renamed?)');
1837
- }
1838
- }, 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);
1839
1949
  return () => {
1950
+ if (timer !== 0) clearTimeout(timer);
1840
1951
  if (mo !== null) mo.disconnect();
1841
- clearTimeout(timer);
1842
1952
  if (dispose !== null) dispose();
1843
1953
  };
1844
1954
  }
@@ -2216,7 +2326,7 @@ html[data-dshome-blur="off"] body{
2216
2326
  * `.gdEzaW_bubble` this list used to carry matches nothing on the current
2217
2327
  * build, which is how the old entry went dead without anyone noticing.
2218
2328
  */
2219
- 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]`;
2220
2330
  function startSpecularSpotter() {
2221
2331
  const onOver = (event) => {
2222
2332
  const target = event.target;
@@ -2236,92 +2346,125 @@ html[data-dshome-blur="off"] body{
2236
2346
  // cursor is on, about its own centre, under an 800px perspective. The
2237
2347
  // cursor-relative offsets are the same half-range clamp, and the release
2238
2348
  // writes the neutral transform first and only drops the inline properties
2239
- // SETTLE_MS later, so the stylesheet's transition animates the return
2240
- // instead of snapping.
2349
+ // SETTLE_MS later, so the transition animates the return instead of
2350
+ // snapping.
2241
2351
  //
2242
2352
  // Only the composer is tiltable here. mineradio tilts every spot it owns;
2243
2353
  // in this theme the other spots are the sidebar shell and the transcript's
2244
2354
  // message bubbles, where a whole-column lean reads as a bug rather than as
2245
- // depth. It is also gated on the liquid recipe, because the frosted pane is
2246
- // meant to stay observably identical to what shipped before.
2355
+ // depth. "The hover effect is missing" is a question about a guard or the
2356
+ // recipe, so the state is readable from the page (`__dshomeTilt()`).
2357
+ //
2358
+ // 2.0.79 -> 2.0.82, the whole story of one hover effect:
2359
+ // * 2.0.79 chased an ~8 Hz hover loop by dropping the lift (`TILT_SCALE`
2360
+ // 1.01 -> 1, measured as an 8px card growth under a stationary cursor)
2361
+ // AND by listing text entry as an interactive control. Both were
2362
+ // over-corrections with one cause: the input covers the card, so the
2363
+ // tilt stopped engaging for the very gesture it exists for, and the lift
2364
+ // went with it. Reported as "鼠标放在对话框,放大和倾斜没了".
2365
+ // * 2.0.82 restores the lift, lets the tilt engage over text entry (the
2366
+ // lean moves the input together with the card, so the pointer cannot
2367
+ // fall off it), and replaces "release on control crossing" with
2368
+ // HYSTERESIS: entering a control FREEZES the current lean -- no repaint,
2369
+ // no snap back, no re-arm -- it resumes on the card body, and it is
2370
+ // released only when the pointer leaves the card's own box, followed by
2371
+ // a cooldown before the next engagement. The loop 2.0.79 was chasing
2372
+ // needed that release to feed it; a lean held still under a stationary
2373
+ // pointer has no feedback path left.
2374
+ // * 2.0.82 also DROPS THE RECIPE GATE. The tilt used to return early
2375
+ // unless the glass was `liquid`, on the theory that the frosted pane
2376
+ // should stay observably identical to what shipped before -- which made
2377
+ // the effect invisible for anyone on the default-looking white frosted
2378
+ // recipe. Both recipes tilt now, and because the liquid sheet used to be
2379
+ // the only place carrying `transition: transform`, the motion carries
2380
+ // its own transition (TILT_TRANSITION) so a recipe cannot lose it.
2247
2381
  const TILT_SELECTOR = '[data-composer-card],.uV2eYG_card';
2248
2382
  const TILT_MAX = 0.0175;
2249
2383
  const TILT_PERSPECTIVE = 800;
2250
- // 2.0.79: 1.01 -> 1. Measured on the live page, the lift grew the card from
2251
- // 757 to 765 px under a stationary cursor (8 px, the same amount the
2252
- // horizontal scrollbar used to steal), and that was enough to flip the
2253
- // element under the pointer between the composer's own controls and the
2254
- // column's width handle; each flip re-armed or released the tilt, giving an
2255
- // ~8 Hz hover loop the user reported as "the buttons flicker". The lean
2256
- // itself (the two rotations) stays: it moves things by ~1 px instead of 8.
2257
- const TILT_SCALE = 1;
2384
+ const TILT_SCALE = 1.01;
2258
2385
  const TILT_SETTLE_MS = 240;
2386
+ const TILT_COOLDOWN_MS = 200;
2387
+ /** The lean's own easing. Mirrors the liquid sheet's values, and is removed
2388
+ * together with the inline transform so nothing keeps animating afterwards. */
2389
+ const TILT_TRANSITION = 'transform .24s cubic-bezier(.2,.8,.2,1),background-color .18s ease,box-shadow .18s ease';
2259
2390
  /**
2260
- * Interactive controls inside the card must never sit on a surface that
2261
- * moves: the pointer being over a control is exactly when a moving edge
2262
- * costs the user a hover state. `andle` covers the column's width handle
2263
- * (`wSkVaW_widthHandle`) and any future drag handle without pinning a
2264
- * hashed class name; the attribute match is case-insensitive.
2391
+ * Controls the pointer can land on *inside* the card, where aiming is the
2392
+ * user's whole intent. Text entry is deliberately NOT in this list: the input
2393
+ * is the surface the tilt exists for, and the lean carries it along with the
2394
+ * card. `andle` covers the column's width handle (`wSkVaW_widthHandle`) and
2395
+ * any future drag handle without pinning a hashed class name; the attribute
2396
+ * match is case-insensitive.
2265
2397
  */
2266
- const TILT_INTERACTIVE = 'button,[role="button"],a[href],input,textarea,select,[contenteditable],[class*="andle" i]';
2398
+ const TILT_CONTROLS = 'button,[role="button"],a[href],[class*="andle" i]';
2267
2399
  function startComposerTilt() {
2268
2400
  let current = null;
2269
2401
  let raf = 0;
2402
+ /** A control is under the pointer: hold the lean, paint nothing, release nothing. */
2403
+ let frozen = false;
2404
+ /** performance.now() of the last release; the next engagement waits out the cooldown. */
2405
+ let releasedAt = -Infinity;
2406
+ let engagements = 0;
2270
2407
  const tilted = new WeakSet();
2271
2408
  const settle = new Map();
2272
2409
  const reduced = typeof window.matchMedia === 'function' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
2273
2410
  if (reduced) return () => {};
2411
+ // Readable from the page: the effect is guarded twice, so "it is missing"
2412
+ // is otherwise a guess about which guard ate it. `__dshomeTilt()` answers
2413
+ // that in one call (and reports the recipe it is running under).
2414
+ try {
2415
+ window.__dshomeTilt = () => ({ recipe: dsttGetGlass(), leaning: current !== null, frozen, engagements, sinceRelease: releasedAt === -Infinity ? null : Math.round(performance.now() - releasedAt) });
2416
+ } catch (error) {}
2274
2417
  const release = (spot) => {
2275
2418
  const pending = settle.get(spot);
2276
2419
  if (pending !== undefined) {
2277
2420
  clearTimeout(pending);
2278
2421
  settle.delete(spot);
2279
2422
  }
2423
+ frozen = false;
2280
2424
  if (!tilted.has(spot)) return;
2425
+ releasedAt = performance.now();
2281
2426
  tilted.delete(spot);
2282
2427
  spot.style.transform = 'perspective(' + TILT_PERSPECTIVE + 'px) rotateX(0rad) rotateY(0rad) scale(1)';
2283
2428
  const handle = window.setTimeout(() => {
2284
2429
  settle.delete(spot);
2285
2430
  spot.style.removeProperty('transform');
2286
2431
  spot.style.removeProperty('transform-origin');
2432
+ spot.style.removeProperty('transition');
2287
2433
  }, TILT_SETTLE_MS);
2288
2434
  settle.set(spot, handle);
2289
2435
  };
2290
2436
  const paint = (spot, rect, clientX, clientY) => {
2437
+ if (frozen) return;
2291
2438
  if (raf !== 0) return;
2292
2439
  raf = requestAnimationFrame(() => {
2293
2440
  raf = 0;
2294
- if (current !== spot) return;
2295
- if (dsttGetGlass() !== 'liquid') {
2296
- release(spot);
2297
- return;
2298
- }
2441
+ if (current !== spot || frozen) return;
2299
2442
  if (clientX < rect.left || clientX > rect.right || clientY < rect.top || clientY > rect.bottom) return;
2300
2443
  const dx = Math.min(0.5, Math.max(-0.5, (clientX - rect.left) / rect.width - 0.5));
2301
2444
  const dy = Math.min(0.5, Math.max(-0.5, (clientY - rect.top) / rect.height - 0.5));
2302
2445
  spot.style.transformOrigin = (rect.width / 2) + 'px ' + (rect.height / 2) + 'px';
2446
+ spot.style.transition = TILT_TRANSITION;
2303
2447
  spot.style.transform = 'perspective(' + TILT_PERSPECTIVE + 'px)'
2304
2448
  + ' rotateX(' + (TILT_MAX * -2 * dy).toFixed(5) + 'rad)'
2305
2449
  + ' rotateY(' + (TILT_MAX * 2 * dx).toFixed(5) + 'rad)'
2306
2450
  + ' scale(' + TILT_SCALE + ')';
2451
+ if (!tilted.has(spot)) engagements += 1;
2307
2452
  tilted.add(spot);
2308
2453
  });
2309
2454
  };
2310
2455
  const onOver = (event) => {
2311
- if (dsttGetGlass() !== 'liquid') return;
2312
2456
  const target = event.target;
2313
2457
  if (target === null || typeof target.closest !== 'function') return;
2314
2458
  const spot = target.closest(TILT_SELECTOR);
2315
2459
  if (spot === null) return;
2316
- // A control (or a drag handle) is under the pointer: leave the card
2317
- // still. See TILT_INTERACTIVE for why this is not cosmetic.
2318
- if (target.closest(TILT_INTERACTIVE) !== null) {
2319
- if (current === spot) {
2320
- release(spot);
2321
- current = null;
2322
- }
2460
+ // A control is under the pointer: hold the lean exactly where it is.
2461
+ // Releasing here is what used to feed the ~8 Hz hover loop.
2462
+ if (target.closest(TILT_CONTROLS) !== null) {
2463
+ if (current === spot) frozen = true;
2323
2464
  return;
2324
2465
  }
2466
+ // The cooldown gates a fresh engagement only, never an ongoing lean.
2467
+ if (!tilted.has(spot) && performance.now() - releasedAt < TILT_COOLDOWN_MS) return;
2325
2468
  const rect = spot.getBoundingClientRect();
2326
2469
  if (event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom) return;
2327
2470
  const pending = settle.get(spot);
@@ -2329,17 +2472,18 @@ html[data-dshome-blur="off"] body{
2329
2472
  clearTimeout(pending);
2330
2473
  settle.delete(spot);
2331
2474
  }
2475
+ frozen = false;
2332
2476
  current = spot;
2333
2477
  paint(spot, rect, event.clientX, event.clientY);
2334
2478
  };
2335
2479
  const onMove = (event) => {
2336
2480
  if (current === null) return;
2337
2481
  const target = event.target;
2338
- if (target !== null && typeof target.closest === 'function' && target.closest(TILT_INTERACTIVE) !== null) {
2339
- release(current);
2340
- current = null;
2482
+ if (target !== null && typeof target.closest === 'function' && target.closest(TILT_CONTROLS) !== null) {
2483
+ frozen = true;
2341
2484
  return;
2342
2485
  }
2486
+ frozen = false;
2343
2487
  paint(current, current.getBoundingClientRect(), event.clientX, event.clientY);
2344
2488
  };
2345
2489
  const onOut = (event) => {
@@ -2367,6 +2511,7 @@ html[data-dshome-blur="off"] body{
2367
2511
  for (const spot of document.querySelectorAll(TILT_SELECTOR)) {
2368
2512
  spot.style.removeProperty('transform');
2369
2513
  spot.style.removeProperty('transform-origin');
2514
+ spot.style.removeProperty('transition');
2370
2515
  }
2371
2516
  current = null;
2372
2517
  };
@@ -2386,7 +2531,7 @@ html[data-dshome-blur="off"] body{
2386
2531
  if (!supportsWebgl2()) return startParticles();
2387
2532
  // Which build is running, readable from the page (see README). One line in
2388
2533
  // the console answers "is the bundle I just installed actually live?".
2389
- try { window.__dshomeBuild = '2.0.81 rgba16f-flow'; } catch (error) {}
2534
+ try { window.__dshomeBuild = '2.0.84 bug-report-fixes'; } catch (error) {}
2390
2535
  const canvas = document.createElement('canvas');
2391
2536
  canvas.setAttribute('data-dsh-deepseek-canvas', '');
2392
2537
  const style = canvas.style;
@@ -2491,7 +2636,7 @@ html[data-dshome-blur="off"] body{
2491
2636
  }
2492
2637
 
2493
2638
  function startParticles() {
2494
- try { window.__dshomeBuild = '2.0.81 rgba16f-flow'; } catch (error) {}
2639
+ try { window.__dshomeBuild = '2.0.84 bug-report-fixes'; } catch (error) {}
2495
2640
  const canvas = document.createElement('canvas');
2496
2641
  canvas.setAttribute('data-dsh-deepseek-canvas', '');
2497
2642
  const style = canvas.style;
@@ -2563,6 +2708,11 @@ html[data-dshome-blur="off"] body{
2563
2708
 
2564
2709
  function step() {
2565
2710
  if (!running) return;
2711
+ // Same self-cleanup as the fluid loop: no canvas in the DOM, no work.
2712
+ if (!canvas.isConnected) {
2713
+ running = false;
2714
+ return;
2715
+ }
2566
2716
  g.clearRect(0, 0, W, H);
2567
2717
  const base = linkBase();
2568
2718
  for (let i = 0; i < parts.length; i++) {
@@ -3893,7 +4043,10 @@ body[data-dshome-dark] .${CLS}{background:rgba(14,30,60,.94);box-shadow:0 18px 4
3893
4043
  // set they asked to keep. Ids match the host schema's `backgroundMode`
3894
4044
  // enum; the free-text value for 方式3 is `customBackground`.
3895
4045
  const DSTT_BACKGROUNDS = ["classic", "white", "custom", "bold"];
3896
- const DSTT_BACKGROUND_DEFAULT = "classic";
4046
+ // `bold` by default: ① classic averages (soft lead + white + near-white) into a
4047
+ // wash the eye reads as blue-white whatever the colour token says, so it is a
4048
+ // poor first impression -- it stays selectable for anyone who wants it.
4049
+ const DSTT_BACKGROUND_DEFAULT = "bold";
3897
4050
  const DSTT_BACKGROUND_LABELS = {
3898
4051
  classic: { zh: "① 主色+纯白+近白", en: "1 · Lead + white + near-white" },
3899
4052
  white: { zh: "② 主色+纯白", en: "2 · Lead + white" },
@@ -4534,7 +4687,7 @@ body[data-dshome-dark] .${CLS}{background:rgba(14,30,60,.94);box-shadow:0 18px 4
4534
4687
  React.createElement('div', { className: 'dshome-dstt-row-title' }, isZh ? '背景方式' : 'Background recipe'),
4535
4688
  React.createElement('div', { className: 'dshome-dstt-row-hint' }, isZh
4536
4689
  ? '① 主色+纯白+近白(默认,旧版那套同色系柔和观感);② 主色+纯白(只有两个色参与,最干净);③ 自选背景:我们的流体与主题底色全部让位(没别的在画时是纯白 / 纯黑);装了 dsh-plugin-wallpaper-engine 且它正在渲染时,连纯白也不画,背景交给它。颜色只驱动按钮之类。输入框可填图片 URL / 任意 CSS 值,或点「桌面壁纸」用本机壁纸;浓三色是 1.43.12 那套,先留着。'
4537
- : '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.'),
4690
+ : '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.'),
4538
4691
  React.createElement('div', { className: 'dshome-dstt-seg dshome-dstt-seg-row', role: 'radiogroup', 'aria-label': isZh ? '背景方式' : 'Background recipe' },
4539
4692
  DSTT_BACKGROUNDS.map((id) => React.createElement('button', {
4540
4693
  key: id,
@@ -4767,7 +4920,19 @@ body[data-dshome-dark] .${CLS}{background:rgba(14,30,60,.94);box-shadow:0 18px 4
4767
4920
  try { stop(); } catch (error) {}
4768
4921
  return;
4769
4922
  }
4770
- try { dispose = startAmbient(); } catch (error) { dispose = () => {}; }
4923
+ try {
4924
+ dispose = startAmbient();
4925
+ } catch (error) {
4926
+ // NEVER silent. A throw inside the mount used to be swallowed right
4927
+ // here, which is how one undeclared identifier (ATTR, deleted in
4928
+ // d862487) hid for nineteen versions: no refraction anywhere, a
4929
+ // 动态背景 toggle that stopped unmounting, and one leaked
4930
+ // simulator per flip. Sweep the orphan canvas too -- the fluid
4931
+ // loop also self-cancels when its canvas leaves the DOM.
4932
+ console.error('[dsh-deepseek-style-theme] ambient background failed to mount:', error);
4933
+ for (const stray of document.querySelectorAll('canvas[data-dsh-deepseek-canvas]')) stray.remove();
4934
+ dispose = () => {};
4935
+ }
4771
4936
  };
4772
4937
  syncAmbient();
4773
4938
  const unsubscribe = dsttSubscribe(syncAmbient);
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.81",
4
+ "version": "2.0.84",
5
5
  "type": "module",
6
6
  "keywords": [
7
7
  "deepseek",