dsh-deepseek-style-theme 2.0.89 → 2.1.91

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 (4) hide show
  1. package/README.md +39 -48
  2. package/lib/client.js +808 -857
  3. package/lib/index.js +143 -390
  4. package/package.json +5 -3
package/lib/client.js CHANGED
@@ -14,6 +14,19 @@
14
14
  // wants and the other entry is simply never asked for. The registrations are
15
15
  // guarded individually because a loader might reject an id it did not request,
16
16
  // and that must not take the working one down with it.
17
+ //
18
+ // Wrapped in an IIFE on purpose: this bundle is a CLASSIC script, so a top-level
19
+ // `const` lands in the global scope and a SECOND evaluation of the same file
20
+ // throws `Identifier 'dshDeepseekStyleThemeFactory' has already been declared`
21
+ // -- and it throws on the declaration line, before `__ModuleLoader__.load()`
22
+ // ever runs, so the new code never registers. The desktop app hot-reloads client
23
+ // plugin bundles on file change, which makes that the normal case rather than an
24
+ // edge case: from the second edit onward the page keeps the FIRST successful
25
+ // evaluation's JS (its styles stay in the DOM, so it still LOOKS themed) and
26
+ // silently ignores every later one. A full reload was the only way out. Inside
27
+ // the IIFE the name is function-scoped, every evaluation is independent, and a
28
+ // reload is no longer required.
29
+ (() => {
17
30
  const dshDeepseekStyleThemeFactory = (require) => {
18
31
  var module = { exports: {} };
19
32
  var exports = module.exports;
@@ -135,7 +148,7 @@ const dshDeepseekStyleThemeFactory = (require) => {
135
148
  // element and the { main, mainCanvas } handle explicitly.
136
149
  //
137
150
  // DOM hooks the ported code still reads (product/platform markup, intentionally
138
- // NOT renamed): `[data-composer-card]` and `[data-dsh-stats]` inside
151
+ // NOT renamed): `[data-composer-card]` and `[data-composer-stats]` inside
139
152
  // visualRect(), and the `button` closest-match in attachFluidInteractions.
140
153
  //
141
154
  // Indentation note: this fragment is tab-indented at a base of 2 tabs to match
@@ -944,14 +957,14 @@ void main() {
944
957
  return target instanceof Element ? target.closest(SPOT_SELECTOR) : null;
945
958
  }
946
959
  // Inlined from upstream `src/client/spot-core.ts` (lines 1717-1729). The
947
- // `[data-composer-card]` / `[data-dsh-stats]` selectors are product markup
960
+ // `[data-composer-card]` / `[data-composer-stats]` selectors are product markup
948
961
  // and are deliberately left unrenamed; a mount that lacks them simply
949
962
  // falls back to the spot's own border box.
950
963
  function visualRect(spot) {
951
964
  if (spot.querySelector("[data-composer-card]") !== null) {
952
965
  const card = spot.querySelector("[data-composer-card]");
953
966
  const r0 = card.getBoundingClientRect();
954
- const stats = spot.querySelector("[data-dsh-stats]");
967
+ const stats = spot.querySelector("[data-composer-stats]");
955
968
  if (stats === null) return r0;
956
969
  const r1 = stats.getBoundingClientRect();
957
970
  const left = Math.min(r0.left, r1.left);
@@ -1092,12 +1105,19 @@ void main() {
1092
1105
  // `[role=menu]`, `.dshome-*`). No product class names — always injected.
1093
1106
  // Color model: `data-dshome-color` ∈ {green(base), red(鲜红 peak), blue}
1094
1107
  // re-tints every var(--ds-brand*) consumer.
1095
- const CORE_CSS = `
1096
- :root{--ds-brand:#059669;--ds-brand-deep:#047857;--ds-brand-mid:#10B981;--ds-brand-light:#34D399;--ds-glass-blur:24px}
1108
+ const CORE_CSS = `:root{--ds-brand:#059669;--ds-brand-deep:#047857;--ds-brand-mid:#10B981;--ds-brand-light:#34D399;--ds-glass-blur:24px}
1109
+ /* 圆角策略 = DSTT 原设计,别自己发明数值:
1110
+ 产品 corner-shape.css 已用 @supports(corner-shape:superellipse(1.5)) 对 *、:before、:after
1111
+ 统一设了 superellipse(1.5),本主题**保留**它,只在胶囊/正圆这类会被超级椭圆压变形的
1112
+ 元素上改回真圆——产品自己的文档也要求这样配对("full-round shapes — border-radius:50%
1113
+ circles and pill radii — pair corner-shape:round with their radius")。
1114
+ 历史上这个覆盖写在 .dshome-locale / .wSkVaW_tab 上,2.1.0 语义化后集中到下面那一处,
1115
+ 覆盖中/EN 切换、标签、header 里各胶囊与圆形按钮。 */
1097
1116
  html,body{background-color:transparent!important}
1098
1117
  /* THE LABEL TOKENS ARE NOT PINNED HERE, AND MUST NOT BE AGAIN.
1099
1118
  A previous attempt at the invisible-text bug declared
1100
- body{--dsw-alias-label-primary:...!important} under our own scheme marker,
1119
+ body{--dsw-alias-label-primary:...!important}
1120
+ under our own scheme marker,
1101
1121
  so that background and text could never end up on opposite sides. It did fix
1102
1122
  that, and it broke something worse: those !important declarations outrank the
1103
1123
  PRODUCT's tokens, so when the user switched appearance the product changed its
@@ -1209,19 +1229,16 @@ body[data-dshome-dark] .dshome-dstt-status[data-peak=false]{color:var(--ds-brand
1209
1229
  .dshome-dstt-input-state[data-ok=false]{color:var(--dsw-alias-state-error-primary)}
1210
1230
  .dshome-dstt-seg-row{margin-top:8px;flex-wrap:wrap}
1211
1231
  .dshome-dstt-warn{border-color:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 45%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 10%,transparent)}
1212
- .dshome-dstt-warn .dshome-dstt-row-title{color:var(--dsw-alias-state-warn-primary)}
1213
- `;
1232
+ .dshome-dstt-warn .dshome-dstt-row-title{color:var(--dsw-alias-state-warn-primary)}`;
1214
1233
 
1215
1234
  // Locale toggle styling. The two header-hover dependent rules live in the
1216
1235
  // `.wSkVaW_root` patch block below, so the toggle stays styled (short
1217
1236
  // labels) even if the header class renames.
1218
- // The product sets `corner-shape:superellipse(1.5)` on `*` (its
1219
- // corner-shape.css), which flattens even a 999px radius into a rounded
1220
- // rectangle; `corner-shape:round` restores true round ends here.
1237
+ // corner-shape 见 CORE_CSS 顶部那段说明:产品全局是 superellipse(1.5),胶囊与正圆
1238
+ // 必须配对 `round`,否则会被超级椭圆压成方圆。这一条就是那个配对(DSTT 原设计)。
1221
1239
  // Label bloom: the short 中/E label grows into 中文/EN with an animated
1222
1240
  // max-width instead of the old `display` hard swap.
1223
- const ACTIONS_CSS = `
1224
- .dshome-locale{display:inline-flex;align-items:center;height:28px;border:1px solid color-mix(in srgb,var(--ds-brand) 25%,transparent);border-radius:999px;background:color-mix(in srgb,var(--ds-brand) 8%,transparent);overflow:hidden;flex:none;corner-shape:round;transition:transform .28s cubic-bezier(.2,.8,.2,1),box-shadow .28s ease,border-color .22s ease,background .22s ease}
1241
+ const ACTIONS_CSS = `.dshome-locale{display:inline-flex;align-items:center;height:28px;border:1px solid color-mix(in srgb,var(--ds-brand) 25%,transparent);border-radius:999px;background:color-mix(in srgb,var(--ds-brand) 8%,transparent);overflow:hidden;flex:none;corner-shape:round;transition:transform .28s cubic-bezier(.2,.8,.2,1),box-shadow .28s ease,border-color .22s ease,background .22s ease}
1225
1242
  .dshome-locale:hover{transform:translateY(-1px) scale(1.03);border-color:color-mix(in srgb,var(--ds-brand) 48%,transparent);background:color-mix(in srgb,var(--ds-brand) 14%,transparent);box-shadow:0 4px 14px color-mix(in srgb,var(--ds-brand) 26%,transparent)}
1226
1243
  body[data-dshome-dark] .dshome-locale{border-color:color-mix(in srgb,var(--ds-brand) 35%,transparent);background:color-mix(in srgb,var(--ds-brand) 16%,transparent)}
1227
1244
  body[data-dshome-dark] .dshome-locale:hover{border-color:color-mix(in srgb,var(--ds-brand) 55%,transparent);background:color-mix(in srgb,var(--ds-brand) 24%,transparent);box-shadow:0 4px 16px color-mix(in srgb,var(--ds-brand) 34%,transparent)}
@@ -1235,17 +1252,20 @@ body[data-dshome-dark] .dshome-locale:hover{border-color:color-mix(in srgb,var(-
1235
1252
  .dshome-locale .dshome-full{max-width:0;opacity:0;transform:translateX(-6px)}
1236
1253
  body[data-dshome-dark] .dshome-locale button{color:var(--ds-brand-light)}
1237
1254
  body[data-dshome-dark] .dshome-locale button:not(.dshome-on):hover{background:color-mix(in srgb,var(--ds-brand) 22%,transparent);color:#fff}
1238
- body[data-dshome-dark] .dshome-locale button.dshome-on{background:linear-gradient(135deg,color-mix(in srgb,var(--ds-brand) 72%,#fff),var(--ds-brand));color:#fff}
1239
- `;
1255
+ body[data-dshome-dark] .dshome-locale button.dshome-on{background:linear-gradient(135deg,color-mix(in srgb,var(--ds-brand) 72%,#fff),var(--ds-brand));color:#fff}`;
1240
1256
 
1241
- const SUBAGENT_PANEL_CSS = `
1242
- .dshome-sa{position:fixed;right:16px;bottom:16px;z-index:70;width:286px;max-width:calc(100vw - 32px);box-sizing:border-box;display:flex;flex-direction:column;gap:2px;padding:10px 12px 8px;border-radius:16px;border:1px solid rgba(255,255,255,.55);background:linear-gradient(165deg,rgba(255,255,255,.46),rgba(255,255,255,.22));box-shadow:0 12px 40px rgba(13,30,60,.14),inset 0 1px 0 rgba(255,255,255,.85),inset 0 0 0 .5px rgba(255,255,255,.5);-webkit-backdrop-filter:blur(24px) saturate(1.6);backdrop-filter:blur(24px) saturate(1.6);font-family:inherit;transition:opacity .22s ease,transform .22s ease;animation:dshome-sa-in .3s cubic-bezier(.2,.8,.2,1)}
1257
+ const SUBAGENT_PANEL_CSS = `.dshome-sa{position:fixed;right:16px;bottom:16px;z-index:70;width:286px;max-width:calc(100vw - 32px);box-sizing:border-box;display:flex;flex-direction:column;gap:2px;padding:10px 12px 8px;border-radius:16px;border:1px solid rgba(255,255,255,.55);background:linear-gradient(165deg,rgba(255,255,255,.46),rgba(255,255,255,.22));box-shadow:0 12px 40px rgba(13,30,60,.14),inset 0 1px 0 rgba(255,255,255,.85),inset 0 0 0 .5px rgba(255,255,255,.5);-webkit-backdrop-filter:blur(24px) saturate(1.6);backdrop-filter:blur(24px) saturate(1.6);font-family:inherit;transition:opacity .22s ease,transform .22s ease;animation:dshome-sa-in .3s cubic-bezier(.2,.8,.2,1)}
1243
1258
  .dshome-sa[hidden]{display:none!important}
1244
1259
  .dshome-sa.dshome-sa-leave{opacity:0;transform:translateY(10px);pointer-events:none}
1245
1260
  body[data-dshome-dark] .dshome-sa{background:linear-gradient(165deg,rgba(21,42,86,.52),rgba(10,26,58,.3));border-color:color-mix(in srgb,var(--ds-brand) 24%,transparent);box-shadow:0 12px 40px rgba(0,0,0,.4),inset 0 1px 0 rgba(255,255,255,.12),inset 0 0 0 .5px color-mix(in srgb,var(--ds-brand) 16%,transparent)}
1246
- @supports not ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){.dshome-sa{background:linear-gradient(165deg,rgba(255,255,255,.84),rgba(255,255,255,.68))}body[data-dshome-dark] .dshome-sa{background:linear-gradient(165deg,rgba(13,30,60,.88),rgba(10,26,58,.78))}}
1261
+ @supports not ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){.dshome-sa{background:linear-gradient(165deg,rgba(255,255,255,.84),rgba(255,255,255,.68))}
1262
+ body[data-dshome-dark] .dshome-sa{background:linear-gradient(165deg,rgba(13,30,60,.88),rgba(10,26,58,.78))}}
1247
1263
  @keyframes dshome-sa-in{from{opacity:0;transform:translateY(14px) scale(.97)}to{opacity:1;transform:none}}
1248
1264
  .dshome-sa-head{display:flex;flex-direction:row;align-items:center;gap:7px;min-height:26px}
1265
+ .dshome-sa-grab{cursor:grab;touch-action:none;user-select:none;-webkit-user-select:none}
1266
+ .dshome-sa-grab:active{cursor:grabbing}
1267
+ .dshome-sa-dragging{transition:none!important;animation:none!important;box-shadow:0 18px 52px rgba(13,30,60,.22),inset 0 1px 0 rgba(255,255,255,.9),inset 0 0 0 .5px rgba(255,255,255,.5)!important}
1268
+ .dshome-sa-dragging .dshome-sa-list{pointer-events:none}
1249
1269
  .dshome-sa-pulse{position:relative;flex:none;width:8px;height:8px;border-radius:50%;background:var(--ds-brand);animation:dshome-sa-pulse 1.8s ease-out infinite}
1250
1270
  @keyframes dshome-sa-pulse{0%{box-shadow:0 0 0 0 color-mix(in srgb,var(--ds-brand) 45%,transparent)}70%{box-shadow:0 0 0 7px color-mix(in srgb,var(--ds-brand) 0%,transparent)}100%{box-shadow:0 0 0 0 color-mix(in srgb,var(--ds-brand) 0%,transparent)}}
1251
1271
  .dshome-sa-title{flex:1;min-width:0;font-size:12.5px;line-height:18px;font-weight:600;color:var(--dsw-alias-label-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
@@ -1284,8 +1304,7 @@ body[data-dshome-dark] .dshome-sa-item.dshome-sa-stopping .dshome-sa-bar{backgro
1284
1304
  .dshome-sa-bar{position:relative;height:3px;border-radius:999px;background:color-mix(in srgb,var(--ds-brand) 12%,transparent);overflow:hidden}
1285
1305
  .dshome-sa-bar-fill{position:absolute;inset:0;width:42%;border-radius:999px;background:linear-gradient(90deg,transparent,var(--ds-brand),transparent);animation:dshome-sa-slide 1.5s ease-in-out infinite}
1286
1306
  @keyframes dshome-sa-slide{0%{transform:translateX(-110%)}100%{transform:translateX(350%)}}
1287
- @media (max-width:760px){.dshome-sa{right:10px;bottom:10px;width:min(286px,calc(100vw - 20px))}}
1288
- `;
1307
+ @media (max-width:760px){.dshome-sa{right:10px;bottom:10px;width:min(286px,calc(100vw - 20px))}}`;
1289
1308
 
1290
1309
  // ── USER_BUBBLE: the one selector both recipes glass user bubbles with ────
1291
1310
  // Declared HERE, above PATCH_BLOCKS and GLASS_CSS, because both interpolate
@@ -1321,7 +1340,40 @@ body[data-dshome-dark] .dshome-sa-item.dshome-sa-stopping .dshome-sa-bar{backgro
1321
1340
  // still working sits outside any flow-kind container until the turn picks it
1322
1341
  // up — is knowingly left unglazed for that moment: a transient unglazed
1323
1342
  // bubble is a cosmetic wait, a teleporting button is not.
1324
- const USER_BUBBLE = ':is([data-chat-flow-kind="user"],[data-chat-flow-kind="steering"]) [class*="bubble" i]:not([class*="_bubble_"]):not([role="tooltip"])';
1343
+ // 结构面的语义判据(0.2.0 起):产品没给侧栏外壳这类容器任何 data-*,
1344
+ // 但它们的**组件 key 是稳定身份**(_newSession / _brand / _scrollBody /
1345
+ // _sessionRow / _bubble),每次构建变的只是前面的哈希。所以按唯一后缀匹配,
1346
+ // 并用 :has() 守卫——产品把 key 也改名时选择器匹配不到元素,结果是该面没有
1347
+ // 样式,而不是套到错误的元素上。(0.2.0 实测这些后缀各只有 1–2 个取值。)
1348
+ // 侧栏外壳的认法。2026-10-03 实测:官方桌面 0.2.0-rc.2 的侧栏根是
1349
+ // `<div class="_2H3hWW_root">`(产品 JSX:jsxs("div", { className: clsx(SidebarRoot.root, …) })),
1350
+ // **既不是 aside 也不是 [role="navigation"]** —— 原来那半支因此 0 命中,
1351
+ // 侧栏玻璃就整块没上(探针里"侧栏外壳: 0")。
1352
+ // 现在两支并列写进一个 :is(),保持"一个复合选择器、没有顶层逗号"这条底线:
1353
+ // 顶层逗号会在 `html[data-dshome-glass="liquid"] :is(SIDEBAR){…}` 这种前缀用法里
1354
+ // 让逗号后面的分支逃出闸门。
1355
+ // · 第 1 支:老写作,留着不亏(产品哪天改回语义标签就用上);
1356
+ // · 第 2 支:直接装着 _logoRow 的那个 div,并且内部有 _newSession。
1357
+ // `_logoRow` 在 JSX 里是 root 的直接子元素(children 数组的第一项之一),
1358
+ // 所以 `>` 成立;两个条件一起用,避免命中别的容器。
1359
+ const SIDEBAR = ':is(:is(aside,[role="navigation"]):has(:is([class*="_newSession"],[class*="_brand"])),div:has(> [class*="_logoRow"]):has([class*="_newSession"]))';
1360
+ // 末尾那道按形状排掉 tooltip 的守卫(:not([class*="_bubble_"]))必须留着。
1361
+ // 它是 2.0.90 用一次事故换来的:产品的 tooltip 类名是 `_bubble_<hash>_<n>`
1362
+ // (前导下划线那种写法),会被 [class*="_bubble"] 一起吃进来;而主题给气泡的
1363
+ // `position:relative` 会把 position:fixed 的浮层打成普通流内块,于是悬停发送/
1364
+ // 复制按钮时按钮被从光标底下顶走、tooltip 卸载、按钮回来、循环往复 —— 用户看到的
1365
+ // 「按钮跳上跳下」。2.1.0 语义化时这道守卫丢了,只剩 [role="tooltip"],而角色不一定
1366
+ // 挂在我们认识的那一层上。消息气泡是 `<hash>_bubble`(下划线结尾),不含 `_bubble_`,
1367
+ // 所以这道守卫不会误伤它们。
1368
+ const USER_BUBBLE = ':is([data-chat-flow-kind="user"],[data-chat-flow-kind="steering"]) [class*="_bubble"]:not([class*="_bubble_"]):not([role="tooltip"])';
1369
+ // 输入区那块玻璃面 = **输入卡自己**。
1370
+ // 2026-10-03 试过两个别的落点,都被实测否掉:
1371
+ // · data-composer-stats 的父元素:display:contents 空壳,盒子 0×0,玻璃画上去等于没画;
1372
+ // · 卡片的父元素:实测 418×158 —— 左右各多 16px 内边距、下面还多出统计行所在的
1373
+ // dock,用户看到的就是「对话框被撑大了」,而且连鼠标随动的倾斜也一起摊到那一大块上。
1374
+ // 所以玻璃、倾斜、光标高光一律认卡片自己(实测 386×98,与产品原样同尺寸),
1375
+ // 并且**不动产品自己的圆角、边框、内边距**,只换底色与模糊。
1376
+ const COMPOSER_PLATE = '[data-composer-card]';
1325
1377
 
1326
1378
  // PATCH_BLOCKS: every rule group that targets product build-time class
1327
1379
  // names. `anchor` is the class that must exist in the DOM before the
@@ -1329,248 +1381,172 @@ body[data-dshome-dark] .dshome-sa-item.dshome-sa-stopping .dshome-sa-bar{backgro
1329
1381
  // renames in a product rebuild, and `anchor` may be a list of
1330
1382
  // alternatives when a surface has already been renamed once.
1331
1383
  const PATCH_BLOCKS = [
1332
- // Frame / sidebar / details columns.
1333
- { anchor: ".pI_x6G_frame", css: `
1334
- .pI_x6G_frame,.pI_x6G_sidebarCol,.pI_x6G_detailsCol,.pI_x6G_centerCol{background:transparent!important}
1335
- .pI_x6G_sidebarCol{border-right:none!important}
1336
- .pI_x6G_detailsCol{border-left:none!important}
1384
+ // 首屏:**字号交还官方**(需求第 8 条:明暗主题与字号都借官方接口,不自建)。
1385
+ // 原来这里挂着两条 font-size:34px 的标题规则,锚点是 [class*="composerHero"]。
1386
+ // 2026-10-03 实测该后缀在当前产品里 0 命中(首屏那一家是 Hqq-bq_headline),
1387
+ // 也就是说它们**早就是死的**——删掉没有任何视觉变化,只是把死代码清掉。
1388
+ // 预览角标那条保留:它是样式不是字号,而且 README 的语义钩子表里本来写的就是
1389
+ // [class*="_previewBadge"],没有 composerHero 那一层——这里让代码跟上文档。
1390
+ { anchor: '[class*="_previewBadge"], [class*="_fish"]', css: `
1391
+ :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}
1392
+ :is([class*="_fish"]){color:var(--ds-brand)!important}
1337
1393
  ` },
1338
- // Sidebar glass root — Apple liquid glass pane: layered translucent
1339
- // gradient + blur/saturate/brightness/contrast + specular edges.
1340
- { anchor: ".hHd-Xa_root", css: `
1341
- .hHd-Xa_root{position:relative;background:transparent!important}
1342
- .hHd-Xa_root::before{content:"";position:absolute;inset:0;z-index:-1;background:rgba(255,255,255,.5);-webkit-backdrop-filter:blur(var(--ds-glass-blur));backdrop-filter:blur(var(--ds-glass-blur));border-right:1px solid rgba(0,0,0,.06);pointer-events:none}
1343
- body[data-dshome-dark] .hHd-Xa_root::before{background:rgba(10,26,58,.48);border-right:1px solid rgba(255,255,255,.08)}
1344
- .hHd-Xa_brand{color:var(--ds-brand);cursor:pointer}
1345
- body[data-dshome-dark] .hHd-Xa_brand{color:#fff}
1346
- .hHd-Xa_newSession{border:none!important;background:linear-gradient(135deg,var(--ds-brand) 0%,var(--ds-brand-mid) 100%)!important;color:#fff!important;border-radius:999px!important;box-shadow:0 4px 14px color-mix(in srgb,var(--ds-brand) 35%,transparent);transition:transform .15s ease,box-shadow .15s ease,background .15s ease}
1347
- .hHd-Xa_newSession:hover{background:linear-gradient(135deg,color-mix(in srgb,var(--ds-brand) 72%,#fff) 0%,var(--ds-brand) 100%)!important;box-shadow:0 6px 18px color-mix(in srgb,var(--ds-brand) 45%,transparent);transform:translateY(-1px)}
1394
+ { anchor: "[data-composer-card]", css: `
1395
+ /* 输入卡玻璃化。2026-10-03 两次实测后的结论,玻璃只画在**输入卡自己**身上:
1396
+ · 画在 data-composer-stats 的父元素上 —— 那是 display:contents 空壳,盒子 0×0,
1397
+ 等于没画,输入卡一直保持产品自己的纯白;
1398
+ · 画在卡片的父元素上 —— 那是 418×158:左右各多 16px 内边距、下面还多出统计行
1399
+ 所在的 dock,看上去就是「对话框被撑大了」,连倾斜也摊到那一大块上。
1400
+ 卡片自己是 386×98,与产品原样同尺寸。这里**不动产品的圆角、边框、内边距**,
1401
+ 只换底色与模糊,所以尺寸与位置一个像素都不变。
1402
+ 本段在模板字符串里,注释里不能出现反引号。 */
1403
+ ${COMPOSER_PLATE}{background:rgba(255,255,255,.42)!important;-webkit-backdrop-filter:blur(28px) saturate(1.42) brightness(1.02)!important;backdrop-filter:blur(28px) saturate(1.42) brightness(1.02)!important;box-shadow:0 8px 32px rgba(13,30,60,.1),inset 0 1px 1px hsla(0,0%,100%,.8)}
1404
+ body[data-dshome-dark] ${COMPOSER_PLATE}{background:rgba(13,30,60,.55)!important;box-shadow:0 10px 40px rgba(0,0,0,.35),inset 0 1px 1px rgba(255,255,255,.06)}
1348
1405
  ` },
1349
- // Session / project rows and icon buttons.
1350
- { anchor: ".YDXeBa_sessionRow", css: `
1351
- .YDXeBa_sessionRow,.YDXeBa_projectRow{background:rgba(255,255,255,.35)!important;border:1px solid transparent;border-radius:10px!important;margin:1px 0;transition:background .15s ease,border-color .15s ease,transform .15s ease}
1352
- .YDXeBa_sessionRow:hover,.YDXeBa_projectRow:hover{background:rgba(255,255,255,.6)!important;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);transform:translateX(2px)}
1353
- .YDXeBa_sessionRow.YDXeBa_selected{background:linear-gradient(135deg,color-mix(in srgb,var(--ds-brand) 16%,transparent),color-mix(in srgb,var(--ds-brand-mid) 12%,transparent))!important;border-color:color-mix(in srgb,var(--ds-brand) 25%,transparent);color:var(--ds-brand-deep)}
1354
- body[data-dshome-dark] .YDXeBa_sessionRow,body[data-dshome-dark] .YDXeBa_projectRow{background:rgba(21,42,86,.4)!important}
1355
- body[data-dshome-dark] .YDXeBa_sessionRow:hover,body[data-dshome-dark] .YDXeBa_projectRow:hover{background:rgba(30,52,100,.6)!important}
1356
- body[data-dshome-dark] .YDXeBa_sessionRow.YDXeBa_selected{background:linear-gradient(135deg,color-mix(in srgb,var(--ds-brand) 28%,transparent),color-mix(in srgb,var(--ds-brand) 20%,transparent))!important;border-color:color-mix(in srgb,var(--ds-brand) 35%,transparent);color:#eef2ff}
1357
- .YDXeBa_iconButton{transition:background .15s ease,color .15s ease,transform .15s ease,opacity .15s ease}
1358
- .YDXeBa_iconButton:hover{transform:scale(1.1)}
1406
+ // 结构面:产品没给这些面语义钩子,但它们的**组件 key 是稳定身份**
1407
+ // (_newSession/_brand/_scrollBody/_sessionRow/_bubble…),每次构建变的
1408
+ // 只是前面的哈希。所以按「唯一后缀」匹配,并用 :has() 守卫:
1409
+ // 一旦产品把这些 key 也改了,选择器匹配不到任何元素 —— 结果是该面
1410
+ // 没有样式,而不是套到错误的元素上。0.2.0 实测这些后缀各只有 1–2 个
1411
+ // 取值,不存在撞名。
1412
+ { anchor: '[class*="_newSession"], [class*="_scrollBody"], [class*="_composerCard"]', css: `
1413
+ :is(${SIDEBAR}){position:relative;background:transparent!important}
1414
+ :is(${SIDEBAR})::before{content:"";position:absolute;inset:0;z-index:-1;background:rgba(255,255,255,.5);-webkit-backdrop-filter:blur(var(--ds-glass-blur));backdrop-filter:blur(var(--ds-glass-blur));border-right:1px solid rgba(0,0,0,.06);pointer-events:none}
1415
+ body[data-dshome-dark] :is(${SIDEBAR})::before{background:rgba(10,26,58,.48);border-right:1px solid rgba(255,255,255,.08)}
1416
+ :is([class*="_brand"]){color:var(--ds-brand);cursor:pointer}
1417
+ body[data-dshome-dark] :is([class*="_brand"]){color:#fff}
1418
+ :where(div):has(> [class*="_overlayLayer"]){background:transparent!important}
1419
+ :where(div):has(> [class*="_overlayLayer"]) [class*="_sidebarCol"]{border-right:none!important}
1420
+ :is([class*="_sessionRow"],[class*="_projectRow"]){background:rgba(255,255,255,.35)!important;border:1px solid transparent;border-radius:10px!important;margin:1px 0;transition:background .15s ease,border-color .15s ease,transform .15s ease}
1421
+ :is([class*="_sessionRow"],[class*="_projectRow"]):hover{background:rgba(255,255,255,.6)!important;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);transform:translateX(2px)}
1422
+ body[data-dshome-dark] :is([class*="_sessionRow"],[class*="_projectRow"]){background:rgba(21,42,86,.4)!important}
1423
+ body[data-dshome-dark] :is([class*="_sessionRow"],[class*="_projectRow"]):hover{background:rgba(30,52,100,.6)!important}
1424
+ :where(div):has(> [class*="_scrollBody"]){position:relative;background:transparent!important}
1425
+ :is([class*="_scrollBody"]){padding-top:56px}
1426
+ :is([class*="_scrollBody"]) [class*="_viewArea"]{padding:0 18px}
1359
1427
  ` },
1360
- // Chat view: header (incl. locale hover swap), tabs, crumbs, title.
1361
- // Hover pill upgraded to a liquid-glass capsule.
1362
- // 0.1.5-rc.1 draws the header divider as a real `border-bottom` on
1363
- // `.wSkVaW_header` (1.37 used an `::after` pseudo-element), so the
1364
- // floating capsule grew a stray hairline under it. Neutralise the
1365
- // border here and keep the legacy `::after` hide for older builds.
1366
- // The header divider would otherwise be a 0.5px `--dsw-alias-border-l3`
1367
- // line spanning exactly this absolutely positioned bar (24px inset).
1368
- // `titleRow`/`titleCluster` are flattened with `display:contents`, so
1369
- // every direct header child needs an explicit `order`; 0.1.5-rc.1 added
1370
- // `headerCorner` (the right-sidebar expand button) and, left at the
1371
- // default `order:0`, it tied with the tabs and landed at the far left,
1372
- // where the first tab pill overlapped it. It now trails the utilities
1373
- // block, i.e. sits right of the 中/EN locale toggle.
1374
- { anchor: ".wSkVaW_root", css: `
1375
- .wSkVaW_root{position:relative;background:transparent!important}
1376
- .wSkVaW_scrollBody{padding-top:56px}
1377
- .wSkVaW_viewArea{padding:0 18px}
1378
- .wSkVaW_viewArea{animation:dshome-view-enter .45s ease}
1379
- /* Header z-index: the product's column-resize grips (.wSkVaW_widthHandle) are
1380
- 40px-wide full-height strips at z-index 8, while this bar sat at 5 — so the
1381
- cursor anywhere inside the title bar's own box turned into col-resize and the
1382
- bar's controls were unreachable. 9 puts the bar above the grips; the grips
1383
- stay grabbable everywhere the bar is not (above y=8, below y=46). */
1384
- .wSkVaW_header{position:absolute;top:8px;left:24px;right:24px;z-index:9;min-height:36px;padding:4px 12px;display:flex;flex-direction:row;align-items:center;gap:8px;justify-content:flex-start;border:0!important;border-radius:0;background:transparent!important;transition:border-radius .3s ease}
1385
- .wSkVaW_header::before{content:"";position:absolute;inset:0;z-index:-1;background:transparent;border:1px solid transparent;border-radius:inherit;-webkit-backdrop-filter:none;backdrop-filter:none;transition:background .3s ease,border-color .3s ease,box-shadow .3s ease}
1386
- .wSkVaW_header:hover{border-radius:999px}
1387
- .wSkVaW_titleRow{display:contents}
1388
- .wSkVaW_titleCluster{display:contents}
1389
- .wSkVaW_crumbs{order:2;flex:none;min-width:0}
1390
- .wSkVaW_tabs{order:0;flex:none;align-items:center;display:none;gap:8px;margin:0;padding:0}
1391
- .wSkVaW_headerActions{order:1;flex:none;align-items:center;display:flex;gap:8px}
1392
- .wSkVaW_headerUtilities{order:3;flex:none;align-items:center;display:flex;gap:8px;margin-left:auto}
1393
- .wSkVaW_headerCorner{order:4;flex:none;align-items:center;display:flex;margin-left:0;margin-right:0}
1394
- .wSkVaW_header.dshome-swap .wSkVaW_tabs{display:flex;animation:dshome-tab-in .22s ease}
1395
- .wSkVaW_root:has(.fV0t5q_root) .wSkVaW_tabs{display:flex!important}
1396
- .wSkVaW_header:hover::before{background:rgba(255,255,255,.45);border-color:rgba(0,0,0,.06);-webkit-backdrop-filter:blur(20px);backdrop-filter:blur(20px);box-shadow:0 4px 16px rgba(0,0,0,.05)}
1397
- body[data-dshome-dark] .wSkVaW_header:hover::before{background:rgba(13,30,60,.45);border-color:rgba(255,255,255,.1);box-shadow:0 4px 20px rgba(0,0,0,.3)}
1398
- .wSkVaW_header:after{display:none!important}
1399
- .SVAs4q_label{background:color-mix(in srgb,var(--ds-brand) 10%,transparent)!important;border:1px solid color-mix(in srgb,var(--ds-brand) 22%,transparent);border-radius:999px!important;padding:0 10px!important;height:28px;line-height:26px;color:var(--ds-brand)!important}
1400
- body[data-dshome-dark] .SVAs4q_label{color:var(--ds-brand-light)!important}
1401
- .wSkVaW_tab{border:1px solid color-mix(in srgb,var(--ds-brand) 25%,transparent);background:color-mix(in srgb,var(--ds-brand) 8%,transparent);border-radius:999px;corner-shape:round;padding:5px 16px;color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:16px;font-weight:500;transition:color .22s ease,background .22s ease,border-color .22s ease,box-shadow .24s ease,transform .24s cubic-bezier(.2,.8,.2,1)}
1402
- .wSkVaW_tab:hover{color:var(--dsw-alias-label-primary);background:color-mix(in srgb,var(--ds-brand) 16%,transparent);border-color:color-mix(in srgb,var(--ds-brand) 42%,transparent);box-shadow:0 4px 12px color-mix(in srgb,var(--ds-brand) 22%,transparent);transform:translateY(-1px)}
1403
- .wSkVaW_tab:active{transform:translateY(0) scale(.96)}
1404
- .wSkVaW_tab::after{display:none!important}
1405
- .wSkVaW_tabActive{background:linear-gradient(135deg,var(--ds-brand),var(--ds-brand-mid));color:#fff;border-color:transparent;box-shadow:0 3px 10px color-mix(in srgb,var(--ds-brand) 26%,transparent);animation:dshome-tab-pop .34s cubic-bezier(.2,.8,.2,1)}
1406
- .wSkVaW_tabActive:hover{color:#fff}
1407
- body[data-dshome-dark] .wSkVaW_tab{border-color:color-mix(in srgb,var(--ds-brand) 32%,transparent);background:color-mix(in srgb,var(--ds-brand) 14%,transparent)}
1408
- body[data-dshome-dark] .wSkVaW_tab:hover{background:color-mix(in srgb,var(--ds-brand) 24%,transparent);box-shadow:0 4px 14px color-mix(in srgb,var(--ds-brand) 34%,transparent)}
1409
- body[data-dshome-dark] .wSkVaW_tabActive{box-shadow:0 3px 12px color-mix(in srgb,var(--ds-brand) 40%,transparent)}
1428
+ // 会话标题栏:风格、玻璃、按钮、悬停一律恢复成原来的样子(需求第 9 条 + "另有"),
1429
+ // 而**切换本身完全归官方**(第 7 条)—— 我们只画,不接管点击,也不碰 active。
1430
+ //
1431
+ // 2.0.90 里这一整套是写死 `.wSkVaW_*` 的,语义化时整块丢了。丢掉的不只是样子:
1432
+ // 其中 z-index 那条修的是**真 bug** —— 产品的列宽抓手是 40px 宽、满高、
1433
+ // z-index 8 的条,标题栏默认压在它下面,于是光标落在标题栏自己的盒子里也会
1434
+ // 变成 col-resize,栏里的控件点不到。9 把栏提到抓手之上,抓手在栏之外照旧可抓。
1435
+ //
1436
+ // 认法(全部按结构,不写哈希):
1437
+ // · 标题栏 = 直接子元素里有 _headerLeading、且内部有 _headerActions 的那一层。
1438
+ // 侧栏那块 _sectionHeader 有 headerActions 但没有 headerLeading,不会误伤。
1439
+ // · 标签页 = 同一层里的 _tabs;单个标签 = _tab / _tabActive;路径 = _crumb。
1440
+ // 实测结构(2026-10-06,0.2.0-rc.2):
1441
+ // _header (56,0,744,76, position:static)
1442
+ // ├ _headerLeading > div > { _titleRow, _tabs } ← 标签页在第二行 y=50
1443
+ // └ div { _headerActions, _crumbs, _moreButton, _label, _split }
1444
+ // 原设计是 36px 单行胶囊,靠 titleRow{display:contents} 把两层压平成一行;
1445
+ // 这里照做,否则胶囊会变成 76px 高的两行块。
1446
+ { anchor: '[class*="_headerLeading"]', css: `
1447
+ :has(> [class*="_headerLeading"]):has([class*="_headerActions"]){position:absolute;top:8px;left:24px;right:24px;z-index:9;min-height:36px;padding:4px 12px;display:flex;flex-direction:row;align-items:center;gap:8px;justify-content:flex-start;border:0!important;border-radius:0;background:transparent!important;transition:border-radius .3s ease}
1448
+ :has(> [class*="_headerLeading"]):has([class*="_headerActions"])::before{content:"";position:absolute;inset:0;z-index:-1;background:transparent;border:1px solid transparent;border-radius:inherit;-webkit-backdrop-filter:none;backdrop-filter:none;transition:background .3s ease,border-color .3s ease,box-shadow .3s ease}
1449
+ :has(> [class*="_headerLeading"]):has([class*="_headerActions"]):hover{border-radius:999px}
1450
+ :has(> [class*="_headerLeading"]):has([class*="_headerActions"]):hover::before{background:rgba(255,255,255,.45);border-color:rgba(0,0,0,.06);-webkit-backdrop-filter:blur(20px);backdrop-filter:blur(20px);box-shadow:0 4px 16px rgba(0,0,0,.05)}
1451
+ body[data-dshome-dark] :has(> [class*="_headerLeading"]):has([class*="_headerActions"]):hover::before{background:rgba(13,30,60,.45);border-color:rgba(255,255,255,.1);box-shadow:0 4px 20px rgba(0,0,0,.3)}
1452
+ :has(> [class*="_headerLeading"]):has([class*="_headerActions"])::after{display:none!important}
1453
+ /* 压平:两层包装 + titleRow 一律 display:contents,让所有控件成为标题栏这一条 flex 的直接子项,
1454
+ 于是 order 才排得动。少了这一步,标签页会留在第二行、胶囊变成两行高的块。 */
1455
+ :has(> [class*="_headerLeading"]):has([class*="_headerActions"]) > :is([class*="_headerLeading"]){display:contents}
1456
+ :has(> [class*="_headerLeading"]):has([class*="_headerActions"]) > :is([class*="_headerLeading"]) > div{display:contents}
1457
+ :has(> [class*="_headerLeading"]):has([class*="_headerActions"]) :is([class*="_titleRow"],[class*="_titleCluster"]){display:contents}
1458
+ :has(> [class*="_headerLeading"]):has([class*="_headerActions"]) :is([class*="_crumb"]){order:2;flex:none;min-width:0;border-radius:999px}
1459
+ :has(> [class*="_headerLeading"]):has([class*="_headerActions"]) :is([class*="_tabs"]){order:0;flex:none;align-items:center;display:none;gap:8px;margin:0;padding:0}
1460
+ :has(> [class*="_headerLeading"]):has([class*="_headerActions"]) :is([class*="_headerActions"]){order:1;flex:none;align-items:center;display:flex;gap:8px}
1461
+ :has(> [class*="_headerLeading"]):has([class*="_headerActions"]) :is([class*="_headerUtilities"]){order:3;flex:none;align-items:center;display:flex;gap:8px;margin-left:auto}
1462
+ :has(> [class*="_headerLeading"]):has([class*="_headerActions"]) :is([class*="_headerCorner"]){order:4;flex:none;align-items:center;display:flex}
1463
+ /* 标签页"悬停标题栏才出现":原样保留,但驱动它的是纯 CSS :hover,不再是那个 JS 类
1464
+ (dshome-swap 随 §3.7 的接管一起删掉了)。active 由产品给,我们只画。 */
1465
+ :has(> [class*="_headerLeading"]):has([class*="_headerActions"]):hover :is([class*="_tabs"]){display:flex;animation:dshome-tab-in .22s ease}
1466
+ /* 标签页胶囊 —— 只在会话标题栏内生效。\n 曾经写成裸的 [class*="_tab"],结果 _tableScroll(含子串 _tab)、以及任何 _tab* 面板\n 都被套上了 999px 胶囊与半透明品牌底 —— 表格容器变蓝、文件预览面板又圆又透,都是它。 */
1467
+ :has(> [class*="_headerLeading"]):has([class*="_headerActions"]) :is([class*="_tab"]):not([class*="_tabs"]){border:1px solid color-mix(in srgb,var(--ds-brand) 25%,transparent);background:color-mix(in srgb,var(--ds-brand) 8%,transparent);border-radius:999px;corner-shape:round;padding:5px 16px;color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:16px;font-weight:500;transition:color .22s ease,background .22s ease,border-color .22s ease,box-shadow .24s ease,transform .24s cubic-bezier(.2,.8,.2,1)}
1468
+ :has(> [class*="_headerLeading"]):has([class*="_headerActions"]) :is([class*="_tab"]):not([class*="_tabs"]):hover{color:var(--dsw-alias-label-primary);background:color-mix(in srgb,var(--ds-brand) 16%,transparent);border-color:color-mix(in srgb,var(--ds-brand) 42%,transparent);box-shadow:0 4px 12px color-mix(in srgb,var(--ds-brand) 22%,transparent);transform:translateY(-1px)}
1469
+ :has(> [class*="_headerLeading"]):has([class*="_headerActions"]) :is([class*="_tab"]):not([class*="_tabs"]):active{transform:translateY(0) scale(.96)}
1470
+ :has(> [class*="_headerLeading"]):has([class*="_headerActions"]) :is([class*="_tab"]):not([class*="_tabs"])::after{display:none!important}
1471
+ :has(> [class*="_headerLeading"]):has([class*="_headerActions"]) :is([class*="_tabActive"]){background:linear-gradient(135deg,var(--ds-brand),var(--ds-brand-mid))!important;color:#fff!important;border-color:transparent!important;box-shadow:0 3px 10px color-mix(in srgb,var(--ds-brand) 26%,transparent);animation:dshome-tab-pop .34s cubic-bezier(.2,.8,.2,1)}
1472
+ :has(> [class*="_headerLeading"]):has([class*="_headerActions"]) :is([class*="_tabActive"]):hover{color:#fff!important}
1473
+ body[data-dshome-dark] :has(> [class*="_headerLeading"]):has([class*="_headerActions"]) :is([class*="_tab"]):not([class*="_tabs"]){border-color:color-mix(in srgb,var(--ds-brand) 32%,transparent);background:color-mix(in srgb,var(--ds-brand) 14%,transparent)}
1474
+ body[data-dshome-dark] :has(> [class*="_headerLeading"]):has([class*="_headerActions"]) :is([class*="_tab"]):not([class*="_tabs"]):hover{background:color-mix(in srgb,var(--ds-brand) 24%,transparent);box-shadow:0 4px 14px color-mix(in srgb,var(--ds-brand) 34%,transparent)}
1475
+ body[data-dshome-dark] :has(> [class*="_headerLeading"]):has([class*="_headerActions"]) :is([class*="_tabActive"]){box-shadow:0 3px 12px color-mix(in srgb,var(--ds-brand) 40%,transparent)}
1410
1476
  @keyframes dshome-tab-pop{0%{transform:translateY(-1px) scale(.88)}60%{transform:translateY(-1px) scale(1.05)}to{transform:none}}
1411
- .wSkVaW_root:has(.fV0t5q_root) .wSkVaW_header:hover{border-radius:0}
1412
- .wSkVaW_root:has(.fV0t5q_root) .wSkVaW_header:hover::before{background:transparent;border-color:transparent;-webkit-backdrop-filter:none;backdrop-filter:none;box-shadow:none}
1413
- .wSkVaW_composerSeat{background:transparent!important}
1414
- .wSkVaW_header:hover .dshome-locale .dshome-short{max-width:0;opacity:0;transform:translateX(6px)}
1415
- .wSkVaW_header:hover .dshome-locale .dshome-full{max-width:3em;opacity:1;transform:none}
1416
- /* Header chips: the product's global corner-shape:superellipse(1.5) squares off
1417
- even pill radii, so every control in the bar is restored to a true round end
1418
- (and the session-title crumb, 12px on a 28px box, becomes a real pill). */
1419
- .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}
1420
- .wSkVaW_header .wSkVaW_crumb{border-radius:999px}
1421
- /* Open-in-app split button: hover lift with a brand tint, and the chevron turns
1422
- over while its menu is open. */
1423
- .wSkVaW_header .CAgGvG_split{transition:transform .28s cubic-bezier(.2,.8,.2,1),box-shadow .28s ease,border-color .22s ease,background .22s ease}
1424
- .wSkVaW_header .CAgGvG_split:hover{transform:translateY(-1px);border-color:color-mix(in srgb,var(--ds-brand) 40%,transparent);background:color-mix(in srgb,var(--ds-brand) 8%,transparent);box-shadow:0 4px 14px color-mix(in srgb,var(--ds-brand) 22%,transparent)}
1425
- .wSkVaW_header .CAgGvG_split:active{transform:translateY(0) scale(.97)}
1426
- .wSkVaW_header .CAgGvG_main,.wSkVaW_header .CAgGvG_chevron{transition:background .22s ease,color .22s ease}
1427
- .wSkVaW_header .CAgGvG_split:hover .CAgGvG_main,.wSkVaW_header .CAgGvG_split:hover .CAgGvG_chevron{background:color-mix(in srgb,var(--ds-brand) 12%,transparent);color:var(--ds-brand-deep)}
1428
- .wSkVaW_header .CAgGvG_chevron svg{transition:transform .3s cubic-bezier(.2,.8,.2,1)}
1429
- .wSkVaW_header .CAgGvG_chevron[aria-expanded=true] svg{transform:rotate(180deg)}
1430
- 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)}
1431
- 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}
1432
- ` },
1433
- // Session-log button (hover expand). The product renamed it to
1434
- // `nL4_yW_moreButton` on the current build (B2, 2026-09-16), so anchor and
1435
- // rules both key on the suffix, the way `[class*="_fade" i]` already does.
1436
- { anchor: '[class*="_moreButton"]', css: `
1477
+ /* 路径标签(会话标题)胶囊 + 品牌标签胶囊 */
1478
+ :has(> [class*="_headerLeading"]):has([class*="_headerActions"]) :is([class*="_crumb"]){border-radius:999px}
1479
+ :has(> [class*="_headerLeading"]):has([class*="_headerActions"]) :is([class*="_label"]){background:color-mix(in srgb,var(--ds-brand) 10%,transparent)!important;border:1px solid color-mix(in srgb,var(--ds-brand) 22%,transparent);border-radius:999px!important;padding:0 10px!important;height:28px;line-height:26px;color:var(--ds-brand)!important}
1480
+ body[data-dshome-dark] :has(> [class*="_headerLeading"]):has([class*="_headerActions"]) :is([class*="_label"]){color:var(--ds-brand-light)!important}
1481
+ /* 打开方式的分裂按钮:悬停抬升 + 品牌着色,菜单打开时箭头翻过来 */
1482
+ :has(> [class*="_headerLeading"]):has([class*="_headerActions"]) :is([class*="_split"]){transition:transform .28s cubic-bezier(.2,.8,.2,1),box-shadow .28s ease,border-color .22s ease,background .22s ease}
1483
+ :has(> [class*="_headerLeading"]):has([class*="_headerActions"]) :is([class*="_split"]):hover{transform:translateY(-1px);border-color:color-mix(in srgb,var(--ds-brand) 40%,transparent);background:color-mix(in srgb,var(--ds-brand) 8%,transparent);box-shadow:0 4px 14px color-mix(in srgb,var(--ds-brand) 22%,transparent)}
1484
+ :has(> [class*="_headerLeading"]):has([class*="_headerActions"]) :is([class*="_split"]):active{transform:translateY(0) scale(.97)}
1485
+ :has(> [class*="_headerLeading"]):has([class*="_headerActions"]) :is([class*="_chevron"]) svg{transition:transform .3s cubic-bezier(.2,.8,.2,1)}
1486
+ :has(> [class*="_headerLeading"]):has([class*="_headerActions"]) :is([class*="_chevron"])[aria-expanded=true] svg{transform:rotate(180deg)}
1487
+ /* 语言开关:悬停标题栏时 中 → 中文 展开(原样的行为) */
1488
+ :has(> [class*="_headerLeading"]):has([class*="_headerActions"]):hover .dshome-locale .dshome-short{max-width:0;opacity:0;transform:translateX(6px)}
1489
+ :has(> [class*="_headerLeading"]):has([class*="_headerActions"]):hover .dshome-locale .dshome-full{max-width:3em;opacity:1;transform:none}
1490
+ /* 真圆配对:产品的全局 superellipse(1.5) 会把胶囊半径压成方圆 */
1491
+ :has(> [class*="_headerLeading"]):has([class*="_headerActions"]) :is([class*="_label"],[class*="_seat"],[class*="_split"],[class*="_crumb"],[class*="_moreButton"],[data-sidebar-right-expand]),.dshome-locale,[class*="_tabs"]{corner-shape:round}
1437
1492
  :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}
1438
1493
  :is([class*="_moreButton"]) span{max-width:0;opacity:0;overflow:hidden;transition:max-width .2s ease,opacity .2s ease}
1439
- .wSkVaW_header:hover :is([class*="_moreButton"]){width:auto;padding:0 12px!important;border-radius:18px!important}
1440
- .wSkVaW_header:hover :is([class*="_moreButton"]) span{max-width:90px;opacity:1}
1494
+ /* 原来这里写的是 :has(_moreButton) —— 那等于"只要存在就展开",形态与原来不符。
1495
+ 原设计是**悬停标题栏才展开**成宽胶囊。 */
1496
+ :has(> [class*="_headerLeading"]):has([class*="_headerActions"]):hover :is([class*="_moreButton"]){width:auto;padding:0 12px!important;border-radius:18px!important}
1497
+ :has(> [class*="_headerLeading"]):has([class*="_headerActions"]):hover :is([class*="_moreButton"]) span{max-width:90px;opacity:1}
1441
1498
  body[data-dshome-dark] :is([class*="_moreButton"]){color:var(--ds-brand-light)!important}
1499
+ :is([class*="_newSession"]){border:none!important;background:linear-gradient(135deg,var(--ds-brand) 0%,var(--ds-brand-mid) 100%)!important;color:#fff!important;border-radius:999px!important;box-shadow:0 4px 14px color-mix(in srgb,var(--ds-brand) 35%,transparent);transition:transform .15s ease,box-shadow .15s ease,background .15s ease}
1500
+ :is([class*="_newSession"]):hover{background:linear-gradient(135deg,color-mix(in srgb,var(--ds-brand) 72%,#fff) 0%,var(--ds-brand) 100%)!important;box-shadow:0 6px 18px color-mix(in srgb,var(--ds-brand) 45%,transparent);transform:translateY(-1px)}
1442
1501
  ` },
1443
- // Glass cards with spinning gradient border — upgraded to liquid glass:
1444
- // specular top edge, saturated blur, brighter inner rim.
1445
- { anchor: ".uV2eYG_card", css: `
1446
- .uV2eYG_card{position:relative;background:rgba(255,255,255,.42)!important;border:1.5px solid rgba(255,255,255,.55)!important;box-shadow:0 8px 32px rgba(13,30,60,.1),inset 0 1px 1px hsla(0,0%,100%,.8);border-radius:22px!important}
1447
- body[data-dshome-dark] .uV2eYG_card{background:rgba(13,30,60,.55)!important;border:1.5px solid rgba(255,255,255,.16)!important;box-shadow:0 10px 40px rgba(0,0,0,.35),inset 0 1px 1px rgba(255,255,255,.06)}
1448
- .uV2eYG_card::after{content:"";position:absolute;inset:0;z-index:-1;border-radius:22px;-webkit-backdrop-filter:blur(28px);backdrop-filter:blur(28px);pointer-events:none}
1449
- .uV2eYG_card::before{content:"";position:absolute;inset:-1px;border-radius:inherit;padding:1px;background:conic-gradient(from var(--dsh-border-angle),color-mix(in srgb,var(--ds-brand) 0%,transparent),color-mix(in srgb,var(--ds-brand) 45%,transparent),color-mix(in srgb,var(--ds-brand-light) 20%,transparent),color-mix(in srgb,var(--ds-brand) 45%,transparent),color-mix(in srgb,var(--ds-brand) 0%,transparent));-webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);-webkit-mask-composite:xor;mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);mask-composite:exclude;pointer-events:none;animation:dsh-border-spin 6s linear infinite;z-index:0}
1450
- body[data-dshome-dark] .uV2eYG_card::before{background:conic-gradient(from var(--dsh-border-angle),rgba(255,255,255,0),color-mix(in srgb,var(--ds-brand) 55%,transparent),color-mix(in srgb,var(--ds-brand-light) 25%,transparent),color-mix(in srgb,var(--ds-brand) 55%,transparent),rgba(255,255,255,0))}
1451
- .uV2eYG_card>*{position:relative;z-index:1}
1452
- .uV2eYG_primary{background:linear-gradient(135deg,var(--ds-brand) 0%,var(--ds-brand-mid) 100%)!important;border:none!important;color:#fff!important;box-shadow:0 4px 14px color-mix(in srgb,var(--ds-brand) 35%,transparent);transition:box-shadow .15s ease,transform .15s ease}
1453
- .uV2eYG_primary:hover{box-shadow:0 6px 18px color-mix(in srgb,var(--ds-brand) 45%,transparent)}
1454
- /* 2.0.80: the hover no longer lifts the button by 1px. That translateY(-1px)
1455
- was ours (it came with the primary-button styling, not from the product), and
1456
- it was the last geometry change this theme made to the send/stop button under
1457
- the cursor -- a 1px surface motion is enough to hand the hover to a neighbour
1458
- or to the column's width handle, which is the same class of loop the composer
1459
- tilt caused (2.0.79). Hover now changes colour and shadow only. The product's
1460
- own hover on this button changes nothing but its background, so nothing else
1461
- moves it. */
1462
- ` },
1463
- // The conversation scroller must never scroll sideways (2.0.78).
1464
- //
1465
- // Measured on the live page while the user hovered the composer's
1466
- // send button and the small circle: the product's own hover affordance
1467
- // on a message lays its row out wider than the column (scrollWidth 917
1468
- // vs clientWidth 891, overflowing element `span._bubble_...`), which
1469
- // summons an 8 px horizontal scrollbar. That scrollbar reserves 8 px of
1470
- // content height, so the scroll body's content box goes 842 -> 834 and
1471
- // the composer pinned below it shifts up 8 px (card top 699 -> 691);
1472
- // the pointer then sits over a different element, the affordance
1473
- // closes, the scrollbar leaves, the composer drops back -- a ~5 Hz
1474
- // bistable loop reported as "the two buttons flicker and a slider
1475
- // flashes at the bottom". The overflow is upstream's, but the theme can
1476
- // decline to scroll sideways at all: `clip` reserves no scrollbar and
1477
- // keeps each code block's own inner scroller working. The trade is that
1478
- // whatever sticks out past the column edge is clipped instead of
1479
- // scrollable, which in a chat column is the lesser evil -- and this is
1480
- // the guarded-block pattern, so it disappears with the anchor.
1481
- //
1482
- // 2.0.82 widens the net to the column's other scrollers. The mechanism
1483
- // needs only ONE `overflow-y: auto` container (the other axis computes
1484
- // to `auto`, so it can grow a horizontal scrollbar) whose child grows on
1485
- // hover; the first report named the message list, but the same 8px step
1486
- // under the send button can come from the composer's own seat or the
1487
- // transcript's view area, which that first fix did not cover. Popups
1488
- // portalled to <body> are unaffected; anything rendered inline inside
1489
- // these containers is clipped, exactly as it already was for the list.
1490
- { anchor: ".wSkVaW_scrollBody", css: `
1491
- [class*="scrollBody"],[class*="composerSeat"],[class*="viewArea"]{overflow-x:clip}
1492
- ` },
1493
- // Empty-state headline + preview badge. The product renamed
1494
- // `pXSMma_headlineText` to `pXSMma_headline` (B2, 2026-09-16), which
1495
- // silently killed this block and left the headline at 26px/500 with no
1496
- // gradient. Keyed on the suffix now, along with the two brand marks that
1497
- // live in the same family.
1498
- { anchor: '[class*="_headline"]', css: `
1499
- :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}
1500
- 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}
1501
- :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}
1502
- :is([class*="_fish"]){color:var(--ds-brand)!important}
1503
- ` },
1504
- // Chat bubbles — liquid glass panes.
1505
- { anchor: ".wSkVaW_scrollBody", css: `
1506
- :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)}
1507
- 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)}
1508
- ` },
1509
- // The line under the composer: round/step/token stats ([data-dsh-stats]) and
1510
- // the whole input bar ([data-dsh-inputbar]) -- both product markup that has
1511
- // never been renamed.
1512
- //
1513
- // Two things happen here.
1514
- //
1515
- // 1. The DOCKED BOTTOM BAR, borrowed from dsh-theme-mineradio's fused state
1516
- // (MIT, @dhicoc): once the stats line exists, the WHOLE input bar becomes
1517
- // one glass slab -- a single border, radius, fill and shadow on the
1518
- // wrapper, with the backdrop blur on its ::before -- and the card and the
1519
- // stats turn transparent inside it, leaving one hairline at the seam. The
1520
- // two surfaces can then never drift apart tonally, and the composer reads
1521
- // as one designed bar instead of a card with loose text under it.
1522
- // `:has()` is what makes this safe to attempt at all: when the product
1523
- // does not render a stats line, none of it applies and the card keeps its
1524
- // own glass.
1525
- // Two details are load-bearing and inherited from mineradio's notes:
1526
- // z-index (not isolation) on the wrapper -- isolation is also a backdrop
1527
- // root and would clip popup frost to the bar -- and `backdrop-filter:
1528
- // none` on the inner surfaces, whose own blur would otherwise double up
1529
- // with the slab's.
1502
+ // 表格的滚动容器:产品给它的是 999px 胶囊 + `overflow:auto` —— 胶囊半径在
1503
+ // 一个 824×226 的盒子上每边要吃约 113px,而 overflow:auto 就在那条弧线上裁,
1504
+ // 于是宽表格的左右两端被**自己的容器**裁没(不是被浮层盖住)。
1505
+ // 2026-10-06 用户实测:`<table>` 本身 radius=0px / overflow=visible,
1506
+ // 而它的父层 `_tableScroll_* _tableFill_*` 是 radius=999px / overflow=auto。
1530
1507
  //
1531
- // 2. The LENS touch the user asked for ("对对话窗下方文字进行放大(模拟液体放大)"):
1532
- // a real lens magnifies what is behind the glass, which a backdrop filter
1533
- // cannot do, so the text is scaled a touch instead -- and a touch further
1534
- // while the pane leans (html[data-dshome-leaning], written by
1535
- // startComposerTilt). `transform-origin: left center` keeps a
1536
- // left-aligned line growing inwards instead of spilling past the slab's
1537
- // right edge.
1538
- { anchor: "[data-dsh-stats]", css: `
1539
- [data-dsh-inputbar]:has([data-dsh-stats]){position:relative;z-index:8;padding:0;border:1.5px solid rgba(255,255,255,.55);border-radius:24px;background:rgba(255,255,255,.42);box-shadow:0 8px 32px rgba(13,30,60,.1),inset 0 1px 1px hsla(0,0%,100%,.8)}
1540
- [data-dsh-inputbar]:has([data-dsh-stats])::before{content:"";position:absolute;inset:0;border-radius:inherit;pointer-events:none;z-index:-1;-webkit-backdrop-filter:blur(28px) saturate(1.42) brightness(1.02);backdrop-filter:blur(28px) saturate(1.42) brightness(1.02)}
1541
- [data-dsh-inputbar]:has([data-dsh-stats]) [data-composer-card]{border:none!important;border-radius:0!important;background:transparent!important;background-image:none!important;box-shadow:none!important;-webkit-backdrop-filter:none!important;backdrop-filter:none!important}
1542
- [data-dsh-inputbar]:has([data-dsh-stats]) [data-composer-card]::before,[data-dsh-inputbar]:has([data-dsh-stats]) [data-composer-card]::after{display:none!important}
1543
- [data-dsh-inputbar]:has([data-dsh-stats]) [data-dsh-stats]{display:block;width:100%;max-width:none;box-sizing:border-box;margin:auto 0 0;min-height:24px;padding:2px 16px;border:none;border-top:1px solid rgba(0,0,0,.06);border-radius:0;background:transparent;box-shadow:none;-webkit-backdrop-filter:none;backdrop-filter:none;transform-origin:left center;transition:transform .24s cubic-bezier(.2,.8,.2,1)}
1544
- [data-dsh-stats]{transform:scale(1.04)}
1545
- html[data-dshome-leaning] [data-dsh-inputbar]:has([data-dsh-stats]) [data-dsh-stats]{transform:scale(1.08)}
1546
- body[data-dshome-dark] [data-dsh-inputbar]:has([data-dsh-stats]){border-color:rgba(255,255,255,.16);background:rgba(13,30,60,.55);box-shadow:0 10px 40px rgba(0,0,0,.35),inset 0 1px 1px rgba(255,255,255,.06)}
1547
- body[data-dshome-dark] [data-dsh-inputbar]:has([data-dsh-stats]) [data-dsh-stats]{border-top-color:rgba(255,255,255,.1)}
1548
- @media (prefers-reduced-motion:reduce){[data-dsh-inputbar]:has([data-dsh-stats]) [data-dsh-stats]{transition:none}}
1508
+ // 改成 8px 小圆角:边缘不再被吃,形态仍与整页的圆角语言一致。
1509
+ // 这一条是**我们主动覆盖产品的设计**,不在《逐项决定》11 条之内,由用户点名选择;
1510
+ // 记在这里是因为它属于"产品换做法时不会跟着变"的那一类,将来要能一眼认出。
1511
+ // 只为矩形感更强可以把 8px 调成 0;圆角与裁切是同一个属性的两面,
1512
+ // 想既留胶囊又不裁在 CSS 里做不到(除非给内容加够内边距)。
1513
+ { anchor: '[class*="_tableScroll"], [class*="_scrollBody"]', css: `
1514
+ :is([class*="_tableScroll"]){border-radius:8px!important;color:inherit!important}
1515
+ /* 表格文字:产品把单元格设成了中灰 rgb(129,133,140),而正文是 rgb(21,36,67)。
1516
+ 用户要求"表格应该一直是黑色",所以单元格继承正文色。用 inherit 而不是某个
1517
+ 具体色值或 token:--dsw-alias-label-primary 在 0.2.0 的 :root 上读出来是空的
1518
+ (实测),写 var() 会整条失效。 */
1519
+ :is([class*="_tableScroll"]) :is(td,th,[class*="_tableCell"]){color:inherit!important}
1520
+ ` }, // 让流动背景透出来。产品给每一块整屏面板都铺了不透明的
1521
+ // `background:var(--dsw-alias-bg-base)`,而我们的底纹(body::before,z-index -2)
1522
+ // 和流体画布(z-index -1)都在 body 背景之下、内容之上——所以只要上面任何一层
1523
+ // 面板是不透明的,流体就等于没画。2026-10-03 的窗口截图量到会话区恒为 #f6f7fb
1524
+ // (= 底纹末段的颜色),而主题正常工作时同一个点是 #a7dfc9(明显的流场色)。
1525
+ // 2.0.90 里这条是写死哈希的(.pI_x6G_frame/_sidebarCol/_detailsCol/_centerCol),
1526
+ // 语义化时整块丢了。这里按结构认:
1527
+ // · 应用外框 = 同时把 centerCol 与 sidebarCol 当**直接子元素**的那一层(产品的
1528
+ // 外框是 display:grid,分栏就是它的网格子项);
1529
+ // · 会话根 = div[class*="_root"][data-phase"](产品 JSX 里它是
1530
+ // `className: ConversationRoot.root, "data-phase": phase`,实测这个组合唯一)。
1531
+ // 面板本身不动布局,只把底色让出来:文字仍在内容层,玻璃由各自的面提供。
1532
+ { anchor: '[class*="_centerCol"], [class*="_scrollBody"]', css: `
1533
+ :has(> [class*="_centerCol"]):has(> [class*="_sidebarCol"]){background:transparent!important}
1534
+ [class*="_centerCol"],[class*="_rightbarCol"],[class*="_sidebarCol"]{background:transparent!important}
1535
+ div[class*="_root"][data-phase]{background:transparent!important}
1536
+ [class*="_composerSeat"]{background:transparent!important}
1537
+ /* 产品的大浮层(设置、以及其它 role=dialog 的面板):它自己的底色是 85% 白、而且
1538
+ **完全没有模糊** —— 实测 wCInkW_panel: bg=rgba(255,255,255,.85)、backdrop=none,
1539
+ 我们一条规则都没命中它。半透明而没有模糊兜着,背后 15% 的正文就直接透上来、字压字,
1540
+ 在流体背景上尤其不可读。用户要求"要么做成液态玻璃、要么全不透明",选玻璃:
1541
+ 保留半透明、补上模糊链,背后的内容变成一团霜,面板自己读得清,形态也与主题
1542
+ 其它玻璃面一致。
1543
+ 注意它是挂在 body 上的浮层(实测距 body 两层、不在会话栏内),所以"给会话栏补一层底"
1544
+ 那条思路盖不到它 —— 只能直挂 role=dialog。这个选择器是 ARIA 角色,产品改哈希不受影响。
1545
+ 这条规则放在这个常驻补丁块里(而不是单开一个以 role=dialog 为锚点的块),是因为
1546
+ 锚点在浮层没打开时不存在,注入会晚一拍、开面板时会闪一下。 */
1547
+ [role="dialog"]{background:rgba(255,255,255,.68)!important;-webkit-backdrop-filter:blur(30px) saturate(1.6) brightness(1.03);backdrop-filter:blur(30px) saturate(1.6) brightness(1.03);border:1px solid rgba(255,255,255,.5);box-shadow:0 24px 70px rgba(13,30,60,.22),inset 0 1px 0 rgba(255,255,255,.75)}
1548
+ body[data-dshome-dark] [role="dialog"]{background:rgba(13,30,60,.74)!important;border-color:rgba(255,255,255,.12);box-shadow:0 24px 70px rgba(0,0,0,.5),inset 0 1px 0 rgba(255,255,255,.08)}
1549
1549
  ` },
1550
- // Trajectory view bar.
1551
- { anchor: ".fV0t5q_root", css: `
1552
- .fV0t5q_root{background:rgba(255,255,255,.45)!important;border-bottom:1px solid rgba(0,0,0,.06)!important}
1553
- body[data-dshome-dark] .fV0t5q_root{background:rgba(13,30,60,.45)!important;border-bottom-color:rgba(255,255,255,.08)!important}
1554
- ` },
1555
- // Timeline root/plot/span.
1556
- { anchor: "._1p9O6q_root", css: `
1557
- ._1p9O6q_root{background:rgba(255,255,255,.3)!important;border-bottom:1px solid rgba(0,0,0,.05)!important}
1558
- ._1p9O6q_plot{background:transparent!important}
1559
- body[data-dshome-dark] ._1p9O6q_root{background:rgba(13,30,60,.3)!important}
1560
- ._1p9O6q_span[data-timeline-span=assistant]{background:var(--ds-brand)!important}
1561
- ._1p9O6q_span[data-timeline-span=user]{background:var(--ds-brand-light)!important}
1562
- ` },
1563
- // Progress panel entrance.
1564
- { anchor: ".Nqubda_panel", css: `
1565
- .Nqubda_panel{animation:dshome-pop-in .25s cubic-bezier(.2,.8,.2,1)}
1566
- ` },
1567
- // Settings overlay / modal — near-opaque glass with specular edge.
1568
- { anchor: ".VOzbGW_panel", css: `
1569
- .VOzbGW_mask{animation:dshome-fade-in .2s ease}
1570
- .VOzbGW_panel{background:rgba(255,255,255,.97)!important;animation:dshome-modal-in .4s cubic-bezier(.2,.8,.2,1)!important}
1571
- body[data-dshome-dark] .VOzbGW_panel{background:rgba(13,30,60,.96)!important}
1572
- .VOzbGW_overlay{position:fixed!important;top:0!important;left:0!important;width:100vw!important;height:100vh!important;inset:0!important}
1573
- ` }
1574
1550
  ];
1575
1551
 
1576
1552
  // LIQUID GLASS — the "wet glass" recipe, imitated from
@@ -1595,8 +1571,7 @@ body[data-dshome-dark] .VOzbGW_panel{background:rgba(13,30,60,.96)!important}
1595
1571
  // `data-dshome-dispersion` attribute that startGlassDispersion() stamps on
1596
1572
  // <html> only where it really mounted — every other browser silently keeps
1597
1573
  // the plain chain.
1598
- const GLASS_CSS = `
1599
- :root{
1574
+ const GLASS_CSS = `:root{
1600
1575
  --dshome-glass-blur:16px;
1601
1576
  --dshome-glass-saturate:1.42;
1602
1577
  --dshome-glass-brightness:1;
@@ -1617,14 +1592,13 @@ body[data-dshome-dark]{
1617
1592
  --dshome-glass-ring:rgba(255,255,255,.18);
1618
1593
  --dshome-glass-shadow:0 14px 44px rgba(0,0,0,.45);
1619
1594
  }
1620
- html .hHd-Xa_root::before,
1621
- html .uV2eYG_card::after,
1622
- html :is(${USER_BUBBLE}),
1623
- html .fV0t5q_root,
1624
- html [data-composer-card],
1625
- html .VOzbGW_panel,
1626
- html .dshome-sa,
1627
- html .dshome-file-menu{
1595
+ /* 用户气泡的基础玻璃。这一条是补回来的:2.0.90 里它和侧栏、输入卡挤在同一条
1596
+ 多面合并规则里(侧栏外壳 ::before、输入卡 ::after、用户气泡三者共用一份
1597
+ 填充与模糊)。语义化时侧栏和输入卡那两支搬进了 PATCH 补丁,
1598
+ 气泡这支没人接——于是气泡只剩下面两条 :hover,静置状态一点玻璃都没有。
1599
+ 这里只补气泡:侧栏由侧栏补丁画、输入卡由输入卡补丁画,都不要再画第二遍。
1600
+ 属性与 2.0.90 逐字一致(含 !important 的位置),免得白磨砂档位换了个样子。 */
1601
+ html :is(${USER_BUBBLE}){
1628
1602
  background-image:
1629
1603
  radial-gradient(340px 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) * .30)), transparent 62%),
1630
1604
  linear-gradient(180deg, rgba(255,255,255,var(--dshome-glass-sheen)), rgba(255,255,255,var(--dshome-glass-sheen-mid)) 38%, rgba(255,255,255,.01))!important;
@@ -1633,40 +1607,30 @@ html .dshome-file-menu{
1633
1607
  box-shadow:inset 0 1px 0 var(--dshome-glass-edge),inset 0 -1px 0 var(--dshome-glass-edge-low),inset 0 0 0 .5px var(--dshome-glass-ring),var(--dshome-glass-shadow);
1634
1608
  transition:background-color .18s ease,box-shadow .18s ease;
1635
1609
  }
1610
+ /* 没有 backdrop-filter 的引擎:气泡退回不透明底,不做半透明假玻璃。 */
1611
+ @supports not ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){
1612
+ html :is(${USER_BUBBLE}){
1613
+ background-color:rgba(255,255,255,.86)!important;
1614
+ }
1615
+ body[data-dshome-dark] :is(${USER_BUBBLE}){
1616
+ background-color:rgba(13,30,60,.92)!important;
1617
+ }
1618
+ }
1636
1619
  /* The specular layer is multiplied by --dshome-glass-spec, so it has to be
1637
1620
  switched on: while the cursor is over a spot surface the highlight follows
1638
1621
  --dshome-spec-x/y (written per element by startSpecularParallax). */
1639
1622
  html [data-dshome-spot]:hover{--dshome-glass-spec:1;}
1640
- html [data-composer-card]:hover{--dshome-glass-spec:1;--dshome-glass-sheen:.12;}
1641
- html :is(${USER_BUBBLE}):hover,html .dshome-sa:hover,html .dshome-file-menu:hover{
1623
+ html ${COMPOSER_PLATE}:hover{--dshome-glass-spec:1;--dshome-glass-sheen:.12;}
1624
+ html :is(${USER_BUBBLE}
1625
+ ):hover,html .dshome-sa:hover,html .dshome-file-menu:hover{
1642
1626
  background-color:rgb(255 255 255 / calc(var(--dshome-glass-sheen) + .16))!important;
1643
1627
  }
1644
- body[data-dshome-dark] :is(${USER_BUBBLE}):hover,
1628
+ body[data-dshome-dark] :is(${USER_BUBBLE}
1629
+ ):hover,
1645
1630
  body[data-dshome-dark] .dshome-sa:hover,
1646
1631
  body[data-dshome-dark] .dshome-file-menu:hover{
1647
1632
  background-color:rgb(255 255 255 / calc(var(--dshome-glass-sheen) + .06))!important;
1648
- }
1649
- html[data-dshome-dispersion] .hHd-Xa_root::before,
1650
- html[data-dshome-dispersion] :is(${USER_BUBBLE}),
1651
- html[data-dshome-dispersion] .VOzbGW_panel{
1652
- -webkit-backdrop-filter:url(#dshome-glass-dispersion) blur(var(--dshome-glass-blur)) saturate(var(--dshome-glass-saturate))!important;
1653
- backdrop-filter:url(#dshome-glass-dispersion) blur(var(--dshome-glass-blur)) saturate(var(--dshome-glass-saturate))!important;
1654
- }
1655
- @supports not ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){
1656
- 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{
1657
- background-color:rgba(255,255,255,.86)!important;
1658
- }
1659
- body[data-dshome-dark] .hHd-Xa_root::before,
1660
- body[data-dshome-dark] .uV2eYG_card::after,
1661
- body[data-dshome-dark] :is(${USER_BUBBLE}),
1662
- body[data-dshome-dark] [data-composer-card],
1663
- body[data-dshome-dark] .VOzbGW_panel,
1664
- body[data-dshome-dark] .dshome-sa,
1665
- body[data-dshome-dark] .dshome-file-menu{
1666
- background-color:rgba(13,30,60,.92)!important;
1667
- }
1668
- }
1669
- `;
1633
+ }`;
1670
1634
 
1671
1635
  /** Append a style tag; returns a disposer that removes it. */
1672
1636
  function injectStyle(css, label) {
@@ -1712,8 +1676,7 @@ html[data-dshome-dispersion] .VOzbGW_panel{
1712
1676
  // (USER_BUBBLE itself is declared above PATCH_BLOCKS: this sheet and the
1713
1677
  // patch blocks both interpolate it, so it has to exist before either is
1714
1678
  // built.)
1715
- const LIQUID_CSS = `
1716
- @property --dshome-liquid-sweep{syntax:"<angle>";inherits:false;initial-value:0deg}
1679
+ const LIQUID_CSS = `@property --dshome-liquid-sweep{syntax:"<angle>";inherits:false;initial-value:0deg}
1717
1680
  @keyframes dshome-liquid-sweep{from{--dshome-liquid-sweep:0deg}to{--dshome-liquid-sweep:360deg}}
1718
1681
  html[data-dshome-glass="liquid"]{
1719
1682
  --dshome-liquid-blur:11px;
@@ -1766,19 +1729,16 @@ body[data-dshome-dark]{
1766
1729
  --dshome-liquid-sweep-lo:rgba(255,255,255,.16);
1767
1730
  --dshome-liquid-shadow:0 20px 60px rgba(0,0,0,.5);
1768
1731
  }
1769
- /* The title bar is deliberately NOT always-on: the user's note on it is
1770
- "rounded corners, and only once the mouse is over it" — the header becomes a
1771
- pill on :hover (the patch block already sets border-radius:999px, and
1772
- ::before inherits it), so the glass is scoped to that same hover state
1773
- instead of turning the strip into a permanent bar. */
1774
- /* The selectors carry an extra body step on purpose. The patch blocks are
1775
- injected lazily when their anchor appears — later than this sheet — and their
1776
- dark variants (body[data-dshome-dark] .uV2eYG_card) tie with a
1777
- html[attr] .surface selector on specificity, at which point source order
1778
- hands the pane back to the old fill. The extra type selector breaks the tie. */
1779
- html[data-dshome-glass="liquid"] body .hHd-Xa_root::before,
1780
- html[data-dshome-glass="liquid"] body [data-composer-card],
1781
- html[data-dshome-glass="liquid"] body .wSkVaW_header:hover::before,
1732
+ /* 液态玻璃的主规则——填充、光标高光、顶部光泽、背板模糊链、内外描边与投影。
1733
+ 2.1.0 语义化时这条规则的选择器头整段被删掉了,只剩一个孤零零的右括号加左花括号,
1734
+ 浏览器把整条丢掉,于是四个面的液态玻璃一起消失。
1735
+ 这里按语义钩子重建三个面:侧栏外壳(::before,产品自己占了这个伪元素)、
1736
+ 输入卡玻璃板([data-composer-stats] 的上层容器)、用户气泡。
1737
+ 标题栏那一支等标题栏恢复后由同一条规则接管(见 PATCH 的标题栏块)。
1738
+ 选择器带一层 body:PATCH 补丁是等锚点出现才注入的,比这张表晚,
1739
+ 同权重时源序会把面交回旧的填充,多一个类型选择器就断开这个平手。 */
1740
+ html[data-dshome-glass="liquid"] body :is(${SIDEBAR})::before,
1741
+ html[data-dshome-glass="liquid"] body ${COMPOSER_PLATE},
1782
1742
  html[data-dshome-glass="liquid"] body :is(${USER_BUBBLE}){
1783
1743
  background-color:var(--dshome-liquid-fill)!important;
1784
1744
  background-image:
@@ -1788,17 +1748,6 @@ html[data-dshome-glass="liquid"] body :is(${USER_BUBBLE}){
1788
1748
  backdrop-filter:blur(var(--dshome-liquid-blur)) saturate(var(--dshome-liquid-saturate)) brightness(var(--dshome-liquid-brightness)) contrast(var(--dshome-liquid-contrast))!important;
1789
1749
  box-shadow:inset 0 1px 0 var(--dshome-liquid-edge),inset 0 -1px 0 var(--dshome-liquid-edge-low),inset 0 0 0 .5px var(--dshome-liquid-ring),var(--dshome-liquid-shadow)!important;
1790
1750
  }
1791
- /* The composer is the one surface the old recipe paints TWICE: the patch block
1792
- blurs .uV2eYG_card::after (and the frosted list paints it too), and the card
1793
- itself is in both lists, so the backdrop was blurred twice over — which is what
1794
- turned the pane into a smear. Under liquid the card element IS the glass, so
1795
- the redundant layer is switched off rather than re-tuned. */
1796
- html[data-dshome-glass="liquid"] [data-composer-card]::after{
1797
- -webkit-backdrop-filter:none!important;
1798
- backdrop-filter:none!important;
1799
- background-image:none!important;
1800
- box-shadow:none!important;
1801
- }
1802
1751
  /* The frosted recipe gives the composer a brand-tinted conic ring, painted by
1803
1752
  the patch block at inset:-1px and spun by dsh-border-spin. Under liquid the
1804
1753
  user asked for it to go: it sits one pixel OUTSIDE the card's own 1.5px white
@@ -1818,25 +1767,12 @@ html[data-dshome-glass="liquid"] body [data-composer-card]::before{
1818
1767
  html[data-dshome-glass="liquid"] body [data-composer-card]{
1819
1768
  border-color:transparent!important;
1820
1769
  }
1821
- /* The sidebar pane carries its own fill and its own shape: rounded only on the
1822
- edge that faces the conversation, so it reads as a panel clipped to the
1823
- window rather than as a full-bleed column. The hairline ring in the shared
1824
- box-shadow above follows the radius. */
1825
- html[data-dshome-glass="liquid"] body .hHd-Xa_root::before{
1826
- background-color:var(--dshome-liquid-fill-sidebar)!important;
1827
- border-radius:0 22px 22px 0;
1828
- /* The veil rides on top of the two layers the shared rule sets, so the
1829
- sidebar keeps its cursor-follow highlight and its sheen underneath. */
1830
- background-image:
1831
- linear-gradient(var(--dshome-liquid-veil-sidebar),var(--dshome-liquid-veil-sidebar)),
1832
- 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%),
1833
- linear-gradient(180deg, rgba(255,255,255,var(--dshome-liquid-sheen)), rgba(255,255,255,.015) 34%, rgba(255,255,255,0))!important;
1834
- }
1835
1770
  /* The message bubble is the one surface that is not already positioned. The
1836
1771
  tooltip is not in this list any more -- that was B5: making a
1837
1772
  position:fixed tooltip relative turned it into an in-flow 78px block that
1838
1773
  shoved the send button out from under the cursor. See USER_BUBBLE. */
1839
- html[data-dshome-glass="liquid"] :is(${USER_BUBBLE}){
1774
+ html[data-dshome-glass="liquid"] :is(${USER_BUBBLE}
1775
+ ){
1840
1776
  position:relative;
1841
1777
  transition:background-color .18s ease,box-shadow .18s ease;
1842
1778
  }
@@ -1849,30 +1785,51 @@ html[data-dshome-glass="liquid"] :is(${USER_BUBBLE}){
1849
1785
  Chromium's backdrop sampling is unreliable on a will-changed element, and the
1850
1786
  compositor already promotes a transformed element while it is actually being
1851
1787
  transformed. The hint cost the effect it was meant to smooth. */
1852
- html[data-dshome-glass="liquid"] [data-composer-card]{
1788
+ html[data-dshome-glass="liquid"] ${COMPOSER_PLATE}{
1853
1789
  transition:transform .24s cubic-bezier(.2,.8,.2,1),background-color .18s ease,box-shadow .18s ease!important;
1854
1790
  }
1855
- html[data-dshome-glass="liquid"] :is(${USER_BUBBLE}):hover{
1791
+ html[data-dshome-glass="liquid"] :is(${USER_BUBBLE}
1792
+ ):hover{
1856
1793
  background-color:rgb(255 255 255 / .18)!important;
1857
1794
  }
1858
1795
  /* Liquid's dark hover used to sit OUTSIDE the gate, so it also repainted the
1859
1796
  frosted recipe's bubbles with liquid's values. Gated like its siblings. */
1860
- html[data-dshome-glass="liquid"] body[data-dshome-dark] :is(${USER_BUBBLE}):hover{
1797
+ html[data-dshome-glass="liquid"] body[data-dshome-dark] :is(${USER_BUBBLE}
1798
+ ):hover{
1861
1799
  background-color:rgb(255 255 255 / .10)!important;
1862
1800
  }
1863
- html[data-dshome-glass="liquid"] body .hHd-Xa_root::after{
1801
+ /* 用户气泡的边缘折射。和主规则一样,这条也只剩半截,头被删了。 */
1802
+ html[data-dshome-dispersion][data-dshome-glass="liquid"] body :is(${USER_BUBBLE}){
1803
+ -webkit-backdrop-filter:url(#dshome-glass-dispersion) blur(var(--dshome-liquid-blur)) saturate(var(--dshome-liquid-saturate))!important;
1804
+ backdrop-filter:url(#dshome-glass-dispersion) blur(var(--dshome-liquid-blur)) saturate(var(--dshome-liquid-saturate))!important;
1805
+ }
1806
+ /* 侧栏自己那一份:更重的填充(它是屏上最大的一块面,用气泡那种"几乎看不见"
1807
+ 的填充会被流体洗掉)、更重的模糊(它是与会话区的分界,共用 11px 时身后的
1808
+ 流场太清楚,面板反而和正文粘成一片)、以及一层额外面纱把它从会话区上抬起来。
1809
+ 2.1.0 里这一整块(含下面 ::after 的走光边环)都不存在。 */
1810
+ html[data-dshome-glass="liquid"] body :is(${SIDEBAR})::before{
1811
+ background-color:var(--dshome-liquid-fill-sidebar)!important;
1812
+ border-radius:0 22px 22px 0;
1813
+ background-image:
1814
+ linear-gradient(var(--dshome-liquid-veil-sidebar),var(--dshome-liquid-veil-sidebar)),
1815
+ 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%),
1816
+ linear-gradient(180deg, rgba(255,255,255,var(--dshome-liquid-sheen)), rgba(255,255,255,.015) 34%, rgba(255,255,255,0))!important;
1817
+ -webkit-backdrop-filter:blur(var(--dshome-liquid-blur-sidebar)) saturate(var(--dshome-liquid-saturate)) brightness(var(--dshome-liquid-brightness)) contrast(var(--dshome-liquid-contrast))!important;
1818
+ backdrop-filter:blur(var(--dshome-liquid-blur-sidebar)) saturate(var(--dshome-liquid-saturate)) brightness(var(--dshome-liquid-brightness)) contrast(var(--dshome-liquid-contrast))!important;
1819
+ }
1820
+ html[data-dshome-dispersion][data-dshome-glass="liquid"] body :is(${SIDEBAR})::before{
1821
+ -webkit-backdrop-filter:url(#dshome-glass-dispersion) blur(var(--dshome-liquid-blur-sidebar)) saturate(var(--dshome-liquid-saturate))!important;
1822
+ backdrop-filter:url(#dshome-glass-dispersion) blur(var(--dshome-liquid-blur-sidebar)) saturate(var(--dshome-liquid-saturate))!important;
1823
+ }
1824
+ html[data-dshome-glass="liquid"] :is(${SIDEBAR})::after{
1864
1825
  border-radius:0 22px 22px 0;
1865
1826
  }
1866
- /* The travelling edge highlight. mask-composite:exclude turns the filled
1867
- conic gradient into a 1px perimeter band; padding:1px on the pseudo-element
1868
- sets that band's width without touching the product's box model.
1869
- It is drawn on the SIDEBAR ONLY. The user asked for it off the composer and
1870
- off the message bubbles under liquid: on a small pill the band reads as a
1871
- bright white outline rather than as light travelling a glass edge, and on the
1872
- composer it doubled up with the card's own border. The bubbles and the
1873
- composer keep liquid's other three marks — the translucent fill, the sheen
1874
- and the refraction. */
1875
- html[data-dshome-glass="liquid"] .hHd-Xa_root::after{
1827
+ /* 沿着玻璃边跑的那圈高光。mask-composite:exclude 把填充的 conic 渐变掏成一条
1828
+ 1px 的边带;伪元素上的 padding:1px 定这条带的宽度,不动产品的盒模型。
1829
+ 只画在侧栏:用户明确要求从输入卡和气泡上去掉(小胶囊上那条带读起来像一圈
1830
+ 白描边,不像光在玻璃边上走),那两处保留液态的另外三个标记——半透明填充、
1831
+ 光泽、折射。 */
1832
+ html[data-dshome-glass="liquid"] :is(${SIDEBAR})::after{
1876
1833
  content:"";position:absolute;inset:0;z-index:2;pointer-events:none;border-radius:inherit;
1877
1834
  padding:1px;
1878
1835
  background:conic-gradient(from var(--dshome-liquid-sweep),
@@ -1885,54 +1842,29 @@ html[data-dshome-glass="liquid"] .hHd-Xa_root::after{
1885
1842
  mask-composite:exclude;
1886
1843
  animation:dshome-liquid-sweep 6.5s linear infinite;
1887
1844
  }
1888
- /* The composer's edge refraction. Three states, selected by the durable
1889
- composerRefraction setting (see html[data-dshome-composer]):
1890
- narrow (default) -- an 8px band top and bottom
1891
- wide -- a 16px band top and bottom
1892
- off -- no dispersion at all
1893
- Left and right are left as they fall: the band is there, but over a
1894
- continuous background a horizontal displacement has nothing to show, which
1895
- the user accepted. */
1896
- html[data-dshome-dispersion][data-dshome-glass="liquid"] body .wSkVaW_header:hover::before,
1897
- html[data-dshome-dispersion][data-dshome-glass="liquid"] body :is(${USER_BUBBLE}){
1898
- -webkit-backdrop-filter:url(#dshome-glass-dispersion) blur(var(--dshome-liquid-blur)) saturate(var(--dshome-liquid-saturate))!important;
1899
- backdrop-filter:url(#dshome-glass-dispersion) blur(var(--dshome-liquid-blur)) saturate(var(--dshome-liquid-saturate))!important;
1845
+ @media (prefers-reduced-motion:reduce){
1846
+ html[data-dshome-glass="liquid"] :is(${SIDEBAR})::after{
1847
+ animation:none;
1848
+ }
1900
1849
  }
1901
- html[data-dshome-dispersion][data-dshome-glass="liquid"][data-dshome-composer="narrow"] body [data-composer-card]{
1850
+ /* 侧栏底部那条淡出渐变,末段用的是产品自己的侧栏底色 token(不透明),
1851
+ 压在半透明玻璃上就对不上身后真实的颜色,于是在「设置」上方露出一条浅带。
1852
+ 两档配方都去掉它,不限于液态——白磨砂的侧栏同样是半透明的。 */
1853
+ body :is(${SIDEBAR}) [class*="_fade" i]{
1854
+ background-image:none!important;
1855
+ }
1856
+ html[data-dshome-dispersion][data-dshome-glass="liquid"][data-dshome-composer="narrow"] body ${COMPOSER_PLATE}{
1902
1857
  -webkit-backdrop-filter:url(#dshome-glass-dispersion-composer) blur(var(--dshome-liquid-blur-composer)) saturate(var(--dshome-liquid-saturate))!important;
1903
1858
  backdrop-filter:url(#dshome-glass-dispersion-composer) blur(var(--dshome-liquid-blur-composer)) saturate(var(--dshome-liquid-saturate))!important;
1904
1859
  }
1905
- html[data-dshome-dispersion][data-dshome-glass="liquid"][data-dshome-composer="wide"] body [data-composer-card]{
1860
+ html[data-dshome-dispersion][data-dshome-glass="liquid"][data-dshome-composer="wide"] body ${COMPOSER_PLATE}{
1906
1861
  -webkit-backdrop-filter:url(#dshome-glass-dispersion-composer-wide) blur(var(--dshome-liquid-blur-composer)) saturate(var(--dshome-liquid-saturate))!important;
1907
1862
  backdrop-filter:url(#dshome-glass-dispersion-composer-wide) blur(var(--dshome-liquid-blur-composer)) saturate(var(--dshome-liquid-saturate))!important;
1908
1863
  }
1909
- html[data-dshome-dispersion][data-dshome-glass="liquid"][data-dshome-composer="off"] body [data-composer-card]{
1864
+ html[data-dshome-dispersion][data-dshome-glass="liquid"][data-dshome-composer="off"] body ${COMPOSER_PLATE}{
1910
1865
  -webkit-backdrop-filter:blur(var(--dshome-liquid-blur-composer)) saturate(var(--dshome-liquid-saturate))!important;
1911
1866
  backdrop-filter:blur(var(--dshome-liquid-blur-composer)) saturate(var(--dshome-liquid-saturate))!important;
1912
1867
  }
1913
- /* The workspace list is faded out at its bottom by a gradient whose end stop is
1914
- the sidebar-fill token — an OPAQUE colour matched to the product's own sidebar
1915
- background. Over a translucent glass pane that stop no longer matches what is
1916
- actually behind it, so the fade shows up as a lighter band above 设置 (the
1917
- user's red box). The pane itself already separates the list from the
1918
- conversation, so the fade is dropped. Scoped to the sidebar and to the
1919
- product's own <hash>_fade naming so nothing else is touched.
1920
- NOT gated on the liquid recipe: it was, and the user sent a screenshot of the
1921
- same light band in 白磨砂 ("白磨砂,有这个渐变") — the frosted pane is translucent
1922
- too, so its end stop mismatches just the same. Both recipes drop it. */
1923
- body .hHd-Xa_root [class*="_fade" i]{
1924
- background-image:none!important;
1925
- }
1926
- /* The sidebar's own, heavier blur. Written after the two rules above because it
1927
- ties with the dispersion one on specificity and so has to come later. */
1928
- html[data-dshome-glass="liquid"] body .hHd-Xa_root::before{
1929
- -webkit-backdrop-filter:blur(var(--dshome-liquid-blur-sidebar)) saturate(var(--dshome-liquid-saturate)) brightness(var(--dshome-liquid-brightness)) contrast(var(--dshome-liquid-contrast))!important;
1930
- backdrop-filter:blur(var(--dshome-liquid-blur-sidebar)) saturate(var(--dshome-liquid-saturate)) brightness(var(--dshome-liquid-brightness)) contrast(var(--dshome-liquid-contrast))!important;
1931
- }
1932
- html[data-dshome-dispersion][data-dshome-glass="liquid"] body .hHd-Xa_root::before{
1933
- -webkit-backdrop-filter:url(#dshome-glass-dispersion) blur(var(--dshome-liquid-blur-sidebar)) saturate(var(--dshome-liquid-saturate))!important;
1934
- backdrop-filter:url(#dshome-glass-dispersion) blur(var(--dshome-liquid-blur-sidebar)) saturate(var(--dshome-liquid-saturate))!important;
1935
- }
1936
1868
  /* Backdrop blur, switched off by the durable backdropBlur setting. Each glass
1937
1869
  recipe reads its radius from a variable, so turning the frosting off is one
1938
1870
  0px per variable -- the translucency, the sheen and the edge refraction all
@@ -1945,26 +1877,48 @@ html[data-dshome-blur="off"] body{
1945
1877
  --dshome-liquid-blur-sidebar:0px;
1946
1878
  --dshome-liquid-blur-composer:0px;
1947
1879
  }
1948
- @media (prefers-reduced-motion:reduce){
1949
- html[data-dshome-glass="liquid"] .hHd-Xa_root::after{
1950
- animation:none;
1951
- }
1952
- }
1880
+ /* 不支持 backdrop-filter 的引擎:三个面退回不透明底。
1881
+ 这一块 2.1.0 里也只剩半截,一并接回来。 */
1953
1882
  @supports not ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){
1954
- html[data-dshome-glass="liquid"] body .hHd-Xa_root::before,
1955
- html[data-dshome-glass="liquid"] body [data-composer-card],
1956
- html[data-dshome-glass="liquid"] body .wSkVaW_header:hover::before,
1883
+ html[data-dshome-glass="liquid"] body :is(${SIDEBAR})::before,
1884
+ html[data-dshome-glass="liquid"] body ${COMPOSER_PLATE},
1957
1885
  html[data-dshome-glass="liquid"] body :is(${USER_BUBBLE}){
1958
1886
  background-color:rgba(255,255,255,.86)!important;
1959
1887
  }
1960
- body[data-dshome-dark] .hHd-Xa_root::before,
1961
- body[data-dshome-dark] [data-composer-card],
1962
- body[data-dshome-dark] .wSkVaW_header:hover::before,
1888
+ body[data-dshome-dark] :is(${SIDEBAR})::before,
1889
+ body[data-dshome-dark] ${COMPOSER_PLATE},
1963
1890
  html[data-dshome-glass="liquid"] body[data-dshome-dark] :is(${USER_BUBBLE}){
1964
1891
  background-color:rgba(13,30,60,.92)!important;
1965
1892
  }
1893
+ }`;
1894
+
1895
+ // MENU GLASS -- the popup surfaces the first two sheets never reached.
1896
+ //
1897
+ // The product's menus (agent preset, model, permission, session row, ...) are
1898
+ // portaled out of every pane we glass, and their own material paints an opaque
1899
+ // `rgba(248,249,250,.58)` backdrop. So a liquid-glass session had opaque white
1900
+ // menus floating over glass -- the one surface family that broke the language.
1901
+ //
1902
+ // This sheet does NOT repaint that material. It adds the glass identity on top
1903
+ // of it -- the hairline ring, the inner sheen and the theme's own shadow -- so
1904
+ // the menu keeps the product's legibility and its backdrop-filter, and simply
1905
+ // reads as part of the same material as the panes around it. Anchored on
1906
+ // `[data-menu-material]`, the product's own material hook, which survives its
1907
+ // class hashes being rebuilt.
1908
+ const MENU_GLASS_CSS = `html[data-dshome-glass="liquid"] [data-menu-material]{
1909
+ border-radius:var(--dsw-radius-lg);
1910
+ box-shadow:var(--dshome-liquid-shadow), inset 0 0 0 1px var(--dshome-liquid-ring);
1966
1911
  }
1967
- `;
1912
+ html[data-dshome-glass="liquid"] [data-menu-material]>*{
1913
+ border-radius:inherit;
1914
+ }
1915
+ /* The product's frosted pane keeps its own look; the two extra pixels of glass
1916
+ identity are what it gains, gated per glass style so switching back to
1917
+ frosted stays the byte-identical sheet the user approved. */
1918
+ html[data-dshome-glass="frosted"] [data-menu-material]{
1919
+ border-radius:var(--dsw-radius-lg);
1920
+ box-shadow:inset 0 0 0 1px var(--dshome-glass-ring);
1921
+ }`;
1968
1922
 
1969
1923
  /**
1970
1924
  * Inject `css` while `anchor` (a selector, or a list of alternatives --
@@ -1985,70 +1939,29 @@ html[data-dshome-blur="off"] body{
1985
1939
  * `querySelector` per frame at worst, and logs once, with the anchors it tried.
1986
1940
  */
1987
1941
  function installGuarded(anchor, css) {
1988
- const selectors = Array.isArray(anchor) ? anchor : [anchor];
1989
- const label = selectors.join('|');
1990
- let dispose = null;
1991
- const applyNow = () => {
1992
- if (dispose !== null) return true;
1993
- for (const selector of selectors) {
1994
- if (document.querySelector(selector) === null) continue;
1995
- dispose = injectStyle(css, 'patch' + selectors[0]);
1996
- return true;
1997
- }
1998
- return false;
1999
- };
2000
- if (applyNow()) {
2001
- return () => { if (dispose !== null) dispose(); };
2002
- }
2003
- let queued = false;
2004
- let missed = false;
2005
- let mo = null;
2006
- const schedule = () => {
2007
- if (queued || dispose !== null) return;
2008
- queued = true;
2009
- try {
2010
- requestAnimationFrame(() => {
2011
- queued = false;
2012
- if (applyNow() && mo !== null) {
2013
- mo.disconnect();
2014
- mo = null;
2015
- }
2016
- });
2017
- } catch (error) {
2018
- queued = false;
2019
- }
2020
- };
2021
- mo = new MutationObserver(schedule);
2022
- try {
2023
- mo.observe(document.body, { childList: true, subtree: true });
2024
- } catch (error) {
2025
- mo = null;
2026
- }
2027
- // One diagnostic line per block, on first sight of a shell that has
2028
- // rendered and still does not carry any of the alternatives.
2029
- const announce = () => {
2030
- if (missed || dispose !== null) return;
2031
- missed = true;
2032
- console.info('[' + PLUGIN_ID + '] patch waiting: none of ' + label + ' in the DOM yet (renamed product surface? the guard keeps watching)');
1942
+ // 语义变体:不再「挂载时查一次」。产品的面板与行是懒挂载的,查一次会让
1943
+ // 晚出现的面永远拿不到样式(旧实现的 30s 超时还会悄悄放弃)。这里用
1944
+ // MutationObserver 持续观察,锚点一出现就注入,注入过就不再重复。
1945
+ const selector = Array.isArray(anchor) ? anchor.join(',') : anchor;
1946
+ let injected = false;
1947
+ const style = document.createElement('style');
1948
+ style.dataset.plugin = PLUGIN_ID;
1949
+ style.dataset.pluginCss = PLUGIN_ID + '/patch.' + selector.slice(0, 40);
1950
+ style.textContent = css;
1951
+ const tryInject = () => {
1952
+ if (injected || document.querySelector(selector) === null) return;
1953
+ document.head.appendChild(style);
1954
+ injected = true;
1955
+ observer.disconnect();
2033
1956
  };
2034
- const timer = setTimeout(() => {
2035
- if (missed || dispose !== null) return;
2036
- announce();
2037
- }, 15000);
1957
+ const observer = new MutationObserver(() => { tryInject(); });
1958
+ tryInject();
1959
+ if (!injected) observer.observe(document.documentElement, { childList: true, subtree: true });
2038
1960
  return () => {
2039
- if (timer !== 0) clearTimeout(timer);
2040
- if (mo !== null) mo.disconnect();
2041
- if (dispose !== null) dispose();
1961
+ observer.disconnect();
1962
+ style.remove();
2042
1963
  };
2043
1964
  }
2044
-
2045
- /**
2046
- * Own the light/dark marker: reflect the theme service's active color
2047
- * scheme onto our own `data-dshome-dark` attribute, so dark styling never
2048
- * depends on the product's attribute name. Falls back to mirroring the
2049
- * product attribute only when the theme service or the `theme/change`
2050
- * subscription is unavailable.
2051
- */
2052
1965
  function createDarkSync(ctx, theme) {
2053
1966
  /** Last value we wrote to the body; null until the first apply(). */
2054
1967
  let lastDark = null;
@@ -2415,7 +2328,7 @@ html[data-dshome-blur="off"] body{
2415
2328
  * `.gdEzaW_bubble` this list used to carry matches nothing on the current
2416
2329
  * build, which is how the old entry went dead without anyone noticing.
2417
2330
  */
2418
- const SPOT_SURFACES = `${USER_BUBBLE},.uV2eYG_card,.dshome-sa,.dshome-file-menu,.hHd-Xa_root,[data-composer-card]`;
2331
+ const SPOT_SURFACES = `${USER_BUBBLE},${COMPOSER_PLATE},.dshome-sa,.dshome-file-menu,${SIDEBAR}`;
2419
2332
  function startSpecularSpotter() {
2420
2333
  const onOver = (event) => {
2421
2334
  const target = event.target;
@@ -2480,9 +2393,9 @@ html[data-dshome-blur="off"] body{
2480
2393
  // (an OS setting that disables the effect and looks like a bug) and
2481
2394
  // `spots` (0 means the composer no longer matches TILT_SELECTOR), and the
2482
2395
  // reduced-motion case logs one info line instead of vanishing.
2483
- const TILT_SELECTOR = '[data-composer-card],.uV2eYG_card';
2396
+ const TILT_SELECTOR = COMPOSER_PLATE;
2484
2397
  /** On <html> while the composer leans; the lens touch on the line under the
2485
- * composer reads it (see the [data-dsh-stats] patch block). */
2398
+ * composer reads it (see the [data-composer-stats] patch block). */
2486
2399
  const LEAN_ATTR = 'data-dshome-leaning';
2487
2400
  // 0.0175 rad was upstream's conservative lean, and after 2.0.85 finally made
2488
2401
  // the effect reachable the verdict was "倾斜效果很微弱" -- fair, at ~4 degrees
@@ -2572,10 +2485,15 @@ html[data-dshome-blur="off"] body{
2572
2485
  raf = requestAnimationFrame(() => {
2573
2486
  raf = 0;
2574
2487
  if (current !== spot || frozen) return;
2575
- if (clientX < rect.left || clientX > rect.right || clientY < rect.top || clientY > rect.bottom) return;
2576
- const dx = Math.min(0.5, Math.max(-0.5, (clientX - rect.left) / rect.width - 0.5));
2577
- const dy = Math.min(0.5, Math.max(-0.5, (clientY - rect.top) / rect.height - 0.5));
2578
- spot.style.transformOrigin = (rect.width / 2) + 'px ' + (rect.height / 2) + 'px';
2488
+ const g = spotGeometry(spot, rect);
2489
+ if (g.halfW <= 0 || g.halfH <= 0) return;
2490
+ if (clientX < g.left || clientX > g.right || clientY < g.top || clientY > g.bottom) return;
2491
+ // Same half-range clamp as before -- `(clientX - left) / width - 0.5`
2492
+ // is `(clientX - centre) / width` -- but over a width that cannot
2493
+ // change because of what this line is about to write.
2494
+ const dx = Math.min(0.5, Math.max(-0.5, (clientX - g.cx) / (g.halfW * 2)));
2495
+ const dy = Math.min(0.5, Math.max(-0.5, (clientY - g.cy) / (g.halfH * 2)));
2496
+ spot.style.transformOrigin = g.halfW + 'px ' + g.halfH + 'px';
2579
2497
  spot.style.transition = TILT_TRANSITION;
2580
2498
  spot.style.transform = 'perspective(' + TILT_PERSPECTIVE + 'px)'
2581
2499
  + ' rotateX(' + (TILT_MAX * -2 * dy).toFixed(5) + 'rad)'
@@ -2600,11 +2518,39 @@ html[data-dshome-blur="off"] body{
2600
2518
  * broken effect: the box just never leans. Geometry is the one thing the
2601
2519
  * theme can measure for itself.
2602
2520
  */
2521
+ /**
2522
+ * Transform-independent geometry for one tilt spot.
2523
+ *
2524
+ * `getBoundingClientRect()` INCLUDES the transform this effect writes, so
2525
+ * feeding it back in closes a loop: the scale resizes the rect, the rect
2526
+ * normalises the cursor offset, the offset picks the next angle, and the
2527
+ * angle moves the rect again. Tuned against the short composer that loop is
2528
+ * a pixel or two and invisible; the moment a long draft grows the composer to
2529
+ * its max-height the same RELATIVE feedback is ~10px, and the card visibly
2530
+ * flaps between two sizes a few times a second. That is the same hover loop
2531
+ * 2.0.79 fought by dropping `TILT_SCALE` to 1 -- and it came back with the
2532
+ * 1.015 lift, because the loop was damped, never broken.
2533
+ *
2534
+ * The centre is the one part of the rect a transform about the centre cannot
2535
+ * move (under `perspective()` the centre plane sits at z=0, so the projection
2536
+ * leaves it alone), and `offsetWidth`/`offsetHeight` never include
2537
+ * transforms. Those two facts make every use below a function of the pointer
2538
+ * and the LAYOUT box alone: nothing reads last frame's transform, so there is
2539
+ * no loop left to damp -- and the lift can stay at full strength.
2540
+ */
2541
+ const spotGeometry = (spot, rect) => {
2542
+ const halfW = spot.offsetWidth / 2;
2543
+ const halfH = spot.offsetHeight / 2;
2544
+ const cx = rect.left + rect.width / 2;
2545
+ const cy = rect.top + rect.height / 2;
2546
+ return { cx, cy, halfW, halfH, left: cx - halfW, right: cx + halfW, top: cy - halfH, bottom: cy + halfH };
2547
+ };
2603
2548
  const spotAt = (clientX, clientY) => {
2604
2549
  for (const spot of document.querySelectorAll(TILT_SELECTOR)) {
2605
2550
  const rect = spot.getBoundingClientRect();
2606
- if (rect.width <= 0 || rect.height <= 0) continue;
2607
- if (clientX < rect.left || clientX > rect.right || clientY < rect.top || clientY > rect.bottom) continue;
2551
+ const g = spotGeometry(spot, rect);
2552
+ if (g.halfW <= 0 || g.halfH <= 0) continue;
2553
+ if (clientX < g.left || clientX > g.right || clientY < g.top || clientY > g.bottom) continue;
2608
2554
  return { spot, rect };
2609
2555
  }
2610
2556
  return null;
@@ -2626,7 +2572,8 @@ html[data-dshome-blur="off"] body{
2626
2572
  const spot = target.closest(TILT_SELECTOR);
2627
2573
  if (spot === null) return;
2628
2574
  const rect = spot.getBoundingClientRect();
2629
- if (event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom) return;
2575
+ const g = spotGeometry(spot, rect);
2576
+ if (event.clientX < g.left || event.clientX > g.right || event.clientY < g.top || event.clientY > g.bottom) return;
2630
2577
  if (controlAt(event.clientX, event.clientY)) {
2631
2578
  if (current === spot) frozen = true;
2632
2579
  return;
@@ -2648,9 +2595,9 @@ html[data-dshome-blur="off"] body{
2648
2595
  // the lean's own movement must not be able to hand the card back
2649
2596
  // and forth under a stationary pointer.
2650
2597
  if (current !== null) {
2651
- const rect = current.getBoundingClientRect();
2652
- const outside = event.clientX < rect.left - TILT_RELEASE_PAD || event.clientX > rect.right + TILT_RELEASE_PAD
2653
- || event.clientY < rect.top - TILT_RELEASE_PAD || event.clientY > rect.bottom + TILT_RELEASE_PAD;
2598
+ const g = spotGeometry(current, current.getBoundingClientRect());
2599
+ const outside = event.clientX < g.left - TILT_RELEASE_PAD || event.clientX > g.right + TILT_RELEASE_PAD
2600
+ || event.clientY < g.top - TILT_RELEASE_PAD || event.clientY > g.bottom + TILT_RELEASE_PAD;
2654
2601
  if (outside) {
2655
2602
  release(current);
2656
2603
  current = null;
@@ -2681,9 +2628,9 @@ html[data-dshome-blur="off"] body{
2681
2628
  };
2682
2629
  const onOut = (event) => {
2683
2630
  if (current === null) return;
2684
- const rect = current.getBoundingClientRect();
2685
- if (event.clientX >= rect.left - TILT_RELEASE_PAD && event.clientX <= rect.right + TILT_RELEASE_PAD
2686
- && event.clientY >= rect.top - TILT_RELEASE_PAD && event.clientY <= rect.bottom + TILT_RELEASE_PAD) return;
2631
+ const g = spotGeometry(current, current.getBoundingClientRect());
2632
+ if (event.clientX >= g.left - TILT_RELEASE_PAD && event.clientX <= g.right + TILT_RELEASE_PAD
2633
+ && event.clientY >= g.top - TILT_RELEASE_PAD && event.clientY <= g.bottom + TILT_RELEASE_PAD) return;
2687
2634
  release(current);
2688
2635
  current = null;
2689
2636
  };
@@ -2721,7 +2668,8 @@ html[data-dshome-blur="off"] body{
2721
2668
  if (!supportsWebgl2()) return startParticles();
2722
2669
  // Which build is running, readable from the page (see README). One line in
2723
2670
  // the console answers "is the bundle I just installed actually live?".
2724
- try { window.__dshomeBuild = '2.0.89 rc2-verified'; } catch (error) {}
2671
+ /* 版本标记统一在激活处写(见 window.__dshomeAnchors 旁边):这里是两条不同的
2672
+ 挂载路径,各自写一个会互相覆盖,2.1.0 的包曾经报成 2.0.89 就是这个原因。 */
2725
2673
  const canvas = document.createElement('canvas');
2726
2674
  canvas.setAttribute('data-dsh-deepseek-canvas', '');
2727
2675
  const style = canvas.style;
@@ -2874,7 +2822,7 @@ html[data-dshome-blur="off"] body{
2874
2822
  washLayer: getComputedStyle(document.body, '::before').position,
2875
2823
  tiltCard: card === null ? null : getComputedStyle(card).backdropFilter,
2876
2824
  tiltCardWillChange: card === null ? null : (getComputedStyle(card).willChange || 'auto'),
2877
- sidebar: chain('.hHd-Xa_root', '::before'),
2825
+ sidebar: chain(SIDEBAR, '::before'),
2878
2826
  bubble: chain(USER_BUBBLE)
2879
2827
  };
2880
2828
  };
@@ -2912,7 +2860,8 @@ html[data-dshome-blur="off"] body{
2912
2860
  }
2913
2861
 
2914
2862
  function startParticles() {
2915
- try { window.__dshomeBuild = '2.0.89 rc2-verified'; } catch (error) {}
2863
+ /* 版本标记统一在激活处写(见 window.__dshomeAnchors 旁边):这里是两条不同的
2864
+ 挂载路径,各自写一个会互相覆盖,2.1.0 的包曾经报成 2.0.89 就是这个原因。 */
2916
2865
  const canvas = document.createElement('canvas');
2917
2866
  canvas.setAttribute('data-dsh-deepseek-canvas', '');
2918
2867
  const style = canvas.style;
@@ -3092,340 +3041,38 @@ html[data-dshome-blur="off"] body{
3092
3041
  return () => document.removeEventListener('click', onClick, true);
3093
3042
  }
3094
3043
 
3095
- function watchHeaderInteraction() {
3096
- const EASE = 'cubic-bezier(.2,.8,.2,1)';
3097
- const DUR = '.3s';
3098
- const timers = [];
3099
-
3100
- const headerOf = (node) => (node && node.closest ? node.closest('.wSkVaW_header') : null);
3101
- const isTrajectory = (header) => {
3102
- const root = header.closest('.wSkVaW_root');
3103
- return root !== null && root.querySelector('.fV0t5q_root') !== null;
3104
- };
3105
-
3106
- const enter = (header) => {
3107
- const actions = header.querySelector('.wSkVaW_headerActions');
3108
- const crumbs = header.querySelector('.wSkVaW_crumbs');
3109
- if (crumbs === null) return;
3110
-
3111
- const actionsFirst = actions ? actions.getBoundingClientRect() : null;
3112
- const crumbsFirst = crumbs.getBoundingClientRect();
3113
-
3114
- header.classList.add('dshome-swap');
3115
-
3116
- const actionsFlow = actions ? actions.getBoundingClientRect() : null;
3117
- const crumbsFlow = crumbs.getBoundingClientRect();
3118
-
3119
- if (actions && actionsFirst && actionsFlow) {
3120
- const dx = actionsFirst.left - actionsFlow.left;
3121
- const dy = actionsFirst.top - actionsFlow.top;
3122
- actions.style.transition = 'none';
3123
- actions.style.transform = 'translate(' + dx + 'px,' + dy + 'px)';
3124
- }
3125
-
3126
- const cdx = crumbsFirst.left - crumbsFlow.left;
3127
- const cdy = crumbsFirst.top - crumbsFlow.top;
3128
- crumbs.style.transition = 'none';
3129
- crumbs.style.transform = 'translate(' + cdx + 'px,' + cdy + 'px)';
3130
-
3131
- void header.offsetWidth;
3132
-
3133
- if (actions) {
3134
- actions.style.transition = 'transform ' + DUR + ' ' + EASE;
3135
- actions.style.transform = 'translate(0,0)';
3136
- }
3137
- crumbs.style.transition = 'transform ' + DUR + ' ' + EASE;
3138
- crumbs.style.transform = 'translate(0,0)';
3139
-
3140
- const timer = setTimeout(() => {
3141
- if (actions) { actions.style.transition = ''; actions.style.transform = ''; }
3142
- crumbs.style.transition = '';
3143
- crumbs.style.transform = '';
3144
- }, 320);
3145
- timers.push(timer);
3146
- };
3147
-
3148
- const leave = (header) => {
3149
- const actions = header.querySelector('.wSkVaW_headerActions');
3150
- const crumbs = header.querySelector('.wSkVaW_crumbs');
3151
-
3152
- const actionsHover = actions ? actions.getBoundingClientRect() : null;
3153
- const crumbsHover = crumbs ? crumbs.getBoundingClientRect() : null;
3154
-
3155
- header.classList.remove('dshome-swap');
3156
-
3157
- if (actions && actionsHover) {
3158
- const n = actions.getBoundingClientRect();
3159
- const dx = actionsHover.left - n.left;
3160
- const dy = actionsHover.top - n.top;
3161
- actions.style.transition = 'none';
3162
- actions.style.transform = 'translate(' + dx + 'px,' + dy + 'px)';
3163
- }
3164
-
3165
- if (crumbs && crumbsHover) {
3166
- const n = crumbs.getBoundingClientRect();
3167
- const cdx = crumbsHover.left - n.left;
3168
- const cdy = crumbsHover.top - n.top;
3169
- crumbs.style.transition = 'none';
3170
- crumbs.style.transform = 'translate(' + cdx + 'px,' + cdy + 'px)';
3171
- }
3172
-
3173
- void header.offsetWidth;
3174
-
3175
- if (actions) {
3176
- actions.style.transition = 'transform ' + DUR + ' ' + EASE;
3177
- actions.style.transform = 'translate(0,0)';
3178
- }
3179
- if (crumbs) {
3180
- crumbs.style.transition = 'transform ' + DUR + ' ' + EASE;
3181
- crumbs.style.transform = 'translate(0,0)';
3182
- }
3183
-
3184
- const timer = setTimeout(() => {
3185
- if (actions) { actions.style.transition = ''; actions.style.transform = ''; }
3186
- if (crumbs) { crumbs.style.transition = ''; crumbs.style.transform = ''; }
3187
- }, 320);
3188
- timers.push(timer);
3189
- };
3190
-
3191
- const over = (event) => {
3192
- const header = headerOf(event.target);
3193
- if (header === null || isTrajectory(header)) return;
3194
- if (header.classList.contains('dshome-swap')) return;
3195
- const from = event.relatedTarget;
3196
- if (from && header.contains(from)) return;
3197
- enter(header);
3198
- };
3199
-
3200
- const out = (event) => {
3201
- const header = headerOf(event.target);
3202
- if (header === null) return;
3203
- if (!header.classList.contains('dshome-swap')) return;
3204
- const to = event.relatedTarget;
3205
- if (to && header.contains(to)) return;
3206
- leave(header);
3207
- };
3208
-
3209
- document.addEventListener('mouseover', over);
3210
- document.addEventListener('mouseout', out);
3211
-
3212
- return () => {
3213
- document.removeEventListener('mouseover', over);
3214
- document.removeEventListener('mouseout', out);
3215
- timers.forEach((t) => clearTimeout(t));
3216
- document.querySelectorAll('.wSkVaW_header.dshome-swap').forEach((h) => {
3217
- h.classList.remove('dshome-swap');
3218
- const a = h.querySelector('.wSkVaW_headerActions');
3219
- const c = h.querySelector('.wSkVaW_crumbs');
3220
- if (a) { a.style.transition = ''; a.style.transform = ''; }
3221
- if (c) { c.style.transition = ''; c.style.transform = ''; }
3222
- });
3223
- };
3224
- }
3225
-
3226
- /**
3227
- * Add an "Open workspace" entry to the folder row's (workspace row) three-dot
3228
- * menu. The workspace browser renders its row menus internally with no plugin
3229
- * slot, so the theme patches the popup DOM: it tracks the ellipsis anchor of a
3230
- * project row, and when the portaled [role=menu] appears it inserts an item
3231
- * that resolves the row's workspace (title match against the workspaces
3232
- * store, DOM-order fallback) and asks the host half (connection RPC) to open
3233
- * the workspace's directory with the OS file manager in the foreground.
3234
- * The private host channel is the primary reveal path; `workspaces.openPath`
3235
- * is used only when it still exists (dsh ≤0.1.1), and a failed reveal is
3236
- * reported on the console instead of failing silently.
3237
- */
3238
- function watchWorkspaceMenu(ctx) {
3239
- let workspaces;
3240
- try {
3241
- workspaces = ctx.get('workspaces');
3242
- } catch (error) {
3243
- workspaces = undefined;
3244
- }
3245
- let connection;
3246
- try {
3247
- connection = ctx.get('connection');
3248
- } catch (error) {
3249
- connection = undefined;
3250
- }
3251
- if (workspaces === undefined || workspaces === null || workspaces.list === undefined) return () => {};
3252
3044
 
3253
- const ROW = '.YDXeBa_projectRow';
3254
- const ACTIONS = '.YDXeBa_rowActions';
3255
- const LABEL = '.YDXeBa_title';
3256
- const CLS = 'dshome-ws-open';
3257
- const FOLDER_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>';
3258
-
3259
- let label = '打开工作区';
3260
- try {
3261
- const locale = ctx.get('locale');
3262
- if (locale !== undefined) {
3263
- const active = locale.getLocale().active;
3264
- if (active !== 'zh' && active !== 'zh-CN' && active !== 'zh-Hans') label = 'Open workspace';
3265
- }
3266
- } catch (error) {}
3267
-
3268
- let anchorRow = null;
3269
-
3270
- const resolveWorkspace = (row) => {
3271
- try {
3272
- const items = workspaces.list.getSnapshot().items;
3273
- if (!Array.isArray(items)) return undefined;
3274
- const titleEl = row.querySelector(LABEL);
3275
- const labelText = titleEl === null ? '' : (titleEl.textContent || '').trim();
3276
- if (labelText === '') return undefined;
3277
- const byTitle = items.filter((w) => w !== null && w !== undefined && w.title === labelText);
3278
- if (byTitle.length === 1) return byTitle[0];
3279
- const rows = Array.from(document.querySelectorAll(ROW));
3280
- const index = rows.indexOf(row);
3281
- if (index === -1) return undefined;
3282
- let ordinal = 0;
3283
- for (let i = 0; i < index; i++) {
3284
- const t = rows[i].querySelector(LABEL);
3285
- const l = t === null ? '' : (t.textContent || '').trim();
3286
- if (l !== '' && l !== 'Ungrouped') ordinal += 1;
3287
- }
3288
- const candidate = items[ordinal];
3289
- return candidate === undefined || candidate === null ? undefined : candidate;
3290
- } catch (error) {
3291
- return undefined;
3292
- }
3293
- };
3294
-
3295
- const track = (target) => {
3296
- anchorRow = null;
3297
- if (target === null || typeof target.closest !== 'function') return;
3298
- const button = target.closest('button');
3299
- if (button === null) return;
3300
- const actions = button.closest(ACTIONS);
3301
- if (actions === null) return;
3302
- const row = actions.closest(ROW);
3303
- if (row === null) return;
3304
- if (button !== actions.querySelector('button')) return;
3305
- anchorRow = row;
3306
- };
3307
-
3308
- const onClickCapture = (event) => track(event.target);
3309
- const onFocusIn = (event) => track(event.target);
3310
- document.addEventListener('click', onClickCapture, true);
3311
- document.addEventListener('focusin', onFocusIn, true);
3312
-
3313
- const injectItem = (menu) => {
3314
- if (menu === null || menu.querySelector('.' + CLS) !== null) return;
3315
- const viewport = menu.querySelector('[role="presentation"]');
3316
- if (viewport === null) return;
3317
- const row = anchorRow;
3318
- if (row === null || !document.contains(row)) return;
3319
- const workspace = resolveWorkspace(row);
3320
- if (workspace === undefined) return;
3321
-
3322
- const wrap = document.createElement('div');
3323
- wrap.className = CLS + '-wrap';
3324
- const item = document.createElement('button');
3325
- item.type = 'button';
3326
- item.className = CLS;
3327
- item.setAttribute('role', 'menuitem');
3328
- item.title = label;
3329
- item.setAttribute('aria-label', label);
3330
- const icon = document.createElement('span');
3331
- icon.className = CLS + '-icon';
3332
- icon.innerHTML = FOLDER_SVG;
3333
- const text = document.createElement('span');
3334
- text.className = CLS + '-label';
3335
- text.textContent = label;
3336
- item.appendChild(icon);
3337
- item.appendChild(text);
3338
- item.addEventListener('click', (event) => {
3339
- event.preventDefault();
3340
- event.stopPropagation();
3341
- const path = workspace.path;
3342
- // The host bridge is a same-origin webServer route (see bridgeCall).
3343
- const fallback = (reason) => {
3344
- if (typeof workspaces.openPath === 'function') {
3345
- try {
3346
- workspaces.openPath(path);
3347
- return;
3348
- } catch (error) {
3349
- console.warn('[deepseek-style-theme] workspaces.openPath failed:', error);
3350
- }
3351
- }
3352
- console.warn(
3353
- '[deepseek-style-theme] 打开工作区失败(' + reason + '):' +
3354
- '私有 RPC 通道不可用,且当前 dsh 版本已移除 workspaces.openPath。' +
3355
- '请确认插件为 1.38.4+ 并重启 dsh web(旧版宿主端因缺少 webServer 注入无法注册通道)。'
3356
- );
3357
- };
3358
- Promise.resolve(bridgeCall('dshome/explorer.open', { path: path }))
3359
- .then((result) => {
3360
- if (result !== undefined && result !== null && result.ok === true) return;
3361
- const detail = result !== undefined && result !== null && result.error !== undefined
3362
- ? String(result.error.message)
3363
- : 'no result';
3364
- fallback('bridge returned ' + detail);
3365
- })
3366
- .catch((error) => {
3367
- fallback('bridge call threw: ' + (error instanceof Error ? error.message : String(error)));
3368
- });
3369
- try {
3370
- document.body.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, cancelable: true }));
3371
- } catch (error) {}
3372
- });
3373
- wrap.appendChild(item);
3374
- viewport.insertBefore(wrap, viewport.firstChild);
3375
- };
3376
-
3377
- const observer = new MutationObserver((records) => {
3378
- for (const record of records) {
3379
- for (const node of record.addedNodes) {
3380
- if (!(node instanceof Element)) continue;
3381
- if (node.matches('[role="menu"]')) {
3382
- injectItem(node);
3383
- continue;
3384
- }
3385
- node.querySelectorAll('[role="menu"]').forEach(injectItem);
3386
- }
3387
- }
3388
- });
3389
- observer.observe(document.body, { childList: true, subtree: true });
3390
-
3391
- return () => {
3392
- observer.disconnect();
3393
- document.removeEventListener('click', onClickCapture, true);
3394
- document.removeEventListener('focusin', onFocusIn, true);
3395
- };
3396
- }
3397
3045
 
3398
3046
  /**
3399
3047
  * Action menu for the product's delivered/changed files
3400
3048
  * (`@deepseek-ai/dsh-client-ui-deliverables`), which renders two separate
3401
3049
  * surfaces:
3402
3050
  *
3403
- * - `[data-produced-files-row]` — the "本轮文件改动" chips, one
3051
+ * - `[data-presented-files-row]` — the "本轮文件改动" chips, one
3404
3052
  * `button[title=absolutePath]` per changed path, whose only own action
3405
3053
  * is the product's preview opener;
3406
3054
  * - `[data-presented-file]` — the declared-file cards, whose overlay button
3407
3055
  * previews and whose chevron already offers default-app open and reveal.
3408
3056
  *
3409
- * Both now open this menu: the changed-file chips on a plain left click
3410
- * (clicking one previously looked like nothing happened) and both surfaces
3411
- * on right click. The product's own action is preserved as a menu item, so
3412
- * hijacking the chip click trades nothing away. "Copy path" does not exist
3413
- * anywhere in the product — the absolute path is otherwise only reachable by
3414
- * reading a `title` tooltip by hand — so it is also added to the product's
3415
- * own chevron menu.
3057
+ * Both open this menu. The product's own action is preserved as a menu item
3058
+ * ("打开文件" re-dispatches the element's own click), so taking over the
3059
+ * gesture trades nothing away. The absolute path rides in each surface's
3060
+ * `title` (a card via the product's own `resolveWorkspacePath`), so this
3061
+ * patch never re-derives a workspace.
3416
3062
  *
3417
- * Both anchors are stable product attributes rather than build-hashed class
3418
- * names, and each surface already carries the absolute path in its `title`
3419
- * (a card via the product's own `resolveWorkspacePath`), so this patch never
3420
- * re-derives a workspace.
3063
+ * 2.1.0 收敛:`[data-produced-files-row]` 是 0.1.7 前后的旧名(0.2.0 已改为
3064
+ * `data-presented-files-row`,旧名在整个安装目录里都不存在了),双名兼容已移除;
3065
+ * 行内 `@file` 引用(`button[data-ref-chip]`)的点击接管也已撤掉——那一类交还
3066
+ * 官方行为(点击即官方预览器)。
3421
3067
  */
3422
3068
  function watchFileCardMenu(ctx) {
3423
3069
  const CARD = '[data-presented-file]';
3424
3070
  // The product renders changed files through a *different* component:
3425
3071
  // ProducedFiles draws one `button[title=absolutePath]` per path inside
3426
- // `[data-produced-files-row]`. Those chips are not
3427
- // `[data-presented-file]` cards at all, so they need their own anchor.
3428
- const PRODUCED = '[data-produced-files-row] button[title]';
3072
+ // the changed-files row (0.2.0's attribute is `data-presented-files-row`).
3073
+ // Those chips are not `[data-presented-file]` cards at all, so they need
3074
+ // their own anchor.
3075
+ const PRODUCED = '[data-presented-files-row] button[title]';
3429
3076
  const MENU = '[role="menu"]';
3430
3077
  const CLS = 'dshome-file-menu';
3431
3078
  const ITEM_CLS = CLS + '-item';
@@ -3435,18 +3082,30 @@ html[data-dshome-blur="off"] body{
3435
3082
  const ICON_REVEAL = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>';
3436
3083
  const ICON_COPY = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
3437
3084
  const ICON_PREVIEW = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z"/><circle cx="12" cy="12" r="3"/></svg>';
3438
- const CSS = `
3439
- .${CLS}{position:fixed;z-index:2147483000;min-width:198px;padding:6px;border-radius:14px;background:rgba(255,255,255,.94);border:.5px solid var(--dsw-alias-border-l2,rgba(0,0,0,.1));box-shadow:0 18px 44px rgba(15,30,60,.2);backdrop-filter:blur(18px) saturate(1.4);-webkit-backdrop-filter:blur(18px) saturate(1.4);animation:dshome-menu-pop .16s cubic-bezier(.2,.8,.2,1);transform-origin:top left}
3440
- body[data-dshome-dark] .${CLS}{background:rgba(14,30,60,.94);box-shadow:0 18px 44px rgba(0,0,0,.5)}
3441
- .${ITEM_CLS},.${COPY_CLS}{display:flex;align-items:center;gap:9px;box-sizing:border-box;width:100%;padding:8px 10px;border:0;border-radius:9px;background:transparent;color:var(--dsw-alias-label-primary,#152443);font:inherit;font-size:13px;line-height:20px;text-align:left;cursor:pointer;transition:background-color .12s ease,transform .12s ease}
3442
- .${ITEM_CLS}:hover,.${COPY_CLS}:hover{background:color-mix(in srgb,var(--ds-brand,#4d6bfe) 13%,transparent);transform:translateX(1px)}
3443
- .${ITEM_CLS}:focus-visible,.${COPY_CLS}:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4d6bfe);outline-offset:-2px}
3444
- .${ITEM_CLS} svg,.${COPY_CLS} svg{flex:none;width:15px;height:15px;opacity:.72}
3445
- .${ITEM_CLS}>span,.${COPY_CLS}>span{display:inline-flex;align-items:center;min-width:0}
3446
- .${CLS}-sep{height:1px;margin:5px 6px;background:var(--dsw-alias-border-l1,rgba(0,0,0,.06))}
3447
- .${TOAST_CLS}{position:fixed;z-index:2147483001;left:50%;bottom:56px;transform:translateX(-50%);padding:8px 15px;border-radius:999px;background:rgba(20,36,70,.93);color:#fff;font-size:12px;line-height:18px;pointer-events:none;animation:dshome-fade-in .18s ease}
3448
- @keyframes dshome-menu-pop{from{opacity:0;transform:scale(.96) translateY(-4px)}to{opacity:1;transform:none}}
3449
- `;
3085
+ const CSS = `.${CLS}
3086
+ {position:fixed;z-index:2147483000;min-width:198px;padding:6px;border-radius:14px;background:rgba(255,255,255,.94);border:.5px solid var(--dsw-alias-border-l2,rgba(0,0,0,.1));box-shadow:0 18px 44px rgba(15,30,60,.2);backdrop-filter:blur(18px) saturate(1.4);-webkit-backdrop-filter:blur(18px) saturate(1.4);animation:dshome-menu-pop .16s cubic-bezier(.2,.8,.2,1);transform-origin:top left}
3087
+ body[data-dshome-dark] .${CLS}
3088
+ {background:rgba(14,30,60,.94);box-shadow:0 18px 44px rgba(0,0,0,.5)}
3089
+ .${ITEM_CLS}
3090
+ ,.${COPY_CLS}
3091
+ {display:flex;align-items:center;gap:9px;box-sizing:border-box;width:100%;padding:8px 10px;border:0;border-radius:9px;background:transparent;color:var(--dsw-alias-label-primary,#152443);font:inherit;font-size:13px;line-height:20px;text-align:left;cursor:pointer;transition:background-color .12s ease,transform .12s ease}
3092
+ .${ITEM_CLS}
3093
+ :hover,.${COPY_CLS}
3094
+ :hover{background:color-mix(in srgb,var(--ds-brand,#4d6bfe) 13%,transparent);transform:translateX(1px)}
3095
+ .${ITEM_CLS}
3096
+ :focus-visible,.${COPY_CLS}
3097
+ :focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4d6bfe);outline-offset:-2px}
3098
+ .${ITEM_CLS}
3099
+ svg,.${COPY_CLS}
3100
+ svg{flex:none;width:15px;height:15px;opacity:.72}
3101
+ .${ITEM_CLS}
3102
+ >span,.${COPY_CLS}
3103
+ >span{display:inline-flex;align-items:center;min-width:0}
3104
+ .${CLS}
3105
+ -sep{height:1px;margin:5px 6px;background:var(--dsw-alias-border-l1,rgba(0,0,0,.06))}
3106
+ .${TOAST_CLS}
3107
+ {position:fixed;z-index:2147483001;left:50%;bottom:56px;transform:translateX(-50%);padding:8px 15px;border-radius:999px;background:rgba(20,36,70,.93);color:#fff;font-size:12px;line-height:18px;pointer-events:none;animation:dshome-fade-in .18s ease}
3108
+ @keyframes dshome-menu-pop{from{opacity:0;transform:scale(.96) translateY(-4px)}to{opacity:1;transform:none}}`;
3450
3109
 
3451
3110
  const isZh = () => {
3452
3111
  try {
@@ -3459,6 +3118,7 @@ body[data-dshome-dark] .${CLS}{background:rgba(14,30,60,.94);box-shadow:0 18px 4
3459
3118
  }
3460
3119
  };
3461
3120
  const labels = () => (isZh() ? {
3121
+ openOfficial: '打开文件',
3462
3122
  open: '用默认应用打开',
3463
3123
  reveal: '打开所在文件夹',
3464
3124
  copy: '复制路径',
@@ -3468,6 +3128,7 @@ body[data-dshome-dark] .${CLS}{background:rgba(14,30,60,.94);box-shadow:0 18px 4
3468
3128
  copyDenied: '浏览器拒绝了剪贴板访问,请手动复制',
3469
3129
  failed: '操作失败(宿主端未响应)'
3470
3130
  } : {
3131
+ openOfficial: 'Open file',
3471
3132
  open: 'Open with default app',
3472
3133
  reveal: 'Open containing folder',
3473
3134
  copy: 'Copy path',
@@ -3486,6 +3147,57 @@ body[data-dshome-dark] .${CLS}{background:rgba(14,30,60,.94);box-shadow:0 18px 4
3486
3147
  return typeof value === 'string' ? value.trim() : '';
3487
3148
  };
3488
3149
 
3150
+ /**
3151
+ * The workspace root this session runs in 鈥?what turns a tool row's relative
3152
+ * text back into a path the OS can open. Read defensively, and never guess:
3153
+ * an unresolvable root leaves the menu without copy/reveal for that surface.
3154
+ */
3155
+ const rootForSession = () => {
3156
+ try {
3157
+ const workspaces = ctx.get('workspaces');
3158
+ if (workspaces === undefined || workspaces === null) return '';
3159
+ // The service face exposes methods only; its snapshot store is not part
3160
+ // of that contract, so read it defensively and fall through to ''.
3161
+ const store = workspaces.list;
3162
+ if (store === undefined || store === null || typeof store.getSnapshot !== 'function') return '';
3163
+ const snapshot = store.getSnapshot();
3164
+ const items = snapshot === undefined || snapshot === null ? undefined : snapshot.items;
3165
+ if (!Array.isArray(items) || items.length === 0) return '';
3166
+ let sessionId = '';
3167
+ const sessions = ctx.get('sessions');
3168
+ if (sessions !== undefined && sessions !== null && typeof sessions.bindingForActive === 'function') {
3169
+ const binding = sessions.bindingForActive();
3170
+ if (binding !== undefined && binding !== null && typeof binding.sessionId === 'string') sessionId = binding.sessionId;
3171
+ }
3172
+ const owner = sessionId === '' ? undefined : items.find((item) => Array.isArray(item.sessionIds) && item.sessionIds.indexOf(sessionId) >= 0);
3173
+ const chosen = owner === undefined ? items[0] : owner;
3174
+ const root = chosen === undefined || chosen === null ? '' : chosen.path;
3175
+ return typeof root === 'string' ? root.replace(/[\\/]+$/, '') : '';
3176
+ } catch (error) {
3177
+ return '';
3178
+ }
3179
+ };
3180
+ const isPathLike = (value) => {
3181
+ if (typeof value !== 'string') return false;
3182
+ const trimmed = value.trim();
3183
+ if (trimmed === '') return false;
3184
+ if (/^[A-Za-z]:[\\/]/.test(trimmed) || trimmed.charAt(0) === '/') return true;
3185
+ // A printed relative path has a separator; a package spec does not.
3186
+ if (trimmed.charAt(0) === '@' || trimmed.indexOf('://') >= 0) return false;
3187
+ return trimmed.indexOf('\\') >= 0 || trimmed.indexOf('/') >= 0;
3188
+ };
3189
+ /** Absolute path for a surface, or '' when nothing trustworthy is available. */
3190
+ const absoluteOf = (value) => {
3191
+ const trimmed = typeof value === 'string' ? value.trim() : '';
3192
+ if (trimmed === '') return '';
3193
+ if (/^[A-Za-z]:[\\/]/.test(trimmed) || trimmed.charAt(0) === '/') return trimmed;
3194
+ if (!isPathLike(trimmed)) return '';
3195
+ const root = rootForSession();
3196
+ if (root === '') return '';
3197
+ const separator = root.indexOf('\\') >= 0 ? '\\' : '/';
3198
+ return root + separator + trimmed.replace(/^[\\/]+/, '');
3199
+ };
3200
+
3489
3201
  /**
3490
3202
  * Resolve a click target to the file it represents. A changed-file chip
3491
3203
  * is both the anchor and the button whose own click opens the product's
@@ -3501,12 +3213,20 @@ body[data-dshome-dark] .${CLS}{background:rgba(14,30,60,.94);box-shadow:0 18px 4
3501
3213
  return path === '' ? null : { path: path, button: chip, produced: true };
3502
3214
  }
3503
3215
  const card = target.closest(CARD);
3504
- if (card === null) return null;
3505
- const overlay = card.querySelector('button[title]');
3506
- if (overlay === null) return null;
3507
- const overlayPath = overlay.getAttribute('title');
3508
- const path = typeof overlayPath === 'string' ? overlayPath.trim() : '';
3509
- return path === '' ? null : { path: path, button: overlay, produced: false };
3216
+ if (card !== null) {
3217
+ const overlay = card.querySelector('button[title]');
3218
+ if (overlay === null) return null;
3219
+ const overlayTitle = overlay.getAttribute('title');
3220
+ const cardPath = absoluteOf(overlayTitle);
3221
+ return cardPath === '' ? null : { path: cardPath, button: overlay, produced: false, previewable: true, replayButton: overlay };
3222
+ }
3223
+ // 行内 `@file` 引用不在这里处理:0.2.0 起交还官方(点击即官方预览器)。
3224
+ // Tool-row file link. Its text is relative to the session's workspace root,
3225
+ // and the product's workspace service does not hand that root over to a
3226
+ // plugin, so there is nothing trustworthy to turn it into a path here. The
3227
+ // product's own click already opens the right file, so this surface is left
3228
+ // entirely alone rather than given a menu whose path items would be wrong.
3229
+ return null;
3510
3230
  };
3511
3231
 
3512
3232
  /**
@@ -3518,7 +3238,8 @@ body[data-dshome-dark] .${CLS}{background:rgba(14,30,60,.94);box-shadow:0 18px 4
3518
3238
  const preview = (surface) => {
3519
3239
  replaying = true;
3520
3240
  try {
3521
- surface.button.click();
3241
+ const target = surface.replayButton === undefined || surface.replayButton === null ? surface.button : surface.replayButton;
3242
+ target.click();
3522
3243
  } finally {
3523
3244
  replaying = false;
3524
3245
  }
@@ -3553,9 +3274,50 @@ body[data-dshome-dark] .${CLS}{background:rgba(14,30,60,.94);box-shadow:0 18px 4
3553
3274
  }
3554
3275
  return Promise.resolve(clipboard.writeText(path)).then(() => true).catch(() => false);
3555
3276
  };
3556
- const invoke = (endpoint, path) => Promise.resolve(bridgeCall(endpoint, { path: path }))
3557
- .then((result) => result !== undefined && result !== null && result.ok === true)
3558
- .catch(() => false);
3277
+ /**
3278
+ * 打开文件 / 在文件管理器中显示,**优先走官方远程**。
3279
+ *
3280
+ * 0.2.0 的 dsh-api-session-controller 提供了这条通路(从宿主端的类型定义里抄下来的形状):
3281
+ * session.canOpenWorkspacePath() → boolean
3282
+ * session.openWorkspacePath({ path }) → 用默认应用打开
3283
+ * session.openWorkspacePath({ path, action: 'reveal' }) → 在文件管理器中显示
3284
+ * session.openWorkspacePath({ path, application }) → 用指定应用打开
3285
+ * 返回 { opened: true }。宿主侧会自己校验路径、解析工作区映射,
3286
+ * 拿不到已验证的映射时抛 RemoteError —— 比我们自己拼路径可靠。
3287
+ *
3288
+ * 我们自己的私有通道(dshome/file.open、dshome/file.reveal)只在官方这条不存在时兜底:
3289
+ * 旧内核没有这个远程。兜底是有代价的——那是本插件自己的实现,产品换做法时它不会跟着变,
3290
+ * 所以它永远排在官方后面,而不是并列。
3291
+ *
3292
+ * 返回 null 表示"官方这条不可用",交给调用方走兜底。
3293
+ */
3294
+ const officialOpen = (path, reveal) => {
3295
+ let session;
3296
+ try {
3297
+ const connection = ctx.get('connection');
3298
+ session = connection === undefined || connection === null ? undefined : connection.api?.session;
3299
+ } catch (error) {
3300
+ session = undefined;
3301
+ }
3302
+ if (session === undefined || session === null || typeof session.openWorkspacePath !== 'function') return null;
3303
+ let call;
3304
+ try {
3305
+ call = session.openWorkspacePath(reveal ? { path: path, action: 'reveal' } : { path: path });
3306
+ } catch (error) {
3307
+ // 同步抛出(参数被拒、通道没接上)——当成"官方这条不可用",别把异常丢给用户。
3308
+ return null;
3309
+ }
3310
+ return Promise.resolve(call)
3311
+ .then((result) => result !== undefined && result !== null && result.opened === true)
3312
+ .catch(() => false);
3313
+ };
3314
+ const invoke = (endpoint, path, reveal) => {
3315
+ const official = officialOpen(path, reveal === true);
3316
+ if (official !== null) return official;
3317
+ return Promise.resolve(bridgeCall(endpoint, { path: path }))
3318
+ .then((result) => result !== undefined && result !== null && result.ok === true)
3319
+ .catch(() => false);
3320
+ };
3559
3321
 
3560
3322
  const buildItem = (icon, label, onPick) => {
3561
3323
  const item = document.createElement('button');
@@ -3586,21 +3348,35 @@ body[data-dshome-dark] .${CLS}{background:rgba(14,30,60,.94);box-shadow:0 18px 4
3586
3348
  const node = document.createElement('div');
3587
3349
  node.className = CLS;
3588
3350
  node.setAttribute('role', 'menu');
3589
- node.appendChild(buildItem(ICON_OPEN, text.open, () => {
3590
- invoke('dshome/file.open', path).then((ok) => { if (!ok) toast(text.failed); });
3591
- }));
3592
- node.appendChild(buildItem(ICON_REVEAL, text.reveal, () => {
3593
- invoke('dshome/file.reveal', path).then((ok) => { if (!ok) toast(text.failed); });
3594
- }));
3595
- node.appendChild(buildItem(ICON_COPY, text.copy, () => {
3596
- copyPath(path).then((ok) => toast(ok ? text.copied : text.copyDenied));
3597
- }));
3598
- const separator = document.createElement('div');
3599
- separator.className = CLS + '-sep';
3600
- node.appendChild(separator);
3601
- node.appendChild(buildItem(ICON_PREVIEW, surface.produced ? text.previewProduced : text.preview, () => {
3602
- preview(surface);
3351
+ // 鎵撳紑鏂囦欢 first; when the surface owns that action (the product's viewer)
3352
+ // the item IS that action, replayed for real instead of a second route.
3353
+ const opensItself = surface.replayButton !== undefined && surface.replayButton !== null;
3354
+ node.appendChild(buildItem(ICON_OPEN, opensItself ? text.openOfficial : text.open, () => {
3355
+ if (opensItself) {
3356
+ preview(surface);
3357
+ return;
3358
+ }
3359
+ invoke('dshome/file.open', path, false).then((ok) => { if (!ok) toast(text.failed); });
3603
3360
  }));
3361
+ // The two path items need a real path. A surface the product only labels
3362
+ // with relative text keeps the viewer item alone rather than offering an
3363
+ // action that would point at the wrong file.
3364
+ if (path !== '') {
3365
+ node.appendChild(buildItem(ICON_REVEAL, text.reveal, () => {
3366
+ invoke('dshome/file.reveal', path, true).then((ok) => { if (!ok) toast(text.failed); });
3367
+ }));
3368
+ node.appendChild(buildItem(ICON_COPY, text.copy, () => {
3369
+ copyPath(path).then((ok) => toast(ok ? text.copied : text.copyDenied));
3370
+ }));
3371
+ }
3372
+ if (surface.previewable === true) {
3373
+ const separator = document.createElement('div');
3374
+ separator.className = CLS + '-sep';
3375
+ node.appendChild(separator);
3376
+ node.appendChild(buildItem(ICON_PREVIEW, surface.produced ? text.previewProduced : text.preview, () => {
3377
+ preview(surface);
3378
+ }));
3379
+ }
3604
3380
  document.body.appendChild(node);
3605
3381
  // Clamp to the viewport so a card near an edge never opens a menu
3606
3382
  // the user cannot reach.
@@ -3627,8 +3403,14 @@ body[data-dshome-dark] .${CLS}{background:rgba(14,30,60,.94);box-shadow:0 18px 4
3627
3403
  const onClickCapture = (event) => {
3628
3404
  if (replaying || event.defaultPrevented) return;
3629
3405
  if (event.button !== undefined && event.button !== 0) return;
3406
+ // 左键只在「本轮文件改动」的 chip 上接管:那类元素自己没有任何可见动作
3407
+ // (点下去像没反应),所以左键直接开菜单是净收益。交付卡片有自己的左键
3408
+ // 预览,保持原样、只在右键开菜单。行内 @file 引用已交还官方。
3409
+ if (event.detail > 1) return;
3410
+ const selection = window.getSelection();
3411
+ if (selection !== null && selection.isCollapsed === false) return;
3630
3412
  const surface = surfaceOf(event.target);
3631
- if (surface === null || !surface.produced) return;
3413
+ if (surface === null || surface.produced !== true) return;
3632
3414
  event.preventDefault();
3633
3415
  event.stopPropagation();
3634
3416
  openMenu(surface, event.clientX, event.clientY);
@@ -3845,6 +3627,78 @@ body[data-dshome-dark] .${CLS}{background:rgba(14,30,60,.94);box-shadow:0 18px 4
3845
3627
  panel.appendChild(listEl);
3846
3628
  document.body.appendChild(panel);
3847
3629
 
3630
+ // 拖动(0.2.0 新增需求):默认仍锚在右下角,一旦拖动就切换到 left/top
3631
+ // 定位并把位置记进 localStorage,下次打开还在原地。用 Pointer Events +
3632
+ // setPointerCapture,拖出面板外也不会丢事件;双击标题栏复位到默认角落。
3633
+ const PANEL_POS_KEY = 'dshome.subagent.panel.position';
3634
+ const clampToViewport = (left, top) => {
3635
+ const box = panel.getBoundingClientRect();
3636
+ const maxLeft = Math.max(8, window.innerWidth - box.width - 8);
3637
+ const maxTop = Math.max(8, window.innerHeight - box.height - 8);
3638
+ return { left: Math.min(Math.max(8, left), maxLeft), top: Math.min(Math.max(8, top), maxTop) };
3639
+ };
3640
+ const placePanel = (left, top) => {
3641
+ const at = clampToViewport(left, top);
3642
+ panel.style.left = at.left + 'px';
3643
+ panel.style.top = at.top + 'px';
3644
+ panel.style.right = 'auto';
3645
+ panel.style.bottom = 'auto';
3646
+ };
3647
+ const savedPosition = () => {
3648
+ try {
3649
+ const raw = window.localStorage.getItem(PANEL_POS_KEY);
3650
+ if (raw === null) return null;
3651
+ const parsed = JSON.parse(raw);
3652
+ return typeof parsed?.left === 'number' && typeof parsed?.top === 'number' ? parsed : null;
3653
+ } catch (error) { return null; }
3654
+ };
3655
+ const persistPosition = () => {
3656
+ try {
3657
+ const box = panel.getBoundingClientRect();
3658
+ window.localStorage.setItem(PANEL_POS_KEY, JSON.stringify({ left: Math.round(box.left), top: Math.round(box.top) }));
3659
+ } catch (error) { /* 存不下就算了,不影响本次会话 */ }
3660
+ };
3661
+ head.classList.add('dshome-sa-grab');
3662
+ head.addEventListener('pointerdown', (event) => {
3663
+ if (event.button !== undefined && event.button !== 0) return;
3664
+ // 标题栏上的折叠按钮不参与拖动。
3665
+ if (event.target instanceof Node && collapseBtn.contains(event.target)) return;
3666
+ const box = panel.getBoundingClientRect();
3667
+ const offsetX = event.clientX - box.left;
3668
+ const offsetY = event.clientY - box.top;
3669
+ head.setPointerCapture(event.pointerId);
3670
+ panel.classList.add('dshome-sa-dragging');
3671
+ event.preventDefault();
3672
+ const move = (moveEvent) => placePanel(moveEvent.clientX - offsetX, moveEvent.clientY - offsetY);
3673
+ const up = () => {
3674
+ head.removeEventListener('pointermove', move);
3675
+ head.removeEventListener('pointerup', up);
3676
+ head.removeEventListener('pointercancel', up);
3677
+ panel.classList.remove('dshome-sa-dragging');
3678
+ try { head.releasePointerCapture(event.pointerId); } catch (error) { /* 已释放 */ }
3679
+ persistPosition();
3680
+ };
3681
+ head.addEventListener('pointermove', move);
3682
+ head.addEventListener('pointerup', up);
3683
+ head.addEventListener('pointercancel', up);
3684
+ });
3685
+ head.addEventListener('dblclick', (event) => {
3686
+ if (event.target instanceof Node && collapseBtn.contains(event.target)) return;
3687
+ panel.style.left = '';
3688
+ panel.style.top = '';
3689
+ panel.style.right = '';
3690
+ panel.style.bottom = '';
3691
+ try { window.localStorage.removeItem(PANEL_POS_KEY); } catch (error) { /* 忽略 */ }
3692
+ });
3693
+ window.addEventListener('resize', () => {
3694
+ const saved = savedPosition();
3695
+ if (saved !== null) placePanel(saved.left, saved.top);
3696
+ });
3697
+ {
3698
+ const saved = savedPosition();
3699
+ if (saved !== null) placePanel(saved.left, saved.top);
3700
+ }
3701
+
3848
3702
  const timingOf = (s) => (
3849
3703
  s.projectionValues === undefined || s.projectionValues === null
3850
3704
  ? undefined
@@ -4058,6 +3912,25 @@ body[data-dshome-dark] .${CLS}{background:rgba(14,30,60,.94);box-shadow:0 18px 4
4058
3912
  row.sweepEl.style.width = '0%';
4059
3913
  };
4060
3914
 
3915
+ /**
3916
+ * 中断失败要看得见。旧版失败时只是把乐观状态退回去,屏幕上什么都没留下,
3917
+ * 用户看到的就是"按了没用"。这里除了退回状态,还把原因写到控制台与按钮的
3918
+ * 悬停提示上,并尽量写进这一行的说明文字(下一秒会被计时刷新覆盖,属于尽力而为)。
3919
+ */
3920
+ const reportStopFailure = (row, btn, error) => {
3921
+ rollbackStop(row, btn);
3922
+ const text = error === undefined || error === null
3923
+ ? '主机没有接受这次中断'
3924
+ : (typeof error === 'string' ? error : (error.message === undefined ? String(error) : error.message));
3925
+ try { console.warn('[dsh-deepseek-style-theme] 中断子代理失败:', error); } catch (ignored) { /* 忽略 */ }
3926
+ if (btn !== undefined && btn !== null) {
3927
+ try { btn.title = '中断失败:' + text; } catch (ignored) { /* 忽略 */ }
3928
+ }
3929
+ if (row !== undefined && row.metaEl !== undefined && row.metaEl !== null) {
3930
+ row.metaEl.textContent = '中断失败:' + text;
3931
+ }
3932
+ };
3933
+
4061
3934
  const beginCompleting = (row) => {
4062
3935
  row.phase = 'completing';
4063
3936
  row.completeStart = performance.now();
@@ -4066,17 +3939,30 @@ body[data-dshome-dark] .${CLS}{background:rgba(14,30,60,.94);box-shadow:0 18px 4
4066
3939
  };
4067
3940
 
4068
3941
  /**
4069
- * Durable parent/child address for the subagents.interrupt RPC.
4070
- * The mode comes from the child's own identity projection; a parent id
4071
- * is required — generic host routing rejects subagent-owned sessions,
4072
- * so the interrupt must ride the address, not sessions.cancel.
3942
+ * 子代理的 parent/child 地址。
3943
+ *
3944
+ * 0.2.0 起**不要再自己拼**:会话摘要里已经没有 `projectionValues.subagent`
3945
+ * 与 `parentId`(这正是旧版红按钮失灵的根因——地址取不到,
3946
+ * canInterrupt 为假,按钮只闪一下红就结束)。官方把这件事收进了
3947
+ * `ctx.sessions.subagentAddress(childSessionId)`,它内部会在各父级的
3948
+ * `subagentCatalog` 里查这条子会话,拿到权威的 { parentSessionId,
3949
+ * childSessionId, mode }。这里优先用它,其余为旧版兜底。
4073
3950
  */
4074
3951
  const addressOf = (s) => {
3952
+ try {
3953
+ if (typeof sessions.subagentAddress === 'function') {
3954
+ const address = sessions.subagentAddress(s.id);
3955
+ if (address !== undefined && address !== null && typeof address.parentSessionId === 'string') return address;
3956
+ }
3957
+ } catch (error) { /* 落回下面的兜底 */ }
3958
+ const parentId = typeof s.parentId === 'string' && s.parentId !== ''
3959
+ ? s.parentId
3960
+ : (typeof s.parentSessionId === 'string' && s.parentSessionId !== '' ? s.parentSessionId : '');
3961
+ if (parentId === '') return undefined;
4075
3962
  const identity = s.projectionValues === undefined || s.projectionValues === null ? undefined : s.projectionValues.subagent;
4076
3963
  let mode = 'continuable';
4077
3964
  if (identity !== undefined && identity !== null && (identity.mode === 'one-shot' || identity.mode === 'continuable')) mode = identity.mode;
4078
- if (typeof s.parentId !== 'string' || s.parentId === '') return undefined;
4079
- return { parentSessionId: s.parentId, childSessionId: s.id, mode };
3965
+ return { parentSessionId: parentId, childSessionId: s.id, mode };
4080
3966
  };
4081
3967
 
4082
3968
  const stopSubagent = (s, btn, event) => {
@@ -4086,18 +3972,31 @@ body[data-dshome-dark] .${CLS}{background:rgba(14,30,60,.94);box-shadow:0 18px 4
4086
3972
  }
4087
3973
  const row = rows.get(s.id);
4088
3974
  if (row !== undefined && row.phase !== 'running') return;
4089
- let face;
4090
- try {
4091
- const binding = sessions.binding(s.id);
4092
- face = binding === undefined || binding === null ? undefined : binding.session;
4093
- } catch (error) {
4094
- face = undefined;
4095
- }
4096
3975
  const address = addressOf(s);
4097
3976
  const api = connection === undefined || connection === null ? undefined : connection.api;
4098
- const canInterrupt = address !== undefined && address.mode === 'continuable' && api !== undefined && api !== null && api.subagents !== undefined && api.subagents !== null && typeof api.subagents.interrupt === 'function';
4099
- const canFaceCancel = face !== undefined && face !== null && typeof face.cancel === 'function';
4100
- if (!canInterrupt && !canFaceCancel) {
3977
+ const subagents = api === undefined || api === null ? undefined : api.subagents;
3978
+ const sessionApi = api === undefined || api === null ? undefined : api.session;
3979
+ // 照官方会话取消的写法(dsh-client-ui-chat 的 cancel):
3980
+ // 有子代理地址 -> subagents.interruptByParent(child, parent, "continuable")
3981
+ // 没有地址 -> session.cancel({ sessionId })
3982
+ // 关键在第三个数是**写死的 "continuable"**,不是这条子代理自己的 mode。
3983
+ // 旧版要求 address.mode === 'continuable' 才肯调用,于是 one-shot 子代理
3984
+ // 永远落进"没权限"分支——按下去只闪一下红,表现就是"点了没用"。
3985
+ const byParent = address !== undefined && subagents !== undefined && subagents !== null
3986
+ && typeof subagents.interruptByParent === 'function';
3987
+ const bySession = !byParent && typeof s.id === 'string' && s.id !== ''
3988
+ && sessionApi !== undefined && sessionApi !== null && typeof sessionApi.cancel === 'function';
3989
+ let face;
3990
+ if (!byParent && !bySession) {
3991
+ try {
3992
+ const binding = sessions.binding(s.id);
3993
+ face = binding === undefined || binding === null ? undefined : binding.session;
3994
+ } catch (error) {
3995
+ face = undefined;
3996
+ }
3997
+ }
3998
+ const byFace = !byParent && !bySession && face !== undefined && face !== null && typeof face.cancel === 'function';
3999
+ if (!byParent && !bySession && !byFace) {
4101
4000
  flashDenied(btn);
4102
4001
  return;
4103
4002
  }
@@ -4107,18 +4006,20 @@ body[data-dshome-dark] .${CLS}{background:rgba(14,30,60,.94);box-shadow:0 18px 4
4107
4006
  if (row !== undefined) startStopProgress(row);
4108
4007
  let cancelPromise;
4109
4008
  try {
4110
- cancelPromise = canInterrupt
4111
- ? Promise.resolve(api.subagents.interrupt(address))
4112
- : Promise.resolve(face.cancel());
4009
+ cancelPromise = byParent
4010
+ ? Promise.resolve(subagents.interruptByParent(address.childSessionId, address.parentSessionId, 'continuable'))
4011
+ : bySession
4012
+ ? Promise.resolve(sessionApi.cancel({ sessionId: s.id }))
4013
+ : Promise.resolve(face.cancel());
4113
4014
  } catch (error) {
4114
- rollbackStop(row, btn);
4015
+ reportStopFailure(row, btn, error);
4115
4016
  return;
4116
4017
  }
4117
4018
  cancelPromise.then((result) => {
4118
4019
  const ok = result !== undefined && result !== null && result.ok === true;
4119
- if (!ok) rollbackStop(row, btn);
4120
- }).catch(() => {
4121
- rollbackStop(row, btn);
4020
+ if (!ok) reportStopFailure(row, btn, result === undefined || result === null ? undefined : result.error);
4021
+ }).catch((error) => {
4022
+ reportStopFailure(row, btn, error);
4122
4023
  });
4123
4024
  };
4124
4025
 
@@ -4319,10 +4220,11 @@ body[data-dshome-dark] .${CLS}{background:rgba(14,30,60,.94);box-shadow:0 18px 4
4319
4220
  // set they asked to keep. Ids match the host schema's `backgroundMode`
4320
4221
  // enum; the free-text value for 方式3 is `customBackground`.
4321
4222
  const DSTT_BACKGROUNDS = ["classic", "white", "custom", "bold"];
4322
- // `bold` by default: ① classic averages (soft lead + white + near-white) into a
4323
- // wash the eye reads as blue-white whatever the colour token says, so it is a
4324
- // poor first impression -- it stays selectable for anyone who wants it.
4325
- const DSTT_BACKGROUND_DEFAULT = "bold";
4223
+ // `classic` by default: soft lead + pure white + near-white, one colour
4224
+ // family for the buttons and the surfaces alike and no second hue -- the look the
4225
+ // product owner asked to ship. `bold` (the 1.43.12 three-chroma set) stays
4226
+ // selectable in the DSTT tab for anyone who wants it back.
4227
+ const DSTT_BACKGROUND_DEFAULT = "classic";
4326
4228
  const DSTT_BACKGROUND_LABELS = {
4327
4229
  classic: { zh: "① 主色+纯白+近白", en: "1 · Lead + white + near-white" },
4328
4230
  white: { zh: "② 主色+纯白", en: "2 · Lead + white" },
@@ -5153,6 +5055,45 @@ body[data-dshome-dark] .${CLS}{background:rgba(14,30,60,.94);box-shadow:0 18px 4
5153
5055
  // every rule inside is gated on html[data-dshome-glass="liquid"], so the
5154
5056
  // frosted pane keeps rendering exactly what it rendered before.
5155
5057
  ctx.effect(() => injectStyle(LIQUID_CSS, 'liquid-glass.css'), 'deepseek-style-theme: apple liquid glass');
5058
+ // Popup menus are portaled out of every pane we glass, so they need their own
5059
+ // sheet; see MENU_GLASS_CSS for why it decorates rather than repaints.
5060
+ ctx.effect(() => injectStyle(MENU_GLASS_CSS, 'menu-glass.css'), 'deepseek-style-theme: menu glass');
5061
+
5062
+ // 更新之后"哪条选择器失效了"的自检入口。
5063
+ // 产品每次构建都会重排类哈希,失效是安静的:规则还在、只是永不命中,
5064
+ // 肉眼看就是"某个面的样式没了"。这个函数把每个锚点当天的命中数摆出来,
5065
+ // 于是"静默失效"变成"一眼能看见":控制台里 __dshomeAnchors()。
5066
+ // 锚点表直接从 PATCH_BLOCKS 推导,不另外维护一份,免得两边走散。
5067
+ // 说明:它只查"产品里还有没有这个锚点",不查"样式有没有真的生效"——
5068
+ // 命中数非零不等于样式正确,命中数为 0 则一定没生效。
5069
+ try {
5070
+ window.__dshomeAnchors = () => {
5071
+ const report = {};
5072
+ const probe = (label, selector) => {
5073
+ try {
5074
+ report[label] = document.querySelectorAll(selector).length;
5075
+ } catch (error) {
5076
+ report[label] = '选择器无效';
5077
+ }
5078
+ };
5079
+ for (const block of PATCH_BLOCKS) {
5080
+ for (const part of block.anchor.split(',')) {
5081
+ const selector = part.trim();
5082
+ if (selector !== '') probe(selector, selector);
5083
+ }
5084
+ }
5085
+ probe('用户气泡', USER_BUBBLE);
5086
+ probe('侧栏外壳', SIDEBAR);
5087
+ probe('输入卡卡片', '[data-composer-card]');
5088
+ probe('滚动区', '[data-conversation-scroll]');
5089
+ probe('菜单材质', '[data-menu-material]');
5090
+ probe('路径标签', '[data-path-label]');
5091
+ probe('流体画布', 'canvas[data-dsh-deepseek-canvas]');
5092
+ const dead = Object.keys(report).filter((k) => report[k] === 0);
5093
+ return { 命中数: report, 零命中的锚点: dead };
5094
+ };
5095
+ window.__dshomeBuild = '2.1.91 adapt-0.2.0-rc.2';
5096
+ } catch (error) { /* 页面拒绝写 window 时不影响主题 */ }
5156
5097
 
5157
5098
  if (theme !== undefined) {
5158
5099
  const disposeTokens = theme.overrideTokens('deepseek-style-theme', {
@@ -5168,7 +5109,20 @@ body[data-dshome-dark] .${CLS}{background:rgba(14,30,60,.94);box-shadow:0 18px 4
5168
5109
  '--dsw-alias-state-error-primary': { light: '#ef4444', dark: '#ff6b6b' },
5169
5110
  '--dsw-alias-state-success-primary': { light: '#22c55e', dark: '#4ed17e' },
5170
5111
  '--dsw-alias-state-warn-primary': { light: '#f59e0b', dark: '#f7ad31' },
5171
- '--dsw-specific-sidebar-fill': { light: 'rgba(255,255,255,0.5)', dark: 'rgba(10,26,58,0.48)' }
5112
+ // Windows 标题栏右上角那三个按钮(最小化/最大化/关闭)的底色就是这一条:
5113
+ // 桌面外壳在渲染进程里放了一个 span,它的 background-color 写成
5114
+ // var(--dsw-specific-sidebar-fill),每帧量出来发给主进程
5115
+ // setTitleBarOverlay 当窗口控件条的颜色(图标色取 --dsw-alias-label-primary)。
5116
+ // 所以这三个按钮我们改不了它们本身,但**能改这一条 token**。
5117
+ // 原来这里是 rgba(255,255,255,0.5):半透明白压在窗口的亚克力底上,
5118
+ // 量出来是 #f9fafe;而旁边的标题栏是半透明底压在流动背景上,量出来 #eaeffa——
5119
+ // 两边差出一圈,就是用户看到的「按钮和标题栏有界线」。
5120
+ // 改成**不透明**的浅蓝(照着旁边 #f1f4fe 取),两边同色,界线消失。
5121
+ // 注意必须是不透明:半透明的话窗口底和页面底不同,又会分家;
5122
+ // 也必须是普通颜色,不能写 color-mix —— 外壳用 canvas fillStyle 解析它,
5123
+ // canvas 不认 color-mix,会静默退回上一个值。
5124
+ // 代价:标题栏最上面那条 40px 不再透出流动背景,变成一块平色。
5125
+ '--dsw-specific-sidebar-fill': { light: '#eef2fd', dark: '#0b1730' }
5172
5126
  });
5173
5127
  ctx.effect(() => disposeTokens);
5174
5128
  }
@@ -5244,11 +5198,7 @@ body[data-dshome-dark] .${CLS}{background:rgba(14,30,60,.94);box-shadow:0 18px 4
5244
5198
  try { return watchBrandLink(); } catch (error) { return () => {}; }
5245
5199
  });
5246
5200
  ctx.effect(() => {
5247
- try { return watchHeaderInteraction(); } catch (error) { return () => {}; }
5248
5201
  });
5249
- ctx.effect(() => {
5250
- try { return watchWorkspaceMenu(ctx); } catch (error) { return () => {}; }
5251
- }, 'deepseek-style-theme: workspace open menu');
5252
5202
  ctx.effect(() => {
5253
5203
  try { return watchFileCardMenu(ctx); } catch (error) { return () => {}; }
5254
5204
  }, 'deepseek-style-theme: delivered file card menu');
@@ -5307,3 +5257,4 @@ try {
5307
5257
  try {
5308
5258
  window.__ModuleLoader__.load({ id: "@dsh-external/dsh-deepseek-style-theme", factory: dshDeepseekStyleThemeFactory });
5309
5259
  } catch (error) {}
5260
+ })();